VHDL洗衣机控制器设计:状态机与模块化实现详解
2026/9/18 16:33:06 网站建设 项目流程

简介:面向电子设计自动化(EDA)学习者与嵌入式系统初学者的简易洗衣机设计文档,系统讲解基于硬件描述语言实现洗衣机控制系统的模块方框图、总体原理图与源代码设计思路,适合作为数字电路课程设计或EDA技术入门的参考资料。资源为单个doc格式文档,压缩包大小仅563KB,目前已吸引129人学习下载。文档自模块划分至运行流程层层展开,完整覆盖定时输入、时间控制、倒计时、状态控制、数码管显示及报警等关键子模块,并通过对状态机的设计讲解,清晰展示浸泡、洗涤、漂洗、脱水各阶段的切换逻辑。借助本设计可快速上手从行为级代码编写、仿真验证、逻辑综合到FPGA/ASIC硬件化的完整EDA全流程,对理解家电智能化设计、数字系统分层设计方法具有直观的参考价值。

1. 从一块FPGA板开始的洗衣机控制器反推

一个洗衣机控制器最核心的逻辑,往往不是电机驱动,而是一个只有两位的状态编码。这个观点可能和直觉相悖,但当你用VHDL在CPLD/FPGA上实现一个完整控制流程时就会发现:正转、反转、待机三个状态加上时间计数,就已经构成了一台简易洗衣机的全部控制语义。本文拆解的就是一份基于EDA技术的课程设计——用VHDL语言在Quartus II环境下实现的洗衣机控制器。整个设计采用自顶向下的模块化思路,拆成定时输入、时间控制、倒计时、状态控制、数码管显示和报警六个功能块,以状态机加计数器的经典组合实现“待机5s→正转10s→待机5s→反转10s”的循环洗涤流程。对于正在做EDA课程设计、或者想快速理解VHDL如何组织一个完整可控系统的人来说,这份设计最大的价值在于它的模块边界划分方式——每个模块的输入输出都被刻意收窄,信号传递路径清晰,非常适合作为CPLD/FPGA入门实践项目的参照。

2. 总体架构与状态编码:为什么模块化对VHDL设计是命门

2.1 六模块划分的依据与信号流设计

这份设计把整个控制器拆成六个模块,本质上是在做行为建模时最常见的分解方式:输入采集、逻辑控制、状态表达、时间基准、输出显示、异常提醒。从设计文档的方框图可以看出,定时输入模块接收外部按键信号并产生预置时间值,时间控制模块以固定脉冲为基准产生分频后的计数信号,倒计时模块负责减法计数,状态控制模块根据计数结果更新正转/反转/待机状态,数码管显示模块实时呈现剩余时间,报警模块负责循环结束后的提示。

模块划分的核心价值在于:每个模块的输入输出被严格定义后,仿真和排错就能分开做。定时输入模块的仿真只看count和dout1的波形是否正确;时间控制模块的仿真只看c和d信号是否按预期翻转;倒计时模块只看count是否逐周期减一。这种隔离式验证方式,在EDA设计中是保命符。一旦顶层综合出现问题,你可以用ENTITY声明快速锁定信号在哪个边界上断掉了。

2.2 状态编码的选择:两位二进制为什么够用

整套控制逻辑中最关键的设计决策是对电机状态的编码方式。设计中用dian这个两位信号表示当前洗衣机所处的工作状态:01代表正转,显示用deng输出10000代表待机,deng输出01011代表反转,deng输出001

这里有个容易被新手忽略的坑:两位二进制最多有四个状态00/01/10/11,设计只用了其中三个,10这个编码被留空。这是一个主动设计决策。留空状态的好处是,当仿真波形中出现10这个非预期值时,说明前面的计数器逻辑跑飞了,这个状态本身就是一个调试信号。如果你把四个状态全部铺满,反而丢了这种自查能力。

-- 状态编码对照 -- dian(1 DOWNTO 0) = "00" -> 待机, deng = "010" -- dian(1 DOWNTO 0) = "01" -> 正转, deng = "100" -- dian(1 DOWNTO 0) = "11" -> 反转, deng = "001" -- "10" 保留未用, 作为异常态供仿真排查

这段注释展示了状态分配的完整逻辑。deng的三位输出分别对应三只LED灯,当某一位置为1时该灯点亮,明确标示当前所处阶段。选用两位编码而非三位独热码,是因为这里只需要区分三种工作状态,独热码虽然更抗毛刺但会浪费寄存器资源。

2.3 循环次数的限值逻辑:LOOP语句与计数变量

设计文档里提到总体代码使用LOOP语句配合变量a来限制洗涤循环次数不超过15次。底层实现逻辑是这样的:每完成一组“待机-正转-待机-反转”周期,变量a加1,当a大于15时通过EXIT语句跳出循环。

需要注意,VHDL中的LOOP语句更常用于仿真测试基准,在可综合代码里过度依赖LOOP容易导致综合工具生成不可预测的硬件结构。更稳健的做法是用一个有限状态机来展开循环,把循环次数当作状态转移的条件。后面的章节我会给出等效的状态机写法对比,这里先保留原设计方案的描述以尊重原始框架。

3. 核心模块逐段拆解:从定时输入到倒计时的VHDL实现

3.1 定时输入模块:上升沿检测与计数边界

定时输入模块的目的是接收用户设定的洗涤时间。设计中的输入量为shuhshudin,其中shu的上升沿使dout(个位)加1,hshu的上升沿使dout1(十位)加1。din在有效时直接把整个计数器置为15,相当于锁定输入。以下是还原后的模块代码骨架:

LIBRARY IEEE; USE IEEE.STD_LOGIC_1164.ALL; USE IEEE.STD_LOGIC_ARITH.ALL; USE IEEE.STD_LOGIC_UNSIGNED.ALL; ENTITY shuru IS PORT ( shu, hshu, din : IN STD_LOGIC; dout : OUT STD_LOGIC_VECTOR (3 DOWNTO 0); dout1 : OUT STD_LOGIC_VECTOR (3 DOWNTO 0) ); END shuru; ARCHITECTURE behave OF shuru IS SIGNAL count : STD_LOGIC_VECTOR (3 DOWNTO 0) := "0000"; SIGNAL count1 : STD_LOGIC_VECTOR (3 DOWNTO 0) := "0000"; BEGIN PROCESS(shu, hshu, din) BEGIN IF (din = '1') THEN count <= "1111"; count1 <= "1111"; ELSE IF (shu'EVENT AND shu = '1') THEN IF (count = "1001") THEN count <= "0000"; ELSE count <= count + 1; END IF; END IF; IF (hshu'EVENT AND hshu = '1') THEN IF (count1 = "0110") THEN count1 <= "0000"; ELSE count1 <= count1 + 1; END IF; END IF; END IF; dout <= count; dout1 <= count1; END PROCESS; END behave;

这段代码中count的计数范围是0到9,对应个位0到9的十帧循环;count1的计数范围是0到6,对应十位的0到6。这个设计的意图是让用户最多可以设定69秒的洗涤时间。din信号的作用需要重点说明:当它为高电平时计数器被强制拉到15,这里的15用二进制表示为1111,后续的倒计时模块一旦拿到这个值会立即把剩余时间判为零。因此din实质上是“禁止输入”的控制信号,用在系统运行期间锁定设定值不被篡改。

3.2 时间控制模块:分频计数的输出策略

时间控制模块非常简单,输入只有clk脉冲,输出是cd两个信号。clk上升沿使内部信号shi加1,shi在0到60之间循环;当shi等于60时输出c为1,否则为0。同理count2在0到5之间循环,当它为5时输出d为1。

-- shi 的计数范围为 0 ~ 60, 每达到 60 输出一次 c = 1 -- count2 的计数范围为 0 ~ 5, 每达到 5 输出一次 d = 1

从硬件角度看,shicount2是两个独立的计数器,它们之间没有关联。c输出的意义在于提供一个周期性的脉冲信号,d输出的意义是产生状态切换的触发条件。这里的设计其实把一个计数器和一个比较器合并了:计数器持续累加,比较器判断是否到达阈值后输出标志位。综合工具通常会将这种结构优化成带复位的计数器,即在计数值达到阈值时产生一个时钟周期宽度的脉冲信号。

3.3 倒计时模块:减法计数与复位的优先级

倒计时模块是整个系统中和用户体验最直接相关的部分,因为它驱动数码管显示剩余时间。输入量有jianresetdinjian的上升沿使当前值减1,reset为高电平时模块无法工作。考虑到洗涤时间需要同时显示十位和个位,实际工程做法是例化两个相同结构的倒计时子模块分别处理十位和个位:

ENTITY daojishi IS PORT ( jian : IN STD_LOGIC; reset : IN STD_LOGIC; din : IN STD_LOGIC_VECTOR (3 DOWNTO 0); dout : OUT STD_LOGIC_VECTOR (3 DOWNTO 0) ); END daojishi; ARCHITECTURE rtl OF daojishi IS SIGNAL cnt : STD_LOGIC_VECTOR (3 DOWNTO 0); BEGIN PROCESS(jian, reset) BEGIN IF (reset = '1') THEN cnt <= din; ELSIF (jian'EVENT AND jian = '1') THEN IF cnt = "0000" THEN cnt <= "1001"; ELSE cnt <= cnt - 1; END IF; END IF; dout <= cnt; END PROCESS; END rtl;

这里需要特别讨论一个细节:当个位从0减到9时产生借位,这个借位信号是如何传递给十位的?设计文档中并没有明确画出借位信号的传递路径,通常的做法是把个位的借位输出作为十位的jian输入。个位计数器减到0的瞬间,下一个jian脉冲到来时回绕到9,同时输出一个高电平作为十位的减计数脉冲。这种级联方式在硬件上就是计数器之间通过进位信号串联。

reset为高电平时把计数器预置为din的值,这就实现了“重新开始洗涤时恢复显示设定时间”的效果。但要注意的是,else分支只处理了cnt = "0000"回绕到9的情况,如果初始设定值大于9,个位从设定状态无法正常工作。因此在顶层集成时,需要对个位输入做取模处理,否则倒计时显示会乱码。这是整个设计里我最想提醒的部分。

3.4 状态控制模块:计数驱动状态轮转

状态控制模块接收cc脉冲信号,每遇到一个上升沿,内部计数器count加1,count在0到5之间循环。dian的输出值根据count的当前值切换待机、正转、反转三种状态。它的信号排布在原文中有明确说明:01对应正转,00对应待机,11对应反转。

表格呈现对应关系更直观:

count值状态dian输出deng输出说明
0待机00010等待启动
1正转01100电机正转
2待机00010正转与反转切换间隙
3反转11001电机反转
4待机00010反转与正转切换间隙
5重置00010计数归零开始下一轮

状态转移的时间由cc脉冲的间隔决定,而cc又来自时间控制模块的输出。这种链式传导的计时代链在仿真中表现非常稳定,因为每个模块的时钟域完全一致,不存在跨时钟域同步问题。可以尝试把状态切换逻辑写成一个独立的进程来驱动LED灯,而非在同一个进程里同时做计数和输出,这样综合出的寄存器会更干净。

4. 顶层集成与Quartus II仿真验证

4.1 顶层实体的接口设计

在Quartus II中做顶层集成时,需要把shuruwashmachinehuidaojishidianjishumaguan等模块通过COMPONENT声明和PORT MAP端口映射连接起来。顶层实体的输入输出信号就是最终FPGA芯片上分配的物理引脚。

ENTITY wash_ctrl_top IS PORT ( clk : IN STD_LOGIC; rst : IN STD_LOGIC; key_set : IN STD_LOGIC; key_add : IN STD_LOGIC; led_state : OUT STD_LOGIC_VECTOR (2 DOWNTO 0); seg_out : OUT STD_LOGIC_VECTOR (6 DOWNTO 0); seg_sel : OUT STD_LOGIC_VECTOR (1 DOWNTO 0) ); END wash_ctrl_top;

顶层模块的端口命名规则建议在命名时添加后缀区分物理引脚和内部信号。seg_out是数码管的七段编码输出,seg_sel是位选信号,用于轮流点亮两个数码管。led_state对应三只LED灯,与前面提到deng信号直接相连。

4.2 仿真验证的核心步骤

在把顶层设计烧录到FPGA开发板之前,必须先做功能仿真。Quartus II的仿真流程包括以下步骤:

  • 建立工程并添加所有VHDL源文件
  • 编译生成网表,检查有无语法错误
  • 创建一个.bdf.vwf波形文件
  • 添加输入引脚和输出引脚
  • 设置时钟和激励信号,配置仿真时间
  • 运行仿真并观察波形

仿真中最重要的是确认倒计时模块的功能。通过jian信号手动输入几个脉冲,观察dout是否从设定值逐周期减到0,然后检查个位回绕时十位是否借位成功。原设计文档中提到“在软件上不能编译”,这通常是以下几个原因造成的。

4.3 编译失败排查思路

对这个设计而言,编译失败的最常见原因是VHDL语法与现代综合工具的兼容性问题。比如原文档附录中出现的dout[3,0]写法在VHDL标准语法中是非法的,端口向量声明必须使用(3 DOWNTO 0)。此外,如果顶层实体同时例化了多个包含PROCESS语句的模块,综合工具对所有进程的敏感信号列表要求非常严格,漏掉任一敏感信号都会导致编译警告甚至错误。

我在遇到类似编译失败案例时通常按以下顺序排查:

  • 检查每个模块的ENTITY端口声明和内部信号位宽是否完全匹配
  • 检查PORT MAP中映射的信号名是否有拼写错误
  • 检查是否有多个进程对同一信号赋值,VHDL规定同一信号不能在多个进程中驱动
  • 检查在Altera/Intel器件上STD_LOGIC_ARITHSTD_LOGIC_UNSIGNED库的引用顺序

另外原设计文档中提到是芯片型号选择问题,这在Quartus II中确实会发生。比如选择了不带乘法器的低端CPLD却编入了除法逻辑,综合工具就会报资源不足。对于本次设计的资源用量来说,任意一款主流的Cyclone系列FPGA或MAX系列CPLD都足够容纳,不需要特殊型号。

5. 状态机重写与循环次数控制:让这份设计真正变成工程可用

5.1 用FSM替换多模块链式结构的改造路径

原设计把状态切换和时间计数拆成了两个独立模块,通过脉冲信号互相联动。从功能验证的角度这种写法可取,但从工程实现角度能有更好的写法。更稳妥的工程化做法是把整个控制逻辑收拢为一个有限状态机,时间计数作为状态内的变量处理,循环次数作为状态转移条件的一部分。

-- 状态机状态定义 -- S_WAIT -> 待机, 进入时清零计时器 -- S_FWD -> 正转, 计时到 10s 后回到 S_WAIT -- S_REV -> 反转, 计时到 10s 后回到 S_WAIT -- 每完成一轮 S_WAIT-S_FWD-S_WAIT-S_REV, 循环计数 +1

这种写法的好处是查错容易,可以直观地看出某一时刻整个电路处于哪个流程阶段。仿真波形成层,排查问题时能精确锁定是哪一拍的迁移条件不满足。

5.2 循环次数控制的工程替代方案

原设计的15次循环上限用LOOP语句配合EXIT实现,但这在可综合代码里并不理想。可以设计一个独立的循环计数器,每当状态机完成一组正反转周期,计数器加1,当计数值达到用户预设次数时,置位完成信号。这个方式和原设计的意图完全一致,但状态转移路径清晰,也更容易扩展到多段自定义洗涤流程。

-- 循环计数参考伪代码 -- IF current_state = S_REV AND next_state = S_WAIT THEN -- cycle_count <= cycle_count + 1; -- IF cycle_count = preset_cycles THEN -- wash_finish <= '1'; -- END IF; -- END IF;

把循环次数的上限从常量改为用户可配置输入,只需增加一个4位输入端口,接上三个按键即可实现在洗涤过程中调整漂洗次数的功能。这个改造在硬件上几乎不增加任何成本,但对用户体验的提升是质变的。

5.3 报警模块的接线方式

原设计文档明确指出报警模块没有在总图中体现,只在附录里给出了思路:当洗涤结束时,finish信号触发,将时钟信号直接连接到蜂鸣器驱动引脚形成连续的报警音。如果使用有源蜂鸣器,这种方式是可行的;但如果是无源蜂鸣器,需要产生特定频率的方波才能发出声音。

-- 无源蜂鸣器驱动代码 -- 以 50MHz 时钟为例, 产生 1kHz 方波 PROCESS(clk) VARIABLE cnt_freq : INTEGER RANGE 0 TO 24999; BEGIN IF rising_edge(clk) THEN IF cnt_freq = 24999 THEN cnt_freq := 0; buzzer <= NOT buzzer; ELSE cnt_freq := cnt_freq + 1; END IF; END IF; END PROCESS;

把这段代码接入报警模块的输出端,蜂鸣器就会发出清晰的响声提示洗涤完成。这里的24999是根据50MHz时钟算出的分频系数:50,000,000除以1,000再除以2得到半个周期计数,由于计数器左闭右开,实际填入24999即可。若开发板时钟频率不同,把分频系数按同样方式换算即可适配。

本文还有配套的精品资源,点击获取

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

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

立即咨询