STM32F103RCT6V4学习历程(持续更新ing)
目录
前言
一、GPIO_LED(STM32开发的基本流程)
二、按键KEY
三、外部中断实验
四、串口通信实验
五、基本定时器
前言
为了促进知识增长,在此处记录STM32F103RCT6V4微控制器自学历程。涵盖各个模块的开发路径及共性问题,并参考开发手册及官方源码自行重写代码(可能存在警告信息)。持续丰富内容并不断更新中,主要目标是帮助快速回顾STM32开发流程,并以便更快地掌握开发方法并独立实现项目。
一、GPIO_LED(STM32开发的基本流程)
GPIO头文件基本流程:
头文件的功能在于为程序实现硬件驱动支持,并在xxx.c中实现必要的宏定义与库函数引用。
具体步骤如下:
- 重新配置引脚所连接的端口号码;
- 配置GPIO总线的时钟周期使其启用;
- 设置GPIO引脚的控制功能;
- 完成对led.c中LED照明功能模块的具体初始化设置
/1、另定义端口和引脚,如:
#define LED0_PORT GPIOA
#define LED0_PIN GPIO_PIN_8
包括一些函数, 都尽量重新命名它们, 这样做有两个好处: 其一, 可以让后续代码编写得更加直观; 其二, 可以避免直接调用库中的函数, 这样在某些特殊语句(如结构体中)就不会导致语法错误
#define LED0_clk_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); } while(0) // 时钟使能
时钟使能函数在 stm32f1xx_hal_rcc.h 文件中
直接使用库函数出错: GPIO_InitTypeDef gpio_init_type ;
// GPIO_InitTypeDef 被认为是一个用于描述 GPIO 结构体的形式化名称,在包含的头文件中定义即可。
// 该结构体不可以直接引用。
// 必须声明该结构体。
// 如果未声明 'gpio_init_type' 变量,则会引发错误。
除了在别处外定义函数之外
/3、
#define LED0(x) do{x ? HAL_GPIO_WritePin(LED0_PORT,LED0_PIN,GPIO_PIN_SET):\
HAL_GPIO_WritePin(LED0_PORT,LED0_PIN,GPIO_PIN_RESET);}while(0)
//控制LED开闭的函数
/4、
void led_INIT(void);
GPIO的led.c文件基本流程:
xxx.c 文件的作用就是给main()函数提供函数,如led的初始化函数
GPIO时钟启用(直接调用库函数)而不建议在头文件中定义(容易导致错误)
LED0_clk_ENABLE();
LED1_clk_ENABLE();
2、初始化GPIO口
如:
GPIO_InitTypeDef gpio_init_type;
//另行定义结构体gpio_init_type,其结构体类型为GPIO_InitTypeDef,根据GPIO_InitTypeDef中的元素依此填入
另设一结构体 gpio_init_type ,其数据类型为 GPIO.InitTypeDef ,遵循 GPIO.InitTypeDef 中的各项元素依次填充
HAL_GPIO_Init(LED0_PORT, &gpio_init_type); //GPIO初始化
确定初始化的GPIO引脚地址
3、关灯(led初始化为熄灭状态)
main()文件的具体流程
1、初始化HAL库
如:
HAL_Init();
2、设置时钟,延时
如:
sys_stm32_clock_init(RCC_PLL_MUL9);
delay_init(72);
3、初始化led
4.执行函数
代码全貌:
led.h
/*定义引脚和使能*/
#ifndef _led_h
#define _led_h
#include ".\SYSTEM\sys\sys.h"
#define LED0_PORT GPIOA
#define LED0_PIN GPIO_PIN_8
#define LED0_clk_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); } while(0)
#define LED1_PORT GPIOD
#define LED1_PIN GPIO_PIN_2
#define LED1_clk_ENABLE() do{ __HAL_RCC_GPIOD_CLK_ENABLE(); } while(0)
/*引脚定义完了,接下来需要定义什么时候led亮和灭*/
#define LED0(x) do{x ? HAL_GPIO_WritePin(LED0_PORT,LED0_PIN,GPIO_PIN_SET):\
HAL_GPIO_WritePin(LED0_PORT,LED0_PIN,GPIO_PIN_RESET);}while(0)
#define LED1(x) do{x ? HAL_GPIO_WritePin(LED1_PORT,LED1_PIN,GPIO_PIN_SET):\
HAL_GPIO_WritePin(LED1_PORT,LED1_PIN,GPIO_PIN_RESET);}while(0)
/*定义x为1时亮(GPIO_PIN_SET)否则灭*/
/*定义LED取反函数*/
#define LED0_Toggle() do{HAL_GPIO_TogglePin(LED0_PORT,LED0_PIN);}while(0)
#define LED1_Toggle() do{HAL_GPIO_TogglePin(LED1_PORT,LED1_PIN);}while(0)
void led_INIT(void);
#endif
led.c
#include "./BSP/led.h"
//定义LED初始化函数
void led_INIT(void)
{
//使能GPIO口
LED0_clk_ENABLE();
LED1_clk_ENABLE();
GPIO_InitTypeDef gpio_init_type; //GPIO_InitTypeDef只是头文件定义的一个GPIO的结构体,不能直接用,需要定义结构体变量
//GPIO初始化
gpio_init_type.Pin = LED0_PIN; //引脚口为LED0的引脚,即8号引脚
gpio_init_type.Mode = GPIO_MODE_OUTPUT_PP; //模式设置为推挽输出
gpio_init_type.Pull = GPIO_PULLUP; //设置为上拉电阻
gpio_init_type.Speed = GPIO_SPEED_FREQ_HIGH; //设置为高速
HAL_GPIO_Init(LED0_PORT, &gpio_init_type); //GPIO初始化
gpio_init_type.Pin = LED1_PIN;
HAL_GPIO_Init(LED1_PORT, &gpio_init_type);//这里是对HAL_GPIO_Init的LED_PORT赋值,所以必须先给LED0附上,再把gpio_init_type.Pin的值改为LED1
LED0(1); //关闭LED
LED1(1); //关闭LED
}
main.c
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/led.h"
int main(void)
{
HAL_Init(); //初始化HAL库
sys_stm32_clock_init(RCC_PLL_MUL9); //设置时钟72Mhz
delay_init(72); //设置延时倍数为72/8=9
led_INIT(); //初始化led
while(1)
{
LED0(0);
LED1(1);//初始设置LED0亮,LED1灭
delay_ms(500); //延时半秒
LED0(1);
LED1(0);
delay_ms(500);
}
}
二、按键KEY
头文件:
HAL_GPIO_ReadPin 函数:用于读取引脚口的电平状态信息
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
定义按键被按下的状态,其余与GPIO无二。
在C文件中存在两个函数:一个是负责普通KEY初始化及执行功能的操作;另一个则是实现消除按键抖动的技术。其主要作用是确保按键不会发生多次触发或误触发的情况。具体而言,在实现这一目标的过程中:首先定义了用于描述按键键值的一组变量(如_key_up_和_keyval_);其次分别判断当前操作是否真正按压了相应的键以及具体是哪个键被按压。这些步骤共同构成了后续程序运行的基础保障机制
uint8_t key_scan(uint8_t Mode)
{
//定义静态变量
static uint8_t key_up = 1; //松开标志
uint8_t keyval = 0; //按键按下种类判断
if (Mode) key_up = 1; /*当mode=1时支持连按,相当于每循环一遍都会将key_up复位为1,
否则,第一下没按完,key_up为0,不会触发后续函数*/
if (key_up && (KEY0 == 0 || KEY1 == 0 || WKUP == 1))//key_up为1,且有任意一个按键按下
{
delay_ms(10);//消抖
key_up = 0;//松开前不会置位
if (KEY0 == 0) keyval = KEY0_tr;
if (KEY1 == 0) keyval = KEY1_tr;
if (WKUP == 1) keyval = WKUP_tr;
}
else if(KEY0 ==1 && KEY1 ==1 && WKUP ==0 )
{
key_up = 1;//没有按键按下,松开标志置位
}
return keyval;
}
代码全貌:
key.h
#ifndef _key_h
#define _key_h
#include ".\SYSTEM\sys\sys.h"
/*定义按键和LED的PORT和引脚 按键:wk_up --> PA0
key1-->PA15
key0-->PC5
*/
#define WKUP_GPIO_PORT GPIOA
#define WKUP_GPIO_PIN GPIO_PIN_0
#define KEY1_GPIO_PORT GPIOA
#define KEY1_GPIO_PIN GPIO_PIN_15
#define KEY0_GPIO_PORT GPIOC
#define KEY0_GPIO_PIN GPIO_PIN_5
/*定义时钟使能*/
//定义C口使能
#define KEY0_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)
//定义A口使能
#define KEY1_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)
/*定义IO口数据读取函数*/
#define KEY0 HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_5)
#define KEY1 HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_15)
#define WKUP HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)
/*定义按键状态*/
#define KEY0_tr 1
#define KEY1_tr 2
#define WKUP_tr 3
void KEY_init(void);
uint8_t key_scan(uint8_t Mode); /* 按键扫描函数 */
#endif
key.c
#include ".\BSP\KEY\key.h"
#include ".\SYSTEM\delay\delay.h"
#include ".\stm32f1xx_it.h"
#include ".\SYSTEM/usart\usart.h"
void KEY_init(void)
{
/*定义初始化GPIO*/
GPIO_InitTypeDef gpio_def_struct;
/*时钟使能*/
KEY0_GPIO_CLK_ENABLE();
KEY1_GPIO_CLK_ENABLE();
/*定义gpio初始特性*/
gpio_def_struct.Pin = KEY0_GPIO_PIN;
gpio_def_struct.Mode = GPIO_MODE_INPUT; //上拉输入
gpio_def_struct.Pull = GPIO_PULLUP; //上拉电阻,KEY0是高有效输入
gpio_def_struct.Speed = GPIO_SPEED_FREQ_HIGH; //高速
HAL_GPIO_Init(KEY0_GPIO_PORT, &gpio_def_struct.Pin); //初始化串口
//key1也是高有效,故其他都一样
gpio_def_struct.Pin = KEY1_GPIO_PIN;
HAL_GPIO_Init(KEY1_GPIO_PORT, &gpio_def_struct.Pin);
//WKUP是低有效,故要改为下拉
gpio_def_struct.Pin = WKUP_GPIO_PIN;
gpio_def_struct.Pull = GPIO_PULLDOWN; //下拉电阻,WKUP是低有效输入
HAL_GPIO_Init(WKUP_GPIO_PORT, &gpio_def_struct.Pin);
//初始化完成
}
//接下来要进行按键消抖
uint8_t key_scan(uint8_t Mode)
{
//定义静态变量
static uint8_t key_up = 1; //松开标志
uint8_t keyval = 0; //按键按下种类判断
if (Mode) key_up = 1; /*当mode=1时支持连按,相当于每循环一遍都会将key_up复位为1,
否则,第一下没按完,key_up为0,不会触发后续函数*/
if (key_up && (KEY0 == 0 || KEY1 == 0 || WKUP == 1))//key_up为1,且有任意一个按键按下
{
delay_ms(10);//消抖
key_up = 0;//松开前不会置位
if (KEY0 == 0) keyval = KEY0_tr;
if (KEY1 == 0) keyval = KEY1_tr;
if (WKUP == 1) keyval = WKUP_tr;
}
else if(KEY0 ==1 && KEY1 ==1 && WKUP ==0 )
{
key_up = 1;//没有按键按下,松开标志置位
}
return keyval;
}
main.c
其中的led函数用的就是上一节所编写的代码
#include ".\SYSTEM\delay\delay.h"
#include ".\SYSTEM\sys\sys.h"
#include ".\BSP\led.h"
#include ".\BSP\KEY\key.h"
int main()
{
uint8_t key;
HAL_Init();//初始化HAL库
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟,72Mhz */
delay_init(72); /* 延时初始化 */
led_INIT(); /* 初始化 LED */
KEY_init(); /* 初始化按键 */
LED0(0); /* 先点亮 LED0 */
while(1)
{
key = key_scan(0); //不支持连按
if(key != 0)
{
switch(key)
{
case KEY0_tr: //按下key0点亮/熄灭LED0
LED0_Toggle();
break;
case KEY1_tr:
LED1_Toggle(); //按下KEY1点亮/熄灭LED0
break;
case WKUP_tr:
LED1(1);
LED0(0); //按下WKUP恢复初始
break;
}
}
else delay_ms(10);
}
}
三、外部中断实验
TM32F103RCT6芯片最多可配置为60个中断处理任务,并规定中断使能寄存器仅占用ISER[0]中的bit 0至31以及ISER[1]中的bit 0至27位。NVIC寄存器的具体定义信息可在 core_cm3.h 文件中找到;相关中断处理功能则详细描述于 stm32f103xe.h 头文件中
ISER:中断启用寄存器。该寄存器仅允许被设置为1时才具有有效功能;而无法通过设置为0来实现其功能;因此,在这种情况下无法通过将该寄存器设置为0来清除中断
ICER: 中断清除寄存器。用于清除中断
IPER:Interrupt Hang Control Register. It can be used to suspend an active interrupt and handle higher or same level interrupts, similar to ISER, writing 0 is ineffective. Thus it is necessary that
ICPR:中断解挂控制寄存器。将挂起的中断解挂
IABR:中断标志位。表示该中断正在使用
IP:中断优先级控制的寄存器
若两者...相同等级,则按自然顺序排列
NVIC寄存器相关的函数在 stm32f1xx_hal_cortex.c
主要用到的NVIC函数为:
void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup); //中断优先级分组函数
void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority,uint32_t SubPriority); //中断优先级设置函数
void HAL_NVIC_EnableIRQ(IRQn_Type IRQn); //中断使能函数
void HAL_NVIC_disableIRQ(IRQn_Type IRQn);//中断除能函数
IRQn_Type的定义在 stm32f103xe.h 中。
外部中断使用步骤:
1、使能并设置对应的GPIO,设置时钟(所有用到GPIO的都要进行这个步骤)
2、设置IO口与中断线的映射对应关系
3、配置中断优先级并使能中断
4、编写中断服务函数 startup_stm32f103xe.s
代码全貌:
exti.h
#ifndef __EXTI_H
#define __EXTI_H
//定义按键引脚,中断服务,时钟等
#include ".\SYSTEM\sys\sys.h"
#define KEY0_INI_PORT GPIOC
#define KEY0_INI_PIN GPIO_PIN_5
//PC口时钟使能
#define KEY0_INI_CLK_ENABLE() do{__HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)
//设置中断线路和中断服务函数
#define KEY0_INI_EXTI_IRQn EXTI9_5_IRQn
#define KEY0_INI_EXTI_IRQHandler EXTI9_5_IRQHandler
#define KEY1_INI_PORT GPIOA
#define KEY1_INI_PIN GPIO_PIN_15
#define KEY1_INI_CLK_ENABLE() do{__HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)
#define KEY1_INI_EXTI_IRQn EXTI15_10_IRQn
#define KEY1_INI_EXTI_IRQHandler EXTI15_10_IRQHandler
#define KEYUP_INI_PORT GPIOA
#define KEYUP_INI_PIN GPIO_PIN_0
#define KEYUP_INI_CLK_ENABLE() do{__HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)
#define KEYUP_INI_EXTI_IRQn EXTI0_IRQn
#define KEYUP_INI_EXTI_IRQHandler EXTI0_IRQHandler
void extix_init(void);
#endif
exti.c
#include ".\SYSTEM\sys\sys.h"
#include ".\SYSTEM\delay\delay.h"
#include ".\BSP\exti\exti.h"
#include ".\BSP\led\led.h"
#include ".\BSP\KEY\key.h"
void KEYUP_INI_EXTI_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(KEYUP_INI_PIN);
__HAL_GPIO_EXTI_CLEAR_IT(KEYUP_INI_PIN);
}
void KEY1_INI_EXTI_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(KEY1_INI_PIN);
__HAL_GPIO_EXTI_CLEAR_IT(KEY1_INI_PIN);
}
void KEY0_INI_EXTI_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(KEY0_INI_PIN);
__HAL_GPIO_EXTI_CLEAR_IT(KEY0_INI_PIN);
}
//编写中断服务函数实际控制逻辑
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
delay_ms(20);
switch (GPIO_Pin)
{
case KEY0_INI_PIN:
if (KEY0 == 0)
{
LED0_Toggle();
LED1_Toggle();
}
break;
case KEY1_INI_PIN:
if(KEY1 == 0)
{
LED1_Toggle();
}
break;
case KEYUP_INI_PIN:
if(WKUP == 1)
{
LED0_Toggle();
}
break;
}
}
void extix_init(void)
{
GPIO_InitTypeDef gpio_init_struct; //定义GPIO初始化类型结构体
KEY0_INI_CLK_ENABLE(); //初始化时钟
KEY1_INI_CLK_ENABLE();
KEYUP_INI_CLK_ENABLE();
gpio_init_struct.Pin = KEY0_INI_PIN; //GPIO引脚定义
gpio_init_struct.Mode = GPIO_MODE_IT_FALLING; //KEY0是低有效,选择外部中断下降沿有效
gpio_init_struct.Pull = GPIO_PULLUP; //上拉电阻
HAL_GPIO_Init(KEY0_INI_PORT, &gpio_init_struct); //gpio初始化
gpio_init_struct.Pin = KEY1_INI_PIN; //GPIO引脚定义
gpio_init_struct.Mode = GPIO_MODE_IT_FALLING; //KEY1是低有效,选择外部中断下降沿有效
gpio_init_struct.Pull = GPIO_PULLUP; //上拉电阻
HAL_GPIO_Init(KEY1_INI_PORT, &gpio_init_struct); //gpio初始化
gpio_init_struct.Pin = KEYUP_INI_PIN; //GPIO引脚定义
gpio_init_struct.Mode = GPIO_MODE_IT_RISING; //KEYUP是高有效,选择外部中断上升沿有效
gpio_init_struct.Pull = GPIO_PULLDOWN; //下拉电阻
HAL_GPIO_Init(KEYUP_INI_PORT, &gpio_init_struct); //gpio初始化
//设置中断优先级函数
HAL_NVIC_SetPriority(KEYUP_INI_EXTI_IRQn,2,2);
HAL_NVIC_EnableIRQ(KEYUP_INI_EXTI_IRQn);
HAL_NVIC_SetPriority(KEY0_INI_EXTI_IRQn,0,2);
HAL_NVIC_EnableIRQ(KEY0_INI_EXTI_IRQn);
HAL_NVIC_SetPriority(KEY1_INI_EXTI_IRQn,1,2);
HAL_NVIC_EnableIRQ(KEY1_INI_EXTI_IRQn);
}
//中断服务函数
main.c
led和key函数与前两节相同,直接将文件拷到BSP文件夹里即可
#include ".\SYSTEM\sys\sys.h"
#include ".\SYSTEM\delay\delay.h"
#include ".\BSP\exti\exti.h"
#include ".\BSP\led\led.h"
#include ".\SYSTEM\usart\usart.h"
#include ".\stm32f1xx_it.h"
int main(void)
{
HAL_Init();
sys_stm32_clock_init(RCC_PLL_MUL9);
delay_init(72);
usart_init(115200);
led_INIT();
extix_init();
LED0(0);
// while(1)
// {
// printf("OK\r\n");
// delay_ms(1000);
// }
}
其中,
// while(1)
// {
// printf("OK\r\n");
// delay_ms(1000);
// }
我认为这可能是在调试阶段使用的工具。因为它注释后不会改变运行结果。当然如果以后发现有其他用途也会进行补充说明。也欢迎指正。
四、串口通信实验
1、硬件连接
接口PA9和接口PA10分别共用了UART1_RX端口和UART1_TX端口。因此,在配置串口通讯时,应将PCB上的引脚PA9和引脚PA10分别与PCB上的引脚RXD引脚TXD通过跳线帽进行连接。
USART的HAL驱动函数主要包含两个关键文件: stm32f1xx_hal_uart.c(实现异步通信功能)和 stm32f1xx_hal_usart.c(实现同步/异步通信功能)。当前项目仅限于使用 stm32f1xx_hal_uart.c 和 stm32f1xx_hal_uart.h 文件中的相关内容。
串口是一种特殊的外设设备,在执行通用流程之前需要进行初始化操作以启用主时钟模块。一旦启动串口控制器后,则需要详细配置中断优先级列表,并指定相应的中断入口点以便后续的操作处理。随后按照既定的流程依次完成各项任务就可以实现对其他外设的控制功能了
步骤如下:
i. 设置串口参数
ii. 配置GPIO总线时钟使其启用
iii. 设置GPIO工作模式
iv. 启用中断服务并设定其优先级等级
v. 配置中断入口并编写相应的中断处理函数
vi. 通过中断机制实现串口与外设的信息传输
i. 串口参数初始化(stm32f1xx_hal_uart.c ):
HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart); //用于初始化异步模式的收发器
用于初始化异步模式的收发器。
UART_HandleTypeDef 结构体类型定义:
在 stm32f1xx_hal_uart.h 中可以找到如下定义
typedef struct __UART_HandleTypeDef
{
USART_TypeDef *Instance; /* UART 寄存器基地址 */
UART_InitTypeDef Init; /* UART 通信参数 */
uint8_t *pTxBuffPtr; /* 指向 UART 发送缓冲区 */
uint16_t TxXferSize; /* UART 发送数据的大小 */
__IO uint16_t TxXferCount; /* UART 发送数据的个数 */
uint8_t *pRxBuffPtr; /* 指向 UART 接收缓冲区 */
uint16_t RxXferSize; /* UART 接收数据大小 */
__IO uint16_t RxXferCount; /* UART 接收数据的个数 */
DMA_HandleTypeDef *hdmatx; /* UART 发送参数设置(DMA) */
DMA_HandleTypeDef *hdmarx; /* UART 接收参数设置(DMA) */
HAL_LockTypeDef Lock; /* 锁定对象 */
__IO HAL_UART_StateTypeDef gState; /* UART 发送状态结构体 */
__IO HAL_UART_StateTypeDef RxState; /* UART 接收状态结构体 */
__IO uint32_t ErrorCode; /* UART 操作错误信息 */
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
void (* TxHalfCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Tx Half Complete Callback */
void (* TxCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Tx Complete Callback */
void (* RxHalfCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Rx Half Complete Callback */
void (* RxCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Rx Complete Callback */
void (* ErrorCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Error Callback */
void (* AbortCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Abort Complete Callback */
void (* AbortTransmitCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Abort Transmit Complete Callback */
void (* AbortReceiveCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Abort Receive Complete Callback */
void (* WakeupCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Wakeup Callback */
void (* MspInitCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Msp Init callback */
void (* MspDeInitCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Msp DeInit callback */
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */
} UART_HandleTypeDef;
摘录STM32MINI开发手册的解释:
1) Instance:指向 UART 寄存器基地址。 实际上这个基地址 HAL 库已经定义好了, 可以选择范围: USART1~ USART3、 UART4、 UART5。
2) Init: UART 初始化结构体,用于配置通讯参数,如波特率、数据位数、停止位等等。下面我们再详细讲解这个结构体。
3) pTxBuffPtr, TxXferSize, TxXferCount:分别是指向发送数据缓冲区的指针,发送数据的大小,发送数据的个数。
4) pRxBuffPtr, RxXferSize, RxXferCount:分别是指向接收数据缓冲区的指针,接受数据的大小,接收数据的个数;
5) hdmatx, hdmarx:配置串口发送接收数据的 DMA 具体参数。
6) Lock:对资源操作增加操作锁保护功能,可选 HAL_UNLOCKED 或者 HAL_LOCKED 两个参数。如果 gState 的值等于 HAL_UART_STATE_RESET,则可认为串口未被初始化,此时,分配锁资源,并且调用 HAL_UART_MspInit 函数来对串口的 GPIO 和时钟进行初始化。
7) gState, RxState:分别是 UART 的发送状态、工作状态的结构体和 UART 接受状态的结构体。 HAL_UART_StateTypeDef 是一个枚举类型,列出串口在工作过程中的状态值,有些值只适用于 gState,如 HAL_UART_STATE_BUSY。
8) ErrorCode:串口错误操作信息。主要用于存放串口操作的错误信息。
主要需要我们设置的是_UART_InitTypeDef 类别中的 Init 结构体参数,在同一文件中可以查找其结构体定义
typedef struct
{
uint32_t BaudRate; /*!< This member configures the UART communication baud rate.
The baud rate is computed using the following formula:
- IntegerDivider = ((PCLKx) / (16 * (huart->Init.BaudRate)))
- FractionalDivider = ((IntegerDivider - ((uint32_t) IntegerDivider)) * 16) + 0.5 */
uint32_t WordLength; /*!< Specifies the number of data bits transmitted or received in a frame.
This parameter can be a value of @ref UART_Word_Length */
uint32_t StopBits; /*!< Specifies the number of stop bits transmitted.
This parameter can be a value of @ref UART_Stop_Bits */
uint32_t Parity; /*!< Specifies the parity mode.
This parameter can be a value of @ref UART_Parity
@note When parity is enabled, the computed parity is inserted
at the MSB position of the transmitted data (9th bit when
the word length is set to 9 data bits; 8th bit when the
word length is set to 8 data bits). */
uint32_t Mode; /*!< Specifies whether the Receive or Transmit mode is enabled or disabled.
This parameter can be a value of @ref UART_Mode */
uint32_t HwFlowCtl; /*!< Specifies whether the hardware flow control mode is enabled or disabled.
This parameter can be a value of @ref UART_Hardware_Flow_Control */
uint32_t OverSampling; /*!< Specifies whether the Over sampling 8 is enabled or disabled, to achieve higher speed (up to fPCLK/8).
This parameter can be a value of @ref UART_Over_Sampling. This feature is only available
on STM32F100xx family, so OverSampling parameter should always be set to 16. */
} UART_InitTypeDef;
他共有七个参数需要设置具体包括:波特率、字节长度、停止位设置、奇偶校验位不需要设置、模式选择包括接收模式仅限于、发送模式仅限于以及双向通信模式、硬件流水控制不启用以及过采样设置为8倍和16倍通常建议使用16倍
现在再回去看初始化函数的内容:
HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart)
{
/* Check the UART handle allocation */
if (huart == NULL)
{
return HAL_ERROR;
}
/* Check the parameters */
if (huart->Init.HwFlowCtl != UART_HWCONTROL_NONE)
{
/* The hardware flow control is available only for USART1, USART2 and USART3 */
assert_param(IS_UART_HWFLOW_INSTANCE(huart->Instance));
assert_param(IS_UART_HARDWARE_FLOW_CONTROL(huart->Init.HwFlowCtl));
}
else
{
assert_param(IS_UART_INSTANCE(huart->Instance));
}
assert_param(IS_UART_WORD_LENGTH(huart->Init.WordLength));
#if defined(USART_CR1_OVER8)
assert_param(IS_UART_OVERSAMPLING(huart->Init.OverSampling));
#endif /* USART_CR1_OVER8 */
if (huart->gState == HAL_UART_STATE_RESET)
{
/* Allocate lock resource and initialize it */
huart->Lock = HAL_UNLOCKED;
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
UART_InitCallbacksToDefault(huart);
if (huart->MspInitCallback == NULL)
{
huart->MspInitCallback = HAL_UART_MspInit;
}
/* Init the low level hardware */
huart->MspInitCallback(huart);
#else
/* Init the low level hardware : GPIO, CLOCK */
HAL_UART_MspInit(huart);
#endif /* (USE_HAL_UART_REGISTER_CALLBACKS) */
}
huart->gState = HAL_UART_STATE_BUSY;
/* Disable the peripheral */
__HAL_UART_DISABLE(huart);
/* Set the UART Communication parameters */
UART_SetConfig(huart);
/* In asynchronous mode, the following bits must be kept cleared:
- LINEN and CLKEN bits in the USART_CR2 register,
- SCEN, HDSEL and IREN bits in the USART_CR3 register.*/
CLEAR_BIT(huart->Instance->CR2, (USART_CR2_LINEN | USART_CR2_CLKEN));
CLEAR_BIT(huart->Instance->CR3, (USART_CR3_SCEN | USART_CR3_HDSEL | USART_CR3_IREN));
/* Enable the peripheral */
__HAL_UART_ENABLE(huart);
/* Initialize the UART state */
huart->ErrorCode = HAL_UART_ERROR_NONE;
huart->gState = HAL_UART_STATE_READY;
huart->RxState = HAL_UART_STATE_READY;
return HAL_OK;
}
由于该函数基于 HAL_StatusTypeDef 枚举定义,在运行过程中可能会返回四种不同的结果:分别为:当Huart指针为空时触发报错(错误状态),成功状态则为 HAL_OK ,串口处于忙碌状态(被占用中)时会显示 HAL_BUSY ,而如果发生超时情况则会返回 HAL_TIMEOUT
typedef enum
{
HAL_OK = 0x00U,
HAL_ERROR = 0x01U,
HAL_BUSY = 0x02U,
HAL_TIMEOUT = 0x03U
} HAL_StatusTypeDef;
iv. 开启中断服务,配置优先级
ii和iii的配置和第一节的内容是一样的,这里不做赘述
开启串口中断的函数为 HAL_UART_Receive_IT
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
/* Check that a Rx process is not already ongoing */
if (huart->RxState == HAL_UART_STATE_READY)
{
if ((pData == NULL) || (Size == 0U))
{
return HAL_ERROR;
}
/* Process Locked */
__HAL_LOCK(huart);
huart->pRxBuffPtr = pData;
huart->RxXferSize = Size;
huart->RxXferCount = Size;
huart->ErrorCode = HAL_UART_ERROR_NONE;
huart->RxState = HAL_UART_STATE_BUSY_RX;
/* Process Unlocked */
__HAL_UNLOCK(huart);
/* Enable the UART Parity Error Interrupt */
__HAL_UART_ENABLE_IT(huart, UART_IT_PE);
/* Enable the UART Error Interrupt: (Frame error, noise error, overrun error) */
__HAL_UART_ENABLE_IT(huart, UART_IT_ERR);
/* Enable the UART Data Register not empty Interrupt */
__HAL_UART_ENABLE_IT(huart, UART_IT_RXNE);
return HAL_OK;
}
else
{
return HAL_BUSY;
}
}
该函数采用三个参数进行处理,包括 UART...类型的句柄变量、指定的数据地址*pData以及所处理的数据总量Size。
该函数的返回值类型与 HAL_UART.Init 函数相同,并且它们都属于 HAL_StatusTypeDef 这一枚举类别。
v. 设置中断入口,编写中断函数
串口的中断处理公共函数为 HAL_UART_IRQHandler 。
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
该函数参数与_HAL_UART.Init_的参数一致,在_HAL_UART中实现中断服务功能。然而_HAL_UART Init_的采用逻辑是固定的,并非用户可随意更改;若需自定义其采用逻辑,则必须使用弱定义的回调函数
__weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_AbortCpltCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_AbortTransmitCpltCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_AbortReceiveCpltCallback(UART_HandleTypeDef *huart)
头文件
串口配置:
参考数据手册可知,在开发板上串口部分的TX与RX端子分别连接至PA10与PA9引脚。因此,在进行硬件配置时,请将串口电路按端口号-引脚号-时钟频率顺序进行相应设置即可配置为按端口号-引脚号-时钟频率顺序建立相应的宏定义。

该包含文件以及.c文件均为官方提供,请无需自行编写或修改配置即可
串口工作的流程图如下:

由正点原子的官方描述,串口接收的逻辑和思路如下:
当接收到从电脑发过来的数据,把接收到的数据保存在数组 g_usart_rx_buf 中,同时在接收状态寄存器(g_usart_rx_sta)中计数接收到的有效数据个数,当收到回车(回车的表示由 2个字节组成: 0X0D 和 0X0A)的第一个字节 0X0D 时,计数器将不再增加,等待 0X0A 的到来,而如果 0X0A 没有来到,则认为这次接收失败,重新开始下一次接收。如果顺利接收到0X0A,则标记 g_usart_rx_sta 的第 15 位,这样完成一次接收,并等待该位被其他程序清除,从 而 开 始 下 一 次 的 接 收 , 而 如 果 迟 迟 没 有 收 到 0X0D , 那 么 在 接 收 数 据 超 过USART_REC_LEN 的时候,则会丢弃前面的数据,重新接收。
代码全貌(由于串口的开发流程比较固定,main.c函数直接摘自程序源码):
main.c
#include "./stm32f1xx_it.h"
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
int main(void)
{
uint8_t len;
uint16_t times = 0;
HAL_Init() ; /* HAL库初始化 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_INIT(); /* 初始化LED */
while (1)
{
if (g_usart_rx_sta & 0x8000) /* 接收到了数据? */
{
len = g_usart_rx_sta & 0x3fff; /* 得到此次接收到的数据长度 */
printf("\r\n您发送的消息为:\r\n");
HAL_UART_Transmit(&g_uart1_handle, (uint8_t*)g_usart_rx_buf, len, 1000); /* 发送接收到的数据 */
while(__HAL_UART_GET_FLAG(&g_uart1_handle, UART_FLAG_TC) != SET); /* 等待发送结束 */
printf("\r\n\r\n"); /* 插入换行 */
g_usart_rx_sta = 0;
}
else
{
times++;
if (times % 5000 == 0)
{
printf("\r\n正点原子 STM32 开发板 串口实验\r\n");
printf("正点原子@ALIENTEK\r\n\r\n\r\n");
}
if (times % 200 == 0) printf("请输入数据,以回车键结束\r\n");
if (times % 30 == 0) LED0_Toggle(); /* 闪烁LED,提示系统正在运行. */
delay_ms(10);
}
}
}
P.S. 在使用官方文件创建模板时,请务必区分寄存器版本(register version)与HAL版本(HAL version)。两者虽然在绝大多数文件上是相同的配置内容,但在具体编码实现上有细微差别。以我之前的串口实验为例,在复制模板文件时误将寄存器版本的内容导入导致 usart.c 和 usart.h 文件与HAL库配置存在不同之处如下所示:
寄存器版本的usart.c文件
/** **************************************************************************************************** * @file usart.c
* @author 正点原子团队(ALIENTEK)
* @version V1.0
* @date 2020-04-17
* @brief 串口初始化代码(一般是串口1),支持printf
* @license Copyright (c) 2020-2032, 广州市星翼电子科技有限公司
**************************************************************************************************** * @attention
* * 实验平台:正点原子 STM32F103开发板
* 在线视频:www.yuanzige.com
* 技术论坛:www.openedv.com
* 公司网址:www.alientek.com
* 购买地址:openedv.taobao.com
* * 修改说明
* V1.0 20200417
* 第一次发布
* **************************************************************************************************** */
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
/* 如果使用os,则包括下面的头文件即可. */
#if SYS_SUPPORT_OS
#include "includes.h" /* os 使用 */
#endif
/******************************************************************************************/
/* 加入以下代码, 支持printf函数, 而不需要选择use MicroLIB */
#if 1
#if (__ARMCC_VERSION >= 6010050) /* 使用AC6编译器时 */
__asm(".global __use_no_semihosting\n\t"); /* 声明不使用半主机模式 */
__asm(".global __ARM_use_no_argv \n\t"); /* AC6下需要声明main函数为无参数格式,否则部分例程可能出现半主机模式 */
#else
/* 使用AC5编译器时, 要在这里定义__FILE 和 不使用半主机模式 */
#pragma import(__use_no_semihosting)
struct __FILE
{
int handle;
/* Whatever you require here. If the only file you are using is */
/* standard output using printf() for debugging, no file handling */
/* is required. */
};
#endif
/* 不使用半主机模式,至少需要重定义_ttywrch\_sys_exit\_sys_command_string函数,以同时兼容AC6和AC5模式 */
int _ttywrch(int ch)
{
ch = ch;
return ch;
}
/* 定义_sys_exit()以避免使用半主机模式 */
void _sys_exit(int x)
{
x = x;
}
char *_sys_command_string(char *cmd, int len)
{
return NULL;
}
/* FILE 在 stdio.h里面定义. */
FILE __stdout;
/* MDK下需要重定义fputc函数, printf函数最终会通过调用fputc输出字符串到串口 */
int fputc(int ch, FILE *f)
{
while ((USART_UX->SR & 0X40) == 0); /* 等待上一个字符发送完成 */
USART_UX->DR = (uint8_t)ch; /* 将要发送的字符 ch 写入到DR寄存器 */
return ch;
}
#endif
/******************************************************************************************/
#if USART_EN_RX /* 如果使能了接收 */
/* 接收缓冲, 最大USART_REC_LEN个字节. */
uint8_t g_usart_rx_buf[USART_REC_LEN];
/* 接收状态
* bit15, 接收完成标志
* bit14, 接收到0x0d
* bit13~0, 接收到的有效字节数目
*/
uint16_t g_usart_rx_sta = 0;
/** * @brief 串口X中断服务函数
* @param 无
* @retval 无
*/
void USART_UX_IRQHandler(void)
{
uint8_t rxdata;
#if SYS_SUPPORT_OS /* 如果SYS_SUPPORT_OS为真,则需要支持OS. */
OSIntEnter();
#endif
if (USART_UX->SR & (1 << 5)) /* 接收到数据 */
{
rxdata = USART_UX->DR;
if ((g_usart_rx_sta & 0x8000) == 0) /* 接收未完成? */
{
if (g_usart_rx_sta & 0x4000) /* 接收到了0x0d? */
{
if (rxdata != 0x0a) /* 接收到了0x0a? (必须先接收到到0x0d,才检查0x0a) */
{
g_usart_rx_sta = 0; /* 接收错误, 重新开始 */
}
else
{
g_usart_rx_sta |= 0x8000; /* 收到了0x0a,标记接收完成了 */
}
}
else /* 还没收到0x0d */
{
if (rxdata == 0x0d)
{
g_usart_rx_sta |= 0x4000; /* 标记接收到了 0x0d */
}
else
{
g_usart_rx_buf[g_usart_rx_sta & 0X3FFF] = rxdata; /* 存储数据到 g_usart_rx_buf */
g_usart_rx_sta++;
if (g_usart_rx_sta > (USART_REC_LEN - 1))g_usart_rx_sta = 0;/* 接收数据溢出, 重新开始接收 */
}
}
}
}
#if SYS_SUPPORT_OS /* 如果SYS_SUPPORT_OS为真,则需要支持OS. */
OSIntExit();
#endif
}
#endif
/** * @brief 串口X初始化函数
* @param sclk: 串口X的时钟源频率(单位: MHz)
* 串口1 的时钟源来自: PCLK2 = 72Mhz
* 串口2 - 5 的时钟源来自: PCLK1 = 36Mhz
* @note 注意: 必须设置正确的sclk, 否则串口波特率就会设置异常.
* @param baudrate: 波特率, 根据自己需要设置波特率值
* @retval 无
*/
void usart_init(uint32_t sclk, uint32_t baudrate)
{
uint32_t temp;
/* IO 及 时钟配置 */
USART_TX_GPIO_CLK_ENABLE(); /* 使能串口TX脚时钟 */
USART_RX_GPIO_CLK_ENABLE(); /* 使能串口RX脚时钟 */
USART_UX_CLK_ENABLE(); /* 使能串口时钟 */
sys_gpio_set(USART_TX_GPIO_PORT, USART_TX_GPIO_PIN,
SYS_GPIO_MODE_AF, SYS_GPIO_OTYPE_PP, SYS_GPIO_SPEED_HIGH, SYS_GPIO_PUPD_PU); /* 串口TX脚 模式设置 */
sys_gpio_set(USART_RX_GPIO_PORT, USART_RX_GPIO_PIN,
SYS_GPIO_MODE_IN, SYS_GPIO_OTYPE_PP, SYS_GPIO_SPEED_HIGH, SYS_GPIO_PUPD_PU); /* 串口RX脚 必须设置成输入模式 */
temp = (sclk * 1000000 + baudrate / 2) / baudrate; /* 得到BRR, 采用四舍五入计算 */
/* 波特率设置 */
USART_UX->BRR = temp; /* 波特率设置 */
USART_UX->CR1 = 0; /* 清零CR1寄存器 */
USART_UX->CR1 |= 0 << 12; /* M = 0, 1个起始位, 8个数据位, n个停止位(由USART_CR2 STOP[1:0]指定, 默认是0, 表示1个停止位) */
USART_UX->CR1 |= 1 << 3; /* TE = 1, 串口发送使能 */
#if USART_EN_RX /* 如果使能了接收 */
/* 使能接收中断 */
USART_UX->CR1 |= 1 << 2; /* RE = 1, 串口接收使能 */
USART_UX->CR1 |= 1 << 5; /* RXNEIE = 1, 接收缓冲区非空中断使能 */
sys_nvic_init(3, 3, USART_UX_IRQn, 2); /* 组2,最低优先级 */
#endif
USART_UX->CR1 |= 1 << 13; /* UE = 1, 串口使能 */
}
HAL库版本的usart.c文件
/** **************************************************************************************************** * @file usart.c
* @author 正点原子团队(ALIENTEK)
* @version V1.0
* @date 2020-04-20
* @brief 串口初始化代码(一般是串口1),支持printf
* @license Copyright (c) 2020-2032, 广州市星翼电子科技有限公司
**************************************************************************************************** * @attention
* * 实验平台:正点原子 STM32F103开发板
* 在线视频:www.yuanzige.com
* 技术论坛:www.openedv.com
* 公司网址:www.alientek.com
* 购买地址:openedv.taobao.com
* * 修改说明
* V1.0 20211103
* 第一次发布
* **************************************************************************************************** */
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
/* 如果使用os,则包括下面的头文件即可. */
#if SYS_SUPPORT_OS
#include "includes.h" /* os 使用 */
#endif
/******************************************************************************************/
/* 加入以下代码, 支持printf函数, 而不需要选择use MicroLIB */
#if 1
#if (__ARMCC_VERSION >= 6010050) /* 使用AC6编译器时 */
__asm(".global __use_no_semihosting\n\t"); /* 声明不使用半主机模式 */
__asm(".global __ARM_use_no_argv \n\t"); /* AC6下需要声明main函数为无参数格式,否则部分例程可能出现半主机模式 */
#else
/* 使用AC5编译器时, 要在这里定义__FILE 和 不使用半主机模式 */
#pragma import(__use_no_semihosting)
struct __FILE
{
int handle;
/* Whatever you require here. If the only file you are using is */
/* standard output using printf() for debugging, no file handling */
/* is required. */
};
#endif
/* 不使用半主机模式,至少需要重定义_ttywrch\_sys_exit\_sys_command_string函数,以同时兼容AC6和AC5模式 */
int _ttywrch(int ch)
{
ch = ch;
return ch;
}
/* 定义_sys_exit()以避免使用半主机模式 */
void _sys_exit(int x)
{
x = x;
}
char *_sys_command_string(char *cmd, int len)
{
return NULL;
}
/* FILE 在 stdio.h里面定义. */
FILE __stdout;
/* MDK下需要重定义fputc函数, printf函数最终会通过调用fputc输出字符串到串口 */
int fputc(int ch, FILE *f)
{
while ((USART_UX->SR & 0X40) == 0); /* 等待上一个字符发送完成 */
USART_UX->DR = (uint8_t)ch; /* 将要发送的字符 ch 写入到DR寄存器 */
return ch;
}
#endif
/******************************************************************************************/
#if USART_EN_RX /*如果使能了接收*/
/* 接收缓冲, 最大USART_REC_LEN个字节. */
uint8_t g_usart_rx_buf[USART_REC_LEN];
/* 接收状态
* bit15, 接收完成标志
* bit14, 接收到0x0d
* bit13~0, 接收到的有效字节数目
*/
uint16_t g_usart_rx_sta = 0;
uint8_t g_rx_buffer[RXBUFFERSIZE]; /* HAL库使用的串口接收缓冲 */
UART_HandleTypeDef g_uart1_handle; /* UART句柄 */
/** * @brief 串口X初始化函数
* @param baudrate: 波特率, 根据自己需要设置波特率值
* @note 注意: 必须设置正确的时钟源, 否则串口波特率就会设置异常.
* 这里的USART的时钟源在sys_stm32_clock_init()函数中已经设置过了.
* @retval 无
*/
void usart_init(uint32_t baudrate)
{
/*UART 初始化设置*/
g_uart1_handle.Instance = USART_UX; /* USART_UX */
g_uart1_handle.Init.BaudRate = baudrate; /* 波特率 */
g_uart1_handle.Init.WordLength = UART_WORDLENGTH_8B; /* 字长为8位数据格式 */
g_uart1_handle.Init.StopBits = UART_STOPBITS_1; /* 一个停止位 */
g_uart1_handle.Init.Parity = UART_PARITY_NONE; /* 无奇偶校验位 */
g_uart1_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; /* 无硬件流控 */
g_uart1_handle.Init.Mode = UART_MODE_TX_RX; /* 收发模式 */
HAL_UART_Init(&g_uart1_handle); /* HAL_UART_Init()会使能UART1 */
/* 该函数会开启接收中断:标志位UART_IT_RXNE,并且设置接收缓冲以及接收缓冲接收最大数据量 */
HAL_UART_Receive_IT(&g_uart1_handle, (uint8_t *)g_rx_buffer, RXBUFFERSIZE);
}
/** * @brief UART底层初始化函数
* @param huart: UART句柄类型指针
* @note 此函数会被HAL_UART_Init()调用
* 完成时钟使能,引脚配置,中断配置
* @retval 无
*/
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
GPIO_InitTypeDef gpio_init_struct;
if (huart->Instance == USART_UX) /* 如果是串口1,进行串口1 MSP初始化 */
{
USART_TX_GPIO_CLK_ENABLE(); /* 使能串口TX脚时钟 */
USART_RX_GPIO_CLK_ENABLE(); /* 使能串口RX脚时钟 */
USART_UX_CLK_ENABLE(); /* 使能串口时钟 */
gpio_init_struct.Pin = USART_TX_GPIO_PIN; /* 串口发送引脚号 */
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* IO速度设置为高速 */
HAL_GPIO_Init(USART_TX_GPIO_PORT, &gpio_init_struct);
gpio_init_struct.Pin = USART_RX_GPIO_PIN; /* 串口RX脚 模式设置 */
gpio_init_struct.Mode = GPIO_MODE_AF_INPUT;
HAL_GPIO_Init(USART_RX_GPIO_PORT, &gpio_init_struct); /* 串口RX脚 必须设置成输入模式 */
#if USART_EN_RX
HAL_NVIC_EnableIRQ(USART_UX_IRQn); /* 使能USART1中断通道 */
HAL_NVIC_SetPriority(USART_UX_IRQn, 3, 3); /* 组2,最低优先级:抢占优先级3,子优先级3 */
#endif
}
}
/** * @brief 串口数据接收回调函数
数据处理在这里进行
* @param huart:串口句柄
* @retval 无
*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART_UX) /* 如果是串口1 */
{
if ((g_usart_rx_sta & 0x8000) == 0) /* 接收未完成 */
{
if (g_usart_rx_sta & 0x4000) /* 接收到了0x0d(即回车键) */
{
if (g_rx_buffer[0] != 0x0a) /* 接收到的不是0x0a(即不是换行键) */
{
g_usart_rx_sta = 0; /* 接收错误,重新开始 */
}
else /* 接收到的是0x0a(即换行键) */
{
g_usart_rx_sta |= 0x8000; /* 接收完成了 */
}
}
else /* 还没收到0X0d(即回车键) */
{
if (g_rx_buffer[0] == 0x0d)
g_usart_rx_sta |= 0x4000;
else
{
g_usart_rx_buf[g_usart_rx_sta & 0X3FFF] = g_rx_buffer[0];
g_usart_rx_sta++;
if (g_usart_rx_sta > (USART_REC_LEN - 1))
{
g_usart_rx_sta = 0; /* 接收数据错误,重新开始接收 */
}
}
}
}
}
}
/** * @brief 串口X中断服务函数
注意,读取USARTx->SR能避免莫名其妙的错误
* @param 无
* @retval 无
*/
void USART_UX_IRQHandler(void)
{
#if SYS_SUPPORT_OS /* 使用OS */
OSIntEnter();
#endif
HAL_UART_IRQHandler(&g_uart1_handle); /* 调用HAL库中断处理公用函数 */
while (HAL_UART_Receive_IT(&g_uart1_handle, (uint8_t *)g_rx_buffer, RXBUFFERSIZE) != HAL_OK) /* 重新开启中断并接收数据 */
{
/* 如果出错会卡死在这里 */
}
#if SYS_SUPPORT_OS /* 使用OS */
OSIntExit();
#endif
}
#endif
在串口初始化函数的具体描述方面存在差异。最初遇到的问题是在编译main.c时出现错误提示:缺少必要的参数配置。后来对比官方提供的usart.c发现两者的实现细节有所不同:在硬件接口中引脚对应关系以及驱动程序的功能模块安排等方面存在明显差异;因此在初期并未给予足够的重视:直接将官方提供的这两个关键文件进行了直接复制导入;最终编译通过并完成了基本功能测试:但在随后的操作过程中发现:由于其他相关资源采用的是寄存器类型的配置方式;这与其所依据的不同体系架构(如HAL框架)造成了一定的不兼容性;特别是在设置波特率等底层控制参数时出现了不可预测的结果偏差:如图所示

我最初认为这个问题可能出在硬件设备或者正点原子调试软件上,但整整一小时下来也没能解决这个困扰QAQ。后来按照官方提供的示例程序运行后发现问题根源在于工程配置文件出现了问题。通过一步步排查最终锁定了具体原因所在。不过考虑到时间因素,并没有对底层代码库进行深入查看(毕竟也没有必要),但经过实际测试发现:如果将HAL接口的串口驱动程序直接复制到寄存器接口的工程环境中运行的话(如图所示),会导致你所设定的实际波特率仅为理论值的1/8

五、基本定时器
博主学习STM32除了个人爱好和专业需求之外,在机器人领域也具有重要意义。为了实现机器人动作控制功能,在硬件设计中采用了基于PWM波的伺服机构驱动方案。该方案利用STM32单片机的定时器模块进行精确时间控制,在数字信号处理的基础上实现了稳定的脉宽调制过程。
STM32F103系列共有8个定时器,在其中 TIM6 和 TIM7 属于基础型 定时 器 ,而(TIM2至 TIM5)则属于通用型 定时 器 。此外, TIM1以及 TIM8属于高级型定 时 器 。
程序设计:
与定时器相关的驱动代码存储在stm32f1xx_hal_tim.h文件夹里。要配置任意外设的固定流程,请按照以下步骤操作:第一步是初始化定时器模块;第二步是使能定时器运行;第三步是配置时钟源并设置计时参数。
1、初始化函数: HAL_StatusTypeDef HAL_TIM_Base_Init(TIM_HandleTypeDef *htim)
位于 stm32f1xx_hal_tim.c 中,函数描述如下:
HAL_StatusTypeDef HAL_TIM_Base_Init(TIM_HandleTypeDef *htim)
{
/* Check the TIM handle allocation */
if (htim == NULL)
{
return HAL_ERROR;
}
/* Check the parameters */
assert_param(IS_TIM_INSTANCE(htim->Instance));
assert_param(IS_TIM_COUNTER_MODE(htim->Init.CounterMode));
assert_param(IS_TIM_CLOCKDIVISION_DIV(htim->Init.ClockDivision));
assert_param(IS_TIM_AUTORELOAD_PRELOAD(htim->Init.AutoReloadPreload));
if (htim->State == HAL_TIM_STATE_RESET)
{
/* Allocate lock resource and initialize it */
htim->Lock = HAL_UNLOCKED;
#if (USE_HAL_TIM_REGISTER_CALLBACKS == 1)
/* Reset interrupt callbacks to legacy weak callbacks */
TIM_ResetCallback(htim);
if (htim->Base_MspInitCallback == NULL)
{
htim->Base_MspInitCallback = HAL_TIM_Base_MspInit;
}
/* Init the low level hardware : GPIO, CLOCK, NVIC */
htim->Base_MspInitCallback(htim);
#else
/* Init the low level hardware : GPIO, CLOCK, NVIC */
HAL_TIM_Base_MspInit(htim);
#endif /* USE_HAL_TIM_REGISTER_CALLBACKS */
}
/* Set the TIM state */
htim->State = HAL_TIM_STATE_BUSY;
/* Set the Time Base configuration */
TIM_Base_SetConfig(htim->Instance, &htim->Init);
/* Initialize the DMA burst operation state */
htim->DMABurstState = HAL_DMA_BURST_STATE_READY;
/* Initialize the TIM channels state */
TIM_CHANNEL_STATE_SET_ALL(htim, HAL_TIM_CHANNEL_STATE_READY);
TIM_CHANNEL_N_STATE_SET_ALL(htim, HAL_TIM_CHANNEL_STATE_READY);
/* Initialize the TIM state*/
htim->State = HAL_TIM_STATE_READY;
return HAL_OK;
}
该函数的参数是属于 TIMHeroes 类型的变量地址。在相关文档中可找到有关 TIMHeroes 结构体的详细描述:
typedef struct
{
TIM_TypeDef *Instance; /*!< Register base address */
TIM_Base_InitTypeDef Init; /*!< TIM Time Base required parameters */
HAL_TIM_ActiveChannel Channel; /*!< Active channel */
DMA_HandleTypeDef *hdma[7]; /*!< DMA Handlers array
This array is accessed by a @ref DMA_Handle_index */
HAL_LockTypeDef Lock; /*!< Locking object */
__IO HAL_TIM_StateTypeDef State; /*!< TIM operation state */
__IO HAL_TIM_ChannelStateTypeDef ChannelState[4]; /*!< TIM channel operation state */
__IO HAL_TIM_ChannelStateTypeDef ChannelNState[4]; /*!< TIM complementary channel operation state */
__IO HAL_TIM_DMABurstStateTypeDef DMABurstState; /*!< DMA burst operation state */
} TIM_HandleTypeDef;
此结构体包含九个成员项:寄存器基准地址( Instance )、基础初始化参数 ( Init )以及通道设置 ( Channel )等三项指标;此外还包括配置的一个DMA请求队列 ( hdma[7] )、ADC锁资源项 ( Lock )以及定时器状态信息 ( State )。需要注意的是,在本节讨论中将暂时不涉及后两个通道的状态信息。
注
typedef struct
{
uint32_t Prescaler;
uint32_t CounterMode;
uint32_t Period;
uint32_t ClockDivision;
uint32_t RepetitionCounter;
uint32_t AutoReloadPreload;
} TIM_Base_InitTypeDef;
初始化参数包括:1)预设置倍率( Preset Rate),即定时器时钟源与APB1总线时钟的关系。当Preset Rate设为1时(即APB\_XBGM = APB1),定时器工作在36MHz频率;当Preset Rate ≥2时(即APB\_XBGM = /2 APB1),定时器频率可达72MHz。2)计数模式(Counting Mode),决定了输出波形类型:基本定时器仅支持递增计数输出PWM波或SPWM波形。3)溢出重载值(Auto Reload Value ARR),基本定时器采用16位寄存器控制溢出重载值范围为0至65535(即0 ≤ ARR ≤ 2^{16} - 1)。4)后续将介绍的基本定时器内部时钟分频因子设置方法。5)高级定时器中新增的重复计数器功能模块。6)自动启用缓冲机制可通过ARPE1位配置:当该位置0时无缓冲功能;当置位后可实现数据存入影子寄存器完成缓冲操作。
基本定时器需要配置的就是1,2,3,6位。
1、预分频系数往往是自己在函数中输入,但也可以选官方的给定值
#define TIM_ETRPRESCALER_DIV1 0x00000000U /*!< No prescaler is used */
#define TIM_ETRPRESCALER_DIV2 TIM_SMCR_ETPS_0 /*!< ETR input source is divided by 2 */
#define TIM_ETRPRESCALER_DIV4 TIM_SMCR_ETPS_1 /*!< ETR input source is divided by 4 */
#define TIM_ETRPRESCALER_DIV8 TIM_SMCR_ETPS /*!< ETR input source is divided by 8 */
2、计数模式位于 stm32f1xx_hal_tim.h 中(个人认为该领域的描述不如MSP430的手册详尽且出色,并且该手册为英文版)
TIM_COUNTERMODE_UP 0x00000000U /*!< Counter used as up-counter */
#define TIM_COUNTERMODE_DOWN TIM_CR1_DIR /*!< Counter used as down-counter */
#define TIM_COUNTERMODE_CENTERALIGNED1 TIM_CR1_CMS_0 /*!< Center-aligned mode 1 */
#define TIM_COUNTERMODE_CENTERALIGNED2 TIM_CR1_CMS_1 /*!< Center-aligned mode 2 */
#define TIM_COUNTERMODE_CENTERALIGNED3 TIM_CR1_CMS /*!< Center-aligned mode 3 */
3、ARR值往往也是后续定的
6、ARPE1就一位就没必要设置官方参数了
请回顾前面提到的初始化函数,请注意其定义如下:int __stdcall HAL.TIM_Base_Init(TIM_HandleTypeDef *htim); 该函数定义为返回值类型为枚举类型的数据。
typedef enum
{
HAL_OK = 0x00U,
HAL_ERROR = 0x01U,
HAL_BUSY = 0x02U,
HAL_TIMEOUT = 0x03U
} HAL_StatusTypeDef;
2、使能定时器
更新定时器中断和使能定时器的函数如下:
HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim)
{
uint32_t tmpsmcr;
/* Check the parameters */
assert_param(IS_TIM_INSTANCE(htim->Instance));
/* Check the TIM state */
if (htim->State != HAL_TIM_STATE_READY)
{
return HAL_ERROR;
}
/* Set the TIM state */
htim->State = HAL_TIM_STATE_BUSY;
/* Enable the TIM Update interrupt */
__HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE);
/* Enable the Peripheral, except in trigger mode where enable is automatically done with trigger */
if (IS_TIM_SLAVE_INSTANCE(htim->Instance))
{
tmpsmcr = htim->Instance->SMCR & TIM_SMCR_SMS;
if (!IS_TIM_SLAVEMODE_TRIGGER_ENABLED(tmpsmcr))
{
__HAL_TIM_ENABLE(htim);
}
}
else
{
__HAL_TIM_ENABLE(htim);
}
/* Return function status */
return HAL_OK;
}
在该函数中被调用的是___HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE);_ 该宏函数被用来更新中断。此外,在此过程中也被使用的还有___HAL.TIM.Enable(hTim);_ 该宏函数也被用来使能定时器。
当然,如果有需要可以单独使能中断和定时器:
__HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE); /* 使能句柄指定的定时器更新中断 */
__HAL_TIM_DISABLE_IT (htim, TIM_IT_UPDATE); /* 关闭句柄指定的定时器更新中断 */
__HAL_TIM_ENABLE(htim); /* 使能句柄 htim 指定的定时器 */
__HAL_TIM_DISABLE(htim); /* 关闭句柄 htim 指定的定时器 */
3、时钟
使能定时器时钟函数在rcc_ex.c文件中
__HAL_RCC_TIMx_CLK_ENABLE();
配置定时器步骤:
1、使能时钟
2、初始化定时器参数并使能定时器
启用定时器更新事件,并配置相应的硬件 NVIC中断源(HAL_NVIC_EnableIRQ)。随后调整该事件的优先级(HAL_NVIC_SetPriority),启动相应的中断入口以响应事件触发情况。所有与中断相关的代码实现将被包含在 _HAL.TIM_BASE, 函数内,并且该函数将作为定时器相关的中断回调机制进行运行处理。
__weak void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
UNUSED(htim);//用于放置函数代码的弱定义回调函数
}
该函数会在底层被 HAL_TIM_Base_Init 函数调用,故不用填入形参。
4、编写中断服务函数。
代码全貌:
timer.h
#ifndef __BTIM_H
#define __BTIM_H
#include ".\SYSTEM\sys\sys.h"
#include "stdio.h"
#define BASE_TIM6_INI TIM6 //相当于定时器引脚
#define BASE_TIM6_INI_IRQn TIM6_DAC_IRQn //中断向量,其实就是TIM6_IRQn
#define BASE_TIM6_INI_IRQHandler TIM6_DAC_IRQHandler //中断公共服务函数
/*凡是与中断有关的,都有一个中断向量,一个中断服务函数,中断向量决定什么时候触发中断,
中断服务函数决定CPU中断现在干的事以后要干嘛*/
#define TIM6_CLK_ENABLE() do{ __HAL_RCC_TIM6_CLK_ENABLE();}while(0)
void TIMER_TIMx_Init(uint32_t ARR,uint32_t PSC);
#endif
timer.c
#include ".\BSP\TIMER\timer.h"
#include ".\BSP\LED\led.h"
//这个.c文件主要做步骤内的三件事:1、初始化定时器。2、设置中断。3、编写中断函数
//1、初始化定时器
TIM_HandleTypeDef timx_init_handle;
void TIMER_TIMx_Init(uint32_t ARR,uint32_t PSC)
{
timx_init_handle.Instance = BASE_TIM6_INI;
timx_init_handle.Init.Prescaler = PSC;
timx_init_handle.Init.CounterMode = TIM_COUNTERMODE_UP;
timx_init_handle.Init.Period = ARR;
HAL_TIM_Base_Init(&timx_init_handle);//定时器初始化
HAL_TIM_Base_Start_IT(&timx_init_handle);//中断更新和定时器使能
}
//2、开启时钟,设置中断,中断时间Tout= ((arr+1)*(psc+1))/Tclk
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
//我们需要判断当这个参数是我们需要的TIM6时触发,因为在其他项目中,timx_init_handle可能会指代多个定时器
if(htim->Instance == BASE_TIM6_INI)
{
//使能时钟
TIM6_CLK_ENABLE();
//设置中断优先级
HAL_NVIC_SetPriority(BASE_TIM6_INI_IRQn,1,3);
//使能中断
HAL_NVIC_EnableIRQ(BASE_TIM6_INI_IRQn);
}
}
//3、编写中断函数,这里依然要使用一个回调函数
void BASE_TIM6_INI_IRQHandler(void)
{
HAL_TIM_IRQHandler(&timx_init_handle);//公共中断处理函数
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == BASE_TIM6_INI)
{
LED1_Toggle(); /* LED1反转 */
}
}
main.c
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/TIMER/timer.h"
int main(void)
{
HAL_Init();//初始化HAL库
sys_stm32_clock_init(RCC_PLL_MUL9);//APB1总线时钟72MHz
delay_init(72);
usart_init(115200);
led_INIT();
TIMER_TIMx_Init(5000-1,7200-1);
while(1)
{
LED0_Toggle();
delay_ms(200);
}
}
