Shop OBEX P1 Docs P2 Docs Learn Events
Modbus RTU slave — Parallax Forums

Modbus RTU slave

Starting to figure this Modbus stuff out.
Really want to do Modbus TCP over ethernet, but starting out with Modbus RTU over serial connection.

Being over serial connection makes debugging tricky, but using a 1.8" TFT to help...

Anyway, can now trick the Chipkin Modbus scanner into thinking there's a device attached with address 10. It tries probing all the addresses until you reply with no data. So, this replies to the first probe with data and the second probe with no data.

Copied the CRC code from FreeModbus Libary in C for now, but perhaps replace later with code that uses the P2 CRC hardware...

Comments

  • RaymanRayman Posts: 15,309

    Found what looks like good documentation here: https://modbus.org/docs/PI_MBUS_300.pdf

  • evanhevanh Posts: 16,528

    When I did that for the Prop1 I started from the Obex version - https://obex.parallax.com/obex/modbus-rtu/

  • evanhevanh Posts: 16,528

    I modified it a little. In particular I re-engineered command 04 (Read Multiple Input Registers) and made it use a reference table of compiled symbols so then the master could directly read a set of read-only variables without the slave's operations needing to go out of its way.

  • evanhevanh Posts: 16,528
    edited 2025-06-21 11:25

    Here it is. Rather poorly commented though.

    Oh, and it relied on a tweaked version of FullDuplexSerial that handled RS485 turnaround.

  • RaymanRayman Posts: 15,309

    Thanks @evanh definitely will use that CRC code.

  • JonnyMacJonnyMac Posts: 9,337
    edited 2025-06-21 14:57

    Copied the CRC code from FreeModbus Libary in C for now, but perhaps replace later with code that uses the P2 CRC hardware...

    I did a P1 MODBUS RTU project a long time ago, converted the CRC code to Spin2 (just needed to change the ROR operator), and then converted to inline PASM2 for speed. I have verified against online CRC calculators.

    pub v1_crc16(p_buf, len) : crc
    
    '' Calculate 16-bit CRC for MODBUS RTU
    
      crc := $FFFF                                                  ' initialize crc
    
      repeat len                                                    ' loop through frame bytes
        crc ^= byte[p_buf++]                                        ' apply frame byte
        repeat 8                                                    ' loop through low bits
          crc ror= 1                                                ' rotate right
          if (crc & $8000_0000)                                     ' carry?
            crc ^= $A001                                            ' yes, apply poly
          crc &= $FFFF                                              ' clear carry
    
    
    pub v2_crc16(p_src, n) : crc | b
    
    '' Calculate 16-bit CRC for MODBUS RTU
    
      org
                            mov       crc, ##$FFFF
                            mov       ptrb, p_src
    
    .loop                   rdbyte    b, ptrb++
                            rev       b
                            setq      b
                            crcnib    crc, ##$A001
                            crcnib    crc, ##$A001
                            djnz      n, #.loop
      end
    
  • RaymanRayman Posts: 15,309

    @JonnyMac Ok, have to try that.

    Got hung up once again on ser.rx() vs ser.rxcheck()...
    Spent too much time figuring out why these don't work the same:
    ```
    Pub GetC():c
    repeat
    c:=ser.rxcheck()
    if (c>0)
    quit
    return c

    Pub GetC2():c
    c:=ser.rx()
    ```

    Guess first one would work in usual cases, but here the returned byte is often 0, and that breaks it... Needs to be (c>=0).

    Hopefully, this sticks in my brain longer this time...

  • JonnyMacJonnyMac Posts: 9,337
    edited 2025-06-21 17:13

    Since Chip's first incarnation of fullduplexserial, the rxcheck() method returns -1 to indicate there is nothing available so, yes, we need to allow 0 as a valid byte. It's nice because it's non-blocking (unline rx() which is). In some of my serial libraries I have added a method called available() to that returns the number of bytes available in the RX buffer.

  • RaymanRayman Posts: 15,309

    Guess an IsAscii() function is another way . But better memory is what is really needed…

  • RaymanRayman Posts: 15,309

    So, the modbus scanner is very sensitive to response timing.
    Think figured out that it somewhat follows the 3.5 character minimum delay mentioned in the modbus.org document.

    But, there also seems to be a maximum delay, so it has to be just right.
    Anyway, need to be able to code the response with CRC within that delay, it seems.

    At 9600 baud, the scanner seemed happy with a delay of ~10 ms.
    At 115200 baud, seems happy with ~80 us.

    This is not exactly linear, so not sure what is going on here.
    Thinking timing is just going to have to baud table lookup.

    Might be best to just stick with one baud for now.
    Will see if 80 us is enough time to calc CRC for general case.

    There is a "timeout" setting in the scanner that seems to range from 3 to 30. But, don't want to mess with that for now...

  • @JonnyMac said:
    [...] and then converted to inline PASM2 for speed.

    But that's not very fast, 'innit?

    pub v3_crc16(p_src, n) : crc | b
    
    '' Calculate 16-bit CRC for MODBUS RTU
    
      org
                            bmask     crc, #15 ' set to $FFFF
                            loc       pa, #\$A001
                            rdfast    #0, p_src
    
                            rep       @.loop, n   
                            rfbyte    b
                            rev       b
                            setq      b
                            crcnib    crc, pa
                            crcnib    crc, pa
    .loop
      end
    
  • JonnyMacJonnyMac Posts: 9,337
    edited 2025-06-22 15:11

    V3 is about 16% faster than V2. That may help with higher baud rates (timing is in system ticks [@200MHz]).

  • MicksterMickster Posts: 2,782

    @Wuerfel_21 said:

    But that's not very fast, 'innit?

    :D:D

    Thought you were German(?) How do you know this Brit stuff?

  • @JonnyMac said:
    V3 is about 16% faster than V2. That may help with higher baud rates (timing is in system ticks [@200MHz]).

    With a small packet the function call / FCACHE overhead starts to dominate the runtime. On a larger one it'd be 2x or so.

    @Mickster said:

    Thought you were German(?) How do you know this Brit stuff?

    Cultural osmosis. As a second-language English speaker you pick up all sorts of phrases.

  • JonnyMacJonnyMac Posts: 9,337

    If we believe the internet, a typical MODBUS RTU packet is 8-12 bytes. At 12 bytes the V2 to V3 improvement is about 25%.

    On the extreme is a 256-byte packet, which is not likely to be very common. That said, V3 is a big improvement in that case.

  • RaymanRayman Posts: 15,309

    Thanks @Wuerfel_21 ! Is it safe to use PA in FlexProp and PropTool with inline assembly? Guess so. Isn't there something about not using ptra?
    Should keep better notes on that kind of stuff...

  • @Rayman said:
    Thanks @Wuerfel_21 ! Is it safe to use PA in FlexProp and PropTool with inline assembly? Guess so. Isn't there something about not using ptra?
    Should keep better notes on that kind of stuff...

    Yes, can't use PTRA, but the other registers are fine (as long as you don't expect them to keep their values afterwards)

  • RaymanRayman Posts: 15,309
    edited 2025-06-22 17:15

    @Wuerfel_21 Ok, great. Just tested V3 and works with modbus scanner.

    The modbus scanner will sometimes not be 100% "good" with P2 replies on first pass. Better, if not perfect on later passes... Not sure how bad that is.
    Going to work on better timing control and see if can improve that...

    There may be some latency going through the FTDI adapter that can't be controlled. Although, maybe can be improved in device manager...

  • JonnyMacJonnyMac Posts: 9,337
    edited 2025-06-22 22:18

    @Rayman Since your version 1e code uses jm_fullduplexserial, you could take advantage of the rx_tix() method to detect the timeout of the master message.

    pub check_query(p_buf) : len | b
    
      repeat
        b := ser.rx_tix(CTO_TIX)                                    ' check with character timeout
        if (b < 0)                                                  ' if timeout
          quit                                                      '   done
        else                                                        ' else
          byte[p_buf][len++] := b                                   '   get char and move to buffer, bump len
    

    p_buf is a pointer to your message array, and the method returns the number of bytes in the message (0 when no message is available). CTO_TIX is the character timeout period in system ticks for your baud rate. One getting a valid message you'd set a timer for responding, build the message, and then send when the timer is expired. Maybe something like this:

      if (check_query(@buffer))
        rspticks := getct() + RSP_DELAY
        ' build message
        repeat until pollct(rspticks)
        ' send response
    

    Of course, RSP_DELAY is the hold-off time between the incoming query and the sending the response.

  • RaymanRayman Posts: 15,309

    @JonnyMac Have noticed things like that in the driver. Need to look into these things:

    pub rxtime(ms) : b | mstix, t
    pub rxtix(tix) : b | t
    pub available() : count
    

    Hoping that the Chipkin modbus scanner will let me know if have the timing right...

  • RaymanRayman Posts: 15,309

    The modbus.org website seems to have some good info.
    Did notice this:

    Modbus Organization Replaces Master-Slave with Client-Server
    The Modbus Organization Board of Trustees announces it is expunging all occurrences of inappropriate language of the query and response paradigm of Modbus communications. All instances of "master-slave" in the organization's literature and on its website will be removed.
    

    Just like with SPI, people notice that the master/slave verbage is perhaps not the best and come up with alternatives. But, nobody really changes?
    See they decided that in 2000, and yet documents from 2006 still refer to master/slave...

  • RaymanRayman Posts: 15,309

    This document here is still looking to be all that is really needed: https://modbus.org/docs/PI_MBUS_300.pdf
    Looking at what function codes need to implemented...

    The scanner probes functions 1..4. Think all that is really needed for this application is 1 and 5. Maybe 4 though too...

    Trying to decide if need to reply to other function calls.

    567 x 631 - 67K
  • RaymanRayman Posts: 15,309

    Just found the section on "Exception Response". This is just what was looking for...
    Now, can respond normally to however many coils or registers want to implement and reply with one of these exceptions for anything else.

    Although, the scanner seemed happy with just replying with 0 in the #bytes field in the reply. This might be a better way...

  • evanhevanh Posts: 16,528
    edited 2025-06-23 06:03

    Certainly don't have to implement more than you want to. The user config on the master is only going to request what's usable in the slave. Thereby not triggering non-functional Modbus commands to that slave.

  • RaymanRayman Posts: 15,309

    Wishing that "read coil status" requests would be 8-bit aligned and in quantities of 8 bits.
    But, the example shows something else. Guess it's not a huge amount of work to implement, just inconvenient...

    646 x 645 - 56K
  • evanhevanh Posts: 16,528
    edited 2025-06-23 09:09

    Well, you could just round it up to next size of eight bits. Since you're using the binary data format it is always going to be 8 bit bytes in your reply. The master will do any truncating it wants to after the CRC has been validated.

Sign In or Register to comment.