STM32 库入门
库 Why
单片机最基本的功能就是电平的输入/输出。在STM32中,I/O引脚可以被软件设置成不同的功能,包括输入和输出,因此又被称为GPIO(General-Purpose I/O)。而STM32的GPIO引脚又分为 GPIOA 、GPIOB 一直到 GPIOG 几个端口,每个端口可以控制0号到15号各个位。
在学习51单片机的过程中,都是通过直接操作单片机的寄存器,来改变51单片机的运行状态。STM32当然也支持这么做,不过在使用STM32时,更建议使用官方库来操作单片机。
例如,如果需要让51单片机对外输出 8 个高电平来控制数码管,可以控制51单片机的 P0 口,让其全部输出高电平,在不包含任何头文件的情况下,其语句类似:
该操作分为两步:首先使用 sfr
关键字找到特殊功能寄存器 P0
对应的内存地址,然后设置该特殊功能寄存器的各位的值。
如果想使用 STM32 实现以上过程,方法则复杂得多。下面是使用 STM32 实现以上要求的步骤。
使用寄存器控制引脚输出
如果需要控制 STM32 的引脚向外输出数字信号,从而控制外部设备例如LED灯、蜂鸣器等,也需要通过某一个寄存器对外输出数据。在 STM32 内,端口输出数据寄存器 GPIOx_ODR 充当该角色。这样的寄存器一共有 5 个,因此 x 可以代表 A~E 。每个端口输出数据寄存器都有 32 位,不过用户仅可以操作它的低 16 位。
例如,已知 GPIOB_ODR 寄存器的绝对地址是 0x4001 0C0C
,通过该信息,可以很快写出以下语句,让 GPIOB_ODR
寄存器的 16 位管脚全部输出高电平:
直接操作寄存器的第一个难点在于找到寄存器对应的内存地址。STM32 的片上外设区分为三条总线,根据外设速度的不同,不同总线挂载了不同的外设:APB1 挂载低速外设,APB2 和 AHB 挂载高速外设。相应总线的最低地址称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。其中 APB1 总线的地址最低,片上外设从这里开始,也叫外设基地址。各个总线的地址如下表所示:
总线名称 | 总线基地址 | 相对外设基地址的偏移 |
---|---|---|
APB1 | 0x4000 0000 | 0x0 |
APB2 | 0x4001 0000 | 0x0001 0000 |
AHB | 0x4001 8000 | 0x0001 8000 |
每一条总线上也挂载着各种外设,这些外设也有自己的地址范围。以 GPIO 外设为例,该外设挂载在 APB2 总线上,外设的地址可以使用相对该总线的偏移地址来衡量。STM32 的和 GPIO 有关的外设共有 7 个,依次命名为 GPIOA~GPIOG 。其中 GPIOB 外设相对 APB2 总线的偏移地址为 C00 ,那么它的基地址就为 0x4001 0C00
GPIO有很多个寄存器,每一个都有特定的功能。每个寄存器为 32bit ,占用 4 个字节。在该外设的基地址上按照顺序排列,寄存器的位置都以相对该外设基地址的偏移地址来描述。以 GPIOB_ODR 寄存器为例,它排在第 4 位,那么相对 GPIOB 寄存器的偏移地址为 0x0C 。将以上偏移地址相加,最终得到 GPIOB_ODR 的绝对地址 0x4001 0C0C
。
仅仅将数据写入寄存器 GPIOB_ODR 内,还不能点亮LED灯。这是因为端口输出数据寄存器的每个位都是可以同时被读/写的。STM32 为了保证读/写过程顺利进行并提升效率,所有的 GPIO 外设需要提前设置好使用哪一个模式。这个读/写模式由端口配置低寄存器 GPIOx_CRL 和端口配置高寄存器 GPIOx_CRH 来决定是输入还是输出。
这两种寄存器都以 2 位为一组控制一个 I/O 位的一个功能。I/O 可以改变两种功能:模式(低2位控制)和速度(高2位控制),因此每 4 位决定一个 I/O 口的具体状态。因此,一个这样的寄存器可以控制 8 个 I/O 口的读写状态,一对端口配置高/低寄存器即可管理一个端口输出数据寄存器 16 位的全部状态。
为了点亮LED灯,查阅资料后决定将 I/O 口配置推挽输出模式(对应的 2 位需要设置为 0b00 ),任意输出速度(只要非 0b00 值即可)。那么就要设置 GPIOB 的端口配置低寄存器 GPIOB_CRL 的值。由于控制LED灯只需要 1 位输出,因此如果要让 GPIOB_ODR 的最低位为输出状态,只需要通过位操作来将 GPIOB_CRL 的最低 4 位设置为 0b0100 即可。再已知 GPIOx_CRL 是对应外设基地址上的第一个寄存器,那么可以计算出 GPIOB_CRL 的绝对地址为 0x4001 0c00
。
将这一步整理成代码为:
其实仅靠以上两步,程序依旧不能让GPIO正常输出低电平。仔细观察STM32系统结构,为了降低功耗,GPIO 置于外设上,如下图所示:
内核要操作这些外设,需要经过 AHB 系统总线、桥接1、APB2 线路到达 GPIO 上。而 STM32 上电后,外设中的复位和时钟控制(RCC)相应的寄存器并没有开始运作,AHB系统总线也就无法传输数据。
APB2 外设时钟使能寄存器 RCC_APB2ENR 负责控制 I/O 端口的时钟使能。它的第 2~8 位 IOPxEN 决定 GPIOx 的 I/O 端口时钟使能,当清 0 时,I/O 端口时钟关闭;置 1 时时钟开启。
与复位和时钟控制(RCC)有关的寄存器都在 AHB 系统总线上,且相对偏移地址为 0x0000 9000 。RCC_AHBENR 是其中的第 7 个寄存器,因此相对再偏移 0x18 字节。通过以上的偏移和,可以计算出 RCC_AHBENR 的绝对地址为 0x4002 1018
。
终于,可以编写出如下代码:
库 How
STM32 的标准外设库可以在 https://www.st.com/content/st_com/en/products/embedded-software/mcu-mpu-embedded-software/stm32-embedded-software/stm32-standard-peripheral-libraries.html#products 里下载到,打开该页面之后选择对应芯片版本的固件库(注意第一个内容不是固件库),点击第一列进入下载页面,然后可以看到中央的“Get latest”按钮,点击后要输入邮箱名,稍等几分钟可以在邮箱中看到一封邮件,点击邮件中的下载按钮就可以下载了。
STM32标准库的一般结构
从官网获取 STM32 的标准库后,直接解压到本地,就可以看到 STM32 标准库的结构了。这里以3.6版本的固件库为例,介绍STM32固件库的主要文件结构。
标准库文件夹里主要包含了以下文件或目录:
文件/目录名 | 说明 |
---|---|
Libraries | 驱动库的源代码及启动文件 |
Project | 用驱动库写的例子和一个工程模板 |
Utilities | 基于ST官方开发板的例程 |
Release_Notes.html | 库版本更新说明 |
stm32f10x_stdperiph_lib_um.chm | 库帮助文档 |
在开发中,主要用到 Libraries 目录,将该目录下的标准库文件添加到工程中,并借助文档来使用这些库函数。
下图展示了 Libraries 目录的结构:
最外层主要包含两个文件夹:CMSIS 文件夹存放内核的库文件,而 STM32F10x_StdPeriph_Driver 文件夹存放外设的库文件。
STM32采用的是 Cortex-M3 内核,采用该内核的单片机 ST 等公司则基于该内核规划了不同的外设,这些差异却导致软件在同内核、不同外设的芯片上移植困难。为了解决不同芯片厂商生产的 Cortex 微控制器软件的兼容性问题,ARM 与芯片厂商建立了 CMSIS 标准(Cortex Microcontroller Software Interface Standard)。该标准在用户和单片机之间建立新建了一个软件抽象层,ARM 公司负责定义内核寄存器和外设的名称,而芯片生产商则负责确定外设的具体地址、中断定义等信息。
所谓 CMSIS 标准,实际是在用户和单片机之间建立新建了一个软件抽象层。用户只需要知道单片机里有哪些可用的设施,而不需要管这些设施的具体位置、实现细节。
CMSIS/CM3 里又分为两个文件夹,其中 DeviceSupport 文件夹存放启动文件,它们根据编译器的不同被划分在各自的文件夹中。另一个文件夹 CoreSupport 包含了两个文件 core_cm3.c 和对应的头文件,它们的作用是为采用 Cortex-M3 核设计 SoC 的芯片商设计的芯片外设提供一个进入 CM3 内核的接口。这里可以先不用理会它有什么用,只需要将它加入工程文件即可。
core_cm3.c 文件还有一些与编译器相关的条件编译语句,用于屏蔽不同编译器的差异。例如,它定义了 uint32_t
等一系列类型声明。这些类型等定义可以用来避免因为编译器不同而导致的跨平台错误。core_cm3.c 与启动文件一样都是底层文件,都是由 ARM 公司提供的,遵守 CMSIS 标准,即所有 Cortex-M3 芯片的库都带有这个文件,这样软件在不同芯片的移植工作就得以简化。
在 DeviceSupport 中包含的是启动文件、外设寄存器定义和中断向量定义层的一些文件,它们是由 ST 公司提供的。层次进入之后可以看到其中包含的 system_stm32f10x.c 及其头文件,功能是设置系统时钟和总线时钟。所有的外设都与时钟的频率有关,所以这个文件的时钟配置是很关键的。除此之外,该文件夹中还包含一个头文件 stm32f10x.h ,它是一个非常重要的头文件,其重要性几乎等价于 51 单片机的 reg51.h 。它包含了 STM32 中所有的地址定义并包含了几个重要的配置文件,没有这些地址就无法正确控制 STM32 ,因此在使用到 STM32 固件库的地方都要包含这个头文件。
STM32F10x_StdPeriph_Driver 文件夹下有 inc 和 src 两个文件夹,属于CMSIS的设备外设函数部分,这些外设是芯片制造商在 Cortex-M3 核外加进去的。要使用到的 GPIO 的相关函数与功能就存放在这个文件夹里。
这两个文件夹中还有很特别的 misc.c 及其头文件,这个文件提供了外设对内核中的 NVIC(中断向量控制器)的访问函数,在配置中断时,必须把这个文件添加到工程中。
标准库的介绍就到这里了。最后,在总目录的 Project/STM32F10x_StdPeriph_Template 文件夹下,存放了官方的一个库工程模板。在用库建立一个完整的工程时,还需要添加这个目录下的 stm32f10x_it.c 、stm32f10x_it.h 和 stm32f10x_conf.h 这三个文件:
- stm32f10x_it.c :专门用来编写中断服务函数,该文件已经定义了一些系统异常的接口,其他普通中断服务函数需要自己添加。这些中断服务函数的接口调用可以在汇编启动文件中找到,之后会介绍中断的编写
- stm32f10x_conf.h :文件被包含进 stm32f10x.h 文件,是用来配置外设资源的头文件,使用该头文件可以很方便地增加或删除 Driver 目录下的外设驱动函数库
标准库对GPIO的封装思路
在之前的示例中,通过寄存器来控制GPIO。在不包括任何头文件的情况下,只能通过指针访问寄存器的地址,这样做需要查阅相关的文档,不仅效率低下,而且容易出错,在查看代码时的可读性也很差。
接下来以通过 GPIO 点亮LED灯(输出)和从 GPIO 读取键盘状态(输入)为例,介绍通过标准库操作 GPIO 的方法,并体会到标准库是如何封装 GPIO 的相关功能的。
之前通过寄存器来操作 GPIO 口点亮LED灯,这个过程需要明白以下信息:
- 需要操作哪一个 GPIO 口的哪些位
- 需要对这个 GPIO 口的这些位实施哪些操作
首先来看标准库对 GPIO 有关的寄存器的封装。这一部分内容在头文件 stm32f10x.h 里。
首先,宏 PERIPH_BASE
提供了所有外设的基地址:
所有的外设都挂载在三条总线上:APB1 总线、APB2 总线和 AHB 系统总线,它们的基地址定义由以下宏给出:
这些地址都是由外设基地址+偏移地址的形式构成。
GPIO 端口的基地址挂载在 APB2 总线上,它们的宏定义同样由基地址和偏移地址构成:
通过宏 GPIOB_BASE
,便可以得到 B 组 GPIO 相关寄存器的基地址。而与 GPIOB 相关的寄存器地址,都可以通过该基地址获取。
例如,在 STM32 官方手册中查阅 GPIOB_ODR 寄存器的地址,可以找到它的地址偏移量:
该偏移量就是基于 GPIOB_BASE
基地址的偏移值。
有了以上 GPIO 的基地址和各个寄存器的地址偏移值,很容易就可以通过宏定义出各个寄存器的地址。不过,STM32的固件库以结构的方式定义了这些寄存器:
这样就可以通过一个结构管理一组 GPIO 有关的寄存器。前文说过,每个寄存器为 32bit ,占用 4 个字节,且它们的地址在该外设的基地址上按照顺序排列。如果采用结构体的方式定义,将每个结构成员设置为 32bit 大小,再将结构体的地址指向对应的外设的基地址上,由于结构体成员的内存空间是紧密排列的,这样结构成员的地址偏移,恰好与 GPIO 外设定义的寄存器地址偏移一一对应!这里,__IO
宏是 volatile
修饰符的别名,它的目的是防止编译器优化地址的排布,而打乱寄存器正确的地址信息。
因此,只要给结构体设置好首地址,就能确定结构体内成员的地址确定,然后就能以结构体的形式访问寄存器:
标准库已经更进一步,直接使用宏定义好的 GPIO_Type
类型指针和各个 GPIO 端口的首地址,可以以结构指针的形式通过该宏访问寄存器:
只要通过这些宏,就可以直接操作外设的寄存器了,无需考虑寄存器的地址如何。
虽然 stm32f10x.h
已经将各个寄存器封装到一种非常简明的程度了,但是既然使用库操作 STM32 ,通过直接操作寄存器的方式来操作 GPIO 仍然十分繁琐。
这个时候,就要借助标准库对外设的封装了。stm32f10x_gpio.h
头文件对GPIO的操作进行了抽象化的封装。接下来看看这一部分。
首先,在该头文件内,有对 GPIO 进行初始化的结构体封装:
第一个字段 GPIO_Pin
用于控制需要配置的引脚。在同一个头文件中,可以找到以下的宏定义,可以提供给该字段用于选定需要配置的管脚:
从以上宏定义的值(都是 2 的整数次方)可以看出,一个 GPIO_initTypeDef
结构可以用于配置 GPIOx 的 16 个输出位引脚中的一个、多个或全部;如果要配置多个,可以使用或运算符 |
将它们相接。
GPIOSpeed_TypeDef
和 GPIOMode_TypeDef
是两个枚举类型,它们的具体定义如下:
这两个字段的含义,就是前文说到的每一个 I/O 口可以改变的两种功能:模式和速度。
于是,可以通过初始化 GPIOSpeed_TypeDef
结构与它的三个字段,使用合适的标识符名便可以对 GPIO 端口进行不同的配置。这些可配置的数值,已经由标准库封装成见名知义的枚举常量,这使编写的代码变得非常简洁与清晰:
这样,以后看到这段代码,就可以立即从表明推断出,它的含义是:将 I/O 口的第 3 、4 、5 号位设置为“通用推挽输出模式”,且输出速度为 50MHz 。
使用标准库操作GPIO
之前已经定义了一个 GPIO_InitTypeDef
结构的变量,但很显然它不能直接用于底层的配置,因为还缺少以下内容:
- 只给了一个结构体,那么要如何根据这个结构体设置相应的寄存器?
- 要设置 GPIOA~GPIOG 的哪一个外设?
对于第一个问题,stm32f10x_gpio.h
提供了以下函数:
该函数接受两个参数,类型 GPIO_TypeDef
之前介绍过,stm32f10x.h 内的宏 GPIOA
~ GPIOG
正是该类型的指针。
该函数的作用是通过初始化结构的信息,来初始化 GPIOx 外设的寄存器。因此,完整的代码:
表达的含义是:初始化 GPIOB 外设,让它的 Pin3 、Pin4 、Pin5 设置为最高频率为 50MHz 的“通用推挽输出模式”。
借助抽象的结构,通过封装的标准库函数,直接将给定的信息映射到底层寄存器中。这就是使用标准库的优点,只需要知道 STM32 提供了哪些资源,然后就可以使用标准库操作这些资源了,根本不需要知道要配置哪些寄存器,设置它们的哪些位为什么值。
最后,可以对配置完成的GPIO引脚置1或清0。它们分别要用到以下函数:
调用以上函数便可对 GPIOx 中的某几个位置1或清零。例如:
可以将 GPIOB 的第 3 、4 、5 位设置为 1 。
以上借助库函数,很方便地便配置完成了 GPIO 相应的寄存器并使其对外输出。不过在这之前还有一步,就是打开外设时钟。
其实打开外设时钟之前,还需要配置好系统时钟,设置一系列的时钟来源、倍频、分频等控制参数。不过这些工作都由标准库处理完成了。这里可以先不管标准库如何处理的,只需要打开外设时钟即可。
与复位和时钟控制(RCC)有关的函数和宏都定义在 stm32f10x_rcc.h 内,这里使用以下函数打开 APB2 外设的时钟:
该函数接收两个参数。第一个参数指定外设名,可以在同一个头文件内找到若干以 RCC_APB2Periph_ 开头的宏定义,它们用于表示哪一个挂载在 APB2 总线上的外设时钟。第二个参数是枚举类型,其定义为:
这样,只需要通过如下调用函数的方式:
就可以将 APB2 总线上的 GPIOB 外设时钟置为打开状态。