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

57з. Brushed motor driver: L298N and TB6612FNG — H-bridge, PWM on a timer, braking, encoder and speed PID

In 57а the brushed motor was a set of equations: a starting current of 6 A for a motor that takes 0.03 A at no load, an H-bridge of four switches, a soft start by the duty cycle. In 57б — how to measure its current with a shunt. Here is what sits on the bench most often: a ready driver module on an L298N or a TB6612FNG, three wires from an STM32F103 to it and an encoder on the shaft.

What is inside: which bridge states the IN1 and IN2 inputs and the PWM input give; how the bipolar L298N differs from the MOSFET TB6612FNG; why the same duty cycle gives a different speed depending on what the bridge does in the PWM pause — braking or coasting; how to measure speed with an encoder on a timer and where the resolution runs out at low speed; how to hold speed with an integer PID controller without accumulating the integral in saturation; what happens on reversal, with a stalled shaft and where the energy of a braking motor goes.

Sources: RM0008 (DocID13902 Rev 17): PWM and the OCxPE preload, p. 386; CCR greater than ARR keeps the output at "1", CCR = 0 keeps it at "0", p. 387; encoder mode and table 85, pp. 391–392; the SMS field, p. 407; the input filter IC1F, p. 414. DocID13587 Rev 17 (STM32F103x8/xB): PA0, PA1 are TIM2_CH1_ETR, TIM2_CH2; PA6, PB0, PB1 are TIM3_CH1, TIM3_CH3, TIM3_CH4, p. 29; PB6, PB7 are TIM4_CH1, TIM4_CH2 with 5 V tolerance, p. 32. The project has no documents on the L298N and the TB6612FNG: their input tables are recalled from the ST and Toshiba documents and marked as unverified, and the section gives no numbers from those documents (voltage drop, allowed current, PWM frequency). The motor is the same as in 57а: 12 V, R = 2 Ω, L = 1 mH, K = 0.02 V·s/rad, J = 10⁻⁵ kg·m², b = 10⁻⁶ N·m·s; the encoder is a nominal one — 100 lines, 400 counts per revolution. All numbers below come from a run of the code _verify/dcdrv57i/dcdrv57i.c on a computer (gcc 12.2.0) with a model of the bridge and the motor in Python 3.11; the firmware is built by armclang 6.24 for the Cortex-M3.

What it is called in the documentation

Original Meaning
brushed DC motor driver, dual H-bridge brushed DC motor driver, two H-bridges in one package
forward, reverse forward, reverse
short brake, fast motor stop short brake: the winding is shorted by the switches
stop, free running motor stop, coast coast: all switches open
standby (STBY) standby: the outputs are off
enable input (ENA, ENB) bridge enable input
drive-brake, slow decay PWM with braking in the pause: slow current decay
drive-coast, fast decay PWM with coasting in the pause: fast current decay through the diodes
flyback (freewheeling) diode flyback diode
encoder interface mode timer encoder mode
counts per revolution (CPR) counts per revolution
integrator windup, anti-windup integral build-up in saturation and the guard against it
stall current current of a stalled motor
regenerative braking braking that returns energy to the source

57з.1 H-bridge and four states

The bridge from 57а: two half-bridges, the motor in the link between them. Of the sixteen combinations of four switches four are useful; the rest either repeat them or short the supply through one leg.

State Switches Voltage on the winding What the motor does
forward Q1 + Q4 +U accelerates one way
reverse Q3 + Q2 −U the other way
short brake Q2 + Q4 (or Q1 + Q3) 0 the back EMF drives current through the shorted winding — it brakes
coast all open −U or +U through the diodes while current flows, then only the back EMF spins on by inertia

Coasting and braking are not the same. An open bridge leaves the current only one path, through the flyback diodes to the supply rail: the current falls fast, against the full voltage, and once it is gone the winding is disconnected and only the back EMF is on it. A shorted winding keeps the current flowing while the back EMF drives it, and that brakes the shaft. A driver chip receives inputs from the controller, not switches, and its table turns the inputs into one of the four states.

Left — an H-bridge of four switches Q1–Q4, the motor M in the link between points A and B, forward is Q1 + Q4; below it the model input table: forward — TB6612 IN1 IN2 PWM = 1 0 1, L298N IN1 IN2 ENA = 1 0 1; reverse — 0 1 1 for both; brake — Q2+Q4 or Q1+Q3, on the TB6612 1 0 0 or 1 1 x, on the L298N 0 0 1 or 1 1 1; coast — all open, on the TB6612 0 0 1, on the L298N x x 0; the Toshiba and ST documents are not in the project. Right — PWM at 20 kHz, ARR 3599: if the pause is braking, the current is not interrupted, the average voltage equals the duty cycle times U, at 50 % under a 0.5 A load the motor runs at 2375 rpm at any PWM frequency, the current ripple is 0.15 A peak to peak; if the pause is coasting, the current goes through the diodes to the rail, falls fast and breaks off, at 50 % the motor stands still under the same load, 2375 rpm comes only at 75 %, while the current flows the average voltage is (2d − 1)·U. Bottom — the speed loop every 1 ms: TIM2 in encoder mode, 400 counts per revolution, speed over an 8 ms window, a ramp of 40 counts per second per tick, a Q16 PID with an integral limit, the bridge with reversal through coasting, three TIM3 channels, the driver, the motor with the encoder on the shaft; stall — the output at least 90 % of the limit with the shaft standing for 200 ticks in a row


57з.2 L298N and TB6612FNG

Both chips are two H-bridges in one package, each driven by three inputs. They differ in the switches and in how the inputs work.

L298N TB6612FNG
switches bipolar transistors MOSFETs
inputs of one bridge IN1, IN2 and the enable ENA IN1, IN2 and the PWM input, a common STBY
pause on the enable or PWM input ENA = 0 — coast PWM = 0 — brake
IN1 = IN2 brake (with ENA = 1) 1 1 — brake, 0 0 — coast
loss in a closed switch a junction drop, weakly dependent on current current × channel resistance
flyback diodes the chip has none — modules carry separate ones present in the MOSFET structure
current shunt SENSE pins to the bridge ground — for your own shunt (57б) none

The table is recalled from the ST and Toshiba documents: they are not in the project, and it must be checked against your own document before a board is made. How many volts the L298N switches lose and what resistance the TB6612FNG switches have is not given here for the same reason. Qualitatively the difference is this: on the bipolar L298N the drop across two closed switches hardly depends on current and eats a noticeable share of a low supply, on the MOSFET TB6612FNG it grows with current and is almost zero at low current. The motor gets the rail voltage minus this drop, and its no-load speed falls by the same share.

The main point for the firmware: the same PWM pause on different inputs means different things. The bridge code dc_pins knows this for both chips:

void dc_pins(uint8_t chip, uint8_t mode, int32_t duty, uint16_t period, dc_pins_t *o)
{
    uint16_t on = (uint16_t)(duty < 0 ? (duty < -(int32_t)period ? period : -duty)
                                      : (duty > (int32_t)period ? period : duty));
    uint16_t a = duty >= 0 ? period : 0, b = duty < 0 ? period : 0;
    if ((chip == DC_TB6612) == (mode == DC_DRIVE_BRAKE)) {
        o->pwm = on;                                    /* ШИМ на входе PWM (ENA), направление — уровнями */
        o->in1 = a;
        o->in2 = b;
    } else {
        o->pwm = period;                                /* PWM (ENA) всегда «1», ШИМ — на входе направления */
        o->in1 = duty >= 0 ? on : 0;
        o->in2 = duty < 0 ? on : 0;
    }
}

All three pins are channels of one timer, and the value of each is a CCR: 0 is "0" all the time, period = ARR + 1 is "1" all the time (RM0008, p. 387), PWM in between. The bench model turns the pins into bridge states by its own tables of both chips. At a duty of 900 out of 3600 (25 %) forward:

Chip Pause PWM (ENA) IN1 IN2 Bridge over a period
TB6612FNG brake 900 3600 0 forward 0.25, brake 0.75
TB6612FNG coast 3600 900 0 forward 0.25, coast 0.75
L298N brake 3600 900 0 forward 0.25, brake 0.75
L298N coast 900 3600 0 forward 0.25, coast 0.75

On the TB6612FNG braking in the pause means PWM on the PWM input, on the L298N on the IN input. For all five tested duty values (−3600, −900, 0, 900, 3600) and both modes the bridge of both chips goes through the same states, and the motor speed in the model is the same. A duty of 0 in the brake mode is braking, in the coast mode it is coasting.


57з.3 PWM on a timer: frequency, braking and coasting

On the F103 TIM3 is convenient: PA6, PB0 and PB1 are its channels 1, 3 and 4 (DocID13587, p. 29). TIM3 sits on APB1 but counts from 72 MHz (section 13.3): PSC = 0, ARR = 3599 give PWM at 20 kHz with 3600 steps. TIM1 also works — it adds the MOE bit (13.5), and complementary outputs with a dead time (57а.6) are needed by a bridge of discrete transistors, not by a module. All three channels use preload (RM0008, p. 386): new CCR values take effect together on one update, and a direction input cannot switch before the PWM input.

void SysTick_Handler(void)
{
    dc_pins_t o;
    ms++;
    dc_ctl_tick(&ctl, (uint16_t)TIM2->CNT, &o);
    TIM3->CCR1 = o.pwm;                                 /* все три — в буферы: вступят вместе */
    TIM3->CCR3 = o.in1;                                 /* на одном обновлении TIM3 */
    TIM3->CCR4 = o.in2;
}

PWM frequency. The electrical time constant of the winding is L / R = 0.5 ms, and everything depends on how many PWM periods fit into it. The model: a duty of 50 %, a load of 0.01 N·m (current 0.5 A), 5 s to reach the steady state, averages over the last 0.1 s:

PWM PSC, ARR Brake: current ripple Copper loss Speed Coast: speed Coast: average on the winding
1 kHz 1, 35 999 2.774 A 3.599 2374 rpm 3348 rpm 8.05 V
4 kHz 0, 17 999 0.746 A 1.177 2375 rpm 580 rpm 2.22 V
20 kHz 0, 3599 0.150 A 1.007 2375 rpm 0 0.29 V

"Copper loss" is the ratio of the squared RMS current to the squared mean current: how many times more the ripple heats the winding than a direct current of the same mean. At 1 kHz it is 3.6 times, at 20 kHz almost nothing extra. Besides, 1 and 4 kHz are in the middle of the audible range: the winding and the housing whine at the PWM frequency; 20 kHz is at the upper edge of hearing. The price of a high frequency is switching loss in the switches: it grows in proportion to the frequency, and how large it is for the L298N and the TB6612FNG is in documents the project does not have.

Braking in the pause. The current is not interrupted, and the winding gets on average exactly the duty cycle × U: 6.00 V at 50 % at any frequency, and the speed is the same. At no load the speed is proportional to the duty cycle — 570, 1425, 2851, 4276 and 5131 rpm at 10, 25, 50, 75 and 90 %, exactly fractions of the 5701 rpm no-load speed (57а).

Coasting in the pause. While the current flows through the diodes the winding sees −U, and the current falls fast. At 20 kHz it manages to drop to zero within the pause — it is discontinuous, and its average is not enough for the load. Under the same 0.01 N·m load, 20 kHz:

Duty Brake, rpm Coast, rpm Coast: average on the winding
10 % 95 0 0.01 V
25 % 950 0 0.07 V
50 % 2375 0 0.29 V
75 % 3801 2375 6.00 V
90 % 4656 4086 9.60 V

Up to 50 % in the coast mode the motor does not start. From 75 % the current is no longer interrupted: for part of the period the winding sees +U, for the rest −U, on average (2d − 1)·U — at 75 % the same 6.00 V and the same 2375 rpm that braking gives at 50 %. At a low frequency, on the contrary, coasting drives harder: at 1 kHz the current ends early, the winding is open for the rest of the pause and carries the back EMF — on average 8.05 V instead of 6. In the coast mode the speed depends on the PWM frequency and the load, in the brake mode it does not. A speed controller uses braking in the pause.


57з.4 Encoder: speed from CNT

Encoder mode is section 13.7: TIM2 with SMS = 011 counts both edges of both channels (RM0008, pp. 391 and 407) and finds the direction itself by table 85 (p. 392). An encoder of 100 lines on the motor shaft gives 400 counts per revolution. The input uses the filter IC1F = 0011: 8 samples at 72 MHz (p. 414); a motor encoder produces edges often at high speed, and the long filter of the knob from 13.10 does not fit here.

Speed is the signed difference of CNT readings, summed over a window of several controller ticks:

int32_t enc_speed(enc_t *e, uint16_t cnt)
{
    int16_t d = (int16_t)(uint16_t)(cnt - e->last);   /* разность со знаком: переход через 0 не страшен */
    e->last = cnt;
    e->sum += d - e->d[e->k];
    e->d[e->k] = d;
    if (++e->k >= e->win)
        e->k = 0;
    return e->sum * (int32_t)CTL_HZ / e->win;
}

The resolution of such a speed is one count per window: with a 1 ms tick and a window of one tick it is 1000 counts/s, 150 rpm. The model turns the shaft at a constant speed for 2 s and watches what enc_speed shows:

Window Step 100 rpm 300 rpm 1000 rpm 3000 rpm
1 ms 150 rpm 0…150 150…450 900…1050 2850…3150
8 ms 18.75 rpm 93.8…112.5 281.2…318.8 993.8…1012.5 2981.2…3018.8
32 ms 4.69 rpm 98.4…103.0 295.2…304.6 998.4…1003.0 2995.2…3004.7

The step is constant, and at low speed it becomes the whole value: with a 1 ms window a speed of 100 rpm is shown as zero or as 150. A long window gives a fine step but lags — a speed over 32 ms trails the real one by about half the window. The section's controller is fine with 8 ms. If you need more precision at low speed, you measure not the number of counts per time but the time between counts — by input capture (13.6); this is not done here.

The signed difference does not notice CNT passing 65 535: 10 s at 3000 rpm is 200 000 counts, three wraps, and not a single tick showed a speed more than one step away from 20 000 counts/s.


57з.5 Integer speed PID and a ramp

The controller is the one from 47.6, only in CCR steps: the error in counts per second, coefficients in Q16, the output is a signed duty within ±3600. The derivative is taken from the measurement, but the firmware has kd = 0: the encoder speed is stepped, and the derivative of steps is noise (47.4).

int32_t pid57_step(pid57_t *p, int32_t sp, int32_t meas)
{
    int32_t e = sp - meas;
    int64_t pd = (int64_t)p->kp * e - (int64_t)p->kd * (meas - p->prev);
    p->prev = meas;
    int64_t u = (pd + p->integ) / 65536;
    int64_t lim = p->out_max;
    /* насыщение: выход уже в пределе, и ошибка гонит его дальше — интеграл не копим */
    if (!p->aw || !((u >= lim && e > 0) || (u <= -lim && e < 0)))
        p->integ = (int32_t)clamp64((int64_t)p->integ + (int64_t)p->ki * e, p->integ_max);
    return (int32_t)clamp64((pd + p->integ) / 65536, lim);
}

There are two guards against build-up: a limit on the integral itself (integ_max is the full output) and a ban on accumulating while the output is at the limit and the error pushes it further. The firmware uses kp = 15000 (0.229 CCR steps per count/s) and ki = 300 per tick. The setpoint moves to the target along a ramp — no faster than 40 counts/s per tick, which is 40 000 counts/s²: up to 3000 rpm (20 000 counts/s) in 0.5 s.

The run: a target of 3000 rpm with the ramp, after 1 s a load of 0.01 N·m:

What Result
reached 3000 rpm ± 2 % after 507 ms (the ramp is 500)
overshoot 2.5 rpm
without load on average 2999.9 rpm, spread 3.8; duty 1895 of 3600
load step a dip to 2880.4 rpm, back within ± 2 % after 76 ms
under load 2999.7 rpm, spread 7.8; duty 2194
largest current 0.54 A

No ramp and no guard. A target of 3000 rpm at once: the controller hits the limit, the motor starts from the full voltage, the current is 5.76 A, nearly the starting current from 57а. With the ban on accumulating in saturation the output stays at the limit for 11 ms and the overshoot is 4.2 rpm (0.1 %); without it — 22 ms at the limit, the integral gathers too much and the overshoot is 52.8 rpm (1.8 %). Closer to the motor's limit the difference is larger: a target of 5000 rpm under load — 43 ms at the limit and 1.8 rpm of overshoot against 159 ms and 56.2 rpm (1.1 %).

Low speed. A target of 100 rpm: with an 8 ms window the shaft speed moves within 3.3 rpm and the duty within 31 steps; with a 32 ms window — 2.6 rpm and 9 steps. The measurement step turns into duty jitter.

The whole controller tick — speed, ramp, PID, protection and bridge:

void dc_ctl_tick(dc_ctl_t *c, uint16_t cnt, dc_pins_t *o)
{
    c->speed = enc_speed(&c->enc, cnt);
    if (c->fault) {
        c->duty = 0;
        dc_state(c->drv.chip, DC_COAST, c->drv.period, o);
        return;
    }
    c->sp = ramp_step(c->sp, c->target, c->accel);
    int32_t u = pid57_step(&c->pid, c->sp, c->speed);

57з.6 Protection: reversal, stall current, regeneration, supply

Reversal. On a sign change dc_drive keeps the bridge coasting for dead ticks (1 ms in the firmware) so that the current of the previous direction decays through the diodes. The model shows this does not save you from a current surge: a motor at 3000 rpm has hardly slowed down in 1 ms, and the reverse voltage adds to its back EMF. A target from +3000 straight to −3000 rpm:

How Peak current Energy from the rail until stop To zero speed
no ramp, no pause −8.81 A +1.885 J 21 ms
no ramp, 1 ms pause −8.81 A +1.885 J 22 ms
ramp of 40 counts/s per tick −0.41 A −0.381 J 515 ms

Without a ramp the reversal current exceeds the starting current (6 A): the winding gets U plus the back EMF. The pause only shifts the surge by a millisecond. A dead time is needed for something else — to keep the two switches of one leg from being on at once; in the L298N and the TB6612FNG the chip provides it itself (according to documents the project does not have), and for a bridge of discrete transistors it is the DTG of TIM1 (57а.6). The ramp removes the reversal surge: with it the peak is −0.41 A.

Regeneration. With the ramp the motor is braked by current of the opposite sign, and in the brake-in-pause mode the rail current becomes negative — down to −0.41 A: the motor works as a generator and returns energy. In the model the rail got back 0.381 J of the 0.493 J stored in the rotor at 3000 rpm before the stop; the rest is heat in the winding. Without a ramp it is the other way round: the rail gives another 1.885 J, and all of it turns into heat. Someone must accept the returned energy: a battery will, a power supply with a diode at its output will not, and the voltage on the rail capacitor rises. By how much depends on the capacitance and the source; it was not computed here. A braking ramp, a capacitor at the module and, for big motors, a rail voltage clamp help.

Stall current. The shaft jams — the controller sees an error and raises the duty to the limit, and the current goes to U / R = 6 A and stays there until the winding or the driver burns. The protection in dc_ctl_tick: the output at least 90 % of the limit while the shaft is slower than 30 rpm for 200 ticks in a row — the bridge coasts until a reset.

    int32_t au = u < 0 ? -u : u, as = c->speed < 0 ? -c->speed : c->speed;
    /* выход почти в пределе, а вал стоит: ток останова греет обмотку и ключи — снять питание */
    if (au >= c->pid.out_max * 9 / 10 && as < c->stall_speed) {
        if (++c->stall_n >= c->stall_ms) {
            c->fault = 1;
            c->duty = 0;
            dc_state(c->drv.chip, DC_COAST, c->drv.period, o);
            return;
        }
    } else {
        c->stall_n = 0;
    }

The model jams the shaft at 1000 rpm: the protection trips after 238 ms (the controller needs time to reach 90 %, plus 200 ticks), the current averages 5.63 A during that time, the peak is 6.00 A, then 0. Without the protection, after 0.5 s the duty is 3600 of 3600 and 6.00 A flow continuously. This is protection by speed, not by current: current cannot be limited this way, it is measured with a shunt (57б).

Supply and ground. The driver logic runs from 3.3 or 5 V, the motor from its own rail; the minus of the rail, the minus of the logic and the STM32 ground are joined, otherwise the inputs have no common zero. The pulsed motor current must not flow through the controller ground: the power minus goes to the module by a thick wire, a separate one goes to the controller. A capacitor across the motor rail sits at the module. The allowed input levels and supply voltages are in the document of your chip.


57з.7 Pitfalls

The motor jerks at a low duty or stands still although "PWM is there". The PWM pause landed on the input that gives coasting: on the TB6612FNG that is PWM on IN, on the L298N on ENA. At 20 kHz under a 0.01 N·m load the motor does not start in coast mode up to 50 %. The brake mode is PWM on PWM for the TB6612FNG and on IN for the L298N.

The speed changes with the PWM frequency. The same coasting in the pause: 3348 rpm at 1 kHz and 580 at 4 kHz with the same 50 % duty. With braking in the pause — 2374…2375 at any frequency.

The motor whines. PWM at 1 or 4 kHz is in the middle of the audible range. 20 kHz (ARR = 3599 at 72 MHz) is at the upper edge of hearing, and the current ripple is smaller: 0.150 A against 2.774 A at 1 kHz.

The speed jumps between 0 and 150 rpm. The measurement window is one tick: a step of 150 rpm at 400 counts per revolution. An 8 ms window gives a step of 18.75.

After CNT passes 65 535 the speed jumps. The code takes CNT itself rather than the signed difference (int16_t)(uint16_t)(cnt - last).

A 5 V encoder on PA0, PA1. These pins are not 5 V tolerant (DocID13587, p. 29): an encoder with 5 V outputs goes to PB6, PB7 (TIM4, 5 V tolerant, p. 32) or through a divider; an open-collector one — with a pull-up to 3.3 V.

Overshoot after acceleration. The integral built up while the output was at the limit: 52.8 rpm of overshoot against 4.2 with the ban on accumulating in saturation.

The driver trips or heats up on reversal. The target changes in a step: −8.81 A when reversing from 3000 rpm. A coast pause does not help, the ramp does: −0.41 A.

The rail voltage rises during braking. A motor with a ramp returns energy to the source; a power supply with a diode will not accept it. A capacitor, a clamp or a battery.

The shaft jammed — the driver is hot. The controller holds 100 % and 6 A. Protection by speed: the output is at the limit while the shaft stands — coast.

The motor does not turn although everything is set up. The TB6612FNG is in standby: STBY = 0. Or the grounds of the module and the controller are not joined.


57з.8 Summary

Full sources

dcdrv57i.h — bridge, encoder, PID and controller: types and prototypes.

/* dcdrv57i.h — раздел 57з: коллекторный двигатель через модуль-драйвер, без регистров.
 * Мост (L298N или TB6612FNG) управляется тремя выводами: два входа направления и вход ШИМ
 * (у L298N — ENA). Все три — каналы одного таймера, значение каждого — CCR: 0 — всегда «0»,
 * period — всегда «1», между ними — ШИМ. Код один для прошивки fw57i.c и стенда host57i.c. */
#ifndef DCDRV57I_H
#define DCDRV57I_H
#include <stdint.h>

#define DC_PERIOD   3600u       /* 72 МГц / 20 кГц: ARR = 3599 */
#define CTL_HZ      1000u       /* регулятор — раз в 1 мс */
#define ENC_CPR     400         /* отсчётов на оборот вала: энкодер 100 штрихов × 4 фронта */
#define ENC_WIN_MAX 32u         /* самое длинное окно измерения скорости, тиков регулятора */
#define RPM(x)      ((int32_t)(x) * ENC_CPR / 60)       /* об/мин → отсчётов в секунду */

enum { DC_COAST = 0, DC_FWD, DC_REV, DC_BRAKE };        /* состояние моста */
enum { DC_TB6612 = 0, DC_L298N };                       /* микросхема */
enum { DC_DRIVE_BRAKE = 0, DC_DRIVE_COAST };            /* что в паузе ШИМ: торможение или выбег */

typedef struct { uint16_t pwm, in1, in2; } dc_pins_t;   /* CCR выводов PWM (ENA), IN1, IN2 */

typedef struct {
    uint8_t chip, mode;
    uint16_t period;            /* ARR + 1 */
    uint16_t dead;              /* тиков в выбеге при смене направления */
    uint16_t wait;              /* сколько ещё ждать */
    int8_t dir;                 /* −1, 0, +1: последнее направление */
} dc_drv_t;

typedef struct {
    uint16_t last;              /* прошлое показание CNT */
    uint8_t win, k;             /* окно и место в кольце */
    int16_t d[ENC_WIN_MAX];     /* приращения за последние win тиков */
    int32_t sum;
} enc_t;

typedef struct {
    int32_t kp, ki, kd;         /* Q16: шагов CCR на (отсчёт/с) */
    int32_t integ, integ_max;   /* Q16, накопитель и его предел */
    int32_t prev;               /* прошлое измерение: производная — по нему */
    int32_t out_max;            /* предел выхода: ±period */
    uint8_t aw;                 /* 1 — не копить интеграл, пока выход в пределе */
} pid57_t;

typedef struct {
    dc_drv_t drv;
    enc_t enc;
    pid57_t pid;
    int32_t target, sp, accel;  /* цель, задание после рампы, шаг рампы за тик (отсчётов/с) */
    int32_t speed, duty;        /* измерено и подано */
    int32_t stall_speed;        /* «стоит», если медленнее, отсчётов/с */
    uint16_t stall_ms, stall_n; /* сколько тиков подряд терпеть и сколько уже */
    uint8_t fault;              /* 1 — останов: мост в выбеге до сброса */
} dc_ctl_t;

void dc_state(uint8_t chip, uint8_t st, uint16_t period, dc_pins_t *o);
void dc_pins(uint8_t chip, uint8_t mode, int32_t duty, uint16_t period, dc_pins_t *o);
int32_t dc_drive(dc_drv_t *d, int32_t duty, dc_pins_t *o);
void enc_init(enc_t *e, uint8_t win, uint16_t cnt);
int32_t enc_speed(enc_t *e, uint16_t cnt);
int32_t pid57_step(pid57_t *p, int32_t sp, int32_t meas);
int32_t ramp_step(int32_t sp, int32_t target, int32_t step);
void dc_ctl_tick(dc_ctl_t *c, uint16_t cnt, dc_pins_t *o);

#endif

dcdrv57i.c — bridge, encoder speed, PID, ramp and protection code, shared by the firmware and the bench.

/* dcdrv57i.c — раздел 57з: мост, энкодер, рампа и ПИД скорости на целых числах. */
#include "dcdrv57i.h"

/* Мост стоит: выбег — все ключи разомкнуты, торможение — обмотка замкнута накоротко. */
void dc_state(uint8_t chip, uint8_t st, uint16_t period, dc_pins_t *o)
{
    if (chip == DC_L298N) {
        o->pwm = st == DC_BRAKE ? period : 0;          /* ENA = 0 — выбег; ENA = 1, IN1 = IN2 — торможение */
        o->in1 = o->in2 = 0;
    } else {
        o->pwm = period;                                /* TB6612: IN1 = IN2 = 0 — выбег, IN1 = IN2 = 1 — торможение */
        o->in1 = o->in2 = st == DC_BRAKE ? period : 0;
    }
}

/* Скважность со знаком: duty = ±period — полное напряжение, 0 — пауза всё время.
 * В паузе ШИМ мост тормозит (DC_DRIVE_BRAKE) или выбегает (DC_DRIVE_COAST). У TB6612 пауза на входе
 * PWM — торможение, на входе IN — выбег; у L298N наоборот: пауза на ENA — выбег, на IN — торможение. */
void dc_pins(uint8_t chip, uint8_t mode, int32_t duty, uint16_t period, dc_pins_t *o)
{
    uint16_t on = (uint16_t)(duty < 0 ? (duty < -(int32_t)period ? period : -duty)
                                      : (duty > (int32_t)period ? period : duty));
    uint16_t a = duty >= 0 ? period : 0, b = duty < 0 ? period : 0;
    if ((chip == DC_TB6612) == (mode == DC_DRIVE_BRAKE)) {
        o->pwm = on;                                    /* ШИМ на входе PWM (ENA), направление — уровнями */
        o->in1 = a;
        o->in2 = b;
    } else {
        o->pwm = period;                                /* PWM (ENA) всегда «1», ШИМ — на входе направления */
        o->in1 = duty >= 0 ? on : 0;
        o->in2 = duty < 0 ? on : 0;
    }
}

/* Смена знака — через выбег на d->dead тиков: ток прошлого направления спадает через диоды,
 * а не встречает сразу противоположную диагональ. Возвращает поданную скважность. */
int32_t dc_drive(dc_drv_t *d, int32_t duty, dc_pins_t *o)
{
    int8_t s = (int8_t)(duty > 0 ? 1 : duty < 0 ? -1 : 0);
    if (s != 0 && d->dir != 0 && s != d->dir) {
        d->wait = d->dead;
        d->dir = 0;
    }
    if (d->wait) {
        d->wait--;
        dc_state(d->chip, DC_COAST, d->period, o);
        return 0;
    }
    if (s != 0)
        d->dir = s;
    dc_pins(d->chip, d->mode, duty, d->period, o);
    return duty;
}

void enc_init(enc_t *e, uint8_t win, uint16_t cnt)
{
    e->last = cnt;
    e->win = win < 1u ? 1u : win > ENC_WIN_MAX ? ENC_WIN_MAX : win;
    e->k = 0;
    e->sum = 0;
    for (unsigned i = 0; i < ENC_WIN_MAX; i++)
        e->d[i] = 0;
}

/* Скорость в отсчётах в секунду: сумма приращений за окно из win тиков. */
int32_t enc_speed(enc_t *e, uint16_t cnt)
{
    int16_t d = (int16_t)(uint16_t)(cnt - e->last);   /* разность со знаком: переход через 0 не страшен */
    e->last = cnt;
    e->sum += d - e->d[e->k];
    e->d[e->k] = d;
    if (++e->k >= e->win)
        e->k = 0;
    return e->sum * (int32_t)CTL_HZ / e->win;
}

static int64_t clamp64(int64_t x, int64_t lim)
{
    return x > lim ? lim : x < -lim ? -lim : x;
}

/* ПИД на Q16 (раздел 47.6): интеграл со своим пределом, производная по измерению, выход — в шагах CCR. */
int32_t pid57_step(pid57_t *p, int32_t sp, int32_t meas)
{
    int32_t e = sp - meas;
    int64_t pd = (int64_t)p->kp * e - (int64_t)p->kd * (meas - p->prev);
    p->prev = meas;
    int64_t u = (pd + p->integ) / 65536;
    int64_t lim = p->out_max;
    /* насыщение: выход уже в пределе, и ошибка гонит его дальше — интеграл не копим */
    if (!p->aw || !((u >= lim && e > 0) || (u <= -lim && e < 0)))
        p->integ = (int32_t)clamp64((int64_t)p->integ + (int64_t)p->ki * e, p->integ_max);
    return (int32_t)clamp64((pd + p->integ) / 65536, lim);
}

/* Рампа: задание идёт к цели не быстрее step за тик. */
int32_t ramp_step(int32_t sp, int32_t target, int32_t step)
{
    if (target > sp + step)
        return sp + step;
    if (target < sp - step)
        return sp - step;
    return target;
}

/* Тик регулятора раз в 1 мс: скорость по энкодеру, рампа, ПИД, защита от останова, мост. */
void dc_ctl_tick(dc_ctl_t *c, uint16_t cnt, dc_pins_t *o)
{
    c->speed = enc_speed(&c->enc, cnt);
    if (c->fault) {
        c->duty = 0;
        dc_state(c->drv.chip, DC_COAST, c->drv.period, o);
        return;
    }
    c->sp = ramp_step(c->sp, c->target, c->accel);
    int32_t u = pid57_step(&c->pid, c->sp, c->speed);
    int32_t au = u < 0 ? -u : u, as = c->speed < 0 ? -c->speed : c->speed;
    /* выход почти в пределе, а вал стоит: ток останова греет обмотку и ключи — снять питание */
    if (au >= c->pid.out_max * 9 / 10 && as < c->stall_speed) {
        if (++c->stall_n >= c->stall_ms) {
            c->fault = 1;
            c->duty = 0;
            dc_state(c->drv.chip, DC_COAST, c->drv.period, o);
            return;
        }
    } else {
        c->stall_n = 0;
    }
    c->duty = dc_drive(&c->drv, u, o);
}

fw57i.c — STM32F103 firmware: TIM3 drives the three driver inputs, TIM2 reads the encoder, the controller runs in SysTick.

/* fw57i.c — раздел 57з: коллекторный двигатель через TB6612FNG (или L298N) на STM32F103 (BluePill), 72 МГц.
 * TIM3 — ШИМ 20 кГц на трёх каналах: PA6 (CH1) — PWMA (у L298N — ENA), PB0 (CH3) — AIN1 (IN1), PB1 (CH4) — AIN2 (IN2).
 * TIM2 — энкодер на PA0, PA1, счёт по обоим фронтам. PB12 — STBY у TB6612 (у L298N не нужен).
 * SysTick раз в 1 мс: скорость, рампа, ПИД, защита от останова (dcdrv57i.c), новые CCR — с ближайшего обновления.
 * Тактирование до 72 МГц — как в разделе 6; TIM2 и TIM3 на APB1 (36 МГц) считают от 72 МГц (раздел 13.3). */
#include "stm32f10x.h"
#include "dcdrv57i.h"

#define CPU_HZ   72000000u
#define DRV_CHIP DC_TB6612
#define DRV_MODE DC_DRIVE_BRAKE

_Static_assert(CPU_HZ / DC_PERIOD == 20000u, "ШИМ 20 кГц: ARR = 3599");

static dc_ctl_t ctl;
static volatile uint32_t ms;

void SysTick_Handler(void)
{
    dc_pins_t o;
    ms++;
    dc_ctl_tick(&ctl, (uint16_t)TIM2->CNT, &o);
    TIM3->CCR1 = o.pwm;                                 /* все три — в буферы: вступят вместе */
    TIM3->CCR3 = o.in1;                                 /* на одном обновлении TIM3 */
    TIM3->CCR4 = o.in2;
}

static void pwm_init(void)
{
    GPIOA->CRL = (GPIOA->CRL & ~(0xFu << 24)) | 0xBu << 24;    /* PA6 — TIM3_CH1, push-pull */
    GPIOB->CRL = (GPIOB->CRL & ~0xFFu) | 0xBBu;                /* PB0, PB1 — TIM3_CH3, TIM3_CH4 */
    TIM3->PSC = 0;
    TIM3->ARR = DC_PERIOD - 1u;
    TIM3->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1PE;      /* ШИМ 1, буфер */
    TIM3->CCMR2 = TIM_CCMR2_OC3M_2 | TIM_CCMR2_OC3M_1 | TIM_CCMR2_OC3PE
                | TIM_CCMR2_OC4M_2 | TIM_CCMR2_OC4M_1 | TIM_CCMR2_OC4PE;
    TIM3->CCER = TIM_CCER_CC1E | TIM_CCER_CC3E | TIM_CCER_CC4E;
    dc_pins_t o;
    dc_state(DRV_CHIP, DC_COAST, DC_PERIOD, &o);        /* до первого тика — выбег */
    TIM3->CCR1 = o.pwm;
    TIM3->CCR3 = o.in1;
    TIM3->CCR4 = o.in2;
    TIM3->CR1 = TIM_CR1_ARPE;
    TIM3->EGR = TIM_EGR_UG;
    TIM3->CR1 |= TIM_CR1_CEN;
}

static void enc_tim_init(void)
{
    /* PA0, PA1 — входы с подтяжкой вверх: у энкодера с открытым коллектором своей нет */
    GPIOA->CRL = (GPIOA->CRL & ~0xFFu) | 0x88u;
    GPIOA->BSRR = GPIO_BSRR_BS0 | GPIO_BSRR_BS1;
    TIM2->CCMR1 = TIM_CCMR1_CC1S_0 | TIM_CCMR1_CC2S_0                        /* TI1, TI2 — входы */
                | TIM_CCMR1_IC1F_1 | TIM_CCMR1_IC1F_0 | TIM_CCMR1_IC2F_1 | TIM_CCMR1_IC2F_0;   /* 0011: N = 8 */
    TIM2->SMCR = TIM_SMCR_SMS_1 | TIM_SMCR_SMS_0;       /* 011: энкодер, оба фронта обоих каналов */
    TIM2->ARR = 0xFFFFu;
    TIM2->CR1 = TIM_CR1_CEN;
}

int main(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN;
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN | RCC_APB1ENR_TIM3EN;
    GPIOB->CRH = (GPIOB->CRH & ~(0xFu << 16)) | 0x2u << 16;    /* PB12 — STBY */
    GPIOC->CRH = (GPIOC->CRH & ~(0xFu << 20)) | 0x2u << 20;    /* PC13 — светодиод платы */
    pwm_init();
    enc_tim_init();

    ctl.drv = (dc_drv_t){DRV_CHIP, DRV_MODE, DC_PERIOD, 1u, 0u, 0};
    enc_init(&ctl.enc, 8u, (uint16_t)TIM2->CNT);
    ctl.pid = (pid57_t){15000, 300, 0, 0, 3600 << 16, 0, DC_PERIOD, 1u};
    ctl.accel = 40;                                     /* 40 отсчётов/с за 1 мс: 0 → 3000 об/мин за 0,5 с */
    ctl.stall_speed = RPM(30);
    ctl.stall_ms = 200u;
    ctl.target = RPM(1500);
    GPIOB->BSRR = GPIO_BSRR_BS12;                       /* TB6612 из дежурного режима */
    SysTick_Config(CPU_HZ / CTL_HZ);

    uint32_t next = 0;
    for (;;) {
        if ((int32_t)(ms - next) >= 0) {
            next += 3000u;                              /* каждые 3 с — реверс: +1500 ↔ −1500 об/мин */
            __disable_irq();
            ctl.target = -ctl.target;
            __enable_irq();
        }
        GPIOC->BSRR = ctl.fault ? GPIO_BSRR_BR13 : GPIO_BSRR_BS13;   /* светодиод горит — останов */
    }
}

Next: section 57и "AS5600 magnetic encoder: angle over I2C and PWM, zero position, turn counting and speed" — from counting increments to an absolute angle: a magnet on the shaft, Hall sensors over I2C and turns without losing the zero.

Open in the interactive handbook