2、开发环境搭建:Keil MDK与GCC工具链配置、FreeRTOS源码移植、第一个任务点亮LED
说实话,很多初学者一上来就啃源码,结果连编译都过不了。我个人习惯是先把环境搭好,跑通一个点灯程序,心里才有底。这一章我们就来搞定两套工具链——Keil MDK和GCC,再把FreeRTOS源码移植进去,最后让LED闪起来。
2.1 工具链的选择:Keil vs GCC
做嵌入式开发,工具链是绕不开的坎。我这些年用过不少IDE,但最常用的还是这两套:
| 对比项 | Keil MDK | GCC + Makefile |
|---|---|---|
| 上手难度 | 低,图形化界面 | 中高,需要命令行基础 |
| 调试体验 | 优秀,自带调试器 | 依赖GDB,配置稍复杂 |
| 商业许可 | 收费(有代码大小限制) | 免费开源 |
| 跨平台 | 仅Windows | Windows/Linux/Mac |
| 社区资源 | 丰富,尤其国内 | 极丰富,全球通用 |
我个人建议:如果你刚开始学,用Keil MDK会省心很多。但如果你想深入理解编译链接过程,或者做产品级开发,GCC是更好的选择。我在项目中遇到过用Keil编译好好的,换GCC就出各种警告的情况——说白了,不同工具链对C标准的支持细节有差异。
2.2 Keil MDK环境搭建
先讲Keil。这个比较简单,但有几个坑要注意。
2.2.1 安装与激活
去Keil官网下载MDK-ARM最新版。安装时注意:
- 路径不要有中文和空格
- 选择对应的器件包(比如STM32F1系列就装Keil.STM32F1xx_DFP)
- 激活时用注册机,注意管理员权限
2.2.2 创建FreeRTOS工程
新建一个空工程,然后手动添加FreeRTOS源码。结构如下:
Project/
├── User/
│ ├── main.c
│ └── FreeRTOSConfig.h
├── FreeRTOS/
│ ├── Source/
│ │ ├── tasks.c
│ │ ├── queue.c
│ │ ├── list.c
│ │ ├── timers.c
│ │ └── portable/
│ │ ├── MemMang/
│ │ │ └── heap_4.c
│ │ └── RVDS/
│ │ └── port.c
│ └── include/
│ ├── FreeRTOS.h
│ └── task.h
└── Startup/
└── startup_stm32f10x_hd.s
嗯,这里要注意:portable目录下的文件要根据你的MCU架构选。比如STM32F103是Cortex-M3,就用RVDS目录下的port.c。heap_4.c是内存管理方案,我一般用heap_4,因为它支持碎片合并。
2.2.3 配置魔术棒
在Keil的Options for Target里:
- Target:选择正确的芯片型号,设置晶振频率
- C/C++:添加头文件路径,Define里写
USE_STDPERIPH_DRIVER, STM32F10X_MD - Debug:选J-Link或ST-Link,Settings里确认SWD接口
--gnu,可以让Keil的编译器更接近GCC风格,减少移植时的麻烦。
2.3 GCC工具链配置
GCC环境我更喜欢用VSCode + Makefile的方式。你想想看,这样既轻量又可控。
2.3.1 安装工具链
下载ARM GCC工具链,推荐用gcc-arm-none-eabi。Windows下可以用MSYS2或者直接下载Windows版。Linux下一条命令搞定:
sudo apt-get install gcc-arm-none-eabi
装完后验证一下:
arm-none-eabi-gcc --version
看到版本号就说明装好了。
2.3.2 编写Makefile
Makefile是GCC项目的核心。我分享一个我常用的模板:
# 工具链
CC = arm-none-eabi-gcc
LD = arm-none-eabi-ld
OBJCOPY = arm-none-eabi-objcopy
# 编译选项
CFLAGS = -mcpu=cortex-m3 -mthumb -Wall -O2
CFLAGS += -I./FreeRTOS/include -I./FreeRTOS/portable/GCC/ARM_CM3
CFLAGS += -I./User
# 源文件
SRCS = ./User/main.c \
./FreeRTOS/tasks.c \
./FreeRTOS/queue.c \
./FreeRTOS/list.c \
./FreeRTOS/portable/MemMang/heap_4.c \
./FreeRTOS/portable/GCC/ARM_CM3/port.c
# 链接脚本
LDFLAGS = -T ./STM32F103C8Tx_FLASH.ld
all: firmware.elf firmware.bin
firmware.elf: $(SRCS)
$(CC) $(CFLAGS) $(SRCS) $(LDFLAGS) -o $@
firmware.bin: firmware.elf
$(OBJCOPY) -O binary $< $@
clean:
rm -f firmware.elf firmware.bin
关键点:链接脚本(.ld文件)必须和你的芯片匹配。我刚开始用GCC时,就因为用了错误的链接脚本,导致程序跑飞。后来我都是直接从STM32CubeMX生成的GCC工程里复制链接脚本。
2.4 FreeRTOS源码移植
移植FreeRTOS,说白了就是让内核知道你的MCU长什么样。核心就三件事:
- 配置FreeRTOSConfig.h:告诉内核有多少优先级、时钟频率等
- 提供系统时钟:一般是SysTick中断
- 实现临界区保护:关中断和开中断
2.4.1 FreeRTOSConfig.h配置
这个文件是移植的灵魂。我贴一个典型配置:
#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H
#define configUSE_PREEMPTION 1
#define configUSE_IDLE_HOOK 0
#define configUSE_TICK_HOOK 0
#define configCPU_CLOCK_HZ ((unsigned long)72000000)
#define configTICK_RATE_HZ ((TickType_t)1000)
#define configMAX_PRIORITIES (5)
#define configMINIMAL_STACK_SIZE ((unsigned short)128)
#define configTOTAL_HEAP_SIZE ((size_t)(20 * 1024))
#define configMAX_TASK_NAME_LEN (16)
#define configUSE_16_BIT_TICKS 0
#define configIDLE_SHOULD_YIELD 1
#define configUSE_MUTEXES 1
#define configUSE_RECURSIVE_MUTEXES 1
#define configUSE_COUNTING_SEMAPHORES 1
#define configUSE_TIMERS 1
#define configTIMER_TASK_PRIORITY (2)
#define configTIMER_QUEUE_LENGTH 10
#define configTIMER_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE * 2)
#define configASSERT(x) if((x) == 0) {taskDISABLE_INTERRUPTS(); for(;;);}
#endif /* FREERTOS_CONFIG_H */
这里configCPU_CLOCK_HZ一定要和你的实际晶振匹配。我遇到过有人设成72MHz,但实际板子是8MHz外部晶振倍频到72MHz,结果SysTick定时不准,任务调度全乱套。
2.4.2 系统时钟与中断
FreeRTOS需要一个1ms的周期性中断来驱动任务调度。通常用SysTick实现。在main.c里添加:
#include "FreeRTOS.h"
#include "task.h"
void SysTick_Handler(void)
{
if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
{
xPortSysTickHandler();
}
}
int main(void)
{
// 初始化硬件
// ...
// 创建任务
xTaskCreate(vLEDTask, "LED", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
// 启动调度器
vTaskStartScheduler();
// 正常情况下不会执行到这里
while(1);
}
2.5 第一个任务:点亮LED
终于到了激动人心的时刻。我们来写一个任务,让LED以1秒为周期闪烁。
2.5.1 硬件准备
假设LED接在GPIOB的Pin 0上,低电平点亮。先初始化GPIO:
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 初始状态:熄灭
GPIO_SetBits(GPIOB, GPIO_Pin_0);
}
2.5.2 任务函数
void vLEDTask(void *pvParameters)
{
(void)pvParameters; // 防止编译器警告
LED_Init();
for (;;)
{
// 点亮LED
GPIO_ResetBits(GPIOB, GPIO_Pin_0);
vTaskDelay(pdMS_TO_TICKS(500));
// 熄灭LED
GPIO_SetBits(GPIOB, GPIO_Pin_0);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
这里用vTaskDelay而不是HAL_Delay,因为FreeRTOS的延时会让出CPU给其他任务。你想想看,如果所有任务都用阻塞延时,那CPU利用率会高很多。
2.5.3 编译与下载
Keil下直接点Build然后Download。GCC下执行make生成bin文件,然后用J-Flash或ST-Link Utility烧录。
2.6 常见问题与避坑
最后分享几个我踩过的坑:
- 编译报错:undefined reference to vApplicationStackOverflowHook——在FreeRTOSConfig.h里把
configCHECK_FOR_STACK_OVERFLOW设为0,或者实现这个钩子函数。 - 任务不调度——检查SysTick中断优先级是否配置正确。FreeRTOS要求SysTick优先级为最低。
- Keil下编译通过但下载后不运行——检查启动文件里的堆栈大小是否足够。我习惯把Stack设为0x400,Heap设为0x200。
嗯,到这里环境就搭好了。下一章我们会深入FreeRTOS的任务创建与调度机制,到时候你就知道这个点灯程序背后,内核到底干了哪些事。