尧图精选

基于STM32的智能晾衣架:ADC采集、PWM调速与状态机设计

🕒 发布时间:2026/9/12 9:58:27 📁 来源:尧图网络
简介在嵌入式系统开发中模拟量传感器采集与直流电机控制是两类高频需求而ADC转换精度、PWM占空比调节及中断实时响应往往决定系统稳定性。以智能晾衣架为例它需要将雨滴与光照传感器的模拟信号转换为数字量并通过PWM精确控制升降电机同时依赖外部中断快速响应限位与安全保护。STM32凭借丰富的定时器、ADC通道和中断机制成为此类多任务控制的理想平台。本文从工程实践出发讲解基于STM32的多路ADC采样、16kHz PWM电机驱动、去极值滤波算法以及状态机与看门狗在固件设计中的应用为智能家居和机电一体化项目提供可复用的实现思路。1. STM32控制晾衣架比你想的更需要实时性一台智能晾衣架的核心难点不在能不能转而在雨滴传感器触发的瞬间系统要在几十毫秒内完成采样确认→刹车→反向启动这一连串动作同时还要兼顾光照判断、限位保护和手动指令。用51单片机做定时器资源分不过来用Arduino做PWM分辨率和中断响应又差点意思。STM32F103系列主频72MHz内置多个16位定时器、ADC和丰富的中断通道恰好覆盖多路传感器采集电机PWM控制异常响应这类混合任务。这份工程是完整可编译的Keil MDK项目目录里CORE放内核启动文件、SYSTEM放延时和串口基础模块、HARDWARE放传感器和电机驱动、USER放主逻辑与中断服务函数适合正在做课程设计的学生也适合想复用一套STM32外设驱动框架的工程师。下文所有代码均对应工程实际模块可直接对照源码修改参数。2. 外设分配与最小系统电路GPIO、定时器和传感器接口怎么规划2.1 引脚分配表与工程目录的对应关系拿到工程先打开HARDWARE目录里面按外设拆分了驱动文件。我习惯先列一张引脚分配表再写代码否则中断、PWM和ADC的引脚一旦冲突编译不报错运行时却会互相干扰。这套工程的外设分配逻辑大致如下功能模块引脚外设资源对应文件升降电机PWMPA8TIM1_CH1motor.c电机方向控制APB0GPIO推挽输出motor.c电机方向控制BPB1GPIO推挽输出motor.c雨滴传感器PA1ADC1_IN1sensor.c光敏传感器PA2ADC1_IN2sensor.c上限位开关PB3EXTI3外部中断limit.c下限位开关PB4EXTI4外部中断limit.c调试串口PA9 / PA10USART1_TX / USART1_RXuart.c这里要特别提醒一个容易踩的坑PA9同时是USART1_TX和TIM1_CH2的复用引脚。如果串口和电机PWM都往PA9上放编译顺利通过但运行时机电信号会串扰串口数据。我一般把电机PWM固定用TIM1_CH1即PA8串口用USART1这样默认调试通道不会被外设抢占。SYSTEM目录下的sys.c和delay.c不依赖具体外设建议先编译这两个模块跑通再去加HARDWARE里的驱动排查问题时范围会小很多。2.2 光敏与雨滴传感器模拟量ADC采集比开关量更可靠市面上常见的光敏和雨滴模块有两种形态一种是带LM393比较器的数字模块输出0或3.3V开关量另一种是纯传感器板输出模拟电压。这套工程用的是模拟量方案因为数字模块的阈值被比较器固定在硬件层面不同天气下的误判率很高。光照变化是渐进的雨滴刚落下时传感板电压只是缓慢改变用ADC采集后可以在固件里灵活设置迟滞区间比硬件比较器可靠得多。雨滴传感器上电后AO引脚输出电压范围是0到3.3V。干燥时电压接近3.3V淋雨后迅速下降。硬件连接上传感器AO直接接PA1即可但要注意供电电压VCC接3.3V时AO输出才在安全范围内如果接5V供电雨滴板受潮后AO输出电压可能超过STM32的模拟输入上限长期使用有损坏ADC的风险。稳妥做法是VCC接3.3V并在AO与PA1之间串一个10kΩ电阻做限流保护。2.2.1 ADC初始化与单通道采样代码/* adc.c - ADC1双通道轮询采样 */ #include adc.h #include stm32f10x.h void ADC_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); /* ADC时钟 72MHz / 6 12MHz */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_1 | GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; /* 模拟输入必须配置为AIN */ GPIO_Init(GPIOA, GPIO_InitStructure); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode DISABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); } uint16_t ADC_ReadChannel(uint8_t channel) { ADC_RegularChannelConfig(ADC1, channel, 1, ADC_SampleTime_55Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); return ADC_GetConversionValue(ADC1); }这段配置里有几个参数值得单独说明。RCC_ADCCLKConfig选RCC_PCLK2_Div6是把72MHz APB2时钟分频到12MHzSTM32F103的ADC最高允许14MHz留出余量保证高温下时钟漂移时依然可靠。ADC_SampleTime_55Cycles5表示单次采样保持时间为55.5个ADC周期对雨滴这类变化缓慢的模拟量绰绰有余还能滤掉一部分高频毛刺。ADC_ReadChannel每次调用前重新配置通道号是为了在PA1和PA2之间切换时不引入扫描模式的中断开销两个通道轮流调用即可。2.3 电机驱动选型TB6612比L298N更适合晾衣架晾衣架升降电机一般是12V直流减速电机额定电流0.5到2A堵转时可能到3A。L298N的饱和压降接近2V12V供电到电机两端只剩10V力矩损失明显而且模块体积大、发热严重。TB6612的MOS管导通压降在0.5V以内同样电压下电机出力更足模块尺寸也小。这套工程HARDWARE目录下的motor.c按TB6612设计逻辑输入引脚接PB0、PB1和PWM通道电机输出接M、M-。电源部分最容易被忽略的是共地问题。12V电源给电机和TB6612的VM供电3.3V给STM32供电两路电源的GND必须连在一起。如果GND不共地PWM信号没有参考地电机只会抖动不转。我见过不少课程设计卡在这里查遍代码找不出问题最后发现是两路电源各浮各的地。另外STM32的IO口驱动能力虽然不足以直接供电给电机但给TB6612的逻辑输入引脚提供电平信号完全没问题不需要加三极管放大。3. 基于定时器PWM的电机升降控制频率、占空比与保护逻辑3.1 PWM频率选择16kHz是噪音与效率的平衡点电机PWM频率直接影响噪音和扭矩。频率低于500Hz时线圈电流纹波大电刷处能听到明显的高频啸叫高于20kHz虽然超出人耳范围但STM32的IO翻转损耗增大TB6612的栅极驱动也可能跟不上。工程里取16kHz附近既听不到噪音驱动芯片也能稳定响应。实现上TIM1的计数时钟为72MHzARR设为4500-1即得到72MHz/450016kHz。占空比由CCR1寄存器控制CCR1从0到4500对应0%到100%。这里有个细节TIM1是高级定时器输出PWM前必须调用TIM_CtrlPWMOutputs使能主输出否则引脚上永远没有波形这是新手最容易卡住的地方。3.2 电机启停与方向切换代码/* motor.c - 基于TIM1_CH1的电机控制 */ #include motor.h #include stm32f10x.h void Motor_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_BaseInitStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_TIM1, ENABLE); /* PB0/PB1 方向控制引脚 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); /* PA8 配置为TIM1_CH1复用推挽输出 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); /* 16kHz PWM72MHz / 4500 16kHz */ TIM_BaseInitStructure.TIM_Period 4500 - 1; TIM_BaseInitStructure.TIM_Prescaler 1 - 1; TIM_BaseInitStructure.TIM_ClockDivision 0; TIM_BaseInitStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, TIM_BaseInitStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM1, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM1, ENABLE); TIM_CtrlPWMOutputs(TIM1, ENABLE); /* 高级定时器必须使能主输出 */ TIM_Cmd(TIM1, ENABLE); } void Motor_SetSpeed(uint16_t duty) { if (duty 4500) duty 4500; /* 限制占空比上限 */ TIM_SetCompare1(TIM1, duty); } void Motor_Ctrl(uint8_t dir, uint16_t duty) { if (dir MOTOR_UP) { /* 上升IN11, IN20 */ GPIO_SetBits(GPIOB, GPIO_Pin_1); GPIO_ResetBits(GPIOB, GPIO_Pin_0); } else { /* 下降IN10, IN21 */ GPIO_SetBits(GPIOB, GPIO_Pin_0); GPIO_ResetBits(GPIOB, GPIO_Pin_1); } Motor_SetSpeed(duty); } void Motor_Brake(void) { GPIO_SetBits(GPIOB, GPIO_Pin_0 | GPIO_Pin_1); /* 同高电平短路刹车 */ Motor_SetSpeed(0); Delay_Ms(50); }Motor_Brake里把IN1和IN2同时拉高TB6612进入短路制动模式电机制动比单纯断开PWM更快。方向切换前必须先刹车50ms否则电机反电动势会冲击驱动芯片和电源长时间频繁换向还可能烧毁MOS管。占空比参数要根据负载调整晾衣架空载时30%到40%占空比足够挂湿被子等重物时建议提升到70%以上低速大扭矩区域电机抖动明显长期运行对减速齿轮也不好。3.3 限位开关外部中断与堵转保护限位开关接PB3和PB4配置成外部中断模式。晾衣架运行到顶或到底时机械结构压住限位开关引脚电平跳变触发中断在中断服务函数里立即刹车不能等主循环轮询否则电机会带着整个晾衣架撞向机械限位。/* stm32f10x_it.c - 上限位中断服务函数 */ void EXTI3_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line3) ! RESET) { if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_3) 0) { up_limit_hit 1; /* 低电平表示压到上限位 */ Motor_Brake(); } EXTI_ClearITPendingBit(EXTI_Line3); } }限位开关若用常开型压下去后引脚从高变低低电平即触发状态。中断服务函数里只做两件事置标志位、刹车。不要在中断里做串口打印或延时那会拖垮整个系统的实时响应。堵转检测放在主循环中实现电机启动后启动一个计时窗口如果在规定时间内没有触发限位例如上升超过15秒还到不了上限就判定传动机构卡死执行刹车并反向松开半秒再恢复待机状态。4. 传感器轮询与晾晒状态机固件逻辑的组织方式4.1 状态机建模空闲、晾晒、收衣、手动晾衣架的控制逻辑如果直接写在主循环的if-else里功能每增加一个就要改一遍结构最后代码会变成一锅粥。我一般拆成四个状态空闲、晾晒、收衣、手动。空闲时电机停止周期性采样传感器满足晾晒条件则进入晾晒状态持续监测光照和雨滴环境恶化则进入收衣状态电机上升直到触发上限位手动状态允许用户通过按键或串口指令随时接管。每个状态对应一个独立的处理函数主循环每轮只做两件事调用当前状态的处理函数然后检查是否需要切换状态。状态切换条件集中在同一段代码里不要在中断里直接改状态变量否则中断优先级差异会导致状态跳变丢失系统行为变得不可预测。状态机还有一个额外好处出问题时可以通过状态值快速定位当前执行到哪个环节。4.2 传感器滤波单次ADC值不靠谱要做去极值平均雨滴传感器的输出噪声很大雨点落在传感板的瞬间ADC读数会剧烈抖动。如果拿单次采样值直接做判断晾衣架会频繁误触发收衣动作。常见的处理方法是连续采样8次去掉最大值和最小值再取平均这种方法对脉冲噪声的抑制效果最好计算量又小不需要引入复杂的卡尔曼滤波。/* filter.c - 去极值平均滤波 */ uint16_t Filter_MedianAverage(uint8_t channel, uint8_t samples) { uint16_t buf[8]; uint16_t min_val, max_val, sum; uint8_t i; if (samples 8) samples 8; if (samples 3) samples 3; for (i 0; i samples; i) { buf[i] ADC_ReadChannel(channel); Delay_Ms(2); /* 采样间隔2ms让采样电容充分恢复 */ } min_val buf[0]; max_val buf[0]; sum 0; for (i 0; i samples; i) { if (buf[i] min_val) min_val buf[i]; if (buf[i] max_val) max_val buf[i]; sum buf[i]; } return (sum - min_val - max_val) / (samples - 2); }samples取8时去掉最大最小值后6个值求平均。Delay_Ms(2)的目的是让ADC内部采样电容充分放电连续快速采样时后一次结果会带上一次残余电荷读数偏大。整个滤波流程耗时约15ms对主循环周期没有压力。注意这个函数不能放进中断服务函数里Delay_Ms是阻塞延时在中断里执行会直接卡死系统。4.3 收衣触发逻辑持续确认加迟滞判断自动收衣的触发条件不能只看一次采样。工程里的逻辑是雨滴传感器ADC值低于阈值且持续3秒才确认下雨光照低于阈值且持续10分钟才确认天黑。持续判断用计时计数实现不用阻塞延时主循环每轮检查一次。/* main.c - 收衣触发与状态切换 */ #define RAIN_THRESHOLD_LOW 1200 /* 雨滴ADC值低于此值视为淋雨 */ #define RAIN_THRESHOLD_HIGH 2000 /* 高于此值视为雨停迟滞恢复 */ #define RAIN_CONFIRM_TICKS 150 /* 3秒按20ms主循环周期折算 */ uint8_t g_state; uint8_t up_limit_hit; uint16_t rain_adc; uint16_t light_adc; uint32_t rain_start_tick; uint8_t rain_counting; void App_Task_20ms(void) { /* 每20ms采样一次并滤波 */ rain_adc Filter_MedianAverage(1, 8); /* 通道1 PA1 */ light_adc Filter_MedianAverage(2, 8); /* 通道2 PA2 */ if (g_state STATE_DRYING) { if (rain_adc RAIN_THRESHOLD_LOW) { if (!rain_counting) { rain_counting 1; rain_start_tick g_tick_count; } else if (g_tick_count - rain_start_tick RAIN_CONFIRM_TICKS) { g_state STATE_RETRACT; Motor_Ctrl(MOTOR_UP, 3000); /* 占空比3000/450067% */ rain_counting 0; } } else { rain_counting 0; /* 雨滴回升取消计数 */ } } if (g_state STATE_RETRACT up_limit_hit) { Motor_Brake(); g_state STATE_IDLE; up_limit_hit 0; } }阈值设置上用了迟滞手法RAIN_THRESHOLD_LOW取1200对应约0.98V雨滴板淋湿后电压会跌到1V以下雨停后要回升到2000即约1.6V才解除下雨标记。这样避免雨量在小雨和毛毛雨之间波动时收衣状态被反复触发。占空比3000对应67%输出收衣时保证足够扭矩。主循环周期稳定在20ms由定时器中断置位标志驱动不能靠while循环空转计时否则串口日志、传感器采样和电机控制的时序会全部乱掉。5. 串口日志、看门狗与Flash状态恢复的实战技巧5.1 用USART1打印状态机切换记录代码写完最难的是现场定位到底是传感器误判、电机没转、还是限位没触发。我在工程中保留了一个串口调试开关通过宏控制是否打印完整日志。日志要包含状态切换点、传感器原始值和关键时间戳不能只打印最终结果。/* debug.h - 串口DEBUG开关 */ #define DEBUG_EN 1 #if DEBUG_EN #include stdio.h #define DBG(fmt, ...) printf([DBG] fmt \r\n, ##__VA_ARGS__) #else #define DBG(fmt, ...) #endif使用时在状态切换处打印DBG(state %d - %d, rain%d, light%d, old_state, new_state, rain_adc, light_adc)。printf重定向到USART1需要实现fputc函数在Keil中勾选微库后于uart.c里重写fputc输出到USART1即可。波特率固定115200日志带g_tick_count时间戳可以精确算出电机从启动到限位触发用了多少毫秒判断传动机构是否卡滞。注意中断服务函数里绝对不要调用printfprintf会占用数百微秒到毫秒级时间在中断里执行会破坏系统的实时性日志只在主循环的状态切换点输出。5.2 独立看门狗IWDG程序跑飞时的最后兜底电机堵转、while循环卡死、数组越界写坏栈任何异常都会让晾衣架停在半空。独立看门狗是硬件级的最后防线。IWDG由LSI内部约40kHz时钟驱动与主时钟无关即使系统时钟崩溃也能工作。/* wdg.c - 独立看门狗超时约1.6秒 */ void IWDG_Config(void) { IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); IWDG_SetPrescaler(IWDG_Prescaler_64); /* 40kHz / 64 625Hz */ IWDG_SetReload(1000); /* 1000 / 625 1.6秒 */ IWDG_ReloadCounter(); IWDG_Enable(); }喂狗调用IWDG_ReloadCounter()放在主循环里每轮执行一次。这里有个隐蔽的坑如果主循环中存在超过1.6秒的阻塞延时比如用Delay_Ms做10秒电机运行计时看门狗会提前复位。我一般把超过200ms的等待都改成非阻塞状态机保证主循环周期稳定在100ms以内。复位后可以在main入口检查RCC_GetFlagStatus(RCC_FLAG_IWDGRST)判断是异常复位还是正常上电异常复位时打印错误标志并让晾衣架先回到IDLE状态避免带着错误状态继续执行。5.3 内部Flash保存掉电状态意外断电后晾衣架停在半空重新上电如果不知道当前位置直接执行收衣动作可能先向上撞一次限位再反向增加机械冲击。把电机状态和传感器均值写入内部Flash上电时恢复是零成本方案。/* flash_store.c - 利用内部Flash最后一页保存状态 */ #define STORE_ADDR 0x0800F800 /* 末页起始地址F103容量64KB */ typedef struct { uint32_t magic; /* 固定值0xA5A5A5A5标识有效数据 */ uint8_t state; /* 上次系统状态 */ uint16_t rain_avg; /* 掉电前雨滴均值 */ uint16_t light_avg; /* 掉电前光照均值 */ } SystemStatus_t; void Store_Save(SystemStatus_t *p) { FLASH_Unlock(); FLASH_ErasePage(STORE_ADDR); /* 擦除整页 */ p-magic 0xA5A5A5A5; FLASH_ProgramWord(STORE_ADDR, p-magic); FLASH_ProgramWord(STORE_ADDR 4, p-state); FLASH_ProgramWord(STORE_ADDR 8, p-rain_avg); FLASH_ProgramWord(STORE_ADDR 12, p-light_avg); FLASH_Lock(); }两个关键细节。第一Flash擦写寿命约一万次只能在状态切换时写入不能每个主循环都调用否则几天内Flash就报废了。第二执行Flash擦写期间CPU会暂停因为Flash控制器忙于擦除操作此时看门狗仍在计时如果擦写时间接近看门狗超时值程序会在写完后被复位。解决办法是在保存前先调用IWDG_ReloadCounter喂一次狗保存完成后主循环正常喂狗。上电读取时检查magic字段若不是0xA5A5A5A5就说明数据无效直接回退到IDLE状态并清除该页宁可重新初始化也不让晾衣架带着脏数据动作。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →