第二章 开发环境搭建:交叉编译工具链配置、RTOS环境准备、硬件平台初始化

说实话,每次带新人做物联网项目,我最怕听到的一句话就是:「环境我搭好了,但编译不过。」

嗯,这章咱们就把开发环境这件事彻底讲透。我个人的习惯是,先把工具链和RTOS的环境理清楚,再碰硬件。顺序搞反了,后面全是坑。

2.1 交叉编译工具链配置

嵌入式开发跟PC开发最大的区别是什么?说白了,你的代码不是在目标板上编译的。你得在PC上写好代码,然后用交叉编译器生成目标架构的机器码。

我刚开始做ARM Cortex-M系列时,就吃过这个亏——用gcc直接编译,结果烧进去跑不起来。后来才发现,交叉编译器的前缀必须是 arm-none-eabi- 这种格式。

2.1.1 工具链的选择

目前主流的交叉编译工具链有这几类:

  • GNU Arm Embedded Toolchain:官方出品,免费,支持ARM Cortex-M/R系列。我个人最常用这个。
  • Linaro GCC:针对ARM Cortex-A系列优化,适合跑Linux的场景。
  • IAR / Keil:商业编译器,IDE集成度高,但价格不菲。

我建议初学者直接上GNU Arm Embedded Toolchain。为什么?因为开源、文档全、社区活跃。我在一个量产项目中遇到过编译器bug,靠社区邮件列表三天就定位到了,换成商业编译器你找谁去?

2.1.2 安装与验证

以Ubuntu 20.04为例,安装步骤其实很简单:

# 下载最新版本(以gcc-arm-none-eabi-10.3-2021.10为例)
wget https://developer.arm.com/-/media/Files/downloads/gnu-rm/10.3-2021.10/gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2

# 解压到指定目录
tar -xjf gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2 -C /opt/

# 配置环境变量
echo 'export PATH=$PATH:/opt/gcc-arm-none-eabi-10.3-2021.10/bin' >> ~/.bashrc
source ~/.bashrc

# 验证安装
arm-none-eabi-gcc --version

验证输出应该类似这样:

arm-none-eabi-gcc (GNU Arm Embedded Toolchain 10.3-2021.10) 10.3.1 20210824
Copyright (C) 2020 Free Software Foundation, Inc.
注意: 我曾经遇到过一个问题——明明安装了工具链,但编译时提示找不到头文件。排查了半天,发现是环境变量没生效。记得 source ~/.bashrc 或者重新打开终端。

2.1.3 编写第一个交叉编译程序

写个简单的测试程序,验证工具链是否正常工作:

// test.c
#include <stdio.h>

int main(void) {
    printf("Hello from ARM Cortex-M!\n");
    return 0;
}
# 编译
arm-none-eabi-gcc -c test.c -o test.o

# 查看目标文件信息
arm-none-eabi-objdump -d test.o

你会看到反汇编出来的指令是ARM指令集,而不是x86指令。这就对了。

2.2 RTOS环境准备

RTOS的选择,说白了就是看你的项目需求。FreeRTOS轻量、生态好;RT-Thread功能丰富、组件多。我两个都用过,各有千秋。

2.2.1 FreeRTOS移植准备

FreeRTOS的源码结构非常清晰,核心文件就这几个:

文件/目录 说明
FreeRTOS/Source/tasks.c 任务管理核心
FreeRTOS/Source/list.c 链表操作
FreeRTOS/Source/queue.c 队列与信号量
FreeRTOS/Source/portable/ 平台相关移植层

我个人的习惯是,先把 portable 目录下的对应架构文件准备好。比如用STM32F4,就找 GCC/ARM_CM4F 这个目录。

小技巧: FreeRTOSConfig.h 这个配置文件一定要仔细看。我见过有人把 configTICK_RATE_HZ 设成1000,结果系统滴答中断太频繁,CPU占用率飙升。一般设成100或200就够了。

2.2.2 RT-Thread环境搭建

RT-Thread的搭建比FreeRTOS稍微复杂一点,但它的工具链支持更好。推荐用 env 工具:

# 下载RT-Thread源码
git clone https://github.com/RT-Thread/rt-thread.git

# 进入BSP目录,以STM32F4为例
cd rt-thread/bsp/stm32/stm32f407-atk-explorer

# 使用env工具配置
scons --menuconfig

# 编译
scons

这里要注意,RT-Thread的包管理用的是 pkgs --update,可以很方便地添加组件。我在一个项目中需要MQTT协议栈,直接 menuconfig 勾选就搞定了,省了不少事。

2.3 硬件平台初始化

硬件初始化这块,说白了就是让芯片「活过来」。我见过很多新手一上来就写应用代码,结果发现串口打印不出来——原来是时钟没配好。

2.3.1 启动流程概览

ARM Cortex-M的启动流程大致是这样的:

  1. 上电复位,从0x00000000取栈顶指针
  2. 从0x00000004取复位向量,跳转到 Reset_Handler
  3. 在 Reset_Handler 中完成:关闭中断、初始化全局变量、初始化BSS段
  4. 调用 SystemInit() 配置系统时钟
  5. 跳转到 main() 函数

嗯,这里有个容易踩的坑——如果你用FreeRTOS,记得在 main() 里先调用硬件初始化,再创建任务。我曾经把顺序搞反了,结果任务创建成功了,但SysTick中断没起来,系统直接卡死。

2.3.2 时钟树配置

以STM32F407为例,配置系统时钟到168MHz:

void SystemClock_Config(void)
{
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

    // 配置HSE振荡器
    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
    RCC_OscInitStruct.HSEState = RCC_HSE_ON;
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
    RCC_OscInitStruct.PLL.PLLM = 8;
    RCC_OscInitStruct.PLL.PLLN = 336;
    RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
    RCC_OscInitStruct.PLL.PLLQ = 7;
    HAL_RCC_OscConfig(&RCC_OscInitStruct);

    // 配置系统时钟源和分频
    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
                                | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
    HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5);
}
关键点: 时钟配置错了,整个系统就跑偏了。我建议你配完时钟后,先通过MCO引脚输出时钟信号,用示波器量一下频率对不对。别问我为什么知道这个——有一次我把PLLN设成了337,结果频率偏了0.3%,UART通信直接乱码。

2.3.3 GPIO与外设初始化

硬件初始化最后一步,就是把要用到的外设打开。以串口为例:

void MX_USART1_UART_Init(void)
{
    huart1.Instance = USART1;
    huart1.Init.BaudRate = 115200;
    huart1.Init.WordLength = UART_WORDLENGTH_8B;
    huart1.Init.StopBits = UART_STOPBITS_1;
    huart1.Init.Parity = UART_PARITY_NONE;
    huart1.Init.Mode = UART_MODE_TX_RX;
    huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    huart1.Init.OverSampling = UART_OVERSAMPLING_16;
    HAL_UART_Init(&huart1);
}

这里有个细节——GPIO的复用功能一定要配对。我见过有人把USART1的TX配成了推挽输出,但忘了配置复用功能,结果数据发不出去。查了半天才发现是GPIO模式没设对。

2.4 环境验证:跑一个点灯程序

所有环境都搭好了,最后跑个点灯程序验证一下。这是嵌入式界的「Hello World」:

#include "main.h"

void LED_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    __HAL_RCC_GPIOB_CLK_ENABLE();

    GPIO_InitStruct.Pin = GPIO_PIN_0;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    LED_Init();

    while (1)
    {
        HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
        HAL_Delay(500);
    }
}

编译、烧录、上电。如果LED以1Hz的频率闪烁,恭喜你,环境搭建成功了。

我的经验: 第一次跑点灯程序时,如果灯不亮,别急着怀疑硬件。先检查这几个地方:时钟使能了没?GPIO模式配对了没?烧录的hex文件对不对?我遇到过最离谱的一次,是烧录器没接好,程序根本没写进去。

好了,环境搭建这部分就到这里。下一章咱们开始讲RTOS内核移植,到时候会用到今天配好的工具链和硬件环境。记得把今天的代码保存好,后面会反复用到。