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:
- Write the chip's final address into
NODECONFthrough address 0. - Drive its NAO low (
OUTPUTbit 0 = 0). The next chip's NAI goes low and it now answers at 0. - 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.