Embedded HandbookApps
All sections › VIII. Electronics around the microcontroller
STM32F103, STM32F407 · level intermediate · 29 min read
Open in the interactive handbook

57е. Stepper motor winding current: the PWM chopper and decay modes

In 57а.3 microstepping came down to a table: at each step winding A needs the current cos, winding B needs sin. But the table tells you what current is needed, not how to get it. A winding is an inductance with a resistance (57.5), and while the motor turns, a back-EMF is also induced in it. The driver holds the current like this: it turns the bridge on, waits until the current rises to the threshold, turns it off for an off-time, turns it on again. That is the chopper. The most interesting part is what to do with the current during the off-time: let it decay slowly through the shorted winding, dump it quickly with a reverse voltage, or mix one with the other.

In this section: the three bridge states and two ways to hold the current (constant off-time and constant frequency); why slow decay cannot hold small currents and why it spoils the sine while the motor turns; your own chopper on TIM1/TIM8 with an external comparator, a DAC for the threshold and an ADC for decay tuning; what of all this you can see in ready-made drivers.

References: RM0090 Rev 19 (TIM1/TIM8 on the F4: clearing OCxREF by ETR — 17.3.13, the TIMx_SMCR register with reset mode and the ETF filter, the OCxM modes; ADC — triggering the injected group, table 69, ADC_JSQR), RM0008 Rev 17 (F1: JEXTSEL, the ADC2_ETRGINJ_REMAP bit, the ETF filter), DS8626 Rev 9 (F407 pins), DocID14611 Rev 12 and DocID13587 Rev 17 (pins and contents of the F103xE and F103x8/xB), RM0440 Rev 6 (G4: the ETR source is a comparator). The winding model is our own; its parameters are an example, not the datasheet of a particular motor: R = 1.5 Ω, L = 3 mH, a 24 V bus, phase back-EMF amplitude 0.25 V per rad/s of the shaft, 50 electrical revolutions per mechanical one (1.8° per step), full current 1.5 A. The run is _verify/chop57e/sim57e.c, independently — ref57e.py, the record is chop57e_host.txt. For the TMC drivers — the sources of Klipper (github.com/Klipper3d/klipper, commit 7bc4d09), license GPL-3.0 (file COPYING, © Kevin O'Connor).

Terms used in the documentation

Term Meaning
chopper, PWM current regulation holding the current by switching the transistors
fixed (constant) off-time the off-time has a fixed length
fixed frequency, peak current mode a fixed period, regulation by the current peak
slow / fast / mixed decay ways to let the current fall during the off-time
blanking time the comparator is ignored for a while after turn-on
recirculation, synchronous rectification current flow during the off-time; conducting transistors instead of diodes
sense resistor current-measuring shunt
back-EMF voltage induced in the winding by the rotating rotor
reset mode (SMS = 100), ETRF counter reset mode, the filtered ETR input
OCxREF clear, OCxCE the ETR input forces the channel inactive
TOFF, TBL, HSTRT, HEND (TMC) off-time, blanking, hysteresis start and end
spreadCycle, stealthChop chopper modes of TMC drivers

57е.1 The three bridge states and two ways to hold the current

The winding sits in an H-bridge: leg 1 and leg 2, each with a high-side and a low-side switch. Let the current flow from leg 1 to leg 2. Then the bridge has three useful states:

State Switches Voltage across the winding Current through the ground shunt
drive top of leg 1, bottom of leg 2 +24 V the whole winding current
slow decay both bottoms 0 (short) zero: the current circulates through the two low-side switches
fast decay bottom of leg 1, top of leg 2 −24 V the winding current, but with the opposite sign

The comparator compares the shunt voltage with a threshold: once it is exceeded, drive is turned off. Model numbers at a current of 1.5 A:

How long to hold the off-time is decided in different ways. Constant off-time: after the comparator trips, the bridge stays in decay for exactly 20 µs, then drives again; the frequency "floats". Constant frequency: every 50 µs (20 kHz) drive starts anew, and a trip only cuts it short until the end of the period.

After turn-on there is a spike on the shunt from recharging the switch capacitances, so the comparator is ignored for a while at first — this is blanking. In our chopper its role is played by the timer's ETR input filter: 8 equal samples every 32 cycles at 168 MHz — 1.524 µs (57е.5). While blanking lasts, the current rises another 11.05 mA above the threshold.

Current of a 1.5 Ω / 3 mH winding under a chopper with a constant off-time of 20 µs: top — standing still at 1.5 A, over 100 µs slow decay gives a 15.2 mA sawtooth at 45.3 kHz, mixed 55.0 mA at 36.3 kHz, fast 174.6 mA at 22.8 kHz; bottom — rotation at 2 rev/s, average current at each of the 64 microsteps: slow decay drives the current up to 2.34 A where zero is commanded, mixed decay and ADC tuning follow the command


57е.2 Standing still: ripple, frequency and average current

The motor stands still, the winding is at step 0 of the table (full current, DAC threshold 1862 — 1.5001 A) or at step 15 (code 25 of 255 — 0.1474 A). A chopper with a constant off-time of 20 µs and one with a constant frequency of 20 kHz:

Mode 1.5 A: average error peak-to-peak frequency 0.147 A: average error peak-to-peak
off-time, slow 1.5030 A +2.84 mA 15.17 mA 45.30 kHz 1.1327 A +985.28 mA 11.33 mA
off-time, mixed ¼ 1.4722 A −27.99 mA 55.00 mA 36.32 kHz 0.1263 A −21.15 mA 41.51 mA
off-time, fast 1.4228 A −77.39 mA 174.55 mA 22.78 kHz 0.0780 A −69.48 mA 160.77 mA
off-time, ADC tuning 1.4941 A −6.09 mA 25.13 mA 42.66 kHz 0.1366 A −10.86 mA 26.53 mA
20 kHz, slow 1.4933 A −6.81 mA 34.01 mA 20.00 kHz 0.4724 A +324.94 mA 11.62 mA
20 kHz, fast 1.3127 A −187.49 mA 380.06 mA 20.00 kHz 0.0588 A −88.68 mA 205.02 mA

What you can see.


57е.3 The "floor" of slow decay

Blanking is the minimum drive time: even if the current is already above the threshold, the comparator trips no earlier than after 1.524 µs. So the bridge cannot give a duty cycle below t / (t + t_off). The average current with slow decay does not go below

I_floor = V_bus · t / (R · (t + t_off)) — at 24 V, 1.5 Ω, t = 1.524 µs:

Off-time Model Formula
20 µs 1.1327 A 1.1327 A
40 µs 0.5872 A 0.5872 A
80 µs 0.2991 A 0.2991 A

The inductance does not enter the formula. With an off-time of 20 µs slow decay holds table steps 0…7 (the last one — 1.1585 A commanded, 1.1633 A in the model), while from step 8 (1.0586 A) and below the current sits at 1.1327 A. That is why pure slow decay is used only with a high-voltage bus and a large current — or the off-time is made longer (the frequency drops into the audible range), or fast decay is added. Mixed ¼ at 0.147 A is off by −21.15 mA, tuning by −10.86 mA.


57е.4 Rotation: back-EMF spoils the sine

The rotor turns evenly together with the field, with no load; the back-EMF of phase A is −E·sin θ, E = 0.785 V at 0.5 rev/s, 1.571 V at 1, 3.142 V at 2 and 6.283 V at 4 rev/s. Microstepping 1/16: 64 steps per electrical revolution, 50 of them per mechanical one. We measure the average current at each step against the commanded one, and the RMS error over a revolution:

Mode 0.5 rev/s 1 rev/s 2 rev/s 4 rev/s Peak-to-peak at 2 rev/s
off-time, slow 555.9 mA 723.7 mA 952.3 mA 1297.7 mA 21.7 mA
off-time, fast 82.8 mA 81.0 mA 84.7 mA 129.4 mA 181.1 mA
off-time, mixed ¼ 40.6 mA 46.5 mA 117.6 mA 524.4 mA 60.7 mA
off-time, tuning 35.0 mA 39.5 mA 65.6 mA 122.5 mA 61.9 mA
20 kHz, slow 285.6 mA 464.6 mA 726.1 mA 1091.0 mA 63.0 mA

Why slow decay fails. In the first quarter of a revolution the commanded current is positive and decreasing, while the back-EMF −E·sin θ is negative. In slow decay the winding sees L·di/dt = −R·i − e = −R·i + E·sin θ: the EMF pushes the current up. The shorted winding works as a generator, and the current grows where it should fall: at step 16 (zero commanded) at 2 rev/s 2.342 A flows (see the figure). The fundamental of the current is 2.0780 A instead of 1.5 and lags by 32.96°. In the second quarter, where the current rises, the EMF helps — there slow decay is good.

Fast decay always dumps the current, but pays with a 181.1 mA sawtooth and an average below the command. Mixed ¼ is good up to 1 rev/s; at 4 rev/s a quarter of the off-time is not enough (524.4 mA). Tuning increases the fast part by itself when needed: on average 14.0 % of the off-time at 0.5 rev/s, 17.8 % at 1, 29.0 % at 2 and 65.1 % at 4 rev/s; the fundamental at 2 rev/s is 1.4776 A with a lag of 4.16°.


57е.5 A chopper on TIM1: the comparator resets the counter

An STM32 timer makes a constant off-time on its own. The comparator output goes to the ETR input. In TIMx_SMCR reset mode is selected (SMS = 100: "a rising edge of TRGI reinitializes the counter and updates the registers"), the source TS = 111 — ETRF, the filter ETF = 1111 (fDTS/32, N = 8). Each trip clears the counter, and channels 1 and 2 in PWM modes split the count into the off-time and drive segments:

Counter The "to" leg (PWM 2, CCR = n_off) The "from" leg (PWM 1, CCR = n_fast) State
CNT < n_fast bottom top fast decay
n_fast ≤ CNT < n_off bottom bottom slow decay
CNT ≥ n_off top bottom drive

When counting up, PWM 1 is active while CNT < CCR, PWM 2 is the opposite; the active level of a channel is the high-side switch of the leg, and the complementary output CHxN with dead time is the low-side one. n_fast = 0 means pure slow decay, n_fast = n_off pure fast decay. The current sign is changed by swapping the channel modes. For the F407 with timers at 168 MHz n_off = 3360 (20 µs), mixed ¼ is n_fast = 840. The code that computes the modes and CCR is shared by the firmware and the model:

void chop_legs(chop_out_t *o, int8_t sign, uint16_t n_off, uint16_t n_fast)
{
    if (n_fast > n_off)
        n_fast = n_off;
    o->sign = sign;
    if (sign > 0) {                    /* ток из плеча 1 в плечо 2 */
        o->ocm1 = OCM_PWM2; o->ccr1 = n_off;
        o->ocm2 = OCM_PWM1; o->ccr2 = n_fast;
    } else if (sign < 0) {             /* ток из плеча 2 в плечо 1 */
        o->ocm1 = OCM_PWM1; o->ccr1 = n_fast;
        o->ocm2 = OCM_PWM2; o->ccr2 = n_off;
    } else {                           /* нулевой ток: оба нижних ключа, обмотка закорочена */
        o->ocm1 = OCM_FORCE_LOW; o->ccr1 = 0;
        o->ocm2 = OCM_FORCE_LOW; o->ccr2 = 0;
    }
}

The bridge of one winding and writing the result into the timer:

static void tim_apply(tim_t *t, const chop_out_t *o, int restart)
{
    t->CCR1  = o->ccr1;
    t->CCR2  = o->ccr2;
    t->CCMR1 = ((uint32_t)o->ocm1 << TIM_CCMR1_OC1M_Pos) | TIM_CCMR1_OC1PE
             | ((uint32_t)o->ocm2 << TIM_CCMR1_OC2M_Pos) | TIM_CCMR1_OC2PE;
    if (restart)
        t->EGR = TIM_EGR_UG;
}

/* Мост одной обмотки: счётчик вверх, сброс по фронту ETR, канал 4 — запуск АЦП. */
static void tim_bridge_init(tim_t *t)
{
    t->CR1   = 0;
    t->PSC   = 0;
    t->ARR   = 0xFFFFu;                   /* не сработал компаратор — пауза через 65536 тактов */
    t->SMCR  = (ETF_CODE << TIM_SMCR_ETF_Pos) | TIM_SMCR_TS_ETRF | TIM_SMCR_SMS_RESET;
    t->CCMR1 = (OCM_FORCE_LOW << TIM_CCMR1_OC1M_Pos) | (OCM_FORCE_LOW << TIM_CCMR1_OC2M_Pos);
    t->CCMR2 = (OCM_PWM2 << TIM_CCMR2_OC4M_Pos) | TIM_CCMR2_OC4PE;   /* OC4REF встаёт при CNT = CCR4 */
    t->CCR4  = N_OFF + N_ADC;
    t->CCER  = TIM_CCER_CC1E | TIM_CCER_CC1NE | TIM_CCER_CC2E | TIM_CCER_CC2NE
             | TIM_CCER_CC4E;                      /* канал 4 на вывод не выведен */
    t->BDTR  = N_DT | TIM_BDTR_MOE;
    t->EGR   = TIM_EGR_UG;
    t->CR1   = TIM_CR1_CEN;
}

Two subtleties from RM0090. First: PWM without CCR preload (OCxPE = 0) is allowed by the document only in one-pulse mode — so OCxPE is on, and new CCR values take effect at the next counter reset (which is also the update event). Second: in PWM modes the OCxREF level changes only when the comparison result changes. Switching from "forced inactive" to PWM when the current sign changes will not turn the output on by itself — so tim_apply with restart issues UG: the counter goes to zero, and a new off-time starts from the beginning. The model does the same. ARR = 65535 is a fallback: if the comparator has not tripped within 390 µs, the counter overflows and starts the off-time by itself.

Constant frequency is done by another TIM1 feature — channel clearing (OCxCE): a high level on ETRF holds OCxREF at zero until the next update event; with CCR > ARR (100 % PWM) the channel turns on again at overflow:

void tim_fixed_slow_init(tim_t *t)
{
    t->CR1   = 0;
    t->ARR   = TIM_HZ / 20000u - 1u;
    t->SMCR  = ETF_CODE << TIM_SMCR_ETF_Pos;      /* ETR только для гашения, режима сброса нет */
    t->CCR1  = TIM_HZ / 20000u;
    t->CCMR1 = (OCM_PWM1 << TIM_CCMR1_OC1M_Pos) | TIM_CCMR1_OC1PE | TIM_CCMR1_OC1CE
             | (OCM_FORCE_LOW << TIM_CCMR1_OC2M_Pos);
    t->CCER  = TIM_CCER_CC1E | TIM_CCER_CC1NE | TIM_CCER_CC2E | TIM_CCER_CC2NE;
    t->BDTR  = N_DT | TIM_BDTR_MOE;
    t->EGR   = TIM_EGR_UG;
    t->CR1   = TIM_CR1_CEN;
}

Only slow decay here: leg 2 stays on the low-side switch all the time. Mixed decay cannot be done this way — clearing holds until the end of the period, and there is nothing to measure "the first 5 µs" in it with.

Pins (winding A — TIM1, winding B — TIM8); according to DS8626 and DocID14611 they are the same on the F407 and the F103xE:

Signal TIM1 TIM8 F407, alternate function
CH1 / CH2 PA8 / PA9 PC6 / PC7 AF1 / AF3
CH1N / CH2N PB13 / PB14 PA7 / PB0 AF1 / AF3
ETR (comparator output) PA12 PA0 AF1 / AF3
shunt → ADC, threshold ← DAC PA1 (ADC1_IN1), PA4 (DAC1) PA2 (ADC2_IN2), PA5 (DAC2) analog input

On the F103 the timers run at 72 MHz: n_off = 1440, filter ETF = 1100 (fDTS/16, N = 8 — 128 cycles, 1.78 µs), dead time 36 cycles. A second timer with complementary outputs and a DAC exist only on the F103xE (high-density): on the F103x8/xB, according to DocID13587, there is only one timer of this kind and no DAC — there you need a different circuit for the threshold and for the second winding.

On the G4 no external comparator is needed: according to RM0440 the timer's ETR source is selected by the ETRSEL field in TIMx_AF1, and tim_etr1 of TIM1 is the COMP1 output; the threshold comes from the internal DAC, and blanking is done by the comparator itself from a timer channel (as in 57б.6 and 44.7). This variant is described but was not built.


57е.6 Steps, the DAC and decay tuning by the ADC

The table and the threshold live in the shared file chop57e.c. Step idx (0…63) gives the magnitude and sign of the current in winding A; winding B is the same call with idx + 48. The field strength √(A² + B²) over the 64 steps is from 0.998 to 1.002 of full, as in 57а.3.

int8_t ustep_level(uint8_t idx, uint8_t *level)
{
    idx &= 63u;
    uint8_t q = idx >> 4, r = idx & 15u;
    uint8_t v = (q == 0u || q == 2u) ? ustep_cos[r] : ustep_cos[16u - r];
    *level = v;
    if (v == 0u)
        return 0;
    return (q == 0u || q == 3u) ? 1 : -1;
}
void chop_set(chop_out_t *o, const chop_cfg_t *c, uint8_t idx)
{
    uint8_t level;
    int8_t sign = ustep_level(idx, &level);
    uint16_t n_fast = c->mode == DECAY_SLOW ? 0u : c->mode == DECAY_FAST ? c->n_off : c->n_fast;
    o->dac = (uint16_t)(((uint32_t)level * c->dac_full + 127u) / 255u);
    chop_legs(o, sign, c->n_off, n_fast);
}

The shunt amplifier gives 1 V per ampere (for example, 0.1 Ω and a gain of 10), the DAC and the ADC run at 3.3 V — one scale, 0.806 mA per code; full current is code 1862. What gets written into the timer with n_off = 3360, n_fast = 840:

Step Code of 255 Sign DAC OC1M / CCR1 OC2M / CCR2
0 255 + 1862 7 (PWM 2) / 3360 6 (PWM 1) / 840
8 180 + 1314 7 / 3360 6 / 840
15 25 + 183 7 / 3360 6 / 840
16 0 0 0 4 (inactive) / 0 4 / 0
17 25 − 183 6 / 840 7 / 3360
32 255 − 1862 6 / 840 7 / 3360

Tuning. Channel 4 (CCR4 = n_off + 168, 1 µs into drive) triggers an injected conversion: ADC1 on TIM1_CH4 (JEXTSEL 0000), ADC2 on TIM8_CH4 (1110). At this moment the shunt sees the current with which the winding came out of the off-time. If it is not below the threshold, the decay did not keep up, and the fast part grows by 1/8 of the off-time; if it is below, the fast part shrinks by 1/32. Eight trips in a row give 420, 840, … 3360 (all fast), then 3255, 3150, 3045.

uint16_t chop_adapt(uint16_t n_fast, uint16_t valley, uint16_t thr, uint16_t n_off)
{
    uint32_t up = n_off / 8u, down = n_off / 32u, n = n_fast;
    if (valley >= thr)
        n = n + up > n_off ? n_off : n + up;
    else
        n = n > down ? n - down : 0u;
    return (uint16_t)n;
}

The sample must finish before the ETR filter trips (1.33…1.52 µs after the start of drive, depending on the phase of the filter samples), otherwise the bridge goes into the off-time in the middle of the sample and the shunt shows zero. So the sampling time is the shortest: 3 ADC cycles at 21 MHz on the F407.

static void adc_init(void)
{
    RCC_APB2ENR |= RCC_ADC1EN | RCC_ADC2EN;
    ADC_CCR = 1u << 16;                                    /* ADCPRE = 01: 84 МГц / 4 = 21 МГц */
    ADC1->SMPR2 = 0;                       /* 3 такта АЦП: выборка кончится раньше, чем сработает фильтр ETR */
    ADC2->SMPR2 = 0;
    ADC1->JSQR = 1u << ADC_JSQR_JSQ4_Pos;                  /* JL = 0: один канал, PA1 */
    ADC2->JSQR = 2u << ADC_JSQR_JSQ4_Pos;                  /* PA2 */
    ADC1->CR1 = ADC_CR1_JEOCIE;
    ADC2->CR1 = ADC_CR1_JEOCIE;
    ADC1->CR2 = (1u << 20) | (0u << 16) | ADC_CR2_ADON;    /* по фронту, JEXTSEL 0000 — TIM1_CH4 */
    ADC2->CR2 = (1u << 20) | (14u << 16) | ADC_CR2_ADON;   /* по фронту, JEXTSEL 1110 — TIM8_CH4 */
}

With JL = 0 one channel is converted, and it is taken from JSQ4, not from JSQ1 — that is how RM0090 and RM0008 put it. On the F103 the same is done via JEXTTRIG/JEXTSEL in bits 15:12: ADC1 — 001 (TIM1_CC4), ADC2 — 110 plus the ADC2_ETRGINJ_REMAP bit in AFIO_MAPR, otherwise 110 means EXTI15; and before use, the CAL calibration.

The step, the ADC interrupt and startup:

void stepper_step(int8_t dir)
{
    int8_t s0 = out[0].sign, s1 = out[1].sign;
    step_idx = (uint8_t)((step_idx + dir) & 63u);
    chop_set(&out[0], &cfg[0], step_idx);
    chop_set(&out[1], &cfg[1], (uint8_t)(step_idx + 48u));
    DAC->DHR12R1 = out[0].dac;
    DAC->DHR12R2 = out[1].dac;
    tim_apply(TIM1, &out[0], out[0].sign != s0);
    tim_apply(TIM8, &out[1], out[1].sign != s1);
}

static void adapt(unsigned w, tim_t *t, uint16_t valley)
{
    chop_out_t *o = &out[w];
    if (cfg[w].mode != DECAY_AUTO || o->sign == 0)
        return;
    cfg[w].n_fast = chop_adapt(cfg[w].n_fast, valley, o->dac, cfg[w].n_off);
    chop_legs(o, o->sign, cfg[w].n_off, cfg[w].n_fast);
    tim_apply(t, o, 0);
}
#if defined(CHIP_F407)
void ADC_IRQHandler(void)
#else
void ADC1_2_IRQHandler(void)
#endif
{
    if (ADC1->SR & ADC_SR_JEOC) {
        ADC1->SR = ~ADC_SR_JEOC;
        adapt(0, TIM1, (uint16_t)ADC1->JDR1);
    }
    if (ADC2->SR & ADC_SR_JEOC) {
        ADC2->SR = ~ADC_SR_JEOC;
        adapt(1, TIM8, (uint16_t)ADC2->JDR1);
    }
}

void chopper_init(void)
{
    pins_init();
    RCC_APB1ENR |= RCC_DACEN;
    RCC_APB2ENR |= RCC_TIM1EN | RCC_TIM8EN;
    DAC->CR = DAC_CR_EN1 | DAC_CR_EN2;
    adc_init();
    step_idx = 0;
    stepper_step(0);                                       /* шаг 0: A — полный ток, B — ноль */
    tim_bridge_init(TIM1);
    tim_bridge_init(TIM8);
    tim_apply(TIM1, &out[0], 1);
    tim_apply(TIM8, &out[1], 1);
    NVIC_ISER0 = 1u << ADC_IRQ_NUM;
}

The ADC interrupt number is 18 on both (ADC_IRQn on the F407, ADC1_2_IRQn on the F103). It fires on every chopper cycle — standing still at 1.5 A that is 45.3 thousand times a second per winding; how much this costs the processor was not measured.

Everything is built with arm-none-eabi-gcc 12.2.1 with -Os -Wall -Wextra -Werror: fw57e.c is 928 bytes of code for the F407 (Cortex-M4) and 940 for the F103 (Cortex-M3), chop57e.c is 241 bytes on both; of these chopper_init takes 544/556, stepper_step 124, adapt 100, the ADC handler 76.

Ready-made drivers. In chips such as the A4988 and DRV8825 the same parts — a comparator with a threshold from the reference voltage input, the off-time, blanking and the choice of decay — are built in; the pins and the current formulas are taken from the datasheet of the particular chip, and this section was not checked against them. On the TMC2208/TMC2209 these are fields of the CHOPCONF register; their layout and defaults can be seen in Klipper (tmc2208.py, tmc2209.py): toff — bits 3:0, hstrt 6:4, hend 10:7, tbl 16:15, vsense — bit 17; Klipper writes toff = 3, hstrt = 5, hend = 0, tbl = 2 (on the TMC2130 — 4, 0, 7, 1). The off-time and blanking are the same two quantities as in our model; the Trinamic datasheet converts them into microseconds. Klipper computes the current in tmc2130.py like this: I = (CS + 1)·Vfs / (32·(Rsh + 0.020)·√2) (RMS value), Vfs = 0.32 V, or 0.18 V at vsense = 1; the default shunt is 0.110 Ω. For 0.8 A this gives vsense = 1, IRUN = 25 and 0.7955 A; for 1.2 A the sensitive scale is not enough (its limit is 0.9791 A), so the result is vsense = 0, IRUN = 21, 1.1966 A. By default Klipper uses spreadCycle mode; stealthChop is enabled with stealthchop_threshold. The Klipper documentation says that spreadCycle gives more torque and accuracy, while stealthChop is noticeably quieter, but in tests it lagged in position by about 75 % of a full step.


57е.7 Pitfalls

A small current is not held — the motor heats up while holding. The "floor" of slow decay: V_bus·t/(R·(t + t_off)). At 24 V, 1.5 Ω, blanking of 1.524 µs and an off-time of 20 µs — 1.1327 A instead of the commanded 0.147 A. You need mixed decay or a longer off-time.

When accelerating, the motor loses torque and is noisy, although it is fine standing still. Slow decay in the decreasing quarter does not dump the current against the back-EMF: at 2 rev/s the current is 2.342 A where zero is commanded, RMS error 952.3 mA. Use mixed or tuned decay.

Constant frequency plus fast decay on conducting switches — a current without limit. The current crossed zero during the off-time, the shunt sees it as drive current, the comparator keeps clearing — and the bridge drives the current in the opposite direction. In the model at 0.5 rev/s up to 15.986 A, at 2 rev/s up to 13.627 A. You need a zero detector or decay through diodes, and above all a fault input (57а.6).

Tuning does not work — the ADC shows zero. The sample is longer than the time until the ETR filter trips: the bridge went into the off-time in the middle of the sample. The trigger comes 1 µs in, the earliest trip is at 1.33 µs: less than a third of a microsecond is left for the sample.

The current sign changed — the winding stays silent for up to 390 µs. The channel was switched from "forced inactive" to PWM without UG: the OCxREF level waits for the comparison result to change, that is, for a counter overflow.

The second winding "trips" on its own. ADC2 on the F103 with JEXTSEL = 110 without ADC2_ETRGINJ_REMAP is triggered by EXTI15, not by TIM8.

The chopper whines. Constant off-time means variable frequency: when turning with fast decay, down to 10.36 kHz in the model. Either a constant frequency, or a shorter off-time.


57е.8 Summary

Full sources

chop57e.c — the chopper logic: the microstep table, bridge modes, the DAC threshold, decay tuning.

/* Раздел 57е: логика чоппера — таблица микрошага, режимы каналов моста, порог ЦАП, подстройка спада. */
#include "chop57e.h"

/* Четверть косинуса для микрошага 1/16: 255·cos(k·π/32), k = 0…16 — та же таблица, что в 57а.3. */
const uint8_t ustep_cos[17] = {
    255, 254, 250, 244, 236, 225, 212, 197, 180, 162, 142, 120, 98, 74, 50, 25, 0
};

/* Ток обмотки A на шаге idx (0…63 — электрический оборот): модуль 0…255 и знак.
 * Обмотка B — тот же вызов с idx + 48 (синус = косинус, сдвинутый на четверть оборота). */
int8_t ustep_level(uint8_t idx, uint8_t *level)
{
    idx &= 63u;
    uint8_t q = idx >> 4, r = idx & 15u;
    uint8_t v = (q == 0u || q == 2u) ? ustep_cos[r] : ustep_cos[16u - r];
    *level = v;
    if (v == 0u)
        return 0;
    return (q == 0u || q == 3u) ? 1 : -1;
}

/* Плечи моста по знаку тока. Счётчик сбрасывается в 0 срабатыванием компаратора:
 *   CNT < n_fast           — быстрый спад: плечо «от» на верхнем ключе, плечо «к» на нижнем (−Vпит);
 *   n_fast <= CNT < n_off  — медленный спад: оба нижних ключа (обмотка закорочена);
 *   CNT >= n_off           — разгон: плечо «к» на верхнем ключе, плечо «от» на нижнем (+Vпит).
 * ШИМ 2 активен при CNT >= CCR, ШИМ 1 — при CNT < CCR; активный выход = верхний ключ плеча. */
void chop_legs(chop_out_t *o, int8_t sign, uint16_t n_off, uint16_t n_fast)
{
    if (n_fast > n_off)
        n_fast = n_off;
    o->sign = sign;
    if (sign > 0) {                    /* ток из плеча 1 в плечо 2 */
        o->ocm1 = OCM_PWM2; o->ccr1 = n_off;
        o->ocm2 = OCM_PWM1; o->ccr2 = n_fast;
    } else if (sign < 0) {             /* ток из плеча 2 в плечо 1 */
        o->ocm1 = OCM_PWM1; o->ccr1 = n_fast;
        o->ocm2 = OCM_PWM2; o->ccr2 = n_off;
    } else {                           /* нулевой ток: оба нижних ключа, обмотка закорочена */
        o->ocm1 = OCM_FORCE_LOW; o->ccr1 = 0;
        o->ocm2 = OCM_FORCE_LOW; o->ccr2 = 0;
    }
}

/* Всё для шага idx: порог ЦАП по таблице и режимы каналов по знаку. */
void chop_set(chop_out_t *o, const chop_cfg_t *c, uint8_t idx)
{
    uint8_t level;
    int8_t sign = ustep_level(idx, &level);
    uint16_t n_fast = c->mode == DECAY_SLOW ? 0u : c->mode == DECAY_FAST ? c->n_off : c->n_fast;
    o->dac = (uint16_t)(((uint32_t)level * c->dac_full + 127u) / 255u);
    chop_legs(o, sign, c->n_off, n_fast);
}

/* Подстройка спада по отсчёту АЦП в начале разгона (valley) против порога (thr), коды одной шкалы.
 * Ток к концу паузы не опустился ниже порога — спад не успевает: быстрая часть растёт на 1/8 паузы.
 * Опустился — быстрая часть убывает на 1/32 паузы: медленный спад даёт меньше пульсаций. */
uint16_t chop_adapt(uint16_t n_fast, uint16_t valley, uint16_t thr, uint16_t n_off)
{
    uint32_t up = n_off / 8u, down = n_off / 32u, n = n_fast;
    if (valley >= thr)
        n = n + up > n_off ? n_off : n + up;
    else
        n = n > down ? n - down : 0u;
    return (uint16_t)n;
}

fw57e.c — a current chopper for two windings on TIM1 and TIM8.

/* Раздел 57е: чоппер тока двух обмоток шагового двигателя на STM32F407 (или STM32F103xE).
 * Обмотка A — мост на TIM1 (CH1/CH1N — плечо 1, CH2/CH2N — плечо 2), обмотка B — тот же мост на TIM8.
 * Внешний компаратор на обмотку: «+» — усиленный шунт общего провода моста (1 В/А), «−» — выход ЦАП;
 * выход компаратора — на вход ETR своего таймера. Срабатывание сбрасывает счётчик (режим сброса):
 * начинается пауза N_OFF тактов, в её начале N_FAST тактов быстрого спада, дальше медленный.
 * Через 1 мкс разгона канал 4 запускает АЦП: ток в начале разгона подстраивает долю быстрого спада.
 * Такты настроены заранее (раздел 6): таймеры — 168 МГц у F407, 72 МГц у F103. */
#include "regs57e.h"
#include "chop57e.h"

#if defined(CHIP_F407)
#define TIM_HZ   168000000u
#define ETF_CODE 15u                      /* 1111: fDTS/32, N = 8 — 256 тактов, 1,52 мкс */
#elif defined(CHIP_F103)
#define TIM_HZ   72000000u
#define ETF_CODE 12u                      /* 1100: fDTS/16, N = 8 — 128 тактов, 1,78 мкс */
#endif
#define N_OFF    (TIM_HZ / 50000u)        /* пауза 20 мкс */
#define N_ADC    (TIM_HZ / 1000000u)      /* выборка АЦП через 1 мкс разгона */
#define N_DT     (TIM_HZ / 2000000u)      /* защитный интервал 0,5 мкс */
#define DAC_FULL 1862u                    /* 1,5 А: 1,5 В при 3,3 В на 4096 кодов */

static chop_cfg_t cfg[2] = {
    { N_OFF, 0, DAC_FULL, DECAY_AUTO },
    { N_OFF, 0, DAC_FULL, DECAY_AUTO },
};
static chop_out_t out[2];
static uint8_t step_idx;

/* CCR с предзагрузкой: новые значения вступают при следующем сбросе счётчика (он же событие
 * обновления). Режим канала (OCxM) меняется сразу, но уровень ШИМ пересчитывается только при смене
 * результата сравнения — поэтому при смене знака тока счётчик сбрасывается вручную (UG). */
static void tim_apply(tim_t *t, const chop_out_t *o, int restart)
{
    t->CCR1  = o->ccr1;
    t->CCR2  = o->ccr2;
    t->CCMR1 = ((uint32_t)o->ocm1 << TIM_CCMR1_OC1M_Pos) | TIM_CCMR1_OC1PE
             | ((uint32_t)o->ocm2 << TIM_CCMR1_OC2M_Pos) | TIM_CCMR1_OC2PE;
    if (restart)
        t->EGR = TIM_EGR_UG;
}

/* Мост одной обмотки: счётчик вверх, сброс по фронту ETR, канал 4 — запуск АЦП. */
static void tim_bridge_init(tim_t *t)
{
    t->CR1   = 0;
    t->PSC   = 0;
    t->ARR   = 0xFFFFu;                   /* не сработал компаратор — пауза через 65536 тактов */
    t->SMCR  = (ETF_CODE << TIM_SMCR_ETF_Pos) | TIM_SMCR_TS_ETRF | TIM_SMCR_SMS_RESET;
    t->CCMR1 = (OCM_FORCE_LOW << TIM_CCMR1_OC1M_Pos) | (OCM_FORCE_LOW << TIM_CCMR1_OC2M_Pos);
    t->CCMR2 = (OCM_PWM2 << TIM_CCMR2_OC4M_Pos) | TIM_CCMR2_OC4PE;   /* OC4REF встаёт при CNT = CCR4 */
    t->CCR4  = N_OFF + N_ADC;
    t->CCER  = TIM_CCER_CC1E | TIM_CCER_CC1NE | TIM_CCER_CC2E | TIM_CCER_CC2NE
             | TIM_CCER_CC4E;                      /* канал 4 на вывод не выведен */
    t->BDTR  = N_DT | TIM_BDTR_MOE;
    t->EGR   = TIM_EGR_UG;
    t->CR1   = TIM_CR1_CEN;
}

/* Для сравнения: постоянная частота 20 кГц, медленный спад. Канал 1 — ШИМ 100 % (CCR1 > ARR),
 * высокий уровень ETR гасит его до следующего переполнения; плечо 2 всё время на нижнем ключе. */
void tim_fixed_slow_init(tim_t *t)
{
    t->CR1   = 0;
    t->ARR   = TIM_HZ / 20000u - 1u;
    t->SMCR  = ETF_CODE << TIM_SMCR_ETF_Pos;      /* ETR только для гашения, режима сброса нет */
    t->CCR1  = TIM_HZ / 20000u;
    t->CCMR1 = (OCM_PWM1 << TIM_CCMR1_OC1M_Pos) | TIM_CCMR1_OC1PE | TIM_CCMR1_OC1CE
             | (OCM_FORCE_LOW << TIM_CCMR1_OC2M_Pos);
    t->CCER  = TIM_CCER_CC1E | TIM_CCER_CC1NE | TIM_CCER_CC2E | TIM_CCER_CC2NE;
    t->BDTR  = N_DT | TIM_BDTR_MOE;
    t->EGR   = TIM_EGR_UG;
    t->CR1   = TIM_CR1_CEN;
}

#if defined(CHIP_F407)
static void pin_af(gpio_t *g, unsigned pin, unsigned af)
{
    g->MODER = (g->MODER & ~(3u << 2 * pin)) | (2u << 2 * pin);
    g->AFR[pin >> 3] = (g->AFR[pin >> 3] & ~(15u << 4 * (pin & 7u))) | (af << 4 * (pin & 7u));
}

static void pins_init(void)
{
    RCC_AHB1ENR |= 7u;                                     /* GPIOA, GPIOB, GPIOC */
    pin_af(GPIOA, 8, 1);  pin_af(GPIOA, 9, 1);             /* TIM1_CH1, TIM1_CH2 */
    pin_af(GPIOB, 13, 1); pin_af(GPIOB, 14, 1);            /* TIM1_CH1N, TIM1_CH2N */
    pin_af(GPIOA, 12, 1);                                  /* TIM1_ETR — компаратор обмотки A */
    pin_af(GPIOC, 6, 3);  pin_af(GPIOC, 7, 3);             /* TIM8_CH1, TIM8_CH2 */
    pin_af(GPIOA, 7, 3);  pin_af(GPIOB, 0, 3);             /* TIM8_CH1N, TIM8_CH2N */
    pin_af(GPIOA, 0, 3);                                   /* TIM8_ETR — компаратор обмотки B */
    GPIOA->MODER |= (3u << 2 * 1) | (3u << 2 * 2) | (3u << 2 * 4) | (3u << 2 * 5); /* PA1, PA2 — АЦП; PA4, PA5 — ЦАП */
}

static void adc_init(void)
{
    RCC_APB2ENR |= RCC_ADC1EN | RCC_ADC2EN;
    ADC_CCR = 1u << 16;                                    /* ADCPRE = 01: 84 МГц / 4 = 21 МГц */
    ADC1->SMPR2 = 0;                       /* 3 такта АЦП: выборка кончится раньше, чем сработает фильтр ETR */
    ADC2->SMPR2 = 0;
    ADC1->JSQR = 1u << ADC_JSQR_JSQ4_Pos;                  /* JL = 0: один канал, PA1 */
    ADC2->JSQR = 2u << ADC_JSQR_JSQ4_Pos;                  /* PA2 */
    ADC1->CR1 = ADC_CR1_JEOCIE;
    ADC2->CR1 = ADC_CR1_JEOCIE;
    ADC1->CR2 = (1u << 20) | (0u << 16) | ADC_CR2_ADON;    /* по фронту, JEXTSEL 0000 — TIM1_CH4 */
    ADC2->CR2 = (1u << 20) | (14u << 16) | ADC_CR2_ADON;   /* по фронту, JEXTSEL 1110 — TIM8_CH4 */
}
#elif defined(CHIP_F103)
static void pin_mode(gpio_t *g, unsigned pin, unsigned mode)
{
    volatile uint32_t *cr = pin < 8u ? &g->CRL : &g->CRH;
    unsigned sh = 4u * (pin & 7u);
    *cr = (*cr & ~(15u << sh)) | (mode << sh);
}

static void pins_init(void)
{
    RCC_APB2ENR |= RCC_AFIOEN | RCC_IOPAEN | RCC_IOPBEN | RCC_IOPCEN;
    pin_mode(GPIOA, 8, 0xB);  pin_mode(GPIOA, 9, 0xB);     /* TIM1_CH1, CH2: AF, 50 МГц */
    pin_mode(GPIOB, 13, 0xB); pin_mode(GPIOB, 14, 0xB);    /* TIM1_CH1N, CH2N */
    pin_mode(GPIOC, 6, 0xB);  pin_mode(GPIOC, 7, 0xB);     /* TIM8_CH1, CH2 */
    pin_mode(GPIOA, 7, 0xB);  pin_mode(GPIOB, 0, 0xB);     /* TIM8_CH1N, CH2N */
    pin_mode(GPIOA, 12, 0x4); pin_mode(GPIOA, 0, 0x4);     /* TIM1_ETR, TIM8_ETR: вход */
    pin_mode(GPIOA, 1, 0x0);  pin_mode(GPIOA, 2, 0x0);     /* АЦП */
    pin_mode(GPIOA, 4, 0x0);  pin_mode(GPIOA, 5, 0x0);     /* ЦАП */
}

static void adc_init(void)
{
    RCC_CFGR = (RCC_CFGR & ~(3u << 14)) | (2u << 14);      /* ADCPRE = 10: 72 МГц / 6 = 12 МГц */
    RCC_APB2ENR |= RCC_ADC1EN | RCC_ADC2EN;
    AFIO_MAPR |= AFIO_ADC2_ETRGINJ_REMAP;
    adc_t *a[2] = { ADC1, ADC2 };
    for (unsigned k = 0; k < 2u; k++) {
        a[k]->CR2 = ADC_CR2_ADON;                          /* включить */
        for (volatile unsigned d = 0; d < 1000u; d++) { }  /* больше 1 мкс на пробуждение */
        a[k]->CR2 = ADC_CR2_ADON | ADC_CR2_CAL;
        while (a[k]->CR2 & ADC_CR2_CAL) { }
        a[k]->SMPR2 = 0;                                   /* 1,5 такта: выборка до срабатывания фильтра */
        a[k]->JSQR = (k + 1u) << ADC_JSQR_JSQ4_Pos;
        a[k]->CR1 = ADC_CR1_JEOCIE;
    }
    ADC1->CR2 = (1u << 15) | (1u << 12) | ADC_CR2_ADON;    /* JEXTTRIG, JEXTSEL 001 — TIM1_CC4 */
    ADC2->CR2 = (1u << 15) | (6u << 12) | ADC_CR2_ADON;    /* JEXTSEL 110 + переназначение — TIM8_CC4 */
}
#endif

/* Шаг микрошага: dir = +1 или −1. Вызывать с выключенным прерыванием АЦП или из прерывания
 * того же приоритета — обработчик АЦП тоже пишет out[] и CCR. */
void stepper_step(int8_t dir)
{
    int8_t s0 = out[0].sign, s1 = out[1].sign;
    step_idx = (uint8_t)((step_idx + dir) & 63u);
    chop_set(&out[0], &cfg[0], step_idx);
    chop_set(&out[1], &cfg[1], (uint8_t)(step_idx + 48u));
    DAC->DHR12R1 = out[0].dac;
    DAC->DHR12R2 = out[1].dac;
    tim_apply(TIM1, &out[0], out[0].sign != s0);
    tim_apply(TIM8, &out[1], out[1].sign != s1);
}

static void adapt(unsigned w, tim_t *t, uint16_t valley)
{
    chop_out_t *o = &out[w];
    if (cfg[w].mode != DECAY_AUTO || o->sign == 0)
        return;
    cfg[w].n_fast = chop_adapt(cfg[w].n_fast, valley, o->dac, cfg[w].n_off);
    chop_legs(o, o->sign, cfg[w].n_off, cfg[w].n_fast);
    tim_apply(t, o, 0);
}

#if defined(CHIP_F407)
void ADC_IRQHandler(void)
#else
void ADC1_2_IRQHandler(void)
#endif
{
    if (ADC1->SR & ADC_SR_JEOC) {
        ADC1->SR = ~ADC_SR_JEOC;
        adapt(0, TIM1, (uint16_t)ADC1->JDR1);
    }
    if (ADC2->SR & ADC_SR_JEOC) {
        ADC2->SR = ~ADC_SR_JEOC;
        adapt(1, TIM8, (uint16_t)ADC2->JDR1);
    }
}

void chopper_init(void)
{
    pins_init();
    RCC_APB1ENR |= RCC_DACEN;
    RCC_APB2ENR |= RCC_TIM1EN | RCC_TIM8EN;
    DAC->CR = DAC_CR_EN1 | DAC_CR_EN2;
    adc_init();
    step_idx = 0;
    stepper_step(0);                                       /* шаг 0: A — полный ток, B — ноль */
    tim_bridge_init(TIM1);
    tim_bridge_init(TIM8);
    tim_apply(TIM1, &out[0], 1);
    tim_apply(TIM8, &out[1], 1);
    NVIC_ISER0 = 1u << ADC_IRQ_NUM;
}

Next: section 57ж "Microstepping without a driver: a gauge stepper motor driven straight from a timer".

Open in the interactive handbook