Pages

20180620

Programming Embedded Systems by Michael Barr & Jack Ganssle


  • Each embedded system is unique, and the hardware is highly specialized to the application domain. As a result, embedded systems programming can be widely varying experience and can take years to master.
  • Forth is efficient but extremely low-level and unusual; learning to get work done with it takes more time than with C.
  • The first thing to notice is that there are two basic types of hardware to which processors connect: memories and peripherals. Memories are for the storage and retrieval of data and code. Peripherals are specialized hardware devices that either coordinate interaction with the outside world or perform a specific hardware function.
  • Memory-mapped peripherals make life easier for the programmer, who can use C-language pointers, structs, and unions to interact with the peripherals more easily.
  • For each new board, you should create a C-language header file that describes its most important features. This file provides an abstract interface to the hardware. In effect, it allows you to refer to the various devices on the board by name rather than by address. This has the added benefit of making your application software more portable.
  • There are two basic communication techniques: polling and interrupts. In either case, the processor usually issues some sort of command to the device by writing--by way of the memory or I/O space--particular data values to particular addresses within the device, and then waits for the device to complete the assigned tasks.
  • If polling is used, the processor repeatedly checks to see whether the task has been completed.
  • When interrupts are used, the processor issues commands to the peripheral exactly as before, but then waits for an interrupt to signal complexion of the assigned work. While the processor is waiting for the interrupt to arrive, it is free to continue working on other things. When the interrupt signal is asserted, the processor finishes its current instruction, temporarily sets aside its current work, and executes a small piece of software called the interrupt service routine (ISR) or interrupt handler. When the ISR completes, the processor returns to the work that was interrupted.
  • When you are designing the embedded software, you should try to break the program down along device lines. It is usually a good idea to associate a software module called a device driver with each of the external peripherals. A device driver is nothing more than a collection of software routines that control the operation of a specific peripheral and isolate the application software for the details of that particular hardware device.
  • Expect that the initial hardware bring-up will be the hardest part of the project.
  • A popular substitute for the “Hello, World!” program is one that blinks an LED at a rate of 1 Hz. Typically, the code required to turn an LED on and off is limited to a few lines of code, so there is very little room for programming errors to occur. And because almost all embedded systems have LEDs, the underlying concept is extremely portable.
  • One of the most fundamental differences between programs developed for embedded systems and those written for other platforms is that the embedded programs almost always have an infinite loop. Typically, this loop surrounds a significant part of the program’s functionality. The infinite loop is necessary because the embedded software job is never done. It is intended to be run until either the world comes to an end or the board is reset, whichever happens first.
  • A debug monitor, also called a ROM monitor, is a small program that resides in nonvolatile memory on the target hardware that facilitate various operations needed during development. One of the tasks that a debug monitor handles is basic hardware initialization. A debug monitor allows you to download and run software in RAM to debug the program.
  • Debug symbols associate variable and function names with their addresses, as well as include type information about the symbol. This allows you to reference a particular variable by using its symbol name.
  • One of the most primitive debug techniques available is the use of an LED as an indicator of success or failure. The basic idea is to slowly walk the LED enable code throughout the larger program. In other words, first begin with the LED enable code at the reset address. If the LED turns on, you can edit the program--moving the LED enable code to just after the next execution milestone--and then rebuild and test.
  • DMA is a technique for transferring blocks of data directly between two hardware devices with minimal CPU involvement. In the absence of DMA, the processor must read the data from one device and write it to the other, one byte or word at a time
  • Here’s how DMA works. When a block of data needs to be transferred, the processor provides the DMA controller with the source and destination addresses and the total number of bytes. The DMA controller then transferred the data from the source to the destination automatically. When the number of bytes remaining reachers zero, the block transfer ends.
  • Endianness doesn’t matter on a single system. It matters only when two computers are trying to communicate. Ever processor and every communication protocol must choose one type of endianness or the other.
  • One of the first pieces of serious embedded software you are likely to write is a memory test. Once the prototype hardware is ready, the designer would like some reassurance that he has wired the address and data lines correctly and that the memory chips are working properly.
  • The purpose of a memory test is to confirm that each storage location in a memory device is working.
  • The first thing we want to test is the data bus wiring. We need to confirm that any value placed on the data bus by the processor is correctly received by the memory device at the other end.
  • A good way to test each bit independently is to perform the so-called walking 1’s test. The name walking 1’s comes from the fact that a single data bit is set to 1 and “walked” through the entire data word. The number of data values to test is the same as the width of the data bus.
  • To perform the walking 1’s test, simply write the first data value in the table, verify it by reading it back, write the second value, verify, and so on. When you reach the end of the table, the test is complete.
  • After confirming that the data bus works properly, you should next test the address bus. Address bus problems lead to overlapping memory locations. [...] You need to confirm that each of the address pins can be set to 0 and 1 without affecting any of the others.
  • To confirm that no two memory locations overlap, you should first write some initial data value at each power-of-two offset within the device. Then write a new value--an inverted copy of the initial value is a good choice--to the first test offset, and verify that the initial data value is still stored at every other power-of-two offset. If you find a location (other than the one you just wrote) that contains the new data value, you have found a problem with the current address bit. If no overlapping is found, repeat the procedure for each of the remaining offsets.
  • Once you know that the address and data bus wiring are correct, it is necessary to test the integrity of the memory device itself. The goal is to test that every bit in the device is capable of holding both 0 and 1. This test is fairly straightforward to implement, but it takes significantly longer to execute than the previous two tests.
  • How can we tell whether the data or program stored in a nonvolatile memory device is still valid? One of the easiest ways is to compute a checksum of the data when it is known to be valid--prior to programming the ROM, for example. Then, each time you want to confirm the validity of the data, you need only recalculate the checksum and compare the result to the previously computed value. If the two checksums match, the data is assumed to be valid.
  • An embedded processor interacts with a peripheral device through 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.
  • 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.
  • In embedded systems featuring memory-mapped I/O devices, it is sometimes useful to overlay a C struct onto each peripheral control and status registers. Benefits of struct overlays are that you can read and write through a pointer to the struct, the register is described nicely by the struct, code can be kept clean, and the compiler does the address construction at compile time.
  • When it comes to designing device drivers, always focus on one easily stated goal: hide the hardware completely. This hiding of the hardware is sometimes called hardware abstraction. When you’re finished, you want the device driver module to be the only piece of software in the entire system that reads and/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. The device driver can then present a generic interface to higher software levels to access the device.
  • The philosophy of hiding all hardware specifics and interactions within the device driver usually consists 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 they are presented.
    • An interface to the control and status registers.
    • Variables to track the current state of the physical (and logical) devices.
    • A routine to initialize the hardware to a known state.
    • An API for users of the device driver.
    • Interrupt service routines.
  • Interrupts allow developers to separate time-critical operations from the main program to ensure they are processed in a prioritized manner. Because interrupts are asynchronous events, they can happen at any time during the main program’s execution.
  • An interrupt controller multiplexes several input interrupts into a single output interrupt. The controller also allows control over these individual input interrupts for disabling them, prioritizing them, and showing which are active.
  • Interrupts can be either maskable or non maskable. Maskable interrupts can be disabled and enabled by software. Non Maskable interrupts (NMI) are critical interrupts, such as power failure or reset, that cannot be disabled by software.
  • It is critical for the programmer to install an ISR for all interrupts, even the interrupts that are not used in the system. If an ISR is not installed for a particular interrupt and the interrupt occurs, the execution of the program can become undefined.
  • The inclusion and use of a watchdog timer is a common way to deal with unexpected software hangs or crashes that may occur after the system is deployed.
  • One way to ensure the instructions that make up the critical section are executed in order and without interruption is to disable interrupts. However, disabling interrupts when using an operating system may not be permitted and should be avoided; other mechanism should be used to execute these atomic operations.
  • The atomicity of the mutex set and clear operations is enforced by the operating system, which disabled interrupts before reading or modifying the state of the binary flag.
  • Mutexes are used for the protection of shared resources between tasks in an operating system. Shared resources are global variables, memory buffers, or device registers that are accessed by multiple tasks. A mutex can be used to limit access to such a resource to one tasks at a time.
  • Mutexes should exclusively be used for controlling access to shared resources. While semaphores are typically used as signalling devices. A semaphore can be used to signal a task from another task or from an ISR--for example, to synchronize activities.
  • An operating system is said to be deterministic if the worst-case execution time of each of the system calls is calculable.
  • The mmap function asks the kernel to provide access to a physical address range contain in the hardware.
  • Serial buses can be either asynchronous or synchronous. In an asynchronous serial connection, the data is sent without using a common timing clock signal. To align the receiver with the sender, there is some sort of start condition to signify when the transmission begins, and a stop condition to indicate the end of the transmission. Asynchronous serial connection typically uses a separate clock signal to synchronize the receiver with the sender.
  • Some embedded systems don’t have hardware dedicated to performing all of the interface functions of a serial interface. In this case, general-purpose I/O signals are connected to external devices, and it is up to the software to implement the communication protocol. Bit banging is a slang term for the process of transferring serial data under software control.
  • An analog signal has a continuously variable value, with effectively infinite resolution in both time and magnitude.
  • In a nutshell, PWM is a way of digitally encoding analog signal levels. Through the use of high-resolution counters, the duty cycle of a square wave is modulated to encode a specific analog signal level.
  • The Simple Network Management Protocol (SNMP) has been the standard for monitoring and controlling networked devices.
  • Most of the optimization performed on code involve a tradeoff between execution speed and code size. Your program can be made either faster or smaller, but not both. In fact, an improvement in one of these areas can have a negative impact on the other. It is up to the programmer to decide which of these improvements is most important.
  • To speed things up, try to put the individual cases in order by their relative frequency of occurrence. In other words, put the most likely cases first and the least likely cases last. This will reduce the average execution time, though it will not improve at all upon the worst-case time.
  • Unless your target platform features a floating-point processor, you’ll pay a very large penalty for manipulating float data in your program.
  • One of the best things you can do to reduce the size of your program is to avoid using large standard library routines. Many of the largest routines are costly in terms of size because they try to handle all possible cases.
  • Power consumption is a major concern for portable or battery-operated devices. Power issues, such as how long the device needs to run and whether the batteries can be recharged, need to be thought out ahead of time.
  • There are several methods to conserve power in an embedded system, including clock control, power-sensitive processors, low-voltage ICs, and circuit shutdown.

No comments:

Post a Comment