198о. Мини-проект: ИК-пульт — приём и передача протокола NEC
Пульт от телевизора — самый дешёвый беспроводной канал: светодиод, приёмник за копейки и протокол, который разбирается таймером. Приёмник ИК снимает несущую сам и отдаёт на вывод огибающую — вспышки и паузы. Задача прошивки — измерить их длительности и отличить ноль от единицы, не испугавшись дрожания фронтов. Передача — обратная: огибающая включает и выключает несущую 38 кГц.
Здесь пульт собран на NUCLEO-G474RE. Приём — захват TIM2 в режиме PWM input: одно прерывание на каждую пару «вспышка + пауза». Передача — блок IRTIM, который у G474 есть готовым: TIM17 даёт несущую, TIM16 — огибающую.
Проверено без платы. Кодер и декодер (ir_core.c) прогнаны в опыте
sim_ir.c: посылки NEC превращаются во фронты выхода приёмника с
дрожанием и растяжкой вспышки, из фронтов получаются пары — такие, какие
отдаёт захват, — и идут в декодер. Опыт собран gcc на ПК и
arm-none-eabi-gcc для Cortex-M4, в qemu-system-arm -M netduinoplus2 вывод
совпал с ПК строка в строку; таймеров G4 QEMU не моделирует, поэтому в
эмуляторе работает только ядро. Модель на Python (ir_host.py) со своим
кодером и декодером повторила все опыты бит в бит и посчитала границу, до
которой декодер не ошибается. Прошивка ir_g474.c собрана armclang по
заголовкам пака G4 без предупреждений; на плате не запускалась.
Опоры: RM0440 — глава 33 «Infrared interface (IRTIM)» и режим PWM input
(29.4.8); DS12288 — альтернативные функции PA0 и PB9; BSP NUCLEO-G474RE —
выводы LPUART1 и такт примера. Описание протокола NEC с разделом не
сверялось: времена — общеизвестные, всё остальное посчитано. Таймеры, захват и ШИМ —
раздел 13, приёмник ИК у датчика присутствия — раздел 100. Проверка —
_verify/ir198o_check.py.
Как это называется в документации
Оригинал Перевод carrier, envelope несущая; огибающая — где несущая включена mark, space вспышка (несущая есть), пауза leader, repeat code ведущий импульс; код повтора, пока кнопку держат PWM input mode захват периода и длины импульса с одного входа slave mode: reset сброс счётчика по входному фронту IR_OUT, I2C_PB9_FMP выход IRTIM; мощный сток на PB9 MOE, main output enable общее разрешение выходов TIM16/TIM17
198о.1 Что делает прибор и из чего он
| Функция | Как |
|---|---|
| приём | выход приёмника ИК на TIM2_CH1 (PA0, AF1), PWM input, такт 2 МГц |
| разбор | пара «вспышка + период» → ведущий импульс, 32 бита или повтор, проверка инверсий |
| передача | IRTIM на PB9 (AF6): TIM17 — несущая 38 кГц, TIM16 — огибающая по таблице |
| проверка | принятое — строкой в LPUART1 (PA2, PA3), виртуальный COM ST-LINK |
Альтернативные функции — по таблице DS12288: PA0 — AF1 TIM2_CH1, PB9 —
AF6 IR_OUT. По RM0440 (глава 33) PB9 и PA13 — единственные выводы с
мощным стоком для светодиода, он включается битом I2C_PB9_FMP в
SYSCFG_CFGR1.
198о.2 Протокол NEC
Шаг протокола — 562,5 мкс. Посылка:
| Часть | Вспышка | Пауза |
|---|---|---|
| ведущий импульс | 9 мс | 4,5 мс |
| бит 0 | 562,5 мкс | 562,5 мкс |
| бит 1 | 562,5 мкс | 1687,5 мкс |
| стоп | 562,5 мкс | — |
| повтор, пока кнопку держат | 9 мс | 2,25 мс, потом стоповая вспышка |
32 бита идут младшим вперёд: адрес, инверсия адреса, команда, инверсия команды. Инверсии дают ровно 16 единиц и 16 нулей в любой посылке, поэтому она всегда одной длины: 34 пары, 67,5 мс до стоповой вспышки и 68,0625 мс с ней. Инверсии — заодно и проверка: байт и его отрицание вместе дают 0xFF.
198о.3 Приём: захват в режиме PWM input
Выход приёмника во время вспышки — низкий (у типичных приёмников ИК так;
у своего — проверить по его документу). Значит, начало вспышки — спад.
TIM2 настраивается так, как описывает RM0440 в 29.4.8: оба канала
захвата смотрят на один вход TI1, первый — по спаду, второй — по фронту,
а спад ещё и сбрасывает счётчик (режим сброса ведомого таймера). На
каждом спаде в CCR1 оказывается время от прошлого спада — период пары,
а в CCR2 — длина прошлой вспышки. Одно прерывание — одна готовая пара.
void TIM2_IRQHandler(void)
{
uint32_t total = TIM2->CCR1, mark = TIM2->CCR2; /* чтение CCR1 сбрасывает CC1IF */
int e = nec_rx_pair(&rx, mark, total);
if (e != NEC_NONE)
last_ev = e;
}
Такт TIM2 — 170 МГц / 85 = 2 МГц ровно, шаг захвата — 0,5 мкс, и все времена NEC — целые числа таких шагов: 562,5 мкс = 1125. Пара ведущего импульса приходит на спаде первого бита, пара последнего бита — на спаде стоповой вспышки, так что посылка готова без ожидания паузы после неё.
Прерываний на посылку — 34. Опрос входа по таймеру на 20 кГц за те же 68,0625 мс — 1361 прерывание, на 10 кГц — 680, и шаг опроса 50 или 100 мкс сам добавляет дрожание.
198о.4 Декодер и его допуски
Декодер сравнивает длительность с номиналом в пределах допуска; если окна
двух номиналов перекрываются, побеждает ближний. Бит можно определять
двумя способами: по паузе (вспышка 562,5 мкс и пауза 562,5 или 1687,5 мкс)
или по периоду от спада до спада (1125 или 2250 мкс). Второй — то, что
сразу даёт CCR1.
Опыт: по 500 посылок со случайными адресом и командой, каждый фронт сдвинут на случайное время в пределах ±J, декодер по периоду:
| Допуск | J = 0 | 50 | 100 | 150 | 200 | 250 | 300 мкс |
|---|---|---|---|---|---|---|---|
| 15 % | 500 | 500 | 332 | 14 | 2 | 0 | 0 |
| 25 % | 500 | 500 | 500 | 475 | 126 | 20 | 2 |
| 35 % | 500 | 500 | 500 | 500 | 496 | 246 | 80 |
Период — разность двух спадов, его ошибка не больше 2J. Отсюда граница, до которой декодер не ошибается ни разу: самое узкое место — бит 0 (1125 мкс), и допуск ограничен ещё серединой до бита 1 (562,5 мкс):
| Допуск | Бит 0 | Бит 1 | Ведущий | Граница |
|---|---|---|---|---|
| 15 % | 84,4 мкс | 168,8 мкс | 562,5 мкс | 84,4 мкс |
| 25 % | 140,6 мкс | 281,2 мкс | 562,5 мкс | 140,6 мкс |
| 35 % | 196,9 мкс | 281,2 мкс | 562,5 мкс | 196,9 мкс |
Таблица опыта с ней согласна: внутри границы — 500 из 500, за ней посылки начинают теряться. Для бита 1 окно упирается в середину уже при 25 %: дальше его ограничивает середина, а не допуск; для бита 0 — при 50 %.
Растяжка вспышки. Если приёмник удлиняет вспышку — её конец запаздывает сильнее начала, — меняется вспышка, а период от спада до спада нет. Опыт — вспышка длиннее на D, дрожание 50 мкс, допуск 25 %:
| Декодер | D = 0 | 100 | 200 | 300 мкс |
|---|---|---|---|---|
| по периоду | 500 | 500 | 500 | 500 |
| по паузе | 500 | 0 | 0 | 0 |
Период от спада до спада растяжки не замечает вовсе. Декодер по паузе проверяет и вспышку: 562,5 + 100 мкс — уже +17,8 %, с дрожанием она выходит за 25 % почти в каждом из 32 битов, и посылка теряется целиком. Поэтому прибор определяет бит по периоду, а вспышку проверяет только на здравый смысл: она короче периода.
198о.5 Передача: IRTIM, TIM17 и TIM16
По RM0440 (глава 33) IRTIM берёт несущую с TIM17_OC1, огибающую — с
TIM16_OC1 и выводит результат на IR_OUT. TIM17 считает 170 МГц: период
4473 такта (ARR = 4472) — 38 005,8 Гц, на 0,0153 % выше 38 кГц;
CCR1 = 1491 — треть периода, 26,31 мкс на период несущей, 21,38
периода на шаг 562,5 мкс.
TIM16 считает на 2 МГц и в режиме ШИМ 1 держит выход, пока счётчик
меньше CCR1. Каждая пара NEC — один период TIM16: ARR = вспышка +
пауза − 1, CCR1 = вспышка:
| Пара | ARR |
CCR1 |
|---|---|---|
| ведущая | 26 999 | 18 000 |
| бит 0 | 2249 | 1125 |
| бит 1 | 4499 | 1125 |
| стоп | 1124 | 1125 |
Самое большое значение — 26 999, в 16 бит TIM16 помещается. Регистры
с предзагрузкой: записанное в прерывании по концу периода вступит со
следующего, поэтому в прерывании пишется пара через одну. После стоповой
вспышки CCR1 = 0 — тишина — и таймер останавливается.
Огибающая и несущая считают независимо, так что вспышка начинается в любой точке периода несущей: её длина гуляет в пределах одного периода, 26,31 мкс. Это меньше границ из 198о.4 в несколько раз.
198о.6 Прошивка и сборка
ir_g474.c — всё на регистрах CMSIS. Такт — HSI16 / 4 × 85 / 2 = 170 МГц,
как в примере BSP платы. Приёмник: CCMR1 отдаёт TI1 обоим каналам,
CCER — спад первому и фронт второму, SMCR — запуск от TI1FP1 и режим
сброса. Передатчик: TIM17 и TIM16 в режиме ШИМ 1 с предзагрузкой, у
обоих — бит MOE. Изменение уровня на PC13 (кнопка B1) посылает команду
0x08 на адрес 0x04, принятое уходит строкой в LPUART1.
Проверки при сборке: 170 МГц / 85 = 2 МГц ровно; ARR несущей = 4472, её
ошибка меньше 0,1 %; CCR1 = 1491; ведущая пара помещается в 16 бит.
armclang 6.24 собрал ядро и прошивку без предупреждений: 560 и 914 байт
кода, 324 байта ОЗУ.
198о.7 Грабли
Бит по паузе. Растяжка вспышки приёмником съедает паузу: при D = 100 мкс декодер по паузе потерял все 500 посылок, по периоду — ни одной.
Допуск умножением. Проверка разность × 100 <= номинал × допуск
переполняется на огромных периодах — а после долгой тишины 32-битный
таймер именно такой и отдаёт. В ядре сначала сравнивается разность с
номиналом, потом умножение.
Перекрытие окон. Ведущий импульс (13,5 мс) и повтор (11,25 мс) при допуске 25 % попадают в окна друг друга. Выбирать ближний номинал.
Без MOE. У TIM16 и TIM17 есть блок защиты с общим разрешением
выходов: без MOE в BDTR на выводе ничего нет.
UG вызвал прерывание. Запись UG для загрузки первой пары поднимает
флаг обновления, и прерывание сдвигает таблицу на шаг. URS = 1 оставляет
прерывание только концу периода.
Слабый вывод. Светодиод на обычном выводе не получит тока; мощный
сток есть только на PB9 и PA13 (I2C_PB9_FMP).
Опрос вместо захвата. 1361 прерывание на посылку при 20 кГц против 34, и шаг опроса добавляется к дрожанию.
198о.8 Итоги
- NEC: шаг 562,5 мкс, 34 пары, 67,5 мс до стоповой вспышки; инверсии адреса и команды — проверка и постоянная длина.
- Захват TIM2 в режиме PWM input:
CCR1— период,CCR2— вспышка, 34 прерывания на посылку против 1361 при опросе на 20 кГц. - Декодер по периоду без единой ошибки выдерживает дрожание фронтов до 84,4 / 140,6 / 196,9 мкс при допуске 15 / 25 / 35 %; растяжку вспышки до 300 мкс не замечает, декодер по паузе ломается уже на 100 мкс.
- Передача через IRTIM: несущая TIM17 — 38 005,8 Гц (+0,0153 %),
скважность 1/3; огибающая TIM16 на 2 МГц,
ARRдо 26 999. - Модель на Python повторила все опыты бит в бит.
- armclang: 560 + 914 байт кода, 324 байта ОЗУ.
Полные исходники
ir_g474.c — ИК-пульт NEC на NUCLEO-G474RE: приём и передача.
/* ir_g474.c — раздел 198о: ИК-пульт NEC на NUCLEO-G474RE (STM32G474RE), регистры CMSIS.
* Такт — HSI16 / 4 × 85 / 2 = 170 МГц, как в примере BSP платы.
* Приём: выход приёмника (вспышка — низкий уровень) на TIM2_CH1 (PA0, AF1), режим PWM input, такт 2 МГц:
* спад — сброс счётчика и захват периода в CCR1, фронт — захват длины вспышки в CCR2.
* Передача: IRTIM на PB9 (AF6): TIM17 — несущая 38 кГц, скважность 1/3; TIM16 — огибающая по таблице ARR/CCR1,
* такт 2 МГц, новое значение — в прерывании по обновлению (предзагрузка: вступит со следующего периода).
* Изменение уровня на PC13 (кнопка B1) — послать команду; принятое — строкой в LPUART1 (PA2, PA3). */
#include "stm32g4xx.h"
#include "ir_core.h"
#define SYSCLK_HZ 170000000u
#define TICK_HZ 2000000u
#define TICK_PSC (SYSCLK_HZ / TICK_HZ - 1u)
#define CAR_HZ 38000u
#define CAR_ARR (SYSCLK_HZ / CAR_HZ - 1u)
#define CAR_CCR ((CAR_ARR + 1u) / 3u)
#define VCP_BRR ((uint32_t)(((uint64_t)SYSCLK_HZ * 256u + 57600u) / 115200u))
_Static_assert(SYSCLK_HZ % TICK_HZ == 0u && TICK_PSC == 84u, "170 МГц / 85 = 2 МГц ровно: такт — полмикросекунды");
_Static_assert(CAR_ARR == 4472u, "170 МГц / 38 кГц = 4473,7 → период 4473 такта");
_Static_assert(SYSCLK_HZ / (CAR_ARR + 1u) - CAR_HZ < 38u, "несущая 38 005 Гц — ошибка меньше 0,1 %");
_Static_assert(CAR_CCR == 1491u, "вспышка несущей — треть периода");
static nec_rx rx;
static nec_pair tx[NEC_MAX_PAIRS];
static uint16_t tx_arr[NEC_MAX_PAIRS], tx_ccr[NEC_MAX_PAIRS];
static volatile unsigned tx_n, tx_i;
static volatile int last_ev;
void TIM2_IRQHandler(void)
{
uint32_t total = TIM2->CCR1, mark = TIM2->CCR2; /* чтение CCR1 сбрасывает CC1IF */
int e = nec_rx_pair(&rx, mark, total);
if (e != NEC_NONE)
last_ev = e;
}
void TIM1_UP_TIM16_IRQHandler(void)
{
TIM16->SR = ~TIM_SR_UIF;
if (++tx_i + 1u < tx_n) { /* в предзагрузку — пара через одну */
TIM16->ARR = tx_arr[tx_i + 1u];
TIM16->CCR1 = tx_ccr[tx_i + 1u];
} else if (tx_i + 1u == tx_n) {
TIM16->CCR1 = 0; /* после стоповой вспышки — тишина */
} else {
TIM16->CR1 &= ~TIM_CR1_CEN;
}
}
static void send(uint8_t addr, uint8_t cmd)
{
tx_n = nec_encode(addr, cmd, tx);
nec_timer_values(tx, tx_n, tx_arr, tx_ccr);
tx_i = 0;
TIM16->ARR = tx_arr[0];
TIM16->CCR1 = tx_ccr[0];
TIM16->EGR = TIM_EGR_UG; /* первая пара — сразу в рабочие регистры */
TIM16->ARR = tx_arr[1]; /* вторая — в предзагрузку */
TIM16->CCR1 = tx_ccr[1];
TIM16->CR1 |= TIM_CR1_CEN;
}
static void clocks(void)
{
RCC->APB1ENR1 |= RCC_APB1ENR1_PWREN | RCC_APB1ENR1_TIM2EN;
PWR->CR5 &= ~PWR_CR5_R1MODE;
FLASH->ACR = (FLASH->ACR & ~FLASH_ACR_LATENCY) | FLASH_ACR_LATENCY_4WS;
RCC->PLLCFGR = RCC_PLLCFGR_PLLSRC_HSI | (4u - 1u) << RCC_PLLCFGR_PLLM_Pos | 85u << RCC_PLLCFGR_PLLN_Pos | RCC_PLLCFGR_PLLREN;
RCC->CR |= RCC_CR_PLLON;
while (!(RCC->CR & RCC_CR_PLLRDY)) {
}
RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_SW) | RCC_CFGR_SW_PLL;
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) {
}
RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN | RCC_AHB2ENR_GPIOBEN | RCC_AHB2ENR_GPIOCEN;
RCC->APB2ENR |= RCC_APB2ENR_TIM16EN | RCC_APB2ENR_TIM17EN | RCC_APB2ENR_SYSCFGEN;
RCC->APB1ENR2 |= RCC_APB1ENR2_LPUART1EN;
}
static void pins(void)
{
/* PA0 — AF1 (TIM2_CH1), PA2/PA3 — AF12 (LPUART1), PB9 — AF6 (IR_OUT); PC13 — вход кнопки */
GPIOA->MODER = (GPIOA->MODER & ~(3u << 0 | 0xFu << 4)) | 2u << 0 | 0xAu << 4;
GPIOA->AFR[0] = (GPIOA->AFR[0] & ~(0xFu << 0 | 0xFFu << 8)) | 1u << 0 | 0xCCu << 8;
GPIOB->MODER = (GPIOB->MODER & ~(3u << 18)) | 2u << 18;
GPIOB->AFR[1] = (GPIOB->AFR[1] & ~(0xFu << 4)) | 6u << 4;
SYSCFG->CFGR1 |= SYSCFG_CFGR1_I2C_PB9_FMP; /* мощный сток на PB9 — для светодиода (RM0440, 33) */
GPIOC->MODER &= ~(3u << 26);
}
static void receiver(void)
{
TIM2->PSC = TICK_PSC;
TIM2->ARR = 0xFFFFFFFFu;
TIM2->CCMR1 = TIM_CCMR1_CC1S_0 | TIM_CCMR1_CC2S_1 | 3u << TIM_CCMR1_IC1F_Pos | 3u << TIM_CCMR1_IC2F_Pos; /* IC1, IC2 — от TI1 */
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->DIER = TIM_DIER_CC1IE;
TIM2->CR1 = TIM_CR1_CEN;
nec_rx_init(&rx, 25u, NEC_BY_PERIOD);
NVIC_EnableIRQ(TIM2_IRQn);
}
static void transmitter(void)
{
TIM17->PSC = 0;
TIM17->ARR = CAR_ARR;
TIM17->CCR1 = CAR_CCR;
TIM17->CCMR1 = 6u << TIM_CCMR1_OC1M_Pos | TIM_CCMR1_OC1PE; /* ШИМ, режим 1 */
TIM17->CCER = TIM_CCER_CC1E;
TIM17->BDTR = TIM_BDTR_MOE; /* у TIM16/TIM17 есть блок защиты: без MOE выхода нет */
TIM17->CR1 = TIM_CR1_CEN;
TIM16->PSC = TICK_PSC;
TIM16->CCMR1 = 6u << TIM_CCMR1_OC1M_Pos | TIM_CCMR1_OC1PE;
TIM16->CCER = TIM_CCER_CC1E;
TIM16->BDTR = TIM_BDTR_MOE;
TIM16->CR1 = TIM_CR1_ARPE | TIM_CR1_URS; /* UG не поднимает UIF: прерывание — только по концу периода */
TIM16->DIER = TIM_DIER_UIE;
NVIC_EnableIRQ(TIM1_UP_TIM16_IRQn);
}
static void vcp_put(const char *s)
{
while (*s) {
while (!(LPUART1->ISR & USART_ISR_TXE))
;
LPUART1->TDR = (uint8_t)*s++;
}
}
int main(void)
{
static const char hex[] = "0123456789ABCDEF";
clocks();
pins();
LPUART1->BRR = VCP_BRR;
LPUART1->CR1 = USART_CR1_TE | USART_CR1_UE;
receiver();
transmitter();
uint32_t was = 0;
for (;;) {
uint32_t b = GPIOC->IDR & (1u << 13);
if (b != was && !(TIM16->CR1 & TIM_CR1_CEN))
send(0x04, 0x08);
was = b;
int e = last_ev;
if (e == NEC_FRAME || e == NEC_REPEAT) {
last_ev = NEC_NONE;
char s[] = "NEC a=00 c=00 r\r\n";
s[6] = hex[rx.addr >> 4]; s[7] = hex[rx.addr & 15u];
s[11] = hex[rx.cmd >> 4]; s[12] = hex[rx.cmd & 15u];
s[14] = e == NEC_REPEAT ? 'R' : ' ';
vcp_put(s);
}
}
}
Дальше: раздел 198п «Мини-проект: MIDI — вход по UART 31250 и синтезатор на ЦАП».