Будучи творческим человеком и техногиком, я обожаю при первой возможности апгрейдить своё оборудование. Время от времени я мониторю маркетплейсы в поисках чего-то новенького и в этот раз я наткнулся на настоящий мультитул для 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
С 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
С 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. Потому нагло рекомендую то, чем пользуюсь сам — вэлкам.
Комментарии (17)

MaFrance351
15.11.2025 14:10Заинтересовался. Поискал поподробнее.

Оказывается, даже прогеры EEPROM на этих чипах уже делают. По отзывам даже лучше, чем "народный" CH341A.

bodyawm Автор
15.11.2025 14:10CH341 сам по себе в оригинале был мостом USB - SPI :) В 347 просто жтаг добавили.

mozg37
15.11.2025 14:10а можно и за 200 аналогичную на алике найти

bodyawm Автор
15.11.2025 14:10О чем я и написал в конце статье) Waveshare интересен тем, что это самостоятельная коробочка которую можно закрепить на столе и использовать когда надо, в то время как для копеечных борд корпус нужно печатать самому.

010011011000101110101
15.11.2025 14:10а что, ардуина нано или esp32 не делают всё то же самое?

bodyawm Автор
15.11.2025 14:10Кроме JTAG (DirtyJTAG не в счет) - в целом делают, но софтовый мост придется писать самому или брать чужой велик. А здесь Industrial Grade изделие - подключил и работай

litalen
15.11.2025 14:10Откройте для себя FT2232/FT4232. Стоят столько же или лишь чуточку дороже (1000-1500р на одном популярном китайском маркетплейсе), а умеют больше, и история у них более давняя. При этом с USB Type-C, 4 канала (а не 2, у второй модели), управляются хоть питоном. Имеют 3.3В-пины, которые толерантны и к 5В. Конфигурационная прошивка давно разобрана, API - опенсорс. Их сила в MPSSE, и при помощи подобного можно даже замахнуться на чтение NAND.

EmCreatore
15.11.2025 14:10Немного конечно неадекватоно, когда однин из главных плюсов инструмента - его дешивизна.
Все равно что выбирать самый дешевый молоток для забивания гвоздей, а лучше вообще первым попавшимся булыжником забивать.
Либо наоборот, давайте микроскопом забивать, т.е. приплетать сюда ПЛИС-ы
Да возьмите любую отладочную плату на микроконтроллере.
Например такую - https://www.renesas.com/en/design-resources/boards-kits/ek-ra8p1
Сконфигурируйте на ней WEB сервер.
И получите тысячи применений вместо четырех.
GPT-5.1-Codex влет напишет нужные крависые WEB , протокол обмена по JSON с платой, а на плате напишет обмен по всем современным интерфейсам: I2C, I3C, SPI, OSPI, QSPI, SDIO, UART, USART, 1WIRE, LIN, CAN, MODBUS, CANFD, USB, SSI, PDM, MMC, MIPI ...
И не нужно никах левых драйверов под Windows. Можно хоть с телефона рулить.
bodyawm Автор
Друзья! На какое-то время я снова начну постить статьи раз в две недели, а не раз в неделю как обычно. Я тут в общем на диету подсел, достаточно строгую, но эффективную: был 153, за 2.5 месяца стал 128. Лимит 1.000 калорий в день, питание - один раз в сутки, всё это в сочетании с еженедельными двухсуточными полными отказами от еды.
В целом, чувствую себя неплохо, но пока ещё ищу баланс между максимальным похудением и работоспособностью. Например, после двухсуточного голодания съедание даже дольки чего-то сладкого (чисто для глюкозы) вызывает тремор, так что нужно быть осторожнее.
Если что, мне 24 - именно из-за молодого возраста выбрал такую строгую диету, поскольку если не привести себя в порядок сейчас, уже даже после 25-и это может быть куда сложнее.
bodyawm Автор
На следующей неделе расскажу о нашумевшей Linux-игровой консоли за 1.800 рублей - R36s. На самом деле, их бывает аж три ревизии, причём одна работает на экзотическом и очень редком MIPS-процессор и это не Ingenic!
Скрытый текст
Узнаем с вами что это за гаджеты, что у них "под капотом" и на что они способны помимо эмуляции ретро-консолей. Дааа, Vulkan в консоли до 2.000 рублей - это было что-то из области мечтаний даже 2-3 года назад...
bodyawm Автор
А, ну и ещё важное дополнение: статья - не реклама, а просто обзор. Я купил коробочку за свои кровные :)