1. 项目概述与需求拆解
做嵌入式这么久,陆陆续续折腾过不少基于各种芯片的小项目。今年把积灰的STM32F103C8T6蓝色Pill板翻出来,趁着整理桌面顺手做了个智能台灯控制系统。选这个题目倒不是图新鲜,主要因为台灯控制系统几乎把嵌入式开发里最常用的几大模块全占了:GPIO控制、PWM调光、ADC采集、传感器融合、状态机逻辑、显示交互。一套流程走下来,新手能积累完整的外设使用经验,老手也能借机整理一套自己的裸机代码框架。
先看这个系统到底要解决什么问题。传统台灯的痛点很明确:手动开关、亮度不可调或者只有几档、不管你在不在桌上就亮一整晚、环境光变了亮度也不会跟着变。智能化的核心就是让台灯具备自动感知和自动调节能力,具体拆解下来包含四层需求。
第一层是基础照明控制。用户能手动开关灯、能调节目标亮度。这在硬件上需要PWM驱动能力和至少两个输入按键。第二层是环境自适应。通过光敏传感器检测环境亮度,当环境光变暗时自动提亮,环境光充足时自动降低亮度,避免浪费电能也保护眼睛。第三层是人体检测。通过热释电红外传感器(PIR)判断当前座位是否有人,实现“人来自动亮灯、人走延时关灯”的联动逻辑。第四层是状态可视化。用户需要知道当前台灯处于什么模式、当前亮度百分比、环境光强度等信息,这就需要一个OLED小屏或者LED指示回路。
结合这四层需求,我用STM32F103C8T6作为主控,外接PIR人体红外模块、光敏电阻分压电路、两个独立按键、0.96寸I2C接口OLED屏幕,通过板载定时器输出PWM信号驱动一个高电平有效的LED灯板,另用一路串口预留调试输出。整个系统通过“自动模式/手动模式”切换组织逻辑,自动模式下优先响应PIR检测结果,参考环境光强闭环调节PWM占空比;手动模式下用户通过按键设置固定占空比,系统只是记录当前参数。
这个方案在技术选型上比较克制,没有引入WiFi模块或手机App,原因是智能台灯本身属于低成本、高可靠性的桌面设备。联网控制这类功能对台灯来说性价比偏低,反而会增加功耗和故障点。如果后续想拓展,可以在串口预留或新增蓝牙模块单独扩展,但核心调节逻辑不受影响。
我在设计之初也想清楚了系统承载的对象:它可以作为一款实际的桌面照明设备长期运行,同时也是一个很好的STM32综合训练项目。如果你正在学习STM32标准外设库的开发方式,或者想给工位做一个不依赖云端的智能照明装置,这篇内容值得你完整过一遍。
2. 硬件选型与电路设计要点
2.1 主控选择与板型确认
STM32F103C8T6是一颗Cortex-M3内核、主频72MHz、64KB Flash、20KB RAM的芯片。用在这类台灯项目上性能有大量富余,但恰好是因为富余,各个外设都能放开手脚跑,不像8位单片机需要精打细算。Flash和RAM足够容纳OLED驱动、PWM控制、传感器滤波运算外加UI状态切换逻辑。
我选的是蓝色Pill开发板,用板载8MHz晶振做HSE时钟源,配合内部PLL把系统时钟拉高到72MHz。这里有个小坑,很多蓝色板出厂默认用的是HSI内部8MHz时钟,程序下载后能跑,但PWM频率和ADC采样周期都会偏离预期。我在项目初始化里明确配置了时钟树,从HSI切到HSE再倍频到72MHz,后续所有定时器分频参数才有准确的计算基础。
电源方面我用USB 5V供电,板载AMS1117-3.3稳压到3.3V给MCU和传感器逻辑供电,LED驱动回路里串联限流电阻后直接用PWM引脚控制一个MOS管开关。因为C8T6的GPIO灌电流能力有限,最好不要直接把多颗大功率LED串在IO口上。用MOS管做二级驱动是一个更稳妥的做法。
2.2 光敏采集电路设计
环境光的采集,我采用CdS光敏电阻加电阻分压的方案。光敏电阻与一个10K固定电阻串联,中间抽头接STM32的ADC输入引脚。光敏电阻的阻值随光照强度变化,光照强时阻值下降,分压点电压升高;光照弱时阻值变大,电压降低。这样ADC读到的数值和环境光照强度之间有一个单调映射关系。
这个电路的关键参数其实不太需要精确计算,因为光敏电阻的阻值范围离散性较大,同一型号个体差异可以达到20%以上。但有几个原则要把握:
- 分压固定电阻取值要和光敏电阻在典型光照下的阻值处于同一数量级。一般室内光照下光敏电阻阻值在5K到20K之间,用10K固定电阻能得到较大动态范围。
- ADC采集引脚建议配置为模拟输入模式,不要开启上拉或下拉。
- 如果光敏电阻离MCU较远,采样线上应加一个0.1uF电容,滤掉高频噪声。
实际调试时发现ADC原始值波动不小,即使光线稳定,相邻两次采样也可能差出几十个数。原因主要是工频干扰和光敏电阻自身的响应噪声。我做了两层处理:第一层是连续采样20次取平均,第二层是对平均结果再做一次滑动滤波。整体下来大约50ms才能稳定反映光强变化,对台灯这种慢时变对象完全够用。
2.3 PIR人体传感器接入要点
PIR传感器模块是目前市面上最常见的HC-SR501那种,输出是三线接口:VCC、GND和数字信号OUT。通电后如果检测到人体移动,OUT引脚输出高电平,持续一段时间后恢复低电平。模块背后有两个电位器可以调节灵敏度和延时时间,另外还有一个跳线帽选择触发模式。
这里有个很关键的设计细节:HC-SR501模块的回滞区间大约有2秒,也就是说一个人坐在桌前不动,两三秒后输出就会落回低电平。如果只是简单检测“输出高就是有人在”,那台灯会在人静止时自动关灯,体验非常糟糕。传统解法是用MCU做锁存逻辑——一旦检测到PIR拉高,就在软件里维持一个“有人状态”一段时间,比如30秒,期间即使PIR引脚变低也不改变状态,只有超过这30秒没有任何新的PIR触发,才判定“人已经离开”。
另一种方案是改用红外对射或微波雷达模块,但成本和角度覆盖都不一样。在桌面台灯场景下,PIR加软件锁存已经能满足绝大多数情况。我把锁存时间设计成了可配置参数,默认30秒,如果用户希望台灯在短暂离开时也不熄灭,可以在代码里修改变量自行调整。
PIR模块给MCU的信号是3.3V兼容的数字电平,直接接GPIO输入没有问题。要注意模块上电后有一段时间的初始化周期,大约30到60秒内输出可能不稳定,所以程序启动时要加一个“稳定延时”后再开启PIR检测逻辑,否则灯会莫名闪动几次。
2.4 OLED显示与交互接口
显示部分用0.96寸I2C接口OLED屏,SSD1306控制芯片,4个引脚分别是VCC、GND、SCL、SDA。STM32F103的I2C外设支持100K和400K两种速率,但这一系列芯片的硬件I2C有时序兼容性方面的问题,很多人干脆用软件模拟I2C。我在这个项目里也选了软件模拟I2C,用两个普通GPIO口用延时翻转的方式产生SCL和SDA时序。
软件模拟有好处,不占硬件I2C外设,引脚随意选,时序自己可控,调试起来直观。缺点是会占用一点CPU时间,但对OLED这种刷新频率不高的场景无压力。我配置了全屏刷新频率为5Hz,也就是每200ms重绘一次,实际看起来画面流畅,也不会影响PWM输出和传感器轮询的实时性。
OLED显示内容规划成两层界面:主界面显示当前模式(自动/手动)、PWM占空比百分比、环境光ADC原始值、人体检测状态;短暂按下“模式键”可以切换显示当前温湿度等扩展信息(如果接了DHT11)。长按模式键切换自动/手动模式。
按键部分用了两个独立按键,一个负责亮度和模式调节,一个负责确认和界面切换。按键输入都做了软件消抖,消抖时间设置为20ms,因为机械按键的抖动时间一般在5~10ms,20ms可以稳妥过滤。如果使用中断方式触发按键则需要更仔细的防抖处理,我在这次开发中使用的是主循环扫描方式,结构简单,不容易出现时序纠缠。
2.5 LED驱动与PWM输出
PWM调光通常使用定时器的PWM输出模式。在STM32F103上,我选用TIM2的CH1通道作为PWM输出引脚,配置为复用推挽输出模式,输出极性可选高电平有效或低电平有效。因为我的LED灯板是共阳极高电平驱动方式,于是把PWM极性设为高电平有效。极性设置错误会导致亮度反相,低占空比时反而最亮,这个细节容易踩坑。
频率选择方面,台灯PWM频率不宜太低,否则LED会产生肉眼可见的闪烁。人眼对低频闪烁的敏感范围大概在50~100Hz区间,超过1KHz基本无感知。我配置PWM频率为2KHz,兼顾了无频闪和定时器分频便利性。通过设置TIM2的预分频器为72分频,计数频率变成1MHz,设置自动重装载值为500,就得到2KHz的PWM频率。占空比由比较寄存器CCR决定,CCR取值范围0到500对应0%到100%。
LED亮度与占空比的关系从物理上看是近似线性的,尤其是使用恒压驱动的LED灯串时,占空比和平均电流成正比。但也有一个细节需要注意,LED在低占空比下发光效率略微下降,而且人眼对暗部变化的敏感度比亮部变化更高。如果直接把0~100%的占空比线性映射到亮度参数,人眼会觉得低亮区的调节跨度太大、高亮区的调节跨度太小。所以我加了一段简单的Gamma矫正映射,把用户设置的“感知亮度”通过一个二次曲线映射到实际占空比。矫正公式是:
实际占空比 = 目标感知亮度的平方 / 100,目标感知亮度取值范围0到100。
这个公式简单粗暴但有效。比如用户设置感知亮度为50%,实际占空比就是50×50/100=25%;感知亮度为80%,实际占空比是64%。这样从视觉上看起来调光更均匀。这个映射只用在手动模式和自动模式的亮度输出端,不影响ADC和环境光采集计算。
3. 软件架构与状态机设计
3.1 主循环框架
整个系统的软件我采用前后台结构,后台是中断服务,前台是一个大轮询主循环。相比RTOS,这个项目任务量不大,RTOS反而会增加任务切换开销和调试复杂度。裸机大循环在资源占用和代码可读性上更符合项目定位。
主循环的执行顺序大致分为五步:第一步轮询按键扫描,更新按键状态机;第二步读取PIR状态并更新人体逻辑状态;第三步读取ADC光敏值并做滑动滤波;第四步根据当前模式计算目标PWM占空比;第五步更新OLED显示。每步的函数都确保不会阻塞过长时间,整个循环周期大约在20ms到30ms之间。
这里特意把按键扫描放在最前面,避免按键响应被后续耗时操作拖慢。OLED刷新放在最后,因为显示耗时相对最长,放到最后即使拖慢一点,下一轮循环也会先处理按键,交互的实时性体验更好。
3.2 人体检测状态机
人体检测状态机的推进逻辑如图1所示。初始状态是“无人”。当PIR引脚为高电平时,状态跳转到“检测到人”,同时设置一个标志位,表示有人存在。此时如果台灯在自动模式,则立刻点亮。进入“检测到人”状态后,主循环里每一轮都检查是否超过设定的锁存时间。只要期间PIR再次输出高电平,就刷新锁存计时器。只有持续静默超过30秒,才判定“人已离开”,状态跳回“无人”。
这个锁存设计规避了PIR静止信号消失的问题。我最初只用PIR电平直接控制灯,结果每次坐下来就看灯亮灭交加。后来加了状态机,彻底解决了这个体验问题。
人体状态机还需要和自动/手动模式做交互。自动模式下,人体检测是开灯和关灯的主控条件。手动模式下,PIR检测结果就不控制灯的开关,只在OLED上显示“有人/无人”,供用户了解座位状态。这样分工明确,逻辑不混乱。
3.3 自动调光策略
自动模式下,台灯的PWM占空比由环境光ADC值经过映射函数计算得出。设计调光策略时我参考了一个直觉性的逻辑:环境光越暗,台灯应该越亮;环境光越亮,台灯可以适当降低亮度。但这里有个需要权衡的点:如果完全反相映射,当天色很暗时台灯会直接拉满至100%,虽然满足“补光”,但在长时间阅读场景下可能过亮刺眼。反过来,如果映射曲线的斜率太小,环境光强度变化时亮度变化不够明显,用户感知不到“自动调节”的意义。
我设计的映射关系是一个带饱和区的分段线性函数:把ADC基准范围切分成三段,低于阈值(环境很暗)时PWM输出恒定为80%;在中间区间,PWM随ADC值线性下降;高于阈值(环境很亮)时PWM输出最低为15%左右,不再继续下调。这样既保留了自动调节的可见效果,又不会让灯在极暗环境里过亮,或者在极亮环境里完全熄灭。
具体映射参数可以在配置头文件里调整,我用一组宏定义管理,后续想改变灵敏度只需要改几个数字。比如“暗光阈值”初始设置是ADC值低于800时进入恒定高亮区;“强光阈值”初始是ADC值高于2400时进入恒定低亮区。这个项目里12位ADC满量程是4095,室内白天的桌面环境光读数大约在2000以上,夜间开台灯时约500~900。
值得注意的是光敏电阻放的位置不能离LED灯板太近,否则灯自身的光线会反馈到传感器,形成正反馈:灯越亮、传感器读数越高、系统判断环境光已经足够、于是下调亮度,亮度下降后传感器读数变低、系统又提亮,来回振荡。这个问题我调了一个晚上才定位到,最后通过物理上把光敏电阻引到灯罩外侧并屏蔽灯珠直射方法解决。软件里也加了变化速率限制,PWM每次调节的步长不超过2%,进一步抑制振荡。
3.4 按键状态机与交互逻辑
按键处理虽然简单,但如果处理不当容易产生误触和重复触发。我定义了三个按键事件:短按、长按、双击。短按触发条件是按下后20ms消抖确认,释放时间在500ms以内;长按触发条件是持续按下超过1秒;双击是两次短按时间间隔在300ms以内。
由于主循环扫描间隔在20~30ms,无法做到精确实时检测硬件边沿,但状态机可以把短按、长按、双击区分得比较靠谱。实际代码里我用了一个结构体保存按键状态,包括当前电平、上次电平、按下计时、释放计时、事件标志。核心逻辑并不复杂,只要保证每次轮询时状态迁移完整即可。
按键功能约定:模式键短按切换OLED显示页面,长按切换自动/手动模式;亮度键短按减小亮度,长按增大亮度,双击恢复默认亮度。这套交互规则简单清晰,不需要说明书也能摸索出来。
4. 核心代码实现细节
4.1 时钟与GPIO初始化
拿到开发板后第一步是配置系统时钟。使用标准外设库方式,我写了一个时钟初始化函数,使能HSE外部晶振,等待起振稳定后通过PLL倍频到72MHz,再把AHB、APB1、APB2的分频系数配置好。注意APB1上的定时器时钟需要额外注意,因为APB1预分频器不为1时,定时器时钟会翻倍。STM32F103中APB1预分频为2,定时器时钟才等于36MHz×2=72MHz,很多初学者在这里算出PWM频率不对就是漏了这个细节。
GPIO初始化把所有用到的引脚统一配置:
- PWM输出引脚配置为复用推挽输出,速度50MHz。
- 光敏采集引脚配置为模拟输入。
- PIR引脚配置为上拉输入。
- 按键引脚配置为上拉输入。
- OLED的SCL和SDA配置为开漏输出,并外部加上拉电阻;软件模拟时不依赖硬件I2C外设,因此普通推挽输出也能工作,但开漏方式更符合真实I2C总线规范。
上拉输入保证按键未按下时读到高电平,按下后接地读到低电平。有些按键模块默认是下拉输入,对应程序逻辑要取反,这个要看具体硬件连接,不能一概而论。
4.2 PWM初始化与动态调光
配置TIM2的PWM输出有一个比较完整的操作序列:开启TIM2时钟,配置GPIO复用功能,设置TIM2的预分频器和自动重装载寄存器,设置捕获比较模式为PWM模式1,设置输出极性,使能捕获比较输出和TIM2主输出。标准库中对应的函数流程比较清晰,关键在于PWM频率的计算公式:
PWM频率 = 定时器时钟频率 / (预分频值 + 1) / (自动重装载值 + 1)
工程要求2KHz频率,定时器时钟72MHz,可以把预分频值设为71(实际七十二分频)、自动重装载值设为499(即500计数周期)。计算公式为72MHz/72/500 = 2000Hz,准确命中。改变占空比只需修改TIM2->CCR1的值,不需要重新初始化定时器。
我用一个变量current_duty表示当前实际输出的PWM比较值,所有逻辑层(自动调光、手动调节、伽马映射)计算出来的目标值都不会直接写入寄存器,而是通过一个渐变更新函数,每10ms把current_duty向目标值靠近一步,每次最多调整2%的量程。这样不仅视觉上更顺滑,也避免PWM突变对LED灯板造成电流冲击。
LED亮度在PWM调节时有轻微声音噪声,这个现象在一些质量一般的LED驱动电路里会出现,原因是PWM频率与电源开关频率产生了可闻噪声的拍频。把PWM频率从2KHz调到20KHz以上可以避免,但人耳对高频不敏感区,不过某些电源模块对20KHz以上PWM响应会变差。这个项目里LED灯板上串联了限流电阻,2KHz工作正常,没有噪音问题,所以保持2KHz不变。
4.3 ADC光敏采样与软件滤波
STM32F103的ADC是12位逐次逼近型ADC,满量程0~4095。我选择ADC1的通道4对应的GPIO引脚作为模拟输入。接着配置ADC为独立模式、右对齐、单次转换模式。因为只采样一个通道,不需要开启扫描模式。
连续采样20次取平均的方式在代码中实现起来很简单:起一个循环20次,每次启动ADC转换并等待转换结束标志位,然后累加结果。20次采样总耗时受ADC采样周期影响,我配置采样周期为55.5个ADC时钟周期,再加上转换本身大约12.5个周期,单次最快约2.5微秒,20次也才50微秒级别,可以忽略不计。但实际环境中光敏电阻信号波动远不是ADC变换时间导致的,做好软件滤波更重要。
软件滤波我采用了一阶低通滤波算法,公式是:
滤波值 = (新采样值 - 旧滤波值) × 滤波系数 + 旧滤波值
滤波系数取值0到1之间,这里用0.3作为经验值。系数越大响应越快,噪声抑制越差;系数越小越平滑,但反应迟钝。台灯环境光变化本身是缓慢过程,0.3的系数在50ms采样间隔下能获得不错的效果。
另外ADC的参考电压使用的是VCC(3.3V),如果想提高精度可以外接高精度基准源,比如REF3030,但对台灯项目完全没必要。板载LDO输出的3.3V精度在±2%以内,能满足光敏采集需求。
4.4 OLED显示驱动集成
SSD1306的驱动我从零写了一遍,因为网上代码很多但质量参差不齐,自己写一遍能透彻理解I2C时序、显存结构和页寻址模式。OLED屏以页为单位,共8页,每页128个像素宽8像素高,往对应页写入两个字节的数据(列地址、数据)即可控制每个点。我把整个显示内容放到一个128×8字节的局部显存数组里,需要更新时先把内容绘制到数组,再一次性通过I2C刷新整个数组到屏幕。
这种帧缓冲方式的好处是避免逐点写屏导致的闪烁,而且绘制逻辑和硬件解耦。绘制文本和图形时,我用了一个内置的5×7点阵ASCII字库实现基本字符显示。OLED显示汉字会比较占空间,所以界面上优先用英文缩写和数字展示,避免字库过大消耗Flash。
关键参数管理放在结构体里,包含当前模式、用户感知亮度、实际PWM占空比、环境光滤波值、人体状态、页面索引。主循环里凡是需要修改状态的地方都直接修改结构体,显示模块读取结构体内容渲染界面。这样逻辑清晰,后续新增传感器或功能时不需要大面积改动。
4.5 串口调试与日志输出
开发阶段串口调试功能特别有用。USART1接USB转串口模块,通过printf重定向到串口,实时打印系统状态。我配置波特率115200,8N1格式,重定向fputc函数后就能直接调用printf。
实际开发中打印太多会影响实时性,所以我在调试阶段保留了一路调试日志,输出内容包括当前环境光ADC值、PIR状态、模式、目标PWM和实际PWM。量产或正常使用阶段可以用宏开关关闭串口调试,节省CPU资源。这个项目里调试日志默认开启,实测对主循环耗时影响很小。
5. 常见问题与排查技巧实录
5.1 PWM输出没有信号
新焊接的板子测试时PWM引脚量不到波形,这类问题一般从三个层面排查。先看GPIO配置是否正确,复用推挽是不是设置对了。再看TIM2有没有开启主输出使能,标准库中TIM_CtrlPWMOutputs这个函数容易漏掉,漏了以后比较器有输出但引脚始终为低或高。最后检查PWM极性,如果极性配反,输出的波形是反相PWM,接高电平有效的LED灯板时会发现亮度逻辑完全相反。
调试这类问题最有效的工具是示波器。没示波器的话,可以直接在初始化后写一个固定50%占空比,用万用表测量引脚的平均电压,大约应该是VCC的一半。如果平均值接近VCC或GND,说明输出通道可能没配置好。
5.2 ADC读数跳变过大
ADC数值在光线稳定时仍有较大跳动,首先检查采样引脚是否悬空或接触不良。其次确认参考电压VREF+有没有外部噪声,给参考电压引脚并一个0.1uF去耦电容能明显改善。还有一个容易被忽略的问题是ADC引脚和PWM引脚靠近时,PWM高频开关噪声会串扰到ADC采样线上,解决办法是尽量拉远模拟和数字走线间距,并在采样点加大滤波电容。
软件层面的平均和低通滤波虽然能压住波动,但会降低响应速度,所以硬件上消除干扰源更关键。
5.3 PIR人感应失灵
传感器装好之后发现坐下后灯不亮或者人站起来走动时灯才亮,这是典型的PIR特性问题。PIR模块只能检测移动中的人体,静止坐姿很容易就进入静默态,因此软件锁存必不可少。另一个容易踩的坑是模块上电初期的稳定期,如果代码一上电就立即判断PIR输出,可能会被初始电平错误触发。应在初始化时延时60秒或等待一个PIR稳定标志后再启用检测逻辑。
PIR模块的灵敏度电位器也不是越大越好,调到最大时很容易被窗外移动物体或暖气气流误触发。调试时我用螺丝刀小幅度旋转,找到一个既能响应正常走动又不会误报的中间位置。环境温度高时PIR灵敏度也会变化,夏天和冬天可能需要微调。
5.4 台灯亮度自动振荡
自动模式下灯光出现周期性亮暗振荡,基本就是光敏电阻受到了自身灯光的反馈影响。解决思路有两个方向:一个是在硬件上把光敏电阻遮蔽起来,只接收环境光不接收灯光直射;另一个是软件上限制PWM步进速率、加滞回区间,比如到达目标亮度后的1秒内不再更新输出,避免微小的ADC波动引发亮度反复调整。
滞回区间的实现也很简单,在目标PWM和当前PWM的差值小于某个阈值时,直接认为已经收敛,清零积分项,不再输出调整。实测下来这条逻辑很有效,台灯在稳定环境光下调节一次后能保持长时间稳定。
5.5 按键触发不稳定
机械按键在按下和释放的瞬间,引脚电平会抖动,如果不做消抖,主循环可能在一次按下时记录多次事件。软件消抖的要点是确认电平状态稳定后才算有效事件。我的做法是连续检测三次(每次间隔20ms)都读到相同电平才更新状态。如果用了外部中断触发方式,消抖更麻烦,因为中断可能在抖动期间被反复触发,需要配合定时器延后处理,比轮询复杂不少。对于项目里的两个按键,轮询消抖足够。
长按检测需要在按下期间持续计时,期间不能因为抖动把按下状态清掉。我的处理方式是在消抖确认按下后,只要读取到引脚仍为低电平就持续累加计时,检查是否达到1秒阈值。释放时根据累计时长判断是短按还是长按事件。
5.6 程序下载失败
蓝色Pill板下载程序偶尔会遇到连接失败的情况,常见原因是BOOT0跳线位置不对或者板载串口芯片的驱动冲突。F103的BOOT0拉高是进入ISP下载模式,拉低是从Flash启动。我用串口ISP方式下载时BOOT0接高,下载完成后自动跳到低。如果电路里有其他外设占用了USART1引脚,也会干扰ISP通信,这时使用ST-Link的SWD接口就能绕开这个限制。建议有调试器的直接使用SWD接口,下载稳定且支持断点调试,效率远高于串口ISP。
6. 个人操作心得与扩展思路
这次做完整套系统后,我个人最大的体会是“自动化台灯的核心不在硬件多复杂,而在状态机设计和反馈稳定性”。PWM、ADC、OLED这些模块每一个单独拿出来都不难,但把光、人、按钮三个输入源协同组织起来,让它们在各种组合状态下都表现良好,才是这个项目真正花时间的部分。
硬件上有一个值得推荐的备选方案:用一颗W25Q64外部Flash存储配置参数,断电后记忆用户上次设置的亮度和模式。这个项目的参数目前是写死在Flash里的,掉电用默认值,对台灯来说影响不算大,但如果你打算做成产品原型,记忆功能还是挺有必要的。
另一处可以扩展的方向是增加环境光变化曲线的学习功能。比如记录用户在不同时间段手动设置的亮度,自动模式下根据时间段预测目标亮度,这样补光策略会更符合每个人的习惯。F103的Flash空余量足够存储一周的曲线数据,实现起来不会太难。
扩展联网功能时要注意安全,不要把台灯控制直接暴露在公网,建议通过局域网MQTT协议接入本地智能中枢,控制指令全部在内部网络完成,这样稳定性和隐私性都好很多。
如果你是自己动手做,我建议从硬件焊接开始,尽量把模块间的连线整理干净。台灯控制板不算复杂,但如果飞线乱成一团,调试时排查问题会很痛苦。我这次所有模块都是插在面包板上联调通过后才转移到洞洞板上焊接,这个过程里顺便验证了每个引脚的信号完整性,对后续稳定运行帮助很大。
最后补充一个实用小技巧:调试自动调光逻辑时,不要用眼睛盯着灯看,而是把实际PWM值打印到串口,用数据曲线观察亮度变化趋势。眼睛容易因为视觉暂留效果产生错觉,数据不会骗人。我调试振荡问题时就是靠串口输出的PWM数值波形发现振荡周期的,准确率比肉眼判断高太多。
这个项目做完后放在桌边实际用了半个多月,每天白天自动压低亮度、晚上自动补光、短暂离开后自动熄灭再回来延时点亮,体验稳定。如果你也想做一个类似的桌面智能设备,希望上面的设计思路和踩坑记录能帮你少走几条弯路。动手之前把需求拆清楚,动手过程中把状态机划分清楚,收尾时把反馈回路调稳,一个实用的嵌入式小项目自然就出来了。