macintosh.world | Log In | Register
Today | News | Books | Recipes | Notes | YouTube | QuickTake
Translate | Wiki | Browse | Maps | Reference | Reddit | About

Running DOS on Behringers DDX3216 with a DIY x86-BIOS from scratch - Chris.Dev.Blog

Open Original Page

Running DOS on Behringers DDX3216 with a DIY x86-BIOS from scratch - Chris.Dev.Blog

Electronics, Programming and Development


Running DOS on Behringers DDX3216 with a DIY x86-BIOS from scratch

Date:8. June 2026Posted By:ChrisCategory:Blog postsTag:programming, retro

In 1994 I got my first computer: an Intel i486 DX2-66 with 4 MB RAM and a 512MB harddisk. The software was IBMs OS/2 and Microsofts Windows 3.11. In the next four years I was upgrading this machine every few months with more RAM (up to 16MB), a CD-ROM-drive and a soundblaster card. So I learned upgrading this machine, installing new software and finally learned how to program new software using BASIC. But I never got in touch with the boot-process or the details of MS-DOS.

In 2026, 32 years later, I learned from some screenshots of the DDX3216, that Behringer used a real 386 processor within this machine. Immediately, some of my neurons fired in my head and I pondered if I could boot software and even a full operating system on this device. My goal was to learn how an x86-system is booting, how DOS takes over and what is necessary to get into the shell.

Technical Details of the Behringer DDX3216

First steps developing own software for bare-metal x86

Getting the LCD up and running - and struggling with Segments

Implementing a full-featured x86 BIOS for the SC300

Interrupt-functions and trying to boot MS-DOS 6.22

Successfully booting FreeDOS v1.4

More internal hardware and next steps

Technical Details of the Behringer DDX3216

The DDX3216 uses the following hardware-components:

Main-Processor: AMD Elan SC300 386 SoC (386SX with integrated UART, PCMCIA, GPIO, etc.)

27C512 64k x 8bit ROM IC (for BIOS)

8x HYB5117400BJ60 4M x 4bit RAM for total of 16MB DRAM

1x UM61256 SRAM (as Video-RAM)

4x 29C040-120 Flash-ICs for the main-software

4-bit LCD on SC300-internal LCD-interface (with 3x Toshiba T6A39 Col- and 1x T6A40 Row-Controller)

Toshiba TLC16C552 external UART (2 Serial-ports and 1x parallel port)

PCMCIA-Connector for external CF-card-connection (with adapter)

unassembled Intel 82078 FDC (Floppy Disk Controller) connected to a spare 34-pin connector

So in summary the hardware around the AMD Elan SC300 is pretty nice and should be compatible to a regular x86-system. Lets deep dive into the x86-system in detail.

First steps developing own software for bare-metal x86

For most computers you can download a ready-to-use BIOS from the internet. So I searched for a BIOS for the AMD ELAN SC and found a promising device in Switzerland: the company "PC Engines" developed BIOS-programs for the AMD ELAN SC400 and 520 as well as some more SoC-devices. So I got in contact with the main-developer and first he gave a promising answer that he still has the sourcecode for the SC300. But a couple of days later he had to admit, that he only has sources from the SC400 upwards. My next try was to get in contact with the company "General Software" that offered the "Embedded BIOS" with support for the SC300. But General Software, founded in 1989, has been acquired by Phoenix in 2008. So I got in contact with one of the responsible persons of Phoenix in Germany. He tried to get some information about an SC300-compatible BIOS-package, but after a couple of weeks he had to tell me that its not possible anymore - 32 years are a long time.

So, I rolled up my sleeves and started reading some documentations about the x86-system and made some notes on programming my own BIOS for the SC300. Even the most-modern x86-compatible CPUs like Intels Core i9 or the AMDs Threadripper have an 8086-compatible boot-process. Directly after the reset, the CPU jumps to the end of the memory-space at the position 0xFFF0 and expects some executable x86 code here - the so called reset-vector. From this reset-vector we have to jump to the desired code that should be executed next - somewhere in the ROM of the BIOS.

Here is my attempt of implementing a valid x86-reset-vector:

ASMreset_vector:
nop // no-operation
cli // disable interrupts
jmp start // jump to beginning of current segment

// Padding to the end and add date
.zero (0x10 - (. - reset_vector) - 8)
.ascii "06/04/26" // MM/DD/YY
reset_vector:
nop // no-operation
cli // disable interrupts
jmp start // jump to beginning of current segment

// Padding to the end and add date
.zero (0x10 - (. - reset_vector) - 8)
.ascii "06/04/26" // MM/DD/YY

This code disables the hardware-interrupts and then jumps to more code in the start-function. By executing this jump-command, the CPU leaves the startup-state and enters the so called "real-mode", the original 16-bit mode of the 8086. The code of the reset vector is placed by the linker-script to the position 0xFFF0 of the final ROM. As you can see in the list above, the DDX3216 uses a 64k x 8bit ROM-Chip, so code and data can be stored somewhere between 0x0000 and 0xFFFF, while the reset-vector has to be placed at 0xFFF0 to be compatible to the x86-cpecifications. Here is the linker-script to tell GCC how to place the code in the final binary-file:

PlaintextOUTPUT_FORMAT("elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(reset_vector)

MEMORY {
ROM (rx) : ORIGIN = 0x0000, LENGTH = 64K
}

SECTIONS {
.text : {
__text_start = .;
KEEP(*(.text))
*(.text.*)
. = ALIGN(2);
__text_end = .;
} > ROM

.reset 0xFFF0 : {
KEEP(*(.reset))
} > ROM
} OUTPUT_FORMAT("elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(reset_vector)

MEMORY {
ROM (rx) : ORIGIN = 0x0000, LENGTH = 64K
}

SECTIONS {
.text : {
__text_start = .;
KEEP(*(.text))
*(.text.*)
. = ALIGN(2);
__text_end = .;
} > ROM

.reset 0xFFF0 : {
KEEP(*(.reset))
} > ROM
}

Finally, the compiled binary looks like this:

Plaintext0000ffa0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000ffb0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000ffc0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000ffd0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000ffe0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000fff0 90 fa e9 0b 00 00 00 00 30 36 2f 30 34 2f 32 36 .úé.....06/04/26 0000ffa0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000ffb0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000ffc0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000ffd0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000ffe0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000fff0 90 fa e9 0b 00 00 00 00 30 36 2f 30 34 2f 32 36 .úé.....06/04/26

So we see a 0x90 at 0xFFF0 which is a "nop" (No Operation), an 0xFA, which is the "cli" to disable all interrupts followed by an 0xE9 which is the jump-instruction. 0x0B is the near-address the jump-command has to jump to. "Near" means, this address is within the current segment. Well, segments are a special thing in the x86-system: as we have only 16-bits, we could address only 65535 bytes. To address more addresses, the x86 uses 64k-Segments to address more data. The problem: to stay backwards-compatible, the segment-address-pointers overlap each other by 16 bytes. This allows addressing of 0xFFFF segments every 16 bytes, resulting in an address-room of 0x00000 to 0xFFFF0, hence 1 MB. The physical address is calculated by the following equation:

PlaintextPhysical Address = (SEGMENT << 4) + OFFSET

// maximum physical address
Physical Address = (0xFFFF << 4) + 0x000F = 0xFFFFF = 1048575 = 1 MB Physical Address = (SEGMENT << 4) + OFFSET

// maximum physical address
Physical Address = (0xFFFF << 4) + 0x000F = 0xFFFFF = 1048575 = 1 MB

Well, finally I learned the reason why DOS and its games had a problem with the conventional memory, which has to be within the 1MB-range. Even more limiting: only the first 640kB can be used as conventional memory, the higher addresses are reserved for the video-memory, the expansion-ROMs and the BIOS itself. This results in the following memory-map for a regular x86 system in real-mode:

Plaintext+--------------------------------------------------+ 0x100000 (1 MB)
| |
| SYSTEM-BIOS | 64 KB ROM
| |
+--------------------------------------------------+ 0xF0000 (960 KB)
| |
| Option-ROMs or free high-memory | 160 KB
| |
+--------------------------------------------------+ 0xC8000 (800 KB)
| Video-BIOS (Graphiccard-ROM) | 32 KB ROM
+--------------------------------------------------+ 0xC0000 (768 KB)
| Video-RAM (VRAM for Textmode & VGA-Graphic) | 128 KB RAM
+==================================================+ 0xA0000 (640 KB)
| |
| |
| |
| |
| CONVENTIONAL MEMORY (RAM) |
| Free space for DOS, Programs, Drivers, etc. | ca. 605 KB
| |
| |
| |
+--------------------------------------------------+ 0x07E00
| Bootsector (loaded from boot-drive) | 512 Bytes
+--------------------------------------------------+ 0x07C00
| Free DOS-Memory / DOS-Kernel | ~29 KB
+--------------------------------------------------+ 0x00500
| BDA (BIOS Data Area) | 256 Bytes
+--------------------------------------------------+ 0x00400
| IVT (Interrupt-Vector-Tables) | 1 KB
+--------------------------------------------------+ 0x00000 +--------------------------------------------------+ 0x100000 (1 MB)
| |
| SYSTEM-BIOS | 64 KB ROM
| |
+--------------------------------------------------+ 0xF0000 (960 KB)
| |
| Option-ROMs or free high-memory | 160 KB
| |
+--------------------------------------------------+ 0xC8000 (800 KB)
| Video-BIOS (Graphiccard-ROM) | 32 KB ROM
+--------------------------------------------------+ 0xC0000 (768 KB)
| Video-RAM (VRAM for Textmode & VGA-Graphic) | 128 KB RAM
+==================================================+ 0xA0000 (640 KB)
| |
| |
| |
| |
| CONVENTIONAL MEMORY (RAM) |
| Free space for DOS, Programs, Drivers, etc. | ca. 605 KB
| |
| |
| |
+--------------------------------------------------+ 0x07E00
| Bootsector (loaded from boot-drive) | 512 Bytes
+--------------------------------------------------+ 0x07C00
| Free DOS-Memory / DOS-Kernel | ~29 KB
+--------------------------------------------------+ 0x00500
| BDA (BIOS Data Area) | 256 Bytes
+--------------------------------------------------+ 0x00400
| IVT (Interrupt-Vector-Tables) | 1 KB
+--------------------------------------------------+ 0x00000

So instead of 640kB we can only use 605kB of free RAM for our code as the IVT, BDA and the bootsector takes some memory. Between 0x0500 and 0x7C00 we have some space for the kernel and in the upper memory between 0xC8000 and 0xF0000 we have 160kB of high-memory that can be used if no option ROMs are available.

But enough about memory-maps for now, first I was unsure how to test my compiled code. Sure, the most common way is to use an EEPROM-programmer to burn the BIOS-image onto a EEPROM-IC. But this would prevent a fast development-cycle. So I searched for a better solution and found the PicoROM-project as well as the OneROM-project. Both projects use a RaspberryPi Pico-Controller that has enough memory available to emulate a ROM-IC. I ordered both devices from the USA and Great Britain, but after a while I was ready to test the code. First I uploaded the original ROM of the DDX3216 to see if the audio-mixing-console is booting without problems.

And the original software booted successfully:

So I was sure that the ROM-Emulator was working well as the original software booted up. To see if my code is working or not I wanted to get the external UART up and running. As you can see in the picture below, the DDX3216 has a RS232 9-pin-connector:

The problem: Behringer did not use the internal UART of the SC300 for this connector, but an external Toshiba TLC16C552 IC. The external UART is connected to address-lines SA0 to SA2 (blue circle) and the data-lines D0 to D7. The TLC16C552 consists of two serial-ports and a single parallel-port, while each function can be enabled using one of the three chip-select-signals CS0# to CS2# (green circle). These chip-select-signals are connected to some logic-ICs and here connected to SA3, SA4, SA12, SA13, SA14 and SA15:

Looking into the datasheets of the connected address-lines release that the CS0# is asserted when IO-address 0x1000 to 0x1007 is used, CS1# is asserted when IO-address 0x1008 to 0x100F is used and the final CS#2 is asserted with IO-addresses 0x1010 to 0x1017. Before we can use the 9-pin UART-connector at the back, we have to make sure, that the logic-IC "IC110" is enabled to pass the TxD signals. This IC is an 74HCT125 and the RS232# signal has to be asserted to enable the output of this IC. RS232# is connected to SLIN# of the TLC16C552 which is connected to the parallel output. So first we have to program the parallel-port, assert SLIN#, than initialize the serial-output for the debugging:

ASM; enable clock for external UART
out 0x0022, 0xBA ; set config-address
out 0x0023, 0b00001000 ; set config-data

; now program the external UART via IO-writes
; first the parallel-port interface at BASE-Address 0x1010
out 0x1012, 0x00001000 ; enable SLIN# = RS232# via parallel port

; now the serial-port interface at BASE-Address 0x1000
out 0x1003, 0x80 ; enable access to div-latches
out 0x1000, 0x5D ; set baud-divider (LSB)
out 0x1001, 0x00 ; set baud-divider (MSB)
out 0x1003, 0x03 ; reset DLAB-bit and set 8N1 mode
out 0x1001, 0x00 ; disable all interrupts
out 0x1002, 0x00 ; disable FIFO
out 0x1004, 0x03 ; set DTR and RTS ; enable clock for external UART
out 0x0022, 0xBA ; set config-address
out 0x0023, 0b00001000 ; set config-data

; now program the external UART via IO-writes
; first the parallel-port interface at BASE-Address 0x1010
out 0x1012, 0x00001000 ; enable SLIN# = RS232# via parallel port

; now the serial-port interface at BASE-Address 0x1000
out 0x1003, 0x80 ; enable access to div-latches
out 0x1000, 0x5D ; set baud-divider (LSB)
out 0x1001, 0x00 ; set baud-divider (MSB)
out 0x1003, 0x03 ; reset DLAB-bit and set 8N1 mode
out 0x1001, 0x00 ; disable all interrupts
out 0x1002, 0x00 ; disable FIFO
out 0x1004, 0x03 ; set DTR and RTS

This should be enough to enable the external UART and let the TxD-signals pass to the 9-pin-connector at the back. From the Programmers Reference Manual I learned the most important registers that have to be set after the boot-reset to bring the SoC in an operational state. So I programmed around 20 more configuration-registers of the ELAN SC300 to bring the SoC to 33MHz and up and running.

Links

Open - English
Open - English
Open - Deutsch
Open - Français
Open - Nederlands
Open - Italiano
Open - Español
Open - Português
Open - Русский
Open - 简体中文

Browse another page:

URL