目录
学习外设前提——学习时钟树
一、时钟树是什么?
二、如何配置时钟树
三、时钟树的组成
3.1.时钟源
3.2.锁相环 PLL
3.3.系统时钟 SYSCLK
3.4.时钟信号输出 MCO
简单创建一个CubeMX工程(STMF103常用)
一、STM32HAL库开发(1)——CubeMX配置GPIO
1.1、GPIO介绍
1.2、GPIO工作模式
1.3、CubeMX怎么配置GPIO
1.4、CubeMX配置点灯实验(输出)
1.5、CubeMX配置读取按键(输入)
1.6、GPIO常用函数讲解
二、STM32HAL库开发(2)——Cube配置基本定时器
2.1、定时器CubeMX配置介绍
2.2、定时器中断控制LED
2.3、定时器常用函数
2.4、定时器从模式(Reset Mode)
2.5、定时器的从模式(Gated Mode)
2.6、定时器的编码器接口
三、STM32HAL库开发(3)——CubeMX配置外部中断
3.1、CubeMX配置外部中断
3.2、代码实现按键控制LED亮灭
3.3、外部中断函数常用函数
四、STM32HAL库开发(4)——Cube配置PWM
4.1、PWM介绍
4.2、PWM呼吸灯
五、STM32HAL库开发(5)——CubeMX配置输入捕获
5.1、基本工作原理
5.2、简单测量频率
5.3、交叉连接测量占空比
5.4、输入捕获常用函数
六、STM32HAL库开发(6)——CubeMX配置串口通讯(中断方式)
6.1、串口实验(非中断)
6.2、串口实验(中断)
6.3、DMA方式
6.4、常用函数
七、STM32HAL库开发(7)——CubeMX配置ADC
7.1、ADC的知识点(很重要)
⭐⭐CubeMX配置ADC须知(ADC必知知识点)
7.2、ADC单次非扫描
7.3、ADC连续非扫描
7.4、单次/连续非扫描总结
7.5、扫描连续模式多通道(非DMA)
7.6、扫描非连续模式多通道(DMA)
7.7扫描连续模式多通道(DMA)
7.8、定时器通道输出(TRGO)触发ADC
7.9、其他更多触发方式
前言:当前笔记时作为入门STM32HAL库,学习CubeMX配置的,主要介绍主要常用函数和CubeMX配置外设的方法还有浅层工作原理,更基础的硬件知识并不注重!因为在学习标准库的时候应该都已经熟悉了!
学习外设前提——学习时钟树
一、时钟树是什么?
在一个复杂电路中,可能会因为一些延时而产生短暂的中间状态,最终导致电路输出错误结果,因此时序在电路中极为重要。以时序电路控制输出,在每一级输出结果前对各个信号进行采样,从而使得电路中某些信号即使出现延时也可以保证各个信号的同步,可以避免电路中发生错误, 达到精确控制输出的效果。由于时序电路的重要且复杂, 在 MCU 设计时就设计了专门用于控制时序的电路,在芯片设计中称为时钟树。
STM32本身非常复杂,外设非常的多,每个外设所需要的时钟频率各不相同(部分外设需要几时Mhz,而部分外设仅仅需要几Khz即可),同时在一个电路中,时钟越快功耗越大,同时抗电磁干扰能力也会越弱,因此为了达到低功耗和EMC的目的, STM32 一般采取多时钟源的方法来解决这些问题,而且STM32默认不开启这些外设功能和时钟。 用户可以根据自己的需要决定 STM32 芯片是否使用这个功能,这个功能开关在 STM32主控中也就是各个外设的时钟。
二、如何配置时钟树
这个就是完成配置时钟树的一个图!
我们配置的一般步骤是:
1、设置时钟源
2、设置系统时钟 SYSCLK
3、设置 AHB 分频因子(决定 HCLK 等于多少)
4、设置 APB2 分频因子(决定 PCLK2等于多少)
5、设置 APB1 分频因子(决定 PCLK1 等于多少)
6、设置各个外设的分频因子;
7、最终达到对于 SYSCLK、HCLK、 PCLK2、 PCLK1 这四个时钟的配置。
三、时钟树的组成
3.1.时钟源
对于 STM32F1,输入时钟源(Input Clock)主要包括 HSI, HSE, LSI, LSE。外部时钟源就是从外部通过接晶振的方式获取时钟源。内部时钟源,芯片上电即可产生,不需要借助外部电路。
高速外部振荡器 HSE (High Speed External Clock signal)
HSE 是高速的外部时钟信号,可以由有源晶振或者无源晶振提供,频率从 4-16MHZ 不等。当使用有源晶振时,时钟从 OSC_IN 引脚进入, OSC_OUT 引脚悬空,当选用无源晶振时,时钟从OSC_IN 和 OSC_OUT 进入,并且要配谐振电容。HSE 最常使用的就是 8M 的无源晶振
低速外部振荡器 LSE (Low Speed External Clock signal)
一般外部晶振使用外接 32.768kHz 石英晶体,主要作用于 RTC 的时钟源(RTC 时钟也可由 HSE/128 分频或者低速内部时钟信号 LSI得到)
高速内部振荡器 HSI(High Speed Internal Clock signal)
由内部 RC 振荡器产生,频率为 8MHz。该时钟源根据温度和环境的情况,频率漂移较大,一般不作为 PLL 的时钟来源。这里我们选 HSE 作为 PLL 的时钟来源。
低速内部振荡器 LSI(Low Speed Internal Clock signal)
由内部 RC 振荡器产生,频率为 40kHz,可作为独立看门狗的时钟源。
3.2.锁相环 PLL
在 STM32 主控中,锁相环的作用主要有两个部分:输入时钟净化和倍频。 前者是利用锁相环电路的反馈机制实现,后者我们用于使芯片在更高且频率稳定的时钟下工作。PLL 时钟来源可以有两个,一个来自 HSE,另外一个是 HSI/2。 HSI 是内部高速的时钟信号,但温漂比较大,一般不作为 PLL 的时钟来源,所以我们选 HSE 作为 PLL 的时钟来源。
3.3.系统时钟 SYSCLK
STM32 的系统时钟 SYSCLK 为整个芯片提供了时序信号,系统时钟来源可以是: HSI、 PLLCLK、 HSE,我们可以把主频通过 PLL 设置为 72MHz。也可以使用 HSI/2,那么可以得到最高主频 8MHz/2*16=64MHz,为了达到stm32f103c6t6芯片推荐最高主频72Mhz,我们这里使用PLL设置SYSCLK。前面讲到STM32 主控是时序电路链接起来的。对于相同的稳定运行的电路,时钟频率越高,指令的执行速度越快,单位时间能处理的功能越多。
3.4.时钟信号输出 MCO
MCO 是 microcontroller clock output 的缩写,是微控制器时钟输出引脚,主要作用是可以对外提供时钟,相当于一个有源晶振。 MCO 的时钟来源可以是:PLLCLK/2、 HSI、 HSE、 SYSCLK,除了对外提供时钟这个作用之外,我们还可以通过示波器监控 MCO 引脚的时钟输出来验证我们的系统时钟配置是否正确。STM32 允许通过设置, 通过 MCO 引脚输出一个稳定四个时钟信号可被选作 MCO 时钟: SYSCLK、HSI、HSE、PLL 时钟。
简单创建一个CubeMX工程(STMF103常用)
CubeMX配置:
①开启外部高速时钟
②设置SYS的Debug为Serial Wire
③配置时钟树(荐最高主频72Mhz)
④工程相关配置
注意:其他的就是外设的学习了,在后面都是使用这个简单的CubeMX工程,后面学习外设就跳过这个部分了,只注重配置使用的外设细节
一、STM32HAL库开发(1)——CubeMX配置GPIO
1.1、GPIO介绍
GPIO通常用法包括:
- 控制引脚输出高、低电平,或直接驱动、或间接驱动LED、继电器等期间实现开关功能;
- 芯片接收外部输入高、低电平,以此确定某些外部设备(例如按钮、开关)状态,做出相应操作。
1.2、GPIO工作模式
GPIO 有八种工作模式,分别是:
输入模式有:
- 输入浮空
- 输入上拉
- 输入下拉
- 模拟功能
输出功能有:
- 开漏输出
- 推挽输出
- 开漏式复用功能
- 推挽式复用功能
下面我们主要介绍各种GPIO工作模式的作用和主要运用场景。虽然在CubeMX中使用串口、PWM、I2C等等配置GPIO时候都不需要手动选择GPIO模式,CubeMX就会为你配置好对应的GPIO工作模式。不过还是需要我们了解的!
1.输入浮空模式
输入浮空模式:数据通道中仅接入TTL触发器(作用是将相对缓慢变化的模拟信号变成矩形信号)整形,随后输出到输入数据寄存器,该种工作模式未接入任何上拉/下拉电阻, IO 口的电平完全是由外部电路决定。 模式特点:在该引脚悬空(无信号输入)的情况下,读取该端口的电平是不确定的。
适用场合:外部按键输入检测/USART RX引脚
2.上拉输入模式
上拉输入模式:与浮空输入模式相比,仅仅是在数据通道前端接入了一个上拉电阻。 特点是:在无信号输入时端口电位受上拉电阻钳制,I/O端口输入电平始终保持为高电平;而当端口输入电平为低电平时,I/O端口输入电平为低电平。
适用场合:配置中断输入(引脚触发中断条件为下降沿触发/低电平触发时),不可以做电流型驱动
3.下拉输入模式
下拉输入模式:与浮空输入模式相比,仅仅是在数据通道前端接入了一个下拉电阻。 模式特点:在无信号输入时端口电位受下拉电阻钳制,I/O端口输入电平始终保持为低电平;而当端口输入电平为高电平时,I/O端口输入电平为高电平。
适用场合:配置中断输入(引脚触发中断条件为上升沿触发/高电平触发时),不可以做电流型驱动
4.模拟输入模式
模拟输入模式:数据通道不接入任何处理单元(TTL触发器/钳制电阻),直接输入MCU内部的处理单元。 模式特点:相较于其他输入模式只能读取到逻辑高/低电平(数字量),该模式能读取到细微变化的值(模拟量)。
适用场合:该模式下需要用到芯片内部的模拟电路单元, 用于 ADC、 DAC、 MCO这类操作模拟信号的外设。ADC模拟输入/低功耗下省电。
5.推挽输出
推挽输出:输出具有驱动能力,当CPU输出逻辑’0’时,I/O端口输出低电平,而当CPU输出逻辑’1’时,I/O端口输出高电平。推挽输出模式下输出高电平,有较强的的驱动能力,驱动电流最大可达25mA。
应用场景:驱动LED、数码管等电子元器件、输出控制某个信号。另外在推挽输出模式下,我们可以读取该 IO 口的电平状态。
6.开漏输出
开漏输出:适合做电流型的驱动,其吸收电流能力较强。当CPU输出逻辑’0’时,I/O端口输出低电平,而当CPU输出逻辑’1’时,该引脚处于开漏,也就是浮空状态(高阻态),如果想输出高电平则必须接入上拉电阻。同时IO口可以由外部电路改变为低电平或不变,即可读IO输入电平变化,实现了I/O端口的双向功能;
应用场景:I2C(I2C_SDA),SMBus,等总线判断总线占用状态。
7.复用推挽输出
复用推挽输出:在STM32中,一个引脚通常可作为普通GPIO来使用,但通常有多个复用模块对应着同一个引脚,那么当这个GPIO作为内置外设引脚时,就叫做复用模式。
适用场合:常见片内外设(USART TX引脚/SPI/PWM输出等等)
8.复用开漏输出
复用开漏输出:与开漏输出特性一致,只不过引脚选择了复用功能。
适用场合:常见片内外设(I2C/SMBus等等)
1.3、CubeMX怎么配置GPIO
1.4、CubeMX配置点灯实验(输出)
1、首先我们先配置好CubeMX创建出工程:
开启时钟源(外部高速时钟,精度高):
设置SYS的debug模式为(Serial Wire防止自锁):
配置时钟树:
配置GPIO进行点灯(使用引脚PA9,使用推挽输出):
生成工程:
代码实现:
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_9, GPIO_PIN_SET);//使GPIOA9置高电平——点亮灯
其他入门常用函数:
void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);//翻转引脚电平
1.5、CubeMX配置读取按键(输入)
这里硬件我选择把按键一边接引脚,一边接阴极(按键按下电平为0,抬起电平为1),所以PA9配置为上拉输入,PA10配置为推挽输出驱动LED,达到按下按键控制LED亮!
代码实现:
/* 按键驱动 */
//读取PA9电平
//返回值:读取电平为0返回0,为1返回1
int Key_Read()
{int temp;temp = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_9);HAL_Delay(20);if(temp == GPIO_PIN_RESET) return 0;else return 1;
}int main(void)
{/* USER CODE BEGIN 1 */int i;//返回值/* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();/* USER CODE BEGIN 2 *//* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */i = Key_Read();if(i == 0) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_10, GPIO_PIN_SET); //点亮else HAL_GPIO_WritePin(GPIOA, GPIO_PIN_10, GPIO_PIN_RESET);//熄灭}/* USER CODE END 3 */
}
1.6、GPIO常用函数讲解
/* GPIO初始化 */
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);
/* 引脚写1或0 */
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);
/* 锁住GPIO的配置、读取、写入状态等操作 */
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);
二、STM32HAL库开发(2)——Cube配置基本定时器
2.1、定时器CubeMX配置介绍
定时器属性配置:
2.2、定时器中断控制LED
1、CubeMX配置(配置定时器频率为1Hz)
开启中断:
继续选择PA9为LED工作引脚(推挽输出):
代码实现:
HAL_TIM_Base_Start_IT(&htim2);//开始定时器中断//中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_9);}
2.3、定时器常用函数
HAL_TIM_Base_Stop_IT(&htim2);//关闭定时器中断
HAL_TIM_Base_Start_IT(&htim2);//开始定时器中断//中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{}
2.4、定时器从模式(Reset Mode)
作用:
它的作用是 TIF 接收到信号时 ( 上升沿或下降沿 ) ,会清空计数器。并且触发一次 触发器中断 ( 由触发器中断标 志位控制) 。 ( 重装载是的中断是 更新中断 )
假如不按下按键, CNT 从 0 自增到 2000 ,后 count 只加 1 ,并且从 0 计数。但按键按下, CNT 清零然后从零重新开始计数
uint32_t hhRetCount=0;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if(htim==&htim3){if(__HAL_TIM_GET_FLAG(htim,TIM_FLAG_TRIGGER)==SET){//从模式Reset的中断__HAL_TIM_CLEAR_FLAG(htim,TIM_FLAG_TRIGGER);//需手动清空标志位}else{//自动重装的中断hhRetCount+=1;}}
}
main函数:
int main(void)
{/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_I2C2_Init();MX_TIM3_Init();/* USER CODE BEGIN 2 */OLED_Init();//OLED初始化OLED_CLS();HAL_TIM_Base_Start_IT(&htim3);OLED_ShowStr(0,0,"CNT:",2); //显示字符串OLED_ShowStr(0,5,"Conut:",2); //显示字符串/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */uint32_t man = __HAL_TIM_GET_COUNTER(&htim3);//OLED_ShowNum(u8 x,u8 y,u32 num,u8 len,u8 size2);//size2(16|12)OLED_ShowNum(40, 0, hhRetCount,5,16);OLED_ShowNum(50, 5, man,4,16);}}
效果:
2.5、定时器的从模式(Gated Mode)
作用:
它的作用是 TIF 接收到信号时 ( 高电平或低电平 ) ,会暂停计数器计数。会设置触发器中断标志位,但是 不 会触发触发器中断 。
其他的和上面一样,代码实现也一样!
现象:按下按键时,TIF会输入高电平。Gated模式检查到后会开始计数器计数。不按下时则会停止计数。(定时器在高电平模式下会正常计数,低电平触发后就会停止计数)
2.6、定时器的编码器接口
简单原理阐述:
Encoder Interface 编码器接口 编码器接口可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减,从而指示编码器的位置、旋转方向和旋转速度
编码器工作模式:
简单可以分为三个模式:要理解这些,我们先要知道怎么分辨编码器是正转和反转:
分辨完反向后,我们就可以根据A和B相来进行算数计次了,TI1模式就为遇到A相上下沿CNT自增,同理TI2模式就为遇到B相上下沿CNT自增,TI1和TI2模式就为遇到A相和B相上下沿CNT自增,无非就是精度问题而已,根据自己需求设定工作模式!
下面我们做两个小实验:①编码器当做时钟源 ②计数编码器速度
编码器硬件接线:
CubeMX配置:
1. 打开编码器模式
2. 设置定时器的相关参数由于是检测速度,所以这里就不分频,尽可能让定时器快点。计数器设置 最大,防止输入信号频率太高溢出。
3. 设置计数模式,TI1计数和TI2计数
代码实现:
main函数(这里我使用的OLED驱动,没有显示负数的函数,所以翻反转没办法显示负数出来):
int main(void)
{/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init */int16_t time = 100;int man =100;/* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_I2C2_Init();MX_TIM3_Init();/* USER CODE BEGIN 2 */OLED_Init();//OLED初始化OLED_CLS();HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);//打开定时器编码器模式OLED_ShowStr(0,0,"CNT:",2);/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */time = __HAL_TIM_GET_COUNTER(&htim3);//获取当前CNTOLED_ShowNum(70, 0, time, 5, 16);HAL_Delay(100);OLED_ShowNum(70, 5, man, 5, 16);}/* USER CODE END 3 */
}
计数编码器速度并显示:
原理就是 1 秒钟读取一次定时器的计数值 ( 编码器的脉冲 ) 并清空。得到的数字就是当前的速度,单 位是Plus/S
代码实现:
int16_t hhGetEncoderSpeedCountAndReset(){int16_t Tmp;Tmp=__HAL_TIM_GET_COUNTER(&htim3);//获取当前CNT__HAL_TIM_SET_COUNTER(&htim3,0);//清空CNTreturn Tmp;
}int main(void)
{HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_TIM3_Init();HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);//打开定时器编码器模式OLED_Init();OLED_Clear();OLED_ShowString(2, 1, "CNT:");while (1){OLED_ShowSignedNum(2,5,hhGetEncoderSpeedCountAndReset(),5);HAL_Delay(1000);}
}
三、STM32HAL库开发(3)——CubeMX配置外部中断
3.1、CubeMX配置外部中断
这里还是使用PA9和10引脚实现,9负责接受外部中断,10负责点亮LED:
详细配置(上拉):
具体GPIO Mode可以选择一下这些,下面是我用翻译软件翻译过来的了!
开启中断:
3.2、代码实现按键控制LED亮灭
//外部中断函数回调函数
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{status ^= 1;if(status == 1) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_10, GPIO_PIN_SET);if(status == 0) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_10, GPIO_PIN_RESET);}
这样每次进入回调函数,就会修改LED引脚的电平状态,从而控制LED亮灭!
3.3、外部中断函数常用函数
/* GPIO初始化 */
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);
/* 引脚写1或0 */
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);
/* 锁住GPIO的配置、读取、写入状态等操作 */
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);
四、STM32HAL库开发(4)——Cube配置PWM
4.1、PWM介绍
PSC(预分频系数),ARR(自动重装系数),CCR(捕获/比较系数)
PWM的频率计算公式为:Freq=时钟频率(72MHZ)/ (PSC + 1) / (ARR + 1);
PWM的工作模式:
PWM模式1(向上计数) :计数器从0计数加到自动重装载值(TIMx_ARR),然后重新从0开始计数,并且产生一个计数器溢出事件
PWM模式2(向下计数) :计数器从自动重装载值(TIMx_ARR)减到0,然后重新从重装载值(TIMx_ARR)开始递减,并且产生一个计数器溢出事件
PWM的输出模式(mode):
模式1:CNT<CCR 输出有效电平
模式2:CNT>CCR 输出有效电平
输出比较极性(有效电平):
Hight:高为有效电平
Low: 低为有效电平
PWMCubeMX配置:
4.2、PWM呼吸灯
这里选择小灯泡正极接引脚,所以有效电平为高,这里选择模式1(CNT<CCR 输出有效电平)
CubeMX配置PWM:
代码实现:
int main(void)
{/* USER CODE BEGIN 1 */int status = 0;int temp = 0;/* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_TIM1_Init();/* USER CODE BEGIN 2 */HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2); //开启定时器PWM输出 /* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */if(status == 0){temp ++;__HAL_TIM_SetCompare(&htim1, TIM_CHANNEL_2, temp);HAL_Delay(1);if(temp == 500) status = 1; }else if(status == 1){temp --;__HAL_TIM_SetCompare(&htim1, TIM_CHANNEL_2, temp);HAL_Delay(1);if(temp == 0) status = 0; }}/* USER CODE END 3 */
}
常用函数:
__HAL_TIM_SET_PRESCALER(&htim2,72-1);//10000HZ//设置预分频
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 83);//83%设置占空比
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2); //开启定时器PWM输出
__HAL_TIM_SetCompare(&htim1, TIM_CHANNEL_2, temp);//设置占空比
五、STM32HAL库开发(5)——CubeMX配置输入捕获
5.1、基本工作原理
5.2、简单测量频率
每次读取完,cnt也会被上升沿清零,所以,下一次读取到的就是一个周期所要经历的次数!
所以一个周期所要经过的时间为T = 1 / F(测) * cnt
转换回频率就是f = 1/T = F(测)/ cnt.
那我们先自己产生一个PWM信号,在用输入捕获去测量:
配置PWM:
int32_t freq = 5;
uint32_t capture;void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{if (htim->Instance == TIM3) {capture =HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1)+1;//这里跟在keil中一样也要加1freq=1000000/capture;}
}
main函数:
int main(void)
{/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_I2C2_Init();MX_TIM2_Init();MX_TIM3_Init();/* USER CODE BEGIN 2 */OLED_Init();//OLED初始化OLED_CLS();HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);OLED_ShowStr(0,0,"freq:",2); //显示字符串OLED_ShowStr(0,5,"capt:",2); //显示字符串/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */OLED_ShowNum(30,0,freq,5,16);OLED_ShowNum(30,5,capture,5,16);}/* USER CODE END 3 */
}
5.3、交叉连接测量占空比
简单定理阐述:
通道一遇到高电平会捕获并清零CNT(假如捕获到的数据为data1),通道二遇到低电平会捕获并清零CNT(假如捕获到的数据为data2),那么
data2就是高电平经历的CNT次数,data1就是周期经过的CNT次数!
那么频率和占空比就好算了:
F = 1000000 / data1;
占空比 = data2 / data1;
3、在打开一个通道2,并配置为交叉连接:
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_2);
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
重写中断函数:
uint32_t capture;void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{if (htim->Instance == TIM3) {if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) {// 上升沿触发的中断capture =HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1)+1;} else if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2){// 下降沿触发的中断uint32_t capture2 =HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2)+1;}}
}
后面自己根据上面的原理计算即可得到!
5.4、输入捕获常用函数
/* 开启输入捕获中断 */
HAL_StatusTypeDef HAL_TIM_IC_Start_IT(TIM_HandleTypeDef *htim, uint32_t Channel);
/* 停止输入捕获中断 */
HAL_StatusTypeDef HAL_TIM_IC_Stop_IT(TIM_HandleTypeDef *htim, uint32_t Channel);非中断模式:
HAL_StatusTypeDef HAL_TIM_IC_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
HAL_StatusTypeDef HAL_TIM_IC_Stop(TIM_HandleTypeDef *htim, uint32_t Channel);/* 设置捕获极性 */
__HAL_TIM_SET_CAPTUREPOLARITY(&htim2,TIM_CHANNEL_1,TIM_ICPOLARITY_FALLING); //设置为下降沿捕获
__HAL_TIM_SET_CAPTUREPOLARITY(&htim2, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING);//设置为上升沿捕获/* 读取当前计数值 */
HAL_TIM_ReadCapturedValue(&htim2,TIM_CHANNEL_1);
六、STM32HAL库开发(6)——CubeMX配置串口通讯(中断方式)
6.1、串口实验(非中断)
1、选定串口
#include <stdio.h>
#include <string.h>
unsigned char ch[20] = {0};int fputc(int ch, FILE *f)//重定义
{unsigned char temp[1]={ch};HAL_UART_Transmit(&huart1,temp,1,0xffff);return ch;
}main函数里:
unsigned char ch[20] = {0};
HAL_UART_Transmit(&huart1, "hello world\n", strlen("hello world\n"), 100);//发送数据
while(1)
{HAL_UART_Receive(&huart1, ch, 19, 100);//接收数据printf(ch);memset(ch, 0, strlen(ch));
}
6.2、串口实验(中断)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{if(huart->Instance == USART1){/* 里面主要逻辑将接收到的buf保存到自己的大方即可,比如(数组、临时变量) */}// 重新开启中断HAL_UART_Receive_IT(&huart1, &buf, 1);}main函数:
{HAL_UART_Receive_IT(&huart1, &buf, 1);}
6.3、DMA方式
使能USART1中断,串口使用DMA方式必须开启中断,否则程序发送一次数据后,不能判断DMA传输是否完成,串口会一直处于busy状态。
DMA参数配置,发送选择 DMA1 Stream 4通道,方向从存储器到外设,优先级为低。接收选择 DMA1 Stream 5通道,方向从外设到存储器。mode设置可以选择Normal表单次传输,传输一次后终止传输,Circular表示循环传输,传输完成后又重新开始继续传输,不断循环永不停止。此处选择单次传输,Mode为Normal。Data Width:串口数据发送寄存器只能存储8bit,每次发送一个字节,所以数据长度选择Byte。
代码实现:(main函数如下,每秒串口打印一次)
/* USER CODE BEGIN 0 */
uint8_t aTxMessage[] = "\r\n****DMA Serial communication***\r\n";
/* USER CODE END 0 *//*** @brief The application entry point.* @retval int*/
int main(void)
{/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_DMA_Init();MX_USART1_UART_Init();/* USER CODE BEGIN 2 *//* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE */HAL_UART_Transmit_DMA(&huart1, (uint8_t *)aTxMessage, sizeof(aTxMessage));HAL_Delay(1000);/* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}
6.4、常用函数
HAL_UART_Transmit(); 串口发送数据,使用超时管理机制
HAL_UART_Receive(); 串口接收数据,使用超时管理机制
HAL_UART_Transmit_IT(); 串口中断模式发送
HAL_UART_Receive_IT(); 串口中断模式接收参数说明:
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart,
uint8_t *pData, uint16_t Size, uint32_t Timeout)
形参 1 :UART_HandleTypeDef 结构体类型指针变量
形参 2:指向要发送的数据地址
形参 3:要发送的数据大小,以字节为单位
形参 4:设置的超时时间,以ms单位
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart,
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); //接收中断回调函数配合DMA:
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
七、STM32HAL库开发(7)——CubeMX配置ADC
7.1、ADC的知识点(很重要)
转换模式:
1 单次转换模式:ADC只执行一次转换;
2 连续转换模式:转换结束之后马上开始新的转换;
3 扫描模式:ADC扫描被规则通道和注入通道选中的所有通道,在每个组的每个通道上执行单次转换。在每个转换结束时,这一组的下一个通道被自动转换。如果设置了CONT位(开启了连续 转换模式),转换不会在选择组的最后一个通道上停止,而是再次从选择组的第一个通道继续转换。
4 间断模式:触发一次,转换一个通道,在触发,在转换。在所选转换通道循环,由触发信号启动新一轮的转换,直到转换完成为止。
扫描模式简单的说是一次对所有所选中的通道进行转换,比如开了ch0,ch1,ch4,ch5。 ch0转换完以后就会自动转换通道1,4,5直到转换完这个过程不能被打断。如果开启了连续转换模式,则会在转换完ch5之后开始新一轮的转换。
这就引入了间断模式,可以说是对扫描模式的一种补充。它可以把0,1,4,5这四个通道进行分组。可以分成0,1一组,4,5一组。也可以每个通道单独配置为一组。这样每一组转换之前都需要先触发一次。
⭐⭐CubeMX配置ADC须知(ADC必知知识点)
配置ADC工作模式属性:
规则通道设置:
设置ADC触发方式:
Regular Conversion launched by software 规则的软件触发 调用函数触发即可
Timer X Capture Compare X event 外部引脚触发,
Timer X Trigger Out event 定时器通道输出触发 需要设置相应的定时器设置
rank转换顺序、Sampling Time(转换周期:默认1.5):
注入通道设置:
硬件内部:
两者的区别就是:
规则通道就相当于一个客人可以一次性点16个菜(开启16个通道),但是出餐的时候只能每次端上一个菜(得到一个转换结果数据),一次性端太多的话就会覆盖前面的(规则通道数据寄存器只有16位),所以最好配合DMA一起工作!DMA会转移数据到其他地方,避免覆盖!
注入通道就相当于一个客人可以一次性点4个菜(开启4个通道),并出餐的时候每次可以端上四个菜(得到四个转换结果数据),(规则通道数据寄存器只有4x16位)!
ADC转换中断:
ADC的DMA传输:
byte:字节,通用8位,与u8相同
word:字长,与硬件的位数相同,STM32是32位,所以对应是u32
Half Word:半个字长,所以对应是u16
7.2、ADC单次非扫描
单次 / 连续:转换后是否继续自动转换,单次就不继续自动转换扫描 / 非扫描:是否支持多通道,扫描就可以多通道可知,单次非扫描就是给指令就开始转换,转换需要等待完成,转换完成后需要再次给指令才能继 续转换。且一次只能转换一个通道。
代码实现:
编写单次转换函数:
HAL_StatusTypeDef HalState;//转换完成效果
uint16_t Ret;//转换完成的数据uint16_t StartAndGetOneResult()
{HAL_ADC_Start(&hadc1);//开始转换(软件触发)HalState= HAL_ADC_PollForConversion(&hadc1, 10);//第二个参数是等待超时时间if(HalState == HAL_OK)//转换成功{Ret=HAL_ADC_GetValue(&hadc1);} else{Ret=0;}//HAL_ADC_Stop(&hadc1);//停止转换return Ret;//返回转换数据
}
main函数:
HAL_ADCEx_Calibration_Start(&hadc1);//校准ADC
其他的就是自己想要转换什么的ADC和显示数据了!
7.3、ADC连续非扫描
方便之处:只需要执行一次开始转换即可,也不需要等待转换完成
main函数:
HAL_StatusTypeDef HalState;
uint16_t Ret;
uint16_t StartAndGetOneResult()//读取ADC数据
{Ret=HAL_ADC_GetValue(&hadc1);return Ret;
}HAL_ADCEx_Calibration_Start(&hadc1);//校准
HAL_ADC_Start(&hadc1);//只执行一次开始转换
7.4、单次/连续非扫描总结
在无论是连续模式还是非连续模式下,一旦执行了 HAL_ADC_Start(&hadc1); 函数, ADC 开始转换。在非连续模式下,您需要使用 HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY) ; 函数来 等待转换完成,然后使用 HAL_ADC_GetValue 函数来读取 ADC 值。此函数会阻塞直到转换完成或者 超时。在连续模式下,由于转换持续进行,您可以直接使用 HAL_ADC_GetValue 函数来读取最新的 ADC 值,无需等待转换完成。
7.5、扫描连续模式多通道(非DMA)
代码读取ADC:
uint32_t ADC_Value[2];
HAL_ADC_Start(&hadc1); //开始ADC转换
HAL_ADC_PollForConversion(&hadc1,10); while (1){HAL_Delay(500);for(i=0;i<2;i++){if(HAL_IS_BIT_SET(HAL_ADC_GetState(&hadc1), HAL_ADC_STATE_REG_EOC)){ADC_Value[i] = HAL_ADC_GetValue(&hadc1);}HAL_ADC_Stop (&hadc1);//停止ADC}}
7.6、扫描非连续模式多通道(DMA)
当我们使用多通道时候,最好还是使用DMA帮忙转运,反正又不占用CPU,上面的方法太鸡肋了,还会有数据丢失的风险!在规则组中的多次(扫描或非扫描)转换ADC出来的结果只有一个寄存 器存放假如不及时使用DMA转运出来数据就会覆盖掉。现在就是利用DMA转运多次ADC的结果。
Cube MX配置:
uint16_t AD_Value[4];
HAL_StatusTypeDef hhStatue;void StartAndGetResult()
{//重新设置计数器,让它实现再次转运hhStatue=HAL_ADC_Start_DMA(&hadc1,(uint32_t*) AD_Value, 4);//开始ADC转换if(hhStatue!=HAL_OK)//读取失败{AD_Value[0]=0;AD_Value[1]=0;AD_Value[2]=0;AD_Value[3]=0;}while(hdma_adc1.State!=HAL_DMA_STATE_READY);//等待DMA转运数据完成HAL_ADC_Stop_DMA(&hadc1); //停止ADC转换
}
校准:
HAL_ADCEx_Calibration_Start(&hadc1);
使用例子:
uint16_t AD1;
uint16_t AD2;
uint16_t AD3;
uint16_t AD4;int main(void)
{HAL_Init();MX_GPIO_Init();MX_DMA_Init();MX_ADC1_Init();OLED_Init();OLED_Clear();HAL_ADCEx_Calibration_Start(&hadc1);OLED_ShowString(1,1,"AD1:");OLED_ShowString(2,1,"AD2:");OLED_ShowString(3,1,"AD3:");OLED_ShowString(4,1,"AD4:");while (1){StartAndGetResult();AD1= AD_Value[0];AD2= AD_Value[1];AD3= AD_Value[2];AD4= AD_Value[3];OLED_ShowNum(1,5,AD1,4);OLED_ShowNum(2,5,AD2,4);OLED_ShowNum(3,5,AD3,4);OLED_ShowNum(4,5,AD4,4);HAL_Delay(100);
}
7.7扫描连续模式多通道(DMA)
代码实现:
main函数(开始转换一次就要可以了):
uint16_t AD_Value[4];HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADC_Start_DMA(&hadc1,(uint32_t*) AD_Value, 4);
温馨提示:
由于使用的是 ADC 和 DMA 的循环模式,他们实现 ADC 转换并 DMA 转运一次是非常非常快的。而 DMA 中 默认开启了中断。每次ADC 转换并 DMA 转运完成都会触发一次中断。使得单片机频繁被中断,没有时间 去处理其他任务了。解决办法就是把DMA 中断关掉。在这里扫描连续模式下的 ADC+DMA 中断并没有什 么用处。把下面两个勾选去掉即可。重新烧录进去即正常获取。
7.8、定时器通道输出(TRGO)触发ADC
前面的例子都是使用软件触发,通过一个简单的函数代码就可以轻松触发ADC开始转换!下面演示使用TIM3的TRGO事件来触发ADC。
实验效果:配置好定时器的频率,定时1秒钟,每过一秒钟,CNT就会溢出,定时器的TRGO触发,定时器是1S。1S更新一个ADC转换结果!
注意:即使是使用TIM的TRGO触发,也需要加上一次软触发时使用的代码。否则获取不到结果
HAL_ADC_Start(&hadc1);
我也不知道为什么,有知道的大佬可以告诉我!
CubeMX配置:
1、配置定时器
1. 使用内部时钟2. 设置定时器相关参数,定时 1 秒钟。3. 打开更新事件。
HAL_StatusTypeDef HalState;
uint16_t Ret;
uint16_t StartAndGetOneResult()
{HalState= HAL_ADC_PollForConversion(&hadc1, 10);//第二个参数是等待超时时间if(HalState == HAL_OK){Ret=HAL_ADC_GetValue(&hadc1);//获取转换结果}return Ret;
}uint16_t ADValue;
float Voltage;
int main(void)
{OLED_Init();OLED_Clear();HAL_ADCEx_Calibration_Start(&hadc1);HAL_TIM_Base_Start(&htim3); // 启动TIM3HAL_ADC_Start(&hadc1);//启动ADC转换OLED_ShowString(1,1,"ADValue:");OLED_ShowString(2,1,"Voltage:0.00V");while (1){ADValue= StartAndGetOneResult();OLED_ShowNum(1,9,ADValue,4);Voltage=(float) ADValue/ 4095 *3.3;OLED_ShowNum(2,9,(uint32_t)Voltage,1);OLED_ShowNum(2,11,((uint16_t)(Voltage * 100)) % 100,2);HAL_Delay(100);}
}
7.9、其他更多触发方式
Regular Conversion launched by software 规则的软件触发 调用函数触发即可
Timer X Capture Compare X event 外部引脚触发,
Timer X Trigger Out event 定时器通道输出触发 需要设置相应的定时器设置
这里就不一一展示了,感兴趣的可以自己试一下!
可以参考这个图片:
示例: