PIC Tutorial Eleven - Analogue Inputs
For these tutorials you require Main Board 2 or 3, the Analogue Board, the LCD Board and the RS232 Board. Download zipped tutorial files.
The 16F876 and the 16F877 both include 10 bit analogue to digital converters, the 876 provides a possible 5 inputs, and the 877 a possible 8 inputs, in both cases there's only one actual converter - it's switched to each input pin as required by setting a register.
There's generally a lot of confusion about using the A2D inputs, but it's actually really very simple - it's just a question of digging the information you need out of the datasheets. The main confusion arises from actually setting them up, in these tutorials I'll give proven working examples, which you can easily modify to use as you wish.
are four main registers associated with using the analogue inputs, these are
listed in this table:
ADRESH and ADRESL are fairly self explanatory, they are the registers that return the result of the analogue to digital conversion, the only slightly tricky thing about them is that they are in different memory banks.
ADCON0 is split into four separate parts, the first part consists of the highest two bits ADCS1 and ADCS0, and sets the clock frequency used for the analogue to digital conversion, this is divided down from the system clock (or can use an internal RC oscillator), as we are using a 20MHz clock on the tutorial boards, we have to use Fosc/32 (as given in the table below). So that's one setup decision already solved:
The second part of ADCON0 consists of the next three bits, CHS2,CHS1 and CHS0, these are the channel select bits, and set which input pin is routed to the analogue to digital converter. Be aware that only the first five (AN0-AN4) are available on the 16F876, the next three (AN5-AN7) are only available on the 16F877. Also notice that AN4 uses digital pin RA5, and not RA4 as you would expect. As the tutorial analogue input board only has two inputs, connected to AN0 and AN1, this reduces the decision to two possibilities, either 000 for AN0, or 001 for AN1, and we simply alter them as we switch between the two inputs.
The third part is a single bit (bit 2), GO/DONE, this bit has two functions, firstly by setting the bit it initiates the start of analogue to digital conversion, secondly the bit is cleared when the conversion is complete - so we can check this bit to wait for the conversion to finish.
The fourth part is another single bit (bit 0), ADON, this simply turns the A2D On or Off, with the bit set it's On, with the bit cleared it's Off - thus saving the power it consumes.
So for our application the data required in ADCON0 is binary '10000001' to read from AN0, and binary '10001001' to read from AN1. Bit 1 isn't used, and can be either '0' or '1' - in this case I've chosen to make it '0'. To initiate a conversion, we need to make Bit 2 high, we'll do this with a "BCF ADCON0, GO_DONE" line. Likewise, we'll use a "BTFSC ADCON0, GO_DONE" line to check for the end of conversion.
ADCON1 is really a little more complicated, although it's only split into two sections. The first section is a single bit, ADFM, this is the Result Format Selection Bit, and selects if the output is Right Justified (bit set) or Left Justified (bit cleared). The advantage of this is that it makes it very easy to use as an 8 bit converter (instead of ten bit) - by clearing this bit, and reading just ADRESH, we get an 8 bit result, ignoring the two least significant bits in ADRESL. For the purposes of these tutorials, I'm intending using the full ten bits - so this bit will be set.
PCFG3-0 are probably the most complicated part of setting the A2D section, they set a lot of different options, and also limit which pins can be analogue, and which can be digital:
As I mentioned above, this part looks rather complicated - but if we split it down, it starts to make more sense. There are actually four different options being set here:
For a start we need to decide what settings we actually require - first we are only using analogue inputs AN0 and AN1, which if you look down the columns eliminates four of the possibilities (0110, 0111, 1110 and 1111, shaded in blue). Secondly we are using a VRef- of Vss (Ground), so that eliminates another four (1000, 1011, 1100 and 1101, shaded in yellow) - so now we've only got eight possibly choices left (down to 50% already). Thirdly we are using an external VRef+ reference, so we need RA3 allocating to Vref+, this eliminates a further four (0000, 0010, 0100 and 1001, shaded in green). This brings us down to four possible choices and, to be honest, any of the four would work quite happily - however, one of our requirements was 'two analogue inputs', this eliminates a further three possibilities (0001, 0011 and 1010, shaded in red) - which leaves the only option which fits all our requirements '0101', so this is the value we will need to write to PCFG3:PCFG0.
So now we've decided what we need to set ADCON1 to, binary '10000101', with 0's in the places of the unused bits, this gives us two analogue inputs, Vref+ set to RA3, and Vref- set to Vss.
know the setting up details, it's time for a little more explaining about
exactly what it all means, I've mentioned earlier that the A2D has '10 bit'
resolution, this means the output of the A2D can vary from 0 (all bits '0')
to 1023 (all bits '1'). A number from 0-1023 probably isn't too helpful,
because you don't know what they represent. This is where Vref+ and Vref-
come in!. When the output of the A2D = 1023, the analogue input is at Vref+,
when the A2D output = 0, then the analogue input is at Vref-. In our case,
with the TL341 reference on RA3, 1023 is about 2.5V, and with Vref- at Vss,
0 from the A2D equals zero volts on the input.
So, to recap, 1023 represents 2.5V, and 0 represents 0V. Numbers in between represent voltages in between, to work out what they actually represent, we need a little mathematics. Dividing 2.5V by 1023 gives 0.00244, this is the voltage resolution of out A2D, so a reading of 1 represents 0.00244V (or 2.44mV), and a reading of 2 represents 0.00488V (or 4.88mV), and so on. As you'll see later on (in the programming tutorials) it's important to know the voltage resolution of the A2D, in order to calculate the voltage. Now while I said the voltage resolution is 0.00244, I was rounding the figure off, the actual value (from Windows Calculator) is 0.00244379276637341153470185728250244 - multiplying 1023 by 0.00244 only gives 2.49612V, and not the 2.5V it should be. Although this is only a 0.388% error, it doesn't look good - I'd really like the display to read from 0 to the maximum input, with out silly limitations like that.
I considered this while designing the analogue input board, and made a decision to get over the problem in the hardware. The amplifiers on the inputs have attenuators feeding them, this gives the overall amplifier an adjustable gain of around minus 4, so 10V (or so) input will give 1000 output from the A2D. So by adjusting the gain of the input amplifiers, we can use an input voltage range from 0-10.23V, which (by no coincidence at all) allows us to get a 0-1023 reading from the A2D converter giving a resolution of 10mV, which is a theoretical accuracy of around 0.1% (obviously the system itself isn't this accurate, nor is the meter we'll use to calibrate it). Another advantage of this is that it calibrates the voltage reference chip as well, this probably isn't going to be exactly 2.5V, by calibrating the inputs against a known meter we take care of this as well.
PIC Assembler routines.
As usual, this tutorial will present simple reusable routines for using the analogue inputs, as I mentioned at the beginning, it's actually really very easy to use, once you've worked through the datasheets and found out how you need to set things. For the actual programming, this is reduced down to two small routines:
Init_ADC ; Set ADCON0 movlw b'10000001' movwf ADCON0 ; Set ADCON1 BANKSEL ADCON1 movlw b'10000101' movwf ADCON1 BANKSEL ADCON0 return
This routine simply sets the values of ADCON0 and ADCON1 with the values we decided on in the discussion earlier, the only point to note is the BANKSEL commands before and after ADCON1. This register is in a different register bank to ADCON0, so we need to select the correct bank before accessing the register, as a matter of course we return to the original register bank afterwards.
Read_ADC bsf ADCON0, GO_DONE ;initiate conversion btfsc ADCON0, GO_DONE goto $-1 ;wait for ADC to finish movf ADRESH,W andlw 0x03 movwf NumH BANKSEL ADRESL movf ADRESL,W BANKSEL ADRESH movwf NumL ;return result in NumL and NumH return
This second routine is the one which actually reads the analogue input, the first line initiates the conversion, and the second and third wait for it to complete. The second section simply reads the converted value from the two registers, and stores them in variables for later use. Notice the bank switching again around ADRESL, this is because ADRESL is in a different register bank, it's important to notice that we move it's value to W, then switch banks back BEFORE storing it in the variable NumL - if we don't switch back it gets stored in the wrong register. Notice I'm AND'ing ADRESH with 0x03, this is because we only use the lower two bits of ADRESH, and although the upper six bits should all be zero - I like to make sure.
So all we need to do to use the A2D is to "call Init_ADC" during the programmes initialisation, and then "call Read_ADC" whenever we wish to read a value. In order to select the individual two analogue channels, I've simply expanded "Init_ADC" to two routines, "Init_ADC0" and "Init_ADC1", these simply set the analogue converter to the correct input pin.
These tutorials use the Analogue Board on PortA, the LCD Board on PortB, and the RS232 Board on PortC.
Tutorial 11.1 - requires main Board Two or Three, Analogue Board, LCD Board.
This tutorial reads a single channel of the A2D converter and displays it on the LCD. First the program sets up the registers explained above, then the rest of the program runs in an endless loop. It reads the analogue input, which gives a value from 0-$3FF (in hexadecimal), then calls a routine which converts that to decimal, giving 0-1023 (representing 0-10.23V). Lastly this decimal value is displayed on the LCD, followed by a space, and the same value in hexadecimal. The program then wait for 100mS and jumps back to the start of the loop and runs again, this gives roughly 10 readings a second.
Tutorial 11.2 - requires as above.
Pretty much the same as Tutorial 11.1, except this time we read both channels and display the two voltages, this demonstrates how to switch between the different channels using the two different "Init_ADC" routines. In order to keep 10 readings per second, the delay has been reduced to 50mS, so we get 20 readings per second, but still only 10 on each channel.
Tutorial 11.3 - requires as above.
Similar to the previous Tutorials, but this time we add a decimal point and a 'V' for voltage at the end of the decimal display, and we don't bother showing the hexadecimal value - just as you would for a simple voltmeter.
Tutorial 11.4 - requires RS232 board as well.
This time we add an extra board, the RS232 Board. The program is essentially Tutorial 11.3, but this time we add a serial output routine to send the data via RS232 to a PC. For the RS232 output we don't send the 'V' at the end, but we do send CR/LF in order to display each reading on a separate line. I've implemented this by modifying the LCD_Char routine and adding a couple of Flags - LCD and RS232 - by setting these flags ON or OFF we can print to either the LCD, the RS232 output, both, or neither.