:::: MENU ::::

Modbus RTU Server (Slave)

Довольно часто программисту встраиваемых систем приходится работать с протоколом Modbus RTU или Modbus через Ethernet. Это клиент-серверный протокол. На встраиваемом устройстве может работать как клиент (шлет запросы), так и сервер (отвечает на запросы).

Далее приведено описание моей реализации modbus сервера (slave) для микроконтроллеров (в принципе ее можно использовать и на ПК).

Для полного понимания, как работает modbus можно почитать этот документ-Modbus RTU. Кроме того, для подсчета CRC16 для modbus RTU можно использовать этот алгоритм.

Команды, которые будут использоваться:

  • 03 — запрос данных группы регистров;
  • 06 — запись данных в один регистр;
  • 16 — запись данных в группу регистров;

cmd 3cmd 6cmd 16

Для программной реализации modbus создадим структуры:

// группа регистров - последовательно расположенные регистры
// каждая группа отделена промежутком modbus адресов и имеет свой уровень доступа
#define NUMBER_GROUP 4                    // количество групп регистров
#define NUMBER_GROUP_4 1

typedef struct 
{
    uint32_t rxcnt;                        // количество принятых байт в modbus буфере
    uint8_t buffer[256];                   // modbus буфер для хранения, анализа и модификации modbus данных
    uint16_t tr_len;                       // количество байт для передачи из modbus буфера
    uint8_t crc_error;                     // флаг указывает об ошибке CRC
    uint16_t adr_end[NUMBER_GROUP];        // массив содержащий конечный адрес каждой группы регистров 
    uint16_t adr_begin[NUMBER_GROUP];      // массив содержащий начальный адрес каждой группы регистров 
    uint8_t not_read_flag[NUMBER_GROUP];   // у каждой группы регистров есть разрешение/запрещение чтения ее
    uint8_t not_write_flag[NUMBER_GROUP];  // у каждой группы регистров есть разрешение/запрещение записи в нее
    uint8_t adr_dev;                       // modbus адрес устройства
} modbus_init;

// физические адреса (в памяти микроконтроллера) modbsu групп регистров
typedef struct 
{
    uint8_t* pB; 
} adr_block_s;

// сами modbus переменные разбитые на группы регистров 
#pragma pack(push, 1)  // для выравнивания по 1 байту (!!обязательно!!)
typedef struct 
{
    uint16_t x0;
    uint32_t y0;
} group_0_s;
#pragma pack(pop)

#pragma pack(push, 1)  // для выравнивания по 1 байту (!!обязательно!!)
typedef struct 
{
    uint16_t x1;
    uint32_t y1;
} group_1_s;
#pragma pack(pop)

#pragma pack(push, 1)  // для выравнивания по 1 байту (!!обязательно!!)
typedef struct 
{
    uint16_t x2;
    uint32_t y2;
} group_2_s;
#pragma pack(pop)

#pragma pack(push, 1)  // для выравнивания по 1 байту (!!обязательно!!)
typedef struct 
{
    uint16_t x3;
    uint32_t y3;
} group_3_s;
#pragma pack(pop)

В самом начале необходимо проинициализировать modbus (заполнить все поля modbus структуры, которые этого требуют):

// начальные адреса modbus групп
#define ADR_GROUP_0 1000    
#define ADR_GROUP_1 1010
#define ADR_GROUP_2 1013
#define ADR_GROUP_3 1016

modbus_init modbus;

adr_block_s adr_block[NUMBER_GROUP];

group_0_s group_0;
group_1_s group_1;
group_2_s group_2;
group_3_s group_3;


// флаги запрещения/разрешения записи и чтения каждой группы начиная с 1

//                    1 2 3 4 
#define NOT_ADR_READ {0,0,0,0}      // 1 - запрет чтения

//                     1 2 3 4 
#define NOT_ADR_WRITE {0,0,0,0}     // 1 - запрет записи

#define ADR_DEVICE 1                // modbus адрес устройства


void init_modbus(void)
{
    uint8_t i;
    uint16_t adr_begin[4] = {ADR_GROUP_0, 
                        ADR_GROUP_1, ADR_GROUP_2, ADR_GROUP_3};  

    uint8_t not_read_flag[4] = NOT_ADR_READ;    
    uint8_t not_write_flag[4] = NOT_ADR_WRITE;

    modbus.adr_dev = ADR_DEVICE;

    for(i = 0; i < NUMBER_GROUP; i++)
    {
        modbus.adr_begin[i] = adr_begin[i];
        modbus.not_read_flag[i] = not_read_flag[i];
        modbus.not_write_flag[i]    = not_write_flag[i];
    }

    // рассчитываем конец modbus адресов для каждой группы
    // и указываем физические адреса переменных для каждой группы

    #ifdef NUMBER_GROUP_1
    modbus.adr_end[0] = sizeof(group_0)/2 + modbus.adr_begin[0];
    adr_block[0].pB = (uint8_t*)&group_0;
    #endif


    #ifdef NUMBER_GROUP_2
    modbus.adr_end[0] = sizeof(group_0)/2 + modbus.adr_begin[0];
    modbus.adr_end[1] = sizeof(group_1)/2 + modbus.adr_begin[1];
    adr_block[0].pB = (uint8_t*)&group_0;
    adr_block[1].pB = (uint8_t*)&group_1;
    #endif


    #ifdef NUMBER_GROUP_3
    modbus.adr_end[0]= sizeof(group_0)/2 + modbus.adr_begin[0];
    modbus.adr_end[1]= sizeof(group_1)/2 + modbus.adr_begin[1];
    modbus.adr_end[2]= sizeof(group_2)/2 + modbus.adr_begin[2];
    adr_block[0].pB = (uint8_t*)&group_0;
    adr_block[1].pB = (uint8_t*)&group_1;
    adr_block[2].pB = (uint8_t*)&group_2;
    #endif


    #ifdef NUMBER_GROUP_4
    modbus.adr_end[0] = sizeof(modbus_temp.group_0)/2 + modbus.adr_begin[0];
    modbus.adr_end[1] = sizeof(modbus_temp.group_1)/2 + modbus.adr_begin[1];
    modbus.adr_end[2] = sizeof(modbus_temp.group_2)/2 + modbus.adr_begin[2];
    modbus.adr_end[3] = sizeof(modbus_temp.group_3)/2 + modbus.adr_begin[3];
    adr_block[0].pB = (uint8_t*)&(modbus_temp.group_0);
    adr_block[1].pB = (uint8_t*)&(modbus_temp.group_1);
    adr_block[2].pB = (uint8_t*)&(modbus_temp.group_2);
    adr_block[3].pB = (uint8_t*)&(modbus_temp.group_3);
    #endif
}

Вам необходимо написать драйвер для вашего физического канала (обычно RS-232, RS-485), который будет принимать modbus сообщения и разделять их (сообщение должно начинаться и заканчиваться интервалом тишины, длительностью не менее 3,5 символов при данной скорости передачи; Во время передачи сообщения не должно быть пауз длительностью более 1,5 символов).

Когда мы приняли по физическому каналу modbus сообщение, вызывается функция modbus_slave, которая переписывает их в modbus буфер, проверяет CRC в зависимости от тех или иных результатов вызывает ту или иную дальнейшую обработку (command_3, command_6command_16, error).

void modbus_slave(void)
{   
    uint8_t tmp_2; 
    uint16_t tmp; 
    uint8_t number_buf;

    // тем или иным способом копируем все modbus сообщение в modbus.buffer
    // если у вас сразу копируется в этот буфер пропустите ее
    copy_to_modbus_byf();

    // проверка что идет запрос к нашему modbus адресу
    if( modbus.buffer[0] == modbus.adr_dev )
    {
        tmp = crc16(modbus.buffer, (modbus.rxcnt - 2)); // расчет CRC16 (см. ст. ...)
        // CRC16 - 2 байта, разделяем байты для сравнения
        tmp_2 = tmp >> 8;           // старший байт
        tmp = tmp & 0x00ff;         // младший байт

        // сравнение расчетного CRC и CRC в сообщение
        if( (tmp == modbus.buffer[modbus.rxcnt - 1]) && (tmp_2 == modbus.buffer[modbus.rxcnt - 2]))
        {
            // 2 байт в сообщении это команада - проверка команды
            switch(modbus.buffer[1])
            {
                case 3:  // запрос данных группы регистров
                    command_3();
                break;

                case 6: // запись данных в один регистр
                    command_6();
                break;

                case 16: // запись данных в группу регистров
                    command_16();
                break;

                default:
                    error(); // неправильная команда
            }
        }

        // CRC не верный
        else
            modbus.tr_len = 0;
            modbus.crc_error = 1;                   
    }

    // не верный modbus адрес устройства - ответа нет
    else
    {
        modbus.tr_len = 0;
        modbus.crc_error = 1;   
    }
}

Поскольку обработка команд выполняется практически по идентичному алгоритму, рассмотрим только обработку command_3:

void command_3(void)
{
    uint16_t reg_l;   // младший байт начального адреса регистра
    uint16_t reg_h;   // старший байт начального адреса регистра
    uint16_t reg;     // адрес регистра, вычисляется по reg_h, reg_l 
    uint16_t N_reg;   // количество байт для записи в modbus регистры (из modbus шапки)

    uint8_t i;
    uint16_t n; 
    uint16_t m; 
    uint8_t offset;

    uint16_t tmp_val_h;     // для копирования из буфера
    uint16_t tmp_val_l;     // для копирования из буфера

    uint16_t crc;   
    uint16_t crc_h; 
    uint16_t crc_l; 


    // начальный адрес регистра с которого читать   
    reg_h = modbus.buffer[2];
    reg_l = modbus.buffer[3];
    reg =  (reg_h << 8) + reg_l;

    // 6й байт в modbus сообщении содержит количество считываемых регистров
    N_reg = modbus.buffer[5];           

    // проверка возможности прочитать регистры 
    for(i = 0; i <= NUMBER_GROUP; i++)
    {
        if(reg >= modbus.adr_begin[i] && reg <= modbus.adr_end[i]) 
            break;
    }

    // если нет такого адреса в группах регистров или запрещено чтение
    if(i == NUMBER_GROUP || modbus.not_read_flag[i] == 1 ||
       (reg + N_reg) > modbus.adr_end[i])
    {
        error();
    }

    // все нормально - читаем регистры
    else
    {
        m = 3; // данные в ответа modbus начинаются с 4 байта поэтому m=3
        offset = 0;
        reg = reg - modbus.adr_begin[i];

        for(n = 0; n < N_reg; n = n + 1)
        {
            // получаем физический адрес
            // 1 регистр 2 байта поэтому reg умножаем на 2 и считываем 2 последовательных байта
            tmp_val_h =  *(adr_block[i].pB + reg*2 + offset);
            tmp_val_l = *(adr_block[i].pB + reg*2 + offset + 1);

            offset = offset + 2;

            // переписываем обратно в modbus буфер
            modbus.buffer[m] = tmp_val_l;
            modbus.buffer[m+1] = tmp_val_h;

            // переписали 2 байта -> прибавляем 2 для смещения в буфере 
            m = m + 2;  
        }

        n = n * 2; // количество считанных байт данных
        modbus.tr_len = n + 5; // количество считанных байт данных  + 5 служебных байт

        modbus.buffer[2] = n;  // 3й байт при ответе, это количество передаваемых байт  

        // расчет CRC16 для передаваемых данных
        crc = crc16(modbus.buffer, (modbus.tr_len - 2) );
        crc_h = crc >> 8;
        crc_l = crc & 0x00ff;


        // переписываем CRC16 в буфер для передачи
        modbus.buffer[modbus.tr_len - 2] = crc_h;
        modbus.buffer[modbus.tr_len - 1] = crc_l;
    }
}

После того как мы сформировали буфер modbus.buffer для ответа его нужно отправить обратно клиенту (master’у) по физическому каналу.


 

P.S. ___ Код выше собран из двух моих проектов (в каждом из них много лишнего для объяснения, и я это повыбрасывал для простоты понимания), и его я не тестировал (не запускал именно такой код), поэтому в нем могут быть ошибки. Но для общего понимания, как просто и быстро реализовать modbus RTU в самый раз.