本文档详细讲解本工程的结构、配置、代码功能,以及搭建过程中遇到的三个典型问题(分散加载错误、ST-Link 下载失败、Semihosting 崩溃)的原因与解决方法,可作为后续工程的参考模板。
一、工程功能
单片机上电后:
- 板载 LED 快闪 6 下(上电自检,不依赖任何外设和 RTOS);
- 串口打印启动横幅(含系统时钟频率);
- FreeRTOS 创建一个打印任务,每秒通过 USART1 输出一行计数信息,同时 LED 每秒翻转一次。
实际运行效果:

二、硬件连接
| 设备 | 连接 | 说明 |
|---|---|---|
| ST-Link V2 | SWDIO → PA13,SWCLK → PA14,GND → GND,3V3 → 3V3 | 烧录与调试 |
| USB 转 TTL | RXD → PA9,TXD → PA10,GND → GND | 串口打印(注意交叉连接) |
| BOOT0 跳线 | 接 0 | 从主 Flash 启动(BOOT1 任意) |
-
串口助手参数:115200,8 数据位,无校验,1 停止位(8-N-1)
-
板子可以用 ST-Link、USB 转 TTL 或板载 USB 口任一方式供电,但必须共地
-
LED 驱动了 4 个常见板载灯引脚(PC13 / PB12 / PA8 / PB2),适配不同板型;确定自己的灯在哪个引脚后可删掉多余的
三、工程目录结构
STM32F103FreeRTOS_Test0815/
│
├── Project/ Keil 工程目录
│ ├── STM32F103_FreeRTOS.uvprojx 工程文件(编译选项、文件分组)
│ ├── STM32F103_FreeRTOS.uvoptx 工程配置(调试器选择、界面状态)
│ ├── Objects/ 编译输出(.axf / .hex / .o)
│ └── Listings/ 链接 map 文件等
│
├── User/ 用户代码
│ ├── main.c 主程序(串口、LED、FreeRTOS 任务)
│ ├── FreeRTOSConfig.h FreeRTOS 配置文件(裁剪内核)
│ ├── stm32f10x_conf.h 标准库配置文件(使能外设头文件)
│ ├── stm32f10x_it.c 中断服务函数
│ └── stm32f10x_it.h
│
├── Libraries/
│ ├── CMSIS/CM3/
│ │ ├── CoreSupport/ core_cm3.c/h(内核寄存器访问层)
│ │ └── DeviceSupport/ST/STM32F10x/
│ │ ├── stm32f10x.h 芯片寄存器定义
│ │ ├── system_stm32f10x.c/h 系统时钟初始化(72MHz)
│ │ └── startup/arm/startup_stm32f10x_md.s 启动文件(中容量 MD)
│ └── STM32F10x_StdPeriph_Driver/
│ ├── inc/ 标准外设库头文件(V3.5.0)
│ └── src/ 标准外设库源文件
│
└── FreeRTOS/Source/ FreeRTOS V10.5.1 内核
├── tasks.c / queue.c / list.c 内核核心
├── timers.c / event_groups.c / stream_buffer.c / croutine.c
├── include/ 内核头文件
└── portable/
├── RVDS/ARM_CM3/ Keil(armcc) 的 Cortex-M3 移植层
│ ├── port.c 上下文切换、SysTick 等
│ └── portmacro.h
└── MemMang/heap_4.c 堆内存管理(支持内存释放)
关键文件的"三个一"原则
-
一个工程配置入口:
stm32f10x_conf.h—— 标准库所有外设的头文件都在这里包含,stm32f10x.h末尾会自动包含它; -
一个 RTOS 配置入口:
FreeRTOSConfig.h—— 内核功能裁剪、时钟、堆大小、中断优先级全在这里; -
一个启动文件:
startup_stm32f10x_md.s—— 中容量(Medium Density,64KB Flash)芯片专用,不能换成 hd/ld 版本。
四、main.c 代码逐段讲解
4.1 禁用半主机(最重要的两行)
#pragma import(__use_no_semihosting) // 告诉链接器:不使用半主机
struct __FILE { int handle; }; // 标准库要求的 FILE 结构
FILE __stdout;
FILE __stdin;
void _sys_exit(int x) { (void)x; for (;;); }
void _ttywrch(int ch) { (void)ch; }
为什么必须加:ARM 标准 C 库(armcc)的 stdio 默认通过 Semihosting(半主机) 实现——即把 printf 的输出通过调试器转发到电脑。裸机运行时没有调试器应答,C 库在 main() 之前的 _initio 初始化阶段就会执行 BKPT 0xAB 指令,程序直接卡死/进 HardFault。加上 __use_no_semihosting 后,链接器走纯嵌入式路径,stdio 由我们自己的 fputc 接管。(本工程就是踩了这个坑,详见第六章。)
4.2 串口初始化
static void USART1_Config(void)
{
// 1. 开时钟:USART1 和 GPIOA 都挂在 APB2 总线上
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 2. PA9(TX) 配成复用推挽输出;PA10(RX) 配成浮空输入
// 3. 串口参数:115200, 8-N-1, 无硬件流控,收发都使能
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
}
标准库初始化外设的固定套路:开时钟 → 配 GPIO → 配外设 → 使能。
4.3 printf 重定向
int fputc(int ch, FILE *f)
{
USART_SendData(USART1, (uint8_t)ch);
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
return ch;
}
printf 最终逐字符调用 fputc,把它重定向到 USART1 的发送寄存器即可。等待 TXE(发送数据寄存器空)标志是阻塞式发送的标准写法。
4.4 FreeRTOS 任务
static void vPrintTask(void *pvParameters)
{
uint32_t ulCount = 0;
for (;;)
{
printf("Hello FreeRTOS! Task running, count = %lu\r\n", ulCount++);
LED_Toggle();
vTaskDelay(pdMS_TO_TICKS(1000)); // 阻塞 1000 个 tick = 1 秒
}
}
任务函数的铁律:必须是死循环 + 循环内必须有阻塞调用(vTaskDelay),让出 CPU 给其他任务(包括空闲任务)。pdMS_TO_TICKS() 把毫秒转换成 tick 数,与具体的 tick 频率解耦。
4.5 main 函数
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); // 见 4.6
LED_Config(); // 上电快闪 6 下:最早的"活着"证据
for (i = 0; i < 6; i++) { LED_Toggle(); RoughDelay(3000000); }
USART1_Config();
printf("...启动横幅...\n");
xTaskCreate(vPrintTask, "Print", 256, NULL, 2, NULL);
vTaskStartScheduler(); // 启动调度器,正常不会返回
for (;;);
}
xTaskCreate 参数依次是:任务函数、任务名、栈深度 256 字(=1KB,注意单位是字不是字节)、传参、优先级 2、任务句柄。
4.6 中断优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
FreeRTOS 要求 STM32 的 4 位优先级全部用作抢占优先级(分组 4),因为 configMAX_SYSCALL_INTERRUPT_PRIORITY 的换算基于这个前提。分组设错会导致中断里的 FromISR API 行为异常,是 FreeRTOS 初学者最常见的隐性 bug 之一。
五、FreeRTOSConfig.h 关键配置
| 配置项 | 本工程值 | 含义 |
|---|---|---|
configUSE_PREEMPTION | 1 | 抢占式调度 |
configCPU_CLOCK_HZ | SystemCoreClock | 72MHz,直接引用标准库变量 |
configTICK_RATE_HZ | 1000 | 系统节拍 1ms 一次 |
configMAX_PRIORITIES | 5 | 可用优先级 0~4 |
configMINIMAL_STACK_SIZE | 128 字 | 空闲任务/定时器任务的最小栈 |
configTOTAL_HEAP_SIZE | 10 × 1024 | heap_4 管理的堆总大小,xTaskCreate 的栈和 TCB 都从这里分配;C8 总共只有 20KB RAM,别贪大 |
configUSE_TIMERS | 1 | 使能软件定时器 |
configPRIO_BITS | 4 | STM32F103 实现 4 位中断优先级 |
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY | 5 | 中断优先级数值 ≤ 5 的 ISR 禁止调用 FromISR API |
configKERNEL_INTERRUPT_PRIORITY | 15 << 4 = 255 | 内核(PendSV/SysTick)用最低优先级 |
异常接管映射(重要)
#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler
Cortex-M3 的任务切换依赖三个系统异常:SVC(启动第一个任务)、PendSV(上下文切换)、SysTick(系统节拍)。这三个宏把 FreeRTOS 移植层里的函数映射到 CMSIS 标准异常名,因此 stm32f10x_it.c 里绝对不能再实现这三个函数,否则链接时报重复定义。这也是移植 FreeRTOS 时最高频的链接错误。
六、踩坑记录(本工程实际遇到的三个问题)
坑 1:Scatter Error: no default 'Read/Write' range selected
现象:全部源文件编译通过,链接时报找不到 STM32F103_FreeRTOS.sct,并提示没有默认读写区。
原因:.uvprojx 是手工编写的,Target 页的存储器配置缺失。Keil 链接时根据 Target 页的 IROM/IRAM 设置自动生成分散加载文件(.sct),而这些设置在 XML 里由一组隐蔽字段控制:
<Im1Chk>1</Im1Chk> <!-- IROM1 复选框:勾选 -->
<Ir1Chk>1</Ir1Chk> <!-- IRAM1 复选框:勾选 -->
<RoSelD>3</RoSelD> <!-- 默认只读区索引 -->
<RwSelD>3</RwSelD> <!-- 默认读写区索引 -->
<StupSel>8</StupSel> <!-- 启动代码位置 -->
复选框没勾(默认 0),等于告诉链接器"没有任何内存区域",自然生成不了 .sct。
排查方法:与 GitHub 上真实可编译的同芯片工程逐字段 diff,定位出差异字段。
修复:对照真实工程补齐上述字段。打开 Options → Target 也能直观看到 IROM1/IRAM1 未勾选,手动勾上等效。
坑 2:Flash Download failed - Target DLL has been cancelled
现象:编译 0 错误,但用 ST-Link 下载时报"Target DLL has been cancelled"。
原因:Keil 里"用哪个调试器"(ST-Link / J-Link / ULINK)不保存在 .uvprojx,而保存在 .uvoptx。最初只交付了 .uvprojx,Keil 使用默认调试器,连不上 ST-Link,弹出连接对话框,用户点取消后就报这个错。
修复:补写 .uvoptx,核心字段:
<pMon>STLink\ST-LINKIII-KEIL_SWO.dll</pMon> <!-- 调试器选 ST-Link -->
...
<Protocol>2</Protocol> <!-- SWD 接口 -->
经验:Keil 工程要完整分享,.uvprojx 和 .uvoptx 必须成对提供。手工排错路径:Options → Debug 页选 “ST-Link Debugger” → Settings 看 SWDIO 是否识别出 IDCODE。
坑 3:烧录 Verify OK,但板子像"死"了一样(最难的一个)
现象:下载校验通过,BOOT 跳线正确,供电正常,但 LED 不闪、串口完全无输出,按复位也无效。
排查过程(这是嵌入式调试的标准思路):
- 先排除"程序没烧进去":命令行烧录显示
Verify OK,固件确实在芯片里; - 在代码里加"上电快闪 LED"(不经过 RTOS 和串口),缩小怀疑范围到"main 之前";
- 进调试器一看便知:Ctrl+F5 → F5 运行 → Stop,发现:
PC = 0x08000758 → BKPT 0xAB ← 半主机陷阱指令
调用栈: _sys_open ← freopen ← _initio ← (C 库启动代码)
根因:ARM 标准 C 库的 stdio 默认走 Semihosting(半主机)——printf 通过调试器转发到电脑。裸机没有调试器应答,C 库在进入 main() 之前的 _initio 阶段执行 BKPT 0xAB,无调试器时直接触发异常卡死。程序根本没活到 main(),所以一切外设表现都没有。
修复(双保险):
- 代码里加
#pragma import(__use_no_semihosting)并提供_sys_exit、_ttywrch、__stdout等桩函数(本工程采用,与是否 MicroLIB 无关,都安全); - 或勾选 Options → Target → Use MicroLIB(MicroLIB 本身不依赖半主机),对应工程文件字段
<useUlib>1</useUlib>。
经验:凡是"程序烧进去了但完全没反应"的标准库工程,九成是半主机问题。调试器里看到 BKPT 0xAB + _initio 调用栈即可确诊。
附:串口助手的小坑
电脑上可能存在多个虚拟串口,选错端口同样表现为"什么都没收到"。判断方法:拔掉 USB 转 TTL,看设备管理器里哪个 COM 消失。
七、程序启动流程总览
上电/复位
│
startup_stm32f10x_md.s: Reset_Handler
│ ├─ 设置栈顶 SP(向量表第一项)
│ ├─ 调用 SystemInit() ← system_stm32f10x.c,HSE(8M) ×9 倍频 → 72MHz
│ └─ 跳转 __main ← C 库初始化:拷贝 RW 段、清 ZI 段、_initio
│ (半主机坑就在这里爆发)
main()
├─ NVIC_PriorityGroupConfig(分组4)
├─ LED_Config + 快闪 6 下 ← 最早的运行证据
├─ USART1_Config(PA9/PA10, 115200)
├─ printf 启动横幅
├─ xTaskCreate(vPrintTask) ← 从 heap_4 的 10KB 堆里分配 1KB 栈 + TCB
└─ vTaskStartScheduler()
├─ 创建空闲任务、定时器任务
├─ SVC 异常启动第一个任务
└─ SysTick 每 1ms 触发 → PendSV 做上下文切换
│
▼
vPrintTask 每秒: printf + LED 翻转 → vTaskDelay 让出 CPU
八、后续扩展建议
-
确定 LED 引脚后,把
LED_Config/LED_Toggle精简到实际的那一颗; -
多加任务时注意:
configTOTAL_HEAP_SIZE(10KB)要够分,每个任务栈 = 创建参数 × 4 字节; -
中断里用 FreeRTOS API 必须用
xxxFromISR版本,且中断优先级数值必须 ≥ 5(即低于等于configMAX_SYSCALL_INTERRUPT_PRIORITY的逻辑优先级); -
需要浮点 printf 时,标准库直接支持;若改用 MicroLIB 则
%f默认不支持,需要额外处理。
工程环境:Keil MDK 5(ARMCC V5.06 update 7)+ Keil.STM32F1xx_DFP 器件包 + STM32F10x 标准外设库 V3.5.0 + FreeRTOS V10.5.1
291

被折叠的 条评论
为什么被折叠?



