STM32光敏电阻智能照明控制系统C语言实现

发布时间:2026/9/16 19:10:57
STM32光敏电阻智能照明控制系统C语言实现 简介基于STM32F103C8T6与光敏电阻传感器的智能照明控制系统完整毕业设计源码面向嵌入式、物联网或计算机相关专业学生及开发者可直接用于课程设计、期末大作业或作为STM32实战入门参考。压缩包共1006个文件以C源程序、头文件、汇编启动文件为主另含STM32CubeMX配置文件、IAR/Keil工程文件、链接脚本及编译产物hex/axf整体体积约18.09MB。该项目经严格调试评审分达96分以上已有310人学习浏览运行可靠性较高。资源内置ARM官方DSP数学库文件便于扩展信号处理能力源码中光敏电阻采样、ADC转换与控制逻辑相互配套并附PDF/Markdown说明文档适合对照理解硬件配置与光强阈值判断等关键实现。整套工程目录结构规范能帮助使用者减少重复造轮子的时间快速完成平台搭建与功能验证。1. 用C语言把光敏电阻传感器接到STM32F103C8T6上智能照明控制系统从哪开始把一盏LED台灯放到窗边白天不亮天色一暗自动点亮人走过去挡了光灯又自己熄灭。这就是标题里那套基于C语言、以STM32F103C8T6为主控、用光敏电阻传感器感知光照的智能照明控制系统。它不算复杂却恰好覆盖ADC采集、数字滤波、阈值判断、PWM调光和串口调试这串嵌入式基本功所以常被选作毕业设计题目。反直觉的是这个项目最难的不是硬件接线而是C语言里对ADC数据的处理。光敏电阻输出有噪声拿一次采样结果就去判断开灯关灯灯会在临界光照下抖个不停。下文从采样电路讲到滤波、滞回阈值和调光最后落到串口标定与答辩演示。无论你是准备开题的在校生还是想快速验证一套光控方案的嵌入式工程师这套思路都能直接搬走。2. 光敏电阻传感器的分压采样电路与STM32F103C8T6 ADC引脚配置2.1 光敏电阻的暗阻、亮阻与分压网络计算常见的光敏电阻比如5506/5516阻值随光照增强而下降这是CdS光敏材料的基本特性。在完全黑暗的环境里阻值可能升到200kΩ以上在晴天窗边可能掉到5kΩ以下。这种变化虽然不线性但做照明控制的分档判断足够用。常见做法是把光敏电阻和一个固定电阻串联把阻值变化转成电压变化再接到STM32F103C8T6的模拟输入引脚。电路接法很直接3.3V接到光敏电阻再到分压点经过固定电阻到GND分压点接PA1。光敏电阻放上面、固定电阻放下面时分压电压随光照增强而升高公式是V_adc 3.3 × R_fixed / (R_light R_fixed)。固定电阻选10kΩ比较合适暗光时分压点电压能压到0.3V以下强光时又能升到2.5V以上12位ADC可以分出上千级亮度变化。如果固定电阻选1kΩ暗光下电压几乎为零强光下也只有零点几伏整个动态范围被压缩得很小调光就没法做。这个分压参数直接关系到“智能”二字。要是只做最简单的光控开关用LM393比较器模块的DO口也能完成但DO只能输出高电平或低电平无法区分黄昏、阴天、开了一盏台灯这种中间光强。要支持多档调光就必须读模拟量而模拟量的动态范围由固定电阻决定所以在这上面多花两分钟换算后面能省下一堆调试时间。2.1.1 传感器模块选型模拟输出还是数字输出市面上常见的“光敏电阻传感器模块”分两类不带比较器的单纯分压模块以及带LM393、同时输出AO和DO的模块。毕设买后者更省事用AO口接STM32的ADCDO口可以留作附加的报警阈值。千万别直接用数字输出当唯一信号源否则后面想加调光功能就得重新改电路。这里有个容易踩的坑如果模块用5V供电AO输出最高可能接近5V而STM32F103C8T6的ADC引脚并不是5V容忍引脚接上去可能损坏MCU。模块尽量用3.3V供电信号范围才能稳稳落在ADC的参考电压以内。插拔杜邦线时先给板子断电再动信号线也是实验室里最常见的损坏原因。提示光敏电阻分压网络的内阻通常在几十kΩ级别ADC采样保持电容偏小会让读数偏低。后文把采样时间调到55.5周期就是针对这种高内阻信号源做的补偿。2.2 STM32F103C8T6最小系统板的引脚分配与原理图对照STM32F103C8T6最小系统板俗称蓝板把芯片引脚基本全部引出做这个项目不需要复杂开发板一块C8T6核心板加几根杜邦线就够了。分配引脚时对照最小系统板原理图看重点避开BOOT0、NRST和晶振占用的引脚其余按外设功能分配。信号引脚外设作用环境光模拟量PA1ADC1_IN1采集光敏电阻分压电压PWM输出PA6TIM3_CH1控制LED亮度调试串口TXPA9USART1_TX打印ADC值和状态调试串口RXPA10USART1_RX预留串口指令模式切换按键PA0GPIO输入自动/手动模式切换选PA1做ADC是把PA0留给按键因为PA0带有唤醒功能中断配置更灵活。PA6做PWM是因为TIM3通道1映射到PA6同一个定时器的通道2在PA7后续如果想加一路暖光LED直接用PA7做第二路两个通道共用一个定时器频率天然一致。驱动板上LED时串联一个220Ω到1kΩ的限流电阻电流控制在5到10mA。如果要驱动大功率灯带必须经过三极管或N-MOS管放大直接拿PA6接继电器并不稳妥感性负载上还要并联续流二极管。2.3 用CubeMX配置ADC后生成的C语言初始化代码现在做STM32开发用STM32CubeMX生成初始化代码是主流生成的是HAL库C语言工程想改成寄存器版也容易。在CubeMX里选择芯片STM32F103C8TxSYS里打开Serial Wire调试口避免烧录一次后SWD被禁用然后开启ADC1的IN1通道参数按下述代码配置。static void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig {0}; hadc1.Instance ADC1; hadc1.Init.ScanConvMode ADC_SCAN_DISABLE; /* 只采一个通道 */ hadc1.Init.ContinuousConvMode ENABLE; /* 连续转换 */ hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; /* 右对齐读回0~4095 */ hadc1.Init.NbrOfConversion 1; HAL_ADC_Init(hadc1); sConfig.Channel ADC_CHANNEL_1; /* PA1对应通道1 */ sConfig.Rank ADC_RANK_CHANNEL_NUMBER; sConfig.SamplingTime ADC_SAMPLETIME_55CYCLES_5; /* 采样周期拉长 */ HAL_ADC_ConfigChannel(hadc1, sConfig); }代码逻辑上ScanConvMode必须为DISABLE因为只做单通道采集ContinuousConvMode打开后可以反复读取最新转换结果省去每次启动转换的开销。右对齐让结果落在0到4095之间左对齐在阈值比较时还得移位没必要。SamplingTime选55.5周期就是为了适配光敏电阻分压网络的高内阻采样时间不足会导致读数整体偏低且不稳定。生成代码后再写一个封装好的读取函数uint16_t read_light_raw(void) { HAL_ADC_Start(hadc1); /* 启动一次转换 */ HAL_ADC_PollForConversion(hadc1, 10); /* 等待完成超时10ms */ uint16_t value HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); /* 停止下次Start重新采样 */ return value; }PollForConversion的第二个参数是超时时间10ms对ADC转换来说绰绰有余但因为后文要在主循环里按20ms周期持续调用这个函数执行时间越短越好任务调度才不会被阻塞。接线正确时函数返回值会随光照在0到4095之间变化。这里特别提一句ADC通道号并不总是等于引脚号PA1对应的是ADC_CHANNEL_1必须对照数据手册确认CubeMX的Pinout视图里看到PA1被标记为ADC1_IN1再生成代码比事后对着报错猜要快。3. 用C语言实现采样滤波、滞回阈值与PWM调光控制3.1 去极值滑动平均滤波的C语言实现直接拿单次ADC读数做控制一定会抖动。原因有两个光敏电阻本身对光线的响应有毛刺灯管频闪会被它捕捉到ADC参考电压内部的噪声也会叠加在采样结果上。工程上很少用纯单次采样常见做法是连续采16次去掉一个最大值和一个最小值再对剩下14个求平均这就是去极值平均滤波。#define ADC_FILTER_NUM 16 uint16_t get_light_filtered(void) { uint32_t sum 0; uint16_t min_val 4095, max_val 0; uint16_t i; for (i 0; i ADC_FILTER_NUM; i) { uint16_t adc read_light_raw(); sum adc; if (adc min_val) min_val adc; if (adc max_val) max_val adc; } sum - min_val max_val; /* 去掉最高和最低 */ return (uint16_t)(sum / (ADC_FILTER_NUM - 2)); }这段代码把16次原始采样压缩成一个相对稳定的代表值。去掉极值能抵抗偶发毛刺但对连续几个周期的同向漂移无能为力所以后面还要配合状态机。ADC_FILTER_NUM取16即可取太多响应太慢人从光敏电阻前走过时灯要过一两秒才反应取8又压不住高频尖峰。采样周期建议压在20到50ms不必每个主循环都跑一遍滤波否则CPU时间全耗在ADC上。3.2 滞回比较状态机避免临界闪烁有了滤波值最直接的写法是电压低于阈值就开灯、高于阈值就关灯但这会造成抖动。傍晚环境光缓慢下降时ADC值会在阈值附近反复穿越LED随之频闪。解决办法是引入滞回区间开灯阈值设低一点关灯阈值设高一点两者之间的区间保持上次状态不变振荡就被切断了。光照电压越高表示环境越亮灯该关闭电压越低表示环境越暗灯该打开。设关灯阈值为1.4V开灯阈值为0.9V对应12位ADC读数约1117和1737写成状态机typedef enum { LIGHT_OFF 0, LIGHT_ON 1 } light_state_t; light_state_t light_run_auto(uint16_t adc) { static light_state_t state LIGHT_OFF; const uint16_t on_th (uint16_t)(0.9f / 3.3f * 4095); /* 1117 */ const uint16_t off_th (uint16_t)(1.4f / 3.3f * 4095); /* 1737 */ switch (state) { case LIGHT_OFF: if (adc on_th) { /* 环境够暗才点亮 */ state LIGHT_ON; } break; case LIGHT_ON: if (adc off_th) { /* 环境够亮才熄灭 */ state LIGHT_OFF; } break; default: state LIGHT_OFF; break; } return state; }static关键字把状态保存在函数内部外面不需要再定义全局变量。这个状态机的核心在于一旦切到ON要等ADC值超过1737才回到OFF中间再经过1320、1450都不动作临界抖动就被挡掉了。滞回区宽度一般取0.4V到0.8V太窄没效果太宽会让用户觉得灯反应迟钝。off_th和on_th千万不要设成同一个数否则状态机退化成普通比较器前功尽弃。参数建议值影响开灯阈值0.9VADC约1117低于此值才点亮关灯阈值1.4VADC约1737高于此值才熄灭滤波样本数16越多越平缓响应越慢采样周期20ms匹配人眼亮度变化PWM频率3.6kHz避免可见频闪3.3 用TIM3输出PWM实现无级调光能开能关只是及格线标题里是“智能照明控制系统”调光才是体现控制能力的地方。环境光越强占空比越低环境光越弱占空比越高。这样灯具在白天自动降低亮度夜里自动补光整体照度相对稳定。PWM用TIM3通道1输出CubeMX里预分频设71自动重载值设19972MHz系统时钟分频后得到1MHz计数频率PWM频率约为3.6kHz。1kHz虽然能用但有些眼睛敏感的人能察觉到LED频闪把重载值降到199后频率提上来亮度只牺牲五档分辨率视觉舒适度好很多。占空比映射代码uint32_t clamp32(uint32_t v, uint32_t lo, uint32_t hi) { if (v lo) return lo; if (v hi) return hi; return v; } void light_set_duty_by_adc(uint16_t adc) { const uint16_t adc_min 250; /* 全黑环境的ADC读数 */ const uint16_t adc_max 3400; /* 强光环境的ADC读数 */ uint32_t ratio; adc clamp32(adc, adc_min, adc_max); /* 限制输入范围 */ ratio (uint32_t)(adc - adc_min) * 199U / (adc_max - adc_min); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 199 - ratio); }对比度的逻辑在这里ADC值越高ratio越大最后写进比较寄存器的199 - ratio就越小占空比越低。adc_min和adc_max要用上一章的标定数据去填不能照抄否则映射会失真。线性映射简单直接缺点是低照度段亮度变化太快夜里有一点环境光LED就很亮。想柔和一些可以把ratio改成二次曲线映射或者查表分段处理这属于调试阶段按实际手感微调的部分。3.4 main循环的节拍调度避免Delay阻塞新手容易写出这样的循环采样、判断、delay(500)、再采样。但一旦加了delay按键扫描和串口打印全部被阻塞灯控系统会觉得“卡顿”。常见做法是用HAL_GetTick()做非阻塞节拍调度20ms做采样滤波100ms做状态机更新500ms串口打印一次。uint32_t tick_20ms HAL_GetTick(); uint32_t tick_100ms HAL_GetTick(); uint16_t adc 0; while (1) { uint32_t now HAL_GetTick(); if (now - tick_20ms 20) { tick_20ms now; adc get_light_filtered(); } if (now - tick_100ms 100) { tick_100ms now; if (g_mode MODE_AUTO) { light_state_t st light_run_auto(adc); if (st LIGHT_ON) light_set_duty_by_adc(adc); else __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 0); } } if (now - tick_500ms 500) { tick_500ms now; printf(%d\r\n, adc); } }HAL_GetTick()是毫秒计数器约49天后溢出回绕但做差值判断不会出错因为无符号减法在回绕场景下依然成立。三个计时变量各管一个周期串口打印偶尔阻塞几毫秒也不会拖垮状态机。打印周期放500ms还有个原因115200波特率下一条几十字节的日志要占好几毫秒如果每20ms打一次PWM更新会被串口挤得肉眼可见地跳动。4. 将源码.zip在Keil中构建并跑通工程结构、串口重定向与光强标定4.1 解压源码.zip后的C语言工程分层大部分STM32毕设源码压缩包解压后的目录结构都类似Keil MDK工程用Group把文件归类典型分层如下Project/ ├── User/ │ ├── main.c │ ├── stm32f1xx_it.c │ └── light_control.h ├── BSP/ │ ├── bsp_adc.c │ ├── bsp_adc.h │ ├── bsp_tim_pwm.c │ ├── bsp_uart.c │ └── bsp_key.c ├── Core/ │ ├── startup_stm32f103xb.s │ └── system_stm32f1xx.c ├── Drivers/ │ ├── CMSIS/ │ └── STM32F1xx_HAL_Driver/ └── MDK-ARM/ ├── project.uvprojx └── project.uvoptx这个结构把硬件操作全部沉淀在bsp_前缀的文件里main.c只做调度light_control.h导出状态机和PWM映射函数。串口、ADC、PWM分开之后后期调阈值或换引脚时只改一个文件调度逻辑完全不动。毕设答辩经常问“代码怎么组织”按这个思路答比说“全写在main.c里”体面得多。4.2 串口重定向printf到USART1用串口打印ADC值是整个调试过程最依赖的反馈。CubeMX里开启USART1异步模式波特率115200然后必须在Keil MDK里勾选Use MicroLIB否则printf依赖完整标准库的格式化实现既容易编译报错又占用大量Flash。重定向代码如下#include stdio.h int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 0xFFFF); return ch; }勾选MicroLIB后printf最终会调用fputc把字符从串口发出串口助手选115200、8N1即可。如果打印出来全是乱码先查CubeMX里系统时钟配置和实际晶振是否一致。最小系统板上的HSE通常是8MHz如果CubeMX里默认配成其他值波特率会整体漂移和串口助手的参数怎么对也对不上。printf不要放到20ms采样节拍里调试数据会刷得太快串口直接卡死。4.3 用串口标定光强-ADC曲线让阈值不再拍脑袋阈值0.9V和1.4V最好由实测得到拿一个串口助手持续打印ADC值在固定位置分别记录典型光照下的读数形成下面这样的表光照环境分压点电压(V)ADC原始值纸箱遮光至全黑0.20248夜间室内只开手机屏幕0.75930傍晚窗边不开灯1.351675白天阴天室内2.202730晴天窗边直射3.103850假设希望“傍晚自动开灯”就把开灯阈值定在930和1675之间的偏低位置比如1200关灯阈值定在1675和2730之间比如2200。这样自动模式下环境光降到约1.0V以下灯开始亮升到约1.8V以上灯才灭中间留出的滞回带正好覆盖阴晴波动。标定数据要写进light_control.h的宏定义里下次重编译只改一行。4.4 编译报错与硬件不工作的排查清单现象优先检查项Undefined symbol源文件是否加入对应Group头文件Include Path是否配置ADC读数恒定4095PA1是否复用为模拟输入引脚是否虚焊ADC读数恒为0模块供电是否接好正负极杜邦线是否插反串口输出乱码CubeMX中HSE频率是否匹配实际晶振波特率是否一致调试器连不上芯片SWD四根线3V3/SWDIO/SWCLK/GND是否接稳BOOT0是否拉低LED完全不动确认TIM使能和GPIO输出配置先写死占空比验证通路一半的故障能靠串口打印定位。先保证串口有输出再调硬件遇到问题不要一次改多处每改一次重新编译烧录观察输出变化比对着代码通读更高效。5. 用手电筒模拟光照做实测再用两条技巧补足自动控制盲区项目在答辩前有个固定动作白天教室里环境光很亮灯本来是灭的评委看不到效果。常见做法是在现场用手电筒或手机闪光灯做光源模拟用遮挡物模拟夜晚整个过程完全可控。手电筒从30cm外慢慢靠近光敏电阻日志应该显示ADC值上升、占空比下降、灯逐渐变暗拿开电筒后几秒内灯重新变亮串口日志长这样[ 12345] ADC3410 DUTY10% MODEAUTO [ 12405] ADC2600 DUTY28% MODEAUTO [ 12503] ADC1100 DUTY68% MODEAUTO日志里ADC和DUTY永远反向联动。如果某个环境光下占空比不跟随ADC走先怀疑PWM映射里adc_min和adc_max设错再怀疑状态机卡在手动模式。测试时用手掌从侧面慢慢遮挡光敏电阻ADC下降灯光渐亮整个过程没有闪烁或突变说明滞回区间设置有效。5.1 自动/手动模式切换按键加一个自动/手动模式切换按键接PA0并配置上拉输入用软件消抖加边沿检测在MODE_AUTO、MODE_MANUAL_ON、MODE_MANUAL_OFF之间循环切换。手动模式对调试非常有用给光敏电阻罩遮光罩排除环境光干扰时自动模式会一直改变输出手动模式可以锁定输出单独验证PWM通路是否正常。消抖计数一般20ms就能滤掉按键机械抖动具体实现就是把电平变化累积计数连续读到20次稳定电平后再切换模式。5.2 用连续置信计数替代延时防误触发自动模式下要防止夜晚车灯扫过或闪电导致灯瞬间开关但不能用delay()那会阻塞整个调度。更稳妥的方式是在状态机里加“置信计数”连续5次每次100ms检测到ADC低于开灯阈值才真正点灯连续8次检测到高于关灯阈值才真正关灯。相比一次判断加长延时这种计数方式不阻塞主循环车灯扫过虽然会让ADC瞬间升高但不会持续8个周期误触发率大幅下降。写进状态机时记得让计数变量保持static否则掉电重启后旧值残留灯会在开机瞬间误亮一下。本文还有配套的精品资源点击获取