4. Verilog基础语法(一):模块结构、端口定义、数据类型、赋值语句

各位同学,今天我们来聊聊Verilog最基础的东西。说实话,很多初学者一上来就被各种语法吓住了,其实没那么复杂。你想想看,Verilog说白了就是描述硬件的一种语言,跟C语言写软件完全是两码事。我刚开始学的时候也犯过糊涂,总觉得这玩意儿怎么这么别扭——后来才明白,是因为脑子里还带着软件的思维。

4.1 模块结构——CPLD设计的“骨架”

在Verilog里,模块(module)是最基本的设计单元。你可以把它想象成一个黑盒子,有输入有输出,里面装着你要实现的逻辑。每个CPLD设计,本质上就是由一个个模块拼起来的。

一个完整的模块长这样:

module 模块名 (
    端口列表
);
    // 端口声明
    // 内部信号声明
    // 逻辑功能描述
endmodule

嗯,这里要注意:moduleendmodule必须成对出现,就像括号一样,少一个编译器就给你报错。我在项目中遇到过好几次,写完一大段代码发现编译不过,最后检查半天——原来是endmodule漏了。这种低级错误,新手尤其容易犯。

4.2 端口定义——模块的“手脚”

端口就是模块跟外界打交道的通道。Verilog里端口分三种:

端口类型 关键字 方向 我常用的场景
输入端口 input 只能读,不能写 按键、时钟、复位信号
输出端口 output 只能写,不能读 LED、蜂鸣器控制
双向端口 inout 可读可写 数据总线(I2C、SPI)

写端口列表的时候,我个人习惯把时钟和复位放在最前面,这样一眼就能看到关键信号。举个例子:

module led_controller (
    input  wire       clk,      // 系统时钟
    input  wire       rst_n,    // 复位信号,低有效
    input  wire       btn,      // 按键输入
    output reg        led       // LED输出
);

你可能会问:为什么复位信号要写成rst_n?加个_n后缀是什么意思?这是业界约定俗成的写法,表示低电平有效。我建议你也养成这个习惯,不然过两个月回头看自己的代码,可能都分不清哪个是高有效哪个是低有效。

4.3 数据类型——wire和reg

这是新手最容易搞混的地方。我当年也在这上面栽过跟头,所以今天重点讲清楚。

4.3.1 wire(线网型)

wire代表物理连线。它本身不存储值,只是把两个点连起来。比如你把一个模块的输出接到另一个模块的输入,中间这根线就是wire。

特点:

  • 只能被assign语句驱动
  • 不能在always块里被赋值
  • 默认值是高阻态Z
wire a, b, c;
assign c = a & b;  // 正确,wire用assign驱动

4.3.2 reg(寄存器型)

reg代表存储单元。它可以在时钟边沿保存数据,也可以组合逻辑里用。注意:reg不一定是寄存器,它只是表示一个变量,能不能综合成寄存器要看你怎么写。

特点:

  • 只能在always块里被赋值
  • 不能在assign语句里用
  • 默认值是不定态X
reg [3:0] counter;
always @(posedge clk) begin
    counter <= counter + 1;  // 正确,reg在always里赋值
end

核心记忆法:

  • wire = 导线,用assign
  • reg = 变量,用always
  • 两者不能混用,否则编译报错

我曾经见过一个同事,把输出端口定义成wire,然后在always块里给它赋值,编译报错后折腾了半天才找到原因。说白了,端口类型和数据类型要匹配:

端口类型 常用数据类型 说明
input wire 输入信号来自外部,默认就是wire
output wire 或 reg 组合逻辑用wire,时序逻辑用reg
inout wire 双向端口必须是wire

4.4 赋值语句——assign和always

赋值语句是描述逻辑的核心。Verilog里主要有两种:连续赋值(assign)过程赋值(always)。这两种用法的区别,直接决定了你的代码是组合逻辑还是时序逻辑。

4.4.1 assign——连续赋值

assign用来描述组合逻辑。它的特点是:只要右边信号变化,左边立刻更新。说白了就是纯硬件连线,没有延迟,没有时钟。

assign led = btn ? 1'b1 : 1'b0;  // 按键按下,LED亮

这个例子很简单:按键按下去,LED就亮。没有任何时钟参与,信号变化是即时的。我在做按键消抖的时候,经常先用assign写一个简单的组合逻辑做验证,然后再改成时序逻辑。

小技巧:assign语句可以同时赋值多个信号,用逗号隔开就行:

assign {led_r, led_g, led_b} = {btn_r, btn_g, btn_b};

4.4.2 always——过程赋值

always块就灵活多了。它可以描述组合逻辑,也可以描述时序逻辑。关键看敏感列表怎么写。

先看一个时序逻辑的例子:

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        counter <= 4'd0;
    else
        counter <= counter + 1;
end

这个代码描述了一个4位计数器。每次时钟上升沿,计数器加1。复位信号低电平时,计数器清零。注意这里用的是<=(非阻塞赋值),这是时序逻辑的标准写法。

再看一个组合逻辑的例子:

always @(*) begin
    if (sel == 1'b0)
        y = a;
    else
        y = b;
end

这里用的是=(阻塞赋值),敏感列表写@(*)表示所有输入信号变化都触发。这个多路选择器跟用assign写效果一样,但always块里可以写更复杂的逻辑。

重要提醒:

  • 时序逻辑用 <=(非阻塞赋值)
  • 组合逻辑用 =(阻塞赋值)
  • 两者混用会导致仿真和综合结果不一致,我吃过这个亏

4.5 实战小例子——一个简单的LED闪烁

说了这么多,咱们来写个完整的例子。假设我们要让LED以1Hz的频率闪烁,系统时钟是50MHz:

module led_blink (
    input  wire       clk,
    input  wire       rst_n,
    output reg        led
);

    // 分频计数器
    reg [24:0] cnt;
    
    // 计数器逻辑
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            cnt <= 25'd0;
        else if (cnt == 25'd24_999_999)
            cnt <= 25'd0;
        else
            cnt <= cnt + 1;
    end
    
    // LED输出
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            led <= 1'b0;
        else if (cnt == 25'd24_999_999)
            led <= ~led;
        else
            led <= led;
    end

endmodule

这个例子把今天讲的知识点都用上了:模块结构、端口定义、wire和reg、always块。你仔细看看,cnt是reg类型,在always块里赋值;led也是reg类型,同样在always块里赋值。两个always块是并行执行的,这就是硬件描述语言的特点——所有always块同时运行,不像软件那样顺序执行。

好了,今天的内容就到这里。下一节我们会继续讲运算符和常用结构,到时候会用到今天学的这些基础。记住:多写代码,多仿真,慢慢就找到感觉了。