【摘要】从STM32新建工程、编译下载程序出发,让菜鸟由浅入深,尽享STM32标准库开发的乐趣。
自从CubeMX等图象配置软件的出现,朋友们常常点几下键盘就解决了单片机的配置问题。对于追求开发速率的业务场景下中国linux,使用快速配置软件是合理的,高效的,但对于中学生的学习场景下,更为重要的是知其然并知其所以然。
以下是学习(包括但不限于)嵌入式的三个重要内容:
1、学会怎样参考官方的指南和官方的代码来独立写自己的程序。
2、积累常用代码段,晓得那里的问题须要什么代码处理。
3、跟随大鳄脚步,一步一个脚印。
首先:我们都晓得编程时通常查的是《参考指南》,而进行芯片选型或须要芯片数据时,查阅的是《数据指南》。据悉市面上所有关于STM32的书籍都是立足于前两者(+Cortex内核指南)进行编绘。
其次:要分清哪些是内核外设与内核之外的外设,为了易于分辨,根据网上的一种说法,将“内核之外的外设”以“处理器外设”代替。
此外:现在极少使用标准库了,都是HAL库,但作为院校目前教学方法。
我们将以STM3232ff1010xxx为例对标准库开发进行概览。
一、STM32系统结构
STM32f10xxx系统结构
内核IP
从结构框图上看,Cortex-M3内部有若干个总线插口,以使CM3能同时取址和访内(访问显存),它们是:指令储存区总线(两条)、系统总线、私有外设总线。有两条代码储存区总线负责对代码储存区(即FLASH外设)的访问,分别是I-Code总线和D-Code总线。
I-Code用于取指,D-Code用于查表等操作,它们按最佳执行速率进行优化。
系统总线(System)用于访问显存和外设,覆盖的区域包括SRAM,片上外设,片外RAM,片外扩充设备,以及系统级储存区的部份空间。
私有外设总线负责一部份私有外设的访问,主要就是访问调试组件。它们也在系统级储存区。
还有一个MDA总线,从字面上看,DMA是datamemoryaccess的意思,是一种联接内核和外设的桥梁,它可以访问外设、内存,传输不受CPU的控制,而且是单向通讯。简而言之,这个家伙就是一个速率很快的且不受老大控制的数据搬运工。
处理器外设(内核之外的外设)
从结构框图上看,STM32的外设有并口、定时器、IO口、FSMC、SDIO、SPI、I2C等,这种外设按照速率的不同,分别挂载到AHB、APB2、APB1这三条总线上。
二、寄存器
哪些是寄存器?寄存器是外置于各个IP外设中,是一种用于配置外设功能的储存器,而且有想对应的地址。一切库的封装源于映射。
是不是“又臭又长”,假如进行寄存器开发,就须要怼地址以及对寄存器进行字节形参,除了效率低并且容易出错。
来,开个玩笑。
你或许据说过“国际C语言乱码比赛(IOCCC)”下面这个反例就是网上广为留传的一个精典作品:
库的存在就是为了解决这类问题,将代码语义化。语义化思想不仅仅是嵌入式有的,后端代码也在追求语义特点。
三、万物源于点灯
(1)内核库文件剖析
这个头文件实现了:1、内核结构体寄存器定义2、内核寄存器显存映射3、内存寄存器位定义。跟处理器相关的头文件stm32f10x.h实现的功能一样,一个是针对内核的寄存器,一个是针对内核之外,即处理器的寄存器。
内核应用函数库头文件,对应stm32f10x_xxx.h
内核应用函数库文件,对应stm32f10x_xxx.c。在CM3这个内核上面还有一些功能组件,如NVIC、SCB、ITM、MPU、CoreDebug,CM3带有极其丰富的功能组件,而且芯片厂商在设计MCU的时侯有一些并不是非要不可的linux sdio wifi 驱动,是可裁切的,例如MPU、ITM等在STM32上面就没有。其中NVIC在每一个CM3内核的单片机中就会有,但就会被剪裁,只能是CM3NVIC的一个子集。在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的世界。
这个文件的作用是上面实现了各类常用的系统时钟设置函数,有72M,56M,48,36,24,8M,我们使用的是是把系统时钟设置成72M。
这个头文件十分重要,这个头文件实现了:1、处理器外设寄存器的结构体定义2、处理器外设的显存映射3、处理器外设寄存器的位定义。
关于1和2我们在用寄存器照亮LED的时侯有讲解。
其中3:处理器外设寄存器的位定义,这个特别重要,具体是哪些意思?我们晓得一个寄存器有好多个位,每位位写1或者写0的功能都是不一样的,处理器外设寄存器的位定义就是把外设的每位寄存器的每一个位写1的16补码数定义成一个宏,宏名即用该位的名称表示,假如我们操作寄存器要开启某一个功能的话,就不用自己亲自去算这个值是多少,可以直接到这个头文件上面找。
我们以片上外设ADC为例,假定我们要启动ADC开始转换,按照指南我们晓得是要控制ADC_CR2寄存器的位0:ADON,即往位0写1,即:
ADC->CR2=0x00000001;
这是通常的操作方式。如今这个头文件上面有关于ADON位的位定义:
#defineADC_CR2_ADON((uint32_t)0x00000001)
有了这个位定义,我们刚才的代码就弄成了:
ADC->CR2=ADC_CR2_ADON
外设xxx应用函数库头文件,这儿面主要定义了实现外设某一功能的结构体,例如通用定时器有好多功能,有定时功能,有输出比较功能,有输入捕捉功能,而通用定时器有特别多的寄存器要实现某一个功能,例如定时功能,我们根本不晓得具体要操作什么寄存器,这个头文件就为我们打包好了要实现某一个功能的寄存器,是以机构体的方式定义的,例如通用定时器要实现一个定时的功能,我们只须要初始化TIM_TimeBaseInitTypeDef这个结构体上面的成员即可,上面的成员就是定时所须要操作的寄存器。有了这个头文件,我们就晓得要实现某个功能须要操作什么寄存器,之后再回指南中精度这种寄存器的说明即可。
stm32f10x_xxx.c:外设xxx应用函数库,这儿面写好了操作xxx外设的所有常用的函数,我们使用库编程的时侯,使用的最多的就是这儿的函数。
(3)SystemInit
工程中新建main.c。
在此文件中编撰main函数后直接编译会报错:
UndefinedsymbolSystemInit(referredfromstartup_stm32f10x_hd.o).
错误提示说SystemInit没有定义。从剖析启动文件startup_stm32f10x_hd.s时我们晓得
汇编中;分号是注释的意思
第五行第六行代码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(resetandclockcontrol),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是:Outputdataregister的缩写,在STM32上面,其寄存器的命名名称都是英语的缩写,很容易记住。从指南上我们晓得ODR是一个32位的寄存器,低16位有效,高16位保留。低16位对应着IO0~IO16,只要往相应的位置写入0或则1就可以输出低或则高电平。
第一层级:基地址宏定义
时钟控制:
在STM32中,每位外设都有一个起始地址,称作外设基地址,外设的寄存器就以这个基地址为标准依照次序排列,且每位寄存器32位,(旁边作为结构体上面的成员恰好显存对齐)。查表见到时钟由APB2外设时钟使能寄存器(RCC_APB2ENR)来控制,其中PB端口的时钟由该寄存器的位3写1使能。我们可以通过基地址+偏斜量0x18,算出RCC_APB2ENR的地址为:0x40021018。这么使能PB口的时钟代码则如下所示:
模式配置:
同RCC_APB2ENR一样,GPIOB的起始地址是:0X40010C00,我们也可以算出GPIO_CRL的地址为:0x40010C00。这么设置PB0为通用单端输出,输出速度为2M的代码则如下所示:
同上,从指南中我们看见ODR寄存器的地址偏斜是:0CH,可以算出GPIOB_ODR寄存器的地址是:0X40010C00+0X0C=0X40010C0C。如今我们就可以定义GPIOB_ODR这个寄存器了,代码如下:
第一层级:基地址宏定义完成用STM32控制一个LED的完整代码:
第二层级:基地址宏定义+结构体封装
外设寄存器结构体封装
前面我们在操作寄存器的时侯,操作的是寄存器的绝对地址,假如每位寄存器都这样操作,那将十分麻烦。我们考虑到外设寄存器的地址都是基于外设基地址的偏斜地址,都是在外设基地址上挨个连续递增的,每位寄存器占32个或则16个字节,这些方法跟结构体上面的成员类似。所以我们可以定义一种外设结构体,结构体的地址等于外设的基地址,结构体的成员等于寄存器,成员的排列次序跟寄存器的次序一样。这样我们操作寄存器的时侯就不用每次都找到绝对地址,只要晓得外设的基地址就可以操作外设的全部寄存器,即操作结构体的成员即可。
下边我们先定义一个GPIO寄存器结构体,结构体上面的成员是GPIO的寄存器,成员的次序根据寄存器的偏斜地址从低到高排列,成员类型跟寄存器类型一样。(struct用法参考【C语言】(2):关键字的详尽介绍)
在《STM32英文参考指南》8.2寄存器描述章节,我们可以找到结构体上面的7个寄存器描述。在照亮LED的时侯我们只用了CRL和ODR这两个寄存器,至于其他寄存器的功能你们可以自行看指南了解。
在GPIO结构体上面我们用了两个数据类型,一个是uint32_t,表示无符号的32位整型,由于GPIO的寄存器都是32位的。这个类型申明在标准头文件stdint.h上面使用typedef对unsignedint重命名,我们在程序上只要包含这个头文件即可。
另外一个是volatile(volatile用法参考【C语言】(2):关键字的详尽介绍)linux游戏,作用就是告诉编译器这儿的变量会变化不因优化而省略此指令,必须每次都直接读写其值,这样才能确保每次读或则写寄存器都真正执行到位。
外设封装
STM32F1系列的GPIO端口分A~G,即GPIOA、GPIOB。。。。。。GPIOG。每位端口都富含GPIO_TypeDef结构体上面的寄存器,我们可以按照指南各个端口的基地址把GPIO的各个端口定义成一个GPIO_TypeDef类型表针,之后我们就可以按照端口名(实际上现今是结构体表针了)来操作各个端口的寄存器,代码实现如下:
外设显存映射
提到基地址的时侯我们再引人一个知识点:Cortex-M3储存器系统,这个知识点在《Cortex-M3权威手册》第5章里边提到。CM3的地址空间是4GB,如右图所示:
我们这儿要讲的是片上外设,就是我们所说的寄存器的依据地,其大小总共有512MB,512MB是其极限空间,并不是每位单片机都用得完,实际上各个MCU厂商都只是用了一部份而已。STM3232FF1系列用到了:0x40000000~0x5003FFFF。
如今我们说的STM32的寄存器就是坐落这个区域
如今我们说的STM32的寄存器就是坐落这个区域,这儿面ST设计了三条总线:
AHB、APB2和APB1,其中AHB和APB2是高速总线,APB1是低速总线。不同的外设按照速率不同分别挂载到这三条总线上。从下往上依次是:APB1、APB2、AHB,每位总线对应的地址分别是:APB1:0x40000000,APB2:0x40010000,AHB:0x40018000。
这三条总线的基地址我们是从《STM32英文参考指南》2.3小节—存储器映像得到的:APB1的基地址是TIM2定时器的起始地址,APB2的基地址是AFIO的起始地址,AHB的基地址是SDIO的起始地址。
其中APB1地址又称作外设基地址,是所有外设的基地址,称作PERIPH_BASE。
如今我们把这三条总线地址用宏定义下来,之后我们在定义其他外设基地址的时侯,只须要在这三条总线的基址上加上偏斜地址即可,代码如下:
由于GPIO挂载到APB2总线上,这么如今我们就可以按照APB2的基址算出各个GPIO端口的基地址,用宏定义实现代码如下:
第二层级:基地址宏定义+结构体封装完成用STM32控制一个LED的完整代码:
第二层级变化:
①、定义一个外设(GPIO)寄存器结构体,结构体的成员包含该外设的所有寄存器,成员的排列次序跟寄存器偏斜地址一样,成员的数据类型跟寄存器的一样。
②外设显存映射,即把地址跟外设构建起一一对应的关系。
③外设申明,即把外设的名子定义成一个外设寄存器结构体类型的表针。
④通过结构体操作寄存器,实现照亮LED。
第三层级:基地址宏定义+结构体封装+“位封装”(每一位的对应字节封装)
前面我们在控制GPIO输出内容的时侯控制的是ODR(Outputdataregister)寄存器,ODR是一个16位的寄存器,必须以字的方式控制
虽然我们还可以控制BSRR和BRR这两个寄存器来控制IO的电平,下边我们简单介绍下BRR寄存器的功能,BSRR自行看指南研究。
位消除寄存器BRR只能实现位清0操作,是一个32位寄存器,低16位有效,写0没影响,写1清0。
如今我们要使PB0输出低电平,照亮LED,则只要往BRR的BR0位写1即可,其他位为0,代码如下:
1GPIOB->BRR=0X0001;
这时PB0就输出了低电平,LED就被照亮了。
假如要PB2输出低电平,则是:
1GPIOB->BRR=0X0004;
假如要PB3/4/5/6。。。。。。这种IO输出低电平呢?道理是一样的,只要往BRR的相应位置赋不同的值即可。由于BRR是一个16位的寄存器,位数比较多,形参的时侯容易出错,但是从形参的16补码数字我们很难清楚的晓得控制的是那个IO。这时linux sdio wifi 驱动,我们是否可以把BRR的每位位置1都用宏定义来实现,如GPIO_Pin_0就表示0X0001,GPIO_Pin_2就表示0X0004。只要我们定义一次,之后都可以使用,但是还见名知意。“位封装”(每一位的对应字节封装)代码如下:
这时PB0就输出了低电平的代码就弄成了:
1GPIOB->BRR=GPIO_Pin_0;
若果同时让PB0/PB15输出低电平,用或运算,代码:
1GPIOB->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寄存器,代码如下:
1voidGPIO_ResetBits(GPIO_TypeDef*GPIOx,uint16_tGPIO_Pin)2{3GPIOx->BRR=GPIO_Pin;4}
这时,PB0输出低电平,照亮LED的代码就弄成了:
1GPIO_ResetBits(GPIOB,GPIO_Pin_0);
同理,我们可以控制BSRR这个寄存器来实现关掉LED,代码如下:
1//GPIO端口置位函数2voidGPIO_SetBits(GPIO_TypeDef*GPIOx,uint16_tGPIO_Pin)3{4GPIOx->BSRR=GPIO_Pin;5}
这时,PB0输出高电平,关掉LED的代码就弄成了:
1GPIO_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来控制,该头文件主要代码如下:
这儿面包含了全部外设的头文件,照亮一个LED我们只须要RCC和GPIO这两个外设的库函数即可,其中RCC控制的是时钟,GPIO控制的具体的IO口。所以其他外设库函数的头文件我们注释掉,当我们须要的时侯就把相应头文件的注释除去即可。stm32f10x_conf.h这个头文件在stm32f10x.h这个头文件的最后面被包含,在第8296行:
1#ifdefUSE_STDPERIPH_DRIVER2#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)函数的原型为:
当程序编译一次以后,把光标定位到函数/变量/宏定义处,按按键的F12或键盘右键的Gotodefinitionof,就可以找到原型。固件库的底层操作的就是RCC外设的APB2ENR这个寄存器,宏RCC_APB2Periph_GPIOB的原型是:0x00000008,即(1