|
3 weeks ago | |
---|---|---|
src | 3 weeks ago | |
LICENSE | 3 weeks ago | |
README.md | 3 weeks ago | |
ROMS.txt | 3 weeks ago | |
asm.sh | 3 weeks ago | |
disasm.sh | 3 weeks ago | |
mkflop.sh | 3 weeks ago | |
runflop.sh | 3 weeks ago |
README.md
cga160x100-hello-world
This is a minimal program to demonstrate how to use the 160x100 pixel 16 color mode of the IBM(TM) Color Graphics Adapter aka the CGA card from the 1980s, in Netwide Assembler. It is designed to run on 8088 or higher Intel(TM) processors with DOS 2 or higher. This code shows a bare minimum to switch to this mode and fill the screen with green+blue pixels.
Building this code
Using NASM (Netwide Assembler) on the Linux(TM) operating system, the following command will "cross assemble" the file to 16-bit 8086 PC DOS, creating a .COM file which can be run directly in DOS.
$ nasm -f bin -o ./bin/160test.com ./src/160test.asm
Alternatively there is a shortcut script included that does the same:
$ ./asm.sh
Running in Emulators
To run under DosBOX on Linux, you can use the folowing:
# Install mtools, install dosbox, then run
./mkflop.sh dosbox
./runflop.sh dosbox
You can replace 'dosbox' above with any of the following:
dosbox, pcem, 86box, bochs, pce, qemu*
Please try to get your emulator basically running/booting DOS before attempting these scripts. Some of them can be a bit wonky with dependencies and paths so make sure that it is working before attempting mkflop.sh
mkflop.sh
creates ./emu/dosbox/diska.img, a virtual 360k floppy disk
by downloading a SvarDOS boot disk .img file, and
modifying autoexec.bat. mkflop also rebuilds the COM file from asm source,
and then creates ./emu/dosbox/diskb.img and copies the COM file to it.
diskb is also a virtual 360k floppy disk.
runflop.sh
will start up the emulator, boot from diska.img, read
autoexec.bat then switch to floppy b:, and run 160test.com
The custom autoexec.bat which switches to b: and runs 160test is in
./src/emu/autoexec.bat
and can be modified, then re-run mkflop and
runflop to retry with modifications.
Emulators with config files have been setup as 8088 / IBM PC/XT with CGA, those config files can be found under ./src/emu and modified as needed.
Emulators that don't work:
- 8086Tiny: missing bios functions
- dustbox: missing bios functions
- Virtualbox: gave up, too hard to use floppy only boot
- UniPCemu: not on linux
- Qemu: only emulates VGA, not CGA, so the screen will be half-height
How this works
80x25 text mode with 16 colors
Imagine normal text mode, which is normally 80 characters by 25 characters. Note this is 640x200 "screen pixels" on the screen even though you only control only 80x25 addressable cells. each cell is 4x8 "screen pixels"
Recall that each character has an "attribute" byte attached to it, and so the foreground color and background color of each character cell can be set independently. If we wanted to, we could fill the whole screen with Spaces, then set the background attribute byte of each cell to any of 16 colors. So we already have an 80x25 16 color mode! We just need a few more tricks to get to 160x100
80x100 text mode with 16 colors
Now, we play around with the Motorola 6845 CRT controller chip on the CGA card so it will only draw the top two pixel rows ("scan lines") of each character instead of all 8. If you were wondering what BIOS mode this is well, it's not BIOS mode. It's directly programming the 6845 chip by writing to I/O ports, using IN and OUT instructions of the 8088 chip, rather than writing to memory addresses in RAM or calling BIOS firmware functions. The 6845 controls certain aspects of driving the Cathode Ray Gun of the original CGA monitors, so by reprogramming it, we can get special effects.
The Motorola 8645 Registers
The reprogramming is done by accessing the registers of the 6845 chip. This chip has 19 registers, an Address Register AR then "normal" registers R0 through R18, each holds 8 bits.
To illustrate the registers, the below diagram combines the visual 2d appearance of the CGA screen with the concept of time. Let's imagine the Cathode Ray Gun and how it's beam travels over the glass on the old CGA monitors. Say it starts in the 'overscan' region, the top left of the screen, in the Border that exists between the edge of the glass and the area of the glass where the actual video RAM contents are displayed. On the CGA monitors this border region was quite noticeably large. Unlike modern screens where the entire LCD panel has addressable pixels right up to the bezel. In CGA there was a 'dead' space, the Border, which could take on the 'background' color, but could not have any pixel data drawn there. Let's imagine the top left of the glass, which is in this Border, is our starting point for our Raygun.
(Note that in the real 6845 chip, the CRTC "sees" the cycle beginning at the top left pixel of the visible area, not the top left area of the glass. It actually counts the top left of the glass as part of the previous frame. This is per VileR's post at int10h.org link below. However in this diagram, for sake of us noobs, we begin at the very upper left of the glass)
The ray gun then gets to the right hand of the screen - for the first several rows it is still in the Overscan (border) area. It is technically drawing the border area as the 'background color', usually black but possibly not. But once it reaches the right hand of the overscan, it gets a horizontal sync signal. The gun turns off for a split second as the Cathode Ray Tube electronics move it back leftwards towards the left side of the screen again, and position it one row down from the last row so it can draw the next row. This blanking is also called the Retrace.
Eventually the ray gun gets down into the part of the screen where active video is, where it starts drawing the contents of the video RAM, based on the patterns of the characters in the character BIOS. After it makes its way through all that, it gets to the bottom border, then keeps going until it gets the Vertical Retrace sync signal. It then moves the gun back to the top of the screen to start all over. This is called the vertical retrace.
In this chart the R# represent registers of the 6845 chip - R0, R1, and so on. Their meaning is listed below, based on int10h.org links below and the 6845 datasheet linked below.
O------------------------------------------------O Horizontal+
| Overscan (border area) /|\ | Blanking |
|<-----------------R0-------------------------|--|----------->|
| A------------------------------------A | | |
| | Active Video area /|\ /|\ | | | |
| | (pixels drawn) | | | | | |
| |<-----------R1-----------|-----|--->| | | |
| | | | | | | |
| |<-----------R2-----------|-----|----|---|->|<---R3----->|
| | | | | R4+ | |
| | R6 R7 | R5 | |
| | | | | | | |
| | \|/ | | | | |
| A-------------------------------|----A | | |
| | | | |
| \|/ | | |
O---------------------------------------------|--O------------+
| | | |
| Vertical Blanking \|/ | Dragons |
+------------------------------------------------+------------+
R0 Horizontal Total, # of character clock units
R1 Horizontal Displayed, # of character clock units
R2 Horizontal Sync Position, # of character clock units
R3 Horizontal Sync Width, aka CGA Blanking Width, in Char Clock Units
R4 Vertical Total, # of Character Rows Total
R5 Vertical Adjust, # of fractional Rows to get 50 or 60 hz refresh rate
R6 Vertical Displayed, # of Character Rows on screen
R7 Vertical Sync Position, in # of Character Rows
R9 Max Scanline Address - # of Scan Lines per Character Row (minus 1)
What are the units here? Character clock units? Well, this is because the diagram is a mixture of space and time. The time it takes to draw one character in the active video area is considered as one character clock unit. Now after overscan, during blanking, obviously nothing is being drawn because the gun is turned off and its traversing its way back from right to left, or bottom to top. But the timing still is expressed in units of how-long-it-would-take-to-draw-a-character. And you will notice in some of the registers, there is even a number representing a fraction of a character so that the refresh rate matches 50 or 60 Hertz.
What numbers to use in the registers? The 6845 data sheets actually provide formulas. But for our case of 160x100 lets look at what is going on.
Registers get set when we call BIOS 10h for 80x25 textmode. We know the values because IBM published the BIOS code and it's online.
Reg Descrip Hex Meaning
R00 Horiz Tot 0x71 113 characters across tot (overscan+retrace)
R01 Horiz Display 0x50 80 characters viewable
R02 Horiz Sync Pos 0x5A Overscan is 90 character clock units across
R03 Horiz Syn Width 0x0A Sync signal is '10' char clock units wide
R04 Vert Tot 0x1F 31 characters down tot (overscan+retrace)
R05 Vert # Scanlines 0x06 Each character is 8 pixels high (6+2)
R06 Vert Displayed 0x19 25 rows of characters
R07 Vert Sync Pos 0x1C Sync signal happens at row 28
R08 Interlace Mode 0x02 Set interlace mode as 2
R09 Max Scaline Adr 0x07 Draw 8 scanlines (pixel rows) of each char
R10 Cursor Start 0x06 Set cursor appearance
R11 Cursor End 0x07 Set cursor appearance
R12 Start Address pt1 0x00 R12+R13 form a 14 bit start address offset
R13 Start Address pt2 0x00 for where the video RAM will be read
R14 Cursor High 0x00 Help with cursor position
R15 Cursor Low 0x00 Help with cursor position
R16 Light Pen n/a R16 is a read-only regsiter
R17 Light Pen n/a R17 is a read-only register
So after the 6845 is setup with those values, we have the 80x25 16 color text mode display working.
To switch to 80x100 we need to modify four of these registers:
R4 is set to 127 - total vertical # chars (show + overscan + blanking)
R6 is set to 100 - vertical number of characters to show
R7 is set to 112 - sync position from top of vid area (show + 1/2 overscan)
R9 is set to 1 - draw only 2 scanlines (pixel rows) per character
Now we have 80 characters across by 100 characters down. 8 pixels across by 2 pixels.
160x100 text mode with 16 colors
Notice the IBM PC character set has a 'half block' character at 0xde position. Now the half-block is just 4 "screen pixels" of background color on the left, 4 of foreground on the right.
character 0xde normal character 0x41
the half block the letter A
__________ ________
|~~~~xxxx| row 0 |~~~~~~~~| x = foreground color
|~~~~xxxx| row 1 |~~~xx~~~| ~ = background color
|~~~~xxxx| row 2 |~~x~~x~~|
|~~~~xxxx| row 3 |~~x~~x~~|
|~~~~xxxx| row 4 |~x~~~~x~|
|~~~~xxxx| row 5 |~xxxxxx~|
|~~~~xxxx| row 6 |~x~~~~x~|
|~~~~xxxx| row 7 |~x~~~~x~|
|~~~~xxxx| row 8 |~~~~~~~~|
---------- ----------
However recall we set the 6845 to only show the first two scanlines of each character. So each text character on our 80x100 screen looks like this:
character 0xde normal character 0x41
the half block the letter A
__________ ________
|~~~~xxxx| row 0 |~~~~~~~~| x = foreground color
|~~~~xxxx| row 1 |~~~xx~~~| ~ = background color
---------- ----------
Imagine you can fill the screen with 80 characters wide, each character is the half-block. You choose one of 16 colors for each side of the half block. This gives you 160 columns. Each with it's own color. The below chart shows this
textcol / text row = shows which character cell in a 80x100 mode
fgc = fake graphics column, there are 160. Each is 4 "screen pixels" wide
for a width of 640 screen pixels, making 160 "fake" pixels out of
what is actually 640 screen pixels.
scrrow = screen row, there are 200 of them. (200 screen pixels)
but there are 100 "fake" pixels, each a separate text row
that has been setup to only draw the first two 'scan lines'
of each character.
~ = background color
x = foreground color
|textcol 0|textcol 1|textcol 2| ... |txtcol 79|
|fgc0|fgc1|fgc2|fgc3|fgc4|fgc5| ... |f158|f159|
A---------------------------------------------A
|~~~~|xxxx|~~~~|xxxx|~~~~|xxxx| ... |~~~~|xxxx| scrrow0 Text
|~~~~|xxxx|~~~~|xxxx|~~~~|xxxx| ... |~~~~|xxxx| scrrow1 Row 0
-----------------------------------------------
|~~~~|xxxx|~~~~|xxxx|~~~~|xxxx| ... |~~~~|xxxx| scrrow2 Text
|~~~~|xxxx|~~~~|xxxx|~~~~|xxxx| ... |~~~~|xxxx| scrrow3 Row 1
-----------------------------------------------
|~~~~|xxxx|~~~~|xxxx|~~~~|xxxx| ... |~~~~|xxxx| scrrow4 Text
|~~~~|xxxx|~~~~|xxxx|~~~~|xxxx| ... |~~~~|xxxx| scrrow5 Row 2
-----------------------------------------------
... ...
-----------------------------------------------
|~~~~|xxxx|~~~~|xxxx|~~~~|xxxx| ... |~~~~|xxxx| scrrow198 Text
|~~~~|xxxx|~~~~|xxxx|~~~~|xxxx| ... |~~~~|xxxx| scrrow199 Row 99
A---------------------------------------------A
With 80 characters by 100 characters, that's 8000 characters on the screen. Each character is two bytes, the 0xde half-block and the attributes for the foreground and background of the block. Each attribute is 4 bits. So each row is 80 * 2 or 160 bytes, and 100 rows, makes 16,000 bytes. This is coincidentally almost the total amount of RAM the CGA card came with
What happens if you don't use 0xde for each character? You get "ANSI from hell" mode, which has been used by some demosceners and artists to create graphics with the illusion of much more than 160x100 by 16 on CGA, see for example the Area 5150 demo. But this is not a demo scene program. This is Hello World.
160x100 - how to assign a specific color to a given pixel
Lets fill the screen with green and blue pixels. How? Let's fill each 16-bit word in video RAM with hex 0x21de. How does this work?
0x21de
0x20 = background color of bright=0,red=0,green=1,blue=0
0x01 = foreground color of bright=0,red=0,green=0,blue=1
0xde = character code for half block character
Below is a chart showing the memory layout of the single text character. There are 2 bytes per character, one for the char code and one for the attributes. The attributes form the colors (and/or blinking).
The charcode is in the least significant byte, the attributes, 4 bits each, are in the most significant byte.
0x21de hex code
2 = left fakepixel, 1 = right fakepixel, de = char code
as green color as blue color
-------------------------------------------------
| 16 bit character in color textmode |
-------------------------------------------------
| Most Significant Byte | Least Significant Byte| little endian word
-------------------------------------------------
|08|09|10|11|12|13|14|15|00|01|02|03|04|05|06|07| bit positions
-------------------------------------------------
|background |foreground | character code (8bits)| data
|bh|br|bg|bb|fh|fr|fg|fb| (index to IBM PC BIOS)| descriptions
-------------------------------------------------
| 0| 0| 1| 0| 0| 0| 0| 1| 1| 1| 0| 1| 1| 1| 1| 0| bit values
-------------------------------------------------
| 2 | 1 | d | e | hex nibble values
-------------------------------------------------
attributes are as follows:
bh = background (left) highlight, aka "brightness", for background color
br = background (left) red, either 1 on, or 0 off.
bg = background (left) green, either 1 on, or 0 off.
bb = background (left) blue, either 1 on, or 0 off.
fh = foreground (right) highlight, aka "brightness", for foreground color
fr = foreground (right) red, either 1 on, or 0 off.
fg = foreground (right) green, either 1 on, or 0 off.
fb = foreground (right) blue, either 1 on, or 0 off.
This works like any other RGB color system based on light, it combines the colors together to create new colors. So, Red + Green = Yellow. Red + Blue = Magenta. Blue + Green = Cyan. etc etc. the 1 and 0 means that there is only one "level" of each color. So we have 8 basic colors to work with. Then we can add the concept of "Brightness" so we can get a new 'level' of each color, for a total of 16 different colors.
In the 0x21de example, we are just using pure green and pure blue.
See Also
Code
- https://deathshadow.com/pakuPaku
- https://github.com/dschmenk/LORES
- https://github.com/drwonky/cgax16demo
CGA info
- https://www.seasip.info/VintagePC/cga.html
- https://www.classicdosgames.com/tutorials/characterattributes.html
- https://int10h.org/blog/2015/04/cga-in-1024-colors-new-mode-illustrated/
- https://int10h.org/blog/2023/03/cga-6845-crtc-phantom-vsync-glitch/#fn:sierpinski
- https://minuszerodegrees.net/oa/OA%20-%20IBM%20Color%20Graphics%20Monitor%20Adapter%20(CGA).pdf
- https://fabiensanglard.net/another_world_polygons_PC_DOS/
- https://retrocomputing.stackexchange.com/questions/24909/how-did-the-ibm-5153-color-display-detect-and-modify-the-signal-to-make-low-inte
- https://www.youtube.com/watch?v=pwATQ-ptJ_s
Intel 8088
- https://wiki.osdev.org/CMOS
- https://wiki.osdev.org/NMI
- https://stackoverflow.com/questions/3215878/what-are-in-out-instructions-in-x86-used-for
Motorola 6845
- http://bitsavers.trailing-edge.com/components/motorola/_dataSheets/6845.pdf
- https://int10h.org/blog/2023/03/cga-6845-crtc-phantom-vsync-glitch/
- https://mrboot.de/mc6845.php
IBM PC BIOS
- http://www.delorie.com/djgpp/doc/rbinter/it/10/0.html
- https://int10h.org/oldschool-pc-fonts/fontlist/font?ibm_bios
- https://github.com/philspil66/IBM-PC-BIOS/blob/main/PCBIOSV3.ASM
SvarDOS
The End
Thanks for reading, if you follow these steps, and get CGA into 160x100x16 mode, congratulations! Have fun. If not, please consider filing an issue here with detailed description of your steps taken and any error messages you got. Thanks
Disclaimer
Use of trademarks does not imply affiliation with the trademark owner.