Embedded HandbookПриложения
Все разделы › XXVI. От макета к изделию
STM32F103, STM32F407 · уровень средний · 19 мин чтения
Открыть в интерактивном справочнике

202. Калибровка на линии

На макете измерительный канал подгоняют константой в коде: подкрутил — совпало — забыл. В серии так нельзя: у каждого экземпляра свой делитель, своя опора и своё смещение, а прошивка одна на всех. Разница между «работает у меня» и «работает у каждой сотой платы» — это процедура на линии: подать эталонное значение, вычислить поправки конкретного экземпляра, записать их в него и проверить. Этот раздел — про то, сколько ошибки набегает из допусков, что завод уже откалибровал внутри камня, как откалибровать остальное и куда положить коэффициенты.

Все числа — из расчёта _verify/lcal202/budget.py и прогона модели _verify/lcal202/lcal.c (gcc 12 в образе stm-lab:1); адреса — из заголовков и документации паков F1 и F4.

Как это называется в документации

Оригинал Перевод
end-of-line calibration калибровка в конце линии, при выпуске
device under test (DUT) испытуемое изделие
test accuracy ratio (TAR) запас точности эталона над допуском
golden unit контрольный экземпляр для проверки самой станции
guard band защитная полоса: допуск приёмки уже допуска изделия
as-found / as-left показания «как пришло» и «как оставили»
calibration interval межкалибровочный интервал
piecewise linear кусочно-линейное приближение

202.1 Зачем калибровать каждый экземпляр

Соберём бюджет ошибки типового канала: измерение до 24 В через делитель 68 кОм / 10 кОм с резисторами ±1 %, опора АЦП — питание 3,3 В от стабилизатора ±1 %, АЦП 12 бит. Коэффициент деления — 0,128205, полная шкала — 25,74 В.

Ошибку делителя считают перебором углов допуска: оба резистора на краях ±1 % во всех сочетаниях. Худшие пары — R1 вниз, R2 вверх и наоборот:

Слагаемое Худший случай
делитель 68 кОм / 10 кОм, резисторы ±1 % −1,73 % … +1,76 %
опора: питание от стабилизатора ±1 % ±1,00 %
АЦП F1, полная неподстроенная ошибка ±5 LSB ±0,12 %
сумма ±2,88 % — это ±0,69 В на входе 24 В
корень из суммы квадратов ±2,03 % — ±0,49 В

Для прикидки хватает формулы: относительный уход коэффициента деления ≈ (1 − k)·(δR2 − δR1). При k = 0,128 она даёт 1,74 % — почти точно; у симметричного делителя 10 кОм / 10 кОм уход меньше, ровно ±1,00 %.

Худший случай — редкость, но и середина распределения не радует: Монте-Карло по 100 000 «экземпляров» с равномерными допусками даёт медиану ошибки 0,68 % шкалы, а хуже 1 % оказывается 32 % партии — каждый третий прибор. Для сравнения: 1 LSB 12-битного АЦП — 0,0244 % шкалы. Худший случай больше в 118 раз: разрядность канала съедена не шумом и не АЦП, а допусками деталей.

С датчиками ещё хуже. У встроенного датчика температуры F103 паспортное напряжение при 25 °C — от 1,34 до 1,52 В при наклоне 4,3 мВ/°C: разброс ±90 мВ — это ошибка до ±21 °C, если считать по типовому значению. VREFINT у F1 гуляет от 1,16 до 1,26 В — от −3,3 % до +5,0 % к типовым 1,20 В. Никакой заботливо выведенной формулой это не лечится: числа другие у каждого экземпляра.

Вывод из бюджета: точность серийного прибора определяется не схемой и не разрядностью, а тем, сравнили ли конкретный экземпляр с эталоном. Калибровка переносит точность с допусков деталей на точность эталона — за минуту на линии.

⚠️ Калибровка не лечит дрейф. Поправки сняты при одной температуре в один день. Уход опоры 100 ppm/°C за 50 °C — уже 0,5 %; старение и температурные ходы — отдельные строки бюджета. Если они больше допуска, нужна либо термокомпенсация, либо более стабильные детали, либо периодическая перекалибровка (межкалибровочный интервал).


202.2 Заводская калибровка внутри камня

Часть работы завод уже сделал: при выпуске кристалла в системную память записываются значения, снятые на измерительном оборудовании точнее любого самодельного стенда. Их адреса — главные константы этого раздела (сверено по заголовку stm32f4xx_ll_adc.h пака и документации обоих семейств):

Что лежит F103 F407 Условия съёма
уникальный номер кристалла, 96 бит 0x1FFF F7E8 0x1FFF 7A10 —
VREFINT_CAL — код VREFINT нет 0x1FFF 7A2A 30 °C, VREF+ = 3,3 В
TS_CAL1 — код датчика температуры нет 0x1FFF 7A2C 30 °C, VDDA = 3,3 В
TS_CAL2 — код датчика температуры нет 0x1FFF 7A2E 110 °C, VDDA = 3,3 В
OTP: 512 байт под свои данные нет 0x1FFF 7800 пишутся один раз
регистр размера Flash 0x1FFF F7E0 0x1FFF 7A22 —

У F1 заводских кодов АЦП нет вовсе — в заголовках и справочном руководстве только типовые значения из даташита, поэтому датчик температуры F1 без своей калибровки и врёт на десятки градусов (202.1). У F4 драйвер LL даёт готовые имена VREFINT_CAL_ADDR, TEMPSENSOR_CAL1_ADDR и TEMPSENSOR_CAL2_ADDR; в комментариях заголовка указан и разброс условий съёма: температура 30 ±5 °C, опора 3,3 В ±10 мВ.

Как пользоваться этими кодами — формулы и целочисленный пересчёт — уже разобрано в 14.9: питание из VREFINT_CAL, температура по двум точкам TS_CAL1/TS_CAL2. Здесь важно другое: заводские коды описывают только сам кристалл. Опора, снятая заводом, — это VREFINT; датчик — тот, что на кристалле. Делитель, шунт, усилитель и внешний датчик завод в глаза не видел — их разброс снимает только своя калибровка собранного изделия.

⚠️ Ячейку проверять перед употреблением. Незаписанная системная память читается как 0xFFFF. Встретили 0xFFFF в TS_CAL1 — значит, у этого кристалла кода нет, и остаётся типовое значение из даташита. И адреса у каждого семейства свои: таблица выше — про F103 и F407, для другого камня адрес ищут в его даташите, а не копируют отсюда.


202.3 Своя калибровка: одна точка, две точки, кривая

Модель lcal.c собирает «экземпляр» с типичным набором бед: опора на 1,2 % выше номинала, смещение +6 LSB, шум ±2 LSB. Пересчёт по номиналам код × 3300 / 4095 даёт на нём ошибку до 35 мВ.

Одна точка снимает только сдвиг: подали эталонные 1650 мВ, сравнили с показанием, разницу запомнили. Поправка вышла +15 мВ, но на краях шкалы осталось 20 мВ — весь наклон (ошибка опоры и делителя) как был, так и есть. Одной точки хватает, когда наклон задаётся точными деталями, а гуляет лишь смещение: тарирование весов (102), ноль датчика тока.

Две точки задают и сдвиг, и наклон — прямую именно этого экземпляра. Станция подала 330 и 2970 мВ — 10 и 90 % шкалы, — модель усреднила по 64 замера на точку и получила коды 411 и 3647. Дальше два коэффициента:

/* по двум точкам: эталон дал v1 и v2 (мВ), устройство — коды c1 и c2 */
static void cal_two_point(int32_t v1, int32_t c1, int32_t v2, int32_t c2, cal_rec_t *r)
{
    r->gain_q16  = (int32_t)((((int64_t)(v2 - v1)) << 16) / (c2 - c1));
    r->offset_mv = v1 - (int32_t)(((int64_t)c1 * r->gain_q16 + 32768) >> 16);
}

Для модельного экземпляра вышло gain_q16 = 53465 и сдвиг −5 мВ; остаток ошибки по всей шкале — 1 мВ: шум, квантование и округление. Из 35 мВ сделали 1 мВ двумя замерами.

График: идеальная прямая пересчёта одна на всех, у конкретного экземпляра свой наклон и сдвиг, две точки калибровки задают его собственную прямую, и ошибка неоткалиброванного пересчёта растёт к краю шкалы

Кривая нужна, когда физика нелинейна и прямая не укладывается в допуск: NTC-термистор (96.2), термопара. Тогда калибровка — не два коэффициента, а таблица с кусочно-линейным приближением между узлами; точек нужно столько, чтобы стрела прогиба между соседними узлами была меньше допуска. Порядок выбора всегда один: сначала прикинуть остаток после прямой — если он меньше допуска, кривая не нужна, две точки дешевле и на линии, и в прошивке.

🔧 Рецепт выбора. Смещение гуляет, наклон точный — одна точка. Гуляют опора, делитель, усиление — две точки на 10 и 90 % шкалы. Остаток после прямой больше допуска — таблица узлов. И в любом варианте каждая точка — среднее из десятков замеров, а не один (Грабли).


202.4 Эталон на линии

Калибровка пересаживает точность прибора на точность эталона, поэтому эталон обязан быть заметно точнее допуска изделия — иначе его ошибка просто перепишется в коэффициенты. Практическое правило — запас минимум в 4 раза, спокойный — в 10 (test accuracy ratio): для допуска ±0,5 % эталон нужен не хуже 0,05–0,125 %. Лабораторный источник с измерителем такого класса — обычная вещь; важно лишь, что и его самого поверяют по расписанию, и просроченная поверка эталона делает недостоверной калибровку всей партии после неё.

Рабочее место калибровки: эталон подаёт точное напряжение на вход изделия, станция командует по UART и получает коды АЦП, дальше шаги — две точки, расчёт коэффициентов, запись во Flash с CRC, контрольная точка и решение годен или брак

Станция — компьютер со сценарием: задать точку эталону, скомандовать изделию замер, забрать коды, посчитать коэффициенты, записать, проверить. В прошивке для этого достаточно сервисных команд по UART (16) — так же устроена консоль мини-проектов 196–198: «отдай сырой код канала», «прими коэффициенты», «покажи, что записано». Те же команды потом пригодятся в сервисе.

🔧 Контроль самой станции. Раз в смену через станцию прогоняют контрольный экземпляр — прибор, который никогда не уходит с линии (golden unit). Его показания записывают до и после — as-found и as-left. Пополз контрольный — значит, поплыла станция или эталон, и под подозрением всё, что калибровано со времени прошлого контрольного прогона. Это дешевле, чем узнать о том же от заказчика.


202.5 Куда писать коэффициенты

Коэффициенты — такая же собственность экземпляра, как серийный номер, и живут они не в прошивке, а в самом изделии. Формат записи — как у журнала настроек 11.8, но проще: калибровку пишут один раз при выпуске, износ и чередование страниц не нужны, а вот признак «запись есть и цела» — нужен обязательно:

typedef struct {
    uint32_t magic;      /* 'L''C''A''L' — страница не пуста */
    uint16_t version;    /* раскладка структуры */
    uint16_t count;      /* сколько каналов откалибровано */
    uint32_t uid[3];     /* копия 96-битного номера кристалла */
    int32_t  gain_q16;   /* мВ на код, Q16: 65536 = 1,0 */
    int32_t  offset_mv;  /* сдвиг после умножения */
    uint32_t crc;        /* CRC-32 всего, кроме этого поля */
} cal_rec_t;

Запись — 32 байта; CRC-32 — тот же многочлен 0xEDB88320, что в 11.8, у F4 её считает аппаратный блок (43.1). Прогон модели показывает все три защитных механизма разом:

запись: 32 байт, CRC-32 = 0xCBF8156D
испорчен один бит: CRC не сошлась — запись отвергнута
стёртая страница: magic 0xFFFFFFFF != 0x4C41434C и CRC не сошлась — калибровки нет
version=2 при ожидаемой 1: раскладка другая — запись не используется

Копия уникального номера — против тихой беды: прошивку вместе с «чужой» страницей калибровки клонировали на другую плату, CRC сходится, а числа — другого экземпляра. Сверка uid с 0x1FFF F7E8 (F1) или 0x1FFF 7A10 (F4) ловит это при старте.

Куда класть:

⚠️ Нет калибровки — это состояние, а не авария. Свежая плата до станции показывает magic 0xFFFFFFFF. Прошивка обязана жить и в этом состоянии: считать по номиналам, честно сообщать «не калибровано» в сервисной команде и не выдавать измерения за точные.


202.6 Проверка после калибровки

Записали коэффициенты — прибор ещё не годен. Годным его делает проверка в точке, которая в калибровке не участвовала: у модели это 1650 мВ, середина шкалы между точками 330 и 2970:

контрольная точка 1650 мВ: прибор показал 1650 мВ, ошибка +0 мВ (допуск ±5)

Допуск приёмки назначают уже допуска изделия — с защитной полосой на погрешность эталона и шум замера (guard band). Изделие обещает ±10 мВ, эталон и стенд дают ±2 мВ — принимать по ±8 мВ: тогда прошедший приёмку экземпляр гарантированно в допуске, даже если эталон в этот раз ошибся в свою худшую сторону.

Не прошёл — одна повторная калибровка, не больше. Экземпляр, который проходит со второго-третьего раза, не «наконец получился» — он нестабилен или неисправен, и завтра уйдёт снова. Дальше — не подгонка, а разбор: почти всегда за «браком по калибровке» стоит не разброс, а дефект — не тот номинал в делителе, непропай, перепутанный канал. На распределении as-found по партии это видно сразу: разброс собирается в колокол, дефекты торчат выбросами.

🔧 Протокол на каждый экземпляр. Станция пишет в файл по серийному номеру: дату, as-found, коды точек, коэффициенты, as-left, вердикт. Партия таких протоколов — бесплатная статистика: медиана ползёт от партии к партии — меняется поставка деталей; растёт хвост выбросов — ищите дефект монтажа, пока он не дошёл до заказчика.


202.7 Пересчёт в прошивке

Станция считала прямую как хотела, а прибору с ней жить в каждом замере. Правильное разделение труда: всё деление — один раз на станции (или один раз при приёме коэффициентов), в рабочем цикле — только умножение и сдвиг:

/* в приборе: только умножение и сдвиг, деление осталось на станции */
static int32_t cal_apply(uint16_t code, const cal_rec_t *r)
{
    return (int32_t)(((int64_t)code * r->gain_q16 + 32768) >> 16) + r->offset_mv;
}

Множитель — в формате Q16 (69.9): 65536 означает 1,0, наш 53465 — это 0,8158 мВ на код. Слагаемое 32768 — половина делителя сдвига: округление к ближайшему, а не отбрасывание (64.8). Произведение 4095 × 65536 укладывается и в 32 бита, но int64_t снимает вопрос при любом множителе — у Cortex-M3 и M4 это одна команда умножения 32×32→64.

Плавающая точка здесь не нужна: у F1 аппаратного FPU нет вовсе (69.6), а float с его семью цифрами (69.2) не добавит точности прямой из двух целочисленных коэффициентов. Модель прогнала оба варианта по всем 4096 кодам:

целые против double по всем 4096 кодам: расхождение не больше 1 мВ

Расхождение — 1 мВ, и это округление на границах, а не систематика. Целочисленный путь при этом воспроизводим бит в бит на любом компиляторе — для калибровки это отдельное достоинство: станция может заранее посчитать, что обязан показать прибор, и сверить дословно.


202.8 Грабли

Откалибровали, перепрошили — прибор снова врёт. Обновление залили с полным стиранием, страница коэффициентов ушла вместе со старой прошивкой. Страницу калибровки исключают из образа и стирают только адресно; у F4 надёжнее всего OTP — Mass Erase её не трогает.

Две точки сняли, а край шкалы всё равно мимо. Точки взяли рядом и по одному замеру: в модели точки 300 и 600 мВ, снятые по одному разу, дали на краю шкалы до 17 мВ — шум точек растянулся на всю длину прямой. Точки — на 10 и 90 % шкалы, каждая — среднее из 64 замеров: остаток 1 мВ.

На линии годен, у заказчика на морозе врёт. Калибровка снята при комнатной температуре и дрейф не лечит: одна только опора со 100 ppm/°C на размахе 50 °C уходит на 0,5 %. Либо детали стабильнее, либо термокомпенсация по второй измеренной величине, либо честный допуск.

Каждый десятый экземпляр — «брак по калибровке». Это не разброс: разброс уже посчитан в бюджете и в допуск заложен. Выбросы на распределении as-found — дефекты: не тот номинал, непропай, обрыв. Их не перекалибровывают, их чинят.

Заводские коды прочитали — температура фантастическая. Либо адрес от другого семейства (у F103 кодов нет вовсе, у F407 — 0x1FFF 7A2A…2E), либо ячейка пуста и читается 0xFFFF, либо опора в приборе не 3,3 В, при которых завод снимал коды, — тогда сначала пересчёт опоры по 14.9.

CRC сходится, а показания чужие. Страницу калибровки склонировали вместе с прошивкой на другую плату. Запись обязана нести копию уникального номера кристалла (0x1FFF F7E8 у F1, 0x1FFF 7A10 у F4), и прошивка сверяет её при старте.


202.9 Итоги

Полные исходники

lcal.c — модель калибровки экземпляра на линии.

/* Раздел 202: модель калибровки экземпляра на линии.
 *
 * gcc -std=c11 -Wall -Wextra -O2 lcal.c -o lcal && ./lcal
 *
 * «Изделие» — канал 0…3300 мВ с 12-битным АЦП, у которого опора на +1,2 %
 * выше номинала, смещение +6 LSB и шум ±2 LSB. «Эталон» подаёт точное
 * напряжение. Сравниваются: пересчёт без калибровки, одна точка, две точки;
 * целочисленный пересчёт против плавающей точки; запись коэффициентов
 * с CRC-32, версией и копией уникального номера кристалла. */
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* ── модель экземпляра ────────────────────────────────────────────── */
#define VREF_REAL_MV  3340.0     /* опора на +1,2 % выше 3300 */
#define OFF_LSB       6          /* смещение канала, LSB */
#define NOISE_LSB     2          /* шум ±2 LSB */

static uint32_t lcg = 12345u;
static int32_t noise(void)                       /* равномерный ±NOISE_LSB */
{
    lcg = lcg * 1664525u + 1013904223u;
    return (int32_t)(lcg >> 16) % (2 * NOISE_LSB + 1) - NOISE_LSB;
}

static int32_t adc_code(double v_mv, int with_noise)
{
    int32_t c = (int32_t)(v_mv / VREF_REAL_MV * 4095.0 + 0.5) + OFF_LSB;
    if (with_noise)
        c += noise();
    if (c < 0) c = 0;
    if (c > 4095) c = 4095;
    return c;
}

/* среднее из 64 замеров — так станция снимает точку калибровки */
static int32_t adc_avg64(double v_mv)
{
    int32_t s = 0;
    for (int i = 0; i < 64; i++)
        s += adc_code(v_mv, 1);
    return (s + 32) / 64;
}

/* ── коэффициенты и их применение (то, что попадает в прошивку) ───── */
typedef struct {
    uint32_t magic;      /* 'L''C''A''L' — страница не пуста */
    uint16_t version;    /* раскладка структуры */
    uint16_t count;      /* сколько каналов откалибровано */
    uint32_t uid[3];     /* копия 96-битного номера кристалла */
    int32_t  gain_q16;   /* мВ на код, Q16: 65536 = 1,0 */
    int32_t  offset_mv;  /* сдвиг после умножения */
    uint32_t crc;        /* CRC-32 всего, кроме этого поля */
} cal_rec_t;

/* по двум точкам: эталон дал v1 и v2 (мВ), устройство — коды c1 и c2 */
static void cal_two_point(int32_t v1, int32_t c1, int32_t v2, int32_t c2, cal_rec_t *r)
{
    r->gain_q16  = (int32_t)((((int64_t)(v2 - v1)) << 16) / (c2 - c1));
    r->offset_mv = v1 - (int32_t)(((int64_t)c1 * r->gain_q16 + 32768) >> 16);
}

/* в приборе: только умножение и сдвиг, деление осталось на станции */
static int32_t cal_apply(uint16_t code, const cal_rec_t *r)
{
    return (int32_t)(((int64_t)code * r->gain_q16 + 32768) >> 16) + r->offset_mv;
}

/* наивный пересчёт «по номиналам» и одна точка (только сдвиг) */
static int32_t mv_naive(uint16_t code)
{
    return (int32_t)(((uint32_t)code * 3300u + 2047u) / 4095u);
}

/* ── CRC-32, тот же многочлен 0xEDB88320, что в 11.8 и 43.1 ───────── */
static uint32_t crc32_buf(const void *p, size_t n)
{
    const uint8_t *b = p;
    uint32_t crc = 0xFFFFFFFFu;
    while (n--) {
        crc ^= *b++;
        for (int i = 0; i < 8; i++)
            crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u)));
    }
    return ~crc;
}

static double max_err(int32_t (*conv)(uint16_t, const cal_rec_t *), const cal_rec_t *r)
{
    double worst = 0;
    for (int v = 0; v <= 3300; v++) {
        int32_t mv = conv((uint16_t)adc_code(v, 0), r);
        double e = mv > v ? mv - v : v - mv;
        if (e > worst)
            worst = e;
    }
    return worst;
}

static int32_t conv_naive(uint16_t c, const cal_rec_t *r) { (void)r; return mv_naive(c); }
static int32_t one_off;
static int32_t conv_one(uint16_t c, const cal_rec_t *r) { (void)r; return mv_naive(c) + one_off; }

int main(void)
{
    cal_rec_t rec = { 0x4C41434Cu, 1, 1, {0x11223344u, 0x55667788u, 0x99AABBCCu}, 0, 0, 0 };

    /* без калибровки */
    printf("без калибровки: наибольшая ошибка %.0f мВ\n", max_err(conv_naive, &rec));

    /* одна точка: эталон 1650 мВ, убрали только сдвиг */
    one_off = 1650 - mv_naive((uint16_t)adc_avg64(1650.0));
    printf("одна точка (1650 мВ): поправка %+d мВ, осталось %.0f мВ\n",
           one_off, max_err(conv_one, &rec));

    /* две точки на 10 %% и 90 %% шкалы, каждая — среднее из 64 замеров */
    int32_t v1 = 330, v2 = 2970;
    int32_t c1 = adc_avg64(v1), c2 = adc_avg64(v2);
    cal_two_point(v1, c1, v2, c2, &rec);
    printf("две точки (%d/%d мВ): коды %d/%d, gain_q16=%d offset=%d, осталось %.0f мВ\n",
           v1, v2, c1, c2, rec.gain_q16, rec.offset_mv, max_err(cal_apply, &rec));

    /* целые против плавающей точки: та же прямая на double */
    double gf = (double)(v2 - v1) / (c2 - c1), bf = v1 - c1 * gf;
    int32_t dmax = 0;
    for (int c = 0; c <= 4095; c++) {
        int32_t mi = cal_apply((uint16_t)c, &rec);
        int32_t mf = (int32_t)(gf * c + bf + (gf * c + bf >= 0 ? 0.5 : -0.5));
        int32_t d = abs(mi - mf);
        if (d > dmax)
            dmax = d;
    }
    printf("целые против double по всем 4096 кодам: расхождение не больше %d мВ\n", dmax);

    /* плохая калибровка: точки рядом (300 и 600 мВ) и по одному замеру */
    cal_rec_t bad;
    lcg = 7u;
    cal_two_point(300, adc_code(300.0, 1), 600, adc_code(600.0, 1), &bad);
    printf("точки рядом (300/600 мВ), по одному замеру: на шкале до %.0f мВ\n",
           max_err(cal_apply, &bad));

    /* приёмка в третьей точке */
    int32_t chk = cal_apply((uint16_t)adc_avg64(1650.0), &rec);
    printf("контрольная точка 1650 мВ: прибор показал %d мВ, ошибка %+d мВ (допуск ±5)\n",
           chk, chk - 1650);

    /* запись: размер, CRC, порча, стёртая страница */
    rec.crc = crc32_buf(&rec, offsetof(cal_rec_t, crc));
    printf("запись: %u байт, CRC-32 = 0x%08X\n", (unsigned)sizeof rec, rec.crc);

    cal_rec_t bit = rec;
    ((uint8_t *)&bit)[21] ^= 0x02;               /* один бит в gain_q16 */
    printf("испорчен один бит: CRC %s\n",
           crc32_buf(&bit, offsetof(cal_rec_t, crc)) == bit.crc ? "сошлась (ПЛОХО)" : "не сошлась — запись отвергнута");

    cal_rec_t blank;
    memset(&blank, 0xFF, sizeof blank);          /* стёртая Flash — все единицы */
    printf("стёртая страница: magic 0x%08X != 0x%08X и CRC не сошлась — калибровки нет\n",
           blank.magic, rec.magic);

    cal_rec_t old = rec;
    old.version = 2;
    old.crc = crc32_buf(&old, offsetof(cal_rec_t, crc));
    printf("version=%u при ожидаемой %u: раскладка другая — запись не используется\n",
           old.version, rec.version);
    return 0;
}

Дальше: раздел 203 — ЭМС изделия: помехи, стойкость и проверка в мастерской.

Открыть в интерактивном справочнике