
Как человек, увлеченный техникой и творчеством, я всегда в поиске новых инструментов для улучшения своего рабочего арсенала. Во время очередного мониторинга онлайн-магазинов я обнаружил крайне интересное устройство для embedded-разработчиков — компактный контроллер, объединяющий поддержку ключевых интерфейсов I2C, SPI, UART и JTAG. Самое удивительное — его цена составляет всего около 1000 рублей. Естественно, я не смог устоять и приобрел его. В этой статье я подробно расскажу об этом гаджете, его возможностях и поделюсь первым опытом использования. Приятного чтения!
Что это за устройство?
Для меня это первый опыт написания обзора на оборудование. Обычно мой инструментарий довольно стандартен и не тянет на громкие сравнения вроде «почему паяльная станция X лучше Y». Однако я решил сделать исключение, так как подробных обзоров на этот конкретный девайс я не нашел, несмотря на его очевидную полезность. Он пригодится не только embedded-инженерам и разработчикам, но и ремонтникам смартфонов, планшетов и другой электроники.

Идея купить это устройство пришла ко мне, когда я готовил материал о написании BIOS для игровой консоли от Waveshare. Среди других продуктов производителя в рекомендациях мелькнул и этот адаптер. Меня сразу привлекли возможность переключения логических уровней 3.3В/5В и широкий набор поддерживаемых интерфейсов. Согласно официальной документации, устройство обладает следующими характеристиками:
Поддерживаемые шины: Один SPI-порт с двумя линиями выбора микросхем (Chip Select), что позволяет подключить до двух устройств. Один I2C, один полноценный JTAG-порт (с линией сброса!) и два UART-порта с дополнительными управляющими линиями CTS/RTS для совместимости с классическими COM-портами.
Сердце устройства: Контроллер WCH CH347. Многим он может быть знаком по более простому и распространенному CH341A.
Питание: Входное напряжение 5В, на выходе доступны 5В и 3.3В. Потребление от USB около 65 мА. На входе питания установлен самовосстанавливающийся предохранитель для защиты.

Waveshare — достаточно известный бренд на рынке одноплатных компьютеров, различных плат расширения и инструментов для разработки.
Я сразу понял, что этот адаптер можно использовать как для восстановления «кирпичей» (например, КПК), так и для отладки собственных электронных проектов. Посылка шла около месяца. В небольшой фирменной коробке я обнаружил само устройство, кабель USB Type-B (жаль, что не Type-C), набор проводов Dupont в удобном IDC-коннекторе для всех интерфейсов и краткую инструкцию. К качеству доставки, кроме ее скорости, претензий не было.

Сам гаджет выполнен в компактном металлическом корпусе с «ушками» для крепления на стену или стол. На верхней крышке нанесена удобная шпаргалка с распиновкой и режимами работы контроллера CH347, а также светодиодные индикаторы активности UART-портов.

Конструкция разборная. Достаточно открутить несколько винтов по бокам, чтобы увидеть плату. Схема довольно простая: самовосстанавливающийся предохранитель, линейный стабилизатор AMS1117 (который питает сам контроллер и может отдать до 600 мА на внешнюю нагрузку), сам чип CH347 и набор ключей для согласования логических уровней и режимов. CH347 — это не просто специализированная микросхема, а полноценный микроконтроллер, прошивку которого можно обновлять. К сожалению, SDK для программирования его как МК производитель не предоставляет.

После подключения к компьютеру устройство ожило — загорелся светодиод PWR. Значит, можно приступать к тестированию!
Работа с UART
Использование UART максимально простое. Нужно перевести тумблер в нужный режим (M0 — два независимых UART, другие режимы — комбинация UART с I2C/SPI или JTAG) и подключить провода к соответствующим контактам целевой платы. Скорость обмена впечатляет: в двухканальном режиме UART0 может работать на скорости до 9 Мбод, а UART1 — до 7.5 Мбод.

Провода в комплектном шлейфе установлены не абы как — они имеют цветовую маркировку, соответствующую функциям, а не только стандартные «красный — плюс, черный — земля».
Для теста я решил снять лог загрузки со своей самодельной игровой консоли. Для работы с последовательным портом я привык использовать программу Putty. Припаял провода RX, TX и GND к консоли, запустил Putty, выбрал соответствующий COM-порт, установил скорость 115200 бод и включил питание:

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

Драйвер можно скачать по ссылке с официального сайта.
Для первичной проверки связи можно использовать тестовую утилиту из SDK, которая позволяет отправлять произвольные данные и даже прошивать микросхемы памяти типа SPI Flash (25-й серии) и EEPROM (24-й серии).

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

Далее я погрузился в изучение PDF-документации сомнительного качества и написание кода инициализации. Начинается всё с открытия устройства с помощью функции CH347OpenDevice, которая принимает индекс контроллера в системе и возвращает хэндл (вероятно, WinUSB). Интересно, что в остальном API используется не этот хэндл, а именно индекс устройства, который в большинстве случаев будет равен 0. Затем мы запрашиваем информацию об устройстве и проверяем его режим работы.
/* Инициализация CH347 */
deviceHandle = CH347OpenDevice(deviceIndex);
if (!deviceHandle)
throw new std::runtime_error("Не удалось открыть устройство CH347");
mDeviceInforS info;
CH347GetDeviceInfor(deviceIndex, &info);
if (info.ChipMode != 1)
throw new std::runtime_error("Неверный режим работы чипа");
Следующий шаг — настройка SPI-контроллера. Доступны все три стандартных режима (SPI Mode 0, 1, 2, 3), настройки полярности, возможность ручного управления двумя линиями выбора микросхем (Chip Select), а также настройка таймингов. Частота SPI задается выбором из предустановленных делителей: 60 МГц, 30 МГц, 15 МГц и т.д. Также важно установить таймаут для USB-транзакций.
CH347SetTimeout(deviceIndex, CommunicationTimeout, CommunicationTimeout);
/* Инициализация шины SPI и GPIO */
mSpiCfgS cfg;
memset(&cfg, 0, sizeof(cfg));
cfg.CS1Polarity = 0; /* CS0 — активный низкий уровень */
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("Ошибка инициализации SPI");
Теперь инициализируем дисплей. Важный нюанс: функция CH347GPIO_Set устанавливает состояние всех GPIO-пинов разом, поэтому она принимает три битовые маски — для направления, выходного значения и маски изменения. Доступны стандартные функции: настройка входа/выхода, а также обработка прерываний через специальный callback.
CH347GPIO_Set(deviceIndex, 1 << config.IOReset, 1 << config.IOReset, 0);
this_thread::sleep_for(16ms); /* Аппаратный сброс */
CH347GPIO_Set(deviceIndex, 1 << config.IOReset, 1 << config.IOReset, 1 << config.IOReset);
/* Программная инициализация */
SendCommand(EMIPICommandList::cmdSWRESET, 0, 0, 16);
SendCommand(EMIPICommandList::cmdSLPOUT, 0, 0, 0);
/* Настройка частоты кадров и обновления */
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);
/* Управление питанием */
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);
/* Настройка адресации */
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;
/* Отправка команды */
GPIOSet(deviceIndex, config.IODataCommand, 0);
CH347SPI_Write(deviceIndex, 0, sizeof(cmd), sizeof(cmd), &cmd);
/* Отправка аргументов (если есть) */
if (data && length)
{
GPIOSet(deviceIndex, config.IODataCommand, 1);
CH347SPI_Write(deviceIndex, 0, length, length, data);
}
this_thread::sleep_for(chrono::milliseconds(delay));
}
Теперь можно запустить программу. Если на экране появился шум или случайные артефакты — это хороший знак! Значит, контроллер дисплея откликнулся и инициализация прошла успешно.

На фото видна перемычка между CS и землей. Однако не все контроллеры дисплеев корректно работают, если линия выбора микросхемы постоянно прижата к низкому уровню. Например, контроллеры от ILI могут отказаться инициализироваться, если не управлять линией CS для каждой транзакции отдельно.
Теперь выведем что-нибудь осмысленное. Подготовим изображение, конвертировав его в массив 16-битных пикселей (RGB565). Переведем контроллер дисплея в режим записи в видеопамять (VRAM) и отправим данные.
void CDisplay::CopyFrameBuffer(uint8_t* pixels)
{
uint8_t cmd = (uint8_t)EMIPICommandList::cmdRAMWR;
/* Отправка команды */
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;
}
Результат — на дисплее отображается картинка!

Потенциал у такого инструмента огромный. Можно создавать умные часы с анимацией, системы мониторинга с выводом данных с датчиков, информационные панели для уведомлений или даже дублировать окно с компьютера. Вспомните, ведь когда-то для подобных задач люди покупали дисплеи от старых телефонов Siemens и городили схемы для преобразования LPT-порта в последовательный интерфейс!
Выводы
Waveshare выпустила действительно интересный и полезный гаджет, и что особенно приятно — по очень доступной цене! Прямые ссылки я по понятным причинам не привожу, но найти это устройство можно на всех крупных маркетплейсах по запросу. Также существует более простая отладочная плата (Breakout board) с тем же чипом CH347 примерно за 500 рублей, но она лишена удобных переключателей и комплектного шлейфа.
К сожалению, в рамках этой статьи не получилось протестировать режим JTAG. У меня пока нет под рукой подходящего устройства для экспериментов с OpenOCD... хотя мой старый HTC Dream всё еще ждет, когда я разберусь с прошивкой его модема!
Если вам интересна тематика ремонта, модинга и программирования электроники, в том числе и ретро-устройств — подписывайтесь на мой Telegram-канал «Клуб фанатов балдежа». Там я публикую бэкстейджи к статьям, анонсы новых материалов и иногда просто интересные находки. Видеоверсии некоторых обзоров можно найти на моем YouTube-канале.
Если вам понравилась статья...
И у вас появилось желание что-то мне задонатить (например прикольный гаджет) - пишите мне в телегу или в комментариях :) Без вашей помощи статьи бы не выходили! А ещё у меня есть Boosty.
Всего голосов: Всего голосов:Подготовлено при поддержке @Timeweb.Cloud
Больше интересных статей здесь: Гаджеты.
Источник статьи: Мультитул для инженера: волшебная коробочка с I2C/SPI/UART/JTAG за 1.000 рублей.