機(jī)、濾波與UART上板實(shí)現(xiàn))
簡介一套基于FPGA的PM2.5與溫濕度傳感器實(shí)時(shí)監(jiān)測系統(tǒng)設(shè)計(jì)資料面向嵌入式/FPGA開發(fā)者和電子類課程設(shè)計(jì)人員。系統(tǒng)采用Verilog HDL實(shí)現(xiàn)涵蓋PM2.5傳感器數(shù)據(jù)采集、DHT系列溫濕度解析、液晶1602驅(qū)動(dòng)與頂層集成模塊完整展示從傳感器接口配置、ADC采樣轉(zhuǎn)換到顯示時(shí)序控制的設(shè)計(jì)流程。壓縮包共1976個(gè)文件以Quartus工程數(shù)據(jù)庫文件cdb/hdb、Verilog源碼.v及輔助腳本/文檔為主整體約16.49MB便于直接打開工程進(jìn)行仿真與驗(yàn)證。已有645人學(xué)習(xí)或下載。資料不僅提供可直接編譯的工程結(jié)構(gòu)還包含綜合、仿真相關(guān)文件與備份版本可幫助讀者快速復(fù)現(xiàn)環(huán)境監(jiān)測平臺(tái)并用于空氣質(zhì)量研究、智能家居、工業(yè)監(jiān)控等場景的硬件原型開發(fā)。1. 用 FPGA 做 PM2.5 與溫濕度采集為什么先別急著選單片機(jī)做環(huán)境監(jiān)測板卡那會(huì)兒我拿到過兩次幾乎一樣的需求采集 PM2.5 濃度和溫濕度把數(shù)據(jù)送上位機(jī)。第一次我用單片機(jī)一周交差第二次客戶追加了“兩個(gè)傳感器同時(shí)跑、數(shù)據(jù)要濾波、異常點(diǎn)要剔除”單片機(jī)串行時(shí)序立刻吃緊。把整套系統(tǒng)設(shè)計(jì)搬到 FPGA 上之后verilog 狀態(tài)機(jī)并行處理傳感器時(shí)序、ADC 采樣和 UART 上送穩(wěn)定性完全不一樣。這篇文章把完整的拆解過程寫出來從傳感器接口、Verilog 核心模塊、幀協(xié)議到上板調(diào)試適合做畢設(shè)、課設(shè)或者想把手頭 FPGA 用起來做數(shù)據(jù)采集的工程師直接對照落地。2. 傳感器接口與選型DHT11 單總線時(shí)序和 PM2.5 模擬電壓通道2.1 選型邏輯為什么是 DHT11 加 GP2Y1010AU0F溫濕度傳感器我選了 DHT11PM2.5 傳感器選了夏普 GP2Y1010AU0F。選這兩個(gè)不是因?yàn)樗鼈z性能多頂尖而是因?yàn)榻涌陬愋突パa(bǔ)DHT11 走單總線數(shù)字協(xié)議GP2Y1010 輸出模擬電壓。一個(gè) FPGA 工程里同時(shí)覆蓋兩類傳感器接口后面換 SHT30、攀藤 PMS5003 這類器件時(shí)只需要替換對應(yīng)驅(qū)動(dòng)模塊系統(tǒng)骨架不用動(dòng)。DHT11 的精度是 ±2℃、±5%RH刷新速率 1Hz 左右。做機(jī)房、倉庫這類環(huán)境監(jiān)測足夠但注意它沒有真正的“小數(shù)位”小數(shù)字節(jié)返回 0別被數(shù)據(jù)手冊里那一長串格式說明帶偏。GP2Y1010AU0F 的粉塵檢測范圍是 0~0.5mg/m3工作電壓 5V典型靜態(tài)輸出電壓 0.9V 左右濃度越高電壓越高最高能到 3.6V 左右。這里有個(gè)容易忽略的點(diǎn)它的輸出電壓不能直接進(jìn) FPGA 的 GPIO必須經(jīng)過 ADC 轉(zhuǎn)成數(shù)字量還要考慮傳感器本身 5V 供電和 FPGA 3.3V bank 之間的電平關(guān)系。選型決策表可以這樣列項(xiàng)目DHT11GP2Y1010AU0F輸出類型單總線數(shù)字模擬電壓 0.9~3.6V供電電壓3.3V~5V5VLED 驅(qū)動(dòng)腳獨(dú)立采樣周期≥1s10ms 周期320μs 脈沖驅(qū)動(dòng)FPGA 側(cè)接口三態(tài) IO 上拉外部 ADC 或板載 ADC典型精度±2℃ / ±5%RH換算后 ±10μg/m3 量級接線上要注意三點(diǎn)DHT11 數(shù)據(jù)線上必須有一個(gè) 5kΩ 左右的上拉電阻否則總線釋放后識(shí)別不到從機(jī)響應(yīng)GP2Y1010 的 LED 驅(qū)動(dòng)腳要接一個(gè)三極管或 MOS 開關(guān)由 FPGA 產(chǎn)生周期脈沖不能直接拿 IO 硬驅(qū)動(dòng)ADC 的基準(zhǔn)電壓決定了濃度換算系數(shù)建議用 3.3V 基準(zhǔn)而不是 5V能把 0.9~3.6V 的信號(hào)壓縮到量化區(qū)間內(nèi)。2.2 DHT11 時(shí)序拆解高低電平寬度才是判斷依據(jù)DHT11 的單總線時(shí)序網(wǎng)上一搜一大把但真正寫 verilog 時(shí)容易翻車的只有兩個(gè)點(diǎn)起始信號(hào)寬度不夠、位判斷閾值取錯(cuò)。完整時(shí)序流程拆開是這樣的總線空閑時(shí)為高電平主機(jī)拉低不少于 18ms然后釋放總線從機(jī)響應(yīng)先把總線拉低約 80μs再拉高約 80μs隨后輸出 40bit 數(shù)據(jù)每一位都是先拉低 50μs然后拉高高電平持續(xù) 26~28μs 表示邏輯 0持續(xù)約 70μs 表示邏輯 1數(shù)據(jù)發(fā)完總線恢復(fù)高電平等待下一次起始。把這段流程轉(zhuǎn)成 Verilog 狀態(tài)機(jī)時(shí)核心是把微秒計(jì)數(shù)器和閾值判斷分開。我習(xí)慣用 1μs 節(jié)拍做所有延時(shí)基準(zhǔn)位判斷時(shí)記錄高電平寬度用兩個(gè)閾值小于 30μs 判為 0大于 65μs 判為 1中間區(qū)間認(rèn)為時(shí)序異常丟棄這一幀重采。40bit 數(shù)據(jù)里前 8bit 是濕度整數(shù)8bit 濕度小數(shù)實(shí)際為 08bit 溫度整數(shù)8bit 溫度小數(shù)實(shí)際為 0最后 8bit 是校驗(yàn)和。校驗(yàn)公式是前四個(gè)字節(jié)相加取低 8 位。很多初學(xué)者把小數(shù)位當(dāng)有效位數(shù)用算出來的校驗(yàn)自然對不上這不是時(shí)序問題是數(shù)據(jù)解析位序理解錯(cuò)了。2.3 PM2.5 模擬通道ADC 采樣時(shí)序和濃度換算GP2Y1010AU0F 的工作時(shí)序有一個(gè)特殊性它的紅外 LED 需要周期性脈沖驅(qū)動(dòng)典型參數(shù)是脈沖寬度 320μs周期 10ms。在 LED 脈沖上升沿之后不能立刻采樣要等大約 280μs 再采樣輸出否則會(huì)采到 LED 開關(guān)帶來的尖峰噪聲。這個(gè)“脈沖-延時(shí)-采樣”的順序在 Verilog 里用一個(gè)移位計(jì)數(shù)器就能實(shí)現(xiàn)。// PM2.5 LED 脈沖與 ADC 采樣節(jié)拍生成 module pm25_sample_ctrl ( input wire clk, // 50MHz 系統(tǒng)時(shí)鐘 input wire rst_n, input wire [11:0] adc_data, // 外部 ADC 轉(zhuǎn)換結(jié)果 output reg led_pulse,// LED 驅(qū)動(dòng)脈沖 output reg [11:0] pm25_raw // 采樣原始值 ); reg [18:0] cnt; // 0~499999 對應(yīng) 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; // 脈沖后延時(shí) 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 計(jì)數(shù)滿 499999 對應(yīng)約 10ms 周期。led_pulse 在前 16000 個(gè)周期拉高就是 320μs 脈沖在第 14000 個(gè)周期產(chǎn)生 sample_en 采樣使能保證采樣點(diǎn)落在 LED 脈沖后段且避開了開關(guān)沿。參數(shù)說明16_000 和 14_000 這兩個(gè)數(shù)不是拍腦袋定的按 50MHz 時(shí)鐘1μs 等于 50 個(gè)計(jì)數(shù)。320μs 就是 16000280μs 就是 14000。如果換 100MHz 時(shí)鐘所有參數(shù)翻倍。采樣后的 adc_data 是電壓數(shù)字量換算濃度通常不在 FPGA 里做交給上位機(jī)按公式處理更靈活FPGA 只負(fù)責(zé)把原始 ADC 值和濾波結(jié)果一起打包上送。3. Verilog 核心模塊落地分頻、狀態(tài)機(jī)與滑動(dòng)窗口濾波3.1 時(shí)鐘分頻與定時(shí)基準(zhǔn)所有時(shí)序從 1μs 節(jié)拍派生FPGA 板卡上常見 50MHz 主時(shí)鐘而 DHT11 時(shí)序、PM2.5 脈沖周期都是微秒甚至毫秒級直接拿 50MHz 計(jì)數(shù)會(huì)很別扭。我一般先做一個(gè) 1μs 脈沖發(fā)生器把它作為全工程的節(jié)拍基準(zhǔn)其他延時(shí)都用“多少個(gè) tick”來表達(dá)代碼可讀性和可維護(hù)性都會(huì)好很多。// 50MHz 時(shí)鐘產(chǎn)生 1us 節(jié)拍脈沖 module clk_1us_pulse ( input wire clk, // 50MHz 系統(tǒng)時(shí)鐘 input wire rst_n, // 低電平復(fù)位 output reg tick_1us // 高電平持續(xù)一個(gè)時(shí)鐘周期 ); reg [5:0] counter; parameter CNT_MAX 50; // 50MHz - 1us 計(jì)數(shù) 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 計(jì)到 49第 50 個(gè)時(shí)鐘周期拉高 tick_1us之后清零。tick_1us 只持續(xù)一個(gè)時(shí)鐘周期本質(zhì)上是一個(gè) enable 信號(hào)。后續(xù)模塊在 always 塊里判斷 if (tick_1us) 才更新計(jì)數(shù)器就能實(shí)現(xiàn)微秒級計(jì)時(shí)。參數(shù)說明CNT_MAX 是唯一需要改的參數(shù)。系統(tǒng)時(shí)鐘 50MHz 時(shí)是 50100MHz 時(shí)改成 100。如果時(shí)鐘不是整數(shù)關(guān)系比如 60MHz建議先用 PLL 整到 100MHz 再做分頻別在計(jì)數(shù)器里算小數(shù)邊界判斷容易出玄學(xué)問題。這也是 FPGA 開發(fā)里最常見的低級坑分頻系數(shù)沒有整除導(dǎo)致微秒周期忽長忽短。有了 tick_1us 之后18ms 延時(shí)就是從 0 數(shù)到 18000。幀周期、采集周期、串口波特率都可以從這個(gè)基準(zhǔn)派生整個(gè)工程的時(shí)序邏輯單一來源排錯(cuò)時(shí)只需要查一個(gè)地方。3.2 DHT11 單總線控制器三段式狀態(tài)機(jī)的標(biāo)準(zhǔn)寫法DHT11 控制器的關(guān)鍵就是狀態(tài)轉(zhuǎn)移時(shí)機(jī)和雙向 IO 控制。我用三段式狀態(tài)機(jī)把狀態(tài)邏輯、次態(tài)判斷、輸出邏輯分開綜合后時(shí)序清晰上板后也方便用 ILA 抓內(nèi)部信號(hào)。// DHT11 單總線控制器狀態(tài)機(jī)框架 module dht11_ctrl ( input wire clk, // 50MHz input wire rst_n, inout wire dht_data, // 單總線數(shù)據(jù)三態(tài) 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; // 釋放總線等待響應(yīng) localparam S_READ 3d3; // 讀 40bit 數(shù)據(jù) reg [2:0] state, next_state; reg [19:0] cnt_us; // 微秒計(jì)數(shù)器 reg [5:0] bit_cnt; // bit 索引 0~39 reg [39:0] data_shift; // 接收移位寄存器 reg dht_out_en; // 輸出使能高電平時(shí)驅(qū)動(dòng)總線 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 語句實(shí)現(xiàn)了三態(tài)門dht_out_en 為高時(shí)把總線拉低為低時(shí)釋放總線交給上拉電阻和從機(jī)控制。S_START_L 狀態(tài)把 cnt_us 計(jì)到 20000 才會(huì)釋放留出 2ms 余量應(yīng)對傳感器個(gè)體差異。釋放后等 40μs 進(jìn)入讀狀態(tài)。參數(shù)說明20_000 這個(gè)值比手冊要求的 18ms 多了一點(diǎn)實(shí)測發(fā)現(xiàn)有些 DHT11 器件在 18ms 邊界不響應(yīng)放寬到 20ms 能提高兼容性。S_READ 里每 1μs 采樣一次總線電平連續(xù)低電平結(jié)束后開始計(jì)高電平寬度寬度小于 30 判 0、大于 65 判 1。這個(gè)閾值需要跟數(shù)據(jù)手冊核對不同批次傳感器高電平寬度會(huì)有波動(dòng)把閾值設(shè)在 45μs 中間值最安全。3.3 滑動(dòng)窗口濾波把 PM2.5 毛刺壓下去的實(shí)用方法PM2.5 傳感器輸出毛刺比溫濕度多得多因?yàn)闅饬?、顆粒物隨機(jī)分布導(dǎo)致 ADC 值跳變是常態(tài)。不加濾波直接把原始值送上位機(jī)畫出來的曲線就是一根“毛線”。我在 FPGA 里做了滑動(dòng)窗口平均沒有用復(fù)雜的中值濾波面積和延遲都可控。// 16 點(diǎn)滑動(dòng)窗口平均用于 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; // 累加器擴(kuò)展位數(shù)防溢出 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 位等價(jià)除 16 end endmodule邏輯說明這是增量式環(huán)形緩沖。每個(gè) data_valid 周期累加器加新采樣值、減最舊采樣值然后指針加一。輸出取 sum 的高位段右移 4 位相當(dāng)于除以 16避免了硬核除法器。參數(shù)說明WIN_W 必須是 2 的冪這樣除法才能用移位替代。累加器位寬在 DATA_W 基礎(chǔ)上加了 4 位因?yàn)?16 個(gè) 12bit 數(shù)求和最大需要 16bit我留了余量防止溢出。窗口大小會(huì)影響滯后程度PM2.5 是秒級變化16 點(diǎn)窗口在 10ms 采樣率下延遲約 160ms完全可接受如果后續(xù)接的是 1Hz 慢速傳感器窗口改 8 點(diǎn)即可不然曲線會(huì)過于遲鈍溫度變化半天看不出來。4. 數(shù)據(jù)上送與仿真驗(yàn)證UART 幀協(xié)議、Testbench 與波形檢查4.1 UART 發(fā)送模塊波特率誤差別忽略采集到的數(shù)據(jù)最終要送到 PC 上。串口是最省事的路我統(tǒng)一用 115200bps、8 數(shù)據(jù)位、無校驗(yàn)、1 停止位。這個(gè)配置上位機(jī)不用額外設(shè)置兼容性最好。// UART 發(fā)送器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; // 波特率分頻計(jì)數(shù) reg [3:0] bit_idx; // 當(dāng)前發(fā)送位索引 reg [9:0] shift_reg; // {停止位, 數(shù)據(jù), 起始位} 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 上升沿觸發(fā)裝載把 10bit 串行幀放進(jìn)移位寄存器。txd 從低位的起始位開始輸出每位保持 BAUD_CNT 個(gè)時(shí)鐘周期發(fā)完停止位后釋放 tx_busy。參數(shù)說明BAUD_CNT 434計(jì)算方式是 50_000_000 / 115200 ≈ 434.03取整后實(shí)際波特率誤差約 0.007%。這個(gè)誤差對 1 字節(jié)短幀沒有任何影響但如果協(xié)議里定義了超過 8 字節(jié)的長幀且持續(xù)發(fā)送誤差會(huì)累積極端情況下會(huì)收到錯(cuò)位數(shù)據(jù)。處理辦法是遇到長幀改成獨(dú)立計(jì)數(shù)或直接用 FPGA 內(nèi)部的 UART IP。4.2 幀協(xié)議設(shè)計(jì)幀頭、類型、負(fù)載、校驗(yàn)裸發(fā)數(shù)據(jù)容易出問題PC 端不知道哪幾個(gè)字節(jié)是溫度、哪幾個(gè)是濕度。我給這套系統(tǒng)設(shè)計(jì)了一個(gè)輕量幀協(xié)議不加多余字段保證上位機(jī)解析邏輯簡單。字節(jié)含義說明0幀頭 0xAA同步用1幀頭 0x55二次確認(rèn)2類型0x01 溫濕度 / 0x02 PM2.53~6負(fù)載溫濕度濕度整數(shù)/濕度小數(shù)/溫度整數(shù)/溫度小數(shù)PM2.5ADC 高字節(jié)/低字節(jié)/濾波值高/低7校驗(yàn)前 7 字節(jié)累加和幀結(jié)構(gòu)說明幀頭固定 0xAA 0x55 是為了讓 PC 端在數(shù)據(jù)流里快速找到幀邊界。類型字段區(qū)分來自哪個(gè)傳感器后續(xù)如果擴(kuò)展到多個(gè)測點(diǎn)可以把類型字節(jié)拆成高 4 位傳感器 ID、低 4 位數(shù)據(jù)類型。這套協(xié)議的特點(diǎn)是固定長度、無轉(zhuǎn)義、累加和夠用。為什么不上更復(fù)雜的協(xié)議因?yàn)椴杉到y(tǒng)每秒只發(fā)兩幀數(shù)據(jù)傳輸壓力小出現(xiàn)連續(xù)丟幀概率低累加和足夠兜底。真要丟幀了上位機(jī)做超時(shí)重連即可沒必要為此引入分包序號(hào)和重傳機(jī)制。4.3 Testbench模擬 DHT11 從機(jī)響應(yīng)和波形檢查要點(diǎn)寫 Testbench 的核心目標(biāo)是讓 DHT11 控制器把完整流程走一遍。我從網(wǎng)上下過不少現(xiàn)成 TB大多只驗(yàn)證了狀態(tài)機(jī)跳轉(zhuǎn)沒驗(yàn)證數(shù)據(jù)采樣正確性。正確做法是在 TB 里寫一個(gè) DHT11 行為模型按時(shí)序反向驅(qū)動(dòng)總線。// DHT11 行為模型模擬從機(jī)響應(yīng)并發(fā)送 40bit 數(shù)據(jù) 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); // 檢測起始信號(hào) #20_000; // 跳過 18ms 起始低電平 drive_low(80); // 響應(yīng)低電平 // 按 bit 輸出數(shù)據(jù)每位低 50us高電平寬度區(qū)分 0/1 end endmodule邏輯說明模型里 wait 語句檢測總線被拉低然后延時(shí) 20ms模擬 DHT11 的響應(yīng)時(shí)序。resp_data 中前 8bit 是濕度整數(shù) 0x1521%、溫度整數(shù) 0x1925℃最后 8bit 是校驗(yàn)三字節(jié)相加 0x150x000x190x000x2ETB 里要核對校驗(yàn)是否一致。仿真波形里重點(diǎn)看三個(gè)點(diǎn)起始信號(hào)低電平是否持續(xù)足夠時(shí)間S_READ 狀態(tài)中采樣時(shí)刻是否落在每個(gè) bit 的穩(wěn)定區(qū)最終 humidity、temperature 和 checksum 與 resp_data 是否逐位匹配。如果校驗(yàn)不對先看狀態(tài)機(jī)是不是提前把總線釋放了讓 DHT11 模型根本沒進(jìn)入響應(yīng)流程。5. 上板調(diào)試與常見問題排查約束、毛刺和數(shù)值漂移的翻車實(shí)錄5.1 管腳與時(shí)序約束XDC 里必須寫清楚的幾件事上板之前先做管腳約束這步不能偷懶。我用 Xilinx 平臺(tái)時(shí)XDC 至少要包含時(shí)鐘管腳、復(fù)位按鍵、LED、DHT11 數(shù)據(jù)線、ADC 接口、UART 輸出。隨便寫個(gè)綜合不報(bào)錯(cuò)的約束跟沒寫差不多時(shí)序約束才是決定系統(tǒng)能不能穩(wěn)定跑的關(guān)鍵。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 聲明了系統(tǒng)時(shí)鐘周期 20ns也就是 50MHz這是時(shí)序分析的基礎(chǔ)。PULLUP true 讓 FPGA 內(nèi)部啟用弱上拉如果板子上沒有為 DHT11 接 5kΩ 外部上拉這個(gè)設(shè)置能讓總線在釋放時(shí)保持高電平但可靠性不如外部電阻二選一時(shí)優(yōu)先外部上拉。注意如果 DHT11 用 5V 供電而 FPGA 是 3.3V bank電平兼容是必須解決的問題。我踩過直接把 5V 數(shù)據(jù)線接 3.3V IO 的坑現(xiàn)象是偶爾能讀到數(shù)據(jù)、大多數(shù)時(shí)候校驗(yàn)失敗。原因不是時(shí)序而是開漏結(jié)構(gòu)和 FPGA 輸入保護(hù)二極管在拉扯電平。后來加了雙向電平轉(zhuǎn)換電路問題立刻消失。這個(gè)坑在畢設(shè)和課設(shè)里特別常見因?yàn)榘遄由系?3.3V 和 5V 電源都有學(xué)生容易忽略傳感器邏輯電平和 FPGA IO bank 電壓的匹配。5.2 上板調(diào)試順序先通串口再接傳感器上板驗(yàn)證的順序我建議固定成這樣先寫一個(gè)串口自發(fā)自收的測試發(fā)固定字符串確認(rèn) UART 鏈路通再只接 DHT11看溫濕度讀數(shù)是否合理最后接 PM2.5 和 ADC看電壓曲線是否平滑。每加一個(gè)模塊驗(yàn)證一次不要一次性把所有傳感器接上去再逐個(gè)猜問題。綜合實(shí)現(xiàn)之后先看時(shí)序報(bào)告里的 setup timing。如果 DHT11 控制器跨時(shí)鐘域采樣有違例最常見的手段是在總線輸入管腳打一拍同步去掉亞穩(wěn)態(tài)窗口。ADC 數(shù)據(jù)如果是異步的也需要同步寄存器。這些在仿真里看不出來只有上板跑起來才會(huì)暴露。5.3 常見問題排查現(xiàn)象、原因、解決現(xiàn)象 1DHT11 讀出的溫濕度一直是 00校驗(yàn)必然不對。原因起始低電平?jīng)]保持住 18ms 就釋放了DHT11 根本沒響應(yīng)。解決把 S_START_L 的計(jì)數(shù)閾值從 18_000 放寬到 20_000同時(shí)檢查 dht_out_en 釋放后是否真正轉(zhuǎn)成高阻別讓輸出驅(qū)動(dòng)一直占著總線?,F(xiàn)象 2PM2.5 電壓跳動(dòng)范圍 ±50mV換算成濃度忽高忽低。原因采樣時(shí)刻沒有避開 LED 脈沖的開關(guān)噪聲。解決把采樣使能延時(shí)到 LED 脈沖上升沿之后 280μs并且保證兩次采樣間隔不小于 10ms。如果還跳檢查 ADC 基準(zhǔn)電壓是否穩(wěn)定建議在 ADC 參考腳加 0.1uF 退耦電容。現(xiàn)象 3串口上位機(jī)幀粘連或者丟字節(jié)。原因UART 發(fā)送模塊還在 busy下一幀數(shù)據(jù)已經(jīng)裝載進(jìn)來兩個(gè)幀擠在一起。解決組幀邏輯必須等到 tx_busy 為低再觸發(fā) tx_start上位機(jī)接收端加一個(gè) 256 字節(jié)環(huán)形緩沖避免操作系統(tǒng)調(diào)度延遲丟數(shù)據(jù)。現(xiàn)象 4上電第一次讀數(shù)正常跑幾分鐘后溫度從 25℃ 漂到 35℃。原因DHT11 離板載穩(wěn)壓芯片太近熱量直接烤在傳感器上。解決把傳感器挪到板邊或用排線引出別讓電路板的局部熱點(diǎn)影響測量。這個(gè)現(xiàn)象不是 FPGA 代碼問題是結(jié)構(gòu)布局問題但最容易迷惑人因?yàn)榭雌饋硐駭?shù)據(jù)采集邏輯壞了。現(xiàn)象 5仿真完全正常上板后 DHT11 在低溫環(huán)境讀不出來。原因DHT11 的時(shí)序參數(shù)受溫度和濕度影響18ms 起始信號(hào)在低溫下可能不夠。解決起始信號(hào)放寬到 20ms位判斷閾值從 30μs/65μs 調(diào)整為 25μs/70μs擴(kuò)大判決窗口。犧牲一點(diǎn)判別精度換穩(wěn)定性值得。6. 板級驗(yàn)證進(jìn)階Python 腳本抓串口做 24 小時(shí)漂移觀測上板能出數(shù)據(jù)只是開始真正讓這套系統(tǒng)設(shè)計(jì)可信的是長時(shí)間穩(wěn)定性監(jiān)測。我驗(yàn)證的方法是串口助手配合 Python 腳本做 24 小時(shí)跑測腳本按幀協(xié)議解析串口數(shù)據(jù)把 DHT11 溫度、濕度、PM2.5 原始 ADC 值和濾波值分別落盤順手畫一張時(shí)間曲線。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 字節(jié)一幀同步串口流parse_frame 先校驗(yàn)幀頭和累加和再按類型位拆分負(fù)載把數(shù)據(jù)追加到 records 列表方便后續(xù)畫圖或入庫。這個(gè)腳本的價(jià)值在于把 FPGA 內(nèi)部看不到的采樣數(shù)據(jù)暴露在上位機(jī)圖上。我第一次跑 24 小時(shí)時(shí)發(fā)現(xiàn) PM2.5 原始值在凌晨有一段持續(xù)偏高排查后確認(rèn)是傳感器進(jìn)風(fēng)口被灰塵堵了一半——這不是電路問題是結(jié)構(gòu)問題。從那以后我每次做傳感器采集都強(qiáng)制先跑一遍 24 小時(shí)數(shù)據(jù)監(jiān)測再回去改代碼。跑測時(shí)重點(diǎn)看兩點(diǎn)數(shù)值基線是否穩(wěn)定濾波曲線有沒有隨時(shí)間的趨勢性漂移。趨勢性漂移往往意味著傳感器老化或環(huán)境變化而不是代碼 bug。從那以后我拿到任何現(xiàn)成的 Verilog 工程也會(huì)把驗(yàn)證腳本一起準(zhǔn)備先仿真跑協(xié)議再上板掛一晚上數(shù)據(jù)曲線出來才算驗(yàn)收通過。不想從零搭這套東西的話FPGA verilog PM2.5 溫濕度傳感器系統(tǒng)設(shè)計(jì)資源里一般會(huì)帶上完整的 RTL 源碼、約束文件和上位機(jī)解析腳本省掉最耗時(shí)的時(shí)序調(diào)校環(huán)節(jié)。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取