Close

Component Object Model for a BIOS?! Why, yes!

A project log for Kestrel-2DX

The Kestrel-2DX, a specific embodiment of the Kestrel Computer Project (also on Hackaday), will help with new hardware bring-up.

Samuel A. Falvo IISamuel A. Falvo II 09/29/2017 at 16:040 Comments

Now that I have SD card access working, I find myself in a position where I must now consider how to invoke BIOS services from programs developed long after BIOS itself has been compiled.  I don't want to have to resort to using the CPU's ECALL or EBREAK instructions (or any trap, for that matter), because this both consumes an opcode that should really be used by a proper operating system, and because it would require that I renovate the linkage approach I have between the assembly language bootstrap and the C code.  Jump-tables or entry-point vectors are ideal for the needs of the BIOS; since I am working primarily in C at the moment, it seems reasonable that invoking services through a C-accessible mechanism is the best solution (otherwise, I'd have to write yet more assembly language stubs/proxies).  I knew just where to look; I decided to go back a decade, and excavate my old code from my GCOM project, my own clean-room clone of Component Object Model.

I'm happy to announce that I've managed to port a reasonable subset of GCOM into the firmware.  As the proof of concept, I only implemented console output functions and basic cursor control.  Keyboard input and SD card access will come later as time permits.

Here's how I defined the console output interface:

BEGIN_INTERFACE(IConsoleState)
	void (*cursor_on)(IConsoleState *);
	void (*cursor_off)(IConsoleState *);
	void (*cursor_up)(IConsoleState *);
	void (*cursor_down)(IConsoleState *);
	void (*cursor_left)(IConsoleState *);
	void (*cursor_right)(IConsoleState *);
	void (*clear_screen)(IConsoleState *);
	void (*write_char)(IConsoleState *, char);
	void (*write_buf)(IConsoleState *, char *, size_t);
	void (*write_str)(IConsoleState *, char *);
END_INTERFACE(IConsoleState)

 This ends up defining two structures for me:

typedef struct IConsoleState IConsoleState;
typedef struct IConsoleStateVtbl IConsoleStateVtbl;
struct IConsoleState {
    IConsoleStateVtbl *_v;
};
struct IConsoleStateVtbl {
    HRESULT (*QueryInterface)(IConsoleState *, REFIID, void**);
    uint32_t (*AddRef)(IConsoleState *);
    uint32_t (*Release)(IConsoleState *);
    // ... rest of definitions as seen above goes here
};

The basic idea is fairly straight-forward: the interface you want to distribute to your clients must start with the IConsoleState header.  For example, elsewhere in the BIOS sources, I define a ConsoleStateImpl structure:

struct ConsoleStateImpl {
    struct IConsoleState base;
    // rest of definitions goes here.
};

In this way, I can conveniently define a single interface object.  Objects which support multiple interfaces requires a bit more thought, but it's not overwhelmingly complicated to work with, and in my experience, are quite rare in practice.  In fact, I've yet to write one in the decade that GCOM has existed as a project.

It should be noted that, because the console is itself a singleton object that never goes away, the AddRef() and Release() methods both are mapped to a function that just returns zero; they do nothing.  QueryInterface() will return itself if you ask it for either IID_IUnknown or for IID_IConsoleState, or E_NOINTERFACE otherwise.  In this way, this implements a proper COM object as far as any client software is concerned.  You can look at the (rather bulgingly ugly at the moment) source file to find how IIDs are defined.

Neither aggregation nor delegation are a consideration, since these are truly primitive objects.

Creation of the first object happens in RAM, where you'd expect.  First, I had to change the console output code to locate its static data in a data structure called BiosData.  Eventually, all BIOS state will appear here.  This allows me to assign a matchword to the structure, thus allowing programs loaded from secondary storage to scan through the first 256KB of memory (or whatever limit I choose) for it.  Once references to the BIOS-provided objects are attained, services upon them may be called.

Here's the new console state definition:

struct ConsoleStateImpl {
	IConsoleState		base;
	// Coordinates of the cursor on the screen currently.
	uint8_t			cx, cy;
	// Flags relating to the cursor.
	//
	// Bit	Purpose
	// 0:	1=cursor blink state visible; 0=cursor blink state invisible.
	uint8_t			cflags;
	// This field counts how many times the cursor_off procedure has been
	// called.  Every cursor_off must be matched with a corresponding call
	// to cursor_on before the cursor will ever be visible again.
	uint_t			coffctr;
};

And here's how it's mapped into the BIOS data structure:

#define BD_MATCHWORD	(0x0BADC0DEB105DA7A)
struct BiosData {
	uint64_t		matchword;
	struct ConsoleStateImpl	c;
};
static struct BiosData g;

So, BIOS-resident code would access the console state variables by referencing g.c.whatever.  Since this is in RAM, we must initialize g.c, and it now looks like this:

static void
con_init(void) {
	g.c.base._v = (IConsoleStateVtbl *)ROM_RELOC(&console_vtbl);
	g.c.cflags = 0;
	g.c.coffctr = 1;
	clear_screen();
}

The new addition here is setting g.c.base._v to refer to the correct address for the IConsoleStateVtbl instance, which was defined statically elsewhere in the code.

Using the new services is pretty straight-forward:

void
_start(void) {
	int erc;
	IConsoleState *ics = (IConsoleState *)(&g.c);
	con_init();
	spi_init();
//	con_write_string((char *)ROM_RELOC("Kestrel-2DX Development System.\r\n"));
//	con_write_string((char *)ROM_RELOC("ROM version 0.1\r\n\n"));
	con_write_string((char *)ROM_RELOC("ics=")); hex64((uint64_t)ics); LOG("\r\n\n");
	ics->_v->write_str(ics, (char *)ROM_RELOC("Kestrel-2DX Development System.\r\n"));
	ics->_v->write_str(ics, (char *)ROM_RELOC("ROM version 0.1\r\n\n"));
	for(;;) timv();
}

First, we need to get a reference to the IConsoleState interface somehow; since we're already in BIOS, we can do this easily by just taking the address of our BiosData field.  However, if we were to do this from RAM, we'd need to scan for the BiosData matchword, and then grab the reference from the discovered address.

Once you have the initial reference, invoking methods is done as you'd expect: dereference _v to access the appropriate function pointer, making sure to pass the interface as the first argument every time.  Using the interface is otherwise identical to using the flat, statically linked interface.

The rest of the code is pretty much boilerplate or inefficient code purely for the sake of expediency.  With COM integrated into BIOS like this, synthesis takes up 71% of the FPGA's area, which means I don't have much more room to play with.  Some things help though:

Once these three things are complete, I feel that the need to alter ROM significantly will fall off a cliff, meaning the Kestrel-2DX will more or less be "complete" as a computer design goes.  From there, it's a "small matter" of writing an operating system for it, but that's a project for another day.

Discussions