共计 17984 个字符,预计需要花费 45 分钟才能阅读完成。
摘要:从 STM32 新建工程、编译下载程序登程,让老手由浅入深,尽享 STM32 规范库开发的乐趣。
自从 CubeMX 等图像配置软件的呈现,同学们往往点几下鼠标就解决了单片机的配置问题。对于谋求开发速度的业务场景下,应用疾速配置软件是正当的,高效的,但对于学生的学习场景下,更为重要的是知其然并知其所以然。
以下是学习(包含但不限于)嵌入式的三个重要内容,
1、学会如何参考官网的手册和官网的代码来独立写本人的程序。
2、积攒罕用代码段,晓得哪里的问题须要哪些代码解决。
3、追随大佬步调,一步一个脚印。
首先:咱们都晓得编程时个别查的是《参考手册》,而进行芯片选型或须要芯片数据时,查阅的是《数据手册》。此外市面上所有对于 STM32 的书籍都是立足于前二者(+Cortex 内核手册)进行编著。
其次:要分清什么是内核外设与内核之外的外设,为了便于辨别,依照网上的一种说法,将“内核之外的外设”以“处理器外设”代替。
再者:现在很少应用规范库了,都是 HAL 库,但作为高校目前教学方式,
咱们将以 STM32f10xxx 为例对规范库开发进行概览。
一、STM32 系统结构
STM32f10xxx 系统结构
内核 IP
从构造框图上看,Cortex-M3 外部有若 干个总线接口,以使 CM3 能同时取址和访内(拜访内存),它们是:指令存储区总线(两条)、系统总线、公有外设总线。有两条代码存储区总线负责对代 码存储区(即 FLASH 外设)的拜访,别离是 I-Code 总线和 D-Code 总线。
I-Code 用于取指,D-Code 用于查表等操作,它们按最佳执行速度进行优化。
系统总线(System)用于拜访内存和外设,笼罩的区域包含 SRAM,片上外设,片外 RAM,片外扩大设施,以及零碎级存储区的局部空间。
公有外设总线负责一部分公有外设的拜访,次要就是拜访调试组件。它们也在零碎级 存储区。
还有一个 MDA 总线,从字面上看,DMA 是 data memory access 的意思,是一种连贯内核和外设的桥梁,它能够拜访外设、内存,传输不受 CPU 的管制,并且是双向通信。简而言之,这个家伙就是一个速度很快的且不受老大管制的数据搬运工。
处理器外设(内核之外的外设)
从构造框图上看,STM32 的外设有 串口、定时器、IO 口、FSMC、SDIO、SPI、I2C 等,这些外设按 照速度的不同,别离挂载到 AHB、APB2、APB1 这三条总线上。
二、寄存器
什么是寄存器?寄存器是内置于各个 IP 外设中,是一种用于配置外设性能的存储器,并且有想对应的地址。所有库的封装始于映射。
是不是“又臭又长”,如果进行寄存器开发,就须要怼地址以及对寄存器进行字节赋值,不仅效率低而且容易出错。
来,开个玩笑。
你兴许据说过“国内 C 语言乱码大赛(IOCCC)”上面这个例子就是网上广为流传的 一个经典作品:
#include <stdio.h>
main(t,_,a)char *a;{return!0<t?t<3?main(-79,-13,a+main(-87,1-_,
main(-86,0,a+1)+a)):1,t<_?main(t+1,_,a):3,main(-94,-27+t,a)&&t==2?_<13?
main(2,_+1,"%s %d %dn"):9:16:t<0?t<-72?main(_,t,
"@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n{n+,/+#n+,/#
;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l
q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;#
){nl]!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw'iK{;[{nl]'/w#q#n'wk nw'
iwk{KK{nl]!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c
;;{nl'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')#
}'+}##(!!/")
:t<-50?_==*a?putchar(31[a]):main(-65,_,a+1):main((*a=='/')+t,_,a+1)
:0<t?main(2,2,"%s"):*a=='/'||main(0,main(-61,*a,
"!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:nuwloca-O;m.vpbks,fxntdCeghiry"),a+1);}
复制代码
库的存在就是为了解决这类问题,将代码语义化。语义化思维不仅仅是嵌入式有的,前端代码也在谋求语义个性。
三、万物始于点灯
(1)内核库文件剖析
· cor_cm3.h
这个头文件实现了:1、内核构造体寄存器定义 2、内核寄存器内存映射 3、内存存放 器位定义。跟处理器相干的头文件 stm32f10x.h 实现的性能一样,一个是针对内核的寄存器,一个是针对内核之外,即处理器的寄存器。
· misc.h
内核利用函数库头文件,对应 stm32f10x_xxx.h
· misc.c
内核利用函数库文件,对应 stm32f10x_xxx.c。在 CM3 这个内核外面还有一些性能组 件,如 NVIC、SCB、ITM、MPU、CoreDebug,CM3 带有十分丰盛的性能组件,然而芯片 厂商在设计 MCU 的时候有一些并不是非要不可的,是可裁剪的,比方 MPU、ITM 等在 STM32 外面就没有。其中 NVIC 在每一个 CM3 内核的单片机中都会有,但都会被裁剪,只能是 CM3 NVIC 的一个子集。在 NVIC 外面还有一个 SysTick,是一个零碎定时器,能够提 供时基,个别为操作系统定时器所用。misc.h 和 mics.c 这两个文件提供了操作这些组件的函数,并能够在 CM3 内核单片机 间接移植。
(2)处理器外设库文件剖析
· startup_stm32f10x_hd.s
这个是由汇编编写的启动文件,是 STM32 上电启动的第一个程序,启动文件次要实现 了:1、初始化堆栈指针 SP;2、设置 PC 指针 =Reset_Handler;3、设置向量表的地址,并 初始化向量表,向量表外面放的是 STM32 所有中断函数的入口地址 4、调用库函数 SystemInit,把零碎时钟配置成 72M,SystemInit 在库文件 stytem_stm32f10x.c 中定义;5、跳转到标号_main,最终去到 C 的世界。
· system_stm32f10x.c
这个文件的作用是外面实现了各种罕用的零碎时钟设置函数,有 72M,56M,48,36,24,8M,咱们应用的是是把零碎时钟设置成 72M。
· Stm32f10x.h
这个头文件十分重要,这个头文件实现了:1、处理器外设寄存器 的构造体定义 2、处理器外设的内存映射 3、处理器外设寄存器的位定义。
对于 1 和 2 咱们在用寄存器点亮 LED 的时候有解说。
其中 3:处理器外设寄存器的位定义,这个十分重要,具体是什么意思?咱们晓得一个寄存器有很多个位,每个位写 1 或 者写 0 的性能都是不一样的,处理器外设寄存器的位定义就是把外设的每个寄存器的每一 个位写 1 的 16 进制数定义成一个宏,宏名即用该位的名称示意,如果咱们操作寄存器要开启某一个性能的话,就不必本人亲自去算这个值是多少,能够间接到这个头文件外面找。
咱们以片上外设 ADC 为例,假如咱们要启动 ADC 开始转换,依据手册咱们晓得是要管制 ADC_CR2 寄存器的位 0:ADON,即往位 0 写 1,即:
ADC->CR2=0x00000001;
复制代码
这是 个别的操作方法。当初这个头文件外面有对于 ADON 位的位定义:
#define ADC_CR2_ADON ((uint32_t)0x00000001)
复制代码
有了这个位定义,咱们刚刚的 代码就变成了:
ADC->CR2=ADC_CR2_ADON
复制代码
· stm32f10x_xxx.h
外设 xxx 利用函数库头文件,这外面次要定义了实现外设某一性能 的构造体,比方通用定时器有很多性能,有定时性能,有输入比拟性能,有输出捕获功 能,而通用定时器有十分多的寄存器要实现某一个性能,比方定时性能,咱们基本不晓得 具体要操作哪些寄存器,这个头文件就为咱们打包好了要实现某一个性能的寄存器,是以机构体的模式定义的,比方通用定时器要实现一个定时的性能,咱们只须要初始化 TIM_TimeBaseInitTypeDef 这个构造体外面的成员即可,外面的成员就是定时所须要 操作的寄存器。有了这个头文件,咱们就晓得要实现某个性能须要操作哪些寄存器,而后 再回手册中精度这些寄存器的阐明即可。
· stm32f10x_xxx.c
stm32f10x_xxx.c:外设 xxx 利用函数库,这外面写好了操作 xxx 外设的所有罕用的函 数,咱们应用库编程的时候,应用的最多的就是这里的函数。
(3)SystemInit
工程中新建 main.c。
在此文件中编写 main 函数后间接编译会报错:
Undefined symbol SystemInit (referred from startup_stm32f10x_hd.o).
复制代码
谬误提醒说 SystemInit 没有定义。从剖析启动文件 startup_stm32f10x_hd.s 时咱们晓得,
1 ;Reset handler
2 Reset_Handler PROC
3 EXPORT Reset_Handler [WEAK]
4 IMPORT __main
5 ;IMPORT SystemInit
6 ;LDR R0, =SystemInit
7 BLX R0
8 LDR R0, =__main
9 BX R0
10 ENDP
复制代码
汇编中;分号是正文的意思
第五行第六行代码 Reset_Handler 调用了 SystemInit 该函数用来初始化零碎时钟,而该函数是在库文件 system_stm32f10x.c 中实现的。咱们从新写一个这样的函数也能够,把性能残缺实现一遍,然而为了简略起见,咱们在 main 文件外面定义一个 SystemInit 空函数,为的是骗过编译器,把这个谬误去掉。对于配置零碎时钟之后会出文章 RCC 时钟树具体介绍,次要配置时钟管制寄存器 (RCC_CR) 和时钟配置寄存器 (RCC_CFGR) 这两个寄存器,但最好是间接应用 CubeMX 间接生成,因为它的配置过程有些简短。
如果咱们用的是库,那么有个库函数 SystemInit,会帮咱们把零碎时钟设置成 72M。
当初咱们没有应用库,那当初时钟是多少?答案是 8M,当内部 HSE 没有开启或者呈现故
障的时候,零碎时钟由外部低速时钟 LSI 提供,当初咱们是没有开启 HSE,所以零碎默认的时钟是 LSI=8M。
(4)库封装层级
如图,达到第四层级便是咱们所熟知的固件库或 HAL 库的成果。当然库的编写还须要思考许多问题,不止于这些内容。咱们须要的是理解库封装的大略过程。
将库封装等级分为四级来介绍是为了有层次感,就像打怪降级一样,进行认知了解的降级。
咱们都晓得,操作 GPIO 输入分三大步:
时钟管制:
STM32 外设很多,为了降低功耗,每个外设都对应着一个时钟,在零碎复位的时候这
些时钟都是被敞开的,如果想要外设工作,必须把相应的时钟关上。
STM32 的所有外设的时钟由一个专门的外设来治理,叫 RCC(reset and clockcontrol),RCC 在 STM32 参考手册的第六章。
STM32 的外设因为速率的不同,别离挂载到三条总系上:AHB、APB2、APB1,AHB 为高速总线,APB2 次之,APB1 再次之。所以的 IO 口都挂载到 APB2 总线上,属于高速
外设。
模式配置:
这个由端口配置寄存器来管制。端口配置寄存器分为高下两个,每 4bit 管制一
个 IO 口,所以端口配置低寄存器:CRL 管制这 IO 口的低 8 位,端口配置高寄存器:CRH
管制这 IO 口的高 8bit。在 4 位一组的管制位中,CNFy[1:0] 用来管制端口的输入输出,
MODEy[1:0]用来管制输入模式的速率,又称驱动电路的响应速度,留神此处速率与程序无关,具体内容见文章:【嵌入式】GPIO 引脚速度、翻转速度、输入速度区别
输出有 4 种模式,输入有 4 种模式,咱们在管制 LED 的时候抉择通用推挽输入。
输入速率有三种模式:2M、10M、50M,这里咱们抉择 2M。
电平管制:
STM32 的 IO 口比较复杂,如果要输入 1 和 0,则要通过管制:端口输入数据存放
器 ODR 来实现,ODR 是:Output data register 的简写,在 STM32 外面,其寄存器的命名
名称都是英文的简写,很容易记住。从手册上咱们晓得 ODR 是一个 32 位的寄存器,低 16
位无效,高 16 位保留。低 16 位对应着 IO0~IO16,只有往相应的地位写入 0 或者 1 就能够
输入低或者高电平。
第一层级:基地址宏定义
时钟管制:
在 STM32 中,每个外设都有一个起始地址,叫做外设基地址,外设的寄存器就以这个基地址为规范依照顺序排列,且每个寄存器 32 位,(前面作为构造体外面的成员正好内存对齐)。查表看到时钟由 APB2 外设时钟使能寄存器 (RCC_APB2ENR) 来管制,其中 PB 端口的时钟由该寄存器的位 3 写 1 使能。咱们能够通过基地址 + 偏移量 0x18,算出 RCC_APB2ENR 的地址为:0x40021018。那么使能 PB 口的时钟代码则如下所示:
#define RCC_APB2ENR *(volatile unsigned long *)0x40021018
// 开启端口 B 时钟
RCC_APB2ENR |= 1<<3;
复制代码
模式配置:
同 RCC_APB2ENR 一样,GPIOB 的起始地址是:0X4001 0C00,咱们也能够算出 GPIO_CRL 的地址为:0x40010C00。那么设置 PB0 为通用推挽输入,输入速率为 2M 的代码则如下所示:
同上,从手册中咱们看到 ODR 寄存器的地址偏移是:0CH,能够算出 GPIOB_ODR 寄存器的地址是:0X4001 0C00 + 0X0C = 0X4001 0C0C。当初咱们就能够定义 GPIOB_ODR 这个寄存器了,代码如下:
#define GPIOB_ODR *(volatile unsigned long *)0x40010C0C
//PB0 输入低电平
GPIOB_ODR = 0<<0;
复制代码
第一层级:基地址宏定义实现用 STM32 管制一个 LED 的残缺代码:
1 #define RCC_APB2ENR *(volatile unsigned long *)0x40021018
2 #define GPIOB_CRL *(volatile unsigned long *)0x40010C00
3 #define GPIOB_ODR *(volatile unsigned long *)0x40010C0C
45
int main(void)
6 {
7 // 开启端口 B 的时钟
8 RCC_APB2ENR |= 1<<3;
9
10 // 配置 PB0 为通用推挽输入模式,速率为 2M
11 GPIOB_CRL = (2<<0) | (0<<2);
12
13 // PB0 输入低电平,点亮 LED
14 GPIOB_ODR = 0<<0;
15 }
16
17 void SystemInit(void)
18 {19}
复制代码
第二层级:基地址宏定义 + 构造体封装
外设寄存器构造体封装
下面咱们在操作寄存器的时候,操作的是寄存器的相对地址,如果每个寄存器都这样操作,那将十分麻烦。咱们思考到外设寄存器的地址都是基于外设基地址的偏移地址,都是在外设基地址上一一间断递增的,每个寄存器占 32 个或者 16 个字节,这种形式跟构造体外面的成员相似。所以咱们能够定义一种外设构造体,构造体的地址等于外设的基地址,构造体的成员等于寄存器,成员的排列程序跟寄存器的程序一样。这样咱们操作寄存器的时候就不必每次都找到相对地址,只有晓得外设的基地址就能够操作外设的全副寄存器,即操作构造体的成员即可。
上面咱们先定义一个 GPIO 寄存器构造体,构造体外面的成员是 GPIO 的寄存器,成员的程序依照寄存器的偏移地址从低到高排列,成员类型跟寄存器类型一样。(struct 用法参考【C 语言】(2): 关键字的具体介绍)
1 typedef struct {
2 volatile uint32_t CRL;
3 volatile uint32_t CRH;
4 volatile uint32_t IDR;
5 volatile uint32_t ODR;
6 volatile uint32_t BSRR;
7 volatile uint32_t BRR;
8 volatile uint32_t LCKR;
9 } GPIO_TypeDef;
复制代码
在《STM32 中文参考手册》8.2 寄存器形容章节,咱们能够找到构造体外面的 7 个寄存器形容。在点亮 LED 的时候咱们只用了 CRL 和 ODR 这两个寄存器,至于其余寄存器的
性能大家能够自行看手册理解。
在 GPIO 构造体外面咱们用了两个数据类型,一个是 uint32_t,示意无符号的 32 位整
型,因为 GPIO 的寄存器都是 32 位的。这个类型申明在规范头文件 stdint.h 外面应用 typedef
对 unsigned int 重命名,咱们在程序上只有蕴含这个头文件即可。
另外一个是 volatile(volatile 用法参考【C 语言】(2): 关键字的具体介绍),作用就是通知
编译器这里的变量会变动不因优化而省略此指令,必须每次都间接读写其值,这样就能确保每
次读或者写寄存器都真正执行到位。
外设封装
STM32F1 系列的 GPIO 端口分 A~G,即
GPIOA、GPIOB。。。。。。GPIOG。每个端口都含有 GPIO_TypeDef 构造体外面的存放
器,咱们能够依据手册各个端口的基地址把 GPIO 的各个端口定义成一个 GPIO_TypeDef 类型
指针,而后咱们就能够依据端口名(实际上当初是构造体指针了)来操作各个端口的寄存器,
代码实现
如下:
1 #define GPIOA ((GPIO_TypeDef *) 0X4001 0800)
2 #define GPIOB ((GPIO_TypeDef *) 0X4001 0C00)
3 #define GPIOC ((GPIO_TypeDef *) 0X4001 1000)
4 #define GPIOD ((GPIO_TypeDef *) 0X4001 1400)
5 #define GPIOE ((GPIO_TypeDef *) 0X4001 1800)
6 #define GPIOF ((GPIO_TypeDef *) 0X4001 1C00)
7 #define GPIOG ((GPIO_TypeDef *) 0X4001 2000)
复制代码
外设内存映射
讲到基地址的时候咱们再引人一个知识点:Cortex-M3 存储器零碎,这个知识点在
《Cortex-M3 权威指南》第 5 章外面讲到。CM3 的地址空间是 4GB,如下图所示:
咱们这里要讲的是片上外设,就是咱们所说的寄存器的根据地,其大小总共有 512MB,512MB 是其极限空间,并不是每个单片机都用得完,实际上各个 MCU 厂商都只
是用了一部分而已。STM32F1 系列用到了:0x4000 0000 ~0x5003 FFFF。
当初咱们说的 STM32 的寄存器就是位于这个区域
- APB1、APB2、AHB 总线基地址
当初咱们说的 STM32 的寄存器就是位于这个区域,这外面 ST 设计了三条总线:
AHB、APB2 和 APB1,其中 AHB 和 APB2 是高速总线,APB1 是低速总线。不同的外设
依据速度不同别离挂载到这三条总线上。从下往上顺次是:APB1、APB2、AHB,每个总
线对应的地址别离是:APB1:0x40000000,APB2:0x4001 0000,AHB:0x4001 8000。
这三条总线的基地址咱们是从《STM32 中文参考手册》2.3 大节—存储器映像失去
的:APB1 的基地址是 TIM2 定时器的起始地址,APB2 的基地址是 AFIO 的起始地址,
AHB 的基地址是 SDIO 的起始地址。
其中 APB1 地址又叫做外设基地址,是所有外设的基地址,叫做 PERIPH_BASE。
当初咱们把这三条总线地址用宏定义进去,当前咱们在定义其余外设基地址的时候,
只须要在这三条总线的基址上加上偏移地址即可,代码如下:
1 #define PERIPH_BASE ((uint32_t)0x40000000)
2 #define APB1PERIPH_BASE PERIPH_BASE
3 #define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
4 #define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)
复制代码
- GPIO 端口基地址
因为 GPIO 挂载到 APB2 总线上,那么当初咱们就能够依据 APB2 的基址算出各个
GPIO 端口的基地址,用宏定义实现代码如下:
1 #define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
2 #define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
3 #define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)
4 #define GPIOD_BASE (APB2PERIPH_BASE + 0x1400)
5 #define GPIOE_BASE (APB2PERIPH_BASE + 0x1800)
6 #define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00)
7 #define GPIOG_BASE (APB2PERIPH_BASE + 0x2000)
复制代码
第二层级:基地址宏定义 + 构造体封装实现用 STM32 管制一个 LED 的残缺代码:
1 #include <stdint.h>
2 #define __IO volatile
3
4typedef struct {
5 __IO uint32_t CRL;
6 __IO uint32_t CRH;
7 __IO uint32_t IDR;
8 __IO uint32_t ODR;
9 __IO uint32_t BSRR;
10 __IO uint32_t BRR;
11 __IO uint32_t LCKR;
12 } GPIO_TypeDef;
13
14typedef struct {
15 __IO uint32_t CR;
16 __IO uint32_t CFGR;
17 __IO uint32_t CIR;
18 __IO uint32_t APB2RSTR;
19 __IO uint32_t APB1RSTR;
20 __IO uint32_t AHBENR;
21 __IO uint32_t APB2ENR;
22 __IO uint32_t APB1ENR;
23 __IO uint32_t BDCR;
24 __IO uint32_t CSR;
25 } RCC_TypeDef;
26
27 #define PERIPH_BASE ((uint32_t)0x40000000)
28
29 #define APB1PERIPH_BASE PERIPH_BASE
30 #define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
31 #define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)
32
33 #define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
34 #define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
35 #define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)
36 #define GPIOD_BASE (APB2PERIPH_BASE + 0x1400)
37 #define GPIOE_BASE (APB2PERIPH_BASE + 0x1800)
38 #define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00)
39 #define GPIOG_BASE (APB2PERIPH_BASE + 0x2000)
40 #define RCC_BASE (AHBPERIPH_BASE + 0x1000)
41
42 #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
43 #define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
44 #define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
45 #define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
46 #define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
47 #define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
48 #define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
49 #define RCC ((RCC_TypeDef *) RCC_BASE)
50
51
52 #define RCC_APB2ENR *(volatile unsigned long *)0x40021018
53 #define GPIOB_CRL *(volatile unsigned long *)0x40010C00
54 #define GPIOB_ODR *(volatile unsigned long *)0x40010C0C
55
56 int main(void)
57 {
58 // 开启端口 B 的时钟
59 RCC->APB2ENR |= 1<<3;
60
61 // 配置 PB0 为通用推挽输入模式,速率为 2M
62 GPIOB->CRL = (2<<0) | (0<<2);
63
64 // PB0 输入低电平,点亮 LED
65 GPIOB->ODR = 0<<0;
66
67 }
68
69 void SystemInit(void)
70 {71}
复制代码
第二层级变动:
①、定义一个外设(GPIO)寄存器构造体,构造体的成员蕴含该外设的所有寄存器,
成员的排列程序跟寄存器偏移地址一样,成员的数据类型跟寄存器的一样。
②外设内存映射,即把地址跟外设建设起一一对应的关系。
③外设申明,即把外设的名字定义成一个外设寄存器构造体类型的指针。
④通过构造体操作寄存器,实现点亮 LED。
第三层级:基地址宏定义 + 构造体封装 +“ 位封装”(每一位的对应字节封装)
下面咱们在管制 GPIO 输入内容的时候管制的是 ODR(Output data register)寄存器,ODR 是一个 16 位的寄存器,必须以字的模式管制
其实咱们还能够管制 BSRR 和 BRR 这两个寄存器来管制 IO 的电平,上面咱们简略介
绍下 BRR 寄存器的性能,BSRR 自行看手册钻研。
位革除寄存器 BRR 只能实现位清 0 操作,是一个 32 位寄存器,低 16 位无效,写 0 没
影响,写 1 清 0。
当初咱们要使 PB0 输入低电平,点亮 LED,则只有往 BRR 的 BR0 位写 1 即可,其余
位为 0,代码如下:
1 GPIOB->BRR = 0X0001;
复制代码
这时 PB0 就输入了低电平,LED 就被点亮了。
如果要 PB2 输入低电平,则是:
1 GPIOB->BRR = 0X0004;
复制代码
如果要 PB3/4/5/6。。。。。。这些 IO 输入低电平呢?情理是一样的,只有往 BRR 的
相应地位赋不同的值即可。因为 BRR 是一个 16 位的寄存器,位数比拟多,赋值的时候容
易出错,而且从赋值的 16 进制数字咱们很难分明的晓得管制的是哪个 IO。这时,咱们是
否能够把 BRR 的每个地位 1 都用宏定义来实现,如 GPIO_Pin_0 就示意 0X0001,
GPIO_Pin_2 就示意 0X0004。只有咱们定义一次,当前都能够应用,而且还见名知意。“位封装”(每一位的对应字节封装)代码如下:
1 #define GPIO_Pin_0 ((uint16_t)0x0001) /*!< Pin 0 selected */
2 #define GPIO_Pin_1 ((uint16_t)0x0002) /*!< Pin 1 selected */
3 #define GPIO_Pin_2 ((uint16_t)0x0004) /*!< Pin 2 selected */
4 #define GPIO_Pin_3 ((uint16_t)0x0008) /*!< Pin 3 selected */
5 #define GPIO_Pin_4 ((uint16_t)0x0010) /*!< Pin 4 selected */
6 #define GPIO_Pin_5 ((uint16_t)0x0020) /*!< Pin 5 selected */
7 #define GPIO_Pin_6 ((uint16_t)0x0040) /*!< Pin 6 selected */
8 #define GPIO_Pin_7 ((uint16_t)0x0080) /*!< Pin 7 selected */
9 #define GPIO_Pin_8 ((uint16_t)0x0100) /*!< Pin 8 selected */
10 #define GPIO_Pin_9 ((uint16_t)0x0200) /*!< Pin 9 selected */
11 #define GPIO_Pin_10 ((uint16_t)0x0400) /*!< Pin 10 selected */
12 #define GPIO_Pin_11 ((uint16_t)0x0800) /*!< Pin 11 selected */
13 #define GPIO_Pin_12 ((uint16_t)0x1000) /*!< Pin 12 selected */
14 #define GPIO_Pin_13 ((uint16_t)0x2000) /*!< Pin 13 selected */
15 #define GPIO_Pin_14 ((uint16_t)0x4000) /*!< Pin 14 selected */
16 #define GPIO_Pin_15 ((uint16_t)0x8000) /*!< Pin 15 selected */
17 #define GPIO_Pin_All ((uint16_t)0xFFFF) /*!< All pins selected */
复制代码
这时 PB0 就输入了低电平的代码就变成了:
1 GPIOB->BRR = GPIO_Pin_0;
复制代码
(如果同时让 PB0/PB15 输入低电平,用或运算,代码:
1 GPIOB->BRR = GPIO_Pin_0|GPIO_Pin_15;
复制代码
为了不使 main 函数看起来冗余,上述库封装 的代码不应该放在 main 外面,因
为其是跟 GPIO 相干的,咱们能够把这些宏放在一个独自的头文件外面。
在工程目录下新建 stm32f10x_gpio.h,把封装代码放外面,而后把这个
文件增加到工程外面。这时咱们只须要在 main.c 外面蕴含这个头文件即可。
第四层级:基地址宏定义 + 构造体封装 +“位封装”+ 函数封装
咱们点亮 LED 的时候,管制的是 PB0 这个 IO,如果 LED 接到的是其余 IO,咱们就
须要把 GPIOB 批改成其余的端口,其实这样批改起来也很快很不便。然而为了进步程序的
可读性和可移植性,咱们是否能够编写一个专门的函数用来复位 GPIO 的某个位,这个函
数有两个形参,一个是 GPIOX(X=A…G),另外一个是 GPIO_Pin(0…15),函数的主体
则是依据形参 GPIOX 和 GPIO_Pin 来管制 BRR 寄存器,代码如下:
1 void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
2 {
3 GPIOx->BRR = GPIO_Pin;
4 }
复制代码
这时,PB0 输入低电平,点亮 LED 的代码就变成了:
1 GPIO_ResetBits(GPIOB,GPIO_Pin_0);
复制代码
同理,咱们能够管制 BSRR 这个寄存器来实现敞开 LED,代码如下:
1 // GPIO 端口置位函数
2 void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
3 {
4 GPIOx->BSRR = GPIO_Pin;
5 }
复制代码
这时,PB0 输入高电平,敞开 LED 的代码就变成了:
1 GPIO_SetBits(GPIOB,GPIO_Pin_0);
复制代码
同样,因为这个函数是管制 GPIO 的函数,咱们能够新建一个专门的文件来放跟 gpio
无关的函数。
在工程目录下新建 stm32f10x_gpio.c,把 GPIO 相干的函数放外面。
这时咱们是否发现刚刚新建了一个头文件 stm32f10x_gpio.h,这两个文件寄存的都是
跟外设 GPIO 相干的。C 文件外面的函数会用到 h 头文件外面的定义,这两个文件是相辅
相成的,故咱们在 stm32f10x_gpio.c 文件中也蕴含 stm32f10x_gpio.h 这个头文件。别忘了把
stm32f10x.h 这个头文件也蕴含进去,因为无关寄存器的所有定义都在这个头文件外面。
如果咱们写其余外设的函数,咱们也应该跟 GPIO 一样,新建两个文件专门来存函
数,比方 RCC 这个外设咱们能够新建 stm32f10x_rcc.c 和 stm32f10x_rcc.h。其余外依葫芦
画瓢即可。
(5)实例编写
以上,是对库封住过程的概述,上面咱们正在地应用库函数编写 LED 程序
①治理库的头文件
当咱们开始调用库函数写代码的时候,有些
库咱们不须要,在编译的时候能够不编译,能够通过一个总的头文件 stm32f10x_conf.h 来
管制,该头文件次要代码如下:
1 //#include "stm32f10x_adc.h"
2 //#include "stm32f10x_bkp.h"
3 //#include "stm32f10x_can.h"
4 //#include "stm32f10x_cec.h"
5 //#include "stm32f10x_crc.h"
6 //#include "stm32f10x_dac.h"
7 //#include "stm32f10x_dbgmcu.h"
8 //#include "stm32f10x_dma.h"
9 //#include "stm32f10x_exti.h"
10 //#include "stm32f10x_flash.h"
11 //#include "stm32f10x_fsmc.h"
12 #include "stm32f10x_gpio.h"
13 //#include "stm32f10x_i2c.h"
14 //#include "stm32f10x_iwdg.h"
15 //#include "stm32f10x_pwr.h"
16 #include "stm32f10x_rcc.h"
17 //#include "stm32f10x_rtc.h"
18 //#include "stm32f10x_sdio.h"
19 //#include "stm32f10x_spi.h"
20 //#include "stm32f10x_tim.h"
21 //#include "stm32f10x_usart.h"
22 //#include "stm32f10x_wwdg.h"
23 //#include "misc.h"
复制代码
这外面蕴含了全副外设的头文件,点亮一个 LED 咱们只须要 RCC 和 GPIO 这两个外
设的库函数即可,其中 RCC 管制的是时钟,GPIO 管制的具体的 IO 口。所以其余外设库函
数的头文件咱们正文掉,当咱们须要的时候就把相应头文件的正文去掉即可。
stm32f10x_conf.h 这个头文件在 stm32f10x.h 这个头文件的最初面被蕴含,在第 8296
行:
1 #ifdef USE_STDPERIPH_DRIVER
2 #include "stm32f10x_conf.h"
3 #endif
复制代码
代码的意思是,如果定义了 USE_STDPERIPH_DRIVER 这个宏的话,就蕴含
stm32f10x_conf.h 这个头文件。咱们在新建工程的时候,在魔术棒选项卡 C /C++ 中,咱们定
义了 USE_STDPERIPH_DRIVER 这个宏,所以 stm32f10x_conf.h 这个头文件就被
stm32f10x.h 蕴含了,咱们在写程序的时候只须要调用一个头文件:stm32f10x.h 即可。(预
解决指令具体内容会在【C 语言】的文章中提到)
编写 LED 初始化函数
通过寄存器点亮 LED 的操作,咱们晓得操作一个 GPIO 输入的编程要点大略如下:
1、开启 GPIO 的端口时钟
2、抉择要具体管制的 IO 口,即 pin
3、抉择 IO 口输入的速率,即 speed
4、抉择 IO 口输入的模式,即 mode
5、输入高 / 低电平
STM32 的时钟性能十分丰盛,配置灵便,为了降低功耗,每个外设的时钟都能够单独的敞开和开启。STM32 中跟时钟无关的性能都由 RCC 这个外设管制,RCC 中有三个存放
器管制着所以外设时钟的开启和敞开:RCC_APHENR、RCC_APB2ENR 和
RCC_APB1ENR,AHB、APB2 和 APB1 代表着三条总线,所有的外设都是挂载到这三条
总线上,GPIO 属于高速的外设,挂载到 APB2 总线上,所以其时钟有 RCC_APB2ENR 控
制。
GPIO 时钟管制
固件库函数:RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE)函数的
原型为:
1 void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph,
FunctionalState NewState)
2 {
3 /* Check the parameters */
4 assert_param(IS_RCC_APB2_PERIPH(RCC_APB2Periph));
5 assert_param(IS_FUNCTIONAL_STATE(NewState));
6 if (NewState != DISABLE) {
7 RCC->APB2ENR |= RCC_APB2Periph;
8 } else {
9 RCC->APB2ENR &= ~RCC_APB2Periph;
10 }
11 }
复制代码
当程序编译一次之后,把光标定位到函数 / 变量 / 宏定义处,按键盘的 F12 或鼠标右键的 Go to definition of,就能够找到原型。固件库的底层操作的就是 RCC 外设的 APB2ENR
这个寄存器,宏 RCC_APB2Periph_GPIOB 的原型是:0x00000008,即(1<<3),还原成
存器操作就是:RCC->APB2ENR |= 1<<<3。相比固件库操作,寄存器操作的代码可读性
就很差,只有才查阅寄存器配置才晓得具体代码的性能,而固件库操作恰好相反,见名知
意。
GPIO 端口配置
GPIO 的 pin,速度,模式,都由 GPIO 的端口配置寄存器来管制,其中 IO0~IO7 由端
口配置低寄存器 CRL 管制,IO8~IO15 由端口配置高寄存器 CRH 配置。
固件库把端口配置的 pin,速度和模式封装成一个构造体:
1 typedef struct {
2 uint16_t GPIO_Pin;
3 GPIOSpeed_TypeDef GPIO_Speed;
4 GPIOMode_TypeDef GPIO_Mode;
5 } GPIO_InitTypeDef;
复制代码
pin 能够是 GPIO_Pin_0~GPIO_Pin_15 或者是 GPIO_Pin_All,这些都是库事后定义好的
宏。
speed 也被封装成一个构造体:
1 typedef enum {
2 GPIO_Speed_10MHz = 1,
3 GPIO_Speed_2MHz,
4 GPIO_Speed_50MHz
5 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
6
7// 设置引脚为推挽输入
8 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
9
10 // 设置引脚速率为 50MHz
11 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
12
13 /* 调用库函数,初始化 GPIOB0*/
14 GPIO_Init(GPIOB, &GPIO_InitStructure);
复制代码
假使同一端口下不同引脚有不同的模式配置,每次对每个引脚配置实现后都要调用 GPIO 初始化函数,代码如下:
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15 ;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输出
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 ;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
复制代码
GPIO 输入管制
GPIO 输入管制,能够通过端口数据输入寄存器 ODR、端口位设置 / 革除寄存器 BSRR
和端口位革除寄存器 BRR 这三个来管制。
端口输入寄存器 ODR 是一个 32 位的寄存器,低 16 位无效,对应着 IO0~IO15,只能
以字的模式操作,个别应用寄存器操作。
// PB0 输入高电平,点亮 LED
GPIOB->ODR = 1<<0;
复制代码
端口位革除寄存器 BRR 是一个 32 位的寄存器,低十六位无效,对应着 IO0~IO15,只
能以字的模式操作,能够独自对某一个位操作,写 1 清 0。
// PB0 输入低电平,点亮 LED
GPIO_ResetBits(GPIOB, GPIO_Pin_0);
复制代码
BSRR 是一个 32 位的寄存器,低 16 位用于置位,写 1 无效,高 16 位用于复位,写 1
无效,相当于 BRR 寄存器。高 16 位咱们个别不必,而是操作 BRR 这个寄存器,所以
BSRR 这个寄存器个别用来置位操作。
// PB0 输入高电平,燃烧 LED
GPIO_SetBits(GPIOB, GPIO_Pin_0);
复制代码
综上:固件库 LED GPIO 初始化函数
1 void LED_GPIO_Config(void)
2 {
3 // 定义一个 GPIO_InitTypeDef 类型的构造体
4 GPIO_InitTypeDef GPIO_InitStructure;
5
6// 开启 GPIOB 的时钟
7 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
8
9// 抉择要管制的 IO 口
10 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
11
12 // 设置引脚为推挽输入
13 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
14
15 // 设置引脚速率为 50MHz
16 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
17
18 /* 调用库函数,初始化 GPIOB0*/
19 GPIO_Init(GPIOB, &GPIO_InitStructure);
20
21 // 敞开 LED
22 GPIO_SetBits(GPIOB, GPIO_Pin_0);
23 }
复制代码
主函数
1 #include "stm32f10x.h"
2
3
void SOFT_Delay(__IO uint32_t nCount);
4 void LED_GPIO_Config(void);
5
6int main(void)
7 {
8 // 程序来到 main 函数之前,启动文件:statup_stm32f10x_hd.s 曾经调用
9 // SystemInit()函数把零碎时钟初始化成 72MHZ
10 // SystemInit()在 system_stm32f10x.c 中定义
11 // 如果用户想批改零碎时钟,可自行编写程序批改
12
13 LED_GPIO_Config();
14
15 while (1) {
16 // 点亮 LED
17 GPIO_ResetBits(GPIOB, GPIO_Pin_0);
18 Time_Delay(0x0FFFFF);
19
20 // 燃烧 LED
21 GPIO_SetBits(GPIOB, GPIO_Pin_0);
22 Time_Delay(0x0FFFFF);
23
24 }
25// 简陋的软件延时函数
26 void Time_Delay(volatile uint32_t Count)
27 {28 for (; Count != 0; Count--);
29 }
复制代码
留神 void Time_Delay(volatile uint32_t Count)只是一个简陋的软件延时函数,如果小伙伴们有趣味能够看一看 MultiTimer,它是一个软件定时器扩大模块,可有限扩大所需的定时器工作,取代传统的标记位判断形式,更优雅更便捷地管理程序的工夫触发时序。
本文分享自华为云社区《【嵌入式】层层递进,理解库开发》,原文作者:LongYorke。
点击关注,第一工夫理解华为云陈腐技术~