This document is a complete standalone specification for the Bedrock 8-bit computer system, a stable and ultra-portable platform for compact applications that will run on any hardware imaginable. The system is implemented either as a thin emulator on top of any existing computer system, or as a bespoke piece of hardware built around any basic microcontroller. A broad set of device interfaces allows systems to expose a wide range of hardware capabilities.
The purpose of this document is to enable any person to implement an entire working system from scratch. System implementers will want to start by reading and implementing the architecture specification (needed to run Bedrock programs), followed by any number of device specifications (needed for programs to interface with hardware).
This document is structured as a set of individual specifications that each specify an aspect of the Bedrock computer system. The most important specification is the architecture specification, with all other specifications describing either standard tooling and formats, or devices which expose hardware capabilities to an existing system.
The core specifications form the foundation of the Bedrock computer system. They describe how to write, run, and inspect programs, and how to interface with physical device boards.
The device specifications describe twelve standard devices that connect to the first twelve slots of the Bedrock device bus to provide programs with useful hardware capabilities. These devices are optional enhancements and are not required to be implemented, however most programs will be of limited use unless at least a couple of these devices are present. Individual devices are expected to be omitted on systems where the necessary hardware is unavailable.
0x0System device specification Provides system information, and controls for sleeping, forking, and resetting the system.
The auxiliary device specifications describe devices that can connect to any of the last four slots of the Bedrock device bus to provide specialised functionality beyond the requirements of most programs. These devices are optional enhancements and are not required or expected to be implemented.
GPIO device specification Provides 32 general purpose IO pins, 4 analogue inputs, and 4 analogue outputs.
This is the third revision of the specification, released on TODO: Add date. It has been rewritten in a more linear format to be easier to read and includes specifications for the previously unspecified tone and waveform devices.
Byte An 8-bit value. This is the base unit of data used by Bedrock.
Double A 16-bit value, stored as a pair of bytes in big-endian order.
Implementation defined A value or behaviour that depends on the constraints or design of the underlying hardware or software. Implementations are free to choose any sensible value or behaviour. TODO: rework this at the end.
Undefined behaviour Behaviour that should not be triggered, can break the system / unstable / . Expected to never be triggered. A system is free to do anything at all, which could include corrupting data.
The program memory is an array of 65536 bytes that holds the current program. To load a program, set every byte of program memory to zero, set the stack and instruction pointers to zero, reset every device on the device bus to their initial states, and then copy each byte of the program into program memory starting from address zero, truncating the program to fit the available memory.
To read a double from program memory, read the high byte from the given address and the low byte from the following address. To write a double to program memory, write the high byte to the given address and the low byte to the following address. Reading or writing a double from address 0xFFFF will cause undefined behaviour.
Systems with fewer than 65536 bytes of program memory are partially supported. Accessing an unimplemented memory address will cause undefined behaviour.
A Bedrock system contains two stacks, called the working stack and the return stack. Each stack is an array of 256 bytes with an 8-bit pointer. To push a byte to a stack, write the byte to the address in the array referenced by the pointer and then increment the pointer. To pop a byte from a stack, decrement the pointer and then return the byte at the referenced address. Overflowing or underflowing the stack pointer will cause undefined behaviour.
To push a double to a stack, first push the high byte of the double and then push the low byte. To pop a double from a stack, pop two bytes; the first byte popped will be the low byte of the double and the second byte popped will be the high byte.
Systems with fewer than 256 bytes of memory per stack are partially supported. Accessing an unimplemented memory address will cause undefined behaviour.
The device bus is an array of 256 ports, grouped evenly into 16 slots. Each port is a channel for sending or receiving bytes from an external device, and each slot is a contiguous group of 16 ports that can connect a single device to the system. Reading from a port will send a read request to the connected device, returning a byte. Writing a byte to a port will send a write request to the connected device, sending that byte. A device will see which of the 16 ports of the slot that a request was sent to, and the processor will pause indefinitely until the device completes the request. If a request is sent to a port that isn’t currently connected to a device, that request will complete immediately, returning a value of zero if it was a read request.
To read a double from the device bus, first read the high byte from the given port and then read the low byte from the following port. To write a double to the device bus, first write the high byte to the given port and then write the low byte to the following port. Reading or writing a double from port 0xFF will cause undefined behaviour.
The first 12 slots are reserved for the standard devices (see the devices specification). The remaining four slots are available for the private use of each implementation.
The processor executes the current program using a 16-bit instruction pointer. To execute the next instruction, read a byte from the address in program memory referenced by the instruction pointer, then increment the instruction pointer, and then execute the instruction represented by that byte. Overflowing the instruction pointer will cause undefined behaviour.
The upper three bits of an instruction byte are the mode flags.
If bit 0x80 is set, swap the working stack and the return stack for the duration of this instruction.
If bit 0x40 is set, values without an explicit size in the operation description will be doubles, otherwise bytes.
If bit 0x20 is set, the first value to be popped by this instruction will instead be read from the address in program memory referenced by the instruction pointer, incrementing the instruction pointer with each byte read.
The lower five bits of an instruction byte represent the operation to perform, as per the following list. WST means “working stack”, RST means “return stack”, IP means “instruction pointer”. Arithmetic wraps on overflow and underflow. Pushing and popping is to the working stack by default.
0x00 Halt the system if no mode flag is set, otherwise do nothing.
0x01 Pop x from RST, push x to WST.
0x02 Pop x from WST.
0x03 Pop x from RST, push x to RST, push x to WST.
0x04 Pop x from WST, push x then x to WST.
0x05 Pop y, x, push x, y, x.
0x06 Pop y, x, push y, x.
0x07 Pop z, y, x, push y, z, x.
0x08 Pop double a, write a to IP.
0x09 Pop double a, push IP to RST, write a to IP.
0x0A Pop double a, pop t. If t is not zero, write a to IP.
0x0B Pop double a, pop t. If t is not zero, push IP to RST, write a to IP.
0x0C Pop double a, read v from memory address a, push v.
0x0D Pop double a, pop v, write v to memory address a.
0x0E Pop byte p, read v from device port p, push v.
0x0F Pop byte p, pop v, write v to device port p.
0x10 Pop y, x, push y plus x.
0x11 Pop y, x, push y minus x.
0x12 Pop x, push x plus 1.
0x13 Pop x, push x minus 1.
0x14 Pop y, x. If x is less than y, push byte 0xFF, else 0x00.
0x15 Pop y, x. If x is greater than y, push byte 0xFF, else 0x00.
0x16 Pop y, x. If x is equal to y, push byte 0xFF, else 0x00.
0x17 Pop y, x, push x, y. If x is not equal to y, push byte 0xFF, else 0x00.
0x18 Pop byte y, pop x, push x bit-shifted left by y bits.
0x19 Pop byte y, pop x, push x bit-shifted right by y bits.
0x1A Pop byte y, pop x, push x bit-rotated left by y bits.
0x1B Pop byte y, pop x, push x bit-rotated right by y bits.
0x1C Pop y, x, push x bitwise-OR y (each bit is set only if at least one corresponding bit of x or y is set).
0x1D Pop y, x, push x bitwise-XOR y (each bit is set only if exactly one corresponding bit of x or y is set).
0x1E Pop y, x, push x bitwise-AND y (each bit is set only if the corresponding bits of x and y are set).
0x1F Pop x, push bitwise-NOT x (each bit is set only if the corresponding bit of x is not set).
0x00 Halt the system if no mode flag is set, otherwise do nothing.
0x01 Pop x from RST, push x to WST.
0x02 Pop x from WST.
0x03 Pop x from RST, push x to RST, push x to WST.
0x04 Pop x, push x, x.
0x05 Pop y, x, push x, y, x.
0x06 Pop y, x, push y, x.
0x07 Pop z, y, x, push y, z, x.
0x08 Pop double a, write a to IP.
0x09 Pop double a, push IP to RST, write a to IP.
0x0A Pop double a, pop t. If t is not zero, write a to IP.
0x0B Pop double a, pop t. If t is not zero, push IP to RST, write a to IP.
0x0C Pop double a, read v from memory address a, push v.
0x0D Pop double a, pop v, write v to memory address a.
0x0E Pop byte p, read v from device port p, push v.
0x0F Pop byte p, pop v, write v to device port p.
0x10 Pop y, x, push y plus x.
0x11 Pop y, x, push y minus x.
0x12 Pop x, push x plus 1.
0x13 Pop x, push x minus 1.
0x14 Pop y, x. If x is less than y, push byte 0xFF, else 0x00.
0x15 Pop y, x. If x is greater than y, push byte 0xFF, else 0x00.
0x16 Pop y, x. If x is equal to y, push byte 0xFF, else 0x00.
0x17 Pop y, x, push x, y. If x is not equal to y, push byte 0xFF, else 0x00.
0x18 Pop byte y, pop x, push x bit-shifted left by y bits.
0x19 Pop byte y, pop x, push x bit-shifted right by y bits.
0x1A Pop byte y, pop x, push x bit-rotated left by y bits.
0x1B Pop byte y, pop x, push x bit-rotated right by y bits.
0x1C Pop y, x, push x bitwise-OR y (each bit is set only if at least one corresponding bit of x or y is set).
0x1D Pop y, x, push x bitwise-XOR y (each bit is set only if exactly one corresponding bit of x or y is set).
0x1E Pop y, x, push x bitwise-AND y (each bit is set only if the corresponding bits of x and y are set).
0x1F Pop x, push bitwise-NOT x (each bit is set only if the corresponding bit of x is not set).
A source file is a sequence of characters, where each character is a Unicode scalar value. To assemble a program, parse the source file as a sequence of tokens, convert each token into a sequence of bytes, and concatenate the sequences of bytes to create the assembled program. If the source file is invalid according to the rules of the assembler, no program will be created.
A token is a sequence of characters from the source file that represents a single language element. The address of a token is equal to the number of bytes assembled before it in the program.
To parse the source file as a sequence of tokens, iterate over the characters of the source file and collect them into tokens according to the following rules:
The characters U+0000 to U+0020 are ignored if no token is in progress.
The characters ', ", or ( start a span token. Collect all characters up to and including the matching terminator, ending the token. The matching terminators for ', ", and ( are ', ", and ), respectively. The source file is invalid if a matching terminator cannot be found.
All other characters start a word token. If the character is ), [, ], {, }, ;, or :, no further characters are collected, ending the token. Otherwise, collect all characters up to and including the next :, or up to and excluding the next (, ), [, ], {, }, ;, or a character in the range U+0000 to U+0020, or until the end of the source file, whichever comes first.
The first character of a token determines how it will assemble into a sequence of bytes, as follows:
(, ), [, and ] denote a comment, which assembles to nothing. The source file is invalid if it contains an unmatched
{ and } denote an opening or closing block delimiter, respectively. A closing block delimiter matches the closest previous unmatched opening block delimiter, forming a pair. The opening delimiter assembles to a double with value equal to the address of the closing delimiter. The closing delimiter assembles to nothing. The source file is invalid if it contains an unmatched block delimiter, or if a delimiter inside a macro definition is matched with a delimiter outside that definition, or if the address of a closing delimiter is greater than 0xFFFF.
@ and & denote a global or local label definition, respectively. The remaining characters of the token are called the identifier. The name of a global label definition is given by the identifier. The name of a local label definition is given by the name of the most recent global label definition (or a blank string if none), followed by a / character, followed by the identifier. A label definition assembles to nothing. The source file is invalid if a label definition shares a name with another label or macro definition, or if the name of a label definition is longer than 63 characters, or if the address of a label definition is greater than 0xFFFF. The maximum number of label definitions allowed in a program is implementation defined.
% and ; denote a macro definition or terminator, respectively. A macro definition token matches the next terminator token, and the sequence of tokens between the two is called the macro body. The name of the macro definition is given by the remaining characters of the macro definition token. The macro definition, macro body, and terminator each assemble to nothing. The source file is invalid if it contains an unmatched macro definition or terminator token, or if a macro body contains a label or macro definition token, or if a macro definition shares a name with another label or macro definition, or if the name of a macro definition is longer than 63 characters. The maximum number of macro definitions allowed in a program is implementation defined.
' and " denote a raw or terminated string, respectively. The remaining characters of the token, excluding the final character, are called the string content. A raw string assembles to the string content as a UTF-8 encoded byte sequence. A terminated string assembles to the string content as a UTF-8 encoded byte sequence, followed by a zero byte.
# denotes a padding token. The remaining characters of the token are called the pad value, and represent an integer value as a hexadecimal number. A padding token assembles to a sequence of zero bytes with length equal to this value. The source file is invalid if the pad value is not exactly two or four characters long, or if the pad value contains characters that don’t fall within the range 0 to 9, a to f, or A to F.
If the first character of the token is any other character, the token will either be a literal or a symbol:
A literal is exactly two or four characters long, where each character falls within the range 0 to 9, a to f, or A to F. The characters of the token represent an integer value as a hexadecimal number. A two or four character literal token assembles to a byte or a double, respectively, with value equal to this value.
Any other token is a symbol. The name of the symbol is given by the characters of the token. If the first character of the name is ~, replace this character with the name of the most recent global label (or a blank string if none), followed by a / character. If the name of the symbol matches the name of a previous or future label definition token, it assembles to a double with value equal to the address of that label definition token. If the name of the symbol is equal to the name of a previous macro definition token, replace this token with a copy of the tokens in the associated macro body and assemble. The name of a symbol inside a macro body is determined when the macro body is first collected. The source file is invalid if the name of a symbol is longer than 63 characters, or if the name is not equal to the name of a label or macro definition token, or if the name is equal to the name of a macro definition token that comes after the symbol token in the source file.
The presence of the 10-byte marker sequence E8 00 18 42 45 44 52 4F 43 4B at the start of a Bedrock program indicates that the following 14 bytes of the program are a table of pointers to metadata values. Each pointer in the table is a double that holds the address of a specific metadata element. If a pointer has a value of zero, the corresponding metadata element has not been provided.
The program identifier is a UTF-8 encoded string followed by a zero byte. The string contains the program name, followed by the / character, followed by the program version. The program name and version must not contain a / character or any character in the range U+0000 to U+001F. The maximum length of the program identifier is 255 bytes, excluding the zero byte. The address of the program identifier is given by the pointer at address 0x000A.
The author list is a UTF-8 encoded string followed by a zero byte. The string is a concatenated list of the names of the program authors, with every name but the last followed by a newline character U+000A. The name of each author must not contain any character in the range U+0000 to U+001F. The maximum length of each name is 255 bytes, excluding the newline character and zero byte, and the maximum number of names in the list is 16. The address of the author list is given by the pointer at address 0x000C.
The description is a UTF-8 encoded string followed by a zero byte. The string is a short description of the program, summarising category and purpose. The description must not contain any character in the range U+0000 to U+001F. The maximum length of the description is 255 bytes, excluding the zero byte. The address of the description is given by the pointer at address 0x000E.
The background colour is a double that represents the 12-bit RGB colour to use when drawing the background of the program icons. The address of the background colour is given by the pointer at address 0x0010.
The foreground colour is a double that represents the 12-bit RGB colour to use when drawing the foreground of the program icons. The address of the foreground colour is given by the pointer at address 0x0012.
The small icon is a sequence of nine 1-bit sprites that represents a 24x24 pixel program icon. The sprites are ordered left-to-right and then top-to-bottom, forming a 3x3 grid. The total length of the sprite data is 72 bytes. The address of the small icon is given by the pointer at address 0x0014.
The large icon is a sequence of 64 1-bit sprites that represents a 64x64 pixel program icon. The sprites are ordered left-to-right and then top-to-bottom, forming an 8x8 grid. The total length of the sprite data is 512 bytes. The address of the large icon is given by the pointer at address 0x0016.
The following data types are used in some elements:
A 12-bit RGB colour is stored as a double. The highest group of four bits in the double is ignored, and the remaining groups of four bits in the double represent the intensities of the red, green, and blue colour channels, respectively.
A 1-bit sprite is an 8x8 pixel image stored as an 8-byte sequence. Each byte represents a row of the sprite, ordered top to bottom, and each bit of a byte represents a pixel in the row, ordered left to right, from highest bit to lowest. If a bit is set, the corresponding pixel of the sprite will be drawn as the foreground colour, else as the background colour.
A device map is a 16-bit value that represents the 16 slots on the device bus. Each bit of a device map represents a slot on the device bus, ordered high to low.
A text buffer is an array of bytes with an associated pointer. The array contains a UTF-8 encoded string followed by a zero byte. The initial value of the pointer is zero, and the maximum value of the pointer is at least one greater than the address of the zero byte. Reading a byte from the text buffer will return the byte at the array address referenced by the pointer, and then the pointer will increment. Reading a byte when the value of the pointer is greater than the address of the zero byte will cause undefined behaviour. Restarting the text buffer will set the value of the pointer to zero.
This device contains six text buffers: a system identifier text buffer, a system authors text buffer, and a device name text buffer for each slot 0xC to 0xF on the device bus.
The system identifier text buffer contains a string that identifies this Bedrock system implementation. The string contains the implementation name, followed by the / character, followed by the implementation version. The implementation name and version must not contain a / character or any character in the range U+0000 to U+001F. The maximum length of the string is 255 bytes.
The system authors text buffer contains a string that identifies the authors of this Bedrock system implementation. The string is a concatenated list of the names of the system authors, with every name but the last followed by the newline character U+000A. The name of each author must not contain any character in the range U+0000 to U+001F. The maximum length of each name is 255 bytes, and the maximum number of names in the list is 16.
Each device name text buffer contains a string that identifies the device currently connected to the corresponding slot. The string must not contain any character in the range U+0000 to U+001F. The maximum length of the string is 255 bytes. If no device is connected to the corresponding slot or if no device name is available, the string will be empty. Connecting or disconnecting a device from the slot will set the pointer of the text buffer to zero.
0x00–0x01 Writing to this port group will request that the system be put to sleep. Write access is atomic.
0x02 Reading from this port will return the slot number of the device that most recently woke the system from sleep. If the system has not yet been woken from sleep, the value returned will be 0x00.
0x03 Writing a non-zero value to this port will create a new Bedrock system instance, with separate core and devices to the current instance, and then the program memory of the current instance will be copied into the new instance. If a new instance cannot be created, the current instance will be reset as if a zero value had been written to this port. Writing a zero value to this port will reset this Bedrock system instance, by setting the instruction pointer and stack pointers to zero, leaving the program memory unmodified, and resetting all devices to their initial states.
0x04 This port is associated with the device name text buffer for slot 0xC. Reading from this port will read and return a byte from the associated text buffer. Writing any value to this port will set the pointer of the associated text buffer to zero.
0x05 This port is associated with the device name text buffer for slot 0xD. It behaves the same as port 0x04.
0x06 This port is associated with the device name text buffer for slot 0xE. It behaves the same as port 0x04.
0x07 This port is associated with the device name text buffer for slot 0xF. It behaves the same as port 0x04.
0x08 This port is associated with the system identifier text buffer. It behaves the same as port 0x04.
0x09 This port is associated with the system authors text buffer. It behaves the same as port 0x04.
0x0A–0x0B Reading from this port group will return the number of bytes of program memory implemented on this system. If the number of bytes implemented is 65536, the value returned will be 0x0000.
0x0C This port is associated with the working stack of the Bedrock system. Reading from this port will return the number of bytes of memory implemented for the associated stack on this system. If the number of bytes implemented is 256, the value returned will be 0x00.
0x0D This port is associated with the return stack of the Bedrock system. It behaves the same as port 0x0C.
0x0E–0x0F Reading from this port group returns a device map, where each bit of the map is set only if a device is connected to the corresponding slot on the device bus.
This device provides access to up to 16 megabytes of additional memory. The memory managed by this device does not share an address space with the program memory.
Memory is allocated in 256-byte chunks, called pages, which form a contiguous array of bytes starting from address zero. Pages are allocated and deallocated from the end of this array. Accessing an unallocated memory address will cause undefined behaviour. When a page is allocated, the value of each byte on that page is zero. The initial number of pages allocated is zero, and the minimum and maximum numbers of pages that can be allocated are implementation defined.
Memory is accessed via two independent read-write heads (called head 1 and head 2). Each head contains an unsigned 16-bit page offset value and an unsigned 16-bit address offset value. The memory address that will be accessed by a given head is calculated by multiplying the page offset by 256 and then adding the address offset, causing undefined behaviour if the result exceeds the range of an unsigned 24-bit integer.
0x10–0x11 Reading from this port group will return the number of pages allocated. Writing to this port group will request that the number of pages allocated be changed to the value written. Pages will be sequentially allocated or deallocated until either the requested value or an implementation defined minimum or maximum value is reached. Write access is atomic.
0x12–0x13 This port group is associated with head 1. Reading from this port group will return the page offset of the associated head. Writing to this port group will set the page offset of the associated head to the value written.
0x14–0x15 This port group is associated with head 1. Reading from this port group will return the address offset of the associated head. Writing to this port group will set the address offset of the associated head to the value written.
0x16 This port is associated with head 1. Reading from this port will return the byte stored at the memory address referenced by the associated head, and then will increment the address offset of the associated head by 1, wrapping to zero on overflow.
0x17 This port is an alias for port 0x16.
0x18–0x19 Writing to this port group will request that a number of pages be copied equal to the value written. The initial source and destination page are given by the page offset value of head 2 and head 1, respectively. The contents of the source page will be copied into the destination page, the page following the source or destination page will become the new source or destination page, respectively, and the process will repeat until the requested number of pages has been copied. Copying to or from an unallocated page will cause undefined behaviour. Write access is atomic.
0x1A–0x1B This port group is associated with head 2. It behaves the same as port group 0x12–0x13.
0x1C–0x1D This port group is associated with head 2. It behaves the same as port group 0x14–0x15.
0x1E This port is associated with head 2. It behaves the same as port 0x16.