The last chapter introduced every possible way to allocate memory from RAM; we’ll now deal with another kind of memory that can be present in the computer: memory on expansion boards. Peripherals do have memory on them. Display boards host a frame buffer, video grabbers hold grabbed data, and an Ethernet interface might host received packets in a memory region; additionally, most peripheral boards have some on-board ROM that must be executed by the processor at system boot. All such entities are ``memory,'' in that the processor accesses them through memory instructions. I am limiting the discussion here to the ISA and PCI devices, as they are the most used nowadays.
There are three common kinds of peripheral memory on standard x86 computers: ISA memory in the 640KB-1MB range, ISA memory in the 14MB-16MB range, and PCI memory above the end of physical memory. The addresses used above are physical addresses, the numbers that travel in the computer’s address bus, and they have nothing to do with the virtual addresses used by program code (see Section 7.3 in Chapter 7). The physical location where I/O memory lives is mainly an historical heritage, as explained later when the three memory ranges are introduced.
Unfortunately (or fortunately, if you prefer good architectural design to easy portability), not every Linux platform supports ISA and PCI; this section is limited to the discussion of those that do.
I’ve already introduced an ``easy'' (and broken) way to deal with such memory in Section 2.5.2 in Chapter 2, where I showed how pointers holding the physical hardware address dereference correctly to the requested I/O memory. Though this technique works on the x86 platforms, it is not portable to all Linux platforms. Pointer dereferencing might be a good choice for small and short-lived projects, but it’s not recommended for a production driver.
The recommended interface to I/O memory was introduced
into the kernel during
the 1.3 development tree and is missing from older kernels. The
sysdep.h
header released with the short sample code,
however, implements the new semantics for kernel versions back to 1.2.
The new interface consists of a set of macros and function calls that are used in place of pointer dereferencing whenever you need to access I/O memory. Such macros and functions are portable; this means that the same source code will compile and run on different architectures, as long as they host the same peripheral bus.
Most of these macros currently expand to pointer dereferencing when you compile code on your Intel-based PC, but their internal behavior could well change in the future. One such change happened, for example, in the initial switch from 2.0 to 2.1, when Linus decided to change the virtual memory layout. With the new layout, ISA memory can’t be accessed in the old-fashioned way described in Chapter 2.
The new interface to I/O memory consists of the following functions:
unsigned readb(address);
,
unsigned readw(address);
,
unsigned readl(address);
These macros are used to retrieve 8-bit, 16-bit, and
32-bit data values from I/O memory. They are missing from
Linux 1.2. The advantage of using macros is the typelessness of
the argument: address
is cast before being used,
because the value ``is not clearly either an integer or a
pointer, and we will accept both'' (from
asm-alpha/io.h
). Neither the reading nor the writing
functions check the validity of address
, as they are
meant to be as fast as pointer dereferencing (we
already know that sometimes
they actually expand into pointer dereferencing).
void writeb(unsigned value, address);
,
void writew(unsigned value, address);
,
void writel(unsigned value, address);
Like the previous functions, these functions (macros) are used to write 8-bit, 16-bit, and 32-bit data items.
memset_io(address, value, count);
When you need to call memset on I/O memory, this function does what you need, while keeping the semantics of the original memset.
memcpy_fromio(dest, source, nbytes);
,
memcpy_toio(dest, source, nbytes);
These functions move
blocks of data to and from I/O memory and behave like
memcpy_tofs. They have been introduced together with
the functions above, and they are missing in Linux 1.2. The
sysdep.h
header distributed with the sample drivers
fixes the version dependency of the functions and defines
them for any kernel from 1.2 on.
The portability of these functions across the supported architectures is currently limited, like that of the port I/O functions. The functions are completely missing from some platforms; on some they are macros that expand to pointer operations, and on others they are real functions.
People like me, accustomed to the flat memory model of our old PCs, might hesitate to bother with a new interface just to access ``a region of physical addresses.'' Actually, getting comfortable with the interface is simply a question of getting some practice using the functions. There’s nothing better for gaining confidence than looking at a silly module that just accesses I/O memory. The module I’m going to show you is actually called silly, short for ``Simple Tool for Unloading and Printing ISA Data.''
The module features four device nodes that perform the same
task using different data transfer functions. The
silly devices act as a window over I/O memory, in a
way similar to /dev/mem
. You can read and write data,
lseek to an arbitrary I/O memory address, and
mmap the region to your process (see Section 13.2 in Chapter 13).
/dev/sillyb
, featuring minor number 0, accesses I/O memory
with readb and writeb. The following code shows the
implementation for read, which remaps the address range
0xA0000-0xFFFFF to file offset 0-0x5FFFF. The read function is
structured as a switch
statement over the different access
modes; here is the sillyb
case
:
case M_8: while (count) { *ptr = readb(add); add++; count--; ptr++; } break;
The next two devices are /dev/sillyw
(minor number 1) and
/dev/sillyl
(minor number 2). They act exactly like
/dev/sillyb
, except that they use 16-bit and 32-bit functions.
Here’s the write implementation of sillyl
, again part
of a switch
:
case M_32: while (count >= 4) { writel(*(u32 *)ptr, add); add+=4; count-=4; ptr+=4; } break;
The last device is /dev/sillycp
(minor number 3),
which uses the
memcpy_*io functions to perform the same task. Here’s the core
of its read implementation:
case M_memcpy: memcpy_fromio(ptr, add, count); break;
Some ISA device boards carry on-board memory that is mapped to the physical addresses between 14MB and 16MB. These devices are slowly disappearing, but it’s worth introducing how their memory range can be accessed. This discussion, however, only applies to the x86 architecture; I’ve no information on how the Alpha or other architectures behave with such ISA boards.
In the old days of the 80286 processor, when the physical address space was 20 bits wide (16MB) and all the address lines were present on the ISA bus, almost no computer carried more than 1 or 2 megs of RAM. Why couldn’t the expansion board steal some high memory addresses for its buffer? This idea is not new; it’s the same concept that led to ISA memory below 1M, and it was later recycled to implement high PCI buffers. The chosen address range for ISA device boards was the top two megs, though most boards just use the top meg.
Nowadays, there are still a few motherboards that can host these old-fashioned boards even when there are more than 14 megs of physical RAM. Correctly handling this memory requires you to play games with the address ranges to avoid ending up with overlapping RAM and bus addresses.
If you have an ISA device with high memory, and you’re unlucky enough to have less than 16MB RAM, managing the memory is easy. Your software should behave as though it had a high PCI buffer (see the next section), except that it will be slower, because ISA memory is slow.
If you have an ISA device with high memory and you have 16MB or more, then you’re in deep trouble.
One possibility is that your motherboard doesn’t correctly support the ``ISA hole.'' In that case, there’s nothing you can do to access the on-board memory except change the board or remove some RAM. If, on the other hand, the motherboard handles the ISA hole, you still need to tell the Linux kernel about such memory and do some work to be able to access the rest of your RAM (the range over 16M).
The place where you need to do some hacking to correctly reserve the
high ISA memory, while not losing access to the remaining RAM, is in the
map of the computer’s physical memory. This map is built in
arch/i386/mm/init.c
, within the mem_init function.
The array mem_map
holds the relevant information about each memory page; if the bit
PG_reserved
is set for a page, the kernel won’t use that page for
normal paging activity (i.e., the page is ``reserved'' and can’t be
touched). Reserved pages can nonetheless be used by drivers; the
range between 640KB and 1MB is marked as ``reserved,'' but it hosts usable
device memory.
The following code, inserted in mem_init, correctly reserves the memory space between 15MB and 16MB:
while (start_mem < high_memory) { if (start_mem >= 0xf00000 && start_mem < 0x1000000) { /* keep it reserved, and prevent counting as data */ reservedpages++; datapages--; } else clear_bit(PG_reserved, &mem_map[MAP_NR(start_mem)].flags); start_mem += PAGE_SIZE; }
Initially all the memory is marked as ``reserved,'' and the lines
shown above take care not to unreserve high I/O memory; the original
code only has the else
branch within the loop shown. Since
every reserved page after the end of the kernel code is counted as
kernel data, two counters are modified to prevent a mismatching
message at boot time. My box, with 32MB and the previous code to
access the ISA hole, reports:
Memory: 30120k/32768k available (512k kernel code, 1408k reserved, 728k data)
I tested this code with my own Intel box (ISA-hole aware) with kernel 2.0.29. If you are running a different kernel version, you might need to tweak the code--the internal structures related to memory management changed slightly in 2.1 and were different in version 1.2 of the kernel. Hacking with kernel code is unavoidable when you’re supporting old-fashioned (and sometimes badly designed) hardware.
Accessing high PCI memory is much easier than accessing high ISA memory. High memory on PCI boards is really high--higher than any reasonable physical RAM address (at least for the next few years).
As discussed in Section 7.3 in Chapter 7, a single call to vremap (ioremap with kernel 2.1) is all it takes to access this memory. If you want to be portable across platforms, however, you should access the remapped memory range only through readb and similar functions. This restriction applies because not all platforms are able to directly map PCI buffers in the processor’s address space.
3.140.188.16