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 пФ.
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 Итоги
- Ток одинаков во всей петле: сопротивление линии не входит в результат, а 0 мА отличим от начала шкалы 4 мА. Напряжение на 1000 м общего провода ошиблось бы на 3,50 %.
- Шунт — под опору: 150 Ом при 3,3 В (0,60–3,00 В, 5,372 мкА на код, предел 22,0 мА), 100 Ом при 2,5 В. 250 Ом даёт 5,00 В — выше 4,0 В.
- Защита входа: 4,7 кОм и два Шоттки держат ток в диод при 24 В на 4,40 мА против предела 5 мА; цена — 31,3 мкА ошибки от утечки.
- Окно 20 × 1 мс гасит 50 Гц полностью, при 49–51 Гц остаётся меньше 0,1 %; окно 16 — 0,823 %.
- Бюджет ошибки на 20 мА: F103 — 108,2 мкА без учёта опоры
VDDA(её 1 % — ещё 200), G474 с VREFBUF — 255,5 мкА. Две точки калибровки убирают допуски, дрейф остаётся. - Бюджет петли при 21,6 В, 22 мА и 12 В на датчике: 286,4 Ом линии — 4091 м кабеля 0,5 мм² с шунтом 150 Ом и 2662 м с шунтом 250 Ом.
- Код: сумма окна → мкА → инженерные единицы в целых; диагностика с подтверждением три окна (60 мс); прогон на ПК — 81901 из 81901.
- Выход: ОУ + n-МОП + 100 Ом даёт 20,00 мА до 1100 Ом нагрузки при 24 В; NPN с усилением 50 теряет 1,96 %; Хауленд с разбросом 1 % плывёт до 21,16 мА. G474: шаг 6,11 мкА, бюджет 285,3 мкА до калибровки.
Полные исходники
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-проводное подключение» — как перевести сопротивление платины в градусы на встроенном АЦП и убрать из результата опору и провода.