1. 项目概述:USART+DMA实现STM32 IAP功能
在嵌入式系统开发中,固件升级是一个常见需求。传统的通过JTAG/SWD接口烧录方式需要专用调试器,而IAP(In-Application Programming)技术允许设备通过通信接口(如USART)自行更新程序。本项目基于STM32平台,利用USART的IDLE中断配合DMA实现不定长数据接收,最终完成IAP功能。
核心实现思路:
- 通过DMA将USART接收数据自动搬运到指定内存区域
- 利用USART的IDLE中断检测数据帧结束
- 在内存中校验固件合法性后写入Flash
- 跳转到新固件执行
这种方案相比传统轮询方式具有明显优势:
- DMA自动搬运数据不占用CPU资源
- IDLE中断可准确识别数据包结束
- 支持任意长度固件传输(受限于内存大小)
- 实现真正的后台升级功能
2. 硬件设计与初始化配置
2.1 外设时钟使能
任何外设使用前必须开启对应时钟。本项目中涉及三个主要外设:
c复制RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); // USART1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // GPIOA时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // DMA1时钟
注意:STM32中不同外设挂载在不同总线(APB1/APB2/AHB),必须正确选择对应的时钟使能函数。
2.2 GPIO引脚配置
USART1默认使用PA9(TX)和PA10(RX)引脚:
c复制GPIO_InitTypeDef GPIO_InitStructure;
// PA9配置为复用推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// PA10配置为上拉输入
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_Init(GPIOA, &GPIO_InitStructure);
2.3 USART参数初始化
设置USART通信参数,关键配置包括:
c复制USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600; // 波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 8位数据
USART_InitStructure.USART_StopBits = USART_StopBits_1; // 1位停止位
USART_InitStructure.USART_Parity = USART_Parity_No; // 无校验
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; // 收发模式
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件流控
USART_Init(USART1, &USART_InitStructure);
实际项目中应根据通信距离和干扰情况选择合适的波特率和校验方式。长距离传输建议降低波特率并启用奇偶校验。
3. DMA配置与数据传输机制
3.1 DMA初始化详解
DMA控制器可以自动在外设和内存间搬运数据,显著减轻CPU负担。本项目中DMA配置如下:
c复制void MY_DMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
DMA_InitTypeDef DMA_InitStructure;
// 外设地址(USART1数据寄存器)
DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址固定
// 内存地址(自定义缓冲区)
DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址自增
// 传输方向:外设到内存
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = Size; // 最大传输量
// 普通模式(非循环)
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 非内存到内存
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; // 中等优先级
DMA_Init(DMA1_Channel5, &DMA_InitStructure);
DMA_Cmd(DMA1_Channel5, ENABLE);
}
关键参数说明:
DMA_PeripheralInc:USART数据寄存器地址固定,设为DisableDMA_MemoryInc:接收缓冲区地址需要自动递增,设为EnableDMA_Mode:普通模式(Normal)下传输完成后需要手动重启DMA
3.2 DMA与USART联动配置
启用USART的DMA接收功能:
c复制USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); // 开启USART1的DMA接收
此配置后,USART接收到数据会自动触发DMA传输,完全无需CPU干预。
4. IDLE中断实现不定长接收
4.1 IDLE中断工作原理
USART总线在检测到1字节时间内没有数据传输时会触发IDLE中断。利用这一特性可以准确判断一帧数据的结束,无论该帧数据长度如何。
启用IDLE中断:
c复制USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); // 开启IDLE中断
// 配置NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
4.2 中断服务函数实现
在IDLE中断中完成关键操作:
c复制void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_IDLE) == SET)
{
// 1. 暂停DMA
DMA_Cmd(DMA1_Channel5, DISABLE);
// 2. 计算接收数据长度
applenth = USART_REC_LEN - DMA_GetCurrDataCounter(DMA1_Channel5);
// 3. 重置DMA配置
DMA1_Channel5->CMAR = (uint32_t)g_usart_rx_buf; // 重置内存地址
DMA_SetCurrDataCounter(DMA1_Channel5, USART_REC_LEN); // 重置计数器
DMA_Cmd(DMA1_Channel5, ENABLE); // 重新使能DMA
// 4. 清除IDLE标志(特殊操作)
USART_GetFlagStatus(USART1, USART_FLAG_IDLE);
USART_ReceiveData(USART1);
}
}
特别注意:IDLE标志清除需要先读SR再读DR寄存器,不能使用常规的USART_ClearITPendingBit()函数。
5. Flash编程实现固件存储
5.1 Flash页擦除操作
在写入新数据前必须先擦除对应扇区:
c复制void MY_FLASH_ErasePage(uint32_t PageAddress)
{
__disable_irq(); // 关键操作期间关闭中断
FLASH_Unlock(); // 解锁Flash
while (FLASH->SR & FLASH_SR_BSY); // 等待就绪
FLASH->SR = FLASH_SR_PGERR | FLASH_SR_WRPRTERR; // 清除错误标志
FLASH_ErasePage(PageAddress); // 执行擦除
while (FLASH->SR & FLASH_SR_BSY); // 等待完成
if (FLASH->SR & FLASH_SR_PGERR) {
// 错误处理
}
FLASH_Lock(); // 重新锁定
__enable_irq(); // 恢复中断
}
5.2 数据写入Flash
以半字(16bit)为单位写入数据:
c复制void flash_write(uint32_t waddr, uint16_t *pbuf, uint16_t length)
{
uint16_t i;
__disable_irq(); // 关中断
FLASH_Unlock();
for (i = 0; i < length; i++) {
while (FLASH->SR & FLASH_SR_BSY); // 等待就绪
FLASH->SR = FLASH_SR_PGERR | FLASH_SR_WRPRTERR; // 清错误标志
FLASH_ProgramHalfWord(waddr + i*2, pbuf[i]); // 写入半字
while (FLASH->SR & FLASH_SR_BSY); // 等待完成
if (FLASH->SR & FLASH_SR_PGERR) break; // 错误检查
}
FLASH_Lock();
__enable_irq(); // 开中断
}
Flash写入必须按半字或字对齐,且目标地址必须在已擦除区域。
6. IAP核心逻辑实现
6.1 主程序流程
c复制int main()
{
uint8_t flag=0;
uint8_t key=0;
// 初始化各外设
Delay_Init();
Serial_Init();
MY_DMA_Init((uint32_t)&USART1->DR, (uint32_t)g_usart_rx_buf, USART_REC_LEN);
KEY_Init();
printf("已进入IAP程序...\r\n");
while(1)
{
if (applenth) // 接收到新固件
{
printf("用户程序接收完成!\r\n");
printf("代码长度:%dBytes\r\n", applenth);
applenth=0;
flag=1;
}
key = KEY_Scan(0);
if (key == KEY_PRES_1) // K1按下,写入Flash
{
if (flag) {
flag=0;
printf("开始更新固件...\r\n");
// 检查固件合法性(检查栈顶地址)
if (((*(volatile uint32_t *)(0X20001000 + 4)) & 0xFF000000) == 0x08000000)
{
iap_write_appbin(FLASH_APP1_ADDR, g_usart_rx_buf, applenth);
printf("固件更新完成!\r\n");
}
else {
printf("非FLASH应用程序!\r\n");
}
}
else {
printf("没有可以更新的固件!\r\n");
}
}
if (key == KEY_PRES_2) // KEY2按下,跳转到APP
{
if (((*(volatile uint32_t *)(FLASH_APP1_ADDR + 4)) & 0xFF000000) == 0x08000000)
{
printf("开始执行FLASH用户代码!!\r\n\r\n");
Delay_ms(10);
iap_load_app(FLASH_APP1_ADDR);
}
else {
printf("没有可以运行的固件!\r\n");
}
}
}
}
6.2 APP程序跳转实现
c复制void iap_load_app(uint32_t appxaddr)
{
// 定义函数指针类型
typedef void (*pFunction)(void);
pFunction Jump_To_Application;
// 检查栈顶地址是否合法
if (((*(volatile uint32_t *)appxaddr) & 0x2FFE0000) == 0x20000000)
{
// 设置主堆栈指针
__set_MSP(*(volatile uint32_t *)appxaddr);
// 获取复位向量地址
Jump_To_Application = (pFunction)*(volatile uint32_t *)(appxaddr + 4);
// 跳转到APP
Jump_To_Application();
}
}
7. 关键问题与解决方案
7.1 IDLE中断标志清除问题
问题现象:使用USART_ClearITPendingBit(USART1, USART_IT_IDLE)无法清除IDLE中断标志。
解决方案:必须通过先读SR再读DR寄存器的方式清除:
c复制USART_GetFlagStatus(USART1, USART_FLAG_IDLE);
USART_ReceiveData(USART1);
7.2 Flash操作死机问题
问题现象:直接使用库函数进行Flash编程时系统卡死。
解决方案:
- 在Flash操作期间关闭所有中断
- 每个操作后等待BSY标志清除
- 及时清除错误标志
7.3 DMA传输计数问题
问题现象:第二次传输时DMA计数器不正确。
解决方案:在普通模式下,每次传输完成后需要手动重置:
c复制DMA1_Channel5->CMAR = (uint32_t)g_usart_rx_buf; // 重置内存地址
DMA_SetCurrDataCounter(DMA1_Channel5, USART_REC_LEN); // 重置计数器
7.4 变量定义位置问题
问题现象:头文件中定义的变量导致重复定义错误。
解决方案:所有全局变量应在.c文件中定义,在.h中使用extern声明。
8. 项目优化与扩展建议
8.1 安全性增强
- 固件校验:增加CRC校验或数字签名验证固件完整性
- 回滚机制:保留上一版本固件,新固件异常时自动回退
- 加密传输:对传输的固件进行加密,防止被篡改
8.2 功能扩展
- 断点续传:记录传输进度,中断后可恢复
- 多固件管理:支持存储多个版本的固件
- 远程触发:通过网络或无线信号触发升级流程
8.3 性能优化
- 双缓冲机制:使用两个DMA缓冲区交替工作,提高吞吐量
- 压缩传输:对固件进行压缩,减少传输数据量
- 差分升级:只传输差异部分,大幅减少升级时间
9. 实际应用中的经验总结
- 波特率选择:高波特率(如115200)可缩短升级时间,但长距离传输时需考虑稳定性
- 缓冲区大小:应根据目标固件大小合理设置,太小会导致无法接收完整固件
- Flash寿命:STM32 Flash通常有1万次擦写寿命,频繁升级需考虑磨损均衡
- 中断优先级:USART中断优先级应高于其他非关键中断,避免数据丢失
- 电源管理:升级过程中应确保电源稳定,意外断电可能导致设备变砖
这个项目成功实现了基于USART和DMA的高效IAP方案,在实际产品中已经过验证,稳定可靠。通过合理配置DMA和IDLE中断,CPU负载几乎为零,可以同时处理其他任务。希望这些经验对您的嵌入式开发工作有所帮助。
