Файл: Embedded Systems Design - An Introduction to Processes Tools and Techniques (A. Berger, 2002).pdf

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 13.06.2025

Просмотров: 2129

Скачиваний: 0

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

out 0x42

");

}

In this example, the instructions in and out are I/O access instructions and not memory access (read/write) instructions.

Please note that these functions involve some hidden assumptions that might not be true for your compiler. First, read_reg() assumes that the function return value should be placed in the accumulator. Different compilers observe different conventions (sometimes dependent on the data size) about where the return value should be placed. Second, write_reg() assumes that the compiler will translate the reference to newval into an appropriate stack reference. (Remember, arguments to functions are passed on the stack.) Not all compilers are so nice!

If your compiler doesn’t support in-line assembly, you’ll have to write similar read/write functions entirely in assembly and link them to the rest of your program. Writing the entire function in assembly is more complex, because it must conform to the compiler’s conventions regarding stack frames. You can get a “template” for the assembly by compiling a trivial C function that manipulates the right number of arguments directly to assembly

int read_reg_fake( )

{

return 0x7531;

}

Substituting the desired port read in place of the literal load instruction and changing the function name converts the generated assembly directly into a complete port read function.

Memory-Mapped Access

Manipulating a memory-mapped device is far simpler. Most environments support two methods, linker-based and pointer-based. The linker-based method uses the extern qualifier to inform the compiler that the program will be using a resource defined outside the program. The line

extern volatile int device_register;

tells the compiler that an integer-sized resource named device_register exists outside the program, in a place known to the linker. With this declaration available, the rest of the program can read and write from the device just as if it were a global variable. (The importance of volatile is explained later in this chapter.)

Of course, this solution begs the question because it doesn’t explain how the linker knows about the device. To successfully link a program with this kind of external declaration, you must use a linker command to associate the “variable” name with the appropriate address. If the register in question was located at $40000000, the command might be something like

PUBLIC _device_register = $40000000

Tip Be forewarned, the linker might not recognize long, lowercase names such as device_register. (Linkers are usually brain-dead compared to compilers.) One way to find out what name the linker is expecting is to compile the

module before you add the PUBLIC linker command and see what name the linker reports as unresolvable.

Those who prefer this method argue that you should use the linker to associate symbols with physical addresses. They also argue that declaring the device register as extern keeps all the information about the system’s memory map in one place: in the linker command file, where it belongs.

The alternative is to access memory-mapped hardware through a C pointer. A simple cast can force a pointer to address any specific memory address. For example, a program can manipulate an Application-Specific Integrated Circuit (ASIC) device that appears to the software as 64, 16-bit, memory-mapped registers beginning at memory address 0x40000000 with code like this

unsigned short x;

/* Local variable */

volatile unsigned short *io_regs;

/* Pointer to ASIC */

io_regs = (unsigned short* ) 0x40000000;/* Point to ASIC */ x = io_regs[10]; /* Read register 10 */

This example declares io_regs to be a pointer to an unsigned, 16-bit (short) variable. The third assignment statement uses a cast to force io_regs to point to memory location 0x40000000. The cast operator directs the compiler to ignore everything it knows about type checking and do exactly what you say because you are the programmer and, best of all, you do know exactly what you are doing.

Bitwise Operations

Embedded programs often need to manipulate individual bits within hardware registers. In most situations, the best practice is to read the entire register, change the bit, and then write the entire register back to the device. For example, to change the third bit from the right

const char status_mask=0x04; extern volatile char device_register;

device_register = device_register | status_mask;

//force the third from the right bit to a one. device_register = device_register & (~status_mask);

//force the third from the right bit to a zero device_register = device_register ^ status_mask;

//change the state of the third from the right bit.

You get the exact same result using the shorthand assignment operators: device_register |= status_mask;

device_register &= (~status_mask); device_register ^= status_mask;

The literal that corresponds to the bit to be changed is called a mask. Defining the constant to represent the mask (status_mask) insulates the rest of your code from unanticipated changes in the hardware (or in your understanding of the hardware).


The constant also can greatly improve the readability of this kind of code. Not all embedded compilers support ANSI C’s const. If your compiler doesn’t support const, you can use the preprocessor to give the status mask a symbolic name, as in the following listing. The const form is preferred because it supports static type checking.

#define STATUS_MASK 0x04

device_register = device_register | STATUS_MASK;

Although this read/modify/write method works in most cases, with some devices, the read can cause unwanted side-effects (such as clearing a pending interrupt). If the register can’t be read without causing a problem, the program must maintain a shadow register. A shadow register is a variable that the program uses to keep track of the register’s contents. To change a bit in this case, the program should:

Read the shadow register

Modify the shadow register

Save the shadow register

Write the new value to the device

In its most compact form, the code would look something like this #define STATUS_MASK 0x04

int shadow;

device_register = (shadow |= STATUS_MASK;)

Using the Storage Class Modifier Volatile

Another important data modifying attribute is sometimes missed when interfacing C or C++ code to hardware peripheral devices: the storage class modifier, volatile. Most compilers assume that memory is memory and, for the purpose of code optimization, can make certain assumptions about that memory. The key assumption is that a value stored in memory is not going to change unless you write to it. However, hardware peripheral registers change all the time. Consider the case of a simple universal asynchronous receiver/transmitter (UART). The UART receives serial data from the outside world, strips away the extraneous bits, and presents a byte of data for reading. At 50 kilobaud, it takes 0.2 milliseconds to transmit one character. In 0.2 milliseconds, a processor with a 100MHz memory bus, assuming four clock cycles per memory write, can write to the UART output data register about 5,000 times. Clearly, a mechanism is needed to control the rate that the transmitted data is presented to the UART.

The UART paces the data rate by having a status bit, typically called Transmitter Buffer Empty (TBMT). Thus, in the example case, the TBMT bit might go low when the first byte of data to be transmitted is sent to the UART and then stay low until the serial data has been sent and the UART is ready to receive the next character from the processor. The C code for this example is shown in Listing 5.1.

Listing 5.1: UART code.

/* Suppose that an I/O port is located at 0x4000

I/O port status is located at 0x4001

Transmitter buffer empty = DB0; DB0 = 1 when character may be sent */

void main(void)

{

int *p_status;/* Pointer to the status port */

int *p_data;/* Pointer to the data port */

p_status = (int*) 0x4001 ;/* Assign pointer to status port */

p_data = ( int* ) 0x4000 ;/* Assign pointer to data port */

do { } while (( *p_status & 0x01) == 0 );/* Wait */

…..

…..

}

C code for a UART polling loop.

Suppose your C compiler sees that you’ve written a polling loop to continuously read the TBMT status bit. It says, “Aha! I can make that more efficient by keeping that memory data in a local CPU register (or the internal data cache).” Thus, the code will be absolutely correct, but it won’t run properly because the new data in the memory location representing the UART is never updated.

The keyword volatile[7,8] is used to tell the compiler not to make any assumptions about this particular memory location. The contents of the memory location might change spontaneously, so always read and write to it directly. The compiler will not try to optimize it in any way nor allow it to be assigned to the data cache.

Note Some compilers can go even further and have special keywords that allow you to specify that this is noncachable data. This forces the compiler to turn off caching in the processor.

Speed and Code Density

In many cases, the compiler generates much more efficient code, both in terms of space and speed, if an operation is performed through a pointer rather than


through a normal variable reference. If a function manipulates the same variable several times or steps through the members of an array, forming the reference through a pointer might produce better code.

Both time and RAM are usually in short supply in most embedded systems, so efficiency is key. For example, this snippet of C code

void strcpy2(char dst[], char const src[])

}

int i;

for (i=0; src[i]; i+=1)

{

dst[i] = src[i];

}

}

translates to the following sequence of assembly language instructions. void strcpy2(char dst[], char const src[])

{

int i;

00000000: 4E56 0000

link

a6,#0

00000004: 226E 0008

movea.l

8(a6),a1

00000008: 206E 000C

movea.l

12(a6),a0

for (i=0; src[i]; i+=1)

{

0000000C: 7000

moveq

#0,d0

0000000E: 6008

bra.s

*+10

; 0x00000018

dst[i] = src[i];

00000010: 13B0 0800 0800

move.b

(a0,d0.l),(a1,d0.l)

}

00000016: 5280

addq.l

#1,d0

00000018: 4A30 0800

tst.b

(a0,d0.l)

0000001C: 66F2

bne.s

*-12

; 0x00000010

0000001E: 4E5E

unlk

a6

00000020: 4E75

rts

00000022: 8773 7472 6370

dc.b

0x87,'strcpy2'

7932

0000002A: 0000

}

When written with subscript references, the function requires 34 bytes. Notice that the repeatedly executed body of the loop (from move.b to bne.s) spans four instructions.


Like many array operations, this loop can be written in terms of pointers instead of subscripted references:

void strcpy(char *dst, char const *src)

{

while (( *dst++ = *src++ )){;}

}

(The double parentheses quiet a compiler warning about the assignment. The curly braces around the semi-colon quiet a compiler warning about the empty statement.) On the same compiler, this version translates to the following assembly:

void strcpy(char *dst, char const *src)

{

00000000: 4E56 0000

link

a6,#0

00000004: 226E 0008

movea.l

8(a6),a1

00000008: 206E 000C

movea.l

12(a6),a0

while (( *dst++ = *src++ )){;}

0000000C: 12D8

move.b

(a0)+,(a1)+

0000000E: 66FC

bne.s

*-2

; 0x0000000c

00000010: 4E5E

unlk

a6

00000012: 4E75

rts

00000014: 8673 7472 6370 dc.b

0x86,'strcpy',0x00

7900

0000001C: 0000

}

In this case, the compiled code occupies only 20 bytes and the loop body reduces to only two instructions: move.b, bne.s.

Anyway, if the example $69 embedded system had 256Mb of RAM and a 700MHz Pentium-class processor, you could probably ignore the overhead issues and not use pointers. However, reality sometimes rears its ugly head and forces you to program in C with the same care that you would use if programming directly in assembly language.

Interrupts and Interrupt Service Routines (ISRs)

Interrupts are a fact of life in all computer systems. Clearly, many embedded systems would be severely hampered if they spent the bulk of the CPU cycles checking the state of a single status bit in a polling loop.

Interrupts need to be prioritized in order of importance (or criticality) to the system. Taking care of a key being pressed on the keyboard is not as time critical as saving data when an impending power failure is detected.

Conceptually, an ISR is a simple piece of code to write. An external device (for a microcontroller, an external device could be internal to the chip but external to the CPU core) asserts an interrupt signal to the interrupt input of the CPU. If the CPU