STM32F103纯汇编工程:Keil5一键编译,点灯/按键/ADC全实现 简介本资源是一份面向嵌入式初学者与底层开发爱好者的STM32F103汇编语言实践项目聚焦“点灯”这一经典入门实验帮助读者深入理解Cortex-M3内核寄存器操作、GPIO配置流程及Keil5环境下汇编程序的完整构建链路。压缩包共19个文件涵盖核心汇编源码.s、工程配置.uvprojx/.uvoptx、链接脚本.sct、编译输出.axf/.map/.lst、调试支持JLinkSettings.ini/.iex及一键清理脚本keilkilll.bat结构完整便于逆向分析编译逻辑与启动流程。资源仅28KB轻量精炼已获1652人学习下载。读者可直接导入Keil5运行调试通过单步执行观察R0–R15寄存器变化、跟踪GPIOx_MODER等外设寄存器写入过程并结合注释清晰的startup_stm32f10x_md.s掌握中断向量表布局与复位流程是夯实ARM汇编基础、建立硬件-指令映射直觉的优质实操素材。1. 这不是“学完就忘”的汇编课一个能真正在STM32F103上点灯、测按键、读ADC的纯汇编工程Keil5一键编译通过烧录即跑你可能试过在Keil5里新建一个ASM文件写几行MOV R0, #0x01结果编译报错undefined symbol __main也可能照着某本《ARM汇编入门》抄了NVIC向量表却卡在复位后不跳转——不是你不会写汇编是缺了一套真实硬件可验证、启动流程完整、外设驱动可调试的最小可行工程。这个“STM32F103汇编程序”包就是为解决这个问题而生它不讲寄存器理论不画抽象框图而是直接给你一个已通过STM32F103C8T6Blue Pill实测的完整汇编项目——从上电复位、栈初始化、向量表配置、系统时钟使能到GPIO翻转LED、EXTI检测按键、ADC采样电位器电压全部用纯ARM Thumb-2指令实现无任何C语言胶水代码。适合两类人一是想穿透C库封装、看清MCU启动本质的嵌入式进阶者二是需要在资源极度受限场景如ROM4KB、RAM1KB下做固件精简的量产工程师。它不是教学Demo而是一个可裁剪、可调试、可嵌入Bootloader的生产级汇编基座。2. 工程结构与Keil5环境配置从新建工程到生成.bin的全流程闭环2.1 目录结构与核心文件职责划分该工程采用扁平化组织所有源码集中于Src/目录无子模块嵌套符合汇编项目轻量可控的特性STM32F103_Assembly/ ├── Src/ │ ├── startup_stm32f103.s ← 核心复位向量、中断向量表、栈指针初始化 │ ├── main.s ← 主程序系统时钟配置、GPIO/EXTI/ADC初始化、主循环 │ └── system_stm32f103.s ← 辅助SysTick配置、延时函数、ADC采样触发逻辑 ├── Inc/ │ └── stm32f103.h ← 寄存器地址宏定义基于ST官方CMSIS头文件精简 ├── Output/ ← Keil5编译输出目录含.map/.lst/.bin ├── STM32F103_Assembly.uvprojx ← Keil5工程文件 └── README.md关键点在于startup_stm32f103.s必须作为第一个被链接的文件Keil5中右键该文件→Options for File→勾选Always build并确保Linker Order中排首位否则向量表无法加载到0x08000000起始地址。system_stm32f103.s中的Delay_ms函数采用空循环实现未使用SysTick中断——这是汇编工程的典型取舍牺牲精度换确定性避免中断嵌套带来的栈管理复杂度。2.2 Keil5工程创建与关键选项设置提示务必使用Keil MDK-ARM v5.37或更高版本低版本对Thumb-2指令支持不全BLX指令会报错新建工程Project → New uVision Project → 选择芯片为STM32F103C8注意不是Cortex-M3 Generic添加源文件右键Target → Add Group 创建Startup、Main、System分组分别加入对应.s文件关键编译选项Project → Options → C/C Tab去除Use MicroLIB汇编项目无需libc定义宏USE_STDPERIPH_DRIVER, STM32F103C8启用标准外设库寄存器定义取消One ELF Section per Function避免链接时函数段错乱关键链接选项Project → Options → Linker TabUse Memory Layout from Target Dialog→取消勾选必须手动指定scatter文件Scatter File→ 指向工程根目录下的STM32F103C8.sct内容见下文生成BIN文件Project → Options → Utilities Tab →勾选Create HEX File再在User Tab中添加Post-Build命令fromelf --bin --output ./Output/STM32F103_Assembly.bin ./Output/STM32F103_Assembly.axf2.3 Scatter文件详解控制代码/数据在Flash/RAM中的精确布局STM32F103C8.sct是此工程的灵魂它强制将向量表锁定在Flash起始地址栈区置于RAM高地址——这是裸机汇编运行的前提; STM32F103C8.sct LR_IROM1 0x08000000 0x00010000 { ; Load Region: Flash (64KB) ER_IROM1 0x08000000 0x00010000 { ; Execution Region: Flash *.o (RESET, First) ; 复位向量必须在最前0x08000000 *(InRoot$$Sections) ; 包含__main等初始化段 .ANY (RO) ; 其他只读代码/常量 } RW_IRAM1 0x20000000 0x00005000 { ; RAM Region (20KB) .ANY (RW ZI) ; 可读写零初始化数据栈、全局变量 } }关键细节*.o (RESET, First)确保startup_stm32f103.s中的Reset_Handler符号被链接到0x08000000RW_IRAM1起始地址0x20000000是STM32F103的SRAM基址大小0x5000(20KB)需与实际芯片匹配C8为20KB若用CB则需改为0x0000A000。若此处填错程序将因栈溢出或向量表错位而静默失败。2.4 启动流程拆解从上电到main.s执行的每一步汇编工程的启动链比C项目更透明但也更易出错。以下是startup_stm32f103.s中关键段落及执行逻辑; 文件开头声明段属性与入口 AREA RESET, DATA, READONLY EXPORT __Vectors EXPORT __Vectors_End EXPORT __Vectors_Size ; 向量表必须严格按地址顺序 __Vectors DCD __initial_sp ; 0x08000000: 栈顶地址由链接脚本指定 DCD Reset_Handler ; 0x08000004: 复位处理函数 DCD NMI_Handler ; 0x08000008: NMI中断 DCD HardFault_Handler ; 0x0800000C: 硬故障 ; ... 后续中断向量省略共60项 ; 复位处理函数真正的启动入口 Reset_Handler PROC EXPORT Reset_Handler ; 声明为外部可见符号 IMPORT SystemInit ; 导入C库的系统初始化此处为空实现 IMPORT __main ; 导入C运行时入口必须存在 LDR R0, __initial_sp ; 加载栈顶地址到R0 MSR MSP, R0 ; 设置主栈指针MSP BL SystemInit ; 调用系统初始化实际为空仅占位 BL __main ; 跳转到C运行时入口Keil要求 ENDP注意__main是Keil链接器强制要求的入口符号即使不用C代码也必须存在。本工程中__main由main.s提供非C库其作用是跳转到真正的用户主程序; main.s 中的 __main 实现 AREA |.text|, CODE, READONLY ENTRY __main PROC IMPORT main_entry B main_entry ; 直接跳转到用户main_entry标签 ENDP这种设计绕过了C库的__rt_entry等冗余流程将控制权100%交还给开发者。main_entry即用户逻辑起点后续所有外设初始化均在此处完成。3. 核心外设驱动实现GPIO点灯、EXTI按键、ADC采样的纯汇编逻辑3.1 GPIO初始化与LED翻转寄存器操作的原子性保障STM32F103的GPIO配置需操作CRL/CRH模式控制、ODR输出数据、BSRR/BR置位/复位寄存器。汇编中必须保证多步寄存器写入的原子性避免中间状态导致IO异常; 初始化PA0为推挽输出LED连接PA0 Init_GPIOA PROC EXPORT Init_GPIOA ; 1. 使能GPIOA时钟APB2ENR[2] 1 LDR R0, 0x40021018 ; APB2ENR寄存器地址 LDR R1, [R0] ; 读当前值 ORR R1, R1, #0x00000004 ; 置位bit2GPIOAEN STR R1, [R0] ; 写回 ; 2. 配置PA0为推挽输出10MHzCRL[3:0] 0b0011 LDR R0, 0x40010800 ; GPIOA_CRL地址 LDR R1, [R0] BIC R1, R1, #0x0000000F ; 清除PA0当前配置bit3:0 ORR R1, R1, #0x00000003 ; 设置为推挽输出0b0011 STR R1, [R0] ; 3. 初始状态PA00LED灭 LDR R0, 0x4001080C ; GPIOA_BSRR地址置位寄存器 MOV R1, #0x00000001 ; PA0置位掩码 STR R1, [R0] ; 写BSRR低16位置位PA0 → LED亮等等 ; 错BSRR写1到低16位是置位但初始应灭灯 → 改用BR寄存器 LDR R0, 0x40010810 ; GPIOA_BRR地址复位寄存器 STR R1, [R0] ; 写BRR低16位复位PA0 → LED灭 BX LR ENDP关键细节BICBit Clear指令比AND更安全避免因读-改-写时序问题导致其他位被意外清零LED控制必须用BRR而非BSRR设置初始状态因为BSRR写1到高16位才是复位新手极易混淆所有寄存器地址均来自stm32f103.h中的宏定义确保可维护性。3.2 EXTI按键检测下降沿触发与软件消抖的汇编实现按键连接PB1需配置EXTI1线为下降沿触发。汇编中中断服务程序ISR必须严格遵循ARM AAPCS规范保存/恢复r0-r3, r12, lr; EXTI1中断服务程序在startup.s中向量表指向此处 EXTI1_IRQHandler PROC EXPORT EXTI1_IRQHandler PUSH {R0-R3,R12,LR} ; 保存寄存器AAPCS要求 ; 1. 读取按键状态防误触发 LDR R0, 0x40010808 ; GPIOB_IDR地址 LDR R1, [R0] TST R1, #0x00000002 ; 测试PB1是否为0按下 BEQ KEY_PRESSED ; 是则进入消抖 B EXIT_ISR ; 否退出 KEY_PRESSED ; 2. 软件消抖延时10ms调用system.s中的Delay_ms MOV R0, #10 BL Delay_ms ; 3. 二次确认避免抖动 LDR R0, 0x40010808 LDR R1, [R0] TST R1, #0x00000002 BNE EXIT_ISR ; 若PB1已释放退出 ; 4. 执行按键动作翻转LEDPA0 LDR R0, 0x4001080C ; GPIOA_BSRR MOV R1, #0x00000001 STR R1, [R0] ; 置位PA0 ; 5. 清除EXTI挂起位关键否则中断持续触发 LDR R0, 0x40010400 ; EXTI_PR地址挂起寄存器 MOV R1, #0x00000002 ; EXTI1挂起位 STR R1, [R0] ; 写1清零 EXIT_ISR POP {R0-R3,R12,PC} ; 恢复寄存器并返回PCLR ENDP提示EXTI_PR写1清零是STM32的特殊设计若忘记此步中断将无限嵌套——这是汇编开发中最经典的“黑匣子”翻车点。3.3 ADC采样电位器单次转换与数据读取的时序控制ADC初始化需配置时钟分频、通道、采样时间。汇编中必须严格遵循参考手册的时序要求如ADON置位后需等待tSTAB稳定; 初始化ADC1规则通道PA0输入 Init_ADC1 PROC EXPORT Init_ADC1 ; 1. 使能ADC1时钟APB2ENR[9] 1 LDR R0, 0x40021018 LDR R1, [R0] ORR R1, R1, #0x00000200 STR R1, [R0] ; 2. 配置ADC1_CR2开启连续转换、校准 LDR R0, 0x4001240C ; ADC1_CR2地址 MOV R1, #0x00000001 ; ADON1启动ADC STR R1, [R0] ; 3. 等待稳定手册要求tSTAB 1us此处用NOP填充 MOV R2, #10 DELAY_STAB SUBS R2, R2, #1 BNE DELAY_STAB ; 4. 校准ADC写CAL1 LDR R0, 0x4001240C MOV R1, #0x00000004 ; CAL1 STR R1, [R0] ; 5. 等待校准完成等待CAL0 WAIT_CAL LDR R0, 0x4001240C LDR R1, [R0] TST R1, #0x00000004 BNE WAIT_CAL ; 6. 配置ADC1_SQR1单通道转换顺序1 LDR R0, 0x4001242C ; ADC1_SQR1 MOV R1, #0x00000000 ; SQ10PA0通道 STR R1, [R0] ; 7. 配置ADC1_SMPR2PA0采样时间239.5周期 LDR R0, 0x40012408 ; ADC1_SMPR2 MOV R1, #0x00000007 ; SMP0[2:0]0b111 STR R1, [R0] BX LR ENDP ; 启动单次转换并读取结果 ADC_Read PROC EXPORT ADC_Read ; 1. 启动转换写SWSTART1 LDR R0, 0x4001240C MOV R1, #0x00000040 ; SWSTART1 STR R1, [R0] ; 2. 等待EOC转换结束标志 WAIT_EOC LDR R0, 0x4001240C LDR R1, [R0] TST R1, #0x00000002 ; EOC bit1 BEQ WAIT_EOC ; 3. 读取数据寄存器ADC1_DR LDR R0, 0x4001244C LDR R1, [R0] ; R1 12位ADC结果低12位有效 BX LR ENDP关键参数说明SMP00b111对应239.5个ADC时钟周期适用于1MHz以下输入信号电位器输出变化缓慢ADC_Read返回值在R1中高16位为0低12位为有效数据可直接用于LED亮度PWM映射手册明确要求ADON置位后必须等待tSTAB典型值1us汇编中用NOP循环是最可靠方式比调用延时函数更精准。4. 常见问题排查5条血泪经验总结的汇编开发避坑指南4.1 现象程序烧录后LED完全不亮J-Link识别到芯片但无法halt原因向量表未正确加载至0x08000000常见于scatter文件中*.o (RESET, First)未生效或startup_stm32f103.s未设为首个链接文件解决打开.map文件搜索__Vectors确认其地址为0x00000000加载地址和0x08000000执行地址若执行地址非0x08000000检查Keil中该文件的Options for File→Linker→Place in ROM/RAM是否被错误勾选4.2 现象按键中断偶尔触发两次LED闪烁异常原因EXTI_PR寄存器未清除或消抖延时不足10ms对机械按键偏短解决在EXTI1_IRQHandler末尾增加STR R1, [R0]后立即用LDR R2, [R0]读回验证是否清零将Delay_ms参数从10改为20并在消抖后增加第二次TST判断4.3 现象ADC读数始终为0或满量程0xFFF且不随电位器变化原因ADC时钟未使能APB2ENR[9]未置位或PA0未配置为模拟输入CRL[3:0]被误设为0b0000以外的值解决用ST-Link Utility读取0x40021018APB2ENR确认bit91读取0x40010800GPIOA_CRL确认PA0字段为0b0000模拟输入模式4.4 现象编译报错Error: L6218E: Undefined symbol __use_two_region_memory原因Keil工程中误启用了Use Memory Layout from Target Dialog导致链接器期望CMSIS标准内存模型解决Project → Options → Linker → 取消勾选Use Memory Layout from Target Dialog并确保Scatter File路径正确4.5 现象程序运行一段时间后死机J-Link显示Core halted at unknown location原因栈溢出RAM中栈空间不足或未处理的中断如未在向量表中定义PendSV_Handler解决检查scatter文件中RW_IRAM1大小是否足够本工程20KB足够在向量表中为所有未使用的中断添加B .无限循环占位符例如DCD B .避免跳转到非法地址5. 二次开发改造指南从功能扩展到生产环境适配的实战技巧5.1 新增UART打印功能寄存器级驱动的最小化实现要添加printf调试能力无需移植整个stdio只需实现fputc重定向。关键在于USART1的TX引脚PA9配置和发送等待; 在system_stm32f103.s中添加 USART1_Init PROC EXPORT USART1_Init ; 1. 使能USART1时钟APB2ENR[14]和GPIOA时钟APB2ENR[2] LDR R0, 0x40021018 LDR R1, [R0] ORR R1, R1, #0x00004004 ; bit14bit2 STR R1, [R0] ; 2. 配置PA9为复用推挽CRL[31:28] 0b1011 LDR R0, 0x40010800 LDR R1, [R0] BIC R1, R1, #0xF0000000 ORR R1, R1, #0xB0000000 STR R1, [R0] ; 3. 配置USART1_BRR 0x271115200bps 72MHz LDR R0, 0x40013808 ; USART1_BRR MOV R1, #0x00000271 STR R1, [R0] ; 4. 使能USART1CR1[13]1, CR1[2]1, CR1[12]1 LDR R0, 0x40013800 ; USART1_CR1 MOV R1, #0x0000200C ; UE1, TXEIE0, TE1 STR R1, [R0] BX LR ENDP ; 发送单字节阻塞式 USART1_Send PROC EXPORT USART1_Send PUSH {R0-R2} LDR R0, 0x40013800 ; USART1_CR1 LDR R1, [R0] TST R1, #0x00000040 ; 检查TXE标志 BEQ USART1_Send ; 未就绪则等待 LDR R0, 0x40013804 ; USART1_DR STR R2, [R0] ; R2为待发送字节 POP {R0-R2} BX LR ENDP ; 重定向fputc供Keil libc调用 IMPORT USART1_Send EXPORT fputc fputc PROC PUSH {R0-R2} MOV R2, R0 ; 字符存入R2 BL USART1_Send POP {R0-R2} BX LR ENDP注意fputc必须声明为EXPORT且函数名不可更改这是Keil libc的硬性约定。实际使用时在C文件中#include stdio.h后即可调用printf(Hello\n);底层自动走汇编发送逻辑。5.2 从Blue Pill迁移到更大容量芯片修改要点清单当项目升级到STM32F103VET6512KB Flash/64KB RAM时仅需三处修改修改位置Blue Pill (C8) 值VET6 值说明Scatter文件LR_IROM1大小0x00010000(64KB)0x00080000(512KB)Flash区域上限需匹配芯片规格Scatter文件RW_IRAM1大小0x00005000(20KB)0x00010000(64KB)RAM区域上限调整避免栈溢出Keil工程芯片选择STM32F103C8STM32F103VE影响启动文件和外设寄存器定义必须同步更新血泪经验曾有项目在C8上调试成功直接更换为VET6芯片后程序跑飞最终发现是RW_IRAM1大小未改导致全局变量覆盖了栈空间——从那以后我每次更换芯片型号都强制走一遍scatter文件参数核对表。5.3 生产环境加固添加看门狗与启动自检在量产固件中汇编层添加独立看门狗IWDG可防止死循环锁死。关键是要在main_entry开头立即启动并在主循环中定期喂狗; 在main.s开头添加 Init_IWDG PROC EXPORT Init_IWDG ; 1. 解锁IWDG写入向KR写0x5555 LDR R0, 0x40003000 ; IWDG_KR MOV R1, #0x5555 STR R1, [R0] ; 2. 写入预分频器PR0b010 → 64分频 MOV R1, #0x00000002 LDR R0, 0x40003004 ; IWDG_PR STR R1, [R0] ; 3. 写入重装载值RLR0xFFF → ~1.6s超时 MOV R1, #0x00000FFF LDR R0, 0x40003008 ; IWDG_RLR STR R1, [R0] ; 4. 启动IWDG向KR写0xCCCC MOV R1, #0xCCCC STR R1, [R0] BX LR ENDP ; 在主循环中喂狗必须在超时时间内执行 Feed_IWDG PROC EXPORT Feed_IWDG LDR R0, 0x40003000 MOV R1, #0xAAAA ; 重装载命令 STR R1, [R0] BX LR ENDP关键约束IWDG一旦启动无法关闭因此Init_IWDG必须在确认所有初始化成功后再调用喂狗间隔必须小于1.6s本例配置建议在主循环顶部插入BL Feed_IWDG并确保循环执行时间远小于超时值。这种底层看门狗比应用层软件定时器更可靠是工业设备固件的标配。希望帮到你。本文还有配套的精品资源点击获取