尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

FPGA驱动DHT11:Verilog状态机实现单总线温湿度采集

FPGA驱动DHT11:Verilog状态机实现单总线温湿度采集 简介状态机是数字系统设计中的核心概念它将复杂的时序流程拆解为可管理的状态跳转。单总线协议作为一种节省IO的通信方式广泛应用于DHT11等温湿度传感器。DHT11通过一根数据线完成双工通信其微秒级时序对驱动设计提出了挑战。理解其数据帧格式与校验规则是正确读取温湿度数据的前提。在FPGA开发中状态机设计不仅要处理时序逻辑还需涉及双向IO与计数器协同。基于EP4CE10平台使用Verilog HDL实现DHT11驱动能够有效训练协议解析能力。该实现覆盖起始信号、响应检测、数据采样、校验及仿真验证并支持数码管显示。这种方法同样适用于DS18B20、I2C等器件为物联网环境监测等应用场景提供可靠基础。通过工程实践可深入掌握单总线驱动的通用思路。 DHT11在FPGA项目里算是很经典的练习对象。很多学FPGA的人卡在它面前是因为它和I2C、SPI不一样——一根数据线既当输入又当输出时序还是微秒级的逻辑分析仪一接上去密密麻麻的波形看得人头皮发麻。我最初也在这上面折腾了差不多三天后来把状态机理顺之后才发现这类单总线协议驱动的难点不在代码量而在于怎么把一个完整的时序流程拆成状态和计数器的组合。这篇文章以我实际做过的FPGA EP4CE10驱动DHT11温湿度传感器工程为主线用Verilog HDL实现把从协议解析、状态机设计、双向IO处理到仿真验证、上板调试的完整过程都过一遍。项目不算大但非常适合FPGA入门者用来练手也能帮有一定基础的朋友理清单总线协议的驱动思路。看完之后你不仅能驱动DHT11再去碰DS18B20、I2C这类器件也会顺手很多。1. 项目概述与方案选型1.1 工程到底做了什么这个工程的核心功能一句话就能讲清楚用FPGA的普通IO口按照DHT11单总线协议发起起始信号接收传感器返回的40位温湿度数据经过校验后提取温度和湿度值。代码里默认把数据送到开发板上的数码管显示如果你愿意改成UART发送到上位机也很方便。做这个项目之前我先明确了一个思路DHT11驱动本身只是手段真正的目标是训练“用状态机解析时序协议”的能力。LED流水灯和按键消抖这类基础项目做完之后新手接触的第一个有点含金量的外设驱动往往就是DHT11或者DS18B20这类单总线器件。相比I2C和SPIDHT11的时序足够简单但又是标准的一对一握手流程很适合用来建立状态机的思维方式。工程结构上顶层模块只暴露了两个对外接口一个连DHT11的数据引脚一个连数码管的段选位选。内部逻辑拆成两部分——DHT11控制核心负责时序显示模块负责把16位温湿度数据转成BCD码后动态扫描显示。这样隔离之后DHT11的时序逻辑可以单独复用以后想改成串口输出只需要换掉显示模块就行。1.2 EP4CE10的资源能不能满足EP4CE10是Cyclone IV E系列里的入门型号逻辑单元10320个嵌入式存储414KbitIO数量根据封装不同大约在90到179个。驱动DHT11这件事本身消耗的资源非常少主要占资源的是数码管动态扫描的计数器以及缓存寄存器整体算下来逻辑单元占用可能不到200个EP4CE10跑这个项目绰绰有余。选择EP4CE10还有一个实际的原因它原生支持3.3V IO电平而DHT11用3.3V供电时输出的高电平也正好是3.3V和FPGA的IO电平直接匹配不需要额外加电平转换。DHT11的数据引脚是开漏输出结构使用时需要在数据线和VCC之间接一个4.7k欧姆上拉电阻。我用的开发板上没集成这个电阻是用面包板飞线解决的。1.3 为什么选DHT11而不是其他传感器DHT11传感器输出的是已经数字化的温湿度数据不需要像PT100那样做ADC采集。它的精度不算高——温度±2摄氏度、湿度±5%RH——但胜在便宜、接线简单、单总线协议逻辑清晰。在很多对精度不敏感的场合比如机房温湿度监控、温室大棚环境采集这个精度完全够用。如果你的项目对精度有更高要求可以后续把驱动逻辑改到DHT22或SHT30上思路是通用的。DHT11单总线协议有个特殊的地方主设备发起通信时至少要拉低总线18ms以上这个时间比I2C的起始条件长得多基本等于用系统时钟“数出”一个18毫秒的长脉冲。再加上数据位需要区分26到28微秒和70微秒左右两种高电平时长整个驱动最核心的设计点就落在微秒级计数、状态跳转和数据采样上。这也是为什么用FPGA驱动DHT11比用单片机驱动更有学习价值。单片机上很多人直接用delay_us()函数阻塞延时CPU一条一条指令把时序拖出来代码看着简单但很难真正理解时序的本质FPGA里没有阻塞延时这种写法只有“计数器数到某个值就切状态”这一种思路。写代码的过程实际上就是在纸上把协议时序图翻译成状态机。2. DHT11单总线协议拆解2.1 一次完整通信的四个阶段DHT11的通信过程可以分成四个阶段起始信号、响应信号、数据位传输、结束。主设备先把总线拉低持续时间要大于18ms一般代码里取20ms留出余量然后主设备释放总线总线被上拉电阻拉高。主设备释放后等待20到40微秒DHT11会主动把总线拉低约80微秒作为响应信号之后再把总线拉高约80微秒表示“准备好了我开始发数据”。接下来DHT11开始发送40位数据。每一位数据都以约50微秒的低电平开始后面跟着一段长度可变的高电平。如果高电平持续26到28微秒代表数据位0如果高电平持续约70微秒代表数据位1。数据发完后总线回到高电平空闲状态。阶段事件时间参数主机发起拉低总线至少18ms常用20ms主机释放拉高总线20-40usDHT11响应拉低约80usDHT11响应拉高约80us数据位起始拉低约50us数据位0高电平时长26-28us数据位1高电平时长约70us这张表看起来简单真正写代码时有个容易忽略的问题DHT11的数据线是双向的。主设备拉低、DHT11也拉低、最后双方都释放靠上拉电阻拉高。这个“释放”在FPGA里对应的是把IO配置成高阻态也就是Verilog里的z状态。inout端口操作如果没写对总线要么被钳在低电平要么一直是高电平通信必然失败。2.2 40位数据的排列和校验规则DHT11一次返回40位数据高位在前顺序如下8位湿度整数部分8位湿度小数部分DHT11的小数位实际通常为08位温度整数部分8位温度小数部分8位校验和校验和的计算规则前四个字节求和取低8位如果和第五个字节相等说明这一帧数据有效。举个例子假设当前湿度是45%RH温度是26摄氏度那么前四个字节就是0x2D、0x00、0x1A、0x00相加等于0x47校验字节应该就是0x47。温度负值的处理是个容易忽略的细节。DHT11在零下温度时温度整数部分的最高位会置1比如-10度会表示为0x8A。接收端需要判断这个符号位去掉0x80后再取数值否则零下温度直接显示成一百多度特别容易让人误判是传感器坏了。这个细节藏得比较深我第一次在户外测试时就中招了后来查资料才弄明白。2.3 从协议参数到FPGA设计约束把协议参数映射到FPGA设计之前先确定系统时钟。EP4CE10开发板最常见的时钟是50MHz周期20ns。如果直接用50MHz计数产生18ms延时需要数到900000用一个20位计数器刚好能覆盖。但更推荐的做法是先用系统时钟产生一个1us的时钟使能脉冲再基于这个脉冲做毫秒级和微秒级延时。代码结构会更清晰也方便后续做通用化封装。协议里有两个时间点需要格外小心。一个是起始信号的18ms它决定DHT11是否被唤醒另一个是数据位中26到28us和70us的区分它决定每个数据位解析成0还是1。如果采用“延时一半再采样”的方法判断数据位采样点取在40us附近比较合适因为0的高电平在28us左右已经结束而1的高电平能持续到70us附近40us正好能区分两者。更稳妥的做法是直接测量高电平宽度——用计数器统计从上升沿到下降沿的时长再根据阈值判断0或1。两种方案我都在后面代码部分展开。另一个约束是读取频率。DHT11内部实际测量周期约2秒如果读取得太频繁传感器来不及更新数据返回的是旧值甚至无响应。所以状态机里通常要做一个大于1秒的空闲间隔或者由外部控制模块每隔1.5秒发起一次读取。这个看起来不起眼的约束在调试时最容易让人误以为代码逻辑出了问题实际上等待时间不够而已。3. Verilog核心实现细节3.1 模块划分与端口定义我把这个工程拆成两个模块。顶层模块dht11_top负责端口定义、inout引脚处理和子模块例化底层的dht11_ctrl实现完整的DHT11时序状态机数据读取完成后以16位并行数据输出。显示部分直接例化开发板自带的数码管控制器不属于DHT11驱动的核心内容这里不做展开。顶层模块的Verilog代码module dht11_top( input clk_50m, input rst_n, inout dht11_data, output [5:0] seg_sel, output [7:0] seg_data ); wire [15:0] temp_humi_data; wire data_valid; dht11_ctrl u_dht11( .clk_50m (clk_50m), .rst_n (rst_n), .dht11_data (dht11_data), .temp_humi_data (temp_humi_data), .data_valid (data_valid) ); // 数码管显示模块例化省略 endmoduledht11_ctrl模块对外输出的16位数据高8位存温度整数低8位存湿度整数显示模块拿到之后直接做BCD转换就能显示。data_valid信号用来通知下游这组数据已经更新下游模块可以用它做锁存避免在数据更新过程中读到半个新值半个旧值。3.2 状态机怎么设计DHT11驱动状态机的核心任务是把上一节的时序流程映射成离散的状态跳转。我定义的状态如下localparam ST_IDLE 4d0; localparam ST_START_LOW 4d1; localparam ST_START_HIGH 4d2; localparam ST_RESP_LOW 4d3; localparam ST_RESP_HIGH 4d4; localparam ST_READ_DATA 4d5; localparam ST_CHECK 4d6; localparam ST_DONE 4d7;ST_IDLE状态做两件事一是等待读取触发信号二是做一段足够长的空闲延时确保DHT11完成内部测量。ST_START_LOW把总线拉低20msST_START_HIGH释放总线并等待20到40us。ST_RESP_LOW和ST_RESP_HIGH分别对应检测DHT11响应信号的拉低、拉高阶段。ST_READ_DATA进入40位数据的逐位读取循环ST_CHECK对收到的40位数据做校验和验证ST_DONE把数据锁存到输出寄存器。设计原则是每个状态只做“拉电平”、“等时间”、“采样数据”中的一件事时间一到立刻跳转。如果某个状态承担了过多职责代码写起来容易乱后面调试也难定位。想让状态机一次跑通基本靠这条原则撑着。3.3 双向IO处理与微秒计数器DHT11的data引脚在Verilog里必须声明为inout。FPGA内部一般用一个方向寄存器dir_reg动态选择输出或高阻代码写法如下reg dir_reg; assign dht11_data dir_reg ? 1b0 : 1bz; assign dht11_in dht11_data;dir_reg为1时FPGA主动把总线拉低用于发送起始信号dir_reg为0时FPGA释放总线dht11_data变成高阻z外部上拉电阻把总线拉回高电平。此时DHT11可以继续拉低或释放总线FPGA通过dht11_in读取当前电平。注意inout引脚在FPGA内部不能直接接到普通逻辑必须先经过三态缓冲再接到内部信号上面两行就是标准的三态缓冲处理方式。1us计时器是整套延时的基础。50MHz时钟下计数50次就是1us写一个使能脉冲reg [5:0] cnt_us; wire us_pulse; always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin cnt_us 6d0; us_pulse 1b0; end else if (cnt_us 6d49) begin cnt_us 6d0; us_pulse 1b1; end else begin cnt_us cnt_us 1b1; us_pulse 1b0; end end有了us_pulse之后毫秒级延时用另一个16位计数器实现。每次us_pulse到来加1计数值到20000就代表20ms。16位计数器最大能计65535us覆盖DHT11起始信号所需的20000us绰绰有余。ST_IDLE的空闲间隔需要1.5秒左右这个用18位计数器计到1500000即可。3.4 数据位读取的两种方案读取40位数据是整个驱动里最容易出错的部分。我实测过两种方案定时采样法和高电平测宽法。定时采样法逻辑简单——在每一位数据起始时等待50us低电平结束检测到上升沿后延时30到40us然后在总线还在高电平区间时做一次采样。采到高电平就是1总线已经回到低电平就是0。做法直观但缺点在于采样点是否合适完全取决于DHT11的时序一致性。我手头有一批DHT11模块位0的高电平有时会超过30us在40us采样点处总线还处于高电平0被误判成1导致校验和错误频出。高电平测宽法的思路不同在数据位上升沿启动计数器下降沿锁存计数值直接测出高电平持续时间再根据阈值判断0或1。代码比定时采样法多十几行但对时序的容错能力强很多。我最终用测宽法换了几批DHT11都没再出现误判。核心逻辑示意// 在数据位检测状态中 if (bit_start) begin width_cnt 0; end else if (dht11_in) begin width_cnt width_cnt 1; end else begin if (width_cnt 40) // 高电平超过40us判为1 r_shift_reg {r_shift_reg[38:0], 1b1}; else if (width_cnt 0) r_shift_reg {r_shift_reg[38:0], 1b0}; end这里的width_cnt以us为单位40是0和1的判定阈值。因为位0高电平26到28us位1高电平大约70us阈值设在40us位置能稳定区分。实际代码里width_cnt的位宽要留够至少8位因为70us需要计到70。3.5 校验、负温度处理和输出锁存40位数据全部收完后把移位寄存器拆成五个字节前四个字节相加后跟校验字节比较。校验通过就把结果打一拍输出校验失败则丢弃这组数据状态机回到IDLE等待下一次读取。wire [7:0] humi_int r_shift_reg[39:32]; wire [7:0] humi_dec r_shift_reg[31:24]; wire [7:0] temp_int r_shift_reg[23:16]; wire [7:0] temp_dec r_shift_reg[15:8]; wire [7:0] check_sum r_shift_reg[7:0]; always (posedge clk_50m or negedge rst_n) begin if (!rst_n) temp_humi_data 16d0; else if (check_pass) temp_humi_data {temp_int[6:0], humi_int}; end这里输出时把temp_int的最高位去掉了因为如果DHT11返回负数温度最高位是符号位表示数值的部分只有低7位。实际显示模块再做一次判断如果原始temp_int最高位为1就在最前面加一个负号。十六位数据里高8位是去符号后的温度低8位是湿度方便后续处理。4. 仿真验证不可跳过4.1 用testbench模拟DHT11行为FPGA开发里仿真这一步不能省外设驱动更是如此。没有仿真就第一次上板跑时序逻辑一旦出错你根本分不清是FPGA代码问题还是硬件接线问题。我先在testbench里用行为模型模拟DHT11的时序把FPGA侧的状态机整个跑通。testbench的DHT11模型分两部分响应起始信号和发送40位数据。响应起始信号的逻辑是检测到FPGA拉低总线超过18ms后释放总线再过20us左右模型把总线拉低80us再拉高80us模拟DHT11的响应脉冲。数据发送按每个数据位50us低电平加对应高电平的方式循环40次。DHT11模型的核心结构task send_bit(input bit val); begin dht11_data 1b0; // 拉低50us #50; dht11_data 1bz; #(val ? 70 : 26); // 高电平持续时间 end endtask把数据位封装成task之后在循环里逐位调用就行。testbench里可以预设一组温湿度值比如湿度45%RH、温度26度然后把对应的40位数据按顺序送到总线上验证FPGA模块能否正确解析出这组数据。4.2 仿真里最容易踩的两个坑第一个坑是双向IO的仿真模型跟真实器件差异。真实DHT11靠开漏输出拉低总线释放后由上拉电阻拉高所以模型里拉低用dht11_data 1b0释放用dht11_data 1bz。如果把释放写成了1b1仿真结果看起来一切正常但真实器件根本不会主动输出强高电平上板立刻翻车。看testbench代码时要特别注意凡是模拟开漏器件释放一定要写高阻。第二个坑是仿真时间太长。起始信号20ms加上数据位传输一次完整握手大约22ms单次仿真不算长。但如果testbench里还加了个1.5秒的空闲延时那仿真时间就直奔分钟级去了。解决方法是把延时参数做成parameter或define仿真时改小到1ms综合前改回1.5秒。也可以专门加一个sim_mode引脚仿真时把内部计数阈值统一除以1000。4.3 仿真波形关注哪些信号跑完仿真后wave窗口里重点检查这几点起始信号释放后是否检测到了上升沿DHT11响应信号的低电平时长是否在80us附近40位数据是否按预期出现在移位寄存器里校验信号是否按时拉高。把dht11_data、current_state、shift_reg这几个信号拖到波形栏就能直观看到状态机每一步跳转和数据位的组装过程。把状态和数据线跳变对应起来看是学习状态机设计最直观的方式。ST_START_LOW状态下dht11_data持续拉低20ms切到ST_START_HIGH后变成高阻z再等到DHT11拉低响应状态机进入ST_RESP_LOW。整个流程和协议时序图完全一致。仿真通过后再上板心里就有底了。5. 上板调试与实测记录5.1 硬件连接和引脚分配上板前先把硬件连接确认一遍。DHT11的VCC接3.3VGND接GNDDATA接FPGA的某个IO脚。建议在DATA和VCC之间加一个4.7k上拉电阻成品模块很多内置了上拉但我还是习惯自己飞一个省得在不同模块之间换来换去时出问题。FPGA侧在Quartus的Pin Planner里要把对应引脚电平标准设置成3.3V-LVTTL或3.3V-LVCMOS和DHT11的3.3V电平匹配。上电之后先用示波器或逻辑分析仪看DATA线的空闲电平正常情况下应该被上拉到3.3V高电平。如果空闲电平异常先查接线和上拉电阻别急着怀疑代码。等确认数据线空闲电平正常了再下载bitstream测试这样能省下不少排查时间。5.2 首次上板最常出现的现象第一次上板最常见的现象是数据一直是0xFF或者固定不变。这类问题八成出在起始信号上。用逻辑分析仪抓FPGA输出端的波形看是否真的拉低了20ms。如果拉低时间不够18msDHT11根本不会被唤醒自然不会有响应。我之前就在这个参数上栽过——起始信号只用了10000us而不是20000us结果数据读不到改成20000us之后一次就通了。另一个常见现象是读到的数据校验和错误。这种情况基本出在数据位采样上。前面讲过定时采样法很容易卡在0和1的边界误判尤其DHT11时序一致性差一些时。解决方式就是把数据位读取改成高电平测宽法或者在定时采样时把采样点从40us往后挪到50us附近。实测下来测宽法最稳定换了几个不同批次的DHT11都没再出现误判。5.3 实测数据稳定性处理工程调通后我连续跑了一整晚每隔1.5秒采集一次温湿度数据观察长时间运行的稳定性。结果发现偶发跳变仍然存在比如湿度从45%突然跳到70%又跳回来。单独看校验逻辑每次都通过说明数据本身没有传输出错更像是DHT11读数偶发异常。后来我在状态机里加了一个简单的滤波连续读取三次取中间值作为有效数据输出。具体做法是保存最近三次有效测量结果排序后取中间值送出。加了这个逻辑之后跳变现象基本消失。虽然DHT11精度本身有限但这种软件滤波思路在FPGA内部做成本很低不占用额外CPU资源值得保留。5.4 常见问题速查表现象可能原因解决思路数据全部为0xFFDHT11未响应检查起始信号是否大于18ms、上电后等待时间是否足够、上拉电阻是否接好数据全部为0x00总线一直为低检查IO方向控制确认FPGA是否一直处于输出拉低状态偶发校验错误数据位采样点不准改用高电平测宽法增强对时序偏差的容忍度数据能读但周期跳变DHT11自身测量误差连续采样取中值滤波温度显示一百多度负温度符号位没处理检查温度整数最高位本文还有配套的精品资源点击获取
返回列表