Embedded HandbookПриложения
Все разделы › III. Периферия
STM32F103 · уровень средний · 26 мин чтения
Открыть в интерактивном справочнике

22ж. ИК-пульт и протокол NEC: приёмник VS1838/TSOP, захват таймера, декодер на прерываниях, повтор и шум

В разделах 22д и 22е микроконтроллер выводил: символы на экран, цифры и точки на индикаторы. Здесь — ввод, и самый дешёвый беспроводной: пульт от телевизора и трёхвыводной ИК-приёмник. Приёмник сам снимает несущую и отдаёт на вывод вспышки и паузы; прошивке остаётся измерить их таймером и отличить ноль от единицы, не потеряв кадр от дрожания фронтов и засветки лампой. Мини-проект 198о делает то же на STM32G474 с готовым блоком IRTIM; здесь — основа на STM32F103: что приходит с приёмника, кадр NEC, захват TIM2 в режиме ШИМ-входа, автомат декодера в прерывании, удержание кнопки, помехи и коротко — передача.

Опоры: RM0008 (DocID13902 Rev 17): частота таймеров на APB1 удваивается, с. 93; захват, с. 383; ШИМ-вход, с. 384; ведомый режим сброса, с. 394; бит CKD, с. 403; бит URS, с. 404; фильтр IC1F, с. 414; CC1P и зарезервированные биты 3:2 CCER, с. 417. DocID13587 Rev 17 (STM32F103x8/xB): PA0 — TIM2_CH1_ETR без допуска 5 В, PA6 — TIM3_CH1, с. 29. Документов на ИК-приёмники и на протокол NEC в проекте нет: времена NEC — общеизвестные, те же, что в разделе 198о и в стенде лаборатории. Все числа ниже — прогон декодера _verify/irnec22h/irnec22h.c на компьютере (gcc 12.2.0) со своей моделью пульта, приёмника и TIM2 на Python 3.11; прошивка собрана armclang 6.24 под Cortex-M3.

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

Оригинал Перевод
carrier несущая
demodulator демодулятор: снимает несущую, оставляет огибающую
mark, burst / space пачка несущей / пауза
leader заголовок кадра
repeat code код повтора
extended NEC расширенный NEC, адрес 16 бит
input capture захват
PWM input mode режим ШИМ-входа
slave mode: reset mode ведомый режим: сброс счётчика
update request source (URS) что поднимает флаг обновления
input filter, sampling frequency фильтр входа, частота выборки
duty cycle заполнение

Вверху — выход ИК-приёмника на PA0 в кадре NEC: «0», пока идёт пачка несущей 38 кГц, в покое «1»; заголовок — пачка 9 мс и пауза 4,5 мс, бит 0 — 1,125 мс от спада до спада, бит 1 — 2,25 мс, повтор — 9 мс и 2,25 мс каждые 108 мс; каждый спад защёлкивает в CCR1 период и сбрасывает счётчик TIM2, каждый фронт защёлкивает в CCR2 длину пачки и вызывает прерывание. Посередине — подключение к STM32F103: выход приёмника VS1838 или TSOP на PA0, TIM2 в режиме ШИМ-входа на 1 МГц с переполнением через 65,536 мс, функция ir_capture; передатчик — несущая TIM3_CH1 на PA6 с периодом 1895 тактов и CCR1 631, огибающая — TIM4 на 2 МГц, ИК-светодиод через транзистор. Внизу — автомат декодера: ждём заголовок 13,5 мс, 32 бита по 1,125 или 2,25 мс, кадр с проверкой инверсии команды, удержание с повтором 11,25 мс; допуск ±25 %, заголовок и повтор делит середина 12 375 мкс, пачка короче 250 мкс — иголка помехи, нет повтора дольше 120 мс — кнопку отпустили


22ж.1 Что отдаёт ИК-приёмник

Пульт светит не постоянно, а пачками несущей — у NEC это 38 кГц. Внутри трёхвыводного приёмника (VS1838, серия TSOP и похожие) стоят фотодиод, усилитель, фильтр на частоту несущей и демодулятор. На выходе — уже огибающая: «0», пока несущая есть, «1» в паузе. Выход активный низкий, поэтому начало пачки — спад, конец — фронт. Помехи от ламп и солнца на выходе выглядят короткими ложными «0» — так их рисует модель стенда лаборатории, о них 22ж.5.

Насколько приёмник запаздывает и на сколько растягивает пачку, написано в документе на конкретный приёмник, а его в проекте нет. Поэтому декодер ниже на длину пачки не опирается: биты он различает по периоду от спада до спада, а он от растяжки пачки не меняется (это разобрано в разделе 198о.4). Модель в проверке, как и модель стенда лаборатории, удлиняет каждую пачку на 50 мкс — это допущение модели, а не данные приёмника.

Подключение — три провода: питание, земля и выход на PA0. Вход PA0 у F103 5 В не допускает (с. 29), так что приёмник с выходом на PA0 питают от 3,3 В, если его документ это разрешает, или ставят делитель. PA0 — только вход: выход push-pull на том же проводе спорит с выходом приёмника.


22ж.2 Кадр NEC

Шаг протокола — 562,5 мкс. Кадр:

Часть Пачка Пауза От спада до спада
заголовок 9 мс 4,5 мс 13,5 мс
бит 0 562,5 мкс 562,5 мкс 1,125 мс
бит 1 562,5 мкс 1687,5 мкс 2,25 мс
стоп 562,5 мкс — —
повтор 9 мс 2,25 мс, потом стоп-пачка 11,25 мс

32 бита идут младшим вперёд: адрес, инверсия адреса, команда, инверсия команды. Кнопка CH- пульта из стенда (адрес 0x00, команда 0x45) даёт слово BA45FF00: младший байт 00 уходит первым, за ним FF, 45 и BA. Инверсии дают ровно 16 единиц в любом кадре, поэтому кадр всегда одной длины — 67,5 мс до стоп-пачки и 68,0625 мс с ней. Пока кнопку держат, пульт кадр не повторяет: через 108 мс от начала кадра и дальше каждые 108 мс идёт короткий код повтора без адреса и команды.

Расширенный NEC. Адресов в 8 бит мало, и часть пультов вместо инверсии адреса передают второй байт адреса: адрес 16 бит, инверсию отдельно проверяет только команда. Адрес 0x1234 и команда 0x08 — слово F7081234. Декодер отличает варианты сам: если второй байт — инверсия первого, кадр обычный, иначе расширенный. Декодер Flipper Zero идёт дальше: кадр без инверсии адреса или команды он отдаёт как расширенный с 16-битными адресом и командой (раздел 94б.5); этот декодер кадр без инверсии команды отбрасывает.


22ж.3 Захват на TIM2: режим ШИМ-входа

Каналу захвата у F103 задаётся один край: бит CC1P выбирает фронт или спад, а биты 3:2 регистра CCER зарезервированы (с. 417): двух краёв сразу один канал не ловит. Есть три пути:

Здесь третий — тот же режим, что у эха дальномера в разделе 100.3, только края наоборот. По спаду в CCR1 защёлкивается время от прошлого спада — период, и счётчик начинает с нуля; по фронту в CCR2 защёлкивается длина пачки, и приходит прерывание. Одно прерывание на пачку: в кадре 34 пачки — заголовок, 32 бита и стоп.

static void rx_init(void)
{
    GPIOA->CRL = (GPIOA->CRL & ~0xFu) | 0x4u;          /* PA0 — вход без подтяжки */
    TIM2->PSC = 71u;                                    /* 72 МГц / 72 = 1 МГц */
    TIM2->ARR = 0xFFFFu;                                /* период 65,536 мс — длиннее любого интервала в кадре */
    TIM2->CCMR1 = TIM_CCMR1_CC1S_0 | TIM_CCMR1_CC2S_1   /* IC1 и IC2 — оба от TI1 (PA0) */
                | TIM_CCMR1_IC1F;                       /* фильтр 1111: 8 выборок на fDTS / 32 */
    TIM2->CCER = TIM_CCER_CC1P | TIM_CCER_CC1E | TIM_CCER_CC2E;  /* IC1 — спад, IC2 — фронт */
    TIM2->SMCR = TIM_SMCR_TS_2 | TIM_SMCR_TS_0 | TIM_SMCR_SMS_2; /* TI1FP1 сбрасывает счётчик */
    TIM2->CR1 = TIM_CR1_CKD_1 | TIM_CR1_URS;            /* fDTS = 72 / 4; UIF — только переполнение */
    TIM2->EGR = TIM_EGR_UG;                             /* загрузить PSC */
    TIM2->SR = 0;
    TIM2->DIER = TIM_DIER_CC2IE | TIM_DIER_UIE;
    TIM2->CR1 |= TIM_CR1_CEN;
    ir_init(&ir, IR_NOISE_US);
    NVIC_EnableIRQ(TIM2_IRQn);
}

PSC и ARR. TIM2 висит на APB1 (36 МГц), и раз делитель APB1 не 1, частота таймера удвоена — 72 МГц (с. 93). PSC = 71 даёт счёт 1 МГц, отсчёт — 1 мкс, ARR = 0xFFFF — период 65,536 мс. Период таймера обязан быть длиннее самого длинного интервала внутри кадра — 13,5 мс от спада заголовка до спада первого бита. Иначе счётчик успевает перевалить через ARR, и CCR1 отдаёт остаток. С забытым делителем (PSC = 0) отсчёт — 13,9 нс, период — 910,2 мкс; модель насчитала 62 переполнения за один кадр, декодер не принял ни одного кадра. С 65,536 мс переполнение внутри кадра невозможно, и оно само становится сигналом: 65,536 мс без единого спада. Декодер узнаёт по нему оборванный кадр и отпущенную кнопку.

URS = 1 нужен, чтобы сброс счётчика спадом не поднимал флаг обновления (с. 404) — иначе «переполнение» приходило бы на каждой пачке (как в разделе 100.3). Цифровой фильтр входа здесь почти ничего не решает: при CKD = 10 и IC1F = 1111 выборка идёт на 72 / 4 / 32 = 0,5625 МГц, и нужно 8 одинаковых выборок — фильтр съедает лишь иголки короче 14,2 мкс. С помехами в сотню микросекунд справляется программа (22ж.5).

Прерывание читает оба регистра и отдаёт пару декодеру:

void TIM2_IRQHandler(void)
{
    uint32_t sr = TIM2->SR;
    int ev = IR_NONE;
    if (sr & TIM_SR_UIF) {
        TIM2->SR = ~TIM_SR_UIF;
        ev = ir_overflow(&ir);                          /* 65,536 мс без спада */
    }
    if (sr & TIM_SR_CC2IF) {                            /* фронт: пачка кончилась */
        uint32_t period = TIM2->CCR1;                   /* от прошлого спада до спада этой пачки */
        uint32_t low = TIM2->CCR2;                      /* длина пачки; чтение снимает CC2IF */
        ev = ir_capture(&ir, period, low);
    }
    if (ev == IR_FRAME) {
        last_cmd = ir.cmd;
        apply(ir.cmd);
    } else if (ev == IR_REPEAT) {
        apply(last_cmd);                                /* повтор — та же команда */
    }
}

В модели первая пачка кадра CH- отдаёт CCR2 = 9050 — пачка 9 мс плюс растяжка модели, — а вторая: CCR1 = 13500, CCR2 = 612. Шесть кнопок подряд — 204 прерывания по фронту, по 34 на кадр.

Попробуйте в приложении: Лаборатория → стенд «ИК-пульт NEC».

В стенде те же времена кадра, PSC = 71 и ARR = 0xFFFF, а декодер проще — только спады. Там же две ошибки из 22ж.7: забытый делитель и захват по фронту.


22ж.4 Автомат декодера

Решение принимается на фронте, когда известны и пачка, и период прошлого импульса: его пачка запомнена на прошлом фронте, а период кончился спадом текущей пачки. Сравнение — с допуском ±25 % от номинала: бит 0 — от 844 до 1406 мкс, бит 1 — от 1688 до 2812 мкс. Заголовок (13 500 мкс) и повтор (11 250 мкс) с таким допуском перекрываются: их окна 10 125–16 875 и 8438–14 062 мкс. Их делит середина — 12 375 мкс, и оба требуют пачку 9 мс. Допуск считается делением, а не умножением: после долгой тишины период огромный, и p * 100 переполнилось бы (раздел 198о.7).

static int pulse(ir_nec_t *d, uint32_t low, uint32_t p)
{
    if (near(low, IR_MARK9_US) && near(p, IR_LEADER_US) && p > IR_SPLIT_US) {
        d->nbit = 0;                                    /* заголовок: дальше 32 бита */
        d->bits = 0;
        d->since = p;
        return IR_NONE;
    }
    if (near(low, IR_MARK9_US) && near(p, IR_REPEAT_US) && p <= IR_SPLIT_US) {
        d->nbit = -1;
        if (!d->held)
            return IR_NONE;                             /* повтор без принятого кадра — не наш */
        d->since = p;
        d->repeats++;
        return IR_REPEAT;
    }
    if (d->nbit < 0)
        return IR_NONE;                                 /* стоп-пачка и пауза после кадра */
    if (near(p, IR_BIT1_US))
        d->bits |= 1u << d->nbit;                       /* младший бит первым */
    else if (!near(p, IR_BIT0_US))
        return fail(d, p > IR_BIT1_US + IR_BIT1_US / 4u ? IR_E_CUT : IR_E_BIT);
    if (++d->nbit < 32)
        return IR_NONE;
    d->nbit = -1;
    uint8_t a0 = (uint8_t)d->bits, a1 = (uint8_t)(d->bits >> 8);
    uint8_t c = (uint8_t)(d->bits >> 16), nc = (uint8_t)(d->bits >> 24);
    if ((uint8_t)(c ^ nc) != 0xFFu)
        return fail(d, IR_E_CMD);                       /* команда и её инверсия не сошлись */
    d->ext = (uint8_t)(a0 ^ a1) != 0xFFu;               /* адрес без инверсии — расширенный NEC */
    d->addr = d->ext ? (uint16_t)(a0 | a1 << 8) : a0;
    d->cmd = c;
    d->held = 1;
    d->frames++;
    return IR_FRAME;
}

Последний бит кадра решается на фронте стоп-пачки, поэтому кадр CH- готов через 68,1 мс от начала заголовка — сразу, без ожидания тишины. Шесть кнопок из примера лаборатории (CH-, 1, 2, 3, +, PLAY) модель приняла верно: 45 0C 18 5E 15 43. Расширенный кадр — адрес 1234, команда 08, признак ext. Кадр с испорченной инверсией команды (BB вместо BA) даёт ошибку, а не чужую команду.

Дрожание фронтов. Каждый фронт пачки сдвинут на случайное время в пределах ±J, по 200 случайных кадров:

J, мкс 0 50 100 140 150 200 250
принято 200 200 200 200 186 35 1
чужих кодов 0 0 0 0 0 0 0

Период — разность двух спадов, его ошибка не больше 2J. Самое узкое окно — у бита 0: ±281,25 мкс, отсюда граница 140,6 мкс, и прогон с ней сходится: при 140 мкс — все 200, при 150 — уже 186. Остальные кадры не теряются молча: декодер насчитывает ошибки, а проверка инверсии не пропускает ни одного чужого кода.


22ж.5 Повтор, обрыв и помехи

Удержание. «+» держат 0,7 с, как в примере лаборатории: кадр и 6 повторов. Событие повтора приходит на фронте его стоп-пачки, и программа повторяет последнюю команду — громкость vol растёт до 7, потом «-» один раз — 6. Повтор без принятого перед ним кадра декодер пропускает: чья это кнопка, неизвестно.

Отпускание. Между спадами соседних повторов 108 − 11,25 = 96,75 мс — больше периода TIM2, поэтому между повторами всегда одно переполнение. Декодер считает время от начала последнего кадра или повтора и объявляет кнопку отпущенной, когда оно больше 120 мс (108 мс и запас). Без помех это выясняется на втором переполнении — через 142,3 мс после начала последнего повтора.

Обрыв. Пульт отвели в сторону после 20 бит. Если дальше тишина, через 65,536 мс после последнего спада переполнение застаёт автомат внутри кадра — ошибка «оборван». Если сразу нажали другую кнопку, ошибку даёт первый же слишком длинный период, а новый кадр (команда 0x46) принимается целиком.

Засветка. Лампы и солнце дают на выходе приёмника короткие ложные «0». Модель повторяет стенд лаборатории: иголки 80–200 мкс, в окне 4 мс с вероятностью 0,6. За 30 с — 4507 иголок, 150,2 в секунду. Иголка в паузе делит период на два коротких, и простой декодер теряет всё: 0 из 200 кадров, 100 ошибок. Помогает длина пачки, которую дал режим ШИМ-входа: настоящая пачка не короче 562,5 мкс, иголка — до 200 мкс. Пачка короче 250 мкс — помеха: её время прибавляется к паузе, будто спада не было:

int ir_capture(ir_nec_t *d, uint32_t period_us, uint32_t low_us)
{
    int ev = IR_NONE;
    add(&d->since, period_us);
    add(&d->acc, period_us);
    if (low_us < d->noise_us) {
        d->noise++;                                     /* иголка: её спад не начало пачки, время копится дальше */
    } else {
        ev = pulse(d, d->low, d->acc);                  /* прошлый настоящий импульс закончился этим спадом */
        d->low = low_us;
        d->acc = 0;
    }
    if (ev == IR_NONE && d->held && d->since > IR_HOLD_US) {
        d->held = 0;
        ev = IR_RELEASE;
    }
    return ev;
}

С порогом 250 мкс — 200 из 200: отброшено 3470 иголок, остальные попали внутрь пачек и ничего не изменили. Удержание «+» под засветкой даёт ровно те же события: кадр, 6 повторов, отпускание, кадр «-». Чужих кодов не было ни с порогом, ни без него — их не пропускает проверка инверсии команды.


22ж.6 Передача: несущая и огибающая

Передатчик — обратная задача: огибающая кадра включает и гасит несущую 38 кГц. Несущая — ШИМ TIM3 на PA6 (TIM3_CH1, с. 29): 72 МГц / 38 кГц = 1894,7, период 1895 тактов (ARR = 1894) — 37 994,7 Гц, на 0,014 % ниже 38 кГц, один период — 26,3 мкс, на пачку 562,5 мкс их 21,4. CCR1 = 631 — пачка несущей в треть периода. Огибающую ведёт TIM4 на 2 МГц (PSC = 35): такт 0,5 мкс, и все времена NEC в нём целые — 562,5 мкс = 1125 тактов. По концу каждого отрезка прерывание включает или гасит несущую и ставит длину следующего:

void TIM4_IRQHandler(void)
{
    TIM4->SR = ~TIM_SR_UIF;
    if (++tx_i < tx_n) {
        TIM3->CCR1 = (tx_i & 1u) ? 0u : CAR_DUTY;       /* чётный отрезок — пачка, нечётный — пауза */
        TIM4->ARR = tx_t[tx_i] - 1u;                    /* без предзагрузки: действует на текущий отрезок */
    } else {
        TIM3->CCR1 = 0;
        TIM4->CR1 &= ~TIM_CR1_CEN;
    }
}

Таблицу готовит кодер ir_encode: 67 отрезков, 136 125 тактов — 68 062,5 мкс, как у кадра в 22ж.2; повтор — 3 отрезка, 23 625 тактов, 11 812,5 мкс. В стенде таблица прошла через модель приёмника и TIM2 в декодер: «+» и расширенный кадр 1234/08 приняты, отрезки совпали с моделью пульта.

CCR1 у TIM3 с предзагрузкой: новое значение вступает со следующего периода несущей, и пачка начинается позже на долю периода — до 26,3 мкс. Это в разы меньше границы 140,6 мкс из 22ж.4. ИК-светодиод включают через транзистор, ток задаёт резистор по документу на светодиод — здесь он не считался. У G474 несущую и огибающую смешивает готовый блок IRTIM (раздел 198о.5).


22ж.7 Грабли

Кодов нет, захваты есть, PSC = 0. Счёт 72 МГц, период 910,2 мкс — 62 переполнения за кадр. Нужен счёт 1 МГц (PSC = 71) и период больше 13,5 мс.

Кодов нет при правильном делителе. Захват по фронту вместо спада: фронт — конец пачки, и от конца заголовка до конца первого бита 5062,5 мкс, а 13,5 мс между фронтами не бывает. Декодер не начинает ни одного кадра. CC1P = 1 — спад.

Флаг обновления на каждой пачке. Нет URS: сброс счётчика спадом поднимает UIF на каждом спаде, и декодер принимает его за 65,536 мс тишины.

Кадры теряются у окна. Иголки засветки режут паузы: без отбраковки 0 из 200. Длину пачки даёт режим ШИМ-входа; иголки короче 250 мкс не считать.

Удержание даёт одну команду. Программа не обрабатывает повтор: пульт шлёт кадр один раз, дальше только коды повтора без команды. Помнить последнюю команду.

Бит по паузе. Приёмник растягивает пачку, и пауза короче на ту же величину; период от спада до спада от этого не меняется (198о.4).

Приёмник на 5 В, выход на PA0. PA0 без допуска 5 В; питать приёмник от 3,3 В, если он это позволяет, или ставить делитель. И PA0 — вход, не выход push-pull.

Расширенный пульт «не работает». Декодер требует инверсию адреса — у расширенного NEC её нет. Проверять инверсию только у команды.


22ж.8 Итоги

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

irnec22h.h — времена NEC, состояние и события декодера, кодер.

/* irnec22h.h — раздел 22ж: приём и передача ИК-пульта NEC без регистров.
 * Декодер получает от захвата TIM2 пары «период от спада до спада» и «длина нуля» (пачки) в мкс:
 * в прошивке fw22h.c их даёт TIM2 в режиме ШИМ-входа, в стенде host22h.c — модель model22h.py.
 * Кодер готовит таблицу длительностей огибающей для передатчика в тактах 0,5 мкс. */
#ifndef IRNEC22H_H
#define IRNEC22H_H
#include <stdint.h>

/* ── времена NEC, мкс ─────────────────────────────────────────────────── */
#define IR_LEADER_US  13500u            /* заголовок: пачка 9 мс + пауза 4,5 мс */
#define IR_REPEAT_US  11250u            /* повтор: пачка 9 мс + пауза 2,25 мс */
#define IR_BIT0_US     1125u            /* бит 0: пачка 562,5 + пауза 562,5 */
#define IR_BIT1_US     2250u            /* бит 1: пачка 562,5 + пауза 1687,5 */
#define IR_MARK9_US    9000u            /* пачка заголовка и повтора */
#define IR_SPLIT_US   12375u            /* середина между повтором и заголовком */
#define IR_NOISE_US     250u            /* «ноль» короче — иголка помехи, а не пачка */
#define IR_HOLD_US   120000u            /* нет повтора дольше — кнопку отпустили */
#define IR_OVF_US     65536u            /* переполнение TIM2: 1 МГц, ARR = 0xFFFF */

/* ── события декодера ─────────────────────────────────────────────────── */
enum { IR_NONE, IR_FRAME, IR_REPEAT, IR_RELEASE, IR_ERROR };
enum { IR_E_BIT = 1, IR_E_CUT, IR_E_CMD };              /* причина ошибки */

typedef struct {
    uint32_t noise_us;                  /* порог иголки; 0 — без отбраковки */
    uint32_t acc;                       /* время с последнего настоящего спада, мкс */
    uint32_t low;                       /* длина нуля последнего настоящего импульса */
    uint32_t since;                     /* время от начала последнего кадра или повтора */
    uint32_t bits;                      /* принятые биты, младший первым */
    int32_t nbit;                       /* -1 — ждём заголовок */
    uint8_t held;                       /* кадр принят, ждём повторы */
    uint8_t err;                        /* причина последней ошибки */
    uint8_t ext;                        /* 1 — расширенный NEC: адрес 16 бит */
    uint8_t cmd;
    uint16_t addr;
    uint32_t frames, repeats, errors, noise;
} ir_nec_t;

void ir_init(ir_nec_t *d, uint32_t noise_us);
int ir_capture(ir_nec_t *d, uint32_t period_us, uint32_t low_us);  /* по фронту: период и длина нуля */
int ir_overflow(ir_nec_t *d);                                       /* 65,536 мс без спада */

/* ── передатчик: огибающая в тактах 0,5 мкс (TIM4 на 2 МГц), пачка, пауза, пачка, … ── */
#define IR_TX_MAX 67u                   /* 2 + 32 × 2 + 1 */
uint32_t ir_encode(uint16_t addr, uint8_t ext, uint8_t cmd, uint16_t t[IR_TX_MAX]);
uint32_t ir_encode_repeat(uint16_t t[3]);

#endif

irnec22h.c — декодер на парах захвата и кодер огибающей без регистров: общая часть прошивки и стенда.

/* irnec22h.c — раздел 22ж: декодер NEC на парах захвата и кодер огибающей (без регистров). */
#include "irnec22h.h"

static int near(uint32_t p, uint32_t nom)
{
    return p >= nom - nom / 4u && p <= nom + nom / 4u;  /* ±25 %: деление, а не умножение — без переполнения */
}

static void add(uint32_t *x, uint32_t v)
{
    *x = *x < 0xF0000000u - v ? *x + v : 0xF0000000u;  /* время копится долго: не дать ему перевалить через 0 */
}

void ir_init(ir_nec_t *d, uint32_t noise_us)
{
    *d = (ir_nec_t){0};
    d->noise_us = noise_us;
    d->nbit = -1;
}

static int fail(ir_nec_t *d, uint8_t why)
{
    d->nbit = -1;
    d->err = why;
    d->errors++;
    return IR_ERROR;
}

/* Импульс целиком: пачка low и период p от её спада до следующего настоящего спада. */
static int pulse(ir_nec_t *d, uint32_t low, uint32_t p)
{
    if (near(low, IR_MARK9_US) && near(p, IR_LEADER_US) && p > IR_SPLIT_US) {
        d->nbit = 0;                                    /* заголовок: дальше 32 бита */
        d->bits = 0;
        d->since = p;
        return IR_NONE;
    }
    if (near(low, IR_MARK9_US) && near(p, IR_REPEAT_US) && p <= IR_SPLIT_US) {
        d->nbit = -1;
        if (!d->held)
            return IR_NONE;                             /* повтор без принятого кадра — не наш */
        d->since = p;
        d->repeats++;
        return IR_REPEAT;
    }
    if (d->nbit < 0)
        return IR_NONE;                                 /* стоп-пачка и пауза после кадра */
    if (near(p, IR_BIT1_US))
        d->bits |= 1u << d->nbit;                       /* младший бит первым */
    else if (!near(p, IR_BIT0_US))
        return fail(d, p > IR_BIT1_US + IR_BIT1_US / 4u ? IR_E_CUT : IR_E_BIT);
    if (++d->nbit < 32)
        return IR_NONE;
    d->nbit = -1;
    uint8_t a0 = (uint8_t)d->bits, a1 = (uint8_t)(d->bits >> 8);
    uint8_t c = (uint8_t)(d->bits >> 16), nc = (uint8_t)(d->bits >> 24);
    if ((uint8_t)(c ^ nc) != 0xFFu)
        return fail(d, IR_E_CMD);                       /* команда и её инверсия не сошлись */
    d->ext = (uint8_t)(a0 ^ a1) != 0xFFu;               /* адрес без инверсии — расширенный NEC */
    d->addr = d->ext ? (uint16_t)(a0 | a1 << 8) : a0;
    d->cmd = c;
    d->held = 1;
    d->frames++;
    return IR_FRAME;
}

int ir_capture(ir_nec_t *d, uint32_t period_us, uint32_t low_us)
{
    int ev = IR_NONE;
    add(&d->since, period_us);
    add(&d->acc, period_us);
    if (low_us < d->noise_us) {
        d->noise++;                                     /* иголка: её спад не начало пачки, время копится дальше */
    } else {
        ev = pulse(d, d->low, d->acc);                  /* прошлый настоящий импульс закончился этим спадом */
        d->low = low_us;
        d->acc = 0;
    }
    if (ev == IR_NONE && d->held && d->since > IR_HOLD_US) {
        d->held = 0;
        ev = IR_RELEASE;
    }
    return ev;
}

int ir_overflow(ir_nec_t *d)
{
    add(&d->since, IR_OVF_US);
    add(&d->acc, IR_OVF_US);
    if (d->nbit >= 0)
        return fail(d, IR_E_CUT);                       /* внутри кадра спады идут не реже 13,5 мс */
    if (d->held && d->since > IR_HOLD_US) {
        d->held = 0;
        return IR_RELEASE;
    }
    return IR_NONE;
}

/* ── передатчик: такт 0,5 мкс ─────────────────────────────────────────── */
#define T_MARK9  18000u                 /* 9 мс */
#define T_SPACE  9000u                  /* 4,5 мс */
#define T_RSPACE 4500u                  /* 2,25 мс */
#define T_BIT    1125u                  /* 562,5 мкс */

uint32_t ir_encode(uint16_t addr, uint8_t ext, uint8_t cmd, uint16_t t[IR_TX_MAX])
{
    uint32_t a = ext ? addr : (uint32_t)(addr & 0xFFu) | (uint32_t)(~addr & 0xFFu) << 8;
    uint32_t bits = a | (uint32_t)cmd << 16 | (uint32_t)(uint8_t)~cmd << 24;
    uint32_t n = 0;
    t[n++] = T_MARK9;
    t[n++] = T_SPACE;
    for (uint32_t i = 0; i < 32u; i++) {
        t[n++] = T_BIT;
        t[n++] = (uint16_t)((bits >> i) & 1u ? 3u * T_BIT : T_BIT);
    }
    t[n++] = T_BIT;                                     /* стоп-пачка: конец последней паузы */
    return n;
}

uint32_t ir_encode_repeat(uint16_t t[3])
{
    t[0] = T_MARK9;
    t[1] = T_RSPACE;
    t[2] = T_BIT;
    return 3u;
}

fw22h.c — прошивка STM32F103: приём на TIM2 (PA0), передача TIM3 + TIM4 (PA6).

/* fw22h.c — раздел 22ж: ИК-пульт NEC на STM32F103 (BluePill), 72 МГц.
 * Приём: выход ИК-приёмника на PA0 (TIM2_CH1), TIM2 в режиме ШИМ-входа, счёт 1 МГц:
 *   спад сбрасывает счётчик и защёлкивает CCR1 (период), фронт защёлкивает CCR2 (длина нуля) и зовёт прерывание.
 * Передача: несущая 38 кГц — TIM3_CH1 на PA6 (ШИМ, треть периода), огибающая — TIM4 на 2 МГц:
 *   по концу каждого отрезка прерывание TIM4 включает или гасит несущую и ставит длину следующего.
 * Кнопка на PB0 (к земле): пока нажата — «+» (0x15), потом повтор каждые 108 мс. Принятые «+» и «-» меняют vol.
 * Тактирование до 72 МГц — как в разделе 6 (здесь не повторяется); APB1 = 36 МГц, таймеры на нём — 72 МГц. */
#include "stm32f10x.h"
#include "irnec22h.h"

#define TIM_HZ   72000000u
#define CAR_HZ   38000u
#define CAR_ARR  ((TIM_HZ + CAR_HZ / 2u) / CAR_HZ - 1u)
#define CAR_DUTY ((CAR_ARR + 1u) / 3u)

_Static_assert(CAR_ARR == 1894u, "72 МГц / 38 кГц = 1894,7 → период 1895 тактов");
_Static_assert(CAR_DUTY == 631u, "пачка несущей — треть периода");
_Static_assert(TIM_HZ / 1000000u - 1u == 71u, "TIM2: PSC = 71 — счёт 1 МГц, отсчёт 1 мкс");

static ir_nec_t ir;
static volatile int32_t vol;
static volatile uint8_t last_cmd;

static uint16_t tx_t[IR_TX_MAX];
static volatile uint32_t tx_n, tx_i;

static void apply(uint8_t cmd)
{
    if (cmd == 0x15u)
        vol++;
    if (cmd == 0x07u)
        vol--;
    GPIOC->ODR ^= 1u << 13;                             /* светодиод платы: событие принято */
}

/* ── приём ────────────────────────────────────────────────────────────── */
void TIM2_IRQHandler(void)
{
    uint32_t sr = TIM2->SR;
    int ev = IR_NONE;
    if (sr & TIM_SR_UIF) {
        TIM2->SR = ~TIM_SR_UIF;
        ev = ir_overflow(&ir);                          /* 65,536 мс без спада */
    }
    if (sr & TIM_SR_CC2IF) {                            /* фронт: пачка кончилась */
        uint32_t period = TIM2->CCR1;                   /* от прошлого спада до спада этой пачки */
        uint32_t low = TIM2->CCR2;                      /* длина пачки; чтение снимает CC2IF */
        ev = ir_capture(&ir, period, low);
    }
    if (ev == IR_FRAME) {
        last_cmd = ir.cmd;
        apply(ir.cmd);
    } else if (ev == IR_REPEAT) {
        apply(last_cmd);                                /* повтор — та же команда */
    }
}

static void rx_init(void)
{
    GPIOA->CRL = (GPIOA->CRL & ~0xFu) | 0x4u;          /* PA0 — вход без подтяжки */
    TIM2->PSC = 71u;                                    /* 72 МГц / 72 = 1 МГц */
    TIM2->ARR = 0xFFFFu;                                /* период 65,536 мс — длиннее любого интервала в кадре */
    TIM2->CCMR1 = TIM_CCMR1_CC1S_0 | TIM_CCMR1_CC2S_1   /* IC1 и IC2 — оба от TI1 (PA0) */
                | TIM_CCMR1_IC1F;                       /* фильтр 1111: 8 выборок на fDTS / 32 */
    TIM2->CCER = TIM_CCER_CC1P | TIM_CCER_CC1E | TIM_CCER_CC2E;  /* IC1 — спад, IC2 — фронт */
    TIM2->SMCR = TIM_SMCR_TS_2 | TIM_SMCR_TS_0 | TIM_SMCR_SMS_2; /* TI1FP1 сбрасывает счётчик */
    TIM2->CR1 = TIM_CR1_CKD_1 | TIM_CR1_URS;            /* fDTS = 72 / 4; UIF — только переполнение */
    TIM2->EGR = TIM_EGR_UG;                             /* загрузить PSC */
    TIM2->SR = 0;
    TIM2->DIER = TIM_DIER_CC2IE | TIM_DIER_UIE;
    TIM2->CR1 |= TIM_CR1_CEN;
    ir_init(&ir, IR_NOISE_US);
    NVIC_EnableIRQ(TIM2_IRQn);
}

/* ── передача ─────────────────────────────────────────────────────────── */
void TIM4_IRQHandler(void)
{
    TIM4->SR = ~TIM_SR_UIF;
    if (++tx_i < tx_n) {
        TIM3->CCR1 = (tx_i & 1u) ? 0u : CAR_DUTY;       /* чётный отрезок — пачка, нечётный — пауза */
        TIM4->ARR = tx_t[tx_i] - 1u;                    /* без предзагрузки: действует на текущий отрезок */
    } else {
        TIM3->CCR1 = 0;
        TIM4->CR1 &= ~TIM_CR1_CEN;
    }
}

static void tx_start(uint32_t n)
{
    tx_n = n;
    tx_i = 0;
    TIM4->CNT = 0;
    TIM4->ARR = tx_t[0] - 1u;
    TIM3->CCR1 = CAR_DUTY;                              /* первая пачка */
    TIM4->CR1 |= TIM_CR1_CEN;
}

static void tx_init(void)
{
    GPIOA->CRL = (GPIOA->CRL & ~(0xFu << 24)) | 0xBu << 24;     /* PA6 — TIM3_CH1, выход 50 МГц */
    TIM3->ARR = CAR_ARR;
    TIM3->CCR1 = 0;                                     /* несущая выключена */
    TIM3->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1PE;   /* ШИМ, режим 1 */
    TIM3->CCER = TIM_CCER_CC1E;
    TIM3->CR1 = TIM_CR1_CEN;
    TIM4->PSC = 35u;                                    /* 72 / 36 = 2 МГц: такт 0,5 мкс */
    TIM4->CR1 = TIM_CR1_URS;
    TIM4->EGR = TIM_EGR_UG;
    TIM4->DIER = TIM_DIER_UIE;
    NVIC_EnableIRQ(TIM4_IRQn);
}

int main(void)
{
    uint32_t t0 = 0;
    uint8_t down = 0;
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN;
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN | RCC_APB1ENR_TIM3EN | RCC_APB1ENR_TIM4EN;
    GPIOB->CRL = (GPIOB->CRL & ~0xFu) | 0x8u;          /* PB0 — вход с подтяжкой */
    GPIOB->BSRR = 1u;                                   /* подтяжка вверх */
    GPIOC->CRH = (GPIOC->CRH & ~(0xFu << 20)) | 0x2u << 20;    /* PC13 — светодиод платы */
    rx_init();
    tx_init();
    for (;;) {
        uint32_t now = DWT->CYCCNT;                     /* такты ядра: разность верна и через переполнение */
        if (!(GPIOB->IDR & 1u) && !(TIM4->CR1 & TIM_CR1_CEN)) {
            if (!down) {
                tx_start(ir_encode(0x00u, 0u, 0x15u, tx_t));   /* «+»: полный кадр */
                down = 1;
                t0 = now;
            } else if (now - t0 >= 108u * (TIM_HZ / 1000u)) {
                tx_start(ir_encode_repeat(tx_t));               /* держат — повтор каждые 108 мс */
                t0 += 108u * (TIM_HZ / 1000u);
            }
        } else if (GPIOB->IDR & 1u) {
            down = 0;
        }
    }
}

Дальше: раздел 22з «Цветной TFT-дисплей ST7735 по SPI: инициализация, окно адресов, RGB565, шрифт, DMA и частичное обновление» — от ввода с пульта снова к выводу: цветной экран, который программа рисует точка за точкой.

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