Memory mapped IO - ylab-nsu/cdm16-llvm-neo GitHub Wiki
In this guide, you will learn how to access memory-mapped I/O registers in C.
Prerequisites
This guide assumes that you have a project set up as described in Setting up a project.
Introduction
One of the easiest ways to implement input and output is via memory-mapped registers. With this approach, a set of memory addresses (usually a contiguous range) is reserved for I/O device registers. Reading from or writing to these addresses will result in an input or output operation, respectively.
In this guide, we will control a Logisim UART with its registers mapped to two bytes of the address space.

| Name | Address | Width (bytes) | Access | Description |
|---|---|---|---|---|
| Flags | 0x10d |
1 | Read | Status flags |
| RX | 0x10e |
1 | Read | Receive queue |
| TX | 0x10e |
1 | Write | Transmit queue |
Writing to the TX register puts a byte into the transmit queue. Reading from the RX register removes a byte from the receive queue (reads are destructive). Bit 0 (LSB) of the flags register indicates that a telnet connection is established. Bit 1 of the flags register indicates that there are bytes available in the receive queue.
Binding a symbol to an absolute address
We are going to bind the symbol UART to the base address of the UART component. UART+0 will be the flags register, and UART+1 will be RX/TX. We will use this symbol as a global variable in C. The steps to do this vary depending on the target used (see LLVM targets).
Cocas target (--target=cdm-cocas)
On the Cocas target, we are going to use a dummy absolute section at the address of our UART component. UART is going to point at the beginning of this section.
Create a new file named uart.asm in src with the following contents:
asect 0x10d
UART>
ds 2
end.
We created an absolute section at address 0x10d. It must take up exactly two bytes to cover both registers. The contents of these bytes don't matter, since they will be overridden by our I/O registers. Thus, we declared the section to contain an empty 2-byte space with ds 2.
Add the assembly file to ASM_SOURCES in the Makefile:
C_SOURCES := # <existing C sources>
ASM_SOURCES := uart.asm # <existing ASM sources>
ELF target (--target=cdm)
Absolute sections (asect) are not supported in ELF. All sections act as relocatable sections (rsect). You cannot set the load address of a section in assembly. Instead, section placement is controlled by linker scripts.
The toolchain provides a default linker script that we can extend. This script recognizes two special symbols that specify the address range the linker will use for regular sections: RAM_ORIGIN and RAM_LENGTH. By default, RAM_ORIGIN is set to the end of the interrupt vector table (0x100), and RAM_LENGTH is set to the maximum possible value. These symbols can be used to reserve an address range at the start and/or the end of the address space. We can also directly assign values to symbols in linker scripts.
Create a new file named memory.ld in the project root directory. Move the RAM origin to 0x110 to avoid the reserved addresses, and assign address 0x10d to UART:
RAM_ORIGIN = 0x110;
UART = 0x10d;
In the Makefile, add memory.ld to the variable LD_SCRIPTS, which holds the list of linker scripts passed to Clang alongside object files during the final linking stage.
LD_SCRIPTS := memory.ld
[!NOTE] The linker script is intentionally passed to Clang without
-Tto tell the driver to treat it as an additional linker script, augmenting the default script without replacing it. If you want to replace the default linker script with a custom one, add it as to the list of prerequisites of$(TARGET_BINARY)and modify the recipe. Use$(C_OBJECTS) $(ASM_OBJECTS)instead of$^and pass your linker script to$(CC)with-T.
C code
Now we can access the symbol from C or assembly and use it to read from and write to the registers. We are going to implement a standard setup with a device structure and a global variable in C.
Create a new header named uart.h in src. Declare a struct type for the UART registers and an extern global variable of this type with the same name as our symbol - UART.
typedef struct {
const volatile char flags;
volatile char rx_tx;
} uart_t;
extern uart_t UART;
Here, the fields of the struct are declared volatile to prevent the compiler from optimizing away reads and writes to them. We want each access to occur exactly as written, without the compiler reordering, combining, or eliminating any read or write. The flags field is declared const because writes to the corresponding address are not supported.
Finally, create a helper function to output a string to the UART.
Declare the prototype in src/uart.h:
void uart_putstr(const char *str);
Create a new source file named uart.c in src and add it to C_SOURCES in the Makefile. Implement the function as follows:
#include "uart.h"
void uart_putstr(const char *str) {
for (const char *ch = str; *ch != '\0'; ch++) {
UART.rx_tx = *ch;
}
}
[!NOTE] Since the Logisim UART component features an infinite internal transmit buffer, we do not need to poll the flags register for transmitter readiness before each write operation. This is necessary for real UART components, because thay have finite buffers that can overflow.
Now you can call this function to print strings to the UART. Reading can be implemented in a similar fashion.