基于HAL库的STM32开发文档(STM32CubeMX)
一、基本IO——从点亮一颗LED开始
1.1、常用的GPIO HAL库函数
①HAL_GPIO_ReadPin():读取GPIO引脚的当前状态
/**
- @brief Reads the specified input port pin.
- @param GPIOx: where x can be (A..G depending on device used) to select the GPIO peripheral
- @param GPIO_Pin: specifies the port bit to read.
- This parameter can be GPIO_PIN_x where x can be (0..15).
- @retval The input port pin value.
*/
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
/**参数说明
GPIOx:GPIO端口号,例如 GPIOA、GPIOB、GPIOC 等。
GPIO_Pin:GPIO引脚号,可以是单独的引脚号,例如 GPIO_PIN_0,也可以是多个引脚的按位或组合,例如 GPIO_PIN_0 | GPIO_PIN_1。
返回值说明:
GPIO_PIN_RESET:引脚为低电平。
GPIO_PIN_SET:引脚为高电平。
*/
②HAL_GPIO_WritePin():设置GPIO引脚的状态
/**
- @brief Sets or clears the selected data port bit.
*
- @note This function uses GPIOx_BSRR register to allow atomic read/modify
- accesses. In this way, there is no risk of an IRQ occurring between
- the read and the modify access.
*
- @param GPIOx: where x can be (A..G depending on device used) to select the GPIO peripheral
- @param GPIO_Pin: specifies the port bit to be written.
- This parameter can be one of GPIO_PIN_x where x can be (0..15).
- @param PinState: specifies the value to be written to the selected bit.
- This parameter can be one of the GPIO_PinState enum values:
- @arg GPIO_PIN_RESET: to clear the port pin
- @arg GPIO_PIN_SET: to set the port pin
- @retval None
*/
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
③HAL_GPIO_TogglePin():翻转GPIO引脚的状态
/**
- @brief Toggles the specified GPIO pin
- @param GPIOx: where x can be (A..G depending on device used) to select the GPIO peripheral
- @param GPIO_Pin: Specifies the pins to be toggled.
- @retval None
*/
void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
1.2、按键点亮LED灯(轮询法)
输入(按键): KEY1:PA0 KEY2:PA1
输出(LED灯): LED1:PB8 LED2:PB9
#define KEY_ON 1
#define KEY_OFF 0
//按键扫描函数
uint8_t key_Scan(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
{
if(HAL_GPIO_ReadPin(GPIOx,GPIO_Pin) == GPIO_PIN_RESET)
{
//按键按下
while(HAL_GPIO_ReadPin(GPIOx,GPIO_Pin) == GPIO_PIN_RESET);//消抖
return KEY_ON;
}
else
{
//按键松开
return KEY_OFF;
}
}
int main(void)
{
while (1)
{
if(key_Scan(GPIOA, GPIO_PIN_0) ==KEY_ON)//按键1被按下,翻转LED1的电平
{
HAL_GPIO_TogglePin(GPIOB, LED1_Pin);
}
if(key_Scan(GPIOA, GPIO_PIN_1) ==KEY_ON)//按键2被按下,翻转LED2的电平
{
HAL_GPIO_TogglePin(GPIOB, LED2_Pin);
}
}
}
1.3、按键点亮LED灯(中断法)
使用CubeMX生成代码工程
- 配置时钟 2. 配置GPIO口 3. 使能中断 4. 配置工程
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)//中断回调函数
{
switch(GPIO_Pin)
{
HAL_Delay(50);//按键消抖
case KEY1_Pin: //如果检测中断来自按键1,翻转LED1灯的电平
if(HAL_GPIO_ReadPin(GPIOA, KEY1_Pin) ==GPIO_PIN_RESET)
{
HAL_GPIO_TogglePin(GPIOB, LED1_Pin);
}
break;
case KEY2_Pin: //如果检测中断来自按键2,翻转LED2灯的电平
if(HAL_GPIO_ReadPin(GPIOA, KEY2_Pin) ==GPIO_PIN_RESET)
{
HAL_GPIO_TogglePin(GPIOB, LED2_Pin);
}
break;
}
}
1.4、外接传感器实战
例如:使用振动传感器,控制LED的亮灭,同样采用中断的方式;
需求:当振动传感器接收到振动信号时,使用中断方式点亮LED1。
振动传感器介绍:
单片机供电VCC GND接单片机
产品不震动,输出高电平,模块上的DO口产品震动,输出低电平,绿色指示灯亮
AO口不用

第一种方法:点灯操作放在中断回调函数内
//重写中断服务函数,如果检测到EXTI中断请求,则进入此函数
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
//一根中断线上接有多个中断源,判断中断请求是否来自PA4
if(GPIO_Pin == GPIO_PIN_4)
{
//如果检测到PA4被拉低
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_4) == GPIO_PIN_RESET)
{
//则点亮LED1
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET);
//延时1秒
HAL_Delay(1000);
//关闭LED1
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);
}
else
{
//未检测到PA4被拉低,则关闭LED1
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);
}
}
}
错误提示:如果直接在中断服务函数里调用 HAL_Delay 函数,则会造成系统卡死。
原因:程序初始化时默认把滴答定时器的中断优先级设为最低,其它中断源很容易打断它导致卡死。
解决:在 main 函数里使用以下函数提高滴答定时器的中断优先级(提升至0),同时降低振动模块的中断优先等级。
HAL_NVIC_SetPriority(SysTick_IRQn,0,0);
第一种方法:点灯操作放在主函数while()内,利用标志位将震动信号传递给主函数
#define vibration_ON 1 //宏定义0和1,增加代码的可读性
#define vibration_OFF 0
uint8_t vibration_Flay =vibration_OFF;//振动标志位
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) //重写中断回调函数,如果检测到EXIT中断请求,进入此函数
{
//一根中断线上接有多个中断源,判断中断请求是否来自PA4
if(GPIO_Pin == GPIO_PIN_4)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_4) == GPIO_PIN_RESET) //如果检测到PA4被拉低
{
vibration_Flay =vibration_ON; //标志位置1
}
}
}
int main(void)
{
while (1)
{
if(vibration_Flay ==vibration_ON)
{
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_7,GPIO_PIN_RESET);
HAL_Delay(3000);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_7,GPIO_PIN_SET);
vibration_Flay =vibration_OFF;
}
}
}
1.5、综合实战——报警系统
项目需求
点击遥控器 A 按键,系统进入警戒模式,一旦检测到震动,则喇叭发出声响报警。
点击遥控器 B 按键,系统退出警戒模式,再怎么摇晃系统都不会报警。
项目框图

项目设计
//如果检测到PA4被拉低,并且警报模式打开
//则将PB7拉低,继电器通电,喇叭一直响
// 如果检测到PA5被拉高(按键A按下),设定为开启警报模式
// 则将PB7拉低(喇叭响),2秒后恢复电平(喇叭不响),表示进入警报模式
// 同时将标志位设置为ON
// 如果检测到PA6被拉高(按键B按下),设定为关闭警报模式
// 则将PB7拉低(喇叭响),1秒后恢复电平(喇叭不响),表示关闭警报模式
// 同时将标志位设置为OFF
编程实现
#define vibration_ON 1
#define vibration_OFF 0
#define J_ON 1
#define J_OFF 0
uint8_t vibration_Flay =vibration_OFF;
uint8_t mark =J_OFF;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) //重写中断回调函数,如果检测到EXIT中断请求,则进入此函数
{
switch (GPIO_Pin)
{
case GPIO_PIN_4:
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_4) == GPIO_PIN_RESET) //如果检测到PA4被拉低
{
vibration_Flay =vibration_ON; //标志位置1
}
break;
case GPIO_PIN_5:
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == GPIO_PIN_SET) //如果检测到PA5被拉高
{
mark =J_ON; //报警标志位置1
HAL_GPIO_WritePin(GPIOB,LED1_Pin,GPIO_PIN_RESET); //点亮LED1
HAL_GPIO_WritePin(GPIOB,LED2_Pin,GPIO_PIN_SET);
}
break;
case GPIO_PIN_6:
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_6) == GPIO_PIN_SET) //如果检测到PA5被拉高
{
mark =J_OFF; //报警标志位置0
HAL_GPIO_WritePin(GPIOB,LED2_Pin,GPIO_PIN_RESET); //点亮LED2
HAL_GPIO_WritePin(GPIOB,LED1_Pin,GPIO_PIN_SET);
}
break;
}
}
int main(void)
{
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(vibration_Flay ==vibration_ON && mark ==J_ON)
{
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_7,GPIO_PIN_RESET);
HAL_Delay(3000);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_7,GPIO_PIN_SET);
vibration_Flay =vibration_OFF;
}
}
}
二、定时器TIM——定时与计数
2.1、定时器的基本原理
使用精准的时基,通过硬件的方式,实现定时功能。定时器核心就是计数器。

2.2、定时器分类
| 定时器类型 | 主要功能 |
|---|---|
| 基本定时器(TIM6~TIM7) | m没有输入输出通道,常用作时基,即定时功能 |
| 通用定时器(TIM2~TIM5) | 具有多路独立通道,可用于输入捕获/输出比较,也可以用作时基 |
| 高级定时器(TIM1和TIM8) | c除具备通用定时器所有功能外,还具备带死区控制的互补信号输出、刹车输入等功能 |
通用定时器介绍:
1) 16 位向上、向下、向上/向下自动装载计数器(TIMx_CNT)。
2) 16 位可编程(可以实时修改)预分频器(TIMx_PSC),计数器时钟频率的分频系数为 1~65535 之间的
任意数值。
3)4 个独立通道(TIMx_CH1~4),这些通道可以用来作为:
A.输入捕获
B.输出比较
C.PWM 生成(边缘或中间对齐模式)
D.单脉冲模式输出
4)可使用外部信号(TIMx_ETR)控制定时器和定时器互连(可以用 1 个定时器控制另外一个定时器)
的同步电路。
5)如下事件发生时产生中断/DMA:
A.更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发)
B.触发事件(计数器启动、停止、初始化或者由内部/外部触发计数)
C.输入捕获
D.输出比较
E.支持针对定位的增量(正交)编码器和霍尔传感器电路
F.触发输入作为外部时钟或者按周期的电流管理
2.3、定时器计数模式
通用定时器可以向上计数、向下计数、向上向下双向计数模式。
①向上计数模式:计数器从0计数到自动加载值(TIMx_ARR),然后重新从0开始计数并且产生一个计数器溢出事件。
②向下计数模式:计数器从自动装入的值(TIMx_ARR)开始向下计数到0,然后从自动装入的值重新开始,并产生一个 计数器向下溢出事件。
③中央对齐模式(向上/向下计数):计数器从0开始计数到自动装入的值-1,产生一个计数器溢出事件,然后向下计数到1并且产生一个计数器溢出事件;然后再从0开始重新计数。

2.4、定时器溢出时间计算公式
Tout=(PSC+1)∗(ARR+1)TCLK T_{out}=\frac{(PSC+1)*(ARR+1)}{T_{CLK}} Tout=TCLK(PSC+1)∗(ARR+1)
多+1的原因是计算机计数从0开始
PSC:预分频数 ARR:装载值 Tclk:定时器的时钟频率
2.5、定时器中断实验——翻转LED灯
项目需求
使用定时器中断方法,每500ms翻转一次LED1灯状态。
实验过程
①配置RCC ②对LED灯引脚配置 ③时钟数配置
④TIM2配置



代码实现
//重写更新中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim ->Instance == TIM2) //判断中断是否来自TIM2
{
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_8);
}
}
int main()
{
HAL_TIM_Base_Start_IT(&htim2); //启动定时器,定时0.5秒
}
2.6、定时器计数实验——超声波传感器实战
超声波传感器介绍:

怎么让它发送波 ?
Trig ,给Trig端口至少10us的高电平
怎么知道它开始发了 ?
Echo信号,由低电平跳转到高电平,表示开始发送波
怎么知道接收了返回波 ?
Echo,由高电平跳转回低电平,表示波回来了
怎么算时间 ?
Echo引脚维持高电平的时间!
波发出去的那一下,开始启动定时器
波回来的拿一下,我们开始停止定时器,计算出中间经过多少时间
怎么算距离 ?
距离 = 速度 (340m/s)* 时间/2

项目需求:
使用超声波测距,当手离传感器距离小于5cm时,LED1点亮,否则保持不亮状态。
接线:Trig — PB6 Echo — PB LED1 — PB
定时器配置:
使用 TIM2 ,只用作计数功能,不用作定时,将 PSC 配置为71,则计数 1 次代表 1us 。

代码实现:
//使用TM2来做us级别的延时函数
void TIM2_Delay_us(uint16_t n_us)
{
__HAL_TIM_ENABLE(&htim2);
__HAL_TIM_SetCounter(&htim2,0);
while(__HAL_TIM_GetCounter(&htim2) < n_us);
__HAL_TIM_DISABLE(&htim2);//关闭定时器
}
int main(void)
{
uint32_t cnt =0;
float distance =0;
while (1)
{
//1. Trig ,给Trig端口(PB6)至少10us的高电平
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_6, GPIO_PIN_SET);
TIM2_Delay_us(20);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_6, GPIO_PIN_RESET);
//2. echo(PB7)由低电平跳转到高电平,表示开始发送波
//波发出去的那一下,开始启动定时器
while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_7)== GPIO_PIN_RESET);//PB7拉高后,跳出while循环
HAL_TIM_Base_Start(&htim2);//启动计数器
__HAL_TIM_SetCounter(&htim2,0);//将计数初值重置
//3. 由高电平跳转回低电平,表示波回来了
//波回来的那一下,我们开始停止定时器
while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_7)== GPIO_PIN_SET);//PB7拉低后,跳出while循环
HAL_TIM_Base_Stop(&htim2);//关闭计数器
//4. 计算出中间经过多少时间
cnt = __HAL_TIM_GetCounter(&htim2);
//5. 距离 = 速度 (340m/s)* 时间/2(计数1次表示1us)
distance =cnt*340/2*0.000001*100; //单位cm
if(distance < 5){
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET);
}
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);
//每500毫秒测试一次距离
HAL_Delay(500);
}
}
三、PWM
3.1、PWM介绍
脉冲宽度调制(PWM),是英文“Pulse Width Modulation”的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术,广泛应用在从测量、通信到功率控制与变换的许多领域中。
3.2、PWM输出模式
**PWM 模式1:**在向上计数时,一旦 CNT < CCRx **时输出为有效电平,否则为无效电平;
在向下计数时,一旦 **CNT > CCRx **时输出为无效电平,否则为有效电平。
**PWM 模式2:在向上计数时,一旦 CNT < CCRx **时输出为无效电平,否则为有效电平;
在向 下计数时,一旦 CNT > CCRx 时输出为有效电平,否则为无效电平。

3.3、PWM实验——呼吸灯
项目需求:
使用PWM点亮LED1实现呼吸灯效果。
项目原理:
LED可以越来越亮,也可以越来越暗,这是由不同的占空比决定的,本项目选择一个周期为0.5ms,则PSC=71,ARR=499,即可将定时器配置为0.5ms,接下来就是调节LED在每一个周期内亮的时间了。
实验过程:
LED连的哪一个引脚,看产品手册,选择对应的PWM通道。



代码实现:
int mian()
{
uint16_t pwmVal =0; //调整占空比的参数
uint8_t dir =1; //改变方向,等于1越来越亮;等于0越来越暗
HAL_TIM_PWM_Start(&htim4,TIM_CHANNEL_3); // 使能 Timer4 第3通道 PWM 输出
while (1)
{
HAL_Delay(1);
if(dir ==1)
pwmVal++;
else
pwmVal--;
if(pwmVal>500)
dir =0;
else if(pwmVal ==0)
dir =1;
__HAL_TIM_SetCompare(&htim4,TIM_CHANNEL_3,pwmVal); //修改比较值CCRx,修改占空比
}
}
3.4、PWM实验——SG90舵机控制
sg90舵机介绍:

确定周期:
PWM波的频率不能太高,大约50HZ,即周期=1/频率=1/50=0.02s,20ms左右,则定时器设置:PSC=7199,ARR=199。
角度控制:
0.5ms-------------0度; 2.5% 对应函数中CCRx为5
1.0ms------------45度; 5.0% 对应函数中CCRx为10
1.5ms------------90度; 7.5% 对应函数中CCRx为15
2.0ms-----------135度; 10.0% 对应函数中CCRx为20
2.5ms-----------180度; 12.5% 对应函数中CCRx为25

项目需求:
每隔1s,转动一个角度:0度 --> 45度 --> 90度 --> 135度 --> 180度 --> 0度

代码实现
int main()
{
HAL_TIM_PWM_Start(&htim4,TIM_CHANNEL_3); // 使能 Timer4 第3通道 PWM 输出
while (1)
{
HAL_Delay(1000);
__HAL_TIM_SetCompare(&htim4,TIM_CHANNEL_3,5);// 转动0度
HAL_Delay(1000);
__HAL_TIM_SetCompare(&htim4,TIM_CHANNEL_3,10);// 转动45
HAL_Delay(1000);
__HAL_TIM_SetCompare(&htim4,TIM_CHANNEL_3,15);// 转动90度
HAL_Delay(1000);
__HAL_TIM_SetCompare(&htim4,TIM_CHANNEL_3,20);// 转动135度
HAL_Delay(1000);
}
}
四、串口
4.1、USART和UART基本概述
USART(universal synchronous asynchronous receiver and transmitte): 通用同步异步收发器
USART是一个串行、异步的通信设备,可以灵活地与外部设备进行全双工数据交换。
UART(universal asynchronous receiver and transmitter): 通用异步收发器
异步串行通信口(UART)就是我们在嵌入式中常说的串口,它还是一种通用的数据通信议。
区别:
USART是指单片机的一个端口模块,可以根据需要配置成同步模式(SPI,I2C),也可以将其配置为异步模式,后者就是UART。所以说UART姑且可以称之为一个与SPI,I2C对等的“协议”,而USART则不是一个协议,而是更应该理解为一个实体。相比于同步通讯,UART不需要统一的时钟线,接线更加方便。但是,为了正常的对信号进行解码,使用UART通讯的双方必须事先约定好波特率,即单位事件内传输码元的个数。
4.2、非中断串口函数介绍
非中断的串口发送/接收函数:
HAL_UART_Transmit(); //串口发送数据,使用超时管理机制
HAL_UART_Receive(); //串口接收数据,使用超时管理机制
/**
* @brief Receives an amount of data in blocking mode.
* @note When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01),
* the received data is handled as a set of u16. In this case, Size must indicate the number
* of u16 available through pData.
* @param huart Pointer to a UART_HandleTypeDef structure that contains
* the configuration information for the specified UART module.
* @param pData Pointer to data buffer (u8 or u16 data elements).
* @param Size Amount of data elements (u8 or u16) to be received.
* @param Timeout Timeout duration
* @retval HAL status
*/
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
//作用:以阻塞的方式接收指定字节的数据
//形参 1:UART_HandleTypeDef 结构体类型指针变量
//形参 2:指向要接收缓存数组的地址
//形参 3:要接收的数据大小,以字节为单位
//形参 4:设置的超时时间,以ms单位
/**
* @brief Sends an amount of data in blocking mode.
* @note When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01),
* the sent data is handled as a set of u16. In this case, Size must indicate the number
* of u16 provided through pData.
* @param huart Pointer to a UART_HandleTypeDef structure that contains
* the configuration information for the specified UART module.
* @param pData Pointer to data buffer (u8 or u16 data elements).
* @param Size Amount of data elements (u8 or u16) to be sent
* @param Timeout Timeout duration
* @retval HAL status
*/
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout)
//作用:以阻塞的方式发送指定字节的数据
//形参 1:UART_HandleTypeDef 结构体类型指针变量
//形参 2:指向要发送的数据地址
//形参 3:要发送的数据大小,以字节为单位
//形参 4:设置的超时时间,以ms单位
4.3、非中断串口函数介绍
中断的串口发送/接收函数:
HAL_UART_Transmit_IT(); //串口中断模式发送
HAL_UART_Receive_IT(); //串口中断模式接收
/**
* @brief Receives an amount of data in non blocking mode.
* @note When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01),
* the received data is handled as a set of u16. In this case, Size must indicate the number
* of u16 available through pData.
* @param huart Pointer to a UART_HandleTypeDef structure that contains
* the configuration information for the specified UART module.
* @param pData Pointer to data buffer (u8 or u16 data elements).
* @param Size Amount of data elements (u8 or u16) to be received.
* @retval HAL status
*/
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
//作用:以中断的方式接收指定字节的数据
//形参 1 是 UART_HandleTypeDef 结构体类型指针变量
//形参 2 是指向接收数据缓冲区
//形参 3 是要接收的数据大小,以字节为单位
//注意:此函数执行完后将清除中断,需要再次调用以重新开启中断。
/**
* @brief Sends an amount of data in non blocking mode.
* @note When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01),
* the sent data is handled as a set of u16. In this case, Size must indicate the number
* of u16 provided through pData.
* @param huart Pointer to a UART_HandleTypeDef structure that contains
* the configuration information for the specified UART module.
* @param pData Pointer to data buffer (u8 or u16 data elements).
* @param Size Amount of data elements (u8 or u16) to be sent
* @retval HAL status
*/
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size)
//作用:以中断的方式发送指定字节的数据
//形参 1 是 UART_HandleTypeDef 结构体类型指针变量
//形参 2 是指向发送数据缓冲区
//形参 3 是要发送的数据大小,以字节为单位
//注意:此函数执行完后将清除中断,需要再次调用以重新开启中断。
串口中断回调函数:
HAL_UART_IRQHandler(UART_HandleTypeDef *huart); //串口中断处理函数
HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart); //发送中断回调函数
HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart); //接收中断回调函数
状态标记变量:
从0开始,串口中断接收到一个数据(一个字节)就自增1。当数据读取全部OK时候(回车和换行符号来的时候),那么 USART_RX_STA (uint16_t) 的最高位置1,表示串口数据接收全部完毕了,然后main函数里面可以处理数据了。
| bit15 | bit14 | bit13~0 |
|---|---|---|
| 接收完成标志 | 接收到0x0D(回车) | 收到的有效数据个数 |
4.4、串口接收中断流程

4.5、串口实验(非中断实验)
串口配置:
①选定串口

②将串口配置为异步通讯

③串口参数配置

④使用MicroLIB库,从魔术棒打开,这个勾勾一定要打上,否则 print函数f 无法重映射!

⑤、编程实现
#include <stdio.h>
#include <string.h>
unsigned char ch[20] = {0};
//Printf重定向,修改fputc()函数的内容,重写这个fputc函数,记得要使用<stdio.h>库
int fputc(int ch ,FILE *f)
{
unsigned char temp[1] ={ch};//长度为1,一次只传递一个字符进来
HAL_UART_Transmit(&huart1,temp, 1,0xffff);//一个字符一个字符的发送
return ch;
}
int main(void)
{
HAL_UART_Transmit(&huart1,(const uint8_t*) "hello world\n",strlen("hello world\n"),100);
while (1)
{
HAL_UART_Receive(&huart1,ch,19,100);
// HAL_UART_Transmit(&huart1,ch, strlen((const char*) ch),100);
printf("%s",ch); //利用printf函数向串口发送数据
memset(ch,0,strlen((const char*)ch)); //清空数组
}
}
4.6、串口实验(中断实现)
串口配置
①前四步同上;(①②③④)
⑤开启串口中断

⑥编程实现
#include "stdio.h"
#include "string.h"
#define UART1_REC_LEN 200 //定义最大接收字节数 200,可根据需求调整
uint8_t buf=0; //串口接收缓存(1个字节)
uint8_t UART1_RX_Buffer[UART1_REC_LEN]; // 接收缓冲, 串口接收到的数据放在这个数组里,最大UART1_REC_LEN个字节
uint16_t UART1_RX_STA=0;// 接收状态, bit15, 接收完成标志,bit14, 接收到0x0d,bit13~0, 接收到的有效字节数目
//重写虚函数——接收回调函数,收到一个数据后,在这里处理
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 判断中断是由哪个串口触发的
if(huart->Instance == USART1)
{
// 判断接收是否完成(UART1_RX_STA bit15 位是否为1),不为1的话,表示接收未完成
if((UART1_RX_STA & 0x8000) ==0)
{
// 如果已经收到了 0x0d (回车)
if((UART1_RX_STA & 0x4000) >1)
{
// 则接着判断是否收到 0x0a (换行)
if(buf ==0x0a)
{
// 如果 0x0a 和 0x0d 都收到,则将 bit15 位置为1
UART1_RX_STA |= 0x8000;
}
else
{
// 否则认为接收错误,重新开始
UART1_RX_STA =0;
}
}
// 如果没有收到了 0x0d (回车)
else
{
//则先判断收到的这个字符是否是 0x0d (回车)
if(buf ==0x0d)
{
// 是的话则将 bit14 位置为1
UART1_RX_STA |=0x4000;
}
else
{
// 否则将接收到的数据保存在缓存数组里
UART1_RX_Buffer[UART1_RX_STA & 0x3fff] =buf;
UART1_RX_STA++;
// 如果接收数据大于UART1_REC_LEN(200字节),则重新开始接收
if(UART1_RX_STA > UART1_REC_LEN-1)
{
UART1_RX_STA =0;
}
}
}
}
// 重新开启中断
HAL_UART_Receive_IT(&huart1, &buf, 1);
}
}
//重写fputc函数,可以直接调用printf函数向窗口发送数据
int fputc(int ch, FILE *f)
{
unsigned char temp[1]={ch};
HAL_UART_Transmit(&huart1,temp,1,0xffff);
return ch;
}
int main(void)
{
HAL_UART_Receive_IT(&huart1,&buf,1);//开启接收中断,接收到一个字符,发生一次中断
while (1)
{
// 如果接收完成(UART1_RX_STA bit15 位为1),向串口发送收到的数据
if(UART1_RX_STA & 0x8000)
{
printf("收到的数据:");
// 将收到的数据发送到串口
HAL_UART_Transmit(&huart1,UART1_RX_Buffer,UART1_RX_STA & 0x3fff,0xffff);
// 等待发送完成
while(huart1.gState != HAL_UART_STATE_READY);
printf("\r\n");
// 重新开始下一次接收
UART1_RX_STA = 0;
}
}
五、独立看门狗(IWDG)
5.1、独立看门狗概述
在由单片机构成的微型计算机系统中,由于单片机的工作常常会受到来自外界电磁场的干扰,造成程序的跑飞,而陷入死循环,程序的正常运行被打断,由单片机控制的系统无法继续工作,会造成整个系统的陷入停滞状态,发生不可预料的后果,所以出于对单片机运行状态进行实时监测的考虑,便产生了一种专门用于监测单片机程序运行状态的模块或者芯片,俗称**“看门狗”** (watchdog) 。
独立看门狗工作在主程序之外,能够完全独立工作,它的时钟是专用的低速时钟(LSI),由 VDD 电压供电, 在停止模式和待机模式下仍能工作。本质是一个 12 位的递减计数器,当计数器的值从某个值一直减到0的时候,系统就会产生一个复位信号,即 IWDG_RESET 。 如果在计数没减到0之前,刷新了计数器的值的话,那么就不会产生复位信号,这个动作就是我们经常说的喂狗。

独立看门狗的时钟由独立的RC振荡器LSI提供,即使主时钟发生故障它仍然有效,非常独立。启用IWDG后,LSI时钟会自动开启。LSI时钟频率并不精确,使用40kHz。LSI经过一个8位的预分频器得到计数器时钟。
注意:独立看门狗(IWDG)是独立于系统之外,因为有独立时钟,所以不受系统影响的系统故障探测器。主要用于监视硬件错误。
5.2、IWDG 相关寄存器介绍
①预分频寄存器(IWDG_PR)

在上图中可以看出,32位寄存器IWDG_PR中,只有低 3 位是可以读写的,分频系数算法:
PSC=4∗2prer
PSC=4*2^{prer}
PSC=4∗2prer
其中prer是IWDG_PR的值,如当PSC=8时,prer=001,即prer=1。
②重装载寄存器(IWDG_RLR)
重装载寄存器是一个12位的寄存器,用于存放重装载值,低12位有效,即最大值为4096,这个值的大小决定着独立看门狗的溢出时间。

③键寄存器(IWDG_KR)
键寄存器IWDG_KR可以说是独立看门狗的一个控制寄存器,主要有三种控制方式,往这个寄存器写入下面三个不同的值有不同的效果。
(1)如期望更改预分频值和重装载值,则往该寄存器写入0x5555;
(2)如期望启动看门狗,则往该寄存器写入0xCCCC;
(3)如期望将重装载值传递给递减计数器,即通常所说的“喂狗”,则往该寄存器写入0xAAAA;如下图所示,键寄存器写 入0xAAAA后,预装载值将传递给递减寄存器;

5.3、溢出时间计算公式
Tout=PSC∗RLRfIWDG T_{out}=\frac{PSC*RLR}{f_{IWDG}} Tout=fIWDGPSC∗RLR
- Tout是IWDG超时时间(没喂狗)
- Fiwdg是WWDG的时钟频率(一般为40kHz)
- RLR 是重装载寄存器重装的数,低12位有效,即最大值为4096
5.4、独立看门狗实验
项目需求:
开启独立看门狗,溢出时间为1秒,使用按键1进行喂狗。
硬件连线:
KEY1 – PA0 UART1 – PA9/PA10
溢出时间计算:
PSC =64 ,f_IWDG =40kHz ,则 PLR =625。

编程实现:
#include "string.h"
int main(void)
{
HAL_UART_Transmit(&huart1,(const unsigned char*)"程序启动\n",strlen("程序启动\n"),100);
while (1)
{
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == GPIO_PIN_RESET)//通过按键喂狗
{
HAL_IWDG_Refresh(&hiwdg);//喂狗
HAL_Delay(50);
}
}
}
六、窗口看门狗(WWDG)
6.1、窗口看门狗概述
窗口看门狗用于监测单片机程序运行时效是否精准,主要检测软件异常,一般用于需要精准检测程序运行时间的场合。窗口看门狗的本质是一个能产生系统复位信号和提前唤醒中断的6位计数器。
根据系统时钟频率,装载一个初始值到向下计数器(假设是1000),并且设置一个窗口值(小于装载到计数器的初始值,假设是500),窗口看门狗一般会定死窗口下线值是64。计数器从1000开始向下减,在减到500之前(1000到500间),是不允许你去喂狗的,一旦喂狗,就会产生复位信号。只有计数器值减到上限值之后(500到64),才允许你去喂狗。当计数器减到下限值(64到0之间),如果喂狗,也会产生复位信号,当减到0之后,自动产生复位信号。
所以窗口看门狗实际上就是设置一个窗口(上下限),在这个范围内,你才允许你去喂狗,只要不在这个范围之内,都会复位。
注意:窗口看门狗(WWDG)是系统内部的故障探测器,时钟与系统相同。如果系统时钟不走了,这个狗也就失去作用了。主要用于监视软件错误。
6.2、窗口看门狗工作原理

**产生复位条件: **
当递减计数器值从 0x40 减到 0x3F 时复位(即T6位跳变到0)
计数器的值大于 W[6:0] 值时喂狗会复位。
**产生中断条件: **
当递减计数器等于 0x40 时可产生提前唤醒中断 (EWI)。
**在窗口期内重装载计数器的值,防止复位,也就是所谓的喂狗。 **
6.3、WWDG相关寄存器介绍

①控制寄存器(WWDG_CR)


②配置寄存器(WWDG_CFR)

③状态寄存器(WWDG_SR)

6.4、溢出时间计算公式
Tout=4096∗2WDGTB∗(T[5:0]+1)fWWDG T_{out}=\frac{4096*2^{WDGTB}*(T[5:0]+1)}{f_{WWDG}} Tout=fWWDG4096∗2WDGTB∗(T[5:0]+1)
- Tout是WWDG超时时间(没喂狗)
- Fwwdg是WWDG的时钟源频率(最大36MHz)
- 4096是WWDG固定的预分频系数
- 2^WDGTB是WWDG_CFR寄存器设置的预分频系数值
- T[5:0]是WWDG计数器低6位,最多63
6.5、窗口看门狗实验
项目需求:
开启窗口看门狗,计数器值设置为 0X7F ,窗口值设置为 0X5F ,预分频系数为 8 。程序启动时点亮 LED1 ,300ms 后熄灭。在提前唤醒中断服务函数进行喂狗,同时翻转 LED2 状态。

硬件接线:
LED1 – PB8 LED2 – PB9
WWDG配置:


编程实现:
void HAL_WWDG_EarlyWakeupCallback(WWDG_HandleTypeDef *hwwdg)//窗口看门狗中断回调函数
{
HAL_WWDG_Refresh(hwwdg); //喂狗
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_9);//翻转led2
}
int main(void)
{
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_8,GPIO_PIN_RESET);
HAL_Delay(300);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_8,GPIO_PIN_SET);//点亮led1
while (1)
{
HAL_Delay(40);
}
}
6.6、IWDG和WWDG的异同点
| 对比点 | 独立看门狗 | 窗口看门狗 |
|---|---|---|
| 时钟源 | 独立时钟、LSI(40kHz),不精确 | PCLK1或PCLK3(最大36MHz),精确 |
| 复位条件 | 递减计数到0 | 窗口期外喂狗或减到0x3F |
| 中断 | 没有中断 | 计数值减到0x40可产生中断 |
| 递减计数器位数 | 12位(最大计数范围:4096~0) | 7位(最大计数范围:127~63) |
| 应用场合 | 防止程序跑飞,死循环,死机 | 检测程序时效,防止软件异常 |
七、DMA
7.1、DMA概述
DMA(Direct Memory Access,直接存储器访问) 提供在外设与内存、存储器和存储器、外设与外设之间的高速数据传输使用。它允许不同速度的硬件装置来沟通,而不需要依赖于CPU,在这个时间中,CPU对于内存的工作来说就无法使用。
**简单描述: DMA就是一个数据搬运工!! **
DMA的意义: **代替 CPU 搬运数据,为 CPU 减负。 **
- 数据搬运的工作比较耗时间;
- 数据搬运工作时效要求高(有数据来就要搬走);
- 没啥技术含量(CPU 节约出来的时间可以处理更重要的事)。
搬运什么数据?
**存储器、外设:**这里的外设指的是spi、usart、iic、adc 等基于APB1 、APB2或AHB时钟的外设,而这里的存储器包括自身的闪存(flash)或者内存(SRAM)以及外设的存储设备都可以作为访问地源或者目的。
三种搬运方式:
①存储器→存储器(例如:复制某特别大的数据buf)

②存储器→外设 (例如:将某数据buf写入串口TDR寄存器)

③外设→存储器 (例如:将串口RDR寄存器写入某数据buf)

7.2、DMA基本原理
**DMA控制器:**STM32F103有2个 DMA 控制器,DMA1有7个通道,DMA2有5个通道。 一个通道每次只能搬运一个外设的数据!! 如果同时有多个外设的 DMA 请求,则按照优先级进行响应。
DMA1有7个通道:

DMA2有5个通道:

DMA通道的优先级:
优先级管理采用软件+硬件:
软件: 每个通道的优先级可以在DMA_CCRx寄存器中设置,有4个等级;
最高级>高级>中级>低级
硬件: 如果2个请求,它们的软件优先级相同,则较低编号的通道比较高编号的通道有较高的优先权。
比如:如果软件优先级相同,通道2优先于通道4
DMA传输方式:
DMA_Mode_Normal(正常模式):一次DMA数据传输完后,停止DMA传送 ,也就是只传输一次
DMA_Mode_Circular(循环传输模式):当传输结束时,硬件自动会将传输数据量寄存器进行重装,进行下一轮的数据传输。 也就是多次传输模式
指针递增模式:
外设和存储器指针在每次传输后可以自动向后递增或保持常量。当设置为增量模式时,下一个要传输的地址将是前一个地址加上增量值。如下图所示,一种是源数据和目标数据的指针同时增;另一种是源数据指针增,目标数据指针保持不变,例如目标数据存在串口,串口存储数据就一个寄存器(USART_DR),它是一个32位的寄存器,但是只有0~8位用于存储数据,数据一位一位的进行传输,因为该寄存器的地址是固定的,所以指针无需增。

7.3、实验1——内存到内存的搬运
实验要求:
使用DMA的方式将数组A的内容复制到数组B中,搬运完之后将数组B的内容打印到屏幕。
CubeMX配置:
如下是DMA配置,其余串口,RCC,GPIO,SYS配置省略。

用到的库函数:
①HAL_DMA_Start();
/**
* @brief Start the DMA Transfer.
* @param hdma: pointer to a DMA_HandleTypeDef structure that contains
* the configuration information for the specified DMA Channel.
* @param SrcAddress: The source memory Buffer address
* @param DstAddress: The destination memory Buffer address
* @param DataLength: The length of data to be transferred from source to destination
* @retval HAL status
*/
HAL_StatusTypeDef HAL_DMA_Start(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength)
//参数一:DMA_HandleTypeDef *hdma,DMA通道句柄
//参数二:uint32_t SrcAddress,源内存地址
//参数三:uint32_t DstAddress,目标内存地址
//参数四:uint32_t DataLength,传输数据长度。注意:需要乘以sizeof(uint32_t),因为DMA传输设置的数据长度是字节为单位,一个uint32_t类型的数据长度为4个字节。
//返回值:HAL_StatusTypeDef,HAL状态(OK,busy,ERROR,TIMEOUT)
②__HAL_DMA_GET_FLAG();
/**
* @brief Get the DMA Channel pending flags.
* @param __HANDLE__: DMA handle
* @param __FLAG__: Get the specified flag.
* This parameter can be any combination of the following values:
* @arg DMA_FLAG_TCx: Transfer complete flag
* @arg DMA_FLAG_HTx: Half transfer complete flag
* @arg DMA_FLAG_TEx: Transfer error flag
* @arg DMA_FLAG_GLx: Global interrupt flag
* Where x can be 1_7 to select the DMA Channel flag.
* @retval The state of FLAG (SET or RESET).
*/
#define __HAL_DMA_GET_FLAG(__HANDLE__, __FLAG__) (DMA1->ISR & (__FLAG__))
//参数一:HANDLE,DMA通道句柄
//参数二:FLAG,数据传输标志。DMA_FLAG_TCx表示数据传输完成标志
//返回值:FLAG的值(SET/RESET)
代码实现:
- 开启数据传输
- 等待数据传输完成
- 打印数组内容
#include "stdio.h"
#define BUF_SIZE 16
//源数组
uint32_t srcBuf[BUF_SIZE] =
{
0x00000000,0x11111111,0x22222222,0x33333333,
0x44444444,0x55555555,0x66666666,0x77777777,
0x88888888,0x99999999,0xAAAAAAAA,0xBBBBBBBB,
0xCCCCCCCC,0xDDDDDDDD,0xEEEEEEEE,0xFFFFFFFF
};
//目标数组
uint32_t desBuf[BUF_SIZE];
//重定义printf函数,记得在魔术棒里勾选Use MicroLIB
int fputc(int ch, FILE *f)
{
unsigned char temp[1]={ch};
HAL_UART_Transmit(&huart1,temp,1,0xffff);
return ch;
}
int main(void)
{
uint8_t i =0;
//开启数据传输
HAL_DMA_Start(&hdma_memtomem_dma1_channel1,(uint32_t)srcBuf,(uint32_t)desBuf,sizeof(uint32_t)*BUF_SIZE);//注意最后一个参数是传输的字节数,uint32_t就是4个字节,一共16个
//等待数据传输完成
while(__HAL_DMA_GET_FLAG(&hdma_memtomem_dma1_channel1,DMA_FLAG_TC1) ==RESET)
HAL_Delay(10);
//打印数组内容
for(i =0;i <BUF_SIZE;i ++)
{
printf("Buf[%d] =%X\r\n",i ,desBuf[i]);
}
}
7.4、实验2——内存到外设的搬运
实验要求:
使用DMA的方式将内存数据搬运到串口1发送寄存器,同时闪烁LED1。
CubeMX配置:
如下是DMA配置,其余串口,RCC,GPIO,SYS配置省略。

用到的库函数:
①HAL_UART_Transmit_DMA();
/**
* @brief Sends an amount of data in DMA mode.
* @note When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01),
* the sent data is handled as a set of u16. In this case, Size must indicate the number
* of u16 provided through pData.
* @param huart Pointer to a UART_HandleTypeDef structure that contains
* the configuration information for the specified UART module.
* @param pData Pointer to data buffer (u8 or u16 data elements).
* @param Size Amount of data elements (u8 or u16) to be sent
* @retval HAL status
*/
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size)
//参数一:UART_HandleTypeDef *huart,串口句柄
//参数二:uint8_t *pData,待发送数据首地址
//参数三:uint16_t Size,待发送数据长度
//返回值:HAL_StatusTypeDef,HAL状态(OK,busy,ERROR,TIMEOUT)
代码实现:
- 准备数据
- 将数据通过串口DMA发送
- 打印数组内容
#define BUF_SIZE 100
unsigned char sendBuf[BUF_SIZE] ={0}; //待发送的数据
int main(void)
{
uint8_t i =0;
//准备数据
for(i =0; i<BUF_SIZE ;i++)
{
sendBuf[i] ='A';
}
//将数据通过串口DMA发送
HAL_UART_Transmit_DMA(&huart1,sendBuf,BUF_SIZE);
while (1)
{
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_8);
HAL_Delay(100);
}
}
7.5、实验3——外设到内存的搬运
实验要求:
使用DMA的方式将串口接收缓存寄存器的值搬运到内存中,同时闪烁LED1。
CubeMX配置:
如下是DMA配置,其余串口,RCC,GPIO,SYS配置省略。

如下是串口中断配置。

用到的库函数:
①__HAL_UART_ENABLE_IT();
/** @brief Enable the specified UART interrupt.
* @param __HANDLE__ specifies the UART Handle.
* UART Handle selects the USARTx or UARTy peripheral
* (USART,UART availability and x,y values depending on device).
* @param __INTERRUPT__ specifies the UART interrupt source to enable.
* This parameter can be one of the following values:
* @arg UART_IT_CTS: CTS change interrupt
* @arg UART_IT_LBD: LIN Break detection interrupt
* @arg UART_IT_TXE: Transmit Data Register empty interrupt
* @arg UART_IT_TC: Transmission complete interrupt
* @arg UART_IT_RXNE: Receive Data register not empty interrupt
* @arg UART_IT_IDLE: Idle line detection interrupt
* @arg UART_IT_PE: Parity Error interrupt
* @arg UART_IT_ERR: Error interrupt(Frame error, noise error, overrun error)
* @retval None
*/
#define __HAL_UART_ENABLE_IT(__HANDLE__, __INTERRUPT__) ((((__INTERRUPT__) >> 28U) == UART_CR1_REG_INDEX)? ((__HANDLE__)->Instance->CR1 |= ((__INTERRUPT__) & UART_IT_MASK)): \
(((__INTERRUPT__) >> 28U) == UART_CR2_REG_INDEX)? ((__HANDLE__)->Instance->CR2 |= ((__INTERRUPT__) & UART_IT_MASK)): \
((__HANDLE__)->Instance->CR3 |= ((__INTERRUPT__) & UART_IT_MASK)))
//参数一:HANDLE,串口句柄
//参数二:INTERRUPT,需要使能的中断
//返回值:无
②HAL_UART_Receive_DMA();
/**
* @brief Receives an amount of data in DMA mode.
* @note When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01),
* the received data is handled as a set of u16. In this case, Size must indicate the number
* of u16 available through pData.
* @param huart Pointer to a UART_HandleTypeDef structure that contains
* the configuration information for the specified UART module.
* @param pData Pointer to data buffer (u8 or u16 data elements).
* @param Size Amount of data elements (u8 or u16) to be received.
* @note When the UART parity is enabled (PCE = 1) the received data contains the parity bit.
* @retval HAL status
*/
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
//参数一:UART_HandleTypeDef *huart,串口句柄
//参数二:uint8_t *pData,接收缓存首地址
//参数三:uint16_t Size,接收缓存长度
//返回值:HAL_StatusTypeDef,HAL状态(OK,busy,ERROR,TIMEOUT)
③__HAL_UART_GET_FLAG();
/** @brief Checks whether the specified UART flag is set or not.
* @param __HANDLE__ specifies the UART Handle.
* UART Handle selects the USARTx or UARTy peripheral
* (USART,UART availability and x,y values depending on device).
* @param __FLAG__ specifies the flag to check.
* This parameter can be one of the following values:
* @arg UART_FLAG_CTS: CTS Change flag (not available for UART4 and UART5)
* @arg UART_FLAG_LBD: LIN Break detection flag
* @arg UART_FLAG_TXE: Transmit data register empty flag
* @arg UART_FLAG_TC: Transmission Complete flag
* @arg UART_FLAG_RXNE: Receive data register not empty flag
* @arg UART_FLAG_IDLE: Idle Line detection flag
* @arg UART_FLAG_ORE: Overrun Error flag
* @arg UART_FLAG_NE: Noise Error flag
* @arg UART_FLAG_FE: Framing Error flag
* @arg UART_FLAG_PE: Parity Error flag
* @retval The new state of __FLAG__ (TRUE or FALSE).
*/
#define __HAL_UART_GET_FLAG(__HANDLE__, __FLAG__) (((__HANDLE__)->Instance->SR & (__FLAG__)) == (__FLAG__))
//参数一:HANDLE,串口句柄
//参数二:FLAG,需要查看的FLAG
//返回值:FLAG的值
④__HAL_UART_CLEAR_IDLEFLAG();
/** @brief Clears the UART IDLE pending flag.
* @param __HANDLE__ specifies the UART Handle.
* UART Handle selects the USARTx or UARTy peripheral
* (USART,UART availability and x,y values depending on device).
* @retval None
*/
#define __HAL_UART_CLEAR_IDLEFLAG(__HANDLE__) __HAL_UART_CLEAR_PEFLAG(__HANDLE__)
//参数一:HANDLE,串口句柄
//返回值:无
⑤HAL_StatusTypeDef HAL_UART_DMAStop();
/**
* @brief Stops the DMA Transfer.
* @param huart Pointer to a UART_HandleTypeDef structure that contains
* the configuration information for the specified UART module.
* @retval HAL status
*/
HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart)
//参数一:UART_HandleTypeDef *huart,串口句柄
//返回值:HAL_StatusTypeDef,HAL状态(OK,busy,ERROR,TIMEOUT)
⑥__HAL_DMA_GET_COUNTER();
/**
* @brief Return the number of remaining data units in the current DMA Channel transfer.
* @param __HANDLE__: DMA handle
* @retval The number of remaining data units in the current DMA Channel transfer.
*/
#define __HAL_DMA_GET_COUNTER(__HANDLE__) ((__HANDLE__)->Instance->CNDTR)
//参数一:HANDLE,串口句柄
//返回值:未传输数据大小
//说明:__HAL_DMA_GET_COUNTER(&hdma_usart1_rx)这句话的意思是当前DMA流传输中剩余数据单元的数量,也就是说开启接收后,需要等接收完成才可以去处理数据,即__HAL_DMA_GET_COUNTER(&hdma_usart1_rx) ==0。
代码实现:
**如何判断串口接收是否完成?如何知道串口收到数据的长度? **
使用串口空闲中断(IDLE)!
- 串口空闲时,触发空闲中断;
- 空闲中断标志位由硬件置1,软件清零
具体说明:空闲中断是在检测到在数据收受后,总线上在一个字节的时间内没有再接收到数据时发生。 即串口的RXNE位被置位之后才开始检测,检测到空闲之后,串口的CR1寄存器的IDLE位被硬件置1,必须采用软件将IDLE位清零才能避免反复进入空闲中断。
**利用串口空闲中断,可以用如下流程实现DMA控制的任意长数据接收: **
- 使能IDLE空闲中断;
- 使能DMA接收中断;
- 收到串口接收中断,DMA不断传输数据到缓冲区;
- 一帧数据接收完毕,串口暂时空闲,触发串口空闲中断;
- 在中断服务函数中,清除中断标志位,关闭DMA传输(防止干扰);
- 计算刚才收到了多少个字节的数据。
- 处理缓冲区数据,开启DMA传输,开始下一帧接收。
//main.c
uint8_t recBuf[BUF_SIZE] ={0};//接收数据缓存数组
uint8_t recLen =0;//接收一帧数据的长度
int main(void)
{
__HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);//使能IDLE空闲中断
HAL_UART_Receive_DMA(&huart1,recBuf,BUF_SIZE);//使能DMA接收中断
while (1)
{
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_8);
HAL_Delay(300);
}
}
//main.h
#define BUF_SIZE 100
//stm32f1xx_it.c
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE) ==SET)//如果IDLE标志位是SET状态,进行数据传输
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1);//清除中断标志位(软件清零)
HAL_UART_DMAStop(&huart1);//停止DMA传输,防止干扰
uint8_t temp =__HAL_DMA_GET_COUNTER(&hdma_usart1_rx);//计算未传输数据长度
recLen = BUF_SIZE -temp;//计算数据长度
HAL_UART_Transmit_DMA(&huart1,recBuf,recLen);//发送数据
HAL_UART_Receive_DMA(&huart1,recBuf,BUF_SIZE);//重新开启DMA
}
/* USER CODE END USART1_IRQn 1 */
}
八、ADC
8.1、ADC概述
ADC全称:Analog-to-Digital Converter,指模拟/数字转换器,是指将**连续变量的模拟信号转换为离散的数字信号的器件。**典型的模拟数字转换器将模拟信号转换为表示一定比例电压值的数字信号。

ADC的性能指标:
- 量程:能测量的电压范围
- 分辨率:ADC能辨别的最小模拟量,通常以输出二进制数的位数表示,比如:8、10、12、16位等;位数越多,分辨率越高,一般来说分辨率越高,转化时间越长
- 转化时间:从转换开始到获得稳定的数字量输出所需要的时间称为转换时间
ADC特性:
- 12位精度下转换速度可高达1MHZ
- 供电电压:V SSA :0V,V DDA :2.4V~3.6V
- ADC输入范围:VREF- ≤ VIN ≤ VREF+
- 采样时间可配置,采样时间越长, 转换结果相对越准确, 但是转换速度就越慢
- ADC 的结果可以左对齐或右对齐方式存储在 16 位数据寄存器中
ADC转换通道:
总共2个ADC(ADC1,ADC2),每个ADC有18个转换通道: 16个外部通道、 2个内部通道(温度传感器、内部参考电压)。外部的16个通道在转换时又分为规则通道和注入通道,其中规则通道最多有16路,注入通道最多有4路。
- 规则组:正常排队的人;
- 注入组:有特权的人(军人、孕妇)
8.2、ADC转换顺序
每个ADC只有一个数据寄存器,16个通道一起共用这个寄存器,所以需要指定规则转换通道的转换顺序。规则通道中的转换顺序由三个寄存器控制:SQR1、SQR2、SQR3,它们都是32位寄存器。SQR寄存器控制着转换通道的数目和转换顺序,只要在对应的寄存器位SQx中写入相应的通道,这个通道就是第x个转换。

和规则通道转换顺序的控制一样,注入通道的转换也是通过注入寄存器来控制,只不过只有一个JSQR寄存器来控制,控制关系如下:

注入序列的转换顺序是从JSQx[ 4 : 0 ](x=4-JL[1:0])开始。只有当JL=4的时候,注入通道的转换顺序才会按照JSQ1、JSQ2、JSQ3、JSQ4的顺序执行。
8.3、ADC基本原理
ADC触发方式:
- 通过向控制寄存器ADC-CR2的ADON位写1来开启转换,写0停止转换。
- 也可以通过外部事件(如定时器)进行转换。
ADC引脚:
在框图中最左边的一列是ADC的各个引脚,它们的名称、信号类型和作用见下图:

一般情况下,VDD是3.3V,VSS接地,相对应的,VDDA是3.3V,VSSA也接地,模拟输入信号不要超过VDD(3.3V)。
ADC时钟配置:
框图中标注的来自ADC预分频器的ADCCLK是ADC模块的时钟来源。通常,由时钟控制器提供的ADCCLK时钟和PCLK2(APB2时钟)同步。RCC控制器为ADC时钟提供一个专用的可编程预分频器。

这里需要注意一下,一般情况下:不要让ADC时钟超过14MHz,否则可能不准。也就是说,如果按照默认设置PCLK2为72MHz,此时应为6分频或者8分频。
ADC转化时间:
ADC是挂载在APB2总线(PCLK2)上的,经过分频器得到ADC时钟(ADCCLK),最高 14 MHz。
转换时间=采样时间+12.5个周期
转换时间=采样时间+12.5个周期
转换时间=采样时间+12.5个周期
12.5个周期是固定的,一般我们设置 PCLK2=72M,经过 ADC 预分频器能分频到最大的时钟只能是 12M,采样周期设置为 1.5 个周期,算出最短的转换时间为 1.17us。
ADC转化模式:
①扫描模式
关闭扫描模式:只转换ADC_SQRx或ADC_JSQR选中的第一个通道
打开扫描模式:扫描所有被ADC_SQRx或ADC_JSQR选中的所有通道
②单次转换/连续转换
单次转换:只转换一次
连续转换:转换一次之后,立马进行下一次转换
8.4、常用的库函数
开启ADC 3种模式 ( 轮询模式 中断模式 DMA模式 ):
- HAL_ADC_Start(&hadcx); //轮询模式开启ADC
- HAL_ADC_Start_IT(&hadcx); //中断轮询模式开启ADC
- HAL_ADC_Start_DMA(&hadcx); //DMA模式开启ADC
关闭ADC 3种模式 ( 轮询模式 中断模式 DMA模式 ):
- HAL_ADC_Stop()
- HAL_ADC_Stop_IT()
- HAL_ADC_Stop_DMA()
ADC校准函数 :
- HAL_ADCEx_Calibration_Start(&hadcx);
读取ADC转换值:
- HAL_ADC_GetValue()
等待转换结束函数:
-
HAL_ADC_PollForConversion(&hadc1, 50);
第一个参数为那个ADC,第二个参数为最大等待时间
ADC中断回调函数:
-
HAL_ADC_ConvCpltCallback()
转换完成后回调,DMA模式下DMA传输完成后调用
规则通道及看门狗配置:
HAL_ADC_ConfigChannel() 配置规则组通道
HAL_ADC_AnalogWDGConfig()
8.5、使用ADC读取烟雾传感器的值
CubeMX配置:

代码实现:
#include "stdio.h"
int fputc(int ch, FILE *f)
{
unsigned char temp[1]={ch};
HAL_UART_Transmit(&huart1,temp,1,0xffff);
return ch;
}
int main(void)
{
uint32_t smoke_value =0;
while (1)
{
HAL_ADC_Start(&hadc1);//启动ADC1转换
HAL_ADC_PollForConversion(&hadc1,50);//等待ADC转换完成
smoke_value =HAL_ADC_GetValue(&hadc1);//读取ADC转换数据
printf("smoke_value =%f\r\n",3.3/4096*smoke_value);
HAL_Delay(500);
}
}
九、IIC
9.1、IIC协议概述
IIC全称Inter-Integrated Circuit (集成电路总线)是由PHILIPS公司在80年代开发的两线式串行总线,用于连接微控制器及其外围设备。IIC属于半双工同步通信方式。
特点:
简单性和有效性:由于接口直接在组件之上,因此IIC总线占用的空间非常小,减少了电路板的空间和芯片管脚的数量,降低了互联成本。总线的长度可高达25英尺,并且能够以10Kbps的最大传输速率支持40个组件
**多主控(multimastering):**其中任何能够进行发送和接收的设备都可以成为主总线。一个主控能够控制信号的传输和时钟频率。当然,在任何时间点上只能有一个主控。
构成:
IIC串行总线一般有两根信号线,一根是双向的数据线SDA,另一根是时钟线SCL,其时钟信号是由主控器件产生。所有接到IIC总线设备上的串行数据SDA都接到总线的SDA上,各设备的时钟线SCL接到总线的SCL上。对于并联在一条总线上的每个IC都有唯一的地址。

9.2、IIC协议
IIC总线在传输数据的过程中一共有三种类型信号,分别为:开始信号、结束信号和应答信号。起始位,停止位,数据位,速度,这些信号中,起始信号是必需的,结束信号和应答信号。
起始信号和终止信号:

应答信号:
发送器每发送一个字节(8个bit),就在时钟脉冲9期间释放数据线,由接收器反馈一个应答信号。应答信号为低电平时,规定为有效应答位(ACK,简称应答位),表示接收器已经成功地接收了该字节;
应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。

数据发送的时序:

9.3、使用的库函数
HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c,
uint16_t DevAddress,
uint16_t MemAddress,
uint16_t MemAddSize,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout)
//参数一:I2C_HandleTypeDef *hi2c,I2C设备句柄
//参数二:uint16_t DevAddress,目标器件的地址,七位地址必须左对齐
//参数三:uint16_t MemAddress,目标器件的目标寄存器地址
//参数四:uint16_t MemAddSize,目标器件内部寄存器地址数据长度
//参数五:uint8_t *pData,待写的数据首地址
//参数六:uint16_t Size,待写的数据长度
//参数七:uint32_t Timeout,超时时间
//返回值:HAL_StatusTypeDef,HAL状态(OK,busy,ERROR,TIMEOUT)
十、SPI
10.1、SPI介绍
SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,越来越多的芯片集成了这种通信协议 。
SPI 物理架构:

SPI 包含 4 条总线,SPI 总线包含 4 条总线,分别为SS、SCK、MOSI、MISO。它们的作用介绍如下 :
- MISO – Master Input Slave Output,主设备数据输入,从设备数据输出
- MOSI – Master Output Slave Input,主设备数据输出,从设备数据输入
- SCK – Serial Clock,时钟信号,由主设备产生
- CS – Chip Select,片选信号,由主设备控制
10.2、SPI工作原理

SPI 工作模式:
时钟极性(CPOL):
没有数据传输时时钟线的空闲状态电平
0:SCK在空闲状态保持低电平
1:SCK在空闲状态保持高电平
时钟相位(CPHA):
时钟线在第几个时钟边沿采样数据
0:SCK的第一(奇数)边沿进行数据位采样,数据在第一个时钟边沿被锁存
1:SCK的第二(偶数)边沿进行数据位采样,数据在第二个时钟边沿被锁存
| SPI模式 | CPOL | CPHA | 空闲时SCK时钟 | 采样时刻 |
|---|---|---|---|---|
| 0 | 0 | 0 | 低电平 | 奇数边沿 |
| 1 | 0 | 1 | 低电平 | 偶数边沿 |
| 2 | 1 | 0 | 高电平 | 奇数边沿 |
| 3 | 1 | 1 | 高电平 | 偶数边沿 |
其中模式 0 和模式 3 最常用,如下是他们的工作时序图;
模式0:

模式3:

t DevAddress,
uint16_t MemAddress,
uint16_t MemAddSize,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout)
//参数一:I2C_HandleTypeDef *hi2c,I2C设备句柄
//参数二:uint16_t DevAddress,目标器件的地址,七位地址必须左对齐
//参数三:uint16_t MemAddress,目标器件的目标寄存器地址
//参数四:uint16_t MemAddSize,目标器件内部寄存器地址数据长度
//参数五:uint8_t *pData,待写的数据首地址
//参数六:uint16_t Size,待写的数据长度
//参数七:uint32_t Timeout,超时时间
//返回值:HAL_StatusTypeDef,HAL状态(OK,busy,ERROR,TIMEOUT)
## 十、SPI
### 10.1、SPI介绍
SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种**高速的,全双工,同步的通信**总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,越来越多的芯片集成了这种通信协议 。
**SPI 物理架构:**
[外链图片转存中...(img-P6jHc4JY-1686411209346)]
SPI 包含 4 条总线,SPI 总线包含 4 条总线,分别为SS、SCK、MOSI、MISO。它们的作用介绍如下 :
-
MISO – Master Input Slave Output,主设备数据输入,从设备数据输出
- MOSI – Master Output Slave Input,主设备数据输出,从设备数据输入
- SCK – Serial Clock,时钟信号,由主设备产生
- CS – Chip Select,片选信号,由主设备控制
### 10.2、SPI工作原理
[外链图片转存中...(img-60isi7Ts-1686411209346)]
**SPI 工作模式:**
**时钟极性(CPOL):**
没有数据传输时时钟线的空闲状态电平
0:SCK在空闲状态保持低电平
1:SCK在空闲状态保持高电平
**时钟相位(CPHA):**
时钟线在第几个时钟边沿采样数据
0:SCK的第一(奇数)边沿进行数据位采样,数据在第一个时钟边沿被锁存
1:SCK的第二(偶数)边沿进行数据位采样,数据在第二个时钟边沿被锁存
| SPI模式 | CPOL | CPHA | 空闲时SCK时钟 | 采样时刻 |
| :-----: | :--: | :--: | :-----------: | :------: |
| 0 | 0 | 0 | 低电平 | 奇数边沿 |
| 1 | 0 | 1 | 低电平 | 偶数边沿 |
| 2 | 1 | 0 | 高电平 | 奇数边沿 |
| 3 | 1 | 1 | 高电平 | 偶数边沿 |
其中模式 0 和模式 3 最常用,如下是他们的工作时序图;
模式0:
[外链图片转存中...(img-9mC3Vd9R-1686411209347)]
模式3:
[外链图片转存中...(img-uk4AxrmP-1686411209347)]

本文详细介绍了基于STM32HAL库的GPIO、定时器、串口、看门狗、DMA和SPI的使用方法,包括配置、中断、实验示例和关键函数,帮助读者理解STM32的外设交互和底层驱动操作。
1365

被折叠的 条评论
为什么被折叠?



