diff --git a/01_CORE.md b/01_CORE.md
deleted file mode 100644
index 7451cfb..0000000
--- a/01_CORE.md
+++ /dev/null
@@ -1,80 +0,0 @@
-# GEX core protocol
-
-## Framing layer
-
-GEX uses [TinyFrame](https://github.com/MightyPork/TinyFrame) to form and parse data frames.
-
-- Frames are sent and received using two USB bulk endpoints (VCOM or raw access) or through UART.
-- Transmitted frame length is virtually unlimited
-- Received frame length is limited by the Rx buffer size.
-
-### Frame structure
-
-```none
-,------+----------+---------+------+------------+- - - - -+------------,                
-| SOF  | frame_id | pld_len | type | head_cksum | payload | pld_cksum  |                
-| 1    | 2        | 2       | 1    | 1          | ...     | 1          | <- size (bytes)
-'------+----------+---------+------+------------+- - - - -+------------'      
-```
-
-- SOF byte is `0x01`
-- Checksum = inverted XOR of all bytes
-
-The checksum is used to catch implementation mistakes (such as mistaken CR-LF transformations).
-USB bulk transfers have CRC error checking built-in.
-
-*Frame ID* is incremented with each transaction (message, request-response or longer). 
-The highest ID bit identifies the peer who started the transaction.
-
-*Type* defines the meaning of the frame. A list is attached below.
-
-## Message types
-
-### General messages
-- `0x00` ... `SUCCESS` - Generic success response; used by default in all responses; payload is transaction-specific
-- `0x01` ... `PING` - Ping request, used to test connection
-  - Response: ASCII string with the version and platform name
-- `0x02` ... `ERROR` - Generic failure response (when a request fails to execute)
-  - Response: ASCII string with the error message
-
-### Bulk transfer (large multi-frame transfer)
-Bulk transfer is used for reading / writing large files that exceed the TinyFrame buffer sizes.
-
-- `0x03` ... `BULK_READ_OFFER` - Offer of data to read. 
-  - Payload: u32 total len
-- `0x04` ... `BULK_READ_POLL` - Request to read a previously announced chunk (`BULK_READ_OFFER`)
-  - Payload: u32 max chunk size to read
-  - Response: a `BULK_DATA` frame
-- `0x05` ... `BULK_WRITE_OFFER` - Offer to receive data in a write transaction.
-  - This is an affirmation to some other frame that introduced a request to write bulk data.
-  - Payload: u32 max total size, u32 max chunk size
-- `0x06` ... `BULK_DATA` - Writing a chunk, or sending a chunk to master.
-- `0x07` ... `BULK_END` - Bulk transfer is done, no more data to read or write. Recipient shall check total len and discard the received data on mismatch. 
-- `0x08` ... `BULK_ABORT` - Discard the ongoing transfer (sent by either peer)
-
-### Unit messages
-Units are instances of a particular unit driver. A unit driver is e.g. Digital Output or SPI.
-Each unit has a TYPE (which driver to use), NAME (for user convenience) and CALLSIGN (for messages)
-
-In the INI file, all three are shown in the unit section: `[type:name@callsign]`
-
-Units can accept commands and generate events. Both are identified by 1-byte codes.
-
-- `0x10` ... `UNIT_REQUEST` - Command addressed to a particular unit
-  - Payload: u8 callsign, u8 command, rest - data for a unit driver
-- `0x11` ... `UNIT_REPORT` - Spontaneous report from a unit
-  - Payload: u8 callsign, u8 type, u64 usec timestamp, rest: data from unit driver
-
-### System messages
-Settings management etc.
-
-- `0x20` ... `LIST_UNITS` - Get all active unit call-signs, types and names
-  - Response: u8 count, { u8 callsign, cstring type, cstring name }
-- `0x21` ... `INI_READ` - Read the ini file via bulk
-  - starts a bulk read transfer
-  - Response: a `BULK_READ_OFFER` frame
-- `0x22` ... `INI_WRITE` - Write the ini file via bulk
-  - starts a bulk write transfer
-  - Response: a `BULK_WRITE_OFFER` frame
-- `0x23` ... `PERSIST_CFG` - Write current settings to Flash (the equivalent of replacing the lock jumper / pushing the button if VFS is active)
-
diff --git a/FRAME_FORMAT.md b/FRAME_FORMAT.md
new file mode 100644
index 0000000..9519844
--- /dev/null
+++ b/FRAME_FORMAT.md
@@ -0,0 +1,111 @@
+# GEX's low-level communication protocol
+
+## Framing layer
+
+GEX uses [TinyFrame](https://github.com/MightyPork/TinyFrame) to form and parse data frames.
+
+- Frames are normally sent and received using two USB bulk endpoints (VCOM or raw access) or through UART.
+- Transmitted frame length is virtually unlimited
+- Received frame length is limited by the Rx buffer size. This is overcome using a Bulk Write function.
+
+### Frame structure
+
+See the TinyFrame documentation for detailed description of the frame structure and protocol functions.
+
+Here's the configuration used by GEX:
+
+```none
+,------+----------+---------+------+------------+- - - - -+------------,
+| SOF  | frame_id | pld_len | type | head_cksum | payload | pld_cksum  |
+| 1    | 2        | 2       | 1    | 1          | ...     | 1          | <- size (bytes)
+'------+----------+---------+------+------------+- - - - -+------------'
+```
+
+- SOF byte is `0x01`
+- Checksum = inverted XOR of all bytes
+
+USB bulk transfers have CRC error checking built-in. The checksum field is mainly used to catch
+implementation mistakes, such as a CR-LF transformation.
+
+*Frame ID* is incremented with each transaction (message, request/response, or longer).
+The highest ID bit identifies the peer who started the transaction ("master", "slave").
+
+*Type* defines the meaning of the frame within a higher level protocol. A list of all types is attached below.
+
+## Message types
+
+### General messages
+- `0x00` ... `SUCCESS` - Generic success response; used by default in all responses;
+  - *Payload:*
+    - optional, differs based on the request
+- `0x01` ... `PING` - Ping request, used to test connection
+  - *Response:*
+    - `SUCCESS` frame with an ASCII string containing the GEX version and the hardware platform name
+- `0x02` ... `ERROR` - Generic failure response, used when a request fails to execute
+  - *Payload:*
+    - ASCII string with the error message
+
+### Bulk transfer (large multi-frame transfer)
+Bulk transfer is used for reading / writing large files that exceed the TinyFrame buffer sizes.
+
+- `0x03` ... `BULK_READ_OFFER` - Offer of data to read.
+  - *Payload:*
+    - u32 - total len
+- `0x04` ... `BULK_READ_POLL` - Request to read a previously announced chunk (`BULK_READ_OFFER`)
+  - *Payload:*
+    - u32 - max chunk size to read
+  - Response: a `BULK_DATA` frame (see below)
+- `0x05` ... `BULK_WRITE_OFFER` - Offer to receive data in a write transaction.
+  - This is a report of readiness after some other frame introduced a request to write bulk data
+    (e.g. writing a config file)
+  - *Payload:*
+    - u32 - max total size
+    - u32 - max chunk size
+- `0x06` ... `BULK_DATA` - Writing a chunk, or sending a chunk to master.
+  - *Payload:*
+    - a block of data
+- `0x07` ... `BULK_END` - Bulk transfer is done, no more data to read or write. Recipient shall check total length and discard the received data on mismatch.
+- `0x08` ... `BULK_ABORT` - Abort and discard the ongoing transfer (sent by either peer)
+
+### Unit messages
+Units are instances of a particular unit driver. A unit driver is e.g. Digital Output or SPI.
+Each unit has a TYPE (which driver to use), NAME (for user convenience) and CALLSIGN (for messages)
+
+In the INI file, all three are shown in the unit section: `[type:name@callsign]`
+
+Units can accept commands and generate events. Both are identified by 1-byte codes.
+
+- `0x10` ... `UNIT_REQUEST` - Command addressed to a particular unit
+  - *Payload:*
+    - u8 callsign
+    - u8 command
+    - rest - data for a unit driver
+- `0x11` ... `UNIT_REPORT` - Spontaneous report from a unit
+  - *Payload:*
+    - u8 callsign
+    - u8 report type
+    - u64 usec timestamp
+    - rest - data from unit driver
+
+### System messages
+Settings management etc.
+
+- `0x20` ... `LIST_UNITS` - Get all active unit call-signs, types and names
+  - *Response:*
+    - u8 number of units
+    - repeat for each unit:
+      - u8 callsign
+      - cstring type
+      - cstring name
+- `0x21` ... `INI_READ` - Read the ini file via bulk
+  - starts a bulk read transfer
+  - *Response:*
+    - a `BULK_READ_OFFER` frame (see above)
+- `0x22` ... `INI_WRITE` - Write the ini file via bulk
+  - starts a bulk write transfer
+  - *Response:*
+    - a `BULK_WRITE_OFFER` frame (see above)
+- `0x23` ... `PERSIST_CFG` - Write current settings to Flash
+  - This is the equivalent of replacing the lock jumper / pushing the button when the virtual filesystem is enabled.
+  - If the virtual filesystem is enabled, this also closes it.
+
diff --git a/INI_FILES.md b/INI_FILES.md
new file mode 100644
index 0000000..e257bf3
--- /dev/null
+++ b/INI_FILES.md
@@ -0,0 +1,97 @@
+# GEX's config files
+
+GEX's configuration is stored in the internal flash memory in a packed binary form.
+To make its editing easier for the user, the configuration is accessible as INI files
+annotated by numerous comments explaining the individual config options.
+
+## Filesystem access
+
+There are two config files: `UNITS.INI` and `SYSTEM.INI`.
+
+To access those files, the user presses the LOCK button (or removes the LOCK jumper),
+which enables an emulated FAT16 storage that will appear on the host computer as a USB disk.
+Some hardware platforms require that the jumper be removed before plugging in the USB cable,
+as it can't trigger re-enumeration without external circuitry, which is missing on those
+evaluation boards.
+
+It's recommended to copy the INI files to a real disk before editing, as some editors create
+temporary back-up files that can confuse the filesystem emulator. However, in-place editing
+is also possible with some editors.
+
+*This method is the only way of accessing the `SYSTEM.INI` file, which can reconfigure
+the main communication port.*
+
+After writing the changed files back to the virtual disk, it will briefly disappear
+and an updated version of the files will become available for review.
+This is when you can check for error messages.
+
+To confirm the config modifications (they take effect imemdiately, but are not written to flash yet),
+push the LOCK button again (or replace the LOCK jumper).
+
+## API access
+
+The `UNITS.INI` file may be accessed through the communication port.
+
+The file is read using Bulk Read (see [FRAME_FORMAT.md](FRAME_FORMAT.md)) after sending a `INI_READ` request.
+
+The modified file is written back using Bulk Write after sending a `INI_WRITE` request.
+
+After writing the changed configuration, it may be persisted to flash using a `PERSIST_CFG` command.
+This is not required if the new configuration will be used only temporarily; the original settings
+will be restored after a restart.
+
+## UNITS.INI file structure
+
+The `UNITS.INI` file is interactive. The general layout is as follows:
+
+```ini
+## UNITS.INI
+## GEX v0.0.1 on STM32F072-HUB
+## built Mar 17 2018 at 17:53:15
+
+# Overwrite this file to change settings.
+# Press the LOCK button to save them to Flash.
+
+[UNITS]
+# Create units by adding their names next to a type (e.g. DO=A,B),
+# remove the same way. Reload to update the unit sections below.
+
+# Digital output
+DO=
+# Digital input with triggers
+DI=btn,btn2
+# (... more unit types here)
+
+[DO:btn@1]
+# Port name
+port=B
+# Pins (comma separated, supports ranges)
+pins=2
+# Initially high pins
+initial=2
+# Open-drain pins
+open-drain=
+```
+
+- The keys in the `[UNITS]` section are lists of instances for each unit type.
+  After adding a name to the list and saving the file, a new unit section will appear below, offering to configure
+  the new unit as needed.
+- Each unit section starts with `[type:name@callsign]`.The name must match the name used in the unit lists above.
+  The callsign can be freely changed, as long as there is no conflict. Callsign 0 is forbidden and will be changed
+  to a new unique callsign on save.
+- If an error occurs during unit initialization, an error message will appear under the unit's section header.
+
+## GEX reconfiguration workflow
+
+1. Enable the mass storage using the LOCK button or jumper (or use the API for reading and writing the file)
+2. Open the `UNITS.INI` file in a text editor
+3. If adding or removing unit instances, add their names to the right type (eg. `DO=LED`), save and reload the file.
+   On some systems, it's necessary to close the editor right after saving and then reopen it again after the disk
+   refreshes.
+4. Configure the units as needed; adjust callsigns if desired. Save and reload the file.
+5. Check for error messages under unit headers. Correct any problems. Save and reload to apply any changes.
+6. Push the LOCK button or replace the jumper to persist all changes to flash. If using the API, issue
+   the `PERSIST_CFG` command for the same effect.
+
+This is much easier when done through the API, perhaps using the PyQt GEX configuration utility included as an
+example in the Python client repository.
diff --git a/INTERNAL_STRUCTURE.md b/INTERNAL_STRUCTURE.md
new file mode 100644
index 0000000..15228f8
--- /dev/null
+++ b/INTERNAL_STRUCTURE.md
@@ -0,0 +1,44 @@
+# GEX's internal structure
+
+GEX is modular, composed of functional blocks called *units*. A unit can be e.g. SPI or a digital output.
+
+There can be more than one instance of a given unit, based on the microcontroller's hardware resources.
+Some units use hardware peripherals, others are implemented in software. Those "soft" units (typically
+bit-banged protocols) are limited only by the available GPIO pins.
+
+## Resources
+
+A system of resource claiming is implemented to avoid units conflicting with each other and interfering with
+peripherals used internally by the system (such as the LED pin). Each unit claims the resources it uses during
+initialization. If the resource needed is not available, it may try to claim an alternate resource or fail to
+initialize.
+
+A init failure is reported in the config file as an error message under the unit's header.
+
+An example of defined resources:
+
+- all GPIO pins
+- timers/counters
+- SPI, I2C, USART peripherals
+- DMA channels
+
+## Unit identification
+
+Each unit has a *name* and a *callsign* (1-255) used for message addressing. Both are defined in the config file.
+
+There can be up to 255 units, in practice fewer due to storage limitations.
+
+## Unit commands and events
+
+Units can receive command messages (`UNIT_REQUEST`) and report asynchronous events using `UNIT_REPORT` frames.
+
+A command is identified by a number 0-127. The highest bit (0x80) of the command number is reserved to request
+confirmation (a `SUCCESS` frame) if the command doesn't normally generate a response. This is used by the client
+library to check that an operation was completed correctly (as opposed to e.g. the firmware crashing and rebooting
+through the watchdog).
+
+Unit events include the unit's callsign, a number 0-255 identifying the event type, and a 64-bit microsecond
+timestamp.
+
+See [FRAME_FORMAT.md](FRAME_FORMAT.md) for the payloads structure.
+
diff --git a/README.md b/README.md
index 15522b3..b4bc688 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,10 @@
 # GEX protocol documentation
 
 This repository specifies the control protocol implemented by [gex-core](https://github.com/gexpander/gex-core).
+
+- [FRAME_FORMAT.md](FRAME_FORMAT.md) - Low level frame format, message types
+- [INTERNAL_STRUCTURE.md](INTERNAL_STRUCTURE.md) - GEX's internal structure, units, requests and events
+- [INI_FILES.md](INI_FILES.md) - INI config files, filesystem and API access
+
+- [UNIT_DO.md](UNIT_DO.md) - Digital Output unit
+
diff --git a/UNIT_DO.md b/UNIT_DO.md
new file mode 100644
index 0000000..25a7242
--- /dev/null
+++ b/UNIT_DO.md
@@ -0,0 +1,52 @@
+# Digital Output
+
+- Direct output access to one or more pins of a port.
+- All selected pins are written simultaneously.
+- Supports generating output pulses of precise length.
+
+Pins are ordered in a descending order (DOWNTO) and accessed in a packed format,
+e.g. if pins 1 and 4 are selected (0b10010 on the port), the control word has
+two bits (4)(1) and e.g. 0b10 means pin 4, 0b11 both. This makes accessing the
+block of pins easier, e.g. when using them to drive a parallel bus.
+
+## Commands
+
+### WRITE
+Write a value to all defined pins.
+
+*Payload:*
+- u16 - new value (packed)
+
+### SET
+Set pins high
+
+*Payload:*
+- u16 - pins to set high (packed)
+
+### CLEAR
+Set pins low
+
+*Payload:*
+- u16 - pins to set low (packed)
+
+### TOGGLE
+Toggle selected pins (high - low)
+
+*Payload:*
+- u16 - pins to toggle (packed)
+
+### PULSE
+Send a pulse. The start will be aligned to 1 us or 1 ms (based on pulse length) of the internal timebase to ensure the highest length precision. (This alignment reduces time jitter.)
+
+*Payload:*
+- u16 - pins to generate the pulse on (packed)
+- u8 - pulse active level (0, 1)
+- u8 - range (0 - milliseconds, 1 - microseconds)
+- u16 - duration in the selected range
+
+*NOTE:* The microsecond range supports durations only up to 999 (1 ms), higher
+numbers will be divided by 1000 and use the millisecond range instead.
+
+## Events
+
+*No events defined for this unit type.*