摘要:从STM32新建工程、编译下载程序出发,让新手由浅入深,尽享STM32标准库开发的乐趣。
自从CubeMX等图像配置软件的出现,同窗们每每点几下鼠标就解决了单片机的配置问题。对于追求开发速度的业务场景下,使用快速配置软件是合理的,高效的,但对于学生的学习场景下,更为重要的是知其然并知其因此然。前端
如下是学习(包括但不限于)嵌入式的三个重要内容,编程
一、学会如何参考官方的手册和官方的代码来独立写本身的程序。函数
二、积累经常使用代码段,知道哪里的问题须要哪些代码处理。学习
三、跟随大佬步伐,一步一个脚印。优化
咱们将以STM32f10xxx为例对标准库开发进行概览。ui
STM32f10xxx 系统结构spa
从结构框图上看,Cortex-M3 内部有若 干个总线接口,以使 CM3 能同时取址和访内(访问内存),它们是: 指令存储区总线(两条)、系统总线、私有外设总线。有两条代码存储区总线负责对代 码存储区(即 FLASH 外设)的访问,分别是 I-Code 总线和 D-Code 总线。操作系统
I-Code 用于取指,D-Code 用于查表等操做,它们按最佳执行速度进行优化。设计
系统总线(System)用于访问内存和外设,覆盖的区域包括 SRAM,片上外设,片外 RAM,片外扩展设备,以及系统级存储区的部分空间。3d
私有外设总线负责一部分私有外设的访问,主要就是访问调试组件。它们也在系统级 存储区。
还有一个 MDA 总线,从字面上看,DMA 是 data memory access 的意思,是一种链接内核和外设的桥梁,它能够访问外设、内存,传输不受 CPU 的控制,而且是双向通讯。简而言之,这个家伙就是一个速度很快的且不受老大控制的数据搬运工。
从结构框图上看,STM32 的外设有 串口、定时器、IO 口、FSMC、SDIO、SPI、I2C 等,这些外设按 照速度的不一样,分别挂载到 AHB、APB二、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 %d\n"):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);}
库的存在就是为了解决这类问题,将代码语义化。语义化思想不只仅是嵌入式有的,前端代码也在追求语义特性。
这个头文件实现了:一、内核结构体寄存器定义 二、内核寄存器内存映射 三、内存寄存 器位定义。跟处理器相关的头文件 stm32f10x.h 实现的功能同样,一个是针对内核的寄存器,一个是针对内核以外,即处理器的寄存器。
内核应用函数库头文件,对应 stm32f10x_xxx.h
内核应用函数库文件,对应 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 内核单片机 直接移植。
这个是由汇编编写的启动文件,是 STM32 上电启动的第一个程序,启动文件主要实现 了
这个文件的做用是里面实现了各类经常使用的系统时钟设置函数,有 72M,56M,48, 36,24,8M,咱们使用的是是把系统时钟设置成 72M。
这个头文件很是重要,这个头文件实现了:一、处理器外设寄存器 的结构体定义 二、处理器外设的内存映射 三、处理器外设寄存器的位定义。
关于 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
外设 xxx 应用函数库头文件,这里面主要定义了实现外设某一功能 的结构体,好比通用定时器有不少功能,有定时功能,有输出比较功能,有输入捕捉功 能,而通用定时器有很是多的寄存器要实现某一个功能,好比定时功能,咱们根本不知道 具体要操做哪些寄存器,这个头文件就为咱们打包好了要实现某一个功能的寄存器,是以机构体的形式定义的,好比通用定时器要实现一个定时的功能,咱们只须要初始化 TIM_TimeBaseInitTypeDef 这个结构体里面的成员便可,里面的成员就是定时所须要 操做的寄存器。 有了这个头文件,咱们就知道要实现某个功能须要操做哪些寄存器,而后 再回手册中精度这些寄存器的说明便可。
stm32f10x_xxx.c:外设 xxx 应用函数库,这里面写好了操做 xxx 外设的全部经常使用的函 数,咱们使用库编程的时候,使用的最多的就是这里的函数。
工程中新建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。
如图,达到第四层级即是咱们所熟知的固件库或HAL库的效果。固然库的编写还须要考虑许多问题,不止于这些内容。咱们须要的是了解库封装的大概过程。
将库封装等级分为四级来介绍是为了有层次感,就像打怪升级同样,进行认知理解的升级。
咱们都知道,操做GPIO输出分三大步:
STM32 外设不少,为了下降功耗,每一个外设都对应着一个时钟,在系统复位的时候这些时钟都是被关闭的,若是想要外设工做,必须把相应的时钟打开。
STM32 的全部外设的时钟由一个专门的外设来管理,叫RCC(reset and clockcontrol),RCC 在STM32 参考手册的第六章。
STM32 的外设由于速率的不一样,分别挂载到三条总系上:AHB、APB二、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 的寄存器就是位于这个区域
如今咱们说的STM32 的寄存器就是位于这个区域,这里面ST 设计了三条总线:AHB、APB2 和APB1,其中AHB 和APB2 是高速总线,APB1 是低速总线。不一样的外设根据速度不一样分别挂载到这三条总线上。从下往上依次是:APB一、APB二、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 挂载到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)
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 14 typedef 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。其余外依葫芦画瓢便可。
以上,是对库封住过程的概述,下面咱们正在地使用库函数编写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 的操做,咱们知道操做一个GPIO 输出的编程要点大概以下:
一、开启GPIO 的端口时钟
二、选择要具体控制的IO 口,即pin
三、选择IO 口输出的速率,即speed
四、选择IO 口输出的模式,即mode
五、输出高/低电平
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 } GPIOSpeed_TypeDef;
速度能够是10M,2M 或者50M,这个由端口配置寄存器的MODE 位控制,速度是针对IO 口输出的时候而言,在输入的时候能够不用设置。mode 也被封装成一个结构体:
1 typedef enum { 2 GPIO_Mode_AIN = 0x0, // 模拟输入 3 GPIO_Mode_IN_FLOATING = 0x04, // 浮空输入(复位后的状态) 4 GPIO_Mode_IPD = 0x28, // 下拉输入 5 GPIO_Mode_IPU = 0x48, // 上拉输入 6 GPIO_Mode_Out_OD = 0x14, // 通用开漏输出 7 GPIO_Mode_Out_PP = 0x10, // 通用推挽输出 8 GPIO_Mode_AF_OD = 0x1C, // 复用开漏输出 9 GPIO_Mode_AF_PP = 0x18 // 复用推挽输出 10 } GPIOMode_TypeDef;
IO 口的模式有8 种,输入输出各4 种,由端口配置寄存器的CNF 配置。平时用的最多的就是通用推挽输出,能够输出高低电平,驱动能力大,通常用于接数字器件。至于剩下的七种模式的用法和电路原理,咱们在后面的GPIO 章节再详细讲解。
最终用固件库实现就变成这样:
1 // 定义一个GPIO_InitTypeDef 类型的结构体 2 GPIO_InitTypeDef GPIO_InitStructure; 3 4// 选择要控制的IO 口 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。