一、基础概念:字节序(大小端存储)
字节序是计算机存储多字节数据时的字节排列规则,核心解决 "高字节与低字节对应哪个内存地址" 的问题,以十六进制数0x12345678
为例(注:1 个十六进制位占 4bit,1 字节 = 8bit,因此该数可拆分为 4 个独立字节)。
1. 字节拆分基础
0x12345678
的 4 个字节按 "高低优先级" 划分:
- 高字节:
0x12
(对应数值的高位部分) - 中间字节:
0x34
、0x56
- 低字节:
0x78
(对应数值的低位部分)
2. 两种存储方式对比
存储方式 | 核心规则 | 以地址A (起始地址)为例的存储分布 |
适用场景 |
---|---|---|---|
大端序(Big-endian) | 高字节对应高地址,低字节对应低地址 | - 地址A :0x78 (低字节)- 地址A+1 :0x56 - 地址A+2 :0x34 - 地址A+3 :0x12 (高字节) |
网络协议(如 TCP/IP)、部分嵌入式芯片(如 PowerPC) |
小端序(Little-endian) | 低字节对应高地址,高字节对应低地址 | - 地址A :0x12 (高字节)- 地址A+1 :0x34 - 地址A+2 :0x56 - 地址A+3 :0x78 (低字节) |
主流 CPU(x86、ARM Cortex-M/A 系列)、STM32 全系列 |
3. 补充:字节序的实际影响
- 跨设备数据传输(如嵌入式设备与 PC 通信)时,需统一字节序(通常转为大端序),否则会出现 "数据错乱"(如
0x1234
在小端设备存为0x3412
,大端设备读取后会解析为0x3412
)。 - 嵌入式开发中,通过 "指针强制转换" 可验证当前设备字节序(例:
unsigned int a=0x12345678; unsigned char b=*(unsigned char*)&a;
,若b=0x78
则为小端,b=0x12
则为大端)。
二、位操作:底层控制的核心手段
位操作是直接对二进制位进行修改的操作,是嵌入式开发中 "控制寄存器特定位" 的核心方法,包括逻辑移位、取反、位与、位或、置位、清位等。
1. 逻辑移位(左移 / 右移)
逻辑移位不考虑符号位,移位后空缺位补 0,以十六进制数0x123
(对应二进制0001 0010 0011
)为例。
(1)逻辑左移(<<)
- 功能:二进制位整体左移,右侧补 0;数值上等价于 "乘以 2 的 n 次方"(n 为移位位数)。
- 示例:
0x123 << 2
- 二进制角度:原二进制:
0001 0010 0011
(12 位)左移 2 位后:0001 0010 0011 00
(补 2 个 0,共 14 位)转为十六进制:0x48C
(注:4 个二进制位对应 1 个十六进制位,补 0 后凑整为 16 位:0001 0010 0011 00
→000100 10001100
?修正:正确转换为0001 0010 0011 00
→拆分 4 位组:0100
(4)、1000
(8)、1100
(C)→0x48C
)。 - 数学角度:左移 2 位 = 乘以
2²=4
,0x123 * 4 = 0x48C
(十六进制计算:0x123*4=0x48C
,十进制验证:0x123=291
,291*4=1164
,0x48C=1164
)。
- 二进制角度:原二进制:
(2)逻辑右移(>>)
- 功能:二进制位整体右移,左侧补 0;数值上等价于 "除以 2 的 n 次方"(n 为移位位数,整除,舍去小数)。
- 示例:
int a=0x123; a >> 2
- 二进制角度:原二进制:
0001 0010 0011
右移 2 位后:00 0001 0010 00
(左侧补 2 个 0,舍弃右侧 2 位)→ 简化为0001 0010 00
(二进制0001001000
)转为十六进制:0x48
(4 位分组:0001
(1)、0010
(2)、0000
(0)?修正:0001001000
共 10 位,补 2 个 0 为 12 位:0001 0010 0000
→0x120
?错误!原0x123
是 12 位二进制0001 0010 0011
,右移 2 位后为00 0001 0010 00
→取低 12 位为0001 0010 00
→补前 4 位为0001 0010 00
→0x120
?不对,重新计算:0x123
右移 2 位,十进制291/4=72
(整除),72
对应十六进制0x48
,二进制01001000
→原 12 位二进制右移 2 位后为0000 0100 1000
(补 0 到 12 位),即0x048
→简化为0x48
,正确。 - 数学角度:右移 2 位 = 除以
2²=4
,0x123 / 4 = 0x48
(十进制291/4=72
,72=0x48
)。
- 二进制角度:原二进制:
2. 核心位操作(置位 / 清位 / 取反 / 位与 / 位或)
操作类型 | 功能描述 | 示例(以寄存器reg 的第 n 位为例) |
应用场景 | ||
---|---|---|---|---|---|
置位(位或 + 左移) | 将某一位置 1,不影响其他位 | `reg | = (1 << n)(例: reg |
= (1<<3)`→第 3 位置 1) | 使能外设(如 GPIO 时钟使能)、输出高电平 |
清位(位与 + 取反 + 左移) | 将某一位清 0,不影响其他位 | reg &= ~(1 << n) (例:reg &= ~(1<<5) →第 5 位清 0) |
关闭外设、输出低电平 | ||
取反(~) | 对所有位反转(0→1,1→0) | reg = ~reg (例:0x123 →0xFFFFFE DC ,32 位寄存器) |
翻转引脚电平(如 Toggle 操作) | ||
位与(&) | 仅当两位均为 1 时结果为 1,用于 "掩码筛选" | reg & 0x0F →仅保留低 4 位,高 28 位清 0 |
读取寄存器某几位的值(如判断引脚电平) | ||
位或( | ) | 仅当两位均为 0 时结果为 0,用于 "叠加置位" | `reg | 0x30`→将第 4、5 位置 1,其他位不变 | 同时设置寄存器多个位 |
3. 补充:位操作的注意事项
- 操作 32 位寄存器时,需确保 "掩码" 为 32 位(如
~(1<<n)
在 32 位环境下是0xFFFFFFFE
(n=0),而非 8 位的0xFE
),避免因 "符号扩展" 导致错误。 - 连续操作同一寄存器时,建议先 "读 - 改 - 写"(如先读寄存器值到变量,修改后写回),而非直接多次赋值,防止覆盖其他位的配置。
三、程序处理流程:从源码到反汇编
程序从源码到可执行文件需经过 "预处理→编译→汇编→链接"4 个步骤,反汇编则是反向将可执行文件转为汇编代码,核心用于底层调试与架构理解(以 LED 程序的start.S
(汇编源码)和main.c
(C 源码)为例)。
1. 4 个核心处理步骤(正向流程)
步骤 1:预处理(针对 C 文件)
- 输入文件 :
main.c
(C 源码) - 处理工具 :GCC(
gcc -E main.c -o main.i
)、KEIL(内置预处理工具) - 核心操作 :
- 展开
#include
头文件(如#include "stm32f10x.h"
→将头文件内容插入main.c
); - 替换
#define
宏定义(如#define GPIO_PIN_5 5
→所有GPIO_PIN_5
替换为 5); - 删除注释、处理
#if/#else
条件编译。
- 展开
- 输出文件 :
main.i
(预处理后的 C 中间文件,仍为文本格式)。
步骤 2:编译(针对预处理后的 C 文件)
- 输入文件 :
main.i
- 处理工具 :GCC(
gcc -S main.i -o main.S
)、KEIL(armclang
编译器) - 核心操作:将 C 代码转换为汇编代码(遵循目标架构的指令集,如 ARM Thumb-2)。
- 输出文件 :
main.S
(汇编源码文件,文本格式,与手写的start.S
格式一致)。
步骤 3:汇编(针对汇编文件)
- 输入文件 :
start.S
、main.S
(汇编源码) - 处理工具 :GCC(
as start.S -o start.o
;as main.S -o main.o
)、KEIL(armasm
汇编器) - 核心操作:将汇编指令转换为 CPU 可识别的机器码(二进制格式)。
- 输出文件 :
start.o
、main.o
(目标文件,二进制格式,包含机器码、符号表,但未链接)。
步骤 4:链接(针对目标文件)
- 输入文件 :
start.o
、main.o
(多个目标文件)、链接脚本(如stm32f103.ld
,定义内存分布) - 处理工具 :GCC(
ld start.o main.o -T stm32f103.ld -o led.elf
)、KEIL(armlink
链接器) - 核心操作 :
- 合并多个目标文件的机器码;
- 解析符号引用(如
main
函数调用delay
函数,链接时确定delay
的地址); - 按链接脚本分配内存地址(如代码段存 Flash、数据段存 RAM)。
- 输出文件 :
led.elf
(可执行文件,二进制格式,可在目标芯片上运行)。
2. 反汇编(反向流程)
- 定义:将可执行文件 / 目标文件中的机器码,反向转换为人类可读的汇编代码,用于调试(如定位程序崩溃位置)、理解编译器优化结果。
- 输入文件 :
led.elf
(可执行文件) - 处理工具 :GCC(
objdump -D led.elf -o led.dis
)、KEIL(fromelf --disassemble led.elf > led.dis
) - 输出文件 :
led.dis
(汇编代码文件,文本格式,包含 "地址 + 机器码 + 汇编指令",例:0x08000020: 0xE59F3034 LDR r3, [pc, #52]
)。
3. 补充:汇编与反汇编的核心区别
维度 | 汇编(正向) | 反汇编(反向) |
---|---|---|
输入 | 汇编源码(.S) | 机器码文件(.o/.elf) |
输出 | 机器码(.o) | 汇编代码(.dis) |
目的 | 生成可执行代码 | 调试 / 理解底层执行逻辑 |
可读性 | 输入文件(.S)可读,输出(.o)不可读 | 输出文件(.dis)可读,输入(.o/.elf)不可读 |
四、ARM 架构核心差异:指令集与 PC 偏移
不同 ARM 架构(如 Cortex-M3、Cortex-A7)的指令集和 PC 寄存器逻辑存在差异,直接影响汇编代码的编写与地址计算,以 "STM32F103(Cortex-M3)" 和 "经典 ARM(如 ARM7、Cortex-A8)" 为例对比。
1. 指令集差异(核心区别)
架构型号 | 支持指令集 | 指令长度 | 核心特点 |
---|---|---|---|
STM32F103(Cortex-M3) | Thumb/Thumb-2 | 16 位(基础指令)、32 位(扩展指令,Thumb-2) | 低功耗、代码密度高(16 位指令占比高,节省 Flash),仅支持 Thumb 模式(无 ARM 模式) |
经典 ARM(ARM7、Cortex-A8) | ARM 指令集(默认)、Thumb 指令集(可选) | 32 位(ARM 指令)、16 位(Thumb 指令) | 高性能、32 位指令并行性好,支持 "ARM 模式" 与 "Thumb 模式" 切换 |
2. PC 寄存器偏移逻辑(三级流水线导致)
ARM 架构普遍采用 "三级流水线"(取指→译码→执行),三个阶段并行处理,因此PC 寄存器始终指向 "正在取指的指令地址",而非 "正在执行的指令地址",偏移量由 "指令长度" 决定。
(1)STM32F103(Cortex-M3,Thumb 指令集)
- 指令长度:16 位(2 字节),两条指令总长度 = 4 字节。
- 流水线并行逻辑:当 "第 n 条指令" 处于「执行」阶段时,"第 n+1 条指令" 处于「译码」阶段,"第 n+2 条指令" 处于「取指」阶段(PC 指向第 n+2 条指令地址)。
- PC 偏移计算:PC 值 = 当前执行指令地址 + 2 条指令总长度 = 当前地址 + 2×2 字节 = 当前地址 + 4 。例:若当前执行指令地址为
0x08000020
,则 PC 值为0x08000020 + 4 = 0x08000024
(指向第 n+2 条指令)。
(2)经典 ARM(ARM 指令集)
- 指令长度:32 位(4 字节),两条指令总长度 = 8 字节。
- 流水线并行逻辑:与 Cortex-M3 一致(执行 n→译码 n+1→取指 n+2)。
- PC 偏移计算:PC 值 = 当前执行指令地址 + 2 条指令总长度 = 当前地址 + 2×4 字节 = 当前地址 + 8 。例:若当前执行指令地址为
0x00000020
,则 PC 值为0x00000020 + 8 = 0x00000028
(指向第 n+2 条指令)。
3. 补充:兼容性设计背景与扩展
- Cortex-M 系列统一规则:Cortex-M0/M4/M7 均采用 Thumb-2 指令集,PC 偏移均为 "当前地址 + 4",无需区分型号,降低开发难度。
- 经典 ARM 的模式切换 :支持在 ARM 模式(32 位指令,高性能)与 Thumb 模式(16 位指令,低功耗)间切换,通过
BX
指令(如BX r0
,若 r0 最低位为 1 则进入 Thumb 模式)实现,而 Cortex-M 系列无此切换(仅 Thumb 模式)。
五、ARM 函数调用标准:ATPCS(ARM-Thumb 过程调用标准)
ATPCS 是 ARM 架构下函数调用的统一规则,规定了 "寄存器用途、参数传递、栈管理",确保不同函数(如汇编调用 C、C 调用汇编)可正确交互,核心是寄存器的分工。
1. 核心寄存器用途约定(r0~r15)
寄存器 | 别名 | 用途约定 | 关键注意事项 |
---|---|---|---|
r0~r3 | 传参寄存器 | 用于 "调用者" 向 "被调用者" 传递参数(最多 4 个参数) | 无需保存恢复:被调用者可直接修改,调用者需自行保存未传递完的参数(>4 个参数时用栈传递) |
r4~r11 | 保存寄存器(Callee-saved) | 函数内部临时使用的寄存器 | 必须保存恢复:被调用者在函数入口需将这些寄存器的值压栈(PUSH {r4-r11} ),出口时弹栈(POP {r4-r11} ),避免影响调用者 |
r12 | IP( Intra-Procedure Call ) | 临时寄存器,用于函数调用过程中的地址过渡(如链接器优化) | 无需保存恢复:被调用者可随意修改,调用者不依赖其值 |
r13 | SP(Stack Pointer) | 栈指针,指向当前栈顶地址 | 必须保持稳定:函数调用过程中栈需平衡(压栈次数 = 弹栈次数),避免栈溢出 |
r14 | LR(Link Register) | 链接寄存器,保存函数调用后的返回地址 | 需按需保存:若函数嵌套调用(如 A 调用 B,B 调用 C),B 需将 LR 压栈,否则 C 会覆盖 LR 的值 |
r15 | PC(Program Counter) | 程序计数器,指向正在取指的指令地址 | 不可直接修改:通过跳转指令(如B 、BL )间接改变 PC 值 |
2. 参数传递规则(示例:汇编调用 C 函数)
以 "汇编调用 C 函数void func(int a, int b, int c, int d)
" 为例:
- 调用者(汇编)将参数依次存入 r0~r3:
MOV r0, #1
(a=1)、MOV r1, #2
(b=2)、MOV r2, #3
(c=3)、MOV r3, #4
(d=4); - 通过
BL
指令调用函数(BL func
):BL
指令会自动将 "返回地址" 存入 LR 寄存器; - 被调用者(C 函数
func
)从 r0~r3 读取参数:直接使用 a、b、c、d 即可(编译器自动映射 r0~r3 到参数); - 函数返回:C 函数执行完后,通过
BX LR
指令跳回 LR 保存的返回地址,回到汇编代码继续执行。
3. 补充:栈传递参数(>4 个参数时)
当函数参数超过 4 个(如void func(int a, int b, int c, int d, int e)
),前 4 个参数用 r0~r3 传递,第 5 个参数(e)需通过栈传递:
- 调用者(汇编)先将 e 压栈:
PUSH {#5}
(e=5); - 再传递前 4 个参数到 r0~r3;
- 调用
BL func
; - 被调用者(C 函数)从栈中读取第 5 个参数(栈是 "先进后出",e 在栈顶下方,需通过 SP 偏移读取)。
六、关键汇编指令解析:LDR(内存加载指令)
LDR 是 ARM 汇编中最常用的指令之一,功能是 "从内存地址读取数据到寄存器",支持多种寻址方式,以LDR r3, [pc, #52]
(STM32F103 中)为例详解。
1. 指令格式与核心含义
- 指令格式:
LDR <目标寄存器>, <源地址>
(LDR
=Load Register,即 "加载数据到寄存器") - 示例解析:
LDR r3, [pc, #52]
- 目标寄存器:r3(数据加载后存入 r3);
- 源地址:
[pc, #52]
(PC 相对寻址,即 "PC 值 + 52(十进制偏移量)",偏移量为正数时表示 "向后寻址")。
2. 地址计算过程(结合 Cortex-M3 的 PC 偏移)
假设当前LDR r3, [pc, #52]
指令的地址为0x08000020
,计算步骤如下:
- 确定当前 PC 值:根据 Cortex-M3 规则,PC = 当前指令地址 + 4 =
0x08000020 + 4 = 0x08000024
; - 计算源地址:源地址 = PC 值 + 偏移量 =
0x08000024 + 52 = 0x08000058
(注:52 为十进制,转为十六进制是0x34
,0x08000024 + 0x34 = 0x08000058
); - 加载数据:从内存地址
0x08000058
中读取 4 字节数据(因 LDR 默认加载 32 位字),假设该地址存储的数据为0x40021018
(STM32 的 RCC_APB2ENR 寄存器地址),则最终r3 = 0x40021018
。
3. 补充:LDR 的其他寻址方式
寻址方式 | 示例 | 功能描述 | 适用场景 |
---|---|---|---|
立即数寻址 | LDR r0, =0x12345678 |
将立即数0x12345678 加载到 r0(注:= 表示 "伪操作",实际编译为LDR r0, [pc, #offset] ,立即数存在内存中) |
加载无法用MOV 指令直接传递的大立即数(MOV 仅支持 8 位立即数 + 4 位旋转) |
寄存器间接寻址 | LDR r0, [r3] |
从 r3 指向的内存地址读取数据到 r0 | 读取寄存器中存储的地址对应的数据(如 r3 存 GPIO 寄存器地址,读取该寄存器值) |
寄存器偏移寻址 | LDR r0, [r3, #4] |
从 "r3 + 4" 指向的内存地址读取数据到 r0 | 访问连续的寄存器(如 GPIO 的 ODR 寄存器地址 = CRL 地址 + 0xC,可通过LDR r0, [r3, #0xC] 读取) |
栈寻址 | LDR r0, [sp, #8] |
从栈顶(sp)+8 的地址读取数据到 r0 | 读取栈中保存的参数或寄存器值(如函数入口读取压栈的 r4 值) |
七、实战:C 与汇编协同控制 STM32 GPIO(引脚闪烁)
以 "STM32F103 控制 GPIOB 引脚(PB5)闪烁" 为例,拆解 C 代码逻辑、对应汇编指令,以及 "软件→硬件" 的控制链路,核心是 "寄存器映射" 与 "读 - 改 - 写" 操作。
1. 背景知识:STM32 寄存器映射
STM32 的外设(如 RCC、GPIO)被 "映射" 到固定的内存地址,CPU 通过读写这些地址即可控制硬件,关键地址如下:
- RCC_APB2ENR(GPIOB 时钟使能寄存器):
0x40021018
(第 3 位控制 GPIOB 时钟,置 1 使能); - GPIOB_CRL(GPIOB 低 8 位引脚配置寄存器):
0x40010C00
(控制 PB0~PB7,PB5 对应 [23:20] 位,配置引脚模式); - GPIOB_ODR(GPIOB 输出数据寄存器):
0x40010C0C
(控制 PB0~PB15 的输出电平,第 5 位对应 PB5,1 = 高电平,0 = 低电平)。
2. C 代码核心逻辑(简化版)
c
运行
void mymain(void) {
// 步骤1:使能GPIOB时钟(外设必须先上电才能工作)
*(unsigned int*)0x40021018 |= (1 << 3); // 第3位置1,使能GPIOB时钟
// 步骤2:配置PB5为推挽输出模式(50MHz)
*(unsigned int*)0x40010C00 &= ~(0xF << 20); // 清除PB5原配置([23:20]位清0,0xF=4位全1)
*(unsigned int*)0x40010C00 |= (0x3 << 20); // 设置PB5为推挽输出(0x3对应50MHz推挽输出)
while(1) {
// 步骤3:PB5输出高电平(ODR第5位置1)
*(unsigned int*)0x40010C0C |= (1 << 5);
delay(1000000); // 延时1秒(简化版,实际需精准延时)
// 步骤4:PB5输出低电平(ODR第5位清0)
*(unsigned int*)0x40010C0C &= ~(1 << 5);
delay(1000000); // 延时1秒
}
}
3. C 代码与汇编指令逐句对应(硬件操作细节)
编译器会将 C 代码转换为 ARM Thumb-2 汇编指令,每条指令对应 "对寄存器的读 - 改 - 写",以下是分步对应:
(1)步骤 1:使能 GPIOB 时钟(C→汇编)
C 代码:*(unsigned int*)0x40021018 |= (1 << 3);
对应汇编(含详细注释):
asm
; 1. 加载RCC_APB2ENR寄存器地址到r3(PC相对寻址,偏移52,最终地址0x40021018)
LDR r3, [pc, #52] ; PC = 当前指令地址+4,+52后得到0x40021018,存入r3
; 2. 读取RCC_APB2ENR当前值到r0("读"操作,避免覆盖其他位)
LDR r0, [r3, #0] ; [r3, #0] = r3 + 0 = 0x40021018,读取该地址值到r0
; 3. 对r0进行"位或"操作,第3位置1(1<<3=8,十六进制0x8)
ORR r0, r0, #8 ; r0 = r0 | 8 → 仅第3位置1,其他位不变
; 4. 将修改后的值写回RCC_APB2ENR寄存器("写"操作,完成时钟使能)
STR r0, [r3, #0] ; 把r0的值存回0x40021018,GPIOB时钟使能
(2)步骤 2:配置 PB5 为推挽输出(C→汇编)
C 代码分两步:先清除原配置,再设置新配置,对应汇编如下:
asm
; -------------------------- 子步骤2.1:清除PB5原配置 --------------------------
; 1. 加载GPIOB_CRL寄存器地址到r3(PC相对寻址,假设偏移48,最终地址0x40010C00)
LDR r3, [pc, #48] ; r3 = 0x40010C00(GPIOB_CRL地址)
; 2. 读取GPIOB_CRL当前值到r0
LDR r0, [r3, #0] ; r0 = GPIOB_CRL当前值
; 3. 清除[23:20]位(0xF<<20=0x000F0000,~后为0xFFFF0FFF,位与操作清0)
AND r0, r0, #0xFFFF0FFF ; r0 = r0 & 0xFFFF0FFF → [23:20]位清0,其他位不变
; 4. 写回清除后的值
STR r0, [r3, #0] ; GPIOB_CRL = r0,完成原配置清除
; -------------------------- 子步骤2.2:设置PB5为推挽输出 --------------------------
; 1. 再次读取GPIOB_CRL值到r0(确保基于最新值修改)
LDR r0, [r3, #0]
; 2. 设置[23:20]位为0x3(0x3<<20=0x00003000,位或操作置位)
ORR r0, r0, #0x00003000 ; r0 = r0 | 0x00003000 → [23:20]位=0x3,对应50MHz推挽输出
; 3. 写回新配置
STR r0, [r3, #0] ; GPIOB_CRL = r0,PB5输出模式配置完成
(3)步骤 3~4:while 循环(PB5 高低电平交替)
C 代码的while(1)
循环对应汇编的 "标签跳转",核心是控制 GPIOB_ODR 寄存器,对应汇编如下:
asm
; 循环开始标签(label1为自定义标签,用于跳转)
label1:
; -------------------------- 子步骤3:PB5输出高电平 --------------------------
; 1. 加载GPIOB_ODR寄存器地址到r3(假设PC偏移36,最终地址0x40010C0C)
LDR r3, [pc, #36] ; r3 = 0x40010C0C(GPIOB_ODR地址)
; 2. 读取ODR当前值到r0
LDR r0, [r3, #0]
; 3. 第5位置1(1<<5=32,十六进制0x20)
ORR r0, r0, #32 ; r0 = r0 | 32 → PB5=1(高电平)
; 4. 写回ODR寄存器
STR r0, [r3, #0]
; 5. 调用delay函数(BL指令:跳转+保存返回地址到LR)
BL delay ; 跳转到delay函数,执行完后返回当前下一条指令
; -------------------------- 子步骤4:PB5输出低电平 --------------------------
; 1. 再次读取ODR值到r0
LDR r0, [r3, #0]
; 2. 第5位清0(~(1<<5)=0xFFFFFEFF,位与操作)
AND r0, r0, #0xFFFFFEFF ; r0 = r0 & 0xFFFFFEFF → PB5=0(低电平)
; 3. 写回ODR寄存器
STR r0, [r3, #0]
; 4. 再次调用delay
BL delay
; 5. 无条件跳回循环开头(B指令:仅跳转,不保存返回地址)
B label1 ; 重复循环,实现PB5闪烁
4. 补充:delay 函数的汇编实现(用户疏漏补充)
用户未提供delay
函数的实现,以下是简化版汇编实现(基于循环减计数,延时约 1 秒,STM32F103 主频 72MHz),遵循 ATPCS 标准(保存 r4):
asm
; 延时函数:void delay(unsigned int count)(参数count通过r0传递)
delay:
PUSH {r4} ; 入口:保存r4(ATPCS要求,避免影响调用者)
MOV r4, r0 ; 将参数count存入r4(用r4作为循环计数器)
delay_loop:
SUBS r4, r4, #1 ; r4 = r4 - 1(计数减1)
BNE delay_loop ; 若r4≠0,跳回delay_loop继续循环
POP {r4} ; 出口:恢复r4
BX LR ; 跳回LR保存的返回地址,结束延时
5. 核心原理:软件如何控制硬件?
- 寄存器映射:STM32 将外设控制逻辑(如时钟、引脚模式)映射到固定内存地址,CPU 读写这些地址即等同于操作硬件电路;
- C→汇编转换 :C 代码中的 "指针操作"(如
*(unsigned int*)0x40021018
)被编译器转换为 "LDR/STR + 位操作" 的汇编指令,本质是 "读 - 改 - 写" 的硬件交互流程; - 硬件响应:当寄存器特定位被修改(如 RCC_APB2ENR 第 3 位置 1),硬件电路会执行对应动作(如给 GPIOB 模块供电),最终实现 "代码控制引脚闪烁"。
八、补充:裸机程序启动流程
用户未提及 "程序如何从 Flash 启动并进入mymain
函数",这是嵌入式裸机开发的关键,以 STM32F103 为例,启动流程如下:
- 复位触发 :芯片上电或复位后,PC 指向 Flash 起始地址(
0x08000000
),该地址存储 "栈顶地址"(而非指令); - 向量表读取 :
0x08000004
地址存储 "复位中断服务程序地址"(Reset Handler),PC 跳转到该地址执行; - 复位服务程序(汇编实现,如 start.S) :
- 初始化栈(设置 SP = 栈顶地址,从
0x08000000
读取); - 初始化数据段(将 Flash 中的初始化数据复制到 RAM);
- 清零 BSS 段(RAM 中未初始化的全局变量清 0);
- 调用
mymain
函数(通过BL mymain
指令,LR 保存返回地址,若mymain
是死循环则无需返回);
- 初始化栈(设置 SP = 栈顶地址,从
- 进入用户逻辑 :执行
mymain
中的 GPIO 控制代码,实现引脚闪烁。