hal库-cnblog
HAL库
一些小细节!
-
alt + / 触发自动补全
-
定时器初始化函数MX_TIM2_Init在进行初始化的时候会把中断标志位至1,导致每次启动时钟都会调用一次中断回调函数!
如果影响了程序的正确运行则需要在初始化后立马将标志位至0
![image-20241107205358650]()
-
相较于TI1和TI2组成的输入捕获通道组合,TI3和TI4的这个组合的线路没有接入到从模式控制器当中,没有接入到编码器上。
-
在设置定时器的启动的时候,如
-
HAL_TIM_Base_Start(&htim1); 和 HAL_TIM_Base_Start_IT(&htim1); 的区别在于加了TI的会启用定时器更新中断。
-
中断函数重定义要写void
-
uint8_t在cubeide当中识别不出来!?
GPIO
GPIO的变量类型
GPIO_PinState state = GPIO_PIN_SET;
GPIO的输入输出
GPIO的输出
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_SET);
GPIO的输入
输入捕获模式
捕获寄存器
每一个通用定时器和高级定时器的每一个通道都会存在一个捕获寄存器
定时器启动计数之后,如果设置的信号源产生的相应的电平,则会立马将计数器当中的数值存储到捕获寄存器当中
与此同时如果设置了输入捕获中断,则还会触发一次输入捕获中断
输入捕获通道不可以设置下降沿和上升沿同时触发,所以可以使用直接模式连接和间接模式连接来将另一个边沿对应的计数器的值记录。

直接模式和间接模式的定义是相对的,如果TI1为信号输入线,则捕获寄存器1对应的模式为直接,寄存器2为间接
- 其中,TI1和TI2、TI3和TI4分别为两对,它们在输入捕获的线路原理图方面是相同的。
输入捕获的软件配置

输入捕获函数的配置并非在此界面!

如上图,向下寻找到通道3和4的设置界面,其中我配置通道三为上升沿触发,直接模式,没有分频且不滤波。
而通道四为下降沿触发,间接模式,没有分频且不滤波。
在NVIC当中使能输入捕获中断即可,不需要调整从模式控制器

使能计数器和输入捕获IC的代码如下
HAL_TIM_Base_Start(&htim1);
HAL_TIM_IC_Start(&htim,TIM_CHANNEL_3);
HAL_TIM_IC_Start_IT(&htim,TIM_CHANNEL_4);
- 注意到通道4增加了IT后缀,会触发输入捕获中断函数!
- 在输入捕获中断函数中,读取两个寄存器的值,做差,再进行计算就可知晓信号源高电平的时间长短
想象一个情况:通道3记录的计数器的值为65534,随后通道4因为计数器重装载记录到的值为一个小于65534的读数,就会导致计算错误
解决方法:触发通道3的中断之后,立马重置计数器的值,使得通道4存储的值即为时间差
重置函数为
__HAL_TIM_SET_COUNTER(&htim1,0);
输入捕获中断回调函数的配置
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if (htim == &htim1 && htim -> Channel == HAL_TIM_ACTIVE_CHANNEL_4)
{
upEdge = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_3);
downEdge = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_4); //读取通道3/4的捕获寄存器
//code
}
}
其中,每次触发通道3/4的设定电平变化条件,就会自动设置通道标志位 htim -> Channel
输出比较模式
- 冻结模式:屁用没有,开启后输出维持原样
- 强制有效模式:屁用没有,强制输出100%占空比信号
- 强制无效模式:屁用没有,强制输出0%占空比信号
- 匹配时有效模式:寄存器和计数器相同时输出有效电平
- 匹配时无效模式:寄存器和计数器相同时输出无效电平
- 匹配时翻转模式:寄存器和计数器相同时翻转输出电平
- PWM模式:最有存在感的模式,包含PWM1和PWM2模式(详见PWM部分)
输出比较寄存器
在一开始设定比较寄存器的值,然后定时器不断比较比较寄存器和计数器的值,根据大小关系来决定输出的是高电平还是低电平
DMA
- DMA触发的中断不会阻塞程序的执行
CAN
CAN的电路连接
有两种连接方法,开环和闭环连接。
开环高速距离短。闭环低速距离长
1. 闭环总线网络
CAN闭环通讯网络是一种遵循ISO11898标准的高速、短距离网络,它的总线最大长度为40m,通信速度最高为1Mbps,总线的两端各要求有一个“120欧”的电阻。

2.开环总线网络
CAN开环总线网络是遵循ISO11519-2标准的低速、远距离网络,它的最大传输距离为1km,最高通讯速率为125kbps,两根总线是独立的、不形成闭环,要求每根总线上各串联有一个“2.2千欧”的电阻。

这里主要讨论高速can的通信
CAN的通信电平
以高速CAN协议为例,当表示逻辑1时(隐性电平),CAN_High和CAN_Low线上的电压均为2.5v,即它们的电压差V H -V L =0V;而表示逻辑0时(显性电平),CAN_High的电平为3.5V,CAN_Low线的电平为1.5V,即它们的电压差为V H -V L =2V。
CAN的时序

图中表示的CAN通讯信号每一个数据位的长度为19Tq,其中SS段占1Tq,PTS段占6Tq,PBS1段占5Tq,PBS2段占7Tq。信号的采样点位于PBS1段与PBS2段之间,通过控制各段的长度,可以对采样点的位置进行偏移,以便准确地采样。
仲裁
每个设备在can网络都是平等的,但是数据包发送的优先级会通过ID仲裁控制,使得重要的内容先发送。

cubemx配置
UART-串口
- UART触发的中断不会阻塞程序的执行
串口发送重定义
不要忘记勾选microUSB!!!!!!!!!!
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
串口的发送

在软件界面设置即可,其中
- Asynchronous模式:
- 在这种模式下,USART的工作方式是异步的,也就是说,它不依赖于外部时钟信号进行同步。数据以特定的波特率进行传输,发送和接收端必须在同一波特率下工作。通常用于标准的串口通讯,如RS-232协议。
- Synchronous模式:
- 如果讨论同步模式,则USART在发送和接收数据时使用一个共享的时钟信号,这样可以更精确地控制数据传输,适合于高速数据传输场景。
设置好下方的波特率即可使用串口
通过串口发送函数发送内容
HAL_UART_Transmit(&huart2,(uint8_t)message,strlen(string),100);
其中,message是需要发送的内容的指针,最后一个参数是等待时间,最高为HAL_MAX_DELAY
- 一般发送内容时等待时间写100(ms),接收写HAL_MAX_DELAY(无限时间等待)
串口的接收
uint8_t receiveData [2]; //设置好接收串口内容的数组
HAL_UART_Receive(&huart2,receiveData,2,HAL_MAX_DELAY); //这次使用了uint8_t为接收数组,所以不需要类型转换

如上图,定义了一个GPIO_PinState类型的、名为state的变量,随后根据计算机发送的信息控制灯的亮灭
(R、G、B控制颜色,0、1控制亮灭)
串口中断发送数据的内容在下面的中断内容中提及
时钟、时钟树、两者和总线的联系
在RCC中设置时钟为晶振,且在时钟树中选择72MHz回车修改即可
关于时钟树更底层的内容----时钟与时钟树
时钟源是心脏,而时钟树则是时钟的动脉。
AHB 先进高性能总线当中的HCLK时钟线
其中内存、处理器和DMA直连HCLK(72MHz)
处理器内部存在一个Sys Tick 滴答定时器,由HCLK分频而来,HAL_Delay就是根据此定时器实现延时功能
APB 先进外设总线
挂载在AHB上,为外设提供时钟支持
APB1(IIC USB CAN SPI2/3 USART1 通用定时器 基本定时器)
APB2 (USART1 SPI1 GPIO ADC 中断 高级定时器TIM)
高速内部时钟(HSI)、高速外部时钟(HSE)、系统时钟(SYSCLK)
HSI
存在于芯片内部,8MHz,精度较低
HSE
外接晶振,精度高,频率根据型号有所区别,能耗高
LSE
内部时钟源,速度慢但是节能!
SYSCLK
区别于滴答时钟SYSTick
SYSCLK可以接入HSI或者HSE,而HSE经过锁相环倍频可产生72MHz的频率
定时器
定时器就是计数器
通过设定APB接入的时钟信号来确定方波信号,而方波信号连接一个最大为65535的寄存器,然后通过预分频器,通过时钟频率和寄存器的数值来计算出时间(103c8t6上只有TIM1-4)
- 通用定时器 TIM 2-5
- 基本定时器 TIM 6-7
- 高级定时器 TIM 1 and 8

当预分频器设置为0时不分频,为1时二分频,以此类推
自动重装载寄存器
监控计数器的值是否和自己(自动重装载寄存器)相同,当相同时则将计数器归零,并且触发定时中断回调函数
- 如果需要设置收到m个脉冲重装载的话,则自动装载寄存器的值应该设置为m-1
定时器的使能
勾选Internal clock

在STM32CubeMX中,TIM的Internal Clock来自下方的APBx Timer clock(MHz)
在参考的样例当中,APB1和APB2的28均改为72MHz

设置计数器的分频和自动重装载

在这里使用了7200分频,使得72MHz的信号变为10000Hz,随后重装载为10000,所以当计数满10000的时候会触发中断,也就意味着每一秒会触发一次中断
代码部分
写入HAL_TIM_Base_Start(&htim4);在while函数之前,tim4init函数之后。随后tim4就会按照设置好的工作状态工作
HAL_TIM_Base_Start(&htim4);
// Get the current counter value
uint32_t counterValue = __HAL_TIM_GET_COUNTER(&htim4);
// Set the counter value (example: set to 0)
__HAL_TIM_SET_COUNTER(&htim4, 0);
// Get the current reload register value
uint32_t reloadValue = htim4.Init.Period; // Typically accessed through the timer handle
// Set the reload register value (example: set to 1000)
__HAL_TIM_SET_AUTORELOAD(&htim4, 1000);
// Set the prescaler value (example: set prescaler to 16)
__HAL_TIM_SET_PRESCALER(&htim4, 16);
// Start the timer if it’s not already started
HAL_TIM_Base_Start(&htim4);


这个是影子寄存器的使能,如果使能了影子寄存器,则更改完预分频器之后不会立马生效,会等到下一个周期才生效更改的值
如果失能可能会导致一些bug,使得计数器和重装载器的值错过,导致计数器到65535才能触发
TIM的中断函数
打开TIM4的NVIC,勾选即可
触发条件:计数器数值达到设定值时触发中断( TIM1 Update Interrupt)
将HAL_TIM_Base_Start(&htim4);函数改为如下即可
HAL_TIM_Base_Start_IT(&htim4);
然后找到中断回调函数,编写中断时需要执行的代码内容(此函数写在main.c当中ji'ke
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim == &htim4)
{
//执行的代码内容
}
}
中断
定时器的计数不需要cpu的参与,但是中断回调函数的内容需要cpu来执行
中断回调函数采用__weak若定义声明,旨在用户自己在主程序当中自发定义其内容

此函数功能对应了每次触发中断后在屏幕上显示字符串内容
定时器更新中断

slave mode 设定为reset mode,实现每次上升或下降沿触发一次定时器重装载中断,在更新定时器数字为0后,还可以同时执行弱定义的中断回调函数当中的内容。也就意味着中断回调函数的内容会有两个执行入口,一个是计数器重装载时产生的自发性质的中断,另一个是指定引脚接入的信号跳变产生的中断
如何判断中断究竟是因为定时器溢出还是由于信号跳变?
当信号触发reset中断的时候,存在
__HAL_TIM_GET_FLAG(htim,TIM_FLAG_TRIGGER) == SET;
如果中断回调函数加入了if语句判断中断源的逻辑,则需要手动使用如下函数,将标志位至0
__HAL_TIM_CLEAR_FLAG(htim,TIM_FLAG_TRIGGER);
输入信号为门模式
当电平为高电平时,才允许时钟信号流入触发控制器,即只在高电平期间计数,如果改变边沿检测器器极性,则低电平计数,高电平不计数。


需要注意:门模式触发的中断和上一个“定时器更新”中断相比,门模式触发的中断不会给计数器重装载。
输入信号TI1FP1跳变,即当门模式被触发的时候,虽然标志位也会被至1,但是并不会触发定时器更新中断(因为定时器没有被重装载),判断是否触发门模式的逻辑代码应该写在main.c的while循环当中,而并非像定时器更新中断一样写在中断回调函数当中。

触发模式

检测到上升或者下降边沿后让计数器开始计数,其代码逻辑和门模式一样。
触发一次之后再次触发并不会停止计数,但是按照上述代码逻辑(即触发时的逻辑判断写在while循环当中),依旧会执行判断当中的内容,视频当中的案例就是串口输出字符串

单脉冲模式
搭配Trigger mode(触发模式)这种从模式使用最佳,勾选后计数器只计数一次,溢出后不再重装载而是停止计数

在停止计数期间,如果再次触发上升/下降沿,则会开始新一轮的计数
输入捕获中断
设置相应的两个TIM通道后,选择直接模式和间接模式,然后在NVIC使能即可,过程详见二级标题“输入捕获模式”
串口中断

串口中断发送
当发送数据寄存器为空时,发送数据寄存器空中断标志位至1,触发对应中断,
使得发送数据寄存器在瞬间装载完毕,多次执行后完整数据被发出。

打开USART2的串口中断功能,其对应发送数据的函数为
HAL_UART_Transmit_IT(&huart2,(uint8_t)message,strlen(string));
即相较于一般的发送数据,串口中断发送数据的函数增加了IT后缀,由于串口中断是非阻塞中断,
所以不需要第四个参数(延时设置)的填写。
串口中断接收
与上方的设置同理,其对应的函数为
HAL_UART_Receive_IT(&huart2,(uint8_t)message,strlen(string));

当接收移位寄存器将内容放置到数据接收寄存器的时候,触发接收数据寄存器非空中断
关于串口的回调函数
因为很多标志位都指向了如下的这个串口中断函数,所以需要使用更具体的中断回调函数来处理串口的各种中断

当串口接收完成的时候,会触发如下函数内容

只需要在main.c重新定义这个回调函数,就可以在串口接收完毕后立马对收到的数据进行处理了

和串口章节实现了相同的功能,但通过串口中断实现的程序内容是非阻塞的
注意:因为HAL_UART_Receive_IT运行一次就会预备触发一次中断内容,所以不能在while循环调用,如果要实现复用,则在上图中
结束的位置再次启动串口中断接收。
注意!!!!!!! 中断回调函数在main函数前执行定义!!!!!!!
串口中断拓展函数
串口空闲中断(收取不定长度的信息)
当串口接收完所有数据的时候,会触发串口空闲中断,它是一个关于串口中断的拓展函数,
可以替代下文的HAL_UART_Receive_DMA函数实现相同的功能
这种中断可以处理不定长度的数据,所以第三个选项和接收的数组大小需要设置的较大才行
uint8_t message[50];
HAL_UARTEx_ReceiveToIdle_DMA(&huart2,message,50);
串口空闲中断函数和HAL_UART_Receive_DMA写在一样的位置,并且和其一样存在回调函数,写法在其基础上多出了一个SIZE参数

注意:RxEventCallback回调还会在数组被填满一半的时候触发一次,所以每一个
HAL_UARTEx_ReceiveToIdle_DMA(&huart2,message,50);
之后还要加入失能DMA传输半中断
__HAL_DMA_DISABLE_TI(&hdma_usart2_rx,DMA_IT_HT);
串口中断+DMA转运--实现CPU的完全解放!?

来到cube MX,设置串口的输出(TX)功能和DMA绑定,其中搬运方向为内存---->外设(发送数据寄存器)
increment address是地址自增选项。因为发送数据寄存器地址固定,而内存地址是一块连续的区域,所以因当勾选memory的自增选项
Mode可以选择循环模式和正常模式(这个以后填坑)
当然,串口的输出(RX)功能也是如上设置即可。
如何使用DMA转运串口并且将数据发送/接收?
在上述发送和接收回调函数的基础上,将_IT改为_DMA即可!
注意:程序的发送(TX)不需要在main.c处增加类似于HAL_UART_Receive_DMA(&huart2,(uint8_t)message,strlen(string));
而接收数据(RX)则需要添加上述函数在while循环和中断回调函数内部!
外部时钟和外接模块
TIE_ED只能通过双边沿产生
TI2FP2和TI1FP1可以自由配置下降沿/上升沿触发
采用外部时钟模式2,通过高级定时器的外部触发器计数(少用)
(slave mode:external clock mode2)


clock filiter : 滤波器设置,可以有效防止抖动的产生,滤波方法如下图,绝大多数情况下填写15(最大值)即可。
注意:clock filiter的计次依据于如下区域,如果采用高频率,则需要调节prescaler来使得输入到定时器的时钟不那么快

polarity : 选择是否高低电平反转(极性选择)
prescaler : 预分频大小设置
采用外部时钟模式1


将时钟源disable,随后如上选择外部时钟模式1,选择ETR1也能达成上面的效果,且ETR1还可以改为更多选项
发现先前的clock设置变成了Trigger设置,但三个选项选择的含义是和之前一样的

将Trigger Source更改为TI1_ED发现计数次数是原先的两倍,因为此通道是上下降沿都会触发
将Trigger Source更改为TI2FP2发现下方多出了上下降沿的选择项
中断函数汇总
在 STM32 微控制器中,所有外设和核心功能都有各自的中断函数。当发生特定事件时,处理器会自动跳转到相应的中断函数进行处理。STM32 的中断函数都是按照其外设的中断源来命名的,每个外设通常会有一到多个中断源,并为每个中断源提供一个对应的中断处理函数。
1. 外部中断(External Interrupts)
外部中断用于响应外部信号(例如按钮按下或引脚电平变化):
EXTI0_IRQHandler- 外部中断 0EXTI1_IRQHandler- 外部中断 1EXTI2_IRQHandler- 外部中断 2EXTI3_IRQHandler- 外部中断 3EXTI4_IRQHandler- 外部中断 4EXTI9_5_IRQHandler- 外部中断 5 至 9EXTI15_10_IRQHandler- 外部中断 10 至 15
2. 定时器中断(Timers Interrupts)
STM32 的定时器(如 TIM1, TIM2 等)具有多种中断源:
TIM1_BRK_IRQHandler- TIM1 断路中断(Break)TIM1_UP_IRQHandler- TIM1 更新中断(Update)TIM1_TRG_COM_IRQHandler- TIM1 触发和换相中断(Trigger and Commutation)TIM1_CC_IRQHandler- TIM1 捕获/比较中断(Capture/Compare)TIM2_IRQHandler- TIM2 更新中断TIM3_IRQHandler- TIM3 更新中断TIM4_IRQHandler- TIM4 更新中断TIM5_IRQHandler- TIM5 更新中断TIM6_IRQHandler- TIM6 更新中断TIM7_IRQHandler- TIM7 更新中断TIM8_BRK_IRQHandler- TIM8 断路中断(Break)TIM8_UP_IRQHandler- TIM8 更新中断(Update)TIM8_TRG_COM_IRQHandler- TIM8 触发和换相中断(Trigger and Commutation)TIM8_CC_IRQHandler- TIM8 捕获/比较中断(Capture/Compare)
3. 串口中断(USART Interrupts)
用于串口通信的中断:
USART1_IRQHandler- USART1 中断USART2_IRQHandler- USART2 中断USART3_IRQHandler- USART3 中断UART4_IRQHandler- UART4 中断UART5_IRQHandler- UART5 中断USART6_IRQHandler- USART6 中断UART7_IRQHandler- UART7 中断UART8_IRQHandler- UART8 中断
4. SPI 中断(SPI Interrupts)
用于 SPI 外设的中断:
SPI1_IRQHandler- SPI1 中断SPI2_IRQHandler- SPI2 中断SPI3_IRQHandler- SPI3 中断SPI4_IRQHandler- SPI4 中断
5. I2C 中断(I2C Interrupts)
用于 I2C 外设的中断:
I2C1_EV_IRQHandler- I2C1 事件中断I2C1_ER_IRQHandler- I2C1 错误中断I2C2_EV_IRQHandler- I2C2 事件中断I2C2_ER_IRQHandler- I2C2 错误中断I2C3_EV_IRQHandler- I2C3 事件中断I2C3_ER_IRQHandler- I2C3 错误中断
6. DMA 中断(DMA Interrupts)
DMA 控制器的中断:
DMA1_Stream0_IRQHandler- DMA1 流 0 中断DMA1_Stream1_IRQHandler- DMA1 流 1 中断DMA1_Stream2_IRQHandler- DMA1 流 2 中断DMA1_Stream3_IRQHandler- DMA1 流 3 中断DMA1_Stream4_IRQHandler- DMA1 流 4 中断DMA1_Stream5_IRQHandler- DMA1 流 5 中断DMA1_Stream6_IRQHandler- DMA1 流 6 中断DMA1_Stream7_IRQHandler- DMA1 流 7 中断DMA2_Stream0_IRQHandler- DMA2 流 0 中断DMA2_Stream1_IRQHandler- DMA2 流 1 中断DMA2_Stream2_IRQHandler- DMA2 流 2 中断DMA2_Stream3_IRQHandler- DMA2 流 3 中断DMA2_Stream4_IRQHandler- DMA2 流 4 中断DMA2_Stream5_IRQHandler- DMA2 流 5 中断DMA2_Stream6_IRQHandler- DMA2 流 6 中断DMA2_Stream7_IRQHandler- DMA2 流 7 中断
7. ADC 中断(ADC Interrupts)
用于 ADC 的中断:
ADC1_2_IRQHandler- ADC1 和 ADC2 中断ADC3_IRQHandler- ADC3 中断
8. CAN 中断(CAN Interrupts)
用于 CAN 总线控制器的中断:
CAN1_TX_IRQHandler- CAN1 发送中断CAN1_RX0_IRQHandler- CAN1 接收 FIFO 0 中断CAN1_RX1_IRQHandler- CAN1 接收 FIFO 1 中断CAN1_SCE_IRQHandler- CAN1 状态改变中断CAN2_TX_IRQHandler- CAN2 发送中断CAN2_RX0_IRQHandler- CAN2 接收 FIFO 0 中断CAN2_RX1_IRQHandler- CAN2 接收 FIFO 1 中断CAN2_SCE_IRQHandler- CAN2 状态改变中断
9. RTC 中断(RTC Interrupts)
RTC(实时时钟)的中断:
RTC_WKUP_IRQHandler- RTC 唤醒中断RTC_ALARM_IRQHandler- RTC 闹钟中断
10. 外设复位和系统控制中断(System Control Interrupts)
这些中断与系统控制和复位相关:
NMI_Handler- 非屏蔽中断HardFault_Handler- 硬故障中断MemManage_Handler- 内存管理故障中断BusFault_Handler- 总线故障中断UsageFault_Handler- 使用故障中断SVCall_Handler- 系统服务调用中断DebugMon_Handler- 调试监视中断PendSV_Handler- 挂起系统中断SysTick_Handler- 系统滴答定时器中断
11. 其他外设中断
其他外设可能有各自的中断函数,以下是几个常见的:
ETH_IRQHandler- 以太网中断USB_LP_CAN1_RX0_IRQHandler- USB 低优先级和 CAN1 接收 FIFO 0 中断USB_HP_CAN1_TX_IRQHandler- USB 高优先级和 CAN1 发送中断FPU_IRQHandler- 浮点单元中断SPI5_IRQHandler- SPI5 中断USART6_IRQHandler- USART6 中断
这些是 STM32 处理器上常见的一些中断函数的列表。每个 STM32 微控制器可能会有所不同,具体的中断函数与外设配置有关。中断函数命名通常遵循以下规则:
- 外设的名称 + 功能类型,例如:
USART1_IRQHandler、TIM2_IRQHandler。 - 如果一个外设有多个中断源(例如定时器的多个通道、DMA 的多个流),会为每个中断源定义一个相应的中断函数。
这些中断处理程序通常在启动时被编译器或启动文件链接到适当的位置,在中断发生时由硬件自动调用。如果你想了解更多特定微控制器的中断函数,推荐查看其参考手册中的“中断向量表”章节。
PWM
通过定时器的输出比较模式输出PWM波
比较寄存器
在一开始设定比较寄存器的值,然后定时器不断比较比较寄存器和计数器的值,根据大小关系来决定输出的是高电平还是低电平

其中向下计数模式和中央对齐模式基本上不用
随后信号连接输出控制器(CH Polarity),对其的设置可以分配有效无效电平和高低电平的对应关系
同一个时钟的不同通道可输出不相干PWM波
PWM波的发生

将TIM3的通道1设置为PWM模式

其中pulse是比较寄存器的值
此时使用的是PWM模式1,意味着计数器小于比较寄存器的值的时候,输出有效电平,除此之外输出无效电平
CH Polarity:输出控制器设置,此时设置的High使得有效电平对应高电平,无效电平对应低电平
output compare preload:设置影子寄存器,使得PWM新的一轮改动在下一次出波时生效
配置输出启动
HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_1);
随后可以更改比较寄存器的值,使得PWM波的占空比改变
__HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_1,cnt); //其中cnt就是比较寄存器的值

小细节:此时GPIO模式为复用推挽输出
使用寄存器修改占空比和频率
1. CNT (计数器寄存器)
uint32_t current_count = TIM3->CNT; // 直接读取计数器值
TIM3->CNT = 0; //直接操控计数器
2. PSC (预分频器)
uint32_t current_psc = TIM3->PSC;
TIM3->PSC = 8399; //直接更改分频系数
3. ARR (自动重装载寄存器)
uint32_t current_arr = TIM3 -> ARR;
TIM3 -> ARR = 0;
4. CCR (比较寄存器)
TIM17 -> CCR1 = 0;
//使得tim17的通道1的比较寄存器为0
编码器的使用

将编码器作为外部输入对方波信号进行处理。
HAL库存在专用的编码器模式,只需要将编码器的两路信号分别接入到TI1FP1和TI2FP2即可,

注意:编码器接口对上下边沿都会计数,对此,需要在数据处理的时候对计数次数除以2!
编码器计次代码部分
此笔记以TIM1时钟的CH1和CH2作为示例,且每个通道有指定的编码器通道,不能随意设置引脚!

设置为编码器模式,CH1和CH2被自动配置成对应的引脚

设置TI1通道(TI1FP1)计数,随后直接将通道滤波拉满即可,其余的设置没啥用,一个是选择上升/下降沿计次,另外有选择通道直连和是否预分频的设置罢了
提前在main.c函数当中设置一个int类型的变量存放读入的计数器的值
首先使能我们的两路编码器通道
int cnt = 0;
HAL_TIM_Encoder_Start(&htim1,TIM_CHANNEL_ALL);
对于AB两个波的编码器可以使用全部通道TIM_CHANNEL_ALL
通过__HAL_TIM_GET_COUNTER(&htim);来获取计数器的值
__HAL_TIM_GET_COUNTER(&htim);
将结果输出到屏幕上发现数值以2/-2的值加到计数器上,于是考虑直接使用整个TIM1通道预分频或者单独对CH1通道和CH2通道进行分频
随后还可以设置通道1或者通道2的上/下沿计次选项,反转其中一个通道即可使得计数方向改变
可以使用如下函数更改计数器的值,使得其在我们需要的可控范围当中
__HAL_TIM_SET_COUNTER(&htim1,0);
此时就是将计数器tim1的值清零
设置完编码器模式之后,__HAL_TIM_GET_COUNTER(&htim);这个函数可以读出负值(即正转反转)
ADC
ADC通过离散采样电压值获取数据,得出的数据取值范围为0~4096-1
线性对应0~3.3V电压值
hal库配置
- 使能ADC

-
ADC频率限制:ADC的频率不适合太高,其时钟源一般为APB2时钟线,过高CUBEMX会报错,更改合适大小的分频值即可
![QQ_1741775013901]()
-
不要忘记使能持续转换软件配置
![QQ_1741775286133]()
HAL_ADC_Start(&hadc1);使能一次ADC,所以需要循环调用HAL_ADC_PollForConversion(&hadc1,HAL_MAX_DELAY);通过这个函数等待ADC的转化成功,等待时间为最大值,返回值为HAL_OKorHAL_TIMEOUTorHAL_ERRORorHAL_BUSYHAL_ADC_GetValue(&hadc1);获取ADC的读数
注意:记得在初始化当中对ADC进行校准,以防止测量值偏移。校准函数如下
HAL_ADCEx_Calibration_Start(&hadc1,ADC_SINGLE_ENDED);其中
ADC_SINGLE_ENDED是单端校准ADC_DIFFERENTIAL_ENDED则为双端校准,需要外部电路的支持。可封装在别的函数当中实现定频率读取ADC,手搓代码如下
void adc_proc(uint32_t time) { static uint32_t last_time = 0; if (uwTick - last_time > time) { last_time = uwTick; HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1,HAL_MAX_DELAY); HAL_ADC_Start(&hadc2); HAL_ADC_PollForConversion(&hadc2,HAL_MAX_DELAY); ADC1_val = HAL_ADC_GetValue(&hadc1); ADC2_val = HAL_ADC_GetValue(&hadc2); v1 = 3.3 * (ADC1_val/4095.0); v2 = 3.3 * (ADC2_val/4095.0); } }
ADC + DMA
hal库配置
- 使能持续转化
![QQ_1741778590856]()
代码部分

使能一次即可
IIC 通信协议
SDA:信号线
SCL:时钟线
因为IIC协议可以实现信息的双向传输,但是不可以进行同时双向通话,所以IIC的通信是半双工通信协议
正是如此,IIC有主从机的设定,主机必须先释放信号,才能开启通信
IIC的通信地址、基本原理
详见《我要成为MPU6050膏手》这里不写了
CUBEIDE设置

配置引脚为标准的IIC模式,下面的配置默认即可
注意到勾选了为每个外设生成单独的头文件后,INC出多出了I2C的头文件
bsp_system.h头文件当中引入i2c.h 方便在写驱动时调用iic相关的函数内容
注意:一般将地址定义为八位地址,也就是在7位地址后补一个0即可,关于读/写地址导致的地址最右的位置为0/1,
iic的函数会自动帮忙解决
# define ADD 0x70 //假设从机的地址就是0x70
uint8_t readbuff[3];
//...
HAL_I2C_Master_Receive(&hi2c1,ADD,readbuff,3,HAL_MAX_DELAY); // 接收数据
HAL_I2C_Master_Transmit(&hi2c1,ADD,readbuff,3 ,HAL_MAX_DELAY); // 发送数据
IIC_DMA中断

打开IIC的两个中断

随后将代码改为_IT,会发现根本收不到正确的数据。
问题很简单,因为调用了receive后,程序就立马进行了数据处理,根本没来得及等IIC填充数据进入数组


于是通过状态机来实现,对于不同的状态有不同的标志位代表,而通过判断标志位来知晓是否完成了发送/接收(样例在下面)
**HAL_I2C_Master_Receive_IT**(&hi2c1,sensor_ADD,&sensor_readbuff,1);
IIC的发送/接收完成回调函数

通过如下的代码内容重新定义两个中断函数,并且注意不断更新状态。


然而还不够,我们像串口那般勾选DMA中断,随后在函数中更改_IT为_DMA就可以使用DMA进行数据转运,进一步提升效率
SPI通信协议
SPI的全双工和半双工
区别于全双工通信是双向且同时的,但是半双工是双向非同时的。
接线如下,可见半双工使用的引脚更少

了解一下flash芯片 W25Q128
连接方式,芯片内部原理

此芯片采用的是3级分区的思路。 页(page)/扇(sector)/块(block)
位(Bit)是计算机当中最小的存储单元,只能是0/1
字节(Byte) = 8 * Bit
- 1 字节 = 8 位
- 1 KB(千字节)= 1024 字节
- 1 MB(兆字节)= 1024 KB = 1048576 字节
Page = 256Byte
Sector = Page * 16 = 4KByte
Block = 64 * Sector = 256KByte

一次只能写256Bytes,超出范围会从头覆盖内容
- MOSI(Master Out Slave In):主设备输出,从设备输入。
- MISO(Master In Slave Out):主设备输入,从设备输出。
- SCK(Serial Clock):时钟信号,由主设备生成。
- NSS(Slave Select):从设备选择信号,由主设备生成,用于选择特定的从设备,用于多主机设备,一般用不到


配置SPI模式 Full-Duplex Master(双工主模式)
SPI相关函数
uint8_t data[8] = {0};
uint8_t rec_data[8] = {0};
HAL_StatusTypeDef HAL_SPI_Transmit(&hspi1,data,8,HAL_MAX_DELAY);
HAL_StatusTypeDef HAL_SPI_Receive(&hspi1,rec_data,8,HAL_MAX_DELAY);
HAL_StatusTypeDef HAL_SPI_TransmitReceive(); // 接收数据的同时发送数据

其中执行传输数据的代码要写在拉高拉低电平代码之间,对应图片NSS的状态
写入数据
写入数据之前,需要将扇区的数据擦除。一般需要delay100ms
写入数据的时候,是以页作为最小单位写入

USB
勾选相应的选项

选择虚拟串口

如下是设备描述

随后可以通过串口模拟进行数据发送
void usb_printf(const char *format, ...)
{
va_list args;
uint32_t length;
va_start(args, format);
length = vsnprintf((char *)UserTxBufferFS, APP_TX_DATA_SIZE, (char *)format, args);
va_end(args);
CDC_Transmit_FS(UserTxBufferFS, length); //USB虚拟串口发送
}
RTC
RTC可通过芯片当中的特殊内部结构实现时间读取和存储的功能。
hal库配置
找到RTC,勾选下列选项。第一个选项为使能RTC,第二个选项为自定义启动时间

设置初始时间 必须使用BCD格式

时钟树

软件部分
/*设置系统时间*/
HAL_StatusTypeDef HAL_RTC_SetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format)
/*读取系统时间*/
HAL_StatusTypeDef HAL_RTC_GetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format)
/*设置系统日期*/
HAL_StatusTypeDef HAL_RTC_SetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format)
/*读取系统日期*/
HAL_StatusTypeDef HAL_RTC_GetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format)
/*启动报警功能*/
HAL_StatusTypeDef HAL_RTC_SetAlarm(RTC_HandleTypeDef *hrtc, RTC_AlarmTypeDef *sAlarm, uint32_t Format)
/*设置报警中断*/
HAL_StatusTypeDef HAL_RTC_SetAlarm_IT(RTC_HandleTypeDef *hrtc, RTC_AlarmTypeDef *sAlarm, uint32_t Format)
/*报警时间回调函数*/
__weak void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
/*写入后备储存器*/
void HAL_RTCEx_BKUPWrite(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister, uint32_t Data)
/*读取后备储存器*/
uint32_t HAL_RTCEx_BKUPRead(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister
uwTick
hal库自带的计时器,可通过全局访问,每次间隔1ms会自动+1
按键消抖/短按/双击/长按
基本思路
详见b站up西风
代码内容
uint32_t key_time[2][4] = {0};
//key_read返回值为按下的按键编号
void my_key_proc(void)
{
static uint32_t last_time = 0;
if (uwTick - last_time >= 10)
{
last_time = uwTick;
key_val = key_read();
key_down = key_val & (key_old ^ key_val);
key_up = ~key_val & (key_old ^ key_val);
key_old = key_val;
if (key_down == 4)
{
key_time[0][3] = uwTick;
}
if (key_up == 4 && key_time[0][3] != 0)
{
key_time[1][3] = uwTick;
if (key_time[1][3] - key_time[0][3] > 500)
{
printf("key4_long\n");
}
else
{
printf("key4_short\n");
}
key_time[0][3] = 0;
key_time[1][3] = 0;
}
}
}
舵机控制
大部分的舵机采用输入50hz不同占空比的信号来进行转动操作

对于大部分180舵机,有
| 高电平时间(脉冲宽度) | 舵机角度(典型值) |
|---|---|
| 0.5ms | 0°(最小角度) |
| 1.0ms | 45° |
| 1.5ms | 90°(中间位置) |
| 2.0ms | 135° |
| 2.5ms | 180°(最大角度) |
蓝牙模块
分为主机和从机
代码规范
-
USER CODE BEGIN Includes / USER CODE END Includes:
- 在这里包含任何额外的头文件,这些头文件是您的应用程序所需要的。例如,如果您需要使用传感器或其他外设的特定库,请在这里包含它们的头文件。
/* USER CODE BEGIN Includes */ #include "sensor_library.h" /* USER CODE END Includes */ -
USER CODE BEGIN PTD / USER CODE END PTD:
- 在这里定义任何私有类型定义(typedefs)。类型定义用于创建新的数据类型或重命名现有的数据类型以提高代码的可读性。
解释/* USER CODE BEGIN PTD */ typedef struct { int sensor_value; float temperature; } SensorData; /* USER CODE END PTD */ -
USER CODE BEGIN PD / USER CODE END PD:
- 在这里定义任何私有常量或宏。常量是固定的值,在程序中不会改变。宏用于定义可重用的代码片段或值。
/* USER CODE BEGIN PD */ #define SENSOR_THRESHOLD 100 /* USER CODE END PD */ -
USER CODE BEGIN PM / USER CODE END PM:
- 在这里定义任何私有宏。宏是预处理指令,用于定义可重用的代码片段或值。
/* USER CODE BEGIN PM */ #define CHECK_SENSOR(value) ((value) > SENSOR_THRESHOLD) /* USER CODE END PM */ -
USER CODE BEGIN PV / USER CODE END PV:
- 在这里声明任何私有变量。这些变量在整个文件中是私有的,不会被其他文件访问。
/* USER CODE BEGIN PV */ uint8_t receivedata[30]; SensorData sensor_data; /* USER CODE END PV */ -
USER CODE BEGIN PFP / USER CODE END PFP:
- 在这里声明任何私有函数原型。这些函数在其他文件中是不可见的。
/* USER CODE BEGIN PFP */ void ProcessSensorData(void); /* USER CODE END PFP */ -
USER CODE BEGIN 0 / USER CODE END 0:
- 在这里编写任何初始化代码或回调函数。例如,您可以初始化传感器或设置中断回调函数。
/* USER CODE BEGIN 0 */ void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { HAL_UART_Receive_IT(&huart1, receivedata, 30); ProcessSensorData(); } /* USER CODE END 0 */ -
USER CODE BEGIN 1 / USER CODE END 1:
- 在这里编写任何需要在
main函数开始前执行的代码。例如,全局变量的初始化。
/* USER CODE BEGIN 1 */ sensor_data.sensor_value = 0; sensor_data.temperature = 0.0f; /* USER CODE END 1 */ - 在这里编写任何需要在
-
USER CODE BEGIN Init / USER CODE END Init:
- 在这里编写任何初始化代码,例如初始化传感器,设置外设等。
/* USER CODE BEGIN Init */ InitSensor(); /* USER CODE END Init */ -
USER CODE BEGIN SysInit / USER CODE END SysInit:
- 在这里编写与系统初始化相关的代码。通常是一些系统级别的设置。
/* USER CODE BEGIN SysInit */ // 系统初始化代码 /* USER CODE END SysInit */ -
USER CODE BEGIN 2 / USER CODE END 2:
- 在这里编写应用程序的初始化代码。通常是一些应用级别的设置。
/* USER CODE BEGIN 2 */ // 应用程序初始化代码 /* USER CODE END 2 */ -
USER CODE BEGIN WHILE / USER CODE END WHILE:
- 在这里编写主循环中的代码。这是程序的主要执行部分。
/* USER CODE BEGIN WHILE */ while (1) { // 主循环代码 } /* USER CODE END WHILE */ -
USER CODE BEGIN 3 / USER CODE END 3:
- 在这里编写任何需要在主循环中执行的代码。通常是一些持续运行的任务或功能。
/* USER CODE BEGIN 3 */ // 持续运行的任务 /* USER CODE END 3 */ -
USER CODE BEGIN 4 / USER CODE END 4:
- 在这里编写任何与系统时钟配置相关的代码。通常是一些系统时钟的设置。
/* USER CODE BEGIN 4 */ // 系统时钟配置代码 /* USER CODE END 4 */ -
USER CODE BEGIN Error_Handler_Debug / USER CODE END Error_Handler_Debug:
- 在这里编写任何与错误处理相关的代码。
/* USER CODE BEGIN Error_Handler_Debug */ // 错误处理代码 /* USER CODE END Error_Handler_Debug */ -
USER CODE BEGIN 6 / USER CODE END 6:
- 在这里编写任何与断言失败相关的代码。
/* USER CODE BEGIN 6 */ // 断言失败处理代码 /* USER CODE END 6 */


不要忘记使能持续转换

浙公网安备 33010602011771号