个人学习笔记
中断简介
中断定义
CPU执行程序中,由于发生了某种随机的事件(外部或内部),引起CPU暂时 中断正在运行的程序,转去执行一段特殊的服务程序(称为中断服务程序或 中断处理程序),以处理该事件,该事件处理完后又返回被中断的程序继续 执行,这一过程称为中断
对于单片机来说
- 中断事件发生
- 产生中断信号
- 单片机接收到中断信号,停止执行当前函数,跳转至中断处理函数
- 执行完中断处理函数后,单片机自动跳转回主函数执行被中断处
NVIC
NVIC是嵌套向量中断控制器,控制着整个芯片中断相关的功能,它跟内核紧密耦合,是内核里面的一个外设。同时掌控中断优先级和中断向量表。
中断流程

- 请求挂起寄存器的改变由外设导致
- 中断信号传递至
NVIC,然后NVIC查找对应中断向量,执行中断处理函数
- 另外中断处理函数都需要去清除请求挂起寄存器对应位,避免中断处理函数被重复调用
HAL库的中断处理及回调函数实现流程
首先是外设发出中断请求信号,NVIC接收到后会根据中断向量表查询对应中断处理函数的入口,在HAL中是对应外设的外设number_IRQHandler,然后再进入HAL库统一的外设中断处理函数HAL_外设_IRQHandler,再调用对应的回调函数

NVIC嵌套向量中断控制器
NVIC会一直检测某一个中断线是否处于激活状态,当中断处理函数运行完成后,需要在中断处理函数中将请求挂起寄存器对应的位清除为0,避免NVIC一直检测到1,一直重复执行中断处理函数,而这一部分代码,在HAL库写的总IRQHandler中已经完成。
另外,NVIC对于中断还有中断优先级的设置,stm32f103c8t6中断优先级由NVIC的优先级寄存器里的4位决定。中断优先级分为抢占优先级和响应优先级(它们一起用这4位)
- 抢占优先级的级别越高,越能够抢占中断通道,优先执行中断
- 响应优先级的级别越高,可以优先排队。
- 中断的优先级,都是数字越小,优先级越高

EXTI外部中断
EXTI外部中断实现
EXTI 可分为两大部分功能,一个是产生中断,另一个是产生事件,这两个功能从硬件上就有所不同。我们的主要关注点是中断。

- 边沿检测电路用来检测输入的电平信号的高低电平的转换,同时会根据 上升沿/下降沿触发选择寄存器来决定是否向后产生高电平信号
- 软件中断事件寄存器使用软件来模拟产生一个中断
- 请求挂起寄存器接收到高电平后会将对应通道的位置1(例如EXTI_9产生高电平进入请求挂起寄存器,则该寄存器会将第9位置1)
- 最后上面的线路就会进入
NVIC(嵌套向量中断控制器)
- 下面的线路,脉冲发生器作为事件信号送到对应外设,这个脉冲信号可以给其他外设电路使用,比如定时器TIM、模拟数字转换器ADC等等,这样的脉冲信号一般用来触发TIM或者ADC开始转换
产生中断线路目的是把输入信号输入到
NVIC,进一步会运行中断服务函数,实现功能,这样是软件级的。
产生事件线路目的就是传输一个脉冲信号给其他外设使用,并且是电路级别的信号传输,属于硬件级的。
调用流程
以EXTI1为例,下降沿触发,当出现下降沿时,就会触发EXTI5的外部中断,进入EXTI9_5_IRQHandler,在EXTI9_5_IRQHandler中调用HAL_GPIO_EXTI_IRQHandler函数统一处理外部中断,在该函数中对挂起寄存器进行清0操作,并调用HAL_GPIO_EXTI_Callback中断回调函数
按键实现外部中断控制小灯亮灭
Cube配置

EXTI配置
打开中断

配置的是下降沿触发,检测下降沿,上拉模式

配置参数解释
- 上升沿触发:指的是当引脚电平从低电平(0)转变为高电平(1)时,触发事件
- 下降沿触发:指的是当引脚电平从高电平(1)转变为低电平(0)时,触发事件
- 双边沿触发:指的是当引脚的电平发生任何变化(即从低电平到高电平或从高电平到低电平)时,都会触发事件
- 上拉:指使引脚在未连接(悬空)时默认为 高电平(1)
- 下拉:指使引脚在未连接(悬空)时默认为 低电平(0)
GPIO配置

推挽模式,初始输出为Low低电平
按键消抖
方式1
1
2
3
4
5
6
7
|
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {//外部中断回调函数
if( GPIO_Pin == GPIO_PIN_5 ) {
//HAL_Delay(10);
for( int i = 0 ; i <= 72000*100 ; ++ i );//空循环延时
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_2);
}
}
|
方式2
1
2
3
4
5
6
7
8
9
|
int lastTick = 0 ;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {//外部中断回调函数
if( GPIO_Pin == GPIO_PIN_5 ) {
if( HAL_GetTick()-lastTick > 150 ) {
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_2);
lastTick = HAL_GetTick();
}
}
}
|
代码实现
1
2
3
4
5
6
7
8
9
|
int lastTick = 0 ;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {//外部中断回调函数
if( GPIO_Pin == GPIO_PIN_5 ) {
if( HAL_GetTick()-lastTick > 150 ) {
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_2);
lastTick = HAL_GetTick();
}
}
}
|
TIM定时器定时中断
在f103c8t6中,TIM1是高级定时器,TIM2,TIM3,TIM4是通用计时器
下面例子中配置的是TIM1,实现每秒翻转1次小灯亮灭
调用流程
触发更新中断后进入更新中断服务函数TIM1_UP_IRQHandler,其中调用HAL_TIM_IRQHandler,在该函数中调用了HAL_TIM_PeriodElapsedCallback回调函数
CubeMX参数配置
使用内部时钟模式

参数配置如下,实现1s触发一次定时器更新中断

Prescaler:预分频,实际分频数是number+1
Counter Mode:计数模式设置
Counter Period:自动重装载计数器计数值
Internal Clock Division:内部时钟预分频
Repetition Counter:重复计数器 (RCR -8 bits),属于高级控制寄存器专用寄存器位,利用它可以非常容易控制输出 PWM 的个数,与内部时钟模式无关
auto-reload preload:是否开启影子寄存器模式
还需要开启定时器更新中断

代码实现
用的PA2作GPIO输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if( htim == &htim1 ) {
HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_2);
}
}
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_TIM1_Init();
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim1);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
|
USART中断
USART的轮询模式会阻塞程序运行,浪费CPU资源
说明:本节保留“中断视角”的基础讲解;若需要串口 + DMA + 空闲中断的完整实践,请结合后文“UART、DMA与空闲中断(综合)”章节阅读。
轮询模式
CPU不断查询发送数据寄存器或者接受数据寄存器导致程序的阻塞
发送
在UART底层有发送移位寄存器和发送数据寄存器,发送数据时,需要CPU将发送数据寄存器的数据移动至发送移位寄存器,然后UART按照指定的波特率发送数据,CPU则不断查询数据发送寄存器是否空,如果空则需要移动数据进来

接收
在UART底层有接收移位寄存器和接收数据寄存器,接收数据时,UART按照指定的波特率接收数据至接收移位寄存器,然后数据会存储在接收数据寄存器,CPU则不断查询数据接收寄存器是否有数据,如果有数据,CPU会把寄存器的数据移动至我们用来接收数据的变量的内存里
当HAL_UART_Receive执行完成,我们就可以知道数据接收完成

中断模式
底层中断实现数据每传送一字节,召回CPU继续运输数据,不需要我们考虑,HAL库底层已经帮我们实现
上层中断实现数据接收完成后的数据处理,通过中断回调函数实现,需要我们自己设计
发送
CPU将数据送入 发送数据寄存器,然后就去执行其他代码,当发送移位寄存器的数据发送后,发送数据寄存器空,会触发发送数据寄存器空中断,CPU再次将数据送入 发送数据寄存器,然后又去执行其他代码,所有数据发送完成后会触发 传送完成中断,调用HAL_UART_TxCpltCallback中断回调函数

接收
CPU在处理其他代码,接收移位寄存器将一帧数据移动至 接受数据寄存器后,会触发 接收数据寄存器非空中断,CPU会回来将 接收数据寄存器 的数据移动至我们用来接收数据的变量的内存里,然后又去执行其他代码,所有数据完成后会调用_HAL_UART_RxCpltCallback_中断回调函数处理接收的数据
而执行HAL_UART_Receive_IT后,我们不能直接对数据继续处理,数据还没接收完成,因为CPU不会进入阻塞态,会去执行其他代码,需要通过HAL_UART_RxCpltCallback回调函数处理接收的数据
定时器中断(补充)
说明:本节偏“更新中断”专题补充;如果想系统学习定时器,优先看后文的“定时器(系统学习)”章节。
基于HAL库 STM32中断详述 - 知乎
STM32–中断使用(超详细!)_stm32中断-CSDN博客
中断机制图解

TIM定时器中断
25_1_TIM,UPDATE定时中断(B站)
定时器更新中断
【话说定时器系列】之四:STM32定时器更新事件及案例分享 - 知乎
更新操作是一种动作,是更新事件的源头,即事件源;
更新事件是基于更新操作所导致的后续影响或结果。
部分更新事件触发定时器更新中断或触发DMA请求。
软件更新操作,触发了更新事件,它不仅仅实现了影子寄存器的数据更新,同时还置位了状态寄存器的更新中断标志位,且使能定时器更新中断,之后就会进入更新中断服务程序。
初始化后立即进入定时器更新中断
基于此,在MX_TIMx_Init()中,初始化了影子寄存器(自动重装载寄存器等),进行了数据的更新,同时还置位了更新中断标志位,所以在时基参数初始化,使能定时器中断之后,就会进入更新中断服务程序。若要解决此问题,我们可以在时基参数初始化完成之后,使能定时器更新中断HAL_TIM_Base_Init_IT()之前,做更新中断标志的清除操作。

复位模式trigger触发后进入更新中断
trigger触发后,计数寄存器被置零,会置位状态寄存器的更新中断标志位,如果使能了更新中断,则会进入更新中断服务函数
更新操作事件源
可能的更新操作【事件源】有3类:
- 核心计数器的溢出【上溢或下溢】
- 软件复位操作【对UG@TIMX_EGR置位】
- 工作在复位模式下的定时器收到触发信号【即复位触发信号】
【特别提醒,对于高级定时器必须发生RCR+1次溢出动作后才可以产生更新事件。对于通用或基本定时器,每溢出一次都可以产生更新事件。】
ADC模块
STM32H743-梳理ADC模数转换器在CubeMX上的配置_overrun behaviour-CSDN博客(写的很好,很详细)
STM32-HAL库-ADC学习 - 曲杨 - 博客园
ADC定义
模数转换器(Analog to Digital Converter,ADC)
模拟信号->数字信号转换器
通过对输入的模拟信号进行测量,将其分级转换为数字信号
STM32单片机中的ADC
一般计算采用一下逻辑方式进行转换
1
2
3
4
5
6
7
|
if( V>V_ref_low && V<V_ref_high ) {
return (V-V_ref_low)/(V_ref_high-V_ref_low) * (2<<X-1);
//归一化,再按照配置的x进行转换
}
else {
return erro ;
}
|
STM32单片机中的ADC,x一般是12,也可以是其他数值
一般情况下,参考电压为0V,参考高电压为3.3V
分辨率与精度
分辨率与位数有关,精度与性能有关,位数越高,分辨率越高,性能越高,波动越小,精度越强
ADC位数为12位,则分辨率是1/4096
使用时PLCK频率控制在36MHz以内,不然ADC模块的精度会大幅降低
STM32CubeMX配置
这里以STM32C8T6为例
- x即Resolution 12bits( 15ADC时钟周期 )(看芯片型号,有的需要配置)
- 数据选择右对齐,得到的数据就是转换后的值
- Mode选择Independent mode

Scan Conversion Mode(扫描转换模式),会采集ADC开启的所有通道,按顺序采集所有通道
Continuous Conversion Mode(连续转换模式),可以在使能ADC时自动开始采集,而非手动采集
Discontinuous Conversion Mode(间断采集模式),可以在一轮采集完成后暂停等待指令控制下一轮采集
Number Of Discontinuous Conversions(单次转换次数)此参数定义每次触发后ADC执行的转换次数,每次转换完都需要HAL_ADC_GetValue来获取值。 如果它设为2,且ADC1使能了通道1,2,5,7的话,那么第一次触发ADC1采样时,就会采样通道1与通道2的值,再一次触发ADC1采样的话,就会采样通道5与通道7值,如此类推。
ADC_Regular_ConversionMode
可以配置每个通道的采集顺序和采样时间
代码使用
1.开始采集
2.采集后转换
1
|
HAL_ADC_PollForConversion(&hadc2,20);//地址,等待时间
|
关于多数据通道采集
ADC_value[1]对应rank_1的通道的值,第一次采集rank1通道
ADC_value[2]对应rank_2的通道的值,第一次采集rank2通道
rank指在ADC_Regular_ConversionMode中的rank设置
1
2
3
4
5
6
7
|
for( int t = 1 ; t <= 10 ; ++ t ) {
for( int i = 0 ; i < 2 ; ++ i ) {
HAL_ADC_Start(&hadc2);
HAL_ADC_PollForConversion(&hadc2,20);//采集后转换
AD_value[i] += HAL_ADC_GetValue(&hadc2) ;//采集十次,平均值处理
}
}
|
检测
1
2
3
|
if(HAL_IS_BIT_SET( HAL_ADC_GetState(&hadc2),HAL_ADC_STATE_REG_EOC) ) {//检测是否正常采集
ADC_value += HAL_ADC_GetValue(&hadc2);
}
|
关于Number Of Discontinuous Conversions
在STM32CubeMX中,Number Of Discontinuous Conversions(单次转换次数)是ADC(模数转换器)的一个配置选项,主要用于控制ADC在非连续采样模式(Discontinuous Mode下的转换行为。以下是详细解释和使用方法:
1. 功能含义
- 作用:
当ADC配置为非连续模式(Discontinuous Mode)时,此参数定义每次触发后ADC执行的转换次数。
- 例如,设置为
3,则每次触发ADC后,会自动连续完成3次转换(无需额外触发信号)。
- 若设置为
1,则每次触发仅执行1次转换(等同于标准单次模式)。
- 应用场景:
适用于需要分组采样的场景,例如:
- 周期性触发ADC,但每次触发需采集多个通道的数据。
- 降低CPU干预频率(通过一次触发完成多次转换)。
2. 配置步骤(STM32CubeMX)
- 启用Discontinuous Mode:
- 在ADC配置界面,勾选
Discontinuous Mode(通常在Parameter Settings选项卡中)。
- 此时,Number Of Discontinuous Conversions选项会生效。
- 设置转换次数:
- 输入需要的转换次数(取值范围取决于型号,通常为1~8)。
- 该值必须 ≤ 总启用通道数(例如,若启用5个通道,最多可设置为5)。
- 配置触发源:
- 选择触发方式(如定时器触发、外部引脚触发等)。
- 每次触发信号到来时,ADC会自动完成设定的转换次数。
3. 工作流程示例
- 硬件环境:
- ADC通道:CH0、CH1、CH2
- 配置:
Discontinuous Mode + Number Of Discontinuous Conversions = 2
- 行为:
- 触发信号到来(如定时器溢出)。
- ADC自动连续转换2个通道(例如CH0→CH1)。
- 转换完成后产生中断/DMA请求。
- 下次触发时,继续从下一个通道开始(CH2→CH0,依此类推)。
在该配置下(Discontinuous Mode + Number Of Discontinuous Conversions = 2),每次触发后ADC会连续转换2个通道(例如CH0→CH1),因此你需要调用两次 HAL_ADC_GetValue(&hadc2) 来分别读取这两个通道的转换结果。以下是具体说明和注意事项:
1. 数据读取方式
方法一:轮询模式(Polling)
每次触发后,需手动读取两次ADC值:
1
2
3
4
5
6
7
8
|
HAL_ADC_Start(&hadc2); // 启动ADC(或通过触发信号启动)
//开启一次,采集两次
if (HAL_ADC_PollForConversion(&hadc2, timeout) == HAL_OK) {//采集后第一次转换
uint32_t ch0_value = HAL_ADC_GetValue(&hadc2); // 读取CH0结果
}
if (HAL_ADC_PollForConversion(&hadc2, timeout) == HAL_OK) {//采集后第二次转换
uint32_t ch1_value = HAL_ADC_GetValue(&hadc2); // 读取CH1结果
}
|
- 注意:每次调用
HAL_ADC_GetValue() 会返回最后一次完成的转换结果,因此需按顺序读取。
方法二:中断模式(Interrupt)
在ADC转换完成中断中读取:
1
2
3
4
5
6
|
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
if (hadc == &hadc2) {
uint32_t ch0_value = HAL_ADC_GetValue(&hadc2); // 第一次转换结果(CH0)
uint32_t ch1_value = HAL_ADC_GetValue(&hadc2); // 第二次转换结果(CH1)
}
}
|
- 关键点:在中断回调中需连续读取两次,顺序由ADC的Rank配置决定。
方法三:DMA模式(推荐)
若启用DMA,ADC会自动将多次转换结果存入指定数组,无需手动调用 HAL_ADC_GetValue():
1
2
|
uint32_t adc_results[2]; // 存储2次转换结果
HAL_ADC_Start_DMA(&hadc2, adc_results, 2); // 启动DMA传输
|
- 结果存储:
adc_results[0] → CH0的值,adc_results[1] → CH1的值(顺序由Rank决定)。
4. 注意事项
- 与连续模式(Continuous Mode)的区别:
- 连续模式:ADC无限循环转换所有启用通道,无需触发。
- 非连续模式:需外部触发,且每次触发仅转换设定的次数。
- DMA配合:
- 建议启用DMA,以高效搬运多组转换结果(尤其是高频触发场景)。
- DMA缓冲区大小需 ≥ 转换次数 × 通道数。
- 通道顺序:
- 转换顺序由Rank(在ADC配置中定义)决定,而非通道编号。
总结
- Number Of Discontinuous Conversions 是ADC非连续模式下每次触发的转换次数。
- 关键配置:启用Discontinuous Mode → 设置转换次数 → 配置触发源。
- 优势:灵活控制采样节奏,减少CPU负担,适合分组采样需求。
中科大RM电控合集:CAN通信+STM32CubeMX编程(B站)
STM32 CAN快速配置(HAL库版本)_stm32 can hal-CSDN博客
一文搞懂CAN和CAN FD总线协议_fdcan-CSDN博客
FDCAN
CubeMX配置解析
这里的配置以STM32G474CET6为例

Classic mode:普通CAN
FD mode without BitRate Switching:FDCAN,没有波特率转换
FD mode with BitRate Switching:FDCAN,带有波特率转换,数据段使用更高波特率
注意选用FD mode with BitRate Switching后还可以通过txheader去选定是否开启BitRateSwithcing,以及是 采用CAN的帧格式还是 FDCAN帧格式
也就是说这个帧格式选择是决定 是否允许拥有FDCAN的特殊格式和能否开启BitRateSwitching,至于发送的帧实际是否用,取决于txheader
从这一段HAL_FDCAN_Init初始化代码中可以看出,只有FD mode with BitRate Switching才会去初始化数据段的传输参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
HAL_StatusTypeDef HAL_FDCAN_Init(FDCAN_HandleTypeDef *hfdcan)
{
uint32_t tickstart;
/* Check FDCAN handle */
//...
/* Check function parameters */
//...
if (hfdcan->Init.FrameFormat == FDCAN_FRAME_FD_BRS)
{
assert_param(IS_FDCAN_DATA_PRESCALER(hfdcan->Init.DataPrescaler));
assert_param(IS_FDCAN_DATA_SJW(hfdcan->Init.DataSyncJumpWidth));
assert_param(IS_FDCAN_DATA_TSEG1(hfdcan->Init.DataTimeSeg1));
assert_param(IS_FDCAN_DATA_TSEG2(hfdcan->Init.DataTimeSeg2));
}
//....
//...
}
|
1. Classic Mode(传统模式)
- 本质:完全兼容传统 CAN 2.0B 协议,不启用 FDCAN 的扩展功能
- 数据长度:最大 8 字节(DLC=0~8)
- 波特率:全程使用标称波特率(Nominal Bit Rate),无速率切换
- 帧结构:与传统 CAN 帧一致,仅包含仲裁段、控制段、数据段(≤8 字节)、CRC 段等
- 核心作用:用于对接仅支持传统 CAN 协议的设备(如老旧传感器、执行器),确保兼容性
2. FD mode with BitRate Switching(带波特率切换的 FD 模式)
- 本质:启用 FDCAN 的核心扩展功能,支持高速长数据传输,且数据段使用更高波特率
- 数据长度:最大 64 字节(DLC=0
15,其中 DLC=915 对应 32~64 字节)
- 波特率:
- 仲裁段(Arbitration Phase):使用标称波特率(与传统模式一致,确保仲裁兼容性)
- 数据段(Data Phase):使用更高的数据波特率(Data Bit Rate)(可配置为标称波特率的 2~8 倍,如 8Mbps)
3. FD mode without BitRate Switching(不带波特率切换的 FD 模式)
- 本质:启用 FDCAN 的长数据功能,但不切换波特率,全程使用标称波特率
- 数据长度:最大 64 字节(与带切换的 FD 模式一致)
- 波特率:仲裁段和数据段均使用标称波特率,无速率提升
- 帧结构:包含 FD 帧标志(FDF),但无波特率切换标志(BRS=0)
- 核心作用:用于需要长数据传输(>8 字节)但总线上存在不支持高速数据段的设备场景,或因硬件限制(如线缆质量差)无法使用高速率的场景
Mode
一般使用Normal mode

- Normal mode(正常模式)
- Restricted operation(受限操作模式)
- Bus Monitoring mode(总线监听模式)
- Internal LoopBack mode(内部回环模式)
- External LoopBack mode(外部回环模式)
Auto-Retransmission & Transmit-Pause & Protocol-Exception

Auto-Retransmission:自动重传,如果发送的帧没有收到应答,出现错误,就不断重新发送,在总线负载较高的时候一定要关掉,会严重拉低控制频率
Transmit-Pause:一种临时停止 FDCAN 发送功能的机制,用于在特定场景下主动暂停数据发送,避免干扰总线或等待特定条件,可以通过软件主动触发,调用HAL库函数
Protocol-Exception:检测到异常情况(帧格式错误等),会去触发异常处理机制(例如异常处理中断)
Data & Nominal
此处提一下,Data Sync Jump Width(数据段同步跳转宽度)是针对数据波特率(Data Bit Rate) 配置的关键时序参数
Data(数据波特率相关)
“Data” 特指 FDCAN 在FD 模式(Flexible Data Rate Mode) 下,帧的数据段(Data Phase) 所使用的传输速率,即数据波特率(Data Bit Rate)
Nominal(标称波特率相关)
“Nominal” 特指 FDCAN 帧在仲裁段(Arbitration Phase)、控制段(Control Phase)、CRC 段(CRC Phase) 和ACK 段(Acknowledgment Phase) 中使用的传输速率,即标称波特率(Nominal Bit Rate)
Filter and Std-Filters-Nbr&Ext-Filters-Nbr
Std Filters Nbr(标准帧过滤器数量) 和Ext Filters Nbr(扩展帧过滤器数量) 是配置 FDCAN(Flexible Data Rate CAN)接收过滤功能的核心参数。它们分别用于定义标准帧(11 位 ID) 和扩展帧(29 位 ID) 的硬件过滤器数量,直接影响 FDCAN 控制器能接收的帧类型和数量
- Std Filters Nbr:配置标准帧(11 位 ID) 的过滤器数量。每个过滤器可匹配一个或多个标准 ID,或通过掩码模式匹配 ID 范围
- Ext Filters Nbr:配置扩展帧(29 位 ID) 的过滤器数量。每个过滤器可匹配一个或多个扩展 ID,或通过掩码模式匹配 ID 范围
过滤器类型
FDCAN 支持多种过滤模式,不同模式对过滤器数量的需求不同:
- 掩码模式(Mask Mode):
通过 ID 和掩码匹配 ID 范围。例如,1 个标准过滤器可匹配 0x100~0x1FF 范围内的所有 ID(掩码 = 0x100,ID=0x1FF)
- 双 ID 模式(Dual ID Mode):
匹配两个特定 ID。例如,1 个扩展过滤器可同时匹配 0x12345678 和 0x87654321
- 范围模式(Range Mode):
匹配 ID 在两个值之间的所有帧。例如,1 个标准过滤器可匹配 0x000~0x0FF 范围内的 ID
配置示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// 配置4个扩展过滤器,每个匹配1个特定ID(双ID模式)
FDCAN_FilterTypeDef sFilterConfig;
sFilterConfig.IdType = FDCAN_EXTENDED_ID;
sFilterConfig.FilterType = FDCAN_FILTER_DUAL;
sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXBUFFER;
// 过滤器0匹配ID=0x01
sFilterConfig.FilterIndex = 0;
sFilterConfig.FilterID1 = 0x01;
sFilterConfig.FilterID2 = 0x01;
sFilterConfig.RxBufferIndex = 0;
HAL_FDCAN_ConfigFilter(&hfdcan2, &sFilterConfig);
// 过滤器1匹配ID=0x02
sFilterConfig.FilterIndex = 1;
sFilterConfig.FilterID1 = 0x02;
sFilterConfig.FilterID2 = 0x02;
sFilterConfig.RxBufferIndex = 1;
HAL_FDCAN_ConfigFilter(&hfdcan2, &sFilterConfig);
// 依此类推配置过滤器2和3
|
关于Txheader的配置
1
2
3
4
5
6
7
8
9
10
11
|
FDCAN_TxHeaderTypeDef Txheader_gimbal ;
Txheader_gimbal.DataLength = 0x08 ;//数据长度
Txheader_gimbal.Identifier = 0x1FF ;//标识符
Txheader_gimbal.TxFrameType = FDCAN_DATA_FRAME ;
Txheader_gimbal.IdType = FDCAN_STANDARD_ID ;
Txheader_gimbal.ErrorStateIndicator=FDCAN_ESI_ACTIVE;//开启错误状态指示
Txheader_gimbal.BitRateSwitch=FDCAN_BRS_OFF;
Txheader_gimbal.FDFormat=FDCAN_CLASSIC_CAN;//传统的CAN模式
//Txheader_gimbal.FDFormat=FDCAN_FD_CAN;//FDCAN模式
Txheader_gimbal.TxEventFifoControl = FDCAN_NO_TX_EVENTS;//无发送事件
Txheader_gimbal.MessageMarker = 0;
|
时间片
不做解析,硬件方面计算出来拿来用即可
使用代码
FilterInit-and-FDCANStart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
void FDCAN_IntFilterAndStart(FDCAN_HandleTypeDef *hfdcan) {
//--------FDCAN1 Filter and Start--------//
FDCAN_FilterTypeDef sFilterConfig;
sFilterConfig.IdType = FDCAN_STANDARD_ID;
sFilterConfig.FilterIndex = 0;
sFilterConfig.FilterType = FDCAN_FILTER_MASK;
sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO0;
sFilterConfig.FilterID1 = 0x00;//0x500;
sFilterConfig.FilterID2 = 0x00;//0x780;
if (HAL_FDCAN_ConfigFilter(hfdcan, &sFilterConfig) != HAL_OK)
{
FDCAN_ErrorHandler();
}
if (HAL_FDCAN_ConfigGlobalFilter(hfdcan, FDCAN_REJECT, FDCAN_REJECT, FDCAN_FILTER_REMOTE, FDCAN_FILTER_REMOTE) != HAL_OK)
{
FDCAN_ErrorHandler();
}
if (HAL_FDCAN_Start(hfdcan) != HAL_OK)
{
FDCAN_ErrorHandler();
}
if (HAL_FDCAN_ActivateNotification(hfdcan, FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0) != HAL_OK)
{
FDCAN_ErrorHandler();
}
if (HAL_FDCAN_ActivateNotification(hfdcan, FDCAN_IT_BUS_OFF, 0) != HAL_OK)
{
FDCAN_ErrorHandler();
}
}
|
解析一下
1
|
if (HAL_FDCAN_ConfigGlobalFilter(hfdcan, FDCAN_REJECT, FDCAN_REJECT, FDCAN_FILTER_REMOTE, FDCAN_FILTER_REMOTE) != HAL_OK)
|
HAL_FDCAN_ConfigGlobalFilter用于定义未被任何特定过滤器匹配的帧的处理规则,参数含义如下:
- 第 2 个参数(
FDCAN_REJECT):未匹配的标准帧被 “拒绝”(不接收);
- 第 3 个参数(
FDCAN_REJECT):未匹配的扩展帧被 “拒绝”(不接收);
- 第 4 个参数(
FDCAN_FILTER_REMOTE):未匹配的标准远程帧被 “拒绝”;
- 第 5 个参数(
FDCAN_FILTER_REMOTE):未匹配的扩展远程帧被 “拒绝”。
结合前面的过滤器配置(所有标准帧都被第 0 号过滤器匹配),这里的全局规则实际效果是:
- 所有标准帧被第 0 号过滤器接收(存入 FIFO0);
- 扩展帧、远程帧(无论标准还是扩展)因未被匹配,全部被拒绝。
数据发送
如果需要兼容CAN,将FDFormat修改为FDCAN_CLASSIC_CAN即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
void FDCAN_InitTxHander(FDCAN_TxHeaderTypeDef* pheader, uint32_t id, uint32_t dlc,uint32_t baudrateswitch,uint32_t can_type){
pheader->Identifier = id;
if(id>=0x800) {
pheader->IdType = FDCAN_EXTENDED_ID;
}
else {
pheader->IdType = FDCAN_STANDARD_ID;
}
pheader->TxFrameType = FDCAN_DATA_FRAME;
pheader->DataLength = dlc;
pheader->ErrorStateIndicator = FDCAN_ESI_ACTIVE;//开启错误状态指示
pheader->BitRateSwitch = baudrateswitch;
pheader->FDFormat = can_type;
pheader->TxEventFifoControl = FDCAN_NO_TX_EVENTS;
pheader->MessageMarker = 0;
}
void FDCAN_SendMessageWithBaudSwitch(FDCAN_HandleTypeDef *hfdcan,uint8_t* pdata,uint32_t dlc,uint32_t id)
{
FDCAN_TxHeaderTypeDef txhead;
FDCAN_InitTxHander(&txhead,id,dlc,FDCAN_BRS_ON,FDCAN_FD_CAN);
HAL_FDCAN_AddMessageToTxFifoQ(hfdcan, &txhead, pdata);
}
void FDCAN_SendMessageWithOutBaudSwitch(FDCAN_HandleTypeDef *hfdcan,uint8_t* pdata,uint32_t dlc,uint32_t id)
{
FDCAN_TxHeaderTypeDef txhead;
FDCAN_InitTxHander(&txhead,id,dlc,FDCAN_BRS_OFF,FDCAN_CLASSIC_CAN);
HAL_FDCAN_AddMessageToTxFifoQ(hfdcan, &txhead, pdata);
if(HAL_FDCAN_AddMessageToTxFifoQ(hfdcan, &txhead, pdata)!=HAL_OK) {
FDCAN_ErrorHandler();
}
}
|
数据接收
1
2
3
4
5
6
7
8
9
10
11
12
13
|
uint8_t RxData[64];
void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs)
{
if((RxFifo0ITs & FDCAN_IT_RX_FIFO0_NEW_MESSAGE)!=RESET)//判断是否是新消息中断
{
if(hfdcan->Instance == FDCAN1) //Slave Arm
{
HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0, &RxHeader, RxData);
//...
}
}
}
|
CAN
Cube配置

模式设置(Mode)

- Normal mode(正常模式):正常发送和接收帧,用于实际总线通信
- Loopback mode(回环模式):发送的帧会被自身接收,无需外部总线,用于调试
- Silent mode(静默模式):只能接收帧,不能发送,用于监听总线
- Silent Loopback mode(静默回环模式):结合静默和回环,仅内部测试
波特率
$$波特率 = APB时钟频率 / [ Prescaler × (1 + TimeSeg1 + TimeSeg2) ]$$

CAN 的波特率由位时间(Bit Time) 决定,位时间 = 1 / 波特率(如 1Mbps 对应位时间 1μs)。位时间由 3 部分组成:
- Prescaler(预分频器):将 APB 时钟(如 APB1=36MHz)分频为时间量子(TQ),公式:
TQ = 1/(APB时钟频率 / Prescaler)
- Sync Jump Width (SJW):同步跳转宽度,用于补偿节点间时钟偏差,取值 1~4TQ(通常设为 1 或 2)
- Time Seg1:包含传播段(PropSeg)和相位段 1(PS1),取值 4~16TQ(用于补偿总线延迟)
- Time Seg2:相位段 2(PS2),取值 2~8TQ(用于同步补偿)
Basic-Parameters
Time Triggered Communication(时间触发通信)
Automatic Bus-Off Management(自动总线关闭管理)
当节点因错误计数超限进入 “总线关闭” 状态(无法发送和接收帧)时,是否需要软件手动恢复,还是控制器自动恢复
Automatic WakeUP mode(自动唤醒模式) 是一种针对低功耗场景的功能,用于让 CAN 控制器在休眠(低功耗)状态下,通过检测总线上的有效活动(如特定 CAN 帧)自动恢复到正常工作状态,无需软件手动触发唤醒
Automatic Retransmission(自动重传)
Receive FIFO Locked Mode(接收 FIFO 锁定模式)
当接收 FIFO(CAN 通常有 2 个接收 FIFO,FIFO0 和 FIFO1)存储的帧数量达到最大深度(如 3 个或 6 个,依芯片型号而定)时,新接收的帧是否覆盖旧的帧
Transmit FIFO Priority(发送 FIFO 优先级)
当发送 FIFO(CAN 通常有 1 个发送 FIFO,可存储多个待发送帧)中有多个帧等待发送时,帧的发送顺序规则
使用代码
FilterInit-and-FDCANStart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
void CAN_Config(void)
{
CAN_FilterTypeDef sFilterConfig;
/* Configure the CAN Filter */
sFilterConfig.FilterBank = 0; // 过滤器编号,使用一个CAN,则可选0-13;使用两个CAN可选0-27
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; // 过滤器模式,掩码模式或列表模式
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; // 过滤器位宽
sFilterConfig.FilterIdHigh = 0x0000; // 过滤器验证码ID高16位,0-0xFFFF
sFilterConfig.FilterIdLow = 0x0000; // 过滤器验证码ID低16位,0-0xFFFF
sFilterConfig.FilterMaskIdHigh = 0x0000; // 过滤器掩码ID高16位,0-0xFFFF
sFilterConfig.FilterMaskIdLow = 0x0000; // 过滤器掩码ID低16位,0-0xFFFF
sFilterConfig.FilterFIFOAssignment = CAN_FilterFIFO0; // FIFOx,0或1
sFilterConfig.FilterActivation = ENABLE; // 使能过滤器
sFilterConfig.SlaveStartFilterBank = 14; // 从过滤器编号,0-27,对于单CAN实例该参数没有意义
if (HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK)
{
/* Filter configuration Error */
}
/* Start the CAN peripheral */
if (HAL_CAN_Start(&hcan1) != HAL_OK)
{
/* Start Error */
}
/* Activate CAN RX notification */
if (HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK)
{
/* Notification Error */
}
}
|
发送
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
uint8_t CAN_Send_Msg(uint16_t output , uint8_t len)
{
uint8_t message[8];
uint32_t TxMailbox;
CAN_TxHeaderTypeDef CAN_TxHeader;
// 设置发送参数
CAN_TxHeader.StdId = 0x200; // 标准标识符(12bit)
//CAN_TxHeader.ExtId = 0x00; // 扩展标识符(29bit)
CAN_TxHeader.IDE = CAN_ID_STD; // 使用标准帧
CAN_TxHeader.RTR = CAN_RTR_DATA; // 数据帧
CAN_TxHeader.DLC = 0x08; // 发送长度
CAN_TxHeader.TransmitGlobalTime = DISABLE;//关闭can的时间触发通信模式
// 装载数据
memset(message, 0, sizeof(message));
message[0] = (output >> 8) & 0xFF;
message[1] = output & 0xFF;
// 发送CAN消息
if(HAL_CAN_AddTxMessage(&hcan1, &CAN_TxHeader, message, &TxMailbox) != HAL_OK)
{
return 0;
}
return 1;
}
|
接收
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{// 收到CAN数据会触发接收中断,进入该回调函数
uint8_t RxData[8];
CAN_RxHeaderTypeDef CAN_RxHeader;
if(HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &CAN_RxHeader, RxData) != HAL_OK)
{
while(1) {};
}
if(hcan==&hcan1) // 只处理CAN1
{
// 处理CAN1接收到的数据
switch (CAN_RxHeader.StdId) // 标准标识符
{
case 0x201:
// 处理ID为0x201的消息
test_motor.motor_raw_angle = (RxData[0] << 8) | RxData[1];
test_motor.motor_angle = (float)test_motor.motor_raw_angle/8191.f*360.f;
test_motor.motor_speed = (RxData[2] << 8) | RxData[3];
test_motor.motor_current = (RxData[4] << 8) | RxData[5];
test_motor.temp = RxData[6];
break;
default:
break;
}
}
}
|
定时器(系统学习)
【STM32】HAL库 STM32CubeMX教程七—PWM输出(呼吸灯)_stm32 hal pwm-CSDN博客
STM32CubeIde中互补PWM配置项的详细解读_ch idle state-CSDN博客
【【STM32入门教程】应该是全B站最好的STM32教程了!!】(https://www.bilibili.com/video/BV12v4y1y7uV?vd_source=5a0790755035f26a67935abfbfcdfd5b)
基于上述博客以及keyskingSTM32教程写的个人学习文档
定时器简介
HAL库开启定时器的基本函数
开启定时器
1
2
|
HAL_TIM_Base_Start(&htim1);//仅开启定时器
HAL_TIM_Base_Start_IT(&htim1);//开启定时器和中断
|
1
2
|
HAL_TIM_Base_Stop(&htim1);//关闭定时器
HAL_TIM_Base_Stop_IT(&htim1);//既关闭定时器,同时关闭中断
|
框架
解释整个TIM外设的构成

计数信号来源
有内部时钟以及外部时钟

内部时钟模式

在f103c8t6中,TIM1是高级定时器,TIM2,TIM3,TIM4是通用计时器
CubeMX参数配置

Prescaler:预分频,实际分频数是number+1
Counter Mode:计数模式设置
Counter Period:自动重装载计数器计数值
Internal Clock Division:内部时钟预分频
Repetition Counter:重复计数器 (RCR -8 bits),属于高级控制寄存器专用寄存器位,利用它可以非常容易控制输出 PWM 的个数,与内部时钟模式无关
auto-reload preload:是否开启影子寄存器模式
还需要开启定时器更新中断

代码实现
用的PA2作GPIO输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {//中断回调函数
if( htim == &htim1 ) {
HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_2);
}
}
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_TIM1_Init();
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim1);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
|
外部时钟模式
STM32动画讲解定时器外部时钟 & 实战传送带测速装置(B站)
__HAL_GET_COUNTER(&htim)获取计数值
外部时钟模式2
在Clock Source处选择

注意外部时钟的滤波器Clock Filter,输入滤波
输入滤波,极性选择,预分频器
采样频率等于内部时钟的频率
一般推荐选择1110(15)
外部时钟模式1
通过定时器的Slave Mode 进行选择
启用从模式,选择外部时钟模式1

选择Trigger Source时,就是选择什么方式来外部计数
就是选择触发器的来源
TI_ED是双边沿触发,且只能是双边沿触发
使用外部计数ETR
-
可以在外部时钟模式1下,Trigger Source选择ETR1
芯片上会有引脚被设置为ETR1
-
直接选择外部时钟模式2,引脚上会自动有一个ETR2

关于定时器的事件更新中断
关于定时器中断(本笔记内)
计数值到达自动重装载值时,触发更新中断
在Cube中开启global interrupt

关于MX_TIM2_Init()启动时会将中断标志位置1
在MX_TIM2_Init()时,会将定时器更新中断标志位置1,在调用HAL_TIM_Base_Start_IT后,NVIC会检测到更新中断标志位被置1,会立马触发中断

在HAL_TIM_Base_Start_IT前选择下面两个函数其中一个写上即可

事件更新中断的逻辑
先是NVIC中断向量表查找到TIM2_IRQhandler,在该函数中,调用了HAL_TIM_IRQhandler函数
其中的事件更新处理的代码(HAL库中HAL_TIM_IRQHandler中的处理事件更新中断的代码)
判断标志位,判断是否开启事件更新中断,清零标志位,调用HAL_TIM_PeriodElapsedCallback函数
定时器的从模式
STM32一看就懂的定时器从模式讲解(B站)
定时器从模式
- Reset Mode 复位模式
- Gate Mode 门模式
- Trigger Mode 触发模式
复位模式
设定为复位模式时
- 选取计数信号,计数信号可以时内部时钟,也可以是外部时钟模式2(不是从模式控制器下外部时钟模式1)
- 触发器的信号,每出现一次,执行一次复位操作(向上计数模式恢复为0,向下计数模式恢复为自动重装载值)
例子:选取计数信号为内部时钟,计数器不断计数,当触发器信号来临时(TI1的TIFP1检测到信号上升沿),则触发一次更新事件,计数器值复位
注意:如果开启定时器的事件更新中断,触发器信号也会触发事件更新中断
复位模式下的从模式控制器,在接收到触发信号后,就会让计数器重新开始计数,并且会触发定时器更新中断
如何判断事件更新中断的产生原因
在复位模式中,需要区分事件更新中断是触发信号的复位导致还是自动挡重装载时导致
从模式收到触发信号后,会设置触发器中断标志位,可以借此来区分中断原因
使用该函数来获取触发器中断标志位

1
|
__HAL_TIM_GET_FLAG(htim,TIM_FLAG_UPDATE)//可以获取定时器对应标志位的值
|
TIM_FLAG_TRIGGER是触发器中断标志位
1
|
__HAL_TIM_GET_FLAG(htim,TIM_FLAG_TRIGGER)
|
使用上述语句可以获取其标志位
使用时需要注意,HAL库为了方便我们判断,并不会对该标志位清零,所以在获取标志位的值后,需要对其进行清零
1
2
3
|
if(__HAL_TIM_GET_FLAG(htim,TIM_FLAG_TRIGGER)==SET) {
__HAL_TIM_CLEAR_FLAG(htim,TIM_FLAG_TRIGGER);
}
|
HAL_TIM_PeriodElapsedCallback回调函数
1
2
3
4
5
6
7
8
9
10
11
|
void HAL_TIM_PeriodElapsedCallback( TIM_HandleTypeDef *htim ) {
if( htim == &htim2 ) {
if( __HAL_TIM_GET_FLAG(htim,TIM_FLAG_TRIGGER) == SET ) {//获取触发标志位,用来区分复位引起的中断还是自动重装载引起的中断
__HAL_TIM_CLEAR_FLAG(htim,TIM_FLAG_TRIGGER);
HAL_UART_Transmit( &huart2 , (uint8_t*)triggerMsg , strlen(triggerMsg) , 100 );
}
else {
HAL_UART_Transmit( &huart2 , (uint8_t*)updateMsg , strlen(updateMsg) , 100 );
}
}
}
|
关于MX_TIM2_Init()启动时会将中断标志位置1
在MX_TIM2_Init()时,会将定时器更新中断标志位置1,在调用HAL_TIM_Base_Start_IT后,NVIC会检测到更新中断标志位被置1,会立马触发中断

在HAL_TIM_Base_Start_IT前选择下面两个函数其中一个写上即可

事件更新中断的逻辑
先是NVIC中断向量表查找到TIM2_IRQhandler,在该函数中,调用了HAL_TIM2_IRQhandler函数
其中的事件更新处理处的代码(HAL库中HAL_TIM_IRQHandler中的处理事件更新中断的代码)
判断标志位,判断是否开启事件更新中断,清零标志位,调用HAL_TIM_PeriodElapsedCallback函数
门模式
如果TI1的TI1FP1为上升沿检测,则是当触发信号是高电平时,时钟信号可以正常进入定时器,定时器能正常计数
当触发信号时低电平时,门关闭,时钟信号不能正常进入定时器,定时器停止计数
若将触发器的极性改为检测下降沿的话,门的控制作用则相反
触发器的极性选择

需要注意,在门模式情况下,上升下降沿的时刻,也会将触发器中断标志位置1,但是不会触发事件更新中断(因为没有对计数值进行复位,就不会触发事件更新中断)

触发模式
在检测到设定的边沿后,让定时器开始计数
开始后不能设定停止
触发模式仅能启动定时器,并不能停止计数器计数
一般配合计时器的单脉冲模式使用
单脉冲模式,是指计数器计数到重装载计数值后,就停止计数(不再循环计数)

一般需要勾选单脉冲模式

三种模式的总结

Trigger Source 触发源的滤波器设置
在有触发源的时候,对触发源的信号的滤波设置

定时器的输入捕获
软件层面上的模拟
在软件层面检测Echo端的电平高低,进行计时,到那时再极高精度要求的情况则 力不足
输入捕获的概括
硬件在检测到上升或者下降沿时,就记录计数器值到捕获寄存器
输入捕获的运行机制
在TI1检测到上升沿时,捕获寄存器立即复制计数器的值
还可以开启输入捕获中断,输入捕获后,启动中断,可以快速处理捕获寄存器上的值
一个通道只能进行一种边沿检测
捕获的模式

注意:TI3和TI4没有接入从模式控制器中


TI1和TI2是一对,可以相互借用
TI3和TI4是一对,可以相互借用

Cube上的配置
注意设置边沿检测

打开捕获中断

启动,该例子中是需要在CHANNEL4捕获到后进行处理,所以CHANNEL44开启了中断
输入捕获的开启

可能遇到的问题

用SET_COUNTER来清零一下
中断回调函数
1
|
HAL_TIM_IC_CaptureCallback(htim);
|
回调函数的代码编写
判断是否是TIM1,判断是否是TIM1的4通道(HAL_TIM_ACTIVE_CHANNEL_4)
再读取指定寄存器的值
定时器的输出比较模式
较为鸡肋,对这些模式能理解其行为即可
定时器的PWM
【STM32】HAL库 STM32CubeMX教程七—PWM输出(呼吸灯)_stm32 hal pwm-CSDN博客
STM32CubeIde中互补PWM配置项的详细解读_ch idle state-CSDN博客
PWM
脉冲宽度调制,用数字信号去模拟 模拟信号

PWM模式

- PWM模式1 是 计数器值 小于 比较器值 时输出有效电平
- PWM模式2 是 计数器值 大于 比较器值 时输出有效电平
Cube配置
为实现一个呼吸灯,进行以下配置
PWM的频率为1000Hz,最大计数值为1000-1
基础时钟配置

预分频器值,计数模式选择(向上计数模式,向下计数模式,中央对齐模式),自动重装载值,内部时钟分频,重复计数器值,是否开启影子寄存器
PWM配置

- Mode:选择PWM模式
- Pulse:设置比较寄存器的值
- Output compare preload:是否开启影子寄存器
- Fast Mode:是否需要输出非常高频的PWM信号
- CH Polarity:设置有效电平极性
- CH Idle State:空闲时,PWM通道的输出
HAL库PWM常用函数
1
2
3
|
HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);//开启PWM
HAL_TIM_PWM_Stop(&htim1,TIM_CHANNEL_1);//关闭PWM
__HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_1,num);//设置比较寄存器的值
|
使用PWM控制流水灯
PWM的频率为1000Hz,最大计数值为1000-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
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_TIM1_Init();
/* USER CODE BEGIN 2 */
HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
for( int i = 0 ; i < 1000 ; ++ i ) {
__HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_1,i);
HAL_Delay(1);
}
for( int i = 1000-1 ; i >= 0 ; -- i ) {
__HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_1,i);
HAL_Delay(1);
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
|
编码器的使用
(给我自己看的)
STM32CubeMX笔记(10)–定时器TIM4的Encoder Mode(编码器模式)使用-CSDN博客
UART、DMA与空闲中断(综合)
(stm32之HAL库)UART工作在DMA模式要打开串口中断吗?_hal uart dma-CSDN博客
10 通用同步异步收发器(USART) - 知乎
串口简介
串口是串行接口的简称
常用串口:RS232,RS485,USB(Universal Serial Bus)通用串行接口,TTL串口
TTL是Transistor-Transistor Logic的简写,是一种电平逻辑,晶体管-晶体管逻辑
USART简介
USART,即通用同步/异步收发传输器(Universal Asynchronous Receiver/Transmitter),是单片机上的一个外设
UART遵循异步串行通信协议的,USART遵循同步串行通信协议
异步不需要统一时钟信号,同步需要统一时钟信号
一般使用最多的是UART,我们此次学习也是针对UART
UART通信协议

- 起始位:发出1位低电平信号,表示开始传输字符
- 数据位:真正发送的数据,一般为8位(1个字节),常采用ASCII编码,从最低位开始发送
- 校验位:用于检验接收到的数据是否正确,分为奇校验和偶校验
- 停止位:一组数据的结束传输的标志。可以是1位、1.5位、2位的高电平
- 空闲位:空闲时数据线为高电平状态,代表无数据传输
- 波特率:衡量传输速率的指标。UART通信中波特率等于比特率
UART发送/接收机制
UART发送端口,首先是CPU将数据放入数据发送寄存器,然后发送移位寄存器会将数据从数据发送寄存器 取出进行移位并通过UART_TX发送出去
UART接收端口,首先是UART_RX接收串行数据至接收移位寄存器,接收移位寄存器将串行数据移位变换为并行数据给数据接收寄存器,再通过APB总线给STM32内部
UART模式
轮询模式 / 中断模式(去重说明)
这两种模式的底层原理与发送/接收时序,前文USART中断章节已经完整展开,此处不再重复。
如果你想快速回看:
- 轮询模式:CPU持续查询寄存器状态,流程直观但会阻塞。
- 中断模式:底层按字节触发中断搬运,上层在回调中处理完整数据。
DMA模式
DMA,全称为Direct Memory Access,直接内存访问,本质是将传输数据从一个内存空间搬运至另一个内存空间,可以用来提供外设和内存,内存和外设之间的高速数据传输
DMA模式中也会有中断参与,在Cube配置中需要开启中断
发送
DMA接管“内存 -> 串口寄存器”的搬运,CPU不再逐字节参与发送中转。
接收
DMA接管“串口寄存器 -> 内存”的搬运,CPU主要负责在回调函数里处理数据。
Cube配置解析
基础参数配置

- Baud Rate:波特率
- Word Length:包含校验位在内的数据包长度
- Parity:奇偶校验位
- Stop Bits:停止位位数
- Data Direction:发送和
NVIC配置
如果使用中断模式,需要在NVIC settings开启USARTi global interrupt

DMA配置

- Add:选择添加
USARTi_RX的DMA或者USARTi_TX的DMA
- Delete:删除对应的DMA
- Mode:选择
normal模式或者circular模式
- Data Byte:数据传输的位数
- Increment Address:开启地址自增,外设只有一个寄存器,地址固定,不需要开启地址自增,而内存中我们存数据的变量是数组,接受一字节数据后需要往后移,需要开启地址自增
- Priority:优先级设置
- Direction:方向,固定死的
GPIO配置

一般用来查看引脚的GPIO配置,不需要更改(有特殊需求时,可以按需修改)
在GPIO Settings处可以查看TX和RX的GPIO引脚配置,此处可以看到RX为输入模式,无上拉无下拉
HAL库UART常用函数
轮询模式
1
2
|
HAL_UART_Receive(&huart1, rxBuffer, 16 , HAL_MAX_DELAY);//接收
HAL_UART_Transmit(&huart1 , txBuffer , sizeof(txBuffer) , 100 );//发送
|
中断模式
只能接收一次,接收完成后需要再次开启
1
2
3
4
5
6
7
|
HAL_UART_Receive_IT(&huart1, rxBuffer, 16 );//接收
HAL_UART_Transmit_IT(&huart1 , txBuffer , sizeof(txBuffer) );//发送
//回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
}
|
DMA模式
(stm32之HAL库)UART工作在DMA模式要打开串口中断吗?hal uart dma-CSDN博客
1
2
3
4
5
6
7
|
HAL_UART_Receive_DMA(&huart1, rxBuffer, 16 );//接收
HAL_UART_Transmit_DMA(&huart1 , txBuffer , sizeof(txBuffer) );//发送
//回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
}
|
HAL库中使用的HAL_UART_Transmit_DMA,HAL_UART_Receive_DMA等都有中断的参与,需要在Cube中开启中断
Demo
接收定长数据Nomad_violet,同时在主函数中翻转小灯
轮询实现数据收发
Cube配置
模式选择为异步

波特率选择115200 Bit/s,其余不动即可

将PA8配置为GPIO_Output

GPIO配置保持默认即可

代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#include "string.h"
#define rxBufferLen 16
#define txBufferLen 25
uint8_t rxBuffer[rxBufferLen] ;
uint8_t txBuffer[txBufferLen] = "to feel,to experience ";// 去感受,去经历「感其生,历其境」
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
while (1)
{
HAL_UART_Receive(&huart1, rxBuffer, 12 , HAL_MAX_DELAY);//只能接收定长数据
if( !strcmp( (char *)rxBuffer , "Nomad_violet" ) ) {//Nomad_violet 12字节
HAL_UART_Transmit(&huart1 , txBuffer , sizeof(txBuffer) , 100 );
}
HAL_GPIO_TogglePin(GPIOA , GPIO_PIN_8 );
}
}
|
中断实现数据收发
Cube配置
配置同上,只需额外开启中断即可

代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
#include "string.h"
#define rxBufferLen 12
#define txBufferLen 25
uint8_t rxBuffer[rxBufferLen] ;
uint8_t txBuffer[txBufferLen] = "to feel,to experience ";// 去感受,去经历「感其生,历其境」
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if(huart == &huart1) {
if( !strcmp( (char *)rxBuffer , "Nomad_violet" ) ) {
HAL_UART_Transmit(&huart1 , txBuffer , sizeof(txBuffer) , 100 );
}
HAL_UART_Receive_IT(&huart1, rxBuffer, rxBufferLen );
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
HAL_UART_Receive_IT(&huart1, rxBuffer, rxBufferLen );
while (1)
{
HAL_GPIO_TogglePin(GPIOA , GPIO_PIN_8 );
HAL_Delay(500);
}
}
|
STM32 hal库串口空闲中断最新用法 - STM32团队 ST意法半导体中文论坛
【STM32】 DMA原理,步骤超细详解,一文看懂DMA-CSDN博客(非常详细)
STM32CubeIDE HAL库DMA与UART不定长数据接收实现:空闲中断详解-物联沃-IOTWORD物联网
STM32 hal库串口空闲中断最新用法_stm32hal库串口空闲中断-CSDN博客
HAL库 串口空闲中断+DMA接收不定长数据 详解及踩坑_hal库串口空闲中断-CSDN博客
(stm32之HAL库)UART工作在DMA模式要打开串口中断吗?hal uart dma-CSDN博客
(写的非常好,非常有助于提升底层理解)
DMA(补充)
说明:本节是 DMA 原理与细节补充,和上文“UART、DMA与空闲中断(综合)”互为补充。
DMA,全称为Direct Memory Access,直接内存访问,本质是将传输数据从一个内存空间搬运至另一个内存空间,可以用来提供外设和内存,内存和外设之间的高速数据传输
在HAL库中直接使用的HAL_UART_Transmit_DMA,HAL_UART_Receive_DMA,HAL_UARTEx_ReceiveToIdle_DMA等都有中断的参与,需要在Cube中开启中断

对具体底层原理和DMA模式实现底层与中断模式底层实现区别感兴趣可参考以下博客
(stm32之HAL库)UART工作在DMA模式要打开串口中断吗?_hal uart dma-CSDN博客
USART发送
轮询/中断下的发送机制前文已讲,这里只保留 DMA 发送新增的关键点。
USART的DMA发送
没有DMA的话,CPU会作为数据发送的中转站,而使用DMA的话,整个数据传输过程不需要CPU的参与,由DMA代理执行,DMA负责将内存数据搬运至外设的传输寄存器
在整个数据发送过半时会触发一个传输过半中断,所有数据传输完成后会调用传输完成中断回调函数(HAL_UART_TxCpltCallback)
1
|
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size)
|
HAL_UART_Transmit_DMA的传输完成中断回调函数HAL_UART_TxCpltCallback以及传输过半中断回调函数HAL_UART_TxHalfCpltCallback(这两个回调函数在HAL_UART_Transmit_IT中同样有)
DMA发送注意事项
在传输的过程中,当传输一半的数据后,半传输标志(HTIF)被置1,当设置了允许半传输中断位(HTIE)时,将产生一个中断请求。在数据传输结束后,传输完成标志(TCIF)被置1,当设置了允许传输完成中断位 (TCIE)时,将产生一个中断请求
会触发的三个中断,传输完成中断,传输过半中断,错误中断
在stm32f1xx_hal_uart.c的HAL_UART_Transmit_DMA函数中可以查看到
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size)
{
.
.
.
/* Set the UART DMA transfer complete callback */
huart->hdmatx->XferCpltCallback = UART_DMATransmitCplt;
/* Set the UART DMA Half transfer complete callback */
huart->hdmatx->XferHalfCpltCallback = UART_DMATxHalfCplt;
/* Set the DMA error callback */
huart->hdmatx->XferErrorCallback = UART_DMAError;
.
.
.
}
}
|
USART接收
轮询/中断下的接收机制前文已讲,这里只保留 DMA 接收新增的关键点。
USART的DMA接收
没有DMA的话,CPU会作为数据转运的中转站,而使用DMA的话,整个数据转运过程不需要CPU的参与,由DMA代理执行
在整个数据接收过半时会触发一个传输过半中断,所有数据接收完成后会调用传输完成中断回调函数(HAL_UART_RxCpltCallback)
1
|
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size)
|
HAL_UART_Receive_DMA的接收完成中断回调函数HAL_UART_RxCpltCallback以及接收过半中断回调函数HAL_UART_RxHalfCpltCallback(这两个回调函数在HAL_UART_Receive_IT中同样有)
DMA接收注意事项
同DMA发送,类比即可
HAL库USART使用DMA(补充)
说明:本节聚焦 HAL API 视角,示例与上文综合章节可交叉阅读。
如果USART串口的DMA出现错误,会调用HAL_UART_ErrorCallback
DMA发送
1
|
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size)
|
会触发中断,对应回调函数为
HAL_UART_TxCpltCallback
HAL_UART_TxHalfCpltCallback
DMA接收
1
|
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size)
|
会触发的中断,对应的回调函数为
HAL_UART_RxCpltCallback
HAL_UART_RxHalfCpltCallback
DMA模式
normal模式
以接收为例
单次接收,接收至数据上限,完成整个固定数据接收后停止,再次接收需要再次开启HAL_UART_Receive_DMA
,需要软件显式重启,触发接收过半和接收完成中断
总结:单次传输,完成后停止,需要软件显式重启,接收过半和接收完成中断
circular模式
以接收为例
无限循环,接收至数据上限一半,触发接收过半中断,接受至数据上限,触发接收完成中断,同时会硬件自动重启,接收完成中断会调用对应的接收完成中断回调函数
总结:无限循环,硬件自动重启,接收过半和接收完成中断
空闲中断
空闲中断定义
空闲中断(IDLE Interrupt) 是UART通信中的一种特殊中断类型,它在检测到UART接收线路连续空闲超过1个字节传输时间时触发的中断。“空闲"指线路保持逻辑1状态(高电平),线上无数据传输
HAL库自带的空闲中断实现
使用的是HAL库提供的拓展函数,同时都需要在Cube中开启中断
1
2
|
//在阻塞模式下接收一定数量的数据,直到接收到预期数量的数据或发生空闲事件。
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint16_t *RxLen, uint32_t Timeout);
|
1
2
|
//在中断模式下接收一定数量的数据,直到接收到预期数量的数据或发生空闲事件。
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
|
1
2
|
//在DMA模式下接收一定数量的数据,直到接收到预期数量的数据或发生空闲事件。
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
|
1
2
|
//使用空闲中断时的接收回调函数
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size);
|
在触发空闲中断,接收完成中断都会调用HAL_UARTEx_RxEventCallback回调函数,需要注意的是在F103c8t6单片机以及其他st系列单片机上,使用HAL_UARTEx_ReceiveToIdle_DMA时,接收过半中断也会去调用该回调函数
接收过半中断的处理
STM32CubeIDE HAL库DMA与UART不定长数据接收实现:空闲中断详解-物联沃-IOTWORD物联网
HAL_UARTEx_ReceiveToIdle_DMA会调用UART_Start_Receive_DMA(huart, pData, Size)函数,该函数会把DMA中断传输完成,半传输,传输错误全部开启
1
|
__HAL_DMA_ENABLE_IT(DMA_IT_TC | DMA_IT_HC | DMA_IT_TE)
|
UART_Start_Receive_DMA(huart, pData, Size) 设置接收数组缓存到达数组一半时会执行下面的回调函数
1
2
|
/* Set the UART DMA Half transfer complete callback */
huart->hdmarx->XferHalfCpltCallback = UART_DMARxHalfCplt;
|
在DMA的中断处理函数中,会调用huart->hdmarx->XferHalfCpltCallback,此处即UART_DMARxHalfCplt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
void HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma)
{
uint32_t flag_it = hdma->DmaBaseAddress->ISR;
uint32_t source_it = hdma->Instance->CCR;
/* Half Transfer Complete Interrupt management ******************************/
if (((flag_it & (DMA_FLAG_HT1 << hdma->ChannelIndex)) != RESET) && ((source_it & DMA_IT_HT) != RESET))
{
......
if(hdma->XferHalfCpltCallback != NULL)
{
/* Half transfer callback */
hdma->XferHalfCpltCallback(hdma);
}
......
}
......
}
|
而UART_DMARxHalfCplt会调用HAL_UARTEx_RxEventCallback
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
static void UART_DMARxHalfCplt(DMA_HandleTypeDef *hdma)
{
UART_HandleTypeDef *huart = (UART_HandleTypeDef *)((DMA_HandleTypeDef *)hdma)->Parent;
/* Initialize type of RxEvent that correspond to RxEvent callback execution;
In this case, Rx Event type is Half Transfer */
huart->RxEventType = HAL_UART_RXEVENT_HT;
/* Check current reception Mode :
If Reception till IDLE event has been selected : use Rx Event callback */ if (huart->ReceptionType == HAL_UART_RECEPTION_TOIDLE)
{
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
/*Call registered Rx Event callback*/
huart->RxEventCallback(huart, huart->RxXferSize / 2U);
#else
/*Call legacy weak Rx Event callback*/
HAL_UARTEx_RxEventCallback(huart, huart->RxXferSize / 2U);
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */
}
else
{
/* In other cases : use Rx Half Complete callback */
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
/*Call registered Rx Half complete callback*/
huart->RxHalfCpltCallback(huart);
#else
/*Call legacy weak Rx Half complete callback*/
HAL_UART_RxHalfCpltCallback(huart);
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */
}
}
|
为了避免接收过半中断的调用,需要使用下面的语句
1
|
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx,DMA_IT_HT);//关闭dma接收半满中断函数,这样我们在接收一组数据时就不会触发半满中断,dma就可以正常接收一组数据
|
另外在UART_DMAReceiveCplt函数中也调用了HAL_UARTEx_RxEventCallback函数
1
|
static void UART_DMAReceiveCplt(DMA_HandleTypeDef *hdma)
|
DMA+空闲中断的实现接收不定长数据
方式1
自定义DMA+空闲中断
前置内容
需要自己写的宏定义
1
2
|
#define __HAL_DMA_SET_COUNTERT(__HANDLE__,__COUNTER__) ((__HANDLE__)->Instance->CNDTR = (uint16_t)(__COUNTER__))
//重新设定计数值
|
1
2
|
//用法
__HAL_DMA_SET_COUNTERT(huart->hdmarx,rxBufferLen);
|
用到的宏定义
1
2
|
__HAL_DMA_GET_COUNTER(huart->hdmarx)
//获取剩余未传输的计数值
|
用到的关于中断的操作
1
2
3
4
5
6
7
8
9
10
|
__HAL_DMA_DISABLE(huart->hdmarx)//关闭DMA
__HAL_DMA_ENABLE(huart->hdmarx)//开启DMA
//清除空闲标志位
__HAL_UART_CLEAR_IDLEFLAG(&huart1)
//开启空闲中断
__HAL_UART_ENABLE_IT(&huart1 , UART_IT_IDLE )
//关闭空闲中断
__HAL_UART_DISABLE_IT(&huart1, UART_IT_IDLE )
|
代码实现
1
2
3
|
#define rxBufferLen 10
uint8_t rxBuffer[rxBufferLen];
uint8_t txBuffer[rxBufferLen];
|
1
2
3
4
5
|
void UART_InitDMAReceive() {
__HAL_UART_CLEAR_IDLEFLAG(&huart1);//清除空闲中断标志位
__HAL_UART_ENABLE_IT(&huart1 , UART_IT_IDLE );//使能空闲中断
HAL_UART_Receive_DMA(&huart1, (uint8_t*)rxBuffer, rxBufferLen);//开启DMA接收
}
|
需要关闭DMA才能重设DMA的COUNTER
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
//自定义的DMA空闲中断回调函数
void UART_DMAIdleCallback(UART_HandleTypeDef *huart) {
if (huart == &huart1 ) {
__HAL_DMA_DISABLE(huart->hdmarx);//失能DMA
int datalen = rxBufferLen - __HAL_DMA_GET_COUNTER(huart->hdmarx);
for(int i=0;i<datalen;i++) {
char temp = rxBuffer[i];
txBuffer[i] = (temp >= 'a' && temp <= 'z') ? temp-32:temp;
}
HAL_UART_Transmit_DMA(&huart1,(uint8_t*)txBuffer,datalen);
__HAL_DMA_SET_COUNTERT(huart->hdmarx,rxBufferLen);//重置DMA计数
__HAL_DMA_ENABLE(huart->hdmarx);//使能DMA
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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) != RESET ) {//在此判断是否为空闲中断
__HAL_UART_CLEAR_IDLEFLAG(&huart1);//清除空闲中断标志位
__HAL_UART_DISABLE_IT(&huart1, UART_IT_IDLE);//失能空闲中断
UART_DMAIdleCallback(&huart1);//调用自定义中断回调函数
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);//使能空闲中断
}
/* USER CODE END USART1_IRQn 1 */
}
|
关于normal和circular在方式1的空闲中断实现的区别
normal模式
在上述空闲中断实现中,如果使用normal模式,需要注意接收的数据一定不能超过rxBufferLen,一旦超过,DMA接收就会终止
执行流程是,触发空闲中断,进入USART1_IRQHandler,再进入自定义空闲中断,在里面重新设置了DMA的计数值(指针也会重新设置,重新指向rxBuffer[0]),最开始开启的HAL_UART_Receive_DMA始终没有接收完 完整的数据,就不会关闭,一直接收
但如果接收的数据超过rxBufferLen,会触发接收过半和接收完成中断(对应回调函数没使用,约等于空函数),同时HAL_UART_Receive_DMA会结束,需要软件显式重启
例如:rxBufferlen=10,发送12345678910,会回复1234567891,随后DMA终止,再发送数据无回复
circular模式
在上述空闲中断实现中,如果使用circular模式,则接收的数据可以超过rxBufferLen,超过后,DMA接收会自动重启
执行流程是,触发空闲中断,进入USART1_IRQHandler,再进入自定义空闲中断,在里面重新设置了DMA的计数值(指针也会重新设置,重新指向rxBuffer[0]),就算接收数据超过rxBufferLen,HAL_UART_Receive_DMA也会重新启动
但是需要注意,如果达到rxBufferLen,会触发接收过半和接收完成中断,然后硬件重启,重新回到rxBufferLen[0]的位置读数据,直至发生空闲中断
例如:rxBufferlen=10,发送12345678910,会回复0,随后DMA仍然正常工作
方式2
使用HAL库提供的拓展空闲中断,使用normal模式
该方式下的空闲中断回调函数为HAL_UARTEx_RxEventCallback
,需要注意接收过半中断也会调用HAL_UARTEx_RxEventCallback的问题,在每次开启HAL_UARTEx_ReceiveToIdle_DMA后都需要关闭接收过半中断
1
|
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx,DMA_IT_HT);//关闭dma接收半满中断函数,这样我们在接收一组数据时就不会触发半满中断,dma就可以正常接收一组数据
|
注意需要添加
1
2
3
|
extern UART_HandleTypeDef huart1;
extern DMA_HandleTypeDef hdma_usart1_rx;
extern DMA_HandleTypeDef hdma_usart1_tx;
|
当接收数据超过rxBufferLen时,接收完成中断函数会调用HAL_UARTEx_RxEventCallback回调函数,然后重新开启接收
例如:rxBufferLen=10,接收12345678910,发送1234567891(就是因为接收完成中断导致),后面DMA空闲中断接收正常开启
代码实现
1
2
3
4
5
6
7
|
extern UART_HandleTypeDef huart1;
extern DMA_HandleTypeDef hdma_usart1_rx;
extern DMA_HandleTypeDef hdma_usart1_tx;
#define rxBufferLen 512
uint8_t rxBuffer[rxBufferLen];
uint8_t txBuffer[rxBufferLen];
|
1
2
3
4
|
void Init_IdleDMA(void) {//初始化,开启DMA空闲中断接收
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, (uint8_t*)rxBuffer, rxBufferLen);
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);//关闭f103板子DMA的接收过半中断
}
|
1
2
3
4
5
6
7
8
9
|
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {//接收完成和接收过半,空闲中断都会调用该中断回调函数
if (huart == &huart1 ) {
memcpy(txBuffer, rxBuffer, Size);
HAL_UART_Transmit(&huart1, (uint8_t*)txBuffer, Size,10);
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, (uint8_t*)rxBuffer, rxBufferLen);//normal模式,接受一次后需要重新开启
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);//关闭f103板子DMA的接收过半中断
}
//Init_IdleDMA();
}
|
关于normal和circular在方式2下的空闲中断实现的区别
normal模式
方式2中,在数据接收过半,接收完成,空闲时会触发对应中断,最后调用HAL_UARTEx_RxEventCallback回调函数,在回调函数中软件显式重启HAL_UARTEx_ReceiveToIdle_DMA
circular模式
方式2中,若使用circular模式,则数据会一直积累,到达rxBufferLen时,会硬件重启,同时也会有数据接收过半,接收完成,空闲时会触发对应中断,最后调用HAL_UARTEx_RxEventCallback回调函数
例如:
(1)关闭接收过半中断
rxBufferLen=10,接收123,发送123,
再接收456,发送123456,
再接收78910,会先发送1234567891(接收完成),再发送0(空闲中断),
再接收111213,发送0111213
(2)未关闭接收过半中断
rxBufferLen=10,接收123,发送123,
再接收123,会先发送12312(接收过半中断),再发送123123空闲中断,
再接收12345,会先发送1231231234(接收完成中断),再发送5空闲中断
方式3
使用HAL库提供的空闲中断,但是使用circular模式实现
在circuar要想实现一帧一帧不定长数据的接收主要需要考虑的就是如何实现在接收完一帧后让DMA的计数值重置
实现原理
关闭
要实现DMA计数值的重置,需要使用HAL_UART_DMAStop(&huart1)函数将DMA关闭
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart)
{
uint32_t dmarequest = 0x00U;
/* The Lock is not implemented on this API to allow the user application
to call the HAL UART API under callbacks HAL_UART_TxCpltCallback() / HAL_UART_RxCpltCallback(): when calling HAL_DMA_Abort() API the DMA TX/RX Transfer complete interrupt is generated and the correspond call back is executed HAL_UART_TxCpltCallback() / HAL_UART_RxCpltCallback() */
/* Stop UART DMA Tx request if ongoing */
dmarequest = HAL_IS_BIT_SET(huart->Instance->CR3, USART_CR3_DMAT);
if ((huart->gState == HAL_UART_STATE_BUSY_TX) && dmarequest)
{
ATOMIC_CLEAR_BIT(huart->Instance->CR3, USART_CR3_DMAT);
/* Abort the UART DMA Tx channel */
if (huart->hdmatx != NULL)
{
HAL_DMA_Abort(huart->hdmatx);
}
UART_EndTxTransfer(huart);
}
/* Stop UART DMA Rx request if ongoing */
dmarequest = HAL_IS_BIT_SET(huart->Instance->CR3, USART_CR3_DMAR);
if ((huart->RxState == HAL_UART_STATE_BUSY_RX) && dmarequest)
{
ATOMIC_CLEAR_BIT(huart->Instance->CR3, USART_CR3_DMAR);
/* Abort the UART DMA Rx channel */
if (huart->hdmarx != NULL)
{
HAL_DMA_Abort(huart->hdmarx);
}
UART_EndRxTransfer(huart);
}
return HAL_OK;
}
|
其中会调用HAL_DMA_Abort(huart->hdmatx),但都不会重置DMA的计数值
开启
在关闭DMA后,需要HAL_UARTEx_ReceiveToIdle_DMA(&huart1, (uint8_t*)rxBuffer, rxBufferLen)重新开启DMA以及空闲中断
在HAL_UARTEx_ReceiveToIdle_DMA中,会进行判断if (huart->RxState == HAL_UART_STATE_READY) ,判断RxState是否为HAL_UART_STATE_READY,如果是,则说明uart没有开启,但是是在预备状态,随后会去调用UART_Start_Receive_DMA(huart, pData, Size)开启uart以及DMA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
HAL_StatusTypeDef status;
/* Check that a Rx process is not already ongoing */
if (huart->RxState == HAL_UART_STATE_READY)
{
if ((pData == NULL) || (Size == 0U))
{
return HAL_ERROR;
}
/* Set Reception type to reception till IDLE Event*/
huart->ReceptionType = HAL_UART_RECEPTION_TOIDLE;
huart->RxEventType = HAL_UART_RXEVENT_TC;
status = UART_Start_Receive_DMA(huart, pData, Size);
/* Check Rx process has been successfully started */
if (huart->ReceptionType == HAL_UART_RECEPTION_TOIDLE)
{
__HAL_UART_CLEAR_IDLEFLAG(huart);
ATOMIC_SET_BIT(huart->Instance->CR1, USART_CR1_IDLEIE);
}
else
{
/* In case of errors already pending when reception is started,
Interrupts may have already been raised and lead to reception abortion. (Overrun error for instance). In such case Reception Type has been reset to HAL_UART_RECEPTION_STANDARD. */ status = HAL_ERROR;
}
return status;
}
else
{
return HAL_BUSY;
}
}
|
在UART_Start_Receive_DMA中会将uart的RxState设置为HAL_UART_STATE_BUSY_RX,中间还会去调用HAL_DMA_Start_IT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
HAL_StatusTypeDef UART_Start_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
uint32_t *tmp;
huart->pRxBuffPtr = pData;
huart->RxXferSize = Size;
huart->ErrorCode = HAL_UART_ERROR_NONE;
huart->RxState = HAL_UART_STATE_BUSY_RX;
......
HAL_DMA_Start_IT(huart->hdmarx, (uint32_t)&huart->Instance->DR, *(uint32_t *)tmp, Size);
......
}
|
在HAL_DMA_Start_IT会调用DMA_SetConfig去对DMA计数值等进行重置
1
2
3
4
5
6
7
|
HAL_StatusTypeDef HAL_DMA_Start_IT(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength)
{
......
/* Configure the source, destination address and the data length & clear flags*/
DMA_SetConfig(hdma, SrcAddress, DstAddress, DataLength);
......
}
|
原理总结
1.HAL_UARTEx_ReceiveToIdle_DMA如果判断出huart->RxState == HAL_UART_STATE_READY会调用UART_Start_Receive_DMA
2.UART_Start_Receive_DMA会调用HAL_DMA_Start_IT,同时会将RxState修改为BUSY
huart->RxState = HAL_UART_STATE_BUSY_RX
3.HAL_DMA_Start_IT会调用DMA_SetConfig进行DMA计数值等的重置
而使用HAL_UART_DMAStop可以保证在回调函数处理数据时停止DMA接收,保证数据不会被覆盖,同时可以将huart->RxState状态设置为HAL_UART_STATE_READY,随后再次开启DMA空闲中断接收就能重置DMA计数值
从而实现非定长数据的接收
代码实现
同样注意,需要关闭接收过半中断
1
2
3
4
5
6
7
8
|
extern UART_HandleTypeDef huart1;
extern DMA_HandleTypeDef hdma_usart1_rx;
extern DMA_HandleTypeDef hdma_usart1_tx;
#define rxBufferLen 512
uint8_t rxBuffer[rxBufferLen];
uint8_t txBuffer[rxBufferLen];
|
1
2
3
4
5
|
//初始化
void Init_IdleDMA(void) {
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, (uint8_t*)rxBuffer, rxBufferLen);
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);//关闭DMA的接收过半中断
}
|
1
2
3
4
5
6
7
8
9
10
11
12
|
//回调函数
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {//接收完成和接收过半,空闲中断都会调用该中断回调函数
if (huart == &huart1 ) {
HAL_UART_DMAStop(&huart1);//不会改变DMA的计数值
memcpy(txBuffer, rxBuffer, Size);
HAL_UART_Transmit(&huart1, (uint8_t*)txBuffer, Size,10);
memset(txBuffer, 0, Size);
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, (uint8_t*)rxBuffer, rxBufferLen);//如果是normal模式,接受一次后需要重新开启,如果关闭了DMA,也需要重新启动
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);//关闭f103板子DMA的接收过半中断
}
}
|
实验检验
我们设置cnt_i去记录HAL_UART_DMAStop(&huart1)前后以及HAL_UARTEx_ReceiveToIdle_DMA(&huart1, (uint8_t*)rxBuffer, rxBufferLen)前后的DMA计数值变化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
#define rxBufferLen 512
uint8_t rxBuffer[rxBufferLen];
uint8_t txBuffer[rxBufferLen];
void Init_IdleDMA(void) {
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, (uint8_t*)rxBuffer, rxBufferLen);
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);//关闭DMA的接收过半中断
}
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {//接收完成和接收过半,空闲中断都会调用该中断回调函数
if (huart == &huart1 ) {
uint32_t cnt_1 = __HAL_DMA_GET_COUNTER(huart1.hdmarx);
HAL_UART_DMAStop(&huart1);//不会改变DMA的计数值
uint32_t cnt_2 = __HAL_DMA_GET_COUNTER(huart1.hdmarx);
memcpy(txBuffer, rxBuffer, Size);
HAL_UART_Transmit(&huart1, (uint8_t*)txBuffer, Size,10);
memset(txBuffer, 0, Size);
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, (uint8_t*)rxBuffer, rxBufferLen);//如果是normal模式,接受一次后需要重新开启,如果关闭了DMA,也需要重新启动
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);//关闭f103板子DMA的接收过半中断
uint32_t cnt_3 = __HAL_DMA_GET_COUNTER(huart1.hdmarx);
sprintf(txBuffer, "\ncnt_1=%d cnt_2=%d cnt_3=%d\n", cnt_1,cnt_2,cnt_3);
HAL_UART_Transmit(&huart1, (uint8_t*)txBuffer, sizeof(txBuffer), 10);
}
}
|
实验结果
1
2
3
4
|
输入:123456789101112131415161718192021
输出:123456789101112131415161718192021
cnt_1=479 cnt_2=479 cnt_3=512
|
证明我们在重启DMA空闲中断时实现了DMA计数值重置
方式4
再仔细想一想,在circuar要想实现一帧一帧不定长数据的接收主要需要考虑的就是如何实现在接收完一帧后让DMA的计数值重置,也就是说没有必要为此使用HAL_UART_DMAStop再重启DMA空闲中断接收
如何优化呢,结合方式1的实现就可以得到答案
1
2
3
|
__HAL_DMA_DISABLE(huart->hdmarx);//需要关闭DMA才能重设DMA的COUNTER
__HAL_DMA_SET_COUNTERT(huart->hdmarx,rxBufferLen);
__HAL_DMA_ENABLE(huart->hdmarx);//需要关闭DMA才能重设DMA的COUNTER
|
代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
#define __HAL_DMA_SET_COUNTERT(__HANDLE__,__COUNTER__) ((__HANDLE__)->Instance->CNDTR = (uint16_t)(__COUNTER__))
//重新设定计数值
extern UART_HandleTypeDef huart1;
extern DMA_HandleTypeDef hdma_usart1_rx;
extern DMA_HandleTypeDef hdma_usart1_tx;
#define rxBufferLen 512
uint8_t rxBuffer[rxBufferLen];
uint8_t txBuffer[rxBufferLen];
void Init_IdleDMA(void) {
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, (uint8_t*)rxBuffer, rxBufferLen);
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);//关闭DMA的接收过半中断
}
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
if (huart == &huart1 ) {
__HAL_DMA_DISABLE(huart->hdmarx);//需要关闭DMA才能重设DMA的COUNTER
__HAL_DMA_SET_COUNTERT(huart->hdmarx,rxBufferLen);
memcpy(txBuffer, rxBuffer, Size);
HAL_UART_Transmit(&huart1, (uint8_t*)txBuffer, Size,10);
__HAL_DMA_ENABLE(huart->hdmarx);//需要关闭DMA才能重设DMA的COUNTER
}
}
|
常见问题汇总
UART传输

易错点,发送数据也需要花费时间,如果没有延时,可能造成rxBuffer先清空,再发送
外设的速度是远远小于单片机代码的执行速度,核心要等待外设执行完动作
STM32F103C8T6 特殊引脚
https://blog.csdn.net/weixin_51462579/article/details/130033153
字符串拼接
1
|
sprintf(message,"counter: %d", counter);
|
按键消抖
此处是检测下降沿,使用的是上拉电平
检测上升沿,下拉电平
检测下降沿,上拉电平
FDCAN通信消息队列Fifo已满
硬件速度远远低于单片机执行程序的速度
关于CubeMX的STM32项目创建
在选定创建的文件路径时,一定要保证路径没有中文和空格
尤其注意不要有空格
关于STM32CubeMX更新
在软件内check for updates无法勾选最新版进行更新
解决方法:
需要以管理员模式运行