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