Isle Text Mode

Published 12 Dec 2025 (DRAFT)

Isle text mode offers a simple and colourful way to display text. Text mode supports the full range of Unicode characters, but for this chapter, we use a small ROM with basic Latin and block characters. Text mode is fast; a single RISC-V store instruction can update a character and its colour.

If you're new to the project, read Isle FPGA Computer for an introduction.

Why?

We already have bitmap graphics hardware that can support arbitrary graphics, including text. Does text mode let us do anything new? It's text mode's simplicity and low memory use make it worthwhile. Writing text becomes almost trivial, even in assembly language. Creating an editor and debugger is feasible, and we're no longer dependent on UART for basic output. By layering text mode on top of bitmaps, we can have high-resolution text and colourful graphics without exceeding the capabilities of a small FPGA.

Isle simulation window showing text mode using internal system ROM. Text shows 'Isle.Computer' and basic Latin and block glyphs.
Text mode 84x24 (672x384) showing different characters and colours.

Unicode

Unicode is more complex than the 8-bit character sets typical on older computer systems, but fundamentally, that's because human languages and orthography are complex. Rendering accurate and attractive text across the range of languages is a hard problem.

In Unicode, each character is assigned a unique code point; for example, capital F is U+0046, the same number as in ASCII. Unicode code points may be up to 21 bits long. The Unicode encoding defines how this code point is represented, with UTF-8 being a popular and efficient encoding.

Isle text mode keeps things simple by assigning one bitmap glyph to each character, which works reasonably well for many, but not all scripts, as we'll discuss below.

Font

Isle uses GNU Unifont, a fixed-width bitmap font with excellent Unicode coverage and an open licence. Full-width glyphs, such as Japanese Katakana, are 16x16 pixels, while half-width glyphs, such as the Latin alphabet, are 8x16 pixels. That's enough pixels for some glyphs to look decent and most to be legible.

Text mode in this chapter only supports 128 glyphs in its internal ROM, covering two Unicode blocks: Basic Latin and Block Elements. Once we add more storage, we'll use the full Unifont that covers CJK, Cyrillic, Greek, Thai, Welsh, emoji, and many other languages. I'll also add support for mixing full- and half-width glyphs in a later update. Text mode supports 16 foreground and 16 background colours for each character, taken from a palette in the same way as for bitmap graphics.

Using GNU Unifont with our display modes, we get (for half-width glyphs):

system-font-rom.mem - showing the 128 glyphs in ROM ($readmemh format)

Compromise

I've tried to strike a good balance between wide language support, attractive text, and simplicity. A bitmap font with a single glyph per code point offers simplicity and wide language support, but at the cost of crude rendering, which can't handle Arabic and Indic scripts well. Once we introduce a CPU, you could implement software font rendering with bitmap graphics to produce more attractive results.

Text Mode Hardware

Text mode is built from four Verilog modules. The key modules are tram that holds the actual characters and colours, and textmode, which renders text mode for display. The font_glyph module handles loading lines of character pixels from the ROM handled by rom_sync. It's the font_glyph module that maps code points to memory locations.

Textmode

The textmode module iterates over the display, fetching the correct line of pixels for the current glyph from font_glyph. The module adjusts for the display window coordinates and scaling. You can get a sense of how the rendering works by looking at the state machine. The AWAIT state primes the pipeline, and then the DRAW state iterates over the pixels in the line before moving down one line in the glyph.

case (state)
    INIT: begin
        if (dy == win_start_y) state <= AWAIT;
        tram_addr <= scroll_offs;
        tram_addr_line <= scroll_offs;
        tx <= 0;
        ty <= 0;
        gx <= 0;
        gy <= 0;
        cnt_x <= 0;
        cnt_y <= 0;
    end
    AWAIT: begin
        if (dx == draw_start_x - 1) state <= DRAW;  // -1 for transition to DRAW
        colr_fg <= tram_data[WORD-CIDXW-1:WORD-2*CIDXW];
        colr_bg <= tram_data[WORD-1:WORD-CIDXW];
        pix_line_reg <= pix_line;
        ucp <= tram_data[UCPW-1:0];
    end
    DRAW: begin
        if (tx == text_hres || dx >= win_end_x-1) begin
            if (ty == text_vres || dy >= win_end_y-1) state <= IDLE;
            else if (glyph_y_end) state <= CHR_LINE;
            else state <= SCR_LINE;
        end

        // step through horizontal pixels
        if (cnt_x == scale_x-1) begin
            cnt_x <= 0;
            /* verilator lint_off WIDTHEXPAND */
            gx <= (gx == GLYPH_WIDTH-1) ? 0 : gx + 1;
            /* verilator lint_on WIDTHEXPAND */
        end else cnt_x <= cnt_x + 1;

        if (gx == 0 && cnt_x == 0)
            tram_addr <= (tram_addr == TRAM_DEPTH-1) ? scroll_offs : tram_addr + 1;

        // register Unicode code point; TRAM_LAT+1 to reg tram_addr
        if (gx == TRAM_LAT+1 && cnt_x == 0) ucp <= tram_data[UCPW-1:0];

        // register glyph pixels and colours at end of current glyph
        if (glyph_x_end) begin
            colr_fg <= tram_data[WORD-CIDXW-1:WORD-2*CIDXW];
            colr_bg <= tram_data[WORD-1:WORD-CIDXW];
            pix_line_reg <= pix_line;
            tx <= tx + 1;
        end
    end
    CHR_LINE: begin  // prepare for next line of chars
        state <= SCR_LINE;
        tram_addr_line <= tram_addr_line + text_hres;  // address for next line of chars
        ty <= ty + 1;  // move down to next line of chars
    end
    SCR_LINE: begin  // new line of pixels
        state <= AWAIT;

        // set tram address to start of line
        if (tram_addr_line > TRAM_DEPTH-1) begin // handle wrapping
            tram_addr <= tram_addr_line - TRAM_DEPTH;
            tram_addr_line <= tram_addr_line - TRAM_DEPTH;
        end else tram_addr <= tram_addr_line;

        // begin with first char on line; reset horizontal position
        tx <= 0;
        gx <= 0;
        cnt_x <= 0;

        // step through lines (vertical pixels)
        if (cnt_y == scale_y-1) begin
            cnt_y <= 0;
            /* verilator lint_off WIDTHEXPAND */
            gy <= (gy == GLYPH_HEIGHT-1) ? 0 : gy + 1;
            /* verilator lint_on WIDTHEXPAND */
        end else cnt_y <= cnt_y + 1;
    end
    default: begin  // IDLE
        if (frame_start) state <= INIT;
    end
endcase

TRAM

Text mode ram (tram) holds the characters and their colours; it's how we interface with text mode. The tram is memory mapped, so the CPU (covered in the next chapter) reads and writes characters with load and store instructions. Each 32-bit location holds the Unicode code point together with 4-bit foreground and background colours. The colours are indexes into the colour palette, as with bitmap graphics.

Isle tram is dual-ported, with one port used by the CPU and the other by the display, so there is no contention, and we can handle separate system and display (pixel) clocks; vram works the same way.

The lower 21 bits hold the Unicode code point (covers all possible Unicode characters). The upper byte holds the foreground and background colours, bits 28-31 the background, and bits 24-27 the foreground. By writing a byte to the upper 8 bits, the CPU can change the colours of an existing character.

Some tram data examples:

Isle has a 2048 x 32-bit tram configuration that supports up to 84x24 or 80x25 characters. This tram configuration requires 8 KiB of block ram.

Textmaps

Normally, you'd write characters to tram with the CPU. We don't have a CPU yet, so we create textmaps, in the same way we created bitmap graphics in chapter 2. Isle includes several textmaps to test functionally and get you started, see isle/res/textmaps.

Text Mode Demo

This demo runs without a CPU, relying on pre-written textmaps loaded into its tram with $readmemh.

Chapter 4 design: hardware/book/ch04/ch04.v

Build the included design. Each board has its own top module (and build instructions):

To change the text displayed, create or edit a textmap in isle/res/textmaps and update the FILE_TXT parameter in top_ch04.v for your board. Don't forget you can use the 32 block characters to create simple graphics; refer to system-font-rom.mem for supported code points. If you use a character that's not in the ROM you'll see a shaded rectangle instead.

ULX3S FPGA dev board connected to widescreen monitor over HDMI cable showing text mode using internal system ROM. Text shows 'Isle.Computer' and basic Latin and block glyphs.
Text mode on ULX3S dev board. I've used block characters to create a border.

Thinking Machine

Text mode completes are our core graphics components. Next time we'll introduce a 32-bit RISC-V CPU, then use it to build the first iteration of the Isle computer.

Next step: RISC-V CPU (under development) or Isle Index

You can sponsor me to support Isle development and get early access to new chapters and designs.

Further Reading