This is a brief, 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 processors with DOS 2 or higher.
You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
don bright a90ec8b4c3 clarify 3 weeks ago
src clarify bios interrupt 3 weeks ago
LICENSE cleanup README, pull in helper scripts from my other project 3 weeks ago
README.md clarify 3 weeks ago
ROMS.txt clarify 3 weeks ago
asm.sh add asm 3 weeks ago
disasm.sh add disasm 3 weeks ago
mkflop.sh fix bug rebuilding for pcjs 3 weeks ago
runflop.sh cleanup README, pull in helper scripts from my other project 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

CGA info

Intel 8088

Motorola 6845

IBM PC BIOS

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.