Skip to content

Instantly share code, notes, and snippets.

@janpom

janpom/bms.ino Secret

Created February 7, 2019 22:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save janpom/9bd20d4c0887847d628b18a05e2fd7eb to your computer and use it in GitHub Desktop.
Save janpom/9bd20d4c0887847d628b18a05e2fd7eb to your computer and use it in GitHub Desktop.
#define DEV_MODE 1
#include <TFT_22_ILI9225.h>
#include <SoftwareSerial.h>
#include "tft_util.h"
#if defined(ESP8266)
#define TFT_RST 4 // D2
#define TFT_RS 5 // D1
#define TFT_CLK 14 // D5 SCK
#define TFT_SDI 13 // D7 MOSI
#define TFT_CS 15 // D8 SS
#else
#define TFT_RST 12
#define TFT_RS 9
#define TFT_CS 10 // SS
#define TFT_SDI 11 // MOSI
#define TFT_CLK 13 // SCK
#endif
#define TFT_LED 0
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <BlynkSimpleEsp8266.h>
char auth[] = "<auth>";
char ssid[] = "<ssid>";
char pass[] = "<pass>";
BlynkTimer timer;
WidgetLED led_charge_disabled(V11);
#endif
#define SCREEN_ORIENTATION 0
#define UPDATE_DELAY 100
#define MAX_INPUT_BYTES 50
#define MIN_BATTERY_CELLS 9
#define MAX_BATTERY_CELLS 15
#define LEN(X) (sizeof(X) / sizeof(X[0]))
uint8_t REQUEST_CELL_VOLTAGES_PACKET[] = {0xDD, 0xA5, 0x04, 0x00, 0xFF, 0xFC, 0x77};
uint8_t REQUEST_BASIC_INFO_PACKET[] = {0xDD, 0xA5, 0x03, 0x00, 0xFF, 0xFD, 0x77};
uint8_t REQUEST_HARDWARE_VERSION_PACKET[] = {0xDD, 0xA5, 0x05, 0x00, 0xFF, 0xFB, 0x77};
const float discharge_ticks[] = {
3.5 ,
3.67,
3.73,
3.76,
3.80,
3.83,
3.86,
3.90,
4.0 ,
4.2 ,
};
TFT_22_ILI9225 tft = TFT_22_ILI9225(TFT_RST, TFT_RS, TFT_CS, TFT_LED, 200);
//SoftwareSerial BMS_UART(5, 6);
HardwareSerial &BMS_UART = Serial;
typedef struct {
uint16_t current_mA;
uint8_t battery_cells;
bool charging_enabled;
} t_basic_info;
uint8_t INPUT_BYTES[MAX_INPUT_BYTES];
uint16_t VOLTAGES[MAX_BATTERY_CELLS];
uint16_t AVG_VOLTAGE;
t_basic_info BASIC_INFO;
float voltage_to_percent(float voltage) {
if (voltage < discharge_ticks[0])
return 0.0;
for (int i = 1; i < LEN(discharge_ticks); i++) {
float cur_voltage = discharge_ticks[i];
if (voltage < cur_voltage) {
float prev_voltage = discharge_ticks[i - 1];
float interval_perc = (voltage - prev_voltage) / (cur_voltage - prev_voltage);
float low = 1.0 * (i - 1) / (LEN(discharge_ticks) - 1);
float high = 1.0 * i / (LEN(discharge_ticks) - 1);
return low + (high - low) * interval_perc;
}
}
return 1.0;
}
int fetch_input() {
int bytes_read = 0;
while (BMS_UART.available()) {
if (bytes_read >= MAX_INPUT_BYTES)
return -1;
INPUT_BYTES[bytes_read] = BMS_UART.read();
bytes_read++;
}
// TODO: CRC check
return bytes_read;
}
bool fetch_basic_info() {
BMS_UART.write(REQUEST_BASIC_INFO_PACKET, sizeof(REQUEST_BASIC_INFO_PACKET));
delay(100);
if (fetch_input() < 26)
return false;
BASIC_INFO.current_mA = ((INPUT_BYTES[6]<<8) | INPUT_BYTES[7]) * 10;
BASIC_INFO.battery_cells = INPUT_BYTES[25];
uint8_t mosfet_status = INPUT_BYTES[24];
BASIC_INFO.charging_enabled = !!(mosfet_status & (1<<0));
return true;
}
bool fetch_voltages(uint8_t battery_cells) {
BMS_UART.write(REQUEST_CELL_VOLTAGES_PACKET, sizeof(REQUEST_CELL_VOLTAGES_PACKET));
delay(100);
int expected_bytes = 7 + 2 * battery_cells;
if (fetch_input() != expected_bytes)
return false;
for (int i=0; i<battery_cells; i++) {
VOLTAGES[i] = (INPUT_BYTES[4+i*2]<<8) | (INPUT_BYTES[4+i*2+1]);
}
return true;
}
void heartbeat(bool alive) {
uint16_t color = alive ? tft.setColor(0, 100, 0) : tft.setColor(100, 0, 0);
tft.fillRectangle(0, 204, 3, 207, color);
delay(UPDATE_DELAY / 2);
tft.fillRectangle(0, 204, 3, 207, COLOR_BLACK);
}
int min(int a, int b) {
return a < b ? a : b;
}
void sync_blynk() {
Blynk.virtualWrite(V0, AVG_VOLTAGE);
Blynk.virtualWrite(V1, VOLTAGES[0]);
Blynk.virtualWrite(V2, VOLTAGES[1]);
Blynk.virtualWrite(V3, VOLTAGES[2]);
Blynk.virtualWrite(V4, VOLTAGES[3]);
Blynk.virtualWrite(V5, VOLTAGES[4]);
Blynk.virtualWrite(V6, VOLTAGES[5]);
Blynk.virtualWrite(V7, VOLTAGES[6]);
Blynk.virtualWrite(V8, VOLTAGES[7]);
Blynk.virtualWrite(V9, VOLTAGES[8]);
Blynk.virtualWrite(V10, VOLTAGES[9]);
// FIXME
if (AVG_VOLTAGE >= 4000 && AVG_VOLTAGE <= 4002) {
Blynk.notify(String("AVG voltage = ") + String(AVG_VOLTAGE));
}
if (BASIC_INFO.charging_enabled)
led_charge_disabled.off();
else
led_charge_disabled.on();
Blynk.virtualWrite(V12, BASIC_INFO.current_mA / 1000.0);
}
void setup() {
// Serial.begin(115200);
Blynk.begin(auth, ssid, pass);
timer.setInterval(1000, sync_blynk);
BMS_UART.begin(9600);
tft.begin();
tft.setOrientation(SCREEN_ORIENTATION);
tft.setBackgroundColor(COLOR_BLACK);
}
void loop() {
Blynk.run();
timer.run();
delay(UPDATE_DELAY / 2);
#ifdef DEV_MODE
BASIC_INFO.battery_cells = 10;
BASIC_INFO.charging_enabled = (AVG_VOLTAGE < 4200);
BASIC_INFO.current_mA = (BASIC_INFO.charging_enabled ? 1250 : 0);
uint16_t voltages[] = {3824, 3825, 3830, 3835, 3810, 3826, 3830, 3825, 3840, 3835, 3835, 3835};
for (int i=0; i<BASIC_INFO.battery_cells; i++)
VOLTAGES[i] = voltages[i] + (millis() / 1000);
heartbeat(true);
#else
if (!fetch_basic_info()) {
heartbeat(false);
return;
}
// sanity check
if (BASIC_INFO.battery_cells < MIN_BATTERY_CELLS || BASIC_INFO.battery_cells > MAX_BATTERY_CELLS) {
heartbeat(false);
return;
}
if (!fetch_voltages(BASIC_INFO.battery_cells)) {
heartbeat(false);
return;
}
#endif
// TODO: constants, better naming
float max_diff = 20; // mV
uint8_t max_len = 48;
uint8_t x_mid = 128;
uint8_t line_height;
if (BASIC_INFO.battery_cells <= 10)
line_height = 20;
else if (BASIC_INFO.battery_cells == 11)
line_height = 18;
else
line_height = 17;
char fmt[20];
uint16_t sum_voltage = 0;
for (int i=0; i<BASIC_INFO.battery_cells; i++) {
sum_voltage += VOLTAGES[i];
}
AVG_VOLTAGE = sum_voltage / BASIC_INFO.battery_cells;
tft.setFont(Terminal6x8);
for (int i=0; i<BASIC_INFO.battery_cells; i++) {
// cell number
dtostrf((i+1), 2, 0, fmt);
if (fmt[0] == ' ')
fmt[0] = '0';
tft.drawText(0, i * line_height + 7, fmt, tft.setColor(128, 128, 128));
// voltage
uint16_t voltage = VOLTAGES[i];
dtostrf(voltage / 1000.0, 5, 3, fmt);
tft_util_draw_number(&tft, fmt, 21, i * line_height, COLOR_WHITE, COLOR_BLACK, 1, 3);
tft.drawText(65, i * line_height + 8, "V", COLOR_WHITE);
// voltage diff indicator
int voltage_diff = voltage - AVG_VOLTAGE;
int abs_voltage_diff = min(abs(voltage_diff), max_diff);
int indicator_len = round(1.0 * abs_voltage_diff / max_diff * max_len);
if (indicator_len != 0) {
int y1 = i * line_height;
int y2 = (i + 1) * line_height - 3;
if (voltage_diff > 0) {
tft.fillRectangle(x_mid - max_len, y1, x_mid - 1, y2, COLOR_BLACK);
tft.fillRectangle(x_mid + 1, y1, x_mid + indicator_len, y2, tft.setColor(0, 100, 0));
tft.fillRectangle(x_mid + indicator_len + 1, y1, x_mid + max_len, y2, COLOR_BLACK);
}
else {
tft.fillRectangle(x_mid - max_len, y1, x_mid - indicator_len - 1, y2, COLOR_BLACK);
tft.fillRectangle(x_mid - indicator_len, y1, x_mid - 1, y2, tft.setColor(100, 0, 0));
tft.fillRectangle(x_mid + 1, y1, x_mid + max_len, y2, COLOR_BLACK);
}
}
}
// voltage diff indicator middle line
tft.fillRectangle(x_mid, 0, x_mid, BASIC_INFO.battery_cells * line_height - 3, tft.setColor(32, 32, 32));
// bottom bar: Arithmetic Mean Voltage
tft.drawText(0, 204 + 7, "AM", tft.setColor(128, 128, 128));
dtostrf(AVG_VOLTAGE / 1000.0, 5, 3, fmt);
tft_util_draw_number(&tft, fmt, 21, 204, tft.setColor(55, 200, 255), COLOR_BLACK, 1, 3);
tft.drawText(65, 204 + 8, "V", COLOR_WHITE);
// bottom bar: Battery Percent
float soc = voltage_to_percent(AVG_VOLTAGE / 1000.0);
dtostrf(min(soc * 100.0, 99.9), 4, 1, fmt);
uint16_t color = tft.setColor(255 * (1 - soc), 255 * soc, 0);
tft_util_draw_number(&tft, fmt, 86, 204, color, COLOR_BLACK, 1, 3);
tft.drawText(120, 204 + 8, "%", COLOR_WHITE);
// bottom bar: Current
dtostrf(BASIC_INFO.current_mA / 1000.0, 4, 2, fmt);
uint16_t current_color = BASIC_INFO.charging_enabled ? tft.setColor(55, 200, 255) : tft.setColor(100, 0, 0);
tft_util_draw_number(&tft, fmt, 136, 204, current_color, COLOR_BLACK, 1, 3);
tft.drawText(170, 204 + 8, "A", COLOR_WHITE);
heartbeat(true);
}
/*
void write_line(String *text, int lineno, uint16_t color = COLOR_WHITE) {
const int max_line_length = 30;
char line_buffer[max_line_length + 1];
String s = String("") + *text;
while (s.length() < max_line_length)
s.concat(String(" "));
s.toCharArray(line_buffer, sizeof(line_buffer));
int y = lineno * 12 + 5;
tft.setFont(Terminal6x8);
tft.drawText(5, y, line_buffer, color);
}
uint8_t input[MAX_BATTERY_CELLS];
void test_fetch_voltages() {
BMS_UART.write(REQUEST_CELL_VOLTAGES_PACKET, sizeof(REQUEST_CELL_VOLTAGES_PACKET));
delay(100);
int bytes_read = 0;
while (BMS_UART.available()) {
input[bytes_read] = BMS_UART.read();
bytes_read++;
}
uint8_t line = 2;
String text = String("bytes read: ") + String(bytes_read);
write_line(&text, line++);
if (bytes_read == 27) {
for (int i=0; i<10; i++) {
uint16_t voltage = (input[4+i*2]<<8) | (input[4+i*2+1]);
text = (i < 10 ? "0" : "") + String(i) + String(": ") + String(voltage) + String(" mV");
write_line(&text, line++);
}
}
}
void test_fetch_hardware_version() {
BMS_UART.write(REQUEST_HARDWARE_VERSION_PACKET, sizeof(REQUEST_HARDWARE_VERSION_PACKET));
delay(100);
int bytes_read = 0;
while (BMS_UART.available()) {
input[bytes_read] = BMS_UART.read();
bytes_read++;
}
Serial.println(String("bytes read: ") + String(bytes_read));
for (int i=0; i<bytes_read; i++) {
Serial.print(char(input[i]));
Serial.println();
}
}
void test_fetch_basic_info() {
BMS_UART.write(REQUEST_BASIC_INFO_PACKET, sizeof(REQUEST_BASIC_INFO_PACKET));
delay(100);
int bytes_read = 0;
while (BMS_UART.available()) {
input[bytes_read] = BMS_UART.read();
bytes_read++;
}
uint8_t header_size = 4;
uint8_t data_size = input[3];
Serial.println(String("bytes read: ") + String(bytes_read));
Serial.println(String("data size: ") + String(data_size));
uint16_t total_voltage_mV = ((input[4]<<8) | input[5]) * 10;
Serial.println(String("total voltage: ") + String(total_voltage_mV) + String(" mV"));
uint16_t current_mA = ((input[6]<<8) | input[7]) * 10;
Serial.println(String("current: ") + String(current_mA) + String(" mA"));
uint16_t remaining_capacity_mAh = ((input[8]<<8) | input[9]) * 10;
Serial.println(String("remaining capacity: ") + String(remaining_capacity_mAh) + String(" mAh"));
uint16_t capacity_mAh = ((input[10]<<8) | input[11]) * 10;
Serial.println(String("capacity: ") + String(capacity_mAh) + String(" mAh"));
uint16_t charge_cycles = ((input[12]<<8) | input[13]) * 10;
Serial.println(String("charge cycles: ") + String(charge_cycles));
// 14+15 manufactured date?
// 16+17 balancing on/off for cells 1-16
// 18+19 balancing on/off for cells 17-32
uint16_t protection_status = (input[20]<<8) | input[21];
Serial.println(String("single cell overvoltage: ") + String(protection_status & (1<<0)));
Serial.println(String("single cell undervoltage: ") + String(protection_status & (1<<1)));
Serial.println(String("battery pack overvoltage: ") + String(protection_status & (1<<2)));
Serial.println(String("battery pack undervoltage: ") + String(protection_status & (1<<3)));
Serial.println(String("high temperature for charging: ") + String(protection_status & (1<<4)));
Serial.println(String("low temperature for charging: ") + String(protection_status & (1<<5)));
Serial.println(String("high temperature for discharging: ") + String(protection_status & (1<<6)));
Serial.println(String("low temperature for discharging: ") + String(protection_status & (1<<7)));
Serial.println(String("charge overcurrent: ") + String(protection_status & (1<<8)));
Serial.println(String("discharge overcurrent: ") + String(protection_status & (1<<9)));
Serial.println(String("short circuit: ") + String(protection_status & (1<<10)));
Serial.println(String("IC inspection error: ") + String(protection_status & (1<<11)));
Serial.println(String("MOSFET software lock: ") + String(protection_status & (1<<12)));
uint8_t firmware_version = input[22];
Serial.println(String("FW version: ") + String(firmware_version));
uint8_t SOC_percent = input[23];
Serial.println(String("SOC: ") + String(SOC_percent) + String("%"));
uint8_t mosfet_status = input[24];
Serial.println(String("charging enabled: ") + String(!!(mosfet_status & (1<<0))));
Serial.println(String("discharging enabled: ") + String(!!(mosfet_status & (1<<1))));
uint8_t battery_cells = input[25];
Serial.println(String("battery cells: ") + String(battery_cells));
uint8_t ntc_count = input[26];
Serial.println(String("NTC count: ") + String(ntc_count));
for (int ntc_i=0; ntc_i<ntc_count; ntc_i++) {
uint16_t temp_raw = (input[27+ntc_i*2]<<8) | input[27+ntc_i*2+1];
float temp_celsius = (temp_raw - 2731) / 10.0;
Serial.println(String("NTC ") + String(ntc_i) + String(": ") + String(temp_celsius) + String(" Celsius"));
}
Serial.println();
}
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment