linux驱动系列学习之温湿度显示(十)

一、引言

        本篇博文写的是一个最近做的小项目。具体的功能为:检测温湿度传感器(dht11)的数据并显示到oled上面,通过mqtt协议传输到onenet平台。本项目功能简单,只是将之前博文介绍的一些东西整合起来。

        具体使用的技术有:linux单总线驱动dht11、i2c框架控制oled、poll机制(驱动和应用)、内核定时器的使用、队列、mqtt协议。

        使用到的环境:imx6ull开发板(正点原子阿波罗开发板)、ubuntu18(已安装交叉环境)、dht11温湿度传感器、i2c接口的0.96寸oled、onenet平台。

二、Linux驱动

1. dht11驱动

        dht11是使用单总线协议控制的数字温湿度传感器。这里使用模拟IO的方式进行驱动dht11。

1)dht11时序(记图、联系监删)

在这里插入图片描述

主机IO引脚拉低数据线,保持 t1(至少 18ms)时间,拉高数据线 t2(20~ 40us)时间,等待 DHT11 的响应。dht11 会拉低数据线并保持 t3(40~50us)时间,作为响应信号, dht11 拉高数据线,保持 t4(40 ~50us)时间后,开始输出数据。

dht11的时序比较简单。程序控制按照其时序编写即可。Linux驱动中使用udelay、mdelay作为微秒级、毫秒级延时。程序代码如下。

2)Linux驱动程序

        a)dht11驱动的结构体,后面的程序需要使用这个结构体创建设备、控制IO,这里记录下dht11使用的系统资源。

struct dht11_device{
	dev_t                dev_id;                   //设备号
	int                  major;                    //主设备号
	int                  minor;                    //次设备号
	struct cdev	         cdev;                     //字符设备
	struct class         *class;                   //类
	struct device        *device;                  //设备
	struct device_node   *device_node;             //设备节点
	int                  gpio;                     //数据引脚
	uint16_t             humidity,  temperature;   //检测到的温湿度数据
	int                  data_flag;                //数据更新标志位
	struct timer_list    timer;
};

struct dht11_device g_dht11_device;  

   b) dht11本身的单总线驱动程序

#define DHT11_DelayMs(t)  mdelay(t)
#define DHT11_DelayUs(t)  udelay(t)   

#define DHT11_PIN_HIGH  1
#define DHT11_PIN_LOW   0 

#define DHT11_IO_OUT(dht11)          gpio_direction_output(dht11->gpio, 1);
#define DHT11_IO_IN(dht11)           gpio_direction_input(dht11->gpio)
#define DHT11_WRITE(dht11,bit)       gpio_set_value(dht11->gpio, bit)
#define DHT11_READ(dht11)            gpio_get_value(dht11->gpio)

//设置引脚电平
void dht11_set_gpio(struct dht11_device* dht11,int value)
{
	if(value == DHT11_PIN_HIGH){
		gpio_direction_output(g_dht11_device.gpio, DHT11_PIN_HIGH);//gpio_direction_output(dht11->gpio, DHT11_PIN_HIGH);
	}else if(value == DHT11_PIN_LOW){
		gpio_direction_output(g_dht11_device.gpio, DHT11_PIN_LOW);//gpio_direction_output(dht11->gpio, DHT11_PIN_LOW);
	}
	
}



//等待响应
static int dht11_wait_for_ready(struct dht11_device *dht)
{   
    int timeout;

    timeout = 400;
    while (DHT11_READ(dht) && timeout)      // 等待低电平到来 
    {
        udelay(1);
        --timeout;
    }
    if (!timeout) 
    {
        printk("timeout %d\n", __LINE__);
        return -1;    // 超时 
    }

    timeout = 1000;
    while (!DHT11_READ(dht) && timeout)      // 等待高电平到来  
    {
        udelay(1);
        --timeout;
    }
    if (!timeout) 
    {
        printk("timeout %d\n", __LINE__);
        return -1;    // 超时 
    }

    timeout = 1000;
    while (DHT11_READ(dht) && timeout)  // 等待高电平结束
    {
        udelay(1);
        --timeout;
    }
    if (!timeout) 
    {
        printk("timeout %d\n", __LINE__);
        return -1;    // 超时 
    }

    return 0;
}

//开始信号
static int dht11_start(struct dht11_device *dht)
{
    DHT11_IO_OUT(dht);
    DHT11_WRITE(dht,0);
    mdelay(20);
    DHT11_WRITE(dht,1);
    udelay(30);
    DHT11_IO_IN(dht);          // 设置为输入 
    udelay(2);
   
    if (dht11_wait_for_ready(dht)) return -1;
    return 0;
}

//读取数据
static int dht11_read_byte(struct dht11_device *dht,unsigned char *byte)
{
    unsigned char i;
    unsigned char bit = 0;
    unsigned char data = 0;
    int timeout = 0;   
    
    for (i = 0; i < 8; i++)
    {
        timeout = 1000;  
        while (DHT11_READ(dht) && timeout)   // 等待变为低电平 
        {
            udelay(1);
            --timeout;
        }
        if (!timeout) 
        {
            printk("timeout %d\n", __LINE__);         
            return -1;           // 超时 
        }

        timeout = 1000;
        while (!DHT11_READ(dht) && timeout)    // 等待变为高电平 
        {
            udelay(1);
            --timeout;
        }
        if (!timeout) 
        {
            printk("timeout %d\n", __LINE__);
            return -1;           // 超时 
        }
        udelay(40);
        
        bit = DHT11_READ(dht);

        data <<= 1;            
        if (bit) 
        {
            data |= 0x01;

        }
        // data <<= 1;          // 导致错误的原因 : 移位要放前面,不能放在这里,若放在后面一旦获取最后一个位就会多移动一位导致数据不对 
    }

    *byte = data;
    return 0;
}


//从DHT11读取一次数据
//temp:温度值(范围:0~50°)
//humi:湿度值(范围:20%~90%)
//返回值:0,正常;1,读取失败
static int dht11_read_data(struct dht11_device* dht)
{        
     unsigned  char data[5] = {0};
	int i = 0,ret = 0;
    // 启动信号 
    if (dht11_start(dht) != 0)
    {
        printk("dht11 start failed\n");
        ret = -EFAULT;
    }

    // 读出5字节数据
    for (i = 0; i < 5; i++)    
    {
        if (dht11_read_byte(dht,&data[i]))
        {
            printk("read data err\n");
            ret = -EAGAIN;
        }
    }
	if (data[4] != (data[0]+data[1]+data[2]+data[3]))
    {
        printk("check data failed\n");
        ret = -EAGAIN;
    }
	dht->humidity = data[0];
    dht->temperature = data[2];
			
	return 0;
}

 c)与Linux驱动框架有关代码

static int __init dht11_init(void)
{
	int ret = 0;
	uint16_t humidity,temperature;

	// 1、获取设备节点:gpioled 

	g_dht11_device.device_node = of_find_node_by_path("/dht11");
	if(g_dht11_device.device_node == NULL) {
		printk("dht11 node not find!\r\n");
		return -EINVAL;
	} else {
		printk("dht11 node find!\r\n");
	}

	// 2、 获取设备树中的gpio属性,得到LED所使用的LED编号 
	g_dht11_device.gpio = of_get_named_gpio(g_dht11_device.device_node, "dht11-gpio", 0);
	if(g_dht11_device.gpio < 0) {
		printk("can't get dht11 gpio");
		return -EINVAL;
	}
	printk("dht11 gpio num = %d\r\n", g_dht11_device.gpio);

	// 3、设置GPIO1_IO03为输出,并且输出高电平
	gpio_request(g_dht11_device.gpio, "dht11_gpio");	// 请求IO 
	printk("dht11 gpio : %d\n",g_dht11_device.gpio);
	//gpio_direction_input(g_dht11_device.gpio);	// 设置为输入 
	ret = gpio_direction_output(g_dht11_device.gpio, 1);
	if(ret < 0) {
		printk("can't set gpio!\r\n");
	}

	// 注册字符设备驱动 
	// 4、创建设备号 
	if (g_dht11_device.major) {		//  定义了设备号 
		g_dht11_device.dev_id = MKDEV(g_dht11_device.major, 0);
		register_chrdev_region(g_dht11_device.dev_id, 1, DHT11_NAME);
	} else {						// 没有定义设备号 
		alloc_chrdev_region(&g_dht11_device.dev_id, 0, 1, DHT11_NAME);	// 申请设备号 
		g_dht11_device.major = MAJOR(g_dht11_device.dev_id);	// 获取分配号的主设备号
		g_dht11_device.minor = MINOR(g_dht11_device.dev_id);	// 获取分配号的次设备号
	}
	printk("gpioled major=%d,minor=%d\r\n",g_dht11_device.major, g_dht11_device.minor);	
	
	// 5、初始化cdev、添加一个cdev
	g_dht11_device.cdev.owner = THIS_MODULE;
	cdev_init(&g_dht11_device.cdev, &dht11_fops);
	cdev_add(&g_dht11_device.cdev, g_dht11_device.dev_id, 1);

	// 6、创建类 
	g_dht11_device.class = class_create(THIS_MODULE, DHT11_NAME);
	if (IS_ERR(g_dht11_device.class)) {
		return PTR_ERR(g_dht11_device.class);
	}

	// 7、创建设备
	g_dht11_device.device = device_create(g_dht11_device.class, NULL, g_dht11_device.dev_id, NULL, DHT11_NAME);
	if (IS_ERR(g_dht11_device.device)) {
		return PTR_ERR(g_dht11_device.device);
	}
	
	// 8、初始化定时器
	init_timer(&g_dht11_device.timer);
	g_dht11_device.timer.function = timer_func;
	g_dht11_device.timer.data = (volatile unsigned long)(&g_dht11_device);
	g_dht11_device.timer.expires = jiffies + msecs_to_jiffies(1000);//设定超时时间,1000代表1秒
	add_timer(&g_dht11_device.timer);//添加定时器,定时器开始生效	
    
    //仅用于驱动测试
	dht11_read_data(&g_dht11_device);	
	printk("humidity: %d   temperature: %d	 \n",g_dht11_device.humidity,g_dht11_device.temperature);
	
	return 0;
}


static void __exit dht11_exit(void)
{

	del_timer(&g_dht11_device.timer);  //删除定时器

	gpio_free(g_dht11_device.gpio);
	
	// 注销字符设备驱动 
	cdev_del(&g_dht11_device.cdev);//	删除cdev 
	unregister_chrdev_region(g_dht11_device.dev_id, 1); // 注销设备号 

	device_destroy(g_dht11_device.class, g_dht11_device.dev_id);
	class_destroy(g_dht11_device.class);

}



module_init(dht11_init);
module_exit(dht11_exit);
MODULE_LICENSE("GPL");

d)poll相关程序

        dht11使用了poll机制,这里用定时器每隔一定时间读取dht11的数据,将其保存下来,并通过poll函数通知应用程序。相关代码如下。

DECLARE_WAIT_QUEUE_HEAD(dht11_poll_queue);  //定义一个队列

//定时器回调函数,每一秒执行一次
static void timer_func(unsigned long arg)
{
	struct dht11_device *dht = (struct dht11_device *)arg;
	dht11_read_data(dht);    //每秒更新一次传感器数据
	dht->timer.function = timer_func;
	dht->timer.expires = jiffies + msecs_to_jiffies(1000);//设定超时时间,1000代表1秒
	add_timer(&(dht->timer));//添加定时器,定时器开始生效	
	//通知poll,数据可以读取
	dht->data_flag= 1;
	//printk("humidity: %d   temperature: %d	 \n",dht->humidity,dht->temperature);
		
}

static unsigned int dht11_poll (struct file * fp, poll_table * wait)
{
	unsigned int mask = 0;
	poll_wait(fp,&dht11_poll_queue, wait);
	if(g_dht11_device.data_flag == 1){  
		g_dht11_device.data_flag = 0;
		mask |= POLLOUT;	
	}
	return mask;
}

static ssize_t dht11_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int err = 0;
	struct dht11_device *dht = filp->private_data;
	uint16_t data[] = { dht->humidity, dht->temperature };

	err = copy_to_user(buf, data, sizeof(data));
	return err;

}

// 设备操作函数 
static const struct file_operations dht11_fops = {
	.owner = THIS_MODULE,
	.open = dht11_open,
	.read = dht11_read,
	.release = 	dht11_release,
	.poll      = dht11_poll,
};

        应用层使用poll机制,等待dht11能够读取。使用read系统调用读取dht11的数据。

linux驱动系列学习之温湿度显示(十)_第1张图片

        insmod进内核之后,显示的结果。

2. oled驱动

i2c接口的0.96寸oled在之前的博文里面已经讲解过。具体请参考博文:linux驱动系列学习之i2c子系统(四)

        这里面想说下,目前手里面只有i2c接口的oled,测试了一下写的驱动,刷新一帧挺慢的,后面考虑换成spi接口的oled,加上缓存区处理,应该能将显示效果提高不少。

三、应用

1.控制dht11

        这里给出dht11读取线程代码,只将其中一部分写出。本次poll函数只有一个dht11设备需要等待操作,这里给出5个pollfd,演示下poll等待多个文件可操作。

char* file_path = "/dev/htq-dht11";
int dht11_thread(void* p)
{

	uint16_t data[2] = { 0 };
	struct pollfd fds[5];
	int rc = 0;
	int i = 0;
	int fd = open(file_path, O_RDWR);
	printf("fd: %d\n",fd);
	fds[0].fd = fd;
	fds[0].events = POLLOUT;
	while(1){
		rc = poll(fds, 5, 3 * 1000);
		printf("rc: %d\n",rc);
		for(i = 0;i < 5;i++){
			if (fds[i].revents == POLLOUT)   //有数据可以读取
			{
				printf("i: %d  revents: %d\n",i,fds[i].revents);
				read(fds[i].fd,data,sizeof(data));
				printf("humidity: %d   temperature: %d	 \n",data[0],data[1]);
			}
		}
		
		sleep(1);
		
	}
}

2.oled显示

        这里,使用ioctl控制oled显示数据到某个位置。等待读取dht11数据,并将其显示出来。

#define OLED_PATH "/dev/oled_htq"
#define TEMP_HUMI_PATH "/dev/htq-dht11"

#define POLL_CNT 1
int show_temperature_humidity(int _oled_fd, int x, int y, double temperature, double humidity);
int oled_fd = 0, temp_humi_fd = 0;

int oled_thread(void* p)
{    
    uint16_t data[2] = { 0 };
	struct pollfd fds[POLL_CNT];  
    double temperature,  humidity;
    int i = 0,rc = 0;
    
    oled_fd = open(OLED_PATH, O_RDWR);
    if (oled_fd < 0) {
        printf("open %s", OLED_PATH);
        fflush(stdout);
        perror(" ");
        return 0;
    }
    temp_humi_fd = open(TEMP_HUMI_PATH, O_RDWR);
    if (temp_humi_fd < 0) {
        printf("open %s", TEMP_HUMI_PATH);
        fflush(stdout);
        perror(" ");
        return 0;
    }
    
    printf("oled_fd: %d\n",oled_fd);
	printf("temp_humi_fd: %d\n",temp_humi_fd);
	fds[0].fd = temp_humi_fd;
	fds[0].events = POLLOUT;
	while(1){
		rc = poll(fds, POLL_CNT, 3 * 1000);
		for(i = 0;i < POLL_CNT;i++){
			if (fds[i].revents == POLLOUT)   //有数据可以读取
			{
				//printf("i: %d  revents: %d\n",i,fds[i].revents);
				read(fds[i].fd,data,sizeof(data));
				//printf("humidity: %0.2f   temperature: %0.2f	 \n",data[0],data[1]);
                humidity = data[0];
                temperature = data[1];
                show_temperature_humidity(oled_fd,0,0,temperature,humidity);
			}
		}
		
		sleep(1);
		
	}

    close(oled_fd);
    close(temp_humi_fd);
    return 0;	
}


int show_temperature_humidity(int _oled_fd, int x, int y, double temperature, double humidity)
{
   
    uint8_t cur_x = 0, cur_y = 0;
    uint32_t data =  0; 
    char buffer[168] =  { 0 };  //最多显示168个字符
    if(_oled_fd <= 0) return -1;
    if(x > 126 || y > 6)   return -2;
    cur_x = x;
    cur_y = y;
    data = cur_y;
    data <<= 8;
    data |=  cur_x;
    ioctl(_oled_fd,13,data);   //13是自定的设置oled的显示(x,y)

    memset(buffer,0,sizeof(buffer));  
    sprintf(buffer,"temperature: %0.2f",temperature);  
    write(_oled_fd,buffer,strlen(buffer));


    cur_x = x;
    cur_y = y + 1;
    data = cur_y;
    data <<= 8;
    data |=  cur_x;
    ioctl(_oled_fd,13,data); 

    memset(buffer,0,sizeof(buffer));  
    sprintf(buffer,"humidity: %0.2f",humidity);  
    write(_oled_fd,buffer,strlen(buffer));
    return 0;
}

        运行代码,在oled上面显示温湿度数据。

3.mqtt传输

         mqtt协议应用十分广泛,本次使用onenet平台演示功能。具体怎么在onenet上搭建mqtt环境,请参考其他博主的文章。本博文使用的多协议接入这个。使用的是onenet自身提供的MQTT协议API传输数据,在demo里面,有三个变量需要根据自己搭建的环境进行修改。分别是

char* prjid = "XX1154"; //project_id

char* auth_info = "htqmqtt2"; //authoriz info

char* devid = "XX9174017";  //device_id

        这三个变量唯一确定一个设备。之后使用响应的API进行传输数据,这里,将主要的代码放下。

int mqtt_thread(void *p)  
{ 
    int ret = 0;
    int err, flags;
    int keep_alive = 1200;
    struct MqttSampleContext smpctx[1],*ctx = smpctx;
    ctx->host   = "183.230.40.39";
    ctx->port   = 6002;

    ret = MqttSample_Init(smpctx);
    printf("MqttSample_Init ret: %d\n",ret); 

    MqttBuffer_Init(ctx->mqttbuf);



    ctx->mqttfd = MqttSample_CreateTcpConnect(ctx->host, ctx->port);
    if(ctx->mqttfd < 0) {
        return -1;
    }
    ctx->mqttctx->read_func_arg = (void*)(size_t)ctx->mqttfd;
    ctx->mqttctx->writev_func_arg = (void*)(size_t)ctx->mqttfd;



    err = Mqtt_PackConnectPkt(ctx->mqttbuf, keep_alive, devid, 1,
                              NULL, NULL, 0,
                              MQTT_QOS_LEVEL0, 0, prjid,
                              auth_info, strlen(auth_info));
    printf("Mqtt_PackConnectPkt ret: %d\n",err); 

    ret = Mqtt_SendPkt(ctx->mqttctx, ctx->mqttbuf, 0); //发送数据包,将数据发送到onennet
    printf("Mqtt_SendPkt ret: %d\n",ret); 

    ret = Mqtt_RecvPkt(ctx->mqttctx);
    printf("Mqtt_RecvPkt ret: %d\n",ret); 


  
    uint32_t size = 0
    int retain = 0;
    int own = 1;
    char send_buffer[1024];
    double temperature = 21.2, humidity = 61.2;
    int i = 0;
    while(1){
        MqttBuffer_Init(ctx->mqttbuf);
        memset(send_buffer,0,sizeof(send_buffer));  

        sprintf(send_buffer,"[{\"temperature\":%0.2f,\"humidity\":%0.2f}]", temperature, humidity);



        temperature += 0.1;
        humidity += 0.2;
        size = strlen(send_buffer);
        ret = Mqtt_PackDataPointByString(ctx->mqttbuf, g_pkt_id++, 0, kTypeSimpleJsonWithoutTime, send_buffer, size, MQTT_QOS_LEVEL0, retain, own);
        printf("Mqtt_RecvPkt ret: %d\n",ret);

        ret =  Mqtt_SendPkt(ctx->mqttctx, ctx->mqttbuf, 0);
        printf("Mqtt_RecvPkt ret: %d\n",ret);
        sleep(1);
    }        



    MqttBuffer_Destroy(smpctx->mqttbuf);
    Mqtt_DestroyContext(smpctx->mqttctx);

    if(smpctx->epfd >= 0) {
        close(smpctx->epfd);
        smpctx->epfd = -1;
    }

    if(smpctx->mqttfd >= 0) {
        close(smpctx->mqttfd);
        smpctx->mqttfd = -1;
    }
 

    return 0;
}

linux驱动系列学习之温湿度显示(十)_第2张图片

 mqtt传输到onenet平台上显示的相应结果。发送的数据是随便发送的,这个是之前进行测试。

四、总结

        本项目做的功能十分简单,只是把之前学的做个简单的总结。里面也有许多不足的地方,如oled显示部分,应该使用GRAM现存,使用定时器每隔一段时间刷新一次,这样显示的效果会更好。在应用层应该使用QT开发界面。下面有时间的话,会进行一定的修改

你可能感兴趣的:(linux驱动学习,linux,驱动开发)