STM32 - 1-Wire protocol analysis & Implementing of OneWire Protocol using UART peripheral and DMA
0 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.
1 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.
2 1-wire protocol
2.0 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.1 Analysis on 1-wire protocol
2.1.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.1.2 Analysis about the ROM function
2.1.2.1 Distinguish-ability
- 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.
-
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 image above, we could know that the 6-bytes 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.
2.1.2.2 ROM command
- Table of ROM command for DS18B20:
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.
-
Such overkill solution bring a new problem when solving a existing 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.
-
Wherever there is a problem, there will always be a hard way to solve it from root cause and a hacky workaround. For 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, just like how the route below does.
-
Almost any kind of multiplexer would work, I had verified that
74HC4052
and74HC4051
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.
2.2 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 theBYTE0
andBYTE1
. -
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 toBYTE2
,BYTE3
andBYTE4
. (all 3 byte is needed) configure data in scratchpad would lost after power failed. -
Copy Scratchpad(0x48)
: copyBYTE2
,BYTE3
andBYTE4
to EERAM.(unlike scratchpad, EERAM would keep after power fails) -
Recall E^2(0xb8)
: copy data stored in EERAM back toBYTE2
-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
When going through the whole communication process from the master side, the write process acquire the master to do the following thing:
3.1 Write a bit
To write a bit, it’s required to:
-
transmit a low voltage for at least 1us,
-
transmit the corresponding signal.
The whole transmit time slot should takes longer than 60us, The following image is a good illustration of the whole process.
In the left half, 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.
3.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,
-
release the data line for some time and sample the signal level on the bus.
The whole reading process of reading a bit should takes longer than 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:
4.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 treat it as a 0 bit.
4.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, otherwise, the bit should be treated as a bit 0.
4.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.
5 The code you need is here
Thanks for the long reading, here I will show you how to use my code and the HOWTO guide of this snippet. If you still have problem, feel free to comment below.
-
Generate the DMA and U(S)ART initialize code with CubeMX.
-
Modify the macro
USART2
inds18b20.c
to any UART or USART peripheral you like. -
Add
#include "ds18b20.h"
between/* USER CODE BEGIN 0 */
and/* USER CODE END 0 */
in theusart.c
. -
Add the following snippet between
/* USER CODE BEGIN 1 */
and/* USER CODE END 1 */
to make sure the library would receive the complete callback of UART.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(); } }
-
In your
main.c
, useOneWire_Init()
to initialize the whole library andOneWire_SetCallback()
to register your own callback. The usage of the other function is listed below: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
-
add the following code as
ds18b20.h
:#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
-
add the following code as
ds18b20.c
:#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(); }