Thursday, 14 November 2013

Reading analogue data directly from the registers of a PIC 16F1825 with SourceBoostC

It's over a year ago since we first made a WAV player using a PIC16F1825 and a transistor, but with Xmas coming up, it's time to start thinking about making geeky-gifts and nerdy presents, so they can be finished in plenty of time for the big day.

This year we're making  some "talking toys".
The idea is to have a base with the sound player part in it, and a couple of connections exposed on a top surface, connected to a couple of magnets. Different characters will be placed on the base, triggering different sounds.

By simply embedding a resistor inside each of the characters, we can change the "bottom half" of a voltage divider and detect which character has been placed on top of the contacts.


Since we're using pretty much the same code as last time for playing WAVs, we've a limited number of pins we can use for the analogue input. Luckily, most of the pins on the 16F1825 can be used as analogue inputs - so we picked RC2 (pin8) as it wasn't being used for anything important.

Now before we go hacking our wav player code, we just wanted to check that we could read analogue input values accurately. In Oshonsoft, this was a doddle - two lines of code. In our now-preferred compiler,  SourceboostC it's not so obvious.

There are a few functions that can assist - things like adc_get(x) but in practice they didn't work very well. After doing a bit of searching, it seems that you need to prepare the correct registers for your target chip before the built-in Sourceboost ADC functions will work properly.

Well, while we're digging about in the datasheets we thought we may as well do the whole thing "old-school" using registers instead of pre-built functions and libraries (SourceboostC has renewed our interest in understanding what these magic little black boxes can do by hitting the datasheet quite often, while Oshonsoft tends to encourage you to rely on pre-built functions; some people like the simplicity of the abstracted layers - we like to know what's going on under the hood!)

For the 16F1825 there are two registers you need to focus on: ADCON0 and ADCON1
The chip puts the analogue inputs into "channels" and you really need to use the datasheet to get the channel numbers - you couldn't just guess that, in our case, RC2 is analogue channel 6!
We need to tell the chip which "channel" to listen to for the analogue-to-digital conversion by setting bits 6-2 of the ADCON0 register. Then set bit zero to tell it to activate the A2D peripheral.

ADCON1 is where we set the acquisition speed, positive and negative voltage references and so on. We went for the slowest option (fosc/64) to hopefully give more reliable values.

In the code below, we sample the analogue input (RC2) every 1/4 second and convert the received value into an id number (our ID numbers run from 0-9). If the received ID number is different to the previous value, we then display a message over UART. In the fullness of time, this will be used to start/stop different sound samples - for now we're just focussing on getting the A2D conversion working, so replying over UART/serial is just fine!


#include <system.h>

// ##################################
// using a 32 Mhz internal oscillator
// on a PIC16F1825
#pragma DATA _CONFIG1, 0x0804
#pragma DATA _CONFIG2, 0x1DFF
#pragma CLOCK_FREQ 32000000
// ##################################

char lastID;
char id;

// program initialise
void initialise(){

       osccon=0b11110000;     // oscillator=32mhz
       delay_ms(50);          // wait for internal voltages to settle
   
       intcon =0b11000000;    // global interrupts enabled and peripheral interrupts
       apfcon0=0b01000000;    // alternative tx/rx so pickit can use prog pins for uart
       trisa=0x00;            // portA is all outputs
       ansela=0x00;           // turn off analogue pins on portA  
       trisc= 0b00000100;     // RC.2 is an input (analogue in)
       wpuc = 0b00000100;     // any inputs on portC have weak pull-ups (except RC2)
       anselc=0b00000100;     // turn off analogue pins on portC except RC2
       porta=0x00;            // set all output pins on portA off
       portc=0x00;            // turn off all output pins on portC
     
       option_reg=0x00;
       option_reg.7=0;        // weak pull-ups enabled on any input pins  
}


void initUART(){
       //
       // UART
       //
       baudcon.4 = 0;    // SCKP            synchronous bit polarity
       baudcon.3 = 0;    // BRG16            enable 16 bit brg
       baudcon.1 = 0;    // WUE            wake up enable off
       baudcon.0 = 0;    // ABDEN            auto baud detect
         
       txsta.6 = 0;        // TX9            8 bit transmission
       txsta.5 = 1;        // TXEN            transmit enable
       txsta.4 = 0;        // SYNC            async mode
       txsta.3 = 0;        // SEDNB            break character
       txsta.2 = 0;        // BRGH            high baudrate
       txsta.0 = 0;        // TX9D            bit 9

       rcsta.7 = 1;        // SPEN serial port enable
       rcsta.6 = 0;        // RX9 8 bit operation
       rcsta.5 = 1;        // SREN enable receiver
       rcsta.4 = 1;        // CREN continuous receive enable
         
       spbrgh = 0;         // brg high byte
       spbrg = 51;         // brg low byte (see datasheet for 9600@32Mhz)
         
       apfcon0.2=1;        // tx onto RA.0 (for PicKit2 sharing)
       pie1.5=1;           // rcie bit 5 on the pie1 enables interrupt on usart receive                  
}

void UARTSend(unsigned char c){
       txreg = c;
       while(!txsta.1);
}

void uart_print(char t){UARTSend(t);}

void uart_print(char *s){
       while(*s) {
           UARTSend(*s);
           s++;
       }    
}

void uart_println(char *s){
       uart_print(s);
       uart_print("\r\n");
}

void uart_print_dec(char number){
       if(number > 99) {uart_print(((number / 100) % 10) + '0');}
       if(number > 9)    {uart_print(((number / 10) % 10) + '0');}
       uart_print((number % 10) + '0');
}

unsigned char getAnalogueInput(){
       unsigned char temp;

       // start the analogue circuit
       adcon0.1=1;
       // wait for the result to come in
       // (hardware clears the bit we just set when done)
       while(adcon0.1==1){}
     
       // return the result
       temp=adresl;
       return(temp);
}

void readInput(){
       char iA;
       iA=getAnalogueInput();
       if(iA<20){
            id=9;
       }else if(iA<40){
            id=8;
       }else if(iA<60){
            id=7;
       }else if(iA<80){
            id=6;
       }else if(iA<100){
            id=5;
       }else if(iA<120){
            id=4;
       }else if(iA<140){
            id=3;
       }else if(iA<160){
            id=2;
       }else if(iA<180){
            id=1;
       }else{
            id=0;
       }

       if(id!=lastID){
            uart_print("new id ");
            uart_print_dec(id);
            uart_println(" ");
       }

       lastID=id;
}

// main program executes from here
void main(){

       initialise();
       delay_ms(50);
       initUART();
       delay_ms(250);
       uart_println("Let's go!");

       // start the analogue circuit (right justified)
       adcon0=0b00011001;    // bit 7 blank, 6-2 are the channel and bit one activates
       adcon1=0b11110000;

       // this is the main code loop
       while(1){
            readInput();
            delay_ms(250);    
       }
}


The key to capturing analogue data is in the getAnalogueInput function.
Set the first bit to start capturing data on the selected analogue "channel". When the data acquisition is complete, and the PIC has converted the analogue input into a digital value, this same bit gets cleared. In our example above, we've used a simple "blocking" loop - i.e. loop until the bit is cleared (or, while the bit is set, it amounts to the same thing). Once the data has been converted, the first bit of the ADCON0 register is cleared (by the hardware) and the rest of the code can execute.

The 10 bit value is stored in the ADRESH and ADRESL registers.
Simply read these values to get the converted value. You can set the ADCON1 register to determine whether this value should be "left shifted" or "right shifted" in the registers. For testing, we're just taking the value from the lower byte register, but in a working product, we should really be combining these two values into a two-byte integer for better accuracy.

But that's it - the code to get data from the analogue-to-digital converter isn't actually more more complex by reading/writing to registers than it is to use the abstracted adc_ functions in SourceBoost. And by doing it this way, we've reacquainted ourselves with the 16F1825 chip and the underlying hardware.