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 заполнение
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): двух краёв
сразу один канал не ловит. Есть три пути:
- ловить только спады — разность двух захватов даёт период, и этого хватает для битов (так сделан пример в лаборатории);
- в каждом прерывании переключать
CC1P— два прерывания на пачку; - режим ШИМ-входа (с. 384): два канала слушают один вход TI1, первый по спаду, второй по фронту, и спад через ведомый режим сброса (с. 394) обнуляет счётчик.
Здесь третий — тот же режим, что у эха дальномера в разделе 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 Итоги
- Приёмник отдаёт огибающую: «0» — пачка 38 кГц, начало пачки — спад.
- NEC: заголовок 9 + 4,5 мс, 32 бита младшим вперёд, бит 0 — 1,125 мс, бит 1 — 2,25 мс от спада до спада; кадр 67,5 мс до стоп-пачки; повтор 9 + 2,25 мс каждые 108 мс.
- TIM2 на F103:
PSC= 71 (1 мкс),ARR= 0xFFFF (65,536 мс), режим ШИМ-входа —CCR1период,CCR2пачка, одно прерывание на пачку, 34 на кадр. - Допуск ±25 %, заголовок и повтор делит 12 375 мкс; дрожание до 140 мкс — 200 из 200 кадров.
- Повтор — та же команда; отпускание — нет повтора дольше 120 мс, узнаётся через 142,3 мс после последнего повтора.
- Засветка: без отбраковки 0 из 200, с порогом 250 мкс — 200 из 200; чужих кодов 0.
- Передача: TIM3 — несущая 37 994,7 Гц, треть периода; TIM4 на 2 МГц — огибающая, 67 отрезков на кадр.
Полные исходники
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 и частичное обновление» — от ввода с пульта снова к выводу: цветной экран, который программа рисует точка за точкой.