blob: 0a2479f050b8615db966e2ee3210651e39f82175 [file]
/*
* SPDX-FileCopyrightText: Copyright (c) 2026 Cherrence Sarip <cherrence.sarip@analog.com>
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @file main.c
* @brief TMC6460 open-loop voltage mode BLDC motor control sample.
*
* Direct port of pytrinamic bldc_openloop_voltage_mode.py with
* explicit hardware initialization that the eval board firmware does.
*/
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/logging/log.h>
#include <zephyr/drivers/misc/tmc6460/tmc6460.h>
LOG_MODULE_REGISTER(tmc6460_voltage_mode, LOG_LEVEL_INF);
/* Board GPIOs */
#define DRV_EN_NODE DT_ALIAS(tmc6460_drv_en)
static const struct gpio_dt_spec drv_en = GPIO_DT_SPEC_GET(DRV_EN_NODE, gpios);
#define SLEEPN_NODE DT_ALIAS(tmc6460_sleepn)
static const struct gpio_dt_spec sleepn = GPIO_DT_SPEC_GET(SLEEPN_NODE, gpios);
/*
* Motor Parameters. The physical pole-pair count is a hardware trait and is
* read from devicetree; the remaining values are open-loop demonstration
* setpoints (matching the reference pytrinamic script).
*/
#define TMC6460_NODE DT_NODELABEL(tmc6460)
#define N_POLE_PAIRS DT_PROP(TMC6460_NODE, pole_pairs)
#define OPENLOOP_VELOCITY 2000
#define OPENLOOP_VOLTAGE 1500
#define RUN_TIME_MS 5000
/*
* Ramper velocity limit. The reference pytrinamic script never writes V_MAX
* because the eval-board firmware pre-initialises it. On a bare chip V_MAX
* powers up as 0, which clamps the velocity ramper (V_ACTUAL stays 0, so
* PHI_E never rotates and the motor cannot spin). Set it to the target
* open-loop velocity so the ramper can actually ramp.
*/
#define RAMPER_V_MAX_VAL 2000U
/*
* Ramper acceleration profile (units per pytrinamic bldc_openloop_voltage_mode.py).
* A1/A2 are the acceleration values for the two ramp phases and A_MAX caps the
* peak acceleration.
*/
#define RAMPER_A1_VAL 100U
#define RAMPER_A2_VAL 200U
#define RAMPER_A_MAX_VAL 100U
/*
* FOC voltage/torque/flux limiters. A genuine cold-VS power-on leaves all of
* these at sane defaults (U_S_MAX/UQ_UD_LIMITS = 0x7fff, TORQUE_FLUX_LIMITS =
* 0x7fff7fff, VELOCITY_LIMIT = 0x7fffffff). A warm nSLEEP-only reset does NOT
* restore the full POR state, so after a prior corrupting run any single one of
* them can read 0 - and one zero limiter silently forces FOC_UQ_UD_LIMITED to 0
* even when the others are wide open. FOC_UQ_UD then shows the request but the
* PWM stays at center and the motor gets no drive. The reference script never
* writes the torque/flux or velocity limiter (it relies on a fresh POR), so
* write every limiter explicitly to make the port independent of reset depth.
*/
#define FOC_U_S_MAX_VAL 0x7FFFU
#define FOC_UQ_UD_LIMIT_VAL 0x7FFFU
#define FOC_TORQUE_FLUX_LIMITS_VAL 0x7FFF7FFFU
#define FOC_VELOCITY_LIMIT_VAL 0x7FFFFFFFU
/*
* Convenience field descriptors
*/
#define FIELD_MOTOR_TYPE \
TMC6460_FIELD(TMC6460_MCC_CONFIG_MOTOR_MOTION, \
TMC6460_MCC_CONFIG_MOTOR_MOTION_MOTOR_TYPE_MASK, \
TMC6460_MCC_CONFIG_MOTOR_MOTION_MOTOR_TYPE_SHIFT, false)
#define FIELD_N_POLE_PAIRS \
TMC6460_FIELD(TMC6460_MCC_CONFIG_MOTOR_MOTION, \
TMC6460_MCC_CONFIG_MOTOR_MOTION_N_POLE_PAIRS_MASK, \
TMC6460_MCC_CONFIG_MOTOR_MOTION_N_POLE_PAIRS_SHIFT, false)
#define FIELD_MOTION_MODE \
TMC6460_FIELD(TMC6460_MCC_CONFIG_MOTOR_MOTION, \
TMC6460_MCC_CONFIG_MOTOR_MOTION_MOTION_MODE_MASK, \
TMC6460_MCC_CONFIG_MOTOR_MOTION_MOTION_MODE_SHIFT, false)
#define FIELD_RAMP_MODE \
TMC6460_FIELD(TMC6460_MCC_CONFIG_MOTOR_MOTION, \
TMC6460_MCC_CONFIG_MOTOR_MOTION_RAMP_MODE_MASK, \
TMC6460_MCC_CONFIG_MOTOR_MOTION_RAMP_MODE_SHIFT, false)
#define FIELD_RAMP_EN \
TMC6460_FIELD(TMC6460_MCC_CONFIG_MOTOR_MOTION, \
TMC6460_MCC_CONFIG_MOTOR_MOTION_RAMP_EN_MASK, \
TMC6460_MCC_CONFIG_MOTOR_MOTION_RAMP_EN_SHIFT, false)
#define FIELD_RAMP_USE_PHI_E \
TMC6460_FIELD(TMC6460_MCC_CONFIG_MOTOR_MOTION, \
TMC6460_MCC_CONFIG_MOTOR_MOTION_RAMP_USE_PHI_E_MASK, \
TMC6460_MCC_CONFIG_MOTOR_MOTION_RAMP_USE_PHI_E_SHIFT, false)
#define FIELD_CHARGE_PUMP_EN \
TMC6460_FIELD(TMC6460_MCC_CONFIG_GDRV, TMC6460_MCC_CONFIG_GDRV_CHARGE_PUMP_EN_MASK, \
TMC6460_MCC_CONFIG_GDRV_CHARGE_PUMP_EN_SHIFT, false)
/*
* Clock source selection.
*
* The TMC6460 generates its internal 1 MHz PLL reference by dividing the
* selected source clock with CLOCK_DIVIDER. The eval board firmware uses:
* - internal 15 MHz oscillator -> CLOCK_DIVIDER = 15 - 1 = 14
* - external 16 MHz clock (CLK_IN pin) -> CLOCK_DIVIDER = 16 - 1 = 15
*
* A bare Nucleo board provides no external clock, so use the internal
* oscillator. The chip's power-on default leaves CLOCK_DIVIDER at 0, so the
* 1 MHz reference never appears and the PLL cannot lock (CLK_1M0_TIMEOUT and
* CLK_STUCK get set). Writing a valid divider is what makes the motor run.
*/
#define TMC6460_USE_EXTERNAL_CLOCK 0
#if TMC6460_USE_EXTERNAL_CLOCK
#define TMC6460_CLOCK_DIVIDER_VAL 15U /* 16 MHz / 16 = 1 MHz */
#define TMC6460_PLL_SRC_VAL 1U /* EXT_CLK */
#else
#define TMC6460_CLOCK_DIVIDER_VAL 14U /* 15 MHz / 15 = 1 MHz */
#define TMC6460_PLL_SRC_VAL 0U /* INT_CLK */
#endif
/**
* @brief Initialize the TMC6460 clock system.
*
* The CLK_CTRL_CONFIG.COMMIT bit latches the clock configuration. At power-on
* the chip already has COMMIT=1 with CLOCK_DIVIDER=0, so it committed an invalid
* divide-by-zero config and the clock FSM is stuck (CLK_STUCK / CLK_1M0_TIMEOUT).
* Simply writing a new value while COMMIT stays high does not re-latch it.
*
* The sequence below therefore:
* 1. Disables the clock FSM and clears COMMIT (write 0) to reset the FSM.
* 2. Loads CLOCK_DIVIDER + clock enables with COMMIT still low.
* 3. Sets COMMIT (0 -> 1 edge) to latch the new configuration.
* Each step reads the register back so we can confirm the divider actually
* stored and the commit took effect.
*/
static int tmc6460_clock_init(const struct device *dev)
{
uint32_t cfg;
uint32_t val;
int ret;
int retry;
/* Read current CLK_CTRL_CONFIG to see the power-on defaults */
ret = tmc6460_read(dev, TMC6460_CLK_CTRL_CONFIG, &val);
if (ret != 0) {
return ret;
}
LOG_INF("CLK_CTRL_CONFIG default = 0x%08x", val);
/* Step 1: Disable the FSM and clear COMMIT to reset the stuck clock FSM */
ret = tmc6460_write(dev, TMC6460_CLK_CTRL_CONFIG, 0U);
if (ret != 0) {
return ret;
}
k_msleep(5);
ret = tmc6460_read(dev, TMC6460_CLK_CTRL_CONFIG, &val);
if (ret != 0) {
return ret;
}
LOG_INF("CLK_CTRL_CONFIG after disable = 0x%08x", val);
/* Step 2: Load divider + clock enables + PLL source, COMMIT still low */
cfg = ((uint32_t)TMC6460_CLOCK_DIVIDER_VAL << TMC6460_CLK_CTRL_CONFIG_CLOCK_DIVIDER_SHIFT) |
TMC6460_CLK_CTRL_CONFIG_CLK_FSM_EN_MASK | TMC6460_CLK_CTRL_CONFIG_PWM_CLK_EN_MASK |
TMC6460_CLK_CTRL_CONFIG_ADC_CLK_EN_MASK | TMC6460_CLK_CTRL_CONFIG_PLL_EN_MASK |
((uint32_t)TMC6460_PLL_SRC_VAL << TMC6460_CLK_CTRL_CONFIG_PLL_SRC_SHIFT);
ret = tmc6460_write(dev, TMC6460_CLK_CTRL_CONFIG, cfg);
if (ret != 0) {
return ret;
}
k_msleep(1);
ret = tmc6460_read(dev, TMC6460_CLK_CTRL_CONFIG, &val);
if (ret != 0) {
return ret;
}
LOG_INF("CLK_CTRL_CONFIG loaded (no commit) = 0x%08x", val);
LOG_INF(" -> CLOCK_DIVIDER read back = %u (want %u)",
(val & TMC6460_CLK_CTRL_CONFIG_CLOCK_DIVIDER_MASK) >>
TMC6460_CLK_CTRL_CONFIG_CLOCK_DIVIDER_SHIFT,
(uint32_t)TMC6460_CLOCK_DIVIDER_VAL);
/*
* Step 3: Set COMMIT (0 -> 1 edge) to latch the configuration. This is a
* fire-and-forget write, so it is not read back immediately.
*
* Committing switches the chip's clock domain (PLL enable + new divider).
* On the UART transport this momentarily changes the reference the chip
* uses to auto-baud off the sync byte, so a register read issued *during*
* the switch comes back with corrupted framing (UART RX timeout). The
* reference recovers once the PLL has locked and the clock is stable, so
* let the switch settle before polling and treat transient read failures
* during the transition as "not ready yet" instead of a hard error. On
* SPI these reads never fail, so the same path is safe for both buses.
*/
ret = tmc6460_write(dev, TMC6460_CLK_CTRL_CONFIG,
cfg | TMC6460_CLK_CTRL_CONFIG_COMMIT_MASK);
if (ret != 0) {
return ret;
}
/* Let the clock-domain switch settle before resuming reads */
k_msleep(10);
/*
* Poll CLK_CTRL_STATUS for PLL lock (up to ~500 ms), tolerating transient
* UART framing errors while the clock is still switching.
*/
for (retry = 0; retry < 50; retry++) {
k_msleep(10);
ret = tmc6460_read(dev, TMC6460_CLK_CTRL_STATUS, &val);
if (ret != 0) {
/*
* Transient read error during the clock switch (UART
* reference momentarily off). Keep polling; the loop
* bound still limits the total wait.
*/
continue;
}
if (val & TMC6460_CLK_CTRL_STATUS_PLL_READY_MASK) {
LOG_INF("PLL locked after %d ms, STATUS=0x%08x", (retry + 1) * 10, val);
return 0;
}
/* Check for PLL error */
if (val & TMC6460_CLK_CTRL_STATUS_PLL_ERR_MASK) {
LOG_ERR("PLL error! STATUS=0x%08x", val);
return -EIO;
}
}
LOG_WRN("PLL did not lock after 500ms, STATUS=0x%08x", val);
LOG_WRN(" CLK_1M0_OK=%u PLL_ERR=%u PLL_LOCK=%u PLL_READY=%u", (val & 0x01U) ? 1U : 0U,
(val & 0x10U) ? 1U : 0U, (val & 0x20U) ? 1U : 0U, (val & 0x40U) ? 1U : 0U);
return 0;
}
/**
* @brief Reset the on-chip ADCs so they re-initialize with a valid clock.
*
* When the TMC6460 powers up without a valid PLL/1 MHz reference, the ADCs
* fail to initialize and latch ADC_FAIL, which blocks SYS_READY (and therefore
* the gate driver). Toggling the active-low NRST_ADC_0/1 bits (assert then
* release) forces the ADC state machines to restart now that the clock runs.
* CHIP_EVENTS is then cleared (write-1-to-clear) to drop the latched failures.
*/
static int tmc6460_adc_reset(const struct device *dev)
{
uint32_t cfg;
uint32_t val;
int ret;
ret = tmc6460_read(dev, TMC6460_ADC_CONFIG, &cfg);
if (ret != 0) {
return ret;
}
LOG_INF("ADC_CONFIG before reset = 0x%08x (CSA_AZ_FLT_EXP=0x%x)", cfg,
(cfg & TMC6460_ADC_CONFIG_CSA_AZ_FLT_EXP_MASK) >>
TMC6460_ADC_CONFIG_CSA_AZ_FLT_EXP_SHIFT);
/*
* If the current-sense ADCs are already ready (for example inherited
* from a prior cold power-on), leave them untouched - a needless NRST
* cycle can only lose that state.
*/
ret = tmc6460_read(dev, TMC6460_ADC_STATUS, &val);
if (ret != 0) {
return ret;
}
if ((val & TMC6460_ADC_STATUS_ADC_0_READY_MASK) &&
(val & TMC6460_ADC_STATUS_ADC_1_READY_MASK)) {
LOG_INF("ADCs already ready (ADC_STATUS=0x%08x); leaving untouched", val);
return 0;
}
/*
* Assert reset by clearing ONLY the NRST bits (2,3). Every other field,
* crucially CSA_AZ_FLT_EXP, is preserved via read-modify-write: zeroing
* the auto-zero filter stops the modulators from ever reaching READY.
*/
cfg &= ~TMC6460_ADC_CONFIG_NRST_BOTH_MASK;
ret = tmc6460_write(dev, TMC6460_ADC_CONFIG, cfg);
if (ret != 0) {
return ret;
}
/*
* Hold the current-sense delta-sigma modulators in reset long enough to
* drain their internal integrator state before releasing them.
*/
k_msleep(150);
/* Release reset by setting the NRST bits, still preserving CSA_AZ_FLT_EXP */
cfg |= TMC6460_ADC_CONFIG_NRST_BOTH_MASK;
ret = tmc6460_write(dev, TMC6460_ADC_CONFIG, cfg);
if (ret != 0) {
return ret;
}
/* Read back the real value the chip holds after release */
ret = tmc6460_read(dev, TMC6460_ADC_CONFIG, &val);
if (ret != 0) {
return ret;
}
LOG_INF("ADC_CONFIG after reset = 0x%08x (CSA_AZ_FLT_EXP=0x%x)", val,
(val & TMC6460_ADC_CONFIG_CSA_AZ_FLT_EXP_MASK) >>
TMC6460_ADC_CONFIG_CSA_AZ_FLT_EXP_SHIFT);
/* Clear latched fail events (write 1s to clear) */
ret = tmc6460_write(dev, TMC6460_CHIP_EVENTS, 0xFFFFFFFFU);
if (ret != 0) {
return ret;
}
/* Give the modulators time to settle before the first poll */
k_msleep(50);
/* Wait for both ADCs to report ready (up to ~1.5s) */
ret = tmc6460_poll_flag(dev, TMC6460_ADC_STATUS,
TMC6460_ADC_STATUS_ADC_0_READY_MASK |
TMC6460_ADC_STATUS_ADC_1_READY_MASK,
10U, 1500U);
if (ret == 0) {
LOG_INF("ADCs ready");
return 0;
}
/*
* The current-sense ADCs did not reach READY. This is the genuine fault:
* ADC_FAIL_STATUS stays asserted and the gate driver will not turn on.
* No override is applied - the failure is reported honestly. If the
* CSA_AZ_FLT_EXP logged above reads 0, a prior blind write wiped the
* auto-zero filter and a cold VS power-cycle is required to restore the
* POR default before this (now non-destructive) firmware can succeed.
*/
(void)tmc6460_read(dev, TMC6460_ADC_STATUS, &val);
LOG_ERR("ADC_0/1 never reached READY (ADC_STATUS=0x%08x, ADC_0_READY=%u ADC_1_READY=%u)",
val, (val & TMC6460_ADC_STATUS_ADC_0_READY_MASK) ? 1U : 0U,
(val & TMC6460_ADC_STATUS_ADC_1_READY_MASK) ? 1U : 0U);
LOG_ERR("ADC_FAIL is real; the motor will not spin. Try a cold VS power-cycle.");
return -EIO;
}
int main(void)
{
const struct device *dev = DEVICE_DT_GET(DT_NODELABEL(tmc6460));
uint32_t val;
int ret;
LOG_INF("TMC6460 Voltage Mode BLDC Sample");
/* --- Board-level GPIO init --- */
if (!gpio_is_ready_dt(&sleepn)) {
LOG_ERR("SLEEPN GPIO not ready");
return -ENODEV;
}
/*
* Perform an explicit nSLEEP reset pulse (low -> high) like the eval
* firmware does. This guarantees the TMC6460 starts from a clean
* power-on state before we reconfigure its clock system.
*/
ret = gpio_pin_configure_dt(&sleepn, GPIO_OUTPUT_INACTIVE);
if (ret < 0) {
LOG_ERR("Failed to configure SLEEPN: %d", ret);
return ret;
}
LOG_INF("SLEEPN asserted (LOW) - resetting chip");
/*
* Hold nSLEEP low long enough to reset the analog front end. nSLEEP is the
* only line that can clear analog state (the current-sense ADCs); a short
* pulse resets the digital core but can leave the modulators latched.
*/
k_msleep(50);
gpio_pin_set_dt(&sleepn, 1);
LOG_INF("SLEEPN deasserted (HIGH)");
k_msleep(10);
if (!gpio_is_ready_dt(&drv_en)) {
LOG_ERR("DRV_EN GPIO not ready");
return -ENODEV;
}
ret = gpio_pin_configure_dt(&drv_en, GPIO_OUTPUT_ACTIVE);
if (ret < 0) {
LOG_ERR("Failed to configure DRV_EN: %d", ret);
return ret;
}
LOG_INF("DRV_EN asserted (HIGH)");
/* Wait for TMC6460 to wake up */
k_msleep(200);
if (!device_is_ready(dev)) {
LOG_ERR("TMC6460 device not ready");
return -ENODEV;
}
/* Verify SPI communication */
ret = tmc6460_read(dev, TMC6460_CHIP_ID, &val);
if (ret != 0) {
LOG_ERR("Failed to read CHIP_ID: %d", ret);
return ret;
}
LOG_INF("CHIP_ID = 0x%08x", val);
/* Check initial status */
ret = tmc6460_read(dev, TMC6460_CHIP_STATUS_FLAGS, &val);
if (ret == 0) {
LOG_INF("Initial STATUS_FLAGS = 0x%08x (SYS_READY=%u, GDRV_ON=%u)", val,
(val >> 30) & 1U, (val >> 31) & 1U);
}
/*
* Hold the ADCs in reset BEFORE touching the clock.
* Tearing down and re-locking the PLL while the current-sense ADCs are
* live can make them latch a failure. Assert NRST now so the ADCs stay in
* reset across the whole clock bring-up, then release them in
* tmc6460_adc_reset() once the PLL is stable. This is a read-modify-write
* that clears ONLY the NRST bits (2,3) and preserves CSA_AZ_FLT_EXP - a
* blind write would wipe the auto-zero filter and prevent the ADCs from
* ever reaching READY.
*/
ret = tmc6460_read(dev, TMC6460_ADC_CONFIG, &val);
if (ret != 0) {
LOG_ERR("Failed to read ADC_CONFIG: %d", ret);
return ret;
}
val &= ~TMC6460_ADC_CONFIG_NRST_BOTH_MASK;
ret = tmc6460_write(dev, TMC6460_ADC_CONFIG, val);
if (ret != 0) {
LOG_ERR("Failed to assert ADC reset: %d", ret);
return ret;
}
LOG_INF("ADC held in reset for clock bring-up (ADC_CONFIG=0x%08x)", val);
/*
* Clock initialization.
* Uses read-modify-write to preserve CLOCK_DIVIDER and other defaults.
*/
ret = tmc6460_clock_init(dev);
if (ret != 0) {
LOG_ERR("Clock init failed: %d", ret);
return ret;
}
/* Check if system is ready now */
ret = tmc6460_read(dev, TMC6460_CHIP_STATUS_FLAGS, &val);
if (ret == 0) {
LOG_INF("Post-clock STATUS_FLAGS = 0x%08x (SYS_READY=%u, GDRV_ON=%u)", val,
(val >> 30) & 1U, (val >> 31) & 1U);
}
/*
* Reset the ADCs.
* The ADCs latched a failure while there was no valid clock at power-on.
* Toggle their NRST now that the PLL is locked so ADC_FAIL clears and the
* chip can reach SYS_READY (required for the gate driver to turn on).
*/
ret = tmc6460_adc_reset(dev);
if (ret != 0) {
LOG_ERR("ADC reset failed: %d", ret);
return ret;
}
/*
* Gate driver + charge pump.
* Use read-modify-write to preserve other GDRV fields.
*/
/* Enable charge pump first */
ret = tmc6460_field_write(dev, FIELD_CHARGE_PUMP_EN, 1U);
if (ret != 0) {
LOG_ERR("Failed to enable charge pump: %d", ret);
return ret;
}
/* Wait for charge pump */
k_msleep(50);
/* Gate driver config */
ret = tmc6460_field_write(dev,
TMC6460_FIELD(TMC6460_MCC_CONFIG_GDRV,
TMC6460_MCC_CONFIG_GDRV_USE_INTERNAL_R_REF_MASK,
TMC6460_MCC_CONFIG_GDRV_USE_INTERNAL_R_REF_SHIFT,
false),
0U);
if (ret != 0) {
LOG_ERR("Failed to set R_REF: %d", ret);
return ret;
}
ret = tmc6460_field_write(dev,
TMC6460_FIELD(TMC6460_MCC_CONFIG_GDRV,
TMC6460_MCC_CONFIG_GDRV_SLEW_RATE_MASK,
TMC6460_MCC_CONFIG_GDRV_SLEW_RATE_SHIFT, false),
TMC6460_SLEW_RATE_SR_400_V_PER_US);
if (ret != 0) {
LOG_ERR("Failed to set SLEW_RATE: %d", ret);
return ret;
}
ret = tmc6460_field_write(dev,
TMC6460_FIELD(TMC6460_MCC_CONFIG_GDRV,
TMC6460_MCC_CONFIG_GDRV_LS_RES_ON_MASK,
TMC6460_MCC_CONFIG_GDRV_LS_RES_ON_SHIFT, false),
TMC6460_LS_RES_55_MOHM);
if (ret != 0) {
LOG_ERR("Failed to set LS_RES: %d", ret);
return ret;
}
/* Enable gate driver */
ret = tmc6460_enable(dev);
if (ret != 0) {
LOG_ERR("Failed to enable gate driver: %d", ret);
return ret;
}
/* Wait and check status */
k_msleep(50);
ret = tmc6460_read(dev, TMC6460_CHIP_STATUS_FLAGS, &val);
if (ret == 0) {
LOG_INF("Post-GDRV STATUS_FLAGS = 0x%08x (SYS_READY=%u, GDRV_ON=%u)", val,
(val >> 30) & 1U, (val >> 31) & 1U);
}
ret = tmc6460_read(dev, TMC6460_MCC_CONFIG_GDRV, &val);
if (ret == 0) {
LOG_INF("GDRV = 0x%08x", val);
}
/*
* Motor configuration (matches pytrinamic script exactly).
*/
LOG_INF("Configuring motor settings...");
ret = tmc6460_field_write(dev, FIELD_MOTOR_TYPE, TMC6460_MOTOR_TYPE_BLDC);
if (ret != 0) {
LOG_ERR("Failed to set MOTOR_TYPE: %d", ret);
return ret;
}
ret = tmc6460_field_write(dev, FIELD_N_POLE_PAIRS, N_POLE_PAIRS);
if (ret != 0) {
LOG_ERR("Failed to set N_POLE_PAIRS: %d", ret);
return ret;
}
/* ADC and CSA setup */
ret = tmc6460_field_write(dev,
TMC6460_FIELD(TMC6460_MCC_ADC_CSA_GAIN,
TMC6460_MCC_ADC_CSA_GAIN_CSA_GAIN_MASK,
TMC6460_MCC_ADC_CSA_GAIN_CSA_GAIN_SHIFT, false),
TMC6460_CSA_GAIN_X1);
if (ret != 0) {
LOG_ERR("Failed to set CSA_GAIN: %d", ret);
return ret;
}
/* PWM setup */
ret = tmc6460_field_write(dev,
TMC6460_FIELD(TMC6460_MCC_CONFIG_PWM_PERIOD,
TMC6460_MCC_CONFIG_PWM_PERIOD_MAX_COUNT_MASK,
TMC6460_MCC_CONFIG_PWM_PERIOD_MAX_COUNT_SHIFT,
false),
4800U);
if (ret != 0) {
LOG_ERR("Failed to set PWM_PERIOD: %d", ret);
return ret;
}
ret = tmc6460_field_write(dev,
TMC6460_FIELD(TMC6460_MCC_CONFIG_PWM,
TMC6460_MCC_CONFIG_PWM_SV_MODE_MASK,
TMC6460_MCC_CONFIG_PWM_SV_MODE_SHIFT, false),
TMC6460_PWM_SV_MODE_HARMONIC);
if (ret != 0) {
LOG_ERR("Failed to set SV_MODE: %d", ret);
return ret;
}
ret = tmc6460_field_write(dev,
TMC6460_FIELD(TMC6460_MCC_CONFIG_PWM,
TMC6460_MCC_CONFIG_PWM_CHOP_MASK,
TMC6460_MCC_CONFIG_PWM_CHOP_SHIFT, false),
TMC6460_PWM_CHOP_CENTERED);
if (ret != 0) {
LOG_ERR("Failed to set CHOP: %d", ret);
return ret;
}
/* Ramper setup */
ret = tmc6460_field_write(dev, FIELD_RAMP_MODE, TMC6460_RAMP_MODE_VELOCITY);
if (ret != 0) {
LOG_ERR("Failed to set RAMP_MODE: %d", ret);
return ret;
}
ret = tmc6460_field_write(dev, FIELD_RAMP_USE_PHI_E, 1U);
if (ret != 0) {
LOG_ERR("Failed to set RAMP_USE_PHI_E: %d", ret);
return ret;
}
ret = tmc6460_field_write(dev, FIELD_RAMP_EN, 1U);
if (ret != 0) {
LOG_ERR("Failed to set RAMP_EN: %d", ret);
return ret;
}
ret = tmc6460_write(dev, TMC6460_RAMPER_A1, RAMPER_A1_VAL);
if (ret != 0) {
LOG_ERR("Failed to set A1: %d", ret);
return ret;
}
ret = tmc6460_write(dev, TMC6460_RAMPER_A2, RAMPER_A2_VAL);
if (ret != 0) {
LOG_ERR("Failed to set A2: %d", ret);
return ret;
}
ret = tmc6460_write(dev, TMC6460_RAMPER_A_MAX, RAMPER_A_MAX_VAL);
if (ret != 0) {
LOG_ERR("Failed to set A_MAX: %d", ret);
return ret;
}
/*
* The reference pytrinamic script never writes V_MAX because the eval
* firmware pre-initialises it; on a bare chip it can be 0, which clamps the
* velocity ramper to a standstill. Write it explicitly.
*/
ret = tmc6460_write(dev, TMC6460_RAMPER_V_MAX, RAMPER_V_MAX_VAL);
if (ret != 0) {
LOG_ERR("Failed to set V_MAX: %d", ret);
return ret;
}
/*
* FOC voltage/torque/flux limiters. The reference script never writes the
* torque/flux or velocity limiter (it relies on a fresh power-on default),
* so write every limiter explicitly to make the sample independent of reset
* depth: one limiter left at 0 by a warm nSLEEP-only reset would silently
* force FOC_UQ_UD_LIMITED to 0 and the motor would get no drive.
*/
ret = tmc6460_write(dev, TMC6460_FOC_PID_U_S_MAX, FOC_U_S_MAX_VAL);
if (ret != 0) {
LOG_ERR("Failed to set U_S_MAX: %d", ret);
return ret;
}
ret = tmc6460_write(dev, TMC6460_FOC_PID_UQ_UD_LIMITS, FOC_UQ_UD_LIMIT_VAL);
if (ret != 0) {
LOG_ERR("Failed to set UQ_UD_LIMITS: %d", ret);
return ret;
}
ret = tmc6460_write(dev, TMC6460_FOC_PID_TORQUE_FLUX_LIMITS, FOC_TORQUE_FLUX_LIMITS_VAL);
if (ret != 0) {
LOG_ERR("Failed to set TORQUE_FLUX_LIMITS: %d", ret);
return ret;
}
ret = tmc6460_write(dev, TMC6460_FOC_PID_VELOCITY_LIMIT, FOC_VELOCITY_LIMIT_VAL);
if (ret != 0) {
LOG_ERR("Failed to set VELOCITY_LIMIT: %d", ret);
return ret;
}
/* Enable voltage control mode */
ret = tmc6460_write(dev, TMC6460_EXT_CTRL_VOLTAGE, 0U);
if (ret != 0) {
LOG_ERR("Failed to clear VOLTAGE: %d", ret);
return ret;
}
ret = tmc6460_field_write(dev, FIELD_MOTION_MODE, TMC6460_MOTION_MODE_VOLTAGE_EXT);
if (ret != 0) {
LOG_ERR("Failed to set MOTION_MODE: %d", ret);
return ret;
}
/*
* Clear any latched bring-up events (ADC/CP/UV fire while the clock and
* ADCs are initialising) so the register starts clean and any new fault
* that trips during motion is unambiguous.
*/
ret = tmc6460_write(dev, TMC6460_CHIP_EVENTS, 0xFFFFFFFFU);
if (ret != 0) {
LOG_ERR("Failed to clear events: %d", ret);
return ret;
}
/* Start motor */
LOG_INF("Turning motor...");
ret = tmc6460_field_write(dev,
TMC6460_FIELD(TMC6460_EXT_CTRL_VOLTAGE,
TMC6460_EXT_CTRL_VOLTAGE_UD_MASK,
TMC6460_EXT_CTRL_VOLTAGE_UD_SHIFT, true),
(uint32_t)(uint16_t)OPENLOOP_VOLTAGE);
if (ret != 0) {
LOG_ERR("Failed to set VOLTAGE UD: %d", ret);
return ret;
}
ret = tmc6460_write(dev, TMC6460_FOC_PID_VELOCITY_TARGET, (uint32_t)OPENLOOP_VELOCITY);
if (ret != 0) {
LOG_ERR("Failed to set PID_VELOCITY_TARGET: %d", ret);
return ret;
}
/* Let the motor run */
k_msleep(RUN_TIME_MS);
ret = tmc6460_read(dev, TMC6460_RAMPER_V_ACTUAL, &val);
if (ret == 0) {
LOG_INF("RAMPER_V_ACTUAL = %d", (int32_t)val);
}
/* Stop motor */
LOG_INF("Stopping motor...");
ret = tmc6460_write(dev, TMC6460_FOC_PID_VELOCITY_TARGET, 0U);
if (ret != 0) {
LOG_ERR("Failed to clear VELOCITY: %d", ret);
return ret;
}
ret = tmc6460_write(dev, TMC6460_EXT_CTRL_VOLTAGE, 0U);
if (ret != 0) {
LOG_ERR("Failed to clear VOLTAGE: %d", ret);
return ret;
}
k_msleep(1000);
/* Turn system off */
LOG_INF("Turning system off...");
ret = tmc6460_field_write(dev, FIELD_MOTION_MODE, TMC6460_MOTION_MODE_PWM_OFF);
if (ret != 0) {
LOG_ERR("Failed to set PWM_OFF: %d", ret);
return ret;
}
ret = tmc6460_disable(dev);
if (ret != 0) {
LOG_ERR("Failed to disable gate driver: %d", ret);
return ret;
}
LOG_INF("Done.");
return 0;
}