你调过UART的波特率,配置过SPI的时钟极性,也给I2C设过从机地址。但如果面试官问“起始位是怎么被采样的”“ACK/NACK在硬件里怎么实现”,你能讲清楚吗?
大多数FPGA工程师的通信协议经验,停留在“配置IP核”的阶段。
打开Vivado的Block Design,拖一个UART IP进去,设一下波特率,连上AXI总线,生成比特流。能用,但你不知道里面发生了什么。
GitHub上有一个项目,用VHDL从零手写了UART、SPI、I2C三大通信协议,在Zynq-7020上跑通,并且用一块几十块钱的Arduino Uno做了对端验证。

01 它到底做了什么?
项目的目标很明确:不调用任何通信IP核,用纯VHDL状态机实现三大协议,并通过与Arduino Uno的实际通信验证功能正确性。
仓库结构非常清晰,分三个目录:Arduino/(Arduino端的测试代码)、VHDL/(Zynq-7020上的VHDL源码)、Vitis/(ARM处理器的C代码)。这意味着你拿到的不是一堆散落的RTL文件,而是一个完整的、可复现的工程——FPGA端和MCU端的代码都在里面。
三大协议各自独立实现,每一个都是从状态机开始,一行一行写出来的。
02 UART:从波特率生成到采样时序
UART是最基础的异步串行协议,也是最容易“调通就行、深究就懵”的协议。
项目的UART实现基于125MHz主时钟,通过分频器生成9600Hz的采样时钟,用于接收端的过采样和中心采样。接收端从rx引脚接收串行数据,转换成8位并行输出到LED显示;发送端在按键触发后,发送ASCII码‘A’(01000001)。
核心模块拆解:
时钟分频器(Clk_div.vhd) ——把125MHz主时钟分频到9600Hz。分频比的计算公式是:分频数 = 主时钟频率 / (16 × 波特率)。在100MHz时钟下跑9600波特率,分频数是651。为什么乘16?因为接收端需要16倍过采样——在每个数据位的中心位置采样,而不是在位边缘。这样可以有效抵抗时钟抖动和噪声。
UART接收器(UART_rx.vhd) ——一个有限状态机,从IDLE状态开始,检测到起始位(低电平)后进入数据接收状态,在每个数据位的中心采样,接收完8位数据后检查停止位。
UART发送器(UART_tx.vhd) ——同样是一个状态机,从IDLE开始,依次发送起始位、8位数据(LSB先发)、停止位。
边沿检测模块(Edge_detection.vhd) ——用于按键消抖,检测按键的上升沿,避免一次按键触发多次发送。

03 SPI:时钟极性与相位的配置逻辑
SPI是同步串行协议,比UART多了一个时钟线。但也正因为有时钟线,SPI的时钟极性(CPOL)和时钟相位(CPHA) 有了四种组合模式,这是最容易让初学者困惑的地方。
项目的SPI实现在VHDL中从零构建了主机控制器,包含时钟生成、片选控制和数据移位三个核心部分。SPI的时钟由FPGA内部生成,通过分频得到合适的SCL频率。数据在MOSI上逐位输出,在MISO上逐位采样。
关键设计点:
SPI主机需要精确控制数据建立时间和保持时间。项目通过状态机在SCL的上升沿或下降沿(取决于CPHA配置)采样MISO,同时更新MOSI。这种“边沿采样、边沿更新”的机制,确保了数据在传输过程中不会发生竞争冒险。
04 I2C:开漏输出与ACK/NACK的硬件实现
I2C是三大协议中最复杂的,因为它只有两根线(SDA和SCL),而且SDA是双向开漏信号。
开漏意味着:你不能主动把SDA拉高,只能把它拉低或释放(高阻态) 。高电平依靠外部上拉电阻维持。这在VHDL中需要用到std_logic的‘Z’状态(高阻态)。
项目的I2C实现包含一个集中式有限状态机,状态包括:IDLE、START、READ、WRITE、ACK、STOP。
ACK/NACK的硬件逻辑: 主机发送完8位数据后,释放SDA线(设为‘Z’),在第9个SCL时钟周期采样SDA。如果从机拉低了SDA,表示ACK;如果SDA保持高电平,表示NACK。这个逻辑在状态机中由“ACK采样状态”实现。
时钟同步与仲裁: I2C支持多主设备,当两个主机同时发起传输时,需要通过时钟同步和仲裁决定谁先占用总线。虽然入门项目通常只实现单主模式,但理解这个机制对读懂I2C协议至关重要。

05 为什么用Arduino做对端测试?
这是这个项目最聪明的地方。
通信协议的验证通常需要示波器或协议分析仪——一台入门级示波器也要几千块,协议分析仪更贵。这个项目用一块几十块钱的Arduino Uno作为对端设备,通过串口打印或LED指示来验证通信是否成功。
Arduino端的代码在Arduino/目录中,实现了与Zynq-7020的UART、SPI和I2C通信。你不需要任何专业仪器,只需要一根杜邦线把FPGA和Arduino连起来,就能验证你的协议实现是否正确。
这种方法还有一个额外的好处:你同时在学两种平台的通信实现。Arduino的Wire库(I2C)和SPI库是软件实现,而你的FPGA是硬件实现。对比两者的差异,能让你更深入地理解协议本身。
06 学习路径:从UART开始,逐步进阶
第一步:UART。 从时钟分频器和接收器开始。先让接收器能正确解析Arduino发来的数据,并显示在LED上。然后实现发送器,用按键触发发送‘A’到Arduino。这一步你会学到:波特率计算、16倍过采样、状态机设计、边沿检测。
第二步:SPI。 实现SPI主机模式,配置CPOL和CPHA。让Zynq作为主机,Arduino作为从机。这一步你会学到:同步时钟生成、数据移位、片选控制、时序约束。
第三步:I2C。 实现I2C主机控制器,处理START/STOP条件、7位地址、ACK/NACK。这一步你会学到:开漏输出、三态逻辑、状态机分层设计。
第四步:上板验证。 把三个协议分别综合到Zynq-7020的PL端,用Arduino做对端测试。这一步你会学到:引脚约束、时序收敛、板级调试。
07 简历怎么写?面试能讲什么?
简历写法示例:
“基于Zynq-7020 FPGA,用VHDL从零实现了UART、SPI、I2C三大通信协议的硬件控制器。通过与Arduino Uno的实际通信验证了协议时序和数据帧格式的正确性。深入理解了16倍过采样、CPOL/CPHA配置、开漏输出与ACK/NACK机制等协议底层细节。”
面试能讲什么(选一个方向,能讲20分钟):
方向A:UART的采样时序。 为什么要用16倍过采样而不是1倍?采样点为什么选在数据位的中心?波特率误差容限怎么计算?
方向B:SPI的四种模式。 CPOL=0和CPOL=1有什么区别?CPHA=0和CPHA=1分别在哪个边沿采样?如果主从设备的模式不匹配,会出现什么现象?
方向C:I2C的仲裁与时钟同步。 开漏输出为什么需要上拉电阻?两个主机同时拉低SDA时,怎么判断谁赢了仲裁?时钟同步是怎么实现的?

这个项目不是一个“跑通就行”的Demo。它的价值在于:它把通信协议从“黑盒”变成了“白盒”。
你不再只是调一个IP核、设几个参数。你从状态机的第一个状态开始,一行一行地写,一个周期一个周期地调。当Arduino收到你发送的字符,当你看到LED正确显示接收到的数据——你对协议的理解,就不再是“我知道怎么配置”,而是“我知道它为什么能工作”。
项目地址:github.com/CosminGorun/UART-SPI-I2C-Communication-Protocols-VHDL-Zynq_7020
关于成电国芯
成电国芯FPGA课程已升级通信协议硬件实现方向,覆盖UART/SPI/I2C状态机设计、时序分析与约束、板级调试等企业级实战内容。从协议原理到上板验证,每一步都有导师带练。想系统掌握FPGA通信协议开发能力,欢迎联系我们了解课程详情。

评论 0