第二章 开发环境搭建:交叉编译工具链配置、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.
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 这个目录。
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的启动流程大致是这样的:
- 上电复位,从0x00000000取栈顶指针
- 从0x00000004取复位向量,跳转到 Reset_Handler
- 在 Reset_Handler 中完成:关闭中断、初始化全局变量、初始化BSS段
- 调用 SystemInit() 配置系统时钟
- 跳转到 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);
}
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的频率闪烁,恭喜你,环境搭建成功了。
好了,环境搭建这部分就到这里。下一章咱们开始讲RTOS内核移植,到时候会用到今天配好的工具链和硬件环境。记得把今天的代码保存好,后面会反复用到。