Программирование PC Visual Studio C#: работа с последовательным портом Thu, November 21 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


Visual Studio C#: работа с последовательным портом Печать
Добавил(а) microsin   

Эта статья показывает, как записывать и читать данные от устройства, подключенного к последовательному порту (COM-порт) из приложения на языке C# в среде .NET. Мы будем читать и записывать данные через TextBox на форме, и будем работать с потоками.

serial-port-pinout

В недалеком прошлом для работы с Serial Port в среде .Net 1.1, мы должны были использовать либо Windows API, либо использовать управление из сторонних библиотек. В среде .Net 2.0 (и в более поздних версиях .NET) компания Microsoft добавила поддержку последовательного порта включением класса SerialPort как части пространства имен System.IO.Ports. Реализация класса SerialPort сделана очень прямо и очевидно. Чтобы создать экземпляр класса SerialPort class, просто передайте опции SerialPort конструктору класса:

// Все опции для последовательного устройства
// ---- могут быть отправлены через конструктор класса SerialPort
// ---- PortName = "COM1", Baud Rate = 19200, Parity = None,
// ---- Data Bits = 8, Stop Bits = One, Handshake = None
SerialPort _serialPort = new SerialPort("COM1", 
                                        19200, 
                                        Parity.None,
                                        8,
                                        StopBits.One);
_serialPort.Handshake = Handshake.None;

Для приема данных нам нужно создать обработчик события EventHandler для "SerialDataReceivedEventHandler":

// "sp_DataReceived" является вручную созданным методом (подпрограммой)
_serialPort.DataReceived += new SerialDataReceivedEventHandler(sp_DataReceived);

Вы можете также установить другие опции, такие как ReadTimeout и WriteTimeout (таймауты чтения и записи):

// milliseconds _serialPort.ReadTimeout = 500;
_serialPort.WriteTimeout = 500;

Как только Вы готовы использовать последовательный порт, Вам нужно открыть его:

// Открытие последовательного порта
_serialPort.Open();

Сейчас мы готовы принять данные. Однако чтобы записать эти данные в область ввода TextBox на форме, нам нужно создать так называемого делегата (delegate). Библиотеки .Net не позволяют межпотоковое взаимодействие (cross-thread action), так что нам нужно использовать делегат. Делегат используется для записи в поток пользовательского интерфейса (User Interface, UI) из другого потока (не UI).

// Делегат используется для записи в UI control из потока не-UI
private delegate void SetTextDeleg(string text);

Мы создадим теперь метод "sp_DataReceived", который будет выполнен при поступлении данных в последовательный порт:

void sp_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
   Thread.Sleep(500);
   string data = _serialPort.ReadLine();
   // Привлечение делегата на потоке UI, и отправка данных, которые
   // были приняты привлеченным методом.
   // ---- Метод "si_DataReceived" будет выполнен в потоке UI,
   // который позволит заполнить текстовое поле TextBox.
   this.BeginInvoke(new SetTextDeleg(si_DataReceived), 
                    new object[] { data });
}

Теперь создадим наш метод "si_DataReceived":

private void si_DataReceived(string data)
{
   textBox1.Text = data.Trim();
}

Мы можем теперь принять данные из последовательного порта от устройства и отобразить их на форме. Некоторые устройства отправляют данные сами, без запроса. Однако некоторым устройствам нужно отправить определенные команды, чтобы они ответили на них какими-то своими данными. Для этих устройств Вы будете записывать данные в последовательный порт, и будете использовать предыдущий код, чтобы получить данные обратно. В этом примере будет происходить обмен со шкалой. Для отдельной шкалы отправка команды "SI\r\n" приведет к возврату веса, который имеется на шкале. Эта команда является специфической именно для этого устройства, в Вашем же случае нужно читать документацию по протоколу устройства, чтобы найти команды, принимаемые устройством. Для записи в последовательный порт создайте кнопку "Start" на форме, и добавьте код в событие клика на ней Click_Event:

private void btnStart_Click(object sender, EventArgs e)
{
   // Перед попыткой записи убедимся, что порт открыт.
   try
   {
      if(!(_serialPort.IsOpen))
         _serialPort.Open();
      _serialPort.Write("SI\r\n");
   }
   catch (Exception ex)
   {
      MessageBox.Show("Error opening/writing to serial port :: "
                      + ex.Message, "Error!");
   }
}

Это все, что нужно Вам сделать. См. ссылку [1] для загрузки готового проекта Microsoft Visual C# 2010.

SerialPortCommunication-C-sharp-example

using System;
using System.IO.Ports;
using System.Threading;
 
public class PortChat
{
   static bool _continue;
   static SerialPort _serialPort;
 
   public static void Main()
   {
      string name;
      string message;
      StringComparer stringComparer = StringComparer.OrdinalIgnoreCase;
      Thread readThread = new Thread(Read);
 
      // Создание нового объекта SerialPort с установками по умолчанию.
      _serialPort = new SerialPort();
 
      // Позволяем пользователю установить подходящие свойства.
      _serialPort.PortName = SetPortName(_serialPort.PortName);
      _serialPort.BaudRate = SetPortBaudRate(_serialPort.BaudRate);
      _serialPort.Parity = SetPortParity(_serialPort.Parity);
      _serialPort.DataBits = SetPortDataBits(_serialPort.DataBits);
      _serialPort.StopBits = SetPortStopBits(_serialPort.StopBits);
      _serialPort.Handshake = SetPortHandshake(_serialPort.Handshake);
 
      // Установка таймаутов чтения/записи (read/write timeouts)
      _serialPort.ReadTimeout = 500;
      _serialPort.WriteTimeout = 500;
      _serialPort.Open();
      _continue = true;
      readThread.Start();
 
      Console.Write("Name: ");
      name = Console.ReadLine();
 
      Console.WriteLine("Type QUIT to exit");
 
      while (_continue)
      {
         message = Console.ReadLine();
 
         if (stringComparer.Equals("quit", message))
         {
            _continue = false;
         }
         else
         {
            _serialPort.WriteLine( String.Format("< {0} >: {1}", name, message) );
         }
      }
 
      readThread.Join();
      _serialPort.Close();
   }
 
   public static void Read()
   {
      while (_continue)
      {
         try
         {
            string message = _serialPort.ReadLine();
            Console.WriteLine(message);
         }
         catch (TimeoutException) { }
      }
   }
 
   public static string SetPortName(string defaultPortName)
   {
      string portName;
 
      Console.WriteLine("Available Ports:");
      foreach (string s in SerialPort.GetPortNames())
      {
         Console.WriteLine(" {0}", s);
      }
 
      Console.Write("COM port({0}): ", defaultPortName);
      portName = Console.ReadLine();
 
      if (portName == "")
      {
         portName = defaultPortName;
      }
      return portName;
   }
 
   public static int SetPortBaudRate(int defaultPortBaudRate)
   {
      string baudRate;
 
      Console.Write("Baud Rate({0}): ", defaultPortBaudRate);
      baudRate = Console.ReadLine();
 
      if (baudRate == "")
      {
         baudRate = defaultPortBaudRate.ToString();
      }
 
      return int.Parse(baudRate);
   }
 
   public static Parity SetPortParity(Parity defaultPortParity)
   {
      string parity;
 
      Console.WriteLine("Available Parity options:");
      foreach (string s in Enum.GetNames(typeof(Parity)))
      {
         Console.WriteLine(" {0}", s);
      }
 
      Console.Write("Parity({0}):", defaultPortParity.ToString());
      parity = Console.ReadLine();
 
      if (parity == "")
      {
         parity = defaultPortParity.ToString();
      }
 
      return (Parity)Enum.Parse(typeof(Parity), parity);
   }
 
   public static int SetPortDataBits(int defaultPortDataBits)
   {
      string dataBits;
 
      Console.Write("Data Bits({0}): ", defaultPortDataBits);
      dataBits = Console.ReadLine();
 
      if (dataBits == "")
      {
         dataBits = defaultPortDataBits.ToString();
      }
 
      return int.Parse(dataBits);
   }
 
   public static StopBits SetPortStopBits(StopBits defaultPortStopBits)
   {
      string stopBits;
 
      Console.WriteLine("Available Stop Bits options:");
      foreach (string s in Enum.GetNames(typeof(StopBits)))
      {
         Console.WriteLine(" {0}", s);
      }
 
      Console.Write("Stop Bits({0}):", defaultPortStopBits.ToString());
      stopBits = Console.ReadLine();
 
      if (stopBits == "")
      {
         stopBits = defaultPortStopBits.ToString();
      }
 
      return (StopBits)Enum.Parse(typeof(StopBits), stopBits);
   }
 
   public static Handshake SetPortHandshake(Handshake defaultPortHandshake)
   {
      string handshake;
 
      Console.WriteLine("Available Handshake options:");
      foreach (string s in Enum.GetNames(typeof(Handshake)))
      {
         Console.WriteLine(" {0}", s);
      }
 
      Console.Write("Handshake({0}):", defaultPortHandshake.ToString());
      handshake = Console.ReadLine();
 
      if (handshake == "")
      {
         handshake = defaultPortHandshake.ToString();
      }
 
      return (Handshake)Enum.Parse(typeof(Handshake), handshake);
   }
}

[Как передавать по одному символу, с задержкой]

В случае, когда нужно реализовать обмен с устройством, рассчитанным на взамодействие с пользователем (управляющая консоль). Так как пользователь вводит символы команды медленно, устройство успевает принять все символы и обработать. Если передавать символы быстро (методом SerialPort.Write), по несколько байт, то есть риск потери данных. Во врезке ниже приведен пример класса COMdevice, где реализован метод Write, который передает символы через задержку.

[Как перекодировать символы ANSI в UTF8]

Очень часть устройства на микроконтроллерах передают русские символы в кодировке ANSI (Windows-1251). Однако среда разработки Visual Studio C# хранит и обрабатывает русскоязычный текст в кодировке UTF8, и при попытке отобразить принятый текст (методом ReadExisting) выводятся кракозябры.

Решить проблему можно, если организовать байтовый буфер, и перекодировать массив байт с помощью класса Encoding (методом GetEncoding(1251).GetString). Пример кода в классе COMdevice приведен во врезке ниже.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO.Ports;
using System.Threading;

namespace MyApp
{
   class COMdevice
   {
      private byte[] rxdata;
      int rxidx;
      private SerialPort _serialPort;
      private const int WAIT_ANSWER_TIMEOUT = 500;
      private log logfile;
      public string COM = "";
      public string errtxt = "";

      private void RxReset()
      {
         rxidx = 0;
         rxdata[0] = 0;
      }

      //Конструктор
      public pmini()
      {
         _serialPort = new SerialPort();
         logfile = new log(this.GetType().ToString());
         rxdata = new byte[256];
      }

      private void sp_DataReceived(object sender, SerialDataReceivedEventArgs e)
      {
         SerialPort sp = (SerialPort)sender;
         try
         {
            while (0 != sp.BytesToRead)
            {
               if (rxidx < rxdata.Length - 2)
               {
                  rxdata[rxidx++] = (byte)sp.ReadByte();
                  rxdata[rxidx] = 0;
               }
               else
                  sp.ReadByte();
            }
         }
         catch (Exception ex)
         {
            logfile.write(ex.Message);
            errtxt = ex.Message;
         }
      }

      private bool Open(string COM)
      {
         bool opened = false;

         _serialPort.PortName = COM;
         _serialPort.BaudRate = 115200;
         _serialPort.Parity = Parity.None;
         _serialPort.DataBits = 8;
         _serialPort.StopBits = StopBits.One;
         _serialPort.Handshake = Handshake.None;
         _serialPort.Open();
         // Создание обработчика события для приема данных:
         _serialPort.DataReceived += new SerialDataReceivedEventHandler(sp_DataReceived);
         _serialPort.WriteTimeout = 50;
         opened = true;
         return opened;
      }

      private void Close()
      {
         _serialPort.Close();
      }

      private String Received()
      {
         return Encoding.GetEncoding(1251).GetString(rxdata);
      }

      private bool Write(string senddata)
      {
         bool result;
         int cnt = senddata.Length;
         int i=0;
         int tumeoutcnt = 0;

         errtxt = "";
         try
         {
            RxReset();
            for (i = 0; i < cnt; i++)
            {
               char sym = senddata[i];
               _serialPort.Write(sym.ToString());
               tumeoutcnt = 50;
               while (!Received().Contains(senddata.Substring(0, i + 1))
                      && (0 != tumeoutcnt))
               {
                  Thread.Sleep(10);
                  tumeoutcnt--;
               }
            }
         }
         catch (Exception ex)
         {
            logfile.write(ex.Message);
            errtxt = ex.Message;
         }
         result = (i == cnt) && (0 != tumeoutcnt);
         return result;
      }
   }
}

[Ссылки]

1. Проекты Visual Studio на языке C# и Visual Basic, работающие с COM-портом.
2. Алгоритм работы CTS и RTS в RS-232.

 

Комментарии  

 
0 #8 Plov 08.01.2018 20:38
[quote name="Владимир"]Не очень шарю в С/С++/С#. При запуске этого приложения выдает ошибку в textbox "Порт 'COM1' не существует." Всё подключено в порт COM4. В Вашем приложении не предлагается выбор портов в ComboBox1, список пуст. Подскажите в чем может быть проблема.

Добавьте в класс frmMain в строку 33, следующий код:
comm.PortName = cboPort.SelectedItem.ToString();
Странно, что сделали выпадающий список, и не обработали его выбор.
Цитировать
 
 
0 #7 Borisius 04.03.2017 11:21
Просто добавьте в метод cmdOpen_Click строку
comm.PortName = "COM4";
где вместо COM4 может быть имя вашего порта... и всё.
Цитировать
 
 
-1 #6 Владимир 29.03.2016 16:45
Не очень шарю в С/С++/С#. При запуске этого приложения выдает ошибку в textbox "Порт 'COM1' не существует." Всё подключено в порт COM4. В Вашем приложении не предлагается выбор портов в ComboBox1, список пуст. Подскажите в чем может быть проблема. Спасибо заранее!

microsin: чтобы выпадающий список выбора COM-портов был непуст, заполните его во время выполнения приложения путем сканирования доступных в системе имен COM-портов (определить наличие порта в системе можно при попытке его открыть), либо заполните список вручную. Пункты в выпадающий список добавляются методом CommBox->Items->Add, см. документацию по API стандартных компонентов Microsoft.
Цитировать
 
 
+4 #5 Виталий 08.12.2015 22:32
Для чего в методе sp_DataReceived остановка потока Thread.Sleep(500);?
Я правильно понимаю, что 500 мс не будут поступать данные из порта? Если да, то что нужно сделать, что бы не пропустить эти данные?

microsin: задержка с помощью Sleep делается для того, чтобы уступить время выполнения другим потокам. Это стандартное действие, без которого остальные приложения будут работать медленно. В течение этой задержки данные будут поступать в буфер драйвера, и задержка подбирается под отсутствие переполнения буфера - чтобы программа успела выбрать все пришедшие данные.
Цитировать
 
 
-1 #4 ale 13.05.2015 14:48
Communicating with Serial Port in C# site:c-sharpcorner.com
Цитировать
 
 
+1 #3 Brig 25.03.2015 07:17
Как организовать работу порта в 9-ти битном режиме?
Цитировать
 
 
0 #2 Valery 10.01.2015 20:56
Спасибо, отличная статья. А можно ли управлять сигналами RTS и DTR, и проверять состояние CTS и DSR?

microsin: да, конечно, все это можно. Прогуглите C# SerialPort CTS RTS, информация доступна даже на русском языке.
Цитировать
 
 
+2 #1 DmitriyKornet 20.03.2013 18:24
Скажите а чем ограничена вообще скорость такого COM порта?

microsin: для обычного COM-порта максимальная скорость обычно ограничена аппаратно на 115200 бод. Для класса USB CDC (виртуальный COM-порт) скорость ограничена возможностями драйвера.
Цитировать
 

Добавить комментарий


Защитный код
Обновить

Top of Page