从零开始制作一个基于STM32和ESP8266-01S的智能时钟(1)BH1750光照传感器模块

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

提示:这里可以添加本文要记录的大概内容:

本项目需要基础的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口输出低电平的时候,才会为低电平。
![[\blog.csdnimg.cn/06dc0504f55e41c8ab257d1ecb4e0106.png)
I2C通信开始的条件为SCL为高电平时,SDA由高电平变为低电平,结束条件为SCL为高电平时,SDA由低电平变为高电平。即必须要在SCL高电平的时候SDA变化才会产生开始和结束条件。
Alt
那么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()应该就能够得到以下结果

在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值