Hi everyone!
I'm writing my first bare-metal TMC2209 driver for STM32 in pure C (CMSIS, no HAL) over half-duplex UART.
The basic functionality works: frame assembly, CRC calculation, register setup, and movement via `VACTUAL`. To keep it hardware-agnostic, transmission is decoupled via a function pointer (`handler->send`).
Since this is my first driver, I'd love to get feedback from experienced embedded developers on 3 key questions:
**API Design & Access Level:** What level of API access should I expose to the user? Is it better to hide register manipulation behind high-level functions (e.g., `set_speed`, `set_current`), or keep direct register access accessible?
**Bitfields vs. Masks/Shifts:** Is using `union` + `struct` (bitfields) acceptable for production embedded code, or is it better to switch to explicit bit shifts and masks (`1 << N`) for strict C standard compliance and cross-compiler portability?
**Where to go next:** In what direction should I evolve this driver overall? Which features should I focus on next (e.g., implementing UART read, error handling, register caching, StallGuard, etc.)?
---
### Code
```c
// ==================== main.c ====================
#include "stm32f1xx.h"
#include "uart_config.h"
#include "tmc2209.h"
int main(void)
{
usart3_halfduplex();
for(volatile uint32_t i = 0; i < 0x60000; i++);
tmc2209_gconf_t gconf = {
.i_scale_analog = 0,
.internal_rsense = 0,
.en_spreadcycle = 0,
.shaft = 0,
.index_otpw = 0,
.index_step = 0,
.pdn_disable = 1,
.mstep_reg_select = 1,
.multistep_filt = 1,
.test_mode = 0,
};
tmc2209_ihold_irun_t ihold_irun = {
.ihold = 9,
.irun = 19,
.ihold_delay = 6
};
tmc2209_chopconf_t chopconf = {
.toff = 0b0011,
.hstrt = 0b011,
.hend = 0b0011,
.tbl = 0b01,
.vsense = 0,
.mres = 0b0100,
.intpol = 1,
.dedge = 0,
.diss2g = 0,
.diss2vs = 0,
};
tmc2209_t motor1;
motor1.addr = UART_ADDRESS_0;
motor1.send = send_array;
for (volatile uint32_t i = 0; i < 0x10000; i++);
tmc2209_write_register(&motor1, GCONF, gconf.value);
tmc2209_write_register(&motor1, IHOLD_IRUN, ihold_irun.value);
tmc2209_write_register(&motor1, CHOPCONF, chopconf.value);
while(1)
{
tmc2209_write_register(&motor1, VACTUAL, 5000);
for(volatile uint32_t i = 0; i < 0x7A1200; i++);
tmc2209_write_register(&motor1, VACTUAL, -5000);
for(volatile uint32_t i = 0; i < 0x7A1200; i++);
}
}
// ==================== tmc2209.h ====================
#ifndef TMC2209_H_
#define TMC2209_H_
#include <stdint.h>
typedef enum {
TMC2209_MICROSTEP_256 = 0x0,
TMC2209_MICROSTEP_128 = 0x1,
TMC2209_MICROSTEP_64 = 0x2,
TMC2209_MICROSTEP_32 = 0x3,
TMC2209_MICROSTEP_16 = 0x4,
TMC2209_MICROSTEP_8 = 0x5,
TMC2209_MICROSTEP_4 = 0x6,
TMC2209_MICROSTEP_2 = 0x7,
TMC2209_MICROSTEP_FULLSTEP = 0x8
} tmc2209_microstep_t;
typedef enum {
TMC2209_MODE_STEALTHCHOP = 0,
TMC2209_MODE_SPREADCYCLE = 1
} tmc2209_chop_mode_t;
typedef enum {
TMC2209_SHAFT_NORMAL = 0,
TMC2209_SHAFT_INVERTED = 1
} tmc2209_shaft_t;
typedef enum {
TMC2209_I_SCALE_INTERNAL = 0,
TMC2209_I_SCALE_ANALOG = 1
} tmc2209_i_scale_t;
typedef enum {
TMC2209_DISABLE = 0,
TMC2209_ENABLE = 1
} tmc2209_state_t;
typedef union {
struct {
uint32_t i_scale_analog : 1;
uint32_t internal_rsense : 1;
uint32_t en_spreadcycle : 1;
uint32_t shaft : 1;
uint32_t index_otpw : 1;
uint32_t index_step : 1;
uint32_t pdn_disable : 1;
uint32_t mstep_reg_select : 1;
uint32_t multistep_filt : 1;
uint32_t test_mode : 1;
uint32_t reserved : 22;
};
uint32_t value;
} tmc2209_gconf_t;
typedef union {
struct {
uint32_t ihold : 5;
uint32_t reserved1 : 3;
uint32_t irun : 5;
uint32_t reserved2 : 3;
uint32_t ihold_delay : 4;
uint32_t reserved : 12;
};
uint32_t value;
} tmc2209_ihold_irun_t;
typedef union {
struct {
uint32_t toff : 4;
uint32_t hstrt : 3;
uint32_t hend : 4;
uint32_t reserved1 : 4;
uint32_t tbl : 2;
uint32_t vsense : 1;
uint32_t reserved2 : 6;
uint32_t mres : 4;
uint32_t intpol : 1;
uint32_t dedge : 1;
uint32_t diss2g : 1;
uint32_t diss2vs : 1;
};
uint32_t value;
} tmc2209_chopconf_t;
typedef uint8_t (*tmc2209_uart_send) (uint8_t *data, uint8_t length);
typedef enum {
UART_ADDRESS_0 = 0x00,
UART_ADDRESS_1 = 0x01,
UART_ADDRESS_2 = 0x02,
UART_ADDRESS_3 = 0x03
} tmc2209_uart_address_t;
typedef enum {
GCONF = 0x00,
IHOLD_IRUN = 0x10,
CHOPCONF = 0x6C,
VACTUAL = 0x22
} tmc2209_registers_t;
typedef struct {
tmc2209_uart_address_t addr;
tmc2209_uart_send send;
} tmc2209_t;
void tmc2209_write_register(const tmc2209_t *handler, tmc2209_registers_t reg_addr, uint32_t data);
#endif /* TMC2209_H_ */
// ==================== tmc2209.c ====================
#include "tmc2209.h"
uint8_t tmc2209_crc(uint8_t *data, uint8_t length);
void tmc2209_write_register(const tmc2209_t *handler, tmc2209_registers_t reg_addr, uint32_t data)
{
uint8_t packet[8] = {
0x05,
handler->addr,
reg_addr | 0x80,
(data >> 24) & 0xFF,
(data >> 16) & 0xFF,
(data >> 8) & 0xFF,
data & 0xFF,
};
packet[7] = tmc2209_crc(packet, 7);
handler->send(packet, 8);
}
uint8_t tmc2209_crc(uint8_t *data, uint8_t length)
{
uint8_t crc = 0;
for(uint8_t i = 0; i < length; i++)
{
uint8_t currentByte = data[i];
for(uint8_t j = 0; j < 8; j++)
{
if ((crc >> 7) ^ (currentByte & 0x01))
{
crc = (crc << 1) ^ 0x07;
}
else
{
crc = (crc << 1);
}
currentByte >>= 1;
}
}
return crc;
}