Программатор CH341T как I2C адаптер

Если ваш вопрос не влез ни в одну из вышеперечисленных тем, вам сюда.
Аватара пользователя
Just_Fluffy
Вымогатель припоя
Сообщения: 532
Зарегистрирован: Ср июн 29, 2022 16:25:45

Re: Программатор CH341T как I2C адаптер

Сообщение Just_Fluffy »

Serjone, поддерживает.
Еще есть 10-битная адресация.
Белая и Пушистая
Аватара пользователя
zAries
Потрогал лапой паяльник
Сообщения: 305
Зарегистрирован: Ср сен 11, 2024 10:18:53

Re: Программатор CH341T как I2C адаптер

Сообщение zAries »

Кто имеет интерес - читайте.
Вложения
i2c_description_rus.pdf
(315.96 КБ) 185 скачиваний
Audiatur et altera pars !
flarens2
Родился
Сообщения: 1
Зарегистрирован: Пн мар 24, 2025 11:06:28

Re: Программатор CH341T как I2C адаптер

Сообщение flarens2 »

[uquote="alex_armV",url="/forum/viewtopic.php?p=4098908#p4098908"]...может кто-то подружил PC и программатор на CH341T для работы по I2C?[/uquote]
Тоже был озадачен проблемой управления 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в, то стабилизатор на платке решает эту проблему и при включении компьютера все пины выключены.

[uquote="BOB51",url="/forum/viewtopic.php?p=4099239#p4099239"]Самый простой способ - взять ардуино-нанку и работать через нее с чем душа пожелает.
:wink:[/uquote]
Просто, но в некоторых случаях недостаточно надежно. У меня есть опыт в использовании Ардуино в режиме 24/7/365 и все они рано или почти рано зависают. Да, для перезагрузки есть Watchdog timer, и он прям спасает. Но вот, к примеру, UNO нормально перезагружается, а STM32 среди успешных ребутов иногда зависает так, что Watchdog не может ее вернуть. Это редкий случай, наверно, раз в месяц, но все же оно есть. Еще после ребута есть риск, что расширитель портов себя может некорректно повести, например, моргнет портами. Лучше этого избежать заранее.
Ответить

Вернуться в «Разные вопросы по МК»