
STM32F103C8T6驱动光敏传感器OLED做环境光检测是我最近在stm32f103c8t6最小系统板上跑通的一个小实验。板上自带的ADC外设采集光敏电阻的模拟电压再用HAL库点亮OLED屏幕实时刷新数值整个过程不复杂但把ADC采样、I2C通信、显示驱动这几个嵌入式基本功全串起来了。这篇教程会从硬件连接讲到代码实现再附上我实测踩过的坑跟着走一遍就能跑出效果。1. 硬件准备与电路原理拆解1.1 所需物料清单先列一下我用的材料都是常见件某宝几块钱就能搞定元件型号/规格数量备注主控板STM32F103C8T6最小系统板1块蓝色pill板带晶振和复位电路光敏电阻GL5516 / 55281个亮阻5-10kΩ暗阻1MΩ以上固定电阻10kΩ1%1个用于分压电路OLED屏0.96寸 I2C接口1块SSD1306驱动芯片128x64分辨率面包板830孔1个搭电路方便杜邦线公对公、母对母若干连接用的下载器ST-Link V2 或 USB转TTL1个我用的ST-LinkKeil里直接下载有几点要特别说明。STM32F103C8T6是Cortex-M3内核72MHz主频20KB RAM64KB 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位逐次逼近型ADCSAR ADC最大转换精度12位也就是说采样值范围是0~4095对应0~3.3V的电压输入。这里有一个重要特性ADC的参考电压VREF在LQFP48封装里是内部连接到VDDA3.3V供电不像大容量型号有独立引脚可以接外部精密基准电压所以精度上限受VDDA稳定性影响。这款芯片的ADC有两大转换模式规则组Regular Group和注入组Injected Group。规则组就是我们常用的扫描转换模式可以配置多个通道依次转换转换结果存放在一个32位寄存器里。注入组优先级更高有独立的结果寄存器适合紧急优先级高的突发采样。我们这个实验只需要一个通道用规则组就完全够了。还有一个重要的知识点是ADC时钟和采样周期。STM32F103的ADC时钟最高只能跑到14MHz超过这个值转换结果就不稳定了。由于ADC时钟源是PCLK2APB2总线时钟在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 Prescaler6分频ADC时钟72/612MHz满足≤14MHz要求Resolution12 bit满分辨率精度最高Data AlignmentRight alignment12位数据右对齐读取方便Scan Conversion ModeDisabled单通道不需要扫描Continuous Conversion ModeEnabled连续转换配合中断/轮询实时刷新Discontinuous Conversion ModeDisabled单通道不需要DMA Continuous RequestsDisabled不用DMA轮询模式足够End Of Conversion SelectionEOC flag at end of channel每次转换结束置EOC标志Number Of Conversion1只用通道0Sampling Time55.5 Cycles采样时间折中数值稳定又不至于太慢这里多说一句Continuous Conversion Mode。如果连续转换模式开启ADC会自动一轮接一轮地转换我们只需要在while循环里读取最新值逻辑上更简单省事。代价是功耗稍高但对于接USB供电的实验场景根本无所谓。2.3 配置硬件I2C还是软件模拟I2COLED这边涉及I2C通信。我在CubeMX里没有直接启用硬件I2C外设而是选择用普通GPIO口软件模拟I2C协议。理由很现实STM32F103系列硬件I2C的HAL库实现有些历史遗留问题经常出现总线挂死、时钟拉不下来的情况处理起来很头疼。网上大量反馈也都是F1的硬件I2C太脆而软件模拟I2C虽然牺牲了一点性能但时序完全可控、排错容易驱动SSD1306这种低速率屏幕根本感觉不到差别。所以OLED的SCL和SDA我分配在两个普通IO口上SCL用PB6SDA用PB7。在CubeMX里把这两个引脚设置为Output Push Pull模式初始输出高电平。后面写驱动代码的时候全部用软件翻转来控制时序完全避开硬件I2C的坑。这两个引脚的命名我建议在CubeMX的“User Labels”里改成自定义名字比如把PB6改成OLED_SCLPB7改成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 ModeHAL_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/621个英文字符。界面布局上第一行显示ADC原始值第二行显示电压第三行显示光照等级分别占据页0-1、页2-3、页4-5。这样留出最底下一行用来显示状态或者日期后续扩展时不用推翻重来。4. 硬件连接和实测效果记录4.1 杜邦线怎么接才不容易出错我按“先电源后信号、先模块后MCU”的顺序来连线接线表整理如下OLED引脚连接到光敏传感器元件连接到VCC3.3V光敏电阻一脚3.3VGNDGND光敏电阻另一脚PA0SCLPB610kΩ电阻一端PA0同一节点SDAPB710kΩ电阻另一端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模块做串口调试。把PA9USART1_TX接到模块的RXPA10USART1_RX接到模块的TX波特率设115200然后在代码里用printf输出采样值。每次按键触发一次采样然后打印比看屏幕上的数字更稳定尤其适合用来标定光照等级的阈值。串口接的时候也有个经典大坑共地。USB转TTL模块的GND必须和STM32开发板的GND相连不然串口收发会出现乱码或者完全不通。我在第一次调试时就忘了共地折腾了十几分钟才反应过来后来把共地列为了必查项。4.3 实测数据记录与分析我分别在五种环境下做了测试记录下来的数据如下环境ADC原始值计算电压(V)光照等级手完全捂住光敏电阻2050.17Dark拉上窗帘的室内10400.84Dim正常室内照明23801.92Bright台灯近距离照射34802.81Strong窗外阳光直射39503.18Strong从数据能看到两个重要的现象。第一ADC值和光照之间确实存在单调递增关系但并不是线性的。从暗到亮数值变化率不一样说明把ADC值直接线性映射成“光照强度百分比”是不科学的用等级划分反而更合理。第二即使把手完全捂住传感器ADC值也没有掉到0而是一个200左右的底噪。原因有两层一是光敏电阻的暗阻不可能是无穷大二是PA0引脚本身会残留一点环境噪声。这个底噪值可以作为软件校准的参考点比如在Dark等级判断时用“adc_val 600”而不是“adc_val 50”就是为了给底噪留出余量。5. 常见问题排查与避坑记录5.1 OLED屏幕不亮或显示乱码这个是最常见的问题我在调试中也翻过车。排查顺序建议按下面这个表来从简单到复杂现象排查方向解决方案屏幕完全不亮电源检查确认VCC是否3.3VGND是否共地屏幕亮但无字符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显示的流程吃透后续无论是接温湿度传感器、空气质量传感器还是音频采集原理都是一模一样的换汤不换药。希望这篇教程能帮你在嵌入式这条路上多踩平一个坑少走一段弯路。