bmp_read 是整条链路里离数据源最近的一个模块,它头顶两个任务:
scan_start_sector 开始,一个扇区一个扇区地读 SD 卡,把符合要求的前几张 BMP 的起始扇区记下来;它的输出只有两个东西——像素(bmp_data + bmp_data_wr_en)和一个「我干完了」的信号(ready)。而 ready 正是上一章 sd_card_bmp 里用来置 source_done_seen 的那个 bmp_ready。所以这一章,本质是在回答上一章埋下的一个问题:bmp_ready 到底什么时候拉高,数据又是在哪一刻、以什么形式流进 FIFO 的。
要读 BMP,先得懂它的格式。标准 BMP 由「文件头 + 像素数据」组成,代码里只解析文件头里的几个关键字段:
| 字节偏移 | 字段 | 位宽 | 代码里的名字 | 含义 |
|---|---|---|---|---|
| 0–1 | 魔数 | 2 字节 | header_0 / header_1 | 固定为 "BM"(0x42 0x4D) |
| 2–5 | 文件大小 | 4 字节 | file_len | 整个文件的字节数(小端) |
| 10–13 | 像素偏移 | 4 字节 | pixel_offset | 像素数据从第几字节开始,标准为 54 |
| 18–21 | 宽度 | 4 字节 | width | 图像宽(本项目要求 640) |
| 22–25 | 高度 | 4 字节 | height | 图像高(本项目要求 480) |
| 28–29 | 位深 | 2 字节 | bit_count | 每像素位数(要求 24) |
| 30–33 | 压缩 | 4 字节 | compression | 0=无压缩,其余都不支持 |
解析逻辑用了一个「字节计数器 + case」的经典写法。rd_cnt 在数据有效时自增,rd_cnt == N 正好对应文件第 N 个字节,于是按偏移把字段填进对应寄存器:
10'd0 : header_0 <= sd_sec_read_data; // 'B'
10'd1 : header_1 <= sd_sec_read_data; // 'M'
10'd2 : file_len[7:0] <= sd_sec_read_data;
10'd3 : file_len[15:8] <= sd_sec_read_data;
...
10'd18: width[7:0] <= sd_sec_read_data; // 宽度低字节在前(小端)
width[7:0] 接第一个字节、width[15:8] 接第二个字节,恰好把小端序还原成正确的 16 位数。读到头之后,怎么判断这一张是不是我们要的 BMP?靠一个组合逻辑:
assign header_match = (header_0 == "B") &&
(header_1 == "M") &&
(width[15:0] == bmp_width) && // 要求 640
(height[15:0] == bmp_height) && // 要求 480
(bit_count == 16'd24) &&
(compression == 32'd0);
五个条件同时成立才算「合格」:
"BM"——不是随便一个文件都能糊弄过去;bmp_width / bmp_height——不匹配的图直接跳过;这套校验在扫描和加载两个阶段都用到了,是「过滤垃圾文件」的第一道关卡。
扫描模式的精妙之处在于「用文件大小跳扇区」。SD 卡一个扇区 512 字节,而一张 640×480×24bit 的 BMP 大约 90 万字节、占 1800 多个扇区。如果挨个扇区盲扫,得读几十万次;但读到头之后 file_len 已知,就能算出这张图占多少扇区、直接跳过去:
assign file_sector_count = (file_len == 32'd0) ? 32'd1 : ((file_len + 32'd511) >> 9);
assign next_scan_sector_if_match = scan_sector + file_sector_count; // 命中:跳过整张图
assign next_scan_sector_if_miss = scan_sector + 32'd1; // 未命中:只进一个扇区
(file_len + 511) >> 9 就是「向上取整的 file_len / 512」,即文件占的扇区数。扫描状态机的流程:
ST_SCAN: begin
sd_sec_read <= 1'b1; // 持续读当前扇区
if (sd_sec_read_end) begin
if (header_match) begin
scan_found_valid <= 1'b1;
scan_found_sector <= scan_sector; // 记下这张图的起始扇区
scan_found_total <= scan_found_total + 3'd1;
sd_sec_read_addr <= next_scan_sector_if_match; // 跳过整张图
scan_sector <= next_scan_sector_if_match;
...
end else begin
sd_sec_read_addr <= next_scan_sector_if_miss; // 只进一个扇区
scan_sector <= next_scan_sector_if_miss;
end
end
end
找到 scan_target_count 张、或者扫到 scan_max_sector 还没找够,就拉高 scan_done 收工。找到的每张图起始扇区,会通过 scan_found_sector 交给上一章的 sd_card_bmp 存进 img_sector0~3。
load_sector 指哪读哪。这就是「先索引、后随机访问」的思路,和文件系统的目录表一个道理。加载一张图时,状态机走 ST_LOAD_HDR → ST_LOAD_WAIT → ST_LOAD_DATA。关键在于进入数据阶段之前,要先和下游的写侧握一次手:
ST_LOAD_HDR: begin
sd_sec_read <= 1'b1;
if (sd_sec_read_end) begin
if (header_match) begin
write_req <= 1'b1; // 向 frame_read_write 申请写
sd_sec_read_addr <= load_sector_latched; // 回到文件头,准备重读像素
state <= ST_LOAD_WAIT;
end else
state <= ST_IDLE; // 头不对,放弃
end
end
ST_LOAD_WAIT: begin
if (write_req_ack) begin
write_req <= 1'b0;
state <= ST_LOAD_DATA; // 下游答应,开读
end
end
这里藏着两个细节:
frame_read_write 的写状态机)要准备好「基地址 + 长度」才能接数据。write_req 拉高、等 write_req_ack 回来,双方对齐后再开始灌像素,才不会丢开头的数据。ST_LOAD_HDR 已经读了一个扇区、解析了头,但像素数据要从 pixel_offset 开始。最简单粗暴的做法是:把地址设回 load_sector_latched,从头再读一遍,用 bmp_len_cnt 计数跳过前 54 字节头。多读一个扇区的代价,换来逻辑简单可靠。BMP 的像素是 BGR 顺序存储的——先蓝、再绿、再红,和常见的 RGB 正好反着。代码用一个 2bit 计数器 bmp_byte_idx 在 0/1/2 之间循环,凑满 3 字节才输出一个像素:
case (bmp_byte_idx)
2'd0: bmp_data[7:0] <= sd_sec_read_data; // 第 1 字节:蓝 B
2'd1: bmp_data[15:8] <= sd_sec_read_data; // 第 2 字节:绿 G
2'd2: begin
bmp_data_wr_en <= 1'b1; // 第 3 字节:红 R,凑满才写
bmp_data[23:16] <= sd_sec_read_data;
end
endcase
配套的还有一个「数据有效」判断,决定哪些字节算像素、哪些该跳过:
assign bmp_data_valid = (sd_sec_read_data_valid) &&
(bmp_len_cnt >= pixel_offset) && // 跳过 54 字节文件头
(bmp_len_cnt < file_len); // 不超出文件
bmp_len_cnt 从进入 ST_LOAD_DATA 开始每字节自增,用 pixel_offset(默认 54)把文件头挡在门外,只有真正的像素字节才拼进 bmp_data。
{R, G, B} 的高位是红、低位是蓝,正好把 BGR 顺序纠正成内部统一的 RGB。到这里,可以把四章的知识连成一条完整的线了。一个像素从 SD 卡到屏幕,要过这五道关卡:
| 环节 | 模块 | 干了什么 |
|---|---|---|
| 1 | bmp_read | 读 BMP 文件、校验头、拼出 24bit 像素 |
| 2 | sd_card_bmp | 决定写哪块缓冲、何时切换显示 |
| 3 | frame_read_write | 写 FIFO 跨域 → 写状态机突发写进 SDRAM |
| 4 | frame_read_write | 读状态机突发读出 → 读 FIFO 跨域 |
| 5 | video_timing_data | 按行/场时序把像素推给 HDMI |
而 bmp_read 输出的 24bit 像素,正是第三章里 sd_card_bmp 那行重打包的输入:
// sd_card_bmp.v 里:把 24bit RGB 扩成 32bit(低 8 位补零)
assign write_data = {bmp_data[23:16], bmp_data[15:8], bmp_data[7:0], 8'b0};
24bit 像素在这里被补成 32bit,对齐 SDRAM 的 32bit 数据总线,然后才进 frame_read_write 的写 FIFO。每一步都环环相扣——这就是官方例程把「读文件」和「存帧」拆成两个模块、用握手衔接的用意:各管一段,接口清晰,出了问题也知道该查谁。
header_match 里放宽格式判断、在数据阶段加解码逻辑。想支持任意分辨率?把 bmp_width/bmp_height 从固定参数改成从文件头现读。改动都集中在这一个模块,不影响下游。