GD32 移植 freeModbus slave RTU 经验分享
国产 MCU 移植 Modbus RTU 协议

GD32 兆易创新单片机,国产芯,必须支持!完美兼容 ARM 系列其他产品,价格美丽,性能卓越,支持到位。
freeModbus,开源且免费的 Modbus slave 实现,支持各种平台,包括嵌入式单片机系统。

1. 准备 GD32 系列单片机设备,本文采用 GD32F130F8;

你需要一块带有 GD32 单片机的板子,可以说开发板或成品设备,电路正常,带有串口 USART,最好具备 RS485 电路;用官方的 GD32F1x0_Firmware_Library_V3.0.0 example 新建项目,编译成功,且下载到设备,能够运行跑马灯,串口能打印正常。

  • 串口输出正常,波特率正常,比如 USART0
  • 串口中断输入正常,收到外部串口输入能及时处理
  • 定时器TIMER正常,需要 us 微秒级控制
  • GPIO输出正常,能够控制高低电平

2、准备 freeModbus V1.6版本,V1.5 也可以;

作为开发者移植 Modbus,相关原理最好还是要熟悉一下:https://www.modbus.org/, 不管怎么样,通过你自己的方式熟读一下 Modbus 协议内容。
github下载 freeModbus, 给作者 star,fork 到自己的项目;

  • modbus 目录结构
    原封不动,搬进你的工程中
├── ascii // ASCII模式文件,本例程未用到
│   ├── mbascii.c
│   └── mbascii.h
├── functions // 功能函数模块,很重要,不需要修改
│   ├── mbfunccoils.c
│   ├── mbfuncdiag.c
│   ├── mbfuncdisc.c
│   ├── mbfuncholding.c
│   ├── mbfuncinput.c
│   ├── mbfuncother.c
│   └── mbutils.c
├── include  // 头文件,很重要,不需要修改
│   ├── mb.h
│   ├── mbconfig.h
│   ├── mbframe.h
│   ├── mbfunc.h
│   ├── mbport.h
│   ├── mbproto.h
│   └── mbutils.h
├── mb.c // 主文件,不需要修改
├── rtu // RTU 模块文件,很重要,本例程需要熟读内容
│   ├── mbcrc.c
│   ├── mbcrc.h
│   ├── mbrtu.c
│   └── mbrtu.h
└── tcp // TCP 模块,本例程未用到
    ├── mbtcp.c
    └── mbtcp.h
  • 移植要求
    主要是针对 port 目录下的移植
├── demo.c // 主函数移植内容
└── port // 移植目录
    ├── port.h // 头文件
    ├── portevent.c // 移植事件文件,本例程不修改
    ├── portserial.c // 移植串口处理,很重要,需要熟读
    └── porttimer.c // 移植定时器处理,很重要,需要熟读

3、移植过程;

main.c

使用上一步中的 demo/BARE/port 下所有文件导入 GD32 工程中,且加入所有的 .h 文件到编译目录;将 demo.c 中的主循环内容移植到你的工程主循环中,波特率,串口号,等等参数,自行根据需要修改。

    eMBErrorCode    eStatus;
    eStatus = eMBInit( MB_RTU, 0x03, 0, 9600, MB_PAR_NONE );
    /* Enable the Modbus Protocol Stack. */
    eStatus = eMBEnable(  );
    for( ;; )
    {
        ( void )eMBPoll(  );
    }

port.c

在工程中新建一个 port.c , 把 modbus 回调函数填入,这里只是主要内容,其他默认回调也必须存在

    /* ----------------------- Defines ------------------------------------------*/
    #define REG_HOLDING_START ( 0x0000 )
    #define REG_HOLDING_NREGS ( 4 )
    
    /* ----------------------- Static variables ---------------------------------*/
    static USHORT   usRegHoldingStart = REG_HOLDING_START;
    static USHORT   usRegHoldingBuf[REG_HOLDING_NREGS] = { 0xAAAA, 0xBBBB, 0xCCCC, 0xDDDD};

    eMBErrorCode
    eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode )
    {
        /* error state */
        eMBErrorCode    eStatus = MB_ENOERR;
        /* offset */
        int16_t iRegIndex;
        
        /* test if the reg is in the range */
        if (((int16_t)usAddress-1 >= REG_HOLDING_START) 
            && (usAddress-1 + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS))
        {
            /* compute the reg's offset */
            iRegIndex = (int16_t)(usAddress-1 - REG_HOLDING_START);
            switch (eMode)
            {
                case MB_REG_READ:
                    while (usNRegs > 0)
                    {
                        *pucRegBuffer++ = (uint8_t)( usRegHoldingBuf[iRegIndex] >> 8 );
                        *pucRegBuffer++ = (uint8_t)( usRegHoldingBuf[iRegIndex] & 0xff);
                        iRegIndex ++;
                        usNRegs --;
                    }
                    break;
                case MB_REG_WRITE:
                    while (usNRegs > 0)
                    {
                        usRegHoldingBuf[iRegIndex] = *pucRegBuffer++ << 8;
                        usRegHoldingBuf[iRegIndex] |= *pucRegBuffer++;
                        iRegIndex ++;
                        usNRegs --;
                    }
                    break;
                    
            }
        }
        else{
            eStatus = MB_ENOREG;
        }
        
        return eStatus;
    }

增加全局中断函数使能/失能调用,这里用 CMSIS 通用配置

void
EnterCriticalSection( void )
{
  __disable_irq();
}

void
ExitCriticalSection( void )
{
  __enable_irq();
}

portserial.c

串口使能/失能配置【很重要】

/* ----------------------- Start implementation -----------------------------*/
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
    /* If xRXEnable enable serial receive interrupts. If xTxENable enable
     * transmitter empty interrupts.
     */
    if (xRxEnable)
    {
      gd_eval_ledoff(LED3); // 输入模式,设置 RS485 芯片 RE 口低电平
      usart_interrupt_enable(EVAL_COM1, USART_INT_RBNEIE); // 打开输入中断
    }
    else
    {
      gd_eval_ledon(LED3); // 输出模式,设置 RS485 芯片 RE 口低电平
      usart_interrupt_disable(EVAL_COM1, USART_INT_RBNEIE); // 关闭输入中断
    }
    
    if(xTxEnable)
    {
      usart_interrupt_enable(EVAL_COM1, USART_INT_TCIE); // 打开输出完成中断
    }
    else
    {
      usart_interrupt_disable(EVAL_COM1, USART_INT_TCIE); // 关闭输出完成中断
    }
}

串口初始化配置

BOOL
xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{
    /* USART interrupt configuration */
    nvic_irq_enable(USART0_IRQn, 0, 2);  // 串口中断号设置
     
    gd_eval_COMinit(EVAL_COM1); // 初始化串口
 
    usart_interrupt_enable(EVAL_COM1, USART_INT_RBNEIE); // 初始化后好就打开串口输入中断
    gd_eval_ledoff(LED3); // 485 芯片 RE 口低电平输入模式
  
    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_data_transmit(EVAL_COM1, 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_data_receive(EVAL_COM1); 
    return TRUE;
}

串口中断回调函数【非常重要,非常重要,非常重要】

void USART0_IRQHandler(void)
{
    if(RESET != usart_interrupt_flag_get(EVAL_COM1, USART_STAT_RBNE, USART_INT_RBNEIE)){
      /* receive data */
      prvvUARTRxISR();
    }
    
    if(RESET != usart_flag_get(EVAL_COM1, USART_STAT_TC)){
      /* transmit data */
      prvvUARTTxReadyISR();
    }
}

其他内容不需要修改。

porttimer.c

此文件主要是定时器设置,关于这个定时器,很多文章有写到,最关键是 50us 这个要相对准确

定时器初始化,说多了都是泪,自己体会,照抄下面的代码即可,注意使用哪个定时器TIMER1,MCU 主频要换算成 50us

BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us )
{
    timer_parameter_struct timer_initpara;

    rcu_periph_clock_enable(RCU_TIMER1);

    timer_deinit(TIMER1);

    /* TIMER1 configuration */
    timer_initpara.timer_prescaler         = 3599;  // 72MHz, 注意,这样就是50us
    timer_initpara.timer_alignedmode       = TIMER_COUNTER_EDGE;
    timer_initpara.timer_counterdirection  = TIMER_COUNTER_UP;
    timer_initpara.timer_period            = usTim1Timerout50us;
    timer_initpara.timer_clockdivision     = TIMER_CKDIV_DIV1;
    timer_initpara.timer_repetitioncounter = 0;
    timer_init(TIMER1,&timer_initpara);
    
    /* TIMER0 channel control update interrupt enable */
    timer_interrupt_enable(TIMER1,TIMER_INT_UP);
    
    /* TIMER1 counter enable */
    timer_enable(TIMER1);
  
    // NVIC CONFIG
    nvic_priority_group_set(NVIC_PRIGROUP_PRE1_SUB3);
    nvic_irq_enable(TIMER1_IRQn, 1, 1);
    
    return TRUE;
}

定时器使能/失能配置【很简单,但是很重要,尤其是计数器要清零】

inline void
vMBPortTimersEnable(  )
{
    /* Enable the timer with the timeout passed to xMBPortTimersInit( ) */
  
    timer_interrupt_flag_clear(TIMER1,TIMER_INT_UP);
    timer_counter_value_config(TIMER1, 0); // 清零
    /* TIMER1 counter enable */
    timer_enable(TIMER1);  
}

inline void
vMBPortTimersDisable(  )
{
    /* Disable any pending timers. */
  
    timer_counter_value_config(TIMER1, 0); // 清零
    /* TIMER1 counter enable */
    timer_disable(TIMER1);
}

定时器中断回调函数【很重要,但是很简单】

void TIMER1_IRQHandler(void)
{
    prvvTIMERExpiredISR();
    timer_interrupt_flag_clear(TIMER1,TIMER_INT_UP); // 清除定时器中断标志位
}

定时器其他照旧即可

portevent.c

没什么修改的

port.h

需要特别注意,修改下面两行

#define ENTER_CRITICAL_SECTION( )   EnterCriticalSection()
#define EXIT_CRITICAL_SECTION( )    ExitCriticalSection()

4、结果验证;

本例程只实现了 RTU 功能号 03 [保持寄存器读取],其他功能请自行扩展!
modbus-pull.PNG

有任何问题,请留言,我们共同讨论。

如何移植 Kionix KXTJ3 系列加速度传感器到嵌入式设备?
KXTJ3-1057/1058