Ща будем подрубать Овеновский ПЛК110 к своей проге в Visual Studio. Полтора года назад мы этим уже занимались, но молча. Будем исправляться.
Должно получиться окошко, отображающее число циклов контроллера (счёт на стороне ПЛК). Также в окошке будет кнопка, позволяющая сбросить счётчик. Начнём с консольной программы, которая будет только выводить число. Закончим WPF-приложением с переходом от опросной модели к событийной.
Затариваемся
Visual Studio. Например, версия 2013 Community Edition — самая навороченная из бесплатных для некоммерческого использования. Также можно не выпендриваться и скачать самую лёгкую редакцию Express — ею можно пользоваться и в коммерческих целях. До первого запуска как-нибудь сами дойдёте, ок? Я верю в вас.
Библиотека NModbus. Пять лет уже не обновлялась, но работает нормально, да и опенсорс, есличо. Бесплатна для любых целей. Качаем ".NET 3.5 NModbus 1.11.0.0 Binaries". Можно не качать, если Visual Studio у вас Community Edition или ещё серьёзнее.
Готовимся
Пока устанавливается студия, можно купить контроллер Овен ПЛК110-60 и написать для него тестовую программу. Начнём с Modbus TCP, но потом я расскажу, как сделать RTU, например.
В общем, тут ничего хитрого: в конфигурацию добавляем Modbus (slave), в этот Modbus (slave) добавляем регистр 2 byte и называем его reg0, а в Modbus [FIX] добавляем TCP. Скриншот:
А в PLC_PRG пишем reg0 := reg0 + 1; Всё, тестовая программа готова. См. файл test0.pro, но он именно для ПЛК110-60. Если у вас другой — делайте сами. Ну и понятно, что можно взять любое другой Modbus-устройство.
Фигачим
В наконец запустившейся студии делаем консольное приложение на C#.
Теперь надо добавить в проект библиотеку NModbus. Тут есть два способа:
Если у вас студия Community Edition или круче, то воспользуемся NuGet. Tools > NuGet Package Manager > Manage NuGet Packages... В появившемся окне ищем NModbus:
Теперь можно написать мастера сети (айпишник своего ПЛК в длинную строчку подставьте):
Код:
using Modbus.Device;
using System;
using System.Net.Sockets;
using System.Threading.Tasks;
namespace NModbusTut
{
class Program
{
static void Main(string[] args)
{
var master = ModbusIpMaster.CreateIp(new TcpClient("10.1.6.10", 502));
while (true)
{
Console.Write("\r{0}", master.ReadHoldingRegisters(1, 0, 1)[0]);
}
}
}
}
Само собой, ModbusIpMaster можно заменить на ModbusSerialMaster, скормить ему вместо TcpClient последовательный порт (SerialPort), и таким образом получить Modbus RTU вместо Modbus TCP.
08.png
Но так делать — не совсем ок. То есть совсем не ок. Видите, как в методе Update мы десятки раз в секунду идём в диспетчерский поток вне зависимости от того, изменился регистр или нет? Давайте-ка перепишем так, чтобы воспользоваться привязкой к данным и при этом не гонять её за зря. Дальше чуть сложнее.
Что нужно исправить и добавить? Контролы в WPF любят сами привязываться к источникам данных. А у нас вместо этого класс окна кормит зелёную полоску с ложечки и называет её по имени. Надо, чтобы был такой объект, который принимал бы на вход модбас-регистры много раз в секунду, а отдавал теги заинтересованным контролам только когда теги изменяются. При этом знать контролы поимённо этот объект не должен, иначе добавление контролов будет затруднено. Делаем вот такой класс:
Скрытый текст:
Код:
public enum Tag
{
Reg0
}
/// <summary>
/// Готов принимать сырые регистры по опросной модели и преобразовывать
/// их в теги с уведомлением об изменениях по событийной модели
/// </summary>
/// <remarks>
/// Для простоты привязки сделан динамическим объектом. Свойства = теги.
/// </remarks>
public class Depoller : DynamicObject, INotifyPropertyChanged
{
// Сюда новые и старые значения тегов
IDictionary<tag, object=""> oldValues, newValues;
// Сюда названия тегов, изменившихся на данном проходе
List<tag> dirtyTags = new List<tag>();
// Через это событие другие объекты узнают об изменениях
public event PropertyChangedEventHandler PropertyChanged;
Dispatcher dispatcher;
public Depoller(Dispatcher d)
{
dispatcher = d;
oldValues = new Dictionary<tag, object="">();
newValues = new Dictionary<tag, object="">();
}
/// <summary>
/// Разбирает Modbus-регистры на теги
/// </summary>
///
Modbus-регистры как есть
public void Input(ushort[] data)
{
// Парсим
newValues[Tag.Reg0] = data[0];
// Составляем список изменившихся
lock (dirtyTags)
{
// Новый список
dirtyTags.Clear();
foreach (var pair in newValues)
{
Object val = null;
// Если тег записывается впервые или если он изменился
if (!oldValues.TryGetValue(pair.Key, out val) || !pair.Value.Equals(val))
{
oldValues[pair.Key] = pair.Value; // Запоминаем на следующий раз
dirtyTags.Add(pair.Key); // Добавляем в список изменённый на этот раз
}
}
}
if (PropertyChanged != null) // Если есть с кем поделиться этой радостной вестью
{
// В потоке пользовательского интерфейса
dispatcher.BeginInvoke(new Action(delegate()
{
lock (dirtyTags)
{
// Сообщить о каждом изменённом теге
foreach (var tag in dirtyTags)
{
PropertyChanged(this, new PropertyChangedEventArgs(tag.ToString()));
}
}
}));
}
}
/// <summary>
/// Доступ к тегам как к свойствам объекта
/// </summary>
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
return oldValues.TryGetValue((Tag)Enum.Parse(typeof(Tag), binder.Name), out result);
}
}
В перечислении Tag именуем свои теги. В методе Input разбираем регистры на теги. Скажем, если один тег типа float у вас передаётся двумя модбас-регистрами (типично), то именно в этом методе из двух ushort'ов (WORD'ов) получается один float (REAL). Дальше немножко магии, и у нас есть объект, который сообщает об изменениях тегов и отдаёт эти теги по запросу. Теперь в классе окна (а в серьёзном проекте лучше где-нибудь повыше) мы скармаливаем регистры уже объекту этого класса и задаём привязку контролов на него. Можно не ковыряя лишний раз код добавить текстовое поле только за счёт разметки, например:
Да я так и не собрался написать. Там всё сводится к вызову функций записи по нажатию кнопки типа master.Write... Хотел ещё рассказать про развязку кнопок (gui) и обработчиков а ля MVVM, но это до меня всё расписано вдоль и поперёк.
master.WriteSingleRegister(1, 0, 96)
1 - адрес слейва
0 - адрес регистра
96 - значение
Продолжаю усложнять себе задачи
Пытаюсь подать/считать сигнал с входа/выхода.
Как я понимаю, для этого нужно использовать Writesinglecoil Readcoilstatus
Но не могу найти адреса для входов/выходов - в инструкции не нашел (у меня ПЛК110-24.60.Р.М).
Не подскажете как обращатся к входу/выходу ПЛК. Я так понимаю должны быть какая-то таблица "Вход/выход ПЛК" соответствует "Номер бита"?
Такой таблицы нет. Вообще говоря слейв, который вы создаёте в ПЛК для связи с ПК, не имеет связей со входами и выходами. Эти связи вам нужно создать самому. По-простому можно в программе ПЛК самому разметить выходы через присваивание типа di10 := slaveRegA.9.
В боевых проектах мне приходится делать сложнее. Нулевой регистр отвожу на код команды, ещё несколько — на аргументы. Дальше в программе ПЛК пишу интерпретатор вроде
Код:
TYPE CMD: (NOP, ON, OFF); END_TYPE
CASE pc_cmd OF
ON: DI1 := TRUE;
OFF: DI1 := FALSE;
END_CASE
pc_cmd := NOP;
При этом в мастере есть такой же
Код:
enum Cmd { Nop, On, Off }
И, соответственно, я могу командовать контроллером примерно как master.WriteSingleRegister(1, 0, Cmd.On). Ну, естественно, там добавляются ещё аргументы и т.д.
Yegor, а есть где-то описание "ПЛК110/Мx110 memory model"?
В частности:
1) word tearing
2) out of order writes / happens-before / data race
3) out of thin air values
Насколько я понимаю, в момент работы цикла "сетевые переменные" своих состояний не меняют. Но как оно работает в момент "до цикла/после цикла"?
Какие-нибудь гарантии на tearing есть?
Не попадалось. Да и не наблюдал таких эффектов. Но я не загружал ПЛК запросами настолько, чтобы это могло проявиться. Ну а модули вообще быстрого дуплексного интерфейса не имеют, чтобы это можно было хотя бы проверить (не говоря о том, чтобы столкнуться с этим в реальном проекте). Если в контексте темы, то лучше, конечно, приостанавливать опрос на время записи — так можно хотя бы себя обезопасить от вероятного рассинхрона.
Такой таблицы нет. Вообще говоря слейв, который вы создаёте в ПЛК для связи с ПК, не имеет связей со входами и выходами. Эти связи вам нужно создать самому.
Спасибо, этого понимания мне не хватало.
Пока буду пользоваться более простым способом (напрямую задавать di10 := slaveRegA.9)
Столкнулся с еще одной проблемой, после запуска своей программы и выполнения какого-то действия (запись или чтение) я не могу подключится к ПЛК из Codesys.
ПЛК у меня подключен по Ethernet (Ip, шлюз и маска прописаны согласно настроек локальной сети).
При подключении из Codesys получаю ошибку: "Ошибка связи (#0): произошло отключение".
Также если потом пытаюсь подключится к ПЛК из своей программы например для чтения (или Вашего консольного приложения) как правило перед успешным подключением получаю две ошибки: "Конечный компьютер отверг запрос на подключение".
Возможно нужно закрывать подключение? Но в Вашем проекте Вы ничего подобного не делаете и ошибок нет.