STM32F103C8T6驱动光敏传感器+OLED做环境光检测,是我最近在stm32f103c8t6最小系统板上跑通的一个小实验。板上自带的ADC外设采集光敏电阻的模拟电压,再用HAL库点亮OLED屏幕实时刷新数值,整个过程不复杂,但把ADC采样、I2C通信、显示驱动这几个嵌入式基本功全串起来了。这篇教程会从硬件连接讲到代码实现,再附上我实测踩过的坑,跟着走一遍就能跑出效果。
1. 硬件准备与电路原理拆解
1.1 所需物料清单
先列一下我用的材料,都是常见件,某宝几块钱就能搞定:
| 元件 | 型号/规格 | 数量 | 备注 |
|---|---|---|---|
| 主控板 | STM32F103C8T6最小系统板 | 1块 | 蓝色pill板,带晶振和复位电路 |
| 光敏电阻 | GL5516 / 5528 | 1个 | 亮阻5-10kΩ,暗阻1MΩ以上 |
| 固定电阻 | 10kΩ(1%) | 1个 | 用于分压电路 |
| OLED屏 | 0.96寸 I2C接口 | 1块 | SSD1306驱动芯片,128x64分辨率 |
| 面包板 | 830孔 | 1个 | 搭电路方便 |
| 杜邦线 | 公对公、母对母 | 若干 | 连接用的 |
| 下载器 | ST-Link V2 或 USB转TTL | 1个 | 我用的ST-Link,Keil里直接下载 |
有几点要特别说明。STM32F103C8T6是Cortex-M3内核,72MHz主频,20KB RAM,64KB Flash,跑这种小实验绰绰有余。蓝板子最小系统板一般板上已经焊好了8MHz晶振和上电复位电路,所以插上ST-Link就能用。OLED我强烈建议选I2C接口的,虽然SPI接口的刷新速度更快,但在这种小数据量显示场景下,I2C连线少、代码也好写,更适合新手起步。
1.2 光敏电阻分压电路原理
光敏电阻的核心特性就是阻值随光照强度改变。拿常用的GL5516来说,室内正常光照下阻值大约在5kΩ到20kΩ之间,用手完全遮住能到几百千欧甚至兆欧级别。如果直接用ADC量它两端的电压是不行的,因为ADC只能测电压、不能直接测电阻,所以要先做一个分压电路,把电阻变化转换成电压变化。
我的接法是这样的:光敏电阻一端接3.3V,另一端接到PA0引脚(同时也是ADC1的通道0),然后从PA0这个点再接一个10kΩ的固定电阻到GND。这样光敏电阻和10kΩ电阻形成串联分压,PA0点的电压就是分压值。
这个电路的核心公式是:Vout = 3.3V × R_fixed / (R_light + R_fixed)。
当光照增强时,光敏电阻阻值变小,根据公式Vout会变大(因为分母变小,分子不变)。反之光照减弱时,Vout会变小。所以这个电路测到的原始ADC值,数值越大代表光照越强,这个方向要牢记,后期写代码映射光照强度的时候才不会搞反。
有人会问为什么选10kΩ这个固定电阻。这里其实有个经验法则:固定电阻的阻值最好落在光敏电阻最关心的阻值变化区间的中值附近。我用的GL5516在室内光下大概是10kΩ上下,所以选10kΩ的固定电阻,可以让分压比在光照常见范围内保持较好的线性度和灵敏度。如果你用的是别的型号,比如暗阻特别大的,可以换成100kΩ试一下,规则是一样的。
1.3 STM32F103C8T6的ADC资源说明
STM32F103C8T6内置一个12位逐次逼近型ADC(SAR ADC),最大转换精度12位,也就是说采样值范围是0~4095,对应0~3.3V的电压输入。这里有一个重要特性:ADC的参考电压VREF+在LQFP48封装里是内部连接到VDDA(3.3V供电),不像大容量型号有独立引脚可以接外部精密基准电压,所以精度上限受VDDA稳定性影响。
这款芯片的ADC有两大转换模式:规则组(Regular Group)和注入组(Injected Group)。规则组就是我们常用的扫描转换模式,可以配置多个通道依次转换,转换结果存放在一个32位寄存器里。注入组优先级更高,有独立的结果寄存器,适合紧急优先级高的突发采样。我们这个实验只需要一个通道,用规则组就完全够了。
还有一个重要的知识点是ADC时钟和采样周期。STM32F103的ADC时钟最高只能跑到14MHz,超过这个值转换结果就不稳定了。由于ADC时钟源是PCLK2(APB2总线时钟),在72MHz主频下,我配置ADC预分频为6分频,得到12MHz的ADC时钟。每个通道的采样时间我设置为55.5个时钟周期(约为55.5/12MHz ≈ 4.6微秒),再加上12个周期(1微秒)的逐次逼近转换时间,单次转换大约5.6微秒,对光敏电阻这种慢变信号来说完全够用。如果你的信号变化很快,可以把采样周期缩短到最快1.5个周期,但光敏电阻不需要这么激进。
2. 用CubeMX生成工程模板
2.1 新建工程并配置时钟树
我习惯先用STM32CubeMX生成HAL库工程,把引脚和时钟配置成可视化操作,然后再去Keil里补业务代码,这样能省很多事。
打开CubeMX后,选择芯片型号STM32F103C8Tx,进入主界面。在“System Core”->“RCC”里,把HSE(高速外部时钟)设置为Crystal/Ceramic Resonator,这样会启用板子上的8MHz外部晶振。接着在“Clock Configuration”(时钟树)界面里,需要手动把系统主频拉到72MHz。
具体操作为:在HCLK输入框里键入72,敲回车,CubeMX会自动分配各总线时钟。这时注意看APB2 Prescaler是否为/2,因为PA0对应的ADC1挂在APB2上,APB2总线时钟是72MHz,而ADC最大只能14MHz,所以后期配置ADC时APB2预分频必须设为/6或合适的值,保证ADC时钟不超过14MHz这个极限。如果你对时钟树不熟,这个界面里会有红色警告,提示当前配置超出外设限制,跟着提示改就行。
2.2 ADC1通道0的引脚与参数配置
引脚分配在“Pinout & Configuration”界面左侧。点击“Analog”下的“ADC1”,勾选“IN0”通道,这样PA0会被自动分配为ADC输入引脚。
接着进入“Parameter Settings”页签,我的配置是这样的(直接抄作业就可以):
| 参数项 | 配置值 | 选择理由 |
|---|---|---|
| Clock Prescaler | 6分频 | ADC时钟=72/6=12MHz,满足≤14MHz要求 |
| Resolution | 12 bit | 满分辨率,精度最高 |
| Data Alignment | Right alignment | 12位数据右对齐,读取方便 |
| Scan Conversion Mode | Disabled | 单通道不需要扫描 |
| Continuous Conversion Mode | Enabled | 连续转换,配合中断/轮询实时刷新 |
| Discontinuous Conversion Mode | Disabled | 单通道不需要 |
| DMA Continuous Requests | Disabled | 不用DMA,轮询模式足够 |
| End Of Conversion Selection | EOC flag at end of channel | 每次转换结束置EOC标志 |
| Number Of Conversion | 1 | 只用通道0 |
| Sampling Time | 55.5 Cycles | 采样时间折中,数值稳定又不至于太慢 |
这里多说一句Continuous Conversion Mode。如果连续转换模式开启,ADC会自动一轮接一轮地转换,我们只需要在while循环里读取最新值,逻辑上更简单省事。代价是功耗稍高,但对于接USB供电的实验场景根本无所谓。
2.3 配置硬件I2C还是软件模拟I2C
OLED这边涉及I2C通信。我在CubeMX里没有直接启用硬件I2C外设,而是选择用普通GPIO口软件模拟I2C协议。理由很现实:STM32F103系列硬件I2C的HAL库实现有些历史遗留问题,经常出现总线挂死、时钟拉不下来的情况,处理起来很头疼。网上大量反馈也都是F1的硬件I2C太脆,而软件模拟I2C虽然牺牲了一点性能,但时序完全可控、排错容易,驱动SSD1306这种低速率屏幕根本感觉不到差别。
所以OLED的SCL和SDA我分配在两个普通IO口上:SCL用PB6,SDA用PB7。在CubeMX里把这两个引脚设置为Output Push Pull模式,初始输出高电平。后面写驱动代码的时候全部用软件翻转来控制时序,完全避开硬件I2C的坑。
这两个引脚的命名,我建议在CubeMX的“User Labels”里改成自定义名字,比如把PB6改成OLED_SCL,PB7改成OLED_SDA。这样后面生成的代码里引脚宏定义就自动变成OLED_SCL_Pin、OLED_SDA_Pin,可读性高很多,写代码时不用回去翻原理图。
2.4 生成工程和Keil配置项
引脚和时钟配置完成后,点击“Project Manager”页签,设置工程名、保存路径,在“Toolchain/IDE”里选择“MDK-ARM”也就是Keil5。在“Code Generator”里勾选“Generate peripheral initialization as a pair of .c/.h files per peripheral”,这样每个外设单独生成一个文件,后续维护方便。
点击“Generate Code”生成工程后,用Keil打开。这里有几个坑要提前说。
第一个是下载设置。如果用的是ST-Link,进入“Options for Target”->“Debug”里选择ST-Link Debugger,然后在“Flash Download”里勾选“Reset and Run”。不然每次烧录完程序,芯片不会自动复位运行,得手动按一下板子的复位键才能看到效果。
第二个是下载速度。如果板线比较长或者ST-Link是山寨货,把下载速度调低到1MHz以下,否则经常报出“RDDI-DAP Error”之类的连接错误。
第三个是HAL库的优先级。CubeMX默认会把SysTick配置为1ms中断,而HAL_Delay函数是依赖SysTick的,如果你的代码里用了其他中断,注意不要在中断回调函数里调用HAL_Delay,否则时基会错乱。
3. HAL库代码实现:从ADC采集到OLED显示
3.1 扒开ADC采集的HAL库API
现在开始写代码。在CubeMX生成的main.c中,ADC1已经被初始化好了,我们只需要在业务代码里调用HAL库函数读取转换值。
先看ADC的启动流程。最基本的三步:调用HAL_ADC_Start开始转换,调用HAL_ADC_PollForConversion等待转换完成,调用HAL_ADC_GetValue拿结果。
uint32_t adc_value = 0; float voltage = 0.0f; // 启动ADC转换(因为是连续模式,启动一次后自动不断转换) HAL_ADC_Start(&hadc1); while (1) { // 等待转换完成,超时时间设为100ms if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK) { // 读取12位转换结果 0~4095 adc_value = HAL_ADC_GetValue(&hadc1); // 换算成实际电压值 0~3.3V voltage = (float)adc_value / 4095.0f * 3.3f; HAL_Delay(100); } }这段代码是ADC采集的最小框架。由于CubeMX里开了Continuous Conversion Mode,HAL_ADC_Start调用一次后,ADC硬件就会自动连续不停地转换,我们只是周期性地去拿一下最新结果。PollForConversion函数的作用是等EOC标志位置位,返回值是HAL_OK表示转换完成。如果超时返回HAL_TIMEOUT,那就是配置有问题,常见原因是ADC时钟没配好或者引脚被其他外设占用。
有个细节值得提醒:HAL_ADC_GetValue返回的数据类型是uint32_t,但有用的只有低12位。如果你配置成了右对齐(Right alignment),那高20位全是0,但如果你哪天改成左对齐,数据就会整体左移4位,最大值变成65535。所以一定要确保对齐方式和读取代码是匹配的,不然算出来的电压完全不对。
3.2 多重采样滤波,解决数值抖动
直接用单次采样值驱动显示,很快你就会发现串口打印出来的数值上下乱蹦,明明光照没变化,数字却在几十个单位内跳动。这就是量化噪声和环境干扰,光敏电阻本身的响应也有微小波动。
我在这套代码里做了三重处理:连续采样30次,去掉最大值和最小值,剩下求算术平均。这个方法在信号处理里叫防脉冲干扰平均滤波法,对付随机尖峰干扰效果很好。
#define SAMPLE_COUNT 30 // 对ADC某一通道进行多次采样并做滤波处理 uint32_t get_adc_average(void) { uint32_t sum = 0; uint16_t value = 0; uint16_t min_val = 4095; uint16_t max_val = 0; uint8_t valid_count = 0; for (uint8_t i = 0; i < SAMPLE_COUNT; i++) { if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK) { value = HAL_ADC_GetValue(&hadc1); if (value < min_val) min_val = value; if (value > max_val) max_val = value; sum += value; valid_count++; } HAL_Delay(1); // 每次采样之间间隔1ms,让电容充分放电 } // 去掉一个最大值和一个最小值,剩余的有效样本数 if (valid_count > 2) { return (sum - min_val - max_val) / (valid_count - 2); } else { return 0; } }这里每个样本间隔1ms,千万别把间隔改成0。ADC采样过程中采样电容需要充电时间,如果连续高速采集,相邻几次采样结果相关性太强,滤波效果会大打折扣。间隔1ms后,每次采到的都是相互独立的样本,30个样本覆盖30ms,对光敏电阻这种缓慢变化的信号,既能滤掉高频噪声,又不至于引入明显的延迟。
3.3 把ADC原始值映射成直观的光照等级
从ADC拿到的原始值是0~4095的整数,直接显示在OLED上虽然也能用,但对用户不够友好。我做了两层映射。
第一层映射到电压值,这个上文写过了,用公式voltage = (float)adc_value / 4095 * 3.3就能得到0~3.3V之间的电压。第二层映射我想做成一个“光照等级”的概念,让显示效果既有科技感又直观。思路是这样的:ADC值越大表示光照越强(因为分压电路里光敏电阻在下面,光照强时阻值小、分压电压大),那我就做一个等级划分规则:
const char* get_light_level(uint32_t adc_val) { if (adc_val < 600) { return "Dark"; } else if (adc_val < 1500) { return "Dim"; } else if (adc_val < 2800) { return "Bright"; } else { return "Strong"; } }阈值怎么定的?我实测在窗户边阳光直射的时候ADC值能到3500以上,室内明亮灯光下是2500~2800左右,拉上窗帘是1000多,用手完全捂住传感器就只有200~300。这几个阈值是基于实际环境标定的,如果你在别的环境下用,阈值需要自己重新测一遍。把这个等级直接显示在OLED上,比单纯看数字更直观。
关于很多新手关心的“能不能算成光照度lux单位”,这里必须说实话。光敏电阻的阻值-光照度关系是非线性的,数据手册上通常给的是对数坐标下的曲线,而且不同批次离散性很大。没有标准光源标定设备,仅靠单片机和ADC根本算不出可靠的lux值。所以我这版代码就显示电压值和光照等级,不做lux换算。有同行在博客里用查表法硬套一个标定曲线,那需要正规照度计测量多个点之后拟合,不是这篇教程的范畴。
3.4 显示数据到OLED:选字库、定位、刷新
OLED驱动这块,我以SSD1306为例写了一个精简版驱动,就是大家常说的“OLED六件套”:初始化函数、清屏函数、设置光标、画点、显示单个字符、显示字符串。
SSD1306的显示原理是:它在内部有一块128x64的GRAM(图形显示存储器),每个bit对应屏幕上一个像素点。我们通过I2C向它的GRAM写入1或0,对应像素点亮或灭。我的OLED驱动里维护了一个128x8字节的缓冲区(因为屏分8页,每页8行像素),所有绘制操作先在缓冲区里改,修改完成后用一次I2C命令把整个缓冲区刷到屏幕GRAM上。这样做的好处是显示不闪烁,因为往SSD1306写数据是一个字节一个字节地写,直接写GRAM会造成可见的撕裂。
显示英文字符我用的是标准6x8点阵字库,这个字库在很多开源工程里都能找到。要显示汉字就得用16x16点阵字库还要自己取模,在这个实验里用不上,所以没加。
驱动初始化函数尤其要注意的,是SSD1306的初始化时序不能搞错。这个屏的初始化序列长得很,但标准做法就是发送一系列配置命令,包括关闭显示、设置显示时钟分频、设置复用率、设置显示偏移、开启显存显示、点亮屏幕等等。很多新手从网上复制初始化代码后屏幕不亮,大概率就是某条命令的顺序不对或者I2C时序有问题,我后面会在问题排查里详细写。
显示字符串的函数如下:
// 在OLED指定的页和列显示字符串,页取值0~7,列0~127 void OLED_ShowString(uint8_t page, uint8_t x, const char *str) { while (*str != '\0') { // 每显示一个字符,x坐标向后移动6个像素(字符宽度6) OLED_ShowChar(page, x, *str); x += 6; str++; // 防止字符串过长超出屏幕范围 if (x > 122) { x = 0; page++; if (page > 7) return; } } }我测试过,写满一屏中文或者三行英文,从修改缓冲区到完整刷新到屏幕,整个过程大约十几毫秒,肉眼完全看不到闪烁。配合100毫秒的采样周期,OLED显示出的数值在视觉上是很平滑的。
3.5 主循环的整体流程设计
主循环逻辑很直白,先读取ADC滤波值,然后更新显示。下面是我在main函数里的while循环完整代码:
while (1) { // 1. 读取ADC采样值并做滤波 uint32_t adc_val = get_adc_average(); // 2. 根据ADC值映射光照等级 const char *level = get_light_level(adc_val); // 3. 计算电压值 float voltage = (float)adc_val / 4095.0f * 3.3f; // 4. 格式化缓冲区 char buf[24]; // 5. 更新OLED显示 OLED_Clear(); OLED_ShowString(0, 8, "ADC Value:"); snprintf(buf, sizeof(buf), "%4d", (int)adc_val); OLED_ShowString(1, 8, buf); OLED_ShowString(2, 8, "Voltage:"); snprintf(buf, sizeof(buf), "%.2f V", voltage); OLED_ShowString(3, 8, buf); OLED_ShowString(4, 8, "Light Level:"); OLED_ShowString(5, 8, level); // 把缓冲区内容一次性刷新到OLED OLED_Refresh(); // 每200ms更新一次 HAL_Delay(200); }注意OLED_ShowString的第一参数是页号,0~7对应屏幕从上到下8行,每行高度8个像素。常见的0.96寸OLED屏是128x64,所以一行最多显示128/6=21个英文字符。
界面布局上,第一行显示ADC原始值,第二行显示电压,第三行显示光照等级,分别占据页0-1、页2-3、页4-5。这样留出最底下一行用来显示状态或者日期,后续扩展时不用推翻重来。
4. 硬件连接和实测效果记录
4.1 杜邦线怎么接才不容易出错
我按“先电源后信号、先模块后MCU”的顺序来连线,接线表整理如下:
| OLED引脚 | 连接到 | 光敏传感器元件 | 连接到 |
|---|---|---|---|
| VCC | 3.3V | 光敏电阻一脚 | 3.3V |
| GND | GND | 光敏电阻另一脚 | PA0 |
| SCL | PB6 | 10kΩ电阻一端 | PA0(同一节点) |
| SDA | PB7 | 10kΩ电阻另一端 | GND |
这里有个新手非常容易犯的错误:OLED的VCC不能接5V。虽然很多0.96寸OLED商家说支持3.3V~5V,但SSD1306本身的工作电压上限是3.6V,板上虽然有稳压甚至降压电路,但为了安全,我建议一律接3.3V。接了5V屏幕虽然是能亮,但长期使用芯片会发热,寿命受影响。
光敏电阻的引脚是无极性的,怎么接都行,但要注意固定电阻那一路别接错位置。如果10kΩ电阻误接到了3.3V那端,那你测到的电压变化方向就会反过来,光照越强ADC值越小,映射逻辑就全反了。
面包板的走线也有讲究。光敏电阻和固定电阻的这个分压节点,如果跟ADC引脚之间的杜邦线太长,容易引入噪声。我从分压节点到PA0之间只用了一根大概10厘米的公对公杜邦线,实测ADC值波动极小,完全可以不额外加滤波电容。如果线长超过了30厘米,建议在PA0对GND之间并一个0.1uF的陶瓷电容,效果立竿见影。
4.2 烧录下载与串口调试技巧
写完代码后,编译下载前要确保Keil的“Use MicroLIB”复选框已勾上。不勾选的话,如果你后面用了printf和snprintf,代码体积会爆炸,Flash可能装不下,而且用微库的printf更省资源。
下载成功后,程序会自动复位运行。我在代码里专门加了一段开机自检逻辑:OLED先全屏点亮1秒,然后全屏熄灭1秒,再进入正常显示。这样如果OLED接线有问题,在开机瞬间就能看出来,不用等主循环跑起来才发现屏幕是黑的,排查范围一下子就缩小了。
如果你想让ADC数值看得更清楚或者做个数据曲线记录,建议再接一个USB转TTL模块做串口调试。把PA9(USART1_TX)接到模块的RX,PA10(USART1_RX)接到模块的TX,波特率设115200,然后在代码里用printf输出采样值。每次按键触发一次采样然后打印,比看屏幕上的数字更稳定,尤其适合用来标定光照等级的阈值。
串口接的时候也有个经典大坑:共地。USB转TTL模块的GND必须和STM32开发板的GND相连,不然串口收发会出现乱码或者完全不通。我在第一次调试时就忘了共地,折腾了十几分钟才反应过来,后来把共地列为了必查项。
4.3 实测数据记录与分析
我分别在五种环境下做了测试,记录下来的数据如下:
| 环境 | ADC原始值 | 计算电压(V) | 光照等级 |
|---|---|---|---|
| 手完全捂住光敏电阻 | 205 | 0.17 | Dark |
| 拉上窗帘的室内 | 1040 | 0.84 | Dim |
| 正常室内照明 | 2380 | 1.92 | Bright |
| 台灯近距离照射 | 3480 | 2.81 | Strong |
| 窗外阳光直射 | 3950 | 3.18 | Strong |
从数据能看到两个重要的现象。第一,ADC值和光照之间确实存在单调递增关系,但并不是线性的。从暗到亮,数值变化率不一样,说明把ADC值直接线性映射成“光照强度百分比”是不科学的,用等级划分反而更合理。
第二,即使把手完全捂住传感器,ADC值也没有掉到0,而是一个200左右的底噪。原因有两层:一是光敏电阻的暗阻不可能是无穷大,二是PA0引脚本身会残留一点环境噪声。这个底噪值可以作为软件校准的参考点,比如在Dark等级判断时用“adc_val < 600”,而不是“adc_val < 50”,就是为了给底噪留出余量。
5. 常见问题排查与避坑记录
5.1 OLED屏幕不亮或显示乱码
这个是最常见的问题,我在调试中也翻过车。排查顺序建议按下面这个表来,从简单到复杂:
| 现象 | 排查方向 | 解决方案 |
|---|---|---|
| 屏幕完全不亮 | 电源检查 | 确认VCC是否3.3V,GND是否共地 |
| 屏幕亮但无字符 | I2C地址错误 | SSD1306通常为0x3C或0x3D,代码里改地址 |
| 点亮但花屏乱码 | 初始化时序不对 | 检查初始化序列,确认波特率/时序匹配 |
| 显示一半亮一半暗 | 对比度设置异常 | 调整SSD1306的Set Contrast命令参数 |
| 上电几秒后熄灭 | 看门狗复位 | 检查I2C通信是否造成HAL_Delay阻塞 |
关于I2C地址,网上有人用0x3C,有人用0x3D,其实跟OLED板子上一个电阻的位置有关。大多数0.96寸模块的SA0引脚被拉低,地址就是0x3C;但也有模块出厂默认0x3D。我的做法是在驱动头文件里定义了一个宏OLED_ADDR,直接用I2C_Scan的例程扫描一遍,确认实际地址再用。
SDD1306初始化的时序问题,最大概率是出现在I2C的起始信号和停止信号上。软件模拟I2C实现时,SCL和SDA的电平变化间隔必须是微秒级,不能一上来就无延时死推,否则从机跟不上,识别不到合法的主机命令。我用的软件模拟I2C,在每翻转一次时钟线后加了一个__NOP()空指令的微小延时,实测非常稳定。
5.2 ADC采集值跳动过大
如果滤波代码都加上了,但OLED显示的数值还是跳动,优先级最高的怀疑对象是ADC参考电压不稳定。STM32F103C8T6内部没有独立的外部电压基准引脚,ADC的参考电压直接取自VDDA。如果USB供电不稳,VDDA会有纹波,ADC参考就跟着晃,结果当然跳。
解决方案是在3.3V和GND之间并联两个电容:一个大一点的100uF电解电容滤低频,一个小一点的0.1uF陶瓷电容滤高频。这两个电容要尽量靠近最小系统板的电源引脚。我加上之后,ADC稳定度肉眼可见地提升。
另外,如果PA0的杜邦线接触不良或者面包板插孔氧化,也可能引起数值跳动。这种问题老旧位置难查,建议直接换一根新杜邦线测试,省时间。
5.3 光敏电阻灵敏度不够怎么办
有读者反馈说,室内灯光变化范围下ADC值变化不明显,基本都是高亮档,区分度太低。这个问题基本是固定电阻阻值没选对。如果你长期在弱光环境下使用,固定电阻要调大;如果常在强光下使用,固定电阻要调小。
原理就是分压比设计。假设光照强度让光敏电阻阻值从R_min变到R_max,如果固定电阻R_fixed远远小于R_light,那电压变化就会被压缩在一个很小的范围内;反过来,如果R_fixed远远大于R_light,也一样。只有R_fixed设置在光敏电阻常见阻值附近,电压变化才最灵敏。我的GL5516在室内光下大约是10kΩ,所以选10kΩ最合适。
如果你手头没有合适的电阻,可以用一个20kΩ的可调电位器来代替固定电阻,现场调节到ADC值在你最关注的亮度范围附近变化率最大,然后拆下来量一下阻值用固定电阻替换,这就是最原始的灵敏度标定方法。
5.4 CubeMX重新生成代码覆盖自己代码的问题
这个坑很多新手会踩:在CubeMX里改了某个引脚配置,重新生成代码后,发现自己在main.c里写的while循环逻辑全没了。CubeMX生成代码时,会把“User Code Begin”和“User Code End”注释之间的内容保留,而这两个注释之外的自定义代码会被清掉。
所以我的习惯是把所有用户业务代码都写在:
/* USER CODE BEGIN WHILE */ while (1) { // ... 业务代码 } /* USER CODE END WHILE */之间。如果有一段代码想跨文件保存,比如OLED驱动、滤波函数,建议新建一个用户自己的头文件和源文件(比如bsp_adc_oled.c和bsp_adc_oled.h),把CubeMX的自动生成和手写业务代码隔离,这样不管CubeMX怎么重新生成都不会影响你的核心逻辑。我在多次项目迭代中都是用这个办法,基本能做到CubeMX随便改配置、业务代码毫发无损。
6. 基于这套实验还能怎么扩展
一个简单的光敏传感器+OLED实验做完后,它的价值远不止“点个灯”这么简单。ADC采样是整个嵌入式系统中最基础也最常用的功能,能用到这个套路的场景非常多。
可以做智能灯控:光敏电阻检测环境光强度,同时驱动继电器控制台灯,光线不足时自动开灯。只需要把本文的ADC部分保留,把OLED部分换成继电器驱动电路和定时逻辑,代码在现有框架上扩展几十行就够。
可以做环境光记录器:把ADC采样值按时间戳存到外挂Flash或者EEPROM里,做一个简易的“光照日志仪”。这对于花房、温室、图书馆这类对光照时长有要求的场景非常实用。OLED界面可以改成显示历史曲线,用像素点画折线图,正好发挥128x64分辨率。
可以和ESP8266一键联网:通过串口把当前光照数据发给ESP8266模块,再经由Wi-Fi上报到MQTT服务器,做一个“光敏传感器IoT节点”。我之前在另一个项目里做过类似的事,本项目的ADC滤波代码可以直接挪过去复用,只需把OLED刷新部分换成AT指令拼包发送。
我的体会是,STM32F103C8T6这块芯片虽然已经有替代的新型号出现,但它的外设库、网上资料和生态支持太成熟了,用来练手和学习非常合适。只要把这个ADC采集和OLED显示的流程吃透,后续无论是接温湿度传感器、空气质量传感器还是音频采集,原理都是一模一样的,换汤不换药。希望这篇教程能帮你在嵌入式这条路上多踩平一个坑,少走一段弯路。