FPGA—DHT11数字温湿度传感器

目录

1. 理论学习

2.实操

2.1 顶层模块

2.1.1 整体模块框图

2.1.2 顶层代码

2.2 DHT11 控制模块

2.2.1 模块框图

2.2.2 状态转换图绘制

2.2.3 波形图绘制

2.2.4 RTL代码

2.3 上板验证

3. 总结

1. 理论学习

DHT11简介

       DHT11数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器。它应用专用的数字模块采集技术和温湿度传感技术,确保产品具有 具有成本低、抗干扰力强、长期稳定等优点。

 实物图

     如上图为DHT11的实物图,有四个引脚,各引脚说明如下表格所示:

      DHT11的性能参数:

      湿度的量程为 5%RH—95%RH,误差为±5%RH。温度的量程为-20℃—60℃,误差为±2℃。需要注意的是采样周期要大于2S/次,也就是要大于 2S 才能读取一次温湿度。

   DHT11通信方式

1.单总线说明

      DHT11器件采用简化的单总线通信。单总线即只有一根数据线,系统中的数据交换、控制均由单总线完成。设备(主机或从机)通过一个漏极开路或三态端口连至该数据线,以允许设备在不发送数据时能够释放总线。,FPGA与传感器是主从结构只有主机呼叫从机时,从机才能应答,因此主机访问器件都必须严格遵循单总线序列。

2.单总线传送数据位定义

      主机与 DHT11 之间的通讯和同步采用单总线数据格式,一次传送 40 位数据,高位先出

       数据格式为: 8bit 湿度整数数据+8bit 湿度小数数据+8bit 温度整数数据+8bit 温度小数数据+8bit 校验位。(其中湿度小数部分为 0)

       校验位数据定义: “8bit 湿度整数数据+8bit 湿度小数数据+8bit 温度整数数据+8bit 温度小数数据”8bit 校验位等于所得结果的末 8 位。

        如上图所示为各个信号的定义图。首先湿度的小数部分为 0,所以输出的湿度为整数。而温度数据是可以显示小数部分的,如:接收到的 40位数据为: 00110101(湿度高八位) +00000000(湿度低八位) +00011000(温度高八位)+00000100(温度低八位) +010110001(校验位)。通过计算: 00110101+00000000+00011000+00000100=01010001 与校验位相同,结果正确。所以其湿度温度分别为:
湿度=整数+小数=00110101+0=35H=53.0%RH
温度=整数+小数=00011000+00000100=18H+04H=24℃+0.4℃=24.4℃。

      需注意湿度是没有负数的,但是温度有。温度的高八位与低八位的最高位都为符号位,不计入校验和中。经实际的测量发现,目前 DHT11 温度只能精确到0.1℃,我们应用时用小数数据的低四位来表示温度的小数值即可。

3. 数据时序图

       主机发送一次开始信号后, DHT11 从低功耗模式转换到高速模式,待主机发送的开始信号结束后, DHT11 发送响应信号,送出 40bit 的数据。信号发送如下图所示:

主机读取 DHT11 温湿度数据的步骤:

step1: DHT11准备状态

     DHT11 上电后(DHT11 上电后要等待 1S 以越过不稳定状态在此期间不能发送任何指令),测试环境温湿度数据,并记录数据,同时 DHT11 的 DATA 数据线由上拉电阻拉高一直保持高电平;此时 DHT11 的单总线引脚处于输入状态,时刻检测外部信号。

step2: 主机发送开始信号,传感器发出应答信号

      主机发送开始信号输出低电平,持续时间不小于 18ms 且不超过 30ms。发送完之后释放总线等待 DHT11 应答。起始信号时序图如下。

      DHT11 的 DATA 引脚检测到外部信号有低电平时,等待外部信号低电平结束,延迟后DHT11 的 DATA 引脚输出 83us 的低电平作为应答信号,紧接着输出 87us 的高电平通知外设准备接收数据。DHT11 响应信号时序图如下。

step3: 传送数据位

40位数据位:

       由 DHT11 的 DATA 引脚输出 40 位数据, 主机根据 I/O 电平的变化接收 40 位数据,位数据 “0” 的格式为: 54us 的低电平后 23~27us 的高电平,位数据 “1” 的格式为: 54us 的低电平后68~74us 的高电平。位数据“0”、 “1”格式信号如下图所示:

结束信号:
        DHT11 的 DATA 引脚输出 40 位数据后,继续输出低电平 54us 后转为输入状态,由上拉电阻变为高电平,又开始再一次检测,等待开始信号的到来。

2.实操

       实验目标:控制DHT11,读出湿度温度数据并显示在数码管中,通过按键使湿度和温度在数码管中切换显示。

硬件资源:

      需使用到开发板上的按键、数码管及 DHT11 接口,其中按键和数码管硬件资源之前说过不再详细说明。

 DHT11 接口

 DHT11 接口原理图

      其接线图和原理图如上图所示,该接口是单总线接口,之前也接过DS18B20(温度传感器),两者相似。

2.1 顶层模块

2.1.1 整体模块框图

 DHT11 温湿度显示实验整体框图

       通过上图可以看到,该工程共分四个模块。各模块简介见下表。

      其中按键消抖模块和数码管动态显示模块已经有学习过,直接调用其模块。重点是学习DHT11 控制模块。

2.1.2 顶层代码

`timescale  1ns/1ns

module  dht11
(
    input   wire    sys_clk     ,  
    input   wire    sys_rst_n   ,  
    input   wire    key_in      ,   //按键信号

    inout   wire    dht11       ,   //数据总线
    output  wire    [5:0]   sel,    //数码管位选信号
    output  wire    [7:0]   seg    //数码管段选信号
    );

wire    [19:0]  data_out;   //需要显示的数据
wire            key_flag;   //按键消抖后输出信号
wire            sign    ;   //输出符号

dht11_ctrl  dht11_ctrl_inst
(
    .sys_clk     (sys_clk  ),   
    .sys_rst_n   (sys_rst_n),   
    .key_flag    (key_flag ),   

    .dht11       (dht11    ),   

    .data_out    (data_out ),   
    .sign        (sign     )    
);

key_filter  key_filter_inst
(
    .sys_clk      (sys_clk  )   , 
    .sys_rst_n    (sys_rst_n)   , 
    .key_in       (key_in   )   , 

    .key_flag     (key_flag )     

);

seg_dynamic seg_dynamic_inst
(
    .sys_clk     (sys_clk  ),
    .sys_rst_n   (sys_rst_n),
    .data        (data_out ), //数码管要显示的值
    .point       (6'b000010), //小数点显示,高电平有效
    .seg_en      (1'b1     ), //数码管使能信号,高电平有效
    .sign        (sign     ), //符号位,高电平显示负号

    .sel         (sel      ),   //数码管位选信号
    .seg         (seg      )    //数码管段选信号

);
endmodule

2.2 DHT11 控制模块

2.2.1 模块框图

      模块作用是将实时的环境湿度,温度转换出来,然后将转换出来的温湿度读出来,通过按键切换显示在数码管上。

2.2.2 状态转换图绘制

       根据 DHT11 的数据时序图可以画出状态跳转图来进一步了解如何控制 DHT11,以及读出 DHT11 的湿度温度值。

         各个状态的时间以下表提供的范围进行设计。

2.2.3 波形图绘制

1. 产生单位时钟为 1us 的时钟

      在整个时序过程中无论是发送开始信号,还是“1”,“0”的时序组成我们都需要用到时间信号,而这些时间信号的最小单位为 us,所以我们先产生单位时钟为 1us 的时钟来作为 DHT11 的控制时钟。如下波形图所示,产生方法为分频法。

2.  提取dht11信号的上升沿和下降沿

      直接对dht11信号延时两拍。

3. 处理dht11信号

       dht11:单总线,其类型为输入输出型,定义为 wire 型变量,所以我们将借助dht11_out(总线输入)和 dht11_en(总线使能信号)来给 dht11 赋值。由时序图可知,只需要控制总线发送开始信号( 18ms 的低电平),其他状态释放即可。如图中的dht11_out 与 dht11_en 信号所示,当 dht11_en 信号为 1 时 dht11 的值等于 dht11_out 的值(输出),当 dht11_en 为 0 时释放总线(输入)。 

4. 各个状态间的跳转 

       state:状态机的状态。初始状态为 S_WAIT_1S,上电后需要等待 1S 以越过不稳定状态。所以我们需要一个计数器(cnt_us)去进行产生 1S 的时间信号。当计数器计到 1S 时跳到下一个状态。
       S_LOW_18MS:发送开始信号状态。开始信号是由 18ms 的低电平组成,又需要用到计器,而且在发送时需要从 0 开始计数。当计数器计到 18ms 时(0-17999),状态机跳转计数器清零。
       S_DLY1:拉高等待状态。等待 10us 后跳转到下一状态,等待回应信号的到来。

       S_REPLY: DHT11 回应状态(DHT11 响应低电平 81~85us)。跳到这个状态后计数器开始持续计数,但是此无法判断有没有此时回应信号开始发送。需要一个新的计数器( cnt_low)进行对响应信号计数,当检测到 dht11 为低电平时,说明响应信号开始发送了,我们就可以让 cnt_low 计数器开始计数。同时 dht11 的上升沿到来时说明响应信号发送完毕。响应这个值协议 规定为81~85us是可以视情况而设,将范围改大如 70us 可使有些没有严格按照协议发送的 DHT11 也可以兼容。

      若在这个状态一直没有检测到响应信号,则在 cnt_us 计到 1ms 时跳回S_LOW_18MS 状态,重新发送开始信号(这里应该是要满足最小测量时间2s左右的特点),波形图如上图所示。

      S_DLY2: DHT11 拉高等待输出状态。当下降沿到来时,若此时的值满足其发送时间则跳转到下一状态。

5. 数据处理
      S_RD_DATA: DHT11 输出数据状态。从时序图可以看到,数据“ 0”和数据“1”的时序都是由低电平开始的,而且持续时间相同,不同的地方在于高电平的持续时间。所以只需要判断高电平的持续时间即可判断其输出的是 0 还是 1。当上升沿到来时计数器清零开始计数,当下降沿到来时说明 1bit 的数据发送完毕,此时对计数器采样。我们知道数据“0”是维持 23-27us 的高电平,数据“1”是维持 68-74us 的高电平。我们取一个中间值(50us)去判断输出的是“0”还是“1”,若下降沿到来 cnt_us 小于等于 50 则判断为0,若大于 50 则判断为 1。

      BIT_CNT:判断传输了采了几个数据。其初始值为 0,当下降沿到来时 bit_cnt 加 1,当其加到 39 时说明发送到了最后一个数据。所以当其为 40 的时候表示数据发送完毕,同时 dht11 会发送结束信号(拉低一段时间)。当 dht11 上升沿到来时表示整个时序结束。状态跳回 S_LOW_18MS状态发送开始信号开始新一轮的数据读取,同时 bit_cnt 清零。

        上图为数据采集波形图。当在 S_RD_DATA 状态时,每来一个下降沿,采集一次数据。因为数据是从高位开始发送的 ,所以第一个采集的数据放在数据寄存器(data_tmp)的最高位即 data_tmp[39],依次类推最后一位放在最低位即 data_tmp[0]。可以看到下降沿状态到来时 bit_cnt 的值应为上一状态的值,所以 bit_cnt 与寄存器赋值位数的关系为 data_tmp[39-bit_cnt]。当 40 位数据发送完之后发现检验位与“ 8bit 湿度整数数据+8bit 湿度小数数据+8bit 温度整数数据+8bit 温度小数数据”所得结果的末 8 位相等时,则将湿度温度数据值赋给 data。

6.数据切换

      当按键按下时, data_flag 取反为 1,此时显示的是温度值,温度小数部分用低四位计算即可;按键没有按下data_flag 为 0,此时显示的是湿度值,无小数部分,显示在第二第三个数码管,而第二三个数码管显示的是十分位和百分位,所以读出的整数值要乘以 10。

      需要注意的是应使用系统时钟对 key_flag 进行采样,而不能使用 clk_1us,因为我们产生 key_flag 用的是系统时钟,而且 key_flag只维持了一个系统时钟的高电平,所以如果用分频的时钟去采的话是几乎采不到这个信号的。

2.2.4 RTL代码

`timescale  1ns/1ns

module  dht11_ctrl
(
    input   wire        sys_clk     ,   
    input   wire        sys_rst_n   ,   
    input   wire        key_flag    ,   //按键消抖后标志信号

    inout   wire        dht11       ,   //控制总线,采用wire型

    output  reg [19:0]  data_out    ,   //输出显示的数据
    output  reg         sign            //输出符号位,高电平显示负号

);

parameter   S_WAIT_1S  = 3'd1   ,   //上电等待1s状态
            S_LOW_18MS = 3'd2   ,   //主机拉低18ms,发送开始信号状态
            S_DLY1     = 3'd3   ,   //等待20-40us状态
            S_REPLY    = 3'd4   ,   //DHT11响应80us状态
            S_DLY2     = 3'd5   ,   //拉高等待80us状态
            S_RD_DATA  = 3'd6   ;   //接收数据状态

parameter   T_1S_DATA    = 999999 ; //1s时间计数值
parameter   T_18MS_DATA  = 17999  ; //18ms时间计数值

reg         clk_1us     ;   //1us时钟,用于驱动整个模块
reg [4:0]   cnt         ;   //时钟分频计数器
reg [2:0]   state       ;   //状态机状态
reg [20:0]  cnt_us      ;   //us计数器
reg         dht11_out   ;   //总线输出数据
reg         dht11_en    ;   //总线输出使能信号
reg [5:0]   bit_cnt     ;   //字节计数器
reg [39:0]  data_tmp    ;   //读出数据寄存器
reg         data_flag   ;   //数据切换标志信号
reg         dht11_d1    ;   //总线信号打一拍
reg         dht11_d2    ;   //总线信号打两拍
reg [31:0]  data        ;   //校验位数据
reg [6:0]   cnt_low     ;   //低电平计数器

wire            dht11_fall; //总线下降沿
wire            dht11_rise; //总线上升沿

//cnt:分频计数器
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        cnt <=  5'b0;
    else    if(cnt == 5'd24)
        cnt <=  5'b0;
    else
        cnt <=  cnt + 1'b1;
//clk_1us:产生单位时钟为1us的控制时钟
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        clk_1us <=  1'b0;
    else    if(cnt == 5'd24)
        clk_1us <=  ~clk_1us;
    else
        clk_1us <=  clk_1us;
		
//检测总线信号的上升沿下降沿
assign  dht11_rise =   (~dht11_d2) & (dht11_d1)    ;
assign  dht11_fall =   (dht11_d2)  & (~dht11_d1)   ;
//对dht11信号打拍
always@(posedge clk_1us or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        begin
            dht11_d1  <=  1'b0 ;
            dht11_d2  <=  1'b0 ;
        end
    else
        begin
            dht11_d1  <=  dht11    ;
            dht11_d2  <=  dht11_d1 ;
        end
		
//当使能信号为1是总线的值为dht11_out的值,为0时值为高阻态(释放总线)
assign  dht11  =   (dht11_en == 1 ) ? dht11_out : 1'bz;

//状态机状态跳转
always@(posedge clk_1us or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        state   <=  S_WAIT_1S   ;
    else
        case(state)
        S_WAIT_1S:
            if(cnt_us == T_1S_DATA) //上电1s后跳入起始状态
                state   <=  S_LOW_18MS  ;
            else
                state   <=  S_WAIT_1S   ;
        S_LOW_18MS:
            if(cnt_us == T_18MS_DATA)
                state   <=  S_DLY1     ;
            else
                state   <=  S_LOW_18MS  ;
        S_DLY1:
            if(cnt_us == 10)    //等待10us后进入下一状态
                state   <=  S_REPLY     ;
            else
                state   <=  S_DLY1     ;
        S_REPLY:  //上升沿到来且低电平保持时间大于70us,则跳转到下一状态
            if(dht11_rise == 1'b1 && cnt_low >= 70)
                state   <=  S_DLY2     ;
                 //若1ms后,dht11还没响应,则回去继续发送起始信号
            else    if(cnt_us >= 1000)
                state   <=  S_LOW_18MS ;
            else
                state   <=  S_REPLY    ;
        S_DLY2: //下降沿到来且计数器值大于70us,则跳转到下一状态
            if(dht11_fall == 1'b1 && cnt_us >= 70)
                state   <=  S_RD_DATA   ;
            else
                state       <=  S_DLY2  ;
        S_RD_DATA:  //读完数据后,回到起始状态
            if(bit_cnt == 40 && dht11_rise == 1'b1)  //结束位
                state   <=  S_LOW_18MS  ;
            else
                state   <=  S_RD_DATA   ;
        default:
                state   <=  S_WAIT_1S   ;
        endcase

//各状态下的计数器赋值
//cnt_us:每到一个新的状态就让该计数器重新计数
always@(posedge clk_1us or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        begin
            cnt_low <=  7'd0      ;
            cnt_us   <=  21'd0    ;
        end
    else
        case(state)
        S_WAIT_1S:
            if(cnt_us == T_1S_DATA) 
                cnt_us   <=  21'd0  ;
            else
                cnt_us   <=  cnt_us + 1'b1;
        S_LOW_18MS:
            if(cnt_us == T_18MS_DATA)
                cnt_us   <=  21'd0  ;
            else
                cnt_us   <=  cnt_us + 1'b1;
        S_DLY1:
            if(cnt_us == 10)
                cnt_us   <=  21'd0  ;
            else
                cnt_us   <=  cnt_us + 1'b1;
        S_REPLY: 
		    if(dht11 == 1'b0) //当dht11发送低电平回应时,计算其低电平的持续时间
                begin
                    cnt_low  <=  cnt_low + 1'b1 ;
                    cnt_us   <=  cnt_us + 1'b1  ;
                end
			else  if(dht11_rise == 1'b1 && cnt_low >= 70) //在规定范围内产生响应
                begin
                    cnt_low <=  7'd0    ;
                    cnt_us   <=  21'd0  ;
                end
            else    if(cnt_us <= 1000)   //若1ms前,dht11没响应
                begin
                    cnt_low <=  cnt_low        ;
                    cnt_us  <=  cnt_us + 1'b1  ;
                end
            else     //若1ms后,dht11还没响应,则回去继续发送起始信号
				begin
                    cnt_low <=  7'd0   ;
                    cnt_us  <=  21'd0  ;
                end
        S_DLY2:
            if(dht11_fall == 1'b1 && cnt_us >= 70)
                cnt_us   <=  21'd0  ;
            else
                cnt_us   <=  cnt_us + 1'b1;
        S_RD_DATA:
            if(dht11_fall == 1'b1 || dht11_rise == 1'b1)
                cnt_us   <=  21'd0  ;
            else
                cnt_us   <=  cnt_us + 1'b1;
        default:
            begin
                cnt_low  <=  7'd0   ;
                cnt_us   <=  21'd0  ;
            end
        endcase

//各状态下的单总线赋值
always@(posedge clk_1us or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        begin
               dht11_out <=  1'b0    ;
               dht11_en  <=  1'b0    ;
        end
    else
        case(state)
        S_WAIT_1S:
                begin
                    dht11_out    <=  1'b0    ;
                    dht11_en     <=  1'b0    ;
                end
        S_LOW_18MS: //拉低总线18ms
                begin
                    dht11_out    <=  1'b0    ;
                    dht11_en     <=  1'b1    ;
                end
    //后面状态释放总线即可,由DHT11操控总线
        S_DLY1:
                begin
                    dht11_out    <=  1'b0    ;
                    dht11_en     <=  1'b0    ;
                end
        S_REPLY:
                begin
                    dht11_out    <=  1'b0    ;
                    dht11_en     <=  1'b0    ;
                end
        S_DLY2:
                begin
                    dht11_out    <=  1'b0    ;
                    dht11_en     <=  1'b0    ;
                end
        S_RD_DATA:
                begin
                    dht11_out    <=  1'b0    ;
                    dht11_en     <=  1'b0    ;
                end
        default:;
        endcase

//bit_cnt:读出数据bit位数计数器
always@(posedge clk_1us or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        bit_cnt <=  6'b0;
    else    if(bit_cnt == 40 && dht11_rise == 1'b1)
        bit_cnt <=  6'b0;
    else    if(dht11_fall == 1'b1 && state == S_RD_DATA)
        bit_cnt <=  bit_cnt + 1'b1;	
//data_tmp:将读出的数据寄存在data_tmp中
always@(posedge clk_1us or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        data_tmp    <=  40'b0;
    else    if(state == S_RD_DATA && dht11_fall == 1'b1 && cnt_us<=50)
        data_tmp[39-bit_cnt]   <=  1'b0;
    else    if(state == S_RD_DATA && dht11_fall == 1'b1 && cnt_us>50)
        data_tmp[39-bit_cnt]   <=  1'b1;
    else
        data_tmp    <=  data_tmp;
//data:校验,若检验位正确,则数据值有效
always@(posedge clk_1us or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        data    <=  32'b0;
    else    if(data_tmp[7:0] == data_tmp[39:32] + data_tmp[31:24] +
                                      data_tmp[23:16] + data_tmp[15:8])
        data    <=  data_tmp[39:8];   
     else
        data    <=  data;

//data_flag:数据变换标志信号的产生,按一次键变换一次  (系统时钟)
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        data_flag   <=  1'b0;
    else    if(key_flag == 1'b1)
        data_flag   <=  ~data_flag;
    else
        data_flag   <=  data_flag;
//data_out:对数码管显示的湿度和温度进行赋值
always@(posedge clk_1us or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        data_out    <=  20'b0;
    else    if(data_flag == 1'b0 ) //湿度小数位为0
        data_out    <=  data[31:24] * 10;  //为了兼容温度显示的小数,将湿度的个
		                               //位与十位扩大10倍,小数位为零
    else    if(data_flag == 1'b1) //温度低四位显示温度小数数据
        data_out    <=  data[15:8] * 10 + data[3:0];	
		
//sign:符号位的显示
always@(posedge clk_1us or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        sign    <=  1'b0;
    else    if(data[7] == 1'b1 && data_flag == 1'b1)//当温度低八位最高位为1时,显示负号
        sign    <=  1'b1;
    else
        sign    <=  1'b0;
endmodule

2.3 上板验证

当按键未被按下,湿度如下图:

当按键被按下,温度如下图:

3. 总结

1. DH1T11温湿度传感器与DS18B20温湿度传感器非常类似,都是严格按照时序要求写代码。建议可以比较学习。

2. 显示温度与湿度部分的数据处理,采用 X10 进行在数码管上的移一位显示,配合了数码管小数部分的显示特点又区分了温度与湿度。

3. 程序是按照一个接一个状态及顺序跳转的,建议使用状态机。

说明:

       本人使用的是野火家Xilinx Spartan6系列开发板及配套教程,以上内容如有疑惑或错误欢迎评论区指出,或者移步B站观看野火家视频教程。

开发软件:ise14.7     仿真:modelsim 10.5 

如需上述资料私信或留下邮箱

猜你喜欢

转载自blog.csdn.net/m0_72885897/article/details/129674357