1. STM32串口升级程序概述
在嵌入式产品开发中,程序更新是一个永恒的话题。作为ST公司推出的主流32位微控制器,STM32系列芯片因其出色的性能和丰富的外设资源,在工业控制、消费电子等领域占据重要地位。而串口升级方案因其硬件简单、成本低廉的特点,成为中小型项目中最常用的程序更新方式之一。
我曾在多个量产项目中负责固件升级系统的开发,从最初的ISP方案到后来的IAP方案,再到结合无线模块的OTA升级,积累了不少实战经验。今天要分享的这套串口升级方案,是我们团队经过多个项目验证的成熟方案,包含完整的ISP、IAP实现代码和配套的上位机工具。
提示:本文所有代码示例均基于STM32F103系列芯片,使用标准外设库开发。实际应用时需根据具体芯片型号调整相关配置。
2. ISP方案实现详解
2.1 ISP技术原理
ISP(In-System Programming)即在线系统编程,是ST芯片内置的编程方式。其核心原理是利用芯片内置的Bootloader,通过特定接口(如USART、USB、CAN等)与外部编程器通信,实现对Flash存储器的编程操作。
STM32芯片的Bootloader存储在系统存储器(System Memory)中,出厂时已预编程。要进入Bootloader模式,通常需要在芯片复位时保持特定引脚的电平状态:
- STM32F1系列:BOOT0=1,BOOT1=0
- STM32F4系列:BOOT0=1,BOOT1=0
- STM32H7系列:BOOT0=1,BOOT1=0
2.2 硬件设计要点
在设计支持ISP的硬件时,需要特别注意以下几点:
-
BOOT引脚设计:必须确保BOOT0和BOOT1引脚可通过跳线或开关改变状态。典型设计是BOOT0通过10k电阻下拉到地,同时可通过跳线帽连接到VCC。
-
串口连接:USART1的TX(PA9)和RX(PA10)引脚应引出到连接器,同时建议在信号线上串联100Ω电阻以提高抗干扰能力。
-
复位电路:必须设计可靠的手动复位按钮,因为在切换BOOT模式后需要复位芯片才能生效。
-
电源稳定性:编程过程中电压波动可能导致编程失败,建议在VCC和地之间并联100μF和0.1μF电容。
2.3 软件实现细节
ISP模式下,芯片运行的是出厂预置的Bootloader程序,我们只需要实现与之通信的上位机即可。ST官方提供了Flash Loader Demonstrator工具,但我们也可以自己开发定制化的上位机。
上位机与Bootloader的通信协议基于简单的请求-应答机制,主要包含以下命令:
- 初始化命令(0x7F)
- 获取版本命令(0x00)
- 获取ID命令(0x02)
- 读内存命令(0x11)
- 写内存命令(0x31)
- 擦除命令(0x43)
- 跳转命令(0x21)
一个典型的ISP编程流程如下:
- 发送初始化命令(0x7F)
- 等待应答(0x79)
- 发送获取版本命令(0x00 0xFF)
- 接收版本信息
- 发送擦除命令擦除目标扇区
- 分块发送编程数据
- 发送跳转命令启动用户程序
3. IAP方案深入解析
3.1 IAP技术原理
IAP(In-Application Programming)与ISP的最大区别在于,IAP是由用户程序自身完成Flash编程操作。这种方式的优势在于:
- 不需要硬件切换BOOT引脚
- 可以实现远程升级(通过无线模块)
- 可以设计更灵活的升级策略(如差分升级)
IAP的核心是STM32的Flash编程接口,通过操作Flash控制寄存器(FLASH_CR)来完成擦除和编程操作。需要注意的是,在执行Flash操作时,CPU会暂停执行指令,直到操作完成。
3.2 内存布局规划
实现IAP功能时,合理的Flash空间划分至关重要。以STM32F103C8T6为例(64KB Flash),典型的划分方式如下:
code复制0x08000000 - 0x08004FFF: IAP引导程序(20KB)
0x08005000 - 0x0800FFFF: 用户应用程序(44KB)
0x08010000 - 0x0801FFFF: 升级数据区(64KB)
对应的链接脚本(.ld文件)需要做相应调整:
code复制MEMORY
{
FLASH (rx) : ORIGIN = 0x08005000, LENGTH = 44K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
}
3.3 关键代码实现
3.3.1 Flash擦除操作
c复制void FLASH_ErasePage(uint32_t Page_Address)
{
while(FLASH->SR & FLASH_SR_BSY);
if(FLASH->SR & FLASH_SR_EOP)
FLASH->SR = FLASH_SR_EOP;
FLASH->CR |= FLASH_CR_PER;
FLASH->AR = Page_Address;
FLASH->CR |= FLASH_CR_STRT;
while(FLASH->SR & FLASH_SR_BSY);
FLASH->CR &= ~FLASH_CR_PER;
}
3.3.2 Flash编程操作
c复制void FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data)
{
while(FLASH->SR & FLASH_SR_BSY);
FLASH->CR |= FLASH_CR_PG;
*(__IO uint16_t*)Address = Data;
while(FLASH->SR & FLASH_SR_BSY);
if(FLASH->SR & FLASH_SR_EOP)
FLASH->SR = FLASH_SR_EOP;
FLASH->CR &= ~FLASH_CR_PG;
}
3.3.3 应用程序跳转
c复制void JumpToApp(uint32_t AppAddr)
{
typedef void (*pFunction)(void);
pFunction Jump_To_Application;
uint32_t JumpAddress;
if((*(__IO uint32_t*)AppAddr & 0x2FFE0000) == 0x20000000)
{
JumpAddress = *(__IO uint32_t*)(AppAddr + 4);
Jump_To_Application = (pFunction)JumpAddress;
__set_MSP(*(__IO uint32_t*)AppAddr);
Jump_To_Application();
}
}
3.4 升级流程设计
一个完整的IAP升级流程通常包含以下步骤:
- 上位机发送升级开始命令
- 设备应答并准备接收数据
- 上位机分块发送升级数据(每块带校验)
- 设备接收并校验数据,存储到临时区域
- 全部数据接收完成后,设备验证整体校验和
- 校验通过后,擦除目标区域并写入新程序
- 跳转到新程序执行
4. 上位机开发实践
4.1 功能需求分析
一个实用的升级上位机应该具备以下功能:
- 串口通信配置(端口、波特率等)
- 文件选择与显示
- 升级进度显示
- 日志记录
- 支持多种协议(如XMODEM、YMODEM等)
- 自动重试机制
4.2 Python实现示例
基于PyQt5和PySerial,我们可以开发一个图形化的升级工具:
python复制import sys
import serial
from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout,
QHBoxLayout, QPushButton, QTextEdit,
QLabel, QComboBox, QProgressBar,
QFileDialog, QWidget)
class BootloaderTool(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
self.serial = None
def initUI(self):
# 串口配置区域
port_label = QLabel('串口:')
self.port_combo = QComboBox()
self.port_combo.addItems(['COM1', 'COM2', 'COM3', 'COM4'])
baud_label = QLabel('波特率:')
self.baud_combo = QComboBox()
self.baud_combo.addItems(['9600', '19200', '38400', '57600', '115200'])
self.baud_combo.setCurrentText('115200')
# 文件选择区域
self.file_btn = QPushButton('选择文件')
self.file_btn.clicked.connect(self.select_file)
self.file_label = QLabel('未选择文件')
# 操作按钮
self.connect_btn = QPushButton('连接')
self.connect_btn.clicked.connect(self.toggle_connect)
self.upgrade_btn = QPushButton('升级')
self.upgrade_btn.clicked.connect(self.start_upgrade)
self.upgrade_btn.setEnabled(False)
# 进度显示
self.progress = QProgressBar()
self.log = QTextEdit()
self.log.setReadOnly(True)
# 布局设置
config_layout = QHBoxLayout()
config_layout.addWidget(port_label)
config_layout.addWidget(self.port_combo)
config_layout.addWidget(baud_label)
config_layout.addWidget(self.baud_combo)
file_layout = QHBoxLayout()
file_layout.addWidget(self.file_btn)
file_layout.addWidget(self.file_label)
btn_layout = QHBoxLayout()
btn_layout.addWidget(self.connect_btn)
btn_layout.addWidget(self.upgrade_btn)
main_layout = QVBoxLayout()
main_layout.addLayout(config_layout)
main_layout.addLayout(file_layout)
main_layout.addLayout(btn_layout)
main_layout.addWidget(self.progress)
main_layout.addWidget(self.log)
container = QWidget()
container.setLayout(main_layout)
self.setCentralWidget(container)
self.setWindowTitle('STM32升级工具')
self.setGeometry(300, 300, 600, 400)
def select_file(self):
file_name, _ = QFileDialog.getOpenFileName(
self, '选择升级文件', '', 'Bin Files (*.bin);;All Files (*)')
if file_name:
self.file_label.setText(file_name)
self.upgrade_btn.setEnabled(True)
def toggle_connect(self):
if self.serial and self.serial.is_open:
self.serial.close()
self.connect_btn.setText('连接')
self.log.append('串口已断开')
else:
try:
self.serial = serial.Serial(
self.port_combo.currentText(),
int(self.baud_combo.currentText()),
timeout=1)
self.connect_btn.setText('断开')
self.log.append('串口已连接')
except Exception as e:
self.log.append(f'连接失败: {str(e)}')
def start_upgrade(self):
if not self.serial or not self.serial.is_open:
self.log.append('错误: 请先连接串口')
return
file_name = self.file_label.text()
try:
with open(file_name, 'rb') as f:
data = f.read()
total = len(data)
chunk_size = 256
sent = 0
self.serial.write(b'\x7F') # 发送初始化命令
response = self.serial.read(1)
if response != b'\x79':
raise Exception('设备无响应')
self.log.append('开始升级...')
for i in range(0, total, chunk_size):
chunk = data[i:i+chunk_size]
self.serial.write(chunk)
sent += len(chunk)
progress = int((sent / total) * 100)
self.progress.setValue(progress)
self.log.append('升级完成')
self.progress.setValue(100)
except Exception as e:
self.log.append(f'升级失败: {str(e)}')
self.progress.setValue(0)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = BootloaderTool()
ex.show()
sys.exit(app.exec_())
4.3 上位机开发注意事项
-
数据校验:每次发送数据后应等待设备应答,并验证数据完整性。常用的校验方式包括累加和、CRC16等。
-
超时处理:每个操作都应设置合理的超时时间,避免因通信中断导致程序卡死。
-
进度反馈:应将升级进度实时显示给用户,提升用户体验。
-
日志记录:详细记录操作过程和错误信息,便于问题排查。
-
多线程设计:通信处理应放在独立线程中,避免阻塞UI线程。
5. 常见问题与解决方案
5.1 ISP模式无法进入
现象:按照要求设置BOOT引脚后,仍然无法进入ISP模式。
排查步骤:
- 检查BOOT引脚电压:BOOT0应为高电平(3.3V),BOOT1应为低电平(0V)
- 检查复位电路:按下复位按钮时应有明显的电平变化
- 检查串口连接:TX/RX线序是否正确,信号线是否连通
- 尝试降低波特率:有些情况下高波特率可能不稳定
5.2 IAP跳转失败
现象:IAP程序执行跳转后,设备无反应或复位。
可能原因:
- 应用程序起始地址设置不正确
- 应用程序中断向量表未重定位
- 堆栈指针指向了非法地址
解决方案:
- 确认应用程序链接脚本中的ORIGIN值与IAP程序中的跳转地址一致
- 在应用程序启动代码中添加中断向量表重定位代码:
c复制void SystemInit(void) { SCB->VTOR = FLASH_BASE | 0x5000; // 根据实际偏移量调整 // ...其他初始化代码 } - 检查应用程序的map文件,确认初始堆栈指针值有效
5.3 Flash编程失败
现象:在执行Flash编程操作时,返回错误或数据未正确写入。
常见原因:
- 未解锁Flash
- 编程前未擦除
- 编程地址不对齐(半字编程必须2字节对齐)
- 在编程过程中发生中断
正确流程:
c复制void FLASH_Program(uint32_t Address, uint8_t *Data, uint32_t Length)
{
FLASH_Unlock();
FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
for(uint32_t i = 0; i < Length; i += 2) {
uint16_t value = Data[i] | (Data[i+1] << 8);
FLASH_ProgramHalfWord(Address + i, value);
}
FLASH_Lock();
}
5.4 上位机通信异常
现象:上位机与设备通信不稳定,数据丢失或错误。
优化措施:
- 增加数据包序号和重传机制
- 采用更可靠的协议如XMODEM
- 实现流量控制,避免缓冲区溢出
- 添加更严格的超时处理
XMODEM协议示例:
python复制def xmodem_send(serial_port, file_path):
with open(file_path, 'rb') as f:
data = f.read()
packet_size = 128
packet_num = 1
retry = 0
while True:
# 等待接收'C'字符(CRC模式)
response = serial_port.read(1)
if response == b'C':
break
time.sleep(0.1)
for i in range(0, len(data), packet_size):
chunk = data[i:i+packet_size]
if len(chunk) < packet_size:
chunk += b'\x1A' * (packet_size - len(chunk))
# 计算CRC16
crc = calc_crc16(chunk)
# 发送数据包
packet = bytearray()
packet.append(0x01) # SOH
packet.append(packet_num % 256)
packet.append(255 - (packet_num % 256))
packet.extend(chunk)
packet.append(crc >> 8)
packet.append(crc & 0xFF)
serial_port.write(packet)
# 等待应答
ack = serial_port.read(1)
if ack == b'\x06': # ACK
packet_num += 1
retry = 0
else:
retry += 1
if retry > 3:
raise Exception('传输失败')
6. 性能优化与高级功能
6.1 差分升级实现
对于大容量固件,可以实施差分升级以减少传输数据量:
- 使用bsdiff算法生成差分包
- 设备端实现bspatch算法
- 升级流程:
- 上位机比较新旧版本生成差分包
- 发送差分包到设备
- 设备端应用差分包重构新固件
- 校验后写入目标区域
6.2 断点续传机制
为防止升级过程中断导致设备变砖,可以实现断点续传:
- 在Flash中开辟状态存储区
- 记录已接收的数据块信息
- 重新连接后,上位机查询升级状态
- 从断点处继续传输
6.3 安全加固措施
- 固件加密:使用AES等算法加密固件,设备端解密后再写入
- 签名验证:使用RSA/ECC验证固件签名
- 防回滚:添加版本检查,防止降级攻击
- 安全启动:在Bootloader中验证应用程序完整性
6.4 内存优化技巧
- 使用内存缓冲池管理升级数据
- 实现流式编程,避免大内存缓冲
- 合理使用DMA减轻CPU负担
- 优化Flash写入时序,提高编程速度
在多个项目实践中,我发现最影响升级成功率的关键因素是通信可靠性和Flash编程稳定性。特别是在工业环境中,电磁干扰可能导致通信错误,因此必须实现完善的错误检测和恢复机制。另外,不同型号STM32的Flash特性有所差异,在实际开发中需要参考对应型号的参考手册调整编程参数。