STM32 - 1-Wire protocol analysis & Implementing of OneWire Protocol using UART peripheral and DMA

TL;DR

If you just want to use 1-wire based device and just don’t want to know any technical detail, jump to the last part.

Reason to use DS18B20

I’m trying to add some temperature sensor to my STM32-based computer water-cooling controller.

You know, a common solution to this is to use some thermistor and try to use some ADC(Analog-Digital-Converter) to capture the voltage on that. The temperature captured by thermistor won’t be too accurate, just around 1 degree or so. If you need a temperature with accuracy of 0.0625 degree, the DS18B20 would be a good choice.

But DS18B20 has an really time sensitive 1-wire protocol. A normal solution to this problem is to use GPIO and NOP() instruction to simulate this, but I tend to use something different.

1-wire protocol

  1. Overview

    1-wire or OneWire is a protocol with single-master and multi-slave, under most situation, we would just use one device on the bus.

  2. Analysis on 1-wire protocol

    1. Overview on 1-wire protocol

      No matter whether it’s a read procedure or a write procedure, the process of 1-wire follow a similar pattern:

      READ PATTERN: Start -> Write a ROM command -> (Optional Read ROM Data) -> Write a READ function command-> READ DATA -> End

      WRITE PATTERN: Start -> Write a ROM command -> (Optional Read ROM Data) -> Write a WRITE function command-> WRITE DATA -> End

      or in a more general version:

      DATA EXCHANGE: Start -> Write a ROM function -> (Optional Read ROM Data) -> Write a EXCHANGE function -> EXCHANGE DATA -> End

    2. Analysis about the ROM function

      Just like all the other communication protocol with multiple slave, the master need to know something about the slave devices to tell the difference between all these devices, at the same time, the slaves also need to has some information to know if one communication on the bus is oriented to itself - check the figure below.

      address demonstration

      For a typical 1-wire device, it has an 8 bytes(64-bit) address. The first byte is assigned for family code, while the following 6 bytes is a unique serial number, another 1 byte for CRC code of the first 7 bytes. If the master want to read the serial number from device, it could use the CRC to verify if the communication is right or not.

      According to the above, we could know that the 6bytes in the medium could be used to distinguish devices - do a simple math $2^{48}=281,474,976,710,656$, this is more than enough for the distinguish problem.

      ROM code ROM command
      0x33 READ ROM
      0x55 MATCH ROM
      0xf0 SEARCH ROM
      0xec ALARM SEARCH ROM
      0xcc SKIP ROM

      In order to play with these ROMs, there are multiple ROM commands. The following figure indicate their behavior in a simplified way.

      ROM Function Flow Chart

      Such overkill solution bring a problem when solving a problem, that is, when you get a 1-wire device, you won’t be able to know its serial address, you may need to use a complicate method described in the specification to read all unknown serial numbers out, check the Book of iButton chapter 5.II.C.3 and Application Note 187 to know more detail about that.

      But in most implementation, programmer won’t know the serial number in advance, they also won’t try to implement such ROM SEARCH function. Instead, they just assume there is only device on the bus, using external multiplexer to add support to multi-device.

      Almost any kind of multiplexer would work, I had verified that 74HC4052and 74HC4051 could work perfectly. If you need more, I think CPLD/FPGA might also work. The figure above illustrate a brief of the whole idea.

      When using the solution above, we don’t have to handle the ROM issue, under most problem, the 0xCC(SKIP ROM) is used most frequently.

    3. Analysis on DS18B20 specific function

      Since there are only 7 internal register (3 reserved) in DS18B20, so the control flow is quite simple, just follow the pattern I mentioned in section 2.1.

      Here is a brief demonstration of each function command:

      Convert T(0x44): start the temperature convert and save the temp to the BYTE0 and BYTE1.

      Read Scratchpad(0xbe): read all data from scratchpad(all 9 byte but you could sample just the bytes you need.)

      Write Scratchpad(0x4e):write 3 byte to BYTE2, BYTE3 and BYTE4. (all 3 byte is needed) configure data in scratchpad would lost after power failed.

      Copy Scratchpad(0x48): copy BYTE2, BYTE3 and BYTE4 to EERAM.(unlike scratchpad, EERAM would keep after power fails)

      Recall E^2(0xb8): copy data stored in EERAM back to BYTE2-BYTE4.

      Read Power Supply(0xb4): return 1 if the device is on an independent ac power, return 0 if the device is using the DQ power.

  3. Signal character

    Watch the whole communication process from the master side, the write process acquire the master to do the following thing:

    1. Write a bit

      transmit a low level signal for at least 1us, then transmit the corresponding signal, the whole transmit time should takes longer than 60us. Check the following image,

      In the left part, the master is sending a zero bit, it should send a low level for more than 1 us, then keep the level at low for at least another (60-1) us. After writing zero bit, the master should release the data line, otherwise if the data line keep at low level for more than 480us, then the device would start another initializing process.

      In the right part, the process is almost the same to send a one bit, but actually the device don’t have to release the data lane, since the data line is kept at high when the line is leased.

      The gray square in the image give a typical operating time, in the first 15us, the master should keep the data line at low level for at least 1us, and then keep it to the corresponding level for at least 15+30 us (which is the sampling time slot for ds18b20 device). After sampling, the master need to release data line.

    2. Read a bit

      The following image demonstrate how to read from the slave device, which is quite similar to the process above.

      Before the master decide to read a bit from the slave device, it should send a low level signal for at least 1us, then release the data line for some time and sample the signal level on the bus. The whole reading process of reading a bit is at least 60us, after that the slave device would release the data line and make preparation for next bit.

  4. Using UART+DMA to implement the 1-Wire Protocol

    The traditional way to implement the 1-wire protocol is using GPIO and while(x) to simulate. However, using such way to simulate would waste many time cycle and the behavior may varies when the compiler changed.

    Connect the device to the MCU like following:

    1. principle behind simulating 1-wire using UART - WRITE

      The UART TTL is low-level for a start bit and a high-level for idle, just same as 1-wire.

      If configured with 1 start bit (low-level), 8 data bit, 1 stop bit, then the UART would behave similar to the scale pattern of 1 bit of 1-wire.

      When 1-wire protocol need to write bit, the whole time would be 60-120us, the device would sample at the time slot in 15-60us. If we set the baud rate of UART to 115200 baud per second, then every bit would occupy a time slot of 1000000/115200=8.6us (enough to make the start bit a start signal in 1-wire, and small enough to avoid interfere the sampling slot), then 1byte data would make a 8.6*9=78us slot - just within the range of 1-wire protocol.

      In a word, if you send a byte 0x00 under UART 115200-8-N-1, the 1-wire device would take that as an 1 bit; if you send a byte 0xff under UART 115200-8-N-1, the 1-wire device would take it as a 0 bit.

    2. principle behind simulating 1-wire using UART - READ

      The pattern is similar to above.

      First, the UART TTL under 115200-8-N-1 send a 0xff, if the UART read a 0xff, then the bit should be a 1, other wise, it should be a 0 bit.

    3. principle behind simulating 1-wire using UART - RESET

      Since the reset on 1-wire protocol require a much longer time, UART of 115200 baud rate can’t meet the requirement. We need to use the 9600-8-N-1 UART to send a 0xf0. Since the UART would send LSB first, the data line would send 5 continuous 0 bit (which is 1000000/9600*5=520us), enough to reset the 1-wire bus. If the UART read a 0xf0, then there is no device on the line, otherwise, the bus is reset as expected.

Except from the reset operation, the rest two operation need to operate 8 continuous byte, which means the user’s code need to respond to the UART interrupt consequently, also means complicate code and keeps interrupting the embedded OS and other processing.

So it’s best to use DMA to avoid such problem, template code is attached at the end.

  1. The code you need is here

    Here I will show you how to use my code, if you still have problem, feel free to comment below:

    1. Generate the DMA and U(S)ART initialize code with CubeMX.

    2. Modify the macro USART2 in ds18b20.c to any UART or USART peripheral you like.

    3. Add #include "ds18b20.h"between /* USER CODE BEGIN 0 */ and /* USER CODE END 0 */ in the usart.c . Add the following block between /* USER CODE BEGIN 1 */ and /* USER CODE END 1 */ to make sure the library would receive the complete callback of UART.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      void HAL_UART_TxCpltCallback(UART_HandleTypeDef *uarth){
      if (uarth->Instance == USART2){
      OneWire_TxCpltCallback();
      }
      }
      void HAL_UART_RxCpltCallback(UART_HandleTypeDef *uarth){
      if (uarth->Instance == USART2){
      OneWire_RxCpltCallback();
      }
      }
    4. In your main.c, use OneWire_Init() to initialize the whole library and OneWire_SetCallback() to register your own callback. The usage of the other function is listed below:

      1
      2
      3
      4
      5
      6
      7
      OneWire_Execute(0x33,&(ROMBuffer[0]),*,*); -> start to read rom
      OneWire_Execute(0xcc,0,*,*); -> skip rom phase
      OneWire_Execute(0xcc,0,0x44,0); -> start to Convert T
      OneWire_Execute(0xcc,0,0xbe,&(FunctionBuffer[0])); -> start to read configuration & result
      OneWire_Execute(0xcc,0,0x4e,&(FunctionBuffer[0])); -> start to write configuration
      OneWire_Execute(0xcc,0,0x48,0); -> copy the Th, Tl & Configuration register to EEPROM
      OneWire_Execute(0xcc,0,0xb8,0); -> recall Th, Tl & Configuration data to scratchpad memory

ds18b20.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef __DS18B20_H__
#define __DS18B20_H__
#include "usart.h"
void OneWire_Init(void);
void OneWire_UARTInit(uint32_t baudRate);
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *uarth);
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *uarth);
void OneWire_Execute(uint8_t ROM_Command,uint8_t* ROM_Buffer,
uint8_t Function_Command,uint8_t* Function_buffer);
void StateMachine(void);
void OneWire_SetCallback(void(*OnComplete)(void), void(*OnErr)(void));
uint8_t ROMStateMachine(void);
uint8_t FunctionStateMachine(void);
void OneWire_TxCpltCallback(void);
void OneWire_RxCpltCallback(void);
#endif

ds18b20.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
#include "ds18b20.h"
#include <string.h>
/*
* USART2(Tx=D5 Rx=D6)
*/
typedef struct {
__IO uint8_t Reset; //Communication Phase 1: Reset
__IO uint8_t ROM_Command; //Communication Phase 2: Rom command
__IO uint8_t Function_Command; //Communication Phase 3: DS18B20 function command
__IO uint8_t *ROM_TxBuffer;
__IO uint8_t *ROM_RxBuffer;
__IO uint8_t ROM_TxCount;
__IO uint8_t ROM_RxCount;
__IO uint8_t *Function_TxBuffer;
__IO uint8_t *Function_RxBuffer;
__IO uint8_t Function_TxCount;
__IO uint8_t Function_RxCount;
__IO uint8_t ROM;
__IO uint8_t Function;
} State;
State state;
uint8_t internal_Buffer[73];
typedef struct {
void(*OnComplete)(void);
void(*OnErr)(void);
}OneWire_Callback;
__IO OneWire_Callback onewire_callback;
void OneWire_SetCallback(void(*OnComplete)(void), void(*OnErr)(void))
{
onewire_callback.OnErr = OnErr;
onewire_callback.OnComplete = OnComplete;
}
void OneWire_Init(){
OneWire_UARTInit(9600);
}
// Declare a USART_HandleTypeDef handle structure.
void OneWire_UARTInit(uint32_t baudRate){
huart2.Instance=USART2;
huart2.Init.BaudRate = baudRate;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
huart2.Init.Mode = UART_MODE_TX_RX;
huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart2.Init.OverSampling = UART_OVERSAMPLING_16;
HAL_UART_Init(&huart2);
return ;
}
void OneWire_TxCpltCallback(){
}
void OneWire_RxCpltCallback(){
StateMachine();
}
/* OneWire_SendBytes & OneWire_ReadBytes */
void StateMachine(){
switch (state.Reset){
case 0: // start the reset produce;
OneWire_UARTInit(9600);
internal_Buffer[0]=0xf0;
HAL_UART_Transmit_DMA(&huart2,&(internal_Buffer[0]),1);
HAL_UART_Receive_DMA(&huart2,&(internal_Buffer[0]),1);
state.Reset++;
break;
case 1: // to check if the device exist or not.
if (internal_Buffer[0]==0xf0)
{
onewire_callback.OnErr();
break;
}
state.Reset++;
case 2:
if (ROMStateMachine()==0)
state.Reset++;
else break;
case 3:
if (FunctionStateMachine()==0)
state.Reset++;
else break;
case 4:
onewire_callback.OnComplete();
break;
}
return ;
}
uint8_t ROMStateMachine(void){
switch(state.ROM){
case 0: // start the ROM command by sending the ROM_Command
OneWire_UARTInit(115200);
for (uint8_t i=0;i<8;i++)
internal_Buffer[i]=((state.ROM_Command>>i)&0x01)?0xff:0x00;
HAL_UART_Transmit_DMA(&huart2,&(internal_Buffer[0]),8);
HAL_UART_Receive_DMA(&huart2,&(internal_Buffer[0]),8);
state.ROM++;
break;
case 1: // continue by sending necessary Tx buffer
if (state.ROM_TxCount!=0){
for (uint8_t i=0;i<state.ROM_TxCount;i++)
for (uint8_t j=0;j<8;j++)
internal_Buffer[i*8+j]=((state.ROM_TxBuffer[i]>>j)&0x01)?0xff:0x00;
HAL_UART_Transmit_DMA(&huart2,&(internal_Buffer[0]),state.ROM_TxCount*8);
HAL_UART_Receive_DMA(&huart2,&(internal_Buffer[0]),state.ROM_TxCount*8);
state.ROM++;
break;
}
if (state.ROM_RxCount!=0){
for (uint8_t i=0;i<=state.ROM_RxCount*8;i++)
internal_Buffer[i]=0xff;
HAL_UART_Transmit_DMA(&huart2,&(internal_Buffer[0]),state.ROM_RxCount*8);
HAL_UART_Receive_DMA(&huart2,&(internal_Buffer[0]),state.ROM_RxCount*8);
state.ROM++;
break;
}
state.ROM++;
case 2:
if (state.ROM_RxCount!=0){
for (uint8_t i=0;i<state.ROM_RxCount;i++)
for (uint8_t j=0;j<8;j++)
state.ROM_RxBuffer[i]=(state.ROM_RxBuffer[i])+(((internal_Buffer[i*8+j]==0xff)?0x01:0x00)<<j);
}
state.ROM=0;
break;
}
return state.ROM;
}
uint8_t FunctionStateMachine(void){
switch(state.Function){
case 0:
OneWire_UARTInit(115200);
for (uint8_t i=0;i<8;i++)
internal_Buffer[i]=((state.Function_Command>>i)&0x01)?0xff:0x00;
HAL_UART_Transmit_DMA(&huart2,&(internal_Buffer[0]),8);
HAL_UART_Receive_DMA(&huart2,&(internal_Buffer[0]),8);
state.Function++;
break;
case 1: // continue by sending necessary Tx buffer
if (state.Function_TxCount!=0){
for (uint8_t i=0;i<state.Function_TxCount;i++)
for (uint8_t j=0;j<8;j++)
internal_Buffer[i*8+j]=((state.Function_TxBuffer[i]>>j)&0x01)?0xff:0x00;
HAL_UART_Transmit_DMA(&huart2,&(internal_Buffer[0]),state.Function_TxCount*8);
HAL_UART_Receive_DMA(&huart2,&(internal_Buffer[0]),state.Function_TxCount*8);
state.Function++;
break;
}
if (state.Function_RxCount!=0){
for (uint8_t i=0;i<=state.Function_RxCount*8;i++)
internal_Buffer[i]=0xff;
HAL_UART_Transmit_DMA(&huart2,&(internal_Buffer[0]),state.Function_RxCount*8);
HAL_UART_Receive_DMA(&huart2,&(internal_Buffer[0]),state.Function_RxCount*8);
state.Function++;
break;
}
state.Function++;
case 2:
if (state.Function_RxCount!=0){
for (uint8_t i=0;i<state.Function_RxCount;i++)
for (uint8_t j=0;j<8;j++)
state.Function_RxBuffer[i]=state.Function_RxBuffer[i]+(((internal_Buffer[i*8+j]==0xff)?0x01:0x00)<<j);
}
state.Function=0;
break;
}
return state.Function;
}
void OneWire_Execute(uint8_t ROM_Command,uint8_t* ROM_Buffer,uint8_t Function_Command,uint8_t* Function_buffer){
memset(&(state),0,sizeof(State));
state.ROM_Command=ROM_Command;
state.Function_Command=Function_Command;
switch (ROM_Command){
case 0x33: // Read ROM
state.ROM_RxBuffer=ROM_Buffer;
state.ROM_RxCount=8; //8 byte
break;
case 0x55: // Match ROM
state.ROM_TxBuffer=ROM_Buffer;
state.ROM_TxCount=8;
break;
case 0xf0: break; // Search ROM it might be too hard to implement you might need to refer to Chapter "C.3. Search ROM Command" in the pdf here:http://pdfserv.maximintegrated.com/en/an/AN937.pdf
case 0xec: break; // Alarm Search it might be too hard to implement refer to http://pdfserv.maximintegrated.com/en/an/AN937.pdf if in need.
case 0xcc: break; // Skip Rom just send the 0xcc only since the code is implement one-slave need.
}
switch (Function_Command){
case 0x44: break; // Convert T need to transmit nothing or we can read a 0 while the temperature is in progress read a 1 while the temperature is done.
case 0x4e: // Write Scratchpad
state.Function_TxBuffer=Function_buffer;
state.Function_TxCount=3;
break;
case 0x48: break; // Copy Scratchpad need to transmit nothing
case 0xbe: // Read Scratchpad
state.Function_RxBuffer=Function_buffer;
state.Function_RxCount=9;
break;
case 0xb8: break; // Recall EEPROM return transmit status to master 0 for in progress and 1 is for done.
case 0xb4: break; // read power supply only work for undetermined power supply status. so don't need to implement it
}
StateMachine();
}