Peripherals are any of the many external devices that connect to your computer. Obviously, the processor must have some way of talking to the peripherals to make them useful.
The communication channel between the processor and the peripherals is called a bus.
A device requires both input and output to be useful. There are a number of common concepts required for useful communication with peripherals.
An interrupt allows the device to literally interrupt the processor to flag some information. For example, when a key is pressed, an interrupt is generated to deliver the key-press event to the operating system. Each device is assigned an interrupt by some combination of the operating system and BIOS.
Devices are generally connected to an programmable interrupt controller (PIC), a separate chip that is part of the motherboard which buffers and communicates interrupt information to the main processor. Each device has a physical interrupt line between it an one of the PIC's provided by the system. When the device wants to interrupt, it will modify the voltage on this line.
A very broad description of the PIC's role is that it receives this interrupt and converts it to a message for consumption by the main processor. While the exact procedure varies by architecture, the general principle is that the operating system has configured an interrupt descriptor table which pairs each of the possible interrupts with a code address to jump to when the interrupt is received. This is illustrated in Figure 3.6, “Overview of handling an interrupt”.
Writing this interrupt handler is the job of the device driver author in conjunction with the operating system.
Most drivers will spilt up handling of interrupts into bottom and top halves. The bottom half will acknowledge the interrupt, queue actions for processing and return the processor to what it was doing quickly. The top half will then run later when the CPU is free and do the more intensive processing. This is to stop an interrupt hogging the entire CPU.
Since an interrupt can happen at any time, it is important that you can return to the running operation when finished handling the interrupt. It is generally the job of the operating system to ensure that upon entry to the interrupt handler, it saves any state; i.e. registers, and restores them when returning from the interrupt handler. In this way, apart from some lost time, the interrupt is completely transparent to whatever happens to be running at the time.
While an interrupt is generally associated with an external event from a physical device, the same mechanism is useful for handling internal system operations. For example, if the processor detects conditions such as an access to invalid memory, an attempt to divide-by-zero or an invalid instruction, it can internally raise an exception to be handled by the operating system. It is also the mechanism used to trap into the operating system for system calls, as discussed in the section called “System Calls” and to implement virtual memory, as discussed in Chapter 6, Virtual Memory. Although generated internally rather than from an external source, the principles of asynchronously interrupting the running code remains the same.
There are two main ways of signalling interrupts on a line — level and edge triggered.
Level-triggered interrupts define voltage of the interrupt line being held high to indicate an interrupt is pending. Edge-triggered interrupts detect transitions on the bus; that is when the line voltage goes from low to high. With an edge-triggered interrupt, a square-wave pulse is detected by the PIC as signalling and interrupt has been raised.
The difference is pronounced when devices share an interrupt line. In a level-triggered system, the interrupt line will be high until all devices that have raised an interrupt have been processed and un-asserted their interrupt.
In an edge-triggered system, a pulse on the line will indicate to the PIC that an interrupt has occurred, which it will signal to the operating system for handling. However, if further pulses come in on the already asserted line from another device.
The issue with level-triggered interrupts is that it may require some considerable amount of time to handle an interrupt for a device. During this time, the interrupt line remains high and it is not possible to determine if any other device has raised an interrupt on the line. This means there can be considerable unpredictable latency in servicing interrupts.
With edge-triggered interrupts, a long-running interrupt can be noticed and queued, but other devices sharing the line can still transition (and hence raise interrupts) while this happens. However, this introduces new problems; if two devices interrupt at the same time it may be possible to miss one of the interrupts, or environmental or other interference may create a spurious interrupt which should be ignored.
It is important for the system to be able to mask or prevent interrupts at certain times. Generally, it is possible to put interrupts on hold, but a particular class of interrupts, called non-maskable interrupts (NMI), are the exception to this rule. The typical example is the reset interrupt.
NMIs can be useful for implementing things such as a system watchdog, where a NMI is raised periodically and sets some flag that must be acknowledged by the operating system. If the acknowledgement is not seen before the next periodic NMI, then system can be considered to be not making forward progress. Another common usage is for profiling a system. A periodic NMI can be raised and used to evaluate what code the processor is currently running; over time this builds a profile of what code is being run and create a very useful insight into system performance.
Obviously the processor will need to communicate with the peripheral device, and it does this via IO operations. The most common form of IO is so called memory mapped IO where registers on the device are mapped into memory.
This means that to communicate with the device, you need simply read or write to a specific address in memory. TODO: expand
Since the speed of devices is far below the speed of processors, there needs to be some way to avoid making the CPU wait around for data from devices.
Direct Memory Access (DMA) is a method of transferring data directly between an peripheral and system RAM.
The driver can setup a device to do a DMA transfer by giving it the area of RAM to put it's data into. It can then start the DMA transfer and allow the CPU to continue with other tasks.
Once the device is finished, it will raise an interrupt and signal to the driver the transfer is complete. From this time the data from the device (say a file from a disk, or frames from a video capture card) is in memory and ready to be used.
Other buses connect between the PCI bus and external devices.
From an operating system point of view, a USB device is a group of end-points grouped together into an interface. An end-point can be either in or out and hence transfers data in one direction only. End-points can have a number of different types:
Control end-points are for configuring the device, etc.
Interrupt end-points are for transferring small amounts of data. They have higher priority than ...
Bulk end-points, which transfer large amounts of data but do not get guaranteed time constraints.
Isochronous transfers are high-priority real-time transfers, but if they are missed they are not re-tried. This is for streaming data like video or audio where there is no point sending data again.
There can be many interfaces (made of multiple end-points) and interfaces are grouped into configurations. However Most devices only have a single configuration.
Figure 3.7, “Overview of a UHCI controller operation” shows an overview of a universal host controller interface, or UHCI. It provides an overview of how USB data is moved out of the system by a combination of hardware and software. Essentially, the software sets up a template of data in a specified format for the host controller to read and send across the USB bus.
Starting at the top-left of the overview, the controller has a frame register with a counter which is incremented periodically — every millisecond. This value is used to index into a frame list created by software. Each entry in this table points to a queue of transfer descriptors. Software sets up this data in memory, and it is read by the host controller which is a separate chip the drives the USB bus. Software needs to schedule the work queues so that 90% of a frame time is given to isochronous data, and 10% left for interrupt, control and bulk data..
As you can see from the diagram, the way the data is linked means that transfer descriptors for isochronous data are associated with only one particular frame pointer — in other words only one particular time period — and after that will be discarded. However, the interrupt, control and bulk data are all queued after the isochronous data and thus if not transmitted in one frame (time period) will be done in the next.
The USB layer communicates through USB request blocks, or URBs. A URB contains information about what end-point this request relates to, data, any related information or attributes and a call-back function to be called when the URB is complete. USB drivers submit URBs in a fixed format to the USB core, which manages them in co-ordination with the USB host controller as above. Your data gets sent off to the USB device by the USB core, and when its done your call-back is triggered.