1. 项目缘起与整体方案设计
1.1 为什么选择ESP32加无刷电机这个组合
最早动这个念头,是因为家里那台老落地扇。三档机械旋钮,夜里要么吹得头疼,要么热得睡不着,中间那档风量永远找不到。市售的直流变频风扇动辄大几百,而手头正好有几片ESP32开发板和几个拆机无刷电机,就想着自己攒一台能根据室温自动调速、还能用手机调风量的智能风扇。
ESP32这颗芯片做这类项目有几个天然优势。双核240MHz主频,跑FOC的电流环和速度环绰绰有余;自带WiFi和蓝牙,手机调参、联网上报温度都不用外挂模块;ADC、LEDC、MCPWM这些外设齐全,驱动三相桥和采样电流都够用。最关键的是生态成熟,Arduino框架下有SimpleFOC这个库,把FOC里最麻烦的坐标变换、SVPWM生成、传感器对齐都封装好了,不用从零推导克拉克变换和帕克变换。
无刷电机这边,我选的是带霍尔传感器的三相直流无刷电机,也就是常说的BLDC。相比无感方案,霍尔能直接给出转子扇区,启动不会失步,低速扭矩也稳。虽然多三根线,但对风扇这种需要从极低转速平滑起步的场景,霍尔方案的体验明显更好。这里要区分一下,BLDC的反电动势是梯形波,PMSM是正弦波,SimpleFOC对两者都支持,配置时选对电机类型就行。
整套方案的核心思路是:ESP32跑SimpleFOC做底层电机控制,上层用温度传感器采集环境温度,通过一个简单的映射算法把温度转换成目标转速,再叠加手机端通过蓝牙发来的手动调速指令。这样既保留了自动温控的便利,又能在想手动干预时随时接管。
1.2 硬件选型背后的取舍逻辑
驱动板这块我踩过坑。一开始图便宜买了那种基于L6234的三相驱动模块,标称3A,实际带这个电机到一半转速就烫得能煎蛋,而且没有电流采样输出,SimpleFOC只能跑开环速度模式,扭矩控制完全没法做。后来换成带INA240电流采样芯片的驱动板,虽然贵了一倍多,但电流环能跑起来,低速时电机不再有那种一顿一顿的脉动感。
电机参数方面,我用的这个电机是12V供电、额定电流1.5A、7对极、霍尔安装角度120度。7对极意味着电角度是机械角度的7倍,SimpleFOC配置极对数时填7。霍尔角度这个参数很关键,填错了电机要么不转要么抖动,后面会详细说怎么确认。
电源部分单独说一下。ESP32用USB供电或者5V适配器都行,但电机驱动必须用独立的12V电源,而且要和ESP32共地。我试过用同一个电源给两者供电,结果电机一启动ESP32就复位,原因是电机启动瞬间的电流冲击把电压拉低了。后来在12V输入端并了个1000uF的电解电容,情况好转很多,但最稳妥的还是分开供电、单点共地。
温度传感器用的是DS18B20,单总线数字输出,精度0.5度,比热敏电阻省心,不用校准。接在ESP32的一个GPIO上,加上4.7K上拉电阻就能工作。风扇本体就是普通交流风扇拆掉原来的电机和电容,把无刷电机固定在原来电机的位置,扇叶直接装到无刷电机轴上。这里要注意扇叶的动平衡,我第一版装上去震动很大,后来在扇叶和电机轴之间加了个橡胶垫圈做柔性连接,震动明显减小。
1.3 软件架构的分层设计
软件上我分成三层来写。最底层是SimpleFOC的电机控制层,负责电流采样、坐标变换、SVPWM生成和霍尔换向。中间层是控制策略层,包含温度到转速的映射、手动自动模式切换、加减速斜坡限制。最上层是通信层,蓝牙串口接收手机指令,同时把当前温度、转速、电流这些状态回传。
这样分层的好处是每层职责清晰,调试时能单独隔离问题。比如电机抖动,先看底层霍尔角度和极对数对不对;温度控制不线性,只动中间层的映射曲线;蓝牙连不上,只查通信层。我见过不少新手把所有这些逻辑揉在一个loop里,出了问题根本无从下手。
SimpleFOC的配置我放在setup里一次性完成,包括电机极对数、霍尔引脚、电压限制、电流采样参数、PID参数。loop里只做三件事:更新传感器、跑FOC循环、处理通信。这样loop的执行周期稳定,FOC的实时性有保障。实测下来loop周期能稳定在1ms左右,对于风扇这种惯性大的负载完全够用。
2. SimpleFOC核心配置与霍尔传感器对齐实操
2.1 霍尔传感器角度标定:最容易翻车的一步
霍尔传感器的角度标定是整个项目里最容易出问题的地方,我在这上面卡了整整一个周末。SimpleFOC需要知道霍尔三个引脚的电平组合对应的电角度,这个角度因电机而异,不能照抄别人的配置。
标定的原理是这样的:三相霍尔传感器在电机旋转时会产生6种有效的电平组合,每种组合对应一个60度电角度区间。SimpleFOC的HallSensor类需要你提供这6个区间对应的角度值。如果你不知道,可以用库自带的校准功能自动测出来。
具体操作是先把电机类型设为BLDCMotor,极对数设为7,然后在setup里调用motor.initFOC()之前,先让电机以开环方式缓慢旋转一圈,同时记录每个霍尔状态跳变时的电角度。SimpleFOC的例程里有find_sensor_offset_and_direction这个函数,直接跑一遍就能打印出6个角度值。
我实测下来,这个电机标定出的霍尔角度是0、60、120、180、240、300度,看起来是标准的120度安装。但如果你拿到的是60度安装的霍尔,角度序列会不一样。标定时电机必须空载,因为开环旋转扭矩很小,带载可能转不动导致标定失败。
注意:标定过程中如果电机只是抖动不转,先检查极对数是否填对。极对数填错是最常见的标定失败原因,7对极的电机填成1,电机只会原地抖。
标定完成后,把得到的角度值填入HallSensor的构造函数,同时确认hall_sensor.direction是1还是-1。方向错了电机也会转,但转速反馈是负的,速度环会失控。判断方法很简单,手动正向转电机轴,看串口打印的角度是递增还是递减。
2.2 电流采样与电流环参数整定
电流采样是FOC能做扭矩控制的前提。我的驱动板用的是INA240加采样电阻的方案,采样电阻0.01欧,INA240增益20倍,所以实际电流到ADC的转换系数是0.01乘以20等于0.2V每安培。ESP32的ADC参考电压3.3V,12位分辨率,所以每安培对应的ADC读数大约是0.2除以3.3乘以4095,约等于248。
SimpleFOC里配置电流采样需要设置gain和offset。gain就是上面算的248,offset是零电流时的ADC读数,这个必须在电机不转、驱动关闭的情况下测。我实测零电流时ADC读数在2048左右,但不同板子会有偏差,一定要自己测。
电流环的PID参数整定有个经验顺序:先把速度环和位置环的P和I都设0,只调电流环。电流环的P从0.5开始,逐步加到电机在阶跃电流指令下能快速响应但不振荡。我最后用的P是2.0,I是50,这个值跟采样频率和电机电感有关,不能照搬。
速度环的P和I在电流环调好后再加。风扇这种负载惯量大,速度环P可以给大一点,我用的是P=0.5,I=2。位置环这个项目用不到,因为风扇不需要精确角度控制,只需要转速。
实操心得:调PID时一定要用SimpleFOC Studio这个上位机工具,能看到实时波形,比盲调快十倍。串口打印数据再画图的方式太慢了,调一次参数要等半天。
2.3 电压限制与死区时间设置
motor.voltage_limit这个参数决定了电机能得到的最大电压。我的电机额定12V,但驱动板有压降,而且FOC的SVPWM调制会损失一部分电压利用率,所以实际设置成11V比较稳妥。设太高电机会发热,设太低高速时扭矩不够。
死区时间这个参数在SimpleFOC里叫dead_zone,默认是0.02,单位是占空比。如果你的驱动板上下桥臂切换时有直通风险,需要根据驱动芯片的开关时间算。我用的驱动芯片开关时间约500ns,PWM频率20kHz,周期50us,所以死区占空比至少是500ns除以50us等于0.01。设0.02留了一倍余量,实测驱动板温度正常。
PWM频率我选的是20kHz,刚好在 audible range 之上,电机不会有啸叫。频率再高开关损耗会增加,驱动板会热。频率太低会有可听噪声,风扇夜里转起来很烦人。
3. 智能温控策略与蓝牙调参实现
3.1 温度到转速的映射曲线设计
温度到转速的映射不是简单的线性关系。我一开始用线性映射,结果28度时风扇转速太低感觉不到风,30度时又突然很大。后来改成三段式映射,体验好很多。
具体是这样:低于26度不转,26到28度之间转速从0线性升到800RPM,28到32度之间从800升到2000RPM,32度以上直接满速3000RPM。这样在常用温度区间转速变化平缓,极端温度时又能快速降温。
映射曲线用查表加线性插值实现,表里存几个关键点,中间的值算出来。这样改曲线只需要改表,不用改代码逻辑。我用的关键点是(26,0)、(28,800)、(32,2000)、(35,3000),实测下来体感很自然。
转速变化还要加斜坡限制,不能突变。我设的是每秒最多变化500RPM,这样从静止到满速要6秒,不会突然一阵猛风吹过来。斜坡限制在代码里就是一个简单的限幅,每次loop计算目标转速和当前转速的差,超过限幅就只走限幅值。
3.2 蓝牙串口通信协议设计
蓝牙这块我用的是ESP32自带的经典蓝牙串口,手机端用任意蓝牙串口APP就能连。协议设计得很简单,ASCII字符串,每条指令以换行结尾。
指令格式是命令字:参数。比如M:1表示切到手动模式,S:1500表示手动模式下设转速1500RPM,A:0表示切回自动模式。ESP32收到后解析命令字,执行对应操作,然后回一个OK或者ERR。
状态回传是每秒一次,格式是T:28.5,R:1200,I:0.8,M:0,分别表示温度、转速、电流、模式。手机端解析后显示。这样调试时不用连串口线,手机就能看到所有状态。
注意:蓝牙串口和USB串口不能同时用同一个
Serial对象。ESP32有两个硬件串口,我用Serial做USB调试,Serial1接蓝牙,这样两边都能打印,互不干扰。
蓝牙连接稳定性方面,我实测在10米内无遮挡很稳,隔一堵墙会偶尔断连。断连后ESP32会自动回到自动模式,这是故意的设计,避免手动模式下一个指令丢失导致风扇一直全速转。
3.3 手动自动模式的平滑切换
模式切换最容易出的问题是切换瞬间转速突变。比如自动模式下转速1200,切到手动模式时手动设定值是3000,如果直接跳变,风扇会猛加速,体验很差。
我的处理方式是切换时把当前实际转速作为手动模式的初始目标值,然后手动设定值通过斜坡慢慢逼近。这样无论怎么切,转速都是平滑变化的。代码里就是切换时把target_speed设成current_speed,然后手动指令只改target_speed,斜坡逻辑照常工作。
自动模式切回来时也一样,先把目标转速设成当前转速,然后温度映射的结果再慢慢覆盖。这样两个模式之间切换完全无感。
4. 常见问题排查与避坑经验实录
4.1 电机不转或抖动的原因排查
电机不转或抖动是新手遇到最多的问题,我整理了一个排查顺序,按这个顺序查基本能定位到原因。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 电机完全不动,有嗡嗡声 | 霍尔线序接错 | 交换任意两根霍尔线,重新标定 |
| 电机抖动但不连续转 | 极对数填错 | 确认电机极对数,7对极填7 |
| 电机转但方向随机 | 霍尔方向参数错 | 手动转电机看角度增减,改direction |
| 低速抖动高速正常 | 电流环未调好 | 重新整定电流环PID |
| 启动瞬间ESP32复位 | 电源共地干扰 | 电机电源和ESP32电源分开,单点共地 |
霍尔线序这个坑我踩过。霍尔三根线A、B、C如果接错顺序,电机表现是抖动或者转得很奇怪。判断方法是看串口打印的霍尔状态,正常旋转时应该是1到6循环,如果顺序乱了就是线序问题。交换任意两根线再试,最多试6次就能找到正确顺序。
极对数这个参数,有些电机铭牌上写的是极数不是极对数。极数除以2才是极对数。比如铭牌写14极,极对数就是7。我见过有人把14直接填进去,电机完全失控。
4.2 转速波动与电流异常的解决
转速波动分两种,低频波动和高频抖动。低频波动一般是速度环P太小或者I太大,表现为转速周期性起伏。高频抖动是电流环问题,表现为电机发出高频噪声。
我遇到过一次转速在设定值上下200RPM波动,查了半天发现是温度传感器的读取周期和速度环周期冲突。DS18B20转换一次温度要750ms,我放在loop里同步读取,导致loop周期被拉长到750ms,速度环更新太慢。后来改成状态机方式,温度转换期间不阻塞loop,问题解决。
电流异常增大一般是电机堵转或者霍尔角度偏差大。如果空载电流就很大,先检查霍尔角度标定值。我标定时电机没固定好,转的时候晃动了,标出来的角度偏了十几度,导致电流比正常大了三倍。重新固定后标定,电流恢复正常。
避坑技巧:调试时一定要在电源回路串一个电流表,实时看电流。很多问题从电流上能提前发现,比如电流缓慢上升说明电机在发热,可能是PID参数太激进。
4.3 蓝牙连接不稳定与指令丢失处理
蓝牙断连这个问题,我试过几种改善方法。首先是把ESP32的天线区域远离电机和电源线,电机驱动的高频开关噪声对蓝牙干扰很大。我把ESP32用排线引出,离驱动板10厘米以上,断连频率明显降低。
指令丢失方面,我在协议里加了重传机制。手机端发指令后等200ms,没收到OK就重发,最多重发3次。ESP32端对重复指令做去重,相同指令在500ms内只执行一次。这样偶尔丢一两条也不会影响使用。
还有一个坑是蓝牙串口的缓冲区溢出。如果手机端发指令太快,ESP32来不及处理,缓冲区满了后面的指令就丢了。我在代码里每次loop只读一条指令,读完就处理,处理完再读下一条。这样虽然响应慢一点,但不会丢指令。
4.4 长时间运行的稳定性优化
连续跑24小时以上,我遇到过两次ESP32死机。一次是内存泄漏,蓝牙库的某个回调里动态分配了内存没释放。后来改成静态分配,问题消失。另一次是看门狗没喂,某个循环卡住了。加上esp_task_wdt_reset()定期喂狗后解决。
温度方面,驱动板连续跑满载,MOS管温度能到70度左右。我加了个小散热片,降到50度左右。ESP32本身温度不高,但要注意如果放在密闭盒子里,夏天可能会过热。我在盒子上开了几个通风孔,实测内部温度比环境高8度左右,可以接受。
电机连续运行后轴承会发热,我用的这个电机跑两小时后轴承位置有点烫手。后来在轴承处加了一滴润滑油,温度降了不少。长期运行的话建议选带滚珠轴承的电机,含油轴承不适合连续高速运转。
5. 完整代码结构与关键片段说明
5.1 项目文件组织方式
代码我分成四个文件:主程序main.ino、电机控制motor_control.h、温度控制temp_control.h、通信comm.h。这样改哪部分就打开哪个文件,不用在一个大文件里翻。
主程序里setup做初始化,loop做三件事:更新FOC、更新温度控制、处理通信。每个部分都有独立的更新周期控制,FOC每1ms跑一次,温度每1s更新一次,通信每10ms处理一次。用millis()做时间戳判断,不用delay(),保证loop不阻塞。
5.2 电机初始化关键代码
#include <SimpleFOC.h> BLDCMotor motor = BLDCMotor(7); BLDCDriver3PWM driver = BLDCDriver3PWM(25, 26, 27, 14); HallSensor sensor = HallSensor(34, 35, 32, 7); void setup() { sensor.pullup = Pullup::USE_EXTERN; sensor.init(); motor.linkSensor(&sensor); driver.voltage_power_supply = 12; driver.voltage_limit = 11; driver.dead_zone = 0.02; driver.init(); motor.linkDriver(&driver); motor.foc_modulation = FOCModulationType::SpaceVectorPWM; motor.controller = MotionControlType::velocity; motor.torque_controller = TorqueControlType::foc_current; motor.PID_current_q.P = 2.0; motor.PID_current_q.I = 50; motor.PID_velocity.P = 0.5; motor.PID_velocity.I = 2; motor.LPF_velocity.Tf = 0.01; motor.voltage_limit = 11; motor.current_limit = 2.0; motor.init(); motor.initFOC(); }这段里sensor.pullup设成外部上拉,因为我的霍尔板上有上拉电阻。如果你的霍尔板没有上拉,改成Pullup::USE_INTERN用ESP32内部上拉。LPF_velocity.Tf是速度低通滤波时间常数,设0.01秒能滤掉大部分噪声,设太大响应会变慢。
5.3 温度映射与斜坡限制代码
float tempToSpeed(float temp) { const float points[][2] = {{26,0},{28,800},{32,2000},{35,3000}}; if (temp <= points[0][0]) return 0; if (temp >= points[3][0]) return points[3][1]; for (int i = 0; i < 3; i++) { if (temp >= points[i][0] && temp < points[i+1][0]) { float ratio = (temp - points[i][0]) / (points[i+1][0] - points[i][0]); return points[i][1] + ratio * (points[i+1][1] - points[i][1]); } } return 0; } float rampSpeed(float target, float current, float maxDelta) { float delta = target - current; if (delta > maxDelta) delta = maxDelta; if (delta < -maxDelta) delta = -maxDelta; return current + delta; }rampSpeed里maxDelta我设的是0.5,因为loop周期1ms,每秒1000次,0.5乘以1000等于500RPM每秒,符合之前设计的斜坡速率。这个值可以根据实际体感调整,觉得加速太慢就加大。
5.4 蓝牙指令解析代码
void handleCommand(String cmd) { cmd.trim(); if (cmd.startsWith("M:")) { int mode = cmd.substring(2).toInt(); if (mode == 0 || mode == 1) { currentMode = mode; targetSpeed = currentSpeed; Serial1.println("OK"); } else { Serial1.println("ERR"); } } else if (cmd.startsWith("S:")) { if (currentMode == 1) { float s = cmd.substring(2).toFloat(); if (s >= 0 && s <= 3000) { targetSpeed = s; Serial1.println("OK"); } else { Serial1.println("ERR"); } } else { Serial1.println("ERR"); } } else { Serial1.println("ERR"); } }解析时先trim()去掉首尾空白,然后判断命令字。手动模式下才接受转速指令,自动模式下发转速指令会返回ERR,避免误操作。转速范围限制在0到3000,超出范围拒绝。
6. 实测效果与后续扩展方向
6.1 实际运行数据与体感评价
装好之后连续跑了三天,记录了一些数据。室温28度时自动模式稳定在800RPM左右,功耗约3.5W,比原来交流风扇的45W省了很多。室温升到31度时转速自动升到1800RPM,风量明显增大,但噪声只有42分贝左右,夜里睡觉不觉得吵。
手动模式通过手机调转速,从0到3000响应很跟手,斜坡限制让加速过程很自然。蓝牙连接在房间内很稳定,走到隔壁房间偶尔断连,但回到房间自动重连,不需要手动操作。
电机温度方面,连续跑满载两小时,电机外壳温度约45度,驱动板MOS管温度约55度,都在安全范围内。整机功耗满载约18W,比同风量的交流风扇省电一半以上。
6.2 可以继续折腾的扩展点
这个项目的基础框架搭好后,能扩展的方向很多。我接下来打算加一个红外接收头,学习原来风扇遥控器的编码,这样原来的遥控器还能用。ESP32的RMT外设做红外收发很合适,不占用CPU。
另一个想法是加个光照传感器,晚上自动把转速上限降低,避免吵到睡觉。或者加个人体传感器,房间没人时自动停机。这些传感器都是I2C或单总线接口,ESP32的GPIO够用。
联网方面,ESP32可以连MQTT服务器,把温度和转速数据上报,手机在外面也能看到家里风扇的状态。不过这个涉及网络配置,我打算单独开一个项目来做,避免把现在这个已经稳定的版本搞复杂。
代码我放在GitHub上了,包含完整的Arduino工程和接线图。接线图里标注了每个引脚的功能和注意事项,特别是电源部分要仔细看,共地没做好很容易出问题。电机参数和霍尔角度每个人用的电机不一样,需要自己标定,代码里留了标定模式的入口,长按板上的BOOT键进入。