STM32F103光敏传感器ADC驱动设计与踩坑实录 📅 发布时间:2026/9/9 16:54:51 👁 浏览次数: 简介面向STM32 HAL库开发者这份资源以STM32F103RCT6为核心演示光敏传感器数据读取与串口输出解决环境光照采集和调试显示的基础问题。工程包共83个文件以HAL驱动头文件、源文件、STM32CubeMX配置的ioc、MDK-ARM工程文件及启动文件为主整体仅610KB结构清晰便于按目录定位。已有849人学习下载工程采用STM32CubeMX进行引脚初始化配置直观且注释完整只要保持控制引脚命名一致就能快速移植到其他F1系列板卡。通过梳理源码可掌握HAL库外设初始化、传感器数据采集、串口打印等关键流程尤其适合具备C语言和基础电路知识、希望上手HAL库开发的初学者。整体方案精简实用可作为智能路灯等光照检测项目的参考基础。 做嵌入式这几年凡是涉及环境光检测的小项目我基本都绕不开STM32F103和光敏传感器这个组合。HAL 库的 ADC 驱动代码写过好几版最近又把其中一个光敏采样项目整理成了独立模板趁着热乎把设计思路和踩坑记录捋一遍。这套代码解决的是很实际的问题用一颗几毛钱的光敏电阻或者一块四针光敏模块让 F103 稳定读取环境光照变化再通过串口把 ADC 原始值和换算后的亮度值输出。调试方便后面接 OLED、继电器、蜂鸣器做自动控制也不费劲。适合刚接触 HAL 库、正从标准库切过来的朋友也适合想把传感器采集做成可复用模块的开发者。下面直接讲项目怎么设计、代码怎么写、坑在哪。1. 整体思路先搞清楚光敏传感器和 F103 的 ADC 通道1.1 传感器选型光敏电阻和光敏模块怎么选光敏传感器本质上是一个阻值随光照变化的元件。光照越强光敏电阻的阻值越低光照越弱阻值越高。要让单片机能读这个变化必须把电阻变化转成电压变化最简单的办法就是电阻分压。市面上的光敏模块通常已经把分压电路和放大器做好了引出 VCC、GND、DO、AO 四个引脚。DO 是数字量输出一般接电位器调阈值输出高或低电平适合做简单的光控开关AO 是模拟量输出输出电压随光照连续变化适合接单片机的 ADC。如果只是做项目原型直接买这种模块最省事但我个人更喜欢自己搭电路用一颗普通光敏电阻加一个固定电阻成本更低也能更清楚电压到底怎么变的。光敏电阻还有一个参数需要注意就是亮电阻和暗电阻的差异。常见型号在强光下可能只有几 kΩ在黑暗环境中能到几百 kΩ 甚至 1 MΩ跨度很大。选固定电阻时要保证常见光照范围内输出不会频繁顶到 0 V 或 3.3 V否则 ADC 采出来不是 0 就是 4095灵敏度会非常差。1.2 电路连接与 ADC 通道选择STM32F103 有多个 ADC每个 ADC 又有十几个通道。以最常用的 F103C8T6 为例PA0、PA1、PA2、PA3 都可以作为 ADC1 的输入通道。我习惯用 PA1原因很简单CubeMX 里点一下就能把 PA1 配成 ADC1_IN1不跟下载调试串口冲突。如果你用 PA0 也行但 PA0 同时也是 WKUP 引脚在低功耗唤醒设计时容易多想一步。电路接法分两种光敏电阻接 3.3 V固定电阻接 GND中间抽头接 PA1。光照增强时光敏电阻阻值下降中间电压上升。固定电阻接 3.3 V光敏电阻接 GND中间抽头接 PA1。光照增强时光敏电阻阻值下降中间电压下降。两种接法逻辑相反代码里只需要改一下换算方向。实际项目里我常用第一种因为光照越强 ADC 值越大直观一些也方便后面做“白天/夜晚”判断。固定电阻用多大假设 VCC 是 3.3 V光敏电阻亮阻 10 kΩ、暗阻 1 MΩ。如果固定电阻选 10 kΩ亮时光敏电阻分压约 1.65 V暗时约 3.27 VADC 值大概从 2048 到 4063这个区间够用。如果固定电阻选 100 kΩ亮时输出约 3.0 V暗时约 1.65 V变化区间更大但暗光范围的分辨率更好。建议先试 10 kΩ再根据实际光照环境调整。2. 核心代码实现HAL 库 ADC 驱动怎么写2.1 CubeMX 配置要点用 HAL 库开发第一件事不是写代码而是用 CubeMX 生成工程。这里有几个关键配置项ADC1 打开选择 IN1 通道。分辨率保持默认 12 位也就是 ADC 值范围 0~4095。扫描模式关闭因为只采一个通道。连续转换模式可以关闭用单次转换需要时再采一次。采样时间选 239.5 周期。光敏电阻信号变化慢采样时间长一点能减小内部采样电容带来的误差。CubeMX 生成完代码后ADC 初始化长这样static void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig {0}; hadc1.Instance ADC1; hadc1.Init.ScanConvMode ADC_SCAN_DISABLE; hadc1.Init.ContinuousConvMode DISABLE; hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 1; HAL_ADC_Init(hadc1); sConfig.Channel ADC_CHANNEL_1; sConfig.Rank 1; sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES_5; HAL_ADC_ConfigChannel(hadc1, sConfig); }这里特别注意ADC 的时钟在 HAL_ADC_MspInit 里配置一般用 APB2 分频。CubeMX 会自动生成不用手写。如果 ADC 采样结果异常先回头看看 RCC 配置ADC 时钟频率最好保持在 12 MHz 左右太快反而容易出问题。2.2 单次读取和多次滤波最简单的读取函数是开始一次转换等待转换完成拿数值uint16_t LightSensor_Read(void) { uint16_t adc_value 0; HAL_ADC_Start(hadc1); if (HAL_ADC_PollForConversion(hadc1, 100) HAL_OK) { adc_value HAL_ADC_GetValue(hadc1); } HAL_ADC_Stop(hadc1); return adc_value; }这样写没问题但直接用在项目里会发现数值会有几十甚至上百的跳动。环境光本身在变光敏电阻也有噪声电源纹波也会影响 ADC。所以实际使用时一定要做滤波。我常用的是“多次采样取平均”简单有效#define LIGHT_SAMPLE_NUM 10u uint16_t LightSensor_ReadFiltered(void) { uint32_t sum 0; for (uint8_t i 0; i LIGHT_SAMPLE_NUM; i) { sum LightSensor_Read(); HAL_Delay(2); } return (uint16_t)(sum / LIGHT_SAMPLE_NUM); }这里故意加了 2 ms 延时让每次采样在时间上稍微错开降低突发干扰。如果你想保留原始数据也可以在循环里把每次结果存数组再做排序取中间值那就是中位值平均滤波效果更好但代码量多一点。对于环境光检测这种慢变量平均滤波已经完全够用。2.3 数据换算ADC 原始值怎么变光照百分比STM32F103 的 ADC 是 12 位参考电压 3.3 V所以原始值换算电压的公式float voltage (float)adc_value * 3.3f / 4095.0f;电压本身不算直观我一般直接输出 0~100 的“光照强度百分比”。在第一种电路接法下电压越高代表光照越强可以用相对量表示#define ADC_MAX 4095.0f #define ADC_MIN 1000.0f uint8_t LightSensor_GetPercent(uint16_t adc_value) { if (adc_value ADC_MIN) return 0; if (adc_value ADC_MAX) return 100; return (uint8_t)((adc_value - ADC_MIN) * 100.0f / (ADC_MAX - ADC_MIN)); }这套映射不是标准照度单位 lux但用于判断天亮、天黑、环境光强足够不够完全够用。做工业级照度计就得用标准光源标定曲线那就不是这几行代码能覆盖的了。3. 实操过程中踩过的坑3.1 采样值满量程或一直为 0这个问题多半出在电路而不是代码。如果 ADC 值一直接近 4095大概率是 PA1 引脚直接看到了高电平先查固定电阻是否虚焊、光敏电阻是否断路。如果一直接近 0大概率是固定电阻短路到 GND或者光敏电阻短路到 VCC。别急着改程序先用万用表量 PA1 上的电压看它是不是真的在变化。还有一个容易忽略的点光敏模块的 AO 引脚输出范围可能不是 0~3.3 V有些模块上拉方式会导致最小电压不是 0ADC 值下限会偏高。这种模块输出要先用万用表实测一下电压范围再在代码里做线性映射否则算出来的百分比误差很大。3.2 串口打印看不到数据或者显示乱码这个问题我印象最深。第一次调通代码后ADC 值明明是对的但串口助手就是不出数。排查半天原因是串口引脚复用没配置好。HAL 库工程里如果用了 USB 转串口一定要确认 CubeMX 里已经把对应引脚的 USART 外设打开并选择了正确的引脚映射比如 PA9、PA10或者 PB6、PB7不同开发板定义不一样。乱码问题基本就是波特率不对。F103 的串口波特率由一个 16 位寄存器分频得到如果系统时钟不是 72 MHz而 CubeMX 里默认时钟配置没有设置好那么 115200 实际跑出来就会有偏差。遇到乱码先打开 CubeMX 确认 RCC 的 HSE 和 PLL 配置最常见问题是外部晶振频率写错了。3.3 CubeMX 重新生成代码后自己写的驱动被覆盖这个问题几乎每个用 HAL 库的人都会遇到。CubeMX 重新生成工程时会保留USER CODE BEGIN和USER CODE END注释块之间的代码但如果你把自定义函数写在其他位置下次生成就会消失。我踩过几次坑之后干脆养成一个习惯所有光敏传感器相关代码单独放在light_sensor.c和light_sensor.h中主函数只调用接口。这样 CubeMX 怎么重新生成都不会动到我的驱动维护起来也清晰。驱动代码里再放一个初始化函数和一个读取函数全局只暴露三个接口初始化、读原始值、读滤波值。4. 功能扩展与实际应用建议4.1 用 DMA 连续采样减小 CPU 占用如果项目里还要跑屏幕刷新、按键扫描、无线通信直接在 while 循环里一次次启动 ADC 会占用不少时间。这时可以用 ADC 的 DMA 功能让硬件自动把采样结果搬运到内存数组里。CubeMX 配置时在 ADC1 设置里开启 DMA Continuous Requests然后添加 DMA 通道数据宽度选 Half Word。代码里可以这样启动uint16_t adc_buffer[16]; HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buffer, 16);之后 DMA 会自动采样并写满数组。在循环里轮询一个标志位比如利用 ADC 的转换完成中断在HAL_ADC_ConvCpltCallback里处理数据。这种方式特别适合需要稳定连续采集的场合也能避开反复启停带来的延迟。4.2 做光控开关阈值判断加滞回很多朋友做完光照采集后下一步就是想控制灯。直接用“光照小于某个值就开灯”看起来简单但实际会因为光照在阈值附近抖动导致继电器反复开关。解决办法是加滞回判断比如光照低于 30 就开灯高于 50 才关灯中间 30~50 保持原状态不动。这个思路在工业控制里很常用代码也简单static uint8_t light_status 0; if (light_status 0 percent 30) { light_status 1; HAL_GPIO_WritePin(RELAY_PIN, GPIO_PIN_SET); } else if (light_status 1 percent 50) { light_status 0; HAL_GPIO_WritePin(RELAY_PIN, GPIO_PIN_RESET); }用这个逻辑后继电器基本不会再出现快速抖动的问题。4.3 移植到其他 F 系列芯片HAL 库的一大优势就是代码层级统一。光敏传感器驱动只要不涉及特殊外设代码从 F103 迁到 F407、F429 时几乎只需要改两点引脚对应的 ADC 通道号。CubeMX 里选择同一个 ADC 外设。我最近把一个 F103C8T6 的光敏采集项目跑到同样封装尺寸的主控上半小时就完成移植。当然如果新芯片 ADC 时钟来源不同还需要看看MX_ADC1_Init里的时钟配置但只要 CubeMX 重新生成这些细节基本都会自动处理。最后说一个我自己的习惯光敏这类慢变量没必要追求高速连续采样。10 ms 采一次、做几次平均再输出足够应付绝大多数环境光检测场景。真要做高精度照度计那就要认真考虑光窗校准、温漂补偿和射线响应曲线已经不是一段简单驱动代码能解决的了。先从能稳定跑起来的版本开始后面再慢慢加东西也不迟。本文还有配套的精品资源点击获取