2. 嵌入式系统基础
做嵌入式系统,尤其是跑RTOS,硬件基础是绕不开的坎。很多人一上来就撸代码,结果芯片为什么跑飞、中断为什么进不去、外设为什么没反应,查半天都找不到原因。说白了,就是没搞懂硬件是怎么工作的。
这一章,我们聚焦ARM Cortex-M系列。我会把架构、存储器、启动、时钟和复位这几个核心模块串起来讲。你在项目中遇到的很多“玄学”问题,根子往往就在这里面。
2.1 ARM Cortex-M 处理器架构概览
Cortex-M系列是ARM专门为微控制器设计的。它跟Linux跑的应用处理器(比如Cortex-A)完全是两码事。M系列追求的是低功耗、快速中断响应和确定性。
我个人习惯把Cortex-M的核心特性归纳为以下几点:
- 哈佛架构:指令总线和数据总线分开。取指令和访存可以同时进行,效率比冯·诺依曼高。
- Thumb/Thumb-2指令集:16位和32位混合指令。代码密度高,省Flash空间。我早期做项目时,Flash只有64KB,全靠Thumb-2硬撑下来的。
- 内置嵌套向量中断控制器(NVIC):这是M系列的灵魂。支持中断优先级嵌套,硬件自动压栈和出栈,中断延迟极低。
- 系统节拍定时器(SysTick):一个24位的递减计数器。RTOS的心跳就靠它。你想想看,没有SysTick,FreeRTOS的调度器根本跑不起来。
- 存储器保护单元(MPU):可选模块。用来划分内存区域,防止任务之间互相踩踏。做安全关键系统时,MPU是必备的。
核心要点:Cortex-M的NVIC和SysTick是RTOS的硬件基石。搞懂这两个,你就掌握了RTOS调度的底层逻辑。
2.2 存储器映射与总线架构
Cortex-M的存储器映射是固定的。不管你用哪家芯片,外设、SRAM、Code区的地址范围都是约定好的。这给代码移植带来了极大的便利。
以Cortex-M3/M4为例,典型的4GB地址空间划分如下:
| 地址范围 | 区域 | 用途 |
|---|---|---|
| 0x0000 0000 - 0x1FFF FFFF | Code区 | 存放程序代码和向量表。通常映射到Flash。 |
| 0x2000 0000 - 0x3FFF FFFF | SRAM区 | 存放堆栈、全局变量、RTOS的任务栈。 |
| 0x4000 0000 - 0x5FFF FFFF | 外设区 | 映射所有片上外设的寄存器。GPIO、UART、TIMER都在这里。 |
| 0xE000 0000 - 0xE00F FFFF | 系统区 | NVIC、SysTick、MPU等内核外设的寄存器。 |
这里有个坑,我必须要提醒你。很多新手写驱动时,喜欢直接拿外设基地址加偏移去操作寄存器。这没问题,但你要确保总线时钟已经使能了。我曾经在调试一个I2C驱动时,死活读不到数据,折腾了两天。最后发现是RCC里I2C的外设时钟没开。嗯,这种低级错误,犯过一次就记住了。
避坑指南:操作外设寄存器前,务必确认该外设的时钟已在RCC中使能。否则,读回来的一律是0x00000000,写进去的也石沉大海。
2.3 启动流程:从复位到main()
芯片上电后,到底发生了什么?很多人以为直接就从main()开始执行了。其实不然。在进入main()之前,内核已经完成了一系列初始化工作。
标准的Cortex-M启动流程分为三步:
- 取向量表:复位后,CPU从0x00000000地址读取栈顶指针(MSP),从0x00000004地址读取复位向量(Reset_Handler的地址)。
- 执行启动文件:跳转到Reset_Handler。启动文件(startup_xxx.s)会做三件事:
- 初始化全局变量(BSS段清零,DATA段从Flash拷贝到SRAM)。
- 调用SystemInit()函数,配置系统时钟。
- 调用C库的__main函数,最终跳转到main()。
- 进入main():用户程序开始运行。
为什么启动文件这么重要?因为RTOS的很多初始化工作,比如堆栈大小、中断向量表的重定位,都需要在启动阶段完成。如果你用的是标准库,启动文件是现成的。但如果你做的是深度定制,比如把向量表搬到SRAM里,那就得自己改启动文件了。
个人经验:我建议你至少读一遍芯片自带的启动文件。不用背,但要知道它干了什么。尤其是SystemInit()函数,很多时钟配置的坑都在里面。
下面是一个简化的启动流程SVG图,帮你理清脉络:
2.4 时钟系统与复位机制
时钟是芯片的心脏。没有时钟,CPU和外设都动不了。Cortex-M芯片内部通常有多个时钟源:高速外部晶振(HSE)、低速外部晶振(LSE)、高速内部RC振荡器(HSI)、低速内部RC振荡器(LSI)以及锁相环(PLL)。
系统时钟(SYSCLK)的配置路径一般是这样的:
- 选择一个时钟源(HSE、HSI或PLL)。
- 通过AHB预分频器(HPRE)得到HCLK(CPU和总线时钟)。
- 通过APB1/APB2预分频器(PPRE1/PPRE2)得到外设总线时钟。
这里有一个关键点:APB1的最高频率通常比APB2低。比如STM32F4系列,APB1最大42MHz,APB2最大84MHz。如果你把UART挂在APB1上,波特率太高就可能出问题。我在项目中遇到过类似情况,串口打印乱码,查了半天才发现是APB1时钟超限了。
经验之谈:配置时钟时,一定要看数据手册里各总线的最大频率限制。别以为PLL倍频越高越好,超出规格外设会罢工的。
复位机制也值得一说。Cortex-M支持多种复位源:
| 复位源 | 触发条件 | 影响范围 |
|---|---|---|
| 上电复位(POR) | 芯片上电 | 全部复位 |
| 外部复位(NRST) | 复位引脚拉低 | 全部复位 |
| 看门狗复位(IWDG/WWDG) | 看门狗超时 | 全部复位 |
| 软件复位 | 写NVIC系统控制寄存器 | 全部复位(除调试逻辑) |
| 低功耗管理复位 | 进入特定低功耗模式 | 部分复位 |
做RTOS时,我建议你养成一个习惯:在main()一开始就读取复位状态寄存器(RCC_CSR或类似寄存器),判断上次复位的原因。如果是看门狗复位,说明系统之前可能卡死了。这个信息对调试非常有用。
小技巧:软件复位可以用NVIC_SystemReset()函数。调用前记得关中断,否则复位过程可能被中断打断,导致不可预知的行为。
2.5 本章小结
这一章的内容比较基础,但都是硬知识。ARM Cortex-M的架构决定了RTOS的实现方式,存储器映射决定了你的代码和数据怎么放,启动流程决定了系统怎么跑起来,时钟和复位决定了系统能不能稳定工作。