news 2026/9/7 5:22:19

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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的传输周期中间一定会发生一次电平跳变,而这个跳变的方向就是数据本身。

  • 逻辑0:bit中间从低电平跳到高电平(也可以定义成下降沿,看协议约定)
  • 逻辑1:bit中间从高电平跳到低电平

很多第一次接触的人会把这个跟“电平高低”搞混。实际上曼彻斯特编码里,高低电平本身不重要,重要的是跳变方向。我用一个比较糙的类比:你跟你朋友约好了,走路时每走一步路中间都要抬一下脚——抬脚这个动作是必须的,而抬完脚先落地哪只脚,代表你要传递的信息。这个“中间必跳”的设计,让接收端不需要单独的时钟线,直接从数据流里恢复出时钟来。

这也解释了为什么叫“自同步编码”。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 = 3'd0; localparam START_BIT = 3'd1; localparam SEND_DATA = 3'd2; localparam STOP_BIT = 3'd3; 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 <= 4'd0; end else begin state_c <= state_n; if (bit_tick) clk_cnt <= 4'd0; else clk_cnt <= clk_cnt + 1'b1; 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:发送一个结束电平(高电平),然后回IDLE

3.2 曼彻斯特编码输出逻辑

编码输出的关键在于:每个bit周期内,前半段和后半段电平相反,且跳变方向代表0/1。

我用的映射关系是:

  • 数据1:bit前半段输出高,后半段输出低(高->低跳变)
  • 数据0:bit前半段输出低,后半段输出高(低->高跳变)

对应的Verilog实现:

always @(*) begin if (state_c == START_BIT) begin // 起始位:固定发一个低->高的0 tx_data = (clk_cnt < CLK_DIV/2) ? 1'b0 : 1'b1; end else if (state_c == SEND_DATA) begin case (data_in[bit_cnt]) 1'b1: tx_data = (clk_cnt < CLK_DIV/2) ? 1'b1 : 1'b0; 1'b0: tx_data = (clk_cnt < CLK_DIV/2) ? 1'b0 : 1'b1; endcase end else if (state_c == STOP_BIT) begin // 停止位:固定发一个高->低的1 tx_data = (clk_cnt < CLK_DIV/2) ? 1'b1 : 1'b0; end else begin tx_data = 1'b1; // 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 = 8'hA5; 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_DIV=16时,每个曼彻斯特bit周期是160ns,8bit数据加起始位和停止位一共10个bit周期,总计1.6us。

4.2 波形检查关键点

跑完仿真后,把tx_data信号拉出来看波形,我一般重点检查三处:

  1. idle状态下tx_data保持高电平
  2. 起始位出现一个完整的低->高跳变,且跳变点在bit周期中点
  3. 数据位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_cntbit_cnt这些内部信号。这种情况下我直接用Vivado的ILA(集成逻辑分析仪)核对状态机。

state_cclk_cntbit_cnttx_data都加进ILA,触发条件设为state_c == SEND_DATA && bit_cnt == 4'd0。抓出来的波形里能清楚看到每个bit周期的码型,如果跟你设计预期不符,直接在这一步定位是状态机跳变的问题还是时钟使能的问题。

5.3 外部接收端采样点不匹配

这也是我踩过的一个坑。接收端如果用“检测bit中间的跳变边沿”来恢复时钟,然后在一个固定的偏移点采样电平,那发送端的CLK_DIV必须是偶数,且前半段和后半段必须严格等长。

如果你的系统时钟是33.333MHz,想发1Mbps数据率,每个bit周期需要恰好33.333个时钟周期,这时候没法严格等分。我建议把数据率稍微调低或者调高,选一个能被CLK_DIV整除的频率组合。比如100MHz时钟下,CLK_DIV=32对应3.125Mbps,CLK_DIV=16对应6.25Mbps,这两个都是整数关系,编码占空比精确50%。

5.4 常见问题速查表

现象可能原因解决办法
波形跳变不在bit中间clk_cnt比较符错误< CLK_DIV/2,别用<=
接收端恢复时钟不稳数据率与时钟分频不是整数倍调整CLK_DIV,保证前后半段等长
上板后输出高阻或电平不对IO标准配置错误检查XDC里set_property IOSTANDARD LVCMOS33
长时间发送后丢帧IDLE电平不确定确认IDLE高电平,停止位用高->低
发送速度上不去组合逻辑太深把bit选择逻辑改成case,避免多级MUX

5.5 给时钟恢复留余量的小技巧

最后一个实战技巧。如果接收端是另一个FPGA,你可以在接收端对tx_data做3倍以上的过采样,然后用“检测跳变沿+边沿到中心偏移半个bit周期”的方式恢复采样点。这样即使发送端有少量抖动,接收端也能稳定采样。

我用这个方案在两个FPGA开发板之间用一根杜邦线传了1Mbps的曼彻斯特码,跑了几个小时没有出现误码。如果传输距离再远一些,建议在链路层加上帧头校验和CRC,不过那就是另一个话题了。

这个模块现在已经被我封装成IP核,DATA_WIDTH改成16就直接能用于以太网帧前导码的发送,需要扩展成双向半双工通信的老哥,也可以在编码器基础上加一个三态门控制,实现单总线收发。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 5:22:09

B站后端校招笔试复盘:题型考点与实战策略

去年秋招季&#xff0c;很多学弟学妹问我B站后端开发方向笔试到底考什么。说实话&#xff0c;B站这几年校招笔试的题目风格变化不小&#xff0c;2023届的笔试卷A更偏向“基础工程”的组合&#xff0c;而不是纯粹刷LeetCode。如果你只刷题不补基础&#xff0c;很容易在选择题上翻…

作者头像 李华
网站建设 2026/9/7 5:21:16

BMS放电MOS管开关速度优化:平衡损耗与EMI的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 4:40:55

B站游戏测试校招笔试题解析:从用例设计到质量Owner思维

先说个挺有意思的现象&#xff1a;很多想进游戏行业的人&#xff0c;第一目标都是策划、程序、美术这些“热门岗”&#xff0c;游戏测试常常被当成“不行就先试试”的备胎。但真到校招笔试的时候&#xff0c;尤其是像哔哩哔哩这种对内容和体验有执念的公司&#xff0c;一张游戏…

作者头像 李华
网站建设 2026/9/6 12:21:52

贪心题目:和有限的最长子序列

文章目录题目标题和出处难度题目描述要求示例数据范围解法一思路和算法代码复杂度分析解法二思路和算法代码复杂度分析题目 标题和出处 标题&#xff1a;和有限的最长子序列 出处&#xff1a;2389. 和有限的最长子序列 难度 2 级 题目描述 要求 给定一个长度为 n\textt…

作者头像 李华
网站建设 2026/9/6 4:41:00

RVC快速语音转换指南:10分钟克隆一条专属声线

RVC快速语音转换指南&#xff1a;10分钟克隆一条专属声线 【免费下载链接】Retrieval-based-Voice-Conversion-WebUI Easily train a good VC model with voice data < 10 mins! 项目地址: https://gitcode.com/GitHub_Trending/re/Retrieval-based-Voice-Conversion-WebU…

作者头像 李华
网站建设 2026/9/5 7:37:42

移动端在线笔试复盘:vConsole调试、ECharts与Vue选型全解析

上个赛季我报名了一场牛客的移动端模考笔试&#xff0c;题量不算大&#xff0c;但考完我把题目和考点重新过了一遍&#xff0c;发现这类在线笔试和平时的技术面试完全是两套逻辑。它不会只问你“用过哪些移动端优化手段”&#xff0c;而是直接把你丢进一个限时、纯编辑器、没有…

作者头像 李华