Here we look at the actual charger control. While not as simple as the inverter code, the code for doing battery charging is not much more complicated.
The code is essentially nothing more than controlling the pulse width of the high-side mosfet.
The rest is mostly details that are hardware specific.
A Lithium battery is charged using a Constant-Current-Constant-Voltage (CCCV) algorithm.
The first phase is controlling the current so it doesn't exceed the maximum.
The second phase is controlling the voltage so it doesn't exceed the maximum.
The way current is controlled is by controlling voltage, which is done by controlling the pulse width,
so these phases are not really different.
The code below is the essential operation of the battery charger:
Code:
checkCharge()
{
if (statusStop == chargeStatus) {
// do nothing, PWM is stopped
}
// reduce pwm if battery voltage is too high
else if (chargeVoltage > voltageConstant) {
chargeStatus = statusCV; // we are in constant voltage phase
if (chargeCurrent < currentMin) { // check if current has dropped below stop cutoff
chargeStatus = statusStop;
stopPWM();
}
else {
decreasePWM(); // lower voltage
}
}
// reduce pwm if we are above the current limit
else if (chargeCurrent > currentLimit) {
chargeStatus = statusCC; // we are in constant voltage phase
decreasePWM(); // lower voltage(current)
}
// otherwise we increase until we hit the above limits
// can be in either CV or CC mode
else {
increasePWM();
}
}
The rest are details, which I go through for completeness.
Most all microprocessors, but particularly the MSP430, is most often designed as an interrupt-driven system.
The inverter code didn't use any interrupts, which is unusual. Interrupts allow the processor to respond immediately to real-time events, and otherwise go to sleep and conserve power. The MSP430 in particular has very low power sleep modes where only
nano amps of power are consumed.
For our battery charger, power consumption is not an issue. What is the main issue is the Timer.
The Timer controls the on/off switching of the mosfet, or pulse-width-modulation (PWM).
The MSP430 has a Timer that is quite powerful and a lot of functionality can be done by the hardware.
The key is to get it configured properly, which can be confusing even after my years of using them.
Let's look at the setupTimer() code that does the hardpart of configuring TimerA which is essentially just three lines:
Code:
CCRO = 256;
TACTL = TASSEL_SMCLK + TACLR + TAIE + MC_UPTO_CCR0;
CCTL1 = OUTMOD_3;
The MSP430 is configured by setting bits in control registers, which can be tedious, and we try to avoid that using some named values and do what looks like adding up a bunch of named features, but it really is constructing a bitfield value and assigning to the register. So TACTL is the TimerA configuration,
which we set four features. TASSEL_SMCLK sets the clock for the timer, which is the 'SubMain' clock otherwise called the CPU clock which we set to 4Mhz earlier. 'TACLR' just initializes the timer to zero.
'TAIE' enables timer interrupts. The key is the 'MC_UPTO_CCRO' setting, which configures the timer to count from 0 up to the value of the CCRO register over and over. So essentially the CCRO register defines the frequency of the PWM timer. With a 4Mhz clock that restarts at zero every 256 clocks, we have a 16Khz frequency.
The next line configures TimerA 'Counter Control' for counter one (there are three, we only use number one). The cryptic OUTMOD_3, configures the counter control to generate an output that turns OFF when the timer hits CCRO, and turns ON when the timer hits CCR1. So with this all setup, we just have to set the CCR1 register to control the PWM on-time.
So for example, when CCR1 is 200, the output is off while it counts from 0 to 200, then output turns on at 200 until 256 when the timer turns off the output and restarts the count back to zero again.
So the on-time is 56/256 or 21% on, 79% off.
I should also mention the detail of what pin on the MSP430 chip has the outputs. The TimerA Counter One output is on the TA1 pin. Which is the TA1 pin? If you look at the chip documentation (shown in the inverter post), you can see pins that have labels TA1 next to them. If you look at the setupPorts() function,
you can see the line where we set the P1SEL register to configure two pins as TA1 pins, and not their normal I/O function.
I hope that was not confusing, but I can understand how it might be, and all it takes is having something a 'bit off' and it can be a mystery why it isn't working the way it should. The good news is the code shown does indeed work.
Staying on the PWM, we can look at the code which updates the PWM.
As a battery charging it only needs to update quite slowly, unlike motor control which one might want it to speed up or slow down very quickly.
We use a very simple method here of just increasing or decreasing the pulse width, and actually do so quite slowly by using a 16 bit counter for the PWM value, but only use the top 8 bits for the actual pulse width.
This reduces the noise effects of the analog-to-digital conversions by essentially averaging them over 256 readings, since we must increase the pwm 256 times before it actually has an effect on the pulse width.
So if we increment 300 times and decrement 200 times, we have averaged an increment of 100, which still isn't an actual increment! We can do this because the timer is running at a pretty high speed of 16Khz, or 16,000 times a second, so 1,000 increments can still be done 16 times a second.
So here is the code that does all this magic in a few lines.
Note again, the what the code is doing is setting CCR1 which adjust the PWM. If the value set is not different than its previous value, it has no effect on the PWM, all we did was adjust the pwm variable.
(note that the '>>' operation is a shift-right, which shifts out the low 8 bits, and only uses the high 8 bits)
Code:
decreasePWM()
{
if (pwm > 1) {
pwm = pwm - 1;
CCR1 = 256-(pwm >> 8); // update for next cycle.
}
}
increasePWM()
{
if (pwm < 0xFFFF) {
pwm = pwm + 1;
CCR1 = 256-(pwm >> 8); // update for next cycle.
}
}
Let us now turn to the Analog-To-Digital Conversion or ADC.
What the ADC does is convert analog voltage levels on the processor pins into digital binary data.
The MSP430 ADC is once again fairly sophisticated and its advanced features are fairly confusing.
We limit it to the basic operation of doing one conversion at a time.
We start with the configuration, which is essentially two lines.
We set the ADC10AE (ADC Enable) register to enable the ADC function for A0, A1, and A2.
Similar to the TA1 output, looking at the chip documentation, there are pins with labels for A0-A8,
and when they are selected by this register, they become analog input pins.
Then we configure the ADC operation itself, ADC10ON turns on the ADC, REFON enables the use of a reference voltage for doing ADC conversions, REF2_5V selects the 2.5v reference, and then yet another cryptic SREF_1 setting specifies that ADC conversions are done using Ground and the Reference Voltage
as the + and - range for the conversion. So what this means is that an input voltage of 1.25v is the midpoint between chip ground and the 2.5v reference voltage.
We have a 10 bit ADC in this chip, thus it has a digital range of 0-1024, and so a mid-point voltage would yield a value of 512. A 2.5v input would yield 1024.
Code:
ADC10AE = 0x07; // ADC enable for ADC A0,A1,A2 (ports P2.0,1,2)
ADC10CTL0 = ADC10ON | REFON | REF2_5V | SREF_1;
And finally we have the function readADC(channel), which takes a 'channel' and converts it to digital and returns the digital value. This is again somewhat cryptic, but basically we disable the ADC before we change the channel (i.e. the pin), then start the conversion. We then wait for the conversion to complete, after completed the value is in the ADC10MEM register, which we return.
Code:
readADC(channel)
{
ADC10CTL0 &= ~ENC; // Disable while changing input channel
ADC10CTL1 = channel; // Set ADC Channel to Read
ADC10CTL0 |= ENC + ADC10SC; // Start the Analog-to-Digital conversion
while ((ADC10CTL1 & ADC10BUSY) == 1); // Wait for ADC to finish the conversion
return ADC10MEM; // return the digital value
}
And finally we have the code the flashes the LED, but it isn't too important or complicated,
so I will not cover it here. The entire source code is attached for your enjoyment.
The major issue we have left is we have hardcoded into the source code the current and voltage limits to control the charging. While that works OK for someone like me that can reprogram the chip for another situation, it isn't an acceptable method for anyone else, and not even for me either. So we need a way to configure these variables, and would also like to have a display that shows the current and voltage, and not just a blinking led.
[
attachment=18]