A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

1.freeModbus开源包的下载

一般STM8用的开发环境是IAR,所以这里我们就讲在IAR下移植FreeModbus,
下载freemodbus-v1.5.0,官方下载地址http://www.freemodbus.org/找到Download

点击freemodbus-v1.5.zip即可下载。

2.freeModbus开源包的简介

打开文件夹的目录如下

然后我们打开主要的文件夹modbus

我们可以看到有ascii、functions、include、rtu、tcp以及mb.c源文件
Ascii Modbus ascii通信方式相关文件夹,
Rtu Modbus Rtu 通信方式和CRC校验相关文件夹,
Tcp Modbus Tcp通信方式相关文件夹,
Functions Modbus 功能函数相关,
Include Modbus 主要头文件都在这里,
Mb.c Modbus 最主要的头文件 ,包含初始化函数,poll函数等等
这些都是移植不需要修改的。涉及到Modbus的协议层,属于硬件无关层,所以都抽象出来,给下面提够统一的软接口就行了。
下面我们看看需要移植的部分,这部分在demo里面有很多平台的实例

我们打开看看,包含一些比较常见的平台,比如Atmel公司的ARM处理器,AVR单片机
TI的Msp430单片机,以及Liunx,Windows操作系统下的平台等等,但是没有我们想要的STM8处理器的接口。

但我们可以依葫芦画瓢,移植到其他平台,这里我们打开AVR下面目录看看,主要看port文件夹下面的文件,这些都是我们要移植的,包括串口驱动相关的portserial.c,定时器驱动相关的porttimer.c以及总中断相关的port.c(这个目录没有,我们添加进去)等等。

3.导入工程

分析完FreeModbus文件夹后,我们开始移植工作
首先在IAR里面建立工程,将Modbus文件夹拷贝到工程下面目录,将port文件夹拷贝到Modbus下面。如下图所示

Port在modbus文件夹下面目录

然后在工程中建立新的Modbus组

然后在工程的options菜单下面添加头文件路径,其中<span class="MathJax" id="MathJax-Element-2-Frame" tabindex="0" data-mathml="PROJDIR" role="presentation" style="box-sizing: border-box; outline: 0px; display: inline; line-height: normal; text-align: left; word-spacing: normal; word-wrap: normal; white-space: nowrap; float: none; direction: ltr; max-width: none; max-height: none; min-width: 0px; min-height: 0px; border: 0px; word-break: break-all; position: relative;">PROJDIRPROJDIR\为当前工程目录

在上述步骤完成后,我们开始改写硬件驱动,主要分三大块

3.1硬件设备驱动移植3.1.1串口分析与移植

打开portserial.c对下面函数进行功能完善
Void vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable );
BOOL xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity );
BOOL xMBPortSerialPutByte( CHAR ucByte );
BOOL xMBPortSerialGetByte( CHAR * pucByte );
以及串口接收中断服务函数,串口发送完成中断服务函数。
首先是Void vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable );这个函数主要是串口发送和接收的中断使能。函数的第一个参数为接收使能布尔型参数,第二个为发送使能布尔型参数。当xRxEnable为真时,打开串口接收中断,为假时,关闭串口接收中断;同样,发送也是一样。为了节约使用低容量处理器有限的存储空间,我们接下来的单片机相关资源的操作都采用寄存器的方式,那么我们先看STM8寄存器手册,


可以很清楚发现UART1_CR2寄存器的第5位和第6位是控制串口接收中断和串口的发送完成中断,这里我们只需要将此两位置高和置低即可实现相应的使能和禁止功能。所以这里我们的代码

void vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable ) //控制串口的收发中断{    if(xRxEnable==TRUE)    {            UART1_CR2|=(1<<5);    }    else    {            UART1_CR2&=~(1<<5);    }    if(xTxEnable==TRUE)    {            UART1_CR2|=(1<<6);    }    else    {           UART1_CR2&=~(1<<6);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

然后就是BOOL xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )函数的实现。
看函数的参数,第一个是端口,第二个是波特率,第三个是数据位,第四个是校验方式
因为本实验移植采用的STM8103F3C6,只带一个Uart串口,所以第一个参数就不用去实现了。
第二个参数是波特率,我们可以看单片机的寄存器手册

波特率的设置是通过时钟与波特率分频的比值决定的。通过查看寄存器,很容易发现,波特率比率由两个8位寄存器构成,但是寄存器的排列方式不是按照一般数据存储方式。UART_BRR1为波特率分频系数的4至11位数据,UART_BRR2的低四位为波特率分频系数的第四位,而UART_BRR2的高四位为波特率分频系数的12至15位。所以这里我们需要进行转换
UART1_BRR2 = div & 0x0f;
UART1_BRR2 |= ((div & 0xf000) >> 8);
UART1_BRR1 = ((div & 0x0ff0) >> 4);
Div为波特率分频系数
那么ulBaudRate = f/Div;(f为系统的时钟)。
接着我们分析第三个参数,数据位,如下图,UART_CR1寄存器第4位,当为0时,是一个起始位,8个

数据位,n个停止位,这里的停止位需要通过UART_CR3寄存器设定;当为1时,是一个起始位,9个数据位,一个停止位。然后我们去分析UART_CR3寄存器

通过查看寄存器,发现停止位有1,1.5,2等3种情况。一般应用下,我们都是用的一个停止位,故这里可以直接保留默认值就行了。所以代码可以

if(ucDataBits == 8){   UART1_CR1&=~(1<<4);}else{   UART1_CR1|=(1<<4);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

最后我们看看串口奇偶校验参数实现,查看寄存器UART1_CR1

第1位 PS 奇偶检验选择 当为0时,偶检验;当为1时奇校验。
第2位 奇偶校验使能位 当为0时,禁用校验;当为1时使能校验。
这些代码之前注意参数eMBParity eParity,位枚举型变量,看看定义

所以代码为

switch(eParity){    case MB_PAR_NONE:        UART1_CR1&=~(1<<2);        break;    case MB_PAR_ODD:         UART1_CR1|=(1<<2);        UART1_CR1|=(1<<1);        break;    case MB_PAR_EVEN:        UART1_CR1|=(1<<2);        UART1_CR1&=~(1<<1);        break;    default:break;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

至此串口的主要功能基本实现差不多,还有BOOL xMBPortSerialPutByte( CHAR ucByte );
BOOL xMBPortSerialGetByte( CHAR * pucByte )主要是数据的写入和读取,也就是直接读取和写入UART_DR就行了。

BOOL xMBPortSerialPutByte( CHAR ucByte ){    while((UART1_SR & 0x80)==0x00);      UART1_DR=ucByte;    return TRUE;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
// 串口收BOOL xMBPortSerialGetByte( CHAR * pucByte ){    *pucByte = UART1_DR;    return TRUE;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
3.1.2 中断分析与移植

除此之外,我们还要对两个中断服务函数进行处理。

/* 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. */ 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. */void prvvUARTRxISR( void ){     pxMBFrameCBByteReceived(  );}//将收到的数据再发送出去#pragma vector= UART1_R_RXNE_vector__interrupt void UART1_R_RXNE_IRQHandler(void){      if(UART1_SR&(1<<3))      {       // UART1_SR&=~(1<<3);      }      else      {        prvvUARTRxISR();//接受中断        //UART1_SR&=~(1<<5);      }      return;}//将收到的数据再发送出去#pragma vector= UART1_T_TC_vector__interrupt void UART1_T_TC_IRQHandler(void){      prvvUARTTxReadyISR();//发送完成中断      //UART1_SR&=~(1<<6);      return;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
3.1.3 定时器分析与移植

下面就开始定时器的移植,定时器的寄存器功能设置和上面讲解的串口寄存器设置类似,所以下面只适当注释下代码,相信大家很容易看懂。定时器的移植主要是对porttimer.c中
BOOL xMBPortTimersInit( USHORT usTim1Timerout50us );
void vMBPortTimersEnable( );
void vMBPortTimersDisable( );
prvvTIMERExpiredISR( );函数的回调
xMBPortTimersInit( USHORT usTim1Timerout50us );主要是50us时基,用于产生和判断1.5-3.5个字符时间,作为产生和判断数据帧的结束标准。那么,产生50us时基我们可以采用定时器分频做到,首先1/50us转换成频率是20kHz,即定时器的计数器频率为20kHz时,计数器每增一或减一,所需要时间就是50us.所以我们分频系数为8MHz/20KHz = 400;而 USHORT usTim1Timerout50us参数为50us的个数,

    /* Set the Prescaler value */    TIM1_PSCRH = (unsigned char)((period >> 8)&0xff);    TIM1_PSCRL = (unsigned char)(period&0xff);    usTim1Timerout50us = usTim1Timerout50us-1;    TIM1_ARRH = (unsigned char)((usTim1Timerout50us >> 8)&0xff);    TIM1_ARRL = (unsigned char)(usTim1Timerout50us&0xff);    /* Set the Repetition Counter value */    TIM1_RCR = 0;    /* Set the ARPE Bit */    TIM1_CR1 |= MASK_TIM1_CR1_ARPE;    /* Enable the Interrupt Upmode sources */    TIM1_IER |= 0x01;    /* set or Reset the CEN Bit */    TIM1_CR1 |= MASK_TIM1_CR1_CEN; 那么 xMBPortTimersInit( USHORT  usTim1Timerout50us )函数实现为:    unsigned int period = 400-1;//分频系数    TIM1_PSCRH = (unsigned char)((period >> 8)&0xff);    TIM1_PSCRL = (unsigned char)(period&0xff);    usTim1Timerout50us = usTim1Timerout50us-1;    TIM1_ARRH = (unsigned char)((usTim1Timerout50us >> 8)&0xff);    TIM1_ARRL = (unsigned char)(usTim1Timerout50us&0xff);    TIM1_RCR = 0;    TIM1_CR1 |= MASK_TIM1_CR1_ARPE;    TIM1_IER |= 0x01;//使能中断    TIM1_CR1 |= MASK_TIM1_CR1_CEN;//使能计数器
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

然后就是实现vMBPortTimersEnable( ),vMBPortTimersDisable( );打开和关闭时钟

void vMBPortTimersEnable() //打开时钟{        TIM1_SR1 &= ~(1<<0);//清除标志        TIM1_IER |= (1<<0);//使能中断        TIM1_CNTRH = 0x00;        TIM1_CNTRL = 0x00;  //计数器清零        TIM1_CR1 |= MASK_TIM1_CR1_CEN;//使能定时器}void vMBPortTimersDisable() //关闭时钟{      TIM1_CR1 &= ~MASK_TIM1_CR1_CEN;//关闭定时器      TIM1_CNTRH = 0x00;      TIM1_CNTRL = 0x00;  //计数器清零      TIM1_IER &= ~(1<<0);//关闭中断      TIM1_SR1 &= ~(1<<0);//清除标志}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

以及在中断服务函数中添加,溢出中断中调用prvvTIMERExpiredISR( )函数进行协议处理

#pragma vector=TIM1_OVR_UIF_vector__interrupt void TIM1_UPD_OVF_TRG_BRK_IRQHandler(void){  prvvTIMERExpiredISR( );  TIM1_SR1 &= ~(1<<0);//清除标志}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

最后还要实现
void EXIT_CRITICAL_SECTION(void)//退出超临界 开总中断
void ENTER_CRITICAL_SECTION(void)//进入超临界 关总中断

void ENTER_CRITICAL_SECTION(void)//进入超临界 关总中断{        asm("sim");}void EXIT_CRITICAL_SECTION(void)//退出超临界 开总中断{       asm("rim");}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

别忘了加入头文件#include “intrinsics.h”

到这里,整个Modbus的移植算是完成了。

3.2 api功能的实现

但在这里,没有读写寄存器,线圈的相关操作函数,我们可以根据自己的需求进行添加和裁剪。
其中声明的函数:

eMBErrorCode eMBRegInputCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs );eMBErrorCode eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode );eMBErrorCode eMBRegCoilsCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNCoils, eMBRegisterMode eMode );eMBErrorCode eMBRegDiscreteCB(UCHAR* pucRegBuffer,USHORT usAddress,USHORT usNDiscrete );
  • 1
  • 2
  • 3
  • 4

这里我们测试,就实现eMBRegHoldingCB保持寄存器的功能函数

eMBErrorCodeeMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode ){    eMBErrorCode    eStatus = MB_ENOERR;    USHORT          iRegIndex;    USHORT *        pusRegHoldingBuf;    USHORT          REG_HOLDING_START;    USHORT          REG_HOLDING_NREGS;    USHORT          usRegHoldStart;    pusRegHoldingBuf = usSRegHoldBuf;    REG_HOLDING_START = S_REG_HOLDING_START;    REG_HOLDING_NREGS = S_REG_HOLDING_NREGS;    usRegHoldStart = usSRegHoldStart;    usAddress--;//FreeModbus功能函数中已经加1,为保证与缓冲区首地址一致,故减1    if( ( usAddress >= REG_HOLDING_START ) &&        ( usAddress + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS ) )    {        iRegIndex = usAddress - usRegHoldStart;        switch ( eMode )        {            // Pass current register values to the protocol stack.         case MB_REG_READ:            while( usNRegs > 0 )            {                *pucRegBuffer++ = ( unsigned char )( pusRegHoldingBuf[iRegIndex] >> 8 );                *pucRegBuffer++ = ( unsigned char )( pusRegHoldingBuf[iRegIndex] & 0xFF );                iRegIndex++;                usNRegs--;            }            break;            // Update current register values with new values from the protocol stack.        case MB_REG_WRITE:            while( usNRegs > 0 )            {                pusRegHoldingBuf[iRegIndex] = *pucRegBuffer++ << 8;                pusRegHoldingBuf[iRegIndex] |= *pucRegBuffer++;                iRegIndex++;                usNRegs--;            }            break;        }    }    else    {        eStatus = MB_ENOREG;    }    return eStatus;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

这段代码直接移植的Armink大神开源代码里面保持寄存器的实现函数。移植后这些后,我们就可以再主函数中添加

int main( void ){        System_Clock_Init();    Led_Init();    eMBInit(MB_RTU, 0x01, 1, 9600, MB_PAR_NONE);//初始化 FreeModbus 为RTU模式 从机地址为1 Uart1 9600 无校验    eMBEnable();        while (1)    {        eMBPoll();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
3.3 测试

连接好串口与电脑

打开Modbus调试精灵,设置好串口号,波特率,校验位,数据位,停止位和设备的地址

通过对寄存器的读取,可以看出通信正常。

4 优化

为了使Modbus协议栈占用资源空间达到最小,我们对其进行裁剪
那么对mbconfig.h里面的内容进行修改,将没有用到的ASCII、TCP进行禁用,那么在编译器进行编译时,就没有对ASCII、TCP相关的模块功能进行编译了。

#define MB_ASCII_ENABLED                        ( 0 )#define MB_RTU_ENABLED                          ( 1 )#define MB_TCP_ENABLED                          ( 0 )#define MB_ASCII_TIMEOUT_SEC                    ( 0 )

0 个回复

您需要登录后才可以回帖 登录 | 加入黑马