当前位置: 首页 > news >正文

基于寄存器的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]二进制值含义
PB001000001b通用推挽输出,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)操作与必要的同步机制。

http://www.dtcms.com/a/415883.html

相关文章:

  • 有哪些做场景秀的网站网站优化销售话术
  • 高光谱成像在分析作物长势和产量预估中的应用
  • C++面向对象编程——封装
  • 优秀设计作品网站seo综合查询平台官网
  • 海城区建设局网站工会门户网站建设需求
  • 武汉市最新街景图像数据!
  • 自己动手创建一个公司网站国家通建设通网站
  • Docker(二)—— Docker核心功能全解析:网络、资源控制、数据卷与镜像构建实战
  • 百度网站大全首页网站源码免费下载
  • 网站的策划建设方案书负面信息网站
  • 绍兴网站建设方案推广微信公众平台绑定网站
  • 计算机网路-TCP
  • 做网站用哪个预装系统源码建站之网站建设
  • Hadoop完全分布式配置
  • 实用主义观点下的函数式编程思想
  • 服务器及网站建设的特点温州品牌网站建设
  • H618-开发板运行第一个Hello World
  • 青岛网站建设市场分析安徽龙山建设网站
  • 珠海网站建设王道下拉惠太原网站seo外包
  • 门户网站怎么做seo乐清网站
  • 贪心:保卫花园
  • 东莞专业微网站建设价格哪个浏览器可以进wordpress
  • HashMap和Hashtable
  • 做个网站得花多少钱建成区违法建设治理网站
  • 革新深层水平位移监测——安锐科技推出全新节段式位移计,以模块化设计显著降低成本
  • 赣州企业网站在那做网站域名的管理密码如何索取
  • 网站建设与管理大纲天津科技公司网站
  • 网站开发与维护是什么企业网站需要什么功能
  • 【有源码】基于python+spark的餐饮外卖平台综合分析系统-基于Hadoop生态的外卖平台数据治理与分析系统
  • 【心力建设】《毛选》里的心法