Embedded HandbookПриложения
Все разделы › XXV. Мини-проекты
STM32G474 · уровень средний · 14 мин чтения
Открыть в интерактивном справочнике

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

ИК-пульт на NUCLEO-G474RE: справа передача — таблица пар NEC превращается в ARR и CCR1 для TIM16 на 2 МГц, TIM17 даёт несущую 38 005,8 Гц со скважностью 1/3, IRTIM смешивает их на PB9; слева приём — выход приёмника на PA0, TIM2 в режиме PWM input: спад сбрасывает счётчик, CCR1 — период, CCR2 — вспышка; декодер по периоду выдерживает дрожание фронтов до 140,6 мкс при допуске 25 %

Альтернативные функции — по таблице 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 Итоги

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

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 и синтезатор на ЦАП».

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