第三章 时序逻辑与状态机:寄存器、计数器、分频器、有限状态机设计

各位好,我是老李。今天咱们聊聊CPLD设计里最核心的东西——时序逻辑与状态机。说实话,我入行那会儿,觉得组合逻辑简单,时序逻辑绕人。后来做多了才发现,时序逻辑才是数字电路的灵魂。你想想看,没有时钟节拍,所有信号就是一锅粥。

3.1 寄存器:时序逻辑的基石

寄存器说白了就是能存数据的触发器。CPLD里的寄存器,每个都是D触发器。我习惯把寄存器想象成一个"听话的盒子"——时钟上升沿来的时候,它把输入数据锁住,然后一直保持到下一个时钟沿。

核心要点:寄存器在时钟上升沿采样数据,其他时间输出保持不变。这是所有时序逻辑的基础。

写Verilog代码时,寄存器描述有固定套路:

// 一个简单的D触发器
reg [7:0] data_reg;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        data_reg <= 8'h00;
    else
        data_reg <= data_in;
end

注意这里用的是非阻塞赋值"<="。我刚开始学的时候,总搞不清阻塞和非阻塞的区别。后来踩过坑才明白:时序逻辑用非阻塞,组合逻辑用阻塞。这是铁律,别问为什么,记住就行。

个人经验:我曾经在一个项目里,把寄存器赋值写成了阻塞赋值,结果仿真怎么都对,上板子就乱跳。查了两天才发现是赋值方式的问题。从那以后,我写always块第一件事就是确认赋值符号。

3.2 计数器:最常用的时序模块

计数器是寄存器的升级版。说白了,就是每个时钟周期加1。我做过那么多项目,几乎每个项目都有计数器——分频、定时、状态跳转,哪哪都离不开它。

一个N位计数器,能计数的范围是0到2^N-1。举个例子:

// 8位计数器,从0计数到255
reg [7:0] cnt;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        cnt <= 8'h00;
    else
        cnt <= cnt + 1'b1;
end

嗯,这里有个坑要注意:计数器溢出后会自动回零。8位计数器加到255,再加1就变成0了。这个特性有时候是好事,有时候是坏事,看你怎么用。

避坑指南:我曾经设计一个定时器,需要计数到1000就停止。结果用了8位计数器,最大只能到255。仿真时没注意,上板后定时时间完全不对。所以设计计数器前,先算好需要的位宽。

3.3 分频器:从高频到低频的转换

分频器本质上就是计数器。你想啊,时钟频率太高,外设跟不上,就得降频。分频器就是干这个的。

分频有两种:偶数分频和奇数分频。偶数分频简单,计数器计到一半翻转就行。奇数分频稍微麻烦点,需要两个计数器配合。

来个2分频的例子:

// 2分频器,输出频率是输入时钟的一半
reg clk_div2;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        clk_div2 <= 1'b0;
    else
        clk_div2 <= ~clk_div2;
end

你看,每个时钟沿翻转一次,输出频率正好是输入的一半。简单吧?

分频类型 实现方式 占空比
偶数分频 计数器计数到N/2-1翻转 50%
奇数分频 两个计数器+或门组合 接近50%
小数分频 累加器+相位累加 不均匀

我建议:项目里尽量用偶数分频,占空比好控制。实在需要奇数分频,用PLL或者DCM,比自己写靠谱。

3.4 有限状态机:Moore与Mealy

状态机是CPLD设计的核心技能。说白了,状态机就是"根据当前状态和输入,决定下一步去哪"。我做过最复杂的协议解析,核心就是一个大状态机。

两种状态机:

  • Moore型:输出只取决于当前状态。稳定,但响应慢一拍。
  • Mealy型:输出取决于当前状态和输入。响应快,但容易出毛刺。

我个人习惯用Moore型。为什么?因为输出稳定,调试方便。Mealy型虽然省一个时钟周期,但那个毛刺问题处理起来太麻烦。

// Moore型状态机示例
localparam IDLE = 2'b00;
localparam START = 2'b01;
localparam DATA = 2'b10;
localparam STOP = 2'b11;

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_bit ? START : IDLE;
        START:  next_state = DATA;
        DATA:   next_state = bit_cnt == 7 ? STOP : DATA;
        STOP:   next_state = IDLE;
        default: next_state = IDLE;
    endcase
end

// 输出逻辑
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        data_out <= 1'b0;
    else case (state)
        DATA: data_out <= serial_in;
        default: data_out <= 1'b0;
    endcase
end

3.5 三段式状态机写法

说到状态机写法,我强烈推荐三段式。为什么?因为结构清晰,综合工具优化得好,而且不容易出bug。

三段式就是:

  1. 第一段:时序逻辑,描述状态寄存器
  2. 第二段:组合逻辑,描述次态转移
  3. 第三段:时序逻辑,描述输出

上面那个例子就是标准的三段式。你注意看,三段各司其职,互不干扰。我见过有人把三段揉在一起写,结果代码又长又乱,调试起来想哭。

核心原则:三段式状态机的精髓在于"时序和组合分离"。状态跳转用组合逻辑,状态寄存和输出用时序逻辑。这样综合出来的电路,时序性能最好。

我画了一张图,帮你理解三段式的结构:

三段式状态机结构图 第一段:状态寄存器 时序逻辑 always @(posedge clk) 第二段:次态逻辑 组合逻辑 always @(*) 第三段:输出逻辑 时序逻辑 always @(posedge clk) 当前状态 次态 当前状态 输入信号 输入信号 → 第二段 第三段 → 输出信号

你看这个图,数据流向很清楚。第一段存当前状态,第二段根据当前状态和输入算下一步去哪,第三段根据当前状态(Moore型)或当前状态+输入(Mealy型)产生输出。

注意:写三段式时,第二段一定要用组合逻辑(always @(*)),千万别加时钟沿。我见过有人把第二段也写成时序逻辑,结果状态机跑起来慢了一拍,整个协议解析全乱套。

好了,关于时序逻辑和状态机,今天就聊这么多。这些东西看着基础,但真用好了,能解决很多复杂问题。我做了十几年CPLD设计,最深的体会就是:基础不牢,地动山摇。寄存器、计数器、状态机,这些基本功练扎实了,什么协议解析都不在话下。

最后说一句:设计状态机时,一定要画状态转移图。别偷懒,画图能帮你发现很多逻辑漏洞。我每次设计新状态机,第一件事就是拿纸笔画图,画清楚了再写代码。


公众号:蓝海资料掘金营,微信deep3321