news 2026/9/11 13:26:02

STM32F103C8T6 手工Bootloader开发:向量重映射、Flash编程与安全跳转

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103C8T6 手工Bootloader开发:向量重映射、Flash编程与安全跳转

简介:本资源是一套面向嵌入式开发初学者与进阶工程师的STM32F103C8T6专用Bootloader完整实现方案,聚焦固件安全启动、在线升级(IAP)及多通信接口烧录等核心需求。项目覆盖启动模式配置、Flash编程算法、UART/USB协议交互、CRC校验机制、双Bank升级流程等关键知识点,适用于智能硬件OTA升级、工业设备远程维护等实际场景。压缩包共196个文件,含32个C源码(如stm32f10x_flash.c、stm32f10x_usart.c)、33个头文件(h)、34个编译中间文件(o/d)及可执行镜像(axf、hex、bin),结构清晰,便于理解Bootloader内存布局与链接脚本设计;包体大小为4.82MB,兼顾完整性与轻量性。目前已有4098人学习下载,提供可直接编译运行的Keil工程(含uvprojx、uvoptx)、调试配置(dbgconf)、映射文件(map)及汇编列表(lst),助读者快速掌握从向量表重定向、中断接管到应用跳转的全流程实现细节。

1. 为什么 STM32F103C8T6 的 Bootloader 不是“烧完就能用”的功能模块,而是必须亲手重写的关键枢纽?

很多刚接触 STM32F103C8T6 最小系统板的开发者,在完成第一个 LED 闪烁例程后,会自然地想:既然芯片支持 IAP(In-Application Programming),那直接调用STM32F10x_StdPeriph_Driver里的FLASH_Unlock()FLASH_ProgramWord()就能升级固件了?——事实恰恰相反:原厂标准库不提供 Bootloader 框架,所有跳转逻辑、校验机制、通信协议、分区管理都必须从零实现。你手上的 Blue Pill 板(STM32F103C8T6)出厂时 Flash 0x08000000 地址存放的是用户 APP,而 Bootloader 必须被强制烧录到 0x08000000 之前的固定起始地址(如 0x08000000 → 0x08003000),并接管复位向量表重映射(Vector Table Remap)。这决定了它不是“附加功能”,而是整个固件生命周期的入口守门人:它要验证 APP 校验和、响应串口/USB 升级指令、安全擦除旧区、写入新区、校验 CRC、最后跳转到 APP 入口。尤其在工业现场或远程设备中,一次错误跳转或未校验的固件写入,直接导致设备变砖。本文聚焦于基于标准外设库(非 HAL)的纯手工 Bootloader 实现,覆盖启动流程、Flash 操作边界、RCC 时钟重配置、TIM 定时器超时控制、USART 协议解析等真实开发链路,所有代码可直接用于 Keil MDK 5.37+ 或 IAR EWARM 8.50+ 环境。

2. 从复位向量重映射到 APP 跳转:Bootloader 启动流程的四层硬件级控制

2.1 复位后执行路径与向量表重映射的物理约束

STM32F103C8T6 上电复位后,CPU 总是从 Flash 起始地址 0x08000000 取第一条指令(即复位向量),该地址必须存放 Bootloader 的 Reset_Handler。但用户 APP 默认也链接在 0x08000000,因此必须将 Bootloader 固定放置在 Flash 前段(例如 0x08000000–0x08002FFF),APP 放置在后续区域(例如 0x08003000–0x0800FFFF)。关键在于:Bootloader 运行期间,APP 的中断向量表仍位于其自身基址(0x08003000),而 CPU 默认从中断向量表偏移 0x00 处读取 MSP 初始值、0x04 处读取 Reset_Handler 地址。若不重映射,APP 中断将无法响应。标准库提供NVIC_SetVectorTable()函数,但其本质是操作SCB->VTOR寄存器(Cortex-M3 内核寄存器),而 F103 系列需配合SYSCFG->MEMRMP寄存器启用内存重映射(Memory Remap)才能使 VTOR 生效。常见误操作是仅调用NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x3000)却忽略RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_SYSCFG, ENABLE)SYSCFG_MemoryRemapConfig(SYSCFG_MemoryRemap_Flash),导致 APP 启动后中断全失效。

// Bootloader 启动后立即执行的向量表重映射(必须在任何中断使能前完成) void Bootloader_VectTableRemap(void) { RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_SYSCFG, ENABLE); // 使能 SYSCFG 时钟 SYSCFG_MemoryRemapConfig(SYSCFG_MemoryRemap_Flash); // 启用 Flash 重映射模式 NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x3000); // 设置 APP 向量表偏移为 0x3000 字节(12KB) }

提示:0x3000是 APP 在 Flash 中的起始偏移(单位:字节),不是地址。若 APP 链接地址为0x08003000,则偏移 =0x08003000 - 0x08000000 = 0x3000。此值必须与 Keil 工程中 Target 页的 IROM1 起始地址严格一致。

2.2 RCC 时钟重配置:Bootloader 与 APP 的独立时钟域管理

Bootloader 通常使用内部 HSI(8MHz)快速启动,以缩短升级等待时间;而 APP 可能依赖外部 HSE(8MHz 晶振)配置为 72MHz 主频。若 Bootloader 不主动关闭 HSE 并重置 RCC 寄存器,APP 启动时SystemInit()会因 HSE 已就绪而跳过初始化,导致主频异常。正确做法是:Bootloader 在跳转前,强制将 RCC 寄存器恢复至复位默认值,并关闭所有外设时钟。标准库无现成函数,需手动操作:

// 清除 RCC 寄存器,模拟复位状态 void RCC_DeInit(void) { RCC->CR = 0x00000001; // HSI ON, 其他 OFF RCC->CFGR = 0x00000000; // 默认配置 RCC->CIR = 0x00000000; // 关闭所有中断 RCC->APB2RSTR = 0x00000000; // 复位 APB2 外设 RCC->APB1RSTR = 0x00000000; // 复位 APB1 外设 RCC->AHBENR = 0x00000014; // 仅保留 SRAM/FLITF/CKL_EN(必要基础) RCC->APB2ENR = 0x00000000; // 关闭 APB2 时钟 RCC->APB1ENR = 0x00000000; // 关闭 APB1 时钟 RCC->CR &= ~(RCC_CR_HSEON | RCC_CR_PLLON); // 强制关闭 HSE/PLL RCC->CFGR &= ~(RCC_CFGR_SW); // 清除系统时钟源选择 }
2.2.1 时钟切换失败的典型现象与定位方法

当 APP 启动后 LED 不闪烁、串口无输出,首先检查RCC_GetSYSCLKSource()返回值是否为0x08(HSI)而非0x0C(HSE)。若返回0x08,说明 APP 的SystemInit()未成功切换至 HSE,根源往往是 Bootloader 未执行RCC_DeInit()RCC->CR中 HSE 使能位残留。使用 J-Link Commander 连接后执行mem32 0x40021000(RCC_BASE)可直接读取 CR 寄存器值,确认 bit16(HSEON)是否为 0。

2.3 TIM 定时器作为 Bootloader 超时判断的核心机制

Bootloader 必须在上电后等待一段可配置时间(如 1.5 秒),在此期间监听串口是否有升级指令;超时则无条件跳转至 APP。使用SysTick不可靠(因 SysTick 依赖系统时钟,而时钟可能未稳定),应选用独立时钟源的TIM2TIM3。F103C8T6 的TIM2由 APB1 总线驱动,默认预分频系数为 72-1(72MHz/72=1MHz),计数周期设为 1.5 秒需装载值1500000

// 初始化 TIM2 作为 Bootloader 超时定时器(1.5s) void Bootloader_TIM2_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1PERIPH_TIM2, ENABLE); TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period = 1499999; // 自动重装载值(ARR) TIM_TimeBaseStructure.TIM_Prescaler = 71; // PSC=71 → 72MHz/(71+1)=1MHz TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // 使能更新中断 TIM_Cmd(TIM2, ENABLE); } // TIM2 中断服务程序:超时标志置位 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { g_BootloaderTimeoutFlag = 1; // 全局超时标志 TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } }

注意:TIM_Period设为1499999是因为计数器从 0 开始,满 1500000 次溢出才触发中断(即 1.5 秒)。若使用HAL库,__HAL_TIM_SET_AUTORELOAD()参数需减 1,但标准库TIM_TimeBaseStructure.TIM_Period直接填入目标值。

3. 串口协议解析与 Flash 编程:Bootloader 的核心数据通路与存储操作

3.1 USART 接收缓冲区与命令帧解析的防错设计

Bootloader 通过 USART1(PA9/PA10)接收升级指令,常见协议为:0xAA 0x55 <CMD> <LEN> <DATA...> <CRC8>。但实际部署中,串口线干扰、MCU 供电波动会导致帧头丢失或数据错位。不能依赖单次USART_ReceiveData()读取,必须实现环形缓冲区 + 状态机解析。标准库无环形缓冲区支持,需自行定义:

#define RX_BUFFER_SIZE 256 typedef struct { uint8_t buffer[RX_BUFFER_SIZE]; uint16_t head; uint16_t tail; } RingBuffer; RingBuffer rx_buffer = {0}; // USART1 中断接收(必须开启 RXNE 中断) void USART1_IRQHandler(void) { uint8_t data; if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { data = USART_ReceiveData(USART1); // 环形缓冲区写入(忽略溢出) uint16_t next_head = (rx_buffer.head + 1) % RX_BUFFER_SIZE; if (next_head != rx_buffer.tail) { // 未满 rx_buffer.buffer[rx_buffer.head] = data; rx_buffer.head = next_head; } } } // 帧解析状态机(在主循环中调用) uint8_t ParseFrame(uint8_t* frame, uint16_t* len) { static uint8_t state = 0; static uint8_t expect_len = 0; static uint8_t crc_calc = 0; uint8_t byte; while (rx_buffer.head != rx_buffer.tail) { byte = rx_buffer.buffer[rx_buffer.tail]; rx_buffer.tail = (rx_buffer.tail + 1) % RX_BUFFER_SIZE; switch (state) { case 0: // 等待 0xAA if (byte == 0xAA) state = 1; break; case 1: // 等待 0x55 if (byte == 0x55) state = 2; else state = 0; break; case 2: // 读取 CMD frame[0] = byte; state = 3; break; case 3: // 读取 LEN expect_len = byte; *len = expect_len; state = 4; crc_calc = 0xAA ^ 0x55 ^ byte; break; case 4: // 读取 DATA frame[state-3] = byte; crc_calc ^= byte; if (--expect_len == 0) state = 5; break; case 5: // 读取 CRC if (crc_calc == byte) { return 1; // 帧正确 } else { state = 0; // 丢弃错误帧 } break; } } return 0; }
3.1.1 串口波特率自适应的工程实践

不同调试工具(ST-Link Virtual COM、CH340、CP2102)可能设置不同波特率。Bootloader 应支持 9600/115200 双速检测:先以 115200 接收 50ms,若无有效帧则切换至 9600 重试。实现时需在USART_Init()前配置USARTDIV,并利用USART_GetFlagStatus(USART1, USART_FLAG_RXNE)轮询检测首字节到达时间。

3.2 STM32F10x_FLASH 操作的三重校验与扇区擦除边界

F103C8T6 的 Flash 分为 128 个 1KB 扇区(0–127),但APP 区域必须按扇区对齐擦除,且每次擦除前必须验证目标地址是否属于合法扇区范围。标准库FLASH_ErasePage()仅接受 Page Address(即扇区起始地址),若传入0x08003000(APP 起始地址),实际擦除的是扇区 6(0x08001800–0x08001FFF),而非 APP 所在扇区 12(0x08003000–0x08003FFF)。正确做法是:计算目标地址所属扇区号,再转换为扇区起始地址

// 根据地址获取对应扇区起始地址(F103C8T6:每扇区 1KB) uint32_t GetSectorStartAddr(uint32_t addr) { uint32_t sector_num = addr / 0x400; // 0x400 = 1024 return 0x08000000 + (sector_num * 0x400); } // 安全擦除 APP 区域(0x08003000–0x0800FFFF) void SafeEraseAppArea(void) { uint32_t start_addr = 0x08003000; uint32_t end_addr = 0x0800FFFF; FLASH_Unlock(); // 必须先解锁 FLASH_ClearFlag(FLASH_FLAG_BSY | FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); for (uint32_t addr = start_addr; addr <= end_addr; addr += 0x400) { uint32_t sector_addr = GetSectorStartAddr(addr); // 额外校验:确保 sector_addr 在合法范围内(0x08000000–0x0801FFFF) if (sector_addr < 0x08000000 || sector_addr > 0x0801F000) continue; FLASH_Status status = FLASH_ErasePage(sector_addr); if (status != FLASH_COMPLETE) { // 记录错误日志,停止擦除 while(1); } } FLASH_Lock(); }

提示:FLASH_ErasePage()返回FLASH_COMPLETE表示擦除成功,但不保证扇区内容全为 0xFF。实际应用中需在擦除后逐字节读取验证,尤其在高温或老化 Flash 上,部分扇区可能擦除不彻底。

3.3 APP 校验和计算与跳转前的最终确认

跳转前必须验证 APP 的完整性,否则可能执行损坏代码。常用方法是计算 APP 区域(0x08003000–0x0800FFFF)的 CRC32 或累加和。标准库无 CRC32 硬件加速(F103 无 CRC 外设),故采用查表法软件 CRC32:

// CRC32 查表法(多项式 0xEDB88320) const uint32_t crc32_table[256] = { 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, /* ... 256 项,此处省略 */ }; uint32_t CalculateCRC32(uint32_t* data, uint32_t len) { uint32_t crc = 0xFFFFFFFF; uint8_t* p = (uint8_t*)data; for (uint32_t i = 0; i < len; i++) { crc = (crc >> 8) ^ crc32_table[(crc ^ p[i]) & 0xFF]; } return crc ^ 0xFFFFFFFF; } // 跳转前校验 uint32_t app_crc = CalculateCRC32((uint32_t*)0x08003000, 0xD000); // APP 区域长度 53248 字节 if (app_crc != *(uint32_t*)0x08002FFC) { // 假设 CRC 存储在 APP 区末尾 4 字节 // 校验失败,保持 Bootloader 循环 while(1); }

4. Keil 工程配置与链接脚本:让 Bootloader 和 APP 各自安放、互不越界

4.1 启动文件修改与向量表重定位的汇编级控制

Keil 默认使用startup_stm32f10x_md.s,其中向量表定义在.isr_vector段。Bootloader 的向量表必须位于0x08000000,而 APP 的向量表需重定位至0x08003000不能仅靠NVIC_SetVectorTable(),必须修改启动文件中的__Vectors符号地址。在 Bootloader 工程的startup_stm32f10x_md.s中,将:

__Vectors DCD __initial_sp ; Top of Stack DCD Reset_Handler ; Reset Handler DCD NMI_Handler ; NMI Handler ; ... 其余中断向量

替换为:

; Bootloader 向量表(固定在 0x08000000) AREA RESET, DATA, READONLY __Vectors DCD __initial_sp ; Top of Stack DCD Reset_Handler ; Reset Handler DCD NMI_Handler ; NMI Handler ; ... 其余中断向量 ALIGN

并在Options for Target → Linker → Use Memory Layout from Target Dialog取消勾选,手动指定 Scatter 文件。

4.2 Scatter 文件定义 Bootloader 与 APP 的 Flash 分区

创建bootloader_scatter.sct

LR_IROM1 0x08000000 0x00003000 { ; load region size = 12KB ER_IROM1 0x08000000 0x00003000 { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00001000 { ; 4KB RAM .ANY (+RW +ZI) } } LR_IROM2 0x08003000 0x0000D000 { ; APP 区域:12KB–64KB ER_IROM2 0x08003000 0x0000D000 { *(+RO) } RW_IRAM2 0x20001000 0x00001000 { ; APP 使用剩余 RAM .ANY (+RW +ZI) } }

注意:0x0000D000= 53248 字节,即 APP 最大容量。若 APP 编译后超过此值,Keil 链接器报错L6218E: Undefined symbol,此时需增大该值或优化代码。

4.3 APP 工程的启动代码修正:跳过 SystemInit 中的 Flash 等待周期配置

APP 的SystemInit()默认调用SetSysClockTo72(),其中包含FLASH_SetLatency(FLASH_Latency_2)。但 Bootloader 已配置好 Flash 等待周期,APP 若重复配置可能导致总线错误。解决方案:在 APP 的system_stm32f10x.c中注释掉FLASH_SetLatency()调用,或添加运行时判断

// APP 的 SystemInit() 中修改 void SystemInit (void) { // ... 其他初始化 // FLASH_SetLatency(FLASH_Latency_2); // 注释掉此行 // ... 继续其他配置 }

5. 实战排错:J-Link 无法连接、APP 跳转后死机、Flash 擦除失败的三大高频问题

5.1 J-Link 连接失败:Bootloader 占用 SWDIO/SWCLK 引脚的硬件冲突

F103C8T6 的 SWD 调试接口引脚为 PA13(SWDIO)和 PA14(SWCLK)。若 Bootloader 初始化了 GPIOA 时钟并重置了 PA13/PA14 为普通推挽输出,则 J-Link 无法建立连接。根本解决方法是在 Bootloader 初始化代码最前端禁用 PA13/PA14 的复用功能

// Bootloader main() 开头立即执行 void DisableSWDPins(void) { RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入,释放 SWD GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); }

提示:若已因引脚占用导致 J-Link 失联,需短接 BOOT0 引脚至 3.3V,上电后强制进入系统存储器启动模式(System Memory),再用 STM32 ST-LINK Utility 重新烧录 Bootloader。

5.2 APP 跳转后死机:MSP 初始值未从 APP 向量表正确加载

跳转代码常见错误是直接((void (*)(void))app_jump_address)();,但 Cortex-M3 要求先从 APP 向量表首地址(0x08003000)读取 MSP 初始值,再设置__set_MSP(),最后跳转。缺失 MSP 设置会导致堆栈指针指向非法地址,执行第一条指令即 HardFault:

// 正确的 APP 跳转序列 typedef void (*pFunction)(void); pFunction Jump_To_Application; uint32_t JumpAddress; // 1. 从 APP 向量表读取 MSP 初始值 if (((*(__IO uint32_t*)0x08003000) & 0x2FFE0000) == 0x20000000) { // 校验 MSP 是否在 SRAM 范围内 JumpAddress = *(__IO uint32_t*)(0x08003000 + 4); // 获取 Reset_Handler 地址 Jump_To_Application = (pFunction)JumpAddress; __set_MSP(*(__IO uint32_t*)0x08003000); // 设置主堆栈指针 Jump_To_Application(); // 执行 APP }
5.2.1 HardFault 定位技巧:通过 SCB->CFSR 寄存器解码错误类型

若跳转后进入 HardFault,可在HardFault_Handler中读取SCB->CFSR

void HardFault_Handler(void) { uint32_t cfsr = SCB->CFSR; if (cfsr & 0x00000080) { // IACCVIOL: 指令访问违规 // 可能原因:JumpAddress 无效(如 0x00000000) } if (cfsr & 0x00000001) { // INSTR_BUS_ERR: 指令总线错误 // 可能原因:APP 未正确烧录,或 Flash 擦除不完整 } while(1); }

5.3 Flash 擦除失败:写保护位未清除或电压不足的底层信号

FLASH_ErasePage()返回FLASH_TIMEOUT时,常见原因是:① Flash 写保护位(WRP)未清除;② 电源电压低于 2.0V(F103 最小工作电压)。必须在擦除前调用FLASH_WRPPageConfig()解锁所有扇区

// 解锁全部扇区(F103C8T6:扇区 0–127) void UnlockAllFlashSectors(void) { FLASH_WRPPageConfig(FLASH_WRPPage_0, DISABLE); FLASH_WRPPageConfig(FLASH_WRPPage_1, DISABLE); // ... 依次禁用扇区 0–127(标准库宏定义到 FLASH_WRPPage_127) // 或使用循环:for (uint16_t i = 0; i <= 127; i++) FLASH_WRPPageConfig(i, DISABLE); }

同时,用万用表测量 VDDA 引脚电压,确保 ≥2.5V。若使用 LDO 供电,注意其压差特性——输入 3.3V 时,某些 LDO 输出可能仅 2.8V,在高温下进一步跌落。

6. 串口升级协议优化:支持断点续传与双备份分区的轻量级增强方案

6.1 断点续传机制:通过 Flash 末尾存储升级进度标记

标准 Bootloader 在升级中断后需重传全部固件,耗时且不可靠。可在 Flash 最后一个扇区(0x0801F000–0x0801FFFF)预留 16 字节,存储当前已接收字节数与校验和:

#define UPGRADE_STATUS_ADDR 0x0801F000 #pragma pack(1) typedef struct { uint32_t received_bytes; // 已接收字节数 uint32_t expected_crc; // 预期 CRC32 uint32_t timestamp; // 升级开始时间戳(秒) uint8_t status_flag; // 0x01=进行中,0x02=完成,0x00=空闲 } UpgradeStatus; UpgradeStatus* status_ptr = (UpgradeStatus*)UPGRADE_STATUS_ADDR;

升级指令0xAA 0x55 0x01 <LEN>后增加0x01表示“续传模式”,Bootloader 读取status_ptr->received_bytes后,跳过已接收部分,从该偏移继续接收。此方案无需额外 EEPROM,完全利用片上 Flash。

6.2 双备份分区(A/B)的地址映射与切换逻辑

为实现无缝升级,可将 Flash 划分为 A 区(0x08003000–0x08009FFF)和 B 区(0x0800A000–0x0800FFFF),各 28KB。Bootloader 维护一个标志位(存于 0x08002FF8),指示下次启动加载 A 或 B:

#define ACTIVE_FLAG_ADDR 0x08002FF8 #define ACTIVE_A 0x55AA55AA #define ACTIVE_B 0xABBAABBA uint32_t GetActivePartition(void) { uint32_t flag = *(uint32_t*)ACTIVE_FLAG_ADDR; if (flag == ACTIVE_A) return 0x08003000; else if (flag == ACTIVE_B) return 0x0800A000; else return 0x08003000; // 默认 A 区 } // 升级 B 区完成后,写入标志并重启 *(uint32_t*)ACTIVE_FLAG_ADDR = ACTIVE_B; FLASH_ProgramWord(ACTIVE_FLAG_ADDR, ACTIVE_B); NVIC_SystemReset();

注意:双分区需在 Scatter 文件中定义两个独立加载区域,并确保 Bootloader 能识别当前激活分区的向量表位置。此方案将升级风险降至最低——即使新固件崩溃,重启后仍可回退至旧版本。

6.3 串口升级速率瓶颈突破:启用 DMA 接收与校验并行化

标准轮询或中断接收在 115200 波特率下,CPU 占用率超 70%。启用 USART1 的 RX DMA(通道 5)可释放 CPU:

void USART1_DMA_Init(void) { RCC_AHBPeriphClockCmd(RCC_AHBPERIPH_DMA1, ENABLE); DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)rx_dma_buffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = RX_DMA_BUFFER_SIZE; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环模式避免溢出 DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_Init(DMA1_Channel5, &DMA_InitStructure); DMA_Cmd(DMA1_Channel5, ENABLE); USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); }

DMA 接收期间,CPU 可并发计算已接收数据的 CRC32,实现“接收-校验”流水线,将升级吞吐量提升至理论极限(115200/10 ≈ 11.5KB/s)。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 13:24:04

如何用 GraphRAG 的 prompt-tune 命令生成领域适配的索引提示词?

如何用 GraphRAG 的 prompt-tune 命令生成领域适配的索引提示词&#xff1f; 【免费下载链接】graphrag A modular graph-based Retrieval-Augmented Generation (RAG) system 项目地址: https://gitcode.com/GitHub_Trending/gr/graphrag GraphRAG 的索引流水线默认使用…

作者头像 李华
网站建设 2026/9/11 13:21:46

低功耗开发入行地图:安卓与嵌入式功耗优化核心知识梳理

做功耗这行时间长了&#xff0c;经常有刚毕业或者想转方向的朋友问我&#xff1a;“低功耗开发到底是干什么的&#xff1f;是不是就是让手机待机时间长一点&#xff1f;”说实话&#xff0c;这个理解不算错&#xff0c;但离真实的岗位需求差得有点远。功耗优化不是某一项具体技…

作者头像 李华
网站建设 2026/9/11 13:18:09

G-Helper 完整指南:华硕笔记本三大核心控制与避坑验收清单

G-Helper 完整指南&#xff1a;华硕笔记本三大核心控制与避坑验收清单 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops with nearly the same functionality. Works with ROG Zephyrus, Flow, TUF, Strix, Scar, ProArt, Vivobook, Zenbook…

作者头像 李华