☰
STM32F103汇编入门:从点亮LED到理解硬件寄存器
2026/10/21 3:24:34 网站建设 项目流程

简介:本资源是一份面向嵌入式初学者与底层开发爱好者的STM32F103汇编语言实践项目,聚焦“点灯”这一经典入门实验,帮助读者深入理解Cortex-M3内核寄存器操作、GPIO配置流程及Keil5汇编开发全流程。压缩包共19个文件,含核心汇编源码(.s)、Keil5工程文件(.uvprojx、.uvoptx)、链接脚本(.sct)、编译输出(.axf、.map、.lst)、启动代码(startup_stm32f10x_md.s)及自动化批处理脚本(keilkilll.bat),结构完整,开箱即用。资源仅28KB,轻量精炼,便于快速导入调试。已有1652人学习下载,适合希望摆脱C语言抽象层、掌握硬件直控能力的学习者——通过该包可清晰看到从寄存器地址映射、MODER/OTYPER位配置到LED循环闪烁的完整汇编实现逻辑,并借助J-Link调试日志与内存视图辅助验证,是夯实ARM底层编程基础的优质实操素材。

1. 这不是“写代码”,是给芯片下指令:STM32汇编程序到底在干啥?

你有没有试过拧开一个遥控器,看到里面密密麻麻的黑色小方块和细如发丝的铜线?那里面最核心的一块,就是像 STM32F103 这样的微控制器(MCU)。它不像手机或电脑那样能直接运行 Python 或微信——它只认一种最原始、最贴近硬件的语言:汇编。
很多人第一次听说“STM32 汇编程序”,第一反应是:“这玩意儿不是早淘汰了吗?谁还手写汇编?” 其实不然。它不是用来开发 App 的,而是当你需要精确控制每一纳秒的时序、让 LED 以 0.1 微秒级精度闪烁、在中断到来的第 3 个机器周期内完成寄存器保护、或者调试一段 C 语言死活跑不起来的底层驱动时,唯一能让你“看见芯片在想什么”的显微镜。
这份“STM32F103 汇编程序”资源,不是教你怎么用 Keil5 点点点生成工程,而是一套从零开始、纯手工写的、能在真实 STM32F103 芯片上跑通的汇编源码包。它包含启动文件、GPIO 配置、延时循环、按键检测、LED 控制等最小可行模块,每行都带中文注释,连堆栈指针 SP 初始化在哪一行、为什么必须先设 SP 再跳 main,都给你标得明明白白。适合完全没碰过嵌入式、但愿意从“芯片开机第一行执行什么”开始搞懂的新手——你不需要会 C,不需要懂 HAL 库,甚至不用会 Keil5,只要敢打开记事本,就能照着跑通第一个亮灯程序。


2. 为什么非得学 STM32 汇编?不是有 Keil5 和 C 语言吗?

2.1 你以为的“点一下就编译”,背后全是黑匣子

Keil5 是个强大的集成开发环境(IDE),但它就像一辆全自动挡汽车:你踩油门,车自己换挡、调喷油、控点火。可如果某天车子突然在上坡时熄火,你既不会看转速表,也不懂离合器行程,那就只能叫拖车。STM32 的 C 语言开发也一样——你写HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0);,Keil5 默默帮你生成几百行汇编,再烧进芯片。一旦 LED 不亮,你查HAL_StatusTypeDef返回值、翻 HAL 库源码、怀疑 CubeMX 配置错误……最后发现,原来是启动文件里.stack段大小设小了,导致主函数一进就栈溢出,而这个错误在 C 层根本看不到报错,只会“静默挂掉”。
汇编,就是让你亲手把离合器、油门、档位全拆开,看清每个零件怎么咬合。它不替代 C,而是给你一把“后悔药”:当 C 程序行为诡异时,你能在反汇编窗口里逐行对照,一眼看出是哪条指令改错了寄存器,哪次压栈破坏了 LR(链接寄存器)。

2.2 STM32F103 的汇编,其实比你想的“友好”

别被“汇编”俩字吓住。ARM Cortex-M3 内核(STM32F103 就是它)用的是 Thumb-2 指令集,不是古老 8086 那种动不动要算段地址+偏移量的玄学。它的指令非常规整:

  • MOV R0, #0x01→ 把数字 1 直接放进寄存器 R0(就像往抽屉里放一张纸条)
  • STR R0, [R1]→ 把 R0 里的数,存到 R1 寄存器里存着的那个地址里(比如 R1 存着 GPIOA_BSRR 的地址 0x40010808,这条指令就是在操作硬件)
  • B loop→ 无条件跳转到标号loop处(就像书签,翻回去重读某页)
    没有复杂的寻址模式,没有段寄存器,没有标志位手动清/置——Cortex-M3 把这些都简化了。你真正要花时间理解的,不是语法,而是STM32 的寄存器映射规则:为什么控制 PA0 输出高电平,要往0x40010808(GPIOA_BSRR)写0x00010000?为什么配置 PA0 为推挽输出,得先设置0x40010800(GPIOA_CRL)的 bit0~3 为0b0011?这份资源里每一个.s文件,都在用最直白的方式回答这个问题。

2.3 Keil5 不是“魔法盒”,它是你的汇编翻译官

Keil5 对 STM32 汇编的支持,远比你想象中扎实。它原生支持 ARMASM 汇编器,能直接编译.s文件;它自带 CMSIS 启动模板,你只需替换掉默认的startup_stm32f103xb.s;它还能在调试时同步显示 C 源码和对应汇编(Disassembly 窗口),甚至允许你在汇编层单步执行、修改寄存器值、观察内存变化。这不是复古情怀,而是现代嵌入式开发的“底层透视能力”。很多资深工程师的血泪经验是:第一次用 C 写串口收发卡死,花 3 小时查 HAL 配置;第二次用汇编重写初始化流程,15 分钟就定位到是 RCC_APB2ENR 寄存器没使能 AFIO 时钟——因为汇编里,每一行都是你亲手写的命令,没有隐藏逻辑。

提示:Keil5 安装时务必勾选 “ARM Compiler” 和 “Device Family Pack”(尤其是 STM32F1xx),否则新建汇编工程会提示找不到器件支持。这不是软件问题,是“翻译官”没拿到词典。


3. 第一步:从零创建你的第一个汇编工程(Keil5 + STM32F103)

3.1 准备工作:三件套缺一不可

你需要:

  • 一台 Windows 电脑(Keil5 目前官方仅支持 Windows)
  • Keil MDK-ARM v5.x(推荐 v5.37 或更新,兼容性最好)
  • 一块基于 STM32F103C8T6(俗称“蓝 pill”)或 STM32F103RCT6 的开发板(带 USB 转串口芯片,如 CH340 或 CP2102)
  • (可选但强烈建议)ST-Link V2 调试器(比串口下载更稳定,支持在线调试)

注意:不要用 Keil uVision4!它对 Cortex-M3 的 Thumb-2 支持不完整,某些伪指令会报错。v5.x 是分水岭。

3.2 新建工程:拒绝模板,手动搭骨架

打开 Keil5 → Project → New uVision Project…

  • 路径选一个空文件夹,比如D:\stm32_asm\led_blink
  • 工程名填led_blink,点击保存
  • 在弹出的 Device Database 中,搜索STM32F103C8,双击选中(如果你用的是其他型号,如 F103RCT6,请选对应型号)
  • 点击 OK,此时不要勾选任何 CMSIS 或 Startup 模板!我们要自己加汇编文件。

接下来,右键左侧 Project 窗口的Target 1→ Manage → Project Items…

  • 在 Files 页,点击 Add Group,新建两个组:Startup和Source
  • 右键Startup组 → Add Existing Files to Group…,添加你下载的资源包里的startup_stm32f103xb.s(这是标准启动文件,已适配 F103 系列)
  • 右键Source组 → Add New Item to Group… → ASM File (.s),命名为main.s,点击 Add

现在工程结构清晰了:Startup放芯片上电后最先执行的代码(设置栈、初始化寄存器、跳转 main),Source放你自己的逻辑(比如点亮 LED)。

3.3 配置关键选项:让 Keil5 “读懂”你的汇编

双击Target选项卡(Project → Options for Target…):

  • Device 页:确认 Device 仍是STM32F103C8,Flash 和 RAM 大小自动填好(F103C8 是 64KB Flash / 20KB RAM)
  • Target 页:
    • Xtal (MHz) 填8(外部晶振频率,蓝 pill 板多数是 8MHz)
    • 去掉勾选 "Use Memory Layout from Target Dialog"—— 我们要手动指定内存布局
  • Output 页:勾选 "Create HEX File"(方便后续用串口工具烧录)
  • Listing 页:勾选 "Assembler Listing"(生成 .lst 文件,方便你对照看汇编和机器码)
  • C/C++ 页:全部清空!不填任何 Define、Include Path 或 Optimization。这是纯汇编工程,不需要 C 编译器参与。

最关键的一步在Linker 页:

  • 勾选 "Use Memory Layout from Target Dialog" →立刻取消勾选!
  • 点击 "Manage Linker Script" → "Open…",选择你资源包里的STM32F103C8_FLASH.ld(或类似名称的 .ld 文件,这是链接脚本,定义了代码、数据、栈放在内存哪个位置)
  • 如果没有现成 .ld 文件,点击 "Manage Linker Script" → "Generate…",Keil5 会自动生成一个基础脚本,但你需要手动编辑它:确保FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 0x00010000(64KB),RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00005000(20KB),并确认_estack = 0x20005000;(栈顶地址)

提示:链接脚本是汇编工程的“地图”。它告诉链接器:“代码放 Flash 开头,全局变量放 RAM 开头,栈从 RAM 结尾往下长”。设错一个数字,程序可能跑飞,且毫无提示。

3.4 写第一个汇编程序:让 PA0 的 LED 闪起来

打开main.s,输入以下内容(逐行抄,别复制粘贴,手敲加深记忆):

; main.s - STM32F103 汇编入门:PA0 LED 闪烁 AREA |.text|, CODE, READONLY ENTRY ; ======== 栈和寄存器初始化 ======== IMPORT SystemInit BL SystemInit ; 调用 CMSIS 系统初始化(设置时钟等) ; ======== GPIOA 时钟使能 ======== LDR R0, =0x40021018 ; RCC_APB2ENR 寄存器地址 LDR R1, =0x00000004 ; BIT2 = GPIOAEN = 1 STR R1, [R0] ; 使能 GPIOA 时钟 ; ======== 配置 PA0 为推挽输出 ======== LDR R0, =0x40010800 ; GPIOA_CRL 地址(低8位配置) LDR R1, =0x00000033 ; PA0: CNF0[1:0]=0b00, MODE0[1:0]=0b11 (推挽, 2MHz) STR R1, [R0] ; 写入配置 ; ======== 主循环:点亮 -> 延时 -> 熄灭 -> 延时 ======== main_loop ; 点亮 PA0: BSRR 的 bit16 置 1 (BS0) LDR R0, =0x40010808 ; GPIOA_BSRR 地址 MOV R1, #0x00010000 ; bit16 = 1 STR R1, [R0] ; 延时约 500ms(粗略,依赖系统时钟) MOV R2, #0x30000 ; 大循环计数 delay_on SUBS R2, R2, #1 BNE delay_on ; 熄灭 PA0: BSRR 的 bit0 置 1 (BR0) MOV R1, #0x00000001 ; bit0 = 1 STR R1, [R0] ; 延时约 500ms MOV R2, #0x30000 delay_off SUBS R2, R2, #1 BNE delay_off B main_loop ; 无限循环 END

这段代码在干啥?白话解释:

  • AREA |.text|, CODE, READONLY:声明下面的代码属于只读代码段(.text),这是汇编的“章节标题”。
  • ENTRY:告诉链接器,程序入口从这里开始(Keil5 会把Reset_Handler指向此处)。
  • LDR R0, =0x40021018:不是把数字0x40021018放进 R0,而是把地址0x40021018(RCC_APB2ENR)加载进 R0——这是访问硬件寄存器的第一步。
  • STR R1, [R0]:把 R1 的值(0x00000004),写到 R0 存的地址里,即“打开 GPIOA 的时钟开关”。没有这步,后续所有对 GPIOA 的操作都无效——就像没开水龙头就拧水龙头。
  • GPIOA_CRL的0x00000033:PA0 占用 CRL 的 bit0~3,0b0011表示“输出模式,最大速度 2MHz”,这是 STM32 手册里规定的“密码”,必须严格匹配。
  • GPIOA_BSRR的妙用:写0x00010000到 bit16,等于“置位 PA0”(点亮);写0x00000001到 bit0,等于“复位 PA0”(熄灭)。BSRR 是原子操作,比先读-改-写更安全,是 STM32 汇编的黄金技巧。
  • SUBS R2, R2, #1:S后缀表示“更新状态位”,BNE(Branch if Not Equal)根据上一条指令是否让 R2=0 来决定是否跳转——这是汇编里实现循环的核心逻辑。

保存main.s,点击 Build(F7)。如果出现0 Error(s), 0 Warning(s),恭喜,你的第一个 STM32 汇编程序编译成功了!


4. 常见问题排查:新手必踩的 5 个坑,我替你翻过车了

4.1 现象:Keil5 编译报错Error: #159: declaration is incompatible with ...

原因:你在main.s里写了IMPORT SystemInit,但工程里没添加system_stm32f10x.c或对应的汇编版system_stm32f10x.s。SystemInit是 CMSIS 提供的 C 函数,Keil5 默认不自动链接。
解决:

  • 下载 CMSIS 5.x 包(从 ARM 官网或 Keil 安装目录找),找到Device\ARM\STM32F1xx\Source\Templates\arm\system_stm32f10x.c
  • 右键Source组 → Add Existing Files…,添加该文件
  • 或者,更简单:删掉IMPORT SystemInit和BL SystemInit两行,自己用汇编配置时钟(需手动写 RCC 寄存器,稍复杂,但彻底摆脱 C 依赖)

4.2 现象:程序烧录后 LED 完全不亮,用 ST-Link Utility 读 Flash 发现全是 0xFF

原因:链接脚本(.ld 文件)里的ORIGIN地址写错了。例如,误写成ORIGIN = 0x08000000(正确)但LENGTH = 0x00001000(只给 4KB,实际 F103C8 有 64KB),导致代码被截断。
解决:

  • 打开 .ld 文件,确认FLASH段的ORIGIN是0x08000000,LENGTH是0x00010000(64KB)
  • 在 Keil5 的 Project → Options for Target… → Linker 页,点击 "Manage Linker Script" → "Edit…",检查生成的LR_IROM1和ER_IROM1是否匹配

4.3 现象:LED 亮了但不闪烁,一直常亮或常灭

原因:GPIOA_BSRR地址或值写错。常见错误:

  • 把0x40010808(BSRR)错写成0x40010800(CRL)
  • 点亮写0x00000001(bit0),熄灭也写0x00000001(应该写0x00010000)
  • 延时循环里SUBS没加S,导致BNE永远不跳转(SUB不更新状态位,BNE总是执行)
    解决:
  • 在 Debug 模式下(Ctrl+F5),打开 View → Registers,观察 R0/R1 值是否符合预期
  • 打开 View → Memory Windows,输入0x40010808,看 BSRR 寄存器值是否随程序变化

4.4 现象:烧录时报错Cannot access Memory at 0x20000000或Target not connected

原因:ST-Link 驱动未安装,或开发板供电异常(USB 供电不足,尤其插了多个外设时)。
解决:

  • 下载 ST-Link 官方驱动(stsw-link009),安装后重启电脑
  • 拔掉开发板 USB,用万用表测3.3V引脚电压是否稳定在 3.2~3.4V;若低于 3.0V,换 USB 线或电脑 USB 口
  • Keil5 中 Project → Options for Target… → Debug 页,确认 Debugger 选的是ST-Link Debugger,Settings → Flash Download 里勾选Reset and Run

4.5 现象:程序能跑,但延时不准,LED 闪烁快得像呼吸灯

原因:SystemInit()未执行,或执行后系统时钟(SYSCLK)不是预期的 72MHz。F103 默认 HSI 是 8MHz,SystemInit会通过 PLL 倍频到 72MHz。如果跳过了它,CPU 就以 8MHz 运行,延时循环次数就得按比例增加(约 9 倍)。
解决:

  • 在main.s开头保留IMPORT SystemInit和BL SystemInit
  • Debug 时,在BL SystemInit后设断点,单步进入,确认RCC_CFGR寄存器的SW[1:0]是否为0b10(PLL 作为系统时钟)
  • 更靠谱的延时方案:用 SysTick 定时器(需配置中断),但那是下一课的内容了。

5. 下一步学什么?从“能跑”到“真懂”的三步跃迁路径

5.1 第一步:吃透这份资源里的 4 个核心文件

别急着写新功能,先把下载包里最基础的 4 个.s文件逐行“翻译”成中文笔记:

  • startup_stm32f103xb.s:重点看_estack(栈顶)、__main(C 运行时入口)、Reset_Handler(复位后第一行)三处。理解为什么SP必须在Reset_Handler最开头就初始化,为什么B __main后面还要跟一堆DCD(定义常量字)——它们是 C 语言全局变量的内存占位符。
  • system_stm32f10x.s(或.c):找到SetSysClockTo72()函数,抄下它操作RCC_CR,RCC_CFGR,RCC_CIR的每一条STR指令,对照《STM32F103xx 参考手册》第 7 章,搞懂 PLL 倍频的 5 个步骤(开 HSE、等就绪、配 PLL、切时钟、关 HSI)。
  • main.s(你刚写的):把所有寄存器地址(0x40021018,0x40010800)在手册里找到对应章节,截图保存。例如0x40021018是 RCC->APB2ENR,手册里明确写着 bit2 是 GPIOAEN。
  • led.s(如果有):对比它和你写的main.s,看它如何用宏(MACRO/MEND)封装BSRR操作,学习汇编的“函数”写法。

我第一次学的时候,花了整整两天,就干这一件事:把startup.s里 200 行代码,每行旁边手写一句“这行让芯片干啥”,贴满 A4 纸。结果第三天调试一个中断问题时,一眼就看出是PSP(进程栈指针)没初始化导致的 HardFault——因为我在纸上画过整个栈帧结构。

5.2 第二步:动手改造,做三个“最小实验”

目标:不追求功能多,只验证你对寄存器的理解是否准确。每个实验独立建工程,命名如exp_gpio_in,exp_usart_tx,exp_systick。

实验名关键动作验证方式为什么重要
GPIO 输入配置 PA1 为浮空输入,读取GPIOA_IDR的 bit1,用 PB0 输出指示(高电平=按键按下)按开发板 KEY 按键,PB0 LED 亮/灭同步变化理解 IDR(输入数据寄存器)和 ODR(输出数据寄存器)的区别,避免“读 ODR 当输入”这种经典翻车
USART 发送配置 USART1(PA9/PA10),手动写USART1_SR查忙、USART1_DR发数据,发送字符串"OK\n"用串口助手(XCOM/SSCOM)接收,看到 OK掌握轮询式通信的时序控制,TXE(发送寄存器空)和TC(传输完成)标志位的使用场景
SysTick 中断配置 SysTick 为 1ms 中断,中断服务程序里翻转 PC13(板载 LED),主循环空跑观察 LED 以精确 1Hz 闪烁,用逻辑分析仪测波形理解异常向量表、PendSV_Handler、SysTick_Handler的注册机制,这是 RTOS 的基石

每个实验做完,务必用 Keil5 的 Debug → View → Disassembly Window,对照你写的汇编和实际生成的机器码(左边是地址,中间是 HEX,右边是助记符)。你会发现LDR R0, =0x40010808被编译成LDR R0, [PC, #offset],这就是汇编器做的“地址重定位”——它在告诉你:硬件地址不是写死的,而是由链接器动态计算的。

5.3 第三步:建立你的“汇编-硬件”反射弧

最终极的训练,是脱离 Keil5,用最原始的方式验证。准备一个文本编辑器(Notepad++ 或 VS Code),关闭所有 IDE:

  • 打开《STM32F103xx 参考手册》PDF,搜索 “GPIOA_BSRR”,找到地址0x40010808和位定义
  • 打开《Cortex-M3 技术参考手册》,搜索 “STR instruction”,确认语法STR <Rt>, [<Rn>{, #<offset>}]
  • 手写一行:STR R1, [R0],然后问自己:R0 此刻存的是什么?R1 存的是什么?执行后,内存0x40010808位置的值变成多少?
  • 再查 “BSRR register”,确认写0x00010000是置位 bit16,对应 PA0 输出高电平

坚持每天做 3 个这样的“反射练习”,一周后,你会发现自己看到0x40010808就条件反射想到“BSRR”,看到STR就想到“内存写入”,看到BNE就想到“循环判断”。这不是死记硬背,而是大脑为 STM32 架起了一条专用神经回路。

从那以后我每次调试一个新外设(比如 I2C 或 SPI),都会先手写 10 行汇编,只做一件事:用STR和LDR强制读写它的控制寄存器,观察硬件响应。哪怕最终用 C 实现,这 10 行汇编也是我的“信任锚点”——它告诉我,芯片真的在按我的指令行动。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询