Сейчас распространено несколько вариантов недорогих микросхем преобразователей 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); }
Полученные таблицы со временем работы функций оказались немного странными. Поэтому я представил результаты некоторых измерений в виде гистограмм с 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. Может быть, мы вместе изобретём какой-то новенький «велосипед»?
Зачем это всё нужно было мне?
На самом деле, при изучении данной темы у меня была конкретная цель: я хотел сделать простой программатор для STM32 на базе преобразователя USB-UART, в котором выводы DTR и RTS используются для автоматического сброса микроконтроллера и активации встроенного заводского загрузчика программ. Мне нужно было понять, какие есть ограничения при работе с данными выходами. А раз я собрал информацию, то заодно решил написать про это статью.
Кстати, в какой-то степени я реализовал такой программатор: он называется NyamFlashLoader. Некоторые микроконтроллеры (в основном старые модели) он программирует. При этом я хотел сделать универсальный программатор, но, как позже выяснилось, у разных микроконтроллеров активация встроенного загрузчика происходит по-разному, а я реализовал лишь один вариант. Поэтому, на данный момент, этот проект приостановлен.
Всем спасибо за внимание!
ссылка на оригинал статьи https://habr.com/ru/articles/842330/
Добавить комментарий