Reading BMP Bitmap Images in VHDL Testbenches with TEXTIO
When working with VHDL testbenches, the simplest way to import an image is to use the BMP raster graphics format. BMP files are natively supported by Windows, and they expose raw pixel data without compression, making them ideal for simulation.
This article explains how to read a binary BMP file, store its pixel data in dynamic memory, feed it to a VHDL DUT, and write the processed image back to a new BMP file. The example DUT performs a grayscale conversion, but the technique applies to any pixel‑wise processing module.
Part of a TEXTIO series:
Why BMP is the Optimal Format for VHDL
JPEG and PNG are ubiquitous on the web, but both employ compression. JPEG is lossy, PNG is lossless; both add a layer of complexity when a testbench must access pixel data directly. BMP, on the other hand, stores image data as uncompressed raster graphics, so each pixel is represented by raw RGB bytes. This eliminates the need for decompression code in VHDL and guarantees deterministic simulation time.
Common image editors such as Photoshop or GIMP load images into memory as raster data internally. For VHDL, converting the source image to BMP manually (or via a script) is the most straightforward approach.
Preparing the BMP File
Use Windows Paint to ensure a consistent file layout:
- Open the image.
- Select File > Save As.
- Choose 24‑bit Bitmap (*.bmp; *.dib) and save.
With this method the file header will always be the 54‑byte BITMAPINFOHEADER variant with an RGB24 pixel format. The following table lists the header fields we read.
| Offset (Dec) | Size (B) | Expected (Hex) | Description |
|---|---|---|---|
| 0 | 2 | 42 4D | ID field |
| 10 | 4 | 54 (36 00 00 00) | Pixel array offset |
| 14 | 4 | 40 (28 00 00 00) | Header size |
| 18 | 4 | Read value | Image width in pixels |
| 22 | 4 | Read value | Image height in pixels |
| 26 | 1 | 01 | Number of color planes |
| 28 | 1 | 18 | Number of bits per pixel |
Only the green‑shaded fields are essential; the rest serve as sanity checks.
The Test Case
Below is a minimal grayscale DUT that accepts 24‑bit RGB input and outputs the corresponding luminance value encoded back into RGB. The module is purely combinational, so no clock is required.
entity grayscale is
port (
r_in : in std_logic_vector(7 downto 0);
g_in : in std_logic_vector(7 downto 0);
b_in : in std_logic_vector(7 downto 0);
r_out : out std_logic_vector(7 downto 0);
g_out : out std_logic_vector(7 downto 0);
b_out : out std_logic_vector(7 downto 0)
);
end grayscale;
For completeness, the full project source (including a 1000×1000 pixel Boeing 747 BMP) is available for download by providing an email address on the linked form.
Importing TEXTIO
At the top of your VHDL file add:
use std.textio.all; use std.env.finish;
These declarations require VHDL‑2008 or newer.
Custom Types for Image Storage
Define the following types in the declarative region of your testbench:
type header_type is array (0 to 53) of character; type pixel_type is record red : std_logic_vector(7 downto 0); green : std_logic_vector(7 downto 0); blue : std_logic_vector(7 downto 0); end record; type row_type is array (integer range <>) of pixel_type; type row_pointer is access row_type; type image_type is array (integer range <>) of row_pointer; type image_pointer is access image_type;
These dynamic data structures allow the testbench to accommodate images of any size without static array limits.
Instantiating the DUT
signal r_in, g_in, b_in : std_logic_vector(7 downto 0);
signal r_out, g_out, b_out : std_logic_vector(7 downto 0);
begin
DUT : entity work.grayscale
port map (
r_in => r_in,
g_in => g_in,
b_in => b_in,
r_out => r_out,
g_out => g_out,
b_out => b_out
);
Reading the BMP Header
The process starts by loading the 54‑byte header into a header variable and validating key fields with assert statements. This guard prevents accidental use of an unsupported file format.
for i in header_type'range loop read(bmp_file, header(i)); end loop; -- Basic sanity checks assert header(0) = 'B' and header(1) = 'M' report "First two bytes are not ""BM"". This is not a BMP file" severity failure; assert character'pos(header(10)) = 54 and character'pos(header(11)) = 0 and character'pos(header(12)) = 0 and character'pos(header(13)) = 0 report "Pixel array offset is not 54 bytes" severity failure; assert character'pos(header(14)) = 40 and character'pos(header(15)) = 0 and character'pos(header(16)) = 0 and character'pos(header(17)) = 0 report "DIB header size is not 40 bytes, is this a Windows BMP?" severity failure; assert character'pos(header(26)) = 1 and character'pos(header(27)) = 0 report "Color planes is not 1" severity failure; assert character'pos(header(28)) = 24 and character'pos(header(29)) = 0 report "Bits per pixel is not 24" severity failure;
After validation, the image width and height are extracted:
image_width := character'pos(header(18)) +
character'pos(header(19)) * 2**8 +
character'pos(header(20)) * 2**16 +
character'pos(header(21)) * 2**24;
image_height := character'pos(header(22)) +
character'pos(header(23)) * 2**8 +
character'pos(header(24)) * 2**16 +
character'pos(header(25)) * 2**24;
report "image_width: " & integer'image(image_width) &
", image_height: " & integer'image(image_height);
Reading Pixel Data
Each BMP row is padded to a multiple of four bytes. Compute the padding per row and allocate dynamic storage for the image.
padding := (4 - image_width*3 mod 4) mod 4;
image := new image_type(0 to image_height - 1);
for row_i in 0 to image_height - 1 loop
row := new row_type(0 to image_width - 1);
for col_i in 0 to image_width - 1 loop
read(bmp_file, char); row(col_i).blue := std_logic_vector(to_unsigned(character'pos(char), 8));
read(bmp_file, char); row(col_i).green := std_logic_vector(to_unsigned(character'pos(char), 8));
read(bmp_file, char); row(col_i).red := std_logic_vector(to_unsigned(character'pos(char), 8));
end loop;
for i in 1 to padding loop read(bmp_file, char); end loop; -- discard padding
image(row_i) := row;
end loop;
Testing the DUT
Feed each pixel into the DUT, wait a small delta cycle, and capture the grayscale output.
for row_i in 0 to image_height - 1 loop
row := image(row_i);
for col_i in 0 to image_width - 1 loop
r_in <= row(col_i).red;
g_in <= row(col_i).green;
b_in <= row(col_i).blue;
wait for 10 ns;
row(col_i).red := r_out;
row(col_i).green := g_out;
row(col_i).blue := b_out;
end loop;
end loop;
Writing the Output BMP
First, write the preserved header. Then serialize the pixel data row by row, adding padding where necessary. Finally, clean up dynamic memory and close the files.
for i in header_type'range loop
write(out_file, header(i));
end loop;
for row_i in 0 to image_height - 1 loop
row := image(row_i);
for col_i in 0 to image_width - 1 loop
write(out_file, character'val(to_integer(unsigned(row(col_i).blue))));
write(out_file, character'val(to_integer(unsigned(row(col_i).green))));
write(out_file, character'val(to_integer(unsigned(row(col_i).red))));
end loop;
deallocate(row);
for i in 1 to padding loop
write(out_file, character'val(0));
end loop;
end loop;
deallocate(image);
file_close(bmp_file);
file_close(out_file);
report "Simulation done. Check ""out.bmp"" image.";
finish;
end process;
Resulting Image
The generated out.bmp file shows the grayscale version of the original Boeing 747 image. A JPEG preview is displayed in the article; the full BMP file is available via the download form.
Further Considerations
- For high‑throughput image processing, consider YUV encoding where luminance (Y) is separated from chrominance (U/V). Converting between RGB and YUV is straightforward and often more hardware‑friendly.
- CMYK conversion is more complex because a one‑to‑one pixel mapping does not exist; it typically requires a lookup table or a more elaborate algorithm.
- When working with custom encodings, you can design a lightweight file format (e.g.,
.yuvor.cmyk) that omits the BMP header entirely, simplifying parsing. - Automating the BMP conversion step (e.g., via ImageMagick’s
convertcommand) ensures consistent test vectors across simulation runs.
VHDL
- Driving VHDL Testbenches from External Stimulus Files with TEXTIO
- Initializing FPGA Block RAM from Text Files Using VHDL’s TEXTIO Library
- C++ File Handling: Mastering Open, Read, Write, and Close Operations
- File Operations in C# – A Practical Guide
- Reading Files in Java with BufferedReader – A Practical Guide with Examples
- How to Rename Files and Directories in Python with os.rename() – Step-by-Step Guide
- How to Read and Write CSV Files in Python: A Comprehensive Guide
- Python JSON: Encoding, Decoding, and File Handling – A Practical Guide
- Seamless PLC‑to‑Cloud Integration: Harnessing IoT for Real‑Time Data Retrieval
- Robust UAV Detection & Tracking via AI-Driven Computer Vision