• Please review our updated Terms and Rules here

notes on low level VGA programming

bakemono

Experienced Member
Joined
Jan 19, 2020
Messages
303
Location
USA
Earlier in the year I went through disassembled VEGA VGA BIOS code with the aim of being able to initialize the card from non-x86 code (ie. my Z280 board). I also did an experiment in my 286 where I jumpered the card to use 0x2xx I/O range instead of the usual 0x3xx, disabled the ROM, and tried to initialize it after booting with a second card active. In other words, there were two VGA cards and two monitors connected. (The second card was WD90C00.) I succeeded in displaying mode 13h on one monitor while the other was in text mode. Unfortunately, any mode switch on the main card would try to re-enable its 0xA0000 window, producing an address conflict and crash :)

As for the Z280 board, I switched to using a Trident 8900D. I got text mode and mode 13h working a while ago and am now picking through the video BIOS for this card looking for SVGA modes. I don't have a datasheet for the chip, but I have one for the TKD8001 DAC, and user's manual with a mode list. (There is also a text file online somewhere with info about proprietary registers on Trident chips.) The user's manual says it can reach a pixel clock of 75MHz, which is good for 640x480 with 24-bit color.

General info on intializing a VGA/SVGA card:
some I/O ports can exist at either $3Bx or $3Dx. The normal range is $3Dx and this should be selected by setting bit 0 of the Misc Output register.
the Misc Output register is set by writing port $3C2.
Attribute Controller registers are set by writing the index to port $3C0 followed by data to port $3C0.
(how do you know if it is currently expecting an index or data? You can reset it to the 'index' state by reading from port $3DA)
(IMPORTANT: the attribute controller index needs to be set back to $20 for anything other than background color to display)
Sequencer registers are set by writing an index to port $3C4 followed by data to port $3C5.
Graphics Controller registers are set by writing an index to port $3CE followed by data to port $3CF.
CRT Controller registers are set by writing an index to port $3D4 followed by data to port $3D5.
CRTC register index $11 contains a 'protect' bit which should be cleared before attempting to write the other registers.
VGA DAC color palette registers can be set by writing an index to port $3C8 followed by three bytes of RGB data to port $3C9.
In nearly all cases, port $3C6 (palette data mask) should be written with $FF, and ports $3CA/$3CC (graphics controller position) should be written with 1 and 0, respectively.

On a given card, there may be extended registers (not part of the original IBM VGA) which also need to be set. Disassembling the VGA BIOS may be the only way to find out about these. (I can post more info about Trident or CL-GD5x0 extended registers later.)

There are some cases where the order matters, when setting registers. For instance, some registers should only be modified while the sequencer is in the 'reset' state.

Sequence used by the Trident BIOS to change screen modes:
Code:
; write 1 to 3C4/00 (reset the sequencer)
; write 4x bytes from table to 3C4/01 ~ 3C4/04
; write value to 3C2
; write 25x bytes from table to CRTC
; write 9x bytes from table to gfx controller
; write 3 to 3C4/00 (un-reset sequencer)
; optionally write 16x bytes from table to attribute controller (color table)
; write 5 more bytes from table to remaining attribute controller registers
; write 0 to 3CC
; write 1 to 3CA
These are the values used for mode 13h:
Code:
; sequencer regs (1~4):  01 0F 00 0E
; misc output reg (3C2):  63
; CRTC regs:  5F 4F 50 82  54 80 BF 1F  00 41 00 00  00 00 00 00  9C 8E 8F 28  40 96 B9 A3  FF
; attribute regs:  00 01 02 03  04 05 06 07  08 09 0A 0B  0C 0D 0E 0F  41 00 0F 00  00
; gfx controller regs  00 00 00 00  00 40 05 0F  FF
I've now managed to set 640x400x8bpp from the Z280 board as well. It uses a 50MHz pixel clock instead of 25MHz, which requires Trident extended reg 3C4/0D to be set to 1.
Code:
; CRTC regs:  C3 9F A1 84  A6 00 BF 1F  00 40 00 00  00 00 00 00  9C 8E 8F 50  40 96 B9 A3  FF
It's not necessary to use the 'mode X' planar layout to access beyond the first 64KB because the Trident card has several methods of bank switching. In this case I just write a bank number XOR 2 to 3C4/0E. On the Z280 I can load a 256KB image from disk with a CP/M program.

I think it would be useful to build a small database of register settings for different screenmodes, with resolution and pixel clock. (Until now I've only seen scattered pieces of info such as on OSDev Wiki or buried in some source code.) Standard VGA only includes 25MHz and 28MHz clocks, but most cards have additional options. The VEGA VGA card has 32.5MHz. The WD90C00 has 36 and 44.9MHz. The Trident 8900 card uses a clock generator rather than discrete oscillators. Higher than 28MHz means you can exceed 360x480x8bpp, without even getting into other potentially undocumented registers.
 
some I/O ports can exist at either $3Bx or $3Dx. The normal range is $3Dx and this should be selected by setting bit 0 of the Misc Output register.

Useless bit of trivia: this is a leftover from EGA; IBM built it so it could coexist with either MDA *or* CGA cards, and these two ranges correspond with the I/O port ranges for those cards respectively. Flipping the location allows the EGA/VGA to (partially) emulate either kind of legacy card for programs that directly tickle the registers.

Almost nobody ever used mono EGA, but the capacity to ape an MDA card was retained in VGA’s hardware, so, there you go.
 
(IMPORTANT: the attribute controller index needs to be set back to $20 for anything other than background color to display)
The attribute controller value is a packed bitfield, with five lowest bits (bits 0-4) representing the index, and bit 5 denoting whether display memory scan is on or off.
So if you want to keep the display memory scan on while updating the index, you can set it by writing

outp(0x3C0, 0x20 | index);
outp(0x3C0, data);

This is more preferable than writing a sequence of

outp(0x3C0, index); // sets given index and disables display memory scan
outp(0x3C0, data);
outp(0x3C0, 0x20); // sets back to index 0 and enables display memory scan

because the latter temporarily turns off the display memory scan, which could theoretically cause a single pixel glitch if done during visible display area. Both forms of code (but especially the second form, to avoid extending the glitch) are best to gated inside interrupts disabled, if there is a possibility of interrupts affecting the index.
 
Disassembling the VGA BIOS may be the only way to find out about these.
Alternatively, you run the VGA BIOS through an emulator and intercept I/O port reads/writes to generate the table.
As far as I know, that's how most projects who use ISA VGA cards on other systems (such as AVR controllers) did it.

For maximum compatibility, you could also just run the VGA BIOS on the card through a CPU emulator. After all, you only need to run the initialization code (and the int10h mode-set) - after that, you can throw away the emulator and continue through (documented) registers.

Makes me wonder how many plain ISA cards provide a linear framebuffer; most VLB/PCI cards do.
 
You may find some useful information on some of the SVGA chipsets in the book " Programmers Guide to the EGA, VGA, and SVGA Cards". You can find the digital version on archive.org.
 
I was sloppy when I referred to 'pixel clocks' in my first post. Obviously 640x480 has a lot more pixels than 320x200, but they both use 25MHz to run things. Sometimes it takes multiple clock cycles to generate one pixel, especially with 16/24-bit color depths produced by combining multiple 8-bit pixels together at the RAMDAC. Related to this, the Trident 8900D has what I'd call a 'high bandwidth' mode which magically doubles the horizontal resolution without needing to select a higher clock or reprogram the horizontal registers in the CRTC. This is needed for the biggest screenmodes, as I've found 57MHz to be the highest usable clock in normal mode. These are settings for some additional Trident modes:
Code:
320x200 high-color (50MHz, normal mode)
; misc output reg (3C2):  6B
; CRTC regs:  C3 9F A1 84  A6 00 BF 1F  00 41 00 00  00 00 00 00  9C 8E 8F 50  40 96 B9 A3  FF
; Trident extended regs (NEW): 3C4/0E = C2, 3C4/0D = 01, 3C4/0C = 4E
; Trident extended regs (OLD): 3C4/0E = A0, 3C4/0D = 00
Code:
640x400 high-color (50MHz, high bandwidth mode)
; misc output reg (3C2):  6B
; CRTC regs:  C3 9F A1 85  A7 1F BF 1F  00 40 00 00  00 00 00 00  9C 8E 8F 50  40 96 B9 A3  FF
; Trident extended regs (NEW): 3C4/0E = 82, 3C4/0D = 01, 3C4/0C = 6E
; Trident extended regs (OLD): 3C4/0E = A0, 3C4/0D = 10
Code:
800x600 256-color (72Hz) (57MHz, high bandwidth mode)
; misc output reg (3C2):  63
; CRTC regs:  7E 63 64 81  6B 18 9A F0  00 60 00 00  00 00 00 00  6E 84 57 32  40 5E 93 A3  FF
; Trident extended regs (NEW): 3C4/0E = C2, 3C4/0D = 01, 3C4/0C = 6E
; Trident extended regs (OLD): 3C4/0E = A0, 3C4/0D = 10
There are separate so-called 'new' and 'old' registers at 3C4/0E and 3C4/0D. New ones are activated by reading 3C4/0B (which is also a chip ID). Old ones are activated by writing 3C4/0B.

High-color modes require changing the RAMDAC setting. There are supposed to be two ways to access the TKD8001 command register. The one I got to work was reading port $3C6 four times, then writing the command byte to port $3C6. Writing 0 selects normal 256 color mode, while writing $A0 gives me 15-bit RGB.
 
Back
Top