FB pixel

Build Your Own Digital Thermometer Using MCC | Embedded C Programming - Part 13

Published


Hello! Today we will keep talking about the DS18B20 sensor and 7-segment LED indicator based digital thermometer but this time we will use the MCC for configuring the required MCU modules. I already talked about the 7-segment LED indicator in tutorial 7, and will base the current tutorial on it. Also I talked about the DS18B20 sensor in this tutorial, but I’ll copy-paste the information about it here as well, to simplify your reading.

The task for this tutorial is to display the temperature value taken from the DS18B20 in Celsius in the 3-digit 7-segment LED indicator. If the temperature is positive, display it in format “xx.x”, otherwise display it in the format “-xx”.

DS18B20 Sensor Description

DS18B20 is a very popular sensor designed by the Dallas Semiconductor company (that’s why the sensor’s name starts with the ‘DS’) which was bought by the Maxim Integrated corporation in 2001.

The price of the sensor starts from $0.60 at Aliexpress. The appearance of the sensor is shown in Figure 1.

DS18 B20 sensor PIC18 F14 K50
Figure 1 - DS18B20 Sensor

As you can see, it’s just a chip in a TO-92 package. There are also modifications in the uSOP-8 and SOIC-8 packages (Figure 2, taken from the data sheet).

DS18 B20 pinout PIC18 F14 K50
Figure 2 - DS180B20 Pinout

The DS18B20 chip only has 3 active pins - two are used for power (VDD and GND), and one (DQ) is used for communication with the microcontroller. This chip uses a special digital interface called 1-wire, which was also developed by Dallas Semiconductor. This interface is not as widespread as UART, I2C or SPI, and the majority of microcontrollers don’t have a hardware module which supports it. This interface has some advantages which make it popular, though. The most obvious is the low number of pins: except for power, it uses just one microcontroller pin. You can also connect several devices in parallel like an I2C bus. And furthermore, the 1-wire bus supports so called “parasitic power”, which means that devices can use the DQ pin both for powering and for sending the data, thus they truly only need just two wires to connect.

Like the I2C bus, 1-wire also has the Master-Slave architecture, which means that all communications are inspired by the Master device while Slave devices just follow its commands. As I said before, there can be several Slave devices connected in parallel. Each 1-wire device has a unique 64-bit address programmed at the factory, so there is no chance that you connect two devices with the same address to the same bus. The schematic diagram of the connection between the Master and the Slave via 1-wire bus is shown in Figure 3, again taken from the data sheet.

wire bus connection
Figure 3 - 1-wire Bus Connection

As you can see, the connection is similar to the I2C bus. Here we also have the pull-up resistor which should be 4.7 kOhm. The devices also have an open-collector output, and that means we will also need to reconfigure the microcontroller’s pin as an input (to send 1 to the bus) or as an output (to send 0 to the bus).

Let’s now see how to communicate using 1-wire. Like an I2C bus, 1-wire also has a special condition which always should be issued to start the communication. This condition is called Reset (Figure 4, taken from the data sheet).

reset condition
Figure 4 - Reset Condition

Unlike an I2C bus, 1-wire doesn’t have the clock pulses, so to synchronize the Master and the Slave devices, they both need to strictly follow the specified timings.

To start the Reset, the Master pulls the bus down for at least 480 us. Then it releases the bus and waits for 15-60 us. If any device is present on the bus, it will respond to this reset pulse by pulling the bus down for 60-240 us. So after sending the Reset pulse, the Master device should wait for about 60 us and then read the bus state. If it’s 0, that means that the Slave device is present, and ready for the communication. If the bus state is 1, then there are no Slave devices, or they are disabled. After the Master reads the bus state it should wait for about 410 us to make sure that the Slave has released the bus.

After the start condition the Master sends the data. Usually the first byte is the command (we already mentioned them and will consider them in greater detail later). Sending a 1 or 0 to the bus also requires strict timings (Figure 5, taken from the data sheet).

wire bus connection sending data
Figure 5 - Sending ‘0’ and ‘1’ to via the 1-wire bus

To write 0 to the bus, the Master pulls the line down for at least 60 us, then it releases the bus for at least 1us (10-15 us is OK). To write 1 to the bus, the Master pulls the line down for at least 1 us (from my experience 8-10 us is OK as well) and then releases the bus for at least 60 us.

Finally, let’s see how reading from the bus is implemented (Figure 6, taken from the data sheet).

wire bus connection reading data
Figure 6 - Reading from the 1-wire bus

The Master device should set the bus low for more than 1 us (3-5 us is OK) and then release it. Then the Slave should either pull the bus low if it transmits 0, or leave the bus high if it transmits 1. The Master waits for about 15 us and then reads the bus state and saves it. Finally, the Master should wait for at least 45 us to make sure that the Slave has released the bus, and then it can start the new time slot to read the next bit.

That’s all about the 1-wire bus signaling. Now let’s briefly consider the logic level of the interface.

After sending the Reset pulse and receiving the presence confirmation from the Slave, Master should send one of the so-called ROM commands. These commands are used to select one of the devices that are present on the bus, using their unique 64-bit address. I will not describe them here, you can always refer to the data sheet. The only command that we will use is called Skip ROM and has the code 0xCC. This command is used to send the data to all the devices that are present on a bus, and as long as we have only one device, we can use it.

After sending the ROM command, the Master issues one of the device-specific commands. In our program we will use two of the commands specific for the DS18B20 sensor: Convert T (0x44) and Read Scratchpad (0xBE). The first one is used to start the temperature measurement and conversion. This process can take up to 750 ms. So it’s better to wait for about 1 second before reading the value. The second command is used to read the so-called Scratchpad. It consists of nine bytes, and includes the temperature value, the threshold values, the configuration register and the CRC byte. We will read just the two first bytes which represent the lower and the upper byte of the temperature correspondingly.

Speaking of the temperature, it’s stored in the format shown in Figure 7, taken from the data sheet.

temperature value format
Figure 7 - Temperature Value Format

As you see, the upper 5 bits represent the sign of the temperature (0 for positive, 1 for negative), the rest 11 bits represent the temperature value with the resolution of 2-4 C, or 0.0625 C.

And that’s all we need to know about the DS18B20 sensor, so now we can switch to the schematics diagram of the thermometer.

Schematics Diagram

The schematics diagram is shown in Figure 8.

B20 sensor 7 segment display
Figure 8 - Schematics Diagram with the PIC18F14K50 with DS18B20 Sensor and 7-segment LED Indicator

This schematics diagram is very similar to the one presented in tutorial 5: it also consists of the PIC18F14K50 MCU (DD1), 7-segment 3-digits LED indicator (7Seg1), but the connection of the digits common pins of the indicator to the MCU is a bit different. Now we will use the RB4-RB6 pins. I made this to release the RB7 pin which is easier to configure as both input and output. The DS18B20 temperature sensor (DD2) is connected to this RB7 pin for this reason. Also, it uses the external pull-up resistor R9 of 4.7kOhm which is required for normal sensor operation (see Figure 3). R1 - R8 resistors limit the current that flows through the LEDs of the indicator. As in tutorial 6, I used the common anode indicator, so if you have the common cathode one, please refer to that tutorial to check for the differences in the program code.

MCC Configuration

Let’s now briefly consider the MCC configuration for the current project. As we will not use any new modules, I will just provide you the screenshots of the used modules (Figure 9-10).

B20 MCC pin manager pin module configuration
Figure 9 - Pin Manager and Pin Module Configuration

Please pay attention as the pins configuration is a bit different than in tutorial 7. Also I added the pin OW_BUS (which stands for “One Wire Bus”) to which the DS18B20 sensor is connected.

PIC18 F14 K50 DS18 B20 MCC system module configuration
Figure 10 - System Module Configuration

Here, I only changed the Internal Clock to 16 MHz, and removed the check from the “Low-voltage Programming Enable” (as usual). Here we overclock the CPU up to 16 MHz to fit the exact timings required for 1-wire bus specification.

This is all about the MCC, and now we can proceed to the program code consideration.

Program Code Description

#include "mcc_generated_files/mcc.h"

uint8_t digit; //Digit number that is currently displayed

int16_t temp; //Temperature received from the DS18B20 sensor

uint8_t data[2]; //Array to read two bytes from the scratchpad

uint32_t tick; //5 ms counter

//=============1-wire functions======================

//---------1-wire bus reset-------------------------------

uint8_t ow_reset(void)

{

uint8_t presence; //Presence flag

OW_BUS_SetDigitalOutput();//Set OW_BUS as output (set bus low)

__delay_us(480); //Set bus low for 480 us

OW_BUS_SetDigitalInput();//Set OW_BUS as input (set bus high)

__delay_us(70); //Waiting for the presence signal for 70us

if(OW_BUS_GetValue() == 0) //If bus is low then the device is present

presence = 1; //Set presence flag high

else //Otherwise

presence = 0; //Set presence flag low

__delay_us(410); //And wait for 410 us

return (presence); //Return the presence flag

}

//---------Writing one bit to the device------------------------

void ow_write_bit (uint8_t bit)

{

OW_BUS_SetDigitalOutput();//Set OW_BUS as output (set bus low)

if (bit) //If we send 1

{

__delay_us(6); //Perform 6 us delay

OW_BUS_SetDigitalInput();//Set OW_BUS as input (set bus high)

__delay_us(64); //Perform 64 us delay

}

else //If we send 0

{

__delay_us(60); //Perform 60 us delay

OW_BUS_SetDigitalInput();//Set OW_BUS as input (set bus high)

__delay_us(10); //Perform 10 us delay

}

}

//--------Reading one bit from the device-------------------------

uint8_t ow_read_bit (void)

{

uint8_t bit; //Value to be returned

OW_BUS_SetDigitalOutput();//Set OW_BUS as output (set bus low)

__delay_us(6); //Perform 6 us delay

OW_BUS_SetDigitalInput();//Configure OW_BUS as input (set bus high)

__delay_us(9); //Perform 9 us delay

bit = OW_BUS_GetValue();//Save the state of the bus in the bit variable

__delay_us(55); //Perform 55 us delay

return (bit);

}

//---------Writing one byte to the device------------------------

void ow_write_byte (uint8_t byte)

{

for (uint8_t i = 0; i < 8; i++) //Loop to transmit all bits LSB first

{

ow_write_bit (byte & 0x01);//Send the bit

byte >>= 1; //Shift the byte at one bit to the right

}

}

//---------Reading one byte from the device-----------------------

uint8_t ow_read_byte (void)

{

uint8_t byte = 0; //Value to be returned

for (uint8_t i = 0; i< 8; i++)//Loop to read the whole byte

{

byte >>= 1; //Shift the byte at one bit to the right

if (ow_read_bit()) //If 1 has been read

byte |= 0x80; //Then add it to the byte

}

return (byte);

}

//============DS18B20 sensor functions=======================

//-----------Command to start the temperature conversion-----------

void convert_t (void)

{

if (ow_reset()) //If sensor is detected

{

ow_write_byte(0xCC);//Issue "Skip ROM" command

ow_write_byte(0x44);//Issue "Convert T" command

}

}

//---Reading and calculating the temperature from the ds18s20 sensor-------

int16_t read_temp (void)

{

int16_t tmp; //Calculated temperature

if (ow_reset()) //If sensor is detected

{

ow_write_byte (0xCC);//Issue "Skip ROM" command

ow_write_byte (0xBE);//Issue "Read Scrathpad" command

for (uint8_t i = 0; i < 2 ; i++)//Loop for reading 2 bytes from scratchpad

data[i] = ow_read_byte();//Write the received byte into the data array

tmp = data[0] + (data[1] << 8);//Calculating the temperature

tmp = (tmp * 10) / 16;//Convert positive temperature into 0.1 C

}

else //If sensor was not detected

tmp = -990; //Then return the value that sensor can't have

return (tmp); //And return the temperature value

}

//=============7-segment LED generator=======================

void show_digit (uint8_t digit, uint8_t show_dp) //Generate a digits

{

switch (digit) //Select digit

{

case 0: //If digit is 0

SEG_A_SetLow(); //Turn on segment A

SEG_B_SetLow(); //Turn on segment B

SEG_C_SetLow(); //Turn on segment C

SEG_D_SetLow(); //Turn on segment D

SEG_E_SetLow(); //Turn on segment E

SEG_F_SetLow(); //Turn on segment F

SEG_G_SetHigh(); //Turn off segment G

break;

case 1: //If digit is 1

SEG_A_SetHigh(); //Turn off segment A

SEG_B_SetLow(); //Turn on segment B

SEG_C_SetLow(); //Turn on segment C

SEG_D_SetHigh(); //Turn off segment D

SEG_E_SetHigh(); //Turn off segment E

SEG_F_SetHigh(); //Turn off segment F

SEG_G_SetHigh(); //Turn off segment G

break;

case 2: //If digit is 2

SEG_A_SetLow(); //Turn on segment A

SEG_B_SetLow(); //Turn on segment B

SEG_C_SetHigh(); //Turn off segment C

SEG_D_SetLow(); //Turn on segment D

SEG_E_SetLow(); //Turn on segment E

SEG_F_SetHigh(); //Turn off segment F

SEG_G_SetLow(); //Turn on segment G

break;

case 3: //If digit is 3

SEG_A_SetLow(); //Turn on segment A

SEG_B_SetLow(); //Turn on segment B

SEG_C_SetLow(); //Turn on segment C

SEG_D_SetLow(); //Turn on segment D

SEG_E_SetHigh(); //Turn off segment E

SEG_F_SetHigh(); //Turn off segment F

SEG_G_SetLow(); //Turn on segment G

break;

case 4: //If digit is 4

SEG_A_SetHigh(); //Turn off segment A

SEG_B_SetLow(); //Turn on segment B

SEG_C_SetLow(); //Turn on segment C

SEG_D_SetHigh(); //Turn off segment D

SEG_E_SetHigh(); //Turn off segment E

SEG_F_SetLow(); //Turn on segment F

SEG_G_SetLow(); //Turn on segment G

break;

case 5: //If digit is 5

SEG_A_SetLow(); //Turn on segment A

SEG_B_SetHigh(); //Turn off segment B

SEG_C_SetLow(); //Turn on segment C

SEG_D_SetLow(); //Turn on segment D

SEG_E_SetHigh(); //Turn off segment E

SEG_F_SetLow(); //Turn on segment F

SEG_G_SetLow(); //Turn on segment G

break;

case 6: //If digit is 6

SEG_A_SetLow(); //Turn on segment A

SEG_B_SetHigh(); //Turn off segment B

SEG_C_SetLow(); //Turn on segment C

SEG_D_SetLow(); //Turn on segment D

SEG_E_SetLow(); //Turn on segment E

SEG_F_SetLow(); //Turn on segment F

SEG_G_SetLow(); //Turn on segment G

break;

case 7: //If digit is 7

SEG_A_SetLow(); //Turn on segment A

SEG_B_SetLow(); //Turn on segment B

SEG_C_SetLow(); //Turn on segment C

SEG_D_SetHigh(); //Turn off segment D

SEG_E_SetHigh(); //Turn off segment E

SEG_F_SetHigh(); //Turn off segment F

SEG_G_SetHigh(); //Turn off segment G

break;

case 8: //If digit is 8

SEG_A_SetLow(); //Turn on segment A

SEG_B_SetLow(); //Turn on segment B

SEG_C_SetLow(); //Turn on segment C

SEG_D_SetLow(); //Turn on segment D

SEG_E_SetLow(); //Turn on segment E

SEG_F_SetLow(); //Turn on segment F

SEG_G_SetLow(); //Turn on segment G

break;

case 9: //If digit is 9

SEG_A_SetLow(); //Turn on segment A

SEG_B_SetLow(); //Turn on segment B

SEG_C_SetLow(); //Turn on segment C

SEG_D_SetLow(); //Turn on segment D

SEG_E_SetHigh(); //Turn off segment E

SEG_F_SetLow(); //Turn on segment F

SEG_G_SetLow(); //Turn on segment G

break;

case '-': //If need to display minus

SEG_A_SetHigh(); //Turn off segment A

SEG_B_SetHigh(); //Turn off segment B

SEG_C_SetHigh(); //Turn off segment C

SEG_D_SetHigh(); //Turn off segment D

SEG_E_SetHigh(); //Turn off segment E

SEG_F_SetHigh(); //Turn off segment F

SEG_G_SetLow(); //Turn on segment G

break;

}

if (show_dp)

{

SEG_DP_SetLow(); //Turn on segment DP

}

else

{

SEG_DP_SetHigh(); //Turn off segment DP

}

}

void main(void)

{

// Initialize the device

SYSTEM_Initialize();

while (1) //Main loop of the program

{

DIG_1_SetLow(); //Turn off digit 1

DIG_2_SetLow(); //Turn off digit 2

DIG_3_SetLow(); //Turn off digit 3

switch (digit) //Which digit to display

{

case 0: //If digit 1

if (temp >= 0) //If temperature is positive

show_digit(temp / 100, 0); //Display the tens of the degrees

else //If temperature is negative

show_digit('-', 0); //Display "-"

DIG_1_SetHigh(); //Turn on digit 1

break;

case 1: //If digit 2

if (temp >= 0) //If temperature is positive

show_digit((temp % 100) / 10, 1); //Display the ones

else //If temperature is negative

show_digit((-temp) / 100, 0); //Display the tens of the degrees

DIG_2_SetHigh(); //Turn on digit 2

break;

case 2: //If digit 3

if (temp >= 0) //If temperature is positive

show_digit(temp % 10, 0); //Display the fractional part

else //If temperature is negative

show_digit(((-temp) % 100) / 10, 0); //Display the ones

DIG_3_SetHigh(); //Turn on digit 3

break;

}

__delay_ms(5); //The delay to let the segment be on for some time

digit ++; //Select next digit

if (digit > 2) //If digit number is higher than 2

digit = 0; //Then set the digit as 0

if ((tick % 200) == 0)//When modulo of tick/200 is 0

convert_t(); //Start temperature conversion

if ((tick % 200) == 180)//When modulo of tick/200 is 180

temp = read_temp();//Read the temperature value

tick ++; //Increase the tick value

}

}

The program is quite long now but actually it’s not very complex. Let’s consider it in more detail.

In lines 3-6, we define the required variables. In line 3 there is the digit variable which represents the number of the LED indicator digit that is currently on. In line 4 we define the temp variable, which is the temperature value in Celsius degrees multiplied by 10. The multiplication by 10 is required because we want to have the temperature with the accuracy of 0.1 degree but don’t want to use the floating point format because operations with it aren’t supported naturally by the PIC18 MCUs and require a lot of memory and time.

The data array defined in line 5 is needed to read two bytes from the DS18B20 scratchpad in which the temperature value is stored.

In line 6, we define the tick variable which is the counter of 5ms intervals which are formed by the delay function in line 257.

In lines 10-77 there are functions that perform the software implementation of the 1-wire protocol.

The ow_reset function defined in lines 10-23 issues the reset condition on the 1-wire bus according to Figure 4, and returns 1 if any device is present on the bus, or 0 if no device is found.

In line 12, we define the presence variable which is the value that the function will return in the end.

In line 13, we set the bus low by configuring the OW_BUS as output. We configure the OW_BUS pin as output or input instead of just setting it low or high because we need to emulate the open-drain output as I described in the comment of Figure 3. So, in 1-wire bus implementation we will always operate with pin direction configuration.

Then we perform the 480us delay (line 14) as is required according to Figure 4. After that, we release the bus (set it high by configuring the OW_BUS pin as input) (line 15) and wait another 70us (line 16). After that, we check the bus state (line 17) by reading the OW_BUS pin state. If a device is present, it will set the bus low, otherwise the bus will remain high. According to this, we set the presence value: if OW_BUS is low, we set it high (line 18) and vice versa (line 20). Then we perform another 410 us delay to make sure that the presence pulse is finished (line 21), and finally return the presence value (line 22) and end the function.

In lines 26-41, there is the ow_write_bit function which accepts a single parameter bit of uint8_t type. This parameter can be either 0 or 1 and represents the bit value we want to send to the bus. Sending the bit is implemented according to Figure 5. First we need to set the bus low (line 28). Then depending on the bit value (line 29) we need to perform the delay: either 6us (line 31) if bit is high or 60us (line 37) otherwise. Then we release the bus (lines 32 or 38) and perform another delay of 64us for “1” (line 33) or 10us for “0” (line 39) to finish the timeslot. And that’s all we need to send the bit.

In lines 44-54 there is the ow_read_bit function which reads one bit from the 1-wire bus and returns it as a function value. First we define the variable bit of type uint8_t (line 46) that represents the value read from the bus. Reading of the bit is implemented according to Figure 6. As usual, we first need to set the bus low (line 47). Then we perform a 6us delay (line 48) and release the bus (line 49). After that, we implement another 9us delay (line 50) to make the overall delay from the pulse start 15us, and save the bus state, set by the DS18B20 sensor, into the bit variable (line 51). Finally, we perform another 55us delay to finish the timeslot (line 52) and return the bit value (line 53).

In lines 57-64 there is the ow_write_byte function. It accepts one parameter byte of uint8_t type which represents the byte that should be sent to the device. This function is quite simple. First, we make the “for” loop to transmit 8 bits of the byte parameter (line 59). In line 61 there is the ow_write_bit function invocation with the parameter “byte & 0x01”. Let’s consider it in more detail. In a 1-wire bus the data is transmitted LSB first. In the expression “byte & 0x01” we perform the bitwise AND between the byte value and the 0x01 constant. The result of this operation will be 1 only if the LSB of the byte is 1, otherwise it will be 0. So by using this expression we extract the LSB of the byte and send it to the bus. Then in line 62 we shift the byte at one bit to the right, so in the next iteration the second bit will be checked, then the third and so on.

In lines 67-77, there is the ow_read_byte function which reads the whole byte from the device and returns it. In line 69, we declare the variable byte and assign 0 to it. We will collect the received from the device bits in this variable to return it at the end of the function. In line 70, we make the “for” loop to receive 8 bits from the device. First, in line 72 we shift the byte value to the right. Then we read one bit from the device and check its value (line 73). If the received bit is 1, we add it as the MSB to the byte variable by implementing the bitwise OR operation “|” between the byte and the constant 0x80 (line 74). If the received bit is 0 we don’t add anything because we already assigned 0 to all bits of the byte variable in line 95.

One may ask why we add the new bit as MSB if the data is transmitted LSB first. Well, shifting the value to the right at every loop iteration will move the first received bit further and further from the MSB, and finally, after eight iterations it will become the LSB, so eventually everything is correct. In line 76 we return the received byte value and exit the function.

That’s all about the common 1-wire functions. Next, in lines 81-106 there are DS18B20-specific functions. The first one is called convert_t and is located at lines 81-88.

As I mentioned above, to start the temperature conversion, we need to send the special command “Convert T” which has the code 0x44 (line 86). But first we need to send the “Skip ROM” 0xCC command (line 85) to skip checking the device address. As long as we have only one device on the bus, such an approach is acceptable. All the commands should have a reset pulse header which we send in line 83 by invoking the function ow_reset. Moreover, this function checks if the device is present on the bus, so if there is no presence pulse followed by the reset condition, then lines 85-86 will be ignored.

And that’s all about the convert_t function. The next one, read_temp located at lines 91-106, is a bit more complicated.

First, we declare the variable tmp (line 93) which will represent the calculated value of the temperature in 0.1 C. In other words, this is the temperature value, multiplied by 10. In line 94 we issue the reset condition on the 1-wire bus and check if the device is present. If it is, we call the “Skip ROM” 0xCC command (line 96), followed by the “Read Scratchpad” 0xBE command (line 97). After the last command, the device will be ready to send the 9 bytes from the scratchpad, from which we actually need only 2, so we will read only them. In line 98 we start the loop to read two bytes from the scratchpad into the data array (line 99). After that the data[0] and data[1] will have the lower and upper bytes of the temperature value, correspondingly. Now we need to merge them into one variable tmp, which we do in line 100. Shifting the data[1] value at 8 bits to the left will put it as the upper byte into the tmp variable. Now, the tmp value has the format shown in Figure 7. This format corresponds to the 2’s complement format normally used in the C language, so both positive and negative values will be recognized by the compiler correctly. In line 101 we multiply the value by 10 to get the temperature in 0.1 C, and then divide the value by 16 which actually means shifting the tmp value four (log216 = 4 or 24 = 16 if your logarithms are rusty) bits to the right. This moves all the 2’s complement with the negative degree (Figure 7) to the right side of the imaginary floating point (it wouldn’t be imaginary if we didn’t multiply the value by 10). The order of the mathematical operations is also important. We need to perform the multiplication prior to the division, otherwise we will lose the accuracy, and there will not be any sense in the x10 multiplication as the fractional part will always be 0.

If the sensor is not present on the bus (line 103), we assign the value -990 to the tmp variable (line 104). The sensor has the minimum temperature value -55 degrees, so the value -990 is not reachable by the sensor and can serve as an indicator of sensor absence. Finally, we return the tmp value and end the function (line 105).

In lines 109-221 there is a function show_digit, which is very similar to the one described in tutorial 7 but has certain differences. Firstly, it accepts two parameters. The first one, digit, is the sign which we want to display, and the second one, show_dp, is the flag to display the decimal point at the current position when it’s 1 or to hide it when it’s 0. The principle of forming the signs is the same as described in tutorial 7 so I’ll skip it. In lines 203-211 we added the part to form the minus sign as we will need it to display the negative temperature.

In line 213 we check the parameter show_dp. If it’s 1 we set the SEG_DP pin low to show the decimal point (line 215), otherwise we set it high (line 219) to hide the decimal point.

Now all the auxiliary functions have been described, and we can switch to the main function of the program (lines 223-267).

The main loop is very similar to the one in tutorial 7. We turn off all the segments at the beginning (lines 230-232), then check which digit is needed to be turned on in the switch construction (lines 233-256). But there is some difference inside the cases which we need to consider in more detail. So, according to the task we need to display data in different formats for positive (xx.x) and negative (-xx) temperatures. Thus, in each case part we check if the temp value is positive (lines 236, 247, 250). If it is, we display the hundreds of the temp (as you remember the temperature is multiplied by 10, so the hundreds of the temp correspond to the tens of the temperature value, tens of the temp correspond to the ones of temperature, and ones of the temp correspond to the fractional part of the temperature) in the first position (line 237), tens of the temp in the second position (line 244), and ones of the temp in the third position (line 251).

If the temperature is negative, the displayed value should be a bit different. We display the minus sign at the first position in line 239, then we display the hundreds of the negated temp (to make it positive) at the second position (line 246) by writing, and finally we display the tens of the negated temp at the third position (line 253).

Please pay attention that in almost all cases we hide the decimal point (the second parameter of the show_digit function is 0), we only need to show it on the second position when the temperature is positive, so in line 244 this parameter is 1.

After setting the correct digits shapes at the required positions, we turn on the corresponding digits in lines 240, 247, and 254. Please pay attention as the pin numbers differ from tutorial 7, as we connected the common indicator pins to other MCU pins.

After turning on the segment we perform the 5ms delay (line 257), and select the next digit to be displayed (lines 258-260).

In line 261 we check if the modulo of division of the tick value by 200 is 0. This event becomes true every second because tick increments every 5ms (line 265), so when it becomes 200, the time passed is 200x5=1000ms. So if this condition is true we send the “Convert T” command to the device (line 262) to start the temperature conversion.

Then in line 263 we check if the modulo of division of the tick value by 200 is 180. This condition also becomes true every second but it is shifted to the “Convert T” sending condition by 180 ticks, which is 180x5=900ms. This is enough to calculate the temperature even with the highest resolution of 12 bit. So if this condition is true, we read the temperature value (line 264) to display it in the LED indicator.

In line 265, as I mentioned before, we increment the tick value every loop iteration.

And that’s all about the program code. Now you can assemble the circuit according to the schematics diagram (Figure 8), compile and download the code, and see how your thermometer works. Don’t forget that I skipped the configuration bits when I wrote the program code but you still need to add them to make the device work correctly. I talked about the configuration bits in detail in tutorial 2.

Also I want to mention that you are free to use the compiler optimisation. I talked about how to set it up in tutorial 11. The optimization allows to significantly reduce the code size. For instance when the optimization level is “0”, the size of the current program is 1978 bytes, and with the optimization level “s” the size reduces to 1520 bytes, thus the code becomes 23% more compact.

As homework, I suggest you convert the temperature into Fahrenheit and display it in these units. In the C language the mathematical operations are much more simple than in Assembly so you won’t have problems with this.

Make Bread with our CircuitBread Toaster!

Get the latest tools and tutorials, fresh from the toaster.

What are you looking for?