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

А вот здесь — улучшеный вариант библиотеки.
Напомню, реализован 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
Спасибо, интересно.
Один вопрос: почему бы для определения интервала в 1,5 символа не использовать режим сравнения таймера?
Один вопрос: почему бы для определения интервала в 1,5 символа не использовать режим сравнения таймера?
Так а смысл? Делать перерывание? Ставить флаг, который потом анализировать — а именно это надо сделать, когда придет следующий импульс. Иначе событие само по себе ничего не значит. А у меня просто в момент прихода следующего импулься делается чтение состояния счетчика-таймера и сравнивается с t1.5 (если работаем прерыванием по началу байта) или t2.5 (если работаем только по прерыванию в момент прихода всего байта). То есть, мне кажется, что это достаточно просто.
Честно говоря, я вообще все еще не верю, что так просто все получилось. Может дальнейшие испытания выявят какие-то мелочи… Я продолжаю допиливать весь проект, подправлю, есличьо…
Честно говоря, я вообще все еще не верю, что так просто все получилось. Может дальнейшие испытания выявят какие-то мелочи… Я продолжаю допиливать весь проект, подправлю, есличьо…
В код не смотрел ещё, но, по описанию медленновато получается, по моему. Если МК не сильно занят чем либо только..., кроме как принимать запросы и выдавать нулевые ответы.
А нельзя разве принимать решение о дальнейшем приёме байтов по результату сравнения — адреса устройства, на правильность номера функции, ну и по номеру функции можно определять количество принимаемых байт — я имею в виду в посылке присутствует же поле с количеством регистров. Это всё чтобы МК не молотил попусту, и занялся какой нибудь другой работой.
А нельзя разве принимать решение о дальнейшем приёме байтов по результату сравнения — адреса устройства, на правильность номера функции, ну и по номеру функции можно определять количество принимаемых байт — я имею в виду в посылке присутствует же поле с количеством регистров. Это всё чтобы МК не молотил попусту, и занялся какой нибудь другой работой.
Все можно. Протокол простой. Но это все общие рассуждения.
Во-первых, о каких нулевых ответах ты говоришь? Это у меня в тестовом примере регистры не инициализированы и не заполняются чем-то полезным — потому и молоти нулевые ответы.
Во вторых, стандарт все же надо уважать. Описано, что адрес проверяется только после контроля по CRC. Кагбэ намекают, что нефик смотреть содержание сообщения, пока не подтвердилась его целостность. А адрес — уже содержание, он тоже входит в контрольку. Да, мало что может случиться, но я могу придумать ситуацию, когда вот такой «предварительный» приговор вреден. Лучше уж по-закону: сначала примем, проверим целостность фрейма, потом разбираем.
В-третьих, прерываться на каждый входной байт все равно нужно. Иначе пропустим конец сообщения. Понимаешь? А теперь посмотри исходники: затраты в прерывании на именно прием байта ничтожны. Избежать их — ничего не выиграешь.
Так что я принимаю весь фрейм, хотя и знаю, что он уже не мой (или коцнутый, если вдруг 1,5 символа паузы выскочило).
А по номеру функции что ты угадаешь? Во-первых, есть ряд функций, у которых фрейм переменной длины. Во-вторых, аналилизровать сообщение до его полного приема, как я уже сказал, не есть правильно. В-третьих, МК очень мало занят модбасом. Грубо говоря, 1...2..3%. Есть смысл что-то выдумывать?
Во-первых, о каких нулевых ответах ты говоришь? Это у меня в тестовом примере регистры не инициализированы и не заполняются чем-то полезным — потому и молоти нулевые ответы.
Во вторых, стандарт все же надо уважать. Описано, что адрес проверяется только после контроля по CRC. Кагбэ намекают, что нефик смотреть содержание сообщения, пока не подтвердилась его целостность. А адрес — уже содержание, он тоже входит в контрольку. Да, мало что может случиться, но я могу придумать ситуацию, когда вот такой «предварительный» приговор вреден. Лучше уж по-закону: сначала примем, проверим целостность фрейма, потом разбираем.
В-третьих, прерываться на каждый входной байт все равно нужно. Иначе пропустим конец сообщения. Понимаешь? А теперь посмотри исходники: затраты в прерывании на именно прием байта ничтожны. Избежать их — ничего не выиграешь.
Так что я принимаю весь фрейм, хотя и знаю, что он уже не мой (или коцнутый, если вдруг 1,5 символа паузы выскочило).
А по номеру функции что ты угадаешь? Во-первых, есть ряд функций, у которых фрейм переменной длины. Во-вторых, аналилизровать сообщение до его полного приема, как я уже сказал, не есть правильно. В-третьих, МК очень мало занят модбасом. Грубо говоря, 1...2..3%. Есть смысл что-то выдумывать?
А адрес — уже содержание, он тоже входит в контрольку.Какая разница? Проверяем адрес сразу. Если совпадает — принимаем, если это был чужой адрес, но его попортило помехой — фрейм потом отбросится по CRC. Если же адрес не совпадает — это не наш фрейм, либо адрес в нем битый. В любом случае — фтопку.
В-третьих, прерываться на каждый входной байт все равно нужно. Иначе пропустим конец сообщения.А зачем нам конец сообщения, если мы его все равно не принимаем? Достаточно выставить автомат на ожидание SOF'а (т.е. паузы 3.5t).
Проверяем адрес сразуНу, можно, кто ж спорит. Посчитаешь смысл, например, в выигрыше процессорного времени? Потом продолжим.
Достаточно выставить автоматА как автомат будет работать? Назови его правильно: автомат, который будет ставить счетчик на нуль с каждым законченным приемом символа, а когда значение счетчика достигнет кода, соответствующего t3.5, переводящего наш фазовый автомат в состояние IDLE. Как это реализовать без работы УАРТа и таймера? Без участия процессора?
Мы спорим о мелочах. У меня есть программа, если она в чем-то ошибочна или может быть улучшена — во, это важно. Ибо (я надеюсь) ее все же кто-нибудь да позаимсвтует. А выиграть единицы процентов ресурса МК путем сомнительных отступлений от требований стандарта — мне не хочется.
Ловля пауз как маркеров старта — это, как раз, не отступление.
Назови его правильно: автомат, который будет ставить счетчик на ...Нет, это как раз не правильное название. Оно ограничивает варианты твоей конкретной реализацией. Но она не единственно возможная, и ее вполне можно при необходимости переделать.
А выиграть единицы процентов ресурса МКМожет и единицы, зато на самом высоком приоритете. Причем с минимальными изменениями кода и без влияния на надежность, совместимость или что-либо еще. Впрочем, это и не обязательно.
В данном. Но библиотека позиционируется как «берите и пользуйтесь». Хотя да, разницы между игнорированием байта (принимать их все равно придется, чтобы ловить SOF, да) и складыванием его в буфер практически ноль.
Вот если проверять адрес только после контроля фрейма по CRC — расход ресурсов заметно вырастет.
Вот если проверять адрес только после контроля фрейма по CRC — расход ресурсов заметно вырастет.
расход ресурсов заметно вырастетНу… не «заметно». Я, когда оценивал потери в единицы процентов, то имел в виду потери на прерывания — они идут достаточно часто (при 38400 бод — каждые 286 мкс). Но по ним, как мы уже все согласны, разницы особой нет, пихаем ли мы байт в буфер или только передергиваем таймер. А вот расчет CRC — один раз на весь принятый фрейм, он уж делится на приличное время (несколько миллисекунд).
В общем, ты меня не переубедил. А желающие могут сделать — в прорамме хорошо видно, где можно анализировать адрес в момент его приема.
Коллега, спасибо за статью.
Позволю себе немного критики. Вы, ИМХО, неудачно декомпозировали (разбили по файлам) реализацию. У Вас много файлов, и все они и имеют большую связанность с остальными (обмен через глобальные переменные и т. д.) А вот четко выраженного интереса у вас нет (вернее но есть, но он разбросан по файлам, и сходу непонятно как ним пользоваться). Я ожидал, что открыв MB_header.h я увижу интерес — а его там нет.
Я бы Вам советовал:
1. выносить интерфейсы в отдельный *.h фал и избегать предеклараций внутри «сторонних» *.с файлов. Проще говоря, для каждого *.с делать *.h с предекларацией экспортируемых функций. В саму предекларацию функций включать названия параметров. Согласитесь что описание
2. в *.h всегда включать «защиту» от повторного включения
ну, или #pragma once
3. избегать связанности кода, экспорта глобальных переменных и глобальных переменных в принципе.
Еще раз извиняюсь за критику.
Позволю себе немного критики. Вы, ИМХО, неудачно декомпозировали (разбили по файлам) реализацию. У Вас много файлов, и все они и имеют большую связанность с остальными (обмен через глобальные переменные и т. д.) А вот четко выраженного интереса у вас нет (вернее но есть, но он разбросан по файлам, и сходу непонятно как ним пользоваться). Я ожидал, что открыв 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-2 хедера, хотя можно и больше. Но увеличивать их число я хотел бы не по количеству файлов, а по смыловой нагрузке — что какой хедер определяет.
Например, общие инклуды, версии и варианты компиляции («главный» хедер, включаемый во все файлы).
Затем можно организовать отдельный хедер с typedef-ами (или слить его в «главный», это близкие хедеры — в том смысле, что включаются во все файлы).
Близкими к перечисленным являются дефайны всяких аппаратных сущностей — пины, частоты и прочее.
И, наконец, есть еще вот та самая (пре)декларация функций. Мне кажется, ее можно разносить по тем файлам, где декларируемые символы используются. Ну, в вобщий хедер включить разве что те глобальные имена, которые нужны нескольким файлам.
Почему я так себе вижу (и кое-что реализовал в критикуемой либе): потому что нет привычки работать в Эклипсе или подобной «услужливой» среде. Это здесь — навел мышу на имя, а тебе уже и макрорасширение, и можно перейти на определение имени… А в обычных редакторах все скучно: ищи-свищи, где там и как я дефинировал эту АБВГДЕ. Ладно еще, «я», а если «он», если люди роются в чужой проге?
Вот я и старался сделать как-то так, чтобы легко было найти все определения. Как получилось… Ну, может и неудачно. Вообще, был вариант слить весь код в один файл (и он был бы совсем небольшой). Может так и сделать?
Вообще-то, в проектах у меня 1-2 хедера, хотя можно и больше. Но увеличивать их число я хотел бы не по количеству файлов, а по смыловой нагрузке — что какой хедер определяет.
Например, общие инклуды, версии и варианты компиляции («главный» хедер, включаемый во все файлы).
Затем можно организовать отдельный хедер с typedef-ами (или слить его в «главный», это близкие хедеры — в том смысле, что включаются во все файлы).
Близкими к перечисленным являются дефайны всяких аппаратных сущностей — пины, частоты и прочее.
И, наконец, есть еще вот та самая (пре)декларация функций. Мне кажется, ее можно разносить по тем файлам, где декларируемые символы используются. Ну, в вобщий хедер включить разве что те глобальные имена, которые нужны нескольким файлам.
Почему я так себе вижу (и кое-что реализовал в критикуемой либе): потому что нет привычки работать в Эклипсе или подобной «услужливой» среде. Это здесь — навел мышу на имя, а тебе уже и макрорасширение, и можно перейти на определение имени… А в обычных редакторах все скучно: ищи-свищи, где там и как я дефинировал эту АБВГДЕ. Ладно еще, «я», а если «он», если люди роются в чужой проге?
Вот я и старался сделать как-то так, чтобы легко было найти все определения. Как получилось… Ну, может и неудачно. Вообще, был вариант слить весь код в один файл (и он был бы совсем небольшой). Может так и сделать?
Мне кажется, ее можно разносить по тем файлам, где декларируемые символы используются.
Предположим, у вас есть *.с файл, который реализует некую void foo(int i);
И эта функция вызывается в сотне (условно) файлов в вашем проекте, и в каждом таком файле, вы явно вставляете предекларацию
void foo(int i);
Теперь, вы решили изменить архитектуру и поменять сигнатуру на void foo(char * c); Поменяли в файле реализации. Теперь вам нужно пройтись по всем остальным файлам, и поменять предекларацию и вызов там.
Основная проблема — вы где-то могли забыть поменять предекларацию (и соответственно, поправить вызов). И компилятор будет молчать. И линкер даже не ругнется. А в результате — крах программы.
А если предекларация существует только в одном месте, и вы ее поменяли — то компилятор не скомпилирует код, пока вы не поправите все вызовы foo().
В больших проектах есть правила, понятно. Но я (в данной теме) не хотел бы расширять фокус на всю идеологию создания С-проектов. В конкретной обсуждаемой библиотеке важна _простая_ реализация Модбаса. И я сейчас хотел бы услышать ваше мнение, коллеги, по простому вопросу: выиграет ли читабельность всей либы от того, что я солью С-файлы в один, а остаточные декларации (их число уменьшится) — в единый хедер?
Переделывать либу под «большие» проекты я просто не хочу. Смысла нет. Она маленькая и хороша именно этим.
Переделывать либу под «большие» проекты я просто не хочу. Смысла нет. Она маленькая и хороша именно этим.
Да и в Notepad++ такое есть… Не в том дело. Я выкладываю проект и озабочен тем, чтобы его 1) максимально просто было понять и 2) приколхозить под разные МК. Первое требование выполняется наилучшим образом при сливе всего в один файл, второе — при разбивке на «железозависимые» и «независимые» части. Вы сейчас видите победу второго подхода — я разбил и прокомментировал как раз, чтобы атмеговцы или пиковцы видели, что тут чисто мсп-шное. Так пока и останется. Пусть коллктивный опыт «внедрения» подскажет. А то мы тут будем долго спорить об общих принципах. Это мне напомнило субдискуссию с уважаемым коллегой Vga по поводу быстродействия…
Вот пусть ребята запустят и пожалуются, что им не хватает того или сего — тогда я готов перелопатить либу.
Вот пусть ребята запустят и пожалуются, что им не хватает того или сего — тогда я готов перелопатить либу.
Коллега, я не хочу настаивать на своем мнении.
Здесь дело не в размере кода/библиотеки (дополнительный *.h файл на размер не повлияет). Просто, в программировании, стараются избавится от «копипаста» от применения «ctrl-c, ctrl-v». Структурное программирование нас подталкивает к вынесению одинакового кода в функции. Аналогично, препроцессор С позволяет нам выносить одинаковые синтаксические структуры в отдельный файл, и избежать «копипаста». Позволяет, но не требует. Предекларация нужна программисту а не компилятору, многие компиляторы (зависит от настроек) реагируют на отсутствие предекларации как warning, а не как error.
Вопрос декомпозиции — это вопрос стиля (читаемости), а не работоспособности конечной программы. Есть общие рекомендации (например, давать переменным осмысленные названия). Но компилятору все равно, как называются переменные. Никто не заставляет Вас придерживаться определенных правил, но если Вы их придерживаетесь — это плюс для восприятия Вашего кода.
Здесь дело не в размере кода/библиотеки (дополнительный *.h файл на размер не повлияет). Просто, в программировании, стараются избавится от «копипаста» от применения «ctrl-c, ctrl-v». Структурное программирование нас подталкивает к вынесению одинакового кода в функции. Аналогично, препроцессор С позволяет нам выносить одинаковые синтаксические структуры в отдельный файл, и избежать «копипаста». Позволяет, но не требует. Предекларация нужна программисту а не компилятору, многие компиляторы (зависит от настроек) реагируют на отсутствие предекларации как warning, а не как error.
Вопрос декомпозиции — это вопрос стиля (читаемости), а не работоспособности конечной программы. Есть общие рекомендации (например, давать переменным осмысленные названия). Но компилятору все равно, как называются переменные. Никто не заставляет Вас придерживаться определенных правил, но если Вы их придерживаетесь — это плюс для восприятия Вашего кода.
по простому вопросу: выиграет ли читабельность всей либы от того, что я солью С-файлы в один, а остаточные декларации (их число уменьшится) — в единый хедер?В один файл лучше не сливать. Лучше сделать два уровня API — физический (платформозависимая работа с USART), и логический — разбор и обработка протокола.
Самое главное, какие качественно-количественные показатели получились.
Удалось «впихнуть в 1 килобайт»?
Удалось «впихнуть в 1 килобайт»?
Плюясь и чертыхаясь, полазил по листингам и ручками посчитал: всего все Модбасное заняло 1696 байт, из которых 552 — вычислитель CRC с его 512 байтами таблиц. Так что следует признать, что в 1К не входим, даже если бы модуль CRC взять нетабличный или малотабличный. ТОгда что-то на уровне 1,2К я бы считал минимальной оценкой.
Но мне проще говорить о том, что в 1,7К входит нормально-функциональный Модбас с реализованныи функциями 03 и 16 (и добавление 1 функции обойдется в 40 байт)
Хотя я специально не минимизировал, конечно. Считаю, что получилось и так очень немного кода. И мне он понятем :)
А весь проект сейчас уверенно меньше 10К. Как я и предполагал, с 15К уверенно вошел в 10К, а уже все работает. Есть мелочи (типа странная работа вотчдога), но они уже не породят нового кода. Так что МСП340 продолжает радовать. Скоро полевые испытания, вот интересно будет!
Но мне проще говорить о том, что в 1,7К входит нормально-функциональный Модбас с реализованныи функциями 03 и 16 (и добавление 1 функции обойдется в 40 байт)
Хотя я специально не минимизировал, конечно. Считаю, что получилось и так очень немного кода. И мне он понятем :)
А весь проект сейчас уверенно меньше 10К. Как я и предполагал, с 15К уверенно вошел в 10К, а уже все работает. Есть мелочи (типа странная работа вотчдога), но они уже не породят нового кода. Так что МСП340 продолжает радовать. Скоро полевые испытания, вот интересно будет!
О, теперь начнем меряться пиписьками :) Выкладывай!
А если серьезно, то даже при всем уважении — я удивлен. Ладно, что я не гнался за минимизацией, но и не вижу настолько раздутого кода, чтобы его споловинить. Вот, смотрю: прерывания — 262 байта, парсинг с подготовкой респонса — 610, инициализация 80. Уже под кило. И из этого нечего выбрасывать, если не идти на отступление от стандарта. Странно… Неужто так играет роль использование структуры? Возможно, парсинг оказался бы чуть короче, но не в 2 раза.
Или ты не все посчитал — прерывания, инициализацию, CRC? Команды 03 и 16 или 03 и 06?
Не томи, колись :)
Кстати, если найдется рациональное объяснение возможностей сокращения кода — я готов переделать либу. Само ее появление здесь предполагало наличие предложений по улучшению — и отработку их. Выше были идеи по созданию более кашерной структуры кода, с физическим и логическим уровнями — они меня не вздрочили, ибо при такой простой библиотеке кашерность не критически важна, а рост кода будет. А вот если я пойму, где у меня набегает удвоение размера программы — о! придется призадуматься.
А если серьезно, то даже при всем уважении — я удивлен. Ладно, что я не гнался за минимизацией, но и не вижу настолько раздутого кода, чтобы его споловинить. Вот, смотрю: прерывания — 262 байта, парсинг с подготовкой респонса — 610, инициализация 80. Уже под кило. И из этого нечего выбрасывать, если не идти на отступление от стандарта. Странно… Неужто так играет роль использование структуры? Возможно, парсинг оказался бы чуть короче, но не в 2 раза.
Или ты не все посчитал — прерывания, инициализацию, CRC? Команды 03 и 16 или 03 и 06?
Не томи, колись :)
Кстати, если найдется рациональное объяснение возможностей сокращения кода — я готов переделать либу. Само ее появление здесь предполагало наличие предложений по улучшению — и отработку их. Выше были идеи по созданию более кашерной структуры кода, с физическим и логическим уровнями — они меня не вздрочили, ибо при такой простой библиотеке кашерность не критически важна, а рост кода будет. А вот если я пойму, где у меня набегает удвоение размера программы — о! придется призадуматься.
У кортексов Thumb-2 код ещё компактнее, чем у мсп (если я опять ничего не путаю)
С другой стороны, смысла жаться нет.
Написал float == прощайте ~4кБ
printf == ещё почти столько же
С другой стороны, смысла жаться нет.
Написал float == прощайте ~4кБ
printf == ещё почти столько же
смысла жаться нетНе, ну интересно же! Жаться то нет, я согласен. То есть, я не стану изучать асм, я не стану жертвовать требованиями станарта на Модбас (скажем, в области таймаутов, отработки исключений, реагирования на широковещательные запросы и т.п.), не солью все в один файл, не заведу кучу глобальных переменных, через которые получу всю инфо для настройки оборудования… Все перечисленное могло бы сократить код. Но я лучше буду пользоваться описанной библиотекой, ибо (сейчас) вижу в ней баланс, как раз милый моему сердцу.
Однако, я немножко же и живой человек. И если есть способы оптимизации размера, не заставляющие «поступаться принципами», а даже наоборот, расширяющие мои горизонты — дык! Займусь с охотой.
Скажем, меня «муляет» то, что я не захотел создавать структуру, содержащую все данные, которыи обменивается главная программа с либой. Возможно, раскрутка длинного списка параметров функции — затратная вешь. Но насколько это даст выигрыш? Несколько байт? Вот бы и послушать комментарии от уважаемого коллеги reptile.
Мы все здесь чуточку заведены на перфектность. Вспоминаю недавнюю дискуссию (на форуме?) о сокращении кода в каком-то микроскопическом цикле, ого! Там копии ломали долго, я перестал даже следить :)
команды 03 и 16, остальные ненужны — зачем плодить сущности?
код примитивный — принимаем байты, складываем в линейный буфер.
по приему очередного — вызываем ф-ю парсинга.
если команда принята — пишем ответ в тот же буфер и возвращаем длину ответа.
если результат >0, остается передать его содержимое.
CRC — не табличный, да и нет смысла — в проце дури много, успевает даже на 115200 принимать без прерываний.
пользовательский код должен определить ф-и чтения/записи регистров mb_reg_get(a), mb_reg_set(a,v).
обработка:
CRC:
код примитивный — принимаем байты, складываем в линейный буфер.
по приему очередного — вызываем ф-ю парсинга.
если команда принята — пишем ответ в тот же буфер и возвращаем длину ответа.
если результат >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);
}
Вау! Вот это да… Остроумно!
То есть, временные паузы похер? На каждый принятый байт, начиная с 3-х, пробуем все накопленное прокрутить на CRC. Если внезапно совпадает — считаем, что это и есть все сообщение. Верно? То есть, не переживаем о том, что кусок сообщения теоретически может тоже дать «правильную» СRC. А ведь может… Тогда начнем толкать ответ «поперед батька»?
То есть, временные паузы похер? На каждый принятый байт, начиная с 3-х, пробуем все накопленное прокрутить на CRC. Если внезапно совпадает — считаем, что это и есть все сообщение. Верно? То есть, не переживаем о том, что кусок сообщения теоретически может тоже дать «правильную» СRC. А ведь может… Тогда начнем толкать ответ «поперед батька»?
временные тайм-ауты обрабатывает код UART.
CRC не прокручивается, обновляется только для одного байта.
я в пром.сетях не работаю, особых потоков в сети не планируется.
вполне достаточно совпадения адреса и CRC.
CRC не прокручивается, обновляется только для одного байта.
я в пром.сетях не работаю, особых потоков в сети не планируется.
вполне достаточно совпадения адреса и CRC.
Но ведь с каждым принятым байтом проверяется условие: адрес наш и последние 2 байта, трактуемуе как CRC, дают совпадение контрольки. Так? Значит таки да, что на произвольном байте принимаемого сообщения мы можем решить, что уже аллес гут, приготовить и набухать ответ. В тот же буфер…
Или я неверно понял фразу
Или я неверно понял фразу
по приему очередного — вызываем ф-ю парсингаи реально парсинг вызывается все же по обнаружению тишины?
«Жаль, что нам так и не удалось послушать начальника транспортного отдела...»
Дискуссия зашла в какую-то жопу, коллеги. Мы увидели (и, как я вижу, даже поняли) очень интересную идею. Брать ее на вооружение я не стану, причины ясны и названы. А о какой-то оптимизации моей программы (по размеру) поговорить не удалось. Я слегка приболел и не могу поработать. Может быть, попозже я все-таки просмотрю свою библиотеку в направлении передачи всей информации через указатель на структуру. Думал, опытные товарищи сразу скажут, стоит ли это делать… Ну ничьо, разберемси :)
Дискуссия зашла в какую-то жопу, коллеги. Мы увидели (и, как я вижу, даже поняли) очень интересную идею. Брать ее на вооружение я не стану, причины ясны и названы. А о какой-то оптимизации моей программы (по размеру) поговорить не удалось. Я слегка приболел и не могу поработать. Может быть, попозже я все-таки просмотрю свою библиотеку в направлении передачи всей информации через указатель на структуру. Думал, опытные товарищи сразу скажут, стоит ли это делать… Ну ничьо, разберемси :)
по-моему вероятность сильно преувеличили.
Почему? В общем случае CRC16 дают 65536 вариантов, которые распределены равновероятно. Соответственно, вероятность совпадения CRC это 1/65536
Если углубится, то первые 2 байта (адрес и код функции) не совсем случайны. Но по, верхней оценке, для того, чтобы распределение CRC16 было равновероятным, достаточно чтобы пакет имел 2 случайных байта с равновероятным распределением. Первые 2 байта этому критерию не соответствуют, но реальный пакет – даст равновероятное распределение CRC.
да нет, не то. Проверяется не только кс, а весь пакет (кроме байтов данных, но они тоже косвенно проверяются через кс).
Плюс контролируется не постоянный поток данных, а только начало пакета.
если правильный пакет попадет где-то среди мусора, то не будет принят — будет постоянное переполнение пока не отвалится по таймауту.
Плюс контролируется не постоянный поток данных, а только начало пакета.
если правильный пакет попадет где-то среди мусора, то не будет принят — будет постоянное переполнение пока не отвалится по таймауту.
да нет, не то. Проверяется не только кс, а весь пакет (кроме байтов данных, но они тоже косвенно проверяются через кс). Плюс контролируется не постоянный поток данных, а только начало пакета.
Не совсем Вас понял.
а весь пакет (кроме байтов данных, но они тоже косвенно проверяются через кс).Хм, а как Вы проверяете пакет (кроме проверки CRC)?
Плюс контролируется не постоянный поток данных, а только начало пакета.
И как контролировать «не постоянный поток данных, а только начало пакета» если не отслуживать тайминги? Кроме «пауз», других признаков начала/конца пакета в MODBUS не существует.
Хм, а как Вы проверяете пакет (кроме проверки CRC)?кроме кс проверяется адрес слейва, команда, длина пакета — это если считать что кс не на 100% надежна, и могут быть совпадения www.hill2dot0.com/wiki/index.php?title=CRC#CRC_Reliability
И как контролировать «не постоянный поток данных, а только начало пакета» если не отслуживать тайминги?тайминги обслуживает тайм-аут UARTa. Если идет поток шума, входной буфер будет переполнен пока шум не прекратится. Только после тайм-аута прием начнется заново.
Т.е. ловля 3.5 байта никакого преимущества не несет.
Именно потому, что я работаю в системах, где таки есть траффик, где задержка с ответом на 50 мс — уже не хорошо, я и придерживаюсь стандарта. Но могут же быть и совершенно другие ситуации.
Кстати, все наши рассуждения о таймауте не имеют отношения к идее «проверять конец по появлению правильной контрольной суммы».
Кстати, все наши рассуждения о таймауте не имеют отношения к идее «проверять конец по появлению правильной контрольной суммы».
цитата по второй ссылке:
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.
Коллега АТАМАН, я попрошу не бросаться словами. Во-первых, присутствующие могут не знать, какие шедевры ты выдавал. Я вот не знаю, уж извини. Во-вторых, коллега reptile выдал очень интересный вариант, который практически должен работать без проблем. Да, я его не применю, в силу приверженности к выполнению требований стандарта. Но идея великолепна. Или — на твой взгляд, не великолепна. Каждый решает для себя. Но «гавнокод» — это уж слишком.
коллега, вы явно недооцениваете скорость контроллера.
STM32F050 обновляет КС одного байта за 6мкс (даже без таблицы).
Если заинлайнить ф-ю вычисления — то в несколько раз быстрее.
по сравнению с периодом приема байта это мизер.
STM32F050 обновляет КС одного байта за 6мкс (даже без таблицы).
Если заинлайнить ф-ю вычисления — то в несколько раз быстрее.
по сравнению с периодом приема байта это мизер.
в некоторых случаях будет выдавать трудноотлаживаемую ошибкупоконкретнее можно?
Коллега, может Вы выложили не тот код, и мы обсуждаем разную реализацию?
Я у Вас вижу 3 if(). Первый проверяет совпадение адреса и КС. Два другие – проверки кода функции.
Давайте подумаем:
1. Что случится, если при передаче произойдет сбой и контрольная сумма не сойдется (первый if() никогда не выполнится)? Подозреваю, будем крутится в цикле, пока не закончится место в буфере?
2. Что произойдет, ели контрольная сумма совпадёт случайно. Мы раньше времени начнем обрабатывать «огрызок» от пакета. Остаток пакета, в дальнейшем, мы примем за начало следующего пакета…
3. Как после описанных случаев мы будем искать начало нового пакета, если буфер у нас забит мусором, а таймауты мы не ловим?
Я у Вас вижу 3 if(). Первый проверяет совпадение адреса и КС. Два другие – проверки кода функции.
Давайте подумаем:
1. Что случится, если при передаче произойдет сбой и контрольная сумма не сойдется (первый if() никогда не выполнится)? Подозреваю, будем крутится в цикле, пока не закончится место в буфере?
2. Что произойдет, ели контрольная сумма совпадёт случайно. Мы раньше времени начнем обрабатывать «огрызок» от пакета. Остаток пакета, в дальнейшем, мы примем за начало следующего пакета…
3. Как после описанных случаев мы будем искать начало нового пакета, если буфер у нас забит мусором, а таймауты мы не ловим?
давайте, для наглядности, возмем такой пакет (в HEX)
пока все ок. Предположим что контрольная сумма блока {01 10 0000 000A 14} равна 0хАААА (число из головы, считать реальное значение лень)
Далее, мы должны получить массив значений регистров. Пусть значение первого регистра АААА (случайно так совпало) имеем
блок принятых данных
Этот блок последовательно и успешно проходит проверки
и мы начинаем обработку неполного пакета. Вроде как-то так, или я что-то недосмотрел в коде?
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)
и мы начинаем обработку неполного пакета. Вроде как-то так, или я что-то недосмотрел в коде?
вот последний код с дополнительными проверками: gist.github.com/edartuz/7469515
Коллега Энштейн хочет понять, что по максимуму можно выжать из остроумной идеи. При этом я бы хотел только указать, что результирующий полный код вряд ли будет существенно меньше — но останется идеологически неверным, пока не начнете ловить то, что и является разделителем сообщений. Я так думаю.
Кроме того, здесь как-то смешали в кучу вопрос таймаута как времени ожидания ответа от Слейва и таймаута t3.5. Это ведь очень разные вещи, как вы понимаете. Если t3.5 — требование стандарта, то таймаут на «непонятливого» Слейва — целиком на совести архитектора системы.
Кроме того, здесь как-то смешали в кучу вопрос таймаута как времени ожидания ответа от Слейва и таймаута t3.5. Это ведь очень разные вещи, как вы понимаете. Если t3.5 — требование стандарта, то таймаут на «непонятливого» Слейва — целиком на совести архитектора системы.
весь код примерно 600 байт — 100 обработка UART и 500 — modbus, думаю и это еще не предел оптимизации. На счет контроля 3.5 — добавить можно, тем кому нужно это сделают. Но у меня нет таких мастеров которые будут точно отмерять этот интервал. При работе через USB/Bt 2 посылки или сольются в 1 пакет (если поместятся в буфер), или пойдут в разных.
На счет ненадежности контроля — передумал разные варианты, но пока не пойму как можно словить неправильный пакет.
На счет ненадежности контроля — передумал разные варианты, но пока не пойму как можно словить неправильный пакет.
Ели не затруднит, выложите код, так будет понятней.
Ели у вас реализован таймаут между соседними байтами в 1 мс, то например, на скорости 115200 пауза в t3.5 будет незаметна, не вызовет таймаута. Пакеты, для такого приемника будут идти сплошным потоком. Наверное, это сработает, если мастер будет слать пакеты с интервалом более 1 мс. Но, согласно стандарту, минимальный интервал — t3.5.
В принципе, раз Вы все-же как-то следите за таймингами приема — почему бы не сделать «честную» реализацию MODBUS, в соответствии со спецификацией? Это не намного усложнит реализацию, но это будет «честная» реализация стандарта, которая будет гарантированно работать в любых условиях.
Ели у вас реализован таймаут между соседними байтами в 1 мс, то например, на скорости 115200 пауза в t3.5 будет незаметна, не вызовет таймаута. Пакеты, для такого приемника будут идти сплошным потоком. Наверное, это сработает, если мастер будет слать пакеты с интервалом более 1 мс. Но, согласно стандарту, минимальный интервал — t3.5.
В принципе, раз Вы все-же как-то следите за таймингами приема — почему бы не сделать «честную» реализацию MODBUS, в соответствии со спецификацией? Это не намного усложнит реализацию, но это будет «честная» реализация стандарта, которая будет гарантированно работать в любых условиях.
может 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.»
Стандарт предписывает фиксированную паузу 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).
Комментарии (85)
RSS свернуть / развернуть