1. Тема дня
Алгоритм детектирования фазы светофора по AC-сигналу после оптопары: окно импульсов, стабильность, подавление ложного OFF и правильный timestamp. Главная задача: Отличать реальное включение/выключение фазы от пульсаций 50/100 Гц, кратких пропаданий, помех и переходных состояний.
2. Зачем это нужно
Если RED реально включён, GPIO может выглядеть так:
__--________--________--________--____Если читать уровень “прямо сейчас”, можно получить ложное:
RED -> OFF -> RED -> OFF -> REDЕсли это отправлять по UDP/TCP/MQTT, камера или Jetson получат мусорные переключения. Нужно строить алгоритм по наличию импульсов в окне времени.
3. Три уровня состояния
Для каждого входа:
raw_active
present
stable11.3.1 raw_active
Мгновенный уровень GPIO.
11.3.2 present
Сигнал присутствует, если недавно был активный импульс:
если активный импульс был за последние N мс:
present = true
иначе:
present = false11.3.3 stable
present не менялся достаточно долго:
present изменился
ждём CONFIRM_TIME
если за это время present не передумал:
stable = presentИменно stable лучше отдавать в phase_detector.
4. Разные времена
Нужно минимум три времени:
AC_PRESENT_TIMEOUT_US
сколько можно ждать между импульсами и всё ещё считать AC присутствующим
STABLE_ON_US
сколько present=true должен продержаться, чтобы считать фазу включённой
STABLE_OFF_US
сколько present=false должен продержаться, чтобы считать фазу выключеннойЧасто:
STABLE_ON_US = 20-30 мс
STABLE_OFF_US = 50-100 мсВыключение подтверждаем осторожнее, чтобы не ловить провал одного-двух импульсов.
5. Timestamp policy
Есть разные времена:
t_raw_first - первый сырой признак изменения
t_present_change - когда present изменился
t_stable_change - когда изменение подтверждено
t_event_emit - когда событие отправили в очередьПлохой вариант - timestamp в момент отправки по сети. Лучше хранить несколько времён:
typedef struct {
traffic_phase_t old_phase;
traffic_phase_t new_phase;
int64_t detected_us;
int64_t confirmed_us;
int64_t emitted_us;
uint32_t raw_mask;
uint32_t stable_mask;
} phase_event_t;Тогда можно посчитать задержки:
filter_delay = confirmed_us - detected_us
queue_delay = emitted_us - confirmed_us
network_delay = receive_time - emitted_us6. AC channel update
Флаг seen_active исключает ложное присутствие на старте. previous_raw_active позволяет считать только переход false→true, наблюдённый polling. Это всё ещё не аппаратный pulse counter: импульс короче периода опроса можно пропустить. last_active_us обновляется на каждом активном sample, чтобы длительный уровень оставался присутствующим. Инициализируйте структуру нулями один раз до цикла и используйте монотонное время одного запуска.
typedef struct {
bool seen_active;
bool previous_raw_active;
bool present;
bool stable;
uint64_t last_active_us;
uint64_t present_changed_us;
uint32_t pulse_count;
} ac_channel_example_t;
#define AC_PRESENT_TIMEOUT_US 45000ULL
#define AC_STABLE_ON_US 25000ULL
#define AC_STABLE_OFF_US 70000ULL
static void ac_channel_example_update(ac_channel_example_t *ch,
bool raw_active,
uint64_t now_us)
{
if (raw_active) {
ch->seen_active = true;
ch->last_active_us = now_us;
if (!ch->previous_raw_active) ch->pulse_count++;
}
ch->previous_raw_active = raw_active;
bool new_present = ch->seen_active &&
(now_us - ch->last_active_us <= AC_PRESENT_TIMEOUT_US);
if (new_present != ch->present) {
ch->present = new_present;
ch->present_changed_us = now_us;
}
uint64_t required_us = ch->present ? AC_STABLE_ON_US : AC_STABLE_OFF_US;
if (ch->stable != ch->present &&
now_us - ch->present_changed_us >= required_us) {
ch->stable = ch->present;
}
}Для OFF используйте отдельную выдержку кандидата 200000 мкс, для обычных фаз — 100000 мкс, а CONFLICT не ждёт этой выдержки. Вызывайте phase_candidate_ready после обновления candidate/candidate_since_us и перед переходом current. Чтобы CONFLICT действительно был немедленным, обновление нового кандидата не должно безусловно делать return до этой проверки. Фильтры AC/stable дают собственную задержку до появления кандидата — она не исчезает от этого helper.
#define MIN_PHASE_HOLD_US 100000ULL
#define OFF_REPORT_US 200000ULL
static bool phase_candidate_ready(traffic_phase_t phase,
uint64_t candidate_since_us,
uint64_t now_us)
{
if (phase == TRAFFIC_PHASE_CONFLICT) return true;
uint64_t required_us = phase == TRAFFIC_PHASE_OFF
? OFF_REPORT_US : MIN_PHASE_HOLD_US;
return now_us - candidate_since_us >= required_us;
}- Старт без активных samples: present=false, pulse_count=0.
- Десять подряд active samples: pulse_count=1.
- active→inactive→active: pulse_count=2.
- OFF-кандидат 199999 мкс не публикуется; на 200000 мкс может публиковаться.
- CONFLICT проверяется в той же итерации, где появился новый кандидат.
typedef struct {
bool raw_active;
bool present;
bool stable;
int64_t last_raw_active_us;
int64_t present_changed_us;
int64_t stable_changed_us;
int64_t candidate_on_us;
int64_t candidate_off_us;
uint32_t pulse_count;
uint32_t stable_change_count;
uint32_t short_drop_count;
uint32_t glitch_count;
} ac_channel_t;
#define AC_PRESENT_TIMEOUT_US 45000
#define AC_STABLE_ON_US 25000
#define AC_STABLE_OFF_US 70000
static void ac_channel_update(ac_channel_t *ch,
bool raw_active,
int64_t now_us)
{
ch->raw_active = raw_active;
if (raw_active) {
ch->last_raw_active_us = now_us;
ch->pulse_count++;
}
bool new_present =
(now_us - ch->last_raw_active_us) <= AC_PRESENT_TIMEOUT_US;
if (new_present != ch->present) {
ch->present = new_present;
ch->present_changed_us = now_us;
if (new_present) {
ch->candidate_on_us = now_us;
} else {
ch->candidate_off_us = now_us;
}
}
if (ch->present != ch->stable) {
int64_t age_us = now_us - ch->present_changed_us;
int64_t need_us = ch->present ? AC_STABLE_ON_US : AC_STABLE_OFF_US;
if (age_us >= need_us) {
ch->stable = ch->present;
ch->stable_changed_us = now_us;
ch->stable_change_count++;
}
}
}7. Декодирование фазы
#define PHASE_MASK_RED (1 << 0)
#define PHASE_MASK_YELLOW (1 << 1)
#define PHASE_MASK_GREEN (1 << 2)
static uint32_t make_stable_mask(bool red, bool yellow, bool green)
{
uint32_t m = 0;
if (red) m |= PHASE_MASK_RED;
if (yellow) m |= PHASE_MASK_YELLOW;
if (green) m |= PHASE_MASK_GREEN;
return m;
}
typedef enum {
TRAFFIC_PHASE_UNKNOWN = 0,
TRAFFIC_PHASE_OFF,
TRAFFIC_PHASE_RED,
TRAFFIC_PHASE_YELLOW,
TRAFFIC_PHASE_GREEN,
TRAFFIC_PHASE_RED_YELLOW,
TRAFFIC_PHASE_CONFLICT,
} traffic_phase_t;
static traffic_phase_t decode_phase(uint32_t mask)
{
switch (mask) {
case 0:
return TRAFFIC_PHASE_OFF;
case PHASE_MASK_RED:
return TRAFFIC_PHASE_RED;
case PHASE_MASK_YELLOW:
return TRAFFIC_PHASE_YELLOW;
case PHASE_MASK_GREEN:
return TRAFFIC_PHASE_GREEN;
case PHASE_MASK_RED | PHASE_MASK_YELLOW:
return TRAFFIC_PHASE_RED_YELLOW;
default:
return TRAFFIC_PHASE_CONFLICT;
}
}8. Минимальное время фазы
Светофорные фазы обычно не меняются каждые 100 мс.
#define MIN_PHASE_HOLD_US 100000
typedef struct {
traffic_phase_t current;
traffic_phase_t candidate;
int64_t current_since_us;
int64_t candidate_since_us;
uint32_t current_mask;
uint32_t candidate_mask;
uint32_t phase_change_count;
uint32_t conflict_count;
} phase_detector_t;Обновление:
static bool phase_detector_update(phase_detector_t *det,
uint32_t stable_mask,
int64_t now_us,
phase_event_t *out)
{
traffic_phase_t decoded = decode_phase(stable_mask);
if (decoded != det->candidate) {
det->candidate = decoded;
det->candidate_mask = stable_mask;
det->candidate_since_us = now_us;
return false;
}
bool candidate_old_enough =
(now_us - det->candidate_since_us) >= MIN_PHASE_HOLD_US;
bool conflict = (decoded == TRAFFIC_PHASE_CONFLICT);
if ((candidate_old_enough || conflict) && decoded != det->current) {
out->old_phase = det->current;
out->new_phase = decoded;
out->detected_us = det->candidate_since_us;
out->confirmed_us = now_us;
out->emitted_us = now_us;
out->stable_mask = stable_mask;
det->current = decoded;
det->current_mask = stable_mask;
det->current_since_us = now_us;
det->phase_change_count++;
if (conflict) {
det->conflict_count++;
}
return true;
}
return false;
}9. Как избежать ложного OFF между фазами
Реальное переключение:
RED выключился
через 50 мс включился GREENНаивный алгоритм может выдать:
RED -> OFF -> GREENМожно ввести правило:
OFF считается отдельной фазой только если длится дольше OFF_REPORT_USНапример:
#define OFF_REPORT_US 200000Политика:
OFF < 200 мс:
переходное состояние, не отправлять как отдельную фазу
OFF >= 200 мс:
отправить PHASE_OFF
CONFLICT:
отправлять быстро, это аварийная диагностика10. Рекомендуемая task-схема
input_task
period: 1-5 мс
reads GPIO raw
updates ac_channel_update()
builds stable_mask
calls phase_detector_update()
sends phase_event_t to phase_event_queueПример:
#define INPUT_TASK_PERIOD_MS 2
void input_task(void *arg)
{
phase_event_t ev;
while (1) {
int64_t now_us = esp_timer_get_time();
bool red_raw = gpio_get_level(RED_GPIO) == RED_ACTIVE_LEVEL;
bool yellow_raw = gpio_get_level(YELLOW_GPIO) == YELLOW_ACTIVE_LEVEL;
bool green_raw = gpio_get_level(GREEN_GPIO) == GREEN_ACTIVE_LEVEL;
ac_channel_update(&s_red, red_raw, now_us);
ac_channel_update(&s_yellow, yellow_raw, now_us);
ac_channel_update(&s_green, green_raw, now_us);
uint32_t stable_mask = make_stable_mask(
s_red.stable,
s_yellow.stable,
s_green.stable
);
if (phase_detector_update(&s_phase, stable_mask, now_us, &ev)) {
ev.emitted_us = esp_timer_get_time();
if (xQueueSend(phase_event_queue, &ev, 0) != pdTRUE) {
diag.phase_event_queue_drops++;
}
}
vTaskDelay(pdMS_TO_TICKS(INPUT_TASK_PERIOD_MS));
}
}11. Типичные ошибки
- делать OFF слишком быстрым;
- отправлять каждую внутреннюю смену present;
- путать detected_us и confirmed_us;
- считать RED+GREEN обычным состоянием;
- логировать каждую итерацию input_task;
- считать, что GPIO glitch filter решит задачу AC-детектирования.
12. Практическое задание
Создай PHASE_DETECTOR_POLICY.md:
# Phase detector policy
## Input model
Raw GPIO level is not a final phase signal.
For AC optocoupler input:
- raw_active means GPIO currently sees active pulse
- present means pulse was seen recently
- stable means present survived confirmation time
## Timing constants
Initial values:
- INPUT_TASK_PERIOD_MS = 2
- AC_PRESENT_TIMEOUT_US = 45000
- AC_STABLE_ON_US = 25000
- AC_STABLE_OFF_US = 70000
- MIN_PHASE_HOLD_US = 100000
- OFF_REPORT_US = 200000
These values must be validated on oscilloscope/HIL.
## Timestamp policy
phase_event_t contains:
- detected_us
- confirmed_us
- emitted_us
Network send time must not replace physical detection time.
## Phase rules
Valid:
- RED
- YELLOW
- GREEN
- RED_YELLOW
- OFF if long enough
Fault:
- RED + GREEN
- YELLOW + GREEN if not expected
- RED + YELLOW + GREENHIL-сценарии:
1. RED pulse train 50/100 Hz for 3 s
expected: OFF/UNKNOWN -> RED
2. RED pulse train stops for 30 ms and resumes
expected: no RED -> OFF event
3. RED stops, GREEN starts after 80 ms
expected: RED -> GREEN or RED -> TRANSITION -> GREEN
4. RED and GREEN pulse together for 300 ms
expected: CONFLICT event
5. Single YELLOW glitch 5 ms during RED
expected: no YELLOW phase event
6. No pulses for 1 s
expected: OFF13. Короткий итог
Для 220 В AC после оптопары фаза определяется не мгновенным уровнем GPIO, а наличием импульсов в окне времени и подтверждением стабильности. Правильная цепочка:
raw GPIO
-> raw_active
-> present: импульс был недавно
-> stable: состояние подтверждено
-> stable_mask
-> decode_phase()
-> phase_event_t
-> transport_taskСтартовые значения:
INPUT_TASK_PERIOD_MS = 2 мс
AC_PRESENT_TIMEOUT_US = 45 мс
AC_STABLE_ON_US = 25 мс
AC_STABLE_OFF_US = 70 мс
MIN_PHASE_HOLD_US = 100 мс
OFF_REPORT_US = 200 мсГлавное: Timestamp изменения фазы должен появляться до сети, модема, MQTT/TCP/UDP и логов.
Задание
Event имеет detected_us=12000, confirmed_us=92000, emitted_us=97000. Вычислите filter_delay и queue_delay по формулам урока. Почему замена detected_us временем network send теряет информацию?
Критерии самопроверки: Получите 80 ms и 5 ms, сохранив все три timestamps вместо network send time.
Показать ответ автора
filter_delay=92000-12000=80000 µs=80 ms. queue_delay=97000-92000=5000 µs=5 ms. Detection, confirmation и emission описывают разные стадии; network-send timestamp скрывает физическое detection time и мешает отделить filter delay от queue/network delay.