ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于FPGA的PM2.5与温湿度采集:状态机、滤波与UART上板实现

基于FPGA的PM2.5与温湿度采集:状态机、滤波与UART上板实现

简介:一套基于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

温湿度传感器我选了 DHT11,PM2.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~5V5V(LED 驱动脚独立)
采样周期≥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 时容易翻车的只有两个点:起始信号宽度不够、位判断阈值取错。完整时序流程拆开是这样的:

  1. 总线空闲时为高电平;
  2. 主机拉低不少于 18ms,然后释放总线;
  3. 从机响应:先把总线拉低约 80μs,再拉高约 80μs;
  4. 随后输出 40bit 数据,每一位都是先拉低 50μs,然后拉高,高电平持续 26~28μs 表示逻辑 0,持续约 70μs 表示逻辑 1;
  5. 数据发完总线恢复高电平,等待下一次起始。

把这段流程转成 Verilog 状态机时,核心是把微秒计数器和阈值判断分开。我习惯用 1μs 节拍做所有延时基准,位判断时记录高电平宽度,用两个阈值:小于 30μs 判为 0,大于 65μs 判为 1,中间区间认为时序异常,丢弃这一帧重采。

40bit 数据里,前 8bit 是湿度整数,8bit 湿度小数(实际为 0),8bit 温度整数,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 <= 1'b0; sample_en <= 1'b0; end else begin cnt <= cnt + 1; // 10ms 周期,320us 高电平脉冲 led_pulse <= (cnt < 16_000) ? 1'b1 : 1'b0; // 脉冲后延时 280us 采样 sample_en <= (cnt[18:0] == 14_000) ? 1'b1 : 1'b0; 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 就是 16000,280μ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 <= 1'b0; end else begin if (counter >= CNT_MAX - 1) begin counter <= 0; tick_1us <= 1'b1; end else begin counter <= counter + 1; tick_1us <= 1'b0; end end end endmodule

逻辑说明:counter 从 0 计到 49,第 50 个时钟周期拉高 tick_1us,之后清零。tick_1us 只持续一个时钟周期,本质上是一个 enable 信号。后续模块在 always 块里判断 if (tick_1us) 才更新计数器,就能实现微秒级计时。

参数说明:CNT_MAX 是唯一需要改的参数。系统时钟 50MHz 时是 50,100MHz 时改成 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 = 3'd0; localparam S_START_L = 3'd1; // 拉低起始 18ms localparam S_RELEASE = 3'd2; // 释放总线等待响应 localparam S_READ = 3'd3; // 读 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 ? 1'b0 : 1'bz; // 开漏输出 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_W+3: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 + 1'b1; end end always @(posedge clk or negedge rst_n) begin if (!rst_n) data_out <= 0; else data_out <= sum[DATA_W+3: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 发送器:115200bps,8N1 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 <= 1'b1; tx_busy <= 1'b0; bit_idx <= 0; cnt <= 0; end else if (tx_start && !tx_busy) begin shift_reg <= {1'b1, tx_data, 1'b0}; tx_busy <= 1'b1; 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 <= 1'b0; end end end else begin txd <= 1'b1; 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.5
3~6负载温湿度:湿度整数/湿度小数/温度整数/温度小数;PM2.5:ADC 高字节/低字节/滤波值高/低
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 = 1'b1; data = 1'b0; repeat (us) #1000; // 1us driving = 1'b0; data = 1'b1; endtask initial begin resp_data = 40'b00010101_00000000_00011001_00000000_00101110; wait (data === 1'b0); // 检测起始信号 #20_000; // 跳过 18ms 起始低电平 drive_low(80); // 响应低电平 // 按 bit 输出数据,每位低 50us,高电平宽度区分 0/1 end endmodule

逻辑说明:模型里 wait 语句检测总线被拉低,然后延时 20ms,模拟 DHT11 的响应时序。resp_data 中前 8bit 是湿度整数 0x15(21%)、温度整数 0x19(25℃),最后 8bit 是校验,三字节相加 0x15+0x00+0x19+0x00=0x2E,TB 里要核对校验是否一致。

仿真波形里重点看三个点:起始信号低电平是否持续足够时间;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 常见问题排查:现象、原因、解决

  • 现象 1:DHT11 读出的温湿度一直是 00,校验必然不对。原因:起始低电平没保持住 18ms 就释放了,DHT11 根本没响应。解决:把 S_START_L 的计数阈值从 18_000 放宽到 20_000;同时检查 dht_out_en 释放后是否真正转成高阻,别让输出驱动一直占着总线。
  • 现象 2:PM2.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, timeout=1) 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 源码、约束文件和上位机解析脚本,省掉最耗时的时序调校环节。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进