3. 存储器系统与总线架构:哈佛架构与冯诺依曼架构、Flash与SRAM的映射、系统总线矩阵(AHB/APB)、位带操作原理
好,咱们今天聊点硬核的。存储器系统和总线架构,说白了就是MCU的“血管”和“仓库”。我刚开始学的时候,总觉得这东西离应用层很远,直到有一次在项目里因为总线冲突导致DMA传输卡死,排查了整整两天……嗯,从那以后我再也不敢小看这部分了。
3.1 哈佛架构 vs 冯诺依曼架构
先问个问题:为什么STM32跑得比同主频的8051快那么多?
答案就在架构上。冯诺依曼架构,指令和数据共用一条总线,走同一条路。你想想看,取指令的时候不能读写数据,读写数据的时候不能取指令——这不就堵车了吗?
哈佛架构就不一样了。它把指令总线和数据总线分开,各走各的道。取指令的同时还能读写数据,相当于双向八车道和单车道比速度。
核心区别一句话:
- 冯诺依曼:一条总线,指令和数据混着走。简单、省钱,但慢。
- 哈佛:两条总线,各走各的。快,但芯片面积大一点。
我个人习惯把Cortex-M系列归为“改进型哈佛架构”。为什么这么说?因为它在芯片内部确实是指令和数据分开取,但对外部存储器又统一编址。说白了,内部是哈佛,外部看起来像冯诺依曼。这种混合设计,兼顾了速度和灵活性。
避坑指南: 我曾经在一个项目里,把代码放到外部SRAM里跑,结果发现执行效率比内部Flash慢了将近一半。原因就是外部存储器走的是冯诺依曼式的共用总线,取指令和数据互相干扰。所以,关键代码一定要放在内部Flash里。
3.2 Flash与SRAM的映射
咱们打开STM32的参考手册,看存储器映射图。你会发现Flash和SRAM被分配在固定的地址区间里。
以STM32F103为例:
- Flash:起始地址 0x0800 0000,大小看你选的型号,64KB到512KB不等。
- SRAM:起始地址 0x2000 0000,通常20KB到64KB。
为什么这么安排?说白了,就是为了让CPU能通过统一的地址总线访问它们。你写C语言的时候,const变量放在Flash里,局部变量放在SRAM里,编译器自动帮你分配地址。
这里有个细节要注意:Flash的读取速度比SRAM慢。STM32F103的Flash读取需要等待周期,主频72MHz时通常要配2个等待周期。SRAM是零等待的,随叫随到。
注意: 如果你把频繁读写的变量放在Flash里(比如用const修饰但实际在改),程序会跑飞。我见过有人把全局变量误写成const,结果程序一运行就HardFault。查了半天才发现是往Flash里写数据了。
再聊一个实用技巧:零等待区。有些MCU(比如STM32F4)在Flash里划了一块区域,叫ART Accelerator,可以做到零等待。把最常用的函数放进去,性能提升很明显。我习惯把中断服务函数和RTOS调度函数放在这个区域。
3.3 系统总线矩阵:AHB与APB
好,现在聊聊总线。Cortex-M内核通过总线矩阵连接各种外设。这个矩阵,你可以理解成一个立交桥,不同方向的车流互不干扰。
STM32的总线架构分两层:
- AHB(高级高性能总线):跑得快,连接CPU、Flash、SRAM、DMA这些核心模块。时钟频率和CPU一样,72MHz。
- APB(高级外设总线):跑得慢,连接GPIO、UART、I2C、SPI这些外设。APB1最高36MHz,APB2最高72MHz。
为什么要分两层?你想想看,如果所有外设都挂在AHB上,那GPIO这种低速设备会拖慢整个总线。分开之后,高速设备走高速路,低速设备走低速路,互不影响。
| 总线 | 时钟频率 | 连接设备举例 |
|---|---|---|
| AHB | 72MHz(与CPU同频) | Flash、SRAM、DMA、USB OTG |
| APB2 | 72MHz | GPIO、SPI1、USART1、ADC1 |
| APB1 | 36MHz | UART2~5、I2C1~2、SPI2、TIM2~7 |
这里有个坑:APB1上的外设时钟只有36MHz,如果你把SPI2配置成72MHz,它根本跑不起来。我刚开始做项目时,把SPI2的波特率设得和SPI1一样,结果通信一直出错。查了半天才发现是总线时钟没搞对。
个人经验: 配置外设时钟时,养成好习惯——先查外设在哪个总线上,再算分频系数。我一般会在代码注释里写上“// 此外设在APB1上,时钟36MHz”,防止以后忘了。
3.4 位带操作原理
位带操作,这是Cortex-M3/M4的一个特色功能。说白了,就是把一个32位的地址空间,映射到另一个地址空间的每一位上。
举个例子:你想控制GPIOB的第5脚输出高电平。正常写法是:
GPIOB->BSRR = (1 << 5); // 设置第5位
用位带操作可以写成:
*(volatile uint32_t *)(0x42000000 + (0x40010C0C - 0x40000000) * 32 + 5 * 4) = 1;
看着很复杂对吧?其实原理很简单:
- SRAM和外设寄存器各有一个位带区
- 每个位(bit)对应位带别名区的一个32位字(word)
- 你往这个别名地址写1或0,就相当于设置对应位的值
为什么要有位带操作?因为有些操作需要“读-改-写”,在多任务环境下可能被中断打断。位带操作是原子的,一条指令完成,不会被中断干扰。
计算公式:
位带别名地址 = 位带基址 + (字节偏移 × 32) + (位号 × 4)
其中:
- SRAM位带基址:0x2200 0000
- 外设位带基址:0x4200 0000
我习惯把位带操作封装成宏:
#define BITBAND(addr, bit) (*(volatile uint32_t *)(0x42000000 + ((uint32_t)(addr) - 0x40000000) * 32 + (bit) * 4))
// 使用示例
BITBAND(&GPIOB->ODR, 5) = 1; // PB5输出高
注意: 位带操作只对SRAM和外设寄存器的前1MB有效。超过这个范围,位带别名区就映射不到了。我曾经在STM32F407上试过映射到FMC控制的LCD寄存器,结果写进去没反应——因为那个地址不在位带区范围内。
好了,这一章的内容就这些。存储器系统是MCU的骨架,总线是血管,理解了它们,你才能真正驾驭这颗芯片。下一章咱们聊聊中断系统和异常处理,那才是实时系统的灵魂。