# SDH-100 boot.conf Configuration Guide

# About the file

The firmware reads /CONF/boot.conf from the microSD card once at boot.

First-time setup

If the file is missing, boot halts before the bus comes up, the SD card is presented as a USB mass-storage volume, and the board waits there until a valid file is copied and power is cycled.

The format is one directive per line, in the form KEY=VALUE.

  • A line whose first character is # is a comment and is skipped.
  • A # after the value also starts an inline comment; everything from # to the end of the line is discarded.
  • Blank lines are ignored.
  • Whitespace around the = is significant on the key side: a leading space turns FDC.transport into FDC.transport, which the firmware will not recognise. Values tolerate trailing spaces in most cases but it is safest to leave none.
  • If the same key appears twice, the last value wins.

Silent typos and case sensitivity

  • Keys are case sensitive. Write FDC.transport, not fdc.transport or FDC.TRANSPORT. Values are usually case-insensitive but a few are not -- the sections below call out any that are.
  • Unknown keys are silently ignored. A typo in a directive name does not produce an error -- it produces a missing setting.

Edits take effect on next boot only

Settings are applied during boot. Editing boot.conf while the board is running has no effect until the next reset.

# Syntax notation

Each directive in the reference below is preceded by a one-line grammar. The notation is:

  • Literal tokens (AUTO, cs8, default, PASS, ...) appear exactly as shown.
  • <name> marks a value you supply (a number, an address, a filename, etc.).
  • A|B|C means choose one of the listed alternatives.
  • [...] marks an optional fragment.
  • [,X...] indicates the fragment may repeat, comma-separated.
  • Spaces in the grammar are for readability only -- the value itself contains no spaces.

# Annotated showcase boot.conf

The file below exercises every directive the firmware recognises. It is intentionally maximal -- a real installation typically only sets the lines it needs and lets the rest default. Use this as a copy-and-cut starting point.

### boot.conf -- SDH-100 configuration

### --- Boot logging ---------------------------------------------
# BOOT_LOG=QUIET           # suppress UART/S-132 console boot log
# BOOT_LOG=TEXT            # strip ANSI colour escapes
BOOT_LOG=TEXT              # plain-text colour-free boot log

### --- Serial ports ---------------------------------------------
UART0=115200,cs8           # console UART (S-100 SIO Port A path)
UART1=115200,cs8,parenb    # SIO Port B path -- enable even parity

### --- Memory map -----------------------------------------------
ROM_LOAD=memon80.hex       # one or more Intel-HEX images from /ROMS/
# ROM_LOAD=mpu-a-rom.hex,viofm1.hex

RAM_MAP=AUTO               # fill every empty page with emulated RAM
# RAM_MAP=00:7F             # alternative: explicit page ranges
# RAM_MAP=00:7F,80:DF
RAM_SET=0x00               # fill emulated RAM with this byte
MEM_TEST=RUN               # one walking test pass after init
# MEM_TEST=RUNLOOP          # ...or loop forever (development use)
BOOT_JUMP=0xF800           # plant JP F800h at 0000h before release

### --- Disk drives ----------------------------------------------
HARDDISK=blank             # /DISKS/blank.hdd as the large disk
FDC.transport=USB          # USB or UART (UART claims UART1)
# FDC.uart.drives=A:CPM22.FDC,B:UTILITY.FDC

### --- Printer -------------------------------------------------
LPT.iobase=default         # 0xF6
LPT.device=S132            # S132 | FILE | PARALLEL

### --- IMSAI VIO ----------------------------------------------
VIO.iobase=default         # 0x04 -- VIO command/data
VIO.membase=default        # 0xF000 -- VIO 2 KB video window

### --- SIO controller -----------------------------------------
SIO.iobase=default         # 0x00 -- SIO-2 base (Port A and Port B)
# SIO.A.baud=9600           # software-throttle SIO Port A
# SIO.B.baud=9600           # software-throttle SIO Port B

### --- Other I/O port bases ------------------------------------
DZLR.iobase=default        # 0x0E -- Cromemco Dazzler
D7AIO.iobase=default       # 0x18 -- 8-port digital/analogue I/O
TIMER.iobase=default       # 0xA0 -- interval timer / control port
SDH.iobase=default         # 0xA1 -- SDH host co-routine port
APU.iobase=default         # 0xA2 -- Am9511 APU
FIF.iobase=default         # 0xFD -- IMSAI FIF floppy controller
MMU.iobase=default         # 0x40 -- IMSAI MMU bank switch
RTC.iobase=default         # 0x41 -- DS3231 real-time clock

### --- USB host integration ------------------------------------
USB.disk=AUTOMOUNT         # auto-present microSD as MSC volume
USB.tty=PASS               # bridge USB CON CDC <-> SIO Port A
# USB.uc1=PASS              # bridge USB AUX CDC <-> SIO Port B
USB.debug=TRUE             # log USB connect / disconnect events

### --- Monitor ------------------------------------------------
# MON.break=NMI            # break via Z80 NMI vector (Z80 only)

### END

The remainder of this document explains every directive in detail.

# Boot logging

# BOOT_LOG

BOOT_LOG=QUIET|TEXT|QUIET,TEXT

Controls how boot-time log output is rendered on the console serial ports. Accepts the keywords QUIET and TEXT, separately or combined with a comma or space (BOOT_LOG=QUIET,TEXT). QUIET suppresses log output on UART0 and on the S-132 console, but leaves the USB MON CDC channel untouched, so the boot log is still available there for diagnosis. TEXT strips the ANSI colour-escape sequences that the firmware normally emits, which matters when the console is a paper teletype, an early CRT terminal that prints escape sequences as gibberish, or a logger that captures raw bytes to a file. The directive only affects the boot phase; once the firmware finishes initialising and reports SDH-100 Running..., the boot log stops emitting and the runtime log channel takes over with its own conventions. If you find yourself wanting clean text in captured logs, set BOOT_LOG=TEXT permanently -- it has no downside on a colour-capable terminal beyond the loss of a few highlights.

# Serial ports

# UART0

UART0=<baud>[,cs7|cs8][,cstopb][,parenb[,parodd]]

Configures the S-100 SIO Port A path on UART0 of the RP2350B. The value is an stty-style list of tokens separated by spaces or commas: a bare integer is the baud rate; cs8 and cs7 set the data-bit width; cstopb selects two stop bits; parenb enables even parity; parodd switches to odd parity (only valid in combination with parenb). The default if the line is missing or empty is 115200,cs8 -- 115200 baud, 8 data bits, 1 stop bit, no parity. The SDH-100 always reinitialises UART0 at boot, so commenting the line out is the same as leaving it at the default; deleting the directive has no special meaning. UART0 is not configured here when an S-132 console is detected -- the S-132 owns the console TTY in that case, and its own settings apply. UART0 is also the primary console for the boot log when BOOT_LOG=QUIET is not in force, so choose a baud rate your terminal can actually keep up with during the rapid boot-time output.

# UART1

UART1=<baud>[,cs7|cs8][,cstopb][,parenb[,parodd]]

Configures UART1 of the RP2350B with exactly the same syntax as UART0. UART1 backs the S-100 SIO Port B path under normal use -- hooked, for example, to a second terminal, a serial printer, or a modem. Default is again 115200,cs8. The interaction worth knowing is that FDC.transport=UART claims UART1 exclusively at 2 000 000 baud and reroutes it to the floppy controller protocol; in that configuration any baud rate or framing you set here is silently overridden, and SIO Port B emulation stops responding. If you need SIO Port B for terminal use, leave FDC.transport=USB (the default) or omit the line. Like UART0, UART1 is reinitialised on every boot even if you delete the directive, so there is no way to leave the hardware UART unmanaged.

# Memory and ROM

# ROM_LOAD

ROM_LOAD=<file>[,<file>...]

Names one or more Intel-HEX files in /ROMS/ that the firmware loads into shadow memory and marks read-only before the CPU is released. The value is a comma-separated list of filenames -- for example ROM_LOAD=memon80.hex or ROM_LOAD=mpu-a-rom.hex,viofm1.hex. Each file is loaded at the addresses encoded in its hex records, so the load address is part of the file, not part of the directive. Pages that receive HEX data become ROM in the live memory map and survive resets initiated by the bus until the next power cycle. If the directive is absent the firmware logs a warning and brings the bus up with no emulated ROM, which is normal when you intend to run on physical S-100 RAM/ROM boards alone. ROM data placed at the reset vector by ROM_LOAD is overwritten by BOOT_JUMP if both are set; BOOT_JUMP runs later.

# RAM_MAP

RAM_MAP=AUTO|<start>:<end>[,<start>:<end>...]

Decides which 256-byte pages of the 64 KB CPU address space the SDH-100 will provide as emulated RAM. The value is either the literal keyword AUTO or a comma-separated list of START:END page-number pairs in hexadecimal -- for example RAM_MAP=00:7F,80:DF. Page numbers are the high byte of the address, so 00:7F covers addresses 0000h-7FFFh (32 KB) and 00:FF covers the entire 64 KB. AUTO is the easiest choice and is the right default for most systems: the firmware first scans the bus for physical memory cards and any ROM you loaded, then fills every remaining page with emulated RAM. Manual ranges are useful when you want to leave a hole for a device that is not yet plugged in, when you intentionally want unmapped pages to NACK to test software, or when you want to run with a smaller RAM footprint for diagnostics. Pages outside the declared ranges return FFh to the CPU and produce no read or write side-effects. If the directive is absent the firmware logs a warning and provides no emulated RAM at all -- a configuration only useful when every page in the system comes from physical S-100 cards.

# RAM_SET

RAM_SET=<byte>

Picks the byte value used to fill emulated RAM at boot. Accepts any form strtol understands, including 0xAA, 170, or 0252. Only the low eight bits matter -- RAM_SET=0x1234 is the same as RAM_SET=0x34. If the line is absent, RAM is filled with pseudo-random bytes from the firmware's PRNG, which is the right choice when you want unmapped variables and untouched stack regions to be detectable by software that checks for known patterns or that crashes on uninitialised reads. Setting RAM_SET=0x00 makes the emulated RAM look like cleared dynamic RAM -- familiar for CP/M loaders and most BASICs. RAM_SET=0xFF mirrors what an unattached data bus normally pulls up to and is useful when you want freshly booted RAM to look exactly like physical EPROM holes. The value applies only to pages declared as emulated RAM (AUTO-filled or listed explicitly); pages backed by physical S-100 RAM keep whatever they hold, and ROM pages keep their HEX-loaded content.

# MEM_TEST

MEM_TEST=RUN|RUNLOOP

Runs a walking-pattern test across every shadow-RAM page after the memory map is built and before the CPU is released. The keyword RUN performs a single pass; RUNLOOP performs the test in an infinite loop and forces the SD card to remain visible as a USB MSC volume so you can pull a log file off the board mid-run. The pattern sequence is AAh, 55h, FFh, 00h, F0h, 0Fh, 5Ah, A5h, 33h, CCh applied to every byte; failures are reported in the boot log with the failing address and the expected and observed bytes. The test runs while the SDH-100 has been granted the bus, so the CPU is held off and not yet fetching from any of the pages under test -- a long test does delay the SDH-100 Running... banner. In daily use the test is unnecessary; RUN is most useful as a one-shot acceptance check after a hardware modification or when chasing intermittent crashes that smell like RAM. RUNLOOP is meant for development, not normal operation. If the directive is absent or set to anything that does not contain RUN (note: MEM_TEST=LOOP on its own does not run the test), the test is skipped.

# BOOT_JUMP

BOOT_JUMP=<addr>

Plants a three-byte unconditional jump (C3 lo hi) at addresses 0000h-0002h immediately before releasing the CPU from HOLD. The value is a 16-bit address in any base strtol accepts, typically hex (BOOT_JUMP=0xF800). The firmware writes the bytes both into shadow RAM and, if the page at zero is also present in physical RAM, into the physical SRAM via DMA -- so the CPU sees the jump on its first fetch regardless of whether 0000h is mapped to a SDH-100 page or to a real RAM card. The directive runs after ROM_LOAD and after MEM_TEST, so it overrides anything the HEX file placed at the reset vector. The most common application is to land directly into a monitor or BIOS that loads at a high address (MEMMON80 at F800h, a CP/M loader at EE00h, etc.) without having to write a two-byte stub into the ROM image first. Invalid values -- non-numeric text, addresses above FFFFh, or trailing garbage -- are rejected with a warning and no jump is injected, which leaves whatever the HEX file or memory pattern put at the reset vector intact. If the directive is absent no bytes are written.

# Disk drives

# HARDDISK

HARDDISK=blank|<name>

Mounts a hard-disk image at the large-disk slot of the IMSAI floppy controller. The value is the basename of a .hdd file in /DISKS/; the .hdd extension is appended automatically, so HARDDISK=cpm68k mounts /DISKS/cpm68k.hdd. The reserved value blank mounts the firmware's standard 4 MB blank image, which is formatted by the host operating system on first use. The drive appears at the floppy controller's "I:" slot (large disk index) and follows the same sector geometry the FIF expects of a hard-disk image. If the directive is absent no large-disk image is mounted and software that probes for it sees an empty drive. Unlike floppy drives, the hard-disk slot is set once at boot -- there is no boot configuration to swap it at runtime, though host commands can change it later via the SDH co-routine port.

# FDC.transport

FDC.transport=USB|UART

Selects the channel the IMSAI floppy controller emulator uses to talk to the host. FDC.transport=USB (the default) routes FDC traffic over the dedicated USB FDC CDC class device, where it is served by a USB host application that streams disk images. UART hands the floppy protocol to UART1 at a fixed 2 000 000 baud, 8N1, for installations where a USB host is impractical and a wired serial link is available instead. The value is matched on its first four characters case-insensitively, so USB, usb, and UART all work. The most important consequence of UART mode is that SIO Port B becomes unavailable: UART1 is fully consumed by the floppy protocol, and any software that drives Port B will see it as silent. USB.uc1=PASS is also incompatible with FDC.transport=UART for the same reason. New as of 0.18.0; releases before that always used USB and accepted no override.

# FDC.uart.drives

FDC.uart.drives=<drive>:<image>[,<drive>:<image>...]

Pre-populates the floppy controller's drive map at boot, but only when FDC.transport=UART. The value is a comma-separated list of drive:image pairs, where the drive letter is A through D and the image is a filename in /DISKS/. For example, FDC.uart.drives=A:CPM22.FDC,B:UTILITY.FDC mounts those two images on drives A and B at the first successful FDC handshake. Whitespace inside an entry is tolerated. Entries that fail to parse, or images that the FDC layer rejects, are logged and skipped without aborting the rest of the list. Only the first successful FDC handshake applies the map; subsequent FDC resets do not re-apply it, and any runtime drive changes made through host commands persist independently. The directive is silently ignored when transport is USB -- a USB host is expected to manage its own image catalogue. If you switch from UART back to USB transport, leaving the FDC.uart.drives line in place is harmless.

# Printer

# LPT.iobase

LPT.iobase=default|<port>

Sets the I/O port base for the parallel-printer interface. The value is default (which selects port F6h) or a hex/decimal port number. The directive must be present for the printer to be registered at all -- if you remove the line entirely, the printer disappears from the I/O map and software targeting it sees no response. When you change the base, the device occupies a single port; the strobe and status lines are generated internally and do not consume additional ports. The directive interacts with LPT.device: changing the base does not change what the printer output is doing, only which port gets writes routed to it.

# LPT.device

LPT.device=FILE|PARALLEL|S132

Routes printer output to one of three sinks. Accepts FILE, PARALLEL, or S132, case-insensitive but with no surrounding whitespace. FILE -- the default if the directive is absent -- buffers writes and appends them to /lpt.txt on the SD card, flushing after a second of inactivity, which is the simplest configuration for capturing CP/M LST: output for later review on a laptop. PARALLEL drives a physical Centronics-style printer through the on-board MCP23017 I2C I/O expander; if the chip is not detected on the I2C bus the firmware logs an error and falls back to FILE silently, so a stale PARALLEL setting on a board without the I2C add-on does not stop the system booting. S132 routes printer writes through the S-132 D7AIO channel, which is useful when the S-132 is connected to a host that wants to render printer output on its own screen. Unrecognised values fall back to FILE with a warning. There is no way to log to multiple sinks at once.

# IMSAI VIO

# VIO.iobase

VIO.iobase=default|<port>

Sets the I/O port base for the VIO video controller's command and data ports. The value is default (port 04h, occupying 04h-05h) or a hex/decimal port number. The VIO command interface is two ports wide regardless of base. Note that this directive is independent of VIO.membase -- moving the I/O base does not move the video memory window, and vice versa. A working configuration requires both bases to be consistent with whatever software you are running.

# VIO.membase

VIO.membase=default|<addr>

Sets the base address of the VIO's 2 KB video memory window in the CPU address space. The value is default (F000h, covering F000h-F7FFh) or a hex address that you have made sure does not overlap any ROM, RAM, or device window. Moving this is most often useful when you load a ROM at the same high addresses the VIO normally occupies and you want to push the video buffer down to E800h or similar. The window is always exactly 2 KB; you can move it but you cannot resize it.

# SIO controller

# SIO.iobase

SIO.iobase=default|<port>

Default 00h. Registers the SIO-2 controller, which provides Port A at base+02h/base+03h (data and command) and Port B at base+22h/base+23h. The data port at base+00h itself is left unassigned. Moving this base shifts both ports; software that hard-codes 02h-style port numbers must be rebuilt or patched.

# SIO.A.baud

SIO.A.baud=<baud>

Imposes a software baud-rate ceiling on SIO Port A. The value is a decimal baud rate -- e.g. SIO.A.baud=9600 -- and it works by delaying every character through Port A by the equivalent of 10/baud seconds (10 bits per character, the worst-case framing). The throttle is purely software: the underlying hardware UART still runs at whatever UART0 configured, so the terminal on the wire sees no change. What does change is what the CPU sees -- a Z80 / 8080 program polling the SIO status will observe characters arriving and the transmit-ready flag clearing at the rate you set, which matters when you are running CP/M software written for a real 9600-baud terminal that loses data when characters arrive faster than its echo buffer can handle. If the directive is absent or set to zero, no throttle is applied and characters move at full firmware speed.

# SIO.B.baud

SIO.B.baud=<baud>

Same throttle as SIO.A.baud, applied to SIO Port B. Note that this directive has no effect when FDC.transport=UART -- in that configuration the SIO Port B emulation is suppressed entirely because UART1 has been claimed by the floppy controller. If you configure both, the floppy controller wins and the throttle is silently ignored until you switch transport back.

# Other I/O port bases

The directives below all set the I/O port base for an emulated device. Each accepts default (the value listed) or a hex/decimal port number; each is a one-line entry in the same form as the others. The directive must be present for the device to be registered. Deleting an entry does not move the device to a default port -- it removes the device from the I/O map entirely. That is intentional and lets you turn devices off cleanly, but it also means accidentally commenting out a line silently disables the device. The defaults are listed both in the showcase sample above and below.

Port-base collisions silently drop the second device

If two devices end up assigned to the same base port, the firmware logs an error and skips registration of the second one; the first device wins. Always verify ranges do not overlap when moving bases.

# DZLR.iobase

DZLR.iobase=default|<port>

Default 0Eh. Registers the Cromemco Dazzler at base+00h and base+01h. Move this if you have a real Dazzler board on the bus that you want the SDH-100 to leave alone, or if your software expects the Dazzler at a non-standard port.

# D7AIO.iobase

D7AIO.iobase=default|<port>

Default 18h. Registers the eight-port digital/analogue I/O block, spanning base+00h through base+07h. The eight-port footprint is fixed; the base can be moved but the span cannot be reduced. If the S-132 is attached, D7AIO traffic at this port is also routed to it by default.

# TIMER.iobase

TIMER.iobase=default|<port>

Default A0h. Registers the interval-timer / hardware control port at a single port address. The port carries the periodic interrupt control bits and is a common target for CP/M time-of-day patches.

# SDH.iobase

SDH.iobase=default|<port>

Default A1h. Registers the SDH-100 host co-routine command port, which CP/M hosted on the SDH-100 uses to issue management commands (disk eject, system map query, etc.) into the firmware. Moving this breaks any pre-built CP/M binary that uses the co-routine; only move it when you have a specific port conflict and a way to rebuild the client.

# APU.iobase

APU.iobase=default|<port>

Default A2h. Registers the Am9511 floating-point APU at base+00h (data) and base+01h (status). Software that uses the APU expects a fixed port pair, so move this only when something else on the bus already occupies A2h-A3h.

# FIF.iobase

FIF.iobase=default|<port>

Default FDh. Registers the IMSAI FIF floppy controller's command port. The FIF emulator interacts with whichever transport FDC.transport selects -- moving the base does not change which transport is active, only the port the CPU writes commands to.

# MMU.iobase

MMU.iobase=default|<port>

Default 40h. Registers the IMSAI MMU bank-switch register at a single port address. The MMU register switches the physical bank in which the lower 64 KB of address space is served from emulated shadow RAM, so software that uses banked memory expects this port to be present.

# RTC.iobase

RTC.iobase=default|<port>

Default 41h. Registers the DS3231 real-time clock interface at a two-port window: a command register at the base address and a data register at base+1. Removing the directive removes the RTC from the I/O map; software polling for it sees no response, which is the correct behaviour when the on-board RTC is not populated.

# USB host integration

# USB.disk

USB.disk=AUTOMOUNT

Set to AUTOMOUNT to make the SD card visible as a USB mass-storage volume whenever a host is connected. Without this directive the firmware leaves the MSC volume ejected once boot is finished, which is the right behaviour when the SD card is being read concurrently by the running CP/M system and you want to avoid the host operating system caching a stale view of it. With AUTOMOUNT, the volume is re-presented after every soft reset and after the SDH-100 finishes its boot. Use it when you are actively iterating on boot.conf, disk images, or HEX files from a laptop and do not need the bus to be writing to the card concurrently. The directive accepts AUTOMOUNT only; any other value disables auto-mount.

# USB.tty

USB.tty=PASS

Set to PASS to bridge the USB CON CDC channel to and from SIO Port A. With this directive, characters typed into the host's CON terminal (most often a screen or picocom session) appear on Port A as if they came from a wired terminal, and characters the CPU writes to Port A are forwarded to the host CON terminal in addition to the physical UART0 path. This lets you use a USB-only host as the operating console without a serial cable. The pass-through is bidirectional; you do not get to choose direction. With no value or any value other than PASS, the bridge is off and the CON channel is monitor / log output only.

# USB.uc1

USB.uc1=PASS

Set to PASS to bridge the USB AUX CDC channel to and from SIO Port B in the same way USB.tty=PASS bridges Port A. Useful when you want a second host-side terminal for a CP/M session that has a secondary console -- for example, a debugger on Port B while normal output runs on Port A. The directive is mutually exclusive with FDC.transport=UART: the floppy controller and the AUX bridge cannot share UART1. If both are set, the floppy controller wins and the AUX bridge becomes inactive.

# USB.debug

USB.debug=TRUE

Set to TRUE to enable boot-log entries for USB connect and disconnect events on the CON, AUX, MON, and FDC CDC channels. This is the directive to enable when you suspect a host-side cable or driver problem is making the SDH-100 misbehave. The extra log entries are visible on whichever channel you are watching the boot log on (and on the runtime MON channel), and they carry the channel name and the new state so you can correlate them with what the host did. Leave it disabled in normal use -- the log is otherwise quiet about USB traffic.

# Monitor

# MON.break

MON.break=NMI

Selects how the SDH-100 monitor breaks into a running CPU for the R, G, I, O, T, U, and W commands. The default behaviour (directive absent or any other value) is to break in via INT and inject an RST 7 opcode on the INTA cycle, which jumps the CPU to 0x0038 and onto the firmware's interrupt trampoline. Setting MON.break=NMI switches the break-in path -- both for direct R, G, I, O commands and for W watch fires -- to the Z80 NMI input: the firmware drives the NMI line, the CPU vectors to 0x0066 where a 2-byte JR redirects into the same trampoline at 0x0008, and the rest of the break-in proceeds identically. The directive is silently ignored on 8080 systems because the 8080 has no NMI input -- a warning is logged at boot to flag the misconfiguration. The advantage of the NMI path is that it is non-maskable: a Z80 program that has executed DI (or is mid-block-move with EI deferred, or in a tight ISR) can still be broken into via R/G/I/O or by a watch fire. The 2-byte JR overlay at 0x0066 sits inside CP/M's default FCB range (0x005C-0x007F). For direct R/G/I/O it is only live for the tens of microseconds between bus release and the CPU running the trampoline -- programs reading the default FCB during that window are extraordinarily unlikely to hit it. For W watches, the overlay is live for the entire armed period (which can last arbitrarily long while the CPU runs waiting for the watch to fire), and the firmware refreshes it every time it acquires the bus to sample the CPU; this is the trade-off MON.break=NMI opts into in exchange for being able to watch DI'd code. Leave the directive unset unless you have a concrete need to break into DI'd Z80 code.

# Common pitfalls and interactions

A few of the boot.conf directives interact in ways that surprise people the first time. The notes below collect them in one place.

Every .iobase line is non-optional. If you delete SIO.iobase or LPT.iobase or any of the entries listed in Other I/O port bases, you do not get the default port -- you get no device at all. Use KEY=default to take the default; only remove the line entirely if you actually want that device gone.

FDC.transport=UART disables SIO Port B. Three things lose their function in this mode: SIO Port B emulation, any software targeting it, and USB.uc1=PASS. Anything you set on UART1 -- baud, parity, framing -- is overridden by the 2 000 000-baud, 8N1 configuration the floppy controller insists on. Switch back to FDC.transport=USB if Port B is needed.

BOOT_JUMP runs after ROM_LOAD. If your HEX file lays code at 0000h, BOOT_JUMP will overwrite the first three bytes of it with the jump. That is usually what you want when the HEX is a high ROM image and 0000h was just collateral; check the layout if it is not.

RAM_MAP=AUTO interacts with both physical hardware and ROM_LOAD. The auto-fill happens after the bus is scanned for physical memory, so any S-100 RAM card you have plugged in claims its address range first; only pages that are still empty become emulated RAM. The same applies to ROM pages claimed by ROM_LOAD.

RAM_SET only fills emulated RAM. Pages backed by real RAM cards keep whatever was already there. ROM pages keep the HEX contents. If you RAM_SET=0xFF and then dump page 00h and find non-FF data, the page is on a physical RAM card, not on the SDH-100.

MEM_TEST=LOOP on its own does nothing. The test only runs if the value contains the substring RUN. Use RUN for a single pass or RUNLOOP for a continuous loop; do not use LOOP alone.

Inline comments end at #. Everything from the first # on a value line is discarded. This is convenient for annotating values but it means a value cannot contain a literal #. None of the directives the firmware honours need one, so this is rarely a limit in practice.

A typo is a missing setting, not an error. The parser does not warn on unknown keys. If a directive seems to have no effect, check the spelling and case before assuming a firmware bug -- Fdc.transport and FDC.Transport both produce silently-ignored entries.

Last Updated: 5/2/2026, 5:36:29 PM