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

Waveshare — достаточно известный бренд, под которым реализуются одноплатные компьютеры, «бутербродная» периферия для них и инструменты.
Waveshare — достаточно известный бренд, под которым реализуются одноплатные компьютеры, «бутербродная» периферия для них и инструменты.

Я сразу смекнул, что смогу использовать гаджет как для восстановления программно-убитых устройств по типу КПК, так и для отладки своих собственных самоделок, благо набор шин к этому располагает. Устройство приехало ко мне примерно через месяц, в небольшом пакетике и брендовой коробочке, в которую входило само устройство, кабель 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Мб/с.

Провода в разъёмы установлены не бездумно — у них есть цветовая маркировка и логика помимо «красный — VCC, чёрный — GND».
Провода в разъёмы установлены не бездумно — у них есть цветовая маркировка и логика помимо «красный — VCC, чёрный — GND».

В качестве теста я решил снять лог загрузки со своего проекта самодельной игровой консоли. Для работы с 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));
}

Теперь можно запустить программу и посмотреть на результат. Если вы увидели шум (или мусор) на экране — значит вы всё делаете правильно и контроллер успешно проинициализирован.

На фото можно заметить перемычку между CS и массой, однако не все контроллеры дисплеев толерантны к постоянному низкому уровню на CS. На моей практике контроллеры ILI отказывались проходить инициализацию, если не разграничивать каждую транзакцию с помощью CS.
На фото можно заметить перемычку между CS и массой, однако не все контроллеры дисплеев толерантны к постоянному низкому уровню на CS. На моей практике контроллеры ILI отказывались проходить инициализацию, если не разграничивать каждую транзакцию с помощью CS.

Теперь можно что-нибудь вывести. Подготавливаем изображение, преобразовав в 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)


  1. bodyawm Автор
    15.11.2025 14:10

    Друзья! На какое-то время я снова начну постить статьи раз в две недели, а не раз в неделю как обычно. Я тут в общем на диету подсел, достаточно строгую, но эффективную: был 153, за 2.5 месяца стал 128. Лимит 1.000 калорий в день, питание - один раз в сутки, всё это в сочетании с еженедельными двухсуточными полными отказами от еды.

    В целом, чувствую себя неплохо, но пока ещё ищу баланс между максимальным похудением и работоспособностью. Например, после двухсуточного голодания съедание даже дольки чего-то сладкого (чисто для глюкозы) вызывает тремор, так что нужно быть осторожнее.

    Если что, мне 24 - именно из-за молодого возраста выбрал такую строгую диету, поскольку если не привести себя в порядок сейчас, уже даже после 25-и это может быть куда сложнее.


    1. bodyawm Автор
      15.11.2025 14:10

      На следующей неделе расскажу о нашумевшей Linux-игровой консоли за 1.800 рублей - R36s. На самом деле, их бывает аж три ревизии, причём одна работает на экзотическом и очень редком MIPS-процессор и это не Ingenic!

      Скрытый текст

      Узнаем с вами что это за гаджеты, что у них "под капотом" и на что они способны помимо эмуляции ретро-консолей. Дааа, Vulkan в консоли до 2.000 рублей - это было что-то из области мечтаний даже 2-3 года назад...


    1. bodyawm Автор
      15.11.2025 14:10

      А, ну и ещё важное дополнение: статья - не реклама, а просто обзор. Я купил коробочку за свои кровные :)


  1. MaFrance351
    15.11.2025 14:10

    Заинтересовался. Поискал поподробнее.

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


    1. bodyawm Автор
      15.11.2025 14:10

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


  1. mozg37
    15.11.2025 14:10

    а можно и за 200 аналогичную на алике найти


    1. bodyawm Автор
      15.11.2025 14:10

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


    1. maxus87
      15.11.2025 14:10

      Главное чтобы там +5 на ногах не было))


      1. bodyawm Автор
        15.11.2025 14:10

        Там и логика, и питание - 3.3В, здесь же есть тумблер - приятно))


  1. 010011011000101110101
    15.11.2025 14:10

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


    1. bodyawm Автор
      15.11.2025 14:10

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


  1. Goron_Dekar
    15.11.2025 14:10

    Чем отличается от bus pirate?


    1. bodyawm Автор
      15.11.2025 14:10

      Пират на базе МК общего назначения собран и немного дороже.


      1. litalen
        15.11.2025 14:10

        И вообще был заброшен (версии 4.0 который) с ~2016 до совсем недавнего времени, когда эту стюардессу зачем-то решили откопать.


  1. litalen
    15.11.2025 14:10

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



  1. 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. Можно хоть с телефона рулить.