Micro Code

Michael's blog about teaching, hardware, software and the things inbetween

More on Oberon

July 08, 2018 — Michael Engel

Reading the Oberon source code, I got the impression that in a large number of places "magic numbers" are used without much explanation (neither in the code nor in the accompanying book).

One thing that had me wondering was the bitmap graphics generation. The Verilog source code (in VID.v) is quite simple. It uses a combination of simple counters to generate a 1024x768 pixel monochrome display. Based on a pixel clock counter, separate counters for the current horizontal (hcnt) and vertical (vcnt) coordinate are derived.

The video memory is addressed in 32-bit chunks, so each video line uses 1024/32 = 32 32-bit words (=128 bytes). The horizonal pixel counter is not 10 bits wide (counting 0..1023), but 11 bits, since it also counts the horizontal traceback time.

From the (10 bit) pixel position in hcnt, a 5 bit word offset in the current line is derived by cutting away the 5 least significant bits:

reg [10:0] hcnt;
reg [4:0] hword;  // from hcnt, but latched in the clk domain

always @(posedge clk) begin  // CPU (SRAM) clock domain
  hword <= hcnt[9:5];

If one wants to figure out which set of pixels is at what absolute memory address in SRAM, one stumbles across some strange things in the code:

localparam Org = 18'b1101_1111_1111_0000_00;  // DFF00: adr of vcnt=1023
assign vidadr = Org + {3'b0, ~vcnt, hword};

The binary constant in the Verilog code omits the two least significant bits of the address, since the 1 MB memory of the Oberon FPGA system is 32-bit word (4 byte) addressed; the address constant 0xDFF00 in hex gives the byte address of the frame buffer.

The display driver code in Display.Mod, however, gives a different frame buffer start address:

base = 0E7F00H;  (*adr of 1024 x 768 pixel, monocolor display frame*)

There's something fishy... the base address in the Oberon driver and the one in the Verilog source code differ by 0xE7F00-0xDFF00 = 0x8000 = 32768 bytes. So, what is going on here?

Pixels are set using the following function:

PROCEDURE Dot*(col, x, y, mode: INTEGER);
  VAR a: INTEGER; u, s: SET;
BEGIN a := base + (x DIV 32)*4 + y*128;
  s := {x MOD 32}; SYSTEM.GET(a, u);
  IF mode = paint THEN SYSTEM.PUT(a, u + s)
  ELSIF mode = invert THEN SYSTEM.PUT(a, u / s)
  ELSE (*mode = replace*)
    IF col # black THEN SYSTEM.PUT(a, u + s) ELSE SYSTEM.PUT(a, u - s) END
END Dot;

The address for a pixel at position (x,y) is calculated as base + (x div 32) * 4 + y * 128. This sounds reasonable - each line is 128 bytes (128 * 8 = 1024 pixel) wide, x div 32 gives the word address offset in the current line, which is multiplied by 4 to give the word's byte address.

Accordingly, the pixel at address (0,0) is contained in the word at address 0xE7F00. But the Verilog source code starts reading the video buffer at address 0xDFF00... or does it? Let's take a closer look:

assign vidadr = Org + {3'b0, ~vcnt, hword};

When calculating the video memory address, the vcnt counter is inverted. Thus, the pixel at position (0,0) is at memory address (trailing "00" added to give the byte address):

0xDFF00 + { 000 1_1111_1111_1 000_00 00 } = 0xDFF00 + 0x1FF80 = 0xFFE80

Wait, that's still not right - what was the start address in the Oberon source again?

base = 0E7F00H;  (*adr of 1024 x 768 pixel, monocolor display frame*)

That's 0xFFE80-0xE7F00 = 0x17F80 = 98176 (dec.) bytes from the start!

Hmmm, wait... that's vaguely familiar. The whole monochrome 1024x768 bitmap screen uses

1024 / 8 * 768 = 98304 bytes

and 98304-98176 = 128, the memory used by one line of pixels. This works out since we were calculating the address of the leftmost pixel in the line.

Maybe you have already figured out what is going on here... Oberon's graphics coordinate system starts in the lower left corner of the screen, whereas the monitor's coordinate system starts at the upper left corner (see Chapter 4 in Wirth's Oberon book). Thus, the line at Oberon's coordinate y = 0 is the 767th line in monitor coordinates. Accordingly, inverting the y line coordinate in the Verilog address calculation

assign vidadr = Org + {3'b0, ~vcnt, hword};

causes the vertical line counter to count backwards. Note that the x coordinates of Oberon and the monitor count from left to right, so hword is not inverted.

The final remaining question is why the video base address is different. This is now also rather easy to see. vcnt is a 10-bit counter, so it counts from 0...1023. The Oberon y coordinates only are valid up to 767, so the 767th Oberon line = 0th monitor video line is at address

767 = 10_1111_1111 (binary), inverted = 01_0000_0000
0xDFF00 + { 000 0_1000_0000_0 000_00 00 } = 0xDFF00 + 0x08000 = 0xE7F00

Voila, there's the video memory start address that's defined in the Oberon driver source code.

If you take a closer look at the Draw function, you might now wonder what will happen if you pass a y coordinate larger than 767 to the Draw function. More on this in an upcoming blog post...

Tags: FPGA, Oberon, Xilinx, Wirth, graphics