1 Linux+Qt驱动dht11温湿度传感器实验过程遇到的问题及解决办法-德赢Vwin官网 网
0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Linux+Qt驱动dht11温湿度传感器实验过程遇到的问题及解决办法

CHANBAEK 来源:嵌入式大本营 作者:小小飞飞哥 2023-05-23 15:54 次阅读

最近想要做一个基于嵌入式Linux+Qt驱动dht11温湿度传感器的实验。 想要实现的功能是通过野火的imx6ull开发板控制dht11传感器,然后使用Qt做一个上位机,在上位机上面把数据显示出来。

这里把我在做的过程中遇到的一些问题先记录一下,免得日后忘记。

在网上关于这方面的资料不多,大多数都是基于stm32来控制的,所以在做的过程中遇到一些问题解决起来也比较麻烦。

下面简述一下我做的过程及遇到的问题

首先查看原理图看使用到了哪个管脚,然后在设备树里添加相应的节点。 这里用到了gpio子系统和pinctrl子系统。

接着参考网上的相关代码,进行了改写,因为这个传感器的时序也比较简单,所以有关时序的部分基本上可以不用改。

遇到的第一个问题 :写好驱动后,在应用程序中使用read函数来读取设备文件,如果只读取一次,可以得到结果,但是如果使用while(1)来尝试反复读取,就会失败。

按照手册来说,只要两次读取间隔超过1秒就行了,但是我使用while(1)即使休眠sleep(3)之类的依然会在第二次读取的失败,而且整个函数会卡死在读取这里,这个进程怎么也杀不死,kill -9杀不死,kill -15 也杀不死。 这里很快把问题定位在了read函数。

后面,我在代码中做了如下修改:本来在驱动程序里面有使用while函数来等待管脚电平的跳变,我认为这样是不合理的,因为没有超时处理,容易卡死,所以我加了一个计数,当超过一定计数值时就跳出while循环。 后来这个问题就解决了。 虽然我是不确定一开始是不是因为这个原因,因为中间过了挺久的时间,我不确定有没有别的因素存在 ,总之后来就不会卡死了,可以使用while循环来反复读取。

遇到的第二个问题 :在解决了上面的问题之后,insmod安装驱动,可以工作,然后rmmod卸载驱动,再次insmod安装驱动就会发现安装不上去。

图片

使用dmesg命令查看内核打印的信息,比较容易猜到应该是卸载驱动的时候没有卸载干净,然后仔细看了一下驱动,在结合网上查找资料,发现我的驱动里没有写remove函数。 所以我添加了remove函数,在remove函数里注销掉那些东西。 而且要注意注销的顺序,和注册是相反的,比如在驱动中最先是申请设备号,在注销的时候就是最后注销它,否则会出现很多错误,包括段错误

遇到的第三个问题 :在解决了第二个问题之后,已经可以反复卸载和安装驱动了,但是发现一个问题,就是在第二次安装的时候,总是会出现gpio_request失败,按道理讲我已经在remove函数里使用gpio_free释放掉了,不应该会失败才对,后来发现是在gpio_request的时候还没拿到引脚号, 全局变量没有初始化默认是0,所以request的是0,后来通过一个函数(忘记叫什么了,总之是gpio子系统的那些函数)从设备树中拿到引脚号,这个引脚号是2,所以后面free的是2,也就是说request和free的不是同一个引脚,当然会出错了。

这属于粗心的错,把这个问题解决了之后,这个驱动总算可以正常工作了,也完全可以反复卸载和安装。

遇到的第四个问题 :在第一个问题里提到我在while里加了超时处理,防止一直死等卡死。 最开始我是这样写的

while(gpio_get_value(gpio)==0 &&cnt<6)< span="">
{
       cnt++;
       udelay(10);
}

这里通过cnt来防止while死掉,也就是说最多等待60微秒就退出循环。 但是直觉告诉我这样不好 ,因为中间延时10个微秒太长了,导致响应性不好。 所以我改成了这样:

while(gpio_get_value(gpio)==0 &&cnt<60)< span="">
{
       cnt++;
       udelay(1);
}

这样的实时响应性好多了,测出的数据也更准确了。

到这里为止,驱动就基本没有问题了,使用应用程序来读取设备文件,也基本没问题,就是有时数据校验会失败,但是测出的数据基本可以,而且是有变化的,说明还是比较可靠的。

接下来是把在Qt里把数据读出来并且显示, 下面说一下调试Qt遇到的问题

在写完驱动之后,很自然会写一个.c的测试程序,用来验证驱动是否能正常工作,很幸运,一下子就成功了,于是我认为在Qt中也是类似,直接用Qt里的read相关的函数去读取设备文件就好了,但是没想到在这个环节卡了我最久

起初,我使用Qfile 里的readAll方法去读,发现控制台会刷屏(刷屏就是驱动中的read一直被调用而打印出的信息刷屏),一读就停不下来,而且后面的程序也执行不了,也就是说函数没有返回。

我不太清楚是什么原因,只能换一个函数,接着我尝试了readLine方法,一样刷屏,接着尝试read方法,这个方法和C语言的read类似,参数里要填读几个字节,这和前面两个不太一样,所以我想,这回应该不会刷屏了吧。

结果确实没有刷屏,但是读取的数据是错的,体现出来的就是从机无响应(这时我还没有注意这个问题)。

虽然说数据是错的,但是好歹没有刷屏了,只要再想一想为什么会读出错的数据就行了。

我想到Qt里还有一种读文件的方式,就是使用数据流Datastream,但是效果和上面的read一样。

接着我开始思考刷屏的原因,百度了一下,有人说要在末尾加一个"\\0",尝试,未果。

接着,我在一些技术交流群寻求帮助,因为此刻我的问题确实很奇怪,在自己写的.c测试程序里,调用read读设备文件是完全没有问题的,现在唯一的区别就是在Qt中读,驱动又不变,为什么读出来的是错的呢? 我怀疑是Qt的read对数据的解析可能和C语言里不太一样,因为此刻是有数据的, 会不会是因为字节对齐之类的原因导致解析数据不对呢 ? 群里大佬建议先排查一下源数据对不对。

于是我拿出了我许久没用过的逻辑分析仪来分析波形,我先观察了我的.c测试程序的波形,和手册描述的基本一致。 接着观察Qt里read时的波形,一观察发现根本没有波形,正常情况应该是主机先拉低18ms,再拉高,等待从机应答。 而我观察到的波形是主机拉低了30多ms才拉高,再看一下终端打印的数据, 发现驱动里的read被调用了两次

这时,我已经猜到原因了,**之所以数据不对,是因为驱动里的read被连续调用了两次,导致时序根本就不对,从机没有应答。 **

再观察之前使用readAll函数来读取,虽然会刷屏,但是偶尔能捕捉到有效的波形。 这已经很能说明问题了,就是要解决驱动里的read为什么会被调用多次这个问题,正常应该是应用层调用一次read,驱动里的read就被调用一次。

关于这个问题,这篇文章讲的不错,使用cat读取和echo写内核文件节点的一些问题

这篇文章对我还是有很大的启发。 总之就是驱动中read 的返回值会影响它是否被多次调用。

先来看一下驱动中read函数的参数和返回值

ssize_t dht11_chr_dev_read(struct file *filp, char __user *buf, size_t count, loff_t *fops)

我经过很多实验,发现以下规律:

对于Qt中的readAll、readLine函数,不管驱动返回什么,readAll都会刷屏,readLine会调用驱动多次。

对于Qt中的read函数,如果驱动返回的是count,将不会刷屏,否则,也会刷屏。 (这一点确实很奇怪)

更奇怪的是同样的实验条件,在多次实验中甚至可能得到不同的结果,但是上面这几点结论是反复实验得到的结论。

最后,我发现可以在Qt中使用C和C++混合编程,方法就是使用

extern "C"{
#include    //这里写用到的C头文件
}

然后在用到的C语言的函数前加两个冒号,比如

::read(fd,buf,sizeof(buf));

这样就可以直接调用C语言代码了,而且发现效果还不错,比Qt中的read系列函数稳定。 (实验次数有限,从我观察到的结果来看是这样)。

所以,最终的解决方法就是:

方法一 :使用Qfile 的read函数,使用方法和C语言类似,可以正确读出数据,但是要注意,如果使用这个函数,驱动中的read要返回参数列表中的count,否则会刷屏。

方法二 :直接使用混合编程的方式,调用C语言中的read ,这样测出的效果是最好的,而且不必要求驱动中的read 返回count,直接返回实际读取的字节即可,也就是copy_to_user的字节数。

驱动代码参考了Linux下DHT11驱动编程,以及测试程序

在此基础上修改得到

#include 
#include 
#include 
#include 
#include 


#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


#include 








/*------------------字符设备内容----------------------*/
#define DEV_NAME            "dht11"
#define DEV_CNT                 (1)


typedef struct
{
  uint8_t  humi_int;   //湿度的整数部分
  uint8_t  humi_deci;   //湿度的小数部分
  uint8_t  temp_int;   //温度的整数部分
  uint8_t  temp_deci;   //温度的小数部分
  uint8_t  check_sum;   //校验和





} DHT11_Data_TypeDef;




//定义字符设备的设备号
static dev_t dht11_devno;
//定义字符设备结构体chr_dev
static struct cdev dht11_chr_dev;




struct class *class_dht11;  //保存创建的类
struct device *device;      // 保存创建的设备
struct device_node  *dht11_device_node; //dht11的设备树节点


int dht11_data_pin;         // 保存获取得到的dht11引脚编号


DHT11_Data_TypeDef DHT11_Data;


//从DHT11读取1byte数据,MSB先行
uint8_t DHT11_ReadByte(void)
{
  uint8_t i, temp=0;

    int cnt=0;
  for(i=0;i<8;i++)    
  {   
    /*每bit以50us低电平标置开始,轮询直到从机发出 的50us 低电平 结束*/
    while(gpio_get_value(dht11_data_pin) == 0  && cnt<60)
        {
            cnt++;
            udelay(1);
        }
        cnt =0;
    /*DHT11 以26~28us的高电平表示“0”,以70us高电平表示“1”,
     *通过检测 x us后的电平即可区别这两个状 ,x 即下面的延时 
     */
    udelay(40); //延时x us 这个延时需要大于数据0持续的时间即可         

    if(gpio_get_value(dht11_data_pin))/* x us后仍为高电平表示数据“1” */
    {
      /* 等待数据1的高电平结束 */
      while(gpio_get_value(dht11_data_pin) && cnt<50)
            {
                cnt++;
                udelay(1);
            }

      temp|=(uint8_t)(0x01<<(7-i));  //把第7-i位置1,MSB先行 
    }
    else   // x us后为低电平表示数据“0”
    {         
      temp&=(uint8_t)~(0x01<<(7-i)); //把第7-i位置0,MSB先行
    }
  }


  return temp;

}


/**
 * 一次完整的数据传输为40bit,高位先出
 * 8bit 湿度整数 + 8bit 湿度小数 + 8bit 温度整数 + 8bit 温度小数 + 8bit 校验和的末8位
 */
uint8_t DHT11_Read_TempAndHumidity(DHT11_Data_TypeDef *DHT11_Data)
{
  //  int ret;
    int cnt=0;

    printk(KERN_ERR"DHT11_Read_TempAndHumidity 被调用\\n");

  /*主机拉低*/
  gpio_direction_output(dht11_data_pin, 0);

  /*延时18ms,(>=18ms)*/
  mdelay(18);

  /*总线拉高 主机延时30us*/
  gpio_direction_output(dht11_data_pin, 1);

  udelay(30);   //延时30us,(20~40us)

  /*主机设为输入 判断从机响应信号*/ 
  gpio_direction_input(dht11_data_pin);

  /*判断从机是否有低电平响应信号 如不响应则跳出,响应则向下运行*/   
  if(gpio_get_value(dht11_data_pin) == 0)     
  {
    /*轮询直到从机发出 的80us 低电平 响应信号结束*/  
    while(gpio_get_value(dht11_data_pin) == 0 && cnt<100)
        {
            cnt++;
            udelay(1);
        }
        cnt = 0;
    /*轮询直到从机发出的 80us 高电平 标置信号结束*/
    while(gpio_get_value(dht11_data_pin) && cnt<100)
        {
            cnt++;
            udelay(1);
        }

    /*开始接收数据*/   
    DHT11_Data->humi_int= DHT11_ReadByte();

    DHT11_Data->humi_deci= DHT11_ReadByte();

    DHT11_Data->temp_int= DHT11_ReadByte();

    DHT11_Data->temp_deci= DHT11_ReadByte();

    DHT11_Data->check_sum= DHT11_ReadByte();




    /*读取结束,引脚改为输出模式,主机拉高*/
    gpio_direction_output(dht11_data_pin, 1);


        printk("humi: %d.%d, temp: %d.%d,check:%d\\n",DHT11_Data->humi_int,\\
                    DHT11_Data->humi_deci,DHT11_Data->temp_int,DHT11_Data->temp_deci,DHT11_Data->check_sum);
    /*检查读取的数据是否正确*/
    //DHT11_Data->check_sum的正确的结果是温湿度总和的末8位,结构体也有定义check_sum为uint8_t类型
    if(DHT11_Data->check_sum == DHT11_Data->humi_int + DHT11_Data->humi_deci + DHT11_Data->temp_int+ DHT11_Data->temp_deci)
      return 0;
    else {
       printk(KERN_ERR " ERROR 数据校验失败");
       return -1;
    }

  }
  else
    {
        printk(KERN_ERR "ERROR 从机无响应");
        return -1;
    }


}




/*字符设备操作函数集,open函数*/
static int dht11_chr_dev_open(struct inode *inode, struct file *filp)
{
  printk("\\n open form driver \\n");
    return 0;
}


/*字符设备操作函数集,write函数*/
static ssize_t dht11_chr_dev_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{



  unsigned char write_data; //用于保存接收到的数据


  int error = copy_from_user(&write_data, buf, cnt);
  if(error < 0) {
    return -1;
  }


  return 0;
}




ssize_t dht11_chr_dev_read(struct file *filp, char __user *buf, size_t count, loff_t *fops)
{




  int size=sizeof(DHT11_Data_TypeDef);
  printk(KERN_ERR " count: %d, fops: %lld\\n", count, *fops);

  printk(KERN_ERR "--------%s---------\\n",__func__);

    /*调用DHT11_Read_TempAndHumidity读取温湿度,若成功则输出该信息*/
  if( DHT11_Read_TempAndHumidity ( & DHT11_Data ) != 0)
  {
    printk(KERN_ERR "Read DHT11 ERROR!\\r\\n");
  }
  else
  {
    if(copy_to_user(buf, &DHT11_Data, size)!=0)
    {
      printk(KERN_ERR " 拷贝失败\\n");
//        return 0;
    }
    else
      printk(KERN_ERR " 拷贝成功\\n");
  }

    // ret= simple_read_from_buffer(buf, count, fops, &DHT11_Data, sizeof(DHT11_Data_TypeDef));
  //   *fops=0;

  return count;
//    return size;
}






/*字符设备操作函数集*/
static struct file_operations  dht11_chr_dev_fops = 
{
  .owner = THIS_MODULE,
    .open = dht11_chr_dev_open,
  .write = dht11_chr_dev_write,
  .read = dht11_chr_dev_read,
};






/*----------------平台驱动函数集-----------------*/
static int dht11_probe(struct platform_device *pdv)
{

  int ret = 0;  //用于保存申请设备号的结果

  printk(KERN_EMERG "\\t  match successed  \\n");

    /*获取dht11的设备树节点*/
    dht11_device_node = of_find_node_by_path("/dht11");
    if(dht11_device_node == NULL)
    {
        printk(KERN_EMERG "\\t  get dht11 failed!  \\n");
    }


    dht11_data_pin = of_get_named_gpio(dht11_device_node, "dht11_data_pin", 0);



    printk("dht11_data_pin = %d\\n ", dht11_data_pin);


  ret=gpio_request(dht11_data_pin, "DQ_OUT");
    if(ret==0)
    {
        printk(KERN_ERR "gpio request success\\n");
    }
    else
    {
        printk(KERN_ERR "gpio request failed \\n");

    }




    gpio_direction_output(dht11_data_pin, 1);





  /*---------------------注册 字符设备部分-----------------*/


  //第一步
    //采用动态分配的方式,获取设备编号,次设备号为0,
    //设备名称为rgb-leds,可通过命令cat  /proc/devices查看
    //DEV_CNT为1,当前只申请一个设备编号
    ret = alloc_chrdev_region(&dht11_devno, 0, DEV_CNT, DEV_NAME);
    if(ret < 0){
        printk("fail to alloc dht11_devno\\n");
        goto alloc_err;
    }
    //第二步
    //关联字符设备结构体cdev与文件操作结构体file_operations
  dht11_chr_dev.owner = THIS_MODULE;
    cdev_init(&dht11_chr_dev, &dht11_chr_dev_fops);
    //第三步
    //添加设备至cdev_map散列表中
    ret = cdev_add(&dht11_chr_dev, dht11_devno, DEV_CNT);
    if(ret < 0)
    {
        printk(KERN_ERR"fail to add cdev\\n");
        goto add_err;
    }


  //第四步
  /*创建类 */
  class_dht11 = class_create(THIS_MODULE, DEV_NAME);
    if(class_dht11==NULL)
    {
        printk(KERN_ERR"class creat failed\\n");
        goto add_class;
    }
  /*创建设备*/
  device = device_create(class_dht11, NULL, dht11_devno, NULL, DEV_NAME);
    if(device==NULL)
    {
        printk(KERN_ERR"device creat failed\\n");
        goto add_device;
    }
  return 0;


 //   device_destroy(class_dht11,dht11_devno);
add_device:
    class_destroy(class_dht11);
    printk(KERN_EMERG "\\t  删除类成功  \\n");
add_class:
    cdev_del(&dht11_chr_dev);
    printk(KERN_EMERG "\\t  删除设备成功  \\n");


add_err:
    //添加设备失败时,需要注销设备号
    unregister_chrdev_region(dht11_devno, DEV_CNT);
  printk(KERN_EMERG"\\n 注销设备号成功! \\n");
alloc_err:


  return -1;


}


int  dht11_remove(struct platform_device *dht11_dev)
{
    printk(KERN_EMERG"开始释放资源");
    gpio_free(dht11_data_pin);
    device_destroy(class_dht11,dht11_devno);
    class_destroy(class_dht11);
    cdev_del(&dht11_chr_dev);
    unregister_chrdev_region(dht11_devno, DEV_CNT);
    printk(KERN_EMERG"释放资源完毕");
    return 0;
}




static const struct of_device_id dht11[] = {
{ .compatible = "dht11"},
  { /* sentinel */ }
};


/*定义平台设备结构体*/
struct platform_driver dht11_platform_driver = {
  .probe = dht11_probe,
    .remove = dht11_remove,
  .driver = {
    .name = "dht11-platform",
    .owner = THIS_MODULE,
    .of_match_table = dht11,
  }
};






/*
*驱动初始化函数
*/
static int __init dht11_platform_driver_init(void)
{
  int DriverState;

  DriverState = platform_driver_register(&dht11_platform_driver);

  printk(KERN_EMERG "\\tDriverState is %d\\n",DriverState);
  return 0;
}




/*
*驱动注销函数
*/
static void __exit led_platform_driver_exit(void)
{


  printk(KERN_EMERG "dht11 module exit!\\n");


  platform_driver_unregister(&dht11_platform_driver);  
}




module_init(dht11_platform_driver_init);
module_exit(led_platform_driver_exit);


MODULE_LICENSE("GPL");
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表德赢Vwin官网 网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 嵌入式
    +关注

    关注

    5082

    文章

    19104

    浏览量

    304798
  • Linux
    +关注

    关注

    87

    文章

    11292

    浏览量

    209323
  • STM32
    +关注

    关注

    2270

    文章

    10895

    浏览量

    355729
  • 温湿度传感器

    关注

    5

    文章

    579

    浏览量

    35706
  • Qt
    Qt
    +关注

    关注

    1

    文章

    302

    浏览量

    37899
收藏 人收藏

    评论

    相关推荐

    基于arduino的dht11温湿度传感器的使用

    本文介绍了DHT11温湿度传感器电气特性、DHT11封装形式及接口说明与典型应用电路,其次介绍了DHT11
    发表于 01-22 15:50 4.5w次阅读
    基于arduino的<b class='flag-5'>dht11</b><b class='flag-5'>温湿度</b><b class='flag-5'>传感器</b>的使用

    DHT11数字式温湿度传感器的应用性研究

    基于DHT11温湿度传感器具有测量精度高、响应速度快、抗干扰能力强等优点。通过介绍DHT11数字式温湿度
    发表于 11-06 16:28

    【众拳】基于STM8的DHT11温湿度传感器实验 附代码

    ~50us)时间,作为响应信号,然后DHT11拉高数据线,保持t4(40~50us)时间后,开始输出数据。DHT11 温湿度传感器时序图【众拳】剑齿虎STM8开发板的
    发表于 12-07 09:14

    DHT11温湿度传感器介绍

    DHT11温湿度传感器介绍,1.实物原理图2.模块说明2.1 DHT11产品概述DHT11数字温湿度
    发表于 07-21 09:04

    DHT11温湿度传感器

    DHT11温湿度传感器1. DHT11简介2. 硬件设计3. 软件设计3.1 STM32CubeMX设置3.2 MDK-ARM编程4. 下载验证
    发表于 08-11 06:04

    DHT11数字温湿度传感器的数据通讯过程是怎样的

    DHT11数字温湿度传感器是什么?有何优点呢?DHT11数字温湿度传感器的数据通讯
    发表于 01-18 06:07

    DHT11温湿度传感器简介

    DHT11温湿度传感器1、DHT11简介DHT11数字温湿度
    发表于 02-16 06:55

    DHT11数字温湿度传感器的相关资料推荐

    STM32采集DHT11温湿度关于DHT11相关参数代码篇接线和实验结果总结关于DHT11DHT11是一款数字
    发表于 02-21 07:34

    DHT11数字温湿度传感器产品介绍

    DHT11数字温湿度传感器产品介绍 DHT11数字温湿度传感器是一款含有已校准数字信号输出的
    发表于 02-26 17:19 256次下载

    DHT11数字式温湿度传感器的应用性研究

    基于DHT11温湿度传感器具有测量精度高、响应速度快、抗干扰能力强等优点。通过介绍DHT11数字式温湿度
    发表于 07-25 17:03 216次下载
    <b class='flag-5'>DHT11</b>数字式<b class='flag-5'>温湿度</b><b class='flag-5'>传感器</b>的应用性研究

    数字温湿度传感器 DHT11

    数字温湿度传感器 DHT11 ►相对湿度和温度测量 ►全部校准,数字输出 ►卓越的长期稳定性 ►无需额外部件 ►超长的信号传输距离 ►超低能耗 ►4 引脚安装 ►完全互换
    发表于 12-02 11:06 4次下载

    温湿度传感器DHT11驱动程序

    本文开始介绍了驱动程序的定义与驱动程序的作用,其次介绍了DHT11温湿度传感器特性、引脚说明与封装详情,最后介绍了
    发表于 01-22 16:52 5.3w次阅读
    <b class='flag-5'>温湿度</b><b class='flag-5'>传感器</b><b class='flag-5'>DHT11</b><b class='flag-5'>驱动</b>程序

    Arduino的实验例程之温湿度传感器DHT11实验

    本文档的主要内容详细介绍的是Arduino的实验例程之温湿度传感器DHT11实验免费下载。
    发表于 03-01 11:42 30次下载

    温湿度传感器DHT11的STM32驱动实现

    温湿度传感器DHT11的STM32驱动实现
    发表于 11-25 20:36 76次下载
    <b class='flag-5'>温湿度</b><b class='flag-5'>传感器</b><b class='flag-5'>DHT11</b>的STM32<b class='flag-5'>驱动</b>实现

    使用ESP8266驱动DHT11温湿度传感器

    DHT11数字温湿度传感器是一款含有已校准数字信号输出的温湿度综合传感器,在Arduino提高篇中已对其进行了介绍,本篇使用ESP8266来
    的头像 发表于 05-19 14:20 6378次阅读
    使用ESP8266<b class='flag-5'>驱动</b><b class='flag-5'>DHT11</b><b class='flag-5'>温湿度</b><b class='flag-5'>传感器</b>