Простая реализация Модбас-стека на MSP430. Часть вторая: Библиотека

Продолжаю тему работы простых МК-устройств с Модбасом. В предыдущей части я кратко описал сам Модбас, тот вариант работы, который я использую (RTU), а также свои подходы к реализации протокола у Слейва. Тепер я хочу рассказать об очень простой, но вполне рабочей библиотеке, написанной на Си, а потому годной не только для моего любимого MSP430FR57xx семейства.
А вот здесь — улучшеный вариант библиотеки.
Напомню, реализован Modbus RTU по последовательному протоколу RS-485.
Итак, программа создана в виде статической библиотеки и состоит из 5 файлов:

MB_header.h — дефайны из driverlib MSP430, дефайны Модбаса, несколько перечислений ENUM и простых макросов. При портировании сюда зайти обязательно, ясен перец.
MB_entries.c — для простоты и понятности создал 2 функции, к которым и обращаются внешние программы. Портируется без правок, здесь ничего аппаратно-зависимого.
MB_hardware.c — здесь 2 функции инициализации 3-х периферийных модулей: таймера, УАРТа и 3 битов порта (RxD, TxD и направление драйвера шины RS-485, сигнал, который я называю RTS). Конечно, это очень платформо-зависимый файл
MB_interrupts.c — собственно, подпрограммы отработки прерываний (ISR) от таймера и УАРТа. Несмотря на то, что оформление ISR традиционно скачет от компилятора к компилятору, сами процедуры править при портировании на другие МК нет необходимости.
MB_parsing.c — главная функция разбора запроса от Мастера (и подготовки ответа, разумеется), а также несколько ее помощников, включая вычислителя CRC.

По сути, вся работа идет в прерываниях и немножко в парсинге. Как же организована эта работа? Сам собой просится конечный автомат, машина состояний, фазовый автомат — кто как любит называть. Основой этого автомата является переменная MB_state, принимающая перечисленные значения:

typedef enum {
    STATE_IDLE,  /* Ready to get a frame from Master           */
    STATE_RCVE,  /* Frame is being received                    */
    STATE_PARS,  /* Frame is being parsed (may take some time) */
    STATE_SEND,  /* Response frame is being sent               */
    STATE_SENT   /* Last byte sent to shift register           */
} eMBState;


В исходном состоянии автомат готов принять первый символ от Мастера, после чего записывает этот символ в начало приемного буфера uint8_t MBBuff[] и сразу же переходит в фазу приема RCVE. В этой фазе автомат находится до тех пор, пока символы идут сплошной чередой, даже если информация в них — полная чушь. Наше дело — принимать и ждать конца фрейма. Как только возникнет разрыв между символами, больший t3.5 (длительность передачи 3,5 символов) — автомат переходит в фазу разбора фрейма PARS.
Парсинг заканчивается сам по себе, без внешних событий. На моем неспешном 7-мегагерцовом МК она может выполняться, скажем, 300-400 мкс. Это «искусственная» фаза, но удобная для понимания и отладки программы.
В ходе парсинга выявляется один момент, существенный для фазовых переходов: будем слать ответ или ну его… Отсутствие ответа — это и плохой фрейм (например, разрыв в принимаемых символах, больший t1.5), и адресация запроса не нам, и широковещательный запрос. В любом случае, следующей фазой будет IDLE, ибо ждать t3.5 просто незачем — мы же только что его поймали.
Если же отвечать придется, то из парсинга переходим в фазу передачи SEND. При этом парсер бросает в передатчик первый символ, переключает микросхему драйвера шины на передачу, а уж дальше все проходит в прерываних. Там же, в очередном прерывании, обнаруживается, что послан последний символ. И там же ставится фаза SENT, короткая, всего лишь ожидание последнего вытолкнутого в линию бита. По прерыванию от УАРТа «Сдвиговый регистр пуст» драйвер шины переключается на прием и автомат переходит в фазу ожидания IDLE.

Работа с фазами непосредственно в прерываниях позволила получить простой код, к тому же, не требующий большого быстродействия МК или пристального внимания к работе Модбас-стека. Фактически, из основной программы следует один раз вызвать функцию MB_HW_init(), а затем в цикле вызывать MB_serve(). Как часто? Нечасто. Все переходы фазового автомата происходят в прерываниях, пропуска события при приеме и передаче не будет. Единственное — парсинг принятой команды запускается при очередном вызове из главной петли функции MB_serve(). Поэтому и формирование ответа Мастеру будет зависеть от того, как часто эта фунция вызывается. В моем девайсе это максимум 1...2 мс, но даже какой-то неспешный девайс редко крутится с периодом больше пары десятков миллисекунд. За это время Мастер не махнет рукой на больного Слейва.
Есть еще более быстрый вариант: не выходить за пределы прерываний вообще. В чем суть: впихнуть функцию MB_Parsing() прямо в прерывание таймера, в котором стало ясно, что пора переходить к парсингу:

__interrupt void TIMER1_A0_ISR_HOOK(void) {   // t3.5 hunting
  if( STATE_RCVE == MBState) {                // It`s the end event: t3.5
    SET_ST(STATE_PARS);                       // Begin parsing of a frame
    STOP_TIMER;
    MBParsing();   // Д О Б А В Л Е Н А Я   С Т Р О К А
  }
}

Здесь я добавил вызов парсинга (компилятор, если он не дурак, предупредит, что это не есть гут) там, где фаза становится PARS. И все, ребята! Достаточно теперь в главной программе произвести инициализацию Модбас-стека, как он начнет сам следить за линией, принимать, отвечать — и все это асинхронно к работе основной программы. Очень интересный вариант, но я его не использую: во-первых, некуда спешить, во-вторых, нужно побеспокоиться о правильном обращении к регистрам со стороны главной программы и Модбас-стека. Фокусы могут быть при изменении части переменной…

Замечание по использованию библиотеки
Приведенные в приложении файлы можно использовать для наскальной росписи, как основу для создания своих вариантов, но можно и как готовую библиотеку. Каковы связи с основной программой и с «железом»:

1) Используется таймер, УАРТ и 3 вывода МК (или 2, если работать с RS-232). Они должны быть инициализированы:
— таймер нужно настроить на генерацию прерывания через 3,5 символа после его запуска (формула расчета видна в инициализации массива Nt35[], файл MB_hardware.c), а также иметь возможность читать его текущее состояние, чтобы отловить пробел 1,5 символа;
— УАРТ настраивается по стандартным требованиям Модбаса, они видны в тексте функции MBUART_init();
— порты RxD и TxD могут становиться таковыми при инициализации УАРТа, а могут (как в MSP430) требовать отдельной инициализации — как вторичная альтернативная функция порта. Кстати, удивился, обнаружив, что порт RxD принимает линию даже если его забыть переключить в альтернативную функцию! Ну, это подробности моего ФРАМ МК.

2) Частота тактирования таймера и УАРТа получается из какого-то генератора, поэтому в файл MB_header.h нужно внести информацию о частоте тактирования.

3) При инициализации Модбас-стеку передаются параметры:
baudrate_code и IDcode — коды скорости работы и адреса Слейва в системе. Эти коды служат индексами для выборки из таблиц, поэтому можно сделать их и не 2-битовыми (как у меня), а любой размерности- только таблицы правьте;
regsinp и regsout — указатели на начало двух массивов регистров, работающих с Модбасом. У меня их 2 для того, чтобы с одним работать (и из него Мастер получает данные, если делает запрос чтения), а в другой принимать данные от Мастера — и иметь возможность либо акцептировать данные, либо отказаться от них. Соответствующее уведомление Мастеру никак не оговорено в стандарте, но я использую один из регистров как Статус — в нем и поднимаю флажок, ежели что. Пусть Мастер читает и знает, что мы тут тоже не лыком шиты, можем и послать…
regsnumb — размер массивов регистров (одинаковый для обоих массивов)
Собственно вот часть главной программы:

#define	FQUARZ_HZ	7372800UL
#define	REGISTERS	25		

void vInit(void);
extern void MB_HW_init( uint16_t, uint16_t, uint16_t*, uint16_t*, uint16_t);
extern void MB_serve( void);

uint16_t	ParsWk[REGISTERS];
uint16_t	ParsIn[REGISTERS];

int
main(void) {
	uint16_t	BR_code, ID_code;
 	vInit();
    BR_code  = CHK_PIN( BR0_PORT, BR0_PIN)? 1:0;
    BR_code |= CHK_PIN( BR1_PORT, BR1_PIN)? 2:0;
	ID_code  = CHK_PIN( SLVID0_PORT, SLVID0_PIN)? 1:0;
	ID_code |= CHK_PIN( SLVID1_PORT, SLVID1_PIN)? 2:0;

 	MB_HW_init( BR_code, ID_code, ParsIn, ParsWk, REGISTERS);
    __bis_SR_register(GIE);

    do {
    	MB_serve();
    } while( 1);
}

4) Я добавил (спасибо коллеге Lagman) вариант работы без прерывания по приему первого бита посылки. Такое прерывание есть у «моего» МСП430, но в той же Атмеге168 — увы. Поэтому я просто поставил директиву условной компиляции NO_06_INTERRUPT — и если это дефинировать, то Атмега сможет работать;

5) В макросе SET_ST(s) я оставил закомментированный вывод кода текущей фазы на 3 порта. Собственно, и весь макрос для этого создан. Желающие могут открыть комментарии, использовав те средства визуализации, которые им доступны. Очень красиво наблюдать смену фаз:


Слева имеем фазу 000 — это IDLE. Когда принят первый символ, код фазы становится 001 — это RCVE. Кстати, это картинка с включенным NO_06_INTERRUPT, поэтому переход в прием происходит в конце первого символа, а не в его начале.
Через 1780 мкс после приема последнего импульса видим код фазы 010 — это PARS. И продолжается парсинг и подготовка ответа около 340 мкс (короткий нулевой импульс в самом нижнем сигнале). А потом идет код 011 — это передача, SEND. Короткий импульс длиной 260 мкс — фаза SENT, когда ждем сигнала на разворот драйвера шины на прием. Спасибо коллеге reptile за ЛА!

6) CRC. Эх, братцы, жаль, что в MSP430FR57xx модуль CRC имеет другой полином! Пришлось взять известную (она приведена прямо в описании Модбаса) функцию с таблицами. Конечно, желающие могут легко ее заменить, секономив 512 байт флеша.

Прилагаю 2 архива: один с файлами библиотеки, второй — с простой вызывающей оболочкой, в которой желающие могут посмотреть мою реализацию работы с портами МК (файл pins.h с кучей макросов). Все вместе компилится CCS от TI и результирующая программа весело отвечает на запросы Мастера нулевыми значениями регистров :)
  • +8
  • 09 ноября 2013, 17:09
  • drvlas
  • 2
Файлы в топике: My_C_files.zip, MB.zip

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

RSS свернуть / развернуть
Спасибо, интересно.
Один вопрос: почему бы для определения интервала в 1,5 символа не использовать режим сравнения таймера?
0
Так а смысл? Делать перерывание? Ставить флаг, который потом анализировать — а именно это надо сделать, когда придет следующий импульс. Иначе событие само по себе ничего не значит. А у меня просто в момент прихода следующего импулься делается чтение состояния счетчика-таймера и сравнивается с t1.5 (если работаем прерыванием по началу байта) или t2.5 (если работаем только по прерыванию в момент прихода всего байта). То есть, мне кажется, что это достаточно просто.
Честно говоря, я вообще все еще не верю, что так просто все получилось. Может дальнейшие испытания выявят какие-то мелочи… Я продолжаю допиливать весь проект, подправлю, есличьо…
0
Ага…
Тогда и правда все логично получается. Просто я исходники не смотрел еще, потому неправильно понял сам алгоритм.
0
Та я мучился: знаю, как неохота загружать и раскручивать архивы с х.з. чем, хотел в тексте привести хоть самые важные фрагменты. Но табуляция едет, мать ее…
0
В код не смотрел ещё, но, по описанию медленновато получается, по моему. Если МК не сильно занят чем либо только..., кроме как принимать запросы и выдавать нулевые ответы.
А нельзя разве принимать решение о дальнейшем приёме байтов по результату сравнения — адреса устройства, на правильность номера функции, ну и по номеру функции можно определять количество принимаемых байт — я имею в виду в посылке присутствует же поле с количеством регистров. Это всё чтобы МК не молотил попусту, и занялся какой нибудь другой работой.
0
Все можно. Протокол простой. Но это все общие рассуждения.
Во-первых, о каких нулевых ответах ты говоришь? Это у меня в тестовом примере регистры не инициализированы и не заполняются чем-то полезным — потому и молоти нулевые ответы.
Во вторых, стандарт все же надо уважать. Описано, что адрес проверяется только после контроля по CRC. Кагбэ намекают, что нефик смотреть содержание сообщения, пока не подтвердилась его целостность. А адрес — уже содержание, он тоже входит в контрольку. Да, мало что может случиться, но я могу придумать ситуацию, когда вот такой «предварительный» приговор вреден. Лучше уж по-закону: сначала примем, проверим целостность фрейма, потом разбираем.
В-третьих, прерываться на каждый входной байт все равно нужно. Иначе пропустим конец сообщения. Понимаешь? А теперь посмотри исходники: затраты в прерывании на именно прием байта ничтожны. Избежать их — ничего не выиграешь.
Так что я принимаю весь фрейм, хотя и знаю, что он уже не мой (или коцнутый, если вдруг 1,5 символа паузы выскочило).
А по номеру функции что ты угадаешь? Во-первых, есть ряд функций, у которых фрейм переменной длины. Во-вторых, аналилизровать сообщение до его полного приема, как я уже сказал, не есть правильно. В-третьих, МК очень мало занят модбасом. Грубо говоря, 1...2..3%. Есть смысл что-то выдумывать?
0
А адрес — уже содержание, он тоже входит в контрольку.
Какая разница? Проверяем адрес сразу. Если совпадает — принимаем, если это был чужой адрес, но его попортило помехой — фрейм потом отбросится по CRC. Если же адрес не совпадает — это не наш фрейм, либо адрес в нем битый. В любом случае — фтопку.
В-третьих, прерываться на каждый входной байт все равно нужно. Иначе пропустим конец сообщения.
А зачем нам конец сообщения, если мы его все равно не принимаем? Достаточно выставить автомат на ожидание SOF'а (т.е. паузы 3.5t).
+1
Проверяем адрес сразу
Ну, можно, кто ж спорит. Посчитаешь смысл, например, в выигрыше процессорного времени? Потом продолжим.
Достаточно выставить автомат
А как автомат будет работать? Назови его правильно: автомат, который будет ставить счетчик на нуль с каждым законченным приемом символа, а когда значение счетчика достигнет кода, соответствующего t3.5, переводящего наш фазовый автомат в состояние IDLE. Как это реализовать без работы УАРТа и таймера? Без участия процессора?
Мы спорим о мелочах. У меня есть программа, если она в чем-то ошибочна или может быть улучшена — во, это важно. Ибо (я надеюсь) ее все же кто-нибудь да позаимсвтует. А выиграть единицы процентов ресурса МК путем сомнительных отступлений от требований стандарта — мне не хочется.
0
Ловля пауз как маркеров старта — это, как раз, не отступление.
Назови его правильно: автомат, который будет ставить счетчик на ...
Нет, это как раз не правильное название. Оно ограничивает варианты твоей конкретной реализацией. Но она не единственно возможная, и ее вполне можно при необходимости переделать.
А выиграть единицы процентов ресурса МК
Может и единицы, зато на самом высоком приоритете. Причем с минимальными изменениями кода и без влияния на надежность, совместимость или что-либо еще. Впрочем, это и не обязательно.
+1
В данном. Но библиотека позиционируется как «берите и пользуйтесь». Хотя да, разницы между игнорированием байта (принимать их все равно придется, чтобы ловить SOF, да) и складыванием его в буфер практически ноль.
Вот если проверять адрес только после контроля фрейма по CRC — расход ресурсов заметно вырастет.
+1
расход ресурсов заметно вырастет
Ну… не «заметно». Я, когда оценивал потери в единицы процентов, то имел в виду потери на прерывания — они идут достаточно часто (при 38400 бод — каждые 286 мкс). Но по ним, как мы уже все согласны, разницы особой нет, пихаем ли мы байт в буфер или только передергиваем таймер. А вот расчет CRC — один раз на весь принятый фрейм, он уж делится на приличное время (несколько миллисекунд).
В общем, ты меня не переубедил. А желающие могут сделать — в прорамме хорошо видно, где можно анализировать адрес в момент его приема.
0
Коллега, спасибо за статью.

Позволю себе немного критики. Вы, ИМХО, неудачно декомпозировали (разбили по файлам) реализацию. У Вас много файлов, и все они и имеют большую связанность с остальными (обмен через глобальные переменные и т. д.) А вот четко выраженного интереса у вас нет (вернее но есть, но он разбросан по файлам, и сходу непонятно как ним пользоваться). Я ожидал, что открыв MB_header.h я увижу интерес — а его там нет.

Я бы Вам советовал:

1. выносить интерфейсы в отдельный *.h фал и избегать предеклараций внутри «сторонних» *.с файлов. Проще говоря, для каждого *.с делать *.h с предекларацией экспортируемых функций. В саму предекларацию функций включать названия параметров. Согласитесь что описание
void somsing_init( uint16_t baudrate_code);
намного информативнее, чем
void somsing_init( uint16_t);


2. в *.h всегда включать «защиту» от повторного включения
#ifndef _SOME_H_
#define _SOME_H_
…
#endif


ну, или #pragma once

3. избегать связанности кода, экспорта глобальных переменных и глобальных переменных в принципе.

Еще раз извиняюсь за критику.
+1
Извиняться не за что, коллега. Это вполне конструктивная критика.
Вообще-то, в проектах у меня 1-2 хедера, хотя можно и больше. Но увеличивать их число я хотел бы не по количеству файлов, а по смыловой нагрузке — что какой хедер определяет.
Например, общие инклуды, версии и варианты компиляции («главный» хедер, включаемый во все файлы).
Затем можно организовать отдельный хедер с typedef-ами (или слить его в «главный», это близкие хедеры — в том смысле, что включаются во все файлы).
Близкими к перечисленным являются дефайны всяких аппаратных сущностей — пины, частоты и прочее.
И, наконец, есть еще вот та самая (пре)декларация функций. Мне кажется, ее можно разносить по тем файлам, где декларируемые символы используются. Ну, в вобщий хедер включить разве что те глобальные имена, которые нужны нескольким файлам.
Почему я так себе вижу (и кое-что реализовал в критикуемой либе): потому что нет привычки работать в Эклипсе или подобной «услужливой» среде. Это здесь — навел мышу на имя, а тебе уже и макрорасширение, и можно перейти на определение имени… А в обычных редакторах все скучно: ищи-свищи, где там и как я дефинировал эту АБВГДЕ. Ладно еще, «я», а если «он», если люди роются в чужой проге?
Вот я и старался сделать как-то так, чтобы легко было найти все определения. Как получилось… Ну, может и неудачно. Вообще, был вариант слить весь код в один файл (и он был бы совсем небольшой). Может так и сделать?
0
 Мне кажется, ее можно разносить по тем файлам, где декларируемые символы используются.

Предположим, у вас есть *.с файл, который реализует некую void foo(int i);
И эта функция вызывается в сотне (условно) файлов в вашем проекте, и в каждом таком файле, вы явно вставляете предекларацию

void foo(int i);


Теперь, вы решили изменить архитектуру и поменять сигнатуру на void foo(char * c); Поменяли в файле реализации. Теперь вам нужно пройтись по всем остальным файлам, и поменять предекларацию и вызов там.

Основная проблема — вы где-то могли забыть поменять предекларацию (и соответственно, поправить вызов). И компилятор будет молчать. И линкер даже не ругнется. А в результате — крах программы.

А если предекларация существует только в одном месте, и вы ее поменяли — то компилятор не скомпилирует код, пока вы не поправите все вызовы foo().
+1
В больших проектах есть правила, понятно. Но я (в данной теме) не хотел бы расширять фокус на всю идеологию создания С-проектов. В конкретной обсуждаемой библиотеке важна _простая_ реализация Модбаса. И я сейчас хотел бы услышать ваше мнение, коллеги, по простому вопросу: выиграет ли читабельность всей либы от того, что я солью С-файлы в один, а остаточные декларации (их число уменьшится) — в единый хедер?
Переделывать либу под «большие» проекты я просто не хочу. Смысла нет. Она маленькая и хороша именно этим.
0
Да и в Notepad++ такое есть… Не в том дело. Я выкладываю проект и озабочен тем, чтобы его 1) максимально просто было понять и 2) приколхозить под разные МК. Первое требование выполняется наилучшим образом при сливе всего в один файл, второе — при разбивке на «железозависимые» и «независимые» части. Вы сейчас видите победу второго подхода — я разбил и прокомментировал как раз, чтобы атмеговцы или пиковцы видели, что тут чисто мсп-шное. Так пока и останется. Пусть коллктивный опыт «внедрения» подскажет. А то мы тут будем долго спорить об общих принципах. Это мне напомнило субдискуссию с уважаемым коллегой Vga по поводу быстродействия…
Вот пусть ребята запустят и пожалуются, что им не хватает того или сего — тогда я готов перелопатить либу.
0
Коллега, я не хочу настаивать на своем мнении.

Здесь дело не в размере кода/библиотеки (дополнительный *.h файл на размер не повлияет). Просто, в программировании, стараются избавится от «копипаста» от применения «ctrl-c, ctrl-v». Структурное программирование нас подталкивает к вынесению одинакового кода в функции. Аналогично, препроцессор С позволяет нам выносить одинаковые синтаксические структуры в отдельный файл, и избежать «копипаста». Позволяет, но не требует. Предекларация нужна программисту а не компилятору, многие компиляторы (зависит от настроек) реагируют на отсутствие предекларации как warning, а не как error.

Вопрос декомпозиции — это вопрос стиля (читаемости), а не работоспособности конечной программы. Есть общие рекомендации (например, давать переменным осмысленные названия). Но компилятору все равно, как называются переменные. Никто не заставляет Вас придерживаться определенных правил, но если Вы их придерживаетесь — это плюс для восприятия Вашего кода.

 по простому вопросу: выиграет ли читабельность всей либы от того, что я солью С-файлы в один, а остаточные декларации (их число уменьшится) — в единый хедер?
В один файл лучше не сливать. Лучше сделать два уровня API — физический (платформозависимая работа с USART), и логический — разбор и обработка протокола.
+1
Самое главное, какие качественно-количественные показатели получились.
Удалось «впихнуть в 1 килобайт»?
0
Плюясь и чертыхаясь, полазил по листингам и ручками посчитал: всего все Модбасное заняло 1696 байт, из которых 552 — вычислитель CRC с его 512 байтами таблиц. Так что следует признать, что в 1К не входим, даже если бы модуль CRC взять нетабличный или малотабличный. ТОгда что-то на уровне 1,2К я бы считал минимальной оценкой.
Но мне проще говорить о том, что в 1,7К входит нормально-функциональный Модбас с реализованныи функциями 03 и 16 (и добавление 1 функции обойдется в 40 байт)
Хотя я специально не минимизировал, конечно. Считаю, что получилось и так очень немного кода. И мне он понятем :)
А весь проект сейчас уверенно меньше 10К. Как я и предполагал, с 15К уверенно вошел в 10К, а уже все работает. Есть мелочи (типа странная работа вотчдога), но они уже не породят нового кода. Так что МСП340 продолжает радовать. Скоро полевые испытания, вот интересно будет!
0
у меня ~500 байт на STM32 (функции 3 и 16).
0
О, теперь начнем меряться пиписьками :) Выкладывай!
А если серьезно, то даже при всем уважении — я удивлен. Ладно, что я не гнался за минимизацией, но и не вижу настолько раздутого кода, чтобы его споловинить. Вот, смотрю: прерывания — 262 байта, парсинг с подготовкой респонса — 610, инициализация 80. Уже под кило. И из этого нечего выбрасывать, если не идти на отступление от стандарта. Странно… Неужто так играет роль использование структуры? Возможно, парсинг оказался бы чуть короче, но не в 2 раза.
Или ты не все посчитал — прерывания, инициализацию, CRC? Команды 03 и 16 или 03 и 06?
Не томи, колись :)
Кстати, если найдется рациональное объяснение возможностей сокращения кода — я готов переделать либу. Само ее появление здесь предполагало наличие предложений по улучшению — и отработку их. Выше были идеи по созданию более кашерной структуры кода, с физическим и логическим уровнями — они меня не вздрочили, ибо при такой простой библиотеке кашерность не критически важна, а рост кода будет. А вот если я пойму, где у меня набегает удвоение размера программы — о! придется призадуматься.
0
У кортексов Thumb-2 код ещё компактнее, чем у мсп (если я опять ничего не путаю)
С другой стороны, смысла жаться нет.
Написал float == прощайте ~4кБ
printf == ещё почти столько же
0
printf == ещё почти столько же
неправда. открываем map:

Code (inc. data)   RO Data    RW Data    ZI Data      Debug   Library Member Name
736         56          0          0          0        184   printf5.o

stm32f100c4
0
без %f, да и вообще флоаты я там не использую.
putc совсем маленький — 14 байт. (переписан)
0
смысла жаться нет
Не, ну интересно же! Жаться то нет, я согласен. То есть, я не стану изучать асм, я не стану жертвовать требованиями станарта на Модбас (скажем, в области таймаутов, отработки исключений, реагирования на широковещательные запросы и т.п.), не солью все в один файл, не заведу кучу глобальных переменных, через которые получу всю инфо для настройки оборудования… Все перечисленное могло бы сократить код. Но я лучше буду пользоваться описанной библиотекой, ибо (сейчас) вижу в ней баланс, как раз милый моему сердцу.
Однако, я немножко же и живой человек. И если есть способы оптимизации размера, не заставляющие «поступаться принципами», а даже наоборот, расширяющие мои горизонты — дык! Займусь с охотой.
Скажем, меня «муляет» то, что я не захотел создавать структуру, содержащую все данные, которыи обменивается главная программа с либой. Возможно, раскрутка длинного списка параметров функции — затратная вешь. Но насколько это даст выигрыш? Несколько байт? Вот бы и послушать комментарии от уважаемого коллеги reptile.

Мы все здесь чуточку заведены на перфектность. Вспоминаю недавнюю дискуссию (на форуме?) о сокращении кода в каком-то микроскопическом цикле, ого! Там копии ломали долго, я перестал даже следить :)
0
команды 03 и 16, остальные ненужны — зачем плодить сущности?
код примитивный — принимаем байты, складываем в линейный буфер.
по приему очередного — вызываем ф-ю парсинга.
если команда принята — пишем ответ в тот же буфер и возвращаем длину ответа.
если результат >0, остается передать его содержимое.
CRC — не табличный, да и нет смысла — в проце дури много, успевает даже на 115200 принимать без прерываний.
пользовательский код должен определить ф-и чтения/записи регистров mb_reg_get(a), mb_reg_set(a,v).
//command codes
enum { MB_CMD_READ_HOLDING_REGISTERS=3, MB_CMD_WRITE_MULTI_REGISTERS=16 };
//MB state structure
struct mb_struct {
	u8  addr;    //slave/master address
	u16 crc;     //current CRC
} mb[MB_BUSES];

void mb_init(u8 bus, u8 addr) {
	mb[bus].addr = addr;
	mb[bus].crc  = CRC16_MODBUS_INIT;
}

u16 mb_update(u8 bus, u8 *buf, u16 len) {
	u16 result = 0;
	if (len == 1) mb[bus].crc = CRC16_MODBUS_INIT; //reset CRC
	if (len >= 3) {
		//update CRC
		mb[bus].crc = crc16_modbus_upd(buf[len - 3], mb[bus].crc);

		//address & CRC ok
		if ((buf[0] == mb[bus].addr)&&(buf[len-2] == (mb[bus].crc & 0xFF))&&(buf[len-1] == (mb[bus].crc >> 8))) {

			//03 - read holding registers
			if ((buf[1] == MB_CMD_READ_HOLDING_REGISTERS)&&(len == 8)) {
				u16 addr = ((u16)buf[2] << 8) + buf[3]; //starting address
				u16 num  = ((u16)buf[4] << 8) + buf[5]; //number of registers
				result   = 5 + (num << 1);

				buf[2]   = num << 1;
				u8 *bufv = buf + 3;
				while (num--) {
					//store next value
					s16 val = mb_reg_get(addr++);
					*bufv++ = (u16)val >> 8;
					*bufv++ = (u16)val;
				}

				//calculate CRC
				u16 crc = crc16_modbus_buf(buf, result-2, CRC16_MODBUS_INIT);
				*bufv++ = crc;
				*bufv++ = crc >> 8;
			}

			//16 - write multi registers
			else if ((buf[1] == MB_CMD_WRITE_MULTI_REGISTERS)&&(len >= 11)) {
				u16 addr = ((u16)buf[2] << 8) + buf[3]; //starting address
				u16 num  = ((u16)buf[4] << 8) + buf[5]; //number of registers
				u8  bytes = buf[6];

				if (bytes == num*2) {
					u8 *bufv = buf + 7;
					while (num--) {
						mb_reg_set(addr++, ((u16)bufv[0] << 8) + bufv[1]);
						bufv += 2;
					}

					u16 crc = crc16_modbus_buf(buf, 6, CRC16_MODBUS_INIT);
					buf[6]  = crc;
					buf[7]  = crc >> 8;
					result  = 8;
				}

			}
		}
	}
	return result;
}


обработка:

		uart_update(); //обновить UART
		if (uart[0].rx.rcv) { //получен след.байт ?
		//вызвать обработку	
		u16 len = mb_update(0, &(uart[0].rx.buf[0]), uart[0].rx.cnt);
		//если есть ответ - передать	
		if (len) uart_tx_buf(0, &uart[0].rx.buf[0], len);
		}


CRC:
#define CRC16_MODBUS_INIT 0xFFFF
#define CRC16_MODBUS_POLY 0xA001
inline U16 crc16_modbus_upd(U8 data, U16 crc) {
    crc ^= data;
    for (U8 b=8; b; b--) {
        U8 flag_xor = crc & 1;
        crc >>= 1;
        if (flag_xor) crc ^= CRC16_MODBUS_POLY;
    }
    return crc;
}
U16 crc16_modbus_buf(U8 *buf, U16 len, U16 crc) {
    while (len--)
        crc = crc16_modbus_upd(*buf++, crc);
    return(crc);
}
0
Вау! Вот это да… Остроумно!
То есть, временные паузы похер? На каждый принятый байт, начиная с 3-х, пробуем все накопленное прокрутить на CRC. Если внезапно совпадает — считаем, что это и есть все сообщение. Верно? То есть, не переживаем о том, что кусок сообщения теоретически может тоже дать «правильную» СRC. А ведь может… Тогда начнем толкать ответ «поперед батька»?
0
временные тайм-ауты обрабатывает код UART.
CRC не прокручивается, обновляется только для одного байта.
я в пром.сетях не работаю, особых потоков в сети не планируется.
вполне достаточно совпадения адреса и CRC.
0
Но ведь с каждым принятым байтом проверяется условие: адрес наш и последние 2 байта, трактуемуе как CRC, дают совпадение контрольки. Так? Значит таки да, что на произвольном байте принимаемого сообщения мы можем решить, что уже аллес гут, приготовить и набухать ответ. В тот же буфер…
Или я неверно понял фразу
по приему очередного — вызываем ф-ю парсинга
и реально парсинг вызывается все же по обнаружению тишины?
0
мы можем решить, что уже аллес гут, приготовить и набухать ответ.
какая вероятность этого?
0
какая вероятность этого?

В общем случае, вероятность этого 1/65536

Коллега drvlas прав, разделение пактов «паузами» — один из ключевых моментов протокола. Идея у Вас оригинальная, но Ваша реализация может дать сбой в самый неподходящий момент.
+2
В общем случае, вероятность этого 1/65536
Но в самый неподходящий момент вероятность стремится к единице :)
+2
«Жаль, что нам так и не удалось послушать начальника транспортного отдела...»

Дискуссия зашла в какую-то жопу, коллеги. Мы увидели (и, как я вижу, даже поняли) очень интересную идею. Брать ее на вооружение я не стану, причины ясны и названы. А о какой-то оптимизации моей программы (по размеру) поговорить не удалось. Я слегка приболел и не могу поработать. Может быть, попозже я все-таки просмотрю свою библиотеку в направлении передачи всей информации через указатель на структуру. Думал, опытные товарищи сразу скажут, стоит ли это делать… Ну ничьо, разберемси :)
0
Да, законы Мерфи никто не отменял :)

И при такой реализации любая ошибка будет фатальной, т. к. в случае ошибки (даже просто искажение данных при передаче) потеряется «синхронизация» между приемником и получателем.
0
приемником и получателем.


Упс, приемником и передатчиком
0
на STM32 организовать отлов тайм-аута несложно — даже таймера не нужно, есть спец счетчик. В прерывании — проверка готовности пакета и старт передачи.
0
по-моему вероятность сильно преувеличили.
реальную опасность представляет только запись регистров, это минимум 11 байт должны совпасть.
0
по-моему вероятность сильно преувеличили.

Почему? В общем случае CRC16 дают 65536 вариантов, которые распределены равновероятно. Соответственно, вероятность совпадения CRC это 1/65536

Если углубится, то первые 2 байта (адрес и код функции) не совсем случайны. Но по, верхней оценке, для того, чтобы распределение CRC16 было равновероятным, достаточно чтобы пакет имел 2 случайных байта с равновероятным распределением. Первые 2 байта этому критерию не соответствуют, но реальный пакет – даст равновероятное распределение CRC.
0
да нет, не то. Проверяется не только кс, а весь пакет (кроме байтов данных, но они тоже косвенно проверяются через кс).
Плюс контролируется не постоянный поток данных, а только начало пакета.
если правильный пакет попадет где-то среди мусора, то не будет принят — будет постоянное переполнение пока не отвалится по таймауту.
0
да нет, не то. Проверяется не только кс, а весь пакет (кроме байтов данных, но они тоже косвенно проверяются через кс). Плюс контролируется не постоянный поток данных, а только начало пакета.

Не совсем Вас понял.

а весь пакет (кроме байтов данных, но они тоже косвенно проверяются через кс).
Хм, а как Вы проверяете пакет (кроме проверки CRC)?

Плюс контролируется не постоянный поток данных, а только начало пакета.


И как контролировать «не постоянный поток данных, а только начало пакета» если не отслуживать тайминги? Кроме «пауз», других признаков начала/конца пакета в MODBUS не существует.
0
Хм, а как Вы проверяете пакет (кроме проверки CRC)?
кроме кс проверяется адрес слейва, команда, длина пакета — это если считать что кс не на 100% надежна, и могут быть совпадения www.hill2dot0.com/wiki/index.php?title=CRC#CRC_Reliability
И как контролировать «не постоянный поток данных, а только начало пакета» если не отслуживать тайминги?
тайминги обслуживает тайм-аут UARTa. Если идет поток шума, входной буфер будет переполнен пока шум не прекратится. Только после тайм-аута прием начнется заново.
Т.е. ловля 3.5 байта никакого преимущества не несет.
0
Ок, коллега. Не вижу смыла дальше спорить.
0
только где гарантия что не будет ложного приема по наступлению тишины?
в современных настольных ОС нет никаких гарантий выдержать интервалы точнее 10мс, тем более если используется USB/Bt/Ethernet/WiFi — задержки/разрывы неизбежны.
остается только тайм-аут 50..100мс.
0
Прерывания в «современных настольных ОС» уже отменили?
0
не помогут, по причине дополнительных прослоек-протоколов
0
Да что ты такое говоришь? Как же тогда SCADA-системы через преобразователи с контроллерами по этому же самому модбасу общаются без интервалов в 100 мс? Чудеса, не иначе.
0
Именно потому, что я работаю в системах, где таки есть траффик, где задержка с ответом на 50 мс — уже не хорошо, я и придерживаюсь стандарта. Но могут же быть и совершенно другие ситуации.
Кстати, все наши рассуждения о таймауте не имеют отношения к идее «проверять конец по появлению правильной контрольной суммы».
0
ну для одного слейва проверка команда+длина+CRC должна работать вполне надежно.
можно добавить проверку диапазона адресов слейва — но это имхо относится к функциям чтения/записи регистра.

в шумной сети с несколькими слейвами можно добавить тайм-аут по таймеру, но и это не панацея от всех проблем.
0
Этот вариант модбаса для последовательных линий и разделение пакетов в нём именно по интервалам тишины. Есть текстовый вариант, который ещё проще в реализации.
0
Modbus-RTU и Modbus-TCP — это несколько разные вещи. У тебя кривой Modbus-RTU.
0
кривой в чем? modpoll работает
0
Для чего стандартный протокол? Чтобы другое устройство с таким протоколом подключить и оно работало.
0
зачем мне другое устройство?
главное — мое работает со стандартной программой с сайта modbus.org
0
цитата по второй ссылке:
By default most ‘off the self’ USB to serial converters have a latency timer of 50ms and a transfer size of 4096 BYTES

The can be a major problem for PC utilities monitoring a serial network. The PC doesn’t get the BYTES fast enough to respond to the serial device before it times out.
0
Тогда нужно использовать другой протокол.

0
0
шедевр-не шедевр, но работает.
попробуйте написать оптимальнее.
0
А я и писал. Хоть говнокод, но не до такой степени.
0
сомневаюсь :)
0
Коллега АТАМАН, я попрошу не бросаться словами. Во-первых, присутствующие могут не знать, какие шедевры ты выдавал. Я вот не знаю, уж извини. Во-вторых, коллега reptile выдал очень интересный вариант, который практически должен работать без проблем. Да, я его не применю, в силу приверженности к выполнению требований стандарта. Но идея великолепна. Или — на твой взгляд, не великолепна. Каждый решает для себя. Но «гавнокод» — это уж слишком.
0
Говнокод и есть и в некоторых случаях будет выдавать трудноотлаживаемую ошибку. Скорость работы вообще замечательной должна быть, контроллер, видимо, только вычислением CRC заниматься должен.
+1
коллега, вы явно недооцениваете скорость контроллера.
STM32F050 обновляет КС одного байта за 6мкс (даже без таблицы).
Если заинлайнить ф-ю вычисления — то в несколько раз быстрее.
по сравнению с периодом приема байта это мизер.
в некоторых случаях будет выдавать трудноотлаживаемую ошибку
поконкретнее можно?
0
никаких 1/65536 там и близко нет — проверяется не кс а весь кадр.
0
Коллега, может Вы выложили не тот код, и мы обсуждаем разную реализацию?
Я у Вас вижу 3 if(). Первый проверяет совпадение адреса и КС. Два другие – проверки кода функции.
Давайте подумаем:
1. Что случится, если при передаче произойдет сбой и контрольная сумма не сойдется (первый if() никогда не выполнится)? Подозреваю, будем крутится в цикле, пока не закончится место в буфере?
2. Что произойдет, ели контрольная сумма совпадёт случайно. Мы раньше времени начнем обрабатывать «огрызок» от пакета. Остаток пакета, в дальнейшем, мы примем за начало следующего пакета…
3. Как после описанных случаев мы будем искать начало нового пакета, если буфер у нас забит мусором, а таймауты мы не ловим?
0
Упс, извиняюсь, не заметил if (bytes == num*2), второй вопрос снимается
0
второй вопрос снимается

хотя нет, bytes вы берете из пакета, игнорируя реальный размер принятых данных (len), главное, чтобы len был больше или равен 11.
0
давайте, для наглядности, возмем такой пакет (в HEX)

01 10 0000 000A 14

01 - адрес
10 - функция записи
0000 - адрес первого регистра
000А - 10 регистров в блоке
14 - 20 байт данных 


пока все ок. Предположим что контрольная сумма блока {01 10 0000 000A 14} равна 0хАААА (число из головы, считать реальное значение лень)

Далее, мы должны получить массив значений регистров. Пусть значение первого регистра АААА (случайно так совпало) имеем
блок принятых данных

01 10 0000 000A 14 AAAA


Этот блок последовательно и успешно проходит проверки
if (len >= 3)
if ((buf[0] == mb[bus].addr)&&(buf[len-2] == (mb[bus].crc & 0xFF))&&(buf[len-1] == (mb[bus].crc >> 8)))
if ((buf[1] == MB_CMD_WRITE_MULTI_REGISTERS)&&(len >= 11))
if (bytes == num*2)


и мы начинаем обработку неполного пакета. Вроде как-то так, или я что-то недосмотрел в коде?
0
вот последний код с дополнительными проверками: gist.github.com/edartuz/7469515
0
О, другое дело, дело. Дополнительная проверка
if ((buf[6] == num<<1)&&(len == (num<<1) + 9))

снимает второй вопрос. А как Вы решили проблему с ошибкой при передаче и последующим восстановлением «синхронизации»?
0
при некорректных данных RX буфер переполнится и перестанет вызываться mb_update.
возобновится после тайм-аута приема (в обработке UART тикает 1ms счетчик для контроля тайм-аута).
0
Можете выложить пример кода? А где гарантия, что после таймаута мы не попадем на середину очередного пакета, проверки не пройдут и ситуация с переполнением буфера не повторится опять (и далее, по кругу)?
0
Коллега Энштейн хочет понять, что по максимуму можно выжать из остроумной идеи. При этом я бы хотел только указать, что результирующий полный код вряд ли будет существенно меньше — но останется идеологически неверным, пока не начнете ловить то, что и является разделителем сообщений. Я так думаю.
Кроме того, здесь как-то смешали в кучу вопрос таймаута как времени ожидания ответа от Слейва и таймаута t3.5. Это ведь очень разные вещи, как вы понимаете. Если t3.5 — требование стандарта, то таймаут на «непонятливого» Слейва — целиком на совести архитектора системы.
0
весь код примерно 600 байт — 100 обработка UART и 500 — modbus, думаю и это еще не предел оптимизации. На счет контроля 3.5 — добавить можно, тем кому нужно это сделают. Но у меня нет таких мастеров которые будут точно отмерять этот интервал. При работе через USB/Bt 2 посылки или сольются в 1 пакет (если поместятся в буфер), или пойдут в разных.
На счет ненадежности контроля — передумал разные варианты, но пока не пойму как можно словить неправильный пакет.
0
на середину пакета никак не попадем — счетчик тайм-аута сбрасывается на каждом принятом байте.
0
Ели не затруднит, выложите код, так будет понятней.

Ели у вас реализован таймаут между соседними байтами в 1 мс, то например, на скорости 115200 пауза в  t3.5 будет незаметна, не вызовет таймаута. Пакеты, для такого приемника будут идти сплошным потоком. Наверное, это сработает, если мастер будет слать пакеты с интервалом более 1 мс. Но, согласно стандарту, минимальный интервал —  t3.5.

В принципе, раз Вы все-же как-то следите за таймингами приема — почему бы не сделать «честную» реализацию MODBUS, в соответствии со спецификацией? Это не намного усложнит реализацию, но это будет «честная» реализация стандарта, которая будет гарантированно работать в любых условиях.
0
на скорости 115200 пауза в  t3.5 будет незаметна
Стандарт предписывает фиксированную паузу t3.5 на скоростях от 38400: 1,75 мс. Аналогчино, пауза t1.5 на скоростях от 38400 равна 0,75 мс.
почему бы не сделать «честную» реализацию MODBUS, в соответствии со спецификацией?
Отож…
0
почитайте буржуйские форумы — с этими паузами больше проблем чем профита — стандарт создавался в 70х когда с портами работали напрямую.
0
Форумы говнокодеров — это, конечно, интересно. Но как быть с тем, что в протоколе пакеты разделяются чётко оговоренными паузами?
0
может Lantronix для вас — говнокодеры, но все же (цитата из их мануала):
«Character Timeout (10-7050 msec)
This sets the timeout between characters received. Official Modbus/RTU defines a 3.5 character time-out, but complex devices have various interrupts that can cause 5 to 10 character “pauses” during transmission. A safe value for general use with Modbus is 50 msec. Note: Setting this value lower than 50 msec will not improve performance and may even make performance worse.»
0
Стандарт предписывает фиксированную паузу t3.5 на скоростях от 38400

Согласен, здесь я погорячился.

А почему 38400? Вроде как фиксированные тайминги при скоростях выше 19200

For baud rates greater than 19200 Bps, fixed values for the 2 timers should be used: it is
recommended to use a value of 750µs for the inter-character time-out (t1.5) and a value of 1.750ms for inter-frame delay (t3.5).
0
А, еще ж 28800. Ну да, ошибся. Я ее не юзаю. Вот и думал, что
«свыше 19200» == «от 38400»
0
так я никому и не навязываю такой подход.
уже писал что тайм-аут 3.5 несложно реализовать через RTOR.
но пока не вижу необходимости из-за непредсказуемых таймингов переходников USB и Bt — не могут они точно отмерить 3.5 символа между пакетами — или сольют 2 посылки в одну, или передадут в разных.
0
Пакеты, для такого приемника будут идти сплошным потоком.
это решается принудительным сбросом буфера приемника после получения правильного пакета
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.