跟着下面的视频,系统梳理复习过往的嵌入式知识,同时也算是 电控成长指南 的一个 部分 实现,能有效且快速的建立一个嵌入式系统和工具链的整体认知
【《从CPU架构到操作系统实现》系列课程 - Git 开源】https://www.bilibili.com/video/BV1ksNCzXEny?vd_source=5a0790755035f26a67935abfbfcdfd5b
视频中详细且硬核的实例不会出现在此笔记中
概述
关于ARM架构
目前arm
内核主要演变分为Cortex-X
,Cortex-A
,Cortex-R
,Cortex-M
Cortex-A
:应用处理器,设计用来处理复杂应用(例如高端嵌入操作系统Linux
,iOS
,Android
,Windows
),对标PC和手机处理器的性能
Cortex-X
:比Cortex-M
更高的性能
Cortex-R
:Real-time
,主要用于实时系统
Cortex-M
:Microcontrol
,微处理器
Cortex-M优点
- 低功耗
- 高性能
- 中断易用
- 代码密度高(原因在于指令集)
- 可调式
- 支持操作系统
哈佛架构,指令与数据在同一内存
指令集
多套指令集用于处理不同的场景
1.CISC(Complex Instruction Set Computer)
复杂指令系统计算机,CISC
指令集包含大量复杂的指令,每条指令可以完成多个操作,这种设计使得编程简单,但硬件实现复杂,执行效率低
x86
架构(Intel
的8086,8088,80286,80386)
2.RISC(Reduced Instruction Set Computer)
精简指令系统计算机,RISC
指令集包含较少的简单指令,每条指令只完成一个操作,使得硬件实现更简单,执行效率更高,但编程更复杂
ARM
架构,RISC-V
架构,MIPS
架构
开发环境(DEV)
GUN-GCC
编译器,CMake
->makefile
STM32F103C8(C8T6)
经典控制器(Cortex-M3
)
编译
编译链工具
ARM-Cortex-M
处理器的编译器很多,常用的是MDK-ARM
(ARM
公司自研的)(Keil
),以及GCC
(GNU-Compiler-Collection
)
编译器之间的最大区别在于汇编伪代码,链接语法,还有使用参数
arm-none-eabi
arm
是指arm
架构,none
是指编译出的可执行文件是运行在硬件上,而不是操作系统上,eabi(The Embedded-Application Binary Interface)
嵌入式应用二进制接口
arm-none-eabi-objcopy
是一个二进制生成工具
嵌入式编译烧录流程
个人更喜欢将编译过程拆解为 预编译,编译,汇编,链接
转换这里是将.elf
文件转换为.bin
文件
.bin
文件是代码的纯机器码
预编译
将所有头文件和宏展开,同时去掉注释,生成.i
文件(c
中间文件)
编译
将.i
文件编译为.s
汇编文件,编译优化发生在此处
汇编
将.s
文件汇编为.o
二进制文件(机器码)
二进制文件中还包含很多的其他信息
在汇编时,可以指定汇编器的参数来选择指令集
链接
链接是将各个二进制文件里面的机器码所在的各个段拼接起来,以形成可执行文件.elf
(带有调试信息)
在链接这个阶段才能检测出有不有重复定义和未定义的错误
对于GNU
来说,可以编写.ld
链接脚本来编排代码的分区地址
转换
通过转换器能将.elf
文件转换为.hex
和.bin
文件,主要用于提取信息
.bin
文件只包含二进制机器码
.hex
文件是Intel
定义的包含地址和二进制数值的ASC
码文本文件
烧录
烧录的过程是将编译好的机器码写到CPU
存储器中
反汇编
GCC
中的反汇编工具Disassembler
是arm-none-eabi-objdump
启动
主要是讲解STM32
的启动流程
中断向量表
中断向量表的构建,根据不同的编译工具,有不同的方式构建
中断向量表的前两项(.woed,4字节)是最关键的
第一项是堆栈指针的值
第二项是Reset_Handler
函数入口
在启动时,需要先设置堆栈指针,否则无法进行压栈的操作,从宏观上说,意味着C
语言函数体不能调用子函数
VTOR寄存器,中断向量表偏移寄存器
启动原理
启动后,从零地址(零地址是经过映射的)出读取堆栈指针和程序计数器的值(此时程序计数器的值指向Reset_Handler
),此外每个芯片的Boot code
是定制化的,需要参考手册
最后Reset_Handler
会指向main
函数
以STM32F103
为例
这里有三种启动模式,根据启动时BOOT1
和BOOT0
的电平来选择不同的启动地址(即 将该不同的地址映射为零地址),一般情况下是第一种
RAM内存分布
启动文件详解
一、STM32F103 启动文件(startup_stm32f103xb.s)核心定位
STM32 的启动文件(如startup_stm32f103xb.s
)是汇编语言编写的底层程序,是硬件复位后执行的第一段代码,负责衔接 “硬件初始化” 与 “用户 C 代码(main
函数)”。其核心功能包括:设置栈指针、构建中断向量表、初始化数据段(.data
)、清零未初始化数据段(.bss
)、配置系统时钟,最终引导程序进入main
函数,是 STM32 程序运行的 “底层基石”。
二、启动文件整体结构拆解
该启动文件按功能可划分为 5 个核心模块,各模块职责明确且环环相扣:
- 汇编环境配置:指定处理器架构、指令集、浮点模式,确保汇编代码与硬件兼容。
- 关键符号声明:引用链接器脚本(
.ld
)中定义的内存地址符号,建立启动文件与内存布局的关联。 - 复位处理函数(
Reset_Handler
):复位后执行的核心逻辑,完成初始化并跳转到main
。 - 中断向量表(
g_pfnVectors
):存储所有异常 / 中断的处理函数地址,供内核响应中断时调用。 - 默认中断处理函数(
Default_Handler
):未自定义的中断会默认进入此处,避免系统崩溃。
三、逐段解析启动文件代码
1. 汇编环境配置(开头基础设置)
|
|
- 作用:告诉汇编器(如
arm-none-eabi-as
)“如何编译这段代码”,确保生成的指令能被 STM32F103 正确执行。 - 关键细节:
softvfp
对应 STM32F103 的硬件特性 —— 该型号无硬件浮点处理单元(FPU),需通过软件模拟浮点运算,与之前解析的.ld
文件中未配置 FPU 参数完全匹配。
2. 关键符号声明(与链接器脚本联动)
|
|
- 核心意义:通过
.word
指令声明的符号,直接引用.ld
文件中根据硬件内存布局(64K FLASH、20K RAM)计算出的地址。例如:_sdata
和_edata
确定了.data
段在 RAM 中的范围,_sidata
确定了其初始值在 FLASH 中的位置,为后续 “数据段复制” 提供地址依据。- 若缺少这些符号,启动文件无法知道 “数据该从哪里复制到哪里”,内存初始化会彻底失败。
3. 复位处理函数(Reset_Handler
:启动核心逻辑)
Reset_Handler
是复位后执行的第一个函数(由中断向量表指定),对应 STM32 启动流程中的 “软件初始化” 阶段,代码逻辑可分为 5 步:
asm
|
|
关键步骤解读:
- 系统时钟初始化(
SystemInit
):STM32 复位后默认使用 8MHz 的内部高速时钟(HSI),SystemInit
函数会配置外部高速时钟(HSE)和锁相环(PLL),将系统时钟提升到 72MHz(F103 的常用工作频率),确保外设和 CPU 高效运行。 - .data 段复制:
.data
段存储 “已初始化的全局 / 静态变量”(如int a = 10;
),这些变量需要读写权限,因此运行时必须在 RAM 中;但编译时其初始值会存放在 FLASH(节省 RAM),需通过此步骤复制到 RAM。 - .bss 段清零:
.bss
段存储 “未初始化的全局 / 静态变量”(如int b;
),C 语言标准要求其初始值为 0,因此无需在 FLASH 中存储初始值,直接将 RAM 对应区域清零即可。 - 跳转到
main
:这是启动文件的 “最终目标”—— 完成所有底层初始化后,将程序控制权交给用户代码,正式进入应用逻辑。
4. 中断向量表(g_pfnVectors
:中断响应的 “地址簿”)
中断向量表是Cortex-M3 内核复位后访问的第一个数据结构,存储了所有异常(如复位、硬 fault)和外设中断(如串口、定时器)的处理函数地址。当发生中断 / 异常时,内核会从向量表中找到对应地址,跳转到处理函数执行。
asm
|
|
核心特性:
- 位置固定:在
.ld
文件中,.isr_vector
段被强制分配到 FLASH 起始地址(0x8000000
),这是 Cortex-M3 内核的硬件规定 —— 复位后内核会自动从0x8000000
读取向量表。 - 第 0 个元素必为栈顶:内核复位后做的第一件事是 “初始化栈指针(MSP)”,因此向量表第 0 个元素必须是栈顶地址(
_estack
,来自.ld 文件),否则后续函数调用、局部变量存储会因栈地址错误崩溃。 - 中断优先级隐含:向量表中元素的顺序对应中断 / 异常的优先级(靠前的优先级更高),例如 “硬 fault”(第 3 个元素)优先级高于 “SysTick”(第 15 个元素)。
5. 默认中断处理函数(Default_Handler
:中断 “兜底” 逻辑)
当用户未为某个中断定义处理函数时,中断向量表会将该中断的地址指向Default_Handler
,避免系统因 “找不到中断处理函数” 而进入未知状态。
|
|
弱别名机制(用户可重定义中断函数)
启动文件通过 “弱别名(.weak
)” 允许用户在 C 代码中重定义中断处理函数,具体逻辑如下:
|
|
- 工作原理:
.weak
标记表示 “该符号是弱定义的,若用户定义了同名符号,优先使用用户定义的版本”。- 用户只需在 C 代码中编写
void USART1_IRQHandler(void) { ... }
,链接时会自动覆盖启动文件中的弱定义,使串口 1 中断触发时执行用户代码。 - 若用户未定义,则使用默认的
Default_Handler
(无限循环)。
四、启动文件与链接器脚本(.ld)的配合关系
启动文件与.ld
文件是紧密耦合的 “搭档”,二者通过 “全局符号” 和 “段分配” 协同工作,缺一不可:
协作维度 | 启动文件角色 | 链接器脚本(.ld)角色 |
---|---|---|
内存地址提供 | 引用.ld 定义的_sdata 、_ebss 、_estack 等符号 |
根据硬件内存布局(64K FLASH、20K RAM)计算并定义这些符号 |
段分配依据 | 将Reset_Handler 放入.text 段、向量表放入.isr_vector 段 |
规定.text 、.isr_vector 等段的存储位置(如.isr_vector 分配到 FLASH 起始地址) |
程序入口指定 | 实现Reset_Handler 函数(入口逻辑) |
通过ENTRY(Reset_Handler) 指定程序入口为该函数 |
内存合法性检查 | 依赖.ld 确保_sdata 、_ebss 不超出 RAM 范围 |
检查堆、栈、数据段总大小是否超过 RAM/FLASH 容量,溢出则报错 |
五、总结:启动文件的核心价值
STM32 启动文件是 “从硬件到软件的桥梁”,其核心作用可概括为 3 点:
- 硬件初始化:完成栈指针设置、系统时钟配置,为程序运行提供基础硬件环境。
- 内存初始化:通过复制
.data
段、清零.bss
段,确保全局 / 静态变量符合 C 语言标准要求。 - 中断系统准备:构建中断向量表,提供默认中断处理逻辑,支持用户自定义中断函数。
- 引导用户代码:最终跳转到
main
函数,将程序控制权交给用户,完成 “底层初始化→应用逻辑” 的过渡。 简言之:没有启动文件,STM32 复位后无法完成任何初始化,更无法进入main
函数 —— 它是 STM32 程序运行的 “第一块拼图”。
STM32 启动流程、.ld 文件作用及示例解析
要理解 STM32 的启动流程、.ld
文件(链接器脚本)的作用,以及你提供的具体.ld
文件内容,我们需要从宏观启动逻辑到链接器脚本细节逐步拆解,最终建立两者的关联。
一、STM32 的启动流程(从复位到main
函数)
STM32 的启动流程是硬件复位→内核初始化→软件初始化→进入main
的过程,核心依赖「中断向量表」和「启动文件」,而.ld
文件为这个流程提供了内存地址映射和段分配规则。
1. 硬件复位:PC 指针的初始指向
当 STM32 上电或复位时,Cortex-M 内核会自动执行以下操作:
- 读取中断向量表的第一个元素(栈顶地址
_estack
),初始化主栈指针(MSP)—— 这是内核执行的第一个动作,因为函数调用、局部变量都依赖栈。 - 将程序计数器(PC)指向中断向量表的第二个元素(
Reset_Handler
的地址)—— 这是复位后的第一个执行函数(启动流程的入口)。
关键:中断向量表的默认地址是
0x8000000
(STM32 FLASH 的起始地址),这也是.ld
文件中FLASH
的ORIGIN
(起始地址)设置为0x8000000
的原因。
2. 执行Reset_Handler
(启动文件核心逻辑)
Reset_Handler
是启动文件(如startup_stm32f103c8tx.s
)中的汇编函数,是软件初始化的核心,其逻辑完全依赖.ld
文件定义的全局符号(如_sdata
、_ebss
),具体步骤如下:
-
初始化数据段(.data):
.data
段存放「初始化过的全局 / 静态变量」(如int a = 10;
),这些变量需要读写权限,因此运行时必须在 RAM 中;但编译时会把它们的初始值存放在 FLASH 中(节省 RAM 空间)。
Reset_Handler
会执行复制操作:从.ld
文件定义的_sidata
(.data 在 FLASH 的加载地址)复制到_sdata
(.data 在 RAM 的运行地址),直到_edata
(.data 的结束地址)。 -
清零未初始化数据段(.bss):
.bss
段存放「未初始化的全局 / 静态变量」(如int b;
),C 语言标准要求它们初始化为 0。
Reset_Handler
会将_sbss
(.bss 的起始地址)到_ebss
(.bss 的结束地址)的 RAM 区域全部清零。 -
初始化系统时钟(SystemInit):
调用SystemInit
函数(由 STM32 标准库 / LL 库提供),配置 HSE/PLL 等,将系统时钟从默认的 HSI(8MHz)切换到更高频率(如 72MHz)。 -
调用
main
函数:
完成所有初始化后,跳转到 C 语言的main
函数,进入用户应用逻辑。
3. 异常 / 中断的触发(补充)
若程序运行中触发中断(如定时器、串口),内核会再次读取「中断向量表」,根据中断号找到对应的中断服务函数(ISR)地址,跳转到 ISR 执行,执行完成后返回断点继续运行。
二、.ld 文件(链接器脚本)的核心作用
.ld
文件是链接器(ld)的配置文件,其本质是告诉链接器:
- 芯片的内存布局:FLASH(只读、存代码 / 常量)和 RAM(可读写、存变量 / 栈 / 堆)的起始地址和大小。
- 代码 / 数据的段分配规则:将编译生成的各个「段(Section)」(如
.text
、.data
)分配到对应的内存区域(FLASH/RAM)。 - 定义全局符号:生成
_sdata
、_ebss
、_estack
等符号,供启动文件和用户代码使用(如启动文件依赖这些符号初始化内存)。 - 内存合法性检查:确保堆、栈、数据段的总大小不超过 RAM/FLASH 的实际容量,避免内存溢出。
简单说:没有
.ld
文件,链接器无法知道 “代码该放哪里、变量该放哪里”,无法生成可执行的.elf
/.hex
文件。
三、解析 STM32F103C8Tx 的.ld 文件
以下按文件结构逐段解析,结合 STM32F103C8Tx 的硬件参数(64K FLASH、20K RAM)说明每部分的作用。
1. 入口点与栈顶定义
|
|
ENTRY(Reset_Handler)
:强制链接器将Reset_Handler
作为程序第一个执行的函数,与启动流程中 PC 指向Reset_Handler
完全对应。_estack
:定义栈顶地址(STM32F103C8Tx 的 RAM 是0x20000000~0x20004FFF
,因此_estack=0x20005000
),启动文件会将这个地址加载到 MSP(主栈指针)。- 堆 / 栈大小:
_Min_Heap_Size
和_Min_Stack_Size
是最小要求,若用户代码中堆 / 栈使用超过这个值,链接器会报错(防止 RAM 溢出),可根据需求调整(如栈不够时增大_Min_Stack_Size
)。
2. 内存区域定义(MEMORY 块)
|
|
这是.ld
文件的核心,定义了芯片的物理内存属性:
RAM (xrw)
:xrw
:权限(x
= 可执行,r
= 可读,w
= 可写)——RAM 支持读写,理论上可执行(但通常不放代码)。ORIGIN = 0x20000000
:STM32 所有 Cortex-M3 内核芯片的 RAM 起始地址(硬件规定)。LENGTH = 20K
:STM32F103C8Tx 的 RAM 实际大小(20KB = 0x5000 字节,范围0x20000000~0x20004FFF
)。
FLASH (rx)
:rx
:权限(r
= 可读,x
= 可执行,w
= 不可写)——FLASH 是只读存储器,只能存放代码和常量。ORIGIN = 0x8000000
:STM32 FLASH 的起始地址(硬件规定,复位后 PC 默认指向这里)。LENGTH = 64K
:STM32F103C8Tx 的 FLASH 实际大小(64KB = 0x10000 字节,范围0x8000000~0x800FFFF
)。
3. 段分配规则(SECTIONS 块)
SECTIONS
块是最复杂的部分,定义了「编译生成的段」如何映射到「MEMORY 块定义的物理内存」,每一个子段都对应特定的代码 / 数据类型。
(1).isr_vector:中断向量表(放 FLASH)
|
|
- 作用:存放 STM32 的中断向量表(如复位向量、定时器中断向量、串口中断向量等),是复位后内核第一个访问的段。
KEEP(*(.isr_vector))
:中断向量表由启动文件定义(如startup_stm32f103c8tx.s
中的g_pfnVectors
数组),KEEP
关键字确保链接器不会因为 “看似未被调用” 而删除这个段 —— 如果删除,复位后内核找不到Reset_Handler
地址,程序会崩溃。>FLASH
:明确将该段分配到MEMORY
块定义的FLASH
区域(符合硬件要求,向量表默认在 FLASH 起始地址)。
(2).text:代码段(放 FLASH)
|
|
- 作用:存放所有可执行代码(C 函数、汇编函数),是 FLASH 中占用空间最大的段。
_etext = .
:.
代表当前地址,_etext
是全局符号,标记.text
段的结束地址 —— 后续的.rodata
段会从_etext
之后的地址开始分配,避免段重叠。
(3).rodata:只读常量段(放 FLASH)
|
|
- 作用:存放只读数据,因为这些数据不需要修改,放在 FLASH 中可节省 RAM 空间。
- 示例:
const char str[] = "STM32";
会被编译到.rodata
段,运行时直接从 FLASH 读取,不会复制到 RAM。
(4).ARM.extab/.ARM:ARM 架构兼容段(放 FLASH)
|
|
- 作用:主要用于ARM 架构与 Thumb 架构的兼容,以及 C++ 的异常处理(若项目用纯 C,这些段几乎为空,但保留可避免链接错误)。
READONLY
:标记为只读,与 FLASH 的权限匹配(GCC11 + 支持,低版本需删除)。
(5).preinit_array/.init_array/.fini_array:初始化 / 结束函数数组(放 FLASH)
|
|
- 作用:存放程序启动 / 退出时自动执行的函数数组(主要用于 C++),例如:
init_array
:main
函数执行前,会自动调用这里的函数(如全局对象A a;
的构造函数)。fini_array
:main
函数退出后,会自动调用这里的函数(如全局对象的析构函数)。
PROVIDE_HIDDEN
:若用户代码未定义__init_array_start
等符号,链接器自动提供(隐藏符号,避免冲突);KEEP
确保这些段不被优化删除。
(6).data:初始化数据段(放 RAM,加载地址在 FLASH)
|
|
- 核心概念:
VMA
(虚拟 / 运行地址)和LMA
(加载地址):>RAM
:.data
段的运行地址(VMA) 在 RAM(因为变量需要读写)。AT>FLASH
:.data
段的加载地址(LMA) 在 FLASH(编译时将变量初始值存到 FLASH,节省 RAM)。
- 与启动流程关联:启动文件的
Reset_Handler
会从_sidata
(FLASH 的 LMA)复制数据到_sdata
(RAM 的 VMA),直到_edata
—— 这就是 “初始化.data 段” 的具体实现。 .RamFunc
:某些对执行速度要求高的函数(如高频中断服务函数),可通过__attribute__((section(".RamFunc")))
标记,链接时会放到 RAM 中执行(RAM 访问速度比 FLASH 快)。
(7).bss:未初始化数据段(放 RAM)
|
|
- 作用:存放未初始化的全局 / 静态变量,C 语言标准要求这些变量初始化为 0,因此不需要在 FLASH 中存储初始值(节省 FLASH 空间)。
- 与启动流程关联:启动文件的
Reset_Handler
会将_sbss
到_ebss
的 RAM 区域清零 —— 这就是 “清零.bss 段” 的具体实现。 COMMON
:对应未声明但外部引用的全局变量(如extern int c;
),链接时会分配到.bss 段。
(8)._user_heap_stack:堆和栈的占位段(放 RAM)
|
|
- 作用:为堆和栈预留空间,并检查 RAM 是否足够 —— 若堆 + 栈 + 已分配段(.data+.bss)的总大小超过 RAM 容量,链接器会报错。
end
/_end
:标记 RAM 中 “已分配段(.data+.bss)” 的结束地址,堆从end
开始向上生长,栈从_estack
(栈顶)开始向下生长。- 堆 / 栈生长方向:
- 堆:从
end
向上(地址增大方向)生长,供malloc
/free
使用。 - 栈:从
_estack
向下(地址减小方向)生长,供函数调用、局部变量使用。 - 若堆和栈生长时重叠,会导致内存溢出(硬 fault),因此需合理设置
_Min_Heap_Size
和_Min_Stack_Size
。
- 堆:从
(9)/DISCARD/:丢弃无用段(减小可执行文件大小)
|
|
- 作用:嵌入式系统通常不需要标准库的完整功能(如
printf
的文件输出、malloc
的复杂内存管理),丢弃这些段可大幅减小.elf
/.hex
文件的大小。 - 注意:若项目需要使用标准库功能(如
printf
、sqrt
),需删除对应行,否则会出现 “未定义引用” 错误(需搭配newlib-nano
等嵌入式精简库)。
四、总结:启动流程与.ld 文件的关联
- 硬件复位 → 内核读取
FLASH
起始地址(0x8000000
)的.isr_vector
段 → 找到Reset_Handler
地址。 - 执行
Reset_Handler
→ 利用.ld
定义的_sdata
/_sidata
/_edata
复制.data
段 → 利用_sbss
/_ebss
清零.bss
段。 - 调用
main
→ 用户代码中的变量(.data/.bss)已在 RAM 中初始化,函数(.text)在 FLASH 中执行,堆 / 栈在 RAM 中预留空间。 可以说:.ld 文件是 STM32 启动流程的 “地图”,没有它,启动文件不知道如何初始化内存,链接器不知道如何分配代码和数据,程序无法正常运行。
寄存器
认识一个CPU
最直接的是去认识它的寄存器组
(Register bank)
arm
系统寄存器组
还有5个特殊寄存器
浮点数寄存器
通用寄存器
通用寄存器通用性强,常用于存储临时的数据
其中R0-R7
是低组寄存器,R8-R12
是高组寄存器,高组寄存器部分指令无法访问
PC程序计数器&程序如何跑起来
arm
架构中的R15
寄存器,是程序计数器(program counter),该寄存器可读可写
在arm cortex-M
的CPU
内,每一次PC
自增4(即向下移动了4个字节),但是在使用thumb
指令集时,thumb
指令集的部分指令是2个字节,这与处理器的执行流水线pipeline
的特性有关,实际取址的时候是一次去4字节,也就是对于2字节的指令是一次取两条
CPU
在不断执行程序,本质就是程序计数器的不断更新,PC
指向的就是当前要执行的指令的地址
堆栈指针&双堆栈指针机制
栈是高址向低址方向生长,堆是低址向高址方向生长
arm cortex-M
架构中R13
是堆栈指针寄存器
在物理上该寄存器是两个不同的寄存器MSP
主堆栈指针和PSP
进程堆栈指针,在程序运行时选用哪一个寄存器会有Control
控制寄存器的值来决定
默认启动时,处理器使用的是主堆栈指针,可以修改Control
控制寄存器的第一位的值来修改处理器使用的堆栈指针
双堆栈指针-MSP
主堆栈指针,PSP
进程堆栈指针
可以用来实现隔离系统内核和应用程序任务
LR链接寄存器&函数调用的本质
函数调用的本质是程序执行流的动态转移与上下文管理,其核心在于通过 栈 来实现代码模块化执行和状态的保存与恢复
arm
架构的CPU
有专门设立一个寄存器R14(link register)
,用于存放子函数的返回地址
以arm
的bl
指令为例(用于函数调用)
bl <label>
是跳转到标签地址,同时将返回地址存入LR
寄存器
需要注意的是,arm cortex-M
架构的处理器指令都是半字对齐(两字节对齐),所以其反汇编的地址都是偶数,存在lr
寄存器的返回地址也都是偶数,但是lr
寄存器的第0位必须设置为1来指示thumb
指令集的状态,所以lr
寄存器的值会是返回地址+1
在多级函数调用时,lr
寄存器的值会被压入栈中
另外lr
寄存器还能记录异常返回值。当处理器进入中断时,链接寄存器会更新为异常返回值,用来实现异常返回机制,用来记录区分一些工作模式现场,这些信息是恢复中断上下文需要的
地址分配
存储器系统
cortex-M
处理器是32位地址,因此会有
4GB
的地址空间,所有的指令和数据通过这个地址访问
硬件资源都是通过地址映射的方式访问
另外cortex-M
架构没有设置MMU
内存管理单元,所以没有MMU
内存管理单元,一般也不会搭载Linux,Windows
这种通用类型的系统
地址映射图
系统架构图
cortex-M3M4内核架构图
三级流水线外的设备属于CPU
的内核外设,例如NVIC
,SysTick
系统滴答定时器,MPU
等,用0xE004 0000-0xE00F F000
地址访问
软件上就是通过读写外设地址空间来控制这些外设,对应的地址空间单元映射到外设寄存器
嵌入式编程本质就是在操作存储器,每一段CPU
代码指令都是在读取某个地址空间的单元,高级编程语言就是不断向上封装抽象
C
语言每条语句追溯到汇编都是在读写存储器的地址空间单元
关于内存和编译优化
指针是内存地址的容器
关于i++
与++i
执行效率的问题,在O0
优化的情况下,不同编译器的具体实现是不一样的,执行效率是不能直接比较的
编译优化会使汇编的指令在执行速度上提高,例如会使用mov
去代替ldr
,或者将一些简单的函数改为内嵌,以减少函数调用带来的开销
中断
在微机原理中已有涉及,后面只做简单回顾用
关于中断
中断的事件一般由硬件触发(实际有硬中断和软中断),会改变程序执行流
NVIC
属于内核外设,专门管理中断
在cortex-M
芯片中,前15项中断向量都一样
处理器具有向量表重定位功能,通过向量表偏移寄存器(VTOR
)来获取向量表的起始地址
中断触发时,处理器通过中断向量表去获取中断处理函数的入口地址
中断返回&中断上下文
电脑为什么能边玩游戏边聊天?CPU这个骚操作,99%的程序员都说不清!| CPU / 多任务 / 进程上下文 / 中断机制_哔哩哔哩_bilibili
该视频中最后讲解的就类似于arm
架构采用的方式
在阅读中断的汇编时,会发现中断没有保存和恢复上下文的代码,实际上是硬件帮忙实现上述功能
关于上下文,如程序上的函数调用需要保存上下文,在操作系统中需要处理线程上下文(任务上下文),其本质都在于保存和恢复若干寄存器的值
cortex-M3M4
中断需要入栈保存上下文的8个寄存器
选择保存上述8个寄存器主要是根据arm
架构的过程调用标准,C
语言的函数实现会改动这些寄存器,为例能让C
函数能作为异常(中断)处理函数,中断(异常)机制需要能自动地保存这些寄存器,这些寄存器也被称之为调用者保存寄存器
处理器恢复上下文,回到原执行流,需要lr
寄存器存储异常返回值来记录区分一些工作模式现场,这些信息是恢复中断上下文需要的
错误处理
介绍arm cortex-M
处理器架构如何捕获和记录错误故障
有专门的寄存器去记录故障,通过查找其对应的地址查看寄存器的位来获取错误信息
在发生错误时,对应错误寄存器的对应位被置1,同时触发错误中断处理
程序执行非法指令操作导致系统进入不可恢复的故障状态,系统软件就会介入终止程序执行流,其实现机制是中断