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的骨架,总线是血管,理解了它们,你才能真正驾驭这颗芯片。下一章咱们聊聊中断系统和异常处理,那才是实时系统的灵魂。