FPGA实现曼彻斯特编码:RTL设计、仿真与上板调试全攻略

发布时间:2026/9/1 21:43:39
FPGA实现曼彻斯特编码:RTL设计、仿真与上板调试全攻略 简介面向FPGA学习者的曼彻斯特编码完整工程包基于Quartus II环境完成编码器与解码器设计适合数字通信、以太网接口等方向的初学者或工程师参考。曼彻斯特编码在每个比特中间跳变兼具时钟与数据传递FPGA可灵活实现该逻辑。工程围绕上升沿与下降沿两种转换方式实现了数据输入、电平切换、时钟同步及输出接口等关键模块可帮助理解自同步编码在实际硬件中的落地方法。压缩包为rar格式共87个文件主要包括VHDL源码m_code.vhd、m_decode.vhd、原理图与符号文件bdf/bsf、仿真波形vwf/cvwf、工程配置qsf/qpf、下载文件sof/pof以及编译报告rpt/summary整体大小仅175KB结构清晰便于按模块查阅。其中rpt与summary记录了综合、适配和时序分析结果方便排查设计问题。已有618人学习下载。通过该工程读者可以快速掌握FPGA中曼彻斯特编码的寄存器传输级实现思路直接参考工程结构进行仿真验证、引脚约束与板级调试也适合作为课程设计或通信实验的起步模板。 做嵌入式或通信接口的老哥们应该都遇到过这种需求两块板子之间或者一颗MCU和一颗FPGA之间不想专门拉一根时钟线只想用一根数据线把数据传过去还得保证接收端能稳定恢复数据。这种场景下曼彻斯特编码经常被拎出来顶上。我最近正好在FPGA上把这一套重新撸了一遍从原理到RTL设计再到仿真和上板实测踩了几个不大不小的坑整理成一篇实操记录给后面接手的兄弟省点时间。这篇文章不打算绕弯子直接从曼彻斯特编码到底在编什么讲起然后给出我在Vivado里实际调通的编码器实现附带完整的Testbench和排查经验。做以太网、RFID读卡器、车载总线或者传感器单线通信的朋友都可以直接参考这套思路。1. 曼彻斯特编码到底在编什么1.1 码型规则与生活化类比曼彻斯特编码的核心规则其实一句话就能说清每个bit的传输周期中间一定会发生一次电平跳变而这个跳变的方向就是数据本身。逻辑0bit中间从低电平跳到高电平也可以定义成下降沿看协议约定逻辑1bit中间从高电平跳到低电平很多第一次接触的人会把这个跟“电平高低”搞混。实际上曼彻斯特编码里高低电平本身不重要重要的是跳变方向。我用一个比较糙的类比你跟你朋友约好了走路时每走一步路中间都要抬一下脚——抬脚这个动作是必须的而抬完脚先落地哪只脚代表你要传递的信息。这个“中间必跳”的设计让接收端不需要单独的时钟线直接从数据流里恢复出时钟来。这也解释了为什么叫“自同步编码”。NRZ不归零码里如果连续发好几个1接收端根本不知道每个bit的边界在哪曼彻斯特不一样每个bit中间都有跳变相当于把时钟信息嵌进数据里了。1.2 为什么不用NRZ或普通归零码一句话回答因为单线传输时NRZ没有自同步能力RZ归零码虽然每个bit有返回零电平的动作但占空比不好控制而且抗干扰和直流分量特性不如曼彻斯特。曼彻斯特码在工程上还有几个隐性好处每个bit中途必跳变平均电平基本是50%占空比没有直流分量方便通过变压器耦合这也是早期以太网10BASE-T选它的原因电平跳变密度高接收端PLL容易锁定时钟恢复电路简单极性反转时曼彻斯特码本身有校验能力差分曼彻斯特则天然免疫极性反转当然代价也有同样的数据率物理线上的信号频率翻倍。所以曼彻斯特编码虽然简单但带宽效率只有50%100Mbps的以太网需要200M波特率的信号能力这也是它后来被PAM编码逐步替代的原因之一。但到今天RFID、部分CAN收发器、红外遥控、单总线传感器里它依然很常见。1.3 曼彻斯特编码与差分曼彻斯特编码的对比做设计之前一定要先搞清楚协议里要求的是“标准曼彻斯特”还是“差分曼彻斯特”两者实现方式差很多。对比项曼彻斯特编码差分曼彻斯特编码表示方式bit中间跳变方向表示0/1bit中间跳变只做时钟同步bit起始处是否跳变表示0/1极性反转影响0/1互换数据错乱不受影响实现复杂度较低状态机直接映射略高需要记录前一bit电平典型应用以太网、RFID令牌环、部分工业总线我在做FPGA实现时把两种模式都做到了同一个模块里用一个参数切后面细讲。2. 编码器整体设计与时钟方案2.1 外部接口定义与场景定位我这次的设计目标是输入一个8bit并行数据模块内部把它转成曼彻斯特串行码流输出不需要外部输入bit时钟只需要系统时钟。顶层接口是这样定义的module manchester_encoder #( parameter DATA_WIDTH 8, parameter CLK_DIV 16 )( input wire clk, input wire rst_n, input wire data_valid, input wire [DATA_WIDTH-1:0] data_in, output reg tx_data, output wire tx_busy );这里两个关键参数要提前说清楚DATA_WIDTH一次发送的数据宽度我默认8bit用并行接口输入CLK_DIV每一个曼彻斯特bit占多少个系统时钟周期。我设成16也就是每个bit周期内前半段8个时钟输出一种电平后半段8个时钟输出另一种电平为什么是16而不是2或者4因为后面接的接收端通常会用过采样来恢复时钟16倍过采样是比较折中的选择。如果系统时钟不够快也可以降到8倍但解码窗口会变窄对信号质量要求更高。2.2 为什么不用PLL专门生成bit时钟刚开始做的时候我本能地想用PLL生成一个独立的bit时钟比如数据率1Mbps就PLL出一个1MHz时钟来驱动编码状态机。后来发现完全没必要而且会引入新的跨时钟域问题。更稳的做法是用系统时钟分频计数器生成“bit周期使能脉冲”。每个系统时钟周期计数器加1计数到CLK_DIV - 1时拉一个高电平脉冲然后把计数器清零。这样整个编码器只有一个时钟域时序约束只需要约束系统时钟省掉一堆跨时钟域分析的麻烦。2.3 状态机还是计数器直通曼彻斯特编码器的控制逻辑有两种实现思路状态机定义IDLE、START_BIT、DATA_BIT、STOP_BIT等状态每个状态下再根据bit周期内的时间片翻转电平计数器直通用两个计数器一个计数bit个数一个计数每个bit内的时钟周期然后组合逻辑直接生成输出电平我实际用的是状态机加计数器混合的方式。外层状态机负责“空闲-发数据-收尾”内层clk_cnt负责细分每个bit周期组合逻辑根据clk_cnt和当前bit值决定tx_data输出高还是低。这样做的优点是以后要扩展成带起始位、停止位的UART风格帧结构只需要在外层状态机里加状态就行不需要重写编码逻辑。3. RTL实现Verilog代码与设计思路3.1 发送状态机与分频使能先看核心状态机部分localparam IDLE 3d0; localparam START_BIT 3d1; localparam SEND_DATA 3d2; localparam STOP_BIT 3d3; reg [2:0] state_c, state_n; reg [3:0] clk_cnt; reg [2:0] bit_cnt; wire bit_tick (clk_cnt CLK_DIV - 1); always (posedge clk or negedge rst_n) begin if (!rst_n) begin state_c IDLE; clk_cnt 4d0; end else begin state_c state_n; if (bit_tick) clk_cnt 4d0; else clk_cnt clk_cnt 1b1; end end这里有个细节bit_tick用的是CLK_DIV - 1也就是计数器计到最后一个时钟周期时才拉高而不是清零后才拉高。这样bit_tick信号宽度是一个系统时钟周期正好可以当作“下一拍该切换bit内容”的使能。很多新手容易在这里搞反导致状态切换慢了一拍帧结构整个错位。外层状态机的跳转就比较直白了IDLE检测到data_valid后进入START_BIT同时把tx_busy拉高START_BIT发送一个固定bit周期的起始电平低电平用来让接收端知道“数据要开始了”SEND_DATA按顺序发送data_in[7]到data_in[0]发送顺序可以根据协议改成LSB优先STOP_BIT发送一个结束电平高电平然后回IDLE3.2 曼彻斯特编码输出逻辑编码输出的关键在于每个bit周期内前半段和后半段电平相反且跳变方向代表0/1。我用的映射关系是数据1bit前半段输出高后半段输出低高-低跳变数据0bit前半段输出低后半段输出高低-高跳变对应的Verilog实现always (*) begin if (state_c START_BIT) begin // 起始位固定发一个低-高的0 tx_data (clk_cnt CLK_DIV/2) ? 1b0 : 1b1; end else if (state_c SEND_DATA) begin case (data_in[bit_cnt]) 1b1: tx_data (clk_cnt CLK_DIV/2) ? 1b1 : 1b0; 1b0: tx_data (clk_cnt CLK_DIV/2) ? 1b0 : 1b1; endcase end else if (state_c STOP_BIT) begin // 停止位固定发一个高-低的1 tx_data (clk_cnt CLK_DIV/2) ? 1b1 : 1b0; end else begin tx_data 1b1; // IDLE时保持高电平 end end这样当data_in[bit_cnt]为1时先高后低为0时先低后高跳变都在bit中间发生。有朋友可能会问为什么起始位要发一个“低-高”而不是“高-低”这其实对应UART风格的低电平起始脉冲接收端可以用下降沿来检测帧起始。停止位用“高-低”可以让总线在空闲时保持高电平方便做线路空闲检测。3.3 支持差分曼彻斯特编码的扩展前面说过要支持差分曼彻斯特我在这里加了一个DIFF_MODE参数generate if (DIFF_MODE) begin : diff_enc always (*) begin if (state_c SEND_DATA) begin // 差分模式bit起始处电平跳变表示0不跳变表示1 // 具体实现需要一个prev_level寄存器 end end end endgenerate差分模式多了一个历史电平状态判断逻辑会复杂一些一般只有工业总线场景才用标准曼彻斯特模式就不展开了。4. 仿真验证Testbench与波形检查4.1 搭建Testbench写完RTL先别急着上板仿真这一步是必须的。我习惯用Vivado自带的XSim不需要额外装ModelSim工程里直接建一个仿真源文件就行。module tb_manchester_encoder(); reg clk 0; reg rst_n 0; reg data_valid 0; reg [7:0] data_in 8hA5; wire tx_data; wire tx_busy; manchester_encoder #( .DATA_WIDTH(8), .CLK_DIV(16) ) dut ( .clk(clk), .rst_n(rst_n), .data_valid(data_valid), .data_in(data_in), .tx_data(tx_data), .tx_busy(tx_busy) ); always #5 clk ~clk; // 100MHz系统时钟 initial begin #20 rst_n 1; #10 data_valid 1; #10 data_valid 0; #1000; $finish; end endmodule时钟周期设5ns对应100MHz系统时钟。CLK_DIV16时每个曼彻斯特bit周期是160ns8bit数据加起始位和停止位一共10个bit周期总计1.6us。4.2 波形检查关键点跑完仿真后把tx_data信号拉出来看波形我一般重点检查三处idle状态下tx_data保持高电平起始位出现一个完整的低-高跳变且跳变点在bit周期中点数据位0xA5对应码型11001010每个bit中间都有跳变方向正确如果发现跳变点不在中间八成是在写组合逻辑时把比较器写成了小于等于导致前半段多了一个时钟周期。clk_cnt从0计数到15前半段应该是0~7共8个周期比较符要写成clk_cnt CLK_DIV/2也就是clk_cnt 8。4.3 用文本打印做自动校验看波形不够严谨尤其是发多组数据的时候。我习惯在Testbench里再加一段自动比对reg [7:0] rx_shift; reg [3:0] rx_cnt; always (posedge clk) begin if (!tx_busy) begin rx_cnt 0; end else if (bit_center) begin // 在bit中间采样理论上应该采到当前bit的电平值 rx_shift {rx_shift[6:0], tx_data}; if (rx_cnt 8) begin $display(RX DATA: %h, rx_shift); end end end所谓bit_center就是在clk_cnt CLK_DIV/2 - 1时拉一个脉冲。在这个点采样正好是bit中间电平最稳定的位置。这样跑完仿真后看打印出来的RX DATA是否等于A5比对起来一目了然。5. 上板调试常见问题与排查实录5.1 仿真通过上板不工作这是FPGA开发里最经典的问题——“仿真一时爽上板火葬场”。我这次也遇到了现象是接收端收到一堆乱码。排查下来原因有两个第一我没有对tx_data输出做时序约束。在Xilinx 7系FPGA上普通IO输出默认的IOB延迟和Slew Rate可能与外部接收端不匹配。如果接收端对边沿要求比较高需要给tx_data加上Output Delay约束或者至少把IO标准设置成LVCMOS33并调节SLEW属性。第二跨板连接时没有考虑线路上的电容负载。曼彻斯特编码最怕的就是边沿变缓跳变不明显会导致接收端时钟恢复失败。解决方法是尽量缩短线缆长度或者加一个74LVC1T45之类的缓冲器驱动长线。5.2 用ILA看内部信号上板调试时如果你靠示波器测外部引脚很难看到clk_cnt、bit_cnt这些内部信号。这种情况下我直接用Vivado的ILA集成逻辑分析仪核对状态机。把state_c、clk_cnt、bit_cnt、tx_data都加进ILA触发条件设为state_c SEND_DATA bit_cnt 4d0。抓出来的波形里能清楚看到每个bit周期的码型如果跟你设计预期不符直接在这一步定位是状态机跳变的问题还是时钟使能的问题。5.3 外部接收端采样点不匹配这也是我踩过的一个坑。接收端如果用“检测bit中间的跳变边沿”来恢复时钟然后在一个固定的偏移点采样电平那发送端的CLK_DIV必须是偶数且前半段和后半段必须严格等长。如果你的系统时钟是33.333MHz想发1Mbps数据率每个bit周期需要恰好33.333个时钟周期这时候没法严格等分。我建议把数据率稍微调低或者调高选一个能被CLK_DIV整除的频率组合。比如100MHz时钟下CLK_DIV32对应3.125MbpsCLK_DIV16对应6.25Mbps这两个都是整数关系编码占空比精确50%。5.4 常见问题速查表现象可能原因解决办法波形跳变不在bit中间clk_cnt比较符错误用 CLK_DIV/2别用接收端恢复时钟不稳数据率与时钟分频不是整数倍调整CLK_DIV保证前后半段等长上板后输出高阻或电平不对IO标准配置错误检查XDC里set_property IOSTANDARD LVCMOS33长时间发送后丢帧IDLE电平不确定确认IDLE高电平停止位用高-低发送速度上不去组合逻辑太深把bit选择逻辑改成case避免多级MUX5.5 给时钟恢复留余量的小技巧最后一个实战技巧。如果接收端是另一个FPGA你可以在接收端对tx_data做3倍以上的过采样然后用“检测跳变沿边沿到中心偏移半个bit周期”的方式恢复采样点。这样即使发送端有少量抖动接收端也能稳定采样。我用这个方案在两个FPGA开发板之间用一根杜邦线传了1Mbps的曼彻斯特码跑了几个小时没有出现误码。如果传输距离再远一些建议在链路层加上帧头校验和CRC不过那就是另一个话题了。这个模块现在已经被我封装成IP核DATA_WIDTH改成16就直接能用于以太网帧前导码的发送需要扩展成双向半双工通信的老哥也可以在编码器基础上加一个三态门控制实现单总线收发。本文还有配套的精品资源点击获取