第1章:数字电路基础——从布尔代数到状态机

各位同学,欢迎来到《软硬件一体开发实战技能大全》的第一章。我是你们的导师,一个在嵌入式领域摸爬滚打了十几年的老工程师。今天咱们聊聊数字电路的基础,这部分内容看似枯燥,但说实话,它是整个软硬件协同设计的根基。你写驱动、调时序、做底层优化,最终都会回到这些基本概念上。

1.1 布尔代数与逻辑门

先问大家一个问题:计算机为什么能算数?答案就是布尔代数。说白了,就是用0和1两个值,配合「与、或、非」三种基本运算,就能描述一切数字逻辑。

我个人习惯把布尔代数看作「数字世界的语法」。比如:

  • 与门(AND):两个输入都是1,输出才是1。我常跟新人说:「与门就是两个开关串联,一个断了灯就不亮」。
  • 或门(OR):只要有一个输入是1,输出就是1。像两个开关并联,按哪个灯都亮。
  • 非门(NOT):输入取反。1变0,0变1。

我在项目中遇到过一个问题:一个同事用了一堆分立逻辑门搭电路,结果功耗超标。后来发现是没化简布尔表达式,白白多用了好几个门。所以记住:化简是基本功

重要公式(德摩根定律):

!(A && B) = !A || !B
!(A || B) = !A && !B

这个在硬件设计里太常用了,尤其是做低功耗设计时,能帮你省掉不少反相器。

1.2 组合逻辑电路

组合逻辑,就是输出只取决于当前输入,跟历史无关。嗯,这里要注意:它没有记忆功能。

1.2.1 加法器

加法器是CPU里最基础的运算单元。我刚开始学的时候,觉得半加器和全加器差不多,后来做项目才发现区别大了。

类型输入输出特点
半加器A, BSum, Carry不考虑进位输入
全加器A, B, CinSum, Cout考虑低位进位

举个例子:你要算二进制 1 + 1,半加器输出 Sum=0, Carry=1。但如果是 1 + 1 + 进位1,就必须用全加器了。我曾经在做一个8位加法器时,偷懒用了半加器级联,结果高位计算全错——这就是血的教训。

1.2.2 多路选择器(MUX)

多路选择器,说白了就是一个「数据开关」。2选1 MUX的表达式很简单:

Y = (S && A) || (!S && B)

当S=1时选A,S=0时选B。我在FPGA项目里经常用MUX来做寄存器配置,比如根据模式选择不同的时钟源。你想想看,如果没有MUX,你得用一堆三态门,麻烦得很。

实战技巧:在Verilog里写MUX,建议用case语句而不是if-else。因为case语句综合出来的硬件更清晰,不容易产生锁存器(latch)。

1.3 时序逻辑电路

时序逻辑和组合逻辑最大的区别是什么?它有记忆。输出不仅看当前输入,还看之前的状态。这就引出了触发器和计数器。

1.3.1 触发器(Flip-Flop)

触发器是数字电路里最基础的存储单元。D触发器最常见:

always @(posedge clk) begin
    Q <= D;
end

这段代码的意思是:在每个时钟上升沿,把D的值锁存到Q。我见过很多新手犯一个错误——把组合逻辑和时序逻辑混在一起写,结果综合出来一堆奇怪的latch。记住:时序逻辑用非阻塞赋值(<=),组合逻辑用阻塞赋值(=)

我曾经在一个项目里,因为没注意复位信号的同步问题,导致上电后触发器状态不确定,整个系统跑飞了。后来加了一个同步复位电路才解决。

1.3.2 计数器

计数器就是触发器的级联应用。最简单的二进制计数器:

always @(posedge clk or posedge rst) begin
    if (rst)
        count <= 0;
    else
        count <= count + 1;
end

这个代码实现了一个4位计数器,从0数到15然后回绕。我在做PWM控制时,经常用计数器来产生不同占空比的波形。注意:计数器的位宽决定了最大计数值,比如8位计数器最大只能到255。

避坑指南:我曾经在做一个高速计数器时,没考虑时钟域同步问题,结果跨时钟域采样时出现了亚稳态。后来加了双级同步器才稳定。记住:跨时钟域信号一定要同步处理!

1.4 状态机设计

状态机(FSM)是数字系统设计的核心。说白了,它就是「根据当前状态和输入,决定下一个状态和输出」的模型。

我习惯把状态机分为两类:

  • Moore型:输出只取决于当前状态。比如一个交通灯控制器,红灯亮多久只跟当前状态有关。
  • Mealy型:输出取决于当前状态和输入。比如一个序列检测器,检测到特定序列才输出高电平。

写状态机时,我建议用三段式写法:

// 第一段:状态转移
always @(posedge clk or posedge rst) begin
    if (rst) state <= IDLE;
    else state <= next_state;
end

// 第二段:下一状态逻辑
always @(*) begin
    case (state)
        IDLE: if (start) next_state = RUN;
              else next_state = IDLE;
        RUN:  if (done) next_state = IDLE;
              else next_state = RUN;
        default: next_state = IDLE;
    endcase
end

// 第三段:输出逻辑
always @(*) begin
    case (state)
        IDLE: out = 0;
        RUN:  out = 1;
    endcase
end

为什么推荐三段式?因为这样综合出来的硬件清晰,时序容易收敛。我见过有人把所有逻辑写在一个always块里,结果综合后时序跑不过,改起来特别痛苦。

个人经验:设计状态机时,一定要先画状态转移图。别急着写代码。我每次画完图,都能发现一些遗漏的状态或非法转移。画图花10分钟,可能省下调试的2小时。

小结

这一章我们聊了布尔代数、逻辑门、组合逻辑(加法器、MUX)、时序逻辑(触发器、计数器)和状态机。这些都是数字电路的「砖瓦」,后面学处理器架构、总线协议、外设驱动时,都会用到这些知识。

下一章,我们会深入处理器内核,看看CPU到底是怎么工作的。到时候你会发现,状态机设计的思想无处不在。

好了,今天就到这里。记住:基础不牢,地动山摇。把这些概念吃透,后面的路会越走越顺。

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