Реализация Modbus RTU Slave на tms320

Рано или поздно (лучше рано) в устройство необходимо «впихнуть» связь с внешним миром.
Можно конечно ограничится обычным выводом данных в UART, но представьте, что будет с тем, кто захочет их разобрать.
Слава богу уже давным давно все придумано и нам остается только подстроится.
Modbus — это открытый протокол, который обычно натягивают поверх RS-232/485.
Весь фокус в том, что формат посылки стандартизирован и вы можете предоставить карту регистров вашего устройства. И при этом все остальные запросто организуют связь с вашим устройством.

В случае записи Modbus предполагает, что такой формат посылки:

  • 1Байт адреса устройства
  • 1Байт функции
  • 2Байта адреса регистра
  • 2Байта данных
  • 2Байта CRC

В случае чтения, 2Байта данных имеют смысл количества читаемых регистров.

Modbus предполагает, что устройства висят на одной шине. При этом есть одно ведущее устройство и множество ведомых. Ведомые устройства не могут вещать сами по себе. Они только отвечают на запросы мастера.

На данный момент предполагается два варианта Modbus сети — Modbus RTU и Modbus TCP.
Modbus TCP — это тот же старый и добрый протокол, только прокинутый через интернет сеть. При этом ведомое устройство открывает TCP сокет и ждет данные. А мастер создает сокет на нужный адрес и засылает туда команды.
Из-за специфики TCP сокетов в Modbus TCP нет байта, который указывает адрес устройства и CRC. Контрольная сумма подсчитывается в пакетах TCP/IP стеком.

Теперь рассмотрим формат посылки и ответа на примере.
Допустим мы хотим записать в регистр с адресом 0x10 устроства 0x05 данные 0xAA55.

0x05  0x06  0x00 0x10 0xAA 0x55 CRC1 CRC2

Итак мы послали устройсву адрес 5.
Затем функцию записи — 6.
Затем адрес регистра — 0x00 0x10
Затем данные старшим байтом вперед.
Затем два байта CRC младшим байтом вперед.

Отлично.
Как понять где начало посылки в потоке данных.
Я использую таймер. После каждой посылки обязательно идет задержка. Зная её размер можно четко отсекать начало.

Приступим к реализации.
Будем считать, что вы уже прочитали статью о Uart в tms320 и настроили SCI.
Нам понадобится прерывание по приему и буфер UartBuffer.

interrupt void SCIRXINTA_ISR(void)     // SCI-A
{
        int i;

        // если достигли конца буфера
        if (UartRxLen > 50 - 2)
                UartRxLen = 0;

        // Получаем 2 символа
        for (i=0;iTCR.bit.TRB = 1;

        SciaRegs.SCIFFRX.bit.RXFFOVRCLR=1;   // Clear Overflow flag
        SciaRegs.SCIFFRX.bit.RXFFINTCLR=1;   // Clear Interrupt flag
        // To receive more interrupts from this PIE group, acknowledge this interrupt
        PieCtrlRegs.PIEACK.all = PIEACK_GROUP9;
}

В обработчике прерывания мы постоянно перебрасываем данные в буфер и сбрасываем таймер.
А вот вся магия будет происходить в обработчике прерывания таймера.

interrupt void cpu_timer0_isr(void)
{
        // stop timer
        CpuTimer0.RegsAddr->TCR.bit.TSS = 1;

        // запрещаем прерывания на время обработки и передачи
        DINT;

        if (UartRxLen > 0)
        {
           Uint16 len = modbus_func(UartBuffer, UartRxLen, 2);
           Uint16 i = 0;
           // отправляем ответ
           for (i = 0; i TCR.bit.TSS = 0;

        // Acknowledge this interrupt to receive more interrupts from group 1
        PieCtrlRegs.PIEACK.all = PIEACK_GROUP1;
}

Тут мы смотрим есть ли в буфере данные и если они есть обрабатываем их и отправляем ответ.
Как же обработать модбас посылку?

Я делаю так. Если поняли, что в буфере есть данные — проверяем наш ли это адрес. Если наш обрабатываем, ну а если нет — молчим.

Uint16 modbus_func(Uint16 *Buffer, Uint16 len, Uint16 ModbusAddress)
{
                Uint16 tmp;

                // отвечаем только если нас спрашивают
                if (*Buffer != ModbusAddress)
                        return 0;

                // проверяем целосность посылки
                if (Crc16(Buffer,len) != 0)
                        return 0;

                // определяем функцию
                switch (Buffer[1])
                {
                        // чтение
                        case 0x03:
                                len = modbus_0x03_func(Buffer, len);
                        break;

                        // запись
                        case 0x06:
                                len = modbus_0x06_func(Buffer, len);
                        break;

                default:
                        len = modbus_error(Buffer,MODBUS_FUNCTION_ERROR);
        }
        // добавляем к посылке CRC
        tmp = Crc16(Buffer,len);
        Buffer[len] = tmp & 0xFF;
        Buffer[len+1] = tmp >> 8;

        return len+2;
}

Когда уверены, что запрос нам, проверяем CRC для уверенности, что посылка верна.
Когда мы уже точно знаем, что нам пришел запрос проверяем номер функции. Если нужно нет — шлем отчет об ошибке.
Ошибка метится добавлением 0x80 в байт номера фунции и собственно номером ошибки.

Вот стандартные номера ошибок.

01 — Принятый код функции не может быть обработан
02 — Адрес данных, указанный в запросе, не доступен
03 — Величина, содержащаяся в поле данных запроса, является недопустимой величиной
04 — Невосстанавливаемая ошибка имела место, пока подчинённый пытался выполнить затребованное действие.
05 — Подчинённый принял запрос и обрабатывает его, но это требует много времени. Этот ответ предохраняет главного от генерации ошибки тайм-аута.
06 — Подчинённый занят обработкой команды. Главный должен повторить сообщение позже, когда подчинённый освободится.
07 — Подчинённый не может выполнить программную функцию, принятую в запросе. Этот код возвращается для неудачного программного запроса, использующего функции с номерами 13 или 14. Главный должен запросить диагностическую информацию или информацию об ошибках от подчинённого.
08 — Подчинённый пытается читать расширенную память, но обнаружил ошибку паритета. Главный может повторить запрос, но обычно в таких случаях требуется ремонт.

Я для ошибок заготовил вот такую функцию:

Uint16 modbus_error(Uint16 *Buffer, Uint16 err)
{
        Buffer[1] |= 0x80;
        Buffer[2] = err;

        return 3;
}

В любом случае, если мы знаем что запрос наш нужно ответить.

Теперь обработаем данные.

Uint16 modbus_0x06_func(Uint16 *Buffer, Uint16 len)
{
        Uint16 Addr, Value;
        const Parameter_type *parameter;

        Addr = (Buffer[2]  ParametersCount)
                return modbus_error(Buffer, MODBUS_ADDRESS_ERROR);

        parameter = &ParametersTable[Addr];

        // проверка доступности регистра для записи
        if (parameter->Flags.bit.w == 1)
        {
                // проверяем пределы
                if (Value >= parameter->LowerLimit && Value UpperLimit)
                {
                        // записываем
                        *(parameter->Addr) = Value;
                }
                else return modbus_error(Buffer, MODBUS_DATA_VALUE_ERROR);
        }
        else return modbus_error(Buffer, MODBUS_DATA_VALUE_ERROR);

        return 6;
}

Тут мы просто берем данные и адрес, проверяем возможность записи и возвращаем длину ответа. А данные складываем в буфер Uart.

Вот и все. Примерно также работает функция чтения регистра.

Uint16 modbus_0x03_func(Uint16 *Buffer, Uint16 len)
{
        Uint16 Addr, size, tmp, s_tmp;
        const Parameter_type *parameter;

        Addr = (Buffer[2]  ParametersCount)
                return modbus_error(Buffer, MODBUS_ADDRESS_ERROR);

        parameter = &ParametersTable[Addr];

        // проверка читаемости регистра
        if (parameter->Flags.bit.r != 1)
                return modbus_error(Buffer, MODBUS_DATA_VALUE_ERROR);


        // кладем в буфер Uart количество переменных
        Buffer[2] = size;
        s_tmp = size;
        Buffer += 3;
        for (; size>0; size--, Buffer+=2, Addr++)
        {
                tmp = *ParametersTable[Addr].Addr;
                *Buffer = tmp >> 8;
                Buffer[1] = tmp & 0xFF;
        }
        // возвращаем длину посылки
        // адрес + функция + количество данных + данные
        return (3 + s_tmp*2);
}

Весь код примера тут: https://github.com/lamazavr/tms320_modbus
Описания протоколов повсеместно в интернетах. На википедии довольно подробное описание.

 

Похожий код:

Фото аватара
Алексей Петров

Программист, разработчик с 5 летним опытом работы. Учусь на разработчика игр на Unity и разработчика VR&AR реальности (виртуальной реальности). Основные языки программирования: C#, C++.

Оцените автора
Бла, бла код
Добавить комментарий