FreeRTOS自学笔记02:STM32+FreeRTOS简单模板

本文档详细讲解本工程的结构、配置、代码功能,以及搭建过程中遇到的三个典型问题(分散加载错误、ST-Link 下载失败、Semihosting 崩溃)的原因与解决方法,可作为后续工程的参考模板。


一、工程功能

单片机上电后:

  1. 板载 LED 快闪 6 下(上电自检,不依赖任何外设和 RTOS);
  2. 串口打印启动横幅(含系统时钟频率);
  3. FreeRTOS 创建一个打印任务,每秒通过 USART1 输出一行计数信息,同时 LED 每秒翻转一次。

实际运行效果:

在这里插入图片描述


二、硬件连接

设备连接说明
ST-Link V2SWDIO → PA13,SWCLK → PA14,GND → GND,3V3 → 3V3烧录与调试
USB 转 TTLRXD → PA9TXD → 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_PREEMPTION1抢占式调度
configCPU_CLOCK_HZSystemCoreClock72MHz,直接引用标准库变量
configTICK_RATE_HZ1000系统节拍 1ms 一次
configMAX_PRIORITIES5可用优先级 0~4
configMINIMAL_STACK_SIZE128 字空闲任务/定时器任务的最小栈
configTOTAL_HEAP_SIZE10 × 1024heap_4 管理的堆总大小xTaskCreate 的栈和 TCB 都从这里分配;C8 总共只有 20KB RAM,别贪大
configUSE_TIMERS1使能软件定时器
configPRIO_BITS4STM32F103 实现 4 位中断优先级
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY5中断优先级数值 ≤ 5 的 ISR 禁止调用 FromISR API
configKERNEL_INTERRUPT_PRIORITY15 << 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 不闪、串口完全无输出,按复位也无效。

排查过程(这是嵌入式调试的标准思路):

  1. 先排除"程序没烧进去":命令行烧录显示 Verify OK,固件确实在芯片里;
  2. 在代码里加"上电快闪 LED"(不经过 RTOS 和串口),缩小怀疑范围到"main 之前";
  3. 进调试器一看便知:Ctrl+F5 → F5 运行 → Stop,发现:
PC = 0x08000758  →  BKPT 0xAB        ← 半主机陷阱指令
调用栈: _sys_open ← freopen ← _initio ← (C 库启动代码)

根因:ARM 标准 C 库的 stdio 默认走 Semihosting(半主机)——printf 通过调试器转发到电脑。裸机没有调试器应答,C 库在进入 main() 之前_initio 阶段执行 BKPT 0xAB,无调试器时直接触发异常卡死。程序根本没活到 main(),所以一切外设表现都没有。

修复(双保险):

  1. 代码里加 #pragma import(__use_no_semihosting) 并提供 _sys_exit_ttywrch__stdout 等桩函数(本工程采用,与是否 MicroLIB 无关,都安全);
  2. 或勾选 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值