Jump to content

Modbus RTU using libmodbus.dll


Go to solution Solved by MattyD,

Recommended Posts

Posted

Hello all,
I'm trying to use the libmodbus.dll 3.1.10 to connect a modbus device using modbus rtu.
I think I succesfully used the modbus_new_rtu function to create the needed context, but although it uses a quite simple syntax, I cannot succeded to use the modbus_set_client function of the dll. It always return a fail code (-1). I cannot understand what is going wrong.

Anyone here used the libmodbus dll to connect a device using modbus rtu?

Here is the simple code I used to test libmodbus functions:

#include <MsgBoxConstants.au3>
#include <Array.au3>
; modbus_t *modbus_new_rtu(const char *device, int baud, char parity, int data_bit, int stop_bit);
Local $Port = "\\.\COM7"
Local $BaudRate = 19200
Local $Parity = "E" ; N=None, E=Even O=Odd
Local $DataBits = 8
Local $StopBits = 1

Local $Modbus_Slave = 5

Local $hDLL = DllOpen("modbus.dll")

If @error Then
  MsgBox($MB_SYSTEMMODAL, "Error", "Error opening modbus.dll, Error code: " & @error)
Else
  Local $RTU_Ptr = DllCall($hDLL, "ptr:cdecl", "modbus_new_rtu", "str", $Port, "int", $BaudRate, "str", $Parity, "int", $DataBits, "int", $StopBits)
  If @error Then
    MsgBox($MB_SYSTEMMODAL, "Error", "Error calling function modbus_new_rtu, Error code: " & @error)
  Else
    MsgBox($MB_SYSTEMMODAL, "Info", "Exit code: " & $RTU_Ptr[0])
    _ArrayDisplay($RTU_Ptr)
  EndIf

  Local $RTU_Result = DllCall($hDLL, "int:cdecl", "modbus_set_slave", "ptr", $RTU_Ptr, "int", $Modbus_Slave)
  If @error Then
    MsgBox($MB_SYSTEMMODAL, "Error", "Error calling function modbus_set_slave, Error code: " & @error)
  Else
    _ArrayDisplay($RTU_Result)
  EndIf

  DllClose ($hDLL)
EndIf

Thank you in advance for your help.

Posted

Hi Geppo,

Its a bit difficult without having the dll to play with, but a couple of general things if it helps...

DllCall() returns an array, and the return value ends up in element 0.  So for the code below, the returned ptr would be in $aCall[0]..

$aCall = DllCall($hDll, "ptr", "SomeFunc", "int", $iParam1)

Also, the "str" type passes a pointer to a char array.  So to pass a single char for the parity param, you'll likely need to use "BYTE"

Posted

Thank you very much for your suggestions.

For what I succeed to understand from the modbus_set_client function documentation here:
modbus_set_slave
the returned value is an int, while I have to supply a pointer to the RTU data structure created using the modbus_new_rtu function documented here:
modbus_new_rtu
that instead seems it was correctly created.
Also, the Autoit DllCall function, documented here
DllCall
seems to require as second parameters the type of returned data, that in the specific case of modbus_set_slave function is an int (not a pointer), followed by a pointer to the RTU data structure and finally the slave ID as an int.
I suppose you didn't run the code, otherwise you could have seen that the RTU data structure was correctly created and the Autoit DllCall does not return errors (meaning the call was correctly formed) and the return value of the called function modbus_new_rtu is zero, confirming it works. Unfortunately when I try to call the modbus_set_slave function, DllCall return no errors, meaning the call was correctly formed but the modbus_set_slave function return -1 meaning it didn't work.
Anyway, for confirmation, I tried to use "byte" instead "str" for the Parity parameter as you suggested. The char "E" after the call was changed to "0" in the RTU data structure, so it doesn't sound good. You can verify it running the code.
Here you can find the ready-to-use compiled libmodbus ddl files:
libmodbus ready-to-use-dll

I attached two screenshots of the array returned after the DllCall to the function modbus_new_rtu and the array returned after the DllCall to the function modbus_set_slave

image.png

image.png

  • Solution
Posted (edited)

Thanks for that link - give this one a go :)

Edit: this was using the 64bit dll! - for the x86 I needed to use :cdecl as you have done in your example above!

#AutoIt3Wrapper_UseX64=Y

Global $hDLL = DllOpen("modbus.dll")

Local $sPort = "\\.\COM7"
Local $iBaudRate = 19200
Local $sParity = "E" ; N=None, E=Even O=Odd
Local $iDataBits = 8
Local $iStopBits = 1
Local $iModbus_Slave = 5

Local $pRTU, $bSuccess
$pRTU = _ModBus_NewRTU($sPort, $iBaudRate, $sParity, $iDataBits, $iStopBits)
ConsoleWrite("pRTU = " & $pRTU & @CRLF)
If $pRTU Then $bSuccess = _ModBus_SetSlave($pRTU, $iModbus_Slave)
ConsoleWrite("Set Slave Success = " & $bSuccess & @CRLF)
_ModBus_Close($pRTU)

DllClose($hDLL)

Func _ModBus_NewRTU($sPort, $iBaudRate, $sParity, $iDataBits, $iStopBits)
    Local $aCall = DllCall($hDLL, "ptr", "modbus_new_rtu", "str", $sPort, "int", $iBaudRate, "byte", Asc($sParity), "int", $iDataBits, "int", $iStopBits)
    If @error Then Return SetError(@error, @extended, 0)
    Return $aCall[0]
EndFunc

Func _ModBus_SetSlave($pRTU, $iSlave)
    Local $aCall = DllCall($hDLL, "int", "modbus_set_slave", "ptr", $pRTU, "int", $iSlave)
    If @error Then Return SetError(@error, @extended, 0)
    Return $aCall[0] = 0
EndFunc

Func _ModBus_Close($pRTU)
    DllCall($hDLL, "none", "modbus_clode", "ptr", $pRTU)
EndFunc

 

Edited by MattyD
Posted

Thank you very much for your help.
Looking at your code I understood lots of things.... 🙂
Firstly, my bad mistake about Parity, then the reason why the DllCall to the function modbus_set_slave couldn't work. I wrote $RTU_Ptr instead $RTU_Ptr[0] to pass the pointer.......


What I don't understand in your code is  "Return $aCall[0] = 0" in the Set_Slave function code.

Moreover although you code works perfectly with the x86 dll (switching to the cdecl calling convention and disabling the #AutoIt3Wrapper_UseX64=Y directive), I was not able to make it working using the x64 dll. It always returns:
pRTU = 0
Set Slave Success =

 

Posted
  On 4/7/2025 at 6:36 AM, Geppo said:

What I don't understand in your code is  "Return $aCall[0] = 0" in the Set_Slave function code.

 

Expand  

The correct function name where you wrote the above code is "_ModBus_SetSlave"

Posted

No worries mate :)

the $aCall[0] = 0 expression is just a quick way to get the function to return True/False. 
The dll call returns 0 on success - So if ($aCall[0] = 0) we return True, otherwise we return False. Hope that makes sense!

with the 64bit stuff, do you by chance have the full version of SciTE4AutoIt installed? I'm not 100% sure if the directive works without it...  (you can verify autoit_64.exe is being run by looking in the console)

>Running:(3.3.16.1):C:\Program Files (x86)\AutoIt3\autoit3_x64.exe "C:\Users\matt\Downloads\libmodbus-3.1.10_VS2008_X64\lib&dll\x64\Release\test.au3"    
+>Setting Hotkeys...--> Press Ctrl+Alt+Break to Restart or Ctrl+BREAK to Stop.
pRTU = 0x00000253258D0C40
Set Slave Success = True
Posted

Thank you again for you help.

Unfortunately I discovered it always runs autoit.exe, so I would have to make some change / update of the current Autoit installation. I will do, but it it is not so important at the moment.

Instead, although I went forward in testing libmodbus.dll basic functions, I didn't succeded to retrieve the content of some registers using function modbus_read_registers.
My intent is to simply test all functions exposed by the dll that I think I will need before coding all the functions to manage them.
What sounds odd I verified the correctness of the command issued activating the debug mode, as well as the proper device reply (I can see the requested data in the reply of the device), but I didn't succeed to extract them.

This is what I get from the console after activating the debug mode:

[01][03][00][00][00][08][44][0C]
Waiting for a confirmation...
<01><03><10><00><00><00><00><00><00><00><00><00><00><00><00><00><00><03><DD><24><F0>

confirming the correctness of the command (SlaveID 1, Function 03, 0 address, 8 registers, CRC16) and of the device reply where 03DD (989) is the content of the last requested device register.

This is the code I used to retrieve and to display the contents of the registers (after setting slave ID 1 and successfully connecting):

Local $RTU_Result = DllCall($hDLL, "int:cdecl", "modbus_set_debug", "ptr", $pRTU_Ptr[0], "boolean", True)
If @error Then
  MsgBox($MB_SYSTEMMODAL, "Error", "Error calling function modbus_set_debug, Error code: " & @error)
Else
  _ArrayDisplay($RTU_Result, "Modbus Set Debug")
EndIf

$iRegisterAddr = 0
$iRegisterNum = 8
Local $RegData_t = "struct;word data[" & $iRegisterNum & "];endstruct"
Local $tRegisterData = DllStructCreate($RegData_t)
Local $aRTU_Result = DllCall($hDLL, "int:cdecl", "modbus_read_registers", "ptr", $pRTU_Ptr[0], "int", $iRegisterAddr, "int", $iRegisterNum, "struct*", $tRegisterData)
If @error Then
  MsgBox($MB_SYSTEMMODAL, "Error", "Error calling function modbus_read_register, Error code: " & @error)
Else
  _ArrayDisplay($aRTU_Result, "Modbus Read Register")
  MsgBox($MB_SYSTEMMODAL, "Info", "$tRegisterData Struct Size: " & DllStructGetSize($tRegisterData) & @CRLF & _
                          "Struct pointer: " & DllStructGetPtr($tRegisterData) & @CRLF & _
                          "Data:" & @CRLF & _
                          "[1] " & DllStructGetData($tRegisterData, 1) & @CRLF & _
                          "[2] " & DllStructGetData($tRegisterData, 2) & @CRLF & _
                          "[3] " & DllStructGetData($tRegisterData, 3) & @CRLF & _
                          "[4] " & DllStructGetData($tRegisterData, 4) & @CRLF & _
                          "[5] " & DllStructGetData($tRegisterData, 5) & @CRLF & _
                          "[6] " & DllStructGetData($tRegisterData, 6) & @CRLF & _
                          "[7] " & DllStructGetData($tRegisterData, 7) & @CRLF & _
                          "[8] " & DllStructGetData($tRegisterData, 8))
EndIf

; Alternative to display registers contents
;For $i = 1 To 8
;  ConsoleWrite("[" & $i & "] = " & $tRegisterData.data($i) & @CRLF)
;Next

Unfortuanately it doesn't work and I always retrieve zero, also for the last register.

 

Posted

I don't have a device to test on - but I'd check this... That word[] array is all one element in the struct, so you'll need to go by indices with DllStructGetData. 

"[1] " & DllStructGetData($tRegisterData, 1, 1) & @CRLF & _
"[2] " & DllStructGetData($tRegisterData, 1, 2) & @CRLF & _

This might also help with debugging... #include <WinAPI.au3> at the top, then...

$iRegisterAddr = 0
$iRegisterNum = 8
Local $RegData_t = "struct;word data[" & $iRegisterNum & "];endstruct"
Local $tRegisterData = DllStructCreate($RegData_t)
Local $aRTU_Result = DllCall($hDLL, "int:cdecl", "modbus_read_registers", "ptr", $pRTU_Ptr[0], "int", $iRegisterAddr, "int", $iRegisterNum, "struct*", $tRegisterData)
If @error Then
    ConsoleWrite("Dll Call Failed" & @CRLF)
Else
    If $aRTU_Result[0] = -1 Then
        $aCall = DllCall($hDLL, "str:cdecl", "modbus_strerror", "int", _WinAPI_GetLastError())
        ConsoleWrite($aCall[0] & @CRLF)
    Else
        ConsoleWrite("Registers Read:" & $aRTU_Result[0] & @CRLF)
    EndIf
EndIf

PS: we got away with it, but just be careful with data typing here:

Local $RTU_Result = DllCall($hDLL, "int:cdecl", "modbus_set_debug", "ptr", $pRTU_Ptr[0], "boolean", True)

"boolean" is a 1-byte type, but the modbus_set_debug expects a 4-byte "int".  Using the 4-byte "bool" type would be a better option ...but to be pedantic we should probably do something like this as its what's documented.

Local $RTU_Result = DllCall($hDLL, "int:cdecl", "modbus_set_debug", "ptr", $pRTU_Ptr[0], "int", 1)

;Or to use True/False (AutoIt should cast True to "int"):
Local $RTU_Result = DllCall($hDLL, "int:cdecl", "modbus_set_debug", "ptr", $pRTU_Ptr[0], "int", True)

 

Posted

Thank you very much. You are an invaluable source of hints.

About the modbus_set_debug function,
Local $RTU_Result = DllCall($hDLL, "int:cdecl", "modbus_set_debug", "ptr", $pRTU_Ptr[0], "int", 1)
was the first syntax I used but it didn't work, so looking at the documentation, it speaks of "flag", so I supposed a flaw in the documentation, I used "boolean" and worked. Anyway, I will verify it.

Surely you are right, the bug is in the wrong retrieving of the registers. I should have to use

"[1] " & DllStructGetData($tRegisterData, 1, 1) & @CRLF & _
"[2] " & DllStructGetData($tRegisterData, 1, 2) & @CRLF & _

and so on to retrieve the register value.


Today I cannot test it, but I will verified as soon as possible. Thank you again.

Posted

You were right. Now I succeed to read the content of registers using

"[1] " & DllStructGetData($tRegisterData, 1, 1) & @CRLF & _
"[2] " & DllStructGetData($tRegisterData, 1, 2) & @CRLF & _

And now also works

Local $RTU_Result = DllCall($hDLL, "int:cdecl", "modbus_set_debug", "ptr", $pRTU_Ptr[0], "int", 1)

using an int parameter. Evidently I have been mistaken.

I will proceed to test other modbus functions, then I will code some Autoit functions to manage them at the best.
Than you very much for your help.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...