GY-30(BH1750)
GY-30是一款采用了ROHM-BH1750FVI芯片的数字输出的感光模块。简而言之,BH1750是一个芯片,而GY-30则是基于这个芯片的一个感光模块。
具体来说,GY-30可以测量的光照度范围为0~65535勒克斯,具有±20%的最小误差变动和1勒克斯的分辨度。它是一个五针脚模块,包括VCC、SCL、SDA、ADDR、GND五个引脚,其中VCC接电源正极,电压范围在3~5V之间;GND接地;SCL为时钟线;SDA为数据线;ADDR为寄存器的地址引脚。
长的下面这个模样。
它使用的是I2C通信协议,关于如何发出I2C可以参考我之前的文章。
我们就只需要关注给GY-30发送什么数据才能得到光照强度就行了。
我们首先要做的就是查看芯片相关的文档。
上面是BH1750芯片手册中的指令列表。
看不懂没关系,我也看不懂,但是我们可以简单地翻译一下。
翻译得不咋地,但是我们依稀可以知道我们需要的命令有哪些。
首先第二个0x01打开电源,这个毫无疑问,我们一开始使用GY-30的时候肯定是要传输这个命令的。
接下来一堆都是设置模式的,我们也可以猜的出来这些就是指定我们采样光照强度的模式。
这一堆模式里分为两种,一种是连续模式,另一种是一次模式。
这个我们也能猜的出来,如果是连续模式,那么只需要发送一次命令即可,GY-30会不断地连续采集数据,而一次模式也就是我们每次要取数据之前都需要发送一次采集命令。所以我们等等选择的是连续模式。
一次和连续模式中又分为了三种,低(L)分辨,高(H)分辨和高分辨2,区别就在于分辨率分别是4lx,1lx,0.5lx以及采样的时间,我们这边就是折中一下,等等选择高分辨1模式。
那么接下来我们可以参考一下手册里提供的时序图了。
从上图可以得知,BH1750的从机地址为0100011,如果是要写命令的话,那么地址是0x46,如果是要读数据的话,那么地址是0x47。
我们首先需要先给GY-30发送上电命令,所以是先用I2C发送0x46,再发送0x01。
下面关于I2C的代码(Z_I2C开头的函数)参考我之前的文章,本文开头有链接。
Z_I2C_Start();
Z_I2C_SendByte(0x46);
if(Z_I2C_ReveiceACK()!=0) return 0;
Z_I2C_SendByte(0x01);
if(Z_I2C_ReveiceACK()!=0) return 0;
Z_I2C_End();
接下来我们发送命令,指定BH1750为高分辨连续采集模式(指令为0x10),并且根据手册中的说法,发送完这个命令之后,我们需要等待最多180ms,我们这边凑个整,延时个200ms。
Z_I2C_Start();
Z_I2C_SendByte(0x46);
if(Z_I2C_ReveiceACK()!=0) return 0;
Z_I2C_SendByte(0x10);
if(Z_I2C_ReveiceACK()!=0) return 0;
Z_I2C_End();
Delay_ms(200);
上述代码完成之后,我们就可以开始读取数据了。
uint16_t Light=0;
Z_I2C_Start();
Z_I2C_SendByte(0x47);
if(Z_I2C_ReveiceACK()!=0) return 0;
Light|=Z_I2C_ReveiceByte();
Light<<=8;
Z_I2C_SendACK(0);
Light|=Z_I2C_ReveiceByte();
Z_I2C_SendACK(1);
Z_I2C_End();
这样,Light这个变量存放的就是GY-30发送来的数据了,根据手册里的例子,这个值还需要/1.2才是光照强度数据(lx)
完整的代码如下。
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#define SCL_Pin GPIO_Pin_0
#define SDA_Pin GPIO_Pin_1
//以下部分为之前文章中软件I2C的代码,可以直接用
void Z_I2C_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef itd;
itd.GPIO_Mode=GPIO_Mode_Out_OD;
itd.GPIO_Pin=SCL_Pin|SDA_Pin;
itd.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&itd);
GPIO_WriteBit(GPIOA,SCL_Pin,Bit_SET); //SCL和SDA默认都是高电平
GPIO_WriteBit(GPIOA,SDA_Pin,Bit_SET); //因此初始化后设为高电平
}
void Z_I2C_SetSCL(uint8_t signal){
if(signal==1) GPIO_WriteBit(GPIOA,SCL_Pin,Bit_SET);
else GPIO_WriteBit(GPIOA,SCL_Pin,Bit_RESET);
Delay_us(5); //防止电平翻转过快,因此加上延时
}
void Z_I2C_SetSDA(uint8_t signal){
if(signal==1) GPIO_WriteBit(GPIOA,SDA_Pin,Bit_SET);
else GPIO_WriteBit(GPIOA,SDA_Pin,Bit_RESET);
Delay_us(5);
}
uint8_t Z_I2C_GetSDA(void){
return GPIO_ReadInputDataBit(GPIOA,SDA_Pin);
}
void Z_I2C_Start(void){
Z_I2C_SetSDA(1);
Z_I2C_SetSCL(1);
Z_I2C_SetSDA(0);
Z_I2C_SetSCL(0);
}
void Z_I2C_End(){
Z_I2C_SetSDA(0);
Z_I2C_SetSCL(1);
Z_I2C_SetSDA(1);
}
void Z_I2C_SendByte(uint8_t byte){
Z_I2C_SetSCL(0);
for(int i=0;i<8;++i){
if((byte&0x80)==0) Z_I2C_SetSDA(0);
else Z_I2C_SetSDA(1);
byte<<=1;
Z_I2C_SetSCL(1);
Z_I2C_SetSCL(0);
}
}
uint8_t Z_I2C_ReveiceByte(){
uint8_t data=0x00;
Z_I2C_SetSDA(1);
for(int i=0;i<8;++i){
Z_I2C_SetSCL(1);
if(Z_I2C_GetSDA()==1) data|=(0x80>>i);
Z_I2C_SetSCL(0);
}
return data;
}
void Z_I2C_SendACK(uint8_t ack){
if(ack==0) Z_I2C_SetSDA(0);
else Z_I2C_SetSDA(1);
Z_I2C_SetSCL(1);
Z_I2C_SetSCL(0);
}
uint8_t Z_I2C_ReveiceACK(){
Z_I2C_SetSDA(1);
Z_I2C_SetSCL(1);
uint8_t ack=Z_I2C_GetSDA();
Z_I2C_SetSCL(0);
return ack;
}
uint16_t Z_GY30_GetData(void){
Z_I2C_Start();
Z_I2C_SendByte(0x46);
if(Z_I2C_ReveiceACK()!=0) return 0;
Z_I2C_SendByte(0x01);
if(Z_I2C_ReveiceACK()!=0) return 0;
Z_I2C_End();
Z_I2C_Start();
Z_I2C_SendByte(0x46);
if(Z_I2C_ReveiceACK()!=0) return 0;
Z_I2C_SendByte(0x10);
if(Z_I2C_ReveiceACK()!=0) return 0;
Z_I2C_End();
Delay_ms(200);
uint16_t Light=0;
Z_I2C_Start();
Z_I2C_SendByte(0x47);
if(Z_I2C_ReveiceACK()!=0) return 0;
Light|=Z_I2C_ReveiceByte();
Light<<=8;
Z_I2C_SendACK(0);
Light|=Z_I2C_ReveiceByte();
Z_I2C_SendACK(1);
Z_I2C_End();
return Light;
}
int main(void){
OLED_Init();
Z_I2C_Init();
while(1){
OLED_ShowNum(1,1,Z_GY30_GetData(),6);
Delay_ms(50);
}
}
按理说我们发送一次指定高分辨连续采集模式就可以了,之后直接等待180ms之后读取数据就行,但是我试了一下,采集的数据极其不稳定,因为最终还是上面的代码,每次读取GY-30的数据的时候都发一次指令。
效果如下。