FB pixel

Embedded C Programming with the PIC18F14K50 - 20. Automated Temperature Control System with PID Control. Part 2 - Program Code

Published


Hello again! In this part of the tutorial we keep talking about the PID control system, and this time we will consider its program code.

We will use separate files for 1-wire and LCD functions, and also will make them more universal. So except for the “main.c” file let’s additionally create two “c” files and three “h” files, and call them “ds18b20.c”, “lcd_1602.c”, “ds18b20.h”, “lcd_1602.h”, and “config.h”. Now the project should look like in Figure 7.

Project structure PIC18
Figure 7 - Project Structure

Let’s start with the “config.h” file. Actually I don’t know why I didn’t create it earlier, because it is very useful (and actually it’s one of the files that the MCC also creates). Let’s consider its content:

// PIC18F14K50 Configuration Bit Settings

// 'C' source line config statements

// CONFIG1L

#pragma config CPUDIV = NOCLKDIV// CPU System Clock Selection bits (No CPU System Clock divide)

#pragma config USBDIV = OFF // USB Clock Selection bit (USB clock comes directly from the OSC1/OSC2 oscillator block; no divide)

// CONFIG1H

#pragma config FOSC = IRC // Oscillator Selection bits (Internal RC oscillator)

#pragma config PLLEN = OFF // 4 X PLL Enable bit (PLL is under software control)

#pragma config PCLKEN = ON // Primary Clock Enable bit (Primary clock enabled)

#pragma config FCMEN = OFF // Fail-Safe Clock Monitor Enable (Fail-Safe Clock Monitor disabled)

#pragma config IESO = OFF // Internal/External Oscillator Switchover bit (Oscillator Switchover mode disabled)

// CONFIG2L

#pragma config PWRTEN = OFF // Power-up Timer Enable bit (PWRT disabled)

#pragma config BOREN = SBORDIS // Brown-out Reset Enable bits (Brown-out Reset enabled in hardware only (SBOREN is disabled))

#pragma config BORV = 19 // Brown-out Reset Voltage bits (VBOR set to 1.9 V nominal)

// CONFIG2H

#pragma config WDTEN = OFF // Watchdog Timer Enable bit (WDT is controlled by SWDTEN bit of the WDTCON register)

#pragma config WDTPS = 32768 // Watchdog Timer Postscale Select bits (1:32768)

// CONFIG3H

#pragma config HFOFST = ON // HFINTOSC Fast Start-up bit (HFINTOSC starts clocking the CPU without waiting for the oscillator to stablize.)

#pragma config MCLRE = ON // MCLR Pin Enable bit (MCLR pin enabled; RA3 input pin disabled)

// CONFIG4L

#pragma config STVREN = ON // Stack Full/Underflow Reset Enable bit (Stack full/underflow will cause Reset)

#pragma config LVP = OFF // Single-Supply ICSP Enable bit (Single-Supply ICSP disabled)

#pragma config BBSIZ = OFF // Boot Block Size Select bit (1kW boot block size)

#pragma config XINST = OFF // Extended Instruction Set Enable bit (Instruction set extension and Indexed Addressing mode disabled (Legacy mode))

// CONFIG5L

#pragma config CP0 = OFF // Code Protection bit (Block 0 not code-protected)

#pragma config CP1 = OFF // Code Protection bit (Block 1 not code-protected)

// CONFIG5H

#pragma config CPB = OFF // Boot Block Code Protection bit (Boot block not code-protected)

#pragma config CPD = OFF // Data EEPROM Code Protection bit (Data EEPROM not code-protected)

// CONFIG6L

#pragma config WRT0 = OFF // Table Write Protection bit (Block 0 not write-protected)

#pragma config WRT1 = OFF // Table Write Protection bit (Block 1 not write-protected)

// CONFIG6H

#pragma config WRTC = OFF // Configuration Register Write Protection bit (Configuration registers not write-protected)

#pragma config WRTB = OFF // Boot Block Write Protection bit (Boot block not write-protected)

#pragma config WRTD = OFF // Data EEPROM Write Protection bit (Data EEPROM not write-protected)

// CONFIG7L

#pragma config EBTR0 = OFF // Table Read Protection bit (Block 0 not protected from table reads executed in other blocks)

#pragma config EBTR1 = OFF // Table Read Protection bit (Block 1 not protected from table reads executed in other blocks)

// CONFIG7H

#pragma config EBTRB = OFF // Boot Block Table Read Protection bit (Boot block not protected from table reads executed in other blocks)

// #pragma config statements should precede project file includes.

// Use project enums instead of #define for ON and OFF.

#define _XTAL_FREQ 32000000 //CPU clock frequency

As you can see, here we have the configuration bits (lines 1-58) about which I talked in tutorial 2. Actually you had to insert these lines into your code every time, but we previously inserted it into the “main.c” file, and now we created a separate file for it. In line 60 we define the _XTAL_FREQ macro with the value of 32MHz. Previously we also defined it in the “main.c” file but in this project we will need it in several files, so there is no reason to define it several times with the risk of forgetting to change it somewhere and having troubles because of this.

The next file that we will consider is “ds18b20.h”:

#include <xc.h>

#define TRIS_OWbits TRISCbits //TRIS register of the 1-wire bus pin

#define TRIS_OW_pin TRISC7 //1-wire bus pin of the TRIS register

#define LAT_OWbits LATCbits //LAT register of the 1-wire bus pin

#define LAT_OW_pin LATC7 //1-wire bus pin of the LAT register

#define PORT_OWbits PORTCbits //PORT register of the 1-wire bus pin

#define PORT_OW_pin RC7 //1-wire bus pin of the PORT register

#define ANSEL_OWbits ANSELHbits //ANSEL register of the 1-wire bus pin

#define ANSEL_OW_pin ANS9 ////1-wire bus pin of the ANSEL register

void ow_configure (void); //Configure the 1-wire pin

uint8_t ow_reset(void); //Reset the 1-wire bus and wait for the presence pulse

void ow_write_bit (uint8_t bit); //Write one bit to the 1-wire bus

uint8_t ow_read_bit (void); //Read one bit from the 1-wire bus

void ow_write_byte (uint8_t byte); //Write one byte to the 1-wire bus

uint8_t ow_read_byte (void); //Read one byte from the 1-wire bus

void convert_t (void); //Start temperature conversion

int16_t read_temp (void); //Read the temperature value in 0.1 C

In line 1, we include the “xc.h” file to be able to use the standard integer types, and the MCU registers and bits names. As I decided to make the DS18B20 and LCD files more universal, I added at the beginning of each header file the macro definitions of the pins and registers. So if you decide to connect the sensor or LCD to other pins than in the current project, you just need to redefine these macros, and everything will work well.

As, in the current project, the DS18B20 sensor is connected to the RC7 pin, we define the TRIS_OWbits macro as TRISCbits (line 3), and TRIS_OW_pin as TRISC7 (line 4). Then we do the same with the LAT (lines 5-6), PORT (lines 7-8), and ANSEL (lines 9-10) registers. The last register is optional as not all the pins have the analog function. In this case, just comment lines 9-10. To see the correspondence between the pin names and the ANSEL(H) bits please refer to the data sheet of the PIC18F14K50 MCU, or to tutorial 16, where I described them.

In lines 12-19 there are function definitions. All functions except for the ow_configure() were already described in detail in tutorial 12.

The ow_configure() function (line 12) is required to configure the selected pin properly, and now you need to invoke it prior to using the 1-wire bus.

Let’s now consider the corresponding source file “ds18b20.c”:

#include <xc.h>

#include "ds18b20.h"

#include "config.h"

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

//---------1-wire pin configuration-------------------------------

void ow_configure (void)

{

TRIS_OWbits.TRIS_OW_pin = 1;//Configure 1-wire pin as input (set bus high)

LAT_OWbits.LAT_OW_pin = 0; //Set the latch of the 1-wire pin low

ANSEL_OWbits.ANSEL_OW_pin = 0;//Disable the analog buffer at 1-wire pin

}

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

uint8_t ow_reset(void)

{

uint8_t presence; //Presence flag

TRIS_OWbits.TRIS_OW_pin = 0;//Configure 1-wire pin as output (set bus low)

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

TRIS_OWbits.TRIS_OW_pin = 1;//Configure 1-wire pin as input (set bus high)

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

if(PORT_OWbits.PORT_OW_pin == 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)

{

TRIS_OWbits.TRIS_OW_pin = 0;//Configure 1-wire pin as output (set bus low)

if (bit) //If we send 1

{

__delay_us(6); //Perform 6 us delay

TRIS_OWbits.TRIS_OW_pin = 1;//Configure 1-wire pin as input (set bus high)

__delay_us(64); //Perform 64 us delay

}

else //If we send 0

{

__delay_us(60); //Perform 60 us delay

TRIS_OWbits.TRIS_OW_pin = 1;//Configure 1-wire pin 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

TRIS_OWbits.TRIS_OW_pin = 0;//Configure 1-wire pin as output (set bus low)

__delay_us(6); //Perform 6 us delay

TRIS_OWbits.TRIS_OW_pin = 1;//Configure 1-wire pin as input (set bus high)

__delay_us(9); //Perform 9 us delay

bit = PORT_OWbits.PORT_OW_pin;//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

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

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

{

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

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

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

data[i] = ow_read_byte();//Writing 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

}

In lines 1-3 we include the necessary header files:

  • “xc.h” (line 1) to use the integer data types and MCU registers and bits names;
  • “ds18b20.h” (line 2) to use the macro definitions of the 1-wire pin we have just considered;
  • “config.h” (line 3) to use the _XTAL_FREQ macro in the delay functions.

In lines 5-111 there are 1-wire and DS18B20-specific functions. All of them except for the ow_configure() have been considered in detail in tutorial 12, so I’ll just briefly talk about them.

The ow_configure() function (lines 7-12) configures the pin which is used for the 1-wire bus operation.

First, we configure it as input by setting the corresponding bit of the TRIS register as 1 (line 9). Please note, that we use here the macros from the “ds18b20.h” file instead of the usual registers and pins names. The text becomes a bit longer but more readable and universal.

In line 10, we set the corresponding bit of the LAT register as 0 because when we reconfigure this pin as output (as you remember we emulate the open-drain output in this way), the state of the bus should be low.

In line 11, we set the corresponding bit of the ANSEL register to 0 to disable the analog input buffer and enable the digital input buffer. If the selected pin doesn’t have the merged analog functionality, this line should be commented.

The rest of the functions are exactly the same as in tutorial 12, so please refer to it for a detailed explanation. The only difference is that we now use our own macro definitions instead of those provided by the MPLAB IDE.

Now let’s switch to the “lcd_1602.h” file.

#include <xc.h> // include processor files - each processor file is guarded.

#define TRIS_Ebits TRISCbits //TRIS register of the E pin

#define TRIS_E_pin TRISC6 //E pin of the TRIS register

#define LAT_Ebits LATCbits //LAT register of the E pin

#define LAT_E_pin LATC6 //E pin of the LAT register

#define TRIS_RSbits TRISCbits //TRIS register of the RS pin

#define TRIS_RS_pin TRISC4 //RS pin of the TRIS register

#define LAT_RSbits LATCbits //LAT register of the RS pin

#define LAT_RS_pin LATC4 //RS pin of the LAT register

#define TRIS_D4bits TRISBbits //TRIS register of the D4 pin

#define TRIS_D4_pin TRISB4 //D4 pin of the TRIS register

#define LAT_D4bits LATBbits //LAT register of the D4 pin

#define LAT_D4_pin LATB4 //D4 pin of the LAT register

#define TRIS_D5bits TRISBbits //TRIS register of the D5 pin

#define TRIS_D5_pin TRISB5 //D5 pin of the TRIS register

#define LAT_D5bits LATBbits //LAT register of the D5 pin

#define LAT_D5_pin LATB5 //D5 pin of the LAT register

#define TRIS_D6bits TRISBbits //TRIS register of the D6 pin

#define TRIS_D6_pin TRISB6 //D6 pin of the TRIS register

#define LAT_D6bits LATBbits //LAT register of the D6 pin

#define LAT_D6_pin LATB6 //D6 pin of the LAT register


#define TRIS_D7bits TRISBbits //TRIS register of the D7 pin

#define TRIS_D7_pin TRISB7 //D7 pin of the TRIS register

#define LAT_D7bits LATBbits //LAT register of the D7 pin

#define LAT_D7_pin LATB7 //D7 pin of the LAT register

void lcd_send (uint8_t value, uint8_t rs); //Send any data to the LCD

void lcd_command (uint8_t command); //Send the command to the LCD

void lcd_data (uint8_t data); //Send the character to the LCD

void lcd_init (uint8_t cursor, uint8_t blink); //Initialize the LCD

void lcd_clear (void); //Clear the LCD

void lcd_write_string (char *s); //Write the string at the LCD

void lcd_write_number (int32_t number, uint8_t dp_pos);//Write the number at the LCD

void lcd_set_cursor (uint8_t x, uint8_t y); //Set cursor at the specified position

void lcd_create_char (uint8_t addr, uint8_t *data);//Create a custom character

As you can see, it’s much longer than in tutorial 18, mainly because of the macro definitions of the ports and pins.

In lines 3-6 we define the TRIS (line 3) and LAT (line 5) registers of the E pin of the LCD, and the bits of these registers (lines 4 and 6, correspondingly). As we’re not going to read the data from the LCD, we don’t need to define PORT or ANSEL registers.

Then we define the same registers for RS pin (lines 8-11), D4 pin (lines 13-16), D5 pin (lines 18-21), D6 pin (lines 23-26), and D7 pin (lines 28-31).

In lines 33-41 there are function declarations. The most of them are the same as in tutorial 18, but there are some new ones:

  • lcd_clear() - clears the LCD and sets the cursor at position (1;1).
  • lcd_write_string() - the same as lcd_write() from the tutorial 18, it accepts the parameter s of char* type and displays it on the LCD. I decided to rename it because of the next function, not to mix them up.
  • lcd_write_number() - displays the number given as parameter number on the LCD, also shows the decimal point at the position given as parameter dp_pos from the end. If dp_pos is 0 then the decimal point is not shown.

Now let’s consider the source file “lcd_1602.c”.

#include <xc.h>

#include "lcd_1602.h"

#include "config.h"


void lcd_send (uint8_t value, uint8_t rs)//Send any data to LCD

{

LAT_RSbits.LAT_RS_pin = rs;//Set RS pin (data/command)

LAT_Ebits.LAT_E_pin = 1;//Set E pin high to start the pulse

LAT_D4bits.LAT_D4_pin = (value & 0x10) >> 4;//Assign bit#4 of the value to D4

LAT_D5bits.LAT_D5_pin = (value & 0x20) >> 5;//Assign bit#5 of the value to D5

LAT_D6bits.LAT_D6_pin = (value & 0x40) >> 6;//Assign bit#6 of the value to D6

LAT_D7bits.LAT_D7_pin = (value & 0x80) >> 7;//Assign bit#7 of the value to D7

__delay_us(1); //Delay needed by the driver

LAT_Ebits.LAT_E_pin = 0;//Set E pin low to finish the pulse

__delay_us(1); //Delay needed by the driver

LAT_Ebits.LAT_E_pin = 1;//Set E pin high to start the pulse

LAT_D4bits.LAT_D4_pin = (value & 0x01);//Assign bit#0 of the value to D4

LAT_D5bits.LAT_D5_pin = (value & 0x02) >> 1;//Assign bit#1 of the value to D5

LAT_D6bits.LAT_D6_pin = (value & 0x04) >> 2;//Assign bit#2 of the value to D6

LAT_D7bits.LAT_D7_pin = (value & 0x08) >> 3;//Assign bit#3 of the value to D7

__delay_us(1); //Delay needed by the driver

LAT_Ebits.LAT_E_pin = 0;//Set E pin low to finish the pulse

__delay_us(40); //Delay more than 37 us to implement the command

}


void lcd_command (uint8_t command)//Send command to LCD

{

lcd_send(command, 0); //Issue lcd_send function with RS = 0

}


void lcd_data (uint8_t data)//Send command to LCD

{

lcd_send(data, 1); //Issue lcd_send function with RS = 1

}


void lcd_init (uint8_t cursor, uint8_t blink) //Initialize LCD

{

TRIS_Ebits.TRIS_E_pin = 0; //Configure E pin as output

TRIS_RSbits.TRIS_RS_pin = 0; //Configure RS pin as output

TRIS_D4bits.TRIS_D4_pin = 0; //Configure D4 pin as output

TRIS_D5bits.TRIS_D5_pin = 0; //Configure D5 pin as output

TRIS_D6bits.TRIS_D6_pin = 0; //Configure D6 pin as output

TRIS_D7bits.TRIS_D7_pin = 0; //Configure D7 pin as output


LAT_Ebits.LAT_E_pin = 0; //Set E pin low

LAT_RSbits.LAT_RS_pin = 0;//Set RS pin low

LAT_D4bits.LAT_D4_pin = 0;//Set D4 pin low

LAT_D5bits.LAT_D5_pin = 0;//Set D5 pin low

LAT_D6bits.LAT_D6_pin = 0;//Set D6 pin low

LAT_D7bits.LAT_D7_pin = 0;//Set D7 pin low


lcd_command(0x30); //Try to use 8-bit interface

__delay_us(4200); //Delay for command implementation

lcd_command(0x30); //Try 8-bit interface one more time

lcd_command(0x28); //Set 4-bit interface, two lines

lcd_command(0x08); //Turn off the display

lcd_command(0x01); //Clear the display and reset the address to 0

__delay_us(4200); //Delay for command implementation

lcd_command(0x06); //Cursor move direction from left to right

lcd_command(0x0C | (cursor << 1) | blink); //Turn on display, set the cursor and blinking parameters

}


void lcd_clear (void) //Clear the LCD

{

lcd_command(0x01); //Clear the display and reset the address to 0

__delay_us(4200); //Delay for command implementation

}


void lcd_write_string (char *s) //Write the string at the LCD

{

uint8_t i = 0; //Characters counter

while (s[i]) //While s[i] is not 0 (end of the string is C)

{

lcd_data(s[i]); //Send the character to LCD

i ++; //Increment the characters counter

}

}


void lcd_write_number (int32_t number, uint8_t dp_pos) //Write the number at the LCD

{

uint8_t length = 0; //Number length

int32_t temp; //Temporary value to calculate the number length

uint32_t mult = 1; //Decimal multiplier

if (number == 0) //If number is 0

{

lcd_data('0'); //Then write 0

return; //And return from the function

}

else if (number < 0) //If number is negative

{

lcd_data('-'); //Then write minus sign

number = -number; //And negate the number value

}


temp = number; //Assign number to the temp

while (temp) //While temp value is not 0

{

temp /= 10; //Divide temp by 10

length ++; //Increment the number length

mult *= 10; //Multiply the decimal multiplier by 10

}

for (uint8_t i = 0; i < length; i ++) //Loop to display the number

{

if (dp_pos == (length - i))//If position of the decimal point is (length -i)

lcd_data('.'); //Then write dot

mult /= 10; //Divide decimal multiplier by 10

lcd_data(number / mult + 0x30);//Write the upper number at the LCD

number = number % mult;//Calculate the modulo of division of number by mult

}

}


void lcd_set_cursor (uint8_t x, uint8_t y) //Set cursor at the specified position

{

if (y > 2) //If we try to set the line number more than 2

y = 2; //Set the line #2

if (x > 16) //If we try to set the position number more than 16

x = 16; //Set the position #16

lcd_command(0x80 | ((y - 1) << 6) | (x - 1)); //Set the cursor position at the LCD

}


void lcd_create_char (uint8_t addr, uint8_t *data) //Create a custom character

{

if (addr > 7) //If the address is higher than 7

addr = 7; //Then set address as 7

lcd_command (0x40 | addr << 3); //Set the address of the CGRAM

for (uint8_t i = 0; i < 8; i ++) //Loop to send all 8 bytes

{

lcd_data (data[i]); //Send data to LCD

}

}

You may notice that some functions remain the same, namely lcd_command (lines 26-29), lcd_data (lines 31-34), lcd_write_string (lines 69-77)(previously called lcd_write), lcd_set_cursor (lines 112-119), and lcd_create_char (lines 121-130), so we will not consider them here, please refer to tutorial 18 for their description.

Let’s start with the function lcd_send (lines 5-24). As you should already know, this is the basic function that implements the low-level communication between the LCD and the MCU. It implements the same as the lcd_send function from tutorial 18 but has some differences. As you can see, we now replaced all standard registers and bits names with the ones defined in file “lcd_1602.h”.

In line 7, we set the correct level of the RS pin of the LCD to transmit data or command.

In line 8, we set the E pin of the LCD high to start the pulse.

In lines 9-12 we set the right level on the D4-D7 pins. Let’s consider these lines in more detail.

As you know, if we use a 4-bit interface, we first transmit the upper four bits of the data byte, so pin D4 corresponds to bit #4, pin D5 corresponds to bit #5, etc.

So in line 9 we check the fourth bit of the value by implementing the bitwise AND operation between the value and the 0x10 constant (0b00010000 in the binary system). The LAT_D4bits.LAT_D4_pin variable can accept two values: 0 or 1, so if the fourth bit of the value is 1, we need to shift it to the right by 4 bits to have 1 in the bit#0.

Then we do the same with bits #5-#7 (lines 10-12).

Next, we implement 1us delay to make sure all data pins have stabilized (line 13), then set the E pin low (line 14) to finish the pulse and latch the data. Then we implement another 1us delay (line 15) to make sure the data has been read by LCD.

Then we start another E pulse (line 16), send the lower four bits of the value (lines 17-20), implement another 1us delay (line 21), finish the E pulse to latch the data again (line 22), and wait for 40 us to implement the command (line 23).

As you can see, setting the data pins is not as elegant as in tutorial 18 but here we have more flexibility, as the connection of the D4-D7 pins can be totally random.

In function lcd_init (lines 36-61) the second part (lines 52-60) is the same as in tutorial 18, where we sent the commands to initialize the LCD. In the first part of the function we configure E, RS, and D4-D7 pins as outputs (lines 38-43), and then set them all low (lines 45-50). This also simplifies the initialization of the LCD, as you don’t need to take care of the MCU pins to which the LCD is connected, after you have defined them in the “lcd_1602.h” file.

The lcd_clear function (lines 63-67) is quite simple. We just send the “Clear display” command 0x01 (line 65) and wait for more than 4.1 ms (line 66) to let the LCD implement the command.

The lcd_write_number function (lines 79-110) is quite complex and tangled. Maybe it can be implemented in a simpler way but this is what I came up with. Let’s now try to understand what this function does.

First, we declare three variables:

  • length (line 81) which is the number of digits of the number parameter. The initial value of it should be 0.
  • temp (line 82) is the temporary auxiliary variable which we will use to calculate the length of the number.
  • mult (line 83) is the multiplier which has the look 10n, and will be used to divide the number by 10n at every iteration. It doesn’t sound clear now, but I’ll explain it later. The initial value of the mult variable should be 1.

First, we check if the number is 0 (line 84). In this case we don’t need to do any further calculations, we just display “0” at the LCD (line 86) and return from the function (line 87).

Then we check if the number is negative (line 89). In this case we display the minus sign at the LCD (line 91) and negate the number value to simplify the operations with it (line 92).

Now, we need to calculate the number length. First, we assign the number value to the temp variable (line 95). Then we implement the loop while the temp value is not 0 (line 96). In this loop we divide the temp by 10 at each iteration (line 98), increment the length value (line 99), and multiply the mult value by 10 (line 100).

Let’s consider an example of implementation of this loop when the number is 23509 for instance:

Iteration number

number

temp

length

mult

0

23509

23509

0

1

1

23509

2350

1

10

2

23509

235

2

100

3

23509

23

3

1000

4

23509

2

4

10000

5

23509

0

5

100000


Iteration #0 stands for the initial value of the variables.

As you see, the number value does not change as it is not used in the loop. The temp value is divided by 10 until it becomes 0 at iteration #5. Now, the length value is 5 which is correct because the 23509 number has exactly 5 digits. With the mult value it’s more complicated. Its last value is 100000, but to split the number by digits we need to divide it by 10000 first to get the first digit 2 of the 23509 number. So we will need to divide the mult by 10 prior to using it later.

Now, as we know the length of the number, we can implement another loop (lines 102-109) to display all the digits of the number one by one.

First we check the position of the decimal point dp_pos from the least significant digit (line 104). If the dp_pos matches with the (length - i) value we display the dot character (line 105). Please note, that if dp_pos is 0 the decimal point will not be displayed because the expression (length - i) never reaches 0, as the maximum i value is length - 1, and when it becomes length, the loop just ends.

Then we divide the mult value by 10 (line 106): as I said before, we need to do it prior to using it.

Next, we send to the LCD a strange value (number / mult + 0x30) (line 107). Let’s try to understand it. As I mentioned before, the mult value has the look of 10n, so dividing the number by mult just leaves the most significant digit of the number. But we need to send to the LCD not just a digit but the ASCII character that corresponds to the digit. If you look at the ASCII table (for example, here https://www.asciitable.com/), you can see that the digits 0-9 have ASCII codes 0x30-0x39. So to convert the digit into the ASCII character we just need to add 0x30 to it.

Finally, we recalculate the number as the modulo of division of the number by mult (line 108).

Let’s now see what’s happening inside this loop when we want to display the number 235.09 (number = 23509, dp_pos = 2)

for (uint8_t i = 0; i < length; i ++) //Loop to display the number

{

if (dp_pos == (length - i))//If position of the decimal point is (length -i)

lcd_data('.'); //Then write dot

mult /= 10; //Divide decimal multiplier by 10

lcd_data(number / mult + 0x30);//Write the upper number at the LCD

number = number % mult;//Calculate the modulo of division of number by mult

}

i

Line number

number

mult

number / mult

length - i

LCD view

0

105

23509

100000

-

5

106

23509

10000

-

5

107

23509

10000

2

5

2

108

3509

10000

-

5

2

1

105

3509

10000

-

4

2

106

3509

1000

-

4

2

107

3509

1000

3

4

23

108

509

1000

-

4

23

2

105

509

1000

-

3

23

106

509

100

-

3

23

107

509

100

5

3

235

108

9

100

-

3

235

3

105

9

100

-

2

235.

106

9

10

-

2

235.

107

9

10

0

2

235.0

108

9

10

-

3

235.0

4

105

9

10

-

1

235.0

106

9

1

-

1

235.0

107

9

1

9

1

235.09

108

0

1

-

1

235.09

Well, that’s all about how to display any integer number at the LCD. I also want to note that the number has the type int32_t, so you can display the values from -2147483648 to 2147483647 which seems to be quite enough taking into account that each LCD line can consist of just up to 16 characters.

Let’s now finally switch to the “main.c” file in which the most interesting things happen.

#include <xc.h>

#include "config.h"

#include "ds18b20.h"

#include "lcd_1602.h"


#define DEBOUNCE 3 //Button debounce, x10ms

#define MIN_TEMP 250 //Minimal temperature is 25 degrees

#define MAX_TEMP 650 //Maximum temperature is 65 degrees


int16_t temp_cur; //Temperature received from the DS18B20 sensor

int16_t temp_set; //Set temperature

int32_t x[3]; //Error signal

int32_t y; //Control impact (0-100%)

uint8_t mode; //Control mode: 0 - manual; 1 - PID

uint32_t tick; //10 ms counter

uint8_t previous; //Previous state of the button

uint32_t lastPress; //Time of the buttons last press

uint8_t bit0, bit1; //Bits of Gray code read from sensor

uint8_t prev_enc_val, new_enc_val;//Previous and new encoder relative values (0..3)

uint32_t seconds; //Seconds counter


const uint16_t h = 1; //Time step is 1s

const int32_t Kp = 200; //Proportional factor

const int32_t Ki = 10; //Integration factor

const int32_t Kd_inv = 200; //Inverted derivative factor (1 / Kd)


void timer0_overflow_isr (void) //Timer0 overflow interrupt subroutine

{

INTCONbits.TMR0IF = 0; //Reset the timer0 interrupt flag

TMR0H = 0x63; //Set the Timer initial value to get the interrupts

TMR0L = 0xBF; //every 10ms

tick ++; //increment the tick value

if ((tick % 100) == 0) //Every 100 ticks

seconds ++; //increment the seconds

}


void ext_int_isr (void) //External interrupts INT1 and INT2 subroutine

{

int8_t step; //Encoder step (+1 or -1)

bit1 = PORTCbits.RC1; //Read the upper bit of the Gray code

bit0 = PORTCbits.RC2; //Read the lower bit of the Gray code

new_enc_val = (bit1 << 1) + (bit0 ^ bit1);//Conversion from Gray to binary code

step = new_enc_val - prev_enc_val; //calculate the encoder step

if (step == -3) step = 1;//If got difference 0-3 (overflow, we made four steps) then step is 1

if (step == 3) step = -1;//If dot difference 3-0 (overflow, we made four steps) then step is -1

prev_enc_val = new_enc_val;//Save new encoder value as previous

if (new_enc_val == 3) //If encoder is in the stall position

{

if (step == 1) //If the direction is clockwise

{

if (mode == 0) //If mode is manual

{

if (y < 1000) //If control impact is less than 100.0

y += 10; //Then increase it by 1.0%

}

else //If mode is automated

{

if (temp_set < MAX_TEMP)//If temperature is less than max value

temp_set += 10;//then increase it by 1.0 degree

}

}

if (step == -1) //If the direction is counterclockwise

{

if (mode == 0) //If mode is manual

{

if (y > 0) //If control impact is bigger than 0%

y -= 10; //Then decrease it by 1.0%

}

else //If mode is automated

{

if (temp_set > MIN_TEMP)//If temperature is bigger than min value

temp_set -= 10;//then decrease it by 1.0 degree

}

}

}

INTCON3bits.INT1F = 0; //Reset the INT1 flag

INTCON3bits.INT2F = 0; //Reset the INT2 flag

}

void __interrupt() INTERRUPT_InterruptManagerHigh (void) //High-priority interrupts

{

if ((INTCONbits.TMR0IE == 1) && (INTCONbits.TMR0IF == 1)) //If Timer0 interrupt is enabled and Timer0 interrupt flag is set

{

timer0_overflow_isr(); //Invoke the interrupt subroutine

}

}


void __interrupt(low_priority) INTERRUPT_InterruptManagerLow (void) //Low-priority interrupts

{

if ((INTCON3bits.INT1E == 1) && (INTCON3bits.INT1F == 1)) //If INT1 is enabled and INT1 flag is set

{

ext_int_isr(); //Invoke the interrupt subroutine

INTCON2bits.INTEDG1 ^= 1;//Change the interrupt edge

}

if ((INTCON3bits.INT2E == 1) && (INTCON3bits.INT2F == 1)) //If INT2 is enabled and INT2 flag is set

{

ext_int_isr(); //Invoke the interrupt subroutine

INTCON2bits.INTEDG2 ^= 1;//Change the interrupt edge

}

}


void lcd_refresh (void) //Update the LCD information

{

lcd_set_cursor(1, 1); //Set the cursor at position 1;1

lcd_write_string("Tc="); //Write text "Tc="

lcd_write_number(temp_cur, 1);//Write the current temperature

lcd_write_string("C Ts="); //Write text "C Ts="

lcd_write_number(temp_set / 10, 0);//Write the set temperature

lcd_write_string("C "); //Write text "C "

lcd_set_cursor(1, 2); //Set cursor at position 1;2

lcd_write_string("Y="); //Write text "Y="

lcd_write_number(y, 1); //Write the control impact value

lcd_write_string("% "); //Write text "% "

lcd_set_cursor(9, 2); //Set the cursor at position 9;2

lcd_write_string("t="); //Write text "t="

lcd_write_number(seconds, 0);//Write the seconds

lcd_set_cursor(16, 2); //Set the cursor at position 16;2

if (mode == 0) //If mode is manual

lcd_write_string("M"); //Then write "M"

else //If mode is automated

lcd_write_string("A"); //Then write "A"

}


void PID_control (void) //PID control algorithm

{

x[2] = x[1]; //Save x1 into x2

x[1] = x[0]; //Save x0 into x1

x[0] = temp_set - temp_cur; //Calculate new value of x0

y = y + (Kp * (x[0] - x[1]) + (x[1] * h) * Ki + ((x[0] - 2 * x[1] + x[2])) / (h * Kd_inv));//Calculate new value of control impact

if (y < 0) //If control impact is less than 0

y = 0; //Then set it as 0

if (y > 1000) //If control impact is more than 100.0

y = 1000; //Then set it as 100.0

}


void main(void)

{

//GPIO configure

TRISCbits.RC5 = 0; //Configure RC5 (CCP1, P1A) as output

TRISAbits.RA5 = 1; //Configure RA5 pins as input

WPUAbits.WPUA5 = 1; //Enable pull-up resistor on pin PA5

TRISCbits.RC1 = 1; //Configure RC1 pins as input

TRISCbits.RC2 = 1; //Configure RC2 pins as input

ANSELbits.ANS5 = 0; //Enable digital buffer on RC1 pin

ANSELbits.ANS6 = 0; //Enable digital buffer on RC2 pin

INTCON2bits.nRABPU = 0; //Enable pull-up resistors at ports A and B


ow_configure(); //Configure 1-wire bus pin


//Oscillator module configuration

OSCCONbits.IRCF = 6; //Set CPU frequency as 8 MHz

OSCTUNEbits.SPLLEN = 1; //Enable PLL


//Timer 0 configuration

TMR0H = 0x63; //Set the Timer initial value to get the interrupts

TMR0L = 0xBF; //every 10ms

T0CONbits.PSA = 0; //Prescaler is assigned

T0CONbits.T0PS = 0; //Prescaler 1:2

T0CONbits.T08BIT = 0; //16-bit mode

T0CONbits.T0CS = 0; //Internal instruction cycle clock

T0CONbits.TMR0ON = 1; //Timer0 is enabled


//Timer 2 configuration

T2CONbits.T2OUTPS = 0b1111;//Postscaler 1:16

T2CONbits.T2CKPS = 0b10;//Prescaler 1:16

T2CONbits.TMR2ON = 1; //Timer2 is enabled


//ECCP module configuration

CCP1CONbits.CCP1M = 0b1100;//PWM mode; P1A-P1D active high

CCP1CONbits.DC1B = 0; //Clear the two LSB of the PWM duty cycle

CCP1CONbits.P1M = 0; //Single PWM output

PSTRCONbits.STRA = 1; //PWM is applied to P1A pin

CCPR1L = 0x00; //PWM value is 0


//Interrupts configuration

INTCON2bits.INTEDG1 = 0;//Interrupt INT1 on falling edge

INTCON2bits.INTEDG2 = 0;//Interrupt INT2 on falling edge

INTCON3bits.INT1IF = 0; //Clear INT1 flag

INTCON3bits.INT2IF = 0; //Clear INT2 flag

INTCON3bits.INT1IP = 0; //INT1 low priority

INTCON3bits.INT2IP = 0; //INT2 low priority

INTCON3bits.INT1IE = 1; //Enable INT1 interrupt

INTCON3bits.INT2IE = 1; //Enable INT2 interrupt

INTCONbits.T0IF = 0; //Clear Timer0 overflow interrupt flag

INTCON2bits.TMR0IP = 1; //Timer0 overflow interrupt high priority

INTCONbits.TMR0IE = 1; //Enable Timer0 overflow interrupt

RCONbits.IPEN = 1; //Enable priority level on interrupts

INTCONbits.GIEH = 1; //Enable global high-priority interrupts

INTCONbits.GIEL = 1; //Enable global low-priority interrupts


lcd_init(0, 0); //Initialize the LCD without cursor and blinking

bit1 = PORTCbits.RC1; //Read the upper bit of the Gray code

bit0 = PORTCbits.RC2; //Read the lower bit of the Gray code

prev_enc_val = (bit1 << 1) + (bit0 ^ bit1);//Conversion from Gray to binary code

temp_set = MIN_TEMP; //Set temperature to minimal value

y = 0; //PID controller output is 0

mode = 0; //Mode is manual

x[1] = 0; //Set x1 as 0

x[2] = 0; //Set x2 as 0


while (1) //Main loop of the program

{

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

convert_t(); //Start temperature conversion

if ((tick % 100) == 90)//When modulo of tick/100 is 90

{

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

if (mode == 1) //If mode is automated

PID_control();//Then calculate control impact by PID algorithm

CCPR1L = y >> 2;//Copy the upper 8 bits of the y into CCPR1L

CCP1CONbits.DC1B = y & 0x03;//Copy the lower 2 bits of the y into DC1B

lcd_refresh(); //Update the LCD

}


if (((tick - lastPress) > DEBOUNCE)) //If the time between the last press and the current time is bigger than the DEBOUNCE

{

lastPress = tick; //Set the last press time as the current time

if ((PORTAbits.RA5 == 0) && (previous != 0)) //If the current button state is low and previous state is high

{

previous = 0; //We reset the previous state to 0 to prevent implementation of this branch the next time

mode ^= 1; //Toddle the mode value

}

else if ((PORTAbits.RA5 != 0) && (previous == 0)) //If the current button state is high and previous state is low

{

previous = 1; //Then we set the previous state as 1 to prevent implementation of this branch the next time

}

}

}

}

Well, the “main.c” file is quite long, and it doesn’t have any repeating parts like in previous long programs. Let’s start from the very beginning.

In lines 1-4 we include the “xc.h” header (line 1) and all header files that we have created (lines 2-4).

In line 6, we define the macro DEBOUNCE which represents the debounce delay for the encoder button. We set its value as 3 because we will count the 10ms intervals.

In lines 7 and 8 we define the MIN_TEMP and MAX_TEMP macros, respectively, in 0.1 centigrades. So the values 250 and 650 correspond to temperatures 25.0 and 65.0 degrees. The MIN_TEMP value is the environment temperature, and as we have the Spring here now, it’s 25 degrees here, and you probably will have another value. The MAX_TEMP is the maximum temperature of the plant. This value is experimental, so you most likely will have a different one. I’ll explain how to get it in the third part of the tutorial.

In lines 10-20 we define a lot of global variables.

The temp_cur (line 10) is the current actual value of the temperature measured by the DS18B20 sensor. The temp_set (line 11) is the temperature setpoint which is set with the encoder in automated mode.

x (line 12) is the array of three values of the error signal (see Figure 1 and equation (11) in the previous portion of this tutorial). As follows from equation (11) we need to know the value of the error signal on three consequent steps, that’s why x has three elements.

y (line 13) is the control impact in 0.1% increments, so its value can vary from 0 to 1000. This value will be either set manually using the encoder in the manual mode, or calculated according to the equation (11) in the automated mode.

The mode (line 14) sets the operation mode of the controller. If it’s 0 then the mode is manual, and if it’s 1 then the mode is automated.

The tick (line 15) is the 10ms intervals counter. Nothing new here, we used these tick-s in a lot of previous projects.

The previous (line 16) and lastPress (line 17) variables are used for non-blocking reading of the encoder button. We have already met such variables in tutorial 8.

The bit0 and bit1 variables (line 18) represent the lower and the upper bits of the two-bits Gray code produced by the encoder.

The previous_enc_val and new_enc_val (line 19) are the values of the encoder outputs after conversion from the Gray code into the binary one, on previous step, and on current step.

The seconds (line 20) is the seconds counter. The only purpose of this variable is to be displayed on the LCD.

In lines 22-25 we define the constants which represent the PID controller parameters: step h (line 22), proportional factor Kp (line 23), integration factor Ki (line 24), and derivative factor Kd_inv (line 25). The values of these factors depend on the plant parameters, and I’ll tell you how to find them in the next part. One may ask, why the derivative factor is inverted while the others are not. The answer is simple: the value of Kd is just 0.005, and to get rid of the floating point calculations we just invert it, and instead of multiplying by 0.005 (according to equation (11)) we will divide by 200.

Now let’s skip the auxiliary functions and consider the main function of the program (lines 135-228).

In the initialization part (lines 137-198) we, as usual. configure the MCU modules and set the initial values of the variables.

First, we configure the RC5 pin as output (line 138), as it will be used as the PWM output. Then we configure the RA5 pin, to which the encoder button is connected, as input (line 139), and enable the pull-up resistor on it (line 140).

In lines 141-142 we configure RC1 and RC2 pins as inputs. We will further use them to generate the INT1 and INT2 interrupts. In lines 143-144 we disable the analog input buffers and enable the digital input buffer on these pins.

Also, we enable the pull-up resistors on ports A and B (line 145). Actually this will affect only pin RA5.

In line 147 we call the ow_configure() function to initialize the pin to which the DS18B20 sensor is connected.

In lines 150-151 we set the CPU frequency as 32 MHz.

In lines 154-160 we initialize the Timer0 to generate interrupts every 10ms. First, we assign the initial values to the TMR0H and TMR0L registers to get the timer period of exactly 10ms. I’ll explain these values a bit later.

In line 156 we assign the prescaler to the Timer0 input, then set the prescaler value as 1:2 (line 157), set the timer resolution as 16 bit (line 158), set the timer input clock as Fosc/4 (line 159), and finally enable the timer (line 160).

Let’s calculate the period of one timer tick. The Fosc frequency is 32MHz, so the Timer0 input frequency before the prescaler is 32 / 4 = 8MHz. After the prescaler the actual timer frequency is 8 / 2 = 4 MHz. So each timer tick is 1 / 4 MHz = 0.25us. To get the 10ms period we need 10ms / 0.25us = 40000 pulses. The maximum Timer0 value for 16 bit resolution is 65535. So we need to set the initial value of the timer as 65535 - 40000 = 25535 to get the 10 ms period. Let’s now split the 25535 into the higher and lower bytes:

TMR0H = 25535 / 256 = 99 or 0x63;

TMR0L = 25535 - 99 x 256 = 191 or 0xBF.

These are exactly those values that we assign in lines 154-155.

In lines 163-165 we configure the Timer2: set the postscaler as 1:16 (line 163), set prescaler as 1:16 (line 164) and enable the Timer2 (line 165). Actually, we might not configure the postscaler as it’s not applicable when the Timer2 is used to generate the PWM signal like in our case. Let’s calculate the timer period. So the input frequency of the timer prescaler is 32 / 4 = 8 MHz. After the prescaler it is 8 / 16 = 0.5 MHz. So each timer tick is 1 / 0.5MHz = 2us. And the Timer2 period is 2 x 256 = 512 us.

In lines 168-172 we configure the ECCP module in the PWM mode. First, we set the PWM mode where the P1A pin is active high (see table 1 from the previous part) (line 168). Actually we could set the mode CCP1CONbits.CCP1M = 0b1101, in our case it’s all the same. Then we clear the two LSBs of the PWM duty cycle DC1B (line 169). Next, we set the single output mode (line 170) and apply the PWM output to the P1A (RC5) pin by setting the corresponding bit of the PWM steering register (line 171). And finally, we reset the CCPR1L register, so the PWM duty cycle is now 0, and the RC5 output is always low (line 172).

In lines 175-188 we configure the interrupts. First, we configure external interrupts INT1 and INT2: set the interrupt edge as falling (lines 175-176) because in any direction the encoder’s first pulse will be from high to low, then clear the INT1 and INT2 interrupt flags (lines 177-178), set the interrupts priority as low (lines 179-180), and enable the interrupts (lines 181-182). Next, we configure the Timer0 overflow interrupt: clear interrupt flag (line 183), set the Timer0 interrupt priority as high (line 184), and enable the interrupt (line 185).

In line 186 we enable the priority levels on the interrupt. In our program, the Timer0 overflow interrupt is more important than the external interrupts, because a lot of program parts are dependent on the timer ticks, and the external interrupts are only used by the encoder. The difference between the high-priority and the low-priority interrupts is that the first one can interrupt the interrupt subroutine of the second one, but not vice versa.

In lines 187-188 we enable the global high-priority and low-priority interrupts, respectively.

In line 190 we initialize the LCD without cursor and blinking.

In lines 191-192 we read the states of the RC1 and RC2 pins to which the encoder outputs are connected and save them into the bit1 and bit0 variables. These variables represent the upper and the lower bits of the 2-bit Gray code.

In line 193 we convert the Gray code into the regular binary code. I didn’t invent this formula, I took the algorithm presented in Wikipedia and simplified it to use with 2 bits. We save this binary value into the prev_enc_val variable. We will consider later how it is used, and now let’s see how the conversion from Gray to binary code works.

Value

bit1

bit0

bit1 << 1

bit0 ^ bit1

Binary code

0

0

0

00

0

00

1

0

1

00

1

01

2

1

1

10

0

10

3

1

0

10

1

11

As you see, our formula works correctly.

In lines 194-199 we set the initial values of the variables that are related to the PID controller.

In line 194, we set the setpoint of the temperature temp_set as the minimum possible temperature value MIN_TEMP.

In line 195, we set the control impact y as 0.

In line 196, we assign 0 to the mode variable, to start in the manual mode.

In lines 197-198, we set the initial values of the error signals on the previous steps x[1] and x[2] as 0.

In lines 200-227 there is the main loop of the program.

In line 202 we check if the modulo of the division of the tick by 100 is 0. As tick increments every 10ms, this check becomes true every second, and in this case we send the “convert_t” command to the DS18B20 sensor (line 203).

In line 204 we check if the modulo of the division of the tick by 100 is 90. This check also becomes true every 1 second but it’s shifted to the previous one by 900ms which are required to calculate the temperature value. So if this condition is true, we implement several actions:

  • Read the temperature from the sensor and save it into the temp_cur variable (line 206).
  • If mode is automated (line 207) we calculate the new control impact value with the PID algorithm (line 208) by calling the PID_control() function which is located in lines 123-133
  • In line 209 we save the upper 8 bits of the y into the CCPR1L value, and in line 210 we save the other two LSBs of the y into the DC1B bits of the CCP1CON register. Let’s consider why this is possible. So I already mentioned that y represents the control impact in 0.1% increments, and changes in the range of 0-1000. The maximum possible value of the 10-bit PWM register is 1024. So if we ignore the values of 1001-1023, we can do the assignments made in lines 209-210. Thus, you need to remember that we can increase the maximum PWM value by another 2.3% but this will need additional calculations.
  • Finally, in line 211 we update the LCD view by calling the lcd_refresh function, which is located in lines 101-121.

In lines 214-226 there is the non-blocking button processing algorithm which I described in detail in tutorial 8. So here I will just consider what is done when the button is pressed. Its only payload is line 220 where we toggle the mode value to switch between the manual and automated mode.

The rest of the functions are implemented inside the functions and interrupt subroutines. Let’s now consider the lcd_refresh function (lines 101-121). As I said before, in it the LCD view is updated every second. Let’s first see the LCD view and consider what is shown in which position (Figure 8).

LCD view PIC18
Figure 8 - LCD View

First we set the cursor to the coordinates [1;1] (line 103), then we write the text “Tc =” (pos. 1, line 104), which means the “current temperature”.

Next we write the value of the current temperature temp_cur with the decimal point after the first digit from the end (pos. 2, line 105), as temp_cur represents the temperature in 0.1C.

Then we write one more static text “C Ts=” (pos. 3, line 106), where “C” stands for centigrades, and “Ts” stands for “temperature setpoint”.

In pos. 4 (line 107) we display the setpoint of the temperature temp_set divided by 10 because we don’t need the fractional part of it. Finally, we write the text “C ” (pos. 5, line 108). The space after the “C” is needed to erase the next character in case the temperature value has only one digit.

Now, we set the cursor to coordinates [1;2] (line 109) and display the text “Y=” (pos. 6, line 110), which stands for the “control impact y”. Then we write the y value itself with one fractional digit (pos. 7, line 111). In pos 8 (line 112) we write “% “. The spaces after the percent are needed to erase the unnecessary characters when the length of the y value changes.

Then we set the cursor to coordinates [9;2] (line 113) to always display the next text in the same position regardless of the length of the y value. There we write text “t=” (pos. 9, line 114), which stands for the “time”. Next we write the seconds value (pos. 10, line 115).

Finally, we set the cursor to coordinates [16;2] (line 116) and display the mode letter (pos. 11): if mode is 0 (line 117) then we write “M” (line 118) which stands for “manual”, and if mode is 1 (line 119) we write “A” (line 120) which stands for “automated”.

Now, let’s consider the PID_control function (lines 123-133) which is short but very important. First, we reassign the error signal values from the previous steps to the next one: x[2] = x[1] (line 125), x[1] = x[0] (line 126), then calculate the error signal at the current step (line 127). Now, as we have all required data, we can calculate the control impact y according to equation (11) from the previous part (line 128):

There is only one difference because we want to get rid of the floating point numbers, so instead of Kd we use the Kd_inv:

Using floating point calculations in the 8-bit MCUs requires a lot of the memory and CPU resources as these numbers are emulated and not supported natively. I first wrote this program using floating point calculations and the code size spent about 53% of the flash memory. And when I got rid of it, the size was reduced to 27%!

After we have calculated the y value, we need to limit its minimal and maximum values to keep it in limits [0…1000] (lines 129-132).

Now let’s consider the last part of the program - interrupts subroutines. As you remember, we have two interrupts priorities: high and low. So we need two interrupt subroutines: one for high-priority which is located at lines 79-85, and another for low-priority which is located at lines 87-99. Please pay attention to the syntax of the titles of these subroutines. For the high-priority interrupts it’s the same as we got used to. We just need to write the service word “__interrupt()” (line 79). For the low-priority interrupts we also need to add the parameter “low_priority” (line 87). The name of the subroutines still can be anything.

In the high-priority interrupt subroutine we check if the Timer0 overflow interrupt is enabled, and if its interrupt flag is set (line 81). In this case we invoke the function “timer0_overflow_isr” (line 83) which is located in lines 27-35. Let’s consider it in more detail.

Actually it’s very similar to one that we did in tutorial 8 but a bit more expanded. First, we reset the interrupt flag, not to be stuck in this function forever (line 29). Then we set the initial values of the registers TMR0H (line 30) and TMR0L (line 31), about which I talked earlier when describing the initialization of the Timer0. Next, we increment the tick value (line 33), and if the modulo of the division of tick by 100 is 0 (which happens one time per second) we increment the seconds value (line 34).

In the low-priority interrupt subroutine we first check if the INT1 interrupt is enabled, and if its interrupt flag is set (line 89). In this case we invoke the ext_int_isr function (line 91) and change the interrupt edge for the INT1 interrupt (line 92). Then we check if the INT2 interrupt is enabled, and if its interrupt flag is set (line 94). In this case we invoke the same ext_int_isr function (line 96) and change the interrupt edge for the INT2 interrupt (line 97). Let’s consider the ext_int_isr function, which is located at lines 37-78, it’s quite interesting.

In line 39, we declare the variable step which represents the encoder’s step, or changing the state. It can be either 1 if the encoder handle moves clockwise, or -1 otherwise. Lines 40-41 are the same as lines 191-192, here we read the encoder current state and save it into variables bit1 and bit0. Line 42 is also very similar to line 193, we also convert the encoder’s Gray code into the regular binary code but unlike line 193, in line 42 we save the result into the new_enc_val variable which is the encoder state on the current step.

In line 43 we calculate the step value as a difference between the current encoder state new_enc_val and the previous encoder state prev_enc_val.

In line 44 we check if the step is -3, this may happen when the new_enc_val is 0, and the prev_enc_val is 3. In this case the step should be positive because transition from 3 (0b11) to 0 (0b00) in one step can happen only in the clockwise direction. So in this case we set the step as 1.

In line 45 we check the opposite case: if the step is 3. This happens when the new_enc_val is 3, and the prev_enc_val is 0, which may occur only in the counterclockwise direction, so step should be -1.

In line 46 we assign the new_enc_val to the prev_enc_val because in the next step the current state will become previous.

In lines 47-75 we process the new encoder state. In line 47 we check if the current state is 3. As I mentioned in the previous part, the normal state of the current encoder is when both its pins are not connected to ground and are high, and each encoder “click” during rotation means changing for consequent states. So if the new_enc_val is 3, this means that the encoder is in the stall position, and we can do what we need from it.

First, we check if the step is 1 (line 49). If yes, then we check the mode variable (line 51). If it is 0 (manual mode), then we increase the y value by 10 (line 54) if it’s smaller than 1000 (line 53). Otherwise (in automated mode) (line 56) we increment the temp_set value by 10 (line 59) if it’s smaller than MAX_TEMP (line 58).

Next, we check if the step is -1 (line 62). If yes, then we check the mode variable again (line 64). If it is 0 (manual mode), then we decrease the y value by 10 (line 67) if it’s greater than 0 (line 66). Otherwise (in automated mode) (line 69) we decrement the temp_set value by 10 (line 72) if it’s greater than MIN_TEMP (line 71).

Finally, in lines 76 and 77 we reset the interrupt flags of the interrupts INT1 and INT2, respectively.

And that’s all about the program code. As you see, it’s quite long but even though it spends just 4512 bytes of the program memory even without the optimization, and with the “s” optimization it reduces to just 3680 bytes, or 22% of the overall memory.

Now, we can proceed to the last part of the tutorial, in which we will make the experimental work with our PID control system.

Make Bread with our CircuitBread Toaster!

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

What are you looking for?