Pull to refresh

Разгоняем .NET

Reading time17 min
Views5.7K
.Net используются при программировании настольных и веб-приложений, а возможно ли использовать фреймворк для управления промышленными объектами?
Разберемся в начале с тем, где возможно применение такого ПО.
Промышленные системы управления состоят из нескольких уровней:
  • уровень датчиков
  • уровень управления (ПЛК, компьютер)
  • уровень визуализации/SCADA

Визуализация – самый неприхотливый уровень АСУ по скорости обработки информации, обычно на операторских панелях отображается информация о состоянии какого-либо агрегата всей системы (некоторые численные значения, биты состояния (вкл-выкл), графики изменения величин). Для этого уровня можно свободно использовать все возможности .NET для вывода графической информации (Win Forms, WPF, SilverLight).
Рассмотрим уровень управления. Здесь нужная скорость обработки информации зависит от объекта управления: если вы хотите просто помигать светодиодом или включить/выключить двигатель без жесткой привязки по времени, то это возможно. Если же необходимо управлять объектами с жестко-определенным временем реакции, то необходимо использовать систему реального времени.
Для систем реального времени необходима ОС реального времени. В таких ОС жёстко определено время переключения контекста задач, т.е. квант времени на обработку каждого потока с одинаковым приоритетом выделяется одинаковый.
А что может получиться при использовании «десктопной» ОС?

Я хочу рассмотреть какого быстродействия можно добиться, используя Windows и .Net. В данном случае возникнут препятствия для высокого быстродействия:
  • параллельно-выполняемые процессы ОС
  • фоновые службы
  • сборщик мусора .NET приложения
  • проверки при выполнении управляемого кода

Поставим задачу: есть ПК c Windows XP + адаптер USB-COM на базе контроллера FTDI. С какой скоростью возможно переключать RTS вывод адаптера для получения стабильного меандра(без изменений периода следования импульсов)? Такая задача –простейший вариант реализации управления внешними устройствами программой на ПК.
Для приближения быстродействия программы к режиму реального времени используем повышение приоритета программы и рабочего потока.

Немного теории по планировщику задач Windows



Всего имеется 32 уровня приоритетов:
  • 0 системный уровень «zero page thread»
  • 1-15 изменяемые уровни
  • 16-31 уровни реального времени

В диапазоне изменяемых приоритетов дополнительно выделяются классы:
  • Idle (2-6)
  • Below Normal (4-8)
  • Normal (6-10)
  • Above Normal(8-12)
  • High(11-15)
  • Real-time(22-26)

Каждому процессу при создании (CreateProcess()) можно задать класс приоритета, если он не указан, то процесс получает класс приоритета Normal (уровень 8). Каждому потоку процесса можно отдельно назначить уровень приоритета. Итоговый, базовый, приоритет потока вычисляется по комбинации класса приоритета процесса и уровня приоритета потока. Если базовый приоритет потока относится к группе приоритетов реального времени (уровень 16-31), то он динамически не изменяется, иначе планировщик может немного изменять приоритет потоков в зависимости от текущей загрузки процессора и событий (UI, I/O).
Квант времени для работы каждого потока выделяется 10мс для системы с одним процессором и 15мс, если система многопроцессорная. Также квант зависит от типа ОС (обычная или серверная (в серверной используются «длинные кванты»)), а также от состояния приложения (приоритет UI-потоков и потоков, работающих с устройствами ввода/вывода, временно повышается при возникновении соответствующих событий).
Для нашего примера используем самый «жесткий» вариант – установим класс приоритета процесса в Real-time и приоритет рабочего потока в Highest. Базовый уровень приоритета потока в таком сочетании равен 24. При таком уровне приоритета поток планировщик не должен переключать контекст на другие потоки (т.к. кроме системных процессов, других потоков с таким приоритетом нет).

Программа


using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Ports;
using System.Threading;
using System.Windows.Forms;
using FTD2XX_NET;

private bool _exitThread;
private Thread _workThreadPort, _workThreadPortUnmanaged, _workThreadDriver;
private List<long> _durationsPort, _durationsPortUnmanaged, _durationsDriver;

public FormMain()
{
    InitializeComponent();
    //System.Diagnostics.Process.GetCurrentProcess().PriorityClass =
    // ProcessPriorityClass.RealTime;
    // поток для теста через класс SerialPort
    _workThreadPort=new Thread(()=>
                                    {
                                        SerialPort port = new SerialPort("COM5");
                                        port.BaudRate = 115200;
                                        port.StopBits = StopBits.One; 
                                        port.Parity = Parity.None;
                                        port.DataBits = 7;
                                        port.Handshake = Handshake.None;
                                        port.Open();
                                        bool flag=false;
                                        Stopwatch stopwatch = new Stopwatch();
                                        stopwatch.Start();
                                        //Thread.CurrentThread.Priority = ThreadPriority.Highest;
                                        while (!_exitThread)
                                        {
_durationsPort.Add(stopwatch.ElapsedMilliseconds);
                                            stopwatch.Reset();
                                            stopwatch.Start();
                                            port.RtsEnable = flag;
                                            flag = !flag;
                                        }
                                        port.Close();
                                    });
    // поток для теста через неуправляемый код (Win API)
    _workThreadPortUnmanaged = new Thread(() =>
                                        {
                                        UnmanagedSerialPort unmanagedSerialPort = new UnmanagedSerialPort("COM5");
                                        unmanagedSerialPort.Open();
                                        bool flag=false;
                                        Stopwatch stopwatch = new Stopwatch();
                                        stopwatch.Start();
                                        //Thread.CurrentThread.Priority = ThreadPriority.Highest;
                                        while (!_exitThread)
                                        {
                                            _durationsPortUnmanaged.Add(stopwatch.ElapsedMilliseconds);
                                            stopwatch.Reset();
                                            stopwatch.Start();
                                            if (flag)
                                            {
                                                unmanagedSerialPort.On();
                                            }
                                            else
                                            {
                                                unmanagedSerialPort.Off();
                                            }
                                            flag = !flag;
                                        }
                                        unmanagedSerialPort.Close();
                                        });
    // поток для теста через API производителя чипа FTDI
    _workThreadDriver=new Thread(()=>
                                        {
                                        FTDI myFtdiDevice = new FTDI();
                                        myFtdiDevice.OpenByIndex(0);
                                        bool flag=false;
                                        Stopwatch stopwatch = new Stopwatch();
                                        stopwatch.Start();
                                        //Thread.CurrentThread.Priority = ThreadPriority.Highest;
                                        while (!_exitThread)
                                        {
                                            FTDI.FT_STATUS ftStatus = myFtdiDevice.SetRTS(flag);
                                            if (ftStatus == FTDI.FT_STATUS.FT_OK)
                                            {
                                                flag = !flag;
                                                _durationsDriver.Add(stopwatch.ElapsedMilliseconds);
                                                stopwatch.Reset();
                                                stopwatch.Start();
                                            }
                                        }
                                        myFtdiDevice.Close();
                                        });
    _durationsDriver = new List<long>();
    _durationsPort = new List<long>();
    _durationsPortUnmanaged = new List<long>();
}

// сохранение статистики
private void b_Save_Click(object sender, EventArgs e)
{
    StreamWriter sw = new StreamWriter("Port.csv");
    _durationsPort.ForEach(l => sw.WriteLine(l));
    sw.Close();
    sw = new StreamWriter("Driver.csv");
    _durationsDriver.ForEach(l => sw.WriteLine(l));
    sw.Close();
    sw = new StreamWriter("PortUnmanaged.csv");
    _durationsPortUnmanaged.ForEach(l => sw.WriteLine(l));
    sw.Close();
}

private void b_RunViaPort_Click(object sender, EventArgs e)
{
    _exitThread = false;
    _workThreadPort.Start();
}

private void b_Stop_Click(object sender, EventArgs e)
{
    _exitThread = true;
}

private void b_RunViaPortUnmanaged_Click(object sender, EventArgs e)
{
    _exitThread = false;
    _workThreadPortUnmanaged.Start();
}

private void b_RunViaDriver_Click(object sender, EventArgs e)
{
    _exitThread = false;
    _workThreadDriver.Start();
}

* This source code was highlighted with Source Code Highlighter.


Рассмотрим 3 варианта работы с портом:
  • класс SerialPort
  • неуправляемый код (Win API: GetCommState, EscapeCommFunction, SetCommState)
  • библиотека, поставляемая фирмой FTDI

В каждом случае мы меняем состояние пина RTS в цикле программы, засекая время одного цикла. Это время соответствует времени переключения пина в нужное состояние.
Условия тестов: каждый из вариантов проверяется в течение 30 с, после 10с процессор на 10с нагружается дополнительным приложением (WinRar – тест быстродействия оборудования). Каждый вариант проверяется при обычном(уровень 8) приоритете потока (синий график) и Real-time(уровень 24, красный график).

Результаты


Класс SerialPort
image
Неуправляемый код и библиотека FTDI
image
По графикам видно, что при работе через стандартный класс SerialPort время цикла примерно в 5 раз превышает время при работе через «нестандартное» решение. Повышение приоритета потока сокращает время одного цикла до 2мс, такой поток не прерывается.

Выводы


При реализации управления внешними устройствами от ПК при помощи управляемого кода .net фреймворка время реакции будет не меньше чем 1-2мс. Частично снизить влияние параллельных процессов возможно увеличением приоритета потока. При этом не следует забывать о других процессах и при возможности вручную переключать контекст (Thread.Sleep(0)) на другие ожидающие потоки. Следует избегать лишних вызовов сборщика мусора (GC) рациональной работой с объектами, использовать правильную архитектуру приложения, это можно проследить профайлером или системными счетчиками производительности. Также в многопроцессорной системе можно закреплять разные потоки за разными процессорами(см. SetThreadAffinityMask()).
Цель статьи – не сделать очередной велосипед, понятно, что для управления техпроцессами без ПЛК или микроконтроллера не обойтись; я хочу показать, что и для .NET возможно найти применение в решении определенного круга задач, где необходимое минимальное время реакции системы на воздействие больше 2-15мс.
Хотелось бы увидеть через N лет .NET RT по примеру Java Real-Time System. А также промышленное применение .NET Micro Framework на ПЛК известных фирм (Siemens, Omron).

Update1

Ссылки


C# for Real-time — статья, подтолкнувшая меня на эту тему
Requirements for a Real-Time .NET Framework — в этой статье приведены принципы, необходимые для реализации .NET RT на основе сравнения с Java Real-time
Real-time GC in Java — Спасибо conscell
Сравнение Thread.Sleep(0) и Thread.Sleep(1): здесь и здесь — Спасибо Frozik и tangro
Tags:
Hubs:
Total votes 54: ↑36 and ↓18+18
Comments36

Articles