habrahabr

Мультитул для инженера: волшебная коробочка с I2C/SPI/UART/JTAG за 1.000 рублей

  • понедельник, 17 ноября 2025 г. в 00:00:09
https://habr.com/ru/companies/timeweb/articles/966104/

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Что думаете о таком преобразователе?
40.74%Реально мультитул! Крутейшая штука за копейки, побежал заказывать!!!44
25%Увы, мне неведомы кейсы его применения. Считаю его бесполезным.27
12.04%Мультитул классный, но у меня уже есть что-то похожее от другого производителя. Не жалуюсь.13
8.33%Полная хрень!9
5.56%Это все туфта. Настоящий мужчина сам соберет на ПЛИСине такой конвертер, при этом не копипастя чужие блоки!!!6
8.33%Какие ПЛИСы??? Какие CH347??? Вы все позеры, я лично софтовый SPI реализовывал на LPT…9
Проголосовали 108 пользователей. Воздержались 26 пользователей.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Что думаете об обзорах на оборудование? Есть смысл рассказывать о всяких ништяках, что я иногда покупаю для работы?
93.14%Да, вполне интересно.95
0%Нет, лучше пиши для уже существующих рубрик.0
6.86%Сейчас напишет 10 обзоров на всю свою оборудку и пойдет по бартеру у Quicko выпрашивать нормальный стол и феном!!! Знаем мы твои хитрости!!!7
Проголосовали 102 пользователя. Воздержались 9 пользователей.