Сен 17 2010

Протокол Modbus в устройствах на базе микроконтроллеров. Часть 2.1. Программная поддержка протокола

Для поддержки протокола Modbus RTU программа должна принимать символы, поступающие в порт и размещать их в буфере приёма. Признаком окончания сообщения служит тайм-аут, т.е. прекращение поступления символов в течение 3,5 — 4.5 длительностей передачи одиночного символа.

По окончании приёма сообщения управление должно передаваться специальному обработчику, который декодирует команду протокола, исполняет её, и формирует строку ответа, которая размещается в буфере передачи.

Передача ответа также ведётся с помощью прерываний. Начало передаваемого сообщения размещается в аппаратном буфере UART, при его исчерпании формируется прерывание, и буфер заполняется снова.

Работа с сообщениями ModbusASCII происходит аналогичным образом, за исключением того, что признаком конца приёма сообщения служит не тайм-аут,  а специальный символ конца сообщения.

В этой статье будет рассмотрен только режим Modbus RTU, как наиболее распространённый в настоящее время.

Итак, всю задачу можно условно разбить на три части, начиная с самого нижнего уровня:

  1. Функции работы с UART
  2. Функции, относящиеся к протоколу Modbus
  3. Функции, относящиеся к основному алгоритму работы контроллера (функции исполнения команд Modbus)

Функции работы с UART в микроконтроллере на ядре Cortex M3 (LPC1768)

Как и большинство микроконтроллеров, LPC1768 предусматривает три основных способа работы с UART: по опросу флагов, по прерыванию, и в режиме прямого доступа к памяти. Работа с UART путём опроса флагов означает, что программа должна ожидать наступления события, такого как поступление символа в буфер приёма или освобождение буфера передачи. Во время ожидания контроллер должен непрерывно, в цикле проверять состояние соответствующих регистров UART. Работа с UART по опросу флагов малопригодна для реальных приложений, обычно используется работа с UART по прерываниям. В данном примере будет рассмотрена работа с UART именно с помощью прерываний. Работа с UART в режиме прямого доступа к памяти несколько сложнее в реализации и (пока) рассматриваться не будет.

Микроконтроллер LPC1768 содержит 4 порта UART, и, если мы используем в устройстве несколько портов, то для каждого из них нам понадобится свой собственный обработчик прерывания и свой набор буферов приёма и передачи.

Следует отметить, что порты UART0 и UART1 в LPC1768 не абсолютно одинаковы, UART1 имеет специальный режим, упрощающий работу с портом RS-485, он будет рассмотрен в дальнейшем. Порты UART0, UART2 и UART3 идентичны и полностью взаимозаменяемы.

Содержимое заголовочного файла модуля LPC_Uart.h приведено ниже. Он содержит функции работы с UART0 и UART1. При необходимости, он может быть дополнен функциями работы с UART2 и UART3.

/*************************************************************************
*  UART functions
*  32bit.me (c)2010  
**************************************************************************/

#ifndef __LPC_UART_H
#define __LPC_UART_H

#include "type.h"

#define FIFODEEP    16

#define RXBUFSIZE  1024
#define TXBUFSIZE  1024

#define BD115200    115200
#define BD38400     38400
#define BD19200     19200
#define BD9600      9600

/* Uart line control register bit descriptions */
#define LCR_WORDLENTH_BIT         0
#define LCR_STOPBITSEL_BIT        2
#define LCR_PARITYENBALE_BIT      3
#define LCR_PARITYSEL_BIT         4
#define LCR_BREAKCONTROL_BIT      6
#define LCR_DLAB_BIT              7

// Uart Interrupt Identification
#define IIR_RSL                   0x3
#define IIR_RDA                   0x2
#define IIR_CTI                   0x6
#define IIR_THRE                  0x1

// Uart Interrupt Enable Type
#define IER_RBR                   0x1
#define IER_THRE                  0x2
#define IER_RLS                   0x4

/* Uart Receiver Errors*/
#define RC_FIFO_OVERRUN_ERR       0x1
#define RC_OVERRUN_ERR            0x2
#define RC_PARITY_ERR             0x4
#define RC_FRAMING_ERR            0x8
#define RC_BREAK_IND              0x10

typedef enum {
 UART0 = 0,
 UART1
} LPC_UartChanel_t;

// Word Lenth type
typedef enum {
 WordLength5 = 0,
 WordLength6,
 WordLength7,
 WordLength8
} LPC_Uart_WordLenth_t;

// Parity Select type
typedef enum {
 ParitySelOdd = 0,
 ParitySelEven,
 ParitySelStickHigh,
 ParitySelEvenLow
} LPC_Uart_ParitySelect_t;

// FIFO Rx Trigger Level type
typedef enum {
 FIFORXLEV0 = 0,    // 0x1
 FIFORXLEV1,        // 0x4
 FIFORXLEV2,        // 0x8
 FIFORXLEV3        // 0xe
} LPC_Uart_FIFORxTriggerLevel_t;

typedef struct {
 unsigned long BaudRate;            // Baud Rate

 LPC_Uart_WordLenth_t WordLenth;    // Frame format
 BOOL TwoStopBitsSelect;
 BOOL ParityEnable;
 LPC_Uart_ParitySelect_t ParitySelect;
} LPC_Uart_Config_t;

void SetUart0RxHandler(void (*_rx_handler)(char *buf, int length));

char Uart0Init(void);

int Uart0Transmit(int length);

char* GetUart0TxBuffer(void);

void SetUart1RxHandler(void (*_rx_handler)(char *buf, int length));

char Uart1Init(void);

int Uart1Transmit(int lenght);

char* GetUart1TxBuffer(void);

#endif //__LPC_UART_H

В следующей части будут рассмотрены функции работы с UART1 (функции работы с UART0 аналогичны, за исключением особенностей, которые будут оговорены далее).

Владимир Татарчевский

  • Юра

    Интересно, будет ли продолжение? Очень нравится простой и обстоятельный стиль изложения.

    Позволю себе вопрос не прямо по тексту. Я впервые столкнулся с задачей реализовать Слейв-устройство по Modbus RTU, причем внедрить это в готовое устройство, в котором работает самодельный ASCII-протокол по RS485.
    Совершенно не представляю, сможет ли имеющийся микроконтроллер AT89S8253 справиться с задачей. Конечно, изучаю Модбас, набрасываю варианты реализации…
    Но очень не хочется перелопатить всю программу, чтобы потом увидеть, что никакой возможности работать у устройства нет и не могло быть.
    Можете ли Вы вот так просто «навскидку» сказать, является ли задача решаемой? Если нельзя сказать однозначно — ну, уже хорошо, буду продолжать.
    Для справки: в том устройстве таймеры использованы «на всю катушку», один из них (Т1) работает как генератор скорости для UART, остальные два — для измерительных задач. В основном цикле main() возможны задержки (например, на запись в ЕЕПРОМ), поэтому временные рамки, столь важные для RTU, меня особенно тревожат. Системный клок тикает с частотой 100 Гц.
    Достаточно работать хотя бы на 9600 бод.
    Как не очень хороший вариант, перейду на ASCII…
    Спасибо!

  • arktur04

    1. Спасибо за комментарий. Продолжение будет, но когда точно, сказать не могу.
    2. С такой задачей, как Modbus RTU, может справиться практически любой контроллер
    3. Однако, если программа «завиасет» для выполнения каких-либо задач на время, большее, чем передача ~2 байт через UART, обмен по Modbus будет прерван мастером. Поэтому, желательно не делать таких перерывов (будем считать передачу 1 байта примерно за 10/9600 ~ 1ms). Ситуацию можно решить запретом прерываний, или алгоритмически, в общем, как удобнее.
    4. Но модификация уже готовой программы может оказаться очень трудоёмкой задачей (особенно, если её писали не вы), поэтому если есть любая возможность обойтись ASCII, лучше использовать её. В ASCII нет ничего плохого.

    С уважением, Владимир.

  • Юра

    Спасибо за ответ!
    Практически, это и есть то, что я спрашивал. Значит, говорю я себе, можно продолжать ковырять программу.
    Я начинал ту программу. Вся измерительная задача — это мое решение. Общая структура — тоже. Потом ребята дорабатывали, но в общих чертах все ясно.
    Не можете ли Вы направить, что почитать по Модбасу? Просто Гугль дает кое-что, вот и Ваш блог, спасибо ему. И исходные MODBUS APPLICATION PROTOCOL SPECIFICATION V1.1b от Modbus-IDA и Modicon
    Modbus Protocol Reference Guide от отцов-основателей я изучаю. Но совсем не помешали бы еще доходчивые материалы.
    Например, мой уровень понимания недостаточен для ответа на простой вопрос: как начинать и как прекращать прием инфо в буфер? Я все-таки говорю об RTU. Должен ли я работать только по временнЫм отсчетам — я говорю про те 14 бит тишины. Или где? :)

    Возможно, в продолжении Вашего интереснейшого урока можно учесть такие (пусть и глупые) вопросы.

    С уважением,
    Юра

  • arktur04

    Массу информации можно найти по адресу http://modbus.org/tech.php. Однако можно обойтись и без сложностей. Я писал реализацию протокола для микроконтроллера, пользуясь только Modicon-овской документацией.

    Да, приём сообщения прекращается при наступлении интервала 3.5-4.5 длительностей передачи одиночного символа, если мне не изменяет память. После этого контроллер может начинать декодирование сообщения. В ряде микроконтроллеров, в том числе и в LPC17xx (ARM Cortex M3) в UART есть возможность генерации прерывания по тайм-ауту (который можно программировать), что облегчает задачу и позволяет обойтись без дополнительных таймеров.

    Однако, на практике, посылки Modbus никогда не идут друг за другом с таким интервалом. Modbus RTU работает строго по схеме «запрос-ответ», т.е. мастер будет ждать ответа слейва определённое время, и если ответа нет, пошлёт запрос снова. Это время может быть достаточно большим (до нескольких секунд), и зависит от конкретного устройства (в большинстве панелей и SCADA-систем это время настраивается). То есть, если у вас есть таймер с интервалом 10 мс, можно использовать его для выявления конца сообщения от мастера, и быть уверенным в том, что мастер не пошлёт повторный запрос. Однако увеличение временного промежутка между запросом и ответом естественно, замедлит обмен, что может быть критично, если мастер циклически считывает множество разных регистров Modbus.

    Насчёт вопросов, они вовсе не глупые, наоборот, хорошие вопросы. А статью про Modbus я допишу, просто сейчас со временем не очень.

    Владимир.

  • Юра

    Нашел по Вашей ссылке еще один полезный документ: MODBUS over serial line specification and implementation guide V1.02. Многое прояснилось.
    Но вот что удивляет. На стр.13 этого документа (и в других документах это есть) сказано, что при скоростях свыше 19200 рекомендовано применять фиксированное время t1.5 = 750мкс и t3.5 = 1750 мкс. В общем-то, понятно, что на высоких скоростях трудно обеспечить соответствие этим временам 1,5*4/BAUDRATE и 3,5*4/BAUDRATE. Но ведь для скоростей 9600 и 19200 получается, что вычисленные таймауты уже меньше фиксированных! Как-то не логично. Я думал, что при постепенном увеличении скорости работы вычисленные времена сокращаются и, например, на 19200 становятся равными рекомендованным фиксированным.
    Или я не правильно вычисляю?
    4800:
    3.5 — 2917 мс
    1.5 — 1250 мкс
    9600:
    3.5 — 1548 мс
    1.5 — 625 мкс
    19200:
    3.5 — 729 мс
    1.5 — 312 мкс
    Далее фиксированные:
    3.5 — 1750 мс
    1.5 — 750 мкс

    Как Вы прокомментируете?

  • arktur04

    Вообще логично. На высоких скоростях «вычисленные» таймауты становятся слишком малы, и «для надёжности» их рекомендовано увеличить.
    Однако на вашем месте я бы не стал особо этим загружаться, величины таймаутов не очень важны (см. мой предыдущий ответ). Их можно сделать и гораздо больше.

  • Юра

    И снова здрасте! Позволю доложить некоторые результаты, которые могут быть и не очень важными, но они точно относятся к реализации Модбаса на мелких контроллерах.
    Я нашел библиотеку для реализации Модбаса на AVR (и на других процессорах) вот здесь: http://freemodbus.berlios.de/
    Запустил на Атмеге168, работает. Потом немного разобрался, что нужно, что не очень — с учетом моих предпочтений и решил ее немного доработать напильником. Дело в том, что автор Кристиан Вальтер, с целью имплементации этой беды на многих машинах и во всей красе (как RTU, так и ASCII, и TCP/IP) «скушал» самый жирный таймер из имеющихся на АТмеге168 — таймер1. Оставшиеся — однобайтные и не имеют режима защелки по фронту внешнего сигнала. Вот я и изменил таймер — сначала на таймер0, а потом на таймер2. При этом (совершенно сознательно) оставил за бортом возможность работы в ASCII режиме. Но меня устраивает RTU и свободные таймеры 0 и 1.
    Конечно, руки чешутся упростить программу, ибо как раз широта подхода автора определила кучу колбэков и приходится бродить по файлам в поисках того кода, который реально исполняется, например, в прерывании от принятого байта. Думаю, что и при исполнении это влечет приличные накладные расходы. Но… работает — и слава Богу.
    О чем хотел бы посоветоваться. Отработка входных команд на уровне парсинга буфера и проверки синтаксиса там реализована для довольно большого числа функций Модбас — 14 шт:
    #define MB_FUNC_READ_COILS ( 1 )
    #define MB_FUNC_READ_DISCRETE_INPUTS ( 2 )
    #define MB_FUNC_WRITE_SINGLE_COIL ( 5 )
    #define MB_FUNC_WRITE_MULTIPLE_COILS ( 15 )
    #define MB_FUNC_READ_HOLDING_REGISTER ( 3 )
    #define MB_FUNC_READ_INPUT_REGISTER ( 4 )
    #define MB_FUNC_WRITE_REGISTER ( 6 )
    #define MB_FUNC_WRITE_MULTIPLE_REGISTERS ( 16 )
    #define MB_FUNC_READWRITE_MULTIPLE_REGISTERS ( 23 )
    #define MB_FUNC_DIAG_READ_EXCEPTION ( 7 )
    #define MB_FUNC_DIAG_DIAGNOSTIC ( 8 )
    #define MB_FUNC_DIAG_GET_COM_EVENT_CNT ( 11 )
    #define MB_FUNC_DIAG_GET_COM_EVENT_LOG ( 12 )
    #define MB_FUNC_OTHER_REPORT_SLAVEID ( 17 )
    А часть, относящуюся к аппликации, автор написал только для одной функции 04. Ну, для демо этого достаточно. Значит, надо ручками поработать.
    Вот я и думаю, что же оставить? Склоняюсь буквально к двум функциям (Вы говорили где-то о 3-х) — 3 и 16. Если я предполагаю в своем устройстве ОБЫЧНО вычитывать минимум 2 регистра (слово состояния и результат измерения) и именно это самая быстрая и частая операция — то что еще нужно? Записать даже 1 регистр командой 16 можно — и я это делаю в «неспешных» моментах. А читать только биты мне просто не нужно.
    Что касается прочих команд, то, при жедании, я могу вытащить что угодно, если использовать технологию «узкого окошка», через которое я адресую любой из моих нескольких десятков параметров и вычитываю его — в специальных режимах…
    Есть ли какие-то соображения, которые я мог упустить?

    Извините за многословность. Это стиль. Не могу себе изменить :)

  • arktur04

    Благодарю за подробный комментарий и за ссылку.
    Я в данный момент реализовал функции 3, 6 и 16. Теоретически для любых целей хватит функций 3 и 16, просто есть устройства (операторские панели), которые используют ещё и функцию 6 при работе с holding reisters.

    Я использую следующий подход к хранению и чтению переменных: все переменные, доступные по Modbus (т.е. все настраиваемые параметры прибора, результаты измерений и т.д., всего ~300 переменных), хранятся в виде 32-битных переменных (signed int или float), т.е. каждая из них отображается на 2 смежных регистра Modbus. Это позволяет унифицировать функции работы с переменными и избавляет от необходимости думать о том, какая переменная какую разрядность имеет.
    Булевые значения также записываются как int (нулевое значение — false, ненулевое — true). В принципе, их можно упаковывать в Int32 по 32 штуки, но у меня в проекте булевых значений мало, и лишние сложности ни к чему.
    С уважением,
    Владимир.

  • Юра

    У Вас подход, опирающийся на роскошь больших камешков. Ведь получается, что Вы храните в памяти обмена (ну, те самые регистры) все переменные — около 1.2К. А по жизни они, наверное, еще и где-то хранятся? Или Вы прямо оттуда их и пользуете в аппликации?
    Если в работе Вы используете некие копии переменных, хранящихся в области обмена, то нужно отслеживать изменения этих переменных и регистров. Изменилась переменная в ходе работы — нужно ее копию пихнуть и в область обмена. Пришло новое значенние от Мастера — нужно его сберечь из области обмена в рабочую область.
    Как-то сложно все. Или я чего-то не понимаю?

  • arktur04

    Нет, копий нет, все переменные хранятся в единственном экземпляре. Это похоже на подход, используемый в ПЛК, при программировании в среде IsaGraf, например. Там есть единая структура — «словарь». В нём хранятся все именованные переменные и их атрибуты, включая и адрес Modbus.
    У меня так же. Есть структура (массив), в котором хранятся все переменные, доступные по Modbus и сохраняемые в энергонезависимой памяти. Доступ к ним из программы осуществляется через специальные функции по номеру переменной (тегу). В этой же структуре хранятся адреса Modbus и адреса в энергонезависимой памяти. Переменная может иметь и тот и другой адрес или не иметь одного из них (потому что если она не доступна по Modbus и не сохраняется в памяти, то её просто не имеет смысла записывать в словарь). При изменении значения переменной, сохраняемой в энергонезависимой памяти, она немедленно перезаписывается, неважно, откуда она была изменена — по Modbus или через интерфейс прибора. Алгоритм программы всегда, таким образом, оперирует актуальными значениями переменных, избыточности (т.е. двух копий одной и той же переменной) при этом нет.

    А насчёт «больших камушков», сейчас цена контроллеров на ядре ARM7 совсем низкая. Фактически, в разрабатываемом приборе микроконтроллер — одна из самых дешёвых микросхем. Так что я не вижу причин применять более слабые микроконтроллеры.

  • Юра

    Вот бы интересно было бы именно с организацией работы с переменными поближе ознакомиться! Может ткнете на ссылку почитать?
    Чтобы не напрягать Вас сверх меры, позволю пофантазировать самостоятельно. Если Вам станет в облом мои фантазии комментировать — прямо так и скажите. Я не обижусь.
    Итак, как бы сделал я. Есть 2 свойства переменной, которые позволяют говорить о ней, как об участнике словаря: доступность для Модбаса и сохраняемость в ЭПЗУ. Любого одного свойства достаточно.
    Для упрощения единообразия работы сохраняем наши словарные переменные в одном размерчике. Скажем, 32 бита (у меня может быть и 16). Делаем массив структур, каждая из которых содержит самое переменную, а также ее дескрипторы — адрес в Модбасном поле, признак сохраняемости, режим доступа (кто может изменять) и т.д. Например, может быть полезным такой описатель, как номер этого параметра работы по описанию прибора. Вовсе не обязательно этот номер равен номеру регистра Модбас. Ну, это так. к слову.
    Отступление в сторону.
    Подобный массив структур я уже делал. Но там не было одного: самой переменной. Там я ставил только указатель на переменную. Что мне это давало? В ходе исполнения программы я обращался к любой из переменных словаря просто по их именам. Никакого вычисления адреса (хотя я понимаю, что обращение типа a[SCALE].v при исполнении ничего не требует — там все компилятор вычислит), простое визуальное использование — имя, да и делов-то…
    Но что выкручивало руки? До именно тот указатель. При разных типах переменных (сам не знаю, зачем я так экономил) пришлось потанцевать, чтобы можно было обращаться к переменной уже не по «родному» имени, а через массив структур.
    Конец отступления в сторону.

    Итак, есть массив структур. Теперь о правилах работы. Ну, использование конкретной переменной просто в программе — это не обязательно даже спциальные функции PUT() и GET(), о которых Вы говорили. Зачем? Я могу обратиться к переменной, которуя я имею в виду, как «SCALE», просто так «a[SCALE].v», где «a» — имя массива структур, а «v» — элемент структуры, содержащий само значение переменной.
    Если я записываю новое значение — оно уже в области обмена Модбаса. Обращение Мастера сети вызовет извлеченеи актуального значения.
    Если Мастер запишет туда новое значение, то я и извлеку это значение.
    Теперь о Модбасе. В силу строгого правила, что все регистры Модбаса есть участники словаря, я реализую абсолютно все записи новых значений регистров через специальную функцию (по номеру регистра — индекс в массиве структур). Более того, функция может ставить в системе событие — обновлена переменная словаря (я не знаю, как это событие использовать, пока).
    Аналогично, при запросе от Модбаса, все упрется в конечную функцию извлечения элемента массива — тоже через функцию. И тоже можно взвести EVENT (может, подскажете, зачем?).
    Ну, а сохранение в ЭПЗУ — вроде бы еще проще. Я бы огранизовал слежение за EVENTом «изменение значения сохраняемой переменной» и просто по системным часикам отзывался на это время от времени. Это бережет ресурс записей в память (если память на флеше).

    Вот этот поток сознания — похоже что-то на то, как организовуете работу со словарем Вы?

    Я хотел бы улучшить свои подходы. Потому и советуюсь, расспрашиваю. Вот, Ваш коммент по поводу «камушков» — тоже ачал рассматривать. А какие АРМы Вы бы посоветовали — если речь идет о замене АВР, причем в ситуации, когда АВР «почти хватает для полного счастья»? Очень желательно при этом оставаться в мейнстриме, работать с контроллерами, достаточно наработанными, с библиотеками, с простыми средствами отладки. Да и вообще, близкими к АВР-кам по духу.

  • arktur04

    Да, всё почти так, как вы описали.
    Можно добавить следующее: фактически массивов получается два (равного размера), один массив дескрипторов переменных, константный и размещённый в ПЗУ, второй массив собственно переменных, в ОЗУ. Обращение к значениям переменных происходит через ф-ции Get/Set, которые внутри себя обращаются напрямую к переменным (типа values[SOMETAG]). Если эти функции сделать inline, то компилятор будет заменять их на прямые обращения к массиву (я думаю, что оптимизирующий компилятор и так их заменит на прямые обращения).
    Отдельных флагов для описания наличия адреса Modbus или адреса ЭПЗУ, если переменная на участвует в Modbus или не сохраняется, то её адрес будет просто -1, в соответствующих функциях доступа этот адрес проверяется (if(addr > -1){…}).

    Событие об обновлении значения переменной есть, оно может использоваться для обновления значений данных в ЭПЗУ, например. Событий при чтении переменных нет (незачем).

    Теперь о железе. В качестве ЭПЗУ я использую память FRAM, не имеющую ограничений по количеству записей (или имеющую теоретическое ограничение ~миллиарда циклов записи), с SPI интерфейсом. В отличие от EPROM-а, и, тем более, FLASH, она производит запись со скоростью обращения, избавляя от необходимости ждать конца записи.
    В качестве контроллера я использовал Atmel SAM7 (ядро ARM7) и с LPC (ядра ARM7 и Cortex M3). В настоящее время это основные производители микроконтроллеров такого типа (ещё STM, но я с ними не работал). LPC мне лично нравится больше: практически пустой Errata Sheet, высокое быстродействие). Atmel тоже неплох, в общем, но у него есть особенность: так как интерфейс между ядром и ПЗУ 16-битный, выборка команд занимает длительное время (может быть, сейчас они что-то поменяли, я имею в виду кристалл at91sam7sxx). У LPC1768 интерфейс flash-ядро 128-битный, и выборка команд происходит со скоростью их обработки, без циклов ожидания. Но если высокая скорость не критична то Atmel at91sam7s64 — at91sam7s256 являются хорошим недорогим решением. Разумеется, вам также понадобится отладчик JTAG.
    Владимир.

  • Avernul

    Очень интересный цикл статей! Большое спасибо! Наконец-то начал понимать что такое Modbus. А будет ли продолжение данных статей? А то, как говориться, «на самом интересном месте…».

  • arktur04

    Не знаю, будет ли продолжение. Пока на это нет ни времени, ни особого желания.
    Однако все возможно…