FB pixel

Making a Simple Capacitance Meter Using MCC | Embedded C Programming - Part 25

Published


Hi there! This tutorial will be devoted to measuring capacitance using the charge-discharge method. In this method, a capacitor is fully charged and then discharged through a resistor with a known value. The discharge time is defined only by the known resistor and an unknown capacitor value (or vice versa if we want to measure the resistance), so it will be quite simple to calculate the unknown value.

In this tutorial we will measure the capacitance and display it on the 1602-character LCD we talked about in tutorial 19. Apart from the LCD, we need just a few resistors and capacitors, which values we will measure.

As usual, let’s first get acquainted with the new MCU modules used in this device. There will be two of them: Analog comparator (as follows from the tutorial’s title) and Timer3, the last timer of PIC18F14K50 MCU that we haven’t considered yet. Let’s start with the latter one, as it’s quite similar to Timer1 which we learned in Tutorial 15.

Timer3 Module

As I’ve said in previous tutorials, there are four timers in the PIC18F14K50 MCU, and they are all different. So let’s now consider the last one we haven’t used yet. As I mentioned, Timer3 is very similar to Timer1, except that Timer3 can’t be used as a clock source for the MCU. Other than that, it has the same features such as:

  • Software selectable operation as a 16-bit timer or counter.
  • Readable and writable 8-bit counting registers (TMR3H and TMR3L).
  • Selectable internal or external clock source or Timer1 (yes, Timer1, not a typo, as only Timer1 can be used to clock the whole MCU) oscillator internal option.
  • Interrupt-on-overflow.
  • Reset on CCP Special Event Trigger.

If you need to understand Timer3 more, please feel free to read chapter 13 of the PIC18F14K50 datasheet.

Analog Comparator Module

The analog comparator included in the PIC18F14K50 MCU is a standard comparator integrated with the microcontroller. If you are unfamiliar with comparators, you can refer to this tutorial. The comparator is a device with two inputs: the analog input and the reference input. If the input voltage is higher than the reference voltage, the comparator’s output becomes high, otherwise it’s low.

The PIC18F14K50 MCU has two comparators that are almost identical and even share the same input and output pins. Because of this, you can’t use the comparators with the same pins, though if one uses the external pins, the other can use internal sources. But if you connect them to the internal voltage sources, there is no problem. But let’s be consistent and consider first the main features of the comparator module:

  • Independent comparator control means that each of the two comparators has its own configuration register and can be configured individually.
  • Programmable input selection. The positive input of each comparator can be connected either to the dedicated pin C12IN+ (RC0) or to a fixed or programmable internal reference voltage. The negative input of each comparator can be connected either to the analog ground or to one of three dedicated pins C12IN1- (RC1), C12IN2- (RC2), or C12IN3- (RC3).
  • The Comparator output is available internally and/or externally. There is a dedicated pin C12OUT (RC4) to which the output of one of the comparators can be connected and accessed externally. (Warning! If you want to use the C12OUT pin as the comparator output you should configure it as a digital output separately in your program) And in any case, the comparator output is available by the firmware as a dedicated bit of the comparator’s control register.
  • Programmable output polarity means that the comparator output can be high if the positive input is greater than the negative one (straight polarity) or low (inverted polarity).
  • Interrupt-on-change. Comparators can generate an interrupt when their output changes, so you don’t need to permanently poll them.
  • Wake up from sleep. Change in the comparator output can also wake the MCU from sleep mode.
  • Programmable speed/power optimization. You can select one of two modes: High-speed or Low-speed. The power consumption in the first mode is higher than in the second, but the propagation delay is lower.

That’s actually all the brief information about the comparator module. Again, for more information, please refer to chapter 11 of the PIC18F14K50 data sheet.

Let’s now consider the schematic diagram and see how the capacitance is measured.

Schematic Diagram and Theoretical Background

The schematic diagram is shown in Figure 1.

Figure 1 - Schematic diagram with the PIC18F14K50 with 1602-character LCD and the capacitance measurement circuit
Figure 1 - Schematic diagram with the PIC18F14K50 with 1602-character LCD and the capacitance measurement circuit

This schematic diagram is quite similar to the one presented in the first part of tutorial 20: it also consists of the PIC18F14K50 MCU (DD1) and 1602-character LCD (X2). The only difference in the LCD connection is that the RS pin of the LCD is connected to pin RC3 instead of RC4 because the latter will be used for another purpose.

The resistors R3 and R4 form a voltage divider supplied from the same voltage as the MCU.

The voltage from the divider goes to pin RC1, which is the negative voltage input of the analog comparator C12IN1-. The values of R3 and R4 are not random and were selected with a specific purpose which I will talk about later. For now, let’s calculate the reference voltage from this divider.

For now, we don’t need the real value in volts; this expression is quite enough.

Now let’s consider the most important thing - the measurement circuit, which consists of the known resistor R5 of 10 kOhm and unknown capacitor Cx. This circuit is supplied from pin RA4 (actually, any pin can be used for this purpose, I just selected this one because I noticed it first). In this case, it’s used as a regular digital output, either high or low. The voltage from the capacitor Cx goes to pin RC0 is merged with the positive input of the comparator C12IN+.

Also, pay attention that pins RC4 and RC5 are tied together. It is vital to remember this link. Pin RC4 is merged with the output of the comparator C12OUT, and pin RC5 is merged with the capture input CCP1 of the ECCP module.

The algorithm of the operation is the following:

  1. Pin RA4 goes high, and the capacitor Cx is charged through resistor R5 for at least 1 second to the VCC voltage.
  2. After charging the capacitor, the pin RA4 goes low, and simultaneously Timer3 is reset to start the counting from 0.
  3. The capacitor discharges through the resistor R5, and its voltage drops by the known law , where is the voltage on the capacitor Cx, and t is the time.
  4. When the voltage on the capacitor Cx reaches the negative voltage from the R3-R4 divider, the comparator’s output becomes low. At this moment, the capture event of the ECCP module is happening because the capture input CCP1 is connected with the comparator output C12OUT, and the Timer3 counting register is copied into the ECCP register.
  5. The time t from the previous equation is already measured by Timer3 and captured by the ECCP module, the value at the capture time is also known and is , and R5 value is known as well, so we can now calculate the capacitance:

    Let’s shorten both parts by VCC and take the natural logarithm:

    Now the value of 2.7 should be understood. It’s very close to e, which is 2.7172. In this case, we can consider that , and thus:


    And finally:

As you can see, the final equation is very simple and doesn’t depend on the supply voltage, only on the resistance R5 and the discharge time t.

That’s everything about the device schematic diagram so we can proceed to the program code consideration.

Configuration of the project using MCC

In this project, we need to configure Timer3, ECCP, and analog comparator 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, set the Internal Clock as 8MHz_HF, and set the checkbox “PLL Enabled”. Also, don’t forget to remove the check from the “Low-voltage programming enable” field (Figure 2).

Figure 2 - System Module configuration
Figure 2 - System Module configuration

Then we need to go to the Device resources tab on the left part of the screen, expand the drop-down list “Timer”, and click on the green plus at “TMR3” (Figure 3).

Figure 3 - Timer3 adding
Figure 3 - Timer3 adding

Then expand the “ECCP” drop-down list and click on the green plus at the “ECCP1” like we already did in Tutorial 15. Finally, we need to expand the “Comparator” list and add the “CMP1” position (Figure 4).

Figure 4 - Comparator 1 adding
Figure 4 - Comparator 1 adding

After that, your Resource Management window should look like in Figure 5.

Figure 5 - Resource Management window
Figure 5 - Resource Management window

Now, let’s open the TMR3 window and configure the Timer3 module according to Figure 6.

Figure 6 - Timer3 configuration
Figure 6 - Timer3 configuration

The fields marked with green should remain unchanged, and the fields marked with red frames should be set according to Figure 6:

  1. Starts Timer3.
  2. The clock source for the Timer3 is Fosc/4, so the input clock frequency for the timer module will be 16 / 4 = 4 MHz.
  3. We will not use the Timer3 overflow interrupt, so we don’t need to enable it this time.
  4. We can ignore this option as it applies only to the external clock. So the value of this option can be anything.
  5. We need to set the pre-scaler as 1:8. After it, the input Timer3 frequency is 32 / 4 x 1:8 = 1 MHz. So each tick of the timer corresponds to 1 / 1MHz = 1us. This value is convenient for us to simplify the capacitance calculation.
  6. This option requires a more detailed description. 16-bit read/write mode allows you to read the whole Timer3 register simultaneously. In this case, the TMR3L register is accessed directly, and the TMR3H register is read through the buffer into which the value of TMR3H is loaded once we read the TMR3L register. The same with writing: the upper byte is written to the buffer first and is copied to the TMR3H register when we write to the TMH3L 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 uncheck this checkbox; in this case, both TMR3L and TMR3H registers will be available independently, and you can reload any of them at any time.
  7. 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 capacitor discharge.

That’s all configuration of the Timer3 required for the current application. Now, switch to the ECCP1 tab and configure the ECCP module (Figure 7).

Figure 7 - ECCP module configuration
Figure 7 - ECCP module configuration

Let’s get the hang of a few settings here:

  1. As previously mentioned, we will use the ECCP module in the Compare mode. Depending on the selected mode, the options below will differ.
  2. As we will use the ECCP with the Timer3, we need to select the last in the drop-down list.
  3. The comparator’s output goes from high to low when the capacitor discharges to the threshold level, so we must select the falling edge.
  4. We need to enable the CCP interrupt because we will save the captured value inside this interrupt callback.

Now, let’s switch to the CMP1 tab and configure the Comparator module (Figure 8).

Figure 8 - Comparator module configuration
Figure 8 - Comparator module configuration

This module has the most options among others we have considered today. Let’s see what they do:

  1. Enables comparator.
  2. When the synchronous mode is enabled, the change of the comparator’s output is synchronized with the MCU clock. As the comparator directly triggers the capture in the ECCP module, we can leave this checkbox unchecked.
  3. Here we can enable the input hysteresis of 65 mV to prevent the output from being changed at a high frequency when the input signal is noisy, and the positive and negative voltages are similar. We don’t need this option for now.
  4. Enables the low power mode for the comparator, in which the propagation delay increases, so we will leave this checkbox unchecked, which leaves the comparator in the high-speed mode.
  5. Enables the output pin C12OUT; as we use it, we need to set this checkbox.
  6. Selects the positive input between the external input CIN+ and internal input CVref; we need to use the CIN+ pin, so we leave this parameter unchanged.
  7. Selects the negative input between the Vss or one of the external inputs CIN1-, CIN1-, or CIN3-. In our case, the R4-R4 divider is connected to the CIN1- pin, so we need to select it in the drop-down list.
  8. Selects the output polarity between inverted and non-inverted; we will leave it non-inverted.
  9. Allows enabling the comparator interrupt when it changes the output state. As we’re not going to use it, we leave it disabled.

Let’s now switch to the Pin Module and Pin Manager and configure all required pins according to the schematic diagram (Figure 8).

Figure 9 - Pin Module configuration
Figure 9 - Pin Module configuration

As you may notice, the RC5 pin now has the CCP1 function and this happened after we configured the ECCP module. It means the ECCP module will control this pin, and we can’t use it as a normal GPIO. Also, pins RC0, RC1, and RC4 are configured to be C12IN+, C12IN1-, and C12OUT, respectively, and are controlled by the comparator. But, as I mentioned before, you need to configure the C12OUT pin as an output yourself, so don’t forget to set the corresponding check in the “Output” column.

Configuration of other pins is similar to Tutorial 21 as we use the same 1602 LCD and almost the same connection except for the RS pin. The RA4 pin, which controls the capacitor charge/discharge, is configured as an output and has the custom name “CHARGE_CTRL”

Finally, let’s switch to the Interrupt Module tab and ensure that the CCPI interrupt is enabled (Figure 9).

Figure 10 - Interrupt Module
Figure 10 - Interrupt Module

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 code writing.

Program Code Description

Before proceeding to code writing, we need to copy the “lcd_1602_mcc.h” and “lcd_1602_mcc.c” files from Tutorial 23 and paste them into the current project (Figure 10-13).

Figure 11 - Copying the “lcd_1602_mcc.h” file from the “Control_system_MCC” project
Figure 11 - Copying the “lcd_1602_mcc.h” file from the “Control_system_MCC” project
Figure 12 - Pasting the “lcd_1602_mcc.h” file into the “Capacitance_meter_MCC” project
Figure 12 - Pasting the “lcd_1602_mcc.h” file into the “Capacitance_meter_MCC” project
Figure 13 - Copying the  “lcd_1602_mcc.c” file from the “Control_system_MCC” project
Figure 13 - Copying the “lcd_1602_mcc.c” file from the “Control_system_MCC” project
Figure 14 - Pasting the  “lcd_1602_mcc.c” file into the “Capacitance_meter_MCC” project
Figure 14 - Pasting the “lcd_1602_mcc.c” file into the “Capacitance_meter_MCC” project

After all these manipulations, your project should look like in Figure 14.

Figure 15 - Project structure with all generated and added files
Figure 15 - Project structure with all generated and added files

Now, let’s open the “main.c” file and write the following code there.

#include "mcc_generated_files/mcc.h"

#include "lcd_1602_mcc.h"

volatile uint8_t measurement_done; //Flag that indicates that the time measurement is complete

volatile uint16_t value; //Time value received from the ECCP module

void putch(char data) //Function to put the character to LCD (needed by printf)

{

lcd_data(data); //Send character to LCD

}

void main(void)

{

SYSTEM_Initialize();

INTERRUPT_GlobalInterruptEnable();

INTERRUPT_PeripheralInterruptEnable();

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

while (1)

{

CHARGE_CTRL_SetHigh(); //Set the CHARGE_CTRL pin high to charge the capacitor

__delay_ms(1000); //Wait for 1 second while the capacitor is being charged

measurement_done = 0;//Reset the measurement_done flag

CHARGE_CTRL_SetLow(); //Set the CHARGE_CTRL pin low to start the capacitor discharge

TMR3_Reload(); //Reset Timer3

while (!measurement_done); //Wait while the capture event happens

lcd_clear(); //Clear the LCD

printf ("C=%u.%unF", (value / 10), (value % 10));//Display the capacitance value

}

}

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 2, we include the “lcd_1602_mcc.h” file, which consists of the 1602 LCD-related functions.

In lines 4 and 5, we declare two global variables: measurement_done is the flag that indicates that the capacitor has been discharged to the level defined by the negative reference voltage set by the R3-R4 divider. The comparator’s output switched from high to low, which, in its turn, caused the Timer3 capture interrupt. The value variable is the captured time of the capacitor discharge in 1 us unit (I will explain later how this happens).

In lines 7-10, we define the putch function as the special function used by the printf function to send the data anywhere. In this exact function, you determine where the output character stream will be directed. The putch function has a single parameter - data of the char type. The application of putch is to send this character somewhere. In our case, we send it to the 1602 LCD using the lcd_dat function (line 9), which has been described in detail in Tutorial 19. And that’s it! Now, when you invoke the printf function, it will display the formed string in the LCD. For sure, we already have the functions lcd_write_string and lcd_write_number, which allow us to do the same, and actually, they require less Flash and RAM memory, but printf is quite convenient and familiar. So it’s up to you what to use - it’s the usual choice between convenience and simplicity on the one hand and memory economy and productivity on the other. In lines 12-30, there is the program’s primary function. As you can see, it’s pretty short. Let’s consider it in detail.

In lines 14-16, the MCC-generated functions are located, which initialize the system (line 14) and enable the global (line 15) and peripheral (line 16) interrupts. In line 17, we initialize the 1602 LCD without the cursor and blinking.

That’s all about the initialization.

Now, let’s consider the main loop of the program. It’s short and occupies lines 19-29. In line 21, we set the CHARGE_CTRL pin high. After that, the capacitor Cx starts charging through the resistor R5 (see Figure 1). This process takes some time, so we need to wait while it completely charges. Thus, in line 22, we implement the delay of 1 second. Actually, the charging time is less than a half a second and depends on the values of the capacitor and resistor R5, so you can probably adjust this value.

In lines 23-25, we prepare everything for the discharge time measurement: reset the measurement_done flag (line 23), set the CHARGE_CTRL pin low to start the discharge (line 24) and simultaneously reset the value of the Timer3 counting register (line 25).

Now the only thing to do is to wait while the capacitor’s voltage becomes 1/2.7 of the initial value. When this happens, the comparator’s output goes low, which causes the ECCP capture event (as we configured it to be triggered by the falling edge, see Figure 7). This event will trigger the capture interrupt, and inside its handler (which we will consider soon) we get the discharge time value and set the flag measurement_done. So, in line 26, we wait while the measurement_done flag becomes 1.

Before proceeding further, we must figure out how to convert the discharge time into the capacitance. We already have the formula for it:

But let’s now set the real values

To get the capacitance in uF, we need to divide the value by 10,000, which doesn’t seem reasonable. It would be better to calculate the capacitance in nF, then the formula will be the following:

Now it looks good. Let’s see the capacitance boundaries we can obtain with the current setup. The value is a 16-bit variable, so it changes from 0 to 65535, so the least possible capacitance is Cmin = 1/10 = 0.1 nF, and the greatest possible capacitance is Cmax = 65535/10 = 6553.5 nF = 6.55 uF. If you want to change these boundaries, you can use another value of the resistor R5.

Now let’s return to the program consideration. We stopped at line 26, where we waited for the capacitor discharge. Now we have the value value (sorry for the tautology, I should probably name things more carefully) and can convert it into the capacitance and display it on the LCD.

In line 27, we clear the LCD and set the cursor on the first position of the first row. Then we call the printf function in line 28. For more information about its use in the PIC MCUs, please refer to this guide. As you can see, it doesn’t differ much from the regular printf function for desktop applications. It even supports the floating point values (which is not supported in a lot of MCU’s implementations of this function). But as I already mentioned in some of the tutorials, the floating point operations drastically increase the Flash and RAM usage by the MCU, so whenever possible, they should be avoided. In our case, it is possible, and I will show how.

So, we call the printf function as

printf ("C=%u.%unF", (value / 10), (value % 10))

So first, it will display the text “C=” then the unsigned number (%u), then the decimal point as plain text, then another unsigned number, and finally the text “nF”. As the first shown number, we display the value divided by 10, and as the second number, we display the modulo of dividing the value by 10. You can calculate by yourself that this is exactly what we need.

Now we’re done with the “main.c” file but that’s not all yet. We need to open the “eccp1.c” file located in the “src/MCC Generated files” folder and add some lines like in Tutorial 15.

#include <xc.h>

#include "eccp1.h"


extern volatile uint8_t measurement_done;

extern volatile uint16_t value;

/**

Section: Capture Module APIs:

*/

void ECCP1_Initialize(void)

{

// Set the ECCP1 to the options selected in the User Interface

// CCP1M Every falling edge; DC1B 0; P1M single;

CCP1CON = 0x04;

// CCPR1H 0;

CCPR1H = 0x00;

// CCPR1L 0;

CCPR1L = 0x00;

// Clear the ECCP1 interrupt flag

PIR1bits.CCP1IF = 0;

// Enable the ECCP1 interrupt

PIE1bits.CCP1IE = 1;

// Selecting Timer3

T3CONbits.T3CCP1 = 0x1;

}

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)

{

measurement_done = 1;//Indicate that the timer capture event has happened

value = capturedValue; //Save the discharge time

}

I’ve highlighted the added lines with the green color. I will not describe the code generated by the MCC; if you are interested in it, please read the Tutorial 15, where I considered it in detail.

Let’s see what’s done in the lines added manually. In lines 4 and 5, we declare the same variables measurement_done and value as in the”main.c” file but this time with the extern modifier, which tells the compiler that these variables are declared in some other place.

In the ECCP callback function (lines 49-53), we set the measurement_done flag as 1 (line 51) to indicate that we have captured the capacitor discharge time and saved it into the value variable (line 52).

That’s all we do inside the interrupt subroutine function, sticking to the principle of the shortest possible code within interrupts.

And that’s it! Now let’s assemble the circuit according to Figure 1, connect the debugger to the PC’s USB port and compile and download the program code.

Testing of the Capacitance Meter

First, I connected the ceramic capacitor of 1 uF as the Cx, and the measurement result was the following (Figure 15).

Figure 16 - Result of the measurement of the 1uF ceramic capacitor
Figure 16 - Result of the measurement of the 1uF ceramic capacitor

If the tolerance is ±20% then this result is fine. In reality, I don’t know their real tolerance as I bought these capacitors several years ago, and they could’ve degraded.

Then I connected several other capacitors, and their measurement results are gathered in table 1.

Table 1 - Results of the capacitance measurements

Nominal value

Measured capacitance

300 pF (ceramic)

0.3 nF

3.3 nF (ceramic)

3.9 nF

10 nF (ceramic)

10.3 nF

330 nF (ceramic)

390 nF

1 uF (ceramic)

890 nF

1.5 uF (ceramic)

1452 nF

1 uF (electrolytic)

860 nF

4.7 uF (electrolytic)

5125 nF

As you can see, some real values are smaller than nominal, and some are greater, so this is not the flaw of the method or the schematics; it’s really the difference in the values.

Finally, I want you to show what’s going on on the low level by connecting the oscilloscope to the capacitor Cx and viewing its discharge curve (Figure 16).

Figure 17 - Oscillogram of the capacitor discharging curve
Figure 17 - Oscillogram of the capacitor discharging curve

Here is the measurement of the capacitor with a nominal 1.5 uF. As you can see, initially, the capacitor is charged to 3.04V (value Vpp in the legend). At the moment marked with the first vertical white line, it starts discharging until the value of 3.04/2.7 = 1.13 V. This moment is marked with the second vertical white line. The time between these two events is 14.5 ms (𝞓T in the legend). If we divide this time by 10 kOhm we will get 1.45 uF, which corresponds to the value from Table 1.

And that’s all about this tutorial. As you can see, the capacitance metering method is not complicated and doesn’t require any active external components, just three resistors. We have learned two new modules - Timer3 and Comparator. Also, we have discovered how to use the printf function to show the information on the LCD.

As for homework, I suggest you do the opposite - using a known capacitor (you can select it using the current circuit), and measure an unknown resistance with the same schematic. The resistance formula is taken from the same equation: R5Cx=t, where now Cx is known and R5 is unknown.

Make Bread with our CircuitBread Toaster!

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

What are you looking for?