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

208а. Прибор от 24 В: промышленное питание, защита входа и цифровые входы

Раздел 208 провёл прибор через границу сети 230 В. Второе типичное питание изделия — шина 24 В постоянного тока в шкафу автоматики: блок питания на рейке, а от него — контроллеры, датчики, реле, клапаны. Трогать её безопасно, но жить на ней трудно. Шину делят с контакторами и соленоидами, провода к датчикам тянутся на десятки метров, клеммы подключают под напряжением и путают полярность. Здесь — что бывает на шине, как собрать вход питания прибора, как читать цифровой вход 24 В микроконтроллером на 3,3 В и как управлять катушкой с выхода, чтобы ключ пережил её отключение.

Опоры: расчёт оптрона из 54.2 (коэффициент передачи 50…600 %, старение вдвое), защита из 173 (P-канальный в плюсовом проводе, ограничитель), верхний ключ из 172.2, пороги выводов — даташиты STM32G474 (DS12288) и STM32F103 (DocID13587) из паков Keil. Числа — модели ngspice в контейнере stm-lab:1 (revpol24.cir, hotplug.cir, input.cir, coil.cir) и расчёт calc208a.py; код собран armclang для обоих МК и прогнан тестом на компьютере. Стандартов на промышленные источники и входы контроллеров на диске нет: 18…30 В ниже — рамка расчёта, та же, что в 54.2, а не допуск из стандарта. Параметры катушки, кабеля и ограничителя условные — это типичные величины для расчёта, а не данные конкретной детали.

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

Оригинал Перевод
24 V DC digital input, type 1 / type 3 дискретный вход 24 В и его типы по стандарту на контроллеры
sinking input / sourcing output вход, принимающий ток датчика / выход, отдающий ток в нагрузку
two-wire sensor, off-state leakage двухпроводной датчик и его ток в выключенном состоянии
current-limited input вход с ограничителем тока
hot plugging подключение под напряжением
LC input filter входной фильтр из дросселя и конденсатора
inductive load demagnetization гашение энергии катушки после выключения
vertical counter вертикальный счётчик: биты счётчиков всех входов в соседних словах
sensor supply monitoring контроль питания датчиков

208а.1 Шина 24 В: что на ней бывает

Напряжение гуляет. Блок питания выдаёт 24 В, но на клеммах прибора в дальнем конце шкафа, при просадке под нагрузкой и после подстройки бывает и меньше, и больше. Считать всё удобно в рамке 18…30 В — размах 12 В. Нижний край проверяют на пороги входов и запас стабилизатора, верхний — на нагрев резисторов (54.2) и предел затвора (ниже, 208а.2).

Соседи отключают катушки. Контактор или клапан на той же шине — индуктивность с запасённой энергией. Условная катушка на 24 В, 48 Ом, 0,3 Гн берёт 0,5 А, её постоянная времени τ = L/R = 6,25 мс, а энергия W = L·I²/2 = 37,5 мДж — в 833 раза больше, чем у реле из 51.2. Если сосед разрывает её контактом без гасящей цепи, напряжение стремится к U = I·√(L/C): при 1 нФ ёмкости обмотки и проводов это 8,7 кВ. Столько не бывает — раньше пробивается воздух между контактами, и в шину уходит та самая пачка наносекундных импульсов из 203.3. Гасящие цепи соседа — не в нашей власти, поэтому вход прибора рассчитывают на то, что до него дойдёт.

Клеммы подключают под напряжением. Провод от шины втыкают в работающий шкаф, и кабель с конденсатором на входе прибора звенит: на шине 30 В вход видит больше 50 В (208а.3).

Прибор от шины 24 В: вход питания — предохранитель, P-канальный со стабилитроном 12 В, ограничитель, LC-фильтр с электролитом, DC-DC до 5 В и LDO 3,3 В; цифровой вход — диод, ограничитель тока, стабилитрон 9,1 В, оптрон с шунтом 680 Ом и подтяжка 22 кОм к выводу МК; выход — P-канальный ключ в плюсе, клемма и катушка с диодом и стабилитроном 12 В


208а.2 Предохранитель и переполюсовка на двух амперах

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

Схемы защиты от переполюсовки те же, что в 173.1, но ток вдвое больше, а шина — 24 В. Модель: нагрузка 12 Ом, 2 А.

Схема Падение Потери Ток при −24 В
кремниевый диод последовательно 0,98 В 1,88 Вт 7 нА
диод Шоттки последовательно 0,49 В 0,95 Вт 30 мкА
P-канальный в плюсовом проводе 0,116 В 0,23 Вт 25 пА

Потери диода растут как U·I, а у транзистора — как I²·R: канал в 58 мОм на 2 А теряет 0,23 Вт, в восемь раз меньше кремниевого диода. На двух амперах почти два ватта в одной детали — заметный нагрев, поэтому на таких токах выбирают транзистор.

Затвор на 24 В без стабилитрона сгорит. Затвор подтянут к минусу, и напряжение затвор-исток равно напряжению шины: на верхнем краю — 30 В при типичном пределе ±20 В (172.2, 173.1). Стабилитрон 12 В между затвором и истоком с резистором 100 кОм держит в модели −11,9 В и на 24, и на 30 В; его ток — от 60 мкА на 18 В до 180 мкА на 30 В, мощность — не больше 2,2 мВт. Падение на канале при 30 В — 0,145 В: нагрузка 12 Ом берёт уже 2,5 А.


208а.3 Ограничитель, горячее подключение и фильтр перед DC-DC

Ограничитель выбирают по трём напряжениям. Рабочее — выше верха шины, иначе он подтекает и греется всё время: для 30 В годится 33 В, запас 3 В. Пробой — выше рабочего. А под импульсом на нём напряжение ограничения Uогр = Uпроб + Rд·I (173.3). Условный ограничитель — рабочее 33 В, пробой 37 В, динамическое сопротивление 1,5 Ом — даёт:

Ток импульса Напряжение ограничения
7,9 А 48,9 В
10 А 52,0 В
20 А 67,0 В

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

Горячее подключение. Кабель — индуктивность (условно 3 мкГн и 0,1 Ом, метр-три пары проводов), на входе прибора — керамика 10 мкФ. Вместе это колебательный контур с волновым сопротивлением Z0 = √(L/C) = 0,55 Ом и затуханием ζ = R/(2·Z0) = 0,091 — почти без потерь, резонанс 29,1 кГц. Ступенька 30 В раскачивает его до U·(1 + e^(−πζ/√(1−ζ²))) = 52,5 В: перерегулирование 75 %. Модель hotplug.cir:

Что на входе Пик на входе Что ещё
только керамика 10 мкФ 52,2 В —
керамика и ограничитель 49,3 В 7,9 А и 3,3 мДж в ограничителе
керамика и электролит 47 мкФ, 0,5 Ом 33,3 В —

Ограничитель почти не помогает: его пробой 37 В плюс 1,5 Ом · 7,9 А — те же 49 В. Помогает затухание: электролит с внутренним сопротивлением 0,5 Ом — порядка Z0 — гасит звон до 33,3 В. Поэтому на входе 24 В рядом с керамикой ставят электролит, и его «плохое» сопротивление здесь — достоинство.

Фильтр перед DC-DC. Импульсный стабилизатор берёт ток рывками на частоте ключа и возвращает их в шину — а по шине они дойдут до соседей. Дроссель 10 мкГн и конденсатор 10 мкФ дают резонанс 15,9 кГц и Z0 = 1 Ом; на частоте ключа 500 кГц такой фильтр ослабляет помеху в (f/f0)² = 987 раз, на 60 дБ. Но это снова LC-контур: на своём резонансе он не ослабляет, а раскачивает, и тот же электролит с внутренним сопротивлением нужен и здесь. Разводка и выбор дросселя — раздел 58, фильтры питания — 168.

Почему DC-DC, а не линейный. Логике нужно 3,3 В при 100 мА. Линейный от 24 В рассеивает (Uвх − Uвых)·I = 2,07 Вт, от 30 В — 2,67 Вт (тепловой расчёт — 55.4). Импульсный с условным КПД 0,85 теряет 58 мВт и берёт от шины 16,2 мА вместо 100 мА. Обычная цепочка: понижающий 24 → 5 В (58.8), за ним LDO на 3,3 В или сразу понижающий на 3,3 В.


208а.4 Цифровой вход 24 В: пороги, ток и мощность

К входу подключают контакт реле, концевик или датчик, который выдаёт на клемму плюс шины. Стандарт на контроллеры делит такие входы на типы по порогам и току; тип 3 придуман ради двухпроводных датчиков, которые и в выключенном состоянии пропускают через себя ток. Таблицы стандарта на диске нет, поэтому пороги ниже выведены из оптрона и схемы, а не взяты из стандарта; перед сертификацией их сверяют с ним.

Критерии задаёт оптрон. На выходе — подтяжка 22 кОм к 3,3 В, это 0,15 мА. Коэффициент передачи считают по 54.2: минимум 50 % пополам на старение, максимум 600 %, и ещё запас 2 в обе стороны.

Схема. Диод от переполюсовки, затем элемент, задающий ток, стабилитрон 9,1 В — он и есть порог: пока на входе меньше суммы падений на диоде, стабилитроне и светодиоде, ток почти не идёт (при 5 В в модели — 6 пА), — и светодиод оптрона, зашунтированный резистором 680 Ом. Ток задаёт либо резистор 1,5 кОм, либо ограничитель на двух транзисторах: 47 кОм в базу, датчик тока 200 Ом, около 0,65 В / 200 Ом. Модель input.cir, развёртка входа от −31 до +31 В:

Резистор 1,5 кОм Ограничитель тока
«1» гарантирована от 14,9 В 13,3 В
«0» гарантирован до 12,3 В 11,7 В
ток входа при 15 В 2,78 мА 2,93 мА
ток входа при 24 В 8,60 мА 3,33 мА
ток входа при 30 В 12,5 мА 3,51 мА
ток светодиода при 24 В 6,91 мА 1,78 мА
мощность при 24 В 206 мВт 80 мВт
мощность при 30 В 376 мВт 105 мВт
ток при −30 В 2,5 нА 2,5 нА

Между двумя порогами — зона, где не гарантировано ничего: вход может показать и «0», и «1». Так и задумано: провод с напряжением посередине — неисправность, а не сигнал.

Резистор честен, но греется. У резистора ток растёт с напряжением: на 30 В схема рассеивает 376 мВт, из них 235 мВт в самом резисторе и 117 мВт в стабилитроне. Ограничитель держит ток почти постоянным, и на 30 В вся схема берёт 105 мВт, из них 65 мВт в транзисторе (18,5 В на нём). На шестнадцати входах это 1,7 Вт против 6,0 Вт — разница, которую видно по температуре платы в закрытом шкафу.

Шунт 680 Ом — от утечки датчика. Выключенный двухпроводной датчик пропускает ток, в модели — 1 мА. Он поднимает вход до 11,5 В (с резистором — до 11,9 В), но светодиод получает 1,0 мкА: весь миллиампер уходит в шунт и создаёт на нём 0,68 В, а при таком напряжении светодиод почти закрыт. Без шунта весь миллиампер после порога стабилитрона пошёл бы в светодиод — между критериями «0» и «1», то есть вход мог бы показать что угодно.


208а.5 Сторона МК: RC-фильтр и антидребезг в прошивке

Аппаратный фильтр — микросекунды. На выводе МК подтяжка 22 кОм и конденсатор 2,2 нФ, τ = R·C = 48,4 мкс. Порог единицы у STM32G474 — 0,7·VDD, нуля — 0,3·VDD (DS12288); у STM32F103 — 0,65·VDD и 0,35·VDD (DocID13587). Когда оптрон гаснет, вывод поднимается подтяжкой до порога единицы за 58,3 мкс у G474 и 50,8 мкс у F103. Когда загорается, транзистор оптрона тянет вниз не меньше 1,2 мА · 0,25 = 0,3 мА, из них 0,15 мА уходит в подтяжку, а 0,15 мА разряжает конденсатор: до порога нуля — 33,9 мкс у G474 и 31,5 мкс у F103. Наносекундные пачки из 203.3 на этом умирают, а дребезг контакта длится миллисекунды и проходит — его убирает прошивка. Как считают RC на выводе — 62.7.

Программный фильтр — миллисекунды. Опрос раз в 1 мс, вход признаётся новым уровнем после четырёх подряд одинаковых отсчётов: задержка 4 мс, импульс короче 4 мс не пройдёт, предел частоты 1/(2 · 4 мс) = 125 Гц. В 198в.6 такой фильтр сделан счётчиком на каждый вход; здесь — сразу на шестнадцать входов порта, без цикла и без ветвлений. Для каждого входа счётчик на два бита, но хранятся они «вертикально»: младшие биты всех шестнадцати счётчиков — в одном слове cnt0, старшие — в cnt1. Одна операция над словом — шаг счёта для всех входов разом:

/* Четыре подряд отсчёта, не совпавших с признанным состоянием, — и вход
 * переключается. Все входы разом, без ветвлений. Возвращает переключившиеся. */
uint32_t di_debounce(di_t *d, uint32_t raw)
{
    uint32_t delta  = (raw ^ d->state) & DI_MASK;   /* где отсчёт спорит с состоянием */
    uint32_t toggle = delta & d->cnt0 & d->cnt1;    /* счётчик был на 3 — это 4-й     */
    d->glitch |= (d->cnt0 | d->cnt1) & ~delta;      /* счёт начался и оборвался       */
    d->cnt1 = (d->cnt1 ^ d->cnt0) & delta;          /* +1, а при согласии — сброс     */
    d->cnt0 = ~d->cnt0 & delta;
    d->state ^= toggle;
    return toggle;
}

Где отсчёт совпал с состоянием, delta — ноль, и оба бита счётчика обнуляются; где спорит — счётчик прибавляет единицу, а на четвёртом несогласном отсчёте бит состояния переворачивается. armclang с -O2 для G474 превращает функцию в 20 команд без единого перехода: время опроса не зависит от того, что творится на входах.

Диагностика. Сам по себе антидребезг скрывает неисправности: вход, который скачет раз в 10 мс, никогда не переключится и будет выглядеть «просто выключенным». Поэтому опрос считает сырые перепады и следит за питанием датчиков:

/* Опрос раз в 1 мс: port — чтение порта как есть, supply_ok — питание датчиков
 * в норме (свой вход или АЦП). Без питания входы не признаются — замораживаются. */
uint32_t di_poll(di_t *d, uint32_t port, int supply_ok)
{
    uint32_t raw = (port ^ DI_ACTIVE_LOW) & DI_MASK;
    uint32_t toggle = 0u;
    uint32_t e = raw ^ d->last_raw;                 /* сырые перепады — для диагностики */
    d->last_raw = raw;
    d->supply_ok = (uint8_t)(supply_ok != 0);
    if (d->supply_ok) {
        toggle = di_debounce(d, raw);
    } else {
        d->cnt0 = 0u;                               /* без питания нули — не сигнал   */
        d->cnt1 = 0u;
    }
    for (uint32_t i = 0u; e != 0u; i++, e >>= 1) {
        if ((e & 1u) != 0u && d->edges[i] < UINT16_MAX) {
            d->edges[i]++;
        }
    }
    if (++d->window_ms >= DI_WINDOW_MS) {           /* окно закрыто: итог по дребезгу */
        uint32_t ch = 0u;
        for (uint32_t i = 0u; i < DI_COUNT; i++) {
            if (d->edges[i] > DI_CHATTER_MAX) {
                ch |= 1u << i;
            }
            d->edges[i] = 0u;
        }
        d->chatter = ch;
        d->window_ms = 0u;
    }
    return toggle;
}

Больше 20 перепадов за окно 1000 мс — флаг «дребезжит», и di_valid_mask() исключает вход из годных. Флаг glitch поднимается, когда счёт начался и оборвался, — была помеха короче четырёх отсчётов; он липкий, прошивка сбрасывает его сама, прочитав. Если пропало питание датчиков, все входы разом падают в ноль — это не сигнал, а авария: состояние замораживается, годных входов нет. Пороги и размеры проверены _Static_assert; порт читается одной командой — у G474 по адресу 0x4800 0410, у F103 по 0x4001 0C08 (регистр IDR у них на разных смещениях, и это тоже проверено при сборке).

Тест di_test.c на компьютере:

Опыт Результат
чистая ступенька в 10 мс признана в 13 мс — на четвёртом отсчёте
дребезг 10…15 мс, дальше стоит одно переключение, в 18 мс; флаг помехи поднят
импульсы 1, 2, 3 мс не прошли; импульс 4 мс прошёл
меандр 100 Гц (полупериод 5 мс) 199 переключений за 1 с из 200
меандр около 167 Гц (полупериод 3 мс) ни одного переключения
вход скачет каждые 10 мс флаг дребезга, годные входы 0xFFF7; через окно покоя снят
питание датчиков пропало на 50 мс состояние 0x00F0 сохранено, годных 0x0000
16 ступенек в 10…25 мс признаны в 13…28 мс, каждая вовремя

208а.6 Цифровой выход: ключ в плюсе и катушка

В шкафу нагрузку обычно сажают одним концом на минус шины, а ключ ставят в плюс. Провод к нагрузке, замкнутый на корпус шкафа (он на минусе), тогда не включает её — а при ключе в минусе включил бы: катушка уже висит на плюсе. Ключ — P-канальный со сдвигом уровня на NPN из 172.2 и тем же стабилитроном 12 В в затворе, что в 208а.2. Нагрузка — условная катушка из 208а.1: 0,3 Гн, 48 Ом, 0,5 А.

При выключении ключа ток катушки продолжает течь и тянет клемму выхода ниже минуса — ровно настолько, насколько нужно, чтобы нашёлся путь. Какой путь ему дать — выбор между скоростью и напряжением на ключе. Модель coil.cir (транзистор с пробоем 40 В, время — от момента выключения):

Гасящая цепь Клемма выхода Спад до 10 % Куда ушла энергия
нет −16,7 В: ключ в пробое 4,7 мс 48,4 мДж в ключ
диод −0,87 В 12,8 мс 2,3 мДж в диод, остальное в обмотку
диод и стабилитрон 12 В −13,4 В 5,5 мс 17,8 мДж в гасящую цепь
диод и ограничитель 37 В −16,7 В: ключ в пробое 4,7 мс 48,4 мДж в ключ

Проверка по модели L/R: ток спадает с постоянной τ к «−Uогр/R», и до 10 % он доходит за τ · ln((I0 + Uогр/R) / (0,1·I0 + Uогр/R)) — 12,68 мс с диодом и 5,39 мс с диодом и стабилитроном; до нуля во втором случае — τ · ln(1 + I0·R/Uогр) = 6,42 мс (в модели 6,5 мс), энергия в гасящей цепи — 17,9 мДж. С одним диодом клапан или реле отпускает вдвое с лишним позже, а контакты расходятся медленнее (51.2).

Цена скорости — напряжение на ключе. Сток-исток видит шину плюс напряжение гасящей цепи: 24 + 13,4 = 37,4 В — при пределе 40 В уже впритык, а на верхнем краю шины 30 + 13,4 = 43,4 В — за пределом. Ключ на 40 В годится только с более низким стабилитроном; иначе берут ключ на 60 В.

Ограничитель «с запасом» бесполезен. Ограничитель на 37 В последовательно с диодом не успевает открыться: ключ пробивается раньше, на 40 В. В ключ уходит 48,4 мДж — больше, чем было в катушке (37,5 мДж): пока ключ в пробое, шина продолжает подливать через него ток. Без гасящей цепи — то же самое. Переживёт ли ключ такое выключение тысячу раз в день, решает паспортная энергия лавинного пробоя, а её модель не знает.


208а.7 Грабли

Прибор выгорает, когда провод от шины втыкают «на ходу». Керамика на входе и индуктивность кабеля звенят: шина 30 В даёт на входе 52 В. Ограничитель спасает мало (49 В), электролит с внутренним сопротивлением порядка Z0 — спасает (33 В). Входные детали — с запасом над пиком, а не над 24 В.

Выход с «хорошим» ограничителем горит на каждом выключении. Ограничитель на 37 В вместе с шиной 24 В требует от ключа больше 60 В, а ключ пробивается на 40 В и берёт на себя 48 мДж. Напряжение гасящей цепи складывают с верхом шины и сравнивают с пределом ключа.

Клапан отпускает с опозданием. Один диод на катушке держит ток 12,8 мс вместо 5,5 мс с диодом и стабилитроном. Где важна скорость отпускания, диод дополняют стабилитроном — и пересчитывают напряжение на ключе.

Все входы разом «выключились». Пропало питание датчиков — и опрос честно видит нули. Без контроля питания прошивка примет аварию за команду; с ним состояние замораживается, а годных входов нет.

Вход «горит», хотя датчик выключен. Двухпроводной датчик в выключенном состоянии пропускает около миллиампера — без шунта у светодиода этот ток уходит в оптрон и попадает между «0» и «1». Шунт 680 Ом оставляет светодиоду 1 мкА.

Счётчик импульсов теряет импульсы. Антидребезг 4 мс режет всё короче 4 мс и выше 125 Гц — меандр 167 Гц не даёт ни одного переключения. Для счёта быстрых импульсов вход заводят на таймер (раздел 13), а антидребезг оставляют кнопкам и концевикам.

Линейный стабилизатор от 24 В раскалён. 100 мА до 3,3 В — это 2,07 Вт на 24 В и 2,67 Вт на 30 В. От шины 24 В логику питают через понижающий DC-DC.


208а.8 Итоги

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

p24v208a.c — опрос входов 24 В: антидребезг на вертикальных счётчиках и диагностика.

/* Раздел 208а: опрос дискретных входов 24 В — антидребезг на вертикальных
 * счётчиках и диагностика (дребезг, отвергнутые помехи, питание датчиков).
 * di_poll() вызывается раз в 1 мс; входы — биты 0…DI_COUNT-1 одного порта.
 * Ядро переносимое; чтение порта — только при сборке под STM32G474 или STM32F103. */
#include <stdint.h>
#include <stddef.h>

#define DI_COUNT        16u        /* входов на порту                           */
#define DI_MASK         ((1u << DI_COUNT) - 1u)
#define DI_ACTIVE_LOW   DI_MASK    /* оптрон тянет вывод к нулю: активен нулём  */
#define DI_WINDOW_MS    1000u      /* окно подсчёта перепадов                   */
#define DI_CHATTER_MAX  20u        /* больше перепадов за окно — «дребезжит»    */

_Static_assert(DI_COUNT >= 1u && DI_COUNT <= 16u, "входы — на одном 16-битном порту");
_Static_assert((DI_ACTIVE_LOW & ~DI_MASK) == 0u, "маска инверсии шире числа входов");
_Static_assert(DI_CHATTER_MAX < DI_WINDOW_MS, "порог дребезга больше числа отсчётов в окне");

typedef struct {
    uint32_t state;              /* признанное состояние, 1 = вход активен      */
    uint32_t cnt0, cnt1;         /* два бита счётчика на вход, «вертикально»    */
    uint32_t last_raw;           /* сырой отсчёт прошлого опроса                */
    uint32_t chatter;            /* флаги: за прошлое окно вход дребезжал       */
    uint32_t glitch;             /* флаги: был импульс короче четырёх отсчётов  */
    uint16_t edges[DI_COUNT];    /* сырые перепады в текущем окне               */
    uint16_t window_ms;
    uint8_t  supply_ok;          /* питание датчиков было в норме в этом опросе */
} di_t;

_Static_assert(DI_WINDOW_MS <= UINT16_MAX, "окно и счётчики перепадов — в uint16_t");
_Static_assert(offsetof(di_t, cnt0) == 4u && offsetof(di_t, cnt1) == 8u, "раскладка di_t");

/* Четыре подряд отсчёта, не совпавших с признанным состоянием, — и вход
 * переключается. Все входы разом, без ветвлений. Возвращает переключившиеся. */
uint32_t di_debounce(di_t *d, uint32_t raw)
{
    uint32_t delta  = (raw ^ d->state) & DI_MASK;   /* где отсчёт спорит с состоянием */
    uint32_t toggle = delta & d->cnt0 & d->cnt1;    /* счётчик был на 3 — это 4-й     */
    d->glitch |= (d->cnt0 | d->cnt1) & ~delta;      /* счёт начался и оборвался       */
    d->cnt1 = (d->cnt1 ^ d->cnt0) & delta;          /* +1, а при согласии — сброс     */
    d->cnt0 = ~d->cnt0 & delta;
    d->state ^= toggle;
    return toggle;
}

/* Опрос раз в 1 мс: port — чтение порта как есть, supply_ok — питание датчиков
 * в норме (свой вход или АЦП). Без питания входы не признаются — замораживаются. */
uint32_t di_poll(di_t *d, uint32_t port, int supply_ok)
{
    uint32_t raw = (port ^ DI_ACTIVE_LOW) & DI_MASK;
    uint32_t toggle = 0u;
    uint32_t e = raw ^ d->last_raw;                 /* сырые перепады — для диагностики */
    d->last_raw = raw;
    d->supply_ok = (uint8_t)(supply_ok != 0);
    if (d->supply_ok) {
        toggle = di_debounce(d, raw);
    } else {
        d->cnt0 = 0u;                               /* без питания нули — не сигнал   */
        d->cnt1 = 0u;
    }
    for (uint32_t i = 0u; e != 0u; i++, e >>= 1) {
        if ((e & 1u) != 0u && d->edges[i] < UINT16_MAX) {
            d->edges[i]++;
        }
    }
    if (++d->window_ms >= DI_WINDOW_MS) {           /* окно закрыто: итог по дребезгу */
        uint32_t ch = 0u;
        for (uint32_t i = 0u; i < DI_COUNT; i++) {
            if (d->edges[i] > DI_CHATTER_MAX) {
                ch |= 1u << i;
            }
            d->edges[i] = 0u;
        }
        d->chatter = ch;
        d->window_ms = 0u;
    }
    return toggle;
}

/* Состояние можно отдавать наружу, только если датчики запитаны и вход не дребезжит. */
uint32_t di_valid_mask(const di_t *d)
{
    return d->supply_ok ? (DI_MASK & ~d->chatter) : 0u;
}

#if defined(STM32G474xx) || defined(STM32F103xB)
/* Входы — GPIOB[15:0]. IDR у G4 на смещении 0x10, у F1 — 0x08 (CRL, CRH, IDR). */
#if defined(STM32G474xx)
_Static_assert(offsetof(GPIO_TypeDef, IDR) == 0x10u, "G4: IDR = база + 0x10");
_Static_assert(GPIOB_BASE == 0x48000400u, "G4: GPIOB на AHB2");
#else
_Static_assert(offsetof(GPIO_TypeDef, IDR) == 0x08u, "F1: IDR = база + 0x08");
_Static_assert(GPIOB_BASE == 0x40010C00u, "F1: GPIOB на APB2");
#endif

uint32_t di_read_port(void)
{
    return GPIOB->IDR;
}
#endif

Дальше: раздел 208б «Нагрузка 230 В от микроконтроллера: реле, симистор, твердотельное реле» — от катушки на 24 В к нагрузке в сети: чем её коммутировать и как не пустить 230 В на плату.

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