在嵌入式系统开发中,固件升级是一个永恒的话题。基于STM32F4系列微控制器的CAN总线升级方案,为工业现场设备提供了可靠、高效的远程更新途径。这个方案的核心在于bootloader的设计实现,它需要在不影响现有功能的前提下,完成新固件的接收、校验和切换。
我曾在多个工业自动化项目中采用类似的方案,实测证明CAN总线升级的稳定性远超UART和I2C等传统方式。特别是在电磁环境复杂的车间里,CAN的抗干扰能力让固件升级成功率保持在99%以上。整套方案包含两个关键部分:驻留在Flash起始地址的bootloader,以及可通过CAN接收的用户应用程序(app)。
STM32F4系列芯片内置了CAN控制器,如STM32F407VG的bxCAN模块支持CAN 2.0B协议。硬件连接上只需:
注意:CAN_H和CAN_L必须使用双绞线,线距不超过1cm能显著降低EMI干扰。我在一个变频器项目中曾因使用平行线导致升级失败率高达30%,改用双绞线后问题立即消失。
典型的Flash分区如下(以STM32F407VG的1MB Flash为例):
| 地址范围 | 用途 | 大小 |
|---|---|---|
| 0x08000000-0x08003FFF | Bootloader | 16KB |
| 0x08004000-0x0800FFFF | 参数存储区 | 48KB |
| 0x08010000-0x080FFFFF | 用户程序(app) | 960KB |
这种分配保证了:
自定义的升级协议帧格式如下:
| 字段 | 长度(bytes) | 说明 |
|---|---|---|
| 帧类型 | 1 | 0x01:开始传输 0x02:数据 0x03:结束 |
| 包序号 | 2 | 大端格式,从0开始 |
| 数据长度 | 1 | 当前包有效数据长度(0-8) |
| 数据 | 0-8 | 有效载荷 |
| CRC8 | 1 | 从帧类型到数据的校验 |
在Keil工程中,这个协议通过结构体实现:
c复制#pragma pack(1)
typedef struct {
uint8_t frame_type;
uint16_t pkg_id;
uint8_t data_len;
uint8_t data[8];
uint8_t crc;
} CAN_Upgrade_Frame;
#pragma pack()
上电后bootloader的执行逻辑:
关键跳转代码:
c复制if (((*(__IO uint32_t*)APP_ADDRESS) & 0x2FFE0000) == 0x20000000) {
JumpToApplication = (pFunction)(*(__IO uint32_t*)(APP_ADDRESS + 4));
__set_MSP(*(__IO uint32_t*)APP_ADDRESS);
JumpToApplication();
}
采用双缓冲接收模式提高效率:
c复制CAN_HandleTypeDef hcan;
hcan.Instance = CAN1;
hcan.Init.Prescaler = 6; // APB1时钟42MHz, 波特率=42M/(6*(1+8+3))=500kbps
hcan.Init.Mode = CAN_MODE_NORMAL;
hcan.Init.SyncJumpWidth = CAN_SJW_3TQ;
hcan.Init.TimeSeg1 = CAN_BS1_8TQ;
hcan.Init.TimeSeg2 = CAN_BS2_3TQ;
hcan.Init.TimeTriggeredMode = DISABLE;
hcan.Init.AutoBusOff = DISABLE;
hcan.Init.AutoWakeUp = DISABLE;
hcan.Init.AutoRetransmission = DISABLE; // 禁止自动重传避免总线拥塞
hcan.Init.ReceiveFifoLocked = DISABLE;
hcan.Init.TransmitFifoPriority = DISABLE;
HAL_CAN_Init(&hcan);
// 配置过滤器只接收升级专用ID
CAN_FilterTypeDef filter;
filter.FilterIdHigh = 0x123 << 5; // 标准ID 0x123
filter.FilterIdLow = 0;
filter.FilterMaskIdHigh = 0x7FF << 5;
filter.FilterMaskIdLow = 0;
filter.FilterFIFOAssignment = CAN_FILTER_FIFO0;
filter.FilterBank = 0;
filter.FilterMode = CAN_FILTERMODE_IDMASK;
filter.FilterScale = CAN_FILTERSCALE_32BIT;
filter.FilterActivation = ENABLE;
HAL_CAN_ConfigFilter(&hcan, &filter);
// 启动CAN并激活通知
HAL_CAN_Start(&hcan);
HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);
STM32F4的Flash编程关键点:
优化后的擦除函数:
c复制#define FLASH_SECTOR_ERASE_TIMEOUT 50000 // 50ms超时
HAL_StatusTypeDef Flash_Erase_Sector(uint32_t sector) {
FLASH_EraseInitTypeDef erase;
uint32_t sector_error;
erase.TypeErase = FLASH_TYPEERASE_SECTORS;
erase.Sector = sector;
erase.NbSectors = 1;
erase.VoltageRange = FLASH_VOLTAGE_RANGE_3; // 适用于3.3V供电
HAL_FLASH_Unlock();
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_OPERR | FLASH_FLAG_WRPERR);
uint32_t tick = HAL_GetTick();
while (__HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY)) {
if (HAL_GetTick() - tick > FLASH_SECTOR_ERASE_TIMEOUT) {
HAL_FLASH_Lock();
return HAL_TIMEOUT;
}
}
if (HAL_FLASHEx_Erase(&erase, §or_error) != HAL_OK) {
HAL_FLASH_Lock();
return HAL_ERROR;
}
HAL_FLASH_Lock();
return HAL_OK;
}
在Keil中需要修改:
c复制// 在main()开头添加
SCB->VTOR = FLASH_BASE | 0x10000;
链接脚本修改示例:
code复制LR_IROM1 0x08010000 0x000F0000 { ; 960KB
ER_IROM1 0x08010000 0x000F0000 {
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x20000000 0x00020000 {
.ANY (+RW +ZI)
}
}
用户程序需要提供升级触发机制,例如:
c复制#define FORCE_UPDATE_PIN GPIO_PIN_0
#define FORCE_UPDATE_PORT GPIOA
void Check_Update_Request(void) {
if (HAL_GPIO_ReadPin(FORCE_UPDATE_PORT, FORCE_UPDATE_PIN) == GPIO_PIN_RESET) {
__disable_irq();
NVIC_SystemReset();
}
}
在高端应用中可采用双bank设计:
关键寄存器操作:
c复制// 切换启动bank
FLASH_OBProgramInitTypeDef ob;
ob.OptionType = OPTIONBYTE_USER;
ob.USERConfig = OB_BOOT_BANK1; // 或OB_BOOT_BANK2
HAL_FLASHEx_OBProgram(&ob);
推荐使用PCAN-View或USB-CAN分析仪配合自研工具。我曾用Python开发过一个简易测试工具:
python复制import can
import time
import crc8
bus = can.interface.Bus(channel='COM3', bustype='slcan', bitrate=500000)
def send_frame(frame_type, pkg_id, data):
frame = bytearray()
frame.append(frame_type)
frame.extend(pkg_id.to_bytes(2, 'big'))
frame.append(len(data))
frame.extend(data)
crc = crc8.crc8(frame).digest()
frame.extend(crc)
msg = can.Message(arbitration_id=0x123, data=frame, is_extended_id=False)
bus.send(msg)
# 发送升级开始帧
send_frame(0x01, 0, b'')
CAN通信失败:
Flash写入错误:
跳转失败:
加速传输:
安全增强:
可靠性提升:
这套方案在工业现场经过三年实际验证,累计完成超过2万次远程升级,平均耗时约3分钟(对于1MB固件)。关键是要做好错误处理和状态反馈,建议在bootloader中添加详细的状态码返回机制,方便现场工程师快速定位问题。