Рано или поздно (лучше рано) в устройство необходимо «впихнуть» связь с внешним миром.
Можно конечно ограничится обычным выводом данных в 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
Описания протоколов повсеместно в интернетах. На википедии довольно подробное описание.