从0开始使用汇编实现stm32 LED闪烁工程
GaoSheng Lv5

最小STM32 LED闪烁系统详解(纯汇编实现)

本文会简要介绍如何用纯ARM汇编语言编写一个最小化的STM32 LED闪烁程序。展示了嵌入式系统的最基本结构,包括启动文件、链接脚本、主程序以及构建过程。

目录

  1. 项目概述
  2. 启动文件详解
  3. 主程序分析
  4. 链接脚本解析
  5. Makefile构建系统
  6. MAP文件解读
  7. 总结
    image

项目概述

这个项目实现了STM32F40上PB2引脚连接的LED灯的简单闪烁功能。整个程序完全使用ARM汇编语言编写,不依赖任何C库或其他高级语言组件。相信这个工程会让初学者对STM32会更加系统性的理解,对于老鸟说不定也会有所得。

启动文件详解

启动文件[startup.s]是嵌入式系统运行的第一个代码,负责初始化硬件环境并跳转到主程序。

向量表(Vector Table)

向量表是位于程序起始位置的一组指针,指向各种异常和中断处理程序:

1
2
3
4
5
6
7
8
9
.section .isr_vector
.align 2
.globl __isr_vector
__isr_vector:
.long __StackTop /* 栈顶指针 */
.long Reset_Handler /* 复位处理程序 */
.long NMI_Handler /* 不可屏蔽中断处理程序 */
.long HardFault_Handler /* 硬件故障处理程序 */
/* 其他异常处理程序... */

第一个条目__StackTop是栈顶地址,当处理器复位时,它会被加载到主堆栈指针(MSP)中。第二个条目Reset_Handler是复位后执行的第一个程序入口点。上面的.align 2 是指按 2^2 = 4 字节边界对齐

栈和堆定义

手动定义栈和堆空间:

1
2
3
4
5
6
7
8
9
/* 栈空间定义 */
.section .stack
.align 3
.equ Stack_Size, 0x400
.globl __StackTop
.globl __StackLimit
__StackLimit:
.space Stack_Size
__StackTop:

这里我们为栈分配了0x400(1KB)的空间。栈是从高地址向低地址增长的内存区域,主要用于存储函数调用时的返回地址、局部变量等。

复位程序

复位程序是系统启动后执行的第一个真正有意义的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.thumb_func
.align 2
.globl Reset_Handler
.type Reset_Handler, %function
Reset_Handler:
/* 将数据段初始值从Flash复制到SRAM */
movs r1, #0
b LoopCopyDataInit

CopyDataInit:
ldr r3, =_sidata
ldr r3, [r3, r1]
str r3, [r0, r1]
adds r1, r1, #4

LoopCopyDataInit:
ldr r0, =_sdata
ldr r3, =_edata
adds r2, r0, r1
cmp r2, r3
bcc CopyDataInit
ldr r2, =_sbss
b LoopFillZerobss

/* 将bss段清零 */
FillZerobss:
movs r3, #0
str r3, [r2], #4

LoopFillZerobss:
ldr r3, = _ebss
cmp r2, r3
bcc FillZerobss

/* 调用主函数 */
bl main
bx lr

复位处理程序的主要任务包括:

  1. 初始化数据段:将已初始化的全局变量从Flash复制到RAM
  2. 清零bss段:将未初始化的全局变量所在的内存区域清零
  3. 调用主函数[main]

注意这里的.thumb_func指令,它告诉汇编器该函数使用Thumb指令集。STM32 Cortex-M系列处理器主要使用Thumb指令集以提高代码密度。
简要介绍一下上面用到的汇编指令:

  • movs r1, #0:将立即数0移动到寄存器r1中,并更新状态标志位
  • ldr r3, =_sidata:将符号_sidata的地址加载到寄存器r3中
  • ldr r3, [r3, r1]:从内存地址(r3 + r1)处加载数据到寄存器r3
  • str r3, [r0, r1]:将寄存器r3中的值存储到内存地址(r0 + r1)处
  • str r3, [r2], #4:将寄存器r3中的值存储到地址r2处,然后将r2增加4

主程序分析

主程序文件[main.s]实现了LED控制逻辑:

寄存器地址定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 定义GPIO寄存器地址 */
.equ GPIOB_BASE, 0x40020400
.equ GPIOB_MODER, GPIOB_BASE + 0x00
.equ GPIOB_OTYPER, GPIOB_BASE + 0x04
.equ GPIOB_OSPEEDR, GPIOB_BASE + 0x08
.equ GPIOB_PUPDR, GPIOB_BASE + 0x0C
.equ GPIOB_IDR, GPIOB_BASE + 0x10
.equ GPIOB_ODR, GPIOB_BASE + 0x14
.equ GPIOB_BSRR, GPIOB_BASE + 0x18
.equ GPIOB_LCKR, GPIOB_BASE + 0x1C
.equ GPIOB_AFRL, GPIOB_BASE + 0x20
.equ GPIOB_AFRH, GPIOB_BASE + 0x24

/* 定义RCC寄存器地址 */
.equ RCC_BASE, 0x40023800
.equ RCC_AHB1ENR, RCC_BASE + 0x30

这些地址定义对应STM32F407参考手册中的外设寄存器地址。通过直接操作这些寄存器,可以控制GPIO引脚的行为。

主函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
main:
/* 使能GPIOB时钟 */
ldr r0, =RCC_AHB1ENR
ldr r1, [r0]
orr r1, r1, #(1<<1) /* 使能GPIOB时钟(第1位) */
str r1, [r0]

/* 配置PB2为输出模式 */
ldr r0, =GPIOB_MODER
ldr r1, [r0]
bic r1, r1, #(0x3 << (LED_PIN * 2)) /* 清除PB2的模式位 */
orr r1, r1, #(0x1 << (LED_PIN * 2)) /* 设置为输出模式 */
str r1, [r0]

主函数首先使能GPIOB端口的时钟,因为STM32的所有外设都需要先使能时钟才能工作。然后配置PB2引脚为输出模式。可能你注意到了这里只配置了GPIO的时钟,并没有配置MCU的系统时钟,通过查阅F407的用户手册可以看到,HSION位在上电后默认开启。为了工程简单,使用默认的内部快速时钟即可。
里面有用到汇编指令:

  • orr r1, r1, #(1<<1):将寄存器 r1 与立即数 (1<<1) 进行按位或运算,结果存储在 r1 中,用于设置特定位(使能 GPIOB 时钟)
  • bic r1, r1, #(0x3 << (LED_PIN * 2)):将寄存器 r1 与立即数 (0x3 << (LED_PIN * 2)) 进行按位清除运算,用于清除 PB2 的模式位

LED控制循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
loop:
/* 点亮LED */
ldr r0, =GPIOB_BSRR
mov r1, #LED_PIN_MASK
str r1, [r0]

/* 简单延时 */
bl delay

/* 熄灭LED */
ldr r0, =GPIOB_BSRR
mov r1, #(LED_PIN_MASK << 16)
str r1, [r0]

/* 简单延时 */
bl delay

/* 循环 */
b loop

在这个循环中,我们通过操作BSRR(Bits Set/Reset Register)寄存器来控制LED的亮灭。当写入低16位时,对应的引脚被置位(点亮);当写入高16位时,对应的引脚被复位(熄灭)。

延时函数

1
2
3
4
5
6
7
8
9
10
11
12
/* 延时函数 */
delay:
push {r4, lr}
mov r4, #0xFF
delay_outer:
mov r3, #0xFFF
delay_inner:
subs r3, r3, #1
bne delay_inner
subs r4, r4, #1
bne delay_outer
pop {r4, pc}

一个软件延时函数,通过嵌套循环消耗时间。

链接脚本解析

链接脚本[linker.ld]定义了程序在内存中的布局:

1
2
3
4
5
6
7
ENTRY(Reset_Handler)

MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

ENTRY(Reset_Handler)指定了程序的入口点是[Reset_Handler]函数。

MEMORY部分定义了STM32F407的内存布局:

  • Flash存储器从0x08000000开始,大小为512KB,具有读(read)和执行(execute)属性
  • RAM从0x20000000开始,大小为128KB,具有读(read)、写(write)和执行(execute)属性

段定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
SECTIONS
{
.text :
{
KEEP(*(.isr_vector))
*(.text*)
*(.rodata*)
_etext = .;
} > FLASH

_sidata = .;

.data : AT (_sidata)
{
_sdata = .;
*(.data*)
_edata = .;
} > RAM

各段的作用:

  • .text段包含程序代码和只读数据,存储在Flash中
  • .data段包含已初始化的全局变量,在Flash中存储但在RAM中运行
  • .bss段包含未初始化的全局变量,全部清零后使用

KEEP(*(.isr_vector))确保向量表不会被链接器优化掉。

Makefile构建系统

我们的Makefile配置了完整的构建流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 编译器和工具
CC = arm-none-eabi-gcc
AS = arm-none-eabi-as
LD = arm-none-eabi-ld
OBJCOPY = arm-none-eabi-objcopy
SIZE = arm-none-eabi-size

# 目标名称
TARGET = stm32f407_blinky

# 源文件
ASM_SOURCES = startup.s main.s

# 链接脚本
LINKER_SCRIPT = linker.ld

# 对象文件
OBJECTS = $(ASM_SOURCES:.s=.o)

# 标志
ASFLAGS = -mcpu=cortex-m4 -mthumb
LDFLAGS = -T$(LINKER_SCRIPT) -Wl,-Map=$(TARGET).map
CPFLAGS = -O binary

# 默认目标
all: $(TARGET).bin $(TARGET).hex $(TARGET).map

关键点解释:

  • 使用ARM GCC工具链进行编译
  • -mcpu=cortex-m4 -mthumb指定目标处理器和指令集
  • -T$(LINKER_SCRIPT) -Wl,-Map=$(TARGET).map指定链接脚本并生成MAP文件
  • 生成多种格式的输出文件:ELF、BIN、HEX

MAP文件解读

MAP文件[stm32f407_blinky.map]提供了详细的内存映射信息:

1
2
3
4
5
6
.text           0x08000000      0x124
*(.isr_vector)
.isr_vector 0x08000000 0x8c startup.o
*(.text*)
.text 0x0800008c 0x44 startup.o
.text 0x080000d0 0x54 main.o

从MAP文件可以看出:

  • 整个程序的代码段只有0x124(292字节),非常小巧
  • 向量表占用了0x8c(140字节)
  • 启动代码占用了0x44(68字节)
  • 主程序占用了0x54(84字节)

这充分体现了纯汇编编程的高效性,相比C语言实现可以显著减小程序体积。

1
2
.stack          0x20000000      0x400
.heap 0x20000000 0x200

栈和堆都在RAM中分配,符合嵌入式系统的内存管理方式。

总结

通过这个最小化的STM32 LED闪烁项目,我们可以知道STM32工程的基本构成:

  1. 启动文件负责系统初始化和跳转到主程序
  2. 主程序实现具体的业务逻辑
  3. 链接脚本定义程序在内存中的布局
  4. Makefile自动化构建过程
  5. MAP文件提供内存使用的详细信息
本站由 提供部署服务