问题出现

公司产品采用了 Xilinx Zynq 7z010 芯片,用于运动控制以及网络通讯。两周前,测试过程中发现网络通信会小概率出错,TCP 收到的数据 CRC 校验失败,无法稳定复现。

设备平台概述:

  1. CPU: Cortex-A9 双核
  2. RAM: 1GB DDR3
  3. 操作系统: FreeRTOS
  4. 网络协议栈: lwip211

定位过程

怀疑应用层数据处理问题

TCP 是二进制数据流,每个包的长度不固定,应用层也许会写错。于是我修改了应用层的处理方案,手动构造了定长的数据包,虽然会导致 TCP 流量大幅上涨,但是逻辑看起来更清晰。

然而,修改后,似乎由于流量变大了,原来小概率出现的错误,现在大概率会出现!这也给 Debug 带来了有利的一面。

怀疑网络通讯链路电磁干扰问题

但是这个怀疑方向很快就被否定了,因为我用了 TCP 协议,理论上只可能超时,不可能出错。

怀疑 lwip 接口调用问题

lwip 有多个 TCP API,之前用的 Socket API,我尝试换成了 RAW API,但是问题依旧。

在调试的过程中,我尝试在网络链路的每一层数据打印出来,惊奇地发现,在数据链路层,数据是正确的!然而 lwip 的代码冗杂且数 MB 数据中才会出现几个错误位,于是我暂时没有考虑一层层分析代码。

怀疑与其他线程之间存在干扰,或者存在数组越界访问

这样 Debug 就很简单了。我关闭了所有其他的线程,不出所料,Bug 消失了。

一点点放开线程,发现是一个运动控制的硬中断造成的 Bug。

然后再“二分法”排除代码,结果排除到最后,仅仅是一行代码:

1
2
// b, c 也为 long long
long long a = (long long)((double)b * (double)c);

这让我大跌眼镜,因为实验证明,把这句话删了,TCP 通讯就正常了。

怀疑是浮点运算的问题

更加让我迷惑的是,把上述语句改下,同样也没问题了:

1
long long a = b * c;

进一步定位:我在另一个线程中添加了浮点运算,并把这个有影响的中断关闭,TCP 通讯同样出问题了。

就此,几乎可以确定是浮点数运算造成的问题了。

问题小结

一句话描述问题:在中断或某个线程中进行浮点数操作,会导致另一个 TCP 通讯线程数据出错。

说实话,我当时也没法理解其中的联系。

只不过我们用的芯片自带双精度 FPU(浮点运算单元),也许是 FPU 的问题?

解决过程

查找资料

关键词 lwip tcp receive wrong datazynq float process corrupt memory 等关键词,都没有找到有价值的解决方案。

求助 Xilinx 技术支持

果然用微信联系的技术支持不靠谱,上午说帮忙复现,下午就没信了。

求助朋友圈资深开发者

只可惜他们都是互联网界的大佬,只有我在嵌入式开发领域摸爬滚打,他山之玉难以攻石。

求助 V2EX 网友

发了帖子 在这里

V 站网友给了非常有价值的线索:

  1. 网友 A 称他们使用同样的平台出现过类似的问题。他们的解决方案是,进行浮点数操作之前,关闭所有的中断;
  2. 网友 B 分析可能 正在计算浮点数的时候,刚好发生了 systick 线程切换,但是线程切换过程中,没有保存 /恢复浮点寄存器
  3. 网友 C 更是找到了相关文章:

    “Some GCC libraries optimise memory copy and memory set (and possibly other) functions by making use of the wide floating point registers. Therefore, by default, any task that uses functions such as memcpy(), memcmp() or memset(), or uses a FreeRTOS API function such as xQueueSend() which itself uses memcpy(), will inadvertently corrupt the floating point registers.”

真可谓一针见血,TCP 协议栈中大量使用了 memcpy,而 memcpy 又使用了 FPU 的寄存器,极有可能在 TCP 处理数据的过程中,另一个中断来了,进行了浮点运算并修改了 FPU 的寄存器,以致 TCP 数据出错。

同样根据网友的指点,看了这篇文章 Using FreeRTOS on ARM Cortex-A9 Embedded Processors,原来 FreeRTOS 自身已经考虑了 FPU 与上下文切换相关的问题,只是要我们将 configUSE_TASK_FPU_SUPPORT 这个宏定义为 2 即可。

问题解决

花了些时间进行 FPU 寄存器相关的搜索,依照 这篇文章 ,对 FPU 的寄存器做了相关处理,总结起来就三行代码:

第一行代码

在中断响应函数开头添加以下代码:

1
__asm("VPUSH {d0-d15}"); // FPU 寄存器入栈

第二行代码

在中断响应函数末尾添加以下代码:

1
__asm("VPOP {d0-d15}"); // FPU 寄存器出栈

第三行代码

FreeRTOS 启用 FPU 支持相关宏:

1
#define configUSE_TASK_FPU_SUPPORT 2

至此,问题解决。