Некоторые задачи требуют подключения электронного устройства к персональному компьютеру. Самый простой и распространенный способ это сделать — использовать переходник USB-COM, или микросхему-мост USB-UART. Однако, на стороне ПК нам понадобится какая-то программа, с помощью которой мы будем управлять нашим девайсом.
Самое простое решение — использовать что-то готовое, например, терминальную программу PuTTY. Через нее в режиме командной строки можно отправлять команды в наше устройство и получать из него ответ:
Но в некоторых случаях недостаточно простой командной строки, нужен GUI (графический пользовательский интерфейс). Олдскульные парни для этих целей используют что-то типа Borland Delphi или Borland C++ Builder. Но нам в универе преподавали C#, поэтому я его и использую (к тому же я не перевариваю паскаль-подобный синтаксис, только c-like languages)) ). К тому же у C# есть удобный класс работы с последовательным портом, и ни какие костыли нам не понадобятся!
Недавно на работе поставили мне задачу, сделать переходник USB-UART к одному хитрому девайсу и написать для него прогу управления. Сам переходник описывать не буду, там ничего интересного нет, UART да двухполярное питание +-12 вольт. А вот на протоколе обмена немного остановлюсь.
Документация естественно на девайс написана через всем известное место, разбираться пришлось с помощью исходников прошивки на него.
;--------------------------------------------------------------------------- ; ; ; SDU ATMEL ATtiny2313 ;--------------------------------------------------------------------------- ;Программа контроллера детектора SDU-3C ФЭУ R6094 ;команды: ;--------------------------------------------------------------------------- ;Напряжение питаня ФЭУ ;VxxCR ;VxxCRLFOK.CRLF> ;80<xx<FF ;--------------------------------------------------------------------------- ;Усиление тракта ;GxxCR ;GxxCRLFOK.CRLF> ;01<xx<0F ;--------------------------------------------------------------------------- ;Верхний уровень дискриминатора ;HxxCR ;HxxCRLFOK.CRLF> ;07<xx<FA ;--------------------------------------------------------------------------- ;Нижний уровень дискриминатора ;LxxCR ;LxxCRLFOK.CRLF> ;07<xx<FA ;---------------------------------------------------------------------------; ;Окно сканирования (амплитудный спектр) ;Устанавливает Нижний уровень дискриминатора=0, Верхний=xx ;OxxCR ;OxxCRLFOK.CRLF> ;01<xx<32 ;--------------------------------------------------------------------------- ;Шаг сканирования (амплитудный спектр) ;Инкремент нижнего и верхнего порога дискриминатора ;возвращает значение нижнего порога ;+ ;CRLFB=xxCRLF> ;01<xx<FF - Нижний уровень дискриминатора ;--------------------------------------------------------------------------- ;Чтение параметров из EEROM ;RCR ;RCRLFOK.CRLF> ;--------------------------------------------------------------------------- ;Запись параметров в EEROM ;WCR ;WCRLFOK.CRLF> ;--------------------------------------------------------------------------- ;Состояние ;Возвращает значения параметров ;SCR ;SCRLFL=xxCRLFH=yyCRLFV=zzCRLFG=vvCRLFO=wwCRLF> ;xx/yy/zz/vv/ww/ - значения параметров ;--------------------------------------------------------------------------- ;Пробел - пустой символ ; ;--------------------------------------------------------------------------- ;Возврат каретки ;CR ;CRLF> ;--------------------------------------------------------------------------- ;--------------------------------------------------------------------------- ;---------------------------------------------------------------------------
В заголовке исходников оказался хорошо расписан весь протокол обмена. В общих чертах он следующий: первый символ — некая буква, которая указывает, какую именно команду мы хотим выполнить. Затем, параметр, который представляет собой шестнадцатеричное число (может отсутствовать) и в конце команды символ ‘\r’. После получения символа ‘\r’ девайс начинает анализировать, что ему за команда пришла. После выполнения всех необходимых действий он выдает ответ, содержащий эхо входной команды, возвращаемое значение и признак конца ответа, который является символом ‘>’. Таким образом, алгоритм обмена прост: отправляем команду, заканчивающуюся на ‘\r’ и читаем ответ до тех пор, пока не получим символ ‘>’. Потом анализируем ответ.
Итак, приступаем. Создадим класс, который будет отвечать за низкоуровневый обмен с устройством и предоставлять удобный программный интерфейс управления.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.IO.Ports; using System.Globalization; namespace SDU3_GUI { SerialPort _serialPort; class SDU3 { public SDU3() { _serialPort = new SerialPort(); } } }
Класс называется SDU3 (так называется сам девайс). С помощью using System.IO.Ports; подключим пространство имен, в котором находится класс работы с COM-портом, _serialPort — наш объект СОМ-порта. У класса SerialPort есть очень удобный метод, с помощью которого можно получить все доступные в системе COM-порты. Это очень удобно, когда мы не знаем номер порта, который система присвоила переходнику USB-COM (или USB-UART, что в принципе то же самое) и нам не придется перебирать все порты от COM1 до COM_овер_дохрена. Чтож, добавим такой метод в наш класс SDU3
public string[] GetPorts() { return SerialPort.GetPortNames(); }
Вызвав GetPorts() мы получим массив строк с именами доступных портов. После этого искать нужный порт методом перебора намного легче.
Следующий метод — открытие порта
public void Open(string port) { //если порт открыть был раньше, //то закрываем его if (_serialPort.IsOpen) _serialPort.Close(); _serialPort.PortName = port; //присваиваем имя порта, например, "COM1" //это тайм-аут на чтение //очень полезная штука //если мы вызвали прочитать строку из порта, //и не получили ответ втечение ReadTimeout миллисекунд, //то получим исключение. //Удобно на случай, если девайс отвалился //и не дает ответ, //мы можем это правильно обратобать //и наша программа не повиснет намертво _serialPort.ReadTimeout = 500; //тайм-аут на запись //насколько я понимаю, актуально только //при аппаратном управлении потоком //ну или вдруг кто-то внезапно выдернул шнур USB //с нашим переходником из компа. //У меня это не используется. _serialPort.WriteTimeout = 500; //Открываем порт. //так как мы не указывали скорость, контроль четности, //количество стоповых битов, то значения будут приняты //"стандартные": 9600, 1 стоп бит, нет контроля четности _serialPort.Open(); }
Более подробно все расписано на сайте мелкософта: https://docs.microsoft.com/ru-ru/dotnet/api/system.io.ports.serialport?view=netframework-4.7.1 или тут https://msdn.microsoft.com/ru-ru/library/system.io.ports.serialport(v=vs.110).aspx . Если вдруг ссылки протухнут, всегда можно воспользоваться гуглом, и по запросу c# serial port можно найти нужную инфу.
Ну и добавим несколько полезных методов, которые просто транслируют запрос далее:
public Boolean IsOpen() { return _serialPort.IsOpen; } public void Close() { _serialPort.Close(); }
Далее, перейдем к самому обмену. Начнем с приема ответа. Согласно протоколу обмена, ответ девайса заканчивается символом ‘>’. То есть, нам надо читать из порта до тех пор, пока не наткнемся на данный символ. В SerialPort как раз для этих целей есть очень удобный метод, который называется .ReadTo(). Таким образом, нам не надо самим анализировать поток данных из порта на предмет наличия онного символа, а просто написать так:
private string sdu3_Read() { return _serialPort.ReadTo(">"); }
Стоит отметить, что сам символ ‘>’ в возвращаемой строке будет отсутствовать. А помните параметр ReadTimeout, который мы указали как 500 на этапе открытия порта? Так вот, если через 500 миллисекунд после вызова _serialPort.ReadTo(«>») девайс не ответит строкой с завершающим символом ‘>’, возникнет исключение, которое можно отловить в нужном месте кода, и вывести ошибку типа «Девайс отвалился!!!! Проверь провода!!!!!11111».
Перейдем теперь к реализации команд обмена. Начнем с команды получения всех настроек устройства. Она имеет вид «S\r». Согласно «документации» ответ мы получим в следующем виде:
S\r\nL=xx\r\nH=yy\r\nV=zz\r\nG=vv\r\nO=ww\r\n> xx/yy/zz/vv/ww/ - значения параметров
Имена настроек обозначены буквами L, H, V, G, O. После буквы идет символ ‘=’, затем значение параметра в шестнадцатеричном значении, затем «\r\n», затем следующая настройка. Причем порядок следования настроек всегда одинаков, то есть на анализ имен настроек можно забить, а парсить только то, что идет после знака ‘=’.
Давайте разберемся, что надо сделать. Для начала разобьем строку ответа на подстроки по разделителю ‘\r’:
S \nL=xx \nH=yy \nV=zz \nG=vv \nO=ww \n>
У нас получилось 7 новых строк. Первую строку пропускаем, в ней нет ничего интересного для нас. Во второй строке находится параметр L. Давайте разобьем ее по разделителю ‘=’:
\nL xx
Получим отдельно ‘\nL’ и значение данного параметра в шестнадцатеричном виде ‘xx’, которое далее можно преобразовать в число и сохранить куда надо.
Проделаем данную операцию и со строками 3, 4, 5, 6. Строка 7 не содержит для нас ничего интересного, выбрасываем. Стоит отметить, что символ ‘>’ будет в ней отсутствовать, так как функция sdu3_Read() его отбросит, но это не особо важно.
Для удобства создадим класс, который будет содержать все перечисленные параметры девайса для удобства работы с ними.
class SDU3_Status { public int Gain; public int HiVoltage; public int UpBound; public int BottBound; public int WinSize; public string StatusToString() { string ret = ""; ret += "Верняя граница: " + Convert.ToString(this.UpBound) + "\r\n"; ret += "Нижняя граница: " + Convert.ToString(this.BottBound) + "\r\n"; ret += "Окно сканирования: " + Convert.ToString(this.WinSize) + "\r\n"; ret += "Напряжение ФЭУ: " + Convert.ToString(this.HiVoltage) + "\r\n"; ret += "Усиление: " + Convert.ToString(this.Gain) + "\r\n"; return ret; } }
Ну и метод StatusToString() добавим, на случай, если мы захотим «красиво» вывести все значения переменных в виде текста. Ну и использовать как напоминалку себе, чтоб не забыть, что какой параметр означает)).
public SDU3_Status GetStatus() { SDU3_Status ret = new SDU3_Status(); _serialPort.DiscardInBuffer(); _serialPort.DiscardOutBuffer(); _serialPort.Write("S\r"); string tmp; tmp = sdu3_Read(); string[] arr_tmp = tmp.Split('\r'); ret.BottBound = int.Parse(arr_tmp[1].Split('=')[1].Trim(), NumberStyles.HexNumber); ret.UpBound = int.Parse(arr_tmp[2].Split('=')[1].Trim(), NumberStyles.HexNumber); ret.HiVoltage = int.Parse(arr_tmp[3].Split('=')[1].Trim(), NumberStyles.HexNumber) * 4; ret.Gain = int.Parse(arr_tmp[4].Split('=')[1].Trim(), NumberStyles.HexNumber); ret.WinSize = int.Parse(arr_tmp[5].Split('=')[1].Trim(), NumberStyles.HexNumber); return ret; }
Вот и сам метод получения всех параметров устройства. С помощью _serialPort.DiscardInBuffer() и _serialPort.DiscardOutBuffer() очищаем входные и выходные буферы порта от возможного мусора в них. Далее, отправляем в порт команду чтения всех параметров и читаем ответ в tmp. Далее, ответ парсим описанным выше способом, засовываем в ret и возвращаем считанные значения.
Это была самая сложная функция, дальше будет легче)).
Рассмотрим функцию, которая называется «Установить верхнюю границу дискриминатора» (не спрашивайте, что это означает)) ) «Lxx\r», где xx — шестнадцатеричное число.
Принимаемые значения лежат в диапазоне 0..255. Вот код функции:
public void SetBottBound(byte val) { string tmp = string.Format("{0:X2}", val); _serialPort.Write("L" + tmp + "\r"); tmp = sdu3_Read(); }
С помощью string.Format(«{0:X2}», val) мы преобразуем восьмибитное число в шестнадцатеричный формат, причем с незначащим нулем и с заглавным буквами. Например, число 13 преобразуется в 0D, а не в d, 0d, D, или 0xD. Далее, отправляем запрос в девайс и ждем ответ. Следует отметить, что нам интересен только сам факт ответа, а не он сам: если что-то вразумительное ответил, значит все норм. Остальные функции однотипны, и на реализации каждой из них останавливаться подробно не буду.
В итоге, у нас получился вот такой класс работы с девайсом:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.IO.Ports; using System.Globalization; namespace SDU3_GUI { class SDU3_Status { public int Gain; public int HiVoltage; public int UpBound; public int BottBound; public int WinSize; public string StatusToString() { string ret = ""; ret += "Верняя граница: " + Convert.ToString(this.UpBound) + "\r\n"; ret += "Нижняя граница: " + Convert.ToString(this.BottBound) + "\r\n"; ret += "Окно сканирования: " + Convert.ToString(this.WinSize) + "\r\n"; ret += "Напряжение ФЭУ: " + Convert.ToString(this.HiVoltage) + "\r\n"; ret += "Усиление: " + Convert.ToString(this.Gain) + "\r\n"; return ret; } } class SDU3 { SerialPort _serialPort; public SDU3() { _serialPort = new SerialPort(); } public string[] GetPorts() { return SerialPort.GetPortNames(); } public void Open(string port) { if (_serialPort.IsOpen) _serialPort.Close(); _serialPort.PortName = port; _serialPort.ReadTimeout = 500; _serialPort.WriteTimeout = 500; _serialPort.Open(); } public Boolean IsOpen() { return _serialPort.IsOpen; } public void Close() { _serialPort.Close(); } public SDU3_Status GetStatus() { SDU3_Status ret = new SDU3_Status(); _serialPort.DiscardInBuffer(); _serialPort.DiscardOutBuffer(); _serialPort.Write("S\r"); string tmp; tmp = sdu3_Read(); string[] arr_tmp = tmp.Split('\r'); ret.BottBound = int.Parse(arr_tmp[1].Split('=')[1].Trim(), NumberStyles.HexNumber); ret.UpBound = int.Parse(arr_tmp[2].Split('=')[1].Trim(), NumberStyles.HexNumber); ret.HiVoltage = int.Parse(arr_tmp[3].Split('=')[1].Trim(), NumberStyles.HexNumber) * 4; ret.Gain = int.Parse(arr_tmp[4].Split('=')[1].Trim(), NumberStyles.HexNumber); ret.WinSize = int.Parse(arr_tmp[5].Split('=')[1].Trim(), NumberStyles.HexNumber); return ret; } private string sdu3_Read() { return _serialPort.ReadTo(">"); } public void SetBottBound(byte val) { string tmp = string.Format("{0:X2}", val); _serialPort.Write("L" + tmp + "\r"); tmp = sdu3_Read(); } public void SetUpBound(byte val) { _serialPort.DiscardInBuffer(); _serialPort.DiscardOutBuffer(); string tmp = string.Format("{0:X2}", val); _serialPort.Write("H" + tmp + "\r"); tmp = sdu3_Read(); } public void SetGain(byte val) { _serialPort.DiscardInBuffer(); _serialPort.DiscardOutBuffer(); string tmp = string.Format("{0:X2}", val); _serialPort.Write("G" + tmp + "\r"); tmp = sdu3_Read(); } public void SetHiVoltage(int val) { _serialPort.DiscardInBuffer(); _serialPort.DiscardOutBuffer(); string tmp = string.Format("{0:X2}", (byte)(val/4)); _serialPort.Write("V" + tmp + "\r"); tmp = sdu3_Read(); } public void SetWinSize(byte val) { _serialPort.DiscardInBuffer(); _serialPort.DiscardOutBuffer(); string tmp = string.Format("{0:X2}", val); _serialPort.Write("O" + tmp + "\r"); tmp = sdu3_Read(); } public void WriteEEPROM() { _serialPort.DiscardInBuffer(); _serialPort.DiscardOutBuffer(); string tmp; _serialPort.Write("W\r"); tmp = sdu3_Read(); } public void ReadEEPROM() { _serialPort.DiscardInBuffer(); _serialPort.DiscardOutBuffer(); string tmp; _serialPort.Write("R\r"); tmp = sdu3_Read(); } } }
Далее, на основе этого класса уже довольно просто нарисовать GUI управления прибором. У меня получилось вот так:
Подробно останавливаться на реализации не буду, так как там все просто: нажали на кнопку — команда сквозняком пошла в наш класс SDU3.
На этом все, спасибо за внимание!))
вышлите пож-та исходники проекта для C# этой статьи
Спасибо за хорошую статью!
Вышлите пожалуйста исходники проекта для C# этой статьи. За ранее благодарен.
Что-то уже 2й раз просят проект))) Вот ссылка https://yadi.sk/d/QQxbGxoycFvdRg
Спасибо за отзыв 🙂
Подскажите в чем вы делали проект(вижуал студио или еще что).
зы с с# для винды не знаком(хочется ваш проект слегка переделать,да и GUI отредактировать)