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

Ну так сказать «дембельский аккорд по АВРам» часть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
Источник

Оставить комментарий

Вы можете использовать следующие теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>