BH1750 传感器实战教学 —— 驱动移植篇

news/2024/11/27 22:43:18/

前言

上一篇 BH1750 的实战教学我们说明的实际应用中传感器的硬件设计 :

BH1750 传感器实战教学 —— 硬件设计篇

我们提到过在本次使用的芯片为 51 内核,I2C 通讯驱动实现与 STM32 上还是有很大区别的。

对于我们来说,已经掌握了 STM32 上 BH1750 驱动,如何能够快速准确的把程序移植过来? 就是本文的主要内容。

说明,我们讨论的驱动为 软件 I2C 驱动,软件 I2C 驱动的好处之一就是可以方便的移植。

我是 矜辰所致,全网同名,尽量用心写好每一系列文章,不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开!
插一句,技术群在 CSDN 文章结尾后面的的推广,讨论学习。

目录

  • 前言
  • 一、I2C 通用驱动
    • 1.1 I2C 通讯的 IO 的宏定义
    • 1.2 关于 us 延时
      • __ASM volatile ("nop")
    • 1.3 _nop_() 函数
    • 1.4 i2c.c
  • 二、 BH1750 驱动移植
    • 2.1 bh1750.h
    • 2.2 bh1750.c
  • 三、 测试
    • 3.1 问题一 (数据完全不对)
    • 3.2 问题二 (光强时数据异常)
      • 数据问题解决
    • 3.3 再次处理电源控制的问题
  • 结语

一、I2C 通用驱动

我们使用一个芯片方案,很多时候厂家都会提供 SDK ,对于以前简单的 51 内核的芯片,有些厂家也会提供,有些就不一定提供。

本次实战所选择的方案,其实是有软件 I2C 传感器示例,当然并不是 BH1750 。但是如果你选用的芯片没有示例,也不要着急。

软件 I2C 的核心是什么? 就在于对 I2C 通讯时序的了解!

所以即便没有现成的示例,只要了解 I2C 通讯的时序,我们要做的只是需要写几个宏定义。

比如说,举一个通用驱动中,I2C 开始的例子:

在这里插入图片描述

在这个简单的 I2C 其实函数中,有几个信息需要说明:

1、sda_high scl_high 这几个的宏定义;

我们需要针对不同的芯片实现不同的宏定义 。

2、us 延时处理;

我们往往也需要自己实现 us 延时函数 。

1.1 I2C 通讯的 IO 的宏定义

对于软件 I2C ,定义好我们的: 时钟线高,时钟线低,数据线高,数据线低,读取数据线 ,是必要的步骤。

这个针对不同的芯片方式都不一样,但是实际上都是简单的对 IO 口的操作而已。

这里值得说明的是:软件 I2C 的 IO 口的设置,如果可以设置为开漏输出就设置为开漏输出。外接上拉电阻,这样直接读取 IO 口的电平也是可以的。

对于有些单片机无法设置,在写 I2C 驱动的时候需要在发数据的时候设置为输出,在读取数据的时候设置为输入(以前很多的 SDK 包上经常看到一下子设置为输出,一下子设置为输入,感觉很是“繁琐” )。

针对本次的应用,我们在通用的 i2c.h 中,有如下定义,这里直接把 i2c.h 源码放上说明:

#include <EO3000I_API.h>
#include <intrins.h>// ------------------------------------------------------------------
// user define area - define SDA and SCL Hardware Pins
// supported values:
//                   SCSEDIO0
//                   SCLKDIO1
//                   WSDADIO2
//                   RSDADIO3     #define sda_pin   SCSEDIO0  
#define scl_pin   SCLKDIO1                       #define SCSEDIO0 0x01  
#define SCLKDIO1 0x02    
#define WSDADIO2 0x04
#define RSDADIO3 0x08// ------------------------------------------------------------------
// direct io registers
sfr gpio0     = 0xC8;                     
sfr gpio0_dir = 0xA1;                     #define scl_port    gpio0 
#define scl_dir     gpio0_dir#define sda_port    gpio0 
#define sda_dir     gpio0_dir// ----------------------------
// line powered direct levels
// always write whole register (whole byte)! DO NOT address by bits#define sda_high()  sda_port |= sda_pin; sda_dir &= ~sda_pin     // set signals to HIGH first before selecting IN -> slew rates
#define sda_low()   sda_dir |= sda_pin; sda_port &= ~sda_pin 
#define sda_read()  (sda_port & sda_pin)? 1 :0                                     //ack on bus is low -> u8AckBit = 1#define scl_high()  scl_port |= scl_pin; scl_dir &= ~scl_pin     // set signals to HIGH first before selecting IN -> slew rates
#define scl_low()   scl_dir |= scl_pin; scl_port &= ~scl_pin // ------------------------
#define DONOTHING()          {;}// ------------------------
// command's
#define I2C_WRITE             0 
#define I2C_READ              1
#define I2C_ACK               0
#define I2C_NACK              1void i2c_init(void);
void i2c_start(void);
void i2c_stop(void);
uint8 i2c_write(uint8 u8Data);
uint8 i2c_read(uint8 u8Ack);

对于不同种类的芯片,可能定义不一样,但是只要注意务必实现这几个宏定义:

在这里插入图片描述

1.2 关于 us 延时

I2C 通讯用到的延时函数都欧式 us 级别的,这个延时很多时候需要自己处理,我在使用 STM32L051 的时候,因为 HAL 库并没有 us 延时,我当时使用的延时函数如下:

void delay_us(uint32_t Delay)
{uint32_t cnt = Delay * 8;   // 32Mhz ,其他频率其他倍数uint32_t i = 0;for(i = 0; i < cnt; i++)__NOP();
}

上面函数中使用了 __NOP() 函数,我们看看这个函数在哪里有说明:

在这里插入图片描述

__ASM volatile (“nop”)

这里有一条指令: __ASM volatile ("nop"); 此语句属于 内嵌汇编 。

在 Linux 内核中常常看到 C 语言中嵌入汇编指令的地方。这是因为在 GCC 中支持在 C 代码中嵌入汇编指令,因此这些汇编代码被称为 GCC Inline ASM也即是 GCC 内联汇编。
.
使用内联汇编主要目的是为了提高效率,同时还是为了实现 C 语言无法实现的部分。

其中 “asm” 是内联汇编语句关键词。

#elif defined ( __GNUC__ )#define __ASM            __asm                                      /*!< asm keyword for GNU Compiler */#define __INLINE         inline                                     /*!< inline keyword for GNU Compiler */#define __STATIC_INLINE  static inline

__asm 用来声明一个内联汇编表达式,任何一个内联汇编表达式都是以它开头的,是必不可少的。

volatile 这里向GCC 声明不允许对该内联汇编优化,否则当 使用了优化选项 (-O) 进行编译时,GCC 将会根据自己的判断决定是否将这个内联汇编表达式中的指令优化掉。

这里我们稍微扯远了一点,对于很多新手来说这里估计不明白,没有关系。上面的语句简单来说,就是使得我们在 C 语言编程的时候可以使用 __NOP() 函数。

1.3 nop() 函数

何为 _nop_() 函数?

_nop_()是C语言库函数,代表运行一个机器周期。

在 KeilC 帮助文件中可以查到此函数:

#include <intrins.h>
void nop (void);
Description:
The nop routine inserts an 8051 NOP instruction into the program. This routine can be used to pause for 1 CPU cycle. This routine is implemented as an intrinsic function. The code required is included in-line rather than being called.

那么一个机器周期是多长时间呢? 这个是与我们使用芯片的主频有关系的:比如单片机的晶振是12M的,那么这调代码会运行1us;

比如上面 在 STM32 上用到的 __NOP() 函数 ,也是这个机器周期。

在没有 us 延时函数的时候,我们可以使用机器周期自己写一个,但是需要注意,这个不是精准延时,只是大概估计的,所以需要精准延时的情况下不适合。

对于 I2C 协议的通讯,并没有规定严格的间隔时间,在几 us 的范围内,多一点少一点都是没有问题的。

但是需要告诉大家的是,根据我的经验,很多时候逻辑正确但是数据不正确,往往是因为时间间隔的问题,比如说发送了读数据的报文,延时时间太短,导致读取的时候数据异常。

1.4 i2c.c

最后,我们可以来看看我们的 i2c.c 程序了,在芯片的提供的示例中,i2c.c 中的函数如下:

在这里插入图片描述

但是这个实际上上面的驱动是很有可能出问题的,因为虽然时序正确,但是执行的时候时间太短了。

操作之间一定得加上一定时间的延时。

比如以前在读取温湿度传感器 sht21 的时候使用的函数如下:

void i2c_start(void)  {                                     sda_high(); _nop_(); _nop_();_nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_();scl_high();	_nop_(); _nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_(); _nop_(); _nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_();	sda_low();_nop_(); _nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_();	scl_low();                                                   _nop_(); _nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_(); _nop_();_nop_();	
}

这里的延时时间是根据自己以前使用的经验而定的,前面也说了 I2C 通讯中延时多少个机器周期,并没有准确的要求,多几个 nop 都是无所谓的。

但是我们可以可以把这么多 _nop_ 写成一个函数,类似在 STM32 下一样,因为是不准确的,所以这里不用 us 表示:

void delay_nop(uint32 delay){uint32_t i = 0;for(i = 0; i < delay; i++)_nop_();
}

最终我们的 i2c.h 如下:

#include "i2c.h"void delay_nop(uint32 delay){uint32 i;for(i = 0; i < delay; i++)_nop_();
}void i2c_start(void)  {                                     sda_high(); delay_nop(28);scl_high();	delay_nop(28);sda_low();delay_nop(28);scl_low();                                                   delay_nop(28);
}
// ------------------------------------------------------------------
// send stop sequence (P)
void i2c_stop(void)  {                             sda_low();                                                                          delay_nop(28);scl_low();                                     delay_nop(28);                                   scl_high();                                    delay_nop(28);                                             sda_high();delay_nop(28);
}
// ------------------------------------------------------------------
// returns the ACK or NACK
uint8 i2c_write(uint8 u8Data)  
{uint8 u8Bit;uint8 u8AckBit;// write 8 data bitsu8Bit = 0x80;                                //msb first  while(u8Bit) {if(u8Data&u8Bit){ sda_high();delay_nop(28); }else{ sda_low();delay_nop(28); }			scl_high();delay_nop(80); 						u8Bit >>= 1; //next bitscl_low();delay_nop(90); 		}// read acknowledge (9th bit) sda_high();                                               delay_nop(45);	scl_high();delay_nop(45);	u8AckBit = sda_read();	//#define sda_read()  (sda_port & sda_pin)? 1 :0  ack on bus is low -> u8AckBit = 1   sda_port gpio0   sda_pin SCSEDIO0delay_nop(45);	scl_low();                                              delay_nop(45);return u8AckBit;
}
// ------------------------------------------------------------------
// pass the ack/nack 
// returns the read data 
uint8 i2c_read(uint8 u8Ack)  
{uint8 u8Bit;uint8 u8Data;u8Bit = 0x80;       // msb firstu8Data = 0;// 8 data bitswhile(u8Bit) {scl_high();delay_nop(70);		u8Bit >>= 1;    //next bitu8Data <<= 1;u8Data |= sda_read();         //(sda_port & sda_pin)? 1 :0      sda_port gpio0   sda_pin SCSEDIO0delay_nop(30);scl_low();delay_nop(80);		}// 9th bit acknowledgeif(u8Ack==I2C_ACK){sda_low();delay_nop(30);}     //I2C_ACK=0else                {sda_high();delay_nop(30);}scl_high();delay_nop(30);scl_low();delay_nop(30);sda_high();delay_nop(30);return u8Data;
}

二、 BH1750 驱动移植

通用驱动讲完了,我们 BH1750 驱动逻辑可以参考曾经分析的流程,具体的逻辑分析可参考下文:

BH1750 光照传感器文档详解 及 驱动设计

2.1 bh1750.h

我们的 bh1750.h 完全可以和上文中的一样(但是注意一下头文件包含以及数据类型),如下图 :

在这里插入图片描述

2.2 bh1750.c

我们的主要任务在于 bh1750.c 如何实现,我们按照定好的逻辑来:

在这里插入图片描述

这里有一个问题需要注意,因为我们本次是需要低功耗设计,所以我们要考虑到模块通电以后默认状态是怎么状态? 是单次测量还是连续模式?这关系到我们是否每次上电都需要初始化。

带着这个问题我重新看了一遍资料的流程图:

在这里插入图片描述

所以其实 BH1750 并不需要我们曾经文章中提到的 void bh1750_init() 初始化函数,当然有也没有问题 ,只不过当成了进行一次单次测量。

那么我们本次初始化函数也不用写了,直接写测量读取函数就行了。

其实 I2C开始,结束这个倒直接换就行了,我们主要是要注意一下接收不接收 ACK 的处理。 当然,因为我在本次芯片上使用的函数是上面的 i2c.c 提供的,需要注意,如果大家愿意,可以自己修改一下驱动,改成和我们在 stm32 上面一样的,这样子把 通用驱动 修改,传感器驱动基本就一致了,这个看个人。

在我们以前的驱动中,发送一条消息等待 ACK 的语句如下:

IIC_Send_Byte(BH1750_ADDRESS << 1); //地址,和读写指令
MYIIC_Wait_Ack();

而在我们这个驱动中,我们需要这样做:

u8Ack = i2c_write(BH1750_ADDRESS << 1);

直接上一下修改的驱动程序把,其中与以前的驱动对比的注释我留着没删除,以做比较:

#include "bh1750.h"void bh1750_read(uint16 *lux)
{uint8 read_buffer[2];uint32 lv_lux;uint8 u8Ack;SensorPowerOn();time_wait(200);i2c_start();// IIC_Send_Byte(BH1750_ADDRESS << 1); //????????// MYIIC_Wait_Ack();u8Ack = i2c_write(BH1750_ADDRESS << 1);// delay_us(150); delay_nop(500);// IIC_Send_Byte(BH1750_MODE_ONE_H_RES);  //????// MYIIC_Wait_Ack();u8Ack = i2c_write(BH1750_MODE_ONE_H_RES);i2c_stop();// HAL_Delay(BH1750_MEASURE_DURATION_MS);time_wait(BH1750_MEASURE_DURATION_MS);i2c_start();// IIC_Send_Byte((BH1750_ADDRESS << 1)|1); //????????// MYIIC_Wait_Ack();u8Ack = i2c_write((BH1750_ADDRESS << 1)|1);// read_buffer[0] = IIC_Read_Byte(1);// delay_us(120);// read_buffer[1] = IIC_Read_Byte(0);// delay_us(120);read_buffer[0] = i2c_read(I2C_ACK);delay_nop(450);read_buffer[1] = i2c_read(I2C_NACK);delay_nop(450);i2c_stop();SensorPowerOff();lv_lux = ((read_buffer[0] << 8) | read_buffer[1]) * 10 / 12;*lux = (uint16)lv_lux;
}

三、 测试

开始测试……

在需要读取光照的地方使用 bh1750_read(&lux_data); 读取即可。

3.1 问题一 (数据完全不对)

数据是有了,但是数据有点不正常

我们前面都是按照顺序一步一步走过来的,使用电筒照着数据不正确…… 先让我理一理……

这就对了,我早就知道会有问题,要不然也没必要写一篇移植的文章!

……测试中…… 测试中……

其实出了问题也比较麻烦,因为相对 STM32 来说,使用的这个 51 调试起来很麻烦。

还记得我们当时硬件设计的时候使用了电源开关电路(本次测试飞线使用的下图中第一个电路):

在这里插入图片描述

我们在上面的程序中使用了 200 ms 的延时:

在这里插入图片描述

当我们在做低功耗的传感器遇到问题了,为了解决问题我们要先排除电源的问题,所以这里我们先让传感器一直供电。

在这里插入图片描述

当然我以前也说过,I2C 通讯中很有可能出问题的地方是通讯的等待延时,传感器驱动 bh1750.c 中的延时我也修改了,我把驱动中需要的延时 等待改成了 1 ms,如下(这是前后测试了很多的得出的结论):

在这里插入图片描述

修改完成以后,我们测试了一下数据,看上去好像正常了:

在这里插入图片描述

3.2 问题二 (光强时数据异常)

我们测试光照往往是让他测一测正常环境,然后用手电筒照着看看数据是否变大。

经过一系列的折腾,最后测试我发现,在光照强度比较低的时候数据基本是正常的,但是光照强度太高的时候数据就异常了,如下图所示。

正常情况:

在这里插入图片描述

在这里插入图片描述

灯光照射异常情况:

在这里插入图片描述

这不由得让我想起难道是数据读取的时候,高字节的数据读取异常一直为0 ,只能读到 低 8位的数据?

我们来计算机看一下:

在这里插入图片描述

那么这样的话,会不会是读取这个地方有问题 ? 还是说数据处理的时候有问题?

数据问题解决

最后测试来测试去,发现是其中有一条语句有问题:

lv_lux = ((read_buffer[0] << 8) | read_buffer[1]) * 10 / 12;

在程序中 read_bufferuint8 类型,我们这样直接位移然后与一下是否会有问题?

我把程序改成:

lv_lux = ((read_buffer[0] << 8) + read_buffer[1]) * 10 / 12;

发现数据就正常了!

为了验证一下是否是数据类型的问题,我把语句改成:

lv_lux = ((uint16)(read_buffer[0] << 8) | read_buffer[1]) * 10 / 12;

能够正常的读取到光强时候的数据:

在这里插入图片描述

到头来,其实数据异常并不是驱动有问题,而是我们数据处理的细节问题!

3.3 再次处理电源控制的问题

我们把数据读取的问题解决以后,我们还得回到我们的应用上来,电源还是需要不用的时候关短,读取传感器数据时候打开。

那么我们这个问题一般如何解决,大部分情况下,都是加大打开电源后的延时时间!

这个延时时间越大,传感器采集的时候功耗就越大,因为这时候并不是休眠状态,但是太小我们前面测试的 200 ms,发现传感器数据会不正常,可能是电源没有稳定下来,也可能是传感器也需要准备时间,所以这个时间需要自己衡量和测试。

这里把时间改成 500 ms ,发现数据就可以正常的采集。

在这里插入图片描述

最后周期数据采集如下图,测试的时候 6s 采集一次,实际使用根据情况而定:

在这里插入图片描述

结语

本文我们把 BH1750 传感器移植到一个 51 内核的芯片上使用。

过程不算顺利,出了很多小问题,但是整体来说,本文所讲解的知识点都是没有问题的,驱动的移植也算是成功的。

居然在数据处理的时候出了问题,虽然我们当时在 STM32 中程序中的语句是这么写的,而且也测试过了,但是确实在 51 上这条语句确实出了问题,而且中途还找错了方向,以至于我画了很多时间在其他地方 = =!

不过最后通过找到问题,也算是给了大家一个很好的示例。

完成本文,BH1750 的实战教学篇就算完成了,相信大家学习以后,不管在什么芯片上使用 BH1750 甚至是其他 I2C 通讯的传感器,都会顺顺利利!

本文就到这里,谢谢大家! 另外,别忘了下面可以加我的技术群哦!


http://www.ppmy.cn/news/907660.html

相关文章

光照传感器BH1750实验

光照传感器BH1750实验 1 实验目的 通过光照传感器 BH1750 检测光照数据实验熟悉 IIC 通信协议&#xff0c;在详细了解该协议的基 础上编程模拟 IIC 实现光照数据的采集。 2 BH1750 模块简介 BH1750 是一种用于两线式串行总线接口的数字型光强度传感器集成电路。这种集成电 路可…

RT-Thread Studio 使用笔记(六)| 获取光传感器数据(I2C设备驱动+BH1750手写驱动代码分享)

1. 介绍 2. 添加I2C设备 2.1. 打开I2C设备驱动框架 双击左侧 RT-Thread Setting 文件,即可打开RT-Thread图形化配置工具,软件模拟I2C这一项是灰色的,表示没有打开,单击一下即可打开软件 I2C 的驱动框架,图标变为彩色表示打开: 右击该选项可以打开更多配置,比如查看该…

BH1750( GY-302 )光照传感器

文章目录 一、产品简介二、IIC通信三、BH1750的使用四、程序源码 这里我先简单的介绍一下BH1750光照传感器模块的基本信息(不多废话)&#xff0c;我将着重讲解它的使用部分&#xff0c;相信对于屏幕前的你也是更关心它是怎么使用的&#xff0c;OK&#xff0c;gogogo&#xff01…

esp8266 BH1750光照强度传感器

BH1750FVI 是一种用于两线式串行总线接口的数字型光强度传感器集成电路。这种集成电路可以根据收集的光线强度数据来调整液晶或者键盘背景灯的亮度。利用它的高分辨率可以探测较大范围的光强度变化&#xff08; 1lx-65535lx&#xff09;。这个模块可以接入3.3~5v的电压。 发送…

spin_lock_bh使用

spin_lock_bh作用&#xff1a; 1.保护临界区 2.禁止抢占 3.禁止软中断 在软中执行函数_do_softirq()中已经使用__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET)来禁止软中断了&#xff0c;所以CPU上执行软中断是串行执行的。 软中断的执行&#xff1a; 1.irq_exit()退出时…

STM32驱动BH1750模块

模块描述 BH1750FVI是一款用于I2C总线接口的数字环境光传感器IC。该集成电路最适合获取环境光数据&#xff0c;用于调整手机的 LCD和键盘背光功率。可以在高分辨率下检测宽范围&#xff08;1-65535 lx&#xff09; 引脚说明 VCC5VGNDGNDSCLPB6SDAPB7ADDRVCC/GND ADDR引脚等…

bh1750采集流程图_基于BH1750光照强度数据采集系统的设计

基于 BH1750 光照强度数据采集系统的设计 刘博 【摘 要】 摘 要&#xff1a;光照度传感器是一种常用的检测装置&#xff0c;在多个行业中都有一 定的应用。 BH1750 是一种用于两线式串行总线接口的数字型光强度传感器集 成电路&#xff0c;利用这种集成电路制成的传感器可以采集…

bh1750采集流程图_基于BH1750的光照度检测)报告方案.doc

成绩评定: 传感器技术 课程设计 题 目 基于BH1750光照度检测 摘要 传统的光照主要采用光敏电阻,光敏电阻的光电流与光照度之间的关系称为光电特性。光敏电阻的光电特性呈非线性,因此不适宜作检测元件,在自动控制中它常被用作丌关式光电传感器。光敏电阻需要用A/D转换器将其…