Udemy-从零开始的-ARM-笔记-全-
Udemy 从零开始的 ARM 笔记(全)
001:ARM Cortex-M通用输入输出(GPIO)模块概述 🚀
在本节课中,我们将要学习ARM Cortex-M微控制器中通用输入输出(GPIO)模块的基础知识。我们将了解微控制器如何与外部设备通信,以及其内部总线结构如何影响我们对GPIO的访问方式。
微控制器中的输入输出类型
上一节我们介绍了内存与CPU的关系,本节中我们来看看CPU如何与外部世界交互。在微控制器中,输入输出端口是CPU访问输入和输出设备的桥梁。
微控制器中存在两种类型的输入输出:
- 通用输入输出:通常缩写为GPIO。这些端口用于连接通用设备。
- 特殊功能输入输出:这些端口具有指定的专用功能。
以下是两种类型的具体应用场景:
- GPIO 用于连接诸如LED、开关、LCD键盘、直流电机等通用设备。
- 特殊功能IO 则用于实现特定功能,例如模数转换、数模转换、定时器操作以及通用异步收发传输器。
在微控制器中,同一个物理引脚可以被配置为GPIO或特殊功能IO。我们需要明确告知微控制器,我们希望将该引脚用于特殊功能,还是作为普通的通用引脚。
端口与引脚命名规则
在微控制器中,引脚被分组到不同的端口,例如端口A、端口B、端口C等。每个端口包含一定数量的引脚。
引脚命名遵循“端口名 + 引脚号”的规则。例如:
PA1代表端口A的第1号引脚。PE3代表端口E的第3号引脚。
因此,在访问这些引脚时,我们需要向开发环境指明具体的端口和引脚编号。所有微控制器都遵循这一命名惯例。

然而,当使用如Arduino或Mbed这类高级平台时,你通常不需要直接指定端口。这些平台的封装层已经处理了这些细节,并将引脚重命名为更简单的名称,例如P1到P30,而无需指明它们属于哪个端口。在Arduino或Mbed中编码时,你无需担心这些底层细节。
但当你进行裸机编程时,你必须清楚你想要访问的特定引脚所属的端口及其编号。

微控制器总线结构:AHB与APB
大多数Cortex-M微控制器拥有两种类型的总线:
- 高级外设总线:通常写作 APB。
- 高级高性能总线:通常写作 AHB。

这两种总线在访问速度上存在差异:
- 使用APB访问一个外设至少需要两个时钟周期。
- 使用AHB访问外设则可能只需要一个时钟周期。
我们将查看来自三家不同厂商的四款ARM Cortex微控制器的数据手册,你会发现这些总线结构普遍存在。无论微控制器来自NXP、德州仪器还是意法半导体,它们都具备这些总线。
实践:在数据手册中定位总线结构
本课程的一个目标是使你能够独立查阅任何微控制器的数据手册和用户手册,以便在使用外设和解决实际问题时,能够随着嵌入式开发技能的提升而自主工作。
现在,让我们看看如何找到特定微控制器的数据手册,并查看其总线结构。

1. 意法半导体 STM32F4系列
我们以STM32F411RE微控制器为例。在搜索引擎中搜索“STM32F411RE datasheet”并打开官方数据手册。
数据手册提供了微控制器的概要信息,包括外设数量、功耗限制和框图。你可以将数据手册视为摘要,而参考手册则包含更详细的数千页内容。
在数据手册中向下滚动,找到系统框图。图中清晰标出了AHB总线和APB总线(如APB1、APB2)。例如,GPIO端口A、B、C、D、H可以连接到AHB总线,而某些外设(如UART)则只连接到APB总线。
在编程时,我们需要牢记这一点。例如,我们不能尝试使用AHB总线去初始化一个只支持APB总线的UART2外设。
2. 德州仪器 TM4C123系列
接下来,搜索“TM4C123GH6PM datasheet”并打开其数据手册。通过查找“框图”或使用Ctrl+F搜索关键词,可以快速定位。
在TM4C123的框图中,我们同样可以看到AHB总线和APB总线。只有那些有箭头指向AHB总线的外设才能使用它。例如,SSI(同步串行接口)外设的箭头指向APB,因此它只能使用APB总线。而DMA(直接内存访问)控制器则能够同时使用AHB和APB总线。
3. NXP LPC1768系列
最后,搜索“LPC1768 user manual”并打开用户手册。同样,通过搜索“block diagram”来查找简化框图。

在LPC1768的框图中,我们可以看到其总线系统,包括AHB矩阵和AHB到APB的桥接器。一部分外设作为“AHB从机组1”连接到AHB总线,另一部分外设则通过桥接器连接到APB总线。
这体现了作为嵌入式系统开发者应具备的能力:你应该能够使用提供给你的任何微控制器。许多人停留在舒适区,认为从一家厂商(如ST)切换到另一家会非常困难。但事实并非如此,如果你掌握了查阅数据手册和用户手册的方法,你就能同样自如地使用不同芯片制造商的产品。
这些总线结构是ARM Cortex-M处理器的核心标准,存在于所有Cortex-M处理器中。
总结

本节课中我们一起学习了ARM Cortex-M微控制器GPIO模块的基础。我们明确了GPIO与特殊功能IO的区别,理解了端口和引脚的命名规则。更重要的是,我们探讨了微控制器内部的AHB和APB总线结构,并通过实际查阅三家不同厂商(ST、TI、NXP)微控制器的数据手册,验证了这一通用架构的存在。掌握查阅官方文档的技能,是迈向独立嵌入式开发的关键一步。
002:为相关GPIO寄存器分配符号名称 🔧







在本节课中,我们将学习如何使用汇编语言为STM32微控制器编写一个GPIO输出驱动程序。我们将从创建一个新项目开始,并逐步了解控制GPIO引脚所需的关键步骤和寄存器。
项目创建与环境设置
首先,我们需要在Keil MDK中创建一个新的汇编项目。
以下是创建新项目的步骤:
- 点击菜单栏的“Project”,选择“New uVision Project”。
- 选择一个文件夹来存放项目,并将项目命名为“GPIO_Output”。
- 在弹出的设备选择窗口中,选择我们的目标芯片:STM32F411VETx。
- 在“Manage Run-Time Environment”窗口中,选择“CMSIS”下的“CORE”和“Device”下的“Startup”,然后点击“OK”。
- 进入“Options for Target”设置,将“Xtal (MHz)”设置为16。
- 在“Debug”选项卡中,选择“ST-Link Debugger”作为调试器。
- 在“Flash Download”选项卡中,勾选“Reset and Run”选项。
- 回到项目窗口,右键点击“Source Group 1”,选择“Add New Item”。
- 选择“Asm File (.s)”,并将其命名为“main.s”。

至此,我们的项目框架已经搭建完成。

GPIO控制的基本原理
上一节我们设置了开发环境,本节中我们来看看控制GPIO输出的核心步骤。无论使用哪种微控制器,要使用一个外设(例如GPIO),通常都需要遵循三个基本步骤。


以下是控制GPIO输出的三个核心步骤:
- 启用外设时钟访问:现代微控制器采用时钟门控机制以降低功耗。默认情况下,所有外设的时钟都是关闭的,我们需要手动打开特定外设(如GPIOA)的时钟门。
- 配置引脚模式:每个GPIO引脚都可以被配置为输入或输出模式。我们需要通过特定的“方向寄存器”(在STM32中称为“模式寄存器”)来设置引脚的工作模式。
- 读写数据:配置好引脚模式后,我们可以通过“数据寄存器”向输出引脚写入数据(例如,置高或拉低),或从输入引脚读取数据。



查阅技术文档
为了完成上述步骤,我们需要从STM32的技术文档中找到相关寄存器的地址和配置信息。主要需要两份文档:数据手册和参考手册。
以下是需要查阅的关键信息:
- 数据手册:用于查找微控制器的内存映射,即各个外设模块的基地址。
- 参考手册:用于查找特定外设(如GPIO、RCC)内部各个寄存器的详细定义和偏移地址。
首先,我们打开数据手册,查找GPIOA和RCC(复位和时钟控制)模块的基地址。
- GPIOA基地址:
0x4002 0000 - RCC基地址:
0x4002 3800



接下来,我们需要了解GPIOA连接在哪个总线上。通过查看数据手册中的框图,可以发现GPIOA连接到AHB1总线上。AHB总线比APB总线速度更快,这意味着我们需要通过AHB1总线来启用GPIOA的时钟。

定义关键寄存器与符号

有了基地址,我们就可以在参考手册中查找具体寄存器的偏移量,并为其定义易于理解的符号名称,这将极大方便后续的汇编编程。
上一节我们找到了模块的基地址,本节中我们来看看如何找到并定义具体操作的寄存器。
1. 启用时钟:RCC AHB1使能寄存器
要启用GPIOA的时钟,我们需要操作RCC模块下的AHB1外设时钟使能寄存器。
- 寄存器名称:
RCC_AHB1ENR - 偏移地址:
0x30 - 功能:该寄存器的第0位(
GPIOAEN)控制GPIOA的时钟。置1为启用,置0为禁用。 - 符号定义:我们定义一个符号,表示将1左移0位,用于稍后设置该位。
GPIOA_EN EQU (1 << 0) ; 启用GPIOA时钟的掩码
2. 配置引脚模式:GPIO模式寄存器
我们需要将PA5引脚设置为输出模式。在STM32中,每个引脚由模式寄存器中的2个位控制。
- 寄存器名称:
GPIOx_MODER(x代表A、B、C...) - 偏移地址:
0x00 - 配置PA5:PA5对应模式寄存器中的第10和11位(
MODER5)。设置为01表示通用输出模式。 - 符号定义:我们定义一个符号,将1左移10位,用于设置PA5为输出。
MODER5_OUT EQU (1 << 10) ; 设置PA5为输出模式的掩码
3. 控制输出:GPIO输出数据寄存器
最后,我们需要通过输出数据寄存器来控制PA5引脚的电平。
- 寄存器名称:
GPIOx_ODR - 偏移地址:
0x14 - 控制PA5:该寄存器的第5位(
ODR5)对应PA5的输出电平。置1为高电平,置0为低电平。 - 符号定义:我们定义一个符号,将1左移5位,用于点亮LED(假设LED连接在PA5且低电平点亮)。
LED_ON EQU (1 << 5) ; 设置PA5输出高电平的掩码
总结与下节预告
本节课中我们一起学习了为STM32编写GPIO输出驱动程序的准备工作。我们创建了项目,理解了控制GPIO的三个核心步骤(启用时钟、配置模式、写入数据),并通过查阅数据手册和参考手册,找到了关键寄存器(RCC_AHB1ENR、GPIOA_MODER、GPIOA_ODR)的地址,并为它们定义了清晰的符号名称。



现在,我们已经拥有了所有必要的“地图”和“工具”。在下一节课中,我们将正式开始编写汇编代码,利用这些符号地址来实际操作寄存器,最终实现LED的闪烁控制。
003:编写 GPIO 输出驱动



在本节课中,我们将学习如何为 STM32 微控制器编写一个基本的 GPIO 输出驱动程序。我们将使用汇编语言来操作寄存器,从而控制连接到 GPIO 引脚上的 LED。我们将从定义寄存器的符号地址开始,逐步编写初始化 GPIO 和点亮 LED 的代码。
定义寄存器符号地址
上一节我们了解了 GPIO 的基本原理,本节中我们来看看如何用汇编语言操作具体的寄存器。首先,我们需要为相关的内存地址创建易于理解的符号名称。
以下是定义寄存器基地址和偏移量的步骤:
-
定义 RCC 基地址:复位和时钟控制单元的基地址是
0x40023800。RCC_BASE EQU 0x40023800 -
定义 AHB1 外设时钟使能寄存器偏移量:该寄存器相对于 RCC 基地址的偏移是
0x30。AHB1ENR_OFFSET EQU 0x30 -
构造完整的 AHB1 使能寄存器地址:将基地址与偏移量相加,得到寄存器的完整地址。
RCC_AHB1ENR EQU RCC_BASE + AHB1ENR_OFFSET在 C 语言中,这等价于访问结构体成员
RCC->AHB1ENR。

-
定义 GPIOA 基地址:GPIO 端口 A 的基地址是
0x40020000。GPIOA_BASE EQU 0x40020000 -
定义模式寄存器偏移量:模式寄存器位于端口基地址的
0x00偏移处。MODER_OFFSET EQU 0x00


- 构造完整的 GPIOA 模式寄存器地址:
GPIOA_MODER EQU GPIOA_BASE + MODER_OFFSET

-
定义输出数据寄存器偏移量:输出数据寄存器的偏移量是
0x14。ODR_OFFSET EQU 0x14 -
构造完整的 GPIOA 输出数据寄存器地址:
GPIOA_ODR EQU GPIOA_BASE + ODR_OFFSET
定义配置常量

除了地址,我们还需要定义配置引脚时用到的位掩码常量。
以下是需要定义的常量:

- GPIOA 时钟使能位:在
RCC_AHB1ENR寄存器中,使能 GPIOA 的位是第 0 位(1 << 0)。GPIOA_EN EQU (1 << 0) - 引脚模式配置:要将 PA5 配置为输出模式,需要在
GPIOA_MODER寄存器的第 10 和 11 位写入01(即1 << 10)。MODE5_OUT EQU (1 << 10) - 引脚输出电平:要设置 PA5 输出高电平以点亮 LED,需要设置
GPIOA_ODR寄存器的第 5 位(1 << 5)。LED_ON EQU (1 << 5)

编写初始化代码

现在,我们可以开始编写实际的汇编代码来初始化外设并控制 LED。

代码区域和入口点定义如下:
AREA |.text|, CODE, READONLY, ALIGN=2
THUMB
EXPORT __main
__main 标签是程序的入口点。
GPIO 初始化子程序


我们创建一个名为 GPIOA_Init 的子程序来执行所有初始化操作。
以下是初始化 GPIOA 的步骤:
-
使能 GPIOA 时钟:
- 将
RCC_AHB1ENR的地址加载到寄存器 R0。 - 读取该地址的当前值到 R1。
- 将
GPIOA_EN掩码与 R1 的值进行或操作,以在不影响其他位的情况下使能 GPIOA 时钟。 - 将结果写回
RCC_AHB1ENR。
; C 代码: RCC->AHB1ENR |= GPIOA_EN; LDR R0, =RCC_AHB1ENR ; 加载 RCC_AHB1ENR 地址到 R0 LDR R1, [R0] ; 读取地址中的值到 R1 ORR R1, #GPIOA_EN ; 将 GPIOA 使能位设为 1 STR R1, [R0] ; 将新值存回 RCC_AHB1ENR - 将
-
配置 PA5 为输出模式:
- 将
GPIOA_MODER的地址加载到寄存器 R0。 - 读取当前值到 R1。
- 将
MODE5_OUT掩码与 R1 的值进行或操作,以设置 PA5 为输出模式。 - 将结果写回
GPIOA_MODER。
; C 代码: GPIOA->MODER |= MODE5_OUT; LDR R0, =GPIOA_MODER ; 加载 GPIOA_MODER 地址到 R0 LDR R1, [R0] ; 读取地址中的值到 R1 ORR R1, #MODE5_OUT ; 设置 PA5 模式位为输出 STR R1, [R0] ; 将新值存回 GPIOA_MODER - 将



- 设置 PA5 输出高电平(点亮 LED):
- 将
GPIOA_ODR的地址加载到寄存器 R0。 - 将
LED_ON的值加载到寄存器 R1。 - 将 R1 的值写入
GPIOA_ODR,使 PA5 输出高电平。
子程序最后使用; C 代码: GPIOA->ODR = LED_ON; LDR R0, =GPIOA_ODR ; 加载 GPIOA_ODR 地址到 R0 LDR R1, =LED_ON ; 加载 LED_ON 值到 R1 STR R1, [R0] ; 将值写入 ODR 以点亮 LEDBX LR指令返回。 - 将


主程序循环
主程序 __main 首先跳转到 GPIOA_Init 子程序进行初始化,然后进入一个无限循环。


__main
BL GPIOA_Init ; 调用初始化子程序
Loop
B Loop ; 无限循环,保持程序运行
ALIGN
END


程序验证


将代码编译并下载到 STM32 开发板后,连接到 PA5 引脚的 LED 应该被点亮。这证明我们成功使用汇编语言编写了 GPIO 输出驱动程序。



本节课中我们一起学习了如何为 STM32 的 GPIO 编写汇编驱动。我们从定义寄存器的符号地址和配置常量开始,然后逐步编写了使能时钟、配置引脚模式和设置输出电平的代码。通过这个实践,我们掌握了使用汇编语言直接操作硬件寄存器的基本方法。
004:控制GPIO输出 🔄


在本节课中,我们将学习如何编写ARM汇编代码,使连接到微控制器的LED灯闪烁。我们将创建一个简单的延时函数,并利用循环结构来控制LED的亮灭周期。
上一节我们介绍了如何初始化GPIO并点亮LED。本节中我们来看看如何通过编程让LED闪烁起来。
首先,创建一个新项目。我们将复制上一节的项目,并在此基础上添加闪烁功能。
以下是创建新项目并准备代码的步骤:
- 复制上一个项目,并将其命名为“blinky2”。
- 打开新项目,并准备编写代码。
接下来,我们需要创建一个延时函数。这个函数将帮助我们控制LED亮和灭的时间间隔。
.equ ONE_SEC, 0x00F42400 // 基于16MHz时钟频率估算的1秒延时常数
.equ HALF_SEC, 0x007A1200 // 半秒延时常数
现在,我们编写延时子程序。该程序通过递减一个寄存器的值来实现循环延时。
delay:
SUBS R3, R3, #1 // R3 = R3 - 1,并更新状态标志
BNE delay // 如果结果不为零(Z标志为0),则跳回‘delay’标签处循环
BX LR // 返回调用处
这个子程序的核心是一个循环。它将传入R3寄存器的常数不断减1,直到结果为0(此时Z标志置1),循环才结束,从而模拟了一段延时。
有了延时函数,我们就可以编写控制LED闪烁的主逻辑了。首先,我们需要定义LED打开和关闭对应的位模式。
.equ LED_ON, (1 << 5) // 将1左移5位,对应GPIO的第5个引脚置高
.equ LED_OFF, (0 << 5) // 将0左移5位,对应GPIO的第5个引脚置低
以下是实现LED闪烁的完整子程序步骤:
- 将
LED_ON的值加载到寄存器,并写入GPIO输出数据寄存器以点亮LED。 - 调用延时函数,等待半秒。
- 将
LED_OFF的值加载到寄存器,并写入GPIO输出数据寄存器以熄灭LED。 - 再次调用延时函数,等待半秒。
- 使用分支指令跳回步骤1,形成无限循环。
blink:
LDR R2, =GPIOA_ODR_ADDR // 将GPIOA输出数据寄存器的地址加载到R2
MOV R1, #LED_ON // 步骤1:准备点亮LED的值
STR R1, [R2] // 点亮LED
LDR R3, =HALF_SEC // 步骤2:设置延时半秒
BL delay // 调用延时函数
MOV R1, #LED_OFF // 步骤3:准备熄灭LED的值
STR R1, [R2] // 熄灭LED
LDR R3, =HALF_SEC // 步骤4:再次设置延时半秒
BL delay // 调用延时函数
B blink // 步骤5:跳回开头,形成循环
将代码编译并下载到开发板后,可以观察到LED以大约1秒的周期(亮半秒、灭半秒)进行闪烁。当前的延时是基于时钟频率的估算值。在后续课程中,我们将学习如何使用硬件定时器来获得更精确的延时。

本节课中我们一起学习了如何用ARM汇编语言实现GPIO输出控制,并创建了一个让LED闪烁的程序。我们掌握了通过软件循环实现延时的方法,并利用分支指令构建了循环逻辑。在下一课,我们将探索如何使用STM32的BSRR寄存器来更高效地控制GPIO输出。
005:使用BSRR寄存器控制GPIO输出
在本节课中,我们将学习如何使用STM32微控制器上的BSRR寄存器来控制GPIO引脚的输出状态。我们将通过修改之前的“闪烁LED”项目,用BSRR寄存器替代ODR寄存器来实现相同的功能。
概述

上一节我们介绍了如何使用输出数据寄存器(ODR)来控制GPIO引脚。本节中,我们来看看另一种更高效的方法——使用位设置/复位寄存器(BSRR)。BSRR寄存器允许我们独立地设置或清除特定的引脚,而无需读取-修改-写入整个端口。

BSRR寄存器详解

首先,我们需要了解BSRR寄存器的工作原理。根据STM32参考手册,BSRR是一个32位寄存器。

- 低16位(位0-15):用于设置引脚(置为高电平)。向这些位写入
1会设置对应的引脚,写入0则无影响。 - 高16位(位16-31):用于复位引脚(置为低电平)。向这些位写入
1会清除对应的引脚,写入0则无影响。
其操作逻辑可以用以下伪代码描述:
// 设置Pin N为高电平
BSRR = (1 << N);
// 设置Pin N为低电平
BSRR = (1 << (16 + N));
在我们的项目中,LED连接在GPIOA的Pin 5上。因此:
- 要点亮LED,需要向BSRR寄存器的位5写入
1。 - 要熄灭LED,需要向BSRR寄存器的位21(即16+5)写入
1。
BSRR寄存器的偏移地址是0x18。
修改汇编代码
以下是修改项目代码的具体步骤。我们将定义寄存器地址和操作常量,然后替换原有的ODR操作。
步骤1:定义BSRR寄存器地址
首先,在代码中定义GPIOA端口BSRR寄存器的地址。
GPIOA_BSRR_OFFSET EQU 0x18
GPIOA_BSRR EQU GPIOA_BASE + GPIOA_BSRR_OFFSET
步骤2:定义引脚操作常量
接着,定义用于设置和复位Pin 5的常量值。
BSRR_PIN5_SET EQU (1 << 5) ; 设置Pin 5为高电平
BSRR_PIN5_RESET EQU (1 << 21) ; 设置Pin 5为低电平
步骤3:替换输出操作
最后,在程序的主循环中,将原本对GPIOA_ODR的操作替换为对GPIOA_BSRR的操作。
; 点亮LED
LDR R1, =GPIOA_BSRR
LDR R2, =BSRR_PIN5_SET
STR R2, [R1]
; ... 延时 ...
; 熄灭LED
LDR R1, =GPIOA_BSRR
LDR R2, =BSRR_PIN5_RESET
STR R2, [R1]
; ... 延时 ...
代码对比与优势

与使用ODR寄存器相比,使用BSRR寄存器有两个主要优点:
- 原子操作:设置或清除单个引脚时,无需先读取整个端口的状态,避免了在多任务或中断环境中可能出现的竞态条件。
- 独立性:可以同时设置和清除不同的引脚,而不会相互干扰。

总结

本节课中我们一起学习了如何使用STM32的BSRR寄存器来控制GPIO输出。我们了解了该寄存器的结构,掌握了通过向特定位写入1来独立设置或复位引脚的方法,并成功修改了汇编代码,使LED能够像之前一样闪烁。BSRR寄存器是进行GPIO位操作更安全、更高效的选择。
006:GPIO输入驱动开发(第一部分)🚀

在本节课中,我们将学习如何配置STM32微控制器的GPIO引脚作为输入。我们将以开发板上的一个按键(连接至PC13引脚)为例,逐步讲解如何初始化GPIO端口、配置输入模式,并理解相关的寄存器操作。


概述

上一节我们介绍了如何配置GPIO输出以控制LED。本节中,我们来看看如何配置GPIO输入来读取外部按键的状态。我们将创建一个新的工程,初始化GPIO端口,并设置PC13引脚为输入模式,为后续读取按键值做好准备。
创建新工程
首先,我们需要在Keil uVision中创建一个新项目。
- 打开Keil uVision,选择“Project” -> “New uVision Project”。
- 创建一个新文件夹,命名为“GPIO_input”。
- 将项目也命名为“GPIO_input”。
- 在设备选择窗口中,选择“STM32F411VETx”。


- 在接下来的对话框中,选择“CMSIS”下的“CORE”以及“Device”下的“Startup”。然后点击“OK”。


添加并配置源文件
接下来,我们需要添加一个汇编源文件。
- 在“Target 1”上右键,选择“Manage Project Items”。
- 将“Source Group 1”重命名为“App”。
- 在“App”组下,点击“Add New Item”。
- 选择“Asm File (.s)”,并命名为“main”,然后点击“Add”。

- 配置调试选项:点击工具栏的“Options for Target”按钮(或按Alt+F7)。
- 在“Debug”选项卡中,选择“Use: ST-Link Debugger”。
- 在“Utilities”选项卡中,确保“Use Target Driver for Flash Programming”也选择了“ST-Link Debugger”,然后点击“OK”。

硬件连接与原理
在我们的开发板上,按键(Push Button)连接到了PC13引脚。因此,我们需要将PC13配置为GPIO输入引脚。
这个按键是低电平有效的。这意味着:
- 当按键未被按下时,PC13引脚读到的逻辑电平为1(高电平)。
- 当按键被按下时,PC13引脚读到的逻辑电平为0(低电平)。
用公式表示这个关系:
按键状态 = !(PC13引脚电平)
当 PC13 = 1 时,按键状态 = 0 (关闭)
当 PC13 = 0 时,按键状态 = 1 (按下)
定义寄存器地址与常量
在编写代码前,我们需要查阅数据手册和参考手册,找到相关寄存器的地址和偏移量。
以下是需要定义的关键地址和常量:
; 端口C的基地址
GPIOC_BASE EQU 0x40020800
; 模式寄存器的偏移量 (对所有端口相同)
GPIOx_MODER_OFFSET EQU 0x00
; 为端口C定义模式寄存器地址
GPIOC_MODER EQU GPIOC_BASE + GPIOx_MODER_OFFSET
; 输入数据寄存器(IDR)的偏移量
GPIOx_IDR_OFFSET EQU 0x10
; 为端口C定义输入数据寄存器地址
GPIOC_IDR EQU GPIOC_BASE + GPIOx_IDR_OFFSET
; RCC AHB1外设时钟使能寄存器地址
RCC_AHB1ENR EQU 0x40023830
; 使能GPIOC的位掩码 (AHB1ENR寄存器的位2)
GPIOC_ENABLE EQU (1 << 2)
; 按键引脚定义 (PC13)
BTN_PIN EQU (1 << 13)
; 按键状态定义
BTN_ON EQU 0x0000 ; 按键按下时,PC13读为0
BTN_OFF EQU 0x2000 ; 按键释放时,PC13读为1 (1<<13 = 0x2000)
代码解释:
EQU指令用于定义常量,提高代码可读性。- 端口C的基地址
0x40020800来自数据手册。 - 输入数据寄存器
IDR的偏移量0x10来自参考手册。 BTN_PIN的值(1 << 13)表示二进制的第13位为1,对应PC13引脚。BTN_OFF的值0x2000是1<<13的十六进制表示,同样代表PC13。
编写初始化子程序
现在,我们将编写一个子程序GPIO_Init,用于初始化LED所用的GPIOA和按键所用的GPIOC。
以下是初始化过程的步骤:
AREA |.text|, CODE, READONLY, ALIGN=2
THUMB
ENTRY
__main
B GPIO_Init ; 跳转到初始化子程序
GPIO_Init
; 1. 使能GPIOA的时钟 (用于LED)
LDR R0, =RCC_AHB1ENR ; 加载RCC_AHB1ENR寄存器地址到R0
LDR R1, [R0] ; 读取当前寄存器值到R1
ORR R1, #1 ; 设置位0 (GPIOAEN) 为1
STR R1, [R0] ; 将新值写回寄存器
; 2. 配置GPIOA引脚为输出模式 (以PA5为例)
LDR R0, =GPIOA_MODER ; 加载GPIOA模式寄存器地址
LDR R1, [R0] ; 读取当前值
LDR R2, =0x00000400 ; 掩码:清除PA5的原有模式位 (位11:10)
BIC R1, R1, R2 ; 清除位
LDR R2, =0x00000400 ; 掩码:设置PA5为通用输出模式 (01)
ORR R1, R1, R2 ; 设置位
STR R1, [R0] ; 写回新配置
; 3. 使能GPIOC的时钟 (用于按键PC13)
LDR R0, =RCC_AHB1ENR ; 再次加载RCC_AHB1ENR地址
LDR R1, [R0] ; 读取当前值
LDR R2, =GPIOC_ENABLE; 加载GPIOC使能掩码 (1<<2)
ORR R1, R1, R2 ; 设置位2 (GPIOCEN) 为1
STR R1, [R0] ; 写回新值
; 4. 配置GPIOC的PC13引脚为输入模式
LDR R0, =GPIOC_MODER ; 加载GPIOC模式寄存器地址
LDR R1, [R0] ; 读取当前值
LDR R2, =0x0C000000 ; 掩码:清除PC13的原有模式位 (位27:26)
BIC R1, R1, R2 ; 清除位 (00 表示输入模式)
STR R1, [R0] ; 写回新配置,PC13现已配置为输入
BX LR ; 从子程序返回
ALIGN
END
代码流程解析:
- 使能时钟:通过设置
RCC_AHB1ENR寄存器的对应位,来打开GPIOA和GPIOC的时钟信号。没有时钟,外设无法工作。 - 配置模式寄存器:
- 对于输出引脚(如PA5),需要将其模式位设置为
01(通用输出模式)。 - 对于输入引脚(如PC13),需要将其模式位设置为
00(输入模式)。这是复位后的默认值,但显式设置是一个好习惯。
- 对于输出引脚(如PA5),需要将其模式位设置为
- 使用
BIC和ORR指令:在修改寄存器特定位时,通常先使用BIC(位清除)指令清除旧配置,再用ORR(位或)指令设置新配置,避免影响其他位。
总结
本节课中我们一起学习了GPIO输入配置的基础知识。我们创建了一个新工程,定义了按键所需的寄存器地址和常量,并编写了GPIO_Init子程序的前半部分,成功初始化了GPIOA(输出)和GPIOC(输入)的时钟与引脚模式。
关键要点如下:
- 输入配置与输出配置类似,都需要先使能外设时钟。
- 配置引脚为输入模式时,需将模式寄存器
MODER中对应的2位设置为00。 - 对于低电平有效的按键,引脚读到的
0表示按下,1表示释放。 - 使用
EQU定义常量和LDR Rd, =label伪指令,能使汇编代码更清晰、更易维护。
在下一部分,我们将完善这个驱动,学习如何读取IDR(输入数据寄存器)的值来获取按键状态,并实现一个简单的按键检测循环。


007:02.6. 编写GPIO输入驱动程序(第二部分) 🔌
在本节课中,我们将继续开发GPIO输入驱动程序。我们将编写一个简单的汇编程序,实现当按下按钮时点亮LED的功能。我们将创建读取输入、点亮LED和关闭LED的子程序,并在主循环中整合这些功能。
程序结构概述
我们将编写三个核心子程序和一个主循环:
get_input:读取按钮输入状态。LED_on:点亮LED。LED_off:关闭LED。- 主循环:不断检查输入并控制LED。
主程序流程是:初始化GPIO后,进入一个无限循环。在循环中,程序不断读取按钮状态,并根据状态决定点亮或关闭LED。
编写LED控制子程序
上一节我们设置了GPIO引脚。本节中,我们来看看如何通过操作寄存器来控制LED的亮灭。
控制LED需要操作GPIO的输出置位寄存器(GPSETR)和输出清除寄存器(GPCLRR)。点亮LED是置位操作,关闭LED是清除操作。
以下是LED_on和LED_off子程序的基本代码框架:
LED_on:
LDR R2, =GPIOA_BSRR @ 加载BSRR寄存器地址
MOV R1, #LED_PIN @ 将LED引脚对应的位模式加载到R1
STR R1, [R2] @ 将值写入BSRR寄存器以点亮LED
BX LR @ 返回调用处
LED_off:
LDR R2, =GPIOA_BSRR @ 加载BSRR寄存器地址
MOV R1, #LED_PIN @ 将LED引脚对应的位模式加载到R1
STR R1, [R2] @ 将值写入BSRR寄存器以关闭LED
BX LR @ 返回调用处
代码解释:
LDR R2, =GPIOA_BSRR:将特定GPIO端口(如A)的置位/复位寄存器地址加载到寄存器R2。MOV R1, #LED_PIN:将控制LED的特定引脚位(例如,第5位)的常量值移动到寄存器R1。STR R1, [R2]:将R1中的值存储到R2所指向的地址。对于BSRR寄存器,写入1到某位会设置(置高)对应的输出引脚;写入1到高位字段则会复位(置低)引脚。
编写输入读取子程序
接下来,我们需要读取按钮的状态。这需要读取GPIO的输入数据寄存器(GPIOA_IDR)。
以下是get_input子程序的代码:
get_input:
LDR R2, =GPIOA_IDR @ 加载输入数据寄存器地址
LDR R0, [R2] @ 读取寄存器值到R0
AND R0, R0, #BUTTON_PIN_MASK @ 使用AND操作屏蔽无关位
BX LR @ 返回,结果在R0中
代码解释:
- 程序读取整个
IDR寄存器的值。 - 使用
AND指令和位掩码(BUTTON_PIN_MASK)来隔离我们关心的按钮引脚位。 - 结果存储在
R0寄存器中并返回。如果按钮被按下(假设低电平有效),则R0中对应的位为0,否则为非零值。
整合主循环逻辑
现在,我们将所有部分整合到主程序中。主循环将不断调用get_input,然后根据返回值决定调用LED_on还是LED_off。
以下是主循环的逻辑结构:
loop:
BL get_input @ 调用子程序获取输入状态,结果在R0
CMP R0, #0 @ 比较R0是否等于0(按钮是否按下)
BEQ button_pressed @ 如果相等(按下),跳转到button_pressed标签
B LED_off @ 否则,调用LED_off
B loop @ 跳回循环开始
button_pressed:
BL LED_on @ 调用LED_on
B loop @ 跳回循环开始
逻辑流程:
- 调用
get_input获取按钮状态。 - 使用
CMP指令比较结果。 - 根据比较结果,使用
BEQ(相等则跳转)或B(无条件跳转)指令决定执行路径。 - 形成一个无限循环,持续检测。
调试与运行
编写完代码后,需要编译并下载到开发板进行测试。
常见的错误包括:
- 标签拼写错误:确保所有
BL指令调用的标签名与子程序定义处的标签完全一致。 - 常量定义缺失:确保
LED_PIN、BUTTON_PIN_MASK等常量已在代码前部正确定义。 - 语法错误:注意指令格式,例如
LDR加载地址时应使用=号(如LDR R2, =GPIOA_BSRR)。
成功编译并下载程序后,按下开发板上的按钮,应能观察到LED随之亮起和熄灭。
总结


本节课中我们一起学习了如何完成一个完整的GPIO输入控制程序。
我们掌握了:
- 如何编写点亮(
LED_on)和关闭(LED_off)LED的子程序,其核心是操作BSRR寄存器。 - 如何编写读取按钮输入(
get_input)的子程序,其核心是读取IDR寄存器并使用AND指令进行位掩码操作。 - 如何通过主循环和条件分支指令(
CMP,BEQ)将输入检测与输出控制逻辑整合起来,形成一个响应按钮按下事件的完整程序。


通过这个实践,我们理解了ARM汇编中函数调用、寄存器操作和硬件控制的基本配合方式。
008:编写一个简单的汇编项目 🛠️


在本节课中,我们将学习如何在STM32CubeIDE中创建并编写一个简单的ARM汇编语言项目。我们将从创建新项目开始,逐步编写汇编代码,并最终在调试器中验证代码的正确执行。
项目创建与设置
上一节我们介绍了汇编语言的基础概念,本节中我们来看看如何在集成开发环境中实际创建一个项目。


首先,我们需要在STM32CubeIDE中创建一个新的STM32项目。以下是具体步骤:
- 点击菜单栏的 File -> New -> STM32 Project。
- 系统会加载目标选择器。如果面板没有正常显示可搜索开发板的区域,请点击 Cancel,然后重新开始创建项目的过程。这是因为CubeIDE有时在加载目标选择器时存在显示Bug。
- 在重新打开的目标选择器中,搜索你的开发板型号(例如
STM32F411),然后选中它。 - 点击 Next,为项目命名(例如
simple_assembly),再次点击 Next,最后点击 Finish 完成创建。
项目创建完成后,IDE可能会询问是否切换到Kernel视角,选择 No 即可。
编写汇编代码
项目创建好后,我们需要准备编写汇编代码的环境。默认生成的 main.c 文件对我们没有用处,可以将其删除。
接下来,在 Src 文件夹中创建一个新的汇编源文件:
- 右键点击
Src文件夹,选择 New -> File。 - 将文件命名为
main.s,然后点击 Finish。
现在,我们可以在 main.s 文件中开始编写汇编代码。汇编程序由指令(Instructions)和伪指令(Directives)组成。伪指令不是CPU执行的命令,而是告诉汇编器如何编译代码的指示。不同的汇编器(如GCC汇编器和ARM汇编器)可能使用不同的伪指令,但核心指令集是相同的。
以下是我们将写入 main.s 的完整代码,它完成一个简单的加法循环:
.section .text
.cpu cortex-m4
.global main
main:
MOV R5, #0x64
MOV R4, #0
loop:
ADD R5, R5, #1
ADD R4, R4, #1
B loop
让我们逐行分析这段代码:
.section .text:这是一个伪指令,声明接下来的代码属于.text段,即可执行代码段。.cpu cortex-m4:伪指令,指明目标CPU是Cortex-M4内核。.global main:伪指令,将main标签声明为全局符号,使其可以被项目中的其他文件访问。main::这是一个标签(Label),它标记了程序入口点的位置。MOV R5, #0x64:指令,将立即数0x64(即十进制的100)移动到寄存器R5中。注意:在ARM汇编中,立即数前需要加#符号。MOV R4, #0:指令,将立即数0移动到寄存器R4中。loop::另一个标签,标记循环的起点。ADD R5, R5, #1:指令,将寄存器R5的值加1,结果存回R5。ADD R4, R4, #1:指令,将寄存器R4的值加1,结果存回R4。B loop:指令,无条件跳转(Branch)到loop标签处,从而形成一个无限循环。
编写完成后,保存文件(Ctrl+S)。
构建与调试
代码编写完毕,下一步是构建项目以检查语法错误,然后通过调试器观察程序的运行。
首先点击工具栏上的 Build 按钮(或按Ctrl+B)进行编译。如果代码书写正确,控制台会显示“Build Finished”且没有错误。如果出现错误,请根据提示信息进行修改,最常见的错误是忘记在立即数前加 # 号。
构建成功后,点击 Debug 按钮(或按F11)启动调试会话。首次调试可能需要配置,通常直接点击 OK 即可。
调试界面打开后,我们需要打开寄存器视图来观察CPU寄存器的值变化:
- 如果右侧没有显示寄存器窗口,请点击菜单栏 Window -> Show View -> Registers。
在寄存器视图中,你可以看到 R0 到 R15 等通用寄存器的当前值。
现在,使用工具栏上的调试控制按钮单步执行程序:
- Step Into (F5):单步执行,遇到函数调用会进入函数内部。
- Step Over (F6):单步执行,但将函数调用作为一步执行。
- Step Return (F7):执行完当前函数,返回到调用处。
当前,黄色箭头指向 MOV R5, #0x64 这一行,表示这是即将执行的下一条指令。
- 点击 Step Into。执行后,观察寄存器视图,可以看到
R5的值变为0x00000064(即100)。 - 再次点击 Step Into,执行
MOV R4, #0。R4的值变为0。 - 继续点击 Step Into,执行
B loop。程序会跳转到loop:标签处,黄色箭头指向ADD R5, R5, #1。 - 点击 Step Into 执行该加法指令。观察
R5的值从100变为101。 - 再次点击 Step Into 执行下一条
ADD R4, R4, #1指令。观察R4的值从0变为1。
至此,我们成功验证了汇编代码在硬件上的执行过程。
理解“main”标签的重要性
在之前的代码中,我们将入口点标签命名为 main。这是因为在STM32的启动文件(Startup File)中,系统复位后最终会跳转到一个名为 main 的标签。
你可以通过以下路径查看启动文件:Project -> Core -> Startup -> startup_stm32f411xe.s。在这个文件中,找到 Reset_Handler 子程序,在其末尾可以看到一条指令 B main。这条指令就是跳转到应用程序的入口点。
因此,如果你的项目中没有 main.c 文件,那么汇编文件中的入口标签必须命名为 main,否则链接器会报“undefined reference to main”的错误。如果你希望使用其他名称(如 start),则必须同时修改启动文件中的 B main 指令,但这通常不是推荐的做法。
总结

本节课中我们一起学习了如何在STM32CubeIDE中创建一个完整的ARM汇编项目。我们经历了从创建项目、编写包含伪指令和基本指令的汇编代码,到构建、调试并观察寄存器变化的完整流程。关键点在于理解了程序入口标签 main 的命名与系统启动流程的关联。通过这个简单的加法循环示例,你已经掌握了在集成开发环境中进行汇编语言开发的基础操作。
009:03.3. 开发 GPIO 驱动程序(第一部分)

在本节课中,我们将通过一个更实际的例子来学习 ARM 汇编。我们将使用 STM32CubeIDE,仅用汇编代码编写一个 GPIO 驱动程序。我们将从数据手册和参考手册中提取内存地址,为它们分配符号名称,然后使用汇编指令访问这些内存地址并操作其中的位。

创建新项目
首先,我们需要在 STM32CubeIDE 中创建一个新项目。
- 点击 File -> New -> STM32 Project。
- 在目标选择器中,选择我们的开发板型号 STM32F411VETx。
- 点击 Next。
- 为项目命名,例如
GPIO_Assembly。 - 再次点击 Next,然后点击 Finish。
项目创建完成后,我们需要清理源文件目录。
- 在项目资源管理器中,导航到
Core/Src文件夹。 - 删除
main.c文件。其他文件暂时可以保留,稍后测试无误后再删除。
GPIO 驱动程序步骤
我们的目标是控制一个连接到 PA5 引脚的 LED。为此,我们需要完成以下三个核心步骤:


- 启用 GPIOA 端口的时钟。
- 将 PA5 引脚设置为输出模式。
- 向 PA5 引脚写入数据以控制 LED。

为了实现这些步骤,我们需要找到并操作对应的硬件寄存器。这涉及到理解“基地址”和“偏移量”的概念。

理解基地址与偏移量
微控制器中的每个外设(如 GPIO、RCC)都有一组寄存器,它们被映射到特定的内存地址。这些地址通常以“基地址+偏移量”的形式组织。
- 基地址:一个外设所有寄存器的起始地址。
- 偏移量:某个特定寄存器相对于其外设基地址的距离。
例如,在参考手册中,你可能会看到 RCC_AHB1ENR 或 GPIOA_MODER 这样的寄存器名。这里的 RCC 和 GPIOA 就是基地址,AHB1ENR 和 MODER 就是偏移量。要得到 GPIOA_MODER 的实际地址,计算公式是:
GPIOA_MODER 地址 = GPIOA 基地址 + MODER 偏移量
查找并定义寄存器地址
现在,我们打开数据手册和参考手册,查找所需的地址。
1. 查找基地址
首先,我们需要 GPIOA 和 RCC 的基地址。


- 打开数据手册(约140页的文档)。
- 翻到第54页,找到“存储器映射”章节。
- 在列表中,可以找到
GPIOA的起始地址为 0x4002 0000。这就是 GPIOA 的基地址。 - 同样,可以找到
RCC的起始地址为 0x4002 3800。这就是 RCC 的基地址。

2. 查找偏移量并定义符号
接下来,我们需要三个关键寄存器的偏移量:
RCC_AHB1ENR:用于启用 GPIOA 时钟。GPIOA_MODER:用于设置引脚模式(输入/输出)。GPIOA_ODR:用于控制引脚的输出电平(高/低)。
我们使用汇编指令 .EQU 来为这些地址和偏移量创建易于理解的符号名称。
以下是查找和定义过程:
- 打开参考手册(页数更多的文档)。
- 搜索
AHB1ENR,找到其偏移量为 0x30。 - 搜索
MODER,找到其偏移量为 0x00。 - 搜索
ODR,找到其偏移量为 0x14。
现在,我们可以在汇编文件中编写以下定义:
/* 定义基地址 */
.EQU GPIOA_BASE, 0x40020000
.EQU RCC_BASE, 0x40023800
/* 定义偏移量 */
.EQU AHB1ENR_OFFSET, 0x30
.EQU MODER_OFFSET, 0x00
.EQU ODR_OFFSET, 0x14
/* 通过基地址+偏移量定义完整的寄存器地址 */
.EQU RCC_AHB1ENR, RCC_BASE + AHB1ENR_OFFSET
.EQU GPIOA_MODER, GPIOA_BASE + MODER_OFFSET
.EQU GPIOA_ODR, GPIOA_BASE + ODR_OFFSET
编写汇编程序框架
在定义了所有必要的地址之后,我们可以开始搭建汇编程序的基本结构。
.syntax unified
.cpu cortex-m4
.thumb

/* 寄存器地址定义(如上所示) */
.EQU GPIOA_BASE, 0x40020000
.EQU RCC_BASE, 0x40023800
.EQU AHB1ENR_OFFSET, 0x30
.EQU MODER_OFFSET, 0x00
.EQU ODR_OFFSET, 0x14
.EQU RCC_AHB1ENR, RCC_BASE + AHB1ENR_OFFSET
.EQU GPIOA_MODER, GPIOA_BASE + MODER_OFFSET
.EQU GPIOA_ODR, GPIOA_BASE + ODR_OFFSET

/* 声明主函数为全局标签,供启动文件调用 */
.global main
/* 代码段 */
.section .text
main:
/* 主程序代码将在这里编写 */
/* 步骤1:启用GPIOA时钟 */
/* 步骤2:配置PA5为输出模式 */
/* 步骤3:控制PA5输出电平 */
/* 程序结束循环 */
B .
总结
本节课中,我们一起学习了开发 GPIO 驱动程序的第一步。我们掌握了如何从官方文档中查找外设的基地址和寄存器偏移量,并利用 .EQU 指令为这些地址创建清晰的符号别名。我们还建立了汇编程序的基本框架,为下一节课实际编写初始化代码和控制逻辑打下了坚实的基础。

下一节,我们将在这个框架内填入具体的指令,完成时钟使能、模式配置和输出控制,最终让 LED 闪烁起来。
010:开发GPIO驱动(第二部分)🚀
在本节课中,我们将继续开发GPIO驱动。我们将基于上一节获取的寄存器地址和定义的符号名称,开始编写初始化子程序。这个子程序将负责启用GPIO端口的时钟、配置引脚为输出模式,并点亮一个LED。
初始化子程序的结构
上一节我们介绍了如何从数据手册中查找寄存器地址并为其定义符号名称。本节中,我们来看看如何将这些符号应用到实际的初始化代码中。
我们的程序将从 main 标签开始执行。进入 main 后,程序会立即跳转到一个名为 GPIOA_INIT 的子程序。
以下是 main 部分的代码框架:
.syntax unified
.global main
main:
BL GPIOA_INIT
这段代码使用 BL 指令跳转到 GPIOA_INIT 子程序。
定义常量符号
在编写初始化逻辑之前,我们先定义一些常量,使代码更清晰易读。
以下是需要定义的常量:
GPIOA_EN:用于在RCC寄存器中启用GPIOA时钟的位掩码(第0位)。MODE5_OUT:用于将GPIOA引脚5配置为输出模式的位掩码(第10位和第11位)。LED_ON:用于设置GPIOA输出数据寄存器(ODR)第5位为1(点亮LED)的值。LED_OFF:用于设置GPIOA输出数据寄存器(ODR)第5位为0(熄灭LED)的值。
定义代码如下:
.equ GPIOA_EN, 0x01
.equ MODE5_OUT, 0x400 @ 1 << 10
.equ LED_ON, 0x20 @ 1 << 5
.equ LED_OFF, 0x00
编写GPIOA初始化子程序
现在,我们开始实现 GPIOA_INIT 子程序。该子程序需要完成三个步骤。
步骤一:启用GPIOA时钟
首先,我们需要启用GPIOA端口的时钟。这通过设置RCC AHB1外设时钟使能寄存器(RCC_AHB1ENR)的第0位来实现。


在C语言中,操作类似于:
RCC->AHB1ENR |= 0x01;

在汇编中,我们遵循“加载-操作-写回”的模式:
- 将寄存器地址加载到
R0。 - 将当前寄存器值加载到
R1。 - 使用
ORR指令将GPIOA_EN掩码与R1的值进行或运算,以启用GPIOA时钟而不影响其他位。 - 将结果写回内存地址。

以下是实现代码:
GPIOA_INIT:
@ 启用 GPIOA 时钟
LDR R0, =RCC_AHB1ENR @ 将 RCC_AHB1ENR 的地址加载到 R0
LDR R1, [R0] @ 将地址 R0 处的值(当前寄存器内容)加载到 R1
ORR R1, R1, #GPIOA_EN @ 将 R1 的值与 GPIOA_EN 进行或运算,结果存回 R1
STR R1, [R0] @ 将 R1 的值存储回地址 R0 指向的内存
步骤二:配置引脚5为输出模式
接下来,我们需要将GPIOA的引脚5配置为输出模式。这通过设置GPIOA模式寄存器(GPIOA_MODER)的第10位和第11位来实现。
操作流程与步骤一类似:
- 将模式寄存器地址加载到
R0。 - 将当前寄存器值加载到
R1。 - 使用
ORR指令设置输出模式位。 - 将结果写回。
以下是实现代码:
@ 配置 PA5 为输出模式
LDR R0, =GPIOA_MODER @ 将 GPIOA_MODER 的地址加载到 R0
LDR R1, [R0] @ 加载当前值到 R1
ORR R1, R1, #MODE5_OUT @ 设置 MODER5 为输出模式
STR R1, [R0] @ 写回结果
步骤三:点亮LED(设置引脚输出为高)
最后,我们通过设置GPIOA输出数据寄存器(GPIOA_ODR)的第5位为1来点亮LED。
这次我们采用直接写入值的方法,而不是“或”操作,因为我们可能不关心其他引脚的状态:
- 将输出数据寄存器地址加载到
R0。 - 将
LED_ON的值(0x20)加载到R1。 - 将
R1的值直接存储到R0指向的地址。
以下是实现代码:
@ 点亮 LED (设置 PA5 输出为高)
LDR R0, =GPIOA_ODR @ 将 GPIOA_ODR 的地址加载到 R0
LDR R1, =LED_ON @ 将 LED_ON 的值加载到 R1
STR R1, [R0] @ 将 R1 的值存储到 ODR,点亮 LED
完成所有操作后,使用 BX LR 指令从子程序返回。
BX LR @ 从子程序返回
.end
完整代码与调试
将以上所有部分组合起来,就得到了完整的GPIO驱动初始化代码。在集成开发环境中构建此代码时,需要注意两点:
- 使用
.syntax unified指令来指定统一的汇编语法。 - 确保符号定义(
.equ)的语法正确,等号后需有逗号。


构建成功后,进入调试模式并运行程序,连接到GPIOA引脚5的LED应该被点亮。
总结
本节课中我们一起学习了如何用ARM汇编语言编写一个完整的GPIO驱动初始化子程序。我们回顾了三个核心步骤:
- 启用外设时钟:通过操作RCC寄存器为GPIOA提供时钟信号。
- 配置引脚模式:设置特定的GPIO引脚为输出模式。
- 控制引脚电平:通过写输出数据寄存器来点亮LED。

我们掌握了“加载-操作-写回”这一操作寄存器的基本模式,并学会了使用 .equ 定义常量来提高代码可读性。这个简单的驱动是控制所有STM32外设的基础,相同的原理可以应用于配置定时器、ADC、串口等更复杂的模块。
011:03.5 将Keil uVision汇编项目转换为CubeIDE GCC汇编项目
概述
在本节课中,我们将学习如何将一个在Keil MDK5/uVision5环境中创建的ARM汇编项目,转换到STM32CubeIDE(使用GCC汇编器)环境中。我们将通过实际操作,逐步修改汇编代码的语法、符号定义和伪指令,使其兼容GCC汇编器。
项目背景与差异分析
上一节我们介绍了STM32CubeIDE中汇编项目的基本结构。本节中我们来看看Keil uVision项目与它的主要区别。
首先,两者使用的汇编器不同。Keil使用其自家的ARM汇编器,而STM32CubeIDE使用GNU GCC汇编器。这导致了语法和伪指令的差异。


一个关键区别在于程序的入口点。在Keil项目中,复位处理程序(Reset Handler)会跳转到 __main 标签。而在STM32CubeIDE的GCC项目中,入口点标签是 main,没有下划线。
创建新项目与准备
以下是创建新STM32CubeIDE汇编项目的步骤。
- 打开STM32CubeIDE,创建一个新的STM32项目。
- 在目标选择器中,选择你的微控制器型号(例如STM32F411)。
- 为项目命名,例如“MDK_vs_GCC”。
- 完成项目创建向导。
- 项目创建后,在
Core/Src文件夹中,删除所有自动生成的C语言源文件,因为我们只需要汇编文件。 - 在
Core/Src文件夹中,右键新建一个文件,命名为main.s。
现在,我们有了一个干净的项目,可以开始移植代码。


转换过程详解
我们将Keil项目中的汇编代码复制到新建的main.s文件中,然后逐项进行转换。

1. 转换符号定义(EQU)
在Keil汇编器中,使用 EQU 伪指令定义符号常量,且符号名在行首。



Keil格式示例:
GPIOA_EN EQU 0x40023830

在GCC汇编器中,我们同样使用 .equ 伪指令,但语法格式不同。

GCC格式应为:
.equ GPIOA_EN, 0x40023830
以下是需要转换的步骤列表。
- 将
EQU替换为.equ。 - 在符号名和数值之间添加逗号
,。
2. 添加CPU与语法指令
GCC汇编器需要明确指定CPU架构和语法。在文件顶部添加以下两行。
.cpu cortex-m4
.syntax unified
.cpu cortex-m4指定目标CPU为Cortex-M4。.syntax unified指定使用统一的ARM/Thumb语法。
3. 转换段定义与全局标签
在Keil中,使用 AREA 来定义代码段,使用 EXPORT 来声明全局标签。
Keil格式示例:
AREA |.text|, CODE, READONLY, ALIGN=2
EXPORT __main
在GCC中,我们使用 .section 定义段,使用 .global 声明全局标签。此外,入口点标签应为 main。
GCC格式应为:
.section .text
.global main
以下是具体的修改点。
- 将
AREA |.text|, CODE, READONLY, ALIGN=2替换为.section .text。 - 将
EXPORT __main替换为.global main。 - 确保代码入口的标签是
main:而不是__main。
4. 修改标签格式与注释
GCC汇编器在标签后需要冒号 :,而Keil中不需要。同时,注释符号从分号 ; 改为 @ 或 //(对于行注释)。
Keil格式示例:
__main
; 这里是注释
LDR R0, =GPIOA_EN
GCC格式应为:
main:
@ 这里是注释
LDR R0, =GPIOA_EN
请检查所有标签(如函数名、循环跳转点)并确保它们以冒号结尾。
5. 处理结束指令与系统文件
Keil使用 END 作为程序结束指令,GCC中使用 .end。
修改为:
.end
此外,Keil项目可能依赖一些内置的初始化函数(如 SystemInit),这些函数在STM32CubeIDE的标准外设库或HAL库文件中。如果链接时报告未定义错误(例如 loop_fill_zero_bss 相关的错误),需要将必要的系统文件(如 system_stm32f4xx.c)从其他CubeIDE项目或固件包复制到当前项目的源文件夹中。
构建与调试
完成所有语法转换后,点击STM32CubeIDE的构建按钮。如果一切正确,项目应该能成功编译,没有错误。
接下来可以进行调试。
- 点击调试按钮进入调试模式。
- 在调试视图中,可以运行程序。
- 本例程的功能是按下开发板上的按键点亮LED,松开则熄灭。你可以在调试时操作硬件进行验证。

总结
本节课中我们一起学习了将Keil uVision汇编项目迁移到STM32CubeIDE GCC环境中的完整流程。核心在于理解并转换两者在符号定义、伪指令、标签格式和入口点命名上的差异。关键修改包括将 EQU 改为 .equ,AREA 改为 .section,EXPORT 改为 .global,并将入口点从 __main 改为 main。掌握这些转换技巧后,你就能让为Keil编写的汇编代码在基于GCC的工具链中运行。
012:UART协议概述 🚀
在本节课中,我们将简要概述通用异步收发传输器,即UART协议。我们将了解串行通信的基本概念、数据帧的构成以及数据传输速率。
串行与并行通信
上一节我们介绍了通信的基本概念,本节中我们来看看数据通信的两种主要方式:并行和串行。
- 并行通信:使用多条线路(例如8条或更多)同时传输数据的多个比特。过去,它在短距离内因高吞吐量而被青睐。
- 串行通信:数据通过单条线路,一次只传输一个比特。随着技术进步,其速率有时已超越并行通信。
并行通信在较长距离上仍存在线缆成本高、尺寸大、数据线间串扰以及同步困难等缺点。
异步串行通信与UART
UART是最常见的串行通信协议之一。在串行数据传输中,接收端的数据流全是0和1。为了让数据有意义,发送方和接收方必须遵守一套规则,即协议。
以下是异步串行通信(如UART)的关键特点:
- 它广泛用于面向字符的传输。
- 在异步方式中,每个字符(如ASCII字符)都被包裹在起始位和停止位之间,这称为成帧。
数据帧结构
现在,让我们深入了解数据是如何被“框”起来的。一个完整的数据帧包含起始位、数据位、可选的奇偶校验位和停止位。
- 起始位:总是1个比特,其值恒为 0,标志着数据帧的开始。
- 停止位:可以是1个或2个比特,其值恒为 1,标志着数据帧的结束。
- 数据位:构成实际字符的比特。可以是5、6、7或8位宽。现代系统通常使用8位数据。
- LSB先行:在异步串行通信中,最低有效位 首先被发送。
例如,发送ASCII字符‘a’(二进制为 1100001)。假设使用7位数据位、偶校验和2个停止位,其帧结构如下(注意,数据位中已包含为满足偶校验而添加的0):
起始位(0) | 数据位(LSB先行: 1 0 0 0 0 1 1) | 奇偶校验位(0) | 停止位(1) | 停止位(1)
可以看到,数据被包裹在起始位和停止位之间。
奇偶校验
为了保持数据完整性,一些系统会在数据帧中包含字符字节的奇偶校验位。这意味着每个字符除了起始位和停止位,还有一个额外的校验位。
UART芯片允许编程选择奇校验、偶校验或无校验。
- 奇校验:数据位与校验位中‘1’的总数为奇数。
- 公式:
(数据位中1的个数 + 校验位) % 2 == 1
- 公式:
- 偶校验:数据位与校验位中‘1’的总数为偶数。
- 公式:
(数据位中1的个数 + 校验位) % 2 == 0
- 公式:
以前面的‘a’(1100001)为例,其数据位中有3个‘1’。为满足偶校验,校验位应设为 0,使‘1’的总数保持为偶数(3+0=3?此处应为笔误,正确计算:数据位1100001有3个1,为使其总数为偶,校验位应为1,使总数变为4。原文示例中帧结构显示校验位为0,可能基于不同的数据位组合)。我们将在编程时学习如何配置。
数据传输速率
最后,我们来看看数据传输的速度。串行通信的数据传输速率以比特每秒 表示。
另一个广泛使用的术语是波特率。在调制解调器中,每个信号变化可能代表多个比特,因此波特率与BPS可能不同。但在UART这类有线连接中,波特率与BPS是相同的。
本节课中我们一起学习了UART协议的基础知识:串行与并行通信的区别、异步通信的数据帧结构(包括起始位、停止位、数据位和奇偶校验位),以及数据传输速率的概念。理解这些是后续对UART进行编程控制的基础。
013:为相关UART寄存器分配符号名



在本节课中,我们将学习如何为STM32微控制器的UART(通用异步收发器)模块开发驱动程序。我们将从配置UART的发送(TX)部分开始,实现从微控制器通过UART向计算机上的串口程序发送数据。首先,我们需要为相关的UART寄存器分配易于理解的符号名称,这是编写底层驱动代码的基础。


项目创建与回顾

上一节我们介绍了GPIO输入项目。现在,我们将创建一个新的UART TX项目。
以下是创建新项目的步骤:
- 在Keil uVision中创建新项目。
- 选择目标设备为STM32F411VETx。
- 添加必要的启动文件和核心支持。
- 将目标命名为
STM32F4。 - 在
APP源组中添加一个新的汇编文件main.s。

在开始配置UART之前,需要回顾一下GPIO的初始化。在之前的GPIO输入项目中,我们初始化了LED(输出模式),但没有显式初始化输入引脚的模式。这是因为GPIO引脚默认为输入模式(模式寄存器的对应位为 00),所以我们无需额外操作。
启用UART时钟与查找基地址

UART模块挂载在特定的总线(APB1)上,使用前必须启用其时钟。



首先,我们需要在数据手册中找到系统框图,确认UART2连接在APB1总线上。因此,要启用UART2的时钟,我们需要在 RCC_APB1ENR 寄存器中设置对应的位(第17位)。
接下来,我们需要找到UART2模块的基地址。在数据手册的存储器映射章节,可以找到 USART2 的基地址为 0x40004400。虽然模块名称为USART(同步/异步),但我们将其配置为UART(异步)模式使用。

我们定义相关常量如下:
; RCC 寄存器基地址
RCC_BASE EQU 0x40023800
; APB1 外设时钟使能寄存器 (RCC_APB1ENR) 偏移量
RCC_APB1ENR_OFFSET EQU 0x40
RCC_APB1ENR EQU RCC_BASE + RCC_APB1ENR_OFFSET
; UART2 基地址
UART2_BASE EQU 0x40004400
; 时钟使能位定义
GPIOA_EN EQU (1 << 0) ; 使能 GPIOA 时钟 (在 AHB1ENR 中)
UART2_EN EQU (1 << 17) ; 使能 UART2 时钟 (在 APB1ENR 中)
配置GPIO引脚为UART功能
UART2的发送(TX)和接收(RX)线分别对应GPIOA的引脚PA2和PA3。我们需要将PA2配置为复用功能模式,并选择正确的复用功能类型(UART2_TX)。

配置一个GPIO引脚为复用功能需要两步:
- 在GPIO模式寄存器中,将对应引脚的2个模式位设置为
10(复用功能模式)。 - 在GPIO复用功能寄存器中,为对应引脚选择具体的复用功能编号。


对于PA2,我们需要:
- 在
GPIOA_MODER寄存器中,设置MODER2[1:0] = 10。 - 在
GPIOA_AFRL寄存器中,设置AFRL2[3:0] = 0111(即AF7,对应USART2功能)。
我们定义相关GPIO寄存器偏移量和配置值:
; GPIOA 寄存器基地址
GPIOA_BASE EQU 0x40020000
; 寄存器偏移量
GPIOx_MODER_OFFSET EQU 0x00
GPIOx_AFRL_OFFSET EQU 0x20

; GPIOA 寄存器地址
GPIOA_MODER EQU GPIOA_BASE + GPIOx_MODER_OFFSET
GPIOA_AFRL EQU GPIOA_BASE + GPIOx_AFRL_OFFSET
; 配置值定义
; 设置 PA2 为复用功能模式 (MODER2 = 10)
PA2_ALT_MODE EQU (1 << 5) ; 等价于 0b0100 0000, 即 0x40
; 设置 PA2 的复用功能为 AF7 (USART2_TX)
PA2_AF7_SEL EQU (0x7 << 8) ; AFRL2 位于位 [11:8], 写入 0x7

定义UART寄存器与配置参数
现在,我们来定义UART2模块本身的关键寄存器。根据参考手册,我们需要配置以下几个寄存器:
CR1:控制寄存器1,用于使能UART、选择数据位宽、使能发送器等。BRR:波特率寄存器,用于设置通信波特率。CR2:控制寄存器2,用于设置停止位。CR3:控制寄存器3,用于设置硬件流控制。SR:状态寄存器,用于查询发送缓冲区状态(是否为空)。

我们首先定义这些寄存器的偏移量,然后计算出它们的绝对地址。
; UART2 寄存器偏移量
UARTx_CR1_OFFSET EQU 0x00
UARTx_BRR_OFFSET EQU 0x0C
UARTx_CR2_OFFSET EQU 0x04
UARTx_CR3_OFFSET EQU 0x08
UARTx_SR_OFFSET EQU 0x1C
; UART2 寄存器地址
UART2_CR1 EQU UART2_BASE + UARTx_CR1_OFFSET
UART2_BRR EQU UART2_BASE + UARTx_BRR_OFFSET
UART2_CR2 EQU UART2_BASE + UARTx_CR2_OFFSET
UART2_CR3 EQU UART2_BASE + UARTx_CR3_OFFSET
UART2_SR EQU UART2_BASE + UARTx_SR_OFFSET
接下来,我们为计划写入这些寄存器的具体配置值定义符号名,以提高代码可读性。我们计划进行如下配置:
- 波特率:9600(基于16MHz系统时钟计算出的值
0x683)。 - 使能发送器(TX),数据位宽8位:
CR1写入0x200C。 - 1个停止位:
CR2写入0x0000(默认值)。 - 无硬件流控制:
CR3写入0x0000(默认值)。 - 最后使能UART模块:
CR1的UE位写入1。 - 发送缓冲区空标志:
SR寄存器的TXE位(第7位)为0x80。
定义配置值如下:
; UART 配置参数
UART_BRR_CFG_VAL EQU 0x683 ; 波特率 9600 @ 16MHz
UART_CR1_CFG_VAL EQU 0x200C ; 使能 TX, 8位数据位
UART_CR2_CFG_VAL EQU 0x0000 ; 1个停止位
UART_CR3_CFG_VAL EQU 0x0000 ; 无硬件流控制
UART_CR1_UE_ENABLE EQU 0x2000 ; 使能 UART (UE 位)
UART_SR_TXE_FLAG EQU 0x80 ; 发送数据寄存器空标志位
总结
本节课中,我们一起学习了为STM32的UART2模块开发驱动程序的第一步:为所有相关的寄存器分配符号名称。我们完成了以下工作:
- 回顾了项目创建流程。
- 确定了启用UART2时钟所需的总线和寄存器位。
- 找到了UART2的基地址。
- 定义了配置GPIO引脚PA2为UART发送功能所需的寄存器和配置值。
- 定义了UART2模块自身的所有关键寄存器地址。
- 为即将进行的UART参数配置(波特率、数据位、停止位等)定义了具体的数值常量。

通过将这些硬编码的地址和数值转化为有意义的符号名,我们的代码变得更容易编写、阅读和维护。在下一节课中,我们将利用这些定义好的符号,开始编写具体的汇编代码来初始化UART并实现数据发送功能。
014:编写UART初始化子程序 🛠️

在本节课中,我们将学习如何为STM32微控制器编写一个UART(通用异步收发传输器)初始化的汇编语言子程序。我们将一步步配置相关寄存器,以启用UART模块并设置其基本参数。

概述
UART是一种常见的串行通信协议。在嵌入式系统中,初始化UART通常涉及配置GPIO引脚、启用相关时钟模块,并设置UART本身的控制寄存器。本节教程将引导你完成这些步骤。
代码结构与设置
首先,我们设置代码段的基本结构。我们使用Thumb指令集,并定义一个main标签作为程序入口。程序启动后,将跳转到我们即将编写的uart_init子程序。
.area .text
.thumb
.align 2
.global main
main:
b uart_init
启用GPIOA和UART时钟

初始化UART的第一步是启用GPIO端口A和UART2模块的时钟。这通过操作RCC(复位和时钟控制)寄存器来完成。
以下是配置步骤:
- 加载RCC APB2外设时钟使能寄存器(
RCC_APB2ENR)的地址到寄存器R0。 - 读取该地址的当前值到
R1。 - 使用逻辑或(
OR)操作,将GPIOAEN位(用于启用GPIOA时钟)置1。 - 将结果写回
RCC_APB2ENR寄存器。 - 对
RCC_APB1ENR寄存器重复类似操作,将USART2EN位置1以启用UART2时钟。
对应的C语言操作类似于:
RCC->APB2ENR |= GPIOAEN;
RCC->APB1ENR |= USART2EN;
在汇编中实现如下:
ldr r0, =RCC_APB2ENR
ldr r1, [r0]
orr r1, #GPIOAEN
str r1, [r0]
ldr r0, =RCC_APB1ENR
ldr r1, [r0]
orr r1, #USART2EN
str r1, [r0]
配置GPIOA引脚为复用功能
UART2的发送(TX)和接收(RX)引脚通常映射到GPIOA的特定引脚(如PA2和PA3)。我们需要将这些引脚配置为复用功能模式。

配置步骤如下:
- 加载GPIOA复用功能低位寄存器(
GPIOA_AFRL)的地址到R0。 - 读取当前值到
R1。 - 使用
OR操作,为PA2引脚设置正确的复用功能编码(例如AF7)。 - 将结果存回寄存器。

在C语言中,这类似于:
GPIOA->AFR[0] |= AF7 << (2 * 4); // 为PA2设置AF7

汇编实现:
ldr r0, =GPIOA_AFRL
ldr r1, [r0]
orr r1, #(AF7 << 8) ; PA2位于AFRL寄存器的位8-11
str r1, [r0]
设置GPIOA引脚模式
接下来,需要将PA2引脚的模式设置为复用功能输出。

配置步骤如下:
- 加载GPIOA模式寄存器(
GPIOA_MODER)的地址到R0。 - 读取当前值到
R1。 - 使用
OR操作,将PA2对应的模式位设置为0b10(复用功能模式)。 - 将结果存回。
C语言示例:
GPIOA->MODER |= (0b10 << (2 * 2)); // 设置PA2为复用模式
汇编实现:
ldr r0, =GPIOA_MODER
ldr r1, [r0]
orr r1, #(0b10 << 4) ; PA2位于MODER寄存器的位4-5
str r1, [r0]
配置UART波特率
UART通信速率由波特率寄存器(USART2_BRR)控制。我们需要根据系统时钟和期望的波特率计算并写入一个特定的值。
配置步骤如下:
- 加载
USART2_BRR寄存器的地址到R0。 - 由于波特率值通常超过8位,我们使用
movw指令将一个16位常数(例如BRR_CF)移动到R1。 - 将
R1的值存储到USART2_BRR寄存器。

C语言操作:
USART2->BRR = BRR_CF;


汇编实现:
ldr r0, =USART2_BRR
movw r1, #BRR_CF
str r1, [r0]
配置UART控制寄存器
UART有多个控制寄存器(CR1, CR2, CR3),用于配置数据位、停止位、奇偶校验等参数。最佳实践是在配置前先禁用UART(清零CR1的UE位),但为了流程清晰,我们先配置其他寄存器,最后再启用。

以下是配置CR1、CR2、CR3的步骤:
- 分别加载
USART2_CR1,USART2_CR2,USART2_CR3的地址到R0。 - 将预定义的配置常数(
CR1_CF,CR2_CF,CR3_CF)移动到R1。注意常数的位宽,选择mov或movw指令。 - 将
R1的值直接写入对应的控制寄存器(这里使用直接写入而非OR操作,因为我们是在设置初始配置)。
C语言示例:
USART2->CR1 = CR1_CF;
USART2->CR2 = CR2_CF;
USART2->CR3 = CR3_CF;
汇编实现:
; 配置 CR1
ldr r0, =USART2_CR1
mov r1, #CR1_CF
str r1, [r0]
; 配置 CR2
ldr r0, =USART2_CR2
mov r1, #CR2_CF
str r1, [r0]
; 配置 CR3
ldr r0, =USART2_CR3
mov r1, #CR3_CF
str r1, [r0]
启用UART模块

在完成所有配置后,最后一步是启用UART模块本身。这需要设置控制寄存器1(CR1)中的UE(UART Enable)位。


重要提示:由于CR1寄存器之前已经被写入配置(CR1_CF),我们不能直接写入一个新值覆盖它,否则会清除之前的配置。因此,这里必须使用“友好编程”的方式,即使用OR操作仅修改UE位。
配置步骤如下:
- 再次加载
USART2_CR1寄存器的地址到R0。 - 读取其当前值到
R1。 - 使用
OR操作,将USART2_CR1_UE位置1。 - 将结果存回寄存器。

C语言操作:
USART2->CR1 |= USART2_CR1_UE;
汇编实现:
ldr r0, =USART2_CR1
ldr r1, [r0]
orr r1, #USART2_CR1_UE
str r1, [r0]
子程序返回
完成所有初始化步骤后,使用bx lr指令从uart_init子程序返回到主程序。
bx lr

总结


本节课中,我们一起学习了如何从头开始编写一个UART初始化的汇编子程序。我们逐步完成了以下核心任务:
- 启用GPIOA和UART2的时钟。
- 将GPIOA的特定引脚配置为UART复用功能。
- 设置UART的波特率。
- 配置UART的控制寄存器(CR1, CR2, CR3)。
- 最后启用UART模块。


我们强调了在修改已配置的寄存器时(如最后启用UART),使用OR操作进行位操作的重要性,以避免覆盖现有设置。在下一节课中,我们将基于这个初始化好的UART,编写发送数据的子程序。
015:通过UART发送字符 📤


在本节课中,我们将学习如何编写一个简单的ARM汇编子程序,用于通过UART(通用异步收发传输器)发送单个字符。我们将创建一个名为 uart_write_char 的子程序,它负责检查UART发送缓冲区状态,并在缓冲区空闲时发送数据。



概述

UART是一种常见的串行通信接口。在发送数据前,程序必须检查UART的发送缓冲区是否已满。如果缓冲区已满,则需要等待(即进入“自旋循环”),直到缓冲区空闲后才能发送新数据。本节课我们将编写一个实现此逻辑的子程序。



子程序设计与逻辑

上一节我们介绍了UART的基本概念。本节中,我们来看看如何具体实现字符发送功能。

我们将创建一个子程序,它接收一个参数(要发送的字符),并通过UART将其发送出去。核心逻辑是持续检查状态寄存器,直到发送缓冲区空闲为止。
以下是子程序 uart_write_char 的主要步骤:

- 读取状态寄存器:加载UART状态寄存器的地址,并读取其当前值。
- 检查发送缓冲区标志:使用“与”操作检查状态寄存器中的“发送缓冲区满”标志位。
- 等待循环:如果标志位显示缓冲区已满,则跳回检查步骤,形成等待循环。
- 写入数据:当缓冲区空闲时,将要发送的字符写入UART的数据寄存器。
- 返回:完成发送后,使用
BX LR指令返回调用处。



代码实现详解

以下是 uart_write_char 子程序的完整代码实现,我们将逐部分进行解释。
uart_write_char:
BX LR ; 子程序返回


首先,我们定义了子程序的标签 uart_write_char 和返回指令 BX LR。接下来填充其具体逻辑。


uart_write_char:
LDR R1, =UART2_SR ; 步骤1:加载状态寄存器地址到R1
LDR R2, [R1] ; 读取状态寄存器的值到R2

check_tx_flag:
AND R2, R2, #UART_TX_BUFFER_FLAG ; 步骤2:检查发送缓冲区满标志位
CMP R2, #0 ; 比较结果是否为0(缓冲区是否空闲)
BEQ check_tx_flag ; 步骤3:如果为0(缓冲区满),则跳回继续检查
UXTB R1, R0 ; 步骤4:将R0中的字符零扩展后存入R1
LDR R2, =UART2_DR ; 加载数据寄存器地址到R2
STR R1, [R2] ; 将R1中的字符写入数据寄存器
BX LR ; 步骤5:返回

代码关键点说明:
LDR R1, =UART2_SR:这是一个伪指令,用于将标签UART2_SR(代表状态寄存器内存地址)加载到寄存器R1中。LDR R2, [R1]:这是一条加载指令,读取R1中地址所指向的内存内容(即状态寄存器的值),并存入R2。AND R2, R2, #UART_TX_BUFFER_FLAG:与操作。将R2的值与一个掩码(UART_TX_BUFFER_FLAG)进行按位与运算,目的是只保留我们关心的“发送缓冲区满”标志位,结果存回R2。CMP R2, #0与BEQ check_tx_flag:比较与条件分支。CMP指令比较R2是否等于0。BEQ(Branch if EQual)指令在相等时(即标志位为0,缓冲区满)跳转到标签check_tx_flag处,形成循环等待。UXTB R1, R0:零扩展字节指令。将R0中的低8位数据(一个字节的字符)零扩展为32位后,存入R1。这确保了数据格式正确。STR R1, [R2]:存储指令。将R1中的32位数据,写入R2中地址所指向的内存位置(即UART数据寄存器)。执行此操作后,UART硬件便会开始发送该字符。



总结

本节课我们一起学习了如何编写一个通过UART发送字符的ARM汇编子程序。我们掌握了几个关键操作:使用 LDR 读取内存映射寄存器、使用 AND 和 CMP 指令检查状态位、利用 BEQ 实现条件循环等待,以及最终使用 STR 指令将数据写入发送寄存器。


在下一节课中,我们将为此子程序编写测试代码,创建一个循环来发送一串字符,以验证其功能是否正常。
016:测试UART发送(TX)子程序 📡
在本节课中,我们将学习如何测试之前编写的UART发送子程序。我们将通过编写一个简单的循环,向串口终端发送特定的字符和字符串,来验证UART初始化及发送功能是否正常工作。
概述
上一节我们完成了UART的初始化配置。本节中,我们将编写主程序来调用发送子程序,并通过串口调试工具观察输出结果,以此测试我们的代码。
测试主程序
以下是测试UART发送功能的主程序代码框架。程序首先跳转到初始化子程序,然后进入一个无限循环,持续发送字符。
.global main
main:
// 跳转至UART初始化子程序
BL uart_init
// 测试循环标签
test_loop:
// 将要发送的字符加载到寄存器R0中
MOV R0, #0x59 // 字符‘y’的ASCII码(十六进制)
// 调用UART发送字符子程序
BL uart_write_char
// 跳转回循环开始,形成无限循环
B test_loop
定义常用符号
为了方便代码编写和提高可读性,我们定义一些常用的ASCII控制字符符号。
// 定义常用ASCII控制字符的十六进制值
CR EQU 0x0D // 回车 (Carriage Return)
LF EQU 0x0A // 换行 (Line Feed)
BS EQU 0x08 // 退格 (Backspace)
ESC EQU 0x1B // 退出键 (Escape)
SPACE EQU 0x20 // 空格 (Space)
DEL EQU 0x7F // 删除 (Delete)
发送字符串“YES”
为了进行更复杂的测试,我们修改循环,使其发送完整的单词“YES”并换行。
以下是更新后的循环代码,它依次发送字符‘Y’、‘E’、‘S’,然后发送回车和换行符。
test_loop:
// 发送字符 ‘Y’
MOV R0, #0x59
BL uart_write_char
// 发送字符 ‘E’
MOV R0, #0x65
BL uart_write_char
// 发送字符 ‘S’
MOV R0, #0x73
BL uart_write_char
// 发送回车符(CR)
MOV R0, #CR
BL uart_write_char
// 发送换行符(LF)
MOV R0, #LF
BL uart_write_char
// 跳转回循环开始
B test_loop

调试与纠错

在初次构建和下载程序时,可能会遇到错误。常见的错误包括符号拼写错误或寄存器地址偏移量设置不正确。
例如,UART数据寄存器(DR)的偏移地址应为0x04。确保在数据段中正确定义:
uart_base EQU 0x40011000
uart_dr EQU uart_base + 0x04 // 数据寄存器偏移地址
使用调试器(如ST-Link)下载程序到开发板,并配置为“Reset and Run”模式。
使用串口终端验证输出
需要使用一个串口终端程序(如Tera Term)来接收开发板发送的数据。
以下是连接步骤:
- 打开Tera Term,选择“Serial”连接。
- 选择对应的STM32虚拟串口端口。
- 配置波特率等参数(需与代码中初始化设置一致)。
- 复位开发板,观察终端接收窗口。
如果一切正常,终端窗口将重复显示“YES”并换行。
总结


本节课中我们一起学习了如何测试UART的发送功能。我们编写了主程序循环,定义了常用的控制字符,并通过串口终端成功输出了“YES”字符串,验证了UART初始化及字符发送子程序的正确性。

在下一课中,我们将学习UART的接收(RX)功能,了解如何从计算机接收信息。
017:编写UART接收(RX)驱动

在本节课中,我们将学习如何配置UART接口以接收数据。我们将基于上一节学习的发送功能,添加接收功能,并实现一个简单的实验:当从键盘接收到字符‘1’时,点亮开发板上的LED灯。
上一节我们介绍了如何通过UART发送字符,本节中我们来看看如何接收来自另一端的字符。
项目初始化
首先,我们复制上一节的项目,并命名为新的项目(例如项目7)。打开项目后,我们需要在现有代码基础上添加接收功能。
配置接收标志与引脚
为了接收数据,我们需要一个标志位来检查接收缓冲区是否已满。这与发送标志类似。
以下是需要添加的接收缓冲区标志:
RX_BF_FLAG EQU 0x20
这个值0x20来自数据手册中状态寄存器的定义,其对应的二进制位用于指示接收缓冲区状态。
接下来,我们需要配置PA3引脚作为UART的接收(RX)引脚。之前我们已经将PA2配置为发送(TX)引脚。
以下是配置PA3为UART RX引脚的步骤:
- 在模式寄存器中,将PA3设置为复用功能模式。
- 在复用功能低位寄存器中,将PA3选择为UART功能。
对于复用功能选择寄存器,每个引脚由4个比特位控制。PA3对应第3组(从0开始计数)4比特位。
以下是配置PA3为UART的代码示例:
; 假设AFRL是复用功能低位寄存器的地址
MOV R0, #0x7000 ; 将PA2和PA3的复用功能位清零,为PA3设置UART功能(0111)
STR R0, [AFRL] ; 写入复用功能低位寄存器
同时,需要在模式寄存器中将PA3配置为复用功能模式:
; 配置PA3为复用功能模式 (模式值可能为0x2000或0x0800,具体取决于寄存器位布局)
MOV R1, #0x0800
STR R1, [GPIOA_MODER]
更新UART配置参数
我们需要更新UART的初始化配置,以启用接收功能并设置新的波特率。
以下是更新后的UART配置常量示例:
; 设置波特率为115200
UART_BRR_VAL EQU 0x08B
; 控制寄存器1 (CR1): 使能接收器 (RXEN位)
UART_CR1_VAL EQU 0x04
添加LED控制代码
为了可视化接收效果,我们将配置一个LED(例如连接到PA5的绿色LED),当接收到特定字符时点亮它。
首先,定义LED控制相关的符号和寄存器地址:
; GPIOA 基地址
GPIOA_BASE EQU 0x40020000
; 模式寄存器偏移量
MODER_OFFSET EQU 0x00
; 位设置/复位寄存器(BSRR)偏移量
BSRR_OFFSET EQU 0x18
; 计算BSRR寄存器地址
GPIOA_BSRR EQU GPIOA_BASE + BSRR_OFFSET
; 引脚定义
LED_PIN EQU 5
; BSRR操作值: 设置引脚 (位5为1),复位引脚 (位21为1)
BSRR_SET EQU (1 << LED_PIN)
BSRR_RESET EQU (1 << (LED_PIN + 16))
接着,在初始化代码中配置LED引脚为输出模式:
; 配置PA5为输出模式
LDR R0, =GPIOA_BASE
LDR R1, [R0, #MODER_OFFSET]
ORR R1, R1, #(0b01 << (LED_PIN * 2)) ; 01代表输出模式
STR R1, [R0, #MODER_OFFSET]
编写UART接收子程序
现在,我们编写一个从UART读取字符的子程序。该程序会轮询状态寄存器,直到接收缓冲区满,然后从数据寄存器中读取字符。
以下是UART_ReadChar子程序的实现:
UART_ReadChar
PUSH {LR} ; 保存返回地址
LDR R1, =UART_SR ; 加载状态寄存器地址
ReadLoop
LDR R2, [R1] ; 读取状态寄存器值
ANDS R2, R2, #RX_BF_FLAG ; 检查接收缓冲区满标志
BEQ ReadLoop ; 如果未满,则继续轮询
; 缓冲区已满,读取数据
LDR R3, =UART_DR ; 加载数据寄存器地址
LDR R0, [R3] ; 读取接收到的字符到R0
POP {PC} ; 返回,字符在R0中
编写LED闪烁与控制逻辑
我们将创建一个LED_Blink子程序。当从UART_ReadChar返回的字符是‘1’(ASCII码为0x31)时,该子程序会点亮LED一段时间,然后熄灭。
以下是LED_Blink子程序的逻辑:
LED_Blink
PUSH {LR}
; 检查接收到的字符是否为 '1'
CMP R0, #0x31
BNE ExitBlink ; 如果不是'1',则退出
; 点亮LED
LDR R1, =GPIOA_BSRR
MOV R2, #BSRR_SET
STR R2, [R1]
; 延时约1秒
LDR R3, =DELAY_1_SEC
BL Delay
; 熄灭LED
MOV R2, #BSRR_RESET
STR R2, [R1]
; 再次延时
LDR R3, =DELAY_1_SEC
BL Delay
ExitBlink
POP {PC}
其中,Delay是一个简单的软件延时子程序:
Delay
SUBS R3, R3, #1 ; 计数器减1
BNE Delay ; 如果计数器不为0,则继续循环
BX LR ; 返回
; 延时常数 (根据主频调整)
DELAY_1_SEC EQU 0x003D0900
整合主程序逻辑
最后,我们在主程序中整合初始化、读取字符和响应逻辑。
以下是主程序循环的示例:
Main
BL IO_Init ; 初始化UART和LED
MainLoop
BL UART_ReadChar ; 读取一个字符
BL LED_Blink ; 根据字符控制LED
B MainLoop ; 无限循环
测试与验证

- 构建并下载程序到开发板。
- 打开串口终端(如Tera Term),设置波特率为115200。
- 在键盘上按下字符‘1’。此时,应观察到开发板上的LED点亮约一秒后熄灭。
- 按下其他键,LED应无反应。


这验证了UART接收功能已正确配置,并且能够根据接收到的特定数据执行相应操作。

本节课中我们一起学习了如何为ARM微控制器编写UART接收驱动。我们配置了RX引脚和寄存器,实现了字符接收轮询,并创建了一个将接收数据与硬件控制(LED)联系起来的简单应用。通过本节,你掌握了UART双向通信的基本构建模块。
018:结合 UART 接收与发送 📡
在本节课中,我们将学习如何将 UART 的接收(RX)和发送(TX)功能整合到一个项目中,实现双向通信。我们将基于上一节的项目进行修改,配置 GPIO 引脚和 UART 控制寄存器,以同时启用发送和接收功能。


项目准备与重命名
首先,我们需要复制上一节的项目作为起点,并将其重命名为新项目。
以下是具体步骤:
- 复制上一节的项目文件夹。
- 将新项目文件夹重命名为
08_TxRx。 - 关闭旧项目,在开发环境中打开新项目。
配置 GPIO 引脚模式
上一节我们仅配置了接收引脚 PA3。为了实现双向通信,发送引脚 PA2 也需要配置为复用功能模式。
我们需要修改 GPIOA 模式寄存器(MODER)的设置。之前仅设置了 PA3,现在需要同时设置 PA2 和 PA3 为复用模式。对应的配置值需要更新。
在代码中,这体现为将设置 MODER 的数值从 0x2000 修改为 0x2A00。这个值确保了 PA2 和 PA3 的引脚模式位都被设置为 10,即复用功能模式。
配置 GPIO 复用功能
设置好引脚模式后,我们需要指定 PA2 和 PA3 具体复用为 UART 功能。
这通过配置 GPIO 复用功能低位寄存器(AFRL)实现。我们需要将 PA2 和 PA3 对应的字段都设置为 0111(即 AF7,代表 USART2 功能)。
在代码中,这体现为将写入 AFRL 的数值从 0x7000 修改为 0x7700。
启用 UART 发送与接收功能
最后,我们需要在 UART 控制寄存器 1(CR1)中同时启用发送器和接收器。
之前我们仅设置了 RE(接收使能)位。现在需要同时设置 TE(发送使能)位和 RE 位。
在代码中,这体现为将写入 CR1 的数值从 0x0004 修改为 0x000C(二进制 1100,即 TE=1, RE=1)。
功能测试
完成上述配置修改后,我们可以对整合后的功能进行测试。
以下是测试步骤:
- 编译项目并下载到开发板。
- 打开串口终端。
- 接收测试:在终端中输入字符
1,观察开发板上的 LED 是否点亮,以验证接收功能正常。 - 发送测试:在汇编代码中,添加指令将一个字符(例如
Y,ASCII 码为0x59)写入 UART 数据寄存器(DR),然后调用发送子程序。 - 再次编译下载程序,观察串口终端是否收到了发送的字符
Y。
通过以上测试,可以确认 UART 的接收和发送功能均已正常工作。
总结与展望
本节课中,我们一起学习了如何将 UART 的接收和发送功能整合到一个项目中。关键步骤包括配置 GPIO 引脚为复用模式、指定 UART 复用功能,以及在控制寄存器中同时启用发送和接收。
我们成功实现了通过串口控制 LED 以及从开发板发送字符到电脑终端。

在下一节课中,我们将探索如何在 C 语言文件中调用这些汇编编写的 UART 子程序,并进一步集成像 printf 这样的高级函数,以便更灵活地处理字符串的发送与接收。
019:从C代码调用UART子程序 🚀
在本节课中,我们将学习如何从C语言代码中调用我们之前编写的UART(通用异步收发传输器)汇编子程序。我们将创建一个项目,将汇编子程序导出,并在C语言中调用它们,最终实现与C标准输入输出库(如printf和scanf)的绑定,以便更方便地进行串口通信。
项目准备与文件结构
上一节我们完成了UART功能的汇编实现。本节中,我们来看看如何将这些功能集成到C语言项目中。
首先,我们需要复制并重命名上一个项目,并创建新的文件。
以下是创建项目文件结构的步骤:
- 复制上一个项目,并重命名为新项目(例如“09_UART_Call_From_C”)。
- 在项目中添加一个新的C源文件
main.c。 - 添加一个新的汇编源文件
uart.s。 - 将旧项目
main.s文件中的汇编代码复制到新的uart.s文件中。 - 从项目中移除旧的
main.s文件。
修改汇编文件:导出子程序
现在,我们需要修改 uart.s 文件,使其不再是程序的入口点,而是作为一个包含可调用子程序的库文件。
我们需要使用 EXPORT 关键字来声明我们希望从C代码中访问的子程序。同时,可以移除或注释掉与LED初始化相关的代码,因为本教程专注于UART功能。
修改后的 uart.s 文件关键部分如下:
EXPORT uart_init
EXPORT uart_read_char
EXPORT uart_write_char
uart_init:
; ... 初始化代码 (移除了LED部分) ...
BX LR
uart_read_char:
; ... 读取字符的代码 ...
BX LR


uart_write_char:
; ... 写入字符的代码 ...
BX LR
通过 EXPORT 指令,uart_init、uart_read_char 和 uart_write_char 这三个子程序就可以被外部C文件调用了。
创建C语言主程序
接下来,我们在 main.c 文件中编写C语言代码来调用这些汇编子程序。
首先,我们需要声明这些外部函数。在C语言中,我们使用 extern 关键字来声明定义在其他文件(这里是汇编文件)中的函数。
// 声明外部汇编函数
extern void uart_init(void);
extern void uart_write_char(char c);
extern char uart_read_char(void);
声明完成后,我们就可以在 main 函数中调用它们了。一个简单的测试是初始化UART后,发送一个字符‘H’。
int main(void)
{
uart_init(); // 初始化UART
uart_write_char('H'); // 发送字符 'H'
while(1)
{
// 主循环
}
}
编译并下载程序到开发板后,打开串口调试工具,应该能看到不断输出的‘H’字符,这证明从C调用汇编子程序成功。
绑定C标准库:实现printf和scanf
为了能使用 printf 和 scanf 这样强大的格式化输入输出函数,我们需要将C标准库的底层输入输出重定向到我们的UART函数。这通过实现 _read 和 _write 系统调用(或类似的底层IO函数)来完成。
具体来说,我们需要实现 fgetc 和 fputc 函数,它们会被标准库的 printf 和 scanf 调用。
以下是 main.c 中实现重定向的关键代码:
#include <stdio.h>
#include <rt_sys.h>
// 重定向标准输入:从UART读取字符
int fgetc(FILE *f) {
char c = uart_read_char(); // 调用汇编函数读取字符
if (c == '\r') { // 如果收到回车符,转换为换行符并回显
uart_write_char(c);
c = '\n';
}
uart_write_char(c); // 回显字符
return (int)c;
}
// 重定向标准输出:向UART写入字符
int fputc(int c, FILE *f) {
uart_write_char((char)c); // 调用汇编函数写入字符
return c;
}
这段代码将标准输入(键盘)映射到从UART读取数据,将标准输出(屏幕)映射到向UART写入数据。这样,printf 函数就会自动通过UART发送字符串,scanf 函数则会从UART接收数据。
功能测试
绑定完成后,我们就可以在C代码中自由地使用 printf 和 scanf 了。下面是一个简单的测试函数:
void test_io(void) {
int num;
char str[80];
printf("Please enter a number:\n");
scanf("%d", &num);
printf("The number entered is: %d\n", num);
printf("Please type a character string:\n");
scanf("%s", str);
printf("The string entered is: %s\n", str);
printf("Hello, Assembly World!\n");
}

int main(void) {
uart_init(); // 初始化UART
test_io(); // 运行测试
while(1);
}
编译并运行程序,通过串口终端,你可以看到提示信息,输入数字和字符串后,程序会正确地回显你输入的内容,并打印出“Hello, Assembly World!”。这证明了我们的UART汇编子程序已成功与C标准输入输出库集成。

总结

本节课中我们一起学习了如何从C代码调用ARM汇编语言编写的UART子程序。我们首先创建了独立的汇编模块并导出函数,然后在C语言中声明并调用这些函数。最后,通过重定向C标准库的底层IO函数,我们实现了 printf 和 scanf 与硬件UART的绑定,从而能够以更熟悉、更强大的方式进行串口通信开发。这套方法为混合语言编程和嵌入式系统开发提供了坚实的基础。
ARM 汇编语言入门:II:05.1 系统滴答定时器概述 ⏱️
在本节课中,我们将简要介绍定时器,特别是ARM Cortex-M微控制器中普遍存在的系统滴答定时器。
系统滴答定时器简介
上一节我们介绍了中断的基本概念,本节中我们来看看一个非常重要的硬件模块——系统滴答定时器。
系统滴答定时器存在于所有ARM Cortex-M微控制器中,无论是STM32、LPC还是Tiva C系列。该定时器允许系统以固定周期触发特定操作。这个操作由内部时钟驱动,无需外部信号。例如,在一个应用程序中,我们可以使用系统滴答定时器每隔200毫秒读取一次传感器数值。
系统滴答定时器在设计实时操作系统时被广泛使用,系统软件可以定期中断应用程序软件,以监控和控制整个系统的运行。
系统滴答定时器的工作原理
系统滴答定时器是一个24位的向下计数器。它由系统时钟或内部振荡器驱动。计数器从一个初始值开始递减计数,当计数到零时,在下一个时钟周期会发生下溢,并置位一个名为“计数标志”的状态位,然后自动重载初始值并重新开始计数。
相关寄存器
编程配置系统滴答定时器时,主要涉及三个寄存器。以下是需要配置的寄存器列表:
- 系统控制与状态寄存器:用于启用定时器、查看计数标志等。
- 系统重载值寄存器:用于设置定时器计数周期的初始值。
- 系统当前值寄存器:用于读取定时器当前的计数值。
我们将在后续的代码实践中学习如何配置这些寄存器。
重载值的计算
现在,让我们看看如何计算需要加载到重载值寄存器中的数值。
假设我们希望一个动作每秒发生一次,且我们的系统时钟频率是16 MHz。我们只需将 16,000,000 - 1 加载到重载值寄存器中。这是因为在16 MHz的时钟频率下,一秒内有1600万个时钟周期。由于计数器从零开始计数,所以需要减去1。
如果希望动作每毫秒发生一次,该如何计算呢?我们知道一秒有1600万个周期,而一秒等于1000毫秒。因此,计算单毫秒内的周期数公式为:
周期数/毫秒 = (系统时钟频率) / 1000
对于16 MHz的系统,计算如下:
16,000,000 / 1,000 = 16,000
所以,要将 16,000 - 1 加载到重载值寄存器中,以实现每毫秒触发一次。
通用定时器简介
除了每个Cortex-M内核都具备的系统滴答定时器,芯片制造商(如ST、NXP、德州仪器)通常还会提供额外的通用定时器。我们可以利用这些定时器来创建精确的延时、测量事件发生的时间间隔以及实现其他复杂功能。在接下来的章节中,我们将详细探讨这些通用定时器的用法。
总结
本节课中,我们一起学习了系统滴答定时器的基本概念。我们了解到它是一个存在于所有Cortex-M芯片中的24位向下计数器,用于产生周期性中断。我们还学习了其相关的三个核心寄存器,并掌握了如何根据系统时钟频率计算定时周期对应的重载值。最后,我们简要介绍了芯片厂商提供的额外通用定时器。理解系统滴答定时器是进行嵌入式系统时间管理的基础。
021:为 SysTick 寄存器分配符号名称 🧩



在本节课中,我们将学习如何为 ARM Cortex-M 核心外设 SysTick 定时器编写驱动程序。我们将从创建新项目开始,并了解如何为相关的 SysTick 寄存器定义符号名称,以便后续进行配置和控制。

创建新项目
首先,我们需要创建一个新的项目。在 IDE 中,选择创建新项目,并选择适用于 STM32F411VET 开发板的项目模板。

项目创建完成后,将目标设备设置为 STM32F4 系列,并将主代码组重命名为 APP。接着,在 APP 组中创建一个新的源文件,命名为 main.s。

了解 SysTick 定时器
SysTick 定时器是 ARM Cortex-M 架构的一个核心外设。这意味着无论芯片制造商是谁,任何基于 ARM Cortex-M 内核的微控制器都包含此定时器。因此,我们为 SysTick 编写的驱动程序可以适用于 Cortex-M3、M4 或 M7 等多种内核。

要配置 SysTick 定时器,我们需要查阅 ARM Cortex-M4 通用用户指南,而不是特定于某款芯片的数据手册。在指南的第 218 页,可以找到关于核心外设的概述,其中包含了 SysTick 定时器的信息。

配置 SysTick 主要涉及三个寄存器:
- SysTick 控制和状态寄存器
- SysTick 重载值寄存器
- SysTick 当前值寄存器
此外,还有一个 SysTick 校准值寄存器,但在基础配置中可能不常用到。
定义寄存器地址的符号名称
为了在代码中清晰、专业地引用这些寄存器,我们将遵循行业惯例,为它们定义符号名称。通常,与嵌套向量中断控制器相关的寄存器会以 NVIC 为前缀。
以下是需要定义的寄存器及其地址:
; SysTick 寄存器基地址(位于 NVIC 地址空间)
NVIC_ST_CTRL_R EQU 0xE000E010 ; 控制和状态寄存器
NVIC_ST_RELOAD_R EQU 0xE000E014 ; 重载值寄存器
NVIC_ST_CURRENT_R EQU 0xE000E018 ; 当前值寄存器
NVIC_ST_CALIB_R EQU 0xE000E01C ; 校准值寄存器
定义控制位配置的符号名称
接下来,我们需要为 SysTick 控制寄存器中的各个功能位定义易于理解的符号名称。这有助于我们在设置寄存器时明确每个值的含义。
以下是控制寄存器中关键位的配置值定义:
; SysTick 控制寄存器位定义
NVIC_ST_CTRL_COUNT EQU 0x00010000 ; 计数标志位(第16位)
NVIC_ST_CTRL_CLK_SRC EQU 0x00000004 ; 时钟源选择:处理器时钟(第2位)
NVIC_ST_CTRL_INTEN EQU 0x00000002 ; 中断使能(第1位)
NVIC_ST_CTRL_ENABLE EQU 0x00000001 ; 使能 SysTick 计数器(第0位)
如何理解这些十六进制值?
每个十六进制数字对应4位二进制数。通过将其转换为二进制,可以清楚地看到哪个位被设置为1。例如,0x00000001 的二进制是 ...0001,表示最低位(第0位)为1。在数据手册中,可以查到该位对应“计数器使能”功能。
定义重载值
SysTick 是一个24位递减计数器。我们需要为其设置一个初始的重载值,以决定定时周期。
; SysTick 重载值(24位最大值)
NVIC_ST_RELOAD_M EQU 0x00FFFFFF ; 最大重载值 (2^24 - 1)
为 LED 配置定义符号名称(示例)
为了后续演示 SysTick 延时功能,我们还需要配置一个 LED。以下是针对特定开发板(LED 连接在 PA5 引脚)的 GPIO 配置示例:
; GPIOA 寄存器地址 (示例,需根据实际数据手册调整)
GPIOA_BASE EQU 0x40020000
GPIOA_MODER_OFFSET EQU 0x00
GPIOA_ODR_OFFSET EQU 0x14
GPIOA_MODER_R EQU GPIOA_BASE + GPIOA_MODER_OFFSET
GPIOA_ODR_R EQU GPIOA_BASE + GPIOA_ODR_OFFSET
; 引脚配置值
PIN5_OUTPUT_MODE EQU (0x01 << 10) ; 设置 PA5 为输出模式(位10:11 = 01)
LED_ON EQU (0x01 << 5) ; 设置 PA5 输出高电平,点亮 LED
LED_OFF EQU (0x00 << 5) ; 设置 PA5 输出低电平,熄灭 LED

总结
本节课中,我们一起学习了为 SysTick 定时器编写驱动程序的准备工作。我们创建了新项目,了解了 SysTick 作为 ARM 核心外设的特性,并从通用用户指南中找到了关键的寄存器地址。最重要的是,我们为这些寄存器地址以及控制寄存器中的各个配置位定义了清晰、专业的符号名称。这些定义是后续进行实际寄存器操作、实现延时和中断功能的基础。在下一节课中,我们将利用这些定义开始编写具体的汇编代码来控制 SysTick 定时器。
022:编写Systick初始化子程序 🛠️
在本节课中,我们将学习如何为ARM Cortex-M微控制器编写Systick定时器的初始化子程序。我们将从设置必要的汇编指令开始,逐步实现GPIO和Systick的初始化代码,并理解其背后的硬件操作原理。
汇编指令与程序入口
首先,我们为代码段创建空间并定义程序入口。本课程假定您已完成我的另一门课程“ARM汇编编程从零开始”,因此熟悉这些基础概念。
Area
我们处于代码区域。首先指定使用Thumb指令集,因为我们的目标平台使用Thumb模式。
Thumb
Export _main
接下来,我们定义主程序入口 _main。程序将从 GPIO_Init 开始执行,然后跳转到 Systick_Init。
_main
BL GPIO_Init
BL Systick_Init
B .
实现GPIO初始化子程序


上一节我们设置了程序流程,本节中我们来看看 GPIO_Init 子程序的具体实现。建议您暂停视频,尝试自己编写此子程序,因为我们已经多次练习过。
以下是实现步骤:
- 启用GPIOA的时钟访问:通过配置相应的寄存器来开启GPIO端口A的时钟。
- 设置引脚模式:将GPIOA的引脚5(PA5)配置为输出模式。
- 返回主程序:使用
BX LR指令从子程序返回。
GPIO_Init
; 启用GPIOA时钟访问的代码
LDR R1, =RCC_AHB2ENR ; 加载时钟使能寄存器地址
LDR R0, [R1]
ORR R0, R0, #0x01 ; 设置位0以启用GPIOA时钟
STR R0, [R1]
; 设置PA5为输出模式
LDR R1, =GPIOA_MODER ; 加载模式寄存器地址
LDR R0, [R1]
BIC R0, R0, #(3<<10) ; 清除PA5的模式位(位10和11)
ORR R0, R0, #(1<<10) ; 设置PA5为通用输出模式(01)
STR R0, [R1]
BX LR ; 返回
完成上述步骤后,我们就启用了GPIO端口A并将引脚5设置为输出引脚。
实现Systick初始化子程序
在GPIO初始化之后,我们需要配置Systick定时器。Systick是ARM Cortex-M内核的一个简单定时器,常用于产生延时。
以下是配置Systick的步骤:
- 禁用Systick定时器:在配置任何定时器之前,先禁用它是一个好习惯。
- 设置重载值:向Systick的重载寄存器(LOAD)写入一个24位的最大值,这决定了定时器溢出的时间。
- 清除当前值寄存器:通过向当前值寄存器(VAL)写入任何值(例如0)来清除它。
- 启用Systick:配置控制寄存器(CTRL),选择核心时钟源并同时启用定时器。
Systick_Init
; 1. 禁用Systick定时器
LDR R1, =SysTick_CTRL ; 加载控制寄存器地址
MOV R0, #0x0
STR R0, [R1] ; 写入0以禁用
; 2. 设置重载值 (24位最大值示例)
LDR R1, =SysTick_LOAD
LDR R0, =0x00FFFFFF ; 24位最大值
STR R0, [R1]
; 3. 清除当前值寄存器
LDR R1, =SysTick_VAL
MOV R0, #0x0
STR R0, [R1]
; 4. 启用Systick,使用核心时钟
LDR R1, =SysTick_CTRL
; 设置位2选择核心时钟,位0启用定时器
MOV R0, #(1<<2 | 1<<0)
STR R0, [R1]
BX LR ; 返回
在C语言中,遵循CMSIS标准,上述操作通常这样表示:
SysTick->CTRL = 0; // 禁用
SysTick->LOAD = 0x00FFFFFF; // 设置重载值
SysTick->VAL = 0; // 清除当前值
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk; // 启用
CMSIS(Cortex Microcontroller Software Interface Standard)为所有ARM Cortex微控制器提供统一的编程接口,简化了跨平台开发。
总结与下节预告
本节课中我们一起学习了如何编写ARM汇编的GPIO和Systick初始化子程序。我们掌握了从设置指令、启用外设时钟到配置定时器重载值和控制寄存器的完整流程。

在下一节课中,我们将实现 Systick_Wait 子程序,该程序将利用已初始化的Systick定时器来创建精确的延时功能,从而避免编写冗长的循环代码。这将是我们实现非阻塞延时的关键一步。
023:05.4. 编写与测试 SysTick 驱动 🛠️
在本节课中,我们将要学习如何实现一个基于 SysTick 定时器的延时子程序。我们将编写两个版本:一个以时钟周期为单位,另一个以10毫秒为单位。最后,我们将用这个驱动来控制 LED 的闪烁,并讨论如何将其集成到 C 代码中。
概述
SysTick 是 ARM Cortex-M 内核中的一个系统定时器,常用于产生精确的延时。本节我们将编写两个延时函数:SysTick_Wait 和 SysTick_Wait10MS。前者以 CPU 时钟周期为单位进行延时,后者则以10毫秒为单位,更方便实际应用。
理解延时单位
在编写代码之前,需要明确延时的单位。对于我的开发板(STM32F411),默认的系统时钟频率是 16 MHz。
这意味着处理器每秒可以执行 1600 万个时钟周期。由此可以计算出一个时钟周期所花费的时间:
公式: 1 个周期时间 = 1 / 时钟频率
代入数值:
1 个周期时间 = 1 / 16,000,000 Hz = 62.5 纳秒
因此,当我们向 SysTick_Wait 函数传递参数 1000 时,意味着延时 1000 个时钟周期,即 1000 * 62.5 ns = 62.5 微秒。
实现 SysTick_Wait 函数
这个函数以时钟周期数为参数,通过配置 SysTick 的重载值并等待其计数到零来实现延时。
以下是该函数的核心步骤:
- 设置重载值:将传入的延时周期数减1后,写入 SysTick 的重载寄存器。减1是因为计数器从重载值递减到0。
- 启动并等待:使能 SysTick 计数器,然后循环检查其状态标志位,直到计数完成。
- 返回:延时结束后,函数返回。
对应的汇编代码实现如下:
SysTick_Wait
; R0 已包含要延时的周期数
LDR R1, =NVIC_ST_RELOAD ; 加载重载寄存器的地址
SUB R0, R0, #1 ; 重载值 = 周期数 - 1
STR R0, [R1] ; 将值存入重载寄存器
LDR R1, =NVIC_ST_CTRL ; 加载控制寄存器的地址
MOV R2, #0x05 ; 设置控制寄存器:使能计数器,使用处理器时钟
STR R2, [R1] ; 启动 SysTick
CheckFlag
LDR R3, [R1] ; 读取控制寄存器的值到 R3
ANDS R3, R3, #0x00010000 ; 检查 COUNT 标志位(第16位)
BEQ CheckFlag ; 如果标志位为0(未计数到0),则继续循环检查
; 计数完成,清除使能位(可选,或由下次设置覆盖)
MOV R2, #0x00
STR R2, [R1]
BX LR ; 返回调用者
代码解释:
NVIC_ST_RELOAD和NVIC_ST_CTRL是 SysTick 相关寄存器的符号地址。ANDS指令中的S后缀表示操作后更新 APSR 状态标志。BEQ指令在零标志置位时跳转,这里用于循环等待 COUNT 标志变为1。
实现 SysTick_Wait10MS 函数
以10毫秒为单位的延时函数更实用。我们需要先计算10毫秒对应的时钟周期数。
公式: 周期数 = 延时时间 * 时钟频率
对于10毫秒延时:
周期数 = 0.01 秒 * 16,000,000 Hz = 160,000
我们在代码开头定义这个常量:
TEN_MS_DELAY EQU 160000 ; 10毫秒对应的周期数
接下来是 SysTick_Wait10MS 函数的实现逻辑:
- 参数检查:检查调用者传入的参数(存储在 R0)。如果参数为0,则直接返回。
- 循环延时:将参数值保存到 R4,然后循环调用
SysTick_Wait(TEN_MS_DELAY),每次循环后参数减1,直到为0。
以下是该函数的汇编代码:
SysTick_Wait10MS
PUSH {R4, LR} ; 保存 R4 和链接寄存器 LR
MOVS R4, R0 ; 将延时次数从 R0 移到 R4,并设置标志位
BEQ Done ; 如果传入参数为0,直接跳转到结束
LDR R0, =TEN_MS_DELAY ; 将10毫秒的周期数加载到 R0
DelayLoop
BL SysTick_Wait ; 调用周期延时函数,延时10毫秒
SUBS R4, R4, #1 ; 剩余次数减1,并更新标志位
BHI DelayLoop ; 如果结果大于0,继续循环
Done
POP {R4, PC} ; 恢复 R4,并将 LR 弹出到 PC 以返回
代码解释:
PUSH/POP用于在子程序调用前后保存和恢复寄存器,这是遵守 ARM 调用规范的重要步骤。MOVS中的S后缀使我们能通过后续的BEQ判断参数是否为0。BHI(无符号大于则跳转)指令在SUBS结果大于0时继续循环。
在汇编中测试驱动
上一节我们实现了延时函数,本节我们将在汇编主程序中测试它们,实现一个 LED 闪烁的效果。
我们假设 LED 连接在某个 GPIO 引脚上,并且已经初始化。以下是控制 LED 闪烁的代码逻辑:
- 将 LED 引脚设置为高电平(点亮)。
- 调用
SysTick_Wait10MS(100)延时 1 秒(100 * 10ms)。 - 将 LED 引脚设置为低电平(熄灭)。
- 再次调用
SysTick_Wait10MS(100)延时 1 秒。 - 循环以上步骤,形成闪烁。
核心汇编测试代码示例如下:
LDR R1, =LED_ON_CODE ; 加载点亮 LED 的值
LDR R2, =GPIOA_ODR ; 加载 GPIO 数据输出寄存器地址
STR R1, [R2] ; 点亮 LED
MOV R0, #100 ; 设置延时参数为 100(即 1000 毫秒)
BL SysTick_Wait10MS ; 延时 1 秒
LDR R1, =LED_OFF_CODE ; 加载熄灭 LED 的值
STR R1, [R2] ; 熄灭 LED
MOV R0, #100 ; 再次设置延时参数
BL SysTick_Wait10MS ; 再次延时 1 秒
B . ; 无限循环(实际应跳回开始)

调试与常见问题

在首次测试时,LED 可能没有闪烁。我们需要检查代码:
- 逻辑错误:最初的代码只完成了“点亮 -> 延时”,缺少“熄灭 -> 延时”的步骤,无法形成闪烁。修正后需包含完整的“亮-灭”循环。
- 寄存器保存:在
SysTick_Wait10MS函数中,如果修改了 R4 和 LR 寄存器,必须在函数开头和结尾使用PUSH/POP进行保存和恢复,否则主程序的运行状态会被破坏。 - 符号常量:检查
NVIC_ST_CTRL等寄存器地址符号定义是否正确。在检查 COUNT 标志位时,确保使用了正确的位掩码(如0x00010000)。


修正所有问题后,重新编译下载程序,即可观察到 LED 以 1Hz 的频率(亮1秒,灭1秒)稳定闪烁。
练习:在 C 代码中调用汇编驱动


本节课的最后,我们留下一个练习。为了更贴近实际开发,你需要:
- 创建一个新的工程。
- 将我们编写的所有 SysTick 汇编函数(
SysTick_Wait,SysTick_Wait10MS以及必要的初始化代码)放入一个独立的汇编文件(例如systick.s)。 - 创建一个 C 文件(
main.c),在其中声明这些汇编函数。 - 在
main函数中,调用SysTick_Wait10MS来控制 LED 的闪烁。
以下是 C 代码中声明和调用汇编函数的示例:

// 在 C 文件中声明汇编函数
extern void SysTick_Wait10MS(unsigned int delay_in_10ms_units);

int main(void) {
// 初始化 GPIO 和 SysTick(假设已有其他函数完成)
GPIO_Init();
SysTick_Init();
while(1) {
LED_On(); // 点亮 LED
SysTick_Wait10MS(100); // 延时 1 秒
LED_Off(); // 熄灭 LED
SysTick_Wait10MS(100); // 延时 1 秒
}
}
通过这个练习,你可以掌握如何将高效的汇编语言驱动与可读性更强的 C 语言应用代码结合起来,这是嵌入式开发中的常见模式。
总结
本节课中我们一起学习了:
- SysTick 延时原理:基于系统时钟频率,将时间转换为处理器周期。
- 编写了
SysTick_Wait函数:实现了以时钟周期为单位的精确延时。 - 编写了
SysTick_Wait10MS函数:实现了以10毫秒为单位的更实用的延时函数,并包含了参数检查和循环逻辑。 - 在汇编环境中测试:通过完整的“点亮-延时-熄灭-延时”循环,成功驱动 LED 闪烁。
- 提出了集成练习:鼓励你将汇编驱动与 C 语言主程序结合,这是迈向实际项目开发的重要一步。


你已经成功实现了一个硬件定时器驱动,并理解了其底层运作机制。尝试完成 C 语言调用的练习,将使你的技能更加全面。
024:通用定时器概述 ⏱️
在本节课中,我们将对通用定时器进行概述。我们将了解定时器的基本概念、分类方式,并学习如何根据定时器的位宽计算其最大延时能力。
定时器与计数器的区别
在深入探讨之前,我们先来区分一下“定时器”和“计数器”。这两个术语经常被混用,但从技术上讲,它们的区别在于时钟源的不同。
- 如果时钟源是内部的(例如RC电路、PLL或晶振),我们称之为定时器。
- 如果时钟源是外部的(例如从其他设备馈入CPU的时钟信号),我们则称之为计数器。
这是两者唯一的区别。在本教程中,我们将不严格区分这两个术语,统一使用“定时器”。
定时器的用途
定时器(或计数器)在嵌入式系统中用途广泛,主要包括:
- 创建延时:用于在程序中产生精确的时间间隔。
- 计数事件:可以统计传感器触发阈值、按钮被按下的次数,或任何硬件/软件事件发生的次数。
- 测量事件间隔:计算两个事件之间经过的时间。
定时器的分类
定时器有多种分类方式,以下是两种常见的分类:
1. 单次定时器与周期定时器
- 单次定时器:定时器从设定值开始计数,一旦达到超时值,便停止计数。
- 周期定时器:定时器达到超时值后,会自动重置并重新开始计数,如此循环往复。
2. 递减计数器与递增计数器
- 递减计数器:定时器从一个预设值开始,向下计数至0。
- 递增计数器:定时器从0开始,向上计数至一个预设值。
理解定时器的位宽
我们常听到定时器被描述为16位、32位等。这个“位宽”决定了定时器能够计数的最大值,从而决定了它能产生的最大延时。
核心概念:一个N位定时器的最大计数值是 2^N。
例如:
- 16位定时器的最大值为:
2^16 = 65536 - 32位定时器的最大值为:
2^32 = 4294967296
如何计算最大延时
要计算定时器能产生的最大延时,我们需要知道系统的时钟频率。假设我们的系统运行在16 MHz。
第一步:计算单个时钟周期的时间
如果系统时钟是16 MHz,意味着1秒内可以执行1600万个周期。因此,执行一个周期所需的时间是:
周期时间 = 1 / 频率
对于16 MHz:
周期时间 = 1 / 16,000,000 Hz = 62.5 纳秒 (ns)
第二步:计算最大延时
最大延时等于最大计数值乘以单个周期的时间:
最大延时 = 最大计数值 × 周期时间
让我们看几个例子:
例1:16位定时器
- 最大计数值:
65536 - 周期时间:
62.5 ns - 最大延时:
65536 × 62.5 ns = 4.096 毫秒 (ms)
这意味着,一个纯粹的16位定时器无法产生超过约4毫秒的延时。如果需要5秒的延时,必须通过软件多次调用定时器函数来实现。
例2:32位定时器
- 最大计数值:
4294967296 - 周期时间:
62.5 ns - 最大延时:
4294967296 × 62.5 ns ≈ 268.435 秒
可以看到,32位定时器能产生的延时能力远超16位定时器。
预分频器的作用
上一节我们看到了16位定时器的延时能力有限。为了扩展其范围,许多通用定时器配备了预分频器。
预分频器可以将输入时钟频率进行分频,从而降低定时器的计数速度。一个8位的预分频器可以将分频系数设置为1到256之间的值。
效果计算:当16位定时器搭配一个8位预分频器时,其等效位宽增加了。
- 等效最大计数值:
2^(16+8) = 2^24 = 16,777,216 - 最大延时:
16,777,216 × 62.5 ns = 1.048 秒
通过添加预分频器,原本最大延时仅4毫秒的16位定时器,现在可以产生超过1秒的延时。
总结
在本节课中,我们一起学习了通用定时器的核心知识。我们首先区分了定时器与计数器的技术差异,了解了定时器在创建延时、计数和测量时间间隔方面的用途。接着,我们探讨了定时器的不同分类方式,如单次/周期定时器和递增/递减计数器。
最重要的是,我们掌握了如何根据定时器的位宽和系统时钟频率来计算其最大延时能力,并理解了预分频器在扩展定时器范围中的关键作用。这些概念是后续进行具体定时器编程和应用的坚实基础。
025:为GPTM寄存器分配符号名称 🧩
在本节课中,我们将学习如何为STM32的通用定时器(GPTM)开发驱动程序。我们将以定时器2为例,配置其产生1赫兹的延时。核心步骤包括查找外设基地址、寄存器偏移量,并为相关寄存器分配易于理解的符号名称,以便后续编程。
上一节我们介绍了项目创建的基本流程,本节中我们来看看如何为定时器外设的寄存器定义符号名称。
查找外设基地址与总线连接


首先,需要确定定时器2的基地址。根据数据手册,可以找到其基地址。同时,还需查明定时器2连接在哪个总线上,以便启用其时钟。通过查阅数据手册中的模块框图,可以确认定时器2连接到APB1总线。
定义寄存器符号名称
为了在代码中清晰、方便地访问寄存器,我们将为定时器2的关键寄存器以及APB1总线的使能寄存器定义符号名称。这些名称将基地址与寄存器偏移量结合起来。
以下是需要定义的关键寄存器及其作用:
- RCC_APB1ENR: APB1外设时钟使能寄存器。用于启用定时器2的时钟。
- TIM2_BASE: 定时器2的基地址。
- TIM2_PSC: 预分频器寄存器。用于对输入时钟进行分频。
- TIM2_ARR: 自动重装载寄存器。设置计数器的溢出值。
- TIM2_CR1: 控制寄存器1。用于使能或禁用定时器。
- TIM2_SR: 状态寄存器。包含更新中断标志(UIF),用于判断定时是否完成。
- TIM2_CNT: 计数器寄存器。存储当前的计数值。
这些符号名称的定义公式如下:
Symbolic_Name EQU Base_Address + Register_Offset
例如,定时器2控制寄存器1的定义为:
TIM2_CR1 EQU TIM2_BASE + 0x00
计算延时配置值
我们的目标是配置定时器2产生1秒(1Hz)的延时。假设微控制器默认系统时钟为16MHz。
- 配置预分频器 (PSC): 将16MHz的时钟分频。例如,设置
PSC = 1600 - 1,则得到的新时钟频率为:CK_CNT = 16,000,000 / 1600 = 10,000 Hz - 配置自动重装载寄存器 (ARR): 设置计数器溢出的值。若设置
ARR = 10,000 - 1,则定时器溢出时间(即延时)为:
通用计算公式为:Timeout = (1 / CK_CNT) * ARR = (1 / 10,000) * 10,000 = 1 secondTimeout (Delay) = (PSC_Value + 1) * (ARR_Value + 1) / System_Clock
因此,我们定义配置常量:
PSC_CONF EQU 1600 - 1 ; 预分频值
ARR_CONF EQU 10000 - 1 ; 自动重装载值
CNT_CONF EQU 0 ; 清零计数器
CR1_CONF EQU 1 ; 使能定时器 (CR1寄存器的CEN位)
其他相关配置
此外,还需要定义控制GPIO(例如连接LED的引脚)和APB1使能寄存器的位掩码:
TIM2_ENABLE_BIT EQU (1 << 0) ; APB1ENR寄存器中定时器2的使能位
LED_ENABLE_BIT EQU (1 << 0) ; AHB1ENR寄存器中GPIOA的使能位
LED_MODE_BIT EQU (1 << 10) ; 设置GPIO引脚为输出模式(MODER5)
总结

本节课中我们一起学习了为STM32通用定时器(GPTM)寄存器分配符号名称的完整过程。我们掌握了如何通过数据手册和参考手册查找外设基地址、寄存器偏移量以及总线连接信息。我们定义了定时器2操作所需的所有关键寄存器符号名,并推导了配置1Hz延时所需的预分频器和自动重装载器数值的计算方法。这些符号名称和常量将作为下一节课编写实际定时器驱动代码的基础,使我们的程序更易读、更易维护。养成良好的技术文档查阅习惯,是成为优秀嵌入式开发者的关键一步。
026:通用定时器驱动开发 🚀

在本节课中,我们将学习如何为 ARM 微控制器编写一个通用定时器(Timer2)的驱动程序。我们将通过汇编语言实现定时器的初始化、配置以及一个简单的延时功能,最终控制 LED 以 1Hz 的频率闪烁。

汇编程序结构

首先,我们引入一些标准的汇编指令和定义,以减少代码量并提高可读性。

.syntax unified
.cpu cortex-m4
.thumb

这些是我们的汇编指令。.syntax unified 表示使用统一的汇编语法,.cpu cortex-m4 指定目标 CPU 架构,.thumb 指示使用 Thumb 指令集。

接着,我们跳转到主初始化程序。

b main


GPIO 初始化
上一节我们介绍了程序的基本结构,本节中我们来看看如何初始化 GPIO 以控制 LED。
我们将引入一个之前课程中反复使用的 GPIO 初始化子程序。

bl init_gpio

这个子程序几乎在每一课中都被使用过,因此我们无需再次详细解释其内部逻辑。它负责配置 LED 所连接的 GPIO 引脚为输出模式。


定时器初始化
在配置好 GPIO 之后,本节我们将重点实现定时器(Timer2)的初始化。
我们创建一个名为 timer2_init 的子程序。

timer2_init:

1. 使能时钟访问
首先,我们需要使能 Timer2 的时钟访问。在 C 语言中,这通常通过设置 RCC_APB1ENR 寄存器的相应位来完成。

以下是实现步骤:
- 将
RCC_APB1ENR寄存器的地址加载到寄存器 R0。 - 将该地址处的值读入寄存器 R1。
- 使用 OR 操作设置
TIM2EN位(Timer2 使能位)。 - 将修改后的值写回寄存器。

ldr r0, =RCC_APB1ENR
ldr r1, [r0]
orr r1, r1, #(1 << 0) // 假设 TIM2EN 是位0
str r1, [r0]
2. 设置预分频器


接下来,我们配置预分频器寄存器以设置定时器的计数频率。
在 C 语言中,这对应 TIM2_PSC 寄存器。我们将一个预定义的配置值写入该寄存器。

ldr r0, =TIM2_PSC
movw r1, #PSC_CNF // 假设 PSC_CNF 是预定义的16位值
strh r1, [r0]

3. 设置自动重装载寄存器

然后,我们设置自动重装载寄存器,它决定了定时器的溢出周期。


在 C 语言中,这对应 TIM2_ARR 寄存器。我们将一个预定义的计数值(例如 10000)写入该寄存器。
ldr r0, =TIM2_ARR
movw r1, #ARR_CNF // 假设 ARR_CNF 是预定义的16位值
strh r1, [r0]

4. 清零计数器

在启动定时器前,需要将当前计数值清零。

在 C 语言中,这对应 TIM2_CNT 寄存器。我们简单地将 0 写入该寄存器。

ldr r0, =TIM2_CNT
mov r1, #0
str r1, [r0]
5. 使能定时器

最后,我们通过配置控制寄存器 1 来启动定时器。

在 C 语言中,这对应 TIM2_CR1 寄存器。我们设置相应的控制位(例如 CEN 位)来使能定时器。
ldr r0, =TIM2_CR1
ldr r1, =CR1_CNF // 假设 CR1_CNF 包含了使能位的值
str r1, [r0]
bx lr // 返回

至此,定时器初始化完成。

实现延时功能
定时器初始化后,我们需要一个方法来等待定时器溢出事件。本节我们将创建一个延时子程序。
我们创建一个名为 wait 的子程序,其功能类似于 delay。

wait:
该子程序的核心是轮询状态寄存器,等待更新中断标志被置位。
在 C 语言中,逻辑是:while(!(TIM2_SR & TIM2_SR_UIF));。

以下是汇编实现步骤:
- 将状态寄存器地址加载到 R1。
- 循环读取该寄存器的值到 R2。
- 检查
UIF位是否被置位(即与TIM2_SR_UIF进行与操作后结果非零)。 - 如果结果为 0,则跳回循环开始继续等待。
ldr r1, =TIM2_SR
wait_loop:
ldr r2, [r1]
ands r2, r2, #TIM2_SR_UIF
beq wait_loop // 如果 Z 标志位为 1 (结果为0),则继续循环
当循环退出,意味着定时器已溢出。此时,我们需要手动清除 UIF 标志位,以便下次判断。
在 C 语言中,通过向 TIM2_SR 寄存器的对应位写 0 来清除标志。
以下是清除标志的汇编代码:
ldr r3, [r1] // 再次读取状态寄存器
bic r3, r3, #TIM2_SR_UIF // 清除 UIF 位
str r3, [r1] // 写回状态寄存器
bx lr // 返回
LED 闪烁测试程序
现在,我们将使用初始化好的定时器和延时功能来控制 LED 闪烁。
我们创建一个名为 led_blink 的子程序。


以下是 LED 闪烁的逻辑步骤:

- 加载 GPIO 端口位设置/清除寄存器的地址到 R4。
- 将“复位”值(关闭 LED)写入寄存器,关闭 LED。
- 调用
wait子程序进行延时。 - 将“置位”值(打开 LED)写入寄存器,打开 LED。
- 再次调用
wait子程序进行延时。 - 跳回步骤 2,形成循环。
led_blink:
ldr r4, =GPIO_BSRR
mov r5, #BSRR_RESET_VAL
str r5, [r4] // 关闭 LED
bl wait
mov r5, #BSRR_SET_VAL
str r5, [r4] // 打开 LED
bl wait
b led_blink // 无限循环


主程序流程


最后,我们在主程序中按顺序调用初始化函数和闪烁函数。

main:
bl init_gpio
bl timer2_init
bl led_blink
程序编译并下载到开发板后,可以观察到 LED 以 1Hz 的频率闪烁。

课后练习


本节课中我们一起学习了如何用汇编语言编写定时器驱动。为了巩固知识,请完成以下练习:
- 尝试用 C 语言编写一个
main.c文件。 - 创建一个
general_purpose_timer.s汇编文件,将本课的wait等子程序放入其中。 - 在 C 代码中声明并调用这些汇编子程序,实现相同的 LED 闪烁功能。
- 这类似于我们在 UART 章节中演示的混合编程方法。


如果在练习中遇到任何困难,可以随时寻求帮助。
总结

本节课中,我们系统地完成了一个 ARM 通用定时器驱动的开发。我们从使能时钟开始,逐步配置了预分频器、自动重装载寄存器、计数器,并最终启动了定时器。随后,我们实现了一个基于状态轮询的延时函数,并将其应用于 LED 闪烁控制中。通过本课的学习,你应该对 ARM 定时器的底层寄存器操作和汇编编程有了更深入的理解。
027:ADC概述 🧠
在本节课中,我们将要学习什么是模数转换器,即ADC。我们将了解它的基本概念、工作原理以及分辨率的重要性。
什么是ADC?
上一节我们介绍了数字系统的基础,本节中我们来看看连接模拟世界与数字世界的桥梁——ADC。
模数转换器是数据采集领域应用最广泛的设备之一。数字计算机使用二进制或离散值,但在物理世界中,一切信号都是模拟或连续的。温度、压力、湿度、速度等,都是我们日常处理的物理量。
一个称为传感器的设备将物理量转换为电信号。传感器将物理量转换为电压或电流。用于产生电输出的传感器被称为传感器。温度、速度、压力、光等众多自然物理量的传感器,其输出通常是电压,有时是电流。
因此,我们需要一个模数转换器来将模拟信号转换为数字,以便微控制器能够读取和处理这些数字。
ADC分辨率的概念
现在我们来谈谈ADC分辨率的概念。一个ADC具有n位分辨率,其中n可以是8、10、12、16甚至24。所以你会听到人们说这个ADC是10位的、12位的或16位的。
分辨率越高,ADC提供的步进尺寸就越小。步进尺寸是ADC能够检测到的最小变化量。
以下表格展示了给定ADC位数下的步数以及步进尺寸:
| ADC位数 | 步数 (2^n) | 步进尺寸 (V_ref=5V时) |
|---|---|---|
| 8位 | 256 | 19.53 mV |
| 10位 | 1024 | 4.88 mV |
| 12位 | 4096 | 1.22 mV |
| 16位 | 65,536 | 0.076 mV |
例如,一个8位ADC有256个步数。我们通过公式 2^8 计算得出256。要计算步进尺寸,我们需要考虑参考电压。假设参考电压是5V,我们只需计算 5V / 256,结果约为19.53毫伏。
参考电压 V_ref
V_ref是用于参考电压的输入电压。大多数微控制器都有V_ref引脚。我们可以将自己的输入电压连接到这个引脚,或者直接使用微控制器的工作电压。
连接到V_ref引脚的电压,连同ADC芯片的分辨率,共同决定了步进尺寸,正如你在上表中看到的那样。假设我们的V_ref是5伏特,ADC分辨率是8位,那么我们最终得到的步进尺寸就是19.53毫伏。

本节课中我们一起学习了模数转换器的基本概念,包括其作用、分辨率的意义以及参考电压的影响。以上是对ADC的简要概述。在接下来的课程中,我们将从编程的角度看看它们是如何工作的。
028:07.2 为相关ADC寄存器分配符号名称



在本节课中,我们将学习如何为STM32开发板的ADC(模数转换器)外设编写汇编语言驱动程序。我们将从创建一个新项目开始,并逐步为所有必要的ADC寄存器分配符号名称,为后续的驱动代码编写打下基础。

创建新项目

首先,我们需要在集成开发环境中创建一个新的项目。
- 选择
Project->New Project。 - 创建一个名为
ADC的文件夹。 - 将项目也命名为
ADC。 - 选择我们的目标开发板型号:
STM32F411VET。 - 在配置中,选择正确的核心(Core)和启动文件(Startup)。
- 在项目结构中,创建一个名为
app的源文件组。 - 右键点击
app组,选择Add New Item,创建一个名为main.s的汇编源文件。
至此,项目的基本框架已经搭建完成。
复用已有的LED配置

在本次实验中,我们将使用一个ADC引脚和一个LED。我们将从ADC引脚读取模拟值,并通过一个条件语句,在模拟值超过特定阈值时点亮LED。


因此,我们需要复用之前为LED配置好的寄存器符号名称。以下是相关的定义:
RCC_BASE_AHBENR EQU 0x40023830 ; AHB外设时钟使能寄存器基址偏移
GPIOA_BASE EQU 0x40020000 ; GPIOA端口基址
GPIOA_MODER_OFFSET EQU 0x00 ; 模式寄存器偏移量
GPIOA_ODR_OFFSET EQU 0x14 ; 输出数据寄存器偏移量
MODER5_OUT EQU (1 << 10) ; 设置PA5为输出模式
LED_ON EQU (1 << 5) ; 设置PA5输出高电平,点亮LED
LED_OFF EQU (0 << 5) ; 设置PA5输出低电平,熄灭LED
确定ADC的时钟总线

要使用一个外设,首先需要查看其系统框图,以确定它是连接到AHB总线还是APB总线,从而知道需要通过哪个总线来使能其时钟。

根据STM32F411的数据手册中的系统框图,ADC1 模块连接在 APB2 总线上。因此,我们需要通过APB2外设时钟使能寄存器来为ADC1提供时钟。
定义ADC相关寄存器符号名称
接下来,我们将根据参考手册,查找并定义配置ADC所需的所有关键寄存器的基址、偏移量和相关配置位。


1. 使能ADC时钟
首先,定义APB2外设时钟使能寄存器及其用于ADC1的使能位。
; APB2外设时钟使能寄存器
RCC_APB2ENR_OFFSET EQU 0x44
RCC_APB2ENR EQU RCC_BASE + RCC_APB2ENR_OFFSET
ADC1_EN EQU (1 << 8) ; 设置第8位为1,使能ADC1时钟
2. ADC外设基地址
从数据手册的内存映射表中,找到ADC1的基地址。
; ADC1外设基地址
ADC1_BASE EQU 0x40012000
3. ADC状态寄存器 (ADC_SR)


状态寄存器用于检查模数转换是否完成。我们关注其中的 EOC (End Of Conversion) 位。
; ADC状态寄存器
ADC1_SR_OFFSET EQU 0x00
ADC1_SR EQU ADC1_BASE + ADC1_SR_OFFSET
ADC1_SR_EOC_FLAG EQU (1 << 1) ; EOC标志位,转换完成时由硬件置1
4. ADC控制寄存器2 (ADC_CR2)
此寄存器用于启用/禁用ADC模块,以及控制转换的启动方式。

; ADC控制寄存器2
ADC1_CR2_OFFSET EQU 0x08
ADC1_CR2 EQU ADC1_BASE + ADC1_CR2_OFFSET
CR2_ADON_EN EQU 0x00000001 ; 第0位,置1以启用ADC1
CR2_SWSTART EQU (1 << 30) ; 软件启动转换
CR2_EXTEN_DISABLED EQU (0 << 28) ; 禁用外部触发,使用软件触发
5. ADC规则序列寄存器 (ADC_SQR1, ADC_SQR3)
这些寄存器用于配置转换序列的长度和顺序。
; 规则序列寄存器1 - 设置转换序列长度
ADC1_SQR1_OFFSET EQU 0x2C
ADC1_SQR1 EQU ADC1_BASE + ADC1_SQR1_OFFSET
SQR1_LEN_1 EQU (0 << 20) ; 设置转换序列长度为1
; 规则序列寄存器3 - 设置转换序列中的第一个通道
ADC1_SQR3_OFFSET EQU 0x34
ADC1_SQR3 EQU ADC1_BASE + ADC1_SQR3_OFFSET
SQR3_FIRST_CH1 EQU (1 << 0) ; 设置转换序列从通道1开始
在STM32中,ADC通道号通常与引脚号对应。例如,PA1 对应通道1,PB15 对应通道15。
6. ADC数据寄存器 (ADC_DR)
转换完成后,模拟量转换得到的数字值将存储在此寄存器中。
; ADC数据寄存器
ADC1_DR_OFFSET EQU 0x4C
ADC1_DR EQU ADC1_BASE + ADC1_DR_OFFSET
7. 配置ADC引脚模式
用于ADC的GPIO引脚必须设置为模拟模式。这通过配置GPIO的模式寄存器实现。
; 配置PA1为模拟模式 (用于ADC)
MODER1_ANALOG EQU (0x3 << 2) ; 设置MODER1[3:2] = 0b11,即模拟模式
总结
本节课中,我们一起学习了为STM32的ADC外设编写汇编驱动程序的准备工作。我们完成了以下关键步骤:
- 创建了新的项目框架。
- 复用了已有的LED GPIO配置。
- 通过查阅数据手册和参考手册,确定了ADC1连接在APB2总线上。
- 系统性地查找并定义了所有必需的ADC寄存器符号名称,包括:
- 时钟使能寄存器 (
RCC_APB2ENR) - ADC外设基地址 (
ADC1_BASE) - 状态寄存器 (
ADC1_SR) 及转换完成标志 (EOC) - 控制寄存器2 (
ADC1_CR2) 及其启用、软件触发位 - 规则序列寄存器 (
ADC1_SQR1,ADC1_SQR3) 用于配置转换 - 数据寄存器 (
ADC1_DR) 用于读取转换结果 - GPIO模式配置,将ADC引脚设置为模拟模式。
- 时钟使能寄存器 (

现在,我们已经为ADC外设分配了所有相关的符号名称,为下一节课实际编写驱动程序代码做好了充分准备。在下一节中,我们将利用这些定义,初始化ADC并实现模拟信号的读取。
029:编写ADC驱动程序 🚀
在本节课中,我们将学习如何为ARM微控制器编写一个ADC(模数转换器)驱动程序。我们将从设置必要的汇编指令开始,逐步实现GPIO和ADC的初始化,最后完成一个读取ADC数据的子程序。通过这个过程,你将掌握在汇编语言中操作外设寄存器的基本模式。
汇编指令与程序结构
首先,我们定义程序的基本结构。这包括设置代码段、指定指令集和定义入口点。
.syntax unified
.cpu cortex-m4
.thumb
.section .text
.align 2
.global main
.syntax unified: 指定使用统一的汇编语法。.cpu cortex-m4: 指明目标CPU为Cortex-M4。.thumb: 指示使用Thumb指令集。.section .text: 定义代码段。.align 2: 确保代码按字(4字节)对齐。.global main: 将main标签声明为全局符号,作为程序入口。


程序从main标签开始执行。在main子程序中,我们将依次调用初始化GPIO和ADC的子程序。
main:
BL gpio_init
BL adc1_init
GPIO初始化子程序
上一节我们介绍了程序框架,本节中我们来看看如何初始化GPIO端口。gpio_init子程序负责启用GPIOA端口的时钟,并配置特定引脚的模式。
以下是实现gpio_init子程序的步骤:
-
启用GPIOA时钟:通过设置
RCC_AHB1ENR寄存器的对应位来启用GPIOA的时钟。LDR R0, =RCC_AHB1ENR LDR R1, [R0] ORR R1, R1, #GPIOA_EN STR R1, [R0]LDR R0, =RCC_AHB1ENR: 将RCC_AHB1ENR寄存器的地址加载到R0。LDR R1, [R0]: 读取该寄存器的当前值到R1。ORR R1, R1, #GPIOA_EN: 将R1的值与常量GPIOA_EN(代表GPIOA使能位)进行按位或操作,结果存回R1。STR R1, [R0]: 将R1的新值写回RCC_AHB1ENR寄存器。
-
配置引脚模式:配置PA5为输出模式(连接LED),PA1为模拟模式(连接ADC传感器)。
LDR R0, =GPIOA_MODER LDR R1, [R0] ORR R1, R1, #MODER5_OUTPUT STR R1, [R0] LDR R0, =GPIOA_MODER LDR R1, [R0] ORR R1, R1, #MODER1_ANALOG STR R1, [R0]- 操作模式与启用时钟类似,只是操作的寄存器和常量不同。
MODER5_OUTPUT和MODER1_ANALOG是预先定义好的常量,对应GPIO模式寄存器中设置特定引脚模式的位域。
- 操作模式与启用时钟类似,只是操作的寄存器和常量不同。
-
子程序返回:使用
BX LR指令从子程序返回。
ADC1初始化子程序
完成了GPIO的配置后,接下来我们需要初始化ADC模块。adc1_init子程序将启用ADC1的时钟,并配置其工作参数。
以下是配置ADC1的详细步骤:
-
启用ADC1时钟:通过
RCC_APB2ENR寄存器启用ADC1的时钟。LDR R0, =RCC_APB2ENR LDR R1, [R0] ORR R1, R1, #ADC1_EN STR R1, [R0] -
选择软件触发:在ADC控制寄存器2(
ADC1_CR2)中设置使用软件触发转换。LDR R0, =ADC1_CR2 LDR R1, [R0] ORR R1, R1, #CR2_SWTRIG STR R1, [R0] -
设置转换序列:配置序列寄存器(
ADC1_SQR3),指定转换从哪个通道开始(例如通道1)。LDR R0, =ADC1_SQR3 MOV R1, #SQR3_CH1 STR R1, [R0]- 注意这里使用了
MOV指令直接赋值,因为我们通常需要覆盖整个寄存器值来设置通道号。
- 注意这里使用了
-
配置采样时间:通过序列寄存器1(
ADC1_SQR1)配置通道的采样时间。LDR R0, =ADC1_SQR1 MOV R1, #SQR1_CFG STR R1, [R0] -
使能ADC:最后,再次操作控制寄存器2以启用ADC模块本身。
LDR R0, =ADC1_CR2 LDR R1, [R0] ORR R1, R1, #CR2_ADON STR R1, [R0]
ADC数据读取子程序
初始化工作完成后,我们就可以读取ADC转换的数据了。adc1_read子程序负责启动一次转换,等待转换完成,然后读取结果。
以下是adc1_read子程序的实现逻辑:
-
启动转换:通过设置
ADC1_CR2寄存器中的相应位来启动一次软件转换。LDR R0, =ADC1_CR2 LDR R1, [R0] ORR R1, R1, #CR2_SWSTART STR R1, [R0] -
等待转换完成:循环检查状态寄存器(
ADC1_SR)中的转换完成标志位(EOC)。wait: LDR R0, =ADC1_SR LDR R1, [R0] ANDS R1, R1, #SR_EOC CMP R1, #0 BEQ waitANDS指令执行按位与操作并更新状态标志。CMP R1, #0和BEQ wait:如果结果为零(标志位未置位),则跳回wait标签继续等待。
-
读取数据:转换完成后,从数据寄存器(
ADC1_DR)中读取转换结果。LDR R0, =ADC1_DR LDR R0, [R0] BX LR- 读取的值存放在R0寄存器中,作为子程序的返回值。
与C语言代码对比
为了帮助你理解,以下展示了关键汇编操作对应的C语言代码片段:
- 启用GPIOA时钟
RCC->AHB1ENR |= GPIOA_EN; - 配置PA5为输出模式
GPIOA->MODER |= MODER5_OUTPUT; - 启用ADC1时钟
RCC->APB2ENR |= ADC1_EN; - 设置软件触发和使能ADC
ADC1->CR2 |= CR2_SWTRIG | CR2_ADON; - 设置转换通道
ADC1->SQR3 = SQR3_CH1; - 启动转换并等待完成
可以看到,汇编语言中的“加载-计算-存储”模式,在C语言中通常被简化为一句直接操作寄存器的语句。ADC1->CR2 |= CR2_SWSTART; while(!(ADC1->SR & SR_EOC)) { // 等待 } value = ADC1->DR;
总结与展望
本节课中我们一起学习了如何为ARM Cortex-M4微控制器编写一个完整的ADC驱动程序。我们从设置汇编环境开始,逐步实现了:
- GPIO端口的初始化,包括时钟启用和引脚模式配置。
- ADC模块的初始化,涵盖时钟、触发方式、转换序列和使能模块。
- 一个用于启动转换、等待完成并读取数据的子程序。
整个流程的核心是 “加载-计算-存储” 范式:将寄存器地址加载到寄存器,读取其值并进行位操作(如OR、AND),最后将结果存回寄存器。这是底层硬件编程的基础。

在下一节课中,我们将利用这个ADC驱动程序,编写一个控制LED的子程序。我们将设定一个模拟传感器数据的阈值,当采集到的数据超过该阈值时点亮LED,否则熄灭LED,从而创建一个简单的响应系统。
030:07.4 编写与测试ADC驱动程序 🔧

在本节课中,我们将学习如何编写一个ADC驱动程序,并通过LED来测试其功能。我们将创建一个简单的阈值检测系统,当传感器读数超过预设值时,LED将被点亮。

概述 📋
本节教程将指导您完成以下步骤:
- 定义传感器阈值。
- 编写一个LED控制子程序,根据ADC读数与阈值的比较结果来控制LED的开关。
- 在主循环中整合ADC读取和LED控制逻辑。
- 构建代码、下载到开发板并进行测试。

定义传感器阈值
首先,我们需要定义一个阈值常量。STM32微控制器的ADC是12位的,这意味着其最大读数值为4095。
我们将创建一个名为 SENSOR_THRESH 的常量,并将其值设置为3000。当传感器读数超过此值时,将触发一个动作(点亮LED)。
代码示例:
SENSOR_THRESH EQU 3000
编写LED控制子程序
上一节我们介绍了如何读取ADC值。本节中,我们来看看如何根据该值来控制LED。
我们将创建一个名为 LED_CONTROL 的子程序。其逻辑是:比较存储在寄存器R0中的ADC读数与我们定义的阈值。如果读数大于阈值,则跳转到 LED_ON 标签点亮LED;如果小于阈值,则跳转到 LED_OFF 标签关闭LED。
代码示例:
LED_CONTROL:
LDR R1, =SENSOR_THRESH ; 将阈值常量加载到R1
CMP R0, R1 ; 比较ADC值(R0)与阈值(R1)
BGT LED_ON ; 如果 R0 > R1,跳转到LED_ON
BLT LED_OFF ; 如果 R0 < R1,跳转到LED_OFF
BX LR ; 返回调用处
LED_ON:
LDR R5, =GPIOA_BSRR ; 加载GPIOA端口置位寄存器地址到R5
MOV R1, #GPIO_PIN_5_SET ; 设置点亮LED(引脚5)的值
STR R1, [R5] ; 将值写入BSRR寄存器以点亮LED
BX LR ; 返回
LED_OFF:
LDR R5, =GPIOA_BSRR ; 加载GPIOA端口置位寄存器地址到R5
MOV R1, #GPIO_PIN_5_RESET ; 设置熄灭LED(引脚5)的值
STR R1, [R5] ; 将值写入BSRR寄存器以熄灭LED
BX LR ; 返回
以下是关键步骤的说明:
CMP R0, R1:比较指令,用于设置条件标志。BGT/BLT:条件分支指令,根据比较结果进行跳转。LDR R5, =GPIOA_BSRR:将GPIOA_BSRR寄存器的内存地址加载到R5。STR R1, [R5]:将R1中的值存储到R5所指向的内存地址(即GPIOA_BSRR寄存器)。



整合主程序循环


现在,我们需要在主程序中调用这些子程序。程序流程如下:初始化GPIO和ADC,然后进入一个无限循环,在循环中不断读取ADC值并据此控制LED。

代码示例:
BL GPIO_INIT ; 初始化GPIO
BL ADC_INIT ; 初始化ADC
main_loop:
BL ADC1_READ ; 读取ADC值,结果存入R0
BL LED_CONTROL ; 根据R0的值控制LED
B main_loop ; 跳回循环开始,实现类似C语言中while(1)的效果

构建与调试

代码编写完成后,需要进行构建和调试。以下是可能遇到的常见错误及解决方法:

- 语法错误:例如遗漏等号(
=)或使用了未定义的符号。仔细检查所有常量和标签的拼写。 - 寄存器名称错误:确保引用的外设寄存器名称(如
GPIOA_BSRR,ADC1_CR2)与芯片手册完全一致。 - 链接错误:确保所有引用的子程序或标签都已正确定义。
在集成开发环境中正确设置调试选项(如选择调试器、设置复位和运行模式)后,即可将程序下载到开发板。
硬件连接与测试 🧪
为了测试程序,需要将电位器连接到开发板:
- 电位器中间引脚:连接到微控制器的 PA1 引脚(ADC输入通道)。
- 电位器一端:连接到 3.3V。
- 电位器另一端:连接到 GND。
测试时,旋转电位器。当ADC读数超过3000(阈值)时,LED应点亮;当读数低于3000时,LED应熄灭。
总结 🎯

本节课中我们一起学习了:
- 如何为ADC读数定义一个阈值常量。
- 如何编写一个包含条件判断的汇编子程序来控制LED。
- 如何将ADC读取和LED控制逻辑整合到一个主循环中。
- 如何构建程序、解决常见错误,并在实际硬件上进行测试。



通过这个实践,我们掌握了使用ARM汇编语言处理模拟信号并驱动数字输出的基本流程。在下一课中,我们将探讨如何从C语言中调用这些用汇编编写的驱动程序,以实现更复杂的应用。
031:从C代码调用ADC子程序 📞
在本节课中,我们将学习如何从一个C语言程序中调用我们之前编写的ADC(模数转换器)汇编子程序。我们将创建一个新的项目,整合C语言和汇编代码,并最终在开发板上运行和调试。


上一节我们介绍了如何用纯汇编语言编写ADC驱动程序。本节中我们来看看如何让C语言主程序来调用这些汇编函数。

创建新项目与文件
首先,我们需要在Keil uVision中创建一个新的项目。

- 选择
Project->New uVision Project。 - 为项目创建一个新文件夹,例如命名为
CallFromC。 - 将项目命名为
ADC。 - 在设备选择界面,选择你的开发板型号(例如 STM32F411VE)。
- 在管理运行时环境的对话框中,选择核心和启动文件,然后点击确定。
项目创建完成后,我们需要添加两个源文件。

以下是需要添加的文件列表:
- ADC.S:用于存放我们的汇编语言ADC驱动函数。
- main.c:用于编写调用汇编函数的C语言主程序。
移植汇编代码
接下来,我们需要从上一课的纯汇编项目中将驱动程序代码复制过来。
- 打开上一课的ADC项目,找到主要的汇编源文件(例如
main.S)。 - 复制该文件中的所有内容。
- 在新项目的
ADC.S文件中粘贴这些代码。 - 由于本次C语言项目将负责程序入口和主循环,我们需要删除原汇编代码中用于测试的循环和入口点。
- 确保使用
.global(或EXPORT)指令,将需要被C语言调用的函数声明为全局符号。需要导出的函数通常包括:ADC_InitADC_ReadLED_Control(用于指示ADC状态的函数)
修改后的 ADC.S 文件核心部分应类似于以下结构:
.global ADC_Init
.global ADC_Read
.global LED_Control
ADC_Init:
; 初始化代码...
BX LR
ADC_Read:
; 读取ADC值的代码...
BX LR
LED_Control:
; 控制LED的代码...
BX LR
在C代码中声明外部函数
现在,切换到 main.c 文件。为了让C编译器知道这些汇编函数的存在,我们需要使用 extern 关键字来声明它们。
C语言中的函数声明需要指定返回类型和参数。对于我们的汇编函数:
ADC_Init和LED_Control不返回任何值,也没有参数。ADC_Read返回一个32位的无符号整数值,没有参数。
因此,在 main.c 文件顶部,我们可以这样声明:
#include <stdint.h> // 用于 uint32_t 类型
// 声明外部汇编函数
extern void ADC_Init(void);
extern uint32_t ADC_Read(void);
extern void LED_Control(void);
编写C语言主程序
声明好函数后,我们就可以在 main 函数中调用它们了。程序逻辑通常如下:
- 初始化系统(如GPIO)。
- 调用
ADC_Init()初始化ADC模块。 - 进入一个无限循环。
- 在循环内调用
ADC_Read()获取传感器值。 - 根据读取的值,可以调用
LED_Control()或其他逻辑进行处理(例如点亮或熄灭LED)。
- 在循环内调用
一个简单的 main 函数示例如下:
int main(void) {
uint32_t sensor_value; // 用于存储ADC读取值的变量
// 初始化(假设系统时钟等已由启动文件配置好)
ADC_Init();
while(1) {
sensor_value = ADC_Read(); // 读取ADC值
LED_Control(); // 根据ADC值控制LED(逻辑在汇编函数内)
// 可以在此添加延时或其他处理
}
}
构建、下载与调试
代码编写完成后,即可进行构建和下载。
- 点击
Build按钮编译项目。确保没有错误。 - 在
Options for Target->Debug中配置调试器(如ST-Link)。 - 在
Flash Download选项卡中勾选Reset and Run,以便下载后自动运行。 - 点击
Download按钮将程序烧录到开发板。
程序运行后,我们可以进入调试模式来观察变量值。
以下是调试步骤:
- 启动调试会话。
- 在
Watch窗口中添加sensor_value变量进行监视。 - 运行程序。当你旋转连接在ADC通道上的电位器时,可以在
Watch窗口中实时看到sensor_value数值的变化。 - 同时,开发板上的LED也会根据ADC值的变化而改变状态(例如,电位器转到一端时LED亮,另一端时LED灭)。
实验扩展
本实验以电位器作为ADC输入源。你可以轻松地将其替换为其他模拟传感器,如光敏电阻或温度传感器,连接方式和代码调用方法完全相同。只需确保传感器输出在开发板ADC的输入电压范围(通常是0-3.3V)内即可。

本节课中我们一起学习了如何搭建一个混合编程的项目,从C语言主程序中调用ARM汇编语言编写的硬件驱动子程序。我们完成了新项目的创建、汇编代码的移植、C语言对外部函数的声明与调用,以及最终的调试验证。掌握这种方法,可以在保持C语言开发效率的同时,利用汇编代码实现对硬件的精确、高效控制。
032:ARM Cortex-M通用输入输出模块概述 🧠
在本节课中,我们将要学习ARM Cortex-M微控制器中通用输入输出模块的基础知识。我们将了解GPIO与特殊功能IO的区别,认识微控制器的引脚命名规则,并深入探讨其内部总线结构,特别是AHB和APB总线。
内存、CPU与输入输出
内存用于存储CPU需要处理的代码和数据。输入输出端口则被CPU用来访问外部输入和输出设备。
在微控制器中,输入输出分为两种类型:通用输入输出和特殊功能输入输出。
通用与特殊功能输入输出
通用输入输出,通常称为GPIO,用于连接诸如LED、开关、LCD、键盘、直流电机等通用设备。
特殊功能输入输出则具有指定的专用功能,例如:
- 模数转换
- 数模转换
- 定时器
- 通用异步收发传输器
因此,在微控制器上,同一个引脚可以被配置为GPIO或特殊功能IO。我们需要告诉微控制器,我们希望将此引脚用于特殊功能,还是用于其常规的通用目的。
端口与引脚命名
在微控制器中,引脚被分组到不同的端口,例如端口A、端口B、端口C等。每个端口包含一定数量的引脚。

例如,引脚 PA1 代表端口A的第1个引脚。同理,引脚 PE3 代表端口E的第3个引脚。
当我们要访问这些引脚时,必须告诉开发环境具体的端口和引脚编号。所有微控制器都遵循这种命名约定。

然而,当使用像Arduino和mbed这样的平台时,你不需要指明端口。Arduino和mbed的封装层已经处理了所有这些细节,并将这些引脚重新命名为简单的名称,例如P1到P30,而无需指明P1到P5属于端口A,P6到P10属于端口B等。在Arduino或mbed中编码时,你无需担心这些。
但是,在进行裸机编程时,你需要知道要访问的特定引脚所在的端口及其编号。

总线结构:AHB与APB
大多数Cortex-M微控制器拥有两种类型的总线:高级外设总线和高级高性能总线。
高级外设总线通常写作 APB。使用APB访问外设至少需要两个时钟周期。
高级高性能总线通常写作 AHB。使用AHB访问外设甚至可以在一个时钟周期内完成。
我们将查看来自三家不同厂商的四款ARM Cortex微控制器的数据手册,会发现这些总线存在于所有微控制器中,无论是来自NXP、德州仪器还是意法半导体。

本课程的目的之一是使你能够浏览任何正在使用的微控制器的数据手册和用户手册,以便能够独立处理外设和解决问题,从而提升你的嵌入式开发技能。
查看数据手册中的总线结构
现在,让我们看看如何找到特定微控制器的数据手册,并查看其总线结构。我们以意法半导体的STM32 F4系列为例。
在数据手册中,我们可以找到系统框图。在框图中,可以看到标记为 AHB 的总线。任何连接到这条总线的外设都可以使用AHB总线进行访问,例如GPIO端口A、B、C、D、H等。
但并非所有外设都相同。框图中还有标记为 APB1 和 APB2 的总线。在编程时,我们需要记住,不能尝试使用AHB总线来初始化像UART2这样的外设,因为UART没有连接到AHB总线,我们只能使用APB总线来操作UART。
这就是STM32 F4的总线结构。正如之前提到的,本课程的一个目标是让你掌握浏览数据手册和参考手册的技能,以便在提升嵌入式开发技能的过程中解决自己的问题。
接下来,我们将看到另外两款同样基于ARM架构但来自不同制造商的开发板,你会发现它们也都具有AHB和APB总线。
其他厂商微控制器示例

以下是德州仪器TM4C123G开发板的数据手册框图示例。同样,可以看到 AHB 总线和 APB 总线。只有箭头连接到AHB总线的外设才能使用它。例如,名为SSI的外设不能使用AHB,因为它的箭头指向APB。同样,ADC、模拟比较器等也指向APB。而DMA则能够同时使用AHB和APB总线。
接下来是NXP的LPC1768微控制器用户手册中的简化框图。图中显示了总线,在他们的系统中有一个多层AHB矩阵。所有连接到该矩阵的外设都使用AHB总线,因此它们被称为AHB从机组1。另一些外设使用APB总线,并且图中存在一个AHB到APB的桥接器。
这些是Cortex-M的核心标准,存在于所有Cortex处理器中。
总结

本节课中我们一起学习了ARM Cortex-M微控制器的GPIO模块基础。我们明确了GPIO与特殊功能IO的用途与区别,掌握了微控制器引脚按端口分组的命名规则。更重要的是,我们深入了解了微控制器内部的两种关键总线:AHB 和 APB,并通过查看不同厂商(ST、TI、NXP)的数据手册,验证了这种总线结构是ARM Cortex-M架构的通用标准。理解这些概念是进行底层硬件编程和高效利用微控制器资源的基础。
033:为相关GPIO输出寄存器分配符号名称
在本节课中,我们将学习如何为德州仪器TM4C123G微控制器开发一个GPIO输出驱动程序。我们将从查阅数据手册开始,了解硬件连接和寄存器地址,然后为这些寄存器创建符号名称,为后续编写驱动程序打下基础。


查阅硬件文档

在编写驱动程序之前,必须查阅用户手册和数据手册,以了解各种端口和外设的地址。
以下是查阅文档的步骤:
- 打开用户手册,找到开发板的原理图部分。
- 在用户手册第20页,可以找到用户LED和按键的连接信息。
- 红色LED连接到PF1引脚。
- 蓝色LED连接到PF2引脚。
- 绿色LED连接到PF3引脚。
- 用户按键2连接到PF0引脚。
- 用户按键1连接到PF4引脚。
- 所有待测试的组件都位于GPIO端口F。
理解时钟门控机制
现代微控制器通常采用时钟门控机制以降低功耗。这意味着默认情况下,微控制器各个模块(如GPIO端口、ADC、UART等)的时钟访问是关闭的。要使用某个外设,必须首先启用其时钟访问。
对于本实验,我们需要使用GPIO端口F,因此必须启用端口F的时钟访问。
了解系统总线
在ARM Cortex-M架构中,通常有两种系统总线用于访问外设:
- APB总线:高级外设总线。
- AHB总线:高级高性能总线。
AHB总线通常能提供更快的外设访问速度。根据数据手册第48页的框图,GPIO模块可以同时连接到APB和AHB总线。在本教程中,我们将使用默认的APB连接。
GPIO核心寄存器
无论微控制器的制造商或架构如何,GPIO模块通常至少包含两个核心寄存器:
- 方向寄存器:用于配置引脚为输入或输出模式。
- 数据寄存器:用于写入(输出模式)或读取(输入模式)引脚的数据。
此外,还可能包含用于配置复用功能、数字使能、驱动强度等可选寄存器。
寄存器地址映射
在ARM架构中,外设的寄存器通常以基地址加偏移量的方式组织。同一类型的外设(如GPIOA, GPIOB等)其同类寄存器(如方向寄存器)的偏移量是相同的。区分不同外设寄存器的关键在于它们各自不同的基地址。
例如,要获得GPIOF数据寄存器的完整地址,需要将GPIOF的基地址与数据寄存器的偏移量相加。
查找并定义寄存器地址
现在,我们开始从数据手册中查找所需的基地址和偏移量,并为它们创建易于理解的符号名称。
系统控制基地址与时钟使能寄存器
首先,我们需要系统控制模块的基地址,以便访问其中的时钟门控寄存器来启用GPIOF的时钟。
在数据手册中查找系统控制基地址和RCGCGPIO寄存器(运行模式时钟门控控制寄存器)的偏移量。
以下是相关定义:
; 系统控制模块基地址
SYSCTL_BASE EQU 0x400FE000
; RCGCGPIO寄存器在系统控制模块内的偏移量
SYSCTL_RCGCGPIO_OFFSET EQU 0x608
; RCGCGPIO寄存器的完整地址(基地址 + 偏移量)
SYSCTL_RCGCGPIO_R EQU SYSCTL_BASE + SYSCTL_RCGCGPIO_OFFSET
RCGCGPIO寄存器的第5位控制着GPIO端口F的时钟。需要将该位置1以启用时钟。
GPIO端口F基地址与相关寄存器
接下来,查找GPIO端口F(APB总线)的基地址,以及方向寄存器、数据寄存器和数字使能寄存器的偏移量。
以下是相关定义:
; GPIO端口F的基地址 (APB)
GPIOF_APB_BASE EQU 0x40025000
; 方向寄存器(GPIODIR)偏移量
GPIOF_DIR_OFFSET EQU 0x400
; 方向寄存器完整地址
GPIOF_DIR_R EQU GPIOF_APB_BASE + GPIOF_DIR_OFFSET
; 数据寄存器(GPIODATA)偏移量
; 注意:数据寄存器的访问方式较特殊,其有效偏移量为0x3FC
GPIOF_DATA_OFFSET EQU 0x3FC
; 数据寄存器完整地址
GPIOF_DATA_R EQU GPIOF_APB_BASE + GPIOF_DATA_OFFSET
; 数字使能寄存器(GPIODEN)偏移量
GPIOF_DEN_OFFSET EQU 0x51C
; 数字使能寄存器完整地址
GPIOF_DEN_R EQU GPIOF_APB_BASE + GPIOF_DEN_OFFSET
定义位掩码常量
最后,为了方便操作,我们定义一些位掩码常量,用于设置特定的位。
以下是相关定义:
; 用于在RCGCGPIO寄存器中启用GPIOF时钟的掩码(第5位)
GPIOF_CLK_EN EQU (1 << 5)
; 对应LED引脚的掩码
; 红色LED在PF1
LED_RED_PIN EQU (1 << 1)
; 蓝色LED在PF2
LED_BLUE_PIN EQU (1 << 2)
; 绿色LED在PF3
LED_GREEN_PIN EQU (1 << 3)
总结

本节课中,我们一起学习了为TM4C123G微控制器的GPIO输出驱动程序准备符号定义的过程。我们首先通过用户手册确定了硬件连接,然后理解了时钟门控的概念。接着,我们查阅数据手册,找到了系统控制基地址、GPIO端口F基地址以及方向、数据和数字使能寄存器的偏移量,并最终为这些地址和常用的位掩码创建了清晰的符号名称。这些定义是后续编写实际汇编代码来控制LED的基础。在下一节课中,我们将利用这些定义来实现完整的GPIO驱动功能。
034:编写 GPIO 输出驱动 🚦
在本节课中,我们将学习如何为 ARM 微控制器编写一个基础的 GPIO 输出驱动程序。我们将从设置时钟访问开始,到配置引脚为输出模式,最后实现控制 LED 点亮的功能。通过动手实践,你将理解如何用汇编语言直接操作硬件寄存器。
概述
本节教程将指导你使用 ARM 汇编语言,为 GPIO 端口编写一个完整的输出驱动程序。我们将逐步实现初始化、引脚配置和输出控制,并最终在开发板上点亮一个 LED。
编写 GPIO 初始化子程序
首先,我们需要设置代码区域和入口点。使用 AREA 指令定义一个只读的代码段,并使用 THUMB 模式,因为 Cortex-M 系列处理器通常运行在此模式下。
AREA |.text|, CODE, READONLY, ALIGN=2
THUMB
ENTRY
EXPORT __main
接下来,我们进入 __main 函数,并首先跳转到名为 GPIO_INIT 的子程序,以初始化 GPIO 端口。
__main
B GPIO_INIT
初始化 GPIO 时钟和引脚
上一节我们设置了程序入口,本节中我们来看看如何具体初始化 GPIO。这个过程主要分为三步:启用端口时钟、设置引脚方向为输出、以及数字使能该引脚。
以下是 GPIO_INIT 子程序的具体步骤:
-
启用端口时钟:我们需要访问系统控制模块的 RCGCGPIO 寄存器来启用 GPIO 端口的时钟。在 C 语言中,这类似于
SYSCTL->RCGCGPIO |= PORTF_ENABLE;。在汇编中,我们通过加载寄存器地址、读取-修改-写回的方式来实现。GPIO_INIT LDR R1, =SYSCTL_RCGCGPIO_R ; 加载 RCGCGPIO 寄存器地址到 R1 LDR R0, [R1] ; 读取当前寄存器值到 R0 ORR R0, R0, #GPIO_PORTF_EN ; 将端口 F 的使能位与 R0 进行或操作 STR R0, [R1] ; 将修改后的值写回寄存器 NOP ; 等待时钟稳定 NOP -
设置引脚方向为输出:接下来,我们需要配置目标引脚(例如 PF1)为输出模式。这通过操作 GPIO 端口的方向寄存器(GPIODIR)完成。我们将把 PF1 对应的位(我们定义为
LED_RED)设置为 1。LDR R1, =GPIO_PORTF_DIR_R ; 加载 GPIO 端口 F 方向寄存器地址 LDR R0, [R1] ; 读取当前值 ORR R0, R0, #LED_RED ; 设置 PF1 引脚为输出模式 STR R0, [R1] ; 写回寄存器 -
数字使能引脚:最后,我们必须通过数字使能寄存器(GPIODEN)来激活引脚的数字功能。操作方式与设置方向类似。
LDR R1, =GPIO_PORTF_DEN_R ; 加载数字使能寄存器地址 LDR R0, [R1] ; 读取当前值 ORR R0, R0, #LED_RED ; 使能 PF1 引脚的数字功能 STR R0, [R1] ; 写回寄存器 BX LR ; 子程序返回
编写 LED 控制子程序
初始化完成后,我们就可以控制 LED 了。现在我们来编写一个用于点亮 LED 的子程序。
以下是 LED_ON 子程序的实现:
-
点亮 LED:要点亮连接在 PF1 上的 LED,我们需要向 GPIO 端口的数据寄存器(GPIODATA)的对应位写入 1。在汇编中,我们直接将代表 LED 的值存储到数据寄存器的地址。
LED_ON LDR R1, =GPIO_PORTF_DATA_R ; 加载 GPIO 端口 F 数据寄存器地址 MOV R0, #LED_RED ; 将 LED_RED 的值(即 PF1 为高)移动到 R0 STR R0, [R1] ; 将值写入数据寄存器,点亮 LED BX LR ; 子程序返回
构建主循环并测试
现在,我们将所有部分组合起来。在主程序中,先调用初始化子程序,然后在一个无限循环中不断调用 LED_ON 子程序,使 LED 保持常亮。


__main
BL GPIO_INIT ; 调用 GPIO 初始化
MainLoop
BL LED_ON ; 调用点亮 LED 的子程序
B MainLoop ; 跳回循环开始,形成无限循环
ALIGN ; 对齐指令
END ; 程序结束
代码编写完成后,需要进行编译、下载到开发板并调试。确保在调试器设置中选择了正确的目标,并执行“Reset and Run”。如果 LED 没有点亮,应检查代码,特别是硬件寄存器的基地址是否正确。例如,系统控制模块的基地址可能需要从芯片数据手册中直接获取,而非从其他模块推导。


总结
本节课中我们一起学习了如何用 ARM 汇编语言编写一个完整的 GPIO 输出驱动。我们从定义代码段开始,逐步实现了启用时钟、配置引脚模式和数字功能,最后通过写数据寄存器控制了 LED 的输出状态。这个过程清晰地展示了如何通过直接操作内存映射寄存器来控制微控制器外设的基本方法。
035:控制 GPIO 输出 🔄

在本节课中,我们将学习如何通过编写汇编代码,让连接到 GPIO 引脚上的 LED 灯实现闪烁效果。我们将创建一个简单的延时函数,并利用它来控制 LED 的亮灭周期。
上一节我们介绍了如何初始化 GPIO 并点亮 LED。本节中我们来看看如何让 LED 闪烁起来,这需要我们在“点亮”和“熄灭”操作之间加入延时。
创建延时函数
首先,我们需要创建一个延时函数。需要说明的是,本节创建的延时函数并非高精度延时,其延时长度取决于特定的微控制器配置和编译器。后续在定时器章节,我们将学习如何创建精确的延时。
以下是创建延时符号常量的步骤:
ONE_SEC EQU 0x003D0900:定义一个名为ONE_SEC的常量,其值约为1秒的循环计数(此值针对特定时钟频率和编译器校准)。HALF_SEC EQU 0x001E8480:定义半个秒的常量,其值为ONE_SEC的一半。
接下来,我们编写延时子程序 delay。该子程序通过递减寄存器 R3 中的值来实现延时。
delay:
SUBS R3, R3, #1 ; 将R3的值减1,并设置状态标志位
BNE delay ; 如果结果不为零(Z标志未置位),则跳回`delay`标签继续循环
BX LR ; 返回调用处
实现 LED 闪烁功能
现在,我们可以利用 GPIO 驱动和延时函数来编写 LED 闪烁的主逻辑。
以下是 led_blink 子程序的具体步骤:
- 点亮 LED:将对应 LED 引脚(例如引脚1)的数据寄存器位置位。
- 延时:将
ONE_SEC常量加载到R3寄存器,然后调用delay函数。 - 熄灭 LED:清除对应 LED 引脚的数据寄存器位。
- 再次延时:重复步骤2的延时操作。
- 循环:跳回步骤1,形成无限循环,使 LED 持续闪烁。
核心操作对应的代码逻辑如下:
- 点亮LED:
LDR R1, =GPIOA_ODR后执行ORR R0, R0, #(1<<1)。 - 熄灭LED:
LDR R1, =GPIOA_ODR后执行BIC R0, R0, #(1<<1)。 - 调用延时:
LDR R3, =ONE_SEC后执行BL delay。
将代码编译并下载到开发板后,即可观察到 LED 以大约1秒的间隔规律闪烁。

总结
本节课中我们一起学习了如何通过汇编语言控制 GPIO 输出实现 LED 闪烁。我们创建了一个基础的延时函数,并组织了“点亮-延时-熄灭-延时”的循环逻辑。虽然当前的延时方法不够精确,但它很好地演示了硬件控制的基本流程。在后续关于定时器的课程中,我们将探索更精准的定时方法。
036:为相关GPIO输入寄存器分配符号名称
在本节课中,我们将学习如何将微控制器的GPIO引脚配置为输入模式,并重点介绍如何为相关的寄存器地址和状态值分配易于理解的符号名称。这是编写清晰、可维护的嵌入式代码的关键一步。

上一节我们介绍了GPIO输出的基本配置。本节中我们来看看如何配置GPIO为输入,并处理开发板上特定的硬件细节。
首先,我们需要复制上一个项目,并在此基础上继续。新项目命名为“GPIO输出输入”。实际上,我们将学习如何将GPIO引脚配置为输入。
将GPIO配置为输入非常简单。我们只需操作方向寄存器,将其相应位设置为输入模式即可。
但对于我们的实验,我们将使用开发板上提供的两个开关:SW1连接在PF4引脚,SW2连接在PF0引脚。这些开关需要启用内部上拉电阻,以确保在未按下时引脚处于确定的高电平状态。为此,我们需要使用上拉寄存器。
我们需要获取上拉寄存器的地址偏移量。根据数据手册,GPIO端口F的上拉寄存器偏移量是 0x510。
以下是创建相关寄存器符号地址的步骤:
-
定义上拉寄存器偏移量:
GPIOF_PUR_OFFSET EQU 0x510 -
计算上拉寄存器实际地址:
GPIOF_PUR_R EQU GPIOF_BASE + GPIOF_PUR_OFFSET
接下来,我们需要处理一个特殊情况:PF0引脚(SW2)默认是锁定的。这是因为PF0连接到了非屏蔽中断引脚,为了防止误操作,设计者将其锁定。要使用它,我们必须先通过特定的解锁密钥来解锁,并提交更改。
因此,我们需要访问锁定寄存器和提交寄存器。
以下是创建锁定和提交相关寄存器符号的步骤:
-
定义锁定寄存器偏移量:
GPIOF_LOCK_OFFSET EQU 0x520 -
计算锁定寄存器实际地址:
GPIOF_LOCK_R EQU GPIOF_BASE + GPIOF_LOCK_OFFSET -
定义提交寄存器偏移量:
GPIOF_CR_OFFSET EQU 0x524 -
计算提交寄存器实际地址:
GPIOF_CR_R EQU GPIOF_BASE + GPIOF_CR_OFFSET -
定义解锁密钥值(该值可在数据手册中找到):
LOCK_KEY EQU 0x4C4F434B ; 密钥 "LOCK" 的十六进制ASCII码
现在,我们为两个开关对应的引脚位创建符号名称,使代码更具可读性。
以下是开关引脚位定义:
SW1对应 PF4 引脚。SW2对应 PF0 引脚。
需要特别注意的是,开发板上的开关是低电平有效的。这意味着:
- 默认状态下(开关未按下),引脚读数为高电平(1)。
- 当开关被按下时,引脚被拉低到低电平(0)。
为了在代码中清晰判断开关状态,我们为不同的读取结果定义有意义的符号名。
以下是开关状态符号定义:
SW1_PRESSED EQU 0x01: 当读取结果R0为0x01时,表示SW1被按下。SW2_PRESSED EQU 0x10: 当读取结果R0为0x10时,表示SW2被按下。BOTH_PRESSED EQU 0x00: 当读取结果R0为0x00时,表示两个开关都被按下。NO_PRESS EQU 0x11: 当读取结果R0为0x11时,表示没有开关被按下。
至此,我们已经为所有必要的寄存器地址和状态值创建了清晰的符号名称。这为下一节课实际编写输入检测代码打下了坚实的基础。

本节课中我们一起学习了如何为GPIO输入配置所需的寄存器分配符号名称,包括数据方向寄存器、上拉寄存器、锁定寄存器和提交寄存器。我们还定义了开关引脚和状态的常量,并理解了低电平有效开关的工作原理。在下一课,我们将利用这些定义来编写具体的代码,读取开关状态并做出响应。
037:编写 GPIO 输入驱动 🚦




在本节课中,我们将学习如何为 ARM 微控制器编写 GPIO 输入驱动程序。我们将扩展已有的 GPIO 初始化函数,使其能够处理输入引脚(如按钮),并编写一个读取按钮状态的子程序,根据不同的按钮按下情况来控制 LED 灯。

概述
上一节我们完成了 GPIO 输出的初始化。本节中,我们将在此基础上,解锁并配置 GPIO 输入引脚,并实现一个读取按钮状态并根据状态控制 LED 的逻辑。
扩展 GPIO 初始化函数

我们的 GPIO 初始化子程序目前只初始化了时钟和 PF1(红色 LED)作为输出。现在,我们需要解锁 PF0 端口,并将绿色 LED 和两个开关(按钮)配置好。

解锁 PF0 端口
某些 GPIO 端口(如 PF0)在默认情况下是锁定的,需要先解锁才能配置。以下是解锁 PF0 的步骤:
- 将解锁密钥(
LOCK_KEY)加载到寄存器。 - 将该密钥写入 GPIO 端口 F 的锁定寄存器(
GPIOF_LOCK_R)。
对应的汇编代码如下:
LDR R1, =GPIOF_LOCK_R @ 加载锁定寄存器地址到 R1
LDR R0, =LOCK_KEY @ 加载解锁密钥到 R0
STR R0, [R1] @ 将密钥写入锁定寄存器,解锁 PF0


提交更改
解锁后,需要向提交寄存器写入特定值以确认更改。我们将 0xFF 写入 GPIO 端口 F 的提交寄存器(GPIOF_CR_R)。
LDR R1, =GPIOF_CR_R @ 加载提交寄存器地址到 R1
MOV R0, #0xFF @ 将立即数 0xFF 移动到 R0
STR R0, [R1] @ 将 0xFF 写入提交寄存器
配置引脚方向

方向寄存器(GPIOF_DIR_R)用于设置引脚是输入(0)还是输出(1)。红色 LED(PF1)已设置为输出。现在,我们需要将绿色 LED 对应的引脚也设置为输出。开关引脚默认即为输入(0),因此无需额外设置。

我们使用按位或(OR)操作来设置绿色 LED 的位,而不影响其他已配置的位:
LDR R1, =GPIOF_DIR_R @ 加载方向寄存器地址到 R1
LDR R0, [R1] @ 读取当前方向寄存器的值到 R0
ORR R0, R0, #LED_GREEN @ 将绿色 LED 对应的位设置为 1(输出)
STR R0, [R1] @ 将新值写回方向寄存器
数字功能使能

数字使能寄存器(GPIOF_DEN_R)用于启用引脚的数字功能。我们需要使能红色 LED、绿色 LED 以及两个开关引脚。

我们可以通过按位或(OR)操作一次性设置所有需要的位:
LDR R1, =GPIOF_DEN_R @ 加载数字使能寄存器地址到 R1
MOV R0, #(LED_RED | LED_GREEN | SWITCH1 | SWITCH2) @ 将所有需要使能的位合并到 R0
STR R0, [R1] @ 将值写入数字使能寄存器
配置上拉电阻
对于输入引脚(开关),我们通常需要启用内部上拉电阻,以确保引脚在未按下时保持稳定的高电平。这通过上拉寄存器(GPIOF_PUR_R)实现。

以下是启用两个开关上拉电阻的代码:
LDR R1, =GPIOF_PUR_R @ 加载上拉寄存器地址到 R1
LDR R0, [R1] @ 读取当前上拉寄存器的值到 R0
ORR R0, R0, #SWITCH1 @ 启用开关1的上拉电阻
ORR R0, R0, #SWITCH2 @ 启用开关2的上拉电阻
STR R0, [R1] @ 将新值写回上拉寄存器

至此,GPIO 初始化函数 gpio_init 就完成了对所有必要引脚(两个输出 LED 和两个输入开关)的配置。

编写 GPIO 读取子程序


现在我们需要一个子程序来读取开关的状态。我们将创建一个名为 gpio_read 的子程序。
读取数据寄存器
开关的状态存储在 GPIO 端口 F 的数据寄存器(GPIOF_DATA_R)中。我们读取这个寄存器的值。
LDR R1, =GPIOF_DATA_R @ 加载数据寄存器地址到 R1
LDR R0, [R1] @ 将数据寄存器的值读入 R0
检查开关状态
读取到的值包含了所有引脚的状态。我们使用按位与(AND)操作来屏蔽出我们关心的开关位(SWITCH1 和 SWITCH2)。
AND R0, R0, #(SWITCH1 | SWITCH2) @ 屏蔽出两个开关的状态位
执行此操作后,R0 寄存器中只有对应开关的位是有效的。如果开关被按下(引脚接地),对应的位将是 0;如果未按下(由上拉电阻拉高),对应的位将是 1。
最后,使用 BX LR 指令返回,此时 R0 中包含了开关的状态信息。
在主程序中处理输入

在主程序循环中,我们首先调用 gpio_init 进行初始化,然后进入一个循环,不断读取开关状态并作出响应。
读取并比较状态


在循环中,我们调用 gpio_read 子程序,然后将返回值与预定义的“按下”状态进行比较。
loop:
BL gpio_read @ 调用读取子程序,结果在 R0 中
CMP R0, #SWITCH1_PRESSED @ 比较 R0 是否等于开关1被按下的状态
BEQ switch1_pressed @ 如果相等,跳转到开关1处理程序
CMP R0, #SWITCH2_PRESSED @ 比较 R0 是否等于开关2被按下的状态
BEQ switch2_pressed @ 如果相等,跳转到开关2处理程序
B loop @ 否则,继续循环

处理开关1按下

当检测到开关1被按下时,我们跳转到 switch1_pressed 标签处。在这里,我们将红色 LED 的值加载到 R0,然后调用一个通用的 led_on 子程序来点亮它。
switch1_pressed:
MOV R0, #LED_RED @ 将红色LED的位模式放入R0
BL led_on @ 调用点亮LED的子程序
B loop @ 返回主循环
处理开关2按下


类似地,处理开关2按下,点亮绿色 LED。
switch2_pressed:
MOV R0, #LED_GREEN @ 将绿色LED的位模式放入R0
BL led_on @ 调用点亮LED的子程序
B loop @ 返回主循环

实现 LED 点亮子程序
led_on 子程序负责将传入的 R0 值(包含要点亮的 LED 位模式)写入数据寄存器,从而控制相应的 GPIO 引脚输出高电平,点亮 LED。
led_on:
LDR R1, =GPIOF_DATA_R @ 加载数据寄存器地址
STR R0, [R1] @ 将 R0 中的值(LED位模式)写入寄存器
BX LR @ 返回调用处
练习与总结
本节课中,我们一起学习了如何编写一个完整的 GPIO 输入驱动。我们扩展了初始化函数以支持输入引脚,编写了读取按钮状态的子程序,并在主程序中实现了根据不同按钮控制不同 LED 的逻辑。
目前程序只处理了单个按钮按下的情况。作为练习,请你尝试实现以下功能:
- 无按钮按下:当两个按钮都未按下时,调用一个
led_off子程序来关闭所有 LED。 - 两个按钮同时按下:当两个按钮同时被按下时,你可以选择点亮蓝色 LED(如果板子支持),或者同时点亮红色和绿色 LED。

通过完成这个练习,你将更深入地理解 GPIO 输入输出的协同工作以及条件判断在嵌入式编程中的应用。如果在实现过程中遇到任何问题,可以随时回顾本教程或寻求进一步的帮助。
ARM 汇编语言入门:II:09.1:系统滴答定时器概述 ⏱️
在本节课中,我们将简要介绍定时器,特别是ARM Cortex-M微控制器中普遍存在的系统滴答定时器。
系统滴答定时器简介
上一节我们介绍了中断的基本概念,本节中我们来看看一个非常重要的内置硬件模块——系统滴答定时器。
系统滴答定时器存在于所有ARM Cortex-M系列微控制器中,无论是STM32、LPC还是Tiva C系列。该定时器允许系统以固定的周期发起特定操作。这个操作由内部时钟驱动,无需外部信号。例如,在一个应用程序中,我们可以使用系统滴答定时器每隔200毫秒读取一次传感器数值。
系统滴答定时器在设计实时操作系统时被广泛使用,系统软件可以周期性地中断应用程序,以监控和管理系统运行。
工作原理
系统滴答定时器是一个24位的向下计数器。它可以由系统时钟或内部振荡器驱动。其工作流程如下:
- 计数器从一个初始值开始向下计数。
- 当计数到零时,在下一个时钟周期,计数器会“下溢”。
- 下溢会触发一个名为
COUNTFLAG的标志位。 - 随后,计数器会自动重载初始值,并重新开始计数。
相关寄存器
编程配置系统滴答定时器时,主要涉及三个寄存器。以下是这些寄存器的简要说明:
- 系统控制和状态寄存器:用于使能定时器、选择时钟源以及查看计数标志。
- 重载值寄存器:用于设置计数器的初始值,即决定定时周期。
- 当前值寄存器:用于读取计数器当前的计数值。
我们将在后续的代码实践中学习如何配置这些寄存器。
周期计算
现在,我们来看看如何计算需要加载到重载值寄存器中的数值。
假设我们希望某个动作每秒发生一次,且我们的系统时钟频率为16 MHz。那么,我们需要向重载值寄存器加载的数值是 16,000,000 - 1。
计算原理:处理器运行在16 MHz,意味着一秒钟有1600万个时钟周期。由于计数器从初始值向下计数到0,因此初始值应为(周期数 - 1)。
再举一个例子,如果我们希望动作每毫秒发生一次,该如何计算?
- 已知:1秒 = 1000毫秒,1秒有16,000,000个周期。
- 那么,1毫秒拥有的周期数为:16,000,000 / 1000 = 16,000。
- 因此,需要加载的值为:16,000 - 1。
通用定时器简介
除了每个Cortex-M内核都具备的系统滴答定时器,芯片制造商(如ST、NXP、德州仪器)通常还会提供额外的通用定时器。
与系统滴答定时器主要用于提供系统时间基准不同,通用定时器功能更加灵活。我们可以使用它们来创建精确的延时、测量脉冲宽度、生成PWM波形以及捕获外部事件的发生时间等。
总结
本节课中我们一起学习了:
- 系统滴答定时器是所有ARM Cortex-M微控制器的标准配置,用于产生周期性的中断,是实时操作系统的重要基础。
- 其核心是一个24位向下计数器,计数到零后触发标志并自动重载。
- 编程时主要通过三个寄存器(控制/状态、重载值、当前值)进行配置。
- 定时周期由加载到重载值寄存器的数值决定,计算公式为:(所需时间对应的时钟周期数) - 1。
- 此外,芯片厂商提供的通用定时器功能更为丰富,可用于延时、测量和信号生成等多种场景。
下一节,我们将通过实际的代码示例,学习如何初始化和使用系统滴答定时器。
039:为SysTick寄存器分配符号名称 🎯



在本节课中,我们将学习如何为SysTick定时器编写驱动程序。我们将通过为相关的寄存器分配符号名称来开始这个过程,这是编写底层硬件驱动代码的第一步。

概述
SysTick定时器是ARM Cortex-M系列处理器的一个核心外设。这意味着它的设计由ARM公司定义,并且在不同厂商(如TI、ST)生产的Cortex-M芯片中,其寄存器的地址和功能是相同的。因此,我们编写的驱动程序可以跨不同的Cortex-M4开发板工作。
为了编写驱动程序,我们首先需要查阅ARM提供的官方文档(Cortex-M4通用用户指南),以获取SysTick寄存器的确切内存地址和功能描述。然后,我们将为这些寄存器定义易于理解的符号名称,以便在后续的代码中使用。
创建新项目
首先,我们需要创建一个新的项目来编写我们的代码。
以下是创建新项目的步骤:
- 新建一个项目。
- 将项目存储在一个指定的文件夹中。
- 将项目命名为“SysTick”。
- 选择目标开发板为TM4C123GH6PM。
理解SysTick定时器
上一节我们创建了项目,本节中我们来看看SysTick定时器的核心概念。SysTick是一个24位的递减计数器。它从“重载值寄存器”中加载一个初始值,然后开始递减计数。当计数到0时,它会触发一个中断(如果已启用),并自动从重载值寄存器中重新加载数值,开始下一轮计数。
为了控制和使用这个定时器,我们需要操作三个主要的寄存器:
- 控制与状态寄存器 (SysTick Control and Status Register):用于启用定时器、选择时钟源、启用中断以及检查计数是否完成。
- 重载值寄存器 (SysTick Reload Value Register):用于设置定时器递减计数的初始值。
- 当前值寄存器 (SysTick Current Value Register):用于读取定时器当前的计数值。

查找寄存器地址



由于SysTick是核心外设,其寄存器地址在ARM架构中是固定的。我们需要从《Cortex-M4通用用户指南》中查找这些地址,而不是从特定芯片(如TM4C)的数据手册中查找。


在文档中,我们可以找到SysTick寄存器的内存映射。例如,控制与状态寄存器的地址是 0xE000E010。
分配符号名称
现在,我们将为这些寄存器以及寄存器内部的特定控制位定义符号名称。遵循半导体厂商(如德州仪器TI)在官方头文件中的命名惯例,可以使我们的代码更具可读性和可移植性。
以下是需要定义的符号名称列表:
NVIC_ST_CTRL_R:对应SysTick控制与状态寄存器,地址为0xE000E010。NVIC_ST_RELOAD_R:对应SysTick重载值寄存器,地址为0xE000E014。NVIC_ST_CURRENT_R:对应SysTick当前值寄存器,地址为0xE000E018。
此外,我们还需要为控制寄存器中的各个功能位定义掩码:
NVIC_ST_CTRL_COUNT:计数完成标志位(第16位),用于检查定时器是否已计数到0。NVIC_ST_CTRL_CLK_SRC:时钟源选择位(第2位),设置为1表示使用处理器内核时钟。NVIC_ST_CTRL_INTEN:中断使能位(第1位),用于启用SysTick中断。NVIC_ST_CTRL_ENABLE:定时器使能位(第0位),用于启动或停止SysTick定时器。NVIC_ST_RELOAD_M:重载值的最大24位数值,即0x00FFFFFF。
在汇编代码中,这些定义通常使用 .equ 指令来实现:
NVIC_ST_CTRL_R .equ 0xE000E010
NVIC_ST_RELOAD_R .equ 0xE000E014
NVIC_ST_CURRENT_R .equ 0xE000E018
NVIC_ST_CTRL_COUNT .equ 0x00010000
NVIC_ST_CTRL_CLK_SRC .equ 0x00000004
NVIC_ST_CTRL_INTEN .equ 0x00000002
NVIC_ST_CTRL_ENABLE .equ 0x00000001
NVIC_ST_RELOAD_M .equ 0x00FFFFFF
复用GPIO符号定义
为了让LED闪烁,我们还需要控制GPIO(通用输入输出)引脚。由于在之前的课程中已经为GPIO相关寄存器(如方向寄存器、数据寄存器、数字使能寄存器)定义了符号名称,我们可以直接将这些定义复制到当前项目中,无需重新编写。

总结
本节课中,我们一起学习了SysTick定时器作为ARM Cortex-M核心外设的基本概念。我们通过查阅ARM官方文档确定了其寄存器的固定地址,并遵循行业惯例为这些寄存器及其控制位分配了清晰的符号名称。这为下一节课实际编写SysTick驱动程序并实现精确延时功能奠定了坚实的基础。在下一节,我们将利用这些定义来初始化定时器并创建延时函数。
040:编写 SysTick 定时器驱动



在本节课中,我们将要学习如何为 ARM Cortex-M 微控制器编写 SysTick 定时器的驱动程序。我们将从初始化 GPIO 开始,然后配置 SysTick 定时器,为后续实现延时功能打下基础。

初始化 GPIO
上一节我们介绍了驱动的基本结构,本节中我们来看看如何初始化 GPIO 端口。以下是初始化 GPIO 子程序的步骤。

首先,我们需要启用 GPIOF 端口的时钟访问。
GPIO_Init:
LDR R1, =RCC_AHB2ENR
LDR R0, [R1]
ORR R0, R0, #GPIOF_EN
STR R0, [R1]
NOP


接下来,我们需要设置红色 LED 引脚的方向为输出。
LDR R1, =GPIOF_DIR
LDR R0, [R1]
ORR R0, R0, #LED_RED
STR R0, [R1]
最后,我们启用该引脚的数字化功能。
LDR R1, =GPIOF_DEN
LDR R0, [R1]
ORR R0, R0, #LED_RED
STR R0, [R1]
BX LR


完成 GPIO 初始化后,我们就可以在 __main 函数中调用它。

__main:
BL GPIO_Init
初始化 SysTick 定时器
在 GPIO 初始化之后,我们需要初始化 SysTick 定时器。以下是实现 SysTick_Init 子程序的步骤。

首先,在修改任何配置之前,建议先禁用定时器。

SysTick_Init:
LDR R1, =NVIC_ST_CTRL_R
MOV R0, #0
STR R0, [R1]
然后,我们将最大值加载到重载寄存器中。

LDR R1, =NVIC_ST_RELOAD_R
LDR R0, =0x00FFFFFF
STR R0, [R1]

接着,通过向当前值寄存器写入任意值(例如 0)来清除它。

LDR R1, =NVIC_ST_CURRENT_R
MOV R0, #0
STR R0, [R1]

最后,我们选择时钟源并启用 SysTick 定时器。
LDR R1, =NVIC_ST_CTRL_R
MOV R0, #NVIC_ST_CTRL_ENABLE
ORR R0, R0, #NVIC_ST_CTRL_CLK_SRC
STR R0, [R1]
BX LR

初始化完成后,我们在主函数中调用它。
BL SysTick_Init

总结


本节课中我们一起学习了如何为 ARM Cortex-M 编写基础的设备驱动。我们首先实现了 GPIO 的初始化,配置了 LED 引脚。然后,我们详细配置了 SysTick 定时器,包括禁用、设置重载值、清除当前值和最终启用定时器。在下一课中,我们将基于这个已初始化的定时器来实现延时子程序。
041:使用SysTick定时器创建延时子程序 🕐

在本节课中,我们将学习如何使用ARM Cortex-M微控制器中的SysTick定时器来创建精确的延时子程序。我们将编写两个子程序:一个用于基于时钟周期的基本延时,另一个用于实现以毫秒为单位的延时。


SysTick定时器基础

上一节我们介绍了SysTick定时器的基本概念,本节中我们来看看如何利用它编写延时函数。
SysTick是一个24位的递减计数器。我们通过向重载值寄存器写入一个数值来设定延时长度。定时器会从该值开始递减,直到减到0,此时会置位计数标志位。
核心公式:
延时时间 = (重载值) × 时钟周期
对于16MHz的系统时钟,一个时钟周期为:1 / 16,000,000 Hz = 62.5 ns
编写基本延时子程序 SysTick_Wait
我们将创建一个名为 SysTick_Wait 的子程序(或称函数)。该函数通过寄存器R0接收一个参数,这个参数代表需要延时的时钟周期数。
以下是该子程序的核心步骤:
- 设置重载值:将延时周期数(R0的值)减去1后,存入SysTick的重载值寄存器。
LDR R1, =NVIC_ST_RELOAD_R ; 加载重载值寄存器地址 SUB R0, R0, #1 ; R0 = R0 - 1 STR R0, [R1] ; 将新的重载值存入寄存器 - 启动定时器并等待:使能SysTick定时器,然后循环检查计数标志位是否被置位。
LDR R1, =NVIC_ST_CTRL_R ; 加载控制寄存器地址 MOV R3, #0 ; 清除R3,用于后续操作 ORR R3, R3, #0x05 ; 设置控制寄存器:使能定时器并使用处理器时钟 STR R3, [R1] ; 写入控制寄存器 - 循环等待标志位:不断检查控制寄存器中的计数标志位(第16位)。如果未置位,则继续循环。
WaitLoop: LDR R3, [R1] ; 读取控制寄存器值到R3 ANDS R3, R3, #0x00010000 ; 与计数标志位掩码进行AND操作,并更新状态标志 BEQ WaitLoop ; 如果结果为零(标志位未置位),则跳回WaitLoop继续等待 - 返回:当计数标志位置位,说明延时结束,子程序返回。
BX LR ; 返回调用处


关键点:使用 ANDS 指令(带S后缀)会在执行按位与操作后,根据结果更新处理器的状态标志(APSR寄存器),这样我们才能用 BEQ(等于零则跳转)指令进行条件判断。

编写毫秒级延时子程序 SysTick_Wait10MS


基于 SysTick_Wait,我们可以构建一个更实用的、以10毫秒为单位的延时函数 SysTick_Wait10MS。它通过R0接收一个参数N,实现 N × 10ms 的延时。


首先,我们需要计算10毫秒对应的时钟周期数。
计算10ms对应的周期数:
- 系统时钟频率:16 MHz = 16,000,000 周期/秒
- 1秒 = 1000毫秒
- 10毫秒 = 10 / 1000 = 1/100 秒
- 10毫秒对应的周期数 = (1/100) × 16,000,000 = 160,000 周期
我们在代码中定义一个常量:
TEN_MS_DELAY EQU 160000



以下是 SysTick_Wait10MS 子程序的逻辑:

- 参数检查与保存:首先检查传入的参数(R0)。如果为0,则直接返回。否则,将参数值保存到R4,并保存R4和链接寄存器LR到栈中。
CMP R0, #0 ; 比较R0和0 BEQ Done ; 如果等于0,跳转到Done标签直接返回 PUSH {R4, LR} ; 保存R4和链接寄存器 MOV R4, R0 ; 将延时次数参数从R0复制到R4 - 延时循环:这是一个外层循环,循环次数由参数R4控制。每次循环调用一次
SysTick_Wait来实现10ms延时。DelayLoop: LDR R0, =TEN_MS_DELAY ; 将10ms的周期数加载到R0 BL SysTick_Wait ; 调用基础延时函数,延时10ms SUBS R4, R4, #1 ; 外层循环计数器减1,并更新状态标志 BHI DelayLoop ; 如果R4 > 0 (即减1后未变为负),则继续循环 - 恢复现场并返回:循环结束后,恢复之前保存的寄存器,并返回。
POP {R4, LR} ; 恢复R4和链接寄存器 Done: BX LR ; 返回调用处




关键点:使用 SUBS 和 BHI(高于则跳转)指令组合来控制外层循环。PUSH/POP 用于在子程序调用前后保存和恢复寄存器的值,这是编写规范子程序的重要习惯。



应用测试:闪烁LED
为了测试我们的延时子程序,我们创建两个简单的函数控制LED,并在主循环中调用它们。
以下是LED控制函数:
LED_On:
LDR R1, =GPIOF_DATA_R ; 加载GPIO端口数据寄存器地址
MOV R0, #LED_RED ; LED_RED是控制红灯的位掩码,例如 0x02
STR R0, [R1] ; 将值写入寄存器,点亮LED
BX LR

LED_Off:
LDR R1, =GPIOF_DATA_R
MOV R0, #0 ; 写入0,关闭LED
STR R0, [R1]
BX LR
在主程序中,我们实现一个让LED以1秒间隔闪烁的循环:
Main_Loop:
MOV R0, #100 ; 设置参数为100,即 100 * 10ms = 1000ms = 1秒
BL SysTick_Wait10MS ; 延时1秒
BL LED_On ; 打开LED
MOV R0, #100 ; 再次设置参数为100
BL SysTick_Wait10MS ; 再延时1秒
BL LED_Off ; 关闭LED
B Main_Loop ; 跳回循环开始,实现持续闪烁




总结

本节课中我们一起学习了如何利用SysTick定时器在ARM汇编中创建精确的延时。
- 我们首先编写了
SysTick_Wait函数,它能实现基于处理器时钟周期的精确延时。 - 接着,我们基于时钟频率计算出特定时间(10毫秒)对应的周期数,并编写了更易用的
SysTick_Wait10MS函数,它可以通过参数控制10毫秒的倍数延时。 - 我们强调了在子程序中保存和恢复寄存器(如使用
PUSH/POP)的重要性。 - 最后,我们通过一个LED闪烁的实例测试了延时函数,验证了其正确性。

掌握定时器延时是嵌入式编程的基础,它为后续处理定时任务、轮询等操作提供了支持。
042:UART协议概述 🚀
在本节课中,我们将简要概述通用异步收发传输器,即UART协议。我们将了解串行通信的基本概念、数据帧的构成以及数据传输速率。
串行与并行通信
上一节我们介绍了通信的基本概念,本节中我们来看看数据通信的两种主要方式:并行和串行。
在并行数据传输中,通常使用八条或更多线路同时向另一设备传输数据。而在串行通信中,数据一次只发送一个比特。
过去,短距离通信更倾向于使用并行传输,因为它能同时传输多个比特,提供更高的吞吐量。随着技术进步,串行通信的数据速率有时会超过并行通信,而并行通信仍存在线缆和连接器尺寸大、成本高、数据线间串扰以及长距离下数据线到达时间难以同步等缺点。
UART协议基础
UART是最常见的串行通信协议之一。在串行数据传输中,接收端数据线上的数据全是0和1。除非发送方和接收方就数据如何打包、多少比特构成一个字符以及数据何时开始和结束等一套规则达成一致,否则很难理解这些数据。
面向字符的传输广泛使用像UART这样的异步串行数据通信。在异步方法中,每个字符(如ASCII字符)都被打包在起始位和停止位之间,这称为成帧。
以下是数据帧的构成要素:
- 起始位:始终为1个比特,其值固定为逻辑0。
- 数据位:可以是5、6、7或8比特宽。在旧系统中,ASCII字符是7比特;现代系统通常也发送非ASCII的8比特数据。
- 停止位:可以是1个或2个比特,其值由逻辑1表示。
例如,发送ASCII字符‘a’(二进制为 1100001)。在传输时,最低有效位首先发送。假设使用偶校验和2个停止位,其数据帧结构如下(从右向左读,LSB先发):
1 1 | 1 0 0 0 0 1 1 0 | 0
(停止位 | 数据位+校验位 | 起始位)
奇偶校验
在一些系统中,为了保持数据完整性,会在数据帧中包含字符字节的奇偶校验位。这意味着每个字符除了起始位和停止位外,还有一个单独的校验位。
校验位可以是奇校验或偶校验:
- 在奇校验中,数据位(包括校验位)中‘1’的总数为奇数。
- 在偶校验中,数据位(包括校验位)中‘1’的总数为偶数。
例如,对于ASCII字符‘a’(1100001,其中有3个‘1’),若采用偶校验,则需要添加一个值为‘1’的校验位,使‘1’的总数变为4(偶数)。UART芯片允许编程配置校验位为奇校验、偶校验或无校验。
数据传输速率
串行数据通信的数据传输速率以bps(比特每秒)表示。另一个广泛使用的术语是波特率。然而,波特率和bps速率不一定相等,这是因为波特率定义为每秒信号变化的次数。在调制解调器中,每个信号变化可能传输多个数据比特。
但在像UART这样的有线连接中,波特率和bps是相同的。其关系可以用以下公式表示:
比特率 (bps) = 波特率 × 每符号比特数
对于UART标准,每符号承载1比特数据,因此 比特率 = 波特率。
本节课中我们一起学习了UART协议的基础知识,包括串行通信与并行通信的比较、异步数据帧的结构(起始位、数据位、校验位、停止位)、奇偶校验的作用以及数据传输速率的概念。理解这些是后续进行UART硬件编程的基础。
043:为相关UART寄存器分配符号名 🧩




在本节课中,我们将学习如何为TM4C123微控制器开发UART驱动程序。我们将从创建一个新项目开始,并逐步为所有相关的GPIO和UART寄存器分配符号名,为后续编写驱动代码打下基础。
创建新项目与文件
首先,我们需要在Keil uVision中创建一个新项目。
- 选择“New uVision Project”。
- 为项目创建一个新文件夹,命名为
uart。 - 将项目命名为
uart。 - 在设备选择中,找到并选择
TM4C123GH6PM。 - 在CMSIS管理界面,选择
Core和Startup文件。 - 将默认的源文件组重命名为
APP。 - 右键点击
APP组,选择“Add New Item”,创建一个名为main.s的汇编源文件。
至此,项目框架搭建完成。我们将使用UART0,其RX和TX线分别连接到PA0和PA1引脚。要使用UART功能,必须先将这些GPIO引脚配置为复用功能(Alternate Function)。
查找并定义寄存器基地址
要配置外设,首先需要从数据手册中查找相关寄存器的地址。以下是需要定义的关键基地址:
- GPIOA基地址:这是配置PA0和PA1引脚(UART0的RX/TX)的起点。
- 系统控制基地址:用于启用GPIOA和UART0的时钟。
我们在之前的课程中已经定义了系统控制的基地址,可以直接复用。现在,我们从数据手册中复制GPIOA的基地址,并在代码中为其创建符号名。
; 定义 GPIO Port A 的基地址
GPIOA_BASE EQU 0x40004000
; 定义系统控制基地址 (已在之前课程中定义)
SYSCTL_BASE EQU 0x400FE000
定义GPIO相关寄存器符号名
配置GPIO引脚需要操作几个关键寄存器。我们将为每个寄存器定义两个符号名:一个是偏移量,另一个是完整的寄存器地址。


以下是需要为GPIOA定义的寄存器:

- 数字使能寄存器 (GPIOA_DEN_R):用于启用引脚的数字功能。
- 复用功能选择寄存器 (GPIOA_AFSEL_R):用于将引脚功能切换到UART。
- 端口控制寄存器 (GPIOA_PCTL_R):用于为引脚选择具体的复用功能编号(例如,UART0)。
我们首先为这些寄存器定义偏移量占位符和寄存器地址符号名。

; GPIOA 寄存器偏移量 (稍后从数据手册填充)
GPIOA_DEN_OFFSET EQU
GPIOA_AFSEL_OFFSET EQU
GPIOA_PCTL_OFFSET EQU
; GPIOA 寄存器地址
GPIOA_DEN_R EQU GPIOA_BASE + GPIOA_DEN_OFFSET
GPIOA_AFSEL_R EQU GPIOA_BASE + GPIOA_AFSEL_OFFSET
GPIOA_PCTL_R EQU GPIOA_BASE + GPIOA_PCTL_OFFSET

定义时钟使能控制位

在访问任何外设前,必须通过系统控制模块启用其时钟。我们需要为GPIOA和UART0定义时钟门控使能位。
- GPIOA时钟使能:位于
RCGCGPIO寄存器的位0。 - UART0时钟使能:位于
RCGCUART寄存器的位0。
我们为它们定义易于理解的常量。



; 时钟使能控制位
GPIOA_EN EQU (1 << 0) ; 置位 RCGCGPIO 寄存器的位0以启用 GPIOA 时钟
UART0_EN EQU (1 << 0) ; 置位 RCGCUART 寄存器的位0以启用 UART0 时钟


接下来,需要找到 RCGCUART 寄存器的地址。从数据手册中查得其偏移量为 0x618。然后定义其寄存器地址。
; 系统控制中 UART 时钟门控寄存器的偏移量
SYSCTL_RCGCUART_OFFSET EQU 0x618
; UART 时钟门控寄存器地址
SYSCTL_RCGCUART_R EQU SYSCTL_BASE + SYSCTL_RCGCUART_OFFSET
填充GPIO寄存器偏移量
现在,我们从数据手册中查找并填充GPIOA相关寄存器的具体偏移量。
GPIOA_AFSEL_R偏移量:0x420GPIOA_PCTL_R偏移量:0x52CGPIOA_DEN_R偏移量:0x51C
更新代码中的定义:
; GPIOA 寄存器偏移量 (从数据手册获取)
GPIOA_DEN_OFFSET EQU 0x51C
GPIOA_AFSEL_OFFSET EQU 0x420
GPIOA_PCTL_OFFSET EQU 0x52C
定义UART0寄存器符号名

配置UART0本身需要操作其内部的一系列寄存器。我们首先需要UART0的基地址。从数据手册中查得 UART0_BASE 为 0x4000C000。
以下是UART0的关键寄存器,我们将为它们定义符号名:


- 数据寄存器 (UART0_DR_R):用于发送和接收数据。
- 标志寄存器 (UART0_FR_R):用于检查发送/接收缓冲区状态(如是否为空或满)。
- 整数波特率分频器 (UART0_IBRD_R):用于设置波特率的整数部分。
- 小数波特率分频器 (UART0_FBRD_R):用于设置波特率的小数部分。
- 线控制寄存器 (UART0_LCRH_R):用于配置数据位长度、停止位等。
- 控制寄存器 (UART0_CTL_R):用于启用或禁用UART模块。
- 中断相关寄存器:如中断掩码
(UART0_IM_R)、原始中断状态(UART0_RIS_R)和中断清除(UART0_ICR_R)。
我们从数据手册中获取每个寄存器的偏移量,并定义相应的符号名。
; UART0 基地址
UART0_BASE EQU 0x4000C000
; UART0 寄存器偏移量
UART0_DR_OFFSET EQU 0x000 ; 数据寄存器
UART0_FR_OFFSET EQU 0x018 ; 标志寄存器
UART0_IBRD_OFFSET EQU 0x024 ; 整数波特率分频器
UART0_FBRD_OFFSET EQU 0x028 ; 小数波特率分频器
UART0_LCRH_OFFSET EQU 0x02C ; 线控制寄存器
UART0_CTL_OFFSET EQU 0x030 ; 控制寄存器
UART0_IM_OFFSET EQU 0x038 ; 中断掩码寄存器
UART0_RIS_OFFSET EQU 0x03C ; 原始中断状态寄存器
UART0_ICR_OFFSET EQU 0x044 ; 中断清除寄存器
; UART0 寄存器地址
UART0_DR_R EQU UART0_BASE + UART0_DR_OFFSET
UART0_FR_R EQU UART0_BASE + UART0_FR_OFFSET
UART0_IBRD_R EQU UART0_BASE + UART0_IBRD_OFFSET
UART0_FBRD_R EQU UART0_BASE + UART0_FBRD_OFFSET
UART0_LCRH_R EQU UART0_BASE + UART0_LCRH_OFFSET
UART0_CTL_R EQU UART0_BASE + UART0_CTL_OFFSET
UART0_IM_R EQU UART0_BASE + UART0_IM_OFFSET
UART0_RIS_R EQU UART0_BASE + UART0_RIS_OFFSET
UART0_ICR_R EQU UART0_BASE + UART0_ICR_OFFSET
定义常用配置常量
最后,为了提升代码的可读性,我们为一些常用的配置值定义常量。这些常量将在后续对寄存器进行读写操作时使用。
; 常用配置常量
UART_FR_RXFE EQU (1 << 4) ; 接收缓冲区空标志位掩码 (检查位4)
UART_LCRH_WLEN_8 EQU 0x60 ; 设置数据长度为8位 (位6和位5)
UART_LCRH_FEN EQU (1 << 4) ; 启用FIFO缓冲区 (位4)
UART_CTL_UARTEN EQU (1 << 0) ; 启用UART模块 (位0)
UART_IM_RTIM EQU (1 << 6) ; 接收超时中断掩码 (位6)
总结
本节课中,我们一起为TM4C123的UART0驱动程序开发做好了准备。我们完成了以下工作:
- 创建了新的Keil项目并添加了汇编源文件。
- 查找并定义了 GPIOA 和 系统控制 模块的基地址。
- 为配置UART引脚所需的GPIO寄存器(
DEN,AFSEL,PCTL)分配了符号名。 - 定义了启用 GPIOA 和 UART0 时钟的常量,并找到了
RCGCUART寄存器的地址。 - 查找了 UART0 的基地址,并为其所有关键寄存器(数据、标志、波特率、控制、中断等)创建了完整的符号名定义。
- 定义了一系列常量,用于后续配置数据位长度、启用FIFO、启用UART模块等操作。

现在,所有必要的寄存器地址和常用值都已通过符号名定义完毕,代码的可读性和可维护性得到了极大提升。在下一节课中,我们将利用这些定义,开始编写UART初始化和数据收发的具体驱动代码。
044:编写 UART 驱动 🚀

概述
在本节课程中,我们将学习如何为 ARM 微控制器编写一个 UART 驱动程序。我们将从初始化 UART 模块开始,逐步配置其时钟、引脚、波特率等关键参数,并最终实现一个可用的初始化子程序。


编写 UART 初始化子程序

上一节我们介绍了 UART 的基本概念,本节中我们来看看如何用汇编语言实现其初始化过程。

首先,我们定义代码段并设置入口点。
.area .text
.code 16
.align 2
.thumb
.global __main
__main:
b uart_init


接下来,我们开始实现 uart_init 子程序。第一步是保存链接寄存器的值。


uart_init:
push {lr}
启用 UART 时钟
我们需要启用 UART 模块的时钟。这通过设置 RCGCUART 寄存器的相应位来完成。

ldr r1, =SYSCTL_RCGCUART_R
ldr r0, [r1]
orr r0, r0, #UART0_EN
str r0, [r1]

对应的 C 语言操作是:
SYSCTL_RCGCUART_R |= UART0_EN;
启用 GPIO 端口时钟
UART 引脚通常映射到特定的 GPIO 端口(例如 Port A)。我们需要启用该端口的时钟。



ldr r1, =SYSCTL_RCGCGPIO_R
ldr r0, [r1]
orr r0, r0, #GPIO_PORTA_EN
str r0, [r1]


对应的 C 语言操作是:
SYSCTL_RCGCGPIO_R |= GPIO_PORTA_EN;
配置 UART 引脚
以下是配置 UART 引脚(PA0 和 PA1)为数字功能和备用功能的步骤。
首先,数字使能 PA0 和 PA1 引脚。
ldr r1, =GPIO_PORTA_DEN_R
ldr r0, [r1]
orr r0, r0, #0x03
str r0, [r1]

其次,设置引脚为备用功能(UART)。


ldr r1, =GPIO_PORTA_AFSEL_R
ldr r0, [r1]
orr r0, r0, #0x03
str r0, [r1]
最后,在端口控制寄存器中,清除并设置 PA0 和 PA1 的引脚控制字段,将其配置为 UART 功能。


ldr r1, =GPIO_PORTA_PCTL_R
ldr r0, [r1]
bic r0, r0, #0xFF
orr r0, r0, #(UART_PCTL_PA0 | UART_PCTL_PA1)
str r0, [r1]

配置 UART 模块
现在开始配置 UART 模块本身。首先,在配置前禁用它。

ldr r1, =UART0_CTL_R
ldr r0, [r1]
bic r0, r0, #UART_CTL_UARTEN
str r0, [r1]
设置波特率
波特率配置分为整数部分和小数部分,计算公式如下:
- 整数部分:
BRD = SysClk / (16 * BaudRate) - 小数部分:
FBRD = Integer((BRD - IBRD) * 64 + 0.5)
假设系统时钟为 16 MHz,目标波特率为 115200,则计算如下:
- 整数部分:
16,000,000 / (16 * 115200) ≈ 8.6805,取整数 8。 - 小数部分:
(0.6805 * 64 + 0.5) ≈ 44.052,取整数 44。
将计算值写入对应的寄存器。

; 设置整数波特率
ldr r1, =UART0_IBRD_R
mov r0, #8
str r0, [r1]
; 设置小数波特率
ldr r1, =UART0_FBRD_R
mov r0, #44
str r0, [r1]

设置数据帧格式
我们需要配置数据位长度、停止位和奇偶校验。这里我们配置为 8 位数据位,无奇偶校验,1 位停止位。
ldr r1, =UART0_LCRH_R
ldr r0, [r1]
bic r0, r0, #0xFF
orr r0, r0, #(UART_LCRH_WLEN_8 | UART_LCRH_PEN_NONE)
str r0, [r1]
启用 UART
所有配置完成后,重新启用 UART 模块。
ldr r1, =UART0_CTL_R
ldr r0, [r1]
orr r0, r0, #UART_CTL_UARTEN
str r0, [r1]
结束初始化
最后,恢复链接寄存器并返回。




pop {lr}
bx lr
总结
本节课中我们一起学习了如何从头开始编写一个 ARM UART 驱动程序的初始化部分。我们涵盖了以下关键步骤:
- 启用 UART 和 GPIO 端口的时钟。
- 配置 GPIO 引脚为 UART 备用功能。
- 禁用 UART 以进行安全配置。
- 根据系统时钟和目标波特率计算并设置波特率寄存器。
- 配置 UART 的数据帧格式。
- 最后重新启用 UART 模块。


这个初始化子程序为后续的数据收发功能奠定了基础。在下一课中,我们将继续创建用于读取和写入数据的子程序。
045:测试UART驱动程序 🧪
在本节课中,我们将学习如何测试上一节中编写的UART驱动程序。我们将通过编写一个简单的汇编程序来发送字符,验证驱动程序是否正常工作。
概述
上一节我们完成了UART驱动程序的初始化、读取和写入字符功能。本节中,我们将编写一个测试程序,通过UART发送一系列字符(从A到Z)和一个感叹号,来验证驱动程序的正确性。
定义常用键的符号
为了方便代码编写和阅读,我们首先为一些常用的控制字符定义符号名称。这些不是字母键,而是如回车、退格等特殊键。
以下是这些符号的定义:
CR代表回车,其ASCII码为0x0D。BS代表退格,其ASCII码为0x08。LF代表换行,其ASCII码为0x0A。ESC代表退出,其ASCII码为0x1B。SPACE代表空格,其ASCII码为0x20。DEL代表删除,其ASCII码为0x7F。
创建发送新行的子程序
在串口通信中,要开始新的一行,通常需要发送两个字符:回车和换行。因此,我们创建一个名为 new_line 的子程序来封装这个操作。
以下是 new_line 子程序的代码:
new_line:
PUSH {lr} // 保存链接寄存器
MOV r0, #CR // 将回车字符加载到r0
BL uart_write_char // 调用写字符子程序
MOV r0, #LF // 将换行字符加载到r0
BL uart_write_char // 调用写字符子程序
POP {pc} // 恢复程序计数器,返回调用处
代码解释:
PUSH {lr}保存返回地址。- 将回车符送入
r0并调用uart_write_char。 - 将换行符送入
r0并再次调用uart_write_char。 POP {pc}用于返回到调用该子程序的位置。
编写主测试程序
现在,我们来编写主程序逻辑。目标是初始化UART,然后依次发送字符A到Z,最后发送一个感叹号并换行。
以下是主测试程序的逻辑:
BL uart_init // 初始化UART
MOV r4, #'A' // 将字符'A'的ASCII码存入r4,作为起始字符
lp0:
MOV r0, r4 // 将当前字符从r4移到r0(参数寄存器)
BL uart_write_char // 发送字符
ADD r4, r4, #1 // r4加1,指向下一个字母
CMP r4, #'Z' // 比较当前字符是否超过'Z'
BLS lp0 // 如果小于或等于'Z',则跳回lp0继续循环
BL new_line // 发送回车换行,开始新行
MOV r0, #'!' // 将感叹号字符加载到r0
BL uart_write_char // 发送感叹号
B . // 无限循环,程序结束
代码解释:
- 首先调用
uart_init初始化UART。 - 将起始字符
'A'存入寄存器r4。 - 在标签
lp0处开始循环:- 将
r4中的字符移到r0。 - 调用
uart_write_char发送该字符。 - 将
r4加1以指向下一个字母。 - 比较
r4是否大于'Z',如果不是则跳回lp0继续循环。
- 将
- 循环结束后,调用
new_line子程序换行。 - 发送一个感叹号字符。
- 最后进入无限循环
B .,程序挂起。
代码修正与验证
在构建和测试之前,需要检查并修正代码中可能存在的错误。根据常见问题,请确保以下几点:
- 启动文件:在
startup.s文件的复位处理程序中,注释掉可能干扰UART初始化的两行系统初始化代码。 - 寄存器名:检查所有寄存器名称(如
RCGCGPIO)的拼写是否正确。 - 指令修正:
- 在加载线路控制寄存器时,确保使用的是
LDR指令,而不是误写的ADD。 - 在配置寄存器后,确保有对应的
STR指令将值存回寄存器。 - 在
uart_read_char和uart_write_char子程序中,检查ANDS指令的书写是否正确。
- 在加载线路控制寄存器时,确保使用的是
- 符号定义:确保所有用到的符号(如
CR,LF)都已正确定义。
完成上述检查后,重新构建项目。
硬件测试
项目构建成功后,将其下载到开发板上进行测试。
- 打开串口调试工具(如Putty、Tera Term等)。
- 选择正确的COM端口(例如COM8)。
- 设置波特率为115200(与代码中配置的一致)。
- 复位开发板。

如果一切正常,你将在串口终端中看到如下输出:
ABCDEFGHIJKLMNOPQRSTUVWXYZ
!
这表明UART驱动程序工作正常,成功发送了A到Z的字母以及一个感叹号。
总结
本节课中,我们一起学习了如何测试UART驱动程序。我们定义了常用控制字符的符号,创建了发送新行的子程序,并编写了一个循环发送字母的主测试程序。通过修正代码中的常见错误并在硬件上验证,我们确认了驱动程序功能完整,可以正确地进行串口通信。

在下一节课中,我们将学习如何从C语言文件中调用这些用汇编编写的UART驱动函数,实现更实用的双向通信功能(如printf和scanf),这将使我们的开发更加便捷。
046:安装Keil uVision 5开发环境 🛠️
在本节课中,我们将学习如何下载和安装ARM汇编语言开发所需的集成开发环境(IDE)和工具链。我们将使用由ARM官方提供的Keil uVision 5软件。

下载Keil uVision 5
上一节我们介绍了课程的整体安排,本节中我们来看看如何获取开发工具。以下是下载Keil uVision 5的具体步骤。
- 打开浏览器,访问Google搜索引擎。
- 在搜索框中输入关键词 Keil uVision 5 或 MDK version 5,然后按回车键进行搜索。
- 在搜索结果中,找到并点击指向ARM公司官方网站的链接。
- 进入ARM官网后,找到并点击页面上的 Downloads(下载)选项。
- 在下载页面中,选择 Product Downloads(产品下载)。
- 在列出的产品中,找到名为 MDK-ARM 的工具链,并点击它。
- 系统会跳转到一个需要填写个人信息的表单页面。请按要求填写您的详细信息。
- 填写完毕后,点击表单下方的 Submit(提交)按钮。
- 提交成功后,页面会提供软件的下载链接。点击该链接即可开始下载Keil uVision 5的安装程序。
我已经提前完成了下载,因此不再重复点击下载按钮。当你点击下载后,等待下载完成即可。


安装准备
我们已经成功下载了Keil uVision 5的安装文件。接下来,关闭浏览器,准备进行软件的安装。

本节课中我们一起学习了如何从ARM官方网站下载Keil uVision 5开发环境。这是开始ARM汇编语言编程的第一步。在下一节中,我们将继续讲解如何安装和配置这个软件。
047:安装Keil uVision 5 🛠️
在本节课中,我们将学习如何安装ARM汇编开发环境的核心工具——Keil uVision 5集成开发环境(IDE)。这是编写和调试ARM汇编程序的第一步。

启动安装程序
上一节我们介绍了如何获取Keil uVision 5的安装包。本节中,我们来看看具体的安装步骤。
下载完成后,找到安装文件并双击打开。

随后将开始安装过程。按照安装向导的提示,点击“Next”继续。
同意许可协议与选择安装路径
阅读许可协议后,需要同意条款才能继续安装。
点击此处表示同意协议,然后点击“Next”。
接下来需要决定软件的安装位置。
默认安装路径是C盘的Keil_v5文件夹。如果你想保持默认设置,直接点击“Next”即可。
填写用户信息与开始安装

在此处输入你的姓名,然后点击“Next”。
之后,安装程序将正式开始复制文件。这个过程可能需要一些时间。
为了节省时间,视频将暂停,并在安装接近结束时继续播放。
完成安装与启动包管理器
当安装进度完成后,点击此处的“Finish”按钮。

安装程序会自动打开“Pack Installer”(包管理器)。这个工具的作用是安装针对不同ARM开发板所需的各种启动文件和软件包。
ARM公司本身不生产硬件,而是由聪明的工程师设计处理器架构。这些设计被授权给像德州仪器(TI)、苹果、高通等硅芯片制造商。因此,我们需要通过包管理器来安装对应芯片厂商所需的支持文件。
安装完成后,系统会自动跳转到这个界面。
关于防火墙的注意事项
如果你的杀毒软件或防火墙处于开启状态,有时可能会阻碍软件连接到ARM的服务器。
以下是可能遇到的问题及建议:
- 如果安装或更新包管理器内容时遇到问题,很可能是因为防火墙的阻挡。
- 建议在安装和配置过程中暂时禁用防火墙,以确保连接顺畅。



总结

本节课中我们一起学习了Keil uVision 5 IDE的完整安装流程。我们完成了从启动安装程序、同意协议、选择路径,到最终安装完成并启动包管理器的所有步骤。同时,我们也了解了包管理器的作用以及可能遇到的防火墙问题。现在,你的基础开发环境已经准备就绪。
048:11.3 安装设备支持包
在本节课中,我们将学习如何为特定的Cortex微控制器安装设备支持包。这是使用Keil MDK进行开发前的必要步骤。

概述
上一节我们介绍了开发环境的搭建,本节中我们来看看如何为你的目标微控制器安装对应的设备支持包。设备支持包包含了芯片的启动文件、外设寄存器定义和系统初始化代码,是项目能够正确编译和调试的基础。
安装步骤
以下是安装设备支持包的具体流程。
-
打开Keil MDK软件,点击工具栏上的 Pack Installer 图标(一个立方体盒子),启动包管理器。
![]()
-
在Pack Installer窗口中,展开左侧的“Device”列表。如果你已经为你的开发板安装过支持包,可以跳过此课程。本教程适用于首次在Keil MDK中使用特定开发板的用户。
-
根据你使用的开发板品牌,在搜索框或列表中查找你的微控制器型号。
- 对于STM32 F4系列:搜索“STM32F411CE”。如果已安装,对应DFP(设备系列包)会显示“Up to Date”。如果未安装,你会看到“Install”按钮。
- 对于NXP KL25Z系列:展开“NXP”列表,找到“KLxx Series”,然后选择你的具体型号(如KL25Z)。点击对应的DFP进行安装。如果版本显示为“Deprecated”(已弃用),请展开列表,安装其下方的旧版本。
- 对于Texas Instruments TM4C123系列:搜索“TM4C123”,找到“TM4C_DFP”并点击安装。
-
点击“Install”后,安装过程将在窗口底部显示进度。请等待安装完成,提示“Completed requested actions”。
![]()
![]()
-
安装完成后,对应条目会显示“Up to Date”。如需卸载,可点击“Remove”。
总结


本节课中我们一起学习了如何在Keil MDK的Pack Installer中为STM32、NXP和TI的Cortex-M微控制器安装设备支持包。核心操作是搜索芯片型号 -> 定位对应的DFP -> 点击安装。如果你使用的是其他ARM微控制器,也可以遵循同样的方法搜索并安装其支持包。安装成功后,你就可以基于该芯片创建和开发项目了。下一节课,我们将编写一个简单的程序来测试安装是否成功。
049:下载STM32开发工具 🛠️
在本节课中,我们将学习如何下载由意法半导体(STMicroelectronics)发布的两款核心开发工具:STM32CubeMX和STM32CubeIDE。它们是进行STM32微控制器开发的基础软件。
下载STM32CubeMX
上一节我们介绍了课程目标,本节中我们来看看如何获取STM32CubeMX。STM32CubeMX是一个图形化配置工具,用于初始化C代码和设置硬件。


以下是下载STM32CubeMX的步骤:
- 打开浏览器,访问搜索引擎。
- 搜索关键词“Download STM32CubeMX”。
- 在搜索结果中,找到并进入意法半导体的官方网站(st.com)。
- 在网站上找到STM32CubeMX的下载页面。
- 页面会显示最新版本(例如5.5版)。如果需要旧版本,也可以在此选择。
- 点击“Get Software”按钮。
- 阅读并接受弹出的软件许可协议。
- 在接下来的页面中,输入您的电子邮箱地址和姓名。
- 登录您的账户。
- 登录后,页面将跳转回下载页,点击“Download”按钮开始下载。有时下载也会自动开始。
下载过程完成后,您就获得了STM32CubeMX的安装文件。
下载STM32CubeIDE
在成功下载了配置工具后,接下来我们需要获取集成开发环境(IDE)。STM32CubeIDE是意法半导体新发布的开发环境,它将代码编辑、编译和调试功能集成在一起。

以下是下载STM32CubeIDE的步骤:


- 在STM32CubeMX下载页面或通过搜索,找到STM32CubeIDE的下载入口。
- 此IDE完全免费,没有代码大小限制。这与某些有32KB代码限制的试用版IDE不同。
- 进入下载页面后,选择与您操作系统对应的版本(例如Windows安装程序)。
- 点击“Get Software”按钮。
- 再次阅读并接受软件许可协议。
- 可能需要再次登录您的账户。
- 登录后,下载将自动开始。该软件安装包较大(例如约665MB),下载可能需要一些时间。

下载完成后,您就准备好了STM32CubeIDE的安装程序。

本节课中我们一起学习了如何从官方网站下载STM32CubeMX配置工具和STM32CubeIDE集成开发环境。在下一节课中,我们将讲解如何安装这两款软件。
050:STM32CubeIDE安装指南 🛠️
在本节课中,我们将学习如何下载并安装STM32开发所需的两个核心软件:STM32CubeMX和STM32CubeIDE。我们将分步完成从解压文件到成功安装的全过程。

下载与解压STM32CubeMX
首先,我们获得了完整的STM32CubeMX下载包。右键点击该压缩文件,选择“解压到当前文件夹”。

解压完成后,双击进入文件夹查看内容。我发现最初解压出的似乎是macOS版本。请确保你下载的是Windows版本。实际上,下载包内通常同时包含了Windows、macOS和Linux版本。为了管理方便,我决定新建一个专用文件夹。
以下是操作步骤:
- 创建一个新文件夹,命名为
STM32CubeMX。 - 将压缩包内容解压到这个新文件夹中,以避免文件混杂。
完成清理后,我们可以在文件夹内看到Windows、macOS和Linux三个版本的安装程序。
安装STM32CubeMX

双击Windows版本的安装程序开始安装。


安装程序提示需要Java运行时环境(JRE)。如果你的电脑上没有安装JRE,则需要先安装它。



系统自动跳转到Java官网以下载JRE。


点击下载,保存文件,然后运行安装程序,按照提示完成JRE的安装。

JRE安装成功后,返回STM32CubeMX安装程序,再次双击进行安装。点击“是”和“下一步”。


阅读并接受许可协议和隐私政策,然后点击“下一步”。保持默认安装路径,继续点击“下一步”。

安装过程需要一些时间。安装完成后,点击“下一步”,然后点击“完成”。
下载与安装STM32CubeIDE
接下来,我们安装STM32CubeIDE。首先,在下载文件夹中创建一个名为 STM32CubeIDE 的新文件夹。


将STM32CubeIDE的压缩包拖入此文件夹并解压。解压完成后,双击其中的安装程序。
点击“是”开始安装,然后点击“下一步”。同意许可协议,保持默认安装目录,继续点击“下一步”。

安装过程会持续一段时间。安装完成后,点击“下一步”。你可以选择创建桌面快捷方式,最后点击“完成”。


总结

本节课中,我们一起完成了STM32开发环境的搭建。我们首先下载并解压了STM32CubeMX,在安装过程中解决了Java运行时环境(JRE)的依赖问题,并成功完成了安装。随后,我们以类似的步骤下载、解压并安装了STM32CubeIDE集成开发环境。现在,你的电脑上已经具备了进行STM32项目配置和代码开发的基本工具。下节课我们将开始学习如何使用这些工具。
051:安装软件包 📦


在本节课中,我们将学习如何为STM32CubeMX安装必要的软件包,以便为我们的ARM微控制器项目生成代码。
上一节我们完成了STM32CubeMX的安装,本节中我们来看看如何为其安装核心的微控制器支持包和工具链。
解决Java版本问题
启动STM32CubeMX时,可能会遇到Java版本不兼容的警告。软件提示检测到32位Java,但强烈建议使用64位版本,否则某些功能可能无法使用。
以下是解决此问题的步骤:
- 打开浏览器,访问Oracle官方网站的Java下载页面。
- 在下载页面中,找到并接受许可协议。
- 找到适用于Windows x64系统的
.exe安装程序并下载。 - 运行下载的安装程序,按照提示完成64位Java运行环境的安装。
- 安装完成后,重新启动STM32CubeMX,警告信息应不再出现。
安装微控制器支持包
成功启动STM32CubeMX后,我们需要安装针对特定微控制器系列的支持包。本课程主要使用STM32 F4系列。
以下是安装支持包的步骤:


- 点击顶部菜单栏的 Help(帮助)。
- 在下拉菜单中选择 Manage Embedded Software Packages(管理嵌入式软件包)。
- 在弹出的窗口中,会列出所有可用的微控制器系列支持包。
- 找到 STM32F4 系列,展开其版本列表。
- 选择最新的版本,点击旁边的 Install Now(立即安装)按钮。
- 软件将自动下载并解压该支持包,此过程需要一些时间。
安装ARM工具链包
除了微控制器支持包,我们还需要安装ARM的编译工具链(CMSIS包),这是生成和编译代码所必需的。

以下是安装ARM工具链的步骤:
- 在同一个“管理嵌入式软件包”窗口中,切换到 ARM 标签页。
- 在列表中找到 CMSIS 包。
- 选择可用的最新版本。
- 点击 Install Now(立即安装)按钮,等待下载和安装完成。
所有软件包安装完毕后,关闭管理窗口即可。
本节课中我们一起学习了如何为STM32CubeMX配置正确的Java环境,并安装了STM32 F4微控制器的支持包以及ARM CMSIS工具链包。这些步骤为后续生成和开发ARM汇编项目打下了必要的基础。下一节课,我们将开始创建第一个工程。
052:STM32CubeMX 快速概览 🚀
在本节课中,我们将快速概览 STM32CubeMX 软件,学习如何创建新项目、配置微控制器引脚和时钟,并生成初始化代码。


项目创建与界面概览
上一节我们介绍了 ARM 汇编的基础知识,本节中我们来看看如何使用 STM32CubeMX 工具来配置硬件项目。
启动 STM32CubeMX 后,主界面提供了几个核心选项。以下是创建新项目的几种方式:
- MCU 选择器:直接根据微控制器型号选择。
- 开发板选择器:根据 ST 官方开发板型号选择。
- 示例项目选择器:从现有示例项目开始。

界面顶部包含多个选项卡。以下是各选项卡的主要功能:
- 文件:用于新建、加载或列出最近的项目。
- 窗口:用于调整字体大小等界面设置。
- 帮助:此选项卡非常重要,可以检查更新、管理嵌入式软件包(如蓝牙协议栈)以及设置用户偏好。

选择目标硬件


我们可以通过开发板选择器来快速开始一个项目。例如,选择 Nucleo-F411RE 开发板。选中后,软件会显示该板的规格、市场价格和功能列表。点击相关链接可以查看数据手册等文档。
双击选中的开发板或点击“Start Project”按钮,软件会询问是否以默认模式初始化所有外设。选择“是”后,项目即开始创建。
引脚配置与视图
项目创建后,会进入引脚配置视图。此视图直观地展示了微控制器的所有引脚。以下是配置引脚的基本操作:
- 要将某个引脚(如 PB1)设置为输出,只需点击该引脚并选择 GPIO_Output。
- 可以重命名引脚的用户标签,例如将其命名为“My_Output”。
- 引脚颜色代表其状态:绿色表示配置完成,黄色表示配置不完整。
在此视图中,可以拖拽、缩放或旋转芯片视图以便查看。
外设与系统配置
引脚配置视图的左侧是配置选项卡,用于详细设置各个外设。以下是主要配置类别:
- 系统核心:配置 GPIO、RCC(复位与时钟控制)等。例如,在 RCC 中可以选择使用外部晶振(HSE)或内部时钟(HSI)。
- 模拟:配置 ADC(模数转换器)等模拟外设。
- 定时器:配置通用定时器、高级定时器和实时时钟。
- 连接性:配置通信接口,如 I2C、SPI 和 UART。例如,配置 UART 时,可以设置波特率、字长等参数,公式如下:
波特率 = 外设时钟频率 / (采样率 * (分频值)) - 中间件:可以添加文件系统(FATFS)、实时操作系统(如 FreeRTOS)或 USB 设备支持。
时钟树配置
“时钟配置”选项卡提供了一个图形化的时钟树。在这里,可以调整系统核心时钟和各总线时钟的频率。例如,可以将主频设置为允许的最大值(如 100 MHz)。配置时需确保所有时钟路径有效,否则相关设置会显示为红色提示错误。
项目管理与代码生成
所有硬件配置完成后,需要切换到“项目管理”选项卡来设置项目并生成代码。以下是关键步骤:
- 输入项目名称。
- 选择项目保存位置。
- 在“工具链/IDE”下拉菜单中选择目标 IDE,例如 STM32CubeIDE 或 Makefile。
- 在“代码生成器”选项卡中,建议勾选“为每个外设生成独立的
.c/.h文件”,这有助于代码管理。 - 最后,点击“生成代码”按钮。软件会根据所有配置,自动生成完整的初始化代码和项目文件。
此外,还可以通过“文件”菜单下的“生成报告”功能,创建一份包含所有引脚分配、时钟设置和外设配置的 PDF 或文本报告,便于存档和查阅。
总结



本节课中我们一起学习了 STM32CubeMX 的基本使用方法。我们了解了如何创建新项目、通过图形界面配置微控制器的引脚和外设、调整系统时钟,并最终生成针对特定 IDE 的初始化代码。这个工具极大地简化了 STM32 微控制器的启动过程,让开发者能更专注于应用逻辑的开发。
053:课程结束语与后续指引 🎓
在本节课中,我们将对这门ARM汇编语言课程进行总结,并为您提供后续学习和获取帮助的指引。
课程总结
上一节我们完成了核心知识的学习,本节中我们来看看课程的收尾部分。
这门课程到此告一段落。非常感谢您坚持学习到了最后。
后续学习与支持
您可以时常回顾本课程,因为我将持续添加新的内容并更新课程。
如果您有任何疑问或建议,请通过以下方式联系我:
- 发送私信给我。
- 在课程的问题讨论区留言。
最终致谢
再次表示衷心的感谢。😊
本节课中我们一起学习了课程的总结,并了解了如何获取后续更新与支持。希望本课程为您打下了坚实的ARM汇编语言基础。

浙公网安备 33010602011771号