Embedded HandbookПриложения
Все разделы › XXI. Аналоговые узлы
STM32F103, STM32G474 · уровень средний · 21 мин чтения
Открыть в интерактивном справочнике

176а. Токовая петля 4–20 мА: приём датчика и выход на ПЛК

Раздел 176 разобрал, как снять малую разность напряжений рядом с платой. Промышленный датчик давления или температуры обычно стоит далеко — в цехе, на трубе, за сотни метров от шкафа, — и отдаёт показание не напряжением, а током: 4 мА — начало шкалы, 20 мА — конец. Здесь — почему ток, как принять его на АЦП STM32 и не сжечь вход, сколько кабеля выдержит петля на 24 В и как самому выдать 4–20 мА на вход ПЛК.

Опоры: даташиты STM32F103x8/xB (DocID13587, в паке Keil — файл CD00161566.pdf) и STM32G474 (DS12288), справочное руководство RM0440. Все расчёты — модель на ПК _verify/loop176a; преобразователь напряжение–ток прогнан в ngspice на макромодели ОУ из раздела 164; код приёма собран armclang для F103 и G474 и прогнан на ПК на всех возможных суммах окна. Пороги обрыва и КЗ (3,6 и 21 мА) взяты для примера: стандарта с допусками на диске нет.

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

Оригинал Перевод
4–20 mA current loop токовая петля 4–20 мА
live zero живой ноль: начало шкалы — не нулевой ток
two-wire (loop-powered) transmitter двухпроводный датчик, питается от самой петли
shunt (burden) resistor шунт приёмника
loop supply, compliance voltage питание петли; напряжение, до которого источник держит ток
voltage-to-current converter преобразователь напряжение–ток
Howland current source источник тока по схеме Хауленда
injected current (IINJ) ток инжекции в вывод

176а.1 Почему ток, а не напряжение

В последовательной цепи ток один и тот же в каждой точке. Датчик задаёт ток, приёмник меряет его на своём шунте, а сопротивление проводов между ними в результат не входит — пока у источника питания хватает напряжения его протолкнуть (176а.4).

С напряжением так не выходит. Датчик 0–10 В берёт ток на своё питание, и этот ток возвращается по общему проводу. Жила 0,5 мм² длиной 200 м — это 7,00 Ом; ток питания 10 мА (пример) поднимает на ней землю датчика на 70,0 мВ, то есть 0,70 % шкалы 10 В. На 1000 м — 35,00 Ом, 350,0 мВ и 3,50 %. Сюда же добавятся наводки: у входа по напряжению сопротивление велико, и помеха на длинном проводе ложится прямо в сигнал.

Второе достоинство — живой ноль. Начало шкалы — 4 мА, а не 0. Нулевой ток значит не «давление ноль», а «петля оборвана». Живой ноль ещё и питает двухпроводный датчик: на свою электронику он тратит меньше 4 мА — то, что течёт при минимуме шкалы. Прошивка ловит два отказа по току: ниже 3,6 мА — обрыв или авария датчика, выше 21 мА — замыкание линии или авария (пороги здесь — пример; бери их из паспорта датчика или требований заказчика).

Поверх постоянного тока петли протокол HART передаёт цифровые данные частотной модуляцией; здесь он не разбирается.


176а.2 Шунт и защита входа

Приёмник — резистор, по которому течёт ток петли. Напряжение на нём меряет АЦП. Классический шунт 250 Ом даёт 1–5 В — но это не для STM32:

Шунт 4 мА 20 мА 21 мА Предел АЦП Вес кода Кодов на 4–20 мА
250 Ом 1,00 В 5,00 В 5,250 В выше предела вывода 4,0 В — —
150 Ом, опора 3,3 В (F103) 0,60 В 3,00 В 3,150 В 22,0 мА 5,372 мкА 2978
100 Ом, опора 2,5 В (G474) 0,40 В 2,00 В 2,100 В 25,0 мА 6,105 мкА 2621

У F103 предел напряжения на выводе, не толерантном к 5 В, — 4,0 В (таблица 6 даташита), а PA0 как раз такой. Шунт 250 Ом можно оставить и поделить его напряжение (раздел 162), но проще сразу взять шунт под опору: 150 Ом при 3,3 В оставляют запас до 22,0 мА — порог КЗ 21 мА ещё измеряется. У G474 с внутренней опорой VREFBUF 2,5 В подходит 100 Ом.

Что шунт рассеивает: при 22 мА — 72,6 мВт. Но если датчик пробило и 24 В легли прямо на шунт, через 150 Ом пойдёт 160 мА и 3,84 Вт — резистор сгорит. Поэтому в петле стоит ограничитель тока: самовосстанавливающийся предохранитель (раздел 173) или ограничитель на транзисторе.

Между шунтом и выводом — резистор R1 и два диода Шоттки: к VDDA и к земле. Резистор считается, как в 62.3, по худшему случаю — 24 В на входе вместо сигнала:

R1 Ток в диод при 24 В Мощность в R1 Ошибка от утечки вывода ±1 мкА Срез RC с 1 мкФ
1 кОм 20,70 мА — больше 5 мА 0,428 Вт 6,7 мкА (0,04 % шкалы) 159,2 Гц
4,7 кОм 4,40 мА 0,091 Вт 31,3 мкА (0,20 % шкалы) 33,9 Гц
10 кОм 2,07 мА 0,043 Вт 66,7 мкА (0,42 % шкалы) 15,9 Гц

Предел тока в вывод у F103 — ±5 мА (таблица 7), утечка обычного вывода — ±1 мкА (таблица 35). Утечка течёт через R1, и её падение АЦП не отличит от сигнала (62.10): чем больше резистор, тем больше ошибка. 4,7 кОм — середина. Конденсатор 1 мкФ за резистором — фильтр и заодно запас заряда для выборки: ёмкость выборки АЦП F103 — 8 пФ.

Приём двухпроводного датчика на F103: источник 24 В, ограничитель тока, датчик и линия в последовательной петле, шунт 150 Ом на землю, дальше резистор 4,7 кОм, конденсатор 1 мкФ и два диода Шоттки к VDDA и к земле перед выводом PA0; внизу полоса бюджета напряжения петли при 22 мА и 21,6 В: 3,3 В на шунте, 12 В датчику, 6,3 В на линию, то есть 286 Ом кабеля


176а.3 Фильтр, окно и бюджет ошибки

Длинная линия ловит наводку 50 Гц. Пусть это 1 мА амплитуды (пример). RC-цепь 4,7 кОм / 1 мкФ пропускает на 50 Гц долю 0,561 — остаётся 560,8 мкА, 3,5 % шкалы. Спасает усреднение по окну, кратному периоду сети:

Окно (измерения через 1 мс) Частота сети Остаток наводки
1 50 Гц 560,8 мкА (3,505 %)
16 50 Гц 131,7 мкА (0,823 %)
20 50 Гц 0,0 мкА
20 49 Гц 11,6 мкА (0,073 %)
20 51 Гц 10,9 мкА (0,068 %)

Двадцать измерений через 1 мс — ровно период 50 Гц, и наводка гасится целиком; при уходе сети на герц остаётся меньше 0,1 %. Окно в 16 измерений — соблазн «делить сдвигом» — оставляет 0,823 %.

Теперь худший случай для постоянной ошибки на 20 мА. Шунт 0,1 % с ТКС 25 ppm/K, плата греется на 60 K (пример), резистор R1 4,7 кОм:

Составляющая, мкА на 20 мА F103: 150 Ом, опора VDDA 3,3 В G474: 100 Ом, VREFBUF 2,5 В
Допуск шунта 20,0 20,0
ТКС шунта 30,0 30,0
Допуск опоры не учтён — это питание 32,0
Дрейф опоры не учтён — это питание 120,0
Полная ошибка АЦП 26,9 (±5 МЗР) 48,8 (±8 МЗР)
из неё нелинейность 16,1 (±3 МЗР) 26,9 (±4,4 МЗР)
Утечка вывода через R1 31,3 (±1 мкА) 4,7 (±100 нА)
До калибровки 108,2 (0,68 % шкалы) 255,5 (1,60 % шкалы)
После калибровки по двум точкам 77,5 (0,48 %) 181,6 (1,13 %)

Числа из даташитов: F103 — таблица 49 (±5 и ±3 МЗР) и таблица 35; G474 — таблица 69 (медленный канал на полной скорости, ±8 и ±4,4 МЗР), таблица 54 (±100 нА), таблица 78 (VREFBUF 2,496–2,504 В, дрейф — дрейф VREFINT плюс 50 ppm/°C, у VREFINT — до 50 ppm/°C). Калибровка при одной температуре убирает допуски и смещения, но не дрейф, не нелинейность и не утечку — она меняется с нагревом.

У F103 в корпусах LQFP опора АЦП — это VDDA, и её ошибка идёт в результат один к одному: 1 % питания — 200 мкА на 20 мА, больше всего бюджета. Питание придётся либо мерить через VREFINT (14.9), либо калибровать вместе с каналом. У G474 главный член — дрейф VREFBUF: внешний источник опоры 20 ppm/°C дал бы 24,0 мкА вместо 120,0 (раздел 177).

Калибровку по двум точкам (202.3) модель проверила на экземпляре с отклонениями: шунт +0,08 %, опора −0,7 %, смещение АЦП +3 МЗР и прогиб 1,5 МЗР посередине шкалы. По номиналам этот экземпляр ошибается на 51, 85, 120, 149 и 173 мкА в точках 4, 8, 12, 16 и 20 мА. После калибровки на 4 и 20 мА — 0, 4, 8, 7 и 0 мкА: остаётся только прогиб.


176а.4 Двухпроводный датчик: бюджет напряжения петли

Двухпроводный датчик питается от той же петли, которую регулирует. Ему нужно минимальное напряжение на выводах — оно есть в паспорте, здесь возьмём 12 В (пример). Источник 24 В должен протолкнуть максимальный ток через шунт, линию и датчик. Считаем на 22 мА — чтобы прошёл и ток аварии выше 21 мА — и на источник 24 В и 21,6 В (−10 %). Медь — 0,0175 Ом·мм²/м, жил две, туда и обратно:

Источник Шунт На шунте Остаётся линии Линия не больше Кабель 0,5 мм² Кабель 0,2 мм²
24,0 В 150 Ом 3,3 В 8,7 В 395,5 Ом 5649 м 2260 м
24,0 В 250 Ом 5,5 В 6,5 В 295,5 Ом 4221 м 1688 м
21,6 В 150 Ом 3,3 В 6,3 В 286,4 Ом 4091 м 1636 м
21,6 В 250 Ом 5,5 В 4,1 В 186,4 Ом 2662 м 1065 м

Длина — это длина трассы: сопротивление двух жил 2·0,0175·L / S. Всё, что стоит в петле последовательно, съедает ту же полосу: диод от переполюсовки и ограничитель (раздел 173), вместе 1 В (пример), оставляют при 21,6 В и 150 Ом 5,3 В, то есть 240,9 Ом — 3442 м при 0,5 мм² и 1377 м при 0,2 мм². Меньший шунт, 150 Ом вместо 250, — это не только АЦП 3,3 В, но и 1429 м лишнего кабеля 0,5 мм² при 21,6 В.

Если бюджета не хватает, датчик не отказывает явно, а просто упирается. Кабель 6000 м при 0,5 мм² — это 420 Ом, и при 21,6 В ток выше 16,8 мА в петлю не пройдёт: верх шкалы срезан, а сигнал аварии 21 мА не дойдёт никогда.


176а.5 Код приёма: окно, микроамперы, обрыв

Код разделён на две части. Переносимая loop_rx.c без регистров считает микроамперы, инженерные единицы и состояние петли — её собирают и armclang для обоих камней, и gcc на ПК для проверки. Регистровая часть для F103 меряет раз в 1 мс и складывает окно из 20 измерений.

Параметры и проверки на этапе сборки:

#define LOOP_N          20u     /* измерений в окне: 20 × 1 мс = ровно период 50 Гц */
#define LOOP_SHUNT_OHM  150u    /* шунт под опору 3,3 В: 20 мА → 3,0 В */
#define LOOP_VREF_MV    3300u   /* у F103 в LQFP опора — это VDDA */
#define LOOP_LO_UA      3600u   /* ниже — обрыв или авария датчика (порог для примера) */
#define LOOP_HI_UA      21000u  /* выше — КЗ линии или авария датчика (порог для примера) */
#define LOOP_DEBOUNCE   3u      /* столько окон подряд до смены состояния: 60 мс */
_Static_assert(LOOP_HI_UA * LOOP_SHUNT_OHM < LOOP_VREF_MV * 1000u,
               "порог КЗ должен лежать ниже предела АЦП: I * R < Vref");
_Static_assert(LOOP_LO_UA < 4000u && LOOP_HI_UA > 20000u, "пороги аварии - вне шкалы 4-20 мА");
_Static_assert(LOOP_N * 4095ull <= 0xFFFFFFFFull, "сумма окна не помещается в 32 бита");

Первая проверка сверяет шунт с опорой: порог КЗ 21 мА должен лежать ниже предела АЦП. Поставь в заголовок шунт 250 Ом — и сборка остановится с этим сообщением, а не выдаст на плате «насыщенный» ток.

Пересчёт суммы окна в микроамперы — прямая через две точки калибровки, в целых и с округлением к ближайшему; инженерные единицы — вторая прямая:

int32_t loop_ua(uint32_t sum, const loop_cal_t *c)
{
    int64_t num = ((int64_t)sum - (int64_t)c->s4) * 16000;
    int64_t den = (int64_t)c->s20 - (int64_t)c->s4;
    num += (num >= 0 ? den : -den) / 2;                            /* к ближайшему */
    return 4000 + (int32_t)(num / den);
}

/* микроамперы -> инженерные единицы шкалы lo…hi; вне 4–20 мА — край шкалы */
int32_t loop_eng(int32_t ua, int32_t lo, int32_t hi)
{
    if (ua < 4000) ua = 4000;
    if (ua > 20000) ua = 20000;
    return lo + (int32_t)(((int64_t)(ua - 4000) * (hi - lo) + 8000) / 16000);
}

Диагностика не верит одному окну: состояние меняется, только если новое держится три окна подряд — 60 мс:

loop_state_t loop_diag(loop_diag_t *d, int32_t ua)
{
    loop_state_t now = ua < (int32_t)LOOP_LO_UA ? LOOP_LOW
                     : ua > (int32_t)LOOP_HI_UA ? LOOP_HIGH : LOOP_OK;
    if (now == d->state) {
        d->count = 0;
    } else if (now != d->pending || d->count == 0) {
        d->pending = now;
        d->count = 1;
    } else if (++d->count >= LOOP_DEBOUNCE) {
        d->state = now;
        d->count = 0;
    }
    return d->state;
}

Измерение на F103: АЦП на 12 МГц, выборка 239,5 такта — одно измерение занимает 21 мкс, раз в миллисекунду из SysTick:

void SysTick_Handler(void)                  /* раз в 1 мс: одно измерение, 21 мкс */
{
    ADC1->CR2 |= ADC_CR2_ADON;
    while (!(ADC1->SR & ADC_SR_EOC)) { }
    acc += ADC1->DR;
    if (++cnt == LOOP_N) {                  /* 20 мс — окно кратно периоду 50 Гц */
        window_sum = acc;
        acc = 0;
        cnt = 0;
        ready = 1;
    }
}

Прогон на ПК (loop_host.c + тот же loop_rx.c) прошёл все 81901 суммы окна от 0 до 20 × 4095 и совпал с моделью на Python в 81901 из 81901. Номинальные суммы окна — 14891 на 4 мА и 74455 на 20 мА; сумма ниже 13401 даёт состояние «низ», выше 78179 — «верх»; полная шкала АЦП читается как 22000 мкА. На последовательности окон обрыв (0 мА) признан на третьем окне, два окна по 22,5 мА подряд пропущены как выброс, три — признаны КЗ.

Главное правило вызова: loop_eng прижимает ток к шкале, и при обрыве вернёт ноль давления. Сначала смотри состояние, потом значение.


176а.6 Выход 4–20 мА от МК

Обратная задача — своё устройство отдаёт показание на вход ПЛК. Нужны напряжение уставки и преобразователь напряжение–ток. Простейший — сток тока из 15.6: ОУ держит на резисторе уставки 100 Ом напряжение ЦАП, транзистор пропускает этот ток, нагрузка — вход ПЛК между +24 В и транзистором. Уставка 0,4 В — 4 мА, 2,0 В — 20 мА.

Модель в ngspice при 24 В и нагрузке 250 Ом, уставка 2,0 В:

Выходной каскад Ток Ошибка
NPN, усиление по току 50 19,61 мА −1,96 %
NPN, усиление 100 19,80 мА −0,99 %
NPN, усиление 300 19,93 мА −0,33 %
n-МОП 20,00 мА —
n-МОП, смещение ОУ 1 мВ 20,01 мА +10 мкА

Резистор уставки меряет ток эмиттера, а в нагрузку идёт ток коллектора: ток базы теряется. У МОП-транзистора ток затвора нулевой, и остаётся только смещение ОУ — каждый милливольт на 100 Ом даёт 10 мкА. От нагрузки ток не зависит: от 1 до 1000 Ом он тот же; при 1100 Ом каскад на МОП ещё держит 20,00 мА, при 1200 Ом — только 18,46 мА: 24 В не хватает.

Сток годится, когда вход ПЛК не привязан к земле нашей платы. Если вход ПЛК заземлён одним концом, нужен источник тока в нагрузку на землю — улучшенная схема Хауленда (четыре резистора 10 кОм и датчик тока 100 Ом) — или изоляция (раздел 54). Хауленд чувствителен к равенству резисторов:

Резисторы Нагрузка 1 Ом 250 Ом 1000 Ом
точные 20,00 мА 20,00 мА 20,00 мА
один на 0,1 % больше 20,01 мА 20,03 мА 20,11 мА
один на 1 % больше 20,10 мА 20,35 мА 21,16 мА

При разбросе 1 % ток плывёт с нагрузкой больше чем на миллиампер — такой Хауленд собирают только на согласованных сборках резисторов.

F103 без ЦАП — ШИМ и RC. У F103C8 ЦАП нет (15.1). ШИМ 72 МГц / 4096 — 17578 Гц, шаг 0,806 мВ, то есть 8,06 мкА на 100 Ом; 4 мА — скважность 12,1 %, 20 мА — 60,6 %. Одна RC-цепь 10 кОм / 1 мкФ оставляет пульсацию 4,69 мВ от пика до пика — 46,9 мкА, 0,29 % шкалы; две развязанные цепи с теми же номиналами — не больше 3,62 мкВ. Цена — время: установление до 0,1 % занимает 69 мс с одной цепью и 92,3 мс с двумя. И ШИМ качается между землёй и питанием вывода: ошибка питания переходит в ток один к одному.

G474 — ЦАП и VREFBUF. С опорой 2,5 В и резистором уставки 100 Ом шаг — 6,11 мкА; коды: 590 для 3,6 мА, 655 для 4 мА, 3276 для 20 мА, 3604 для 22 мА.

#define TX_RSET_OHM  100u                   /* резистор уставки: 2,0 В -> 20 мА */
#define TX_VREF_MV   2500u                  /* VREFBUF, VRS = 01 */
#define TX_MAX_UA    22000u                 /* ток верхней аварии тоже должен пройти */

_Static_assert(TX_MAX_UA * TX_RSET_OHM < TX_VREF_MV * 1000u, "уставка на максимум тока выше опоры ЦАП");

/* код ЦАП для тока: U = I * Rset, код = U * 4095 / Vref, с округлением */
uint32_t tx_code(uint32_t ua)
{
    if (ua > TX_MAX_UA)
        ua = TX_MAX_UA;
    return (uint32_t)(((uint64_t)ua * TX_RSET_OHM * 4095u + TX_VREF_MV * 500u) / (TX_VREF_MV * 1000u));
}

Вход «+» ОУ высокоомный, поэтому выходной буфер ЦАП выключен (MODE1 = 010): полная ошибка ЦАП без буфера — ±12 МЗР, с буфером — ±30 МЗР (таблица 75). Для Хауленда так нельзя: его вход — резистор 10 кОм, а выходное сопротивление ЦАП без буфера — 9,6–13,8 кОм (таблица 74). VREFBUF на 2,5 В требует VDDA не ниже 2,8 В (таблица 78), а поле VRS меняют только при выключенном буфере — поэтому в tx_init две записи.

Бюджет выхода на 20 мА, мкА: ЦАП без буфера — 73,3 (с буфером было бы 183,2), допуск VREFBUF — 32,0, его дрейф на 60 K — 120,0, допуск резистора уставки 0,1 % — 20,0, его ТКС — 30,0, смещение ОУ 1 мВ — 10,0. Всего 285,3 мкА, 1,78 % шкалы. Калибровка по двум точкам с миллиамперметром на выходе (202.4) снимает всё, кроме дрейфа: 150,0 мкА, 0,94 %.


176а.7 Грабли

Обрыв показан как ноль давления. loop_eng прижимает ток ниже 4 мА к началу шкалы — ноль тока превращается в «давление 0». Перед значением всегда проверяй состояние loop_diag.

Шунт 250 Ом на АЦП 3,3 В. На 20 мА — 5,00 В, выше предела вывода F103 4,0 В. Шунт выбирают под опору; _Static_assert в loop_rx.c не даст собрать несогласованную пару.

Окно из 16 измерений. Сдвиг вместо деления удобен, но 16 мс — не период сети: остаётся 0,823 % наводки против нуля у окна из 20.

Верх шкалы срезан, и никто не видит. На длинной линии или просевшем источнике датчик упирается ниже 20 мА (16,8 мА на 6 км) — показание выглядит правдоподобно. Бюджет петли считают на худшее питание, 22 мА и со всеми последовательными элементами.

Шунт сгорел вместе с датчиком. Пробитый датчик кладёт 24 В на шунт: 160 мА и 3,84 Вт через 150 Ом. Без ограничителя тока в петле так и будет.

Диод Шоттки поднимает VDDA. При 24 В на входе через R1 4,7 кОм в шину VDDA уходит 4,40 мА. Если сама шина потребляет меньше, её напряжение растёт — а у F103 это опора, и сдвигаются все каналы. Стабилитрон или ограничитель на шине, либо диод на отдельную шину.

Земля петли замкнута в двух местах. Шунт стоит в обратном проводе, и минус петли — это земля платы. Если его заземлили ещё и у датчика или в шкафу, по земле течёт посторонний ток. Тогда развязка (раздел 54).

Выход на NPN недодаёт ток. С усилением 50 теряется 1,96 % — ток базы не проходит через нагрузку. МОП-транзистор снимает вопрос.

Хауленд на резисторах 1 %. Ток зависит от нагрузки: 20,10 мА на 1 Ом и 21,16 мА на 1000 Ом. И ЦАП без буфера на его входе нельзя — делитель с резистором 10 кОм.


176а.8 Итоги


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

loop_rx.h — приём 4–20 мА: пересчёт, инженерные единицы, диагностика — объявления.

/* 176а: приём 4–20 мА — пересчёт суммы окна в микроамперы, инженерные единицы, диагностика.
 * Без регистров: этот же файл собирается armclang для F103 и G474 и gcc на ПК для проверки. */
#ifndef LOOP_RX_H
#define LOOP_RX_H
#include <stdint.h>

#define LOOP_N          20u     /* измерений в окне: 20 × 1 мс = ровно период 50 Гц */
#define LOOP_SHUNT_OHM  150u    /* шунт под опору 3,3 В: 20 мА → 3,0 В */
#define LOOP_VREF_MV    3300u   /* у F103 в LQFP опора — это VDDA */
#define LOOP_LO_UA      3600u   /* ниже — обрыв или авария датчика (порог для примера) */
#define LOOP_HI_UA      21000u  /* выше — КЗ линии или авария датчика (порог для примера) */
#define LOOP_DEBOUNCE   3u      /* столько окон подряд до смены состояния: 60 мс */

typedef struct { uint32_t s4, s20; } loop_cal_t;          /* суммы окна на 4 и 20 мА */
typedef enum { LOOP_OK, LOOP_LOW, LOOP_HIGH } loop_state_t;
typedef struct { loop_state_t state, pending; uint8_t count; } loop_diag_t;

loop_cal_t   loop_cal_nominal(void);
int32_t      loop_ua(uint32_t sum, const loop_cal_t *c);
int32_t      loop_eng(int32_t ua, int32_t lo, int32_t hi);
loop_state_t loop_diag(loop_diag_t *d, int32_t ua);

#endif

loop_rx.c — приём 4–20 мА — переносимая часть.

/* 176а: приём 4–20 мА — переносимая часть (см. loop_rx.h) */
#include "loop_rx.h"

_Static_assert(LOOP_HI_UA * LOOP_SHUNT_OHM < LOOP_VREF_MV * 1000u,
               "порог КЗ должен лежать ниже предела АЦП: I * R < Vref");
_Static_assert(LOOP_LO_UA < 4000u && LOOP_HI_UA > 20000u, "пороги аварии - вне шкалы 4-20 мА");
_Static_assert(LOOP_N * 4095ull <= 0xFFFFFFFFull, "сумма окна не помещается в 32 бита");

/* сумма окна для тока ua по номиналам: N * I * R * 4095 / Vref, с округлением */
static uint32_t nominal_sum(uint32_t ua)
{
    uint64_t uv = (uint64_t)ua * LOOP_SHUNT_OHM;                  /* мкА * Ом = мкВ */
    return (uint32_t)((uv * LOOP_N * 4095u + LOOP_VREF_MV * 500u) / (LOOP_VREF_MV * 1000u));
}

loop_cal_t loop_cal_nominal(void)
{
    loop_cal_t c = { nominal_sum(4000u), nominal_sum(20000u) };
    return c;
}

/* сумма окна -> микроамперы по прямой через две точки калибровки (4 и 20 мА) */
int32_t loop_ua(uint32_t sum, const loop_cal_t *c)
{
    int64_t num = ((int64_t)sum - (int64_t)c->s4) * 16000;
    int64_t den = (int64_t)c->s20 - (int64_t)c->s4;
    num += (num >= 0 ? den : -den) / 2;                            /* к ближайшему */
    return 4000 + (int32_t)(num / den);
}

/* микроамперы -> инженерные единицы шкалы lo…hi; вне 4–20 мА — край шкалы */
int32_t loop_eng(int32_t ua, int32_t lo, int32_t hi)
{
    if (ua < 4000) ua = 4000;
    if (ua > 20000) ua = 20000;
    return lo + (int32_t)(((int64_t)(ua - 4000) * (hi - lo) + 8000) / 16000);
}

/* обрыв и КЗ: состояние меняется, только если новое держится LOOP_DEBOUNCE окон подряд */
loop_state_t loop_diag(loop_diag_t *d, int32_t ua)
{
    loop_state_t now = ua < (int32_t)LOOP_LO_UA ? LOOP_LOW
                     : ua > (int32_t)LOOP_HI_UA ? LOOP_HIGH : LOOP_OK;
    if (now == d->state) {
        d->count = 0;
    } else if (now != d->pending || d->count == 0) {
        d->pending = now;
        d->count = 1;
    } else if (++d->count >= LOOP_DEBOUNCE) {
        d->state = now;
        d->count = 0;
    }
    return d->state;
}

loop176a_f1.c — приём 4–20 мА на F103: шунт 150 Ом, окно 20 измерений.

/* 176а: приём 4–20 мА на F103 — шунт 150 Ом, PA0, окно 20 измерений по 1 мс */
#include "loop_rx.h"

#define LOOP_CH  0u                         /* PA0 = ADC1_IN0 */

static volatile uint32_t acc, window_sum;
static volatile uint8_t cnt, ready;

void loop_adc_init(void)
{
    RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_ADCPRE) | RCC_CFGR_ADCPRE_DIV6;   /* 72 / 6 = 12 МГц */
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_ADC1EN;
    GPIOA->CRL &= ~(GPIO_CRL_CNF0 | GPIO_CRL_MODE0);   /* PA0 аналоговый */
    ADC1->SMPR2 |= ADC_SMPR2_SMP0;          /* 239,5 такта: вход через 4,7 кОм и 1 мкФ */
    ADC1->SQR3 = LOOP_CH;
    ADC1->CR2 = ADC_CR2_ADON;
    for (volatile uint32_t i = 0; i < 100u; i++) { }   /* tSTAB: до 1 мкс */
    ADC1->CR2 |= ADC_CR2_CAL;               /* калибровка F1 обязательна (14.3) */
    while (ADC1->CR2 & ADC_CR2_CAL) { }
}

void SysTick_Handler(void)                  /* раз в 1 мс: одно измерение, 21 мкс */
{
    ADC1->CR2 |= ADC_CR2_ADON;
    while (!(ADC1->SR & ADC_SR_EOC)) { }
    acc += ADC1->DR;
    if (++cnt == LOOP_N) {                  /* 20 мс — окно кратно периоду 50 Гц */
        window_sum = acc;
        acc = 0;
        cnt = 0;
        ready = 1;
    }
}

/* из главного цикла: 1 — готово новое окно; ток в мкА, давление в сотых бара, состояние */
int loop_poll(const loop_cal_t *cal, loop_diag_t *d, int32_t *ua, int32_t *cbar)
{
    if (!ready)
        return 0;
    ready = 0;
    *ua = loop_ua(window_sum, cal);
    *cbar = loop_eng(*ua, 0, 1000);         /* датчик 0–10 бар */
    loop_diag(d, *ua);
    return 1;
}

loop176a_g4.c — выход 4–20 мА на G474: VREFBUF, DAC и сток тока.

/* 176а: выход 4–20 мА на G474 — VREFBUF 2,5 В, DAC1 канал 1 (PA4) без буфера,
 * дальше сток тока: ОУ + n-МОП + резистор уставки (вход «+» ОУ высокоомный) */
#include <stdint.h>

#define TX_RSET_OHM  100u                   /* резистор уставки: 2,0 В -> 20 мА */
#define TX_VREF_MV   2500u                  /* VREFBUF, VRS = 01 */
#define TX_MAX_UA    22000u                 /* ток верхней аварии тоже должен пройти */

_Static_assert(TX_MAX_UA * TX_RSET_OHM < TX_VREF_MV * 1000u, "уставка на максимум тока выше опоры ЦАП");

/* код ЦАП для тока: U = I * Rset, код = U * 4095 / Vref, с округлением */
uint32_t tx_code(uint32_t ua)
{
    if (ua > TX_MAX_UA)
        ua = TX_MAX_UA;
    return (uint32_t)(((uint64_t)ua * TX_RSET_OHM * 4095u + TX_VREF_MV * 500u) / (TX_VREF_MV * 1000u));
}

void tx_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;   /* такт SYSCFG + COMP + VREFBUF + OPAMP */
    VREFBUF->CSR = (VREFBUF->CSR & ~(VREFBUF_CSR_VRS | VREFBUF_CSR_HIZ | VREFBUF_CSR_ENVR))
                 | VREFBUF_CSR_VRS_0;       /* VRS = 01: 2,5 В; менять только при ENVR = 0 */
    VREFBUF->CSR |= VREFBUF_CSR_ENVR;
    while (!(VREFBUF->CSR & VREFBUF_CSR_VRR)) { }                      /* 2,5 В на VREF+ */
    RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN | RCC_AHB2ENR_DAC1EN;
    GPIOA->MODER |= GPIO_MODER_MODE4;       /* PA4 аналоговый */
    DAC1->MCR = (DAC1->MCR & ~(DAC_MCR_MODE1 | DAC_MCR_HFSEL))
              | DAC_MCR_MODE1_1             /* MODE1 = 010: на вывод, буфер выключен */
              | DAC_MCR_HFSEL_1;            /* HCLK 170 МГц > 160 */
    DAC1->DHR12R1 = tx_code(3600u);         /* пока нет данных — ток нижней аварии */
    DAC1->CR |= DAC_CR_EN1;
}

void tx_set_ua(uint32_t ua)
{
    DAC1->DHR12R1 = tx_code(ua);
}

Дальше: раздел 176б «Термометр сопротивления PT100/PT1000: ратиометрия и 2-, 3-, 4-проводное подключение» — как перевести сопротивление платины в градусы на встроенном АЦП и убрать из результата опору и провода.

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