雅安市网站建设_网站建设公司_UI设计师_seo优化
2026/1/16 12:13:17 网站建设 项目流程

深入浅出:用VHDL构建通信协议的数据通路——以UART为例

你有没有遇到过这样的情况?在做VHDL课程设计大作业时,老师布置了一个“实现串口通信”的任务。你打开开发环境,手握语法手册,却不知道从何下手:数据怎么发?接收怎么同步?控制信号如何协调?模块之间又该怎么连接?

别急,这正是我们今天要解决的问题。

本文不讲空泛理论,也不堆砌术语,而是带你一步步拆解一个典型通信协议——UART 的数据通路结构,并用 VHDL 实际搭建出来。目标很明确:让你不仅“能写代码”,更“理解为什么这么写”。


为什么选UART?因为它是最适合入门的通信协议

在 FPGA 设计中,通信无处不在。SPI、I2C、Ethernet、USB……每一种都有其用途,但对初学者最友好的,非 UART 莫属。

它有三大优势:

  • 物理简单:只需要 TX(发送)和 RX(接收)两根线;
  • 异步传输:不需要共享时钟线,靠双方约定波特率即可工作;
  • 帧结构清晰:起始位 + 数据位 + 校验位 + 停止位,逻辑直观。

更重要的是,UART 是理解所有串行协议的基础。掌握了它的数据流动方式和控制机制,再学 SPI 或 I2C 就会轻松很多。

那我们在 FPGA 中实现 UART,核心是什么?

答案是:构建一条清晰的数据通路,并用状态机精确控制时序


数据通路的本质:数据往哪走?什么时候动?

先来思考一个问题:你在电脑上通过串口助手发送一个字符'A',它是怎么被 FPGA 接收到的?

这个过程可以分解为两个方向:

发送路径(FPGA → PC)

  1. CPU 或逻辑模块提供一个字节0x41(即'A');
  2. 并行输入到发送模块;
  3. 模块自动添加起始位、停止位,形成完整帧;
  4. 在波特率时钟驱动下,逐位移出,变成串行信号;
  5. 经 TX 引脚输出,传给 PC。

接收路径(PC → FPGA)

  1. 外部串行信号进入 RX 引脚;
  2. 接收模块检测到下降沿(起始位);
  3. 启动内部采样逻辑,通常采用 16 倍频采样确保准确;
  4. 完成一帧后,将 8 位数据合并成并行字节;
  5. 输出结果,并置位rx_valid通知主控读取。

整个过程中,数据流负责“搬运”,而控制流决定“何时启动”、“是否完成”、“要不要重试”。

✅ 关键洞察:优秀的 VHDL 设计一定是数据与控制分离的。混乱的耦合只会让代码难以调试、无法复用。


如何用VHDL组织模块?分而治之才是正道

面对复杂系统,人类大脑的处理能力有限。解决办法只有一个:模块化设计

我们可以把 UART 系统拆成几个独立功能块:

模块功能
baud_gen波特率发生器,产生精准的时间基准
uart_tx发送器,完成并串转换和帧封装
uart_rx接收器,实现位同步与串并转换
uart_top顶层整合,对外暴露接口

每个模块各司其职,通过标准接口连接。这种“搭积木”式的设计,既便于单独测试,也利于后期扩展。

比如你想改成双通道 UART?只需多例化一次收发模块即可。


波特率发生器:时间的节拍器

所有操作都依赖时间基准。在数字电路中,没有真正的“延时”,只有计数 + 分频

假设系统时钟为 50MHz,我们要生成 115200bps 的波特率,意味着每位持续时间为:

T_bit = 1 / 115200 ≈ 8.68μs

在这段时间内,50MHz 时钟会走:

Count = 50_000_000 × 8.68e-6 ≈ 434

所以,我们设计一个计数器,每累计到 434 次就输出一个使能脉冲enable,这个信号就作为发送/接收模块的动作触发条件。

下面是精简高效的baud_gen实现:

-- baud_gen.vhd library IEEE; use IEEE.STD_LOGIC_1164.ALL; use IEEE.NUMERIC_STD.ALL; entity baud_gen is generic ( CLK_FREQ : integer := 50_000_000; BAUD_RATE : integer := 115200 ); port ( clk : in std_logic; rst : in std_logic; enable : out std_logic ); end entity; architecture rtl of baud_gen is constant COUNT_MAX : integer := CLK_FREQ / BAUD_RATE; signal counter : integer range 0 to COUNT_MAX - 1 := 0; begin process(clk) begin if rising_edge(clk) then if rst = '1' then counter <= 0; enable <= '0'; elsif counter = COUNT_MAX - 1 then counter <= 0; enable <= '1'; -- 周期结束,发出使能 else counter <= counter + 1; enable <= '0'; end if; end if; end process; end architecture;

📌要点解析
- 使用generic参数化设计,换平台或改波特率只需修改参数;
- 计数达到阈值时输出单周期脉冲,避免持续高电平干扰;
- 所有操作都在时钟上升沿完成,符合同步设计规范;
- 不生成锁存器(条件全覆盖),综合安全。


发送模块:把并行数据变成串行波形

发送的核心任务是:把一个字节按顺序一位一位发出去,前后加上起始位和停止位

为此,我们使用一个有限状态机(FSM)来管理流程:

-- uart_tx.vhd(简化版) library IEEE; use IEEE.STD_LOGIC_1164.ALL; entity uart_tx is port ( clk : in std_logic; rst : in std_logic; data_in : in std_logic_vector(7 downto 0); start : in std_logic; tx_done : out std_logic; tx_out : out std_logic; enable : in std_logic -- 来自baud_gen ); end entity; architecture rtl of uart_tx is type state_t is (IDLE, START, DATA, STOP); signal state : state_t := IDLE; signal shift_reg : std_logic_vector(7 downto 0); signal bit_cnt : integer range 0 to 7 := 0; begin process(clk) begin if rising_edge(clk) then if rst = '1' then state <= IDLE; tx_out <= '1'; -- 空闲高电平 tx_done <= '0'; elsif enable = '1' then -- 只在波特率节拍动作 case state is when IDLE => tx_out <= '1'; tx_done <= '0'; if start = '1' then state <= START; end if; when START => tx_out <= '0'; -- 起始位:低电平 state <= DATA; shift_reg <= data_in; when DATA => tx_out <= shift_reg(0); if bit_cnt < 7 then bit_cnt <= bit_cnt + 1; shift_reg <= '0' & shift_reg(7 downto 1); else bit_cnt <= 0; state <= STOP; end if; when STOP => tx_out <= '1'; -- 停止位:高电平 tx_done <= '1'; state <= IDLE; end case; end if; end if; end process; end architecture;

🧠关键设计思想
- 利用enable控制状态跳转节奏,保证每一位持续一个波特周期;
- 移位寄存器右移发送低位(LSB first),符合 UART 规范;
-tx_done输出单周期脉冲,可用于中断或握手;
- 状态机结构清晰,易于扩展奇偶校验等功能。


接收模块:如何准确抓取外来信号?

相比发送,接收更具挑战性。因为外部信号可能带有噪声、抖动,甚至相位偏移。

通用做法是:16倍频采样。即每个数据位采样16次,取中间时刻的值作为判决依据,提高抗干扰能力。

不过为了教学清晰,我们先实现基础版本(1×采样),后续再谈优化。

-- uart_rx.vhd(基础版) library IEEE; use IEEE.STD_LOGIC_1164.ALL; entity uart_rx is port ( clk : in std_logic; rst : in std_logic; rx_in : in std_logic; data_out : out std_logic_vector(7 downto 0); valid : out std_logic; enable : in std_logic ); end entity; architecture rtl of uart_rx is type state_t is (IDLE, START, DATA, STOP); signal state : state_t := IDLE; signal shift_reg : std_logic_vector(7 downto 0) := (others => '0'); signal bit_cnt : integer range 0 to 7 := 0; begin process(clk) begin if rising_edge(clk) then if rst = '1' then state <= IDLE; valid <= '0'; elsif enable = '1' then case state is when IDLE => valid <= '0'; if rx_in = '0' then -- 检测起始位 state <= START; end if; when START => -- 确认是有效起始位(可加滤波) state <= DATA; bit_cnt <= 0; when DATA => shift_reg(bit_cnt) <= rx_in; if bit_cnt < 7 then bit_cnt <= bit_cnt + 1; else bit_cnt <= 0; state <= STOP; end if; when STOP => if rx_in = '1' then -- 验证停止位 data_out <= shift_reg; valid <= '1'; end if; state <= IDLE; end case; end if; end if; end process; end architecture;

🔧注意事项
- 起始位检测应增加去抖动处理(例如连续多个周期为低才算有效);
- 停止位必须为高,否则视为帧错误;
-valid是单周期脉冲,需由主控及时读取;
- 实际项目中建议加入 FIFO 缓冲,防止数据丢失。


顶层集成:拼图的最后一块

现在四个模块都齐了,接下来就是“组装”:

-- uart_top.vhd entity uart_top is port ( clk : in std_logic; rst : in std_logic; tx_data_in : in std_logic_vector(7 downto 0); tx_start : in std_logic; tx_done : out std_logic; rx_data_out : out std_logic_vector(7 downto 0); rx_valid : out std_logic; tx_pin : out std_logic; rx_pin : in std_logic ); end entity; architecture struct of uart_top is signal tx_enable, rx_enable : std_logic; begin -- 波特率发生器(收发独立) baud_tx_inst: entity work.baud_gen generic map(CLK_FREQ => 50_000_000, BAUD_RATE => 115200) port map(clk => clk, rst => rst, enable => tx_enable); baud_rx_inst: entity work.baud_gen generic map(CLK_FREQ => 50_000_000, BAUD_RATE => 115200) port map(clk => clk, rst => rst, enable => rx_enable); -- 发送模块 tx_inst: entity work.uart_tx port map( clk => clk, rst => rst, data_in => tx_data_in, start => tx_start, tx_done => tx_done, tx_out => tx_pin, enable => tx_enable ); -- 接收模块 rx_inst: entity work.uart_rx port map( clk => clk, rst => rst, rx_in => rx_pin, data_out => rx_data_out, valid => rx_valid, enable => rx_enable ); end architecture;

🎯设计亮点
- 收发使用各自的enable信号,互不干扰;
- 顶层仅负责连线,逻辑干净简洁;
- 易于替换不同速率或添加多路实例;
- 完全适合作为VHDL课程设计大作业的标准模板


实战建议:如何写出高质量的课程设计代码?

如果你正在准备答辩或提交作业,以下几点能帮你脱颖而出:

1. 先仿真,再下载

别急着烧板子!用 ModelSim 写个简单的 testbench 验证发送接收功能:

-- testbench 示例片段 stim_proc: process begin rst <= '1'; wait for 100ns; rst <= '0'; -- 测试发送 tx_data_in <= "01000001"; -- 'A' tx_start <= '1'; wait for 100ns; tx_start <= '0'; wait; end process;

看波形是否符合预期:起始位→A的二进制→停止位。

2. 添加注释和框图

一份优秀的报告要有:
- 模块层级图(可用Visio或Draw.io绘制)
- 关键信号说明表
- 状态机转换图
- 注释规范如:-- [OUT] tx_done: 单周期脉冲,表示发送完成

3. 命名规范统一

  • 输入信号:xxx_in,i_xxx
  • 输出信号:xxx_out,o_xxx
  • 内部信号:sig_xxx
  • 低电平有效:加_n后缀,如reset_n

4. 加入容错机制(加分项)

  • 接收端增加起始位确认(连续采样3次为低才认为有效)
  • 支持可变数据位宽(通过 generic 控制)
  • 添加奇偶校验生成功能

总结:掌握数据通路,你就掌握了FPGA通信的灵魂

回顾一下,我们做了什么?

  • 从零开始分析 UART 协议的行为特征;
  • 提炼出“数据通路 + 控制逻辑”的基本模型;
  • 用 VHDL 实现了波特率发生器、发送器、接收器;
  • 在顶层完成模块整合;
  • 给出了仿真、命名、文档等实战技巧。

这套方法论不仅可以用于 UART,还能迁移到 SPI、I2C 乃至自定义协议的设计中。

当你下次接到“做一个带通信功能的数据采集系统”这类题目时,你会知道:

“哦,不过是加个 ADC 模块,把采样结果交给 uart_tx 发出去而已。”

这才是真正的工程思维。


如果你在实现过程中遇到了其他挑战,欢迎在评论区分享讨论。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询