第4章 移植环境搭建:Keil MDK工程创建、芯片启动文件配置、系统时钟配置
好,咱们正式开始动手了。
前面几章聊了不少理论,什么RTOS调度机制、任务切换、临界区保护……说实话,光看这些概念确实容易犯困。我当年学RTOS的时候,也是看了好几遍《野火》和《安富莱》的文档,总觉得隔着一层窗户纸。直到自己亲手搭了一个工程,把FreeRTOS跑起来,看到串口打印出任务切换的日志,才真正理解了那些概念到底在说什么。
所以这一章,咱们就直奔主题——把开发环境搭起来。说白了,就是让Keil MDK认识你的芯片,让芯片上电后能正常跑起来,再把系统时钟配置好。这三件事做完了,后面移植RTOS就是水到渠成的事。
4.1 Keil MDK工程创建——别小看这一步
很多人觉得建工程嘛,不就是点几下鼠标?嗯,确实不难,但细节决定成败。我在项目中遇到过好几次,同事把工程建好了,编译零错误,下载到板子上就是跑不起来。查了半天,发现是芯片型号选错了,或者启动文件没加对。
咱们以STM32F407为例,一步步来。
第一步:选择芯片型号
打开Keil MDK,点Project → New μVision Project。找个干净的文件夹,给工程起个名字,比如“BloodGlucose_RTOS”。然后会弹出一个芯片选择对话框——这里要注意,别选错了系列。我习惯直接搜“STM32F407ZGT6”,确认一下Flash大小和RAM大小对不对得上。
经验之谈: 如果你用的是国产替代芯片,比如GD32、AT32,记得去官网下载对应的Keil Pack。否则Keil里根本找不到这个型号。我踩过这个坑,当时还以为是Keil版本太老……
第二步:管理运行时环境
选完芯片后,Keil会弹出一个“Manage Run-Time Environment”窗口。这里我建议新手先别勾太多东西。咱们的目标是跑RTOS,不是搞CMSIS-Driver全家桶。只勾选最基础的:
- CMSIS → CORE(必须,这是ARM核的抽象层)
- Device → Startup(启动文件,后面会细说)
其他的,比如GPIO、UART这些驱动,咱们后面手动加。为什么?因为自动生成的代码有时候会跟你自己写的冲突,排查起来很麻烦。
第三步:添加源文件
工程建好后,左侧Project窗口里会有几个默认的组(Target、Source Group等)。我习惯自己重新组织一下:
- 右键工程名 → Add Group,新建这几个文件夹:
Startup—— 放启动文件和链接脚本Drivers—— 放外设驱动(GPIO、UART、SPI等)RTOS—— 放FreeRTOS源码App—— 放你自己的应用代码- 然后把对应的.c文件拖进去。注意,启动文件是汇编写的,后缀是.s。
小技巧: 我习惯把FreeRTOS的源码单独放在工程目录下的“FreeRTOS”文件夹里,而不是跟工程文件混在一起。这样以后升级RTOS版本,直接替换文件夹就行,不用重新整理工程结构。
4.2 芯片启动文件配置——芯片的“第一行代码”
启动文件,说白了就是芯片上电后执行的第一段程序。它负责三件事:
- 设置堆栈指针(SP)
- 初始化中断向量表
- 调用
SystemInit()和main()
嗯,听起来很简单对吧?但这里有个坑——不同的编译器、不同的芯片,启动文件的写法可能不一样。Keil MDK用的是ARMCC编译器,它的启动文件通常叫 startup_stm32f407xx.s。
启动文件里到底写了什么?
咱们打开这个文件看看。开头是一堆中断向量表的定义:
; 中断向量表
__Vectors DCD __initial_sp ; 栈顶地址
DCD Reset_Handler ; 复位中断
DCD NMI_Handler ; NMI中断
DCD HardFault_Handler ; 硬错误中断
; ... 后面还有一堆外设中断
这里要注意,__initial_sp 的值是从链接脚本(.sct文件)里算出来的。如果你改了链接脚本,比如把堆栈大小调大了,那这个值会自动更新。但如果你手动改了启动文件里的这个值……嗯,我曾经这么干过,结果芯片一上电就跑飞了。
警告: 不要手动修改启动文件里的 __initial_sp 和中断向量表顺序!除非你非常清楚自己在做什么。否则,老老实实用Keil自动生成的文件就好。
启动文件需要改什么?
其实大部分情况下,你不需要改启动文件。但有一个例外——如果你用到了某些外设中断,但启动文件里默认的Handler是死循环(B .),你需要自己实现这个中断服务函数。比如:
; 启动文件里默认的弱定义
TIM2_IRQHandler PROC
EXPORT TIM2_IRQHandler [WEAK]
B .
ENDP
你在自己的代码里写一个同名的函数,链接器就会用你的函数替换掉这个弱定义。这就是为什么你可以在任意.c文件里写 void TIM2_IRQHandler(void) { ... },而不需要修改启动文件。
个人习惯: 我一般会在工程里单独建一个 stm32f4xx_it.c 文件,专门放所有中断服务函数。这样结构清晰,也方便以后移植到其他芯片。
4.3 系统时钟配置——让芯片跑在正确的频率上
时钟配置,是嵌入式开发里最容易出问题的地方之一。你想想看,芯片上电后默认用的是内部HSI振荡器,频率只有16MHz(对于F4系列)。而咱们的RTOS需要精确的SysTick定时器来做任务调度,如果时钟不准,那任务切换的时间片就全乱了。
时钟树长什么样?
STM32F4的时钟树比较复杂,但咱们只需要关注几个关键点:
- HSE:外部高速晶振,通常是8MHz或25MHz
- PLL:锁相环,用来倍频。比如8MHz的HSE,通过PLL倍频到168MHz
- SYSCLK:系统时钟,最终给CPU和总线用的
- AHB、APB1、APB2:总线时钟分频
我习惯用CubeMX来生成时钟配置的初始化代码,但咱们这里手动写一遍,加深理解。
手动配置时钟的步骤:
- 打开HSE,等待它稳定
- 配置PLL的倍频系数和分频系数
- 将PLL输出作为系统时钟源
- 配置AHB、APB1、APB2的分频
- 配置Flash等待周期(这个容易忘!)
代码长这样:
void SystemClock_Config(void)
{
// 1. 使能HSE
RCC->CR |= RCC_CR_HSEON;
while(!(RCC->CR & RCC_CR_HSERDY)); // 等待HSE稳定
// 2. 配置PLL:HSE 8MHz → PLL_M=8 → PLL_N=336 → PLL_P=2 → 168MHz
RCC->PLLCFGR = (8 << 0) | // PLL_M
(336 << 6) | // PLL_N
(0 << 16) | // PLL_P = 2 (00表示2分频)
(RCC_PLLCFGR_PLLSRC_HSE); // 选择HSE作为PLL输入
// 3. 使能PLL,等待锁定
RCC->CR |= RCC_CR_PLLON;
while(!(RCC->CR & RCC_CR_PLLRDY));
// 4. 配置总线分频
RCC->CFGR |= RCC_CFGR_HPRE_DIV1; // AHB = SYSCLK / 1 = 168MHz
RCC->CFGR |= RCC_CFGR_PPRE1_DIV4; // APB1 = AHB / 4 = 42MHz
RCC->CFGR |= RCC_CFGR_PPRE2_DIV2; // APB2 = AHB / 2 = 84MHz
// 5. 选择PLL作为系统时钟
RCC->CFGR |= RCC_CFGR_SW_PLL;
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
// 6. 配置Flash等待周期(168MHz需要5个等待周期)
FLASH->ACR = FLASH_ACR_LATENCY_5WS | FLASH_ACR_PRFTEN | FLASH_ACR_ICEN | FLASH_ACR_DCEN;
}
避坑指南: 我曾经在配置PLL时,把PLL_N设成了336,但忘了改PLL_M。结果PLL输出频率超出了芯片允许的最大值(168MHz),芯片直接不工作。后来用示波器量HSE引脚才发现晶振根本没起振——因为PLL配置错了,芯片进入了某种保护状态。
所以,配置完时钟后,建议用 RCC_GetClocksFreq() 函数读一下实际频率,确认一下。
SysTick定时器配置
RTOS需要SysTick来产生周期性的中断,用于任务调度。SysTick的时钟源通常来自AHB的8分频(或者直接AHB,看你怎么配)。
void SysTick_Config_For_RTOS(void)
{
// 假设AHB = 168MHz,SysTick时钟 = AHB / 8 = 21MHz
// 想要1ms中断一次,则重装载值 = 21MHz / 1000 = 21000
SysTick->LOAD = 21000 - 1;
SysTick->VAL = 0;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_DIV8 | // 时钟源为AHB/8
SysTick_CTRL_TICKINT | // 使能中断
SysTick_CTRL_ENABLE; // 使能定时器
}
注意: 不同的RTOS对SysTick的配置要求可能不一样。比如FreeRTOS默认使用SysTick,但如果你移植到uCOS-III,它可能用其他定时器。所以这里只是演示,具体配置要参考RTOS的移植文档。
4.4 验证时钟配置——别光靠感觉
配置完了,怎么知道对不对?我一般用两种方法:
- 方法一: 在
main()里加个GPIO翻转,用示波器量频率。比如让PA0每1ms翻转一次,示波器上应该看到500Hz的方波。 - 方法二: 用串口打印
SystemCoreClock这个全局变量。Keil的启动文件里会自动调用SystemInit(),而这个函数会更新SystemCoreClock的值。
我个人更推荐方法二,因为不用动硬件。但要注意,如果你手动改了时钟配置,SystemCoreClock 可能不会自动更新。这时候需要手动调用 SystemCoreClockUpdate() 函数。
printf("System Clock: %lu Hz\r\n", SystemCoreClock);
如果打印出来是168000000,那就对了。
4.5 本章小结
嗯,这一章的内容其实挺琐碎的,但每一步都关系到后面RTOS能不能跑起来。总结一下:
- 建工程时,芯片型号别选错,启动文件别漏加
- 启动文件一般不用改,除非你要自定义中断Handler
- 时钟配置要算清楚PLL的倍频系数,别忘了Flash等待周期
- SysTick的配置要跟AHB时钟匹配,否则RTOS的时间片会乱
下一章,咱们就要正式把FreeRTOS的源码加到工程里,然后让第一个任务跑起来。到时候你会发现,前面这些环境搭建的功夫,全都没白费。