STM32 и порт FreeModbus (RTU Slave + Master) на HAL

Поделюсь своим простым портом мастера и слейва FreeModbus на STM32 с использованием HAL без костылей. Вроде бы и простая тема, но какие гайды бы я ни видел, они или устаревшие уже, или с какими-либо костылями (хотя заявляется, что, мол, всё просто и на HAL), более того, гайдов с мастером я не видел вообще.

Сразу признаюсь, я воспользовался открытой реализацией мастера на FreeModbus от китайского программиста Armink, написан мастер был для китайской RTOS RT-Thread, но возможна работа и без RTOS, я просто подшаманил файлы мастера, а также немного подшаманил оригинальную реализацию FreeModus.

B93-0

Разбор порта, примеры работы RTU версии и ссылки на исходники как моего порта, так и оригинальные источники, всё под катом:

Различные гайды

Недавно, решая поставленную задачу, задался вопросом: а есть ли уже готовые решения (порты) для STM32, чтобы не делать одну и ту же работу по портированию (как раз вышла новая версия FreeModbus, а также это хороший вариант обновить пример старого проекта с использованием Modbus, который я набыдлокодил для Миландровского МК)? Я нашел только пару гайдов (не очень новых) на эту тему:

  • Портирование FreeModbus 1.5 под STM32 HAL rs485 без RTOS — старый гайд на хабре с прошлой версией библиотеки, да приправлено всё костылями при использовании HAL, плохой пример;
  • Портирование FreeModbus под STM32. Версия от Динара — улучшенный вариант, решены проблемы с __critical_enter \ __critical_exit (я использовал аналогичную реализацию), вроде бы вот оно, хороший пример, но как только я дохожу до портянки кода в portserial.c, я понимаю, что и этот пример мне не нравится.

Собственно, и всё, больше более-менее толковых гайдов на русском я не видел (а гуглить на ангельском стало уже лень), поэтому и было решено написать свой! С блек… с HAL-ом и на блюпилах (как самое народное применение)!

Правки порта

Итак, обозначу сразу небольшие требования, от которых я отталкивался при создании своего порта и внесении правок в саму библиотеку FreeModbus:

  • Организовать возможность использования HAL с минимальными изменениями в файлах portserial и porttimer при переносе проекта на другой камень (т. е. максимально отвязаться от конкретного камня в примере), если это возможно.
  • Убрать инициализацию периферии из файлов portserial и porttimer, т. к. при использовании HAL она уже происходит до вызова инициализации FreeModbus, тем самым мы просто добавляем себе проблем, пытаясь её организовать в этих файлах.
  • Добавить отправку пакета в portserial целиком, а не пулингом по одному байту.
  • Добавить приём байт по DMA или прерыванию, а не пулингом по одному (но тут большой вопрос, как это сделать, т. к. мы не знаем заранее длину принимаемого пакета, и она может меняться, поэтому этот пункт открытый и НЕ решён в моей реализации.

Я взял последний порт с реализацией мастера от Armink как стартовую точку, этот порт я и начну модифицировать.

Для начала поправим места в оригинальной библиотеке, которые мне не нравятся, а именно:

— Файл mb.c\mb.h и функция инициализации:

eMBErrorCode eMBInit( eMBMode eMode, UCHAR ucSlaveAddress, void *dHUART, ULONG ulBaudRate, void *dHTIM );

В оригинале это выглядит так:

eMBErrorCode eMBInit ( eMBMode eMode, UCHAR ucSlaveAddress, UCHAR ucPort, ULONG ulBaudRate, eMBParity eParity);

Т. е. я убрал номер порта, а также бит чётности, а вместо них теперь передаются не привязанные к STM32 ссылки на экземпляр UART и таймера, которые можно будет использовать в файлах порта без всяких extern и прочих костылей. Соответствующие правки по коду и в вызовах функций порта eMBRTUInit и eMBASCIIInit, в которые теперь передаются эти ссылки. Файлы mbrtu.c подправлены аналогично, и из него теперь вызываются xMBPortSerialInit и xMBPortTimersInit с указанными ссылками. Расписывать подробно нет смысла, главное, рассказал суть.

Мы просто передаём ссылки на конкретный UART и таймер, не запариваясь с файлами порта.

Примечание: Скорость UART тут таки приходится передавать, т. к. на основе неё рассчитывается таймаут по приёму символа.

— Файл mbport.h, появилось новое объявление:

#define SEND_ALL_BYTES_IN_ONE_CALL (1)

Которое используется в mbrtu.c на подправленном участке кода функции xMBRTUTransmitFSM.

Как можно увидеть, от SEND_ALL_BYTES_IN_ONE_CALL зависит, вызывается ли xMBPortSerialPutBytes с передачей всего буфера на отправку или вызывается  xMBPortSerialPutByte как в оригинале, отправляя по одному байту пулингом. Новая функция отправки появилась в portserial файлах.

— Файл порта porttimer.c, теперь появилась возможность полностью абстрагироваться от конкретного таймера, и реализация выглядит максимально просто.

Примечание: единственный минус, который я увидел в такой реализации, это то, что теперь этот callback определен тут, и его нельзя использовать в проекте где-либо ещё, и если вам, к примеру, захочется использовать callback для другого таймера, придется убрать его отсюда в тот же main.c и оттуда уже вызывать pxMBPortCBTimerExpired().

— Файл порта portserial.c, уже несколько сложнее. Во-первых, тут добавились макросы для RTS линии (правда, я не придумал, как лаконично передавать пин\порт для RTS). Во-вторых, функции инициализации, переключения потока, приёма и отправки (а также добавилась новая — xMBPortSerialPutBytes) тоже упростились. И в-третьих, используются специальные callback-и UART по отправке и приёму. В итоге никаких замудрёных проверок битов и прочего по второму кругу (это и так есть в HAL), как это было в гайдах выше, и никакой привязки к конкретному экземпляру UART.

Примечание: аналогичный минус, как и с таймером, теперь эти callback-и определены тут, и их нельзя использовать в проекте где-либо ещё, и если вам, к примеру, захочется реализовать и мастера, и слейва на одном МК, то придётся вынести определение callback-ов в тот же main.c и оттуда уже определить вызовы pxMBFrameCBByteReceived и pxMBFrameCBTransmitterEmpty для соответствующего UART.

— Файл portevent.c особых изменений не претерпел, зато portevent_m.c пришлось адаптировать для работы без RTOS, и в полностью правильной реализации я не уверен.

— Файлы user_mb_app.c\user_mb_app.h содержат callback-и для всех типов ячеек (это я условно назову Input Reg, Holding Reg, Coil, Discret Input, назвать их регистрами тоже нельзя, а общее слово по вики — просто «типы данных», что звучит «не очень»). Изменения тут в основном были в плане добавления директив для лучшего контроля используемых ячеек (к примеру, если вы не используете Holding, то реализация callback-а упрощается, и не объявляются все переменные для обработки Holding). Также я перенёс их в папку «app», что мне кажется более логичным, т. к. это уже не файлы порта.

В целом, вроде бы и всё.

Тестовый проект и железо для слейва

Всё довольно просто, для слейва я возьму STM32F103C8T6 на дешевой отладке (да, НЕ ПОКУПАЙТЕ такую, фигово спроектирована, не выведено питание +5 В, а также выведен только один пин +3,3 В, два для общего, лучше уж блупил, я как-то по глупости купил ради интереса, появилась как новинка — альтернатива блупилам, вот и повёлся), к нему добавлю простой джойстик (тоже хрень полная, тугой и очень резкое изменение «значения положения», рабочий ход раза в 2 больше, чем измеряемый переменным резистором на каждой оси) и UART<->RS-485 на MAX485ESA (да, она 5 В, но на вход уровней 3,3 В ей хватает, а выход подключен к МК через резисторый делитель 2 кОм \ 1 кОм). Выглядит это дело так:

B93-1

Теперь открываем STM32CubeMX и включаем, настраиваем необходимую периферию.

Начнём с таймеров, их будет 2. TIM4 настраиваем так:

B93-2

Просто включаем таймер, никаких каналов не задействуем, выключаем автозагрузку нового значения периода после окончания счёта, значения Prescaler и Period высчитываем для периода = 50 мкс.

Примечание: Тактовая частота МК = 72 МГц. При prescaler = 71, тик таймера будет 72,000,000 Гц / 71 + 1 = 1,000,000 Гц. При Period = 49, частота его прерываний = 1,000,000 / 49 + 1 = 20,000 Гц или 1 сек / 20,000 Гц = 0,00005 сек = 50 мкс (то самое значение, что требуется для отсчета таймаута по приёму символа в файле porttimer.c)

Да, не забываем включить прерывание (из него и вызывается callback):

B93-3

И TIM3 настраиваем так:

B93-4

Также включаем таймер без выходов, настраиваем Prescaler, Period, включаем автозагрузку нового значения периода (таймер будет молотить постоянно) и, главное, выставляем триггер как Update Event, чтобы таймер мог управлять АЦП.

Примечание: тут уже не столь важен получившийся период, этот таймер будет использоваться для АЦП, и главное, чтобы период у него был больше, чем занимает оцифровка 1 канала АЦП. 1…100 мс для теста будет более чем достаточно. В данном случае это 1 мс.

Настраиваем АЦП:

B93-5

Особо ничего примечательного, выбираем 2 канала, режим сканирования, отключаем постоянную оцифровку (для того чтобы по таймеру делать оцифровку с заданным периодом, хотя для теста это оверкилл, это больше нужно для звука или DSP), выставляем 2 преобразования и настраиваем их на 2 выбранных канала и, самое главное, выставляем в качестве триггера событие от TIM3, чтобы он управлял АЦП.

Примечание: у каждого АЦП обычно Trigger Out Event от конкретного таймера, и, соответственно, нужно посмотреть сначала, от какого именно АЦП может работать, а потом уже настраивать соответствующий таймер.

Не забываем настроить DMA:

B93-6

И в прерывании от DMA уже будем копировать результаты в Input Register. Примечание: А почему сразу туда не копировать, что бы не нагружать МК ? Может возникнуть ситуация, когда во время чтения из Input Register DMA будет писать в него же новые данные и в итоге будет фигня. На деле же, в callback по запросу или в общем цикле можно просто вызывать оцифровку АЦП и обновлять значения Input Register, но я сначала сделал по привычке, а потом уже поленился переделывать попроще… :)

В остальном ничего примечательного, разве что проверяем тактовую частоту МК, чтобы она соответствовала расчётам для таймеров:

B93-7

Генерим проект, добавляем в папку Middlewares саму библиотеку FreeModbus с файлами порта под STM32, добавляем дополнительные файлы для открытия и работы в VSC (всё можно прочесть тут Visual Studio Code — написание и отладка прошивок для ARM Cortex-M) и начинаем вносить правки в проект:

— Файл Middlewares\FreeModbus\include\mbconfig.h — тут настраиваем библиотеку FreeModbus, собственно, указываем, что у нас слейв RTU.

— Файл Middlewares\FreeModbus\app\user_mb_app.h — тут настраиваем количество ячеек у слейва и их адреса.

— Файл main.c — и, собственно, главное: подключаем FreeModbus, определяем callback для DMA, вызываем инициализацию слейва с адресом 0xA и пулим в основном цикле. Всё. :)

Тестовый проект и железо для мастера

Всё так же просто, взял простую отладку на STM32F401CCU6 (советую покупать именно её или на F411 даже, а не блупил, на который уже очень много подделок или «китайских аналогов» паяют и в целом, F4 свежее, больше плюшек имеет), к ней подключаю SPI дисплей на ST7735 и опять же UART<->RS-485 на MAX485ESA. Вот так всё получилось:

B93-8

По прошивке тут всё аналогично слейву, разве что в main.c добавлена extern переменная xNeedPoll, которая обновляется из portevent_m.c, и она означает готовность мастера к следующему запросу от слейва. Это не обязательное действие, но всё же.

Примечание: есть одно важное отличие в работе мастера от слейва. В файле user_mb_app_m.h настраиваются ячейки для одного экземпляра слейва, а мастер должен хранить все данные возможных слейвов, максимальное количество которых указывается в mbconfig.h, в директиве MB_MASTER_TOTAL_SLAVE_NUM, и фактически там указывается МАКСИМАЛЬНЫЙ адрес слейва, который мастер может опросить, поэтому не стоит делать слейвы с адресами 100500, потому что в мастере в итоге придётся резервировать на столько экземпляров слейвов ячеек (например два Input Register, MB_MASTER_TOTAL_SLAVE_NUM 100, и в итоге usMRegInBuf[MB_MASTER_TOTAL_SLAVE_NUM][M_REG_INPUT_NREGS] из файла user_mb_app_m.c будет размером 2*100 байт!).

Ну и чуть отличается основной цикл:

eMBMasterPoll();
HAL_Delay(1);
ST7735_PutDec(usMRegInBuf[9][0], 4, 30, 0, COLOR565_AQUA, COLOR565_BLACK);
ST7735_PutDec(usMRegInBuf[9][1], 4, 60, 0, COLOR565_FIRE_BRICK, COLOR565_BLACK);
if(xNeedPoll)
{
eMBMasterReqReadInputRegister(0xA, 0, 2, 2);
xNeedPoll = FALSE;
}

Фактически тут пулится библиотека мастера, после чего постоянно выводятся на дисплей значения первых двух Input Register по адресу слейва 0xA (10) и при возможности делается новый запрос на чтение двух Input Register из слейва по адресу 0xA.

Примечание: прошу не бить за драйвер для дисплея ST7735, он собран левой ногой из разных проектов, чисто для теста.

Вот и всё, ничего сложного. Давайте проверим это на реальных железяках. :)

Результаты

Водим джостиком, смотрим результаты на дисплее:

B93-9

Да, не покупайте вот такую хрень, типа универсальный USB<->RS-485<->RS-232<->TTL:

B93-10

B93-11

Подключаешь это, вроде бы всё нормально, определился и готов к работе, но как только начинается более-менее плотный обмен по тому же RS-485, оно начинает хаотично отваливаться и заново появляться в ОС. Продолбавшись с этим чудом (а я думал, что это я накосячил в проге или подключении, может, коротило чего на линии RS-485), я заметил, что микросхема RS-232 иногда начинала греться ощутимо, а иногда была холодная. Выпаял. Ничего не изменилось… но, наглядевшись на дивную разводку сей платы, я приметил фишку, что тут всего 2 конденсатора на все питания платы (3,3 В и 5 В), более того, стоит обычный MAX485 (5 В), который без конденсатора по питанию рядом подключен к 3,3 В (чтобы не париться с логическими уровнями, лайфхак от китайцев)!

В общем, помогло припаивание двух конденсаторов (10 мкФ на 5 В и 1 мкФ около MAX485) по питаниям, а также терминатор китайцы забыли, тоже прилепил как мог, всё отметил красными прямоугольниками. Отмывать от флюса уже не стал это чудо китайских гениев инженерной мысли…

На этом всё. Спасибо, что прочитали! Пишите комменты, буду рад, если найдёте ошибки и сообщите о них. ;)

Ссылки

Добавить комментарий

Please log in using one of these methods to post your comment:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Google photo

Для комментария используется ваша учётная запись Google. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s