1. Тема дня

Дискретные входы через оптопары: что реально происходит между внешним сигналом 220 В / 24 В / 12 В и GPIO ESP32/STM32, почему на выходе могут быть пульсации, дребезг, задержки, ложные фронты. Главная мысль: Оптопара не превращает внешний сигнал в идеальный логический уровень. Она превращает его в ток через светодиод, затем в ток фототранзистора, а уже потом схема подтяжки и фильтрации делает уровень для MCU.

2. Зачем это нужно

Входы фаз светофора могут быть:

text
220 В AC
24 В DC
12 В DC
промышленная линия
длинный кабель с помехами
сигнал через реле/оптопару/модуль

А MCU видит только GPIO 0/1. Между ними:

  • AC после оптопары может пульсировать 50/100 Гц;
  • фототранзистор медленно выходит из насыщения;
  • CTR оптопары гуляет;
  • длинный кабель ловит помехи;
  • слабая подтяжка даёт медленные фронты;
  • GPIO-прерывания могут ловить ложные события.

Поэтому нельзя просто:

c
if (gpio_get_level(INPUT_RED)) {
    phase = RED;
}

3. Базовая схема

text
Внешний сигнал
   -> токоограничивающий резистор
   -> LED внутри оптопары
   -> световая связь
   -> фототранзистор
   -> pull-up / pull-down
   -> GPIO ESP32/STM32

Типовая схема:

text
3.3 В
 │
 Rpullup
 │
 ├── GPIO
 │
 collector оптопары
 emitter
 │
GND

Часто есть инверсия:

text
внешний сигнал есть  -> GPIO = 0
внешнего сигнала нет -> GPIO = 1

В коде это надо задавать явно:

c
#define INPUT_ACTIVE_LEVEL 0

4. DC проще, чем AC

Для 24 В DC:

text
24 В есть -> LED оптопары светит постоянно
24 В нет  -> LED не светит

Для 220 В AC:

text
+ полуволна
0
- полуволна
0

Если схема проводит только одну полуволну, выход будет импульсным 50 Гц. Если обе - 100 Гц. Поэтому наличие 220 В AC часто нужно детектировать как:

text
за последние 20-50 мс были импульсы?

а не как:

text
какой уровень GPIO прямо сейчас?

5. Программный AC detector

c
#define AC_PRESENT_TIMEOUT_MS 40
typedef struct {
    int64_t last_active_us;
    bool present;
} ac_input_detector_t;

Логика:

text
если увидели активный уровень:
    last_active_us = now
если now - last_active_us < 40 мс:
    AC присутствует
иначе:
    AC отсутствует

Для сети 50 Гц период 20 мс. Окно 30-50 мс обычно перекрывает несколько полупериодов.

6. Pull-up

Внутренняя подтяжка MCU часто слабая и с большим разбросом. Для оптопарного входа лучше внешний pull-up:

text
10 kΩ  - экономичнее, фронты медленнее
4.7 kΩ - быстрее и устойчивее
2.2 kΩ - ещё жёстче, но больше ток через фототранзистор

7. CTR оптопары

CTR = ток фототранзистора / ток LED. Он зависит от:

  • экземпляра;
  • температуры;
  • старения;
  • тока LED;
  • партии;
  • режима насыщения.

Надо проверять худший случай:

text
минимальный ток LED
минимальный CTR
максимальная температура
старение
самая жёсткая подтяжка

8. RC-фильтр

RC-фильтр сглаживает помехи, но добавляет задержку.

text
τ = R × C

Пример:

text
R = 10 kΩ
C = 100 nF
τ = 1 мс

А вот 100 кΩ и 1 мкФ дадут 100 мс - это может слишком сильно задерживать события.

9. Schmitt trigger

Если фронты медленные или шумные, полезен буфер с гистерезисом:

text
внешний сигнал
-> защита
-> токоограничение
-> оптопара
-> pull-up
-> RC / фильтр
-> Schmitt trigger / компаратор
-> GPIO MCU

Гистерезис убирает дрожание около порога.

10. GPIO interrupt или polling

GPIO interrupt:

  • быстрый timestamp;
  • хорошо для чистых фронтов;
  • плохо для грязного AC после оптопары.

Polling:

  • проще фильтровать;
  • меньше проблем с шумом;
  • удобно для HIL;
  • timestamp имеет погрешность до периода опроса.

Для светофора лучше начинать с polling 1-5 мс.

11. Детектор DC-входа

Для DC можно использовать integrator:

c
typedef struct {
    uint8_t acc;
    bool stable;
} dc_input_filter_t;
#define FILTER_MAX 10
#define FILTER_ON_THRESHOLD  8
#define FILTER_OFF_THRESHOLD 2
static void dc_input_update(dc_input_filter_t *f, bool raw_active)
{
    if (raw_active) {
        if (f->acc < FILTER_MAX) f->acc++;
    } else {
        if (f->acc > 0) f->acc--;
    }
    if (f->acc >= FILTER_ON_THRESHOLD) {
        f->stable = true;
    } else if (f->acc <= FILTER_OFF_THRESHOLD) {
        f->stable = false;
    }
}

12. Связь с архитектурой

text
input_driver
  читает raw GPIO
input_filter
  превращает raw в stable_present
phase_detector
  определяет RED/YELLOW/GREEN/OFF/CONFLICT
phase_task
  создаёт phase_event_t с timestamp
transport_task
  отправляет событие

13. Конфликты фаз

text
RED = 1, YELLOW = 0, GREEN = 0 -> RED
RED = 0, YELLOW = 1, GREEN = 0 -> YELLOW
RED = 0, YELLOW = 0, GREEN = 1 -> GREEN
RED = 0, YELLOW = 0, GREEN = 0 -> OFF/UNKNOWN
RED = 1, YELLOW = 1, GREEN = 0 -> RED_YELLOW или TRANSITION
RED = 1, YELLOW = 0, GREEN = 1 -> CONFLICT

Невозможные комбинации нужно явно логировать.

14. Типичные ошибки

  • читать AC-вход как обычный уровень;
  • использовать один фильтр для AC и DC;
  • ставить слишком длинный debounce;
  • не ставить внешнюю подтяжку;
  • забыть про инверсию;
  • делать тяжёлую обработку в GPIO ISR;
  • не измерить реальный сигнал осциллографом.

15. Практическое задание

Создай INPUT_FILTER_POLICY.md:

markdown
# Input filter policy
## Input types
- AC optocoupler input:
  Detect presence of pulses within a time window.
  Do not use instantaneous GPIO level as final state.
- DC optocoupler input:
  Use integrator/debounce filter.
## Timing
- Polling period: 1-5 ms
- AC present timeout: start with 40 ms for 50 Hz mains
- Stable confirm time: 20-50 ms
- Timestamp policy:
  Capture raw sample timestamp immediately.
## Signal inversion
All input channels must define active level explicitly.
## Diagnostics
Track:
- raw pulse count
- stable transitions
- glitches
- time since last pulse
- impossible phase combinations

16. Короткий итог

Оптопара не даёт идеальный логический уровень. Для AC-входа правильно думать:

text
сигнал активен,
если за последние 30-50 мс были импульсы

а не:

text
сигнал активен,
если gpio_get_level() прямо сейчас равен active_level
Почему один неактивный GPIO sample может вводить в заблуждение для AC optocoupler input?

Задание

По τ=R×C вычислите nominal time constant для R=10 kΩ, C=100 nF. Чего это значение не доказывает для полного input detector?

Критерии самопроверки: Получите 1 ms и отличите RC time constant от полного timing detector или hardware validation.

Показать ответ автора

τ=10000×100×10^-9 s=0.001 s=1 ms. Это RC time constant, не полная latency phase detection и не validated threshold. Поведение оптопары, форма сигнала, polling и confirmation policy также влияют на detector; source examples требуют измерений конкретной схемы.