1. 项目概述
这个基于STM32的光照监测系统是我刚接触嵌入式开发时做的第一个完整项目。它通过BH1750数字光照传感器采集环境光强度,再通过0.96寸OLED屏幕实时显示光照值(单位:Lux)。整个系统采用I2C总线连接,硬件结构简单但功能完整,非常适合作为STM32外设开发的入门练手项目。
我在实际开发过程中遇到了不少典型问题,比如I2C通信失败、传感器数据异常、OLED显示闪烁等。本文将详细解析硬件连接、软件实现和调试技巧,分享我从这个项目中获得的实战经验。这个项目虽然简单,但涵盖了嵌入式开发的几个核心环节:外设驱动开发、总线通信协议、数据显示和人机交互。
2. 硬件设计与连接
2.1 核心器件选型
STM32F103C8T6:作为主控制器,这款Cortex-M3内核的MCU性价比极高,72MHz主频完全能满足本项目需求。我选择它的另一个原因是其硬件I2C外设稳定可靠,且有丰富的社区资源支持。
BH1750FVI:数字光照传感器,测量范围1-65535 lux,分辨率最低1lx,直接输出数字信号,省去了传统光敏电阻需要的ADC电路。它支持I2C总线接口,工作电压2.4-3.6V,典型应用电路非常简单。
SSD1306 0.96寸OLED:I2C接口的128x64单色显示屏,对比度高、功耗低,特别适合嵌入式设备的数值显示。相比LCD屏,OLED不需要背光,在低光照环境下显示效果更好。
2.2 I2C总线连接原理
硬件连接的核心是I2C总线的正确配置。BH1750和OLED共用同一组I2C总线,接线方式如下:
code复制STM32F103 BH1750 OLED
PB6(SCL) ---- SCL ---- SCL
PB7(SDA) ---- SDA ---- SDA
3.3V ---- VCC ---- VCC
GND ---- GND ---- GND
关键细节:两个设备的I2C地址不同,BH1750默认地址0x23,OLED通常为0x3C或0x3D,因此可以挂载在同一总线上而不会冲突。
必须注意的上拉电阻问题:
- I2C总线需要上拉电阻确保信号稳定
- 典型值4.7KΩ(3.3V系统)
- 电阻太小会增加功耗,太大会导致信号上升沿过缓
- 必须接在SCL和SDA线上(靠近总线末端)
3. 软件实现详解
3.1 I2C外设初始化
硬件I2C的配置有几个关键点容易出错:
c复制void I2C_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
I2C_InitTypeDef I2C_InitStructure;
// 使能GPIO和I2C时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
// PB6(SCL)和PB7(SDA)配置为复用开漏
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 关键配置!
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// I2C参数配置
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_OwnAddress1 = 0xAA; // 主设备地址(任意不冲突值)
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_ClockSpeed = 400000; // 400kHz标准模式
I2C_Init(I2C1, &I2C_InitStructure);
I2C_Cmd(I2C1, ENABLE);
}
常见错误:GPIO模式误设为推挽输出(GPIO_Mode_Out_PP),这会导致总线冲突。必须使用开漏输出(GPIO_Mode_AF_OD)配合上拉电阻。
3.2 BH1750驱动实现
BH1750的操作流程包括:上电初始化、设置测量模式、读取数据三个主要步骤。
传感器初始化:
c复制void BH1750_Init(void)
{
// 发送上电命令
I2C_Start();
I2C_Send_Byte(0x23 << 1); // 器件地址+写方向
I2C_Wait_Ack();
I2C_Send_Byte(0x01); // POWER ON指令
I2C_Wait_Ack();
I2C_Stop();
// 设置连续高分辨率模式(0x10)
I2C_Start();
I2C_Send_Byte(0x23 << 1);
I2C_Wait_Ack();
I2C_Send_Byte(0x10); // 连续H分辨率模式
I2C_Wait_Ack();
I2C_Stop();
DelayMs(180); // 等待传感器稳定
}
数据读取函数:
c复制float BH1750_ReadLux(void)
{
uint8_t buf[2];
uint16_t raw_value;
float lux;
// 启动I2C读操作
I2C_Start();
I2C_Send_Byte((0x23 << 1) | 0x01); // 器件地址+读方向
I2C_Wait_Ack();
// 读取两个字节数据
buf[0] = I2C_Read_Byte(1); // 读第一个字节并发送ACK
buf[1] = I2C_Read_Byte(0); // 读第二个字节并发送NACK
I2C_Stop();
// 数据转换
raw_value = (buf[0] << 8) | buf[1];
lux = raw_value / 1.2f; // 根据手册转换公式
return lux;
}
数据精度说明:BH1750在H分辨率模式下,原始数据与实际照度的关系为lux = raw_value/1.2。例如读到的原始值为300,则实际照度为300/1.2=250lux。
3.3 OLED显示实现
使用u8g2库驱动OLED显示光照值:
c复制// 全局u8g2对象初始化
u8g2_t u8g2;
void OLED_Init(void)
{
u8g2_Setup_ssd1306_i2c_128x64_noname_f(&u8g2, U8G2_R0, u8x8_byte_sw_i2c, u8x8_gpio_and_delay);
u8g2_InitDisplay(&u8g2);
u8g2_SetPowerSave(&u8g2, 0);
u8g2_ClearBuffer(&u8g2);
}
// 刷新显示函数
void OLED_Refresh(float lux)
{
char str[20];
// 将浮点数转换为字符串
sprintf(str, "%.1f lux", lux);
// 清空缓冲区并绘制新内容
u8g2_ClearBuffer(&u8g2);
u8g2_SetFont(&u8g2, u8g2_font_ncenB14_tr);
u8g2_DrawStr(&u8g2, 10, 30, "Light:");
u8g2_DrawStr(&u8g2, 10, 50, str);
// 根据光照强度自动调节屏幕亮度
if(lux > 1000) {
u8g2_SetContrast(&u8g2, 255); // 强光下最高亮度
} else {
u8g2_SetContrast(&u8g2, 120); // 弱光下降低亮度
}
u8g2_SendBuffer(&u8g2);
}
4. 系统整合与优化
4.1 主程序逻辑
c复制int main(void)
{
HAL_Init();
SystemClock_Config();
I2C_Config();
BH1750_Init();
OLED_Init();
uint32_t last_update = 0;
float lux_values[5] = {0}; // 用于滑动平均滤波
uint8_t index = 0;
while(1) {
// 每500ms更新一次数据
if(HAL_GetTick() - last_update > 500) {
// 读取光照值并存入数组
lux_values[index] = BH1750_ReadLux();
index = (index + 1) % 5;
// 计算滑动平均值
float avg_lux = 0;
for(int i=0; i<5; i++) {
avg_lux += lux_values[i];
}
avg_lux /= 5;
// 更新显示
OLED_Refresh(avg_lux);
// 强光警告
if(avg_lux > 20000) {
OLED_Blink(3); // 闪烁3次
}
last_update = HAL_GetTick();
}
__WFI(); // 进入低功耗模式
}
}
4.2 关键优化技术
1. 滑动平均滤波
原始光照数据可能会有小幅波动,采用滑动平均滤波可以平滑显示:
c复制#define FILTER_SIZE 5
float lux_filter[FILTER_SIZE] = {0};
uint8_t filter_index = 0;
float filter_lux(float new_lux)
{
static float sum = 0;
sum -= lux_filter[filter_index]; // 减去最旧的值
lux_filter[filter_index] = new_lux;
sum += new_lux; // 加上最新的值
filter_index = (filter_index + 1) % FILTER_SIZE;
return sum / FILTER_SIZE;
}
2. 低功耗设计
- 使用WFI指令让CPU在空闲时休眠
- 动态调整OLED背光亮度
- 适当降低采样频率(本项目使用500ms间隔)
3. 异常处理机制
c复制#define MAX_RETRY 3
float safe_read_lux(void)
{
uint8_t retry = 0;
float lux = 0;
while(retry < MAX_RETRY) {
lux = BH1750_ReadLux();
// 检查数据合理性(0-65535 lux)
if(lux >=0 && lux <=65535) {
break;
}
retry++;
DelayMs(10);
if(retry == MAX_RETRY) {
lux = -1; // 返回错误值
}
}
return lux;
}
5. 常见问题与解决方案
5.1 I2C通信失败
现象:OLED或BH1750无响应,读取数据全为0或异常值。
排查步骤:
-
检查硬件连接
- SCL/SDA线是否接反
- 上拉电阻是否正确连接(4.7KΩ)
- 电源电压是否稳定(3.3V)
-
检查I2C地址
- 使用I2C扫描工具确认设备地址
- BH1750默认0x23,OLED通常0x3C或0x3D
-
检查GPIO配置
- 必须配置为复用开漏模式(GPIO_Mode_AF_OD)
- 时钟使能是否正确
-
用逻辑分析仪抓取I2C波形
- 观察起始条件、地址字节、ACK信号
- 检查时钟频率是否符合预期
5.2 光照数据异常
现象:读数不稳定或明显偏离实际值。
解决方案:
- 确保传感器未被遮挡
- 检查电源稳定性(纹波过大会影响精度)
- 添加软件滤波(如滑动平均)
- 避免强光直射导致传感器饱和
5.3 OLED显示问题
现象:屏幕闪烁、显示不全或乱码。
解决方法:
- 检查I2C地址配置
- 确保刷新间隔合理(不宜过快)
- 增加显示缓冲区,避免直接操作屏幕
- 检查电源滤波电容(建议增加10μF电容)
6. 项目扩展思路
这个基础项目可以进一步扩展为更实用的应用:
-
光强阈值报警:当光照超过设定值时触发蜂鸣器或LED报警
-
数据记录功能:添加SD卡模块存储历史光照数据
-
无线传输:通过蓝牙或WiFi模块将数据发送到手机APP
-
自动调节系统:根据光照强度自动调节LED灯亮度
-
多传感器融合:结合温湿度传感器创建环境监测站
c复制// 光强阈值报警示例代码
void check_lux_threshold(float lux)
{
#define LUX_THRESHOLD 10000
static uint8_t alarm_on = 0;
if(lux > LUX_THRESHOLD && !alarm_on) {
buzzer_on();
alarm_on = 1;
} else if(lux <= LUX_THRESHOLD && alarm_on) {
buzzer_off();
alarm_on = 0;
}
}
在实际开发中,我建议先确保基础功能稳定,再逐步添加扩展功能。每个新功能都应当单独测试,避免引入新的问题。