STM32 移植FreeModbus 详细过程
本帖最后由 xukai871105 于 2012-8-13 19:32 编辑FreeModbus移植 经验分享一 为什么要移植Freemodbus 为什么要移植Freemodbus,这个问题需要从两个方面来回答。第一,modbus是一个非常好的应用层协议,它很简洁也相对完善。对于还没有接触过modbus的朋友来说,我非常不建议直接移植freemodbus,应该耐心的从modbus文档入手,并充分把握身边的所有资源,例如PLC的中modbus部分。第二,其实嵌入式系统的通信协议可以自己制定,但是通过实践发现自己定制的协议漏洞百出,尤其是扩展极为困难。我始终认为借鉴他人的经验是很好的途径。借鉴他人成熟的代码,可以减少调试的时间,实现的功能也多了不少。 个人观点,仅供参考。 freemodbus小提示 freemodbus只能使用从机功能。freemodbus更适合嵌入式系统,虽然例子中也有WIN32的例子,如果想要做PC机程序并实现主机功能,推荐使用另一个modbus库——NMODBUS,使用C#开发。同样WINFORM也可以通过自己编写串口代码实现modbus功能,但是这会花费很长的时间,可能是一周也可能是一个月,如果使用现成的代码库,那么开发时间可能只有10分钟。
自己整理的modbus协议
代码参考了这个帖子,感谢你的分享。点击这里
二freeemodbus中如何通过串口发送和接收数据 freemodbus通过串口中断的方式接收和发送数据。采用这种做法我想可以节省程序等待的时间,并且也短充分使用CPU的资源。串口中断接收毋庸置疑,在中断服务函数中把数据保存在数组中,以便稍后处理。但是串口发送中断使用哪种形式?串口发送中断至少有两种方式,第一种,数据寄存器空中断,只要数据寄存器为空并且中断屏蔽位置位,那么中断就会发生;第二种,发送完成中断,若数据寄存器的数据发送完成并且中断屏蔽位置位,那么中断也会发送。我非常建议各位使用串口发送完成中断。freemodbus多使用RS485通信中,从机要么接收要么发送,多数情况下从机处于接收状态,要有数据发送时才进入发送状态。进入发送状态时,数据被一个一个字节发送出去,当最后一个字节被发送出去之后,从机再次进入接收状态。如果使用发送寄存器为空中断,还需要使用其他的方法才可以判断最后一个字节的数据是否发送完成。如果使用数据寄存器为空中断,那么将很有可能丢失最后一个字节。(马潮老师的AVR图书中也推荐使用发送完成中断,交流性质的文章,就没有参考文献了。)
二freemodbus中如何判断帧结束 大家应该清楚,modbus协议中没有明显的开始符和结束符,而是通过帧与帧之间的间隔时间来判断的。如果在指定的时间内,没有接收到新的字符数据,那么就认为收到了新的帧。接下来就可以处理数据了,首当其冲的就是判断帧的合法性。Modbus通过时间来判断帧是否接受完成,自然需要单片机中的定时器配合。
三 整体代码下面给出一个STM32平台上使用FREEMODBUS最简单的例子,操作保持寄存器,此时操作指令可以为03,06和16;
<FONT size=3>#include "stm32f10x.h"
#include <stdio.h>
#include "mb.h"
#include "mbutils.h"
//保持寄存器起始地址
#define REG_HOLDING_START 0x0000
//保持寄存器数量
#define REG_HOLDING_NREGS 8
//保持寄存器内容
uint16_t usRegHoldingBuf
= {0x147b,0x3f8e,0x147b,0x400e,0x1eb8,0x4055,0x147b,0x408e};
int main(void)
{
//初始化 RTU模式 从机地址为1 USART1 9600 无校验
eMBInit(MB_RTU, 0x01, 0x01, 9600, MB_PAR_NONE);
//启动FreeModbus
eMBEnable();
while (1)
{
//FreeMODBUS不断查询
eMBPoll();
}
}
/**
* @brief 保持寄存器处理函数,保持寄存器可读,可读可写
* @param pucRegBuffer 读操作时--返回数据指针,写操作时--输入数据指针
* usAddress 寄存器起始地址
* usNRegs 寄存器长度
* eMode 操作方式,读或者写
* @retval eStatus 寄存器状态
*/
eMBErrorCode
eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs,
eMBRegisterMode eMode )
{
//错误状态
eMBErrorCode eStatus = MB_ENOERR;
//偏移量
int16_t iRegIndex;
//判断寄存器是不是在范围内
if( ( (int16_t)usAddress >= REG_HOLDING_START ) \
&& ( usAddress + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS ) )
{
//计算偏移量
iRegIndex = ( int16_t )( usAddress - REG_HOLDING_START);
switch ( eMode )
{
//读处理函数
case MB_REG_READ:
while( usNRegs > 0 )
{
*pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf >> 8 );
*pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf & 0xFF );
iRegIndex++;
usNRegs--;
}
break;
//写处理函数
case MB_REG_WRITE:
while( usNRegs > 0 )
{
usRegHoldingBuf = *pucRegBuffer++ << 8;
usRegHoldingBuf |= *pucRegBuffer++;
iRegIndex++;
usNRegs--;
}
break;
}
}
else
{
//返回错误状态
eStatus = MB_ENOREG;
}
return eStatus;
}
</FONT>先给大家一个整体的印象,先让大家会使用FREEMODBUS,再详细描述细节//保持寄存器起始地址#define REG_HOLDING_START 0x0000//保持寄存器数量#define REG_HOLDING_NREGS 8这两个宏定义,决定了保持寄存器的起始地址和总个数。需要强调的是,modbus寄存器的地址有两套规则,一套称为PLC地址,为5位十进制数,例如40001。另一套是协议地址,PLC地址40001意味着该参数类型为保持寄存器,协议地址为0x0000,这里面有对应关系,去掉PLC地址的最高位,然后剩下的减1即可。这会存在一个问题,PLC地址30002和PLC地址40002的协议地址同为0x0001,此时访问时是不是会冲突呢。亲们,当然不会了,30001为输入寄存器,需要使用04指令访问,而40001为保持寄存器,可以使用03、06和16指令访问。所以,用好modbus还是要熟悉协议本生,切不可着急。//保持寄存器内容uint16_t usRegHoldingBuf= {0x147b,0x3f8e,0x147b,0x400e,0x1eb8,0x4055,0x147b,0x408e};接下来定义了保持寄存器的内容,在这里请大家注意了,保持寄存器为无符号16位数据。在测试的情况下,我随便找了一些数据进行测试。看数据的本质似乎看不出说明规律,但是usRegHoldingBuf却是以16进制保存了浮点数。
int main(void)
{
//初始化 RTU模式 从机地址为1 USART1 9600 无校验
eMBInit(MB_RTU, 0x01, 0x01, 9600, MB_PAR_NONE);
//启动FreeModbus
eMBEnable();
while (1)
{
//FreeMODBUS不断查询
eMBPoll();
}
}
接下来就进入主函数部分。有三个FREEMODBUS提供的函数,eMBInit,eMBEnable和eMBPoll。eMBInit为modbus的初始化函数,eMBEnable为modbus的使能函数,而eMBPoll为modbus的查询函数,eMBPoll也是非常单纯的函数,查询是否有数据帧到达,如果有数据到达,便进行相依的处理。再次观察这几个函数,只有eMBInit有很多的参数,这些参数和位于系统底层的硬件有关,这个应该引起移植过程的更多关注。下面几个章节再议。
<FONT size=3>eMBErrorCode
eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs,
eMBRegisterMode eMode )
{
//错误状态
eMBErrorCode eStatus = MB_ENOERR;
//偏移量
int16_t iRegIndex;
//判断寄存器是不是在范围内
if( ( (int16_t)usAddress >= REG_HOLDING_START ) \
&& ( usAddress + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS ) )
{
//计算偏移量
iRegIndex = ( int16_t )( usAddress - REG_HOLDING_START);
switch ( eMode )
{
//读处理函数
case MB_REG_READ:
while( usNRegs > 0 )
{
*pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf >> 8 );
*pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf & 0xFF );
iRegIndex++;
usNRegs--;
}
break;
//写处理函数
case MB_REG_WRITE:
while( usNRegs > 0 )
{
usRegHoldingBuf = *pucRegBuffer++ << 8;
usRegHoldingBuf |= *pucRegBuffer++;
iRegIndex++;
usNRegs--;
}
break;
}
}
else
{
//返回错误状态
eStatus = MB_ENOREG;
}
return eStatus;
}
</FONT>最后,如果收到一个有效的数据帧,那么就可以开始处理了。第一步,判断寄存器的地址是否在合法的范围内。if( ( (int16_t)usAddress >= REG_HOLDING_START ) \ && ( usAddress + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS ) )第二步,判断需要操作寄存器的偏移地址。 给个例子可以迅速的说明问题,例如访问寄存器的起始地址为0x0002,保持寄存器的起始地址为0x0000,那么这个访问的偏移量为2,程序就从保持寄存器数组的第2个(从0开始)开始操作。第三步,读写操作分开处理 case MB_REG_READ: while( usNRegs > 0 ) { *pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf >> 8 ); *pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf & 0xFF ); iRegIndex++; usNRegs--; } break; 以读操作为例,代码不多说了,请大家注意操作的顺序。保持寄存器以16位形式保存,但是modbus通信时以字节为单位,高位字节数据在前,低位数据字节在后。四 串口相关部分代码编写 串口部分的代码编写比较常规,主要有三个函数,串口初始化,串口数据发送和串口数据接收。除了以上三个函数之外,还有串口中断服务函数。/**
* @brief 串口初始化
* @param ucPORT 串口号
* ulBaudRate 波特率
* ucDataBits 数据位
* eParity 校验位
* @retval None
*/
BOOL
xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{
(void)ucPORT; //不修改串口
(void)ucDataBits; //不修改数据位长度
(void)eParity; //不修改校验格式
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
//使能USART1,GPIOA
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |
RCC_APB2Periph_USART1, ENABLE);
//GPIOA9 USART1_Tx
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);
//GPIOA.10 USART1_Rx
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮动输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = ulBaudRate; //只修改波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
//串口初始化
USART_Init(USART1, &USART_InitStructure);
//使能USART1
USART_Cmd(USART1, ENABLE);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
//设定USART1 中断优先级
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
//最后配置485发送和接收模式
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);
//GPIOD.8
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOD, &GPIO_InitStructure);
return TRUE;
}
传入的参数有端口号,波特率,数据位和校验位,可以根据实际的情况修改代码。在这里我并没有修改其他参数,至于传入的波特率是有效的。除了配置串口的相关参数之外,还需要配置串口的中断优先级。最后,由于使用485模式,还需要一个发送接收控制端,该IO配置为推挽输出模式。
<FONT size=3>/**
* @brief 控制接收和发送状态
* @param xRxEnable 接收使能、
* xTxEnable 发送使能
* @retval None
*/
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
if(xRxEnable)
{
//使能接收和接收中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
//MAX485操作 低电平为接收模式
GPIO_ResetBits(GPIOD,GPIO_Pin_8);
}
else
{
USART_ITConfig(USART1, USART_IT_RXNE, DISABLE);
//MAX485操作 高电平为发送模式
GPIO_SetBits(GPIOD,GPIO_Pin_8);
}
if(xTxEnable)
{
//使能发送完成中断
USART_ITConfig(USART1, USART_IT_TC, ENABLE);
}
else
{
//禁止发送完成中断
USART_ITConfig(USART1, USART_IT_TC, DISABLE);
}
}
</FONT>由于485使用半双工模式,从机一般处于接收状态,有数据发送时才会进入发送模式。在FreeModbus中有专门的控制接收和发送状态的函数,在这里不但可以打开或关闭接收和发送中断,还可以控制485收发芯片的发送接收端口。代码非常简单,但是还是建议各位使用发送完成中断。
<FONT size=3>BOOL
xMBPortSerialPutByte( CHAR ucByte )
{
//发送数据
USART_SendData(USART1, ucByte);
return TRUE;
}
BOOL
xMBPortSerialGetByte( CHAR * pucByte )
{
//接收数据
*pucByte = USART_ReceiveData(USART1);
return TRUE;
}
xMBPortSerialPutByte和xMBPortSerialGetByte两个函数用于串口发送和接收数据,在这里只要调用STM32的库函数即可。
static void prvvUARTTxReadyISR( void )
{
//mb.c eMBInit函数中
//pxMBFrameCBTransmitterEmpty = xMBRTUTransmitFSM
//发送状态机
pxMBFrameCBTransmitterEmpty();
}
static void prvvUARTRxISR( void )
{
//mb.c eMBInit函数中
//pxMBFrameCBByteReceived = xMBRTUReceiveFSM
//接收状态机
pxMBFrameCBByteReceived();
}
void USART1_IRQHandler(void)
{
//发生接收中断
if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
prvvUARTRxISR();
//清除中断标志位
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
//发生完成中断
if(USART_GetITStatus(USART1, USART_IT_TC) == SET)
{
prvvUARTTxReadyISR();
//清除中断标志
USART_ClearITPendingBit(USART1, USART_IT_TC);
}
}
</FONT>若进入串口中断服务函数,则要调用FreeModbus中响应的函数,串口接收中断服务函数对应prvvUARTRxISR(),其代码如下
<FONT size=3>static void prvvUARTRxISR( void )
{
//mb.c eMBInit函数中
//pxMBFrameCBByteReceived = xMBRTUReceiveFSM
//接收状态机
pxMBFrameCBByteReceived();
}
</FONT>在prvvUARTRxISR中又调用了pxMBFrameCBByteReceived(),其实pxMBFrameCBTransmitterEmpty()并不是一个函数,而是一个函数指针。其定义如下,请注意函数指针的声明和函数声明的区别。BOOL( *pxMBFrameCBTransmitterEmpty ) ( void );在mb.c文件的eMBInit函数完成赋值。一般情况下都会选择RTU模式,那么pxMBFrameCBByteReceived就和xMBRTUReceiveFSM等价了,pxMBFrameCBByteReceived = xMBRTUReceiveFSM;
同理,若发生串口发送完成中断,该中断服务函数对应prvvUARTTxReadyISR,其代码如下
<FONT size=3>static void prvvUARTTxReadyISR( void )
{
//mb.c eMBInit函数中
//pxMBFrameCBTransmitterEmpty = xMBRTUTransmitFSM
//发送状态机
pxMBFrameCBTransmitterEmpty();
}
</FONT>在prvvUARTTxReadyISR中又调用了pxMBFrameCBTransmitterEmpty(),pxMBFrameCBTransmitterEmpty也是函数指针,在eMBInit函数完成赋值,它等价于xMBRTUTransmitFSM。 特别提醒,由于我使用的是串口发送完成中断,想要进入该中断服务函数,需要发送一个字节的数据并启动串口发送中断,代码还需要少许修改。在mbRTU.c的eMBRTUSend中稍作修改,代码如下。
<P style="MARGIN: 0cm 0cm 0pt" class=MsoNormal> </P>/* 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 = ucSlaveAddress;
usSndBufferCount += usLength;
/* Calculate CRC16 checksum for Modbus-Serial-Line-PDU. */
usCRC16 = usMBCRC16( ( UCHAR * ) pucSndBufferCur, usSndBufferCount );
ucRTUBuf = ( UCHAR )( usCRC16 & 0xFF );
ucRTUBuf = ( UCHAR )( usCRC16 >> 8 );
/* Activate the transmitter. */
//发送状态转换,在中断中不断发送
eSndState = STATE_TX_XMIT;
//插入代码 启动第一次发送,这样才可以进入发送完成中断
xMBPortSerialPutByte( ( CHAR )*pucSndBufferCur );
pucSndBufferCur++;
usSndBufferCount--;
//使能发送状态,禁止接收状态
vMBPortSerialEnable( FALSE, TRUE );
写到这里给位可能看的不是很明白,建议研究一下FreeModbus的源码,稍作一些修改使用起来才会更加方便。
本帖最后由 xukai871105 于 2012-8-21 11:22 编辑
工程代码,IAR 5.5V3.4库函数
请配合MODBUS POLL工具使用,会有更好的效果!
本帖最后由 xukai871105 于 2012-8-12 20:03 编辑
五 定时器相关部分代码编写
定时器的作用前面已经说明了,在这里就罗列一下相关的移植代码。定时器的代码要比串口的代码简单一些。static void prvvTIMERExpiredISR( void );
BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us )
{
TIM_TimeBaseInitTypeDefTIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
//
uint16_t PrescalerValue = 0;
//使能定时器4时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
//定时器时间基配置说明
//HCLK为72MHz,APB1经过2分频为36MHz
//TIM4的时钟倍频后为72MHz(硬件自动倍频,达到最大)
//TIM4的分频系数为3599,时间基频率为72 / (1 + Prescaler) = 20KHz,基准为50us
//TIM最大计数值为usTim1Timerout50u
PrescalerValue = (uint16_t) (SystemCoreClock / 20000) - 1;
//定时器1初始化
TIM_TimeBaseStructure.TIM_Period = (uint16_t) usTim1Timerout50us;
TIM_TimeBaseStructure.TIM_Prescaler = PrescalerValue;
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);
//预装载使能
TIM_ARRPreloadConfig(TIM4, ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
//定时器4中断优先级
NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
//清除溢出中断标志位
TIM_ClearITPendingBit(TIM4,TIM_IT_Update);
//定时器4溢出中断关闭
TIM_ITConfig(TIM4, TIM_IT_Update, DISABLE);
//定时器4禁能
TIM_Cmd(TIM4,DISABLE);
return TRUE;
}
void
vMBPortTimersEnable( )
{
TIM_ClearITPendingBit(TIM4, TIM_IT_Update);
TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE);
//设定定时器4的初始值
TIM_SetCounter(TIM4,0x0000);
//定时器4启动
TIM_Cmd(TIM4, ENABLE);
}
void
vMBPortTimersDisable()
{
TIM_ClearITPendingBit(TIM4, TIM_IT_Update);
TIM_ITConfig(TIM4, TIM_IT_Update, DISABLE);
TIM_SetCounter(TIM4,0x0000);
//关闭定时器4
TIM_Cmd(TIM4, DISABLE);
}
static void prvvTIMERExpiredISR( void )
{
( void )pxMBPortCBTimerExpired();
}
void TIM4_IRQHandler(void)
{
if (TIM_GetITStatus(TIM4, TIM_IT_Update) != RESET)
{
//清除定时器T4溢出中断标志位
TIM_ClearITPendingBit(TIM4, TIM_IT_Update);
prvvTIMERExpiredISR( );
}
}
在这里请注意STM32的TIM有一个自动倍频的功能,如果APB1被分频的话,TIM的时钟就会自动倍频,当然再如何倍频也不应超过72MHz。 六 各种寄存器的读或写函数 Modbus通信中,总共有四类的寄存器,开关输入寄存器,线圈寄存器,保持寄存器和输入寄存器。
/**
* @brief 输入寄存器处理函数,输入寄存器可读,但不可写。
* @param pucRegBuffer 返回数据指针
* usAddress 寄存器起始地址
* usNRegs 寄存器长度
* @retval eStatus 寄存器状态
*/
eMBErrorCode
eMBRegInputCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs )
{
eMBErrorCode eStatus = MB_ENOERR;
int16_t iRegIndex;
//查询是否在寄存器范围内
//为了避免警告,修改为有符号整数
if( ( (int16_t)usAddress >= REG_INPUT_START ) \
&& ( usAddress + usNRegs <= REG_INPUT_START + REG_INPUT_NREGS ) )
{
//获得操作偏移量,本次操作起始地址-输入寄存器的初始地址
iRegIndex = ( int16_t )( usAddress - REG_INPUT_START );
//逐个赋值
while( usNRegs > 0 )
{
//赋值高字节
*pucRegBuffer++ = ( uint8_t )( usRegInputBuf >> 8 );
//赋值低字节
*pucRegBuffer++ = ( uint8_t )( usRegInputBuf & 0xFF );
//偏移量增加
iRegIndex++;
//被操作寄存器数量递减
usNRegs--;
}
}
else
{
//返回错误状态,无寄存器
eStatus = MB_ENOREG;
}
return eStatus;
}
/**
* @brief 保持寄存器处理函数,保持寄存器可读,可读可写
* @param pucRegBuffer 读操作时--返回数据指针,写操作时--输入数据指针
* usAddress 寄存器起始地址
* usNRegs 寄存器长度
* eMode 操作方式,读或者写
* @retval eStatus 寄存器状态
*/
eMBErrorCode
eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs,
eMBRegisterMode eMode )
{
//错误状态
eMBErrorCode eStatus = MB_ENOERR;
//偏移量
int16_t iRegIndex;
//判断寄存器是不是在范围内
if( ( (int16_t)usAddress >= REG_HOLDING_START ) \
&& ( usAddress + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS ) )
{
//计算偏移量
iRegIndex = ( int16_t )( usAddress - REG_HOLDING_START );
switch ( eMode )
{
//读处理函数
case MB_REG_READ:
while( usNRegs > 0 )
{
*pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf >> 8 );
*pucRegBuffer++ = ( uint8_t )( usRegHoldingBuf & 0xFF );
iRegIndex++;
usNRegs--;
}
break;
//写处理函数
case MB_REG_WRITE:
while( usNRegs > 0 )
{
usRegHoldingBuf = *pucRegBuffer++ << 8;
usRegHoldingBuf |= *pucRegBuffer++;
iRegIndex++;
usNRegs--;
}
break;
}
}
else
{
//返回错误状态
eStatus = MB_ENOREG;
}
return eStatus;
}
/**
* @brief 线圈寄存器处理函数,线圈寄存器可读,可读可写
* @param pucRegBuffer 读操作---返回数据指针,写操作--返回数据指针
* usAddress 寄存器起始地址
* usNRegs 寄存器长度
* eMode 操作方式,读或者写
* @retval eStatus 寄存器状态
*/
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 开关输入寄存器处理函数,开关输入寄存器,可读
* @param pucRegBuffer 读操作---返回数据指针,写操作--返回数据指针
* usAddress 寄存器起始地址
* usNRegs 寄存器长度
* eMode 操作方式,读或者写
* @retval eStatus 寄存器状态
*/
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;
}
{:smile:}好东西啊.........................................
精神可佳! 这个建议穿裤啊。 哇塞原来是新鲜出炉的帖子,做了几个产品,都是自己定的协议,感觉很乱,想来个一统江湖的办法,就想到了modbus 认真的看完了,楼主继续写啊!
还有发现有句话不对:在prvvUARTRxISR中又调用了pxMBFrameCBTransmitterEmpty() xuxi2009 发表于 2012-8-13 00:04 static/image/common/back.gif
认真的看完了,楼主继续写啊!
还有发现有句话不对:在prvvUARTRxISR中又调用了pxMBFrameCBTransmitterEmpt ...
多谢你的提醒,我已经修正过来了!
还有一些细节的东西,抓紧这几天写出来!
NMODBUS的资料我也汇总一下,有了NMODBUS做上位机,有了FreeModbus做从机
这样使用MODBUS就天衣无缝了!安全可靠,稳定! 好像官方有移植好的例子,不过这个有中文注释,支持! wallelectronics 发表于 2012-8-13 19:45 static/image/common/back.gif
好像官方有移植好的例子,不过这个有中文注释,支持!
呵呵,还是理解一个编程的思路!理解为什么要使用MODBUS而不推荐自己编写协议! xukai871105 发表于 2012-8-12 20:00 static/image/common/back.gif
五 定时器相关部分代码编写
定时器的作用前面已经说明了,在这里就罗列一下相关的移植代码。定时器 ...
楼主,不知你是否测试了下本代码的稳定性呢?我在使用串口发送完成进中断的时候,长期测试,会出现丢字节的现象,有时候第一个字节数据会丢失。在串口接收到数据,启动串口发送,我是在使能串口发送的时候进行的处理。 好文呀,这个要顶。
不过我认为最好是带个RTOS,将FreeModbus作为一个任务来运行,同时再运行其它任务,以充分发挥STM32的能力。 yaodongliang 发表于 2012-8-14 00:02 static/image/common/back.gif
楼主,不知你是否测试了下本代码的稳定性呢?我在使用串口发送完成进中断的时候,长期测试,会出现丢字节 ...
我是经过认真的测试的,没有问题我才敢放在里面!
今天我再认真的测试一下,他测试结果贴出来! yzhu 发表于 2012-8-14 00:12 static/image/common/back.gif
好文呀,这个要顶。
不过我认为最好是带个RTOS,将FreeModbus作为一个任务来运行,同时再运行其它任务,以 ...
多谢你的建议,我抓紧时间试试。
官方给的是FreeRTOS的例子,我不是很熟悉,我试试uCOS
有结果和大家分享!
好东西!~ 好帖,一定要顶。。 yaodongliang 发表于 2012-8-14 00:02 static/image/common/back.gif
楼主,不知你是否测试了下本代码的稳定性呢?我在使用串口发送完成进中断的时候,长期测试,会出现丢字节 ...
测试了10000多次,没有出现问题!
测试过程:
使用03指令访问8个保持寄存器,每1s发送一次查询命令!
至少可以证明,使用发送完成中断时可行的!
电脑端的调试工具是不是就是Modbus poll这个好用? 好啊顶起,精华,保存下来,学习学习 果断收藏啊,现在也在定协议,头疼得一踏 学习中!!! xuxi2009 发表于 2012-8-14 17:19 static/image/common/back.gif
电脑端的调试工具是不是就是Modbus poll这个好用?
非常好用!
关于10楼的问题,我移植的时候也出现过,具体解决办法参照下面程序 在430上移植的!
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
ENTER_CRITICAL_SECTION();
if( xTxEnable )
{
RS485_EN_TX();
//2xx
UC1IFG |= UCA1TXIFG; //start
UC1IE |= UCA1TXIE;// 开发送中断 USCI_A1 RX interrupt
}
else
{
//2xx
UC1IE &= ~UCA1TXIE; //stop
__delay_cycles(3200);//delay200us @ 115200
RS485_EN_RX();
}
if( xRxEnable )
{
RS485_EN_RX();
//2xx
UC1IE |= UCA1RXIE; // 开中断 USCI_A1 RX interrupt
}
else
{
// RS485_EN_RX();
//UCA0IE &= ~UCRXIE;//IE1 &= ~URXIE0;
//2xx
UC1IE &= ~UCA1RXIE; // 关中断 USCI_A1 RX interrupt
}
EXIT_CRITICAL_SECTION();
}
好资料,先收藏了!! 好资料,先收藏了!! 最近自己也在不断的测试中,过几天还是要把工程代码贴上去! mark {:biggrin:} 非常不错,顶 mark 好东西 好东西!一直想用STM32和工业触摸屏通讯!不知道这个代码是否可行! hengta 发表于 2012-8-17 00:01 static/image/common/back.gif
好东西!一直想用STM32和工业触摸屏通讯!不知道这个代码是否可行!
我就是使用freemodbus和屏通(一家台湾的触摸屏公司)的触摸屏通信的!
其他的屏,例如西门子,欧姆龙的也可以的! {:handshake:},可以啊,顶,不知道楼主用的STM32是哪个型号?还有请问楼主,是不是用了外部晶振? 很好~~~~~~~~~~~~~~! liuyingqing139 发表于 2012-8-18 10:27 static/image/common/back.gif
,可以啊,顶,不知道楼主用的STM32是哪个型号?还有请问楼主,是不是用了外部晶振? ...
STM32F103系列 8M外部晶振 楼主太无私了,以前看过楼主的fatfs文件系统,太给力了。 请问,8楼提到的官方的例子,我刚去官网扫荡了一番,没发现,是我找错地方了吗?呵呵{:loveliness:} wangpeng_521 发表于 2012-8-18 16:50 http://www.amobbs.com/static/image/common/back.gif
请问,8楼提到的官方的例子,我刚去官网扫荡了一番,没发现,是我找错地方了吗?呵呵 ...
的确是没有STM32的例子,但是可以从BARE文件中的例子演化出STM32的应用
好的,谢谢! 写的非常详细,顶起 哈哈 好贴啊 已收藏和 举报 置酷 本帖最后由 xukai871105 于 2012-8-18 22:24 编辑
Name_006 发表于 2012-8-18 22:20 static/image/common/back.gif
哈哈 好贴啊 已收藏和 举报 置酷 ...
多谢啊,混了那么久还没有一个帖子穿上裤子!
感觉穿上裤子的难度和写SCI论文难度差不多啊!
我个人认为还差一个工程文件!
明天必当上传! 步骤很详细,好资料,顶 好东西,mark{:victory:}{:victory:} 收藏了,好帖子稍后研究 cool ! 研究下,谢谢分享,,, 写的很好,非常感谢 绝世好帖~!
必须收藏啊 看看,酷帖 好文章得表起 这一个是非常的不错 好,不错 顶一个!必须的! 感谢楼主,之前也做过FreeModbus的移植,但是弄的马马虎虎,有机会重新来过。 好............... 灰常不错的帖子!好好学习了! 先学习下modbus再来看看 非常不错喜欢 好东西,必须顶。谢谢楼主分享。 收藏,准备研究 要顶{:victory:} Mark ,收下! 不错,不错,看看 学习!学习......... 顶顶顶
好东西,好风格
哇正需要这个 好贴,值得用心学习 好帖 mark xukai871105 发表于 2012-8-14 12:17 static/image/common/back.gif
测试了10000多次,没有出现问题!
测试过程:
楼主,我试了一下,怎么全是错误;
MODBUS POLL需要怎么设置?
看了一下,实际接收到的数据,全是0X01;应该是PC发到板子的
yaodongliang 发表于 2012-8-14 00:02 static/image/common/back.gif
楼主,不知你是否测试了下本代码的稳定性呢?我在使用串口发送完成进中断的时候,长期测试,会出现丢字节 ...
一般是硬件问题
和端口方向问题 abnerle 发表于 2012-9-7 16:42 static/image/common/back.gif
楼主,我试了一下,怎么全是错误;
MODBUS POLL需要怎么设置?
看了一下,实际接收到的数据,全是0X01; ...
请问从机地址设对了吗,还有串口 和串口通信参数等! 本帖最后由 abnerle 于 2012-9-7 17:34 编辑
xukai871105 发表于 2012-9-7 17:03 static/image/common/back.gif
请问从机地址设对了吗,还有串口 和串口通信参数等!
串口 和串口通信参数等,这个是没有问题,电脑串口软件可以正常接收板子的输出数据,
板子也可以收到电脑发的数据;
从机地址,从机地址为1,就是默认的那个,
板子软件也是1 学习!谢谢! xcodes 发表于 2012-9-7 16:49 static/image/common/back.gif
一般是硬件问题
和端口方向问题
端口方向问题指的是?我的电路没有,方向选择,硬件上做了处理,和串口收发一样,这个电路,是成熟的电路,产品都生产几十万了 yaodongliang 发表于 2012-9-8 07:53 static/image/common/back.gif
端口方向问题指的是?我的电路没有,方向选择,硬件上做了处理,和串口收发一样,这个电路,是成熟的电路 ...
有的单片机 有端口方向寄存器 这个必须mark! 最近要在STM32上做MODBUS,突然发现此贴,果断先顶后看! xukai871105 发表于 2012-9-7 17:03 static/image/common/back.gif
请问从机地址设对了吗,还有串口 和串口通信参数等!
你好
我参考了你的代码移植到STM32上,运行正常,读出的线圈状态也是对的,但是我发现从Feemodbus网站上下载的v1.5的代码替代了你提供的相关modbus代码部分后就无法正常访问了
稍微看了一下,你好像修改了协议栈的代码,能告知修改了什么内容吗 xukai871105 发表于 2012-9-7 17:03 static/image/common/back.gif
请问从机地址设对了吗,还有串口 和串口通信参数等!
刚看了代码,你将functions文件夹下的功能函数里面的usRegAddress++;都注释掉了,这是为何呢?
Flyback 发表于 2012-9-12 15:00 static/image/common/back.gif
刚看了代码,你将functions文件夹下的功能函数里面的usRegAddress++;都注释掉了,这是为何呢?
...
你看的还是非常仔细的1我也是不断测试得到的结果!
我些的文档里面说了,modbus的地址有两种,一种是PLC的,一种是通信时用到的,两者有一个加1减1的关系。
我想freemodbus也可能纠结此处。所以有了后面的加1.
刚开始调试的时候,若定义一个数组,但是怎么都访问不到元素0,不信你可以试试。
然后我发现了代码中地址被加1,我觉得应该要去掉这句代码。
谢谢楼主,好东西 xukai871105 发表于 2012-9-12 17:10 static/image/common/back.gif
你看的还是非常仔细的1我也是不断测试得到的结果!
我些的文档里面说了,modbus的地址有两种,一种是PLC ...
这就是我纠结的地方,网上没人提到这个事情
我觉得还是不修改协议栈的好,保留那个+1,在回调函数里减掉更有通用性
估计上面说访问没反应的也有我这原因吧 xukai871105 发表于 2012-9-7 17:03 static/image/common/back.gif
请问从机地址设对了吗,还有串口 和串口通信参数等!
今天写了寄存器部分,poll通信正常,用的232
下午改成485 立刻不行了,看了下,电脑这边接收总少最后一个字节
感觉485使能提前一个字节被关掉了……
暂时还没找到原因,有遇到过这种状况吗 xukai871105 发表于 2012-9-7 17:03 static/image/common/back.gif
请问从机地址设对了吗,还有串口 和串口通信参数等!
示波器测了一下,485使能的确在发完倒数第二个字节的时候关了,导致最后一个字节发送不出去 Flyback 发表于 2012-9-13 14:34 static/image/common/back.gif
今天写了寄存器部分,poll通信正常,用的232
下午改成485 立刻不行了,看了下,电脑这边接收总少最后一 ...
这个问题我也是遇到过的,就是使用发送完成中断还是使用寄存器孔中断的问题!
请问你使用的是哪一种?
因为RS485有控制引脚,一定要发送完毕才可以返回接收状态! xukai871105 发表于 2012-9-13 14:59 static/image/common/back.gif
这个问题我也是遇到过的,就是使用发送完成中断还是使用寄存器孔中断的问题!
请问你使用的是哪一种?
看了你的代码,使用的是USART_IT_TC
我用的是USART_IT_TXE,改成和你一样的方式后,就不发送了 xukai871105 发表于 2012-9-13 14:59 static/image/common/back.gif
这个问题我也是遇到过的,就是使用发送完成中断还是使用寄存器孔中断的问题!
请问你使用的是哪一种?
STM32用的还不是很顺,现在代码改成和你上传的一样的方式,使用发送完成中断TC
但是只能响应一次 ,通信成功一次后就无法进入发送中断了
free MODBUS 很好,很强大 非常好,非常好。
佩服楼主的开源精神。
另外请教下,Modbus 还有个 TCP/IP 的,不晓得楼主有使用过么? 本帖最后由 Flyback 于 2012-9-14 12:17 编辑
xukai871105 发表于 2012-9-13 14:59 static/image/common/back.gif
这个问题我也是遇到过的,就是使用发送完成中断还是使用寄存器孔中断的问题!
请问你使用的是哪一种?
大神,你能告诉我你到底改了多少协议的程序吗
我上面的问题不修改协议的确是搞不定的
你在eMBRTUSend函数里加了下面代码,就是为了触发发送完成中断
xMBPortSerialPutByte( ( CHAR )*pucSndBufferCur );
pucSndBufferCur++;/* next byte in sendbuffer. */
usSndBufferCount--;
我把这段代码放到TX使能函数里了,但是也需要改源码
static volatile UCHAR *pucSndBufferCur;
static volatile USHORT usSndBufferCount;
上面这两个定义里的static 要去掉……
希望这点经验能帮上正在移植到stm32的同行一点小忙吧
PS:修改错别字 freemodbus确实代码写的很简洁好用,就是可惜只支持一个从机,也不支持主机。自己用了最笨的方法,把相关功能函数几乎复制了一遍,才让自己移植的freemodbu支持了两个从机。
这里想请教下楼主,支持多从机有啥好修改的建议么? whhityang 发表于 2012-9-14 11:39 static/image/common/back.gif
freemodbus确实代码写的很简洁好用,就是可惜只支持一个从机,也不支持主机。自己用了最笨的方法,把相关功 ...
我觉得可以试一下C++,面向对象可以省去不少代码!
我只是设想啊,我自己也遇到freemodus只支持一个串口的问题! Flyback 发表于 2012-9-14 11:31 static/image/common/back.gif
大神,你能告诉我你到底改了多少协议的程序吗
我上面的问题不修改协议的确是搞不定的
对了,为了适应发送完成中断,在发送的代码部分我也做了相应的修改!
主要是手动启动一次发送!之后,把发送的数据放入队列中。发送中断服务函数中,再逐个发送,直到完全发完毕! 好文,真心赞一个! 很好,正好好好学习一下! 感谢你们的支持了! 哈哈 好贴啊{:biggrin:} 先回复再看贴!{:smile:} 我在51上移植过freemodbus,在STM上直接搞个RTOS做个任务就差不多了吧 javabean 发表于 2012-9-19 13:24 static/image/common/back.gif
我在51上移植过freemodbus,在STM上直接搞个RTOS做个任务就差不多了吧
FreeModbus的uCOS应用,我争取这周整理出来!
多谢各位提的意见,我好好改正,希望大家可以满意!