 # Digital Voltmeter - Part 18 Microcontroller Basics (PIC10F200)

Published

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).

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.

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.

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

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
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'
END_SPLIT_100
#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'
END_SPLIT_10
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'
END_SPLIT_100
#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'
END_SPLIT_10
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).

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.  