Compare commits

..

32 commits

Author SHA1 Message Date
Thomas Kolb e6891aed78 Initialize the addon board 2023-09-16 22:48:41 +02:00
Thomas Kolb 19735ee550 overload: exponential backoff for retry
Whenever overload is detected, the time that must pass before the load is
turned on again is doubled. If the load was on for 5 minutes, the retry time is
reset to the configured value.
2023-06-18 16:43:41 +02:00
Thomas Kolb e8dff1f017 Correct timebase_ms after deep sleep
During deep sleep, the millisecond counter is stopped. To ensure correct delay
times, we add the sleep duration to the counter after wakeup.
2023-06-18 16:18:44 +02:00
Thomas Kolb bd08440584 charge_control: delay overload trigger by a configurable time 2023-06-18 16:18:13 +02:00
Thomas Kolb 2cedcb712a Implement heater control
This change controls a GPIO on the expansion connector depending on the
surrounding temperature. If external temperature is too low for
charging, the GPIO is set high, else it is set low.

In combination with the new LNSC-2420-Addon board this allows to switch
on a high-power heater connected directly to the solar panels whenever
the temperature is too low to charge the battery. Once the temperature
becomes high enough again, the heater is switched off and the battery is
charged instead.
2023-04-22 20:02:16 +02:00
Thomas Kolb 1153da6fc2 config_b26: adjust minimum voltage to 24.5V 2022-11-13 15:05:33 +01:00
Thomas Kolb a140b3ade6 Do not send BMP280 data if invalid 2022-11-13 14:35:34 +01:00
Thomas Kolb 5090b7de4a Try to make BMP280 communication more resilient
If communication times out or a NAK is received, this is now recognized
and sensor values are flagged as invalid. The communication then
restarts on the next regular cycle. This is still to be tested.
2022-11-09 20:47:13 +01:00
Thomas Kolb 4d4cc41b46 fix: actually use the new calibration factors in flash 2022-11-09 20:45:34 +01:00
Thomas Kolb 430b7c73d0 i2c_dma: removed unused include 2022-10-15 21:49:01 +02:00
Thomas Kolb 1cbe0ce410 Prevent deep sleep during BMP280 communication 2022-10-15 21:48:15 +02:00
Thomas Kolb 5c6497fb82 charge_control: block charging if external temperature is too low 2022-10-15 21:44:27 +02:00
Thomas Kolb d7b8c90cb4 Add new temperature thresholds to flash config 2022-10-15 21:42:46 +02:00
Thomas Kolb c807acec11 bmp280: add function to check for valid measurements 2022-10-15 21:41:50 +02:00
Thomas Kolb 07dd91ecc2 bmp280: read out via DMA in the background
This removes any busy waiting on the I²C from the main loop.
Initialization is still done in a blocking way.
2022-10-15 20:49:31 +02:00
Thomas Kolb 055186180f Merge branch 'main' into bmp280 2022-10-15 16:43:36 +02:00
Thomas Kolb 3dcf412a02 BMP280: Crude workaround for detecting chip presence 2022-10-08 21:49:28 +02:00
Thomas Kolb 2f0f6a01f2 Initial BMP280 integration
This version uses only blocking i2c calls, and long transfers will
probably cause frame loss. Also, if no BMP280 is present, the firmware
will not start up and hang in an endless loop instead.
2022-10-08 20:50:08 +02:00
Thomas Kolb 16619397b1 Added script for flash config upload 2022-09-25 17:35:37 +02:00
Thomas Kolb 481cf6f9f4 Use the new flash config module 2022-09-25 17:24:21 +02:00
Thomas Kolb 4ebb231346 Preparations for dedicated configuration flash page 2022-09-25 15:57:05 +02:00
Thomas Kolb 1e093978f3 config: avoid switching between sleep and active state too often
Unfortunately, the offset voltage of the opamp for charge current
measurement breaks measurement at low currents with a small shunt, so at
dusk, when currents become low, it may happen that 0 current is measured
while the battery is still being charged. This would falsely trigger the
transition to sleep state. To improve this situation, we simply keep the
charger active for a longer time.
2022-09-24 16:39:46 +02:00
Thomas Kolb 5528259517 calibration: add development device 2022-09-24 16:28:35 +02:00
Thomas Kolb 2275edb475 Update libopencm3 2022-09-14 21:35:36 +02:00
Thomas Kolb 87ddba7af7 Recalibrate using Voltcraft VC-523SE multimeter 2021-09-19 14:27:39 +02:00
Thomas Kolb 8dd5c8121f Send error state over RS485 2021-09-19 14:21:00 +02:00
Thomas Kolb 6920c548e9 Tune high-voltage charge time and reset voltage 2021-09-19 14:07:26 +02:00
Thomas Kolb 6ff8918e62 Fixed incorrect state transition due to 0-current glitch 2021-09-19 14:06:24 +02:00
Thomas Kolb 716c158716 Discharging: use averaged values for thresholds 2021-07-10 16:38:47 +02:00
Thomas Kolb 0339b521dd charge_control: add extra state to fix initial charging
Before this change, initial charging ended exactly HOLD_TIME after the
CHARGE_INITIAL state was entered. This was usually not sufficient to
reach the INITIAL_FULL voltage. Now a new state is entered once the
INITIAL_FULL voltage is reached.
2021-07-10 16:29:48 +02:00
Thomas Kolb d239a947cd Zero meas_data on startup for consistent behaviour 2021-07-01 21:15:13 +02:00
Thomas Kolb 79fe1113de charge_control: implement voltage transition 2021-07-01 21:10:13 +02:00
27 changed files with 1476 additions and 180 deletions

3
.gitignore vendored
View file

@ -4,3 +4,6 @@
# Vim swap files
.*.sw?
# generated config HEX files
utils/*.hex

View file

@ -32,7 +32,7 @@ LDFLAGS+=--static \
-nostartfiles -Wl,--gc-sections \
-mthumb -mcpu=cortex-m0 -mthumb -mfloat-abi=soft
# the LD script
# the LD script (RAM-only does not work because the code is too large)
#LDFLAGS+=-Tldscripts/lnsc-2420-$(BUILD).ld
LDFLAGS+=-Tldscripts/lnsc-2420-release.ld

View file

@ -7,6 +7,7 @@
MEMORY
{
/*rom (rx) : ORIGIN = 0x08000000, LENGTH = 64K*/
conf (r) : ORIGIN = 0x08007c00, LENGTH = 1K
ram (rwx) : ORIGIN = 0x20000000, LENGTH = 4K
}
@ -96,6 +97,10 @@ SECTIONS
. = ALIGN(4);
end = .;
.conf : {
__conf_start = .;
} >conf
}
PROVIDE(_stack = ORIGIN(ram) + LENGTH(ram));

View file

@ -3,10 +3,18 @@
/* Define memory regions. */
MEMORY
{
rom (rx) : ORIGIN = 0x08000000, LENGTH = 32K
rom (rx) : ORIGIN = 0x08000000, LENGTH = 31K
conf (r) : ORIGIN = 0x08007c00, LENGTH = 1K
ram (rwx) : ORIGIN = 0x20000000, LENGTH = 4K
}
/* Include the common ld script. */
INCLUDE cortex-m-generic.ld
SECTIONS
{
.conf : {
__conf_start = .;
. = ALIGN(4);
} >conf
}

@ -1 +1 @@
Subproject commit 3b89fc5999874c49f6f5be65bbd5e75f7d77469c
Subproject commit c82c7406aa57948d1adabfdc51e8577ae37e99d4

35
src/addon_io.c Normal file
View file

@ -0,0 +1,35 @@
#include <libopencm3/stm32/gpio.h>
#include "pinout.h"
#include "addon_io.h"
static const uint32_t OUTPUT_LIST[2] = {ADDON_ISO_IO_OUT1, ADDON_ISO_IO_OUT2};
void addon_io_init(void)
{
// Isolated outputs configuration: output, initially low = off
gpio_clear(ADDON_ISO_IO_PORT, ADDON_ISO_IO_OUT1 | ADDON_ISO_IO_OUT2);
gpio_mode_setup(ADDON_ISO_IO_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, ADDON_ISO_IO_OUT1 | ADDON_ISO_IO_OUT2);
// Isolated inputs configuration: input, no pull resistors
gpio_mode_setup(ADDON_ISO_IO_PORT, GPIO_MODE_INPUT, GPIO_PUPD_NONE, ADDON_ISO_IO_IN);
}
void addon_io_iso_out_on(uint8_t idx)
{
gpio_set(ADDON_ISO_IO_PORT, OUTPUT_LIST[idx]);
}
void addon_io_iso_out_off(uint8_t idx)
{
gpio_clear(ADDON_ISO_IO_PORT, OUTPUT_LIST[idx]);
}
bool addon_io_read_iso_in(void)
{
return gpio_get(ADDON_ISO_IO_PORT, ADDON_ISO_IO_IN) != 0;
}

14
src/addon_io.h Normal file
View file

@ -0,0 +1,14 @@
#ifndef ADDON_IO_H
#define ADDON_IO_H
#include <stdint.h>
#include <stdbool.h>
void addon_io_init(void);
void addon_io_iso_out_on(uint8_t idx);
void addon_io_iso_out_off(uint8_t idx);
bool addon_io_read_iso_in(void);
#endif // ADDON_IO_H

284
src/bmp280.c Normal file
View file

@ -0,0 +1,284 @@
#include <libopencm3/stm32/i2c.h>
#include <libopencm3/stm32/gpio.h>
#include "i2c_dma.h"
#include "pinout.h"
#include "bmp280_comp.h"
#include "bmp280.h"
#define BMP280_7BIT_ADDR 0x76
typedef enum {
READY,
SEND_REQUEST,
WAIT_MEASUREMENT_COMPLETE,
READ_RESULTS,
COMPENSATE_TEMPERATURE,
COMPENSATE_PRESSURE,
ERROR // set by the I²C callback in case of errors/timeout
} bmp280_readout_state_t;
static bmp280_readout_state_t m_readout_state;
// calibration value cache, shared with bmp280_comp.c
extern uint16_t dig_T1;
extern int16_t dig_T2;
extern int16_t dig_T3;
extern uint16_t dig_P1;
extern int16_t dig_P2;
extern int16_t dig_P3;
extern int16_t dig_P4;
extern int16_t dig_P5;
extern int16_t dig_P6;
extern int16_t dig_P7;
extern int16_t dig_P8;
extern int16_t dig_P9;
static fxp_t m_last_temperature = 0;
static fxp_t m_last_pressure = 0;
static bool m_measurements_valid = false;
static int32_t m_temp_raw;
static int32_t m_press_raw;
static void cb_i2c_dma(i2c_dma_evt_t evt, const uint8_t *rdata, size_t rlen);
bool bmp280_init(void)
{
m_measurements_valid = false;
// configure pins for I2C1
gpio_set_af(BMP280_I2C_PORT, GPIO_AF4, BMP280_I2C_SCL | BMP280_I2C_SDA);
gpio_mode_setup(BMP280_I2C_PORT, GPIO_MODE_AF, GPIO_PUPD_PULLUP, BMP280_I2C_SCL | BMP280_I2C_SDA);
// Set up I²C
i2c_reset(I2C1);
i2c_set_speed(I2C1, i2c_speed_sm_100k, 48);
i2c_peripheral_enable(I2C1);
// Set up the Chip
uint8_t wdata[2];
uint8_t rdata[32];
uint8_t wsize, rsize;
// check for chip presence by accessing the chip id register (not a proper read!)
i2c_set_7bit_address(I2C1, BMP280_7BIT_ADDR);
i2c_set_write_transfer_dir(I2C1);
i2c_set_bytes_to_transfer(I2C1, 1);
i2c_enable_autoend(I2C1);
i2c_send_start(I2C1);
uint32_t timeout = 1000000;
while(--timeout && !i2c_transmit_int_status(I2C1)); // wait for the address transfer to complete
i2c_send_data(I2C1, 0xD0);
if((timeout == 0) || i2c_nack(I2C1)) {
// something went wrong during communication
goto err_out;
}
// read the chip ID
wsize = 0;
wdata[wsize++] = 0xD0;
rsize = 1;
i2c_transfer7(I2C1, BMP280_7BIT_ADDR, wdata, wsize, rdata, rsize);
if(rdata[0] != 0x58) {
// unexpected chip id
goto err_out;
}
// read calibration data
wsize = 0;
wdata[wsize++] = 0x88; // first calibration data register of first block
rsize = 0xA1 + 1 - 0x88;
i2c_transfer7(I2C1, BMP280_7BIT_ADDR, wdata, wsize, rdata, rsize);
dig_T1 = (((uint16_t)rdata[ 1]) << 8) | rdata[ 0]; // 0x88, 0x89
dig_T2 = (((int16_t)(int8_t)rdata[ 3]) << 8) | rdata[ 2]; // 0x8A, 0x8B
dig_T3 = (((int16_t)(int8_t)rdata[ 5]) << 8) | rdata[ 4]; // 0x8C, 0x8D
dig_P1 = (((uint16_t)rdata[ 7]) << 8) | rdata[ 6]; // 0x8E, 0x8F
dig_P2 = (((int16_t)(int8_t)rdata[ 9]) << 8) | rdata[ 8]; // 0x90, 0x91
dig_P3 = (((int16_t)(int8_t)rdata[11]) << 8) | rdata[10]; // 0x92, 0x93
dig_P4 = (((int16_t)(int8_t)rdata[13]) << 8) | rdata[12]; // 0x94, 0x95
dig_P5 = (((int16_t)(int8_t)rdata[15]) << 8) | rdata[14]; // 0x96, 0x97
dig_P6 = (((int16_t)(int8_t)rdata[17]) << 8) | rdata[16]; // 0x98, 0x99
dig_P7 = (((int16_t)(int8_t)rdata[19]) << 8) | rdata[18]; // 0x9A, 0x9B
dig_P8 = (((int16_t)(int8_t)rdata[21]) << 8) | rdata[20]; // 0x9C, 0x9D
dig_P9 = (((int16_t)(int8_t)rdata[23]) << 8) | rdata[22]; // 0x9E, 0x9F
i2c_dma_init(cb_i2c_dma);
return true;
err_out:
return false;
}
static void trigger_read_status_register(void)
{
uint8_t wdata[1];
uint8_t wsize = 0, rsize;
wdata[wsize++] = 0xF3; // status register
rsize = 1;
i2c_dma_start_transfer7(I2C1, BMP280_7BIT_ADDR, wdata, wsize, rsize);
}
static void trigger_read_results(void)
{
uint8_t wdata[1];
uint8_t wsize = 0, rsize;
wdata[wsize++] = 0xF7;
rsize = 8;
i2c_dma_start_transfer7(I2C1, BMP280_7BIT_ADDR, wdata, wsize, rsize);
}
static void cb_i2c_dma(i2c_dma_evt_t evt, const uint8_t *rdata, size_t rlen)
{
(void)rlen;
switch(evt) {
case I2C_DMA_EVT_TRANSFER_COMPLETE:
switch(m_readout_state) {
case SEND_REQUEST:
m_readout_state = WAIT_MEASUREMENT_COMPLETE;
trigger_read_status_register();
break;
case WAIT_MEASUREMENT_COMPLETE:
if((rdata[0] & 0x09) != 0) {
// either "measuring" or "im_update" is active, so measurement is not complete yet.
// try again:
trigger_read_status_register();
} else {
m_readout_state = READ_RESULTS;
trigger_read_results();
}
break;
case READ_RESULTS:
m_press_raw =
((int32_t)rdata[0] << 12) // press_msb (0xF7)
| ((int32_t)rdata[1] << 4) // press_lsb (0xF8)
| ((int32_t)rdata[2] >> 4); // press_xlsb (0xF9)
m_temp_raw =
((int32_t)rdata[3] << 12) // temp_msb (0xFA)
| ((int32_t)rdata[4] << 4) // temp_lsb (0xFB)
| ((int32_t)rdata[5] >> 4); // temp_xlsb (0xFC)
m_readout_state = COMPENSATE_TEMPERATURE;
break;
default:
// should never happen
while(1);
break;
}
break;
case I2C_DMA_EVT_TIMEOUT:
case I2C_DMA_EVT_NAK:
m_readout_state = ERROR;
break;
}
}
bool bmp280_loop(void)
{
bool retval = false;
switch(m_readout_state) {
case READY:
// nothing to do in this state
break;
case SEND_REQUEST:
case WAIT_MEASUREMENT_COMPLETE:
case READ_RESULTS:
// handled in cb_i2c_dma exclusively
break;
case COMPENSATE_TEMPERATURE:
m_last_temperature = bmp280_comp_temperature(m_temp_raw);
m_readout_state = COMPENSATE_PRESSURE;
break;
case COMPENSATE_PRESSURE:
m_last_pressure = bmp280_comp_pressure(m_press_raw);
m_readout_state = READY;
m_measurements_valid = true;
retval = true; // measurement complete
break;
case ERROR:
m_readout_state = READY;
m_measurements_valid = false;
retval = true; // measurement cycle terminated
break;
}
if(m_readout_state != READY) {
i2c_dma_loop();
}
return retval;
}
bool bmp280_start_measurement(void)
{
uint8_t wdata[2];
uint8_t wsize = 0;
if(m_readout_state != READY) {
return false; // still busy
}
wdata[wsize++] = 0xF4; // measurement control register
wdata[wsize++] = (0x01 << 5) | (0x01 << 2) | 0x01; // pressure x1, temp x1, forced mode
i2c_dma_start_transfer7(I2C1, BMP280_7BIT_ADDR, wdata, wsize, 0);
m_readout_state = SEND_REQUEST;
return true;
}
bool bmp280_are_measurements_valid(void)
{
return m_measurements_valid;
}
fxp_t bmp280_get_temperature(void)
{
return m_last_temperature;
}
fxp_t bmp280_get_pressure(void)
{
return m_last_pressure;
}

18
src/bmp280.h Normal file
View file

@ -0,0 +1,18 @@
#ifndef BMP280_H
#define BMP280_H
#include <stdbool.h>
#include <stdint.h>
#include <fxp.h>
bool bmp280_init(void);
bool bmp280_loop(void); // returns true when measurement cycle has terminated (successfully or with error)
bool bmp280_start_measurement(void);
bool bmp280_are_measurements_valid(void);
fxp_t bmp280_get_temperature(void);
fxp_t bmp280_get_pressure(void);
#endif // BMP280_H

77
src/bmp280_comp.c Normal file
View file

@ -0,0 +1,77 @@
/* BMP280 value compensation code, from the datasheet. */
#include "bmp280_comp.h"
// calibration value cache. Values are set externally in bmp280.c
uint16_t dig_T1;
int16_t dig_T2;
int16_t dig_T3;
uint16_t dig_P1;
int16_t dig_P2;
int16_t dig_P3;
int16_t dig_P4;
int16_t dig_P5;
int16_t dig_P6;
int16_t dig_P7;
int16_t dig_P8;
int16_t dig_P9;
typedef int32_t BMP280_S32_t;
typedef uint32_t BMP280_U32_t;
typedef int64_t BMP280_S64_t;
// Returns temperature in DegC, resolution is 0.01 DegC. Output value of “5123” equals 51.23 DegC.
// t_fine carries fine temperature as global value
BMP280_S32_t t_fine;
static BMP280_S32_t bmp280_compensate_T_int32(BMP280_S32_t adc_T)
{
BMP280_S32_t var1, var2, T;
var1 = ((((adc_T>>3) - ((BMP280_S32_t)dig_T1<<1))) * ((BMP280_S32_t)dig_T2)) >> 11;
var2 = (((((adc_T>>4) - ((BMP280_S32_t)dig_T1)) * ((adc_T>>4) - ((BMP280_S32_t)dig_T1))) >> 12) * ((BMP280_S32_t)dig_T3)) >> 14;
t_fine = var1 + var2;
T = (t_fine * 5 + 128) >> 8;
return T;
}
// Returns pressure in Pa as unsigned 32 bit integer in Q24.8 format (24 integer bits and 8 fractional bits).
// Output value of “24674867” represents 24674867/256 = 96386.2 Pa = 963.862 hPa
static BMP280_U32_t bmp280_compensate_P_int64(BMP280_S32_t adc_P)
{
BMP280_S64_t var1, var2, p;
var1 = ((BMP280_S64_t)t_fine) - 128000;
var2 = var1 * var1 * (BMP280_S64_t)dig_P6;
var2 = var2 + ((var1*(BMP280_S64_t)dig_P5)<<17);
var2 = var2 + (((BMP280_S64_t)dig_P4)<<35);
var1 = ((var1 * var1 * (BMP280_S64_t)dig_P3)>>8) + ((var1 * (BMP280_S64_t)dig_P2)<<12);
var1 = (((((BMP280_S64_t)1)<<47)+var1))*((BMP280_S64_t)dig_P1)>>33;
if (var1 == 0) {
return 0; // avoid exception caused by division by zero
}
p = 1048576-adc_P;
p = (((p<<31)-var2)*3125)/var1;
var1 = (((BMP280_S64_t)dig_P9) * (p>>13) * (p>>13)) >> 25;
var2 = (((BMP280_S64_t)dig_P8) * p) >> 19;
p = ((p + var1 + var2) >> 8) + (((BMP280_S64_t)dig_P7)<<4);
return (BMP280_U32_t)p;
}
fxp_t bmp280_comp_temperature(int32_t adc)
{
int32_t compensated = bmp280_compensate_T_int32(adc);
return ((fxp_t)compensated << POINTPOS) / 100L;
}
fxp_t bmp280_comp_pressure(int32_t adc)
{
uint32_t compensated = bmp280_compensate_P_int64(adc);
return (fxp_t)(((uint64_t)compensated << (POINTPOS - 8L)) / 100);
}

23
src/bmp280_comp.h Normal file
View file

@ -0,0 +1,23 @@
#ifndef BMP280_COMP_H
#define BMP280_COMP_H
#include <stdint.h>
#include <fxp.h>
/*!@brief Calculate temperature from BMP280 raw sensor value.
* @returns The temperature in °C.
*/
fxp_t bmp280_comp_temperature(int32_t adc);
/*!@brief Calculate relative humidity from BMP280 raw sensor value.
* @returns The relative humidity in %.
*/
fxp_t bmp280_comp_humidity(int32_t adc);
/*!@brief Calculate pressure from BMP280 raw sensor value.
* @returns The pressure in hPa.
*/
fxp_t bmp280_comp_pressure(int32_t adc);
#endif // BMP280_COMP_H

View file

@ -6,10 +6,24 @@
* than the actual voltage, you have to scale by 1.02 and therefore specify
* 1020 in this list. */
#define CAL_FACTOR_U_BAT 1000
#define CAL_FACTOR_U_SOLAR 1002
#define CAL_FACTOR_U_SW 1005
/* Values for the device at the B26 tower */
#if 0
#define CAL_FACTOR_U_BAT 994
#define CAL_FACTOR_U_SOLAR 997
#define CAL_FACTOR_U_SW 996
#define CAL_FACTOR_I_SOLAR 1015
#define CAL_FACTOR_I_LOAD 1000
#endif
/* Values for the development device */
#if 1
#define CAL_FACTOR_U_BAT 1012
#define CAL_FACTOR_U_SOLAR 1015
#define CAL_FACTOR_U_SW 1006
#define CAL_FACTOR_I_SOLAR 3980
#define CAL_FACTOR_I_LOAD 1000
#endif
#endif // CALIBRATION_H

View file

@ -2,28 +2,33 @@
#include <fxp.h>
#include "bmp280.h"
#include "power_switch.h"
#include "measurement.h"
#include "charge_pump.h"
#include "rs485.h"
#include "config.h"
#include "flash_config.h"
#include "addon_io.h"
#include "charge_control.h"
static const char *CHARGE_STATE_TEXT[] = {
static const char *CHARGE_STATE_TEXT[CHARGE_NUM_STATES] = {
"WAIT_CHARGEPUMP",
"INITIAL",
"INITIAL_HOLD",
"TRANSITION",
"FLOAT",
"SLEEP",
"HIGH_TEMPERATURE"
"HIGH_INTERNAL_TEMPERATURE",
"LOW_EXTERNAL_TEMPERATURE"
};
static const char *DISCHARGE_STATE_TEXT[] = {
static const char *DISCHARGE_STATE_TEXT[DISCHARGE_NUM_STATES] = {
"WAIT_CHARGEPUMP",
"OK",
"VOLTAGE_LOW",
"OVERCURRENT"
"OVERCURRENT",
"OVERCURRENT_DELAY"
};
static enum ChargeState charge_state;
@ -40,6 +45,8 @@ static fxp_t u_bat_regulation_corridor;
static fxp_t u_bat_initial_full;
static fxp_t u_bat_initial_low;
static fxp_t u_bat_initial_hold_cancel;
static fxp_t u_bat_float_full;
static fxp_t u_bat_float_low;
@ -49,13 +56,19 @@ static fxp_t u_bat_load_on;
static fxp_t u_bat_load_off;
static fxp_t load_current_limit;
static fxp_t load_current_limit_delay;
static fxp_t internal_temperature_limit;
static fxp_t internal_temperature_recovery;
static fxp_t external_temperature_limit;
static fxp_t external_temperature_recovery;
static fxp_t sleep_solar_current;
static fxp_t sleep_solar_excess_voltage;
static uint32_t overload_retry_time;
static enum ChargeState control_solar_charging(
fxp_t corridor_high,
@ -83,15 +96,21 @@ static enum ChargeState control_solar_charging(
solar_switch_onoff_duration = 0;
}
// temperature limit
// internal temperature limit: prevent overheating of the power transistors.
if(meas->avg_temperature > internal_temperature_limit) {
return CHARGE_HIGH_TEMPERATURE;
return CHARGE_HIGH_INTERNAL_TEMPERATURE;
}
// external temperature limit: prevent charging the battery if temperature is too low.
if(bmp280_are_measurements_valid() &&
bmp280_get_temperature() < external_temperature_limit) {
return CHARGE_LOW_EXTERNAL_TEMPERATURE;
}
// low-current limit (go to sleep at night)
if((time_in_state > SLEEP_STATE_DELAY)
if((time_in_state > FLASH_CONFIG_SLEEP_STATE_DELAY)
&& (current_switch_state == true)
&& (solar_switch_onoff_duration > SLEEP_SWITCH_DELAY)
&& (solar_switch_onoff_duration > FLASH_CONFIG_SLEEP_SWITCH_DELAY)
&& (meas->avg_i_solar < sleep_solar_current)) {
return CHARGE_SLEEP;
}
@ -127,28 +146,58 @@ static void solar_fsm_update(uint64_t uptime_ms, struct MeasurementResult *meas)
charge_state,
charge_time_in_state);
// time limit for initial charging
if(charge_time_in_state > INITIAL_CHARGE_HOLD_TIME) {
// switch to hold state when high threshold is reached
if(meas->u_bat >= u_bat_initial_full) {
charge_state = CHARGE_INITIAL_HOLD;
}
break;
case CHARGE_INITIAL_HOLD:
charge_state = control_solar_charging(
u_bat_initial_full,
u_bat_initial_low,
uptime_ms,
meas,
charge_state,
charge_time_in_state);
// cancel charge hold if battery voltage is below threshold
if(meas->u_bat <= u_bat_initial_hold_cancel) {
charge_state = CHARGE_INITIAL;
}
// time limit for initial hold charging
if(charge_time_in_state > FLASH_CONFIG_INITIAL_CHARGE_HOLD_TIME) {
charge_state = CHARGE_TRANSITION;
}
break;
case CHARGE_TRANSITION:
// FIXME: dynamically adjust thresholds
charge_state = control_solar_charging(
u_bat_float_full,
u_bat_float_low,
uptime_ms,
meas,
charge_state,
charge_time_in_state);
if(charge_time_in_state < FLASH_CONFIG_INITIAL_TO_FLOAT_TRANSITION_TIME) {
// dynamically adjust thresholds
fxp_t u_bat_full =
fxp_add(u_bat_initial_full,
fxp_mult(
fxp_sub(u_bat_float_full, u_bat_initial_full),
fxp_div(charge_time_in_state, FLASH_CONFIG_INITIAL_TO_FLOAT_TRANSITION_TIME)));
// time limit for transition to float charging
if(charge_time_in_state > INITIAL_TO_FLOAT_TRANSITION_TIME) {
fxp_t u_bat_low = fxp_sub(u_bat_full, u_bat_regulation_corridor);
charge_state = control_solar_charging(
u_bat_full,
u_bat_low,
uptime_ms,
meas,
charge_state,
charge_time_in_state);
} else {
// time limit for transition to float charging reached
charge_state = CHARGE_FLOAT;
break;
}
break;
case CHARGE_FLOAT:
@ -162,13 +211,7 @@ static void solar_fsm_update(uint64_t uptime_ms, struct MeasurementResult *meas)
// temperature limit
if(meas->temperature > internal_temperature_limit) {
charge_state = CHARGE_HIGH_TEMPERATURE;
break;
}
// low-current limit (go to sleep at night)
if(meas->i_solar < sleep_solar_current) {
charge_state = CHARGE_SLEEP;
charge_state = CHARGE_HIGH_INTERNAL_TEMPERATURE;
break;
}
break;
@ -190,7 +233,7 @@ static void solar_fsm_update(uint64_t uptime_ms, struct MeasurementResult *meas)
break;
case CHARGE_HIGH_TEMPERATURE:
case CHARGE_HIGH_INTERNAL_TEMPERATURE:
if(charge_state_entered) {
power_switch_solar_off();
}
@ -201,6 +244,27 @@ static void solar_fsm_update(uint64_t uptime_ms, struct MeasurementResult *meas)
}
break;
case CHARGE_LOW_EXTERNAL_TEMPERATURE:
if(charge_state_entered) {
power_switch_solar_off();
// switch on the heater via the isolated I/O addon board
addon_io_iso_out_on(0);
}
// this state can only be entered if the BMP280 measurement is valid, so
// no need to check it again here.
if(bmp280_get_temperature() > external_temperature_recovery) {
charge_state = CHARGE_WAIT_CHARGEPUMP;
// switch the heater off again when this state is left
addon_io_iso_out_off(0);
break;
}
break;
default:
// unknown state
break;
@ -233,11 +297,24 @@ static void load_fsm_update(uint64_t uptime_ms, struct MeasurementResult *meas)
}
if((meas->i_load > load_current_limit)
&& (discharge_time_in_state > LOAD_CURRENT_INRUSH_TIME)) {
discharge_state = DISCHARGE_OVERCURRENT;
&& (discharge_time_in_state > FLASH_CONFIG_LOAD_CURRENT_INRUSH_TIME)) {
if(load_current_limit_delay == 0) {
// switch off immediately
power_switch_load_off();
discharge_state = DISCHARGE_OVERCURRENT;
} else {
discharge_state = DISCHARGE_OVERCURRENT_DELAY;
}
}
if(meas->u_bat < u_bat_load_off) {
if((overload_retry_time != FLASH_CONFIG_OVERLOAD_RETRY_TIME) &&
(discharge_time_in_state > 300000)) {
// overload did not trigger for 5 minutes, so we assume its stable and
// reset the retry time delay.
overload_retry_time = FLASH_CONFIG_OVERLOAD_RETRY_TIME;
}
if(meas->avg_u_bat < u_bat_load_off) {
discharge_state = DISCHARGE_VOLTAGE_LOW;
}
break;
@ -249,19 +326,38 @@ static void load_fsm_update(uint64_t uptime_ms, struct MeasurementResult *meas)
}
// Can only switch on again after a specific amount of time has passed
if((meas->u_bat > u_bat_load_on)
&& (discharge_time_in_state > LOAD_ON_DELAY)) {
if((meas->avg_u_bat > u_bat_load_on)
&& (discharge_time_in_state > FLASH_CONFIG_LOAD_ON_DELAY)) {
discharge_state = DISCHARGE_WAIT_CHARGEPUMP;
}
break;
case DISCHARGE_OVERCURRENT_DELAY:
if(meas->i_load < load_current_limit) {
// current recovered
discharge_state = DISCHARGE_OK;
} else if(discharge_time_in_state >= FLASH_CONFIG_OVERLOAD_DELAY_TIME) {
// switch off immediately
power_switch_load_off();
discharge_state = DISCHARGE_OVERCURRENT;
}
break;
case DISCHARGE_OVERCURRENT:
// Battery voltage is too low, so keep the load switched off
// Current limit reached
if(discharge_state_entered) {
power_switch_load_off();
}
// no way out except reset
// Overload recovery
if(discharge_time_in_state >= overload_retry_time) {
// double the overload retry time for the next turn if it is less than 7 days
if(overload_retry_time < (7*24*3600*1000)) {
overload_retry_time *= 2;
}
discharge_state = DISCHARGE_WAIT_CHARGEPUMP;
}
break;
default:
@ -280,28 +376,35 @@ void charge_control_init(void)
discharge_state_entered = true;
/* calculate thresholds */
u_bat_regulation_corridor = fxp_div(FXP_FROM_INT(U_BAT_REGULATION_CORRIDOR),
u_bat_regulation_corridor = fxp_div(FXP_FROM_INT(FLASH_CONFIG_U_BAT_REGULATION_CORRIDOR),
FXP_FROM_INT(1000));
u_bat_initial_full = fxp_div(FXP_FROM_INT(U_BAT_INITIAL_FULL), FXP_FROM_INT(1000));
u_bat_initial_full = fxp_div(FXP_FROM_INT(FLASH_CONFIG_U_BAT_INITIAL_FULL), FXP_FROM_INT(1000));
u_bat_initial_low = fxp_sub(u_bat_initial_full, u_bat_regulation_corridor);
u_bat_float_full = fxp_div(FXP_FROM_INT(U_BAT_FLOAT_FULL), FXP_FROM_INT(1000));
u_bat_initial_hold_cancel = fxp_div(FXP_FROM_INT(FLASH_CONFIG_U_BAT_INITIAL_HOLD_CANCEL), FXP_FROM_INT(1000));
u_bat_float_full = fxp_div(FXP_FROM_INT(FLASH_CONFIG_U_BAT_FLOAT_FULL), FXP_FROM_INT(1000));
u_bat_float_low = fxp_sub(u_bat_float_full, u_bat_regulation_corridor);
min_charge_pump_excess_voltage = fxp_div(FXP_FROM_INT(MIN_CHARGE_PUMP_EXCESS_VOLTAGE),
min_charge_pump_excess_voltage = fxp_div(FXP_FROM_INT(FLASH_CONFIG_MIN_CHARGE_PUMP_EXCESS_VOLTAGE),
FXP_FROM_INT(1000));
u_bat_load_on = fxp_div(FXP_FROM_INT(U_BAT_LOAD_ON), FXP_FROM_INT(1000));
u_bat_load_off = fxp_div(FXP_FROM_INT(U_BAT_LOAD_OFF), FXP_FROM_INT(1000));
u_bat_load_on = fxp_div(FXP_FROM_INT(FLASH_CONFIG_U_BAT_LOAD_ON), FXP_FROM_INT(1000));
u_bat_load_off = fxp_div(FXP_FROM_INT(FLASH_CONFIG_U_BAT_LOAD_OFF), FXP_FROM_INT(1000));
load_current_limit = fxp_div(FXP_FROM_INT(LOAD_CURRENT_LIMIT_MA), FXP_FROM_INT(1000));
load_current_limit = fxp_div(FXP_FROM_INT(FLASH_CONFIG_LOAD_CURRENT_LIMIT_MA), FXP_FROM_INT(1000));
internal_temperature_limit = fxp_div(FXP_FROM_INT(INTERNAL_TEMPERATURE_LIMIT), FXP_FROM_INT(10));
internal_temperature_recovery = fxp_div(FXP_FROM_INT(INTERNAL_TEMPERATURE_RECOVERY), FXP_FROM_INT(10));
internal_temperature_limit = fxp_div(FXP_FROM_INT(FLASH_CONFIG_INTERNAL_TEMPERATURE_LIMIT), FXP_FROM_INT(10));
internal_temperature_recovery = fxp_div(FXP_FROM_INT(FLASH_CONFIG_INTERNAL_TEMPERATURE_RECOVERY), FXP_FROM_INT(10));
sleep_solar_current = fxp_div(FXP_FROM_INT(SLEEP_SOLAR_CURRENT), FXP_FROM_INT(1000));
sleep_solar_excess_voltage = fxp_div(FXP_FROM_INT(SLEEP_SOLAR_EXCESS_VOLTAGE), FXP_FROM_INT(1000));
external_temperature_limit = fxp_div(FXP_FROM_INT(FLASH_CONFIG_EXTERNAL_TEMPERATURE_LIMIT), FXP_FROM_INT(10));
external_temperature_recovery = fxp_div(FXP_FROM_INT(FLASH_CONFIG_EXTERNAL_TEMPERATURE_RECOVERY), FXP_FROM_INT(10));
sleep_solar_current = fxp_div(FXP_FROM_INT(FLASH_CONFIG_SLEEP_SOLAR_CURRENT), FXP_FROM_INT(1000));
sleep_solar_excess_voltage = fxp_div(FXP_FROM_INT(FLASH_CONFIG_SLEEP_SOLAR_EXCESS_VOLTAGE), FXP_FROM_INT(1000));
overload_retry_time = FLASH_CONFIG_OVERLOAD_RETRY_TIME;
}
@ -352,7 +455,8 @@ void charge_control_update(uint64_t uptime_ms, struct MeasurementResult *meas)
bool charge_control_is_idle(void)
{
return (((charge_state == CHARGE_SLEEP)
|| (charge_state == CHARGE_HIGH_TEMPERATURE))
|| (charge_state == CHARGE_HIGH_INTERNAL_TEMPERATURE)
|| (charge_state == CHARGE_LOW_EXTERNAL_TEMPERATURE))
&& ((discharge_state == DISCHARGE_VOLTAGE_LOW)
|| (discharge_state == DISCHARGE_OVERCURRENT)));
}
@ -361,7 +465,8 @@ bool charge_control_is_idle(void)
bool charge_control_is_charge_blocked(void)
{
switch(charge_state) {
case CHARGE_HIGH_TEMPERATURE:
case CHARGE_HIGH_INTERNAL_TEMPERATURE:
case CHARGE_LOW_EXTERNAL_TEMPERATURE:
case CHARGE_WAIT_CHARGEPUMP:
return true;

View file

@ -11,10 +11,14 @@ enum ChargeState
{
CHARGE_WAIT_CHARGEPUMP,
CHARGE_INITIAL,
CHARGE_INITIAL_HOLD,
CHARGE_TRANSITION,
CHARGE_FLOAT,
CHARGE_SLEEP,
CHARGE_HIGH_TEMPERATURE
CHARGE_HIGH_INTERNAL_TEMPERATURE,
CHARGE_LOW_EXTERNAL_TEMPERATURE,
CHARGE_NUM_STATES
};
enum DischargeState
@ -22,7 +26,10 @@ enum DischargeState
DISCHARGE_WAIT_CHARGEPUMP,
DISCHARGE_OK,
DISCHARGE_VOLTAGE_LOW,
DISCHARGE_OVERCURRENT
DISCHARGE_OVERCURRENT,
DISCHARGE_OVERCURRENT_DELAY,
DISCHARGE_NUM_STATES
};
// Error flags

View file

@ -1,6 +1,7 @@
#include <libopencm3/stm32/rcc.h>
#include "clock.h"
#include "libopencm3/stm32/f0/rcc.h"
void init_clock(void)
{
@ -26,6 +27,9 @@ void init_clock(void)
// DMA1 is used for ADC data transfer
rcc_periph_clock_enable(RCC_DMA1);
// I2C1 is used for communication with the BMP280
rcc_periph_clock_enable(RCC_I2C1);
}

View file

@ -1,95 +0,0 @@
#ifndef CONFIG_H
#define CONFIG_H
/* Thresholds for charging control */
/* Battery regulation corridor width (in mV). */
#define U_BAT_REGULATION_CORRIDOR 100
/* Initial charge battery voltage threshold (in mV). */
#define U_BAT_INITIAL_FULL 28600 // stop charging if battery voltage reaches this threshold
/* Transition to floating voltage levels after this time (in ms). */
#define INITIAL_CHARGE_HOLD_TIME 3600000
/* Duration of the transistion from initial charging to float (in ms). */
#define INITIAL_TO_FLOAT_TRANSITION_TIME 600000
/* Float charge battery voltage threshold (in mV). */
#define U_BAT_FLOAT_FULL 27600 // stop charging if battery voltage reaches this threshold
/* Minimum voltage difference to U_bat that the solar panels must produce
* before charging is resumed after it was switched off (in mV). */
#define SLEEP_SOLAR_EXCESS_VOLTAGE 1000
/* Minimum charge current required before charging is stopped to save power at
* the charge pump (in mA). */
#define SLEEP_SOLAR_CURRENT 1
/* Delay between state change and sleep state check (in ms). */
#define SLEEP_STATE_DELAY 100
/* Delay between charging switch state change and sleep state check(in ms). */
#define SLEEP_SWITCH_DELAY 1000
/* Maximum allowed microcontroller temperature (in units of 0.1 °C). If this
* temperature is exceeded, charging is stopped. The load is kept on. Do not
* set this too high as the heat has to propagate from the power MOSFETs. */
#define INTERNAL_TEMPERATURE_LIMIT 500
/* Resume operation below this temperature (in units of 0.1 °C). */
#define INTERNAL_TEMPERATURE_RECOVERY 450
/* Thresholds for load control */
/* Voltage above which the load is turned on (in mV). */
#define U_BAT_LOAD_ON 27000
/* Voltage below which the load is turned off (in mV). */
#define U_BAT_LOAD_OFF 24000
/* Current at which the overload protection triggers (in mA). */
#define LOAD_CURRENT_LIMIT_MA 10000
/* Inrush tolerance time (in ms). Overload protection is not enforced for this
* time after load power-on. */
#define LOAD_CURRENT_INRUSH_TIME 10
/* Minimum voltage that the charge pump must produce above U_bat before any
* power FET is switched on (in mV). */
#define MIN_CHARGE_PUMP_EXCESS_VOLTAGE 10000
/* The minimum time the load must be off before it can be switched on again (in ms). */
#define LOAD_ON_DELAY 10000
/* Measurement Averaging:
* Alpha is specified in units of 1/1000. 1000 means that only the latest
* value is relevant, 0 means that the measurement has no influence. The latter
* is useless.
*
* The formula to calculate the next averaged value avg from a measurement meas is:
* avg[k] = meas * (alpha/1000) + avg[k-1] * (1 - alpha/1000)
*
* For overload protection (battery voltage, load current), the latest values
* are always used.
* */
/* Averaging factor for load current. */
#define AVG_ALPHA_I_SOLAR 10
#define AVG_ALPHA_I_LOAD 10
#define AVG_ALPHA_U_BAT 100
#define AVG_ALPHA_U_SW 100
#define AVG_ALPHA_U_SOLAR 100
#define AVG_ALPHA_TEMP 10
/* Generic configuration */
/* Time (in ms) to stay active in idle state before entering deep sleep. */
#define DEEPSLEEP_DELAY 1000
/* Deep sleep duration (in seconds). */
#define DEEPSLEEP_DURATION 10
#endif // CONFIG_H

7
src/flash_config.c Normal file
View file

@ -0,0 +1,7 @@
#include "flash_config.h"
bool flash_config_is_present(void)
{
return (FLASH_CONFIG_CAL_FACTOR_U_BAT != 0xFFFF)
&& (FLASH_CONFIG_DEEPSLEEP_DURATION != 0xFFFFFFFF);
}

145
src/flash_config.h Normal file
View file

@ -0,0 +1,145 @@
#ifndef FLASH_CONFIG_H
#define FLASH_CONFIG_H
#include <stdint.h>
#include <stdbool.h>
extern uint8_t __conf_start;
#define FLASH_CONFIG_BASE_PTR ((uint8_t*)(&__conf_start))
/***** Calibration factors *****/
/* Calibration factor defined here are divided by 1000 to determine the actual
* scaling factor. Therefore, if the voltage determined by the firmware is 2%
* lower than the actual voltage, you have to scale by 1.02 and therefore
* specify 1020 in this list. */
#define FLASH_CONFIG_CAL_FACTOR_U_BAT (*(uint16_t*)(FLASH_CONFIG_BASE_PTR + 0x0000))
#define FLASH_CONFIG_CAL_FACTOR_U_SOLAR (*(uint16_t*)(FLASH_CONFIG_BASE_PTR + 0x0002))
#define FLASH_CONFIG_CAL_FACTOR_U_SW (*(uint16_t*)(FLASH_CONFIG_BASE_PTR + 0x0004))
#define FLASH_CONFIG_CAL_FACTOR_I_SOLAR (*(uint16_t*)(FLASH_CONFIG_BASE_PTR + 0x0006))
#define FLASH_CONFIG_CAL_FACTOR_I_LOAD (*(uint16_t*)(FLASH_CONFIG_BASE_PTR + 0x0008))
/***** General configuration *****/
/* Battery regulation corridor width (in mV). */
#define FLASH_CONFIG_U_BAT_REGULATION_CORRIDOR (*(int32_t*)(FLASH_CONFIG_BASE_PTR + 0x0100))
/* Initial charge battery voltage threshold (in mV). */
#define FLASH_CONFIG_U_BAT_INITIAL_FULL (*(int32_t*)(FLASH_CONFIG_BASE_PTR + 0x0104))
/* Cancel initial charge voltage hold below this battery voltage (in mV). */
#define FLASH_CONFIG_U_BAT_INITIAL_HOLD_CANCEL (*(int32_t*)(FLASH_CONFIG_BASE_PTR + 0x0108))
/* Transition to floating voltage levels after this time (in ms). */
#define FLASH_CONFIG_INITIAL_CHARGE_HOLD_TIME (*(uint32_t*)(FLASH_CONFIG_BASE_PTR + 0x010C))
/* Duration of the transistion from initial charging to float (in ms). */
#define FLASH_CONFIG_INITIAL_TO_FLOAT_TRANSITION_TIME (*(uint32_t*)(FLASH_CONFIG_BASE_PTR + 0x0110))
/* Float charge battery voltage threshold (in mV). */
#define FLASH_CONFIG_U_BAT_FLOAT_FULL (*(int32_t*)(FLASH_CONFIG_BASE_PTR + 0x0114))
/* Minimum voltage difference to U_bat that the solar panels must produce
* before charging is resumed after it was switched off (in mV). */
#define FLASH_CONFIG_SLEEP_SOLAR_EXCESS_VOLTAGE (*(int32_t*)(FLASH_CONFIG_BASE_PTR + 0x0118))
/* Minimum charge current required before charging is stopped to save power at
* the charge pump (in mA). */
#define FLASH_CONFIG_SLEEP_SOLAR_CURRENT (*(int32_t*)(FLASH_CONFIG_BASE_PTR + 0x011C))
/* Delay between state change and sleep state check (in ms). */
#define FLASH_CONFIG_SLEEP_STATE_DELAY (*(uint32_t*)(FLASH_CONFIG_BASE_PTR + 0x0120))
/* Delay between charging switch state change and sleep state check(in ms). */
#define FLASH_CONFIG_SLEEP_SWITCH_DELAY (*(uint32_t*)(FLASH_CONFIG_BASE_PTR + 0x0124))
/* Maximum allowed microcontroller temperature (in units of 0.1 °C). If this
* temperature is exceeded, charging is stopped. The load is kept on. Do not
* set this too high as the heat has to propagate from the power MOSFETs. */
#define FLASH_CONFIG_INTERNAL_TEMPERATURE_LIMIT (*(int32_t*)(FLASH_CONFIG_BASE_PTR + 0x0128))
/* Resume operation below this temperature (in units of 0.1 °C). */
#define FLASH_CONFIG_INTERNAL_TEMPERATURE_RECOVERY (*(int32_t*)(FLASH_CONFIG_BASE_PTR + 0x012C))
/* Minimum allowed external (BMP280) temperature (in units of 0.1 °C). If the
* temperature falls below this value, charging is stopped to prevent damage to
* the battery. */
#define FLASH_CONFIG_EXTERNAL_TEMPERATURE_LIMIT (*(int32_t*)(FLASH_CONFIG_BASE_PTR + 0x0130))
/* Resume operation above this temperature (in units of 0.1 °C). */
#define FLASH_CONFIG_EXTERNAL_TEMPERATURE_RECOVERY (*(int32_t*)(FLASH_CONFIG_BASE_PTR + 0x0134))
/* Thresholds for load control */
/* Voltage above which the load is turned on (in mV). */
#define FLASH_CONFIG_U_BAT_LOAD_ON (*(int32_t*)(FLASH_CONFIG_BASE_PTR + 0x0138))
/* Voltage below which the load is turned off (in mV). */
#define FLASH_CONFIG_U_BAT_LOAD_OFF (*(int32_t*)(FLASH_CONFIG_BASE_PTR + 0x013c))
/* Current at which the overload protection triggers (in mA). */
#define FLASH_CONFIG_LOAD_CURRENT_LIMIT_MA (*(int32_t*)(FLASH_CONFIG_BASE_PTR + 0x0140))
/* Inrush tolerance time (in ms). Overload protection is not enforced for this
* time after load power-on. */
#define FLASH_CONFIG_LOAD_CURRENT_INRUSH_TIME (*(uint32_t*)(FLASH_CONFIG_BASE_PTR + 0x0144))
/* Inrush tolerance time (in ms). Overload protection is not enforced for this
* time after load power-on. */
#define FLASH_CONFIG_LOAD_CURRENT_INRUSH_TIME (*(uint32_t*)(FLASH_CONFIG_BASE_PTR + 0x0144))
/* Minimum voltage that the charge pump must produce above U_bat before any
* power FET is switched on (in mV). */
#define FLASH_CONFIG_MIN_CHARGE_PUMP_EXCESS_VOLTAGE (*(int32_t*)(FLASH_CONFIG_BASE_PTR + 0x0148))
/* The minimum time the load must be off before it can be switched on again (in ms). */
#define FLASH_CONFIG_LOAD_ON_DELAY (*(uint32_t*)(FLASH_CONFIG_BASE_PTR + 0x014c))
/* Overload delay time (in ms). If load current is too high for this duration,
* load is switched permanently off. */
#define FLASH_CONFIG_OVERLOAD_DELAY_TIME (*(uint32_t*)(FLASH_CONFIG_BASE_PTR + 0x0170))
/* Overload retry time (in ms). Load is switched on again after this time if
* overload condition has triggered. */
#define FLASH_CONFIG_OVERLOAD_RETRY_TIME (*(uint32_t*)(FLASH_CONFIG_BASE_PTR + 0x0174))
/* Measurement Averaging:
* Alpha is specified in units of 1/1000. 1000 means that only the latest
* value is relevant, 0 means that the measurement has no influence. The latter
* is useless.
*
* The formula to calculate the next averaged value avg from a measurement meas is:
* avg[k] = meas * (alpha/1000) + avg[k-1] * (1 - alpha/1000)
*
* For overload protection (battery voltage, load current), the latest values
* are always used.
* */
/* Averaging factor for load current. */
#define FLASH_CONFIG_AVG_ALPHA_I_SOLAR (*(int32_t*)(FLASH_CONFIG_BASE_PTR + 0x0150))
#define FLASH_CONFIG_AVG_ALPHA_I_LOAD (*(int32_t*)(FLASH_CONFIG_BASE_PTR + 0x0154))
#define FLASH_CONFIG_AVG_ALPHA_U_BAT (*(int32_t*)(FLASH_CONFIG_BASE_PTR + 0x0158))
#define FLASH_CONFIG_AVG_ALPHA_U_SW (*(int32_t*)(FLASH_CONFIG_BASE_PTR + 0x015c))
#define FLASH_CONFIG_AVG_ALPHA_U_SOLAR (*(int32_t*)(FLASH_CONFIG_BASE_PTR + 0x0160))
#define FLASH_CONFIG_AVG_ALPHA_TEMP (*(int32_t*)(FLASH_CONFIG_BASE_PTR + 0x0164))
/* Generic configuration */
/* Time (in ms) to stay active in idle state before entering deep sleep. */
#define FLASH_CONFIG_DEEPSLEEP_DELAY (*(uint32_t*)(FLASH_CONFIG_BASE_PTR + 0x0168))
/* Deep sleep duration (in seconds). */
#define FLASH_CONFIG_DEEPSLEEP_DURATION (*(uint32_t*)(FLASH_CONFIG_BASE_PTR + 0x016c))
/* FIXME: next free memory location: 0x178. Update when adding a value! */
/* Functions */
bool flash_config_is_present(void);
#endif // FLASH_CONFIG_H

171
src/i2c_dma.c Normal file
View file

@ -0,0 +1,171 @@
#include <string.h>
#include <libopencm3/stm32/i2c.h>
#include <libopencm3/stm32/dma.h>
#include "i2c_dma.h"
#define DMA_CHANNEL_TX DMA_CHANNEL2
#define DMA_CHANNEL_RX DMA_CHANNEL3
#define TIMEOUT_COUNTER_VALUE 100 // milliseconds
static uint8_t m_wdata[2];
static uint8_t m_wlen;
static uint8_t m_rdata[32];
static uint8_t m_rlen;
static uint8_t m_addr;
static uint32_t m_i2c;
static uint32_t m_timeout_counter;
typedef enum {
IDLE,
SEND_DATA,
RECEIVE_DATA
} i2c_dma_state_t;
static i2c_dma_state_t m_state;
static i2c_dma_callback_t m_callback;
static void setup_txdma(void)
{
dma_channel_reset(DMA1, DMA_CHANNEL_TX);
dma_set_priority(DMA1, DMA_CHANNEL_TX, DMA_CCR_PL_LOW);
dma_set_memory_size(DMA1, DMA_CHANNEL_TX, DMA_CCR_MSIZE_8BIT);
dma_set_peripheral_size(DMA1, DMA_CHANNEL_TX, DMA_CCR_PSIZE_8BIT);
dma_enable_memory_increment_mode(DMA1, DMA_CHANNEL_TX);
dma_set_read_from_memory(DMA1, DMA_CHANNEL_TX);
dma_set_peripheral_address(DMA1, DMA_CHANNEL_TX, (uint32_t) &I2C_TXDR(m_i2c));
// send m_wlen bytes from m_wdata
dma_set_memory_address(DMA1, DMA_CHANNEL_TX, (uint32_t)m_wdata);
dma_set_number_of_data(DMA1, DMA_CHANNEL_TX, m_wlen);
dma_enable_channel(DMA1, DMA_CHANNEL_TX);
i2c_enable_txdma(m_i2c);
}
static void setup_rxdma(void)
{
dma_channel_reset(DMA1, DMA_CHANNEL_RX);
dma_set_priority(DMA1, DMA_CHANNEL_RX, DMA_CCR_PL_LOW);
dma_set_memory_size(DMA1, DMA_CHANNEL_RX, DMA_CCR_MSIZE_8BIT);
dma_set_peripheral_size(DMA1, DMA_CHANNEL_RX, DMA_CCR_PSIZE_8BIT);
dma_enable_memory_increment_mode(DMA1, DMA_CHANNEL_RX);
dma_set_read_from_peripheral(DMA1, DMA_CHANNEL_RX);
dma_set_peripheral_address(DMA1, DMA_CHANNEL_RX, (uint32_t) &I2C_RXDR(m_i2c));
// receive m_rlen bytes and store them in m_rdata
dma_set_memory_address(DMA1, DMA_CHANNEL_RX, (uint32_t)m_rdata);
dma_set_number_of_data(DMA1, DMA_CHANNEL_RX, m_rlen);
dma_enable_channel(DMA1, DMA_CHANNEL_RX);
i2c_enable_rxdma(m_i2c);
}
void i2c_dma_init(i2c_dma_callback_t callback)
{
m_callback = callback;
m_state = IDLE;
}
void i2c_dma_loop(void)
{
switch(m_state) {
case IDLE:
// nothing to do
return;
case SEND_DATA:
//if(i2c_transfer_complete(m_i2c)) { // does not work for some reason :(
if(dma_get_interrupt_flag(DMA1, DMA_CHANNEL_TX, DMA_TCIF)) {
dma_clear_interrupt_flags(DMA1, DMA_CHANNEL_TX, DMA_TCIF);
if(m_rlen) {
m_state = RECEIVE_DATA;
i2c_set_7bit_address(m_i2c, m_addr);
i2c_set_read_transfer_dir(m_i2c);
i2c_set_bytes_to_transfer(m_i2c, m_rlen);
i2c_send_start(m_i2c);
i2c_enable_autoend(m_i2c); // important to do it after start() for a proper repeated start!
setup_rxdma();
} else {
// no RX requested => we are done here
m_state = IDLE;
m_callback(I2C_DMA_EVT_TRANSFER_COMPLETE, NULL, 0);
}
}
if(I2C_ISR(m_i2c) & I2C_ISR_NACKF) {
// there should be no NACK during the transfer => abort
I2C_ICR(m_i2c) |= I2C_ICR_NACKCF; // clear the interrupt flag
m_state = IDLE;
m_callback(I2C_DMA_EVT_NAK, NULL, 0);
}
break;
case RECEIVE_DATA:
//if(i2c_transfer_complete(m_i2c)) { // does not work for some reason :(
if(dma_get_interrupt_flag(DMA1, DMA_CHANNEL_RX, DMA_TCIF)) {
dma_clear_interrupt_flags(DMA1, DMA_CHANNEL_RX, DMA_TCIF);
m_state = IDLE;
m_callback(I2C_DMA_EVT_TRANSFER_COMPLETE, m_rdata, m_rlen);
}
if(I2C_ISR(m_i2c) & I2C_ISR_NACKF) {
// there should be no NACK during the transfer => abort
I2C_ICR(m_i2c) |= I2C_ICR_NACKCF; // clear the interrupt flag
m_state = IDLE;
m_callback(I2C_DMA_EVT_NAK, NULL, 0);
}
break;
}
m_timeout_counter--;
if(m_timeout_counter == 0) {
m_callback(I2C_DMA_EVT_TIMEOUT, NULL, 0);
}
}
void i2c_dma_start_transfer7(uint32_t i2c, uint8_t addr, const uint8_t *w, size_t wn, size_t rn)
{
m_wlen = wn;
m_rlen = rn;
memcpy(m_wdata, w, wn);
m_state = SEND_DATA;
m_i2c = i2c;
m_addr = addr;
i2c_set_7bit_address(i2c, addr);
i2c_set_write_transfer_dir(i2c);
i2c_set_bytes_to_transfer(i2c, wn);
if(rn) {
i2c_disable_autoend(i2c); // prepare repeated start
} else {
i2c_enable_autoend(i2c); // stop condition after write transfer
}
m_timeout_counter = TIMEOUT_COUNTER_VALUE;
i2c_send_start(i2c); // start the transfer. The rest is handled by the DMA.
setup_txdma();
}

22
src/i2c_dma.h Normal file
View file

@ -0,0 +1,22 @@
#ifndef I2C_DMA_H
#define I2C_DMA_H
#include <stdint.h>
#include <stddef.h>
typedef enum
{
I2C_DMA_EVT_TRANSFER_COMPLETE,
I2C_DMA_EVT_NAK,
I2C_DMA_EVT_TIMEOUT,
} i2c_dma_evt_t;
typedef void (*i2c_dma_callback_t)(i2c_dma_evt_t evt, const uint8_t *rdata, size_t rlen);
void i2c_dma_init(i2c_dma_callback_t callback);
void i2c_dma_loop(void);
void i2c_dma_start_transfer7(uint32_t i2c, uint8_t addr, const uint8_t *w, size_t wn, size_t rn);
#endif // I2C_DMA_H

View file

@ -3,6 +3,8 @@
#include <libopencmsis/core_cm3.h>
#include <string.h>
#include <fxp.h>
#include <fxp_basic.h>
@ -14,12 +16,19 @@
#include "power_switch.h"
#include "measurement.h"
#include "deepsleep.h"
#include "bmp280.h"
#include "addon_io.h"
#include "pinout.h"
#include "config.h"
#include "flash_config.h"
volatile int wait_frame = 1;
static enum {
BMP280_NOT_PRESENT,
BMP280_IDLE,
BMP280_MEASURING,
} bmp280_state;
/* Set up systick to fire freq times per second */
static void init_systick(int freq)
@ -34,6 +43,16 @@ static void init_systick(int freq)
}
static void config_err_blink_code(uint64_t timebase_ms)
{
if(timebase_ms % 500 < 250) {
led_chplex_mask(0x3F); // all on
} else {
led_chplex_mask(0x00); // all off
}
}
static bool ledtest(uint64_t timebase_ms)
{
if(timebase_ms == 0) {
@ -152,6 +171,24 @@ static void report_status(struct MeasurementResult *meas_data)
fxp_format(meas_data->temperature, number, 2);
rs485_enqueue(number);
rs485_enqueue(":");
rs485_enqueue(charge_control_is_charge_blocked() ? "1" : "0");
rs485_enqueue(":");
rs485_enqueue(charge_control_is_discharge_blocked() ? "1" : "0");
if(bmp280_state != BMP280_NOT_PRESENT
&& bmp280_are_measurements_valid()) {
rs485_enqueue(":");
fxp_format(bmp280_get_temperature(), number, 2);
rs485_enqueue(number);
rs485_enqueue(":");
fxp_format(bmp280_get_pressure(), number, 2);
rs485_enqueue(number);
}
rs485_enqueue("\n");
}
@ -184,6 +221,25 @@ static void report_averaged(struct MeasurementResult *meas_data)
fxp_format(meas_data->avg_temperature, number, 2);
rs485_enqueue(number);
rs485_enqueue(":");
rs485_enqueue(charge_control_is_charge_blocked() ? "1" : "0");
rs485_enqueue(":");
rs485_enqueue(charge_control_is_discharge_blocked() ? "1" : "0");
if(bmp280_state != BMP280_NOT_PRESENT
&& bmp280_are_measurements_valid()) {
rs485_enqueue(":");
fxp_format(bmp280_get_temperature(), number, 2);
rs485_enqueue(number);
rs485_enqueue(":");
fxp_format(bmp280_get_pressure(), number, 2);
rs485_enqueue(number);
}
rs485_enqueue("\n");
}
@ -209,6 +265,29 @@ static void low_power_mode(uint32_t duration_sec)
}
static void handle_bmp280(uint64_t timebase_ms)
{
switch(bmp280_state) {
case BMP280_NOT_PRESENT:
// do nothing
break;
case BMP280_IDLE:
if(timebase_ms % 1000 == 137) {
bmp280_start_measurement();
bmp280_state = BMP280_MEASURING;
}
break;
case BMP280_MEASURING:
if(timebase_ms % 10 == 0 && bmp280_loop()) {
bmp280_state = BMP280_IDLE;
}
break;
}
}
int main(void)
{
//uint32_t cpuload = 0;
@ -221,9 +300,12 @@ int main(void)
struct MeasurementResult meas_data;
memset(&meas_data, 0, sizeof(meas_data));
init_clock();
init_rtc();
addon_io_init();
rs485_init();
charge_pump_init();
power_switch_init();
@ -233,6 +315,12 @@ int main(void)
led_chplex_init();
led_chplex_on(LED_CHPLEX_IDX_SOLAR_ON);
if(bmp280_init()) {
bmp280_state = BMP280_IDLE;
} else {
bmp280_state = BMP280_NOT_PRESENT;
}
init_systick(1000);
rs485_enqueue("LNSC-2420 v" VERSION " initialized.\n");
@ -244,9 +332,14 @@ int main(void)
ledtest_done = ledtest(timebase_ms);
led_chplex_periodic();
} else if(!startup_done) {
charge_pump_start();
if(flash_config_is_present()) {
charge_pump_start();
startup_done = true;
startup_done = true;
} else {
config_err_blink_code(timebase_ms);
led_chplex_periodic();
}
} else {
measurement_start();
@ -267,6 +360,9 @@ int main(void)
report_averaged(&meas_data);
}
// update BMP280
handle_bmp280(timebase_ms);
measurement_wait_for_completion();
measurement_finalize(&meas_data);
@ -275,19 +371,24 @@ int main(void)
charge_control_update(timebase_ms, &meas_data);
// deep sleep control
if(charge_control_is_idle()) {
if(!charge_control_was_idle) {
charge_control_was_idle = true;
charge_control_idle_since = timebase_ms;
} else {
// charge control already idle
if((timebase_ms - charge_control_idle_since) > DEEPSLEEP_DELAY) {
low_power_mode(DEEPSLEEP_DURATION);
charge_control_was_idle = false;
if(bmp280_state != BMP280_MEASURING) { // general blockers
if(charge_control_is_idle()) {
if(!charge_control_was_idle) {
charge_control_was_idle = true;
charge_control_idle_since = timebase_ms;
} else {
// charge control already idle
if((timebase_ms - charge_control_idle_since) > FLASH_CONFIG_DEEPSLEEP_DELAY) {
low_power_mode(FLASH_CONFIG_DEEPSLEEP_DURATION);
charge_control_was_idle = false;
// correct the time base after deep sleep
timebase_ms += FLASH_CONFIG_DEEPSLEEP_DURATION * 1000;
}
}
} else {
charge_control_was_idle = false;
}
} else {
charge_control_was_idle = false;
}
}

View file

@ -7,8 +7,7 @@
#include "pinout.h"
#include "measurement.h"
#include "calibration.h"
#include "config.h"
#include "flash_config.h"
#define ADC_NUM_CHANNELS 6
static volatile int16_t adc_values[ADC_NUM_CHANNELS];
@ -55,7 +54,7 @@ static fxp_t calc_temperature(uint16_t adc_val)
static fxp_t adc_val_to_pin_voltage(uint16_t adc_val)
{
return fxp_div(
fxp_mult(FXP_FROM_INT(adc_val), fxp_div(FXP_FROM_INT(33), FXP_FROM_INT(10))),
fxp_mult(FXP_FROM_INT((int32_t)adc_val), fxp_div(FXP_FROM_INT(33), FXP_FROM_INT(10))),
FXP_FROM_INT(4096));
}
@ -94,23 +93,23 @@ void measurement_init(void)
// Convert calibration factors to fixed-point numbers for direct use
calibration_factors[ANALOG_INPUT_U_BAT] =
fxp_div(FXP_FROM_INT(CAL_FACTOR_U_BAT), FXP_FROM_INT(1000));
fxp_div(FXP_FROM_INT(FLASH_CONFIG_CAL_FACTOR_U_BAT), FXP_FROM_INT(1000));
calibration_factors[ANALOG_INPUT_U_SOLAR] =
fxp_div(FXP_FROM_INT(CAL_FACTOR_U_SOLAR), FXP_FROM_INT(1000));
fxp_div(FXP_FROM_INT(FLASH_CONFIG_CAL_FACTOR_U_SOLAR), FXP_FROM_INT(1000));
calibration_factors[ANALOG_INPUT_U_SW] =
fxp_div(FXP_FROM_INT(CAL_FACTOR_U_SW), FXP_FROM_INT(1000));
fxp_div(FXP_FROM_INT(FLASH_CONFIG_CAL_FACTOR_U_SW), FXP_FROM_INT(1000));
calibration_factors[ANALOG_INPUT_I_SOLAR] =
fxp_div(FXP_FROM_INT(CAL_FACTOR_I_SOLAR), FXP_FROM_INT(1000));
fxp_div(FXP_FROM_INT(FLASH_CONFIG_CAL_FACTOR_I_SOLAR), FXP_FROM_INT(1000));
calibration_factors[ANALOG_INPUT_I_LOAD] =
fxp_div(FXP_FROM_INT(CAL_FACTOR_I_LOAD), FXP_FROM_INT(1000));
fxp_div(FXP_FROM_INT(FLASH_CONFIG_CAL_FACTOR_I_LOAD), FXP_FROM_INT(1000));
// Convert and precalculate coefficients for exponential averaging
avg_alpha_i_solar = fxp_div(FXP_FROM_INT(AVG_ALPHA_I_SOLAR), FXP_FROM_INT(1000));
avg_alpha_i_load = fxp_div(FXP_FROM_INT(AVG_ALPHA_I_LOAD), FXP_FROM_INT(1000));
avg_alpha_u_bat = fxp_div(FXP_FROM_INT(AVG_ALPHA_U_BAT), FXP_FROM_INT(1000));
avg_alpha_u_sw = fxp_div(FXP_FROM_INT(AVG_ALPHA_U_SW), FXP_FROM_INT(1000));
avg_alpha_u_solar = fxp_div(FXP_FROM_INT(AVG_ALPHA_U_SOLAR), FXP_FROM_INT(1000));
avg_alpha_temp = fxp_div(FXP_FROM_INT(AVG_ALPHA_TEMP), FXP_FROM_INT(1000));
avg_alpha_i_solar = fxp_div(FXP_FROM_INT(FLASH_CONFIG_AVG_ALPHA_I_SOLAR), FXP_FROM_INT(1000));
avg_alpha_i_load = fxp_div(FXP_FROM_INT(FLASH_CONFIG_AVG_ALPHA_I_LOAD), FXP_FROM_INT(1000));
avg_alpha_u_bat = fxp_div(FXP_FROM_INT(FLASH_CONFIG_AVG_ALPHA_U_BAT), FXP_FROM_INT(1000));
avg_alpha_u_sw = fxp_div(FXP_FROM_INT(FLASH_CONFIG_AVG_ALPHA_U_SW), FXP_FROM_INT(1000));
avg_alpha_u_solar = fxp_div(FXP_FROM_INT(FLASH_CONFIG_AVG_ALPHA_U_SOLAR), FXP_FROM_INT(1000));
avg_alpha_temp = fxp_div(FXP_FROM_INT(FLASH_CONFIG_AVG_ALPHA_TEMP), FXP_FROM_INT(1000));
// Inverse (1 - alpha) exponential averaging coefficients
avg_alpha_i_solar_inv = fxp_sub(FXP_FROM_INT(1), avg_alpha_i_solar);

View file

@ -43,4 +43,21 @@
#define RS485_TX_PIN GPIO6
#define RS485_RX_PIN GPIO7
/*** Expansion connector signals ***/
/* BMP280 I²C */
#define BMP280_I2C_PORT GPIOA
#define BMP280_I2C_SCL GPIO9
#define BMP280_I2C_SDA GPIO10
/* Isolated inputs and outputs on I/O addon board */
#define ADDON_ISO_IO_PORT GPIOA
#define ADDON_ISO_IO_OUT1 GPIO5
#define ADDON_ISO_IO_OUT2 GPIO6
#define ADDON_ISO_IO_IN GPIO7
#endif // PINOUT_H

125
utils/config_b26.yaml Normal file
View file

@ -0,0 +1,125 @@
# Values for the device at the B26 tower
calibration:
# Calibration factor defined here are divided by 1000 to determine the actual
# scaling factor. Therefore, if the voltage determined by the firmware is 2%
# lower than the actual voltage, you have to scale by 1.02 and therefore
# specify 1020 in this list.
CAL_FACTOR_U_BAT: 994
CAL_FACTOR_U_SOLAR: 997
CAL_FACTOR_U_SW: 996
CAL_FACTOR_I_SOLAR: 1015
CAL_FACTOR_I_LOAD: 1000
config:
# Thresholds for charging control
# Battery regulation corridor width (in mV).
U_BAT_REGULATION_CORRIDOR: 100
# Initial charge battery voltage threshold (in mV).
# stop charging if battery voltage reaches this threshold
U_BAT_INITIAL_FULL: 28600
# Cancel initial charge voltage hold below this battery voltage (in mV).
U_BAT_INITIAL_HOLD_CANCEL: 27000
# Transition to floating voltage levels after this time (in ms).
INITIAL_CHARGE_HOLD_TIME: 1800000
# Duration of the transistion from initial charging to float (in ms).
INITIAL_TO_FLOAT_TRANSITION_TIME: 600000
# Float charge battery voltage threshold (in mV).
# stop charging if battery voltage reaches this threshold
U_BAT_FLOAT_FULL: 27600
# Minimum voltage difference to U_bat that the solar panels must produce
# before charging is resumed after it was switched off (in mV).
SLEEP_SOLAR_EXCESS_VOLTAGE: 1000
# Minimum charge current required before charging is stopped to save power at
# the charge pump (in mA).
SLEEP_SOLAR_CURRENT: 1
# Delay between state change and sleep state check (in ms).
SLEEP_STATE_DELAY: 60000
# Delay between charging switch state change and sleep state check(in ms).
SLEEP_SWITCH_DELAY: 1000
# Maximum allowed microcontroller temperature (in units of 0.1 °C). If this
# temperature is exceeded, charging is stopped. The load is kept on. Do not
# set this too high as the heat has to propagate from the power MOSFETs.
INTERNAL_TEMPERATURE_LIMIT: 500
# Resume operation below this temperature (in units of 0.1 °C).
INTERNAL_TEMPERATURE_RECOVERY: 450
# Minimum allowed external (BMP280) temperature (in units of 0.1 °C). If the
# temperature falls below this value, charging is stopped to prevent damage to
# the battery.
EXTERNAL_TEMPERATURE_LIMIT: -30
# Resume operation above this temperature (in units of 0.1 °C).
EXTERNAL_TEMPERATURE_RECOVERY: 0
# Thresholds for load control
# Voltage above which the load is turned on (in mV).
U_BAT_LOAD_ON: 27000
# Voltage below which the load is turned off (in mV).
U_BAT_LOAD_OFF: 24500
# Current at which the overload protection triggers (in mA).
LOAD_CURRENT_LIMIT_MA: 10000
# Inrush tolerance time (in ms). Overload protection is not enforced for this
# time after load power-on.
LOAD_CURRENT_INRUSH_TIME: 10
# Overload delay time (in ms). If load current is too high for this duration,
# load is switched permanently off.
FLASH_CONFIG_OVERLOAD_DELAY_TIME: 10
# Overload retry time (in ms). Load is switched on again after this time if
# overload condition has triggered. If overload immediately triggers again,
# this time is doubled.
FLASH_CONFIG_OVERLOAD_RETRY_TIME: 10000
# Minimum voltage that the charge pump must produce above U_bat before any
# power FET is switched on (in mV).
MIN_CHARGE_PUMP_EXCESS_VOLTAGE: 10000
# The minimum time the load must be off before it can be switched on again (in ms).
LOAD_ON_DELAY: 10000
# Measurement Averaging:
# Alpha is specified in units of 1/1000. 1000 means that only the latest
# value is relevant, 0 means that the measurement has no influence. The latter
# is useless.
#
# The formula to calculate the next averaged value avg from a measurement meas is:
# avg[k] = meas * (alpha/1000) + avg[k-1] * (1 alpha/1000)
#
# For overload protection (battery voltage, load current), the latest values
# are always used.
# Averaging factor for load current.
AVG_ALPHA_I_SOLAR: 10
AVG_ALPHA_I_LOAD: 10
AVG_ALPHA_U_BAT: 100
AVG_ALPHA_U_SW: 100
AVG_ALPHA_U_SOLAR: 100
AVG_ALPHA_TEMP: 10
# Generic configuration
# Time (in ms) to stay active in idle state before entering deep sleep.
DEEPSLEEP_DELAY: 1000
# Deep sleep duration (in seconds).
DEEPSLEEP_DURATION: 10

116
utils/config_dev.yaml Normal file
View file

@ -0,0 +1,116 @@
# Values for the development device
calibration:
# Calibration factor defined here are divided by 1000 to determine the actual
# scaling factor. Therefore, if the voltage determined by the firmware is 2%
# lower than the actual voltage, you have to scale by 1.02 and therefore
# specify 1020 in this list.
CAL_FACTOR_U_BAT: 1012
CAL_FACTOR_U_SOLAR: 1015
CAL_FACTOR_U_SW: 1006
CAL_FACTOR_I_SOLAR: 3980
CAL_FACTOR_I_LOAD: 1000
config:
# Thresholds for charging control
# Battery regulation corridor width (in mV).
U_BAT_REGULATION_CORRIDOR: 100
# Initial charge battery voltage threshold (in mV).
# stop charging if battery voltage reaches this threshold
U_BAT_INITIAL_FULL: 28600
# Cancel initial charge voltage hold below this battery voltage (in mV).
U_BAT_INITIAL_HOLD_CANCEL: 27000
# Transition to floating voltage levels after this time (in ms).
INITIAL_CHARGE_HOLD_TIME: 1800000
# Duration of the transistion from initial charging to float (in ms).
INITIAL_TO_FLOAT_TRANSITION_TIME: 600000
# Float charge battery voltage threshold (in mV).
# stop charging if battery voltage reaches this threshold
U_BAT_FLOAT_FULL: 27600
# Minimum voltage difference to U_bat that the solar panels must produce
# before charging is resumed after it was switched off (in mV).
SLEEP_SOLAR_EXCESS_VOLTAGE: 1000
# Minimum charge current required before charging is stopped to save power at
# the charge pump (in mA).
SLEEP_SOLAR_CURRENT: 1
# Delay between state change and sleep state check (in ms).
SLEEP_STATE_DELAY: 60000
# Delay between charging switch state change and sleep state check(in ms).
SLEEP_SWITCH_DELAY: 1000
# Maximum allowed microcontroller temperature (in units of 0.1 °C). If this
# temperature is exceeded, charging is stopped. The load is kept on. Do not
# set this too high as the heat has to propagate from the power MOSFETs.
INTERNAL_TEMPERATURE_LIMIT: 500
# Resume operation below this temperature (in units of 0.1 °C).
INTERNAL_TEMPERATURE_RECOVERY: 450
# Minimum allowed external (BMP280) temperature (in units of 0.1 °C). If the
# temperature falls below this value, charging is stopped to prevent damage to
# the battery.
EXTERNAL_TEMPERATURE_LIMIT: 250
# Resume operation above this temperature (in units of 0.1 °C).
EXTERNAL_TEMPERATURE_RECOVERY: 270
# Thresholds for load control
# Voltage above which the load is turned on (in mV).
U_BAT_LOAD_ON: 27000
# Voltage below which the load is turned off (in mV).
U_BAT_LOAD_OFF: 24000
# Current at which the overload protection triggers (in mA).
LOAD_CURRENT_LIMIT_MA: 10000
# Inrush tolerance time (in ms). Overload protection is not enforced for this
# time after load power-on.
LOAD_CURRENT_INRUSH_TIME: 10
# Minimum voltage that the charge pump must produce above U_bat before any
# power FET is switched on (in mV).
MIN_CHARGE_PUMP_EXCESS_VOLTAGE: 10000
# The minimum time the load must be off before it can be switched on again (in ms).
LOAD_ON_DELAY: 10000
# Measurement Averaging:
# Alpha is specified in units of 1/1000. 1000 means that only the latest
# value is relevant, 0 means that the measurement has no influence. The latter
# is useless.
#
# The formula to calculate the next averaged value avg from a measurement meas is:
# avg[k] = meas * (alpha/1000) + avg[k-1] * (1 alpha/1000)
#
# For overload protection (battery voltage, load current), the latest values
# are always used.
# Averaging factor for load current.
AVG_ALPHA_I_SOLAR: 10
AVG_ALPHA_I_LOAD: 10
AVG_ALPHA_U_BAT: 100
AVG_ALPHA_U_SW: 100
AVG_ALPHA_U_SOLAR: 100
AVG_ALPHA_TEMP: 10
# Generic configuration
# Time (in ms) to stay active in idle state before entering deep sleep.
DEEPSLEEP_DELAY: 1000
# Deep sleep duration (in seconds).
DEEPSLEEP_DURATION: 10

73
utils/config_to_hex.py Executable file
View file

@ -0,0 +1,73 @@
#!/usr/bin/env python3
import sys
import struct
import yaml
from intelhex import IntelHex
BASE_ADDR = 0x08007C00
CALIBRATION_KEY_TO_OFFSET = {
"CAL_FACTOR_U_BAT": 0x0000,
"CAL_FACTOR_U_SOLAR": 0x0002,
"CAL_FACTOR_U_SW": 0x0004,
"CAL_FACTOR_I_SOLAR": 0x0006,
"CAL_FACTOR_I_LOAD": 0x0008,
}
CONFIG_KEY_TO_OFFSET = {
"U_BAT_REGULATION_CORRIDOR": 0x0100,
"U_BAT_INITIAL_FULL": 0x0104,
"U_BAT_INITIAL_HOLD_CANCEL": 0x0108,
"INITIAL_CHARGE_HOLD_TIME": 0x010C,
"INITIAL_TO_FLOAT_TRANSITION_TIME": 0x0110,
"U_BAT_FLOAT_FULL": 0x0114,
"SLEEP_SOLAR_EXCESS_VOLTAGE": 0x0118,
"SLEEP_SOLAR_CURRENT": 0x011C,
"SLEEP_STATE_DELAY": 0x0120,
"SLEEP_SWITCH_DELAY": 0x0124,
"INTERNAL_TEMPERATURE_LIMIT": 0x0128,
"INTERNAL_TEMPERATURE_RECOVERY": 0x012C,
"EXTERNAL_TEMPERATURE_LIMIT": 0x0130,
"EXTERNAL_TEMPERATURE_RECOVERY": 0x0134,
"U_BAT_LOAD_ON": 0x0138,
"U_BAT_LOAD_OFF": 0x013c,
"LOAD_CURRENT_LIMIT_MA": 0x0140,
"LOAD_CURRENT_INRUSH_TIME": 0x0144,
"MIN_CHARGE_PUMP_EXCESS_VOLTAGE": 0x0148,
"LOAD_ON_DELAY": 0x014c,
"AVG_ALPHA_I_SOLAR": 0x0150,
"AVG_ALPHA_I_LOAD": 0x0154,
"AVG_ALPHA_U_BAT": 0x0158,
"AVG_ALPHA_U_SW": 0x015c,
"AVG_ALPHA_U_SOLAR": 0x0160,
"AVG_ALPHA_TEMP": 0x0164,
"DEEPSLEEP_DELAY": 0x0168,
"DEEPSLEEP_DURATION": 0x016c,
"FLASH_CONFIG_OVERLOAD_DELAY_TIME": 0x0170,
"FLASH_CONFIG_OVERLOAD_RETRY_TIME": 0x0174
}
if __name__ == "__main__":
output = IntelHex()
if len(sys.argv) < 3:
print(f"usage: {sys.argv[0]} <yaml-config> <output-hex>")
sys.exit(1)
with open(sys.argv[1], 'r') as configfile:
config = yaml.safe_load(configfile)
for key, offset in CALIBRATION_KEY_TO_OFFSET.items():
value = config['calibration'][key]
enc_bytes = struct.pack('<H', value)
output.puts(BASE_ADDR + offset, enc_bytes)
for key, offset in CONFIG_KEY_TO_OFFSET.items():
value = config['config'][key]
enc_bytes = struct.pack('<i', value)
output.puts(BASE_ADDR + offset, enc_bytes)
output.write_hex_file(sys.argv[2])

18
utils/upload_config_hex.sh Executable file
View file

@ -0,0 +1,18 @@
#!/bin/sh
set -euo pipefail
HEXFILE="$1"
if [ ! -f "$HEXFILE" ]; then
echo "$HEXFILE cannot be accessed."
exit 1
fi
JLinkExe -device STM32F030C8 -speed 1000 -if SWD <<EOF
connect
loadfile $HEXFILE
r
g
exit
EOF