...может кто-то подружил PC и программатор на CH341T для работы по I2C?
Тоже был озадачен проблемой управления i2c расширителем портов напрямую с компьютера. Моей первой попыткой была платка
FT200XD CJMCU-200. Несколько дней проломал над ней голову, ничего не получилось и забил. Спустя какое-то время купил героя этой ветки
CH341T и был приятно удивлен как просто ее подключить и как все четко работает. В некоторой степени мне этот форум помог со стартом, вот теперь поделюсь и я своими наработками.
Фото:
Спойлер

1) Для работы модуля обязательно нужен драйвер. В начале нашел просто файлы драйвера, которые нужно вручную ставить, но не рекомендую так делать. Как оказалось, при ручной установке приложение автоматически не подхватывает необходимую для работы DLL. Нашел корректные файлы драйверов с установщиком в
этом репозитории github.
2) Теперь достаточно подключить платку к USB, она определиться как "USB-EPP/I2C... CH341A" в ветке Interface и готова к работе
3) Чтобы протестировать работоспособность существует
готовое приложение CH341A-tool в том же репозитории гитхаба. Меня интересовала работа с
расширителем портов MCP23017 (зеленая продолговатая платка на 16 логических портов, доступна на Алиэкспресс). Чтобы ей управлять необходимо слать по i2c пары чисел (регистр - значение). Для MCP23017 можно протестировать следующую последовательность на вкладке I2C write/read (изначальный адрес MCP23017 равен 0x20):
0x0A 0x20 - настройка устройства
0x00 0x00 - все пины порта А на output
0x12 0xFF - все пины порта А установить в HIGH
0x12 0x00 - все пины порта А установить в LOW
Регистры MCP23017:
Спойлер
Код:
#define MCPR_IODIR_A 0x00 // Для блока A | Настраивает работу портов на (1 вход, 0 выход) (соотношение бита к порту 0b00000000 - pa7 pa6 pa5 pa4 pa3 pa2 pa1 pa0)
#define MCPR_IODIR_B 0x01 // Для блока B |
#define MCPR_IPOL_A 0x02 // Для блока A | Задает для инпутных портов инверсию получаемого значения
#define MCPR_IPOL_B 0x03 // Для блока B |
#define MCPR_GPINTEN_A 0x04 // Для блока A | Опредетяет разрешена ли работа портов в качестве источника прерывания
#define MCPR_GPINTEN_B 0x05 // Для блока B |
#define MCPR_DEFVAL_A 0x06 // Для блока A | Хранит бит для каждого порта и если значение на порту не равно этому биту, то генерит прерывание (если разрешено регистром GPINTEN)
#define MCPR_DEFVAL_B 0x07 // Для блока B |
#define MCPR_INTCON_A 0x08 // Для блока A | Если 1чка для порта - то прерывания будут при любой смене состояния порта
#define MCPR_INTCON_B 0x09 // Для блока B |
#define MCPR_IOCON 0x0A // Управление всем устройством, каждый бит настройка (слева направо идут 0b00100000):
// BANK = 0 : если 0, то регистры идут как тут поочередно. Если 1, то сначала все регистры для порта A, после все для B
// MIRROR = 0 : если 0, то порт А и В генерит прерывания на свои ножки, если 1, то сразу на ножки прерываний обеих портов
// SEQOP = 1 : если 1, то адрес инкрементируется (можно слать последовательно данные и следующая порция попадет в следующий регистр)
// DISSLW = 0 : можно включать(0) и отключать (1) ножку SDA в i2c выходе
// HAEN = 0 : пины адреса всегда включены в микросхеме MCP23017 (этот бит не влияет)
// ODR = 0 : делает выходы открытый коллектор (1) или обычные цифровые (0)
// INTPOL = 0 : значение выхода на пинах прерывания: 1 - при активном hight, 0 - при активном будет low
// последний бит зарезервирован
#define MCPR_GPPU_A 0x0C // Для блока A | Определяет будет ли подтяжка input портов (если 1, то подтяжка 100кОм на +)
#define MCPR_GPPU_B 0x0D // Для блока B |
#define MCPR_INTF_A 0x0E // Для блока A | Только для чтения. Определяет на какой из ножек было прерывание (там будет 1)
#define MCPR_INTF_B 0x0F // Для блока B |
#define MCPR_INTCAP_A 0x10 // Для блока A | Напряжение на всех портах в момент события прерывания
#define MCPR_INTCAP_B 0x11 // Для блока B |
#define MCPR_GPIO_A 0x12 // Для блока A | Текущие значения портов input (чтение считывает порт, запись модифицирует OLAT регистр)
#define MCPR_GPIO_B 0x13 // Для блока B |
#define MCPR_OLAT_A 0x14 // Для блока A | Доопределяет output ножки (чтение его читает буфер, а не порт, запись модифицирует выходной буфер, который изменит выходные порты)
#define MCPR_OLAT_B 0x15 // Для блока B |
4) Программа для теста это хорошо, но в реальных условиях нужна своя программа. Написал на c++ минимальный простой код, который реализует пример, аналогичный blink на ардуино. Мигает всеми пинами порта A (порт A - это пины 0-7, порт B - пины 8-15).
Код:
Спойлер
Код:
#include <windows.h>
#include <iostream>
// Загрузка функций из CH341DLL.dll
typedef BOOL(__stdcall* CH341OpenDevice_t)(ULONG iIndex);
typedef BOOL(__stdcall* CH341CloseDevice_t)(ULONG iIndex);
typedef BOOL(__stdcall* CH341StreamI2C_t)(ULONG iIndex, ULONG iWriteLength, PUCHAR iWriteBuffer, ULONG iReadLength, PUCHAR oReadBuffer);
int main() {
// Загрузка DLL
HMODULE hDLL = LoadLibrary("CH341DLL.dll");
if (!hDLL) {
std::cerr << "Failed to load CH341DLL.dll. Install the driver for CH341T" << std::endl;
std::cin.get();
return -1;
}
// Получение указателей на функции
CH341OpenDevice_t CH341OpenDevice = (CH341OpenDevice_t)GetProcAddress(hDLL, "CH341OpenDevice");
CH341CloseDevice_t CH341CloseDevice = (CH341CloseDevice_t)GetProcAddress(hDLL, "CH341CloseDevice");
CH341StreamI2C_t CH341StreamI2C = (CH341StreamI2C_t)GetProcAddress(hDLL, "CH341StreamI2C");
if (!CH341OpenDevice || !CH341CloseDevice || !CH341StreamI2C) {
std::cerr << "Failed to get function pointers from CH341DLL.dll" << std::endl;
FreeLibrary(hDLL);
std::cin.get();
return -1;
}
// Открытие устройства CH341A (индекс 0)
ULONG deviceIndex = 0;
if (!CH341OpenDevice(deviceIndex)) {
std::cerr << "Failed to open CH341A device" << std::endl;
FreeLibrary(hDLL);
std::cin.get();
return -1;
}
// Адрес I2C устройства
const UCHAR i2cAddress = 0x20;
// Последовательность команд
UCHAR commands[4][2] = {
{ 0x0A, 0x20 },
{ 0x00, 0x00 },
{ 0x12, 0x00 },
{ 0x12, 0xFF }
};
// Буфер для данных
UCHAR writeBuffer[3]; // 1 байт для адреса + 2 байта данных
UCHAR readBuffer[1]; // Не используется в данном случае
for (int i = 0; i < 2; ++i) {
// Подготовка буфера для записи
writeBuffer[0] = i2cAddress << 1; // Адрес устройства (сдвинутый влево для I2C)
writeBuffer[1] = commands[i][0];
writeBuffer[2] = commands[i][1];
// Отправка команды через I2C
if (!CH341StreamI2C(deviceIndex, 3, writeBuffer, 0, readBuffer)) {
std::cerr << "Failed to send I2C command " << i + 1 << std::endl;
CH341CloseDevice(deviceIndex);
FreeLibrary(hDLL);
std::cin.get();
return -1;
}
}
// Мигалка
int i = 2;
while (true) {
writeBuffer[0] = i2cAddress << 1;
writeBuffer[1] = commands[i][0];
writeBuffer[2] = commands[i][1];
if (!CH341StreamI2C(deviceIndex, 3, writeBuffer, 0, readBuffer)) {
std::cerr << "Failed to send I2C command " << i + 1 << std::endl;
CH341CloseDevice(deviceIndex);
FreeLibrary(hDLL);
std::cin.get();
return -1;
}
Sleep(500);
i++;
if (i > 3) i = 2;
}
std::cout << "Commands sent successfully!" << std::endl;
// Закрытие устройства
CH341CloseDevice(deviceIndex);
// Освобождение DLL
FreeLibrary(hDLL);
std::cin.get();
return 0;
}
Все реализуется через библиотеку CH341DLL.dll из драйвера (если бы его ставили вручную, тогда требовалось ее таскать с приложением, а так она подхватывается из System32). Основная функция взаимодействия это CH341StreamI2C, которая принимает 4 аргумента: индекс устройства (если подключена только 1 плата CH341T, ее индекс 0), количество байт для отправки, буфер для отправки, количество байт для приема, буфер приема. Чтобы найти какие именно коды управления нужно слать конкретно для вашего i2c устройства, можно штудировать документацию. Но как по мне гораздо проще изучить библиотеку для этого устройства под Ардуино. Как правило, ввиду дефицита памяти, они написаны минималистично и можно легко отследить какая информация отсылается/принимается при инициализации и работе конкретного устройства.
5) На платке конвертора есть перемычка логики 3.3в или 5в. Пока тестировал заметил особенность. Если логика выставлена в 5в и пину расширителя портов задать HIGH состояние. То если выключить компьютер и через некоторое время его включить - это значение сохранится при старте. Дело в том, что конденсаторы в БП компьютера до некоторого значения разряжаются, а после продолжительное время хранят минимальный заряд, которого хватает чтобы не сбросить логику MCP23017. Это может вызвать путаницу и непредсказуемое поведение. Но если перемычку выставить в 3.3в, то стабилизатор на платке решает эту проблему и при включении компьютера все пины выключены.
Самый простой способ - взять ардуино-нанку и работать через нее с чем душа пожелает.

Просто, но в некоторых случаях недостаточно надежно. У меня есть опыт в использовании Ардуино в режиме 24/7/365 и все они рано или почти рано зависают. Да, для перезагрузки есть Watchdog timer, и он прям спасает. Но вот, к примеру, UNO нормально перезагружается, а STM32 среди успешных ребутов иногда зависает так, что Watchdog не может ее вернуть. Это редкий случай, наверно, раз в месяц, но все же оно есть. Еще после ребута есть риск, что расширитель портов себя может некорректно повести, например, моргнет портами. Лучше этого избежать заранее.