@Rayman said:
Guess had a 50/50 shot at getting byte order correct and of course was wrong...
Yeah, Modbus is something of a dog's breakfast when it comes to consistency. When reading the docs it's apparent it had distinctly different authors between the original and later extensions. It shows the likely ages it was written. Before the PC became dominant Big Endian was seen as the right way but after, then Little Endian gets preferential treatment.
Ray: I noticed that you're using jm_fullduplexserial in your code. Are you going through an RS-485 converter? If yes, are you using a 4-wire connection? I don't see any mechanism in your code to enable the transmit of a '485 chip.
Your MODBUS work inspired me to get an USB-to-RS485 adapter and give it a go. I already had a RS-485 driver for the P2 which is [mostly] compatible with my other serial drivers (though it lacks [unneeded] formatting methods). My friend John at JonyJib uses the RS-485 driver is his projects, though in that case, we have a custom protocol (that looks a lot like XBee messages). That is to say I think the RS-485 driver is solid.
Over the past several nights I started chipping away. I liberated some bits and bobs from your project; maybe you'll find something useful here. ATM it is a framework for a MODBUS slave; command 2 (read inputs) works. I have a 4-button board that attaches to an Eval (my version of the Parallax Control accessory). I can press any of the buttons and the scanner shows the message working. On that board I have an APA102c. If you find a piece of shareware that lets you write registers I will use that command to set the R, G, and B levels of the pixel.
Was using ChatGPT as an assistant while programming and asked about a piece of freeware for MODBUS testing. It recommended QModMaster. It's easy, if a bit annoying in that it sends a message any time you change a control, but it does allow you to use the write commands. I updated my program to use the Write Register ($06) and Write Registers ($10) commands.
'defaults for Copeland VFD's
Const bitrate=19200 'communication speed
ID=45 'default modbus id
'init system for ZERO using gp0/gp1/gp2
'open COM1 port with modbus defaults 19200/8bit/even parity/1 stop
setpin gp1, gp0, COM1 'rx,tx,port
setpin gp2,dout : pin(gp2)=0 'DE pin is GP2
Open "COM1:"+Str$(bitrate)+",EVEN" As #1
device ws2812 o,gp16,1,pur% 'gp16 is connected to the RGB color LED
dim oe$ = "" , e$ = ""
timout!= 2 * 11 * 1000 / bitrate 'message end = 2 characters, 11 bits each, in ms
timer=0
'snoop bus
do
timestamp!=timout!+timer
do
if loc(1) then e$=e$+Input$(Loc(1),#1): timestamp!=timout!+timer
loop until timer>timestamp! 'end of message when no new character for 22 bittime
if len(e$)>0 then prtx e$ : oe$ = e$ : e$ = ""
loop
'prints the string from UART in readable form on console
Sub prtx a$
Local i
print gr$;str$(timer/1000,4,3,"0");" : ";wh$;
For i=1 To Len(a$)
If i=2 Then Print choice(Asc(Mid$(a$,i,1))>127,rd$,bl$);
If i=Len(a$)-1 Then Print choice(check(a$),wh$,rd$);
Print Right$("0"+Hex$(Asc(Mid$(a$,i,1))),2);" ";
'separate CRC from message
cr$=Right$(a$,2)
crc_mes=256Asc(Right$(cr$,1))+Asc(Left$(cr$,1))
'compare CRC's -> when equal, message is okay.
If crc_mes=crc_pd Then
check=1 : device ws2812 o,gp16,1,grn%
else
check=0 : device ws2812 o,gp16,1,red%
end if
else
device ws2812 o,gp16,1,pur%
End If
The write multiple coils and write multiple registers seems a little dangerous to me...
System working on now also has local control buttons, 7 on and 7 off buttons, that basically control 7 "coils", in PLC jargon.
Write Multiple Coils would be like if somebody just started jamming several buttons at the same time.
Doesn't seem like something you'd want to encourage...
Of those two tasks, Write Multiple Coils seems the most straightforward. Even then it seems like the app has to do a lot of bounds checking to ensure there are no problems. For the P2 I figured the maximum write count would be 32 (for pinwrite) and implemented as below. Now... I've only tested with the P56 and P57 LEDs on the P2, but it does seem to work.
I added the ofs, first, and last parameters so that the routine could be called from a higher-level dispatch routine in the event an application has multiple groups of coils outputs.
pub write_coils() | ofs ' MODBUS command 15 (W_COILS)
ofs := get_reg(@rxbuf, 2) ' get first coil offset
' Dispatch wr_coils() based on offset in the command message
' -- first parameter in wr_coils is offset within the specific group
' * group offset is message offset minus first offset of group
case ofs
00..01 : wr_coils(ofs-00, LED1, LED2) ' 00001..00002 --> P56..P57
10..17 : wr_coils(ofs-10, 0, 7) ' 00011..00018 --> P00..P07
other : exception_response(EC_ADDR)
pri wr_coils(ofs, first, last) | cmax, count, lsb, msb, b, outbits, crc
'' Write up to 32 coils
'' -- ofs is offset within defined pin group
'' -- first and last designate IO pin bounds of group
cmax := last - first + 1 ' pins in the group
count := (rxbuf[4] << 8) | rxbuf[5] ' # of coils to write
if (count < 1) || (count > cmax) ' validate
exception_response(EC_VALUE)
return
lsb := first + ofs ' lsb output pin
msb := lsb + count - 1 ' msb output pin
if (lsb > last) || (msb > last)
exception_response(EC_VALUE)
return
b := rxbuf[6] ' # of bytes in outbits
if (b < 1) || (b > 4)
exception_response(EC_VALUE)
return
outbits := 0 ' get coils status
bytemove(@outbits, @rxbuf[7], b) ' Little Endian!
pinwrite(lsb addpins (count-1), outbits) ' refresh the coils
if (rxbuf[0] == BROADCAST)
return
bytemove(@txbuf, @rxbuf, 6) ' copy front of command
crc := calc_crc(@txbuf, 6) ' calc crc of reply
bytemove(@txbuf[6], @crc, 2) ' add crc (Little Endian)
send_msg(8) ' reply to master
show_reply(8)
I was up way too late playing with this, but it is a bit of fun.
I finally took a friend's advice and am using ChatGPT when working on this. Given MODBUS has been around for such a long time and is well-documented, it's nice to ask it for examples and clarification. Give it a try if you haven't already.
Sorry to be a distraction from the main topic but where I am working has several devices that support Modbus over Serial so I am interested in any P1 code.
It's strange, but think works.
When you do "New" you can get to that main window than will continuously scan functions 1, 2, 3, or 4.
Here, doing 1 (Read Coil Status)
Then, can do Setup->Extended->User Msg.
This brings up the top window where you can pick things like 5 (Write Single Coil).
With this open, you can send 4 bytes to set a coil and then see it change in the back window (doing read coil status).
The first two bytes are the address and the next two bytes are the value.
If the value is "FF 00" then the coil is set to 1, anything else sets the coil to 0.
It seems that it injects this function 5 in between the continuous calls to function 1...
Guess that's OK.
So, this (although a bit clunky), seems to do everything needed to exercise the code.
There is also a "Modbus Scanner". This defaults to function 17 (Report Slave ID), but can be changed to function 1.
This has me thinking that Report Slave ID is something that should be implemented.
Guess this could be like a virtual serial number for the device...
Hmm... Looks like function 17 (Report Slave ID) return value is more or less arbitrary. There are a few examples here: https://modbus.org/docs/PI_MBUS_300.pdf
Might just have it answer with something random for now...
ON/OFF state is interesting though, might include that.
Got the Modbus scanner part of Open ModScan to be happy with response from the default scan of "17: REPORT SLAVE ID".
It's a bit annoying that the scanner has a separate serial interface than the rest of it, so you can't have both connected at the same time.
Also, closing the serial port has the also annoying affect of rebooting the P2...
But, am thinking this is where it needs to be now.
Guess should actually implement CRC check on incoming data.
That's a bit silly here because the USB interface is already doing that, but would be good in case were an actual RS232 connection...
I decided to follow your lead and add handling of command 17 to my MODBUS Slave framework.
Guess should actually implement CRC check on incoming data.
I am testing with a USB-to-RS485 adapter on my PC and my own RS-485 board on the P2, so I've been doing this. I do it at the top level so that it doesn't have to be checked by individual routines.
crc := calc_crc(@rxbuf, len-2)
if (crc == extract_crc(@rxbuf, len)) ' if good message
process_request() ' reply to master
As an experiment, I decided to return the slave id, run status, and three bytes from my firmware version number.
pub read_slave_id() | crc
if (rxbuf[0] == BROADCAST)
return
txbuf[0] := myaddr
txbuf[1] := $11
txbuf[2] := 5
txbuf[3] := MY_ID
txbuf[4] := (devStatus) ? $FF : $00
txbuf[5] := VERSION / 100 ' major
txbuf[6] := VERSION // 100 / 10 ' minor
txbuf[7] := VERSION // 10 ' bug fix
crc := calc_crc(@txbuf, 8)
bytemove(@txbuf[8], @crc, 2)
send_msg(10) ' reply to master
show_reply(10)
I'm using QModMaster which also has a separate dialog for the slave ID response, and it doesn't show the additional bytes past the device status.
This is a guess, but the response seems to be structured in a way that the master could only care about the ID and the status. I wonder if cmd 17 is really intended in systems where the master and slaves are from the same vendor, hence the master would know what to do with the additional data.
I know this is test code and under development, but with the CRC being Little Endian, you can so this:
Comments
Yeah, Modbus is something of a dog's breakfast when it comes to consistency. When reading the docs it's apparent it had distinctly different authors between the original and later extensions. It shows the likely ages it was written. Before the PC became dominant Big Endian was seen as the right way but after, then Little Endian gets preferential treatment.
Eg: There is a 32-bit extension for register sizes.
Ray: I noticed that you're using jm_fullduplexserial in your code. Are you going through an RS-485 converter? If yes, are you using a 4-wire connection? I don't see any mechanism in your code to enable the transmit of a '485 chip.
Just using regular serial via FTDI USB chip, nothing special.
Although in future will probably be Modbus TCP. Although maybe a serial connection to something nearby might be useful, who knows...
Your MODBUS work inspired me to get an USB-to-RS485 adapter and give it a go. I already had a RS-485 driver for the P2 which is [mostly] compatible with my other serial drivers (though it lacks [unneeded] formatting methods). My friend John at JonyJib uses the RS-485 driver is his projects, though in that case, we have a custom protocol (that looks a lot like XBee messages). That is to say I think the RS-485 driver is solid.
Over the past several nights I started chipping away. I liberated some bits and bobs from your project; maybe you'll find something useful here. ATM it is a framework for a MODBUS slave; command 2 (read inputs) works. I have a 4-button board that attaches to an Eval (my version of the Parallax Control accessory). I can press any of the buttons and the scanner shows the message working. On that board I have an APA102c. If you find a piece of shareware that lets you write registers I will use that command to set the R, G, and B levels of the pixel.
(Program updated. See below)
Was using ChatGPT as an assistant while programming and asked about a piece of freeware for MODBUS testing. It recommended QModMaster. It's easy, if a bit annoying in that it sends a message any time you change a control, but it does allow you to use the write commands. I updated my program to use the Write Register ($06) and Write Registers ($10) commands.
Qmodmaster looks good. Have to try that. Most seem to only handle commands 1..4. This looks to do more.
@Rayman
Don't know if there's anything of any use to you in the attached but you never know.
Also "Modbus Spy"
The write multiple coils and write multiple registers seems a little dangerous to me...
System working on now also has local control buttons, 7 on and 7 off buttons, that basically control 7 "coils", in PLC jargon.
Write Multiple Coils would be like if somebody just started jamming several buttons at the same time.
Doesn't seem like something you'd want to encourage...
Of those two tasks, Write Multiple Coils seems the most straightforward. Even then it seems like the app has to do a lot of bounds checking to ensure there are no problems. For the P2 I figured the maximum write count would be 32 (for pinwrite) and implemented as below. Now... I've only tested with the P56 and P57 LEDs on the P2, but it does seem to work.
I added the ofs, first, and last parameters so that the routine could be called from a higher-level dispatch routine in the event an application has multiple groups of coils outputs.
I was up way too late playing with this, but it is a bit of fun.
I finally took a friend's advice and am using ChatGPT when working on this. Given MODBUS has been around for such a long time and is well-documented, it's nice to ask it for examples and clarification. Give it a try if you haven't already.
ChatGPT chat on Write Multiple Coils
-- https://chatgpt.com/share/685dfa0d-03ac-8006-933c-5cd6ee74cff3
Sorry to be a distraction from the main topic but where I am working has several devices that support Modbus over Serial so I am interested in any P1 code.
@Genetix Think there might be a modbus thing for P1 in OBEX.
I linked it at post #3.
Just tried that Open Mod Scan: https://github.com/sanny32/OpenModScan
It's strange, but think works.
When you do "New" you can get to that main window than will continuously scan functions 1, 2, 3, or 4.
Here, doing 1 (Read Coil Status)
Then, can do Setup->Extended->User Msg.
This brings up the top window where you can pick things like 5 (Write Single Coil).
With this open, you can send 4 bytes to set a coil and then see it change in the back window (doing read coil status).
The first two bytes are the address and the next two bytes are the value.
If the value is "FF 00" then the coil is set to 1, anything else sets the coil to 0.
It seems that it injects this function 5 in between the continuous calls to function 1...
Guess that's OK.
So, this (although a bit clunky), seems to do everything needed to exercise the code.
There is also a "Modbus Scanner". This defaults to function 17 (Report Slave ID), but can be changed to function 1.
This has me thinking that Report Slave ID is something that should be implemented.
Guess this could be like a virtual serial number for the device...
Hmm... Looks like function 17 (Report Slave ID) return value is more or less arbitrary. There are a few examples here: https://modbus.org/docs/PI_MBUS_300.pdf
Might just have it answer with something random for now...
ON/OFF state is interesting though, might include that.
Seems like it's for vendor and -- as you indicated -- on/off status information.
-- https://chatgpt.com/share/68923df1-ffc4-8006-974d-3956bb442e96
Got the Modbus scanner part of Open ModScan to be happy with response from the default scan of "17: REPORT SLAVE ID".
It's a bit annoying that the scanner has a separate serial interface than the rest of it, so you can't have both connected at the same time.
Also, closing the serial port has the also annoying affect of rebooting the P2...
But, am thinking this is where it needs to be now.
Guess should actually implement CRC check on incoming data.
That's a bit silly here because the USB interface is already doing that, but would be good in case were an actual RS232 connection...
I decided to follow your lead and add handling of command 17 to my MODBUS Slave framework.
I am testing with a USB-to-RS485 adapter on my PC and my own RS-485 board on the P2, so I've been doing this. I do it at the top level so that it doesn't have to be checked by individual routines.
As an experiment, I decided to return the slave id, run status, and three bytes from my firmware version number.
I'm using QModMaster which also has a separate dialog for the slave ID response, and it doesn't show the additional bytes past the device status.
@JonnyMac Yeah, should probably add more things to READ SLAVE ID reply, but will save that for later.
Did just add CRC check on incoming data though.
This is a guess, but the response seems to be structured in a way that the master could only care about the ID and the status. I wonder if cmd 17 is really intended in systems where the master and slaves are from the same vendor, hence the master would know what to do with the additional data.
I know this is test code and under development, but with the CRC being Little Endian, you can so this:
Get CRC from incoming message
Put CRC onto outgoing message: