Depending upon the design of the processor and board, peripheral devices are located either in the processor's memory space or within the I/O space. In fact, it is common for embedded systems to include some peripherals of each type. These are called memory-mapped and i/o-mapped peripherals, respectively. Of the two types, memory-mapped peripherals are generally easier to work with and are increasingly popular.
Memory-mapped control and status registers can be made to look just like ordinary variables. to do this, you need simply declare a pointer to the register, or block of registers, and set the value of the pointer explicitly. For example, if the P2LTCH register from Chapter 2, were memory-mapped and located at physical address 7205Eh, we could have implemented toggleLed entirely in C, as shown below. A pointer to an unsigned short — a 16-bit register — is declared and explicitly initialized to the address 0x7200:005E. From that point on, the pointer to the register looks just like a pointer to any other integer variable:
unsigned short * pP2LTCH = (unsigned short *) 0x7200005E;
void toggleLed(void) {
*pP2LTCH ^= LED_GREEN; /* Read, xor, and modify. */
} /* toggleLed() */
Note, however, that there is one very important difference between device registers and ordinary variables. The contents of a device register can change without the knowledge or intervention of your program. That's because the register contents can also be modified by the peripheral hardware. By contrast, the contents of a variable will not change unless your program modifies them explicitly. For that reason, we say that the contents of a device register are volatile, or subject to change without notice.
The C/C++ keyword volatile should be used when declaring pointers to device registers. this warns the compiler not to make any assumptions about the data stored at that address. For example, if the compiler sees a write to the volatile location followed by another write to that same location, it will not assume that the first write is an unnecessary use of processor time. In other words, the keyword volatile instructs the optimization phase of the compiler to treat that variable as though its behavior cannot be predicted at compile time.
Here's an example of the use of volatile to warn the compiler about the P2LTCH register in the previous code listing:
volatile unsigned short * pP2LTCH = (unsigned short *) 0x7200005E;
It would be wrong to interpret this statement to mean that the pointer itself is volatile. In fact, the value of the variable pP2LTCH will remain 0x7200005E for the duration of the program (unless it is changed somewhere else, of course). Rather, it is the data pointed to that is subject to change without notice. This is a very subtle point, and it is easy to confuse yourself by thinking about it too much. Just remember that the location of a register is fixed, though its contents might not be. And if you use the volatile keyword, the compiler will assume the same.
The primary disadvantage of the other type of device registers, I/O-mapped registers, is that there is no standard way to access them from C or C++. Such registers are accessible only with the help of special machine-language instructions. And these processor-specific instructions are not supported by the C or C++ language standards. So it is necessary to use special library routines or inline assembly (as we did in Chapter 2) to read and write the registers of an I/O-mapped device.
7.2 The Device Driver Philosophy
When it comes to designing device drivers, you should always focus on one easily stated goal: hide the hardware completely. When you're finished, you want the device driver module to be the only piece of software in the entire system that reads or writes that particular device's control and status registers directly. In addition, if the device generates any interrupts, the interrupt service routine that responds to them should be an integral part of the device driver. In this section, I'll explain why I recommend this philosophy and how it can be achieved.
Of course, attempts to hide the hardware completely are difficult. any programming interface you select will reflect the broad features of the device. That's to be expected. The goal should be to create a programming interface that would not need to be changed if the underlying peripheral were replaced with another in its general class. For example, all Flash memory devices share the concepts of sectors (though the sector size can differ between chips). An erase operation can be performed only on an entire sector, and once erased, individual bytes or words can be rewritten. So the programming interface provided by the Flash driver example in the last chapter should work with any Flash memory device. The specific features of the AMD 29F010 are hidden from that level, as desired.
Device drivers for embedded systems are quite different from their workstation counterparts. In a modern computer workstation, device drivers are most often concerned with satisfying the requirements of the operating system. For example, workstation operating systems generally impose strict requirements on the software interface between themselves and a network card. The device driver for a particular network card must conform to this software interface, regardless of the features and capabilities of the underlying hardware. Application programs that want to use the network card are forced to use the networking API provided by the operating system and don't have direct access to the card itself. In this case, the goal of hiding the hardware completely is easily met.
By contrast, the application software in an embedded system can easily access your hardware. In fact, because all of the software is linked together into a single binary image, there is rarely even a distinction made between application software, operating system, and device drivers. The drawing of these lines and the enforcement of hardware access restrictions are purely the responsibilities of the software developers. Both are design decisions that the developers must consciously make. In other words, the implementers of embedded software can more easily cheat on the software design than their non-embedded peers.
The benefits of good device driver design are threefold. First, because of the modularization, the structure of the overall software is easier to understand. Second, because there is only one module that ever interacts directly with the peripheral's registers, the state of the hardware can be more accurately tracked. And, last but not least, software changes that result from hardware changes are localized to the device driver. Each of these benefits can and will help to reduce the total number of bugs in your embedded software. But you have to be willing to put in a bit of extra effort at design time in order to realize such savings.
If you agree with the philosophy of hiding all hardware specifics and interactions within the device driver, it will usually consist of the five components in the following list. To make driver implementation as simple and incremental as possible, these elements should be developed in the order in which they are presented.
1. A data structure that overlays the memory-mapped control and status registers of the device
The first step in the driver development process is to create a C-style struct that looks just like the memory-mapped registers of your device. This usually involves studying the data book for the peripheral and creating a table of the control and status registers and their offsets. Then, beginning with the register at the lowest offset, start filling out the struct . (If one or more locations are unused or reserved, be sure to place dummy variables there to fill in the additional space.)
Читать дальше