抱歉不会插入图片如果需要pdf版本请email我14518918qq.com或用PC浏览器直接下载附件。GD32单片机的串口编程难点不在于串口发送字符串而在于串口接收不定长字符串的编程。原厂例程我参考的是GD32E232_Firmware_Library_V1.0.1\Examples\USART\Transmitterreceiver_interrupt是针对定长度字符串的接收的所以当遇到不定长字符串我就犯难了幸好通常工程上都约定一条字符串以换行符’\n’作为本条字符串的结束符。而串口发送字符串可以直接调用功能强大到没朋友的printf()函数。上一篇几乎同名的文档《GD32E232串口编程 20221012》似乎有bug而且是以换行符’\n’作为指令结束符索性这次升版支持接收不定长字符串指令以实现modbus的交互。1 在main.c中将printf()函数重定向到USART0因为标准库函数的默认输出设备是显示器而不是串口要实现printf()到串口必须重定义标准库函数里调用的与输出设备相关的函数即fputc()将之输出指向指定序号的串口设备。该函数只需要放在main.c中就好是被后台调用的。串口发送字符串直接调用功能强大到没朋友的printf()函数就可以了完全不需要自己编代码很是方便。//printf()重定向到USART0int 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 clockrcu_periph_clock_enable(RCU_USART0);//2管脚//指定 USART TxDPA9, RxDPA10模式是 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); //复位 USART0usart_word_length_set(USART0, USART_WL_8BIT); //数据位 8bitusart_stop_bit_set(USART0, USART_STB_1BIT); //停止位 1bitusart_parity_config(USART0, USART_PM_NONE); //校验位 0bitusart_baudrate_set(USART0, 115200U); //波特率 115200usart_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)函数将返回1if条件成立则通过usart_data_receive(USART0)函数读取串口接收器FIFO中的字符并赋值到receiver_buffer[]字符串同时累加已接收字符数计数器rxcount。如果当前接收到的字符是换行符’\n’注意这是我们约定的一条串口指令的结束符代码随即置标志位linefeed_received_flag1。注意在main()函数主循环体中如果探知到linefeed_received_flag1就会引发串口指令的响应操作。//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_flag1就会进入串口指令的响应的代码段开始分析指令并执行响应操作。首先会清零linefeed_received_flag标志位免得进入死循环然后会将串口接收到的字符串转存到data_str[]字符串进而清掉串口接收相关的相关全局变量包括串口接收到的字符串变量receiver_buffer[]和串口已接收字符串长度rxcount。此后才开始分析指令并执行响应操作。int main(void){//初始化USART0USART0_init();//测试串口打印printf(“Hello Modbus\n”);//清零串口收到换行符标志位linefeed_received_flag 0;while (1) { //在串口0中断服务程序中已经收到一条完整指令以换行符结尾的字符串,则linefeed_received_flag1 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错误指令为例是上报了0x800x03的操作码和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 842寄存器首地址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 363寄存器首地址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 C14寄存器首地址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