ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32直流充电桩开发:CAN通信与状态机设计实战

STM32直流充电桩开发:CAN通信与状态机设计实战 简介基于STM32的直流充电桩完整工程程序面向嵌入式开发工程师、新能源汽车充电设施研发人员及高校相关专业学生用于解决直流充电桩主控板程序架构设计、充电流程控制、通信协议对接与安全保护逻辑实现等问题尤其适合已有一定STM32基础、希望快速入手充电桩项目的开发者参考。资源打包为zip格式共192个文件其中C源码与头文件共173个覆盖主要功能模块另含启动文件、编译列表、Eclipse/CMake工程配置、J-Link调试器设置文件与批处理脚本方便在不同开发环境下编译调试整包仅609KB结构清晰。目前已有224人学习下载。程序按模块化方式组织包含通信、充电控制、用户接口、安全监测和数据记录等关键部分可帮助读者理清充电桩软件框架配套的工程配置和调试脚本能减少环境搭建成本源码可直接阅读或移植对设计固件、评估方案和二次开发都有实用价值。1. 直流充电桩程序在STM32上跑起来先想清这三件事直流充电桩的控制程序和常见的物联网板子不一样核心不在于“功能多”而在于“每一步都带着安全约束”。程序要同时处理BMS的CAN请求、功率模块的电压电流闭环、绝缘检测和急停信号的瞬时响应。STM32在这类设备里的位置很明确既有CAN控制器、多路ADC和定时器又不需要上Linux那样的复杂系统裸机加状态机就能把整条充电流程跑稳。很多基于STM32的毕业设计也会选这个方向但大多停在CAN收发演示上真正把充电流程串起来需要解决的是资源分配、通信协议解析和状态迁移三件事。这篇文章按这三个层次展开最后落到调试手段上。2. 先把外设分好STM32硬件资源分配与最小电路2.1 主控选型F103能不能扛住直流充电桩直流充电桩的主控并不需要跑复杂的嵌入式系统裸机加状态机就够了。常见做法是选择STM32F103RCT6或者F407VET6这两颗芯片在工业控制项目里都很常见。F103拥有72MHz主频、单路CAN、三个12位ADC用于单枪直流桩已经基本够用。F407主频升到168MHzCAN通道多出一路还带了硬件浮点单元。如果充电桩里还要同时跑计量芯片的SPI读取、显示屏的串口刷新或者需要双枪控制F103的中断压力会明显变大这时候可以直接选F407。对比项STM32F103RCT6STM32F407VET6主频72 MHz168 MHzCAN 通道1 个2 个ADC3 个 12 位3 个 12 位最高 2.4 Msps硬件浮点无有典型场景单枪直流桩、功能固定双枪或多功率模块控制选型不是跑分越高越好。充电桩的CAN报文周期通常在10ms到100ms之间大部分协议解析和状态判断在这两个频率下运行F103的性能完全能覆盖。只有当功率控制算法涉及浮点计算比如要根据温度做电流降额曲线插值F407的硬件FPU才会体现出明显优势。另一个实际考量是备货周期和团队对开发环境的熟悉度用哪颗芯片写得顺手比纸面参数更能影响项目进度。2.2 外设资源地图ADC、定时器、CAN怎么拆直流充电桩要采集的信号包括直流母线电压、充电电流、三相输入电压、电表脉冲以及通过串口上报的绝缘检测数据。落到程序层面要先在代码头部写清外设分配清单这样后续写驱动时不会产生引脚复用冲突的问题。/* 外设分配清单 * CAN1: BMS 通信250 kbps * USART1: 绝缘检测仪9600 8N1 * USART2: 电表模块Modbus RTU 协议 * ADC1_IN0: 直流母线电压分压后 * ADC1_IN1: 充电电流霍尔传感器输出 * ADC1_IN2: 输出接触器后端电压 * TIM2_CH1: 充电枪 CC 信号 PWM 输出 * TIM3: 1 kHz 周期中断控制环路节拍 */清单里最容易踩坑的是引脚复用。F103的PA0同时是ADC1_IN0和TIM2_CH1的复用引脚如果既要用它采样电压又要输出CC信号的PWM硬件上就冲突了。所以这份清单应该在画板子之前就定下来等PCB回来再改程序只是把问题换了个位置。每条分配还要考虑外设时钟树比如USART2和CAN1都在APB1总线上调整APB1分频会影响双方的波特率清晰写下来能减少联调时的困惑。2.3 最小硬件连接与接触器回检程序启动前要确认的最小电路包括主控供电、CAN收发器、采样分压网络、接触器驱动与回检。其中接触器回检最容易出问题。程序不能只在下发闭合命令后读一次回检电平而要在命令发出后持续监测命令状态与回检结果不一致超过200ms就要判定为接触器故障并进入急停流程。#define CONTACTOR_FB_PIN GPIO_PIN_5 #define CONTACTOR_FB_PORT GPIOB uint8_t contactor_check(void) { /* 返回 1 表示接触器状态与命令一致 */ uint8_t level HAL_GPIO_ReadPin(CONTACTOR_FB_PORT, CONTACTOR_FB_PIN); return (level (uint8_t)g_contactor_cmd) ? 1 : 0; }这段代码的作用是把回检引脚电平与当前命令目标做比较。调用它的是周期任务而不是中断外的单次查询。单次读取可能会碰到机械触点抖动或电平毛刺连续多次校验不通过再置故障才会让系统的误判率降下来。回检错误还应该记录触发的时刻和当时所在的状态机节点方便后期定位是接触器烧蚀还是驱动电路失效。3. CAN通信是命脉BMS握手与协议栈实现3.1 CAN底层初始化250 kbps如何配置出来充电桩与BMS之间的CAN通信常见波特率是250 kbps。以F103的APB1外设时钟36 MHz计算预分频器设为9时间片组合选择BS113、BS22、SJW1得到的时间量子总数是(1321)×914436 MHz除以144正好等于250 kHz。hcan1.Instance CAN1; hcan1.Init.Prescaler 9; hcan1.Init.Mode CAN_MODE_NORMAL; hcan1.Init.SyncJumpWidth CAN_SJW_1TQ; hcan1.Init.TimeSeg1 CAN_BS1_13TQ; hcan1.Init.TimeSeg2 CAN_BS2_2TQ; hcan1.Init.TimeTriggeredMode DISABLE; hcan1.Init.AutoBusOff ENABLE; hcan1.Init.AutoWakeUp ENABLE; hcan1.Init.AutoRetransmission ENABLE; if (HAL_CAN_Start(hcan1) ! HAL_OK) { /* 进入错误处理关闭功率输出并上报 */ system_fault_report(FAULT_CAN_INIT); }这里有几个参数值得展开。AutoBusOff打开后总线因长时间显性位而进入bus-off状态时硬件会自动恢复并重新参与总线通信省去在应用层实现恢复流程的麻烦。AutoRetransmission开启后发送失败的帧会在硬件层面持续重发这在总线繁忙时能提高交付率但要注意如果某类报文本身要求时效性比如实时状态上报重发反而会挤占新报文的时间片。所以项目里通常只对关键握手报文开启自动重传实时状态报文在软件层判断超时后直接丢弃。接收侧使用中断和FIFO。中断回调里只把报文内容拷贝到环形缓冲并置一个新帧标志真正解析放在主循环里。这样做的好处是中断执行时间短不会干扰ADC采样和PWM更新节拍。如果中断里做完整解析一旦报文结构变了中断时间会被拉长对控制环路的稳定性影响很大。3.2 协议解析层从CAN帧到充电参数BMS下发的报文内容包含电池总电压、总电流、SOC、单体最高最低电压等。程序里把每一帧解析结果存到结构体并保留原始数据便于日志记录。typedef struct { uint16_t total_voltage_x10; /* 0.1V 单位 */ uint16_t total_current_x10; /* 0.1A 单位 */ uint8_t soc; /* 1% 单位 */ uint16_t max_cell_voltage_x1000; /* 1mV 单位 */ uint16_t min_cell_voltage_x1000; } bms_data_t; static bms_data_t g_bms; void parse_bsp(uint8_t *data, uint8_t len) { if (len 6) { return; } /* 高字节在前低位在后与 CAN 报文默认大端格式一致 */ g_bms.total_voltage_x10 (uint16_t)((data[0] 8) | data[1]); g_bms.total_current_x10 (uint16_t)((data[2] 8) | data[3]); g_bms.soc data[4]; }解析时最重要的一点是统一数据单位。CAN报文里电压用0.1V、电流用0.1A、单体电压用1mV编码如果直接拿原始整数去和阈值比较会出现量级错误。程序里全程保持整型运算只有在显示或上传时才转成浮点字符串。因为控制环路的比较操作非常频繁浮点运算在F103上会占用较多CPU周期而且浮点比较容易引入不可预期的舍入整数缩放在这里更可控。3.3 超时与重试BMS不回复的时候怎么退出BMS停止发送报文的情况并不罕见原因可能是BMS重启、CAN线松动或者充电机高压连接器进入保护。程序必须在秒级时间内发现通信中断并停止功率输出。常见做法是维护一个报文时间戳和看门狗计数器。/* 主循环或 1 kHz 中断里周期调用 */ void can_timeout_check(void) { uint32_t now HAL_GetTick(); if ((now - g_last_bms_rx_tick) BMS_RX_TIMEOUT_MS) { /* 超过 5 秒未收到 BMS 报文进入故障状态 */ fault_set(FAULT_BMS_TIMEOUT); chg_state_force(CHG_FAULT); } }这里BMS_RX_TIMEOUT_MS取5000。设置超时时间时要考虑BMS本身的发送周期如果BMS发送周期是100ms5秒意味着连续50帧丢失这个判定相对保守。把手握阶段的上报周期和实际充电阶段的上报周期分别看待更合理握手时BMS可能只在收到请求后才回复所以安全超时应覆盖完整的请求-响应间隔不能简单复用稳定充电阶段的超时值。重试逻辑放在发起请求的一方。如果充电桩发出的握手请求没有收到响应通常要重发3到5次重试次数达到上限后才判断通信失败。注意每次重试之间要留有间隔常见做法是延时30ms到100ms不等。有些程序把所有报文都设置重发反而会把总线打满要让不同的报文走不同的重传策略。4. 充电主流程状态机设计与功率控制4.1 把充电流程拆成状态机直流充电桩的运行流程按标准大体分为物理连接后握手、参数配置、充电中、充满、故障五个阶段。用状态机实现的好处是每个阶段对外设的操作不同拆开以后不会出现一个主循环里堆满各种标志位的混乱状态。typedef enum { CHG_IDLE 0, CHG_HANDSHAKE, CHG_PARAM_CFG, CHG_CHARGING, CHG_DONE, CHG_FAULT } chg_state_t; void chg_state_machine_run(void) { static chg_state_t state CHG_IDLE; switch (state) { case CHG_IDLE: if (g_can_connect_flag g_gun_connected) { state CHG_HANDSHAKE; } break; case CHG_HANDSHAKE: if (g_handshake_done) { state CHG_PARAM_CFG; } else if (fault_has(FAULT_BMS_TIMEOUT)) { state CHG_FAULT; } break; case CHG_PARAM_CFG: /* 收到完整的 BMS 参数组后进入充电 */ if (g_param_ready fault_none()) { state CHG_CHARGING; } break; case CHG_CHARGING: if (g_charge_finish_flag) state CHG_DONE; if (fault_any()) state CHG_FAULT; break; case CHG_DONE: /* 等待接触器断开确认再回到空闲 */ if (contactor_opened()) state CHG_IDLE; break; case CHG_FAULT: if (fault_cleared() contactor_opened()) state CHG_IDLE; break; default: state CHG_FAULT; break; } }状态机的核心是“一个时刻只允许一个状态”。进入下一个状态前要检查前置条件比如进入充电状态前必须满足三件事BMS参数已经收到本机故障表为空接触器已经吸合并通过回检。任何一个条件不满足都不能进入充电。另外故障状态不能随意自动化恢复通信中断这类瞬时故障可以自动恢复但急停、绝缘故障需要人为确认并重新握手后再进入充电。提示从 CHG_DONE 回到 CHG_IDLE 之前必须先确认接触器回检电平已经变为断开否则下一次充电流程会在错误的初始状态下启动。4.2 功率控制目标电压电流怎么取充电过程中BMS会周期性下发请求电压和请求电流充电桩实际输出的目标值要在BMS请求值和设备自身能力之间取较小值。这段逻辑代码很简单难的是边界条件的处理。uint16_t calc_target_voltage(uint16_t bms_req, uint16_t station_max) { /* 单位 0.1V取较小值作为输出目标 */ uint16_t target bms_req; if (target station_max) { target station_max; } if (target station_min) { target station_min; } return target; }参数说明station_max由功率模块额定电压和充电枪线缆载流能力共同决定station_min是保证输出电压不会低于BMS预期低限的下限保护。目标值不能一次性从当前输出跳到BMS请求值要在1 kHz的控制节拍里逐步爬升。爬坡速率通常设置为每100ms变化额定电压的0.5%到1%这样功率模块内部的环路来得及响应不会因为给定跳变造成输出过冲。充电末段BMS请求电流开始下降下降过程同样需要爬坡不能直接切断否则容易在接触器两端产生拉弧。4.3 异常处理优先级急停、绝缘、温度的处置差异不同异常对充电过程的影响不同程序里要区分“立即停机”和“降额运行”。急停信号触发后必须在几个毫秒内断开接触器无论当前处于哪个状态绝缘检测仪报警不只是停机还要在屏幕上给出明确故障码温度过高则不一定停机可以优先降电流让系统自动降温。故障码登记表是运维排查的核心。故障码故障描述触发条件恢复方式0x01急停按下急停开关断开手动复位0x03BMS通信超时5s 未收到BMS报文自动恢复0x07绝缘故障绝缘检测仪上报人工确认0x0B模块过温温度采样超限自动降额故障码的登记策略要注意同一个故障在同一个充电过程中不能反复触发和清除否则后台记录里会出现抖动。常见做法是故障置位后进入锁定状态只有满足复位条件才允许清除复位条件由人工触发或者下电重启实现。5. 调试三板斧时序验证、故障注入、日志回放5.1 用示波器验证CAN时序和接触器回检实测第一件事是抓CAN波形。把示波器接到CAN收发器芯片的CAN_H和CAN_L之间用“触发于CAN唤醒帧”的模式抓一发一收两帧对照波特率和报文ID确认配置正确。这一步能快速排除“波特率不匹配”和“收发器方向接反”两类问题。第二件事是抓接触器命令引脚和回检引脚的时间差确认程序里的200ms故障判定阈值与硬件实际动作时间是否匹配。如果接触器动作本身要300ms200ms的判定就会误报需要按实测值调整。5.2 用CAN盒模拟BMS做故障注入把BMS从链路上摘掉用CAN盒或另一块开发板按充电流程模拟BMS发包。至少覆盖三种异常发到一半停止请求电压瞬时跳变到上限报文字节长度错误。每次注入后观察充电桩状态机的迁移路径和故障码确认程序没有在某一步陷入死循环。这套故障注入用例可以固化成脚本每轮代码改动后在开发板上回放一遍属于性价比最高的回归测试手段。5.3 日志回放把现场还原到PC上程序里维护环形缓冲把CAN原始帧、状态迁移、故障码连同毫秒级时间戳一起写入FLASH。建议缓冲深度设置256条按10条每秒的典型记录频率能覆盖约25秒的故障前窗口。导出后按时间戳过滤对应CAN ID把数据输入到Wireshark或自制脚本里重放能够还原整个事件链。实际项目里现场售后反馈“充电中断”时我们首先看的就是故障码和时间戳顺序这比让现场人员回忆步骤要可靠得多。本文还有配套的精品资源点击获取
返回列表