
Cocojunk
🚀 Dive deep with CocoJunk – your destination for detailed, well-researched articles across science, technology, culture, and more. Explore knowledge that matters, explained in plain English.
Bank-switching
Read the original article here.
The Forbidden Code: Underground Programming Techniques They Won’t Teach You in School
Chapter X: Busting Through the Memory Wall - The Art of Bank-Switching
Welcome, fellow traveler on the path of "The Forbidden Code." In the clean, well-lit classrooms of modern computer science, you're taught about vast, seemingly infinite memory spaces, managed effortlessly by the operating system. But before this luxurious era, pioneers of programming faced a fundamental, ironclad limitation: the CPU could only "see" a tiny fraction of the memory that hardware designers wanted to provide.
How did they break free? Not with complex memory managers or virtual addressing (those came later), but with a clever, hardware-bending trick: bank-switching. This technique, essential for squeezing large programs and data into constrained systems, is a prime example of the ingenuity required when resources are scarce – a skill often overlooked in today's world of abundance.
Definition: Bank-Switching
Bank-switching is a computer memory management technique used to increase the amount of addressable memory beyond the limitations of the CPU's address bus. It works by swapping different physical "banks" of memory into a fixed portion of the CPU's address space, effectively allowing the CPU to access more memory than it can directly address at any single moment.
The Problem: The Shackles of the Address Bus
To understand bank-switching, you first must grasp the problem it solves: the limited reach of the CPU's address bus.
Definition: Address Bus
The address bus is a set of parallel electrical lines used by the CPU to specify the physical memory location (or I/O device) it wants to read data from or write data to. The number of lines determines the maximum number of unique locations the CPU can directly address.
Early CPUs, built with simpler and fewer transistors, had narrow address buses. A common size was 16 bits.
- An 8-bit address bus can directly address 2⁸ = 256 unique locations (usually bytes). A mere 256 bytes!
- A 16-bit address bus can directly address 2¹⁶ = 65,536 unique locations. This is 64 kilobytes (64KB).
Definition: Address Space
The address space is the total range of memory addresses that a CPU can generate. This space is a contiguous range of numbers (from 0 up to 2^N - 1, where N is the width of the address bus). Physical memory (RAM, ROM) and I/O ports are "mapped" into this address space, meaning they occupy specific address ranges within it.
Definition: Memory Mapping
Memory mapping is the way physical memory (RAM, ROM chips) and hardware devices (like I/O ports, video memory) are assigned specific address ranges within the CPU's address space. When the CPU accesses a particular address, the hardware decodes that address to select the appropriate memory chip or device.
In systems with a 16-bit address bus, the CPU's entire "view" of memory was limited to 64KB. This 64KB address space had to accommodate everything: the program code (often stored in ROM), variable data (in RAM), the stack, and even hardware registers and I/O ports.
As software grew more complex – games needed more levels and graphics, applications needed more features – 64KB quickly became a crippling limitation. Developers had megabytes of ROM available on cartridges or expansion boards, but the CPU simply couldn't "see" it all at once.
This is where the "forbidden" art of bank-switching comes in. It's about tricking the CPU into accessing more physical memory than its address bus should allow.
The Solution: Swapping Memory Banks
The core idea of bank-switching is straightforward:
- You have more physical memory (RAM or ROM) than can fit into the CPU's address space window.
- You divide this larger physical memory into distinct chunks, called banks.
- You reserve a specific portion of the CPU's address space as a window (or page frame). This window is where you will make different banks appear.
- You implement hardware (often simple logic gates or dedicated chips) that, based on a control signal, maps a specific physical bank into that address space window.
- Your software controls this mapping by sending the appropriate control signal, typically by writing a value to a dedicated I/O port or a memory-mapped control register.
Definition: Memory Bank
A memory bank is a segment or block of physical memory (either RAM or ROM) that can be individually selected and mapped into a portion of the CPU's address space during bank-switching. You have multiple banks, but only one or a few can be active (mapped into the address space window) at any given time.
Definition: Memory Window (or Page Frame)
The memory window (or page frame) is a fixed range of addresses within the CPU's address space. This is the region where different memory banks are swapped in and out. The CPU accesses addresses within this window, but the physical memory accessed changes depending on which bank is currently switched in.
Definition: I/O Port
An I/O (Input/Output) port is a specific address used by the CPU to communicate directly with hardware devices outside of the main memory. Writing a value to an I/O port sends data or commands to the device, and reading from an I/O port receives data or status from the device. In bank-switching, an I/O port is often used as the control mechanism to select which bank is currently active.
Imagine you have 256KB of ROM on a game cartridge, but your CPU only has a 64KB address space. You could divide the ROM into four 64KB banks (Bank 0, Bank 1, Bank 2, Bank 3). Let's say you dedicate the address range from $8000 to $FFFF (32KB) in the CPU's address space as your bankable window. You could then design hardware such that:
- Writing
0
to I/O port $FE maps the first 32KB of Bank 0 into $8000-$FFFF. - Writing
1
to I/O port $FE maps the first 32KB of Bank 1 into $8000-$FFFF. - Writing
2
to I/O port $FE maps the first 32KB of Bank 2 into $8000-$FFFF. - Writing
3
to I/O port $FE maps the first 32KB of Bank 3 into $8000-$FFFF.
By writing to port $FE, your program can instantly swap which 32KB chunk of physical ROM is accessible in the $8000-$FFFF window.
The Fixed/Common Area
Crucially, not all of the CPU's address space is typically bankable. There's usually a portion that is always mapped to the same physical memory, regardless of the current bank selection. This is called the fixed or common area.
Why? Because the CPU needs constant access to things like:
- Interrupt vectors (low memory addresses)
- The stack (often high memory addresses)
- Frequently used code routines that might be called from multiple banks
- Variables or data structures needed across bank switches
- The bank-switching control logic itself (the I/O port or register).
Without a common area, managing jumps between code in different banks becomes a nightmare. You'd need a small routine in each bank to switch to another bank, and that routine would need to be in memory somehow. A fixed area provides a stable base for essential code and data.
Implementing Bank-Switching in Code
Mastering bank-switching means carefully structuring your program and data to account for the memory layout. Code residing in the fixed area can call routines or access data in any bank (after switching). Code within a bank can easily access other code and data within the same bank or in the fixed area.
However, code within one bank cannot directly jump to code or access data in another bank without first performing a bank switch.
Here's a conceptual look at the process:
; Assume:
; - I/O port $FE controls bank selection
; - Window is at $8000-$FFFF
; - Fixed area is $0000-$7FFF
; We are currently running code in Bank 0
; Need to access data in Bank 3 located at physical address 0x6000 (relative to start of Bank 3)
; This location would appear at address $8000 + (0x6000 % 0x8000) = $E000 if Bank 3 was mapped in.
; 1. Save current state (registers, etc.) if necessary
; (For simplicity, assume current bank is not critical for this example)
; 2. Switch to the desired bank (Bank 3)
LDA #3 ; Load the value for Bank 3 into accumulator
STA $FE ; Write it to the bank-switching I/O port
; Now, physical Bank 3 is mapped into the window $8000-$FFFF
; 3. Access the data in the window
; The data we want (physical 0x6000 in Bank 3) is now at address $E000 in the CPU's address space
LDA $E000 ; Load data from the window address
; Use the loaded data...
; 4. (Optional but often necessary) Switch back to the original bank (Bank 0) if needed
LDA #0 ; Load the value for Bank 0
STA $FE ; Write it to the bank-switching I/O port
; Now, physical Bank 0 is mapped back into the window $8000-$FFFF
; Continue execution...
This simple example hides complexity. What if the code that needs to access Bank 3 is itself in a different bank (say, Bank 1)? It would first need to be in the fixed area, or call a helper routine in the fixed area, or carefully manage the bank switch before the access instruction.
Libraries and tools for systems using bank-switching often included linker scripts and memory management helpers to assist programmers in organizing code and data across banks and generating the necessary switching logic.
Where Bank-Switching Was a Secret Weapon
This technique wasn't just theoretical; it was the backbone of memory expansion in many classic systems:
- Nintendo Entertainment System (NES) / Famicom: The NES CPU (a variant of the 6502) had a 16-bit address bus (64KB). Game cartridges used sophisticated Memory Management Controllers (MMCs), which were essentially custom bank-switching chips. These MMCs allowed cartridges to hold many megabytes of ROM (for game code, graphics, sound data) and even expand RAM, far exceeding the CPU's direct addressability. Different MMCs used various bank sizes (e.g., 8KB, 16KB, 32KB) and window configurations. This is perhaps the most famous example of extensive, cartridge-based ROM bank-switching.
- Commodore 64: While the C64's 6510 CPU also had a 64KB address space, the system had 64KB of RAM plus multiple ROM chips (KERNAL, BASIC, Character ROM). Bank-switching was used to swap these ROMs in and out of the address space, and also to map in I/O devices or extra RAM/ROM from cartridges or expansions.
- Atari 2600: Late in its life, to support larger games, cartridges adopted bank-switching. This allowed games much larger than the original 4KB limit.
- Apple II: Expansion cards could add more RAM (up to 1MB!) to Apple II systems, which had a 64KB address space. This extra RAM was accessed via bank-switching techniques, often using memory-mapped registers to control the banks.
- Early IBM PC (via EMS): The original IBM PC and XT had a 1MB address space (using a 20-bit address bus), but only 640KB was typically available for user programs and data (the rest was reserved for video memory, ROM BIOS, etc.). To access more RAM (up to 8MB), the Lotus-Intel-Microsoft Expanded Memory Specification (LIM EMS) was developed. EMS used dedicated hardware on expansion cards to bank-switch 16KB "pages" of expanded RAM into a 64KB "page frame" within the reserved upper memory area of the 1MB address space. This was bank-switching applied to RAM to overcome the 640KB DOS limit.
- Embedded Systems: Many modern microcontrollers, especially smaller, lower-cost ones, still have limited address spaces. Bank-switching (often called "paging") is used to access larger external Flash memory chips containing application code, configuration data, fonts, or graphical assets that don't fit into the CPU's direct reach.
Advantages and Disadvantages
Bank-switching was a brilliant workaround, but it came with its own set of challenges:
Advantages:
- Overcame Address Space Limitations: The primary benefit was enabling systems to use much more memory than the CPU could natively address.
- Relatively Simple Hardware: Compared to implementing a full Memory Management Unit (MMU) with virtual memory, the hardware required for basic bank-switching was relatively cheap and simple, making it suitable for cost-sensitive systems like game consoles and early home computers.
- Utilized Cheaper Memory: Allowed systems to take advantage of larger, cheaper memory chips even if the CPU wasn't designed for them.
Disadvantages:
- Software Complexity: The burden of managing memory fell directly on the programmer. Code had to be carefully designed, often split across banks, and the bank-switching logic had to be explicitly handled whenever accessing cross-bank resources. This was significantly more complex than programming on systems with a flat, larger address space.
- Performance Overhead: Switching banks takes time (CPU cycles) to write to the control port. While often fast relative to other operations, frequent switching could impact performance.
- Non-Uniform Memory Access: Not all memory was equally accessible at all times. This made common programming patterns like using pointers to arbitrary memory locations difficult or impossible without careful bank management. Jumps and subroutine calls across banks required special care or helper routines in the fixed area.
- Hardware Dependence: The implementation was highly specific to the hardware. Code written for bank-switching on an NES MMC1 would be completely different from code for a C64 or an Apple II EMS card. This lack of standardization added to the "underground" nature – you had to know the specific hardware secrets.
The Legacy: Why You Might Not Learn This Today
In mainstream computing, bank-switching, as described here, is largely obsolete. Modern CPUs boast 32-bit or 64-bit address buses, capable of addressing gigabytes or even petabytes of memory directly. Complex Memory Management Units (MMUs) handle memory protection, paging, and virtual memory, abstracting the physical memory layout from the programmer. The operating system and hardware work together to give each process the illusion of a large, contiguous address space, eliminating the need for manual bank management in application software.
However, the principles of dealing with limited resources and mapping larger data stores into smaller address windows remain relevant in certain niches, particularly in embedded systems where low-cost, low-pin-count microcontrollers are used. Understanding bank-switching provides a valuable historical perspective on how fundamental hardware limitations were overcome and highlights the trade-offs between hardware simplicity and software complexity.
Think of it as a powerful, albeit tricky, tool from the past. They might not teach you how to manually swap memory banks in your university course on operating systems, but knowing why it existed and how it worked reveals a fascinating chapter in the history of computing and the clever lengths programmers went to bend hardware to their will. It's a true "forbidden" technique – born of necessity, replaced by abstraction, but forever etched in the lore of squeezing every drop of potential from limited machines.