FB pixel

Digital Voltmeter - Part 18 Microcontroller Basics (PIC10F200)

Published


Note: Microchip recently made some changes and their newer IDE versions use the XC8 compiler for Assembly, whereas all sample code here is created for the MPASM compiler. However, the sample code has been ported to XC8 by user tinyelect on our Discord channel (to whom we are extremely grateful!) and the XC8 version of the code is at the bottom of this tutorial for your reference. To see the changes needed to switch from MPASM to XC8, please check out the process that he shared.

Hi! In this tutorial we will continue discovering analog features of the PIC10F200 microcontroller. And this time we will learn how to make the opposite conversion: from analog signal into digital form, so we will create an analog-to-digital converter (ADC).

We will use a relatively rare and exotic way of converting the voltage into digital code. It’s based on measuring the time required to discharge the capacitor from a known voltage to an unknown measured voltage. To understand this method, let’s consider the schematic diagram of the device (figure 1).

Figure 1. Schematic diagram of the digital voltmeter.
Figure 1. Schematic diagram of the digital voltmeter.

We have the traditional microcontroller PIC10F200, and the programming connector X1. Also we have the 7-segment LED display TM1637 (X2) which we learned about in the tutorial devoted to the digital thermometer. The new components are resistor R1, capacitor C1, and the analog comparator DA1. I specifically didn’t write the part number for DA1 as you can use any comparator or opamp chip you have available. The only thing you should note, that it’s better to use the chip with the rail-to-rail inputs, this will allow you to measure the voltage in the full range from 0 to the VCC.

I used the LM358 op-amp just because I had it at home, and so I face the restriction of not being able to go higher than 3.5V. If the voltage to be measured is higher than this value, the chip can’t distinguish it. The values of R1 and C1 were selected empirically, and I suggest you not to change them as they work well - I’ll explain later why. It’s better to select the resistor and capacitor with 1% tolerance to increase the measurement accuracy (which, frankly, is quite low).

The principle of operation is quite simple and is based on the main feature of the comparator: if V+ > V- then the output is high, if V+ < V- then the output is low.

Our program first sets GP2 high and waits a certain amount of time to let the capacitor C1 charge through the R1 resistor. After that, it sets GP2 low and resets the timer which measures the time of the capacitor discharging. The voltage on the capacitor is applied to the V+ pin of the comparator, and the measured voltage Ux is applied to its V- pin. So while the voltage on the capacitor is higher than Ux, the output of the comparator will remain high. As soon as the voltage on the capacitor becomes equal or lower to the Ux, the comparator output becomes low.

The output of the comparator is connected to the GP3 input (see figure 1), and when this input becomes low, we read the timer value, and convert it into the voltage. Now let’s see how this conversion is done.

As you probably know, the voltage on the capacitor Uc, which discharges through the resistor R, changes according the following equation:

Here Uc(t) - voltage on the capacitor is dependent on time
U0
- initial voltage of the capacitor, which is VCC in our case
R - resistance of the R1 resistor
C - capacitance of the C1 capacitor

As you see, the relationship is exponential. So to convert the time into the voltage we need to divide it by the RC value, then calculate the exponent of the expression (-t/RC), and finally multiply this value by the U0. This sounds too complicated for our primitive microcontroller, and in fact, it is. We will not do all these calculations, we will use the table method.

To use the table, we will need to perform a so-called “calibration” of the voltmeter. To make it we need another, example, voltmeter. We will apply the voltage to the Ux input, and measure it with the example voltmeter, at the same time we will measure the time t, which is needed to discharge the capacitor from U0 to Ux, and indicate it in the LED display. After that we will create a table with 2 columns: t and Ux, and write down the results into it.

As we don’t expect high accuracy from this ADC, we can set and measure the voltages with .25V steps. Also, as the TM1637 display doesn’t have a decimal point, I suggest displaying the voltage in millivolts. Thus, with the last two limitations we will have the values: 0000, 0250, 0500, 0750, 1000, …. 3250, 3500. I limited the maximum voltage to 3.5V, as my op-amp can’t distinguish more, as I mentioned. If you are so lucky to have a rail-to-rail inputs comparator, you can extend the scale up to 5V.

When doing the calibration, I suggest you measure the voltage not at the node points but in between them. I mean, it is better to set the voltages of 0.125, 0.375, 0,625V… 3.375V, and measure the t value for these Ux values. In this case the voltage below 0.125V will be considered as 0V, The voltage in the range of 0.125 to 0.375V will be considered as 0.25V, the voltage in the range 0.375 to 0.625 will be considered as 0.5V, and so on and so forth.

The values obtained during my calibration are presented in Table 1. You will probably have similar values if you use the same R1 and C1 as me.

Table 1. Results of the calibration.
Table 1. Results of the calibration.

As you see, there are just 14 values, which are not that difficult to put into the program. Also you may notice that the voltage of 0.125V corresponds to the time of 240 timer samples which is almost the full timer scale. This is because I correctly chose R1 and C1 (see figure 1).

If you build the graph of t(Ux), you will get the one presented in figure 2. It looks like an exponential graph but a bit uglier.

Figure 2. t(Ux) dependence.
Figure 2. t(Ux) dependence.

Well, I guess we now have all the required information, and are ready to consider the code of the program.

#include "p10f200.inc"

i   EQU    10    ;Delay variable

j   EQU    11    ;Delay variable

bit_count    EQU    12    ;Counter of processed bits

tw_data   EQU    13    ;Data to receive/transmit via TWI

port   EQU    14    ;Helper register to implement 1-wire and TWI

ack   EQU    15    ;Acknowledgment received from the TWI device

digit   EQU    16    ;Digit to be decoded for the display

num_l   EQU    17    ;Lower byte of the number

num_m   EQU    18    ;Middle byte of the number

num_h   EQU    19    ;Higher byte of the number

dio   EQU    GP0    ;DIO pin of the display

clk   EQU    GP1    ;CLK pin of the display

rc   EQU    GP2    ;RC-chain

comp   EQU    GP3    ;Comparator output

#define CALIBRATION       ;Calibration mode

;#undefine CALIBRATION    ;Uncomment for normal mode

__CONFIG _WDT_OFF & _CP_OFF & _MCLRE_OFF

ORG 0x0000

INIT

MOVLW  ~((1<<T0CS)|(1<<PSA))   ;enable GPIO2

OPTION     ;and set timer prescaler to 256

MOVLW (1<<dio)|(1<<clk)|(1<<comp)

MOVWF port   ;It's used to switch DIO/CLK pins direction

TRIS GPIO   ;Set 'clk', 'dio', and 'comp' as inputs

    CLRF GPIO    ;Clear GPIO to set all pins to 0

LOOP       ;Main loop of the program

    MOVLW (1<<rc)   ;Set 'rc' pin high to charge the capacitor

    MOVWF GPIO

    MOVLW D'255'       ;Delay for 200 ms to make sure it's charged

    CALL DELAY    

    CLRF GPIO   ;Clear GPIO to set 'rc' low and start discharging

    CLRF TMR0   ;Reset the timer

WAIT_FOR_COMPARATOR      ;And start waiting while comparator sets low

    MOVLW 0xFF   ;Check if the timer value becomes 255

    XORWF TMR0, W

    BTFSC STATUS, Z    

    GOTO CHECK_RESULT    ;and leave the waiting loop in this case

    BTFSC GPIO, comp     ;Otherwise check the comparator output

    GOTO WAIT_FOR_COMPARATOR   ;If it's still 1, return to the waiting

CHECK_RESULT       ;Check the timer value

    MOVF TMR0, W       ;Copy the TMR0 value

    MOVWF num_l   ;into the 'num_l'

#ifndef CALIBRATION   ;If normal mode

    CALL DECODE_VOLTAGE  ;Decode voltage

    MOVWF num_l   ;And copy the value of W register into the 'num_l'

#endif

    CALL SPLIT_NUM   ;Then split the 'num_l' into digits

;Send first command to LED display

    CALL TW_START   ;Issue Start condition

    MOVLW 0x40   ;Send command 0x40 -

    CALL TW_WRITE_BYTE   ;"Write data to display register"

    CALL TW_STOP       ;Issue Stop condition

;Send second command and data to LED display

    CALL TW_START   ;Issue Start condition

    MOVLW 0xC0   ;Send address 0xC0 -

CALL TW_WRITE_BYTE   ;The first address of the display

#ifdef CALIBRATION   ;In calibration mode

    MOVLW 0   ;Blank the first digit

#else   ;In normal mode

    MOVF num_m, W   ;display the 'num_m' value

    CALL DECODE_DIGIT    ;Get the display data

#endif

CALL TW_WRITE_BYTE   ;Write the first digit to the display

#ifdef CALIBRATION   ;In calibration mode

    MOVF num_h, W   ;Display the 'num_h' value

#else   ;In normal mode

    MOVF num_l, W   ;Display the 'num_l' value

#endif

    CALL DECODE_DIGIT    ;Get the display data

CALL TW_WRITE_BYTE   ;Write the second digit to the display

#ifdef CALIBRATION   ;In calibration mode

    MOVF num_m, W   ;Display the 'num_l' value

#else   ;In normal mode

    CLRW       ;Load 0 into the W register

    BTFSC num_l, 1   ;if bit 1 of 'num_l' is 1 (if 'num_l' is 2 or 7)

    MOVLW 5   ;then load 5 into the W register

#endif

    CALL DECODE_DIGIT    ;Get the display data

CALL TW_WRITE_BYTE   ;Write the third digit to the display

#ifdef CALIBRATION   ;In calibration mode

    MOVF num_l, W   ;Display the 'num_l' value

#else   ;In normal mode

    MOVLW 0   ;Display '0'

#endif

    CALL DECODE_DIGIT    ;Get the display data

CALL TW_WRITE_BYTE   ;Write the fourth digit to the display

    CALL TW_STOP       ;Issue Stop condition

;Send brightness command to LED display

    CALL TW_START   ;Issue Start condition

    MOVLW 0x8F   ;Send display control byte 0x8F -

    CALL TW_WRITE_BYTE   ;"Display ON"

    CALL TW_STOP    ;Issue Stop condition

    MOVLW D'255'       ;Perform 200 ms delay

    CALL DELAY   ;before the next measurement

GOTO LOOP        ;loop forever

;-------------Helper subroutines---------------

DIO_HIGH   ;Set DIO pin high

    BSF port, dio   ;Set 'dio' bit in the 'port' to make it input

    MOVF port, W   ;Copy 'port' into W register

TRIS GPIO   ;And set it as TRISGPIO value

    RETLW 0    

DIO_LOW   ;Set DIO pin low

    BCF port, dio   ;Reset 'dio' bit in the 'port' to make it output

    MOVF port, W   ;Copy 'port' into W register

TRIS GPIO   ;And set it as TRISGPIO value

    RETLW 0

CLK_HIGH   ;Set CLK pin high

    BSF port, clk   ;Set 'clk' bit in the 'port' to make it input

    MOVF port, W   ;Copy 'port' into W register

TRIS GPIO   ;And set it as TRISGPIO value

    RETLW 0

CLK_LOW   ;Set CLK pin low

    BCF port, clk   ;Reset 'clk' bit in the 'port' to make it output

    MOVF port, W   ;Copy 'port' into W register

TRIS GPIO   ;And set it as TRISGPIO value

    RETLW 0

;-------------TW start condition--------------

TW_START    

    CALL CLK_HIGH   ;Set CLK high

    CALL DIO_LOW   ;Then set DIO low

    RETLW 0

;-------------TW stop condition---------------

TW_STOP    

    CALL DIO_LOW   ;Set DIO low

    CALL CLK_HIGH   ;Set CLK high

    CALL DIO_HIGH   ;Then set DIO highs and release the bus

    RETLW 0

;------------TW write byte--------------------

TW_WRITE_BYTE

    MOVWF tw_data   ;Load 'tw_data' from W register

    MOVLW 8   ;Load value 8 into 'bit_count'

    MOVWF bit_count   ;to indicate we're going to send 8 bits

TW_WRITE_BIT   ;Write single bit to TW

    CALL CLK_LOW   ;Set CLK low, now we can change DIO

    BTFSS tw_data, 0    ;Check the LSB of 'tw_data'

    GOTO TW_WRITE_0   ;If it's 0 then go to the 'TW_WRITE_0' label

TW_WRITE_1   ;Else continue with 'TW_WRITE_1'

    CALL DIO_HIGH   ;Set DIO high

GOTO TW_SHIFT   ;And go to the 'TW_SHIFT' label

TW_WRITE_0    

    CALL DIO_LOW   ;Set DIO low

TW_SHIFT

    CALL CLK_HIGH   ;Set CLK high to start the new pulse

    RRF tw_data, F   ;Shift 'tw_data' at one bit to the right

    DECFSZ bit_count, F ;Decrement the 'bit_count' value, check if it's 0

    GOTO TW_WRITE_BIT    ;If not then return to the 'TW_WRITE_BIT'

TW_CHECK_ACK   ;Else check the acknowledgement bit

    CALL CLK_LOW   ;Set TW low to end the last pulse

    CALL DIO_HIGH   ;Set DIO high to release the bus

    CALL CLK_HIGH   ;Set TW high to start the new pulse

    MOVF GPIO, W   ;Copy the GPIO register value into the 'ack'

    MOVWF ack   ;Now bit 'dio' of the 'ack' will contain ack bit

    CALL CLK_LOW   ;Set CLK low to end the acknowledgement bit

    RETLW 0

;------------Decode the voltage-----------------

DECODE_VOLTAGE

    MOVLW D'9'   ;If the 'num_l' value is less than 9

    SUBWF num_l, W    

    BTFSS STATUS, C

    RETLW D'35'   ;then U=3.5V, load 35 into the W register

    MOVLW D'12'   ;If the 'num_l' value is less than 12

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'32'   ;then U=3.25V, load 32 into the W register

    MOVLW D'16'   ;If the 'num_l' value is less than 16

    SUBWF num_l, W    

    BTFSS STATUS, C

    RETLW D'30'   ;then U=3.0V, load 30 into the W register

    MOVLW D'20'   ;If the 'num_l' value is less than 20

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'27'   ;then U=2.7V, load 27 into the W register

    MOVLW D'24'   ;If the 'num_l' value is less than 24

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'25'   ;then U=2.5V, load 25 into the W register

    MOVLW D'31'   ;If the 'num_l' value is less than 31

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'22'   ;then U=2.25V, load 22 into the W register

    MOVLW D'38'   ;If the 'num_l' value is less than 38

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'20'   ;then U=2.0V, load 20 into the W register

    MOVLW D'46'   ;If the 'num_l' value is less than 46

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'17'   ;then U=1.75V, load 17 into the W register

    MOVLW D'58'   ;If the 'num_l' value is less than 58

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'15'   ;then U=1.5V, load 15 into the W register

    MOVLW D'63'   ;If the 'num_l' value is less than 63

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'12'   ;then U=1.25V, load 12 into the W register

    MOVLW D'73'   ;If the 'num_l' value is less than 73

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'10'   ;then U=1.0V, load 10 into the W register

    MOVLW D'87'   ;If the 'num_l' value is less than 87

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'7'   ;then U=0.75V, load 7 into the W register

    MOVLW D'148'       ;If the 'num_l' value is less than 148

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'5'   ;then U=0.5V, load 5 into the W register

    MOVLW D'240'       ;If the 'num_l' value is less than 240

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'2'   ;then U=0.25V, load 2 into the W register

    RETLW 0   ;Otherwise U=0V, load 0 into the W register

;------------Split number-----------------

SPLIT_NUM

#ifdef CALIBRATION   ;We have the hundreds only in calibration mode

    CLRF num_h   ;Clear the 'num_h' register

    MOVLW D'100'       ;Load 100 into the W register

DIVIDE_100   ;Start loop of the division operation

    SUBWF num_l, F   ;Subtract W from 'num_l'

    BTFSS STATUS, C   ;And check if Borrow status occurred

    GOTO END_SPLIT_100   ;If it was, then end operation

    INCF num_h, F   ;Otherwise increment 'num_h'

    GOTO DIVIDE_100   ;And return to the 'DIVIDE_100' label

END_SPLIT_100

    ADDWF num_l, F   ;Add W to 'num_l'

#endif

    CLRF num_m   ;Clear 'num_m' register

    MOVLW D'10'   ;Load 10 into the W register

DIVIDE_10   ;Start loop of the division operation

    SUBWF num_l, F   ;Subtract W from 'num_l'

    BTFSS STATUS, C   ;And check if Borrow status occurred

    GOTO END_SPLIT_10    ;If it was, then end operation

    INCF num_m, F   ;Otherwise increment 'num_m'

    GOTO DIVIDE_10   ;And return to the 'DIVIDE_10' label

END_SPLIT_10

    ADDWF num_l, F   ;Add W to 'num_l'

    RETLW 0   ;And return from the subroutine

;------------Decode digit----------------------

DECODE_DIGIT

    MOVWF digit   ;Copy W into 'digit'

    MOVF digit, F   ;Check if 'digit' is 0

    BTFSC STATUS, Z   ;If yes, then return with the code

    RETLW B'00111111' ;to display '0' digit

    DECFSZ digit, F   ;Decrement the 'digit' and check if it's 0

    GOTO $+2   ;If not, skip the next line

    RETLW B'00000110'   ;Otherwise return with the code of '1'

    DECFSZ digit, F   ;Decrement the 'digit' and check if it's 0

    GOTO $+2   ;If not, skip the next line

    RETLW B'01011011'   ;Otherwise return with the code of '2'

    DECFSZ digit, F   ;Decrement the 'digit' and check if it's 0

    GOTO $+2   ;If not, skip the next line

    RETLW B'01001111'   ;Otherwise return with the code of '3'

    DECFSZ digit, F   ;Decrement the 'digit' and check if it's 0

    GOTO $+2   ;If not, skip the next line

    RETLW B'01100110'   ;Otherwise return with the code of '4'

    DECFSZ digit, F   ;Decrement the 'digit' and check if it's 0

    GOTO $+2   ;If not, skip the next line

    RETLW B'01101101'   ;Otherwise return with the code of '5'

    DECFSZ digit, F   ;Decrement the 'digit' and check if it's 0

    GOTO $+2   ;If not, skip the next line

    RETLW B'01111101'   ;Otherwise return with the code of '6'

    DECFSZ digit, F   ;Decrement the 'digit' and check if it's 0

    GOTO $+2   ;If not, skip the next line

    RETLW B'00000111'   ;Otherwise return with the code of '7'

    DECFSZ digit, F   ;Decrement the 'digit' and check if it's 0

    GOTO $+2   ;If not, skip the next line

    RETLW B'01111111'   ;Otherwise return with the code of '8'

    DECFSZ digit, F   ;Decrement the 'digit' and check if it's 0

    GOTO $+2   ;If not, skip the next line

    RETLW B'01101111'   ;Otherwise return with the code of '9'

    RETLW 0x00   ;In any other case return 0

;-------------Long delay subroutine--------------

DELAY     

MOVWF i    ;Copy the value to the register i

MOVWF j    ;Copy the value to the register j

DELAY_LOOP    ;Start delay loop

DECFSZ i, F    ;Decrement i and check if it is not zero

GOTO DELAY_LOOP  ;If not, then go to the DELAY_LOOP label

DECFSZ j, F    ;Decrement j and check if it is not zero

GOTO DELAY_LOOP  ;If not, then go to the DELAY_LOOP label

RETLW 0    ;Otherwise return from the subroutine

    END

The program is quite long but we already know the majority of it, because it was introduced in the tutorial devoted to the thermometer, so please refer to it for the details.

In the register definition part I described three new ones: ‘num_l’ , num_m’, and ‘num_h’ which represent the lower, middle, and higher number to be displayed (lines 9-11).

In the GPIO definitions I added two more pins: ‘rc’ (line 15) for the output, to which the RC-chain is connected , and ‘comp’ (line 16) for the input, to which the output of the comparator is connected.

#define CALIBRATION       ;Calibration mode

;#undefine CALIBRATION    ;Uncomment for normal mode

__CONFIG _WDT_OFF & _CP_OFF & _MCLRE_OFF

ORG 0x0000

In line 18 we see something new, it’s the preprocessor directive ‘#define’. If you are familiar with the C language, you don’t need an explanation, but for others I’ll describe it briefly.

This directive allows us to define some value to use it further in the program. In fact, it’s similar to the ‘EQU’ directive of the assembler, but it can be used to perform conditional compilation. So, what does this mean?

If you define some literal by the #define directive (in our case it’s ‘CALIBRATION’), you can use the other directives: #ifdef - #else - #endif (or #ifndef - #else - #endif) to compile certain parts of the code. I’ll explain them later when we meet them in the code.

In line 19 there is another new preprocessor directive ‘#undefine’. Its action is the opposite of the #define directive, it allows us to discard the previously defined value. In our case, if we want to run the program in calibration mode to see the values of the timer, we need to comment this line, and leave the ‘CALIBRATION’ value defined. Otherwise, if we are ready to run the program in the normal mode, we should uncomment this line, and thus undefine the ‘CALIBRATION’ value.

INIT

MOVLW  ~((1<<T0CS)|(1<<PSA))   ;enable GPIO2

OPTION         ;and set timer prescaler to 256

    

    MOVLW (1<<dio)|(1<<clk)|(1<<comp)

    MOVWF port   ;It's used to switch DIO/CLK pins direction

    TRIS GPIO   ;Set 'clk', 'dio', and 'comp' as inputs

    CLRF GPIO    ;Clear GPIO to set all pins to 0

In lines 26, 27 we configure GP2 as the output, also we configure Timer 0 to be clocked by the internal source, set the timer prescaler to 256, and apply the prescaler to Timer 0.

In lines 28-30 we configure all GPIOs as inputs except for ‘rc’, which is the output.

LOOP   ;Main loop of the program

    MOVLW (1<<rc)   ;Set 'rc' pin high to charge the capacitor

    MOVWF GPIO

    MOVLW D'255'       ;Delay for 200 ms to make sure it's charged

    CALL DELAY    

    CLRF GPIO   ;Clear GPIO to set 'rc' low and start discharging

    CLRF TMR0   ;Reset the timer

In line 35, the main loop of the program begins.

First, we set the ‘rc’ pin high (lines 36-37). Here’s another weird thing which I couldn’t explain. If I used the BSF instruction to set ‘rc’ high then the TWI refused working, just nothing happened on the bus. But as soon as I replaced the read-modify-write instruction BSF with the pair of MOVLW-MOVWF, everything became fine.

So, when we set the ‘rc’ pin high, the capacitor C1 starts to charge through the R1 resistor. Then we wait for some time (about 200 ms) to make sure it’s fully charged (lines 38, 39). Then we set the ‘rc’ pin low (line 40) by calling the CLRF GPIO instruction (here I also decided to eliminate the BCF instruction), and also reset the TMR0 register (line 41) to clear the timer value.

WAIT_FOR_COMPARATOR      ;And start waiting while comparator sets low

MOVLW 0xFF   ;Check if the timer value becomes 255

    XORWF TMR0, W

    BTFSC STATUS, Z    

    GOTO CHECK_RESULT    ;and leave the waiting loop in this case

    BTFSC GPIO, comp     ;Otherwise check the comparator output

    GOTO WAIT_FOR_COMPARATOR   ;If it's still 1, return to the waiting

Then we start waiting for the comparator output to become low in the loop starting with the label ‘WAIT_FOR_COMPARATOR’ (line 42). In the loop, we first check if the TMR0 value becomes 0xFF (lines 43-45) which is its maximum possible value, and if it reaches this value, we leave the loop and go to the ‘CHECK_RESULT’ label (line 46). We are OK with that, as according to Table 1, if t > 240, we consider the input voltage as 0.

Then we check the ‘comp’ input (line 47) and if it’s still high, we return to the ‘WAIT_FOR_COMPARATOR’ label (line 48). Otherwise line 48 is skipped, and we get to the ‘CHECK_RESULT’ label, located in line 50.

CHECK_RESULT   ;Check the timer value

    MOVF TMR0, W       ;Copy the TMR0 value

    MOVWF num_l   ;into the 'num_l'

#ifndef CALIBRATION   ;If normal mode

    CALL DECODE_VOLTAGE  ;Decode voltage

    MOVWF num_l   ;And copy the value of W register into the 'num_l'

#endif

    CALL SPLIT_NUM   ;Then split the 'num_l' into digits

Here, we first of all copy the value of TMR0 into the ‘num_l’ register (lines 51, 52), as during the calculations the TMR0 can change and distort the results.

In line 53, we meet, for the first time, the above-mentioned directive #ifndef. It means that the following part of the code will be implemented only if the literal is not defined. The action of this directive lasts till the directive #else or #endif is met. In the following case we meet the #endif directive in line 56, which means that lines 54, 55 will be compiled and implemented only if the ‘CALIBRATION’ literal is not defined. And this will happen if we uncomment line 19 and undefine ‘CALIBRATION’.

So in line 54 we call the ‘DECODE_VOLTAGE’ subroutine, in which the timer value is converted into the voltage according to Table 1 (I will describe it later). This subroutine returns the 2-digit number which represents the higher two digits of the voltage, in the W register (for instance if voltage is 2.75V, it will return 27). This is done to fit the returned value in one byte, because otherwise for voltages above 2.5V the value will be bigger than a byte (for instance 275 > 255). To restore the last digit (5 or 0) we will check the lower returned number. If it’s 0 or 5 then the last digit is 0, and if it’s 2 or 7 then the last digit is 5.

After the ‘DECODE_VOLTAGE’ subroutine, we need to copy the W register into the ‘num_l’ register (line 55). Then we call the ‘SPLIT_NUM’ subroutine (line 57) which is very similar to the ‘SPLIT_TEMP’ of the thermometer tutorial. After it, we will have the separated digits in the registers ‘num_h’, ‘num_m’, and ‘num_l’.

But what happens if we are in calibration mode, and the ‘CALIBRATION’ value is defined? In this case lines 54, 55 are ignored, and we call the ‘SPLIT_NUM’ (line 57) having the TMR0 value in the ‘num_l’ register (after lines 51, 52).

;Send first command to LED display

CALL TW_START   ;Issue Start condition

    MOVLW 0x40   ;Send command 0x40 -

    CALL TW_WRITE_BYTE   ;"Write data to display register"

    CALL TW_STOP       ;Issue Stop condition

;Send second command and data to LED display

    CALL TW_START   ;Issue Start condition

    MOVLW 0xC0   ;Send address 0xC0 -

CALL TW_WRITE_BYTE   ;The first address of the display

Then we display the split value in the LED indicator (lines 59-103). The first and the third commands are the same as in the thermometer tutorial, the only difference is in the displayed values.

We will display different values in different formats according to the mode (calibration or normal). In calibration mode, we will leave the first digit blank, and then display the 3 digits of the TMR0 value (hundreds in ‘num_h’, tens in ‘num_m’, and units in ‘num_l’). In normal mode, we will display the first two digits of the voltage in mV at the first two positions. In the third position we will show either 0 (if the previous value is 0 or 5) or 5 (if the previous value is 2 or 7). And in the fourth position we always show 0 as we don’t measure the units of millivolts. Let’s see how it’s implemented.

#ifdef CALIBRATION   ;In calibration mode

MOVLW 0   ;Blank the first digit

#else   ;In normal mode

    MOVF num_m, W   ;display the 'num_m' value

    CALL DECODE_DIGIT    ;Get the display data

#endif

CALL TW_WRITE_BYTE   ;Write the first digit to the display

In line 68, we meet a similar yet new directive #ifdef. Its meaning is opposite to the #ifndef. It means that the next part of the code will be implemented if the mentioned literal has been defined, and was not undefined then. So in calibration mode we set the value 0 at the first position (line 69), and thus leave all segments off.

In line 70 there is the #else directive. It means that the part, started with the #ifdef (or #ifndef) directive is finished, and the following part will be compiled only in case of the opposite condition (in our case, if ‘CALIBRATION’ is undefined). As we agreed earlier, we will display the first digit of the voltage (the thousand of millivolts), stored in the ‘num_m’ register. So we copy its value into the W register (line 71) and call the ‘DECODE_DIGIT’ subroutine (line 72) which is absolutely the same as in the thermometer tutorial.

#ifdef CALIBRATION   ;In calibration mode

MOVF num_h, W   ;Display the 'num_h' value

#else   ;In normal mode

    MOVF num_l, W   ;Display the 'num_l' value

#endif

    CALL DECODE_DIGIT    ;Get the display data

CALL TW_WRITE_BYTE   ;Write the second digit to the display

In lines 75-81 we select and display the second digit: in calibration mode it is the hundred of the timer value written in the ‘num_h’ register (line 76), in normal mode it is the second digit returned by the ‘DECODE_VOLTAGE’ subroutine (‘num_l’ register) which represents the hundred of millivolts (line 78).

#ifdef CALIBRATION   ;In calibration mode

MOVF num_m, W   ;Display the 'num_l' value

#else   ;In normal mode

    CLRW       ;Load 0 into the W register

    BTFSC num_l, 1   ;if bit 1 of 'num_l' is 1 (if 'num_l' is 2 or 7)

    MOVLW 5   ;then load 5 into the W register

#endif

    CALL DECODE_DIGIT    ;Get the display data

CALL TW_WRITE_BYTE   ;Write the third digit to the display

In lines 82-90 we select and display the third digit: in calibration mode, it is the tens of the timer value (‘num_m’ register) (line 83), in normal mode it is the tens of millivolts, and we set it to 5 or 0 depending on the previous value, as I mentioned before. So initially we load 0 into the W register (line 85), and in the next line we check bit 1 of the ‘num_l’ register. To understand why we do this, let’s write the numbers 0, 2, 5, and 7 in the binary form:

0 - 0b00000000
2 - 0b00000010
5 - 0b00000101
7 - 0b00000111

So we need something that is common at 2 and 7 but is absent at 0 and 5. If you look carefully, you will notice, that bit #1 of 2 and 7 is 1, but at 0 and 5 this bit is 0. So to distinguish these two groups of numbers (and we are sure we will not have any others but these) we just need to check the bit 1.

So we check bit 1 of the ‘num_l’ register (line 86) and display ‘5’ if it is 1 (line 87), because the previous digit was 2 or 7. Otherwise line 87 is skipped and the W remains 0.

#ifdef CALIBRATION   ;In calibration mode

MOVF num_l, W   ;Display the 'num_l' value

#else   ;In normal mode

    MOVLW 0   ;Display '0'

#endif

    CALL DECODE_DIGIT    ;Get the display data

CALL TW_WRITE_BYTE   ;Write the fourth digit to the display

    CALL TW_STOP       ;Issue Stop condition

;Send brightness command to LED display

    CALL TW_START   ;Issue Start condition

    MOVLW 0x8F   ;Send display control byte 0x8F -

    CALL TW_WRITE_BYTE   ;"Display ON"

    CALL TW_STOP    ;Issue Stop condition

In lines 91-95 we select and display the fourth digit: in calibration mode it is the units of the timer value written in the ‘num_l’ register (line 92), in normal mode it’s always 0 (line 94).

MOVLW D'255'       ;Perform 200 ms delay

    CALL DELAY   ;before the next measurement

GOTO LOOP        ;loop forever

After displaying the value we perform another 200 ms delay (lines 105, 106) and return to the beginning of the main loop.

Now let’s consider the subroutines.

All the subroutines implementing the TWI bus were described in the thermometer tutorial, so I’ll skip the description of lines 110-170 and jump directly to line 172 where the ‘DECODE_VOLTAGE’ subroutine starts.

;------------Decode the voltage-----------------

DECODE_VOLTAGE

MOVLW D'9'   ;If the 'num_l' value is less than 9

    SUBWF num_l, W    

    BTFSS STATUS, C

    RETLW D'35'   ;then U=3.5V, load 35 into the W register

    MOVLW D'12'   ;If the 'num_l' value is less than 12

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'32'   ;then U=3.25V, load 32 into the W register

    MOVLW D'16'   ;If the 'num_l' value is less than 16

    SUBWF num_l, W    

    BTFSS STATUS, C

    RETLW D'30'   ;then U=3.0V, load 30 into the W register

    MOVLW D'20'   ;If the 'num_l' value is less than 20

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'27'   ;then U=2.7V, load 27 into the W register

    MOVLW D'24'   ;If the 'num_l' value is less than 24

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'25'   ;then U=2.5V, load 25 into the W register

    MOVLW D'31'   ;If the 'num_l' value is less than 31

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'22'   ;then U=2.25V, load 22 into the W register

    MOVLW D'38'   ;If the 'num_l' value is less than 38

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'20'   ;then U=2.0V, load 20 into the W register

    MOVLW D'46'   ;If the 'num_l' value is less than 46

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'17'   ;then U=1.75V, load 17 into the W register

    MOVLW D'58'   ;If the 'num_l' value is less than 58

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'15'   ;then U=1.5V, load 15 into the W register

    MOVLW D'63'   ;If the 'num_l' value is less than 63

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'12'   ;then U=1.25V, load 12 into the W register

    MOVLW D'73'   ;If the 'num_l' value is less than 73

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'10'   ;then U=1.0V, load 10 into the W register

    MOVLW D'87'   ;If the 'num_l' value is less than 87

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'7'   ;then U=0.75V, load 7 into the W register

    MOVLW D'148'       ;If the 'num_l' value is less than 148

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'5'   ;then U=0.5V, load 5 into the W register

    MOVLW D'240'       ;If the 'num_l' value is less than 240

    SUBWF num_l, W

    BTFSS STATUS, C

    RETLW D'2'   ;then U=0.25V, load 2 into the W register

    RETLW 0   ;Otherwise U=0V, load 0 into the W register

It’s long enough (lines 172-243) but it consists of 14 identical blocks (according to the number of lines in Table 1), so I’ll describe only one of them.

In line 173, we load the number ‘9’ into the W register according to the last line of Table 1 (we will check from the last line to the first). Then we subtract this value from the ‘num_l’ register and save the result into the ‘W’ register (line 174) as we still need the value stored in ‘num_l’. After that, we check the Carry/Borrow bit of the STATUS register (line 175). If it is 0 (the borrow event occured, which means that the ‘num_l’ is smaller than 9) then we know that the voltage is 3.5V or greater (according to last line of Table 1) and thus we return from the subroutine with the value ‘35’ in the W register (line 176).

The other groups are the same: we compare if the ‘num_l’ value is smaller than the loaded into W register number according to Table 1, and if it is so, then we return from the subroutine with the corresponding voltage value.

;------------Split number-----------------

SPLIT_NUM

#ifdef CALIBRATION   ;We have the hundreds only in calibration mode

CLRF num_h   ;Clear the 'num_h' register

    MOVLW D'100'       ;Load 100 into the W register

DIVIDE_100   ;Start loop of the division operation

    SUBWF num_l, F   ;Subtract W from 'num_l'

    BTFSS STATUS, C   ;And check if Borrow status occurred

    GOTO END_SPLIT_100   ;If it was, then end operation

    INCF num_h, F   ;Otherwise increment 'num_h'

    GOTO DIVIDE_100   ;And return to the 'DIVIDE_100' label

END_SPLIT_100

    ADDWF num_l, F   ;Add W to 'num_l'

#endif

    CLRF num_m   ;Clear 'num_m' register

    MOVLW D'10'   ;Load 10 into the W register

DIVIDE_10   ;Start loop of the division operation

    SUBWF num_l, F   ;Subtract W from 'num_l'

    BTFSS STATUS, C   ;And check if Borrow status occurred

    GOTO END_SPLIT_10    ;If it was, then end operation

    INCF num_m, F   ;Otherwise increment 'num_m'

    GOTO DIVIDE_10   ;And return to the 'DIVIDE_10' label

END_SPLIT_10

    ADDWF num_l, F   ;Add W to 'num_l'

    RETLW 0   ;And return from the subroutine

The ‘SPLIT_NUM’ subroutine (lines 245-269) also doesn’t have anything new if you read my explanation of the homework from the thermometer tutorial devoted to converting the temperature from the Celsius into Farenheit. First, we divide the ‘num_l’ value by 100 (lines 246-257) and save the result into the ‘num_h’ register. Then we divide the modulo of the previous operation by 10 (lines 259-268) and save the result in the ‘num_m’ register, and the modulo in the ‘num_l’ register. Please note that the division by 100 part is surrounded by the #ifdef CALIBRATION …. #endif directives (lines 246 and 257 respectively). This is done because in normal mode we get just a 2-digit number from the ‘DECODE_VOLTAGE’ subroutine, and thus we don’t need to get the hundreds value.

The ‘DECODE_DIGIT’ and ‘DELAY’ subroutines are also familiar to you, so I’ll not stop on them, and suddenly - that’s it! We’re done.


Now we can start doing some practical work.

First, assemble the device according to figure 1, but disconnect the wires DIO and CLK from the LED display, and also the comparator output from the GP3 pin to prevent its damage during the programming. Second, comment line 19 of the code to run in the calibration mode, and then compile and load the program. Then disconnect the wires MCLR/VPP, ICSPDAT, and ICSPCLK from the programmer, and connect the previously removed wires.

Now apply the voltage source to the Ux input. You can use a simple potentiometer of 1-10 kOhm to set the voltage. Now connect the example or comparing voltmeter to the Ux input as well and set the voltage to about 0.12-0.13V by rotating the potentiometer handle. Then read the value from the LED display, it’s number of timer samples, and write down both values into the table.

Repeat these steps for all the voltages up to the maximum value limited by your comparator abilities. Now change the ‘DECODE_VOLTAGE’ subroutine according to your values, then uncomment the line 19, and recompile the project. Now you will have the normal mode operation. Don’t forget to disconnect the wires from the comparator and LED indicator before reprogramming the device, and also to disconnect the wires from the programmer after finishing the programming.

Now you should have the functioning voltmeter. Apply the different voltages to the Ux input and compare the values that you have with your device and with the example voltmeter - they should be similar (within the defined accuracy, obviously).

Now, I want to illustrate you what happens inside the device with some oscillograms (figures 3 and 4).

Figure 3. Signals when Ux = 2V.
Figure 3. Signals when Ux = 2V.
Figure 4. Signals when Ux = 3.5V.
Figure 4. Signals when Ux = 3.5V.

The cyan line in figures 3 and 4 is the voltage on the capacitor C1. You can see how it charges and discharges exponentially. The yellow line is the comparator output. You may notice that in fig. 3 the toggling of the comparator output happens at the lower voltage on the capacitor because the Ux applied to the negative input of the comparator is lower.

In conclusion, I want to say that we’ve learnt how to make the ADC using a simple RC-chain and the analog comparator. Obviously this method is not very accurate, also the comparator I used is quite poor, but still, as an illustration of the ability to convert an analog signal into digital form, it’s fine. This time the program is much larger than the DAC one, it uses 203 words in calibration mode and 200 words in normal mode. Also, we’ve learnt new preprocessor directives which allow us to create the program with the conditional compilation: #define, #undefine, #ifdef - #else - #endif, #ifndef - #else - #endif.


As homework, I want to suggest you make a capacitance-meter. There are a lot of ohmmeters but not every multimeter can measure the capacitance. I’ll explain to you how it should work, but will not give you the program, as you are now experienced enough to write it by yourself.

So if you look at the formula of the capacitor discharge, you may notice that the speed of changing the voltage Uc depends on the resistance and the capacitance. But what if we set the voltage Uc at some known level and try to calculate the capacitance from this equation? Let’s try and see what happens.

If we select the Uc = 2.7U0 then,

In this case, C = t/R, which is a very simple formula, you just measure the time that the capacitor discharges to the value of U0/2.7 and then divide this time by the known resistance. This formula also works in reverse: you can calculate the unknown resistance, knowing the capacitance.

Figure 5. Schematic diagram of the capacitance meter.
Figure 5. Schematic diagram of the capacitance meter.

To get the voltage value of 1/2.7 we can use the voltage divider made of the resistors 51 kOhm (R2) and 30 kOhm (R3) as shown in figure 5. This might look confusing but the divider output voltage is VCC*R3/(R2+R3) = VCC*30/(51+30)=VCC/2.7. Also, changing the resistance R1 you can change the measurement range.

Make Bread with our CircuitBread Toaster!

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

What are you looking for?