本文将以STM32F10x为例,对标准库开发进行概览。主要分为三块内容:

·STM32系统结构

·寄存器

·通过点灯案例,详解如何基于标准库构建STM32工程

(文末有STM32、物联网开发相关的网盘资料,包括物联网开发从基础到实战、IoT-ARM结构下的各类智能产品的设计、STM32的开发、全国电赛优秀作品分析等,如有需要请自行领取)

STM32系统结构

STM32f10xxx系统结构

内核IP

从结构框图上看,Cortex-M3内部有若干个总线接口,以使CM3能同时取址和访内(访问内存),它们是:指令存储区总线(两条)、系统总线、私有外设总线。有两条代码存储区总线负责对代码存储区(即 FLASH 外设)的访问,分别是 I-Code 总线D-Code 总线

I-Code用于取指,D-Code用于查表等操作,它们按最佳执行速度进行优化。

系统总线(System)用于访问内存和外设,覆盖的区域包括SRAM,片上外设,片外RAM,片外扩展设备,以及系统级存储区的部分空间。

私有外设总线负责一部分私有外设的访问,主要就是访问调试组件。它们也在系统级存储区。

还有一个DMA总线,从字面上看,DMA是data memory access的意思,是一种连接内核和外设的桥梁,它可以访问外设、内存,传输不受CPU的控制,并且是双向通信。简而言之,这个家伙就是一个速度很快的且不受老大控制的数据搬运工。

处理器外设(内核之外的外设)

从结构框图上看,STM32的外设有串口、定时器、IO口、FSMC、SDIO、SPI、I2C等,这些外设按照速度的不同,分别挂载到AHB、APB2、APB1这三条总线上。

寄存器

什么是寄存器?寄存器是内置于各个IP外设中,是一种用于配置外设功能的存储器,并且有想对应的地址。一切库的封装始于映射

是不是看的眼都花了,如果进行寄存器开发,就需要怼地址以及对寄存器进行字节赋值,不仅效率低而且容易出错。

库的存在就是为了解决这类问题,将代码语义化。语义化思想不仅仅是嵌入式有的,前端代码也在追求语义特性。

从点灯开始学习STM32

内核库文件分析

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内核单片机直接移植。

处理器外设库文件分析

startup_stm32f10x_hd.s

这个是由汇编编写的启动文件,是STM32上电启动的第一个程序,启动文件主要实现了

  • 初始化堆栈指针 SP;
  • 设置 PC 指针=Reset_Handler ;
  • 设置向量表的地址,并 初始化向量表,向量表里面放的是 STM32 所有中断函数的入口地址
  • 调用库函数 SystemInit,把系统时钟配置成 72M,SystemInit 在库文件 stytem_stm32f10x.c 中定义;
  • 跳转到标号_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 外设的所有常用的函数,我们使用库编程的时候,使用的最多的就是这里的函数。

SystemInit

工程中新建main.c 。

在此文件中编写main函数后直接编译会报错:

Undefined symbol SystemInit (referred from startup_stm32f10x_hd.o).

错误提示说SystemInit没有定义。从分析启动文件startup_stm32f10x_hd.s时我们知道,

;Reset handler

Reset_Handler PROC

EXPORT Reset_Handler [WEAK]

IMPORT __main

;IMPORT SystemInit

;LDR R0, =SystemInit

BLX R0

LDR R0, =__main

BX R0

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、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的完整代码:

#define RCC_APB2ENR *(volatile unsigned long *)0x40021018
#define GPIOB_CRL *(volatile unsigned long *)0x40010C00
#define GPIOB_ODR *(volatile unsigned long *)0x40010C0C

int main(void)
{
// 开启端口B 的时钟
RCC_APB2ENR |= 1<<3;

// 配置PB0 为通用推挽输出模式,速率为2M
GPIOB_CRL = (2<<0) | (0<<2);

// PB0 输出低电平,点亮LED
GPIOB_ODR = 0<<0;
}

void SystemInit(void)
{
}

第二层级:基地址宏定义+结构体封装

外设寄存器结构体封装

上面我们在操作寄存器的时候,操作的是寄存器的绝对地址,如果每个寄存器都这样操作,那将非常麻烦。我们考虑到外设寄存器的地址都是基于外设基地址的偏移地址,都是在外设基地址上逐个连续递增的,每个寄存器占32个或者16个字节,这种方式跟结构体里面的成员类似。

所以我们可以定义一种外设结构体,结构体的地址等于外设的基地址,结构体的成员等于寄存器,成员的排列顺序跟寄存器的顺序一样。这样我们操作寄存器的时候就不用每次都找到绝对地址,只要知道外设的基地址就可以操作外设的全部寄存器,即操作结构体的成员即可。

下面我们先定义一个GPIO寄存器结构体,结构体里面的成员是GPIO的寄存器,成员的顺序按照寄存器的偏移地址从低到高排列,成员类型跟寄存器类型一样。

typedef struct

{

volatile uint32_t CRL;

volatile uint32_t CRH;

volatile uint32_t IDR;

volatile uint32_t ODR;

volatile uint32_t BSRR;

volatile uint32_t BRR;

volatile uint32_t LCKR;

} GPIO_TypeDef;

《STM32 中文参考手册》8.2 寄存器描述章节,我们可以找到结构体里面的7个寄存器描述。在点亮LED的时候我们只用了CRL和ODR这两个寄存器,至于其他寄存器的功能大家可以自行看手册了解。

在GPIO结构体里面我们用了两个数据类型,一个是uint32_t,表示无符号的32位整型,因为GPIO的寄存器都是32位的。这个类型声明在标准头文件stdint.h 里面使用typedef对unsigned int重命名,我们在程序上只要包含这个头文件即可。

另外一个是volatile作用就是告诉编译器这里的变量会变化不因优化而省略此指令,必须每次都直接读写其值,这样就能确保每次读或者写寄存器都真正执行到位。

外设封装

STM32F1系列的GPIO端口分A~G,即GPIOA、GPIOB。。。。。。GPIOG。每个端口都含有GPIO_TypeDef结构体里面的寄存器,我们可以根据手册各个端口的基地址把GPIO的各个端口定义成一个GPIO_TypeDef类型指针,然后我们就可以根据端口名(实际上现在是结构体指针了)来操作各个端口的寄存器,代码实现如下:

#define GPIOA ((GPIO_TypeDef *) 0X4001 0800)

#define GPIOB ((GPIO_TypeDef *) 0X4001 0C00)

#define GPIOC ((GPIO_TypeDef *) 0X4001 1000)

#define GPIOD ((GPIO_TypeDef *) 0X4001 1400)

#define GPIOE ((GPIO_TypeDef *) 0X4001 1800)

#define GPIOF ((GPIO_TypeDef *) 0X4001 1C00)

#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。

现在我们把这三条总线地址用宏定义出来,以后我们在定义其他外设基地址的时候,只需要在这三条总线的基址上加上偏移地址即可,代码如下:

#define PERIPH_BASE ((uint32_t)0x40000000)

#define APB1PERIPH_BASE PERIPH_BASE

#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)

#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)

GPIO 端口基地址

因为GPIO挂载到APB2总线上,那么现在我们就可以根据APB2的基址算出各个GPIO端口的基地址,用宏定义实现代码如下:

#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
#define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)
#define GPIOD_BASE (APB2PERIPH_BASE + 0x1400)
#define GPIOE_BASE (APB2PERIPH_BASE + 0x1800)
#define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00)
#define GPIOG_BASE (APB2PERIPH_BASE + 0x2000)

第二层级:基地址宏定义+结构体封装完成用STM32控制一个LED的完整代码:

#include <stdint.h>

#define __IO volatile

typedef struct

{

__IO uint32_t CRL;

__IO uint32_t CRH;

__IO uint32_t IDR;

__IO uint32_t ODR;

__IO uint32_t BSRR;

__IO uint32_t BRR;

__IO uint32_t LCKR;

} GPIO_TypeDef;

typedef struct

{

__IO uint32_t CR;

__IO uint32_t CFGR;

__IO uint32_t CIR;

__IO uint32_t APB2RSTR;

__IO uint32_t APB1RSTR;

__IO uint32_t AHBENR;

__IO uint32_t APB2ENR;

__IO uint32_t APB1ENR;

__IO uint32_t BDCR;

__IO uint32_t CSR;

} RCC_TypeDef;

#define PERIPH_BASE ((uint32_t)0x40000000)

#define APB1PERIPH_BASE PERIPH_BASE

#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)

#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)

#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)

#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)

#define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)

#define GPIOD_BASE (APB2PERIPH_BASE + 0x1400)

#define GPIOE_BASE (APB2PERIPH_BASE + 0x1800)

#define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00)

#define GPIOG_BASE (APB2PERIPH_BASE + 0x2000)

#define RCC_BASE (AHBPERIPH_BASE + 0x1000)

#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)

#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)

#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)

#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)

#define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)

#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)

#define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)

#define RCC ((RCC_TypeDef *) RCC_BASE)

#define RCC_APB2ENR *(volatile unsigned long *)0x40021018

#define GPIOB_CRL *(volatile unsigned long *)0x40010C00

#define GPIOB_ODR *(volatile unsigned long *)0x40010C0C

int main(void)

{

// 开启端口B 的时钟

RCC->APB2ENR |= 1<<3;

// 配置PB0 为通用推挽输出模式,速率为2M

GPIOB->CRL = (2<<0) | (0<<2);

// PB0 输出低电平,点亮LED

GPIOB->ODR = 0<<0;

}

void SystemInit(void)

{

}

第二层级变化:

①、定义一个外设(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,代码如下:

GPIOB->BRR = 0X0001;

这时PB0就输出了低电平,LED就被点亮了。

如果要PB2输出低电平,则是:

GPIOB->BRR = 0X0004;

如果要PB3/4/5/6。。。。。。这些IO输出低电平呢?

道理是一样的,只要往BRR的相应位置赋不同的值即可。因为BRR是一个16位的寄存器,位数比较多,赋值的时候容易出错,而且从赋值的16进制数字我们很难清楚的知道控制的是哪个IO。

这时,我们是否可以把BRR的每个位置1都用宏定义来实现,如GPIO_Pin_0就表示0X0001,GPIO_Pin_2就表示0X0004。只要我们定义一次,以后都可以使用,而且还见名知意。“位封装”(每一位的对应字节封装) 代码如下:

#define GPIO_Pin_0 ((uint16_t)0x0001) /*!< Pin 0 selected */

#define GPIO_Pin_1 ((uint16_t)0x0002) /*!< Pin 1 selected */

#define GPIO_Pin_2 ((uint16_t)0x0004) /*!< Pin 2 selected */

#define GPIO_Pin_3 ((uint16_t)0x0008) /*!< Pin 3 selected */

#define GPIO_Pin_4 ((uint16_t)0x0010) /*!< Pin 4 selected */

#define GPIO_Pin_5 ((uint16_t)0x0020) /*!< Pin 5 selected */

#define GPIO_Pin_6 ((uint16_t)0x0040) /*!< Pin 6 selected */

#define GPIO_Pin_7 ((uint16_t)0x0080) /*!< Pin 7 selected */

#define GPIO_Pin_8 ((uint16_t)0x0100) /*!< Pin 8 selected */

#define GPIO_Pin_9 ((uint16_t)0x0200) /*!< Pin 9 selected */

#define GPIO_Pin_10 ((uint16_t)0x0400) /*!< Pin 10 selected */

#define GPIO_Pin_11 ((uint16_t)0x0800) /*!< Pin 11 selected */

#define GPIO_Pin_12 ((uint16_t)0x1000) /*!< Pin 12 selected */

#define GPIO_Pin_13 ((uint16_t)0x2000) /*!< Pin 13 selected */

#define GPIO_Pin_14 ((uint16_t)0x4000) /*!< Pin 14 selected */

#define GPIO_Pin_15 ((uint16_t)0x8000) /*!< Pin 15 selected */

#define GPIO_Pin_All ((uint16_t)0xFFFF) /*!< All pins selected */

这时PB0就输出了低电平的代码就变成了:

GPIOB->BRR = GPIO_Pin_0;

(如果同时让PB0/PB15输出低电平,用或运算,代码:

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寄存器,代码如下:

void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)

{

GPIOx->BRR = GPIO_Pin;

}

这时,PB0输出低电平,点亮LED的代码就变成了:

GPIO_ResetBits(GPIOB,GPIO_Pin_0);

同理, 我们可以控制BSRR这个寄存器来实现关闭LED,代码如下:

// GPIO 端口置位函数

void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)

{

GPIOx->BSRR = GPIO_Pin;

}

这时,PB0输出高电平,关闭LED的代码就变成了:

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来控制,该头文件主要代码如下:

//#include "stm32f10x_adc.h"

//#include "stm32f10x_bkp.h"

//#include "stm32f10x_can.h"

//#include "stm32f10x_cec.h"

//#include "stm32f10x_crc.h"

//#include "stm32f10x_dac.h"

//#include "stm32f10x_dbgmcu.h"

//#include "stm32f10x_dma.h"

//#include "stm32f10x_exti.h"

//#include "stm32f10x_flash.h"

//#include "stm32f10x_fsmc.h"

#include "stm32f10x_gpio.h"

//#include "stm32f10x_i2c.h"

//#include "stm32f10x_iwdg.h"

//#include "stm32f10x_pwr.h"

#include "stm32f10x_rcc.h"

//#include "stm32f10x_rtc.h"

//#include "stm32f10x_sdio.h"

//#include "stm32f10x_spi.h"

//#include "stm32f10x_tim.h"

//#include "stm32f10x_usart.h"

//#include "stm32f10x_wwdg.h"

//#include "misc.h"

这里面包含了全部外设的头文件,点亮一个LED我们只需要RCC和GPIO 这两个外设的库函数即可,其中RCC控制的是时钟,GPIO控制的具体的IO口。所以其他外设库函数的头文件我们注释掉,当我们需要的时候就把相应头文件的注释去掉即可。

stm32f10x_conf.h这个头文件在stm32f10x.h这个头文件的最后面被包含,在第8296行:

#ifdef USE_STDPERIPH_DRIVER

#include "stm32f10x_conf.h"

#endif

代码的意思是,如果定义了USE_STDPERIPH_DRIVER这个宏的话,就包含stm32f10x_conf.h这个头文件。

我们在新建工程的时候,在魔术棒选项卡C/C++中,我们定义了USE_STDPERIPH_DRIVER 这个宏,所以stm32f10x_conf.h 这个头文件就被stm32f10x.h包含了,我们在写程序的时候只需要调用一个头文件:stm32f10x.h即可。

②编写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)函数的

原型为:

void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph,

FunctionalState NewState)

{

/* Check the parameters */

assert_param(IS_RCC_APB2_PERIPH(RCC_APB2Periph));

assert_param(IS_FUNCTIONAL_STATE(NewState));

if (NewState != DISABLE)

{

RCC->APB2ENR |= RCC_APB2Periph;

}

else

{

RCC->APB2ENR &= ~RCC_APB2Periph;

}

}

当程序编译一次之后,把光标定位到函数/变量/宏定义处,按键盘的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,速度和模式封装成一个结构体:

typedef struct

{

uint16_t GPIO_Pin;

GPIOSpeed_TypeDef GPIO_Speed;

GPIOMode_TypeDef GPIO_Mode;

} GPIO_InitTypeDef;

pin可以是GPIO_Pin_0~GPIO_Pin_15或者是GPIO_Pin_All,这些都是库预先定义好的宏。speed也被封装成一个结构体:

typedef enum

{

GPIO_Speed_10MHz = 1,

GPIO_Speed_2MHz,

GPIO_Speed_50MHz

} GPIOSpeed_TypeDef;

速度可以是10M,2M或者50M,这个由端口配置寄存器的MODE位控制,速度是针对IO口输出的时候而言,在输入的时候可以不用设置。mode也被封装成一个结构体:

typedef enum

{

GPIO_Mode_AIN = 0x0, // 模拟输入

GPIO_Mode_IN_FLOATING = 0x04, // 浮空输入(复位后的状态)

GPIO_Mode_IPD = 0x28, // 下拉输入

GPIO_Mode_IPU = 0x48, // 上拉输入

GPIO_Mode_Out_OD = 0x14, // 通用开漏输出

GPIO_Mode_Out_PP = 0x10, // 通用推挽输出

GPIO_Mode_AF_OD = 0x1C, // 复用开漏输出

GPIO_Mode_AF_PP = 0x18 // 复用推挽输出

} GPIOMode_TypeDef;

IO口的模式有8种,输入输出各4种,由端口配置寄存器的CNF配置。平时用的最多的就是通用推挽输出,可以输出高低电平,驱动能力大,一般用于接数字器件。

最终用固件库实现就变成这样:

// 定义一个GPIO_InitTypeDef 类型的结构体

GPIO_InitTypeDef GPIO_InitStructure;

// 选择要控制的IO 口

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;

// 设置引脚为推挽输出

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;

// 设置引脚速率为50MHz

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;

/*调用库函数,初始化GPIOB0*/

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初始化函数

void LED_GPIO_Config(void)

{

// 定义一个GPIO_InitTypeDef 类型的结构体

GPIO_InitTypeDef GPIO_InitStructure;

// 开启GPIOB 的时钟

RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE);

// 选择要控制的IO 口

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;

// 设置引脚为推挽输出

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;

// 设置引脚速率为50MHz

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

/*调用库函数,初始化GPIOB0*/

GPIO_Init(GPIOB, &GPIO_InitStructure);

// 关闭LED

GPIO_SetBits(GPIOB, GPIO_Pin_0);

}

主函数

#include "stm32f10x.h"

void SOFT_Delay(__IO uint32_t nCount);

void LED_GPIO_Config(void);

int main(void)

{

// 程序来到main 函数之前,启动文件:statup_stm32f10x_hd.s 已经调用

// SystemInit()函数把系统时钟初始化成72MHZ

// SystemInit()在system_stm32f10x.c 中定义

// 如果用户想修改系统时钟,可自行编写程序修改

LED_GPIO_Config();

while ( 1 )

{

// 点亮LED

GPIO_ResetBits(GPIOB, GPIO_Pin_0);

Time_Delay(0x0FFFFF);

// 熄灭LED

GPIO_SetBits(GPIOB, GPIO_Pin_0);

Time_Delay(0x0FFFFF);

}

}

// 简陋的软件延时函数

void Time_Delay(volatile uint32_t Count)

{

for (; Count != 0; Count--);

}

注意void Time_Delay(volatile uint32_t Count)只是一个简陋的软件延时函数,如果小伙伴们有兴趣可以看一看MultiTimer,它是一个软件定时器扩展模块,可无限扩展所需的定时器任务,取代传统的标志位判断方式, 更优雅更便捷地管理程序的时间触发时序。

END

链接:https://pan.baidu.com/s/1-wH4pmEUw5SNS25DTPrdYQ 
提取码:1234

入手STM32单片机的知识点总结相关推荐

  1. STM32单片机的知识点总结

    本文将以STM32F10x为例,对标准库开发进行概览.主要分为三块内容: STM32系统结构 寄存器 通过点灯案例,详解如何基于标准库构建STM32工程 STM32系统结构 STM32f10xxx系统 ...

  2. stm32单片机入门视频教程看哪个?一般用什么软件编程比较好?

    现在我们随便去招聘平台搜索"嵌入式工程师",有很多岗位要求都是需要STM32. 可见目前STM32市场之大,如果我们想从事这个嵌入式单片机开发这个工作,就必须要跟着市场走. 这也是 ...

  3. stm32单片机实现多个闹钟_如何学习单片机:单片机都是相通的,一通百通

    单片机是一种可编程的器件,现在所见到的电子产品中几乎都是以单片机为核心的,根据不同的功能需求,搭建不同的电路,从8位的单片机到32位的单片机,甚至比较高大上的ARM和DSP都可以看成是高级一点的单片机 ...

  4. 最简单DIY基于STM32单片机的WIFI智能小车设计方案

    STM32库函数开发系列文章目录 第一篇:STM32F103ZET6单片机双串口互发程序设计与实现 第二篇:最简单DIY基于STM32单片机的蓝牙智能小车设计方案 第三篇:最简单DIY基于STM32F ...

  5. STM32单片机IAP介绍

    1.什么是IAP? 首先区分下两个概念:ISP和IAP: ISP:In System Programming (在系统中编程),通过芯片专用的串行编程接口对其内部的程序存储器进行擦写. IAP:In ...

  6. stm32单片机基础知识总结(三)

    今天天津下了好大的雪,我想这应该是2022年的第一场雪.虽然已经进入了春天,但是能下这么大的雪,对于大多数北方人来说,并不会感到意外.大家都知道有个词叫做"倒春寒",就是说春天来临 ...

  7. STM32单片机下载调试解决方案 e-link32

     在使用STM32单片机的时候,本人遇到以及看到同行们遇到了各种下载和调试过程中问题种种,比如下载器的驱动不合适,下载出现卡死,调试器会崩掉,还有同行购买了盗版的J-LINK在使用的过程中固件容易 ...

  8. stm32单片机零基础怎么入门,用什么语言编程?

    我们随便在招聘平台搜"嵌入式开发"这个职位,看到很多职位都要求会STM32,为什么? 我个人也用了STM32单片机几年,感觉非常爽,后面做产品能用ST的基本不用别的. 主要原因有以 ...

  9. 基于STM32单片机模块练习——OLED模块

    基于STM32单片机模块练习--OLED模块 相关知识点 向OLED写一个字节 /*** @brief I2C_WriteByte,向OLED寄存器地址写一个byte的数据* @param addr: ...

  10. (转)jLink使用ITM机制实现调试stm32单片机

    ----------------------------------------------------------------------------------------------- 作者:p ...

最新文章

  1. CSDN2008最有价值博客获奖感言--放飞梦想,让我们扬帆远航
  2. Uboot分析(三)
  3. 前端性能优化知识,包括css和js
  4. USACO-Section1.6 Prime Palindromes(素数和回文数)
  5. Java 并发编程:Synchronized 及其实现原理
  6. iOS根据经纬度获得地理名称
  7. html中的a标签、img标签、iframe标签、列表标签
  8. 剑指offer面试题10- I. 斐波那契数列
  9. 信息安全数学基础(近世代数部分)
  10. 群晖服务器白群晖有哪些型号,白群晖和黑群晖,有什么区别?
  11. 中科大EPC课程爬取-带步骤
  12. Linux 安装QT5
  13. 基于微信小程序的毕业设计题目(32)求职招聘小程序(含开题报告、任务书、中期报告、答辩PPT、论文模板)
  14. Java前后端分离项目部署
  15. 科学计算机sd mode使用方法,科学计算器使用方法
  16. Win10文件名排序
  17. 解决联通宽带域名劫持
  18. ios屏幕的旋转,Device Orientation
  19. 罗斯蒙特333D信号转换器
  20. Pytorch实现mnist手写数字识别

热门文章

  1. ADB安装 apk 时,安装失败
  2. D盘根目录下的msdia80.dll文件能不能删除?
  3. QLabel控件功能示例
  4. cvtColor使用
  5. 青龙面板之【追书神器】——5.29
  6. 小学英语与计算机技术整合,信息技术与小学英语课程整合初探
  7. 施耐德M241 plc与IAI伺服电缸通过ethernet/ Ip通讯,plc与伺服套装,送软件和资料
  8. Python学习笔记(15)-Python常用模块总结
  9. Educode--头歌 《软件工程》实验作业6-软件开发计划
  10. PDF解密怎么弄?分享这3个解密软件