現(xiàn)ROM存儲圖片的VGA顯示方案:從存儲計算到時序?qū)R)
簡介本資源面向FPGA初學(xué)者與數(shù)字電路設(shè)計愛好者提供一套通過ROM存儲64×64像素圖片并經(jīng)VGA接口顯示的完整工程實(shí)踐方案幫助讀者理解從圖像數(shù)據(jù)生成到硬件顯示的完整鏈路。壓縮包共245個文件約1.12MB以hdb、cdb、tdf、vwf等Quartus工程中間文件為主同時包含8個v文件Verilog源碼、mif存儲器初始化文件、qsf引腳約束、sof與pof下載文件及readme說明覆蓋綜合、仿真、布局布線與下載全流程。已有1056人學(xué)習(xí)下載適合作為課程設(shè)計或自學(xué)練手項(xiàng)目。資源核心價值在于將MATLAB生成mif數(shù)據(jù)、Verilog編寫ROM與VGA控制器、時序信號生成及PLL時鐘配置等知識點(diǎn)串聯(lián)為可運(yùn)行工程讀者可據(jù)此掌握像素數(shù)據(jù)到RGB信號的轉(zhuǎn)換方法并借助仿真波形與工程文件排查顯示異??焖偻瓿蓮臄?shù)據(jù)準(zhǔn)備到板級驗(yàn)證的閉環(huán)實(shí)踐。1. 從一塊 ROM 說起為什么圖片顯示要先解決存儲問題做 VGA 顯示的項(xiàng)目很多人第一反應(yīng)是「怎么把圖顯示出來」于是埋頭去調(diào)時序、算行場同步結(jié)果圖像要么花屏、要么顏色錯位折騰幾天才發(fā)現(xiàn)根子不在顯示模塊而在圖片數(shù)據(jù)根本沒被正確讀出。這個項(xiàng)目的核心其實(shí)是一件事把一張圖片預(yù)先寫進(jìn) FPGA 內(nèi)部的 ROM再按 VGA 時序一個像素一個像素地讀出來送到屏幕上。它適合正在做數(shù)字邏輯課程設(shè)計、FPGA 入門畢設(shè)的同學(xué)也適合想搞清楚「存儲—讀取—顯示」這條鏈路的從業(yè)者。選 ROM 而不是 RAM是因?yàn)閳D片是靜態(tài)的、只讀的用 ROM 綜合出來的塊存儲更省資源、時序更干凈這就是這個方案最反直覺也最省事的地方。下面從需求、架構(gòu)、實(shí)現(xiàn)到驗(yàn)收把整套方案拆開講清楚。2. 需求拆解與技術(shù)選型分辨率、色深和存儲介質(zhì)怎么定2.1 先算清楚一張圖要占多少存儲動手寫代碼之前必須先把「一張圖到底要多少 bit」算明白否則綜合時塊存儲爆掉或者圖像被截斷都是血淚經(jīng)驗(yàn)。假設(shè)目標(biāo)分辨率是 640×480色深用 RGB332紅綠藍(lán)各占 3、3、2 位共 8 位那么一幀像素總數(shù)是 640×480 307200 個每個像素 8 位總共需要 307200 字節(jié)約 300 KB。這個數(shù)字直接決定了你能不能塞進(jìn)片內(nèi) ROM。通常的做法是入門板卡片內(nèi)塊存儲有限640×480 全彩往往放不下所以要么降分辨率比如 320×240存儲量降到 1/4約 75 KB要么降色深RGB121 或單色。我一般會先查目標(biāo)器件的塊存儲容量再反推能支持的最大分辨率而不是先定分辨率再硬塞。下面這張表是幾種常見組合的存儲占用方便你直接對照選型。分辨率色深單像素位寬總存儲量適用場景640×480RGB3328 bit約 300 KB資源充足的器件320×240RGB3328 bit約 75 KB入門板卡首選320×240RGB1214 bit約 37.5 KB資源緊張時160×120RGB3328 bit約 18.75 KB驗(yàn)證流程用選型邏輯很直接先看器件塊存儲夠不夠再決定分辨率和色深。別一上來就追求 640×480 真彩翻車概率極高。2.2 為什么用 ROM 而不是 RAM以及初始化文件怎么來ROM 在這個項(xiàng)目里的角色是「圖片倉庫」。它的地址線接像素計數(shù)器數(shù)據(jù)線輸出像素顏色值只要地址給對顏色就自動出來不需要寫使能、不需要讀寫切換時序上少了一大堆麻煩。RAM 雖然可寫但圖片是靜態(tài)的用 RAM 屬于殺雞用牛刀還容易因?yàn)樽x寫沖突引入玄學(xué)問題。ROM 的內(nèi)容需要在綜合前初始化通常用.mifMemory Initialization File或.hex文件。生成方式一般是用腳本把一張 BMP 或 PNG 圖片轉(zhuǎn)成逐像素的顏色數(shù)據(jù)。下面是一段把圖片轉(zhuǎn)成 mif 的 Python 示例邏輯是讀圖、量化顏色、按地址逐行寫出。from PIL import Image # 打開圖片并統(tǒng)一到目標(biāo)分辨率 img Image.open(source.png).convert(RGB).resize((320, 240)) WIDTH, HEIGHT 320, 240 with open(image.mif, w) as f: f.write(fWIDTH{8};\n) # 每個像素 8 位 f.write(fDEPTH{WIDTH * HEIGHT};\n) f.write(ADDRESS_RADIXDEC;\n) f.write(DATA_RADIXHEX;\n) f.write(CONTENT BEGIN\n) addr 0 for y in range(HEIGHT): for x in range(WIDTH): r, g, b img.getpixel((x, y)) # 量化成 RGB332紅3位 綠3位 藍(lán)2位 pixel ((r 5) 5) | ((g 5) 2) | (b 6) f.write(f{addr} : {pixel:02X};\n) addr 1 f.write(END;\n)這段腳本的關(guān)鍵在量化那一步r 5把 8 位紅壓成 3 位g 5壓成 3 位b 6壓成 2 位再按 RGB332 的位序拼成一個字節(jié)。地址從 0 遞增和后面 ROM 的讀地址一一對應(yīng)。參數(shù)上WIDTH和DEPTH必須和 Verilog 里 ROM 的地址位寬匹配否則綜合會報地址越界。圖片源建議先用小圖驗(yàn)證跑通了再換大圖。3. 總體架構(gòu)設(shè)計像素時鐘、地址生成與 ROM 讀取的配合3.1 三個模塊的職責(zé)劃分整套方案拆成三個模塊最清晰時鐘分頻模塊、VGA 時序控制模塊、ROM 存儲模塊。時鐘分頻負(fù)責(zé)把板卡晶振常見 50 MHz分出一個 25.175 MHz 的像素時鐘640×48060Hz 就靠它VGA 時序控制負(fù)責(zé)產(chǎn)生行同步、場同步信號以及當(dāng)前掃描到的像素坐標(biāo)ROM 存儲模塊則根據(jù)坐標(biāo)換算出地址把顏色數(shù)據(jù)吐出來。這里有個容易忽略的點(diǎn)ROM 讀數(shù)據(jù)有一個時鐘周期的延遲。也就是說你這一拍給出地址下一拍數(shù)據(jù)才出來。如果直接把 ROM 輸出接到 VGA 顏色線上圖像會整體右移一個像素。通常的做法是在時序模塊里把行場同步信號也延遲一拍讓顏色和同步對齊這個細(xì)節(jié)后面避坑章節(jié)會展開。3.2 地址換算從 (x, y) 到線性地址ROM 是一維的而屏幕是二維的所以必須把像素坐標(biāo) (x, y) 映射成線性地址。公式很簡單addr y * WIDTH x。在 Verilog 里如果 WIDTH 是 2 的冪比如 512可以直接用移位和拼接省資源但 320 不是 2 的冪就得老老實(shí)實(shí)做乘法或者用查找表。下面這段是地址生成的核心邏輯。// 像素坐標(biāo)轉(zhuǎn) ROM 線性地址 // H_ACTIVE / V_ACTIVE 為有效顯示區(qū)寬度和高度 wire [18:0] rom_addr; assign rom_addr v_cnt * H_ACTIVE h_cnt; // ROM 例化讀延遲一拍 rom_image u_rom ( .address (rom_addr), .clock (pixel_clk), .q (pixel_data) // 8 位 RGB332 顏色 );v_cnt和h_cnt是當(dāng)前掃描的行、列計數(shù)只在有效顯示區(qū)內(nèi)累加。H_ACTIVE是每行有效像素數(shù)乘出來就是行偏移。注意rom_addr的位寬要夠320×240 需要 76800 個地址至少 17 位這里給 19 位留了余量。ROM 例化時clock接像素時鐘保證讀地址和顯示節(jié)奏同步。3.3 時序參數(shù)640×48060Hz 的完整配置VGA 時序是一堆數(shù)字配錯一個就黑屏或抖動。640×48060Hz 的標(biāo)準(zhǔn)參數(shù)如下這些值來自 VGA 行業(yè)規(guī)范直接照抄即可。參數(shù)行水平場垂直有效像素640480前沿1610同步脈沖962后沿4833總周期800525行同步和場同步都是低電平有效同步脈沖期間拉低。計數(shù)器從 0 數(shù)到總周期減一落在有效區(qū)間的坐標(biāo)才去讀 ROM。這套參數(shù)配對了屏幕才會穩(wěn)定顯示配錯了就是滿屏雪花或者圖像滾動。4. 關(guān)鍵模塊實(shí)現(xiàn)Verilog 代碼與逐段說明4.1 時鐘分頻與像素時鐘生成板卡晶振通常是 50 MHz而 640×48060Hz 需要 25.175 MHz 的像素時鐘兩者不是整數(shù)倍關(guān)系。工程上一般用鎖相環(huán)PLL來生成比手寫分頻器穩(wěn)定得多。如果器件沒有 PLL或者只是做課程設(shè)計圖省事也可以用計數(shù)器分頻但頻率會有偏差屏幕可能輕微抖動。// 用 PLL 生成 25.175MHz 像素時鐘以常見 IP 核為例 // 輸入 50MHz輸出 pixel_clk 和 locked 信號 pll_vga u_pll ( .inclk0 (clk_50m), .c0 (pixel_clk), // 25.175MHz .locked (pll_locked) ); // 復(fù)位信號PLL 未鎖定時保持復(fù)位避免時序錯亂 wire sys_rst_n rst_n pll_locked;這里把pll_locked和外部復(fù)位相與是個實(shí)用習(xí)慣PLL 鎖定前輸出不穩(wěn)定如果此時就讓時序模塊跑起來屏幕會先花一下。等鎖定后再釋放復(fù)位顯示就干凈了。參數(shù)上PLL 的倍頻、分頻系數(shù)由 IP 核向?qū)愫貌挥檬炙恪?.2 VGA 時序控制模塊時序模塊是整個方案的心臟它同時產(chǎn)生行場同步和像素坐標(biāo)。下面這段是精簡后的核心邏輯行場計數(shù)器各自數(shù)到總周期歸零同步信號在指定區(qū)間拉低。// 行計數(shù)0 ~ 799 always (posedge pixel_clk or negedge sys_rst_n) begin if (!sys_rst_n) h_cnt 0; else if (h_cnt H_TOTAL - 1) h_cnt 0; else h_cnt h_cnt 1; end // 行同步96~191 區(qū)間拉低 assign hsync ~((h_cnt H_SYNC_START) (h_cnt H_SYNC_END)); // 場計數(shù)每行結(jié)束加一0 ~ 524 always (posedge pixel_clk or negedge sys_rst_n) begin if (!sys_rst_n) v_cnt 0; else if (h_cnt H_TOTAL - 1) begin if (v_cnt V_TOTAL - 1) v_cnt 0; else v_cnt v_cnt 1; end end // 場同步2~3 區(qū)間拉低 assign vsync ~((v_cnt V_SYNC_START) (v_cnt V_SYNC_END)); // 有效顯示區(qū)標(biāo)志 assign display_en (h_cnt H_ACTIVE) (v_cnt V_ACTIVE);H_TOTAL是 800V_TOTAL是 525H_SYNC_START是 65664016H_SYNC_END是 752。這些常量建議用parameter定義改分辨率時只改一處。display_en用來控制 ROM 地址是否累加非顯示區(qū)地址保持不變避免讀出無效數(shù)據(jù)。4.3 ROM 讀取與顏色輸出對齊前面提過 ROM 有一拍延遲所以顏色輸出要和同步信號對齊。做法是把hsync、vsync、display_en都打一拍和pixel_data同步輸出。// 同步信號延遲一拍與 ROM 數(shù)據(jù)對齊 reg hsync_d, vsync_d, de_d; always (posedge pixel_clk or negedge sys_rst_n) begin if (!sys_rst_n) begin hsync_d 1b1; vsync_d 1b1; de_d 1b0; end else begin hsync_d hsync; vsync_d vsync; de_d display_en; end end // 顏色輸出非顯示區(qū)給黑色 assign vga_r de_d ? pixel_data[7:5] : 3b000; assign vga_g de_d ? pixel_data[4:2] : 3b000; assign vga_b de_d ? pixel_data[1:0] : 2b00;pixel_data是 ROM 輸出的 8 位 RGB332拆成 3、3、2 位分別送到 VGA 的紅綠藍(lán)通道。de_d為低時輸出黑色這樣消隱區(qū)不會出現(xiàn)雜色。這一步對齊做對了圖像位置才準(zhǔn)確否則會整體偏移一個像素肉眼看得出來。5. 避坑與常見問題排查五個最容易翻車的地方5.1 圖像整體右移或下移一個像素現(xiàn)象是顯示出來的圖比預(yù)期偏右或偏下。原因是 ROM 讀延遲一拍而同步信號沒延遲顏色和坐標(biāo)錯位。解決辦法就是把hsync、vsync、display_en都打一拍再輸出和pixel_data對齊也就是 4.3 節(jié)那段代碼。這個坑幾乎每個新手都會踩一次。5.2 屏幕花屏或顏色錯亂現(xiàn)象是滿屏彩色噪點(diǎn)或者顏色明顯不對。原因通常是 mif 文件的地址位寬和 ROM 配置不匹配或者量化時位序拼錯了。排查方法是先用一張純色圖比如全紅生成 mif看屏幕是不是純紅如果不是就是位序問題。RGB332 的位序是{R[2:0], G[2:0], B[1:0]}拼錯一位顏色就全亂。5.3 綜合報塊存儲不足現(xiàn)象是綜合工具提示 RAM/ROM 資源超限。原因是分辨率或色深定太高片內(nèi)存儲放不下。解決辦法是降分辨率或降色深參考 2.1 節(jié)的表格重新選型。也可以把 ROM 改成從外部存儲讀取但那就超出這個方案的范圍了。5.4 屏幕抖動或不同步現(xiàn)象是圖像滾動、抖動或者顯示器提示「無信號」。原因是像素時鐘頻率不對或者行場總周期參數(shù)配錯。排查時先用示波器量hsync和vsync的頻率640×48060Hz 的行頻應(yīng)該是 31.5 kHz場頻 60 Hz。頻率不對就回去檢查 PLL 配置和時序常量。5.5 mif 文件生成后 ROM 讀出來全是 0現(xiàn)象是屏幕全黑ROM 輸出恒為 0。原因是 mif 文件路徑?jīng)]被綜合工具識別或者文件格式有誤。檢查方法是看綜合日志里有沒有「initialized with mif」之類的提示沒有就說明文件沒加載。另外 mif 的DEPTH必須和 ROM 深度一致ADDRESS_RADIX和DATA_RADIX也要和代碼里的進(jìn)制對上。6. 驗(yàn)收與答辯怎么證明這套方案真的跑通了驗(yàn)收這件事很多人以為「屏幕亮了」就算完其實(shí)答辯時老師或評審最想看的是你能不能拿出可復(fù)現(xiàn)的證據(jù)鏈。我一般會準(zhǔn)備三樣?xùn)|西一份時序波形截圖、一組對照實(shí)驗(yàn)、一份資源占用報告。先說波形。用仿真工具跑一段把hsync、vsync、display_en、rom_addr、pixel_data都抓出來重點(diǎn)看rom_addr在有效顯示區(qū)是否連續(xù)遞增pixel_data是否比地址晚一拍。這張圖能直接證明你理解了 ROM 讀延遲比嘴上說一百遍都管用。下面是一個簡單的仿真激勵片段用來產(chǎn)生幾個像素時鐘觀察地址和數(shù)據(jù)的關(guān)系。// 仿真激勵給足時鐘觀察 ROM 讀時序 initial begin clk_50m 0; rst_n 0; #100 rst_n 1; // 釋放復(fù)位 #1000000; // 跑足夠長時間覆蓋多行 $stop; end always #10 clk_50m ~clk_50m; // 50MHz 時鐘這段激勵跑起來后在波形窗口里量rom_addr和pixel_data的相位差確認(rèn)是一拍。參數(shù)上#1000000是仿真時長要足夠覆蓋至少一整幀否則看不到完整的行場周期。再說對照實(shí)驗(yàn)。準(zhǔn)備兩張 mif一張純色、一張簡單圖案比如棋盤格。純色圖驗(yàn)證顏色通道對不對棋盤格驗(yàn)證地址映射對不對。如果純色顯示正確但棋盤格錯位說明地址換算有問題如果純色都不對說明顏色位序或 ROM 初始化有問題。這種二分排查法在答辯時特別有說服力評審一看就知道你是系統(tǒng)性地驗(yàn)證過而不是碰運(yùn)氣調(diào)通的。最后是資源占用報告。綜合完成后工具會給出一份報告里面列出用了多少邏輯單元、多少塊存儲、多少 IO。把這份報告和 2.1 節(jié)的存儲估算對照如果實(shí)際占用和估算接近說明你的設(shè)計是可控的如果差很多就要回頭查是不是 ROM 被綜合成了分布式存儲而不是塊存儲。答辯時把這份報告一亮等于告訴評審這套方案我心里有數(shù)。還有個小技巧答辯現(xiàn)場如果屏幕效果不理想別慌著解釋直接切到仿真波形用波形證明邏輯是對的問題可能出在板卡或連線。評審更看重你的分析能力而不是一次成功。從那以后我每次做顯示類項(xiàng)目都強(qiáng)制先跑仿真波形再上板這個習(xí)慣幫我省了無數(shù)個熬夜的夜晚。希望這套方案能幫到你把 ROM 存儲圖片的 VGA 顯示一次做通。本文還有配套的精品資源點(diǎn)擊獲取