tip:寄存器与库函数具有同等重要的地位,在使用时没有优劣之分,笔者往往都是混合编程。
源码地址(免费):
https://download.csdn.net/download/qq_18381887/14926291
文章目录
前言
读者在学习8位单片机时是否经历过记忆大量寄存器的经历呢?在STM32中具有更多的寄存器,所以出现了各种库,方便人们去使用。这次我们基于正点原子精英版串口(STM32F103)例程讲解 寄存器,STD库,HAL库之间不同与相同。
一、寄存器与静态库都是什么?
1.寄存器
简单来说,寄存器就是存放东西的东西。从名字来看,跟火车站寄存行李的地方好像是有关系的。只不过火车站行李寄存处,存放的行李;寄存器可能存放的是指令、数据或地址。
2.静态库
静态库是指在我们的应用中,有一些公共代码是需要反复使用,就把这些代码编译为“库”文件;在链接步骤中,连接器将从库文件取得所需的代码,复制到生成的可执行文件中的这种库。STM32的静态库主要是将寄存器操作封装至C语言函数之中,方便人们去调用。
二、例程讲解
首先我们先过一遍使用串口的方法:
0.系统初始化
1.初始化 串口及相关外设
2.设置串口中断
0.系统初始化
寄存器
我们翻取手册可得到这样的一张时钟图,我们最终要设置的是SYSCLK,PCLK1,PCLK2。于是我们可以通过这样的一条路径去设置
我们先要设置时钟源为外部时钟(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
STD库
(1)时钟从初始化
标准库例程打开并未发现有时钟初始化代码,那么初始化去哪里了呢?
仔细查看我们发现库函数有一个使用汇编写好的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发现逻辑顺序与寄存器版本无差别,而且都是操作寄存器。
(2)NVIC组配置
打开STD库文件:misc.h
翻至文件底部
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);
void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset);
void NVIC_SystemLPConfig(uint8_t LowPowerMode, FunctionalState NewState);
void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource);
发现已经申明NVIC_PriorityGroupConfig函数
我们只需要在主函数中调用此函数就可轻松的配置好相关设置;
一般情况下我们使用4个抢占优先级,4个响应优先级,那我们怎么去配置呢?
首先我们对NVIC_PriorityGroupConfig右键,选择 Go To Defination,进入函数主体部分
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)
{
/* Check the parameters */
assert_param(IS_NVIC_PRIORITY_GROUP(NVIC_PriorityGroup));//检查输入数据有效性
/* Set the PRIGROUP[10:8] bits according to NVIC_PriorityGroup value */
SCB->AIRCR = AIRCR_VECTKEY_MASK | NVIC_PriorityGroup;
}
发现传入参数都是先检查参数有效性,我们对着S_NVIC_PRIORITY_GROUP进行Go To Defination
#define IS_NVIC_PRIORITY_GROUP(GROUP) (((GROUP) == NVIC_PriorityGroup_0) || \
((GROUP) == NVIC_PriorityGroup_1) || \
((GROUP) == NVIC_PriorityGroup_2) || \
((GROUP) == NVIC_PriorityGroup_3) || \
((GROUP) == NVIC_PriorityGroup_4))
所以我们应该在是主函数中输入
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
HAL库
打开HAL库例程后发现有一个Stm32_Clock_Init(u32 PLL)的函数,但是我们在使用时必须先初始化HAL库,所以我们首先使用
HAL_Init();
我们现在进入Stm32_Clock_Init查看发现
void Stm32_Clock_Init(u32 PLL)
{
HAL_StatusTypeDef ret = HAL_OK;
RCC_OscInitTypeDef RCC_OscInitStructure;
RCC_ClkInitTypeDef RCC_ClkInitStructure;
RCC_OscInitStructure.OscillatorType=RCC_OSCILLATORTYPE_HSE; //时钟源为HSE
RCC_OscInitStructure.HSEState=RCC_HSE_ON; //打开HSE
RCC_OscInitStructure.HSEPredivValue=RCC_HSE_PREDIV_DIV1; //HSE预分频
RCC_OscInitStructure.PLL.PLLState=RCC_PLL_ON; //打开PLL
RCC_OscInitStructure.PLL.PLLSource=RCC_PLLSOURCE_HSE; //PLL时钟源选择HSE
RCC_OscInitStructure.PLL.PLLMUL=PLL; //主PLL倍频因子
ret=HAL_RCC_OscConfig(&RCC_OscInitStructure);//初始化
if(ret!=HAL_OK) while(1);
//选中PLL作为系统时钟源并且配置HCLK,PCLK1和PCLK2
RCC_ClkInitStructure.ClockType=(RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2);
RCC_ClkInitStructure.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK; //设置系统时钟时钟源为PLL
RCC_ClkInitStructure.AHBCLKDivider=RCC_SYSCLK_DIV1; //AHB分频系数为1
RCC_ClkInitStructure.APB1CLKDivider=RCC_HCLK_DIV2; //APB1分频系数为2
RCC_ClkInitStructure.APB2CLKDivider=RCC_HCLK_DIV1; //APB2分频系数为1
ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure,FLASH_LATENCY_2); //同时设置FLASH延时周期为2WS,也就是3个CPU周期。
if(ret!=HAL_OK) while(1);
}
以结构体+函数为主,编程人员不需要知道操作那些寄存器,只需要调整相关结构体就行。
2.初始化 串口及相关外设
我们也是分为几步:
GPIO初始化
串口初始化
寄存器
和时钟初始化相同,我们查询STM32f1手册,进行编写可得
float temp;
u16 mantissa;
u16 fraction;
temp=(float)(pclk2*1000000)/(bound*16);//得到USARTDIV
mantissa=temp; //得到整数部分
fraction=(temp-mantissa)*16; //得到小数部分
mantissa<<=4;
mantissa+=fraction;
RCC->APB2ENR|=1<<2; //使能PORTA口时钟
RCC->APB2ENR|=1<<14; //使能串口时钟
GPIOA->CRH&=0XFFFFF00F;//IO状态设置
GPIOA->CRH|=0X000008B0;//IO状态设置
RCC->APB2RSTR|=1<<14; //复位串口1
RCC->APB2RSTR&=~(1<<14);//停止复位
//波特率设置
USART1->BRR=mantissa; // 波特率设置
USART1->CR1|=0X200C; //1位停止,无校验位.
STD库
我们要初始化GPIO和USART,我们可以打开stm32f10x_gpio.h与stm32f10x_usart.h查看相关函数。
发现有
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);
void USART_Cmd(USART_TypeDef* USARTx, FunctionalState NewState);
调用俩函数,调整相关结构体数据
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
//使能USART1,GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);
//USART1_TX GPIOA.9
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9
//USART1_RX GPIOA.10初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10
//USART 初始化设置
USART_InitStructure.USART_BaudRate = bound;//串口波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
HAL库
HAL库的精髓在于非常高的可移植性,所以HAL库引入了三个概念
1.句柄
2.MCU底层初始化函数
3.回调函数
这里我们先进行串口初始化,方法与STD库类似
UART1_Handler.Instance=USART1; //USART1
UART1_Handler.Init.BaudRate=bound; //波特率
UART1_Handler.Init.WordLength=UART_WORDLENGTH_8B; //字长为8位数据格式
UART1_Handler.Init.StopBits=UART_STOPBITS_1; //一个停止位
UART1_Handler.Init.Parity=UART_PARITY_NONE; //无奇偶校验位
UART1_Handler.Init.HwFlowCtl=UART_HWCONTROL_NONE; //无硬件流控
UART1_Handler.Init.Mode=UART_MODE_TX_RX; //收发模式
HAL_UART_Init(&UART1_Handler); //HAL_UART_Init()会使能UART1
在执行HAL_UART_Init时会调用weak void HAL_UART_MspInit(UART_HandleTypeDef *huart),这里的weak类型是代表能被重定义,我们现在编写void HAL_UART_MspInit(UART_HandleTypeDef *huart)
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
//GPIO端口设置
GPIO_InitTypeDef GPIO_Initure;
if(huart->Instance==USART1)//如果是串口1,进行串口1 MSP初始化
{
__HAL_RCC_GPIOA_CLK_ENABLE(); //使能GPIOA时钟
__HAL_RCC_USART1_CLK_ENABLE(); //使能USART1时钟
__HAL_RCC_AFIO_CLK_ENABLE();
GPIO_Initure.Pin=GPIO_PIN_9; //PA9
GPIO_Initure.Mode=GPIO_MODE_AF_PP; //复用推挽输出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH;//高速
HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化PA9
GPIO_Initure.Pin=GPIO_PIN_10; //PA10
GPIO_Initure.Mode=GPIO_MODE_AF_INPUT; //模式要设置为复用输入模式!
HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化PA10
}
}
这样的的编写方法可以是我们能将此程序轻易的移植到各种MCU之上,且仅需改动Msp函数。
2.设置串口中断
这一步里我们需要配置并打开串口中断,同时编写终端服务函数
寄存器
和往常一样,查询寄存器,编写函数(以前没有初始化NVIC)
u32 temp,temp1;
USART1->CR1|=1<<5; //接收缓冲区非空中断使能
temp1=(~2)&0x07;//取后三位
temp1<<=8;
temp=SCB->AIRCR; //读取先前的设置
temp&=0X0000F8FF; //清空先前分组
temp|=0X05FA0000; //写入钥匙
temp|=temp1;
SCB->AIRCR=temp; //设置分组
temp=2<<(2);
temp|=2&(0x0f>>2);
temp&=0xf; //取低四位
NVIC->ISER[USART1_IRQn/32]|=(1<<USART1_IRQn%32);//使能中断位(要清除的话,相反操作就OK)
中断服务函数单开一节
STD库
查询stm32f10x_usart.h与misc.h可编写:
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断
中断服务函数单开一节
HAL 库
和 STD库方法无差别
代码:
HAL_NVIC_EnableIRQ(USART1_IRQn); //使能USART1中断通道
HAL_NVIC_SetPriority(USART1_IRQn,3,3); //抢占优先级3,子优先级3
诶?发现没,他以前也没有初始化NVIC 现在也没有初始化。这不会报错么?
不会的,因为NVIC在HAL_Init这个函数里面配置过了🤓
可见HAL库代码极其少。
中断服务函数单开一节
中断服务函数(STD与寄存器版)
这是正点原子自带的中断服务函数,协议为0d 0a为结尾数据有效,这里不给与讲解。有兴趣的小伙伴可以仔细看看。
void USART1_IRQHandler(void)
{
u8 res;
#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
OSIntEnter();
#endif
if(USART1->SR&(1<<5)) //接收到数据
{
res=USART1->DR;
if((USART_RX_STA&0x8000)==0)//接收未完成
{
if(USART_RX_STA&0x4000)//接收到了0x0d
{
if(res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
else USART_RX_STA|=0x8000; //接收完成了
}else //还没收到0X0D
{
if(res==0x0d)USART_RX_STA|=0x4000;
else
{
USART_RX_BUF[USART_RX_STA&0X3FFF]=res;
USART_RX_STA++;
if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收
}
}
}
}
#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
OSIntExit();
#endif
}
中断服务函数(HAL库)
前文我们所说,我们的HAL库引入了3个概念
这里是最后一个概念,回调(callback)函数。
在发生中断以后,不同芯片相关标志位不同,所以我们将中断服务函数重新编写,使得清除中断标志位以及获取中断类型的任务交给库函数,我们在使用中断服务函数时只需要编写回调函数即可
回调函数名称可在相关.h文件中找到比如串口读取回调函数可在stm32f1xx_hal_uart.h找到void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
例:
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&UART1_Handler); //调用HAL库中断处理公用函数
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance==USART1)//如果是串口1
{
if((USART_RX_STA&0x8000)==0)//接收未完成
{
if(USART_RX_STA&0x4000)//接收到了0x0d
{
if(aRxBuffer[0]!=0x0a)USART_RX_STA=0;//接收错误,重新开始
else USART_RX_STA|=0x8000; //接收完成了
}
else //还没收到0X0D
{
if(aRxBuffer[0]==0x0d)USART_RX_STA|=0x4000;
else
{
USART_RX_BUF[USART_RX_STA&0X3FFF]=aRxBuffer[0] ;
USART_RX_STA++;
if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收
}
}
}
}
}
三者比较
寄存器相对于其他两者来说,更为底层,执行效率更高,但是每一步都需要查询数据手册,可读性较低,移植性几乎不存在。
STD库时ST官方发布的静态库文件,底层仍为寄存器操作,相当于将同功能的寄存器操作打包,使得程序更具有可读性同时兼顾执行速度。但是移植性仍然一般。
HAL库将MCU分为多层,人们可以方便的裁剪部分功能。同时对于不同的硬件来说,只需要修改硬件层代码对于ST的芯片来说可以使用STM32CubeMX生成驱动代码,而逻辑部分不需要任何改变,非常易于在不同种芯片上的移植,但是牺牲了部分的执行速度。其底层仍为寄存器操作
总结
三者都有各自的优缺点,但是其根本都是对相关寄存器操作,虽然库函数能大幅度减少我们的代码量,但是在部分对于运行速度要求高的代码区域仍然需要使用直接操作寄存器,甚至是使用执行效率更高的汇编进行编写。总的来说库函数操作简单,但是效率不如寄存器操作的高;寄存器操作很复杂,因为要熟悉上百个寄存器,但是程序效率很高。