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:

Or in a more general version:

2.1.2 Analysis about the ROM function

2.1.2.1 Distinguish-ability

address demonstration

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

ROM Function Flow Chart

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:

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:

  1. transmit a low voltage for at least 1us,

  2. 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:

  1. send a low level signal for at least 1us,

  2. 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.

  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 .

  4. 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();
    		}
    }
  5. 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:

    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
  6. 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
  7. 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();
    }
comments powered by Disqus