Firmware link
DCS and RepRapFirmware (RRF) exchange data over a binary link. Every code DCS does not handle itself crosses this link, and RRF uses the same link to ask DCS for macros, file I/O, and SBC-side code execution. The link supports two transports behind a common interface:
- SPI - the SBC is the SPI master and a GPIO line (
TfrRdy) signals readiness. The default. - USB - a serial connection over
/dev/ttyACM*.
The choice is made by the CommunicationMethod setting ("spi" or "usb") and wired up in
Link/Extensions.cs. Everything above the transport - the transfer loop, per-channel buffering, the
packet protocol, and the firmware-request handlers - is shared.
- Transport interface:
src/DuetControlServer/Link/Adapter/ILinkAdapter.cs - Transports:
Link/Adapter/SPI.cs,Link/Adapter/USB.cs - Transfer loop and request handlers:
Link/LinkService.cs - Higher-level link API:
Link/LinkInterface.cs - Per-channel buffering:
Link/Channel/Processor.cs,Channel/Manager.cs - Wire format:
Link/Protocol/
This article continues from the Firmware stage of the code pipeline: once a code reaches that stage, the link layer described here transmits it.
Layering
flowchart TD
FW["Firmware stage<br/>(gcode-flow.md)"] --> CHAN["Channel processors<br/>per-channel code buffering"]
CHAN --> LI["LinkInterface<br/>buffer-space accounting, high-level API"]
LS["LinkService<br/>transfer loop + request routing"] --> LI
LI --> ADAPTER{"ILinkAdapter"}
ADAPTER -- "CommunicationMethod = spi" --> SPI["SPI adapter<br/>/dev/spidev0.0 + GPIO TfrRdy"]
ADAPTER -- "CommunicationMethod = usb" --> USB["USB adapter<br/>/dev/ttyACM*"]
SPI <--> RRF["RepRapFirmware"]
USB <--> RRF
ILinkAdapter is the transport abstraction. It exposes Connect, PerformFullTransfer,
ReadNextPacket, and a set of typed readers for firmware requests (ReadMacroRequest,
ReadMessage, ReadObjectModel, ReadCodeBufferUpdate, ...). LinkService, LinkInterface, and the
channel processors are written entirely against this interface, so they do not care which transport is
in use.
The transfer loop
One high-priority thread runs LinkService.Execute():
flowchart TD
subgraph loop["LinkService.Execute() - highest priority"]
RECV["Read received packets<br/>ProcessPacket() routes each"] --> SPIN["channels.Spin()<br/>round-robin over channels"]
SPIN --> BUF["Processor.Spin() per channel:<br/>replies, locks, aborts,<br/>macros, BufferCode()"]
BUF --> XFER["adapter.PerformFullTransfer()"]
XFER --> RECV
end
FW["Firmware stage<br/>(gcode-flow.md)"] --> BUF
XFER <-->|"chosen transport"| RRF["RepRapFirmware"]
Each iteration: read and route the packets RRF sent, let every code channel buffer outgoing codes, then perform one full transfer over the active transport.
Sending codes to the firmware
Link/Channel/Manager.cs visits each channel round-robin and calls Processor.Spin()
(Link/Channel/Processor.cs), which handles, in priority order: pending replies, lock/unlock
requests, abort requests, macro management, resumption of suspended codes, then buffering of new codes
via BufferCode().
Each channel tracks a BufferedCodes list and a BytesBuffered counter. A code is only buffered when
it fits within MaxBufferSpacePerChannel and RRF has reported enough free space (RRF sends
CodeBufferUpdate packets; LinkInterface keeps a running BufferSpace figure). The code is
serialized by Link/Protocol/Writer.cs into a CodeHeader plus its parameters, wrapped in an 8-byte
PacketHeader of type Code, and copied into the transmit buffer.
The wire format
A single transfer carries one transfer header followed by a sequence of packets. Each packet
begins with an 8-byte PacketHeader (request type, id, length, resend id) followed by a
4-byte-aligned payload. The packet layer is identical on both transports; only the transfer header and
the integrity guarantees differ:
SPI (TransferHeader) |
USB (UsbTransferHeader) |
|
|---|---|---|
| Header size | 16 bytes | 8 bytes |
| Format code | yes | no |
| Sequence number | yes (detects resets) | no |
| CRC | CRC32 (v4+) / CRC16 | none - USB guarantees integrity and ordering |
| Readiness signalling | GPIO TfrRdy line |
serial flow, no GPIO |
| Direction | full-duplex (TX and RX swapped together) | serial read/write |
Both transports keep three transmit buffers so a firmware resend request can be honoured.
SPI transport
Link/Adapter/SPI.cs drives /dev/spidev0.0. PerformFullTransfer():
- finalises the transmit header (packet count, incremented sequence number, data length, CRC),
ExchangeHeader()- waits forTfrRdy, swaps the 16-byte headers full-duplex, validates the CRC and protocol version, and acknowledges,ExchangeData()- if either side has data, swaps the payloads full-duplex and validates the CRC,- rotates to the next transmit buffer and resets the pointers.
USB transport
Link/Adapter/USB.cs drives a SerialPort on /dev/ttyACM* (default /dev/ttyACM1). Because USB
already provides reliable, ordered delivery, the UsbTransferHeader drops the CRC, format code, and
sequence number, and there is no GPIO handshake - the adapter simply writes the header and data and
reads the firmware's response from the serial stream. The packet protocol above it is unchanged, so
the same LinkService request handlers and channel buffering apply.
Replies
When RRF returns a Message carrying the binary-code-reply flag, DCS routes it to the originating
channel (Manager.HandleReply -> Processor.HandleReply), pops the matching code off that channel's
BufferedCodes (FIFO), and attaches the text as the code's
Result. The code then finalises in the
Executed stage and the reply travels back to whichever
client submitted it.
Requests initiated by the firmware
RRF is not only a sink for codes - it drives work back into DCS. These arrive as packets and are
routed by LinkService.ProcessPacket(), independent of transport:
flowchart LR
RRF["RepRapFirmware"] -->|ExecuteMacro| MAC["DoMacroFile()<br/>push macro on channel stack"]
RRF -->|DoCode| DC["DoFirmwareCode()<br/>SimpleCode, IsFromFirmware"]
RRF -->|Message / code reply| RPL["HandleReply()<br/>complete buffered code"]
RRF -->|AbortFile| AB["FilesAborted()<br/>unwind macro stack"]
RRF -->|PrintPaused| PP["pause the job<br/>invalidate file codes"]
RRF -->|Locked| LK["resolve movement-lock request"]
RRF -->|MacroFileClosed| MFC["pop SBC macro stack"]
RRF -->|OpenFile/Read/Write/Seek/Close| FIO["filesystem I/O on the SBC"]
MAC --> PIPE["into the code pipeline"]
DC --> PIPE
- ExecuteMacro - RRF asks DCS to run a macro (homing files,
config.g, tool-change macros, ...). DCS resolves the virtual path, pushes aMacroFileonto the channel stack, and its codes flow through the normal pipeline. - DoCode - RRF asks DCS to execute a code string; a
SimpleCodeflaggedIsFromFirmwareis run. - AbortFile / PrintPaused / MacroFileClosed - keep DCS's per-channel macro stack and job state consistent with the firmware's.
- Locked - completes a pending movement-lock request so a code waiting on motion synchronisation can proceed.
- File I/O - RRF delegates file open/read/write/seek/close to the SBC, which performs the actual Linux filesystem access and streams data back.
The object model is also kept current over this link via
GetObjectModel requests and CodeBufferUpdate packets.
See also
- G-code flow - how a code reaches the Firmware stage in the first place
- File management - macros, jobs, and the virtual paths RRF requests
- Object model - the model updates carried over this link