Modbus RTU для AVR на Assembler. часть1

AVR
Понадобилось мне недавно разобраться с Модбасом. А точнее освоить подключение панели оператора к Меге.
Приглашаю всех желающих посмотреть мою реализацию такого решения

В этой части будет много кода и мало картинок. Если совсем слабо представляете что такое Modbus RTU то загляните к товарищу khomin. Там подробно описан сам протокол. Кстати от туда был взят «код» подсчета CRC
Для экспериментов я намыл у знакомого панель оператора ИП320 производства ОВЕН она конечно очень старая 2008 г.в. но рабочая. Панель поддерживает команды 0x01, 0x03, 0x05, 0x10 и еще якобы 0x06 но я не нашел в своей такой команды возможно это очень старая панель, но кстати этих команд вполне достаточно для всего что можно придумать. По сути панель никак не влияет на саму программу поэтому подойдет любая панель у которой есть Modbus RTU на RS232/485
Первое с чего следует начать это конечно настройка Меги32 а точнее UART.
Тут все очень просто. Наверное у каждого есть такой макрос из конспектов DIHALTa

.MACRO	USART_INIT
			.equ XTAL 		= 16000000	//MainClock
			.equ baudrate 		= 19200
			.equ bauddivider 	= XTAL/(16*baudrate)-1
			
			OUTI 	UBRRL,low(bauddivider)
			OUTI 	UBRRH,high(bauddivider)
			OUTI 	UCSRA, 0
			OUTI 	UCSRB,(1<<RXEN)|(1<<TXEN)|(1<<RXCIE)|(1<<TXCIE)
			OUTI 	UCSRC,(1<<URSEL)|(1<<UCSZ0)|(1<<UCSZ1)
			.ENDM

Кстати панель на скорости 19200 показала наилучшие свои характеристики.
Еще нам потребуется таймер для контроля окончания сообщения. Я взял Т0 в Normal режиме для этих целей поэтому инициализация тупо
SETB	TIMSK,TOIE0,R16 	; Разрешаем прерывание таймера

Инициализацию входов/выходов показывать смысла нет и все.
Гораздо важнее рассказать о том как я реализовал адресное пространство входов и выходов. Схема универсальная и очень пластичная.
Итак. Нам потребуется видеть/менять на панели состояние некоторых входов/выходов и некоторых битовых флагов (ну чтобы менять действие программы в МК или знать о каких либо тревогах). Для этого создадим в ОЗУ область
out_register:		 .byte  10 //сколько это дело личное 
где
out_register+0 это 8бит PORTA
out_register+1 это 8бит PORTB
out_register+2 это 8бит PORTC
out_register+3 это 8бит PORTD
т.е. все порты по алфавиту. А вот out_register+4...+9 это регистры битов-флагов. Теперь получается чтобы на панели видеть состояние PINB.4 панель должна запросить состояние бита №12 командой 0x01, а для PIND.6 №30. Если с панели запросить бит №32 то это будет какой то бит-флаг отвечающий например за режим счета огурцов в поле. Т.е. я посчитал что мне например хватит 80 бит для команд-запросов 0x01(чтение) и 0x05(запись).
Ну понятное дело что если out_register+0...+3 это паразитный буфер между ИП320 и лапками МК то должна быть процедура которая раскидывает состояние битов в порты, причем процедура по всем правилам этикета может изменять в PORTах только ноги выходов а точнее только те выходы которые мы считаем возможным для изменения, а вот считывать можно состояние всех PINов или только входов это не так важно.
Теперь переходим к CRC. Что это такое я думаю вы уже прочитали. Поскольку я очень ленивый я решил использовать табличный метод расчета контрольной суммы. Итак берем Сишный код

        crc_hi = 0xFF;   // high byte of CRC initialized
        crc_lo = 0xFF;   // low byte of CRC initialized

        do
        {
           uint8_t i = crc_hi ^ *p++;        // will index into CRC lookup table
           crc_hi = crc_lo ^ (uint8_t)(&auchCRCHi[i]);    // calculate the CRC
           crc_lo =          (uint8_t)(&auchCRCLo[i]);
        }
        while (--n);         // pass through message buffer (max 256 items)

И чтобы не обвинили в плагиате меняем названия регистров на может кривые но удобные для контроля

		   //программа вычисления crc for modbus_rtu
			   //причем  crc_first улетает первым а  crc_second наверно следом
		   #define      crc_second r17			 
		   #define	crc_first r18
		   #define	crc_cou r19
		   #define	crc_temp r16
		   #define	buf_temp r20
crc:			
		   //первоначально загружаем 0xFF в оба регистра выходящего црц
		   ldi crc_second,0xFF
		   ldi crc_first,0xFF
		   lds  crc_cou, count_crc_byte		//берем кол-во байт для обработки
	CRC_m1: //начали
		   ld   buf_temp,X+		   //взяли байт из сообщения
		   eor 	crc_first,buf_temp
		   mov  crc_temp,crc_first
		   LDZ auchCRCHi*2	   //таблицы во флэше
		   ADZR crc_temp            //макрос Z=Z+r
		   lpm
		   eor crc_second,r0
		   mov  crc_first,crc_second	//это наш текущий crc_first
		   LDZ auchCRCLo*2
		   ADZR crc_temp            //макрос Z=Z+r
		   lpm crc_second,Z			// а это наш текущий crc_second
			//проверяем кончились байты или нет если что на новый круг
		   dec crc_cou
		   tst crc_cou
			brne CRC_m1
			ret	

 auchCRCHi:
; High
.db 0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41, 0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40, 0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40, 0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41
.db 0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40, 0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41, 0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41, 0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40 
.db 0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40, 0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41, 0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41, 0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40
.db 0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41, 0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40, 0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40, 0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41
.db 0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40, 0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41, 0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41, 0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40
.db 0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41, 0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40, 0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40, 0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41
.db 0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41, 0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40, 0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40, 0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41
.db 0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40, 0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41, 0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41, 0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40

; Low 
auchCRCLo:
.db 0x00,0xc0,0xc1,0x01,0xc3,0x03,0x02,0xc2, 0xc6,0x06,0x07,0xc7,0x05,0xc5,0xc4,0x04, 0xcc,0x0c,0x0d,0xcd,0x0f,0xcf,0xce,0x0e, 0x0a,0xca,0xcb,0x0b,0xc9,0x09,0x08,0xc8
.db 0xd8,0x18,0x19,0xd9,0x1b,0xdb,0xda,0x1a, 0x1e,0xde,0xdf,0x1f,0xdd,0x1d,0x1c,0xdc, 0x14,0xd4,0xd5,0x15,0xd7,0x17,0x16,0xd6, 0xd2,0x12,0x13,0xd3,0x11,0xd1,0xd0,0x10
.db 0xf0,0x30,0x31,0xf1,0x33,0xf3,0xf2,0x32, 0x36,0xf6,0xf7,0x37,0xf5,0x35,0x34,0xf4, 0x3c,0xfc,0xfd,0x3d,0xff,0x3f,0x3e,0xfe, 0xfa,0x3a,0x3b,0xfb,0x39,0xf9,0xf8,0x38
.db 0x28,0xe8,0xe9,0x29,0xeb,0x2b,0x2a,0xea, 0xee,0x2e,0x2f,0xef,0x2d,0xed,0xec,0x2c, 0xe4,0x24,0x25,0xe5,0x27,0xe7,0xe6,0x26, 0x22,0xe2,0xe3,0x23,0xe1,0x21,0x20,0xe0
.db 0xa0,0x60,0x61,0xa1,0x63,0xa3,0xa2,0x62, 0x66,0xa6,0xa7,0x67,0xa5,0x65,0x64,0xa4, 0x6c,0xac,0xad,0x6d,0xaf,0x6f,0x6e,0xae, 0xaa,0x6a,0x6b,0xab,0x69,0xa9,0xa8,0x68
.db 0x78,0xb8,0xb9,0x79,0xbb,0x7b,0x7a,0xba, 0xbe,0x7e,0x7f,0xbf,0x7d,0xbd,0xbc,0x7c, 0xb4,0x74,0x75,0xb5,0x77,0xb7,0xb6,0x76, 0x72,0xb2,0xb3,0x73,0xb1,0x71,0x70,0xb0
.db 0x50,0x90,0x91,0x51,0x93,0x53,0x52,0x92, 0x96,0x56,0x57,0x97,0x55,0x95,0x94,0x54, 0x9c,0x5c,0x5d,0x9d,0x5f,0x9f,0x9e,0x5e, 0x5a,0x9a,0x9b,0x5b,0x99,0x59,0x58,0x98
.db 0x88,0x48,0x49,0x89,0x4b,0x8b,0x8a,0x4a, 0x4e,0x8e,0x8f,0x4f,0x8d,0x4d,0x4c,0x8c, 0x44,0x84,0x85,0x45,0x87,0x47,0x46,0x86, 0x82,0x42,0x43,0x83,0x41,0x81,0x80,0x40
	

Итак эта процедура с легкостью вычисляет CRC но перед ее вызовом необходимо чтобы в регистре X был адрес буфера откуда брать байты на обработку и в регистре ОЗУ count_crc_byte лежало кол-во байт для обработки. Вот как у меня идет подготовка к вызову этой процедуры
ldi r16,3    //не обращаем внимание что здесь число 3 поскольку в большинстве случаев мы вообще не знаем какое значение грузится в count_crc_byte оно может меняться в зависимости от запроса  
sts count_crc_byte,r16    
LDX 	Modbus_Buf_TX		//указываем откудава будем брать байты для подсчета  CRC
//все готово для подсчета CRC
	rcall crc    

Итак переходим к заключительному разделу подготовки — прерывания. У нас их целых 3 шт. OVF0, URXC, UDRE.
Начнем с URXC. Нам однозначно нужен буфер для приема сообщения и регистр в ОЗУ для хранения количества пришедших байтов.
.equ MBRX	=25    // то сколько байт отдаем под буфер приемник
Modbus_buf_RX:	.byte	MBRX	;
Count_RX:	.byte	1	; здесь храним кол-во принятых байт

Вообще надо сказать что тут все прерывания очень уж простые. Поместил принятый байт в приемный буфер, перезапустил таймер окончания приема, увеличил счетчик и вышел
RX_OK:		
			PUSHF						; Макрос, пихающий в стек SREG и R16
			PUSH XL	
			PUSH XH
			PUSH r0
			
			OUTI	TCCR0,1<<CS02		 //пере/запустим таймер ожидания конца сообщения с делителем 256

			LDX		Modbus_buf_RX
			lds		r16, Count_RX
			ADXR	r16					;макрос X=X+r где есть r0
			INC		R16					; Увеличиваем смещение
			sts		Count_RX,r16			
			in		R16,UDR				; Забираем данные
			ST		X,R16				; сохраняем их 

			;перезапуск таймера0 на новое ожидание интервала 3.5 пакета 
			ldi r16,255-(32*XTAL)/(256*baudrate)
			out TCNT0,temp
			
RX_OUT:		
			POP r0
			POP XH
			POP XL
			POPF						; Достаем SREG и R16

Прерывание UDRE. Здесь нам также потребуется буфер на отправку, регистр в ОЗУ для подсчета байт отправленных и регистр в ОЗУ для хранения кол-ва на отправку
.equ MBTX	=25
Modbus_Buf_TX:		.byte	MBTX	;		 
Count_TX:		.byte	1	;
Over_Count_TX:		.byte	1	;

Отправили байт, увеличили счетчик (если последний то глушим прерывание) и выходим.
UD_OK:	
 
			PUSHF						; Макрос, пихающий в стек SREG и R16
			PUSH XL	
			PUSH XH
			PUSH r0	
			PUSH	R17
			
			 //отправляем очередной байт
			LDX		Modbus_Buf_TX
			lds		r16, Count_TX
			ADXR	r16				//макрос где есть r0
			ld		R17,X			; Забираем данные				
			out		UDR,R17		; отправляем их		
			 	//проверяем кол-во отправляемых байт
			lds		r17, Over_Count_TX
			inc		r16
			cp		r17,r16
			brne	UD_Out //переходим если не последний

			//а если последний то
			OUTI 	UCSRB,(1<<RXEN)|(1<<TXEN)|(1<<RXCIE)|(0<<UDRIE); Запрет прерывания По пустому UDR
			 clr r16	 //очищаем счетчик

UD_Out:	   	sts   Count_TX,r16

			POP		R17
			POP r0
			POP XH
			POP XL
			POPF						; Достаем SREG и R16
			RETI

Прерывание OVF0.Здесь нам потребуется 1 байт ОЗУ для копии кол-ва принятых байт и еще один байт ОЗУ для хранения разрешения обработки принятого сообщения по Модбасу
flag_obabotki_Modbus:    .byte	1
Count_RX2:	        .byte	1	;

Ну комментарии все объясняют
T0_OVF:
		    PUSHF						; Макрос, пихающий в стек SREG и R16
			
			//останавливаем таймер поскольку он сработал и это значит мы получили последний байт по Модбасу 
			OUTI	TCCR0,0<<CS02
			//теперь подготовим данные для обработки принятого пакета 
			 ldi r16,1
			 sts flag_obabotki_Modbus,r16		 //выставим флаг разрешающий начать обработку принятых данных по модбасу
			 lds temp, Count_RX
			 sts  Count_RX2,temp			  //сохраним копию кол-ва принятых байт
			 clr temp
			 sts Count_RX,temp				 //очистим счетчик принятых байт это лучше сделать здесь пока соображаем
			
			POPF						; Достаем SREG и R16
			RETI	

На этом пока остановимся. Во второй части описание парирования принятых сообщений, немного скринов с лог.анализатора и скорее всего видео работы связки ИП320+Mega32
Всю критику и пожелания готов почитать и обсудить
  • +3
  • 02 августа 2016, 22:27
  • deses

Комментарии (5)

RSS свернуть / развернуть
Тоже хотел сделать что-то подобное.
А сколько занимает модулёк?
0
Не совсем понял вопрос
0
Ну, в сколько байт уложилась реализация модбаса?
0
Так сложно сказать. Отладочный где то 1.5 кило. Планирую к среде следующей накидать моргалку на диспетчере чтобы на видео была понятна работа по изменению регистров и в среду выложу. Ща некогда а на выходные в поход на природу пока погода хорошая
0
Ща глянул конкретно модбас 1к и 500 байт таблица во флэше
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.