FB pixel

IR-Controlled Car with Servo Motors Using MCC | Embedded C Programming - Part 33

Published


Hi there! In this tutorial, we will blow off the dust from the SMARS car, which we actively used in the PIC10F200 tutorials series (Bluetooth Controlled Robot, Line Following Car, Obstacle Avoidance Robot). Here we will use this car without any changes, which means it is still driven by the hacked servo motors whose shaft can revolve freely. You can use the ready-to-go servos with 360-degree rotation, but they are more expensive and less widespread, and hacking regular motors is relatively easy. This tutorial also describes it in depth: “How to make a 360-degree Continuous Rotation Servo Motor?” so it’s up to you what to choose.

Unlike the tutorials mentioned above, we will control the car with an IR remote this time. You can use almost any remote which supports the NEC protocol. I doubt if a smartTV or air conditioner still uses this protocol but any simple remote from an Arduino kit (Figure 1) or light control (Figure 2) will work well.

Figure 1 - IR remote from Arduino sets
Figure 1 - IR remote from Arduino sets
Figure 2 - RGB IR remote
Figure 2 - RGB IR remote

Also, you can test available remotes to see if any of them produces the required signal. I’ll talk about this in detail later. And we also dealt with the IR remotes in the PIC10F200 series (Infrared RGB LED controller) if you’d like to review those.

So, to refresh your memory, you can refer to these tutorials. If you missed the PIC10F200 series, I will repeat a lot of the required information here, so it isn’t necessary to go and review those, though it would certainly be helpful.

Servo Motors

The appearance of the SG90 servo is shown in figure 3. As you can see, it consists of the servo itself and 3 changeable attachments, which can be attached to the servo’s shaft and fixed with a screw.

The servo has 3 wires; usually, they are orange, red, and brown. They mean the following - red and brown wires for the power supply (plus 5V and ground, respectively), while the orange wire is the control.

Figure 3 - The look of the SG90 servo
Figure 3 - The look of the SG90 servo

Control of the servo motor position is performed by means of the PWM. In the SG90 datasheet, you can find the following picture (Figure 4).

Figure 4 - Servo motor control
Figure 4 - Servo motor control

The position of the servo’s shaft is set by changing the pulse duration between 1 and 2 milliseconds, where 1 ms corresponds to the very far left position and 2 ms corresponds to the very far right position. Thus, 1.5 ms will put the shaft in the middle position. For 360-degree rotating servos, the 1.5 ms pulse corresponds to the stall position of the shaft, the pulse in the range of 1..1.5 ms will rotate the shaft in one direction, and the pulse in the range of 1.5…2 ms will rotate the shaft in another direction. The more the deviation of the pulse width from 1.5 ms, the higher is the rotation speed.

In Figure 4, you can see that the PWM period is 50 Hz, but from my experience, it’s the minimum period, so you can just set the stall time as 20 ms regardless of the pulse duration, meaning your actual period could be anywhere from 21 milliseconds to 22 milliseconds. Also, you don’t need to always send the pulse; you can send them just when you want to rotate the shaft to the given position and then disable the PWM.

However! You should know that in this case, the servo’s shaft will be released, and someone can rotate it manually relatively easily. But if you keep sending the pulses, the torque will still be applied to the shaft, and you will more likely break the gear rather than move it manually. So it depends on your task and requirements whether you should continuously send the modulated signal or just send it and then disable the PWM.

Car Chassis

The last thing you will need is the chassis, where you will assemble all the parts. If you are lucky enough to have a 3D printer, I recommend making this cool robot called SMARS. It was designed to be used with DC motors, but you can find the chassis and wheels version for the SG90 servos here and print it instead of the original one. All other parts can be taken from the original design.

The fully assembled robot is shown in Figure 5.

Figure 5 - SMARS robot
Figure 5 - SMARS robot

The “head” and the “tail” are not needed for this but they make the car look prettier.

If you don’t have a 3D printer, you can buy a ready-made chassis on eBay or AliExpress (search “Arduino car chassis”), or even cut it by yourself of plywood, acrylic, or textolite.

Infrared NEC Protocol

There are plenty of IR protocols, many of which are very different: they use different coding schemes, timings, and packet formats. We will consider one of the earliest and the most widespread protocols which was designed by NEC, and thus it’s called “NEC protocol”.

The packet consists of the preamble followed by 4 data bytes: address, inverted address, command, and data command (see Figure 6).

Figure 6 - Packet format of the NEC protocol
Figure 6 - Packet format of the NEC protocol

If you keep the button pressed for some time, only the first packet looks like the one shown in Figure 6. The other packets are shorter and follow with a period of 110 ms (see Figure 7).

Figure 7 - Repeated packets sequence
Figure 7 - Repeated packets sequence

The bits are coded by the length of the pause (1.6ms for ‘1’ and 0.56ms for ‘0’) between pulses of the fixed length (0.56ms), see Figure 8.

Figure 8 - Bits coding in the NEC protocol
Figure 8 - Bits coding in the NEC protocol

On a physical level, the IR signal represents the modulated infrared beam emitted by the IR diode. The carrying frequency for the NEC protocol is 38 kHz. So the remote modulates the pulses with this frequency and sends it off. The IR receiver consists of the demodulator, which removes the carrying frequency and leaves just the initial pulses (Figure 9).

Figure 9 - Modulated and demodulated IR signals
Figure 9 - Modulated and demodulated IR signals

This modulation-demodulation process is used to increase the noise immunity of the IR channel, as there are a lot of the IR sources around us, and they all can cause false pulses.

IR Receiver

I recommend using the TL1838 (VS1838) IR receiver (Figure 10). It’s prevalent, cheap, and easy to use. As you see in Figure 10, it has just 3 pins: VCC, GND, and SIGNAL. The supply voltage is 2.7 - 6.0 V. This receiver has the in-built IR decoder, which means that there are ready-to-use pulses on the SIGNAL pin, so all we need to do is to directly connect this pin to one of the GPIOs of the microcontroller and measure the duration of the pulses to decode the IR packet.

Figure 10 - 1838 IR receiver
Figure 10 - 1838 IR receiver

RA and RB port change interrupt

Now that we have all the required information about the hardware we will use in this tutorial, let’s briefly consider the new module we will use- RA and RB port change interrupt.

We are already familiar with one type of the external interrupt - INTx, where x is the interrupt number from 0 to 2. We used these interrupts in the PID controller tutorial. Today we will familiarize ourselves with another type.

Each INTx interrupt has its own flag and is masked individually, while RA and RB port change interrupt has one flag for all pins. So if any of the allowed pins of port RA or RB changes its state, the interrupt (if unmasked) will be triggered. Also for INTx interrupt we could select the edge at which it will be triggered, and RA and RB port change interrupt is triggered at both high-to-low and low-to-high edges, and you can’t configure it to be specific.

These interrupts don’t belong to the peripherals, so to enable them you only need to enable the GIE bit, and don’t need to enable the PEIE bit (this is separate from the individual interrupts masking bits).

IOCA and IOCB registers allow the specific pins to generate the RA and RB Port Change interrupt. These registers are configured in the “Pin Module” tab of the MCC, in the last column called “IOC”. If some bit of these registers is 1 (the checkbox in the “IOC” column is checked) then the corresponding pin can be the source of the interrupt, and if a bit is 0 then changes of the pin state won’t trigger the interrupt.

And that’s everything we need to know about these interrupts. You just need to remember that even if you configure the interrupts, you still need to configure your pin as an input to make it work properly.

Schematic Diagram

We’re done with the theoretical part, now we can switch to the schematic diagram of the device which is shown in Figure 11.

Figure 11 - Schematic diagram with the PIC18F14K50 with 1602 character LCD, servo motors and IR receiver
Figure 11 - Schematic diagram with the PIC18F14K50 with 1602 character LCD, servo motors and IR receiver

This time the schematic diagram is quite complex, but some familiar components still exist. The “heart” of the device is still the PIC18F14K50 MCU to which the PICkit programmer (X1), 1602 character LCD (X2), two servo motors (X3 and X4), and the IR receiver TL1838 (X5) are connected. The LCD is the same as we used in the previous tutorials, and its connection is also the same. Actually, the LCD is not needed for the working car, and we will remove it in the final installation. Its purpose is only to show the codes of the commands which are received via IR from the remote. If you already know these codes from some other source (for example, from here), you do not need to use the LCD, and you can skip its part in the program code. Otherwise, welcome onboard!

Two SG90 servos (X3 and X4) are connected to pins RC4 and RC5 of the MCU. This connection is not random as these pins are merged with the PWM outputs of the ECCP (enhanced capture-compare-PWM) module P1A and P1B.

The TL1838 IR receiver is connected to the RA5 pin of the MCU. We will use the pin change interrupt to capture the time of the pulses , which any pin of the RA and RB ports can trigger. As you can see in Figure 11, there are few options: only RA5 or RA4 pins of these ports are free. Besides, the RA4 pin starts as the analog pin and requires more configuration (one more line of code, but still…), so we better use the RA5 pin.

Maybe you remember that I mentioned that I use the 3.3V version of the LCD and power the device from the PICkit programmer with the VCC = 3.25V. The servos require a voltage of 4.5 - 6V, so in working conditions, we need to remove the LCD to prevent its damage (again, only if you use the 3.3V version as I do. If you have the 5V version, it’s fine to leave it).

That’s everything about the device schematic diagram so we can proceed to the next step.

Configuration of the project using MCC

Let’s create a new project now. I’ve called it “IR_car_MCC” but you as usual can give it any name you want.

We must configure Timer1, Timer2 and ECCP1 modules with the MCC in this project. All of these modules are already familiar to us: Timer1 has been considered in tutorial 15, Timer2 and ECCP1 in PWM mode have been considered in tutorials 20 and 23.

So, as our project is already created, let’s run the MCC plugin, and change the MCU package. Open the System Module page, set the Internal Clock as 4MHz_HF, and leave the “PLL Enabled” checkbox unchecked. Also (as usual), don’t forget to remove the check from the “Low-voltage programming enable” field (Figure 12).

Figure 12 - System Module configuration
Figure 12 - System Module configuration

As you can notice, in this project we set the CPU clock frequency as 4 MHz instead of 32 MHz as we used to do in previous projects. I will explain why we changed this value later.

Then we need to go to the Device resources tab on the left part of the screen and add the TMR1, TMR2, and ECCP1 modules to the project. I will not provide detailed screenshots this time as we have already done these actions several times. After all, your Resource Management window should look like in Figure 13.

Figure 13 - Resource Management window
Figure 13 - Resource Management window

Before we start the configuration of the peripheral modules, let’s consider why we need each of them, and what are the limitations (and there are a lot of them, actually).

As you already understand, we will use Timer1 and Timer2 and the RA and RB Port change interrupt functionality. Besides, we will need the ECCP module in PWM mode. And here, the limitations of the PIC18F14K50 MCU begin. With the PIC10F200, we had no such limitations because we didn’t have any peripheral modules except for the very restricted timer/counter. And here, we would want to use the hardware modules wherever possible because they usually work better than the software emulation.

The first limitation is that the PIC18F14K50 MCU has only one ECCP module. So if we use it in the PWM mode, we can’t use it in the capture mode to automatically save signal change time at the IR sensor output. So we need to decide which mode to choose. The width of the pulses from the IR remote is relatively long (starting from 560 us, see Figure 8), and we don’t need a very high accuracy, unlike the distance meter we made in tutorial 15. So we will use the RA and RB Port change interrupt to detect the signal changes and simply read the Timer1 counting register value at this moment.

Speaking of Timer1, we will configure it with the tick period of 1 us to simplify the comparison of the read result with Figure 8. Also it will be used to figure out when the button in IR is released, and the car should be stopped.

Timer2 will be used to work with the ECCP module in PWM mode. And here we face another limitation. Even though the ECCP module can produce up to 4 PWM signals, they are either all the same or half are inverted compared to another half. To normally run the car, we need to produce the pulses with a width of 1-2 ms and a period of greater than 20 ms. And here, we get the third limitation. To get the period of 20 ms with the 8-bit timer, its clocking frequency should be 256 / 20 ms = 12.8 kHz. Even if we use the 1:16 prescaler (and as you remember, the postscaler isn’t applicable in the PWM mode because these bits are used to increase the PWM resolution up to 10 bits), the CPU frequency should be not higher than 12.8 x 16 = 204.8 kHz. OK, we can run the MCU at such a low frequency, but in this case, it will not be able to process the IR signal promptly and will miss some pulses.

It was a challenge for me to make it work with such limitations. But finally, I found the solution. To generate a signal similar to the one shown in Figure 4, we don’t need to have a period of 20 ms; moreover, we can’t get it to work with such conditions because we need two PWM signals, one of which can be either the same as the second one, or the same as the inverted second one. So I decided to set the PWM period to 3 ms. In this case, if I set the PWM pulse width as 50%, it will give the pulses of 1.5 ms at both PWM outputs, whether or not they are mutually inverted. Now, if I set the pulse width at about 60%, the pulse width will be 1.8 ms, and the inverted pulse width will be 3 - 1.8 = 1.2 ms. The deviation from the average value of 1.5 ms for the first motor will be 1.8 - 1.5 = 0.3 ms, and 1.2 - 1.5 = -0.3 ms for the second motor. This makes the motors spin in different directions but at the same speed, which is exactly what we need! But how do we obtain the period of 20+ ms like in Figure 4? Well, that’s quite simple! We enable the PWM outputs not all the time but during one Timer2 period of seven. So the full generated period will be 3 ms x 7 = 21 ms, which suits us. And the pulse width will be less than 2 ms because the pulse will be output only during the first Timer2 period of seven, and during the other six periods, the output signal on both pins will be 0.

It’s probably unclear now, but I hope it will become more apparent after considering the program code.

So, let’s open the TMR1 window and configure the Timer1 module according to Figure 14.

Figure 14 - Timer1 configuration
Figure 14 - Timer1 configuration

As you can see, we leave the clock source as FOSC/4. As the CPU frequency is 4 MHz (see Figure 12) then Timer1 clocking frequency will be 4 MHz / 4 = 1 MHz. This gives us the period of each timer’s tick as 1 / 1 MHz = 1 us, which is our goal, as I already mentioned. As we can’t use the ECCP module to capture the timer's current counting register value, we need to do this manually, so it would be a good idea to enable 16-bit read functionality, to read both upper and lower counting registers at once.

Also, we enable Timer1 interrupt. As I said, we will use this timer to check when the button in the IR remote is released, and the car should be stopped. Inside the interrupt handler we will just count the 65.536 ms periods (see Figure 14), and if the number of these periods exceeds the given value, we stop the car. I’ll talk about it in more detail when we consider the program code.

The other settings of the timer can remain unchanged.

Next, let’s open the TMR2 window and configure the Timer2 module according to Figure 15.

Figure 15 - Timer2 configuration
Figure 15 - Timer2 configuration

First, we set the prescaler as 1:16 to be able to set the timer’s period as 3 ms. As you can see, with this prescaler value maximum Timer2 period is 4.096 ms. This value is easy to prove. The timer’s clock source is FOSC/4, and the clocking frequency is 1 MHz. With the prescaler of 1:16 the timer’s frequency will be 1 000 000 / 16 = 62500 Hz, and each tick will take 1 / 62500 Hz = 16 us. Timer2 is a 8-bit timer, and its maximum period is 16 us x 256 = 4096 us, or 4.096 ms which we see in Figure 15.

Also we need to enable Timer2 interrupt, as in its handler we will either enable the output PWM pins or disable them to generate the pulses with the period of 21 ms.

Here we can’t use the postscaler, as if Timer2 is used to generate PWM, the postscaler bits are used to expand the PWM resolution up to 10 bits, and thus are not applicable.

Next, we need to configure the ECCP1 module according to Figure 16.

Figure 16 - ECCP module configuration
Figure 16 - ECCP module configuration

Here we do a lot of changes. First, we need to set the ECCP mode as “Enhanced PWM”. The only available timer in this mode is Timer2, so we leave this field as is. Next, we set the PWM duty cycle as 60%. I already provided the calculation why we set this value. Even though we use two PWM outputs, we leave the “Enhanced PWM mode” field as “single”. The “PWM pin polarity” field can remain unchanged for now as we will change it in the program code as needed.

We need to set the checkbox “Enable Steering” to be able to provide the PWM pulses to several pins simultaneously.

We will use pins P1A and P1B (see Figure 11), so we need to set the checkboxes “PxA” and “PxB” to provide PWM to these pins.

As for the field “PWM steering occurs on the” “start_at_next”, this is the option that I have selected empirically. It makes the output steering changes applied only on the next period of Timer2. Without setting it, the changes are applied immediately which can produce a very short (several microseconds) pulse, which nevertheless totally messes up the servo control. So selecting this option is vital for our application.

Now, let’s switch to the Pin Module and Pin Manager and configure all required pins according to the schematic diagram (Figure 17).

Figure 17 - Pin Module configuration
Figure 17 - Pin Module configuration

Here we can see the familiar LCD pins D4, D5, D6, D7, RS, and E which are connected the same as in several previous tutorials, to the pins RB4, RB5, RB6, RB7, RC3, and RC6 of the MCU, respectively.

Pins RC4 and RC5 are controlled by the ECCP1 module and are used to produce the PWM pulses, as we have configured them recently (Figure 16).

As for the pin RA5, we give it the custom name “IR” as to this pin the IR sensor is connected (see Figure 11). We need to configure it as an input without pull-up resistors, because the Tl1838 sensor has the push-pull type of the output. Also, you may notice that in the column “IOC” we change the value for this pin from “none” to “any”. This enables the interrupt generation when the RA5 pin changes its state both from low to high and from high to low.

But we need to make sure that the port RA and RB pin change interrupt is enabled. To do this we need to switch to the Interrupt Module tab and make sure that the TMR1, TMR2, and Pin Module interrupts are enabled (Figure 18).

Figure 18 - Interrupt Module
Figure 18 - Interrupt Module

There is no need to enable the priorities of the interrupts as we don’t need a very high accuracy, it won’t hurt if timer interrupt is processed in the competing conditions before the pin change interrupt and vice versa.

Now everything is configured, and we can click the “Generate” button and switch to code writing.

Program Code Description

The same as in previous tutorials, we will need to copy and paste the “lcd_1602_mcc.h” and “lcd_1602_mcc.c”. One can take them from tutorial 23 or 25, as I showed you. After all, your project should look like in Figure 18.

Figure 18 - Project structure with all required files
Figure 18 - Project structure with all required files

If you compare the schematic diagrams (Figure 18) in the current tutorial and Tutorial 23 (or later), you will notice that the connection of the display is equal, so if you took the LCD-related files from them, you don’t need to make any changes to them. And if you copied them from one of the previous tutorials, please remember to change the pin assignment; otherwise, your display won’t work properly.

Now let’s consider the “main.c” file and write the following code.

#include "mcc_generated_files/mcc.h"

#include "lcd_1602_mcc.h"

#define PWM_VALUE 450 //The value loaded into PWM register

volatile uint8_t pulse_count; //Counter of the pulses received via IR

volatile uint16_t timestamp[70];//Timer1 values when the pulses are received

uint8_t ir_data[4]; //4 bytes received via IR

uint8_t long_packet_valid; //Indicates that the long packet received via IR is valid

uint8_t short_packet_valid; //Indicates that the short packet received via IR is valid

uint8_t timer2_ticks; //Number of Timer2 ticks, required for producing the servo control signals

volatile uint8_t movement_allow;//Allows the movement of the car

volatile uint8_t timer1_ticks; //Number of ticks of Timer1, needed to stop the movement when no packets are received via IR

void ca5_isr_handler (void) //Handler of the RA5 pin change interrupt

{

timestamp[pulse_count] = TMR1_ReadTimer();//Read the Timer1 value into the current timestamp element

pulse_count ++; //Increment the number of received pulses

volatile uint8_t dummy = PORTA; //Read the values of ports A and B

dummy = PORTB; //to prevent triggering this interrupt again

}

void timer2_handler (void) //Handler of the Timer2 overflow interrupt

{

PSTRCONbits.STRA = 0; //Disable PWM steering at pin P1A

PSTRCONbits.STRB = 0; //Disable PWM steering at pin P1B

timer2_ticks ++; //Increment the timer2_ticks variable

if (timer2_ticks == 7) //If timer2_ticks is 7 (30 ms x 7 = 210 ms)

{

if (movement_allow) //If movement is allowed

{

switch (ir_data[2]) //Check the command byte received via IR

{

case 64: CCP1CONbits.CCP1M = 0x0D; break; //Forward (P1A - active high, P1B - active low)

case 9: CCP1CONbits.CCP1M = 0x0C; break; //Right (P1A and P1B active high)

case 7: CCP1CONbits.CCP1M = 0x0F; break; //Left (P1A and P1B active low)

case 25: CCP1CONbits.CCP1M = 0x0E; break; //Backward (P1A - active low, P1B - active high)

}

PSTRCONbits.STRA = 1;//Enable PWM steering at pin P1A

PSTRCONbits.STRB = 1;//Enable PWM steering at pin P1B

}

timer2_ticks = 0; //Reset the timer2_ticks counter

}

}

void timer1_handler (void) //Handler of the Timer1 overflow interrupt

{

timer1_ticks ++; //Increment the timer1_ticks variable

if (timer1_ticks >= 3) //If timer1_ticks greater or equal to 3

movement_allow = 0; //then disable the car movement

}

void parse_code (void) //Function to parse the code received via IR

{

short_packet_valid = 0; //Reset the short_packet_valid flag

long_packet_valid = 0; //Reset the long_packet_valid flag

for (uint8_t i = 0; i < pulse_count - 1; i ++)//In this loop we calculate the duration of the pulses

{

timestamp[i] = timestamp[i + 1] - timestamp[i];//by subtracting the neighbor values of the timestamp array

if ((int16_t)timestamp[i] < 0) //If the obtained value is negative (which means Timer1 overflow)

timestamp[i] += 65536; //then we add 65536 to the result

}

if ((timestamp[0] > 8500) && (timestamp[0] < 9500)) //Check if the first pulse is about 9 ms (9000 us)

{

if ((timestamp[1] > 2000) && (timestamp[1] < 2500)) //If the pause after the first pulse is about 2.25 ms

short_packet_valid = 1; //then it's the short packet

else if ((timestamp[1] > 4000) && (timestamp[1] < 5000)) //If the pause after the first pulse is about 4.5 ms

{ //then it's the start of the long packet

for (uint8_t i = 0; i < 4; i ++) //Clear all four elements

ir_data[i] = 0; //of the ir_data array

for (uint8_t i = 0; i < 32; i ++) //We expect to receive 4 bytes which is 32 bits

{

if ((timestamp[i * 2 + 2] > 400) && (timestamp[i * 2 + 2] < 700)) //If the pulse is about 560 us

{

if ((timestamp[i * 2 + 3] > 400) && (timestamp[i * 2 + 3] < 700)) //If the pause is also about 560 us (which corresponds to 0)

{

ir_data[i / 8] >>= 1; //then we just shift the current ir_data element to the right

}

else if ((timestamp[i * 2 + 3] > 1400) && (timestamp[i * 2 + 3] < 1900))//If the pause is about 1700 us (which corresponds to 1)

{

ir_data[i / 8] >>= 1; //then we shift the current ir_data element to the tight

ir_data[i / 8] |= 0x80; //and add 1 to the MSB of it

}

else return; //If the pause is wrong then we leave the function immediately

}

else return; //If the pulse is wrong then we leave the function immediately

}

if (((ir_data[0] + ir_data[1]) == 255) && ((ir_data[2] + ir_data[3]) == 255))//If the first byte is the same as negated second byte, and if the third byte is the same as the negated fourth byte

{

long_packet_valid = 1; //Then we set the long_packet_valid flag

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

for (uint8_t i = 0; i < 4; i ++)

{

lcd_write_number(ir_data[i], 0);//And write all four received bytes at the LCD

lcd_write_string(" ");

}

}

}

}

}

void main(void)

{

SYSTEM_Initialize(); // Initialize the device

IOCA5_SetInterruptHandler (ca5_isr_handler); //Set the RA5 pin change interrupt handler

TMR2_SetInterruptHandler (timer2_handler); //Set the Timer 2 interrupt handler

TMR1_SetInterruptHandler (timer1_handler); //Set the Timer 1 interrupt handler

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

lcd_clear(); //Clear the LCD

lcd_write_string("Ready"); //And type "Ready"

pulse_count = 0; //Reset the pulse_count variable

movement_allow = 0; //Reset the movement_allow flag

EPWM1_LoadDutyValue(PWM_VALUE); //Set the PWM duty value

INTERRUPT_GlobalInterruptEnable(); // Enable the Global Interrupts

INTERRUPT_PeripheralInterruptEnable(); // Enable the Peripheral Interrupts

while (1)

{

if (((TMR1_ReadTimer() - timestamp[pulse_count-1]) > 16000) && (pulse_count > 2)) //If time after the last pulse is more than 16 ms and the number of received pulses is greater than 2

{

parse_code(); //Then we consider this as the end of the packet and parse it

if (long_packet_valid || short_packet_valid) //If we received the long or short packet

{

movement_allow = 1; //Then set the movement_allow flag

timer1_ticks = 0; //And reset the timer_ticks variable

}

for (uint8_t i = 0; i < 70; i ++) //In any case we clear the timestamp array

timestamp[i] = 0;

pulse_count = 0; //and clear the pulse_count variable

}

}

}

In line 1, we, as usual, include the “mcc_generated_files/mcc.h” file to be able to use the MCC-generated variables, functions, and macros. In line 3, we include the “lcd_1602_mcc.h” file, which consists of the 1602 LCD-related functions.

In line 4, we define the PWM_VALUE macro as 450. This is the number of Timer2 ticks which corresponds to the duty cycle of 60% (see Figure 16, field “CCRP value”).

In lines 6-13, there are the variables declarations:

  • pulse_count (line 6) would be better to call “edge counter” as it counts the changes of the state of the IR sensor output. It counts both high-to-low and low-to-high transitions.
  • timestamp (line 7) is the array in which the time the state changes of the IR sensor output is saved. To measure this time, we will use Timer1.
  • ir_data (line 8) is the array in which the four bytes received via IR are stored (see Figure 6).
  • long_packet_valid (line 9) is the flag that becomes 1 if all the timings of the received long packet are valid and the format of the received bytes also corresponds to the NEC protocol.
  • short_packet_vlaid (line 10) is the flag that becomes 1 if the timings of the short (repeat) packet are valid (see Figure 7).
  • timer2_ticks (line 11) is the counter of the Timer2 overflows. It’s required to produce the correct signal to control the servo motors (see Figure 4).
  • movement_allow (line 12) is the flag that becomes 1 if the valid packet is received via IR and if the command is recognized (forward/backward/left/right).
  • timer1_ticks (line 13) is the counter of the Timer1 overflows. It’s required to stop the car after a certain time after receiving the last command.

You may notice that some variables have the modifier volatile. It’s used for those variables which are used both in a main loop and in the interrupts handlers. If you don’t put this modifier, the compiler will consider this as two different variables - one in the main loop and another in the interrupt handlers. As such, they will have different values, breaking the program’s logic. So using the volatile modifier in such cases is always a good idea. But don’t overuse it because it takes more time for the CPU to process the volatile variable.

In lines 15-21, there is the function ca5_isr_handler which is the interrupt handler of the RA5 pin change.

In lines 23-44, there is the function timer2_handler which is the Timer2 overflow interrupt handler.

In lines 46-51, there is the function timer1_handler which is the Timer1 overflow interrupt handler.

In lines 53-100, there is the parse_code function which parses the received packet. This is the most complex function of this program, and we will consider it later as well.

And finally, in lines 102-134, there is the main function.

The configuration part of the main function occupies lines 104-117.

In line 104, there is the MCC-generated function SYSTEM_Initialize which initializes all the hardware modules of the MCU. In lines 105-107, we define the interrupt handler functions for the RA and RB Port Change, Timer2 overflow, and Timer1 overflow interrupts, respectively. I already mentioned all of them earlier.

In line 108, we initialize the LCD. For some reason, at low CPU frequencies, we need to call the lcd_init function twice; otherwise, it doesn’t work in 4-bit mode and displays a total mess.

In line 109, we clear the LCD and then write “Ready” (line 110) to indicate that the device is ready to receive commands from the IR remote.

In lines 112 and 113, we clear the pulse_count and movement_allow variables, respectively.

In line 114, we load the PWM pulse width PWM_VALUE into the ECCP module using the MCC-generated function EPWM1_LoadDutyValue. We won’t need to change this value in the program anymore, we will just change the polarity of the pulses on the P1A and P1B pins.

In line 116, we enable all unmasked interrupts, and in line 117, we enable the peripheral interrupts. And here, the initialization part is over.

The program’s main loop occupies lines 119-133 and is relatively short.

First, we check if the difference between the current Timer1 value and the current timestamp element is greater than 16000 and if the pulse_count is greater than 2 (line 121). The timestamp value is obtained in the RA and RB port change interrupt handler as the current Timer1 value. But if there are no pulses, then the timestamp will not be updated anymore because the interrupt will not be triggered. That’s why we make this check in the program’s main loop. So if 16000 us (or 16 ms) has passed since the last pulse, we don’t expect any more pulses in this packet, and it’s completed and can be processed. The second condition about pulse_count > 2 is needed because if the number of pulses is less than 2, the packet can’t be complete.

So if these two conditions are met, we proceed to the packet processing. First, we invoke the parse_code function (line 123), inside which we check if the full or repeat packet has been received and if this packet is valid.

If one of the packet types is valid (line 124), we set the movement_allow flag to enable the moving of the car (line 126) and reset the timer1_ticks variable (line 127), which, as I already mentioned, is used to stop the car after some time after the last packet received.

Finally, regardless of the packet’s validity, we clear the timestamp array (lines 129-130) and reset the pulse_count variable (line 131).

That’s everything about the main function of the program. Now let’s consider the interrupt handler functions; some are quite interesting.

In lines 15-21, there is RA and RB port change interrupt handler ca5_isr_handler. It is invoked when the signal from the IR sensor changes. When this happens, we need to save the Timer1 value to calculate the pulses’ duration and decode the IR code. We do this in line 17, where we copy the value of the Timer1 counter register TMR1 into the element of the timestamp array with the index pulse_count. Then we increase the pulse_count value to save the next value in the next array element (line 18). Lines 19 and 20 are tricky. I mean, this is another unclear thing done in the PIC18F14K50 MCU. So if we just do the regular routine, such as doing something useful and then clearing the interrupt flag, then the RA and RB port change interrupt will be triggered again and again as if the flag is still set. Previously, I didn’t find the solution of this phenomena and used the INTx interrupts but this time I decided to beat it finally. In the data sheet of this MCU I found a small note (Figure 19).

Figure 19 - Note about solving the continuous RABIF flag setting
Figure 19 - Note about solving the continuous RABIF flag setting

So, as you see, we need to read the PORTA and PORTB registers to fix this, which we do in lines 19 and 20. As we don’t need these registers’ values, we just copy them into the dummy variable. I declared it with the volatile modifier to prevent its optimization by the compiler, as it also sees that even though we assign the variable, we still do nothing with it.

The timer2_handler (lines 23-44) is the function that controls the movement of the car in different directions, and there are also some tricks in it. I already explained the algorithm for controlling the car ,so let’s see how it’s implemented.

In lines 25 and 26, we disable the steering of the PWM signal at pins P1A and P1B, respectively, operating directly with the MCU registers as there is no corresponding MCC-generated function (you can read the PIC18F14K50 datasheet for more details). By doing this, we keep the P1A (RC5), and P1B (RC4) pins low regardless of the PWM duty cycle. Then we increment the timer2_ticks variable (line 27). In line 28, we check if the value of this variable reaches 7. This is what I talked about earlier. Six Timer2 periods (which, as you remember, are 3 ms), we keep both RC4 and RC5 pins low. And during the seventh period, we can send the pulses to these pins if the remote receives the right command.

So we check if the movement_allow variable is set (line 30). We have already considered that this variable becomes 1 if we have received the full or repeat packet via IR. If so, we check what command we have received (line 32). We will return to the distinction and displaying of the commands later, and now we are at the point where we know the codes of the remote buttons from somewhere. In Figure. 6, the IR packet consists of four bytes: address, inverted address, command, and inverted command. The first two bytes are the same for the remote, so we need to check only the command byte, which is stored in the second element of the ir_data array.

In our program, we will distinguish four commands: to move the car forward or backward, turn left or turn right. I have the remote shown in Figure 1, the most left one, and I will use the following buttons:

“+” - forward;
“-” - backward;
“⏭” - turn right;
“⏮” - turn left.

So the lines 34-37 may be different in your code depending on the remote and the car chassis you use.

If the command “forward” has been received (line 34) we need to configure the RC4 and RC5 outputs in a way that makes the car move forward. The code of the “+” button in my remote is 64, your value may be different, and you can see it in the LCD.

To understand the expression CCP1CONbits.CCP1M = 0x0D we need to recall the PWM modes (Table 1 from Tutorial 20). I will repeat the information we are interested in here:

CCP1M3

CCP1M2

CCP1M1

CCP1M0

Mode

1

1

0

0

PWM mode: P1A, P1C active high; P1B, P1D active high

1

1

0

1

PWM mode: P1A, P1C active high; P1B, P1D active low

1

1

1

0

PWM mode: P1A, P1C active low; P1B, P1D active high

1

1

1

1

PWM mode: P1A, P1C active low; P1B, P1D active low

As you can see, the CCP1Mx bits set the active level of pins P1A-P1D. If we configure a pin as active high, then during the PWM_VALUE time, it will be high, and during the rest of the Timer2 period, it will be low, producing a pulse of 1.8 ms, as I explained earlier. If we configure it as active low, then its behavior will be vice versa: during the PWM_VALUE time, the pin will stay low, and then for the rest of the period, it will be high, and in this case, the pulse duration will be 1.12 ms.

Now, let’s look at the table and line 34 again—the expression CCP1CONbits.CCP1M = 0x0D means that we configure the P1A and P1B pins in the following way: “PWM mode: P1A, P1C active high; P1B, P1D active low”. So at pin P1A (RC5), we will have the long pulses (as it is active high), and at pin P1B (RC4), we will have short pulses (as it is active low), which will make servos spinning with the same speed but in the opposite directions. In my chassis, the motors are mounted so that their shafts should spin in the opposite direction to make the wheels spin in the same direction, which may be different in your setup.

The other lines (35-37) do the same but set the different PWM modes when the corresponding command has been received. When the “Right” command has come (line 35), which in my case has the code 9, then we configure P1A and P1B pins as follows: “PWM mode: P1A, P1C active high; P1B, P1D active high,” which will produce the identical long pulses at both PWM outputs and make the wheels spin in a different direction which turns the car to the right.

When the “Left” command is received (line 36), which has the code 7, then we configure P1A and P1B pins as follows: “PWM mode: P1A, P1C active low; P1B, P1D active low” which will produce the short pulses at both PWM outputs and make the wheels spin in an opposite direction to the “Right” command.

And finally, when the “Backward” command is received (line 37), code 25, then P1A and P1B are configured as “PWM mode: P1A, P1C active low; P1B, P1D active high” which will produce the pulses opposite the “Forward” command, and the car will move backward.

After configuration of the PWM mode we enable the output steering at both P1A and P1B pins (lines 39, 40). This enabling will become active only at the next period of Timer2 (remember the “PWM Steering occurs on the” “start_at_next” option in Figure 16?), and in such a way we can eliminate the short pulses that might appear if the changes are applied immediately.

Finally, we reset the timer2_ticks variable to start the whole algorithm from the very beginning (line 42).

So, let’s summarize this all. During the six periods of Timer2 (which gives 3 x 6 = 18 ms) both pins RC4 and RC5 remain low. If there is no valid packet received via IR during the seventh period they remain low leaving the servos alone and adding another 3 ms to the whole servo period. If we receive a valid command then during the seventh period of Timer2 either long (1.8 ms) or short (1.12 ms) pulse will be produced at pins RC4 and RC5 depending on the command code, after that the RC4 and RC5 pins will become low for another six Timer2 periods and so on and so forth.

If we receive the repeated packet, the last command will be repeated while we keep the button on the remote pressed. But how to stop the car now? We need to reset the movement_allow variable after we stop receiving the repeat packets from a remote.

And we do this in the Timer1 overflow interrupt handler timer1_handler (lines 46-51). This function is the most boring in this program. In line 48, we increment the timer1_ticks variable. As you remember, this variable is reset when a valid IR packet is received (line 127). Then, in line 49, we check if it is greater or equal than 3. If this happens, this means that since the last valid packet has passed 65536 us x 3 = 196 608 us or about 200 ms. So in 200 ms after the last received packet the car is stopped by resetting the movement_allow variable (line 50).

And finally, let’s consider the longest function of this program - parse_code (lines 53-100). It is invoked from the main function in 16 ms after the last activity on the RA5 pin (lines 121-123) and inside it we check if the received packet is valid, and what the type of the packet is.

First, we reset both packet validation flags: short_packet_valid (line 55) and long_packet_valid (line 56).

Next, we need to calculate the duration of the pulses and pauses. It’s quite simple to do this by subtracting the previous timestamp element from the current one. So this difference will give the time interval between the signal changes in microseconds (that’s why we configured Timer1 so that its tick takes 1 us). The result will be saved into the same array timestamp not to create new instances and save the RAM. So keep in mind that this array plays two roles: during receiving the packet it consists of the Timer1 values at the moments when the signal from IR changes; and after the parse_code function it consists of the durations of the pulses and pauses between them.

So, in line 57 we start the new loop from 0 to pulse_count -1. It ends with pulse_count - 1 instead of just pulse_count because for obvious reasons the number of pulses is less than the number of signal changes by 1. Inside this loop we subtract the value of the current timestamp element from the next one and save the result in the current element (line 59). It may happen that the current timestamp value was taken at the end of the current Timer1 period, and the next value was taken at the next Timer1 period. In this case the duration will be negative. We check this case in line 60. Please pay attention that we need to cast the type of the timestamp to the int16_t in order to compare it with 0 because the default type of it is uint16_t, and is always positive, so the condition would always be false without the type casting. So if this happens, we just add the Timer1 period (which is 65536) to the timestamp value (line 61), and then the result will be correct.

Now, as we have calculated all the pulses and pauses durations we can start checking the packet validity.

First, we check the first pulse. According to Figure 7, it should be 9 ms, or 9000 us. But IR sensors can have a deviation of the pulse width, also the signal edges can be smooth enough which also adds some error, and moreover, the frequency of the MCU is not stable as well, so there are a lot of sources of distortion of the pulse and pause durations. That’s why we check not the exact value of 9000 us but the range of ±5%…±10% from this value. In our case we check if the pulse width is greater than 8500 us and smaller than 9500 us (line 63). If this condition is met, we check the pause width. There can be three situations (Figure 7):

  • the pause is 2.25 ms which means the repeat packet;
  • the pause is 4.5 ms which means the beginning of the full packet;
  • the pause is different which means fault packet.

So first we check if the pause is in the range of 2…2.5 ms (line 65). If this is true then it’s the valid repeat packet, so we can set the flag short_packet_valid (line 66) and end the function.

Then we check if the pause is in the range of 4…5 ms (line 67). If it is true then it’s the beginning of the full packet, and we need to decode it according to Figure 7 and 8.

We clear all four elements of the ir_data array (lines 69, 70). Then we start the loop from 0 to 32 (line 71) to receive all 32 bits (or 4 bytes) of the packet according to Figure 6. Inside the loop we first check if the pulse width is in the range of 400…700 us (average value is 560 us according to Figure 8) (line 73). If the pulse width is valid we check the pause width. If it is in the same range (line 75) then this is the 0 bit (Figure 8), and we just shift the current ir_data element at one bit to the right (line 77). The shift direction is to the right because data is sent LSB first (Figure 6).

Then we check if the pause width is in the range of 1400…1900 us (average value is 1690 us) (line 79). If it is so, then we shift the current ir_data element at one bit to the right (line 81) and add 1 at the MSB of it (line 82).

If the pause width or pulse width are invalid then we immediately leave the function (lines 84, 86) leaving both short_packet_valid and long_packet_valid flags low.

I think I should now explain a bit more about the array indexes used in this loop. Timestamp has either index 2 * i + 2 for pulse or 2 * i + 3 for pause. Each bit is coded by one pulse and one pause, thus to transmit 32 bits we need 32 * 2 = 64 timestamp values, that’s why we multiply the i value by 2. As for the adding 2 or 3, this is also simple - the first pulse is the packet header (9 ms and 4.5 ms), so the first payload pulse has the index 2, and the first pause has the index 3.

ir_data array has the index i / 8. It’s also not difficult. The value of i represents the bits while ir_data elements are the bytes which, as you know, consist of 8 bits.

So, by line 87 we have already decoded the IR packet, and have the address, inverted address, command, and inverted command (Figure 6) in the ir_data array.

Finally, before stating that the packet is valid, we need to check if the first element of the array is the same as the inverted zero element, and if the third element is the same as the inverted second element. We do this in line 88. The simplest way to do this is to add the tested elements. If one of them is the same as the inverted other one, then their sum will be equal to 255, or 0xFF. Why is it so? In the inverted value all the 0 and 1 are swapped in comparison to the initial value. So when we sum them, each resulting bit will be either 1 + 0 or 0 + 1 which anyway gives 1. And all ones in the binary form are exactly 255 in decimal form or 0xFF in hexadecimal form.

So now line 88 should be clear. If this condition is met, we finally can set the long_packet_valid flag (line 90) indicating that we have received the valid packet. The next four lines are just for the information and are needed if you don’t know the codes of the buttons of the remote. We set the cursor at the first position (line 91) then write all four received bytes (line 94) separating them with the spaces (line 95).

And finally that’s everything about the program code. This is not the simplest program we had but we need to grow in our knowledge, so this is not surprising. Let’s now proceed to the testing of our device.

Testing of the IR-controlled Car

Let’s assemble the device according to Figure 11, compile and build the project and flash it into the PIC18F14K50 MCU. If everything is assembled and programmed correctly, you should see the “Ready” text on your LCD (well, if you use it at all). I will consider that you, like me, have some remote and don’t know the codes of the buttons.

Point the remote at the IR sensor and press the button whose code you want to know. If the remote has the NEC encoding, and if you connected everything correctly, you should see something like this in your LCD (Figure 20).

Figure 20 - Code of the pressed button
Figure 20 - Code of the pressed button

Here I pressed the button “+” on the remote. As you see the address byte is 0, and the inverted address is 255 which is correct. The command byte is 64 and the inverted command code is 191. If we sum these two values, we will get 64 + 191 = 255, which is also correct. Let me now show you what’s going on at the IR sensor output when this command comes with my logic analyzer (Figure 21).

Figure 21 - Waveform of the button “+” signal
Figure 21 - Waveform of the button “+” signal

As you can see, the signal can be decoded even visually: the long pause is 1, and the short pause is 0. Please note that the TL1838 sensor has a low active level, so the pulse level is “0” and the pause level is “1”.

Now you can see that after the heading long pulse there is the following sequence of zeros and ones: 00000000111111110000001011111101. Let’s group them by 8 into bytes and convert them into decimal system: 00000000 11111111 00000010 11111101

00000000b = 0
11111111b = 255
00000010b = 64
11111101b = 191

These results match the values in Figure 20 which is not surprising.

Now let’s see the output PWM signal which corresponds to this button. As I said before, the “+” button stands for the “Forward” signal, at which one motor should spin in one direction, and another in the opposite direction. The logic analyzer shows the following (Figure 22).

Figure 22 - PWM signals corresponding to the “Forward” command
Figure 22 - PWM signals corresponding to the “Forward” command

As you can see, everything is as I described before. The period of the pulses is about 21 ms (7 x 3 ms), the width of the long pulse is 1.871 ms (theoretical value is 1.8 ms), and the short pulse is 1.123 ms (theoretical value is 1.2 ms). Short and long pulses are shifted because they both are generated within one Timer2 period of 3 ms.

The “Backward” command has the same waveform, just the pulses are swapped (Figure 23).

Figure 23 - PWM signals corresponding to the “Backward” command
Figure 23 - PWM signals corresponding to the “Backward” command

The signals corresponding to the “Right” and “Left” commands are shown in Figure 24 and 25, respectively.

Figure 24 - PWM signals corresponding to the “Right” command
Figure 24 - PWM signals corresponding to the “Right” command
Figure 25 - PWM signals corresponding to the “Left” command
Figure 25 - PWM signals corresponding to the “Left” command

As you can see, in the last two cases the signals at both outputs are the same.

When you send some of the movement commands to the car you should see that its wheels are spinning in the right direction. If the direction is wrong, correct lines 35-38 of the code. If the motors don’t spin at all and just crackle, most likely there is not enough voltage to overcome the initial torque. My PICkit 3 doesn’t provide much current to the device so I had to use the external power supply like battery or USB port. Again, I’m reminding you that if you use the 3.3V LCD, you need to disconnect it before applying 5V to the car to prevent its damage.

And that’s everything I wanted to tell you in this tutorial. We didn’t learn much in terms of new MCU modules but we have studied how to decode IR signals from the remote. Probably in one of the next tutorials I will show you the opposite process - how to generate the IR signal for controlling some external devices.

As homework I suggest you make a tweak of the car. For now, it has the following flaw. After you release the button which is recognized by the car, you can press any button on the remote, and the car will keep repeating the last command. Fix this issue, so the car reacts only on four buttons and ignores the others.

Make Bread with our CircuitBread Toaster!

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

What are you looking for?