STM32F103C8T6驱动光敏传感器+OLED 显示实验(ADC 采集光敏电阻完整教程)
一、前言
本篇基于 STM32F103C8T6 最小系统板,搭配光敏电阻模块、0.96 寸 I2C OLED 屏幕,完成光照强度模拟量采集与实时显示。程序可输出 ADC 原始采样值、引脚电压、相对亮度百分比,自动区分昏暗 / 正常 / 明亮三种光照状态,加入 16 次均值滤波解决采样跳变问题,非常适合单片机 ADC 模拟采集入门学习,配套完整硬件接线、原理讲解、代码逻辑、调试标定方案,新手可直接复刻。
视频讲解:STM32F103C8T6驱动光敏传感器+OLED 显示实验(ADC 采集光敏电阻完整教程)https://www.bilibili.com/video/BV1xuMy6WENP/?share_source=copy_web&vd_source=91b967b41a17a5686913521545a8c3fb
二、整体实现流程
整体信号转换链路:环境光照强度变化 → 光敏电阻阻值改变 → 模块分压输出 AO 模拟电压 → STM32 PB1(ADC1 通道 9)采集 → 16 次均值滤波降噪 → 换算电压、相对亮度 → OLED 屏幕刷新所有数据 简单概括:光敏模块把光信号转为电压模拟量,STM32 通过 ADC 将电压转为数字信号,最终 OLED 可视化展示光照数据。
三、硬件接线说明
主控:STM32F103C8T6 外设:光敏传感器模块、I2C OLED 屏幕
3.1 引脚对应表
| 外设引脚 | STM32 引脚 | 功能说明 |
|---|---|---|
| 光敏模块 AO | PB1 | 模拟信号输出,ADC1_IN9 采集通道 |
| 光敏模块 VCC | 3.3V | 推荐 3.3V 供电,防止 AO 电压超 ADC 量程 |
| 光敏模块 GND | GND | 主控与传感器共地,保证电平基准统一 |
| OLED SCL | PB8 | 软件 I2C 时钟线 |
| OLED SDA | PB9 | 软件 I2C 数据线 |
| OLED VCC | 3.3V | 屏幕供电 |
| OLED GND | GND | 屏幕接地 |
3.2 AO 电压保护注意事项
STM32F103 内置 ADC 输入最大电压等于芯片供电 3.3V,因此光敏模块优先使用 3.3V 供电。 若项目必须使用 5V 给光敏模块供电:
- 先测量模块 AO 空载最高电压;
- 若电压超过 3.3V,必须在 AO 与 PB1 之间增加电阻分压电路降压,避免烧毁 ADC 外设。
补充:OLED 上显示的 Volt 为 PB1 引脚实际分压后电压,加装分压电路后数值不等于光敏模块原始 AO 输出电压。
3.3 AO 模拟输出与 DO 数字输出区别
市面上多数光敏模块同时引出 AO、DO 引脚 + 可调电位器,两者作用完全不同:
- AO:连续变化模拟电压,光照强弱全程线性变化,本实验使用该引脚,可精准读取光照变化数值;
- DO:比较器数字输出,只有高低电平两种状态,仅能判断光照是否超过阈值;
- 电位器:仅调节 DO 引脚电平翻转阈值,不会改变 AO 模拟电压输出。
对比 51 单片机常用 DO 数字读取方案:仅能获取亮 / 暗两种状态;本实验采集 AO 模拟量,可量化光照强度,教学与实操价值更高。
四、光敏传感器核心工作原理
模块核心器件为光敏电阻(LDR 光导电阻),材质为光敏半导体:
- 弱光 / 黑暗环境:内部导电载流子数量少,光敏电阻阻值极大;
- 强光环境:光能激发大量载流子,导电能力提升,光敏电阻阻值大幅减小。
光敏电阻仅能改变自身电阻,无法直接输出电压,因此模块内置分压电路,把电阻变化转化为可采集的 AO 电压信号。
标准分压电路原理图
3.3V ── 光敏电阻 ──┬── AO输出引脚 │ 固定下拉电阻 │ GND分压计算公式: AO 电压 = 3.3V × 固定电阻 ÷(光敏电阻阻值 + 固定电阻) 电路特性:环境越亮,光敏电阻阻值越小,AO 输出电压越低。
注意:部分厂家分压电路元器件调换,会出现光照越强 AO 电压越高,程序提供宏定义一键适配两种硬件接法,无需修改复杂运算逻辑。
五、STM32 ADC 采集原理
基础参数
- 使用通道:PB1 映射 ADC1 通道 9
- ADC 分辨率:12 位,采样数值范围 0 ~ 4095
- 输入电压量程:0V ~ 3.3V
- 电压换算公式:电压 (mV) = ADC 原始值 × 3300 ÷ 4095
采样数值对应参考表
| PB1 输入电压 | 对应 ADC 数值 | 满量程占比 |
|---|---|---|
| 0V | 0 | 0% |
| 0.6V | 744 | 18% |
| 1.65V | 2048 | 50% |
| 2.31V | 2867 | 70% |
| 3.3V | 4095 | 100% |
ADC 时钟与采样周期配置
- ADC 时钟分频:系统 72MHz 主频 ÷ 6 = 12MHz,低于 F103 ADC 最大 14MHz 限制,符合规格;
- 采样周期设置 239.5 个 ADC 时钟周期:光敏分压电路输出阻抗较高,长采样时间让芯片内部采样电容充分充电,降低测量误差,读数更稳定。
六、均值滤波算法(解决数值跳动)
光敏模拟信号易受市电灯光频闪、电源纹波、电磁干扰影响,单次采样数值会持续跳变,肉眼观察 OLED 数字抖动严重。 本工程采用16 次采样均值滤波降噪处理,执行逻辑:
- 循环启动 ADC 转换,连续读取 16 次原始 ADC 值并累加;
- 16 次采集完成后,总和除以 16 得到稳定平均 ADC 值;
- 使用平均值计算电压、亮度百分比,刷新屏幕。
补充:均值滤波只能消除随机噪声,无法过滤光源周期性频闪带来的规律性数值波动。
工程核心文件功能划分
| 文件路径 | 函数名 | 功能介绍 |
|---|---|---|
| GuangMin.c | GuangMin_Init() | PB1 引脚、ADC1 初始化,内置 ADC 自校准 |
| 同上 | GuangMin_ReadRaw() | 单次读取 ADC 原始采样值 |
| 同上 | GuangMin_ReadAverage() | 16 次采样均值滤波函数 |
| 同上 | GuangMin_RawToMilliVolts() | ADC 值转换为引脚实际电压 |
| 同上 | GuangMin_RawToPercent() | ADC 数值换算相对亮度百分比 |
| main.c | GuangMin_ShowData() | OLED 屏幕打印所有光照数据、明暗状态 |
七、OLED 屏幕显示格式与参数说明
程序上电后 OLED 固定排版输出,显示示例:
Light Sensor PB1 ADC:2048 Volt:1.65V Light:050% State:NORMAL各字段释义:
- ADC:12 位模数转换原始数值,区间 0~4095;
- Volt:PB1 引脚实测模拟电压;
- Light:相对亮度百分比,仅用于对比光照变化趋势,非标准照度勒克斯 (lx);
- State:光照分级状态,分三档:
- DARK:亮度百分比 < 30%(昏暗)
- NORMAL:30% ≤ 亮度 < 70%(正常光照)
- BRIGHT:亮度 ≥ 70%(强光)
八、亮度方向适配与阈值配置
8.1 硬件分压方向宏定义适配
不同光敏模块分压电路接法相反,电压随光照变化趋势不同,工程通过宏一键切换计算逻辑,宏定义存放于GuangMin.h:
#define GUANGMIN_BRIGHT_VALUE_INCREASES 0- 配置为 1:ADC 数值越大,亮度百分比越高(光照越强电压越高);
- 配置为 0:ADC 数值越小,亮度百分比越高(光照越强电压越低,本实验模块默认配置)。
本模块换算公式(反向模式): 亮度百分比 = 100 - ADC 原始值 × 100 ÷ 4095 实测效果:手遮挡光敏电阻 → ADC≈3971 → 亮度 3% → 屏幕显示 DARK;手电筒强光照射 → ADC 降低,亮度百分比上升,切换 NORMAL/BRIGHT。
8.2 明暗分级阈值
阈值宏定义在 main.c 中,可自由修改适配使用环境:
#define GUANGMIN_DARK_PERCENT 30 // 昏暗临界值 #define GUANGMIN_BRIGHT_PERCENT 70 // 强光临界值判断逻辑: 亮度<30% → DARK;30%≤亮度<70% → NORMAL;亮度≥70% → BRIGHT。 教室环境光线偏暗 / 偏亮时,可通过遮挡、强光照射记录实际亮度数值,修改阈值切换状态区间。
九、实验代码
main.c代码
/* * Photoresistor teaching demonstration * Sensor AO -> PB1 (ADC1 channel 9) * OLED SCL -> PB8, SDA -> PB9 */ #include "delay.h" #include "sys.h" #include "oled.h" #include "GuangMin.h" #define GUANGMIN_SAMPLE_COUNT 16 #define GUANGMIN_REFRESH_MS 200 #define GUANGMIN_DARK_PERCENT 30 #define GUANGMIN_BRIGHT_PERCENT 70 static void GuangMin_ShowData(u16 raw, u16 millivolts, u8 percent); int main(void) { u16 raw; u16 millivolts; u8 percent; delay_init(); NVIC_Configuration(); OLED_Init(); OLED_ColorTurn(0); OLED_DisplayTurn(0); OLED_Clear(); OLED_ShowString(8, 0, (u8 *)"Light Sensor", 16); OLED_ShowString(24, 24, (u8 *)"AO -> PB1", 16); OLED_ShowString(8, 48, (u8 *)"Initializing...", 16); OLED_Refresh(); GuangMin_Init(); delay_ms(500); while(1) { raw = GuangMin_ReadAverage(GUANGMIN_SAMPLE_COUNT); millivolts = GuangMin_RawToMilliVolts(raw); percent = GuangMin_RawToPercent(raw); GuangMin_ShowData(raw, millivolts, percent); delay_ms(GUANGMIN_REFRESH_MS); } } static void GuangMin_ShowData(u16 raw, u16 millivolts, u8 percent) { OLED_Clear(); OLED_ShowString(16, 0, (u8 *)"Light Sensor PB1", 12); OLED_ShowString(0, 13, (u8 *)"ADC:", 12); OLED_ShowNum(30, 13, raw, 4, 12); OLED_ShowString(0, 26, (u8 *)"Volt:", 12); OLED_ShowNum(30, 26, millivolts / 1000U, 1, 12); OLED_ShowString(36, 26, (u8 *)".", 12); OLED_ShowNum(42, 26, (millivolts % 1000U) / 10U, 2, 12); OLED_ShowString(54, 26, (u8 *)"V", 12); OLED_ShowString(0, 39, (u8 *)"Light:", 12); OLED_ShowNum(36, 39, percent, 3, 12); OLED_ShowString(54, 39, (u8 *)"%", 12); OLED_ShowString(0, 52, (u8 *)"State:", 12); if(percent < GUANGMIN_DARK_PERCENT) { OLED_ShowString(42, 52, (u8 *)"DARK", 12); } else if(percent < GUANGMIN_BRIGHT_PERCENT) { OLED_ShowString(42, 52, (u8 *)"NORMAL", 12); } else { OLED_ShowString(42, 52, (u8 *)"BRIGHT", 12); } OLED_Refresh(); }GuangMin.c代码
#include "GuangMin.h" #include "stm32f10x_adc.h" /* * 函数名称:GuangMin_Init * 函数功能:初始化光敏模块使用的 PB1 和 ADC1。 * 硬件连接:光敏模块 AO -> PB1 -> ADC1 通道 9。 */ void GuangMin_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; /* 开启 GPIOB 和 ADC1 的外设时钟。 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_ADC1, ENABLE); /* * 系统时钟为 72MHz 时,PCLK2 六分频后 ADC 时钟为 12MHz, * 不超过 STM32F103 数据手册规定的 14MHz 上限。 */ RCC_ADCCLKConfig(RCC_PCLK2_Div6); /* PB1 配置为模拟输入,模拟输入模式下不使用内部上拉或下拉。 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); /* ADC1 使用独立、单通道、单次转换和软件触发模式。 */ ADC_DeInit(ADC1); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); /* 使能 ADC1,并依次完成复位校准和 ADC 校准。 */ ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1) == SET) { } ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1) == SET) { } } /* * 函数名称:GuangMin_ReadRaw * 函数功能:读取一次光敏模块的 ADC 原始值。 * 返 回 值:0~4095,数值对应 PB1 上 0~3.3V 的模拟电压。 */ u16 GuangMin_ReadRaw(void) { /* * 选择 ADC1 通道 9。光敏电阻分压电路的输出阻抗可能较高, * 因此使用 239.5 个 ADC 周期的长采样时间,使采样结果更稳定。 */ ADC_RegularChannelConfig(ADC1, ADC_Channel_9, 1, ADC_SampleTime_239Cycles5); /* 启动软件转换,并等待转换完成标志置位。 */ ADC_SoftwareStartConvCmd(ADC1, ENABLE); while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET) { } return ADC_GetConversionValue(ADC1); } /* * 函数名称:GuangMin_ReadAverage * 函数功能:对多次 ADC 采样结果求平均,降低随机噪声和显示抖动。 * 输入参数:sample_count,采样次数;传入 0 时自动执行一次采样。 * 返 回 值:多次采样的平均 ADC 原始值。 */ u16 GuangMin_ReadAverage(u8 sample_count) { u8 i; /* 使用 32 位变量累加,避免多次采样时发生 16 位数据溢出。 */ u32 sum = 0; if(sample_count == 0) { return GuangMin_ReadRaw(); } for(i = 0; i < sample_count; i++) { sum += GuangMin_ReadRaw(); } return (u16)(sum / sample_count); } /* * 函数名称:GuangMin_RawToMilliVolts * 函数功能:把 12 位 ADC 原始值换算成毫伏值。 * 计算公式:电压(mV) = ADC原始值 × 3300 ÷ 4095。 */ u16 GuangMin_RawToMilliVolts(u16 raw) { /* 对异常输入进行限幅,保证换算结果不超过参考电压。 */ if(raw > GUANGMIN_ADC_MAX_VALUE) { raw = GUANGMIN_ADC_MAX_VALUE; } /* 除法前加上除数的一半,实现整数计算的四舍五入。 */ return (u16)(((u32)raw * GUANGMIN_ADC_REFERENCE_MV + (GUANGMIN_ADC_MAX_VALUE / 2U)) / GUANGMIN_ADC_MAX_VALUE); } /* * 函数名称:GuangMin_RawToPercent * 函数功能:把 ADC 原始值换算成 0~100% 的亮度百分比。 * 说 明:亮度增减方向由 GUANGMIN_BRIGHT_VALUE_INCREASES 宏决定。 */ u8 GuangMin_RawToPercent(u16 raw) { u32 percent; /* 对异常输入进行限幅,避免计算结果超过 100%。 */ if(raw > GUANGMIN_ADC_MAX_VALUE) { raw = GUANGMIN_ADC_MAX_VALUE; } /* 使用整数运算计算百分比,并通过加上除数的一半完成四舍五入。 */ percent = ((u32)raw * 100U + (GUANGMIN_ADC_MAX_VALUE / 2U)) / GUANGMIN_ADC_MAX_VALUE; #if GUANGMIN_BRIGHT_VALUE_INCREASES /* AO 电压随环境亮度增加时,直接返回计算出的百分比。 */ return (u8)percent; #else /* AO 电压随环境亮度降低时,对百分比进行反向换算。 */ return (u8)(100U - percent); #endif }GuangMin.h代码
#ifndef __GUANGMIN_H #define __GUANGMIN_H #include "sys.h" /* * 光敏模块驱动头文件 * * 硬件连接: * 光敏模块 AO -> STM32F103C8T6 的 PB1(ADC1 通道 9) * 光敏模块 VCC -> 3.3V * 光敏模块 GND -> GND */ /* STM32F103 内部 ADC 为 12 位,参考电压按 3.3V 计算。 */ #define GUANGMIN_ADC_MAX_VALUE 4095U #define GUANGMIN_ADC_REFERENCE_MV 3300U /* * 亮度方向配置: * 1:环境越亮,AO 电压和 ADC 数值越大; * 0:环境越亮,AO 电压和 ADC 数值越小。 * * 本工程实测模块在遮挡时 AO 约为 3.2V,说明其输出电压随环境 * 变暗而升高,因此这里设置为 0,对 ADC 百分比进行反向换算。 */ #define GUANGMIN_BRIGHT_VALUE_INCREASES 0 /* 初始化 PB1 和 ADC1。 */ void GuangMin_Init(void); /* 进行一次 ADC 转换,返回范围为 0~4095 的原始值。 */ u16 GuangMin_ReadRaw(void); /* 连续采样 sample_count 次并返回平均值,用于减小显示抖动。 */ u16 GuangMin_ReadAverage(u8 sample_count); /* 把 ADC 原始值换算为毫伏,返回范围为 0~3300mV。 */ u16 GuangMin_RawToMilliVolts(u16 raw); /* 把 ADC 原始值换算为亮度百分比,返回范围为 0~100%。 */ u8 GuangMin_RawToPercent(u16 raw); #endif十、实验标定与误差影响因素
光敏电阻存在明显非线性特性与个体差异,实验读数会受多重外界条件干扰:
- 光源类型:太阳光、白炽灯、LED 灯光谱不同,同等距离下采样数值差距大;
- 光源距离、照射角度:光照入射角度、远近直接改变光敏电阻接收光能量;
- 遮挡干扰:外壳、导线、手部阴影遮挡感光区域;
- 供电电压波动:3.3V 电源纹波会同步影响分压 AO 与 ADC 换算精度;
- 器件非线性:光敏电阻阻值和真实照度不成线性正比,百分比仅为相对参考。