前言
纸上谈兵大家都会,设计一定要实际落地才有价值!
上一篇 BH1750 的实战教学我们说明的实际应用中传感器的硬件设计 :
BH1750 传感器实战教学 —— 硬件设计篇
https://xie.infoq.cn/article/f819a35a36a64e3115c7b5f01
我们提到过在本次使用的芯片为 51 内核,I2C 通讯驱动实现与 STM32 上还是有很大区别的。
对于我们来说,已经掌握了 STM32 上 BH1750 驱动,如何能够快速准确的把程序移植过来? 就是本文的主要内容。
说明,我们讨论的驱动为 软件 I2C 驱动,软件 I2C 驱动的好处之一就是可以方便的移植。
我是 矜辰所致,全网同名,尽量用心写好每一系列文章,不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开!
一、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 1
void 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 bits
u8Bit = 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 bit
scl_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 SCSEDIO0
delay_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 first
u8Data = 0;
// 8 data bits
while(u8Bit)
{
scl_high();
delay_nop(70);
u8Bit >>= 1; //next bit
u8Data <<= 1;
u8Data |= sda_read(); //(sda_port & sda_pin)? 1 :0 sda_port gpio0 sda_pin SCSEDIO0
delay_nop(30);
scl_low();
delay_nop(80);
}
// 9th bit acknowledge
if(u8Ack==I2C_ACK){
sda_low();
delay_nop(30);
} //I2C_ACK=0
else
{
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 光照传感器文档详解 及 驱动设计
https://xie.infoq.cn/article/7da5636517271217cc7c03ca2
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_buffer
为 uint8
类型,我们这样直接位移然后与一下是否会有问题?
我把程序改成:
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 通讯的传感器,都会顺顺利利!
本文就到这里,谢谢大家!
评论