Notice: Memcache::get(): Server localhost (tcp 11211) failed with: Connection refused (111) in /home/a146/www/we.easyelectronics.ru/engine/lib/external/DklabCache/Zend/Cache/Backend/Memcached.php on line 134
Modbus RTU для AVR на Assembler. часть2 / AVR / Сообщество EasyElectronics.ru

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

AVR
Ну так сказать «дембельский аккорд по АВРам» часть2. Если что то начало можно глянуть здесь


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


вот ссылка на ютуб

Итак начинаем потрошить принятые сообщения по Modbus RTU от панели HMI. Сразу скажу что тестовый проект я сделал на простом диспетчере из учебного курса от DIHALTa и, возможно, это кого-то отпугнет, но Модбас там сам по себе и никак не привязан к диспетчеру более того очень понятна работа с оперативой. С неё и начнем.
Первым делом создаем область ОЗУ Holding_Registers_0x03 в которую мы будем складывать принятые данные по команде 0х03 и из которой мы будем брать значения для отправки по команде 0х10. Т.е это область в которой мы храним какие-то числа размером 2 байта которые являются некиеми параметрами (например время включения и выключения «помигать диодом» а точнее тремя)
.equ HolReg	=20
Holding_Registers_0x03:		.byte	HolReg		   // здесь 2 байта на данные те здесь мы храним   HolReg/2 значений причем сначала high а затем low части слова

У меня есть привычка все дефайнить поскольку не люблю магию чисел. Поэтому дефайним регистры ОЗУ Holding_Registers_0x03 в более удобные названия. Я это делаю так
#define  Tim_Led1_on	Holding_Registers_0x03+0    //здесь храним время время включения Led1 
 #define  Tim_Led1_off	Holding_Registers_0x03+2    //и т.д.

 #define  Tim_Led2_on	Holding_Registers_0x03+4
 #define  Tim_Led2_off	Holding_Registers_0x03+6

 #define  Tim_Led3_on	Holding_Registers_0x03+8
 #define  Tim_Led3_off	Holding_Registers_0x03+10

 #define  Key_registr	Holding_Registers_0x03+12    //здесь храним смещение относительно начала Holding_Registers_0x03 чтобы знать какой параметр будем менять клавишами + и - 
Теперь адрес каждых 2-х байт этой области имеет свое название. Отлично.
Теперь обратим внимание на область ОЗУ out_register (о которой говорилось в часть1). Первые 4 байта (0-31 бит) это паразитные байты между HMI и лапками МК, а начиная с 5-го байта (32-го бита) это флаговые регистры в которых каждый бит за что-то может отвечать а за что именно это решать нам. Создаем 3 бита — №32,33,34 которые будут давать разрешение на «помигать диодом» через дефайны
.equ Led1_nomer	=32    //<em>бит разрешающий мигание Led1</em> 
 #define  EN_Led1_byte	out_register+Led1_nomer/8    //<em>адрес байта хранящего в себе бит разрешения мигания</em> Led1
 #define  EN_Led1_bit	Led1_nomer&0b00000111    //<em>порядковый номер бита в байте</em>
//и т.д.
 .equ Led2_nomer	=33
 #define  EN_Led2_byte	out_register+Led2_nomer/8
 #define  EN_Led2_bit	Led2_nomer&0b00000111

 .equ Led3_nomer	=34
 #define  EN_Led3_byte	out_register+Led3_nomer/8
 #define  EN_Led3_bit	Led3_nomer&0b00000111


А вот теперь мы уже можем приступать к самому разбору принятого сообщения. Начинается оно с ID устройства к которому направлено следом идет код функции которую устройство должно выполнить. В целом процедура большая но выполняется лишь одна ее ветка — код функции. Общая схема такова
1. проверили ID — если не наш то на выход
2. проверили CRC — если ошибка то на выход (ничего страшного — придет следующее сообщение)
3. взяли код функции и ушли ее обрабатывать (если таковая есть а если нет то сформировали ответ об ошибке)
4. запускаем УАРТ на отправку ответа
5. очищаем нужные регистры и на выход из процедуры
Пункт 3 наверное самый ценный в этой процедуре. Итак моя ИП320 поддерживает 0х01, 0х03, 0х05 и 0х10 функции.
Пожалуй самая мутная в плане алгоритма получается функция 0х01. Очень не хотелось делать эту функцию в духе «и так сойдет» поэтому решил реализовать ее в полной мере без каких либо привязок и ограничений типа «опрашиваем начиная с первого бита и только по одному!». Ограничение только одно — область опроса 80 бит (хотя при желании можно расширить до 120 без особых усилий). Из сообщения мы знаем что необходимо прочитать биты начиная с N-го в количестве S штук и упаковать их в байты где N-ный бит займет нулевой бит первого отправляемого байта а все биты после S должны быть ==0.

Алгоритм такой.
1. определяем в каком байте нашей области начинается N бит путем деления на полные 8 и запомним остаток
2. определяем в каком байте лежит последний нужный бит — (N+S-1)/8 и +1 если есть остаток
3. читаем только нужные нам байты их количество нам известно из разницы п1 и п2
4. сдвигаем прочитаные байты вправо на кол-во бит в остатке п1 т.е. выравниваем
5. выясняем сколько байт мы вообще должны отправить (S/8) и +1 если есть остаток
6. обнуляем все последние (незапрошеные) биты в последнем байте
7. можно складывать в буфер отправки и считать црц
Благодаря ICALL и IJMP функция получилась на мой взгляд очень компактная и шустрая.
// Начальный адрес Hi	Начальный адрес Lo		Количество Hi		Количество Lo		 
 //YL-с какого начать читать выходы-катушки   YH-сколько подряд выходов нас интересует
 //но нас абсолютно не интересует 	Начальный адрес Hi и  Количество Hi поскольку у нас явно меньше 0xFF выходов и упакованы побитно те они =0
			lds YL,Modbus_buf_RX+3
			lds YH,Modbus_buf_RX+5
//первым делом узнаем с кокого байта начать считывать биты выходов для этого поделим на 8 регистр хронящий начало чтения выходов а остаток от деления сохраним поскольку нам потребуется выровнять для отправки по 0-му биту

mov r17,YL
mov r18,YL
andi r18,0b00000111	//остаток от деления на 8
lsr	r17
lsr r17
lsr	r17		   //результат деления на 8. 
//теперь посчитаем в каком регистре лежит последний запрашиваемый бит-coil (мы же знаем из сообщения с какого начать и сколько выходов требуется отправить в сообщении) но очень важно понимать что количество запрошеных битов надо убавить на 1 поскольку если например запросили с 7бита 1 бит то последний запрошеный бит это бит7 и есть
mov r19,YL
mov r20,YH
dec	r20
add r19,r20
mov r20,r19
andi r20,0b00000111	//остаток от деления на 8
lsr	r19
lsr r19
lsr	r19		  //результат деления на 8.
tst r20		  //проверяем остаток от деления на 8 и если он не 0 то  
breq M01_01	  //увеличим целую часть на 1 поскольку последний бит лежит в следующем за целой частью байте
inc r19
M01_01:	//итак сейчас мы  знаем с какого r17 и до кокого r19 байта нам надо прочитать наши выходы - читать их мы будем в младшую группу r0..r9, но для начала нацеливаемся на начальный адрес откуда будем читать и куда будем читать и заодно посчитаем сколько регистров мы прочитали считать будем в r21
LDX out_register
ADXR r17	//макрос X=X+@R ВНИМАНИЕ СОДЕРЖИТ	clr r0
LDZ  0		// нацелились на r0
clr r21
dec r17		//убавим чтобы начать со сложения
M01_02:
inc r17			//увеличиваем регистр начала чтения 
ld	r16,X+		//	читаем
st  Z+,r16		//сохраняем в rN, rN=rN+1
inc r21			//увеличиваем число прочитаных регистров
cp	r19,r17		//проверяем достигли ли конца чтения 
brne  M01_02	//если нет то повторяем теперь нацелены по Х на следующий байт и на следующий младший регистр rN
 //итак мы прочитали все байты содержащие интересующие нас биты но нам необходимо теперь сдвинуть их вправо так чтобы в 0-вом бите 
 //r0 содержался первый запрошеный бит-coil, 
 //на сколько необходимо сдвинуть мы тоже знаем ответ лежит в r18 (остаток от деления на 8 начального бита)  
 //	число прочитанных регистров лежит в r21
//чисто теоретически мы можем тупо сдвигать вправо все регистры с r0 по r9 но поскольку мы знаем сколько мы прочитали регистров то сделаем при помощи ijmp
 LDZ lsr_r0r9	//
 clr r16
 sub ZL,r21
 sbc ZH,r16
 M01_03:
 tst r18
 breq M01_04
 clc
 ijmp
 
				lsr r9
				ror r8
				ror r7
				ror r6
				ror r5
				ror r4
				ror r3
				ror r2
				ror r1
				ror r0
	lsr_r0r9:	nop
				dec r18
				rjmp M01_03

M01_04:
// итак мы выровняли наши биты и теперь в r0(0) лежит первый запрошеный бит,но и это еще не все нам теперь необходимо обнулить последние биты в последнем байте которые не входят в область запроса, для начала узнаем сколько вообще байт с битами мы будем отправлять
 mov r22,YH
 mov r23,YH
 andi r23,0b00000111	//здесь остаток от деления на 8 и это нам очень пригодится для обнуления
 lsr	r22
 lsr	r22
 lsr	r22		  //результат деления на 8.
 tst r23		  //проверяем остаток от деления на 8 и если он не 0 то  
 breq M01_05	  //увеличим целую часть на 1 поскольку последний бит лежит в следующем за целой частью байте
 inc r22
 M01_05:
 //теперь зная остаток от деления мы можем на последний байт настелить маску для обнуления всех ненужных старших битов
 LDZ Bitmask_M01*2		 //маска хранится во флеше
 clr r16
 add ZL,r23		 
 adc ZH,r16
 lpm r24,Z		  //r24 наша маска
 
 //	теперь имея маску и зная сколько полных регистров у нас на отправку  наложим маску на последний
 mov r23,r22		//сохраним копию количества регистров на отправку чтобы знать сколько записывать в регистр количество отправляемых байт
 lsl r22			//увеличим вдвое поскольку у нас маскирование занимает 2 команды
 LDZ M01_mask		// берем адрес метки команд маскирования
 clr r16
 add ZL,r22
 adc ZH,r16			  //складываем и в результате в Z у нас лежит адрес маскирования последнего байта, поскольку всего у нас 10 байт для
 icall				 //выходов мы делаем 10 блоков маскирования, 1 блок это	 "and rN ,r24  +  ret"	Короче прыгаем и сюда же вернемся по ret

 //Вот теперь вроде у нас все сделано и осталось занести наши данные в буфер отправки
 //кол-во байт у нас сохранилось в 	r22 причем уже удвоенное значит используя отрицательное смещение(т.е. смещение относительно метки не вниз по тексту а вверх) переходим в раздел заполнения буфера на отправку
 LDZ M01_save
 clr r16
 sub ZL,r22
 sbc ZH,r16
 ijmp		 //прыгаем в раздел записи в буфер

 Bitmask_M01: .db	0b11111111, 0b00000001, 0b00000011, 0b00000111, 0b00001111, 0b00011111, 0b00111111, 0b01111111

	M01_mask:	nop
				nop
				and r0 ,r24		   //если мы попали сюда значит r0 это последний байт
				ret
				and r1 ,r24			//если мы попали сюда значит r1 это последний байт
				ret
				and r2 ,r24		   //если мы попали сюда значит r2 это последний байт
				ret
				and r3 ,r24		  //если мы попали сюда значит r3 это последний байт
				ret
				and r4 ,r24		  //если мы попали сюда значит r4 это последний байт
				ret
				and r5 ,r24		  //и тд
				ret
				and r6 ,r24
				ret
				and r7 ,r24
				ret
				and r8 ,r24
				ret
				and r9 ,r24
				ret
	
//вот наш раздел записи в буфер и придем сюда в ту строчку которая содержит последний регистр с данными
//т.е. если у наc в запросе было 12бит то это значит мы отправим  2 байта в котором старшие 4 бита ==0
// причем второй (он же последний) байт у нас в r1 вот значит на строку   sts Modbus_Buf_TX+4 ,r1 мы и попадем
			sts Modbus_Buf_TX+12 ,r9
			sts Modbus_Buf_TX+11 ,r8
			sts Modbus_Buf_TX+10 ,r7
			sts Modbus_Buf_TX+9 ,r6
			sts Modbus_Buf_TX+8 ,r5
			sts Modbus_Buf_TX+7 ,r4
			sts Modbus_Buf_TX+6 ,r3
			sts Modbus_Buf_TX+5 ,r2
			sts Modbus_Buf_TX+4 ,r1
			sts Modbus_Buf_TX+3 ,r0
	M01_save: nop
			sts Modbus_Buf_TX+2 ,r23		//число байт
			ldi r16,0x01
			sts Modbus_Buf_TX+1 ,r16		// функция
			ldi r16,Modbus_ID
			sts Modbus_Buf_TX ,r16		//Modbus_ID
			subi r23,-3					// +3 потому что кроме данных в CRC учавствуют и 3 других байта (Modbus_ID,функция,число байт)
			sts count_crc_byte,r23	   
			LDX 	Modbus_Buf_TX		//указываем откудава будем брать байты для подсчета  CRC
			//все готово для подсчета CRC
			rcall crc	//crc_first r18 crc_second r17
			 
			lds  r16, count_crc_byte	 //еще раз глянем сколько байт было на обработке
			LDX 	Modbus_Buf_TX 
			ADXR r16
			st X+,r18				 //и указав это кол-во как смещение сохраняем наш  CRC
			st X, r17
			subi r16,-2				//теперь увеличим на 
			sts Over_Count_TX, r16			   //это и есть кол-во байт для отправки (используется в прерывании на отправку)
			//вот и все функция 0х01 обработана

Функции 0х05, 0х03,0х10 очень простые в обработке. Опишу только алглритмы

0х05 запись одного значения дискретного выхода т.е. установка бита в пространстве out_register. в принятом сообщении N -номер бита, FF00 — ON, 0000 — OFF:
1. определяем в каком байте out_register (N/8) и в какой позиции (остаток от N/8) требуемый бит
2. определяем действие над этим битом ON или OFF. другие варианты это ошибка — формируем соответствующий ответ
3. производим действие ON или OFF
4. тупо копируем принятое сообщение в буфер отправки УАРТ и запускаем отправку

0х03 Эта команда используется для чтения значений (по 2 байта). В принятом сообщении адрес регистра с которого начать N и в каком количестве S. Причем запрос идет на 2-х байтные данные т.е. если запрошены данные 5-ти регистров то в ответе мы укажем что отправляем 10 байт данных
1.Выясним точку чтения данных относительно начала Holding_Registers_0x03 (N*2) и кол-во (S*2)
2.тупо копируем в буфер отправки на позиции начиная с четвертой (1-ID, 2-команда, 3-кол-во отправляемых данных)
3. считаем црц и запускаем УАРТ

0х10 команда записи одного/нескольких значений.(обратная команде 0х03). В принятом сообщении N- с какого байта* начать запись и S- кол-во байт** для записи
* — имеется в виду 2-х байтный регистр
**- реальное кол-во байт
Ту все еще проще
1. выяснили точку записи данных относительно начала Holding_Registers_0x03 (N*2)
2. скопировали данные из приемного буфера УАРТ в кол-ве S
3. скопировали из приемного буфера УАРТ первые 6 байт в буфер отправки УАРТ
4. посчитали црц и включили УАРТ

Теперь бег по граблям
1. правильность подключения А и В если rs485, RX и TX если rs232
2. если в проекте используются данные 2 байта и 4 байта то в области Holding_Registers_0x03 их лучше не мешать а сгруппировать — сначала одни потом другие
3. не стоит пытаться увидеть на панели как мигает ВЫХОД с частотой выше 10Гц
4. если ВЫХОД работает с частотой выше 50Гц или же очень критичен к моменту изменения состояния (+-0.01сек) то таким выходом лучше управлять напрямую типа
sbi PORTout2, out2
и обязательно надо в таблице Port_mask соответствующий бит разрешения/запрета установить =0 т.е. запретить устанавливать его значение из out_register поскольку если этого не сделать то при ближайшем же входе в процедуру этот выход будет установлен исходя из состояния соответствующего бита. А так при установленном запрете бит при каждом входе будет принимать фактическое состояние ВЫХОДА.

Теперь пару слов как этот проект использовать в своих целях:
1. скачать архив
2. выдернуть от туда Interrupts.asm — здесь обработчики прерываний (проверьте свой temp у меня — R16)
3. выдернуть от туда Interrupts_vector.asm — здесь вектора Меги16/32 (надо закрыть вектор OC2addr это вектор RTOS)
4. выдернуть от туда research_Modbus.asm — это потрошитель принятого сообщения по Модбасу
5. выдернуть от туда Modbus_0x01.asm, Modbus_0x03.asm, Modbus_0x05.asm, Modbus_0x10.asm — это файлы вложенные в research_Modbus.asm (являются ветками исполнения функции research_Modbus)
6. выдернуть от туда update_port.asm — процедура чтения всех входов, чтения «запрещенных к изменению выходов» и изменение состояния выходов (не забудьте поправить Port_mask по своим требованиям)
7. выдернуть от туда Modbus_DSEG.inc — фаил содержит определение рабочих регистров Модбаса в DSEG (не забудьте задать свой Modbus_ID)
8. выдернуть от туда CRC16_Modbus_RTU.asm — процедура подсчета CRC
9. Возможно придется потаскать некоторые макросы из моего архива. Теперь можно строить свой проект.
Каких то глюков в работе программы я не обнаружил поэтому больше сказать нечего.
Рекомендую заглянуть на сайт Просто о Modbus RTU Здесь я брал принцип формирования сообщения/ответа
В прикрепленных файлах проект в AtmelStudio7 и для Конфигуратор ИП320 v6.5
  • +2
  • 16 августа 2016, 15:25
  • deses
  • 2
Файлы в топике: Проект13082016.zip, Modbus_RTU-RTOS.zip

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

RSS свернуть / развернуть
Спасибо!
В первой части дайте ссылку на вторую часть.
0
Пожалуйста. Сделал.
0
Благодарю!
Объясню, почему я это попросил:
У меня в закладках обычно находятся только первые части многосерийных публикаций.
И по первым частям я обычно нахожу остальные.
0
Я это делаю так
Вообще говоря, это синтаксис препроцессора С, который ассемблер понимать не обязан. У ассемблера есть свой синтаксис — equ.
0
  • avatar
  • Vga
  • 16 августа 2016, 19:48
Так-то да, но через #define гораздо удобней.
0
Сразу оговорюсь что в оригинальном Модбасе каждому такому выходу отдается аж 2 байта только для того чтобы там хранить значение 0х0001 или 0х0000. Я решил что это слишком расточительно для Меги, и для этого хватит одного бита.

Ээээ, что? С каких пор Read Coils требует на каждый вывод два байта? В спецификации чётко сказано, что кол-во байт в ответе — это N. N — это результат целочисленного деления кол-ва выходов на 8. Если остаток ненулевой, то прибавляем 1. Вот документ, функция с кодом 0x01 на 12-ой странице. www.modbus.org/docs/Modbus_Application_Protocol_V1_1b.pdf
0
В цитате за ответ нет ни слова. В цитате сказано что в памяти контроллера отведено 2 байта для хранения информации о его состоянии
0
В цитате сказано что в памяти контроллера отведено 2 байта для хранения информации о его состоянии
Не совсем так. Команда 5 (я так понимаю речь о ней) при передаче кодирует состояние двумя байтами. Но Модбас никак не регламентирует как Вы будете хранить это состояние (или вообще, будете ли Вы его хранить в явном виде).
0
Нет эта цитата не имеет отношения ни к коком командам. Эту цитату вообще надо перенести в первую часть где я объясняют как я организовал пространство хранения значения coils
0
У вас в статье сказано про некий оригинальный в сферическом вакууме Модбас. Под таким Модбасом, обычно, подразумевается как раз-таки спецификация. А в спецификации сказано то, что я написал выше.
0
Уже обсудили. Я понял.
0
Почему дембель?
0
Уже с пол года на стм32 вожусь и изучаю. Но пока особого восторга не испытал кроме отладки стлинком — это конечно инструмент превосходный
0
А как же превосходный RCC? А периферия с кучей гибких настроек? А плюшки, вроде аппаратного MAC?
0
В связи отсутствия большого ума переход от авр асма на си в стм сопровождается мозговой эпилепсией. Обилие плюшек выводят на стадию отрицания диагноза а обилие настроек сталкиваются к стадии смирения. Вот так и происходит это освоение
0
Но я не отчаиваюсь. я планируют в ближайшие 120 ну максимум 130 лет освоить стмки.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.