👁 1029
ESP32 + RS485 Modbus 读 JK-PB 16S/280Ah BMS,GC9A01A 圆屏实时显示,BLE 扫描发现蓝牙 BMS,数据全入 HA
这套东西是太阳能系统里独立监控电池组的——跟德业一体机那路 Modbus 不冲突,一体机读的是逆变器侧数据,这路直接读 BMS 本体,16 电芯电压、SOC、温度、告警全拿到,还带个圆屏本地看。
硬件接线
| 功能 | ESP32 GPIO | 对端 |
|---|---|---|
| RS485 TX | GPIO25 | MAX485 RXD |
| RS485 RX | GPIO26 | MAX485 TXD |
| 圆屏 MOSI | GPIO23 | GC9A01A MOSI |
| 圆屏 CLK | GPIO18 | GC9A01A SCK |
| 圆屏 CS | GPIO16 | GC9A01A CS |
| 圆屏 DC | GPIO4 | GC9A01A DC |
| 圆屏 RST | GPIO17 | GC9A01A RST |
| CAN 预留 | GPIO12/13 | — |
圆屏是 SPI 不是 I2C。RS485 接 BMS 独立的监控/主机 RS485 口,A 对 A、B 对 B。不要把 ESP32 作为第二个 Modbus 主机接到逆变器正在用的总线,也不要接 BMS 的并联通信 485P 口。
BMS DIP 地址设为 1。
两套固件
固件 A:ESPHome(带圆屏)
ESPHome 2026.6.5,ESP-IDF 框架,4MB Flash。加密 HA API,原生 + Web 双 OTA。圆屏 1 秒刷新,8 秒轮换电芯电压页。
固件 B:Universal ESP-IDF(Web 配置 + MQTT)
独立 ESP-IDF 构建,不需要 ESPHome。Web 页面运行时配置(Wi-Fi、RS485 GPIO、BMS 地址/波特率/电芯数、MQTT),MQTT Home Assistant 自动发现。恢复 AP、Web OTA、出厂重置。不含圆屏驱动。
两套互不干扰,按需选。
ESPHome 配置
secrets.yaml(脱敏)
wifi_ssid: "你的WiFi"
wifi_password: "你的密码"
api_encryption_key: "你的HA加密key"
ota_password: "你的OTA密码"
web_username: "admin"
web_password: "你的Web密码"
fallback_ap_password: "你的AP密码"
jk-bms-ha.yaml 主配置
substitutions:
device_name: jk-bms
friendly_name: "极空 BMS"
device_description: "JK-PB 16S 280Ah BMS via RS485 Modbus"
bms_address: "1"
bms_update_interval: 5s
bms_tx_pin: GPIO25
bms_rx_pin: GPIO26
packages:
jk_pb_16s: !include packages/jk-pb-16s.yaml
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
comment: ${device_description}
project:
name: "godsun.jk-pb-bms-display"
version: "3.0.0"
esp32:
board: esp32dev
flash_size: 4MB
framework:
type: esp-idf
preferences:
flash_write_interval: 10min
logger:
level: DEBUG
baud_rate: 115200
logs:
uart_debug: DEBUG
modbus: DEBUG
modbus_controller: DEBUG
api:
id: api_server
reboot_timeout: 0s
encryption:
key: !secret api_encryption_key
ota:
- platform: esphome
password: !secret ota_password
- platform: web_server
web_server:
port: 80
version: 3
log: false
include_internal: false
auth:
username: !secret web_username
password: !secret web_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
min_auth_mode: WPA2
power_save_mode: none
reboot_timeout: 0s
ap:
ssid: "JK-BMS Fallback"
password: !secret fallback_ap_password
captive_portal:
safe_mode:
BLE 扫描(发现蓝牙 BMS)
esp32_ble_tracker:
scan_parameters:
active: true
on_ble_advertise:
then:
- lambda: |-
const std::string name = x.get_name();
if (name.rfind("JK-", 0) == 0 || name.rfind("JK_", 0) == 0) {
const std::string result = name + " | " + x.address_str();
ESP_LOGI("ble_adv", "JK-BMS found: %s", result.c_str());
id(jk_bms_ble_scan).publish_state(result);
}
text_sensor:
- platform: template
name: "JK BMS BLE 扫描结果"
id: jk_bms_ble_scan
entity_category: diagnostic
圆屏显示(GC9A01A 240×240)
spi:
clk_pin: GPIO18
mosi_pin: GPIO23
color:
- id: color_background
hex: "090D12"
- id: color_surface
hex: "17212B"
- id: color_text
hex: "F2F5F7"
- id: color_muted
hex: "91A0AC"
- id: color_green
hex: "35D07F"
- id: color_cyan
hex: "27B8D9"
- id: color_amber
hex: "F2B84B"
- id: color_red
hex: "F05A67"
font:
- file: "fonts/NotoSans.ttf"
id: font_tiny
size: 11
glyphs: " !%()+,-./0123456789:***|"
- file: "fonts/NotoSans.ttf"
id: font_small
size: 15
glyphs: " !%()+,-./0123456789:***|"
- file: "fonts/NotoSans.ttf"
id: font_medium
size: 22
glyphs: " !%()+,-./0123456789:***|"
- file: "fonts/NotoSans.ttf"
id: font_large
size: 43
glyphs: " -%0123456789"
display:
- platform: mipi_spi
id: bms_display
model: GC9A01A
cs_pin: GPIO16
dc_pin: GPIO4
reset_pin: GPIO17
data_rate: 20MHz
color_order: bgr
invert_colors: false
rotation: 0
update_interval: 1s
lambda: |-
it.fill(id(color_background));
it.circle(120, 120, 118, id(color_surface));
const bool bms_ok = id(bms_online).state;
const bool cell_page = bms_ok && ((millis() / 8000U) % 2U == 1U);
if (cell_page) {
// 电芯电压页(8秒轮换)
it.printf(120, 13, id(font_small), id(color_text), TextAlign::TOP_CENTER, "16S CELL VOLTAGE");
float cells[16] = {
id(cell_voltage_01).state, id(cell_voltage_02).state,
id(cell_voltage_03).state, id(cell_voltage_04).state,
id(cell_voltage_05).state, id(cell_voltage_06).state,
id(cell_voltage_07).state, id(cell_voltage_08).state,
id(cell_voltage_09).state, id(cell_voltage_10).state,
id(cell_voltage_11).state, id(cell_voltage_12).state,
id(cell_voltage_13).state, id(cell_voltage_14).state,
id(cell_voltage_15).state, id(cell_voltage_16).state
};
for (int i = 0; i < 16; i++) {
const int col = i % 4;
const int row = i / 4;
const int x = 43 + col * 51;
const int y = 45 + row * 39;
it.printf(x, y, id(font_tiny), id(color_muted), TextAlign::TOP_CENTER, "C%02d", i + 1);
if (isnan(cells[i])) {
it.printf(x, y + 14, id(font_small), id(color_red), TextAlign::TOP_CENTER, "--");
} else {
it.printf(x, y + 14, id(font_small), id(color_text), TextAlign::TOP_CENTER, "%.3f", cells[i]);
}
}
it.printf(120, 207, id(font_tiny), id(color_muted), TextAlign::TOP_CENTER,
"MIN %.3f MAX %.3f D %.3f",
id(min_cell_voltage).state, id(max_cell_voltage).state,
id(delta_cell_voltage).state);
return;
}
// 主页:SOC + 总电压 + 电流
it.printf(120, 13, id(font_small), id(color_text), TextAlign::TOP_CENTER, "JK-PB BMS 1");
if (!bms_ok) {
it.printf(120, 72, id(font_medium), id(color_red), TextAlign::TOP_CENTER, "NO BMS DATA");
it.printf(120, 110, id(font_small), id(color_muted), TextAlign::TOP_CENTER, "RS485 115200");
it.printf(120, 135, id(font_small), id(color_muted), TextAlign::TOP_CENTER, "ADDR 1");
it.printf(120, 202, id(font_tiny), id(color_muted), TextAlign::TOP_CENTER,
"%s | %s", id(esp_ip_address).state.c_str(),
id(api_server).is_connected() ? "HA OK" : "HA --");
return;
}
float soc = id(bms_soc).state;
if (isnan(soc)) soc = 0.0f;
if (soc < 0.0f) soc = 0.0f;
if (soc > 100.0f) soc = 100.0f;
const auto soc_color = soc > 50.0f ? id(color_green) : (soc > 20.0f ? id(color_amber) : id(color_red));
it.printf(120, 36, id(font_large), soc_color, TextAlign::TOP_CENTER, "%.0f%%", soc);
it.rectangle(48, 89, 144, 9, id(color_surface));
it.filled_rectangle(50, 91, (int) (140.0f * soc / 100.0f), 5, soc_color);
it.printf(65, 108, id(font_tiny), id(color_muted), TextAlign::TOP_CENTER, "PACK");
it.printf(175, 108, id(font_tiny), id(color_muted), TextAlign::TOP_CENTER, "CURRENT");
it.printf(65, 124, id(font_medium), id(color_cyan), TextAlign::TOP_CENTER, "%.2fV", id(total_voltage).state);
const auto current_color = id(bms_current).state >= 0.0f ? id(color_green) : id(color_amber);
it.printf(175, 124, id(font_medium), current_color, TextAlign::TOP_CENTER, "%.1fA", id(bms_current).state);
it.printf(120, 158, id(font_small), id(color_text), TextAlign::TOP_CENTER,
"CELL %.3f - %.3fV", id(min_cell_voltage).state, id(max_cell_voltage).state);
it.printf(120, 179, id(font_small),
id(delta_cell_voltage).state > 0.050f ? id(color_red) : id(color_muted),
TextAlign::TOP_CENTER, "DELTA %.3fV TEMP %.1fC",
id(delta_cell_voltage).state, id(battery_temperature_1).state);
const char *mode = id(charging).state ? "CHARGE" : (id(discharging).state ? "DISCHARGE" : "IDLE");
it.printf(120, 205, id(font_tiny), id(color_muted), TextAlign::TOP_CENTER,
"%s | %s | %s", mode,
id(api_server).is_connected() ? "HA OK" : "HA --",
id(bms_alarms_bitmask).state == 0 ? "NO ALARM" : "ALARM");
JK-PB Modbus 寄存器映射(16S)
UART/Modbus 配置
uart:
- id: bms_uart
tx_pin: ${bms_tx_pin}
rx_pin: ${bms_rx_pin}
baud_rate: 115200
data_bits: 8
stop_bits: 1
parity: NONE
rx_buffer_size: 384
modbus:
- id: bms_modbus
uart_id: bms_uart
send_wait_time: 200ms
modbus_controller:
- id: bms_controller
modbus_id: bms_modbus
address: ${bms_address}
setup_priority: -10
update_interval: ${bms_update_interval}
command_throttle: 50ms
max_cmd_retries: 2
电芯电压(0x1200-0x121E)
| 电芯 | 寄存器 | 类型 | 换算 |
|---|---|---|---|
| C01 | 0x1200 | U_WORD | ×0.001 V |
| C02 | 0x1202 | U_WORD | ×0.001 V |
| C03 | 0x1204 | U_WORD | ×0.001 V |
| C04 | 0x1206 | U_WORD | ×0.001 V |
| C05 | 0x1208 | U_WORD | ×0.001 V |
| C06 | 0x120A | U_WORD | ×0.001 V |
| C07 | 0x120C | U_WORD | ×0.001 V |
| C08 | 0x120E | U_WORD | ×0.001 V |
| C09 | 0x1210 | U_WORD | ×0.001 V |
| C10 | 0x1212 | U_WORD | ×0.001 V |
| C11 | 0x1214 | U_WORD | ×0.001 V |
| C12 | 0x1216 | U_WORD | ×0.001 V |
| C13 | 0x1218 | U_WORD | ×0.001 V |
| C14 | 0x121A | U_WORD | ×0.001 V |
| C15 | 0x121C | U_WORD | ×0.001 V |
| C16 | 0x121E | U_WORD | ×0.001 V |
统计与实时数据(0x1244-0x12C2)
| 数据 | 寄存器 | 类型 | 换算 | 单位 |
|---|---|---|---|---|
| 电芯平均电压 | 0x1244 | U_WORD | ×0.001 | V |
| 最大压差 | 0x1246 | U_WORD | ×0.001 | V |
| MOS 温度 | 0x128A | S_WORD | ×0.1 | °C |
| 总电压 | 0x1290 | U_DWORD | ×0.001 | V |
| 功率 | 0x1294 | S_DWORD | ×0.001 | W |
| 电流 | 0x1298 | S_DWORD | ×0.001 | A |
| 电池温度1 | 0x129C | S_WORD | ×0.1 | °C |
| 电池温度2 | 0x129E | S_WORD | ×0.1 | °C |
| 告警位掩码 | 0x12A0 | U_DWORD | — | bitmask |
| 均衡电流 | 0x12A4 | S_WORD | ×0.001 | A |
| SOC | 0x12A6 | U_WORD | 低8位 | % |
| 均衡状态 | 0x12A6 | U_WORD | 高8位 | — |
| 剩余容量 | 0x12A8 | U_DWORD | ×0.001 | Ah |
| 满充容量 | 0x12AC | U_DWORD | ×0.001 | Ah |
| 循环次数 | 0x12B0 | U_DWORD | — | 次 |
| 累计循环容量 | 0x12B4 | U_DWORD | ×0.001 | Ah |
| SOH | 0x12B8 | U_WORD | — | % |
| 累计运行 | 0x12BC | U_DWORD | — | s |
| 充电/放电状态 | 0x12C0 | U_WORD | 高8/低8 | — |
可写配置寄存器
| 参数 | 寄存器 | 范围 | 步长 | 单位 |
|---|---|---|---|---|
| 电芯欠压保护 | 0x1004 | 2.0-3.2 | 0.01 | V |
| 电芯欠压恢复 | 0x1008 | 2.1-3.4 | 0.01 | V |
| 电芯过压保护 | 0x100C | 3.4-3.75 | 0.01 | V |
| 电芯过压恢复 | 0x1010 | 3.3-3.65 | 0.01 | V |
| 均衡触发压差 | 0x1014 | 0.005-0.2 | 0.001 | V |
| 充电电流上限 | 0x102C | 1-300 | 1 | A |
| 放电电流上限 | 0x1038 | 1-300 | 1 | A |
| 电池额定容量 | 0x107C | 50-1000 | 1 | Ah |
| 开始均衡电压 | 0x1084 | 2.8-3.65 | 0.01 | V |
| 充电开关 | 0x1070 | 0/1 | — | On/Off |
| 放电开关 | 0x1074 | 0/1 | — | On/Off |
| 均衡开关 | 0x1078 | 0/1 | — | On/Off |
告警位定义(22位)
| 位 | 告警 |
|---|---|
| 0 | 线阻异常 |
| 1 | MOS过温 |
| 2 | 电芯数异常 |
| 3 | 电流传感器 |
| 4 | 电芯过压 |
| 5 | 电池组过压 |
| 6 | 充电过流 |
| 7 | 充电短路 |
| 8 | 充电过温 |
| 9 | 充电低温 |
| 10 | CPU通信 |
| 11 | 电芯欠压 |
| 12 | 电池组欠压 |
| 13 | 放电过流 |
| 14 | 放电短路 |
| 15 | 放电过温 |
| 16 | 充电MOS |
| 17 | 放电MOS |
| 18 | GPS断开 |
| 19 | 密码超时 |
| 20 | 放电使能失败 |
| 21 | 电池过温 |
Universal 固件(Web 配置 + MQTT)
独立 ESP-IDF 构建,不依赖 ESPHome。运行时 Web 配置,MQTT HA 自动发现。
首次刷写
esptool --chip esp32 --port /dev/ttyUSB0 --baud 460800 write-flash 0x0
dist/jk-bms-universal-1.0.0.factory.bin
配置热点
| 项目 | 默认值 |
|---|---|
| 热点 | JK-BMS-Setup-XXXX |
| 密码 | jk-bms-setup |
| 地址 | http://192.168.4.1/ |
| 用户 | admin |
| 初始密码 | jk-bms-admin |
Web 配置项
- Wi-Fi SSID/密码、DHCP 或静态 IP
- RS485 TX/RX/DE GPIO
- BMS Modbus 地址、波特率、轮询周期
- 电芯档位:自动识别 1-32S / 固定 8S/16S/24S/32S / 自定义
- 标称容量
- MQTT 服务器/端口/账号/主题/HA 发现前缀
MQTT 发布
jk-bms/state
jk-bms/availability
homeassistant/{sensor|binary_sensor}/jk-bms/<entity>/config
GPIO 限制
- GPIO0 保留 BOOT
- GPIO1/3 保留调试串口
- GPIO6-11 保留板载 Flash
- GPIO2/5/12/15 启动绑带引脚,不能做 RS485
- GPIO34-39 只能做 RX
圆屏实拍
30秒实拍视频,主页和电芯页轮换显示:
与太阳能系统的关系
这套独立读 BMS,跟光伏储能接入 HA那篇文章里的德业一体机 Modbus 读取是两条独立数据通道:
- 一体机路:ESP32-S3 + MAX485 读德业 12K 一体机,电池数据通过 CAN→一体机→Modbus 间接获取
- BMS 直读路(本篇):ESP32 + RS485 直读 JK-PB BMS 本体,16 电芯电压/压差/均衡状态这些一体机拿不到的细节数据
两路数据在 HA 里并存,互为校验。
参考
- syssi/esphome-jk-bms — JK-PB Modbus 协议基础
- ESPHome GC9A01A display — 圆屏驱动
💖 打赏支持
如果这篇文章帮到了你,欢迎打赏支持
微信扫一扫,赞赏支持
发表回复