
简介一套基于STM32F103C8T6与光敏电阻传感器开发的智能照明控制系统毕业设计源码适合计算机、电子及嵌入式相关专业的学生用于毕业设计、课程设计或期末大作业。项目已通过严格调试并获评审高分可直接运行。系统通过光敏电阻实时采集环境光照强度经STM32的ADC转换后由PWM调节照明亮度完整展示了从传感器数据采集到执行器控制的嵌入式开发链路。包内共1006个文件、约18MB主体为564个C源文件与249个头文件包含完整的STM32固件库及驱动代码另有工程配置文件、编译生成的hex/axf文件以及设计说明文档便于直接在Keil或IAR环境中打开查看、重新编译与烧录验证。资源结构清晰可对照学习GPIO、ADC、PWM等外设的初始化与中断处理。目前已有310人学习下载源码工程还附带了链接脚本与内存映射文件适合作为嵌入式C语言和STM32开发的入门到进阶参考。1. 智能照明控制系统的核心问题选型、接线与阈值设定把 C 语言、STM32F103C8T6 最小系统板、光敏电阻传感器和继电器凑在一起做一个智能照明控制系统一周就能跑通但真正拿来当毕业设计答辩问题往往出在三个地方光敏电阻接线方式不对导致 ADC 数值恒为 0 或 4095只做一次阈值比较导致继电器不停吸合抖动还有临时在黄昏调好的参数换一天阴天就完全失灵。这三件事分别对应模拟分压、软件滤波和阈值迟滞也是整套系统里最能体现工程能力的地方。这篇内容按最小系统板常见玩法把原理讲清楚同时给出一套能复现的 C 语言代码和调参流程适合电子、嵌入式方向的毕业生也适合刚拿到 F103 板子想做感光控制的初学者。2. 硬件原理与电路设计光敏电阻分压、ADC通道与引脚映射2.1 光敏电阻为什么不能直接接STM32引脚光敏电阻LDR是阻值随光照变化的器件强光下阻值可能只有几 kΩ暗环境下可以到几百 kΩ 甚至 MΩ 级。它的本质是电阻电阻没有极性也没有有源输出能力如果直接一端接 3.3V、另一端接 STM32 的 PA1PA1 工作在高阻输入状态整个回路没有电流ADC 读到的是浮空电平绝大多数情况下就是 3.3V换算成 12 位数值就是 4095。这也是“光敏电阻传感器插上板子读数不变化”的第一大原因。正确的接法是用分压电路把电阻变化转成电压变化3.3V → 光敏电阻 → 固定电阻 → GNDADC 采样点接在两者中间。此时采样点电压为Vadc 3.3 × R_fixed / (R_ldr R_fixed)环境变暗时光敏电阻阻值增大Vadc 降低ADC 读数变小环境变亮时读数变大。这个方向关系需要在写代码之前明确我见过不少样例代码把比较符号写反结果大白天灯亮着晚上反而灭灯。固定电阻的选取有讲究让固定电阻和光敏电阻在目标光照区间处于同一数量级ADC 读数变化最明显。比如目标场景是区分“傍晚需要开灯”和“白天不需要开灯”假设傍晚时 LDR 阻值约 50kΩ明亮室内约 5kΩ固定电阻选 10kΩ那么明亮时约 2.2V对应约 2730傍晚时约 0.55V对应约 680中间有近 2000 LSB 的区分空间如果固定电阻选 1kΩ两个状态的 ADC 读数差距会被压缩到几百 LSB阈值很难摆准。市面上的光敏电阻模块通常已经带了一个 10kΩ 电位器和比较器引出 AO 和 DO 两个输出AO 就是分压后的模拟电压本项目推荐直接读 AO。提示DO 输出的数字量来自板载比较器适合不需要调阈值的场合要做成有说服力的智能照明控制系统最好用 ADC 读 AO阈值由软件控制。2.2 STM32F103C8T6的ADC引脚分配与接线表STM32F103C8T6 内部有一个 12 位逐次逼近型 ADC1PA0 到 PA7、PB0、PB1 都能作为模拟输入通道但同一个 ADC 同一时刻只能采一路。实际毕业设计中PA1 是比较好用的通道因为它不在 SWD 下载口上也不与 USART1 冲突引脚扇出也方便杜邦线。PA0 带有 WKUP 唤醒功能如果后面要做低功耗唤醒可以留给按键不推荐挤占。信号STM32F103C8T6引脚CubeMX 配置说明光照分压输出PA1 / ADC1_IN1Analog采样时间 239.5 Cycles接光敏电阻模块 AO 或自制分压节点继电器控制PB0GPIO Output 推挽通过三极管驱动继电器线圈板载 LEDPC13GPIO Output程序心跳指示防止误以为死机调试串口PA9 / PA10USART11152008N1打印 ADC 读数与环境状态电源3.3V / GND—光敏分压与 STM32 共地接线时注意不要占用 PA13、PA14这两个引脚是 SWD 调试口一旦被复用下载器可能连不上芯片PA9、PA10 作为串口也要尽量保留。若确实引脚不够USART1 重映射后可以放到 PB6、PB7但不建议在毕业设计里增加这种复杂度。STM32F103C8T6 的内存只有 20KB RAM、64KB Flash程序里不要使用 malloc 动态分配全局数组和静态数组足够用。2.3 继电器负载的驱动与供电注意STM32 GPIO 引脚输出电流大约只有几 mA直接驱动继电器线圈不现实。常见做法是 GPIO 经过 1kΩ 电阻接 NPN 三极管S8050基极三极管集电极接继电器线圈一端线圈另一端接 5V发射极接 GND线圈两端并联 1N4007 二极管吸收关断瞬间的反电动势二极管负极接 5V。这样 GPIO 输出高电平继电器吸合输出低电平释放。如果买的是成品继电器模块模块内部已经有了三极管和续流二极管有些还带光耦隔离用起来更省事。但要注意成品模块一般有两个电源引脚VCC 接 5VGND 与 STM32 共地控制引脚直接接 PB0。测试时继电器负载不要真接 220V 灯具用 DC12V 的 LED 灯带或普通小灯泡验证逻辑即可安全第一演示效果也不差。供电方面最忌讳的是继电器和光敏分压共用一个 3.3V 的引脚。继电器吸合瞬间电流可能到几十毫安甚至更高会造成 3.3V 电压抖动ADC 读数随之漂移。工程做法是光敏分压使用 MCU 的 3.3V 基准继电器模块独立接 5V 电源只在 GND 端共地这样采样值不会受负载动作影响。3. CubeMX配置与C语言驱动实现从ADC采集到照明逻辑3.1 CubeMX 最小工程配置ADC、GPIO、USART先建立一个 STM32F103C8Tx 工程。打开 STM32CubeMX在 System Core 中把 SYS 的 Debug 设为 Serial Wire这一步保留 SWD 下载口否则后续程序烧写可能出问题。RCC 选项里 HSE 选择 Crystal/Ceramic Resonator对应最小系统板上的 8MHz 晶振。时钟树配置是容易被忽略的一步。F103 的 ADC 时钟来自 APB2最高不能超过 14MHz。外设时钟总线配置为 72MHz 时ADC 时钟分频至少要选 6得到 12MHz。采样时间建议选 239.5 CyclesADC 工作在单次转换模式关闭连续转换关闭扫描模式这是入门阶段最稳的组合。配置项位置设置值说明DebugSystem Core → SYSSerial Wire保留 SWD 下载调试RCC HSESystem Core → RCCCrystal/Ceramic Resonator配合板载 8MHz 晶振ADC1_IN1Analog → ADC1PA112bit239.5 Cycles高采样时间适配光敏电阻高阻PB0GPIO Output推挽Low 默认继电器控制引脚USART1Connectivity → USART1Asynchronous115200打印调试数据ADC PrescalerClock Configuration/612MHz不超 14MHz 上限GPIO 配置中PB0 模式设为 GPIO_Output初始电平直接设为 Low避免上电瞬间继电器误吸合。串口默认引脚 PA9、PA10波特率 115200。生成工程后Keil 中把 C 标准选为 C99方便在代码里声明中间变量。3.2 HAL 库 ADC 读取代码轮询模式与滑动滤波CubeMX 生成的代码中ADC1 初始化在 MX_ADC1_Init 中完成HAL 库已经处理好了寄存器配置。实际读取不需要开启中断用最简单的轮询模式即可/* 读取一次 PA1 的 ADC 值返回 0~4095 */ uint16_t light_adc_read(void) { uint16_t v 0; HAL_ADC_Start(hadc1); // 启动单次转换 if (HAL_ADC_PollForConversion(hadc1, 10) HAL_OK) { v HAL_ADC_GetValue(hadc1); // 得到 12 位转换结果 } HAL_ADC_Stop(hadc1); // 单次模式下停止释放 ADC return v; }这段代码每次调用完成一次完整的启动、等待、读取、停止流程。HAL_ADC_PollForConversion 第二个参数 10 表示超时 10ms实际转换时间约 21μs所以不会走到超时分支。采集到的数值范围 04095对应 03.3V分辨率约 0.8mV/LSB对光照控制来说精度远远够用。直接用单次读取结果做判断会存在毛刺原因是光敏电阻输出本身有噪声周围灯光频闪也会叠加干扰。加一个简单的滑动平均滤波器#define FILTER_SIZE 8 static uint16_t filter_buf[FILTER_SIZE] {0}; static uint8_t filter_idx 0; static uint32_t filter_sum 0; /* 返回 8 次采样的平均值 */ uint16_t light_adc_filtered(void) { filter_sum - filter_buf[filter_idx]; // 去掉窗口中最旧的数据 filter_buf[filter_idx] light_adc_read(); filter_sum filter_buf[filter_idx]; filter_idx (filter_idx 1) % FILTER_SIZE; return (uint16_t)(filter_sum / FILTER_SIZE); }这里维护了一个长度为 8 的环形缓冲区和一个累加和每次采样新值减去窗口最旧值、加入新值然后取平均运算量很小在 F103 上几乎不占资源。程序里不要使用链表或动态数组STM32F103C8T6 的 20KB RAM 完全能放下这些静态数组但是申请多样化的动态资源反而容易出问题。3.3 照明控制主循环阈值比较与迟滞防抖如果直接写成“读数小于阈值就开灯大于阈值就关灯”临界光照条件下 ADC 只要抖动几个 LSB继电器就会在开关之间反复切换机械继电器在这种状态下几十次就会损坏。解决办法是使用滞回比较开灯阈值和关灯阈值分开#define TH_DARK 1000 // 光照读数低于此值认为环境变暗 #define TH_BRIGHT 1800 // 光照读数高于此值认为环境已亮 static uint8_t light_on 0; // 0灯灭1灯亮 void lighting_control_loop(void) { uint16_t lv light_adc_filtered(); if (light_on 0 lv TH_DARK) { light_on 1; HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, GPIO_PIN_SET); } else if (light_on 1 lv TH_BRIGHT) { light_on 0; HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, GPIO_PIN_RESET); } }RELAY_GPIO_Port 和 RELAY_Pin 是 CubeMX 生成的宏定义这里按 PB0 命名。当环境照度落在 1000 和 1800 之间时程序不更新输出状态维持此前状态这段区间就是滞回区。滞回区消除了临界闪烁也避免了继电器在黄昏时分频繁动作。主循环可以这样组织while (1) { lighting_control_loop(); HAL_Delay(100); // 控制周期 100ms人眼不会觉得迟钝 }控制周期由滤波窗口和 HAL_Delay 共同决定滤波窗口 8、周期 100ms最坏情况下响应延迟约 800ms。照明系统对速度不敏感关键是状态稳定这个延迟可以接受如果后续要加 PWM 调光再把周期缩到 20ms。4. 参数标定与坑点排查阈值、迟滞量和滤波窗口怎么调4.1 串口打印ADC值先建立环境对照表不要拍脑袋定阈值。先把串口调通打印当前 ADC 读数分别在几个典型环境下手动记录数据这是最可靠的标定方式。Keil 中重定向 printf 到串口需要补一个 fputc#include stdio.h int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 10); return ch; }主循环里打印printf(LDR ADC%u, light_on%u\r\n, lv, light_on);打开串口助手就能看到实时的数值变化。以下是一组使用 10kΩ 固定电阻配合常见光敏电阻模块得到的示例数据具体数值会因器件批次和分压电阻不同而变化这里只展示量级关系环境场景ADC 读数示例系统应做出的动作手机手电筒直射3500 以上关灯白天室内靠窗20003300关灯阴天室内8001500处于滞回区拉窗帘夜间600 以下开灯拿到这组数据后TH_DARK 可以设在 7001000 附近TH_BRIGHT 设在 16002000 附近。关键是 TH_BRIGHT 一定大于 TH_DARK滞回区间要覆盖阴天室内的读数否则中午云层遮住太阳这种场景就会触发一次误开灯。4.2 迟滞量与滑动平均的数值选择迟滞量取多大取决于你对“频繁动作”的容忍度。如果只是做单纯的光控开关滞回带拉到 600800 LSB 都没有问题如果要求傍晚尽早亮灯滞回带就要压缩到 200300 LSB。建议先用串口观察 10 分钟数据看 ADC 在稳态环境下的自然波动幅度。比如白天读数在 25002600 之间抖动抖动幅度 100 LSB那么滞回量至少取 300留出 3 倍余量。滑动平均窗口同样影响系统行为。窗口短响应快但滤波效果弱窗口长数据平滑但延迟增大。延迟的估算公式不变总延迟 ≈ 滤波窗口大小 × 控制周期。照明场景里总延迟控制在 0.51 秒的范围内是合理的。窗口 8、周期 100ms 约 0.8 秒延迟换成窗口 4、周期 20ms 则约 0.08 秒适合快速响应的对象。注意光敏电阻自身的响应时间在几十毫秒量级采样周期比 20ms 更短没有任何实际意义。4.3 常见坑ADC读数为0、继电器抖动、上电误动作这几个问题在毕业设计调试中基本都会遇到逐个排查很快。ADC 恒为 0确认 CubeMX 中 PA1 被配置成 Analog而不是 GPIO_Output否则引脚被强制推挽读不出真实电平再确认分压电路 GND 和 STM32 共地。ADC 恒为 4095检查固定电阻是否接到了 GND 侧光敏电阻和固定电阻位置接反也会出现这种结果。成品模块上顺时针调电位器读数会变化如果怎么调都不变优先怀疑模块 AO 引脚虚焊。继电器吸合时单片机复位几乎可以断定是继电器线圈没有续流二极管或者电源余量不足。线圈两端必须并 1N4007且继电器独立供电。上电瞬间灯闪一下复位后 GPIO 在配置完成前是浮空输入状态外部电磁干扰可能让三极管短暂导通。解决方案是原理图上在 PB0 加一个 10kΩ 下拉电阻到 GND程序侧在 main 函数最开始就把 PB0 拉低再执行外设初始化。提示在 final 版本的程序里加入一个开机自检动作——上电点亮 LED 1 秒再熄灭答辩时演示这个动作能直观说明电路板运行正常还能证明继电器控制链路是完好的。5. 答辩演示与进阶升级把固定阈值改成人机可调5.1 阈值电位器与PWM调光扩展固定阈值最大的问题是不通用换一个光照环境就要重新编译烧录。常见做法是在 PA2 上再加一个 10kΩ 电位器用同一个 ADC1 的通道 2 实时读取阈值把控制逻辑里的 TH_DARK 从宏定义改成变量。CubeMX 里给 ADC1 增加一个 IN2 通道代码中通过 HAL_ADC_ConfigChannel 切换通道uint16_t read_adc_channel(uint32_t channel) { ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel channel; sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES_5; HAL_ADC_ConfigChannel(hadc1, sConfig); return light_adc_read(); }主逻辑里每次读取光照值后再去读一次阈值电位器用变量替代宏uint16_t threshold read_adc_channel(ADC_CHANNEL_2); if (light_on 0 lv threshold) { light_on 1; HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, GPIO_PIN_SET); } else if (light_on 1 lv threshold 300) { light_on 0; HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, GPIO_PIN_RESET); }这套方案在答辩现场的演示效果很好评委旋转电位器就能看到灯在对应位置翻转不需要临时改代码。如果想让项目再往上走一档可以把“开关式控制”升级成 PWM 调光使用 PA6 复用为 TIM3_CH1把环境光照反向映射为占空比实现光线越暗亮度越高/* TIM3 输出 PWM 到 PA6ARR 设为 999 */ uint32_t ccr (4095U - lv) * 999U / 4095U; __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, ccr);5.2 演示验证与记录方法答辩演示环节不要只展示“手一挡灯就亮”要有可观测、可记录的数据支撑。建议现场按下面的顺序演示演示动作预期现象判定点用手掌逐渐遮住光敏电阻灯光平滑变亮或继电器吸合开灯变化连续无跳变手机闪光灯照射光敏电阻灯光逐渐变暗或继电器释放关灯不误触发不抖动缓慢在滞回区晃动遮挡物灯不会反复开关滞回带生效连续开关继电器十次单片机不死机ADC 读数不回跳供电与续流可靠同时把串口日志打开每一条状态变化都带 ADC 数值和时间戳现场抄录一组原始数据。运行时保留串口日志等演示结束后整理成一张光照-动作映射表打印出来附在设计文档后面评分老师一眼就能看出这套智能照明控制系统有完整的验证闭环不是只写在纸上的方案。本文还有配套的精品资源点击获取