基于寄存器的STM32开发指南:使用Keil MDK创建工程模板
引言
本文介绍如何使用 KEIL μVision5 (KEIL5) 创建一个基于寄存器(裸寄存器操作)的 STM32 工程模板,并在此模板基础上用寄存器直接控制 GPIO,从而点亮开发板上的 LED。阅读本章后,你应该能够:
- 准备工程所需的基本文件并建立目录结构;
- 在 KEIL5 中新建工程并完成必要配置(Target / Output / Debug / Flash 等);
- 理解 GPIO 的硬件结构与寄存器含义;
- 用寄存器方式配置 GPIO 并点亮 LED(含注意项与推荐实践)。
说明:本文以 STM32F103 系列(示例用 STM32F103VE )为例。若你使用的芯片不同,请参考相应芯片的参考手册与设备头文件(stm32f10x.h / CMSIS)进行对应修改。建议在真实工程中尽量使用厂商提供的 CMSIS 或芯片头文件,而不是手工编写全部寄存器地址;本文示例为教学目的,展示寄存器映射与寄存器级操作。
1. 工程模板基础文件准备
1.1 创建工程目录结构
首先,在计算机的合适位置创建一个名为 template 的文件夹作为工程根目录。在该文件夹下建立两个子文件夹:
- Obj文件夹:用于存放编译生成的中间产物(对象文件、link 列表、调试信息、HEX/BIN 等)。
- User文件夹:用于存放用户代码(main.c)、设备头文件(stm32f10x.h)与启动文件(startup_stm32f10x_hd.s)等。
目录示例:
template/
├─ Obj/
└─ User/├─ main.c├─ startup_stm32f10x_hd.s└─ stm32f10x.h
1.2 获取必备基础文件
将以下三个核心文件放入 User 文件夹:
- main.c:主程序入口文件,大部分应用逻辑都写在这里。
- 启动文件(startup_stm32f10x_hd.s):主要作用是完成系统的最基本初始化,例如堆栈指针的设置、中断向量表的定义等。初学者一般不需要修改这个文件。
- 设备头文件(stm32f10x.h):包含了 STM32 芯片相关的寄存器定义、地址映射和外设结构体。通过它,我们就可以用 “结构体成员”的形式直接访问寄存器,而不用死记硬背寄存器地址。
这些文件可以从ST官方提供的标准外设库或HAL库中获取,也可以从开发板供应商提供的资料中提取,也可以通过以下提供的百度网盘链接下载。
通过百度网盘分享的文件:寄存器模板创建基础文件.zip
链接:https://pan.baidu.com/s/1HpMSt4w-Oy3dddy2R3juWg
提取码:22y9
接下来就开始使用KEIL5软件正式创建工程。
2. 创建寄存器工程
2.1 新建Keil工程
打开 KEIL μVision → Project → New μVision Project,选择工程目录(即 template
),将工程命名为"Template"(建议使用英文命名以避免路径编码问题),再点击保存。具体步骤如下:
2.2 选择 CPU 型号
在弹出的设备选择对话框中选择目标 MCU(示例:STM32F103VE)
注意:如果列表中找不到所需的CPU型号,可能是因为KEIL5没有添加相应的设备库。KEIL5不像KEIL4那样自带大量MCU型号,需要手动安装STM32芯片包。具体安装方法可以参考网络上的相关教程。
在选择设备后,通常会弹出 “Manage Run-Time Environment” 对话框。因为我们使用裸寄存器方式,可以关闭该对话框(无需添加固件包中的中间件或 HAL)。
2.3 添加文件到工程
向新建的工程中添加文件。双击 Source Group 1,将 User 文件夹下的 main.c、startup_xxx.s、stm32f10x.h 等文件加入工程。操作步骤如下:
添加文件后,工程结构应包含main.c、启动文件和头文件。如下所示
2.4 配置工程选项(魔术棒选项卡)
这一步的配置工作非常重要,主要决定编译后代码的运行方式,因此每一项参数都要与硬件相符。许多人遇到无法生成HEX文件或printf无法输出信息的问题,大多是在这里配置不当导致的。
2.4.1 Target选项配置
点击魔术棒图标(Options for Target),在弹出的窗口中选择Target选项卡,选中微库"Use MicroLib",这主要是为了后面printf重定向输出使用。其他设置保持默认即可。配置如下:
2.4.2 Output选项配置
选择Output选项卡,点击"Select Folder for Objects"按钮,把输出文件夹定位到工程目录Template下的Obj文件夹。如果需要生成hex文件,需勾选"Create HEX File"选项。配置如下:
2.4.3 Listing选项配置
选择Listing选项卡,点击"Select Folder for Listings"按钮,把输出文件夹定位到工程目录下的Listing文件夹。其他设置保持默认。配置如下:
2.5 下载器(Debug / Flash)配置
在仿真器连接好电脑和开发板且开发板供电正常的情况下,进行下载器配置。具体过程看图示:
2.5.1 Debug设置
在Debug选项卡中选择使用的调试器(如CMSIS-DAP、J-Link或ST-Link)。
选择好调试器型号后点击Settings,会弹出调试器配置界面。正常情况下会自动识别ARM仿真器ID,然后可以设置SW或JTAG模式以及复位方式。
推荐:使用SW模式时,须勾选前面的SWJ复选框。高版本的KEIL5可能默认会勾选上。
具体设置步骤如下:
2.5.2 Flash Download设置
选择目标芯片的 Flash 类型与擦写策略。例如,STM32F103ZET6芯片的Flash为512K。
技巧:建议勾选 “Reset and Run”(程序下载后自动复位运行,否则需要按下复位键才能运行);擦除策略通常选 Erase Sectors(比Erase Full Chip 快)。
具体配置如下:
2.5.3 Utilities设置
在Utilities选项卡中勾选"Use Debug Driver"。
完成以上配置后,保存设置并关闭对话框。至此,一个最小的裸工程目录已搭建完成,后续所有代码均在此基础上添加。
3. 编写测试程序与下载
3.1 最基本的 main 框架
在 main.c 中写入基本框架(示例):
#include "stm32f10x.h"/* 主函数 */
int main(void)
{/* 系统初始化(如果需) */while (1){/* 主循环 */}
}
如果启动文件中引用了 SystemInit()(大多数官方 startup 都会调用),而你不打算使用库版本的 SystemInit,可以在 main.c 中提供一个空的实现以避免链接错误:
void SystemInit(void)
{/* 暂不配置系统时钟,使用芯片上电默认时钟(通常 HSI) */
}
注:长远来看应实现或使用厂商提供的 SystemInit 来正确配置时钟树(HSE/PLL 等)。
3.2 编译与下载程序
点击编译(Build),若显示 0 error, 0 warning,则说明工程结构与配置基本正确。
连接好仿真器/调试器与开发板电源后,点击 KEIL 中的 LOAD 按钮下载程序到开发板芯片中。若 Flash Download 设置了 “Reset and Run”,下载完成后程序会自动复位并运行。
程序下载后,Build Output选项卡如果打印出"Application running..."则表示程序下载成功。如果没有出现实验现象,尝试按复位键。当然,这只是一个工程模板,我们还没写实际功能程序,开发板不会有任何现象。
至此,一个新的工程模板新建完毕
4. 使用寄存器点亮第一个LED
不论学习什么单片机,最简单的外设莫过于IO口的高低电平控制。本章将介绍如何在创建好的寄存器模板上,通过控制STM32寄存器使开发板上的LED灯点亮。
学习建议:学习本章时可以参考《STM32F1xx中文参考手册》的"通用和复用功能I/O(GPIO和AFIO)"章节,特别是在涉及到寄存器功能部分。
4.1 STM32 GPIO 介绍
GPIO(general purpose input/output,通用输入输出端口)是STM32可编程控制的引脚,用于实现芯片与外部设备的数据交换、控制及采集功能。STM32的GPIO引脚采用分组管理方式,每组包含16个引脚,以STM32F103VET6为例,该芯片配备GPIOA至GPIOE共5组GPIO接口。
所有GPIO引脚都支持基本的输入输出功能, 最基本的输出功能是由STM32控制引脚输出高、低电平,实现开关控制,如把GPIO引脚接入 到LED灯,那就可以控制LED灯的亮灭,引脚接入到继电器或三极管,那就可以通过继电器或 三极管控制外部大功率电路的通断。 最基本的输入功能是检测外部输入电平,如把GPIO引脚连接到按键,通过电平高低区分按键是 否被按下。这类接口在STM32芯片引脚资源中占据主要部分,为设备控制提供了灵活便捷的硬件支持。
以STM32F103VET6为例,此芯片共有144引脚, 芯片引脚图如下图所示。
那么是不是所有引脚都是GPIO呢?当然不是,STM32引脚可以分为这么几大类:
- 电源引脚:引脚图中的VDD、VSS、VREF+、VREF-、VSSA、VDDA等都属于电源引脚。
- 晶振引脚:引脚图中的PC14、PC15和OSC_IN、OSC_OUT都属于晶振引脚,也可以作为普通引脚使用。
- 复位引脚:引脚图中的NRST属于复位引脚,不做其他功能使用。
- 下载引脚:引脚图中的PA13、PA14、PA15、PB3和PB4属于JTAG或 SW 下载引脚。也可以作为普通引脚或者特殊功能使用,具体的功能可以查看芯片数据手册,里面都会有附加功能说明。当然,STM32的串口功能引脚也是可以作为下载引脚使用。
- BOOT引脚:引脚图中的BOOT0和PB2(BOOT1)属于BOOT引脚,PB2还可以作为普通管脚使用。在STM32启动中会有模式选择,其中就是依靠着BOOT0 和BOOT1 的电平来决定。
- GPIO引脚:引脚图中的PA、PB、PC、PD等均属于GPIO引脚。从引脚图可以看出,GPIO占用了STM32芯片大部分的引脚。并且每一个端口都有16个引脚,比如PA端口,它有PA0-PA15。其他的PB、PC等端口是一样的。
每个GPIO端口都有16个引脚(如PA0-PA15)。要了解具体某个引脚的功能,可以查阅STM32芯片数据手册中的引脚定义表。
4.2 GPIO 框图剖析
通过GPIO硬件结构框图,就可以从整体上深入了解GPIO外设及它的各种应用模式。该图从最右端看起,最右端就是代表STM32芯片引出的GPIO引脚,其余部件都位于芯片内部。
下面我们按图中的编号对GPIO端口的结构部件进行说明。
4.2.1 保护二极管及上、下拉电阻
引脚的两个保护二级管可以防止引脚外部过高或过低的电压输入,当引脚电压高于VDD时,上方的二极管导通,当引脚电压低于VSS时,下方的二极管导通。这样可以防止不正常电压引入芯片导致芯片烧毁。
注意:尽管有这样的保护,并不意味着STM32的引脚能直接外接大功率驱动器件。驱动电机等大功率设备时,必须增加大功率及隔离电路。
4.2.2 P-MOS 管和 N-MOS 管
GPIO引脚线路通过保护二极管后分为两个方向:向上进入"输入模式"结构,向下进入"输出模式"结构。输出模式部分包含由P-MOS和N-MOS管构成的单元电路,该结构使GPIO支持"推挽输出"和"开漏输出"两种工作模式。
推挽输出模式:该名称源自两个MOS管的工作方式。在该结构中输入高电平时, 经过反向后,上方的P-MOS导通,下方的N-MOS截止,对外输出高电平;而在该结构中输入低电平时,经过反向后,N-MOS管导通,P-MOS关闭,对外输出低电平。在电平切换过程中,两个MOS管交替导通:P管负责灌电流,N管负责拉电流。这种结构显著提升了负载能力和开关速度。推挽输出的低电平为0伏,高电平为3.3伏,具体参考图推挽等效电路,它是 推挽输出模式时的等效电路。
开漏输出模式:此时上方的P-MOS管完全不工作。如果我们控制输出为0,低电平,则P-MOS 管关闭,N-MOS管导通,使输出接地,若控制输出为1(处于浮空状态,无法直接输出高电平),则P-MOS 管和N-MOS管都关闭,所以引脚既不输出高电平,也不输出低电平,为高阻态。由于开漏输出无法主动输出高电平,必须外接上拉电阻至电源(如VCC)。当引脚为高阻态时,上拉电阻将引脚电平拉高至电源电压。参考图开漏电路中等效电路,这种设计允许多个开漏引脚并联实现“线与”逻辑,也就是说,若有很多个开漏模式引脚连接到一起时,只有当所有引脚都输出高阻态,才由上拉电阻提供高电平,此高电平的电压为外部上拉电阻所接的电源的电压。若其中一个引脚为低电平,那线路就相当于短路接地,使得整条线路都被拉低至低电平0V。
“线与”逻辑特性
多个开漏引脚并联时,输出状态遵循以下规则:
- 所有引脚为高阻态:上拉电阻提供高电平,电压值为外部电源电压。
- 任一引脚输出低电平:整条线路被强制拉低至0V,其他高阻态引脚无效。
推挽输出模式一般应用在输出电平为0和3.3伏而且需要高速切换开关状态的场合。在STM32 的应用中,除了必须用开漏模式的场合,我们都习惯使用推挽输出模式。 开漏输出一般应用在I2C、SMBUS通讯等需要“线与”功能的总线电路中。除此之外,还用在电平不匹配的场合,如需要输出5伏的高电平,就可以在外部接一个上拉电阻,上拉电源为5伏, 并且把GPIO设置为开漏模式,当输出高阻态时,由上拉电阻和电源向外输出5伏的电平,具体见图STM32_IO对外输出5V电平。
4.2.3 输出数据寄存器
前面提到的双 MOS 管结构电路,其输入信号就是由 端口输出数据寄存器 (GPIOx_ODR) 控制。也就是说,当我们修改 GPIOx_ODR 的相应位时,就能直接改变对应 GPIO 引脚的输出电平。
// 示例:普通 GPIO 输出,GPIOB 的 16 个 IO 全部置 1
GPIOB->ODR = 0xFFFF;
此外,端口位设置/清除寄存器 (GPIOx_BSRR) 并不会绕过 ODR,而是通过对 ODR 的特定位进行置位或清零操作,从而间接改变 GPIO 引脚的输出状态。换句话说,BSRR 是一种更高效、更安全的方式来修改 ODR 的某个位,避免了对整个 ODR 寄存器进行读-改-写操作所可能带来的竞争风险。
4.2.4 复用功能输出
所谓 “复用功能输出”,其中“复用”是指 STM32 片上其它外设能够控制 GPIO 引脚,此时 GPIO 引脚不再单纯作为普通 I/O,而是作为某个外设的功能引脚来使用,相当于该引脚的 第二用途。
在硬件结构上,来自 GPIO 数据寄存器 (ODR/BSRR) 的输出信号,以及 来自外设的复用功能信号,都连接到引脚前的 双 MOS 管结构。通过一个 控制开关(框图里的梯形选择器) 来决定究竟由哪个信号驱动引脚。
例如,当我们将某个引脚配置为 USART 发送引脚 (TX) 时,该引脚就由 USART 外设 接管,外设会在该引脚上输出串口数据,而不是由 ODR/BSRR 控制。
4.2.5 输入数据寄存器
在 GPIO 的结构框图上半部分,可以看到 GPIO 引脚首先经过 可选的上拉/下拉电阻,然后进入一个 施密特触发器。
- 上拉/下拉电阻可以将悬空引脚稳定在高电平或低电平。
- 施密特触发器将模拟电平信号转化为 数字信号 (0 或 1),并增强信号的抗干扰能力。
最终得到的数字信号会存储在 输入数据寄存器 (GPIOx_IDR) 中。我们只需读取 IDR
寄存器,就能获知当前每个引脚的电平状态。
// 示例:读取 GPIOB 端口的 16 位电平状态
uint16_t temp;
temp = GPIOB->IDR;
4.2.6 复用功能输入
与 复用功能输出 类似,在 复用功能输入模式 下,GPIO 引脚上的信号不再送入 IDR
供软件读取,而是直接传输给某个 片上外设。
例如,将某个引脚配置为 USART 接收引脚 (RX) 时,该引脚的输入信号就会被 USART 外设 采集,用于接收来自外部设备的数据,而不是作为普通 GPIO 输入由 IDR
读取。
换句话说:
- 普通输入模式 → 电平状态存储在
IDR
,CPU 可读取。 - 复用输入模式 → 电平状态直接送入外设,由外设读取和处理。
4.2.7 模拟输入输出
在某些情况下,GPIO 引脚需要处理 模拟信号,这时必须绕过数字电路。
- 模拟输入模式 (ADC):当引脚作为 ADC 通道时,必须接收原始的模拟电压信号。此时信号不会经过施密特触发器,否则会被转化成 0/1 的数字电平,导致丢失模拟特性。因此,ADC 直接采集引脚上的电压值。
- 模拟输出模式 (DAC):当引脚作为 DAC 输出通道时,DAC 产生的模拟电压信号也不会经过双 MOS 管结构,而是直接输出到引脚,保证信号的连续性和精确度。
这就是 GPIO 在 模拟输入/输出 模式下的特殊之处:它绕过了 GPIO 的数字逻辑,保持信号的原始模拟特性。
4.3 GPIO 工作模式
由 GPIO 的内部电路结构决定,GPIO 引脚可以配置成多种不同的工作模式,以适应不同的应用场景。
在固件库中,GPIO 总共有 8 种细分的工作模式,定义如下:
typedef enum
{GPIO_Mode_AIN = 0x00, // 模拟输入 (Analog Input)GPIO_Mode_IN_FLOATING = 0x04, // 浮空输入 (Floating Input)GPIO_Mode_IPD = 0x28, // 下拉输入 (Input Pull-Down)GPIO_Mode_IPU = 0x48, // 上拉输入 (Input Pull-Up)GPIO_Mode_Out_OD = 0x14, // 开漏输出 (Output Open-Drain)GPIO_Mode_Out_PP = 0x10, // 推挽输出 (Output Push-Pull)GPIO_Mode_AF_OD = 0x1C, // 复用开漏输出 (Alternate Function Open-Drain)GPIO_Mode_AF_PP = 0x18 // 复用推挽输出 (Alternate Function Push-Pull)
} GPIOMode_TypeDef;
从功能角度来看,这 8 种模式大致可以归类为以下 三大类:
4.3.1 输入模式 (模拟 / 浮空 / 上拉 / 下拉)
在输入模式下:
- 施密特触发器开启,负责将模拟电压信号转化为稳定的数字信号 (0 或 1)。
- 输出功能被禁止,此时只能读取引脚电平,不能输出。
- 引脚的状态存储在 输入数据寄存器 (GPIOx_IDR) 中,通过读取该寄存器即可获知引脚电平。
输入模式分为四种:
- 上拉输入 (Pull-Up):默认由内部上拉电阻将引脚电平保持在高电平。
- 下拉输入 (Pull-Down):默认由内部下拉电阻将引脚电平保持在低电平。
- 浮空输入 (Floating):引脚电平完全由外部电路决定,没有固定电平。常用于按键输入,因为按下/释放时电平会发生明显变化。
- 模拟输入 (Analog Input):直接绕过施密特触发器,将原始电压信号提供给 ADC 采集。
4.3.2 输出模式 (推挽 / 开漏)
在输出模式下:
- 引脚的高低电平由 输出数据寄存器 (GPIOx_ODR) 控制。
- 施密特触发器仍然开启,因此引脚的电平状态也能通过 输入数据寄存器GPIOx_IDR 读取,便于检测实际输出是否正确。
输出模式分为两种:
- 推挽输出 (Push-Pull):双 MOS 管交替导通,可以可靠输出高电平或低电平。适合驱动 LED、数字电路等需要稳定高低电平的场景。
- 开漏输出 (Open-Drain):仅 N-MOS 管导通,可以输出低电平或高阻态(相当于断开)。如果需要输出高电平,必须外接上拉电阻。常用于 I²C、1-Wire 等总线通信。
另外,GPIO 输出速度可配置为 2MHz、10MHz 或 50MHz,此处的输出速度即 I/O 高低电平切换的最高频率。
- 输出速度越高,切换频率越快,但功耗也更大。
- 如果对功耗没有严格要求,通常选择 最大速度 (50MHz),保证信号响应能力。
4.3.3 复用功能模式 (推挽 / 开漏)
在复用功能模式下:
- 引脚仍然可以配置成推挽或开漏输出。
- 输出信号不再来自 GPIOx_ODR,而是由片上外设产生(例如 USART、SPI、TIM 等)。
- 输出速度可配置,同样支持 2MHz / 10MHz / 50MHz。
- 输入功能依然可用,引脚电平会被采样到
IDR
,但在实际应用中,一般通过对应外设的寄存器获取信号。
例如:
- 当引脚配置为 USART_TX 时,引脚上的电平变化由 USART 外设控制,而不是 ODR。
- 当引脚配置为 USART_RX 时,输入数据由 USART 外设读取,而不是直接读取 IDR。
4.4 硬件连接
本教程中,STM32芯片与LED灯的连接方式如图"LED灯电路连接图"所示。这是一个RGB灯,里面由红、蓝、绿三个LED组成,通过PWM控制可实现256种不同的颜色混合效果。
在实验电路中,三个 LED 灯的 阳极 统一连接到 3.3V 电源,阴极 各自通过一个限流电阻后,再连接到 STM32 的三个 GPIO 引脚。这样一来,只要我们控制这三个 GPIO 引脚的 输出电平,就可以决定对应 LED 的亮灭状态:
- 当 GPIO 引脚输出 低电平 时,引脚相当于接地,电流从 3.3V → LED → 电阻 → GPIO 流动,LED 点亮。
- 当 GPIO 引脚输出 高电平 时,LED 两端电压相等,没有电流流过,LED 熄灭。
如果你所使用的开发板上,LED 灯的接线方式或极性与这里的电路不同,只需要在程序中修改对应的 GPIO 引脚配置即可,其基本工作原理都是相同的:通过 GPIO 的高低电平控制电流通断,从而控制 LED 的亮灭。
在本实验中,我们的目标是:
- 将 GPIO 引脚配置为 推挽输出模式 (Push-Pull Output);
- 默认输出 低电平,这样连接的 LED 就会在程序运行后自动点亮。
4.5 软件实现:使用寄存器点亮 LED
本小节将通过一个实例,讲解如何直接通过 寄存器控制 来点亮 LED 灯。需要说明的是,这里主要侧重于原理的学习,建议大家先用我们提供的实验例程在 KEIL5 中运行并阅读源码。等熟悉了原理之后,再尝试自己从零开始新建一个工程。
本节配套例程名称为 “8-使用寄存器点亮LED灯”。
在实验工程目录中找到后缀名为 “.uvprojx” 的文件,用 KEIL5 打开即可。配套例程链接: https://pan.baidu.com/s/1ZX-7bnSd5Cjt1qsXfIrUmQ 提取码: 4urc
打开该工程,见图工程文件结构 ,可看到一共有三个文件,分别 startup_stm32f10x_hd.s 、 stm32f10x.h 以及 main.c,下面我们对这三个文件进行讲解。
4.5.1 startup_stm32f10x_hd.s 文件
4.5.1.1 启动文件简介
在 STM32 的工程中,startup_stm32f10x_hd.s
文件被称为 启动文件。它是由汇编语言编写的程序,当 STM32 芯片上电启动时,首先会执行启动文件中的代码,以便为 C 语言程序建立运行环境。
注:由于该文件使用的是 Cortex-M3 内核支持的汇编指令,如果想深入了解其细节,可以参考《Cortex-M3 权威指南》中关于指令集的章节。
startup_stm32f10x_hd.s 文件是由 ST 官方提供 的,一般情况下我们不需要从零开始编写,而是基于官方版本进行修改即可。该文件从ST固件库里面找到,找到该文件后把启动文件添加到工程里面即可。不同芯片型号和编译器环境所使用的启动文件可能有所不同,但它们的核心功能是一致的。
4.5.1.2 启动文件的主要功能
总结起来,启动文件负责完成以下任务:
- 初始化堆栈指针 SP —— 设置栈空间的起始位置,用于存放局部变量、函数返回地址等。
- 初始化程序计数器 PC —— 确定程序复位后的入口地址,保证程序能从正确位置开始执行。
- 设置堆、栈大小 —— 为程序运行分配必要的存储空间。
- 初始化中断向量表 —— 建立外部中断和异常的入口地址映射。
- 配置外部 SRAM(可选) —— 如果开发板有外部 SRAM,可通过启动文件配置为数据存储区;没有的话则跳过。
- 调用 SystemInit() —— 完成系统时钟初始化,例如配置 AHB、APB 总线的时钟。
- 调用 __main —— 这是 C 库的初始化入口,负责堆、栈、全局变量的初始化,最后跳转到用户编写的 main() 函数。
对于初学者而言,最需要理解的是最后两点:STM32 上电复位后,会执行 SystemInit() 来完成时钟初始化,然后跳转到 C 语言的 main() 函数,进入我们熟悉的 C 世界。
4.5.1.3 启动文件关键代码解析
在启动文件中,有一段在复位后立即执行的汇编程序,名为 Reset_Handler。在实际工程中阅读时,可使用编辑器的搜索(快捷键:Ctrl+F)功能查找这 段代码在文件中的位置,搜索Reset_Handler即可找到。
代码如下:
; Reset handler
Reset_Handler PROCEXPORT Reset_Handler [WEAK]IMPORT __mainIMPORT SystemInitLDR R0, =SystemInitBLX R0 LDR R0, =__mainBX R0ENDP
下面我们逐行解释:
- ; ——> 汇编中的注释符,相当于 C 语言的 //。
- Reset_Handler PROC ——> 定义一个子程序:Reset_Handler。PROC是子程序定义伪指令。相当于 C 中的函数 void Reset_Handler() {}。
- EXPORT Reset_Handler [WEAK] ——> 声明 Reset_Handler 可被外部模块使用,相当于C语言的函数声明。[WEAK] 表示弱定义(如果编译器发现在别处有同名函数,则在链接时用别处的地址进行链接。如果其它地方没有定义,编译器也不报错,以此处地址进行链接)。
- IMPORT SystemInit & IMPORT __main ——> 告诉编译器这两个函数在别的文件中定义,需要在链接时去其他文件去寻找。相当于C语言中,从其它文件引入函数声明。以便下面对外部函数进行调 用。
关于 SystemInit 和 __main
- SystemInit():由我们在工程中定义(固件库里已提供默认实现)。主要任务是配置系统时钟,使芯片运行稳定。
- __main:注意不要和我们写的
main()
混淆!这是 C 标准库提供的入口函数,负责编译器层面的初始化(栈、堆、全局变量等)。在它的最后会调用我们熟悉的main()
,正式进入 C 语言世界。
- LDR R0, =SystemInit ——> 把 SystemInit 函数的入口地址加载到寄存器 R0。
- BLX R0 ——> 程序跳转到R0中的地址执行程序,即执行SystemInit函数,配置系统时钟。
- LDR R0, =__main ——> 把 __main 的入口地址加载到 R0。
- BX R0 ——> 程序跳转到R0中的地址执行程序,即执行__main函数,完成堆、栈初始化,最终进入用户编写的 main()。
- ENDP ——> 标志子程序结束。
总之,看完这段代码后,了解到如下内容即可:我们需要在外部定义一个SystemInit函数设置 STM32 的时钟;STM32上电后,会执行SystemInit函数,最后执行我们C语言中的main函数。
4.5.1.4 小结
通过阅读这段代码,我们需要理解的核心点是:
- SystemInit():由我们自己实现或使用 ST 提供的版本,主要负责配置系统时钟和总线。
- __main:这是 C 库的入口函数,完成运行环境初始化,最后调用 main()。
- Reset_Handler:STM32 上电复位后执行的第一段程序,最终会把程序引导到 main()。
因此,STM32 启动流程大致是:
上电复位 → 执行启动文件 → 调用 SystemInit() → 调用 __main → 进入用户 main()。
4.5.2 stm32f10x.h 文件
在前一小节中,我们学习了启动文件的作用,STM32 上电后最终会执行到 main()
函数。
但如果想在 main()
中控制 LED 灯亮灭,就必须能够操作 GPIO 引脚。而 GPIO 的控制依赖于寄存器。
简单理解,寄存器就是片上外设暴露出来的一块特殊内存区域,这些寄存器在编译器看来就是一串固定地址的内存,我们通过指针访问这些固定地址,就能间接操作硬件。所以,在编程之前,必须先把这些寄存器的地址“翻译”成 C 语言能理解的形式,这就是寄存器映射。
在 STM32 工程中,寄存器映射相关的宏定义和结构体,通常统一写在 stm32f10x.h 文件里。可以把它看作是 STM32 寄存器的“说明书”,有了它,我们才能用 C 语言的方式去读写硬件寄存器。
/*本文件用于添加寄存器地址及结构体定义*//*片上外设基地址 */
#define PERIPH_BASE ((unsigned int)0x40000000)/*APB2 总线基地址 */
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
/* AHB总线基地址 */
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)/*GPIOB外设基地址*/
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)/* GPIOB寄存器地址,强制转换成指针 */
#define GPIOB_CRL *(unsigned int*)(GPIOB_BASE+0x00)
#define GPIOB_CRH *(unsigned int*)(GPIOB_BASE+0x04)
#define GPIOB_IDR *(unsigned int*)(GPIOB_BASE+0x08)
#define GPIOB_ODR *(unsigned int*)(GPIOB_BASE+0x0C)
#define GPIOB_BSRR *(unsigned int*)(GPIOB_BASE+0x10)
#define GPIOB_BRR *(unsigned int*)(GPIOB_BASE+0x14)
#define GPIOB_LCKR *(unsigned int*)(GPIOB_BASE+0x18)/*RCC外设基地址*/
#define RCC_BASE (AHBPERIPH_BASE + 0x1000)
/*RCC的AHB1时钟使能寄存器地址,强制转换成指针*/
#define RCC_APB2ENR *(unsigned int*)(RCC_BASE+0x18)
GPIO 外设的地址跟前面章节讲解的相同,不过此处把寄存器的地址值都直接强制转换成了指针, 方便使用。代码的最后两段是RCC外设寄存器的地址定义,RCC外设是用来设置时钟的,以后我们会详细分析,本实验中只要了解到使用GPIO外设必须开启它的时钟即可。
4.5.3 main 文件
前面我们已经准备好了启动文件(startup_stm32f10x_hd.s)和寄存器映射文件(stm32f10x.h)。现在就可以进入 main 文件来编写我们点亮 LED 的第一个程序了。
4.5.3.1 main 函数的骨架
C 语言程序必须有一个 main() 函数作为入口。刚开始时,我们可以先写一个空的 main() 函数:
int main (void)
{}
如果此时直接编译,编译器会报错:
Error: L6218E: Undefined symbol SystemInit (referred from startup_stm32f10x.o)
错误提示SystemInit 没有定义。在分析启动文件时,我们知道 Reset_Handler 会调用 SystemInit 来初始化系统时钟。但目前我们还没有写这个函数。为了先通过编译,我们可以在 main.c 中写一个空的 SystemInit() 函数,目的是骗过编译器,这样再次编译,就不会再报错了
注意:实际上 SystemInit 应该用来配置 STM32 的系统时钟,我们后续章节会补充完整。这里先写个空函数,只是“占个位置”。当我们不配置系统时钟时,STM32会把HSI当作系统时钟,HSI=8M,由芯片内部的振荡器提供。
在 main 中添加如下函数:
// 函数为空,目的是为了骗过编译器不报错
void SystemInit(void)
{}
这时再编译就没有错了,完美解决。
补充:另一个办法是直接修改启动文件,把调用 SystemInit
的代码注释掉。不过这种方法不推荐,因为会破坏启动文件的完整性。见代码清单:点亮LED-4注释掉启动文件中调用 SystemInit 的代码
; Reset handler
Reset_Handler PROCEXPORT Reset_Handler [WEAK];IMPORT __mainIMPORT SystemInit;LDR R0, =SystemInit;BLX R0 LDR R0, =__mainBX R0ENDP
接下来在main函数中添加代码,实现我们的点灯之旅。
4.5.3.2 GPIO 模式配置
在 STM32 中,每个 GPIO 引脚都需要通过寄存器进行配置。以 PB0 为例,它的模式配置寄存器是 GPIOB_CRL(低 8 个引脚 0~7 的配置寄存器)。
CRL 寄存器的结构
CRL(端口配置低寄存器,GPIOx_CRL)专门配置 0~7 号引脚。每个引脚占用 4 个比特位(即一个“寄存器位段”):
- MODE[1:0] :控制输出速度,或者设置为输入。
- CNF[1:0] :控制输入/输出模式。
如下表(以 PB0 为例,其他引脚依次类推):
引脚 | 位段位置 | 配置位 | 说明 |
---|---|---|---|
PB0 | [3:0] | MODE0[1:0], CNF0[1:0] | 控制 PB0 |
PB1 | [7:4] | MODE1[1:0], CNF1[1:0] | 控制 PB1 |
PB2 | [11:8] | MODE2[1:0], CNF2[1:0] | 控制 PB2 |
… | … | … | … |
PB7 | [31:28] | MODE7[1:0], CNF7[1:0] | 控制 PB7 |
也就是说,每个引脚占 4 位,第 n 个引脚 的配置位段就是 [4n+3 : 4n]。例如:PB0 的配置位就是 [3:0],PB1 的配置位就是 [7:4]。
配置 PB0
在本实验中,我们需要让 PB0 输出低电平来点亮 LED,所以要配置成通用推挽输出(CNF=00),速度为 10MHz(MODE=01)。对应的寄存器值:
位段 | MODE[1:0] | CNF[1:0] | 二进制值 | 含义 |
---|---|---|---|---|
PB0 | 01 | 00 | 0001b | 通用推挽输出,10MHz |
对应 PB0 的配置代码为:
// 清空控制 PB0 的配置位(每个引脚占 4 位,这里是第 0 组)
GPIOB_CRL &= ~(0x0F << (4 * 0));// 配置 PB0 为通用推挽输出,速度为 10MHz
GPIOB_CRL |= (1 << (4 * 0));
在代码中,我们先把控制PB0的端口位清0,然后再向它赋值“0001b”,从而使GPIOB0引脚设置成输出模式,速度为10M。这样,PB0 就被正确配置成输出模式,可以用来控制 LED 灯了。
补充说明
配置 GPIO 时,推荐采用以下标准写法:
GPIOB_CRL &= ~(0x0F << (4 * 0)); // 清零 PB0 的配置位段
GPIOB_CRL |= (0x01 << (4 * 0)); // 设置 PB0 为推挽输出 10MHz
这种位操作方式( &=~ 和 |= )是必要的,因为寄存器不能按位独立写入,如果直接赋值,会把寄存器中其他引脚的配置覆盖掉。
举个错误示例:GPIOB_CRL = 0x00000001,虽然 PB0 被正确配置为 0001(通用推挽输出,10MHz),但会导致 PB1~PB7 的配置位被清零,相当于它们都被强制改为输入模式,这就破坏了其它 GPIO 引脚的原有配置,导致程序运行异常。
因此,正确做法是:先清零目标引脚的配置位段(不影响其他位),再写入新的配置值,这就是为什么配置寄存器时通常要用 &=~
和 |=
来操作。
4.5.3.3 控制引脚输出电平
当 GPIO 引脚已经配置为输出模式后,就可以通过相关寄存器来控制它的电平状态,从而控制 LED 的亮灭。
在 STM32 中,常见的用于控制引脚电平的寄存器有:
BSRR(端口位设置/复位寄存器):这是一个 32 位寄存器,低 16 位(位 0~15)用于设置引脚为高电平,高 16 位(位 16~31)用于复位引脚为低电平。例如:
- 写 1 到 bit 0 → PB0 输出高电平;
- 写 1 到 bit16 → PB0 输出低电平;
- 写 1 到 bit 5 → PB5 输出高电平;
- 写 1 到 bit21 → PB5 输出低电平。
这样每个 GPIO 引脚都对应 BSRR 中两个位置:低 16 位控制置位,高 16 位控制复位。
BRR(端口位复位寄存器):这是一个 16 位寄存器,bit0~bit15 分别对应 PB0~PB15。在任意 bit 上写入 1,就会把对应的引脚清零(输出低电平)。例如:
- 写 1 到 bit0 → PB0 清零(输出低电平);
- 写 1 到 bit7 → PB7 清零。
它的效果等价于 BSRR 的高 16 位部分。
ODR(端口输出数据寄存器):这是一个 16 位寄存器,bit0~bit15 分别对应 PB0~PB15 的输出状态。读取 ODR 还能得到所有引脚的当前电平状态。例如:
- 写 1 到 bit0 → PB0 输出高电平;
- 写 0 到 bit0 → PB0 输出低电平;
- 写 1 到 bit8 → PB8 输出高电平。
注意:操作 BSRR 或 BRR 时,最终影响的都是ODR寄存器,因为 ODR 是实际输出数据的寄存器。为了简单演示,我们直接操作 ODR 来控制 PB0 的电平。
// PB0 输出低电平(LED 点亮)
GPIOB_ODR &= ~(1 << 0);
4.5.3.4 开启外设时钟
前面已经完成了 GPIO 引脚配置和电平控制,但要想真正驱动外设,还必须开启外设时钟。
STM32 外设众多,为了降低功耗,芯片上电后默认外设时钟全部关闭。若要使用某个外设,必须先在 RCC(Reset and Clock Control,复位与时钟控制器) 中打开对应时钟。
在 STM32F1 系列中,所有 GPIO 外设都挂载在 APB2 总线上,时钟使能由 APB2 外设时钟使能寄存器(RCC_APB2ENR) 控制。
例如,要使能 GPIOB 外设时钟,需要设置 RCC_APB2ENR 的 bit3:
// 开启 GPIOB 端口时钟
RCC_APB2ENR |= (1 << 3);
这样,GPIOB 外设才会真正工作,否则即使寄存器配置正确,也无法驱动引脚。
4.5.3.5 完整代码
开启时钟,配置引脚模式,控制电平,经过这三步,我们总算可以控制一个LED了。现在我们 完整组织下用STM32控制一个LED的代码,见代码清单:点亮LED-8。 列表8: 代码清单:点亮LED-8main文件中控制LED灯 的代码
#include "stm32f10x.h"/* 空的 SystemInit 函数,仅为满足启动文件的调用 */
void SystemInit(void)
{/* 若需配置系统时钟(HSE/PLL),可在此实现或调用官方库函数 */
}int main(void)
{/* 1. 开启 GPIOB 外设时钟 RCC_APB2ENR 第 3 位(IOPBEN) = 1*/RCC_APB2ENR |= (1 << 3);/* 2. 配置 PB0 为通用推挽输出,速度 10MHz- PB0 属于 CRL(控制 0~7 引脚)- 每个引脚占 4 位:[CNF1 CNF0 MODE1 MODE0]- 推挽输出 10MHz: CNF=00, MODE=01 -> 0b0001*/GPIOB_CRL &= ~(0x0F << (4 * 0)); // 清除 PB0 的控制位GPIOB_CRL |= (0x1 << (4 * 0)); // 配置为 10MHz 推挽输出/* 3. 控制 PB0 输出电平- 若 LED 接共阳(常见开发板),低电平点亮- 推荐使用 BRR/BSRR 原子写,避免并发下读-改-写问题*/GPIOB_BRR = (1 << 0); // 输出低电平,点亮 LED// 若需用 ODR,也可写:// GPIOB_ODR &= ~(1 << 0);while (1){/* 主循环,LED 始终保持点亮 */}return 0; // 实际不会执行到此
}
说明:
- 对于设置/清除 IO,优先使用 BSRR(低 16 位写 1 -> 置位;高 16 位写 1 -> 复位)或 BRR(复位寄存器)进行原子写操作,避免读-改-写导致的竞态问题,尤其在中断或多任务环境下。
- 若 LED 极性相反(共阴),则要输出高电平(可用 GPIOB_BSRR = (1<<0) 设置 PB0)。
4.6 下载与验证
编译无错后使用 KEIL 的 LOAD 将程序下载到目标板。若设置了 “Reset and Run” 并且连接正确,下载完成后 LED 应立即点亮。若无反应:
- 检查开发板电源;
- 查看该引脚是否确实连接到 LED(参考板上原理图)并注意 LED 极性(共阳/共阴);
- 尝试手动复位开发板;
- 用调试器读取 GPIOB_ODR / GPIOB_IDR 检查寄存器是否如预期改变。
4.7 总结
本文以 KEIL5 为开发环境,演示了如何建立一个基于寄存器的 STM32 工程模板,并详解了 GPIO 的硬件结构与寄存器级配置方法,最后给出一个点亮 LED 的完整示例。本文尽量保持教学示例的简洁与可读性,但对于生产或复杂项目,建议使用厂商提供的头文件与库、正确实现 SystemInit,并在并发场景下使用原子寄存器(BSRR/BRR)操作与必要的同步机制。