基于STM32的洗衣机控制系统:从状态机到烧录排错实战
发布时间:2026/9/16 20:20:16来源:尧图网络
简介基于STM32的洗衣机控制源码包面向嵌入式学习者整体共92个文件以40个C源文件、41个H头文件为主配套.s启动文件、.uvprojx/.uvoptx工程配置与.lst列表文件压缩包仅358KB。已有351人学习下载。代码结合STM32标准外设库包含电机PWM驱动、水位检测、按键、红外遥控、LCD12864显示、蜂鸣器等模块目录按CORE、SYSTEM、HARDWARE和STM32F10x_FWLib组织可清晰查看初始化、外设配置、中断服务及任务调度链路。通过研读该工程能直观理解STM32外设复用与资源分配方法以及洗衣机洗涤、脱水、加热、定时等控制逻辑的具体落地方式适合作为课程设计或入门项目的参考。1. 解压就能编译不代表装到板子上就能转起来一套基于 STM32 的洗衣机控制源代码硬件上无非是主控、电机驱动、水位传感器、按键和显示真正决定程序能不能跑起来的是把进水、洗涤、漂洗、排水、脱水这些动作排成一条不依赖人工介入的时序链。拿到这类工程的人通常不是来欣赏代码风格的而是改洗涤时间、换板子、或者把电机驱动换成自己的负载。多数时候编译能过烧录时却报 no target found或者电机只抖不转、进水不停问题正好出在引脚映射、定时器配置和状态切换条件上。这篇文章按「外设资源认识 → 状态机实现 → 烧录排错 → 功能优化」展开用 HAL 库写照着改就能用到自己的工程里。2. 洗衣机控制核心外设与 STM32 资源分配洗衣机的电气负载并不复杂麻烦的是这些负载的动作有严格时序进水阀不能和排水阀同时开电机反转前要先停止一段时间脱水时打开排水阀还要限制最高转速。STM32 的优点在于定时器数量充足、中断优先级可配一套外设资源就能同时服务 PWM 输出、按键扫描和 ADC 采集。开始改代码前先把外设分工想清楚后面查问题会省很多时间。2.1 定时器 PWM 与电机驱动方式的选型原因洗衣机程序里最核心的执行部件是电机。低成本波轮洗衣机常见驱动方式有两种交流电机配双向可控硅直流有刷电机配 MOS 全桥。驱动方式直接决定占空比怎么给、频率怎么设、用哪个定时器。交流电机移相调速不需要很高频率常见做法是用一个普通定时器做 100Hz 左右的触发脉冲配合过零检测控制导通角直流电机则适合用高级定时器的互补 PWM 输出比如 TIM1 的 CH1/CH1N可以自然形成死区避免上下桥直通。频率上直流有刷电机取 10kHz20kHz低于 10kHz 电机会有啸叫高于 20kHz 对 MOS 开关损耗不友好。定时器分配时还要留一个通道做测速。电机轴上装霍尔或光电码盘时用另一个定时器的输入捕获模式测脉宽比在主循环里读电平更可靠。比较规范的源代码工程里PWM 定时器和捕获定时器是分开的原因就在这里捕获中断不能让 PWM 输出中断阻塞否则电机会抖动。2.2 按键输入与水位 ADC 的采集设计按键不能直接在中断里做判断洗衣机的按键多数是薄膜开关按下时电平会有几毫秒到几十毫秒的抖动。常见做法是把按键扫描放在 10ms 的周期任务里连续两次读到相同状态才确认有效。长按、短按则在按键确认有效后通过计时变量区分。水位传感器输出的是 0~5V 电压或 4~20mA 电流接到 STM32 的 ADC 引脚前要确认量程匹配。采集端要做滤波不能把单次 ADC 值直接当水位用水面晃动会让数值来回跳。简单的移动平均就够用uint16_t read_water_adc(void) { uint16_t sum 0; for (uint8_t i 0; i 8; i) { HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); sum HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); HAL_Delay(2); // 等待采样电容完全充电 } return sum 3; // 8 次平均值右移 3 位等效除以 8 }这里把HAL_Delay(2)放在两次采样之间是为了让片内采样电容稳定充电采样值更接近实际电压。PollForConversion的超时参数设为 10ms正常情况下转换远快于这个时间设置它是为了防止 ADC 异常时程序卡死在等待里。sum 3比除法快在资源紧张的工程里是常见写法。2.3 洗衣机外设资源分配参考表与优先级拿到一份不带原理图的源代码时最快的方法是先根据代码里的 GPIO 初始化反推外设分配。下表是一份比较典型的资源划分和多数低成本洗衣机方案兼容功能STM32 外设常见引脚关键配置电机 PWM 输出TIM1_CH1 / CH1NPA8 / PB13PWM1 模式10~20kHz互补输出带死区电机换向GPIOPB0 / PB1开漏输出加下拉禁止上下桥同时导通按键输入GPIO EXTI 或轮询PC0 ~ PC3上拉输入10ms 消抖水位检测ADC1_IN0PA0采样时间尽量拉长软件滤波进水阀/排水阀GPIOPB12 / PB13推挽输出接继电器或可控硅数码管段码GPIO 或 SPIPA0 ~ PA7动态扫描2ms 切换一位调试串口USART1PA9 / PA10115200 8N1仅调试用一个值得注意的细节PA13、PA14 默认是 SWD 接口量产板和调试板都不建议把功能复用到这两个脚上不然烧录器连不上时又要飞线。数码管的动态扫描会占用很多 GPIO不少工程会用 74HC595 串转并来节省 IO这也是断码显示常见做法。串口资源在正常运行时可以让出来但源代码里保留一个调试串口对排查时序问题帮助非常大不建议注释掉。3. 面向状态机的代码结构从启动到脱水洗衣机程序最忌讳写成从上到下一大段顺序执行因为进水需要等、洗涤需要转任何一个阻塞延时都会让按键失去响应。成熟一点的做法是状态机加时间片一个周期任务不断查询当前状态、判断迁移条件同时把按键、ADC、显示放进不同的时间槽里。3.1 用 SysTick 时间片替代 delay 堆时序裸机方案里SysTick 中断负责维护一个全局的g_tick_ms主循环通过比较差值来实现非阻塞延时。这样做的好处是电机转 30 秒时不耽误按键扫描排水时也能实时响应停止指令。void SysTick_Handler(void) { g_tick_ms; } uint8_t is_timeout(uint32_t start, uint32_t duration) { return (g_tick_ms - start) duration; }g_tick_ms - start是无符号减法即使计数值回绕只要差值小于 2^32 就能正确判断不需要额外处理溢出。这个写法比g_tick_ms start duration更可靠后者在边界处有溢出的风险。所有需要定时的位置都记录开始时间然后周期查询不阻塞主循环。3.2 用枚举和结构体维护洗涤状态机把整机状态定义成枚举把运行参数收进结构体状态迁移集中在同一个函数里。这样清洗逻辑清晰也方便用按键在任意状态回到待机typedef enum { PHASE_IDLE 0, PHASE_WASH, PHASE_RINSE, PHASE_SPIN, PHASE_DRAIN, PHASE_DONE } phase_t; typedef struct { phase_t phase; uint32_t phase_start_ms; // 进入当前状态的时刻 uint16_t wash_seconds; // 洗涤剩余时间 uint16_t rinse_seconds; // 漂洗剩余时间 uint8_t water_level; // 目标水位挡位 } washer_t; static washer_t wm { PHASE_IDLE, 0, 0, 0, 0 };phase_start_ms在每次状态切换时更新后面判断超时都以它为准避免使用多个散落的计时变量。实际的片段式逻辑常写成 switch-case每个分支只处理一件事要么迁移状态要么调用对应的执行函数。下表是波轮洗衣机最常见的状态迁移条件当前状态迁移条件下一个状态IDLE启动键按下WASHWASH水位到达且运行时间耗尽DRAINDRAIN排水计时结束RINSE 或 SPINRINSE漂洗次数未达到WASH 或 DRAINSPIN脱水计时结束DONEDONE任意键按下IDLE这套结构里「水位到达」「时间耗尽」都只是状态位真正的检测逻辑在各自的周期任务里完成状态机本身不做 ADC 采集也不直接去翻转电机引脚。3.3 电机正反转、停止与刹车的控制函数电机控制函数是状态机的主要执行出口。对直流有刷电机正转、反转通过两个 GPIO 决定方向占空比通过定时器比较寄存器设定。停止时要区分自由停机还是刹车自由停机只撤 PWM电机靠摩擦慢慢停刹车则把电机两端短接利用反电动势快速制动脱水结束前一般需要刹车。void motor_set(uint8_t dir, uint8_t duty) { __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, 0); switch (dir) { case DIR_CW: GPIOB-BSRR GPIO_PIN_0; // 正向引脚置高 GPIOB-BSRR GPIO_PIN_1 16; // 反向引脚拉低 __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, duty); break; case DIR_CCW: GPIOB-BSRR GPIO_PIN_0 16; GPIOB-BSRR GPIO_PIN_1; __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, duty); break; default: GPIOB-BSRR GPIO_PIN_0 16; GPIOB-BSRR GPIO_PIN_1 16; // 两个方向引脚同时拉低 break; } }先把占空比清零再切方向是为了避免换向瞬间产生过流。两个方向引脚先全低再给 PWM能保证切换过程不出现直通。GPIO_PIN_1 16是 HAL 库中 BRR 对应的位置直接操作寄存器比调用 HAL_GPIO_WritePin 快在 10ms 周期任务里调用频率高时更合适。3.4 进水超时与安全保护逻辑洗衣机装水时如果水位传感器失效进水阀会一直开。源代码里必须有超时保护进水阀打开后启动计时连续 3 分钟没到达目标水位就关阀报警。排水也同理排水阀开启后如果 5 分钟水位没降下来同样判为异常。void water_fill_logic(void) { if (valve_state VALVE_ON) { if ((g_tick_ms - fill_start_ms) 180000UL) { valve_ctrl(VALVE_OFF); alarm_set(ALARM_WATER); // 报警并停留当前状态 wm.phase PHASE_IDLE; } else if (read_water_adc() level_threshold[wm.water_level]) { valve_ctrl(VALVE_OFF); // 水位到了就关阀 wm.phase PHASE_WASH; } } }fill_start_ms在进水阀打开的一瞬间用g_tick_ms赋值超时判断用无符号差值比较防止 49 天计数器回绕引发误判。这里把水位阈值做成数组level_threshold[]不同挡位对应不同 ADC 值后面如果要支持更多水位挡位只需要扩数组不需要改状态机逻辑。报警之后把状态切回 IDLE保证主循环里所有执行函数都不再动作。4. 编译烧录实战芯片包、目标板与三处高频排错代码逻辑再完整烧不进板子也白搭。从 AT89C51 这类 51 单片机转过来的开发者最容易卡在两点Keil 里没有对应芯片型号以及 ST-Link 提示找不到目标。这两类问题都不是程序逻辑问题处理起来有一套固定的排查顺序。4.1 解压后的工程目录与芯片型号核对比较规范的 STM32 工程目录会分成Core、Drivers、User三层。Core里是芯片启动文件和系统时钟Drivers里是 HAL 库或标准外设库User里是 main.c 和业务逻辑。拿到源代码第一件事不是双击工程而是打开魔术棒选项确认 Device 型号。如果是 STM32F103C8T6 的工程用 CubeMX 重建时钟配置最快。旧工程直接打开有时会提示 Device 不存在说明本机没装对应芯片包。此时打开 Keil 的 Pack Installer在 Packs 列表里搜索 STM32F1 系列并安装不需要重装整个 Keil。常见问题是装了 C51 版 Keil 之后又装 MDK两者共存时编译器路径互相覆盖处理办法是在魔术棒的 Target 标签里重新指定 ARM 编译器目录而不是卸载重装。4.2 ST-Link 连接失败 error no stm32 target found 的排查Error: No STM32 target found!是烧录环节最高频的报错。这句话下面往往还有一行If your product embeds debug authentication意思是目标芯片可能开启了调试保护也可能是根本没有连上。排查顺序很有讲究确认 ST-Link 的 SWDIO、SWCLK、GND、3.3V 四根线都接对SWD 比 JTAG 省脚多数开发板只引这四个按住开发板复位键再点下载点击后 0.5 秒左右松开排除程序里把 SWD 引脚复用成 GPIO 导致连接失败把 ST-Link 通讯速率从 4MHz 降到 1MHz长线连接时高速更容易失败用 ST-Link Utility 单独连接一次如果 Utility 也连不上基本排除了 Keil 配置问题回到硬件接线。工程里如果确实把 PA13/PA14 复用掉了最省事的解决方案是用 STM32CubeProgrammer 的 Connect Under Reset 模式烧录它能在复位期间抢占调试接口。这个操作不需要改电路比飞线快得多。4.3 换板子后引脚和时钟对不上的最小改动源代码大概率不是给当前手头这块板子写的直接编译烧录可能数码管不亮、电机没反应。不要试图去把所有 GPIO 配置都重写一遍先定位三处时钟频率、电机引脚、串口引脚。static void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_0 | GPIO_PIN_1; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); }如果新板子的电机驱动接在 PA4 和 PA5把GPIOB改成GPIOAPin 改成对应位即可。关键是改完后要同步检查定时器的通道输出引脚TIM1_CH1 固定映射在 PA8如果原理图把 PWM 接到了 PB13需要改用 TIM1_CH1N或者在 CubeMX 里重映射到其他定时器。时钟树方面外部晶振频率不同会导致串口波特率偏差典型现象是能发数据但上位机收到乱码这时检查 RCC 配置里的 HSE_VALUE 宏定义。4.4 串口打印与 USB 虚拟串口的两个细节串口是观察洗衣机状态机最直接的手段。在状态切换处打印一行信息运行轨迹一目了然。HAL 库环境下需要重定向fputcint fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 10); return ch; }FILE *f参数是必须保留的省略后会因为 C 库的底层调用冲突导致编译报错。使用后要注意两点其一printf会占用 CPU 时间在高速 PWM 控制循环里别放打印其二板载 ST-Link 的虚拟串口在 Windows 下有时会识别成带黄色叹号的设备装好 ST-Link 驱动后仍不行检查是不是被系统分到了错误的串口号用 STM32 Virtual COM Port 驱动覆盖安装一次通常能解决。5. 从能用做到好用的进阶模糊推理、软启动与验证基础功能跑通只是第一步。市面上的洗衣机控制板普遍有模糊称重、水位自动判断、启动防抖动这些细化功能这些是高分与及格线的差距所在。5.1 用 Python 先验证模糊推理规则再进 C模糊推理难以调参是因为凭感觉写的规则表往往互相矛盾。常见做法是在 PC 上用 Python 先把输入输出关系算一遍确认趋势正确后再移植成 C 的查表结构。一段极简的洗衣机模糊推理原型def wash_time(weight, turbidity): # weight: 衣物重量归一化 0~1 # turbidity: 洗涤水浊度归一化 0~1 time 8 weight * 10 turbidity * 12 return min(max(time, 8), 30)这里的 8 分钟是最短洗涤时间10 和 12 是重量和浊度对时间的贡献系数输出范围限制在 8~30 分钟。数据量小纯查表也够用不必在 STM32 里跑真隶属度函数。C 端只需要把这个公式转成定点运算权重和浊度从 ADC 值归一化后查表即可。5.2 电机软启动与脱水刹车的实现技巧启动时从零逐渐升高占空比能明显减小启动电流和机械冲击。软启动的步长既不能太大也不能太小步长 10、间隔 20ms大约 0.2 秒达到最大占空比对波轮洗衣机来说体感最自然。脱水结束前要先断 PWM间隔几百毫秒后再短接电机两端刹车否则反电动势会倒灌到母线电容轻则电压升高影响传感器采样重则损坏 MOS 管。刹车时间一般控制在 0.5~2 秒具体看桶的惯性大小。5.3 波形、电流与边界工况的验证清单别只看程序跑没跑完一个流程关键波形要实测。验证时按以下几点过一遍PWM 频率是否在预期范围、换向瞬间有没有直通、进水阀打开后 ADC 采集值是否缓慢上升、断电重启后状态机能否回到 IDLE。脱水时电流曲线如果出现持续上升优先怀疑机械不平衡而不是改程序。用两个万用表同时量水位电压和电机电流比只看一个点更容易定位问题。本文还有配套的精品资源点击获取
网站建设高端定制企业官网