Let’s recreate a 90’s PC BIOS Boot Screen

Because why not, it looks simpler than it really is and we’ll get to fall down a rabbit hole trying to draw the Energy Star logo.

“Hello World” is one of the most basic programming tasks we do. It’s the equivalent of getting a new car and taking it for a short drive down the road to check the wheels don’t fall off and that the engine doesn’t explode. Entire websites have been devoted to it.

Once you’ve tested the compiler and figured out how to work the IDE, what next? It seems a bit of a jump going straght into a massive project.

I’m attempting to learn how to write DOS and x86 programs and need something a bit more complex than printing “Hello World”, but not so complex that I need to spend six months learning the inner bowels of a PC or its VGA card before starting.

Being unable to think of anything, I was sat staring at my emulated PC as it booted up and decided the BIOS boot screen would do. It’s mostly text, but there’s a curious image in the upper right of the screen. That big Energy Star logo. Except this is the BIOS, on a 90s PC. It’s in text mode. How’s that work?

Figuring that out took me down a deep rabbit hole into how the VGA on a PC works. There’s loads left to find out and explore but the basics are approachable while being sufficiently difficult to hold my interest. It was a nice introduction to some more complex concepts that I’d need to deal with later.

Common DOS programming tasks nobody does any more such as firing off interrupts, talking to the VGA card directly, and then obscure things like the difference between Real and Protected mode, and dealing with DJGPP being based on GCC for Linux.

The BIOS Screen

If you had a PC in the 90s, you spent far too long staring at this screen. All it does is waste a bit of time while your PC does a few basic checks, counts the RAM and waits for the HDD to spin up.

Other BIOS manufacturers exist, but Award was the one I saw the most.

The big logo in the corner though, that’s a bit special. Back in the mists of time computers used electricity and nobody cared how much of it they consumed. Then “the environment” became a hot topic of debate and we slowly woke up to the idea that human activity affected the whole planet.

PCs started to acquire basic power saving technologies, and that was a good marketing ploy. A motherboard was just a motherboard, nobody cared what motherboard they had, but if you were making a choice and one had energy saving abilities then it might be the one you chose.

The US Government created a department – the EPA – and from that created a way to brand consumer items with something that indicated it was using less energy. They called it Energy Star. It had a logo, everyone likes a good logo.

So the Award BIOS boot screen looks like this

Recreating the majority of this is easy, it’s just a load of printf() statements and some code to position the cursor properly. A small amount of effort is needed to simulate the RAM being counted, but it’s very straight forward.

I decided to overcomplicate this program to check things were functioning as I expect. Here’s the first bit of code to print some BIOS-like text on the screen. It handles VGA colour text to get the correct bold and regular style writing and later on the colours in the logo, and uses some screen positioning functions from DJGPP to put text accurately on the screen.

#include <stdio.h>
#include <pc.h>
#include <dpmi.h>
#include <dos.h>
#include <sys/types.h>
#include <sys/movedata.h>
#include <go32.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>

unsigned char *row1 = "   Award Modular BIOS v4.50PG, An Energy Star Ally";
unsigned char *row2 = "   Copyright (C) 1984-95, Award Software, Inc.";
unsigned char *row3 = "";
unsigned char *row4 = "ASUS P5a ACPI BIOS Revision 1011 Beta 005";
unsigned char *row5 = "";
unsigned char *row6 = "80486DX2 CPU at 66MHz";

void VGAPutChar(unsigned char *line, char c, char attrib, int index)
{
    line[index*2] = c;
    line[index*2+1] = attrib;
}

void PrintBIOSLine(unsigned char *line, int row)
{
    unsigned char screen_line[160];
    memset(screen_line, 0,160);
    
    for (int i = 0; i < strlen(line); i++) {
        unsigned char colour = 0x07;
        if ((row == 1 || row == 2) && (i < 3)) colour = 0x09;
        VGAPutChar(screen_line, line[i], colour, i);
    }
    
    ScreenUpdateLine(&screen_line, row);
}

void main()
{
    // Clear the screen
    __dpmi_regs r;
    memset(&r, 0, sizeof(r));
    r.h.ah = 0;
    r.h.al = 0x03;
    __dpmi_int(0x10,&r);

    PrintBIOSLine(row1, 1);
    PrintBIOSLine(row2, 2);
    PrintBIOSLine(row3, 3);
    PrintBIOSLine(row4, 4);
    PrintBIOSLine(row5, 5);
    PrintBIOSLine(row6, 6);

    for (int j = 0; j < 4; j++) {
        for (int i = 0; i <= 32768; i+=1024) {
            ScreenSetCursor(7,0);
            printf("Memory Test : %6dK OK", i);

            ScreenSetCursor(23,0);
            printf("Press DEL to enter SETUP, ESC to skip memory test\n");
            printf("02/21/97-VT496G-2A4L6F0IC-00");
            
            delay(10);
        }
    }
    delay(1000);
    ScreenSetCursor(23,0);
    printf("Press DEL to enter SETUP                                   \n");
    printf("02/21/97-VT496G-2A4L6F0IC-00");
    ScreenSetCursor(9,0);
    printf("Award Plug and Play BIOS Extension  v1.0A\n");
    printf("Copyright (C) 1995, Award Software, Inc.\n");
    char c = getchar();
}
Code language: C++ (cpp)

The logo though? Let’s go down that rabbit hole…

All the BIOS code and data lives in a ROM chip on the motherboard. As a kid in the 90s I understood this chip was vital to the functioning of my PC, and that if the BIOS is corrupt, my PC becomes an expensive brick.

Turns out that BIOS chip contains quite a bit of data, all mashed into a kind of compressed archive. It’s pretty clever really. If you’re curious people have documented it, here’s an example of what’s inside

D:\HD_LAM~1\HARDWA~2\Tweaking\BIOS_R~1>cbrom208 865IDC19.bin /D 
CBROM V2.08 (C)Award Software 2000 All Rights Reserved.
                ******** 865IDC19.bin BIOS component ********   
No. Item-Name         Original-Size   Compressed-Size  Original-File-Name ================================================================================
   0. System BIOS       20000h(128.00K) 13C31h(79.05K)   865IDC19.BIN
   1. XGROUP CODE       0D960h(54.34K)  09806h(38.01K)   awardext.rom
   2. CPU micro code    04000h(16.00K)  03FA2h(15.91K)   CPUCODE.BIN
   3. ACPI table        045C1h(17.44K)  01A7Dh(6.62K)    ACPITBL.BIN
   4. EPA LOGO          0168Ch(5.64K)   002AAh(0.67K)    AwardBmp.bmp
   5. YGROUP ROM        05D00h(23.25K)  03E56h(15.58K)   awardeyt.rom
   6. GROUP ROM[ 0]     05360h(20.84K)  024B5h(9.18K)    _EN_CODE.BIN
   7. VGA ROM[1]        0C000h(48.00K)  06B05h(26.75K)   SDG_2831.DAT
   8. GROUP ROM[ 5]     004F0h(1.23K)   002A4h(0.66K)    SDG_2831.VBT
   9. Flash ROM         0A00Ch(40.01K)  05777h(21.87K)   AWDFLASH.EXE
  10. PCI ROM[A]        0C000h(48.00K)  05DFCh(23.50K)   4212.BIN
    Total compress code space  = 4A000h(296.00K)
   Total compressed code size = 3B727h(237.79K)
   Remain compress code space = 0E8D9h(58.21K)
                            ** Micro Code Information ** 
Update ID  CPUID  |  Update ID  CPUID  |  Update ID  CPUID   |  Update ID  CPUID 
------------------+--------------------+---------------------+------------------- 
SLOT2  17   0F29  |  SLOT2  2C   0F12  |  SLOT2  01   0F21   |  SLOT2  08   0F23 
SLOT2  1E   0F24  |  SLOT2  05   0F13  |  SLOT2  15   0F25   |  SLOT2  37   0F27 
SLOT2  17   0F29  | 
D:\HD_LAM~1\HARDWA~2\Tweaking\BIOS_R~1>

There’s CPU microcode, tables of lookup data, boot code and more. It’s like a mini filesystem and not just a binary that the CPU dumbly executes at boot.

Amongst the highly important boot code and so on, you can see something called “EPA LOGO”. That’s what we want.

If we obtain a dump of an Award BIOS (exercise left to the reader) from that period and use a suitable tool, it’s possible to extract the BIOS logo file. It comes in two different formats, and the easiest to cope with is version 1. The tools seem to extract the logo as a V2 file, so it will need converting. One of the tools can do this.

Reading this into our little C program isn’t very hard.

The BIOS logo is probably copyrighted by someone, so I probably can’t give it away. I found it inside a BIOS dump that was inside a PC Emulator.

Reading data into C

The great thing about C is that it’s portable. When writing a game for my Agon Light in C I had to read in some binary data. The code I wrote can be used here too.

I just need a struct to store the data, some basic file reading code and a few malloc() calls to grab some RAM.

This goes at the top of the file somewhere appropriate.

typedef struct EPAFile {
    unsigned char width, height;
    unsigned char *colour_map;
    unsigned char *bitmap;
    unsigned char *award_logo;
} EPAFile;

EPAFile epa_file;

// And then a bit later on...
void loadEPA(char *filename)
{
    FILE *epa = fopen(filename, "rb");
    fread(&epa_file, 1, 2, epa);
    //printf("EPA File is %d x %d in size\n", epa_file.width, epa_file.height);
    epa_file.colour_map = malloc(epa_file.width * epa_file.height);
    fread(epa_file.colour_map, 1, epa_file.width * epa_file.height,epa);
    epa_file.bitmap = malloc(epa_file.width * epa_file.height * 14);
    fread(epa_file.bitmap, 1, epa_file.width * epa_file.height * 14,epa);
    epa_file.award_logo = malloc(70);
    fread(epa_file.award_logo, 1, 70,epa);
}Code language: C++ (cpp)

Displaying the BIOS Logo

When the PC boots it is in 80×25 text mode, not a graphics mode. Drawing individual pixels on the screen isn’t possible, all we can do is print ASCII text. However, it is possible to redefine the character set to be our own character shapes. And if we’re clever with what shapes we use, a small two colour image can be drawn on the screen.

This is how the BIOS logo is done. I’m not sure which characters are really used, but with 8 bit ASCII the entire second page is unused by the BIOS – it only needs to print text and numbers, and when DOS appears it all gets reset anyway.

So a plan is to read in the BIOS logo as ASCII character glyphs and somehow use them as user defined characters in the second half of the ASCII table. Then it’s just a case of printing those characters on the screen in the right order to draw our image.

Printing the text on the screen isn’t too hard.

// This function needs modifying
void PrintBIOSLine(unsigned char *line, int row)
{
    unsigned char screen_line[160];
    memset(screen_line, 0,160);
    
    for (int i = 0; i < strlen(line); i++) {
        unsigned char colour = 0x07;
        if ((row == 1 || row == 2) && (i < 3)) colour = 0x09;
        VGAPutChar(screen_line, line[i], colour, i);
    }
    
    // Print the EPA logo
    int idx = (row-1) * 17;
    for (int i = 0; i < 17; i++) {
        unsigned char colour = epa_file.colour_map[i+idx];
        if (colour == 0x02) colour = 0x0E;
        if (colour == 0x01) colour = 0x02;
        VGAPutChar(screen_line, 128+i+idx, colour, i+60);
    }
    ScreenUpdateLine(&screen_line, row);
}

// And inside main() this needs adding before the printBIOSLine() calls
// It prints out the little Award logo in the far upper left of the screen
row1[0] = 0x20; row1[1] = 0xE6; row1[2] = 0xE7;
row2[0] = 0xE8; row2[1] = 0xE9; row2[2] = 0xEA;Code language: JavaScript (javascript)

Loading Data into the VGA Card

This is where things get a bit difficult, and where our simple “Hello World with a bit of a twist” becomes a journey into DOS interrupts and x86 memory modes.

For historical reasons that we won’t go into now, all PCs boot up assuming they are little Intel 8086 PCs with 640K of RAM and a whole 1Mb of address space. Our programs get the first 640K to play in, the remaining 384K is out of bounds and full of special stuff – like the VGA card.

I found a site listing the memory map of a PC when it boots into Real Mode. What “Real Mode” is will be a topic for the future.

startendsizedescriptiontype
Real mode address space (the first MiB)
0x000000000x000003FF1 KiBReal Mode IVT (Interrupt Vector Table)unusable in real mode640 KiB RAM (“Low memory”)
0x000004000x000004FF256 bytesBDA (BIOS data area)
0x000005000x00007BFF29.75 KiBConventional memoryusable memory
0x00007C000x00007DFF512 bytesYour OS BootSector
0x00007E000x0007FFFF480.5 KiBConventional memory
0x000800000x0009FFFF128 KiBEBDA (Extended BIOS Data Area)partially used by the EBDA
0x000A00000x000BFFFF128 KiBVideo display memoryhardware mapped384 KiB System / Reserved (“Upper Memory”)
0x000C00000x000C7FFF32 KiB (typically)Video BIOSROM and hardware mapped / Shadow RAM
0x000C80000x000EFFFF160 KiB (typically)BIOS Expansions
0x000F00000x000FFFFF64 KiBMotherboard BIOS

Since our PC runs DOS, Microsoft and PC BIOS manufacturers saw fit to save us some effort and include a load of routines for communicating with hardware. We do this using interrupts, specifically interrupt 0x10 (or “Int 10H” as it was known).

There is a full and extensive list of all the interrupts and what they do in something called “Ralph Brown’s Interrupt List“, the HTML version is also available all over the Internet.

It seems to be the DOS equivalent of the K&R C book as in it’s the source of info everyone needs to own, and preferably have memorised.

According to it, we need to use the following interrupts to do our bidding

  • Int 10H Set video mode. Setting the mode also clears the screen.
  • Int 10h, AX=1100H to load user defined characters.

Understanding what this means takes a bit of poking around, but the benefit of learning DOS is that there’s 30 years of documentation on the Internet to look at. Other people have done this before.

Protected mode and DJGPP

Problem is, none of this is any use to us because DJGPP uses GCC, and GCC comes from Linux where there is no Real Mode. It’s just protected mode which treats the PC as a more modern CPU that can access a flat address space. You can’t actually just call BIOS interrupts because where they are in RAM isn’t consistent from your program’s point of view.

Fortunately DJGPP can deal with this with some special interrupt handling code that is thoroughly documented. It’s a bit heavy going, and page 2 contains more relevant information. And if you bang your head against your wall enough times, and do the usual random distracted Google research, you’ll figure out the following code… probably.

void setFont()
{
    loadEPA("BiosLogo.epa");

    dosmemput(epa_file.bitmap,epa_file.width*epa_file.height*14,__tb);
    __dpmi_regs r;
    memset(&r, 0, sizeof(r));
    r.x.ax = 0x1110;
    r.x.cx = epa_file.width*epa_file.height;
    r.x.dx = 128;
    r.h.bl = 0;
    r.h.bh = 14;
    r.x.es = __tb >> 4;
    r.x.bp = __tb & 0x0f;
    __dpmi_int(0x10,&r);

    dosmemput(epa_file.award_logo, 70, __tb);
    r.x.ax = 0x1110;
    r.x.cx = 70;
    r.x.dx = 128+epa_file.width*epa_file.height;
    r.h.bl = 0;
    r.h.bh = 14;
    r.x.es = __tb >> 4;
    r.x.bp = __tb & 0x0f;
    __dpmi_int(0x10,&r);
};Code language: C++ (cpp)

The final code

Here is the final code I came up with, warts and all. It has a few weird glitches though. if you look at the logo it has vertical gaps between each character that aren’t present in the real BIOS screen. I think I know why this happens, but I don’t understand how to fix it.

The BIOS must assume the PC is a super basic 8086 with a CGA or EGA card in it, not a VGA card. And CGA or EGA have slightly different font sizes and shapes.

I don’t know how to fix it though. If you do, let me know!

#include <stdio.h>
#include <pc.h>
#include <dpmi.h>
#include <dos.h>
#include <sys/types.h>
#include <sys/movedata.h>
#include <go32.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>


typedef struct EPAFile {
    unsigned char width, height;
    unsigned char *colour_map;
    unsigned char *bitmap;
    unsigned char *award_logo;
} EPAFile;

EPAFile epa_file;

unsigned char *row1 = "   Award Modular BIOS v4.50PG, An Energy Star Ally";
unsigned char *row2 = "   Copyright (C) 1984-95, Award Software, Inc.";
unsigned char *row3 = "";
unsigned char *row4 = "ASUS P5a ACPI BIOS Revision 1011 Beta 005";
unsigned char *row5 = "";
unsigned char *row6 = "80486DX2 CPU at 66MHz";

void loadEPA(char *filename)
{
    FILE *epa = fopen(filename, "rb");
    fread(&epa_file, 1, 2, epa);
    //printf("EPA File is %d x %d in size\n", epa_file.width, epa_file.height);
    epa_file.colour_map = malloc(epa_file.width * epa_file.height);
    fread(epa_file.colour_map, 1, epa_file.width * epa_file.height,epa);
    epa_file.bitmap = malloc(epa_file.width * epa_file.height * 14);
    fread(epa_file.bitmap, 1, epa_file.width * epa_file.height * 14,epa);
    epa_file.award_logo = malloc(70);
    fread(epa_file.award_logo, 1, 70,epa);
}

void VGAPutChar(unsigned char *line, char c, char attrib, int index)
{
    line[index*2] = c;
    line[index*2+1] = attrib;
}

void PrintBIOSLine(unsigned char *line, int row)
{
    unsigned char screen_line[160];
    memset(screen_line, 0,160);
    
    for (int i = 0; i < strlen(line); i++) {
        unsigned char colour = 0x07;
        if ((row == 1 || row == 2) && (i < 3)) colour = 0x09;
        VGAPutChar(screen_line, line[i], colour, i);
    }
    
    // Print the EPA logo
    int idx = (row-1) * 17;
    for (int i = 0; i < 17; i++) {
        unsigned char colour = epa_file.colour_map[i+idx];
        if (colour == 0x02) colour = 0x0E;
        if (colour == 0x01) colour = 0x02;
        VGAPutChar(screen_line, 128+i+idx, colour, i+60);
    }
    ScreenUpdateLine(&screen_line, row);
}

void setFont()
{
    loadEPA("BiosLogo.epa");

    dosmemput(epa_file.bitmap,epa_file.width*epa_file.height*14,__tb);
    __dpmi_regs r;
    memset(&r, 0, sizeof(r));
    r.x.ax = 0x1110;
    r.x.cx = epa_file.width*epa_file.height;
    r.x.dx = 128;
    r.h.bl = 0;
    r.h.bh = 14;
    r.x.es = __tb >> 4;
    r.x.bp = __tb & 0x0f;
    __dpmi_int(0x10,&r);

    dosmemput(epa_file.award_logo, 70, __tb);
    r.x.ax = 0x1110;
    r.x.cx = 70;
    r.x.dx = 128+epa_file.width*epa_file.height;
    r.h.bl = 0;
    r.h.bh = 14;
    r.x.es = __tb >> 4;
    r.x.bp = __tb & 0x0f;
    __dpmi_int(0x10,&r);
};

void main()
{
    // Clear the screen
    __dpmi_regs r;
    memset(&r, 0, sizeof(r));
    r.h.ah = 0;
    r.h.al = 0x03;
    __dpmi_int(0x10,&r);

    // Switch to 8x14 EGA font
    r.h.ah = 0x11;
    r.h.al = 0x11;
    r.h.bl = 0;
    __dpmi_int(0x10,&r);

    setFont();
    

    row1[0] = 0x20; row1[1] = 0xE6; row1[2] = 0xE7;
    row2[0] = 0xE8; row2[1] = 0xE9; row2[2] = 0xEA;

    PrintBIOSLine(row1, 1);
    PrintBIOSLine(row2, 2);
    PrintBIOSLine(row3, 3);
    PrintBIOSLine(row4, 4);
    PrintBIOSLine(row5, 5);
    PrintBIOSLine(row6, 6);

    for (int j = 0; j < 4; j++) {
        for (int i = 0; i <= 32768; i+=1024) {
            ScreenSetCursor(7,0);
            printf("Memory Test : %6dK OK", i);

            ScreenSetCursor(23,0);
            printf("Press DEL to enter SETUP, ESC to skip memory test\n");
            printf("02/21/97-VT496G-2A4L6F0IC-00");
            
            delay(10);
        }
    }
    delay(1000);
    ScreenSetCursor(23,0);
    printf("Press DEL to enter SETUP                                   \n");
    printf("02/21/97-VT496G-2A4L6F0IC-00");
    ScreenSetCursor(9,0);
    printf("Award Plug and Play BIOS Extension  v1.0A\n");
    printf("Copyright (C) 1995, Award Software, Inc.\n");
    char c = getchar();
}
Code language: C++ (cpp)

Leave a Reply

Your email address will not be published. Required fields are marked *