3.2 程序执行流程:取指译码执行的循环


3.2 程序执行流程:取指译码执行的循环

本节摘要:程序在芯片上的运行,本质是一个永不停歇的循环:取指、译码、执行、写回。本节从复位后第一条指令讲起,完整走一遍顺序执行、条件分支、函数调用三种控制流的机器级过程,量化分析流水线与跳转的冲突,并用一个精确计时的案例演示"执行流程知识"如何变成时间预算能力。

跟着一条指令走完循环

上电复位后,CPU 从向量表取到初始 PC,指向第一条指令的地址。从那一刻起,一个循环接管一切:取指(按 PC 从 Flash 读出指令码,PC 预增)、译码(拆解比特,弄清做什么)、执行(ALU 或访存单元干活)、写回(结果落位,标志位更新)。这个循环每秒重复数千万次——72MHz 的意思是每秒七千二百万次节拍,主流单周期指令意味着理想状态下每拍完工一条。

用一个能纸上推演的小程序观察循环。假设三周期流水线,五条顺序指令的推进如下:

; 地址 指令 行为解读 0x200 MOVS R0, #0 ; R0 清零 0x202 MOVS R1, #10 ; 计数初值 10 0x204 ADDS R0, R0, R2 ; 循环体:累加 0x206 SUBS R1, R1, #1 ; 计数减一 0x208 BNE 0x204 ; 标志位非零则跳回累加,构成循环

顺序段每条一个周期完工;到 BNE 这条条件分支时事情变了——硬件已经把 0x20A、0x20C 的指令取进流水线,译码后才发现"要跳回 0x204",已取的两条作废清空,流水线重新从 0x204 灌起。这个循环跑十次,就有十次流水线冲刷。分支是流水线的天敌,这就是全部程序里"热路径少分支"优化的硬件根源。

三种控制流的机器级真相

C 语言的三大控制结构,在机器层面对应三种 PC 操作。

顺序执行是默认态:PC 自动递增,流水线满速流动,最理想的情况。

条件分支(if、循环)对应"条件成立才改 PC"。硬件上,比较指令更新标志位,分支指令查询标志位决定是否跳转。进一步,现代内核还做分支预测:猜测最可能的方向提前取指,猜对零开销,猜错付清空流水线的代价。Cortex-M 系列的部分型号带简单的静态预测,高端内核有动态预测器。写实时代码时有一条经验法则:把最常走的分支路径放在前面,帮助预测与预取。

函数调用是最大的一台戏。C 里一行 sum = add(a, b);,机器层面依次发生:参数按调用约定放进寄存器 R0、R1;执行跳转链接指令 BL,它做两件事——把返回地址(下一条指令位置)存入链接寄存器 LR,同时 PC 改道到函数入口;函数体执行,若要再调别人,先把 LR 压栈保命;结束时跳回 LR 指向的返回地址。函数层层嵌套,返回地址就在栈上层层叠叠——调用栈这个词的物理本体,就是栈上那串 LR 备份与局部变量的叠加

; main 调用 add 的机器级过程(简化) MOVS R0, #3 ; 第一个参数放进 R0 MOVS R1, #4 ; 第二个参数放进 R1 BL add_func ; LR 存返回地址,PC 跳到 add_func ; add_func 内部: ADDS R0, R0, R1 ; 结果放 R0(调用约定:返回值住 R0) BX LR ; 跳回 LR,返回 main

延迟隐蔽与精确计时:一个完整案例

背景:某项目要求用 GPIO 模拟时序驱动一款没有现成驱动的外设,规格书写"数据在时钟上升沿后 100 纳秒内有效"。操作:先做时间预算——72MHz 下一个周期约 13.9 纳秒,翻转一次 GPIO 的写寄存器指令约 1 到 2 个周期,加上函数调用开销与流水线状态,软件循环的位宽抖动实测在 60 到 120 纳秒之间浮动。结果:纯软件翻转的抖动越过了规格上限,时序偶发出错,示波器上表现为 occasionally 的数据错位。解读:抖动的来源正是本节的内容——Flash 等待周期、分支清空流水线、中断随时插入,三者让"软件精确延时"天生不可靠。变式:改用定时器硬件产生时钟沿(第 4 章的内容),软件只准备数据,抖动问题消失。这个案例的教训值得刻在脑子里:软件循环适合毫秒级粗粒度延时;任何百纳秒级的精确时序,都应该交给硬件外设。而知道这条界线在哪,靠的正是对执行流程每一步耗时的理解。

中断打断执行流:另一种"改道"

顺序、分支、调用之外,还有第四种改变 PC 的力量——中断,它不属于程序本身,而来自硬件(第 5 章的主角)。这里只需先建立位置感:中断响应在机器层面是一次"硬件版函数调用"——硬件自动保存关键现场、把 PC 改到向量表指定的服务程序入口、执行完再返回断点。它与函数调用的两点关键差异:其一,保存现场的时机不由程序控制,随时可能发生,所以任何时刻的数据结构都可能被打断——这是 5.3 节竞态问题的根源;其二,它打断了调用链的语义——被中断的函数并不知道自己被暂停过,栈上的调用链在中断期间多了一层"看不见的现场"。提前记住这幅图景,第 5 章讲到挂起、压栈、嵌套时会顺畅得多。

看懂反汇编:把知识装进调试器

掌握了执行流程,调试器的反汇编窗口就从天书变成路线图。几个立即可用的读法:跳转指令前的比较指令组,对应你的 if 语句;连续的压栈 PUSH 开场,是函数序言(保存现场);调用 BL 后 R0 的值就是返回值;一段没有分支的直线代码,是估算耗时的"安全区"。做嵌入式久了会形成一个习惯:关键路径的代码,写完就去反汇编里瞄一眼——看它有没有被编译器意外插入的调用、循环体有没有被优化得面目全非。这是对编译器保持"信任但验证"的工程姿态。

💡 关键直觉:程序的源代码是给人们读的故事,指令循环才是芯片经历的实质。调试诡异问题时,永远可以问一句"此刻 PC 在哪、流水线里有什么、栈上压着谁"——三个问题问完,问题往往自己现形。

一个必踩的坑:延时循环被编译器"优化没了"

把执行循环的知识用在真实事故上。有人在裸机里写软件延时:

void delay_ms(unsigned int ms) { unsigned int i; while (ms--) { for (i = 0; i < 8000; i++) { ; /* 空转,靠执行循环拖时间 */ } } }

开发环境里一切正常,换个优化等级重新编译,LED 闪烁快得反常——因为编译器看到"空循环不产生任何结果",直接把 8000 次空转删成了零次。这是取指执行模型与优化器视角的冲突:人看到的是"执行要花时间",编译器看到的是"没有产出"。规范解法有三:用硬件定时器延时(第 4 章的标准答案);在空转里插入内联空指令(它们本身是指令,不会被当废码删除);或给循环变量加 volatile 修饰,强制每次真实访存。这个坑的教训超出延时本身:凡是"靠执行过程本身产生效果"的代码,都要考虑优化器的存在

问题:中断响应也走这套循环吗?

走,但要加一条"硬件快速通道"。中断到来时,PC 的改道不经过"取指到跳转指令再译码"的软件路径,而是硬件直接查向量表、自动压栈、把取指流切到服务程序入口——这正是它比任何软件调用都快的原因。但进入服务程序之后,ISR 内部的每条指令仍回到同一个取指译码执行循环里排队。换句话说:改道是硬件抄近路,赶路仍是同一条路。这个图景也解释了为什么 ISR 里的分支同样付流水线冲刷的代价、为什么 ISR 耗时必须按指令周期数估算。

本节要点回顾

  • 四步循环:取指、译码、执行、写回,每秒数千万次,是程序运行的物理本体。
  • 分支的代价:跳转让已取指令作废、流水线冲刷;热路径少分支、常走路径放前面。
  • 函数调用的机器戏:参数住 R0、R1,BL 存返回地址并跳转,返回值住 R0,栈上叠着 LR 备份。
  • 时间预算意识:Flash 等待、分支冲刷、中断插入让软件延时天生抖动;纳秒级时序交给硬件。
  • 体系位置:程序跑起来的静态规则已齐,下一节看你的源码如何被加工成 Flash 里那串比特。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U