Портирование FreeModbus 1.5 под STM32 HAL rs485 без RTOS

С недавних пор я начал заниматься встраиваемыми системами и докатился до программирования микроконтроллеров, а именно STM32F373. Одной из задач было развернуть Modbus Slave RTU поверх интерфейса rs485.

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

0. Готовим проект


Для быстрого старта я использую STM32CubeMX, он позволяет быстро генерить код инициализации контроллера без боли. В моем случае это STM32F373VCTx LQFP100.

Периферия:

  • TIM6
  • USART1

Делаю это только для генерации функций HAL_TIM_Base_MspInit() и HAL_UART_MspInit(), это упростит вам жизнь, если будете подключать другие таймеры и usart'ы.

peripheral

Тактирование:

Нам необходимо, чтобы таймер TIM6 или тот, который вы выбрали работал на частоте не ниже 20кГц. Пошуршав по коду инициализации я понял, что TIM6 работает на PCLK1 (Peripheral CLocK). По умолчанию он (таймер) тактируется от HSI (High Speed Internal resonator), так же как и все остальное, но мне показалось, что это маловато, поэтому я погнал все это дело через PLL (Phase-Locked Loop) и выставил значение множителя на x8, чтобы поднять до 32МГц, так МК работает плавнее и приятнее.

clock config

Дополнительная конфигурация:

Нам потребуется работать с таймером по прерыванию, поэтому включим его на вкладке Configuration раздел System NVIC «TIM6 global interrupt and DAC1 underrun error interrupts». Также нам нужны прерывания от USART1 «USART1 global interrupt / USART1 wake-up interrupt through EXTI line 25».

image

Я использую много дополнительных прерываний для индикации ошибок, поэтому не обращайте внимание на кучу галочек.

На этом заканчиваем, сохраняем проект, генерим код.

Я генерирую код для SW4STM32 (System Workbench for STM32, потребуется регистрация). Предпочитаю генерировать отдельные .h/.c файлы для периферии, включать full_assert и отключать генерацию вызова функций инициализации периферии.

imageimageimage

1. Download & install


Тут все просто: качаем архив, распаковываем, копируем сорцы из архив/modbus/ в проект, не забываем про архив/modbus/include, добавить в include'ы.
Также нам потребуется port layer, с которым мы и будем работать, берем его из архив/demo/BARE, копируем, готово.

2. Портируем


Все делаем опираясь на официальную документацию.

port.h

Начнем с include'ов: я убираю assert.h, потому что перекидываю assert() на assert_param(), и добавляю stm32f3xx_hal.h, если вы используете процессор другой серии, то вместо этой используйте соответствующую библиотеку.

Далее нас интересуют макросы ENTER_CRITICAL_SECTION( ) и EXIT_CRITICAL_SECTION( ). Заменяем их на __disable_irq() и __enable_irq() соответственно.
Сразу стоит отметить, что это не лучшая реализация критических секций: если попадется ситуация в которой функция с такой критической секцией вызовет другую функцию с такой критической секцией из своей критической секции, то мы получим преждевременное включение прерываний (пример в спойлере), но если портировать описанным в статье способом, то такой ситуации не произойдет. А решение проблемы можно найти тут.

Демонстрация проблемы
void foo() {
    ENTER_CRITICAL_SECTION(); // второе попадание в критическую секцию

    // делаем полезные штуки не боясь прерываний

    EXIT_CRITICAL_SECTION(); // тут плохо, прерывания не должны включаться, мы все еще в критической секции функции bar()
}

void bar() {
    ENTER_CRITICAL_SECTION(); // первое попадание в критическую секцию

    foo(); // после выполнения функции прерывания уже работают, а это неправильно

    EXIT_CRITICAL_SECTION(); // бесполезно =(
}



Также я добавляю прототип UART_IRQ_Handler(), который будет использоваться для немного костыльной реализации реакции на прерывания.

port.h
/*
 * FreeModbus Libary: BARE Port
 * Copyright (C) 2006 Christian Walter <wolti@sil.at>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * File: $Id: port.h,v 1.1 2006/08/22 21:35:13 wolti Exp $
 */

#ifndef _PORT_H
#define _PORT_H

#include "stm32f3xx_hal.h"
#include <inttypes.h>

#define	INLINE                      inline
#define PR_BEGIN_EXTERN_C           extern "C" {
#define	PR_END_EXTERN_C             }

// управление прерываниями
#define ENTER_CRITICAL_SECTION( )	__disable_irq()
#define EXIT_CRITICAL_SECTION( )	__enable_irq()

// подменим вызовы по всей библиотеки
#define assert(val) assert_param(val)

typedef uint8_t BOOL;

typedef unsigned char UCHAR;
typedef char CHAR;

typedef uint16_t USHORT;
typedef int16_t SHORT;

typedef uint32_t ULONG;
typedef int32_t LONG;

#ifndef TRUE
#define TRUE            1
#endif

#ifndef FALSE
#define FALSE           0
#endif

// callback для uart
BOOL UART_IRQ_Handler(USART_TypeDef * usart);

#endif


porttimer.c

Опять начнем с include'ов: в разделе platform includes добавим stm32f3xx_hal_tim.h, из него нужна константа TIM_COUNTERMODE_UP.

В разделе static functions я добавляю handler для таймера и 2 переменных для хранения таймаута, и текущего значения счетчика.
Функцию xMBPortTimersInit() наполним инициализацией таймера. Рассчитываем на то, что он будет тикать каждые 50 мкс.

htim — наш handler, тип — static TIM_HandleTypeDef, глобальный
timeout — static uint16_t, глобальный
downcounter — static uint16_t, глобальный

    htim.Instance = TIM6; // указываем, что будем работать с 6 таймером
    htim.Init.CounterMode = TIM_COUNTERMODE_UP; // тип работы таймера (от 0 и вверх)
    /* инициализируем делитель частоты таймера, нам же не нужен таймер на 32МГц
       HAL_RCC_GetPCLK1Freq() - возвращает частоту PCLK1, в нашем случае будет 32000000
       делим на 1000000, чтобы получить делитель для работы таймера с частотой 1МГц
       а -1 - это самый сок: дело в том, что 0 - это делитель на 1, 1 - делитель на 2 и т.д.*/
    htim.Init.Prescaler = (HAL_RCC_GetPCLK1Freq() / 1000000) - 1;
    // период 50 мкс, с -1, думаю, все понятно
    htim.Init.Period = 50 - 1;

    // запишем значение таймаута, оно понадобится для перезапуска таймера
    timeout = usTim1Timerout50us;

    // все, вызываем инициализацию таймера, все остальное сделает HAL
    return HAL_OK == HAL_TIM_Base_Init(&htim) ? TRUE : FALSE;

Функция vMBPortTimersEnable(). Тут 2 простых действия:

  1. Сбросить таймер.
  2. Запустить таймер асинхронно с обратной связью по прерыванию.

    downcounter = timeout;
    HAL_TIM_Base_Start_IT(&htim);

Функция vMBPortTimersDisable(). Тут просто отключим таймер.

    HAL_TIM_Base_Stop_IT(&htim);

Также добавим функцию для реагирования на таймер HAL_TIM_Base_Stop_IT():

    // проверяем, что прерывание того типа, который нужен и он нашего таймера
    if(__HAL_TIM_GET_FLAG(&htim, TIM_FLAG_UPDATE) != RESET && __HAL_TIM_GET_IT_SOURCE(&htim, TIM_IT_UPDATE) !=RESET) {
        __HAL_TIM_CLEAR_IT(&htim, TIM_IT_UPDATE); // сбрасываем флаг прерывания
        if (!--downcounter) // декрементируем счетчик, пока он не достигнет нуля
            prvvTIMERExpiredISR(); // вызываем callback библиотеки
    }

porttimer.c
/*
 * FreeModbus Libary: BARE Port
 * Copyright (C) 2006 Christian Walter <wolti@sil.at>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * File: $Id: porttimer.c,v 1.1 2006/08/22 21:35:13 wolti Exp $
 */

/* ----------------------- Platform includes --------------------------------*/
#include "port.h"
#include "stm32f3xx_hal_tim.h"

/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"

/* ----------------------- static functions ---------------------------------*/
static void prvvTIMERExpiredISR( void );
static TIM_HandleTypeDef htim;

static uint16_t timeout = 0;
static uint16_t downcounter = 0;

/* ----------------------- Start implementation -----------------------------*/
BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us )
{
	htim.Instance = TIM6;
	htim.Init.CounterMode = TIM_COUNTERMODE_UP;
	htim.Init.Prescaler = (HAL_RCC_GetPCLK1Freq() / 1000000) - 1;
	htim.Init.Period = 50 - 1;

	timeout = usTim1Timerout50us;

    return HAL_OK == HAL_TIM_Base_Init(&htim) ? TRUE : FALSE;
}

inline void
vMBPortTimersEnable(  )
{
    /* Enable the timer with the timeout passed to xMBPortTimersInit( ) */
	downcounter = timeout;
	HAL_TIM_Base_Start_IT(&htim);
}

inline void
vMBPortTimersDisable(  )
{
    /* Disable any pending timers. */
	HAL_TIM_Base_Stop_IT(&htim);
}

/* Create an ISR which is called whenever the timer has expired. This function
 * must then call pxMBPortCBTimerExpired( ) to notify the protocol stack that
 * the timer has expired.
 */
static void prvvTIMERExpiredISR( void )
{
    ( void )pxMBPortCBTimerExpired(  );
}

void TIM6_DAC1_IRQHandler(void) {
	/* TIM Update event */
	if(__HAL_TIM_GET_FLAG(&htim, TIM_FLAG_UPDATE) != RESET && __HAL_TIM_GET_IT_SOURCE(&htim, TIM_IT_UPDATE) !=RESET) {
		__HAL_TIM_CLEAR_IT(&htim, TIM_IT_UPDATE);
		if (!--downcounter)
			prvvTIMERExpiredISR();
	}
}



portserial.c

В общем то вся статья была написана именно из-за этого файла, потому что портировать UART-layer на STM32 под «чистым» HAL мне не удалось и пришлось лезть глубже. В описании библиотеки написано, что нужны прерывания, сигнализирующие о том, что буффер передачи пуст и что буффер приема не пуст. А HAL не поддерживает такие callback'и, поэтому будем выпендриваться.

Тезаурус:

huart — handler для UART, static UART_HandleTypeDef
DE_Port — порт ножки управления направлением канала, static GPIO_TypeDef *
DE_Pin — пин управления направлением канала, static uint16_t

Начнем с функции vMBPortSerialEnable(). Тут мы обратимся к «скрытым возможностям» HAL.

    if (xRxEnable) { // если переключаемся в режим приема
        // переключаем драйвер в режим приема (см. принципы работы rs485)
        HAL_GPIO_WritePin(DE_Port, DE_Pin, GPIO_PIN_RESET);
        __HAL_UART_ENABLE_IT(&huart, UART_IT_RXNE); // разрешаем прерывание, реагирует если буфер приема не пуст
    } else {
        __HAL_UART_DISABLE_IT(&huart, UART_IT_RXNE); // запрещаем прерывание
    }
    if (xTxEnable) { // если переключаемся в режим передачи
        // переключаем драйвер в режим передачи (см. принципы работы rs485)
        HAL_GPIO_WritePin(DE_Port, DE_Pin, GPIO_PIN_SET);
        __HAL_UART_ENABLE_IT(&huart, UART_IT_TXE); // разрешаем прерывание, реагирует если буфер передачи пуст
    } else {
        __HAL_UART_DISABLE_IT(&huart, UART_IT_TXE); // запрещаем прерывание
    }

Далее функция инициализации порта xMBPortSerialInit(). Сделал ее более-менее универсальной, она сама инициализирует заданный UART, но не забываем, что часть с MSP-инициализацией должна быть сгенерирована: инициализация пинов, прерывания и пр.

xMBPortSerialInit()
    huart.Init.Mode = UART_MODE_TX_RX; // работаем на прием и передачу
    huart.Init.HwFlowCtl = UART_HWCONTROL_NONE; // без контроля потока (у нас же rs485)
    // сэмплинг, не могу нормально объяснить, но это нужно для защиты от шумов
    huart.Init.OneBitSampling = UART_ONEBIT_SAMPLING_DISABLED;
    huart.Init.OverSampling = UART_OVERSAMPLING_16;
    // один стоп-бит
    huart.Init.StopBits = UART_STOPBITS_1;
    // без доп-фич
    huart.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;

    // портозависимая инициализация: UART, ножка контроля направления приемопередачи
    switch (ucPORT) {
    case 0:
        huart.Instance = USART1;
        DE_Port = GPIOA;
        DE_Pin = GPIO_PIN_12;
            break;
    case 1:
        huart.Instance = USART2;
        DE_Port = GPIOA;
        DE_Pin = GPIO_PIN_1;
            break;
    case 2:
        huart.Instance = USART3;
        DE_Port = GPIOB;
        DE_Pin = GPIO_PIN_14;
            break;
    default:
        // вызовем ошибку, если выбрали не тот номер порта, хотя это будет ошибкой портирования, но хоть что-то
        return FALSE;
    }

    // скорость
    huart.Init.BaudRate = ulBaudRate;

    // размер слова
    switch (ucDataBits) {
    case 8:
        huart.Init.WordLength = UART_WORDLENGTH_8B;
        break;
    case 9:
        huart.Init.WordLength = UART_WORDLENGTH_9B;
        break;
    default:
        // вызовем ошибку, если выбрали не тот номер порта, хотя это будет ошибкой портирования, но хоть что-то
        return FALSE;
    }

    // настраиваем бит четности
    switch (eParity) {
    case MB_PAR_NONE:
        huart.Init.Parity = UART_PARITY_NONE;
        break;
    case MB_PAR_EVEN:
        huart.Init.Parity = UART_PARITY_EVEN;
        break;
    case MB_PAR_ODD:
        huart.Init.Parity = UART_PARITY_ODD;
        break;
    default:
        // вызовем ошибку, если выбрали не тот номер порта, хотя это будет ошибкой портирования, но хоть что-то
        return FALSE;
    }

    // Инициализируем порт, как rs485
    return HAL_OK == HAL_RS485Ex_Init(&huart, UART_DE_POLARITY_HIGH, 0, 0) ? TRUE : FALSE;


Теперь функции ввода/вывода xMBPortSerialPutByte() и xMBPortSerialGetByte(). Тут мы будем делать хардкорный низкоуровневый (для HAL) IO, используя регистры UART'а.

Для чтения байта из порта: *pucByte = huart.Instance->RDR
Для записи байта в порт: huart.Instance->TDR = ucByte

И, напоследок, добавим функцию UART_IRQ_Handler(), которую мы описали в port.h. Она будет отвечать за перехват прерываний ввода/вывода. Основная идея: если прерывание «наше», т.е. от нашего порта, и то, что мы ждем, то возвращаем TRUE — это значит, что мы его перехватили.

    if (usart == huart.Instance) { // проверим, что прерываниеот нашего порта
        if((__HAL_UART_GET_IT(&huart, UART_IT_RXNE) != RESET) && (__HAL_UART_GET_IT_SOURCE(&huart, UART_IT_RXNE) != RESET)) { // проверим, что прерывание которое нам нужно
            pxMBFrameCBByteReceived(); // сообщим об этом библмотеке
            __HAL_UART_SEND_REQ(&huart, UART_RXDATA_FLUSH_REQUEST); // надо сбросить прерывание
            return TRUE; // говорим, что перехватили
        }
        if((__HAL_UART_GET_IT(&huart, UART_IT_TXE) != RESET) &&(__HAL_UART_GET_IT_SOURCE(&huart, UART_IT_TXE) != RESET)) { // проверим, что прерывание которое нам нужно
            pxMBFrameCBTransmitterEmpty(); // сообщим об этом библмотеке
            return TRUE; // говорим, что перехватили
        }
    }
    return FALSE; // говорим, что это не наше, пусть обрабатывают дальше

portserial.c
/*
 * FreeModbus Libary: BARE Port
 * Copyright (C) 2006 Christian Walter <wolti@sil.at>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * File: $Id: portserial.c,v 1.1 2006/08/22 21:35:13 wolti Exp $
 */

#include "port.h"

/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"

/* ----------------------- static functions ---------------------------------*/
static UART_HandleTypeDef huart;
static GPIO_TypeDef * DE_Port;
static uint16_t DE_Pin;
/* ----------------------- Start implementation -----------------------------*/
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
    /* If xRXEnable enable serial receive interrupts. If xTxENable enable
     * transmitter empty interrupts.
     */
	if (xRxEnable) {
		HAL_GPIO_WritePin(DE_Port, DE_Pin, GPIO_PIN_RESET);
		__HAL_UART_ENABLE_IT(&huart, UART_IT_RXNE);
	} else {
		__HAL_UART_DISABLE_IT(&huart, UART_IT_RXNE);
	}

	if (xTxEnable) {
		HAL_GPIO_WritePin(DE_Port, DE_Pin, GPIO_PIN_SET);
		__HAL_UART_ENABLE_IT(&huart, UART_IT_TXE);
	} else {
		__HAL_UART_DISABLE_IT(&huart, UART_IT_TXE);
	}
}

BOOL
xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{
	huart.Init.Mode = UART_MODE_TX_RX;
	huart.Init.HwFlowCtl = UART_HWCONTROL_NONE;
	huart.Init.OneBitSampling = UART_ONEBIT_SAMPLING_DISABLED;
	huart.Init.OverSampling = UART_OVERSAMPLING_16;
	huart.Init.StopBits = UART_STOPBITS_1;
	huart.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;

	switch (ucPORT) {
	case 0:
		huart.Instance = USART1;
		DE_Port = GPIOA;
		DE_Pin = GPIO_PIN_4;
		break;
	case 1:
		huart.Instance = USART2;
		DE_Port = GPIOA;
		DE_Pin = GPIO_PIN_1;
		break;
	case 2:
		huart.Instance = USART3;
		DE_Port = GPIOB;
		DE_Pin = GPIO_PIN_14;
		break;
	default:
		return FALSE;
	}

	huart.Init.BaudRate = ulBaudRate;

	switch (ucDataBits) {
		case 8:
			huart.Init.WordLength = UART_WORDLENGTH_8B;
			break;
		case 9:
			huart.Init.WordLength = UART_WORDLENGTH_9B;
			break;
		default:
			return FALSE;
	}

	switch (eParity) {
	case MB_PAR_NONE:
		huart.Init.Parity = UART_PARITY_NONE;
		break;
	case MB_PAR_EVEN:
		huart.Init.Parity = UART_PARITY_EVEN;
		break;
	case MB_PAR_ODD:
		huart.Init.Parity = UART_PARITY_ODD;
		break;
	default:
		return FALSE;
	}

    return HAL_OK == HAL_RS485Ex_Init(&huart, UART_DE_POLARITY_HIGH, 0, 0);
}

BOOL
xMBPortSerialPutByte( CHAR ucByte )
{
    /* Put a byte in the UARTs transmit buffer. This function is called
     * by the protocol stack if pxMBFrameCBTransmitterEmpty( ) has been
     * called. */
	huart.Instance->TDR = ucByte;
    return TRUE;
}

BOOL
xMBPortSerialGetByte( CHAR * pucByte )
{
    /* Return the byte in the UARTs receive buffer. This function is called
     * by the protocol stack after pxMBFrameCBByteReceived( ) has been called.
     */
	*pucByte = huart.Instance->RDR;
    return TRUE;
}

BOOL UART_IRQ_Handler(USART_TypeDef * usart) {
	if (usart == huart.Instance) {
		if((__HAL_UART_GET_IT(&huart, UART_IT_RXNE) != RESET) && (__HAL_UART_GET_IT_SOURCE(&huart, UART_IT_RXNE) != RESET)) {
			pxMBFrameCBByteReceived();
			__HAL_UART_SEND_REQ(&huart, UART_RXDATA_FLUSH_REQUEST);
			return TRUE;
		}
		if((__HAL_UART_GET_IT(&huart, UART_IT_TXE) != RESET) &&(__HAL_UART_GET_IT_SOURCE(&huart, UART_IT_TXE) != RESET)) {
			pxMBFrameCBTransmitterEmpty();
			return TRUE;
		}
	}
	return FALSE;
}


Казалось бы на этом все, но есть еще пара моментов:

Как вы могли заметить, функцию UART_IRQ_Handler() никто не вызывает. Чтобы это исправить, надо посетить файл stm32f3xx_it.c. Там добавим include для port.h. Во все USARTx_IRQ_Handler'ы надо добавить следующие строки (в нашем случае USART1_IRQ_Handle()).

    // если прерывание перехвачено, то не будем его обрабатывать дальше
    if (FALSE != UART_IRQ_Handler(USART1))
        return;

stm32f3xx_it.h
/**
  ******************************************************************************
  * @file    stm32f3xx_it.c
  * @brief   Interrupt Service Routines.
  ******************************************************************************
  *
  * COPYRIGHT(c) 2016 STMicroelectronics
  *
  * Redistribution and use in source and binary forms, with or without modification,
  * are permitted provided that the following conditions are met:
  *   1. Redistributions of source code must retain the above copyright notice,
  *      this list of conditions and the following disclaimer.
  *   2. Redistributions in binary form must reproduce the above copyright notice,
  *      this list of conditions and the following disclaimer in the documentation
  *      and/or other materials provided with the distribution.
  *   3. Neither the name of STMicroelectronics nor the names of its contributors
  *      may be used to endorse or promote products derived from this software
  *      without specific prior written permission.
  *
  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  *
  ******************************************************************************
  */
/* Includes ------------------------------------------------------------------*/
#include "stm32f3xx_hal.h"
#include "stm32f3xx.h"
#include "stm32f3xx_it.h"

/* USER CODE BEGIN 0 */
#include "port.h"
/* USER CODE END 0 */

/* External variables --------------------------------------------------------*/
extern UART_HandleTypeDef huart1;

/******************************************************************************/
/*            Cortex-M4 Processor Interruption and Exception Handlers         */ 
/******************************************************************************/

/**
* @brief This function handles System tick timer.
*/
void SysTick_Handler(void)
{
  /* USER CODE BEGIN SysTick_IRQn 0 */

  /* USER CODE END SysTick_IRQn 0 */
  HAL_IncTick();
  HAL_SYSTICK_IRQHandler();
  /* USER CODE BEGIN SysTick_IRQn 1 */

  /* USER CODE END SysTick_IRQn 1 */
}

/******************************************************************************/
/* STM32F3xx Peripheral Interrupt Handlers                                    */
/* Add here the Interrupt Handlers for the used peripherals.                  */
/* For the available peripheral interrupt handler names,                      */
/* please refer to the startup file (startup_stm32f3xx.s).                    */
/******************************************************************************/

/**
* @brief This function handles USART1 global interrupt / USART1 wake-up interrupt through EXTI line 25.
*/
void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
	if (FALSE != UART_IRQ_Handler(USART1))
		return;
  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */

  /* USER CODE END USART1_IRQn 1 */
}

/* USER CODE BEGIN 1 */

/* USER CODE END 1 */
/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/


И самое странное, что я пока так и не смог решить: библиотека, при ответе, отдавала все байты кроме последнего. Меня это убивало, причем при подсчете все было ОК, вероятно это вопрос UART'а или кривых рук, но спасло следующее решение: просто добавим в счетчик байт на отправку еще единицу (файл modbus/rtu/mbrtu.c функция eMBRTUSend()).

modbus/rtu/mbrtu.c функция eMBRTUSend()
eMBErrorCode
eMBRTUSend( UCHAR ucSlaveAddress, const UCHAR * pucFrame, USHORT usLength )
{
    eMBErrorCode    eStatus = MB_ENOERR;
    USHORT          usCRC16;

    ENTER_CRITICAL_SECTION(  );

    /* Check if the receiver is still in idle state. If not we where to
     * slow with processing the received frame and the master sent another
     * frame on the network. We have to abort sending the frame.
     */
    if( eRcvState == STATE_RX_IDLE )
    {
        /* First byte before the Modbus-PDU is the slave address. */
        pucSndBufferCur = ( UCHAR * ) pucFrame - 1;
        usSndBufferCount = 1;

        /* Now copy the Modbus-PDU into the Modbus-Serial-Line-PDU. */
        pucSndBufferCur[MB_SER_PDU_ADDR_OFF] = ucSlaveAddress;
        usSndBufferCount += usLength;

        /* Calculate CRC16 checksum for Modbus-Serial-Line-PDU. */
        usCRC16 = usMBCRC16( ( UCHAR * ) pucSndBufferCur, usSndBufferCount );
        ucRTUBuf[usSndBufferCount++] = ( UCHAR )( usCRC16 & 0xFF );
        ucRTUBuf[usSndBufferCount++] = ( UCHAR )( usCRC16 >> 8 );


        usSndBufferCount++; // вот тут этот костыль обитает


        /* Activate the transmitter. */
        eSndState = STATE_TX_XMIT;
        vMBPortSerialEnable( FALSE, TRUE );
    }
    else
    {
        eStatus = MB_EIO;
    }
    EXIT_CRITICAL_SECTION(  );
    return eStatus;
}


3. Usage


Уже почти готово.

Настройка

Посетим mbconfig.h. Ищите его в include'ах. У меня SW4STM32, поэтому меня спасает Ctrl + Click. Сначала сконфигурируем библиотеку для работа только с ModbusRTU:

/*! \brief If Modbus ASCII support is enabled. */
#define MB_ASCII_ENABLED                        (  0 )

/*! \brief If Modbus RTU support is enabled. */
#define MB_RTU_ENABLED                          (  1 )

/*! \brief If Modbus TCP support is enabled. */
#define MB_TCP_ENABLED                          (  0 )

Еще можете поотключать функции которые не используете, это облегчит библиотеку и позволит не реализовывать callback'и.

Использование

  1. #include «mb.h
  2. eMBInit()
  3. eMBEnable()
  4. eMBPoll()

И не забудьте реализовать callback'и.

На этом считаю, что свою миссию выполнил, за примерами и документацией идите на офф-сайт.

Usage: Modules/Modbus
Callbacks: Modules/Modbus Registers
Поделиться публикацией

Комментарии 19

    0
    Отличная статья! Очень наглядно и доступно.
    А можно ли применять STM32CubeMX для генерации кода для камней STM32 F0 и F1?
      +1
      Можно.
        +1
        Да, смотри, на офф-сайте STM заявляют, что CubeMX работает с линейками от F0 до L4.
        +1
        Похоже каждый уважающий себя Программист написал свой вариант модбаса на стм32!)))
          0
          Но почему-то не каждый опубликовал. Не смог найти реализацию под HAL. А при портировании выяснил, что чистый HAL не может того, что нужно.
          +1
          как по мне хуарт так называется неспроста. его использование во многих случаях чрезмерно. единственное что есть переносимость с f0 на другие кристаллы вплоть до л4. хотя я не вижу ничего страшного в правке имен пары регистров для настройки, а вот висеть столько в прерывание — не камильфо. имхо далеко не лушая реализация поставленной перед ТС задачи.
            0
            А как поступили бы Вы?
            0
            Благодарю за статью. Мне как раз предстоит в ближайшем будущем реализовывать поддержку Modbus/RTU.

            Только я не вижу обработки интервалов. Их нет вообще или просто Вы не включили их в статью? Я имею в виду межсимвольный интервал t1.5 и межпакетный интервал t3.5 (по оф. терминологии).

            Я хочу сделать следующим образом. Согласно документации, интервалы таковы:

            19200 бод и ниже: t1.5 = 1.5 char = 3 x 0.5 char; t.3.5 = 3.5 char = 7 x 0.5 char;
            19200 бод и выше: t1.5 = 750 мкс = 3 x 250 мкс; t3.5 = 1750 мкс = 7 x 250 мкс.

            Т.о. таймер программируется на t0.5 (по моей терминологии). Прикинул, что значение Prescaler = 125(-1) будет оптимальным. Оно позволяет получить целочисленное значение Period, а также единую формулу вычисления для различных камней/частот и скоростей обмена.

            Понадобятся 2 счётчика:

            счётчик повторений, c_rep — количество срабатываний таймера;
            счётчик «ошибок», c_err — количество «косых» фреймов.

            При получении байта счётчик c_rep сбрасывается. Когда c_rep == 3, счётчик c_err увеличивается. Т.о. если c_rep == 7, то пакет получен. При этом, если c_err == 1, то пакет целый, в противном случае — битый.

            Поскольку я далеко не профи в данной области, прошу прокомментировать мой [пока] чисто умозрительный подход.
              0
              Я не включал их в статью, потому что она (реализация интервалов) спрятана под капотом.
              Сразу стоит уточнить, что это не те интервалы, которые надо выдерживать, а те, которые нельзя превышать. То есть, если Вы превысите межпакетный интервал, то система считает, что на линии проблемы. Проверкой межпакетного интервала занимается таймер, который мы портировани в porttimer.c.
                0
                _1. Похоже, я чего-то недопонимаю.

                В документации на modbus написано:

                In RTU mode, message frames are separated by a silent interval of at least 3.5 character times

                и

                If a silent interval of more than 1.5 character times occurs between two characters, the message frame is declared incomplete and
                should be discarded by the receiver.

                Т.е. слэйв должен следить как за межпакетным, так и межсимвольным интервалами. Я этого не увидел в статье (может, проглядел), поэтому и спросил. Мне интересно, как именно Вы это реализовали. Например, я слышал, что в MSP есть возможность сделать "двойной" таймер, т.е. в одном таймере задать два времени срабатывания.

                _2. Если не ошибаюсь, имеется возможность отправить целиком буфер с помощью HAL_UART_Transmit_DMA. Что можете сказать по этому поводу?
                  0
                  1. Вероятно должен, но в freeModbus контролируют только 3.5t интервал. В porttimer.c мы как раз инициализируем таймер, который занимается контролем этого интервала. FreeModbus перезапускает таймер каждый раз, когда приходит новый байт, и начинает обрабатывать сообщение, когда таймер обнуляется.
                    По поводу "двойного" таймера: знаю, что есть многоканальные, не расскажу, потому что не работал с ними.

                  2. Официальная документация на HAL описывает 3 способа взаимодействия с периферией:
                    1. Polling
                    2. Interrupt
                    3. DMA

                    И во всех случаях можно и нужно передавать буферы данных.
                    Если интересует конкретно DMA, то я, пока, его не использовал, но там идея в том, что мы пишем данные в память, а потом DMA-контроллер сам скармливает его периферии. DMA надо отдельно инициализировать (CubeMX справится).
              0
              Вот теперь Вы поняли, почему многие так любят Ардуино?
                0
                Если я правильно понимаю, Ардуино — это отладочные платы, правильно?
                Их сделали, чтобы студенты могли практиковаться, или для создания прототипов.
                0
                По поводу костыля. Вы обрабатываете событие TXE передатчика, поэтому линия DE деактивируется в момент начала передачи последнего байта, а не тогда, когда этот байт уже целиком «выдвинется». Поэтому либо Ваш костыль, либо после записи последнего байта нужно ждать события TC (transmission complete).
                  0
                  Я не совсем понял. Я же сам управляю направлением передачи, нет?
                  По поводу TC: я смотрел исходники HAL, и там TC вызывается программно. Или есть возможность получить это прерывание аппаратно?
                  Но идея мне понравилась, можно перенести переключение направления передачи прямо в обработчики прерываний и посмотреть что получится.
                    0
                    Бит TC в регистре статуса, прерывание от него маскируется битом TCIE в USART_CR1.
                    Вызов pxMBFrameCBTransmitterEmpty() в UART_IRQ_Handler() при передаче последнего байта вызывает vMBPortSerialEnable( TRUE, FALSE ), что выключает DE в то время как этот байт только начал передаваться по линии.
                  0
                  Далее нас интересуют макросы ENTER_CRITICAL_SECTION( ) и EXIT_CRITICAL_SECTION( ). Заменяем их на __disable_irq() и __enable_irq() соответственно.

                  С таким можно отгрести, если произойдёт вложенный вызов. У меня (Bare-metal ARM926E-JS, правда ещё ThreadX есть, но временами и прерывания гасить нужно) сделано с сохранением прерывания и при выходе из критической секции восстанавливается исходное. Т.е. если оно было и так выключено — оно выключенным и останется:

                  // Declare interrupt save area
                  #define INTR_SAVE_AREA unsigned int _interrupt_save, _interrupt_temp
                  
                  // Disable iterrupts and save old status register
                  #define INTR_DISABLE __asm volatile (\
                      "mrs %0, CPSR\n\t" \
                      "orr %1, %0, #128\n\t" \
                      "msr CPSR_c, %1" \
                      : "=r"(_interrupt_save), "=r"(_interrupt_temp) /* output  */ \
                      : /* input   */ \
                      : /* clobber */ \
                      )
                  
                  // Restore status register
                  #define INTR_RESTORE __asm volatile (\
                      "msr CPSR_c, %0" \
                      : /* output  */ \
                      : "r"(_interrupt_save)/* input   */ \
                      : /* clobber */ \
                      )

                  В вашем случае нужно совместить INTR_SAVE_AREA и INTR_DISABLE. Правда как оно будет выглядеть без C99 (при объявлении переменной не в начале функции) и при наличии goto — нужно думать.

                  Кстати, попытался поискать как используются эти вызовы и наткнулся: http://we.easyelectronics.ru/Yanichar/portiruem-freemodbus-rtu-na-primere-stm8l.html, в прочем вышеописанная проблема там тоже присутствует, на что так же указали в комментарии.

                  Вот ещё описание портирования: http://ctrl-v.biz/blog/6 — на STM32F4 (плата STM32F4Discovery). Похоже, что всё изобретено до нас :)
                    0
                    По поводу вложенных прерываний: да, я знаю, читал про это, но в этой библиотеке нет вложенных вызовов, поэтому простительно. Они могли бы быть, если бы мы вызывали функцию отключения прерываний внутри файлов портирования библиотеки, но там я не нашел мест, где это нужно делать. Думаете стоит включить пояснения по этому вопросу в статью?

                    По поводу "изобретено до нас": я не зря указал в статье, что мы работаем с HAL. HAL снижает порог вхождения в разработку под STM, но на нем можно сделать не все, это же все-таки абстракция. Пришлось немного запарится, и я решил поделиться своим видением: можно использовать HAL, но нужно использовать немного хаков, думаю это будет полезно тем, кому надо "очень срочно" портировать библиотеку и у них нет опыта работы с StdPeriph, который, как заявляют STM, устарел.
                      +1
                      По поводу вложенных прерываний

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

                      я не зря указал в статье, что мы работаем с HAL

                      А, этот нюанс не уловил (не работал с STM). Тогда просто пожелание — опубликовать статью на easyelectronics.ru — там и людей со знанием дело больше может откликнуться, может что-то ещё вылезет. Я бегло — только вышеизложенное заметил.

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

                  Самое читаемое