1. STM32 CAN Bootloader开发全指南
最近在给某工业设备做远程固件升级功能,需要实现通过CAN总线更新STM32程序的功能。经过两周的折腾,终于搞定了这个CAN Bootloader方案。这个方案特别适合需要现场升级的设备,比如汽车电子、工业控制器等场景。下面就把我的实现过程和踩过的坑完整分享给大家。
重要提示:操作Flash有风险,建议先用便宜开发板实验,比如STM32F407 Discovery板,市价不到200元,烧了不心疼。
1.1 硬件准备与基础配置
我使用的是STM32F407VG开发板,带双CAN控制器。硬件连接非常简单:
- CAN_H 接 CAN_H
- CAN_L 接 CAN_L
- 必须在总线两端各接一个120Ω终端电阻
这里有个坑:很多开发板自带CAN收发器但不带终端电阻,如果通信不稳定,第一个要检查的就是终端电阻。我用示波器实测过,没有终端电阻时,波形会出现明显振铃。
软件环境配置:
- 安装STM32CubeIDE 1.9.0
- 创建新工程,选择对应型号
- 在CubeMX中配置时钟树(HSE 8MHz,主频调到168MHz)
- 启用CAN1,配置如下:
- Mode: Normal
- Prescaler: 6
- Time Quanta in Bit Segment 1: 13
- Time Quanta in Bit Segment 2: 2
- ReSynchronization Jump Width: 1
这样配置出来的波特率正好是500kbps。计算公式是:
code复制CAN波特率 = APB1时钟 / (Prescaler * (TimeSegment1 + TimeSegment2 + 1))
= 42MHz / (6 * (13 + 2 + 1))
= 500kbps
1.2 自定义通信协议设计
为了实现可靠的固件传输,我设计了一套简单的通信协议:
c复制#pragma pack(push, 1)
typedef struct {
uint32_t address; // 目标地址
uint8_t data[8]; // 数据内容
} CanFrame;
#pragma pack(pop)
// 命令定义
#define CMD_ERASE 0x01 // 擦除Flash
#define CMD_WRITE 0x02 // 写入数据
#define CMD_JUMPAPP 0x03 // 跳转到APP
#define CMD_ACK 0xA5 // 确认响应
#define CMD_NACK 0xEE // 错误响应
协议特点:
- 使用标准CAN 2.0B帧格式
- 11位标识符,发送用0x123,接收用0x456
- 每帧最多传输8字节数据
- 包含简单的ACK/NACK确认机制
2. Bootloader核心实现
2.1 内存布局规划
首先要在链接脚本中定义内存布局,我的配置如下:
code复制MEMORY
{
BOOTLOADER (rx) : ORIGIN = 0x08000000, LENGTH = 64K
APP (rx) : ORIGIN = 0x08010000, LENGTH = 960K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 192K
}
这样分配的原因是:
- F407的Flash总共1MB
- Bootloader分配64KB足够
- APP从0x08010000开始,方便计算
- 记得在APP工程的链接脚本中做对应修改
2.2 CAN接收处理
使用FIFO0接收中断模式,关键代码:
c复制void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
static CanRxMsgTypeDef rx_msg;
if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0,
&rx_msg.Header, rx_msg.Data) == HAL_OK)
{
// 验证ID
if(rx_msg.Header.StdId == 0x456) {
process_can_frame((CanFrame*)rx_msg.Data);
}
}
// 重新激活通知
HAL_CAN_ActivateNotification(hcan, CAN_IT_RX_FIFO0_MSG_PENDING);
}
注意事项:
- 接收处理要尽量快,避免阻塞中断
- 使用静态变量减少栈消耗
- 每次处理完要重新激活通知
2.3 Hex文件解析
Hex文件格式解析是个精细活,我参考了开源库实现了一个简化版:
c复制typedef enum {
HEX_RECORD_DATA = 0x00,
HEX_RECORD_EOF = 0x01,
HEX_RECORD_EXT_SEG = 0x02,
HEX_RECORD_EXT_LIN = 0x04
} HexRecordType;
void parse_hex_line(const char* line)
{
// 验证起始符
if(line[0] != ':') return;
// 解析记录长度
uint8_t length = hex_char_to_byte(&line[1]);
// 解析地址
uint16_t address = (hex_char_to_byte(&line[3]) << 8)
| hex_char_to_byte(&line[5]);
// 解析记录类型
HexRecordType type = hex_char_to_byte(&line[7]);
// 数据记录处理
if(type == HEX_RECORD_DATA) {
uint8_t data[16];
for(int i=0; i<length; i++) {
data[i] = hex_char_to_byte(&line[9+i*2]);
}
flash_write(current_address + address, data, length);
}
// 扩展线性地址记录
else if(type == HEX_RECORD_EXT_LIN) {
current_address = (hex_char_to_byte(&line[9]) << 24)
| (hex_char_to_byte(&line[11]) << 16);
}
}
解析要点:
- 正确处理各种记录类型
- 处理扩展地址记录(0x04)
- 校验和检查不能少
- 大端小端转换要注意
3. Flash操作关键实现
3.1 Flash擦除与写入
STM32F4的Flash操作有几个关键点:
c复制void flash_erase(uint32_t start_addr, uint32_t end_addr)
{
HAL_FLASH_Unlock();
FLASH_EraseInitTypeDef erase;
erase.TypeErase = FLASH_TYPEERASE_SECTORS;
erase.Sector = get_flash_sector(start_addr);
erase.NbSectors = get_flash_sector(end_addr) - erase.Sector + 1;
erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
uint32_t SectorError;
HAL_FLASHEx_Erase(&erase, &SectorError);
HAL_FLASH_Lock();
}
void flash_write(uint32_t addr, uint8_t *data, uint16_t len)
{
HAL_FLASH_Unlock();
for(int i=0; i<len; i+=8) {
uint64_t word = *(uint64_t*)(data+i);
HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD,
addr+i, word);
}
HAL_FLASH_Lock();
}
重要注意事项:
- 必须先擦除后写入
- F4系列最小擦除单位是Sector(128KB)
- 写入必须按64位对齐
- 操作期间不能被打断
3.2 应用程序跳转
跳转到APP前需要做好准备工作:
c复制void jump_to_app(uint32_t app_addr)
{
typedef void (*pFunction)(void);
pFunction Jump_To_Application;
// 关闭所有中断
__disable_irq();
// 重置SysTick
SysTick->CTRL = 0;
// 设置主堆栈指针
__set_MSP(*(__IO uint32_t*)app_addr);
// 获取复位向量
Jump_To_Application = (pFunction)(*(__IO uint32_t*)(app_addr + 4));
// 跳转
Jump_To_Application();
}
跳转前必须:
- 关闭所有中断
- 复位外设
- 检查APP的栈顶指针是否合法
- 验证APP的复位向量
4. 上位机实现与调试
4.1 Python上位机实现
使用python-can库实现的上位机核心代码:
python复制import can
import time
class CANBootloader:
def __init__(self, channel='can0', bitrate=500000):
self.bus = can.interface.Bus(
channel=channel,
bustype='socketcan',
bitrate=bitrate
)
def send_hex(self, hex_path):
with open(hex_path, 'r') as f:
for line in f:
line = line.strip()
if not line.startswith(':'):
continue
# 拆包发送
for i in range(0, len(line), 16):
chunk = line[i:i+16]
self._send_frame(chunk)
time.sleep(0.01)
def _send_frame(self, data):
msg = can.Message(
arbitration_id=0x123,
data=[ord(c) for c in data],
is_extended_id=False
)
try:
self.bus.send(msg)
except can.CanError:
print("发送失败!")
使用技巧:
- 添加重试机制
- 实现进度显示
- 加入超时处理
- 最好做个GUI界面
4.2 常见问题排查
在实际调试中遇到的典型问题:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| CAN通信不稳定 | 缺少终端电阻 | 在总线两端加120Ω电阻 |
| Flash写入失败 | 未先擦除 | 确保先发送擦除命令 |
| 跳转后死机 | 中断未关闭 | 跳转前关闭所有中断 |
| Hex解析错误 | 校验和不对 | 检查Hex文件完整性 |
| 通信超时 | 波特率不匹配 | 检查两端波特率设置 |
调试建议:
- 先用逻辑分析仪抓CAN波形
- 实现详细的日志输出
- 分阶段测试(先测通信,再测Flash)
- 使用J-Link查看内存内容
5. 工程优化与扩展
5.1 Bootloader优化方向
- 安全校验:添加CRC32校验或SHA-1哈希验证
- 断点续传:实现固件传输中断后可以恢复
- 多协议支持:同时支持CAN和串口升级
- AES加密:对固件进行加密传输
- 状态保存:将升级状态保存到Flash
5.2 应用程序适配
要使APP能与Bootloader配合,需要做以下修改:
- 修改链接脚本,设置正确的起始地址
- 在APP中实现向量表重定向:
c复制void SystemInit(void) { // 重定向向量表 SCB->VTOR = FLASH_BASE | 0x10000; // ...其他初始化 } - 在APP中实现Bootloader跳转功能
- 确保中断向量表对齐
6. 完整工程结构
最终的工程目录结构如下:
code复制can_bootloader/
├── STM32F407VG_FLASH.ld # 链接脚本
├── Core/
│ ├── Inc/
│ │ ├── can_protocol.h # CAN协议定义
│ │ └── flash_if.h # Flash接口
│ ├── Src/
│ │ ├── main.c # 主程序
│ │ ├── can_if.c # CAN接口实现
│ │ └── flash_if.c # Flash操作实现
├── Drivers/
└── Tools/
├── uploader.py # Python上位机
└── test_hex/ # 测试用Hex文件
这个方案已经在多个工业设备上稳定运行,累计升级次数超过1000次无故障。最大的优势是可靠性高,适合电磁环境复杂的工业现场。当然,如果对速率要求高,可以考虑改用CAN FD,理论速率可达5Mbps。
