STM32 RS485串口DMA接收及发送,问题记录及调试解决
芯片型号:STM32F767IGT6、SP3485,如图1、图2所示。
![image-20230622184446680](https://i-blog.csdnimg.cn/blog_migrate/436e1c9004446ab69eb5f27127999f28.png#pic_center)
![image-20230622185006024](https://i-blog.csdnimg.cn/blog_migrate/166f21675efd42cb269abfd295048b11.png#pic_center)
开发环境:Keil uVision5、STM32CubeMX,如图3、图4所示。
![image-20230622184317724](https://i-blog.csdnimg.cn/blog_migrate/68b64aa0975ea6a3809582241b0ca372.png#pic_center)
![image-20230622184536168](https://i-blog.csdnimg.cn/blog_migrate/0bdc01a3c8402e2d8b401443035e0b33.png#pic_center)
之前与上层设备的通讯协议是基于MODBUS TCP进行地相应开发,但因为STM32F767IG芯片只有一个物理网口,并且其也不得不接入局域网。所以当局域网内有大量数据时可能导致网络阻塞,其与上层设备的通讯会不稳定,从而无法正常工作。于是乎便考虑使用基于MODBUS RTU的协议进行通讯。在开发过程中,碰到了些许问题,花费了不少时间解决,觉得有必要记录总结下。
好了,背景阐述完毕,咱们进入正题。
其实MODBUS协议从网口改为兼容串口通讯,协议部分的处理并不棘手,只要考虑如何兼容网口的MBAP报文头的处理,以及串口的CRC校验;串口通讯主要的难点在于命令帧之间的区分、应答的及时性,同时若使用RS485通讯,需要注意485芯片数据处理方向的转换,以及转换的时机。
1.数据接收
如图5所示,在文档Modbus_over_serial_line_V1 2.5.1.1节阐述,在MODBUS RTU模式中,消息帧之间的最小间隔时间是3.5字符的时间(具体时间由串口比特率及单个字符包含的位数决定,如比特率为9600bits/s,单个字符包含1个起始位,8个数据位,0个校验位,1个停止位,则最小间隔时间具体值t3.5=1/9600*(1+8+0+1)*3.5≈3.65ms)。因此数据接收可以使用芯片自带的超时功能实现,如图6所示,除了要使能接收器超时中断外,同时也要使能接收器超时功能,并使能DMA接收的循环模式,即数据接收采用 串口接收器超时中断+DMA接收循环模式 的方式,在串口中断中判断是否是超时中断,若是则获取此次中断录入DMA的新数据长度值,数据长度值的存储采用10级的循环缓冲区实现,并在程序主循环中判断存储数据长度值的缓冲区是否为空,若不为空则根据存储的数据长度值读取DMA接收缓冲区相应的数据量,然后送入MODBUS协议解析函数中进行处理。
![image-20230626090402165](https://i-blog.csdnimg.cn/blog_migrate/2034861de9eb4c009f423e36b7b81350.png#pic_center)
![image-20230622190810356](https://i-blog.csdnimg.cn/blog_migrate/4e80dd273aa46fcc12788ece9a9f4a3b.png#pic_center)
2.数据发送
2.1调用HAL_UART_Transmit()进行发送
计划是准备先通过标准函数HAL_UART_Transmit()调通后,再考虑使用DMA发送。现实却是一直有接收到正确的数据,但就是没有进行正确的应答。查了很长一段时间,甚至在Debug模式下一步步运行,程序都是在按设想地运行,最后弄得都有点怀疑人生了,才猛得意识到是没有正确地转换485芯片的方向。后续是在转换为发送方向前延时一定时间,再调用HAL_UART_Transmit(),然后再延时一定时间后,转换为接收方向,整个链路总算能正常工作了。
2.2调用HAL_UART_Transmit_DMA()进行发送
DMA发送配置的是正常模式(DMA发送循环模式还没来得及研究),开始是直接把HAL_UART_Transmit()替换成HAL_UART_Transmit_DMA(),数据是能够正常发送出去,但总是比应该应答的少一两个字节,然而在Debug模式下打断点,一步步运行又能正常应答,因此猜测是某处的时序不对,可身边没有示波器直接对信号进行监测;便上网搜索相应的解决方法,果然大家也遇到过同样的问题,经过一番查找与阅读,及修改程序再实测(在调用完HAL_UART_Transmit_DMA()后,延时时间加长,再转换为接收方向)。定位到正常运行时丢应答数据的本质原因是485方向转换的时机不对。可以在调用完HAL_UART_Transmit_DMA()后,延时时间加长再转换485芯片为接收方向,来实现正常应答。但更稳妥以及更高效地处理,是通过串口发送完成中断来进行485芯片方向的转换。
待以为这样处理完后,就调通串口DMA发送了。没想到又遇到了新的问题:发送两帧才正常应答一次。当然又是一番网上搜索,发现网上大部分遇到的是只会发送一次,解决方法也是有多种,但通过仔细查阅,个人感觉只是打补丁,没有找到真正的原因。后来又是一步步仿真调试,发现是如图7标记所示的条件语句就满足了一次,后续huart->gState的值都是HAL_UART_STATE_BUSY_TX,导致后续调用HAL_UART_Transmit_DMA()实际没有正常执行。
![image-20230622201523659](https://i-blog.csdnimg.cn/blog_migrate/86ec3c5b679b8fb18b3e4642aedcc298.png#pic_center)
因此可在串口发送完成中断后重新赋值huart->gState = HAL_UART_STATE_READY就可以了。说来也是巧,之所以会遇到这个问题,是因为我刚好在调用串口中断函数HAL_UART_IRQHandler()前进行了__HAL_UART_CLEAR_FLAG(huart, UART_FLAG_TC)清除串口发送完成中断的操作,不然如图8所示在HAL_UART_IRQHandler()中会调用UART_EndTransmit_IT(),如图9所示也会执行huart->gState = HAL_UART_STATE_READY操作,如此就不需要再手动赋值huart->gState = HAL_UART_STATE_READY。
![image-20230622234515900](https://i-blog.csdnimg.cn/blog_migrate/c0a2814fb74ee9d17964a5f3ecfb0e26.png#pic_center)
![image-20230622202142086](https://i-blog.csdnimg.cn/blog_migrate/c453621bec587780969dc292ffeb6db0.png#pic_center)
程序按照以上方法修改后,串口DMA发送也能正常应答了。但是我实际遇到的现象是发送两帧才正常应答一次,为了不给后续的开发埋下“炸弹”,便决定得找到真正的原因。又是通过一系列的断点,总算定位到问题了,如图10所示,是因为DMA产生了DMA_IT_FE中断,从而导致HAL_UART_Transmit_DMA()会执行如图11的语句。
![image-20230622234748794](https://i-blog.csdnimg.cn/blog_migrate/df2480da924168f38b95ac0de4614490.png#pic_center)
![image-20230622203356432](https://i-blog.csdnimg.cn/blog_migrate/88628f9d23c76030de15a0bc4725f478.png#pic_center)
while-sending-data-on-to-usart1-i-always-get-feif-dma-errors-why
![image-20230626084531685](https://i-blog.csdnimg.cn/blog_migrate/58dcd72a4649ffa2615414a8d3687eb2.png#pic_center)
原来是我提前调用LL_USART_EnableDMAReq_TX(),使能了串口DMA的发送器导致的,如图13所示,技术文档AN4031 4.3节也有详细的说明。
![image-20230622204239552](https://i-blog.csdnimg.cn/blog_migrate/7d7425ed65cde9fec43ad8038ffbea55.png#pic_center)
按照以上要求进行修改后,程序终于能够按照开发的要求正常运行了。至此,MODBUS协议从网口到兼容串口通讯正式开发完成了。
经过此次开发经历,给我最大的感受就是 搜商 也是一件强有力的“利刃”。