DIY Electric Car Forums banner

1 - 20 of 28 Posts

1,148 Posts
Discussion Starter #1 (Edited)
Minimal Discussion!

Please direct all discussions to the Elcon/TC Charger Firmware: Discussion thread.

Obsolescence Notice
This topic refers to the oldest range of Elcon/TC charger hardware, those manufactured with Elcon or TC branding prior to December 2013.
However, Elcon branded PFC models (not the UHF models) seem to retain the same basic design and processor chip.
From 2014 onwards, the TC branded HQ and later models use a different processor altogether
(a 32-bit ARM based processor, compared to an 8-bit 8051 based one).
As a result, this topic DOES NOT APPLY to the following models: UHF models, or TC branded PFC models manufactured 2014 or later.
As far as I know, no work has been done on the firmware for the models with the ARM based processor.​

This thread is intended to discuss just the FACTS of Elcon/TC charger firmware.

History and acknowledgement.
The processor.
Reading the firmware.
User selection.
Hardware for reading/writing firmware.
Writing the firmware.
Understanding the firmware:

Serial data contents.

EEPROM contents.
Building new firmware.
Flashing new firmware.
The comparator.
Firmware downloads

  • The Calibrator for setting the value of EEPROM contents.
    UniCAN: the (almost) Universal CAN bus firmware.

1,148 Posts
Discussion Starter #2 (Edited)
History and Acknowledgements.

This thread and many of the more useful Elcon/TC charger threads would not have been possible without the help of members KennyBobby and Pdove. These guys saw the potential of these chargers, and had a curiosity and passion for finding out all they could about them. I'm so glad that they did, and they provided the first clues on how to get at the firmware.

There have been others who have been helpful; I'll acknowledge you anonymously for now.

I have a bit if a history with reverse engineering, but I'm no cracker. I enjoy reading other people's code, even if that means I have to do it at the assembler level.

1,148 Posts
Discussion Starter #3 (Edited)

I have no wish to cause trouble with this project. It seems plain to me that sharing knowledge of these chargers makes them more valuable than they were when no-one know how to repair them or change their behavior.

No doubt someone, and possibly the manufacturers, will insist that firmware is intellectual property, and can't be legally copied. There actually are inter-operability provisions where it is legal to reverse engineer firmware to allow operating something that co-operates with the original firmware. You could argue that our new firmware needs to know about ports and bit assignments, and that this is an inter-operability exercise. I actually am not that fussed about the legalities; I think that the benefits, especially for the manufacturer, far outweigh any disadvantages. It's not as if everyone wants to build their own chargers and undercut TC, with an advantage because they didn't have to pay for firmware development first. I think it's pretty obvious that this makes no economic sense at all.

Nevertheless, if someone can really convince me that disseminating this information is morally wrong, then I'm willing to delete all my posts in this thread, and I would encourage others to do the same. (Maybe I'd ask the admins to do that, or maybe they'll be forced to do it to protect their interests.) I really hope it won't come to that.

Simply put: making these chargers more flexible makes them more desirable to purchase. I think that knowing that it will be possible to modify how the charger behaves if needed in the future will sway people in favor of buying this charger over some other charger. So shutting down this information is economically unsound, it seems to me.

I sincerely hope that other charger manufacturers won't attempt to shut this down, in order to make their own chargers relatively more desirable. You could offer flexibility of your own. Hopefully, the charger buyers will benefit, and it won't cost the manufacturers much if anything.

1,148 Posts
Discussion Starter #4 (Edited)
The Processor.

The microcontroller used in the Elcon/TC chargers is the NXP P89LPC938. This is an 8-bit machine using the old Intel 8051 (MCS51) architecture. That means that there are plenty of tools out there - assemblers, disassemblers, simulators, and so on.

The original Intel 8051 microprocessor had little internal RAM and no peripherals, but modern chips like the NXP '938 have many peripherals built in. So there are ADC (Analog to Digital Converter) inputs, a UART (Universal Asynchronous Receiver/Transmitter or "serial port"), PWM (Pulse Width Modulation) pins, and so on.

The modern chips include Flash memory (8 kiB), the original 256 bytes of "internal" ram, and 512 bytes of "external RAM" (i.e. it would have been external on the original 8051, but it's internal in the '938). There is also 512 bytes of EEPROM (Electrically Erasable Programmable Read Only Memory). This is almost exactly like flash memory. The chargers use EEPROM to store parameter information, e.g. maximum voltage and current, calibration data, some version numbers, the high frequency transformer turns ratio, and so on.

The chip comes in a 28-pin surface mount package. The pins are finely spaced, but anyone competent with surface mount parts can replace the chip if necessary.

The LPC938 is one of the largest members of a whole family of LPC900 chips. When looking for data or anything related to these chips, a good search term is LPC900; this term encompasses the whole family, most of which are very similar in terms of programming. The part is readily available for under US$5, e.g. from Digi-Key (US).

The firmware program is programmed into flash via a variety of means. For the chargers, the most sensible path is to use the 5-pin connector located near the microcontroller chip. You can use either an FDI dongle (a third party printed circuit board with adapter cables) or a home-made board connected to an Arduino to use this connector. This interface is for flash or EEPROM reading and writing (programming). See the Hardware for reading/writing firmware page for details.

The IA-52 architecture is a little unusual in that it has no less than four address spaces. One of these is code; this is where the program counter is fetched from, and bytes in this address space can be read with the movc instruction. Next is RAM address space; this address space is 256 bytes long, addresses 0x00 to 0xFF. Another address space is so-called external RAM (actually internal in modern chips). This is 512 bytes long, addresses 0x000 - 0x1FF. Finally, there is peripheral space. These occupy addresses 0x80 to 0xFF. That means that an address in the range 0x80-0xFF could actually refer to four distinct things: a byte of flash, a byte of RAM, a byte of external RAM, or a peripheral byte. Different instructions and sometimes different addressing modes imply different address spaces. For example, RAM is accessed by @R0 or @R1 (indirect addressing on the R0 or R1 registers, here used as pointers; the @ sign indicates "indirect", like * in the C language). External RAM is more than 256 bytes, so the @DPTR addressing mode is used (in conjunction with movx instructions) to refer to external RAM. Direct instruction, if the address is in the range 0x80-0xFF, refer to peripheral memory. Confusingly, if the address falls in the range 0x00-0x7F, it refers to RAM. It takes a while to get used to, but it does eventually make sense. Here are some examples:

  MOV $30,A       ; Write A to RAM location 0x30
  MOV $90,A       ; Write A to the peripheral whose address is 0x90
  MOV #$90, R0    ; Point R0 to RAM location 0x90
  MOV @R0,A       ; Write A to RAM location 0x90
  MOV #$90,DPTR   ; Point DPTR to eXternal RAM location 0x90
  MOVX @DPTR,A    ; Write A to external RAM location 0x90
  MOVC A, @A+DPTR ; Use DPTR+A as address to read flash memory to A. A is usually cleared before this instruction
The processor has three special registers that are useful for controlling what happens at reset:

  1. The Boot Status Byte. This can be set to 0 or 1. When set to 1, the Boot Vector is active. When clear, the processor always resets to address 0000, regardless of the contents of the Boot Vector.
  2. The Boot Vector. When the Boot Status Byte is set to 1, this value becomes the upper half of the reset address. So if the Boot Vector has the value $1D, the processor will reset to address $1D00. This is how to force the processor to execute peek code. However, it means that the peek code has to start at a 256 byte boundary (i.e. XX00 where XX has the value 00-1F). This implies that there are only 32 addresses where the peek code can reside.
  3. UCFG1 (User Configuration register 1). This register has a variety of uses, e.g. setting which clock source to use. Most of these bits should be strictly left alone, except for the WDTE bit (MSB, bit 7). This enables (1) or disables (0) the watchdog reset timer. The firmware expects this bit set, but the peek code needs it reset.

Vital information is contained in the NXP UM10119 P89LPC938 User Manual.

1,148 Posts
Discussion Starter #5 (Edited)
Reading the Firmware

Generally, firmware is protected by security bits. The idea is that the manufacturer programs the micro, sets the security bits, then no-one can read the firmware without erasing the whole thing. The 8 KiB flash memory is broken up into eight 1 KiB sectors, and each can be set to be protected or not.

However, there are a few factors mitigating this:

  • Sometimes the security bits just aren't set.
  • Some sectors get written by the firmware, and so can't be write protected. Generally, this seems to mean that they aren't read protected either.
  • If just one sector is unprotected, we can write a short "peek" program to read the other sectors, if certain conditions are met. The code or data that is overwritten by the peek program can't be recovered. However, once we have read a number of firmware images, it is often possible to guess what was there, and recover the image exactly. We can even check if we get it right, because each sector has a 32-bit CRC (Cyclic Redundancy Check), so we can compare the CRC before damaging the sector, and after repairing it with guessed contents.
  • There are some sneakier ways of getting the data, which involve possible damage to the chip, but we've not had to resort to any of these so far.
If the data has to be peeked, typically we will dump it over the serial port. This requires extra hardware (a small circuit involving one transistor and a couple of resistors, plus a serial to USB dongle). For serious firmware researchers, the serial port interface is doubly useful; when the charger is running, it is often spitting out useful data once or twice a second. This data can tell us all sorts of information about what the charger is doing.

The peek program has to be as small as possible, preferably 64 bytes or less, because 64 bytes is one "page" of flash memory, and this is the smallest size that can be programmed in one go. (It is also possible to program one byte, and in fact when you make a new number-of-cells and capacity selection with the front panel push-button, the result is remembered by writing just one byte to flash memory. See User Selection for more.)

Here is a memory peek program (hex dumper) that we sometimes use:

seg001:1D00 75 98 52                 mov     SCON, #0x52     ; Serial Port Control
seg001:1D03 75 BE F0                 mov     FSR_BE, #0xF0
seg001:1D06 75 BF 0B                 mov     FSR_BF, #0xB
seg001:1D09 75 BD 03                 mov     FSR_BD, #3
seg001:1D0C 75 91 F0                 mov     FSR_91, #0xF0
seg001:1D0F 90 00 00                 mov     DPTR, #0
seg001:1D12          loop:                                   ; CODE XREF: seg001:1D19j
seg001:1D12 E4                       clr     A
seg001:1D13 93                       movc    A, @A+DPTR
seg001:1D14 B1 1F                    acall   hexOut
seg001:1D16 A3                       inc     DPTR
seg001:1D17 74 20                    mov     A, #0x20
seg001:1D19 B5 83 F6                 cjne    A, DP0H, loop   ; Data Pointer High Byte
seg001:1D1C 02 00 00                 ljmp    code_0
seg001:1D1F          ; =============== S U B R O U T I N E =======================================
seg001:1D1F          hexOut:                                 ; CODE XREF: seg001:1D14p
seg001:1D1F F9                       mov     R1, A
seg001:1D20 C4                       swap    A
seg001:1D21 B1 24                    acall   digit
seg001:1D23 E9                       mov     A, R1
seg001:1D23          ; End of function hexOut
seg001:1D24          ; =============== S U B R O U T I N E =======================================
seg001:1D24          digit:                                  ; CODE XREF: hexOut+2p
seg001:1D24 54 0F                    anl     A, #0xF
seg001:1D26 24 F6                    add     A, #-10
seg001:1D28 50 02                    jnc     seg001_1D2C
seg001:1D2A 24 07                    add     A, #7
seg001:1D2C          seg001_1D2C:                            ; CODE XREF: digit+4j
seg001:1D2C 24 3A                    add     A, #0x3A        ; '0'+10
seg001:1D2E          cout:                                   ; CODE XREF: digit:coutj
seg001:1D2E 30 99 FD                 jnb     SCON.1, cout    ; Serial Port Control
seg001:1D31 C2 99                    clr     SCON.1          ; Serial Port Control
seg001:1D33 F5 99                    mov     SBUF, A         ; Serial Port Buffer
seg001:1D35 22                       ret
Please pardon the lack of comments in parts. The first five instructions initialize the serial port. DPTR (the double byte pointer register) points to flash memory, which starts at zero. In the loop, there is a movc instruction (move code, i.e. use the code address space) to the A register (the accumulator). Acall is a special type of subroutine call instruction that only takes 2 bytes instead of the usual 3. It prints the byte in A as a pair of hex digits. The pointer is incremented (to point to the next byte of flash memory), and the top half (register DP0H, the top half of DPTR) is compared with 0x20 (so the whole of DPTR is effectively compared with 0x2000, which is one past the end of the last flash memory address). If the compare fails, the program jumps back to the start of the loop. Otherwise, it jumps to location 0, the start of the flash image, to start the charger firmware (just like after a reset).

There are no frills here; no printing of the address, or even a newline; this program emits 16384 characters, and that's it. These characters are then captured in a terminal program on a laptop connected to the serial port of the charger. There they can be converted to a binary image by a tiny C program. From binary, it can be converted to proper Intel hex format (with addresses, checksums, and so on), or disassembled as is.

The next question is: how do we get this peek program into the charger, and how do we force the processor to run it instead of the usual flash image at address zero? See Hardware for Reading/Writing the firmware. Either of these connects to the 5-pin connector near the processor, accessible from outside the charger once the label is peeled off.

1,148 Posts
Discussion Starter #6 (Edited)
User Selection.

For non-CAN chargers, there are 10 "user selections". Only one of these is active at any one time. The ten selections programmed into any one charger is theoretically tailored to what the user wants. If the user can foresee all the changes s/he will need over the life of the charger. then there will be no need for firmware upgrades. Unfortunately, it's often not possible to predict what changes will be needed. Also, the factory may not be successful in extracting this information out of the customer. But that's what comes as standard with an Elcon/TC charger.

Even worse, it's not possible to change from a lead-acid charging algorithm to a lithium one, or even from one lithium profile to another. The available selections are combinations of 1) Amp-hour capacity (this affects the cutoff current, for example), and 2) Number of cells (as long as the number of cells is within the capacity of the charger; you can't have a charger that will do a 24 V battery and a 144 V battery as two of the selections). All the ten selections will use the same algorithm; some of the parameters (e.g. voltage at which the battery is considered too low to charge) are dependent on the number of cells, and some (e.g. current below which the charger shuts off at the end of charge) depend on the Ah capacity of the selection.

Example: a 144 V nominal (actually around 168 V maximum) charger might allow for 44, 45, or 46 LiFePO4 cells, and capacities of 100, 130, and 180 Ah. That's actually nine combinations (44x100, 44x100, 44x100, 45x130, ... , 46x180). In this case, the last selection might be unused, or it might be filled with some other combination, e.g. 45 cells and 60 Ah. Often, there are just two voltages (say 45 and 48 cells), and five Ah capacities (e.g. 50, 75, 100, 150, 200 Ah), for a total of 10 combinations.

When a non-CAN charger powers up, it displays the "current selection" as a series of red flashes of the main tricolor LED, followed by a single green flash. For example, for user selection 4, the sequence would be R-R-R-R-G---.

The user selection can be changed by holding down a push-button at power-up. The push-button is hidden behind a label on the opposite face from the three-color LED. The label also covers the all-important 5-pin connector, as shown below:

When the charger powers up with the push-button down, it flashes the red LED as before, but if you release it soon after the nth flash, then n will be recorded as the new selection. If if you release after the third flash, user selection 3 will be recorded.

In fact, it is recorded in flash memory, near the start of a data section:

Don't worry if you don't understand all of the above assembler code all at once; you probably don't know 8051 assembler (or any other assembler; using assembly language is a dying skill). The essentials are: the address 0CD2 is passed to subroutine ProgramFlash (in two registers, each register is only 8 bits wide). The number of bytes to program (in memory location count, address $0D) is set to 1.

You can see that the present user selection is zero; this is an index (0-9), one less than the number of flashes (1-10), so the presently selected combination is number one (the first), or 50 Ah and 144 V.

It happens that just after $0CD2 is the table of amp-hour capacities (in floating point format, unfortunately the assembler doesn't have a proper way of defining floating point numbers); note how the five values repeat twice. Next is the number of cells (this particular charger is for lead acid, so these are 2.0 V nominal cells, so 72 cells represents 72 x 2.0 = 144 V nominal). You can just see the start of the last set of cell numbers is 78, so user selections 6-10 are for 78 x 2 = 156 V nominal.


1,148 Posts
Discussion Starter #7 (Edited)
Hardware for Reading/Writing the Firmware

There are two main ways to interface with the 5-pin programming port near the microcontroller.

One is the off-the-shelf USB In-Circuit Programmer for LPC9XX from FDI:

This is used with a free tool called Flash Magic (download).

An alternative is to use an Arduino board with this simple interface circuit:

It can be used with the attached ICP.ino (in The advantage of the latter is that we have full control of the software, so we can do special things like injecting the peek program.


1,148 Posts
Discussion Starter #8 (Edited)
Writing the Firmware

Writing the firmware uses the same tools (hardware and software) as reading them. First, you will need to build the new firmware; see other sections for details. The firmware will ultimately exist as an Intel Hex file. These files look like this:
Each line starts with a colon, then a two hexadecimal digit length (10 means 0x10 or 16 bytes). The next 4 hex digits are an address, in this case a flash address. There are the stated number of data bytes (each in a pair of hexadecimal digits), followed by a two hex digit checksum. In the above, the addresses progress by strictly 16 addresses each line (so 0000, 0010, 0020, ... 1FE0, 1FF0 for the start of each line). This "no gaps" format is required by the Arduino software, but not by Flash Magic.

Some hex files start like this:
As you can see, the lines are not all 16 bytes in size, and the first two don't even refer to flash addresses at all. It is possible to convert such a hex file to the type required by the Arduino software as follows:
1) Edit the text file to remove any references to addresses outside 0000-1FFF. Any text editor can be used for this.
2) Use this pair of commands in a Cygwin window to convert the file to binary and back to hex:
objcopy -I ihex -O binary <input file>.hex <input file>.bin
objcopy -O ihex -I binary <input file>bin <input file>.ordered.hex
Now <input_fle>.ordered.hex is the file to send to the charger using the Arduino software (ICP.ino).

The program objcopy is part of the binutils package on Cygwin.

BTW, the parts of hex files that address locations such as FF00 are to set such things as security bits.

To make the new firmware active, it will be necessary to change the boot vector to zero, or to clear the Boot Status Byte. There are commands in the Arduino software to accomplish this.

1,148 Posts
Discussion Starter #9 (Edited)
Understanding the Firmware: Tools

The original firmware was written in C. We know this because snippets of code have leaked, but nowhere near a complete source code is available.

Ideally, a decompiler would be used to convert the binary firmware read from the processor into readable, maintainable C code, or at least decent C code that could be manually prettied up and commented. Unfortunately, the state of decompilers at this time is not up to this task, at least for a processor like the 8051.

So we are stuck with disassembling the binary image to assembler source code, and maintaining the software by editing assembler source code. An alternative could be to write new C code that achieves the same result as the firmware we have. Maybe one day we'll get to that point, but for now I believe it's a better use of our time to stick with assembler source code.

The lack of a floating point data assembler directive forces an exception to this. It is much easier to use a C compiler to compile only the main data section of the code, and this code gets linked with assembler code and other data to form the final firmware image.

Unfortunately, it's difficult to find free tools (disassemblers and assemblers) to work with 8051 opcodes. The main researchers have opted to use two for-money tools for Windows:
* IDA Pro. This is widely regarded as the best available interactive disassembler. (The same company makes a decompiler, but so far this is for the x86/x64 instruction set only).
* Keil Embedded Development Tools for 8051 (compiler/assembler/linker and some other tools, with a Visual Studio-like GUI development environment).

We also find Cygwin indispensable, but fortunately this is free software.

Because of the expense and complexity of these software tools, it is expected that only a few people would be actively creating new firmware. It is hoped that moderately skilled (in programming) people could prepare their own data sections, and ensure that it compiles (even if it targets a non-8051 platform, such as x86). This could be sent to one of the software experts for building. Alternatively, it may be possible to choose from a set of canonical firmware images, and users could edit the hex files (or use free tools) to customize them for their needs. It remains to be seen how this will work, and it could depend on what the demand for custom firmware actually is.

1,148 Posts
Discussion Starter #10 (Edited)
Understanding the firmware: algorithms

I'm not sure how to go about this. I think I'll introduce the code as I see it, and see how people react to this level of detail. Maybe most people just don't care.

I'll start with the firmware that is oldest (for me) and best commented. Most of them are fairly similar in basic structure.

Let's start at the very beginning, address zero. I'm also experimenting with the best way to display IDA Pro output on this forum. I tried the HTML tag, but it seems to be designed for displaying the details of the HTML in pretty colors, not actually rendering the code in that code's colors. Sigh. So here goes text paste with Courier New font:

code:0000 ; public start
code:0000 start:
code:0000 02 19 07 ljmp Reset

So this disassembly tells us that we're in the code
(flash memory) section, and the address is zero (after the colon). Ida has generated the label start for us. We see a single 3-byte instruction (02, 19, and 07), and it's a long jump instruction to the label Reset. You can see from the second and third bytes of the instruction that Reset is at address 1907h (hexadecimal 1907). Pretty simple so far.

What a pain. I have to wrap code output in [ CODE ] ... [ /CODE ] tags (without the brackets) otherwise multiple spaces are compressed to single ones, and the output is a mess. But now, for no extra charge, the code is compressed into a third of my (desktop) screen, and there doesn't seem to be a way to expand it. Sigh. So just get used to the scroll bars, I guess. If anyone can come up with a better way, please let me know. For now, I'll attach images, link to the images, and delete the attachments. Painful.

I won't go through every line of code. The first loop (clearRam) clears RAM (addresses 00h-FFh). R0 is an 8-bit pointer. @R0 means the memory pointed to by R0. Djnz is the decrement and jump if non zero instruction.

Similarly, the next loop clears "external" memory. DPTR is a 16-bit register, and @DPTR is the byte of memory pointed to by DPTR. The nested djnz instructions accomplish a loop of 2 x 256 bytes. The inner loop iterates 256 times, not zero, since it is tested for zero after first being decremented. The stack pointer is initialized to
B1h, and grows upwards towards FFh. There is a long jump
to StartInitialiseMem, which comes back to doneInitMem (not obvious, I'm just telling you it does), which eventually jumps to main. main() is the start of all C programs, so this is all library startup code so far.

I won't bore you with the details of StartInitialiseMem, especially since it's such a job to paste images of code. Suffice to say that it references a table called InitCodeTable, and in conjunction with this table performs the initialization of global variables. Most global variables will be initialized to zero, but occasionally one will have a different value, so this table can be important. I've left out a lot of the table that is repetitive.

The LPC900 has bit instructions, and the C compiler supports bit variables. The first entry in the table with table code C1h means clear the bit whose bit address is the value of the following byte (01h). This bit is referred to elsewhere as bools1.1 (first Boolean byte, bit 1 (i.e. second-to-least significant). This maps to bit 1 of RAM address 20h. Bits 8 through F are located in RAM address 21h, and so on.

The next table code is 2, which I've labelled as a string copy, but really it's just a straight initialise of a 2-byte variable at the RAM address given by the following byte, to the value in the following word. In this case, the variable labelled uAD_TEMP at address 49h in RAM (occupying 49h and 4Ah) is initialised to zero. Because RAM is initially cleared, this is not necessary. So this table could be compressed to free up flash memory if needed. More locations and bits are initialized. Note that led_t is initialized to 8, so this is one of the few initializations that actually does anything useful. The table code of 00h at 1C7Eh marks the end of the table.

Next post: the beginning of main.


1,148 Posts
Discussion Starter #11 (Edited)
The start of main():

The call to LPC900_config_init is a standard call, and it does really standard initialization. serial_init and CCU_init initialize the serial port and Capture Compare Unit respectively.

The next few calls are to PWM_set. There are several PWM outputs from the LPC938 chip to various parts of the charger. The first of these, called channel A, controls the PWM target voltage, and is set to maximum (represented by the largest 10-bit unsigned integer, 1023, or 3FFh). Other calls set the output current and voltage setpoints to zero.

AD_init initializes the Analog to Digital system. The next call is to delay_ms with a parameter of 100, which busy waits for about 100 ms. WDfeed "feeds the watchdog". The watchdog timer is a counter that counts down (I think) and if it ever reaches zero, something has gone wrong and a watchdog interrupt occurs. Typically this will reset the processor. The idea is that good code will periodically "feed" the watchdog, in other words set the counter value to a large value, and it takes many milliseconds for the counter to reach zero. If the processor "goes off into the weeds" (crashes, starts executing random code), it is unlikely to come across code that feeds the watchdog, so eventually the counter will reach zero and the processor resets. Hopefully, things will not go bad again, and the batteries won't cook from the charger freezing.

The call to read_EEPROM reads 58 bytes from the start of the EEPROM to th evariable named EEPROM, which is located in "external" memory (indicated by setting R3 to 1; don't worry about the details). If the first byte is no E5h, then the processor will freeze in the tight loop errorLoop1. Presumably, the developers have some way of knowing where the processor is frozen; the processor would appear to be completely dead if it ends up in this loop (no flashing of the tiny LED near the 5-pin connector, for example). In every example I've seen so far, the second byte has been 02h, so control branches to EEPROM_INFO_VER_is_02. Perhaps to cater for old EEPROM contents, the firmware checks for 9Ch, and if so, intializes EEPROM with default values, such as 205.0 V for full power maximum voltage (for this particular model).
If the second EEPROM byte is neither of these two values, the processor gets stuck in another endless loop (errorLoop2).


506 Posts
Very informative even though I know very little about programming, I just want to thank you for all the time and effort explaining some of it.

My question is this, are they doing this on purpose to make it hard for someone else to program, or is it to using older and maybe cheaper processors etc.

I saw Jack showing how to program with the CAN unit, and it was easy to re program. I'm just amazed that the software the factory used has not been hacked, I'm sure that the hardware they use cant be anything special.



1,148 Posts
Discussion Starter #13
Next (not shown) is a call to User_curve_set(). This checks for the button being pushed or not, and changes the user selection if necessary. This includes the one call to ProgramFlash(), shown earlier, where the user selection is saved (programmed) to flash memory.

One of the values in EEPROM is the hardware version (multiplied by 10 to make it an integer). If this is not 13 (representing hardware version 1.3), the processor will get stuck in a third endless loop (that's it now, there are only 3 of these). This hardware version number does not seem to correspond to anything written on the charger case or on any of the charger PCBs; this seems to be a notional "firmware hardware version", which possibly only changes if and when the hardware changes in such a way that the software has to "drive" it differently.

Now the code checks the "requirements" of the firmware against the "capabilities" of the charger, as stated in the EEPROM contents. For example, a given firmware may require 168.0 V at full power, and the hardware may be capable of say 172.0 V at full power. This is acceptable; the hardware is at least as capable as the firmware requires. If however the charger requirement was for 180.0 V, then this could not be achieved with this hardware. At least one of these comparisons would therefore fail, and yet again, this would result in an infinite loop (errorLoop3 again). So this is a sort of sanity check that the firmware and hardware are matched. If not, the charger will appear to be dead. This is something we have to keep in mind when we change our requirements by changing any settings.

Finally, setting the special bit IE.7 (bit addresses 80-FF are actually special function registers in the microcontroller) enables interrupts. The main interrupt is the timer interrupt, which occurs 110 times per second (every 9.09 ms). Other interrupts are associated with the serial port (received character interrupt, and transmit buffer empty interrupt).

After that and a little more setting up, we're almost ready for the main loop:

You can see the variable (actually a structure member, they look like separate variables at the assembly language level) f_DC_vol_SET (the DC voltage set point) is set to zero by a call to StoreFloatConstRam(). This function passes the 32-bit constant (here 0.0, or four bytes of 00h) after the call. The routiune adds 4 to the return address before returning, thus skipping over the parameter (it would not be good to execute such data as code, in general). HD_CUR_CLOSE() returns a floating point value (in registers R4 through R7) to a value that will turn off (close) the current set point. This value is stored at location f_DC_cur_SET. Throughout the code, there is a distinction between the DC voltage (before the output relay) and the battery voltage (after the output relay). While these are the same thing when the output relay is closed, they are different when the output relay is open.

The clr Bools1.1 instruction clears the bit whose bit address is 01h (second byte of the instruction). As mentioned before, this is actually bit one of RAM byte 20h. In this firmware (it can vary), Bools1.1 represents a bit variable called clk_100ms, which will be set when a 100 ms "boundary" is passed. The call to state_jump(0) prepares the firmware for operating in "state zero". This is a special non-charging state where the output relay is off, and its sole purpose is to decide whether or not it is safe to jump to state one, the first charging state. csp_n_secs is some sort of counter associated with the main charge logic function, called charge_state_PRO (charge state processing).


1,148 Posts
Discussion Starter #14 (Edited)
The main loop.

After initialization, the processor runs an endless loop:

The main loop starts by busy waiting for a Boolean (bit variable) to be set. This one is set by the real time interrupt routine every 100 ms. Next is a call to feed the watchdog timer (important after a long delay like this). The Boolean is cleared, ready for the next iteration of the main loop. So the main loop runs ten times per second.

Next a few counters are updated, so the charger knows when a second boundary is traversed (clk_s) and a minute boundary (clk_m). n_100ms counts the number of 100 ms ticks have elapsed since the last second boundary was crossed. Similarly, timer_s counts the number of seconds since the last minute boundary was crossed. No rocket science here.

At 134e, a variable called work_loop_num is updated. It doesn't seem to be used much in this firmware.

Some real work starts at 1361 with the call to get_AD_param. This routine retrieves the values captured in the real-time interrupt routine and converts them to floating point values. Along the way, EEPROM values are used to calibrate the values. For example, the battery voltage is read as the sum of 11 values (thus effectively averaging them), and is multiplied by the value stored in RAM (copied from EEPROM) called EE_AD_TO_BATTER_VOL. This value has the factor 1/11 in it, which converts the sum of 11 samples to the average of those 11 samples. The real-time clock interrupt has a period of 9.09 ms, so that 11 such interrupts takes 100 ms. All ADC values are handled this way (taking the average of 11 samples, 11 times per 100 ms, or 110 interrupts per second). As well as the factor 1/11, EE_AD_TO_BATTER_VOL takes into account the 10 bit ADC register value (so the maximum reading is 1023, or (2^10)-1), and also the scaling caused by the voltage divider used to convert the battery voltage (perhaps 150 V) to a value less than 3.3 V, so the processor can read it. Different voltage chargers have different voltage divider ratios. In addition, these values calibrate out the actual value of the resistors. Hence, the EEPROM values are unique to a charger, and should not be copied from one charger to another without great care.

Two of the ADC values are for temperature (internal and external). External temperature is connected to the pin 1 "enable" input that controls charger power. (I believe that when an actual external temperature probe is used, it connects to pin 1, and you can't control power via pin 1 any more.) This routine does a lot of testing of these temperatures against limits such as -70C and +130C. Depending on these tests, some error bits may be set, and various RAM locations are set.

If n_100ms is equal to one (so once per second), PFC_VOL_SET is called. This establishes a setpoint for the Power Factor Correction "front end" to aim for; this sets the DC bus voltage. If running on 240 V, this set point is 385 V (basically, run flat out), but if the mains is around 120 V, the set point is calculated based on the battery voltage, required power, and a few other factors.

The next part of the code runs three critical routines once per second. These are charge_state_PRO (Process the charger state machine), send_master_info, and bump_current. The state machine is the heart of the charger control algorithm. It starts in state zero, when the battery is not yet connected (the output relay is open), and various tests are performed to see whether it is safe to proceed to state one; states one through seven are actively charging with the output relay on. State 8 is "charge complete", with the usually solid green light, and the output relay is off. charge_state_pro decides how much current to charge the battery with at any time. It does the ramping up of current, attempts to maintain a constant voltage (depending on the "charge curve"), and so on.

send_master_info sends a large data packet over the serial port. This data is purely for the user's information. The same serial port also connects the master to the slave(s) (if present), but this is done by a different routine, and the data packet for that has a different byte near the start, distinguishing the two packet types. The diagnostic information is in essence the raw contents of one large data structure, the "RUN" data structure. In my disassemblies, all members of this structure have names starting with RUN_ . (Similarly, all members of the structure that holds the EEPROM data have names starting with EE_ ). The RUN information contains such data as the present state, battery and DC voltage (output of the back end; this is basically the same as the battery voltage if the relay is closed), the current, the voltage and current setpoints, temperatures, and so on. A lot can be learned about what a charger is doing by examining this data. bump_current is associated with some current ramps. That's why the current ramps in one second steps when the battery if first connected, for example.

FAULT_PRO processes the errors. There are 16 error bits.

VOL_CUR_SET takes the result of charge_state_PRO and outputs these to the PWM registers to make the voltage and current settings happen.

LED_PRO does the flashing of the red/green main LED, according to the present state and any error conditions.

send_listen_info checks a flag in the flash image, and if set sends the master to slave packet. When a slave or slaves is present, the master works on its half (or quarter or whatever) of the current, and assumes that the slave(s) is(are) doing the exact same thing (same voltage and current settings as the master).

At 1396, there is an unconditional branch to the start of the main loop, so the same thing repeats forever.


1,148 Posts
Discussion Starter #15 (Edited)
Serial Data

This is another post that is basically a paste from an email from some time ago.

This is the so-called "listen data", which contains a binary copy of the "run" structure, which contains a lot of useful information about what the charger is doing. It is sent every two seconds, from memory.

Floating point constants such as 41DC1943 can be converted using web pages like this one: . It will tell you that this hex number represents the floating point number 27.512335, representing in this case 27.5 degrees Celsius as the internal temperature.

FF FE _____ Serial data packets always start with these two bytes
F0 ________ COMM_DESC_LISTEN, indicates that this is a listen data packet
4A ________ length sizeof(struct SLAVE_RUN_s) (74 decimal bytes, plus 5 bytes of overhead)
02 ________ EEPROM_INFO_VER Some sort of version number for the EEPROM data format
0D ________ hardware version 1.3 (decimal 13)
14 14 _____ software and curves ver 20 (decimal)
00 00 _____ ERR_CODE: no errors
01 ________ VAC_S: 1 110 VAC (2 would indicate 220/240 V)
41 DC 19 43 internal temperature 27.512335
41 C6 05 F0 fFIRST_INTEMP 24.7529 Internal temperature at start of charge
C2 03 27 E0 f_EXTTEMP -32.78894 "External temperature", but may be derived from pin 1 analog control
42 9B B3 C2 f_DC_VOL 77.85109vols
41 7F 86 C6 f_DC_CUR 15.970404amps (this charge unit)
00 00 00 00 f_DC_CUR_WAVE
42 9B A5 07 f_BATTER_VOL 77.82232vols
01 ________ OUTSIDE_TEMP_SENSE_state 1
C1 EE 4F C0 f_BATTER_TEMP -29.78894
00 00 00 00 f_VOL_TEMP_compensate
43 A0 0A 11 PVC_Vout 320.07864
42 CF 00 00 f_DC_vol_SET 103.5vols
41 80 00 00 f_DC_cur_SET 16.0amps (this charge unit)
42 00 00 00 f_BATTER_CUR_SET 32.0amps (all units combined)
00 00 00 00 f_DVDT_15M
3F A2 83 84 fAh 1.2696385 Amp-hours delivered
00 02 _____ time_m zero minutes charging 2min
02 ________ charge_state : 1 = relay on, charging
01 ________ RELAY_FLG (relay is on)
02 ________ com_err_n (number of comms errors seen?)
6E ________ Checksum

Another packet that might be seen is the "master data" sent from the master to the slave(s). I don't have captured data handy, but it would look like this:

FF FE _____ Serial data packets always start with these two bytes
C5 ________ COMM_DESC_MASTER, indicates that this is a master data packet
0A ________ length of data sent, excluding 5 byte overhead
14 ________ software ver 20 (decimal)
01 ________ Relay flag (01 = turn relay on)
XX XX XX XX CUR_LEV: current setpoint level
YY YY YY YY HD_VOL_SET: voltage setpoint
ZZ ________ Checksum

[ Edit: annotated currents for whether they are for one charge unit, or all charge units combined. ]

1,148 Posts
Discussion Starter #16 (Edited)
EEPROM contents

This information is basically a paste from an email, so it may take a few edits before it makes sense. There are 512 bytes of EEPROM in the Elcon charger microcontrollers, but only the first few tens of bytes are used. The "documentation" refers to a PDF (or similar) file, in Chinese and broken English, that I found somewhere on the internet. It seems to be slightly out of date compared to what I find in modern chargers.

I have an Arduino program that can change the contents of the EEPROM if needed. This would usually only be needed if voltage divider resistors are changed, or if the microcontroller itself has to be replaced.

Floating point constants such as 3CAD4C9C can be converted using web pages like this one: . It will tell you that this hex number represents the floating point number 0.021154694 .

  • C5 This is a field named "valide" in the documentation. It seems to indicate the start of EEPROM information.
  • 02 EEPROM_INFO_VER. I believe that this means 2.0, and it may indicate that it is designed to work with version 2.0 firmware. I think as long as this value is 02, the rest of the data should have the same format.
  • 3CAD4C9C = float AD_to_DC_VOL. This is a constant to multiply by an ADC reading to get DC voltage. It will depend on the voltage divider resistors in the charger. Mine has the value 0.02115, which means a full scale reading of 1023 (maximum value of a 10 bit unsigned number) will convert to 1023 * 11 * 0.02115= 237.9 V. (Remember that the code uses the sum of 11 samples. That way you are effectively averaging over 11 samples without having to do a divide.) This seems like a reasonable maximum-voltage-ever-likely-to-be-seen for a 144 V nominal charger, with 160 V available at either maximum or half power.
    Let's compare this with what we find on the circuit. I see a 600k resistor in series with a 3k resistor, in a voltage divider with 5.1k at the bottom. So the voltage ratio is 608.1/5.1 = 119.2. 119 happens to be half of 238; in other words, when there is 238 V at the DC measurement point, there will be 2.00 V at the ADC input. That sounds reasonable to me.
  • 3CAE7A74 = float AD_TO_BATTER_VOL. This is 0.02130, slightly different, but the voltage divider is slightly different too. We expect a maximum of 1023 * 11 * 0.02130 = 239.7 V. Using the voltage dividers, we again see 600k plus 3.6k this time, the ratio is 608.7/5.1 = 119.35, so with 239.7 V at the battery, we'd see 239.7 / 119.35 = 2.008 V. Maybe they are actually calibrating out the tolerance of the resistors; it would be good to compare different chargers with the same voltage.
  • 408983E1 = float VOL_TO_PWM. This value is 4.297. I think that if the charger wants to target 238.0 V, it would send 238.0 * 4.297 to the PWM register, which would be 1022.7, which would round to 1023.
  • 3ACCD3A1 = float AD_to_CUR. This is 0.001563, so full scale would be 0.001563 * 1023 * 11 = 17.59. Mine is a 30 A charger, so 15 amps per half, so this seems reasonable. Looking at the circuit, 15 A across 5 mR comes to 75 mV, and the gain of the current sensor seems to be 37.54 ((3k9 // 100k) / 100R). So that comes to 2.82 V higher than the op-amp offset at the output.
  • 4268B26D = float CUR_TO_PWM. This value is 58.17. 58.17 * 17.59 = 1023.2. So to request 15 A, send (15 + 0.6532) * 58.17 to the comparator. (See below for the 0.6532).
  • 3F2738C6 = float CUR_BASE. This is 0.6532, meaning that at a real current of zero, the ADC reading times AD_TO_CUR will read this. It's because the current shunt op-amp has a deliberate offset; The PWM system doesn't work right down to zero volts. This current is added to the requested current before multiplying by CUR_TO_PWM to set the charge current. Similarly, after a measurement and multiplication by AD_TO_CUR, this current is subtracted. The ADC reading must be 0.6532 / 0.001563 = 418, which is 418/1024/11 = 3.7% of full scale, or 122 mV.

  • 0D = uint8 HD_VER. This is decimal 13, representing hardware version 1.3 (written on the side of the charger). It could be that all figures above this point are calibration values (specific to the parts in this particular unit), and the values below this point are all hardware design values (the same for all units with this design).
  • 0712 = uint16 HD_VOL_MAX_FULL_POWER. This is 1810, representing 181.0 volts, so it is saying that the charger can output this voltage at full power. Not bad for a 160 V unit. This is compared against what the curves require, and if the curves need more than this, the charger will go into one of its infinite loops.
  • 0780 = uint16 HD_VOL_MAX_HALF_POWER. This is 1920, representing 192.0 volts. So the charger can output this voltage at half power.
  • 0E 08 = uint8 TRANS_P_circle and TRANS_S_circle. I believe that "circle" is a poor translation of "turns" from Chinese to English. These are the turns ratio of the transformer, 14:8, as is written on the transformer itself.
  • 44898000 = float AC110V_POWER_MAX. Per charge unit. This is 1100.0, so half of this 5000 W charger will be limited to 1100 W at 110 V input. I believe that this is also compared against the requirements of the curves, but I don't remember the details.
  • 451C4000 = float AC220V_POWER_MAX. Per charge unit. This is 2500.0, so half of this 5000 W charger will be limited to 2500 W. Full advertised power!
  • 43629999 = float HD_VOL_LIMIT_MAX. This is 226.6 V, the battery voltage limit beyond which the charger shuts down, possibly with an error. It's usually about 3% more than the no power maximum voltage. This is NOT the highest voltage the PWM can request with 10 bits all ones (as I used to believe). In fact, the charger voltage set point is often set to this value (when it doesn't want to be voltage limited at all). This never overflows the PWM register, in other words, it's always less than the "overflow" voltage.
  • 41700000 = float HD_CUR_MAX. Per charge unit. This is 15.0, half of the 30 A advertised. Again, full current!
  • 435C0000 = HD_VOL_NOP_MAX. This is the voltage the hardware can safely produce at zero power. The value here is 220.0.
    Edit: this seems to be derived from DEF_HD_VOL_MAX_NOP_POWER, a definition for maximum voltage with no power.
  • 0000000000 These 5 bytes of zeroes are not documented. If the above was 1+2+2 bytes, then this would be 4 bytes, which could represent a float that happened to have the value zero.
  • Some firmwares load an additional 34 bytes of EEPROM data, but don't do anything with it. I suspect a copyright string.
  • FFFF... There are all FFs in the EEPROM from here on, though I did not dump the second half.

[ Edit: My current shunt is 5 mR, not 20 mR.
Extra 34 bytes.
More on CUR_BASE.
Shunt op-amp gain was wrong.
Calculate zero-current voltage on op-amp.
Updated description of HD_VOL_LIMIT_MAX.
HD_VOL_LIMIT_MAX never overflows, and is often the voltage set point.
Powers and current are per charge unit. ]

1,148 Posts
Discussion Starter #17 (Edited)
Building New Firmware

For those wanting to change the firmware in their chargers, this is the post with the meat in it. I'm sorry it took so long to find the time to get this together.

The original firmware was written in C. I have snippets of old versions of the code, but nowhere near enough to generate a functioning firmware. So for now, we have to settle for assembling from source code that has been disassembled from the original. Worse than that, we have to keep the code aligned with the original code, because we might have have separated code from data properly, so some bytes that should be relocated perhaps aren't, and some that should not be might be. By keeping the code largely in its original locations, we avoid all these problems; pointers still point to the code or data where they are meant to point.

Unfortunately at this point, it is necessary to use the Keil uVision compiler / assembler / linker / debugger tool. This software is expensive, and we only need a fraction of its full power. Hopefully one day, I'll figure out a way of using open source tools for this, but that could be a long way off.

[ Edit: see later post re IAR Embedded Workbench. ]

The good news is that for the most part, we only want to change some data, not executable code. For this we could also use assembly language, except that alas the assembler involved doesn't handle floating point constants, and we need a lot of these. They are a royal pain to maintain by hand, so we actually hand translate the data to C, and get the C compiler to generate the data for us in the correct format.

But now we have two source code files, and we need to generate a hex file for programming into the microcontroller. So these source code files are assembled or compiled to object code, and the linker is used to combine these into an "executable" file. This file gets converted to a hex file using some command line tools. To make matters worse, there are a few edits along the way.

If this sounds complicated and scary, well, it is a bit. So this is not for everyone. But if you know a little about bits and bytes, and aren't scared by having to edit a few files and execute a few scripts, then read on.

Let's start with the most important part, the "curve" data. Here is the start of such a file; the complete contents are available as attachment

[FONT=Courier New]// 144 V one stage curves

#pragma src                             // Generate assembler
typedef unsigned char byte;             // U8
typedef unsigned int word;              // U16

byte code charge_curve = 20;            // Version 2.0 data
byte code user_sel = 9;                 // User has chosen the third capacity (index=2)
float code Ah_capacities[10] = {
        180.0, 180.0, 180.0,
        180.0, 180.0, 180.0,
        180.0, 180.0, 180.0,

byte code cell_num[10] = {
        42, 43, 44,
        45, 46, 47,
        48, 49, 50,

byte code master_info_send_en = 1;         // Leave as 1 to talk to the slave if needed
byte code listen_info_send_en = 1;         // Leave as 1 to emit useful data packet
float code output_line_res = 12.5e-3;      // 12.5 mR line resistance (others have 25 mR)

// 51 x 3.65 = 186.15 V; call it 186V? Same at full and half power since one stage
word code vol_need_full_power = 1860;      // Voltage needed at full power, times 10, as integer
word code vol_need_half_power = 1860;      // Voltage needed at half power, times 10, as integer
// 51 * 3.8 V = 193.8
float code unused1 = 193.8;                // Unused; make it the same as below
float code vol_need_limit_max = 193.8;     // Maximum voltge ever needed?
float code ac110v_power_max = 1350.0;      // Maximum power to be delivered when powered from 110/120 V
float code ac220v_power_max = 2420.0;      // Maximum power to be delivered when powered from 220/240 V
float code s0_batter_vol_start = 1.50;     // Minimum volts per cell before charger will turn on
float code s0_batter_vol_over = 3.70;      // Voltage per cell above which the battery is considered overcharged
byte code stateForOverTemp = 8;            // State to go to if the battery becomes overheated
                                                                                // NOTE: this may mean what to do when the voltage on pin 1 of the 7-pin connector
                                                                                //      does something (too high in voltage?)
                                                                                // State 8 is the special "all finished, LED is solid green" state
float code s1_7_temp_over_protect_vol = 0;      // ?
word code max_time_any_state = 0;               // or e.g. 30 * 24 * 60;
                                                                                // Maximum amount of time (minutes) in any state. 0 means unlimited
                                                                                // Have seen 43200 = 30 days
float code code_dd3 = 3.20;                     // ?
I can't remember what the keyword code is needed for; I think it has to do with the fact that the 8051 processor has about 4 address spaces, and here we need the "code" address space (flash memory).

After a few definitions, there is the declaration of the Ah_capacities array. There are ten "user selections", which can be selected with the switch at the front panel; the details are described elsewhere. This is the capacities part of the user selections, there is also the number of cells selection. In this file, we're using 180 Ah for all selections. (Sometimes the ten selections are split between a few combinations of capacity and number of cells). cell_num is the array for the number of cells. So user selection one is 180 Ah and 42 cells; user selection 10 is 180 Ah and 51 cells.

Not far down is a pair of variables called vol_need_full_power and vol_need_half_power . It's not totally clear what these should exactly be set to; for this example they are set the same. Unlike many of the variables here, this one is in the form of an integer number, representing tenths of a volt. So 186.0 V is represented by 1860; 123.4 V would be represented by 1234 in these variables. The charger will check these values against values stored in EEPROM, representing the maximum voltage the charger is capable of. If it fails this test, the charger will end up in an infinite loop apparently doing nothing. So these are a sort of desperation sanity check.

The variables ac110v_power_max and ac110v_power_max set the maximum power that the charger will attempt to use with 110/120 V and 220/240 V power respectively. You could adjust these if you had a limited supply available.

s0_batter_vol_start sets the voltage (per cell, average) below which the charger considers the battery to be too low to charge safely. Similarly, if it sees higher than s0_batter_vol_over volts per cell average, it will refuse to charge it also. You can see that these are values appropriate to LiFePO4 cells, not lead acid or any of the higher voltage lithium chemistries. Only the total voltage is really important, but you may as well get these right to make it easier on yourself. These have "s0" in the name, indicating that they are accessed in state zero, which is the special state the charger begins in, before it closes the output relay and does actual charging.

Next part of the file:
[FONT=Courier New]byte code s0_led_r[5] = { 0x11, 0x44, 0, 0, 0};
                                                                                // The LED pattern for state 0 (waiting to connect to the bat
tery at the very
                                                                                //      start of charging). First byte is for the red LED, se
cond is for green;
                                                                                //      others seem to be provision for more LEDs in the future
                                                                                // LSB comes out first, e.g. with 0x1 0x00 0 0 you would see the red LED on for
                                                                                //      one second, then off for 7 seconds.
byte code s8_led_r[5] = {0, 0xFF, 0, 0, 0};     // LED pattern for state 8; solid green
byte code s8_SOC_percent = 100;                 // SOC at state 8 is 100%
byte code unused2 = 0;
float code extemp_sensor_modify = 3.0;
float code intemp_sensor_modify = 2.0;
float code intemp_sensor_use_low = 15.0;
float code intemp_sensor_use_high = 30.0;
float code bat_temp_pro_high = 50.0;
float code bat_temp_pro_high_hy = 40.0;
float code s0_batter_temp_protect_low = -20.0;
float code batter_temp_curve_base = 10.0;       // Intercept and slope of temperature compensation line?
float code batter_temp_curve_lv = 0.0;
float code new_unknown1 = 10.0;
float code new_unknown2 = 0.0;
When in state zero, the dual-color LED will be flashing according to the pattern proscribed by the s0_led_r array. Only the first two bytes are used. The 0x11 (binary 00010001) and 0x44 (binary 01000100) set the patterns for the red and green LEDs respectively. The least significant bit (last written) are accessed first. So you will see red (1 and 0), then black (0 and 0) then green (0 and 1) then black again (0 and 0). These are repeated again with the upper four bits, and the whole cycle is repeated.

Similarly, the LED pattern for special state 8 (end of charge) is set by array s8_led_r. With the second byte set to 0xFF (binary 11111111), the green LED is on solid, no flashing.

Continued next post.


1,148 Posts
Discussion Starter #18 (Edited)
The next parts of the file declares an array of structures. Each entry represents one stage. So the first entry is for stage 1 (in C, this is at index 0, but we don't need to worry about that here). Although the original source code would have an actual array, we don't bother with that here, and simply declare a bunch of variables in the right order. As with everywhere in this file, it is important not to delete or add any data elements, or change the type such that the data emitted would have a different size.

The structures declare important variables that pertain only when that stage is active. If the battery passes the tests in stage zero, it will go to stage one. What happens after that depends on the data in these structures, and the measurements made by the charger. You could have a single stage, with a single voltage and current limit, and this might be appropriate for a lithium battery. But you might put in a second stage where the current limit is set lower, so that the battery gets a full charge. For lead acid, you could have up to seven stages, which you could think of as bulk, first absorb, and so on. There are variables in the structure that tell the charger what stage to move to when certain conditions are met. Eventually, the charger should end up in stage 8, where the relay turns off and the LED is solid green, indicating end of charge.

 * Start of curve[0] for state 1
 * For this file, this is the only stage there is: CC (cv_macCur1 = 1.0) and 3.65 V (cv_vol_soft1)

byte  code cv_temp_compensate_en1 = 0;                  // ?
byte  code cv_led1[5] = {0x55, 0x00, 0, 0, 0};  // Flashing red LED
byte  code cv_SOC_percent1 = 0;                                 // 0% completed at start of stage
float code cv_vol_hd_limit1 = 4.50;                             // Harware voltage limit, expressed as volts per cell
float code cv_maxCur1 = 1.0;                                    // Max current this stage = 1.0C
float code cv_vol_soft1 = 3.65;                                 // Software voltage setpoint
float code cv_vol_soft_ctrl_speed1 = 1.0;               // Speed at which voltage can change? Units?
float code cv_vol_soft_p_append1 = 1./60.;              // If the battery voltage rises this voltage per cell over the set point,
                                                                                                        // then ramp down quickly with curve[
CURVE].cv_cur_down_step_lv_ah1 per Ah
float code cv_cur_down_step_lv_ah1 = 1./400.;   // Current (per Ah) to ramp down quickly with
float code cv_vol_soft_n_append1 = -1./120.;    // Voltage per cell below the setpoint to ramp up current quickly
float code cv_cur_up_step_lv_ah1 = 1./400.;             // Current (per Ah) to ramp up quickly with
float code cv__out_line_res1 = 1.0e-6;                  // Output line resistance per Ah, added to output_line_res for this state
byte  code cv_nextToState1 = 2;                                 // All going well, next state will be 2
float code cv_currUnder_ah1 = 0.1;                              // ?
float code cv_vol_over1 = 3.50;                                 // If over this voltage, go to next stage?
float code cv_dvdtunder_15m1 = -1.0e6;                  // ?
float code cv_volPulseOver1 = 1.0e6;                    // ?
word  code cv_timePulseOver1 = 0;                               // Some time in minutes?
float code cv_volPulseUnder1 = 0.0;                             // ?
word  code cv_timePulse1 = 0;                                   // ?
byte  code cv_LineCutToState1 = 0;                              // State to jump to when wire break is detected
byte  code cv_OverToState1 = 0;                                 // State to jump to when ?
byte  code cv_OverErrFlag1 = 1;                                 // ?
float code cv_unknown1_1 = 1.0e6;                               // ?
float code cv_unknown2_1 = 2.0;                                 // ?
word  code cv_time_m_over1 = 240;                               // Total time this stage? 240 min = 4 hr
float code cv_totalAhOver1 = 1.0e6;                             // Maximum total Ah this state?
word  code cv_time_m_over_total1 = 0xFFFF;              // Some total time in minutes
byte  code cv_unknown3_1 = 1;                                   // ? State?
byte  code cv_unknown4_1 = 1;                                   // State?
byte  code cv_restrt_to_state1 = 1;                             // State to jump to when ?
float code cv_vol_under_restrt1 = -1.0e6;               // Maybe if voltage gets less than this, jump to the state above
The second entry declares the LED pattern for this stage. Here it causes a flashing red LED (0x55 = binary 01010101). Setting cv_SOC_percent1 = 0 I believe affects one of the fields of the "listen info" output to the serial port. The start of this stage is nominated as SOC=0 (percent). I don't know if that figure gets updated, or if the SOC is reported as zero throughout all of stage 1. So the SOC field is an extremely rough figure.

Stage 1.

cv_maxCur1 = 1.0 means that the current in this stage is limited to 1C. For most charger and battery combinations, this should have no effect, since the charger usually can't charge the battery in one hour. Since we declared the capacity to be 180 Ah, this is a limit of 180 A, but none of the chargers I know of can output this much current. In later stages, this field will be way more important.

float code cv_vol_soft1 = 3.65 means that the charger could stay in this state until the voltage reaches an average of 3.65 VPC. However, other limits must be expected to be exceeded, so that a transition to stage 2 should occur fairly soon.
cv_time_m_over1 = 240 means that this stage will be exited after 240 minutes (4 hours) at most.

cv_nextToState1 = 2 means that unless some emergency limit is breached, the next state will be state 2.

I won't bother pasting the data from the next stages here, but will just highlight the important values.

Stage 2.

cv_led2[5] = {0xFF, 0x00, 0, 0, 0} means that this stage will have a solid red LED.
cv_SOC_percent2 = 10 means that this stage represents very roughly 10% SOC (or more like 10% of the charge has completed). I think this means that stage 1 is expected to be exited by one of the ramp figures, which I don't fully understand yet, so I haven't mentioned.
cv_maxCur2 = 1.0 means that this stage has the same current limit as the last one, 1C.
cv_vol_soft2 = 3.55 means that this stage will limit itself to 3.55 VPC.
cv_nextToState2 = 3 says that all going well, the next stage will be stage 3.
cv_time_m_over2 = 0xFFFF says that there is no time limit for this stage.

Stage 3.

code cv_led3[5] = {0x11, 0x00, 0, 0, 0} makes the red LED flash with a 1:3 duty cycle (one on, three off).
cv_SOC_percent3 = 70 means that this is expected to be near the end of charge.
cv_maxCur3 = 0.2 limits the current to 0.2C (36 A for these 180 Ah cells).
cv_vol_soft3 = 3.60 limits the voltage to 3.60 VPC average.
cv_nextToState3 = 4 so the next stage will be stage 4.
cv_time_m_over3 = 0xFFFF so there is no time limit on this stage either.

State 4.

code cv_led4[5] = {0xFF, 0xFF, 0, 0, 0} so this stage will have the LED solid yellow (both red and green LEDs on all the time).
cv_SOC_percent4 = 80 so this is about 80% complete.
cv_maxCur4 = 0.02
so now the current will be limited to 0.02C, which will be 3.6 A for the 180 Ah battery.
cv_vol_soft4 = 3.65 so this stage has a limit of 3.65 VPC.
cv_nextToState4 = 8 so when the above voltage is reached, it is the end of charge.
cv_time_m_over_total4 = 0xFFFF so again no time limit on this stage.

1,148 Posts
Discussion Starter #19 (Edited)
Building the Firmware

The goal here is to create the file foo.ordered.hex, which will be the file needed by the Arduino or other program that flashes the new firmware.

This is the overall process:

We start with foo.c, the source file discussed in the previous two posts. When this is compiled, we end up with a relocatable image, foo.obj, but for reasons outlined next, we need to keep the assembler file (which is needed to generate that relocatable image). So that's the first step at the top left of the diagram, where we generate foo.hex. (Of course, foo will be replaced with the real name of our project.)

This file has the data which will be inserted into a "canonical" disassembled image. For now, that image is 1500W_master_rep.a51. I've created that file from IDA Pro, by disassembling an image I obtained from an Elcon/TC charger. For this image, the data of interest happens to start at address 0xD76. In order to force the data to be linked at that address, we need to edit the assembler source for foo.c slightly. This is done by a sed script (sed is a Stream EDitor, a Cygwin utility). It inserts an ORG statement, and also deletes two lines which cause problems. So that's the second step, where we generate foo.a51. Don't be too concerned about the number of steps; once everything is set up, most of it is done automatically, either by the Keil development environment, or by the Cygwin script

We use a second Keil project (sorry, I can't find a way to combine these two projects). This project takes the two .a51 files, assembles them, links them, and produces two output files: an executable (which we don't need), and the foo.hex file. Alas, this foo.hex file needs a little massaging, mainly to combine the various odd-sized segments so that the final foo.ordered.hex has contiguous lines that all have 16 bytes of data in them. That makes the processing of that file much easier. This is done by two calls to objcopy (one converts from hex to binary, the other from binary to hex; in the process, the massaging is accomplished.) The script file does this for us, as well as the assemble and link process via a batch file generated from the second Keil project. Phew!

The two Keil project files needed have paths embedded in them, so it's best to generate these as needed. The first one, foo.uv2, is the one you'll be using most of the time. From Project / New uVision Project... enter the name of the project; use the same name as the .c file but with .uv2 extension. You will be asked for the CPU; there are an amazing range of these. Find NXP then near the end of the list of NXP processors, select P89LPC938. When it offers to add the standard startup files, choose No.
You should now have a new project. At left should be a Project Workspace with a Target 1 and a Source Group 1 under that. Right mouse on that source group and select Add Files to Group. Add your foo.c file.

Because we end up with two project files, the next step is important. Use Project / Options for target Target 1. Click on the Output tab. In the text box next to Name of Executable, enter "dummy" (without the quotes). This is so that it doesn't overwrite important files with the name foo. Now you can use the build toolbar icons to do the compile. Even after you have fixed all compile time errors, there could well be some sort of error message about a file not being found. This is normal, and can be ignored. However, you should see the file foo.SRC with a recent time stamp. You should run the script now, even though it will bomb out with errors, to generate foo.a51. To invoke it, in a Cygwin window, change to the directory with foo.c and enter this command:
./ foo
You should see foo.a51 with a recent time stamp.

To create the second project, begin again with Project / New uVision Project and give it the name foo_combine.uv2. Select the CPU as before, and choose No for the startup files as before. Right mouse button on the Source Group 1 and add these files: foo.a51, and 1500W_master_rep.a51. Choose Project / Options for Target Target 1, and click on the Output tab. This time, ensure that the Name of Executable is foo . Also tick these two boxes: Create Batch File, and Create Hex File.

Use the Rebuild All Target Files toolbar icon to build everything. This should create foo.hex with a recent timestamp, and the file Target 1.BAT. The latter file is needed by the script to do the compile and link with the updated foo.a51 file, when you make changes to foo.c later. Finally, run the script again, passing foo as before. You should now have foo.ordered.hex, ready for flashing.

Later when you make changes to foo.c, you just need to use the first projrect (foo.uv2, not foo_combine.uv2) to regenerate foo.SRC. Then run the script again, and the foo.ordered.hex file should pop out with no further effort.

The file should contain the files you need.


1,148 Posts
Discussion Starter #20 (Edited)
Reading the serial data stream

I've posted earlier about the useful information available in the serial data stream, but I seem to have forgotten to show how to access it.

You will need:

  • A small interface board, with a few dollars worth of components on it; see below
  • A 12 V plug pack (wall wart, power brick), any size
  • A computer with a serial port (these are rare now, only really old models are likely to have them) or a computer with a USB port and a USB to serial adapter (US$25 or so, like this one from Fry's)
  • A terminal emulator program; I use Tera Term. Set the program up for 2400 bps, no parity, 8 bits, binary mode. (In Tera Term, shift-escape cycles through four display modes, one of which shows the data in binary).
Schematic for the interface board is attached. It's most convenient to connect directly to the control board's 0.1" spacing 7-pin header, rather than the 7-pin round connector that it connects to. (If you really want one, those 7-pin round waterproof connectors are hard to find; I have some I got from TME in Poland, part number SP1310/P7 (male version; note that some older chargers will need a female one). Part values are not critical; any general purpose NPN transistor would do; the 3k3 resistor could be 5k1 or higher; the 15k resistor could be 10k to 50k. I used two diodes to cut the ~ 14 V output from my nominal 12 V plug-pack down to closer to 12 V.

NOTE: Do not connect RS232 directly to the 7-pin round connector (or its equivalent 7-pin straight connector on the control board). One direction requires an inverter, and there is no current limiting in the charger. I've blown up a charger's serial port doing this!

NOTE: the charger's serial port is not isolated from the battery. Check that your USB to serial adapter is isolating, or don't connect a battery, or use a laptop (with care!) that isn't charging. The isolating USB to serial adapter is by far the most convenient of these options. Usually, it won't be advertised whether the adapter is isolating. Check with a meter (e.g. metal part of computer to RS232 pin 5, signal common).

This will also work for the CAN version of the charger, since the CAN box talks to the charger over the same serial link. However, the data will be very different, and there won't be much information there, just voltage, current, and one byte with the relay status, maybe a few error bits. For some clues on the CAN data format, see this AEVA post.

Edit: The below layout proved to have too much of a strain on the Veroboard tracks. See this post for the latest physical arrangement (same schematic).


1 - 20 of 28 Posts