news 2026/10/21 4:23:05

STM32F103汇编入门:从寄存器控制到LED点亮

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103汇编入门:从寄存器控制到LED点亮

简介:本资源是一份面向嵌入式初学者与底层开发爱好者的STM32F103汇编语言实践项目,聚焦“点灯”这一经典入门实验,帮助读者深入理解Cortex-M3内核寄存器操作、GPIO配置流程及Keil5环境下汇编程序的完整构建链路。压缩包共19个文件,含核心汇编源码(.s)、Keil5工程文件(.uvprojx、.uvoptx)、链接脚本(.sct)、编译输出(.axf、.map、.lst)、启动代码(startup_stm32f10x_md.s)及自动化批处理脚本(keilkilll.bat),覆盖从编写、编译、链接到调试的全流程所需构件。资源体积仅28KB,结构精炼,无冗余依赖,便于快速导入Keil5直接运行与单步调试。目前已有1652人学习下载,适合希望夯实ARM汇编基础、掌握硬件寄存器直控方法、理解中断向量表与启动流程的开发者,是进阶C语言裸机开发前不可或缺的底层认知训练材料。

1. 这不是“写代码”,是给芯片下指令:STM32F103汇编程序到底是什么、为什么新手要从它开始?

你有没有试过拧开一个遥控器,看到里面密密麻麻的黑色小方块和细如发丝的铜线?那里面真正干活的,不是电池,也不是按钮,而是一块叫“微控制器”的芯片——比如 STM32F103。它就像一台微型电脑,但没有屏幕、没有键盘,甚至不认得 Python 或 C 语言写的“你好世界”。它只听一种话:二进制机器码。而汇编语言,就是人类能看懂、又能准确翻译成机器码的“最后一层人话”。

所以,“STM32F103汇编程序”不是一堆老古董代码,它是你第一次亲手把想法变成芯片动作的桥梁:按一下按键,LED 灯亮;读一次温度传感器,寄存器里就多出两个字节;让电机转半圈,其实是往某个地址写了特定数值……这些事,C 语言帮你藏起来了,而汇编,让你亲手摸到开关。

它适合谁?不是给想快速做出智能手表的工程师,而是给刚拆开第一块开发板、对着原理图发懵、搞不清“GPIOA_BSRR”和“RCC_APB2ENR”到底谁管谁的新手。因为只有从汇编起步,你才不会把“点亮 LED”当成魔法,而是清楚知道:
→ 先打开时钟(不然外设根本没电)
→ 再配置引脚模式(推挽?上拉?)
→ 最后往输出寄存器写 1(或 0)——这一行指令,就是光本身。

这不是为了复古,而是为了建立“控制感”:当程序不跑,你知道该查哪几个寄存器;当灯不亮,你不会先怀疑硬件坏了,而是翻出参考手册第 142 页,核对 BSRR 的 bit 位是否写反了。这种确定性,是所有嵌入式开发的底气起点。


2. 为什么非得用 Keil5?先搞清工具链:编辑、编译、烧录,三步缺一不可

2.1 Keil5 不是“软件”,而是一整套“芯片翻译工厂”

想象你要把中文菜谱(你的汇编代码)做成一道真实可吃的菜(能让 STM32F103 执行的机器码),你需要三样东西:

  • 厨师(编辑器):让你写代码、改错、加注释——Keil5 的编辑器支持语法高亮、跳转定义,写LDR R0, =0x40010800时,它会立刻告诉你这地址是不是 GPIOA 的基地址;
  • 翻译官(ARMASM 汇编器):把MOV R1, #0x01这种人类可读的指令,逐条转成E3A01001这种芯片能执行的 32 位二进制;
  • 快递员(Flash 下载器):把生成的.hex或.axf文件,通过 ST-Link 或 J-Link,一比特一比特地“灌”进芯片的 Flash 存储器里。

Keil5 把这三者打包成一个界面:左边是文件树,中间是代码编辑区,顶部是“Build”(编译)、“Download”(烧录)、“Debug”(单步调试)三个大按钮。它不强制你学 Makefile、不让你手动敲arm-none-eabi-gcc,新手第一次点“Build”,看到 Output 窗口跳出[0 errors, 0 warnings],那种“我造出来了”的实感,比任何教程都管用。

提示:Keil5 官方提供免费版(MDK-Lite),最大支持 32KB 代码——而一个纯汇编的 LED 闪烁程序,通常不到 200 字节。够用,且无功能阉割。

2.2 你的第一个工程长什么样?5 个真实文件,一个都不能少

新建一个 Keil5 工程后,你会看到这些文件(名称可能略有差异,但角色固定):

文件名类型作用新手易忽略点
startup_stm32f103xb.s启动文件(汇编)芯片上电后第一条执行的代码:初始化栈指针、复制数据段、跳转到main别删!它定义了中断向量表位置,删了程序根本启动不了
main.s主程序(汇编)你写的逻辑:配置时钟、点灯、循环延时别写成main.c!.s后缀告诉 Keil:“请用 ARMASM,不是 C 编译器”
stm32f103xb.h头文件(C)定义所有寄存器地址,如#define GPIOA_BASE 0x40010800汇编里不能直接#include,但 Keil 会自动关联,你在main.s里写GPIOA_BSRR,它知道对应0x40010808
system_stm32f103xb.c系统初始化(C)配置系统时钟(HSE/HSI/PLL)汇编项目里可删,但删前必须确认startup.s里已手动配置好时钟(否则 APB2 总线没电,GPIO 不工作)
target.hex输出文件(二进制)编译后生成,可直接用烧录工具写入芯片不是.bin!.hex是 Intel 格式,含地址信息,ST-Link Utility 默认认它

注意:Keil5 创建新工程时,默认勾选“Run-Time Environment”,会自动生成system_stm32f103xb.c和startup_stm32f103xb.s。新手建议保留startup.s,删掉system_*.c——因为我们要用汇编手动配时钟,避免 C 初始化和汇编逻辑打架。

2.3 从零创建工程:6 步操作,每一步都在解决一个具体问题

  1. 打开 Keil5 → Project → New µVision Project
    → 选一个空文件夹(如D:\stm32_asm\led_blink),输入工程名led_blink.uvprojx
    →为什么强调“空文件夹”?Keil 会在该目录下建Objects/、Listings/等子目录,混入其他文件会导致编译路径错乱。

  2. 选择 Device:STM32F103C8(或你手头芯片型号)
    → 在弹出窗口搜索 “STM32F103”,选STM32F103C8T6(常见蓝 pill 板主控)
    →关键点:Keil 会自动加载该芯片的启动文件和头文件路径,省去手动配置。

  3. 添加启动文件:Project → Manage → Run-Time Environment
    → 展开Device→ 勾选Startup(确保startup_stm32f103xb.s被加入)
    →不勾选CMSIS或RTOS——汇编项目不需要这些高级组件。

  4. 新建汇编文件:File → New → 保存为main.s(注意后缀是.s,不是.asm)
    → 右键Source Group 1→ Add Existing Files to Group → 选中main.s
    →为什么必须是.s?Keil 对.s文件默认调用 ARMASM;.asm可能被识别为其他汇编器格式,报错“unknown instruction”。

  5. 配置汇编器:Project → Options for Target → ASM 标签页
    → 勾选Generate Listings(生成.lst列表文件,方便查指令编码)
    → 在Define栏填USE_STDPERIPH_DRIVER=0(关闭标准外设库,避免符号冲突)
    →这步漏掉,编译时可能报Error: #20: identifier "GPIOA" is undefined——因为 Keil 默认启用 CMSIS,而汇编里未声明符号。

  6. 设置输出格式:Output 标签页 → 勾选Create HEX File
    → Debug 标签页 → UseST-Link Debugger(如果你用 ST-Link)
    →.hex是烧录必备格式;不勾选,编译完只有.axf,无法用 ST-Link Utility 烧录。

完成这 6 步,你的工程骨架就立住了——不是“看起来像”,而是每个文件各司其职,编译器知道怎么处理每一行。


3. 第一个例子:用汇编点亮一颗 LED,从寄存器地址开始写起

3.1 硬件准备:蓝 pill 板上的 LED 接在哪?先查手册再动手

别急着写代码。拿出你的 STM32F103C8T6 开发板(俗称“蓝 pill”),找到板载的 LED(通常标着LD2或PC13)。翻开《STM32F103xx 参考手册》(RM0008)第 279 页,查 GPIO 端口寄存器映射:

  • GPIOC 基地址:0x40011000
  • GPIOC_BSRR(置位/复位寄存器):0x40011008
  • PC13 对应 BSRR 的 bit 13(置位)和 bit 13+16=bit 29(复位)

为什么是 PC13?因为蓝 pill 板原理图上,LED 阳极接 3.3V,阴极通过限流电阻接到 PC13 引脚——这意味着:
✅PC13 输出低电平(0)→ LED 亮
❌PC13 输出高电平(1)→ LED 灭

这个“低电平点亮”是硬件决定的,不是软件规则。很多新手翻车,就是因为没看原理图,死磕“为什么写 1 不亮”。

提示:Keil5 自带的stm32f103xb.h里已定义#define GPIOC_BASE 0x40011000,但汇编里不能直接#include。我们用更稳妥的方式:在main.s里用EQU伪指令定义常量。

3.2 逐行写main.s:12 行汇编,解释每一行在干什么

; main.s - STM32F103 汇编点灯程序 AREA RESET, DATA, READONLY EXPORT __main IMPORT SystemInit IMPORT __main_post_stack_init ; 定义寄存器地址常量(用 EQU,不是 C 的 #define) GPIOC_BASE EQU 0x40011000 GPIOC_BSRR EQU GPIOC_BASE + 0x08 RCC_BASE EQU 0x40021000 RCC_APB2ENR EQU RCC_BASE + 0x18 AREA |.text|, CODE, READONLY ENTRY __main: ; Step 1: 开启 GPIOC 时钟(APB2 总线) LDR R0, =RCC_APB2ENR LDR R1, [R0] ORR R1, R1, #(1 << 4) ; bit4 = IOPCEN, 使能 GPIOC 时钟 STR R1, [R0] ; Step 2: 配置 PC13 为推挽输出(CNF13[1:0]=00, MODE13[1:0]=11) LDR R0, =GPIOC_BASE LDR R1, [R0, #0x04] ; 读取 GPIOC_CRL(低 8 位配置寄存器) BIC R1, R1, #(0xF << 20) ; 清除 CNF13[1:0] 和 MODE13[1:0](bit20~23) ORR R1, R1, #(0x3 << 20) ; MODE13=11(输出模式),CNF13=00(推挽) STR R1, [R0, #0x04] ; 写回 GPIOC_CRL ; Step 3: 点亮 LED(PC13 = 0 → 写 BSRR 的 bit29 复位) LDR R0, =GPIOC_BSRR MOV R1, #(1 << 29) ; bit29 = 复位 PC13 STR R1, [R0] stop B stop ; 无限循环,防止跑飞 END

逐行白话解释:

  • AREA RESET, DATA, READONLY:声明一个名为RESET的只读数据区(存放向量表等)
  • EXPORT __main:告诉链接器,“__main是程序入口”,Keil 启动文件会跳转到这里
  • GPIOC_BASE EQU 0x40011000:EQU是汇编里的“宏定义”,相当于 C 的#define,让代码可读
  • LDR R0, =RCC_APB2ENR:把RCC_APB2ENR地址(0x40021018)装入 R0 寄存器(=表示加载立即数地址)
  • LDR R1, [R0]:从 R0 指向的地址(即RCC_APB2ENR)读出当前值,存入 R1
  • ORR R1, R1, #(1 << 4):对 R1 执行“按位或”,把 bit4 置 1 → 即开启 GPIOC 时钟
  • STR R1, [R0]:把修改后的 R1 值写回RCC_APB2ENR地址,生效!
  • LDR R1, [R0, #0x04]:[R0, #0x04]表示“R0 地址 + 4”,即GPIOC_CRL(配置低 8 位寄存器)
  • BIC R1, R1, #(0xF << 20):BIC是“位清除”,把 R1 的 bit20~23 清零(为重写 MODE/CNF 做准备)
  • ORR R1, R1, #(0x3 << 20):把 MODE13 设为11(最大输出速度 50MHz),CNF13 设为00(推挽)
  • MOV R1, #(1 << 29):构造一个数:bit29 = 1,其余为 0 → 写入 BSRR 就是复位 PC13(输出 0)
  • B stop:无条件跳转到stop标签,形成死循环,防止 CPU 执行垃圾内存

关键参数说明:

  • 1 << 4:左移 4 位 = 16 = 0x10,对应RCC_APB2ENR的 bit4(IOPCEN)
  • 0x3 << 20:0x3是二进制11,左移 20 位后占据 bit20~21(MODE13),bit22~23 保持 0(CNF13)
  • 1 << 29:BSRR 高 16 位是复位位,bit29 对应 PC13,写 1 即复位(输出 0)

3.3 编译 & 烧录:3 个按钮,看清每一步的反馈信息

  1. 点击 Build(锤子图标)
    → 观察 Output 窗口:

    compiling main.s... assembling startup_stm32f103xb.s... linking... Program Size: Code=124 RO-data=0 RW-data=0 ZI-data=1624 Bytes

    ✅ 如果出现0 Error(s), 0 Warning(s),说明汇编通过;
    ❌ 如果报Error: #20: identifier "GPIOC_BASE" is undefined,检查main.s是否漏写EQU定义;
    ❌ 如果报Error: #65: expected a string,检查LDR R0, =RCC_APB2ENR中的=是否误写为:或空格缺失。

  2. 点击 Download(向下箭头图标)
    → 确保 ST-Link 已连接、开发板供电正常;
    → Keil 会自动调用 ST-Link 驱动,显示:

    Loading file... Erasing target... Programming... Verify OK.

    ✅Verify OK表示烧录成功;
    ❌ 如果卡在Erasing target...,检查 ST-Link 接线(SWDIO/SWCLK/GND)是否松动;
    ❌ 如果报No Cortex-M device found,重启 Keil 并右键 Target → Connect 重连。

  3. 观察现象
    → 上电后,LD2(PC13)应常亮(因为我们只写了复位,没写置位);
    → 如果不亮:用万用表测 PC13 引脚电压,应为 0V(低电平);若为 3.3V,说明寄存器没写成功,回头查BSRR地址和 bit 位。


4. 避坑指南:新手必踩的 4 个汇编“玄学”问题,亲测有效

4.1 现象:编译通过,但 LED 完全不响应,万用表测 PC13 电压是 3.3V

原因:GPIOC_BSRR地址写错,或 bit 位弄反。BSRR 高 16 位是复位(写 1 置 0),低 16 位是置位(写 1 置 1)。新手常把1 << 29写成1 << 13,结果写的是低 16 位 → 置位 PC13(输出 1),LED 灭。
解决:打开 Keil 的View → Memory Windows → Memory 1,输入0x40011008,查看 BSRR 寄存器值。运行后应看到0x20000000(bit29=1),而不是0x00002000(bit13=1)。

4.2 现象:编译报错Error: #65: expected a string,指向LDR R0, =RCC_APB2ENR

原因:=符号前后有中文空格、全角字符,或RCC_APB2ENR未用EQU定义(汇编器不认识这个符号)。
解决:删除整行重新手打,确保=是英文半角;检查RCC_APB2ENR是否在main.s开头用EQU正确定义(如RCC_APB2ENR EQU RCC_BASE + 0x18)。

4.3 现象:烧录时报No Cortex-M device found,ST-Link 指示灯不亮

原因:ST-Link 与开发板接线错误。常见错误:SWDIO 和 SWCLK 接反;GND 未接;或开发板 USB 供电不足导致 ST-Link 无法识别。
解决:断电,用杜邦线对照原理图重接:

  • ST-Link 的SWDIO→ 开发板PA13(或SWDIO标注点)
  • ST-Link 的SWCLK→ 开发板PA14(或SWCLK标注点)
  • ST-Link 的GND→ 开发板任意 GND
  • 不接 ST-Link 的 3.3V 引脚(开发板自己供电,避免电压冲突)。

4.4 现象:LED 闪一下就灭,或者完全不亮,Debug 模式下单步执行却正常

原因:startup_stm32f103xb.s里的SystemInit()函数被调用,它会重置时钟配置,覆盖你手动写的 RCC 设置。
解决:打开startup_stm32f103xb.s,找到__main_post_stack_init调用处,注释掉BL SystemInit这一行(在它前面加;)。因为纯汇编项目,我们自己配时钟,不需要 C 库的初始化。


5. 下一步学什么?从“点灯”到“能干活”的 3 个扎实台阶

5.1 台阶一:给汇编加“时间感”——手写延时循环,理解时钟树

现在 LED 是常亮的,但真正的应用需要“闪烁”。C 语言用Delay_ms(500),汇编得自己算周期。STM32F103 默认用内部 HSI 时钟(8MHz),经 PLL 倍频后系统时钟为 72MHz。一条SUBS R0, R0, #1指令耗时 1 个周期,那么让 R0 从 1000000 减到 0,大约耗时:
1000000 / 72000000 ≈ 0.0139 秒

但实际还要考虑流水线、分支预测。更可靠的做法是:

  1. 用 Keil 的View → Performance Analyzer查看某段循环的精确周期数;
  2. 写一个嵌套循环:外层控制秒级,内层控制毫秒级;
  3. 把延时封装成子程序,用BL delay_ms调用,避免代码重复。
delay_ms PUSH {R4-R6} ; 保护寄存器 MOV R4, #1000 ; 外层计数(1ms ≈ 1000 次内层) outer_loop MOV R5, #72000 ; 内层计数(72MHz / 1000 = 72000 cycles per ms) inner_loop SUBS R5, R5, #1 BNE inner_loop SUBS R4, R4, #1 BNE outer_loop POP {R4-R6} BX LR ; 返回调用处

从那以后我每次写延时,都先用 Performance Analyzer 实测 10 次取平均值,再微调内层计数。因为不同优化等级、不同 Keil 版本,指令周期会有细微差别——这是嵌入式里最朴素的“后悔药”:不靠猜,靠测。

5.2 台阶二:让芯片“听懂按钮”——汇编读取 GPIO 输入

点灯是输出,读按键是输入。蓝 pill 板的按键(B1)通常接PA0,按下时接地(低电平)。要检测它,需:

  • 开启 GPIOA 时钟(同点亮灯,改RCC_APB2ENRbit2);
  • 配置 PA0 为浮空输入(GPIOA_CRL bit0~3 = 0b0100);
  • 循环读GPIOA_IDR(输入数据寄存器)的 bit0;
  • 加消抖:连续读 3 次,间隔 10ms,都为 0 才判定“按下”。

这里的关键是理解IDR和ODR的分离:IDR只读,反映引脚真实电平;ODR只写,控制输出值。新手常误用ODR读按键,结果永远读到自己上次写的值。

5.3 台阶三:走出“裸机”,接入中断——用汇编写 NVIC 配置

目前程序是“查询式”(Polling):CPU 一直问“按键按了没?”,很累。中断能让 CPU 去干别的,等按键按下时,硬件自动暂停当前任务,跳转到你的中断服务程序(ISR)。
步骤精简为三步:

  1. 在startup.s的中断向量表里,把EXTI0_IRQHandler地址指向你的 ISR 标签;
  2. 配置 EXTI:设置EXTI_IMR(中断屏蔽寄存器)bit0 = 1,允许 PA0 中断;
  3. 配置 NVIC:写NVIC_ISER(中断使能寄存器)bit6 = 1(EXTI0 对应 IRQ6);
  4. 在 ISR 里清中断标志(EXTI_PR bit0 = 1),再执行点灯逻辑。

从那以后我每次配置中断,都强制走一遍“查向量表 → 设屏蔽 → 开 NVIC → 清标志”四步 checklist。因为漏一步,中断就不触发,而现象是“完全没反应”,比逻辑错误更难排查。

5.4 学习资源推荐:拒绝碎片化,用好这 3 个“官方锚点”

资源用途为什么靠谱
《STM32F103xx 参考手册》(RM0008)查寄存器地址、位定义、时序图ST 官方发布,PDF 里 Ctrl+F 搜GPIOC_BSRR,第 282 页直接给出 bit 功能表,比任何博客都准
Keil ARMASM 用户指南(ARM DUI0204)查LDR/BIC/BL等指令语法、寻址模式Keil 官网免费下载,讲透LDR R0, =label和LDR R0, [R1]的区别,避坑神器
STM32CubeMX 生成的汇编启动文件对照学习:用 CubeMX 配一个 GPIO 输出,导出汇编工程,看它怎么写RCC和GPIO不用它烧录,只当“活教材”——生成的startup.s和system_stm32f103xb.s是经过千人验证的工业级写法

希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 4:22:11

2026马年情人节AI内容生成方案:文案、图像、视频、语音全拆解

1. 2026年情人节为什么特殊&#xff1a;马年、爱意与团圆的三重叠加1.1 双节同框的时间窗&#xff0c;藏着一波被低估的需求2026年的2月14日&#xff0c;和往年最大的不同在于——它紧贴着农历丙午马年的新春。这个时间窗非常少见&#xff0c;情人节和春节几乎是前后脚抵达&…

作者头像 李华
网站建设 2026/10/21 4:21:11

可验证能力单元:Skills图谱的构建与工程化实践

1. 项目概述&#xff1a;当“skills”不再只是简历上的单词&#xff0c;而成为可验证、可组合、可进化的个人能力操作系统最近在几个技术社区和职业发展论坛里&#xff0c;“skills”这个词高频出现&#xff0c;但几乎没人再把它当成简历末尾那行加粗的“Python/SQL/Project Ma…

作者头像 李华
网站建设 2026/10/21 4:20:12

Android权限提升工具终极指南:Shizuku与Dhizuku深度对比

Android权限提升工具终极指南&#xff1a;Shizuku与Dhizuku深度对比 在Android生态系统中&#xff0c;权限管理一直是用户和开发者面临的重要挑战。awesome-shizuku和Dhizuku作为两大Android权限提升工具&#xff0c;正改变着我们与设备交互的方式。无论你是想要更多设备控制权…

作者头像 李华
网站建设 2026/10/21 4:19:53

从零构建AI驱动智能工作流:OpenClaw全栈自动化实战

从零构建AI助手驱动的智能工作流系统&#xff0c;这句话我一开始也是当口号看的。直到我连续被凌晨三点的告警电话吵醒两次&#xff0c;才下定决心把手里那套“定时脚本if-else大法”推翻重来。这个过程中最顺手的工具&#xff0c;是一个相对小众的全栈自动化框架——OpenClaw。…

作者头像 李华
网站建设 2026/10/21 4:18:03

戴森球计划工厂蓝图指南:7000+ 套免费方案,4 步装完直接套用

戴森球计划工厂蓝图指南&#xff1a;7000 套免费方案&#xff0c;4 步装完直接套用 【免费下载链接】FactoryBluePrints 游戏戴森球计划的**工厂**蓝图仓库 项目地址: https://gitcode.com/GitHub_Trending/fa/FactoryBluePrints FactoryBluePrints 是一个戴森球计划工厂…

作者头像 李华