继续创造,加快成长!这是我参加「日新计划 10 月更文应战」的第29天,点击检查活动详情
前言
上一篇 BH1750 的实战教育咱们阐明的实际运用中传感器的硬件规划 。
咱们说到过在本次运用的芯片为 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 驱动逻辑能够参考曾经分析的流程。
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 通讯的传感器,都会顺顺利利!
本文就到这儿,谢谢咱们! 别的,别忘了下面能够加我的技术群哦!