Перейти до основного вмісту

📺 ІЧ сигнал

Продовжуючи тему з таймерами, варто розглянути ще один їх режим роботи, який був пропущений в попередній статті про таймери — режим захоплення вхідних даних (Input Capture). Розглядати будемо на прикладі безпровідного протоколу передачі даних японської компанії NEC. Зараз ця компанія належить Renesas, яка в свою чергу належить японській Hitachi (якій доречі також належить GlobalLogic). Протокол NEC — один із найпопулярніших протоколів передачі даних за допомогою інфра-червоного (ІЧ) випромінювання, особливо в японській побутовій техніці. Він використовується для керування недорогими побутовими приладами навіть сьогодні.

Передачу даних ІЧ випромінюванням можна вважати прародичем сучасних Bluetooth та Wi-Fi, хоча останні двоє надто сильно відрізняються від першого. Цей спосіб передачі даних використовувався повсюдно: мобільні телефони, пейджери, звичайні пульти і навіть принтери, відеокамери, Nintendo Game Boy.

🔦 NEC

Для початку розглянемо фізичний рівень передачі ІЧ сигналу:

  • Передавач (пульт) випромінює імпульси невидимого інфрачервоного світла (940nm) з частотою 38 kHz
  • Приймач вловлює ці швидкі імпульси світла, але натомість видає безперервний низький рівень напруги на своєму виході Data впродовж всього часу поки імпульси надходять. Коли імпульси припиняються, то на виході маємо високий рівень. Це все має наступний вигляд:

Електричний сигнал

Отже ІЧ приймач отримав світлові імпульси, а на своєму виводі видав електричний сигнал. Розглянемо формат цього електричного сигналу:

  • Повідомлення завжди починається з довгого імпульсу (низький рівень) в 9 ms (мілісекунд) та павзою (високий рівень) 4.5 ms після нього
  • Біти даних кодуються не імпульсами, а тривалістю павзи після цих імпульсів
    • Тривалість імпульсу — 562.5 μs (мікросекунди). Якщо після цього імпульс був відсутній стільки ж — значить маємо 0, якщо відсутній 3 x 562.5 μs часу — маємо 1
    • Точна тривалість імпульсу 562.5 μs не завжди може бути забезпечена, тому допускаються відхилення в кілька відсотків. Це потрібно буде врахувати в коді
  • Структура даних наступна: 8 біт адреси і знову 8 біт цієї ж адреси, але побітово інвертованої; 8 біт команди і знову 8 біт інвертованої команди
    • Інвертовані адреса та команда виконують роль свого роду хеш-суми для перевірки чи оригінальні 8 біт були отримані правильно
  • Закінчення повідомлення після 32-ох біт даних фіксується тим самим імпульсом тривалістю 562.5 μs
    • якщо користувач після завершення передачі пакету даних досі тримає кнопку пульта натиснутою, то передається спеціальний короткий пакет даних кожні 108 ms: 9 ms імпульс + 2.25 ms павза + 562.5 μs імпульс

Все, що було описане вище можна роздивитися на діаграмі з логічного аналізатора:

🎙️ Режим захоплення

Тепер, коли ми маємо уявлення як виглядає сигнал, який нам треба прочитати (захопити) давайте подивимось як це можна зробити за допомогою таймера. Насправді таймер не захоплює якимось чином одразу весь сигнал, хоча це було б зручно. Він всього лиш записує значення свого лічильника в спеціальний регістр в момент зміни рівня сигналу. Тобто, щоб записати весь сигнал треба кудись переписати значення лічильника таймера у всі моменти зміни сигналу. Потім порахувати різницю між одним значенням і попереднім до нього і таким чином отримати тривалість кожного імпульсу. Так треба зробити для всіх імпульсів напротязі всього сигналу. Нам достатньо зберігати значення лічильника на спадання сигналу і рахувати різницю лише між спадами на діаграмі вище.

Таймери — це апаратні блоки, але вони не мають своїх окремо виведених пінів на корпусі МК. Натомість їх можна підключити до існуючих GPIO в режимі альтернативної функції, щоб скористатися режимом захоплення. Але не можна підключити будь який таймер до будь-якого піна, комбінації обмежені і прописані в даташиті. На своєму МК STM32G030F6P6 я обрав пін PA11, який згідно даташиту можна підключити до 4-го каналу таймера TIM1. До цього піна на бредборді я підключив ІЧ модуль з AliExpress (за 0.6$). Модуль має всього 3 контакти: VDD, GND і Data. На виводі Data буде той сигнал, який ми хочемо прочитати, тому підключаємо його до піна PA11.

🎬 Демо

uint16_t prevValue = 0;
uint32_t fullMessage = 0;
uint8_t bitIndex = 0;

void processSignal(TIM_HandleTypeDef *htim) {
uint16_t curValue = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_4);
uint16_t pulseWidth = (curValue>prevValue) ? curValue-prevValue : (0xFFFF-prevValue)+curValue;
prevValue = curValue;

if (pulseWidth > 12825 && pulseWidth < 14175) // (9ms + 4.5ms) ± 5% — start message
{
bitIndex = 0;
fullMessage = 0;
}
else if (pulseWidth > 1068 && pulseWidth < 1181) // (562.5μs + 562.5 μs) ± 5% — received '0'
{
bitIndex++;
}
else if (pulseWidth > 2137 && pulseWidth < 2362) // (562.5μs + 3*562.5 μs) ± 5% — received '1'
{
fullMessage |= 1 << bitIndex;
bitIndex++;
}

if (bitIndex >= 32) { // Finish!
uint8_t address = fullMessage & 0xFF;
uint8_t command = (fullMessage >> 16) & 0xFF;

// Successfully decoded!!! Now do reaction here.
}
}

Ось так просто в кілька if-ів ми декодували ІЧ сигнал за допомогою таймера:

  • функція processSignal щоразу викликається на кожен спад рівня сигналу
  • в 1-му if перевіряємо чи це довгий імпульс в 9 мілісекунд та довга павза після нього 4.5 ms. Якщо так — очищаємо попередній пакет даних і починаємо зчитувати біти даних з наступного виклику функції
  • в 2-му if перевіряємо чи це імпульс 562.5 μs з короткою павзою 562.5 μs після нього. Якщо так — маємо 0
  • в 3-му if перевіряємо чи це імпульс 562.5 μs з довгою павзою 3*562.5 μs після нього. Якщо так — маємо 1, яку записуємо в 32-бітне число
  • в останньому if перевіряємо чи ми прочитали всі 32 біти. Якщо так — вважаємо, що пакет даних отримано

Повна демка написана для STM32G030F6P6 (32KB/8KB/TSSOP-20) та стандартну частоту ядра 16 MHz та опублікована на GitHub.

⏫ Можливі покращення

Код вище дуже простий, але повністю робочий (відео вище). Хоча можна було б додати кілька можливих покращень:

  • перевіряти "хеш-суму" присутню в сигналі для команди і адреси, щоб пересвідчитись у валідності отриманих даних
  • обробляти сигнали повтору (Repeat message), щоб опрацьовувати від користувача постійно затиснуту кнопку
  • в коді вище кінцем передачі вважається отримання останнього 32-го біту даних, проте правильніше було б опрацьовувати імпульс закінчення
  • щоб ускладнити приклад можна було б прикрутити DMA (але це вже інша історія)
  • щоб спростити приклад можна було б використати будь-який інший таймер взагалі без прив'язки до GPIO, а переривання повісити на сам GPIO. Частота нашого сигналу відносно низька і не вимагає точних значень лічильника таймера, тому це можливо в нашому випадку

🔚 Висновки

В цій і попередній статті ми розглянули кілька простих прикладів з таймерами для розуміння що це і для чого можна використовувати. Але тема таймерів невичерна. Існує ще багато можливих параметрів налаштування таймерів, їх режимів і способів запуску, способів тактування та навіть взаємодії таймерів між собою.

Якщо цікаво поглибити знання в темі таймерів, то рекомендую книгу Mastering STM32 2-nd Edition.



Дата публікації
2025.01.29