☰
Verilog并行与串行语句的本质区别:FPGA开发避坑指南
2026/10/12 10:54:49 网站建设 项目流程

1. 从一个反直觉的仿真结果说起

刚接触FPGA那会儿,我写过一个特别简单的模块:两个计数器,一个在时钟上升沿加一,另一个在时钟下降沿加一,然后我把它们的值相减,想看看差值是不是稳定在某个范围。结果仿真波形出来,我盯着屏幕看了半天——两个计数器确实都在动,但那个减法器的输出每隔半个周期就跳一次,而且跳得毫无规律。我当时的第一反应是“仿真器坏了”,第二反应是“我是不是哪里写错了”。

后来把代码翻来覆去看了十几遍,才意识到问题根本不在语法上,而在于我对Verilog里“并行”和“串行”这两个概念的理解一直是模糊的。我脑子里默认代码是从上往下执行的,但实际上Verilog描述的是硬件,硬件里所有东西都在同时工作。那个减法器的两个输入随时都在变,输出当然也跟着乱跳。这件事让我明白了一个道理:在FPGA开发里,如果你用软件的顺序思维去理解Verilog,迟早会被仿真结果教做人。

这篇内容就是围绕这个核心问题展开的。我会把Verilog中并行语句和串行语句的区别、各自的使用场景、常见的踩坑点,以及怎么在实际项目里做出正确的选择,从头到尾捋一遍。不管你是刚学Verilog没多久,还是已经写过一些模块但总觉得仿真和预期对不上,这篇应该都能帮你把一些模糊的地方理清楚。

2. 并行与串行的本质区别

2.1 硬件描述语言不是编程语言

很多人学Verilog的时候会带着C语言的思维惯性,觉得代码就是一条一条执行的。这个想法在写testbench的时候可能还能凑合,但在写综合模块的时候,它会让你犯各种莫名其妙的错误。

Verilog的本质是硬件描述语言,你写的每一行代码最终都要映射到实际的电路上。电路里的门、触发器、连线,它们没有“先后执行”的概念,只有“同时存在”和“信号传播”的概念。所以Verilog里的并行语句,描述的是同时存在的硬件结构;而串行语句,描述的是同一个硬件结构在不同时间点上的行为。

打个比方:并行语句就像是一张建筑图纸,上面画了墙、门、窗户,它们同时存在于那个空间里;串行语句则像是一份施工流程,告诉你先砌墙、再装门、最后装窗户。图纸描述的是最终结构,流程描述的是建造过程。两者用途完全不同,但很多人会把它们混在一起用。

2.2 并行语句:描述同时发生的硬件

Verilog里最常见的并行语句包括:

  • 连续赋值语句(assign)
  • 实例化语句(模块例化、门级原语例化)
  • always块和initial块本身(多个块之间是并行的)
  • generate块

这些语句有一个共同特点:它们在仿真开始时就全部“同时”生效,不存在谁先谁后的问题。比如你写了两条assign语句:

assign a = b & c; assign d = b | c;

这两条语句描述的是两个独立的逻辑门,它们同时从b和c取信号,同时输出结果。你如果把这两条语句的顺序调换一下,综合出来的电路完全一样,仿真结果也完全一样。

再比如模块例化:

adder u1 (.a(x), .b(y), .sum(s1)); adder u2 (.a(s1), .b(z), .sum(s2));

这里u1和u2是两个独立的加法器实例,它们同时存在于电路中。虽然u2的输入依赖u1的输出,但这不意味着u1先执行、u2后执行——它们只是通过连线连接在一起,信号从u1的输出端传播到u2的输入端,这是一个物理传播过程,不是软件里的函数调用。

2.3 串行语句:描述同一硬件的时间行为

串行语句主要出现在always块和initial块内部。在一个always块里,语句是按顺序执行的,但这个“顺序执行”和软件里的顺序执行有本质区别。

always @(posedge clk) begin a = b; c = a; end

这段代码在仿真器里的执行过程是:当时钟上升沿到来时,先把b的值赋给a,然后把a的值赋给c。因为a已经被更新了,所以c拿到的是b的值。最终效果是a和c都等于b。

但如果换成非阻塞赋值:

always @(posedge clk) begin a <= b; c <= a; end

这时候两条语句虽然写在同一个块里,但它们的赋值是在同一个时间步的末尾同时完成的。c拿到的是a在时钟沿之前的旧值,而不是b的新值。这就是非阻塞赋值和阻塞赋值的核心区别,也是串行语句里最容易踩坑的地方。

注意:在时序逻辑的always块里,永远使用非阻塞赋值(<=);在组合逻辑的always块里,永远使用阻塞赋值(=)。这条规则不是建议,是铁律。违反它可能不会立刻报错,但会在某个你意想不到的时刻让你debug到怀疑人生。

3. 并行语句的实战用法与避坑指南

3.1 连续赋值语句的正确打开方式

assign语句是Verilog里最纯粹的并行语句,它描述的是一个持续驱动的组合逻辑关系。左边必须是wire类型,右边可以是任意表达式。

wire [7:0] sum; wire carry; assign {carry, sum} = a + b;

这段代码描述的是一个8位加法器,carry和sum同时由a和b决定。只要a或b发生变化,sum和carry就会在传播延迟后更新。这里没有时钟,没有触发条件,就是纯粹的组合逻辑。

用assign的时候有几个常见的坑:

第一个坑是忘记左边必须是wire。很多人习惯性地把输出定义成reg,然后写assign,综合工具会直接报错。reg类型只能在always块里赋值,assign的左边必须是wire。

第二个坑是组合逻辑环路。比如:

assign a = b & c; assign b = a | d;

这就形成了一个环路,a依赖b,b又依赖a。综合工具可能会报错,也可能综合出一个不稳定的振荡电路。仿真的时候你会看到信号在0和1之间疯狂跳变,或者直接变成x。

第三个坑是位宽不匹配。Verilog在做赋值的时候会自动截断或扩展,但截断的方向和扩展的方式可能和你想的不一样。比如把一个16位的值赋给8位的wire,它会保留低8位,高8位直接丢掉。如果你没注意到这一点,仿真结果可能和预期差很远。

3.2 模块例化:并行结构的核心

模块例化是FPGA设计里最常用的并行语句。你可以在一个顶层模块里例化多个子模块,它们全部同时工作。

module top ( input wire clk, input wire rst_n, input wire [7:0] data_in, output wire [7:0] data_out ); wire [7:0] stage1_out; wire [7:0] stage2_out; stage1 u_stage1 ( .clk (clk), .rst_n (rst_n), .din (data_in), .dout (stage1_out) ); stage2 u_stage2 ( .clk (clk), .rst_n (rst_n), .din (stage1_out), .dout (stage2_out) ); assign data_out = stage2_out; endmodule

这里u_stage1和u_stage2是两个独立的模块实例,它们同时存在于电路中。数据从data_in进入u_stage1,经过一级处理后传给u_stage2,再经过一级处理输出。虽然数据流是串行的,但两个模块本身是并行工作的——u_stage1在处理当前数据的同时,u_stage2可能在处理上一拍的数据。

例化的时候有几个细节值得注意:

  • 端口连接方式:推荐使用命名连接(.port_name(signal_name)),而不是位置连接。命名连接更清晰,也不容易因为端口顺序变化而出错。
  • 未连接端口:如果某个输出端口不需要,可以留空不连,但输入端口如果悬空,仿真时会变成z,综合时可能被优化掉,导致功能异常。
  • 例化名不能重复:同一个模块在同一个层次里例化多次,每个实例的名字必须唯一。

3.3 generate块:批量例化的利器

当你需要例化大量结构相似的模块时,generate块可以帮你省很多事。

genvar i; generate for (i = 0; i < 8; i = i + 1) begin : gen_adder adder u_adder ( .a (a[i]), .b (b[i]), .cin (carry[i]), .sum (sum[i]), .cout (carry[i+1]) ); end endgenerate

这段代码会生成8个加法器实例,形成一个8位的行波进位加法器。generate块里的for循环在综合时会被展开,每个迭代生成一个独立的硬件实例。注意genvar是专门用于generate循环的变量类型,它只在综合时存在,仿真时不会出现在波形里。

用generate的时候,begin后面必须跟一个标签(比如上面的gen_adder),否则有些工具会报错。另外,generate块里的条件判断(if、case)也是在综合时求值的,不能包含运行时才确定的信号。

4. 串行语句的深层逻辑与常见误区

4.1 always块的本质:敏感列表决定一切

always块是Verilog里最灵活也最容易出问题的结构。它的行为完全由敏感列表决定。

// 组合逻辑 always @(*) begin y = a & b; end // 时序逻辑 always @(posedge clk) begin q <= d; end

第一个always块的敏感列表是*,表示只要a或b发生变化,块内的语句就会执行。这描述的是一个组合逻辑门。第二个always块只在时钟上升沿执行,描述的是一个D触发器。

很多人会犯的一个错误是在组合逻辑的always块里使用非阻塞赋值:

always @(*) begin y <= a & b; // 不推荐 end

虽然仿真结果可能看起来是对的,但综合工具可能会推断出锁存器,或者在某些情况下产生和预期不一致的行为。组合逻辑就用阻塞赋值,时序逻辑就用非阻塞赋值,这条规则没有例外。

4.2 阻塞赋值与非阻塞赋值的本质区别

这是Verilog里被讨论最多的话题之一,但很多人只是记住了“时序用非阻塞、组合用阻塞”这个结论,并不清楚背后的原因。

阻塞赋值(=)的行为类似于软件里的变量赋值:立即执行,执行完之后再执行下一条语句。在同一个always块里,后面的语句能看到前面语句的赋值结果。

非阻塞赋值(<=)的行为是:在时间步开始时计算右边表达式的值,但在时间步结束时才更新左边的变量。在同一个always块里,所有非阻塞赋值同时更新,后面的语句看不到前面语句的更新结果。

用一个例子来说明:

// 阻塞赋值 always @(posedge clk) begin a = b; c = a; end // 结果:a = b, c = b // 非阻塞赋值 always @(posedge clk) begin a <= b; c <= a; end // 结果:a = b, c = a的旧值

在时序逻辑里,我们描述的是触发器。触发器的行为是:在时钟沿到来时,所有触发器的输出同时更新,每个触发器看到的是其他触发器在时钟沿之前的旧值。非阻塞赋值的语义正好和这个行为一致。如果用阻塞赋值,综合出来的电路可能和预期一致,也可能不一致,取决于综合工具的优化策略。为了代码的可移植性和可预测性,时序逻辑必须用非阻塞赋值。

4.3 串行语句里的“并行”陷阱

即使在一个always块里,如果你不小心,也可能写出实际并行但看起来串行的代码。

always @(posedge clk) begin if (en) begin data <= data_in; end valid <= en; end

这段代码里,data和valid的更新是同时发生的。valid在下一个时钟沿变成en的值,而data在同一个时钟沿更新。如果你期望valid比data晚一拍,这段代码就不符合预期。正确的写法应该是:

always @(posedge clk) begin if (en) begin data <= data_in; valid <= 1'b1; end else begin valid <= 1'b0; end end

这样data和valid在同一个时钟沿更新,valid有效时data也有效。

实操心得:在写时序逻辑的时候,我习惯把同一个时钟沿更新的所有信号放在同一个if-else结构里,这样能保证它们的更新条件完全一致。如果某个信号需要延迟一拍,我会显式地用一个额外的寄存器来打拍,而不是依赖语句顺序来实现延迟。

5. 并行与串行的混合使用场景

5.1 流水线设计:并行结构+串行控制

流水线是FPGA设计里最经典的并行与串行混合场景。每一级流水线是一个独立的模块实例(并行),但每一级内部的数据处理是时序逻辑(串行)。

module pipeline ( input wire clk, input wire rst_n, input wire [7:0] din, output wire [7:0] dout ); wire [7:0] stage1_out; wire [7:0] stage2_out; wire [7:0] stage3_out; // 第一级:加法 always @(posedge clk or negedge rst_n) begin if (!rst_n) stage1_out <= 8'd0; else stage1_out <= din + 8'd1; end // 第二级:移位 always @(posedge clk or negedge rst_n) begin if (!rst_n) stage2_out <= 8'd0; else stage2_out <= {stage1_out[6:0], stage1_out[7]}; end // 第三级:异或 always @(posedge clk or negedge rst_n) begin if (!rst_n) stage3_out <= 8'd0; else stage3_out <= stage2_out ^ 8'hA5; end assign dout = stage3_out; endmodule

三个always块是并行的,它们同时工作。但每个块内部是串行的时序逻辑,数据在每个时钟沿向前推进一级。这种结构的好处是每一级可以独立优化,整体吞吐率取决于最慢的那一级。

5.2 状态机:串行逻辑的典型应用

状态机是串行逻辑最典型的应用场景。状态转移是串行的——当前状态决定下一个状态,下一个状态又决定再下一个状态。

localparam IDLE = 2'd0; localparam WORK = 2'd1; localparam DONE = 2'd2; reg [1:0] state, next_state; always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= IDLE; else state <= next_state; end always @(*) begin case (state) IDLE: next_state = start ? WORK : IDLE; WORK: next_state = finish ? DONE : WORK; DONE: next_state = IDLE; default: next_state = IDLE; endcase end

这里第一个always块是时序逻辑,描述状态寄存器的更新;第二个always块是组合逻辑,描述状态转移条件。两个块是并行的,但状态转移本身是串行的。

写状态机的时候,我强烈建议把状态寄存器和组合逻辑分开写。这样代码更清晰,也更容易调试。如果把两者混在一个always块里,虽然也能工作,但一旦状态机复杂起来,维护成本会急剧上升。

5.3 跨时钟域处理:并行信号的同步

跨时钟域是FPGA设计里另一个并行与串行交织的场景。两个时钟域是并行工作的,但信号从一个时钟域传到另一个时钟域时,需要同步处理。

// 时钟域A always @(posedge clk_a) begin data_a <= data_in; valid_a <= valid_in; end // 时钟域B:两级同步器 reg valid_a_sync1, valid_a_sync2; always @(posedge clk_b) begin valid_a_sync1 <= valid_a; valid_a_sync2 <= valid_a_sync1; end // 时钟域B:数据采样 always @(posedge clk_b) begin if (valid_a_sync2) data_b <= data_a; end

这里clk_a和clk_b是两个独立的时钟,它们并行工作。valid_a通过两级同步器传到clk_b域,data_a在valid_a_sync2有效时被采样。这种结构在跨时钟域通信里非常常见,但有几个细节需要注意:

  • 同步器级数:一般用两级就够了,但在高频设计里可能需要三级。
  • 数据稳定性:data_a必须在valid_a有效期间保持稳定,否则采样到的数据可能不对。
  • 亚稳态:同步器的第一级输出可能进入亚稳态,但经过第二级后基本稳定。

6. 常见问题与排查技巧实录

6.1 仿真结果和预期不一致的排查思路

当你发现仿真结果和预期不一致时,可以按以下顺序排查:

排查步骤检查内容常见问题
1敏感列表是否完整组合逻辑漏了信号,导致仿真和综合不一致
2赋值方式是否正确时序逻辑用了阻塞赋值,组合逻辑用了非阻塞赋值
3位宽是否匹配隐式截断或扩展导致数据错误
4时钟沿是否选对上升沿和下降沿混用,导致时序混乱
5复位是否生效复位极性写反,或者复位没连到所有寄存器
6组合逻辑是否有环路信号互相依赖,导致振荡或x态

这个表是我自己debug时常用的检查清单,基本上80%的问题都能在前三步找到原因。

6.2 综合结果和仿真不一致的典型原因

仿真通过但上板不工作,这是FPGA开发里最让人头疼的问题之一。常见原因包括:

第一个原因是仿真模型和综合模型不一致。有些行为在仿真时有效,但综合工具不支持。比如在always块里使用#delay,仿真时能看到延迟效果,但综合工具会直接忽略。

第二个原因是时序约束不完整。仿真时没有时序信息,所有信号都是理想传播。但实际电路里信号有延迟,如果时钟频率太高或者路径太长,建立时间和保持时间可能不满足。

第三个原因是跨时钟域处理不当。仿真时两个时钟的相位关系是确定的,但实际电路中两个时钟的相位关系是随机的。如果跨时钟域信号没有同步处理,上板后可能出现亚稳态。

第四个原因是复位策略不一致。仿真时复位信号可能只在开始有效,但实际电路中复位信号可能受到毛刺影响。建议使用同步复位或者对异步复位做同步释放处理。

6.3 几个我踩过的坑

坑一:在组合逻辑里用非阻塞赋值。有一次我写了一个译码器,在always @(*)块里用了<=。仿真结果看起来是对的,但综合工具推断出了一个锁存器,导致上板后输出在某个输入组合下保持上一个值不变。后来改成阻塞赋值就正常了。

坑二:敏感列表漏信号。早期写组合逻辑的时候,我习惯手动列敏感列表,结果漏了一个信号。仿真时因为测试激励恰好覆盖了那个信号的变化,所以没发现问题。但上板后在某些输入组合下输出不对。后来全部改用always @(*),这个问题就再也没出现过。

坑三:在同一个always块里混用阻塞和非阻塞赋值。有一次我在一个时序always块里,前面用非阻塞赋值更新寄存器,后面用阻塞赋值更新一个中间变量。仿真结果看起来没问题,但综合工具报了一个警告,说可能存在竞争条件。后来我把中间变量改成wire类型,用assign单独赋值,问题就解决了。

坑四:generate块里的循环变量用错类型。有一次我用integer作为generate循环的变量,综合工具直接报错。后来改成genvar就正常了。genvar是专门为generate循环设计的,它只在综合时存在,不会出现在仿真波形里。

7. 一些实用的编码习惯

7.1 模块划分的粒度

模块划分的粒度直接影响代码的可读性和可维护性。我的经验是:

  • 功能独立的逻辑尽量拆成单独模块。比如一个FIFO、一个CRC校验、一个状态机,都应该是独立的模块。
  • 模块的端口不要太多。如果一个模块的端口超过20个,考虑是不是可以把一些相关信号打包成总线。
  • 顶层模块只做例化和连线。顶层模块里尽量不要写逻辑,只负责把各个子模块连接起来。

7.2 命名规范

好的命名能让代码自解释。我常用的命名习惯:

  • 时钟信号:clk_前缀,比如clk_sys、clk_100m
  • 复位信号:rst_前缀,低电平有效加_n后缀,比如rst_n、rst_sys_n
  • 低电平有效信号:_n后缀,比如cs_n、we_n
  • 寄存器输出:_r后缀,比如data_r、valid_r
  • 组合逻辑输出:_c后缀,比如data_c、valid_c

7.3 注释的写法

注释不是越多越好,关键是写在对的地方。我通常在以下位置写注释:

  • 模块头部:说明模块功能、端口含义、时序特性
  • 状态机状态定义:说明每个状态的含义和转移条件
  • 复杂的组合逻辑:说明逻辑表达式的含义
  • 跨时钟域处理:说明同步策略和注意事项

实操心得:我习惯在写代码之前先用注释把模块的框架搭出来,包括端口定义、主要寄存器、状态机状态。这样写代码的时候思路更清晰,也不容易漏掉东西。

8. 从仿真到上板的完整验证流程

8.1 仿真验证的三个层次

仿真不是跑一次就完事了,我一般分三个层次来验证:

第一层是功能仿真。用简单的测试激励验证模块的基本功能。比如一个加法器,输入几组数,看输出对不对。这一层主要检查逻辑是否正确。

第二层是边界仿真。测试边界条件,比如最大值、最小值、零值、溢出情况。这一层主要检查模块在极端情况下的行为。

第三层是随机仿真。用随机激励跑大量测试用例,覆盖各种可能的输入组合。这一层主要检查模块的鲁棒性。

8.2 时序约束的基本写法

时序约束是保证设计能在目标频率下工作的关键。最基本的约束包括时钟约束和输入输出延迟约束。

# 时钟约束 create_clock -name clk -period 10 [get_ports clk] # 输入延迟约束 set_input_delay -clock clk -max 2 [get_ports data_in] set_input_delay -clock clk -min 1 [get_ports data_in] # 输出延迟约束 set_output_delay -clock clk -max 3 [get_ports data_out] set_output_delay -clock clk -min 1 [get_ports data_out]

时钟周期是10ns,对应100MHz。输入延迟和输出延迟根据外部器件的时序参数来确定。这些约束告诉综合工具信号在时钟沿之前和之后需要保持稳定的时间窗口。

8.3 上板调试的基本方法

上板调试最常用的工具是逻辑分析仪。通过JTAG接口把内部信号引出来,在真实硬件上观察信号行为。

我一般会先抓几个关键信号:时钟、复位、状态机的状态、主要数据通路。先确认时钟和复位正常,再看状态机是否按预期转移,最后看数据通路是否正常。

如果发现某个信号不对,可以逐级往前排查。比如输出不对,先看最后一级寄存器的输入对不对;如果输入不对,再看前一级的输出。这样一层一层往前推,很快就能定位到问题所在。

9. 并行与串行的选择原则

9.1 什么时候用并行语句

并行语句适合描述以下场景:

  • 独立的硬件结构:多个模块实例、多个逻辑门,它们同时存在,互不依赖。
  • 纯组合逻辑:用assign描述的组合逻辑,输出只取决于当前输入。
  • 批量例化:用generate批量生成结构相似的硬件。

并行语句的核心优势是直观。你写出来的代码结构和硬件结构一一对应,综合结果容易预测。

9.2 什么时候用串行语句

串行语句适合描述以下场景:

  • 时序逻辑:寄存器更新、状态机转移、计数器计数。
  • 有条件的赋值:根据不同的条件给同一个信号赋不同的值。
  • 复杂的组合逻辑:用always @(*)描述比用assign更清晰。

串行语句的核心优势是灵活。你可以用if-else、case等结构描述复杂的逻辑关系,代码更紧凑。

9.3 混合使用的注意事项

并行和串行混合使用时,有几个原则需要遵守:

  • 一个信号只能在一个地方赋值。要么在assign里赋值,要么在一个always块里赋值,不能同时在两个地方赋值。
  • 组合逻辑和时序逻辑分开写。不要在一个always块里既写组合逻辑又写时序逻辑。
  • 跨模块的信号用wire连接。模块的输出端口连接到wire,wire再连接到其他模块的输入端口。

10. 一个完整的例子:UART发送模块

最后用一个完整的UART发送模块来把前面讲的内容串起来。

module uart_tx ( input wire clk, input wire rst_n, input wire tx_start, input wire [7:0] tx_data, output reg tx, output reg tx_busy ); localparam IDLE = 2'd0; localparam START = 2'd1; localparam DATA = 2'd2; localparam STOP = 2'd3; reg [1:0] state; reg [2:0] bit_cnt; reg [7:0] data_r; reg [15:0] baud_cnt; localparam BAUD_DIV = 16'd104; // 假设时钟100MHz,波特率9600 // 状态机:时序逻辑 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; bit_cnt <= 3'd0; data_r <= 8'd0; baud_cnt <= 16'd0; tx <= 1'b1; tx_busy <= 1'b0; end else begin case (state) IDLE: begin tx <= 1'b1; tx_busy <= 1'b0; if (tx_start) begin state <= START; data_r <= tx_data; baud_cnt <= 16'd0; tx_busy <= 1'b1; end end START: begin tx <= 1'b0; if (baud_cnt == BAUD_DIV - 1) begin baud_cnt <= 16'd0; state <= DATA; bit_cnt <= 3'd0; end else begin baud_cnt <= baud_cnt + 16'd1; end end DATA: begin tx <= data_r[bit_cnt]; if (baud_cnt == BAUD_DIV - 1) begin baud_cnt <= 16'd0; if (bit_cnt == 3'd7) begin state <= STOP; end else begin bit_cnt <= bit_cnt + 3'd1; end end else begin baud_cnt <= baud_cnt + 16'd1; end end STOP: begin tx <= 1'b1; if (baud_cnt == BAUD_DIV - 1) begin baud_cnt <= 16'd0; state <= IDLE; end else begin baud_cnt <= baud_cnt + 16'd1; end end default: state <= IDLE; endcase end end endmodule

这个模块里,状态机是串行逻辑,描述发送的各个阶段;tx和tx_busy是时序逻辑,在每个时钟沿更新;波特率计数器是串行逻辑,控制每个比特的持续时间。整个模块只有一个always块,所有寄存器在同一个时钟沿更新,逻辑清晰,不容易出错。

这个模块的测试也很简单:给一个tx_start脉冲,然后观察tx引脚是否按UART协议输出数据。波特率是9600,时钟是100MHz,分频系数是100000000/9600≈10416,但16位计数器最大只能到65535,所以实际分频系数需要根据时钟频率调整。如果时钟是100MHz,分频系数应该是10416,但16位寄存器放不下,需要用更大的计数器或者降低时钟频率。

注意:上面代码里的BAUD_DIV设为104只是为了示例,实际使用时需要根据时钟频率和波特率重新计算。分频系数的计算公式是:BAUD_DIV = 时钟频率 / 波特率。比如100MHz时钟、9600波特率,分频系数是10416,需要至少14位的计数器。

这个模块后续还可以扩展:增加奇偶校验位、支持可配置的波特率、增加发送FIFO等。但核心的状态机结构和时序逻辑写法是不变的。把并行和串行的关系理清楚之后,你会发现不管模块多复杂,拆解开来都是这些基本结构的组合。

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

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

立即咨询