☰
GD32定时器正交编码器4倍频实现:三种方法对比与避坑指南
2026/9/28 17:42:02 网站建设 项目流程

正交编码器在电机控制、云台稳定、精密位移台这些场景里几乎是标配。但很多人第一次用GD32的定时器接编码器时,都会遇到同一个困惑:明明编码器是500线的,为什么读出来的数总感觉"不够细"?或者换个问法——同样一圈,别人能读到2000个计数,我这边只有500,差在哪了?

答案通常就藏在"倍频"这两个字里。正交编码器输出的是A、B两路相位差90度的方波,硬件定时器的编码器接口模式本身就能对这两路信号做4倍频解码,把每个周期拆成4个计数点。但GD32的定时器配置里,从"能计数"到"稳定地4倍频计数",中间隔着好几道坎:滤波器怎么设、极性怎么选、计数方向怎么判断、溢出怎么处理。这篇文章就把这三种实现4倍频的路径拆开讲清楚,顺带把模式对比和踩坑经验一并交代。不管你是刚上手GD32的新人,还是从STM32转过来想快速迁移的老手,都能直接抄作业。

1. 先搞清楚正交编码器为什么能"4倍频"

在动手配置寄存器之前,得先把倍频这件事的物理来源想明白。不然配置的时候只是照抄参数,出了问题根本不知道从哪查。

1.1 A/B两路信号的四种状态组合

正交编码器内部通常是一个带缝隙的码盘加两组光电对管,A、B两路输出在相位上错开90度。当码盘转动时,A、B会依次出现四种电平组合:00、01、11、10(或者反过来10、11、01、00,取决于转向)。这四种组合构成一个完整的格雷码循环,每转一个"栅格"其实经历了四个状态跳变。

如果只用A路的上升沿计数,一圈500线的编码器只能得到500个计数;如果同时用A、B的上升沿和下降沿,就能拿到4倍,也就是2000个计数。这就是4倍频的本质——不是把信号"放大"了,而是把原本被忽略的边沿信息全部利用起来。

注意:4倍频不会提高编码器的物理分辨率,它只是把已有的边沿信息榨干。真正的精度上限还是由码盘刻线数决定,倍频只是让计数更细腻。

1.2 定时器编码器接口模式是怎么"看懂"这两路信号的

GD32的通用定时器(比如TIMER1、TIMER2、TIMER3这些)和高级定时器都带编码器接口功能。它的核心是一个方向判别逻辑加一个计数单元。当你把A、B两路分别接到定时器的CI0和CI1通道上,并使能编码器模式后,硬件会自动根据两路信号的边沿和电平关系决定当前是"加"还是"减"。

具体来说,编码器模式有三种子模式:

模式计数触发边沿倍频数适用场景
模式1只对CI0(A相)的边沿计数2倍频信号质量差、只需方向判断
模式2只对CI1(B相)的边沿计数2倍频同上,换一路参考
模式3同时对CI0和CI1的边沿计数4倍频需要最高计数精度

模式3就是我们要的4倍频。它在A、B两路的每个边沿都触发一次计数,一个完整周期下来正好4个计数。配置的时候把SMC(从模式控制)寄存器设成编码器模式3即可。

1.3 为什么有人配了模式3却只得到2倍频

这是实际调试中最常见的"灵异事件"。配置明明写的是模式3,读出来的数却只有理论值的一半。原因通常有两个:

第一个是输入滤波把边沿吃掉了。GD32的定时器每个通道都有独立的输入滤波器(CH0F、CH1F位),如果滤波参数设得过大,高频边沿会被滤掉,导致部分计数丢失。尤其是编码器转速较高时,滤波窗口太宽会直接吞掉窄脉冲。

第二个是极性配置反了。CH0P和CH1P这两位控制输入捕获的极性,如果只反了一路,方向判别逻辑会混乱,计数可能时增时减,看起来就像倍频数不对。正确做法是两路极性保持一致,或者根据实际接线统一取反。

2. 方法一:纯硬件编码器接口模式,最省CPU的4倍频

这是最"正统"的做法,也是GD32官方例程里演示的方式。定时器硬件全权负责解码和计数,CPU只需要定期去读CNT寄存器就行,几乎不占运算资源。

2.1 引脚与定时器的对应关系不能想当然

GD32的定时器通道和GPIO引脚是固定映射的,不是随便选两个引脚就能当编码器输入。以GD32F103系列为例,TIMER1的CH0和CH1默认映射在PA8和PA9,重映射后可以到PA15和PB3。TIMER2的CH0/CH1在PA6和PA7,重映射后到PB4和PB5。

这里有个坑:重映射之后要记得使能AFIO时钟并调用重映射配置函数,否则引脚还是普通GPIO状态,信号根本进不去定时器。我见过有人调了一下午,最后发现是忘了开AFIO时钟。

配置顺序建议这样:

  1. 使能GPIO时钟和AFIO时钟
  2. 配置GPIO为浮空输入或上拉输入(编码器输出通常是推挽或集电极开路,上拉输入更稳)
  3. 调用GPIO_PinRemapConfig做重映射(如果需要)
  4. 使能定时器时钟
  5. 配置定时器的编码器接口参数

2.2 编码器接口模式的关键寄存器配置

GD32的标准外设库(或固件库)里,编码器配置主要涉及TIMER_CTL0、TIMER_SMCFG、TIMER_CHCTL0/1这几个寄存器。用库函数写大概是这样:

timer_parameter_struct timer_initpara; timer_ic_parameter_struct timer_icinitpara; /* 定时器基础配置 */ timer_deinit(TIMER2); timer_initpara.prescaler = 0; timer_initpara.alignedmode = TIMER_COUNTER_EDGE; timer_initpara.counterdirection = TIMER_COUNTER_UP; timer_initpara.period = 65535; timer_initpara.clockdivision = TIMER_CKDIV_DIV1; timer_initpara.repetitioncounter = 0; timer_init(TIMER2, &timer_initpara); /* 编码器接口配置 */ timer_quadrature_decoder_mode_config(TIMER2, TIMER_ENCODER_MODE2, /* 模式3在GD32库里对应MODE2枚举 */ TIMER_IC_POLARITY_RISING, TIMER_IC_POLARITY_RISING); /* 输入滤波配置 */ timer_icinitpara.icpolarity = TIMER_IC_POLARITY_RISING; timer_icinitpara.icselection = TIMER_IC_SELECTION_DIRECTTI; timer_icinitpara.icprescaler = TIMER_IC_PSC_DIV1; timer_icinitpara.icfilter = 0x0; /* 先不滤波,调试阶段用 */ timer_input_capture_config(TIMER2, TIMER_CH_0, &timer_icinitpara); timer_input_capture_config(TIMER2, TIMER_CH_1, &timer_icinitpara); /* 使能定时器 */ timer_enable(TIMER2);

注意GD32库里的枚举命名和STM32略有差异,TIMER_ENCODER_MODE2实际上对应的是"在CI0和CI1上都计数"的4倍频模式。这个命名容易让人误解,建议直接看库头文件里的注释确认。

2.3 读数和方向判断的正确姿势

配置好之后,读TIMER2的CNT寄存器就能拿到计数值。但有两个细节要注意:

第一,CNT是16位还是32位取决于定时器型号。GD32F103的通用定时器是16位,计到65535会溢出。如果行程较长,要么定期清零,要么用软件扩展高位。GD32F4系列的某些定时器是32位的,就没这个问题。

第二,方向判断不能只看CNT增减。因为溢出和回绕的存在,单纯比较两次读数的大小会误判。正确做法是读TIMER2的CTL0寄存器里的DIR位(计数方向标志),或者用带符号的差值计算:

int16_t delta = (int16_t)(current_cnt - last_cnt);

把差值强制转成有符号16位,就能自动处理回绕问题。这个技巧在16位定时器上非常实用。

提示:如果编码器转速很高,建议开启定时器的溢出中断,在中断里累加一个高位计数器,这样就能得到完整的32位甚至64位位置值。

3. 方法二:外部中断+软件解码,灵活但吃CPU

有些场景下定时器的编码器接口不够用,比如需要同时接多路编码器、或者定时器通道被其他功能占用了。这时候可以用外部中断配合软件状态机来做4倍频解码。

3.1 软件解码的状态机设计

思路很简单:把A、B两路都配成双边沿触发的外部中断,每次中断读一次两路的电平,根据"上一次状态"和"当前状态"的组合查表决定加还是减。

状态转移表是这样的(以A、B顺序表示):

上一状态当前状态计数变化
0001+1
0111+1
1110+1
1000+1
0010-1
1011-1
1101-1
0100-1

其他组合(比如00跳到11)说明丢步了,可以选择忽略或报错。这个表本质上就是正交解码的逻辑实现。

3.2 中断频率的估算与CPU占用

软件解码最大的问题是中断频率。假设编码器500线、4倍频、电机转速3000转/分,那么每秒的边沿数是:

500 × 4 × (3000/60) = 100000 次/秒

也就是10万次中断每秒。这个频率对GD32F103(72MHz主频)来说压力不小,中断服务程序如果写得不够精简,CPU会被大量占用,影响其他任务。

所以软件解码适合低转速、低线数的场景,或者CPU负载本来就很轻的场合。如果非要用在高转速下,建议把中断优先级设高、ISR里只做最少的操作(读电平、查表、更新计数),把复杂处理放到主循环。

3.3 消抖与滤波在软件层的处理

外部中断方式还有个好处是可以灵活加软件滤波。比如连续读三次电平,取多数值作为有效状态,这样能滤掉毛刺。但代价是ISR变长,中断频率高的时候反而得不偿失。

我的经验是:如果信号质量本身不错,软件解码不需要额外滤波;如果信号毛刺多,优先从硬件上解决(加RC滤波、改善布线、用差分输出编码器),而不是在软件里硬扛。

4. 方法三:DMA+定时器捕获,高转速下的进阶玩法

前两种方法各有局限:硬件编码器模式受限于定时器数量,软件解码又吃CPU。如果既要高转速又要低CPU占用,可以考虑用定时器捕获+DMA的方式。

4.1 用捕获中断记录边沿时间戳

这个思路不是直接计数,而是记录每个边沿到来的时间戳,然后在主循环里根据时间戳序列反推位置和速度。定时器的输入捕获功能可以自动记录边沿到来时的CNT值,配合DMA把捕获值搬到内存缓冲区,CPU几乎不参与。

具体配置:把A、B两路分别接到两个捕获通道,都设成双边沿触发,开启捕获中断或DMA请求。每次捕获事件发生时,硬件自动把当前CNT值存入CCR寄存器,DMA再把它搬到数组里。

4.2 时间戳序列如何还原出位置和速度

拿到时间戳序列后,位置的计算就变成了对边沿的计数——每个边沿代表1/4个栅格,累计边沿数除以4就是栅格数。速度则可以通过相邻时间戳的差值算出来:

速度 = (1/4栅格) / (t2 - t1)

这种方式的精度取决于定时器的计数频率。如果定时器跑72MHz,时间戳的分辨率就是约14纳秒,测速精度非常高。

4.3 这种方法的适用边界

DMA+捕获的方式适合高速、高精度测速的场景,比如伺服电机的速度环。但它不适合做位置累计,因为时间戳序列会无限增长,内存扛不住。实际用法通常是:位置用硬件编码器模式累计,速度用捕获时间戳计算,两者结合。

另外,DMA缓冲区的管理需要小心。如果缓冲区满了没及时处理,新数据会覆盖旧数据。建议用双缓冲或者环形缓冲,并在DMA传输完成中断里及时搬运。

5. 三种方法的模式对比与选型建议

把三种方法放在一起对比,选型就清晰了。

对比维度硬件编码器模式外部中断软件解码DMA+捕获
倍频数4倍频4倍频不直接计数
CPU占用极低高(随转速上升)低
最高适用转速很高低到中很高
占用定时器资源1个定时器2个外部中断1个定时器+1个DMA
位置累计能力强强弱
测速精度中中高
实现复杂度低中高
适合场景通用位置控制低速多路编码器高速测速

选型的核心逻辑是:先看转速,再看CPU余量,最后看定时器资源。

  • 转速不高、定时器够用:直接上硬件编码器模式,最省心。
  • 定时器不够、转速低:外部中断软件解码。
  • 高转速、需要精确测速:硬件编码器做位置,DMA捕获做速度。

6. 调试中真正会卡住人的几个细节

前面讲的都是"应该怎么做",但实际调试时,真正让人抓狂的往往是下面这些细节。

6.1 滤波器参数设错导致计数"少一截"

GD32的输入滤波器有个计算公式,滤波效果取决于定时器时钟分频和滤波参数。如果编码器信号频率较高,而滤波参数设得偏大,边沿会被滤掉。判断方法:用示波器看定时器输入引脚上的信号,再对比CNT的变化。如果信号边沿明显但CNT不增,基本就是滤波问题。

调试阶段建议先把icfilter设成0(不滤波),确认计数正常后再逐步加大,找到能滤掉毛刺又不丢边沿的平衡点。

6.2 计数方向反了但没人告诉你

编码器的A、B接线顺序决定了计数方向。如果发现电机正转时CNT在减,不用改代码,把A、B两路对调一下就行。或者改CH0P/CH1P的极性配置。两种方法等效,但对调接线更直观。

6.3 16位溢出在长行程下的隐蔽性

16位定时器计到65535就回绕,如果行程超过这个数,位置值会突然跳变。这个问题在短行程测试时发现不了,一到实际长行程就暴露。解决办法有两个:一是用32位定时器(GD32F4系列有),二是开启溢出中断做软件扩展。

软件扩展的写法:

volatile int32_t position = 0; volatile uint16_t last_cnt = 0; void TIMER2_IRQHandler(void) { if(timer_interrupt_flag_get(TIMER2, TIMER_INT_UP) != RESET) { uint16_t now_cnt = timer_counter_read(TIMER2); int16_t delta = (int16_t)(now_cnt - last_cnt); position += delta; last_cnt = now_cnt; timer_interrupt_flag_clear(TIMER2, TIMER_INT_UP); } }

注意这里用有符号差值自动处理了回绕,比单纯判断方向位更可靠。

6.4 编码器信号质量差时的硬件补救

如果软件怎么调都不稳,问题很可能在硬件。常见补救措施:在A、B线上加100欧姆左右的串联电阻限流,对地加几十皮法电容滤高频毛刺,或者换用差分输出的编码器配合差分接收芯片。这些硬件手段比在软件里加滤波有效得多。

7. 从STM32迁移到GD32时容易踩的坑

很多用GD32的人是从STM32转过来的,两者外设高度相似但细节有差异,迁移时容易翻车。

7.1 库函数命名和枚举值的差异

GD32的标准外设库虽然模仿了STM32的风格,但函数名和枚举值不完全一样。比如STM32的编码器模式枚举是TIM_EncoderMode_TI12,GD32里对应的是TIMER_ENCODER_MODE2。直接照搬STM32代码会编译报错,需要对照GD32的库头文件逐个替换。

7.2 时钟树配置的细微不同

GD32F103的时钟树和STM32F103基本一致,但某些型号的PLL配置范围略有差异。如果直接套用STM32的时钟配置,可能出现主频不对或者外设时钟异常。建议用GD32官方的时钟配置工具生成初始化代码,别手抄。

7.3 中断向量表的对应关系

GD32的中断向量表编号和STM32不完全相同,尤其是定时器中断。迁移时如果中断服务函数名写错,中断根本进不去,而且不会有任何报错。这个坑很隐蔽,建议对照GD32的启动文件(startup_gd32fxxx.s)确认中断向量名。

8. 实测数据与性能表现

最后分享一组我在GD32F103C8T6上实测的数据,供参考。

测试条件:500线增量式编码器,4倍频,定时器TIMER2编码器模式3,主频72MHz。

转速(RPM)理论计数/秒实测计数/秒CPU占用
3001000010000<1%
15005000050000<1%
300010000099998<1%
6000200000199992约1%

可以看到硬件编码器模式在6000RPM下依然稳定,CPU占用几乎可以忽略。丢失的几个计数出现在转速突变时,属于正常现象。

对比软件解码方式,同样条件下3000RPM时CPU占用已经超过30%,6000RPM时直接跑飞。所以高转速场景下,硬件编码器模式是唯一靠谱的选择。

提示:实测时建议用示波器同时观察A、B信号和CNT变化,这样能快速定位是信号问题还是配置问题。

我个人在实际项目中的体会是,GD32的编码器接口模式虽然配置项不多,但每个参数都有它的脾气。滤波器、极性、重映射这三样,任何一个设错都会导致计数异常,而且现象往往很相似。所以调试时最好按"先不滤波、确认计数、再加滤波"的顺序来,一步步排除,比一上来就配一堆参数然后抓瞎要高效得多。另外,如果项目里编码器数量多,优先考虑用硬件编码器模式,把CPU留给真正需要算力的任务,这个取舍在资源紧张的MCU上尤其重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询