VHDL洗衣机控制器设计:状态机与模块化实现详解

发布时间:2026/9/18 16:33:25
VHDL洗衣机控制器设计:状态机与模块化实现详解 简介面向电子设计自动化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 定时输入模块上升沿检测与计数边界定时输入模块的目的是接收用户设定的洗涤时间。设计中的输入量为shu、hshu和din其中shu的上升沿使dout个位加1hshu的上升沿使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 (shuEVENT AND shu 1) THEN IF (count 1001) THEN count 0000; ELSE count count 1; END IF; END IF; IF (hshuEVENT 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脉冲输出是c和d两个信号。clk上升沿使内部信号shi加1shi在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从硬件角度看shi和count2是两个独立的计数器它们之间没有关联。c输出的意义在于提供一个周期性的脉冲信号d输出的意义是产生状态切换的触发条件。这里的设计其实把一个计数器和一个比较器合并了计数器持续累加比较器判断是否到达阈值后输出标志位。综合工具通常会将这种结构优化成带复位的计数器即在计数值达到阈值时产生一个时钟周期宽度的脉冲信号。3.3 倒计时模块减法计数与复位的优先级倒计时模块是整个系统中和用户体验最直接相关的部分因为它驱动数码管显示剩余时间。输入量有jian、reset和dinjian的上升沿使当前值减1reset为高电平时模块无法工作。考虑到洗涤时间需要同时显示十位和个位实际工程做法是例化两个相同结构的倒计时子模块分别处理十位和个位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 (jianEVENT 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加1count在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中做顶层集成时需要把shuru、washmachine、huidaojishi、dianji、shumaguan等模块通过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_ARITH和STD_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即可。若开发板时钟频率不同把分频系数按同样方式换算即可适配。本文还有配套的精品资源点击获取