Peripherals 7.1 Control and Status Registers 7.2 The Device Driver Philosophy 7.3 A Simple Timer Driver 7.4 Das Blinkenlights, Revisited Each pizza glides into a slot like a
Trang 1flashBase[UNLOCK2_OFFSET] = FLASH_CMD_UNLOCK2;
flashBase[COMMAND_OFFSET] = FLASH_CMD_BYTE_PROGRAM; /*
* Perform the actual write operation
*/
baseAddress[offset] = data[offset];
/*
* Wait for the operation to complete or time-out
*/
while (((baseAddress[offset] & DQ7) != (data[offset] & DQ7)) &&
!(baseAddress[offset] & DQ5));
if ((baseAddress[offset] & DQ7) != (data[offset] & DQ7))
{
break;
}
}
return (offset);
} /* flashWrite() */
/******************************************************************
****
*
* Function: flashErase()
*
* Description: Erase a block of the Flash memory device
*
* Notes: This function is specific to the AMD 29F010 Flash
* memory In this device, individual sectors may be
* hardware protected If this algorithm encounters
* a protected sector, the erase operation will fail
* without notice
*
* Returns: O on success
* Otherwise -1 indicates failure
*
******************************************************************
****/
int
flashErase(unsigned char * sectorAddress)
{
unsigned char * flashBase = FLASH_BASE;
Trang 2/*
* Issue the command sequence for sector erase
*/
flashBase[UNLOCK1_OFFSET] = FLASH_CMD_UNLOCK1;
flashBase[UNLOCK2_OFFSET] = FLASH_CMD_UNLOCK2;
flashBase[COMMAND_OFFSET] = FLASH_CMD_ERASE_SETUP;
flashBase[UNLOCK1_OFFSET] = FLASH_CMD_UNLOCK1;
flashBase[UNLOCK2_OFFSET] = FLASH_CMD_UNLOCK2;
*sectorAddress = FLASH_CMD_SECTOR_ERASE;
/*
* Wait for the operation to complete or time-out
*/
while (!(*sectorAddress & DQ7) && !(*sectorAddress & DQ5));
if (!(*sectorAddress & DQ7))
{
return (-1);
}
return (0);
} /* flashErase() */
Of course, this is just one possible way to interface to a Flash memory—and not a particularly advanced one at that In particular, this implementation does not
handle any of the chip's possible errors What if the erase operation never
completes? The function flashErase will just keep spinning its wheels, waiting for
that to occur A more robust implementation would use a software time-out as a backup For example, if the Flash device doesn't respond within twice the
maximum expected time (as stated in the databook), the routine could stop polling and indicate the error to the caller (or user) in some way
Another thing that people sometimes do with Flash memory is to implement a small filesystem Because the Flash memory provides nonvolatile storage that is also rewriteable, it can be thought of as similar to any other secondary storage system, such as a hard drive In the filesystem case, the functions provided by the
driver would be more file-oriented Standard filesystem functions like open, close, read, and write provide a good starting point for the driver's programming
interface The underlying filesystem structure can be as simple or complex as your system requires However, a well-understood format like the File Allocation Table (FAT) structure used by DOS is good enough for most embedded projects
[1] 128 kilobytes is one-eighth of the total 1-megabyte address space
Trang 3[2] The divisor is simply a binary representation of the coefficients of the
generator polynomial—each of which is either or 1 To make this even more
confusing, the highest-order coefficient of the generator polynomial (always a 1) is left out of the binary representation For example, the polynomial in the first
standard, CCITT, has four nonzero coefficients But the corresponding binary representation has only three 1's in it (bits 12, 5, and 0)
[3] There is one other potential twist called "reflection" that my code does not support You probably won't need that anyway
[4] There is one small difference worth noting here The erase and write cycles take longer than the read cycle So if a read is attempted in the middle of one of those operations, the result will be either delayed or incorrect, depending on the device
Chapter 7 Peripherals
7.1 Control and Status Registers
7.2 The Device Driver Philosophy
7.3 A Simple Timer Driver
7.4 Das Blinkenlights, Revisited
Each pizza glides into a slot like a circuit board into a computer, clicks into place
as the smart box interfaces with the onboard system of the car The address of the customer is communicated to the car, which computes and projects the optimal route on a heads-up display
—Neal Stephenson, Snow Crash
In addition to the processor and memory, most embedded systems contain a
handful of other hardware devices Some of these devices are specific to the
application domain, while others—like timers and serial ports—are useful in a wide variety of systems The most generically useful of these are often included within the same chip as the processor and are called internal, or on-chip,
peripherals Hardware devices that reside outside the processor chip are, therefore, said to be external peripherals In this chapter we'll discuss the most common software issues that arise when interfacing to a peripheral of either type
7.1 Control and Status Registers
Trang 4The basic interface between an embedded processor and a peripheral device is a set
of control and status registers These registers are part of the peripheral hardware, and their locations, size, and individual meanings are features of the peripheral For example, the registers within a serial controller are very different from those in a timer/counter In this section, I'll describe how to manipulate the contents of these control and status registers directly from your C/C++ programs
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
Trang 5stored 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
Trang 6That'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