Мультитул для инженера: волшебная коробочка с I2C/SPI/UART/JTAG за 1.000 рублей
- понедельник, 17 ноября 2025 г. в 00:00:09
Будучи творческим человеком и техногиком, я обожаю при первой возможности апгрейдить своё оборудование. Время от времени я мониторю маркетплейсы в поисках чего-то новенького и в этот раз я наткнулся на настоящий мультитул для Embedded-разработчика — контроллер I2C/SPI/UART/JTAG в одной коробочке и всё это всего за 1.000 рублей... Конечно я не смог пройти мимо этой штучки и в рамках сегодняшней статьи хочу рассказать что оно из себя представляет и как с ним работать. Жду вас под катом!
На самом деле такой формат статей для меня «в новинку», до этого я ни разу не делал обзоров на оборудование. Да и мой инструментарий слишком зауряден, чтобы делать ещё одну статью уровня «почему Quciko T12 лучше любой 900M станции» или «почему Вам не стоит покупать компрессорный люкей в 2025 году». Однако обзоров на сегодняшний гаджет я не нашёл, несмотря на его огромную пользу как для Embedded-разработчиков и инженеров, так и мастеров по ремонту смартфонов, планшетов и ноутбуков.
Во время подготовки статьи о том, как я написал BIOS для игровой консоли от Waveshare, в рекомендациях мне попадались другие товары от этого производителя — в том числе и сегодняшний гаджет. Меня сразу привлекла возможность переключения 3v3->5v логики и обширный набор поддерживаемых шин. В официальной вики были описаны следующие характеристики:
Шины: 1x SPI с двумя чип-селектами (можно подключить до двух устройств на одну шину), 1x I2C, 1x JTAG (полноценный, с ресетом!) и 2x UART с дополнительными линиями CTS/RTS для совместимости с классическими COM-портами.
Используемый контроллер: WCH CH347. Некоторым читателям чип может показаться знакомым по аналогии с классическим CH341A.
Питание: 5В, 3.3В, потребление ~65мА на VBus. Есть самовосстанавливающийся предохранитель на «входе».

Я сразу смекнул, что смогу использовать гаджет как для восстановления программно-убитых устройств по типу КПК, так и для отладки своих собственных самоделок, благо набор шин к этому располагает. Устройство приехало ко мне примерно через месяц, в небольшом пакетике и брендовой коробочке, в которую входило само устройство, кабель USB Type-B (ну почему не Type-C?), Dupont-провода в IDC-коннекторе для всех шин, а также небольшой мануал. Нареканий к доставке кроме скорости не возникло.
Сам гаджет представляет из себя компактную металлическую коробочку с «ушками» для удобного крепления на столе или стене. Сверху расположена шпаргалка по распиновке и режимам работы CH347, а также светодиодные индикаторы для UART.
Разбирается гаджет очень просто: достаточно лишь открутить несколько винтов с обеих боковых пластин устройства и перед нами открывается вид на плату. Схемотехника здесь простейшая: самовосстанавливающийся предохранитель, линейный регулятор AMS1117, который питает контроллер и нагрузку на VCC (до ~600мА), сам CH347, а также набор ключей для согласования режимов работы. CH347 — это не просто ASIC, а вполне себе полноценный микроконтроллер, прошивку которого можно обновить, правда SDK для использования CH347 как МК производитель не предоставляет.
После подключения гаджет радостно зажег индикатор PWR, подтвердив свою работоспособность, а значит пришло время протестировать возможные варианты использования!
С UART всё просто и понятно: нам достаточно лишь выбрать желаемый режим работы (M0 — двухканальный UART, остальные режимы — UART + I2C/SPI или UART + JTAG) с помощью тумблера и подключить/припаять Dupont'ы к соответствующим пинам на плате. UART здесь достаточно быстрый: при двухканальном режиме работы, на UART0 можно добиться до 9Мб/с (мегабод), а на UART1 — до 7.5Мб/с.
В качестве теста я решил снять лог загрузки со своего проекта самодельной игровой консоли. Для работы с UART я привык использовать Putty: сначала я припаял RX/TX и массу, затем запустил Putty и выбрал COM-порт, соответствующий первому каналу, установил бодрейт в 115200 и включил консоль:

Всё работает! В целом, гаджет можно использовать и для прошивки более сложных устройств: например многие смартфоны и кнопочные телефоны всё ещё имеют альтернативный режим прошивки через UART, а ретро-телефоны Samsung и LG так вообще не имеют альтернатив — если нет специального JIG, то остаётся лишь вызванивать RX/TX с разъёма и подпаиваться напрямую к UART процессора!
С SPI и I2C уже всё чуточку интереснее. Дело в том, что как вы уже могли понять — чип использует свой собственный проприетарный протокол для организации моста между программой на ПК и шиной данных. Для работы с этим протоколом производитель предоставляет уже готовую библиотеку для Windows начиная с 2000, так что возможно у чипа есть перспективы для оживления легаси пром. оборудования. Для Linux же есть альтернативные драйвера, которые пробрасывают CH347 как обычные spidev и i2c-dev устройства.

Для проверки коммуникации можно использовать специальную тестовую программу из SDK, которая позволяет отправлять произвольные данные и даже прошивать флэшки 25 'ой и EEPROM'ки 24'ой серии.

Давайте же попробуем написать что-нибудь полезное! Например, подключим к гаджету 1.8-дюймовый дисплей и что-нибудь на него выведем.
С разводкой дисплея проблем не возникает: SDO к MOSI, SCK к CLK, VCC к VCC и BL (питание подсветки), однако для управления DBI-дисплеями необходимы ещё две дополнительные линии: D/C (линия, определяющая как интерпретировать байт на входе), а также RESET для аппаратного сброса контроллера. И с этим проблем тоже не возникает: у контроллера есть как минимум четыре свободных GPIO, два из которых мы с вами и будем использовать для управления линиями дисплея — GPIO6 (CTS на UART1) и GPIO7 (RTS на UART).

Далее я начал изучать PDF-ку с документацией сомнительного качества и писать код инициализации. Начинается всё с получения контекста устройства с помощью функции CH347OpenDevice, которая принимает в себя индекс нужного контроллера в системе и возвращает непонятный идентификатор (вероятно WinUSB?). Интересно то, что в остальном API используется не идентификатор, а как раз тот самый индекс, который в большинстве случаев будет 0. Далее мы получаем информацию об устройстве и сверяем режим работы, если он отличается от нужного — выбрасываем исключение:
/* Initialize CH347 */
deviceHandle = CH347OpenDevice(deviceIndex);
if (!deviceHandle)
throw new std::runtime_error("Failed to open CH347 device");
mDeviceInforS info;
CH347GetDeviceInfor(deviceIndex, &info);
if (info.ChipMode != 1)
throw new std::runtime_error("Incorrect chip mode");Далее настраиваем SPI-контроллер. На выбор есть все три существующих режима, настройки полярности и возможность вручную дергать один из двух доступных ChipSelect'ов, а также тайминги. Частота работы определяется предустановленным набором делителей — 60МГц, 30МГц, 15МГц и т.п. Не забываем настроить таймаут каждой USB-транзакции:
CH347SetTimeout(deviceIndex, CommunicationTimeout, CommunicationTimeout);
/* Initialize SPI bus & GPIO */
mSpiCfgS cfg;
memset(&cfg, 0, sizeof(cfg));
cfg.CS1Polarity = 0; /* CS0 = active low */
cfg.CS2Polarity = 0;
cfg.iActiveDelay = DefaultChipSelectDelay;
cfg.iByteOrder = 1;
cfg.iClock = 0;
cfg.iDelayDeactive = DefaultChipSelectDelay;
cfg.iIsAutoDeativeCS = 0;
cfg.iMode = 0;
cfg.iChipSelect = 0;
cfg.iSpiWriteReadInterval = DefaultChipSelectDelay;
if (!CH347SPI_Init(deviceIndex, &cfg))
throw new std::runtime_error("Failed to initialize SPI");И инициализируем дисплей. Здесь есть важный момент: функция CH347GPIO_Set устанавливает состояние всего GPIO-контроллера в чипе и поэтому принимает в себя три битовые маски с конфигурацией каждого пина. Функции GPIO стандартные — вход/выход, плюс обработка прерываний с помощью специального callback'а:
CH347GPIO_Set(deviceIndex, 1 << config.IOReset, 1 << config.IOReset, 0);
this_thread::sleep_for(16ms); /* HW reset */
CH347GPIO_Set(deviceIndex, 1 << config.IOReset, 1 << config.IOReset, 1 << config.IOReset);
/* Software initialization */
SendCommand(EMIPICommandList::cmdSWRESET, 0, 0, 16);
SendCommand(EMIPICommandList::cmdSLPOUT, 0, 0, 0);
/* Framerate and refresh */
uint8_t frameRateControlRegister[] = { 0x01, 0x2C, 0x2D };
SendCommand(EMIPICommandList::cmdFRMCTL1, frameRateControlRegister, sizeof(frameRateControlRegister), 0);
SendCommand(EMIPICommandList::cmdFRMCTL2, frameRateControlRegister, sizeof(frameRateControlRegister), 0);
SendCommand(EMIPICommandList::cmdFRMCTL3, frameRateControlRegister, sizeof(frameRateControlRegister), 0);
SendCommand(EMIPICommandList::cmdFRMCTL4, frameRateControlRegister, sizeof(frameRateControlRegister), 0);
/* Power control */
uint8_t powerControlRegister1[] = { 0xA2, 0x02, 0x84 };
SendCommand(EMIPICommandList::cmdPWCTL1, powerControlRegister1, sizeof(powerControlRegister1), 0);
uint8_t powerControlRegister2 = 0xC5;
SendCommand(EMIPICommandList::cmdPWCTL2, &powerControlRegister2, sizeof(powerControlRegister2), 0);
uint8_t powerControlRegister3[] = { 0x0A, 0x00 };
SendCommand(EMIPICommandList::cmdPWCTL3, powerControlRegister3, sizeof(powerControlRegister3), 0);
uint8_t powerControlRegister4[] = { 0x8A, 0x2A };
SendCommand(EMIPICommandList::cmdPWCTL4, powerControlRegister4, sizeof(powerControlRegister4), 0);
uint8_t powerControlRegister5[] = { 0x8A, 0xEE };
SendCommand(EMIPICommandList::cmdPWCTL5, powerControlRegister5, sizeof(powerControlRegister5), 0);
uint8_t powerControlVCOMRegister = 0x0E;
SendCommand(EMIPICommandList::cmdPWCTL6, &powerControlVCOMRegister, sizeof(powerControlVCOMRegister), 0);
/* Addressing */
uint8_t madCtlMode = 0xC8;
SendCommand(EMIPICommandList::cmdMADCTL, &madCtlMode, sizeof(madCtlMode), 0);
uint8_t rasetRegister[] = { 0x0, 0x0, 0x0, 0x7f };
uint8_t casetRegister[] = { 0x0, 0x0, 0x0, 0x9f };
SendCommand(EMIPICommandList::cmdRASET, casetRegister, sizeof(rasetRegister), 0);
SendCommand(EMIPICommandList::cmdCASET, rasetRegister, sizeof(casetRegister), 0);
uint8_t colorMode = 0x05; /* RGB565 */
SendCommand(EMIPICommandList::cmdCOLMOD, &colorMode, sizeof(colorMode), 0);
SendCommand(EMIPICommandList::cmdDISPON, nullptr, 0, 0);
...
void CDisplay::SendCommand(EMIPICommandList command, uint8_t* data, size_t length, uint32_t delay)
{
uint8_t cmd = (uint8_t)command;
/* Send command */
GPIOSet(deviceIndex, config.IODataCommand, 0);
CH347SPI_Write(deviceIndex, 0, sizeof(cmd), sizeof(cmd), &cmd);
/* Send arguments (if any) */
if (data && length)
{
GPIOSet(deviceIndex, config.IODataCommand, 1);
CH347SPI_Write(deviceIndex, 0, length, length, data);
}
this_thread::sleep_for(chrono::milliseconds(delay));
}Теперь можно запустить программу и посмотреть на результат. Если вы увидели шум (или мусор) на экране — значит вы всё делаете правильно и контроллер успешно проинициализирован.
Теперь можно что-нибудь вывести. Подготавливаем изображение, преобразовав в 16-битный массив пикселей, переводим контроллер в режим записи в VRAM, отправляем изображение:
void CDisplay::CopyFrameBuffer(uint8_t* pixels)
{
uint8_t cmd = (uint8_t)EMIPICommandList::cmdRAMWR;
/* Send command */
GPIOSet(deviceIndex, config.IODataCommand, 0);
CH347SPI_Write(deviceIndex, 0, sizeof(cmd), sizeof(cmd), &cmd);
GPIOSet(deviceIndex, config.IODataCommand, 1);
uint32_t frameBufferSize = config.Width * config.Height * 2;
uint32_t offset = 0;
CH347SPI_Write(deviceIndex, 0, frameBufferSize, 2, pixels);
}И пишем небольшую демо-программу:
int main(int argc, char** argv)
{
CDisplayConfiguration config = {
ChipSelectIndex, ResetGPIOIndex, DataCommandGPIOIndex, 128, 160
};
CDisplay display(config);
display.CopyFrameBuffer(beach);
return 0;
}Результат — на дисплее появляется картинка!
Потенциальных применений у такого гаджета много: можно сделать красивые анимированные часы, мониторинг датчиков, показ уведомлений или дублировать окно с основного ПК. А ведь когда-то ради такого покупали LPT-провода, дисплеи от Сименсов и вручную превращали параллельную шину в последовательную...
Вот такой интересный гаджет выпустила компания Waveshare — и, что радует, по очень приятной цене! Ссылку по понятным причинам прилагать не буду, но при желании вы сможете его найти на всех трёх крупных маркетплейсах. Кроме того, можно купить Breakout-плату с тем же самым чипом за ~500 рублей, но там не будет таких удобных переключателей и Dupont'ов.
К сожалению, теста JTAG в статье не будет. У меня пока нет готовых к работе необычных гаджетов, где можно было бы протестировать OpenOCD... однако мой HTC Dream всё ещё ждёт свою прошивку модема!
А если вам интересна тематика ремонта, моддинга и программирования для гаджетов прошлых лет — подписывайтесь на мой Telegram-канал «Клуб фанатов балдежа», куда я выкладываю бэкстейджи статей, ссылки на новые статьи и видео, а также иногда выкладываю полезные посты и щитпостю. А ролики (не всегда дублирующие статьи) можно найти на моём YouTube канале.
У меня также есть Boosty.


А ещё я держу все свои мобилы в одной корзине при себе (в смысле, все проекты у одного облачного провайдера) — Timeweb. Потому нагло рекомендую то, чем пользуюсь сам — вэлкам.