;Feel free to modify and distribute as you wish - consider this open source
;A credit back to the original work would be nice if you find it useful!
;
;Jeremy Harris - August 2012
;
;
;Electric bike power control and power/energy display using 18M2 built in to AXE133Y OLED display
;
;
;The control board that this drives has a FET power switch on the low side plus a Hall effect
;current sensor and potential dividers to measure/detect switched battery voltage.
;The circuit draws zero current when powered off.
;
;Power off control causes the current capacity value to be stored in EEPROM and then allows the picaxe
;to "commit suicide" by turning off it's own power supply
;
;Power is turned back on by a momentary push button and is then latched on by the picaxe
;The code includes an automatic turn off function. If no appreciable current is detected for between 2.5 and 3
;minutes the picaxe turns the power off to conserve battery capacity
;
;When the picaxe is powered up with the push button and detects that the battery is being charged
;(using a shorting link on the charging connector) it resets the capacity in EEPROM to the battery fully charged capacity
;
;Due to the word length limit, the maximum allowable battery capacity is 18.2 Ah (65,536 amp seconds)
;
;This version is set for a battery capacity of 5 Ah (18,000 amp seconds)
;
;
#picaxe 18M2
EEPROM $00, ("E",3,1,1,1,1,1,1,1,1,1,1,1,1,2,"F") ; store 0 bar (EMPTY) msg in the EEPROM memory
EEPROM $10, ("E",0,1,1,1,1,1,1,1,1,1,1,1,1,2,"F") ; store 1 bar msg in the EEPROM memory
EEPROM $20, ("E",0,0,1,1,1,1,1,1,1,1,1,1,1,2,"F") ; store 2 bar msg in the EEPROM memory
EEPROM $30, ("E",0,0,0,1,1,1,1,1,1,1,1,1,1,2,"F") ; store 3 bar msg in the EEPROM memory
EEPROM $40, ("E",0,0,0,0,1,1,1,1,1,1,1,1,1,2,"F") ; store 4 bar msg in the EEPROM memory
EEPROM $50, ("E",0,0,0,0,0,1,1,1,1,1,1,1,1,2,"F") ; store 5 bar msg in the EEPROM memory
EEPROM $60, ("E",0,0,0,0,0,0,1,1,1,1,1,1,1,2,"F") ; store 6 bar msg in the EEPROM memory
EEPROM $70, ("E",0,0,0,0,0,0,0,1,1,1,1,1,1,2,"F") ; store 7 bar msg in the EEPROM memory
EEPROM $80, ("E",0,0,0,0,0,0,0,0,1,1,1,1,1,2,"F") ; store 8 bar msg in the EEPROM memory
EEPROM $90, ("E",0,0,0,0,0,0,0,0,0,1,1,1,1,2,"F") ; store 9 bar msg in the EEPROM memory
EEPROM $A0, ("E",0,0,0,0,0,0,0,0,0,0,1,1,1,2,"F") ; store 10 barmsg in the EEPROM memory
EEPROM $B0, ("E",0,0,0,0,0,0,0,0,0,0,0,1,1,2,"F") ; store 11 bar msg in the EEPROM memory
EEPROM $C0, ("E",0,0,0,0,0,0,0,0,0,0,0,0,1,2,"F") ; store 12 bar msg in the EEPROM memory
EEPROM $D0, ("E",0,0,0,0,0,0,0,0,0,0,0,0,0,2,"F") ; store 13 bar msg in the EEPROM memory
EEPROM $E0, ("E",0,0,0,0,0,0,0,0,0,0,0,0,0,0,"F") ; store 14 bar (FULL)msg in the EEPROM memory
symbol current0 = C.0 ; current sense input 0 (ADC) (also used for capacity reset when shorted to ground)
symbol voltage1 = C.1 ; voltage sense input 1 (ADC)
symbol poweron = C.2 ; power on output 2 (out)
symbol on_off = C.5 ; on/off button sense input 5 (input)
symbol enable = C.6 ; LCD enable
symbol rs = C.7 ; LCD RS
; LCD data pins are on B.0 to B.7
; Supported display bytes:
; 0-7, 8-15 CGRAM characters
; 16-252 normal ASCII characters, according to selected character map table
; Supported command bytes:
; 1 Display clear (leaves cursor position where it is)
; 2 Return cursor to home position (line 1, position 1)
; 128 sets cursor at first character, first line (128 + x moves to position x)
; 192 sets cursor at first character, second line (192 + x moves to position x)
; 8 hides display
; 12 restore display
; 14 turn on cursor
; 16 move cursor left one character
; 20 move cursor right one character
SYMBOL display_byte = b0
SYMBOL loopcounter = b1 ;loopcounter is used by "measurementloop" and "charged"
SYMBOL Capacity = w1 ;Capacity holds battery capacity remaining in amp seconds (18.2 Ah max, due to 16 bit limit)
SYMBOL Current = w2
SYMBOL Voltage = w3
SYMBOL Averagecurrent = w4
SYMBOL Averagevoltage = w5
SYMBOL zerocurrent = w6
SYMBOL Power = w7
SYMBOL digit0 = b16
SYMBOL digit1 = b17
SYMBOL digit2 = b18
SYMBOL digit3 = b19
SYMBOL sampleloopcounter = b20
SYMBOL msg_no = b21
SYMBOL temp1 = b22 ;only used in message display sub-routine, so can be used elsewhere
SYMBOL temp2 = b23
SYMBOL temp3 = b24
SYMBOL time_out = b25
SYMBOL Totalcurrent = w13
SYMBOL antijittercounter = $1C
init:
;Set up ports and turn on power
let dirsC = %11000100 ; PortC 2,6,7 outputs
let dirsB = %11111111 ; PortB all outputs
high poweron ;turn on power switch
; Winstar OLED Module Initialisation
; according to WS0010 datasheet (8 bit mode)
pause 500 ; Power stabilistation = 500ms
; Function set - select only one of these 4 character table modes
let pinsB = %00111000 ; 8 bit, 2 line, 5x8 , English_Japanese table
;let pinsB = %00111001 ; 8 bit, 2 line, 5x8 , Western_European table1
;let pinsB = %00111010 ; 8 bit, 2 line, 5x8 , English_Russian table
;let pinsB = %00111011 ; 8 bit, 2 line, 5x8 , Western_European table2
pulsout enable,1 ;
let pinsB = %00001100 ; Display on, no cursor, no blink
pulsout enable,1
let pinsB = %00000001 ; Display Clear
pulsout enable,1
pause 7 ; Allow 6.2ms to clear display
setfreq m16 ; change clockto 16Mhz
let pinsB = %00000010 ; Return Home
pulsout enable,1
let pinsB = %00000110 ; Entry Mode, ID=1, SH=0
pulsout enable, 1
high rs ; Leave in character mode
; load custom characters for bargraph display to cgram in display:
let display_byte =64
gosub display_command
let display_byte =63
gosub display_data
let display_byte =63
gosub display_data
let display_byte =63
gosub display_data
let display_byte =63
gosub display_data
let display_byte =63
gosub display_data
let display_byte =63
gosub display_data
let display_byte =63
gosub display_data
let display_byte =32
gosub display_data ;load custom character 0 (full block)
let display_byte =72
gosub display_command
let display_byte =63
gosub display_data
let display_byte =32
gosub display_data
let display_byte =32
gosub display_data
let display_byte =32
gosub display_data
let display_byte =32
gosub display_data
let display_byte =32
gosub display_data
let display_byte =63
gosub display_data
let display_byte =32
gosub display_data ;load custom character 1 (centre, empty)
let display_byte =80
gosub display_command
let display_byte =63
gosub display_data
let display_byte =33
gosub display_data
let display_byte =33
gosub display_data
let display_byte =33
gosub display_data
let display_byte =33
gosub display_data
let display_byte =33
gosub display_data
let display_byte =63
gosub display_data
let display_byte =32
gosub display_data ;load custom character 2 (right hand empty)
let display_byte =88
gosub display_command
let display_byte =63
gosub display_data
let display_byte =48
gosub display_data
let display_byte =48
gosub display_data
let display_byte =48
gosub display_data
let display_byte =48
gosub display_data
let display_byte =48
gosub display_data
let display_byte =63
gosub display_data
let display_byte =32
gosub display_data ;load custom character 3 (left hand end, empty)
;Reset capacity to 5Ah (18,000 amp seconds) if reset link (which shorts the current sense to 0V) is connected at power on
READADC10 current0, Current
IF Current < 10 THEN GOTO charged
;read value of Capacity stored in non-volatile memory
READ $F0, WORD Capacity
;Read averaged supply current over 100 readings to get zero value
FOR loopcounter = 1 TO 100
READADC10 current0, Current
zerocurrent = zerocurrent + current
NEXT loopcounter
zerocurrent = zerocurrent / 100 ;set zero current value at power on
;check for capacity over-range from misreading
IF Capacity >18000 THEN LET Capacity = 18000
ENDIF
Totalcurrent = 0
;display starting capacity
GOTO capacitydisplay
mainloop:
;reset internal time variable to zero
time = 0
;reset sample loop counter to zero
sampleloopcounter = 0
sampleloop:
;take 64 measurements and sum and divide the results to give average current and voltage
; it takes ~ 196 mS to do this loop 64 times, so ~ 3mS per iteration
;check push button for power off command
IF pinc.5 = 1 THEN goto poweroff
FOR loopcounter = 1 to 64
;Take voltage reading using 100k/10 divider on battery (55 V maximum) 1 bit ~ 53.8 mV
READADC10 voltage1, Voltage
;at this point Voltage can be 0 to 1023, where 1023 = 55V
;Take current reading using ACS712 sensor.
READADC10 current0, Current
;At this point Current can be any value between about 512 (~0A) and 1023 (~30A) 1 bit ~ 58.9 mA
;Subtract zero current value obtained during initialisation
Current = Current - zerocurrent
zerocurrent = zerocurrent + 500 ;temp increase zerocurrent to max reading expected
IF Current > zerocurrent THEN LET Current = 0 ;check for under-run error due to noise
ENDIF
zerocurrent = zerocurrent - 500 ;return zerocurrent to original value
;At this point Current can be any value between 0 and ~511 (where 511 = 30.0A)
;add current and voltage to averagecurrent and averagevoltage, maximum allowable is 65,536
Averagecurrent = Averagecurrent + Current
Averagevoltage = Averagevoltage + Voltage
;maximum of 64 times around this loop to stay inside 16 bit overflow on totals
NEXT loopcounter
;voltage is now 0 to 65472 for 0 to 55 V, current is 0 to 32704 for 0 to 30 A
;convert current to true average over 30 seconds using number of times around sample loop
;uses sampleloopcounter as temp counter variable (not used elseswhere for anything useful)
LET sampleloopcounter = sampleloopcounter + 1 ;increment counter every time around sample loop
;Averagecurrent is divided by 109 to bring within range 0 to 300 for 0 to 30.0A
Totalcurrent = Averagecurrent / 109 + Totalcurrent
GOSUB display
IF time = 30 THEN GOTO capacitydisplay ;check to see if 30 seconds has elapsed, if not go around the loop again
GOTO sampleloop
capacitydisplay:
;Totalcurrent is (amps x 10 x number of times around sampleloop) at this point
;first convert Totalcurrent to amps * 10 over 30 seconds (sampleloopcounter is no of samples)
Totalcurrent = Totalcurrent / sampleloopcounter
;Totalcurrent is now in the range 0 to 300
;Totalcurrent = 300 = 30A = 900 amp seconds, so multiply Totalcurrent x 3 to get amp seconds
Totalcurrent = Totalcurrent * 3
Capacity = Capacity - Totalcurrent
;Capacity now has capacity remaining in amp seconds, within the range 0 to 18,000
display_byte = 192 ;go to start of second line on display
gosub display_command
IF Capacity <643 THEN LET msg_no = 0
gosub msg
ENDIF
IF Capacity >=643 AND Capacity <1928 THEN LET msg_no = 1
gosub msg
ENDIF
IF Capacity >=1928 AND Capacity <3213 THEN LET msg_no = 2
gosub msg
ENDIF
IF Capacity >=3213 AND Capacity <4498 THEN LET msg_no = 3
gosub msg
ENDIF
IF Capacity >=4498 AND Capacity <5783 THEN LET msg_no = 4
gosub msg
ENDIF
IF Capacity >=5783 AND Capacity <7068 THEN LET msg_no = 5
gosub msg
ENDIF
IF Capacity >=7068 AND Capacity <8353 THEN LET msg_no = 6
gosub msg
ENDIF
IF Capacity >=8353 AND Capacity <9638 THEN LET msg_no = 7
gosub msg
ENDIF
IF Capacity >=9638 AND Capacity <10923 THEN LET msg_no = 8
gosub msg
ENDIF
IF Capacity >=10923 AND Capacity <12208 THEN LET msg_no = 9
gosub msg
ENDIF
IF Capacity >=12208 AND Capacity <13493 THEN LET msg_no = 10
gosub msg
ENDIF
IF Capacity >=13493 AND Capacity <14778 THEN LET msg_no = 11
gosub msg
ENDIF
IF Capacity >=14778 AND Capacity <16063 THEN LET msg_no = 12
gosub msg
ENDIF
IF Capacity >=16063 AND Capacity <17348 THEN LET msg_no = 13
gosub msg
ENDIF
IF Capacity >=17348 THEN LET msg_no = 14
gosub msg
ENDIF
idle_check:
;Check current and if less than 1 amp second drawn for 5 consecutive times around capacity loop (between 2.5 and 3 minutes) turn power off as a power save measure
IF Totalcurrent < 30 THEN LET time_out = time_out + 1 ;check for less than 1 amp second average in past 30 seconds
ELSE time_out = 0 ;reset timeout counter if current is still flowing
ENDIF
IF time_out = 5 THEN GOTO poweroff ;poweroff saves capacity in EEPROM and turns power off
GOTO mainloop
;*******************************************************************************************************************************
poweroff:
;save value of Capacity in non-volatile storage for use on next power up
;Note: EEPROM locations from 0 to 240 ($00 to $E9) are used for fixed message storage, $F0 is next available slot
WRITE $F0, WORD Capacity
pause 500
;turn off power
LOW poweron
END
;*******************************************************************************************************************************
charged:
;programme redirects to here if charging link is detected at power on
LET Capacity = 18000 ;reset capacity to 5 Ah (18,000 amp seconds)
;save reset value of Capacity in non-volatile storage
WRITE $F0, WORD Capacity
let display_byte = 128 ;set cursor to first digit, upper line of display
gosub display_command
LET display_byte = "B"
GOSUB display_data ;display "Battery charged" message on upper line of display
LET display_byte = "A"
GOSUB display_data
LET display_byte = "T"
GOSUB display_data
LET display_byte = "T"
GOSUB display_data
LET display_byte = "E"
GOSUB display_data
LET display_byte = "R"
GOSUB display_data
LET display_byte = "Y"
GOSUB display_data
LET display_byte = " "
GOSUB display_data
LET display_byte = "C"
GOSUB display_data
LET display_byte = "H"
GOSUB display_data
LET display_byte = "A"
GOSUB display_data
LET display_byte = "R"
GOSUB display_data
LET display_byte = "G"
GOSUB display_data
LET display_byte = "E"
GOSUB display_data
LET display_byte = "D"
GOSUB display_data
FOR loopcounter = 0 TO 14
let display_byte = 192 ;set cursor to first digit, lower line, for "fill up" display
gosub display_command
let msg_no = loopcounter
gosub msg
PAUSE 1000
NEXT loopcounter
PAUSE 20000
;turn off power
LOW poweron
END
;*******************************************************************************************************************************
;ONLY SUBROUTINES BELOW THIS POINT
;*******************************************************************************************************************************
msg:
; display message from EEPROM sub routine
; message number 0-15 must be in msg_no when called
; uses (alters) temp1, temp2, temp3
let temp1 = msg_no & %00001111 * 16
; EEPROM start address is 0 to 15 multiplied by 16
let temp2 = temp1 + 16 - 1 ; end address is start address + (line_length - 1)
for temp3 = temp1 to temp2 ; for 16 times
read temp3,msg_no ; read next character from EEPROM data memory into b1
let pinsB = msg_no ; output the data
pulsout enable,1 ; pulse the enable pin to send data.
next temp3 ; next loop
RETURN
;*******************************************************************************************************************************
display_data:
;This sub-routine reads data in display_byte and send it to OLED display
;it returns after acting on the byte
let pinsB = display_byte ; output the data
pulsout enable,1 ; pulse the enable pin to send data.
RETURN
;*******************************************************************************************************************************
display_command:
low rs ; change to command mode for next character
let pinsB = display_byte ; output the data
pulsout enable,1 ; pulse the enable pin to send data.
pause 30
high rs ; back to character mode
RETURN
;*******************************************************************************************************************************
display:
;check to see how many times display has been called to reduce display character jitter by only changing display every X calls
;uses temp3 as loop counter but this doesn't have to be kept sacrosant between calls to this routine as it only holds a peeked value within this routine
PEEK antijittercounter, temp3
IF temp3 = 4 THEN
;Averagevoltage is 0 to 65472 (0 to 55.0V) and Averagecurrent is 0 to 32704 (0 to 30.0A)
;convert average voltage back to volts x 10
Averagevoltage = Averagevoltage / 119
;at this point Averagevoltage can be any value between 0 and 550 for 0 to 55.0V
;convert averagecurrent back to amps x 10
Averagecurrent = Averagecurrent / 109
;at this point Averagecurrent can be any value between 0 and 300 for 0 to 30.0A
;convert Averagecurrent to ASCII
BINTOASCII Averagecurrent, digit2, digit1, digit0
IF digit2 = "0" THEN LET digit2 = " "
ENDIF
let display_byte = 128 ;set cursor to first digit, first line
gosub display_command
let display_byte = digit2
gosub display_data
let display_byte = digit1
gosub display_data
let display_byte = "."
gosub display_data
let display_byte = digit0
gosub display_data
let display_byte = "A"
gosub display_data
let display_byte = " "
gosub display_data ;display current in format "30.0A_", where _ is a space
;last digit is a space at character 133
;convert Averagevoltage to ASCII
BINTOASCII Averagevoltage, digit2, digit1, digit0
let display_byte = digit2
gosub display_data
let display_byte = digit1
gosub display_data
let display_byte = "."
gosub display_data
let display_byte = digit0
gosub display_data
let display_byte = "V"
gosub display_data ;display voltage in format "44.2V"
;last digit is "V" at character 138
;calculate power and display, range is 0 to 999 W
;Average voltage is 0 to 550, Averagecurrent is 0 to 300
Averagevoltage = Averagevoltage / 10 ;scale down to volts
Power = Averagevoltage * Averagecurrent ;range is 0 to 16500 watts x 10 (30A x 55V)
Power = Power / 10 ;scale power down to watts
BINTOASCII Power, temp3, digit3, digit2, digit1, digit0
;discard temp3 and digit3, only hundreds and below displayed (temp3 is re-used here as a digit that's not displayed)
;remove leading zeros from first two digits
IF digit1 = "0" AND digit2 = "0" AND digit3 = "0" THEN LET digit1 = " "
ENDIF
IF digit2 = "0" AND digit3 = "0" THEN LET digit2 = " "
ENDIF
IF digit3 = "0" THEN LET digit3 = " "
ENDIF
LET display_byte = digit3
GOSUB display_data
LET display_byte = digit2
GOSUB display_data
LET display_byte = digit1
GOSUB display_data
LET display_byte = digit0
GOSUB display_data
LET display_byte = "W"
GOSUB display_data
;W is at character position 143 which is 16th position on upper line of display
LET temp3 = 0 ;reset antijitter loop counter
POKE antijittercounter, temp3
ELSE
LET temp3 = temp3 + 1 ;increment antijitter loop counter
POKE antijittercounter, temp3
ENDIF
;keep these resets here as they are needed to maintain correct averaging of voltage and current
;reset loopcounter to zero
loopcounter = 0
;reset averagecurrent drawn over sample period to zero
Averagecurrent = 0
;reset averagevoltage measured over sample period to zero
Averagevoltage = 0
RETURN
;*******************************************************************************************************************************
;*******************************************************************************************************************************