版权声明

本文档所有内容文字资料由凌云实验室郭工编著,主要用于凌云嵌入式Linux教学内部使用,版权归属作者个人所有。任何媒体、网站、或个人未经本人协议授权不得转载、链接、转帖或以其他方式复制发布/发表。已经授权的媒体、网站,在下载使用时必须注明来源,违者本人将依法追究责任。

  • Copyright (C) 2022 凌云物网智科实验室·郭工

  • Author: Guo Wenxue Email: guowenxue@gmail.com QQ: 281143292

wechat_pub

2.7 I2C接口编程之温湿度采样

2.7.1 I2C协议简介

I2C Bus(IIC,Inter-Integrated Circuit Bus) 是由 Philips 公司(现被 NXP 收购)推出的一种在电子通信控制领域常用的通信协议。它由时钟线(SCL)数据线(SDA)两根线构成通信线路,利用上拉电阻将它们拉成高电平(表示总线空闲), 其典型的电压准位为+3.3V 或**+5V**,具有电路简单、连接线少、控制简单、通信速率高等优点。

2.7.1.1 I2C物理层

I2C通讯设备之间的常用连接方式如下图

I2C_Master_Slaver_connect

  • I2C 总线是一种主从结构(Master/Slave)总线,I2C 总线上的每一个设备都可以作为主设备或者从设备, 但一个总线上一般只有一个主设备,可以带多个从设备。

  • 主设备用来产生允许传输的时钟信号,并初始化总线的数据传输,所以主设备通常是 CPU;而从设备只能被动响应主设备发起的通信请求,所以各种I2C 接口芯片将作为从设备使用。

  • 每个连接到总线的设备都有一个独立的设备地址,主机可以利用这个地址进行不同设备之间的访问。 其中地址是一个七位或十位的数字。

  • 具有三种传输模式:标准模式传输速率为100kbit/s ,快速模式为400kbit/s , 高速模式下可达 3.4Mbit/s,但目前大多I2C设备尚不支持高速模式。

  • 连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制

2.7.1.2 I2C协议层

在 I2C 总线上传送的每一位数据都由一个同步时钟脉冲相对应,即在 SCL 串行时钟的配合下,数据在 SDA 上从高位向低位依次串行传送每一位的数据。下面是 I2C 通信的时序图:

I2C_Communication_sequence

2.7.1.2.1 起始位

I2C 总线在空闲时 SDASCL 都处于高电平状态(由上拉电阻拉成高电平),当主设备要开始一次 I2C 通信时就发送一个 START(S)信号,这个起始位就可以告诉所有 I2C 从机,“我”要开始进行 I2C 通信了;当要结束一次 I2C 通信时,则发送一个 STOP(P)信号结束本次通信。

I2C_Start_Stop_signal

  • I2C 协议起始位:当 SCL 保持高电平时,SDA 出现一个下降沿,产生一个起始位;

  • I2C 协议停止位:当 SCL 保持高电平时,SDA 出现一个上升沿,产生一个停止位;

2.7.1.2.2 I2C读写地址

主机在发送 START 信号之后,第 2 个时序应该立刻给出要通信的目标从机物理地址。此外,I2C 总线是一种能够实现半双工通信的同步串行通信协议,站在主设备的角度来看应该具有读/写从设备的功能。

这时候 I2C 的读写地址除了 7bit 物理地址以外,还有 1bit 用来标识读/写方向位。这样I2C 的从设备读写地址通常是一个字节,其中高 7bit 是上面描述的物理地址,最低位用来表示读写方向(0 为写操作,1 为读操作)。

I2C_Read_Write_address

2.7.1.2.3 I2C应答信号

主机往 I2C 总线上传输器件地址,所有的从机接收到这个地址后与自己的地址相比较若相同则发出一个应答 ACK 信号,主机收到这个应答信号后通讯连接建立成功,若未收到应答信号则表示寻址失败。可以在上文中总线时序中第9个时钟时的电平高低进行判断。

此外,主/从机在之后的数据通信中,数据接收方(可能是主机也可能是从机)收到传输的一个字节数据后,需要给出响应,此时处在第九个时钟,发送端释放 SDA 线控制权,将 SDA 电平拉高,由接收方控制。

  • 若希望继续,则给出“应答(ACK, Acknowledge)”信号,即SDA 为低电平

  • 若不希望继续,则给出“非应答(NACK,Not Acknowledge)”信号,即 SDA 为高电平

2.7.1.2.4 数据位收发

主机在收到从机的应答信号之后,开始给从机发送数据。SDA 数据线上的每个字节必须是 8 位,每次传输的字节数量没有限制,每个字节发送完成之后,从机必须跟一个应答信号。

I2C 总线通信时数据位传输采用 **MSB(最高位优先)**方式发送,其中高电平表示数据位 1,低电平表示数据位 0。当传输的数据位需要改变时(如上一个位发送的是 1,下一个位要发送 0),必须发生在 SCL 为低电平期间。即规定所有设备在SCL高时候,进行数据的获取,SCL低时候,进行数据的变化。

I2C_Data_Bit_Data_Bit_Rev_Send

2.7.1.2.5 总线速率

I2C 总线是一种同步、半双工、采用电平信号收发的串行总线,其速率支持:

  • 标准模式(Standard-mode):速率高达 100kbit/s

  • 快速模式(Fast-mode):速率高达 400kbit/s

  • 快速模式+(Fast-mode Plus):速率高达 1Mbit/s。

  • 高速模式(High-speed mode):速率高达 3.4Mbit/s

  • 超快速模式(Ultra Fast-mode):速率高达 5Mbit/s (单向传输时支持)

2.7.1.2.6 主机发送数据流程

  1. 主机在检测到总线为空闲时,发送一个启动信号“S”,开始一次通信的开始;

  2. 主机接着发送一个从设备地址,它由 7bit 物理地址和 1bit 读写控制位 R/W 组成(此时 R/W=0 写操作);

  3. 相对应的从机收到命令字节后向主机回馈应答信号 ACK(ACK=0);

  4. 主机收到从机的应答信号后开始发送第一个字节的数据;

  5. 从机收到数据后返回一个应答信号 ACK

  6. 主机收到应答信号后再发送下一个数据字节;

  7. 主机发完最后一个字节并收到 ACK 后,向从机发送一个停止信号 P 结束本次通信并释放总线;

  8. 从机收到 P 信号后也退出与主机之间的通信;

I2C_Master_Send_Data_Process

2.7.1.2.7 主机接收数据流程

  1. 主机发送启动信号后,接着发送地址字节(其中 R/W=1 读操作);

  2. 对应的从机收到地址字节后,返回一个应答信号并向主机发送数据;

  3. 主机收到数据后向从机反馈一个应答信号 ACK

  4. 从机收到应答信号后再向主机发送下一个数据;

  5. 当主机完成接收数据后,向从机发送一个 NAK,从机收到非应答信号后便停止发送;

  6. 主机发送非应答信号后,再发送一个停止信号,释放总线结束通信;

I2C_Master_Receive_Data_Process

2.7.2 使能开发板IIC通信接口

I.MX6ULL有4个I2C外设,但是我们的IGKBoard开发板只使能了两路I2C外设,其中

I2C2 ---> 触摸屏控制器,rtc时钟,摄像头控制器
I2C1 ---> 40pin扩展口使用,由DToverlay进行开启
	GPIO01_IO03 ---> I2C1_SDA
	GPIO01_IO03 ---> I2C1_SCL

想要使能40pin扩展口的I2C1的话,需要修改开发板上的DTOverlay配置文件,添加该管脚对I2C的支持,具体修改具体方法为修改 eMMC 启动介质的 boot 分区下的 config.txt 文件,将dtoverlay_i2c1的选项修改为yes,如下所示。

root@igkboard:~# vi /run/media/mmcblk1p1/config.txt 

# Enable I2C overlay
dtoverlay_i2c1=yes

修改完成后重启系统,系统启动时将会自动加载 I2C 协议驱动。查看/dev下是否存在I2C设备节点,或查看/sys/bus/i2c/devices中设备文件,已验证I2C驱动是否加载

root@igkboard:~# ls -l /dev/i2c-*
crw------- 1 root root 89, 0 Oct 10 09:29 /dev/i2c-0
crw------- 1 root root 89, 1 Oct 10 09:29 /dev/i2c-1
root@igkboard:~# ls /sys/bus/i2c/devices/
1-005d  1-006f  i2c-0  i2c-1

由于驱动是从0开始编写的,故实际的I2C1对应的设备节点是i2c-0 ,并且可以看到/sys/bus/i2c/devices/下还有两个“1-”开始的链接文件,这些都是I2C2下的设备,即板载的触摸屏控制器和RTC时钟控制器(从设备)。

2.7.3 I2C工具 i2c-tools

2.7.3.1 i2c-tools简介

I2C tools包含一套用于Linux应用层测试各种各样I2C功能的工具。它的主要功能包括:总线探测工具、SMBus访问帮助程序、EEPROM解码脚本、EEPROM编程工具和用于SMBus访问的python模块。只要你所使用的内核中包含I2C设备驱动,那么就可以在你的板子中正常使用这个测试工具。

我们IGKBoard上烧录的系统中已经预先安装好了该工具,我们可以直接开始使用该工具。

该工具包有以下常用的工具命令

root@igkboard:~# i2c
i2cdetect              i2cdump                i2cget                 i2cset                 i2ctransfer  

2.7.3.1.1 i2cdetect

i2cdetect的主要功能就是I2C设备查询,它用于扫描I2C总线上的设备。它输出一个表,其中包含指定总线上检测到的设备的列表。该命令的常用格式为:

i2cdetect [-y] [-a] [-q|-r] i2cbus [first last]

具体参数的含义如下:

-y

取消交互模式。默认情况下,i2cdetect将等待用户的确认,当使用此标志时,它将直接执行操作。

-a

强制扫描非规则地址。一般不推荐。

-q

使用SMBus“快速写入”命令进行探测。一般不推荐。

-r

使用SMBus“接收字节”命令进行探测。一般不推荐。

-F

显示适配器实现的功能列表并退出。

-V

显示I2C工具的版本并推出。

-l

显示已经在系统中使用的I2C总线。

i2cbus

表示要扫描的I2C总线的编号或名称。

first last

表示要扫描的从设备地址范围。

​ 通过i2cdetect -l查看当前系统中的I2C的总线情况

root@igkboard:~# i2cdetect -l
i2c-0   i2c             21a0000.i2c                             I2C adapter
i2c-1   i2c             21a4000.i2c                             I2C adapter

​ 总线上挂载I2C从设备,可通过i2cdetect扫描某个I2C总线上的所有设备。可通过控制台输入i2cdetect -y 1或0(确定哪一条总线) 来查看所有设备

root@igkboard:~# i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- UU -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- UU 
70: -- -- -- -- -- -- -- --                         
root@igkboard:~# i2cdetect -y 0
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --
# 在40pin上I2C1上插上SHT20模块后
root@igkboard:~# i2cdetect -y 0
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: 40 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --    

显示的内容分析

  • ‘–’:表示该地址被检测,但没有芯片应答。

  • ‘UU’:表示该地址当前由内核驱动程序使用。

  • ’**’:**表示以十六进制表示的设备地址编号,如 “40”或“4e”。

使用 i2cdetect -F i2cbus 查询i2c总线的功能,参数i2cbus表示i2c总线。

root@igkboard:~# i2cdetect -F 0
Functionalities implemented by /dev/i2c-0:
I2C                              yes
SMBus Quick Command              yes
SMBus Send Byte                  yes
SMBus Receive Byte               yes
SMBus Write Byte                 yes
SMBus Read Byte                  yes
SMBus Write Word                 yes
SMBus Read Word                  yes
SMBus Process Call               yes
SMBus Block Write                yes
SMBus Block Read                 yes
SMBus Block Process Call         no
SMBus PEC                        yes
I2C Block Write                  yes
I2C Block Read                   yes

2.7.3.1.2 i2cget

i2cget:读取指定IIC设备的某个寄存器的值,相关命令语法

i2cget [-f] [-y] i2cbus chip-address [data-address [mode]]

具体参数含义如下

-f

强制访问设备,即使它已经很忙。 默认情况下,i2cget将拒绝访问已经在内核驱动程序控制下的设备。

-y

取消交互模式。默认情况下,i2cdetect将等待用户的确认,当使用此标志时,它将直接执行操作。

-a

允许在0x00 - 0x07和0x78 - 0x7f之间使用地址。一般不推荐。

i2cbus

表示要扫描的I2C总线的编号或名称。这个数字应该与i2cdetect -l 列出的总线之一相对应。

chip-address

要操作的外设从地址。

data-address

被查看外设的寄存器地址。

mode

显示数据的方式: b (read byte data, default) w (read word data) c (write byte/read byte)

读取0总线上从地址为0x40的外设的0x10寄存器的数据

root@igkboard:~# i2cget -y -f 0 0x40 0xe5
0x56

2.7.3.1.3 i2cdump

i2cdump的主要功能查看I2C从设备器件所有寄存器的值。 该命令的常用格式为:

i2cdump [-f] [-r first-last] [-y] [-a] i2cbus address [mode [bank [bankreg]]]

具体参数含义如下

-f

强制访问设备,即使它已经很忙。 默认情况下,i2cget将拒绝访问已经在内核驱动程序控制下的设备。

-r

限制正在访问的寄存器范围。 此选项仅在模式b,w,c和W中可用。对于模式W,first必须是偶数,last必须是奇数。

-y

取消交互模式。默认情况下,i2cdetect将等待用户的确认,当使用此标志时,它将直接执行操作。

-V

显示I2C工具的版本并推出。

i2cbus

表示要扫描的I2C总线的编号或名称。这个数字应该对应于i2cdetect -l列出的总线之一。

first last

表示要扫描的从设备地址范围。

mode

b: 单个字节
w:16位字
s:SMBus模块
i:I2C模块的读取大小 c: 连续读取所有字节,对于具有地址自动递增功能的芯片(如EEPROM)非常有用。
W与 w类似,只是读命令只能在偶数寄存器地址上发出;这也是主要用于EEPROM的。

下面是读取0总线上地址为0x40的sht20的数据,命令为:

root@igkboard:~# i2cdump -f -y 0 0x40
No size specified (using byte-data access)
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f    0123456789abcdef
00: XX XX XX 69 XX 57 3a 3a XX 06 XX XX XX XX XX 02    XXXiXW::X?XXXXX?
10: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX    XXXXXXXXXXXXXXXX
20: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX    XXXXXXXXXXXXXXXX
30: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX    XXXXXXXXXXXXXXXX
40: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX    XXXXXXXXXXXXXXXX
50: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX    XXXXXXXXXXXXXXXX
60: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX    XXXXXXXXXXXXXXXX
70: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX    XXXXXXXXXXXXXXXX
80: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX    XXXXXXXXXXXXXXXX
90: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX    XXXXXXXXXXXXXXXX
a0: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX    XXXXXXXXXXXXXXXX
b0: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX    XXXXXXXXXXXXXXXX
c0: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX    XXXXXXXXXXXXXXXX
d0: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX    XXXXXXXXXXXXXXXX
e0: XX XX XX 69 XX 57 3a 3a XX 06 XX XX XX XX XX 02    XXXiXW::X?XXXXX?
f0: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX    XXXXXXXXXXXXXXXX

由于sht20并不是一个完整的存储设备,所以很多寄存器值显示是XX,该命令适合在使用eeprom等存储设备的场景。

2.7.3.1.4 i2cset

i2cset的主要功能是通过I2C总线设置设备中某寄存器的值。该命令的常用格式为:

i2cset [-f] [-y] [-m mask] [-r] i2cbus chip-address data-address [value][mode]

具体参数的含义如下

-f

强制访问设备,即使它已经很忙。 默认情况下,i2cget将拒绝访问已经在内核驱动程序控制下的设备。

-r

在写入值之后立即读取它,并将结果与写入的值进行比较。

-y

取消交互模式。默认情况下,i2cdetect将等待用户的确认,当使用此标志时,它将直接执行操作。

-V

显示I2C工具的版本并推出。

i2cbus

表示要扫描的I2C总线的编号或名称。这个数字应该对应于i2cdetect -l列出的总线之一。

-m mask

如果指定mask参数,那么描述哪些value位将是实际写入data-addres的。
掩码中设置为1的位将从值中取出,而设置为0的位将从数据地址中读取,从而由操作保存。

mode

b: 单个字节
w:16位字
s:SMBus模块
i:I2C模块的读取大小
c: 连续读取所有字节,对于具有地址自动递增功能的芯片(如EEPROM)非常有用。

假设板载有eeprom硬件,可以向0总线上从地址为0x50的eeprom的0x10寄存器写入0x55,命令为(下面命令仅供参考,IGKBoard上暂时无法使用)

root@igkboard:~# i2cset -y -f 0 0x50 0x10 0x55

2.7.3.1.5 i2ctransfer

i2ctransfer的主要功能是在一次传输中发送用户定义的I2C消息。i2ctransfer是一个创建I2C消息并将其合并为一个传输发送的程序。对于读消息,接收缓冲区的内容被打印到stdout,每个读消息一行。该命令的常用格式为:

i2ctransfer [-f] [-y] [-v] [-a] i2cbus desc [data] [desc [data]]

具体参数的含义如下

-f

强制访问设备,即使它已经很忙。 默认情况下,i2cget将拒绝访问已经在内核驱动程序控制下的设备。

-y

取消交互模式。默认情况下,i2cdetect将等待用户的确认,当使用此标志时,它将直接执行操作。

-V

显示I2C工具的版本并推出。

-v

启用详细输出。它将打印所有信息发送,即不仅为读消息,也为写消息。

-a

允许在0x00 - 0x02和0x78 - 0x7f之间使用地址。一般不推荐。

i2cbus

表示要扫描的I2C总线的编号或名称。这个数字应该对应于i2cdetect -l 列出的总线之一。

假设板载有eeprom硬件,下面是完成向0总线上从地址为0x50的eeprom的0x20开始的4个寄存器写入0x01,0x02,0x03,0x04,命令为

root@igkboard:~# i2ctransfer -f -y 0 w5@0x50 0x20 0x01 0x02 0x03 0x04

再通过下面命令读取刚写入的4个寄存器的值

root@igkboard:~# i2ctransfer -f -y 0 w1@0x50 0x20 r4

2.7.4 I2C编程驱动SHT20

2.7.4.1 I2C相关数据结构

  • ioctl函数

在编写应用程序时需要使用ioctl函数设置i2c相关配置,其函数原型如下

 #include <sys/ioctl.h>
 int ioctl(int fd, unsigned long request, ...);

其中对于终端request的值常用的有以下几种

I2C_RETRIES

设置收不到ACK时的重试次数,默认为1

I2C_TIMEOUT

设置超时时限的jiffies

I2C_SLAVE

设置从机地址

I2C_SLAVE_FORCE

强制设置从机地址

I2C_TENBIT

选择地址长度0为7位地址,非0为10位

使用ioctl系统调用初始化好i2c设备后,设置从设备地址后,便可以使用write和read系统调用进行数据的写入和读取,此时一般会按照传感器数据手册提供的数据传输方式进行命令的写入,然后按其要求读取数据,完成一次SHT20温湿度数据的读取。

2.7.4.2 SHT20数据手册解读

通过官方提供的信息和i2c-tools了解sht20的从设备地址为0x40,下面我们来看看sht20的其他命令,查看数据手册可知,sht20主要提供以下几个命令

I2C_sht20_command_code

我们常用红色框选中的命令,使用**非保持主机模式(no hold master)**的命令,可以使sht20在测量时候,释放i2c总线,使得主机可以处理其他从设备的通信任务。

查看sht20的通讯时序

![I2C_sht20_no_hold_master_Communication sequence](images/I2C_sht20_no_hold_master_Communication sequence.png)

可以清楚知道,大体分为两步

  • 写入模式,对sht20写入非主机湿度测量命令

  • 读取模式,读取sht20返回的值

查看sht20返回的数据格式

由上图知道返回的两个字节数据,高字节数据 ‘0110 0011’ ,低字节数据 ‘01010010’,除去最后2bit的状态位,第43bit (0:温度值 1:湿度值),实际数据有效位为14bit,即0x6350 = 25424。这个2byte的数据需要通过一个公式计算才能获取到,温度和湿度的计算公式如下所示:

I2C_sht20_RH_T_conv_formula

按照公式计算示例图中的结果,可以计算出上图的0x6450对应的湿度值为

RH = -6 + 125*(25424 / 65536) = 42.4924  #单位为%

2.7.4.3 SHT20物理连接

找到IGKBoard的I2C接口,然后使用杜邦线将其按下图与sht20连接

I2C_sht20_IGKBoard_connect

实物连接如下。

I2C_sht20_IGKBoard_real_connect

2.7.4.4 SHT20应用程序编写

下面使sht20测试程序源码

guowenxue@ubuntu20:~/igkboard/apps$ vim sht20.c
/*********************************************************************************
 *      Copyright:  (C) 2022 Guo Wenxue <guowenxue@gmail.com>
 *                  All rights reserved.
 *
 *       Filename:  sht20.c
 *    Description:  This file is temperature and relative humidity sensor SHT20 code
 *
 *        Version:  1.0.0(2022/4/14)
 *         Author:  Guo Wenxue <guowenxue@gmail.com>
 *      ChangeLog:  1, Release initial version on "2022/04/14 12:13:26"
 *
 ********************************************************************************/

/*
 * Hardware connection:
 *
 *   Sht20 Module       RaspberryPi/IGKBoard
 *      VCC      <----->      3.3V
 *      SDA      <----->      SDA
 *      SCL      <----->      SCL
 *      GND      <----->      GND
 */

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/types.h>
#include <sys/stat.h>
#include <linux/i2c-dev.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <stdint.h>
#include <time.h>
#include <errno.h>
#include <string.h>

#define SOFTRESET                        0xFE
#define TRIGGER_TEMPERATURE_NO_HOLD      0xF3
#define TRIGGER_HUMIDITY_NO_HOLD         0xF5

static inline void msleep(unsigned long ms);
static inline void dump_buf(const char *prompt, uint8_t *buf, int size);
int sht2x_init(char *i2c_dev);
int sht2x_get_serialnumber(int fd, uint8_t *serialnumber, int size);
int sht2x_get_temp_humidity(int fd, float *temp, float *rh);

int main(int argc, char **argv)
{
    int         fd;
    float       temp;
    float       rh;
    uint8_t     serialnumber[8];

    if( argc != 2)
    {
        printf("This program used to do I2C test by sht20 temperature & humidity module.\n");
        printf("Usage: %s /dev/i2c-x\n", argv[0]);
        return 0;
    }

    fd = sht2x_init(argv[1]);
    if(fd < 0)
    {
        printf("SHT2x initialize failure\n");
        return 1;
    }

    if( sht2x_get_serialnumber(fd, serialnumber, 8) < 0)
    {
        printf("SHT2x get serial number failure\n");
        return 3;
    }

    if( sht2x_get_temp_humidity(fd, &temp, &rh) < 0 )
    {
        printf("SHT2x get get temperature and relative humidity failure\n");
        return 3;
    }

    printf("Temperature=%lf ℃ relative humidity=%lf%\n", temp, rh);

    close(fd);
}

/* ms级休眠函数 */
static inline void msleep(unsigned long ms)
{
    struct timespec cSleep;
    unsigned long ulTmp;

    cSleep.tv_sec = ms / 1000;
    if (cSleep.tv_sec == 0)
    {
        ulTmp = ms * 10000;
        cSleep.tv_nsec = ulTmp * 100;
    }
    else
    {
        cSleep.tv_nsec = 0;
    }

    nanosleep(&cSleep, 0);
}

/* 打印 buf值*/
static inline void dump_buf(const char *prompt, uint8_t *buf, int size)
{
    int          i;

    if( !buf )
    {
        return ;
    }

    if( prompt )
    {
        printf("%s ", prompt);
    }

    for(i=0; i<size; i++)
    {
        printf("%02x ", buf[i]);
    }
    printf("\n");

    return ;
}

/* sht20命令复位 */
int sht2x_softreset(int fd)
{
    uint8_t           buf[4];

    if( fd<0 )
    {
        printf("%s line [%d] %s() get invalid input arguments\n", __FILE__, __LINE__, __func__ );
        return -1;
    }

    /* software reset SHT2x */
    memset(buf, 0, sizeof(buf));

    /* 按照手册命令表进行命令发送 并休眠需要的时间 */
    buf[0] = SOFTRESET;
    write(fd, buf, 1);

    msleep(50);

    return 0;
}

/* 初始化sht20 设备新参为i2c总线节点 */
int sht2x_init(char *i2c_dev)
{
    int          fd;

    if( (fd=open(i2c_dev, O_RDWR)) < 0)
    {
        printf("i2c device open failed: %s\n", strerror(errno));
        return -1;
    }

    /* set I2C mode and SHT2x slave address */
    ioctl(fd, I2C_TENBIT, 0);    /* Not 10-bit but 7-bit mode */
    ioctl(fd, I2C_SLAVE, 0x40); /* set SHT2x slava address 0x40*/

    if( sht2x_softreset(fd) < 0 )
    {
        printf("SHT2x softreset failure\n");
        return -2;
    }

    return fd;
}

/* 获取sht20的温度湿度值 */
int sht2x_get_temp_humidity(int fd, float *temp, float *rh)
{
    uint8_t           buf[4];

    if( fd<0 || !temp || !rh )
    {
        printf("%s line [%d] %s() get invalid input arguments\n", __FILE__, __LINE__, __func__ );
        return -1;
    }

    /* send trigger temperature measure command and read the data */
    memset(buf, 0, sizeof(buf));
    /* 发送非主机保持的温度测量命令 */
    buf[0]=TRIGGER_TEMPERATURE_NO_HOLD;
    write(fd, buf, 1);

    /* 等待需要的时间 数据手册可知 */
    msleep(85); /* datasheet: typ=66, max=85 */

    memset(buf, 0, sizeof(buf));
    /* 读取三个字节的数据,最后一个字节为crc检验值 */
    read(fd, buf, 3);
    dump_buf("Temperature sample data: ", buf, 3);
    /* 计算实际温度值*/
    *temp = 175.72 * (((((int) buf[0]) << 8) + (buf[1] & 0xfc)) / 65536.0) - 46.85;

    /* send trigger humidity measure command and read the data */
    memset(buf, 0, sizeof(buf));
    /* 发送非主机保持的湿度测量命令 */
    buf[0] = TRIGGER_HUMIDITY_NO_HOLD;
    write(fd, buf, 1);

    msleep(29); /* datasheet: typ=22, max=29 */
    memset(buf, 0, sizeof(buf));

    read(fd, buf, 3);
    dump_buf("Relative humidity sample data: ", buf, 3);
    /* 计算实际湿度值*/
    *rh = 125 * (((((int) buf[0]) << 8) + (buf[1] & 0xfc)) / 65536.0) - 6;

    return 0;
}

/* 获取sht20的唯一序列号 详情可以查看数据手册*/
int sht2x_get_serialnumber(int fd, uint8_t *serialnumber, int size)
{
    uint8_t           buf[4];

    if( fd<0 || !serialnumber || size!=8 )
    {
        printf("%s line [%d] %s() get invalid input arguments\n", __FILE__, __LINE__, __func__ );
        return -1;
    }

    /* Read SerialNumber from Location 1 */
    memset(buf, 0, sizeof(buf));
    buf[0] = 0xfa;  /* command for readout on-chip memory */
    buf[1] = 0x0f;  /* on-chip memory address */
    write(fd, buf, 2);

    memset(buf, 0, sizeof(buf));
    read(fd, buf, 4);

    serialnumber[5]=buf[0]; /* Read SNB_3 */
    serialnumber[4]=buf[1]; /* Read SNB_2 */
    serialnumber[3]=buf[2]; /* Read SNB_1 */
    serialnumber[2]=buf[3]; /* Read SNB_0 */

    /* Read SerialNumber from Location 2 */
    memset(buf, 0, sizeof(buf) );
    buf[0]=0xfc;  /* command for readout on-chip memory */
    buf[1]=0xc9;  /* on-chip memory address */
    write(fd, buf, 2);

    memset(buf, 0, sizeof(buf) );
    read(fd, buf, 4);

    serialnumber[1]=buf[0]; /* Read SNC_1 */
    serialnumber[0]=buf[1]; /* Read SNC_0 */
    serialnumber[7]=buf[2]; /* Read SNA_1 */
    serialnumber[6]=buf[3]; /* Read SNA_0 */

    dump_buf("SHT2x Serial number: ", serialnumber, 8);

    return 0;
}

编写Makefile如下

guowenxue@ubuntu20:~/igkboard/apps$ vim Makefile
CC=arm-linux-gnueabihf-gcc
APP_NAME=sht20

all:clean
	@${CC} ${APP_NAME}.c -o ${APP_NAME}

clean:
	@rm -f ${APP_NAME}

2.7.4.5 交叉编译测试运行

在ubuntu下的相关源码路径下执行make命令将会编译源码生成ARM开发板上的可执行文件。

guowenxue@ubuntu20:~/igkboard/apps$ make
guowenxue@ubuntu20:~/igkboard/apps$ ls
Makefile  sht20  sht20.c

现在我们在开发板上通过 tftp 命令 或其它方式将编译生成的测试程序下载到开发板上。

root@igkboard:~# tftp -gr sht20 192.168.2.2

接下来,我们给该程序加上执行权限并运行。可以正常打印当前开发板周围的温湿度值。

root@igkboard:~# chmod a+x sht20
root@igkboard:~# ./sht20 /dev/i2c-0
SHT2x Serial number:  12 01 4a b5 3c 74 00 d5 
Temperature sample data:  69 a0 6a 
Relative humidity sample data:  53 a2 54 
Temperature=25.651661 ℃ relative humidity=34.832520%

2.7.5 I2C 逻辑分析仪 时序抓取

以下是使用逻辑分析仪抓取的IGKBoard上运行的SHT20应用程序运行后的实际的时序波形图,以下五个图对应五个部分: 1.地址 写命令 + 软复位 TS_sht20_imx6ull_write_softreset

2.非保持主机模式 温度采集命令 TS_sht20_imx6ull_write_temp_cmd

3.读取命令+读取到的三字节数据(2byte 温度相关(需要公式计算最后结果) + 1byte CRC8校验和) TS_sht20_imx6ull_read_temp_data

4.非保持主机模式 湿度采样命令 TS_sht20_imx6ull_write_humidity_cmd

5.读取命令+读取到的三字节数据(2byte 湿度相关(需要公式计算最后结果) + 1byte CRC8校验和) TS_sht20_imx6ull_read_humidity_data

由上图中可以看出每次时序的起始和终止位的波形,然后读写地址情况,以及每个字节的应答信号情况,对于最终数据而言还需要根据数据手册进行公式计算,才能得出最终的温湿度情况。