查看原文
其他

用CubeMX + HAL库开发它不香吗?

武理余阳 果果小师弟 2022-07-15

击上方“果果小师弟”,选择“置顶/星标公众号

干货福利,第一时间送达!

摘要:如果你学过STC51,你一定知道STC51操作是极其方便的。如果你学过STM32的库函数,你一定知道STM32操作是极其繁琐的。传统的库函数开发方式,将太多时间花费在各种东西的初始化上。同时,如果你学过STM32F1、STM32F3、STM32F4的话,你会发现对于不同型号的STM32在使用库函数的开发方式下,他的初始化流程也是不一样的,这也是传统开发方式的一种弊端。而CubeMX + HAL库开发的方式,则是省去了初始化的部分,让开发人员将更多的精力放在业务的处理!但是寄存器及库函数的开发方式也是有必要学习的,因为CubeMX也可能存在 Bug, 如果你对寄存器及库函数不了解那你会很被动。

本文主要讲解GPIO、串口通信、外部中断、时钟树、定时器五个内容,关于STM32标准库开发已经在前面讲过了,需要的小伙伴可以到前面看看。本文将介绍CubeMX + HAL库开发需要注意的部分,关于具体的细节部分不做介绍。

一、GPIO

GPIO(英语:General-purpose input/output),通用型之输入输出的简称,其接脚可以供使用者由程控自由使用,PIN 脚依现实考量可作为通用输入(GPI)或通用输出(GPO)或通用输入与输出(GPIO)

1.1 GPIO 8 种工作模式

GPIO_Mode_AIN 模拟输入 GPIO_Mode_IN_FLOATING 浮空输入 GPIO_Mode_IPD 下拉输入 GPIO_Mode_IPU 上拉输入 GPIO_Mode_Out_OD 开漏输出 GPIO_Mode_Out_PP 推挽输出 GPIO_Mode_AF_OD 复用开漏输出 GPIO_Mode_AF_PP 复用推挽输出

1.2 应用总结

1、上拉输入、下拉输入可以用来检测外部信号;例如,按键等;

2、浮空输入模式,由于输入阻抗较大,一般把这种模式用于标准通信协议的 I2C、USART 的接收端;

3、普通推挽输出模式一般应用在输出电平为 0 和 3.3V 的场合。而普通开漏输出模式一般应用在电平不匹配的场合,如需要输出 5V 的高电平,就需要在外部一个上拉电阻,电源为 5V,把 GPIO 设置为开漏模式,当输出高阻态时,由上拉电阻和电源向外输出 5V 电平。

4、对于相应的复用模式(复用输出来源片上外设),则是根据 GPIO 的复用功能来选择,如 GPIO 的引脚用作串口的输出(USART/SPI/CAN),则使用复用推挽输出模式。如果用在 I2C、SMBUS 这些需要线与功能的复用场合,就使用复用开漏模式。

5、在使用任何一种开漏模式时,都需要接上拉电阻。

1.3 CubeMX相关配置

3.1 选择引脚类型

GPIO_Input输入引脚 GPIO_Output输出引脚

选择引脚类型

3.2 配置引脚

对于输入引脚,可以配置的就是GPIO Pull-up/Pull-down。这分别对应的就是Pull-up(输入上拉)与 Pull-down (输入下拉)。

Pull-up: 输入上拉就是把电位拉高,比如拉到 Vcc。上拉就是将不确定的信号通过一个电阻嵌位在高电平。电阻同时起到限流的作用。弱强只是上拉电阻的阻值不同,没有什么严格区分。

Pull-down: 输入下拉就是把电压拉低,拉到 GND。与上拉原理相似。简单的说,如果你希望你的引脚平时处于高电平用于检测低电平,你就使用 Pull-up。如果你希望你的引脚平时处于低电平用于检测高电平,你就使用 Pull-down。

配置输入引脚

对于输出引脚,比输入多了更多的配置:

GPIO output level -> 初始化输出电平
GPIO mode -> 输出方式 -> 开漏或推挽输出
GPIO Pull-up/Pull-down -> 上拉或下拉输出
Maximum output speed 选中 GPIO 管脚的速率

选中GPIO 管脚的速率

I/O 口的输出模式下,有 3 种输出速度可选 (Low - 2MHz、Medium - 10MHz、High -50MHz),这个速度是指 I/O 口驱动电路的响应速度而不是输出信号的速度,输出信号的速度与程序有关(芯片内部在 I/O 口的输出部分安排了多个响应速度不同的输出驱动电路,用户可以根据自己的需要选择合适的驱动电路)。通过选择速度来选择不同的输出驱动模块,达到最佳的噪声控制和降低功耗的目的。高频的驱动电路,噪声也高,当不需要高的输出频率时,请选用低频驱动电路,这样非常有利于提高系统的 EMI 性能。当然如果要输出较高频率的信号,但却选用了较低频率的驱动模块,很可能会得到失真的输出信号。

举个栗子:

1、USART串口,若最大波特率只需115.2k,那用2M的速度就够了,既省电也噪声小。

2、I2C 接口,若使用400k波特率,若想把余量留大些,可以选用10M的GPIO引脚速度。

3、SPI 接口,若使用18M或9M波特率,需要选用50M的GPIO的引脚速度。

配置输出引脚

编写业务代码

3.3 初始化及重置相关

//初始化引脚
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);
//重置引脚
void HAL_GPIO_DeInit(GPIO_TypeDef *GPIOx, uint32_t GPIO_Pin);

3.4 IO 口操作相关

//读取电平状态
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
//设置引脚状态
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState
PinState)
;
//转换引脚状态
void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
//锁定引脚状态
HAL_StatusTypeDef HAL_GPIO_LockPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

同时 HAL 库帮我定义好了GPIO_PIN_RESETGPIO_PIN_SET,代表着 1(高电平)、0(低电平)。

User Label

对于任意引脚,它都有这么一个选项。我想告诉你这个选项特别特别好用!这个选项简单的说就是它帮你在main.h中生成define语句。但是对于HAL库编程,main.h会被用户的每个模块调用,也就是这些define语句的作用域几乎是全局。

举个例子让你感受一下,在一次开发中,我使用 PA0 来作为输出引脚。如果随着开发的继续 PA0 被迫要用于其他功能,那么你该怎么办?那你必须使用另外一个引脚(假设是 PB1)来替代它。如果你没有配置User Label 选项,那你的代码中可能大量的充斥着

HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);//将PA0引脚状态改为低电平
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);//将PA0引脚状态改为高电平

然后你又需要用PB1来代替PA0,那你就需要将整个代码中有关PA0的GPIOA改成GPIOB,将 GPIO_PIN_0 改成GPIO_PIN_1。这会导致巨大的工作量,并且容易出错。

那么我们来看看使用了UserLabel会带来什么变化,使用UserLabel把他取名R1。那你的代码中充斥着的不在是HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_RESET),而是 HAL_GPIO_WritePin(R1_GPIO_Port, R1_Pin, GPIO_PIN_RESET)。当遇到 PA0 被迫要用于其他功能,你只需要把PB1的User Label取名为R1后,代码不需要做丝毫改变。

在我的开发中,这个应用最典型的两个例子就是矩阵键盘ADS1256的开发。用矩阵键盘来举例,需要用到 8 个引脚。

Cube MX的配置

我的矩阵键盘中的代码全是由 R1-R4、C1-C4 组成,所以在各这个代码的复用性极其强,无论是换引脚还是换单片机型号,我只需要在Cube MX中配置一下,就可以马上投入使用。

矩阵键盘代码截图

二、串口通信

串口通信(Serial Communications)的概念非常简单,串口按位(bit)发送和接收字节。

2.1 UART 与 USART

UART: 通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),通常称作UART。它将要传输的资料在串行通信与并行通信之间加以转换。作为把并行输入信号转成串行输出信号的芯片,UART 通常被集成于其他通讯接口的连结上

USART:(Universal Synchronous/Asynchronous Receiver/Transmitter) 通用同步/异步串行接收/发送器,USART 是一个全双工通用同步/异步串行收发模块,该接口是一个高度灵活的串行通信设备

2.2 Cube MX 相关配置

2.1 初始化引脚

Mode :

Asynchronous : 异步, 整个过程,不会阻碍发送者的工作。Synchronous : 同步, 同步信息一旦发送,发送者必须等到应答,才能继续后续的行为。Single Wire : 单总线, 半双工。

使能引脚

2.2 配置引脚

Baud Rate: 波特率, 波特率表示每秒钟传送的码元符号的个数,是衡量数据传送速率的指标,它用单位时间内载波调制状态改变的次数来表示。对于串口最重要的就是波特率, 常用的波特率为 115200 与 9600。

Wrod Length:数据长

Parity:奇偶校验->无、奇校验、偶校验

Stop:停止位

以上的配置与需要通信双方完全配对。

配置引脚

2.3 编写逻辑代码

//发送数据
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData,
uint16_t Size, uint32_t Timeout)
;
//接收数据
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData,
uint16_t Size, uint32_t Timeout)
;
//发送中断
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *
pData, uint16_t Size)
;
//接收中断
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData
, uint16_t Size)
;
//使用DMA发送
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *
pData, uint16_t Size)
;
//使用DMA接收
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *
pData, uint16_t Size)
;
//DMA暂停
HAL_StatusTypeDef HAL_UART_DMAPause(UART_HandleTypeDef *huart);
//DMA恢复
HAL_StatusTypeDef HAL_UART_DMAResume(UART_HandleTypeDef *huart);
//DMA停止
HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart);

就我目前的学习来看HAL并没有对同步通信的方式做拓展,所以上述都是关于UART的函数

2.4 printf 重定向

在 Private includes 中引入:

#include <stdio.h>

在 USER CODE BEGIN 0 添加:

int fputc(int ch, FILE *f)
{
 uint8_t temp[1] = {ch};
 HAL_UART_Transmit(&huart1, temp, 12);//huart1需要根据你的配置修改
 return ch;
}

然后你就可以在任意地方使用printf语句方便的输出你想要的内容。

2.5 Log 信息格式

格式1

参考目前主流嵌入式、安卓等输出方式:

[日志级别] 文件名 : 日志信息
//例:[info] main.c : init ok!
//例: [debug] adc.c : adc_getvalue -> 3.3v

格式2

参考 Java 日志框架的输出方式:

[ 文件名] 日志级别 : 日志信息
//例:[ main] info : init ok!
//例: [ adc] debug : adc_getvalue -> 3.3v

下面截选mppt算法中条件编译的使用:

条件编译在代码中的使用

2.6 可变参数宏

关于这个内容,是我在阅读国内某云物联网模块源码是发现并学习的。

源码学习

我觉得这个解决方案比之前提到的条件编译强100倍,甚至让我感觉到以前的做法多么的愚蠢。这种方法不仅达到了代码的格式化,同时也完成了条件编译。

在此分享我的设计:

#ifdef USER_MAIN_DEBUG
#define user_main_printf(format, ...) printf( format "\r\n", ##__VA_ARGS__)
#define user_main_info(format, ...) printf("[\tmain]info:" format "\r\n", ##
__VA_ARGS__)
#define user_main_debug(format, ...) printf("[\tmain]debug:" format "\r\n", ##
__VA_ARGS__)
#define user_main_error(format, ...) printf("[\tmain]error:" format "\r\n",##
__VA_ARGS__)
#else
#define user_main_printf(format, ...)
#define user_main_info(format, ...)
#define user_main_debug(format, ...)
#define user_main_error(format, ...)
#endif

当我需要打印串口信息的时候,define 一个USER_MAIN_DEBUG, 在我不需要时将其注释。

2.7 串口中断

1、Cube MX 中开启中断

开启中断

2、在 USER CODE BEGIN 2 中打开串口中断

HAL_UART_Receive_IT(&huart1, temp, 1);

3、在 USER CODE BEGIN 4 中实现回调函数

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) 
{
 if(huart -> Instance == huart1.Instance ) 
 {
  ...//业务代码
 }
}

三、外部中断

外部中断是单片机实时地处理外部事件的一种内部机制。当某种外部事件发生时,单片机的中断系统将迫使 CPU 暂停正在执行的程序,转而去进行中断事件的处理;中断处理完毕后.又返回被中断的程序处,继续执行下去。

3.1 Cube MX 相关配置

3.1.1 初始化引脚

如果你想使用PA1作为外部中断的接收引脚,那么你只需要点击PA1,在点击它对应的GPIO_EXTIx

3.1.2 使能中断

3.1.3 配置引脚

这个地方与此前不同的地方在于 GPIO mode。

External Interrupt Mode with Rising edge trigger detection//上升沿触发
External Interrupt Mode with Falling edge trigger detection//下降沿触发
External Interrupt Mode with Rising/Falling edge trigger detection//上升沿或下降沿触发
配置引脚

3.2 编写逻辑代码

main.c 中的USER CODE BEGIN 4 编程范围内添加外部中断的回调函数:

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) 
{
 if(GPIO_Pin == PWM_Pin) 
 {
  ...//业务代码
 }
}

3.3 测量 pwm 频率

在我平时的学习中没有太多的使用外部中断,但是在最后的电赛中却巧妙的使用了它。

当时的情况是我们需要测量一个 PWM 的频率,我的解决办法是这样的:

当有上升沿的时候,就进入外部中断将pwm_value的值 +1。it is clear that "1s 钟上升沿的次数就是 pwm 的频率"。所以当我要用 pwm 的频率时,我就先将 pwm_value 置 0,再延时1s,最后再使用 pwm_value。当然这并不是我最终的代码,因为你读到这里还有很多的内容没有学习, 往后的定时器章节将介绍它的滤波算法

int pwm_value =0 ;
int main()
{
 while (1)
 {
  pwm_value = 0// pwm_value置0
  HAL_Delay(1000); // 延时1s
  printf("[\tmain]info:pwm_value=%d\r\n",pwm_value); // 读取pwm_value
 }
}
/**
* @brief 外部中断的回调函数
* @param GPIO_Pin 触发中断的引脚
* @retval None
*/

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) 
{
 if(GPIO_Pin == PWM_Pin) 
 { 
  // 判断触发引脚是否是定义的引脚
  pwm_value++;
 }
}

四、 时钟树

说到STM32,必然逃不开时钟树。但是时钟树要展开讲的话会很麻烦,而且我也不一定讲的好。但是我想告诉你的是:通常我们会让单片机的频率(决定单片机的处理速度)提到最大,再进行其他分频操作。

4.1 使能外部时钟源

使能外部时钟源

4.2 将频率调至最大

不同单片机的最大运行频率是不同的,例如stm32f10372Mstm32f40784M

将频率调至最大

4.3 按需分频

按需分频

五、定时器

分享完时钟树的部分,接下来就是和它最紧密的定时器了。定时器最基本的内容就是定时产生中断了:

5.1 Cube MX 相关配置

5.1.1 配置定时器时钟

如之前所示,将定时器的时钟设为72M

配置定时器频率

5.1.2 选择时钟源

选择内部时钟

选择时钟源

5.1.3 配置定时器

定时器的配置主要有两个:定时时间与是否重装定时器

定时频率=定时器时钟/(预分频+1)/(计数值+1)Hz

定时时间=1/定时频率 s

配置定时器

5.1.4 开启中断 - 基本定时器

勾选 Enabled 框即可。

开启中断

5.1.5 开启中断 - 高级定时器

勾选TIM X update interrupt 后的Enabled框即可。

开启中断

5.2 编写业务代码

int main()
{
 HAL_TIM_Base_Start_IT(&htim1); //定时器1使能
 HAL_TIM_Base_Start_IT(&htim2); //定时器2使能
 ...
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) 
{
 if (htim->Instance == htim1.Instance) 
 {
  ...//定时器1中断业务
 }
 else if(htim-> Instance == htim2.Instance) 
 {
  ...//定时器2中断业务
 }
 ...
}

5.3 平滑滤波

在这里我想在介绍定时器的另一种用法:平滑滤波。绝大部分人的滤波算法都是用的时候,多次采样再滤波。但是我希望让采样值在另一个线程一直滤波,而在我需要他的时候,直接取它的值即可。之前我描述过用外部中断实现的测量 pwm 波的频率,接下我想分享一下用定时器对其进行滤波。

/* 定时器2配置为0.1s触发一次中断 */
/**
* @brief 定时器中断的回调函数
* @param htim 触发中断的定时器
* @retval None
*/

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
 if(htim-> Instance == htim2.Instance) 
 {
  pwm_sum += pwm_value * 10//pwm_sum累加
  pwm_sum -= pwm_avg; //pwm_sum减去上次的平均值
  pwm_avg = pwm_sum * 1.0 / 5//更新pwm的平均值
  pwm_value_final = pwm_avg; //pwm_value_final的值即为当前pwm的频率
  pwm_value = 0//将pwm_value清空,重新计数
 }
}
/**
* @brief 外部中断的回调函数
* @param GPIO_Pin 触发中断的引脚
* @retval None
*/

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) 
{
 if(GPIO_Pin == PWM_Pin) 
 { 
  // 判断触发引脚是否是定义的引脚
  pwm_value++;
 }
}

当我们在任意时刻需要使用pwm的频率时,只需要使用pwm_value_final的值即可。

最后给大家推荐一个宝藏网站:

www.cxy521.com

程序员我爱你


还有程序员相亲专栏哟!



End

推荐好文  点击蓝色字体即可跳转

【收藏】烂大街的ESP82666该咋玩
☞【鸡汤】一个普通人的大学四年
☞【干货】基于STM32的PS2遥控小车
☞【干货】手把手教你写一个安卓APP
☞【干货】手把手教你写单片机的指针

☞【干货】手把手教你写单片机的结构体

欢迎转发、留言、点赞、分享,感谢您的支持!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存