基于FPGA的DHT11数字温湿度传感器测试

1、DHT11数字温湿度传感器

本文将使用三段式状态机(Moore型)来对DHT11进行读取温湿度的操作,以便了解DHT11和熟悉三段式状态机的写法。

1.1、概述

DHT11数字温湿度传感器是一款含有已校准数字信号输出的温湿度 复合传感器。它应用专用的数字模块采集技术和温湿度传感技术,确保 、产品具有极高的可靠性与卓越的长期稳定性。传感器包括一个电容式感湿元件和一个NTC测温元件,并与一个高性能8位单片机相连接。

1.2、DHT11 外形及参数

DHT11 外形如下:

引脚说明 :

  1. VDD 供电3.3~5.5V DC
  2. DATA 串行数据,单总线
  3. NC 空脚
  4. 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--野火电子