十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA实战:Verilog实现SD卡图像读取与VGA显示

FPGA实战:Verilog实现SD卡图像读取与VGA显示 简介这份FPGA设计资源提供读取SD卡中图片并通过VGA屏显示输出的完整Verilog工程源码基于Cyclone IV E系列EP4CE10F17C8器件Quartus 18.0环境面向FPGA学习者、嵌入式及数字图像显示方向开发者解决SD卡SPI读取、SDRAM缓存与VGA时序控制联动问题。压缩包约9.86MB共375个文件以.v/.tdf源码、.qsf/.qpf工程配置、.sdc时序约束和.sof下载文件为主另有.tcl脚本、图片素材及readme说明目录结构完整。已有721人学习下载。工程顶层top_sd_photo_vga清晰划分sd_ctrl_top、sdram_top、vga_driver、sd_read_photo等子模块涵盖SD卡读扇区、SDRAM缓存、640×480分辨率VGA输出适合对照源码理解跨时钟域与接口时序也可作为毕业设计或课程设计的基础框架。1. 从SD卡到VGA图片数据流的“节奏”才是设计核心把SD卡读出来的图片直接接到VGA显示器上画面只会是一团灰白的闪条——因为SD卡在SPI模式下是突发传输一次读512字节中间夹着命令、响应和CRC而且FIFO随时可能被读空。而VGA要求每个像素时钟周期都稳定送出一个像素停一拍画面就闪。这个工程真正要解决的问题是把SPI读卡、FAT32文件解析、VGA时序生成和跨时钟域缓冲这四件事串成一条数据流水线。对已经写过流水灯和UART的开发者来说这套设计正好把状态机设计、异步FIFO、时序约束和板级调试这些高频技能串到了一起对刚接触FPGA的人它也是一个能看得见、摸得着的完整项目比跑仿真更有成就感。下面就从最底层SD卡读命令讲起一步步把这条链路搭建出来。2. 用Verilog在SPI模式下初始化SD卡先跑通单块读命令2.1 为什么优先选SPI模式以及SD卡的引脚怎么接SD卡支持SD总线和SPI两种工作模式。SD总线用4bit数据线并行传输理论带宽更高但主机侧要处理CMD线、数据线、CRC校验和更复杂的协议状态在FPGA里实现工作量明显大于SPI。SPI模式只需要SCK、MOSI、MISO、CS四根线逻辑简单FPGA侧只用移位寄存器和状态机就能驱动。图片显示这类应用数据量一般在几十KB到几百KB之间SPI模式下25MHz时钟、每字节8个时钟周期理论吞吐约3MB/s完全够用。硬件连接上常见做法是FPGA的3.3V IO直接连SD卡的SPI引脚CS、MOSI、SCK各接一个10kΩ上拉到3.3VMISO是SD卡输出不需要上拉。VCC和VSS之间加大于10μF的退耦电容因为SD卡读卡瞬间电流变化很大。提示SD卡的物理写保护滑块和SPI模式是两套独立机制很多卡座上读卡失败都源于卡座弹片接触不良在排错时先排除卡座问题否则会一直怀疑代码。2.2 SD卡进入SPI模式的完整时序SD卡上电后默认进入SD总线模式必须发送特定命令序列才能切到SPI模式。整个过程分两步先让CS保持高电平发送至少74个SCK脉冲让卡完成内部上电初始化并同步时钟。然后把CS拉低发送CMD0如果收到响应0x01表示卡已进入SPI模式。SD卡SPI命令是6字节固定格式第一个字节是01 命令号命令号6bit接着4字节是大端序参数最后一个字节是CRC7校验加停止位1。下表列出了初始化时需要用到的命令。命令编号字节参数说明CMD00x400x00000000复位卡进入SPI模式返回0x01CMD80x480x000001AA判断卡类型SDHC卡返回0x01后跟4字节CMD580x7A0x00000000读取OCR寄存器判断是否支持SDHCCMD170x51块地址32bit读取一个512字节的数据块进入SPI模式后要发送CMD8这个命令带0x1AA参数用来区分SDSC和SDHC卡。随后循环发送ACMD41先发CMD55再发CMD41直到响应为0x00表示卡初始化完成。ACMD41的HCS位bit30在支持SDHC时必须置1否则卡可能以字节寻址方式工作返回值会不同。初始化完成后读操作就固定用块寻址——SDHC卡的地址直接是逻辑块号而不是字节偏移。这在后续FAT32解析时直接影响扇区地址的计算方式。FAT32的扇区号换算成字节地址后要除以512才是CMD17的参数但如果卡是SDSC地址就是字节地址。两种卡混用是新手最常见的初始化错误。2.3 单块读取的状态机设计从发命令到收数据SD卡在SPI模式下读一个块的完整流程发CMD17等待R1响应0x00表示成功然后继续等数据起始令牌0xFE0xFE之后跟着512字节有效数据最后是2字节CRC。注意SPI模式下的CRC可以不校验但必须把两个字节读掉否则卡发下一块数据时会错位。下面是一个精简版的状态机代码片段直接用Verilog实现CMD17的发送和数据接收。// sd_read_fsm.v // 支持CMD17单块读取的状态机数据输出到rd_data localparam IDLE 3d0; localparam SEND_CMD 3d1; localparam WAIT_R1 3d2; localparam WAIT_TOKEN 3d3; localparam READ_DATA 3d4; localparam READ_CRC 3d5; reg [2:0] state; reg [5:0] bit_cnt; // 发送/接收的bit个数 reg [7:0] tx_byte; // 当前发送字节 reg [7:0] rx_byte; // 当前接收字节 reg [8:0] byte_cnt; // 数据字节计数 0~511 reg [7:0] cmd_buf [5:0]; // 6字节命令缓存 always (posedge sclk or negedge rst_n) begin if (!rst_n) begin state IDLE; cs_n 1b1; end else begin case (state) IDLE: begin cs_n 1b0; state SEND_CMD; end SEND_CMD: begin // 在每个bit的中间位置采样/更新 mosi cmd_buf[cmd_idx][bit_idx]; if (bit_cnt 6d47) begin bit_cnt 6d0; state WAIT_R1; end else begin bit_cnt bit_cnt 1b1; end end // WAIT_R1状态接收8bit值不为0xFF表示卡有响应 endcase end end这段代码演示了命令发送核心逻辑状态机在SEND_CMD状态下逐bit移位发出6字节bit_cnt计数48个SCK周期。WAIT_R1状态要求从MISO采样响应如果读到0xFF说明卡还没准备好继续等待只有读到不是0xFF的字节才认为是有效R1响应。实际使用时CMD17发送前要先在IDLE状态把卡选择信号CS拉低命令发完后保持低电平直到整个数据块接收完。数据接收的完整过程是等待0xFE令牌出现后连续读512个字节再跳过2个CRC字节最后回到IDLE态并把CS拉高。这里的byte_cnt需要计数到512超过视为超时错误FPGA工程里要加一个超时计数器避免卡无响应时状态机卡死。提示SPI的采样沿和输出沿要错开。数据在SCK下降沿变化、上升沿采样是常见做法。初始化阶段时钟频率设低一点400kHz以下正常读写时再切到25MHz这个切换用Quartus里的PLL IP实现最方便。3. 在FPGA内部手写一个FAT32解析器定位图片文件的起始扇区3.1 为什么不在FPGA里移植FatFs而是自己写解析逻辑FatFs是STM32等MCU上常用的文件系统库它依赖malloc、字符串比较、多任务锁等C语言运行环境。FPGA里想用FatFs就得用软核处理器如Nios II跑软件但这个工程标题限定为“Verilog设计”意味着纯逻辑实现。纯RTL直接读FAT表、算簇号、按簇读数据代码量不大查一张目录表只需要几种固定状态比在FPGA里挂一个CPU省资源也更直接。FAT32的文件查找过程是固定的套路先读BPB参数确认每扇区字节数、每簇扇区数、FAT表起始扇区等然后从根目录簇号开始逐条扫描32字节的目录项找到文件名匹配的项目录项里记录了文件起始簇号高16位和低16位拼成32bit簇号最后按文件长度算出要读多少个簇。文件数据在磁盘上可能不连续需要查FAT表拿到下一簇号。3.2 BPB参数表先读取这些偏移量FAT32的BPB位于第0号扇区主引导扇区从偏移地址0x0B开始是一组定长字段。FPGA端把扇区读到内存后按字节偏移取数即可。下面列出最关键的几个字段。偏移地址字段长度内容说明0x0B2字节每扇区字节数FAT32固定为5120x0D1字节每簇扇区数常见值1、8、16、320x244字节FAT表起始扇区号0x2C4字节根目录起始簇号0x204字节总扇区数用于判断卡容量解析逻辑里最需要注意的是字节序FAT32所有多字节字段都是小端存储FPGA端接收到的数据每个字节独立进入FIFO拼接时要把低字节放低位。比如读取0x24处的4字节若依次读到0x00 0x00 0x00 0xF8实际FAT起始扇区是0xF8000000。在Verilog里做BPB解析通常是把扇区的512字节存入RAM然后用状态机逐个偏移读取。代码思路是读扇区时将数据按字节写入reg [7:0] sector_buf[0:511]然后用一个case语句根据当前读取阶段取出对应字节组。3.3 目录项查找从“根目录簇”到“目标文件”的路径FAT32的根目录不是一个固定区域而是一个普通目录从BPB偏移0x2C处的簇号开始。目录项每32字节一条文件名偏移0x0B、扩展名偏移0x0C。目录项结构里偏移0x14是文件起始簇号低16位偏移0x1A是高16位拼起来就是文件的起始簇号偏移0x1C是文件字节长度。FPGA端扫描目录时要按“簇号→扇区→目录项”的顺序遍历。一个簇可能包含多个扇区每扇区512字节包含16个目录项。常见做法是维护一个512字节的扇区缓冲逐条比对目录项的前几个字节是否为目标文件名如PIC.BMP。// fat_dir_scan.v // 目录项扫描匹配文件名并输出起始簇号和文件长度 reg [4:0] dir_cnt; // 当前扇区内的目录项序号 0~15 reg [8:0] byte_idx; // 当前扇区内的字节偏移 reg [31:0] start_cluster; reg [31:0] file_size; always (posedge clk) begin if (match_en) begin // 从sector_buf取32字节目录项 if (sector_buf[byte_idx32d11] P sector_buf[byte_idx32d12] I sector_buf[byte_idx32d13] C) begin start_cluster[15:0] {sector_buf[byte_idx8h15], sector_buf[byte_idx8h14]}; start_cluster[31:16] {sector_buf[byte_idx8h1B], sector_buf[byte_idx8h1A]}; file_size {sector_buf[byte_idx8h1F], sector_buf[byte_idx8h1E], sector_buf[byte_idx8h1D], sector_buf[byte_idx8h1C]}; found_flag 1b1; end end end这里的关键是目录项名称比较时用的是ASCII码字符串字面量Verilog的字符串比较是按字节进行的。sector_buf[byte_idx11]对应目录项的短文件名第一个字符BMP文件在FAT32里如果文件名不超过8字符就直接存储在偏移0x0B。文件名是PIC.BMP时前三个字符匹配即可继续校验扩展名段。拿到起始簇号后读取文件数据时每次读完一个簇要查一次FAT表找到下一簇号。FAT32的FAT表项是4字节FAT表起始扇区加上簇号乘以4得到该簇项的扇区内部偏移。查FAT表就意味着每读一个簇的数据都要先读一个FAT表扇区——视频流这样查会慢但图片只需要几十次查找速度无所谓。提示如果文件名带中文或超过8.3短名格式SD卡会按长文件名机制存储逻辑复杂度大幅上升。做这个显示项目时建议在上位机把图片改成8.3格式的短文件名再拷入SD卡可以省掉LFN解析。3.4 BMP文件在SD卡里的真实存储形态图片文件建议直接采用BMP格式对应搜索里常出现的“bmg”说法。BMP无压缩、无跨行数据校正文件头固定54字节从偏移0x0A处读出像素数据偏移量从0x12处读出图像宽度从0x16处读出高度。RGB888的BMP每像素3字节做成RGB565显示时FPGA端需要把每两个字节拼成一个16bit像素。常见做法是先用PC把图片转成RGB565格式的BMP这样FPGA侧读到的数据流就是连续16bit一个像素不需要做像素格式转换。转换命令用Python脚本几行就写完。from PIL import Image img Image.open(lena.png).convert(RGB) # 统一缩放到 320x240 便于VGA居中显示 img img.resize((320, 240)) # 转RGB565R取高5位G取高6位B取高5位 raw bytearray() for r, g, b in img.getdata(): rgb565 ((r 3) 11) | ((g 2) 5) | (b 3) raw rgb565.to_bytes(2, little) with open(pic.bmp, wb) as f: # 简单BMP头54字节固定 f.write(bBM (len(raw)54).to_bytes(4,little) b\x00\x00\x00\x00 (54).to_bytes(4,little)) f.write((40).to_bytes(4,little) (320).to_bytes(4,little) (240).to_bytes(4,little)) f.write((1).to_bytes(2,little) (16).to_bytes(2,little) (0).to_bytes(4,little) (len(raw)).to_bytes(4,little) (2835).to_bytes(4,little) (2835).to_bytes(4,little) (0).to_bytes(4,little) (0).to_bytes(4,little)) f.write(raw)这段脚本把任意图片转成320×240、RGB565的BMP文件头54字节像素区紧跟在后面。FPGA端读到文件数据后从文件头偏移54字节开始取数据即可完全不需要动态解析BMP信息头。BMP的存储顺序是从下到上、从左到右VGA显示时如果图像上下颠倒把读地址镜像一下就行。4. VGA时序驱动与FIFO缓冲把“突发读”变成“像素流”4.1 640x48060Hz的VGA时序参数VGA接口的针脚定义很规律1脚R、2脚G、3脚B、13脚HSYNC、14脚VSYNC同步信号是负极性TTL电平RGB是模拟电压0~0.7V。FPGA输出端通常用3.3V的LVTTL通过电阻分压网络或专用DAC芯片如ADV7123转换成模拟信号这个项目的Verilog代码一般直接输出数字RGB。以最常用的640×48060Hz为例像素时钟25.175MHzFPGA端一般直接用25MHz。整个行周期800个像素时钟96个同步脉冲、48个后沿、640个有效像素、16个前沿。整个场周期525行2行同步、33行后沿、480行有效、10行前沿。参数H_SYNCH_BACKH_ACTIVEH_FRONT整行像素时钟数964864016800参数V_SYNCV_BACKV_ACTIVEV_FRONT整帧行数23348010525这两个表是写VGA时序生成器的直接依据。行计数器在0~799循环场计数器在0~524循环。当行计数落在96~143区间时HSYNC拉低场计数落在2~34区间时VSYNC拉低有效区域是行计数144~783、场计数35~514这个区间内RGB输出像素数据否则输出黑色全0。// vga_timing.v // VGA 640x48060Hz 时序生成 reg [9:0] h_cnt; reg [9:0] v_cnt; wire h_active (h_cnt 144) (h_cnt 784); wire v_active (v_cnt 35) (v_cnt 515); wire active h_active v_active; assign hsync (h_cnt 96) ? 1b0 : 1b1; assign vsync (v_cnt 2) ? 1b0 : 1b1; // 有效区域取像素坐标 wire [9:0] x h_cnt - 144; wire [9:0] y v_cnt - 35; always (posedge clk) begin if (h_cnt 799) begin h_cnt 0; if (v_cnt 524) v_cnt 0; else v_cnt v_cnt 1b1; end else begin h_cnt h_cnt 1b1; end end assign rgb active ? pixel_data : 16h0000;这段代码是VGA驱动的基础框架x和y就是当前像素在有效区域内的逻辑坐标。active信号作为读FIFO的使能信号在有效区每个像素时钟读取一个像素数据在消隐区强迫输出黑色。像素数据的位宽取决于RGB输出管脚数量实例工程里通常是16bitRGB565通过3个电阻网络转成模拟信号。4.2 用Quartus的FIFO IP核连接SD卡读模块和VGA模块SD卡读出的数据是突发式的一次读512字节停顿一段时间而VGA每个像素时钟都要消费一个像素中间需要缓冲。用Quartus的FIFO IP核解决跨时钟域和速率匹配写时钟接SD卡模块的SPI得到的数据有效信号读时钟接VGA的像素时钟。创建FIFO IP时选择“FIFO Intel FPGA IP”参数配置注意三点数据宽度16bit、深度1024、读写使用独立时钟域。为什么深度选1024而不用2048因为320×240的图片每行320像素VGA水平消隐期964816160个时钟内SD卡可以把FIFO填满深度1024足够覆盖两行缓冲。参数项设置值说明Data Width16RGB565每像素2字节FIFO Depth10241024×16bit缓冲Read Clock Domain独立读时钟VGA像素时钟25MHzWrite Clock Domain独立写时钟SD卡SPI时钟25MHzOverflow Flag启用用于调试丢数据FIFO的读使能由VGA有效区信号触发每个像素时钟在active为高且FIFO非空时读一次读出数据直接送到RGB输出。FIFO为空时输出黑色这样画面不会出现错行代价是空拍位置显示黑点。FIFO在SD卡数据源源不断注入时能自然填满到某一水位但VGA的消耗速率稳定SD卡突发读完后FIFO会被逐渐抽空。如果FIFO空了行的后面部分显示黑边视觉上是整条竖带缺失。避免这个问题的常用做法是在有效区开始前预填充FIFO等到FIFO半满才开始显示或者直接增大FIFO深度至2048。提示FIFO IP的读写计数器信号wrusedw和rdusedw可以接到SignalTap里观察调试时能看到缓冲水位变化曲线比凭感觉猜快得多。4.3 图片居中显示的逻辑处理320×240的图放到640×480的屏幕上像素坐标要加偏移量水平方向(640-320)/2 160垂直方向(480-240)/2 120。实现时在VGA时序模块里把x和y分别减去偏移值只有坐标落在源图范围内的像素才读FIFO其余输出黑边。wire in_src (x 160) (x 480) (y 120) (y 360); wire rd_en active in_src !fifo_empty;坐标判断放在读使能端不产生额外的存储成本。FIFO读出的数据直接输出所有边界都用比较器完成在一拍内就能决定当前像素是原图还是黑边不会破坏VGA时序。这一步也是很多工程里“图片位置整体偏移”的根源一旦x的计算基准错了画面会整体往右或往下平移一段距离检查坐标偏移量是定位此类问题的最快路径。5. 排错三板斧先证明VGA通道再验证SD卡数据最后查FAT解析5.1 第一板斧用纯色和棋盘格验证VGA通道把SD卡相关模块全部注释掉VGA驱动模块的输入数据改为固定常量分别输出红色0xF800、绿色0x07E0、蓝色0x001F确认屏幕能正确显示纯色。再输出棋盘格图案坐标(x/40 y/40) % 2决定当前格是白色还是黑色验证行场时序计数没有问题。棋盘格能看到整齐的方格说明同步信号和消隐区间都正确这时才能进入下一步。这一步也顺手解决了一个常见问题Quartus里Modelsim仿真VGA模块没有显示效果只能看波形无法验证颜色和位置是否匹配而实板测试可以直观看到画面。如果纯色画面有斜条纹大概率是RGB管脚顺序接反或者电阻网络匹配不对如果棋盘格左右不对称检查行前沿和行后沿是否按参数表实现。5.2 第二板斧用板上LED验证SD卡读回的字节SD卡读模块单独验证的时候一次CMD17读取后把数据的低8位接到LED上。卡上放一个已知内容的文件比如首字节是0xAA的文件读回来如果LED显示0xAA说明SPI通信链路是通的。再换几个不同的扇区地址确认寻址方式正确。具体做法是写一个简单的testbench用modelsim仿真时把SD卡SPI模型换成hex文件数组但要真正验证物理链路必须上板。把读到的512字节中第一、二个字节分别接到两组LED上对比SD卡里文件的实际内容能快速判断是位颠倒、字节序颠倒还是CS时序错误。位颠倒常见于SPI的CPOL/CPHA配置和SD卡规定不一致字节序颠倒则几乎总是命令参数高低位拼接错误。5.3 第三板斧SignalTap抓FAT解析的中间变量FAT解析最容易出错的地方是簇号拼接和扇区地址计算。在SignalTap里挂4个信号current_cluster、fat_entry、sector_addr、rd_data。先跳过FAT表直接读已知扇区再用SignalTap观察目录扫描时读到的目录项字节和PC上用WinHex打开SD卡镜像文件看到的字节逐位对比。如果目录项读出来都是0xFF或全0检查SPI命令发出的地址是否为扇区地址而非字节地址——SDHC卡上把扇区号当字节地址发导致的典型现象就是响应正常但数据全是0。如果名字匹配了但起始簇号明显偏大或偏小重点检查高16位和低16位拼接时是否有位宽截断。5.4 一个省事的调试技巧把BMP头直接写进FIFO初始化文件调试阶段可以绕过FAT32用Quartus的$readmemh把一幅小尺寸图片的裸像素数据初始化到一块片上ROM里VGA直接读这块ROM先证明“内存→VGA”这条路是通的。跑通之后再把数据源换成SD卡模块你会发现定位问题容易得多——画面花是图片解析问题画面黑是SD卡链路问题画面错位是FIFO时序问题。这样分模块调试的另一个好处是你手里始终有一个能出画面的工程备份改坏了随时回退不会陷入“满屏雪花不知道从哪查起”的困境。最后把ROM替换回SD卡FAT模块整个工程就完整了。如果出现画面只有上半部分优先查BMP的存储方向——BMP是自底向上的倒序读读地址即可修正。本文还有配套的精品资源点击获取
返回列表