As an embedded software engineer, you’ll have the opportunity to work with many different pieces of hardware in your career. In this chapter, I will teach you a simple procedure that I use to familiarize myself with any new board. In the process, I’ll guide you through the creation of a header file that describes the board’s most important features and a piece of software that initializes the hardware to a known state.
Understand the Big Picture
Before writing software for an embedded system, you must first be familiar with the hardware on which it will run. At first, you just need to understand the general operation of the system. You do not need to understand every little detail of the hardware; that kind of knowledge will not be needed right away and will come with time.
Whenever you receive a new board, you should take some time to read whatever documents have been provided with it. If the board is an off-the-shelf product, it may arrive with a “User’s Guide” or “Programmer’s Manual” that has been written with the software developer in mind. However, if the board was custom designed for your project, the documentation may be more cryptic or written primarily for the reference of the hardware designers. Either way, this is the single best place for you to start.
While you are reading the documentation, set the board itself aside. This will help you to focus on the big picture. There will be plenty of time to examine the actual board more closely when you are done reading. Before picking up the board, you should be able to answer two basic questions about it:
- What is the overall purpose of the board?
- How does data flow through it?
For example, imagine that you are a member of a modem design team. You are a software developer who has just received an early prototype board from the hardware designers. Because you are already familiar with modems, the overall purpose of the board and data flow through it should be fairly obvious to you. The purpose of the board is to send and receive digital data over an analog telephone line. The hardware reads digital data from a set of electrical connections on one edge and writes an analog version of the data to an attached telephone line. Data also flows in the opposite direction when analog data is read from the telephone line jack and output digitally.
Though the purpose of most systems is fairly obvious, the flow of the data may not be. I often find that a data-flow diagram is helpful in achieving rapid comprehension. If you are lucky, the documentation provided with your hardware will contain a superset of the block diagram you need. However, you might still find it useful to create your own data-flow diagram. That way, you can leave out those hardware components that are unrelated to the basic flow of data through the system.
In the case of the Arcom board, the hardware was not designed with a particular application in mind. So, for the remainder of this chapter, we’ll have to imagine that it does have a purpose. We shall assume the board was designed for use as a printer-sharing device. A printer-sharing device allows two computers to share a single printer. The user of the device connects one computer to each serial port and a printer to the parallel port. Both computers can then send documents to the printer, though only one of them may do so at a given time.
In order to illustrate the flow of data through the printer-sharing device, I’ve drawn the diagram in Figure 5-1. (Only those hardware devices that are involved in this application of the Arcom board are shown.) By looking at the block diagram, you should be able to quickly visualize the flow of the data through the system. Data to be printed is accepted from either serial port, held in RAM until the printer is ready for more data, and delivered to the printer via the parallel port. The software that makes all of this happen is stored in ROM.
Once you’ve created a block diagram, don’t just crumple it up and throw it away. You should instead put it where you can refer to it throughout the project. I recommend creating a project notebook or binder, with this data-flow diagram on the first page. As you continue working with this piece of hardware, write down everything you learn about it in your notebook. You may also want to keep notes about the software design and/or implementation. A project notebook is valuable not only while you are developing the software, but also once the project is complete. You will appreciate the extra effort of keeping a notebook when you need to make changes to your software, or work with similar hardware, months or years later.
If you still have any big picture questions after reading the hardware documents, ask a hardware engineer for some help. If you don’t already know the hardware’s designer, take a few minutes to introduce yourself. If you have some time, take him out to lunch or buy him a beer after work. (You don’t even have to talk about the project the whole time!) I have found that many software engineers have difficulty communicating with hardware engineers, and vice versa. In embedded systems development especially, it is essential that the hardware and software teams be able to communicate with one another.
Examine the Landscape
It is often useful to put yourself in the processor’s shoes for a while. After all, the processor is only going to do what you ultimately instruct it to with your software. Imagine what it is like to be the processor: What does the processor’s world look like? If you think about it from this perspective, one thing you quickly realize is that the processor has a lot of compatriots. These are the other pieces of hardware on the board, with which the processor can communicate directly. In this section, you will learn to recognize their names and addresses.
The first thing to notice is that there are two basic types: memories and peripherals. Obviously, memories are for data and code storage and retrieval. But you may be wondering what the peripherals are. These are specialized hardware devices that either coordinate interaction with the outside world (I/O) or perform a specific hardware function. For example, two of the most common peripherals in embedded systems are serial ports and timers. The former is an I/O device and the latter simply knows how to count.
Members of Intel’s 80x86 and some other processor families have two distinct address spaces through which they can communicate with these memories and peripherals. The first address space is called the memory space and is intended primarily for memory devices; the second is reserved exclusively for peripherals and is called the I/O space. However, peripherals may also be located within the memory space, at the discretion of the hardware designer. When that happens, we say that those peripherals are “memory-mapped”.
From the processor’s point of view, memory-mapped peripherals look and act very much like memory devices. However, the function of a peripheral is obviously quite different than a memory. Instead of simply storing the data that is provided to it, a peripheral may instead interpret it as a command or as data to be processed in some way. When peripherals are located within the memory space, we say that the system has memory-mapped I/O.
The designers of embedded hardware often prefer to use memory-mapped I/O exclusively, since it has advantages for both the hardware and software developers. It is attractive to the hardware developer because he may be able to eliminate the I/O space, and some of its associated wires, altogether. This may not significantly reduce the production cost of the board, but it may reduce the complexity of the hardware design. Memory-mapped peripherals are also better for the programmer, who is able to use pointers, data structures, and unions to interact with the peripherals more easily and efficiently.
Memory Map
All processors store their programs and data in memory. In some cases this memory may reside on the very same chip as the processor, but more often it is located in external memory chips. These chips are located in the processor’s memory space and the processor communicates with them by way of two sets of electrical wires called the address bus and the data bus. To read or write a particular location in memory, the processor first writes the desired address onto the address bus. The data is then transferred over the data bus.
As you are reading about a new board, create a table with the name and address range of each memory device and peripheral that is located in the memory space. Organize the table so that the lowest address is at the bottom and the highest address is at the top. Each time you add a device to the memory map, place it in its approximate location in memory and label the starting and ending addresses, in hexadecimal. After you have finished inserting all of the devices into the memory map, be sure to label any unused memory regions as such.
If you look back at the block diagram of the Arcom board in Figure 5-1, you will see that there are three devices attached to the address and data buses. These devices are the RAM and ROM and a mysterious device labeled “Zilog 85230 Serial Controller.” The documentation provided by Arcom says that the RAM is located at the bottom of memory and extends upward for the first 128 Kbytes of the memory space. The ROM is located at the top of memory, and extends downward for 256 Kbytes. But this area of memory actually contains two ROMs—an EPROM and a Flash memory device—each of size 128 Kbytes. The third device, the Zilog 85230 Serial Communications Controller, is a memory-mapped peripheral whose registers are accessible between the addresses 70000h and 72000h.
The memory map in Figure 5-2 shows what these devices look like to the processor. In a sense, this is the processor’s “address book.” Just as you maintain a list of names and addresses in your personal life, you must maintain a similar list for the processor. The memory map contains one entry for each of the memories and peripherals accessible from the processor’s memory space. This diagram is arguably the most important piece of information about the system and should be kept up to date and as part of the permanent records associated with the project.
For each new board, you should create a 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. If the memory map ever changes—for example, if the 128 Kbytes of RAM is moved—you need only change the affected lines of the board-specific header file and recompile your application.
Pointers vs. Addresses |
In both C and C++, the value of a pointer is an address. So when we say that we have a pointer to some data, we really mean that we have the address at which the data is stored. But programmers don’t usually set or examine these addresses directly. The exception to this rule are the developers of operating systems, device drivers, and embedded software, who sometimes need to set the value of a pointer explicitly in their code. Unfortunately, the exact representation of an address may change from processor to processor or may even be compiler dependent. This means that a physical address like 12345h might not be stored in exactly that form, or might even be stored differently by different compilers. The issue that then arises is how a programmer can set the value of a pointer explicitly so that it points to the desired location in the memory map. Most C/C++ compilers for 80x86 processors use 32-bit pointers. However, the older processors don’t have a simple linear 32-bit address space. For example, Intel’s 80188EB processor has only a 20-bit address space. And, in addition, none of its internal registers can hold more than 16 bits. So, on this processor, two 16-bit registers—a segment register and an offset register—are combined to create the 20-bit physical address. (The physical address computation involves left-shifting the contents of the segment register by four bits and adding the contents of the offset register to the result. Any overflow into the 21st bit is ignored.) To declare and initialize a pointer to a register located at physical address 12345h we, therefore, write: int * pRegister = (int *) 0x10002345; where the leftmost 16 bits contain the segment value and the rightmost 16 bits contain the offset value. For convenience, 80x86 programmers sometimes write addresses as segment:offset pairs. Using this notation, the physical address 12345h would be written as 0x1000:2345. This is precisely the value—sans colon—that we used to initialize the pointer above. However, for each possible physical address there are 4096 distinct segment:offset pairs that point to a given physical address. For example, the pairs 0x1200:0345 and 0x1234:0005 (and 4093 others) also refer to physical address 12345h. |
As this chapter progresses, I will show you how to create a header file for the Arcom board. The first section of this file is listed below. The part of the header file below describes the memory map. The most notable difference between the memory map in the header file and that in Figure 5-2 is the format of the addresses. The sidebar “Pointers vs. Addresses” explains why.
/********************************************************************** * * Memory Map * * Base Address Size Description * -------------- ----- ----------------------------------- * 0000:0000h 128K SRAM * 2000:0000h Unused * 7000:0000h Zilog SCC Registers * 7000:1000h Zilog SCC Interrupt Acknowledge * 7000:2000h Unused * C000:0000h 128K Flash * E000:0000h 128K EPROM * **********************************************************************/ #define SRAM_BASE (void *) 0x00000000 #define SCC_BASE (void *) 0x70000000 #define SCC_INTACK (void *) 0x70001000 #define FLASH_BASE (void *) 0xC0000000 #define EPROM_BASE (void *) 0xE0000000
I/O Map
If a separate I/O space is present, it will be necessary to repeat the memory map exercise to create an I/O map for the board as well. The process is exactly the same. Simply create a table of peripheral names and address ranges, organized in such a way that the lowest addresses are at the bottom. Typically, a large percentage of the I/O space will be unused, since most of the peripherals located there will have only a handful of registers.
The I/O map for the Arcom board is shown in Figure 5-3. It includes three devices: the Peripheral Control Block (PCB), Parallel Port, and Debugger Port. The PCB is a set of registers within the 80188EB that are used to control the on-chip peripherals. The chips that control the parallel port and debugger port reside outside of the processor. These ports are used to communicate with the printer and a host-based debugger, respectively.
The I/O map is also useful when creating the header file for your board. Each region of the I/O space maps directly to a constant called the base address. The translation of the above I/O map into a set of constants can be found in the listing below.
/********************************************************************** * * I/O Map * * Base Address Description * --------------- ---------------------------------------- * 0000h Unused * FC00h SourceVIEW Debugger Port (SVIEW) * FD00h Parallel I/O Port (PIO) * FE00h Unused * FF00h Peripheral Control Block (PCB) * **********************************************************************/ #define SVIEW_BASE 0xFC00 #define PIO_BASE 0xFD00 #define PCB_BASE 0xFF00
Learn How to Communicate
Now that you know the names and addresses of the memory and I/O devices attached to the processor, it is time to learn how to communicate with the latter. There are two basic communication techniques: polling and interrupts. In either case, the processor usually issues some sort of commands to the device—by way of the memory or I/O space—and waits for the device to complete the assigned task. For example, the processor may ask a timer to count down from 1000 to 0. Once the countdown begins, the processor is interested in just one thing: Is the timer done counting yet?
If polling is used, then the processor repeatedly checks to see if the task has been completed. This is analogous to the small child who repeatedly asks “Are we there yet?” throughout a long trip. Like the child, the processor spends a large amount of otherwise useful time asking the question and getting a negative response. To implement polling in software, you need only create a loop that reads the status register of the device in question. An example of this is shown below.
do { // Play games, read, listen to music, etc. ... // Poll to see if we’re there yet. status = areWeThereYet(); } while (status == NO);
The second communication technique uses interrupts. An interrupt is an asynchronous electrical signal from a peripheral to the processor. When interrupts are used, the processor issues commands to the peripheral exactly as before, but then waits for an interrupt to signal completion of the assigned work. While the processor is waiting for the interrupt to arrive, it is free to continue working on other things. But when the interrupt signal is finally asserted, the processor temporarily sets aside its current work and executes a small piece of software called the interrupt service routine (ISR). When the ISR completes, the processor returns to the work that was interrupted.
Of course, this isn’t all automatic. The programmer must write the ISR himself and “install” and enable it so that it will be executed when the relevant interrupt occurs. The first few times you do this, it will be a significant challenge. But, even so, the use of interrupts generally decreases the complexity of one’s overall code by giving it a better structure. Rather than embedding device polling within an unrelated part of the program, the two pieces of code remain appropriately separate.
On the whole, interrupts are a much more efficient use of the processor than polling. The processor is able to use a larger percentage of its waiting time to perform useful work. However, there is some overhead associated with each interrupt. It takes a good bit of time—relative to the length of time it takes to execute an opcode—to put aside the processor’s current work and transfer control to the interrupt service routine. Many of the processor’s registers must be saved in memory and lower-priority interrupts must be disabled. So, in practice both methods are used frequently. Interrupts are used when efficiency is paramount or multiple devices must be monitored simultaneously. Polling is used when the processor must respond to some event more quickly than is possible using interrupts.
Interrupt Map
Most embedded systems have only a handful of interrupts. Associated with each of these are an interrupt pin (on the outside of the processor chip) and an ISR. In order for the processor to execute the correct ISR, a mapping must exist between interrupt pins and ISRs. This mapping usually takes the form of an interrupt vector table. The vector table is usually just an array of pointers to functions, located at some known memory address. The processor uses the interrupt vector (a unique number associated with each interrupt pin) as its index into this array. The value stored at that location in the vector table is usually just the address of the ISR to be executed.
It is important to initialize the interrupt vector table correctly. (If it is done incorrectly, the ISR may be executed in response to the wrong interrupt or never executed at all.) The first part of this process is to create an interrupt map that organizes the relevant information. An interrupt map is a table containing a list of interrupt vectors and the devices to which they refer. This information should be included in the documentation provided with the board. Table 5-1 shows the interrupt map for the Arcom board.
Interrupt Number | Generating Device |
8 | Timer/Counter #0 |
17 | Zilog 85230 SCC |
18 | Timer/Counter #1 |
19 | Timer/Counter #2 |
20 | Serial Port Receive |
21 | Serial Port Transmit |
Once again, our goal is to translate the information in the table into a form that is useful for the programmer. After constructing an interrupt map like the one above, you should add a third section to the board-specific header file. Each line of the interrupt map becomes a single #define within the file, as shown in the listing below.
/********************************************************************** * * Interrupt Map * **********************************************************************/ /* * Zilog 85230 SCC */ #define SCC_INT 17 /* * On-Chip Timer/Counters */ #define TIMER0_INT 8 #define TIMER1_INT 18 #define TIMER2_INT 19 /* * On-Chip Serial Ports */ #define RX_INT 20 #define TX_INT 21
Get to Know the Processor
If you haven’t worked with the processor on your board before, you should take some time to get familiar with it now. This shouldn’t take very long if you will do all of your programming in C or C++. To the user of a high-level language, most processors look and act pretty much the same. However, if you’ll be doing any assembly language programming, you will need to familiarize yourself with the processor’s architecture and basic instruction set.
Everything you need to know about the processor can be found in the databooks provided by the manufacturer. If you don’t have a databook or “Programmer’s Guide” for your processor already, you should obtain one immediately. If you are going to be a successful embedded systems programmer, you must be able to read databooks and get something out of them. Processor databooks are usually well written—as databooks go—so they are an ideal place to start. Begin by flipping through the databook and noting the sections that are most relevant to the tasks at hand. Then go back and begin reading the processor overview section.
Processors, In General
Many of the most common processors are members of families of related devices. In some cases, the members of such a processor family represent points along an evolutionary path. The most obvious example is Intel’s 80x86 family, which spans from the original 8086 to the Pentium II—and beyond. In fact, the 80x86 family has been so successful that it has spawned an entire industry of imitators.
As it is used in this book, the term “processor” refers to any of three types of devices known as microprocessors, microcontrollers, and digital signal processors. The name microprocessor is usually reserved for a chip containing a powerful CPU that has not been designed with any particular computation in mind. These chips are usually the foundation of personal computers and high-end workstations. The most common microprocessors are members of Motorola’s 68k—found in older Macintosh computers—and the ubiquitous 80x86 families.
A microcontroller is very much like a microprocessor, except that it has been designed specifically for use in embedded systems. Microcontrollers typically include a CPU, memory (a small amount of RAM and/or ROM), and other peripherals in the same integrated circuit. By purchasing all of these items on a single chip, it is possible to reduce the cost of an embedded system substantially. Among the most popular microcontrollers are the 8051 and its many imitators and Motorola’s 68HCxx series. It is also common to find microcontroller versions of popular microprocessors. For example, Intel’s 386EX is a microcontroller version of the very successful 80386 microprocessor.
The final type of processor is a digital signal processor, or DSP. The CPU within a DSP is specially designed to perform discrete-time signal processing calculations—like those required for audio and video communications—extremely fast. Because DSPs can perform these types of calculations much faster than other processors, they offer a powerful, low-cost microprocessor alternative for designers of modems and other telecommunications and multimedia equipment. Two of the most common DSP families are the TMS320Cxx and 5600x series from TI and Motorola, respectively.
Intel's 80188EB Processor
The processor on the Arcom board is an Intel 80188EB—a microcontroller version of the 80186. In addition to the CPU, the 80188EB contains an Interrupt Control Unit, 2 Programmable I/O ports, 3 Timer/Counters, 2 Serial Ports, a DRAM Controller, and a Chip-Select Unit. These extra hardware devices are located within the same chip and are referred to as on-chip peripherals. The CPU is able to communicate with and control the on-chip peripherals directly, via internal busses.
Although the on-chip peripherals are distinct hardware devices, they act like little extensions of the 80186 CPU. The software can control them by reading and writing a 256-byte block of registers known as the Peripheral Control Block. You may recall that we encountered this block when we first discussed the memory and I/O maps for the board. By default the PCB is located in the I/O space, beginning at address FFOOh. However, if so desired, the PCB can be relocated to any convenient address in either the I/O or memory space.
The control and status registers for each of the on-chip peripherals are located at fixed offsets from the PCB base address. The exact offset of each register can be found in a table in the 80188EB Microprocessor User’s Manual. To isolate these details from your application software, it is good practice to include the offsets of any registers you will be using in the header file for your board. I have done this for the Arcom board, but have only shown below those registers that will be discussed in later chapters of the book.
/********************************************************************** * * On-Chip Peripherals * **********************************************************************/ /* * Interrupt Control Unit */ #define EOI (PCB_BASE + 0x02) #define POLL (PCB_BASE + 0x04) #define POLLSTS (PCB_BASE + 0x06) #define IMASK (PCB_BASE + 0x08) #define PRIMSK (PCB_BASE + 0x0A) #define INSERV (PCB_BASE + 0x0C) #define REQST (PCB_BASE + 0x0E) #define INSTS (PCB_BASE + 0x10) /* * Timer/Counters */ #define TCUCON (PCB_BASE + 0x12) #define T0CNT (PCB_BASE + 0x30) #define T0CMPA (PCB_BASE + 0x32) #define T0CMPB (PCB_BASE + 0x34) #define T0CON (PCB_BASE + 0x36) #define T1CNT (PCB_BASE + 0x38) #define T1CMPA (PCB_BASE + 0x3A) #define T1CMPB (PCB_BASE + 0x3C) #define T1CON (PCB_BASE + 0x3E) #define T2CNT (PCB_BASE + 0x40) #define T2CMPA (PCB_BASE + 0x42) #define T2CON (PCB_BASE + 0x46) /* * Programmable I/O Ports */ #define P1DIR (PCB_BASE + 0x50) #define P1PIN (PCB_BASE + 0x52) #define P1CON (PCB_BASE + 0x54) #define P1LTCH (PCB_BASE + 0x56) #define P2DIR (PCB_BASE + 0x58) #define P2PIN (PCB_BASE + 0x5A) #define P2CON (PCB_BASE + 0x5C) #define P2LTCH (PCB_BASE + 0x5E)
Other things you’ll want to learn about the processor from its databook are:
- Where should the interrupt vector table be located? Does it have to be located at a specific address in memory? If not, how does the processor know where to find it?
- What is the format of the interrupt vector table? Is it just a table of pointers to ISR functions?
- Are there any special interrupts, sometimes called traps, that are generated within the processor itself? Must an ISR be written to handle each of these?
- How are interrupts enabled and disabled (globally and individually)?
- How are interrupts acknowledged or cleared?
Study the External Peripherals
At this point, you’ve studied every aspect of the new hardware except the external peripherals. These are the hardware devices that reside outside the processor chip and communicate with it by way of interrupts and I/O or memory-mapped registers.
Begin by making a list of the external peripherals. Depending on your application, this list might include LCD or keyboard controllers, A/D converters, network interface chips, or custom ASICs. In the case of the Arcom board, the list contains just three items: a Zilog 85230 SCC, a parallel port, and a debugger port.
You should obtain a copy of the “User’s Manual” or databook for each device on your list. At this early stage of the project, your goal in reading these documents is to understand the basic functions of the device. What does the device do? What registers are used to issue commands and receive the results? What do the various bits and larger fields within these registers mean? When, if ever, does the device generate interrupts? How are interrupts acknowledged or cleared at the device?
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. This is nothing more than a collection of software routines that control the operation of the peripheral and isolate the application software from the details of that particular hardware device. I’ll have a lot more to say about device drivers in Chapter 7.
Initialize the Hardware
The final step in getting to know your new hardware is to write some initialization software. This is your best opportunity to develop a close working relationship with the hardware—especially if you will be developing the remainder of the software in a high-level language. During hardware initialization it will be impossible to avoid using assembly language. However, after completing this step, you will be ready to writing small programs in C or C++.
If you are one of the first software engineers to work with a new board—especially a prototype—the hardware may not work as advertised. All processor-based boards require some amount of software testing to confirm the correctness of the hardware design and the proper functioning of the various peripherals. This puts you in an awkward position when something is not working properly. How do you know if the hardware or your software is to blame? If you happen to be good with hardware or have access to a simulator, you might be able to construct some experiments to answer this question. Otherwise, you should probably ask a hardware engineer to join you in the lab for a joint debugging session. |
The hardware initialization should be executed before the startup code described in Chapter 3. The code described there assumes that the hardware has already been initialized and concerns itself only with creating a proper runtime environment for high-level language programs. Figure 5-4 provides an overview of the entire initialization process, from processor reset through hardware initialization and C/C++ startup code to main.
The first stage of the initialization process is the reset code. This is a small piece of assembly (usually just two or three instructions) that the processor executes immediately after it is powered on or reset. The sole purpose of this code is to transfer control to the hardware initialization routine. The first instruction of the reset code must be placed at a specific location in memory, usually called the reset address, that is specified in the processor databook. The reset address for the 80188EB—and all other members of the 80x86 family—is FFFF0h.
Most of the actual hardware initialization takes place in the second stage. At this point, we need to inform the processor about its environment. This is also a good place to initialize the interrupt controller and other critical peripherals. Less critical hardware devices can be initialized when the associated device driver is started, usually from within main.
Intel’s 80188EB has several internal registers that must be programmed before any useful work can be done with the processor. These registers are responsible for setting up the memory and I/O maps and are part of the processor’s internal Chip-Select Unit. By programming the chip-select registers, you are essentially waking up each of the memory and I/O devices that are connected to the processor. Each chip-select register is associated with a single “chip enable” wire that runs from the processor to some other chip. The association between particular chip-selects and hardware devices must be established by the hardware designer. All you need to do is get a list of chip-select settings from him and load those settings into the chip-select registers.
Upon reset, the 80188EB assumes a worst-case scenario. It assumes there are just 1024 bytes of ROM—located in the address range FFC00h to FFFFFh—and that no other memory or I/O devices are present. This is the processor’s “fetal position” and it implies that the hw_init routine must be located at address FFC00h (or higher). It must also not require the use of any RAM. The hardware initialization routine should start by initializing the chip-select registers to inform the processor about the other memory and I/O devices that are installed on the board. By the time this task is complete, the entire range of ROM and RAM addresses will be enabled. So the remainder of your software can be located at any convenient address in either ROM or RAM.
The third initialization stage contains the startup code. This is the assembly-language code that we saw back in Chapter 3. In case you don’t remember, its job is to the prepare the way for code written in a high-level language. Of importance here is only that the very last instruction of the startup code calls main. From that point forward, all of your other software can be written in C or C++.
Hopefully, you are starting to understand how embedded software gets from processor reset to your main program. Admittedly, the very first time you try to pull all of these components together (reset code, hardware initialization, C/C++ startup code, and application) on a new board there will be problems. So expect to spend some time debugging each of them. Honestly, this will be the hardest part of the project. You will soon see that once you have a working Blinking LED program to fall back on, the work just gets easier and easier—or at least more similar to ordinary computer programming.
Up to this point in the book we have been building the infrastructure for embedded programming. But the topics we’re going to talk about in the remaining chapters concern higher-level structures: memory tests, device drivers, operating systems, and actually useful programs. These are pieces of software you’ve probably seen before on other computer systems projects. However, there will still be some new twists related to the embedded programming environment.