当前位置: 首页 > news >正文

GD32E232的Modbus串口编程入门

抱歉不会插入图片,如果需要pdf版本,请email我14518918@qq.com或用PC浏览器直接下载附件。

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

http://www.cnnetsun.cn/news/1308771.html

相关文章:

  • PlayGround AI —— 使用图片生图器轻而易举
  • CN_GreenLumaGUI 项目常见问题解决方案
  • 如何使用 Boost.Python 实现 C++ 与 Python 的无缝桥接:完整指南
  • 10倍提升Laravel应用性能:图像优化完全指南(2025最新版)
  • 终极Grafana Dash Gen问题解决方案:从入门到精通的完整指南
  • 【开源】基于51单片机的简易智能楼道照明设计
  • 贡献者必读:如何为Its Hover图标库提交新的动画图标
  • 解决Meta Llama模型转换中的符号链接错误:3种方案让模型部署提速90%
  • Docker-Android环境变量终极指南:解锁容器化Android的全部潜能
  • RequireJS终极指南:如何构建高性能前端模块加载系统
  • YAPF性能优化终极指南:让大型Python项目格式化速度提升300%
  • Druid数据本地化终极指南:10个存储访问优化技巧
  • 终极指南:掌握dio指数退避策略,让网络请求永不失败
  • Sorcar实时更新功能详解:参数调整即时反馈的秘密
  • 掌握Hasura Backend Plus存储规则:自定义S3访问权限的实用技巧
  • blender_mmd_tools终极指南:轻松导入MikuMikuDance模型与动作
  • AICore游戏AI开发库:从零构建智能游戏角色的终极指南
  • SpringBoot 脚手架搭建指南:从零构建企业级开发框架
  • Awesome Go:终极Go语言资源宝库
  • Ostrakon-VL-8B入门必看:零售数字化转型中首个可私有部署的领域MLLM
  • Alpamayo-R1-10B作品集:不同天气条件(晴/雨/夜)下视觉鲁棒性对比
  • Qwen3-0.6B-FP8效果展示:复杂逻辑题的CoT分步推理折叠演示
  • HybridPageKit vs 传统方案:为什么它能成为新闻类App的首选框架?
  • Zed‘s community论坛功能深度探索:高效交流与问题解决的终极技巧
  • Home Assistant Glow快速上手:3步完成固件安装与Home Assistant集成
  • 原生开发的利与弊:Vanilla-Todo项目经验与最佳实践总结
  • IPED命令行帮助文档生成:自动生成帮助文档的例子
  • RAG Search API性能优化技巧:提升检索速度与结果质量的10个实用方法
  • 深入理解batt工作原理:守护进程如何智能管理电池充电
  • django-split-settings源码解析:include函数的工作原理与实现细节