GD32 移植 freeModbus slave 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 [保持寄存器读取],其他功能请自行扩展!
有任何问题,请留言,我们共同讨论。