记录初学fpga实现的一个功能。了解完DHT11所用的单总线协议后,其详细的读写规则有很多博客介绍,大概是主机发送低电平再发送高电平,然后从机发送低电平再高电平来响应(电平都得保持一定的时间),最后就是从机发数据给主机,用低电平做准备,高电平持续时间长短来表示0和1(低电平持续时间短,高电平持续时间长)。
按照如下状态转移图编写verilog代码,最终实现的功能是从DHT11读取温湿度数据,一次读取的数据总共有40bit,并将读取的数据存储在data_reg寄存器中。其中data_reg[39:32]是湿度的整数部分,data_reg[31:24]是湿度的小数,data_reg[23:16]是温度的整数,data_reg[15:8]是温度的小数,data_reg[7:0]是校验和,也就是前面4个Byte一起加起来的结果。
需要对状态图补充说明的是我认为从机一定会给响应信号过来,从而简化了问题,但是也有可能从机不给响应信号(坏了啥的),那样就卡在Waitlow状态下了。
以上状态转移图中IDLE是空闲状态,当start信号变为高电平时进入Mlow状态,也就是主机发低电平,当低电平时间足够了进入Mhigh状态发高电平,发高电平结束后进入Waitlow状态检查从机是否发送了低电平来响应(代码中用检测下降沿方式来实现),最后就是从机发数据状态。
代码如下:
// 读DHT11数据模块
// hqz 20230309
module DHT11
(
input sys_clk, //系统时钟信号,50MHz,0.02us
input sys_rst_n, //异步复位信号,低电平有效
input start, //DHT11 driver module start signal
output data_end, //一次传输数据结束
inout wire dat //双向数据线
);
//状态机定义
localparam IDLE = 3'd0, //空闲状态
Mlow = 3'd1, //主机发送18ms低电平
Mhigh = 3'd2, //主机拉高电平30us
Waitlow = 3'd3, //检测从机响应低电平
DHTdata = 3'd4; //读取从机数据
//时间计数定义
localparam t1 = 20'd900_000,
t2 = 13'd1500,
t5 = 13'd2000, //数据为0和1的分界,0的高电平持续26~28us,1的高电平持续116~118us
t6 = 13'd1000;
reg sda_en; //双向口使能信号
reg sda_out; //iic sda输出寄存器
wire sda_in; //iic sda输入信号
reg datin_reg; //dat输入寄存器
reg [12:0] cnt_t2; //t1,t2时间计数器
reg [19:0] cnt_t1;
reg [5:0] cnt_bit_num; //数据位传输的个数
reg [2:0] cur_state,next_state; //状态机的现态和次态
reg t1_end,t2_end; //t1,t2计满信号
reg dat_pre1,dat_pre2; //上一拍dat总线上的数据
reg ack; //从机应答有效信号
reg bit_num_full; //数据计数满40个
reg [12:0] cnt_01/* synthesis noprune */; //总线高电平时间计数器
reg data_0,data_1,cnt_01_en; //计数总线上高电平持续时间
reg [5:0] data_bit; //40bit寄存器的地址
reg [39:0] data_reg /* synthesis noprune */; //从机发送的数据
//双向口处理
assign sda_in = dat;
assign dat = sda_en?sda_out:1'bz;
assign data_end=bit_num_full;
//状态机第一段
always @(posedge sys_clk or negedge sys_rst_n)
begin
if(~sys_rst_n)
cur_state <= IDLE;
else
cur_state <= next_state;
end
//状态机第二段
always @(*)
begin
case(cur_state)
IDLE: next_state <= (start?Mlow:IDLE);
Mlow: next_state <= (t1_end?Mhigh:Mlow);
Mhigh: next_state <= (t2_end?Waitlow:Mhigh);
Waitlow: next_state <= (ack?DHTdata:Waitlow);
DHTdata: next_state <= (bit_num_full?IDLE:DHTdata);
default: next_state <= IDLE;
endcase
end
//状态机第三段
always @(posedge sys_clk or negedge sys_rst_n)
begin
if(~sys_rst_n)
begin
sda_en <= 1'b0;
cnt_bit_num <= 6'b0;
cnt_01_en <= 1'b0;
bit_num_full <= 1'b0;
ack <= 1'b0;
dat_pre1 <= 1'b1;
dat_pre2 <= 1'b1;
end
else
case(cur_state)
IDLE:
begin
sda_en <= 1'b1; //dat输出
sda_out <= 1'b1; //拉高电平
end
Mlow: sda_out <= 1'b0; //拉低电平t1时间
Mhigh: sda_out <= 1'b1; //拉高电平t2时间
Waitlow:
begin
sda_en <= 1'b0; //总线输入
dat_pre1 <= sda_in; //检测sda_in下降沿
dat_pre2 <= dat_pre1;
if((~dat_pre1)&dat_pre2) ack <= 1'b1;
else ack <= 1'b0;
end
DHTdata:
begin
dat_pre1 <= sda_in; //检测sda_in上升沿
dat_pre2 <= dat_pre1;
if(dat_pre1&(~dat_pre2))
begin
cnt_01_en <= 1'b1;
cnt_bit_num <= cnt_bit_num + 6'd1;
end
else if((~dat_pre1)&dat_pre2) //检测sda_in下降沿
begin
cnt_01_en <= 1'b0;
if(cnt_bit_num == 6'd40) bit_num_full <= 1'b1;
else bit_num_full <= 1'b0;
end
else
begin
cnt_bit_num <= cnt_bit_num;
bit_num_full <= 1'b0;
end
end
endcase
end
always@(posedge sys_clk, negedge sys_rst_n)begin
if(~sys_rst_n) begin
data_bit <= 6'd40;
data_reg <= 40'd0;
end
else begin
if(data_bit != 6'd0)
if(data_0) begin
data_bit <= data_bit - 6'd1;
data_reg[data_bit] <= 1'b0;
end
else if(data_1)begin
data_bit <= data_bit - 6'd1;
data_reg[data_bit] <= 1'b1;
end
else data_bit <= data_bit;
else data_bit <= 6'd40;
end
end
//判断从机发送的电平,并对应的输出高低电平标志信号
always@(posedge sys_clk, negedge sys_rst_n)begin
if(~sys_rst_n) begin
cnt_01 <= 12'd0;
data_0 <= 1'b0;
data_1 <= 1'b0;
end
else begin
if(cnt_01_en) cnt_01 <= cnt_01 + 12'd1;
else begin
if(cnt_01>t5) data_1 <= 1'b1; //从机发送的高电平
else if(cnt_01>t6) data_0 <= 1'b1; //从机发送的低电平
else begin
data_0 <= 1'b0;
data_1 <= 1'b0;
end
cnt_01 <= 12'd0; //清空计数器
end
end
end
//主机拉低电平时间计数器,18ms,计数t1个
always @(posedge sys_clk or negedge sys_rst_n)
begin
if(~sys_rst_n)begin
cnt_t1 <= 20'd0;
t1_end<=1'b0;
end
else
if(next_state == Mlow)
begin
if(cnt_t1 == t1)
begin
cnt_t1 <= 20'd0;
t1_end <= 1'b1;
end
else
cnt_t1 <= cnt_t1 + 20'd1;
end
else
begin
cnt_t1 <= 20'd0;
t1_end <= 1'b0;
end
end
//主机拉高电平时间计数器, 30us,计数t2个
always@(posedge sys_clk,negedge sys_rst_n)begin
if(~sys_rst_n)begin
cnt_t2 <= 13'd0;
t2_end <= 1'b0;
end
else
if(next_state == Mhigh)
begin
if(cnt_t2 == t2)
begin
cnt_t2 <= 13'd0;
t2_end <= 1'b1;
end
else
cnt_t2 <= cnt_t2 + 13'd1;
end
else
begin
cnt_t2 <= 13'd0;
t2_end <= 1'b0;
end
end
endmodule
使用SignalTap Logical Analyzer抓取的波形如下:
波形中data_reg数据是4E00140668h,先看校验和4E+00+14+06=68(十六进制),湿度是78.0%,温度是20.6°C。总结下来,设计的比较简陋,有一些情况没有考虑,但是第一步能跑就行了哈哈哈,还有上板调试用好逻辑分析仪非常有用,帮我调了很多bug。