HF-TMC51x0 Driver (TMC5130 & TMC5160) 0.1.0-dev
Hardware Agnostic C++ Driver for the TMC51x0 (TMC5130 & TMC5160)
Loading...
Searching...
No Matches
tmc51x0_comm_interface.hpp
Go to the documentation of this file.
1
110#pragma once
111#include <algorithm>
112#include <array>
113#include <cstdarg>
114#include <cstdint>
115#include <cstdio>
116
117#include "tmc51x0_result.hpp"
118
119namespace tmc51x0 {
120
134enum class LogLevel : uint8_t {
135 Error = 0,
136 Warn = 1,
137 Info = 2,
138 Debug = 3,
139 Verbose = 4
140};
141
149#ifndef TMC51X0_DISABLE_DEBUG_LOGGING
150// Optional compile-time log level filter:
151// 0=Error, 1=Warn, 2=Info, 3=Debug, 4=Verbose.
152// If not defined, everything up to Verbose is compiled in.
153#ifndef TMC51X0_LOG_LEVEL
154#define TMC51X0_LOG_LEVEL 4
155#endif
156
157// Level-specific macros provide best compile-time dead-stripping.
158#if TMC51X0_LOG_LEVEL >= 0
159#define TMC51X0_LOGE(comm_obj, tag, ...) (comm_obj).LogDebug(::tmc51x0::LogLevel::Error, tag, __VA_ARGS__)
160#else
161#define TMC51X0_LOGE(comm_obj, tag, ...) ((void)0)
162#endif
163
164#if TMC51X0_LOG_LEVEL >= 1
165#define TMC51X0_LOGW(comm_obj, tag, ...) (comm_obj).LogDebug(::tmc51x0::LogLevel::Warn, tag, __VA_ARGS__)
166#else
167#define TMC51X0_LOGW(comm_obj, tag, ...) ((void)0)
168#endif
169
170#if TMC51X0_LOG_LEVEL >= 2
171#define TMC51X0_LOGI(comm_obj, tag, ...) (comm_obj).LogDebug(::tmc51x0::LogLevel::Info, tag, __VA_ARGS__)
172#else
173#define TMC51X0_LOGI(comm_obj, tag, ...) ((void)0)
174#endif
175
176#if TMC51X0_LOG_LEVEL >= 3
177#define TMC51X0_LOGD(comm_obj, tag, ...) (comm_obj).LogDebug(::tmc51x0::LogLevel::Debug, tag, __VA_ARGS__)
178#else
179#define TMC51X0_LOGD(comm_obj, tag, ...) ((void)0)
180#endif
181
182#if TMC51X0_LOG_LEVEL >= 4
183#define TMC51X0_LOGV(comm_obj, tag, ...) (comm_obj).LogDebug(::tmc51x0::LogLevel::Verbose, tag, __VA_ARGS__)
184#else
185#define TMC51X0_LOGV(comm_obj, tag, ...) ((void)0)
186#endif
187
188// Backwards-compatible macro: routes to the level-specific macros when possible.
189// If `level` is a constant `LogLevel` (recommended), the compiler will fold the branch.
190// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) - Intentional: compile-time logging control
191#define TMC51X0_LOG_DEBUG(comm_obj, level, tag, ...) \
192 do { \
193 const int _lvl = static_cast<int>(level); \
194 if (_lvl <= 0) { \
195 TMC51X0_LOGE(comm_obj, tag, __VA_ARGS__); \
196 } else if (_lvl == 1) { \
197 TMC51X0_LOGW(comm_obj, tag, __VA_ARGS__); \
198 } else if (_lvl == 2) { \
199 TMC51X0_LOGI(comm_obj, tag, __VA_ARGS__); \
200 } else if (_lvl == 3) { \
201 TMC51X0_LOGD(comm_obj, tag, __VA_ARGS__); \
202 } else { \
203 TMC51X0_LOGV(comm_obj, tag, __VA_ARGS__); \
204 } \
205 } while (0)
206#else
207// Debug logging disabled - optimize out completely (arguments not evaluated)
208// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) - Intentional: compile-time
209// logging control
210#define TMC51X0_LOGE(comm_obj, tag, ...) ((void)0)
211#define TMC51X0_LOGW(comm_obj, tag, ...) ((void)0)
212#define TMC51X0_LOGI(comm_obj, tag, ...) ((void)0)
213#define TMC51X0_LOGD(comm_obj, tag, ...) ((void)0)
214#define TMC51X0_LOGV(comm_obj, tag, ...) ((void)0)
215#define TMC51X0_LOG_DEBUG(comm_obj, level, tag, ...) ((void)0)
216#endif
217
221enum class CommMode : uint8_t {
222 SPI,
224 UART
226};
227
258enum class TMC51x0CtrlPin : uint8_t {
259 // Basic control pins (always available)
260 EN,
261 DIR,
263 STEP,
265
266 // Reference switch pins (when SD_MODE=0, internal ramp generator mode)
267 // Same physical pins as STEP/DIR but used as reference switches
268 REFL_STEP,
270 REFR_DIR,
272
273 // Diagnostic output pins / UART pins (mode-dependent)
274 DIAG0,
278 DIAG1,
282
283 // Encoder input pins (when SD_MODE=0, internal ramp generator mode)
284 // Same physical pins as DC Step pins but used as encoder inputs
285 ENCA,
287 ENCB,
289 ENCN,
291
292 // DC Step control pins (when SD_MODE=1, SPI_MODE=1, external step/dir mode)
293 // Same physical pins as encoder pins but used for DC Step control
294 DCEN,
296 DCIN,
298 DCO,
300
301 // Clock pin (optional external clock)
302 CLK,
304
305 // Mode configuration pins (if made available as control pins)
306 // WARNING: These pins are typically hardwired and read at startup.
307 // Only use these if you have connected these pins to GPIO outputs for dynamic
308 // control.
309 // Changing these pins requires a chip reset to take effect.
310 SPI_MODE,
312 SD_MODE
314};
315
319enum class GpioSignal : uint8_t {
320 INACTIVE = 0,
321 ACTIVE = 1
322};
323
353 // Basic control pins (per TMC51x0 datasheet)
354 bool en{false};
355 bool dir{true};
356 bool step{true};
357
358 // Reference switch pins (when SD_MODE=0)
360 false};
362 false};
363
364 // Diagnostic pins (read-only outputs from TMC51x0 - not typically configured)
365 bool diag0{true};
367 bool diag1{true};
369
370 // Encoder pins (when SD_MODE=0, read-only inputs)
371 bool enca{true};
373 bool encb{true};
375 bool encn{true};
377
378 // DC Step pins (when SD_MODE=1, SPI_MODE=1)
379 bool dcen{true};
380 bool dcin{true};
381 bool dco{true};
383
384 // Clock pin
385 bool clk{
386 true};
387
388 // Mode configuration pins (if available as control pins)
389 bool spi_mode{true};
391 true};
392
398 [[nodiscard]] bool GetActiveLevel(TMC51x0CtrlPin pin) const noexcept {
399 switch (pin) {
401 return en;
403 return dir;
405 return step;
407 return ref_left;
409 return ref_right;
411 return diag0;
413 return diag1;
415 return enca;
417 return encb;
419 return encn;
421 return dcen;
423 return dcin;
425 return dco;
427 return clk;
429 return spi_mode;
431 return sd_mode;
432 default:
433 return true; // Default to HIGH
434 }
435 }
436
442 void SetActiveLevel(TMC51x0CtrlPin pin, bool active_level) noexcept {
443 switch (pin) {
445 en = active_level;
446 break;
448 dir = active_level;
449 break;
451 step = active_level;
452 break;
454 ref_left = active_level;
455 break;
457 ref_right = active_level;
458 break;
460 diag0 = active_level;
461 break;
463 diag1 = active_level;
464 break;
466 enca = active_level;
467 break;
469 encb = active_level;
470 break;
472 encn = active_level;
473 break;
475 dcen = active_level;
476 break;
478 dcin = active_level;
479 break;
481 dco = active_level;
482 break;
484 clk = active_level;
485 break;
487 spi_mode = active_level;
488 break;
490 sd_mode = active_level;
491 break;
492 default:
493 break;
494 }
495 }
496};
497
522 // Basic control pins
523 int en_pin{-1};
525 -1};
527 -1};
528
529 // Reference switch pins (when SD_MODE=0)
531 -1};
533 -1};
534
535 // Diagnostic pins (read-only outputs from TMC51x0)
536 int diag0_pin{-1};
537 int diag1_pin{-1};
538
539 // Encoder pins (when SD_MODE=0)
540 int enc_a_pin{-1};
541 int enc_b_pin{-1};
542 int enc_n_pin{-1};
543
544 // DC Step pins (when SD_MODE=1, SPI_MODE=1)
545 int dc_in_pin{-1};
547 int dc_en_pin{-1};
550 -1};
551
552 // Clock pin
553 int clk_pin{-1};
554
555 // Mode configuration pins (if made available as control pins)
556 // WARNING: These pins are typically hardwired and read at startup.
557 // Only configure these if you have connected SPI_MODE (pin 22) and SD_MODE
558 // (pin 21) to GPIO outputs for dynamic mode control. Changing these requires
559 // a chip reset.
560 int spi_mode_pin{-1};
562 int sd_mode_pin{-1};
564
568 TMC51x0PinConfig() = default;
569
576 TMC51x0PinConfig(int en, int dir = -1, int step = -1) noexcept
577 : en_pin(en), dir_pin(dir), step_pin(step) {}
578};
579
587struct SpiStatus {
588 uint8_t value;
589
595 static SpiStatus FromByte(uint8_t status_byte) noexcept {
596 SpiStatus status{};
597 status.value = status_byte;
598 return status;
599 }
600
609 [[nodiscard]] bool HasError() const noexcept {
610 return DriverError(); // Only bit 1 (driver_error) is an error; bit 0
611 // (reset) is informational
612 }
613
618 [[nodiscard]] bool ResetFlag() const noexcept { return (value & 0x01) != 0; }
619
624 [[nodiscard]] bool DriverError() const noexcept {
625 return (value & 0x02) != 0;
626 }
627
632 [[nodiscard]] bool StallGuard2() const noexcept {
633 return (value & 0x04) != 0;
634 }
635
640 [[nodiscard]] bool Standstill() const noexcept { return (value & 0x08) != 0; }
641
646 [[nodiscard]] bool VelocityReached() const noexcept {
647 return (value & 0x10) != 0;
648 }
649
654 [[nodiscard]] bool PositionReached() const noexcept {
655 return (value & 0x20) != 0;
656 }
657
662 [[nodiscard]] bool StopLeft() const noexcept { return (value & 0x40) != 0; }
663
668 [[nodiscard]] bool StopRight() const noexcept { return (value & 0x80) != 0; }
669
680 void FormatStatusBits(char *out, size_t cap) const noexcept {
681 if (out == nullptr || cap == 0) return;
682 std::snprintf(
683 out, cap,
684 "RST:%d STST:%d VEL:%d POS:%d STOP_L:%d STOP_R:%d SG2:%d DRV_ERR:%d",
685 ResetFlag() ? 1 : 0, Standstill() ? 1 : 0, VelocityReached() ? 1 : 0,
686 PositionReached() ? 1 : 0, StopLeft() ? 1 : 0, StopRight() ? 1 : 0,
687 StallGuard2() ? 1 : 0, DriverError() ? 1 : 0);
688 }
689
696 [[nodiscard]] const char *ToString() const noexcept {
697 // This is a simplified version - in practice, you'd want a buffer
698 // For now, return a static description of key flags
699 if (HasError()) {
700 return "DRV_ERR"; // Only driver_error is an error
701 }
702 if (ResetFlag()) {
703 return "RST"; // Reset is informational
704 }
705 return "OK";
706 }
707};
708
724 union Frame {
725 uint8_t bytes[5];
726 struct {
727 uint8_t
729 uint8_t data_bytes[4];
731 uint64_t raw;
734
739 [[nodiscard]] uint8_t GetAddress() const noexcept {
740 return frame.fields.address_byte & 0x7F;
741 }
742
747 [[nodiscard]] bool IsWrite() const noexcept {
748 return (frame.fields.address_byte & 0x80) != 0;
749 }
750
755 [[nodiscard]] uint32_t GetValue() const noexcept {
756 return (static_cast<uint32_t>(frame.fields.data_bytes[0]) << 24) |
757 (static_cast<uint32_t>(frame.fields.data_bytes[1]) << 16) |
758 (static_cast<uint32_t>(frame.fields.data_bytes[2]) << 8) |
759 static_cast<uint32_t>(frame.fields.data_bytes[3]);
760 }
761
766 void SetFrame(const uint8_t *bytes) noexcept {
767 for (size_t i = 0; i < 5; ++i) {
768 frame.bytes[i] = bytes[i];
769 }
770 }
771
776 void GetFrame(uint8_t *bytes) const noexcept {
777 for (size_t i = 0; i < 5; ++i) {
778 bytes[i] = frame.bytes[i];
779 }
780 }
781
786 static SpiCommand Read(uint8_t addr) noexcept {
787 SpiCommand cmd{};
788 cmd.frame.raw = 0; // Initialize to zero
789 cmd.frame.fields.address_byte = addr & 0x7F; // Clear write bit (bit 7 = 0)
790 // Data bytes are already zero (dummy data for read)
791 return cmd;
792 }
793
799 static SpiCommand Write(uint8_t addr, uint32_t val) noexcept {
800 SpiCommand cmd{};
802 (addr & 0x7F) | 0x80; // Set write bit (bit 7 = 1)
803 cmd.frame.fields.data_bytes[0] =
804 static_cast<uint8_t>((val >> 24) & 0xFF); // MSB
805 cmd.frame.fields.data_bytes[1] = static_cast<uint8_t>((val >> 16) & 0xFF);
806 cmd.frame.fields.data_bytes[2] = static_cast<uint8_t>((val >> 8) & 0xFF);
807 cmd.frame.fields.data_bytes[3] = static_cast<uint8_t>(val & 0xFF); // LSB
808 return cmd;
809 }
810};
811
821 uint32_t value;
823 bool success;
824};
825
840static constexpr uint8_t calculateCrc8(const uint8_t *data,
841 size_t length) noexcept {
842 uint8_t crc = 0; // Initial value is zero per datasheet
843
844 // Process each byte LSB to MSB
845 for (size_t i = 0; i < length; ++i) {
846 uint8_t current_byte = data[i];
847
848 // Process each bit LSB to MSB (j=0 is LSB, j=7 is MSB)
849 for (uint8_t j = 0; j < 8; ++j) {
850 // Check: (CRC >> 7) XOR (current_byte & 0x01)
851 // This XORs the MSB of CRC with the LSB of current_byte
852 if (((crc >> 7) ^ (current_byte & 0x01)) != 0) {
853 crc = (crc << 1) ^ 0x07; // Polynomial 0x07 (CRC8-ATM)
854 } else {
855 crc = (crc << 1);
856 }
857 current_byte = current_byte >> 1; // Shift to next bit (LSB to MSB)
858 }
859 }
860
861 return crc;
862}
863
867enum class UartFrameType : uint8_t {
870 ReadReply
871};
872
890struct UartFrame {
894 union Frame {
895 uint8_t bytes[8];
896
897 // Write Access Structure (8 bytes)
898 struct {
900 uint8_t node_addr;
901 uint8_t rw_address;
902 uint8_t data_bytes[4];
903 uint8_t crc;
905
906 // Read Request Structure (4 bytes)
907 struct {
908 uint8_t sync_reserved;
909 uint8_t node_addr;
910 uint8_t rw_address;
911 uint8_t crc;
913
914 // Read Reply Structure (8 bytes)
915 struct {
916 uint8_t sync_reserved;
917 uint8_t master_addr;
918 uint8_t reg_addr;
919 uint8_t data_bytes[4];
920 uint8_t crc;
922
924
926
931 [[nodiscard]] size_t GetSize() const noexcept {
932 return (type == UartFrameType::ReadRequest) ? 4 : 8;
933 }
934
939 [[nodiscard]] uint8_t GetAddress() const noexcept {
942 }
943 // For ReadRequest and WriteAccess, address is in the same byte (Byte 2)
944 // Need to mask off RW bit (bit 7)
945 return frame.write_fields.rw_address & 0x7F;
946 }
947
952 [[nodiscard]] bool IsWrite() const noexcept {
954 }
955
960 [[nodiscard]] uint32_t GetValue() const noexcept {
962 return 0;
963 }
964 // Both WriteAccess and ReadReply have data at offset 3
965 return (static_cast<uint32_t>(frame.write_fields.data_bytes[0]) << 24) |
966 (static_cast<uint32_t>(frame.write_fields.data_bytes[1]) << 16) |
967 (static_cast<uint32_t>(frame.write_fields.data_bytes[2]) << 8) |
968 static_cast<uint32_t>(frame.write_fields.data_bytes[3]);
969 }
970
975 void CalculateCrc() noexcept {
976 size_t frame_size = GetSize();
977 size_t crc_length = frame_size - 1; // All bytes except CRC byte
978
979 // Calculate CRC over bytes 0 to (frame_size-2)
980 uint8_t calculated_crc = calculateCrc8(frame.bytes, crc_length);
981
982 // Set CRC in the last byte
983 frame.bytes[frame_size - 1] = calculated_crc;
984 }
985
990 [[nodiscard]] bool VerifyCrc() const noexcept {
991 size_t frame_size = GetSize();
992 size_t crc_length = frame_size - 1; // All bytes except CRC byte
993
994 uint8_t calculated_crc = calculateCrc8(frame.bytes, crc_length);
995
996 // Compare with received CRC (last byte)
997 return calculated_crc == frame.bytes[frame_size - 1];
998 }
999
1005 void SetFrame(const uint8_t *bytes, UartFrameType frame_type) noexcept {
1006 type = frame_type;
1007 size_t frame_size = GetSize();
1008 for (size_t i = 0; i < frame_size; ++i) {
1009 frame.bytes[i] = bytes[i];
1010 }
1011 // Zero out unused bytes for safety (if any)
1012 if (frame_size < 8) {
1013 for (size_t i = frame_size; i < 8; ++i) {
1014 frame.bytes[i] = 0;
1015 }
1016 }
1017 }
1018
1023 void GetFrame(uint8_t *bytes) const noexcept {
1024 size_t frame_size = GetSize();
1025 for (size_t i = 0; i < frame_size; ++i) {
1026 bytes[i] = frame.bytes[i];
1027 }
1028 }
1029
1037 static UartFrame Write(uint8_t node_addr, uint8_t reg_addr,
1038 uint32_t value) noexcept {
1039 UartFrame uart_frame{};
1040 uart_frame.type = UartFrameType::WriteAccess;
1041
1042 // Byte 0: Sync nibble (0x05)
1043 uart_frame.frame.write_fields.sync_reserved = 0x05;
1044
1045 // Byte 1: Node Address
1046 uart_frame.frame.write_fields.node_addr = node_addr;
1047
1048 // Byte 2: RW bit (1) + Register Address
1049 uart_frame.frame.write_fields.rw_address = (reg_addr & 0x7F) | 0x80;
1050
1051 // Bytes 3-6: Data (MSB-first)
1052 uart_frame.frame.write_fields.data_bytes[0] =
1053 static_cast<uint8_t>((value >> 24) & 0xFF);
1054 uart_frame.frame.write_fields.data_bytes[1] =
1055 static_cast<uint8_t>((value >> 16) & 0xFF);
1056 uart_frame.frame.write_fields.data_bytes[2] =
1057 static_cast<uint8_t>((value >> 8) & 0xFF);
1058 uart_frame.frame.write_fields.data_bytes[3] =
1059 static_cast<uint8_t>(value & 0xFF);
1060
1061 // Byte 7: CRC (calculated)
1062 uart_frame.CalculateCrc();
1063
1064 return uart_frame;
1065 }
1066
1073 static UartFrame ReadRequest(uint8_t node_addr, uint8_t reg_addr) noexcept {
1074 UartFrame uart_frame{};
1075 uart_frame.type = UartFrameType::ReadRequest;
1076
1077 // Byte 0: Sync nibble (0x05)
1078 uart_frame.frame.read_request_fields.sync_reserved = 0x05;
1079
1080 // Byte 1: Node Address
1081 uart_frame.frame.read_request_fields.node_addr = node_addr;
1082
1083 // Byte 2: RW bit (0) + Register Address
1084 uart_frame.frame.read_request_fields.rw_address = reg_addr & 0x7F;
1085
1086 // Byte 3: CRC (calculated)
1087 uart_frame.CalculateCrc();
1088
1089 return uart_frame;
1090 }
1091
1097 static UartFrame ReadReply(const uint8_t *bytes) noexcept {
1098 UartFrame uart_frame{};
1099 uart_frame.type = UartFrameType::ReadReply;
1100 uart_frame.SetFrame(bytes, UartFrameType::ReadReply);
1101 return uart_frame;
1102 }
1103
1108 [[nodiscard]] bool IsValid() const noexcept {
1109 if (!VerifyCrc()) {
1110 return false;
1111 }
1113 // Verify Master Address (Byte 1) is 0xFF
1114 return frame.read_reply_fields.master_addr == 0xFF;
1115 }
1116 return true;
1117 }
1118};
1119
1144template <typename Derived> class CommInterface {
1145public:
1152 CommInterface() noexcept = default;
1153
1158 [[nodiscard]] CommMode GetMode() const noexcept {
1159 return static_cast<const Derived *>(this)->GetMode();
1160 }
1161
1175 uint8_t address_param = 0) noexcept {
1176 uint32_t value = 0;
1177 return static_cast<Derived *>(this)->ReadRegister(address,
1178 address_param);
1179 }
1180
1194 Result<void> WriteRegister(uint8_t address, uint32_t value,
1195 uint8_t address_param = 0) noexcept {
1196 return static_cast<Derived *>(this)->WriteRegister(address, value,
1197 address_param);
1198 }
1199
1207 return static_cast<Derived *>(this)->GpioSet(pin, signal);
1208 }
1209
1216 return static_cast<Derived *>(this)->GpioRead(pin);
1217 }
1218
1225 return GpioSet(pin, GpioSignal::ACTIVE);
1226 }
1227
1236
1237protected:
1245 void DebugLog(int level, const char *tag, const char *format,
1246 va_list args) noexcept {
1247 static_cast<Derived *>(this)->DebugLog(level, tag, format, args);
1248 }
1249
1250public:
1255 void DelayMs(uint32_t ms) noexcept {
1256 static_cast<Derived *>(this)->DelayMs(ms);
1257 }
1258
1263 void DelayUs(uint32_t us) noexcept {
1264 static_cast<Derived *>(this)->DelayUs(us);
1265 }
1266
1282 Result<void> SetPowerEnabled(bool enabled) noexcept {
1283 (void)enabled;
1285 }
1286
1299 Result<void> PowerCycle(uint32_t power_off_ms = 20,
1300 uint32_t power_on_settle_ms = 20) noexcept {
1301 auto r_off = SetPowerEnabled(false);
1302 if (!r_off) {
1303 return r_off;
1304 }
1305 DelayMs(power_off_ms);
1306
1307 auto r_on = SetPowerEnabled(true);
1308 if (!r_on) {
1309 return r_on;
1310 }
1311 DelayMs(power_on_settle_ms);
1312 return Result<void>();
1313 }
1314
1376 Result<void> SetClkFreq(uint32_t frequency_hz) noexcept {
1377 // Default implementation returns UNSUPPORTED (not supported / using
1378 // internal clock) Derived classes can override this if they support
1379 // external clock generation When frequency_hz = 0, this means "use internal
1380 // clock" (set CLK pin to GND)
1381 (void)frequency_hz; // Suppress unused parameter warning
1383 }
1384
1385protected:
1389 ~CommInterface() = default;
1390
1391 // Allow moving
1394
1395public:
1396 // Prevent copying
1397 CommInterface(const CommInterface &) = delete;
1406#ifndef TMC51X0_DISABLE_DEBUG_LOGGING
1407 void LogDebug(int level, const char *tag, const char *format, ...) noexcept {
1408 va_list args{}; // va_start will properly initialize this
1409 va_start(args, format);
1410 // Do not allocate (no std::string) just to enforce a newline.
1411 // Platform log backends (ESP-IDF, etc.) typically add their own newline.
1412 DebugLog(level, tag, format, args);
1413 va_end(args);
1414 }
1415
1419 void LogDebug(LogLevel level, const char *tag, const char *format, ...) noexcept {
1420 va_list args{}; // va_start will properly initialize this
1421 va_start(args, format);
1422 DebugLog(static_cast<int>(level), tag, format, args);
1423 va_end(args);
1424 }
1425#else
1426 // Debug logging disabled - function optimized out completely
1427 inline void LogDebug(int /*level*/, const char * /*tag*/,
1428 const char * /*format*/, ...) noexcept {
1429 // Empty function body - all logging optimized out
1430 }
1431
1432 // Debug logging disabled - overload also optimized out completely
1433 inline void LogDebug(LogLevel /*level*/, const char * /*tag*/,
1434 const char * /*format*/, ...) noexcept {
1435 // Empty function body - all logging optimized out
1436 }
1437#endif
1438};
1439
1659template <typename Derived>
1660class SpiCommInterface : public CommInterface<Derived> {
1661public:
1662 //--------------------------------------------------------------------------------
1663 // Configuration
1664 //--------------------------------------------------------------------------------
1665 // Maximum supported daisy-chain size for static SPI scratch buffers.
1666 // Default keeps RAM small while supporting common chains.
1667 // Override at compile time if you have longer chains:
1668 // #define TMC51X0_SPI_MAX_CHAIN_DEVICES 255
1669 // Note: scratch buffers are sized for (max_devices+2)*5 bytes to support
1670 // AutoDetectChainLength() and the "beyond last device" probe.
1671#ifndef TMC51X0_SPI_MAX_CHAIN_DEVICES
1672#define TMC51X0_SPI_MAX_CHAIN_DEVICES 8
1673#endif
1674
1675 static_assert(TMC51X0_SPI_MAX_CHAIN_DEVICES >= 1,
1676 "TMC51X0_SPI_MAX_CHAIN_DEVICES must be >= 1");
1677 static_assert(TMC51X0_SPI_MAX_CHAIN_DEVICES <= 255,
1678 "TMC51X0_SPI_MAX_CHAIN_DEVICES must be <= 255");
1679
1680 static constexpr size_t kSpiScratchBytes =
1681 static_cast<size_t>(TMC51X0_SPI_MAX_CHAIN_DEVICES + 2U) * 5U;
1682
1689 SpiCommInterface() noexcept : CommInterface<Derived>() {}
1690
1708 void SetDaisyChainLength(uint8_t total_length) noexcept {
1709 total_chain_length_ = total_length;
1710 user_specified_chain_length_ = total_length; // Track user-specified value
1711 chain_length_verified_ = false; // Reset verification flag
1712 }
1713
1719 [[nodiscard]] uint8_t GetDaisyChainLength() const noexcept {
1720 return total_chain_length_;
1721 }
1722
1757 uint8_t AutoDetectChainLength(uint8_t max_devices = 8) noexcept {
1758 if (max_devices == 0) {
1759 max_devices = 8; // Default to 8 devices
1760 }
1761 if (max_devices > TMC51X0_SPI_MAX_CHAIN_DEVICES) {
1762 max_devices = TMC51X0_SPI_MAX_CHAIN_DEVICES;
1763 }
1764
1765 // Create a unique command pattern that we can reliably identify when it
1766 // loops back Use a read command to a register that's safe to read (GSTAT =
1767 // 0x00) But we'll use a unique address pattern to make it more distinctive
1768 // Actually, let's use a write command with a unique value that we can
1769 // recognize But writes modify state... better to use read with a unique
1770 // address
1771
1772 // Best approach: Use a read command with a distinctive address
1773 // We'll use address 0x73 (last register) which is safe to read
1774 // The command pattern will be: [0x73] [0x00] [0x00] [0x00] [0x00]
1775 // This is distinctive enough to identify
1776
1777 // Actually, even better: Use a read to an address that's unlikely to appear
1778 // in device responses Let's use 0x73 (last register address) - this is
1779 // distinctive
1780 SpiCommand cmd = SpiCommand::Read(0x73); // Read last register (0x73)
1781
1782 // Get the command frame bytes so we can search for it
1783 uint8_t cmd_bytes[5];
1784 cmd.GetFrame(cmd_bytes);
1785
1786 // Send command to position (max_devices+1) - beyond the last device
1787 // Total transfer: (max_devices+2)*40 bits to ensure we capture loopback
1788 size_t transfer_bytes = static_cast<size_t>(max_devices + 2) * 5;
1789 if (transfer_bytes > kSpiScratchBytes) {
1790 // Should not happen due to clamping, but keep it safe.
1791 return 0;
1792 }
1793
1794 // No heap: use static scratch buffers sized by TMC51X0_SPI_MAX_CHAIN_DEVICES.
1795 std::fill(tx_scratch_.begin(), tx_scratch_.end(), 0);
1796 std::fill(rx_scratch_.begin(), rx_scratch_.end(), 0);
1797 uint8_t *tx_buf = tx_scratch_.data();
1798 uint8_t *rx_buf = rx_scratch_.data();
1799
1800 // Place command at the beginning (bytes 0-4)
1801 cmd.GetFrame(tx_buf);
1802 // Rest is padding (zeros) - already initialized to 0
1803
1805 *static_cast<Derived *>(this), 2, "SPI",
1806 "AutoDetectChainLength: Probing up to %u devices, transfer_bytes=%zu, "
1807 "cmd_pattern=%02X %02X %02X %02X %02X",
1808 max_devices, transfer_bytes, cmd_bytes[0], cmd_bytes[1], cmd_bytes[2],
1809 cmd_bytes[3], cmd_bytes[4]);
1810
1811 // Perform SPI transfer
1812 if (!SpiTransfer(tx_buf, rx_buf, transfer_bytes)) {
1813 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 1, "SPI",
1814 "AutoDetectChainLength: SPI transfer failed");
1815 return 0;
1816 }
1817
1818 // Search for our exact command pattern in the received data
1819 // The command loops back after n*40 bits, appearing at offset n*5 bytes
1820 // Since each device delays data by 40 clocks, our command should appear
1821 // unmodified at the loopback point
1822
1823 uint8_t detected_length = 0;
1824
1825 // Search backwards from max_devices to find where our command appears
1826 // For n devices, our command appears at offset n*5 after looping back
1827 // We require an EXACT match of all 5 bytes to confirm loopback
1828 for (uint8_t n = max_devices; n >= 1; --n) {
1829 size_t loopback_offset = static_cast<size_t>(n) * 5;
1830
1831 if (loopback_offset + 4 < transfer_bytes) {
1832 // Check if the 5-byte chunk at loopback_offset matches our command
1833 // pattern EXACTLY This is the only reliable way to confirm the command
1834 // looped back correctly
1835 bool exact_match = true;
1836
1837 for (uint8_t i = 0; i < 5; ++i) {
1838 if (rx_buf[loopback_offset + i] != cmd_bytes[i]) {
1839 exact_match = false;
1840 break;
1841 }
1842 }
1843
1844 if (exact_match) {
1845 // Found our exact command pattern! This confirms it looped back after
1846 // n devices
1847 detected_length = n;
1848 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 2, "SPI",
1849 "AutoDetectChainLength: Found EXACT command "
1850 "pattern match at offset "
1851 "%zu (expected for n=%u), chain length = %u",
1852 loopback_offset, n, detected_length);
1853 break;
1854 }
1855 }
1856 }
1857
1858 // If exact match failed, log debug info to help diagnose
1859 if (detected_length == 0) {
1860 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 1, "SPI",
1861 "AutoDetectChainLength: Exact command pattern not "
1862 "found in received data");
1864 *static_cast<Derived *>(this), 3, "SPI",
1865 "AutoDetectChainLength: Expected pattern: %02X %02X %02X %02X %02X",
1866 cmd_bytes[0], cmd_bytes[1], cmd_bytes[2], cmd_bytes[3], cmd_bytes[4]);
1867
1868 // Log first few potential loopback positions for debugging
1869 for (uint8_t n = 1; n <= 3 && n <= max_devices; ++n) {
1870 size_t loopback_offset = static_cast<size_t>(n) * 5;
1871 if (loopback_offset + 4 < transfer_bytes) {
1873 *static_cast<Derived *>(this), 3, "SPI",
1874 "AutoDetectChainLength: At offset %zu (n=%u): %02X %02X %02X "
1875 "%02X %02X",
1876 loopback_offset, n, rx_buf[loopback_offset],
1877 rx_buf[loopback_offset + 1], rx_buf[loopback_offset + 2],
1878 rx_buf[loopback_offset + 3], rx_buf[loopback_offset + 4]);
1879 }
1880 }
1881 }
1882
1883 // If we detected a length, set it (but don't overwrite user-specified value
1884 // yet) The caller will handle verification and update
1885 if (detected_length > 0) {
1886 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 2, "SPI",
1887 "AutoDetectChainLength: Detected chain length = %u",
1888 detected_length);
1889 // Only update if not user-specified, or if user-specified value matches
1891 user_specified_chain_length_ == detected_length) {
1892 total_chain_length_ = detected_length;
1893 }
1894 } else {
1895 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 1, "SPI",
1896 "AutoDetectChainLength: Command pattern not found, "
1897 "assuming single chip");
1898 // Don't reset total_chain_length_ if it was already set (e.g., to 1 for
1899 // single-chip mode) Only reset if it was 0 and user hasn't specified a
1900 // length
1902 // Keep it at 0 - caller will handle single-chip case
1903 }
1904 }
1905
1906 return detected_length;
1907 }
1908
1913 [[nodiscard]] CommMode GetMode() const noexcept { return CommMode::SPI; }
1914
1922 Result<void> SpiTransfer(const uint8_t *tx, uint8_t *rx,
1923 size_t length) noexcept {
1924 return static_cast<Derived *>(this)->SpiTransfer(tx, rx, length);
1925 }
1926
1992 uint8_t daisy_chain_position = 0) noexcept {
1993 uint32_t value = 0;
1994 // Log function call with arguments (level 3 = DEBUG, only shows at DEBUG
1995 // log level)
1996 if (daisy_chain_position > 0) {
1997 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 3, "SPI",
1998 "ReadRegister(0x%02X, daisy_chain=%u)", address,
1999 daisy_chain_position);
2000 } else {
2001 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 3, "SPI",
2002 "ReadRegister(0x%02X)", address);
2003 }
2004
2005 // CRITICAL: Chain length MUST be known for correct response extraction
2006 // Ensure chain length is known and verified (auto-detects if needed)
2007 if (!EnsureChainLengthKnown(daisy_chain_position, "ReadRegister")) {
2009 }
2010
2011 // Build command using SpiCommand structure (union-based frame)
2012 SpiCommand cmd = SpiCommand::Read(address);
2013
2014 // CRITICAL: Chain length MUST be known at this point
2015 // If daisy_chain_position > 0, total_chain_length_ must be > 0 (detected or
2016 // set) If daisy_chain_position == 0, total_chain_length_ should be 1
2017 // (single chip)
2018 if (daisy_chain_position > 0 && total_chain_length_ == 0) {
2020 *static_cast<Derived *>(this), 1, "SPI",
2021 "ReadRegister: Chain length unknown for daisy_chain_position=%u. "
2022 "Cannot proceed without chain length.",
2023 daisy_chain_position);
2025 }
2026
2027 // Calculate transfer size and response offset using datasheet formula
2028 // Transfer size: max((k+1)*5, (n-k)*5) bytes
2029 // - Sending: (k+1)*5 bytes needed to address device k
2030 // - Receiving: (n-k)*5 bytes needed to shift response back (datasheet
2031 // formula)
2032 // - Use max() to ensure both requirements are met
2033 // Response offset: (n-k-1)*5 bytes (reverse order: last device first)
2034 uint8_t n = total_chain_length_;
2035 uint8_t k = daisy_chain_position;
2036 if (n == 0) {
2037 // Single-chip mode should effectively be 1 device.
2038 n = 1;
2039 }
2042 *static_cast<Derived *>(this), 1, "SPI",
2043 "ReadRegister: Chain length %u exceeds TMC51X0_SPI_MAX_CHAIN_DEVICES=%u",
2044 n, static_cast<unsigned>(TMC51X0_SPI_MAX_CHAIN_DEVICES));
2046 }
2047
2048 // Validate: k must be < n (device position must be less than total chain
2049 // length)
2050 if (k >= n) {
2052 *static_cast<Derived *>(this), 1, "SPI",
2053 "ReadRegister: Invalid daisy_chain_position=%u for chain length=%u. "
2054 "Position must be < chain length.",
2055 k, n);
2057 }
2058
2059 // Calculate transfer size: max of sending and receiving requirements
2060 size_t sending_bytes =
2061 static_cast<size_t>(k + 1) * 5; // Command must reach device k
2062 size_t receiving_bytes = static_cast<size_t>(n - k) *
2063 5; // Response extraction (datasheet formula)
2064 size_t transfer_bytes = std::max(sending_bytes, receiving_bytes);
2065
2066 // Response offset: (n-k-1)*5 bytes (based on reverse order of devices)
2067 size_t response_byte_offset = static_cast<size_t>(n - k - 1) * 5;
2068
2069 // Validate transfer size meets receiving requirement
2070 if (transfer_bytes < receiving_bytes) {
2071 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 1, "SPI",
2072 "ReadRegister: Transfer size %zu bytes < receiving "
2073 "requirement %zu bytes. "
2074 "Response extraction may fail.",
2075 transfer_bytes, receiving_bytes);
2077 }
2078
2079 // Validate response offset is within buffer bounds
2080 if (response_byte_offset + 4 >= transfer_bytes) {
2082 *static_cast<Derived *>(this), 1, "SPI",
2083 "ReadRegister: Response offset %zu + 4 >= transfer size %zu. "
2084 "Cannot read full 5-byte response.",
2085 response_byte_offset, transfer_bytes);
2087 }
2088
2089 if (transfer_bytes > kSpiScratchBytes) {
2090 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 1, "SPI",
2091 "ReadRegister: transfer_bytes=%zu exceeds scratch cap=%zu",
2092 transfer_bytes, kSpiScratchBytes);
2094 }
2095
2096 std::fill(tx_scratch_.begin(), tx_scratch_.end(), 0);
2097 std::fill(rx_scratch_.begin(), rx_scratch_.end(), 0);
2098 uint8_t *tx_buf = tx_scratch_.data();
2099 uint8_t *rx_buf = rx_scratch_.data();
2100
2101 // Place command at the beginning (bytes 0-4)
2102 // For daisy-chain position k, the command is placed first, then padding
2103 // (zeros) Padding structure:
2104 // - Bytes 5 to (k+1)*5-1: Padding to shift command to device k
2105 // - Bytes (k+1)*5 to transfer_bytes-1: Additional padding for full-duplex
2106 // response extraction
2107 // (only present if transfer_bytes > (k+1)*5, i.e., when (n-k)*5 >
2108 // (k+1)*5)
2109 cmd.GetFrame(tx_buf);
2110 // Bytes 5 onwards are padding (zeros) - already initialized to 0 by vector
2111 // constructor
2112
2113 // First transaction: Send read command (TX1), receive status (RX1)
2114 if (!SpiTransfer(tx_buf, rx_buf, transfer_bytes)) {
2116 }
2117
2118 // Log [TX1]/RX1 after first transfer
2119 // Show "=0x00000000" for reads to align with Write format (read command has
2120 // no data, all zeros)
2121 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 3, "SPI",
2122 "Read 0x%02X=0x00000000: [TX1] %02X %02X %02X %02X %02X "
2123 "/ RX1 %02X %02X %02X %02X %02X",
2124 address, tx_buf[0], tx_buf[1], tx_buf[2], tx_buf[3],
2125 tx_buf[4], rx_buf[response_byte_offset],
2126 (response_byte_offset + 1 < transfer_bytes)
2127 ? rx_buf[response_byte_offset + 1]
2128 : 0,
2129 (response_byte_offset + 2 < transfer_bytes)
2130 ? rx_buf[response_byte_offset + 2]
2131 : 0,
2132 (response_byte_offset + 3 < transfer_bytes)
2133 ? rx_buf[response_byte_offset + 3]
2134 : 0,
2135 (response_byte_offset + 4 < transfer_bytes)
2136 ? rx_buf[response_byte_offset + 4]
2137 : 0);
2138
2139 // Minimum CSN high time: 2*tclk + 10ns (typically ~176ns with 12MHz clock)
2140 // Per datasheet: CSN must go high between pipelined read transfers
2141 // Use 10us delay to ensure TMC5160 has time to prepare pipelined data
2142 // Some registers (like GLOBAL_SCALER, X_COMPARE) may require longer delay
2143 // Note: ESP32 spi_device_transmit automatically handles CSN (pulls high
2144 // after transfer)
2145 this->DelayUs(10);
2146
2147 // Second transaction: Send address again (TX2), receive actual data (RX2 -
2148 // pipelined read) Per datasheet: Read data is transferred back with the
2149 // subsequent access Use same transfer size for daisy-chaining consistency
2150 if (!SpiTransfer(tx_buf, rx_buf, transfer_bytes)) {
2152 }
2153
2154 // Log TX2/[RX2] after second transfer (RX2 contains the actual read data)
2155 // Align TX2 line with TX1 line by padding address field
2156 SpiStatus status = SpiStatus::FromByte(rx_buf[response_byte_offset]);
2157 char status_bits[128];
2158 status.FormatStatusBits(status_bits, sizeof(status_bits));
2159
2160 // Align TX2 bytes with TX1 bytes: "Read 0xXX=0x00000000: " (25) + "[TX1] "
2161 // (6) = 31 chars to first byte For TX2: "Read 0xXX=0x00000000: " (25) + "
2162 // " (6 spaces) + "TX2 " (4) = 35, but bytes should be at 31 Actually: align
2163 // "TX2" label with "[TX1]" label, then bytes naturally align "Read
2164 // 0xXX=0x00000000: [TX1] " = 31, bytes at 31 "Read 0xXX=0x00000000: TX2 " =
2165 // 31 (25 + 6), bytes at 31 ✓
2166 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 3, "SPI",
2167 "Read 0x%02X: TX2 %02X %02X %02X %02X %02X / "
2168 "[RX2] %02X %02X %02X %02X %02X (STATUS=0x%02X)",
2169 address, tx_buf[0], tx_buf[1], tx_buf[2], tx_buf[3],
2170 tx_buf[4], rx_buf[response_byte_offset],
2171 (response_byte_offset + 1 < transfer_bytes)
2172 ? rx_buf[response_byte_offset + 1]
2173 : 0,
2174 (response_byte_offset + 2 < transfer_bytes)
2175 ? rx_buf[response_byte_offset + 2]
2176 : 0,
2177 (response_byte_offset + 3 < transfer_bytes)
2178 ? rx_buf[response_byte_offset + 3]
2179 : 0,
2180 (response_byte_offset + 4 < transfer_bytes)
2181 ? rx_buf[response_byte_offset + 4]
2182 : 0,
2183 rx_buf[response_byte_offset]);
2184
2185 // Log status bit breakdown with arrow pointing to STATUS byte
2186 // Calculate position: "Read 0xXX: TX2 XX XX XX XX XX / [RX2] " =
2187 // ~60 chars, then STATUS byte at ~68
2189 *static_cast<Derived *>(this), 3, "SPI",
2190 " └─> %s",
2191 status_bits);
2192
2193 // Extract response data based on daisy-chain position
2194 // IMPORTANT: Responses come back in REVERSE order (last device first, first
2195 // device last) Response offset is calculated above based on whether
2196 // total_chain_length_ is known
2197 // - If total_chain_length_ > 0: Use datasheet formula, response at
2198 // (n-k-1)*5 bytes
2199 // - If total_chain_length_ == 0: Use simplified approach, response at k*5
2200 // bytes (end of transfer)
2201
2202 // Extract SPI_STATUS from response byte 0 (bits 39-32 per datasheet
2203 // section 4.1.2) For daisy-chaining, this is at the calculated offset
2204 // (status was already extracted above for logging)
2205 if (response_byte_offset >= transfer_bytes) {
2207 *static_cast<Derived *>(this), 1, "SPI",
2208 "Read register 0x%02X: Response offset %zu exceeds buffer size %zu",
2209 address, response_byte_offset, transfer_bytes);
2211 }
2212
2213 // Log SPI_STATUS with detailed flag information.
2214 // Note: RESET (bit 0) is informational (normal on power-up). DRV_ERR (bit
2215 // 1) indicates a driver-side fault (e.g., OT/short) but does NOT imply a
2216 // transport failure, so we do not return COMM_ERROR for it.
2217 if (status.HasError()) {
2218 // Rate-limit DRV_ERR logs to avoid spamming at high read rates.
2219 const bool should_log = (drv_err_log_count_ < 5U) ||
2220 ((drv_err_log_count_ % 50U) == 0U);
2222
2223 if (should_log) {
2224 // Build informational flags string (reset + status flags)
2225 char info_flags[64] = "";
2226 if (status.ResetFlag() || status.StallGuard2() || status.Standstill() ||
2227 status.VelocityReached() || status.PositionReached() ||
2228 status.StopLeft() || status.StopRight()) {
2229 snprintf(info_flags, sizeof(info_flags), " [%s%s%s%s%s%s%s]",
2230 status.ResetFlag() ? "RST " : "",
2231 status.StallGuard2() ? "SG2 " : "",
2232 status.Standstill() ? "STST " : "",
2233 status.VelocityReached() ? "VEL " : "",
2234 status.PositionReached() ? "POS " : "",
2235 status.StopLeft() ? "STOP_L " : "",
2236 status.StopRight() ? "STOP_R " : "");
2237 }
2238
2239 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 1, "SPI",
2240 "Read 0x%02X: STATUS=0x%02X DRV_ERR%s", address,
2241 status.value, info_flags);
2242 }
2243 } else {
2244 // Log response bytes (first 8 or all if less)
2245 size_t log_rx_bytes = (transfer_bytes < 8) ? transfer_bytes : 8;
2246 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 3, "SPI",
2247 "Read register 0x%02X: RX[0..%zu] %02X %02X %02X %02X "
2248 "%02X %02X %02X %02X "
2249 "| SPI_STATUS=0x%02X [%s%s%s%s%s%s%s%s]",
2250 address, log_rx_bytes - 1, rx_buf[0],
2251 (log_rx_bytes > 1) ? rx_buf[1] : 0,
2252 (log_rx_bytes > 2) ? rx_buf[2] : 0,
2253 (log_rx_bytes > 3) ? rx_buf[3] : 0,
2254 (log_rx_bytes > 4) ? rx_buf[4] : 0,
2255 (log_rx_bytes > 5) ? rx_buf[5] : 0,
2256 (log_rx_bytes > 6) ? rx_buf[6] : 0,
2257 (log_rx_bytes > 7) ? rx_buf[7] : 0, status.value,
2258 status.ResetFlag() ? "RST " : "",
2259 status.DriverError() ? "DRV_ERR " : "",
2260 status.StallGuard2() ? "SG2 " : "",
2261 status.Standstill() ? "STST " : "",
2262 status.VelocityReached() ? "VEL " : "",
2263 status.PositionReached() ? "POS " : "",
2264 status.StopLeft() ? "STOP_L " : "",
2265 status.StopRight() ? "STOP_R " : "");
2266 }
2267
2268 // Extract 32-bit value from bytes (response_byte_offset+1) to
2269 // (response_byte_offset+4) Byte (response_byte_offset+0) contains
2270 // SPI_STATUS (bits 39-32) Bytes (response_byte_offset+1) to
2271 // (response_byte_offset+4) contain data (bits 31-0)
2272 if (response_byte_offset + 4 >= transfer_bytes) {
2274 *static_cast<Derived *>(this), 1, "SPI",
2275 "Read register 0x%02X: Data offset %zu+4 exceeds buffer size %zu",
2276 address, response_byte_offset, transfer_bytes);
2278 }
2279
2280 // Extract 32-bit value from RX2 (bytes response_byte_offset+1 to
2281 // response_byte_offset+4)
2282 value = (static_cast<uint32_t>(rx_buf[response_byte_offset + 1]) << 24) |
2283 (static_cast<uint32_t>(rx_buf[response_byte_offset + 2]) << 16) |
2284 (static_cast<uint32_t>(rx_buf[response_byte_offset + 3]) << 8) |
2285 static_cast<uint32_t>(rx_buf[response_byte_offset + 4]);
2286
2287 // Do not treat DRV_ERR as a transport failure. Caller may query GSTAT /
2288 // DRV_STATUS to diagnose the underlying cause.
2289 return Result<uint32_t>(value);
2290 }
2291
2332 Result<void> WriteRegister(uint8_t address, uint32_t value,
2333 uint8_t daisy_chain_position = 0) noexcept {
2334 // Log function call with arguments (level 3 = DEBUG, only shows at DEBUG
2335 // log level)
2336 if (daisy_chain_position > 0) {
2337 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 3, "SPI",
2338 "WriteRegister(0x%02X=0x%08X, daisy_chain=%u)", address,
2339 value, daisy_chain_position);
2340 } else {
2341 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 3, "SPI",
2342 "WriteRegister(0x%02X=0x%08X)", address, value);
2343 }
2344
2345 // CRITICAL: Chain length MUST be known for correct response extraction
2346 // Ensure chain length is known and verified (auto-detects if needed)
2347 if (!EnsureChainLengthKnown(daisy_chain_position, "WriteRegister")) {
2349 }
2350
2351 // Build command using SpiCommand structure (union-based frame)
2352 SpiCommand cmd = SpiCommand::Write(address, value);
2353
2354 // CRITICAL: Chain length MUST be known at this point
2355 // If daisy_chain_position > 0, total_chain_length_ must be > 0 (detected or
2356 // set) If daisy_chain_position == 0, total_chain_length_ should be 1
2357 // (single chip)
2358 if (daisy_chain_position > 0 && total_chain_length_ == 0) {
2360 *static_cast<Derived *>(this), 1, "SPI",
2361 "WriteRegister: Chain length unknown for daisy_chain_position=%u. "
2362 "Cannot proceed without chain length.",
2363 daisy_chain_position);
2365 }
2366
2367 // Calculate transfer size and response offset using datasheet formula
2368 // Transfer size: max((k+1)*5, (n-k)*5) bytes
2369 // - Sending: (k+1)*5 bytes needed to address device k
2370 // - Receiving: (n-k)*5 bytes needed to shift response back (datasheet
2371 // formula)
2372 // - Use max() to ensure both requirements are met
2373 // - Extra bytes beyond (k+1)*5 are padding (zeros) for full-duplex
2374 // behavior
2375 // Response offset: (n-k-1)*5 bytes (reverse order: last device first)
2376 uint8_t n = total_chain_length_;
2377 uint8_t k = daisy_chain_position;
2378 if (n == 0) {
2379 n = 1;
2380 }
2383 *static_cast<Derived *>(this), 1, "SPI",
2384 "WriteRegister: Chain length %u exceeds TMC51X0_SPI_MAX_CHAIN_DEVICES=%u",
2385 n, static_cast<unsigned>(TMC51X0_SPI_MAX_CHAIN_DEVICES));
2387 }
2388
2389 // Validate: k must be < n (device position must be less than total chain
2390 // length)
2391 if (k >= n) {
2393 *static_cast<Derived *>(this), 1, "SPI",
2394 "WriteRegister: Invalid daisy_chain_position=%u for chain length=%u. "
2395 "Position must be < chain length.",
2396 k, n);
2398 }
2399
2400 // Calculate transfer size: max of sending and receiving requirements
2401 size_t sending_bytes =
2402 static_cast<size_t>(k + 1) * 5; // Command must reach device k
2403 size_t receiving_bytes = static_cast<size_t>(n - k) *
2404 5; // Response extraction (datasheet formula)
2405 size_t transfer_bytes = std::max(sending_bytes, receiving_bytes);
2406
2407 // Response offset: (n-k-1)*5 bytes (based on reverse order of devices)
2408 size_t response_byte_offset = static_cast<size_t>(n - k - 1) * 5;
2409
2410 // Validate transfer size meets receiving requirement
2411 if (transfer_bytes < receiving_bytes) {
2412 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 1, "SPI",
2413 "WriteRegister: Transfer size %zu bytes < receiving "
2414 "requirement %zu bytes. "
2415 "Response extraction may fail.",
2416 transfer_bytes, receiving_bytes);
2418 }
2419
2420 // Validate response offset is within buffer bounds
2421 if (response_byte_offset + 4 >= transfer_bytes) {
2423 *static_cast<Derived *>(this), 1, "SPI",
2424 "WriteRegister: Response offset %zu + 4 >= transfer size %zu. "
2425 "Cannot read full 5-byte response.",
2426 response_byte_offset, transfer_bytes);
2428 }
2429
2430 if (transfer_bytes > kSpiScratchBytes) {
2431 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 1, "SPI",
2432 "WriteRegister: transfer_bytes=%zu exceeds scratch cap=%zu",
2433 transfer_bytes, kSpiScratchBytes);
2435 }
2436
2437 std::fill(tx_scratch_.begin(), tx_scratch_.end(), 0);
2438 std::fill(rx_scratch_.begin(), rx_scratch_.end(), 0);
2439 uint8_t *tx_buf = tx_scratch_.data();
2440 uint8_t *rx_buf = rx_scratch_.data();
2441
2442 // Place command at the beginning (bytes 0-4)
2443 // For daisy-chain position k, the command is placed first, then padding
2444 // (zeros) Padding structure:
2445 // - Bytes 5 to (k+1)*5-1: Padding to shift command to device k
2446 // - Bytes (k+1)*5 to transfer_bytes-1: Additional padding for full-duplex
2447 // response extraction
2448 // (only present if transfer_bytes > (k+1)*5, i.e., when (n-k)*5 >
2449 // (k+1)*5)
2450 cmd.GetFrame(tx_buf);
2451 // Bytes 5 onwards are padding (zeros) - already initialized to 0 by vector
2452 // constructor
2453
2454 // First transaction: Send write command (TX1), receive status (RX1)
2455 if (!SpiTransfer(tx_buf, rx_buf, transfer_bytes)) {
2457 }
2458
2459 // Log [TX1]/RX1 after first transfer
2460 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 3, "SPI",
2461 "Write 0x%02X=0x%08X: [TX1] %02X %02X %02X %02X %02X / "
2462 "RX1 %02X %02X %02X %02X %02X",
2463 address, value, tx_buf[0], tx_buf[1], tx_buf[2],
2464 tx_buf[3], tx_buf[4], rx_buf[response_byte_offset],
2465 (response_byte_offset + 1 < transfer_bytes)
2466 ? rx_buf[response_byte_offset + 1]
2467 : 0,
2468 (response_byte_offset + 2 < transfer_bytes)
2469 ? rx_buf[response_byte_offset + 2]
2470 : 0,
2471 (response_byte_offset + 3 < transfer_bytes)
2472 ? rx_buf[response_byte_offset + 3]
2473 : 0,
2474 (response_byte_offset + 4 < transfer_bytes)
2475 ? rx_buf[response_byte_offset + 4]
2476 : 0);
2477
2478 // Extract response data based on daisy-chain position
2479 // IMPORTANT: Responses come back in REVERSE order (last device first, first
2480 // device last) Response offset is calculated above based on whether
2481 // total_chain_length_ is known
2482 // - If total_chain_length_ > 0: Use datasheet formula, response at
2483 // (n-k-1)*5 bytes
2484 // - If total_chain_length_ == 0: Use simplified approach, response at k*5
2485 // bytes (end of transfer)
2486 if (response_byte_offset >= transfer_bytes) {
2488 *static_cast<Derived *>(this), 1, "SPI",
2489 "Write register 0x%02X: Response offset %zu exceeds buffer size %zu",
2490 address, response_byte_offset, transfer_bytes);
2492 }
2493
2494 // Extract SPI_STATUS from first transaction response
2495 // Per datasheet: First write response contains SPI_STATUS + dummy/previous
2496 // data
2497 SpiStatus status1 = SpiStatus::FromByte(rx_buf[response_byte_offset]);
2498
2499 // Note: RESET is informational. DRV_ERR is a driver-side fault; we log it
2500 // (rate-limited) but do not treat it as a transport error.
2501 if (status1.HasError()) {
2502 const bool should_log = (drv_err_log_count_ < 5U) ||
2503 ((drv_err_log_count_ % 50U) == 0U);
2505
2506 if (should_log) {
2507 // Build informational flags string (reset + status flags)
2508 char info_flags[64] = "";
2509 if (status1.ResetFlag() || status1.StallGuard2() ||
2510 status1.Standstill() || status1.VelocityReached() ||
2511 status1.PositionReached() || status1.StopLeft() ||
2512 status1.StopRight()) {
2513 snprintf(info_flags, sizeof(info_flags), " [%s%s%s%s%s%s%s]",
2514 status1.ResetFlag() ? "RST " : "",
2515 status1.StallGuard2() ? "SG2 " : "",
2516 status1.Standstill() ? "STST " : "",
2517 status1.VelocityReached() ? "VEL " : "",
2518 status1.PositionReached() ? "POS " : "",
2519 status1.StopLeft() ? "STOP_L " : "",
2520 status1.StopRight() ? "STOP_R " : "");
2521 }
2522
2523 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 1, "SPI",
2524 "Write 0x%02X (TX1): STATUS=0x%02X DRV_ERR%s",
2525 address, status1.value, info_flags);
2526 }
2527 } else {
2528 // Log response bytes (first 8 or all if less)
2529 size_t log_rx1_bytes = (transfer_bytes < 8) ? transfer_bytes : 8;
2530 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 3, "SPI",
2531 "Write register 0x%02X (TX1): RX[0..%zu] %02X %02X "
2532 "%02X %02X %02X %02X %02X %02X | "
2533 "SPI_STATUS=0x%02X [%s%s%s%s%s%s%s%s]",
2534 address, log_rx1_bytes - 1, rx_buf[0],
2535 (log_rx1_bytes > 1) ? rx_buf[1] : 0,
2536 (log_rx1_bytes > 2) ? rx_buf[2] : 0,
2537 (log_rx1_bytes > 3) ? rx_buf[3] : 0,
2538 (log_rx1_bytes > 4) ? rx_buf[4] : 0,
2539 (log_rx1_bytes > 5) ? rx_buf[5] : 0,
2540 (log_rx1_bytes > 6) ? rx_buf[6] : 0,
2541 (log_rx1_bytes > 7) ? rx_buf[7] : 0, status1.value,
2542 status1.ResetFlag() ? "RST " : "",
2543 status1.DriverError() ? "DRV_ERR " : "",
2544 status1.StallGuard2() ? "SG2 " : "",
2545 status1.Standstill() ? "STST " : "",
2546 status1.VelocityReached() ? "VEL " : "",
2547 status1.PositionReached() ? "POS " : "",
2548 status1.StopLeft() ? "STOP_L " : "",
2549 status1.StopRight() ? "STOP_R " : "");
2550 }
2551
2552 // Minimum CSN high time: 2*tclk + 10ns (typically ~176ns with 12MHz clock)
2553 // Use 1us delay for safety
2554 this->DelayUs(1);
2555
2556 // Second transaction: Send dummy read to receive write confirmation/status
2557 // The response from the second transaction contains the status/confirmation
2558 // for the write command sent in the first transaction
2559 // Clear tx_buf and place read command at the beginning (same position as
2560 // write command)
2561 std::fill(tx_scratch_.begin(), tx_scratch_.end(), 0);
2562 SpiCommand read_cmd = SpiCommand::Read(address);
2563 read_cmd.GetFrame(tx_buf);
2564 // Padding (zeros) after byte 4 will shift this command to the target device
2565
2566 if (!SpiTransfer(tx_buf, rx_buf, transfer_bytes)) {
2568 }
2569
2570 // Log TX2/[RX2] after second transfer (RX2 contains the write confirmation)
2571 // Align TX2 line with TX1 line by padding address field
2572 SpiStatus status2 = SpiStatus::FromByte(rx_buf[response_byte_offset]);
2573 char status2_bits[128];
2574 status2.FormatStatusBits(status2_bits, sizeof(status2_bits));
2575
2576 // Align TX2 bytes with TX1 bytes: "Write 0xXX=0xXXXXXXXX: " (25) + "[TX1] "
2577 // (6) = 31 chars to first byte For TX2: "Write 0xXX: " (13) + padding to
2578 // reach 31 = 18 spaces needed But we want "TX2 " to align with "[TX1] ",
2579 // so: "Write 0xXX: " (13) + 6 spaces + "TX2 " (4) = 23 To align bytes:
2580 // "Write 0xXX: " (13) + 18 spaces = 31, then bytes start Actually simpler:
2581 // align "TX2" label with "[TX1]" label, then bytes naturally align "Write
2582 // 0xXX=0xXXXXXXXX: [TX1] " = 31, bytes at 31 "Write 0xXX: TX2 "
2583 // = 31 (13 + 18), bytes at 31 ✓
2584 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 3, "SPI",
2585 "Write 0x%02X: TX2 %02X %02X %02X %02X %02X "
2586 "/ [RX2] %02X %02X %02X %02X %02X (STATUS=0x%02X)",
2587 address, tx_buf[0], tx_buf[1], tx_buf[2], tx_buf[3],
2588 tx_buf[4], rx_buf[response_byte_offset],
2589 (response_byte_offset + 1 < transfer_bytes)
2590 ? rx_buf[response_byte_offset + 1]
2591 : 0,
2592 (response_byte_offset + 2 < transfer_bytes)
2593 ? rx_buf[response_byte_offset + 2]
2594 : 0,
2595 (response_byte_offset + 3 < transfer_bytes)
2596 ? rx_buf[response_byte_offset + 3]
2597 : 0,
2598 (response_byte_offset + 4 < transfer_bytes)
2599 ? rx_buf[response_byte_offset + 4]
2600 : 0,
2601 rx_buf[response_byte_offset]);
2602
2603 // Log status bit breakdown with arrow pointing to STATUS byte
2605 *static_cast<Derived *>(this), 3, "SPI",
2606 " └─> %s",
2607 status2_bits);
2608
2609 // Validate response offset (status2 was already extracted above for
2610 // logging)
2611 if (response_byte_offset >= transfer_bytes) {
2612 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 1, "SPI",
2613 "Write register 0x%02X (TX2): Response offset %zu "
2614 "exceeds buffer size %zu",
2615 address, response_byte_offset, transfer_bytes);
2617 }
2618
2619 // Note: RESET is informational. DRV_ERR is a driver-side fault; we log it
2620 // (rate-limited) but do not treat it as a transport error.
2621 if (status2.HasError()) {
2622 const bool should_log = (drv_err_log_count_ < 5U) ||
2623 ((drv_err_log_count_ % 50U) == 0U);
2625
2626 if (should_log) {
2627 // Build informational flags string (reset + status flags)
2628 char info_flags[64] = "";
2629 if (status2.ResetFlag() || status2.StallGuard2() ||
2630 status2.Standstill() || status2.VelocityReached() ||
2631 status2.PositionReached() || status2.StopLeft() ||
2632 status2.StopRight()) {
2633 snprintf(info_flags, sizeof(info_flags), " [%s%s%s%s%s%s%s]",
2634 status2.ResetFlag() ? "RST " : "",
2635 status2.StallGuard2() ? "SG2 " : "",
2636 status2.Standstill() ? "STST " : "",
2637 status2.VelocityReached() ? "VEL " : "",
2638 status2.PositionReached() ? "POS " : "",
2639 status2.StopLeft() ? "STOP_L " : "",
2640 status2.StopRight() ? "STOP_R " : "");
2641 }
2642
2643 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 1, "SPI",
2644 "Write 0x%02X (TX2): STATUS=0x%02X DRV_ERR%s",
2645 address, status2.value, info_flags);
2646 }
2647 } else {
2648 // Log response bytes (first 8 or all if less)
2649 size_t log_rx2_bytes = (transfer_bytes < 8) ? transfer_bytes : 8;
2650 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 3, "SPI",
2651 "Write register 0x%02X (TX2): RX[0..%zu] %02X %02X "
2652 "%02X %02X %02X %02X %02X %02X | "
2653 "SPI_STATUS=0x%02X [%s%s%s%s%s%s%s%s]",
2654 address, log_rx2_bytes - 1, rx_buf[0],
2655 (log_rx2_bytes > 1) ? rx_buf[1] : 0,
2656 (log_rx2_bytes > 2) ? rx_buf[2] : 0,
2657 (log_rx2_bytes > 3) ? rx_buf[3] : 0,
2658 (log_rx2_bytes > 4) ? rx_buf[4] : 0,
2659 (log_rx2_bytes > 5) ? rx_buf[5] : 0,
2660 (log_rx2_bytes > 6) ? rx_buf[6] : 0,
2661 (log_rx2_bytes > 7) ? rx_buf[7] : 0, status2.value,
2662 status2.ResetFlag() ? "RST " : "",
2663 status2.DriverError() ? "DRV_ERR " : "",
2664 status2.StallGuard2() ? "SG2 " : "",
2665 status2.Standstill() ? "STST " : "",
2666 status2.VelocityReached() ? "VEL " : "",
2667 status2.PositionReached() ? "POS " : "",
2668 status2.StopLeft() ? "STOP_L " : "",
2669 status2.StopRight() ? "STOP_R " : "");
2670 }
2671
2672 // Per datasheet: Write access returns SPI_STATUS (byte 0) + previously
2673 // written data (bytes 1-4) "If the previous access was a write access, then
2674 // the data read back mirrors the previously received write data." Verify
2675 // that the returned data matches what we wrote
2676 if (response_byte_offset + 4 < transfer_bytes) {
2677 uint32_t returned_value =
2678 (static_cast<uint32_t>(rx_buf[response_byte_offset + 1]) << 24) |
2679 (static_cast<uint32_t>(rx_buf[response_byte_offset + 2]) << 16) |
2680 (static_cast<uint32_t>(rx_buf[response_byte_offset + 3]) << 8) |
2681 static_cast<uint32_t>(rx_buf[response_byte_offset + 4]);
2682
2683 if (returned_value != value) {
2684 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 1, "SPI",
2685 "WriteRegister(0x%02X): Write verification failed - "
2686 "wrote 0x%08X, got back 0x%08X",
2687 address, value, returned_value);
2688 // Don't fail the write operation, but log the mismatch for debugging
2689 } else {
2690 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 3, "SPI",
2691 "WriteRegister(0x%02X): Write verification passed - "
2692 "wrote 0x%08X, got back 0x%08X",
2693 address, value, returned_value);
2694 }
2695 }
2696
2697 return Result<void>();
2698 }
2699
2700protected:
2711 0};
2716
2721
2722 // Allow moving
2725
2726public:
2727 // Prevent copying
2730
2731private:
2732 // Static scratch buffers used for SPI transfers to avoid heap allocation.
2733 // Sized by TMC51X0_SPI_MAX_CHAIN_DEVICES (+2 for auto-detect probe).
2734 std::array<uint8_t, kSpiScratchBytes> tx_scratch_{};
2735 std::array<uint8_t, kSpiScratchBytes> rx_scratch_{};
2736
2737 // Counts how many times we've seen SPI_STATUS.DRV_ERR so we can rate-limit
2738 // logs without relying on platform-specific time functions.
2740
2760 Result<void> EnsureChainLengthKnown(uint8_t daisy_chain_position,
2761 const char *context) noexcept {
2762 // Auto-detect chain length on first access if needed
2763 if (daisy_chain_position > 0 && total_chain_length_ == 0) {
2764 uint8_t detected_length =
2765 AutoDetectChainLength(TMC51X0_SPI_MAX_CHAIN_DEVICES); // Probe up to configured max
2766 if (detected_length == 0) {
2767 // Detection failed - cannot proceed without chain length
2769 *static_cast<Derived *>(this), 1, "SPI",
2770 "%s: Auto-detection failed, but daisy_chain_position=%u > 0. "
2771 "Chain length is required for correct response extraction. "
2772 "Operation failed.",
2773 context, daisy_chain_position);
2775 }
2776 }
2777
2778 // For single chip (daisy_chain_position == 0), chain length is not needed
2779 // But if it's set, ensure it's at least 1
2780 if (daisy_chain_position == 0 && total_chain_length_ == 0) {
2781 // Single chip mode - this is valid
2782 total_chain_length_ = 1; // Set to 1 for single chip (n=1, k=0)
2784 true; // Single-chip mode doesn't need verification
2785 }
2786
2787 // Verify chain length if user specified one
2789 // User specified a chain length, verify it matches detected length
2790 uint8_t detected_length = AutoDetectChainLength(8);
2791 if (detected_length > 0 &&
2792 detected_length != user_specified_chain_length_) {
2793 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 1, "SPI",
2794 "%s: DAISY CHAIN LENGTH MISMATCH! "
2795 "User specified: %u, Auto-detected: %u. "
2796 "Response extraction will be incorrect. "
2797 "Call SetDaisyChainLength(%u) to fix.",
2799 detected_length, detected_length);
2800 // Update to detected length and mark as verified (use detected value)
2801 total_chain_length_ = detected_length;
2803 // Continue with detected length (don't fail, but log error)
2804 } else if (detected_length > 0) {
2805 // Match confirmed
2807 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 2, "SPI",
2808 "%s: Chain length verified: %u devices", context,
2809 detected_length);
2810 }
2811 } else if (!chain_length_verified_ && total_chain_length_ > 0) {
2812 // Chain length was set but not verified yet, verify it now
2813 // Skip verification for single-chip mode (daisy_chain_position == 0,
2814 // total_chain_length_ == 1) Auto-detection can fail for single-chip
2815 // setups, so we trust the set value
2816 if (daisy_chain_position == 0 && total_chain_length_ == 1) {
2817 // Single-chip mode - no verification needed
2819 } else {
2820 // Multi-chip mode - verify chain length
2821 uint8_t detected_length = AutoDetectChainLength(8);
2822 if (detected_length > 0 && detected_length != total_chain_length_) {
2823 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 1, "SPI",
2824 "%s: DAISY CHAIN LENGTH MISMATCH! "
2825 "Specified: %u, Auto-detected: %u. "
2826 "Updating to detected length.",
2827 context, total_chain_length_, detected_length);
2828 total_chain_length_ = detected_length;
2829 }
2831 }
2832 }
2833
2834 return Result<void>();
2835 }
2836};
2837
2934template <typename Derived>
2935class UartCommInterface : public CommInterface<Derived> {
2936public:
2951 UartCommInterface() noexcept : CommInterface<Derived>() {}
2952
2957 [[nodiscard]] CommMode GetMode() const noexcept { return CommMode::UART; }
2958
2976 Result<void> SetNaiPin(bool active) noexcept {
2977 return static_cast<Derived *>(this)->SetNaiPin(active);
2978 }
2979
2999 bool active = false;
3000 return static_cast<Derived *>(this)->GetNaoPin(active);
3001 }
3002
3009 Result<void> UartSend(const uint8_t *data, size_t length) noexcept {
3010 return static_cast<Derived *>(this)->UartSend(data, length);
3011 }
3012
3019 Result<void> UartReceive(uint8_t *data, size_t length) noexcept {
3020 return static_cast<Derived *>(this)->UartReceive(data, length);
3021 }
3022
3031 uint8_t node_address = 0) noexcept {
3032 uint32_t value = 0;
3033 // Build read request frame (4 bytes)
3034 uint8_t node_addr = node_address & 0x7F;
3035 UartFrame read_request = UartFrame::ReadRequest(node_addr, address);
3036
3037 // Get frame bytes (4 bytes)
3038 std::array<uint8_t, 8> tx_buf{}; // Max size for any frame is 8
3039 read_request.GetFrame(tx_buf.data());
3040 size_t tx_size = read_request.GetSize(); // 4 bytes
3041
3043 *static_cast<Derived *>(this), 3, "UART",
3044 "Read register 0x%02X (NodeAddr=0x%02X): TX %02X %02X %02X %02X",
3045 address, node_addr, tx_buf[0], tx_buf[1], tx_buf[2], tx_buf[3]);
3046
3047 if (!UartSend(tx_buf.data(), tx_size)) {
3049 }
3050
3051 // Receive read reply (8 bytes)
3052 std::array<uint8_t, 8> rx_buf{};
3053 if (!UartReceive(rx_buf.data(), 8)) {
3055 }
3056
3057 // Parse read reply using UartFrame structure
3058 UartFrame read_reply = UartFrame::ReadReply(rx_buf.data());
3059
3061 *static_cast<Derived *>(this), 3, "UART",
3062 "Read register 0x%02X: RX %02X %02X %02X %02X %02X %02X %02X %02X",
3063 address, rx_buf[0], rx_buf[1], rx_buf[2], rx_buf[3], rx_buf[4],
3064 rx_buf[5], rx_buf[6], rx_buf[7]);
3065
3066 // Verify CRC8 and frame validity
3067 if (!read_reply.VerifyCrc()) {
3068 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 1, "UART",
3069 "Read register 0x%02X: CRC8 verification failed",
3070 address);
3072 }
3073
3074 if (!read_reply.IsValid()) {
3075 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 1, "UART",
3076 "Read register 0x%02X: Invalid frame structure",
3077 address);
3079 }
3080
3081 // Extract 32-bit value
3082 value = read_reply.GetValue();
3083
3084 return Result<uint32_t>(value);
3085 }
3086
3094 Result<void> WriteRegister(uint8_t address, uint32_t value,
3095 uint8_t node_address = 0) noexcept {
3096 // Build write access frame (8 bytes)
3097 uint8_t node_addr = node_address & 0x7F;
3098 UartFrame write_frame = UartFrame::Write(node_addr, address, value);
3099
3100 // Get frame bytes (8 bytes)
3101 std::array<uint8_t, 8> tx_buf{};
3102 write_frame.GetFrame(tx_buf.data());
3103 size_t tx_size = write_frame.GetSize(); // 8 bytes
3104
3105 TMC51X0_LOG_DEBUG(*static_cast<Derived *>(this), 3, "UART",
3106 "Write register 0x%02X = 0x%08X (NodeAddr=0x%02X): TX "
3107 "%02X %02X %02X %02X "
3108 "%02X %02X %02X %02X",
3109 address, node_addr, tx_buf[0], tx_buf[1], tx_buf[2],
3110 tx_buf[3], tx_buf[4], tx_buf[5], tx_buf[6], tx_buf[7]);
3111
3112 if (!UartSend(tx_buf.data(), tx_size)) {
3114 }
3115
3116 // Write does NOT have a reply packet from the device (only updates internal
3117 // counter). We return true if send was successful. Note: Some single-wire
3118 // implementations receive their own TX (echo). If so, the derived class or
3119 // HAL should handle flushing the echo. This interface assumes UartSend
3120 // handles the transmission.
3121
3122 return Result<void>();
3123 }
3124
3125protected:
3130
3131 // Allow moving
3134
3135public:
3136 // Prevent copying
3139};
3140
3141} // namespace tmc51x0
CRTP-based communication interface for register read/write operations.
Definition tmc51x0_comm_interface.hpp:1144
Result< GpioSignal > GpioRead(TMC51x0CtrlPin pin) noexcept
Read GPIO pin signal state (input state)
Definition tmc51x0_comm_interface.hpp:1215
void DebugLog(int level, const char *tag, const char *format, va_list args) noexcept
Debug logging function for detailed debugging information.
Definition tmc51x0_comm_interface.hpp:1245
~CommInterface()=default
Protected destructor.
CommInterface(const CommInterface &)=delete
Result< uint32_t > ReadRegister(uint8_t address, uint8_t address_param=0) noexcept
Read a 32-bit register from the TMC5160.
Definition tmc51x0_comm_interface.hpp:1174
void LogDebug(LogLevel level, const char *tag, const char *format,...) noexcept
LogDebug overload that accepts the driver-native LogLevel enum.
Definition tmc51x0_comm_interface.hpp:1419
Result< void > SetPowerEnabled(bool enabled) noexcept
Enable/disable power to the TMC51x0 (optional)
Definition tmc51x0_comm_interface.hpp:1282
void LogDebug(int level, const char *tag, const char *format,...) noexcept
Public debug logging wrapper for external classes.
Definition tmc51x0_comm_interface.hpp:1407
CommInterface & operator=(const CommInterface &)=delete
CommInterface & operator=(CommInterface &&)=default
CommInterface(CommInterface &&)=default
CommInterface() noexcept=default
Construct communication interface.
Result< void > SetClkFreq(uint32_t frequency_hz) noexcept
Set external clock frequency on CLK pin (optional)
Definition tmc51x0_comm_interface.hpp:1376
Result< void > WriteRegister(uint8_t address, uint32_t value, uint8_t address_param=0) noexcept
Write a 32-bit register to the TMC5160.
Definition tmc51x0_comm_interface.hpp:1194
CommMode GetMode() const noexcept
Get the underlying communication mode used by this interface.
Definition tmc51x0_comm_interface.hpp:1158
Result< void > GpioSet(TMC51x0CtrlPin pin, GpioSignal signal) noexcept
Set GPIO pin signal state (output control)
Definition tmc51x0_comm_interface.hpp:1206
void DelayMs(uint32_t ms) noexcept
Delay execution for specified milliseconds.
Definition tmc51x0_comm_interface.hpp:1255
void DelayUs(uint32_t us) noexcept
Delay execution for specified microseconds.
Definition tmc51x0_comm_interface.hpp:1263
Result< void > PowerCycle(uint32_t power_off_ms=20, uint32_t power_on_settle_ms=20) noexcept
Power-cycle the TMC51x0 (optional)
Definition tmc51x0_comm_interface.hpp:1299
Result< void > GpioSetInactive(TMC51x0CtrlPin pin) noexcept
Set GPIO pin to inactive state (convenience method)
Definition tmc51x0_comm_interface.hpp:1233
Result< void > GpioSetActive(TMC51x0CtrlPin pin) noexcept
Set GPIO pin to active state (convenience method)
Definition tmc51x0_comm_interface.hpp:1224
Result type for operations that return a value.
Definition tmc51x0_result.hpp:90
CRTP-based SPI implementation of TMC5160CommInterface.
Definition tmc51x0_comm_interface.hpp:1660
CommMode GetMode() const noexcept
Get communication mode (always SPI for this interface)
Definition tmc51x0_comm_interface.hpp:1913
Result< void > EnsureChainLengthKnown(uint8_t daisy_chain_position, const char *context) noexcept
Ensure chain length is known and verified for daisy-chain operations.
Definition tmc51x0_comm_interface.hpp:2760
void SetDaisyChainLength(uint8_t total_length) noexcept
Set the total number of devices in the daisy chain.
Definition tmc51x0_comm_interface.hpp:1708
Result< void > SpiTransfer(const uint8_t *tx, uint8_t *rx, size_t length) noexcept
Low-level SPI transfer for register read/write.
Definition tmc51x0_comm_interface.hpp:1922
uint8_t total_chain_length_
Total number of devices in the daisy chain.
Definition tmc51x0_comm_interface.hpp:2710
SpiCommInterface(SpiCommInterface &&)=default
Result< void > WriteRegister(uint8_t address, uint32_t value, uint8_t daisy_chain_position=0) noexcept
Write a 32-bit register via SPI.
Definition tmc51x0_comm_interface.hpp:2332
std::array< uint8_t, kSpiScratchBytes > rx_scratch_
Definition tmc51x0_comm_interface.hpp:2735
SpiCommInterface(const SpiCommInterface &)=delete
~SpiCommInterface()=default
Protected destructor.
uint8_t AutoDetectChainLength(uint8_t max_devices=8) noexcept
Auto-detect the daisy chain length by sending a unique command that loops back.
Definition tmc51x0_comm_interface.hpp:1757
bool chain_length_verified_
Definition tmc51x0_comm_interface.hpp:2714
SpiCommInterface & operator=(SpiCommInterface &&)=default
SpiCommInterface & operator=(const SpiCommInterface &)=delete
std::array< uint8_t, kSpiScratchBytes > tx_scratch_
Definition tmc51x0_comm_interface.hpp:2734
uint8_t user_specified_chain_length_
Definition tmc51x0_comm_interface.hpp:2712
SpiCommInterface() noexcept
Construct SPI communication interface.
Definition tmc51x0_comm_interface.hpp:1689
static constexpr size_t kSpiScratchBytes
Definition tmc51x0_comm_interface.hpp:1680
uint32_t drv_err_log_count_
Definition tmc51x0_comm_interface.hpp:2739
Result< uint32_t > ReadRegister(uint8_t address, uint8_t daisy_chain_position=0) noexcept
Read a 32-bit register via SPI.
Definition tmc51x0_comm_interface.hpp:1991
uint8_t GetDaisyChainLength() const noexcept
Get the total number of devices in the daisy chain.
Definition tmc51x0_comm_interface.hpp:1719
CRTP-based UART implementation of TMC5160CommInterface.
Definition tmc51x0_comm_interface.hpp:2935
~UartCommInterface()=default
Protected destructor.
Result< bool > GetNaoPin() noexcept
Read NAO (Next Address Output) pin state.
Definition tmc51x0_comm_interface.hpp:2998
UartCommInterface(const UartCommInterface &)=delete
Result< void > UartSend(const uint8_t *data, size_t length) noexcept
Send raw bytes via UART.
Definition tmc51x0_comm_interface.hpp:3009
Result< uint32_t > ReadRegister(uint8_t address, uint8_t node_address=0) noexcept
Read a 32-bit register via UART.
Definition tmc51x0_comm_interface.hpp:3030
UartCommInterface & operator=(const UartCommInterface &)=delete
CommMode GetMode() const noexcept
Get communication mode (always UART for this interface)
Definition tmc51x0_comm_interface.hpp:2957
Result< void > WriteRegister(uint8_t address, uint32_t value, uint8_t node_address=0) noexcept
Write a 32-bit register via UART.
Definition tmc51x0_comm_interface.hpp:3094
UartCommInterface() noexcept
Construct UART communication interface with pin active level configuration.
Definition tmc51x0_comm_interface.hpp:2951
Result< void > UartReceive(uint8_t *data, size_t length) noexcept
Receive raw bytes via UART.
Definition tmc51x0_comm_interface.hpp:3019
UartCommInterface & operator=(UartCommInterface &&)=default
Result< void > SetNaiPin(bool active) noexcept
Set NAI (Next Address Input) pin state for daisy chaining.
Definition tmc51x0_comm_interface.hpp:2976
UartCommInterface(UartCommInterface &&)=default
Definition tmc51x0_register_defs.cpp:10
GpioSignal
GPIO signal states with board-agnostic naming.
Definition tmc51x0_comm_interface.hpp:319
@ ACTIVE
Active signal state (logical high)
@ INACTIVE
Inactive signal state (logical low)
UartFrameType
TMC5160 UART frame types.
Definition tmc51x0_comm_interface.hpp:867
@ ReadReply
Read access reply datagram (8 bytes: 7 bytes + CRC)
@ WriteAccess
Write access datagram (8 bytes: 7 bytes + CRC)
@ ReadRequest
Read access request datagram (4 bytes: 3 bytes + CRC)
LogLevel
Driver-native log levels for debug output.
Definition tmc51x0_comm_interface.hpp:134
static constexpr uint8_t calculateCrc8(const uint8_t *data, size_t length) noexcept
Calculate CRC8 checksum for UART communication.
Definition tmc51x0_comm_interface.hpp:840
@ UNSUPPORTED
Feature not supported by this chip variant.
@ COMM_ERROR
Communication interface error (SPI/UART)
TMC51x0CtrlPin
TMC51x0 control pin identifiers with board-agnostic naming.
Definition tmc51x0_comm_interface.hpp:258
@ EN
Enable pin (DRV_ENN, pin 28) - Active HIGH disables power stage.
CommMode
Supported physical communication modes for TMC51x0.
Definition tmc51x0_comm_interface.hpp:221
Pin active level configuration structure.
Definition tmc51x0_comm_interface.hpp:352
bool diag0
Definition tmc51x0_comm_interface.hpp:365
bool diag1
Definition tmc51x0_comm_interface.hpp:367
bool dir
DIR pin (REFR_DIR, pin 18): HIGH=active (active HIGH)
Definition tmc51x0_comm_interface.hpp:355
bool clk
CLK (pin 12): HIGH=active (active HIGH, if used as output)
Definition tmc51x0_comm_interface.hpp:385
bool encn
Definition tmc51x0_comm_interface.hpp:375
bool dco
Definition tmc51x0_comm_interface.hpp:381
bool GetActiveLevel(TMC51x0CtrlPin pin) const noexcept
Get active level for a specific pin.
Definition tmc51x0_comm_interface.hpp:398
bool ref_right
REFR_DIR (pin 18): LOW=active (typically active LOW)
Definition tmc51x0_comm_interface.hpp:361
bool step
STEP pin (REFL_STEP, pin 17): HIGH=active (active HIGH)
Definition tmc51x0_comm_interface.hpp:356
bool ref_left
REFL_STEP (pin 17): LOW=active (typically active LOW)
Definition tmc51x0_comm_interface.hpp:359
bool enca
Definition tmc51x0_comm_interface.hpp:371
bool encb
Definition tmc51x0_comm_interface.hpp:373
bool spi_mode
SPI_MODE (pin 22): HIGH=SPI mode (active HIGH)
Definition tmc51x0_comm_interface.hpp:389
void SetActiveLevel(TMC51x0CtrlPin pin, bool active_level) noexcept
Set active level for a specific pin.
Definition tmc51x0_comm_interface.hpp:442
bool dcin
DCIN (pin 24): HIGH=active (active HIGH)
Definition tmc51x0_comm_interface.hpp:380
bool en
EN pin (DRV_ENN, pin 28): LOW=enable (active LOW)
Definition tmc51x0_comm_interface.hpp:354
bool sd_mode
SD_MODE (pin 21): HIGH=External Step/Dir (active HIGH)
Definition tmc51x0_comm_interface.hpp:390
bool dcen
DCEN (pin 23): HIGH=active (active HIGH)
Definition tmc51x0_comm_interface.hpp:379
TMC5160 SPI command structure with union-based frame representation.
Definition tmc51x0_comm_interface.hpp:720
void SetFrame(const uint8_t *bytes) noexcept
Set the 5-byte frame from raw bytes.
Definition tmc51x0_comm_interface.hpp:766
static SpiCommand Write(uint8_t addr, uint32_t val) noexcept
Construct a write command.
Definition tmc51x0_comm_interface.hpp:799
static SpiCommand Read(uint8_t addr) noexcept
Construct a read command.
Definition tmc51x0_comm_interface.hpp:786
uint8_t GetAddress() const noexcept
Get register address (bits 6-0 of address byte)
Definition tmc51x0_comm_interface.hpp:739
void GetFrame(uint8_t *bytes) const noexcept
Get the 5-byte frame as raw bytes.
Definition tmc51x0_comm_interface.hpp:776
union tmc51x0::SpiCommand::Frame frame
The 40-bit SPI frame.
bool IsWrite() const noexcept
Check if this is a write command.
Definition tmc51x0_comm_interface.hpp:747
uint32_t GetValue() const noexcept
Get 32-bit data value (for writes) or dummy data (for reads)
Definition tmc51x0_comm_interface.hpp:755
TMC5160 SPI response structure.
Definition tmc51x0_comm_interface.hpp:819
uint32_t value
Definition tmc51x0_comm_interface.hpp:821
SpiStatus status
SPI_STATUS flags from the chip.
Definition tmc51x0_comm_interface.hpp:820
bool success
true if the transport transaction succeeded
Definition tmc51x0_comm_interface.hpp:823
SPI_STATUS structure - status flags returned with each SPI datagram.
Definition tmc51x0_comm_interface.hpp:587
bool PositionReached() const noexcept
Get position reached flag (bit 5)
Definition tmc51x0_comm_interface.hpp:654
static SpiStatus FromByte(uint8_t status_byte) noexcept
Extract SPI_STATUS from response byte.
Definition tmc51x0_comm_interface.hpp:595
bool StopRight() const noexcept
Get stop right switch flag (bit 7)
Definition tmc51x0_comm_interface.hpp:668
const char * ToString() const noexcept
Format status flags as human-readable string.
Definition tmc51x0_comm_interface.hpp:696
bool StopLeft() const noexcept
Get stop left switch flag (bit 6)
Definition tmc51x0_comm_interface.hpp:662
void FormatStatusBits(char *out, size_t cap) const noexcept
Format status bits as compact string (bit names and values)
Definition tmc51x0_comm_interface.hpp:680
bool StallGuard2() const noexcept
Get StallGuard2 flag (bit 2)
Definition tmc51x0_comm_interface.hpp:632
bool DriverError() const noexcept
Get driver error flag (bit 1)
Definition tmc51x0_comm_interface.hpp:624
uint8_t value
Raw SPI_STATUS byte value.
Definition tmc51x0_comm_interface.hpp:588
bool HasError() const noexcept
Check if any error flags are set.
Definition tmc51x0_comm_interface.hpp:609
bool ResetFlag() const noexcept
Get reset flag (bit 0)
Definition tmc51x0_comm_interface.hpp:618
bool Standstill() const noexcept
Get standstill flag (bit 3)
Definition tmc51x0_comm_interface.hpp:640
bool VelocityReached() const noexcept
Get velocity reached flag (bit 4)
Definition tmc51x0_comm_interface.hpp:646
TMC51x0 GPIO pin configuration structure.
Definition tmc51x0_comm_interface.hpp:521
int dc_out_pin
DC Step ready output (ENCN_DCO_CFG6, pin 25) - Same as enc_n_pin.
Definition tmc51x0_comm_interface.hpp:549
int clk_pin
Clock input (CLK, pin 12) - Optional.
Definition tmc51x0_comm_interface.hpp:553
int enc_b_pin
Encoder B (ENCB_DCEN_CFG4, pin 23) - Same as dc_en_pin.
Definition tmc51x0_comm_interface.hpp:541
int ref_left_pin
Left reference switch (REFL_STEP, pin 17) - Same as step_pin.
Definition tmc51x0_comm_interface.hpp:530
int diag1_pin
DIAG1 pin (DIAG1_SWP, pin 27) - Optional.
Definition tmc51x0_comm_interface.hpp:537
int dc_en_pin
Definition tmc51x0_comm_interface.hpp:547
int en_pin
EN pin (DRV_ENN, pin 28) - Required.
Definition tmc51x0_comm_interface.hpp:523
int enc_n_pin
Encoder N (ENCN_DCO_CFG6, pin 25) - Same as dc_out_pin.
Definition tmc51x0_comm_interface.hpp:542
int spi_mode_pin
Definition tmc51x0_comm_interface.hpp:560
TMC51x0PinConfig()=default
Default constructor - all pins unmapped (-1)
int dc_in_pin
Definition tmc51x0_comm_interface.hpp:545
int sd_mode_pin
hardwired. HIGH=External step/dir, LOW=Internal ramp
Definition tmc51x0_comm_interface.hpp:562
int ref_right_pin
Right reference switch (REFR_DIR, pin 18) - Same as dir_pin.
Definition tmc51x0_comm_interface.hpp:532
int diag0_pin
DIAG0 pin (DIAG0_SWN, pin 26) - Optional.
Definition tmc51x0_comm_interface.hpp:536
int step_pin
STEP pin (REFL_STEP, pin 17) - Optional, same as ref_left_pin.
Definition tmc51x0_comm_interface.hpp:526
int dir_pin
DIR pin (REFR_DIR, pin 18) - Optional, same as ref_right_pin.
Definition tmc51x0_comm_interface.hpp:524
TMC51x0PinConfig(int en, int dir=-1, int step=-1) noexcept
Constructor with basic pins.
Definition tmc51x0_comm_interface.hpp:576
int enc_a_pin
Encoder A (ENCA_DCIN_CFG5, pin 24) - Same as dc_in_pin.
Definition tmc51x0_comm_interface.hpp:540
TMC5160 UART command/response frame structure with built-in CRC8.
Definition tmc51x0_comm_interface.hpp:890
bool IsWrite() const noexcept
Check if this is a write frame.
Definition tmc51x0_comm_interface.hpp:952
static UartFrame Write(uint8_t node_addr, uint8_t reg_addr, uint32_t value) noexcept
Construct a write access frame (8 bytes)
Definition tmc51x0_comm_interface.hpp:1037
static UartFrame ReadReply(const uint8_t *bytes) noexcept
Construct a read reply frame from received bytes (8 bytes)
Definition tmc51x0_comm_interface.hpp:1097
uint8_t GetAddress() const noexcept
Get register address from frame.
Definition tmc51x0_comm_interface.hpp:939
bool VerifyCrc() const noexcept
Verify CRC8 checksum of the frame.
Definition tmc51x0_comm_interface.hpp:990
size_t GetSize() const noexcept
Get frame size in bytes based on type.
Definition tmc51x0_comm_interface.hpp:931
void CalculateCrc() noexcept
Calculate and set CRC8 checksum for the frame CRC8 is calculated over all bytes except the CRC byte i...
Definition tmc51x0_comm_interface.hpp:975
union tmc51x0::UartFrame::Frame frame
void GetFrame(uint8_t *bytes) const noexcept
Get the frame as raw bytes.
Definition tmc51x0_comm_interface.hpp:1023
UartFrameType type
Frame type.
Definition tmc51x0_comm_interface.hpp:925
uint32_t GetValue() const noexcept
Get 32-bit data value from frame.
Definition tmc51x0_comm_interface.hpp:960
static UartFrame ReadRequest(uint8_t node_addr, uint8_t reg_addr) noexcept
Construct a read request frame (4 bytes)
Definition tmc51x0_comm_interface.hpp:1073
bool IsValid() const noexcept
Check if frame is valid.
Definition tmc51x0_comm_interface.hpp:1108
void SetFrame(const uint8_t *bytes, UartFrameType frame_type) noexcept
Set the frame from raw bytes.
Definition tmc51x0_comm_interface.hpp:1005
#define TMC51X0_SPI_MAX_CHAIN_DEVICES
Definition tmc51x0_comm_interface.hpp:1672
#define TMC51X0_LOG_DEBUG(comm_obj, level, tag,...)
Definition tmc51x0_comm_interface.hpp:191
Result type for error handling in TMC51x0 driver.
Union for accessing the 40-bit SPI frame in different ways.
Definition tmc51x0_comm_interface.hpp:724
struct tmc51x0::SpiCommand::Frame::@23 fields
Frame as structured fields.
uint64_t raw
Definition tmc51x0_comm_interface.hpp:731
uint8_t bytes[5]
Frame as 5 bytes (for direct byte access)
Definition tmc51x0_comm_interface.hpp:725
uint8_t address_byte
Address byte (bit 7 = write, bits 6-0 = address)
Definition tmc51x0_comm_interface.hpp:728
uint8_t data_bytes[4]
Data bytes (MSB to LSB)
Definition tmc51x0_comm_interface.hpp:729
Union for accessing UART frames in different ways.
Definition tmc51x0_comm_interface.hpp:894
uint8_t reg_addr
Byte 2: Register Address (0x00)
Definition tmc51x0_comm_interface.hpp:918
uint8_t data_bytes[4]
Bytes 3-6: 32-bit data (MSB-first)
Definition tmc51x0_comm_interface.hpp:902
struct tmc51x0::UartFrame::Frame::@26 read_reply_fields
struct tmc51x0::UartFrame::Frame::@24 write_fields
uint8_t node_addr
Byte 1: Node Address.
Definition tmc51x0_comm_interface.hpp:900
uint8_t bytes[8]
Frame as 8 bytes (maximum size)
Definition tmc51x0_comm_interface.hpp:895
uint8_t sync_reserved
Byte 0: Sync (0x05)
Definition tmc51x0_comm_interface.hpp:899
uint8_t master_addr
Byte 1: Master Address (0xFF)
Definition tmc51x0_comm_interface.hpp:917
uint8_t rw_address
Byte 2: RW bit (1) + 7-bit register address.
Definition tmc51x0_comm_interface.hpp:901
struct tmc51x0::UartFrame::Frame::@25 read_request_fields
uint8_t crc
Byte 7: CRC8 checksum.
Definition tmc51x0_comm_interface.hpp:903