刚接触嵌入式开发时,传感器联动总是最能激发学习兴趣的项目。最近我用STM32F103C8T6(俗称"蓝莓派")搭配BH1750光照传感器和0.96寸OLED屏,做了一个光照监测装置。这个项目虽然不大,但完整涵盖了硬件连接、I2C通信、传感器驱动和显示控制等嵌入式开发核心技能点。下面我就把这个项目的完整实现过程分享给大家,包括那些官方手册里不会写的实战经验。
先说说为什么选择这些器件组合。BH1750是一款数字式环境光强度传感器,采用I2C接口,测量范围1-65535 lux,精度±20%,完全满足日常光照监测需求。而0.96寸OLED屏同样是I2C接口设备,这样两个外设可以共用一组I2C总线,最大限度节省MCU的IO资源。STM32F103C8T6作为Cortex-M3内核的MCU,有足够的性能处理传感器数据并驱动显示,价格还非常亲民。
整个系统的硬件连接非常简单,核心就是I2C总线的正确配置。BH1750和OLED屏都连接到STM32的同一组I2C接口上,具体引脚分配如下:
这里有个关键细节:I2C总线必须加上拉电阻。根据I2C规范,SCL和SDA线都需要通过电阻上拉到VCC。电阻值的选择有讲究:
实际接线时,两个器件的SCL和SDA引脚是并联关系,只需要一组上拉电阻即可。有些开发板上已经集成了上拉电阻,这种情况下就不需要额外添加。
在硬件连接阶段,最容易遇到以下问题:
设备无响应:首先检查电源是否正常,然后用万用表测量SDA/SCL线电压。正常时应为高电平(约3.3V),如果电压偏低,可能是上拉电阻未接或阻值过大。
地址冲突:BH1750的默认地址是0x23,而OLED屏常见地址是0x3C或0x3D。如果两个设备地址相同,需要使用地址选择引脚(如果有)或更换器件。
波形畸变:用示波器观察I2C波形时,如果发现上升沿过缓或振铃现象,通常是上拉电阻不合适或走线过长导致的。缩短连线或调整电阻值可以改善。
提示:在面包板上搭建电路时,建议先单独测试每个器件,确认正常工作后再组合连接。这样可以快速定位问题器件。
I2C初始化的关键在于GPIO模式配置,这是很多初学者容易出错的地方。正确的初始化代码如下:
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_Mode_AF_OD配置。I2C总线要求使用开漏输出模式,这样多个设备才能实现"线与"逻辑。如果错误配置为推挽输出,会导致总线冲突甚至器件损坏。
BH1750的操作流程相对简单,主要包含以下几个步骤:
对应的驱动代码如下:
c复制// BH1750上电唤醒
void BH1750_PowerOn(void)
{
I2C_Start();
I2C_Send_Byte(0x23 << 1); // 器件地址+写操作
I2C_Wait_Ack();
I2C_Send_Byte(0x01); // POWER ON指令
I2C_Wait_Ack();
I2C_Stop();
DelayMs(180); // 等待传感器稳定
}
// 设置测量模式
void BH1750_SetMode(uint8_t mode)
{
I2C_Start();
I2C_Send_Byte(0x23 << 1);
I2C_Wait_Ack();
I2C_Send_Byte(mode); // 测量模式指令
I2C_Wait_Ack();
I2C_Stop();
if(mode == 0x10) DelayMs(180); // 高精度模式需要更长时间
}
// 读取光照数据
float BH1750_Read(void)
{
uint8_t buf[2];
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();
uint16_t val = (buf[0]<<8) | buf[1];
return val / 1.2; // 转换为lux单位
}
BH1750支持多种测量模式,常用的有:
高分辨率模式精度更高但测量时间更长。在实际应用中,需要根据响应速度和精度要求选择合适的模式。
OLED显示部分我使用了u8g2库,这是一个功能强大且兼容多种显示器的开源库。显示光照值的核心代码如下:
c复制// OLED初始化
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_SetContrast(&u8g2, 120);
}
// 刷新显示
void OLED_Refresh(float lux)
{
char str[16];
// 使用整型运算避免sprintf的浮点开销
int lux_int = (int)(lux * 10);
snprintf(str, sizeof(str), "Lux: %d.%d", lux_int/10, lux_int%10);
u8g2_ClearBuffer(&u8g2);
u8g2_SetFont(&u8g2, u8g2_font_ncenB14_tr);
u8g2_DrawStr(&u8g2, 5, 30, str);
u8g2_SendBuffer(&u8g2);
// 根据环境光调节OLED亮度
if(lux > 1000)
u8g2_SetContrast(&u8g2, 255); // 强光下提高亮度
else
u8g2_SetContrast(&u8g2, 120);
}
这里有几个优化点值得注意:
snprintf替代浮点运算,减少MCU负担将各个模块整合后的主程序逻辑如下:
c复制int main(void)
{
// 硬件初始化
HAL_Init();
SystemClock_Config();
I2C_Config();
OLED_Init();
BH1750_PowerOn();
BH1750_SetMode(0x10); // 高分辨率模式
// 主循环
while(1)
{
static uint32_t last = 0;
if(HAL_GetTick() - last > 500) // 500ms采样一次
{
float lux = BH1750_Read();
OLED_Refresh(lux);
last = HAL_GetTick();
// 强光警告
if(lux > 20000) OLED_Blink(3);
}
__WFI(); // 进入休眠省电
}
}
这个主循环实现了以下功能:
对于电池供电的应用,功耗优化非常重要。本项目中采用的优化措施包括:
使用WFI指令:__WFI()(Wait For Interrupt)指令使CPU在空闲时进入休眠状态,可显著降低功耗。
动态刷新率:根据环境光照变化速度调整采样频率。稳定环境下可降低采样率,突变时提高采样率。
屏幕亮度自适应:如前所述,根据环境光强度自动调整OLED背光。
外设电源管理:长时间不使用时,可以完全关闭传感器和显示器的电源。
实测这些优化措施使系统平均电流从8mA降到了3mA左右,对于纽扣电池供电的应用已经足够。
在实际开发过程中,我遇到了不少问题,这里总结几个典型的案例:
现象:读取的数据全为0或0xFF,逻辑分析仪显示无ACK响应。
排查步骤:
解决方案:
现象:在稳定光照环境下,读数仍有较大波动。
可能原因:
解决方案:
c复制// 滑动平均滤波实现
#define FILTER_SIZE 5
float lux_filter[FILTER_SIZE] = {0};
uint8_t filter_index = 0;
float filter_lux(float new_val)
{
lux_filter[filter_index] = new_val;
filter_index = (filter_index + 1) % FILTER_SIZE;
float sum = 0;
for(int i=0; i<FILTER_SIZE; i++) {
sum += lux_filter[i];
}
return sum / FILTER_SIZE;
}
现象:屏幕显示乱码或完全不显示。
排查步骤:
解决方案:
这个基础项目可以进一步扩展为更实用的应用:
光强报警器:设置阈值,当光照超过或低于设定值时触发蜂鸣器或LED报警。
数据记录仪:添加SD卡模块,定期记录光照数据,用于环境监测。
智能调光系统:根据环境光自动调节LED灯的亮度,实现节能照明。
无线传输:加入蓝牙或WiFi模块,将数据发送到手机或云平台。
低功耗优化:使用STM32的低功耗模式,配合RTC定时唤醒,适合电池长期供电。
实现光强报警功能的示例代码:
c复制// 光强报警功能
void check_lux_threshold(float lux)
{
static bool alert_state = false;
if(lux > LUX_THRESHOLD_HIGH) {
if(!alert_state) {
buzzer_on();
alert_state = true;
}
}
else if(lux < LUX_THRESHOLD_LOW) {
if(!alert_state) {
buzzer_on();
alert_state = true;
}
}
else {
if(alert_state) {
buzzer_off();
alert_state = false;
}
}
}
这个项目虽然简单,但涵盖了嵌入式开发的多个重要方面:外设驱动、总线通信、数据显示、功耗优化等。通过这个案例,我们可以掌握STM32开发的基本流程和方法,为更复杂的项目打下基础。