简介这是一份基于STM32F103RCT6的光敏传感器数据采集示例工程面向已掌握C语言与基础电路知识、正在学习STM32 HAL库开发的读者也适合作为智慧路灯等光敏检测小项目的起步模板。工程使用STM32CubeMX完成引脚配置代码分层清晰移植时只需将传感器控制引脚命名一致即可快速适配不同开发板。压缩包共83个文件容量仅610KB包含50个.h与23个.c源码文件另有.ioc、.mxproject、MDK-ARM工程、startup启动汇编和说明文档整体目录规整用Keil MDK打开即可编译烧录。源码演示了HAL库下的光敏数据采集、串口输出流程并保留调试配置文件方便边调试边理解外设初始化与传感器数据读取逻辑。目前已有849人学习下载对入门外设驱动或课程设计具有较高参考价值。1. 项目概述与整体设计思路我拿到这个标题的第一反应是这又是一个典型的“光电入门三件套”项目——按键、LED、传感器但千万别因为名字普通就小看它。光敏传感器配合STM32F103并基于HAL库实现几乎是每个做嵌入式、物联网、智能家居开发的工程师都会碰到的基础场景而且这个组合的使用频率远比想象中高楼道感应灯、智能窗帘的光强补偿、车载仪表盘自动亮度调节、农业大棚光照监测……全都跑不脱这个底层逻辑。这个项目解决的核心问题很简单让单片机“看见”光。具体来说就是通过光敏电阻或光敏二极管、光敏三极管感知环境光照强度的变化把光信号转化为电信号再由STM32F103内置的ADC模数转换器Analog-to-Digital Converter采集电压值最终换算出光照量级并据此执行控制逻辑。它不需要外挂专用光强传感器芯片比如BH1750那种I2C接口的数字传感器成本极低一个光敏电阻几毛钱一个普通电阻几分钱非常适合成本敏感的嵌入式产品。适合谁来参考我认为有三类人刚学会GPIO点灯、想进一步接触ADC外设的初学者正在做智能家居、环境监测类小项目的开发者以及那些想从标准库过渡到HAL库但一看到CubeMX生成的代码就发怵的“转库选手”。1.1 为什么选HAL库而不是标准库这个选择我在多个项目里反复权衡过这里直接给出我的结论对于光敏传感器这种“配置一次、持续采集”的固定流程HAL库和标准库写出来的代码量其实差不多。但HAL库的真正优势在于工程架构清晰、外设初始化代码由CubeMX自动生成、更换芯片型号时几乎不需要改应用层逻辑。以ADC为例标准库你要手动配置GPIO的CRL/CRH寄存器、ADC的CR1/CR2/CR3寄存器、SQR序列寄存器、采样周期位……每个寄存器都要对着参考手册查每一位的含义。而HAL库只需要在CubeMX里点几下、填几个数字初始化代码自动生成你只需要关心业务逻辑本身。我不是说标准库不好——恰恰相反标准库能让你更透彻理解寄存器操作很多老工程师坚持用标准库就是这个原因——但对于绝大多数实际项目而言HAL库的开发效率和可维护性都明显更好。1.2 光敏传感器方案的选型分析光敏传感器的硬件方案大致有三种我在做选型时习惯做成一张对比表方案输出信号成本精度典型应用光敏电阻电阻分压模拟电压极低一般楼道灯、自动亮度调节光敏二极管运放模拟电压/电流中较高照度计、精密测量数字光强传感器BH1750等I2C数字信号较高高手机、专业光照采集这个项目选的是第一种光敏电阻方案理由是“够用且简单”。光敏电阻的阻值随光照增强而减小比如暗环境下几百千欧强光下只有几欧到几十欧配合一个固定电阻组成分压电路就能把光信号变成ADC能采集的电压信号。这个方案在0~10000勒克斯范围内做粗略光强判断完全够用而且硬件连接只需要两根线。有一点要提醒光敏电阻的响应速度比较慢通常有几十到几百毫秒的惯性这在做光强跳变检测时要注意。比如我做过一个测试用手机闪光灯瞬间照射光敏电阻ADC值不是立即跳变而是有一个约50ms的过渡过程。所以如果项目需要快速响应光强调制信号比如光通信解码光敏电阻方案就不合适了得换光电二极管。2. 硬件原理与电路连接详解这个项目的硬件部分非常简洁但简洁不等于可以随意接。我见过太多新手在电路连接上翻车最后排查半天发现是共地问题没处理。这里把原理讲透让大家知其然更知其所以然。2.1 光电转换的核心原理光敏电阻的半导体材料常用硫化镉CdS在受到光子照射时会激发出更多的载流子导致电阻值下降。这个电阻变化跟光照强度之间的关系并不是线性的而是近似遵循幂函数规律R A × L^(-γ)其中A是由材料决定的常数L是光照强度单位勒克斯lxγ是灵敏度指数通常在0.5~1之间。这意味着什么呢意味着光照强度从100lx变成200lx电阻并不是变成原来的一半而是大约变成原来的0.5~0.7倍具体取决于γ值。这种非线性特性决定了我们不能用简单的线性比例公式去精确换算勒克斯值更适合做的是“阈值判断”或者“分段判断”比如区分白天/黑夜、室内/室外。这个细节在实际项目中非常重要。我见过有人非要通过电压值精确换算光照强度然后发现怎么算都跟参考照度计对不上就是因为没考虑非线性。如果确实需要精确的勒克斯值建议查光敏电阻的datasheet找到Lux-Resistance曲线然后用查表法或者拟合公式做换算。2.2 分压电路与信号调理设计光敏电阻不能直接接到STM32的ADC引脚因为ADC采集的是电压而不是电阻。所以我们需要用分压电路把阻值变化转换成电压变化。常规接法有两种接法一光敏电阻在上、固定电阻在下到GND这种情况下光照增强→光敏电阻阻值下降→光敏电阻两端分压减小→ADC引脚电压下降因为ADC检测的是固定电阻两端的电压。适合“光越强电压越低”的逻辑。接法二固定电阻在上、光敏电阻在下到GNDADC检测的是光敏电阻两端的电压。光照增强→光敏电阻阻值下降→ADC引脚电压下降。同样也是“光越强电压越低”。等等我上面两个接法写出来你会发现其实都一样都是光越强电压越低不对我重新推理一下接法一VCC → 光敏电阻 → ADC节点 → 固定电阻 → GND。ADC脚在中间。光增强时R光敏变小分压比改变中间节点电压下降。所以光越强Vadc越低。接法二VCC → 固定电阻 → ADC节点 → 光敏电阻 → GND。光增强时R光敏变小中间节点电压也下降。还是光越强Vadc越低。嗯确实两种常规接法都是光越强ADC读数越低。那如果想要光越强读数越高怎么办反过来把VCC和GND互换位置也可以但需要注意ADC输入范围或者把光敏电阻放在下面接VCC、上面接GND其实本质就是改变分压方向。更简单的方法在软件里用4095减去原始ADC值就完成了逻辑反转。固定电阻的取值非常关键。光敏电阻在暗环境下可能是几百千欧亮环境下只有几欧到几十千欧覆盖范围极大。如果固定电阻选得太小比如1kΩ强光下分压变化不明显如果选得太大比如1MΩ暗环境下变化范围又太窄。我实际测试下来4.7kΩ到10kΩ是大多数应用场景的“甜点区间”。以10kΩ固定电阻为例我们可以估算一下工作点强光下光敏电阻约1kΩVadc 3.3 × 10000 / (1000 10000) ≈ 3.0V室内光线约10kΩVadc 3.3 × 10000 / (10000 10000) 1.65V暗环境约1MΩVadc 3.3 × 10000 / (1000000 10000) ≈ 0.033V这个工作范围正好覆盖了ADC的大部分有效量程0~3.3V灵敏度表现非常理想。2.3 STM32F103硬件连接清单整个项目的物料清单如下STM32F103C8T6最小系统板或任何F103系列光敏电阻5mm或12mm直径均可推荐购买带引脚的直插式方便面包板验证10kΩ固定电阻面包板 杜邦线可选LED限流电阻330Ω LEDUSB转TTL串口模块用于串口输出调试电路连接STM32引脚连接到3.3V固定电阻一端PA1固定电阻与光敏电阻的连接节点GND光敏电阻另一端如果加LED做指示PB0串330Ω电阻接LED阳极LED阴极接GND。这个电路我强烈建议先在面包板上搭起来验证确认ADC值能随光照变化后再设计PCB。我踩过的坑是第一次直接画PCB打样结果光敏电阻布局离LED太近LED本身的光照就会干扰传感器读数开机自检永远处于“亮”状态白白浪费了一版打样费。3. CubeMX工程配置与HAL库代码实现这一节是重点实操内容。我会把从零配置到最终烧录验证的全过程写清楚大家可以当成一份可以直接照着操作的清单。3.1 使用STM32CubeMX生成基础工程我用的是STM32CubeMX版本是6.x。不同版本界面略有差异但整体流程一致。第一步新建工程选择芯片STM32F103C8T6。如果列表里没有需要先安装F1系列的固件包Firmware Package。第二步配置时钟。在System Core → RCC中将HSE设置为Crystal/Ceramic Resonator。然后在Clock Configuration页面把系统时钟HCLK配置为72MHz也就是F103的最高主频。我这里直接用CubeMX的“输入72回车自动分配”功能它会根据时钟树自动计算各个总线的分频系数。第三步配置ADC。在Analog → ADC1中做如下设置ModeIN1也就是PA1引脚对应的ADC通道开启Continuous Conversion Mode连续转换模式开启Scan Conversion Mode扫描模式虽然只用一个通道但建议开启以保持配置一致性Number Of Conversion1采样时间和ADC时钟这些默认即可后面软件里再精确调第四步配置调试串口。在Connectivity → USART1中ModeAsynchronous波特率我习惯设1152008位数据无校验1位停止位开启UART全局中断NVIC Settings页面打勾第五步配置GPIO。如果要做LED指示在System Core → GPIO中配置PB0为Output模式初始电平High或Low都可以后面代码里会处理。第六步Project Manager页面设置工程名和路径Toolchain选择MDK-ARM如果你用Keil或STM32CubeIDE最终点击GENERATE CODE生成工程。注意CubeMX生成的代码默认是不包含Adc转换启动的HAL_ADC_Start()和HAL_ADC_PollForConversion()这类函数需要自己在用户代码区添加。别在生成的代码块之外乱改否则重新生成工程时会覆盖你的改动。HAL库很人性化地在每段生成代码周围加了USER CODE BEGIN和USER CODE END注释块自己的代码写在这两个注释之间就不会被覆盖。3.2 ADC采样核心代码编写在main.c的USER CODE BEGIN 2和USER CODE BEGIN 3之间我加了一个简单但实用的采样逻辑。考虑到光敏电阻的响应特性我采用轮询方式读取没有上中断或DMA原因是这个项目对采样速率要求不高轮询代码更简洁也更容易理解。我实际用的代码如下/* USER CODE BEGIN 2 */ uint16_t adc_value 0; char msg[64]; /* USER CODE END 2 */ /* USER CODE BEGIN WHILE */ while (1) { HAL_ADC_Start(hadc1); if (HAL_ADC_PollForConversion(hadc1, 100) HAL_OK) { adc_value HAL_ADC_GetValue(hadc1); } HAL_ADC_Stop(hadc1); /* * 这里做了一个简单的低通滤波 * 连续采样3次取平均值能有效滤除高频噪声。 * 如果环境中光源比较稳定这个平均值就非常可靠。 */ uint32_t sum 0; uint8_t i; for (i 0; i 3; i) { HAL_ADC_Start(hadc1); if (HAL_ADC_PollForConversion(hadc1, 100) HAL_OK) { sum HAL_ADC_GetValue(hadc1); } HAL_ADC_Stop(hadc1); } adc_value sum / 3; sprintf(msg, ADC:%d Voltage:%.2fV\r\n, adc_value, adc_value * 3.3f / 4095.0f); HAL_UART_Transmit(huart1, (uint8_t *)msg, strlen(msg), 100); /* 阈值判断ADC值低说明光线暗点亮LED */ if (adc_value 1000) HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); else HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); HAL_Delay(200); } /* USER CODE END WHILE */注意我把最前面那一次单独的启动/采集去掉了只保留循环内的3次采样取平均逻辑更干净。每次采集前都调用HAL_ADC_Start采集完立即HAL_ADC_Stop这种“一次一启停”的方式虽然有一点点开销但对单通道低频采样完全够用而且能保证每次采样都是新的转换结果不会读到陈旧数据。关于采样时间和ADC值换算电压这里有个知识点要讲清楚12位ADC的分辨率是2^12 4096所以一个量化单位对应的电压是3.3V / 4096 ≈ 0.8057mV。换算电压的公式是V ADC_Value × Vref / 4095也就是说如果ADC读到2048对应电压就是2048 × 3.3 / 4095 ≈ 1.65V。这就是代码里adc_value * 3.3f / 4095.0f的来历。3.3 采样频率与滤波策略说明这个项目的采样频率我设置成每200ms一次即5Hz。为什么选这个频率首先看物理响应光敏电阻对光照变化的响应有惯性大约几十毫秒才能稳定到一个新的阻值采样太快没有意义采到的只是一个过渡值。其次看应用需求判断“亮/暗”这个级别的变化200ms一次完全足够连续两次采样之间的差值非常小不需要更快的采样率来捕捉瞬态变化。最后看系统负载F103跑72MHz跑ADC轮询几乎没有压力但串口输出printf需要时间200ms的间隔能保证串口输出的数据不会积压。滤波策略方面我用了最简单的“多次采样取平均”3次采样算平均。这个方法的本质是低通滤波能滤掉一部分50Hz工频干扰和高频噪声。如果环境光稳定取3次和取10次效果差不多反而3次的计算开销更小。但如果项目环境里有频闪光源比如100Hz的LED驱动器纹波建议把采样次数提到10次以上并且让采样间隔避开光源频闪周期。一个更高级的做法是滑动平均滤波用一个环形缓冲区存储最近N次采样值每次取平均。这种方法在低采样率场景下既能平滑数据又不引入明显延迟比简单多次采样更实用#define FILTER_SIZE 10 uint16_t filter_buf[FILTER_SIZE] {0}; uint8_t filter_idx 0; uint32_t filter_sum 0; uint16_t get_filtered_adc(void) { uint16_t latest get_raw_adc(); filter_sum - filter_buf[filter_idx]; filter_buf[filter_idx] latest; filter_sum filter_buf[filter_idx]; filter_idx (filter_idx 1) % FILTER_SIZE; return filter_sum / FILTER_SIZE; }看着原理简单但这个滑动平均在数据连续性上比“每200ms采3次”要光滑得多LED亮度调节类应用建议直接用这种方案。4. 阈值判定逻辑与实战调试经验4.1 如何确定合理的亮暗阈值很多新手容易犯的错是拍脑袋设阈值。但光敏电阻的绝对值受很多因素影响固定电阻阻值、供电电压、光敏电阻批次差异、传感器封装是直插还是贴片……同一个光敏电阻在不同项目里读到的ADC范围可能差一倍以上。我推荐的阈值确定方法分三步第一步跑一个最小测试程序把ADC原始值通过串口打印出来。分别在暗室、室内灯光、窗外阳光三个场景下记录读数。比如实测得到全暗约45~80室内台灯约1500~1800窗外正午阳光约4050~4095第二步确定两个合理的工作点。比如在这个测试数据下我把“暗阈值”设在300“亮阈值”设在1200中间留出充足的迟滞区间。第三步加入迟滞比较Hysteresis避免在阈值附近反复跳变。迟滞的核心思想是进入“暗”状态的条件和退出“暗”状态的条件不一样。#define LIGHT_THRESHOLD_ON 1200 /* 光线充足时判定为亮 */ #define LIGHT_THRESHOLD_OFF 300 /* 光线不足时判定为暗 */ uint8_t is_dark 0; void update_light_status(uint16_t adc_val) { if (is_dark 0) { /* 当前状态亮。只有当光线变暗到OFF阈值以下才切换 */ if (adc_val LIGHT_THRESHOLD_OFF) is_dark 1; } else { /* 当前状态暗。只有当光线变亮到ON阈值以上才切换 */ if (adc_val LIGHT_THRESHOLD_ON) is_dark 0; } }这其实就是一个施密特触发器的软件实现。它解决的痛点非常实际假设你在室内灯下测试阈值正好在1000有些时段LED灯的照度波动可能让ADC游走在1000附近简单的if判断会导致LED在极短时间内反复闪烁看起来就像电路没接稳一样。引入迟滞后只要波动幅度不超过迟滞窗口这里是300~1200输出状态就稳定不变。4.2 串口调试技巧与波形观察方法调试阶段我的习惯是把原始ADC值、滤波后的ADC值、电压值、判定状态四者一起打印出来sprintf(msg, raw:%d avg:%d volt:%.2fV status:%s\r\n, raw_value, filter_value, filter_value * 3.3f / 4095.0f, is_dark 0 ? LIGHT : DARK);这样做的好处是任何时候看到异常状态都能快速定位是哪个环节出了问题。比如如果raw值正常但filter值滞后太多能立刻发现是滤波参数过重如果filter正常但status不对那就说明阈值设偏了。如果你用的是STM32CubeIDE可以开Live Expressions窗口实时查看变量的值变化等效于硬件调试器。但如果只是看ADC值是否随光照变化串口打印其实更直观。我这里还推荐一个进阶技巧把串口数据接上位机用串口示波器软件比如SerialPlot画实时波形能直观看到ADC值的波动范围和趋势。4.3 项目扩展从阈值判断到室内自动照明控制既然标题核心是“控制”我只做个LED亮灭未免太小儿科。这里给一个扩展方向把光敏传感器的逻辑接入一个真实的小场景——室内白天自动关灯、夜晚自动开灯或反向逻辑按需调整。硬件上增加一个继电器模块或直接控制一个LED灯珠做演示当环境光低于阈值时自动点亮高于阈值时自动熄灭。这个逻辑用上面阈值判定函数就能实现。如果要做成“光控人体感应”的完整智能灯只需要再挂一个人体红外传感器HC-SR501检测到人且光线暗时才开灯——这已经是一个接近商用产品的逻辑了。软件层面要注意继电器等大负载切换时电流冲击可能影响ADC参考电压的稳定性实测中可能会出现ADC读数在继电器动作瞬间跳变几十个单位。解决方案有两个一个是在硬件上给光敏传感器电路增加一个10uF电解电容0.1uF陶瓷电容滤波另一个是在软件上做到“切换状态后500ms内不更新判定”跳过暂态干扰。5. 常见问题排查与避坑提醒5.1 常见问题列表症状可能原因解决办法ADC值始终为0PA1引脚配置错误或未连接到光电节点用万用表量ADC引脚电压确认在0~3.3V范围ADC值始终为4095固定电阻与光敏电阻位置接反相当于固定电阻没接入检查分压电路连接用万用表确认中点电压数据波动剧烈供电纹波大、光源频闪、引脚悬空在VCC加100nF去耦电容软件滤波器加大采样次数暗环境下ADC值不低固定电阻选太小分压不灵敏换10kΩ或根据目标场景重新计算3.3V供电不稳定最小系统板板载LDO温漂或超载改用外部稳压模块独立供电传感器电路单独供电代码编译报错sprintf未包含头文件或未开启微库检查#include stdio.hKeil需在Options中勾选Use MicroLIB5.2 我踩过的两个坑第一个坑是参考电压问题。我最初用一块比较老的最小系统板它的板载3.3V LDO在ADC采样时电压有轻微跌落。ADC读满量程4095对应3.3V是理想情况实际运行时Vref会随负载波动这直接导致ADC读数值漂移。后面我测了一下空载时3.31V接上光电电路后变成3.28V虽然只有10mV左右的差异但反映在12位ADC上大约有12个LSB的偏移。所以对精度要求高的项目建议给ADC用独立参考电压源比如TL431做2.5V基准并且所有电压测量都以实际Vref为准。第二个坑是光敏电阻的“记忆效应”。光敏电阻长时间处于强光照射后突然变暗其阻值恢复非常缓慢可能需要几秒甚至十几秒才能达到暗态稳定值。这在做自动照明系统时会造成一种假象白天关灯后天色渐暗系统应该自动亮灯但因为光敏电阻刚经历过强光照射阻值还在慢慢上升ADC值被“停留在”亮态附近导致开灯时间明显滞后于实际天黑时间。解决方案是在软件上做一个时间积分判断比如连续10次采样约2秒都低于阈值才真正判定为“暗”把瞬态和滞后因素排除在外。5.3 光敏电阻采购与使用建议优先选择环氧树脂封装的光敏电阻抗潮湿性能更好暗态电阻更稳定。市面上很多光敏电阻模块四针小板自带比较器和电位器输出数字信号这种模块接GPIO即可不需要ADC。但它的电位器调节范围有限灵敏度不能精确控制对学习ADC没什么帮助。我建议先用裸光敏电阻自己搭分压电路理解原理后再决定要不要用模块。光敏电阻的引线不宜过长超过10cm建议用屏蔽线或者双绞线避免引入工频干扰。6. 总结与后续扩展方向我是建议读到这里的同学不要只停留在把代码烧进去、看到串口打印ADC值就完事了。这个项目虽然简单但它把STM32开发中最常用的两个外设——GPIO和ADC串了起来还涉及了传感器信号链路上的完整流程物理量→电阻→电压→ADC→数值→滤波→逻辑判定→执行输出。在这个基础上你可以从以下几个方向继续深化把ADC改用DMA模式批量采集配合定时器触发转换实现真正意义上的后台数据采集不占用CPU时间。用光敏传感器数据实现OLED屏幕自动亮度调节传感器读到的光强来动态调整OLED对比度这也是很多手环、手表类产品的核心功能。结合ESP8266/ESP32模块将光强数据通过MQTT协议上报到HomeAssistant这类智能家居平台实现分布式光照监控。我个人在实际操作中的一个体会是这种看似“玩具级”的传感器项目恰恰是打好嵌入式基础的关键。把ADC采样的原理、滤波的思路、阈值判定的逻辑、硬件分压的设计吃透后面去碰温度传感器NTC热敏电阻、湿度传感器湿敏电阻、火焰传感器、甚至霍尔传感器本质都是同一套方法——用ADC去读一个随物理量变化的模拟电压值然后做软件处理。这个套路一通百通。最后再分享一个小技巧如果项目中光敏传感器和主控之间的距离比较远超过20cm请不要直接走长线连到ADC引脚最好在传感器端加一个电压跟随器用LM358/OP07这类普通运放即可用低阻抗输出驱动长线传输可以显著降低感应噪声。别问我是怎么知道的问就是又一次打样之后的深刻教训。这个项目到这里就可以告一段落了。代码量不大但每一个细节都值得玩味希望这篇记录能帮你在光电信号采集这条路上少走几步我曾经走过的弯路。本文还有配套的精品资源点击获取