
1、软件准备

串口调试助手

2、硬件准备

开发板一块,由于条件有限我们就不使用编码器,考虑使用两个GPIO引脚模拟编码器输出信号
3、Cube mx配置

这回到了嵌入式系统(特别是STM32微控制器)的核心概念。在STM32中,"自动重装值" 是一个极其重要且具体 的硬件功能,主要与定时器 相关。它不是指重装整个系统,而是指定时器在计数达到某个值后,自动重新加载初始值并继续运行。
我们来深入解析这个概念。
核心定义
在STM32的定时器(TIM)模块中,自动重装值 是指定时器计数器(CNT)向上(或向下)计数到的目标值 。当计数器计数到这个值时,会产生一个更新事件 ,然后计数器自动重装到预先设置的初始值(通常是0或另一个值),并开始下一轮的计数周期。
这个值通常存储在定时器的 "自动重装寄存器" 中,对于通用定时器是 ARR 寄存器。
关键原理:ARR 和 CNT 的关系
你可以把定时器的工作想象成一个水桶接水:
-
自动重装值 :就是水桶的容量。
-
计数器 :就是当前桶里的水量。
-
设置ARR:你决定水桶的容量(例如,ARR = 999)。
-
开始计数:水龙头打开,水开始流入(CNT 从0开始递增)。
-
达到ARR:当水量(CNT)达到桶的容量(ARR = 999)时,再增加一滴水就会溢出。
-
溢出与重装 :溢出这一瞬间,就产生了 "更新事件" 。同时,水桶被瞬间清空(CNT 自动重装为0或其他预设值)。
-
循环往复:水继续流入,开始下一个周期。
这个"溢出"事件就是定时器中断或PWM周期切换的核心触发源。
计算公式与影响
自动重装值ARR直接决定了两个最重要的定时参数:
-
定时周期/溢出频率(用于基本定时) * 公式:
溢出时间 = (ARR + 1) * (定时器时钟周期)* 或:溢出频率 = 定时器时钟频率 / (ARR + 1)* 示例 :定时器时钟为72MHz,预分频器(PSC)设为7199(将时钟分频为10KHz)。若ARR设为9999,则溢出时间为(9999+1) * (1/10KHz) = 10000 * 0.1ms = 1秒。ARR越大,定时周期越长。 -
PWM信号的周期和占空比(用于输出PWM) * PWM周期 :由ARR决定。
PWM周期 = (ARR + 1) * 时钟周期* PWM占空比 :由捕获/比较寄存器(CCR) 决定。占空比 = CCR / (ARR + 1)* 示例:ARR=999,若CCR=300,则占空比约30%。改变CCR可以调节占空比,改变ARR则同时改变整个PWM信号的频率(周期)。
在代码中的体现(以HAL库为例)
当你配置一个定时器时,自动重装值ARR是一个必须设置的参数。
// 示例:配置TIM3为1秒溢出中断
TIM_HandleTypeDef htim3;
htim3.Instance = TIM3;
htim3.Init.Prescaler = 7199; // 预分频,72MHz / (7199+1) = 10KHz
htim3.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数模式
htim3.Init.Period = 9999; // !!!这就是自动重装值ARR!!!
// 10KHz下,计数10000次就是1秒
htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; // 见下文"影子寄存器"
HAL_TIM_Base_Init(&htim3);
HAL_TIM_Base_Start_IT(&htim3); // 启动定时器并开启中断
当 CNT 计数到 Period(即ARR)的值9999,并再加1回到0时,就会触发更新中断,你可以在中断服务函数中执行任务(如翻转LED)。
重要特性:预装载与影子寄存器
这是STM32定时器非常巧妙的设计,为了解决ARR值更新时的同步和安全问题。
-
影子寄存器:用户写入的ARR值,并非直接作用于当前计数比较逻辑。实际起作用的是另一个不可见的"影子寄存器"。
-
预装载功能 : * 启用时 (
AutoReloadPreload = ENABLE):你写入ARR寄存器的新值 ,会先保存在"预装载寄存器"中。只有在下一次更新事件 发生时,这个新值才会被同步 到影子寄存器中并生效。这保证了在一个完整的定时周期内,ARR值不会中途改变,避免了产生奇怪的毛刺或半个周期的PWM。 * 禁用时 :你写入ARR的值会立即更新到影子寄存器,立刻影响当前计数周期,可能导致不可预期的输出。
简单比喻:
-
有预装载 :像编辑一个文档,你全部改完后,统一按"保存"键才会生效。
-
无预装载 :像直接编辑正在播放的幻灯片,改一个字就立刻显示一个字,可能导致页面混乱。
总结
在STM32中,自动重装值 是定时器模块的核心配置参数,它:
-
物理实体:是ARR寄存器的值。
-
核心作用 :定义了定时器的计数上限 ,从而决定了定时周期 和PWM周期。
-
工作过程:计数器CNT达到ARR后,产生更新事件,并自动重装,开始新周期。
-
高级特性 :通过预装载影子寄存器机制,保证了在运行中修改ARR时的稳定性和安全性。
理解ARR是掌握STM32所有定时器功能(基本定时、输入捕获、输出比较、PWM生成)的基石。
开启中断

建议选择生成所有

这里推荐选择这个
4、编写代码和解析
/* USER CODE BEGIN 2 */
// 1. 启动TIM3编码器模式(带溢出中断)
__HAL_TIM_ENABLE_IT(&htim3, TIM_IT_UPDATE); // 使能TIM3溢出中断
HAL_TIM_Encoder_Start_IT(&htim3, TIM_CHANNEL_ALL); // 启动编码器中断模式
// 2. 启动TIM2定时中断(100ms采样)
HAL_TIM_Base_Start_IT(&htim2);
/* USER CODE END 2 */
由于缺少器件;我们使用两个GPIO引脚生成一个正交波,来模拟编码器
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
HAL_Delay(200);
// 状态2: A=1, B=1
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET);
HAL_Delay(100);
// 状态3: A=0, B=1
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
HAL_Delay(100);
// 状态4: A=0, B=0
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET);
HAL_Delay(200);
/* USER CODE BEGIN 4 */
int fputc(int ch, FILE *f) //printf重定向函数
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
return ch;
}
int n = 0;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
if(htim->Instance == TIM3){
//1为下溢
uint32_t direction = __HAL_TIM_DIRECTION_STATUS(htim);
if(direction == 0){
n++;
}else{
n--;
}
// printf("ARR direction = %d\r\n",direction);
}
if(htim->Instance == TIM2){
//printf("TIM2\r\n");
uint16_t x = TIM3->CNT;
uint16_t total = 0;
//判断正反
if(n >= 0){
total = x + n*10;
}else{
total = (10-x)-(n+1)*10;
}
printf("speed%d = %lf\r\n",n>=0?1:-1,total*1.0/0.1);
TIM3->CNT = 0;
n = 0;
}
}
/* USER CODE END 4 */
**为什么要printf重定向?**因为printf是面向屏幕打印,不是面向串口,所以需要重定向
代码解释
HAL_TIM_PeriodElapsedCallback 是 STM32 HAL 库中定时器周期溢出中断的统一回调函数,所有配置为 "周期溢出中断" 的定时器(如 TIM2、TIM3)触发中断时,都会进入该函数。需通过定时器实例(Instance)区分不同定时器,实现差异化逻辑。
// -------------------------- 第一部分:TIM3编码器溢出中断处理 --------------------------
if(htim->Instance == TIM3) // 判断是否为TIM3的溢出中断
{
// 获取编码器当前计数方向:__HAL_TIM_DIRECTION_STATUS是HAL库方向判断宏
// 返回值说明(对应TIM3编码器模式):
// 0 = TIM_COUNTERMODE_UP :上计数(编码器反转,计数器从0→9,触发上溢)
// 1 = TIM_COUNTERMODE_DOWN :下计数(编码器正转,计数器从9→0,触发下溢)
// 注:原注释"1为下溢"是对该返回值的备注
uint32_t direction = __HAL_TIM_DIRECTION_STATUS(htim);
// 根据方向更新溢出次数:正转(下溢)n累加,反转(上溢)n递减
if(direction == 0) // 方向0 → 反转(上溢)
{
n++; // 反转溢出,n递减(此处逻辑与注释匹配:正转n++,反转n--)
}
else // 方向1 → 正转(下溢)
{
n--;
}
// 串口打印当前编码器旋转方向(0=反转,1=正转),用于调试方向判断是否正确
printf("ARR direction = %d\r\n",direction);
}
HAL_TIM_DIRECTION_STATUS 宏完全解析
__HAL_TIM_DIRECTION_STATUS 是 STM32 HAL 库中用于读取定时器计数方向 的核心宏,在编码器测速场景中,它直接决定旋转方向(正 / 反转)的判断,该宏本质是直接操作定时器的控制寄存器 1(TIMx_CR1) ,读取其中的 DIR 位(Direction,方向位)
// -------------------------- 第二部分:TIM2定时采样中断处理 --------------------------
if(htim->Instance == TIM2) // 判断是否为TIM2的定时中断(配置为100ms触发一次)
{
// 调试用:打印TIM2中断触发标记(注释后不影响功能,可取消注释验证中断是否生效)
//printf("TIM2\r\n");
uint16_t x = TIM3->CNT; // 读取TIM3编码器当前计数值(范围0~9,未溢出的"剩余脉冲数")
uint16_t total = 0; // 存储100ms采样周期内编码器的总脉冲数
// 根据溢出次数n的正负,区分整体旋转方向,计算总脉冲数
// 核心逻辑:总脉冲数 = 当期计数值 + 溢出次数×每溢出一次的脉冲数(ARR+1=10)
if(n >= 0) // n≥0 → 整体为正转(包括无溢出的正转)
{
// 正转总脉冲:当前计数值(x) + 溢出次数(n)×10(每溢出一次对应10个脉冲)
total = x + n*10;
}
else // n<0 → 整体为反转
{
// 反转总脉冲计算逻辑:
// (10-x) :反转时计数器从0→9,剩余未溢出的脉冲数为10-x
// -(n+1)*10 :n为负数,抵消负溢出的基数(例如n=-1时,-(n+1)*10=0,仅计算10-x)
total = (10-x)-(n+1)*10;
}
// 串口打印转速:
// speed1 = 正转转速,speed-1 = 反转转速;
// 转速计算:total(100ms总脉冲) / 0.1(采样时间,单位秒) → 脉冲/秒
// total*1.0 是为了将整数转为浮点数,避免除法取整
printf("speed%d = %lf\r\n",n>=0?1:-1,total*1.0/0.1);
TIM3->CNT = 0; // 重置TIM3计数值为0,为下一次采样周期做准备
n = 0; // 重置溢出次数为0,为下一次采样周期做准备
}
编译下载
打开串口调试助手
正接---正向旋转

反接-反像旋转

数据解析

为什么速度始终是10和0
推测:
1,GPIO模拟正交波的问题?与编码器产生不同
编码器工作原理 脉冲产生码盘旋转时,A、B 两相会输出连续的方波脉冲,且两相脉冲相位差 90°。 正转时:A 相超前 B 相 90°; 反转时:B 相超前 A 相 90°。 这一特性是 STM32 判断电机转向的核心依据。
2、自动重装值
上述两个问题第二问题本人以实践过,依旧和上述数据一样没有发生改变