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

资讯详情

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

FPGA+Verilog实战:DHT11与PMS5003传感器采集显示系统

FPGA+Verilog实战:DHT11与PMS5003传感器采集显示系统 简介本资源是一套基于FPGA的环境监测系统完整硬件设计工程面向电子工程、嵌入式系统及数字电路初学者与实践者解决PM2.5浓度与温湿度参数的实时采集、处理与本地显示问题适用于课程设计、毕业设计及智能传感类创新项目开发。压缩包共1976个文件主体为Verilog源码.v、Quartus工程配置文件.qpf/.qsf、编译中间产物843个.cdb、840个.hdb及仿真/综合报告.rpt/.logdb等总大小16.49MB结构完整含顶层模块pm_25_top及传感器驱动、ADC接口、DHT协议解析、LCD1602时序控制等关键子模块。已有644人学习下载资源提供可直接编译下载至主流FPGA开发板运行的完整工程包含readme说明、.sof配置文件及详细信号连接定义便于读者理解多传感器协同逻辑、掌握Verilog模块化设计方法与外设驱动时序实现要点。 做FPGA的环境监测项目很多人第一反应是“杀鸡用牛刀”但真要是把PM2.5浓度和温湿度同时采进来、处理好、显示出来还要保证实时性和稳定性MCU的方案反而会在并行采集和精确时序上吃不少苦头。这篇就把我用FPGA Verilog搭的一套“PM2.5 温湿度传感器采集显示系统”完整拆给你看从方案选型、模块划分到DHT11单总线时序、PMS5003串口帧解析、滑动平均滤波再到顶层集成和LCD显示最后是调试实录。不管是拿这个题目做课设、毕设还是纯粹想练FPGA传感器接口这篇都值得花十分钟读完。1. 系统方案与选型思路1.1 为什么用FPGA来做传感器采集先说说为什么不用STM32这类单片机。你如果只接一个温湿度传感器单片机完全够用代码写起来还快。但你要是想把PM2.5、温湿度、还有后续可能加的CO2、甲醛、风速传感器都挂上去并且希望每个传感器都能以最高优先级被实时采集MCU做起来就很别扭——要么用中断轮着来要么上RTOS处理时序抖动是难免的。FPGA的典型优势就在这里所有传感器接口模块在硬件层面并行工作DHT11的时序、PMS5003的串口接收、LCD的刷新各自跑在独立的逻辑状态机里互不干扰。你不需要在“先读温度还是先读PM2.5”之间做调度决策因为硬件本来就是同时进行的。再加上FPGA对时序的控制是微秒级、甚至纳秒级的确定性控制调起DHT11这种单总线协议来比软件模拟时序稳得多。另外从学习角度讲这个题目覆盖了FPGA开发的几类核心技能状态机设计、计数器分频、UART通信协议解析、数字信号处理滤波、模块化集成。做完这一套你对Verilog的理解会提升一个台阶。1.2 传感器选型DHT11还是DHT22PMS5003还是GP2Y1010选型决定了你后续的工作量这个环节值得仔细想想。温湿度传感器主流就是DHT11和DHT22AM2302PM2.5传感器常见的是PMS5003数字串口输出和GP2Y1010AU模拟电压输出。传感器输出接口精度/量程价格区间FPGA侧工作量DHT11单总线湿度±5%RH温度±2℃温度0-50℃3-8元需要实现时序较严格的单总线主机状态机DHT22单总线湿度±2%RH温度±0.5℃温度-40-80℃10-20元与DHT11类似时序参数有差异PMS5003UART串口PM2.5精度±10%量程0-500ug/m³40-80元需要UART接收帧解析校验GP2Y1010AU模拟电压灵敏度约0.5V/(mg/m³)20-40元需要外加ADCFPGA侧要写ADC驱动和电压换算我的建议温湿度选DHT11就够了。便宜、文档多、网上例程多出了问题容易找到参考。精度不够这种环境监测看的是趋势不是计量认证±2℃完全够用。PM2.5选PMS5003别选GP2Y1010。原因很简单GP2Y1010那个模拟输出你还得在FPGA上挂一片ADC比如ADS1115或者用开发板自带的ADC写IIC/SPI驱动不说电压和粉尘浓度的换算曲线还得自己拟合陡增不少工作量。PMS5003是UART输出FPGA这边写一个9600波特率的UART接收再按照协议解析帧结构数据直接就是ug/m³省事得多。1.3 系统功能定义这个项目我定义为三个功能层次数据采集层DHT11每2秒采一次温湿度PMS5003每800毫秒主动上报一帧PM2.5数据数据处理层对PM2.5原始值做8点滑动平均滤波对温湿度做简单的毛刺抑制连续两次读数差超过阈值则丢弃数据展示层通过LCD1602实时显示同时通过UART把数据转发到PC串口调试助手便于观察和调试为什么显示层要同时挂LCD和UARTLCD是给“终端用户”看的UART是给“开发人员”看的。调试阶段你不可能一直盯着LCD串口打印才是王道。而且PMS5003的原始帧我也可以选择直接转发出去用PC端的上位机软件解析这样能看到传感器内部更多的数据PM1.0、PM10、粒子计数等对排查问题非常有帮助。2. 系统总体架构与模块划分2.1 顶层数据流在打开Vivado或Quartus之前先把顶层结构想清楚。我的做法是先画数据流传感器通过各自的接口模块进FPGA数据统一走到一个寄存器组里然后送滤波模块处理滤波完的结果再通过显示控制模块输出。大致可以分成这样几条链路DHT11物理引脚 → dht11_ctrl状态机 → 温湿度寄存器PMS5003 TX引脚 → uart_rx模块 → pm25_parser帧解析模块 → 原始PM2.5寄存器原始PM2.5寄存器 → sliding_avg滤波模块 → 滤波后PM2.5寄存器全部传感器寄存器 → bcd_converter → lcd1602_driver → LCD屏全部传感器寄存器 → uart_tx模块 → PC调试助手有一个容易忽略的点数据在两个模块之间传递时要不要打一拍同步如果你的所有模块都用同一个时钟域寄存器直接赋值问题不大。但如果在不同的时钟域之间传递比如UART接收模块的采样时钟是由波特率分频出来的不是系统主时钟最好做一次同步处理。我在这套系统里统一用50MHz系统时钟UART内部用计数器产生采样使能脉冲保持单时钟域避免跨时钟域带来的亚稳态问题。这是新手特别容易踩的坑。2.2 模块划分与各自职责下面这个表格是我在实际RTL编码时用的模块清单每个模块的职责边界划得很清楚模块名功能职责主要接口复杂度clk_gen时钟管理倍频/分频本设计直接使用50MHzclk_in, clk_out低dht11_ctrlDHT11单总线时序控制、数据采集、校验trig, dht_data, humidity, temperature, data_valid高uart_rx通用UART串口接收9600, 8N1rx_uart, rx_data, rx_valid中pm25_parser从UART数据流中寻找PMS5003帧头、校验、提取PM2.5值rx_data, rx_valid, pm25_raw, frame_valid中sliding_avg滑动平均滤波data_in, data_valid, data_out, data_out_valid低bcd_converter二进制转BCD码供LCD显示bin_in, bcd_out中lcd1602_driverLCD1602初始化、写命令/数据、光标控制wcmd, wdata, lcd接口高uart_tx调试用把寄存器值格式化输出到串口tx_uart, tx_data, tx_valid中top顶层例化、连线、复位同步所有外部引脚中这里面dht11_ctrl、uart_rx、pm25_parser和lcd1602_driver是工作量最大的四个后面我会逐个展开讲。sliding_avg和bcd_converter逻辑上不难但细节讲究也不少。2.3 时钟方案与计数器参数设计系统时钟用的是开发板上最常见的50MHz有源晶振。50MHz对这类传感器应用来说绰绰有余DHT11的时序单位是微秒级PMS5003的串口波特率才9600LCD1602的写入时序也是微妙级50MHz足够满足采样精度而且没有跨时钟域问题是最省事的选法。用50MHz作为唯一主时钟所有计数器参数都按这个频率换算。记住一个公式计数值 时间(秒) × 50,000,000。我在设计每个状态机前会在代码旁边列一张参数表类似这样时序参数时间长度在50MHz下的计数值计数器位宽DHT11起始信号低电平18ms900,00020位DHT11起始信号高电平20-40us1,000-2,00011位DHT11响应低电平80us4,00012位数据位低电平50us2,50012位UART波特率周期104.17us5,20813位UART半bit采样点52.08us2,60413位为什么把计数器位宽单独列出来因为Verilog里计数器溢出位宽不够会导致状态机卡死这种Bug非常隐蔽。比如18ms在50MHz下要数900,000个时钟20位计数器最大能数到1,048,575够用但如果你图省事定义成19位最大524,287数到一半就归零状态机永远跳不出去整块逻辑直接死掉。这类细节我会在每个模块实现时反复检查。3. DHT11温湿度采集模块实现3.1 单总线协议的关键时序DHT11是单总线器件一根线上既要主机发命令又要从机回数据所以必须是开漏输出配合上拉电阻。在FPGA里最简单粗暴的做法是把引脚配置成三态输出低电平时直接拉低输出高电平时把IO置为高阻态让外部上拉电阻把电平拉高。一次完整的DHT11读取时序分四个阶段。第一阶段主机先把总线拉低至少18ms然后释放总线并延时20-40us这相当于发出起始信号。第二阶段DHT11检测到这个起始信号后会把总线拉低80us再拉高80us应答主机的读取请求。第三阶段DHT11连续输出40位数据8位湿度整数、8位湿度小数、8位温度整数、8位温度小数、8位校验和。每一位都用相同的低电平50us开始区别在于后面的高电平长度——高电平26-28us表示逻辑0高电平70us表示逻辑1。第四阶段主机读完40位后DHT11释放总线回到空闲状态。校验规则是湿度整数 湿度小数 温度整数 温度小数 的低8位应该等于校验字节。如果不等这一帧数据直接丢弃。3.2 计数器的计算与状态机跳转标准的DHT11协议里一次从起始信号到40位数据读完大约需要20ms左右。我用一个状态机来跟踪整个过程状态划分成IDLE → START_LOW → START_HI → WAIT_ACK_LOW → ACK_HIGH_LOW → ACK_HIGH_HI →循环40次→ READ_LOW → READ_HIGH → 采样 → CHECKSUM → 输出数据。这里最核心的计数器设计还是基于上一节的参数表。比如START_LOW状态要拉低总线18ms那么在50MHz下就是900,000个时钟周期。专门定义一个20位计数器cnt每拍加一计数到899,999就跳走。注意从0数到899,999一共是900,000个周期不要多打一拍给自己制造0.02us的误差。采样点的选择是DHT11采集成败的关键。每一位数据都是50us低电平开头紧接着高电平既然是先低再高数据位的起点就是低电平结束的地方。逻辑0的高电平只有26-28us逻辑1的高电平有70us。如果我在高电平开始后的第40us处采样逻辑0早就变回低电平了而逻辑1还是高电平这样就能准确区分0和1。换算成50MHz就是高电平计数到第2,000个时钟时读引脚电平。这个“中间采样”的思路和UART在bit中心采样的原理完全一致核心目的都是避开电平翻转沿附近的不稳定区。3.3 Verilog核心代码下面是我项目中DHT11控制器的核心状态机片段去掉了部分冗余逻辑保留主干module dht11_ctrl #( parameter CLK_FREQ 50_000_000 )( input wire clk, input wire rst_n, input wire trig, // 外部触发拉高至少1us开始采集 inout wire dht_data, // 单总线数据线 output reg [7:0] humidity, output reg [7:0] temperature, output reg data_valid ); localparam START_LOW_CNT CLK_FREQ / 1000 * 18; // 18ms localparam START_HI_CNT CLK_FREQ / 1000000 * 30; // 30us localparam BIT_SAMPLE_CNT CLK_FREQ / 1000000 * 40; // 40us采样点 reg [19:0] cnt; reg [5:0] bit_cnt; reg [7:0] data_buf[4:0]; reg [2:0] state; reg dht_out_en; reg dht_out_val; assign dht_data dht_out_en ? dht_out_val : 1bz; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; dht_out_en 1b0; data_valid 1b0; end else begin case (state) IDLE: begin if (trig) begin cnt 0; dht_out_en 1b1; dht_out_val 1b0; // 拉低总线 state START_LOW; end end START_LOW: begin if (cnt START_LOW_CNT - 1) begin cnt 0; dht_out_en 1b0; // 释放总线上拉到高 state START_HI; end else begin cnt cnt 1b1; end end START_HI: begin if (cnt START_HI_CNT - 1) begin cnt 0; state WAIT_ACK_LOW; end else begin cnt cnt 1b1; end end // 后续状态等待应答低、应答高、读40bit数据、校验 // ... endcase end end endmodule注意我把DHT11的引脚做成了inout类型。FPGA的IO引脚驱动高电平时不能像单片机那样直接推挽输出高——虽然硬件上也能配置但如果你输出高电平的驱动力不够遇到DHT11上拉后还能保持高电平一般问题不大。最稳妥的方案还是三态门配合外部4.7kΩ上拉电阻输出0时使能下拉输出1时高阻释放让外部电阻决定电平。3.4 实际操作中的必修坑第一次调DHT11的时候我在读数据的环节卡了一整天最后发现是读间隔太短。DHT11手册明确写了两次读取间隔要大于1秒。如果你的状态机在读完一帧后立刻又读下一帧DHT11根本不会应答总线一直保持高电平状态机就一直等应答超时。我最终的方案是每次读完后强制IDLE至少1.2秒再允许下一次读取。第二个坑是上电启动时的状态。FPGA程序跑起来后DHT11可能还没完成上电初始化这时候如果立刻发起始信号DHT11不会响应。我给DHT11模块加了一个复位后的初始延时计数器上电后先等1秒再进入IDLE稳妥多了。第三个建议是不要省略校验。40位数据里的校验和不是摆设在某些电磁环境差的地方尤其电机、继电器附近数据大概率会出错。有了校验坏帧直接丢弃宁缺毋滥。这也是“可靠性系统设计”这个关键词在FPGA项目里的最小落地形式。4. PM2.5数据接收、解析与滑动平均滤波4.1 PMS5003的串口协议与帧格式PMS5003这传感器我很推荐它是激光散射原理内置风扇主动吸气输出的是数字信号。上电后默认工作在主动模式每800ms左右自动通过UART发送一帧数据不需要你主动去请求FPGA只要在UART接收端等着收帧就行。帧格式固定是32字节Byte0~1帧头固定0x42 0x4D字符‘B’‘M’Byte2~3帧长度值为0x00 0x1C即28代表后面28字节是本帧的数据加校验段不含帧头Byte4~5PM1.0标准值高字节在前Byte6~7PM2.5标准值高字节在前Byte8~9PM10标准值后面还有大气环境下PM1.0/PM2.5/PM10、0.1L~10L粒子计数、保留字节等Byte30~31校验和值为Byte0~Byte29共30个字节的累加和要用到的PM2.5值就在Byte6和Byte7直接拼接成16位数就是单位ug/m³的浓度。注意如果只要PM2.5你可以跳过大量无关字节但必须把帧头、帧长、校验都做完整否则数据错位会直接污染读取结果。4.2 UART接收模块9600波特率的采样策略FPGA里写UART接收模块有固定的套路。9600波特率下一个bit的周期是1/9600≈104.17us在50MHz时钟下就是5,208个时钟周期。接收模块先持续检测空闲状态下的下降沿——一旦检测到RX引脚从高变低说明收到了起始位这时开始计时。采样的关键策略和DHT11一样在每一个bit的中间采样。起始位下降沿检测到后先等半bit周期2,604个时钟再采样一次确认仍然是低电平这样可以过滤掉毛刺误触发的假起始位。之后每隔一个完整bit周期5,208个时钟采样一次依次读出Bit0到Bit7的数据位。第8次采样对应停止位如果停止位读到高电平说明这一字节有效输出8位数据如果停止位读到低电平则判定为帧错误丢弃这一字节。parameter BAUD_CNT 50_000_000 / 9600; // 5208 parameter HALF_BIT BAUD_CNT / 2; // 2604 always (posedge clk or negedge rst_n) begin if (!rst_n) begin // 复位 end else begin case (rx_state) RX_IDLE: begin if (rx_uart 1b0) begin // 检测起始位下降沿 cnt 0; rx_state RX_START; end end RX_START: begin if (cnt HALF_BIT - 1) begin if (rx_uart 1b0) begin // 半bit后确认起始位有效 cnt 0; bit_idx 0; rx_state RX_DATA; end else begin rx_state RX_IDLE; // 毛刺丢弃 end end else begin cnt cnt 1b1; end end RX_DATA: begin if (cnt BAUD_CNT - 1) begin cnt 0; if (bit_idx 7) begin rx_state RX_STOP; end else begin bit_idx bit_idx 1b1; end rx_data[bit_idx] rx_uart; end else begin cnt cnt 1b1; end end // ... endcase end end这个UART接收模块设计成通用的以后接GPS模块、蓝牙模块、其他串口传感器都能复用所以参数化做得足调用的时候只需要例化时改波特率参数。4.3 帧解析状态机与校验逻辑UART模块出来后每一个字节通过rx_data和rx_valid信号交给上层。上层就是pm25_parser帧解析模块它内部有一个小状态机做的事情是先等0x42下一个字节必须等0x4D然后收帧长、收数据、到最后两位做校验和比对。我踩过的一个坑是不能在第0个字节等于0x42时就锁死然后死等0x4D。如果中间有噪声数据把状态机带偏它会一直找不到帧头。正确做法是当检测到0x42后如果下一个字节不是0x4D就退回到初始状态重新找帧头。有一个更稳的小技巧起始查找状态时数据一进来就同时判断“本字节是不是0x42”和“上一个字节是0x42且本字节是0x4D”这样即使在0x42 0x4D被噪声分隔的情况下也不会错过。校验和的计算要在收帧过程中同步累加。我建议把Byte0~Byte29的累加过程放在数据流水线里做边收边加收到Byte30时再比对。如果在FPGA里把32字节先存到RAM再回头算会多消耗时钟周期而且代码结构也啰嗦。校验通过后直接把Byte6和Byte7拼成16位的pm25_raw同时拉高frame_valid一个时钟周期通知下游滤波模块“有新数据了”。4.4 滑动平均滤波的Verilog实现为什么需要滤波我看过PMS5003原始数据的实时打印相邻两次读数经常出现几十ug/m³的跳变比如上一帧是35下一帧突然到78再下一帧又是42。这种跳变一部分是真实的颗粒物湍流变化一部分是传感器本身噪声。直接显示原始值LCD上的数字会跳得人眼花缭乱。滤波就是要把趋势保留下来把毛刺抹掉。滑动平均是性价比最高的方案。维护一个长度为N的窗口每来一个新数据就把窗口中最旧的数据丢掉新数据加进来然后对整个窗口取平均。在硬件上N8时平均操作等价于求和后右移3位完全不需要除法器这也是选8点而不是10点的直接原因——移位操作只消耗LUT里的布线不需要DSP资源。module sliding_avg #( parameter DATA_WIDTH 16, parameter AVG_NUM 8 )( input wire clk, input wire rst_n, input wire data_valid, input wire [DATA_WIDTH-1:0] data_in, output reg [DATA_WIDTH-1:0] data_out, output reg data_out_valid ); localparam CNT_WIDTH 3; // log2(AVG_NUM) reg [DATA_WIDTH-1:0] shift_reg [0:AVG_NUM-1]; reg [DATA_WIDTHCNT_WIDTH-1:0] sum; reg [CNT_WIDTH-1:0] idx; wire [DATA_WIDTHCNT_WIDTH-1:0] sum_next; assign sum_next sum data_in - shift_reg[idx]; always (posedge clk or negedge rst_n) begin if (!rst_n) begin // 清空寄存器、累加器、输出 end else if (data_valid) begin sum sum_next; shift_reg[idx] data_in; data_out sum_next[DATA_WIDTHCNT_WIDTH-1 : CNT_WIDTH]; idx (idx AVG_NUM - 1) ? 0 : idx 1b1; data_out_valid 1b1; end else begin data_out_valid 1b0; end end endmodule这里有个资源细节值得说一下我用了shift_reg[idx]作为环形缓冲区索引来覆盖最旧的数据避免了每来一个新数据就把整个数组移位一遍。如果写得比较粗心的同学会这么做把shift_reg[0]更新然后循环把shift_reg[i]搬到shift_reg[i1]。这种写法在AVG_NUM8时没问题但窗口大了会消耗大量寄存器翻转功耗而且综合出来逻辑会很大。环形索引的思路在FPGA数字信号处理里非常常用其实就是一个最简单的实现方式能省则省。系数宽度上再提醒一句求和结果比单点数据宽3位因为8个16位数相加最大可能到2^19级别。如果你在sum定义时不小心用成16位数据一大就会溢出滤波结果会出现突然跳零又跳回大数的诡异现象。这个问题非常隐蔽我在调试时靠加了一个计数器监视sum的最大值才定位到。4.5 滤波窗口大小与实时性的取舍窗口选8点是因为PMS5003每800ms来一帧8点窗口对应的时间跨度为6.4秒。这个时间跨度对室内空气质量监测来说非常合适——你不会看到瞬时跳变但也能感知到开窗后PM2.5在十几秒内的明显下降趋势。如果窗口选4点响应更快但滤波效果弱选16点曲线会非常平滑但人员走动产生的PM2.5突变要10多秒才能反映到屏幕上。至于选32点滤波曲线就基本是一条慢吞吞的爬行线了观感上会觉得系统“迟钝”。所以8到16点之间是室内环境监测的甜点区你自己按需调。5. 顶层集成与LCD显示输出5.1 顶层模块例化与连线要点把所有子模块写完最后就是套壳。顶层模块主要做三件事例化所有子模块、把传感器引脚约束到FPGA封装的正确管脚上、统一处理复位逻辑。复位这地方有个容易忽视的细节外部按键复位信号是异步的而且按下去会有机械抖动。我的做法是在顶层加一个同步复位模块把外部复位信号用两级触发器打两拍得到rst_n_sync再分发给所有子模块。不然每个模块都直接用自己的异步复位综合时会出现复位树的时序问题上板后偶尔会莫名其妙卡死。顶层连线还有一个原则不要跨模块直接访问内部寄存器。比如显示模块需要PM2.5值不要写成top.pm25_parser.pm25_raw这种路径引用应该把PM2.5值在顶层单独定义一根wire从pm25_parser输出端口引出来再接到LCD模块的输入端口。这样以后想换显示方式或者想加一个UART转发只需要在顶层改连线不用动子模块的代码。5.2 LCD1602驱动的初始化顺序与写入流程LCD1602是并口字符液晶数据位DB0~DB7接FPGA的IO口控制信号RS、RW、E接三个IO口。写入流程并不复杂设置RS和RW状态把数据放到DB总线上然后拉高E产生一个下降沿让LCD锁存数据。但初始化时序极其讲究必须严格按照数据手册的步骤来。初始化序列8位模式我整理成固定流程上电后等15ms以上写命令0x30等5ms写命令0x30等5ms写命令0x30等5ms写命令0x388位总线、2行显示、5x8字库等100us或查忙标志写命令0x0C显示开、光标关、不闪烁等100us写命令0x01清屏等2ms写命令0x06写入后地址自动加1等100us很多人直接抄代码不关心为什么每个命令之间要等待上了板子LCD白屏就开始怀疑硬件。其实原因很简单LCD1602内部的控制器是单片机处理每条指令都需要时间尤其是清屏指令要逐字清零DDRAM耗时最长。你如果不等下一条命令来了它还在忙指令就被吞了。FPGA实现时我建议写成状态机而不是函数。因为Verilog没有真正的阻塞函数调用每个命令对应一个状态每写一条命令就跳到一个延时状态延时结束后再写下一条。写完初始化序列后进入主循环先设置DDRAM地址0x00写第一行再设置地址0x40写第二行。5.3 显示数据预处理二进制转BCD与占位处理LCD1602只能显示ASCII字符所以PM2.5这个16位二进制数不能直接送显示得先转成十进制数字。PM2.5的典型值在0~500ug/m³之间也就是最多3位十进制数。二进制转BCD我用了最简单快速的方法三次比较减法。因为PM2.5值最大不超过999我可以直接对二进制的pm25_value做这样的逻辑——先判断是否大于等于100如果大于等于100百位加1数值减100循环直到小于100再判断是否大于等于10循环得到十位剩下的就是个位。虽然用了循环但这里循环次数是固定的综合出来就是组合逻辑一个时钟周期就能出结果。如果在别的项目里遇到更大的数值范围用经典的Double Dabble移位算法更合适。占位处理是显示效果的关键。如果不处理数字是1位时LCD上数字会在第4列跳到第2列再回到第3列晃来晃去很难受。我在转BCD后做了固定输出拼接如果百位为0就显示空格如果百位和十位都为0十位也显示空格。这样显示出来的格式永远是“xxx”数字位数变化时不会乱跳位置。5.4 仿真验证先把模块测透了再上板我个人的习惯是每个模块写完都要单独仿真一遍尤其是DHT11控制器和UART接收模块——这两个是通信协议类模块时序错了上板调试非常痛苦。testbench里我会模拟DHT11的应答行为在收到起始信号后按协议延时、拉低80us、拉高80us然后模拟40位数据其中一位一位生成对应的高电平持续时间。这样DHT11控制器的状态机就能被完整驱动起来我可以观察内部信号是不是按预期跳转。UART接收模块更简单testbench里用延时模拟9600波特的波形把一帧要发的数据发出去然后看接收端能不能正确恢复出8位数据。PMS5003的帧解析模块我就把一帧32字节的测试报文通过串口data线灌进去验证解析出来的PM2.5值是不是和报文里Byte6/Byte7一致。上板后如果还有问题优先用Vivado的ILA集成逻辑分析仪抓内部信号不用急着改代码。我通常会把DHT11的引脚电平、UART的rx引脚、状态机状态值、pm25_raw这些信号都挂到ILA上跑一遍观察。有了波形图大多数问题一眼就能看出来。6. 常见问题与调试实录这套项目做下来我在调试阶段记录了不少问题选几个最有代表性的整理成表现象根本原因解决办法DHT11永远返回超时读间隔小于1秒DHT11不响应状态机回到IDLE后强制等待1.2秒再允许下次读DHT11数据校验经常失败采样点太靠近数据位高电平结束沿把采样点放在高电平开始后40us处不要在高电平结束时采样PM2.5解析偶尔出大数没做校验和噪声数据被误当做有效帧严格对Byte0~Byte29累加和Byte30/31比对滤波后数据仍明显跳动滤波窗口太小或直接把非法帧数据也喂进滤波器窗口选8/16点parser在帧校验失败时不输出validLCD第一行正常第二行乱码写第二行前没有切换DDRAM地址到0x40在写第二行前先写命令0x800x40LCD完全白屏初始化等待时间不足清屏命令后至少等2ms上电后至少等15ms上电后LCD显示正常但几秒后数据不动传感器上电初始化和FPGA逻辑竞争顶层增加上电延时计数器延时1秒再开始采集6.1 DHT11读不到数据的终极排查法如果你按上面表格还解决不了最有效的办法是用示波器或者逻辑分析仪抓DHT11数据引脚的实际波形。示波器上看一眼就明白起始信号有没有正常拉低18msDHT11有没有拉低80us响应响应后数据位到底输出了什么波形对比一轮问题定位到具体时间段基本就八九不离十了。没有示波器的话用ILA抓FPGA引脚的电平变化历史也凑合只是采样率要够高。还有一种诡异情况数据线接错引脚了。有次我在约束文件里把DHT11的IO引脚写到了相邻的一个引脚上DHT11波形在示波器上完全正常FPGA就是拿不到数据。这种问题示波器都看不出来只能仔细核对原理图和XDC引脚约束。6.2 一个调试时的深刻教训别急着改代码先加计数器我调试这套系统的过程中印象最深的不是某个具体Bug而是一个方法论。有一次PM2.5数据随机跳大数我没排查先怀疑是滤波模块写错了改了两次滤波算法还是老样子后来才发现是解析模块在校验失败时把旧数据又吐了一遍数据重复采样导致滤波窗口里混进了重复值。打那以后我就养成一个习惯每个关键模块里都加一个短计数器和状态寄存器通过UART定期向外打印。比如DHT11模块里统计成功帧数和校验失败帧数PMS5003解析模块里统计收帧总数和校验失败总数。数据一旦异常看这些统计值就能快速定位问题出在哪个环节而不是靠猜。6.3 滤波算法的取舍与实时性的实际手感对于室内PM2.5监测我个人最终选定的是8点滑动平均也就是约6.4秒的时间常数。试过4点屏幕上数字还是偶尔会跳试过16点在房间里开净化器时PM2.5从120降到30的过程被拉得很长看起来“反应迟钝”体验不好。折中后8点刚好。DHT11的温湿度数据我没做滑动平均而是在每2秒一次采样中做了个简单的去毛刺如果当前读数与上次读数的差值超过5温度或10湿度判定为异常跳变保留上次值。这是因为DHT11精度本来就不高过度滤波反而会让温度变化看起来拖泥带水。不同物理量用不同处理策略这比一套滤波打天下要合理。实操总结与扩展思路做完整套系统我最大的体会是FPGA做传感器项目难的不是某个模块本身而是多个模块集成后的整体把控。DHT11、UART、LCD这些单点技术网上都有教程但把它们在同一个时钟域里稳稳当当串起来并且保证长时间运行不卡死、数据不漂移才是真正考验工程能力的地方。顺着这个项目往下走还有几条不错的扩展路线。一是加WiFi模块比如ESP8266FPGA通过UART把数据推给WiFi模组数据就能上云手机端实时看二是把显示换成OLED屏走I2C接口顺便把I2C控制器写了三是在数据处理层加一个报警逻辑PM2.5超过阈值时点亮LED或者驱动蜂鸣器四是可以换更高精度的传感器把整个系统的数据质量往上推一个档次。硬件平台允许的话甚至可以把采集到的数据通过以太网口传出去做成一个小型物联网关。最后分享一个操作习惯我是把所有传感器接口的时序参数统一放在一个parameter文件里管理比如defines.vh里面定义50MHz时钟的分频基数、传感器时序的长度、串口波特率等。调一次参数所有模块自动跟着变省去四处翻代码改数字的麻烦。这套系统做完之后已经成了我后面的“传感器接口模板库”再做其他环境监测类的项目DHT11和PMS5003的模块直接拿过来用改个引脚约束就能跑这就是当时花心思做模块化的回报。本文还有配套的精品资源点击获取
返回列表