STM32F103_Bootloader_从零到UART升级成功_博客原稿

STM32F103RCT6 自定义 UART Bootloader 完整实战教程

从零创建工程、划分 Flash、修改 Com APP,到使用 Python 成功升级 74,016 字节固件

工程实录 / Windows 10 + STM32CubeIDE + STM32CubeProgrammer + VSCode + CH340C

记录日期:2026-11-01;适用型号:STM32F103RCT6。

适用范围:当前这套"PC USB → CH340C → F103 USART2 → Flash"的实测方案;不是 F401 中转升级方案,也不是 STM32 出厂 ROM Bootloader 的教程。

总 Flash Bootloader 预留 Com APP 可用 已实测文件
256 KiB 32 KiB 224 KiB 72.28 KiB

本文给出了:从初学者出发的原理解释、CubeIDE 逐项设置、链接脚本修改、向量表设置、Bootloader 关键 C 代码、串口协议、完整的实测 Python 脚本、串口助手试验、Flash 回读、故障排查、上线前风险检查。

验证范围先讲清楚 :用户实际升级日志已证明 V/B1 → S/K → 290 个 D/K → F/K;Com.bin 74,016 字节,CRC32 为 9F551106。这不等于已经证明应用的 CANopen 功能正常,也不等于已经独立完成本轮升级后的全量 ST-LINK 回读比对。 后两项在文中作为独立验收步骤。

阅读路线(适合第一次做 Bootloader)

阶段 要做什么 完成标志
认识原理 理解 Bootloader / APP / bin / Flash 能画出两个程序所在地址
配置底层 CubeIDE、引脚、时钟、串口 Bootloader 编译通过
改 APP 链接地址、向量表、bin 输出 Com.bin 前 8 字节合法
烧录与单项验证 ST-LINK、串口助手 V/S/D 得到 B1,APP 能擦写
正式升级 虚拟环境、pyserial、Python 脚本 F 返回 K
严格验收 回读比对、跳转、APP 功能 逐字节相同且 APP 运行正常

01 为什么需要自定义 Bootloader?

常规 STM32 程序每次通过 ST-LINK 下载,适合开发阶段。设备装进机箱后,现场可能不方便接 SWD。Bootloader(引导程序)是一个先于业务应用运行的小程序:它等待上位机升级请求,负责接收固件、擦除指定 Flash、写入新固件、校验、再启动业务程序。

本方案把 F103 中的程序分为两部分:

  • Bootloader:负责 USART2 收命令、接收升级文件、擦除 APP、分包写 Flash、CRC32 检查、跳转。
  • Com APP:原有业务程序,例如 CANopen。本文不修改其业务逻辑,只改变它放在 Flash 的位置以及中断向量位置。

重要区分:STM32 芯片还有厂商提供的 System Memory ROM Bootloader ,那是另一个东西。本项目是用户自己编写的、存放在 0x08000000 的 Bootloader。PC 端跑的 upgrade_com.py 也是为本项目自定义通信协议编写的,不能直接拿 ST 官方串口下载工具替代。

一次正常上电

text 复制代码
F103 上电/复位
    |
    v
执行 Flash 0x08000000 的 Bootloader
    |
    +-- 收到 V --> 返回 B1
    +-- 收到 S --> 擦除 APP,进入升级状态
    |                 |
    |                 +-- 多个 D --> 写入数据
    |                 +-- F --> 验证 CRC,最后写向量表
    +-- 收到 J --> 检查有效后,跳转 APP
    |
    +-- 未升级且 APP 有效,3 秒窗口结束 --> 自动跳转 APP

Com.bin 不是"自动运行的文件"。它只是连续的机器码字节流;只有 链接地址正确、写入位置正确、启动向量正确,CPU 才能启动它。


02 本项目硬件与软件(不要把它当通用配置硬套)

项目 本次实测设备 / 配置
MCU STM32F103RCT6,Arm Cortex-M3,256 KiB Flash,48 KiB SRAM
PC 到 MCU PC USB → 板载 CH340C USB-UART → F103 USART2
USART2 PA2 = TX,PA3 = RX;115200 bps,8 数据位,无校验,1 停止位(8N1)
电平 板载 CH340C 的 UART 侧为 3.3 V 电平;以原理图实际供电和连线为准
板上串联电阻 R75 / R76 各约 33 Ω;按照自己板子的原理图核对
烧录/调试 ST-LINK/V2,SWDIO、SWCLK、GND(及正确的目标供电/参考电压)
时钟 本项目使用 HSE 8 MHz,PLL ×9,SYSCLK 72 MHz;APB1 36 MHz
软件 STM32CubeIDE、STM32CubeProgrammer、VSCode、Python 3.15(uv 管理)、pyserial
本次可用串口 COM7;这是当时 Windows 分配的值,其他电脑/USB 插口可能改变

接线的实际方向

text 复制代码
PC USB --- CH340C --- USART2 --- STM32F103RCT6
             TXD  ------------> PA3 / USART2_RX
             RXD  <------------ PA2 / USART2_TX
             GND  ------------- GND

如果自己外接 USB-UART,务必交叉 TX/RX,不要用 RS-232 的 ± 电压直接接 MCU。如果板载 CH340C 已焊接,使用其 Type-C 接口即可,不要重复接第二个串口转换器。

关于 BOOT0 :一般正常从用户 Flash 启动时,需确认启动配置选择主 Flash;该项目依赖 MCU 先从 0x08000000 运行自定义 Bootloader。实际 BOOT 管脚组合以板子的硬件设计为准。


03 Flash / RAM 内存怎么分配?

这一步决定整个项目能否正常跳转,是最容易出错的地方。

3.1 先算清楚总容量

STM32F103RCT6 的主 Flash 从 0x08000000 开始,共 256 KiB,也就是 0x40000 字节:

text 复制代码
Flash 起点                         Flash 末尾(不含)
0x08000000 ------------------------ 0x08040000
|<- Bootloader 0x8000 ->|<--- APP 0x38000 --->|
0x08000000             0x08008000             0x08040000
|    32 KiB            |      224 KiB          |
|    16 页 (2KiB/页)   |      112 页            |
区域 起始地址(包含) 末尾地址(包含) 长度(字节 / KiB)
Bootloader 0x08000000 0x08007FFF 0x8000 = 32768 = 32 KiB
Com APP 0x08008000 0x0803FFFF 0x38000 = 229376 = 224 KiB
全 Flash 0x08000000 0x0803FFFF 0x40000 = 262144 = 256 KiB
SRAM 0x20000000 0x2000BFFF 0xC000 = 49152 = 48 KiB

FLASH_LIMIT = 0x08040000 是末尾之后的第一个地址,不是能够写入的地址。把它设成"不包含上界"可以更方便做地址范围检查。

特别提醒 :32 KiB 只是给 Bootloader 预留的区域 ,不意味着它本身占满 32 KiB。以前备份的某版 Bootloader.bin 约 4.4 KiB;预留 32 KiB 是为了后续扩展并让 APP 起始地址固定。

3.2 为何 APP 必须从 0x08008000 开始?

因为 0x08000000 ~ 0x08007FFF 已经留给 Bootloader。Com 必须链接到后面的区域。不能仅在烧录时把原来链接到 0x08000000 的 bin 偏移下载到 0x08008000;那样程序内部中断向量和跳转地址仍可能指向错误位置。

APP 原本可能是:

text 复制代码
原 Com 链接地址:0x08000000
改后 Com 链接地址:0x08008000

这需要修改 APP 项目的链接脚本 .ld ,并处理 向量表 SCB->VTOR,两者都不能少。

3.3 RAM 要不要也拆成两份?

本项目不需要。Bootloader 和 Com 不是并行运行;跳转后 Com 会使用自己的初始栈及 RAM 变量。所以两个项目都可以配置为:

ld 复制代码
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 48K

本次 Com.bin 的前四字节对应 0x2000C000,恰好是 48 KiB SRAM 的栈顶地址,这是正常的。栈顶是末尾后的地址,不是 RAM 内要存程序的最后一个字节。

3.4 Flash 页大小为什么是 2048?

本型号属于 STM32F103 高密度系列,主 Flash 按 2 KiB 一页擦除,因此:

c 复制代码
#define PAGE_BYTES   2048UL
#define APP_CAPACITY (0x08040000UL - 0x08008000UL)
// 229376 / 2048 = 112 页

切记:不能把别的 F103 型号的 1 KiB 页设置直接套过来。 F103 不同容量密度的页大小不同,量产前按芯片完整型号和数据手册再次确认。


04 先创建 Bootloader 工程:STM32CubeIDE 从零设置

4.1 新建工程

  1. 打开 STM32CubeIDE,选择 File → New → STM32 Project。
  2. 在 MCU/MPU Selector 中搜索 STM32F103RCT6,不要误选 F103C8T6、F103RB 等不同容量芯片。
  3. 创建工程,例如 F103_Bootloader;选择 STM32Cube HAL 方案,保存工程。
  4. 打开工程 .ioc 配置界面。CubeIDE 界面细节可能随版本不同,但下列功能名称相同或相近。

4.2 设置调试引脚

在 .ioc 中找到 System Core → SYS:

  • Debug 选 Serial Wire,给 ST-LINK 保留 SWDIO / SWCLK。
  • 如果硬件有外部 8 MHz 晶振,在 System Core → RCC 中将 High Speed Clock (HSE) 设为与硬件一致的晶振模式;如果硬件不是晶振而是输入时钟,不能直接照抄 Crystal 选项。

4.3 设置 USART2

在 Connectivity → USART2 中:

参数 配置
Mode Asynchronous
Baud Rate 115200
Word Length 8 Bits
Parity None
Stop Bits 1
Hardware Flow Control None
TX / RX 默认映射 PA2 / PA3,核对 Pinout 无冲突

本项目使用轮询式 HAL_UART_Receive(),不依赖 UART DMA、中断接收,也不需要把 USB 外设设成 CDC。PC 侧虽然插 Type-C,MCU 实际收到的仍是 USART2 数据。

4.4 配置时钟

根据本项目的外部晶振配置:

text 复制代码
HSE = 8 MHz
PLL source = HSE
PLL multiplier = ×9
SYSCLK = 72 MHz
HCLK = 72 MHz
APB1 = 36 MHz(预分频 /2)

CubeIDE 中若时钟树标红,先按实际硬件修正。F103 的 APB1 不能直接照搬成 72 MHz。完成 Pinout 与 Clock Configuration 后生成代码。

4.5 修改 Bootloader 链接脚本

在 Bootloader 工程根目录找到生成的 STM32F103RCTX_FLASH.ld 或类似命名文件(实际以工程文件为准),定位 MEMORY:

ld 复制代码
MEMORY
{
  RAM (xrw)   : ORIGIN = 0x20000000, LENGTH = 48K
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 32K
}

只修改对应 MEMORY 容量/起点,不要不加区分地删除原链接脚本的 .isr_vector、.text、.data 等段定义。检查构建输出没有 region FLASH overflowed。

4.6 新增 boot_upgrade.h 与 boot_upgrade.c

工程中添加:

text 复制代码
F103_Bootloader/
  Core/
    Inc/
      main.h
      boot_upgrade.h       <- 新增
    Src/
      main.c
      boot_upgrade.c       <- 新增
  STM32F103RCTX_FLASH.ld

boot_upgrade.h 内容:

c 复制代码
#ifndef BOOT_UPGRADE_H
#define BOOT_UPGRADE_H
#include "main.h"
void Boot_Run(UART_HandleTypeDef *huart);
#endif

boot_upgrade.c 的关键函数在第 07~10 章讲解,完整参考整理版 另附源文件。来源说明:该参考 C 文件来自项目现有协议代码,并加入了边界检查与 30 秒测试超时;新增的防护修改没有重新在你的板子上单独烧录验证,不能说它与原实测二进制完全一致。

4.7 main.c 中在哪里调用?

只把下面的代码放进 CubeIDE 的 USER CODE 区域,避免后续重新生成 .ioc 时被覆盖。

c 复制代码
/* USER CODE BEGIN Includes */
#include "boot_upgrade.h"
/* USER CODE END Includes */

找到 main() 初始化的位置,确保 USART2 初始化后再调用:

c 复制代码
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART2_UART_Init();

/* USER CODE BEGIN 2 */
Boot_Run(&huart2);   // 常驻循环;正常情况下不会返回
/* USER CODE END 2 */

Bootloader 工程的 .ld 仍是 0x08000000 。注意不要把 Bootloader 和 Com 的 .ld 改反。


05 修改 Com APP 工程:链接地址 + 中断向量表

这是整个项目最关键的一章。 有人升级数据显示全部成功,但程序不运行,常见原因就在这里。

5.1 先备份现有 Com 工程

建议另存一份工程,例如 Com_before_bootloader,确保原始工程可以随时恢复。只改和 Bootloader 兼容所需的启动地址,不要在第一次联调时同时重构 CANopen 功能代码。

5.2 修改 Com 的链接脚本 .ld

在 Com 工程 的 .ld 找 MEMORY,改为:

ld 复制代码
MEMORY
{
  RAM (xrw)   : ORIGIN = 0x20000000, LENGTH = 48K
  FLASH (rx)  : ORIGIN = 0x08008000, LENGTH = 224K
}

这个设置使 Com 的 .isr_vector 起始于 0x08008000,后面的 .text/.rodata 也按新的 Flash 地址链接。APP 不得超过 224 KiB,否则编译链接时应报空间不足。

5.3 修改 Com 的向量表偏移:新版 CubeF1 写法

进入 Com 工程 的 Core/Src/system_stm32f1xx.c,搜索:

c 复制代码
/* #define USER_VECT_TAB_ADDRESS */

在使用 ST 新版 CMSIS 模板时,取消注释,并设置:

c 复制代码
#define USER_VECT_TAB_ADDRESS
#define VECT_TAB_OFFSET 0x00008000U

新版模板的 SystemInit() 内通常有:

c 复制代码
#if defined(USER_VECT_TAB_ADDRESS)
SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET;
#endif

前提是 VECT_TAB_BASE_ADDRESS 为 FLASH_BASE。最终效果必须是:

c 复制代码
SCB->VTOR == 0x08008000U

不同 STM32CubeF1 版本可能不同 :有的旧模板没有 USER_VECT_TAB_ADDRESS 这个开关,而是直接使用 VECT_TAB_OFFSET。因此要检查实际文件的 SystemInit() 源码,不能只复制宏定义却不确认它被使用。

另一个等效验证办法是在 APP 很早的启动阶段、使能中断前 设置 SCB->VTOR = 0x08008000U;但不要不分析就同时加入多处相互冲突的重定位代码。最可靠的是让 SystemInit() 使用正确偏移,并在调试时读取 VTOR 寄存器确认。

5.4 为什么 Bootloader 设置一次 VTOR 还不够?

跳转函数 jump_app() 会先设置 SCB->VTOR = APP_BASE,但是 Com 的启动过程可能再次执行 SystemInit() 并覆盖向量表设置。所以 Com 项目自己也必须知道它的向量位置。这也是"主循环能跑,但定时器 / CAN / UART 中断异常"的重要排查点。

5.5 打开 .bin 文件输出

在 Com 项目 右键 Properties → C/C++ Build → Settings → Tool Settings → MCU Post build outputs,勾选:

text 复制代码
Convert to binary file (-O binary)

重新 Build 后,通常在工程的 Debug 或 Release 输出目录生成 Com.bin(具体名称以工程名和构建配置为准)。

  • .elf:含段地址、调试符号,适合 ST-LINK 调试。
  • .bin:纯字节流,无嵌入的烧录起始地址,正是这个 Python 脚本需要的文件。
  • .hex:通常含地址信息,但本文协议不直接接收 Intel HEX。

5.6 检查生成的 Com.bin 前 8 字节

本次实际文件的开头是:

text 复制代码
00 C0 00 20   B5 0B 01 08
|  4字节SP  | |4字节Reset|

按小端序解释:

text 复制代码
SP    = 0x2000C000  (RAM 栈顶,合法)
Reset = 0x08010BB5  (位 0 为 1,Thumb 入口)

注意 Reset 的最低位 1 是 Cortex-M Thumb 状态标志,它代表实际指令地址 0x08010BB4;不是地址写错。

如果文件开头是 FF FF FF FF ...,或者复位入口仍然指向旧 APP 布局,先解决工程链接和 bin 生成,再尝试串口升级。


06 Bootloader 的"自定义参数"集中在哪里?

boot_upgrade.c 顶部定义了一组宏,理解它们比一开始死记代码更重要。

c 复制代码
#define APP_BASE       0x08008000UL
#define FLASH_LIMIT    0x08040000UL
#define APP_CAPACITY   (FLASH_LIMIT - APP_BASE)
#define SRAM_BASE      0x20000000UL
#define SRAM_LIMIT     0x2000C000UL
#define PAGE_BYTES     2048UL
#define PACKET_BYTES   256U
#define UART_TIMEOUT   30000U
宏 含义 什么时候改
APP_BASE APP 第一字节的地址 修改分区方案时,与 APP .ld 同步
FLASH_LIMIT Flash 末尾的下一地址 MCU Flash 容量变化时
APP_CAPACITY APP 最大文件长度 自动计算,勿凭感觉填写
SRAM_BASE/LIMIT 判断启动向量的栈指针是否落在有效 RAM MCU RAM 容量变化时
PAGE_BYTES 页擦除粒度 按 MCU 型号查数据手册
PACKET_BYTES 每个 D 包的数据上限 256 字节 PC / MCU 协议必须一致
UART_TIMEOUT S/D 接收指定字节数的超时,单位 ms 实测用 30000(30 秒)

三个不同"时间"不要混淆:

  1. UART_TIMEOUT = 30000U:接收 S 元数据、D 长度或 D 数据时的单次等待上限,30 秒。
  2. timeout = 100U:主循环轮询"下一个命令字节"的单次等待上限,100 ms。
  3. HAL_GetTick() - start >= 3000U:Bootloader 初次上电后的 APP 自动跳转窗口,3 秒。

S 已回 K 到下一次 D 之间没有固定的 30 秒时限 ;在 MCU 不复位、升级状态仍有效的前提下可以稍后发送。UART_TIMEOUT 只在对应 HAL_UART_Receive() 被调用时计时。


07 自定义 UART 通信协议(PC 和 STM32 必须完全一致)

协议不是 HAL 库自带的,由本项目的 Boot_Run 自己解析。

命令 首字节 HEX 后续数据 正常回复
V 版本查询 56 无 42 31(ASCII B1)
S 开始升级 53 size 4 字节 + crc32 4 字节 4B(K)
D 数据包 44 len 2 字节 + len 字节固件数据 4B(K)
F 完成升级 46 无 4B(K)
J 运行 APP 4A 无 4B 后尝试跳转

失败一般返回 45(ASCII E)。V 返回两字节 B1,而其他成功回复一般是单字节 K,不能把 B1 按一个字节读取。

7.1 S 的 8 字节元数据

S 的包结构:

text 复制代码
+------+-----------------------+-----------------------+
| 'S'  | firmware size (4B LE) | firmware CRC32 (4B LE)|
+------+-----------------------+-----------------------+
 0x53     小端序                小端序

本次实测 Com.bin 长度 74016 = 0x00012120;CRC32 0x9F551106。小端编码为:

text 复制代码
size  = 20 21 01 00
CRC32 = 06 11 55 9F

所以针对这一次确切的 Com.bin ,完整 S 数据为:

text 复制代码
53 20 21 01 00 06 11 55 9F

这 9 字节会触发 APP 整区擦除,发之前必须有备份。不允许拿这个固定 CRC 头去升级后来重新编译的其他文件,文件内容变了,CRC 就可能变。

7.2 D 的数据包结构

text 复制代码
+------+------------------+--------------------------+
| 'D'  | len (2B little)  | 固件原始 data(1~256B) |
+------+------------------+--------------------------+
 0x44

发送一包,等到 K,再发下一包。每包都包含 D 和自己的 len,不能把整个 Com.bin 当成裸数据一次"文件发送"。

本次 74016 字节按 256 字节分包为:

text 复制代码
289 包 × 256 字节 = 73984 字节
最后 1 包 × 32 字节 = 32 字节
总计 290 包

STM32F1 的 Flash 按 16 位半字 写,所以本项目要求每个非最后数据包的长度为偶数;最后一包可以有 1 字节尾数,由代码补 0xFF 对齐。最省事就是由脚本固定每包最多 256 字节。

7.3 CRC32 的算法必须完全一致

STM32 代码使用:

c 复制代码
crc = 0xFFFFFFFFUL;         // 初始值
// 每个字节参与 CRC,反射多项式 0xEDB88320
final_crc = crc ^ 0xFFFFFFFFUL;

Python 使用:

python 复制代码
crc = zlib.crc32(firmware) & 0xffffffff

两者针对完整的 Com.bin 所有字节 算出的结果一致。这里使用的是常见的反射 CRC-32(IEEE/ZIP),并非直接调用 STM32 外设 CRC 单元默认算法,因为默认多项式/输入处理约定可能不同。


08 Bootloader 关键 C 代码:怎么擦除?怎么写入?

8.1 升级函数顺序

text 复制代码
Boot_Run()
  |
  +-- 'S' -> begin_upgrade() -> erase_app()
  |
  +-- 'D' -> receive_packet() -> program_halfwords()
  |
  +-- 'F' -> finish_upgrade() -> crc_step()
  |
  +-- 'J' -> jump_app()

8.2 真正擦除 APP 的代码

c 复制代码
static int erase_app(void)
{
    FLASH_EraseInitTypeDef cfg = {0};
    uint32_t page_error = 0;
    cfg.TypeErase = FLASH_TYPEERASE_PAGES;
    cfg.PageAddress = APP_BASE;
    cfg.NbPages = APP_CAPACITY / PAGE_BYTES; // 112 pages
    return HAL_FLASHEx_Erase(&cfg, &page_error) == HAL_OK;
}

它擦的是从 APP_BASE 开始的 112 页 = 224 KiB ,不是只擦除本次文件的 72 KiB。调用前必须 HAL_FLASH_Unlock(),结束后 HAL_FLASH_Lock()。S 回复 K 是"固件报告本次擦除函数成功",如要独立证明某个地址真正变成 FF,用 STM32CubeProgrammer 读取 Flash。

为什么不能 Full Chip Erase? 因为整片擦除会把 0x08000000 的 Bootloader 一并删掉;设备接下来就无法靠这套 Bootloader 做远程升级,必须借助 ST-LINK 等恢复。

8.3 把 RAM 收到的字节写进 Flash

STM32F103 Flash 半字编程:每次写 uint16_t(2 字节),地址需要 2 字节对齐。示意核心:

c 复制代码
uint16_t half = src[i];
half |= (uint16_t)((i + 1U < n ? src[i+1] : 0xFFU) << 8);
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, address+i, half) != HAL_OK)
    return 0;
if (*(volatile uint16_t *)(address+i) != half)
    return 0;

建议的边界保护(当前最终粘贴代码的地址判断不够完整;下面是建议修正,须重新编译/上板确认):

c 复制代码
if ((address & 1U) != 0U ||
    address < APP_BASE ||
    address >= FLASH_LIMIT ||
    n == 0U ||
    n > FLASH_LIMIT - address ||
    ((n & 1U) != 0U && n == FLASH_LIMIT - address)) {
    return 0;
}

为什么要先检查 address < APP_BASE / address >= FLASH_LIMIT?因为 FLASH_LIMIT - address 是无符号减法 ,如果地址已越界,减法可能下溢而绕回很大的数,错误数据可能侥幸通过仅有的长度检查。千万不要在 APP 下载功能里省略地址范围保护。

8.4 为什么前 8 字节最后写?

Com.bin 的前 8 字节是:

  1. 初始栈指针 MSP(4 字节)。
  2. Reset_Handler 复位入口(4 字节)。

如果先写它们、后来升级断电,下一次启动时 Bootloader 可能认为 APP"看起来有效",但其他指令还没写完。当前方案先将前 8 字节暂存:

c 复制代码
static uint8_t vector_bytes[8];

uint32_t skip = received < 8U ? 8U - received : 0U;
if (skip > n) skip = n;
if (skip) memcpy(vector_bytes + received, packet, skip);

最后通过 F 完成所有检查,才把 vector_bytes 写到 APP_BASE。这能减少"半升级 APP 被误启动"的风险,但不能完全代替持久化的升级有效标志和容错机制。

8.5 F 命令检查了什么?

text 复制代码
F 到来
 -> updating == 1 ?
 -> received == file_size ?
 -> 接收流 CRC32 == expected_crc ?
 -> 栈指针/Reset 向量合理 ?
 -> 拼接"前 8 字节+Flash 中其他数据"计算 CRC 正确 ?
 -> 最后写入前 8 字节启动向量
 -> 从 Flash 整体回读计算 CRC32 正确 ?
 -> 返回 K,否则 E

这也是为什么只做完 D 不能称作"升级成功"。D 的 K 只表示这一包 通过,只有 F 返回 K 才表示 Bootloader 确认整个文件大小和 CRC 校验成功。


09 Bootloader 如何跳到 Com?上电 3 秒又是什么?

9.1 查启动向量是否有效

c 复制代码
static int app_valid(void)
{
    uint8_t v[8];
    memcpy(v, (const void *)APP_BASE, sizeof(v));
    return valid_vectors(v);
}

valid_vectors() 主要检查栈指针落在 SRAM 范围且 8 字节对齐、Reset 入口位于 APP Flash 区并设置 Thumb 位。这个检查不等于整个 APP 的 CRC 检查。 它只能判断"入口看起来合理"。

9.2 这份最新源码恢复了 3 秒自动跳转

c 复制代码
uint32_t start = HAL_GetTick();
while (1) {
    uint8_t cmd;
    uint32_t timeout = 100U;
    if (rx(&cmd, 1, timeout) != HAL_OK) {
        if (!updating && app_valid() &&
            HAL_GetTick() - start >= 3000U)
            jump_app();
        continue;
    }
    // V/S/D/F/J 命令处理... 
}

不是每次接到命令后重新数 3 秒 ,而是从进入 Boot_Run() 时记录的 start 开始计时。只有主循环本轮 rx() 没收到命令、且 updating==0、且 APP 向量有效,才会自动跳转。

有个容易被忽略的实际后果:如果整次升级已花费超过 3 秒,F 成功将 updating 清零后,下一次 100ms 等待命令超时,就可能立刻自动跳到 Com 。因此 Python 显示"暂不发送 J",只是说 Python 没主动发 J,不能据此推断 MCU 一定继续留在 Bootloader。

9.3 jump_app() 的主要动作

text 复制代码
读取 APP_BASE 的 MSP / Reset
停用 Bootloader 侧 UART 与 HAL、恢复时钟状态
关中断、停 SysTick、清 NVIC 使能和挂起位
SCB->VTOR = APP_BASE
设置 MSP = APP 新栈顶
跳转到 APP Reset_Handler(Thumb 地址)

下面是本方案的核心跳转语句(完整上下文在附带参考 C 文件):

c 复制代码
SCB->VTOR = APP_BASE;
__DSB();
__ISB();
__asm volatile (
    "msr msp, %0\n"
    "cpsie i\n"
    "bx %1\n"
    : : "r" (sp), "r" (reset) : "memory");

这不是调用普通 C 函数。跳转后,主程序会从自己编译产生的 Reset_Handler 开始执行,配置自己的 .data/.bss、时钟和外设。若 APP 启动仍异常,先确认链接地址、向量表、时钟初始化和中断配置,而不是重新猜测 UART 升级没成功。

9.4 我该用 3000、1000 还是 500ms?

等待窗口 优点 风险/建议
3000 ms 开发调试方便、Python 有充分时间握手 设备上电后 APP 运行慢,当前验证阶段适合
1000 ms 平衡 仍须测试 PC 端能否及时进入升级
500 ms 业务启动快 PC 开串口/USB 枚举可能赶不上,不建议仅靠时间窗口做升级

对于你的工业 CANopen 从站,后续可采用 APP 接到升级请求 → 写升级标志/握手 → 软复位进入 Bootloader 的专用模式。这个机制目前还没写进此次实测代码,不要把它当作已实现功能。


10 第一次烧录与串口助手单项测试

10.1 先把 Bootloader 烧到 0x08000000

  1. 用 ST-LINK 接 F103 的 SWD 接口,打开 STM32CubeProgrammer。
  2. 建立连接,确认识别出正确的 STM32F103 芯片。
  3. 首次部署时将 Bootloader 的 .elf/.hex 或 .bin 写到 0x08000000(使用 .bin 要明确输入下载地址)。
  4. 后续更新 Com 不能再使用整片擦除 ,尤其不要随手勾选 Full Chip Erase。
  5. 烧录 Bootloader 后复位,进入串口助手测试 USART2。

首次部署允许在完全确认备份和恢复方式后重新规划整个 Flash;但一旦 Bootloader 有用了,日常 Com 升级必须保护 0x08000000 ~ 0x08007FFF。

10.2 V:验证 Bootloader 和串口

串口助手配置 115200、8N1、HEX 发送,发送:

text 复制代码
56

应收到:

text 复制代码
42 31    // ASCII B1

当时先选 COM12 得到异常响应,后来在 COM7 成功。串口号应以当前设备管理器为准,不要凭记忆固定。

10.3 S:测试擦除(破坏性操作)

只有准备好备份和恢复条件才执行。 测试用假元数据:

text 复制代码
53 00 00 01 00 00 00 00 00

意思是:S + 假设文件 65536 字节 + 测试 CRC = 0。它会触发 APP 擦除,成功时返回 4B。这个头不是用于正式 Com.bin 的,后面的 F 无法用少量测试字节通过。

10.4 D:测试半字写入

在S 已成功返回 K,且 MCU 未复位的前提下,串口助手一次性 HEX 发送:

text 复制代码
44 0A 00 00 C0 00 20 01 81 00 08 11 22

这里:44 就是 ASCII D,不用另外再发字母 D 。0A 00 表示后续 10 字节;前 8 字节作为测试向量暂存,11 22 被写入 0x08008008。用户实际收到 K,并通过 STM32CubeProgrammer 看到了:

text 复制代码
0x08008000: FFFFFFFF
0x08008004: FFFFFFFF
0x08008008: FFFF2211

这是 32 位显示格式与小端字节顺序共同形成的结果,并非把 11 22 写反了。此时不能发送 F 期望通过,因为从 65536 字节中只发送了 10 字节。

实测截图:0x08008008 为 FFFF2211;这是手工测试图,不是本轮正式 Com.bin 升级后的完整回读证据。

10.5 为什么串口助手一发 S 或 D 就收到 E?

S 后 MCU 要接收 8 字节元数据 。只发送单个 53,超时后就会 E。D 也是一样,必须提供 2 字节长度 + 对应数量的数据。手工方式最好一次点击发送完整帧,而不是先发一个字符再慢慢打剩余字节。

不要在 S 得到 K 与 D 之间通过 ST-LINK 让 MCU 复位 。升级状态 updating 保存在 RAM,复位后就丢了。观察 Flash 可以等完整升级结束后再做。


11 Windows + VSCode + Python 环境配置(实测遇到的问题)

11.1 为什么 py 和 python 都不能直接用?

本次 Windows 上的 py -m pip install pyserial 报错:

text 复制代码
py : 无法将"py"项识别为 ...

python --version 也报了"系统无法访问此文件"。这并不能推出 Python 根本没安装。VSCode 实际检测到由 uv 管理的 CPython 3.15.0,路径形如:

text 复制代码
%APPDATA%\uv\python\cpython-3.15.0-windows-x86_64-none\python.exe

实测截图:VSCode 中可选择 uv 安装的 Python 3.15.0。

尝试直接全局 pip install pyserial 后又遇到了:

text 复制代码
error: externally-managed-environment
This Python installation is managed by uv

正确做法是:在升级项目文件夹创建独立 .venv 虚拟环境,不要为了安装一个串口库去破坏全局 uv 管理环境。

11.2 VSCode 中准备目录

例如:

text 复制代码
C:\Users\Administrator\Desktop\xiazai\
  Com.bin
  upgrade_com.py
  .venv\          <- 创建后出现

打开 VSCode,选择 File → Open Folder,打开这个 xiazai 文件夹;在 Terminal → New Terminal 打开终端。

11.3 在 PowerShell 创建虚拟环境

如果 VSCode 已能选择 uv 管理的 Python,仍可按本次实际路径执行:

powershell 复制代码
& "$env:APPDATA\uv\python\cpython-3.15.0-windows-x86_64-none\python.exe" -m venv .venv

之后不需要激活脚本,直接调用虚拟环境 Python:

powershell 复制代码
.\.venv\Scripts\python.exe -m pip install pyserial

如果提示没有 pip:

powershell 复制代码
.\.venv\Scripts\python.exe -m ensurepip --upgrade
.\.venv\Scripts\python.exe -m pip install pyserial

验证:

powershell 复制代码
.\.venv\Scripts\python.exe -c "import serial; print(serial.__version__)"

本次脚本用的是 pyserial,Python 代码中 import serial 是正常的。不要装一个不相关的名为 serial 的包来代替。

11.4 找当前 COM 串口

设备管理器 → 端口 (COM 和 LPT) → 找 USB-SERIAL CH340 (COMx);或者在 PowerShell:

powershell 复制代码
[System.IO.Ports.SerialPort]::GetPortNames()

多个 COM 口时,可以比较拔掉/插上设备前后的列表。用户实际最后成功的是 COM7,不是最早测试过的 COM12。


12 正式升级:Com.bin 的真实参数与成功记录

12.1 实际固件参数(已从附件校验)

参数 实测值
文件名 Com.bin
文件大小 74016 字节 = 72.28125 KiB ≈ 72.28 KiB
CRC32(zlib) 0x9F551106
APP 允许容量 229376 字节 = 224 KiB
文件占 APP 容量 约 32.27%
数据包 290 包,最多 256B / 包
初始栈顶 0x2000C000
Reset 入口 0x08010BB5

12.2 正式升级命令

进入 C:\Users\Administrator\Desktop\xiazai(实际文件夹以你电脑为准),关闭串口助手,复位 F103 到 Bootloader,在 VSCode PowerShell 执行:

powershell 复制代码
.\.venv\Scripts\python.exe upgrade_com.py --port COM7 --file Com.bin

这条命令会自动发送 S 并擦除 APP,不要把它当无害的"查询命令"。过程中不要断电、不要连接可能引发复位的调试器、不要让串口助手同时占用 COM7。

12.3 本次成功日志(原样记录)

text 复制代码
PS C:\Users\Administrator\Desktop\xiazai> .\.venv\Scripts\python.exe upgrade_com.py --port COM7 --file Com.bin
固件: Com.bin | 74016 字节 | CRC32=9F551106
警告: S 命令会擦除 APP 区域 0x08008000~0x0803FFFF
Bootloader 握手成功: B1
APP 擦除成功
已写入 74016/74016 字节
升级成功!F 返回 K,固件已完成 CRC32 校验。
暂不发送 J;可用 ST-LINK 回读 Flash 验证。

这份日志证明 F103 当前 Bootloader 报告完整升级成功;它没有显示实际秒数,所以博客中不能声称"实际耗时 10 秒"之类的未测量结论。

12.4 Python 是怎样分包的?

脚本中的核心循环:

python 复制代码
for offset in range(0, size, 256):
    block = firmware[offset:offset + 256]
    ser.write(b'D' + struct.pack('<H', len(block)) + block)
    wait_ack(ser, f'D/偏移 0x{offset:X}')

它对每一包发送 D + len + block,等到 K 才发下一包。最后:

python 复制代码
ser.write(b'F')
wait_ack(ser, 'F/CRC32 完成校验')

这就是串口助手不能直接"发送整个裸 BIN 文件"的原因。


13 如何严格证明 Flash 写对了?

13.1 先查前 8 字节

升级完成后再用 STM32CubeProgrammer + ST-LINK 连接(这时不会打断升级状态,因为 F 已完成)。在 Memory & File Editing 读取:

text 复制代码
Address : 0x08008000
Size    : 0x100

应该看到的前 8 字节:

text 复制代码
按字节:00 C0 00 20 B5 0B 01 08
32位列:2000C000   08010BB5

显示差异来自小端存储和界面的 Data width 选项,不要把 32-bit 列看成线性字节顺序。

13.2 全量回读才是独立验收

真正严谨的方法是从 0x08008000 开始读取 74,016 个字节 ,保存成 Com_after_readback.bin(不包含 Bootloader 前 32 KiB)。将回读文件与原始 Com.bin 逐字节比较。

在 VSCode 终端可以执行:

powershell 复制代码
.\.venv\Scripts\python.exe -c "from pathlib import Path; a=Path('Com.bin').read_bytes(); b=Path('Com_after_readback.bin').read_bytes(); print('PASS: exact match' if a==b else 'FAIL: mismatch or different length')"

只有确保保存时读取地址就是 0x08008000、长度恰好是 74016 ,上面的全文件比较才有效。如果读的是整个 224 KiB APP,则应只比较回读文件的前 74016 字节,而且仍须核对它的起始地址。

记录边界 :之前存在一份较早的 com_readback.bin,不能把它冒充为这次成功升级后的回读证据。必须在这次升级完成后重新保存回读文件。

13.3 跳转成功与业务成功是两件事

串口发送 4A(ASCII J),若返回 4B,表示 Bootloader 接受跳转请求。是否真正启动 Com,需要通过调试器断点、业务状态、LED 或最终 CANopen 通信来确认。

本记录中的重点是固件升级功能已跑通。业务程序功能、上电自动跳转后的长期运行稳定性属于后续测试。


14 调试 Com APP 时不覆盖 Bootloader 的做法

14.1 CubeIDE 直接调试 APP 可以吗?

可以。两个工程分别链接 :Bootloader 在 0x08000000,Com 在 0x08008000。只要 IDE 的调试下载行为只擦除必要的 APP 页,Com 可以通过 ST-LINK 反复开发。

操作要检查:

  • 调试配置引用的是 Com 的 .elf ,不是 Bootloader .elf。
  • Com 的链接脚本 FLASH ORIGIN 已改到 0x08008000。
  • CubeIDE / Programmer 下载擦除策略是按需擦除区域/扇页,不应开启 Full Chip Erase。
  • APP 的 VTOR 在其自身启动时正确重定位;出现中断问题先查这一项。

注意 :调试器如何启动,取决于 Debug Configuration 的启动 / 复位策略。有的配置从 ELF 入口接管,有的会硬复位先运行 Bootloader。不要笼统认为"按 Debug 必然跳过 Bootloader",最好读一下当前 PC、VTOR,或在 main() 设断点确认。

14.2 修改了 Bootloader 源码,芯片会自动变吗?

不会。你最新贴出的 Boot_Run() 有 3 秒自动跳转逻辑,但它只有在重新编译、重新烧录 Bootloader 之后才会替代旧版本。改 C 文件 ≠ 已更新芯片 Flash。

14.3 什么时候还需要 ST-LINK?

Bootloader 丢失、写保护误配置、错误跳转导致无法进入升级、Flash 被 Full Chip Erase 等情况,仍需要 SWD / ST-LINK 作为恢复手段。正式产品应保留必要的下载/维护接口。


15 经历过的典型错误:原因、诊断、解决

现象 真正要检查的点 建议处理
V 不是 B1 COM 口不对、串口助手 HEX/ASCII 混用、MCU 已进入 APP、噪声 复位 F103、关闭其他串口软件、核对 CH340 端口,HEX 发 56
只发 S 回复 E 缺少 8B size+CRC32 一次发送完整 9B 帧,不要只发 53
S 完整帧仍 E 接收超时、文件长度非法、Flash Unlock 或 Erase 失败 区分失败阶段,确认 HAL 返回值,不要盲目重复擦除
D 回复 E 未先成功 S、复位导致 updating=0、len/数据缺失、写入失败 不要在 S 与 D 之间复位;发送完整 44+len+data
D 收到 K 但前 8B 是 FF 这是设计 前 8B 启动向量保留到 F 再写入
F 回复 E 数据没收齐、CRC 不符、向量非法、回读 CRC 不匹配 根据 received/file_size 和计算值定位,不要直接 J
J 回复 K 但 APP 不运行 J 仅接受跳转,未证明业务已正常启动 检查 APP .ld、SystemInit/VTOR、启动入口、业务初始化
串口握手收到异常字节 返回的不是期望的 B1 核对 COM 口、复位后重试、清空串口接收缓存
py 命令不存在 Python launcher 未提供/未在 PATH 改用已有 Python 的完整路径或 .venv
全局安装提示 externally-managed-environment Python 安装由 uv 管理 新建 .venv,安装 pyserial,不改全局环境
上电卡在 Bootloader 实际芯片还是"禁用自动跳转"的旧版,或 APP 向量无效 查烧录是否更新、app_valid()、是否已到 3 秒
3 秒很慢 是设计的升级窗口,不是 UART_TIMEOUT 30 秒 调试阶段用 3 秒,正式设备设计专用升级入口

16 上线前必须解决的不足(当前是跑通的实验版本)

这套协议已经在你的 F103 板子上证明"能写入并校验固件",但还不应直接称为高可靠量产方案。

A. 断电恢复 :S 一开始会擦除整个 APP。若升级中断,旧 APP 已不存在;系统需要可稳定再次进入 Bootloader 的机制,最好再考虑备份镜像 / A-B 分区(当前 256KiB 容量要重新评估分区)。

B. 包级完整性 :目前仅有每包 K/E 和最终全文件 CRC,没有包序号和逐包 CRC。串口乱序、丢包或主机重试可能使双方状态不一致。应增加序号、数据包校验和明确重传机制。

C. 镜像有效性标志 :当前 app_valid() 只检查头 8 字节;建议持久化记录 APP 有效状态/版本,并区分"已收齐文件""CRC 成功""APP 健康启动"等状态。

D. 超时与状态机 :如果升级过程中 PC 掉线,updating 可能一直为 1,现有主循环又没有全局升级会话超时。应定义升级超时、取消、重入及错误恢复策略。

E. 稳定进入升级模式:未来将自动启动窗口缩至 500ms 时,电脑可能来不及发送 V/S。量产优先设计 APP 触发升级标志或硬件恢复入口。

F. 安全性 :CRC32 能检测常见传输错误,不能防止恶意替换固件。如果设备可能接入不受信任的上位机或外部网络,需考虑固件签名、鉴权和安全升级策略。

G. 调试器擦除设置:确保修改 APP 时不会意外擦除 Bootloader;正式发布前用备用板反复验证断电、错误大小、CRC 错、丢包、复位恢复。


17 一张最终核对清单(每次升级按此检查)

  • F103 型号确实为 STM32F103RCT6,Flash 256 KiB / RAM 48 KiB。
  • Bootloader .ld:FLASH 起点 0x08000000,长度 32K。
  • Com .ld:FLASH 起点 0x08008000,长度 224K。
  • Com 向量表 VTOR 为 0x08008000,不是默认的 0x08000000。
  • CH340C 与 USART2 PA2/PA3 接线、115200 8N1 正确。
  • Com.bin 有效,当前大小不超过 229376 字节,前 8B 向量合法。
  • Python 使用 .venv,pyserial 已安装,串口号为当前实际 COM 口。
  • 升级之前保留 Com 备份与可用的 ST-LINK 恢复手段。
  • 关闭串口助手,不要两个程序同时占用同一 COM 口。
  • Python 收到 B1、S 回复 K、全部 D 回复 K、F 回复 K。
  • 升级后按 0x08008000 起点回读,与原 .bin 比较;不能用旧回读替代。
  • 确认 APP 跳转与业务功能;对上电自动跳转单独测试。
  • 发布博客时注明"实测条件"和"未完成的量产可靠性增强"。

附录 A:最终成功的 upgrade_com.py 完整源代码(原文件原样保留)

下面内容直接取自本次上传的 upgrade_com.py,不做算法改写 。这正是产生 Bootloader 握手成功: B1、已写入 74016/74016、F 返回 K 日志的脚本。运行前 pip install pyserial 并确认 COM 口和文件名。

python 复制代码
import argparse
import struct
import sys
import time
import zlib

try:
    import serial
except ImportError:
    print('请先安装: py -m pip install pyserial')
    sys.exit(1)


def wait_ack(ser, stage):
    response = ser.read(1)
    if response != b'K':
        raise RuntimeError(f'{stage} 失败: 收到 {response.hex(" ") if response else "超时"}(预期 4B)')


def main():
    parser = argparse.ArgumentParser(description='STM32F103 UART Bootloader Com.bin 升级')
    parser.add_argument('--port', required=True, help='串口号,例如 COM5')
    parser.add_argument('--file', default='Com.bin', help='固件 BIN 文件路径')
    parser.add_argument('--baud', type=int, default=115200)
    args = parser.parse_args()

    with open(args.file, 'rb') as f:
        firmware = f.read()
    size = len(firmware)
    crc = zlib.crc32(firmware) & 0xffffffff
    if not (8 <= size <= 0x38000):
        raise ValueError(f'固件大小 {size} 超出 APP 容量')
    sp, reset = struct.unpack_from('<II', firmware)
    if not (0x20000000 <= sp <= 0x2000C000 and sp % 8 == 0):
        raise ValueError(f'栈指针异常: 0x{sp:08X}')
    if not (0x08008000 <= (reset & ~1) < 0x08040000 and reset & 1):
        raise ValueError(f'复位入口异常: 0x{reset:08X}')

    print(f'固件: {args.file} | {size} 字节 | CRC32={crc:08X}')
    print('警告: S 命令会擦除 APP 区域 0x08008000~0x0803FFFF')
    with serial.Serial(args.port, args.baud, timeout=35, write_timeout=10) as ser:
        # 避免串口助手遗留的回显干扰协议解析
        ser.reset_input_buffer()
        ser.write(b'V')
        version = ser.read(2)
        if version != b'B1':
            raise RuntimeError(f'Bootloader 握手失败: {version!r},请复位 MCU 并确认串口号')
        print('Bootloader 握手成功: B1')

        ser.write(b'S' + struct.pack('<II', size, crc))
        wait_ack(ser, 'S/擦除')
        print('APP 擦除成功')

        for offset in range(0, size, 256):
            block = firmware[offset:offset + 256]
            ser.write(b'D' + struct.pack('<H', len(block)) + block)
            wait_ack(ser, f'D/偏移 0x{offset:X}')
            print(f'\r已写入 {offset + len(block)}/{size} 字节', end='', flush=True)
        print()

        ser.write(b'F')
        wait_ack(ser, 'F/CRC32 完成校验')
        print('升级成功!F 返回 K,固件已完成 CRC32 校验。')
        print('暂不发送 J;可用 ST-LINK 回读 Flash 验证。')


if __name__ == '__main__':
    try:
        main()
    except (OSError, RuntimeError, ValueError, serial.SerialException if 'serial' in globals() else OSError) as exc:
        print(f'错误: {exc}', file=sys.stderr)
        sys.exit(1)

附录 B:Bootloader 参考源码的结构(完整版另附)

boot_upgrade_参考整理版.c、boot_upgrade.h 可以放入 Core/Src 和 Core/Inc,与第 04 章 main.c 调用配合。它们保留了 V/S/D/F/J 的协议,并对用户最后粘贴的 program_halfwords 范围检查做了安全性修正;该源码是博客复现参考,不冒称已重新烧录成功。实际使用时须检查工程生成的 HAL、链接脚本、编译错误和板上测试结果。

Bootloader 主要函数索引:

函数名 作用
Boot_Run 主循环,串口命令分发与自动跳转
begin_upgrade 解析 S、校验大小、擦除 APP、初始化状态
receive_packet 解析 D、接收数据、保存前 8B、半字写入、更新 CRC
finish_upgrade 检查大小 / CRC / 向量,最后写入启动向量并回读校验
program_halfwords 半字写 Flash,写后立即读回验证
erase_app 用 112 页的页擦除覆盖全部 APP
jump_app 清理运行环境、设置 VTOR/MSP、跳转 APP Reset

自定义参数修改时的同步关系

想改什么 必须同步确认什么
APP 从 0x08008000 改地址 两工程链接脚本、APP_BASE、APP VTOR、PC 脚本中的校验范围
每包 256 改成其他大小 PACKET_BYTES、PC 的分块步长、最大包长验证、内存缓冲区
使用新的 F1 型号 Flash/RAM 大小、页大小、启动向量范围、时钟、串口引脚
3 秒改 500ms Boot_Run 的 3000U;同时必须设计可靠的进入升级模式途径
30 秒接收超时改 2 秒 UART_TIMEOUT,要保证 PC 完整帧发送不会中途超时

相关推荐
二本咕咕-机械转码2 小时前
FreeRTOS深入解析(一)_链表与并发保护——哨兵循环链表、排序键游标、临界区与关调度
单片机·算法·链表
Zwawa3 小时前
串口命令如何从字符串长成协议:帧头、长度、校验和与状态机
c语言·单片机·嵌入式
小李不想当小白3 小时前
BKP备份寄存器与RTC实时时钟(STM32标准库学习笔记)
笔记·stm32·单片机·嵌入式硬件·学习·嵌入式
Zw-awa5 小时前
串口命令如何从字符串长成协议:帧头、长度、校验和与状态机
stm32
La Pulga6 小时前
【FreeRTOS源码学习笔记】链表的实现
数据结构·笔记·stm32·mcu·链表·嵌入式·freertos
铅笔小新z9 小时前
【stm32】WWDG(窗口看门狗)
stm32·单片机·嵌入式硬件
comyccccc10 小时前
平衡车底板设计:精准布局与高效布线学习笔记
单片机·嵌入式硬件
La Pulga10 小时前
【FreeRTOS源码学习笔记】任务创建
笔记·stm32·mcu·学习·嵌入式·freertos·实时操作系统
芯片人00711 小时前
GEK100系列芯片进入上市公司上海移远通信供应链
人工智能·单片机·嵌入式硬件·物联网·芯片