尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32在直流充电桩控制中的应用:从ADC采样到固件升级

STM32在直流充电桩控制中的应用:从ADC采样到固件升级 简介面向新能源汽车直流充电桩开发的嵌入式工程资料适合具备一定STM32基础、希望快速上手充电桩控制与通信程序设计的开发者可用于毕业设计、竞赛或产品预研阶段。资源围绕STM32F10x平台实现充电控制、通信交互、用户界面、安全监测与数据记录等核心模块源码层次清晰便于实际项目移植与二次开发。压缩包共包含192个文件以91个.h头文件、82个.c源文件和9个.s启动文件为主另有uvprojx、cproject等工程配置文件以及JLinkSettings.ini调试设置、BAT批处理脚本整体体积约609KB结构紧凑可直接导入MDK或Eclipse环境查看。内容预览可见stm32f10x系列驱动、uip协议栈、lcd_ui界面等关键代码覆盖从底层驱动到上层交互的完整程序链路省去从零搭建框架的繁琐过程。目前已有224人学习下载适合正在做充电桩相关项目的开发者作为程序框架与排错参考。1. 直流充电桩为什么选 STM32F10x 做主控一台直流充电桩摆在现场主控要同时干的事比大多数人想的杂和 BMS 握手、看电压电流、控输出功率、走以太网上传数据、驱动 LCD 和按键、还要处理急停和绝缘检测。高端的可以用 Cortex-A 加 Linux但大量模块化直流桩尤其功率模块外置的机型一颗 STM32F103 级别的 MCU 就能扛下来。ARM Cortex-M3 内核、72 MHz 主频跑 uIP 协议栈和 Modbus 轮询足够标准外设库的函数级封装让 ADC、TIM、Flash 这些外设的开发门槛压得很低出了问题翻代码也比翻 HAL 的抽象层快。项目压缩包里能看到 stm32f10x_rcc.c、stm32f10x_adc.c、stm32f10x_tim.c 这些标准库文件还有 stm32_eth.c、uip.c、lcd_ui.c、fw_update.bin说明这套程序走的是芯片选型务实、软件分层清晰的路线。适合的对象是做桩控、充电模块控制器、或者想拿 STM32 入门电力电子控制的人——你不需要懂 Linux 设备树但能把中断、DMA、Flash 擦写这些底层玩明白。2. 充电控制核心ADC 采样链路与定时器 PWM 输出2.1 电压电流采样为什么走 ADC DMA而不是定时器里轮询直流充电桩输出电压范围常见 200 V 到 1000 V电流最大能到 250 A这些量不可能直接进 MCU都得先经过霍尔传感器或隔离运放变成 03.3 V 的模拟信号再进 STM32 的 ADC。F103 的 ADC 是 12 位采样率最高 1 MHz对充电控制这种毫秒级响应场景绰绰有余。很多初学者第一个坑是把 ADC 采集放在主循环里用 delay 轮询。问题在于充电过程中电压电流波形有纹波特别是功率模块开关频率在 20 kHz 到 100 kHz 时单次采样结果抖动非常明显而且主循环里一旦跑 LCD 刷新或 uIP 协议栈采集周期就变得不稳定PID 调节器拿到的数据时间基准是乱的。常见做法是用 ADC1 的注入组或规则组多通道扫描配合 DMA 循环模式让硬件自动把电压、电流、温度三路数据搬进内存缓冲区主循环只去读最新值。代码骨架如下void ADC_DMA_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; DMA_InitTypeDef DMA_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // PA0: 充电电压采样, PA1: 充电电流采样 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)(ADC1-DR); DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)adc_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize 2; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_Init(DMA1_Channel1, DMA_InitStructure); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode ENABLE; ADC_InitStructure.ADC_ContinuousConvMode ENABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 2; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_239Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); DMA_Cmd(DMA1_Channel1, ENABLE); }DMA 缓冲区adc_buf[0]始终保存最近的电压原始值adc_buf[1]是电流原始值不需要中断干预。选ADC_SampleTime_239Cycles5是为了降低信号源内阻带来的采样误差充电桩的采样信号虽然经过运放跟随但长线传输引入的阻抗不可忽略采样时间拉长能让内部采样电容充分充电。注意DMA_BufferSize必须和通道数一致这里两个通道设 2多一个少一个都会导致缓冲区错位。2.2 定时器 PWM 与充电策略怎么配合直流充电桩控制功率模块一般是 MCU 输出 PWM 给移相全桥或 LLC 变换器的驱动芯片通过调节占空比或移相角来改变输出电压电流。F103 的 TIM1 是高级定时器带互补输出和死区插入可以直接驱动半桥电路这也是项目里出现 stm32f10x_tim.c 的原因。充电策略可以拆成三个阶段恒流阶段、恒压阶段、涓流阶段。BMS 在握手阶段会告诉桩端需求的充电曲线主控根据当前电压/电流反馈实时调整 PWM 目标值。阶段判定条件控制目标功率模块状态恒流电池电压低于恒压阈值输出电流恒定在设定值CC 模式恒压电池电压达到阈值输出电压恒定电流自然下降CV 模式涓流电流下降到 0.1 C 以下维持最小充电电流小功率输出TIM1 产生中心对齐 PWM频率 20 kHz波形质量适合 LLC死区时间按功率管爬坡时间算一般设 100 ns 到 300 ns。初始化代码里的关键参数是 PSC 和 ARRvoid TIM1_PWM_Init(uint16_t freq_khz, uint16_t deadtime_ns) { TIM_TimeBaseInitTypeDef TIM_BaseInitStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_BDTRInitTypeDef TIM_BDTRInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); // 72 MHz / (PSC 1) / (ARR 1) PWM 频率 TIM_BaseInitStructure.TIM_Prescaler 0; TIM_BaseInitStructure.TIM_CounterMode TIM_CounterMode_CenterAligned2; TIM_BaseInitStructure.TIM_Period 3600 - 1; // 72MHz / 3600 20kHz TIM_BaseInitStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM1, TIM_BaseInitStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OutputNState TIM_OutputNState_Enable; TIM_OCInitStructure.TIM_Pulse 1800; // 初始占空比 50% TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OCNPolarity TIM_OCNPolarity_High; TIM_OCInitStructure.TIM_OCIdleState TIM_OCIdleState_Set; TIM_OCInitStructure.TIM_OCNIdleState TIM_OCNIdleState_Reset; TIM_OC1Init(TIM1, TIM_OCInitStructure); TIM_BDTRInitStructure.TIM_OSSRState TIM_OSSRState_Enable; TIM_BDTRInitStructure.TIM_OSSIState TIM_OSSIState_Disable; TIM_BDTRInitStructure.TIM_LOCKLevel TIM_LOCKLevel_OFF; TIM_BDTRInitStructure.TIM_DeadTime deadtime_ns; // 按寄存器换算 TIM_BDTRInitStructure.TIM_Break TIM_Break_Enable; TIM_BDTRInitStructure.TIM_BreakPolarity TIM_BreakPolarity_High; TIM_BDTRInitStructure.TIM_AutomaticOutput TIM_AutomaticOutput_Enable; TIM_BDTRConfig(TIM1, TIM_BDTRInitStructure); TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); }TIM_Prescaler 0意味着定时器时钟就是 72 MHzTIM_Period 3600 - 1配合中心对齐模式实际 PWM 频率是 72 MHz / 3600 20 kHz。占空比通过改变TIM_Pulse实现范围 0 到 3599。调试时用示波器看 CH1 和 CH1N 之间的死区时间如果发现上下管直通炸机先查TIM_BDTRInitStructure.TIM_DeadTime的寄存器换算值——它不是直接填纳秒数不同芯片版本换算关系有差异F103 的 DT 寄存器每个步进约等于 tCK_INT 的倍数仔细对照参考手册的表格。2.3 闭环参数在串口上怎么调电流环和电压环的闭环F103 上跑数字 PID 完全够。实际工程项目里更常见的是 PI 控制器D 项对采样噪声太敏感充电桩这种大感性负载场景微分项放大了反而容易震荡。建议在串口调试助手输出调节过程中的关键变量目标电压、实际电压、PWM 占空比、PI 输出值。把这个调试通道做成独立中断不占用主循环用 DMA 发送避免和 uIP 协议栈抢 CPU 时间。参数整定从纯 P 开始P 值逐渐增大到系统开始震荡然后取临界值的一半再加 I 消除稳态误差。这是齐格勒-尼科尔斯经验的简化流程对充电桩这种响应速度要求不极端的对象足够。3. 通信链路uIP 协议栈与 STM32 以太网实现3.1 为什么 uIP 比 lwIP 更适合 F103直流充电桩需要和后台监控系统通信上报充电状态、计费信息、故障代码同时接收远程启停指令。以太网是主流选择而 F103 资源有限——SRAM 只有 20 KB 到 64 KB跑完整 lwIP 虽然可行但留给协议栈的缓冲就会挤压控制任务的生存空间。uIP 是专为 8/16 位 MCU 设计的轻量级 TCP/IP 协议栈代码量小、内存占用低配合 stm32_eth.c 驱动文件正好塞进 F103 的资源预算。压缩包里的 stm32_eth.c 和 uip.c 是一对前者负责 STM32 内部以太网 MAC 的寄存器操作后者实现 TCP/IP 协议逻辑。两者的连接点在 uip_input 和 uip_periodic 两个函数上前者处理网卡收到的报文后者驱动 TCP 定时器。3.2 以太网驱动与 uIP 轮询的接线方式初始化流程分三层MAC 初始化、PHY 复位、uIP 配置。F103 系列有的型号带内部 MAC外部接 PHY 芯片常见是 DP83848 或 LAN8720不带内部 MAC 的型号会用 SPI 接口外挂 ENC28J60。项目里既然直接叫 stm32_eth.c大概率走的是内部 MAC 加外部 PHY 路线。MAC 初始化核心是设置 MAC 地址、配置 RMII 接口、开启接收中断。PHY 部分要读取 ID 寄存器确认芯片在位然后配置自动协商。uIP 侧主要做三件事设置 IP 和网关、注册 TCP 端口监听、在 while(1) 循环里周期调用 uip_periodic。void uIP_Server_Init(uint8_t *ip_addr, uint16_t port) { uip_ipaddr_t ipaddr; uip_init(); uip_ipaddr(ipaddr, ip_addr[0], ip_addr[1], ip_addr[2], ip_addr[3]); uip_sethostaddr(ipaddr); // 监听充电桩上位机端口例如 502 (Modbus TCP) uip_listen(HTONS(port)); } void uIP_Poll_Task(void) { uip_len stm32_eth_read_packet(); // 从 MAC 接收 FIFO 读一包 if (uip_len 0) { uip_input(); // 处理收到的包 if (uip_len 0) { stm32_eth_send_packet(); // 有响应数据则发出 } } for (int i 0; i UIP_CONNS; i) { uip_periodic(i); // 驱动每个 TCP 连接 if (uip_len 0) { stm32_eth_send_packet(); } } }uIP 的设计哲学是事件驱动收到报文后 uip_input 里会自动调用应用层回调响应数据存放在 uip_appdata 缓冲区uip_len标记数据长度。如果回调里没有设置 uip_len协议栈就认为应用层没有响应不会发出空包。这里容易踩的坑是uip_periodic的调用频率——TCP 超时重传依赖这个周期性驱动常见做法是放在 100 ms 的定时器中断标志里触发延迟太长会导致 TCP 建连超时。3.3 充电桩通信协议怎么定充电桩和后台的通信国标里对直流充电桩有 GB/T 27930 的 CAN 通信要求但以太网层面的协议并没有统一强制格式很多厂家用 Modbus TCP 或私有 TCP 协议。Modbus TCP 的好处是调试工具多、现成上位机组件多wireshark 抓包解析直观。功能码寄存器地址含义读写属性0x030x0000输出电压值(0.1V)只读0x030x0001输出电流值(0.1A)只读0x030x0002充电状态字只读0x060x0010启动充电命令读写0x060x0011停机命令读写0x100x0020充电参数下发(电压/电流)读写Modbus 报文解析放在 uIP 回调里做收到完整一帧后校验 CRC16再分发到功能码处理函数。如果项目文件里有 freemodbus 移植痕迹那么驱动层可以直接复用只把串口收发换成 TCP 收发。这里注意 Modbus TCP 的报文和 RTU 不同MPAB 头占 6 个字节CRC 校验去掉了解析时不要套用 RTU 的帧格式。4. 从 autotools 到 J-Link构建环境与在线调试4.1 STM32 标准库工程的构建方式这套程序用的是 STM32F10x 标准外设库工程组织方式和现在主流的 STM32CubeMX 生成的 HAL 库工程有明显区别。标准库工程更直接每个外设对应一个 .c/.h 文件启动文件 startup_stm32f10x_cl.lst 里列出向量表编译时直接包含 stm32f10x_rcc.c、stm32f10x_flash.c 这些源文件即可不需要额外生成初始化代码。构建系统方面项目里有 .autotools 目录和 Target 1.BAT。autotools 是 Linux 系的构建工具链用来生成 Makefile但在 Windows 环境下开发 STM32更多人用 Keil MDK 或 IAR。Target 1.BAT 是批处理脚本常见做法是设置编译器路径、调用 make 或者直接启动 Keil 的命令行编译。一个简单的构建脚本长这样echo off set UV4C:\Keil_v5\UV4\UV4.exe set PROJECTdc_charger.uvprojx set TARGET输出 Hex/Bin 文件路径 echo STM32 DC Charger Build %UV4% -b %PROJECT% -o build_log.txt if errorlevel 1 ( echo Build FAILED type build_log.txt exit /b 1 ) echo Build OK fromelf --bin --output %TARGET% .\Objects\dc_charger.axfKeil 的 UV4 命令行编译参数-b是 build 当前工程-r是 rebuild。如果机器上装的是 Keil C51 兼容版本要先用 Pack Installer 安装 STM32F1 系列的芯片包否则打开工程会提示找不到设备。这个步骤卡住很多人JLinkSettings.ini 文件存在说明调试器已经配置过但换电脑后 SW Device 里如果显示 No STM32 target found先别急着怀疑板子可能是调试器固件和芯片的 debug authentication 设置不匹配或者目标板上的 SWDIO/SWCLK 引脚被程序复用成了 GPIO。4.2 把 SWD 调试接口锁死的恢复办法F103 的 PA13/PA14 默认是 SWDIO/SWCLK但很多工程师在初始化代码里把这两个脚重映射成了普通 GPIO导致第一次下载程序后调试器就连不上了。这是项目里 stm32f10x_rcc.c 中 GPIO 配置的常见隐患。引脚SWD 功能JTAG 功能常见误用PA13SWDIOJTMS按键输入PA14SWCLKJTCKLED 输出PA15—JTDI电机方向控制PB3—JTDOPWM 输出PB4—JTRST蜂鸣器调试器连不上的时候用 J-Flash 或 STM32 ST-LINK Utility 执行 connect 时的 reset 选项选择 Hardware Reset在芯片上电瞬间按住复位键再点连接。如果这样也不行需要把 BOOT0 拉高进入 ISP 模式用串口下载一段擦除程序把 Flash 清了。具体做法是把 BOOT0 跳线跳到 1BOOT1 跳到 0复位后芯片从系统存储器启动此时 SWD 引脚释放再用工具连接即可。这段经验对刚接触 STM32 的人来说是必须过的坎对老手而言则是习惯性的预防检查点。更稳妥的做法是在工程初始化里显式关闭 JTAG只保留 SWDvoid GPIO_Config_SWD(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); // 关闭 JTAG 复用功能释放 PA15/PB3/PB4 GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); // PA13/PA14 保持 SWD 功能不要做 GPIO 初始化 GPIO_InitStructure.GPIO_Pin GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); }GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE)只关闭 JTAGSWD 不受影响。用这个函数之后PA15、PB3、PB4 才能当普通 IO 用但 PA13/PA14 仍然留给调试器。调试环境里如果开了 RTT 或者其他实时跟踪功能注意别在低功耗模式下唤醒时重新初始化 GPIO那会短暂拉低 SWCLK 导致调试器掉线。4.3 mcuviewer 和串口打印的配合F103 的调试手法之一是用 SEGGER RTT 而不是串口打印。RTT 走 J-Link 的 SWD 接口不占用 UART 引脚输出速度远高于串口在调试 PID 波形和充电策略状态机时非常有用。用 J-Link RTT Viewer 或 MCUViewer 查看变量把采样值以文本流形式输出比 LCD 上翻页效率高得多。串口打印用于现场问题定位RTT 用于开发阶段的实时观测两者分工明确。注意 stm32 的串口调试如果发现 delay 函数卡死先查 SysTick 中断优先级如果被 uIP 的临界区关掉了delay 就永远等不到中断这是 F103 上非常隐蔽的时序坑。5. 固件升级fw_update.bin 的 Flash 分区与 IAP 流程5.1 Flash 分区怎么划直流充电桩部署在现场远程升级固件是刚需。基于 STM32 的升级方案核心是 IAPBootloader 程序放在 Flash 起始地址App 程序放在后面的分区通过通信接口接收新固件写入 App 分区。fw_update.bin 就是编译好的 App 固件Bin 格式直接可烧写体积比 Hex 小适合走以太网传输。F103 的 Flash 是 256 KB 到 512 KB 不等以 512 KB 为例分区可以这样设计分区起始地址大小用途Bootloader0x0800000032 KB上电引导、升级入口App 主程序0x08008000448 KB充电桩业务逻辑参数区0x0807F0004 KB充电参数、校准数据App 工程需要改两个地方链接脚本里 ROM 起始地址改为 0x08008000中断向量表偏移在 SystemInit 后设置SCB-VTOR 0x08008000。烧录方式和普通程序不同先用 J-Link 烧 Bootloader然后通过以太网或串口把 fw_update.bin 传给 Bootloader。调试时用 J-Flash 读取 Flash 内容先确认 App 分区的起始字节是栈顶地址和向量表偏移值对上才能保证跳转不跑飞。5.2 升级流程的状态机怎么写Bootloader 的升级流程本质上是个状态机接收帧、校验帧、写入 Flash、跳转。每个数据帧可以定义成 1024 字节数据块加上头部信息头部包含帧序号和数据长度。收到完整帧后做 CRC32 校验通过后执行 Flash 擦写。typedef struct { uint32_t total_len; // 固件总长度 uint32_t crc32; // 固件 CRC32 校验值 uint32_t block_size; // 数据块大小 uint32_t blocks; // 数据块总数 } fw_header_t; void Bootloader_ProcessFrame(uint8_t *buf, uint16_t len) { fw_header_t *hdr (fw_header_t *)buf; if (len sizeof(fw_header_t)) return; if (hdr-total_len APP_MAX_SIZE) { // 固件大小超限拒绝升级 return; } FLASH_Unlock(); FLASH_ErasePage(APP_BASE_ADDR); // 擦除第一个页 // 接收完所有数据块后写入 for (uint32_t i 0; i hdr-blocks; i) { uint8_t *block GetNextBlock(); FLASH_ProgramWord(APP_BASE_ADDR i * hdr-block_size, *(uint32_t *)block); } FLASH_Lock(); }这段是示意代码实际上每个 1024 字节块需要单独擦写页F103 的页大小是 1 KB 或 2 KB具体看型号。写 Flash 前必须FLASH_Unlock()写完后FLASH_Lock()中途不能断电否则 Brick。更稳妥的做法是双备份先写到参数区后的备份区全部校验通过后再拷贝到 App 区这样升级失败还能回滚到旧版本。5.3 验证升级结果的方法固件升级完成后Bootloader 跳转到 App第一件事是读SCB-VTOR确认向量表偏移生效。然后在 App 的初始化里把版本号写入参数区的一个固定地址上位机通过 Modbus 寄存器 0x0100 读取版本号如果版本号和 fw_update.bin 的 build 号一致说明升级成功。如果跳转后程序跑飞大概率是中断向量表没有重映射或者 App 编译时使用的 Flash 起始地址没有在链接脚本里同步修改。用 J-Flash 读回 Flash 内容做逐字节对比是最可靠的验证手段确认 bin 文件每一字节都落在正确地址上再结合上位机的版本号回读整个升级链路才算闭环。本文还有配套的精品资源点击获取
返回列表