/* LCD1 - LCD Volts/Amps Display This device adds an LCD to a variable Power Supply Unit (PSU) to display voltage and current The PSU is an old Coutant GPE 500/12 originally used for charging batteries It had an existing 'low side' current sense resistor in series with its output The 'component' side of Rsense is 'true ground' which is used for this PIC circuit The 'output' side is therefore at a higher potential This means that voltage measurements have to be adjusted by the voltage loss though Rsense It also means that current measurements are positive relative to true ground The voltage drop over Rsense is too small to measure with a PIC So it is amplified with an op amp, the LM358N Its low offset voltage and common mode voltage range down to 0v makes it suitable The gain is set with a trimmer but could have been fine-tuned in software The LCD is a 16x1 Sanyo DM161B which has an HD44780 compatible controller chip In order to save on pins, this program uses the '4 bit' mode Port C has been chosen to interface with the LCD The lower 4 bits are used for data (D4, D5, D6, D7) and the 5th for RS (RW is not used) So these 5 bits are set for each communication via the 'lcd' function The program uses fixed timings between instructions rather than reading LCD busy indicator Fairly slow delays (1ms) are used during initialisation as there is no rush Much quicker 'nop' delays are used where possible when writing characters (nop = 1us @ 4MHz clock) The timing results in the LCD being updated about 40 times a second at 4MHz Voltage and current is measured 16 times before each update to smooth results Code has also been written to de-jitter the results to remove 'flutter' The net effect is a fairly stable display with good correlation with a Fluke Voltage is within 0.01v at low currents and 0.03v at 4A Current is within 0.01A at low settings and within 0.1A at 4A These are very close to the inherent accuracy of this configuration Instead of using floating point arithmetic, the program uses large numbers All arithmetic ends up in 'hundreths' (ie: '1' = 0.01 amps or volts) The LCD display code inserts the decimal indicator ('.') at the appropriate location Written for PIC16F688 (14 pin) / BoostC compiler Personal and non-commercial use encouraged Copyright 2007 - David Theunissen - See www.flyelectric.ukgateway.net */ //PREPROCESSOR section ======================================================== #include //Generic device include (actual device set in Settings/Target) #include //Needed for Integer to ASCII conversion (uitoa) //Core configuration option_regs: #pragma DATA _CONFIG, _FCMEN_OFF & _IESO_OFF & _BOD_ON & _CPD_OFF & _CP_OFF & _MCLRE_OFF & _PWRTE_ON & _WDT_OFF & _INTOSCIO #pragma CLOCK_FREQ 4000000 //VARIABLES section =========================================================== #define disp portc //Defines 'disp' as PortC //Wired as: RC5=RW, RC4=RS, RC3=D7, RC2=D6, RC1=D5, RC0=D4 #define e porta.5 //Defines 'e' as the LCD Enable pin on RA5 (Pin2) typedef unsigned char u8; //Defines u8 as an unsigned char type (8bits) typedef signed char s8; //Defines s8 as an signed char type (8bits) typedef unsigned short u16; //Defines u16 as an unsigned short type (16bits) typedef unsigned int i16; //Defines i16 as an unsigned short integer (16bits) typedef unsigned long u32; //Defines u32 as an unsigned long type (32bits) u16 result_a; //Result of ADC conversions (amps) u16 result_v; //Result of ADC conversions (volts) void write_data(); //Function to write to EEPROM for testing void measure(); //Function to run ADC for amps and volts void lcd (u8 high, u8 low, u8 rs); //Function to send characters and instructions to LCD //FUNCTIONS section =========================================================== void write_data() //Function to write to EEPROM flash memory { intcon.7 = 0; //Disable all interrupts while writing to memory pir1.7 = 0; //EEIF: Clear this bit which gets set after each write eecon2 = 0x55; //Required before writing to memory eecon2 = 0xAA; //Required before writing to memory eecon1.1 = 1; //WR: Writes data eedata to address eeadr while(eecon1.1==1); //Wait for write to complete (becomes 0 when complete) eeadr++; //Increment the address by 1 for next write (8 bit 0-255) intcon.7 = 1; //Enable interrupts again } //------------------------------------------------------------------------------ void measure () //Function to trigger ADC conversions to measure voltage and amps { u8 i; //Counter //Current //Select channel adcon0.4 = 0; //Makes AN2 (pin11) channel active adcon0.3 = 1; // part of above adcon0.2 = 0; // part of above //Run ADC for(i=0; i<1; i++); //Delay for acquisition capacitor to charge (1 = ~14uS) pir1.6 = 0; //Clear the ADC Complete flag adcon0.1 = 1; //GO: Start ADC acquisition while(adcon0.1==1); //Wait for conversion to complete (becomes 0) //Merge results MAKESHORT (result_a, adresl, adresh); //Volts //Select channel adcon0.4 = 0; //Makes AN3 (pin3) channel active adcon0.3 = 1; // part of above adcon0.2 = 1; // part of above //Run ADC for(i=0; i<1; i++); //Delay for acquisition capacitor to charge (1 = ~14uS) pir1.6 = 0; //Clear the ADC Complete flag adcon0.1 = 1; //GO: Start ADC acquisition while(adcon0.1==1); //Wait for conversion to complete (becomes 0) //Merge results MAKESHORT (result_v, adresl, adresh); } //----------------------------------------------------------------------------- void lcd (u8 high, u8 low, u8 rs) //Sends characters and instructions to LCD //'high' is first nibble //'low' the second (99 prevents it from being sent) //'rs' is made high when 1 (for writing characters) //'rs' is made low when 0 (for instructions) //'disp' represents Port C (RS then 4 data pins) { if(rs==1) rs <<= 4; //Converts 1 to 0b10000 to make 5th pin in Port C high e=1; //Set Enable high before writing disp = rs + high; //Sets Port C RS setting (5th pin) + 1st nibble (lower 4 pins) nop(); //Brief delay e=0; //Execute by pulling low nop(); if(low==99) //If low = 99, don't send 2nd nibble (ie: still in 8bit mode) { delay_ms(1); //Pause for last E to complete return; //Exits function } e=1; disp = rs + low; //2nd nibble nop(); e=0; delay_ms(1); } //=========================================================================== void main() //main program { //Declare local variables u16 i; //Variable for counters i16 digits; //Temporary variable for selecting individual digits to display u8 buff[8]; //Array for storing Unsigned Integer to ASCII conversions u32 amps; //Variable for determining the amps measured u8 amps_old; //Previous measurement u32 volts; //Variable for determining the voltage measured u8 volts_old; //Previous measurement s8 diff; //Difference between values (used to determine jitter) bit jitter; //Jitter flag //Initialise all ports, etc porta=0; portc=0; //Start low volts=0; amps=0; volts_old=0; amps_old=0; //Main port settings (1=Input 0=Output) trisa = 0; //Port A all output except... trisa.2 = 1; //RA2 (Pin11) AN2 Current measurement trisa.4 = 1; //RA4 (Pin3) AN3 Voltage measurement trisc = 0; //Port C all output (only writing to LCD, not reading) //ADC port settings (1=Analog 0=Digital) ansel = 0; //All digital except... ansel.2 = 1; //RA2 (Pin11) AN2 Amps ansel.3 = 1; //RA4 (Pin3) AN3 Volts //Static ADC settings adcon0.7 = 1; //ADFM Right justify results adcon0.6 = 0; //VCFG Use Vdd for reference voltage adcon0.0 = 1; //ADON AD module enabled adcon1 = 101; //AD Conversion Clock Fosc/16 pie1.6 = 0; //ADC interrupts disabled //Static EEPROM data logging settings eecon1.7 = 0; //EEPGD: Access data memory eecon1.3 = 0; //WRERR: clear on startup in case previously set eecon1.2 = 1; //WREN: enable writing to memory eeadr=0; //First address for writing to EEPROM (testing) //------------------------------------------------------------------------------ //Initialise LCD //LCD starts in 8bit mode by default //However, only the upper 4 data lines are physically connected in the circuit //So while in 8bit mode, only the upper 4 bits are sent (until changed to 4bit mode) //The parameter '99' prevents the LCD function from sending the 2nd nibble delay_ms(250); //Wait for LCD to start //'Set Function' (in 8bit mode) //Send instruction three times to stabilise display (selects default 8bit mode) for(i=0; i<3; i++) { lcd (0b0011, 99, 0);//8bit interface length, 99=no 2nd nibble, RS=0 delay_ms(1); //Extra delay to aid initialisation } //'Set Function' again //Send instruction to select 4bit mode lcd (0b0010, 99, 0); //4bit interface length, no 2nd nibble delay_ms(1); //From here all instructions and data writes require two 4bit nibbles //'Set Function' (full instruction) //Set interface length characteristics lcd (0b0010, 0b1000, 0); //4bit, 2 display lines, 5x7 font delay_ms(1); //'Display Clear' and return cursor to home position lcd (0b0000, 0b0001, 0); //Clear display delay_ms(1); //Requires 1.6ms delay in total (1ms exists in lcd function) //'Set Entry Mode' //Cursor auto-increment direction lcd (0b0000, 0b0100, 0); //Decrement cursor (left) after writing; don't shift display //lcd (0b0000, 0b0110, 0); //Increment cursor (right) after writing; don't shift display delay_ms(1); //'Cursor/Display Shift' //Set cursor move direction lcd (0b0001, 0b0000, 0); //Decrement address and moves cursor left delay_ms(1); //'Display ON' //Enable display/cursor lcd (0b0000, 0b1100, 0); //Display on, cursor off, blinking off //lcd (0b0000, 0b1111, 0); //Display on, cursor on, blinking on delay_ms(1); //----------------------------------------------------------------------------- while(1) { //Measure Current and Voltage (must be done at same time since they interract) amps = 0; volts = 0; //Initialise variables for(i=0; i<16; i++) //Measure and sum both 16 times to smooth results { measure(); //Runs ADC and populates 'results' variables amps += result_a; //Sums each measurement volts += result_v; //Sums each measurement } //----------------------------------------------------------------------------- //Determine Current //PSU has a 0.1ohm current sense resistor //The voltage drop across this is proportional to current through it // eg: 1A yields a 0.1v drop (1 x 0.1) //Op amp gain is calibrated with 1A to yield ADC result of 100 // ie: this requires an output from the op amp of 0.488v (100 / 1023 * 4.99v) // (5v regulator used supplies 4.99v) //This means that 1 ADC step is 0.01A, ie: intended to be displayed direct on LCD // eg: ADC value of 1 displays as 0.01A, ADC 10 = 0.10, ADC 100 = 1.00, ADC 1000 = 10.00A //Note that the output of LM358N cannot exceed ~3.5v from a 5v supply // (Vdd - 1.5v common mode limit) //So op amp will peak at ~7A (PSU is rated to 5A so not a constraint) // (7 x 0.1 = 0.7v x ~5 gain = 3.5v max) amps /= 16; //16 measurements taken above so average results to smooth readings //----------------------------------------------------------------------------- //Determine Voltage volts <<= 9; //Scale up measurement to improve accuracy of conversion to actual volts //An arbitray 'big' multiplier but chosen via spreadsheet to optimise //Voltage already comprises sum of 16 measurements //Increased further by 2 to power 9 (512) (PIC does shifts quickly) //Effect is 'volts' is now x8192 larger than a single measure (16 x 512) //x100 thereof is to change value to 'hundredths' to avoid decimals //x81.92 allows larger resistor divider factor below to improve its accuracy //100x81.92 = 8192 volts += 3809; //'Round up' to compensate for the fact that the PIC always rounds down //Value is not critical but same number used in next step seems appropriate //Modelled in spreadsheet to prove that it improves measurement accuracy volts /= 3809; //Scale down for resistor divider ratio and ADC step value //Voltages are sampled over a 2k7 resistor in series with 9k1 //2k7/11k8 resistors = 0.22881 (measured voltages are reduced by this) // (10v in actually yields 2.268v so actual ratio is 0.2268) //5v/1023 ADC steps = 0.004888 (reference voltage per ADC step) // (Regulator in prototype actually yields 4.99v = 0.004878 v/step) //0.2268/0.004878 = 46.49627 (ADC steps per measured volt) //Scale up by 81.92 to improve accuracy (46.49627 * 81.92 = 3808.9744) // (ADC value was increased by 81.92 above so it get 'reversed' here) /* Example: 11v should yield an ADC value of 511 (11 x 0.2268 / 0.04878) * 16 = 8,176 (16 measurements summed to smooth results) * 512 = 4,186,112 + 3809 = 4,189,921 / 3809 = 1100 Program displays this as 11.00 Note: PIC can only measure to 0.02v with chosen resistor divider ratio So other examples may be out by 0.01 or 0.02v ie: no worse than the inherent accuracy of the device */ volts -= (amps/10); //Adjust for losses in current sense resister //Circuit has a 'low side' Rsense //PIC is grounded on one side (true ground) and PSUs output is on other //The voltage measurement is between true ground and positive // so is different to the output by the voltage drop through Rsense //Each ADC step current measurement represents 0.01A //With 0.1ohm Rsense, loss is 0.001v per ADC step (0.1ohm x 0.01A) //Eg: 1A = 100 ADC value = 0.1v loss (ie: over-reading by 10 hundreths) //----------------------------------------------------------------------------- //Display Current digits = (i16)amps; //Casts result to 16bit integer to convert to ASCII //An interger is required by the uitoa_dec function called below if(digits<=1023) //Display results if in valid range { //Check for jitter (difference between last measurement and latest one) diff = (s8)( (u8)amps - amps_old); //Also casts variable correctly //Set jitter flag if change in measurement is just jitter (ie: changes by 1 step) //Note: this relies on jitter being > 1 step occasionally to allow display to update //ie: if voltage kept changing by exactly 1 step, display would never get updated //In practice this does not appear to cause any adverse effects if (diff == -1) jitter=1; //-1 = Jitter else if (diff == 1) jitter=1; //+1 = Jitter else jitter=0; //Set flag to 0 if not jitter if(jitter==0) //No jitter (ie: a valid new measurement) { //We need individual decimals for display on LCD //So, first convert to ASCII using special BoostC function //Generates individual ASCII bytes (in an array) for each decimal character //eg: '12' becomes 0x31 and 0x32 which are ASCII hex for decimals 1 and 2 //By deducting 0x30 we are left with a single numeral (eg: 1 and 2) to send to LCD //'uitoa_dec' is name of function being called //'buff' is a buffer array into which results of conversion are stored //'digits' is the integer being converted //'b' is number of ASCII bytes to be created uitoa_dec (buff, digits, 4); //Populates buffer array with ASCII characters lcd (0b1100, 0b0101, 0); //Move cursor to 14th position (ie: address 0x45) //1st bit (1) sets address mode; next 7bits are address lcd (6, 1, 1); //'a' for Amps; program writes backwards (right to left) //'6' and '1' are req'd nibbles for 'a'; '1' = RS high buff[3] -= 0x30; //Hundreths (convert ASCII to single characters) lcd (3, buff[3], 1); //1st nibble 3, 2nd nibble 0-9, 1 to make RS high buff[2] -= 0x30; //Tenths lcd (3, buff[2], 1); lcd (2, 14, 1); //'.' for decimal if (digits>99) //Only display a character if > 0.99v { buff[1] -= 0x30; //0-9 lcd(3,buff[1],1); } else lcd(2, 0, 1); //Clear character (in case one existed before) if (digits>999) //Only display a character if > 9.99v { buff[0] -= 0x30; //Tens lcd(3,buff[0],1); } else lcd(2, 0, 1); //Clear character } } else //Display error message if out of range { lcd (0b1100, 0b0101, 0);//Move cursor to 14th position (ie: address 0x45) lcd(2,0,1); //Clear character lcd(3,1,1); //1 lcd(7,2,1); //r lcd(7,2,1); //r lcd(4,5,1); //E } amps_old = (u8)amps; //Save last measurement (to determine jitter) //----------------------------------------------------------------------------- //Display Voltage digits = (i16)volts; //Casts V measurement to 16bit integer to convert to ASCII if(digits<=2200 && amps<=1023) //Display results if in valid range (PSU goes to 21v) { //Check for jitter (same as above) diff = (s8)( (u8)volts - volts_old); //Set jitter flag (same as above) if (diff == -1) jitter=1; //-1 = Jitter else if (diff == 1) jitter=1; //+1 = Jitter else jitter=0; //Set flag to 0 if not jitter if(jitter==0) //No jitter (ie: a valid new measurement) { uitoa_dec (buff, digits, 4); //Populates buffer array with ASCII characters lcd (0b1000, 0b0111, 0); //Move cursor to 8th position (ie: address 0x07) lcd (7, 6, 1); //'v' for volts buff[3] -= 0x30; //Hundreths (converts ASCII to single characters) lcd (3, buff[3], 1); buff[2] -= 0x30; //Tenths lcd (3, buff[2], 1); lcd (2, 14, 1); //'.' for decimal if (digits>99) //Only display a character if > 0.99v { buff[1] -= 0x30; //0-9 lcd(3,buff[1],1); } else lcd(2, 0, 1); //Clear character (in case one existed before) if (digits>999) //Only display a character if > 9.99v { buff[0] -= 0x30; //Tens lcd(3,buff[0],1); } else lcd(2, 0, 1); //Clear character } } else //Display error message if out of range { lcd (0b1000, 0b0111, 0);//Move cursor to 8th position (ie: address 0x07) lcd(2,0,1); //Clear character lcd(3,2,1); //2 lcd(7,2,1); //r lcd(7,2,1); //r lcd(4,5,1); //E } volts_old = (u8)volts; //Save last measurement (to determine jitter) } //end while } //end main