EDN logo


Columnist: September 28, 1995

Banking basics

Jack Ganssle,
Embedded Systems Contributing Editor




When asked how much money is enough, Nelson Rockefeller reportedly replied, "Just a little bit more." We poor folks may have trouble understanding his perspective, but, all too often, we exhibit the same response when picking the size of the address space for a new design. Given that the code inexorably grows to fill any allocated space, "just a little more" is a plea we hear from the software people too frequently. Is the solution to make exclusive use of 32-bit machines, cramming 4 Gbytes of RAM into our cost-sensitive application in the hopes that no one could possibly use that much memory?

Although most systems clearly couldn't tolerate the costs associated with such a poor decision, a lot of designers take a middle tack, selecting high-end processors to cover their (ahem) posterior parts. Although 32-bit CPUs have tons of address space, 16 bit devices generally sport 1 to 16 Mbytes. It's hard to imagine needing more than 16 Mbytes for a typical embedded application; even 1 Mbit is enough for most designs.

A typical 8-bit processor, though, is limited to 64 kbytes. Once, this was an ocean of memory we could never imagine filling. Now, C compilers let us reasonably produce applications far more complex than those dreamed of even a few years ago. Today, the midrange embedded systems I see usually burn up 64 to 256 kbytes of program and data space--too much for an 8-bit processor alone to handle.

If horsepower were not an issue, I'd simply toss in an 80188 and profit from the cheap 8-bit bus that runs 16-bit instructions over 1 Mbyte of address space. Sometimes, this is simply not an option; a lot of us design upgrades to older systems. We're stuck with tens of thousands of lines of "legacy" code that are too expensive to change. The code forces us to continue using the same CPU. Like taxes, programs always get bigger, demanding more address space that the processor can handle. Whatcha gonna do?

Perhaps the only solution is to add address bits. Build an external mapper using PLDs or discrete logic. The mapper's outputs go into high-order address lines on your RAM and ROM devices. Add code to remap these lines, swapping sections of program or data in and out as required.

Add a mapper, though, and you'll suddenly face two distinct address spaces that complicate software design. The first is the physical space, the entire universe of memory on your system. Expand your processor's 64-kbyte limit to 256 kbytes by adding two address lines, and the physical space is 256 kbytes.

Logical addresses are those that your program generates and then places onto the processor's bus. Executing a MOV A (0FFFF) instruction tells the processor to read from the last address in its 64-kbyte logical address space. External banking hardware can translate this to another address, but the code itself remains blissfully unaware of such actions. All it knows is that some data comes from memory in response to the 0FFFF placed on the bus. For a typical 8-bit CPU with 16 address lines, the program can never generate a logical address larger than 64 kbytes.

This situation is much like the one that 80x86 assembly-language programmers face, because 64-kbyte segments are essentially logical spaces. You can't get to the rest of physical memory without doing something--in this case, reloading a segment register. Conversely, if there's no mapper, then the physical and logical spaces are identical.

Consider doubling your address space by taking advantage of processor-cycle types. If the CPU differentiates memory reads from fetches, you may be able to easily produce separate data and code spaces. The 68000's seldom-used function codes are for just this purpose, potentially giving the processor distinct 16-Mbyte code and data spaces.

Writes should clearly go to the data area; you're not writing self-modifying code, are you? Reads are more problematic. It's easy to distinguish memory reads from fetches when the processor generates a fetch signal for every instruction byte. Some processors, such as the Z80, produce a fetch only on the read of the first byte of a multibyte operating code; subsequent ones all look the same as any data read. Forget trying to split the memory space if cycle types are not truly unique.

When such a space-splitting scheme is impossible, build an external mapper that translates address lines. However, avoid the temptation to simply latch upper address lines. It's easy to store A16, A17, et al, in an output port. Every time the latch changes, however, the entire program gets mapped out. There are awkward ways to write code to handle this problem, but adding a bit more hardware eases the software team's job.

Design a circuit that maps in and out just portions of the logical space. Look first at software requirements to see what hardware configuration makes sense. Every program needs access to a data area that holds the stack and miscellaneous variables. The stack must always be visible to the processor, so that calls and returns function. Some amount of "common" program storage should always be mapped in. The remapping code, at least, should be stored in program storage, so that the code doesn't disappear during a bank switch. Design the hardware so that these regions are always available.

Is the address space limitation due to an excess of code or of data? Perhaps the code is tiny, but a gigantic array requires tons of RAM. Clearly, you'll be mapping RAM in and out, leaving one area of ROM--enough to store the entire program--always in view. An obese program yields just the opposite design. In either of these cases, a logical-address space split into three sections makes the most sense: common code (always visible, containing runtime routines called by a compiler and the mapping code), mapped code or data, and common RAM (stack and other critical variables needed all the time).

For example, perhaps 0000 to 03FFF is common code, and 4000 to 7FFF is banked code, which, depending on the setting of a port, could map to almost any physical address. Then, 8000 to FFFF would be common RAM.

Sure, you can use heroic programming to simplify the hardware. It's a mistake, though, because the incremental parts cost is minuscule compared to the increased bug rate implicit in any complicated bit of code. It is possible--and reasonable--to remove one bank by copying the common code to RAM and executing it there, using one bank for common code and data.

It's easy to implement a three-bank design. Suppose addresses are arranged, as in the previous example. A0 to A14 go to the RAM, which is selected when A15=1. Turn ROM on when A15 is low. Run A0 to A14 into the ROM. Assuming you're mapping a 128k×8-bit ROM into the 32-kbyte logical space, generate a fake A15 and A16 (simple bits latched into an output port) that go to the ROM's A15 and A16 inputs. However, feed these through AND gates. Enable the gates only when A15=0 (RAM off) and A14=1 (bank area enabled).

You select RAM with logical addresses between 8000 and FFFF, because any address lower than 4000 disables the gates and enables the first 4000 locations in ROM. When A14 is a one, the values you've stuck into the fake A15 and A16 select a 4000-byte-long chunk of ROM. The virtue of this design is its simplicity and its conservation of ROM. It wastes no memory chunks, a common problem with other mapping schemes.

Occasionally, a designer directly generates chip selects instead of extra address lines from the mapping output port. I think this is a mistake, because it complicates the ROM-select logic. Worse, it's sometimes hard to make your debugging tools understand the translation from addresses to symbols. By translating addresses, you can provide your debugger with a logical-to-physical-translation cheat sheet.

When you use assembly language, you control everything, so handling banked memory is not too difficult. The hardest part of designing remappable code is figuring out how to segment the banks. Casual calling of other routines is out, because you dare not call something that is not mapped in. Some folks write a bank manager that tracks which routines are in the logical space. All calls, then, go through the bank manager, which dynamically brings routines in and out as needed.

If you are foresighted enough to design your system around a real-time operating system (RTOS), managing the mapper is much simpler. Assign one task per bank. Modify the context switcher to remap whenever a new task is spawned or reawakened. Many tasks are small--much smaller than the logical banked area. Use memory more efficiently by giving tasks two banking parameters: the bank number associated with the task and a starting offset into the bank. If the context switcher both remaps and then starts the task at the given offset, you can pack multiple tasks per bank.

Some C compilers come with built-in banking support. Check with your vendor. Some completely manage a multiple-bank system, automatically remapping as needed to bring code into and out of the logical-address space. Figure on making a few patches to the supplied remapping code to accommodate your unique hardware design.

In C or assembly, with or without an RTOS, be sure to put all of your interrupt-service routines and associated vectors into a common area. Put the banking code there, as well, along with all frequently used functions. When using a compiler, put the entire runtime package into unmapped memory.

As always, when designing the hardware, carefully document the approach you select. Include this information in the banking routine, so that some poor soul several years in the future has a fighting chance to figure out what you've done.


Jack Ganssle is president of Softaid, a vendor of emulators and other embedded-systems tools. He can be contacted via Compuserve at "76366,3333," or via Internet at jack@softaid.net Send mail c/o Softaid, 8310 Guilford Road, Columbia, MD 21046.

Jack's Home Page


| EDN Access | feedback | subscribe to EDN! |
| design features | design ideas | columnist |


Copyright © 1995 EDN Magazine. EDN is a registered trademark of Reed Properties Inc, used under license.