Сейчас распространено несколько вариантов недорогих микросхем преобразователей USB-UART, позволяющих организовать обмен данными между устройством и компьютером на основе древнего интерфейса под названием COM-порт (или RS-232).
Но в этой статье мы не будем углубляться именно в передачу данных через этот последовательный интерфейс. Мне было интересно узнать возможности дополнительных выводов, которые когда-то давно использовались для инициализации и синхронизации передачи данных. К этим выводам относятся цифровые выходы DTR и RTS и цифровые входы CTS, DSR, DCD и RI.
Для тестов я решил взять три наиболее популярных сейчас микросхемы USB-UART: CP2102, FT232 и CH340. Первые две микросхемы можно купить на алиэкспрессе на платах с нужными нам выводами. Только на модулях с FT232 очень часто стоит фейковая микросхема, которая постоянно зависает, поэтому лучше их вообще там не покупать.
А вот для микросхемы CH340 распространены только модули с основными выводами RX и TX, без дополнительных. Поэтому мне пришлось спаять самому такой модуль для тестов. Правда я купил микросхему CH340 с индексом «G» – в этом варианте в схеме должен быть кварц на 12МГц с конденсаторами. А вот для CH340C кварц не требуется, поэтому в будущем для своих поделок лучше брать только её.
Моя пайка под спойлером (названия спойлеров слетают, поддержка исправляет глюк, поэтому я пока просто добавлю текст рядом)
Скрытый текст
Как управлять дополнительными выводами UART
Примеры кода я буду показывать на C# из Visual Studio, но всё легко переносится в другие языки. Главное – найти нужные функции для управления COM-портом.
Для обращений к порту я использовал стандартный класс C# System.IO.Ports.SerialPort, который даёт всё необходимое, кроме функции считывания вывода RI. Мне этот вывод не нужен для тестов, но наверняка есть какой-то способ считать и этот вывод порта. Возможно, кто-то из экспертов напишет в комментариях, как это сделать.
Код для управления дополнительными выводами в C#
Скрытый текст
//Создаём объект для дальнейшего использования
public static SerialPort MyPort = new SerialPort();
//Далее просто список часто используемых методов и свойств (не рабочий код)
//Настройка порта:
string[] SerialPort.GetPortNames(); //Выдаёт список названий доступных COM-портов
MyPort.PortName = "COM1"; //Название нужного порта
MyPort.BaudRate = 115200; //Частота обмена данными
MyPort.Parity = Parity.None; //Чётность для проверки данных
MyPort.DataBits = 8; //Количество бит для передачи
MyPort.StopBits = StopBits.One; //Количество стоп-бит
MyPort.ReadTimeout = 200; //Максимальное время ожидания данных
MyPort.WriteTimeout = 1000; // Максимальное время отправки данных
//Инициализация порта
MyPort.Open(); //Открытие указанного порта
MyPort.Close(); //Закрытие активного порта
//Работа с данными
MyPort.Write(byte[] buffer, int offset, int count); //Запись байт в порт
MyPort.Read(byte[] buffer, int offset, int count); //Чтение байт из порта
MyPort.BytesToRead; //Количество доступных байт в приёмном буфере
MyPort.DiscardInBuffer(); //Очистка входного буфера
MyPort.DiscardOutBuffer(); //Очистка выходного буфера
//Управление дополнительными выводами:
MyPort.DtrEnable = true; //Управление выходом DTR
MyPort.RtsEnable = true; //Управление выходом RTS
bool cts_read = MyPort.CtsHolding; //Считывание входа CTS
bool dsr_read = MyPort.DsrHolding; //Считывание входа DSR
bool cd_read = MyPort.CDHolding; //Считывание входа DCD
При записи значения «true», на цифровых выходах DTR или RTS устанавливается логический 0 (нулевое значение напряжения). При записи «false», устанавливается логическая единица (3.3В или 5В, в зависимости от схемы питания микросхемы).
Аналогично при считывании цифровых входов CTS, DSR или DCD: если возвращается «true», значит, на входе логический 0. Если возвращается «false», значит, на вывод подано напряжение выше порога срабатывания.
При включении питания всех тестовых микросхем, на выходах DTR и RTS устанавливается высокий уровень напряжения. Это означает, что в них по умолчанию пишется «false». Однако в каких-то других микросхемах USB-UART инициализация может быть другая. Кроме того, время инициализации тоже может отличаться, поэтому сразу после включения питания какое-то время на выводах DTR и RTS могут быть нули, которые затем переключаются в высокий уровень.
Скорость работы цифровых выводов
К сожалению, дополнительные выводы UART работают очень медленно, если сравнивать их со скоростью передачи данных через основные выводы TX и RX.
Для оценки времени работы функций я создал программу с кнопкой на форме и прописал такой код на событие нажатия:
private void button1_Click(object sender, EventArgs e)
{
//Переключение пинов
for (int i = 0; i < 10; i++)
{
MyPort.DtrEnable = !MyPort.DtrEnable;
}
}
Этот код 10 раз инвертирует значение на выводе DTR. На осциллографе я получил следующую картину для микросхемы CP2102:
Как видно, длительность моментов переключения меняется, причём это происходит рандомно от запуска к запуску. Для выхода RTS картинка аналогичная, но средние значения времени работы функции вывода сильно отличаются для разных микросхем: для CP2102 оно составляет 12-20 мс, для FTDI 6-8 мс, а для CH340G – 2.5-3.5 мс.
Это значения, измеренные с помощью осциллографа. Но я также попробовал оценить время работы функции записи, используя системный таймер компьютера. Ниже код, измеряющий время работы для 1000 вызовов функции записи в порт DTR (аналогично было для RTS). Результаты записываются в текстовый файл для дальнейшего анализа.
Тестовый код для вывода в DTR
Скрытый текст
//Считывает время системного таймера (1 Tick = 100 ns)
Int64 GetTicks()
{
return DateTime.Now.Ticks;
}
//Обработка нажатия на кнопку запуска теста
private void button2_Click(object sender, EventArgs e)
{
int n = 1000;//Количество циклов опроса
string textDelta = "";//Текст для сохранения значений
for (int i = 0; i <= n; i++)
{
bool b = (i % 2 == 0);//Значение для записи в порт
Int64 start_time = GetTicks();//Запоминаем начальное время
MyPort.DtrEnable = b;//Запись в DTR
Int64 deltaTime = GetTicks() - start_time;//Вычисляем время работы
//Строим текстовую таблицу c микросекундами:
textDelta += ((double)deltaTime * 0.1).ToString("F01") + "\n";
}
//Сохраняем данные
File.WriteAllText(@"z:\WriteTimes.txt", textDelta);
}
Update: В комментариях подсказали, что вместо DateTime.Now.Ticks лучше использовать класс Stopwatch для повышения точности. Но с ним результаты примерно те же, так как функции обращения к выводам COM-порта реально медленные.
Полученные таблицы со временем работы функций оказались немного странными. Поэтому я представил результаты некоторых измерений в виде гистограмм с 20 интервалами. Снизу шкала с миллисекундами (интервалы), а по оси Y – количество попаданий в соответствующий интервал.
Гистограммы для 1000 измерений
Скрытый текст
Как видно, затрачиваемое время в основном приближено к целым миллисекундам, хотя значения младших разрядов всегда рандомные. То есть, например, для CP2102 время в основном либо около 10 мс, либо 12 мс, либо 14 мс, а остальные интервалы более редкие.
Вероятно, причина таких разбросов в разной работе драйверов для этих микросхем в системе Windows. Наверняка есть привязка к программному таймеру Windows, который работает нестабильно. Возможно, где-то в драйвере используется функция Sleep(1) при ожидании ответа от микросхемы. Это объясняет тот факт, что при выполнении программы она не загружает процессор.
Не думаю, что время будет сильно меняться от компьютера к компьютеру, но вот в других операционных системах значения могут быть совсем другими. Также время может быть другим, если использовать какие-то низкоуровневые функции Windows для работы с портами.
Для оценки скорости считывания выводов я также использовал системный таймер. Ниже код для чтения входа CTS (для DSR и DCD будет аналогично).
Тестовый код для считывания CTS
Скрытый текст
private void button3_Click(object sender, EventArgs e)
{
int n = 10;//Количество циклов опроса
string textDelta = "";
string textValue = "";
for (int i = 0; i <= n; i++)
{
Int64 start_time = GetTicks();//Запоминаем начальное время
bool pinValue = MyPort.CtsHolding;//Считываем значение на входе
Int64 deltaTime = GetTicks() - start_time;//Вычисляем время работы
//Строим текстовую таблицу c микросекундами:
textDelta += ((double)deltaTime * 0.1).ToString("F01") + "\n";
//Строим таблицу с входными значениями:
if (pinValue)
{
textValue += "1\n";
}
else
{
textValue += "0\n";
}
}
//Сохраняем данные
File.WriteAllText(@"z:\ReadTimes.txt", textDelta);
File.WriteAllText(@"z:\Values.txt", textValue);
}
Тут всё аналогично в плане разброса: в основном время занимает миллисекунды, но иногда есть промежуточные значения. Однако в среднем для считывания требуется меньше времени. У меня получились такие средние значения: для CP2102 и CH340G оно составляет 2-3 мс, а для FTDI 0.001-1 мс (всех быстрее). И для других входов эти значения очень похожи.
Конечно, такая скорость работы очень плохая. Но для каких-то задач это может пригодиться.
Не забываем про безопасность
Ниже будут показаны разные обобщающие схемы для понимания основной идеи. Но при создании реального устройства всегда надо помнить о рисках работы с высокими напряжениями и токами. Если вы решите добавить в своё устройство дополнительный блок питания для включения какой-то нагрузки, то стоит также добавить гальваническую развязку, чтобы случайно не сжечь USB порт компьютера или целиком компьютер. Например, можно использовать оптроны, твёрдотельные реле или маломощные механические реле.
Также помните о том, что по умолчанию USB порт ограничивает ток потребления до 0.1А и превышение этого порога может вызвать отключение порта.
И ещё важная деталь: готовые модули USB-UART могут быть настроены как на работу с логикой 5В, так и 3.3В. В дальнейших примерах я везде использовал вариант 5В, но схемы должны работать и с логикой 3.3В.
Мигаем светодиодами
Маломощные светодиоды можно подключить прямо к выводам DTR и RTS, а при необходимости включить что-то помощнее, можно использовать транзисторы.
Думаю, тут не нужно объяснять, как включать и отключать светодиоды. Из предыдущих примеров должно быть всё понятно.
Индикаторы в виде светодиодов могут пригодиться для отображения статусов каких-то длительных процессов на компьютере. На самом деле, это очень удобно: можно отдельно запустить процесс, наблюдающий за другим процессом и включающий светодиоды в соответствии с режимами работы. Можно даже делать такую экзотическую отладку программ: оценивать работу своей программы по светодиодам гораздо интереснее, чем через отладочную консоль.
А если нужно сделать что-то типа светофора, то можно добавить микросхему двоичного дешифратора К561ИД1 (аналог CD4028B). Такая схема даст возможность отображать статус в виде свечения одного из 4 разных светодиодов (например, синий, зелёный, жёлтый, красный).
Схема с дешифратором
Скрытый текст
Правда из-за инерционности выводов DTR и RTS при их переключениях иногда будут помигивать не те светодиоды.
Вывод TX можно дополнить генератором моноимпульса на микросхеме NE555, чтобы мигать ещё одним светодиодом. То есть, можно отправлять в порт нулевой байт, чтобы зажечь светодиод на некоторое время. Так можно показывать, что регулярно выполняются какие-то действия.
Вот функция, которая выводит на TX нулевой байт, работающий как синхроимпульс:
//Выводит импульс на выход TX
void WriteTxClock()
{
MyPort.Write(new byte[1] { 0 }, 0, 1);
}
Схема генератора моноимпульса на 555
Скрытый текст
Считываем кнопки
Раз у нас есть цифровые входы CTS, DSR и DCD, то к ним можно подключить кнопки. К ним надо добавить подтягивающие резисторы и можно ещё конденсаторы для сглаживания дребезга контактов.
Схема подключения кнопок
Скрытый текст
В данной схеме кнопки замыкают вывод на землю, поэтому в программе при нажатии кнопки функция считывания будет возвращать значение «true».
Для считывания кнопок в программу C# можно добавить компонент таймера System.Windows.Forms.Timer, задать ему время срабатывания 1-10 мс, а в обработке события срабатывания таймера можно считывать значения выводов CTS, DSR и DCD. Анализируя считанные значения, можно выполнять какие-то действия, например, запускать какие-то программы. Так можно сделать примитивные кнопки быстрого запуска программ.
Помимо обычных кнопок можно использовать кнопку-педаль, лазерный барьер или выход с термореле. Можно даже подключить цифровые входы к выходу какого-то генератора, чтобы считывать рандомные биты, если вам нужен простейший генератор произвольных чисел. Идей можно придумать много.
Делаем больше выходов
Если вам мало цифровых выходов, то можно добавить микросхемы последовательно-параллельных регистров 74HC595. На вход данных такого регистра можно назначить вывод DTR, на «защёлку» данных (вход LD у регистра) – вывод RTS, а в качестве импульсов синхронизации можно использовать нулевые байты данных из последовательного порта, то есть, с вывода TX. Только нужно инвертировать этот сигнал, например, с помощью транзистора.
Схема подключения регистра 74HC595
Скрытый текст
Причём можно последовательно подключить много регистров 74HC595. Каждая микросхема даст 8 цифровых выходов.
Правда скорость вывода данных в эти регистры будет небольшой. Если вызывать функцию записи в DTR только при смене значения бита, то быстрее всего будут выводиться байты 0x00 и 0xFF, а всех дольше – 0x55 и 0xAA (0b01010101 и 0b10101010).
Код для записи байта в регистр 74HC595
Скрытый текст
void WriteTxClock()
{
MyPort.Write(new byte[1] { 0 }, 0, 1);
}
void WriteDtr(bool data)
{
MyPort.DtrEnable = !data;
}
void WriteRts(bool data)
{
MyPort.RtsEnable = !data;
}
bool[] ConvertByteToBoolArray(byte data)
{
bool[] result = new bool[8];
for (int i = 0; i < 8; i++)
{
if ((data & (1 << i)) != 0)
{
result[i] = true;
} else
{
result[i] = false;
}
}
return result;
}
//Вывод байта в регистр 74HC595
void WriteToSerialReg(byte data)
{
bool[] bits = ConvertByteToBoolArray(data);
bool data_bit = false;
for (int i = 0; i < 8; i++)
{
if ((i == 0) | (data_bit != bits[i]))
{
WriteDtr(bits[i]);
}
data_bit = bits[i];
WriteTxClock();
}
//Load clock
WriteRts(true);
WriteRts(false);
}
Как и раньше, используя системный таймер компьютера, я оценил время работы функций. Для микросхемы CP2102 я получил среднее время записи байта (в зависимости от его значения) от 39 мс до 130 мс, для FT232 – от 20 мс до 69 мс, а для микросхемы CH340 – от 8 мс до 30 мс.
Делаем больше входов
Есть и другой тип регистров: параллельно-последовательный регистр – микросхема 74HC165. У него 8 входов, значения которых можно считывать последовательно. Такие регистры можно использовать для увеличения количества входов.
Мы можем назначить вывод RTS на «защёлку» данных, а для синхронизации также использовать вывод TX (это будет быстрее, чем формировать импульсы через DTR). Считывать данные можно любым входом, например, через CTS. Только нужно иметь ввиду, что у регистра 74HC165 вход LD («защёлка» данных) с инверсией: «защёлкивание» данных будет происходить при переходе RTS из 1 в 0.
Схема подключения регистра 74HC165
Скрытый текст
Код для считывания байта из 74HC165
Скрытый текст
bool ReadCts()
{
return !MyPort.CtsHolding;
}
byte BoolArrayToByte(bool[] bits)
{
byte result = 0;
for (int i = 0; i < 8; i++)
{
if (bits[i])
{
result |= (byte)(1 << i);
}
}
return result;
}
byte ReadSerialReg()
{
bool[] bits = new bool[8];
//Load clock
WriteRts(false);
WriteRts(true);
for (int i = 0; i < 8; i++)
{
bits[i] = ReadCts();
WriteTxClock();
}
return BoolArrayToByte(bits);
}
Скорость считывания данных из регистра будет выше, чем при выводе данных, потому что, как мы выяснили ранее, чтение CTS занимает меньше времени. Для микросхемы CP2102 я получил среднее время считывания байта 45 мс, для FT232 – 14 мс, а для CH340 – 24 мс.
Аналогично, можно подключить сразу несколько микросхем 74HC165. Но раз у нас есть несколько свободных цифровых входов, то для увеличения количества регистров их можно ставить не последовательно, а параллельно, чтобы сэкономить время на количестве синхроимпульсов (хотя это будет очень небольшая экономия времени).
Больше входов и выходов
Если нужно одновременно расширить входы и выходы, то можно использовать оба типа регистров. Например, можно сделать 8 входов и 8 выходов, используя одновременно 74HC165 и 74HC595.
Схема подключения 74HC165 и 74HC595
Скрытый текст
Правда тут для считывания входов из 74HC165 нужно обязательно сначала загружать данные в 74HC595. То есть, нельзя считать входы без обновления выходов, поэтому в любом случае нужно будет тратить двойное время.
Код работы с 74HC595 и 74HC165
Скрытый текст
byte WriteAndReadSerialReg(byte data)
{
WriteToSerialReg(data);
bool[] bits = new bool[8];
for (int i = 0; i < 8; i++)
{
bits[i] = ReadCts();
WriteTxClock();
}
return BoolArrayToByte(bits);
}
Некоторые функции были показаны выше
Для микросхемы CP2102 я получил среднее время записи и чтения от 60 мс до 152 мс (в зависимости от выводимого байта), для FT232 – от 20 мс до 69 мс (тут считывание очень быстрое), а для CH340 – от 33 мс до 59 мс.
Как это выглядело в реальности
Скрытый текст
Делаем интерфейс I2C
Существует достаточно много устройств, которыми можно управлять через интерфейс I2C. Обычно такие устройства не требовательны к скорости обмена данными. Поэтому, имея два цифровых выхода, можно легко организовать такой интерфейс, просто добавив к выходам по транзистору с подтягивающими резисторами. При необходимости считывать данные, можно задействовать вход CTS.
Схема организации интерфейса I2C
Скрытый текст
Чтобы проверить работу нашего супер тормозного медленного I2C, я решил подключить к нему модуль семисегментного индикатора на 6 разрядов с драйвером TM1637.
Такие дисплеи продаются на алиэкспрессе, но часто там на линиях SDA и SCL стоят слишком большие конденсаторы, сглаживающие сигналы. Так что если дисплей не работает, найдите и удалите эти конденсаторы. Про эту неисправность я рассказывал в своём видео.
Для вывода данных на этот индикатор, нам нужно создать низкоуровневые функции протокола I2C, а также функцию установки яркости и вывода информации в дисплей.
Код для вывода на индикатор с драйвером TM1637
Скрытый текст
void I2C_SetClockLow()
{
MyPort.RtsEnable = false;
}
void I2C_SetClockHigh()
{
MyPort.RtsEnable = true;
}
void I2C_SetDataLow()
{
MyPort.DtrEnable = false;
}
void I2C_SetDataHigh()
{
MyPort.DtrEnable = true;
}
void I2C_Start()
{
I2C_SetClockHigh();
I2C_SetDataHigh();
I2C_SetDataLow();
}
void I2C_Stop()
{
I2C_SetClockLow();
I2C_SetDataLow();
I2C_SetClockHigh();
I2C_SetDataHigh();
}
bool I2C_ReadAck()
{
I2C_SetClockLow();
I2C_SetDataHigh();
bool ack = ReadCts();
I2C_SetClockHigh();
I2C_SetClockLow();
return ack;
}
void I2C_WriteByte(byte data)
{
bool[] bits = ConvertByteToBoolArray(data);
bool data_bit = false;
for (int i = 0; i < 8; i++)
{
I2C_SetClockLow();
if ((i == 0) | (data_bit != bits[i]))
{
if (bits[i])
{
I2C_SetDataHigh();
}
else
{
I2C_SetDataLow();
}
}
data_bit = bits[i];
I2C_SetClockHigh();
}
}
//Convert number to 7-segment code
byte NumberToSegments(int n)
{
if (n == 0) return 0x3F;//0
if (n == 1) return 0x06;//1
if (n == 2) return 0x5B;//2
if (n == 3) return 0x4F;//3
if (n == 4) return 0x66;//4
if (n == 5) return 0x6D;//5
if (n == 6) return 0x7D;//6
if (n == 7) return 0x07;//7
if (n == 8) return 0x7F;//8
if (n == 9) return 0x6F;//9
if (n == 10) return 0x77;//A
if (n == 11) return 0x7C;//B
if (n == 12) return 0x39;//C
if (n == 13) return 0x5E;//D
if (n == 14) return 0x79;//E
if (n == 15) return 0x71;//F
if (n == 16) return 0x40;//-
if (n == 17) return 0x77;//A
if (n == 18) return 0x3D;//G
if (n == 19) return 0x76;//H
if (n == 20) return 0x3C;//J
if (n == 21) return 0x73;//P
if (n == 22) return 0x38;//L
if (n == 23) return 0x6D;//S
if (n == 24) return 0x3E;//U
if (n == 25) return 0x6E;//Y
return 0x00;
}
//Send segments data into display
//d0 - low, d5 - high
void DisplayUpdate(byte d0, byte d1, byte d2, byte d3, byte d4, byte d5)
{
I2C_Start();
I2C_WriteByte(0x40);//Memory write command
I2C_ReadAck();
I2C_Stop();
I2C_Start();
I2C_WriteByte(0xc0);//Start address
I2C_ReadAck();
I2C_WriteByte(d3);
I2C_ReadAck();
I2C_WriteByte(d4);
I2C_ReadAck();
I2C_WriteByte(d5);
I2C_ReadAck();
I2C_WriteByte(d0);
I2C_ReadAck();
I2C_WriteByte(d1);
I2C_ReadAck();
I2C_WriteByte(d2);
I2C_ReadAck();
I2C_Stop();
}
// Brightness values: 0 - 8
void SetBrightness(byte brightness)
{
I2C_Start();
brightness += 0x87;
I2C_WriteByte(brightness);
I2C_ReadAck();
I2C_Stop();
}
//Send number into display
void DisplaySendNumber(int num)
{
byte dg0, dg1, dg2, dg3, dg4, dg5;
dg0 = NumberToSegments((byte)(num / 100000));
num = num % 100000;
dg1 = NumberToSegments((byte)(num / 10000));
num = num % 10000;
dg2 = NumberToSegments((byte)(num / 1000));
num = num % 1000;
dg3 = NumberToSegments((byte)(num / 100));
num = num % 100;
dg4 = NumberToSegments((byte)(num / 10));
num = num % 10;
dg5 = NumberToSegments((byte)num);
DisplayUpdate(dg5, dg4, dg3, dg2, dg1, dg0);
}
Всё прекрасно заработало, если не считать большую задержку обновления дисплея. Вызов функции SetBrightness для CP2102 занимает в среднем 348 мс, для FT232 – 193 мс, а для CH340 – 91 мс.
Функция DisplayUpdate для CP2102 требует от 2.1 с (когда все байты 0xFF или 0x00) до 2.6 с (когда все байты 0xAA). Для FT232 аналогичные задачи требуют от 1.2 с до 1.5 с, а для CH340 – от 545 мс до 725 мс.
Напомню, что это время нестабильное и отдельные вызовы могут занимать на 20-30% больше или меньше времени от указанного. Впрочем, с другими микросхемами или драйверами это время может быть кардинально другим.
Фото рабочей схемы
Скрытый текст
Обобщение
Как видим, дополнительные выводы преобразователя USB-UART вполне могут быть полезными для каких-то задач, хотя и с большими ограничениями. Уверен, можно значительно расширить функциональность этих устройств, если добавить больше логических микросхем, но в современном мире будет проще приделать к порту какой-то микроконтроллер. Иногда будет даже проще подключиться напрямую к USB, если микроконтроллер имеет такой интерфейс.
Кстати, если выбрать порт COM1, то скорость работы функций значительно выше. Но я не проверял этот порт с реальной схемой, так как он запрятан где-то внутри компьютера.
Весь упомянутый код для тестов схем можно скачать в архиве с проектом Visual Studio с моего сайта.
Пишите в комментариях, какие вы придумали идеи по использованию этих дополнительных выводов USB-UART. Может быть, мы вместе изобретём какой-то новенький «велосипед»?
Update: Подписчик в Телеграм прислал свой экспериментальный код на Си для подключения LCD1602 через дополнительные выводы COM-порта: ссылка на GitHub.
Зачем это всё нужно было мне?
На самом деле, при изучении данной темы у меня была конкретная цель: я хотел сделать простой программатор для STM32 на базе преобразователя USB-UART, в котором выводы DTR и RTS используются для автоматического сброса микроконтроллера и активации встроенного заводского загрузчика программ. Мне нужно было понять, какие есть ограничения при работе с данными выходами. А раз я собрал информацию, то заодно решил написать про это статью.
Кстати, в какой-то степени я реализовал такой программатор: он называется NyamFlashLoader. Некоторые микроконтроллеры (в основном старые модели) он программирует. При этом я хотел сделать универсальный программатор, но, как позже выяснилось, у разных микроконтроллеров активация встроенного загрузчика происходит по-разному, а я реализовал лишь один вариант. Поэтому, на данный момент, этот проект приостановлен.
Всем спасибо за внимание!
Комментарии (41)
nikolz
11.09.2024 13:05+3А вот для микросхемы CH340 распространены только модули с основными выводами RX и TX, без дополнительных
вот здесь есть DTR
на этом есть RTS,CTS
на этом есть RTS,CTS,DTR
nanoslavic Автор
11.09.2024 13:05+1Спасибо. Я эти модули видел, но как-то не рассмотрел на них дополнительные выводы.
nanoslavic Автор
11.09.2024 13:05+1Спасибо. Я эти модули видел, но как-то не рассмотрел на них дополнительные выводы.
Jury_78
11.09.2024 13:05+2Вроде в мс FTDI есть режим Bit-banging, не проще ли через это действать?
nanoslavic Автор
11.09.2024 13:05+1Не уверен, что проще, вроде бы это делается через SDK от FTDI, а не через функции COM-порта. То есть, надо будет что-то ставить и изучать другие функции. Да и решение будет не универсальным. Хотя, конечно, так было бы больше возможностей. Но так то и дешёвый микроконтроллер поставить на UART не так уж сложно:)
Jury_78
11.09.2024 13:05+2Для ftdi вариантов не мало, например библиотека и на python.
NutsUnderline
11.09.2024 13:05+1А еще можно зайти на сайт производителя и найти библиотеки даже под visualBasic. там есть уже непросто ногодрыг, а сразу еще реализованы интерфейсы i2C/SPI/jtag. Что и используют производители огромного числа программаторов/отладчиков на базе микросхемы FT2232H для огромного числа микросхем.
Аналогичные штуки есть и в серии usb интерфейсов CP и CH.
конечно придется чему то подучиться но и работает это все заметно быстрее.
VirtualVoid
11.09.2024 13:05+3Оригинальную FTDI, где SN не A50285BI, ещё нужно постараться найти.
Для ногодрыга бы пожалуй подошла CH341, которая известна всем по одноимённому программатору.Jury_78
11.09.2024 13:05Как у автора FT232 у меня вполне работает.
nanoslavic Автор
11.09.2024 13:05У меня есть два модуля FT232 с аликспресса и оба работают некоторое время, потом подвисают на полсекунды и заново начинают работать. Причём модули куплены в разное время. Для статьи я нашёл совсем другой модуль FT232, купленный очень давно, наверное лет 5 назад:) Он хорошо работает.
Jury_78
11.09.2024 13:05Я пару лет назад покупал 2 шт., использовал из python. Если и была задержка то я ее не заметил. :) Опять же это работает через USB там свои задержи могут быть.
Astroscope
11.09.2024 13:05+4А вы знали про возможности этих дополнительных выводов USB-UART?
Да, использую их вместо отсутствующих в подавляющем большинстве компьютеров GPIO. Мой основной кейс - управление любительской радиостанцией, переключение с приема на передачу, чтение статуса шумоподавителя и тому подобные нехитрые действия, которые хорошо поддерживаются соответствующим софтом и работают по сути везде, где есть USB порты. Да, более продвинутые радиостанции принимают расширенные команды по UART (COM) и отвечают на них - позволяют устанавливать практически все параметры и отчитываться об их статусе, начиная с очевидной установки частоты и вида модуляции, но и с ними переключение прием/передача лучше осуществлять примитивным, быстрым и надежным сигналом на RTS или DTR, а не ждать, пока управляющий софт сформирует и отправит команду, а радиостанция ее распарсит и выполнит - просто сменить уровень быстрее и помехозащищеннее. В результате все это делается на одном USB-UART, проще не придумаешь.
Кому такого импровизированного GPIO мало, подумайте об USB-LPT адаптере. :)
nanoslavic Автор
11.09.2024 13:05+3Ничего не понятно, но выглядит круто:) Пока читал ваш комментарий, пришла идея: можно через DTR и RTS управлять электронными ключами (типа DG408 или 74HC4066), чтобы "нажимать" отдельные кнопки на плате от клавиатуры или других устройств. То есть, получается программное нажатие, но распознаваться будет как реальное от пользователя.
Guestishe
11.09.2024 13:05+1А что если для этого использовать лампочки с клавиатуры. И usb дополнительный занимать не надо.
nanoslavic Автор
11.09.2024 13:05Идея, конечно, гениальная:) Правда светодиоды CapsLock и NumLock вполне полезны. И ещё они включаются, когда нажимаешь на соответствующие кнопки.
Но можно добавить переключатель, который активирует эту скрытую функцию лампочек на клавиатуре:)
Flammmable
11.09.2024 13:05CH340 бывают очень разные. У CH340N, например, всего 8 выводов, 6 из которых это Vcc, GND, Tx, Rx, D+, D-.
nanoslavic Автор
11.09.2024 13:05+1Да, эта миниатюрная версия мне очень нравится, она прям создана для каких-то портативных устройств.
Flammmable
11.09.2024 13:05+3В плане миниатюрности CH340N всё же не самый маленький мост :)
Он занимает 5х6мм.
А, например, CP2102N-A02-GQFN20 занимает 3х3мм.Но миниатюризация чипа с определённого момента начинает быть не вполне осмысленной - разъём-то не уменьшить ниже определённого уровня.
Я тут, кстати, накидал небольшой обзор всяких разных мостов, чтобы оценивать, скажем так, тенденции и масштабы.
15432
11.09.2024 13:05Мне больше нравится cp2104, шустрая, стабильная, есть отдельные GPIO с управлением, правда, через их API, можно зашить конфиг с нужным VID/PID и настройками GPIO (правда, только два раза)
LAutour
11.09.2024 13:05Вот-бы еще и версии корпусов у CP-шек были удобные для ручной пайки.
nanoslavic Автор
11.09.2024 13:05Да, мне тоже CP2102 не подходят именно из-за корпуса. Одну кое-как припаял, но дальше перешёл на CH340 :) Хотя для устройств такой миниатюрный корпус это скорее преимущество, но не для самодельщиков:)
strvv
11.09.2024 13:05+2Благодарю!
Очень хорошо структурированная статья, более того, ценная не только кодом и использованием сейчас, после COM-мышей, данных выводов, но и различными примерами аппаратного окружения, в одной статье.
О всех этих микросхемах вроде помнишь, а когда доходит до тела-дела, вечно начинаешь тупить и искать конкретные применения.
Ну а код можно написать любой, благо что при наличии офисных пакетов почти на всех компьютерах, питон и Васик (Оба в MS и в Либре Офисах всегда в наличии) всегда при нас, даже в урезаной офисной машинке ;).
w0lf
11.09.2024 13:05+1Да, способ этот известен с древних времен. В некоторых компьютерах 80-ых, в целях экономии, чтобы не ставить лишние корпуса i8255, использовали редко используемые линии полного RS-232 (типа Ring Indicator) как линии последовательного ввода-вывода.
BarabashkaS
11.09.2024 13:05По поводу
Полученные таблицы со временем работы функций оказались немного странными.
в C# использовать
DateTime.Now
для оценки времени не самая удачная идея. Можно посмотреть, например, здесь - https://habr.com/ru/companies/tbank/articles/454058/ или тут - https://ru.stackoverflow.com/questions/758708/Как-точно-измерить-время-выполнения-операции-Сnanoslavic Автор
11.09.2024 13:05+2Судя по описанию функции
DateTime.Now.Ticks
, она выдаёт таймкод с шагом 100 нс. Это очень даже точно для моих задач:) Я её немного протестировал вместе с осциллографом и вроде бы она вполне точно показывает отсчёты как для измерения времени, так и для организации задержки в цикле.Если бы функции COM-порта работали значительно быстрее, тогда, действительно, лучше было бы использовать специальные методы. Но тут я уже знал, что функции работают медленно (так как видел на осциллографе задержки в миллисекунды), поэтому такого таймера вполне достаточно.
BarabashkaS
11.09.2024 13:05+1"DateTime.Now.Ticks основана на функции WinAPI GetSystemTimeAsFileTime(). Она выражается в сотых наносекунды. Фактическая точность DateTime.Ticks зависит от системы. В Windows XP приращение системных часов составляет около 15,6 мс. В Windows 7 его точность составляет 1 мс. Однако при использовании схемы энергосбережения (обычно на ноутбуках) оно может уменьшиться до 15,6 мс. На Windows 10 и 11 я не в курсе." - взято из 1го ответа отсюда https://stackoverflow.com/questions/243351/environment-tickcount-vs-datetime-now
в комментариях кто-то добавил что в windows 10 точность у него составила чуть более 1мс
P.S Замерил у себя на работе (на windows 10, core i3-10100, .Net 8). Разница двух подряд идущих считываний тиков (в цикле считывал два раза подряд тики и выводил на экран разницу) давало от 0,1мс до 4мс с лишним, иногда выбросы были больше ( у меня было 18 мс)
nanoslavic Автор
11.09.2024 13:05Да, похоже, вы правы, хотя у меня код:
Int64 start_time1 = DateTime.Now.Ticks;
Int64 start_time2 = DateTime.Now.Ticks;
label1.Text = (start_time2 - start_time1).ToString();
Всегда показывает ноль:) Но если добавить какой-то большой цикл с делением между считываниями, то, действительно, время почти всегда кратно 1 мс, хоть и не ровно.
Попробовал такой вариант:
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
bool b = MyPort.CtsHolding;
stopWatch.Stop();
label1.Text = ((double)stopWatch.ElapsedTicks / (double)Stopwatch.Frequency).ToString();
label2.Text = b.ToString();
Вроде бы это должно показывать время в секундах. Как результат - примерно то же самое: в среднем функция выполняется 2 мс для CP2102. То есть, вы правы насчёт точности
DateTime.Now.Ticks
, но в итоге результаты те же и соStopwatch();
Попробовал и другие функции - тоже результат совпадает и так же значения постоянно прыгают с шагом примерно в мс.То есть, функции реально медленные и привязаны к мс, поэтому особенно ничего не изменилось при переходе на более точный метод. Или я что-то не так сделал и тут:)
BarabashkaS
11.09.2024 13:05попробуйте из примера отсюда - https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.stopwatch.frequency?view=net-8.0#system-diagnostics-stopwatch-frequency - вычислить разрешение вашего таймера, если оно меньше микросекунды, таково свойство CP2102, если больше - значит измерения нужно делать на другом компьютере
гляньте еще тут - https://ru.stackoverflow.com/questions/758708/Как-точно-измерить-время-выполнения-операции-С - чтобы понять проблематику
P.S Ну и C# не очень хороший инструмент для коротких измерений, слишком многое нужно учитывать
nanoslavic Автор
11.09.2024 13:05+1Да, спасибо, вроде я всё это уже изучил и понял проблему. Свойство Stopwatch.Frequency возвращает true, значит, у меня таймер высокого разрешения. И вышеприведённый код я уже проверил на измерении времени выполнения расчётов в цикле - он показывает микросекунды. Например, код
Stopwatch stopWatch = new Stopwatch();
double f = 0;
stopWatch.Start();
for (int i = 0; i < 10000; i++)
{
f += (double)i / 3;
}
stopWatch.Stop();
label1.Text = ((double)stopWatch.ElapsedTicks / (double)Stopwatch.Frequency).ToString();
label2.Text = f.ToString();всегда выдаёт примерно 5E-05 . То есть, это 50 мкс. Если менять количество циклов, то время тоже меняется через микросекунды, а не миллисекунды, как с функциями COM-порта. То есть, это реально более точный способ, но именно для задачи в статье ничего особо не изменилось.
mpa4b
11.09.2024 13:05RTS/CTS не "когда-то давно", а в любой момент могут быть использованы для приостановки потока по uart'у, если вдруг имбеднутый девайс не вывозит.
DTR можно заводить (через джампер есс-но или нормально невпаянный резистор) на сброс девайса, тогда распальцовка в терминалке позволяет удобно девайс сбрасывать при отладке.nanoslavic Автор
11.09.2024 13:05Вы имеете ввиду, что микросхемы USB-UART автоматически перестанут передавать данные, если на CTS будет 0? Что-то мне кажется, это не работает, иначе нужно было бы обязательно подтягивать эти входы к питанию. Если это самому можно отслеживать в своей передающей программе, тогда это неплохой вариант использования. Правда можно просто пореже передавать данные, чтобы не переполнять буфер устройства.
Насчёт ресета девайса: возможно, можно придумать какую-то хитрую логическую схему, чтобы устройство не сбрасывалось случайно при подключении и отключении USB. Например, можно выводить через DTR и RTS специальные комбинации для ресета устройства и активации бутлоадера при необходимости обновить прошивку. Я это продумывал для программатора STM32, но это достаточно сильное усложнение и проще заставить пользователя переключателем выбирать режим "работа" или "прошивка". Хотя для каких-то проектов это может быть актуально.
mpa4b
11.09.2024 13:05+1Конечно это работает, стоит только в настройках сериального порта разрешить 'hardware flow control'
Mike-M
11.09.2024 13:05Есть предположение, что реализация bit-bang на Си покажет меньшие задержки.
nanoslavic Автор
11.09.2024 13:05Скорее всего намного быстрее, но это же будет не универсальное решение. То есть, такой режим используется только в API драйвера, который у каждой фирмы свой. Тогда программу надо затачивать на конкретную микросхему. В этом плане лучше будет использовать специализированные версии типа CP2104 и CH341, у которых есть дополнительные GPIO. Статья же про стандартный COM порт и его выводы:)
LAutour
Модули с фейковыми CH340 тоже бывают. Под маркировкой CH340G абсолюно левая по прозвонке выводов микра и бутафрский кварц, никуда не идущий. Работают подделки нормально (система видит как CH340), вот только кроме RX, TX больше ничего нет.
nanoslavic Автор
Да, возможно, и на CP2102 есть подделки. Китайцы очень умелые в этом деле:)