С недавних пор я начал заниматься встраиваемыми системами и докатился до программирования микроконтроллеров, а именно 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)


  1. build_your_web
    21.03.2016 18:52

    Отличная статья! Очень наглядно и доступно.
    А можно ли применять STM32CubeMX для генерации кода для камней STM32 F0 и F1?


    1. noonv
      21.03.2016 19:05
      +1

      Можно.


    1. Lynnfield
      21.03.2016 22:42
      +1

      Да, смотри, на офф-сайте STM заявляют, что CubeMX работает с линейками от F0 до L4.


  1. proglyk
    21.03.2016 21:39
    +1

    Похоже каждый уважающий себя Программист написал свой вариант модбаса на стм32!)))


    1. Lynnfield
      21.03.2016 22:38

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


  1. skjame
    22.03.2016 08:56
    +1

    как по мне хуарт так называется неспроста. его использование во многих случаях чрезмерно. единственное что есть переносимость с f0 на другие кристаллы вплоть до л4. хотя я не вижу ничего страшного в правке имен пары регистров для настройки, а вот висеть столько в прерывание — не камильфо. имхо далеко не лушая реализация поставленной перед ТС задачи.


  1. d_ilyich
    22.03.2016 13:10

    Благодарю за статью. Мне как раз предстоит в ближайшем будущем реализовывать поддержку 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, то пакет целый, в противном случае — битый.

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