1. 项目概述
在嵌入式设备开发中,程序更新是一个常见但至关重要的需求。传统的通过JTAG/SWD接口烧录程序的方式虽然可靠,但在设备部署后往往难以实施。基于STM32F103的BootLoader IAP方案提供了一种优雅的解决方案,允许设备在不依赖专用烧录工具的情况下,通过串口完成固件更新。
这个方案的核心价值在于:
- 使用常见的UART或485接口,无需额外硬件支持
- 采用工业级的YModem协议确保数据传输可靠性
- 上位机使用C#开发,便于在Windows平台部署
- 下位机代码设计考虑移植性,可适配多种ARM芯片
2. 系统架构设计
2.1 整体工作流程
典型的IAP更新流程包含以下几个阶段:
- BootLoader启动并初始化硬件
- 检测是否有更新请求(如特定按键按下或收到上位机指令)
- 进入IAP模式,准备接收新固件
- 通过YModem协议接收数据并写入Flash
- 校验固件完整性
- 跳转到新固件执行
2.2 存储空间规划
对于STM32F103系列MCU,Flash通常划分为三个区域:
- BootLoader区:存放引导程序(通常占用16-32KB)
- 应用程序区:存放用户固件
- 参数存储区:存放版本号、校验信息等
关键考虑因素:
- 根据芯片型号调整分区大小
- 确保应用程序的向量表偏移设置正确
- 保留足够的空间用于临时存储接收到的数据
3. 下位机实现细节
3.1 BootLoader核心功能
BootLoader需要实现以下关键功能:
- 硬件初始化(时钟、串口、GPIO等)
- 更新条件检测逻辑
- 通信协议处理
- Flash编程接口
- 应用程序跳转机制
3.1.1 串口初始化优化
在实际项目中,串口配置需要考虑更多细节:
c复制void USART_Init(void) {
USART_InitTypeDef USART_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
// 时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1 | RCC_APB2Periph_AFIO, ENABLE);
// GPIO配置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // TX
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // RX
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// USART配置
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
// 中断配置
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART1, ENABLE);
}
注意:在实际项目中,建议添加硬件流控制(如RTS/CTS)以提高通信可靠性,特别是在长距离485通信场景下。
3.2 Flash编程注意事项
Flash编程是IAP的核心,需要特别注意:
- 操作前必须解锁Flash
- 按页擦除(STM32F103通常为1KB或2KB一页)
- 写入前确保目标地址已擦除
- 采用半字(16bit)写入方式
- 操作完成后重新锁定Flash
优化后的Flash写入函数:
c复制#define FLASH_PAGE_SIZE 1024 // 根据具体芯片型号调整
uint8_t IAP_Write_Flash(uint32_t WriteAddr, uint8_t *pBuffer, uint32_t NumToWrite) {
FLASH_Status status;
uint32_t PageError = 0;
uint32_t pages = (NumToWrite + FLASH_PAGE_SIZE - 1) / FLASH_PAGE_SIZE;
uint32_t pageStart = WriteAddr & ~(FLASH_PAGE_SIZE - 1);
FLASH_Unlock();
FLASH_ClearFlag(FLASH_FLAG_BSY | FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
// 擦除所需页
for(uint32_t i = 0; i < pages; i++) {
status = FLASH_ErasePage(pageStart + i * FLASH_PAGE_SIZE);
if(status != FLASH_COMPLETE) {
FLASH_Lock();
return 1; // 擦除失败
}
}
// 写入数据
for(uint32_t i = 0; i < NumToWrite; i += 2) {
uint16_t data = (i+1 < NumToWrite) ? (pBuffer[i+1] << 8) | pBuffer[i] : pBuffer[i];
status = FLASH_ProgramHalfWord(WriteAddr + i, data);
if(status != FLASH_COMPLETE) {
FLASH_Lock();
return 2; // 写入失败
}
}
FLASH_Lock();
return 0; // 成功
}
4. YModem协议实现
4.1 协议概述
YModem是XModem的增强版,主要特点:
- 支持1024字节数据块传输
- 支持批量文件传输
- 使用CRC-16校验
- 支持文件名和文件大小传输
4.2 协议帧格式
YModem传输包含以下几种帧类型:
- 起始帧:包含文件名和文件大小
- 数据帧:包含数据块和序号
- 结束帧:表示传输结束
典型的数据帧结构:
code复制[SOH][序号][~序号][数据][CRC高][CRC低]
其中:
- SOH:0x01(表示128字节数据块)或STX(0x02,表示1024字节数据块)
- 序号:从1开始的递增序号
- ~序号:序号的补码
- 数据:实际数据,不足部分填充0x1A
- CRC:16位CRC校验值
4.3 协议状态机实现
建议采用状态机方式实现协议解析:
c复制typedef enum {
YMODEM_STATE_IDLE,
YMODEM_STATE_WAIT_C,
YMODEM_STATE_RECEIVE_HEADER,
YMODEM_STATE_RECEIVE_DATA,
YMODEM_STATE_WAIT_ACK,
YMODEM_STATE_COMPLETE,
YMODEM_STATE_ERROR
} YModemState;
typedef struct {
YModemState state;
uint8_t buffer[1024 + 4]; // 数据缓冲区
uint16_t blockNumber;
uint32_t fileSize;
uint32_t receivedSize;
uint8_t fileName[256];
} YModemHandler;
void YModem_Process(YModemHandler *handler, uint8_t data) {
switch(handler->state) {
case YMODEM_STATE_IDLE:
if(data == 'C') {
handler->state = YMODEM_STATE_WAIT_C;
// 发送ACK
}
break;
case YMODEM_STATE_WAIT_C:
// 处理起始帧
break;
// 其他状态处理...
}
}
5. 上位机开发要点
5.1 C#串口通信实现
完整的YModem发送器实现需要考虑更多细节:
csharp复制public class YModemSender
{
private SerialPort _serialPort;
private const byte SOH = 0x01;
private const byte STX = 0x02;
private const byte EOT = 0x04;
private const byte ACK = 0x06;
private const byte NAK = 0x15;
private const byte CAN = 0x18;
private const byte C = 0x43;
public YModemSender(string portName, int baudRate)
{
_serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One);
_serialPort.Handshake = Handshake.RequestToSend;
_serialPort.ReadTimeout = 3000;
_serialPort.WriteTimeout = 3000;
}
public bool SendFile(string filePath)
{
try
{
_serialPort.Open();
// 等待接收方发送'C'
if(!WaitForCharacter(C, 5000))
return false;
// 发送文件头信息
FileInfo fileInfo = new FileInfo(filePath);
byte[] headerPacket = CreateHeaderPacket(fileInfo.Name, fileInfo.Length);
_serialPort.Write(headerPacket, 0, headerPacket.Length);
// 等待ACK
if(!WaitForCharacter(ACK, 1000))
return false;
// 发送文件数据
using (FileStream fs = new FileStream(filePath, FileMode.Open))
{
byte[] buffer = new byte[1024];
int bytesRead;
ushort blockNumber = 1;
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
byte[] dataPacket = CreateDataPacket(blockNumber++, buffer, bytesRead);
_serialPort.Write(dataPacket, 0, dataPacket.Length);
if(!WaitForCharacter(ACK, 1000))
return false;
}
}
// 发送EOT
_serialPort.Write(new byte[] { EOT }, 0, 1);
return WaitForCharacter(ACK, 1000);
}
finally
{
if(_serialPort.IsOpen)
_serialPort.Close();
}
}
private byte[] CreateHeaderPacket(string fileName, long fileSize)
{
// 实现头包创建逻辑
}
private byte[] CreateDataPacket(ushort blockNumber, byte[] data, int length)
{
// 实现数据包创建逻辑
}
private bool WaitForCharacter(byte expected, int timeout)
{
// 实现等待特定字符的逻辑
}
}
5.2 用户界面设计建议
对于生产环境使用的上位机,建议包含以下功能:
- 串口参数配置(端口、波特率、校验等)
- 文件选择功能
- 传输进度显示
- 日志记录窗口
- 多语言支持
- 自动重试机制
6. 系统集成与测试
6.1 集成注意事项
- 确保BootLoader和应用程序的向量表偏移设置正确
- 应用程序的链接脚本需要调整以避免与BootLoader区域冲突
- 在应用程序中添加IAP跳转接口
- 实现版本检查机制,避免重复更新
6.2 测试方案
完整的测试应该包括:
-
功能测试:
- 正常文件传输
- 传输中断恢复
- 错误文件处理
-
性能测试:
- 不同波特率下的传输速度
- 大文件传输稳定性
- 长时间运行可靠性
-
异常测试:
- 突然断电恢复
- 错误数据包处理
- 超时重传机制
7. 常见问题与解决方案
7.1 传输失败问题排查
-
现象:无法建立连接
- 检查物理连接
- 确认波特率等参数一致
- 检查流控制设置
-
现象:传输中途失败
- 降低波特率测试
- 检查电源稳定性
- 增加超时重试机制
-
现象:数据校验失败
- 确认CRC算法一致
- 检查缓冲区溢出问题
- 验证Flash写入是否正确
7.2 性能优化建议
- 增大数据块大小(使用1024字节块)
- 合理设置超时时间(通常500ms-1000ms)
- 实现滑动窗口机制提高吞吐量
- 在资源允许的情况下使用双缓冲机制
8. 进阶扩展方向
-
安全增强:
- 增加数字签名验证
- 实现加密传输
- 添加防回滚机制
-
功能扩展:
- 支持差分升级
- 实现无线更新(通过蓝牙/WiFi)
- 添加远程触发更新功能
-
管理功能:
- 版本管理
- 更新日志记录
- 批量设备更新
在实际项目中,我们根据产品需求对这些扩展功能进行了选择性实现。特别是在工业控制领域,安全性和可靠性往往是首要考虑因素。