拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

冯·诺依曼与哈佛架构:嵌入式开发必须掌握的底层逻辑

冯·诺依曼与哈佛架构:嵌入式开发必须掌握的底层逻辑

1. 面试官一句"你这块板子属于哪种架构",把我问在当场

三年前面一家做工业控制器的公司,技术面聊到中断响应时间,对方突然插了一句:你这块板子是冯·诺依曼架构还是哈佛架构?我当场愣了两秒,脑子里只剩教科书上那句"冯·诺依曼就是程序和数据放一起、哈佛就是分开放",然后硬着头皮答了个"哈佛吧,反正STM32嘛"。面试官点点头没再追问,可我自己心里清楚——我不知道STM32到底"哈佛"在哪,更不知道搞明白这件事对我写中断服务函数有什么用。

后来带项目、调时序、看反汇编,我才慢慢把这块知识补回来,也才明白它不是考试题,而是能实实在在解释很多现象的底层逻辑:为什么同样主频的两颗芯片跑同一段循环,一个要三个周期一个要两个周期;为什么有些芯片能从RAM里执行代码,有些不行;为什么改一行把常量数组挪到RAM,程序速度突然快了一大截。这些问题的答案,全都挂在"冯·诺依曼"和"哈佛"这两个词上。

如果你正在走嵌入式学习路线,不管是刷嵌入式面试题、跑嵌入式开源项目,还是用vscode开发嵌入式编程折腾点嵌入式项目,这块知识都属于那种"懂了不亏、不懂早晚被问住"的底子。它不解决具体某个外设怎么配,但它决定了你看待整个芯片的方式。下面我就按我自己踩过的顺序,把这两种架构讲透,中间顺带把改进哈佛、缓存、DMA这些容易混的东西一并理清。

1.1 两种架构最朴素的一句话版本

先给一句人话版本,方便你建立第一印象。冯·诺依曼架构的核心是:程序(指令)和数据放在同一个存储器里,用同一组地址编址,靠同一套总线来读写。CPU要取指令就从这块内存里读,要取数据还从这块内存里读,两边排队共用一条通道。

哈佛架构的核心是:指令和数据物理上分开存放,各走各自的总线,指令有自己的存储器和通道,数据也有自己的存储器和通道,两者可以同时进行,互不抢道。你可以把它想成一个路口:冯·诺依曼是十字路口只有一个方向能走,哈佛是架了立交桥,直行和转弯同时放行。

这两句话能应付面试的开场,但完全不够用。真正让人栽跟头的,是后面那些"看起来像冯诺依曼、实际上是哈佛""地址空间统一但总线分离"的混合情况。

1.2 为什么嵌入式开发者比PC程序员更该关心它

写PC上层的同学基本感受不到这件事,因为x86从出生到现在就是标准的冯·诺依曼血统,统一地址空间、统一访存,操作系统和缓存把差异全抹平了。可嵌入式不一样,你直接面对的是单片机的存储结构和总线组织。

嵌入式硬件的选型往往跟架构强绑定。8051、AVR、PIC这些经典微控制器走的是哈佛路线,绝大多数DSP为了同时取指令和取两个操作数,也是哈佛架构;而MSP430这类低功耗MCU又是冯·诺依曼。你换平台的时候,如果只记寄存器名字,不懂底层差异,遇到性能问题就会卡住。

我见过太多人在嵌入式开发里调一段控制算法,发现怎么优化都上不去速度,最后发现瓶颈根本不在代码,而是在芯片的取指方式上。这就是为什么我一直建议,学嵌入式linux也好、玩裸机也罢,架构这课都得补。

2. 冯·诺依曼架构:一条总线走天下,代价藏在时序里

要看懂冯·诺依曼,得先把它的结构在脑子里搭出来。CPU里负责取指令的部分叫程序计数器PC和指令寄存器IR,负责算数的叫ALU,负责临时放数的叫寄存器组;外面挂一块存储器和一组I/O,所有这些通过一根总线连起来。总线再细分成地址总线、数据总线和控制总线,但关键是——指令和数据共用它。

这就带来一个绕不开的后果:同一时刻,CPU要么在取指令,要么在读写数据,不能两头兼顾。因为指令和数据在同一个地址空间里,你没法从地址上区分"这是指令还是数据",只能靠时序上先后来错开。这个概念有个专门的叫法,叫冯·诺依曼瓶颈。

2.1 一条指令背后的访存次数

我拿一个最普通的"两个数相加存回内存"来算笔账。假设用的是冯·诺依曼结构的机器,一条总线,所有访存串行执行,每次访存占一个机器周期。执行 ADD A, B 大概要经历这几步:

  1. 取指令:从内存读回这条ADD指令,1次访存
  2. 取操作数A:从内存读出A,1次访存
  3. 取操作数B:从内存读出B,1次访存
  4. ALU执行加法
  5. 存结果:把和写回内存,1次访存

算下来,光访存就用了4个周期,加上执行,一条指令可能吃掉5个周期。注意这4次访存是完全串行的,因为它们抢同一条总线。哈佛架构下,取指令走指令总线,取操作数A、B走数据总线,理论上取指和取数能并行,这条指令就有机会压到3个周期甚至更少。别小看这2个周期的差距,跑一个上万次的循环,差距就是几万周期。

注意:上面是简化模型,真实芯片有流水线、缓存、多周期总线,数字不会这么干净。但"冯诺依曼要多花访存次数"这个结论是成立的,这也是理解架构差异的起点。

2.2 冯诺依曼瓶颈带来的实际影响

对我们写代码的人来说,这个瓶颈最直观的体现就是"代码越复杂、访存越频繁,越容易撞墙"。比如你在没有指令缓存的老架构上跑一个密集的数组运算,每读一个元素都要走总线,而取指令也要走总线,两边来回抢,CPU空转的时间有时比干活的还多。

反过来说,冯·诺依曼架构也有它的好处:结构简单,指令和数据统一编址,编译器写起来方便,程序可以从内存里任意位置加载执行,灵活性高。这也是为什么通用计算机和现代操作系统偏爱它——程序动态加载、链接、共享内存这些机制,都建立在统一地址空间之上。

我个人的体会是,冯·诺依曼适合"通用、灵活、跑大程序"的场景,而嵌入式里那些"实时、确定性、小循环高频"的活,往往更适合哈佛。

3. 哈佛架构:指令和数据各修一条路

哈佛架构把指令和数据物理隔离,指令存在程序存储器(比如Flash),数据存在数据存储器(比如RAM),两者有各自的地址空间、各自的总线。CPU取指令的同时,可以从数据存储器里取操作数,两件事同时发生,不需要排队。这就是它比冯·诺依曼快的地方。

这个设计最初来自哈佛大学Mark I计算机,后来被微控制器和DSP发扬光大。原因很现实:嵌入式里大量是控制循环和信号处理,指令流短而重复(适合放Flash),数据流密集且需要实时读写(适合放RAM),两者分离正好对症下药。

3.1 并行取指带来的好处

还是拿前面那条ADD来对比。哈佛架构下,CPU一个周期里可以一边从指令存储器取指令、一边从数据存储器取操作数,取指和取数不再互斥。虽然操作的"步数"没变,但因为并行,实际耗时会明显缩短。

这个优势在流水线结构里会被进一步放大。现代处理器把指令执行拆成取指、译码、执行、写回等阶段,流水线让多条指令同时处在不同阶段。如果取指和数据访问共用一条总线,流水线一到访存阶段就可能"打嗝";分开之后,取指阶段和数据阶段各用各的通道,流水线能跑得更顺,这就是为什么大多数高性能微控制器和DSP选哈佛。

我调过一段音频采样处理,在哈佛结构的DSP上,采样读入和系数取用几乎不打架;换到同频的冯诺依曼平台上,同样算法覆盖率就掉下来了。这差距不是玄学,就是总线并行与否的问题。

3.2 8051、AVR、DSP为什么选它

经典的8051就是哈佛架构的代表:程序存储器(CODE空间)和数据存储器(DATA空间)地址独立,指令用MOVC从程序存储器读常量。AVR同样把Flash和SRAM分开,常量要靠专门的LPM类指令读取。多数DSP(用于音频、电机、通信信号处理)选择哈佛,是因为它们需要在一个周期内同时取一条指令和两个操作数,冯·诺依曼的单总线根本喂不饱。

这些选择背后有一条清晰的逻辑:当"同时访问指令和数据"成为性能刚需时,哈佛是更自然的答案。你在嵌入式项目里只要涉及到实时信号处理、电机控制、音频算法,多半会遇到哈佛血统的芯片。

3.3 哈佛架构的隐性代价

哈佛不是没有缺点。指令和数据分开,意味着你要多一条总线、多一套存储逻辑,芯片面积和成本上去了;程序不能像冯诺依曼那样"随便从内存任何地方加载执行",代码和数据之间的搬运也更麻烦——比如你想把一段算法动态加载到RAM里跑,在纯哈佛架构上就得走特殊通道,甚至做不到。

还有个经常被忽略的点:常量数据放在程序存储器(Flash)里,CPU读它走的是数据通道,如果这个通道没有连到Flash,你就得先把它复制到RAM。这就是为什么很多嵌入式代码里会有一句"把查找表搬到RAM",原因往往就在这里。这条经验,是我在实际调音频卷积时才真正咬牙记住的。

4. 别被"地址空间"骗了:改进哈佛架构才是你正在用的

讲到这里,很多人脑子里的模型是"冯诺依曼=统一地址,哈佛=分离地址"。这个理解只对了前半段。现实中大量芯片是"地址空间统一、物理总线分离"的改进哈佛架构(Modified Harvard),你现在手上跑的STM32、很多ARM核,都属于这一类。

我一开始也混淆过:既然STM32的指针能一视同仁地指向Flash和RAM,那它不就是统一编址、不就是冯诺依曼吗?后来才知道,编址方式和物理结构是两码事。ARM Cortex-M把整个4GB空间统一编成一整块,程序看过去是连续的,但芯片内部给指令和数据留了不同的通道。

4.1 统一编址不等于冯·诺依曼

判断一个架构,看的不是"地址连不连续",而是"指令和数据是不是共用同一条物理通路"。这一点特别容易被地址空间误导:

  • 如果一个芯片地址空间分离、通路也分离,那它是纯哈佛(如经典8051、AVR)。
  • 如果地址空间统一、通路也统一,那是纯冯·诺依曼(如x86、MSP430)。
  • 如果地址空间统一、但指令和数据通路分离,那就是改进哈佛——这正是Cortex-M的做法。

所以"STM32是哈佛架构吗"这个问题,严谨的答案是:它是改进哈佛架构,程序看到了统一的地址空间,但硬件层面给取指和取数准备了不同的总线。

4.2 Cortex-M的ICode、DCode和System总线

具体到Cortex-M3/M4,芯片内部有几组总线,分工非常明确:

总线名称负责内容连接对象
ICode总线取指令Flash / 程序存储器
DCode总线取数据、查表常量Flash / 程序存储器
System总线访问外设、SRAM数据读写外设、RAM

这张表解释了很多现象:取指走ICode,取数据走DCode,两者可以同时进行——这就是并行取指的硬件基础。但注意,ICode和DCode虽然逻辑上分开,很多芯片里它们连的是同一块Flash,Flash本身一次只能服务一次访问,所以实际并行程度还要看Flash的读带宽和等待周期。这就引出了后面要讲的Flash等待周期。

4.3 四种情况一张表分清

我把容易混的四种情况整理成表,方便你对照记忆:

架构类型地址空间指令/数据通路典型代表
纯冯·诺依曼统一共用一条x86、MSP430
纯哈佛分离各自独立8051、AVR、部分DSP
改进哈佛统一物理分离ARM Cortex-M3/M4
混合(缓存分离+主存统一)统一缓存层分离现代高性能CPU

提示:面试被问到"STM32是什么架构"时,直接答"改进哈佛"并补一句"统一编址但取指取数总线分离",比单纯答"哈佛"显得懂行得多。

5. 光看理论没用:架构差异怎么在调试台上露头

架构这知识,光背概念很快就会忘,只有当你亲手在嵌入式开发里撞上它,才记得住。我下面说几个自己在调试台上真实遇到的现象,你会发现它们其实都是架构差异在作祟。

5.1 从C代码到反汇编,看取指和取数

想知道你的代码在架构上是怎么跑的,最直接的办法是看反汇编。用GCC编译一个简单函数,加上-O2 -S,你会看到类似下面的东西(以Cortex-M为例):

ldr r3, [pc, #16] ; 从Flash取常量地址(走DCode) ldr r2, [r3, #4] ; 从数据地址取操作数(走System总线) mul r2, r2, r1 ; 执行乘法 str r2, [r3, #8] ; 写回结果

你会发现,取指令走的是ICode,取常量走DCode,取RAM数据走System总线——三条路各司其职,这就是改进哈佛带来的并行性。如果你换一片纯冯诺依曼芯片,反汇编里取指令和取数据会挤在一起,流水线更容易停顿。

顺着这个思路,用vscode常用插件 嵌入式开发 c++里的Cortex-Debug、或者配合反汇编工具看嵌入式内核源码的启动过程,你能直接观察到不同架构下指令流的差别。这种"看着总线干活"的体验,比看十页教材管用。

5.2 中断响应、DMA抢总线、Flash等待周期

架构差异在下面几个场景里特别明显:

中断响应。中断来的时候,CPU要保存现场、跳转到中断向量、取中断服务函数的第一条指令。哈佛架构下,取指令和数据保存可以并行一些,响应更快;冯诺依曼下,这两件事要排队。所以同样是72MHz,不同架构的芯片中断延迟可能差出好几个周期,这在嵌入式rtc 常见硬件电路、定时器捕获这类对时序敏感的场景里很要命。

DMA抢总线。DMA控制器搬数据的时候要占用总线。在统一总线的冯诺依曼架构下,DMA搬运会和CPU取指、取数抢带宽,CPU可能被"饿"到;哈佛架构因为有多条总线,DMA占用数据通道时,CPU还能继续从指令通道取指,影响小一些。这就是为什么高带宽搬运场景,架构越"宽"的芯片越占优。

Flash等待周期。这个陷阱太常见了。Flash的读取速度通常跟不上CPU主频,所以芯片会插入等待周期(Wait State)。如果指令和数据都放在Flash里(比如常量数组没搬到RAM),每次访问都撞等待周期,速度就下来了。我有一回优化一段查表算法,怎么改逻辑都不快,最后把查找表从Flash搬进RAM,速度直接上了一个台阶——根源就是ICode/DCode都要访问Flash,并行优势被等待周期吃掉了。

5.3 两个我调过的真实场景

第一个场景是STM32上跑电机FOC。电流环要在一个PWM周期内完成采样、克拉克变换、帕克变换、PID、反变换、SVPWM,代码密集、循环小、反复执行。STM32的改进哈佛结构让取指和数据访问分开,配合把关键系数和中间变量放RAM,实时性才稳得住。如果把常量表留在Flash,等待周期一上来,电流环就容易抖。

第二个场景是音频处理。在一颗哈佛DSP上做FIR滤波,一个周期要取一条指令加两个操作数,哈佛的分离总线刚好满足,吞吐拉满;换到统一总线的低端MCU上,同样算法覆盖率掉一半,只能降采样或者换芯片。

这两个例子说明同一件事:架构决定了芯片"上限"在哪,你的代码优化只能在架构给的框里腾挪。选型阶段多花半小时搞清架构,比后期熬夜优化省事得多。

6. 缓存与DMA把界限搅浑:现代芯片为什么两只手都用

如果你觉得上面讲的已经够复杂,那缓存和DMA会把水搅得更浑。现代芯片很少纯走一条路,而是在指令和数据之间做取舍,这也就是为什么会出现"哈佛式缓存、冯诺依曼式主存"这种混合设计。

6.1 哈佛式缓存,冯诺依曼式主存

在高性能CPU里,你会发现一级缓存(L1)是分开的:指令缓存I-Cache和数据缓存D-Cache分离,这其实就是把哈佛思想搬到了缓存层;但往下走,主存是统一的,指令和数据都从同一块内存加载——这又是冯·诺依曼。这种设计兼顾了"取指取数并行"和"程序灵活加载"两个需求。

很多中高端MCU(比如带缓存的Cortex-M7)也采用类似思路:Cache层分离提升并行性,主存统一保持编程简单。你写代码的时候可能感觉不到这套机制,但一旦牵涉到Cache一致性——比如DMA把新数据直接写进RAM,而CPU的D-Cache里还存着旧值——问题就来了。

6.2 DMA和总线矩阵怎么配合

DMA和总线之间的配合,能直观看出架构的"宽窄"。在改进哈佛的Cortex-M芯片里,内部有个总线矩阵,让CPU、DMA、外设可以同时访问不同的存储区域而少打架。DMA搬RAM数据时,CPU取指走ICode,两者各走一路,冲突被降到最低。

但要注意一个坑:DMA和外设常常只能访问RAM或特定区域,不能直接访问Flash里的数据。所以如果你想让DMA搬运一张放在Flash里的表,往往得先把表搬到RAM。这背后就是哈佛架构"程序存储器"和"数据存储器"分工的老逻辑,换了个现代马甲而已。

6.3 常量为什么要搬到RAM

结合前面的分析,我把"常量搬到RAM"这件事的逻辑完整捋一遍,这是我在实际项目里反复用到的经验:

  1. Flash有等待周期。CPU主频越高,Flash读越跟不上,指令和数据都撞等待周期。
  2. ICode和DCode可能共用Flash。改进哈佛的并行优势,在取指和取常量都走Flash时会被削弱。
  3. DMA可能访问不到Flash。想让DMA搬数据,数据得在RAM。

所以对性能敏感、或者要让DMA参与的表和常量数组,搬进RAM是常规操作。代价是RAM资源有限,得权衡。我一般会优先搬那些"高频访问 + 体积不大"的查找表,用const加段定位或者链接脚本把它放到RAM区,效果立竿见影。

注意:把常量搬RAM会占用宝贵的内存,还增加启动时的复制时间。不是所有常量都值得搬,先用性能分析确认瓶颈,再动手。

7. 嵌入式学习路线上,这块知识该学到什么深度

写到这里你可能想问:这块知识到底要学到多深?我是不是得把每条总线的时序都背下来?我的答案是:知道原理、会用结论、能解释现象就够了,别陷进微架构的细节里。除非你要去做芯片设计或者写BSP,否则没必要把总线仲裁协议啃到底。

7.1 面试里它通常怎么问

结合我这些年看过的嵌入式八股和嵌入式面试题,架构这个问题通常不会单独考,而是嵌在别的问题里:

  • "为什么你的中断响应这么设计?" —— 背后是取指和访存能否并行。
  • "怎么优化这段循环的性能?" —— 背后是等待周期和取数带宽。
  • "STM32和8051在执行效率上有什么差异?" —— 背后就是两种架构。
  • "DMA为什么不能直接搬Flash的数据?" —— 背后是程序存储器和数据存储器的分工。

回答这类问题的诀窍是:先说架构,再说结论,最后联系实际现象。比如被问性能优化,你可以先讲"这股芯片是改进哈佛,取指和取数分离,但Flash有等待周期",再给"把热点数据搬RAM"的方案。这样答,比干背八股有说服力得多。

7.2 别陷进去:什么时候该停

学架构最容易犯的错,是越钻越细,最后迷失在总线协议里。给自己划条线:

  • 该掌握:冯诺依曼、哈佛、改进哈佛的区别;统一编址不等于统一通路;Cortex-M的ICode/DCode/System分工;等待周期、Cache一致性、常量搬RAM这几条实用结论。
  • 可以了解:总线矩阵、流水线阶段、DMA与Cache的交互。
  • 不必深究(除非是芯片方向):物理层总线时序、仲裁算法细节、缓存替换策略。

把精力花在"能解释现象、能指导优化"的层面,性价比最高。

7.3 一个可以照着走的学习路径

如果你想系统补这块,我建议按这个顺序来,我自己也是这么补的:

  1. 先看教材概念,把两种架构的定义和区别搞清楚,能画出示意图。
  2. 拿一片手边的芯片(比如蓝桥杯用的STM32G431,Cortex-M4,改进哈佛),翻它的参考手册,找到总线和存储章节。
  3. 写一段小代码编译反汇编,观察取指、取常量、取RAM数据分别走哪条路。
  4. 做一次对比实验:同一个算法,常量放Flash和放RAM各跑一遍,测时间。
  5. 进阶:看怎么用链接脚本控制数据段位置,理解const和多段定位的用法。

这条路径不依赖任何付费课程,也和你用vscode开发嵌入式编程还是别的IDE无关,动手做一遍就懂了。第17届蓝桥杯嵌入式省赛那种题,本质上也是在考你对自己芯片存储结构的熟悉程度。

我自己现在的习惯是,接到一片新芯片,第一件事翻数据手册看它是什么架构、有几条总线、Flash有没有预取和等待周期配置。这套动作花不了十分钟,但能让我在后面写代码时心里有底,少走很多弯路。

后面如果你要做嵌入式环境监控这类对实时性和确定性要求高的项目,或者想深挖嵌入式开源项目里的启动代码,架构知识就是你理解那些代码的钥匙。它不解决某个具体外设怎么点灯,但它决定了你看待整颗芯片的方式——这大概就是它值得反复提的原因。

最后再分享一个小技巧:很多人纠结怎么记忆改进哈佛,其实你只要记住一句话——地址统一是为了好写程序,通路分离是为了跑得快。把这两句话对应到具体芯片上,架构这课就算过关了。

返回列表