STM32F4 增量式编码器角度测量与 LCD 实时显示
一、项目简介
本文介绍一个基于 STM32F4 的增量式编码器角度测量程序。该程序使用 TIM1 编码器接口模式采集编码器 A、B 两相信号,通过 Z 相索引信号完成每圈计数测量,并将编码器分辨率、当前角度、Z 相脉宽以及脉冲次数实时显示在 LCD 上。
程序主要实现以下功能:
-
使用 TIM1 读取增量式编码器 A、B 相信号;
-
使用 PE13 外部中断读取编码器 Z 相信号;
-
动态测量编码器每圈分辨率;
-
当分辨率接近 2880 时自动锁定;
-
实时计算当前机械角度;
-
区分正转和反转;
-
测量正向、反向 Z 相高电平脉宽;
-
通过 LCD 实时显示编码器状态信息。
二、硬件连接说明
本文程序使用的硬件资源如下表所示。
| 功能 | STM32 引脚 | 外设功能 |
|---|---|---|
| 编码器 A 相 | PE9 | TIM1_CH1 |
| 编码器 B 相 | PE11 | TIM1_CH2 |
| 编码器 Z 相 | PE13 | EXTI13 |
| LCD 显示 | I2C 接口 | RSCG12864B |
| 5V 使能 | PA1 | GPIO 输出 |
其中:
-
A、B 相用于判断编码器旋转方向和计数;
-
Z 相用于一圈零位参考和动态分辨率测量;
-
LCD用于显示分辨率、角度和 Z 相统计信息;
-
PA1用于控制外部 5V 使能。
三、程序文件结构
本工程主要包含以下三个代码文件:
encoder.c // 编码器采集、方向判断、分辨率测量、角度计算
encoder.h // 编码器模块对外接口声明
main.c // 主函数,LCD显示和周期刷新
整体功能关系如下:
编码器 A/B 相
↓
TIM1 编码器模式计数
↓
软件计算增量 diff 和累计计数 s_total_cnt
↓
根据动态分辨率计算角度
↓
LCD 显示角度和分辨率
编码器 Z 相
↓
EXTI13 外部中断
↓
锁存一圈计数
↓
动态测量分辨率
↓
判断是否锁定到 2880
四、主要全局变量说明
在 encoder.c 中定义了一组全局变量,用于保存编码器状态。
volatile uint32_t Encoder_DynamicResolution = 0;
volatile uint8_t Encoder_ResolutionMeasured = 0;
volatile int8_t Encoder_Direction = 0;
1. 动态分辨率变量
Encoder_DynamicResolution
用于保存当前测得的编码器每圈计数值。例如程序中目标分辨率为:
#define ENCODER_TARGET_RES 2880u
当程序测得的分辨率接近 2880 时,会自动锁定为 2880。
2. 方向变量
Encoder_Direction
用于保存当前旋转方向:
| 数值 | 含义 |
|---|---|
| 1 | 正转 |
| -1 | 反转 |
| 0 | 未确定或静止 |
3. Z 相统计变量
volatile uint32_t Encoder_ZPulseWidthCnt = 0;
volatile uint32_t Encoder_ZPulseCount = 0;
volatile uint32_t Encoder_FPulseWidthCnt = 0;
volatile uint32_t Encoder_FPulseCount = 0;
含义如下:
| 变量 | 含义 |
|---|---|
Encoder_ZPulseWidthCnt |
正向 Z 相高电平宽度 |
Encoder_ZPulseCount |
正向 Z 相脉冲次数 |
Encoder_FPulseWidthCnt |
反向 Z 相高电平宽度 |
Encoder_FPulseCount |
反向 Z 相脉冲次数 |
这里的“宽度”不是时间单位,而是编码器计数单位 cnt。
4. 分辨率锁定标志
volatile uint8_t Encoder_ResFixedFlag = 0;
含义如下:
| 数值 | 含义 |
|---|---|
| 0 | 未锁定 |
| 1 | 已锁定到 2880 |
LCD 显示中也会根据该变量改变显示格式:
未锁定:_2880
已锁定:2880_
五、TIM1 编码器模式初始化
程序使用 TIM1 的编码器接口模式采集 A、B 相。
TIM_EncoderInterfaceConfig(TIM1, TIM_EncoderMode_TI12,
TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
其中:
-
TIM_EncoderMode_TI12表示同时使用 CH1 和 CH2; -
TIM_ICPolarity_Rising表示采集上升沿; -
TIM1 的 CNT 会根据 A、B 相自动加减计数;
-
方向由 TIM1 的硬件方向位
DIR判断。
读取方向的代码如下:
static inline int8_t Encoder_GetHwDirection(void)
{
return (TIM1->CR1 & TIM_CR1_DIR) ? -1 : 1;
}
含义是:
| TIM1 DIR 位 | 方向 |
|---|---|
| 0 | 正向计数 |
| 1 | 反向计数 |
六、软件累计计数逻辑
虽然 TIM1 可以自动计数,但是 TIM1 是 16 位计数器,因此需要软件进行连续累计。
核心函数为:
static void Encoder_UpdateDeltaAndTotal(void)
其基本逻辑是:
cur = TIM1_GetCounter();
diff = (int32_t)cur - (int32_t)s_prev_cnt;
其中:
-
cur是当前 TIM1 计数值; -
s_prev_cnt是上一次计数值; -
diff是本次增量。
然后对 diff 进行溢出修正:
if (diff > (int32_t)(span / 2u))
diff -= (int32_t)span;
else if (diff < -(int32_t)(span / 2u))
diff += (int32_t)span;
这样可以避免计数器从最大值跳到 0,或者从 0 跳到最大值时产生异常大差值。
最后累加到软件总计数:
s_total_cnt += diff;
s_total_cnt 是一个有符号变量,可以表示正向累计,也可以表示反向累计。
七、方向确认与反转处理
为了防止编码器在静止或抖动时方向来回切换,程序加入了方向确认机制。
#define DIR_CONFIRM_N 3u
表示连续 3 次检测到同一方向增量后,才确认方向切换。
正向确认逻辑如下:
if (diff > 0)
{
s_pos_confirm++;
s_neg_confirm = 0u;
if (s_pos_confirm >= DIR_CONFIRM_N)
{
Encoder_Direction = 1;
}
}
反向确认逻辑如下:
else if (diff < 0)
{
s_neg_confirm++;
s_pos_confirm = 0u;
if (s_neg_confirm >= DIR_CONFIRM_N)
{
Encoder_Direction = -1;
}
}
这样处理后,可以降低编码器抖动导致的方向误判。
八、Z 相中断处理逻辑
Z 相连接到 PE13,并配置为外部中断 EXTI13,采用双边沿触发。
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling;
因此:
-
Z 相上升沿用于测量一圈计数和分辨率;
-
Z 相下降沿用于测量 Z 相高电平宽度。
九、Z 相上升沿:测量一圈分辨率
Z 相上升沿处理中,程序会读取当前 TIM1 计数值:
raw_u16 = (uint16_t)TIM_GetCounter(TIM1);
abs_cnt = RawToAbsCount(raw_u16);
Encoder_LastRoundCount = abs_cnt;
然后将 TIM1 计数器清零:
TIM_SetCounter(TIM1, 0);
s_total_cnt = 0;
s_prev_cnt = 0;
这一步的作用是:
每检测到一次 Z 相上升沿,就认为编码器完成了一圈,
然后锁存这一圈内 TIM1 的累计计数值。
如果连续几圈测得的计数值比较接近,程序就认为分辨率测量稳定。
十、动态分辨率确认与锁定
程序中设置了以下参数:
#define CONFIRM_COUNT 2u
#define RES_TOLERANCE 30u
#define ENCODER_TARGET_RES 2880u
#define RES_LOCK_TOLERANCE 20u
含义如下:
| 参数 | 含义 |
|---|---|
CONFIRM_COUNT |
连续确认次数 |
RES_TOLERANCE |
两次测量允许误差 |
ENCODER_TARGET_RES |
目标分辨率 2880 |
RES_LOCK_TOLERANCE |
锁定允许误差 |
当两次测量结果误差小于 RES_TOLERANCE 时,认为分辨率有效:
if (diff_res <= RES_TOLERANCE)
{
s_confirm_cnt++;
}
当测量分辨率处于 2880 ± 20 范围内时,强制锁定为 2880:
if ((Encoder_DynamicResolution >= (ENCODER_TARGET_RES - RES_LOCK_TOLERANCE)) &&
(Encoder_DynamicResolution <= (ENCODER_TARGET_RES + RES_LOCK_TOLERANCE)))
{
Encoder_DynamicResolution = ENCODER_TARGET_RES;
Encoder_ResFixedFlag = 1;
Encoder_SetTim1ARR(Encoder_DynamicResolution - 1u);
}
锁定后:
Encoder_DynamicResolution = 2880
TIM1->ARR = 2879
Encoder_ResFixedFlag = 1
这样后续角度计算会更加稳定。
十一、Z 相下降沿:测量 Z 相高电平宽度
Z 相下降沿中,程序读取从 Z 相上升沿到下降沿之间的计数值:
raw_fall = TIM1_GetCounter();
width_cnt = RawToAbsCount(raw_fall);
如果上升沿时判断方向为正向,则保存到正向变量:
Encoder_ZPulseWidthCnt = width_cnt;
Encoder_ZPulseCount++;
如果方向为反向,则保存到反向变量:
Encoder_FPulseWidthCnt = width_cnt;
Encoder_FPulseCount++;
因此 LCD 上显示的:
Z_Cnt:正向 Z 相高电平宽度
Z_Num:正向 Z 相脉冲次数
F_Cnt:反向 Z 相高电平宽度
F_Num:反向 Z 相脉冲次数
十二、角度计算方法
角度计算函数为:
float Encoder_GetAngle(void)
{
pos = (uint32_t)wrap_mod_res(s_total_cnt, res);
ang = ((float)pos * 360.0f) / (float)res;
return ang;
}
计算公式为:
角度 = 当前计数位置 × 360° / 每圈分辨率
例如分辨率为 2880 时:
每 1 个计数对应角度 = 360 / 2880 = 0.125°
如果当前计数为 720,则:
角度 = 720 × 360 / 2880 = 90°
十三、LCD 显示内容
主函数中 LCD 显示 6 行主要数据:
print_string_5x7_xy(0x00, 0, 8, (U8*)"Res:");
print_string_5x7_xy(0x00, 0, 16, (U8*)"Ang:");
print_string_5x7_xy(0x00, 0, 24, (U8*)"Z_Cnt:");
print_string_5x7_xy(0x00, 0, 32, (U8*)"Z_Num:");
print_string_5x7_xy(0x00, 0, 40, (U8*)"F_Cnt:");
print_string_5x7_xy(0x00, 0, 48, (U8*)"F_Num:");
显示含义如下:
| LCD 字段 | 含义 |
|---|---|
| Res | 当前动态分辨率 |
| Ang | 当前机械角度 |
| Z_Cnt | 正向 Z 相高电平宽度 |
| Z_Num | 正向 Z 相脉冲次数 |
| F_Cnt | 反向 Z 相高电平宽度 |
| F_Num | 反向 Z 相脉冲次数 |
主循环每 100ms 刷新一次 LCD:
for (i = 0; i < 100; i++)
{
Encoder_UpdateDirection();
delay_ms(1);
}
也就是说,程序每 1ms 更新一次编码器方向和累计计数,每 100ms 刷新一次显示。
十四、显示效果说明
程序启动后,LCD 初始显示如下:
Res: Wait
Ang: Wait
Z_Cnt: 0
Z_Num: 0
F_Cnt: 0
F_Num: 0
当编码器旋转并检测到 Z 相后,程序开始测量分辨率。
如果分辨率尚未锁定,显示格式类似:
Res: _2876
如果分辨率成功锁定到 2880,则显示格式变为:
Res: 2880_
角度显示格式为一位小数,例如:
Ang: 123.4
十五、程序设计亮点
1. 使用硬件编码器模式,计数稳定
TIM1 编码器模式可以由硬件自动完成 A、B 相计数和方向判断,比普通 GPIO 中断方式更加稳定。
2. 使用 Z 相自动测量分辨率
程序不需要一开始手动写死分辨率,而是可以根据 Z 相自动测量每圈计数值。
3. 支持分辨率锁定
当测量值接近 2880 时,程序会自动锁定为 2880,避免后续测量抖动导致角度计算不稳定。
4. 软件连续计数
虽然 TIM1 是 16 位计数器,但程序通过 s_total_cnt 实现了连续计数,方便计算角度和方向。
5. 方向防抖机制
通过连续多次确认同一方向,避免编码器抖动时方向频繁跳变。
6. LCD 局部刷新
主循环中只有当数据发生变化时才刷新对应区域,可以减少 LCD 闪烁和残影。
十六、需要注意的问题
1. delay_ms_counter 必须正常递增
Z 相上升沿中使用了:
now_ms = delay_ms_counter;
如果 delay_ms_counter 没有在 SysTick 或定时器中断中正常递增,则 Z 相最小间隔判断会失效。
2. TIM5 被用作 1 MHz 微秒计时器
程序中 TIM5 用于 Z 相边沿去抖:
TIM_TimeBaseStructure.TIM_Prescaler = 84 - 1;
这里默认 APB1 定时器时钟为 84 MHz。
如果你的系统时钟配置不同,需要同步修改 TIM5 的预分频值。
3. PE9、PE11、PE13 引脚需要与硬件一致
本文程序默认:
PE9 -> A 相
PE11 -> B 相
PE13 -> Z 相
如果实际 PCB 使用的引脚不同,需要修改 GPIO 和复用配置。
4. 目标分辨率目前固定为 2880
代码中写死了目标分辨率:
#define ENCODER_TARGET_RES 2880u
如果使用其他编码器,需要根据实际分辨率修改该值。
5. Z 相中断中不要加入耗时操作
Z 相中断函数中只适合做计数锁存、状态更新等轻量操作,不建议在中断里加入 LCD 显示、串口打印等耗时函数。
十七、核心代码结构
1. 编码器初始化
void Encoder_Init(void)
{
// 初始化 GPIOE、TIM1、TIM5、EXTI13
// 配置 PE9/PE11 为 TIM1 编码器输入
// 配置 PE13 为 Z 相外部中断输入
// 初始化状态变量
// 启动 TIM1
}
2. 编码器周期更新
void Encoder_UpdateDirection(void)
{
Encoder_UpdateDeltaAndTotal();
}
建议在主循环中定时调用,例如每 1ms 调用一次。
3. 获取角度
float Encoder_GetAngle(void)
{
uint32_t res;
uint32_t pos;
float ang;
res = Encoder_DynamicResolution;
if (res == 0u)
{
return 0.0f;
}
pos = (uint32_t)wrap_mod_res(s_total_cnt, res);
ang = ((float)pos * 360.0f) / (float)res;
return ang;
}
4. Z 相中断
void EXTI15_10_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line13) != RESET)
{
// 清除中断标志
// 判断 Z 相当前电平
// 上升沿:测量一圈计数和分辨率
// 下降沿:测量 Z 相高电平宽度
}
}
十八、完整代码
1. encoder.c
#include "stm32f4xx.h"
#include "encoder.h"
/* ===================== 全局变量定义 ===================== */
volatile uint32_t Encoder_DynamicResolution = 0;
volatile uint8_t Encoder_ResolutionMeasured = 0;
volatile int8_t Encoder_Direction = 0;
volatile uint32_t Encoder_LastRoundCount = 0;
volatile uint32_t g_z_irq_count = 0;
/* Z相脉宽与计数(正向/反向)- 宽度单位cnt */
volatile uint32_t Encoder_ZPulseWidthCnt = 0; // 正向最新脉宽(cnt)
volatile uint32_t Encoder_ZPulseCount = 0; // 正向脉冲次数
volatile uint32_t Encoder_FPulseWidthCnt = 0; // 反向最新脉宽(cnt)
volatile uint32_t Encoder_FPulseCount = 0; // 反向脉冲次数
/* 分辨率固定标志:1=已固定到2880,0=未固定 */
volatile uint8_t Encoder_ResFixedFlag = 0;
/* 在delay模块中定义(1ms节拍计数) */
volatile uint32_t delay_ms_counter = 0;
/* ===================== DBG变量(Live Watch) ===================== */
volatile uint16_t Encoder_DBG_CNT = 0; //TIM1 计数器 CNT 的值
volatile uint16_t Encoder_DBG_ARR = 0; //TIM1 自动重装值 ARR
volatile int32_t Encoder_DBG_DIFF = 0; //相对上次的增量差值
volatile int32_t Encoder_DBG_TOTAL_CNT = 0;
volatile float Encoder_DBG_ANGLE = 0.0f; //机械角度
volatile uint32_t Encoder_DBG_RES = 0; //分辨率(每圈计数)
volatile uint8_t Encoder_DBG_REV_MODE = 0; //0:正转; 1:反转
/* ===================== 内部参数/状态 ===================== */
#define MIN_ROUND_MS 200u // Z上升沿最小间隔过滤(防抖/抗干扰)
#define CONFIRM_COUNT 2u
#define RES_TOLERANCE 30u
/* 锁定目标与容差 */
#define ENCODER_TARGET_RES 2880u
#define RES_LOCK_TOLERANCE 20u // 可改10/20/30
/* Z边沿去抖(避免一圈+2) */
#define Z_EDGE_DEBOUNCE_US 20u
/* 方向切换确认(防抖动来回切模式) */
#define DIR_CONFIRM_N 3u // 连续3次同方向增量才确认切换
static volatile uint32_t s_last_z_ms = 0;
static volatile uint32_t s_last_valid_res = 0;
static volatile uint8_t s_confirm_cnt = 0;
static uint16_t s_last_raw_u16 = 0;
/* Z高电平宽度测量状态(单位cnt) */
static volatile uint8_t s_z_high_active = 0;
static volatile int8_t s_z_rise_dir = 0;
/* Z边沿去抖(避免一圈+2) */
static volatile uint32_t s_last_z_edge_us = 0;
/* 电平状态机,避免同电平重复处理中断 */
static volatile uint8_t s_z_last_level = 0xFF;
/* 连续计数(用于角度计算) */
static volatile int32_t s_total_cnt = 0; // 连续总计数(可正可负)
static volatile uint16_t s_prev_cnt = 0; // 上次原始CNT
/******** 反转模式 ********/
static volatile uint8_t s_reverse_mode = 0; // 0:正常 1:反转模式
static volatile int32_t s_arr_cnt = 0; // 反转逻辑计数
/* 方向切换确认(防抖动来回切模式) */
static volatile uint8_t s_pos_confirm = 0;
static volatile uint8_t s_neg_confirm = 0;
/* ===================== 内部函数 ===================== */
static inline uint16_t TIM1_GetCounter(void)
{
return (uint16_t)TIM_GetCounter(TIM1);
}
static uint32_t RawToAbsCount(uint16_t raw)
{
if (raw <= 32767u) return (uint32_t)raw;
return (uint32_t)(65536u - (uint32_t)raw);
}
/* TIM5作为1MHz自由运行计时器(1tick=1us) */
static void Encoder_UsTimerInit(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE);
TIM_TimeBaseStructure.TIM_Prescaler = 84 - 1; // 84MHz/84=1MHz
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_Period = 0xFFFFFFFF; // 32位自动重装
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM5, &TIM_TimeBaseStructure);
TIM_Cmd(TIM5, ENABLE);
}
static inline uint32_t Encoder_GetUsTick(void)
{
return TIM_GetCounter(TIM5);
}
/* 直接读取TIM1硬件方向位:DIR=0上数(正向), DIR=1下数(反向) */
static inline int8_t Encoder_GetHwDirection(void)
{
return (TIM1->CR1 & TIM_CR1_DIR) ? -1 : 1;
}
/* EXTI13使能/关闭(反转时关闭,防止Z继续清零) */
static inline void Encoder_SetZExtiEnable(uint8_t en)
{
if (en)
{
EXTI->IMR |= EXTI_Line13;
}
else
{
EXTI->IMR &= ~EXTI_Line13;
EXTI_ClearITPendingBit(EXTI_Line13);
}
}
/* 设置TIM1 ARR,并同步软件计数参考(会重置计数状态) */
static void Encoder_SetTim1ARR(uint32_t arr)
{
if (arr > 0xFFFFu) arr = 0xFFFFu;
TIM_Cmd(TIM1, DISABLE);
TIM_SetAutoreload(TIM1, (uint16_t)arr);
TIM_SetCounter(TIM1, 0);
TIM_GenerateEvent(TIM1, TIM_EventSource_Update);
TIM_Cmd(TIM1, ENABLE);
s_prev_cnt = 0;
s_total_cnt = 0;
}
/* 运行时修改ARR:不重置s_total_cnt/s_prev_cnt */
static void Encoder_SetTim1ARR_Runtime(uint32_t arr)
{
uint16_t cnt;
if (arr > 0xFFFFu) arr = 0xFFFFu;
TIM_Cmd(TIM1, DISABLE);
TIM_SetAutoreload(TIM1, (uint16_t)arr);
/* 若当前CNT超过新ARR,钳位到ARR */
cnt = (uint16_t)TIM_GetCounter(TIM1);
if (cnt > (uint16_t)arr)
TIM_SetCounter(TIM1, (uint16_t)arr);
TIM_GenerateEvent(TIM1, TIM_EventSource_Update);
TIM_Cmd(TIM1, ENABLE);
}
/* 归一到[0,res) */
static inline int32_t wrap_mod_res(int32_t v, uint32_t res)
{
if (res == 0) return 0;
v %= (int32_t)res;
if (v < 0) v += (int32_t)res;
return v;
}
/* 按“三公式”更新:先溢出修正增量,再累计连续计数 */
static void Encoder_UpdateDeltaAndTotal(void)
{
uint16_t cur;
int32_t diff;
uint32_t res;
uint32_t span;
cur = TIM1_GetCounter();
diff = (int32_t)cur - (int32_t)s_prev_cnt;
res = Encoder_DynamicResolution;
/* diff修正窗口 */
if (res > 0u) span = res; /* 例如2880 */
else span = (uint32_t)TIM1->ARR + 1u;
if (span == 0u) span = 65536u;
if (diff > (int32_t)(span / 2u))
diff -= (int32_t)span;
else if (diff < -(int32_t)(span / 2u))
diff += (int32_t)span;
/* 连续计数 */
s_total_cnt += diff;
/* 方向确认 */
if (diff > 0)
{
if (s_pos_confirm < 255u) s_pos_confirm++;
s_neg_confirm = 0u;
if (s_pos_confirm >= DIR_CONFIRM_N)
{
if (s_reverse_mode)
{
/* 反转 -> 正转:无缝衔接 */
s_total_cnt = s_arr_cnt;
s_reverse_mode = 0u;
if (res > 0u)
Encoder_SetTim1ARR_Runtime(res - 1u); /* ARR = res - 1 */
Encoder_SetZExtiEnable(1u);
s_z_last_level = GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_13) ? 1u : 0u;
}
Encoder_Direction = 1;
s_pos_confirm = DIR_CONFIRM_N;
}
}
else if (diff < 0)
{
if (s_neg_confirm < 255u) s_neg_confirm++;
s_pos_confirm = 0u;
if (s_neg_confirm >= DIR_CONFIRM_N)
{
if (!s_reverse_mode)
{
/* 正转 -> 反转 */
s_reverse_mode = 1u;
if (res > 0u)
{
s_arr_cnt = wrap_mod_res(s_total_cnt, res);
Encoder_SetTim1ARR_Runtime(res - 1u); /* ARR = res - 1 */
}
else
{
s_arr_cnt = 0;
}
Encoder_SetZExtiEnable(0u); /* 反转时关Z中断 */
}
Encoder_Direction = -1;
s_neg_confirm = DIR_CONFIRM_N;
}
}
/* 反转模式逻辑回卷 */
if (s_reverse_mode && (res > 0u) && (diff != 0))
{
int32_t t = s_arr_cnt + diff; /* diff<0时递减 */
t %= (int32_t)res;
if (t < 0) t += (int32_t)res;
s_arr_cnt = t;
/* 统一角度源:s_total_cnt */
s_total_cnt = s_arr_cnt;
}
s_prev_cnt = TIM1_GetCounter();
/* ===== DBG快照 ===== */
Encoder_DBG_CNT = s_prev_cnt;
Encoder_DBG_ARR = (uint16_t)TIM1->ARR;
Encoder_DBG_DIFF = diff;
Encoder_DBG_TOTAL_CNT = s_total_cnt;
Encoder_DBG_RES = res;
Encoder_DBG_REV_MODE = s_reverse_mode;
}
/* ===================== 对外接口 ===================== */
void Encoder_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
Encoder_UsTimerInit();
/* AB相:PE9、PE11 -> TIM1 CH1/CH2 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOE, &GPIO_InitStructure);
GPIO_PinAFConfig(GPIOE, GPIO_PinSource9, GPIO_AF_TIM1);
GPIO_PinAFConfig(GPIOE, GPIO_PinSource11, GPIO_AF_TIM1);
/* Z相:PE13 输入 + EXTI13 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOE, &GPIO_InitStructure);
/* TIM1 编码器模式 */
TIM_TimeBaseStructure.TIM_Prescaler = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_Period = 0xFFFF;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
TIM_EncoderInterfaceConfig(TIM1, TIM_EncoderMode_TI12,
TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICFilter = 0x0F;
TIM_ICInit(TIM1, &TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
TIM_ICInit(TIM1, &TIM_ICInitStructure);
TIM_SetAutoreload(TIM1, 0xFFFF);
/* PE13 -> EXTI13,双边沿触发 */
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE, EXTI_PinSource13);
EXTI_InitStructure.EXTI_Line = EXTI_Line13;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
/* 状态清零 */
TIM_SetCounter(TIM1, 0);
s_last_raw_u16 = 0;
Encoder_Direction = 0;
Encoder_LastRoundCount = 0;
Encoder_DynamicResolution = 0;
Encoder_ResolutionMeasured = 0;
Encoder_ResFixedFlag = 0;
g_z_irq_count = 0;
s_last_z_ms = 0;
s_last_valid_res = 0;
s_confirm_cnt = 0;
Encoder_ZPulseWidthCnt = 0;
Encoder_ZPulseCount = 0;
Encoder_FPulseWidthCnt = 0;
Encoder_FPulseCount = 0;
s_z_high_active = 0;
s_z_rise_dir = 0;
s_last_z_edge_us = 0;
s_z_last_level = GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_13) ? 1 : 0;
s_total_cnt = 0;
s_prev_cnt = TIM1_GetCounter();
s_reverse_mode = 0;
s_arr_cnt = 0;
s_pos_confirm = 0;
s_neg_confirm = 0;
/* DBG清零 */
Encoder_DBG_CNT = 0;
Encoder_DBG_ARR = (uint16_t)TIM1->ARR;
Encoder_DBG_DIFF = 0;
Encoder_DBG_TOTAL_CNT = 0;
Encoder_DBG_ANGLE = 0.0f;
Encoder_DBG_RES = 0;
Encoder_DBG_REV_MODE = 0;
Encoder_SetZExtiEnable(1);
TIM_Cmd(TIM1, ENABLE);
}
void Encoder_ResetCount(void)
{
TIM_SetCounter(TIM1, 0);
s_last_raw_u16 = 0;
Encoder_Direction = 0;
s_total_cnt = 0;
s_prev_cnt = 0;
s_reverse_mode = 0;
s_arr_cnt = 0;
s_pos_confirm = 0;
s_neg_confirm = 0;
Encoder_DynamicResolution = 0;
Encoder_ResolutionMeasured = 0;
Encoder_ResFixedFlag = 0;
s_last_valid_res = 0;
s_confirm_cnt = 0;
Encoder_SetTim1ARR(0xFFFFu);
Encoder_SetZExtiEnable(1);
/* DBG清零 */
Encoder_DBG_CNT = 0;
Encoder_DBG_ARR = (uint16_t)TIM1->ARR;
Encoder_DBG_DIFF = 0;
Encoder_DBG_TOTAL_CNT = 0;
Encoder_DBG_ANGLE = 0.0f;
Encoder_DBG_RES = 0;
Encoder_DBG_REV_MODE = 0;
}
void Encoder_UpdateDirection(void)
{
Encoder_UpdateDeltaAndTotal();
}
uint32_t Encoder_GetResolution(void)
{
return Encoder_DynamicResolution;
}
uint32_t Encoder_GetAbsCount(void)
{
return RawToAbsCount(TIM1_GetCounter());
}
float Encoder_GetAngle(void)
{
uint32_t res;
uint32_t pos;
float ang;
/* 不在这里更新,避免重复更新 */
res = Encoder_DynamicResolution;
if (res == 0u)
{
Encoder_DBG_ANGLE = 0.0f;
return 0.0f;
}
pos = (uint32_t)wrap_mod_res(s_total_cnt, res);
ang = ((float)pos * 360.0f) / (float)res;
if (ang >= 360.0f) ang -= 360.0f;
if (ang < 0.0f) ang += 360.0f;
Encoder_DBG_ANGLE = ang;
return ang;
}
uint8_t Encoder_IsResFixed(void)
{
return Encoder_ResFixedFlag;
}
/* ===================== Z相中断 ===================== */
void EXTI15_10_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line13) != RESET)
{
uint8_t z_level;
uint32_t now_us;
EXTI_ClearITPendingBit(EXTI_Line13);
z_level = GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_13) ? 1 : 0;
now_us = Encoder_GetUsTick();
/* 边沿去抖 */
if ((uint32_t)(now_us - s_last_z_edge_us) < Z_EDGE_DEBOUNCE_US)
return;
s_last_z_edge_us = now_us;
/* 同电平重复中断过滤 */
if ((s_z_last_level != 0xFF) && (z_level == s_z_last_level))
return;
s_z_last_level = z_level;
if (z_level) /* ---------- Z上升沿 ---------- */
{
uint32_t now_ms;
uint32_t elapsed;
uint32_t abs_cnt;
uint16_t raw_u16;
now_ms = delay_ms_counter;
elapsed = now_ms - s_last_z_ms;
abs_cnt = 0;
if ((s_last_z_ms != 0) && (elapsed < MIN_ROUND_MS))
return;
s_last_z_ms = now_ms;
g_z_irq_count++;
s_z_rise_dir = Encoder_GetHwDirection();
/* 一圈计数锁存 + 清零 */
TIM_Cmd(TIM1, DISABLE);
raw_u16 = (uint16_t)TIM_GetCounter(TIM1);
abs_cnt = RawToAbsCount(raw_u16);
Encoder_LastRoundCount = abs_cnt;
TIM_SetCounter(TIM1, 0);
s_last_raw_u16 = 0;
/* 同步软件参考 */
s_total_cnt = 0;
s_prev_cnt = 0;
TIM_Cmd(TIM1, ENABLE);
s_z_high_active = 1;
/* 已锁定后,始终保持2880,同时确保ARR=2879 */
if (Encoder_ResFixedFlag)
{
Encoder_DynamicResolution = ENCODER_TARGET_RES;
if (TIM1->ARR != (ENCODER_TARGET_RES - 1u))
Encoder_SetTim1ARR(ENCODER_TARGET_RES - 1u);
Encoder_DBG_ARR = (uint16_t)TIM1->ARR;
Encoder_DBG_CNT = TIM1_GetCounter();
Encoder_DBG_RES = Encoder_DynamicResolution;
return;
}
/* 分辨率稳定确认 */
if (Encoder_ResolutionMeasured == 0)
{
Encoder_ResolutionMeasured = 1;
s_confirm_cnt = 0;
s_last_valid_res = 0;
if ((abs_cnt < 100) || (abs_cnt >= 100000))
return;
}
if ((abs_cnt > 100) && (abs_cnt < 100000))
{
if (s_last_valid_res == 0)
{
s_last_valid_res = abs_cnt;
s_confirm_cnt = 1;
}
else
{
uint32_t diff_res;
diff_res = (abs_cnt > s_last_valid_res)
? (abs_cnt - s_last_valid_res)
: (s_last_valid_res - abs_cnt);
if (diff_res <= RES_TOLERANCE)
{
s_confirm_cnt++;
if (s_confirm_cnt >= CONFIRM_COUNT)
{
Encoder_DynamicResolution = (abs_cnt + s_last_valid_res) / 2;
s_last_valid_res = Encoder_DynamicResolution;
s_confirm_cnt = CONFIRM_COUNT;
/* 是否锁定到2880 */
if ((Encoder_DynamicResolution >= (ENCODER_TARGET_RES - RES_LOCK_TOLERANCE)) &&
(Encoder_DynamicResolution <= (ENCODER_TARGET_RES + RES_LOCK_TOLERANCE)))
{
Encoder_DynamicResolution = ENCODER_TARGET_RES;
Encoder_ResFixedFlag = 1;
/* ARR = res - 1 */
Encoder_SetTim1ARR(Encoder_DynamicResolution - 1u);
}
else
{
Encoder_ResFixedFlag = 0;
}
s_arr_cnt = (int32_t)Encoder_DynamicResolution;
}
}
else
{
s_confirm_cnt = 1;
s_last_valid_res = abs_cnt;
}
}
}
else
{
s_confirm_cnt = 0;
s_last_valid_res = 0;
Encoder_ResFixedFlag = 0;
}
}
else /* ---------- Z下降沿 ---------- */
{
if (s_z_high_active)
{
uint16_t raw_fall;
uint32_t width_cnt;
raw_fall = TIM1_GetCounter();
width_cnt = RawToAbsCount(raw_fall);
s_z_high_active = 0;
if (Encoder_DynamicResolution > 0)
{
if ((width_cnt == 0) || (width_cnt >= Encoder_DynamicResolution))
return;
}
if (s_z_rise_dir > 0)
{
Encoder_ZPulseWidthCnt = width_cnt;
Encoder_ZPulseCount++;
}
else if (s_z_rise_dir < 0)
{
Encoder_FPulseWidthCnt = width_cnt;
Encoder_FPulseCount++;
}
}
}
}
}
2. encoder.h
#ifndef __ENCODER_H
#define __ENCODER_H
#include "stm32f4xx.h"
/* 动态分辨率(每圈计数值),例如2880 */
extern volatile uint32_t Encoder_DynamicResolution;
/* 分辨率是否已进入测量/已测得(由实现逻辑定义):
* 0 = 未测量
* 1 = 已开始测量或已得到有效测量流程
*/
extern volatile uint8_t Encoder_ResolutionMeasured;
/* 当前旋转方向:
* 1 = 正方向
* -1 = 反方向
* 0 = 未确定/静止(视实现)
*/
extern volatile int8_t Encoder_Direction;
/* 最近一圈计数(通常在Z相上升沿锁存) */
extern volatile uint32_t Encoder_LastRoundCount;
/* Z相有效中断次数统计(调试/监控用) */
extern volatile uint32_t g_z_irq_count;
/* Z相(正向)高电平脉宽(单位:编码器计数cnt) */
extern volatile uint32_t Encoder_ZPulseWidthCnt;
/* Z相(正向)脉冲累计次数 */
extern volatile uint32_t Encoder_ZPulseCount;
/* Z相(反向)高电平脉宽(单位:编码器计数cnt) */
extern volatile uint32_t Encoder_FPulseWidthCnt;
/* Z相(反向)脉冲累计次数 */
extern volatile uint32_t Encoder_FPulseCount;
/* 分辨率是否已固定到目标值2880:
* 1 = 已固定(锁定)
* 0 = 未固定(动态测量中或未达锁定条件)
*/
extern volatile uint8_t Encoder_ResFixedFlag;
/* ===================== 对外函数接口 ===================== */
/* 编码器模块初始化:
* - 初始化GPIO/TIM/EXTI等外设
* - 初始化内部状态变量
* - 启动编码器计数与Z相中断逻辑
*/
void Encoder_Init(void);
/* 获取当前绝对计数值(由实现决定转换方式) */
uint32_t Encoder_GetAbsCount(void);
/* 复位编码器相关计数与状态:
* - 清零计数
* - 重置分辨率/方向/锁定标志等
*/
void Encoder_ResetCount(void);
/* 获取当前动态分辨率(每圈计数) */
uint32_t Encoder_GetResolution(void);
/* 周期更新方向与累计计数(建议在主循环定时调用) */
void Encoder_UpdateDirection(void);
/* 获取当前机械角度(通常范围0~360°) */
float Encoder_GetAngle(void);
/* 查询分辨率是否已锁定到2880:
* 返回1表示已锁定,0表示未锁定
*/
uint8_t Encoder_IsResFixed(void);
#endif
3. main.c
#include "stm32f4xx.h"
#include "RSCG12864B.h"
#include "delay.h"
#include "encoder.h"
extern volatile uint32_t Encoder_ZPulseWidthCnt;
extern volatile uint32_t Encoder_ZPulseCount;
extern volatile uint32_t Encoder_FPulseWidthCnt;
extern volatile uint32_t Encoder_FPulseCount;
/* 1ms节拍计数(由delay模块维护) */
extern volatile uint32_t delay_ms_counter;
/* UI刷新周期(ms) */
#define UI_REFRESH_MS 100u
/* 打印 x10 角度 */
static void lcd_print_ang_x10(U8 addr, U8 x, U8 y, uint32_t ang_x10)
{
if (ang_x10 == 0xFFFFFFFF)
{
print_string_5x7_xy(addr, x, y, (U8*)"Wait");
return;
}
{
uint32_t ang_int = ang_x10 / 10;
uint32_t ang_frac = ang_x10 % 10;
uint8_t int_digits;
uint8_t x_dot;
uint8_t x_frac;
if (ang_int >= 100) int_digits = 3;
else if (ang_int >= 10) int_digits = 2;
else int_digits = 1;
x_dot = x + int_digits * 6;
x_frac = x_dot + 6;
print_U32_5x7_xy(addr, x, y, ang_int);
print_string_5x7_xy(addr, x_dot, y, (U8*)".");
print_U32_5x7_xy(addr, x_frac, y, ang_frac);
}
}
/* 显示 Res 锁定状态:
* fixed=1 -> "2880_"
* fixed=0 -> "_2880"
*/
static void lcd_print_res_lock_style(U8 addr, U8 x, U8 y, uint32_t res, uint8_t fixed)
{
uint8_t digits = 1;
U8 x_num = x + 6; /* 左边预留1字符 '_' */
U8 x_tail;
/* 清空区域,避免残影 */
print_string_5x7_xy(addr, x, y, (U8*)" ");
if (res == 0)
{
print_string_5x7_xy(addr, x, y, (U8*)"Wait");
return;
}
if (res >= 1000000000UL) digits = 10;
else if (res >= 100000000UL) digits = 9;
else if (res >= 10000000UL) digits = 8;
else if (res >= 1000000UL) digits = 7;
else if (res >= 100000UL) digits = 6;
else if (res >= 10000UL) digits = 5;
else if (res >= 1000UL) digits = 4;
else if (res >= 100UL) digits = 3;
else if (res >= 10UL) digits = 2;
x_tail = x_num + digits * 6;
if (fixed)
{
/* 固定成功:2880_ */
print_U32_5x7_xy(addr, x_num, y, res);
print_string_5x7_xy(addr, x_tail, y, (U8*)"_");
}
else
{
/* 未固定:_2880 */
print_string_5x7_xy(addr, x, y, (U8*)"_");
print_U32_5x7_xy(addr, x_num, y, res);
}
}
int main(void)
{
uint32_t last_res, last_ang10, last_zcnt, last_zn, last_fcnt, last_fn;
uint8_t last_fixed;
uint32_t last_ui_ms;
SystemInit();
delay_init(168);
/* 5V使能 PA1 */
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
{
GPIO_InitTypeDef g;
g.GPIO_Pin = GPIO_Pin_1;
g.GPIO_Mode = GPIO_Mode_OUT;
g.GPIO_Speed = GPIO_Speed_50MHz;
g.GPIO_OType = GPIO_OType_PP;
g.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(GPIOA, &g);
}
GPIO_SetBits(GPIOA, GPIO_Pin_1);
delay_ms(200);
Encoder_Init();
I2C_Initial();
delay_ms(1000);
brightness(0x00, 0xFF);
clear(0x00);
/* 7行显示(5x7字体,行距8像素) */
print_string_5x7_xy(0x00, 0, 8, (U8*)"Res:");
print_string_5x7_xy(0x00, 0, 16, (U8*)"Ang:");
print_string_5x7_xy(0x00, 0, 24, (U8*)"Z_Cnt:");
print_string_5x7_xy(0x00, 0, 32, (U8*)"Z_Num:");
print_string_5x7_xy(0x00, 0, 40, (U8*)"F_Cnt:");
print_string_5x7_xy(0x00, 0, 48, (U8*)"F_Num:");
last_res = 0xFFFFFFFF;
last_fixed = 0xFF;
last_ang10 = 0xFFFFFFFF;
last_zcnt = 0xFFFFFFFF;
last_zn = 0xFFFFFFFF;
last_fcnt = 0xFFFFFFFF;
last_fn = 0xFFFFFFFF;
last_ui_ms = delay_ms_counter;
while (1)
{
uint32_t res, zp_cnt, zn, fp_cnt, fn, ang_x10;
uint8_t fixed;
float angle;
uint8_t i;
/* 100ms窗口:每1ms更新一次方向/计数 */
for (i = 0; i < 100; i++)
{
Encoder_UpdateDirection();
delay_ms(1);
}
/* ---- 以下每100ms刷新一次显示 ---- */
res = Encoder_GetResolution();
fixed = Encoder_IsResFixed();
angle = Encoder_GetAngle();
zp_cnt = Encoder_ZPulseWidthCnt;
zn = Encoder_ZPulseCount;
fp_cnt = Encoder_FPulseWidthCnt;
fn = Encoder_FPulseCount;
if ((res != last_res) || (fixed != last_fixed))
{
lcd_print_res_lock_style(0x00, 54, 8, res, fixed);
last_res = res;
last_fixed = fixed;
}
if (res == 0)
{
ang_x10 = 0xFFFFFFFF;
}
else
{
ang_x10 = (uint32_t)(angle * 10.0f + 0.5f);
if (ang_x10 > 3600) ang_x10 = 0;
}
if (ang_x10 != last_ang10)
{
print_string_5x7_xy(0x00, 54, 16, (U8*)" ");
lcd_print_ang_x10(0x00, 54, 16, ang_x10);
last_ang10 = ang_x10;
}
if (zp_cnt != last_zcnt)
{
print_string_5x7_xy(0x00, 54, 24, (U8*)" ");
print_U32_5x7_xy(0x00, 54, 24, zp_cnt);
last_zcnt = zp_cnt;
}
if (zn != last_zn)
{
print_string_5x7_xy(0x00, 54, 32, (U8*)" ");
print_U32_5x7_xy(0x00, 54, 32, zn);
last_zn = zn;
}
if (fp_cnt != last_fcnt)
{
print_string_5x7_xy(0x00, 54, 40, (U8*)" ");
print_U32_5x7_xy(0x00, 54, 40, fp_cnt);
last_fcnt = fp_cnt;
}
if (fn != last_fn)
{
print_string_5x7_xy(0x00, 54, 48, (U8*)" ");
print_U32_5x7_xy(0x00, 54, 48, fn);
last_fn = fn;
}
}
}
十九、总结
本文实现了一个基于 STM32F4 的增量式编码器角度测量与 LCD 显示程序。程序利用 TIM1 编码器接口模式完成 A、B 相计数,通过 Z 相中断实现分辨率自动测量,并在检测到目标分辨率 2880 后自动锁定。同时,程序还能实时计算机械角度,并在 LCD 上显示当前分辨率、角度、Z 相脉宽和脉冲次数。
该程序适用于以下场景:
-
增量式编码器调试;
-
电机转角检测;
-
转台角度显示;
-
编码器分辨率自动识别;
-
机械位置反馈系统;
-
运动控制实验平台。
更多推荐



所有评论(0)