The Möbius Operating System: Driver Book
HOME DOWNLOAD DOCUMENTATION SCREENSHOTS  

Devices

Back: Initialization | Next: File System Drivers

Devices are created through the add_device function of the driver object:

void SampleAddDevice(driver_t* drv, const wchar_t* name, dev_config_t* cfg)
{
}

The driver of the bus to which your device is attached is responsible for calling add_device via DevInstallDevice in the kernel. The bus driver will usually be either the PCI or ISA drivers, although bus drivers include drivers for things like ATA controllers and serial port enumerators.

  • drv: this is the same pointer that was passed to DrvInit
  • name: the bus driver's idea of what your device should be called
  • cfg: contains all the information the bus driver knows about your device

The dev_config_t structure looks like this:

struct dev_config_t
{
    device_t *bus;		/* pointer to the bus device */
    unsigned bus_type;		/* type of bus */
    unsigned num_resources;	/* number of elements in resources array */
    dev_resource_t *resources;	/* resources array */
    wchar_t *profile_key;	/* name of the key where device should read its configuration */
    uint16_t device_class;	/* PCI-style device class */
    void *businfo;		/* pointer to bus driver's information structure */
    union			/* device location, encoded as... */
    {
	void *ptr;		/* ...a pointer, or */
	uint32_t number;	/* ...an integer */
    } location;
};
  • bus: the bus device which recognised your device (buses are devices too)
  • bus_type: the bus's type. The following bus types are pre-defined:
    • DEV_BUS_UNKNOWN: an unknown type of bus. This type should be avoided.
    • DEV_BUS_ISA: the ISA bus. Includes Plug and Play ISA.
    • DEV_BUS_PCI: the PCI bus
    • DEV_BUS_ATA: an ATA controller. Contains ATA drives.
    • DEV_BUS_ATA_DRIVE: an ATA drive. Contains volumes (partitions).
    • DEV_BUS_SERIAL: all serial ports. Contains devices such as serial mice, modems or serial ports.
  • num_resources: the number of elements in the resources array
  • resources: an array of resources which the bus driver thinks your device might own
  • profile_key: the full name of the key in system.pro where your driver should look for its configuration. There is generally one set of keys for each bus type, containing device types.
  • device_class: the device's class. Devices within a class get standard names under /System/Devices/Classes. See /coreos/kernel/device.c for a list of classes supported by Möbius.
  • businfo: a structure defined by the bus driver containing extra information the driver knows about your device. All buses with a common bus_type share the same data structure here. Some examples are:
    struct pci_businfo_t	    /* bus info for PCI devices */
    {
        uint16_t vendor_id;
        uint16_t device_id;
    
        uint16_t command;
        uint16_t status;
    
        uint8_t revision_id;
        uint8_t interface;
        uint8_t sub_class;
        uint8_t base_class;
    
        uint8_t cache_line_size;
        uint8_t latency_timer;
        uint8_t header_type;
        uint8_t bist;
    
        uint8_t irq;
        uint32_t base[6];
        uint32_t size[6];
    
        uint16_t subsys_vendor;
        uint16_t subsys;
    };
    
    struct isa_businfo_t	/* bus info for ISAPnp devices */
    {
        uint16_t vendor_id;
        uint16_t device_id;
        wchar_t name[8];
    };
  • location: an identifier defined by the bus driver containing the location of your device. This is represented as either a pointer or a 32-bit integer depending on the bus type. For PCI devices, this is a pci_location_t structure cast to a 32-bit integer:
    struct pci_location_t
    {
        uint8_t bus;
        uint8_t dev;
        uint8_t func;
    };
    For ISAPnP devices, this is the index of the device on the bus.

Your add_device routine should work out whether it supports the device asked for, then create a device object for it by calling the kernel's DevAddDevice function:

device_t *DevAddDevice(driver_t *drv, 
                       const device_vtbl_t *vtbl, 
                       uint32_t flags, 
                       const wchar_t *name, 
                       dev_config_t *cfg, 
                       void *cookie);
  • drv: pointer to the driver, passed to your add_device routine
  • vtbl: pointer to the virtual function table to use for the device. You need to declare one of these: you should only need one for each type of device your driver supports.
  • flags: flags for the device. There are currently no flags defined, so pass zero.
  • name: name of the device. You should probably use the name passed to add_device.
  • cfg: device configuration. You should probably use the configurtation passed to add_device, although you can modify it if you want. For instance, drivers for ISA devices will need to set the device_class field if their devices are to show up under /System/Devices/Classes.
  • cookie: an opaque pointer which will be passed back to your driver when the kernel calls it in future. You can store whatever you like inside the cookie.

The cookie you pass to DevAddDevice is stored in the cookie field of the device object. The same device object pointer will be passed to each virtual function called by the kernel, so don't worry about storing it.

When you call DevAddDevice, your device is entered into the device manager's namespace (and into the /System/Devices and /System/Devices/Classes directories) and is available for use by applications.

The pointer returned by DevAddDevice can now be passed to DevRegisterIrq if your device handles any interrupts:

bool DevRegisterIrq(uint8_t irq, device_t *dev);

Handling requests

The device_vtbl_t structure mentioned above looks like this:

struct device_vtbl_t
{
    void (*delete_device)(device_t *dev);
    status_t (*request)(device_t *dev, request_t *req);
    bool (*isr)(device_t *dev, uint8_t irq);
    void (*finishio)(device_t *dev, request_t *req);
    bool (*cancelio)(device_t *dev, asyncio_t *io);
    status_t (*claim_resources)(device_t *dev,
	device_t *child,
	dev_resource_t *resources, 
	unsigned num_resources,
	bool is_claiming);
    status_t (*remove_child)(device_t *dev,
	device_t *child);
};

It is a structure containing only function pointers. When the kernel needs your device to do something, it calls the appropriate function in the virtual function table. Any of the pointers can be NULL except request.

The functions are:

  • delete_device: your device is being deleted. You should shut down the hardware and free any memory your driver allocated associated with the device, including the cookie if necessary.
  • request: the main I/O entry point into your driver
  • isr: an interrupt handled by the device has been triggered
  • finishio: I/O requested by your driver has completed
  • cancelio: an I/O request queued to your device should be cancelled. For example, the thread that requested the I/O is exiting.
  • claim_resources: (bus drivers) a device on the bus is requesting or releasing some resources
  • remove_child: (bus drivers) a device on the bus is being deleted

request

Your device will receive I/O requests through its request function. This is the core of the device's code and it is responsible for dispatching requests issued via IoRequest (and, by extension, IoReadSync, FsWrite and the others). Requests are represented by the request_t structure passed to the request function. This is identical to the pointer passed to IoRequest (the request function runs in the same context as the originator) and it is really only a pointer to the header of a larger structure. The various types of requests can be distinguished by the request_t::code field; each request code has different information ("parameters") stored after the request header.

The two main codes you'll see are DEV_READ and DEV_WRITE. Not surprisingly, these are invoked when something needs to read from or write to your device; when invoked from user mode, these come via FsRead and FsWrite. Both DEV_READ and DEV_WRITE have similar parameters: the request_dev_t structure defines both the parameters and the header, and request_dev_t::params::buffered can be used for each. You are provided with three pieces of information: a buffer, the length of the buffer, and an offset:

union params_dev_t
    {
    struct
    {
	uint32_t length;	/* length of the request in bytes */
	page_array_t *pages;	/* page array representing the caller's buffer */
	uint64_t offset;	/* offset within the device, in bytes */
    } buffered;

    /* ...more fields here... */
};
  • The buffer represents the address passed to FsRead or FsWrite and could be in user or kernel space. It is a page array, i.e. a structure representing an array of physical page addresses, and must be mapped into kernel memory before it can be used. Physical pages within a page array are locked in place, so they can be given to the hardware without fear that they will be freed before the request has finished.
  • The extent of the buffer is defined by the length. Do not write anywhere outside the valid extent of the buffer.
  • The offset is used for block devices (such as hard drives), and can be ignored for character devices (such as mice). It is specified as a 64-bit number relative to the start of the device.
Note: If your device can't handle the length or offset given (for example, disk drivers generally read in 512-byte blocks on boundaries of 512 bytes), feel free to set request_t::result appropriately and return false.

You've got all the parameters you need now. If the operation is going to be quick then you can handle it there and then and copy the results into the buffer. If you don't read as many bytes as requested, update the length field before returning. If the operation fails you need to return an appropriate error code from the request function; for success you just need to return zero.

If the operation is going to take some time (any interrupt-driven I/O falls into this category) then you need to use asynchronous I/O. Each device has an internal queue of asynchronous requests, which the device manager maintains. In the case of async I/O, all the request function does is validate the parameters and queue the request using DevQueueRequest:

asyncio_t* DevQueueRequest(device_t *dev, 
                           request_t *req, 
                           size_t size, 
                           page_array_t *pages, 
                           size_t user_buffer_length);

If the hardware device is idle the driver needs to start processing the request it just queued, usually in a common startio routine. Here the driver needs to get the hardware to start the I/O operation, with the expectation that it will trigger a hardware interrupt when it has finished. Once the hardware has started, but before it finishes, the request function should return SIOPENDING.

Note: Although most of the asyncio_t structure is opaque, you can modify the length and extra fields as your request is processed. A lot of drivers record the number of bytes transferred in the length field, and the extra field can point to anything in kernel memory.

Now that the request has been queued, and your request function has returned, the kernel is free to keep on scheduling other threads; if it wants, the originator (which might be a user process, a file system driver or something else) can keep on working until your driver has finished.

isr

When your hardware triggers an interrupt your isr function will be called (assuming you called DevRegisterIrq in add_device). Remember that it will be called in an arbitrary context: don't modify any user memory and don't take too long. Here you need to check which request this interrupt applies to (if you're handling requests sequentially it will always be the first one, at device_t::io_first) and retrieve the results from the hardware. You'll probably need to access the request's user buffer at this point. You can call MemMapPageArray to access that buffer, which maps the a page array into a temporary region of kernel address space. Call MemUnmapPageArray when you're finished with the memory.

If this request has finished (it might be finished after one IRQ, or the device might give you several IRQs before the operation is finished, as floppy drives do) then you need to call DevFinishIo. This signals the originator's finishio routine, if the originator was a device (e.g. the devfs file system that maintains the /System/Devices directory). This allows nested hierarchies of devices to be created: the originator is able to go off and continue its own request, which will notify the next higher originator, and so on (until user mode is reached).

Back: Initialization | Next: File System Drivers

Post a comment

From: