
简介一套基于FPGA的PM2.5与温湿度传感器实时监测系统设计资料面向嵌入式/FPGA开发者和电子类课程设计人员。系统采用Verilog HDL实现涵盖PM2.5传感器数据采集、DHT系列温湿度解析、液晶1602驱动与顶层集成模块完整展示从传感器接口配置、ADC采样转换到显示时序控制的设计流程。压缩包共1976个文件以Quartus工程数据库文件cdb/hdb、Verilog源码.v及辅助脚本/文档为主整体约16.49MB便于直接打开工程进行仿真与验证。已有645人学习或下载。资料不仅提供可直接编译的工程结构还包含综合、仿真相关文件与备份版本可帮助读者快速复现环境监测平台并用于空气质量研究、智能家居、工业监控等场景的硬件原型开发。1. 用 FPGA 做 PM2.5 与温湿度采集为什么先别急着选单片机做环境监测板卡那会儿我拿到过两次几乎一样的需求采集 PM2.5 浓度和温湿度把数据送上位机。第一次我用单片机一周交差第二次客户追加了“两个传感器同时跑、数据要滤波、异常点要剔除”单片机串行时序立刻吃紧。把整套系统设计搬到 FPGA 上之后verilog 状态机并行处理传感器时序、ADC 采样和 UART 上送稳定性完全不一样。这篇文章把完整的拆解过程写出来从传感器接口、Verilog 核心模块、帧协议到上板调试适合做毕设、课设或者想把手头 FPGA 用起来做数据采集的工程师直接对照落地。2. 传感器接口与选型DHT11 单总线时序和 PM2.5 模拟电压通道2.1 选型逻辑为什么是 DHT11 加 GP2Y1010AU0F温湿度传感器我选了 DHT11PM2.5 传感器选了夏普 GP2Y1010AU0F。选这两个不是因为它俩性能多顶尖而是因为接口类型互补DHT11 走单总线数字协议GP2Y1010 输出模拟电压。一个 FPGA 工程里同时覆盖两类传感器接口后面换 SHT30、攀藤 PMS5003 这类器件时只需要替换对应驱动模块系统骨架不用动。DHT11 的精度是 ±2℃、±5%RH刷新速率 1Hz 左右。做机房、仓库这类环境监测足够但注意它没有真正的“小数位”小数字节返回 0别被数据手册里那一长串格式说明带偏。GP2Y1010AU0F 的粉尘检测范围是 0~0.5mg/m³工作电压 5V典型静态输出电压 0.9V 左右浓度越高电压越高最高能到 3.6V 左右。这里有个容易忽略的点它的输出电压不能直接进 FPGA 的 GPIO必须经过 ADC 转成数字量还要考虑传感器本身 5V 供电和 FPGA 3.3V bank 之间的电平关系。选型决策表可以这样列项目DHT11GP2Y1010AU0F输出类型单总线数字模拟电压 0.9~3.6V供电电压3.3V~5V5VLED 驱动脚独立采样周期≥1s10ms 周期320μs 脉冲驱动FPGA 侧接口三态 IO 上拉外部 ADC 或板载 ADC典型精度±2℃ / ±5%RH换算后 ±10μg/m³ 量级接线上要注意三点DHT11 数据线上必须有一个 5kΩ 左右的上拉电阻否则总线释放后识别不到从机响应GP2Y1010 的 LED 驱动脚要接一个三极管或 MOS 开关由 FPGA 产生周期脉冲不能直接拿 IO 硬驱动ADC 的基准电压决定了浓度换算系数建议用 3.3V 基准而不是 5V能把 0.9~3.6V 的信号压缩到量化区间内。2.2 DHT11 时序拆解高低电平宽度才是判断依据DHT11 的单总线时序网上一搜一大把但真正写 verilog 时容易翻车的只有两个点起始信号宽度不够、位判断阈值取错。完整时序流程拆开是这样的总线空闲时为高电平主机拉低不少于 18ms然后释放总线从机响应先把总线拉低约 80μs再拉高约 80μs随后输出 40bit 数据每一位都是先拉低 50μs然后拉高高电平持续 26~28μs 表示逻辑 0持续约 70μs 表示逻辑 1数据发完总线恢复高电平等待下一次起始。把这段流程转成 Verilog 状态机时核心是把微秒计数器和阈值判断分开。我习惯用 1μs 节拍做所有延时基准位判断时记录高电平宽度用两个阈值小于 30μs 判为 0大于 65μs 判为 1中间区间认为时序异常丢弃这一帧重采。40bit 数据里前 8bit 是湿度整数8bit 湿度小数实际为 08bit 温度整数8bit 温度小数实际为 0最后 8bit 是校验和。校验公式是前四个字节相加取低 8 位。很多初学者把小数位当有效位数用算出来的校验自然对不上这不是时序问题是数据解析位序理解错了。2.3 PM2.5 模拟通道ADC 采样时序和浓度换算GP2Y1010AU0F 的工作时序有一个特殊性它的红外 LED 需要周期性脉冲驱动典型参数是脉冲宽度 320μs周期 10ms。在 LED 脉冲上升沿之后不能立刻采样要等大约 280μs 再采样输出否则会采到 LED 开关带来的尖峰噪声。这个“脉冲-延时-采样”的顺序在 Verilog 里用一个移位计数器就能实现。// PM2.5 LED 脉冲与 ADC 采样节拍生成 module pm25_sample_ctrl ( input wire clk, // 50MHz 系统时钟 input wire rst_n, input wire [11:0] adc_data, // 外部 ADC 转换结果 output reg led_pulse,// LED 驱动脉冲 output reg [11:0] pm25_raw // 采样原始值 ); reg [18:0] cnt; // 0~499999 对应 10ms reg sample_en; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 0; led_pulse 1b0; sample_en 1b0; end else begin cnt cnt 1; // 10ms 周期320us 高电平脉冲 led_pulse (cnt 16_000) ? 1b1 : 1b0; // 脉冲后延时 280us 采样 sample_en (cnt[18:0] 14_000) ? 1b1 : 1b0; if (cnt 499_999) cnt 0; end end always (posedge clk or negedge rst_n) begin if (!rst_n) pm25_raw 0; else if (sample_en) pm25_raw adc_data; end endmodule逻辑说明cnt 计数满 499999 对应约 10ms 周期。led_pulse 在前 16000 个周期拉高就是 320μs 脉冲在第 14000 个周期产生 sample_en 采样使能保证采样点落在 LED 脉冲后段且避开了开关沿。参数说明16_000 和 14_000 这两个数不是拍脑袋定的按 50MHz 时钟1μs 等于 50 个计数。320μs 就是 16000280μs 就是 14000。如果换 100MHz 时钟所有参数翻倍。采样后的 adc_data 是电压数字量换算浓度通常不在 FPGA 里做交给上位机按公式处理更灵活FPGA 只负责把原始 ADC 值和滤波结果一起打包上送。3. Verilog 核心模块落地分频、状态机与滑动窗口滤波3.1 时钟分频与定时基准所有时序从 1μs 节拍派生FPGA 板卡上常见 50MHz 主时钟而 DHT11 时序、PM2.5 脉冲周期都是微秒甚至毫秒级直接拿 50MHz 计数会很别扭。我一般先做一个 1μs 脉冲发生器把它作为全工程的节拍基准其他延时都用“多少个 tick”来表达代码可读性和可维护性都会好很多。// 50MHz 时钟产生 1us 节拍脉冲 module clk_1us_pulse ( input wire clk, // 50MHz 系统时钟 input wire rst_n, // 低电平复位 output reg tick_1us // 高电平持续一个时钟周期 ); reg [5:0] counter; parameter CNT_MAX 50; // 50MHz - 1us 计数 50 次 always (posedge clk or negedge rst_n) begin if (!rst_n) begin counter 0; tick_1us 1b0; end else begin if (counter CNT_MAX - 1) begin counter 0; tick_1us 1b1; end else begin counter counter 1; tick_1us 1b0; end end end endmodule逻辑说明counter 从 0 计到 49第 50 个时钟周期拉高 tick_1us之后清零。tick_1us 只持续一个时钟周期本质上是一个 enable 信号。后续模块在 always 块里判断 if (tick_1us) 才更新计数器就能实现微秒级计时。参数说明CNT_MAX 是唯一需要改的参数。系统时钟 50MHz 时是 50100MHz 时改成 100。如果时钟不是整数关系比如 60MHz建议先用 PLL 整到 100MHz 再做分频别在计数器里算小数边界判断容易出玄学问题。这也是 FPGA 开发里最常见的低级坑分频系数没有整除导致微秒周期忽长忽短。有了 tick_1us 之后18ms 延时就是从 0 数到 18000。帧周期、采集周期、串口波特率都可以从这个基准派生整个工程的时序逻辑单一来源排错时只需要查一个地方。3.2 DHT11 单总线控制器三段式状态机的标准写法DHT11 控制器的关键就是状态转移时机和双向 IO 控制。我用三段式状态机把状态逻辑、次态判断、输出逻辑分开综合后时序清晰上板后也方便用 ILA 抓内部信号。// DHT11 单总线控制器状态机框架 module dht11_ctrl ( input wire clk, // 50MHz input wire rst_n, inout wire dht_data, // 单总线数据三态 output reg [7:0] humidity, output reg [7:0] temperature, output reg [7:0] checksum, output reg valid ); localparam S_IDLE 3d0; localparam S_START_L 3d1; // 拉低起始 18ms localparam S_RELEASE 3d2; // 释放总线等待响应 localparam S_READ 3d3; // 读 40bit 数据 reg [2:0] state, next_state; reg [19:0] cnt_us; // 微秒计数器 reg [5:0] bit_cnt; // bit 索引 0~39 reg [39:0] data_shift; // 接收移位寄存器 reg dht_out_en; // 输出使能高电平时驱动总线 assign dht_data dht_out_en ? 1b0 : 1bz; // 开漏输出 always (posedge clk or negedge rst_n) begin if (!rst_n) state S_IDLE; else state next_state; end always (*) begin case (state) S_IDLE: next_state S_START_L; S_START_L: next_state (cnt_us 20_000) ? S_RELEASE : S_START_L; S_RELEASE: next_state (cnt_us 40) ? S_READ : S_RELEASE; S_READ: next_state (bit_cnt 40) ? S_IDLE : S_READ; default: next_state S_IDLE; endcase end // 读写逻辑略在 S_READ 里按位采样高电平宽度判断 0/1 endmodule逻辑说明assign 语句实现了三态门dht_out_en 为高时把总线拉低为低时释放总线交给上拉电阻和从机控制。S_START_L 状态把 cnt_us 计到 20000 才会释放留出 2ms 余量应对传感器个体差异。释放后等 40μs 进入读状态。参数说明20_000 这个值比手册要求的 18ms 多了一点实测发现有些 DHT11 器件在 18ms 边界不响应放宽到 20ms 能提高兼容性。S_READ 里每 1μs 采样一次总线电平连续低电平结束后开始计高电平宽度宽度小于 30 判 0、大于 65 判 1。这个阈值需要跟数据手册核对不同批次传感器高电平宽度会有波动把阈值设在 45μs 中间值最安全。3.3 滑动窗口滤波把 PM2.5 毛刺压下去的实用方法PM2.5 传感器输出毛刺比温湿度多得多因为气流、颗粒物随机分布导致 ADC 值跳变是常态。不加滤波直接把原始值送上位机画出来的曲线就是一根“毛线”。我在 FPGA 里做了滑动窗口平均没有用复杂的中值滤波面积和延迟都可控。// 16 点滑动窗口平均用于 PM2.5 ADC 原始值平滑 module sliding_avg #( parameter DATA_W 12, // ADC 位宽 parameter WIN_W 16 // 窗口大小必须是 2 的幂 )( input wire clk, input wire rst_n, input wire [DATA_W-1:0] data_in, input wire data_valid, output reg [DATA_W-1:0] data_out ); reg [DATA_W-1:0] buffer [0:WIN_W-1]; reg [$clog2(WIN_W)-1:0] ptr; reg [DATA_W3:0] sum; // 累加器扩展位数防溢出 always (posedge clk or negedge rst_n) begin if (!rst_n) begin ptr 0; sum 0; end else if (data_valid) begin sum sum data_in - buffer[ptr]; buffer[ptr] data_in; ptr ptr 1b1; end end always (posedge clk or negedge rst_n) begin if (!rst_n) data_out 0; else data_out sum[DATA_W3:4]; // 右移 4 位等价除 16 end endmodule逻辑说明这是增量式环形缓冲。每个 data_valid 周期累加器加新采样值、减最旧采样值然后指针加一。输出取 sum 的高位段右移 4 位相当于除以 16避免了硬核除法器。参数说明WIN_W 必须是 2 的幂这样除法才能用移位替代。累加器位宽在 DATA_W 基础上加了 4 位因为 16 个 12bit 数求和最大需要 16bit我留了余量防止溢出。窗口大小会影响滞后程度PM2.5 是秒级变化16 点窗口在 10ms 采样率下延迟约 160ms完全可接受如果后续接的是 1Hz 慢速传感器窗口改 8 点即可不然曲线会过于迟钝温度变化半天看不出来。4. 数据上送与仿真验证UART 帧协议、Testbench 与波形检查4.1 UART 发送模块波特率误差别忽略采集到的数据最终要送到 PC 上。串口是最省事的路我统一用 115200bps、8 数据位、无校验、1 停止位。这个配置上位机不用额外设置兼容性最好。// UART 发送器115200bps8N1 module uart_tx ( input wire clk, // 50MHz input wire rst_n, input wire [7:0] tx_data, input wire tx_start, output reg txd, output reg tx_busy ); localparam BAUD_CNT 434; // 50MHz / 115200 ≈ 434 reg [8:0] cnt; // 波特率分频计数 reg [3:0] bit_idx; // 当前发送位索引 reg [9:0] shift_reg; // {停止位, 数据, 起始位} always (posedge clk or negedge rst_n) begin if (!rst_n) begin txd 1b1; tx_busy 1b0; bit_idx 0; cnt 0; end else if (tx_start !tx_busy) begin shift_reg {1b1, tx_data, 1b0}; tx_busy 1b1; bit_idx 0; cnt 0; end else if (tx_busy) begin cnt cnt 1; if (cnt BAUD_CNT - 1) begin cnt 0; txd shift_reg[bit_idx]; bit_idx bit_idx 1; if (bit_idx 9) begin tx_busy 1b0; end end end else begin txd 1b1; end end endmodule逻辑说明tx_start 上升沿触发装载把 10bit 串行帧放进移位寄存器。txd 从低位的起始位开始输出每位保持 BAUD_CNT 个时钟周期发完停止位后释放 tx_busy。参数说明BAUD_CNT 434计算方式是 50_000_000 / 115200 ≈ 434.03取整后实际波特率误差约 0.007%。这个误差对 1 字节短帧没有任何影响但如果协议里定义了超过 8 字节的长帧且持续发送误差会累积极端情况下会收到错位数据。处理办法是遇到长帧改成独立计数或直接用 FPGA 内部的 UART IP。4.2 帧协议设计帧头、类型、负载、校验裸发数据容易出问题PC 端不知道哪几个字节是温度、哪几个是湿度。我给这套系统设计了一个轻量帧协议不加多余字段保证上位机解析逻辑简单。字节含义说明0帧头 0xAA同步用1帧头 0x55二次确认2类型0x01 温湿度 / 0x02 PM2.53~6负载温湿度湿度整数/湿度小数/温度整数/温度小数PM2.5ADC 高字节/低字节/滤波值高/低7校验前 7 字节累加和帧结构说明帧头固定 0xAA 0x55 是为了让 PC 端在数据流里快速找到帧边界。类型字段区分来自哪个传感器后续如果扩展到多个测点可以把类型字节拆成高 4 位传感器 ID、低 4 位数据类型。这套协议的特点是固定长度、无转义、累加和够用。为什么不上更复杂的协议因为采集系统每秒只发两帧数据传输压力小出现连续丢帧概率低累加和足够兜底。真要丢帧了上位机做超时重连即可没必要为此引入分包序号和重传机制。4.3 Testbench模拟 DHT11 从机响应和波形检查要点写 Testbench 的核心目标是让 DHT11 控制器把完整流程走一遍。我从网上下过不少现成 TB大多只验证了状态机跳转没验证数据采样正确性。正确做法是在 TB 里写一个 DHT11 行为模型按时序反向驱动总线。// DHT11 行为模型模拟从机响应并发送 40bit 数据 module dht11_model ( inout wire data ); reg [39:0] resp_data; reg driving; task drive_low(input integer us); driving 1b1; data 1b0; repeat (us) #1000; // 1us driving 1b0; data 1b1; endtask initial begin resp_data 40b00010101_00000000_00011001_00000000_00101110; wait (data 1b0); // 检测起始信号 #20_000; // 跳过 18ms 起始低电平 drive_low(80); // 响应低电平 // 按 bit 输出数据每位低 50us高电平宽度区分 0/1 end endmodule逻辑说明模型里 wait 语句检测总线被拉低然后延时 20ms模拟 DHT11 的响应时序。resp_data 中前 8bit 是湿度整数 0x1521%、温度整数 0x1925℃最后 8bit 是校验三字节相加 0x150x000x190x000x2ETB 里要核对校验是否一致。仿真波形里重点看三个点起始信号低电平是否持续足够时间S_READ 状态中采样时刻是否落在每个 bit 的稳定区最终 humidity、temperature 和 checksum 与 resp_data 是否逐位匹配。如果校验不对先看状态机是不是提前把总线释放了让 DHT11 模型根本没进入响应流程。5. 上板调试与常见问题排查约束、毛刺和数值漂移的翻车实录5.1 管脚与时序约束XDC 里必须写清楚的几件事上板之前先做管脚约束这步不能偷懒。我用 Xilinx 平台时XDC 至少要包含时钟管脚、复位按键、LED、DHT11 数据线、ADC 接口、UART 输出。随便写个综合不报错的约束跟没写差不多时序约束才是决定系统能不能稳定跑的关键。set_property PACKAGE_PIN E3 [get_ports clk] set_property IOSTANDARD LVCMOS33 [get_ports clk] create_clock -period 20.000 -name sys_clk [get_ports clk] set_property PACKAGE_PIN F2 [get_ports rst_n] set_property IOSTANDARD LVCMOS33 [get_ports rst_n] set_property PULLUP true [get_ports rst_n] set_property PACKAGE_PIN H2 [get_ports uart_txd] set_property IOSTANDARD LVCMOS33 [get_ports uart_txd] set_property PACKAGE_PIN J3 [get_ports dht_data] set_property IOSTANDARD LVCMOS33 [get_ports dht_data] set_property PULLUP true [get_ports dht_data]逻辑说明create_clock 声明了系统时钟周期 20ns也就是 50MHz这是时序分析的基础。PULLUP true 让 FPGA 内部启用弱上拉如果板子上没有为 DHT11 接 5kΩ 外部上拉这个设置能让总线在释放时保持高电平但可靠性不如外部电阻二选一时优先外部上拉。注意如果 DHT11 用 5V 供电而 FPGA 是 3.3V bank电平兼容是必须解决的问题。我踩过直接把 5V 数据线接 3.3V IO 的坑现象是偶尔能读到数据、大多数时候校验失败。原因不是时序而是开漏结构和 FPGA 输入保护二极管在拉扯电平。后来加了双向电平转换电路问题立刻消失。这个坑在毕设和课设里特别常见因为板子上的 3.3V 和 5V 电源都有学生容易忽略传感器逻辑电平和 FPGA IO bank 电压的匹配。5.2 上板调试顺序先通串口再接传感器上板验证的顺序我建议固定成这样先写一个串口自发自收的测试发固定字符串确认 UART 链路通再只接 DHT11看温湿度读数是否合理最后接 PM2.5 和 ADC看电压曲线是否平滑。每加一个模块验证一次不要一次性把所有传感器接上去再逐个猜问题。综合实现之后先看时序报告里的 setup timing。如果 DHT11 控制器跨时钟域采样有违例最常见的手段是在总线输入管脚打一拍同步去掉亚稳态窗口。ADC 数据如果是异步的也需要同步寄存器。这些在仿真里看不出来只有上板跑起来才会暴露。5.3 常见问题排查现象、原因、解决现象 1DHT11 读出的温湿度一直是 00校验必然不对。原因起始低电平没保持住 18ms 就释放了DHT11 根本没响应。解决把 S_START_L 的计数阈值从 18_000 放宽到 20_000同时检查 dht_out_en 释放后是否真正转成高阻别让输出驱动一直占着总线。现象 2PM2.5 电压跳动范围 ±50mV换算成浓度忽高忽低。原因采样时刻没有避开 LED 脉冲的开关噪声。解决把采样使能延时到 LED 脉冲上升沿之后 280μs并且保证两次采样间隔不小于 10ms。如果还跳检查 ADC 基准电压是否稳定建议在 ADC 参考脚加 0.1uF 退耦电容。现象 3串口上位机帧粘连或者丢字节。原因UART 发送模块还在 busy下一帧数据已经装载进来两个帧挤在一起。解决组帧逻辑必须等到 tx_busy 为低再触发 tx_start上位机接收端加一个 256 字节环形缓冲避免操作系统调度延迟丢数据。现象 4上电第一次读数正常跑几分钟后温度从 25℃ 漂到 35℃。原因DHT11 离板载稳压芯片太近热量直接烤在传感器上。解决把传感器挪到板边或用排线引出别让电路板的局部热点影响测量。这个现象不是 FPGA 代码问题是结构布局问题但最容易迷惑人因为看起来像数据采集逻辑坏了。现象 5仿真完全正常上板后 DHT11 在低温环境读不出来。原因DHT11 的时序参数受温度和湿度影响18ms 起始信号在低温下可能不够。解决起始信号放宽到 20ms位判断阈值从 30μs/65μs 调整为 25μs/70μs扩大判决窗口。牺牲一点判别精度换稳定性值得。6. 板级验证进阶Python 脚本抓串口做 24 小时漂移观测上板能出数据只是开始真正让这套系统设计可信的是长时间稳定性监测。我验证的方法是串口助手配合 Python 脚本做 24 小时跑测脚本按帧协议解析串口数据把 DHT11 温度、湿度、PM2.5 原始 ADC 值和滤波值分别落盘顺手画一张时间曲线。import serial import matplotlib.pyplot as plt ser serial.Serial(COM10, 115200, timeout1) records [] def parse_frame(frame): if frame[0] ! 0xAA or frame[1] ! 0x55: return None if sum(frame[:7]) 0xFF ! frame[7]: return None if frame[2] 0x01: return {type: dht11, hum: frame[3], temp: frame[5]} if frame[2] 0x02: raw (frame[3] 8) | frame[4] return {type: pm25, raw: raw, filtered: (frame[5] 8) | frame[6]} return None while True: buf ser.read(8) r parse_frame(buf) if r: records.append(r) print(r)逻辑说明脚本按 8 字节一帧同步串口流parse_frame 先校验帧头和累加和再按类型位拆分负载把数据追加到 records 列表方便后续画图或入库。这个脚本的价值在于把 FPGA 内部看不到的采样数据暴露在上位机图上。我第一次跑 24 小时时发现 PM2.5 原始值在凌晨有一段持续偏高排查后确认是传感器进风口被灰尘堵了一半——这不是电路问题是结构问题。从那以后我每次做传感器采集都强制先跑一遍 24 小时数据监测再回去改代码。跑测时重点看两点数值基线是否稳定滤波曲线有没有随时间的趋势性漂移。趋势性漂移往往意味着传感器老化或环境变化而不是代码 bug。从那以后我拿到任何现成的 Verilog 工程也会把验证脚本一起准备先仿真跑协议再上板挂一晚上数据曲线出来才算验收通过。不想从零搭这套东西的话FPGA verilog PM2.5 温湿度传感器系统设计资源里一般会带上完整的 RTL 源码、约束文件和上位机解析脚本省掉最耗时的时序调校环节。希望帮到你。本文还有配套的精品资源点击获取