摘要:如果你学过STC51,你一定知道STC51操作是极其方便的。如果你学过STM32的库函数,你一定知道STM32操作是极其繁琐的。传统的库函数开发方式,将太多时间花费在各种东西的初始化上。同时,如果你学过STM32F1、STM32F3、STM32F4的话,你会发现对于不同型号的STM32在使用库函数的开发方式下,他的初始化流程也是不一样的,这也是传统开发方式的一种弊端。而CubeMX + HAL库开发的方式,则是省去了初始化的部分,让开发人员将更多的精力放在业务的处理!但是寄存器及库函数的开发方式也是有必要学习的,因为CubeMX也可能存在 Bug, 如果你对寄存器及库函数不了解那你会很被动。
本文主要讲解GPIO、串口通信、外部中断、时钟树、定时器五个内容,关于STM32标准库开发已经在前面讲过了,需要的小伙伴可以到前面看看。本文将介绍CubeMX + HAL库开发需要注意的部分,关于具体的细节部分不做介绍。
一、GPIOGPIO(英语: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_RESET
与GPIO_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, 1, 2);//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 将频率调至最大
不同单片机的最大运行频率是不同的,例如stm32f103
为72M
而stm32f407
为84M
。
将频率调至最大
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
程序员我爱你
还有程序员相亲专栏哟!