
简介面向FPGA学习者的DHT11温湿度传感器读写完整工程基于Cyclone IV E系列EP4CE10F17C8器件Quartus 18.0环境开发实现单总线时序驱动、按键切换温度/湿度显示以及数码管动态扫描。工程采用模块化设计顶层top_dht11例化了dht11_drive、key_debounce、dht11_key和seg_led四个子模块代码注释清晰便于理解单总线协议、状态机跳转和动态显示原理适合FPGA入门者作为综合练习也适合工程师快速移植复用。资源包共289个文件主体为Quartus工程及编译中间文件含5个Verilog源文件.v、大量tdf工程文件以及qpf、qsf等工程配置文件另有编译报告与配置文件可供参考压缩包大小6.19MB。目前已有1369人学习下载。获取后可在Quartus 18.0中直接打开重建工程结合EP4CE10开发板验证温湿度采集通过按键切换显示当前湿度或温度能有效缩短DHT11时序调试时间。1. 从零写DHT11的Verilog驱动一次把单总线时序吃透DHT11温湿度传感器在嵌入式圈子里几乎人人用过但大多数人用的是STM32的HAL库或者Arduino库到FPGA上就换了一套玩法。这个Quartus工程把DHT11的完整读写流程用纯Verilog实现了一遍目标平台是Cyclone IV E的EP4CE10F17C8配套的数码管显示模块和按键切换逻辑也一应俱全。对于想理解单总线协议本质、或者需要在FPGA上对接廉价温湿度传感器的工程师来说这套源码的价值在于启动信号、响应判断、40位数据读取的每一个tick都有明确的计数约束而不是靠延时函数蒙混过关。下文按模块拆解读写时序、按键消抖和数码管动态扫描方案并结合Quartus 18.0实际编译环境把引脚约束与调试手段一并说清楚。2. DHT11单总线时序拆解与Verilog驱动状态机实现2.1 单总线协议的时间参数与读数据模型DHT11只有一根数据线主机和传感器之间靠高低电平的持续时间传递信息。主机发起通信时先拉低总线至少18ms然后释放由外部上拉电阻把总线拉回高电平。传感器检测到这个跳变序列后输出80us低电平和80us高电平作为应答随后发送40位数据。每一位都以50us低电平开始如果后面跟26~28us高电平表示逻辑0跟70us左右高电平表示逻辑1。单总线读时序的难点不在理解这几个数字而在于FPGA的时钟是连续运行的你不能像单片机那样靠delay函数精确等一个脉冲必须把“在哪个时间点采样”换算成计数器边界。提示DHT11规格书中给出的时间参数允许一定偏差实际驱动里只要保证拉低时间超过18ms、采样点落在位周期中段就能可靠读取。在这个工程里系统时钟是50MHz对应周期20ns。1us的延时就是50个时钟周期1ms就是50000个周期。dht11_drive模块用一个大计数器配合状态机推进把ms级启动、us级应答和位级采样全部落在同一个计数基准上避免多个模块各写一套延时逻辑导致时序漂移。2.2 dht11_drive模块的状态机划分与三态IO处理驱动模块的状态机分为五个阶段空闲、拉低启动、释放等待应答、读取40位数据、输出完成。每个状态内部都有一个独立计数器状态跳转条件根据计数器和总线电平共同决定。先看模块的端口和三态IO骨架这是整个驱动的基础module dht11_drive #( parameter CNT_1US 6d50, // 50MHz时钟下1us计数 parameter CNT_1MS 16d50_000 // 50MHz时钟下1ms计数 )( input sys_clk, input rst_n, inout dht11, output reg [31:0] data_valid ); localparam IDLE 5b0_0001; localparam START_LOW 5b0_0010; localparam WAIT_RESP 5b0_0100; localparam READ_BIT 5b0_1000; localparam DONE 5b1_0000; reg [4:0] state; reg [31:0] cnt; reg [5:0] bit_cnt; reg [39:0] data_buf; reg dht11_out; reg dht11_oe; assign dht11 dht11_oe ? dht11_out : 1bz;dht11_oe是输出使能信号高电平时FPGA主动驱动总线低电平时总线释放为高阻态。inout端口如果漏掉1bz分支综合器会把它当作普通输出端口处理读方向永远拿不到传感器拉低电平的数据这是单总线在FPGA上最常见的错误之一。还要注意dht11_out在释放总线期间要保持1否则释放瞬间总线会被瞬时拉低产生一个假起始沿。2.3 启动与应答阶段的计数设定启动阶段要把总线在START_LOW状态保持20ms也就是从IDLE进入后计数器从0数到CNT_1MS的40倍。释放总线后进入WAIT_RESP状态此时开始检测下降沿。这里有一个工程细节总线释放后FPGA引脚输入的电平不是立刻稳定的需要在模块入口加两级寄存器同步既消除亚稳态也避免在电平翻转的当拍误判边沿。reg [1:0] dht11_sync; wire dht11_in; assign dht11_in dht11_sync[1]; always (posedge sys_clk or negedge rst_n) begin if (!rst_n) begin dht11_sync 2b00; end else begin dht11_sync[0] dht11; dht11_sync[1] dht11_sync[0]; end end检测到应答低电平后计数器清零等80us后再采一次电平确认应答信号的低电平宽度符合规格。如果宽度偏差超过20us说明传感器没有正常启动状态机要回到IDLE重新发起通信而不是继续读位。这个超时判断在真实硬件上很有用DHT11上电后需要1秒稳定时间如果FPGA配置完成后立刻读传感器很可能不响应。2.4 40位数据逐位采样与校验和验证进入READ_BIT状态后bit_cnt从0到39循环。每一位的流程是检测到上升沿即50us低电平结束后计数器清零在40us处采样总线电平。采样点定在40us而不是高电平结束再判断是因为逻辑0的高电平只有26~28us逻辑1的高电平约70us在40us处逻辑0已经回到低电平逻辑1还在高电平两者区分度最大。数据位采完后拼成40bit的临时缓冲再按字节拆分。DHT11的数据格式是湿度整数、湿度小数、温度整数、温度小数、校验和每字节8位高位在前。校验和等于前四个字节相加取低8位等校验通过再把数据搬运到data_valid输出wire [7:0] check_sum data_buf[39:32] data_buf[31:24] data_buf[23:16] data_buf[15:8]; always (posedge sys_clk or negedge rst_n) begin if (!rst_n) data_valid 32d0; else if (state READ_BIT bit_cnt 6d40) begin if (check_sum data_buf[7:0]) begin data_valid[31:24] data_buf[39:32]; // 湿度整数 data_valid[23:16] data_buf[31:24]; // 湿度小数 data_valid[15:8] data_buf[23:16]; // 温度整数 data_valid[7:0] data_buf[15:8]; // 温度小数 end end end对比一下常见误用有人直接把40位数据不加校验就送显示一旦总线有毛刺显示值就会乱跳。校验和能过滤掉大部分单bit翻转错误但如果是接线不良导致的连续多位错误校验失败后应该保留上一次正确的数据而不是把0值塞进显示模块。我在驱动里加了一个有效标志只有校验通过才刷新data_valid思路同样适用于其他单总线传感器。3. 按键消抖与温湿度显示切换的Verilog实现3.1 机械按键的抖动模型与消抖策略选型DHT11驱动读回来的数据同时包含湿度和温度板上只有一个按键用来切换数码管显示内容。机械按键按下和释放时触点会经历数ms的不稳定弹跳直接采样会在一个按键动作里产生多个上升沿或下降沿显示状态就会连续切换好几次。工程里的key_debounce模块采用“持续低电平计数”方案检测到下降沿后开始计数只有当低电平持续超过20ms才判定为一次有效按下否则认为是一次抖动并把计数器清零。选型上还可以用状态机消抖和移位寄存器消抖但这个单按键场景里计数方案的代码量最少时序也直观。20ms的阈值根据常见轻触开关数据手册而定实际如果按键老化可以调到50ms代价是按键响应延迟变大对切换显示这种操作完全感知不到。3.2 key_debounce模块实现与参数说明module key_debounce #( parameter CNT_MAX 20d999_999 // 50MHz时钟下20ms )( input sys_clk, input sys_rst_n, input key, output reg key_flag, // 有效按下标志单周期脉冲 output reg key_value // 消抖后的按键电平 ); reg [19:0] cnt; reg [1:0] key_sync; always (posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) key_sync 2b11; else begin key_sync[0] key; key_sync[1] key_sync[0]; end end wire key_negedge key_sync[1] ~key_sync[0]; always (posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) begin cnt 20b0; key_flag 1b0; key_value 1b1; end else if (!key_sync[0]) begin if (cnt CNT_MAX) begin key_flag 1b1; key_value 1b0; end else begin cnt cnt 1b1; key_flag 1b0; end end else begin cnt 20b0; key_flag 1b0; key_value 1b1; end end endmodulekey_sync两级寄存器把外部异步信号同步到系统时钟域同时通过组合逻辑产生key_negedge下降沿脉冲。按下期间key_flag只在计数达到20ms的那一拍拉高一次不会持续拉高下游的dht11_key模块用key_flag做触发就不会误触发。key_value在按键释放后恢复高电平反映按键当前的稳定状态。这里有个细节CNT_MAX取值999999对应50MHz时钟下的20ms如果把工程移植到25MHz或100MHz时钟下这个参数必须同步调整否则消抖时间会变成10ms或40ms。常见错误是改时钟不改参数导致按键偶尔失灵。3.3 dht11_key模块的数据选择与符号处理dht11_key模块把data_valid的32位数据拆开默认显示湿度按一次按键切换到温度再按切回湿度。切换信号用key_flag作为触发而不是直接拿key_value做组合选择否则按键按住不放时显示内容会反复跳动。温度数据存在负数场景DHT11温度低于0℃时温度整数部分的最高位为1。数码管显示模块如果不处理符号位会把负温度显示成很大的正数。工程里单独引出了sign信号在dht11_key中把温度整数最高位解析出来reg temp_humi_sel; always (posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) temp_humi_sel 1b0; // 默认显示湿度 else if (key_flag) temp_humi_sel ~temp_humi_sel; end always (*) begin if (!temp_humi_sel) begin // 湿度显示整数部分在高16位小数部分在低16位 data[19:16] 4h0; data[15:12] data_valid[31:28]; // 湿度十位 data[11:8] data_valid[27:24]; // 湿度个位 data[7:4] data_valid[23:20]; // 湿度十分位 data[3:0] data_valid[19:16]; // 湿度百分位 point 6b001000; sign 1b0; end else begin // 温度显示同样拆成4位BCD码 data[19:16] 4h0; data[15:12] data_valid[15:12]; data[11:8] data_valid[11:8]; data[7:4] data_valid[7:4]; data[3:0] data_valid[3:0]; point 6b001000; sign data_valid[15]; // 负数标志 end enddata位宽是20位正好对应五位数码管再加上符号位和单位指示六位数码管刚好够用。实际工程中建议先把字节拆成4位BCD码再送显示如果直接用二进制数做段码映射译码逻辑会很绕。point信号用来点亮需要的小数点位DHT11的湿度精度是1%温度精度也是1%小数点后一位就够第二位在数码管上保留或者熄灭都可以。4. 动态数码管显示与Quartus工程顶层集成4.1 动态扫描显示原理与刷新频率计算六位数码管动态扫描的原理是利用人眼视觉暂留效应同一时刻只点亮一位按固定频率循环扫描各位当刷新频率高于50Hz时人眼看到的就是所有位同时亮起。工程里设计刷新频率约1kHz每位停留1ms扫描一轮6ms对应166Hz刷新率不存在闪烁感。seg_led模块内部用计数器产生位选信号sel每隔1ms切换一次同时根据当前位号从data里取出对应的BCD码经过七段译码后输出到seg_led。选择1ms作为位切换周期是实践中的折中太短则每个数码管点亮时间不足亮度偏低太长则肉眼能分辨出扫描过程出现拖影。如果你的开发板用的是大尺寸数码管发光效率低可以适当延长到2ms刷新率仍有83Hz人眼依然感觉不到闪烁。4.2 seg_led模块的译码与位选实现seg_led内部包含一个BCD到七段码的译码逻辑以及动态扫描的位选计数。代码骨架如下module seg_led ( input clk, input rst_n, input [19:0] data, // BCD码每4位表示一位数字 input [5:0] point, // 小数点位置 input en, input sign, output reg [5:0] seg_sel, // 位选 output reg [7:0] seg_led // 段选含小数点 ); reg [15:0] cnt_scan; reg [5:0] scan_pos; always (posedge clk or negedge rst_n) begin if (!rst_n) cnt_scan 16b0; else if (cnt_scan 16d49_999) begin cnt_scan 16b0; scan_pos (scan_pos 6b100000) ? 6b000001 : {scan_pos[4:0], 1b0}; end else cnt_scan cnt_scan 1b1; end计数到49999后清零并移位位选信号每一位点亮时间为1ms。后面的组合逻辑根据scan_pos选中数码管并从data里取出当前位要显示的4位BCD码。BCD码到段码的映射用case语句把0~9各自对应的seg_led列出来比如共阴极数码管0对应8h3f1对应8h06如果换成共阳极段码要按位取反这个必须和开发板原理图保持一致否则上板显示全是乱码。4.3 Quartus 18.0工程文件组织与引脚约束整个工程在Quartus 18.0里编译目标器件是Cyclone IV E系列的EP4CE10F17C8。工程根目录下除了顶层v文件和各个模块的.v源文件还有带.bak后缀的备份文件和.ammdb、.bpm这类Quartus自动生成的中间文件。.qsf文件记录器件型号、引脚分配和综合选项.sdc时序约束文件约束时钟。从压缩包解压后直接打开.qpf工程文件即可但如果换了一张同型号开发板必须重新核对引脚分配。下面是一份典型的引脚分配表具体网络号要以开发板原理图为准信号名FPGA引脚电平标准说明sys_clkPIN_E13.3-V LVTTL50MHz有源晶振sys_rst_nPIN_E163.3-V LVTTL复位按键dht11PIN_A143.3-V LVTTLDHT11数据线需外接4.7k上拉keyPIN_B143.3-V LVTTL切换显示按键sel[5:0]PIN_C1 ~ PIN_C63.3-V LVTTL数码管位选seg_led[7:0]PIN_D1 ~ PIN_D83.3-V LVTTL数码管段选DHT11的数据线必须接一个4.7k~10k的上拉电阻这是单总线通信的基础。如果开发板上已有上拉直接接GPIO即可如果自己搭电路上拉电阻接到3.3VEP4CE10的普通IO是3.3V电平直接与3.3V供电的DHT11模块相连不需要电平转换。4.4 顶层模块例化与信号互联顶层top_dht11把四个子模块例化并连接起来。文件列表里每个模块都有对应的.v源文件.bak后缀是编辑器备份不影响编译。顶层的关键是data_valid、data、point等信号的位宽和连接关系一一对应任何一位连接错误在仿真里很难发现但上板后显示数据会错位。从项目里摘出来的顶层连接关系就是文章开头那段代码这里不再重复贴整个文件。需要注意dht11这个端口在顶层仍然声明为inout子模块的inout和顶层端口连接不需要额外处理但编译后要在Pin Planner里确认dht11引脚被设置为双向引脚否则FPGA内部的三态缓冲不会生效。5. SignalTap实测DHT11时序的三个排错技巧5.1 用SignalTap II抓取总线波形代码编译烧录后如果数码管没有反应第一步不是改代码而是用Quartus自带的SignalTap II逻辑分析仪抓取dht11总线和data_valid。在SignalTap配置里添加dht11_sync、state、bit_cnt三个内部信号采样时钟选50MHz系统时钟触发条件设为state READ_BIT。这样可以清楚看到传感器是否回了应答、每一位高电平时长是否在规格内。如果state在WAIT_RESP和IDLE之间反复跳说明传感器一直不应答优先查上拉电阻和接线如果state能进入READ_BIT但bit_cnt卡在某个值不动说明某一位低电平没有按时结束通常是信号线接触不良或干扰过大。5.2 上电时序与首帧失败的处理DHT11上电后需要1秒稳定时间之后才能接受主机指令。很多人在FPGA配置完成后立刻发起读取结果第一帧永远是失败的。可以在dht11_drive的IDLE状态加一个上电延时计数等cnt计数到1秒后再进入启动状态。处理后上电到数码管出现稳定数值时间会延长一点但不再出现首帧失败。5.3 多帧校验与波形核对建议连续读三帧至少两帧校验通过才更新显示校验失败时保留上一帧正确数值。这个方法成本很低但能显著减少显示跳动。如果手头有示波器可以直接观测dht11引脚波形对照协议时间参数逐项核对定位是主机拉低时间不够还是传感器应答异常。SignalTap抓取深度默认1K采样点抓完40位数据大约需要100us配置成2K深度就够了。DHT11本身精度不高±2℃和±5%RH的误差在多数室内监测场景够用。后续要接更精确的SHT30或AHT21这套驱动框架稍作修改就能复用核心的inout三态控制和状态机划分思路是相通的。本文还有配套的精品资源点击获取