Некоторые задачи требуют подключения электронного устройства к персональному компьютеру. Самый простой и распространенный способ это сделать — использовать переходник 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 отредактировать)