Универсальный приёмник TPMS
(Все фото будут позже)
Захотелось мне как-то отобразить давление в колёсах на Android магнитоле. Вот только есть проблемка: внешний USB приёмник не видит родные датчики авто, а бортовой компьютер авто не видит универсальные датчики для приёмника. А всё потому, что протоколов обмена довольно много. И это мы ещё не говорим про частоту, на которой идёт обмен, и модуляцию.
Начал я разбираться и выяснил много для себя интересного.
Приём сигнала
Первое, что надо сделать — как-то принять сигнал. В большинстве случаев частота передачи датчиков давления 433.92 MHz (Европа, Россия, Азия) или 315 МГц (Америка, Япония), а модуляция либо OOK, либо FSK (2-FSK).
Для этого нам помогут:
- Специальные профессиональные приёмники. Но с них полезных сырых данных не получишь.
- RTL-SDR — https://habr.com/ru/companies/bastion/articles/1025864/ и библиотека RTL-433 https://github.com/merbanan/rtl_433, тем более там уже реализовано некоторое количество датчиков (пример работы: https://habr.com/ru/articles/516460/). Это уже интересней, можно сканировать эфир, на лету всё подстраивать.
- Блок приёмника TPMS для Android — дёшево и сердито. Подпаялся — и изучай с логическим анализатором до посинения. Берём, даже два!
< Фото комплектов >
Раз я уже купил комплект датчиков с приёмником, то логично было бы использовать именно его. На самом деле у меня три комплекта — фирменный от магнитолы и два NoName. Последние два я разобрал.
< Фото платы 1 >
< Фото платы 2 >
Состоят они из трёх чипов: CH340T как интерфейс USB <-> UART, микроконтроллер PIC16LF1825 в одном и что-то с затёртой маркировкой в другом, приёмник CMT2218B в одном и CMT2219B в другом. Платка с PIC очень перспективная, потому что на ней выведены контакты программирования PIC и CMT, а так же есть тестпоинт данных с передатчика. Раз так, то можно попытаться считать прошивку контроллера при помощи PicKit3, но китайцы сказали своё Bù (нет) и залочили прошивку. По-простому не получилось. Значит, подпаиваюсь к точке DATA.
< Фото платы снизу >
Здесь стоит кратко сказать, как работает приёмник CMT.
У него есть 3-Wire SPI интерфейс для записи настроек во внутренний EEPROM, даже есть софт для этого. Есть GPIO, которые настраиваются в зависимости от значений в EEPROM. Там же настраивается частота, модуляция и прочие параметры приёмного тракта. В CMT2219B ещё есть FIFO для хранения данных, который можно вычитать по SPI. В целом, нас интересует только GPIO, который настроен как DOUT — выходной сигнал после демодулятора, то есть почти сырые данные с эфира. Именно туда на CMT2218B я и подпаиваюсь. Для этого там как раз есть тестпоинт. Заодно попробовал подключиться по SPI (есть посадочное под гребёнку PLS), но похоже запись там однократная, чип отвечает только 0xFF или совсем не отвечает.
Декодирование данных
< Скрин анализатора с сигналом DOUT >
Далее понадобится логический анализатор. Можно, конечно, и осциллографом, но придётся ловить блох. Как я и говорил, смотрим линию DOUT.
Первое, что мы видим, записав сигнал, — периодические короткие посылки. Они нас совершенно не интересуют, нам надо принять сигнал с датчика. Но датчик отправляет данные только при изменении давления или на скорости, когда колесо крутится. Тогда надо вдуть! Подойдёт обычный “бутылочный” метод активации датчика.
< Фото бутылки с датчиком внутри >
Понадобится бутылка с широким горлышком (например баклашка 5 литров), сосок (можно скрутить с датчика) и любой насос. В крышке делаем отверстие, прикручиваем сосок, пропихиваем датчик в бутылку, прикручиваем крышку с соском обратно и качаем. Не забываем смотреть данные с анализатора. И вот среди периодических коротких посылок появилась какая-то колбаса. Это именно то, что мы ищем. Скорей всего данные будут закодированы манчестерским кодом (с ведущим нулём). Что это значит?
< Скрин анализатора с сигналом датчика >
Самый простой вариант работает так. Есть равные временные отрезки (такты). Есть сигнал, который изменяется один раз внутри этого такта. Если изменился с 0 на 1, то это 0, если изменился с 1 в 0 — это 1. Ведущий ноль — это посылка из нескольких нулей перед основными данными. За время приёма нулей приёмник синхронизируется, потом ловит одну единицу — это синхронизация, и начинает приём со следующих битов.
Таким образом, алгоритм примерно такой:
- Пошли какие-то данные, ловим повторяющиеся фронт/спад, запоминаем период — это ведущий ноль
- Ловим двойной период — это синхронизация, 1 бит
- Читаем данные, пока тайминги не собьются
< Пример диаграммы манчестерского кода >
Во многих датчиках используются другие алгоритмы кодировки, например PCM. У меня таких датчиков нет, поэтому алгоритм не реализовывал.
Чтобы глаза не сбегались в кучу, решил это дело автоматизировать на Arduino Uno. Оказалось — зря. Потому что сигнал идёт с периодами 29-104 мкс (у разных датчиков разные), а таймер на 8 МГц чипе так быстро не отработает. Стоявший на плате PIC умеет в 32 МГц — не зря он там именно такой. А я тогда возьму классическую копеечную платку на STM32 по типу BluePull.
Выставляю максимальную для этого чипа частоту в 64 МГц, тактирование таймера 32 МГц. Настраиваю Timer2 на захват (Input Capture), канал 1 — direct rising, канал 2 — indirect falling. Предделитель 31 — чтобы считал микросекунды. Включаю прерывание.
И включаю UART1, куда буду выдавать декодированные данные.
Изначальный алгоритм я взял из вот этого примера: https://github.com/victornpb/manch_decode
Теперь снова качаем/спускаем бутылку, поглядывая на консоль, куда передаёт данные STM32. Периодически будут появляться одиночные байты, но при активации датчика начнут сыпать, как из рога изобилия. Остаётся только их распарсить.
Анализ данных
Я тестировал три разных датчика. А данные я проверял сначала на глаз, но потом сделал передатчик и смотрел изменение данных по UART и в Android приложении. Рассмотрим каждый датчик по-отдельности.
< Фото трёх датчиков >
NoName датчик 1
Это датчики с комплекта приёмника, к которому я подпаивался.
Последовательность байт у него такая:
25 43 D3 B5 B4 06 50 08 EA 02
- 25 43 D3 B5 — ID, который можно узнать, зайдя в меню “Сопряжение” в программе на Android.
- B4 — вероятно батарея, но при изменении ничего не происходит. И при значении ниже определённого приём вообще прерывается.
- 06 — давление в 2 PSI. Итоговое давление: PP * 0.5 * 0,0689476 = Bar.
- 50 — температура + 50. Итоговая температура: TT - 50 = C
- 08 — флаги (0x80 — утечка)
- EA — контрольная сумма 1 — XOR всех байт
- 02 — контрольная сумма 2 — сумма всех байт & 0xFF
NoName датчик 2
Это датчики со второго комплекта приёмника, где CMT2219B и чип с затёртой маркировкой.
Последовательность байт такая:
51 51 06 17 B6 06 55 08 FC 10 C4 A8
- 51 51 06 17 — ID, который можно узнать, зайдя в меню “Сопряжение” в программе на Android.
- B6 — вероятно батарея, но при изменении ничего не происходит. И при значении ниже определённого приём вообще прерывается.
- 06 — давление в 2 PSI. Итоговое давление: PP * 0.5 * 0,0689476 = Bar.
- 55 — температура + 55. Итоговая температура: TT - 55 = C
- 08 — флаги (0x80 — утечка)
- EA — контрольная сумма 1 — XOR всех байт
- 02 — контрольная сумма 2 — сумма всех байт & 0xFF
- C4 — контрольная сумма 3 (не знаю)
- A8 — контрольная сумма 4 (не знаю)
Датчик Hyundai Creta 1
Это штатные для моего авто датчики Schrader с партномером 52933-C1100. Здесь есть пример: https://www.hackster.io/jsmsolns/arduino-tpms-tyre-pressure-display-b6e544, но преобразование давления там неправильное.
Последовательность байт такая:
80 28 B0 53 05 00 50 01 01
- 80 28 B0 53 — ID, который можно узнать только OBD ланчем.
- 05 — давление в 2 PSI. Итоговое давление: PP * 0.5 * 0,0689476 = Bar.
- 00 — не знаю
- 50 — температура + 56. Итоговая температура: TT - 56 = C
- 01 — не знаю
- 01 — контрольная сумма — сумма всех байт & 0xFF
А после этой посылки идёт ещё какая-то колбаса. Скорей всего это данные ABS, по которым борт.компьютер авто понимает на каком колесе стоит датчик, сравнивая их с датчиками ABS.
С этим уже можно что-то делать.
DIY приёмник TPMS
И чтобы что-то делать, надо определиться на чём. Да на том же STM32, как простой, доступный и быстрый вариант. Любители ESP могут возмутиться, но я с ESP не знаком, да и только-только перехожу с AVR на STM32. Приёмник можно взять народный CC1101. Для общения с магнитолой всё тот же CH340T. А ещё можно добавить экран, чтобы можно было работать не подключаясь.
Начнём с самого главного.
Приёмопередатчик CC1101
Один из самых доступных модулей для быстрого старта — CC1101 от Техаса. Для него есть и библиотеки под разные платформы и примеры, и проекты. Но я ССЗБ и переделал библиотеку под себя. Настраивать и отлаживать можно в SmartRF Studio 7 при помощи CC Debugger (а запустить можно даже без CC1101). Там же сразу можно включить карту регистров, которая будет меняться при изменении настроек.
Так как моей целью в первую очередь было сделать копию имеющегося приёмника, то вот такие настройки у меня получились:
- Частота 433.92 МГц (по факту чуть-чуть ниже, но не важно)
- Модуляция 2-FSK
- Девиация 25 кГц
- Ширина канала 100 кГц
- Полоса пропускания 325 кГц, хотя с хорошим кварцем должно работать и 58 КГц
- Скорость данных 19.2 кГц для Schrader и 9.6 кГц для китайских датчиков (для приёма Async Serial это не надо, настраивал для тестовой передачи данных)
- Режим приёма Serial Async
Все остальные плюшки я выключил. Часть регистров подсмотрел в https://www.hackster.io/jsmsolns/arduino-tpms-tyre-pressure-display-b6e544, они отличаются от значений по-умолчанию. А некоторые регистры там указаны неправильные для моей платки. Поэтому изучите каждое значение в той статье, в моей программе и что выдаёт SmartRF Studio и проверьте по даташиту. Почему так — не понял.
И вообще, много кода взято из проекта выше, но переработано.
Если хотите проверить ваш приёмник, не важно для Android или в машине, включайте отправку без преамбул, синхрослов, длин пакетов и пр., и перед посылкой добавьте байты синхронизации, например 0x00 0x00 0x00 0x01.
Особенности приёма с CC1101
Да, не всё так просто. В отличии от CMT, здесь данные летят сплошной колбасой, и выцепить нужное ещё сложнее. А ещё встречаются помехи шириной 20-100 нс. Поэтому нужно улучшить приём. Для этого я сделал:
- RC фильтр на выход с приёмника (линия GD0 в режиме Async Serial Data): 1 кОм + 1 нФ достаточно. Это уберёт короткие помехи, которые ломают приём.
- В коде ввёл ожидаемую длину импульса. Всё, что короче x0.75 и длиннее x2.5 от этого значения на входе в декодер считаем ошибкой. В алгоритме ловим преамбулу только ожидаемой частоты и не подсчитываем реальную частоту.
- Для более точного декодирования вводим раздельную длительность импульсов: для 1 и для 0. В нуле чуть-чуть короче.
- Принимаем посылки только определённой длины.
Так стало гораздо лучше. Остаётся только вычитать все поля и занести в переменные.
Про вывод на экран особо писать не буду, это не интересно. Изначально взял для отладки экран 128x64 на SSD1309, библиотеку для SSD1306 и обновляю данные только когда они реально изменяются.
Общение с Android приложением
< Фото приложения Android >
Работать мы будем с универсальным приложением Deelife. Отправной точкой в протоколе будет этот пост: https://4pda.to/forum/index.php?showtopic=1004199&st=1280#entry132287707. И вот эта библиотека: https://pypi.org/project/tpms/. А ещё возьмём и декомпилируем APK файл.
Помните, что оба приёмника содержат CH340T? Это неспроста. Есть посмотреть исходники в com\cz\usbserial\driver\UsbId.java, то увидим следующие:
Поддерживаемые устройства:
- ARDUINO LEONARDO
- ARDUINO MEGA 2560
- ARDUINO MEGA 2560 R3
- ARDUINO MEGA ADK
- ARDUINO MEGA ADK R3
- ARDUINO MICRO
- ARDUINO SERIAL ADAPTER
- ARDUINO SERIAL_ADAPTER R3
- ARDUINO UNO
- ARDUINO UNO R3
- ATMEL LUFA CDC DEMO APP
- FTDI FT231X
- FTDI FT232R
- LEAFLABS MAPLE
- PROLIFIC PL2303
- QINHENG HL340
- SILABS CP2102
- SILABS CP2105
- SILABS CP2108
- SILABS CP2110
- VAN OOIJEN TECH TEENSYDUINO SERIAL
Поддерживаемые производители:
- VENDOR ARDUINO
- VENDOR ATMEL
- VENDOR FTDI
- VENDOR LEAFLABS
- VENDOR PROLIFIC
- VENDOR QINHENG
- VENDOR_SILABS
- VENDOR VAN OOIJEN TECH
Круто! Так много всего! Но UART-USB от SILABS у меня не заработали. Не уверен и насчёт других устройств. Из списка была только Arduino UNO R3 и Leonardo, но как уже писал выше — они слишком медленные. Поэтому купил конвертер на базе CH340, его видит сразу, если есть правильный обмен данными.
Протокол
Основываясь на ссылках выше и исходниках: com\tpms\decode\PackBufferFrame3.java и com\tpms\encode\PackBufferFrameEn3.java можно составить такой протокол.
0x55 0xAA LEN CMD DATA CRC
- 0x55 0xAA — заголовок, всегда одинаковый
- LEN — длина всей посылки, минимум 6
- CMD — команда
- DATA — дополнительные данные, может быть несколько байт
- CRC — XOR всех байт кроме последнего (который и есть CRC)
Список команд из исходников APK тоже сразу выпишу.
/TODO/
Команды от приложения:
| Код | Команда | Данные | Ответ | Примечание |
|---|---|---|---|---|
| 0x00 | “Пакет принял” | Не используется | ||
| 0x01 | Начать поиск датчика | Колесо | 0x19 | |
| 0x03 | Поменять датчики местами | Откуда и куда | 0x30 | |
| 0x06 | Остановить поиск датчика | Колесо | ||
| 0x07 | Список ID датчиков | 0x00 | 0x01, 0x02, 0x03, 0x04, 0x05 | |
| 0x19 | Проверка связи с приёмником | 0x00 | Идёт перед 0x5A или 0x5B | |
| 0x58 | Перезагрузка, данные | 0x55 | ||
| 0x5A | Использовать старый протокол | Кодовое слово | 0x5A | |
| 0x5B | Использовать новый протокол | Кодовое слово | 0xB5 |
Команды от приёмника
| Код | Команда | Данные | Примечание |
|---|---|---|---|
| Статус датчика | Отправляется каждые 3с | ||
| 0x00 | Переднее левое колесо | Старый протокол: PP TT FF VV 0x00 Новый протокол: PP TT FF |
|
| 0x01 | Переднее правое колесо | См. выше | |
| 0x10 | Заднее левое колесо | См. выше | |
| 0x11 | Заднее правое колесо | См. выше | |
| 0x05 | Запасное колесо | См. выше | |
| ID датчика | Отправляется по пять раз каждый | ||
| 0x01 | Переднее левое колесо | 4 бита подряд | |
| 0x02 | Переднее правое колесо | См. выше | |
| 0x03 | Заднее левое колесо | См. выше | |
| 0x04 | Заднее правое колесо | См. выше | |
| 0x05 | Запасное колесо | См. выше | |
| Остальные команды | |||
| 0x16 | “Я тут” | Не используется | |
| 0x19 | Датчик найден | ??? | |
| 0x30 | Датчики местами поменял | Откуда и куда | |
| 0x5A | Использую старый протокол | Кодовое слово | |
| 0xB5 | Использую новый протокол | Кодовое слово |
Приложение периодически отправляет две команды: 0x19 и 0x5B (0x5A) — пингует и говорит какой протокол хочет использовать. При первом подключении надо ответить 0xB5 (0x5A) 10 раз подряд. На последующие запросы отвечать один раз. Между командами пауза в 10 мс. Кодовое слово, полученное в команде 0x5A или 0x5B (pswin), надо преобразовать и отправить обратно:
- Старый протокол: pswin = pswin ^ 0x20 ^ 0x15 ^ 0x10 ^ 0x01 ^ 0x02 ^ 0x03 ^ 0x04 ^ 0x05;
- Новый протокол: pswin = pswin ^ 0x20 ^ 0x17 ^ 0x01 ^ 0x86 ^ 0x64 ^ 0x01 ^ 0x82 ^ 0x76;
После подключения можно слать данные о состоянии колёс: 0x00, 0x01, 0x10, 0x11, 0x05 по одному разу, вся пачка из пяти пакетов отправляется раз в три секунды.
При запросе ID датчиков (0x07) в ответ отправляется пачка из пяти команд 0x01, 0x02, 0x03, 0x04, 0x05, по пять раз каждый.
/TODO/
Проверка связи, выбор протокола, передача данных и запрос ID — это минимум, который надо реализовать.
Окончательная реализация
Макетки и модули — это, конечно, хорошо для отладки, но как готовое устройство я бы не стал их использовать. Поэтому разработал устройство с нуля. Разберём что получилось.
Схема
Первым делом определимся с набором чипов. Раз на BluePill всё получилось, то не будем долго думать и поставим STM32F103C8T6. Радиомодуль будем разводить сами, поэтому закладываем чип CC1101RGPR. И USB-UART будет CH340G — его легче достать. Также добавил EEPROM, чтобы хранить в ней настройки и реализовать конфигурации под разные датчики — хочу сделать более универсальное устройство. В принципе, можно хранить и во Flash самого контроллера, но почему бы и да?
Тактировать STM32 будем от внутреннего генератора, а вот CC1101 надо обеспечить минимум PPM. В даташите есть рекомендуемые кварцы, но я нашёл другие, на 10-20ppm, этого достаточно. Но больше 25ppm я бы брать не стал, тем более при температуре работы -30-+70 градусов. Ну и схема с номиналами под 433 МГц будет из даташита. На выход поставил U.FL разъём — пигтейл. Сам разъём антенны, SMA, будет крепиться на корпусе. Уж лучше пусть будет торчать антенна наружу, чем хилая спиралька внутри корпуса.
Из дополнительных разъёмов — UART, SWD, BOOT0/1, Reset, i2c, SPI. Так же добавил четыре светодиода: на питание 5В с USB после предохранителя и диода, на питание 3.3В, и два к микроконтроллеру.
А ещё будут четыре кнопки — пусть будет возможность управлять без Android устройства. Совсем универсальная приблуда получится.
Ещё понадобится USB разъём. Сразу заложил современный USB-C. Из-за толстой стенки корпуса искал разъём с большим вылетом. Нашёл подходящий только штыревой, повёрнутый на 90 градусов вдоль оси. Т.е. отверстие сбоку корпуса будет не горизонтальное, а вертикальное. При подключении главное соединить дублирующиеся контакты и добавить резисторы для определения стороны подключения. Подробнее можете почитать вот тут: https://habr.com/ru/companies/ruvds/articles/714476/ Дисплей будет 2.08” OLED с разрешением 256x64 пикселя и контроллером SH1122. К тому же он ещё имеет 16 градаций серого. Выбрал из-за его размеров — почти идеально подходит к выбранному корпусу. Кстати, корпус будет Gainta G200MF, просто потому что их у меня есть некоторое количество.
Плата
Плата будет двухслойная, самая обычная, FR4.
Первым делом я попытался развести дифпару USB на 90 Ом, но со стандартной толщиной платы (1.0-1.6мм) получается как-то не очень. Ладно, сделаем просто максимально короткие проводники, разместим CH340 почти вплотную к USB-C. И не забудем добавить защиту. При толщине дорожек 10 mil и зазоре 10 mil импеданс должен получиться 155-160 Ом.
По STM32 особых вопросов нет, а вот с CC1101 надо аккуратней. Согласно даташиту ей надо сделать термалпад с пятью переходными отверстиями (via) — одно посередине и четыре по краям. И грамотно развести радиотракт. Вот с этим у меня знаний нет, поэтому старался скопировать топологию с радиомодуля. Вокруг радиотракта поставил много переходных отверстий и вообще залил земляным полигоном. Под радиотрактом никаких дорожек быть не должно, только земляной полигон!
Вообще я сначала развёл питание толстым проводником, и уже от него делал отводы к потребителям. Смотрел, чтобы подтяжки были примерно там же, где и приёмник сигналов с этих дорожек. После этого уже залил земляные полигоны на обоих слоях.
Получилось очень плотно, что, в прочем, неудивительно для такой маленькой платки.
Исходный код
Данный раздел наверно больше про мой вариант реализации велосипеда. С костылями. Так что если можете лучше — делайте.
А у меня STM32CubeIDE и конфиг основанный на том, что было в начале статьи. Повторим:
- Общее тактирование максимально поднято до 64 МГц
- Timer2 тактирование 32 МГц. Предделитель 31, т.е. таймер считает по 1 мкс. Режим Input Capture. CH1 на direct Rising, CH2 на indirect Falling. Включено прерывание.
- SPI без CSn с частотой 1M. Режим 0, 0. Так же включил DMA на TX.
- UART1 на скорость 19200 и DMA на TX. Включено прерывание и Callback функции.
- I2C с частотой 100k.
- Несколько GPIO на вход и выход.
Я использовал эти библиотеки:
- https://github.com/pkarsy/CC1101_RF
- https://github.com/SumanSynth/at24cxx
- https://github.com/mikhail-tsaryov/SH1122-STM32-HAL-Driver
Но всё равно их потом переделал.
При запуске мы сперва инициализируем все модули.
eeprom_init();
preferences_init();
tpms_data_init();
tpms_decoder_init();
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, 0);
uart_init();
display_init();
- Читаем EEPROM, проверяем есть ли там что, сверяем CRC. Если данные в порядке, то следующие модули считают данные, иначе просто ставим значения по-умолчанию.
- Читаем настройки
- Читаем ID датчиков
- Читаем параметры протокола.
- И только после инициализируем CC1101 (в tpms_decoder_init). С ней у меня бывают проблемы — в зависимости от сборки что-то оптимизируется, и считанные значения регистров не совпадают с записанными. Как с этим бороться я пока не понял. Лучше всего помогает грамотная разводка платы.
- Включаем USB-UART преобразователь
- Инициализируем UART
- Включаем экран в самом конце
Про манчестерский код я писал выше. Тут скажу, как устроен обмен данными.
Данные принимаются в прерывании (как с радио, так и UART). И главное правило — максимально быстро выйти из прерывания, чтобы успеть поймать следующее прерывание. А потому бОльшую часть обработки следует выносить в общий цикл.
Всю работу с данными я реализовал через кольцевые буферы. Кольцевой буфер — это массив, но когда индекс доходит до его конца, он тут же перескакивает на начало, как бы замыкаясь в кольцо. Поэтому и кольцевой. Каждый элемент массива (у меня) — структура, где есть поле с данными (обычный массив), поле с длинной данных и поле со статусом (свободен, записан). Работа с ним простая: принял данные — записал данные, сколько байт принял, поставил статус “записан” и быстренько отправил в буфер (с инкрементом индекса записи). А в основном цикле проверяю, что если по индексу чтения найден “записанный” пакет, то я его считываю и записываю “свободен” (с инкрементом индекса чтения). Это подходит и для данных с радио, и для пакетов UART общения с Android приложением.
Отправка по UART тоже идёт в основном коде через кольцевой буфер. Так сделал, потому что задержка должна быть 10 мс между посылками, а положить данные на отправку можно сразу и много.
Кстати, о задержке в 10 мс. Она реализована через HAL_GetTick, который использует специальный выделенный таймер на 1мс: сохраняем текущее значение, сравниваем с предыдущим, если больше 10мс — делаем что задумали.
Точно так же сделана и задержка 3 секунды для отправки данных с колёс. И ещё есть третий счётчик — подключен/отключен. Если приходит опрос от Android приложения — счётчик сбрасывается. Всего у меня 7 таких счётчиков.
Отрисовка на экране происходит только по запросу и по готовности SPI DMA TX. Если меняются данные (по приёму, например), то выставляем переменную “перерисовать”, и в основном цикле проверяем эту переменную. Если выставлена — перерисовываем экран и выставляем обратно в ноль. Сама перерисовка занимает довольно много времени.
Обязательно используем DMA для отправки данных: как UART, так SPI для экрана. Дело в том, что у нас с бешеной скоростью молотит таймер и постоянно прерывает основной код. Ждать, пока отправится весь буфер на экран довольно расточительно, да ещё (в зависимости от приоритетов), это действие может прерваться. Поэтому просто отправляем через DMA и не трогаем буфер пока не закончится отправка (ловим окончание прерыванием).
Отдельно стоит сказать про шрифты. Раз у нас экран может в 4bit градации серого, то стоит этим воспользоваться. Здесь сразу встаёт вопрос экономии памяти, так как места такой шрифт будет занимать в разы больше. Вот, что я придумал:
- Шрифт — массив uint8_t
- Храним не байты, а нибблы
- Берём только латиницу/кирилицу и нужные символы
- Обрезаем нулевые строки и нулевые столбцы в начале и конце глифа
- В начале идёт 4 байта — координаты границ глифа
- Делаем второй массив uint16_t, в котором хранится индекс глифа в первом массиве.
Это всё пакуется в структуру для более удобного доступа.
И работать с такой структурой надо так:
- Прилетает символ на отрисовку, отсеиваем те, которые у нас в шрифте
- Ищем индекс через второй массив uint16_t
- Читаем координаты глифа по этому индексу из основного массива uint8_t
- А дальше заполняем frameBuffer нибблами по координатам
Для изображений всё чуть проще. Для них мы сразу знаем размер, который указан в структуре, а само изображение — массив нибблов в uint8_t.
Для создания шрифтов (из ttf) и изображений (из bmp 24bit) я сделал скрипт на питоне, он есть в репозитории проекта.
Так же есть скрипт с тестом отрисовки шрифтов и изображений.
Есть ещё второй вариант кодирования, он лучше подходит для больших изображений: храним данные как 0xLD, где L — количество повторений цвета D. Это сократит длинную строку до одного байта. Например, изображение 256x64 с 8к сжало до 3к — весьма существенно.
Реализован функционал:
- Основной экран с показаниями датчиков
- Экран с графиками температуры или давления за 32 минуты, с автоподстройкой масштаба
- Экран замены колёс друг с другом
- Экран сопряжения ID датчиков
- Таблица с данными восьми найденных в эфире датчиков в реальном времени — выбор для сопряжения
- Основное меню
- Выбор протокола датчиков
- Ручное редактирование ID
- Два комплекта датчиков (лето/зима)
- Настройки высокого и низкого давления — выход за пределы будет считаться ошибкой
- Единицы измерения давления/температуры
- Выбор языка (рус/англ)
Хранение данных в EEPROM
Если мы говорим про универсальность, то её надо как-то реализовывать. Я изначально заложил EEPROM для хранения настроек, ID датчиков и описания протоколов. Но в итоге всё сводилось к тому, что под каждый протокол мне надо было писать свою функцию обработки температуры, давления и других данных.
А что, если хранить формулы прямо в EEPROM? Давайте так и сделаем!
Но сперва посмотрим, что там в EEPROM:
| Адрес | Длина | Данные | Описание |
|---|---|---|---|
| 0x0000 | 4 | 0x54 0x50 0x4D 0x53 | Заголовок “TPMS” |
| 0x0004 | 1 | 24 | Маркер настроек |
| 0x0005 | 6 | 0x00 0x00 0x0 0x00 0x00 0x00 | Настройки |
| 0x000B | 1 | 0x07 | Маркер ID |
| ——– | —— | —- | ID летних колёс |
| 0x000C | 1 | 0x00 | Перед левый |
| 0x000D | 4 | 0x00000000 | ID |
| 0x0011 | 1 | 0x01 | Перед правый |
| 0x0012 | 4 | 0x00000000 | ID |
| 0x0016 | 1 | 0x02 | Зад левый |
| 0x0017 | 4 | 0x00000000 | ID |
| 0x001B | 1 | 0x03 | Зад правый |
| 0x001C | 4 | 0x00000000 | ID |
| 0x0020 | 1 | 0x04 | Запаска |
| 0x0021 | 4 | 0x00000000 | ID |
| ——– | —— | —- | ID зимних колёс |
| 0x0025 | 1 | 0x10 | Перед левый |
| 0x0026 | 4 | 0x00000000 | ID |
| 0x002A | 1 | 0x11 | Перед правый |
| 0x002B | 4 | 0x00000000 | ID |
| 0x002F | 1 | 0x12 | Зад левый |
| 0x0030 | 4 | 0x00000000 | ID |
| 0x0034 | 1 | 0x13 | Зад правый |
| 0x0035 | 4 | 0x00000000 | ID |
| 0x0039 | 1 | 0x14 | Запаска |
| 0x003A | 4 | 0x00000000 | ID |
| 0x003E | 1 | 0xFE | Маркер CRC |
| 0x003F | 1 | 0x00 | XOR8 CRC 0x000C-0x0021 |
| 0x0040 | 1 | 0x19 | Маркер протоколов |
| 0x0041 | 1 | 0x01 | Количество протоколов |
| ——– | —— | —- | Адресная таблица |
| 0x0042 | 2 | 0x0044 | Адрес протокола |
| ——– | —— | —- | Описание протокола |
| Addr + 0x00 | 1 | 0x00 | ID протокола |
| Addr + 0x01 | 1 | 0x00 | Декодер 0 = Манчестер 1 = PCM (не реализовано) |
| Addr + 0x02 | 1 | 0x09 | Длина пакетаh |
| Addr + 0x03 | 2 | 0x0000 | Манчестер, длина высокого уровня (мс) |
| Addr + 0x05 | 2 | 0x0000 | Манчестер, длина низкого уровня (мс) |
| Addr + 0x07 | 47 | 0x00 - 0x00 | Регистры CC1101 |
| Addr + 0x36 | 32 | 0x00 - 0x00 | Название протокола |
| Addr + 0x56 | 1 | 0x01 | Количество полей данных |
| Field + 0x00 | 1 | 0x00 | Длина поля в битах |
| Field + 0x01 | 1 | 0x00 | Назначение и тип поля (field & 0xF0) » 4 = назначение field & 0x0F = тип |
| Field + 0x02 | 1 | 0x00 | Длина выражения преобразования |
| Field + 0x03 | Len | 0x00 | Выражение преобразования |
| Field + M | 1 | 0x00 | Тип CRC1 |
| Field + M+1 | 1 | 0x00 | Начало CRC1 |
| Field + M+2 | 1 | 0x00 | Длина CRC1 |
| Field + M+3 | 1 | 0x00 | Тип CRC2 |
| Field + M+4 | 1 | 0x00 | Начало CRC2 |
| Field + M+5 | 1 | 0x00 | Длина CRC2 |
| N | 1 | 0xFE | Маркер CRC |
| N+1 | 1 | 0x00 | XOR 8 CRC 0x0041-N-1 |
Фух. Многобукафф.
Самое важное - держать CRC для проверки и адресная таблица, потому что длина данных протокола непостоянна.
Преобразования данных
Теперь про выражения преобразования.
Данные мы получаем как массив uint8_t. И с ним можно делать что угодно побитно!
Но сперва надо указать сколько бит нам надо, указать назначение и тип данных.
Назначения:
| Цифирь | Назначение |
|---|---|
| 0x00 | Пропуск |
| 0x01 | ID |
| 0x02 | Давление |
| 0x03 | Температура |
| 0x04 | Флаги (Не использовать!!!) |
| 0x05 | Батарейка (Не тестировалось) |
| 0x07 | CRC1 |
| 0x08 | CRC2 |
Типы данных:
| Цифирь | Тип данных |
|---|---|
| 0x00 | None (пропуск) |
| 0x01 | Byte[4] |
| 0x02 | uint8_t |
| 0x04 | uint16_t |
| 0x05 | uint32_t |
| 0x06 | float |
| 0x42 | int8_t |
| 0x44 | int16_t |
| 0x45 | int32_t |
| 0x80 | Взять данные с датчика |
Вроде много, но для поля можно использовать только 4 бита, поэтому доступны только первые 6 типов данных.
С этим разобрались. Теперь самое интересное - стековый калькулятор. Ну, то есть калькулятор, который работает со стеком.
Стек - это структура данных по типу “первый зашёл, первый вышел”. То есть как стопка бумаг, где вы можете класть только сверху и брать тоже только сверху.
Глубина стека у меня всего 4, после выхода из обработки он не очищается - это можно использовать для сложных вычислений.
Синтаксис такой:
Операнд Тип [Данные]
Данные нужны только для операнда RAW и PUSH. Для использования данных напрямую с датчика давления добавьте 0x80 к типу. А уже вот здесь типы данных доступны все.
Операнды:
| Цифирь | Операнд | Описание |
|---|---|---|
| 0x00 | RAW | Как есть |
| 0x01 | PUSH | Положить |
| 0x02 | POP | Забрать и выйти |
| 0x03 | ADD | Сложение |
| 0x04 | SUB | Вычитание |
| 0x05 | MULT | Умножение |
| 0x06 | DIV | Деление |
| 0x07 | AND | Логическое И |
| 0x08 | OR | Логическое ИЛИ |
| 0x09 | NOT | Инверсия |
| 0x0A | XOR | Исключающее Или |
| 0x0B | LSH | Бит.сдвиг влево |
| 0x0C | RSH | Бит.сдвиг вправо |
Калькулятор работает так:
переменная1 = POP
переменная2 = POP
PUSH = переменная2 ОПЕРАНД переменная1
Пример:
01 82 01 02 06 04 02 02 02
Объяснение:
-
01 82 = PUSH uint8_t Положить данные сенсора в стек -
01 02 06 = PUSH uint8_t Положить 06 to stack -
04 02 = SUB = sensor - 06 Вычесть из данных сенсора 06 -
02 02 = POP uint8_t Забрать результат и выйти
В описании полей есть ещё CRC, контрольные суммы.
Поддерживаются такие:
| Цифирь | Тип CRC |
|---|---|
| 0x00 | None |
| 0x01 | SUM 8bit |
| 0x02 | XOR 8bit |
И осталось описать как данных кранятся в самом устройстве. То есть, к чему надо их приводить:
| Данных | Тип |
|---|---|
| Pressure | PSI * 2 |
| Temperature | T°С + 50 |
| Flags | Это мои флаги. Их не трогать и не писать. |
| Battery | 0-100% |
Готовое устройство
Платки я заказал в китае через AliExpress, получилось $21 за 10 плат с RoHS или $15 — без RoHS.
Первым делом запаял всю мелочовку и LDO на 3.3В — проверил, что питание работает. Потом запаял USB-UART и проверил его — в системе определяется. Значит можно паять STM32, EEPROM и CC1101. Проблемы возникли с USB разъёмом — в документации не был указан размер одной ножки корпуса, я рассчитал на глаз, но чуть заузил, поэтому при установке пришлось их откусить. И была проблемка с CC1101 — тоже с посадочным. Делал рекомендованный паттерн из документации, но чип всё равно плохо садился. Судя по всему площадки были чуть маловаты.
Сразу проверил STM32 — он видится, прошивается. Залил прошивку с отключенной поддержкой CC1101, EEPROM и дисплея, подключил к смартфону со включенной программой TPMS и… оно сразу же определилось! Первая победа. Потом включил CC1101 и всё зависло как раз из-за непропая чипа. Когда уже разобрался с запайкой, CC1101 стал определяться, причём не было глюков с неправильными ответами, как когда всё было собрано на макетке на проводах.
На работе решил посмотреть, как получилась дифпара USB — замерить импеданс на векторном анализаторе. По сути, там несколько измерений:
- Измерительная оснастка с USB
- Плюс переходник USB-A/USB-C
- Плюс незапаянный разъём
- Плюс разъём в плате
- Плюс CH340G
- Плюс защита ULC3304P10
Из графиков видно, что импеданс проседает до 40 Ом, что как-то маловато для USB. Но эта просадка только в CH340G, да и работает всё отлично. Защита ULC3304P10 если и влияет на импеданс, то не слишком сильно. Больше всего интересует график без CH340G, по которому импеданс проседает до 60 Ом. Уже лучше, чем 40 Ом, но всё равно маловато. Скорей всего это получилось из-за перекрещивания линий D+ и D- на противоположных слоях. Сделать по-другому наверно было можно, но в ограниченном пространстве я других вариантов на два слоя не увидел.
И последняя проверка — в машине. Жёстко задал в коде ID датчиков в колёсах, подключил антенну и воткнул в USB магнитолы. Стоило мне поехать, как датчики определились! (Напомню, что датчики начинают передавать или при изменении давления, или по акселерометру) Показания в программе TPMS были чуть ниже, чем на приборке авто (там от версии приложения показания могут отличаться, проблема известная), но показания в штатном приложении магнитолы совпадают. Покатался с часик, всё работало стабильно, ничего не зависло и не отключилось. Теперь знаю, что при нуле на улице, колёса на трассе за час нагреваются до +15. Уже позже, когда приделал дисплей, снова сравнил данные уже по своим внутренним вычислениям — всё совпадает!
Осталось разве что оформить в корпус.
Сделал резиновое уплотнение между USB разъёмом и корпусом, вырез под дисплей и под толкатели кнопок. Разработал дополнительную деталь, в которую поставил пружинки и толкатели кнопок. Она с одной стороны упирается в плату, обнимает кнопки, с другой стороны упирается в крышку, и в неё кладётся экран.
Итого
В целом, получилось именно то, что мне и надо — читать данные из эфира и выводить их в UART для Android приложения. По ходу изучения узнал для себя новое, хоть и задолбался ловить блох.
В планах сделать устройство универсальным. И сделать своё Android приложение, но с функцией выбора протокола, перепрошивки, обновления и пр. Но это уже задачка со звёздочкой.
Ссылки
Использованные библиотеки:
- manch_decode от victornpb
- CC1101_RF от pkarsy
- at24cxx от SumanSynth
- stm32-ssd1306 от afiskon
- SH1122-STM32-HAL-Driver от mikhail-tsaryov
Проекты по теме:
- Всё про USB-C: введение для электронщиков
- Тема на 4pda
- Tyre Pressure Monitoring System Python Library
- Arduino TPMS Tyre Pressure Display
- Исследование протокола системы контроля давления воздуха в шинах автомобиля (TPMS)
Компоненты:
- STM32F103C8T6
- CC1101RGPR
- CH340G
- 24AT64C
- Дисплей 2.08” 256x64px на SH1122
Ссылка на проект на GitHub:






