STM32 FreeModbus RTU从机移植以及UART配置

来源:互联网 发布:abb工业机器人编程 编辑:程序博客网 时间:2024/06/04 19:32

FreeModbus的具体介绍就不提了。至于为什么要移植,大概就是因为移植比较快,而且比较稳定,可以减少因为自己编写出现的漏洞。
但是FreeModbus 1.5版本是没有主机的,因此移植的时候只可以做从机。网上有几个关于Modbus主机的源代码,回头等我弄好了再更新。

==================================

理论上来说,此处我移植了全部,但是只调试了RTU部分,因此其他部分不做赘述。

移植过程:
1.将modbus目录下所有文件拷贝加入工程。
2.对modbus中的include下的mbconfig.h进行编辑,裁剪其中需要的模块。(此处我没有进行裁剪,因此选项都是默认)
3.将demo中的合适的port文件夹下的文件加入工程。
4.修改port文件夹下的代码,移植UART驱动。
5.使用modbus poll调试。

因为没有配置mbconfig.h,因此直接从port的移植开始说。

port.c

#include "stm32f10x.h"void EnterCriticalSection(  ){    __set_PRIMASK(1);}void ExitCriticalSection(  ){    __set_PRIMASK(0);}

port.c中的两个函数作用是开关总中断并保存。

port.h

#ifndef _PORT_H#define _PORT_H#include "stm32f10x.h"#include <assert.h>#include <inttypes.h>#define INLINE#define PR_BEGIN_EXTERN_C           extern "C" {#define PR_END_EXTERN_C             }#define ENTER_CRITICAL_SECTION( )       EnterCriticalSection( )#define EXIT_CRITICAL_SECTION( )    ExitCriticalSection( )void            EnterCriticalSection( void );void            ExitCriticalSection( void );#ifndef TRUE#define TRUE            1#endif#ifndef FALSE#define FALSE           0#endiftypedef u8 UCHAR;typedef u16 USHORT;typedef u8 BOOL;typedef u32 ULONG;typedef char CHAR;typedef long LONG;typedef short SHORT;typedef int INT;#endif

port.h只要是针对一些类型的跨平台支持。

portevent.c

#include "mb.h"                  #include "mbport.h"/* ----------------------- Variables ----------------------------------------*/static eMBEventType eQueuedEvent;static BOOL     xEventInQueue;/* ----------------------- Start implementation -----------------------------*/BOOLxMBPortEventInit( void ){    xEventInQueue = FALSE;    return TRUE;}BOOLxMBPortEventPost( eMBEventType eEvent ){    xEventInQueue = TRUE;    eQueuedEvent = eEvent;    return TRUE;}BOOLxMBPortEventGet( eMBEventType * eEvent ){    BOOL            xEventHappened = FALSE;    if( xEventInQueue )    {        *eEvent = eQueuedEvent;        xEventInQueue = FALSE;        xEventHappened = TRUE;    }    return xEventHappened;}

portevent.c没有改动,作用是起到一个简单的事件队列。

接下来两个是重点移植的文件
portserial.c

1).vMBPortSerialEnable 函数的作用是单独禁止或开启发送或接受中断。在此处有人推荐使用发送完成中断而并非发送缓存为0中断。想想是有道理的,这里还在测试阶段,因此还没有去修改。

void vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable ){    if(xRxEnable)    {        USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);         DE1 = 0;    }    else    {        USART_ITConfig(USART2, USART_IT_RXNE, DISABLE);        DE1 = 1;    }       if(xTxEnable)    {        USART_ITConfig(USART2, USART_IT_TXE, ENABLE);        prvvUARTTxReadyISR();    }    else        USART_ITConfig(USART2, USART_IT_TXE, DISABLE);}

2).vMBPortClose 在Demo的源代码中没有编写,我也没有移植。

3).xMBPortSerialInit 是串口初始化的函数,其中只要把对应需要的串口移植添加进来就可以了。

BOOL xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity ){    BOOL bInitialized = TRUE;    GPIO_InitTypeDef GPIO_InitStructure;    USART_InitTypeDef  USART_InitStructure;    NVIC_InitTypeDef NVIC_InitStructure;    (void)ucPORT;    (void)ucDataBits;    (void)eParity;    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB|RCC_APB2Periph_AFIO, ENABLE);//使能GPIO外设时钟    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);    GPIO_InitStructure.GPIO_Pin=GPIO_Pin_3;             //RX2    GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;    GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;         GPIO_Init(GPIOA, &GPIO_InitStructure);    GPIO_InitStructure.GPIO_Pin=GPIO_Pin_2;           //TX2    GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;    GPIO_Init(GPIOA, &GPIO_InitStructure);    GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9;           //DE    GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_OD;    GPIO_Init(GPIOB, &GPIO_InitStructure); /*************************************************************************************/    USART_InitStructure.USART_BaudRate = ulBaudRate;         USART_InitStructure.USART_StopBits = USART_StopBits_1;   //  USART_InitStructure.USART_Parity = USART_Parity_No; //设置奇校验时,通信出现错误     switch(eParity)    {    case MB_PAR_NONE:USART_InitStructure.USART_Parity = USART_Parity_No;USART_InitStructure.USART_WordLength = USART_WordLength_8b; break;    case MB_PAR_ODD:USART_InitStructure.USART_Parity = USART_Parity_Odd;USART_InitStructure.USART_WordLength = USART_WordLength_9b; break;    case MB_PAR_EVEN:USART_InitStructure.USART_Parity = USART_Parity_Even;USART_InitStructure.USART_WordLength = USART_WordLength_9b; break;    default:break;    }    USART_InitStructure.USART_HardwareFlowControl=USART_HardwareFlowControl_None;        USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;     USART_Init(USART2, &USART_InitStructure);     USART_Cmd(USART2,ENABLE);    vMBPortSerialEnable(FALSE,FALSE);    USART_ClearFlag(USART2,USART_FLAG_TC); /*************************************************************************************/    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);     NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn; //通道设置为串口2中断    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority =0;//抢占优先级    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;  //中断响应优先级0    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //打开中断    NVIC_Init(&NVIC_InitStructure);   //初始化     return bInitialized;}

串口初始化程序很简单。设置一下数据位和校验还有波特率就可以了。此处我只针对了UART2移植,因此没有加入其他的串口初始化。

4).xMBPortSerialPutByte 的作用是将字节数据送到串口。把正常用的串口发送拿过来就可以了

BOOL xMBPortSerialPutByte( UCHAR ucByte ){    USART_SendData(USART2, ucByte);      while(USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET){};    return TRUE; }

5).xMBPortSerialGetByte 的作用是从串口接收数据并且把数据传给指定内存。

BOOLxMBPortSerialGetByte( UCHAR * pucByte ){    *pucByte = (UCHAR)UARTRecvBuffer;    return TRUE;}

此处的UARTRecvBuffer是一个接受缓冲区。我这样做是为了防止在串口接收的时候数据来不及接收被冲毁,是自己因为在仿真测试的时候发现数据经常丢字节而做的一个措施,具体正确性要等待上板测试。

6).USART2_IRQHandler 串口2中断此处中断有两个。发送和接受。

void USART2_IRQHandler(void){    if(USART_GetITStatus(USART2,USART_IT_RXNE)!=RESET)    {        USART_ClearITPendingBit(USART2,USART_IT_RXNE);        UARTRecvBuffer = USART_ReceiveData(USART2);        prvvUARTRxISR();    }    if(USART_GetITStatus(USART2,USART_IT_TXE)!=RESET)    {        USART_ClearITPendingBit(USART2,USART_IT_TXE);        prvvUARTTxReadyISR();    }}

7).prvvUARTTxReadyISR 这个函数其实就是发送的时候会调用的函数。

static void prvvUARTTxReadyISR( void ){    DE1 = 1;    delay_ms(1);    pxMBFrameCBTransmitterEmpty(  );    delay_ms(1);    DE1 = 0;}

DE1是485的控制器,为1的时候就是发送状态,为0是接收状态。

8).prvvUARTRxISR是串口接收函数,调用pxMBFrameCBByteReceived();

static void prvvUARTRxISR( void ){    pxMBFrameCBByteReceived(  );}

porttimer.c

1).xMBPortTimersInit 是用来初始化定时器的,modbus-rtu需要有一个定时器来计算每个字节之间的间隔时间,若间隔时间超过3.5T,则是认为此帧已经接收完毕。
此处的
timerBaseInit.TIM_Period = usTim1Timerout50us - 1;
timerBaseInit.TIM_Prescaler = 1799;// 50us
周期是重复次数,是在定时器中断为50us的前提下的重复次数,一般在波特率≤19200的情况下,公式为:
usTimerT35_50us = ( 7UL * 220000UL ) / ( 2UL * ulBaudRate );
在大于19200的情况下,固定为35.
TIM_Period 和TIM_Prescaler 传入的时候 都要减一才准确。此处我的时钟是TIM2,所以挂载APB1上,APB1我设置为最大的3600MHz。因此当分频为1800的时候,刚刚好是50us中断一次,所以分频设置为1799.

BOOL xMBPortTimersInit( USHORT usTim1Timerout50us ){    TIM_TimeBaseInitTypeDef timerBaseInit;    NVIC_InitTypeDef NVIC_InitStructure;    TIM_DeInit(TIM2); //重新将Timer设置为缺省值    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);// 使能Timer2外设时钟    //TIM_InternalClockConfig(TIM2); //采用内部时钟给TIM2提供时钟源    // TIM2    timerBaseInit.TIM_Period = usTim1Timerout50us - 1;     timerBaseInit.TIM_Prescaler = 1799;// 50us    timerBaseInit.TIM_CounterMode = TIM_CounterMode_Up;    timerBaseInit.TIM_ClockDivision = TIM_CKD_DIV1;    TIM_TimeBaseInit(TIM2, &timerBaseInit);    TIM_ClearITPendingBit(TIM2, TIM_IT_Update);//清标志    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);     //中断使能    TIM_Cmd(TIM2, DISABLE);    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;//抢占优先级    NVIC_InitStructure.NVIC_IRQChannelSubPriority =0;//响应优先级    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//允许中断    NVIC_Init(&NVIC_InitStructure);    return TRUE;}

2).vMBPortTimersEnable TIM2使能函数
在此使能时钟的时候,要把时钟重置。

void vMBPortTimersEnable(  ){    TIM_Cmd(TIM2, DISABLE);    TIM_Cmd(TIM2, ENABLE);}

3).vMBPortTimersDisable TIM2除能函数

voidvMBPortTimersDisable(  ){    TIM_Cmd(TIM2, DISABLE);}

4).TIM2_IRQHandler 定时器中断,定时进入判断是否接收超时或完毕

void TIM2_IRQHandler(void){   u8 i;   (void)i;   if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET){      TIM_ClearITPendingBit(TIM2, TIM_IT_Update);      ( void )pxMBPortCBTimerExpired();   }}

至此FreeModbus从机部分移植完毕。接下来就是调试了。

UART配置问题
一开始这边有一个问题,就是传输的时候每个字节都少最高位。如传输0xCD,就变成了0X4D。最终找了很久原因,终于找到了两个可能的原因,第一个就是我发现在串口接收函数xMBPortSerialGetByte中,传入的变量是char类型而不是unsigned char类型,这会导致数据符号位变化?所以这里我也统一修改成unsigned char了。第二个是我阅读资料后,发现如果设置奇偶校验位的情况下,如果M即字长这一位是0,那么则数据位只有7位?所以不发送最高位?参考手册中这样写道。

因此我将该位在奇偶校验情况下设置M为9位,最终我忘记这两个方法哪一个解决了此问题。

配置时钟的时候,我发现波特率并不是指定的整数,而是近似的整数,我查了一下手册,然后找到晶振和波特率之间的对应关系,需要设置一个除数来达到近似的波特率。高频率竞争更容易的到更精确的波特率,这样误码率会比较低。

对应关系如下表

APB1下 UART2的波特率:
这里写图片描述

这些是手动计算出来的公式为
USARTDIV = 晶振频率/(16 * 波特率)
DIV_M就是USARTDIV的整数部分的16进制形式,DIV_F是小数部分 * 16后四舍五入的16进制形式BRR寄存器的值是DIV_M与DIV_F拼接起来的值。然后后面跟随的是实际计算分出的波特率。我试了一下若频率是11.0592MHz的时候,计算出的115200波特率是刚刚好的 误差是0%。误差小了,相对的误码率就会减少。所以选择一个好的时钟是很有必要的。

0 0
原创粉丝点击