Go to the previous, next section.

Device Drivers

This chapter documents the individual physical device drivers we have implemented. A device driver is a module to control one of the host system's external devices. Each device itself will only be addressed by its driver, all other parts of the system will communicate with the device driver and not the driver itself.

Generic Block Device Support

Many of the peripheral devices found in a PC are block-oriented, this means that the data stored on the device can only be accessed in units of a block (a fixed-size group of bytes). For example, most hard and floppy disks store data in 512-byte blocks.

To make the implementation of a device driver for a block device easier we have defined the structure which such a driver can take, together with generic code to implement the core parts of the driver.

Since the driver must work in a multi-tasking environment where many tasks may want access to the device at the same time special care has to be taken; the generic device structure basically defines a list of requests that the device should process, the structure of each individual request block and several functions to manage the request list.

The C header file containing this is called `<vmm/blkdev.h>', it should be included by each block device driver implementation. It requires two preprocessor macros to be defined before it is included, these are:

BLKDEV_TYPE
This should be defined to the type of the device driver's private data structure used to contain a single device's state.

BLKDEV_NAME
This must be defined as a string naming the device driver.

The IDE device driver uses the following piece of code to define these macros and then include the header file:

#define BLKDEV_TYPE ide_dev_t
#define BLKDEV_NAME "ide"
#include <vmm/blkdev.h>

The header file `blkdev.h' defines each request structure as the following:

typedef struct {
    /* Link in the list of requests. */
    list_node_t node;
    /* Device to access */
    BLKDEV_TYPE *dev;
    /* Block being read/written */
    char *buf;
    /* Index of block being read/written */
    u_long block;
    /* Number of blocks to access */
    int nblocks;
    /* Device-specific command. */
    int command;
    /* := result of request, zero for success */
    int result;
    /* Times we tried to do this command */
    char retries;
    /* TRUE when request has finished */
    bool completed;
    /* Task locked on this request. */
    struct semaphore sem;
} blkreq_t;

The `<vmm/blkdev.h>' header file also contains three functions which are used to manipulate the list of requests maintained by each device. Since each device driver is free to name the symbols it uses to implement the driver as it wishes these functions are defined as macros taking the names of these objects as arguments. Each device drivers typically has a single expansion of the macros defining the functions that it wishes to use at the start of its C code. These macros are documented in the following paragraphs.

Macro: END_REQUEST_FUN current-req-var

This macro defines the function called end_request. Its only argument is the name of this driver's variable storing the request currently being processes.

Macro: SYNC_REQUEST_FUN do-req-fun

This macro defines the function called sync_request. Its parameter names the function used by this driver to add a new request to its queue and start processing this request if possible.

For example, the IDE driver calls this macro once as:

SYNC_REQUEST_FUN(do_request)

and later defines the function do_request something like:

/* If no request is currently being processed and there's new
   requests in the queue, process the first one. This can be
   called from an interrupt or the normal kernel context. REQ
   is either a request to try and process or NULL. It will get
   added to the list of requests if necessary. */
static void
do_request(blkreq_t *req)
{
    u_long flags;
    save_flags(flags);
    cli();
    if(REQ-IN-PROGRESS)
    {
        if(req != NULL)
            APPEND-TO-REQ-LIST(req);
        load_flags(flags);
        return;
    }
    if(req == NULL)
        req = NEXT-REQUEST-FROM-QUEUE();
    current_req = req;
    load_flags(flags);
    /* Process this request... */

Note the careful saving and restoring of the interrupt-enable flag. Since most device drivers are interrupt-driven (i.e. requests usually end when an interrupt signals that a transaction has completed) it is essential that all accesses to the driver's data structures are atomic.

Macro: ASYNC_REQUEST_FUN do-req-fun

This macro defines the function async_request. It is very similar to the macro SYNC_REQUEST_FUN.

Once the necessary functions have been expanded by a single call to each of the corresponding macros, they can be used as normal. These function are documented in the following paragraphs.

Function: static void end_request (int result)

This function should be called when the current request has been completed (either successfully or unsuccessfully). It sets the request's result field to result, its completed field to TRUE and calls signal (see section Semaphores) on the request's semaphore.

Note that this function may be called from interrupt handlers and that it is usually necessary to call the driver's function to fire up the next request in the queue.

Function: static inline bool sync_request (blkreq_t *req)

This function is used to synchronously execute a single request (the block request structure pointed to by req). After the request have been processed the function returns TRUE if it completed successfully, or FALSE otherwise.

Note that tasks calling this function will be suspended while the request is executing.

Function: static inline void async_request (blkreq_t *req)

This function is used to start a block request; the request req will be added to the end of the correct request queue.

The only way to find out when the request has completed is by testing the value of the completed field of req. If the task wishes to be suspended until the request completes it can simply call the wait function (see section Semaphores) on the request's semaphore.

For examples of block device drivers based on the `<vmm/blkdev.h>' skeleton see the system source files `drivers/physical/hd/ide.c' and `drivers/physical/floppy/floppy.c'.

The Floppy Disk Driver

The floppy disk device driver provides the filing system (see section The Filing System) with an interface to talk to the floppy disk drives of the machine. The module is divided into three sections:

  1. Filing System interface. This deals with requests from the filing system, and the operating system generally, and performing the desired functions.

  2. Function and Queue handling. This area acts as a go-between between the Filing system and the lowest level of the floppy driver. The functions performed by this module are those that are not low level, but aren't part of the interface to the rest of the system.

  3. Low level FDC (Floppy Disk Controller) handling. This is concerned with the lowest level I/O required to control the floppy drive and it's associated hardware.

The floppy disk driver is found in the `fd' module (see section Modules) and all the functions documented in this section of the manual are located in this module (relevant header files are `<vmm/floppy.h>' and `<vmm/fd_regs.h>' as well as `fd.h' in the floppy driver module directory).

Floppy Filing System Interface

The only real function of this module of code, located in the file `fd-fs.c', is to define the functions which are exported primarily to the Filing system. It converts requests in terms of blocks to those of sectors and queues them for execution.

The Floppy hardware on PC's defines a sector size of 512 bytes, and the Floppy device treats each sector as a block. The Filing system, however, does not have to use the same size for a block, and presently uses 1024 byte blocks. This mapping is performed quite simply before the request is queued.

fd Function: long floppy_read_block (void *fd, blkno block, void *buf, int count)

This function, called from the Filing system, merely converts the block and count values, which are in terms of the block size used by the Filing system, into sector based values and issues a call to the floppy_read_blocks function (see below). fd is a pointer to the desired Floppy drive, and buf is where the information is to be read into.

fd Function: long floppy_write_block (void *fd, blkno block, void *buf, int count)

This function is exactly the same as floppy_read_block, above, with the exception is issues a call to the floppy_write_blocks function, described below. Similarly, the buf variable points to the information to be written out.

fd Function: long floppy_read_blocks (fd_dev_t *fd, void *buf, u_long block, int count)

This function take the information passed and builds a request buffer based on it, queuing this read operation for execution using the mechanism defined in `<vmm/blkdev.h>' (see section Generic Block Device Support).

fd Function: long floppy_write_blocks (fd_dev_t *fd, void *buf, u_long block, int count)

As with floppy_read_blocks, this function queues the specified write request with the Floppy driver.

Other requests the the Filing system can generate are in terms of device management, rather than utilisation. These are more oriented towards fixed-devices, rather than removable, such as mount and make partition. Floppy disks are generally too small to warrant partitioning. These functions thus just return FALSE to indicated that they are not supported.

During the initialisation stage of the driver, the Filing system is informed of the Floppy devices available, and this is handled in this section.

Finally, the module contains the timer interrupt routine used to disable the Floppy drive motors after a period of inactivity.

Floppy Function And Queue handling

This code, found in `floppy.c', contains the central queue handling code, which itself contains the code to drive the lower level elements of the driver, and the module initialisation code.

During initialisation, the driver queries the CMOS RAM to find out the number of drives present and their types, resetting the FDC and causing a recalibrate of each drive in the process. It also starts the timer required to disable drive motors after a period of inactivity and allocates space for DMA transfers.

There are presently three requests that can be performed, Read, Write and Seek. Read and write are self explanatory, and seek moves the head of the specified drive to a specific cylinder. These each perform the set of functions calls, found in the lower level FDC module described next.

FDC Low Level Routines

The operation of the Floppy subsystem falls into two areas as a result of it's implementation in the PC architecture. The PC uses a the 8272A Floppy Disc Controller (FDC) produced by Intel, although most modern implementations use the slightly enhanced NEC PD765B version.

As part of the mechanism to control drive motors and the drive select, possibly across more than one FDC, the PC introduces what is known as the Digital Output and Digital Input Registers (DOR and DIR, respectively). On older XT based machines, only the DOR is present. The DIR's function is to set the data rate for High-density floppy devices.

The `fdc.c' module defines a set of functions for setting up the DOR and DIR, accounting for motors that should be running.

Each function of the FDC then calls, if relevant, the DOR function to select the relevant drive and then performs the function. Most, but not all, of the FDC functions generate an interrupt during their execution.

The Hard Disk Driver

The hard disk device driver allows the system (and especially, the filing system, see section The Filing System) to talk to any hard disks connected to the host computer. This driver is split into two main parts:

  1. Generic hard disk support. This part of the driver is concerned with the higher-level aspects of disks. For example, this part of the driver handles all interfacing between the filing system and the lower- level physical drivers. Also, disks are split into partitions for compatibility with other PC operating systems.

  2. Drivers for each supported type of hard disk interface; currently we have only implemented an IDE driver. If a SCSI driver is required this would be easy to install (even without recompiling the hard disk module).

The hard disk driver is contained in the `hd' module (see section Modules), all functions documented in this section of the manual are located in this module (see the header file `<vmm/hd.h>').

Generic Hard Disk Handling

This is a layer of code using the physical hard disk drivers to present a unified interface to any type of hard disk to the rest of our operating system. It defines two important data structures, hard disk devices and hard disk partitions.

The device structure is the most fundamental; each low-level driver creates device structures for each disk that it detects and gives each device a special name (for example the IDE driver calls the first IDE disk `hda' and the second `hdb') which it may be referred to by.

After each hard disk device is registered with the generic hard disk layer (by the low-level driver) the disk is scanned for partitions (these are standard PC partitions, except that any number of extended primary partitions are allowed) and a partition structure created for each one found. These partitions are also named, their name is the concatenation of the name of the device which they occur on and a number. For IDE disks the four primary partitions are given the numbers one to four (i.e. `hda1' ... `hda4') and any extended partitions are numbered from five upwards.

The actual data structures are defined as:

#define PARTN_NAME_MAX 8
typedef struct hd_partition {
    struct hd_partition *next;
    /* The first logical block of this partition, relative to
       the start of the disk. */
    u_long start;
    /* The number of blocks in the partition. */
    u_long size;
    /* The device which the partition occurs on. */
    struct hd_dev *hd;
    /* The name of the partition. */
    char name[PARTN_NAME_MAX];
} hd_partition_t;

typedef struct hd_dev {
    struct hd_dev *next;
    /* The name of the device. */
    const char *name;
    /* The geometry of the disk. */
    u_long heads, cylinders, sectors;
    /* The number of blocks on the disk. */
    u_long total_blocks;
    /* A function to read a sequence of blocks from the
       device, returning TRUE on success. */
    bool (*read_blocks)(struct hd_dev *hd, void *buf, u_long block,
                        int count);
    /* A function to write a sequence of blocks to the
       device, returning TRUE on success. */
    bool (*write_blocks)(struct hd_dev *hd, void *buf, u_long block,
                         int count);
} hd_dev_t;

Note that the hard disk module always treats blocks as being 512 bytes long; this is different to the filing system (which currently uses 1024 bytes per blocks).

As each physical disk driver initialises itself, and scans for any attached drives that it should control, it creates an hd_dev_t for each disk and calls the function add_dev to get the generic hard disk code to initialise its partitions.

hd Function: bool add_dev (hd_dev_t *dev)

This function adds dev to the list of hard disk devices and then scans the disk for partitions, creating a new partition structure for each one found. As it does this it prints a line of output to the console describing the partitions it finds.

If any partitions have their "system-type" field set to the value 48 the partition will be automatically mounted to the filing system (see the function mount_partition).

As well as creating a partition structure for each partition recognised this function also creates one spanning the whole of the physical disk, using the same name as the name of dev.

When this function succeeds it returns TRUE, otherwise FALSE.

All the other functions exported by this part of the hard disk module are used to manipulate partitions, since they are the usual way that the hard disk is accessed.

hd Function: hd_partition_t * find_partition (const char *name)

This function searches the system's list of partition structures for one whose name matches the string name. If such a partition is found a pointer to it is returned, otherwise a null pointer is returned.

Note that this function introduces a large flaw into the system: it precludes the removal of partition structures from the system (removable hard disks?) since there is no guarantee that a partition structure is not being used. To solve this a reference-count field is needed in each partition structure and two functions to get and release a partition (adjusting its reference count as required).

hd Function: bool read_blocks (hd_partition_t *partn, void *buf, u_long block, int count)

This function reads count blocks from the partition represented by the partition structure partn. The first block read is block and it is stored at the address buf in the kernel segment.

If this function fails it returns FALSE, otherwise TRUE.

hd Function: bool write_blocks (hd_partition_t *partn, void *buf, u_long block, int count)

This function writes count blocks from the partition represented by the partition structure partn. The first block written is block and its new contents are taken from the address buf in the kernel segment.

If this function fails it returns FALSE, otherwise TRUE.

The following two functions provide a means of using hard disk partitions as devices in the filing system (see section The Filing System).

hd Function: bool mount_partition (hd_partition_t *partn, bool read-only)

This function is used to add the partition partn to the filing system as one of its devices. The device's name will be the same as the name of partn.

If the read-only parameter is not FALSE the partition will be added as a read-only file system.

This function returns TRUE on success, FALSE otherwise.

hd Function: bool mkfs_partition (hd_partition *partn, u_long reserved)

This function creates a new, empty, file system on the partition represented by partn.

The parameter reserved defines the number of blocks to reserve at the start of the disk (starting at block number one). Note that these blocks are filing system blocks, that is 1024-byte blocks, not the 512-byte blocks that the hard disk module deals in.

Note that it is an error to attempt to mkfs a mounted partition.

This function returns TRUE in case of success, FALSE otherwise.

The IDE Driver

The IDE driver portion of the hard disk module is a low-level device driver providing an interface with the first IDE controller to the higher-level hard disk layer.

It uses the standard structure defined by the header file `<vmm/blkdev.h>' (see section Generic Block Device Support) to implement a simple interrupt-driven IDE driver. It attempts to handle errors as sensibly as possible, recalibrating the individual drives and resetting the controller when it thinks it necessary.

Tasks submitting IDE requests are put to the sleep for the duration of the request, thereby allowing other tasks to utilise the CPU while IDE I/O is in progress. This allows hard disk access to cause as little performance loss as possible to the system.

When the driver initialises itself it probes for the disks which are connected to the IDE controller then uses the BIOS hard disk tables to provide the geometry of the disk. As each disk is recognised the function add_dev is used to add it to the generic hard disk layer's list of devices.

The Ramdisk Device

The ramdisk driver is in fact a very simple block driver, which represents a block of memory in a form usable by the filing system see section The Filing System module.

The ramdisk is designed to be able to mimic a floppy disk, and as such works on an internal block size of 512 bytes, which is mapped from the filing system's present block size of 1024 bytes.

ramdisk Function: rd_dev_t * create_ramdisk (u_long blocks)

This function is called to create a ramdisk device. In doing so, the relevant amount of memory is allocated and the filing system is instructed to prepare it as a file system, where it is subsequently mounted ready for use. As it stands, a 1.4meg ramdisk is created at driver initialisation, also. Ramdisk devices are labelled `rd0' where 0 is the number of the ramdisk.

The other functions inside the Ramdisk module are fairly standard Filing system and block device interface functions.

Video Drivers

The video driver controls all direct access to the host computer's video hardware; it provides the ability for each task to have its own virtual video adapter. The user is then able to switch between the virtual adaptors, displaying whichever they wish, by special hot-key combinations.

Several types of virtual adaptor are already implemented (CGA and MDA); the video subsystem has been designed to allow other types of virtual display adaptor to be supported. Currently the video driver will only work with a VGA-compatible video card, although it would be feasible to change this if necessary (without having to recompile the video module).

All existing video support is located in the `video' module (see section Modules), and all functions documented in this part of the manual are found in this module unless otherwise stated.

Note that normally the `video' module is not used explicitly to create a new screen, this function is normally performed by the `tty' module. See section TTY Driver.

Using Virtual Adaptors

As already stated, tasks may create virtual video adapters to give themselves a way of producing screen output. Each of these virtual adaptors is represented by an instance of the following structure:

struct video {
    /* Points to a constant structure defining the type
       of this adaptor. */
    struct video_ops *ops;
    /* The task which created this adaptor. */
    struct task *task;
    /* Data describing the state of the *physical* adaptor
       when this virtual adaptor is in the foreground. */
    struct vga_info vga;
    /* Data describing the current video mode of this
       adaptor. */
    struct mode_info mi;
    /* Defines the video mode, these numbers are the same as
       used by the BIOS INT 0x10,0 function. */
    u_char mode;
    /* TRUE when this virtual adaptor is in the foreground. */
    bool in_view;
    /* Local data for each type of virtual adaptor. */
    union {
	struct mda_data mda;
	struct cga_data cga;
	char padding[MAX_VIDEO_DATA];
    } data;
};

When a task wishes to create a new virtual display it simply allocates one of these structures somehow, then calls the function init_video with the video structure, a string naming the type of adaptor that it wishes be virtualised and some other arguments.

video Function: bool init_video (struct video *new, struct task *task, const char *type, u_long flags)

This function initialises the video structure new and creates the virtual adaptor that it will represent. The task task is designated as being the owner of the new adaptor (see section Task Handling).

The string type names the type of adaptor to virtualise; by default the system only supports two options here: `mda' and `cga' for MDA and CGA virtualisations respectively. The flags parameter is passed to the initialisation function of the chosen video type and therefore is defined by the individual video types.

The created virtual adaptor will not be viewable; to switch to this display the function switch_video can be used.

If this function fails (for example it can't find a way to virtualise an adaptor of type type) it will return FALSE, otherwise it returns TRUE.

Note that since each virtual video adaptor is mapped into the task's user segment (where the adaptor's real video memory would be), each task may only own one virtual adaptor at any one time.

video Function: void kill_video (struct video *video)

This function is used to deallocate any resources associated with the virtual video adaptor represented by the video structure video.

Note that it is not a particularly good idea to call this function when the display being killed is still in the foreground (i.e. it's being displayed by the physical video adaptor). Nothing too catastrophic should happen, but it may not be pretty.

Only one virtual adaptor may be displayed at once (no windowing system as yet!), this adaptor is referred to as being in the foreground (the others are in the background surprisingly). The function switch_video is used to control which adaptor is being displayed at any one time.

video Function: void switch_video (struct video *video)

This function brings the virtual adaptor represented by the video structure video to the foreground if it's not already there.

Video Modes

Many types of video adaptor provide more than one possible display mode (i.e. different resolutions, numbers of colours, text or graphics, etc...). Obviously the virtual video adaptors must also provide all of these modes to be as compatible as possible with the actual adaptor specifications.

Each virtual adaptor structure (see section Using Virtual Adaptors) contains an instance of the following structure called mi, describing the adaptor's current video mode:

struct mode_info {
    /* The dimensions of the display. In text modes this
       is in characters, otherwise in pixels. */
    short cols, rows;
    /* A pointer to the start of the video buffer. Note that
       this buffer is in the *user* segment of the task
       owning this adaptor. */
    char *buffer;
    /* The number of bytes in each page of the buffer. */
    size_t page_size;
    /* The maximum number of pages that the adaptor can
       contain. */
    u_char pages;
    /* TRUE if this is a text-based mode, FALSE for
       a graphical mode. */
    bool text_mode;
    /* TRUE if this mode supports colour. */
    bool colour;
};

Each video structure also contains a field mode; this is an integer corresponding to the BIOS mode that the adaptor is in. Although virtual machines running in a virtual adaptor are likely to use programmed I/O to change video modes (see section Virtualisation Details) the function set_mode can be used to achieve the same effect with less overhead (this is how the virtual BIOS changes video modes).

video Function: bool set_mode (struct video *video, u_int mode)

This function changes the video mode being emulated by the virtual adaptor represented by video to the BIOS mode defined by the integer mode.

If the operation fails -- usually because the type of device that video is emulating doesn't support the specified mode -- the function returns FALSE. Otherwise, if everything succeeds, the value TRUE is returned.

Virtualisation Details

As has been alluded to in the previous sections, virtual video adaptors are mainly intended for running virtual machines in. They are also used to great effect by the system's command-shell but this is accomplished through the TTY interface (see section TTY Driver). In general they concern themselves with providing as compatible a replica as possible of the actual video systems found in a PC, because of this very little time has been spent on providing easy methods of accessing the virtual displays from the kernel level.

There are two aspects to each of the virtualisation implementations:

  1. Video buffer virtualisation. Since displays can be put in the background each has its own piece of memory the same size as its video buffer. When the display is in the background this memory is mapped into the task's address space where the adaptor's video buffer would normally be; this allows tasks to continue to access their "video memory" even when they are in the background.

    The one display which is in the foreground has the host system's physical video buffer mapped into its task's address space, allowing that access to the virtual adaptors video buffer to go straight to the physical video buffer.

    When the foreground display is changed (see the function switch_video) the old foreground display has the contents of the system's physical video buffer copied to its local buffer and that buffer mapped to its task's video memory's address. The new foreground display then has the contents of its local buffer copied to the true video buffer and that mapped into the task's address space.

  2. I/O port virtualisation. All the usual PC video cards are controlled by accessing registers on the card through standard I/O ports. Using the standard method of virtualising I/O ports (see section Virtual I/O Ports) the video driver traps all access by virtual machines of the I/O ports in the ranges 0x3b0 to 0x3bb (the MDA registers) and 0x3c0 to 0x3df (VGA and CGA) registers.

    Each type of virtual adaptor is responsible for handling, as best as it can, each reference to a port in these ranges; ignoring those which don't concern it (for example the MDA virtualisation only handles the ports in the 0x3b0 to 0x3bb range).

Code running at the kernel level does not have any of I/O operations emulated automatically, since it is often necessary to access the virtualised video ports from the kernel the video module provides two functions inb and outb to do this.

video Function: u_char inb (struct video *video, u_short port)

This function emulates an IN instruction from the video adaptor port number port from the virtual display represented by video. The value returned is the result of this operation.

video Function: void outb (struct video *video, u_char value, u_short port)

This function emulates an OUT instruction to the video adaptor I/O port numbered port for the virtual display video. The byte sent to this port is value.

Currently we have only virtualised two types of video adaptor:

MDA
The most straightforward of all PC video cards, the MDA card can only support one mode: 80x25 monochrome text.

CGA
The CGA card support several modes, including various text modes (colour or monochrome and 80x25 or 40x25) as well as two crude graphical modes, 320x200 with 16 colours and 640x200 monochrome.

Implementing New Video Types

The video driver subsystem is not limited to emulating the MDA or CGA video cards, it would be reasonably straightforward to provide an emulation of any other type of card. In fact, this can be achieved without even recompiling the video module: simply create a new module for the new emulation and then use the functions and data types described in this section to register it with the video driver. If a virtual display is initialised with a type matching that of your emulation then it will be used automatically.

Each type of video card being virtualised is represented by a single instance of the following structure:

struct video_ops {
    struct video_ops *next;

    /* The name of this virtualisation, this is compared
       with the type string passed to the function `init_video'. */
    const char *name;

    /* The following function vectors are used to perform
       most manipulation of the video structures of this
       type. */
    bool (*init)(struct video *v, u_long flags);
    u_char (*inb)(struct video *v, u_short port);
    void (*outb)(struct video *v, u_char byte, u_short port);
    void (*switch_to)(struct video *v);
    void (*switch_from)(struct video *v);
    void (*kill)(struct video *v);
    char *(*find_page)(struct video *v, u_int page);
    bool (*set_mode)(struct video *v, u_int mode);

    /* This function will be called when the video module
       expunges itself from the system. */
    void (*expunge)(void);
};

To register a new video mode emulation with the video driver simply fill in an instance of this structure (the next field is used as a link in a list of modes, it shouldn't be touched) then call the video module function add_video.

video Function: void add_video (struct video_ops *new)

This function adds the new video type structure new to the list of video types maintained by the video module.

After calling this function any calls to init_video with a type parameter matching the name field of new use new as their video emulator.

Normally each type of virtualised video card will need to store per-display values, this can be accomplished through the use of the data field of the struct video (see section Using Virtual Adaptors). This area of the video structure will always contain at least 128 bytes which may be used in any way the video emulation desires. Normally a structure of the values to be stored will be defined and a pointer to the data field, cast to the type of the structure. There is actually a predefined macro to do this for you:

Macro: VIDEO_DATA (video, type)

This macro expands to a pointer to the data field of the video structure video cast to the data type type.

For example:

VIDEO_DATA (v, struct foo *)
  ==> ((struct foo *)(&(v)->data))

Note that the following function descriptions actually refer to the functions in the struct video_ops type, not to functions in the video module. The v parameter that each of these functions gets is the virtual display to operate on, obviously a particular emulation type's functions are only called when the virtual display is of that type.

video_ops Function: bool init (struct video *v, u_long flags)

This function is called when the video structure v is initialised to a display of this type. flags is the parameter of the same name passed to the init_video function.

Note that the only values needing to be initialised in v are those stored in the data field, all others are initialised by init_video. This function should also initialise the page table entries mapping the display's virtual video buffer.

When this function succeeds it should return TRUE, otherwise FALSE.

video_ops Function: u_char inb (struct video *v, u_short port)

This function is responsible for handling IN instructions from the virtual video adaptor v. It should return the result of reading the I/O port numbered port.

video_ops Function: void outb (struct video *v, u_char value, u_short port)

This function handles OUT instructions to v. It should emulate writing the value value to the I/O port numbered port.

video_ops Function: void switch_to (struct video *v)

This function is called when a video structure of this type is brought into the foreground. Generally this function only has to map the system's physical video memory into the address space of the task owning v (together with the necessary copying of the current video buffer contents).

Note that the VGA registers for this display are reloaded automatically (see section The VGA Functions).

video_ops Function: void switch_from (struct video *v)

This function is called when the virtual display represented by v is moved from the foreground to the background and v is of the type maintained by this set of emulation functions. This usually has to map the display's virtual video buffer back into the task's address space.

Note that the video module does not automatically save the VGA registers each time a display goes into the background.

video_ops Function: void kill (struct video (v)

This function is called when v is destroyed, it provides a chance to deallocate any local resources which the display is using (for example memory pages).

video_ops Function: char * find_page (struct video *v, u_int page)

This function should return a pointer (valid in the kernel segment) to the display page number page of the virtual display represented by v. If such a page does not exist a null pointer should be returned.

video_ops Function: bool set_mode (struct video *v, u_int mode)

This function is used to change the video mode of v to the BIOS mode mode. Generally this just checks that the type of adaptor being virtualised supports the mode then calls vga_set_mode if it does.

When this function is successful in changing the mode it should return TRUE, otherwise FALSE.

The VGA Functions

To aid in the implementation of video adaptor emulations the video subsystem provides a number of functions to manipulate the system's VGA hardware. Most importantly they allow the storing and subsequent reloading of the complete set of VGA control registers. This makes switching virtual displays very easy; simply reload the new display's register set (and setup the video buffer, but that doesn't concern us here).

The header file `<vmm/vga.h>' defines the VGA registers and the structure used by these functions, it stores all the VGA registers as well as some other information:

struct vga_info {
    /* An image of the VGA control registers. The actual format
       of this array is defined in the header file, it's
       something like:

         24 CRT controller registers
         21 Attribute controller registers
         9  Graphics controller registers
         5  Sequencer registers
         1  Miscellaneous control register */
    u_char regs[VGA_TOTAL_REGS];

    /* These define the I/O ports used to access the CRT index
       and data ports (either 3b4/3b5 or 3d4/3d5). */
    u_short crt_i, crt_d;

    /* The I/O port used to access the Input Status Register 1,
       either 3ba or 3da. */
    u_short is1_r;
};

Each video structure (see section Using Virtual Adaptors) contain an instance of this structure as their vga field. This holds the VGA information for each virtual display, and is loaded automatically each time the foreground display is switched.

video Function: void vga_save_regs (struct vga_info *vga)

This function stores the current values of the VGA control registers into the structure pointed to by vga.

Note that this function leaves bit 5 of the Attribute controller address register unset; this means that the display is disabled.

video Function: void vga_load_regs (struct vga_info *vga)

This function loads the VGA control register values stored in the structure pointed to by vga into the VGA hardware.

Note that this function leaves bit 5 of the Attribute controller address register unset; this means that the display is disabled.

video Function: void vga_enable_video (struct vga_info *vga)

This function enables the video hardware by setting bit 5 of the Attribute controller address register.

The vga parameter is only used to get the I/O address of the Input Status 1 register.

video Function: void vga_disable_video (struct vga_info *vga)

This function disables the video hardware by clearing bit 5 of the Attribute controller address register.

The vga parameter is only used to get the I/O address of the Input Status 1 register.

As well as manipulating the VGA registers the video module also stores instances of the above structure for many of the standard BIOS video modes. When it is necessary to switch the video mode that a display uses it is therefore possible to get a copy of the register values to put the physical display into that mode.

video Function: bool vga_get_mode (u_int mode, const char **regsp, const struct mode_info **infop)

This function attempts to find a set of VGA registers and a mode_info structure for the standard BIOS video mode number mode. If it can find such a mode the two pointers regsp and infop have the location of these structures stored in the address that they point to, and the function returns TRUE. Otherwise, the function returns FALSE.

For example:

const struct vga_info *vga;
const struct mode_info *mi;
if(video->vga_get_mode(3, &vga, &mi))
{
    /* Use the information... */

video Function: bool vga_set_mode (struct video *video, u_int mode)

This function attempts to change the video mode used by the virtual display video to the standard BIOS video mode number mode.

If it succeeds it returns TRUE, else FALSE is returned.

Keyboard Driver

The keyboard device driver handles all low-level interfacing with the keyboard and its controller, it supports multiple logical keyboards, allowing the system to control where the keys typed at the keyboard are actually sent.

Several types of logical keyboards are provided, raw keyboards simply transmit each scan code as it is received while cooked keyboards translate scan codes into ASCII strings. A separate part of the system implements virtual keyboards which emulate the keyboard hardware for a virtual machine (see section Virtual Keyboard).

All functions described in this section of the manual are members of the `kbd' module (see section Modules), the header file describing this device driver is `<vmm/kbd.h>'.

Keyboard Overview

The heart of the keyboard driver is its IRQ handler, this is the function which is called each time a scan code is available in the keyboard buffer. Since the keyboard IRQ does not have to be serviced that quickly the handler is implemented as a queued IRQ handler --- this relaxes the large restrictions that IRQ handlers normally face (see section Interrupt Handlers).

When the IRQ handler receives a scan code it attempts to convert it to a key code, this is an eight-bit value defining the key which was just pressed or released (sometimes more than one scan code is received for a single event, key codes simplify our handling of these events). The mapping between key codes and the keys on the keyboard is defined by the header file `<vmm/keycodes.h>'; where a single scan code is sent for an event its key code is the same as the scan code, where more than one scan code is sent for a single event an arbitrary key code value has been chosen.

Once a key code has been computed for the received scan code the next thing the IRQ handler does is to check it against each of the special system hot-keys, these include Alt-SysReq, Alt-ESC, Ctrl-Alt-DEL, Alt-KpPlus and Alt-KpMinus. The action that these keys have is described in another part of this manual. If the key code doesn't match one of these hot keys the IRQ handler then checks if it is one of the various `shift' keys (i.e. Shift, Alt, Ctrl, ...), if so the current shift state (a value coding which shift keys are currently held down) is altered accordingly and the handler exits. Note that the `lock' keys are not recognised by the IRQ handler.

If the key code is not a hot-key and not a shift-key the IRQ handler simply finds the logical keyboard which currently has the keyboard focus and calls its use_key function with the key code and whether or not it was pressed or released. It is then up to the logical keyboard driver to do what it wants with the key code.

Raw Keyboards

The keyboard driver allows multiple logical keyboards to exist at once, at any moment one of these keyboards has the keyboard focus, that is, only it will receive the key codes typed at the keyboard. Each logical keyboard is represented by an instance of the following structure:

struct kbd {
    /* This function is called each time a key code is
       typed and KBD is in focus. */
    void (*use_key)(struct kbd *kbd, int shift_state,
                    u_char key_code, u_char up_code);

    /* Called when the keyboard focus is taken from KBD. */
    void (*switch_from)(struct kbd *kbd, int shift_state);

    /* Called when KBD is given the keyboard focus. Normally
       it has to set the keyboard leds to reflect it's current
       lock state. */
    void (*switch_to)(struct kbd *kbd, int shift_state);

    /* TRUE when this keyboard is in focus. */
    bool in_focus;
};

As you can see it is basically just a set of three functions which get called when the keyboard driver needs to. To initialise one of these use the function init_kbd,

kbd Function: void init_kbd (struct kbd *kbd)

This function initialises the logical keyboard structure kbd so that each function vector is undefined and the keyboard is out of focus.

Although it seems as though this function is pointless it should still be called to initialise every logical keyboard which is used.

After being initialised by init_kbd the function vectors should be set to point at suitable functions. The following three function descriptions document what these functions are supposed to do and how they are called:

Function: void use_key (struct kbd *kbd, int shift_state, u_char key_code, u_char up_code)

This function is called whenever a key code is received from the keyboard hardware and the logical keyboard kbd is in focus. The actual key code is defined by the key_code parameter, if it was pressed up_code will be zero, otherwise it was released.

The shift_state parameter is a bit mask defining which of the various shift keys are currently held down, the following bit-values are defined in `<vmm/kbd.h>':

S_LEFT_SHIFT
The left shift key.

S_RIGHT_SHIFT
The right shift key.

S_SHIFT
Either of the shift keys.

S_LEFT_CTRL
The left control key.

S_RIGHT_CTRL
The right control key.

S_CTRL
Either of the control keys.

S_LEFT_ALT
The left alt key.

S_RIGHT_ALT
The right alt key.

S_ALT
Either of the alt keys.

Note that since this function is called by keyboard IRQ handler this function is always called in the context of the `irq-dispatcher' task.

Also note that this function should handle the lock keys if it wants to, the keyboard IRQ handler totally ignores them.

Function: void switch_from (struct kbd *kbd, int shift_state)

This function is called when the keyboard focus is taken from the logical keyboard kbd. The parameter shift_state defines which of shift keys are currently held down.

Function: void switch_to (struct kbd *kbd, int shift_state)

This function is called when the logical keyboard kbd receives the keyboard focus. The parameter shift_state defines which of shift keys are currently held down.

To switch the keyboard focus between logical keyboards the keyboard module provides the following function.

kbd Function: void switch_focus (struct kbd *new)

This function gives the keyboard focus to the logical keyboard represented by the structure new. This means that the switch_from function of the logical keyboard currently in-focus is called, then the switch_to function of new is called.

The keyboard driver also allows total control of the keyboard LEDs, via the following function.

kbd Function: bool set_leds (u_char led_state)

This function sets the keyboard LEDs to the state defined by the parameter led_state, a bit mask with a bit per LED. The bit values KB_LED_SCROLL_LOCK, KB_LED_NUM_LOCK and KB_LED_CAPS_LOCK are defined in the keyboard header file.

If this function succeeds in setting the LEDs it returns TRUE, otherwise FALSE.

Cooked Keyboards

As well as the `raw' logical keyboards described in the previous section the keyboard driver provides a more comfortable interface to the characters typed at the keyboard. Cooked keyboards are an extended type of logical keyboard, they use the use_key function in a logical keyboard to call a function to `cook' the key code into an ASCII string.

The struct cooked_kbd is the structure used to represent each cooked keyboard, the fields which may be accessed by users are defined thus:

struct cooked_kbd {
    /* The logical keyboard we derive this structure from. */
    struct kbd kbd;

    /* The current state of the `lock' keys. */
    int lock_state;

    /* Private data follows... */

The lock_state field is a bit mask defining which of the lock keys are active, the bit values it may contain are:

L_CAPS_LOCK
The caps-lock key.

L_NUM_LOCK
The num-lock key.

L_SCROLL_LOCK
The scroll-lock key.

Note that these have the same values as their KB_LED_ counterparts.

kbd Function: void init_cooked (struct cooked_kbd kbd)

This function initialises a new cooked keyboard, kbd.

There are two ways in which the owner of a cooked keyboard may receive the ASCII characters generated, either immediately through a function callback (much like the use_key idea of a raw keyboard), or alternatively they may call a function which suspends the task until keyboard input arrives, when they are made runnable and a character is returned.

kbd Function: void cooked_set_function (struct cooked_kbd *kbd, void (*func)(u_char c))

This function sets the cooked keyboard kbd to send all keyboard input to the function func as soon as it arrives. The parameter func is a function taking one argument, c, which is the ASCII character received.

kbd Function: void cooked_set_task (struct cooked_kbd *kbd)

This function makes the cooked keyboard store all characters it receives in an internal buffer until the cooked_read_char function is called to pop one character out of the buffer.

kbd Function: u_char cooked_read_char (struct cooked_kbd *kbd)

This function reads one character from the cooked keyboard kbd (which must have had the cooked_set_task function called on it) and returns it.

If no characters are currently available from kbd the current task is suspended until the next character arrives.

The keyboard driver also provides a function to explicitly cook key codes, this may be useful in some cases.

kbd Function: char * cook_keycode (u_char key_code, int shift_state, int lock_state)

This function translates the key code key_code into an ASCII string which is returned. If the function is unable to make a translation a null pointer is returned.

The parameters shift_state and lock_state define which shift and lock keys are currently active, they are defined in the usual way.

Note that by default the keyboard driver uses a keymap suited to Great Britain. Also provided is a US keymap but one line of the file `drivers/physical/kbd/cooked.c' will need to be changed to include it instead of the GB keymap. Currently there is no way to dynamically load keymaps into the driver.

TTY Driver

The tty device driver does not actually control any hardware device like most device drivers. Instead it provides a unified interface to the video and keyboard drivers, allowing tasks to open ttys (logical consoles). A tty is like a character terminal -- ASCII characters are received from the logical keyboard (see section Keyboard Driver) and programs may output characters to the related virtual display (see section Video Drivers).

At its simplest the tty driver can be seen as combining a cooked keyboard and a display (together with functions for character output) into a single entity -- a console.

All functions and variables documented in this section of the manual are members of the `tty' module, defined in the header file `<vmm/tty.h>'.

Opening And Closing TTYs

A tty may have one of three types of keyboard: raw, cooked or virtual. Normally a cooked keyboard will be chosen to allow ASCII characters to be received, although virtual machines will use virtual keyboards. The type of keyboard which a tty has is coded by the following enum:

enum tty_kbd_type {
    Raw, Cooked, Virtual
};

Each tty is represented by an instance of the following structure:

struct tty {
    list_node_t node;
    struct task *owner;
    enum tty_kbd_type kbd_type;
    union {
        struct kbd raw;
        struct cooked_kbd cooked;
        struct vkbd virtual;
    } kbd;
    /* Points to the `struct kbd' of whichever type
       of keyboard is used. */
    struct kbd *base_kbd;
    struct video video;
    /* The current display page. */
    u_char current_page;
    /* The current cursor position. */
    short x, y;

    /* The following two fields are used by the
       readline() function to maintain the tty's
       input history. */
    list_t rl_history;
    int rl_history_size;
};

Most of the fields are fairly self-explanatory; none of them should be modified except by the tty module itself. These structures should not be declared statically, the function open_tty allocates a piece of memory for each tty opened.

tty Function: struct tty * open_tty (struct task *owner, enum tty_kbd_type kbd, const char *video_type, u_long video_flags)

This function allocates a new tty structure and initialises it according to the function parameters, then returns a pointer to the new structure.

The parameter task defines the task which `owns' this tty, it does not have to be the current task. kbd defines the type of logical keyboard to use for this tty. video_type is a string naming one of the video virtualisations provided by the video module, video_flags is passed unchanged to the init_video function of the video module.

All ttys are created in the foreground displaying their first page.

If this function fails for some reason it will return a null pointer.

tty Function: void close_tty (struct tty *tty)

This function releases all resources associated with the tty represented by tty then deallocates the tty structure itself.

If tty is the foreground tty when it is killed the tty immediately below it will inherit the foreground.

The Foreground TTY

The foreground tty is the tty which is currently being display as well as having the keyboard focus. The tty driver maintains a list of all the ttys in the system with the foreground tty as the first element; the second element is the tty immediately under the foreground tty and so on down the list. As the foreground tty and the stacking order of the ttys changes the list of ttys is updated to reflect the changes.

tty Variable: struct tty * current_tty

This member of the video module structure stores a pointer to the foreground tty.

tty Function: void to_front (struct tty *tty)

This function brings the tty represented by tty to the foreground.

tty Function: void next_tty (void)

This function rotates the list of ttys (and hence the foreground tty) down through the list (i.e. the new foreground tty is the tty that was immediately under the foreground tty).

tty Function: void prev_tty (void)

This function does the opposite of the next_tty function, that is: bring the bottom-most tty to the foreground and push the rest down.

TTY Output

All functions which perform output to a tty follow a set of standard rules:

tty Function: void set_cursor (struct tty *tty, short x, short y)

This function sets the cursor's position to be at column x and row y of the current display page in the tty represented by tty.

tty Function: void read_cursor (struct tty *tty)

This function resets the tty's notion of its cursor position to the actual location of the cursor in the display.

This function is only useful if tty output is being combined with access to the virtual display.

tty Function: void printn (struct tty *tty, const u_char *buf, size_t length)

This is the function to output an ASCII string to the display of the tty represented by tty. All other tty character output functions call this function to accomplish their task.

buf is a pointer to the string to print, exactly length characters from the string will be output.

tty Function: void print (struct tty *tty, const u_char *buf)

This function prints the zero-terminated string buf to the tty tty.

tty Function: void vprintf (struct tty *tty, const u_char *fmt, va_list args)

This function performs a standard formatted output (see section Formatted Output) to the tty tty using the format specification string fmt and the list of parameters args.

tty Function: void printf (struct tty *tty, const u_char *fmt, ...)

This function uses the vprintf function to do a formatted output to the tty tty.

tty Function: void clear (struct tty *tty)

This function clears all of the display belonging to tty and sets its cursor position to the top-left corner.

tty Function: void clear_chars (struct tty *tty, size_t length)

Clear the length number of characters following the cursor in the tty device tty. The cursor is left where it is.

tty Function: void scroll_up (struct tty *tty)

This function scrolls the contents of the display belonging to tty up one line and then clears the exposed bottom line.

tty Function: void set_page (struct tty *tty, u_int pageno)

This function sets the display page being used by tty to the one numbered pageno.

tty Function: void beep (struct tty *tty)

This function sounds a bell for the tty tty.

tty Function: void speaker_on (struct tty *tty, u_int freq)

Turn on the system's speaker, it will be set to oscillate at a frequency of freq hertz. The speaker is associated with the tty device tty, it will be brought to the foreground to show the user which console is beeping.

tty Function: void speaker_off (struct tty *tty)

Disable the speaker, this should only be called after using the function speaker_on to enable it.

TTY Input

When a tty has been created with a cooked keyboard as its input device the tty module provides a couple of functions to make reading input from the tty easier. If a tty does not use a cooked keyboard the functions described in this section will not work.

tty Function: int get_char (struct tty *tty)

This function returns the next character available from the tty represented by tty. If no characters are in the buffer the current task is suspended until a character is received.

If an error occurs the value -1 is returned.

tty Function: char * readline (struct tty *tty, size_t *lengthp)

This function reads a single line of input from the tty device tty. A buffer to contain the typed characters is allocated using the kernel malloc function and a pointer to this buffer is returned, it contains characters typed followed by a zero terminator. The value at location lengthp is set to the number of characters actually entered. After using the contents of the buffer, the caller should use free to deallocate it.

If an error occurs, or the user types an end-of-line character at the start of the line a null pointer is returned.

Note that although a zero terminator is appended to the entered line it is possible that the line contains null characters in its body. Use the contents of the lengthp parameter to find the end of the line if this may be a problem.

Simple line editing and history functionality is provided, in general these will be bound to the same keys as the equivalent functions in the Emacs editor. The following table lists the available commands, where the variable arg is mentioned it refers to the prefix argument entered by the metafied numeric keys.

C-f
Right
Move forward arg characters.

C-b
Left
Move backward arg characters.

C-a
Move to the beginning of the line.

C-e
Move to the end of the line.

C-d
Either delete the arg characters starting with the one under the cursor or if the cursor is at the end of the line accept the current contents of the buffer (an end-of-line character).

Backspace
Delete the arg characters preceding the cursor.

RET
Insert a newline character at the end of the line and accept the contents of the buffer.

C-l
Redisplay the buffer.

C-SPC
Set the value of the mark to the current cursor position.

C-x C-x
Exchange the cursor position with that of the mark.

C-n
Down
Move forward arg lines in the history list.

C-p
Up
Move back arg lines in the history list.

M-<
Display the first line in the history list.

M->
Display the last line in the history list.

M-0
M-1
M-2
M-3
M-4
M-5
M-6
M-7
M-8
M-9
Append the number typed to the end of the (decimal) prefix argument for the following command. For example typing M-1 M-0 C-f moves the cursor ten characters forwards.

M--
(That's Meta-minus.) Negate the value of the prefix argument for the next command. For example typing M-- M-4 C-f moves backwards four characters.

The notation C-x and M-x means the x key with the Control modifier and the Meta qualifier respectively. To generate a M-x event either type Alt-x or ESC x.

See section Dynamic Memory Allocation.

Go to the previous, next section.