porting - how to port GeckOS onto a new architecture
Porting has been made much easier with this kernel version. In general, there is a proto architecture subdirectory that holds some prototype files for a non-MMU system. that can easily be changed to fit own needs.
- Note
-
the proto files are not fully maintained, so a maintained architecture should be used as basis if possible.
The kernel needs two files in the arch/*/kernel subdirectory, namely kenv.a65 and kinit.a65. The kinit.a65 file is included in the kernel startup code, i.e. after setting the interrupt flag and clearing the decimal flag this file is executed, and it is expected that the CPU gets out of this file at the end of it.
One of the first decisions to be made is the memory setup.
- Plain
-
I systems with up to 64k of memory (potentially after re-mapping some ROM), a plain memory map like in the C64 or the PET32k can be used.
- Partial map
-
Other options are with partially mapped memory, similar to the CBM8296 (which actually has two banks of RAM in the upper 32k only). In this case system tasks are managed in one mapped bank, with only zeropage, lib6502 and kernel in shared memory. The user programs are loaded into the second bank, that, together with the rest of the not-mapped RAM, makes up a larger linear address space. The kenv.a65 code must switch between the banks appropriately.
- Full map
-
In the CS/A machine, the memory can be re-mapped in blocks of 4k. This means every task can have its own zeropage and stack. The kenv.a65 code above must handle this as well. In this port, the memtask code for example re-maps all the MMU entries. During processing of the kernel calls, the kernel may even map task memory into the kernel space.
When porting, it may be good to start with a plain setup to get a grip on porting the devices and filesystems, before venturing into more complicated setups. The C64 implementation is a good start for that (see also build below).
The system manages multiple so-called environments. These are specific memory setups. In a plain mapping, there is only a single environment. In a full map, each task can have its own environment, like in the CS/A machine.
In a partial map system the situation can be more complicated. Typically in such systems there is more than 64k of memory available, that is split into multiple banks of 64k each. The system then has system-specific ways of letting the CPU access these banks of memory.
Two important areas are the start of memory with the zeropage, the stack (and the SENDBUF), and the end of memory with the interrupt vectors.
Interrupt vectors should typically be accessible in all banks / environments, as they are needed by the CPU when an interrupt occurs. The interrupt code then needs to call the kernel - either the kernel (including kernel variables) is available in all environments too, or, in banks other then where the kernel is, a small interrupt stub switches environments to kernel space. Note that the architecture-specific code (see below) needs to provide a memsys routine to handle entering the kernel not just from a task but also from an interrupt. Returning from an interrupt then needs to have at least the memtask routine available that allows to return to any bank / environment.
Zeropage, stack, and SENDBUF can / should be separate per environment - but they don’t need to be. In the CS/A machine every task has its own zeropage, stack and SENDBUF. In the CBM8x96 port zeropage, stack, and SENDBUF are shared by both environments available on this platform.
-
Sharing stack is done by the kernel. It reserves a certain amount of stack space for itself. In a STACKCOPY system the rest of the stack can be used per task, and is copied over to/from a separate memory are when tasks are switched.
-
Sharing zeropage is done by a) assigning a fixed area to kernel and lib6502 program and init rom programs, and then have lib6502 manage the rest of the zeropage itself. To be able to do this, the lib6502 needs variable space that is also shared between the environments, to store the zeropage allocation map. Note that the kernel maintains six locations in the zeropage, 2-7. Addresses 2/3 are a value that are stored per environment. Adresses 4/5 are values stored per task. Adresses 6/7 are stored per thread. The kernel replaces the value of these variables each time a thread / task / environment is switched (note: the environment values need to be taken care of in the architecture specific part). These variables are used by the lib6502 implementation to make sure it is re-entrant. If you don’t need lib6502, you can use these addresses yourself.
-
Sharing the SENDBUF is done by protecting it with the SEM_SENDBUF semaphore.
The kernel has code and data sections (but when loading it does not use the actual data in the data section), so for the kernel to work the kernel variables (data section) need to be available. So, for the kernel to work, it has to either run on a specific bank where the kernel variables reside, or has the kernel variables in memory shared across all banks. If the kernel is in a specific bank only, at least the interrupt handler, kernel jump table, and entry/exit routines (memtask and memsys) need to be available in every bank.
- Note
-
It is probably easier to have the kernel itself in all banks even if it runs in one bank only, as the kernel code does not (yet) have a specific separation of the code that is needed in all banks and the rest of the kernel code.
The lib6502 itself is re-entrant, and should be available in all environments. So, the lib6502 code can be in shared memory. lib6502 data maintains the zeropage allocation map, as well as the pointers to the environment’s specific main memory map (to be used for malloc and free). The lib6502 variable space (data section) is thus locked to the decision if zeropage and main memory are shared or not. If the zeropage is shared, there is only one main memory map, and other environments cannot use malloc and related calls. If the zeropage is not shared, the lib6502’s data section also needs to be in non-shared memory, and each environment can have its own dynamically allocated memory map.
- Note
-
Currently the lib6502 does not provide the ability to separately manage a shared zeropage and different main memory maps, or vice versa. A shared zeropage with different main memory allocation maps may be provided by overriding the location of the zeropage allocation map in lib6502/zmem.a65.
init-image-based (i.e. "rom"-based) programs are started by the kernel and the init process, and run directly in the memory where they are mapped during the initial memory setup. As they run from fixed memory locations and are not relocated, only a single instance can run at any one time. Also, they need their own variable space space too. Both init-image (ROM) and the variable space for those programs need to be accessible only from the specific environment where the init image programs are started.
-
Flat memory map: The C64 has a linear 64k address space, with an I/O windows at $dxxx. All of the memory is shared, there is only a single environment. The memory map starts with zeropage, stack, SENDBUF, and then all the variables for the ROM-based programs and lib6502. Then comes the freely allocatable memory, then the init image programs and lib6502. After that there is the I/O window at $dxxx, then screen buffers for multiple consoles, and finally the kernel up to the end of memory. (for more details see c64(p).
-
MMU: The CS/A machine has an MMU that allows to map any of the 16 4k blocks of CPU address space into any or 256 4k blocks of physical address space on the CS/A bus. There is an I/O window of 2k between $e800 and $efff. The top of memory is shared and contains the lib6502 and the kernel (with a window for the I/O address space). This area is mapped into any environment. Each task gets its own environment, with its own zeropage, stack, and SENDBUF. After this the lib6502 variable space is located, which is environment-specific. After that any application memory can be malloc’d. At the start each task (in its own environment) only gets mapped a total of 4k - the bottom block - of memory. Using the SBRK kernel call the lib6502 extends this memory as needed. The kernel has its own 4k memory block at the bottom that gets mapped in when a task enters the kernel (by call or by interrupt).
-
Multiple maps with shared zeropage/stack: The CBM8x92 has two environments, as it can replace the upper half of main memory with two banks of RAM. Also, the I/O window is only one page at $e8xx. The bottom half of memory is shared between the environments, including zeropage, stack, and SENDBUF. The memory starts with the shared zeropage, stack, and SENDBUF at the bottom, followed by the lib6502 code and (shared) lib6502 variables. In the first environment then comes the allocatable memory up to the kernel at the end of the kernel (the I/O window is mapped out of the way in this environment to maximise the available program memory). The other environment is unused from lib6502 space to mid of memory, where the second memory bank in the machine begins. In this space is the video window, the init-image program and their data, as well as kernel data. When entering the kernel from a userspace program (the first environment), the kernel executes memsys first that switches to the second environment where the kernel variable space is located, and memtask switches back if the program is running in the first environment. For more details see cbm8x96(p).
-
Multiple maps with different zeropage: The C128 has two memory banks of 64k each, with potentially shared memory on the top and/or the bottom of each bank. In addition, the zeropage and stack can be re-mapped to other locations. This allows for two environments that are not shared. The C128 has shared memory at the top of memory that contains the kernel. The lib6502 code is copied twice into the bottom of each bank, after the SENDBUF. The init image programs and their data are mapped into the single system bank only. The kernel data will be mapped into shared memory as well. The reason for this is that it is very difficult to copy data from one bank to another if not going through a shared memory area. Also, the infrastructure, to copy task data to kernel or back is already there for when the task memory is mapped into directly accessible space in the kernel memory space / environment. The lib6502 data is specific to each environment and will be in non-shared memory. The C128 can actually re-map the stack to other locations as well. This, however, is currently not used and the usual STACKCOPY copied or shared stack options are used.
If the system that is ported to has a (linearly) memory mapped screen buffer, the devices/console.a65 console code can be used as starter. It includes architecture-specific code to map from ASCII (the system code table) to a system-specific table if needed, and other functions. You can use arch/pet32k/con_pet.a65 as an example. This again includes other files e.g. for keyboard mapping and the blinking cursor.
If there is another type of mapping (e.g. Apple-II with non-linear screen buffer space), or even non-mapped video buffer, a new device needs to be written.
Devices are built into a single block (that comes from the 4k block mapping in GeckOS original CS/A implementation) that is initialized during kernel init. The architecture-specific devices are defined in arch/c64devices/c64dev.a65 or arch/pet32k/devices.a65 for example.
The currently supported filesystems support Commodore IEC/IEEE devices, PC floppy disks with FAT12 format (using a WD1771 controller), and the devices filesystem.
The devices filesystem uses the the kernel DEVCMD interface to show device names and handle them. Note: that includes the mapping between device names and numbers. So, this should be relatively generic. On the other hand, if FSDEV_NOROM is not set, it also shows the content of the init ROM and allows to access its content.
- Note
-
The init ROM access will probably be replaced at some point, so it is recommended to just use FSDEV_NOROM.
This file must initialize the stack, the memory management and mapping of the system ROM and RAM. Then the system preemption timer must be set up. When including kernel/zerotest.a65 and kernel/ramtest.a65 here all the memory options from above are available. Also the ramtest file returns the size of memory found in 256-byte blocks in a. This value must be passed to the end of the init routine in this file.
The kinit file also sets up the system intervall timer, and defines a macro to check and clear the timer (see CHECKTIMER below).
In addition to this some Macros have to be defined, that are used in the kernel later.
GETFREQ() returns a=0 for 1Mhz, a=128 for 2MHz
H_ERROR() H_ERROR defins a routine that outputs the detected hardware error.
This could be implemented by flashing screen borders, or an LED.
CHECKTIMER() clear the system preemption timer interrupt and return state in C
SAVEMEM() save system mem config for interrupt
RESTOREMEM() restore system mem config after interrupt
STARTMEM() This is used to alloc/enable memory pages found
during kernel init. Takes mem size in AC as given
by the kernel init. It is called _after_ inimem!
ramtest options if ramtest.a65 or zerotest.a65 are included in the kinit file:
RAMSTART page where the ramtest should start (optional)
RAMEND page (+1) where the ramtest should end
MIN_MEM minimum needed size of RAM to start
RAMTEST if set, the RAM from address 2 up is being tested up
to RAMSIZE. If not set, RAMSIZE is assumed to be the
real RAM size.
If set, then BATMEM, MEMINIVAL, and NOMIRRORMEM can
be used
BATMEM during memory test the main memory is not cleared,
the value is kept intact (for battery buffered RAM)
NOMIRRORMEM can be set if definitely _no_ mirrored memory is
there. Should definitely be set when a 32k RAM socket
can be fed with an 8 kByte RAM.
MEMINIVAL value to fill RAM with after RAM test (0 if not set)
The kenv.a65 file is included in another part of the kernel. It is the part that maps the memory according to the environment number for each task. Several routines must be defined here:
inienv init environment handling
setthread set active thread
initsp init task for thread (no in xr)
push push a byte to active threads stack
pull pull a byte from active threads stack
memtask jump to active thread
memsys enter kernel
getenv get e free environment
freenv free an environmen
sbrk reduce/increase process memory in an environment
enmem enable memory blocks for environments
(this kernel call is used for MMU systems and
heavily system specific)
setblk sets a specific memory block in the memory map
of the active task. Also for MMU systems only
Also some macros must be defined:
MEMTASK2() This is equivalent to memtask above, but it
is used for returns from interrupt
MAPSYSBUF maps the PCBUF of the active task to the
address given by SYSBUF in system memory
SYSBUF mapped tasks PCBUF
MAPENV() maps the address given in a/y in env x to somewhere
in the kernel map, returns mapped address in a/y
MAPAENV() as MAPENV, but does it for actual env.
GETTASKMEM returns (in AC) how much memory a task has
(task id in y)
CPPCBUFRX copies PCBUF from other task (yr) to active one
CPPCBUFTX copies PCBUF from active task to another one (yr)
CPFORKBUF() copies the PCBUF from the FORK call to the task.
GETACTENV() returns active environment in ac
The full descriptions can be found in the C64 kenv file for example. This may look complicated, but it isn’t. If you have a simple 6502 system without memory management, just copy (or set a link) to the C64 file. If you have a more complicated system, have a look at the CS/A65 file.
A number of configuration options determine the internal size of the various tables, and thus how many streams, threads, etc can be managed. These are typically defined per architecture, in config.i65
#define ANZSTRM 16 number of streams
#define ANZSEM 8 number of semaphores - multiple of 8
#define SYSSEM 8 multiple of 8, no of system semaphores
i.e. sems from -SYSSEM to -1
#define MAXDEV 16 maximum number of devices
#define MAXENVS 1 maximum number of environments (for kenv.a65 only)
#ifndef STACKCOPY
#define MAXNTHREADS 5 (Stackpage-56)/40
#define MAXNTASKS 5
#define STACKSIZE 40 (TH_SLEN * 5)
#else
#define MAXNTHREADS 12 (up to 16 is possible, if enough memory)
#define MAXNTASKS 12
#define STACKSIZE 64 (TH_SLEN * 8)
#endif
#define ANZXENV 5 number of redirectable task IDs
#define IRQCNT 3 default priority for ROM-started tasks
#define MAXFS 4 maximum number of fileserver tasks
#undef MAP_ZERO if set, map various data items into zp for speed
Depending on the target architecture, the filesystem code needs to be re-writting, if none of the existing code can be re-used.
To implement a new filesystem driver, the API described in include/fdefs.a65 needs to be implemented. Existing filesystems could be used as reference, but note that the codebase is very old and messy.
Three main components need to be loaded into the target system:
-
The kernel
-
lib6502
-
init ROM
The kernel provides the entry points into the system by providing the OSA2KERNEL API. The lib6502 provides the lib6502 calls by providing the LIB6502 API. Note that the kernel and the lib6502 are re-entrant and only need to be in memory once, even if used in multiple tasks.
The Init ROM contains all the extra code to be executed. The first block typically are the devices, then the init program and the fsdev filesystem. After that come further filesystems and other programs to be started without access to storage. Each ROM entry has a type attached. The kernel starts types PK_DEV and PK_INIT. This is typically only the device block and the init program.
The init program then scans the init ROM again, and starts the other tasks (like PK_PRG, or PK_FS filesystems). For this, the init program needs to "see" the init ROM in its memory mapping. Note, that init already uses lib6502.
In a plain setting, where no memory mapping is involved, all three items can be combined into a single binary, as is the case in the C64 or PET32k architectures. Typically a rom.o65 is produced that is relocated to the target address, loaded into memory in one blob, potentially re-located by a loader, and then started by calling the KERNEL’s RESET entry. During relocation it must be ensured that the three segments - text, data, bss - do not overlap, by adjusting the options to reloc65. During the build, the kernel and lib6502 APIs may be located anywhere, but the lib6502 loader automatically relocates the calls from any binary by using the OSA2KERNEL and LIB6502 to the correct address that is "baked into" the lib6502 code.
- Note
-
for the ROM build the .data segment is not initialized!
In other settings, three binaries for kernel, lib6502, and init ROM can be produced separately, but must be controlled from the build process (Makefile). For example the OSA2KERNEL and LIB6502 addresses should be defined and given to the build using the "-D" option to xa65. These can then be loaded separately into memory, moved, and then started by a loader as can be see in the cbm8x96 architecture.
Please report bugs at https://github.com/fachat/GeckOS-V2/issues