1. 编码器与译码器:从数字电路视角看“翻译官”
做数字电子技术这些年,我越来越觉得编码器和译码器像是一对“翻译官”——编码器把人类世界的信息翻译成机器能识别的二进制代码,译码器则反过来把二进制代码翻译回人类能理解的形式。无论是课堂上教《数字电子技术基础》时讲到组合逻辑电路,还是实际调试STM32、FPGA工程时处理传感器数据、驱动数码管显示,这对“翻译官”几乎无处不在。
这篇文章我想换一个角度,不按教材目录平铺直叙,而是从“工程到底需要什么”出发,把编码器、译码器的原理、芯片选型、Verilog实现、以及与旋转编码器、磁编码器这类实际传感器相关的延伸问题串起来。适合正在学数字电路的学生、刚接触嵌入式的开发者、以及想系统梳理这块知识的工程师。读完你至少能回答这三个问题:编码器和译码器到底在电路里扮演什么角色?优先编码器和普通编码器差在哪?3-8译码器为什么能当数据分配器用?
1.1 一切从组合逻辑的“翻译”需求说起
数字电路两大阵营,组合逻辑和时序逻辑。编码器和译码器都属于典型的组合逻辑电路——输出只取决于当前输入,和过去没关系。它们是后续所有复杂数字系统的基础构件,就像盖楼用的标准砖块。
理解它们时用“翻译”这个角度就很直观。你去餐厅点菜,菜单上写着“宫保鸡丁、鱼香肉丝、麻婆豆腐”,你跟服务员说“来份2号套餐”,这里的“2号”就是对菜品的编码。后厨看到“2号”,要通过对照表知道是宫保鸡丁,这就是译码。编码器干的就是“把有意义的信号变成二进制代码”的活,译码器干的是“把二进制代码还原成有意义的信号”的活。
从信息论角度看,编码的本质是压缩——用更少的比特位表示更多的信息,n位二进制代码能表示2的n次方种不同状态。译码是编码的逆过程。一压一放,构成了数字系统内部二进制世界和外部现实世界之间的桥梁。
2. 编码器的分类与内部逻辑拆解
2.1 普通编码器:n位二进制代码表示2的n次方个输入
先看最基础的二进制编码器。教材上的典型例子是8线-3线编码器:8个输入I0到I7,3个输出Y2、Y1、Y0。任何时刻只允许一个输入为有效电平,输出对应的二进制编码。比如I5有效时,输出就是101。
这里有个很关键的工程约束:普通编码器要求输入“互斥”——同一时刻只能有一个输入有效。一旦两个输入同时有效,输出就会混乱,变成一个没有意义的组合。现实生活中这个条件往往很难满足,比如按键键盘上用户同时按下两个键怎么办?所以实际工程中普通编码器用得很少,优先编码器才是主流。
8线-3线编码器的逻辑表达式也很典型,以高电平有效为例:
Y2 = I4 + I5 + I6 + I7 Y1 = I2 + I3 + I6 + I7 Y0 = I1 + I3 + I5 + I7
这个表达式背后的规律是:某一位输出为1,取决于哪些输入对应的二进制编码在该位上是1。比如编码为4到7的输入,二进制第2位(权值4)都是1,所以Y2就是I4到I7相或。理解了这个规律,任意位数的编码器都能很快推导出来,不用死记真值表。
2.2 优先编码器:现实世界最常用的折中方案
优先编码器的思想特别“工程”——既然无法保证输入互斥,那就规定一个优先级顺序。当多个输入同时有效时,优先响应编号最大的那一个(也可以设计成优先响应编号最小的,取决于芯片规格)。这就像公司里多个紧急请求同时到达,处理规则是“级别高的先来”。
经典芯片是74LS148,16脚DIP封装,8个输入低电平有效,3个编码输出也是低电平有效,还额外引出两个非常有用的控制端:选通输入端EI(Enable Input)和选通输出端EO(Enable Output),以及扩展输出端GS(Group Select)。
这里我必须多说一句关于EI和EO的价值。教材上往往只讲真值表,但工程上这两个引脚恰恰是系统扩展的核心。EI为高电平时整个芯片不工作,所有输出都是高电平,相当于被禁用。EI为低电平时芯片正常工作。EO则是“本片无有效输入且被启用时为低”,GS是“本片有有效输入时为低”。用一片74LS148的EO接到下一片的EI,就能把两片8输入优先编码器级联成16输入。这就是为什么74LS148能构成16线-4线优先编码器的原因。
对于74LS148,带优先级的真值表核心逻辑是:I7优先级最高,只要I7有效,无论其他输入是什么,输出都是I7的编码;只有I7无效时才看I6,以此类推。工程直觉是:优先级高的输入“屏蔽”了优先级低的输入,实现方式是让高优先级信号同时参与低优先级对应的逻辑判断。
2.3 从编码器到FPGA:用Verilog实现行为级描述
到了FPGA时代,硬件描述语言让我们不用再靠离散门电路拼芯片,但这不代表原理不重要。恰恰相反,理解原理才能写出高效的RTL代码。
用Verilog实现8线-3线优先编码器,最直接的方式是用casez语句,配合z通配符处理“任意位”:
module priority_encoder_8to3 ( input [7:0] din, // 输入,高电平有效 input en, // 使能 output reg [2:0] dout, // 编码输出 output reg valid // 输出有效标志 ); always @ (*) begin if (!en) begin dout = 3'b000; valid = 1'b0; end else begin casez (din) 8'b1???????: begin dout = 3'b111; valid = 1'b1; end 8'b01??????: begin dout = 3'b110; valid = 1'b1; end 8'b001?????: begin dout = 3'b101; valid = 1'b1; end 8'b0001????: begin dout = 3'b100; valid = 1'b1; end 8'b00001???: begin dout = 3'b011; valid = 1'b1; end 8'b000001??: begin dout = 3'b010; valid = 1'b1; end 8'b0000001?: begin dout = 3'b001; valid = 1'b1; end 8'b00000001: begin dout = 3'b000; valid = 1'b1; end default: begin dout = 3'b000; valid = 1'b0; end endcase end end endmodulecasez里的问号表示该位可以任意匹配,综合工具会把它实现成优先级逻辑。注意case分支从上到下是“第一个匹配优先”,这正好对应了I7最高优先级的硬件行为。
这里有个很重要的工程经验:如果综合工具发现某个输入位从未参与逻辑综合(综合优化后变成了常量),通常意味着你的编码逻辑写错了。我在实际项目里吃过这个亏,用casez时最怕把分支顺序写反,结果优先级完全反了,调试了大半天才发现问题。
3. 译码器:二进制代码的“反向翻译官”
3.1 二进制译码器与74LS138的工程地位
译码器的经典代表是74LS138,3线-8线译码器。3位二进制输入A2、A1、A0,8个输出Y0到Y7,低电平有效。除此之外,74LS138还有三个使能端:G1高电平有效,G2A和G2B低电平有效。只有当G1=1且G2A=0且G2B=0时芯片才工作,否则所有输出都为高。
这里我想着重强调使能端在工程中的实际意义,远不止“开关”这么简单。首先,它可以做片选信号,在存储器扩展时,用高位地址线通过译码器产生各个存储芯片的片选,CPU访问某个地址区间时只有一个芯片被激活,其他芯片保持高阻态。其次,容易级联扩展,用两片74LS138加上一个反相器就能构成4线-16线译码器,这正是用“使能端”作为第4位地址输入的原理。
3线-8线译码器的输出逻辑表达式,以低电平有效为例:
Y0_n = NOT( G1 · NOT(G2A) · NOT(G2B) · NOT(A2) · NOT(A1) · NOT(A0) ) Y1_n = NOT( G1 · NOT(G2A) · NOT(G2B) · NOT(A2) · NOT(A1) · A0 )
规律很清楚:每个输出对应一个最小项,取反后输出低电平。这就是教材上“译码器是最小项发生器”说法的由来。在FPGA内部,LUT实现本质上也是最小项逻辑,理解这个对应关系对做逻辑优化有帮助。
3.2 显示译码器:从BCD码到数码管的最后一公里
教材上另一类重要的译码器是显示译码器,典型芯片是74LS48(共阴极)和74LS247(共阳极)。它们的输入是8421 BCD码,输出是7段数码管的段驱动信号a到g。
以共阴数码管为例,要显示数字“1”,需要点亮b和c两个段,所以段码是0000110(按a到g排列,g为最高位或最低位取决于习惯)。工程上常用16进制数表示段码表,但不同教材、不同板卡的段码定义顺序可能完全不同,这是新手最容易踩坑的地方。
这里给出一份在FPGA开发板上常见的高电平有效的共阴段码表(g fedcba顺序,即g是最高位):
| 显示字符 | 二进制段码(fedcbag... 注意顺序) | 十六进制 |
|---|---|---|
| 0 | 011 1111 或 100 0000(按实际位序) | 0x3F或0x40 |
| 1 | 000 0110 | 0x06 |
| 2 | 101 1011 | 0x5B |
| 3 | 100 1111 | 0x4F |
| 4 | 110 0110 | 0x66 |
| 5 | 110 1101 | 0x6D |
| 6 | 111 1101 | 0x7D |
| 7 | 000 0111 | 0x07 |
| 8 | 111 1111 | 0x7F |
| 9 | 110 1111 | 0x6F |
上表只是一个约定,你完全可能在其他开发板上看到完全不同的对应关系。真正的工程做法是:以数据手册或板卡原理图为准,自己推导一遍段码表,而不是直接抄网上的例子。我在一次项目中因为照搬网上段码表,结果数码管显示的数字和预期差了十万八千里,原理图上清楚地标着段a接在FPGA的第X引脚,网上例子的位序却完全相反——这类问题在数字电子技术调试时极为常见。
不用74LS48而直接在FPGA里用Verilog实现显示译码器也很简单,本质就是一个case语句查表。
module seg_decoder ( input [3:0] bcd, output reg [6:0] seg // {g,f,e,d,c,b,a},高电平点亮 ); always @ (*) begin case (bcd) 4'h0: seg = 7'b0111111; 4'h1: seg = 7'b0000110; 4'h2: seg = 7'b1011011; 4'h3: seg = 7'b1001111; 4'h4: seg = 7'b1100110; 4'h5: seg = 7'b1101101; 4'h6: seg = 7'b1111101; 4'h7: seg = 7'b0000111; 4'h8: seg = 7'b1111111; 4'h9: seg = 7'b1101111; default: seg = 7'b0000000; // 其他输入全部熄灭 endcase end endmodule值得注意的是default分支,很多初学者会漏掉,结果仿真时出现锁存器(latch),这是因为case语句没有覆盖所有分支,综合工具默认保持原值,意外生成了存储元件。组合逻辑中绝不允许出现锁存器,这是数字电路设计的红线,违背设计意图。
3.3 译码器实现组合逻辑函数:教材中的“标准答案”思维
教材里常说“用3线-8线译码器加与非门可以实现任意三变量组合逻辑函数”。原理很简单:译码器输出的是全部最小项,而任何组合逻辑函数都可以写成最小项之和。把函数对应的最小项输出用与非门合到一起,就实现了函数。
以函数F=Σm(1,3,5,7)也就是F=A'C+AC或者等效形式为例,只需要74LS138的Y1_n、Y3_n、Y5_n、Y7_n接到一个四输入与非门的四个输入端,与非门输出就是F。
工程上这个做法的价值在于:当你需要一个三变量函数、又不想用一堆散装门电路时,一片74LS138外加几个与非门就搞定了,成本低、调试方便。在FPGA时代,这种思路看似被LUT取代了,但理解最小项和LUT之间的映射关系,对做资源优化仍然有帮助——如果你想用更少的LUT实现一个多个输出共用的逻辑,本质上还是在做“最小项共享”。
4. 从教材到实战:旋转编码器、磁编码器与STM32工程应用
如果只停留在74系列芯片层面,数字电子技术这门课就失去了和现实世界的连接感。真实项目中我们遇到的“编码器”,很多不是编码键、编码芯片,而是各种电机位置反馈传感器,比如增量式旋转编码器、绝对式磁编码器。它们虽然在物理形态上完全不同于74LS148,但从数字电路视角看,本质上还是在做“位置信息到数字代码的编码”这件事。
4.1 为什么伺服电机、机器人都离不开编码器
你在相关搜索词里看到“STM32正交编码器应用”“增量式编码器”“双磁编码器测电机位置”,这些都属于运动控制场景。电机一转动,控制器必须实时知道转了多少度、转向如何、转速多快,否则PID控制无从谈起。编码器就是电机的“眼睛”,把机械转角或直线位移转换成电信号。
增量式编码器输出A、B两路正交方波信号,相位差90度。正转时A相超前B相,反转时B相超前A相。通过检测两路信号的相位关系就可以判断方向,通过计数脉冲数量就可以知道角度变化量。四倍频技术更是常用——同时检测A、B的上升沿和下降沿,一个完整脉冲周期可以得到4个计数沿,分辨率直接提升4倍。
STM32的定时器自带正交编码器接口模式,硬件自动完成方向判断和计数,无需软件干预。使用HAL库时配置TIM_Encoder_Interface,拿TIM3为例,CH1对应A相、CH2对应B相:
TIM_Encoder_InitTypeDef encoderConfig = {0}; encoderConfig.EncoderMode = TIM_ENCODERMODE_TI12; // 双沿计数,4倍频 encoderConfig.IC1Polarity = TIM_ICPOLARITY_RISING; encoderConfig.IC2Polarity = TIM_ICPOLARITY_RISING; HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);注意EncoderMode设置为TI12时,芯片在A、B两相的上升沿和下降沿都计数,计数分辨率是物理线数的4倍。假如电机轴端编码器是500线,4倍频后每转有2000个计数脉冲。同时还需要在程序中定时读取CNT寄存器并清除,避免溢出,或者配置溢出中断做累加处理。
4.2 I2C磁编码器:MT6701的读取与滤波校准思路
和光电增量式编码器相比,磁编码器体积更小、耐油污、抗振动,在舵机和云台等紧凑结构中越来越流行。相关热词里出现的MT6701就是一颗基于AMR或霍尔的磁编码器,支持I2C接口输出14位绝对角度。
用STM32F103的HAL库模拟I2C读取SPI/I2C类传感器,基本流程是:初始化引脚为开漏输出,配合外部上拉电阻,手动翻转SCL和SDA电平模拟起始、停止、应答等时序。MT6701的I2C地址一般是0x06(7位),读角度的命令通常是0x00开头,连续读两个字节,拼接成14位角度数据。
这里我特别想强调两个容易翻车的工程细节:
一是I2C时序中应答位(ACK)的处理。霍尔传感器或者磁编码器从机如果没收到正确的寄存器地址,不会产生ACK,主机读回来的数据全是0xFF。排查这种问题最简单的办法是拿逻辑分析仪看波形,比对着数据手册的时序图一帧一帧核对。
二是滤波与校准。磁编码器虽然安装便捷,但不可避免存在偏心、轴磁化不均匀等误差,直接读取的原始角度往往带有一个周期性的波动。工程上常用的做法是采集一整圈数据,做一次最小二乘拟合的误差补偿,或者简单点,在低速转动时做均值滤波后再查表校正。如果要求不高,加一个一阶低通滤波就能有效抑制噪声:
float angle_filtered = 0.9f * angle_last + 0.1f * angle_raw;0.9和0.1的系数决定滤波带宽,响应太慢会影响控制实时性,这个系数需要根据控制频率实测调整,不能盲目抄。
4.3 从编码器数据到实际控制:方向判断与Z相归零
增量式编码器除了A、B两相,通常还有Z相(索引信号),每转一圈输出一个脉冲,用于机械原点归零。工程上“找零”的先后来但是很讲究的:正转、反转时Z相出现的位置可能差半个脉冲周期,如果不加处理,重复上电后的零点位置就不一致,对需要每次上电回原点的设备影响极大。
推荐的做法是:上电后先让电机以一个固定方向低速转动,捕获到Z相脉冲后记下当前计数方向,再继续以同方向转至某预设位置完成归零。整个过程必须保证方向一致,否则Z相会错半个刻线。这类“隐性坑”在教材里完全不会提,但实际调试时让你抓狂的往往就是这种问题。
5. 常见问题与排查技巧实录
5.1 编码器与译码器电路的通用排查思路
我在带学生做综合实验和帮同事定位硬件问题时,总结出一套百试不爽的排查流程。无论是74LS138译码器驱动数码管不亮,还是STM32编码器接口计数异常,第一步永远是“确认电平”。拿万用表或示波器实测引脚电压,对照真值表确认芯片是否处于使能状态,是否存在接触不良或虚焊。
第二步是“确认时序”。对译码器而言,地址输入和使能信号的建立时间/保持时间是否满足芯片要求;对编码器接口而言,A、B相是否真的相差90度,脉冲频率是否超出定时器的最大计数范围。很多STM32开发者在编码器模式下“计数一直为0”,查来查去发现是没有在CubeMX里把对应的两个引脚映射到同一个定时器的CH1和CH2上,导致硬件上根本没有信号进入编码器接口。
第三步是“确认配置”。比如看门狗、时钟树配置是否影响定时器时钟,DMA有没有和编码器计数冲突等。这类问题在纯数字电路教材里不会出现,但在嵌入式项目中占比极高。
5.2 常见故障速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 74LS138输出全为高 | 使能端G1为低或G2A/G2B为高 | 检查使能端接线和电源电压 |
| 数码管显示乱码 | 段码表位序不对、共阴共阳弄反 | 对照原理图确认段码顺序,用一个固定数字逐一测试 |
| 编码器计数一直为0 | 引脚映射错误、编码器接口未使能、信号线接触不良 | 示波器测量A/B相波形,检查CubeMX引脚配置 |
| I2C读回数据全为0xFF | 地址错误、从机未应答、上拉电阻缺失 | 逻辑分析仪抓取时序,查数据手册核对7位地址 |
| 编码器方向判断反了 | 硬件接反或配置中正反转逻辑不对 | 交换A/B相接线或在软件里取反方向标志 |
| FPGA中case语句出现锁存器 | case分支未完全覆盖组合逻辑 | 添加default分支,检查所有输出是否每个分支都赋值 |
| 编码器读数抖动 | 信号未滤波、机械振动、干扰 | 加RC滤波、软件去抖,必要时用施密特触发器整形 |
5.3 一个真实定位案例:旋转编码器“丢步”排查全过程
我之前的项目里有个伺服云台,客户反馈偶尔回不到原位。刚开始以为是机械间隙问题,检查结构没问题,后来定位到增量式编码器读数偶尔会“丢步”。用示波器同时抓A、B相波形后发现,下降沿有明显的振铃,导致STM32的编码器接口多计了几次。
解决方法是:硬件上在A、B相到MCU之间各串联一个1kΩ电阻,并对地并一个10nF电容构成低通滤波,同时在CubeMX里把输入引脚配置为上下拉并使能数字滤波。两招齐下,再跑1000次回原点测试,零丢步。这个案例再次说明,数字电路的设计不能只看逻辑功能,还要考虑物理信号质量,这也是“数字电子技术基础”真正落地到工程时最具价值的部分。
6. 一些过来人的经验和学习建议
学编码器与译码器这块内容,最容易犯的毛病是只背真值表、不深挖逻辑。真值表背完一个星期就会忘,但如果你理解了“编码器输出就是输入最小项的或逻辑”“译码器每个输出对应一个最小项”,那么遇到任何型号的新芯片,拿着数据手册就能自己推导。
动手实践是唯一的捷径。芯片级验证可以买一块74LS138和74LS148,插面包板点亮LED,亲手体验一下“输入”和“输出”的对应关系;FPGA验证可以写Verilog代码实现3-8译码器然后上板测试。理论学得再好,不如到板子上看到真实波形来得扎实。
如果你正走在嵌入式或FPGA方向上,建议把编码器和译码器这一章当成“接口思维”的训练场。它们不仅在74系列芯片里,SPI/I2C接口、地址译码、传感器数据解析、状态机编码,处处都是“编码/译码”思想的延伸。基础打牢了,后面学起来会顺很多。