CH32V003F4P6 - DS18B20 Thermometer
In this article, I show you how to communicate with the popular DS18B20 thermometer chip using the 1-wire protocol. This chip-based thermometer has a measurement range from -55°C up to 125°C, and between -10°C to 85°C it provides +/- 0.5°C accuracy. Thanks to its simplicity, it is used in many places. One can buy the bare chip or even the encapsulated version of it which makes the sensor waterproof so it can be used in wet environments.
Communication and initialisation
So, the chip operates with 3 wires: VCC, GND and signal. In fact, it could operate with two wires only, using the parasitic power mode, but I don’t cover that in this article.
Our focus is on the signal (or data) wire. First, and foremost, the data wire requires a 4.7 kOhm pull-up resistor! After adding this pull-up resistor to the circuit, we can focus on configuring the GPIO pin based on requirements.
We will recycle what we already learned in the first video of this series regarding initializing a GPIO pin. I want to use this chip on PD2, so I assigned it as GPIO_Pin_2. I set the speed to 50 MHz, but slower will do just fine.
Then, as one can see in the declaration of the function, it accepts a parameter as an 8-bit integer. Without adding extra libraries, we cannot directly use bool, so I rely on integers and limit them in the if-else blocks to 1 and 0 (true and false). So, after setting up the pins, the code checks the value of the parameter passed to the function. If it is ==1, it means that we want to set up the pin (PD2) as an output. So the GPIO_Mode will be set up as GPIO_Mode_Out_OD. We use OD to align with the sensor’s requirements. PD2 is a shared line: both the microcontroller and the sensor must be able to pull it LOW, but only the pull-up resistor brings it high. If we’d use push-pull (PP) instead of open-drain (OD) configuration, there’s a risk of shorting the line because the microcontroller could drive the pin HIGH while the sensor is trying to pull it LOW at the same time.
If the output parameter was ==0, then the GPIO_Mode would be set as GPIO_Mode_IN_FLOATING. This is because when we want to accept incoming data, we want the sensor to drive the data line. If we set it to floating, then the state (LOW or HIGH) of the PD2 pin will be defined by the temperature sensor.
void ds18b20_set_pin_mode(uint8_t output) { GPIO_InitTypeDef GPIO_InitStructure = {0}; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; if (output == 1) { GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; } else if(output == 0) { GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; } GPIO_Init(GPIOD, &GPIO_InitStructure); }
Initialisation
Now we have a function that can set the GPIO pin as an input or output, so we can start the basic communication with the chip. The first step is to initialize the communication by performing a sequence of sending a reset pulse to the sensor and receiving a presence pulse from it.
Since the chip is supposed to return a presence pulse, we need both to be able to write and read the data line and interpret the data.
As we can see, the initialisation consists of two steps. First, the master (microcontroller) has to send out a reset pulse by pulling the data line LOW. The data line must be kept LOW for at least 480 us. Then, the line must be released. At the same time of the release, the data pin (PD2) on the microcontroller is set as an input. Then, the DS18B20 detects this rising edge, and within 15-60 us, the chip should pull the data line LOW and keep it low for 60-240 us. The code waits 30 us and then checks the PD2 pin’s state and passes its inverted value to the corresponding variable. The reason why the value is inverted is that the chip indicates its presence by pulling the data line LOW, which is represented by zero. So, when the PD2 pin is read, it returns zero if the chip is present. However, it makes more sense to indicate the presence with “1” when we use it as a status indicator. Therefore, the bit read from PD2 must be inverted.
Finally, after another delay to comply with the timing chart, the presence value is returned.
uint8_t ds18b20_init(void) { uint8_t presence; ds18b20_set_pin_mode(1); GPIO_WriteBit(GPIOD, GPIO_Pin_2, Bit_RESET); Delay_Us(480); ds18b20_set_pin_mode(0); Delay_Us(30); presence = !GPIO_ReadInputDataBit(GPIOD, GPIO_Pin_2); Delay_Us(450); return presence; }
Reading and writing
Now we can detect the presence of the chip; it is time to implement the reading and writing operations. According to the datasheet, there are well-defined time slots where the chip can send and receive “0” and “1” values.
By setting the PD2 pin as an output and pulling it low, the microcontroller is prepared to receive the bit sent by the DS18B20 chip. After a brief 6 us delay, the line is released by setting the PD2 pin as input. Then, after another 6 us of waiting, the state of the PD2 pin is read by the microcontroller. The sensor must set the state of the line within 15 us, so that’s why the total 6+6 us delay before reading. After reading the state of PD2, another 48 us delay is added, so in total, there is 60 us, which matches the timing chart’s 15+45 us timing.
As one can see, if the released bus is still zero after 12 us, it is certain that the sensor kept it low. If it is high after 12 us, we can be sure that the sensor pulled it up.
The writing (by the MCU) is fairly similar to the reading sequences.
First, the PD2 pin is set as an output, and it is pulled LOW. Then, depending on which bit we want to pass to the sensor, there are two possibilities:
1: In case we want to write a “1”, a 5 us delay is added, then the bus is released, which is then pulled automatically HIGH by the pull-up resistor. Then another 55 us delay is added so the total pulse time becomes around 60 us, as per the timing chart of the sensor.
2: In case we want to write a “0”, a 60 us delay is added, and then the bus is released. So, throughout the whole 60 us, the bus is kept LOW.
uint8_t ds18b20_read_bit(void) { uint8_t bit = 0; ds18b20_set_pin_mode(1); GPIO_WriteBit(GPIOD, GPIO_Pin_2, Bit_RESET); Delay_Us(6); ds18b20_set_pin_mode(0); Delay_Us(6); bit = GPIO_ReadInputDataBit(GPIOD, GPIO_Pin_2); Delay_Us(48); return bit; }
void ds18b20_write_bit(uint8_t bit) { ds18b20_set_pin_mode(1); GPIO_WriteBit(GPIOD, GPIO_Pin_2, Bit_RESET); if (bit == 1) { Delay_Us(5); ds18b20_set_pin_mode(0); Delay_Us(55); } else { Delay_Us(60); ds18b20_set_pin_mode(0); Delay_Us(1); } }
Arranging the sent and received bits into bytes
Above, we learned how to send and receive individual bits, but the chip communicates via bytes.
Both the read_byte and send_byte functions are similar in the sense that they just “repeat” the read_bit and write_bit functions eight times and shift the incoming/outgoing bits accordingly to build or decompose a byte.
The write_byte() function takes the byte we want to send to the sensor, and after masking the byte’s LSB, it sends it to the sensor. So each time, it only sends a “0” or a “1” depending on what the masking returned. After sending the bit, the byte is shifted right by 1, so in the next iteration, the next bit of the whole byte will be sent to the sensor.
The read_byte() function does the “opposite”. In the first iteration, the bit shifting has no effect. If the read_bit() function returns 1, the byte variable is “bitwise ORed” together with 1000 000, so its MSB is set to one. Then, in the second iteration, this bit is shifted right by 1 bit, and then the next incoming bit is evaluated. If the bit is 1, we pass it to the byte as the MSB; otherwise, we don’t do anything. After reading out a full byte, the function returns the value of it.
void ds18b20_write_byte(uint8_t byte) { for(int i = 0; i < 8; i++) { ds18b20_write_bit(byte & 0x01); byte >>= 1; } }
uint8_t ds18b20_read_byte(void) { uint8_t byte = 0; for(int i = 0; i < 8; i++) { byte >>= 1; if(ds18b20_read_bit() == 1) { byte |= 0x80; } } return byte; }
Reading the temperature
To read the temperature, first, the code checks for the presence of the sensor by the init() function.
Then, we start sending bytes to the sensor. First, 0xCC to skip ROM (see Figure 13 in the datasheet). Then 0x44 to initiate a single temperature conversion. The conversion data is stored in the 2-byte temperature register in the scratchpad memory.
Then the bus is released by setting the pin mode as input, and the code waits for the bus to go HIGH.
Once it is HIGH, the chip is reinitialised, and after skipping the ROM, the read scratchpad (0xBE) command is issued. This command allows the master to read the contents of the scratchpad.
Since the scratchpad has 2 bytes, two readings are necessary: one to read the LSB and another to read the MSB.
Finally, the function returns with the reconstructed 16-bit variable that holds the 12-bit temperature conversion.
Since this is just the raw data, it must be converted into Celsius degrees. As usual, we want to print the conversion values on the serial terminal as floating-point numbers. I showed it in many of my examples; this does not work directly, so we need to cheat a bit to convert the float to a printable set of characters.
One thing I want to highlight is the 1/16 division in the first line of the printFloatTemp() function. That number is introduced because to convert the raw data of the chip into Celsius degrees, we need to divide it by 16, which is related to the increments determined by the resolution (1/16 or 0.0625 at 12 bits) set on the sensor. Then the rest of it is just splitting the float up into two integers that store the whole and fractional parts separately. Finally, the number is printed on the terminal as it was a float, but in reality, it is just a string of characters arranged to look like a float.
int16_t ds18b20_get_temperature_raw(void) { if (ds18b20_init() == 0) { printf("Error"); return -10000; } ds18b20_write_byte(0xCC); ds18b20_write_byte(0x44); ds18b20_set_pin_mode(0); while(!ds18b20_read_bit()); ds18b20_init(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0xBE); uint8_t temp_lsb = ds18b20_read_byte(); uint8_t temp_msb = ds18b20_read_byte(); return (int16_t)((temp_msb << 8) | temp_lsb); }
void printFloatTemp(int16_t raw) { int32_t scaled = (int32_t) raw * 10 / 16; int whole = (int)scaled / 10; int frac = (int)abs(scaled % 10); printf("%d.%01d°C\n", whole, frac); }