汇编与底层编程笔记

一、基础概念:字节序(大小端存储)

字节序是计算机存储多字节数据时的字节排列规则,核心解决 "高字节与低字节对应哪个内存地址" 的问题,以十六进制数0x12345678为例(注:1 个十六进制位占 4bit,1 字节 = 8bit,因此该数可拆分为 4 个独立字节)。

1. 字节拆分基础

0x12345678的 4 个字节按 "高低优先级" 划分:

  • 高字节:0x12(对应数值的高位部分)
  • 中间字节:0x340x56
  • 低字节:0x78(对应数值的低位部分)

2. 两种存储方式对比

存储方式 核心规则 以地址A(起始地址)为例的存储分布 适用场景
大端序(Big-endian) 高字节对应高地址,低字节对应低地址 - 地址A0x78(低字节)- 地址A+10x56- 地址A+20x34- 地址A+30x12(高字节) 网络协议(如 TCP/IP)、部分嵌入式芯片(如 PowerPC)
小端序(Little-endian) 低字节对应高地址,高字节对应低地址 - 地址A0x12(高字节)- 地址A+10x34- 地址A+20x56- 地址A+30x78(低字节) 主流 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
    1. 二进制角度:原二进制:0001 0010 0011(12 位)左移 2 位后:0001 0010 0011 00(补 2 个 0,共 14 位)转为十六进制:0x48C(注:4 个二进制位对应 1 个十六进制位,补 0 后凑整为 16 位:0001 0010 0011 00000100 10001100?修正:正确转换为0001 0010 0011 00→拆分 4 位组:0100(4)、1000(8)、1100(C)→0x48C)。
    2. 数学角度:左移 2 位 = 乘以2²=40x123 * 4 = 0x48C(十六进制计算:0x123*4=0x48C,十进制验证:0x123=291291*4=11640x48C=1164)。
(2)逻辑右移(>>)
  • 功能:二进制位整体右移,左侧补 0;数值上等价于 "除以 2 的 n 次方"(n 为移位位数,整除,舍去小数)。
  • 示例:int a=0x123; a >> 2
    1. 二进制角度:原二进制: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 00000x120?错误!原0x123是 12 位二进制0001 0010 0011,右移 2 位后为00 0001 0010 00→取低 12 位为0001 0010 00→补前 4 位为0001 0010 000x120?不对,重新计算:0x123右移 2 位,十进制291/4=72(整除),72对应十六进制0x48,二进制01001000→原 12 位二进制右移 2 位后为0000 0100 1000(补 0 到 12 位),即0x048→简化为0x48,正确。
    2. 数学角度:右移 2 位 = 除以2²=40x123 / 4 = 0x48(十进制291/4=7272=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(例:0x1230xFFFFFE 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(内置预处理工具)
  • 核心操作
    1. 展开#include头文件(如#include "stm32f10x.h"→将头文件内容插入main.c);
    2. 替换#define宏定义(如#define GPIO_PIN_5 5→所有GPIO_PIN_5替换为 5);
    3. 删除注释、处理#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.Smain.S(汇编源码)
  • 处理工具 :GCC(as start.S -o start.oas main.S -o main.o)、KEIL(armasm汇编器)
  • 核心操作:将汇编指令转换为 CPU 可识别的机器码(二进制格式)。
  • 输出文件start.omain.o(目标文件,二进制格式,包含机器码、符号表,但未链接)。
步骤 4:链接(针对目标文件)
  • 输入文件start.omain.o(多个目标文件)、链接脚本(如stm32f103.ld,定义内存分布)
  • 处理工具 :GCC(ld start.o main.o -T stm32f103.ld -o led.elf)、KEIL(armlink链接器)
  • 核心操作
    1. 合并多个目标文件的机器码;
    2. 解析符号引用(如main函数调用delay函数,链接时确定delay的地址);
    3. 按链接脚本分配内存地址(如代码段存 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) 程序计数器,指向正在取指的指令地址 不可直接修改:通过跳转指令(如BBL)间接改变 PC 值

2. 参数传递规则(示例:汇编调用 C 函数)

以 "汇编调用 C 函数void func(int a, int b, int c, int d)" 为例:

  1. 调用者(汇编)将参数依次存入 r0~r3:MOV r0, #1(a=1)、MOV r1, #2(b=2)、MOV r2, #3(c=3)、MOV r3, #4(d=4);
  2. 通过BL指令调用函数(BL func):BL指令会自动将 "返回地址" 存入 LR 寄存器;
  3. 被调用者(C 函数func)从 r0~r3 读取参数:直接使用 a、b、c、d 即可(编译器自动映射 r0~r3 到参数);
  4. 函数返回:C 函数执行完后,通过BX LR指令跳回 LR 保存的返回地址,回到汇编代码继续执行。

3. 补充:栈传递参数(>4 个参数时)

当函数参数超过 4 个(如void func(int a, int b, int c, int d, int e)),前 4 个参数用 r0~r3 传递,第 5 个参数(e)需通过栈传递:

  1. 调用者(汇编)先将 e 压栈:PUSH {#5}(e=5);
  2. 再传递前 4 个参数到 r0~r3;
  3. 调用BL func
  4. 被调用者(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,计算步骤如下:

  1. 确定当前 PC 值:根据 Cortex-M3 规则,PC = 当前指令地址 + 4 = 0x08000020 + 4 = 0x08000024
  2. 计算源地址:源地址 = PC 值 + 偏移量 = 0x08000024 + 52 = 0x08000058(注:52 为十进制,转为十六进制是0x340x08000024 + 0x34 = 0x08000058);
  3. 加载数据:从内存地址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. 核心原理:软件如何控制硬件?

  1. 寄存器映射:STM32 将外设控制逻辑(如时钟、引脚模式)映射到固定内存地址,CPU 读写这些地址即等同于操作硬件电路;
  2. C→汇编转换 :C 代码中的 "指针操作"(如*(unsigned int*)0x40021018)被编译器转换为 "LDR/STR + 位操作" 的汇编指令,本质是 "读 - 改 - 写" 的硬件交互流程;
  3. 硬件响应:当寄存器特定位被修改(如 RCC_APB2ENR 第 3 位置 1),硬件电路会执行对应动作(如给 GPIOB 模块供电),最终实现 "代码控制引脚闪烁"。

八、补充:裸机程序启动流程

用户未提及 "程序如何从 Flash 启动并进入mymain函数",这是嵌入式裸机开发的关键,以 STM32F103 为例,启动流程如下:

  1. 复位触发 :芯片上电或复位后,PC 指向 Flash 起始地址(0x08000000),该地址存储 "栈顶地址"(而非指令);
  2. 向量表读取0x08000004地址存储 "复位中断服务程序地址"(Reset Handler),PC 跳转到该地址执行;
  3. 复位服务程序(汇编实现,如 start.S)
    • 初始化栈(设置 SP = 栈顶地址,从0x08000000读取);
    • 初始化数据段(将 Flash 中的初始化数据复制到 RAM);
    • 清零 BSS 段(RAM 中未初始化的全局变量清 0);
    • 调用mymain函数(通过BL mymain指令,LR 保存返回地址,若mymain是死循环则无需返回);
  4. 进入用户逻辑 :执行mymain中的 GPIO 控制代码,实现引脚闪烁。
相关推荐
lzj_pxxw3 小时前
嵌入式开发技巧:舍弃标志位,用宏定义函数实现程序单次运行
笔记·stm32·单片机·嵌入式硬件·学习
润 下4 小时前
C语言——回调函数的典型示例(分析详解)
c语言·开发语言·人工智能·经验分享·笔记·程序人生
朝新_4 小时前
【EE初阶 - 网络原理】传输层协议
java·开发语言·网络·笔记·javaee
koo3644 小时前
李宏毅机器学习笔记27
人工智能·笔记·机器学习
峰顶听歌的鲸鱼5 小时前
1.云计算与服务器基础
运维·服务器·笔记·云计算·学习方法
Kay_Liang5 小时前
大语言模型如何精准调用函数—— Function Calling 系统笔记
java·大数据·spring boot·笔记·ai·langchain·tools
bnsarocket5 小时前
Verilog和FPGA的自学笔记7——流水灯与时序约束(XDC文件的编写)
笔记·fpga开发
wdfk_prog5 小时前
[Linux]学习笔记系列 -- [kernel][irq]softirq
linux·笔记·学习
摇滚侠5 小时前
Spring Boot 3零基础教程,WEB 开发 内容协商机制 笔记34
java·spring boot·笔记·缓存