FPGA正交编码器非法跳变检测:把16种状态转移和采样边界测清楚
观察正交编码器时,假设连续两次有效采样读到 00→11。A、B都变了,但中间经过 01 还是 10,已经无法从这两个值判断。
这里做一个小模块:比较相邻两次有效采样,发现双比特变化时输出 illegal。重点放在三个容易写错的地方:复位后第一次采样怎么处理,暂停采样后跟谁比较,以及报错后下一次比较用哪个基准。
它适合放在编码器采样链路后面,给调试或上层控制提供异常事件。接口不包含位置计数和方向输出。
1. 先看四个状态,不急着写逻辑
本文把状态写成 AB={A,B}。两个方向的正常状态序列分别为:
text
00 → 01 → 11 → 10 → 00
00 → 10 → 11 → 01 → 00
每次只改变一位。停住时,前后状态也可以完全相同。

图中绿色连线可以双向走,代表单比特变化;红色对角线代表两位都变化。这是状态关系示意图,不是示波器截图。
Microchip的QEI参考手册列出了完整转移表:保持状态不计数,相邻状态对应一个方向的计数,双比特变化属于无效状态变化。Microchip QEI参考手册,Table 1-1
把"是否非法"单独拿出来,只有下面这张表:
| 上次采样 \ 本次采样 | 00 | 01 | 10 | 11 |
|---|---|---|---|---|
| 00 | 保持 | 合法 | 合法 | 非法 |
| 01 | 合法 | 保持 | 非法 | 合法 |
| 10 | 合法 | 非法 | 保持 | 合法 |
| 11 | 非法 | 合法 | 合法 | 保持 |
16种组合中,4种保持、8种单比特变化、4种双比特变化。方向突然改变仍然可以是合法单步,例如 00→01→00,本模块不会把它判成非法。
二进制异或恰好能回答"哪几位变了":
text
previous ^ ab = 00:没有变化
previous ^ ab = 01:只有B变化
previous ^ ab = 10:只有A变化
previous ^ ab = 11:A、B都变化,判非法
所以核心条件是 (previous ^ ab) == 2'b11,不能写成"只要前后不一样就报错"。
2. 采样契约决定这行比较是否有意义
模块名为 quadrature_guard,全部状态在 clk 上升沿更新。
| 信号 | 含义 |
|---|---|
rst |
同步、高有效复位,优先于采样 |
sample_en |
为1时接受一个新的AB样本 |
ab[1:0] |
已同步到本时钟域的AB状态 |
illegal |
这一拍接受的样本是否构成双比特跳变 |
ab 已经同步是接口前提。本模块没有输入同步器,也没有消抖逻辑。实际接编码器引脚时,要先在顶层处理异步输入,再把同步后的结果交给它。
这里还有四条时序规则:
- 复位后的第一次有效采样只建立基准,任何AB值都不报错
sample_en=0时不更新基准,输入端的变化不会被记作已接受样本- 非法样本也会更新基准,下一次仍与最近一次已接受样本比较
- 没有新的非法事件时,
illegal下一拍清零;连续发生非法事件时,它可以连续为高
最后一条影响错误统计。如果上层想统计非法样本数量,应在每个时钟检查 illegal,不能只检测它的上升沿。连续两个非法样本可能对应一段宽度为两拍的高电平。
3. 完整RTL
将以下代码保存为 rtl.sv。代码使用SystemVerilog,文件扩展名和编译选项都要对应。
宏 MUT_ANY_CHANGE 用来故意注入一个错误,后面检查testbench能否抓到它。正常编译不定义这个宏。
systemverilog
`timescale 1ns/1ps
// ab must already be synchronous; sample_en marks an accepted sample.
module quadrature_guard(input logic clk, rst, sample_en,
input logic [1:0] ab, output logic illegal);
logic [1:0] previous;
logic have_previous;
always_ff @(posedge clk) begin
if (rst) begin
previous <= 0; have_previous <= 0; illegal <= 0;
end else begin
illegal <= 0;
if (sample_en) begin
`ifdef MUT_ANY_CHANGE
illegal <= have_previous && ((previous ^ ab) != 0);
`else
illegal <= have_previous && ((previous ^ ab) == 2'b11);
`endif
previous <= ab;
have_previous <= 1;
end
end
end
endmodule
have_previous 很重要。虽然复位把 previous 写成了0,但这个0只是寄存器复位值,还不是编码器已经提供过的样本。没有这个标志,上电时AB恰好为11就会被误判。
illegal <= 0 是每拍默认值。同一个时序块内,接受样本时后面的非阻塞赋值会覆盖这个默认安排,所以该拍事件可以置1;没有接受样本则自然清零。
previous <= ab 放在是否非法的判断之后,但它不受非法结果限制。比较使用的是更新前的 previous,然后把本次样本存下来,作为下次基准。
4. 用一段时序说明暂停和报错后的行为
下面是按接口契约展开的时序示意。表中"基准"和输出均指该上升沿更新后的值。
| 时钟边沿 | sample_en |
ab |
比较对象 | illegal |
更新后基准 |
|---|---|---|---|---|---|
| t0 | 1 | 00 | 首个样本,只建基准 | 0 | 00 |
| t1 | 0 | 01 | 不接受样本 | 0 | 00 |
| t2 | 1 | 11 | 00与11 | 1 | 11 |
| t3 | 1 | 00 | 11与00 | 1 | 00 |
| t4 | 1 | 01 | 00与01 | 0 | 01 |
| t5 | 0 | 10 | 不接受样本 | 0 | 01 |
t1输入端出现了01,但采样使能为0。到了t2,模块比较的仍是00与11,应该报错。如果每拍都更新 previous,这次异常就会被吞掉。
t2报错后基准变成11,t3又出现00,所以连续第二拍报错。到t4,00到01是合法单步,错误输出清零。
更新基准只是继续跟踪后面的观测序列。已经丢失的运动路径不会因此补回来。若上层维护位置计数,发生非法事件后如何标记位置可信度、是否需要重新找零,应由上层控制策略决定。
TI的eQEP把QEPA、QEPB同时变化列为相位错误事件。TI eQEP用户指南,QFLG的PHE字段 但对这个离散采样模块,报错的准确含义是"两次接受的样本相差两位"。它可能来自输入干扰,也可能是两个正常边沿之间的状态没有被采到,单凭这一位告警不能区分原因。
5. 把16种组合全部跑一遍
这一题的状态空间很小,可以直接穷举,不必只挑一条漂亮的正转序列。
testbench对每一组 old/nxt 都先复位,再送入基准和下一次采样。期望值用 old+nxt==3 表达:在取值0、1、2、3内,只有0与3、1与2这四个有序组合互为双比特取反。它与RTL的异或表达式不同,便于人工逐项核对。
穷举之后继续测试正向一圈、反向一圈、停采样、连续非法、非法后的基准更新和复位优先级。
5.1 完整testbench
保存为同一目录中的 tb.sv:
systemverilog
`timescale 1ns/1ps
module tb;
logic clk=0, rst=1, sample_en=0;
logic [1:0] ab=0;
wire illegal;
integer checks=0;
always #5 clk=~clk;
quadrature_guard dut(.*);
task automatic step(input bit reset_i, en_i, input logic [1:0] value,
input bit expected, input string label_i);
@(negedge clk); rst=reset_i; sample_en=en_i; ab=value;
@(posedge clk); #1;
checks++;
if (illegal !== expected)
$fatal(1,"CHECK_FAIL QUAD %s got=%b expected=%b",label_i,illegal,expected);
endtask
initial begin
// All 16 old/new pairs, including 4 holds and 4 two-bit jumps.
for (int old=0; old<4; old++) begin
for (int nxt=0; nxt<4; nxt++) begin
step(1,0,0,0,"reset");
step(0,1,old,0,"first sample");
step(0,1,nxt,(old+nxt==3),"all transitions");
step(0,0,0,0,"pulse clears without sampling");
end
end
step(1,0,0,0,"reset");
step(0,1,0,0,"seed");
step(0,1,1,0,"forward 00->01");
step(0,1,3,0,"forward 01->11");
step(0,1,2,0,"forward 11->10");
step(0,1,0,0,"forward 10->00");
step(0,1,2,0,"reverse 00->10");
step(0,1,3,0,"reverse 10->11");
step(0,1,1,0,"reverse 11->01");
step(0,1,0,0,"reverse 01->00");
step(0,0,1,0,"disabled input ignored");
step(0,1,3,1,"compare with last accepted sample");
step(0,1,0,1,"consecutive illegal samples");
step(0,1,1,0,"baseline follows illegal sample");
step(1,1,3,0,"reset priority");
step(0,1,2,0,"new baseline after reset");
$display("TEST_PASS QUAD checks=%0d transitions=16",checks); $finish;
end
initial begin #10000; $fatal(1,"WATCHDOG QUAD"); end
endmodule
激励在下降沿改变,上升沿后1 ns检查输出。这样检查点落在非阻塞赋值更新之后,避免testbench读到上一拍结果。
!== 是四态比较,输出里有X或Z也会失败。末尾的watchdog避免错误修改后仿真一直不结束。
5.2 运行方法
本次实测采用Windows PowerShell和Vivado Simulator 2022.1,时钟周期10 ns;进行了数字RTL仿真,没有进行综合、静态时序分析、CDC或板级测试。
创建一个新的空目录,把上面的 rtl.sv 与 tb.sv 放进去。确保当前终端能找到 xvlog、xelab、xsim,然后运行:
text
xvlog -sv rtl.sv tb.sv
xelab work.tb -s sim -debug typical
xsim sim -runall
三条是仿真器命令,不是Tcl脚本。Windows可以在配置好Vivado环境的PowerShell或命令提示符中执行;Linux应先加载本机Vivado的 settings64.sh,再在Bash中执行同样三条命令。这里不依赖PowerShell专用脚本,也不需要其他HDL库。AMD UG900 2022.1,命令行仿真
原版实测输出中的关键行是:
text
TEST_PASS QUAD checks=80 transitions=16
$finish called at time : 806 ns
checks=80 表示80次逐拍输出检查,其中64次来自16组状态对各自的复位、首样本、下一样本和脉冲清零检查。剩下16次覆盖连续序列和接口边界。它不是80种独立状态转移。
5.3 故意改错一次,检查测试是否真的有效
另建一个空目录,复制同样两份源码,编译时加上宏:
text
xvlog -sv -d MUT_ANY_CHANGE rtl.sv tb.sv
xelab work.tb -s sim -debug typical
xsim sim -runall
宏会把判据改成"任何变化都非法"。这次实际输出为:
text
Fatal: CHECK_FAIL QUAD all transitions got=1 expected=0
Time: 76 ns
失败发生在合法的00到01转移处,说明testbench确实区分了"输入变化"和"非法变化"。它先成功编译、展开,再由功能检查报错。
这次xsim在 $fatal 后仍返回了进程退出码0。因此自动化脚本应同时检查 TEST_PASS、CHECK_FAIL、Fatal 和watchdog,不能只根据退出码判定通过。
6. 接到板子前,再核对三个边界
6.1 同步和消抖不是本模块的功能
异步引脚不能直接作为同步逻辑的数据前提。AMD的单比特同步宏说明了同步链的采样要求和可配置级数。AMD XPM_CDC_SINGLE
实际同步电路要结合器件CDC约束和输入速率设计。两级寄存器的作用是降低亚稳态传播风险,不能保证任意窄脉冲都被捕获,也不能让采样不足自动消失。
机械旋钮还可能需要滤波。之前的按键消抖文章讨论了单路电平保持一段时间才确认的办法。用于A/B两路时,要重新核算最短状态保持时间,不能直接照搬长延时。滤波窗口太长会吞掉正常边沿,两路不同的延迟还可能改变观察到的先后次序。
6.2 采样频率要按边沿数核算
假设编码器每转产生N个完整AB周期,转速为R rpm,那么理想情况下,每秒状态变化次数为:
text
f_transition = 4 × N × R / 60
例如N=1000、R=3000,得到每秒200000次状态变化,理想相邻状态间隔5 μs。如果每10 μs才接受一次样本,即使系统主时钟很快,也已经可能跳过中间状态。
这里真正相关的是 sample_en 的接受节奏。上面的平均间隔还没有包含占空比误差、相位误差和抖动,工程上应检查最短保持时间并保留余量,不能只让两个平均频率刚好相等。
6.3 illegal=0 不代表没有丢步
考虑从00开始、期间跨过多次真实状态变化:
text
跨过2次:00 → 01 → 11 采到00与11,会报非法
跨过3次:00 → 01 → 11 → 10 采到00与10,看起来像反向单步
跨过4次:00 → 01 → 11 → 10 → 00 采到00与00,看起来没有移动
本模块能抓到第一种观测,但抓不到后两种。排查时应同时看原始A/B、同步后的A/B、sample_en 和 illegal;先确认有没有漏采样,再检查状态判断。
把这些边界写清楚,非法检测器就能提供明确的信息:哪一拍接受了双比特变化。测试也围绕这个契约展开,首样本、停采样和连续异常都能在同一组源码里复现。