Measuring Distance With An Ultrasonic Sensor Using MCC | Embedded C Programming - Part 15
Published
Hi there! In this tutorial we keep talking about the HC-SR04 sensor based distance meter. In the previous tutorial we created it without the MCC module, and this time I’ll show you how to configure the required modules using the MCC. Also in this tutorial I’ll show you some new approaches to program code organization, so even if you don’t use the MCC, it will be useful for you to look through this tutorial.
The task for today is the same: to create a distance meter that will display the distance in centimeters in the 3-digit 7-segment LED indicator. If the distance is less than 1 meter, display it in format “xx.x”, otherwise it will be shown as “xxx”.
Let’s again recall what the HC-SR04 sensor is, if you missed its description in the previous tutorial. And again, if you are already familiar with it, just skip to the next section.
HC-SR04 Sensor Description
The HC-SR04 sensor is very widespread and is often included in Arduino sets. If you don’t have a set, you can buy it separately on Adafruit, Sparkfun, eBay or Aliexpress (the price is about $0.70-$1.00 from the cheaper locations).
Let’s briefly consider the principle of operation of this sensor module. As follows from its name, it is based on ultrasonic waves. These “eyes” in Figure 1 are in fact the ultrasonic transmitter and the ultrasonic receiver. The transmitter sends a short ultrasonic packet. If some obstacle appears in the way of the ultrasonic wave, it reflects and returns back to the sensor, where the receiver detects it. Knowing the speed of sound in air (which is about 340 m/s), and the time between sending and receiving the packet (time of flight or ToF), you can calculate the distance to the object (d):
We have to divide the result by two as the wave has to cover the distance twice - from the transmitter to the obstacle, and from the obstacle to the receiver.
Usually the ToF is very short, so it’s more convenient to use it in microseconds (us), and the distance is better to measure in cm, then we’ll have:
So, to calculate the distance to the obstacle we simply need to know the ToF and divide it by 58.
To understand how it’s implemented with the module, let’s consider Figure 2 which I copied from the data sheet of the HC-SR04 module.
As you can see from Figure 1, the module has four pins - VCC (5V), GND, Trig, and Echo.
VCC and GND are clear. The “Trig” is the trigger input of the module. You need to send a 10us pulse to this pin to start the ranging (see Figure 2). After that the module will send out an 8 cycle burst of ultrasound at 40 kHz and set its Echo output as high. When the receiver detects the reflected wave, the Echo goes low. Thus the Echo pulse width represents the ToF time which we need for calculating the distance. As the module simplifies the process, the only thing we need to focus on is measuring this pulse width, and converting it into the distance.
In the PIC10F200 MCU we checked the timer value at every loop iteration to get know when the pulse ends. In more advanced MCUs (to which PIC18F14K50 belongs) there is a special module which helps to precisely measure the time, which is called the ECCP (Enhanced Capture/Compare/PWM) module. This module works with either Timer1 or Timer3. We will use Timer1 this time, so let’s consider it in more detail.
Timer1 Module
We are already familiar with the Timer0 module from tutorial 9. I mentioned in it that there are four timers in the PIC18F14K50 MCU, and they all are different. So let’s now familiarize ourselves with Timer1.
It has something in common with Timer0 but also has some unique features which we will consider now:
- Software selectable operation as 16-bit timer or counter.
- Readable and writable 8-bit counting registers (TMR1H and TMR1L).
- Selectable internal or external clock source and Timer1 oscillator option.
- Interrupt-on-overflow.
- Reset on CCP Special Event Trigger.
So unlike Timer0, Timer1 works only in 16-bit mode. But like Timer0, it has readable and writable timer counter registers, can work both from internal and external pulses, has the prescaler, and can cause the interrupt on overflow.
The unique feature of this timer is that it has a separate oscillator with two external pins to which a 32768 Hz quartz can be connected. This oscillator can be used both for clocking Timer1 and for clocking the CPU. In this tutorial we will not consider external clocking in detail and focus on the internal clock source. If you want to know more about the Timer1, please feel free to read chapter 11 of the PIC18F14K50 datasheet.
Enhanced Capture/Compare/PWM (ECCP) Module
Well, this module is really enhanced, and its description can take several pages, but in fact the enhancements are only in PWM mode, capture and compare are quite ordinary. Today we will consider only the capture mode in detail.
It’s not a very common thing in 8-bit MCUs that the capture/compare module is separated from the timer module, so usually each timer has its own capture/compare module. But for some reason the PIC18F14K50 MCU designers decided to stand out and be unique.
So what this module is used for? It has a 16-bit register which operates as a capture or compare or PWM register. Let’s briefly consider every of these modes.
In capture mode if there is a certain change of a dedicated pin, the value from the timer register is automatically copied into the capture register, and an interrupt may be generated. After that the capture register can be read by software. This mode is useful for precise measurement of the time between events. And we will use exactly the capture mode in our program.
In compare mode you can write some 16-bit value into the compare register, and when the timer register reaches this value some events may happen, like: timer reset, pin change, interrupt generation, A/D conversion start. We will consider it in later tutorials.
In PWM mode, the value of the 16-bit register sets the PWM width that is generated on dedicated pins. This mode is very advanced, it can generate PWM pulses on up-to four pins with the configured polarity and dead-band for motor control. And again, we will discuss this mode in one of the next tutorials.
As for me, the disadvantage is that the PIC18F14K50 has only one such module. So for instance if you use the ECCP in capture mode, you can’t use it for PWM generation. But what can you do? We’ll deal with what we have.
To configure the ECCP module, the register CCP1CON is used. We will not consider the whole register as half of the bits are used to configure the PWM mode. There are bits CCP1M0-CCP1M3 that configure the mode of the module (table 1).
Table 1 - ECCP Module Operation Modes Configuration
CCP1M3 | CCP1M2 | CCP1M1 | CCP1M0 | Mode |
0 | 0 | 0 | 0 | Capture/Compare/PWM off (module reset) |
0 | 0 | 0 | 1 | Reserved |
0 | 0 | 1 | 0 | Compare mode, toggle output on match |
0 | 0 | 1 | 1 | Reserved |
0 | 1 | 0 | 0 | Capture mode, every falling edge |
0 | 1 | 0 | 1 | Capture mode, every rising edge |
0 | 1 | 1 | 0 | Capture mode, every 4th rising edge |
0 | 1 | 1 | 1 | Capture mode, every 16th rising edge |
1 | 0 | 0 | 0 | Compare mode, initialize CCP1 pin low, set output high on compare match (set CC1IPF bit) |
1 | 0 | 0 | 1 | Compare mode, initialize CCP1 pin high, set output low on compare match (set CC1IPF bit) |
1 | 0 | 1 | 0 | Compare mode, generate software interrupt only, CCP1 pin reverts the state |
1 | 0 | 1 | 1 | Compare mode, trigger special event (ECCP resets Timer1 or Timer3, start A/D conversion, set CC1IPF bit) |
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 high |
As you can see there are a variety of modes, but we don’t need the majority of them so far, so we’ll consider them later. For now, we are interested only in the capture modes which I highlighted with the green color in table 1.
As you see, the capture event may occur on every rising or falling edge, on every 4th or every 16th rising edge. In our application we will need the first two options: rising edge to detect the time of starting the pulse, and falling edge to detect the end of the pulse. Thus we will need to reconfigure the module after each detection.
All these changes are detected only on a dedicated pin called CCP1 which is merged with the RC5 pin (Figure 3). This pin should be configured manually as an input. If it’s configured as an output, changing the CCP1 state by the program code will cause the capture event, and this can be used as a trick if you want the capture to happen via firmware.
The ECCP module 16-bit register is called CCPR1 and contains two 8-bit registers: CCPR1H and CCPR1L. In these registers, the value of the timer register will be copied when the capture event happens.
As I mentioned before, the ECCP module is separated from the timers, and each mode works with the dedicated timer:
- Capture or Compare mode - Timer1 or Timer3
- PWM mode - Timer2
The ECCP module is described in more detail in the chapter 14 of the PIC18F14K50 datasheet.
I think that’s all you need to know about the Timer1 and the ECCP modules, and thus we can move forward and consider the schematics diagram of the device.
Schematics Diagram
The schematics diagram is shown in Figure 3.
This schematics diagram is very similar to the one presented in tutorial 13: it also consists of the PIC18F14K50 MCU (DD1) and 7-segment 3-digits LED indicator (7Seg1). There are some differences in their connection though. As I mentioned earlier, the RC5 pin is merged with the CCP1 pin which we will use to connect the ultrasonic sensor. So we need to free it. Previously the segment E was connected to the RC5 pin, and if we move it to another port, this will break the simplicity of the program, so it’s better to move the DP segment somewhere else from the RC0 pins and connect the E segment instead. This is what we do here. And the DP pin is now connected to the RA4 pin of the MCU.
The ultrasonic sensor HC-SR04 (U1) has four pins as I mentioned before. The VCC and GND pins are connected to the VDD and the VSS pins of the MCU, respectively. The TRIG pin to which the start pulse will be provided is connected to the RA5 pin, and the ECHO pin, where the echo pulse will be present, should be connected to the CCP1 (RC5) pin to measure the pulse width precisely.
That’s actually everything about the device schematics diagram so we can proceed to the programming part.
Configuration of the Project using MCC
In this project we need to configure Timer1 and ECCP modules with the MCC. So let’s create a new project, run the MCC plugin, and change the MCU package. Open the System Module page and set the Internal Clock as 16MHz_HF (Figure 4). Also, don’t forget to remove the check from the “Low-voltage programming enable” field.
Then we need to go to the Device resources tab in the left part of the screen, expand the drop-down list “Timer” and click on the green plus at “TMR1” (Figure 4).
Then expand the “ECCP” drop-down list and click on the green plus at the “ECCP1”. Then both modules will appear in the Project resources, and the new tabs will be opened in the main field (Figure 5).
Now, let’s open the TMR1 window and configure the Timer1 module according to Figure 6.
The fields marked with the green color should remain unchanged, and the fields marked with red frames should be set according to Figure 6:
- The clock source for the Timer1 is Fosc/4, so the input clock frequency for the timer module will be 16 / 4 = 4 MHz.
- Starts Timer1.
- As we’re not going to use the Timer1 oscillator, we need to disable it for power saving.
- We will not use the Timer1 overflow interrupt, so don’t need to enable it this time.
- We can just ignore this option as it is applicable only for the external clock. So the value of this option can be anything.
- We need to set the prescaler as 1:4. After it the input Timer1 frequency is 4 x 1:4 = 1 MHz. So each tick of the timer corresponds to 1 / 1MHz = 1us. This value is convenient for us to simplify the distance calculation.
- This option requires a more detailed description. 16-bit read/write mode allows you to read the whole Timer1 register at once. In this case, the TMR1L register is accessed directly, and the TMR1H register is read through the buffer into which the value of TMR1H is loaded once we read the TMR1L register. The same with writing: the upper byte is written to the buffer first, and is copied to the TMR1H register when we write to the TMH1L register. This guarantees that the timer register is updated (or read) at once, not in two steps. If you don’t need high precision, you can set the RD16 bit to 0, in this case both TMR1L and TMR1H registers will be available independently, and you can reload any of them at any time.
- This value sets the timer period. For our application, it’s better to have as long a period as possible to prevent the timer from overflowing during the echo pulse from the sensor. According to the datasheet of the HC-SR04 sensor the max range is 4 m. From this formula d[cm] = ToF[us] / 58 we can calculate the max ToF : ToF max = 58 x 4 x 100 = 23200 us = 23.2 us. So this value fits into the 65,536 ms period with a great margin.
That’s all configuration of the Timer1 required for the current application. Let’s now switch to the ECCP1 tab and configure the ECCP module (Figure 7).
There are fewer settings here, so let’s briefly consider them:
- As I mentioned before, we will use the ECCP module in the Compare mode. Depending on the selected mode, the options below will differ.
- As we will use the ECCP with the Timer1, we need to select the last in the drop-down list.
- The echo pulse from the HC-SR04 module starts with the rising edge, so we need to select this option for the detection of the start of the pulse.
- We need to enable the CCP interrupt because we will save the captured value inside this interrupt callback.
Let’s now switch to the Pin Module and Pin Manager and configure all required pins according to the schematics diagram (Figure 8).
As you may notice, the RC5 pin now has the CCP1 function. This happened after we configured the ECCP module. This means that now the ECCP module will control this pin, and we can’t use it as a normal GPIO. However all other pins are configured as GPIO.
Finally, let’s switch to the Interrupt Module tab and make sure that the CCPI interrupt is enabled (Figure 9).
As in tutorial 9 there is no need to enable the priorities of the interrupts as we have only one enabled.
Now everything is configured, and we can click the “Generate” button and switch to the “main.c” file. But actually, this time I suggest you split the code into several files to make it more readable. So let’s move to the tab “Projects” (in the left-top corner of the window), right click on the “Source Files” folder, then in the opened menu select “New” -> “main.c” (like we do in the non-MCC tutorials, see Figure 10).
In the opened window, write the File Name as “indicator” and click “Finish”.
Now right click on the “Header Files” folder, and select “New” -> “xc8_header.h…” (Figure 11)
In the opened window, write the same File Name as the previous time - “indicator”, and click “Finish”.
Let’s now consider what we have just done. As you know, in the C language, the code is mainly written in files with the exеtension “c”. Also there are so-called header files with the extension “h” in which the function declarations are usually written. The content of the declared function should be written in the “c” file with the same name. In the created files we will take away the functions from the “main.c” file that operates with the 7-segment indicator.
Except for these three files we will also make some changes in one of the MCC-generated files, called “eccp1.c”. This file can be found in the “Source Files” - “MCC Generated Files” folder, and consists of the ECCP callback function, in which we will write our own code.
Let’s now consider all the files one by one, and start with the smallest one, “indicator.h”. We need to delete all the initial content in it and write the following code instead.
#include <xc.h> // include processor files - each processor file is guarded.
extern uint32_t distance; //Distance calculated from HC-SR04 sensor
void generate_digit (uint8_t digit, uint8_t show_dp); //Digit shapes generator
void display_number (uint32_t number, uint8_t dp); //Displays the 3-digit number
In line 1, we add the <xc.h> file. It’s needed to use the MCU-related functions and macros. Also it allows the use of the integer types from the “stdint.h” file, like “uint8_t”, “uint32_t” etc.
In line 3, we declare the variable distance which represents the calculated distance obtained from the HC-SR04 sensor, in [0.1cm] units. The modifier “extern” is very important here. It tells the compiler that the variable in fact is defined in some other file (we will see soon, where exactly). This is needed if we want to use the same variable in several “c” files.
In line 5, we declare the generate_digit function. This function is actually the same as in tutorial 13.
In line 6, we declare the display_number function. This function allows to display the 3-digit number specified as parameter number with the decimal point in the position specified as parameter dp.
These two functions are described in the “indicator.c” file which we will consider now.
#include "mcc_generated_files/mcc.h"
#include "indicator.h"
uint8_t pos; //Digit position that is currently displayed
uint32_t distance; //Distance calculated from HC-SR04 sensor in 0.1cm
void generate_digit (uint8_t digit, uint8_t show_dp) //Function to generate a digit
{
switch (digit) //Select digit
{
case 0: //If digit is 0
SEG_A_SetLow(); //Turn on segment A
SEG_B_SetLow(); //Turn on segment B
SEG_C_SetLow(); //Turn on segment C
SEG_D_SetLow(); //Turn on segment D
SEG_E_SetLow(); //Turn on segment E
SEG_F_SetLow(); //Turn on segment F
SEG_G_SetHigh(); //Turn off segment G
break;
case 1: //If digit is 1
SEG_A_SetHigh(); //Turn off segment A
SEG_B_SetLow(); //Turn on segment B
SEG_C_SetLow(); //Turn on segment C
SEG_D_SetHigh(); //Turn off segment D
SEG_E_SetHigh(); //Turn off segment E
SEG_F_SetHigh(); //Turn off segment F
SEG_G_SetHigh(); //Turn off segment G
break;
case 2: //If digit is 2
SEG_A_SetLow(); //Turn on segment A
SEG_B_SetLow(); //Turn on segment B
SEG_C_SetHigh(); //Turn off segment C
SEG_D_SetLow(); //Turn on segment D
SEG_E_SetLow(); //Turn on segment E
SEG_F_SetHigh(); //Turn off segment F
SEG_G_SetLow(); //Turn on segment G
break;
case 3: //If digit is 3
SEG_A_SetLow(); //Turn on segment A
SEG_B_SetLow(); //Turn on segment B
SEG_C_SetLow(); //Turn on segment C
SEG_D_SetLow(); //Turn on segment D
SEG_E_SetHigh(); //Turn off segment E
SEG_F_SetHigh(); //Turn off segment F
SEG_G_SetLow(); //Turn on segment G
break;
case 4: //If digit is 4
SEG_A_SetHigh(); //Turn off segment A
SEG_B_SetLow(); //Turn on segment B
SEG_C_SetLow(); //Turn on segment C
SEG_D_SetHigh(); //Turn off segment D
SEG_E_SetHigh(); //Turn off segment E
SEG_F_SetLow(); //Turn on segment F
SEG_G_SetLow(); //Turn on segment G
break;
case 5: //If digit is 5
SEG_A_SetLow(); //Turn on segment A
SEG_B_SetHigh(); //Turn off segment B
SEG_C_SetLow(); //Turn on segment C
SEG_D_SetLow(); //Turn on segment D
SEG_E_SetHigh(); //Turn off segment E
SEG_F_SetLow(); //Turn on segment F
SEG_G_SetLow(); //Turn on segment G
break;
case 6: //If digit is 6
SEG_A_SetLow(); //Turn on segment A
SEG_B_SetHigh(); //Turn off segment B
SEG_C_SetLow(); //Turn on segment C
SEG_D_SetLow(); //Turn on segment D
SEG_E_SetLow(); //Turn on segment E
SEG_F_SetLow(); //Turn on segment F
SEG_G_SetLow(); //Turn on segment G
break;
case 7: //If digit is 7
SEG_A_SetLow(); //Turn on segment A
SEG_B_SetLow(); //Turn on segment B
SEG_C_SetLow(); //Turn on segment C
SEG_D_SetHigh(); //Turn off segment D
SEG_E_SetHigh(); //Turn off segment E
SEG_F_SetHigh(); //Turn off segment F
SEG_G_SetHigh(); //Turn off segment G
break;
case 8: //If digit is 8
SEG_A_SetLow(); //Turn on segment A
SEG_B_SetLow(); //Turn on segment B
SEG_C_SetLow(); //Turn on segment C
SEG_D_SetLow(); //Turn on segment D
SEG_E_SetLow(); //Turn on segment E
SEG_F_SetLow(); //Turn on segment F
SEG_G_SetLow(); //Turn on segment G
break;
case 9: //If digit is 9
SEG_A_SetLow(); //Turn on segment A
SEG_B_SetLow(); //Turn on segment B
SEG_C_SetLow(); //Turn on segment C
SEG_D_SetLow(); //Turn on segment D
SEG_E_SetHigh(); //Turn off segment E
SEG_F_SetLow(); //Turn on segment F
SEG_G_SetLow(); //Turn on segment G
break;
}
if (show_dp)
{
SEG_DP_SetLow(); //Turn on segment DP
}
else
{
SEG_DP_SetHigh(); //Turn off segment DP
}
}
void display_number (uint32_t number, uint8_t dp)
{
DIG_1_SetLow(); //Turn off digit 1
DIG_2_SetLow(); //Turn off digit 2
DIG_3_SetLow(); //Turn off digit 3
switch (pos) //Which digit to display
{
case 0: //If digit 1
generate_digit(number / 100, dp == 1 ? 1 : 0); //Display the hundreds
DIG_1_SetHigh(); //Turn on digit 1
break;
case 1: //If digit 2
generate_digit((number % 100) / 10, dp == 2 ? 1 : 0); //Display tens
DIG_2_SetHigh(); //Turn on digit 2
break;
case 2: //If digit 3
generate_digit(number % 10, dp == 3 ? 1 : 0); //Display the ones
DIG_3_SetHigh(); //Turn on digit 3
break;
}
__delay_ms(5);//Perform the delay to let the segment be on for some time
pos ++; //Select next digit
if (pos > 2) //If digit number is higher than 2
pos = 0; //Then set the digit as 0
}
In line 1, we include the “mcc_generated_files/mcc.h” file which allows us to use, in the current file, the functions and macros generated by the MCC plugin.
In line 2, we include the recently created header file “indicator.h”. It’s a regular practice in C to include the corresponding header file in the “c” file.
In line 4, we declare the variable pos, which reflects the digit position of the indicator which is currently on. In previous tutorials we called this variable “digit” but this time let’s make some changes.
In line 5, we declare the variable distance. This is the same variable which we already declared in line 3 of the “indicator.h” file, but this time we don’t use the “extern” modifier, so here is the real position where the distance variable is declared.
In lines 7-110, there is the function generate_digit. It is the same function that we called “show_digit” in tutorial 13, so I won’t stop there.
In lines 113-136, there is the function display_number, which has two parameters: number, which represents the 3-digit number to be displayed in the indicator, and dp which represents the position of the decimal point: 0 - for no decimal point, 1, 2, 3 for the first, second, and third digits positions, respondingly.
This function is the part of the main function of the previous tutorials that works with the 7-segment indicator, but modified to be more universal.
In lines 114-116, we turn off all three segments. Then in lines 117-131 we have the familiar switch construction to select what number to display in the corresponding position. As you remember, we now have the variable called pos instead of digit which now reflects the position that is currently turned on.
In the first position, when digit = 0 (line 119), we display the hundreds of the number value: number / 100. The second parameter (which turns on the decimal point) is less clear, so let’s consider this expression in more detail:
dp == 1 ? 1 : 0
This is a special conditional operator. You can read more about it here, for instance: https://docs.microsoft.com/en-us/cpp/cpp/conditional-operator-q?view=msvc-170
In a few words, it’s a special operator which can implement one of two expressions depending on the first condition:
condition ? expression1 : expression2.
If the condition is true, then the expression1 is implemented, otherwise the expression2 is implemented.
So in our case, “dp == 1 ? 1 : 0” means: if dp is 1 then the value should be 1, otherwise it is 0. This operator is equal to the following expression in meaning:
if (dp == 1)
value = 1;
else
value = 0;
But unlike this expression the conditional operator is considered as an operator in C language, the same as “+”, “-”, “&&” etc. so it can be used in regular expressions.
The same operator is met in lines 124 and 128 as well, but there we check if dp is 2 or 3, respectively.
In line 121, we turn on the first digit.
Lines 123-126 allow us to display tens of the number in the second position, and lines 127-130 allow us to display ones of the number in the third position.
In line 132, we perform the 5 ms delay to let the segment be on for this time.
In lines 133-135, we select the next digit to be displayed in the next loop iteration.
And that’s all about the “indicator.c” file. Let’s now consider the “main.c” file.
#include "mcc_generated_files/mcc.h"
#include "indicator.h"
uint32_t tick; //5 ms counter
void main(void)
{
// Initialize the device
SYSTEM_Initialize();
// Enable the Global Interrupts
INTERRUPT_GlobalInterruptEnable();
// Disable the Global Interrupts
//INTERRUPT_GlobalInterruptDisable();
// Enable the Peripheral Interrupts
INTERRUPT_PeripheralInterruptEnable();
// Disable the Peripheral Interrupts
//INTERRUPT_PeripheralInterruptDisable();
while (1) //Main loop of the program
{
if (tick % 50 == 0)//If module of division tick by 50 is 0 (every 250ms)
{
TRIG_SetHigh();//Set TRIG pin high
__delay_us(10);//Perform 10us delay
TRIG_SetLow(); //Set TRIG pin low
TMR1_Reload(); //Reset the timer register
}
if (distance < 1000)//If distance is less than 1m
{
display_number(distance, 2); //Display the distance as "xx.x"
}
else //If distance is bigger than 1m
{
display_number(distance / 10, 0);//Display the distance as "xxx"
}
tick ++; //Increment the tick value
}
}
In line 1 there is the auto-generated line that includes the mcc.h header file into the “main.c” file.
In line 2 we add the “indicator.h” file as we will use the functions from it in the “main.c” file.
The main function of the program is located in lines 6-42 and is quite short now.
In the initialization part we use only the auto-generated functions.
In line 9 there is the SYSTEM_Initialize() function which is called in all MCC-based programs.
In line 12, we uncomment the function INTERRUPT_GlobalInterruptEnable() to enable global interrupts, and in line 18 we uncomment the function INTERRUPT_PeripheralInterruptEnable() to enable peripheral interrupts to which the ECC module interrupt belongs as well.
In lines 23-41, there is the main loop of the program.
In line 25 we check if the modulo of division of the tick value by 50 is 0. The tick is incremented every 5 ms so this condition becomes true every 50 x 5 = 250 ms. So we start the distance measurement 4 times per second. To initialize the measurement we set the TRIG pin high (line 27), wait for 10 us (line 28), and set the TRIG pins low (line 29). Also, we reset the Timer1 register by calling the TMR1_reload() function (line 30). The last function is located in the MCC-generated file “tmr1.c” and allows us to write to the timer the initial value, which was set during configuring the Timer1 in the MCC (Figure 6), value of the field “Period count” (0 in our program).
After we issue the pulse on the TRIG pin, after some time there will be the echo pulse which will be processed in the ECCP module callback.
And here we just process the result (variable “distance”). If it is less than 1000 (which corresponds to 100.0 cm, or 1 m) (line 32), then we display the distance as is, and turn on the decimal point in the second position (line 34). If the distance is bigger or equal than 1000 (line 36) we divide it by 10 and display without a decimal point (38), which corresponds to the initial task.
In line 40, we increment the tick value every loop iteration to get the 5ms intervals count.
And that’s all about the “main.c” file, let’s now switch to the last file we need to discover in this tutorial: “eccp1.c”.
#include <xc.h>
#include "eccp1.h"
#include "../indicator.h"
/**
Section: Capture Module APIs:
*/
uint16_t start;
void ECCP1_Initialize(void)
{
// Set the ECCP1 to the options selected in the User Interface
// CCP1M Every rising edge; DC1B 0; P1M single;
CCP1CON = 0x05;
// CCPR1H 0;
CCPR1H = 0x00;
// CCPR1L 0;
CCPR1L = 0x00;
// Clear the ECCP1 interrupt flag
PIR1bits.CCP1IF = 0;
// Enable the ECCP1 interrupt
PIE1bits.CCP1IE = 1;
// Selecting T3CON
T3CONbits.T3CCP1 = 0x0;
}
void ECCP1_CaptureISR(void)
{
CCP1_PERIOD_REG_T module;
// Clear the ECCP1 interrupt flag
PIR1bits.CCP1IF = 0;
// Copy captured value.
module.ccpr1l = CCPR1L;
module.ccpr1h = CCPR1H;
// Return 16bit captured value
ECCP1_CallBack(module.ccpr1_16Bit);
}
void ECCP1_CallBack(uint16_t capturedValue)
{
if (CCP1CONbits.CCP1M0 == 1)//If interrupt has occurred on rising edge
start = capturedValue; //Save the time of pulse starting
else //If interrupt has occurred on falling edge
distance = ((uint32_t)(capturedValue - start) * 10) / 58; //Calculate the distance
CCP1CONbits.CCP1M0 ^= 1; //And toggle the capture detection edge
}
The majority of the file is auto-generated by the MCC module, and I highlighted the lines that I added in green.
So, first we need to add the header file “indicator.h” (line 3) because here we will also use the distance variable defined in it. You may notice that unlike the other files in which we included the “indicator.h” header, here we write “../indicator.h”. This is an important moment because without the “../” part the compiler will give an error. If you look at the files structure (Figure 12), you may see that the “eccp1.c” file is located in the “mcc_generated_files” folder, and the “indicator.h” file is located in the root folder of the project.
Maybe, if you remember the ancient times of DOS or use Terminal, you might know that double dots in the path name mean the folder which is located one level up. So as the “main.c” and “indicator.c” files are located in the same folder as “indicator.h” we can include it without any prefixes in the path. And as the “eccp1.c” file is located in the nested folder, we need to add this “../”prefix.
In line 16, we declare the “start” variable which represents the Timer1 register value at the start of the echo pulse.
In lines 11-32, there is the function “ECCP1_Initialize” in which the initialization of the ECCP module is performed. We believe in the MCC generator and won’t consider this function in detail.
In lines 34-47, there is the function “ECCP1_CaptureISR” which represents the interrupt subroutine for the ECCP capture event. It is also auto generated, but let’s consider it briefly. In line 36, there is a variable module of type CCP1_PERIOD_REG_T. This type allows to deal with the ECCP module register both as with two 8-bit registers by operating with the fields “ccpr1h” and “ccpr1l” or as with one 16-bit register by operating with the field “ccpr_16Bit”.
In line 39, we clear the ECCP module interrupt flag, as we need to do it manually.
In lines 42-43, we copy the value of the ECCP register (CCPR1) into the fields ccpr1l and ccpr1h of the module variable.
In line 46, we invoke the ECCP callback function ECCP1_CallBack and send the ccpr_16Bit of the module variable as its parameter.
And now, in the ECCP1_CallBack function, located in lines 49-56, we write our code.
In line 51, we check if the CCP1M0 bit of the register CCP1CON is 1. Let’s consider this condition in detail. In line 16 of the ECCP1_Initialize() there is CCP1CON = 0x05. If we look at table 1 we can see that this value (5 in decimal system is 0101 in binary system) corresponds to the “Capture mode, every rising edge”. As I mentioned before, to capture both the start and end of a pulse we need to toggle the sensing edge. In the same table 1, we find that the “Capture mode, every falling edge” corresponds to the CCP1M value of 0100 which differs from the previous value only with the last bit: to sense the rising edge it should be 1, and to sense the falling edge, it should be 0. Now line 40 should be clear. If the CCP1M0 bit is 1 then the capture happens at the rising edge which is the start of the pulse.
When the capture event happens, the value of the Timer1 register is automatically copied to the CCPR1 register, and now is saved in the capturedValue parameter. So in line 52 we copy the capturedValue value into the start variable.
If bit CCP1M0 is 0, it means that the capture has happened on the falling edge, which is the end of the pulse (line 53). In this case we take the value of the capturedValue and subtract the start value from it (capturedValue - start). Then we multiply the result by 10 and divide by 58 (line 54). The last two actions require additional description. As every tick of the timer is 1us (which I explained earlier), if we divide the timer value by 58, we will have the distance in cm. But according to the task we need to display the fractional part of the distance for the values less than 1m. So we need to multiply this value by 10 to get rid of the floating point.
There is also something in line 54 that we didn’t see before - the uint32_t type in the brackets. So, in C if you write the type in the brackets before some expression, you explicitly set the type of it. The thing is that the value (capturedValue - start) * 10 overflows the 16-bit max value of 65535, and for some reason the compiler doesn’t expand the integer type to 32-bit, which it should. So we need to set the 32-bit type explicitly, and this solves the problem. Thus the distance variable now has the measured distance in 0.1cm.
In line 55 we toggle the CCP1M0 bit of the CCP1CON register to change the capture edge for the next time.
And this is all we do in the callback function. As I said in one of the previous tutorials, it should be as short as possible not to block the program for a long time.
So, now we know where and how the distance is calculated. And that’s finally all the program code. As you can see it’s quite simple and similar to ones that we had in some previous tutorials, despite the splitting it into several files.
Now you can assemble your device, compile the code, and download it to the MCU. If you made everything correctly, the device will start working without additional adjustment. If something is wrong, check the assembly of your circuit.
As homework, try to change the measurement units: mm, inches, feet, etc.
Get the latest tools and tutorials, fresh from the toaster.