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

50а. Circuits in frequency: magnitude response, phase response and the Bode plot

Section 50 looked at the RC circuit in time, and in frequency only through the cutoff formula and the "20 dB per decade" rule. Here is the same circuit in full: how amplitude and phase change with frequency, why they are drawn in decibels on a logarithmic axis, what happens when there are two stages, how an RC in front of the ADC helps and how it does not help, how a filter on an op-amp behaves, and how to measure a frequency response with the microcontroller itself — the DAC outputs a sine, the ADC measures the response.

References: reference manual RM0090 Rev 19 (fields TSEL1 in DAC_CR and EXTSEL in ADC_CR2) and datasheet DS8626 Rev 9 for the F405/407 (DAC output buffer, ADC sampling capacitance) — both from the Keil pack. Every circuit is computed three times: with a formula, with ngspice 39 (image stm-lab:1, op-amp — the macromodel of section 164) and with a measurement model on a PC that runs the same C code as the firmware.

Terms used in the documentation

Term Meaning
frequency response how a circuit responds to sines of different frequencies
magnitude response gain versus frequency
phase response phase shift versus frequency
Bode plot magnitude and phase plotted on a logarithmic frequency axis
cutoff (corner) frequency frequency where the response bends down
roll-off, dB/decade rate of fall of the response above cutoff
aliasing high frequencies appearing as low ones after sampling
anti-aliasing filter filter in front of the ADC that prevents aliasing
frequency sweep stepping a test sine through a range of frequencies
zero-order hold holding each sample as a step (the DAC staircase)

50а.1 What magnitude and phase responses show

A sine is applied to the input of a linear circuit. At the output there is a sine of the same frequency, but of a different amplitude and shifted in time. The ratio of the amplitudes is the magnitude response, the shift in degrees of the period is the phase response. Both depend only on frequency, so it is convenient to write them as one complex number. For the RC circuit of section 50:

H(f) = 1 / (1 + j · f / fc),   fc = 1 / (2 · π · R · C)
|H| — во сколько раз ослаблена амплитуда, arg H — сдвиг фазы

Amplitude is expressed in decibels: 20 · lg(K). This is more convenient for two reasons: circuits connected one after another multiply their gains, while decibels simply add up; and one graph fits both 1 and 0.0001. Reference points worth remembering:

Ratio Decibels
0.707 (square root of one half) −3.01
0.5 −6.02
0.1 −20
0.01 −40
2 +6.02
10 +20

Frequency is plotted on a logarithmic axis: a decade is a step of 10 times, an octave of 2 times. A roll-off of 20 dB per decade is the same as 6.02 dB per octave.


50а.2 Bode plot of an RC circuit

Take R = 10 kΩ and C = 10 nF: the cutoff is 1591.5 Hz. The formula and ngspice give the same result:

Frequency Magnitude, dB Phase
0.1 of cutoff −0.043 −5.71°
0.5 of cutoff −0.969 −26.57°
cutoff −3.010 −45.00°
2 × cutoff −6.990 −63.43°
10 × cutoff −20.04 −84.29°
100 × cutoff −40.00 −89.43°

ngspice finds the −3.01 dB point exactly at 1591.5 Hz, the phase at cutoff is −45.00°, and at ten times the cutoff −20.04 dB and −84.29°.

Bode plot of a 10 kΩ and 10 nF RC circuit and of two such stages through a buffer: cutoff 1.59 kHz at −3.01 dB, roll-off of 20 and 40 dB per decade, phase −45° and −90° at cutoff, asymptotes dashed

The curve hardly needs to be computed — it is replaced by straight lines (asymptotes):

At low frequency the phase is simply a delay. At 50 Hz the circuit shifts the signal by −1.80°, that is by 99.97 µs, and the RC time constant is 100 µs: below the cutoff an RC circuit lags by τ without distorting the waveform.


50а.3 Second order: two stages in a row

Put two identical RC stages in a row. If there is a buffer between them (an op-amp voltage follower), the transfer functions simply multiply: H². The roll-off becomes 40 dB per decade (ngspice: −40.09 dB at ten times the cutoff, −80.00 dB at a hundred times), the phase at cutoff is −90° and goes further towards −180°. But the bandwidth also narrows: −3 dB is now not at 1591.5 but at 1024.3 Hz — at 0.6436 of the single-stage cutoff, and at the cutoff itself it is already −6.02 dB.

Without a buffer the second stage loads the first, and the result is a different circuit:

H(x) = 1 / (1 − x² + 3 · j · x),   x = f / fc

Its Q factor is 1/3, and the bandwidth is narrower still: −3 dB at 0.3742 of the cutoff, 595.6 Hz (ngspice gives the same 595.6 Hz). At the cutoff it is −9.54 dB instead of −6.02 with a buffer, phase −90°. Far from the cutoff there is no difference — the same −40 dB per decade (−80.00 dB at a hundred times the cutoff) and the phase towards −180° (−178.28° at the same point).

Two RCs in a row are the simplest second-order filter, but also the most sluggish: its knee is gentle. A sharp knee at the same order is given by the Sallen–Key circuit from section 178, which also explains how to choose the Q factor.


50а.4 RC in front of the ADC and aliasing

An ADC that takes samples at a rate fs cannot tell frequency f from |f − k · fs|. At fs = 10 kHz a 9 kHz interference looks in the samples like an honest 1 kHz signal, and it can no longer be filtered out in software. So everything above fs/2 must be suppressed before the ADC.

How much our RC circuit with a 1.59 kHz cutoff gives:

Interference RC Two RCs through a buffer
5 kHz (half of fs) −10.36 dB, 3.30 times −20.72 dB
9 kHz (seen as 1 kHz) −15.18 dB, 5.74 times −30.36 dB

ngspice confirms all four numbers. But for full-amplitude interference to fall below half an LSB of a 12-bit ADC, you need 78.3 dB. Conclusion: a single RC at the ADC input is not an anti-aliasing filter. Its job is to hold charge for the sampling capacitor (on the F407 it is 4 pF per DS8626, in detail — 50.6 and 180) and to remove part of the broadband noise. Aliasing is prevented differently: raise fs to several times the signal bandwidth and filter digitally, or use a high-order filter (178.5).


50а.5 A filter on an op-amp: gain and cutoff at once

An inverting amplifier with a capacitor in parallel with the feedback resistor: R1 = 10 kΩ, R2 = 100 kΩ, C = 1 nF. Gain −R2/R1 = −10 (+20 dB), cutoff 1 / (2π · R2 · C) — the same 1591.5 Hz. The supply is single 3.3 V, the reference is the midpoint 1.65 V (section 162).

Frequency Ideal op-amp Op-amp with 1 MHz bandwidth (ngspice)
10 Hz 20.00 dB, 179.6° 20.00 dB, 179.6°
1591.5 Hz 16.99 dB, 135.0° 16.92 dB, 134.5°
100 kHz −15.96 dB, 90.9° −16.10 dB, 85.2°
1 MHz −35.96 dB, 90.1° −36.03 dB, 37.3°

The phase here is counted including the inversion: 180° at low frequency, towards 90° above the cutoff. The −3 dB level from 20 dB is 16.99 dB; a real op-amp reaches it at 1566 Hz, 1.6 % below the calculated value: the amplifier itself has a finite bandwidth (the gain-bandwidth product is 1 MHz in the model), and at frequencies where the excess gain is small it adds its own pole. This is clearly visible in the phase: at 1 MHz the ideal holds 90°, while the model is already at 37° — the filter becomes second-order. The rule is simple: the op-amp bandwidth must be much greater than the product of gain and cutoff (here 10 · 1.59 kHz), otherwise the cutoff and the roll-off will shift. On phase margin — section 163.


50а.6 Measuring the frequency response with the microcontroller itself

You do not need a generator and an oscilloscope: the F407 has a DAC and an ADC. The DAC outputs a sine from a table of M points, the circuit passes it, the ADC samples the output. Both are started by one event — the TIM2 update: in RM0090 this is TSEL1 = 100 in DAC_CR and EXTSEL = 0110 in ADC_CR2. The timer sets the sampling rate fs, and the sine frequency is fs / M.

void bode_trigger_setup(uint32_t arr)
{
    TIM2->ARR = arr;                                            /* fs = 84 МГц / (ARR + 1) */
    TIM2->CR2 = TIM_CR2_MMS_1;
    DAC->CR = DAC_CR_TSEL1_2 | DAC_CR_TEN1 | DAC_CR_DMAEN1 | DAC_CR_EN1;
    ADC1->CR2 = ADC_CR2_EXTEN_0 | ADC_CR2_EXTSEL_1 | ADC_CR2_EXTSEL_2 | ADC_CR2_DMA | ADC_CR2_DDS | ADC_CR2_ADON;
    TIM2->CR1 = TIM_CR1_CEN;
}

The APB1 bus timers count from 84 MHz with a 168 MHz core (section 13). DMA streams for the table and the samples are as in section 9. After waiting eight periods for settling, take 16 whole periods and correlate them with reference sine and cosine of the same phase as the DAC table:

    for (int k = 0; k < n; k++) {
        float x = (float)adc[k] - mean;
        i += x * s_tab[k % m];
        q += x * c_tab[k % m];
    }
    float amp = 2.0f * sqrtf(i * i + q * q) / (float)n;
    float ph = atan2f(q, i) * 57.295780f;
    if (zoh) {
        const float x = 3.14159265f / (float)m;
        amp /= sinf(x) / x;                    /* ступенька ослабляет первую гармонику: sinc */
        ph += 180.0f / (float)m;               /* и запаздывает на полшага */
    }

The DC component (the midpoint 2048) is subtracted, and what remains is the amplitude and phase of the first harmonic. The zoh correction is needed because the DAC outputs not a sine but steps: at M = 32 they attenuate the first harmonic by a factor of 0.99839 (−0.0140 dB) and shift it by half a step — 5.625°. Without the correction, the phase response at 32 points and frequencies from 1 kHz is wrong by 5.3–5.9°.

The PC model compiles the same file with gcc and feeds it samples from an exact model of the circuit (DAC staircase, RC, sampling at the trigger instant, 12 bits). The result:

Numbers to check on the board: the DAC table 448…3648 lies inside 0x0E0…0xF1C, where the DAC buffer per DS8626 gives the full swing; the datasheet allows updating the DAC up to 1 million times per second for small steps, and our fs is no higher than 320 kHz (but at the upper frequencies the table steps are large — hundreds of codes, and buffer settling on them is not verified here); the buffer load must be no less than 5 kΩ, and the input of the RC circuit is never less than R = 10 kΩ. At 1591.5 Hz with M = 32 the timer divider comes out at 1649, and the frequency is 1591.87 Hz — compute the formula for that one, not for the requested one. Memory at M = 1024: reference sine and cosine — 8 KB, DAC table — 2 KB, 16 periods of samples — 32 KB.


50а.7 Pitfalls

Two stages of 1.59 kHz each, but the cutoff is at 1 kHz. −3 dB of the cascade is not the cutoff of each stage: through a buffer 1024 Hz, without a buffer 596 Hz. Calculate the overall magnitude response, not the stages separately.

A second RC stage without a buffer is a different circuit. At the cutoff −9.54 dB instead of −6.02: the second stage loads the first. Either a buffer between the stages, or calculate the cascade as one circuit.

The RC at the ADC input is taken for aliasing protection. At 9 kHz with fs = 10 kHz it attenuates the interference only 5.74 times, while 78 dB are needed. The interference lands in the signal band for good.

The phase is measured through the DAC without the staircase correction. At 32 points — an extra 5.6° of lag that is not in the circuit.

A sweep with 32 points at low frequencies. When the DAC step is longer than the circuit's time constant, the "sine plus half a step" model breaks down; the phase drifts by degrees. Keep fs well above the cutoff by increasing the number of points.

Correlation over a non-integer number of periods. The reference sine and cosine stop being orthogonal, and the amplitude and phase drift depending on where the record is cut. The number of samples must be exactly M × the number of periods.

Expecting an op-amp filter to roll off forever. At frequencies where the op-amp bandwidth runs out, its own pole is added: at 1 MHz the phase is 37° instead of 90°, and the cutoff has moved down by 1.6 %.


50а.8 Summary

Full source code

bode50b.c — magnitude and phase response of a circuit, measured with the microcontroller's DAC and ADC.

/* bode50b.c — раздел 50а: АЧХ и ФЧХ цепи, снятые самим микроконтроллером.
 *
 * ЦАП выдаёт синус по таблице из m точек, АЦП берёт отсчёт с выхода цепи по тому же
 * запуску таймера. После целого числа периодов свёртка с опорными sin и cos даёт амплитуду
 * и фазу первой гармоники. Файл собирается двумя способами:
 *   - armclang для STM32F407 (-DSTM32F407xx -include stm32f4xx.h): математика + запуск от TIM2;
 *   - gcc на ПК (bode_host.c включает этот файл): та же математика на отсчётах модели цепи.
 */
#include <stdint.h>
#include <math.h>

#define BODE_MMAX 1024     /* точек синуса на период — не больше */
#define BODE_MID 2048      /* середина шкалы 12 бит */
#define BODE_AMP 1600      /* амплитуда, коды: 448…3648 — внутри 0x0E0…0xF1C, где буфер ЦАП линеен */

static float s_tab[BODE_MMAX], c_tab[BODE_MMAX];

/* Таблица для ЦАП на m точек и опорные синус и косинус той же фазы. */
void bode_tables(uint16_t *dac, int m)
{
    for (int k = 0; k < m; k++) {
        float w = 6.2831853f * (float)k / (float)m;
        s_tab[k] = sinf(w);
        c_tab[k] = cosf(w);
        dac[k] = (uint16_t)lrintf(BODE_MID + BODE_AMP * s_tab[k]);
    }
}

typedef struct { float gain_db, phase_deg; } bode_point;

/* n отсчётов АЦП — целое число периодов по m точек. zoh = 1: поправка на ступеньку ЦАП. */
bode_point bode_measure(const uint16_t *adc, int n, int m, int zoh)
{
    float mean = 0.0f, i = 0.0f, q = 0.0f;
    for (int k = 0; k < n; k++)
        mean += adc[k];
    mean /= (float)n;
    for (int k = 0; k < n; k++) {
        float x = (float)adc[k] - mean;
        i += x * s_tab[k % m];
        q += x * c_tab[k % m];
    }
    float amp = 2.0f * sqrtf(i * i + q * q) / (float)n;
    float ph = atan2f(q, i) * 57.295780f;
    if (zoh) {
        const float x = 3.14159265f / (float)m;
        amp /= sinf(x) / x;                    /* ступенька ослабляет первую гармонику: sinc */
        ph += 180.0f / (float)m;               /* и запаздывает на полшага */
    }
    bode_point p = { 20.0f * log10f(amp / BODE_AMP), ph };
    return p;
}

#ifdef STM32F407xx
/* Один запуск на двоих: TIM2 TRGO по обновлению, ЦАП (TSEL1 = 100) и АЦП (EXTSEL = 0110)
 * берут его оба. Потоки DMA для таблицы и отсчётов — как в разделе 9, здесь не показаны. */
_Static_assert(DAC_CR_TSEL1_2 == (4UL << DAC_CR_TSEL1_Pos), "TSEL1 = 100 — TIM2 TRGO (RM0090, DAC_CR)");
_Static_assert((ADC_CR2_EXTSEL_1 | ADC_CR2_EXTSEL_2) == (6UL << ADC_CR2_EXTSEL_Pos), "EXTSEL = 0110 — TIM2 TRGO (RM0090, ADC_CR2)");
_Static_assert(TIM_CR2_MMS_1 == (2UL << TIM_CR2_MMS_Pos), "MMS = 010 — TRGO по обновлению");

void bode_trigger_setup(uint32_t arr)
{
    TIM2->ARR = arr;                                            /* fs = 84 МГц / (ARR + 1) */
    TIM2->CR2 = TIM_CR2_MMS_1;
    DAC->CR = DAC_CR_TSEL1_2 | DAC_CR_TEN1 | DAC_CR_DMAEN1 | DAC_CR_EN1;
    ADC1->CR2 = ADC_CR2_EXTEN_0 | ADC_CR2_EXTSEL_1 | ADC_CR2_EXTSEL_2 | ADC_CR2_DMA | ADC_CR2_DDS | ADC_CR2_ADON;
    TIM2->CR1 = TIM_CR1_CEN;
}
#endif

Next: section 51 "Semiconductors: diodes, Zener diodes, bipolar transistors" — parts for which non-linearity is not a nuisance but a working tool.

Open in the interactive handbook