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)
  • 激活时用注册机,注意管理员权限
我曾经遇到过安装后找不到芯片的情况,后来发现是器件包没装全。建议装完MDK后,用Pack Installer把常用系列都勾上。

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接口
一个小技巧:在C/C++选项卡的Misc Controls里加上 --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长什么样。核心就三件事:

  1. 配置FreeRTOSConfig.h:告诉内核有多少优先级、时钟频率等
  2. 提供系统时钟:一般是SysTick中断
  3. 实现临界区保护:关中断和开中断

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);
}
注意:SysTick_Handler这个名字在不同启动文件里可能不一样。有的叫SysTick_Handler,有的叫SysTickHandler。我建议你打开启动文件确认一下。

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烧录。

调试小技巧:如果LED不亮,先检查GPIO配置是否正确。我一般会在main函数里先写一个死循环点灯,排除FreeRTOS的问题。等灯亮了,再添加任务调度。

2.6 常见问题与避坑

最后分享几个我踩过的坑:

  • 编译报错:undefined reference to vApplicationStackOverflowHook——在FreeRTOSConfig.h里把configCHECK_FOR_STACK_OVERFLOW设为0,或者实现这个钩子函数。
  • 任务不调度——检查SysTick中断优先级是否配置正确。FreeRTOS要求SysTick优先级为最低。
  • Keil下编译通过但下载后不运行——检查启动文件里的堆栈大小是否足够。我习惯把Stack设为0x400,Heap设为0x200。

嗯,到这里环境就搭好了。下一章我们会深入FreeRTOS的任务创建与调度机制,到时候你就知道这个点灯程序背后,内核到底干了哪些事。