Skip to content

Multi-chip

How to drive several TMC5160s from one MCU: an SPI daisy chain on one chip select, or several nodes on one single-wire UART.

Each chip gets its own TMC51x0 object; all of them share one port object. The driver passes each chip's position (SPI) or node address (UART) with every register access.

SPI daisy chain

The chips share CSN, SCK and MOSI. Data shifts through them: the MCU's MOSI feeds chip 0's SDI, each chip's SDO feeds the next chip's SDI, and the last chip's SDO returns to the MCU's MISO.

flowchart TB
    MOSI[MCU MOSI] --> C0[Chip 0<br/>SDI to SDO]
    C0 --> C1[Chip 1<br/>SDI to SDO]
    C1 --> C2[Chip 2<br/>SDI to SDO]
    C2 --> MISO[MCU MISO]

Every chip has a 40-bit shift register. While CSN is low, bits clocked in on SDI come out on SDO 40 clocks later; when CSN rises, each chip takes the 40 bits it holds as its command. To reach chip k of n, the driver sends the command followed by padding, max(k + 1, n − k) × 5 bytes in one transfer, and takes chip k's reply from byte (n − k − 1) × 5.

spi.SetDaisyChainLength(3);                       // three chips share CSN, SCK and MOSI

tmc51x0::TMC51x0<MySpi> x_axis(spi, 0);           // position 0: SDI on the MCU's MOSI
tmc51x0::TMC51x0<MySpi> y_axis(spi, 1);
tmc51x0::TMC51x0<MySpi> z_axis(spi, 2);           // position 2: SDO on the MCU's MISO

for (auto* axis : {&x_axis, &y_axis, &z_axis}) {
  if (!axis->Initialize(cfg, false)) {
    std::printf("chip %u did not initialise\n", axis->communication.GetDaisyChainPosition());
  }
}
Rule Detail
Tell the interface the chain length SetDaisyChainLength(n) on the port before the first access. The interface also measures the chain on first use and logs a mismatch; it then uses the measured length
Position 0 is next to MOSI The chip whose SDI is on the MCU's MOSI
One transfer per access Your SpiTransfer() must keep CSN low for the whole buffer: 5 × n bytes or more
Chain limit TMC51X0_SPI_MAX_CHAIN_DEVICES, 8 by default; raise it at compile time for longer chains

TMC51x0DaisyChain (inc/features/tmc51x0_daisy_chain.hpp) does the same bookkeeping: it creates one driver per position, keeps the chain length in step as you add or remove chips at the end, and initialises them all with one configuration.

tmc51x0::TMC51x0DaisyChain<MySpi, 4> chain(spi, 3);   // positions 0..2; sets the chain length to 3
if (!chain.InitializeAll(cfg)) {
  std::printf("a chip in the chain did not initialise\n");
}
chain[2].rampControl.SetTargetPosition(90.0f, tmc51x0::Unit::Deg);

Chips on separate chip selects are simpler: give each its own port object (or one port that selects the CS line) and leave the chain length at 1.

UART nodes

In UART mode (SPI_MODE and SD_MODE low) the chips share the single-wire bus and each answers to its own 8-bit node address. A chip's address is NODECONF.NODEADDR, plus one while its NAI pin is high.

Wire NAI of the first chip to GND and each chip's NAO to the next chip's NAI. After power-up every NODEADDR is 0, so the first chip answers at 0 and all others at 1. Then, one chip at a time:

  1. Write the chip's final address into NODECONF through address 0.
  2. Drive its NAO low (OUTPUT bit 0 = 0). The next chip's NAI goes low and it now answers at 0.
  3. Repeat for the next chip.
// After power-up the first chip (NAI tied low) answers at address 0; the others answer at 1.
tmc51x0::TMC51x0<MyUart> first(uart);
first.uartConfig.ConfigureUartNodeAddress(254, 2);   // NODECONF via address 0; SENDDELAY 2 for a shared bus
first.io.SetSdoCfg0Polarity(false);                  // NAO low: the next chip now answers at 0

tmc51x0::TMC51x0<MyUart> second(uart);
second.uartConfig.ConfigureUartNodeAddress(253, 2);
second.io.SetSdoCfg0Polarity(false);

for (auto* node : {&first, &second}) {
  if (!node->Initialize(cfg, false)) {
    std::printf("node %u did not initialise\n", node->communication.GetUartNodeAddress());
  }
}
Rule Detail
Count addresses down Chips not yet programmed answer at 1, so give programmed chips high addresses, as the datasheet does (254, 253, ...)
SENDDELAY of 2 or more With several nodes a shorter reply delay makes the other nodes see a transmission error
Keep DriverConfig::uart_config at its defaults here A non-zero node_address or send_delay in it makes Initialize() write NODECONF again
Power cycles reset NODECONF Program the addresses after every power-up

For two chips, tying one NAI low and the other high gives addresses 0 and 1 without any programming.

TMC51x0MultiNode (inc/features/tmc51x0_multi_node.hpp) runs the procedure for you: ProgramSequentially() writes NODECONF through address 0 and drives NAO low, chip by chip, giving addresses 254, 253, 252, ... in wiring order. nodes[i] is the i-th chip from the one with NAI on GND.

tmc51x0::TMC51x0MultiNode<MyUart, 4> nodes(uart, 3);
if (nodes.ProgramSequentially(2) && nodes.InitializeAll(cfg)) {   // addresses 254, 253, 252
  nodes[1].rampControl.SetTargetPosition(90.0f, tmc51x0::Unit::Deg);
}

Next: Communication protocols.