当前位置: 首页 > news >正文

XJTUSE - 从零构建:一个基于自拟协议与FPGA的通信装置实战

1. 项目缘起:从“灯语”到数字通信

几年前,我还在实验室里捣鼓单片机,想用两个开发板互相传个“Hello World”。那时候用的是现成的串口协议,配置好波特率,调用几个库函数,数据就发出去了。方便是方便,但总觉得像个黑盒子,知其然不知其所以然。后来接触到FPGA,这种“一切皆可自定义”的硬件编程方式让我着迷。我就在想,能不能抛开那些现成的协议栈,从最底层开始,亲手搭建一个最简单的通信系统?哪怕只是传输一个0到9的数字,也要把数据怎么组织、怎么校验、怎么握手、怎么确认的每一步都搞清楚。

这就是“基于自拟协议与FPGA的通信装置”项目的初衷。它不是一个追求高性能的工业级设计,而是一个绝佳的学习沙盒。我们的目标很纯粹:在一块FPGA开发板(比如常用的FMK50T4)上,用硬件描述语言Verilog,设计一套我们自己的、极其精简的通信规则,并让它真正跑起来,用LED灯显示结果。这个过程,你会亲手触摸到数字通信的骨架:数据编码、状态控制、错误校验。当你按下按钮,看到LED灯按照你设计的逻辑亮灭,那种“从无到有”的创造感和对底层原理的透彻理解,是使用现成模块无法比拟的。

这个项目非常适合电子工程、计算机、嵌入式相关专业的学生,或者任何对硬件和通信底层感兴趣的技术爱好者。你不需要有深厚的FPGA开发经验,但需要对数字电路基础(比如触发器、状态机)和Verilog语法有初步了解。跟着做下来,你收获的将不仅仅是一个会闪灯的小装置,而是一套完整的硬件系统设计思维——从协议设计、RTL编码、功能仿真到物理约束、上板调试的全流程实战经验。下面,我就把我从零搭建这个系统的详细过程、踩过的坑和最终调试成功的经验,毫无保留地分享给你。

2. 核心基石:理解我们的“通信语言”

在开始写代码之前,我们必须先定义好通信双方都能听懂的“语言”。这套语言包括两大部分:数据如何表示(编码)和传输如何保证基本正确(校验)。在我们的简易系统中,我们选择了BCD码和奇偶校验这对经典组合。

2.1 数据身份证:BCD码的妙用

我们日常习惯用十进制数,但数字电路只认识0和1。传输一个十进制数字“9”,最直接的想法是把它转换成二进制“1001”。这没问题,但如果我想传输一个多位十进制数,比如“23”,电路处理起来就需要先理解这是一个整体。

这里我们引入BCD码。它的规则非常简单:用4位二进制数,直接表示1位十进制数(0-9)。注意,是“直接表示”,而不是数学上的“等值转换”。

  • 十进制“5”: 在BCD码里就是0101(二进制4‘b0101)。
  • 十进制“9”: 就是1001(4‘b1001)。
  • 那么“23”呢? 不是把23转换成二进制10111,而是把“2”和“3”分别编码:2->0010,3->0011。所以“23”的BCD码是0010 0011

你可能会问,1010(十进制10)到1111(十进制15)这6个编码去哪了?在BCD码里,它们是非法的,不会被使用。这就像一个约定:凡是看到以4位为一组,并且值在10-15之间的,就知道传输出错了。

在我们的项目中,为了极致简化,我们只传输1位十进制数(0-9)。所以,我们需要4个比特(也就是4个LED灯或者4个寄存器位)来承载这个BCD码。例如,我们想传输数字“7”,那么在电路内部,数据线data[3:0]上就应该出现0111。这个设计选择让数据表示非常直观,也方便我们后续用LED灯来可视化显示。

2.2 简单的“哨兵”:奇偶校验码

数据在传输中,可能因为干扰出现比特翻转,比如0111(7)变成了0110(6)。如何让接收方发现这个错误?我们需要增加一点冗余信息,这就是校验码。

奇偶校验是最简单的一种。它只关心数据中“1”的个数是奇数还是偶数。

  • 奇校验: 保证“数据位+校验位”的总共“1”的个数为奇数
  • 偶校验: 保证“数据位+校验位”的总共“1”的个数为偶数

怎么算校验位呢?以奇校验为例:假设我们的4位BCD码是0111(里面有3个“1”,是奇数个)。为了保持总数为奇数,我们添加的校验位就应该是0,这样0 0111中“1”的个数还是3(奇数)。如果数据是0011(2个“1”,偶数个),那么校验位就应该是1,变成1 0011,总共3个“1”(奇数)。

在Verilog里,计算奇校验位有一个非常优雅的写法,就是用异或操作。对于4位数据data[3:0]

wire parity_bit = ~(data[3] ^ data[2] ^ data[1] ^ data[0]); // 奇校验

^是按位异或,相同为0,不同为1。data[3]^data[2]^data[1]^data[0]这个连续异或的结果,本质上就是判断这4位中有奇数个1还是偶数个1(奇数个1结果为1,偶数个1结果为0)。我们取反~,就得到了奇校验位(需要让总数为奇,所以校验位与异或结果相反)。

在我们的装置里,verify信号就是这个奇校验位。我们会把它和4位BCD码一起“传输”(实际上是在同一块FPGA内,从一个模块传递到另一个模块,但逻辑上模拟了发送和接收)。接收方(状态机)可以重新计算一次校验,与收到的verify比对,如果不一致,就说明传输可能出错了。虽然奇偶校验只能检测奇数个比特的错误(比如1个、3个比特翻转变能发现,2个比特翻转就发现不了),但对于我们这个学习性的、短距离的板内“传输”来说,已经足够帮助我们理解校验的概念了。

3. 协议设计:为通信制定“交通规则”

有了数据格式(BCD码+奇校验),现在需要设计通信的流程,也就是协议。想象一下两个人打电话:A先拨号(发起连接),B接听(响应),然后A说话(发送数据),说完后A说“再见”(结束连接),B挂断。我们的自定义协议就是定义这样一个简单的“通话礼仪”。

我们采用状态机来建模这个协议,这是数字设计中的核心思想。状态机把整个通信过程划分为几个明确的“状态”,每个状态下系统只做特定的事,并且根据输入条件,清晰地跳转到下一个状态。

我设计的协议状态机包含5个状态:Idle(空闲)、Start(开始)、Data(传输数据)、Stop(停止)、Error(错误)。整个流程是这样的:

  1. Idle(空闲): 系统上电或复位后的初始状态。在此状态下,系统等待“开始通信”的指令。这个指令是什么呢?我们约定:当用户按下数据按钮(btn)且当前准备输入的数据位data_in0时,才认为是一个有效的启动信号。这相当于打电话前先拨一个特定的“启动码”。
  2. Start(开始): 收到有效的启动信号(data_in==0)后,状态机进入Start状态。这个状态通常很短暂,它标志着通信会话正式建立,准备接收真实数据。
  3. Data(传输数据): 这是核心状态。在此状态下,用户每按一次btn按钮,就录入1比特数据。我们需要连续录入4比特,构成一个完整的BCD码。状态机内部需要一个计数器cnt,从0计数到3,记录已经收到了几位数据。当cnt==3且第4位数据录入后,状态机就要判断下一步:如果此时data_in1,则进入Stop状态(正常结束);如果是0,则进入Error状态(协议违规)。
  4. Stop(停止): 成功接收到4位BCD码,并且以1作为结束标志,状态机进入Stop。这里我们会拉高一个done信号,表示“一次通信成功完成!”。状态机将停留在此状态,直到系统复位。
  5. Error(错误): 如果在Data状态收满4位后,结束标志不是1,或者在通信过程中出现其他未预期的序列,状态机进入Error状态。在此状态下,系统可以点亮一个错误指示灯,并等待复位或特定的恢复序列(比如等待data_in变为1后回到Idle)。

这个状态转移逻辑,我用一个表格来总结,会更清晰:

当前状态条件(输入data_in及计数器)下一个状态说明
Idledata_in == 0且按下btnStart收到有效启动信号
Idledata_in == 1且按下btnIdle启动信号无效,保持空闲
Start按下btnData开始接收数据位,计数器清零
Datacnt < 3且按下btnData继续接收数据位,计数器加1
Datacnt == 3data_in == 1,按下btnStop数据接收完毕且结束符正确
Datacnt == 3data_in == 0,按下btnError数据接收完毕但结束符错误
Errordata_in == 1且按下btnIdle错误恢复,回到空闲
Errordata_in == 0且按下btnError保持错误状态
Stop任何输入Stop完成状态,等待复位

这个协议虽然简单,但包含了通信协议的几个关键要素:帧起始界定符(Start)、数据载荷(Data)、帧结束界定符(Stop)以及错误处理(Error)。用Verilog实现这个状态机,是整个项目的逻辑中枢。

4. 硬件实现:在FMK50T4上“搭积木”

理论设计好了,现在要用硬件语言Verilog在FPGA里把它构建出来。我使用的平台是FMK50T4开发板,它上面有足够的LED灯、按键和拨码开关,非常适合做这种交互实验。整个系统的顶层模块,我把它叫做top_module,它的“积木块”(模块)划分和信号连接是这样的:

首先,看看我们和外界交互的“手脚”——输入输出端口

module top_module( input btn, // 主按钮,用于确认输入和触发状态转移 input switch, // 拨码开关,用于切换要输入的数据是0还是1 input reset, // 复位按钮,让一切归零 input change, // 另一个按钮,用于触发某些参数更新(如done) output reg done, // 完成标志,1表示一次通信成功 output [5:0] led, // 6个LED,低4位[3:0]显示BCD码,高2位可作它用 output verify // 奇校验位输出 );
  • btn: 这是我们的“确认键”。无论是启动通信、输入数据还是结束通信,都需要按下它来告诉状态机:“当前data_in的值有效,请处理”。
  • switch: 这是一个拨动开关。拨到一边,data_in信号为1;拨到另一边,data_in为0。它让我们可以自由选择下一次按下btn时,输入的是0还是1。
  • reset: 全局复位。无论状态机跑到哪里,一按reset,全部回到初始Idle状态,所有寄存器清零。
  • change: 这是一个辅助时钟信号。在实际调试中,我发现状态更新和done信号更新需要另一个时钟沿来同步,避免产生毛刺,所以引入了这个按钮作为另一个触发时钟。

系统内部的核心逻辑,主要由三个“功能单元”构成:

1. 数据录入与存储单元:这个单元负责把用户通过switchbtn输入的串行比特流,组装成一个4位的并行数据(我们的BCD码)。我采用了一个移位累加的思路。定义一个4位寄存器mem[3:0]来存储数据。

reg [3:0] mem = 1; // 初始值,方便观察 reg [3:0] step = 4'b0010; // 乘数因子,实现左移效果 always@(posedge btn) begin if (!reset) mem <= 1; else mem <= mem * step + (data_in ? 1 : 0); end

这段代码的意思是:每次按下btn(上升沿),如果没有复位,就把mem恢复到初始值1。否则,执行mem = mem * 2 + data_in。这其实是一个巧妙的二进制左移并加新位的过程。例如,初始mem=0001data_in=1时按下btn,新mem = 0001*2 + 1 = 0011。再按一次btndata_in=0,新mem = 0011*2 + 0 = 0110。这样,我们就在mem中依次存入了10。最终,mem的4位就是我们要传输的BCD码,同时直接赋值给led[3:0]输出显示。

2. 协议状态控制单元(核心状态机):这就是实现上一节状态表的具体代码。它定义了状态寄存器state、下一个状态nstate,以及状态转移逻辑。

parameter IDLE=0, START=1, DATA=2, STOP=3, ERROR=4; reg [2:0] state, nstate; // 状态转移逻辑(在btn上升沿触发) always@(posedge btn) begin if(!reset) begin nstate <= IDLE; end else begin case(state) IDLE: nstate <= (data_in == 0) ? START : IDLE; START: nstate <= DATA; DATA: begin if(cnt == 4) // 假设cnt从1计数到4 nstate <= (data_in == 1) ? STOP : ERROR; else nstate <= DATA; end ERROR: nstate <= (data_in == 1) ? IDLE : ERROR; STOP: nstate <= STOP; default: nstate <= IDLE; endcase end end // 状态寄存器更新(在change上升沿触发) always@(posedge change) begin if(!reset) state <= IDLE; else state <= nstate; end

这里有个细节:状态转移的判断(always@(posedge btn))和状态寄存器的更新(always@(posedge change))用了两个不同的时钟沿。这是一种常见的异步状态机设计技巧,可以避免组合逻辑环路,并让状态变化更稳定。cnt计数器则在另一个always块中,在Data状态下随着btn按下而递增。

3. 输出生成单元:这个单元根据当前状态和存储的数据,产生doneverify信号。

  • done信号:当状态机进入STOP状态时,done被置为1,表示一次通信成功完成。它直接驱动一个LED灯,成功则灯亮/灭。
  • verify信号:这是一个连续赋值语句,实时计算mem中4位数据的奇校验位。
assign verify = ~(mem[1] ^ mem[2] ^ mem[3] ^ mem[4]); // 注意这里索引从1开始,根据实际连接调整 assign led = {2'b00, mem}; // 将mem赋值给LED的低4位

verify的计算结果也会驱动一个LED灯。你可以直观地看到,随着你输入不同的BCD码(LED[3:0]变化),verify对应的LED灯也会自动变化,验证了奇偶校验的逻辑。

5. 仿真验证:在电脑里“预演”一切

代码写完了,千万别急着烧录到板子上!在硬件设计里,仿真是保证设计正确的关键一步。我用的是Vivado自带的仿真工具(当然你也可以用ModelSim等)。仿真的目的,就是给我们的设计模块输入一系列模拟的“测试信号”,看看它的输出是否符合预期。

首先,要编写一个测试平台。这个测试平台就像一个小导演,负责生成btnswitchreset等信号的“剧本”。

`timescale 1ns / 1ps // 定义时间单位 module tb_top_module(); reg btn, switch, reset, change; wire done, verify; wire [5:0] led; // 实例化被测试的设计 top_module uut ( .btn(btn), .switch(switch), .reset(reset), .change(change), .done(done), .led(led), .verify(verify) ); initial begin // 初始化所有信号 btn = 0; switch = 1; reset = 0; change = 0; #100 reset = 1; // 100个时间单位后释放复位 // 测试用例1:正常通信流程 (传输BCD码 0101, 即十进制5) // 1. 启动:data_in需要为0 switch = 0; // 设置data_in=0 #20 btn = 1; #20 btn = 0; // 模拟按下并松开btn,发出启动信号 // 2. 输入4位数据 0101 switch = 1; #20 btn = 1; #20 btn = 0; // 输入1 switch = 0; #20 btn = 1; #20 btn = 0; // 输入0 switch = 1; #20 btn = 1; #20 btn = 0; // 输入1 switch = 0; #20 btn = 1; #20 btn = 0; // 输入0 (此时数据位已满) // 3. 输入结束符1 switch = 1; #20 btn = 1; #20 btn = 0; // 输入结束符1 // 触发状态更新(需要change信号) #20 change = 1; #20 change = 0; // 观察波形,此时done应该变为1,led[3:0]应为0101,verify根据0101计算(有偶数个1,奇校验位应为1) // 测试用例2:错误流程(结束符为0) #100 reset = 0; #20 reset = 1; // 复位 // ... 重复上述步骤,但在最后一步输入结束符时让switch=0 (data_in=0) // 观察波形,此时状态机应进入ERROR状态,done保持0 #200 $finish; // 结束仿真 end endmodule

运行仿真后,会弹出波形查看器。你需要像侦探一样,仔细核对每一个时间点:

  • 按下btndata_in为0时,状态state是否从IDLE跳到了START
  • 随后每次按下btnmem寄存器是否按预期左移并添加新位?cnt计数器是否在递增?
  • 输入第4位数据后,再按btn,如果data_in是1,状态是否跳转到STOPdone变高?如果data_in是0,是否跳转到ERROR
  • led输出是否始终等于memverify信号是否始终等于~(mem[1]^mem[2]^mem[3]^mem[4])的结果?

我强烈建议你多设计几个测试用例:正常流程、错误结束符、中途复位、连续传输等。只有仿真波形完美符合设计预期,才能进行下一步。我在这里花了大量时间,因为最初的状态机设计有漏洞,在某个边缘条件下会卡住,全靠仿真发现了问题。

6. 约束与上板:让设计在真实世界运行

仿真通过,只代表逻辑正确。要让它在真实的FMK50T4开发板上跑起来,我们必须告诉综合布线工具两件关键事:每个输入输出信号对应板子上的哪个物理引脚,以及如何处理那些被用作时钟的按钮信号。这就是约束文件的作用。

1. 引脚约束文件 (.ucf 或 .xdc):我们需要查阅FMK50T4的开发板原理图或用户手册,找到按键、LED对应的FPGA引脚编号。例如:

# 假设的引脚分配,请务必根据你的实际板卡手册修改! NET "btn" LOC = "AA22"; # 某个按键 NET "switch" LOC = "T20"; # 某个拨码开关 NET "reset" LOC = "U20"; # 另一个按键 NET "change" LOC = "T19"; # 另一个按键 NET "led[0]" LOC = "N17"; # LED0 NET "led[1]" LOC = "P18"; # LED1 NET "led[2]" LOC = "P19"; # LED2 NET "led[3]" LOC = "N19"; # LED3 NET "led[4]" LOC = "N18"; # LED4 (可能用于其他指示) NET "led[5]" LOC = "R18"; # LED5 (可能用于其他指示) NET "done" LOC = "M17"; # 连接到一个LED,指示完成 NET "verify" LOC = "R19"; # 连接到一个LED,指示校验位

这个文件将逻辑信号(如btn)和物理引脚(如AA22)绑定在一起。没有它,你的设计就像无头苍蝇,不知道信号该从哪里进哪里出。

2. 时序约束与时钟例外:我们的btnchange等信号在代码中被用作时钟边沿触发(posedge btn)。但本质上,它们是机械按键产生的信号,不是纯净、稳定的时钟。FPGA工具默认会把所有posedge信号当作时钟来处理,并试图将其布线到专用的全局时钟网络上,这会导致布局布线错误或警告。

因此,我们需要创建一个额外的约束文件(如.fdc或直接在.xdc中声明),告诉工具:“别把btnchange当成真正的全局时钟来严格对待”。

# Vivado 中的示例语法 set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets btn]; set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets change];

这条约束的意思是,对于btnchange这两个网络,关闭专用的时钟布线路径约束。允许工具使用普通的布线资源来连接它们,从而避免报错。这是使用按键作为时钟信号时一个非常常见且重要的技巧。

生成比特流文件后,通过JTAG下载到FPGA中。上电的瞬间,心情是最激动的。

7. 调试与优化:和硬件“对话”的艺术

板子跑起来了,但LED的闪烁可能不按你预想的来。别慌,硬件调试是项目最“硬核”也最有成就感的部分。我总结了几步排查法:

1. 静态检查:

  • 复位测试: 首先,什么都别动,只按一下reset键。所有LED(除了电源灯)应该恢复到初始状态(比如只有led[0]亮,代表mem=0001)。如果复位都没反应,检查复位信号极性(是高复位还是低复位?我的代码假设是低电平复位!reset),以及引脚约束是否正确。
  • 输入响应测试: 拨动switch,观察一个专门用来指示data_in的LED(如果你分配了的话),或者通过后续逻辑验证。按一下btn,观察led[3:0]是否变化。这一步验证最基本的输入输出通路是否畅通。

2. 动态协议测试:

  • 走一遍正常流程: 严格按照协议操作:switch拨到0 -> 按btn(启动)->switch拨到1 -> 按btn(输入1)->switch拨到0 -> 按btn(输入0)-> … 输入4位BCD码 ->switch拨到1 -> 按btn(结束)。观察done对应的LED是否亮起。如果亮了,恭喜,核心协议通了!
  • 触发状态更新: 注意!在我的设计中,state的更新和done的赋值是在change信号的上升沿。所以完成上述序列后,你需要再按一下change按钮,done灯才会变化。这是一个设计选择,为了同步。如果你发现数据对了但done没反应,检查是不是忘了按change

3. 问题排查:

  • 状态卡死: 如果操作后系统无反应,可能是状态机卡在某个状态了。可以尝试用多个LED来直接显示state的二进制值(比如state是3位,用3个LED表示),这是最直接的调试手段。对比你的操作和状态显示,就能知道卡在哪一步。
  • 校验位不对: 如果verify灯的状态和你心算的奇校验结果不一致,检查assign verify = ...这行代码的位索引是否正确。mem[1]mem[4]对应的是led[1]led[4]吗?硬件连接顺序容易搞错。
  • 计数器问题: 如果数据没录满4位就跳转了,检查计数器cnt的逻辑。它是在Data状态下,每次按btn就加1吗?它的清零时机对吗(在进入Stop或复位时清零)?

我踩过的一个坑:最初我没有把状态更新(state <= nstate)和done赋值放在change时钟域,而是和状态判断一样放在了posedge btn里。结果在仿真中没问题,但上板后出现了不可预测的毛刺和亚稳态,done信号偶尔会闪烁。这就是典型的异步时钟域问题。将状态寄存器更新移到一个独立的、相对稳定的时钟域(哪怕是另一个按键change),大大提高了系统的稳定性。硬件调试就是这样,需要耐心观察、大胆假设、小心验证。每解决一个问题,你对整个系统的理解就加深一层。

当最终,你按照自己设计的协议,一步步操作按钮,看到LED灯依次亮灭,最终done灯如愿点亮,verify灯也符合奇偶规律时,那种亲手赋予一堆硅芯片以逻辑生命的成就感,是软件编程难以完全给予的。这个项目就像一把钥匙,帮你打开了用硬件思维解决通信问题的大门。你可以在此基础上无限扩展:增加更复杂的校验(如CRC)、设计全双工通信、加入地址寻址、甚至用高速串行器实现更快的传输。一切,都从这最简单的自拟协议开始。

http://www.cnnetsun.cn/news/1287055.html

相关文章:

  • 从零部署到实战:OpenPCDet 3D检测环境搭建与模型调优全攻略
  • 深入解析32/64位Windows虚拟扫描仪的自定义图片加载机制
  • AI智能二维码工坊实战落地:企业宣传页集成部署详细步骤
  • [深度解析]机器人正向运动学建模:从关节角度到末端坐标的实战推演
  • 均匀面阵波束合成方向图的MATLAB仿真与关键参数影响分析
  • 微信DAT文件解码实战:免费开源工具开发与取证应用
  • Autosar架构下非发动机ECU的OBD II诊断实现:从UDS基础到法规遵从
  • C语言完美演绎3-14
  • 直流电流采样方案深度对比与选型指南
  • 马尔可夫决策过程(MDP)在强化学习中的核心作用与实战解析
  • Playwrite(Proxy和指纹库)
  • ANIMATEDIFF PRO商业应用:短视频平台智能封面生成
  • 企业级自动化新范式:开源RPA工具OpenRPA零基础到精通实战指南
  • Z-Image-Turbo-辉夜巫女开发者协作:Git同步Gradio配置+Xinference模型注册
  • 基于n8n与FastGPT构建智能客服系统的效率优化实践
  • Windows系统下MATLAB 2024b高效部署指南:从镜像获取到激活配置
  • 立创 CPSOe_Terminal:基于F1C100s/F1C200s与机械键盘的便携式Linux终端DIY全记录
  • Chord - Ink Shadow 环境配置详解:Anaconda虚拟环境管理最佳实践
  • 3步实现代理高效管理:ZeroOmega全场景应用指南
  • 在线考试app毕业设计:从零实现一个高可用防作弊系统(新手入门实战)
  • LightOnOCR-2-1B功能体验:支持数学公式识别的OCR工具实测
  • 真的太省时间!千笔·专业降AI率智能体,碾压级的降AI率平台
  • 彻底搞懂GeoJSON.io:重新定义地理数据处理的零门槛工具
  • 新手入门指南:在快马平台边学边练,轻松玩转狼蛛f87pro宏编程
  • 手把手教你用雪女-造相Z-Turbo:从部署到出图,新手也能快速画出斗罗大陆雪女
  • RetinaFace在教育教学中的应用:课堂专注度分析
  • 避坑指南:QMT对接聚宽策略常见的5个配置错误与解决方案(含Redis连接问题)
  • QGIS vs ArcGIS大比拼:栅格矢量化操作差异全解析(含SHP文件生成技巧)
  • GD32450i-EVAL IPA图像处理加速器避坑指南:背景层与前景层配置详解
  • TightVNC二次开发入门:从源码编译到第一个自定义功能实现