版权声明

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

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

  • Author: GuoWenxue <guowenxue@gmail.com> QQ: 281143292

wechat_pub

3.7 I2C子系统——SHT20驱动实验

3.7.1 I2C基本知识

3.7.1.1 i2c物理总线

i2c_bus1

如上图所示,i2c支持一主多从,各设备地址独立,标准模式传输速率为100kbit/s,快速模式为400kbit/s。总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空 闲,都输出高阻态时,由上拉电阻把总线拉成高电平。

I2C物理总线使用两条总线线路,SCL和SDA。

  • SCL: 时钟线,数据收发同步

  • SDA: 数据线,传输具体数据

3.7.1.2 i2c基本通信协议

3.7.1.2.1 起始信号与停止信号

i2c_bus2

当SCL线为高电平时,SDA线由高到低的下降沿,为传输开始标志(S)。直到主设备发出结束信号(P), 否则总线状态一直为忙。结束标志(P)为,当SCL线为高电平时,SDA线由低到高的上升沿。

3.7.1.2.2 数据格式与应答信号

i2c_bus3

i2c的数据字节定义为8-bits长度,对每次传送的总字节数量没有限制,但对每一次传输必须伴有一个应答(ACK)信号, 其时钟由主设备提供,而真正的应答信号由从设备发出,在时钟为高时,通过拉低并保持SDA的值来实现。如果从设备忙, 它可以使 SCL保持在低电平,这会强制使主设备进入等待状态。当从设备空闲后,并且释放时钟线,原来的数据传输才会继续。

3.7.1.2.3 主机与从机通信

i2c_bus4

开始标志(S)发出后,主设备会传送一个7位的Slave地址,并且后面跟着一个第8位,称为Read/Write位。 R/W位表示主设备是在接受从设备的数据还是在向其写数据。然后,主设备释放SDA线,等待从设备的应答信号(ACK)。 每个字节的传输都要跟随有一个应答位。应答产生时,从设备将SDA线拉低并且在SCL为高电平时保持低。 数据传输总是以停止标志(P)结束,然后释放通信线路。 然而,主设备也可以产生重复的开始信号去操作另一台从设备, 而不发出结束标志。综上可知,所有的SDA信号变化都要在SCL时钟为低电平时进行,除了开始和结束标志。

3.7.1.1 I2C驱动框架

在Linux系统中采用了总线、设备驱动模型。平台设备也是采用了这种模型,只不过平台总线是一个虚拟的总线。

我们知道一个i2c(例如i2c1)上可以挂在多个i2c设备,例如sht20、i2c接口的OLED显示屏、摄像头(摄像头通过i2c接口发送控制信息)等等, 这些设备共用一个i2c,这个i2c的驱动我们称为i2c总线驱动。而对应具体的设备,例如sht20的驱动就是i2c设备驱动。 这样我们要使用sht20就需要拥有“两个驱动”一个是i2c总线驱动和sht20设备驱动。

  • i2c总线驱动由芯片厂商提供(驱动复杂,官方提供了经过测试的驱动,我们直接用)。

  • sht20设备驱动可以从sht20芯片厂家那里获得,也可以我们手动编写。

3.7.i2c_driver_framework

如上图所示,i2c驱动框架包括i2c总线驱动、具体某个设备的驱动。

i2c总线包括i2c设备(i2c_client)和i2c驱动(i2c_driver), 当我们向linux中注册设备或驱动的时候,按照i2c总线匹配规则进行配对,配对成功,则可以通过i2c_driver中.prob函数创建具体的设备驱动。 在现代linux中,i2c设备不再需要手动创建,而是使用设备树机制引入,设备树节点是与paltform总线相配合使用的。 所以需先对i2c总线包装一层paltform总线,当设备树节点转换为平台总线设备时,我们在进一步将其转换为i2c设备,注册到i2c总线中。

设备驱动创建成功,我们还需要实现设备的文件操作接口(file_operations),file_operations中会使用到内核中i2c核心函数(i2c系统已经实现的函数,专门开放给驱动工程师使用)。 使用这些函数会涉及到i2c适配器,也就是i2c控制器。由于ic2控制器有不同的配置,所有linux将每一个i2c控制器抽象成i2c适配器对象。 这个对象中存在一个很重要的成员变量——Algorithm,Algorithm中存在一系列函数指针,这些函数指针指向真正硬件操作代码。

3.7.1.2 I2C设备核心驱动函数

i2c_add_driver()宏

注册一个i2c驱动 (内核源码/include/linux/i2c.h)

 #define i2c_add_driver(driver) 

这个宏函数的本质是调用了i2c_register_driver()函数,函数如下。

(1)i2c_register_driver()函数

注册一个i2c驱动 (内核源码/drivers/i2c/i2c-core-base.c)

int i2c_register_driver(struct module *owner, struct i2c_driver *driver) 

参数:

  • owner: 一般为 THIS_MODULE

  • driver: 要注册的 i2c_driver.

返回值:

  • 成功: 0

  • 失败: 负数

(2)i2c_del_driver()函数

将前面注册的 i2c_driver 从 Linux 内核中注销掉

  void i2c_del_driver(struct i2c_driver *driver)   
  • driver:要注销的 i2c_driver

  • **返回值:**无。

(3)i2c_transfer()函数

i2c_transfer()函数最终就是调用我们前面讲到的i2c_imx_xfer()函数来实现数据传输。

收发i2c消息

int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num) 

参数:

  • adap : struct i2c_adapter 结构体,收发消息所使用的i2c适配器,i2c_client 会保存其对应的 i2c_adapter

  • msgs: struct i2c_msg 结构体,i2c要发送的一个或多个消息

  • num : 消息数量,也就是msgs的数量

返回值:

  • 成功: 发送的msgs的数量

  • 失败: 负数

I2C 进行数据收发 说白了就是消息的传递,Linux 内核使用 i2c_msg 结构体来描述一个消息

使用i2c_transfer 函数之前需要先构建好i2c_msg

	struct i2c_msg { 
	
	        __u16 addr; /* 从机地址 */ 
	        __u16 flags; /* 标志 */ 
	        #define I2C_M_TEN 0x0010
	        #define I2C_M_RD 0x0001 
	        #define I2C_M_STOP 0x8000
	        #define I2C_M_NOSTART 0x4000
	
	        #define I2C_M_REV_DIR_ADDR 0x2000
	        #define I2C_M_IGNORE_NAK 0x1000
	        #define I2C_M_NO_RD_ACK 0x0800 
	        #define I2C_M_RECV_LEN 0x0400 
	        __u16 len; /* 消息(本 msg)长度 */ 
	
	        __u8 *buf; /* 消息数据 */ 
	}; 

(4)用于数据传输的其他API

I2C 数据发送函数为 i2c_master_send,函数原型如下:

  int i2c_master_send(const struct i2c_client *client, const char *buf, int  count)   
  • client:I2C 设备对应的 i2c_client。

  • buf:要发送的数据。

  • count:要发送的数据字节数,要小于 64KB,以为 i2c_msg 的 len 成员变量是一个 u16(无 符号 16 位)类型的数据。

  • **返回值:**负值,失败,其他非负值,发送的字节数。

I2C 数据接收函数为 i2c_master_recv,函数原型如下:

int i2c_master_recv(const struct i2c_client *client, char *buf, int   count)   
  • client:I2C 设备对应的 i2c_client。

  • buf:要接收的数据。

  • count:要接收的数据字节数,要小于 64KB,以为 i2c_msg 的 len 成员变量是一个 u16(无 符号 16 位)类型的数据。

  • **返回值:**负值,失败,其他非负值,发送的字节数

3.7.2 SHT20驱动实验

3.7.2.1 SHT20工作原理

sht20是基于i2c协议的温湿度传感器。

I2C是一种半双工串行通信总线,使用多主从架构,同一总线上可以挂多个i2c设备,每一个设备都有自己的地址,总线通过先传输地址,在传输命令,再由相应地址的i2c设备应答。

i2c的地址是根据pin2所接的位置电平来决定。如果需要多个一起控制,可以通过切换ADDR引脚上的电平来动态的改变传感器地址,但是不能在通信中更改。

sht20_ds1

发送完mersure命令后,数据准备好后,会收到来自从机的数据,有6个字节,首先是温度的高序字节和低序字节,接着是温度的CRC。再是相对湿度的高低字节和CRC。通过下面计算就能得到温度和相对湿度。

sht20_ds2

3.7.2.2 设备树

i2c设备驱动中包含i2c_driver 和 i2c_client,前面在设备树的i2c1节点下我们添加了i2c_sht20c@40这个节点并设置了compatible,那么在内核中就会注册这样的i2c_client。

&i2c1 {
	clock-frequency = <100000>;
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_i2c1>;
	status = "okay";

	i2c_sht20@40{
		compatible = "imx_i2c_sht20";
		status = "okay";
		reg = <0x40>;
	};
};

&iomuxc {
	pinctrl_i2c1: i2c1grp {
		fsl,pins = <
			MX6UL_PAD_GPIO1_IO02__I2C1_SCL	0x4001b8b0
			MX6UL_PAD_GPIO1_IO03__I2C1_SDA	0x4001b8b0
		>;
	};	
};

3.7.3 SHT20驱动实验

3.7.3.1 SHT20驱动实现

// &i2c1 {
// 	clock-frequency = <100000>;
// 	pinctrl-names = "default";
// 	pinctrl-0 = <&pinctrl_i2c1>;
// 	status = "okay";

// 	i2c_sht20@40{
// 		compatible = "imx_i2c_sht20";
// 		status = "okay";
// 		reg = <0x40>;
// 	};
// };

// &iomuxc {
// 	pinctrl_i2c1: i2c1grp {
// 		fsl,pins = <
// 			MX6UL_PAD_GPIO1_IO02__I2C1_SCL	0x4001b8b0
// 			MX6UL_PAD_GPIO1_IO03__I2C1_SDA	0x4001b8b0
// 		>;
// 	};	
// };

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>           //file_operations,用于联系系统调用和驱动程序
#include <linux/errno.h>        //ENODEV,ENOMEM存放的头文件
#include <linux/kernel.h>       // printk(),内核打印函数
#include <linux/device.h>       // 用于自动生成设备节点的函数头文件
#include <linux/gpio.h>         // gpio相关函数
#include <linux/gpio/consumer.h>// gpiod相关函数
#include <linux/of_gpio.h>      // gpio子系统相关函数
#include <linux/uaccess.h>        // copy_to_user函数头文件
#include <linux/timer.h>          //定时器相关
#include <linux/i2c.h>            //i2c相关
#include <linux/delay.h>          // 延时函数头文件
#include <linux/cdev.h>           //cdev相关函数

#define DEV_NAME            "sht20"    //最后在/dev路径下的设备名称,应用层open的字符串名 
#define DEV_CNT              1

#ifndef DEV_MAJOR	
#define DEV_MAJOR  0
#endif

/* SHT20 命令 */
#define SOFE_RESET                 0xFE    // 软复位
#define T_MEASURE_NO_HOLD_CMD      0xF3    // 无主机模式触发温度测量
#define RH_MEASURE_NO_HOLD_CMD     0xF5    // 无主机模式触发湿度测量
#define T_MEASURE_HOLD_CMD         0xE3    // 主机模式触发温度测量
#define RH_MEASURE_HOLD_CMD        0xE5    // 主机模式触发湿度测量

#define CRC_MODEL                   0x31
#define CRC_SUCCESS                 0
#define CRC_FAIL                    1

static int dev_major = DEV_MAJOR;	/*主设备号*/

/*存放sht20的私有属性*/
struct sht20_priv {
    struct cdev          cdev;     /*cdev结构体*/
    struct class        *dev_class;/*自动创建设备节点的类*/
    struct i2c_client   *client;
    struct device       *dev;
};


/*复位i2c
 *返回值,成功,返回0。失败,返回 -1
 */
static int sht20_soft_reset(struct i2c_client   *client)
{
    int                 rv = 0;
    struct i2c_msg      sht20_msg;
    uint8_t             cmd_data = SOFE_RESET;


    /*设置读取位置i2c_msg,发送 iic要写入的地址 reg*/
    sht20_msg.addr = client->addr; //sht20在 iic 总线上的地址
    sht20_msg.flags = 0;                 //标记为发送数据
    sht20_msg.buf = &cmd_data;           //写入的首地址
    sht20_msg.len = 1;                   //写入长度

    /*复位sht20*/
    rv = i2c_transfer(client->adapter, &sht20_msg, 1);

    // 软复位所需时间不超过15ms
    msleep(15);

    return rv;
}



/*检查CRC
 *返回值,成功,返回0。失败,返回 -1
 */
static int sht20_crc8(unsigned char *data, int len, unsigned char checksum)
{
    unsigned char   crc = 0x00; 
    int             i, j;  
 
    for(i=0; i<len; i++) 
    {
        crc ^= *data++;  		   
		
        for (j=0; j<8; j++)     
        { 
			crc = (crc & 0x80)?(crc << 1) ^ CRC_MODEL:(crc << 1);
        }
    }
    // printk("crc clu data : [%x]\n", crc);
 
    if(checksum == crc)
	{
        return CRC_SUCCESS;
	}
	else 
    {
        return CRC_FAIL;
    }
}


/*通过i2c 读sht20数据
 *sht20_client:sht20的i2c_client结构体。
 *cmd, 要写入的数据
 *返回值,错误,-1。成功,0
 */
static int i2c_read_sht20(struct i2c_client *client, uint8_t cmd, void *recv, uint32_t length)
{
    int rv = 0;
    u8 cmd_data = cmd;
    struct i2c_msg sht20_msg[2];

    /*设置读取位置i2c_msg,发送 iic要写入的地址 reg*/
    sht20_msg[0].addr = client->addr; //sht20在 iic 总线上的地址
    sht20_msg[0].flags = 0;                 //标记为发送数据
    sht20_msg[0].buf = &cmd_data;           //写入的首地址
    sht20_msg[0].len = 1;                   //写入长度

    /*读取i2c_msg*/
    sht20_msg[1].addr = client->addr; //sht20在 iic 总线上的地址
    sht20_msg[1].flags = I2C_M_RD;          //标记为读取数据
    sht20_msg[1].buf = recv;                //读取得到的数据保存位置
    sht20_msg[1].len = length;              //读取长度

    rv = i2c_transfer(client->adapter, sht20_msg, 2);

    if (rv != 2)
    {
            printk(KERN_DEBUG "\n i2c_read_sht20 error \n");
            return -1;
    }
    return 0;
}


/* @description: 读取sht20 的温湿度数据 
 *
 * @parm  : client - i2c 设备
 * @parm  : tx_data - 存储读取的数据
 * @return: 0 successfully , !0 failure
 */
static int read_t_rh_data(struct i2c_client *client, unsigned char *tx_data)
{
    int rv = 0;

    unsigned char rx_data[6];

    unsigned char    checksum[2];
    unsigned char  crc_data_t[2];
    unsigned char crc_data_rh[2];


    char cmd_t = T_MEASURE_HOLD_CMD;
    char cmd_rh = RH_MEASURE_HOLD_CMD;

    //形参判断
    if(!client || !tx_data)
    {
        printk("%s line [%d] %s() get invalid input arguments\n", __FILE__, __LINE__, __func__ );
        return -1;
    }

    //读取温度数据
    rv = i2c_read_sht20(client, cmd_t, rx_data, 3);
    if(rv < 0)
    {
        dev_err(&client->dev, "i2c recv tmper data failure.\n");
        return -1; 
    }
    
   // printk("read temperature: rx_data[0] %x rx_data[1] %x ; crc : rx_data[2] %x\n", rx_data[0], rx_data[1], rx_data[2]); 
   
   //读取湿度数据

    rv = i2c_read_sht20(client, cmd_rh, rx_data+3, 3);
    if(rv < 0)
    {
        dev_err(&client->dev, "i2c recv tmper data failure.\n");
        return -1; 
    }

     
   
   //数据处理 set to 12 bit relative humidity and 14 bit temperature reading. t+crc+rh+crc
    tx_data[0] = rx_data[0];
    tx_data[1] = (rx_data[1]&0xFC);//1111 1100 
    tx_data[2] = rx_data[3];
    tx_data[3] = (rx_data[4]&0xF0);//1111 0000 

    //printk("read humidity: tx_data[0] %x tx_data[1] %x ; tx_data[2] %x, tx_data[3] %x\n", tx_data[0], tx_data[1], tx_data[2], tx_data[3]);  

    //TODO: 可以加上CRC校验
    checksum[0] = rx_data[2];
    checksum[1] = rx_data[5];

    crc_data_t[0] = rx_data[0];
    crc_data_t[1] = rx_data[1];
    crc_data_rh[0] = rx_data[3];
    crc_data_rh[1] = rx_data[4];

    if(0 != sht20_crc8(crc_data_t, 2, checksum[0]))
    {
        dev_err(&client->dev, "tmperature data fails to pass cyclic redundancy check\n");
        return -1;
    }

    if(0 != sht20_crc8(crc_data_rh, 2, checksum[1]))
    {
        dev_err(&client->dev, "humidity data fails to pass cyclic redundancy check\n");
        return -1;
    }

    return 0;
}




/*
* @description : 字符设备操作函数集,open函数实现
* @param – inode : 传递给驱动的 inode
* @param - filp : 设备文件,file 结构体有个叫做 private_data 的成员变量
* 一般在 open 的时候将 private_data 指向设备结构体。
*@return : 0 成功;其他 失败
*/
static int sht20_open(struct inode *inode, struct file *filp)
{
    struct sht20_priv *priv;
    int    rv;

    printk("\n sht20_open \n");

    priv = container_of(inode->i_cdev, struct sht20_priv, cdev);

    filp->private_data = priv;
    
    /*向 sht20 发送配置数据,让sht20处于正常工作状态*/
    rv = sht20_soft_reset(priv->client);
    if(rv < 0)
    {
        dev_err(priv->dev, "sht20 init failure.\n");
    }

    return 0;
}


/* @description: 从设备读取文件
 *
 * @parm  : filp - 设备文件,文件描述符
 * @parm  : buf - 返回给用户空间的数据缓冲区
 * @parm  : cnt - 要读取的数据长度
 * @parm  : offt - 相对于文件首地址的偏移
 * @return: 读取的字节数,负数 - 读取失败
 */
static ssize_t sht20_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
    int rv = 0;
    //struct i2c_client *client = filp->private_data;
    struct sht20_priv *priv = filp->private_data;
    unsigned char tx_data[4];

    read_t_rh_data(priv->client, tx_data);    

    printk("test %x %x %x %x \n", tx_data[0], tx_data[1], tx_data[2], tx_data[3]);

    rv = copy_to_user(buf, tx_data, sizeof(tx_data));
    if(rv)
    {
        dev_err(priv->dev, "copy to user error.\n");
        return -EFAULT;
    }

    return rv;
}



static int sht20_release(struct inode *inode, struct file *filp)
{
    printk("sht20 release\n");
    return 0;

}


/*字符设备操作函数集*/
static struct file_operations sht20_fops =
{
            .owner = THIS_MODULE,
            .open = sht20_open,
            .read = sht20_read,
            .release = sht20_release,
};


/* @description: sysfs - 温度属性显示函数
 *
 * @parm  : devp - 设备指针,创建file时候会指定dev
 * @parm  : attr - 设备属性,创建时候传入
 * @parm  : buf - 传出给sysfs中显示的buf
 * @return: 显示的字节数
 */
 static ssize_t temp_humility_show(struct device *devp, struct device_attribute *attr, char *buf)
 {
    int rv = 0;
    unsigned char tx_data[4];
    struct sht20_priv *priv = dev_get_drvdata(devp);
    unsigned int temperature;
    unsigned int humidity;


    rv = read_t_rh_data(priv->client, tx_data);
    if(rv != 0)
    {
        dev_err(priv->dev, "read_t_rh_data to show error.\n");
        return -EFAULT;
    }

    temperature = ((tx_data[0] << 8) | tx_data[1]);//放大100倍

    humidity = ((tx_data[2] << 8) | tx_data[3]);//放大100倍
    printk("show_test %x %x %x %x \n", tx_data[0], tx_data[1], tx_data[2], tx_data[3]);

    return sprintf(buf, "t=%d,h=%d\n",temperature, humidity);//1000倍
 }

/* @description: sysfs - echo写入属性函数
 * @parm  : dev - 设备指针,创建file时候会指定dev
 * @parm  : attr - 设备属性,创建时候传入
 * @parm  : buf - 用户空间的buf
 * @parm  : count - 传入buf的size
 * @return: 写入的buf大小
 */
static ssize_t temp_humility_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
    return count;
}

/*声明并初始化一个 device_attribute结构体*/
DEVICE_ATTR(temp_humility, 0644, temp_humility_show, temp_humility_store);

/*i2c总线设备函数集:.probe函数只需要添加、注册一个字符设备即可。 */
static int sht20_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    struct sht20_priv       *priv = NULL; //临时存放私有属性的结构体
    dev_t                   devno; //设备的主次设备号
    int                     rv = 0;


    //0.给priv分配空间
    priv = devm_kzalloc(&client->dev, sizeof(struct sht20_priv), GFP_KERNEL);
    if(!priv)
    {
        return -ENOMEM;
    }

    /*---------------------注册 字符设备部分-----------------*/
    /*1.分配主次设备号,这里即支持静态指定,也至此动态申请*/
    if (0 != dev_major)  /*static*/
    {
        devno = MKDEV(dev_major, 0);
        rv = register_chrdev_region(devno, DEV_CNT, DEV_NAME);	/* /proc/devices/sht20 */
    } 
    else
    {
        rv = alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME);	/*动态申请字符设备号*/
        dev_major = MAJOR(devno);	/*获取主设备号*/
    }
	
	/*自动分配设备号失败*/
    if (rv < 0) 
    {
        printk("%s driver can't get major %d", DEV_NAME, dev_major);
        return rv;
    }
    printk(" %s driver use major %d\n", DEV_NAME, dev_major);
    
    /*2.分配cdev结构体,绑定主次设备号、fops到cdev结构体中,并注册给Linux内核*/
    priv->cdev.owner = THIS_MODULE;	/*.owner这表示谁拥有你这个驱动程序*/
    cdev_init(&priv->cdev, &sht20_fops);  /*初始化cdev,把fops添加进去*/
    rv = cdev_add(&priv->cdev, devno, DEV_CNT);	/*注册给内核,设备数量1个*/
    
    if (0 != rv) {
        printk( " %s driver can't register cdev:result=%d\n", DEV_NAME, rv);
        goto undo_major;
    }
    printk( " %s driver can register cdev:result=%d\n", DEV_NAME, rv);
    
    //3.创建类,驱动中进行节点创建
    priv->dev_class = class_create(THIS_MODULE, DEV_NAME);
    if(IS_ERR(priv->dev_class))
    {
        printk("%s driver create class failure\n", DEV_NAME);
        rv = -ENOMEM;
        goto undo_cdev;
    }

    //4.创建设备
    devno = MKDEV(dev_major, 0); //给每一个led设置设备号
    priv->dev = device_create(priv->dev_class, NULL, devno, NULL, DEV_NAME);		 /* /dev/sht20 注册这个设备节点*/
    if(IS_ERR(priv->dev_class))
    {
            rv = -ENOMEM;   //返回错误码,应用空间strerror查看
            goto undo_class;
    }

    //5.创建sysfs文件初始化
    if(device_create_file(priv->dev, &dev_attr_temp_humility))
    {
        rv = -ENOMEM;
        goto undo_device;
    }

    //6. 保存私有数据
    priv->client = client;
    i2c_set_clientdata(client, priv);
    dev_set_drvdata(priv->dev, priv);
    dev_info(&client->dev, "sht20 i2c driver probe okay.\n");

    return 0; 

undo_device:
        device_destroy(priv->dev_class, devno); /*注销每一个设备号*/

undo_class:
        class_destroy(priv->dev_class);

undo_cdev:
        cdev_del(&priv->cdev);

undo_major:
        unregister_chrdev_region(devno, DEV_CNT);

        return rv;
}

static int sht20_remove(struct i2c_client *client)
{
    /*删除设备*/
    struct sht20_priv   *priv = i2c_get_clientdata(client);//临时存放私有属性的结构体

    dev_t devno = MKDEV(dev_major, 0);


    devno = MKDEV(dev_major, 0);

    //删除sys中的属性
    device_remove_file(priv->dev, &dev_attr_temp_humility);

    device_destroy(priv->dev_class, devno); /*注销每一个设备号*/
    

    class_destroy(priv->dev_class); //注销类
    cdev_del(&priv->cdev);  //删除cdev
    unregister_chrdev_region(MKDEV(dev_major, 0), DEV_CNT);//释放设备号

    devm_kfree(&client->dev, priv);//释放堆
    printk("sht20 driver removed.\n");
    return 0;
}

/*定义设备树匹配表*/
static const struct of_device_id sht20_of_match_table[] = {
    {.compatible = "imx_i2c_sht20"},
    {/* sentinel */}};

/*定义i2c总线设备结构体*/
struct i2c_driver sht20_driver = {
    .probe = sht20_probe,
    .remove = sht20_remove,
    .driver ={
            .name	= "imx_i2c_sht20",      //无设备树时,用于设备和驱动间匹配
        	.owner = THIS_MODULE,    
            .of_match_table = sht20_of_match_table,
    },
};

/*
 * 驱动初始化函数
 */
static int __init sht20_driver_init(void)
{
    int ret;
    printk("sht20_driver_init\n");
    ret = i2c_add_driver(&sht20_driver); /*添加一个i2c设备驱动*/
    return ret;
}

/*
 * 驱动注销函数
 */
static void __exit sht20_driver_exit(void)
{
    printk("sht20_driver_exit\n");
    i2c_del_driver(&sht20_driver); /*删除一个i2c设备驱动*/
}

module_init(sht20_driver_init);
module_exit(sht20_driver_exit);

MODULE_AUTHOR("Guowenxue <guowenxue@gmail.com>");
MODULE_DESCRIPTION("I2C_sht20 driver on i.MX6ULL platform");
MODULE_LICENSE("GPL");
MODULE_ALIAS("i2c:sht20");

3.7.3.1.1 驱动入口和出口函数实现

驱动入口和出口函数仅仅用于注册、注销I2C设备驱动,代码如下:

/*定义i2c总线设备结构体*/
struct i2c_driver sht20_driver = {
    .probe = sht20_probe,
    .remove = sht20_remove,
    .driver ={
            .name	= "imx_i2c_sht20",      //无设备树时,用于设备和驱动间匹配
        	.owner = THIS_MODULE,    
            .of_match_table = sht20_of_match_table,
    },
};

/*
 * 驱动初始化函数
 */
static int __init sht20_driver_init(void)
{
    int ret;
    printk("sht20_driver_init\n");
    ret = i2c_add_driver(&sht20_driver); /*添加一个i2c设备驱动*/
    return ret;
}

/*
 * 驱动注销函数
 */
static void __exit sht20_driver_exit(void)
{
    printk("sht20_driver_exit\n");
    i2c_del_driver(&sht20_driver); /*删除一个i2c设备驱动*/
}

module_init(sht20_driver_init);
module_exit(sht20_driver_exit);

MODULE_AUTHOR("Zou Ying <1019804140@qq.com>");
MODULE_DESCRIPTION("I2C_sht20 driver on i.MX6ULL platform");
MODULE_LICENSE("GPL");
MODULE_ALIAS("i2c:sht20");

3.7.3.1.2 .probe函数和.remove函数实现

.probe函数用于实现初始化工作,.remove用于实现退出之前的清理工作。SHT20需要初始化的内容很少,.probe函数只需要添加、注册一个字符设备即可。程序源码如下:

/*i2c总线设备函数集:.probe函数只需要添加、注册一个字符设备即可。 */
static int sht20_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    struct sht20_priv       *priv = NULL; //临时存放私有属性的结构体
    dev_t                   devno; //设备的主次设备号
    int                     rv = 0;


    //0.给priv分配空间
    priv = devm_kzalloc(&client->dev, sizeof(struct sht20_priv), GFP_KERNEL);
    if(!priv)
    {
        return -ENOMEM;
    }

    /*---------------------注册 字符设备部分 省略-----------------*/
    /*1.分配主次设备号,这里即支持静态指定,也至此动态申请*/
    /*2.分配cdev结构体,绑定主次设备号、fops到cdev结构体中,并注册给Linux内核*/
    //3.创建类,驱动中进行节点创建
    //4.创建设备
    //5.创建sysfs文件初始化
    if(device_create_file(priv->dev, &dev_attr_temp_humility))
    {
        rv = -ENOMEM;
        goto undo_device;
    }

    //6. 保存私有数据
    priv->client = client;
    i2c_set_clientdata(client, priv);
    dev_set_drvdata(priv->dev, priv);
    dev_info(&client->dev, "sht20 i2c driver probe okay.\n");

    return 0; 

undo_device:
        device_destroy(priv->dev_class, devno); /*注销每一个设备号*/

undo_class:
        class_destroy(priv->dev_class);

undo_cdev:
        cdev_del(&priv->cdev);

undo_major:
        unregister_chrdev_region(devno, DEV_CNT);

        return rv;
}

static int sht20_remove(struct i2c_client *client)
{
    /*删除设备*/
    struct sht20_priv   *priv = i2c_get_clientdata(client);//临时存放私有属性的结构体

    dev_t devno = MKDEV(dev_major, 0);


    devno = MKDEV(dev_major, 0);

    //删除sys中的属性
    device_remove_file(priv->dev, &dev_attr_temp_humility);

    device_destroy(priv->dev_class, devno); /*注销每一个设备号*/
    

    class_destroy(priv->dev_class); //注销类
    cdev_del(&priv->cdev);  //删除cdev
    unregister_chrdev_region(MKDEV(dev_major, 0), DEV_CNT);//释放设备号

    devm_kfree(&client->dev, priv);//释放堆
    printk("sht20 driver removed.\n");
    return 0;
}

3.7.3.1.3 实现字符设备操作函数集

在.probe函数中添加了一个字符设备。sht20的初始化以及转换结果的读取都在这个字符设备的操作函数中实现,其中最主要的是.open和.read函数。下面是这两个函数的实现。

.open函数实现,我们在open函数中实现sht20复位。

/*
* @description : 字符设备操作函数集,open函数实现
* @param – inode : 传递给驱动的 inode
* @param - filp : 设备文件,file 结构体有个叫做 private_data 的成员变量
* 一般在 open 的时候将 private_data 指向设备结构体。
*@return : 0 成功;其他 失败
*/
static int sht20_open(struct inode *inode, struct file *filp)
{
    struct sht20_priv *priv;
    int    rv;

    printk("\n sht20_open \n");

    priv = container_of(inode->i_cdev, struct sht20_priv, cdev);

    filp->private_data = priv;
    
    /*向 sht20 发送配置数据,让sht20处于正常工作状态*/
    rv = sht20_soft_reset(priv->client);
    if(rv < 0)
    {
        dev_err(priv->dev, "sht20 init failure.\n");
    }

    return 0;
}

发送复位指令。


/*复位i2c
 *返回值,成功,返回0。失败,返回 -1
 */
static int sht20_soft_reset(struct i2c_client   *client)
{
    int                 rv = 0;
    struct i2c_msg      sht20_msg;
    uint8_t             cmd_data = SOFE_RESET;


    /*设置读取位置i2c_msg,发送 iic要写入的地址 reg*/
    sht20_msg.addr = client->addr; //sht20在 iic 总线上的地址
    sht20_msg.flags = 0;                 //标记为发送数据
    sht20_msg.buf = &cmd_data;           //写入的首地址
    sht20_msg.len = 1;                   //写入长度

    /*复位sht20*/
    rv = i2c_transfer(client->adapter, &sht20_msg, 1);

    // 软复位所需时间不超过15ms
    msleep(15);

    return rv;
}

sht20_read函数源码如下所示。

/* @description: 从设备读取文件
 *
 * @parm  : filp - 设备文件,文件描述符
 * @parm  : buf - 返回给用户空间的数据缓冲区
 * @parm  : cnt - 要读取的数据长度
 * @parm  : offt - 相对于文件首地址的偏移
 * @return: 读取的字节数,负数 - 读取失败
 */
static ssize_t sht20_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
    int rv = 0;
    struct sht20_priv *priv = filp->private_data;
    unsigned char tx_data[4];

    read_t_rh_data(priv->client, tx_data);    

    printk("test %x %x %x %x \n", tx_data[0], tx_data[1], tx_data[2], tx_data[3]);

    rv = copy_to_user(buf, tx_data, sizeof(tx_data));
    if(rv)
    {
        dev_err(priv->dev, "copy to user error.\n");
        return -EFAULT;
    }

    return rv;
}

sht20_read()函数调用read_t_rh_data()函数读取sht20 的温湿度数据。调用copy_to_user函数将转换得到的数据拷贝到用户空间。

/* @description: 读取sht20 的温湿度数据 
 *
 * @parm  : client - i2c 设备
 * @parm  : tx_data - 存储读取的数据
 * @return: 0 successfully , !0 failure
 */
static int read_t_rh_data(struct i2c_client *client, unsigned char *tx_data)
{
    int rv = 0;

    unsigned char rx_data[6];

    unsigned char    checksum[2];
    unsigned char  crc_data_t[2];
    unsigned char crc_data_rh[2];


    char cmd_t = T_MEASURE_HOLD_CMD;
    char cmd_rh = RH_MEASURE_HOLD_CMD;

    //形参判断
    if(!client || !tx_data)
    {
        printk("%s line [%d] %s() get invalid input arguments\n", __FILE__, __LINE__, __func__ );
        return -1;
    }

    //读取温度数据
    rv = i2c_read_sht20(client, cmd_t, rx_data, 3);
    if(rv < 0)
    {
        dev_err(&client->dev, "i2c recv tmper data failure.\n");
        return -1; 
    }
    
   // printk("read temperature: rx_data[0] %x rx_data[1] %x ; crc : rx_data[2] %x\n", rx_data[0], rx_data[1], rx_data[2]); 
   
   //读取湿度数据

    rv = i2c_read_sht20(client, cmd_rh, rx_data+3, 3);
    if(rv < 0)
    {
        dev_err(&client->dev, "i2c recv tmper data failure.\n");
        return -1; 
    }

     
   
   //数据处理 set to 12 bit relative humidity and 14 bit temperature reading. t+crc+rh+crc
    tx_data[0] = rx_data[0];
    tx_data[1] = (rx_data[1]&0xFC);//1111 1100 
    tx_data[2] = rx_data[3];
    tx_data[3] = (rx_data[4]&0xF0);//1111 0000 

    //printk("read humidity: tx_data[0] %x tx_data[1] %x ; tx_data[2] %x, tx_data[3] %x\n", tx_data[0], tx_data[1], tx_data[2], tx_data[3]);  

    checksum[0] = rx_data[2];
    checksum[1] = rx_data[5];

    crc_data_t[0] = rx_data[0];
    crc_data_t[1] = rx_data[1];
    crc_data_rh[0] = rx_data[3];
    crc_data_rh[1] = rx_data[4];

    if(0 != sht20_crc8(crc_data_t, 2, checksum[0]))
    {
        dev_err(&client->dev, "tmperature data fails to pass cyclic redundancy check\n");
        return -1;
    }

    if(0 != sht20_crc8(crc_data_rh, 2, checksum[1]))
    {
        dev_err(&client->dev, "humidity data fails to pass cyclic redundancy check\n");
        return -1;
    }

    return 0;
}

通过调用i2c_read_sht20()发送命令。

发送无主机温度测量命令,能读到14bit的温度数据+一个字节的校验位

发送无主机湿度测量命令,能读到12bit的湿度数据+一个字节的校验位

我们只需要获取温度和湿度数据。

校验位通过sht20_crc8()函数来判断数据是否正确。

/* @description: 通过i2c 读sht20数据 
 * @parm  : client - i2c 设备
 * @parm  : cmd, 要写入的数据
 * @parm  :void *recv, 收到的数据
 * @parm  :uint32_t length,收到数据的长度
 * @return: 0 successfully , !0 failure
 */
static int i2c_read_sht20(struct i2c_client *client, uint8_t cmd, void *recv, uint32_t length)
{
    int rv = 0;
    u8 cmd_data = cmd;
    struct i2c_msg sht20_msg[2];

    /*设置读取位置i2c_msg,发送 iic要写入的地址 reg*/
    sht20_msg[0].addr = client->addr; //sht20在 iic 总线上的地址
    sht20_msg[0].flags = 0;                 //标记为发送数据
    sht20_msg[0].buf = &cmd_data;           //写入的首地址
    sht20_msg[0].len = 1;                   //写入长度

    /*读取i2c_msg*/
    sht20_msg[1].addr = client->addr; //sht20在 iic 总线上的地址
    sht20_msg[1].flags = I2C_M_RD;          //标记为读取数据
    sht20_msg[1].buf = recv;                //读取得到的数据保存位置
    sht20_msg[1].len = length;              //读取长度

    rv = i2c_transfer(client->adapter, sht20_msg, 2);

    if (rv != 2)
    {
            printk(KERN_DEBUG "\n i2c_read_sht20 error \n");
            return -1;
    }
    return 0;
}

检查crc8函数

/* @description: 检查CRC 
 * @parm  : data, 原始数据
 * @parm  :len, 原始数据长度
 * @parm  :checksum,校验位
 * @return: 0 successfully , !0 failure
 */
static int sht20_crc8(unsigned char *data, int len, unsigned char checksum)
{
    unsigned char   crc = 0x00; 
    int             i, j;  
 
    for(i=0; i<len; i++) 
    {
        crc ^= *data++;  		   
		
        for (j=0; j<8; j++)     
        { 
			crc = (crc & 0x80)?(crc << 1) ^ CRC_MODEL:(crc << 1);
        }
    }
    // printk("crc clu data : [%x]\n", crc);
 
    if(checksum == crc)
	{
        return CRC_SUCCESS;
	}
	else 
    {
        return CRC_FAIL;
    }
}

3.7.3.1.4 sysfs文件系统显示函数

将获取的温湿度原始数据显示在文件夹下。

/* @description: sysfs - 温度属性显示函数
 *
 * @parm  : devp - 设备指针,创建file时候会指定dev
 * @parm  : attr - 设备属性,创建时候传入
 * @parm  : buf - 传出给sysfs中显示的buf
 * @return: 显示的字节数
 */
 static ssize_t temp_humility_show(struct device *devp, struct device_attribute *attr, char *buf)
 {
    int rv = 0;
    unsigned char tx_data[4];
    struct sht20_priv *priv = dev_get_drvdata(devp);
    unsigned int temperature;
    unsigned int humidity;


    rv = read_t_rh_data(priv->client, tx_data);
    if(rv != 0)
    {
        dev_err(priv->dev, "read_t_rh_data to show error.\n");
        return -EFAULT;
    }

    temperature = ((tx_data[0] << 8) | tx_data[1]);//放大100倍

    humidity = ((tx_data[2] << 8) | tx_data[3]);//放大100倍
    printk("show_test %x %x %x %x \n", tx_data[0], tx_data[1], tx_data[2], tx_data[3]);

    return sprintf(buf, "t=%d,h=%d\n",temperature, humidity);//1000倍
 }

/* @description: sysfs - echo写入属性函数
 * @parm  : dev - 设备指针,创建file时候会指定dev
 * @parm  : attr - 设备属性,创建时候传入
 * @parm  : buf - 用户空间的buf
 * @parm  : count - 传入buf的size
 * @return: 写入的buf大小
 */
static ssize_t temp_humility_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
    return count;
}

/*声明并初始化一个 device_attribute结构体*/
DEVICE_ATTR(temp_humility, 0644, temp_humility_show, temp_humility_store);

3.7.3.2 sht20测试应用程序


/* 在C语言编程时,一般系统的头文件用<xxx.h>,我们自己写的头文件则用"zzz.h" */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <string.h>
#include <time.h>
#include <errno.h>

/* 在C语言编程中,函数应该先定义再使用,如果函数的定义在函数调用后面,应该前向声明。*/
int sht20_get_temp_humi(float *temp, float *humility);

int main(int argc, char *argv[])
{
    float       temp = 0; /* 温度值有小数位,所以使用浮点数 */
    float       humility = 0;

    /* 1,在Linux下做C语言编程时,函数返回值一般是0表示成功,<0表示失败,我们也遵循这个规约;
     * 2,但函数调用只能有一个返回值,所以这里的采样函数只能通过指针来返回采样的温度值;
     * 3,因为要在sht20_get_temp_humi()函数中修改main()中temp、humility的值,所以这里传&temp,&humility;
     */
    if( sht20_get_temp_humi(&temp, &humility) < 0 )
    {
        printf("ERROR: sht20 get temprature and humility failure\n");
        return 1;
    }
    /* 打印DS18B20的采样温度值,因为℃是非ASCII打印字符,所以这里用 'C 代替 */
    printf("sht20 get temperature: %f 'C\n", temp);
    printf("sht20 get humility: %f %\n", humility);
    
    return 0;
}

/*
 * 函数说明: 该函数用来使用 sht20 温度传感器采样获取当前的温度值;
 * 参数说明: $temp: 通过指针返回DS18B20的采样温度,
 * 参数说明: humility:通过指针返回DS18B20的采样湿度,
 * 返回说明: ==0 表示成功, <0 表示失败
 */
int sht20_get_temp_humi(float *temp, float *humility)
{
    const char     *sht20_path = "/sys/class/sht20/sht20/";/*my_sht20_driver*/
    char            ds_path[50]; /* DS18B20 采样文件路径 */
    char            buf[128];    /* read() 读数据存储 buffer */
    DIR            *dirp = NULL;        /* opendir()打开的文件夹句柄 */
    int             fd =-1;      /* open()打开文件的文件描述符 */
    char           *ptr_begin = NULL; /* 一个字符开始指针,用来字符串处理 */
    char           *ptr_end = NULL;/* 一个字符开始指针,用来字符串处理 */
    char            data_buf[16];
    int             rv = 0;      /* 函数返回值,默认设置为成功返回(0) */


    /* 在C语言编程时,进入函数的第一件事应该进行函数参数的合法性检测,检查参数非法输入。
     * 否则调用者"不小心"通过 $temp 传入一个空指针,下面的代码就有可能出现段错误。
     */
    if( !temp )
    {
        return -1;
    }
    
    /*我的驱动 打开 "/sys/class/w1_ds18b20/w1_ds18b20/" 文件夹,如果打开失败则打印错误信息并退出。*/
    if((dirp = opendir(sht20_path)) == NULL)
    {
        printf("opendir error: %s\n", strerror(errno));
        return -2;
    }

    snprintf(ds_path, sizeof(ds_path), "%s/temp_humility", sht20_path);/*my_sht20_driver*/

    /* 接下来打开 DS18B20 的采样文件,如果失败则返回相应的错误码-4。 */
    if( (fd=open(ds_path, O_RDONLY)) < 0 )
    {
        printf("open %s error: %s\n", ds_path, strerror(errno));
        return -4;
    }

    /* 读取文件中的内容将会触发 DS18B20温度传感器采样,这里读取文件内容保存到buf中 */
    if(read(fd, buf, sizeof(buf)) < 0)
    {
        printf("read %s error: %s\n", ds_path, strerror(errno));
        
        /* 1, 这里不能直接调用 return -5 直接返回,否则的话前面open()打开的文件描述符就没有关闭。
         * 这里设置 rv 为错误码-5,通过 goto 语句跳转到函数后面统一进行错误处理。
         *
         * 2, 在C语言编程时我们应该慎用goto语句进行"随意"的跳转,因为它会降低代码的可读性。但这里是
         * goto语句的一个非常典型应用,我们经常会用它来对错误进行统一的处理。
         *
         * 3,goto后面的cleanup为标号,它在下面的代码中定义。
         */
        rv = -5;
        goto cleanup;
    }

    /* 采样温度值是在字符串"t="后面,这里我们从buf中找到"t="字符串的位置并保存到ptr指针中 */
    ptr_begin = strstr(buf, "t=");
    ptr_end = strstr(buf, ",");
    
    if( !ptr_begin || !ptr_end )
    {
        printf("ERROR: Can not get temperature\n");
        rv = -6;
        goto cleanup;
    }
    else
    {
        /* 因为此时ptr是指向 "t="字符串的地址(即't'的地址),那跳过2个字节(t=)后面的就是采样温度值 */
        ptr_begin += strlen("t=");
        //printf("sht20 get ptr_begin: %s 'C\n", ptr_begin);
        memcpy(data_buf, ptr_begin, ptr_end - ptr_begin);
    }

    //printf("sht20 get temp buf:%s ptr: %s \n", buf, data_buf);
    /* 接下来我们使用 atof() 函数将采样温度值字符串形式,转化成 float 类型。*/
    *temp = -46.85 + 175.72/65536 * atof(data_buf);

    ptr_begin = strstr(buf, "h=");
    ptr_begin += strlen("h=");

    //printf("sht20 get buf:%s ptr: %s \n", buf, ptr_begin);

    *humility = -6.0  + 125.0 /65536 * atof(ptr_begin);

    //printf("sht20 get temperature:%f and humility:%f \n", *temp, *humility);
    /* 1,在这里我们对函数返回进行集中处理,其中 cleanup 为 goto 语句的标号;
     * 2,在函数退出时,我们应该考虑清楚在前面的代码中做了哪些事,这些事是否需要进行反向操作。如
     *    打开的文件或文件夹是否需要关闭,malloc()分配的内存是否需要free()等。
     * 3, 在最开始我们定义rv并赋初值为0(表示成功)是有原因的,如果前面的代码任何一个地方出现错误,
     *    则会将rv的值修改为相应的错误码,否则rv的值将始终为0(即没有错误发生),这里将统一返回。
     */
cleanup: 
    close(fd);
    return rv;
}


通过驱动代码获取的温度都是原始数据,因为驱动无法做浮点运算,所以在应用程序进行浮点运算,计算出真实的温湿度值。

3.7.sht20_ds3

3.7.sht20_ds4

3.7.3.3 Makefile

KERNAL_DIR := /home/zying/imx6ull/bsp/kernel/linux-imx
CROSS_COMPILE := /opt/gcc-arm-11.2-2022.02/bin/arm-none-linux-gnueabihf-
TFTP_DTR := /tftp/zouying
PWD := $(shell pwd)
obj-m := i2c_sht20.o

modules:
	$(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules
	$(CROSS_COMPILE)gcc i2c_sys_App.c -o i2c_sys_App
	@make clear
	cp i2c_sht20.ko i2c_sys_App $(TFTP_DTR) -f

clear:
	@rm -f *.o *.cmd *.mod *.mod.c
	@rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f
	@rm -f .*ko.cmd .*.o.cmd .*.o.d
	@rm -f *.unsigned

clean:
	@rm -f *.ko

3.7.4 实验准备

3.7.4.1 加载设备树

设备树内容如下, imx6ull/bsp/kernel/linux-imx/arch/arm/boot/dts/overlays下创建i2c_sht20.dts

在Makefile 中加上

dtb-y += i2c_sht30.dtbo

保存后在linux源码根目录下

make dtbs

即可生成 i2c_sht30.dtbo文件。

在开发板上挂载boot目录 修改config.txt,加上i2c_sht30。

dtoverlay_extra=i2c_sht30

并在overlays上把i2c_sht30.dtbo 传入。sync后重启。

启动的过程中看到有i2c_sht30.dtbo applying的记录 说明成功打入dts base。设备树调试完成。

3.7.4.2 硬件连接

sht20有4个引脚,VCC SDA SCL GND,分别连接到imx6ull开发板上扩展引脚的对应位置。

sht20_hwconn

3.7.5 程序运行结果

安装i2c_sht20.ko模块

root@igkboard:~/dirver/07i2c_sht20# ls
i2c_App  i2c_sht20.ko
root@igkboard:~/dirver/07i2c_sht20# insmod i2c_sht20.ko 
[ 1075.284487] sht20_driver_init
[ 1075.287716]  sht20 driver use major 243
[ 1075.291586]  sht20 driver can register cdev:result=0
[ 1075.305252] imx_i2c_sht20 0-0040: sht20 i2c driver probe okay.

3.7.5.1 测试效果

在sysfs中显示

root@igkboard:~/driver/07i2c_sht20# cat /sys/class/sht20/sht20/temp_humility 
t=26788,h=33456

运行测试app

root@igkboard:~/driver/07i2c_sht20# ./i2c_sys_App 
sht20 get temperature: 24.975979 'C
sht20 get humility: 57.415527 %