tip:寄存器与库函数具有同等重要的地位,在使用时没有优劣之分,笔者往往都是混合编程。
文章目录
前言
读者在学习8位单片机时是否经历过记忆大量寄存器的经历呢?在STM32中具有更多的寄存器,所以出现了各种库,方便人们去使用。这次我们基于正点原子精英版跑马灯(STM32F103)例程讲解 寄存器,STD库之间不同与相同。
一、寄存器与静态库都是什么?
1.寄存器
简单来说,寄存器就是存放东西的东西。从名字来看,跟火车站寄存行李的地方好像是有关系的。只不过火车站行李寄存处,存放的行李;寄存器可能存放的是指令、数据或地址。
2.静态库
静态库是指在我们的应用中,有一些公共代码是需要反复使用,就把这些代码编译为“库”文件;在链接步骤中,连接器将从库文件取得所需的代码,复制到生成的可执行文件中的这种库。STM32的静态库主要是将寄存器操作封装至C语言函数之中,方便人们去调用。
二、寄存器例程
0.准备阶段
(1)STM32参考手册.PDF
(2)寄存器模板例程
1.目标任务拆分
(1)初始化系统时钟
(2)LED灯初始化
(3)LED灯闪烁
2.目标实现
初始化时钟
我们翻取手册可得到这样的一张时钟图,我们最终要设置的是SYSCLK,PCLK1,PCLK2。于是我们可以通过这样的一条路径去设置
按照顺序为使能外部时钟、设置PLL、使能PLL、选择PLL为系统时钟。
我们先要设置时钟源为外部时钟(HSE)
查看手册找到RCC->CR寄存器
我们只需要将HSE设置为 1就行。
所以我们
RCC->CR|=0X00010000;
打开寄存器版本的例程,发现例程使用的是自己编写的函Stm32_Clock_Init(9);我们将其展开查看
发现相同。接下来只需要依照图上的线以此编写就可写出下列代码。
void Stm32_Clock_Init(u8 PLL)
{
unsigned char temp=0;
MYRCC_DeInit(); //复位并配置向量表
RCC->CR|=0x00010000; //外部高速时钟使能HSEON
while(!(RCC->CR>>17));//等待外部时钟就绪
RCC->CFGR=0X00000400; //APB1=DIV2;APB2=DIV1;AHB=DIV1;
PLL-=2; //抵消2个单位(因为是从2开始的,设置0就是2)
RCC->CFGR|=PLL<<18; //设置PLL值 2~16
RCC->CFGR|=1<<16; //PLLSRC ON
FLASH->ACR|=0x32; //FLASH 2个延时周期(这块属于FLASH操作部分,此处不予讲解)
RCC->CR|=0x01000000; //PLLON
while(!(RCC->CR>>25));//等待PLL锁定
RCC->CFGR|=0x00000002;//PLL作为系统时钟
while(temp!=0x02) //等待PLL作为系统时钟设置成功
{
temp=RCC->CFGR>>2;
temp&=0x03;
}
}
最后控制一系列寄存器确定使用外部时钟输入后经过PLL倍频9倍后的72MHz作为系统时钟
AHB与APB1时钟为72MHz,APB2为36MHz
LED灯初始化
LED灯操作其实就是对GPIO的电平进行操作。
GPIO属于STM32的外设,在默认情况下STM32会关闭不需要的外设时钟来减少功耗,所以控制GPIO的第一步是打开GPIO的时钟,查询系统结构能看见GPIOB与GPIOE都挂载在APB2总线下;
时钟控制为RCC寄存器组:查询手册可以看到APB2外设时钟使能寄存器。
查看它的第3位与第6位。
所以写出
RCC->APB2ENR|=1<<3;
RCC->APB2ENR|=1<<6;
到此为止GPIOB、GPIOE已经具备了运行的条件,下一步我们需要配置GPIO,让他实现输出功能。因为要实现闪烁功能这里使用开漏模式与推挽模式均可,这里以推挽模式为例。
我们可以在手册中找到端口配置寄存器
低寄存器是配置GPIOX 0~7,高寄存器是配置GPIOX 8~16
这里我们使用的是GPIOB5与GPIOE5
依照上表能写出
GPIOB->CRL&=0XFF0FFFFF;
GPIOB->CRL|=0X00300000;//PB.5 推挽输出
GPIOE->CRL&=0XFF0FFFFF;
GPIOE->CRL|=0X00300000;//PE.5 推挽输出
这样GPIO就可以使用了,我们初始化的时候可以给这个GPIO设一个初始值。我们查询手册能找到端口输出数据寄存器我们这里都设置为高电平
GPIOE->ODR|=1<<5; //PE.5输出高
GPIOB->ODR|=1<<5; //PB.5 输出高
到此为止GPIO就初始化完成了
LED灯闪烁
LED灯闪烁从根本上说就是GPIO的电平变换,我们可以每次改变GPIOX->ODR的值,但是代码量多了就会变得可读性极为差,所以我们引入Cortex M3架构支持的位带操作
#include "sys.h"
#define LED0 PBout(5)// PB5
#define LED1 PEout(5)// PE5
我们现在就可以用
LED0=1;// LED0灯灭
LED1=0;// LED1灯亮
LED1=1;// LED1灯灭
LED0=0;// LED0灯灭
控制灯的闪烁。
但是闪烁时间过快我们还需要在其中加入延时。我们可以像51那样在两个之间加入36M次的运算来实现半秒的延时,我们这里使用正点原子配套的delay程序来延时,delay程序具体实现方法为调用内部systick时钟进行定时,想了解的小伙伴可以自行学习,这里不过多讲述。
delay中包含个函数
函数原型 | 函数功能 |
---|---|
void delay_init(void); | 初始化delay模块 |
void delay_ms(u16 nms); | 延时nms毫秒 |
void delay_us(u32 nus) | 延时nus微秒 |
所以我们可以写出循环闪烁的代码
delay_init();
while(1)
{
LED0=1;// LED0灯灭
LED1=0;// LED1灯亮
delay_ms(500);
LED1=1;// LED1灯灭
LED0=0;// LED0灯灭
delay_ms(500);
}
这样我们的寄存器版本程序就写完了,可以发现我们全程都在查表、编写,同时我们的操作也是直接对底层寄存器进行操作。
三、库函数例程
我们先看一下库函数的例程构成那么库函数是怎么控制底层的呢?我们看一下手册可以发现用户去调用FWLib组与Hardware中的函数从而间接的控制寄存器。
首先要提一下, 在固件库中, GPIO 端口操作对应的库函数函数以及相关定义在文件stm32f10x_gpio.h 和 stm32f10x_gpio.c 中。
0.准备阶段
(1)STM32F1开发指南-库函数版本.pdf
(2)库函数模板例程
1.目标任务拆分
(1)初始化系统时钟
(2)LED灯初始化
(3)LED灯闪烁
2.目标实现
初始化时钟
标准库例程打开并未发现有时钟初始化代码,那么初始化去哪里了呢?
仔细查看我们发现库函数有一个使用汇编写好的startup_stm32f10x_hd.s文件
打开查看发现这一行
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
单片机上电后会产生复位,依据此段代码可知,首先运行SystemInit的代码,再运行main主函数。
打SystemInit发现逻辑顺序与寄存器版本无差别,而且都是操作寄存器。
LED灯初始化
GPIO 相关的函数和定义分布在固件库文件 stm32f10x_gpio.c 和头文件 stm32f10x_gpio.h 文件中。在固件库开发中, 操作寄存器 CRH 和 CRL 来配置 IO 口的模式和速度是通过 GPIO 初始化
函数完成:
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
这个函数有两个参数, 第一个参数是用来指定 GPIO,取值范围为 GPIOA~GPIOG。第二个参数为初始化参数结构体指针,结构体类型为 GPIO_InitTypeDef。下面我们看看这个结构体的定义。
typedef struct
{
uint16_t GPIO_Pin;
GPIOSpeed_TypeDef GPIO_Speed;
GPIOMode_TypeDef GPIO_Mode;
}GPIO_InitTypeDef;
具体操作在上次推送中有详细解释,这里不做赘述。
最终我们可以编写出一下代码
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOE, ENABLE); //使能PB,PE端口时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //LED0-->PB.5 端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO口速度为50MHz
GPIO_Init(GPIOB, &GPIO_InitStructure); //根据设定参数初始化GPIOB.5
GPIO_SetBits(GPIOB,GPIO_Pin_5); //PB.5 输出高
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //LED1-->PE.5 端口配置, 推挽输出
GPIO_Init(GPIOE, &GPIO_InitStructure); //推挽输出 ,IO口速度为50MHz
GPIO_SetBits(GPIOE,GPIO_Pin_5); //PE.5 输出高
LED灯闪烁
在固件库中设置 ODR 寄存器的值来控制 IO 口的输出状态是通过函数 GPIO_Write 来实现
的:
函数名 | 函数举例 | 函数功能 |
---|---|---|
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); | GPIO_SetBits(GPIOB,GPIO_Pin_5); | 将GPIOx的GPIO_Pin口设置为高电平 |
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); | GPIO_ResetBits(GPIOB,GPIO_Pin_5); | 将GPIOx的GPIO_Pin口设置为低电平 |
再加上正点原子提供的delay_ms函数可写出闪烁代码
delay_init(); //初始化延时函数
while(1)
{
GPIO_ResetBits(GPIOB,GPIO_Pin_5); //LED0对应引脚GPIOB.5拉低,亮 等同LED0=0;
GPIO_SetBits(GPIOE,GPIO_Pin_5); //LED1对应引脚GPIOE.5拉高,灭 等同LED1=1;
delay_ms(300); //延时300ms
GPIO_SetBits(GPIOB,GPIO_Pin_5); //LED0对应引脚GPIOB.5拉高,灭 等同LED0=1;
GPIO_ResetBits(GPIOE,GPIO_Pin_5); //LED1对应引脚GPIOE.5拉低,亮 等同LED1=0;
delay_ms(300); //延时300ms
}
当然也可以和寄存器一样使用位带操作控制LED灯,和寄存器版本相同先定义后控制
delay_init(); //延时函数初始化
while(1)
{
LED0=!LED0;
LED1=!LED1;
delay_ms(300); //延时300ms
}
这样我们程序算是写
完啦,可以发现我们的查表量减少了很多,而且从函数名称我们也能直观的了解这个函数的功能,不需要了解抽象的寄存器。极大的精简了编程环节。
两者比较
名称 | 移植性 | 阅读性 | 代码空间 | 执行效率 |
---|---|---|---|---|
标准库 | ++ | ++ | +++ | + |
寄存器 | + | + | +++ |
库函数相比于寄存器在编写、阅读与移植方面较高的优势,而寄存器在代码所占空间执行效率方面也有绝对性的优势,我们不能仅仅通过某方面评判某种编译方式的优劣,而要取长补短,在实时性强的地方使用寄存器,在对阅读性与移植有要求的地方尽可能使用库函数。这样我们的单片机才能充分的发挥他的功能。