APM32F407Tiny开发板移植FreeModbus协议
本帖最后由 lemonhub 于 2024-11-21 15:54 编辑Modbus协议Modbus是一种串行通信协议,是Modicon公司(现施耐德电气)于1979年为使用可编程逻辑控制器 (PLC)通信而发表。Modbus是工业领域通信协议的业界标准,并且是工业电子设备之间 常用的连接方式。Modbus协议使用的是主从的通讯技术,即由主设备主动查询和操作从设备。一般将主控设备方所使 用的协议称为Modbus Master,从设备方所使用的协议称为Modbus Slave。典型的主设备包括工控 机和工业控制器等;典型的从设备如可编程逻辑控制器(PLC)等。 MODBUS是OSI模型第7层上的应用层报文传输协议,它在连接至不同类型总线或网络的设备之间提 供客户机/服务器通信。 Modbus通讯物理接口可以选用串口(包括RS232、RS485等),也可以选择以太网口等。MODBUS协议定义了一个与基础通信层无关的简单协议数据单元(PDU)。特定总线或网络上的 MODBUS协议映射能够在应用数据单元(ADU)上引入一些附加域。当服务器对客户机响应时,它使用功能码域来指示正常(无差错)响应或者出现某种差错(称为异常 响应)。需要管理超时,以便明确地等待可能不会出现的应答。Modbus功能码MODBUS有三类功能码:公共功能码、用户定义功能码、保留功能码。 公共功能码是较好地被定义的功能码,保证是唯一的、公开证明的,具有可用的一致性测试。Modbus数据模型MODBUS 数据模型有四种,通过不同的功能码来读写这些数据对象。Modbus主从请求Modbus 串行链路协议是一个主从协议。在同一时刻,只有一个主节点连接于总线,一个或多个子节 点(最大编号为247)连接于同一个串行总线。Modbus通信总是由主节点发起。子节点在没有收到 来自主节点的请求时,从不会发送数据。子节点之间从不会互相通信。主节点在同一时刻只会发起一 个Modbus事务处理。 主节点以两种模式对子节点发出Modbus 请求:单播模式单播模式,主节点以特定地址访问某个子节点,子节点接到并处理完请求后,子节点向主节点返 回一个报文(一个'应答')。每个子节点必须有唯一的地址 (1 到 247),这样才能区别于其它节点被 独立的寻址。广播模式 广播模式,主节点向所有的子节点发送请求。对于主节点广播的请求没有应答返回。广播请求一 般用于写命令。所有设备必须接受广播模式的写功能。地址 0 是专门用于表示广播数据的。Modbus传输模式 Modbus-RTU & Modbus-ASCIIModbus有两种串行传输模式被定义:RTU 模式(默认) 和 ASCII 模式FreeModbus协议移植FreeModbus是一个针对通用的Modbus协议栈在嵌入式系统中应用的实现。它提供了RTU/ASCII传 输模式及TCP协议支持。FreeModbus遵循BSD许可证,这意味着用户可以将FreeModbus应用于商 业环境中。目前FreeMODBUS只免费提供了一个Modbus从机节点的协议栈。该协议栈使用ANSI C 编写,并且支持多个变量。Freemodbus代码获取可在网络上面获取,不同版本可能有一定区别,需要仔细分辨,或者直接使用本工程版本的代码。freemodbus下载地址:https://github.com/cwalter-at/freemodbus本应用指南将介绍如何在APM32F407单片机上,通过FreeModbus协议栈实z现Modbus从机节点的主 要功能,并提供基于APM32F4xx_StdPeriphDriver和FreeModbus协议栈的源代码。如结合APM32F407Tiny Board和RS485转换模块搭建起基于RS485的Modbus从机节点。
移植步骤1. 准备基础工程模板
[*]下载最新版本BSP&PACK文件,按照其应用指南进行安装及配置,本文档及例程均基于APM32F4xx_StdPeriphDriver.x的BSP&PACK文件进行开发。可借用apm32f407tiny文件夹下的工程来进行修改,更改文件夹及工程名为freemodbus,并准备在该工程内添加FreeMODBUS 源码。(本文使用的是根据个人习惯的工程模板基础上移植reeMODBUS )
2.添加Freemodbus源码
Freemodbus源码在Middlewares中,代码文件主要包括ascii,rtu,tcp;在MDK中添加代码和头文件路径。因为移植的是RTU通讯,所以暂且不用管ASCII TCP两个文件夹中的内容。
源文件描述
modbus\mb.c给应用层提供Modbus 从机设置及轮询相关接口
modbus\functions\mbfunccoils.c从机线圈相关功能
modbus\functions\mbfuncdisc.c从机离散输入相关功能
modbus\functions\mbfuncholding.c从机保持寄存器相关功能
modbus\functions\mbfuncinput.c从机输入寄存器相关功能
modbus\functions\mbfuncother.c其余Modbus 功能
modbus\functions\mbutils.c一些协议栈中需要用到的工具函数
modbus\rtu\mbcrc.cCRC 校验功能
modbus\rtu\mbrtu.c从机RTU 模式设置及其状态机
“include”文件夹内为modbus源代码的头文件存放文件夹
3. 添加完成modbus源码文件,编写接口文件portmodbus_port重点完善串口和定时器代码。其中bsp_rs485.c和bsp_rs485.h,主要功能是RS485-EN使用接口配置。
4. 完善bsp_rs485#include "bsp_rs485.h"
#include "bsp_usart.h"
void RS485_1_Init(void)
{
GPIO_Config_T gpioConfigStruct;
RCM_EnableAHB1PeriphClock(RS485_1_GPIO_CLK);
gpioConfigStruct.pin = RS485_1_GPIO_PIN;
gpioConfigStruct.mode = GPIO_MODE_OUT;
gpioConfigStruct.speed = GPIO_SPEED_50MHz;
gpioConfigStruct.otype = GPIO_OTYPE_PP;
gpioConfigStruct.pupd = GPIO_PUPD_NOPULL;
GPIO_Config(RS485_1_GPIO_PORT, &gpioConfigStruct);
RS485_1_RX; //低电平接收 高电平发送
//board_usart3_init(115200);
}5. 完善portserial下面开始程序移植,首先是portserial.c文件,该文件是串口的接口文件,包含以下函数:串口使能、串口初始化、发送一个字节、接收一个字节等。我们需要自己实现完成这些函数的内容。完成后的内容如下:串口使用串口3,TX-PD8 RX-PD9 (由于PC10,PC11接板载按键接口,因此换其他的,也可以找其他的串口,不一定要串口3)#include "main.h"
#include "port.h"
/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"
/* ----------------------- static functions ---------------------------------*/
static void prvvUARTTxReadyISR(void);//发送中断处理
static void prvvUARTRxISR(void); //接收中断处理
/* ----------------------- Start implementation -----------------------------*/
void
vMBPortSerialEnable(BOOL xRxEnable, BOOL xTxEnable)//适配中断使能
{
/* If xRXEnable enable serial receive interrupts. If xTxENable enable
* transmitter empty interrupts.
*/
if (xRxEnable)
{
//使能串口接收中断
USART_EnableInterrupt(USART3, USART_INT_RXBNE);
//低电平接收 高电平发送
RS485_1_RX;
}
else
{
USART_DisableInterrupt(USART3, USART_INT_RXBNE);
}
if (xTxEnable)
{
//使能串口发送完成中断
USART_EnableInterrupt(USART3, USART_INT_TXC);
//低电平接收 高电平发送
RS485_1_TX;
}
else
{
USART_DisableInterrupt(USART3, USART_INT_TXC);
}
}
BOOL
xMBPortSerialInit(UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity)//适配串口初始化
{
/*RS485-GPIO-Configuratio*/
RS485_1_Init(); //RS485_1_RX; 低电平接收 高电平发送
/** USART3 configuration */
board_usart3_init(ulBaudRate);
(void)ucPORT; //不修改串口
(void)ucDataBits; //不修改数据位长度
(void)eParity; //不修改校验格式
return TRUE;
}
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. */
USART_TxData(USART3, 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 = USART_RxData(USART3);
return TRUE;
}
/* Create an interrupt handler for the transmit buffer empty interrupt
* (or an equivalent) for your target processor. This function should then
* call pxMBFrameCBTransmitterEmpty( ) which tells the protocol stack that
* a new character can be sent. The protocol stack will then call
* xMBPortSerialPutByte( ) to send the character.
*/
static void prvvUARTTxReadyISR(void)
{
pxMBFrameCBTransmitterEmpty();
}
/* Create an interrupt handler for the receive interrupt for your target
* processor. This function should then call pxMBFrameCBByteReceived( ). The
* protocol stack will then call xMBPortSerialGetByte( ) to retrieve the
* character.
*/
static void prvvUARTRxISR(void)
{
pxMBFrameCBByteReceived();
}
#ifdef Modbus_Slave
//串口中断服务函数
void USART3_IRQHandler(void)//适配中断服务函数
{
//发生接收中断
if (USART_ReadIntFlag(USART3, USART_INT_RXBNE) == SET)
{
//串口接收中断调用函数
prvvUARTRxISR();
//清除中断标志位
USART_ClearIntFlag(USART3, USART_INT_RXBNE);
}
//接收溢出中断
if (USART_ReadIntFlag(USART3, USART_INT_OVRE_ER) == SET)
{
//串口发送中断调用函数
prvvUARTRxISR();
//清除中断标志位
USART_ClearIntFlag(USART3, USART_INT_OVRE_ER);
}
//发生完成中断
if (USART_ReadIntFlag(USART3, USART_INT_TXC) == SET)
{
prvvUARTTxReadyISR();
//清除中断标志
USART_ClearIntFlag(USART3, USART_INT_TXC);
}
}
#endif
6. 完善porttimer然后是porttimer.c文件,该文件是定时器的接口文件。定时器的作用是用于通知modbus协议栈3.5个字符的空闲时间已经到达。我们需要实现定时器的初始化和中断相关函数,定时需要配置为50us计数一次,具体计数周期与波特率有关。完成后的内容如下:/* ----------------------- Platform includes --------------------------------*/
#include "port.h"
#include "main.h"
/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"
/* ----------------------- static functions ---------------------------------*/
static void prvvTIMERExpiredISR(void);
/* ----------------------- Start implementation -----------------------------*/
BOOL
xMBPortTimersInit(USHORT usTim1Timerout50us)//适配定时器初始化
{
TMR_BaseConfig_T TMR_BaseConfigStruct;
RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_TMR14); //开启定时器时钟
TMR_BaseConfigStruct.clockDivision = TMR_CLOCK_DIV_1;
TMR_BaseConfigStruct.countMode = TMR_COUNTER_MODE_UP; //向上计数模式
TMR_BaseConfigStruct.division = 8400-1;
TMR_BaseConfigStruct.period = usTim1Timerout50us;
TMR_BaseConfigStruct.repetitionCounter = 0;
TMR_ConfigTimeBase(TMR14, &TMR_BaseConfigStruct); //定时器初始化
TMR_EnableInterrupt(TMR14, TMR_INT_UPDATE);
NVIC_EnableIRQRequest(TMR8_TRG_COM_TMR14_IRQn, 0, 0);
TMR_Enable(TMR14); //使能中断
return TRUE;
}
void
vMBPortTimersEnable()
{
/* Enable the timer with the timeout passed to xMBPortTimersInit( ) */
TMR_ClearIntFlag(TMR14, TMR_INT_UPDATE);
TMR_EnableInterrupt(TMR14, TMR_INT_UPDATE);
TMR_ConfigCounter(TMR14, 0x0000);
TMR_Enable(TMR14);
}
void
vMBPortTimersDisable()
{
/* Disable any pending timers. */
TMR_ClearIntFlag(TMR14, TMR_INT_UPDATE);
TMR_DisableInterrupt(TMR14, TMR_INT_UPDATE);
TMR_ConfigCounter(TMR14, 0x0000);
TMR_Disable(TMR14);
}
/* 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();
}
#ifdef Modbus_Slave
void TMR8_TRG_COM_TMR14_IRQHandler(void)//适配定时器中断服务函数
{
if(TMR_ReadIntFlag(TMR14, TMR_INT_UPDATE) == SET)
{
prvvTIMERExpiredISR();
TMR_ClearIntFlag(TMR14, TMR_INT_UPDATE);
}
}
#endif
7. 编译代码,解决错误这里给了一个常见的错误,其他的错误可能移植过程中某些东西没有定义的情况,具体错误具体分析。8.补全输入寄存器操作函数、保持寄存器操作函数 APP_Modbusmodbus功能进行初始化,设置地址和波特率。这部分内容可以参考官方资料里的例程,也可以直接复制别人写好的#include "main.h"
/** public variables*/
//输入寄存器起始地址
#define REG_INPUT_START 0x0001
//输入寄存器数量
#define REG_INPUT_NREGS 16
//保持寄存器起始地址
#define REG_HOLDING_START 0x0001
//保持寄存器数量
#define REG_HOLDING_NREGS 16
//线圈起始地址
#define REG_COILS_START 0x0001
//线圈数量
#define REG_COILS_SIZE 16
//开关寄存器起始地址
#define REG_DISCRETE_START 0x0001
//开关寄存器数量
#define REG_DISCRETE_SIZE 16
//输入寄存器内容
uint16_t usRegInputBuf = {0x1000,0x1001,0x1002,0x1003,0x1004,0x1005,0x1006,0x1007};
//输入寄存器起始地址
uint16_t usRegInputStart = REG_INPUT_START;
//保持寄存器内容
uint16_t usRegHoldingBuf = {0, 11, 22, 33, 44, 55, 66, 77, 88, 99};
//保持寄存器起始地址
uint16_t usRegHoldingStart = REG_HOLDING_START;
//线圈状态
uint8_t ucRegCoilsBuf = {0xFF,0xEE};
//开关输入状态
uint8_t ucRegDiscreteBuf = {0x01,0x02};
/**
* @brief : 读输入寄存器处理函数,功能码04
* @parampucRegBuffer 数据缓存区 大端模式,高字节在前
* @paramusAddress 寄存器地址
* @paramusNRegs 读取个数
* @return?eMBErrorCode?
*/
eMBErrorCode
eMBRegInputCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs )
{
eMBErrorCode eStatus = MB_ENOERR;
int iRegIndex;
if( ( usAddress >= REG_INPUT_START )\
&& ( usAddress + usNRegs <= REG_INPUT_START + REG_INPUT_NREGS ) )
{
iRegIndex = ( int )( usAddress - usRegInputStart );
while( usNRegs > 0 )
{
*pucRegBuffer++ = ( UCHAR )( usRegInputBuf >> 8 );
*pucRegBuffer++ = ( UCHAR )( usRegInputBuf & 0xFF );
iRegIndex++;
usNRegs--;
}
}
else
{
eStatus = MB_ENOREG;
}
return eStatus;
}
/**
* @brief : 读保持寄存器处理函数,功能码03
* @parampucRegBuffer
* @paramusAddress
* @paramusNRegs
* @parameMode
* @return?eMBErrorCode?
*/
eMBErrorCode
eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode )
{
eMBErrorCode eStatus = MB_ENOERR;
int iRegIndex;
if((usAddress >= REG_HOLDING_START)&&\
((usAddress+usNRegs) <= (REG_HOLDING_START + REG_HOLDING_NREGS)))
{
iRegIndex = (int)(usAddress - usRegHoldingStart);
switch(eMode)
{
case MB_REG_READ://读 MB_REG_READ = 0
while(usNRegs > 0)
{
*pucRegBuffer++ = (u8)(usRegHoldingBuf >> 8);
*pucRegBuffer++ = (u8)(usRegHoldingBuf & 0xFF);
iRegIndex++;
usNRegs--;
}
break;
case MB_REG_WRITE://写 MB_REG_WRITE = 1
while(usNRegs > 0)
{
usRegHoldingBuf = *pucRegBuffer++ << 8;
usRegHoldingBuf |= *pucRegBuffer++;
iRegIndex++;
usNRegs--;
}
}
}
else//错误
{
eStatus = MB_ENOREG;
}
return eStatus;
}
/**
* @brief : 读线圈寄存器处理函数,功能码01
* @parampucRegBuffer
* @paramusAddress
* @paramusNCoils
* @parameMode
* @return?eMBErrorCode?
*/
eMBErrorCode
eMBRegCoilsCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNCoils,
eMBRegisterMode eMode )
{
//错误状态
eMBErrorCode eStatus = MB_ENOERR;
//寄存器个数
int16_t iNCoils = ( int16_t )usNCoils;
//寄存器偏移量
int16_t usBitOffset;
//检查寄存器是否在指定范围内
if( ( (int16_t)usAddress >= REG_COILS_START ) &&
( usAddress + usNCoils <= REG_COILS_START + REG_COILS_SIZE ) )
{
//计算寄存器偏移量
usBitOffset = ( int16_t )( usAddress - REG_COILS_START );
switch ( eMode )
{
//读操作
case MB_REG_READ:
while( iNCoils > 0 )
{
*pucRegBuffer++ = xMBUtilGetBits( ucRegCoilsBuf, usBitOffset,\
( uint8_t )( iNCoils > 8 ? 8 : iNCoils ) );
iNCoils -= 8;
usBitOffset += 8;
}
break;
//写操作
case MB_REG_WRITE:
while( iNCoils > 0 )
{
xMBUtilSetBits( ucRegCoilsBuf, usBitOffset,\
( uint8_t )( iNCoils > 8 ? 8 : iNCoils ),
*pucRegBuffer++ );
iNCoils -= 8;
}
break;
}
}
else
{
eStatus = MB_ENOREG;
}
return eStatus;
}
/**
* @brief : 读离散量寄存器处理函数,功能码02
* @parampucRegBuffer
* @paramusAddress
* @paramusNDiscrete
* @return?eMBErrorCode?
*/
eMBErrorCode
eMBRegDiscreteCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNDiscrete )
{
//错误状态
eMBErrorCode eStatus = MB_ENOERR;
//操作寄存器个数
int16_t iNDiscrete = ( int16_t )usNDiscrete;
//偏移量
uint16_t usBitOffset;
//判断寄存器时候再制定范围内
if( ( (int16_t)usAddress >= REG_DISCRETE_START ) &&
( usAddress + usNDiscrete <= REG_DISCRETE_START + REG_DISCRETE_SIZE ) )
{
//获得偏移量
usBitOffset = ( uint16_t )( usAddress - REG_DISCRETE_START );
while( iNDiscrete > 0 )
{
*pucRegBuffer++ = xMBUtilGetBits( ucRegDiscreteBuf, usBitOffset,
( uint8_t)( iNDiscrete > 8 ? 8 : iNDiscrete ) );
iNDiscrete -= 8;
usBitOffset += 8;
}
}
else
{
eStatus = MB_ENOREG;
}
return eStatus;
}9. 添加主函数测试代码#if USE_FREEMODBUS
static void modbus_slave_init(void)
{
eMBInit(MB_RTU, 0x01, 0x00, 9600, MB_PAR_NONE); //modbus串口配置 串口三 RTU模式 从机地址为1 USART3 9600 无校验
eMBEnable(); //modbus使能
}
// 主循环中调用eMBPoll();
#endif完整代码见仓库:https://gitee.com/End-ING/embedded-apm32-board10. 测试结果串口打印结果 从打印信息可以看到,从设备已经正常的运行起来。 这时我们需要将此设备与上位机相连接,再打开Modbus Poll软件,模拟主设备来进行单播通信,即 发送请求并接收应答。 修改一些东西,同时使用Modbus-Commix.exe也可以得到Modbus响应。<blockquote>//输入寄存器起始地址
04功能码 读输入寄存器
输入 01 04 00 00 00 08 F1 CC 返回 01 04 10 10 00 10 01 10 02 10 03 10 04 10 05 10 06 10 07 B6 0C一、输入内容解析
[*]设备地址或通信标识(第一个字节)
[*]“01”:通常用于指定此次通信所针对的目标设备或者作为一种通信事务的标识。在存在多个设备进行通信的场景下,它能确保数据准确地发送到对应的设备,并让接收设备知晓该数据是针对自己的。
[*]功能码(第二个字节)
[*]“04”:在常见的工业通信协议中,功能码 “04” 一般表示读取输入寄存器操作。结合后面的数据内容以及通常的通信流程来看,这里大概率是在请求读取某些输入寄存器的值。
[*]起始地址偏移量(第三、四个字节)
[*]“00 00”:这两个字节很可能表示起始地址偏移量。参照之前类似代码示例中有关于寄存器起始地址的定义方式(如 REG_INPUT_START 定义为 0x0001 等),这里的 “00 00” 可能意味着相对于输入寄存器的起始地址的偏移量为 0,即从输入寄存器的起始地址开始进行读取操作。
[*]要读取的寄存器数量(第五、六个字节)
[*]“00 08”:这两个字节通常表示要读取的寄存器数量。十六进制的 “00 08” 换算成十进制为 8,表示此次请求要读取 8 个输入寄存器的值。
[*]校验和或其他补充信息(第七个字节)
[*]“F1 CC”:在一些通信协议中,这部分可能是作为校验和或者其他补充信息存在。如果是校验和,一般是通过对前面所有字节(从功能码之前到这两个字节之前)按照特定算法(如 CRC 校验算法等)计算得出的值,用于验证数据在传输过程中是否出现错误。不过具体是否为校验和以及其确切含义还需要结合更详细的通信协议规范来确定。
二、输出内容解析
[*]设备地址或通信标识(第一个字节)
[*]“01”:与输入中的第一个字节相对应,用于标识这是对前面那个请求设备的回应,确保通信的对应性。
[*]功能码(第二个字节)
[*]“04”:和输入中的功能码一致,表示这是对读取输入寄存器请求的响应。
[*]后续数据字节数(第三个字节)
[*]“10”:这个字节表示后续数据的字节数。十六进制的 “10” 换算成十进制为 16,表示后面跟着 16 个字节的数据内容(不包括前面的 “01 04 10” 这 3 个字节)。
[*]实际读取到的寄存器值(第四至第十六个字节)
[*]“10 00 10 01 10 02 10 03 10 04 10 05 10 06 10 07”:这部分数据是实际读取到的 8 个输入寄存器的值。根据常见的通信协议中数据存储格式以及与之前类似代码示例中输入寄存器内容数组(如 uint16_t usRegInputBuf = {0x1000,0x1001,0x1002,0x1003,0x1004,0x1005,0x1006,0x1007} )的关联,这里应该就是从输入寄存器中读取出来的值。不过同样需要注意字节序问题,例如如果是大端序,那么 “10 00” 组合起来可能对应一个寄存器的值(比如对应代码示例中的 0x1000 ,但要根据实际的字节序来准确判断具体对应哪个值),依此类推对后续的字节组合进行解读。
[*]校验和或其他补充信息(第十七个字节)
[*]“ B6 0C ”:这部分和输入中的类似部分作用可能相同,在一些通信协议中,可能是作为校验和或者其他补充信息存在。如果是校验和,一般是通过对前面所有字节(从功能码之前到这两个字节之前)按照特定算法(如 CRC 校验算法等)计算得出的值,用于验证数据在传输过程中是否出现错误。不过具体是否为校验和以及其确切含义还需要结合更详细的通信协议规范来确定。
很详细的移植步骤,感谢分享
页:
[1]