基于FPGA的DHT11数字温湿度传感器测试
1、DHT11数字温湿度传感器
本文将使用三段式状态机(Moore型)来对DHT11进行读取温湿度的操作,以便了解DHT11和熟悉三段式状态机的写法。
1.1、概述
DHT11数字温湿度传感器是一款含有已校准数字信号输出的温湿度 复合传感器。它应用专用的数字模块采集技术和温湿度传感技术,确保 、产品具有极高的可靠性与卓越的长期稳定性。传感器包括一个电容式感湿元件和一个NTC测温元件,并与一个高性能8位单片机相连接。
1.2、DHT11 外形及参数
DHT11 外形如下:
引脚说明 :
- VDD 供电3.3~5.5V DC
- DATA 串行数据,单总线
- NC 空脚
- GND 接地,电源负极
1.3、通讯方式
从上面的引脚说明可以看出来,DHT11是采用单总线(DATA脚)的方式来与上位机通讯,一次通讯时间4ms左右。单总线即只有一根数据线,系统中的数据交换、 控制均由单总线完成。设备(主机或从机)通过一个漏极开路或三态端口连至该数据线, 以允许设备在不发送数据时能够释放总线。单总线通常要求外接一个约 4.7kΩ 的上拉电阻,这样,当总线闲置时,其状态为高电平。由于它们是主从结构,只有主机呼叫从机 时,从机才能应答,因此主机访问器件都必须严格遵循单总线序列,如果出现序列混乱, 器件将不响应主机。
1.3.1、数据组成
数据分为整数部分和小数部分,数据格式如下:
一次完整的数据传输为40bit,高位在前。
数据格式:8bit湿度整数数据+8bit湿度小数数据 +8bit温度整数数据+8bit温度小数数据 +8bit校验和数据
数据传送正确时:校验和数据等于“8bit湿度整数数据 + 8bit湿度小数数据 + 8bit温度 整数数据 + 8bit温度小数数据”所得结果的末8位
示例一:
接收到的40位数据为: 00110101 00000000 00011000 00000100 0101 0001
湿度高8位 湿度低8位 温度高8位 温度低8位 校验位
计算:00110101+00000000+00011000+00000100=01010001
接收数据正确:
湿度:00110101(整数)=35H=53%RH00000000(小数)=00H=0.0%RH=>53%RH+0.0%RH=53.0%RH
温度:00011000(整数)=18H=24℃00000100(小数)=04H=0.4℃=>24℃+0.4℃=24.4℃
1.3.2、通信时序
主机发送一次开始信号后,DHT11 从低功耗模式转换到高速模式,待主机发送的开始信号结束后,DHT11 发送响应信号,送出 40bit 的数据,并触发一次信息采集。信号发送如下图:
主机和从机之间的通信可以通过以下几个步骤完成(主机读取 DHT11 温湿度数据的步 骤):
步骤一: DHT11 上电后(DHT11 上电后要等待 1S 以越过不稳定状态在此期间不能发送任何指令),测试环境温湿度数据,并记录数据,同时 DHT11 的 DATA 数据线由上拉电阻拉高 一直保持高电平;此时 DHT11 的单总线引脚处于输入状态,时刻检测外部信号。
步骤二: 主机发送开始信号:输出低电平,持续时间不小于18ms 且不超过 30ms。发送完之后释放总线等待 DHT11 应答。发送信号如图所示:
步骤三: DHT11 的 DATA 引脚检测到外部信号有低电平时,等待外部信号低电平结束,延迟后 DHT11 的 DATA 引脚输出 83us 的低电平作为应答信号,紧接着输出 87us 的高电平通知外设准备接收数据,发送信号如图所示:
步骤四: 由 DHT11 的 DATA 引脚输出 40 位数据,主机根据 I/O 电平的变化接收 40 位数据,位 数据 “0” 的格式为:54us 的低电平后 23~27us 的高电平,位数据 “1” 的格式为:54us 的低 电平后 68~74us 的高电平。位数据“0”、“1”格式信号如图所示:
结束信号: DHT11 的 DATA 引脚输出 40 位数据后,继续输出低电平 54us 后转为输入状态,由于上拉电阻随之变为高电平。DHT11 内部重测环境温湿度数据,并记录数据,等待外部信号的到来。
注意:每两次采样应间隔2s。
各信号的电平持续时间范围应满足下表,我们进行设计时建议将下表的时间都用parameter给参数化,调试的时候再结合实际内容更改,这样会方便一点。
2、采用三段式状态机测试
2.1、整体说明
使用三段式状态机(Moore型)来对DHT11进行读取温湿度的操作。三段式状态机的概念可以参考:状态机(一段式、二段式、三段式)、摩尔型(Moore)和米勒型(Mealy)
因为本文只写DHT11的驱动,不涉及到其他模块(如数码管),所以模块框图如下:
信号说明如下:
- sys_clk:系统时钟,50M
- rst_n:低电平有效的复位信号
- dht11:单总线(双向信号)
- data_valid:输出的有效数据,位宽32
2.2、状态机设计
根据 DHT11 的数据时序图可以画出状态跳转图来进一步了解如何控制 DHT11,以及读出 DHT11 的湿度温度值。如图所示:
下面对各状态说明:
WAIT_1S:上电后就进入这个状态,因为上电后需要1s的是时间稳定DHT11的状态。在这个状态使用计数器计时,满足1s就跳转到下一个状态START,并且计数清零;不满足就停留在这个状态一直计数
START:在这个状态DHT11进入工作状态。主机将总线拉低18ms~30ms(一开始参数化一个值)后再拉高。在这个状态仍使用计数器计时,拉低总线后计数开始,计数满足条件后清零并跳转到下一个状态DELAY_10us;不满足就停留在这个状态一直计数
DELAY_10us:在这个状态主机应拉高10us后释放总线进入等待状态等待从机(DHT11)拉低作为回应。主机将总线拉低10us,然后释放总线。在这个状态仍使用计数器,拉低总线后计数开始,计数满足条件后清零并跳转到下一个状态DELAY_10us;不满足就停留在这个状态一直计时
REPLY:这个状态是检查从机是否回应以及回应是否符合时序。一旦主机检测到上升沿且此时计数器介于70us~100us,则说明从机回应了一个70us~100us的低电平信号,就跳转到下一个状态DELAY_75us;一旦主机检测到上升沿但此时计数器大于100us或者计数器大于500us时仍没有检测到上升沿,则说明从机的应答是不符合时序的,那么状态机跳转到START状态(不跳转到WAIT_1S状态,是因为只有在每次上电后才需要延迟1s)并将计数器清零,在这个状态计数器一直计数知道跳转到另一个状态才清零(边沿检测可以参考:边沿检测电路(上升沿、下降沿))
DELAY_75us:这个状态是从机输出87us高电平通知主机准备接收数据。因为后续发送数据时一定是以低电平开始,所以在这里一定会检测到数据线上的一个下降沿。所以一旦检测到下降沿且计数器大于70us,那么跳转到下一个状态REV_data并将计数器清零;不然就停留在这个状态一直计时
需要注意以下:
- 信号线DHT11是一个双向信号,所以使用时要用使用三态门的方法来操作,具体方法参考:如何规范地使用双向(inout)信号?
- 之前说到,每两次采样之间应该间隔2s,所以在REPLY状态时,一旦从机响应不满足时序则会跳转到状态WAIT_1S重新开始整个流程,直到满足时序要求为止。这样做的好处是不用每次接受完数据后延时两秒,能省掉不少时间
2.3、Verilog代码
根据上面的状态分析图,编写Verilog代码如下:(这里就不写分析了,注释已经写得很详细了,如果你看到了这边文章且这段代码又不懂的地方,可以评论给我)
//==================================================================
//--3段式状态机(Moore)实现的DHT11驱动
//==================================================================
//------------<模块及端口声明>----------------------------------------
module dht11_drive(
input sys_clk , //系统时钟,50M
input rst_n , //低电平有效的复位信号
inout dht11 , //单总线(双向信号)
output reg [31:0] data_valid //输出的有效数据,位宽32
);
//------------<参数定义>----------------------------------------------
//状态机状态定义,使用独热码(onehot code)
localparam WAIT_1S = 6'b000001 ,
START = 6'b000010 ,
DELAY_10us = 6'b000100 ,
REPLY = 6'b001000 ,
DELAY_75us = 6'b010000 ,
REV_data = 6'b100000 ;
//时间参数定义
localparam T_1S = 999_999 , //上电1s延时计数,单位us
T_BE = 17_999 , //主机起始信号拉低时间,单位us
T_GO = 12 ; //主机释放总线时间,单位us
//------------<reg定义>----------------------------------------------
reg [6:0] cur_state ; //现态
reg [6:0] next_state ; //次态
reg [4:0] cnt ; //50分频计数器,1Mhz(1us)
reg dht11_out ; //双向总线输出
reg dht11_en ; //双向总线输出使能,1则输出,0则高阻态
reg dht11_d1 ; //总线信号打1拍
reg dht11_d2 ; //总线信号打2拍
reg clk_us ; //us时钟
reg [21:0] cnt_us ; //us计数器,最大可表示4.2s
reg [5:0] bit_cnt ; //接收数据计数器,最大可以表示64位
reg [39:0] data_temp ; //包含校验的40位输出
//------------<wire定义>----------------------------------------------
wire dht11_in ; //双向总线输入
wire dht11_rise ; //上升沿
wire dht11_fall ; //下降沿
//==================================================================
//===========================<main code>===========================
//==================================================================
//-----------------------------------------------------------------------
//--双向端口使用方式
//-----------------------------------------------------------------------
assign dht11_in = dht11; //高阻态的话,则把总线上的数据赋给dht11_in
assign dht11 = dht11_en ? dht11_out : 1'bz; //使能1则输出,0则高阻态
//-----------------------------------------------------------------------
//--us时钟生成,因为时序都是以us为单位,所以生成一个1us的时钟会比较方便
//-----------------------------------------------------------------------
//50分频计数
always @(posedge sys_clk or negedge rst_n)begin
if(!rst_n)
cnt <= 5'd0;
else if(cnt == 5'd24) //每25个时钟500ns清零
cnt <= 5'd0;
else
cnt <= cnt + 1'd1;
end
//生成1us时钟
always @(posedge sys_clk or negedge rst_n)begin
if(!rst_n)
clk_us <= 1'b0;
else if(cnt == 5'd24) //每500ns
clk_us <= ~clk_us; //时钟反转
else
clk_us <= clk_us;
end
//-----------------------------------------------------------------------
//--上升沿与下降沿检测电路
//-----------------------------------------------------------------------
//检测总线上的上升沿和下降沿
assign dht11_rise = ~dht11_d2 && dht11_d1; //上升沿
assign dht11_fall = ~dht11_d1 && dht11_d2; //下降沿
//dht11打拍,捕获上升沿和下降沿
always @(posedge clk_us or negedge rst_n)begin
if(!rst_n)begin
dht11_d1 <= 1'b0; //复位初始为0
dht11_d2 <= 1'b0; //复位初始为0
end
else begin
dht11_d1 <= dht11; //打1拍
dht11_d2 <= dht11_d1; //打2拍
end
end
//-----------------------------------------------------------------------
//--三段式状态机
//-----------------------------------------------------------------------
//状态机第一段:同步时序描述状态转移
always @(posedge clk_us or negedge rst_n)begin
if(!rst_n)
cur_state <= WAIT_1S;
else
cur_state <= next_state;
end
//状态机第二段:组合逻辑判断状态转移条件,描述状态转移规律以及输出
always @(*)begin
next_state = WAIT_1S;
case(cur_state)
WAIT_1S :begin
if(cnt_us == T_1S) //满足上电延时的时间
next_state = START; //跳转到START
else
next_state = WAIT_1S; //条件不满足状态不变
end
START :begin
if(cnt_us == T_BE) //满足拉低总线的时间
next_state = DELAY_10us; //跳转到DELAY_10us
else
next_state = START; //条件不满足状态不变
end
DELAY_10us :begin
if(cnt_us == T_GO) //满足主机释放总线时间
next_state = REPLY; //跳转到REPLY
else
next_state = DELAY_10us; //条件不满足状态不变
end
REPLY :begin
if(cnt_us <= 'd500)begin //不到500us
if(dht11_rise && cnt_us >= 'd70
&& cnt_us <= 'd100) //上升沿响应,且低电平时间介于70~100us
next_state = DELAY_75us; //跳转到DELAY_75us
else
next_state = REPLY; //条件不满足状态不变
end
else
next_state = START; //超过500us仍没有上升沿响应则跳转到START
end
DELAY_75us :begin
if(dht11_fall && cnt_us >= 'd70) //上升沿响应,且低电平时间大于70us
next_state = REV_data; //跳转到REV_data
else
next_state = DELAY_75us; //条件不满足状态不变
end
REV_data :begin
if(dht11_rise && bit_cnt == 'd40) //接收完了所有40个数据后会拉低一段时间作为结束
//捕捉到上升沿且接收数据个数为40
next_state = START; //状态跳转到START,重新开始新一轮采集
else
next_state = REV_data; //条件不满足状态不变
end
default:next_state = START; //默认状态为START
endcase
end
//状态机第三段:时序逻辑描述输出
always @(posedge clk_us or negedge rst_n)begin
if(!rst_n)begin //复位状态下输出如下
dht11_en <= 1'b0;
dht11_out <= 1'b0;
cnt_us <= 22'd0;
bit_cnt <= 6'd0;
data_temp <= 40'd0;
end
else
case(cur_state)
WAIT_1S :begin
dht11_en <= 1'b0; //释放总线,由外部电阻拉高
if(cnt_us == T_1S)
cnt_us <= 22'd0; //计时满足条件则清零
else
cnt_us <= cnt_us + 1'd1; //计时不满足条件则继续计时
end
START :begin
dht11_en <= 1'b1; //占用总线
dht11_out <= 1'b0; //输出低电平
if(cnt_us == T_BE)
cnt_us <= 22'd0; //计时满足条件则清零
else
cnt_us <= cnt_us + 1'd1; //计时不满足条件则继续计时
end
DELAY_10us :begin
dht11_en <= 1'b0; //释放总线,由外部电阻拉高
if(cnt_us == T_GO)
cnt_us <= 22'd0; //计时满足条件则清零
else
cnt_us <= cnt_us + 1'd1; //计时不满足条件则继续计时
end
REPLY :begin
dht11_en <= 1'b0; //释放总线,由外部电阻拉高
if(cnt_us <= 'd500)begin //计时不到500us
if(dht11_rise && cnt_us >= 'd70
&& cnt_us <= 'd100) //上升沿响应,且低电平时间介于70~100us
cnt_us <= 22'd0; //计时清零
else
cnt_us <= cnt_us + 1'd1; //计时不满足条件则继续计时
end
else
cnt_us <= 22'd0; //超过500us仍没有上升沿响应,则计数清零
end
DELAY_75us :begin
dht11_en <= 1'b0; //释放总线,由外部电阻拉高
if(dht11_fall && cnt_us >= 'd70) //上升沿响应,且低电平时间大于70us
cnt_us <= 22'd0; //计时清零
else
cnt_us <= cnt_us + 1'd1; //计时不满足条件则继续计时
end
REV_data :begin
dht11_en <= 1'b0; //释放总线,由外部电阻拉高,进入读取状态
if(dht11_rise && bit_cnt == 'd40)begin //数据接收完毕
bit_cnt <= 6'd0; //清空数据接收计数器
cnt_us <= 22'd0; //清空计时器
end
else if(dht11_fall)begin //检测到低电平,则说明接收到一个数据
bit_cnt <= bit_cnt + 1'd1; //数据接收计数器+1
cnt_us <= 22'd0; //计时器重新计数
if(cnt_us <= 'd100)
data_temp[39-bit_cnt] <= 1'b0; //总共所有的时间少于100us,则说明接收到“0”
else
data_temp[39-bit_cnt] <= 1'b1; //总共所有的时间大于100us,则说明接收到“1”
end
else begin //所有数据没有接收完,且正处于1个数据的接收进程中
bit_cnt <= bit_cnt;
data_temp <= data_temp;
cnt_us <= cnt_us + 1'd1; //计时器计时
end
end
default:;
endcase
end
//校验读取的数据是否符合校验规则
always @(posedge clk_us or negedge rst_n)begin
if(!rst_n)
data_valid <= 32'd0;
else if((data_temp[7:0] == data_temp[39:32] + data_temp[31:24] +
data_temp[23:16] + data_temp[15:8]))
data_valid <= data_temp[39:8]; //符合规则,则把有效数据赋值给输出
else
data_valid <= data_valid; //不符合规则,则舍弃这次读取的数据,输出仍保持上次的状态不变
end
endmodule
2.4、调试
因为通讯过程涉及到从机的响应,我又找不到相应的器件模型,仿真就不搞了。
直接使用signaltap抓下波形:
上图中:
- 状态机从WAIT_1S跳转到START状态
- cnt_us计数到999999后清零,说明时间过了1s
- dht11_en拉高同时dht11_out为0,说明主机拉低了总线
- 捕获到了一个下降沿
上图中:
- 在①处状态机从状态START跳转到状态DELAY_10us
- 在①处cnt_us计数到17999后清零,说明时间过了18ms
- 进入状态DELAY_10us后dht11_en拉低,说明主机释放了总线
- 在②处状态机从状态DELAY_10us跳转到状态REPLY
- 在②处cnt_us计数到12后清零,说明时间过了13us,此时总线被从机拉低,开始发送响应信号
上图中:
- 状态机从状态REPLY跳转到状态DELAY_75us
- cnt_us计数到83后清零,说明时间过了84us
- cnt_us计数到81后dht11从0变为1,说明响应信号的低电平持续了82us,符合时序要求
上图中:
- 状态机从状态DELAY_75us跳转到状态REV_data
- cnt_us计数到85后清零,说明时间过了86us
- cnt_us计数到83后dht11从1变为0,说明响应信号的高电平持续了84us,符合时序要求
上图中:
- 在①处状态机进入状态REV_data
- 在接收数据期间,每接收一个数据bit_cnt+1,直到40,一共40个数据加最后的拉低停止
- 在②处状态机从状态REV_data进入状态START,开始新一轮的信息采集
3、上板调试
添加数码管显示模块,和按键控制模块(每次只单独显示温度或者湿度,每次按下按键即切),编译工程,板卡显示如下:
温度:
湿度:
码字真的很费时间,麻烦您给个点赞或者关注!
- -!
- -!- -!
- -!- -!- -!
4、参考
温湿度模块 DHT11 产品手册--ASAIR
FPGA Verilog 开发实战指南--基于 Intel Cyclone IV--野火电子