/* 4 Channel Analog to Digital convertor */ /* PJL, Jan 2009 */ /* 4 channels at 12 bit (10 bit, oversampled * 32) */ /* with RS232 output for logging and input for customisation */ /* 1200 Baud, asynchronous, pins 5 (Gnd), 2 (Tx), 3 (Rx) connected */ /* Input commands allow the user to program a sampling rate (default 5 Sec */ /* an ID string, reset the seconds count and fine-tune the processors */ /* internal oscillator (this is a maintenance mode and should be needed only rarely */ /* All programmed parameters are set temporarily, but can be hard-set into eeprom */ /* so they are preserved across poiwer downs */ /* The format of the output is: * ADCx ssssssss.dd aaaa bbbb cccc dddd * * where ADCx is a 4 character ID string * ssssssss.dd is the time since last power-up or user reset, * 8 digits and 2 decimals places of time, for 1/100th second resolution * aaaa .. dddd ADC output as decimal values, range 0 - 4095 * each string is terminated with ASCII 13, 10 * * The string is sent at the end of each measurement at 1200 Buad (non-changeable) * for a minimum practical sampling rate of 3 readings per second - limited by the Baud rate * */ /* Command syntax is: * with *no* spaces between the command letter * and the parameters. * Note. There is no error checking, only send correct data * * At present the commands are all single letters. One of: * * C calibrate on-chip oscillator * P patch eeprom (writing 'y' to location 00, hard-sets other paramter shanges * D set the delay between samples/output. data is 8 characters of HEX, in 1/100 second units * T reset the internal timer back to 0.0 seconds * I change the 4 character ID to a different string * */ /* /* When run from an on-board supply of 4.096V, */ /* the ADC has a sensitivity of 1 bit per milliVolt */ /* planned mods. * change output routine to remove "pauses" during output * add ident strings to each output channel, to follow the data - e.g. "0123mV" "7777 Amps" etc. * */ #include #include __CONFIG(FCMDIS & IESODIS & BOREN & MCLRDIS & PWRTEN & UNPROTECT & WDTDIS & INTIO); #define _XTAL_FREQ 8000000 #define TMR0_BAUD (256-133) /* trial and error value, comes to 3603 intr/Sec */ int read_adc(char); char to_hex(int); void pr_ulong(unsigned long); void pr_ch99(char); void pr_int(unsigned int); void wr_eeprom(unsigned char, unsigned char); unsigned char rd_eeprom(unsigned char); unsigned char hex2num(char, char); char proc_ip(void); volatile char tick_10ms; volatile unsigned long tick_1s; volatile unsigned long delay_ticks; volatile unsigned char TxFlags; volatile unsigned char RxFlags; volatile char InByte; volatile char RecSkips; volatile char TxSkips; volatile char RxChar; volatile char TxChar; #define TxBUF_SIZE 32 volatile char TxBuf[TxBUF_SIZE]; #define TxBUF_MASK (TxBUF_SIZE-1) volatile char RxBuf[8]; volatile char RxIPtr; volatile char RxOPtr; volatile char TxIPtr; volatile char TxOPtr; // char buf[8]; /* Enqueue data in the transmit buffer to send down the serial line */ /* This routine adds one character at a time to the 32 byte buffer */ /* (the size is constrained b the compiler and RAM limitations of the */ /* processor). When a character has been added, the "new data" flag is */ /* set to tell the interrupt routine to restart sending data. */ /* Note: data can be added to the buffer faster than the interrupt */ /* routine can transmit it */ void SendChar(char c) { while(TxFlags & 0x10) ; /* Tx buffer full */ GIE = 0; /* disable interrupts to avoid */ /* a race condition */ TxBuf[TxOPtr++] = c; TxOPtr = TxOPtr & TxBUF_MASK; TxFlags |= 0x80; /* new data to send */ if(TxOPtr == TxIPtr) { TxFlags |= 0x10; } /* buffer now full */ GIE = 1; /* safe to restart them now */ } /* TMR0 drives the Baud rate generator. It's frequency is 3 times */ /* the required Baud rate - although there is some trial and error */ /* needed to account for software delays in servicing the routine */ /* The current value (256-133) is for 1200 Baud */ void tmr0_setup() { T0IE = 1; /* enable TMR0 interrupts */ TMR0 = TMR0_BAUD; /* overflow 278 uS for 1200 Bd */ } /* Initialise TMR2. This is used to provide 10mSec interrupts */ /* which update the internal time (in 1/100 sec tick and second */ /* and also drives the ADC delay loop, which defines the dealy */ /* between sending lines of ADC data to the host */ void tmr2_setup() { tick_10ms = 0; tick_1s = 0; T2CON = (0x4 << 3) | 0x4 | 0x2; /* 1:5 postscale, timer on, 1:16 prescale */ PR2 = 249; /* 2MHz / (16 * 250 * 5) == 10mSec */ TMR2IE = 1; /* enable TMR2 interrupt */ PEIE = 1; /* turn on PIE interrupts */ } /* Initialise the ADC */ void adc_setup() { ANSEL = 0x5f; /* Fosc/16 (Tad = 2uS) */ /* ANS0,1,2,3 == analog */ ADCON0 = 0x80 | (0 << 2) | 0x01; /* right-justified, channel 0, ADC enabled */ __delay_us(20); /* allow acquisition time 11 * Tad */ } /* Main code starts here */ /* Perform processor initialisation and then call the various routines */ /* to initilalise the peripherals (timers, ADC etc.) and then enable */ /* interrupts */ /* Go into the main program loop, taking ADC measurements and outputting */ /* them as RS232 data. Check for input data that can alter the frequency */ /* of the loop (number of 1/100th second ticks between running it) and */ /* the ID of the chip, which is send as part of the serial data packet. */ void main(void) { int i; int adc0, adc1, adc2, adc3; /* The loop delay value is a 32 bit integer. In 1/100ths of a second */ /* allows for delays between readings of (2**32)/100 ~ 43million */ /* seconds, or 497 days, although only the lower 24 bits can be set */ /* through the "DELAY" command, the top 8 bits must be patched in. */ /* This is a design decision (ahem) to minimise stupid accidents */ /* We use a union to ease setting the 8-bit values in the unsigned long */ union { unsigned long ul; struct { unsigned char uc_1; unsigned char uc_2; unsigned char uc_3; unsigned char uc_4; } uc; } read_delay; unsigned char t10ms; unsigned long t1s; OSCCON = 0x71; /*01110001 8MHz */ INTCON = 0; /* no interrupts - ever! */ OPTION = 0x80|0x1; /* Weak pullup, TMR0 prescale 1:4 */ TRISIO = 0x1f; /* GPIO3,0 == input, rest are outputs */ /* Voice of experience: You MUST */ /* set analog channels as TRIS inputs */ tmr0_setup(); tmr2_setup(); adc_setup(); TxFlags = 0; RxFlags = 0; RxIPtr = 0; RxOPtr = 0; TxIPtr = 0; TxOPtr = 0; /* This sets initial values in the EEPROM for the ADC. */ /* These include an ID and a delat time */ /* If the user has updated these values (via the serial port) */ /* Their updates will be overwritten unless a "y" - N.B. lower case */ /* is patched into the EEPROM at location zero */ if(eeprom_read(0) != 'y') { /* "y" means use preprogrammed values */ eeprom_write(3, 'A'); /* otherwise, set up defaults */ eeprom_write(4, 'd'); eeprom_write(5, 'c'); eeprom_write(6, ' '); /* initialise the unit's ID */ eeprom_write(8, 0); /* and a 5 second sample time */ eeprom_write(9, 0); eeprom_write(10, (500 >> 8)); eeprom_write(11, (500 & 0xff)); /* delay between outputs (1/100s second) */ } GIE = 1; /* allow interrupts when all set up */ GPIO5 = 1; /* idle state for serial port is 1. */ for (;;) { read_delay.uc.uc_1 = eeprom_read(11); read_delay.uc.uc_2 = eeprom_read(10); read_delay.uc.uc_3 = eeprom_read(9); read_delay.uc.uc_4 = eeprom_read(8); delay_ticks = read_delay.ul; /* timer for delay between reads */ /* delay_ticks gets dercremented */ /* 100 tiems asecond by the TMR2 intr */ adc0 = read_adc(0); adc1 = read_adc(1); adc2 = read_adc(2); adc3 = read_adc(3); TMR2IE = 0; /* prevent timer updates while we''re reading it */ t1s = tick_1s; t10ms = tick_10ms; TMR2IE = 1; SendChar(eeprom_read(3)); SendChar(eeprom_read(4)); SendChar(eeprom_read(5)); SendChar(eeprom_read(6)); SendChar(' '); pr_ulong(t1s); /* send the seconds count */ SendChar('.'); pr_ch99(t10ms); /* hundreths */ SendChar(' '); pr_int(adc0); SendChar(' '); pr_int(adc1); SendChar(' '); pr_int(adc2); SendChar(' '); pr_int(adc3); SendChar(13); /* CR LF pair to terminate the output */ SendChar(10); proc_ip(); /* Here we wait until the delay time expires, and it's time to read */ /* the next set of values. Continually check for new serial commands */ /* as the delay time can be long, and we don't want the latency of */ /* having to wait until this loop expires (coule be over a year! */ /* The proc_ip() routine returns a 1 if the dealy time is changed */ /* In this case, break out of the loop and restart immediately */ while (delay_ticks > 0){ if(proc_ip()) { delay_ticks = 0; /* break if delay is changed */ } } } } /* Check for serial input in the buffer */ /* We want to process completed strings (terminated with a LF (ASCII 10)) */ /* Once the string has been read, reset the buffer pointer for the next one */ char proc_ip(void) { unsigned char adr; unsigned char dat; if((RxFlags & 2) && (RxChar == 10)) { /* echo it back out */ SendChar(10); SendChar(13); SendChar('G'); SendChar('o'); SendChar('t'); SendChar(':'); SendChar(RxBuf[0]); SendChar(RxBuf[1]); SendChar(RxBuf[2]); SendChar(RxBuf[3]); SendChar(RxBuf[4]); SendChar(RxBuf[5]); SendChar(RxBuf[6]); SendChar(RxBuf[7]); SendChar('-'); SendChar(10); SendChar(13); if((RxBuf[0] == 'P') && (RxBuf[3] == ':')) { /* patch eeprom */ adr = hex2num(RxBuf[1], RxBuf[2]); dat = hex2num(RxBuf[4], RxBuf[5]); eeprom_write(adr, dat); } if(RxBuf[0] == 'D') { /* set ADC delay to 24 bit 1/100 sec value */ eeprom_write(8, 0); /* MSB always 0 */ dat = hex2num(RxBuf[1], RxBuf[2]); eeprom_write(9, dat); /* second MSB */ dat = hex2num(RxBuf[3], RxBuf[4]); eeprom_write(10, dat); /* third byte */ dat = hex2num(RxBuf[5], RxBuf[6]); eeprom_write(11, dat); /* last, LSB */ } if(RxBuf[0] == 'I') { /* Set ADC Ident (4 chars max) */ eeprom_write(3, RxBuf[1]); eeprom_write(4, RxBuf[2]); eeprom_write(5, RxBuf[3]); eeprom_write(6, RxBuf[4]); } if(RxBuf[0] == 'T') { /* set the internal seconds count */ tick_1s = 0; tick_10ms = 0; } if(RxBuf[0] == 'C') { /* adjust the chip's Osc frequency */ OSCTUNE = hex2num(RxBuf[1], RxBuf[2]); } RxFlags &= 2; RxChar = 0; RxIPtr = 0; return(1); } return(0); } /* convert two ASCII characers in to an 8-bit value */ unsigned char hex2num(char h1, char h2) { unsigned char num; char h; h = h1 - '0'; if(h > 9) { h -= 7; } num = h * 16; h = h2 - '0'; if(h > 9) { h -= 7; } num += (h & 0xf); return(num); } char to_hex(n) { n = n & 0xf; if(n > 9) { n += 7; } n += '0'; return(n); } /* routine to read the ADC, use oversampling */ /* to improve the resolution of the ADC from 10 to 12 bits */ int read_adc(unsigned char chn) { int i, res; ADCON0 = 0x80 | (0 << 2) | 0x00; /* clear channel number */ ADCON0 = 0x80 | (chn << 2) | 0x01; /* set channel, turn ADC on */ __delay_us(22); /* 11 * Tan @ 2uSec */ /* Here we oversample the same channel 32 times to improve ADC resolution */ /* Note: resolution, not accuracy */ /* At the end, drop the 3 LSBs to return a 12 bit result */ for (res = 0, i=0; i < 32; i++) { GODONE = 1; /* trigger conversion */ __delay_us(10); /* probably unnecessary? */ while(GODONE == 1) { /* wait for conversion to complete */ asm("nop"); /* to prevent the loop being optimised out */ } res += ((ADRESH * 256) + ADRESL); /* read Hi/Lo ADC registers */ __delay_us(22); /* New 11 * Tad for next read */ } return(res >> 3); /* return full 16 bit result */ } /* convert an 8 bit value (less than 100) to 2 ASCII characters */ void pr_ch99(char c) { SendChar('0' + (c / 10)); SendChar('0' + (c % 10)); } /* convert a 16 bit value to ASCII and transmi it */ void pr_int(unsigned int i) { // SendChar('0' + (i / 10000)); SendChar('0' + ((i / 1000) % 10)); SendChar('0' + ((i / 100) % 10)); SendChar('0' + ((i / 10) % 10)); SendChar('0' + (i % 10)); } /* convert an unsigned long to ASCII and transmit it */ /* N.B. the divisions and remainders take over 10mSec to execute */ void pr_ulong(unsigned long i) { SendChar('0' + ((i / 10000000L) % 10)); SendChar('0' + ((i / 1000000L) % 10)); SendChar('0' + ((i / 100000L) % 10)); SendChar('0' + ((i / 10000) % 10)); SendChar('0' + ((i / 1000) % 10)); SendChar('0' + ((i / 100) % 10)); SendChar('0' + ((i / 10) % 10)); SendChar('0' + (i % 10)); } /* general purpose interrupt handler */ void interrupt isr(void) { if(T0IF) { T0IF = 0; TMR0 = TMR0_BAUD; #asm ; Routine to receive asynchronous data from GPIO.3 ; credit: http://www.electro-tech-online.com/micro-controllers/18828-finding-serial-start-bit-bit-banging.html#post117227 decfsz _RecSkips,F goto DoneRS232_Rx btfss _RxFlags,0 ; b_receiving goto get_start_bit bsf _STATUS, 0 btfss _GPIO, 3 bcf _STATUS,0 rrf _InByte,F movlw 3 movwf _RecSkips btfss _STATUS, 0 goto DoneRS232_Rx movf _InByte,W movwf _RxChar bsf _RxFlags,1 ; b_byte_available bcf _RxFlags,0 ; b_receiving #endasm RxBuf[RxIPtr++] = RxChar; /* write received byte to circular buffer */ RxIPtr = RxIPtr & 0x7; #asm goto DoneRS232_Rx get_start_bit incf _RecSkips,F; set to 1 btfsc _GPIO, 3 goto DoneRS232_Rx movlw 4 movwf _RecSkips bsf _RxFlags,0 ; b_receiving movlw 80h movwf _InByte DoneRS232_Rx ; Code to transmit data from GPIO.5 ; Note: this can be done while reception is in progress, ; it''s full duplex ; Here check if there''ta being sent, or ready to send btfsc _TxFlags, 6 ; already sending ? goto Tx_Sending btfsc _TxFlags, 7 ; data to send goto Tx_Start_Bit incf _TxSkips, F ; set back to 1, so we retest next time round goto DoneRS232_Tx ; nothing to do ; come here to start sending a new byte Tx_Start_Bit decfsz _TxSkips, f goto DoneRS232_Tx bcf _GPIO, 5 ; hardware start bit #endasm TxChar = TxBuf[TxIPtr++]; TxIPtr = TxIPtr & TxBUF_MASK; #asm movlw 8 movwf _TxFlags ; initialise bit count, clear FULL flag bsf _TxFlags, 6 ; indicate we''re sending data movlw 3 movwf _TxSkips goto DoneRS232_Tx ; here if we''ve already in the process of sending data Tx_Sending decfsz _TxSkips, F ; only waggle o/p every third intr. goto DoneRS232_Tx btfsc _TxFlags, 5 ; time to send stop bit? goto Tx_Stop_Bit ; bcf _STATUS, 0 ; carry bit rrf _TxChar, f btfss _STATUS, 0 goto Tx_Set_Bit bsf _GPIO, 5 goto Tx_Sent_Bit Tx_Set_Bit bcf _GPIO, 5 ; here we''ve sent the data bit, decr the bit counter Tx_Sent_Bit movlw 3 movwf _TxSkips ; reset Tx skip counter for 3 more intrs decf _TxFlags, F ; bit count in lower 3 bits movf _TxFlags, W andlw 7 btfss _STATUS, 2 ; if result is zero goto DoneRS232_Tx bsf _TxFlags, 5 ; all data sent, sent stop bit next time goto DoneRS232_Tx ; here to send the stop bit and check if there''s more data ; to send, once this byte has completed Tx_Stop_Bit bsf _GPIO, 5 movlw 3 movwf _TxSkips bcf _TxFlags, 6 ; sending_data flag bcf _TxFlags, 5 ; send stop_bit flag btfsc _TxFlags, 4 ; check if buffer is full (then there's more to send) goto DoneRS232_Tx movf _TxIPtr, w ; check if In pointer == Out pointer subwf _TxOPtr, w btfss _STATUS, 2 ; difference is Zero? goto DoneRS232_Tx ; more to send bcf _TxFlags, 7 ; no more data to send, stop future processing DoneRS232_Tx #endasm } if(TMR2IF) { tick_10ms++; if(delay_ticks) delay_ticks--; /* timing loop for ADC reads */ if(tick_10ms > 99) { tick_10ms = 0; tick_1s++; } TMR2IF = 0; /* typ. service time 7.5uS */ } }