Modbus protocol expansion options: polling acceleration and a little bit about security

    image

    Modbus is, in fact, a generally accepted standard in automation systems for interacting with sensors, actuators, I / O modules and programmable logic controllers.

    In areas where an event model is required, it is gradually superseded by newer standards, such as IEC 60870-5-101 / 103/104, CANopen, DNP3 and the like, but due to its simplicity, the request-response model and the possibility of working in half-duplex mode and widely supported by a wide variety of equipment, Modbus is still frequently used.


    An important issue will be maintaining compatibility so that, for example, your controller can also be polled by any other standard OPC server, and not just your development. In the case of Modbus, this is easy because there are about 24 standard functions (of which half is usually used in practice at best), and the rest are either “user-defined” or reserved. At a minimum, functions from 65 to 72 and from 100 to 110 can be freely redefined for your needs, which we will do.

    Large packages


    According to the standard, the maximum amount of data in a packet is 253 bytes. However, no one forbids us to transfer more within our client-server system, and for this we don’t even have to redo anything much.
    Let's look at the usual Modbus request for function 0x3 (Read holding registers):

    01 03 FF 01 00 07 4B 44

    Device address, 3rd function, start register address, number of requested registers, CRC-16. For the number of requested registers in the ADU, 2 bytes are reserved, that is, nothing prevents you from requesting values ​​greater than 255 - the main thing is that both the polled device and the polling server support this feature.

    The answer is a little trickier

    01 03 FF 01 00 07 14 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E F2 56

    as you can see, one byte is added there (containing the length of the data in bytes), we won’t fit into it, and therefore we will have to implement a custom modbus function in which 2 bytes will be allocated for this field.

    Data compression


    The compression algorithm is required to be simple (since it will need to be implemented not only on the server, but also on the controller), and work well with small portions of data (from 16 to 1024 bytes).

    In our case, the RLE algorithm copes with these conditions very well, which is ridiculously simple, but it turned out to be very effective on data sets typical of PLC registers (in-order data from ADC channels, configuration settings, etc.). True, the classical implementation, which can be found on almost all sites with algorithms, is still not perfect, since it encodes only the number of repetitions, and if there are few repetitions, then the output buffer can turn out even more than the input one. Therefore, I use my implementation, which encodes not only the number of repetitions, but also the number of non-repetitions :)
    The simplified code is something like this:

    /**
         * @brief Процедура сжатия
         * @param in_buf указатель на массив сжимаемых данных
         * @param out_buf указатель на выходной массив сжатых данных (размером не менее maxclen)
         * @param len длина данных для сжатия
         * @param maxclen максимальная длина выходного массива.
         * @return объем данных в сжатом виде, либо -1 если сжатие вышло неээфективным
         */
    int compress(char *in_buf, char *out_buf, int len, int maxclen)
    {
        char *c_start = in_buf;
        char *c_curr = in_buf;
        int i;   
        int result_size = 0;
        char curr_type;
        char run_len = 1;
        if (*c_curr == *(c_curr+1))
            curr_type = 0;
        else
            curr_type = 1;
        while (((c_curr - in_buf) <= len) && (result_size < maxclen)) {
            if (
                 ( (*c_curr != *(c_curr+1)) && (curr_type == 1) ) ||
                 ( (*c_curr == *(c_curr+1)) && (curr_type == 0) ) &&
                 (run_len < 127) && ((c_curr - in_buf) <= len)
               )
            {
                c_curr++;
                run_len++;
            }
            else
            {
                if (curr_type == 0)
                {
                    *out_buf = run_len;
                    out_buf++;
                    *out_buf = *c_curr;
                    out_buf++;
                    c_curr++;
                    result_size = result_size + 2;
                }
                else
                {
                    run_len--;
                    if (run_len > 0)
                    {
                        *out_buf = 0x80 | run_len;
                        out_buf++;
                        for (i = 0; i <= run_len; i++)
                            *(out_buf + i) = *(c_start + i);
                        out_buf = out_buf + run_len;
                        result_size = result_size + run_len + 1;
                     }
                }
                c_start = c_curr;
                curr_type = curr_type ^ 1;
                run_len = 1;
            }
        }
        if (result_size >= maxclen)
            return -1;
        return result_size;
    }
    /**
         * @brief Процедура распаковки
         * @param in_buf указатель на массив разжимаемых данных
         * @param out_buf указатель на выходной массив данных (память должна быть выделена заранее)
         * @param len длина входных данных
         * @return объем распакованных данных
         */
    int decompress(char *in_buf, char *out_buf, int len)
    {
        char* c_curr = in_buf;
        char count;
        char size = 0;
        int i;
        while ((c_curr - in_buf) < len)
        {
            count = *c_curr & 0x7F;
            size = size + count;
            if (*c_curr & 0x80)
            {
                c_curr++;
                for (i = 0; i < count; i++)
                {
                    *out_buf = *c_curr;
                    c_curr++;
                    out_buf++;
                }
            }
            else
            {
                c_curr++;
                for (i = 0; i < count; i++)
                {
                    *out_buf = *c_curr;
                    out_buf++;
                }
                c_curr++;
            }
        }
        return size;
    }

    In compressed form, the data will look something like this:

    81 45 32 41 81 42 02 41

    First there is a byte that determines whether a block of repeating or non-repeating bytes follows it (the most significant bit 0x80 is active for non-repeating blocks, and vice versa), and then directly either the repeating byte itself or an array of non-repeating ones, and so on Further.

    The given code is simplified, “quick and dirty,” there is always room for doing better. It should be noted that this example compresses data in terms of bytes, and in the case of Modbus, it will sometimes be more reasonable to compress in terms of words (2 bytes each). It is easy to guess that the changes in the code will be minimal, and which of the options will suit you more depends on the characteristics of the data that you will compress.

    Incremental reading


    If polling happens very often, or vice versa, data in the controller is updated very rarely, then sometimes it makes sense to read only the changed blocks of the address space.
    The algorithm may be something like this:

    1. The server sends a request similar to the 0x3 or 0x4 modbus function, but also indicating the number of the last read (just an incremental counter)

    2. The controller checks if the number of the last read matches the same number in the controller (then if the previous answer was correctly delivered and processed), then XOR passes through the prepared register buffer for sending, comparing it with the same buffer saved from the previous request, after which it encodes only the changed data in approximately the following way

    <смещение от начала буфера><длина блока><блок данных>

    As you can see, the algorithm will be very similar to the RLE described above (we seem to believe that all the unchanged data is the same for us) and there is no magic here.
    When using this method, more or less frequently changing data (timers, signals from the ADC, etc.) must be located next to each other in the register map, and only then infrequently changing data (bit masks of the task status, discrete signals) can go separately in a grouped way inputs, etc.).

    A bit about security


    Modbus does not offer any technology for encryption and authentication. And from this it follows that if security is not an empty phrase for you, then it is better to use a more suitable protocol for this.

    It’s clear that no one will control critical systems through an open channel, but in my life I have probably seen a dozen systems that worked on RS-485 using a pair of wires thrown between objects across the road, and developers and users, did not even think that any passerby with a laptop and a converter, clinging to the line and knowing the register card can send any commands to objects on behalf of the server.

    The situation is complicated by the fact that often PLCs and embedded systems are limited in resources, and therefore it will not be so easy to put some more or less complex and reliable symmetric algorithm there, not to mention asymmetric ones.

    Therefore, when writing to registers (sending an executive command or changing a configuration), in addition to the register values ​​themselves, you can additionally forward, for example, an MD5 hash (the algorithm is simple, fast, and despite that, it found flaws in terms of collisions, in our case they do not play a serious role) from the data itself, from a certain “salt” (a key known only to the server and the controller, and it would be nice to provide for the rotation of the keys with a small frequency, and, for example, the current time stamp with an accuracy of several seconds, h Oba protect against replay-attacks. Protection is far from perfect, but a combination of factors may significantly affect the implementation of evil designs.

    Address Extension


    This solution is no longer so simple, and not technically (because it is just that simple), but rather, administratively, because, alas, compatibility is already breaking.

    If there is a need to interview more than 247 devices on one communication line or in the same frequency range, it is worth considering the option of switching to double-byte addresses. Some well-known controllers (for example, ScadaPack) support this solution out of the box.

    To be able to interrogate the device with a locally standard modbus scanner, you can either reset the device to single-byte mode, or reserve some address that will never be found in the high byte of extended addresses, and upon receipt of a packet with which the controller will always consider it as standard package.

    Also popular now: