The chip on the GHI Module automatically switches between send and receive. It is not necessary (and not possible) to set any delay.
The downside of the chip on the GHI Module is that it need 5V. Most other chips with read/write pin can work with 3V3.
I have released a bug fix version V1.0.2 which specially fixes some nasty issues when implementing a Modbus TCP Device.
The Sockets from disconnected clients were not closed, which caused some nasty side effects after a while.
Hi all, I know itās not needed because out of date but I have done ModbusAscii version. I havenāt a lot of time to test it. Do you want to give it a try? Also I havenāt modbus ascii equipment to test itā¦ sorryā¦ :wall:
Basically it convert buffer from ASCII to RTU and from RTU to ASCII in the interface so it has the same handling from outside as a RTU telegramā¦ and use 1 byte LRC instead of 2 byte CRC, : at the beginning and \r\n at the end.
Itās the original code with added the ModbusAsciiInterface and you will find some changes in ModbusUtils static class for convertion and LRC.
[url]https://github.com/gremlinc5/ModbusLib[/url]
Let us now about it, I think if it works we could add it to the Reinhard Ostermeier repository here on GHI ;D
Do you have some tips on a modbus ascii device that I could buy ?
@ gremlinc5 - I do not have or know of any Modbus ASCII device.
If someone gets a Chance to test it I will add it, but as Long as itās not tested I do not want to add it to my public repository.
Very few devices are Modbus ASCII these days. Modbus is an old protocol by todayās standards and if anything new comes out and supports it, it generally uses the RTU protocol. If you have to interface to an ASCII device it would be very unusual or old which from the fact you donāt have an ASCII device to test with would sort of indicate that.
Hi all, today a person that I worked for say me that needs help with a PLCā¦ that PLC is a Elsist Slimline PLCā¦ thats has Modbus Ascii protocol supportā¦ what a luck!
I will test it and make you know about it! 8)
I used before Modbus RTU electric analizers for work, brand like Contrel (EMS-96 if I remember right) and Gavazzi. There is a lot of RTU out there. But writing Modbus Ascii interface makes me better understand other interfaces, I hope
TESTED!
Here the first stable:
[url]https://github.com/gremlinc5/ModbusLib[/url]
Tested with Read/Write Coils and Registers in Modbus Ascii, like:
using System;
using System.IO.Ports;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using System.Threading;
using Osre;
using Osre.Modbus;
using Osre.Modbus.Interface;
namespace MFConsoleModbusAsciiTest
{
public class Program
{
public static void Main()
{
Debug.Print(Resources.GetString(Resources.StringResources.String1));
var seriale = new SerialPort("COM2", 115200, Parity.None, 8, StopBits.One);
seriale.Open();
var asciiInterface = new ModbusAsciiInterface(seriale);
var master = new ModbusMaster(asciiInterface);
Debug.Print("Reading...");
while (true)
{
var array = new ushort[32];
var new_array = new byte[32];
try
{
master.WriteMultipleRegisters(1, 39999, new ushort[3] { 1234, 5678, 0 });
array = master.ReadHoldingRegisters(1, 39999, 2); // throws ModbusException on error
new_array = master.ReadCoils(1, 40003, 1);
master.WriteSingleCoil(1, 40003, true);
new_array = master.ReadCoils(1, 40003, 1);
Debug.Print("Read: " + array[0].ToString());
}
catch (Exception ex)
{
Debug.Print("Error: " + ex.Message);
}
Thread.Sleep(2222);
}
}
}
}
Elsist Slimline PLC registers start at 40000 address.
Hi all, could be used BitConverter on the data output from Reading functions? Or data need to be swapped? I see some example of BitConverter, like:
But in modbus array[0] isnāt the LSBā¦ I think buffer must be swapped modbus is Big-endianā¦ or Iām wrongā¦
@ gremlinc5 - The buffers Needs to be swaped for Modbus.
This is already done correctly in my library.
@ Reinhard Ostermeier -
Hi Reinhard
Iāve been using version 1 class library with my Netduino P2 on .NET MF 4.2 using Modbus TCP/IP to read PLC data.
Sending the command āMaster.WriteSingleCoil(0, 7, false); // Write to PLC coil 0008ā works perfectly.
Iāve added code to read a holding register from the same PLC (Modicon 140CPU11302) as below.
Master.ReadHoldingRegisters(1, 40001, 1);
Looking at your class library, Iām not completely clear on how to retrieve the data word received from the PLC. The ModbusDevice.cs has the methods to read the incoming data.
Do you have have examples to retrieve the received holdingregister data?
See full code listing below.
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Collections;
using System.IO;
using System.IO.Ports;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using Microsoft.SPOT.Net.NetworkInformation;
using SecretLabs.NETMF.Hardware;
using SecretLabs.NETMF.Hardware.NetduinoPlus;
using SecretLabs.NETMF.IO;
using Osre.Modbus;
using Osre.Modbus.Interface;
using Toolbox.NETMF;
namespace NetduinoModbusTCP
{
public class Program
{
public static SPI.Configuration LCDSPI1;
public static SerialPort GPSSerialPort = new SerialPort(āCOM1ā, 9600, Parity.None, 8, StopBits.One);
//Intialize the PWM pin D9 for alarm driver output
static PWM alarm = new PWM(PWMChannels.PWM_PIN_D9, 2000, .5, false); //Internal alarm speaker
//Analog Variables
public static double CheckBattVoltage = 0;
static Microsoft.SPOT.Hardware.AnalogInput pinA0 = new Microsoft.SPOT.Hardware.AnalogInput(AnalogChannels.ANALOG_PIN_A0);//
//Define new Interrupt Ports to external buttons on SCL, SDA, D4 and D7
// NOTE: Set glitch filter to "false". This is only needed to
// prevent switch bounce and in this case not needed.
// NOTE: The pullup on the MCU is disabled and the onboard switch
// has a pulldown resistor so we set this to "Disabled".
// NOTE: The switch has a pulldown resistor and is connected to 3.3 (logic HIGH)
// so the switch will show HIGH when pressed or LOW when released.
// NOTE: Set the Interrupt to fire on edges levels (HIGH and LOW) *** verify all inputs ***.
static InterruptPort Upbutton = new InterruptPort(Pins.GPIO_PIN_D4, false, Port.ResistorMode.Disabled, Port.InterruptMode.InterruptEdgeHigh);
static InterruptPort Enterbutton = new InterruptPort(Pins.GPIO_PIN_SDA, false, Port.ResistorMode.Disabled, Port.InterruptMode.InterruptEdgeHigh);
static InterruptPort Downbutton = new InterruptPort(Pins.GPIO_PIN_SCL, false, Port.ResistorMode.Disabled, Port.InterruptMode.InterruptEdgeHigh);
static InterruptPort GPSDataReady = new InterruptPort(Pins.GPIO_PIN_D7, false, Port.ResistorMode.Disabled, Port.InterruptMode.InterruptNone);
// Define new Output Ports for front panel Red and Green LED's
static OutputPort LED1 = new OutputPort(Pins.GPIO_PIN_D5, true);
static OutputPort LED2 = new OutputPort(Pins.GPIO_PIN_D6, true);
//Declare timers, Modbus TCP interfaces and devices
public static Timer ReadRegisters;
public static ModbusTcpInterface tcpInterface = new ModbusTcpInterface("10.0.0.100");
public static ModbusMaster Master = new ModbusMaster(tcpInterface);
public static MyModbusDevice device = new MyModbusDevice(ModbusConst.TcpDeviceAddress);
//PLC Registers
public static int Coil_Address = 5;
public static bool Coil_State = false;
public static void Main()
{
// Configure SPI1 port for LCD communications
LCDSPI1 = new SPI.Configuration(
Pins.GPIO_PIN_D10, //Chip Select pin
false, // Chip Select Active State low
0, // Chip Select Setup Time 0us
0, // Chip Select Hold Time 0us
true, // Clock Idle State
true, // Clock Edge
100, // Clock Rate in kHz
SPI_Devices.SPI1); //SPI Module
// Delay for x seconds to allow LCD to stablize.
DelayxmS(2);
// Create an event handler for the push buttons and GPS Data Ready signal
Upbutton.OnInterrupt += new NativeEventHandler(Upbutton_OnInterrupt);
Enterbutton.OnInterrupt += new NativeEventHandler(Enterbutton_OnInterrupt);
Downbutton.OnInterrupt += new NativeEventHandler(Downbutton_OnInterrupt);
GPSDataReady.OnInterrupt += new NativeEventHandler(GPSDataReady_OnInterrupt);
//Setup the board static IP address
NetworkInterface.GetAllNetworkInterfaces()[0]
.EnableStaticIP("10.0.0.99", "255.255.255.0", "10.0.0.1");
string localip = NetworkInterface.GetAllNetworkInterfaces()[0]
.IPAddress;
Debug.Print("The local IP address of your Netduino Plus is " + localip);
//Display system prompt
PrintSystemReady();
//Read SD Card text file
//ReadSDCard(); *** Add updated method when needed ***
//Read Analog Input
ReadBatteryVoltage();
Alarm(1000);
//Display Main Menu
PrintMainMenu();
//Thread.Sleep is necessary to keep the program running
Thread.Sleep(Timeout.Infinite);
}
public static void InitTimers()
{
TimerCallback RegisterDelegate = new TimerCallback(HoldingRegisters);
ReadRegisters = new Timer(RegisterDelegate, null, 1000, 1000);
var listener = ModbusTcpInterface.StartDeviceListener(device);
device.Start();
}
// Use timer delegate to read four holding registers per 1 second
public static void HoldingRegisters(Object stateInfo)
{
// Create an object of type Globals.
Globals ArrayClass = new Globals();
Master.ReadHoldingRegisters(1, 40001, 1);
//byte[] Hold_Reg_Data = Tools.UShortsToBytes(Hold_Reg_Result);
DelayxmS(500);
DisplayArray(ArrayClass.ClrScreen);
DisplayArray(ArrayClass.CursorPosition1);
//DisplayArray(Hold_Reg_Data);
}
public static void ReadBatteryVoltage()
{
// Create an object of type Globals.
Globals ArrayClass = new Globals();
double maxADCVoltage = 3.3; // Maximum full scale voltage at analog input A0
double maxBattVoltage = 10.58; // Maximum NiMH battery voltage after full charge
double value = 0;
double tempvalue = 0;
int maxAdcValue = 4096; // ADC is 12-bit Resolution
int CalcCounter = 10;
// Calculate battery voltage from 10 analog input A0 readings
while (CalcCounter != 0)
{
int rawValue = pinA0.ReadRaw(); // Binary value read from analog input A0
double aValue = (rawValue * maxADCVoltage); // A0 Binary value multiplied by 3.3
value = aValue / maxAdcValue; // A0 input voltage value from 0 to 3.3
tempvalue = tempvalue + value;
CalcCounter--;
}
value = tempvalue / 10;
double VoltageScale = value / maxADCVoltage; // Voltage scale factor from 0 to 1
double BattVoltage = (VoltageScale * maxBattVoltage); // Calculated NiMH battery voltage from voltage scale
double CheckBattVoltage = System.Math.Round(BattVoltage * 10000.0) / 10000.0;
string BatteryVoltage = CheckBattVoltage.ToString(); // Convert battery voltage to a string format
byte[] BatteryData = Encoding.UTF8.GetBytes(BatteryVoltage);
if (CheckBattVoltage < 9.6)
{
DisplayArray(ArrayClass.CursorPosition1);
DisplayArray(ArrayClass.Message4);
DisplayArray(ArrayClass.CursorPosition2);
DisplayArray(BatteryData);
}
else
DisplayArray(ArrayClass.CursorPosition1);
DisplayArray(ArrayClass.Message5);
DisplayArray(ArrayClass.CursorPosition2);
DisplayArray(BatteryData);
}
public static void GPSDataReady_OnInterrupt(uint port, uint data, DateTime time)
{
//Add future code here...
}
//Print System ready message
public static void PrintSystemReady()
{
// Create an object of type Globals.
Globals ArrayClass = new Globals();
DisplayArray(ArrayClass.LCDBrightness);
DisplayArray(ArrayClass.LCDContrast);
DisplayArray(ArrayClass.TurnOnBlinkingCursor);
DisplayArray(ArrayClass.TurnOnUnderlineCursor);
DisplayArray(ArrayClass.ClrScreen);
Coil_State = true;
}
//PrintMainMenu in order to select main menu items with button entry
public static void PrintMainMenu()
{
// Create an object of type Globals.
Globals ArrayClass = new Globals();
//Clear LCD Screen
DisplayArray(ArrayClass.ClrScreen);
//Display Main Menu Line1 if MainMenuInstance = 1
if (Globals.MainMenuInstance == 1)
{
DisplayArray(ArrayClass.CursorPosition1);
DisplayArray(ArrayClass.Menu1);
}
}
//PrintSubMenu to increase or decrease LCD brightness and contast
public static void PrintSubMenu()
{
// Create an object of type Globals.
Globals ArrayClass = new Globals();
//Clear LCD Screen
DisplayArray(ArrayClass.ClrScreen);
//Display Main Menu Line1
DisplayArray(ArrayClass.CursorPosition1);
DisplayArray(ArrayClass.Menu2);
//Display Main Menu Line2
DisplayArray(ArrayClass.CursorPosition2);
DisplayArray(ArrayClass.Menu3);
}
public static void Upbutton_OnInterrupt(uint port, uint data, DateTime time)
{
// Create an object of type Globals.
Globals ArrayClass = new Globals();
if (Globals.SubMenuInstance == 1)
{
if (Globals.SubMenuIndex > 1 || Globals.SubMenuIndex < 3)
{
Globals.SubMenuIndex--;
Globals.UpToggle = true;
}
else Globals.SubMenuIndex = 1;
if (Globals.SubMenuIndex == 3)
{
Globals.SubMenuIndex--;
Globals.UpToggle = true;
PrintSubMenu();
}
switch (Globals.SubMenuIndex)
{
case 1:
//Set cursor at first character at line 1
DisplayArray(ArrayClass.CursorPosition1);
break;
case 2:
//Set cursor at first character at line 2
DisplayArray(ArrayClass.CursorPosition2);
break;
default:
break;
}
}
}
public static void Enterbutton_OnInterrupt(uint port, uint data, DateTime time)
{
// Create an object of type Globals.
Globals ArrayClass = new Globals();
// Determine if the MainMenuInstance is set to initialize the SubMenu
if (Globals.MainMenuInstance == 1)
{
PrintSubMenu();
Globals.SubMenuInstance = 1;
Globals.MainMenuInstance = 0;
}
if (Globals.SubMenuInstance == 1)
{
switch (Globals.SubMenuIndex)
{
case 1:
//Start the TCP Server
StartTCPServer();
break;
case 2:
//Start the Modbus TCP Server
StartModbusTCP();
break;
case 3:
//Toggle the LCD Backlight On/Off
ToggleLCDLight();
break;
default:
break;
}
}
}
public static void Downbutton_OnInterrupt(uint port, uint data, DateTime time)
{
// Create an object of type Globals.
Globals ArrayClass = new Globals();
if (Globals.SubMenuInstance == 1)
{
if (Globals.SubMenuIndex < 4)
{
Globals.SubMenuIndex++;
Globals.DownToggle = true;
}
else Globals.SubMenuIndex = 1;
PrintSubMenu();
switch (Globals.SubMenuIndex)
{
case 1:
//Set cursor at first character at line 1
DisplayArray(ArrayClass.CursorPosition1);
break;
case 2:
//Set cursor at first character at line 2
DisplayArray(ArrayClass.CursorPosition2);
break;
case 3:
//Set cursor at first character at line 1
DisplayArray(ArrayClass.ClrScreen);
DisplayArray(ArrayClass.CursorPosition1);
DisplayArray(ArrayClass.Menu4);
break;
default:
break;
}
}
}
//Start TCP Server
public static void StartTCPServer()
{
LED1.Write(false); // turn on the LED1
DelayxmS(500);
LED1.Write(true); // turn off the LED1
}
//Start ModbusTCP Server
public static void StartModbusTCP()
{
LED2.Write(false); // turn on the LED2
DelayxmS(500);
LED2.Write(true); // turn off the LED2
InitTimers();
}
//Toggle LCD BackLight
public static void ToggleLCDLight()
{
// Create an object of type Globals.
Globals ArrayClass = new Globals();
if (Globals.LCDLight == false)
{
DisplayArray(ArrayClass.LCDLightOff);
Master.WriteSingleCoil(0, 7, false); // Write to PLC coil 0008
}
else
{
DisplayArray(ArrayClass.LCDBrightness);
Master.WriteSingleCoil(0, 7, true); // Write to PLC coil 0008
}
Globals.LCDLight = !Globals.LCDLight;
Coil_State = !Coil_State;
}
//Display received bytes from external TCP client
public static void DisplayTCPData(byte[] array)
{
// Create an object of type Globals.
Globals ArrayClass = new Globals();
byte[] LineNumber = new byte[1];
byte[] Space = new byte[] { 0x20 };
//Array.Copy(a, 1, b, 0, 3);
// array = source array
// i = start index in source array
// LineNumber = destination array
// 0 = start index in destination array
// 1 = elements to copy
Array.Copy(array, 0, LineNumber, 0, 1);
//Determine which LCD line number to display
String LineNumberString = new String(System.Text.UTF8Encoding.UTF8.GetChars(LineNumber));
Int32 LCDLineNumber = Int32.Parse(LineNumberString);
switch (LCDLineNumber)
{
case 1:
//Display LCD Display Line1 ASCII Text
DisplayArray(ArrayClass.CursorPosition1);
byte[] LCDLine1 = new byte[20];
Array.Copy(array, 1, LCDLine1, 0, 20);
DisplayArray(LCDLine1);
DisplayArray(ArrayClass.CursorPosition2);
break;
case 2:
//Display LCD Display Line2 ASCII Text
byte[] LCDLine2 = new byte[20];
Array.Copy(array, 1, LCDLine2, 0, 20);
DisplayArray(LCDLine2);
DisplayArray(ArrayClass.CursorPosition1);
break;
default:
break;
}
}
//DisplayArray for displaying LCD data array contents for menus and data received from an external TCP client
public static void DisplayArray(byte[] arr)
{
// Create an object of type Globals.
Globals ArrayClass = new Globals();
//Send Array contentd to LCD display over SPI1 port
using (SPI spi = new SPI(LCDSPI1))
{
//Array.Copy(a, 1, b, 0, 3);
// arr = source array
// i = start index in source array
// LCDData = destination array
// 0 = start index in destination array
// 1 = elements to copy
for (int i = 0; i < arr.Length; i++)
{
Array.Copy(arr, i, ArrayClass.LCDData, 0, 1);
int decValue = ArrayClass.LCDData[0];
if (decValue == 254 || decValue >= 32 || decValue <= 127)
{
spi.Write(ArrayClass.LCDData);
DelayxmS(1);
}
}
spi.Dispose();
}
}
//Turn on alarm speaker for x seconds
//Turn on alarm speaker for 2 seconds
public static void Alarm(int OnTime)
{
//Turn on alarm speaker for 2 kHz PWM
alarm.Start();
DelayxmS(OnTime);
alarm.Stop();
}
//Delay method for LCD display stablization
public static void DelayxmS(int Multx)
{
Globals.xmSMultiplier = Multx * 1;
Thread.Sleep(Globals.xmSMultiplier);
}
}
// implement slave device
public class MyModbusDevice : ModbusDevice
{
public MyModbusDevice(byte deviceAddress, object syncObject = null)
: base(deviceAddress, syncObject)
{ }
public MyModbusDevice(IModbusInterface intf, byte deviceAddress, object syncObject = null)
: base(intf, deviceAddress, syncObject)
{ }
protected override string OnGetDeviceIdentification(ModbusObjectId objectId)
{
switch (objectId)
{
case ModbusObjectId.VendorName:
return "MeManufacturer";
case ModbusObjectId.ProductCode:
return "1";
case ModbusObjectId.MajorMinorRevision:
return "1.0";
case ModbusObjectId.VendorUrl:
return "http://www.MeManufacturer.org";
case ModbusObjectId.ProductName:
return "MyModbusDevice";
case ModbusObjectId.ModelName:
return "1";
case ModbusObjectId.UserApplicationName:
return "MyModbusApplication";
}
return null;
}
protected override ModbusConformityLevel GetConformityLevel()
{
return ModbusConformityLevel.Regular;
}
protected override ModbusErrorCode OnWriteSingleCoil(bool isBroadcast, ushort address, bool value)
{
if (false) // check address here
{
return ModbusErrorCode.IllegalDataAddress;
}
// set coil value here
return ModbusErrorCode.NoError;
}
protected override ModbusErrorCode OnReadCoils(bool isBroadcast, ushort startAddress, ushort coilCount, byte[] coils)
{
if (false) // check coil count here
{
return ModbusErrorCode.IllegalDataValue;
}
if (false) // check address here
{
return ModbusErrorCode.IllegalDataAddress;
}
// write coil values into parameter coils here
return ModbusErrorCode.NoError;
}
// override any On<ModusFunction> methods here
}
}
@ djr2077 - Why donāt you use the return value of ReadHoldingRegisters()?
its a ushort[] holding the number of Registers you have requested.
Hi djr2077,
could be that your timer will overlap in case of timeout? Standard Timeout in the library is 2000 ms, your timer fire every 1000 msā¦ in case of timeout timer will fire 2 timesā¦
I like much more Threads instead of Timers for external data exchangeā¦ I like more Timers for LCD refresh as an exampleā¦
In standard .NET for external data exchange it could be used the BackGroundWorker itās a thread.
What do you think boys? I could be wrong!
I started playing around with this library, but Iād like to write a transport for a WIZnet 5100 instead of .net sockets. I saw an older post where someone might have done this with a WIZnet 5100:
https://www.ghielectronics.com/community/forum/topic?id=6563
But I think the post is so old the link to the example code is broken. Anyone happen to have that or a similar example?
Iām looking to implement Function Codes 0x07 and 0x08 for and am trying to figure them out.
both look like a broadcast event or are they master only?
next the comment on them is Serial Line Only. does this mean RTU/ASCII only or should they work with TCP/RTU as well?
TIA.
BTW, pulled your git version, great work.
@ smgvbest - I think my master implementation already has both function codes implemented.
Hi
Iām working on the slave side and it does not appear to be implemented there.
If Iām wrong please correct me? I see it on the master side as you say, just not on the slave side.
@ smgvbest - No, I didnāt implement it on the slave side. But it should be straight Forward if you take the other slave side implementations and the master side as templates/hints.
@ Reinhard
Yep, thatās what Iāve been working on.
I thought I would post on here in case others are using this library and run into the same issues. Iāve already contacted the author on the issue.
I used this library in slave mode with a single device on the bus and it works well with no lost packets.
Yesterday I connected a Variable Speed Drive to the same bus and now the slave does not respond. Digging into the code I have spotted that it does not like the data being sent by the VSD. What is happening is that the request to the VSD and the reply are being received at the same time by the Modbus driver and the CRC fails so it goes back around for more data. If I check the buffer before the CRC check I can see the 2 complete messages are there, the request to the VSD and itās reply.
Looking at the scope, there is a 20ms gap after the request before the VSD responds so this should be more than enough time gap for the driver to detect the end of the telegram but it doesnāt appear to be working. It should be 3.5 character times which in this case is 960uS per character at 9200 bps so around 3ms max. This could be down to the timing waiting for the end of the telegram.
Iāve setup a similar setup in my office and I get the exact same issue with a similar VSD and my device.
I am working on this today as I need this working ASAP. If anyone else has seen this issue and you fixed it, can you post back here please.