STM32之HAL库开发(一)

HAL 库

STM32CubeMX

  • 简介及下载

    在 CubeMX 上,通过傻瓜化的操作便能实现相关配置,最终能够生成 C 语言代码,支持多种工具链,比如 MDK、IAR For ARM、TrueStudio 等 省去了我们配置各种外设的时间,大大的节省了时间。

    在蓝桥杯中,可以通过使用 STM32CubeMX 实现 HAL 库,达成快速编写代码的效果。

    TM32CubeMX 的使用需要 JAVA 支持。

  • 安装 HAL 库

    • 在线安装

      打开安装好的 STM32CubeMX 软件 点上面的 Help –> Manage embedded software packages

      1

      会跳出来一个选择型号界面 勾选上你要安装的 HAL 库, 点击“Install Now” 直到安装成功。 如下图:

      2

    • 离线安装(需掌握)

      蓝桥杯中若主办方提供的固件包有误,需使用离线安装。

      直接导入安装包 Help –> Manage embedded software packages –> From Local 选择离线包即可

      3

  • 新建工程方式

    在主界面选择 File –> New Project 或者直接点击 ACCEE TO MCU SELECTOR

    直接搜索型号

    4

    通过可视化界面快速设置引脚,无需进行枯燥的代码编写

    5

    淡黄色表示不可配置引脚 电源专用引脚以黄色突出显示。其配置不能更改

    6

    深黄色表示你配置了一个 I/O 口的功能,但是没有初始化相对应的外设功能 引脚处于 no mode 状态

    7

    绿色表示配置成功

    8

    点击 System Core –> SYS –> Debug 将其使能为 Serial Wrire,否则无法使用 STLink 烧录代码

    11

    接下来需要配置时钟树:

    点击 System Core –> RCC –> HSE 将其使能,否则无法使用外部高速时钟

    9

    点击 Clock Configuration 切换至时钟树页面进行配置

    其中需要将系统时间配平为 72MHZ , APB1 为 TIM2 - 4、5(普通时钟),APB2 为 TIM1\8 (高级时钟)

    10

    点击 Project Manager 进行工程文件设置

    其中 Toolchain / IDE 需要设置为 MDK-ARM,存储目录不能有中文

    12

    然后点击 Code Generator,进行进一步配置:

    :选择 不复制文件,直接从软件包存放位置导入 .C 和 .H 文件 的话,当工程复制到其他电脑上或者软件包位置改变,就需要修改相对应的路径

    13

    然后点击 GENERATE CODE 创建工程

    所有自己编写的代码请放在/_ USER CODE BEGIN XXX _//_ USER CODE END XXX _/之间

    这样我们修改工程的时候你自己写的代码就不会被删除

    14

GPIO

GPIO 工作模式

  • 输入模式
    GPIO_Mode_IN_FLOATING 浮空输入
    GPIO_Mode_IPU 上拉输入
    GPIO_Mode_IPD 下拉输入
    GPIO_Mode_AIN 模拟输入

  • 输出模式
    GPIO_Mode_Out_OD 开漏输出(带上拉或者下拉)
    GPIO_Mode_AF_OD 复用开漏输出(带上拉或者下拉)
    GPIO_Mode_Out_PP 推挽输出(带上拉或者下拉)
    GPIO_Mode_AF_PP 复用推挽输出(带上拉或者下拉)

  • 输出速度
    2MHZ 低速
    25MHZ 中速
    50MHZ 快速
    100MHZ 高速

GPIO 工作模式含义

  • 浮空输入
    I/O 端口的电平信号直接进入输入数据寄存器。MCU 直接读取 I/O 口电平,I/O 的电平状态是不确定的,完全由外部输入决定;如果在该引脚悬空(在无信号输入)的情况下,读取该端口的电平是不确定的。

  • 上拉输入
    IO 内部接上拉电阻,此时如果 IO 口外部没有信号输入或者引脚悬空,IO 口默认为高电平 如果 I/O 口输入低电平,那么引脚就为低电平,MCU 读取到的就是低电平

    STM32 的内部上拉是”弱上拉“,即通过此上拉输出的电流是很弱的,如要求大电流还是需要外部上拉

  • 下拉输入
    IO 内部接下拉电阻,此时如果 IO 口外部没有信号输入或者引脚悬空,IO 口默认为低电平 如果 I/O 口输入高电平,那么引脚就为高电平,MCU 读取到的就是高电平

  • 模拟输入
    当 GPIO 引脚用于 ADC 采集电压的输入通道时,用作”模拟输入”功能,此时信号不经过施密特触发器,直接直接进入 ADC 模块,并且输入数据寄存器为空 ,CPU 不能在输入数据寄存器上读到引脚状态

    当 GPIO 用于模拟功能时,引脚的上、下拉电阻是不起作用的,这个时候即使配置了上拉或下拉模式,也不会影响到模拟信号的输入输出

    除了 ADC 和 DAC 要将 IO 配置为模拟通道之外其他外设功能一律 要配置为复用功能模式,

  • 开漏输出
    若控制输出为 0 (低电平),I/O 端口的电平就是低电平。

    若控制输出为 1 (高电平),此时 I/O 端口的电平就不会由输出的高电平决定,而是由 I/O 端口外部的上拉或者下拉决定 如果没有上拉或者下拉 IO 口就处于悬空状态

  • 推挽输出
    若控制输出为 0 (低电平),I/O 端口的电平就是低电平。

    若控制输出为 1 (高电平),I/O 端口的电平就是高电平。外部上拉和下拉的作用是控制在没有输出时 IO 口电平
    此时施密特触发器是打开的,即输入可用。

  • 复用开漏输出
    GPIO 复用为其他外设,输出数据寄存器 GPIOx_ODR 无效;
    输出的高低电平的来源于其它外设,施密特触发器打开,输入可用,通过输入数据寄存器可获取 I/O 实际状态 除了输出信号的来源改变 其他与开漏输出功能相同

  • 复用推挽输出
    GPIO 复用为其他外设(如 I2C),输出数据寄存器 GPIOx_ODR 无效;
    输出的高低电平的来源于其它外设,施密特触发器打开,输入可用,通过输入数据寄存器可获取 I/O 实际状态 除了输出信号的来源改变 其他与开漏输出功能相同

HAL 库 GPIO 函数库

stm32f4xx_hal_gpio.h中 一共定义有 8 个函数

1
2
3
4
5
6
7
8
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);
void HAL_GPIO_DeInit(GPIO_TypeDef *GPIOx, uint32_t GPIO_Pin);
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);
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin);
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin);
  • GPIO 初始化

    void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);

    示例:HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

  • 在函数初始化之后的引脚恢复成默认的状态,即各个寄存器复位时的值

    void HAL_GPIO_DeInit(GPIO_TypeDef *GPIOx, uint32_t GPIO_Pin);

    示例:HAL_GPIO_DeInit(GPIOC, GPIO_PIN_4);

  • 读取引脚的电平状态、函数返回值为 0 或 1

    GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

    示例:HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_4)

  • 引脚电平写 0 或 1

    void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);

    示例:HAL_GPIO_WritePin(GPIOC, GPIO_PIN_4,0);

  • 翻转引脚的电平状态

    void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

    示例:HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_4); 常用在 LED 上

  • 锁住引脚电平

    比如说一个管脚的当前状态是 1,当这个管脚电平变化时保持锁定时的值。

    HAL_StatusTypeDef HAL_GPIO_LockPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

    示例:HAL_GPIO_LockPin(GPIOC, GPIO_PIN_4);

  • 外部中断服务函数,清除中断标志位

    void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin);

    示例:HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_4);

  • 中断回调函数

    void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin);

    示例:HAL_GPIO_EXTI_Callback(GPIO_PIN_4);

  • 按键消抖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
    if(GPIO_Pin==KEY1_GPIO_PIN){

    if(HAL_GPIO_ReadPin(KEY1_GPIO,KEY1_GPIO_PIN)==KEY1_DOWN_LEVEL){
    BEEP_TOGGLE;
    LED1_ON;
    LED2_ON;
    LED3_ON;
    }

    __HAL_GPIO_EXTI_CLEAR_IT(KEY1_GPIO_PIN);
    }
    else if(GPIO_Pin==KEY2_GPIO_PIN){

    if(HAL_GPIO_ReadPin(KEY2_GPIO,KEY2_GPIO_PIN)==KEY2_DOWN_LEVEL){
    BEEP_TOGGLE;
    LED1_OFF;
    LED2_OFF;
    LED3_OFF;
    }

    __HAL_GPIO_EXTI_CLEAR_IT(KEY2_GPIO_PIN);
    }
    }

UART

STM32CubeMX 设置

点击 USATR1 –> 设置 MODE 为 Asynchronous (异步通信) –> 设置波特率 –> 设置 GPIO 引脚 (TX\RX) –> 使能中断

16

17

HAL 库 UART 函数库

UART 结构体定义:UART_HandleTypeDef huart1;

15

  • 串口发送/接收函数

    1
    2
    3
    4
    5
    6
    HAL_UART_Transmit();          //串口发送数据,使用超时管理机制
    HAL_UART_Receive(); //串口接收数据,使用超时管理机制
    HAL_UART_Transmit_IT() //串口中断模式发送
    HAL_UART_Receive_IT(); //串口中断模式接收
    HAL_UART_Transmit_DMA(); //串口DMA模式发送
    HAL_UART_Transmit_DMA(); //串口DMA模式接收
  • 发送数据

    如果超时没发送完成,则不再发送,返回超时标志(HAL_TIMEOUT)

    HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)

    UART_HandleTypeDef *huart UATR 的别名 (如: UART_HandleTypeDef huart1; 别名就是 huart1)

    *pData 需要发送的数据

    Size 发送的字节数

    Timeout 最大发送时间 (超时时间)

    示例:

    1
    HAL_UART_Transmit(&huart1, (uint8_t *)ZZX, 3, 0xffff);   //串口发送三个字节数据,最大传输时间0xffff
  • 中断接收数据

    串口中断接收,以中断方式接收指定长度数据。

    HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)

    UART_HandleTypeDef *huart UATR 的别名 (如: UART_HandleTypeDef huart1; 别名就是 huart1)

    *pData 需要发送的数据

    Size 发送的字节数

    示例:

    1
    HAL_UART_Receive_IT(&huart1,(uint8_t *)&value,1);   //中断接收一个字符,存储到value中
  • 串口中断函数

    1
    2
    3
    4
    5
    6
    HAL_UART_IRQHandler(UART_HandleTypeDef *huart);             //串口中断处理函数
    HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart); //串口发送中断回调函数
    HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart); //串口发送一半中断回调函数(用的较少)
    HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart); //串口接收中断回调函数
    HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart); //串口接收一半回调函数(用的较少)
    HAL_UART_ErrorCallback(); //串口接收错误函数
  • 中断回调函数

    串口中断接收完成之后,会进入该函数,该函数为空函数,用户需自行修改

    HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);

    UART_HandleTypeDef *huart UATR 的别名 (如: UART_HandleTypeDef huart1; 别名就是 huart1)

  • 中断处理函数

    对接收到的数据进行判断和处理 判断是发送中断还是接收中断,然后进行数据的发送和接收

    HAL_UART_IRQHandler(UART_HandleTypeDef *huart);

    如果接收数据,则会进行接收中断处理函数:

    1
    2
    3
    4
    if((tmp_flag != RESET) && (tmp_it_source != RESET))
    {
    UART_Receive_IT(huart);
    }

    如果发送数据,则会进行发送中断处理函数:

    1
    2
    3
    4
    5
    if (((isrflags & USART_SR_TXE) != RESET) && ((cr1its & USART_CR1_TXEIE) != RESET))
    {
    UART_Transmit_IT(huart);
    return;
    }
  • 查询函数

    HAL_UART_GetState(); 判断 UART 的接收是否结束,或者发送数据是否忙碌

    示例:

    1
    while(HAL_UART_GetState(&huart4) == HAL_UART_STATE_BUSY_TX)    //检测 UART 发送结束

HAL 库重定义 printf 函数

stm32f4xx_hal.c 中包含 #include <stdio.h>

1
2
3
#include "stm32f4xx_hal.h"
#include <stdio.h>
extern UART_HandleTypeDef huart1; //声明串口

stm32f4xx_hal.c 中重写 fgetfput 函数

1
2
3
4
5
6
7
8
9
10
11
12
13

int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}

int fgetc(FILE *f)
{
uint8_t ch = 0;
HAL_UART_Receive(&huart1, &ch, 1, 0xffff);
return ch;
}

main.c 中添加

1
2
 #define RXBUFFERSIZE  256
char RxBuffer[RXBUFFERSIZE];
1
2
3
4
5
6
7
8

while (1)
{
/* USER CODE END WHILE */
printf("123456\n");
HAL_Delay(1000);
/* USER CODE BEGIN 3 */
}

之后便可以使用 Printf 函数和 Scanf,getchar 函数

HAL 库 UART 接收中断

在 main.c 中添加下列定义

1
2
3
4
5
6
#include <string.h>

#define RXBUFFERSIZE 256 //最大接收字节数
char RxBuffer[RXBUFFERSIZE]; //接收数据
uint8_t aRxBuffer; //接收中断缓冲
uint8_t Uart1_Rx_Cnt = 0; //接收缓冲计数

在 main()主函数中,调用一次接收中断函数

1
2
3
/* USER CODE BEGIN 2 */
HAL_UART_Receive_IT(&huart1, (uint8_t *)&aRxBuffer, 1);
/* USER CODE END 2 */

在 main.c 下方添加中断回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* USER CODE BEGIN 4 */

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(huart);
/* NOTE: This function Should not be modified, when the callback is needed,
the HAL_UART_TxCpltCallback could be implemented in the user file
*/

if(Uart1_Rx_Cnt >= 255) //溢出判断
{
Uart1_Rx_Cnt = 0;
memset(RxBuffer,0x00,sizeof(RxBuffer));
HAL_UART_Transmit(&huart1, (uint8_t *)"数据溢出", 10,0xFFFF);

}
else
{
RxBuffer[Uart1_Rx_Cnt++] = aRxBuffer; //接收数据转存

if((RxBuffer[Uart1_Rx_Cnt-1] == 0x0A)&&(RxBuffer[Uart1_Rx_Cnt-2] == 0x0D)) //判断结束位
{
HAL_UART_Transmit(&huart1, (uint8_t *)&RxBuffer, Uart1_Rx_Cnt,0xFFFF); //将收到的信息发送出去
while(HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_TX);//检测UART发送结束
Uart1_Rx_Cnt = 0;
memset(RxBuffer,0x00,sizeof(RxBuffer)); //清空数组
}
}

HAL_UART_Receive_IT(&huart1, (uint8_t *)&aRxBuffer, 1); //再开启接收中断
}
/* USER CODE END 4 */

随后便可在 XCOM(或其他串口助手) 中接收到发送的数据。

HAL 库 UART 接收帧头帧尾

在 main.c 中添加下列定义

1
2
3
4
5
#include <string.h>

#define RXBUFFERSIZE 256
extern uint8_t aRxBuffer;
extern char RxBuffer[RXBUFFERSIZE];

在 usart.c 中添加下列定义

1
2
3
4
5
#define RXBUFFERSIZE 256
char RxBuffer[RXBUFFERSIZE];
uint8_t Uart_Rx_Cnt = 0;
uint8_t aRxBuffer;
uint8_t flag = 0;

在 main() 主函数中,调用一次接收中断函数

1
2
3
/* USER CODE BEGIN 2 */
HAL_UART_Receive_IT(&huart1, (uint8_t *)&aRxBuffer, 1);
/* USER CODE END 2 */

在 usart.c 中重定义中断接收函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){

if(flag == 0 && aRxBuffer == '@'){
flag++;
} // 帧头

else if(flag == 1){
if(aRxBuffer == '#'){
flag++;
} // 帧尾

else{
RxBuffer[Uart_Rx_Cnt++] = aRxBuffer;
}
}

else if(flag == 2){
if(aRxBuffer == '&'){
flag = 0;
RxBuffer[Uart_Rx_Cnt] = '\n';
Uart_Rx_Cnt = 0;
} // 帧尾

else{
flag = 0;
Uart_Rx_Cnt = 0;
memset(RxBuffer, 0, sizeof(RxBuffer));
}
}

HAL_UART_Receive_IT(&huart1, (uint8_t *)&aRxBuffer, 1);

}

PWM

工作原理

  • SMT32F1 系列共有 8 个定时器:

    高级定时器:TIM1、TIM8 (APB2) (可同时产生 7 路输出)

    通用定时器:TIM2 - TIM5 (APB1)

    基本定时器:TIM6、TIM7 (不可以产生 PWM 输出)

18

在 PWM 输出模式下,除了 CNT(计数器当前值)、ARR(自动重装载值)之外,还多了一个值 CCRx(捕获/比较寄存器值)

当 CNT 小于 CCRx 时,TIMx_CHx 通道输出低电平

当 CNT 等于或大于 CCRx 时,TIMx_CHx 通道输出高电平

TIMx_ARR 寄存器确定 PWM 频率

TIMx_CCRx 寄存器确定占空比

STM32CubeMX 设置

  • 设置 RCC

    设置高速外部时钟 HSE 选择外部时钟源

    9

  • 设置定时器

    Channel1~4 就是设置定时器通道的功能 (输入捕获、输出比较、PWM 输出、单脉冲模式)

    19

    Mode 选择 PWM 模式 1
    Pulse (占空比值) 先给 0
    Fast Mode PWM 脉冲快速模式 : 和我们配置无关,不使能
    PWM 极性: 设置为低电平
    : 由于 LED 是低电平点亮,所以我们把极性设置为 low

    20

  • PWM 频率

    Fpwm =Tclk / ((arr+1)*(psc+1))(单位:Hz)

    arr 是计数器值
    psc 是预分频值

  • 占空比

    duty circle = TIM3->CCR1 / arr(单位:%)

    TIM3->CCR1 用户设定值

比如:定时器频率 Tclk = 72MHZ arr=499 psc=71
那么 PWM 频率就是 720000 / 500 / 72= 2000Hz,即 2KHz
arr = 499 ,TIM3 -> CCR = 250 则 pwm 的占空比为 50%

改 CCR1 可以修改占空比,修改 arr 可以修改频率

  • 案例:

    使能 TIM3 的 PWM Channel1 输出:

    1
    2
    3
    /* USER CODE BEGIN 2 */
    HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_1);
    /* USER CODE END 2 */

    修改占空比:

    1
    __HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_1, pwmVal);

    其中, &htim3 为 TIM3, TIM_CHANNEL_1 为 PWM Channel1, pwmVal 为占空比,

    由于 arr = 499, 则最大占空比为(499 + 1), 即 500 为 100%。

    直接将 LED 接入 PWM, 则占空比表示灯泡的亮度, 占空比越大, LED 越亮。