This howto presents KESO's mechanisms for accessing memory mapped device registers from Java code. This is mostly needed for writing device drivers. This guide does not cover devices that require special CPU instructions for being programmed. Such accesses require direct usage of the KNI (Keso Native Interface), unless an API is already available in KESO for your target platform.
KESO provides two mechanisms for accessing raw memory locations:
Memory objects and
Memory mapped objects.
Memory objects are basically equivalent to RawMemoryAccess as specified by the
Real-time Specification for Java (RTSJ), although the API of KESO's Memory
class is slightly different. Memory-mapped Objects allow the layout of a memory
region to be specified by a Java class definition, similar as one would do
using a structure definition in the C programming language.
For both abstractions, the raw memory area can for safety reasons only be accessed
using primitive data types. It is not possible to read or write reference values to
such a memory area. In KESO, the abstractions of Memory objects and memory-mapped
objects are also used for accessing special memory areas that are managed by the
runtime system. To distinguish from this type of raw memory areas, you may sometimes
also encounter the term Device Memory when speaking of memory areas that are
not managed by the runtime, such as memory-mapped device registers that reside
at fixed addresses in the address space.
We will present the device memory abstractions at the example of a simple device driver for the
GPIO ports of an 8-bit AVR processor. For a better understanding, we briefly explain the core
registers used to configure one of the AVR's GPIO ports.
Each of the AVR's GPIO ports consists of 8 pins that can independantly be used as input or
output pins. The core operations on each port are done through three 8-bit registers, that are
mapped into the address space and can be accessed using normal memory access operations:
Data direction register DDRA, address 0x1a
This register is used to configure the direction (input or output) for each of the port's pins.
Setting bit X in this register configures pin X as output, clearing it configures the pin as input.
Input pins DDRA, address 0x19
This read-only register can be used to determine the level of a pin. It is normally used to determine
the state of input pins.
Data register DDRA, address 0x1b
For output pins, writing to this register sets the level of the pin. Setting bit X in this register
sets a high level at pin X, clearing the bit a low level. For input pins, setting bit X in this register
activates the pull-up resistor for pin X.
In our example, we will use a single of these Ports, PortA. The register names for this port all have the
suffix A appended.
The figure shows the memory areas of interest for the example. Normally, a Java application
can only access the managed memories of its domain, i.e. the stack of the current thread,
the static fields of the domain and objects on the heap of the domain. The three port
configuration registers that we need to access in order to write a device driver for PortA
are outside these managed area, hence there is no way for our Java code to gather access
to these registers without using special mechanisms provided by the Java runtime.
Memory objects themselves are regular objects that reside on the heap of a domain or in
immortal memory. These objects however internally reference a memory region outside the
regular managed memory areas of the domain. Such a raw memory region is defined by
its start address in memory and its size. For our example, we need access to the memory
region at address 0x19 of size 3 (bytes).
The Memory object in addition provides methods for reading and writing primitive values
of varying sizes to the referenced area, and convenience methods for directly performing
certain bit operations on a value within the referenced area. Details are available in
the API documentation of the Memory
class.
All accesses to the memory area require an explicit offset into the area. Similar to
Java array accesses, these offsets need to be bounds checked for safety reasons.
Creating a new Memory object for accessing a region of device memory
To create a new Memory object that provides access to a device memory region you need
to use the methods of the MemoryService
class provided for this purpose, allocStaticDeviceMemory()
or allocDynamicDeviceMemory(), both of which are provided
the start address and the size of the desired memory area as parameters. The static variant creates
a Memory object in immortal memory for that call site of the allocStaticDeviceMemory().
Subsequent invocations at the same call site will always return the same Memory object, with the referenced memory
region adjusted to the most recently provided parameters. The second variant will dynamically allocate the
Memory object on the heap of the current domain. If the first variant fulfills your needs, it should be
preferred as it is more efficient.
Driver using Memory objects
You should now know enough to write a driver class for the above GPIO port. The driver
class could look as follows:
import keso.core.*;
public final class PortA {
// base address
private static final int BASE=0x19;
// offsets
private static final int PIN =0;
private static final int DDR =1;
private static final int PORT=2;
private static final Memory regs = MemoryService.allocStaticDeviceMemory(BASE, 3);
public static void setMode(int pin, boolean isOutput) {
if(isOutput) {
regs.or8(DDR, (1<<pin));
} else {
regs.and8(DDR, ~(1<<pin));
writePin(pin, true); // activate pull-up resistor
}
}
public static void writePin(int pin, boolean level) {
if(level) regs.or8(PORT, (1<<pin));
else regs.and8(PORT, ~(1<<pin));
}
public static boolean readPin(int pin) {
return (regs.get8(PIN) & (1<<pin)) != 0;
}
}
In the example, the driver class is purely static. Since AVRs contain multiple equivalent
such ports that merely defer in the start address of the registers, a real-world example
would probably be non-static and allow instantiation using varying base addresses.
Memory objects are inconvenient to use in many simple cases and require runtime bounds
checks. While dynamically computed offsets can make Memory objects a flexible and
powerful mechanism for more complex scenarios, this flexibility is not needed in many
simple cases such as our AVR port example.
KESO provides a second abstraction called memory-mapped objects for accessing raw memory
regions. This mechanism allows the layout of a memory region to be defined using a
standard Java class definition.
Defining a Class used as template for creating memory-mapped objects
Defining a class to be used for creating memory-mapped objects is basically
equivalent to defining a regular Java class. The class may contain regular
(not mapped) fields and methods, but in addition it contains mapped fields.
When defining such a class, the class needs to implement the
MemoryMappedObject
marker interface. To describe the layout of the raw memory area that should be accessed,
one simply defines fields in the class using the special memory types from package keso.core,
all subclasses of
keso.core.MT
carrying the prefix MT_. The chosen memory type determines the size occupied by
the respective field in the memory area, the position of the field definition in the class relative to other mapped field definitions
determines the offset. An example for such a memory type is
MT_U8,
which represents an unsigned byte value. The use of special types rather than Java's standard types for primitive
data fields allows the class to also contain regular heap fields.
The operations provided by the memory types are similar to that provided by Memory objects, however, the size
of the target value is defined by the memory type, thus there is only one variant of each operation. In addition,
the offsets of the values are implicitely given by the position of the field in the class definition, therefore it
is not required as a parameter to these methods and no runtime bound check is needed.
Creating a new memory-mapped object
Memory-mapped objects need to be created using services of the MemoryService.
You can either directly create a new memory-mapped object using a base address for the mapping,
or you can use an existing Memory object as the basis for this mapping. In the latter case, a
bound check will be performed when creating the mapping to ensure that the Memory object references
an area of sufficient size.
To directly create a memory-mapped object, use the
mapStaticDeviceMemory() service,
for creating the object on the basis of an existing Memory object, use the
mapMemoryToStaticObject() service.
Driver using Memory-mapped objects
Back to our example, a driver using memory-mapped object could look as follows:
package driver.avr;
import keso.core.*;
public final class AVRPort implements MemoryMappedObject {
private MT_U8 PIN; // offset 0
private MT_U8 DDR; // offset 1
private MT_U8 PORT; // offset 2
public void setMode(int pin, boolean isOutput) {
if(isOutput) {
DDR.setBit(pin);
} else {
DDR.clearBit(pin);
writePin(pin, true); // activate pull-up resistor
}
}
public void writePin(int pin, boolean level) {
if(level) PORT.setBit(pin);
else PORT.clearBit(pin);
}
public boolean readPin(int pin) {
return PIN.isBitSet(pin);
}
// provide a mapped object for using PortA at address 0x19
public static AVRPort getPortA() {
return (AVRPort) MemoryService.mapStaticDeviceMemory(0x19,"driver/avr/AVRPort");
}
}
As described above, the class needs to implement the
MemoryMappedObject
marker interface. The class contains three mapped fields,
PIN,
DDR, and
PORT, in the order that they
appear in the address space. The methods provided are the same as in the
Memory object variant, except that we now need to use instance methods since
we need to access the mapped fields of a memory-mapped instance of the class.
The methods show that accessing the registers is now more convenient, since
we can directly access them using their field name. Since memory types appear
to be a non-primitive data type on the Java language level, all accesses look
like methods calls on the mapped field - in the generated C code, these will
be downcompiled to direct accesses.
Finally, the last method provides a mapped instance of the class mapped to
the base address of PortA's registers.