【无标题】

本文档介绍了如何使用GD32E232单片机进行Modbus串口通信编程,包括重定向printf函数到USART0,初始化USART0,设置中断服务程序以处理不定长字符串,以及响应ModbusRTU从机指令的方法。文中详细阐述了串口接收中断、数据帧格式配置和错误处理策略,并提供了烧录验证的步骤和实际应用示例。
摘要由CSDN通过智能技术生成

GD32E232的Modbus串口编程入门

抱歉不会插入图片,如果需要pdf版本,请email我14518918@qq.com索取。

GD32单片机的串口编程,难点不在于串口发送字符串,而在于串口接收不定长字符串的编程。原厂例程,我参考的是GD32E232_Firmware_Library_V1.0.1\Examples\USART\Transmitter&receiver_interrupt,是针对定长度字符串的接收的,所以当遇到不定长字符串我就犯难了,幸好通常工程上都约定一条字符串以换行符’\n’作为本条字符串的结束符。而串口发送字符串,可以直接调用功能强大到没朋友的printf()函数。
上一篇几乎同名的文档《GD32E232串口编程 20221012》似乎有bug,而且是以换行符’\n’作为指令结束符,索性这次升版,支持接收不定长字符串指令,以实现modbus的交互。

1, 在main.c中将printf()函数重定向到USART0
因为标准库函数的默认输出设备是显示器而不是串口,要实现printf()到串口,必须重定义标准库函数里调用的与输出设备相关的函数即fputc(),将之输出指向指定序号的串口设备。该函数只需要放在main.c中就好,是被后台调用的。串口发送字符串,直接调用功能强大到没朋友的printf()函数就可以了,完全不需要自己编代码,很是方便。
//printf()重定向到USART0
int fputc(int ch, FILE *f)
{
usart_data_transmit(USART0, (uint8_t) ch);
while(RESET == usart_flag_get(USART0, USART_FLAG_TBE));
return ch;
}

2, 在main.c中的USART0初始化函数
串口初始化,主要是使能时钟、定位管脚、约定通信格式、使能中断四个部分。比如需要将GD32E232的PA9/PA10配置成USART0的Tx/Rx管脚。串口数据帧格式是:数据位8bit、停止位1bit、校验位0bit、波特率115200。
//USART0初始化
void USART0_init(void)
{
//1,时钟
//使能 COM 口所在的 GPOIA 的时钟
rcu_periph_clock_enable(RCU_GPIOA);
//使能 USART clock
rcu_periph_clock_enable(RCU_USART0);
//2,管脚
//指定 USART TxD=PA9, RxD=PA10,模式是 AF、推挽输出且内部上拉
gpio_af_set(GPIOA, GPIO_AF_1, GPIO_PIN_9);
gpio_af_set(GPIOA, GPIO_AF_1, GPIO_PIN_10);
gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_9);
gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_10);
gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_10MHZ, GPIO_PIN_9);
gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_10MHZ, GPIO_PIN_10);
//3,格式
//USART0 的帧格式定义及使能
usart_deinit(USART0); //复位 USART0
usart_word_length_set(USART0, USART_WL_8BIT); //数据位 8bit
usart_stop_bit_set(USART0, USART_STB_1BIT); //停止位 1bit
usart_parity_config(USART0, USART_PM_NONE); //校验位 0bit
usart_baudrate_set(USART0, 115200U); //波特率 115200
usart_receive_config(USART0, USART_RECEIVE_ENABLE);//使能接收机
usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);//使能发射机
usart_enable(USART0); //使能 USART0
//4,中断
//使能串口接收机接收中断
usart_interrupt_enable(USART0, USART_INT_RBNE);
//串口中断优先级2级
nvic_irq_enable(USART0_IRQn, 2);
}

3, gd32e232_it.c中的USART0中断服务程序
串口接收采用中断模式。所以MCU串口每收到一个字符都会进一次串口中断,导致MCU频繁进串口中断效率低下。将来可试试DMA模式。
MCU串口每收到一个字符,USART_INT_FLAG_RBNE标志位就会置1,则usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE)函数将返回1,if条件成立,则通过usart_data_receive(USART0)函数读取串口接收器FIFO中的字符并赋值到receiver_buffer[]字符串,同时累加已接收字符数计数器rxcount。如果当前接收到的字符是换行符’\n’,注意这是我们约定的一条串口指令的结束符,代码随即置标志位linefeed_received_flag=1。注意在main()函数主循环体中,如果探知到linefeed_received_flag=1就会引发串口指令的响应操作。
//USART0串口中断服务函数
void USART0_IRQHandler(void)
{
uint8_t Rchar;
if (RESET != usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE))
{
//从串口接收机FIFO中扣一个字符过来
Rchar = usart_data_receive(USART0);
//将次字符添加到receiver_buffer[]缓存的最后一位
receiver_buffer[rxcount++] = Rchar;
//在串口0中断服务程序中收到换行符,表示上位机已经发完一条完整指令
if (‘\n’ == Rchar)
{
linefeed_received_flag = 1;
}
}
}

4, main.c中main()对串口0的初始化和收到上位机一条指令的响应
当串口收到换行符结尾的字符串,linefeed_received_flag标志位会被置1。在main.c中while(1)主循环里,当判断到linefeed_received_flag=1,就会进入串口指令的响应的代码段,开始分析指令并执行响应操作。首先会清零linefeed_received_flag标志位,免得进入死循环;然后会将串口接收到的字符串转存到data_str[]字符串;进而清掉串口接收相关的相关全局变量,包括串口接收到的字符串变量receiver_buffer[]和串口已接收字符串长度rxcount。此后才开始分析指令并执行响应操作。
int main(void)
{
//初始化USART0
USART0_init();
//测试串口打印
printf(“Hello Modbus\n”);
//清零串口收到换行符标志位
linefeed_received_flag =0;

while (1)
{
    //在串口0中断服务程序中已经收到一条完整指令(以换行符结尾的字符串),则linefeed_received_flag=1
    if (1 == linefeed_received_flag)
    {
        linefeed_received_flag = 0; //清零标志位
        //转存接收字符串到 data_str
        strcpy(data_str, receiver_buffer);
        //为下一次处理接收字符串做好准备,清零串口接收相关的相关全局变量
        memset(receiver_buffer, 0, sizeof(receiver_buffer));
        rxcount = 0;
        //--------------判断 data_str[],处理具体事务--------------
        /*轮询使能或禁止*/
        if ((SlaveID == data_str[0]))
        {
            printf(data_str);
        }
    }
}

}

5, 烧录验证
复位后上位机串口应收到GD32上报的"Hello Modbus\n",然后当上位机下发一行以”\n”做结束符的指令,且第一个字符和GD32的SlaveID相符,则GD32将收到的这条指令原封不动地上报,但如果第一个字符和GD32的SlaveID不相符,GD32不做出响应。

6, Modbus RTU的从机编程
Modbus RTU是基于16进制数组的数据交互,数据流中不可避免地会出现0x0A也就是”\n”换行符,所以item5拿”\n”做一条指令的结束符,就不行了,还是得回到modbus协议规定的,两个数据帧之间间隔应该大于3.5个字符时间,一条数据帧内部两个字符之间的间隙不能大于1.5个字符时间。所以Arduino的Modbus RTU从机例程,就是判断,当从机接收到一个字符之后,等待1.5个字符时间,发现串口接收机还未接收到新来数据,就默认主机已经发完一条完整指令了,就该转头去解析并执行指令了。
这显然是一种不定长度的串口从机编程,似乎颇具难度。幸好GD32有串口接收机超时中断机制,即当串口接收机接收到最后一个字符之后的指定时间,会产生一个超时中断,中断标志位USART_FLAG_RT将置1,注意读取该标志位并不会自动清零它,需要用户编代码清零。这部分代码我是参考了原厂例程《\Examples\USART\Receiver_timeout》,没搞定,再接着参考了网贴https://blog.csdn.net/wojiusuibiankan/article/details/124864414,看来是需要同时使能串口收端非空中断和串口收端超时中断,这个超时中断的标志位才能正确被触发。
终于初步实现了modbus的03操作码的响应。

以回读FW_Ver为例,是上报了2个byte的0x0001。
以回读MCU_SN为例,是上报了16个byte的系列号的16进制数,翻译成ASCII码就是"SN_GD32_12345678"。
以CRC错误指令为例,是上报了0x80+0x03的操作码,和0x01的错误代码。
目前已实现03多字节读和16多字节写两个功能码,下图是用串口助手演示的交互过程。

下面详细解释上位机发出modbus指令及下位机的响应。
1,寄存器首地址0x0000单word读,固件版本号
发出:01 03 00 00 00 01 84 0A //从寄存器首地址0x0000读0001个word数据,即回读1byte的版本号
设备地址 功能码 寄存器首地址 欲回读的word数量 校验码
01 03 00 00 00 01 84 0A
响应:01 03 02 00 01 79 84 //可见固件版本号0x0001被正确读出了
设备地址 功能码 已回读byte数量 寄存器word数据值 校验码
01 03 02 00 01 79 84

2,寄存器首地址0x0001多word读,16byte的MCU_SN
发出:01 03 00 01 00 08 15 CC //从寄存器首地址0x0001读0008个word数据,即回读16byte的MCU_SN
设备地址 功能码 寄存器首地址 欲回读的word数量 校验码
01 03 00 01 00 08 15 CC
响应:01 03 10 53 4E 5F 47 44 33 32 5F 31 32 33 34 35 36 37 38 7D 36 //可见MCU_SN被正确读出了
设备地址 功能码 已回读byte数量 寄存器word数据值 校验码
01 03 10 53 4E 5F 47 44 33 32 5F 31 32 33 34 35 36 37 38 7D 36

3,寄存器首地址0x0001多word写,再多word读,16byte的MCU_SN
发出:01 10 00 01 00 02 04 31 32 33 34 88 77 //往从寄存器首地址0x0001写2个word数据,即修改MCU_SN前4byte
设备地址 功能码 寄存器首地址 欲写入的word数量 欲写入的byte数量
01 10 00 01 00 02 04
欲写入的word数据值 校验码
11 12 13 14 5B A9
响应:01 10 00 01 00 02 10 08 //正确写入
设备地址 功能码 寄存器首地址 已写入的word数量 校验码
01 10 00 01 00 02 10 08

发出:01 03 00 01 00 08 15 CC //从寄存器首地址0x0001读0008个word数据
设备地址 功能码 寄存器首地址 欲回读的word数量 校验码
01 03 00 01 00 08 15 CC
响应:01 03 10 31 32 33 34 44 33 32 5F 31 32 33 34 35 36 37 38 7E C1 //可见MCU_SN被正确读出,而且前4byte已经被成功改写
设备地址 功能码 已回读byte数量 寄存器word数据值 校验码
01 03 10 31 32 33 34 44 33 32 5F 31 32 33 34 35 36 37 38 7E C1

4,寄存器首地址0x0001多word写,16byte的MCU_SN,但忘加校验码
发出:01 10 00 01 00 02 04 11 12 13 14 //往从寄存器首地址0x0001写2个word数据,即修改MCU_SN前4byte
设备地址 功能码 寄存器首地址 欲写入的word数量 欲写入的byte数量
01 10 00 00 00 02 04
欲写入的word数据值 校验码
11 12 13 14 无
响应:01 90 01 8D C0 //报错,功能码在原始值基础上加0x80,错误代码01
设备地址 功能码 错误代码 校验码
01 90 01 8D C0

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值