提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
提示:这里可以添加本文要记录的大概内容:
本项目需要基础的stm32单片机知识,这里我推荐
链接:https://www.bilibili.com/video/BV1th411z7sn?p=1&vd_source=e9ab6ae9ee7c74bb73c9334f2da0a743
如果不想看那么多,看到4-2 OLED显示屏就差不多。我使用的是他的OLED基本例程。
提示:以下是本篇文章正文内容,下面案例可供参考
一、BH1750光照传感器模块
BH1750FVI 是一种用于两线式串行总线接口的数字型光强度传感器集成电路。其使用I2C通信。其基本使用方法就是使用I2C通信方法与其通信,发送相关指令,然后BH1750会返回其检测到的光照度数据,然后我们将数据进行一定的处理就可以得到我们想要的光照度。
二、I2C通信
1.原理
如果想了解I2C原理,推荐https://www.bilibili.com/video/BV1th411z7sn?p=31&vd_source=e9ab6ae9ee7c74bb73c9334f2da0a743,最好把I2C的几节都看了。我这里就简单说一下我的理解。I2C通信协议一般有4根线,分别是VCC、GND、SDA、SCL。各设备的VCC、SDA、SCL、GND一一对应接到一起。SCL为时钟线,用来产生一个通信时钟。打比方说就像中文听写一样,我这1s说一个字给你,然后下1s你将这个字记录在纸上,如此往复直到完成,而这个1s的时间段就是SCL时钟线决定的,而SCL一直是主机控制的,所以主机是整个通信过程的主导。事实上做决定的并不是1s,而是高低电平的变化。SDA是数据线,即上面提到的字就是通过这根线传输的,无论是发送数据还是接收数据都是通过SDA数据线发送和接收数据的。SCL时钟线和SDA数据线需要通过上拉电阻接到某个VCC上,而BH1750模块内部应该有上拉电阻,不需要我们操心。VCC也是使用BH1750模块的内部电源,IO口模式需配置为开漏输出,即默认高电平,只有在IO口输出低电平的时候,才会为低电平。
I2C通信开始的条件为SCL为高电平时,SDA由高电平变为低电平,结束条件为SCL为高电平时,SDA由低电平变为高电平。即必须要在SCL高电平的时候SDA变化才会产生开始和结束条件。
那么SCL低电平的时候,SDA变化就是传输数据。就像前面提到的听写一样,如果想发送1bit数据,SCL低电平时,主机需要拉高或拉低SDA线代表发送的1bit数据为1还是0。然后主机迅速拉高SCL线代表从机正在接收。所以总的过程就是,主机将SCL拉低时将1bit数据放到SDA线上,等到SCL拉高时从机会自动读取SDA的数据,如此往复8个循环即发送1个byte,发送完1byte后,从机会发送1bit的应答数据返回主机,0代表从机成功接收到这1byte的数据,而1代表从机没有成功接收到这1byte的数据。主机接收过程为,主机将SCL拉低,从机将1bit数据放到SDA线上,然后主机将SCL拉高,并且读取SDA线上的1bit数据,如此往复8次就可以接收1byte数据。如果主机还想要接收数据,需要发送1bit的应答位给从机,从机才会发送下一byte数据。可以看到无论是发送还是接收SCL线都是由主机控制,即发送和接收过程一直是由主机控制。还有发送还是接收一字节后都需要接收和发送一位应答位。
I2C通信使用地址选择通信设备的方法,即开启I2C通信后,如果想要和某个特定的设备通信,需要主机发送1或2byte地址,如果该设备存在会发送应答回主机,然后主机就可以与设备一对一通信。如果不存在,主机就接收不到应答。
2.代码讲解
I2C通信可以使用软件模拟I2C,也可以使用硬件I2C(我使用过硬件I2C,一开始是可以成功读取到光照度的,但是后面加了串口的代码后,不知道为什么硬件I2C会出错,读取不了光照度,可能是因为中断的问题导致硬件I2C会出现问题),所以这里我就采用软件模拟I2C时序读取光照度。
/* BH1750初始化函数,主要把SCL和SDA连接的IO口配置为开漏输出模式*/
void BH1750_Init()
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); /*初始化GPIOB时钟,
如需修改IO口位置,请注意*/
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStruct.GPIO_Pin = SCL_Pin;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SCL_GPIO,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStruct.GPIO_Pin = SDA_Pin;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SDA_GPIO,&GPIO_InitStruct);
SDA_Set(1);
SCL_Set(1); //将SCL和SDA都初始化为高电平状态
Send_Cmd(0x01); /*因为这里我使用的测量模式为连续H分辨率模式,
不需要每次都重新发送通电指令,所以直接在初始化函数调用一次就无需再次调用*/
}
I2C通信是高位先行的通信方式,即发送和接收1byte数据,是从最高位开始发送和接收,到最低位结束。开始I2C通信后,需要发送想要与其通信的从机地址(地址是包括读写位的,一般是1byte的最后一位,0代表写,1代表读),一般来说还需要发送从机里面特定的寄存器地址,与从机里面的寄存器通信。但是BH1750只有一个寄存器,所以就没必要了。(追加:一般读和写都是指读写特点的寄存器,但是从机地址+读发送后,从机会立刻发送数据给主机,主机没有机会发送特定的寄存器地址,所以会从第一个寄存器开始,这就不是特点的寄存器了,所以有个小技巧,先发送从机地址+写,再发送特定的寄存器地址,这样指针就会指到特点的寄存器地址,然后再次发送起始条件和从机地址+读,就可以读特点的寄存器内容)
/*SCL设置高低电平函数,输入1为高电平,输入0为低电平*/
void SCL_Set(uint8_t value)
{
GPIO_WriteBit(SCL_GPIO, SCL_Pin, (BitAction)value);
Delay_us(5);
}
/*SDA设置高低电平函数,输入1为高电平,输入0为低电平*/
void SDA_Set(uint8_t value)
{
GPIO_WriteBit(SDA_GPIO, SDA_Pin, (BitAction)value);
Delay_us(5);
}
/*读取SDA当前的高低电平状态,输出为1或0*/
uint8_t SDA_Read()
{
// Delay_us(5);
return GPIO_ReadInputDataBit(SDA_GPIO, SDA_Pin);
}
/*开启I2C通信函数
。初始化时SCL和SDA已经为1了
,所以只要SDA先变为0即开启了I2C通信
,然后将SCL设置为0是为了连贯性,方便下一次修改SDA时不需要先修改SCL
,所以除了I2C结束函数,SCL最后的状态都应该设置为低电平
,前面的两句主要是为了兼容读特定寄存器时重新开始的调用*/
void My_I2C_Start()
{
SDA_Set(1);
SCL_Set(1);
SDA_Set(0);
SCL_Set(0);
}
/*结束I2C通信函数
。经过一系列操作后,不知道SDA此时的状态
,所以需要先将SDA设置为0,前面说过开始后SCL的默认状态为0
,为了产生结束条件,需要将SCL设置为1,再将SDA设置为1*/
void My_I2C_End()
{
SDA_Set(0);
SCL_Set(1);
SDA_Set(1);
}
/*主机发送应答函数
。SCL默认为0,SCL为0时放数据到SDA上
,所以将输入放到SDA上,再将SCL设置为1等待从机接收应答
,最后恢复SCL的默认状态0*/
void Send_ACK(uint8_t bit)
{
SDA_Set(bit);
SCL_Set(1);
SCL_Set(0);
}
/*主机接收应答函数
。SCL默认为0,SCL为0时放数据到SDA上
,所以从机将输入放到SDA上(不需要我们管),再将SCL设置为1表示主机接收应答
,调用读取SDA函数,读取SDA的数据,因为0表示应答成功,1表示失败
,所以这里我设置了一个变量初始值为1,再接收应答,可以根据返回值判断此次I2C通信是否成功
,最后恢复SCL的默认状态0*/
uint8_t Receive_ACK()
{
uint8_t temp = 1;
SCL_Set(1);
temp = SDA_Read();
SCL_Set(0);
return temp;
}
/*主机发送1byte数据函数(发送数据高位先行)
。SCL默认为0,SCL为0时放数据到SDA上(主机),I2C通信是高位先行
,所以将输入从高到低一位位提取出来放到SDA上,再将SCL设置为1等待从机接收应答
,最后恢复SCL的默认状态0,循环8次就可以发送1byte数据*/
void Send_Byte(uint8_t Byte)
{
uint8_t i;
for(i=0;i<8;i++)
{
SDA_Set(Byte & (0x80>>i));
SCL_Set(1);
SCL_Set(0);
}
}
/*主机发送1byte命令函数
。先开启I2C通信,再发送需要通信的设备地址,接收应答(无论是否需要使用应答,都要接收)
,(因为BH1750只有一个寄存器,所以不需要发送寄存器地址)
,然后使用Send_Byte()函数发送命令,最后接收应答,结束I2C通信
(如果I2C通信不成功,可以设置一个变量接收应答位看是哪一步出错了)*/
void Send_Cmd(uint8_t Cmd)
{
// uint8_t ack = 1;
My_I2C_Start();
Send_Byte(address);
/* ack = */
Receive_ACK();
// if(ack == 0)
// {
// SUCCESS ack;
// }
Send_Byte(Cmd);
Receive_ACK();
My_I2C_End();
}
/*主机接收1byte数据函数(接收的1byte数据也是高位先行)
。SCL默认为0,SCL为0时放数据到SDA上(从机),I2C通信是高位先行
,将SCL设置为1并且主机读取SDA的数值,
,最后恢复SCL的默认状态0,循环8次就可以接收1byte数据
(需要注意这里的byte变量必须初始化为0,不然会接收出错)*/
uint8_t Receive_Byte()
{
uint8_t i;
uint8_t byte = 0;
SDA_Set(1);
for(i=0; i<8; i++)
{
SCL_Set(1);
byte <<= 1;
byte |= SDA_Read();
SCL_Set(0);
}
return byte;
}
/* BH1750初始化函数,主要把SCL和SDA连接的IO口配置为开漏输出模式*/
void BH1750_Init()
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); /*初始化GPIOB时钟,如需修改IO口位置,请注意*/
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStruct.GPIO_Pin = SCL_Pin;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SCL_GPIO,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStruct.GPIO_Pin = SDA_Pin;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SDA_GPIO,&GPIO_InitStruct);
SDA_Set(1);
SCL_Set(1); //将SCL和SDA都初始化为高电平状态
Send_Cmd(0x01); /*因为这里我使用的测量模式为连续H分辨率模式,不需要每次都重新发送通电指令,所以直接在初始化函数调用一次就无需再次调用*/
}
想要读取BH1750的光照度需要发送相应的指令,这篇博客非常详细建议看一下他的步骤部分。https://blog.csdn.net/ShenZhen_zixian/article/details/103542972。第1步:发送上电命令(0x01,我在初始化函数已经发送了)。第2步:发送测量命令(0x10)。第3步:等待测量结束(延时200ms)。第4步:读取数据。(数据为16位,先发送高八位,后发送低八位,表示范围为0~65535)第5步:计算结果。计算公式是:光照强度 =(寄存器值[15:0] * 分辨率) / 1.2 (单位:勒克斯lx)。
uint16_t Light_INT; //光照度的整数部分
uint8_t Light_FLOAT; //光照度的小数部分
/*获得光照度,并且将光照度显示到oled上函数*/
void BH1750_Get_Light()
{
uint16_t data = 0; //定义一个16位数据变量接收返回的16位数据
Send_Cmd(0x10); //发送测量指令
Delay_ms(200); //等待
/*使用I2C读数据,将数据放到变量data里*/
My_I2C_Start();
Send_Byte(address|0x01); //地址+读(1)代表I2C读数据
Receive_ACK();
data |= Receive_Byte();
data <<= 8;
Send_ACK(0);
data |= Receive_Byte();
Send_ACK(1);
My_I2C_End();
/*计算光照度的整数部分和小数部分*/
Light_INT = data*1/1.2; //整数部分使用无符号16位接收,因为整数部分最大可为65535/1.2
Light_FLOAT = (uint32_t)(data*10/1.2)%10; /*小数部分使用无符号8位接收
,乘上10再对10取余即可得小数后1位
,加上(uint32_t)是因为取余是要对整数进行取余
,而(data*10/1.2)最大值为65535*10/1.2,
所以要用32位进行强制类型转换*/
/*最后显示到OLED上*/
OLED_ShowString(4,1,"Light:");
OLED_ShowNum(4,7,Light_INT,5);
OLED_ShowString(4,12,".");
OLED_ShowNum(4,13,Light_FLOAT,1);
OLED_ShowString(4,14,"LX");
}
2.全部代码
BH1750.h:
#ifndef __BH1750_H
#define __BH1750_H
void BH1750_Init(void);
void BH1750_Get_Light(void);
#endif
BH1750.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "oled.h"
#define address 0x46 //BH1750的地址
/*如果想改变IO口除了下面的四个数值要改,还需要改变一下BH1750_Init()函数的时钟函数*/
#define SCL_GPIO GPIOB //SCL对应的GPIO
#define SCL_Pin GPIO_Pin_6 //SCL对应的Pin
#define SDA_GPIO GPIOB //SDA对应的GPIO
#define SDA_Pin GPIO_Pin_7 //SDA对应的Pin
uint16_t Light_INT; //光照度的整数部分
uint8_t Light_FLOAT; //光照度的小数部分
/*SCL设置高低电平函数,输入1为高电平,输入0为低电平*/
void SCL_Set(uint8_t value)
{
GPIO_WriteBit(SCL_GPIO, SCL_Pin, (BitAction)value);
Delay_us(5);
}
/*SDA设置高低电平函数,输入1为高电平,输入0为低电平*/
void SDA_Set(uint8_t value)
{
GPIO_WriteBit(SDA_GPIO, SDA_Pin, (BitAction)value);
Delay_us(5);
}
/*读取SDA当前的高低电平状态,输出为1或0*/
uint8_t SDA_Read()
{
// Delay_us(5);
return GPIO_ReadInputDataBit(SDA_GPIO, SDA_Pin);
}
/*开启I2C通信函数
。初始化时SCL和SDA已经为1了
,所以只要SDA先变为0即开启了I2C通信
,然后将SCL设置为0是为了连贯性,方便下一次修改SDA时不需要先修改SCL
,所以除了I2C结束函数,SCL最后的状态都应该设置为低电平
,前面的两句主要是为了兼容读特定寄存器时重新开始的调用*/
void My_I2C_Start()
{
SDA_Set(1);
SCL_Set(1);
SDA_Set(0);
SCL_Set(0);
}
/*结束I2C通信函数
。经过一系列操作后,不知道SDA此时的状态
,所以需要先将SDA设置为0,前面说过开始后SCL的默认状态为0
,为了产生结束条件,需要将SCL设置为1,再将SDA设置为1*/
void My_I2C_End()
{
SCL_Set(0);
SDA_Set(0);
SCL_Set(1);
SDA_Set(1);
}
/*主机发送应答函数
。SCL默认为0,SCL为0时放数据到SDA上
,所以将输入放到SDA上,再将SCL设置为1等待从机接收应答
,最后恢复SCL的默认状态0*/
void Send_ACK(uint8_t bit)
{
SDA_Set(bit);
SCL_Set(1);
SCL_Set(0);
}
/*主机接收应答函数
。SCL默认为0,SCL为0时放数据到SDA上
,所以从机将输入放到SDA上(不需要我们管),再将SCL设置为1表示主机接收应答
,调用读取SDA函数,读取SDA的数据,因为0表示应答成功,1表示失败
,所以这里我设置了一个变量初始值为1,再接收应答,可以根据返回值判断此次I2C通信是否成功
,最后恢复SCL的默认状态0*/
uint8_t Receive_ACK()
{
uint8_t temp = 1;
SCL_Set(1);
temp = SDA_Read();
SCL_Set(0);
return temp;
}
/*主机发送1byte数据函数(发送数据高位先行)
。SCL默认为0,SCL为0时放数据到SDA上(主机),I2C通信是高位先行
,所以将输入从高到低一位位提取出来放到SDA上,再将SCL设置为1等待从机接收应答
,最后恢复SCL的默认状态0,循环8次就可以发送1byte数据*/
void Send_Byte(uint8_t Byte)
{
uint8_t i;
for(i=0;i<8;i++)
{
SDA_Set(Byte & (0x80>>i));
SCL_Set(1);
SCL_Set(0);
}
}
/*主机发送1byte命令函数
。先开启I2C通信,再发送需要通信的设备地址,接收应答(无论是否需要使用应答,都要接收)
,(因为BH1750只有一个寄存器,所以不需要发送寄存器地址)
,然后使用Send_Byte()函数发送命令,最后接收应答,结束I2C通信
(如果I2C通信不成功,可以设置一个变量接收应答位看是哪一步出错了)*/
void Send_Cmd(uint8_t Cmd)
{
// uint8_t ack = 1;
My_I2C_Start();
Send_Byte(address);
/* ack = */
Receive_ACK();
// if(ack == 0)
// {
// SUCCESS ack;
// }
Send_Byte(Cmd);
Receive_ACK();
My_I2C_End();
}
/*主机接收1byte数据函数(接收的1byte数据也是高位先行)
。SCL默认为0,SCL为0时放数据到SDA上(从机),I2C通信是高位先行
,将SCL设置为1并且主机读取SDA的数值,
,最后恢复SCL的默认状态0,循环8次就可以接收1byte数据
(需要注意这里的byte变量必须初始化为0,不然会接收出错)*/
uint8_t Receive_Byte()
{
uint8_t i;
uint8_t byte = 0;
SDA_Set(1);
for(i=0; i<8; i++)
{
SCL_Set(1);
byte <<= 1;
byte |= SDA_Read();
SCL_Set(0);
}
return byte;
}
/* BH1750初始化函数,主要把SCL和SDA连接的IO口配置为开漏输出模式*/
void BH1750_Init()
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); /*初始化GPIOB时钟,如需修改IO口位置,请注意*/
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStruct.GPIO_Pin = SCL_Pin;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SCL_GPIO,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStruct.GPIO_Pin = SDA_Pin;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SDA_GPIO,&GPIO_InitStruct);
SDA_Set(1);
SCL_Set(1); //将SCL和SDA都初始化为高电平状态
Send_Cmd(0x01); /*因为这里我使用的测量模式为连续H分辨率模式,
不需要每次都重新发送通电指令,所以直接在初始化函数调用一次就无需再次调用*/
}
/*获得光照度,并且将光照度显示到oled上函数*/
void BH1750_Get_Light()
{
uint16_t data = 0; //定义一个16位数据变量接收返回的16位数据
Send_Cmd(0x10); //发送测量指令
Delay_ms(200); //等待
/*使用I2C读数据,将数据放到变量data里,这里我就直接调用吧,就不封装成函数了*/
My_I2C_Start();
Send_Byte(address|0x01); //地址+读(1)代表I2C读数据
Receive_ACK();
data |= Receive_Byte();
data <<= 8;
Send_ACK(0);
data |= Receive_Byte();
Send_ACK(1);
My_I2C_End();
/*计算光照度的整数部分和小数部分*/
Light_INT = data*1/1.2; //整数部分使用无符号16位接收,因为整数部分最大可为65535/1.2
Light_FLOAT = (uint32_t)(data*10/1.2)%10; /*小数部分使用无符号8位接收
,乘上10再对10取余即可得小数后1位
,加上(uint32_t)是因为取余是要对整数进行取余
,而(data*10/1.2)最大值为65535*10/1.2
,所以要用32位进行强制类型转换*/
/*最后显示到OLED上*/
OLED_ShowString(4,1,"Light:");
OLED_ShowNum(4,7,Light_INT,5);
OLED_ShowString(4,12,".");
OLED_ShowNum(4,13,Light_FLOAT,1);
OLED_ShowString(4,14,"LX");
}
总结
确保接线正确,然后在main.c中调用BH1750_Init()和BH1750_Get_Light()应该就能够得到以下结果