Реализация 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
Описания протоколов повсеместно в интернетах. На википедии довольно подробное описание.

Просмотров:   2780

Комментарии

Добавить комментарий

Plain text

  • HTML-теги не обрабатываются и показываются как обычный текст
  • Адреса страниц и электронной почты автоматически преобразуются в ссылки.
  • Строки и параграфы переносятся автоматически.
CAPTCHA
Введи эти символы. Ато роботы одолели!