news 2026/4/18 10:54:31

PROFINET 回环测试调试记录|ERTEC ↔ STM32 SPI 对齐问题分析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PROFINET 回环测试调试记录|ERTEC ↔ STM32 SPI 对齐问题分析

1.调试背景和目标

在PROFINET的设备开发中,为了验证通讯链路的完整性,做了PLC到STM32的回环测试,即PLC周期下发数据,然后STM32接收后将数据放在SPI发送区下个周期发回。

测试目标:

  1. 验证SPI收发是否可靠;
  2. 验证 PLC 周期(1ms)下 SeqBack 是否严格递增 1;
  3. 确认数据完整,时序稳定;
  4. 为下个阶段丢包率测试提供基础;

理论上,返回的数据SeqBack应该严格执行每个周期+1的情况,但是在实际测试中,我看到PLC端下个周期采集的数据减去上个周期数据,往往差值为2,偶尔会出现3,下面重点分析这种情况为什么出现?

2.回环链路的整体流程和和关键时序

整体流程:

在PCL的OB30中写SeqPLC数据,周期1ms,递增ERTEC去打你收到PCL输出数据并写道缓存区shadow buffer,ERTEC通过SPI主机模式发送一帧给STM32;STM32通过DMA接收解析,再将收到数据通过SPI回传,ERTEC在下个PROFINET周期将STM32数据填入输入数据,PCL在OB30下一个周期读SwqBack.。

关键时序:

整个系统包含五个不同的“时间点”

环节时间来源是否可控
PLC OB35 采样点PLC 周期任务可控
ERTEC PNIO 周期PNIO 固定同步周期不可控
ERTEC Shadow Buffer 更新点内部逻辑,固定延迟不可控
STM32 SPI DMA 完成时刻SPI 数据完成时刻半可控
PLC Input 更新到程序PROFINET 栈行为不可控

这5个点无法完全对齐,疑似问题的根源。

3.实际测试现象记录

3.1.SPI的NSS每个字节都会短暂的拉高一次(300ns)

通过示波器测试SPI收发数据时发现,在接收一帧数据NSS拉低后,在一帧67字节数据中,每个字节,都会短暂的拉高一次。

影响:STM32无法通过NSS外部中断判断帧边界,只能采用DMA固定字节接收比较稳定;

3.2 PLC和STM32的丢包统计都是异常偏高

使用博途软件监控PLC数据发现,丢包数据大概在百分之50左右;SeqBack和LastSeqBack差值通过监控发现大多数保持在2左右;

使用STM32主循环打印日志显示如下:

total_cnt=141294, lost_cnt=70816, repeat=74536

total_cnt=146294, lost_cnt=73321, repeat=77042

total_cnt=151293, lost_cnt=75824, repeat=79546
这个丢失的数据大概也在百分之50;

通过每次回调打印delta,也就是两次接收差值,显示如下:

delta = 2 delta = 2 delta = 2 delta = 1 delta = 2 delta = 3 delta = 2;

大多数为2,偶尔会出现1和3,这个现象看起来像是数据跳了过去。

3.3 将PLC的OB30周期从1ms改为5ms后,回环测试比较稳定了,不会出现上述现象

分析应该是,STM32接收和处理数据的时机与PROFINET的时机对不上才导致的。

比如,PLC在T0发送数据给ERTEC,ERTEC在T0+delta1发送给STM32,STM32在T0+delta1+delta2解析并回传,ERTEC在下个周期T1才回传给PLC。如果STM32 的解析时机刚好落在两个 PROFINET 周期之间;PLC 的采样点刚好落在更新前后边界;就会出现这种情况。

4 尝试解决方案

方案 1:降低 PLC 序号递增速度(已验证)

例如:

  • PLC 每 5ms 才递增一次 seqPLC;

  • STM32 1ms 解析 → 保证每次递增都会被采样到;

  • 这样 delta = 1 变成正常情况。

这可大幅减少误报,使丢包统计更接近真实情况。


方案 2:ACK 握手机制(待验证)

PLC:

  1. 发新序号

  2. 等 STM32 回环确认

  3. 再递增 seq

确保每个序号一定被 STM32 接收。

5 总结

本次调试中出现的“50% 丢包率”并非链路问题,而是:

PLC、ERTEC、STM32 三者节拍不同步导致 STM32 下采样了 PLC 的序号,引入了大量“伪丢包”。

链路本身是稳定的。 通过调整序号递增策略或丢包判断逻辑,即可得到真实的丢包率。

6 代码附录

(* 回环测试 IF "G_VAR".ResetFlag THEN "DO4" := 1; "G_VAR".seqPLC := 0; "G_VAR".ResetFlag := FALSE; ELSE "DO4" := 0; //每个周期加1 "G_VAR".seqPLC += 1; END_IF; // "G_VAR".seqPLC_Byte[0] := DWORD_TO_BYTE(SHR(IN := "G_VAR".seqPLC, N := 24)); "G_VAR".seqPLC_Byte[1] := DWORD_TO_BYTE(SHR(IN := "G_VAR".seqPLC, N := 16)); "G_VAR".seqPLC_Byte[2] := DWORD_TO_BYTE(SHR(IN := "G_VAR".seqPLC, N := 8)); "G_VAR".seqPLC_Byte[3] := DWORD_TO_BYTE("G_VAR".seqPLC); //写序号到输出区:周期执行 "DO0" := "G_VAR".seqPLC_Byte[0]; "DO1" := "G_VAR".seqPLC_Byte[1]; "DO2" := "G_VAR".seqPLC_Byte[2]; "DO3" := "G_VAR".seqPLC_Byte[3]; //拼接输入区接收到的字节 #SeqBack := SHL(IN := BYTE_TO_DWORD("DI0"), N := 24) OR SHL(IN := BYTE_TO_DWORD("DI1"), N := 16) OR SHL(IN := BYTE_TO_DWORD("DI2"), N := 8) OR BYTE_TO_DWORD("DI3"); //总接收帧数 //判断接收到的数据是否更新 IF #SeqBack <> "G_VAR".LastSeqBack THEN IF "G_VAR".TotalCount = DWORD#0 THEN "G_VAR".LastSeqBack := #SeqBack; "G_VAR".TotalCount := 1; "G_VAR".IncOK_Cnt := 1; ELSE IF #SeqBack > "G_VAR".LastSeqBack THEN #value_diff := #SeqBack - "G_VAR".LastSeqBack; "G_VAR".TotalCount := "G_VAR".TotalCount + #value_diff; IF #value_diff = DWORD#1 THEN "G_VAR".IncOK_Cnt += 1; ELSE //跳帧 "G_VAR".LostCount := "G_VAR".LostCount + (#value_diff - DWORD#1); END_IF; //更新接收数据 "G_VAR".LastSeqBack := #SeqBack; ELSIF #SeqBack = "G_VAR".LastSeqBack THEN ; ELSE ; END_IF; END_IF; END_IF; //计算丢包率 IF "G_VAR".TotalCount > DWORD#0 THEN "G_VAR".LossRate := ((DWORD_TO_DINT("G_VAR".LostCount) * 1000) / DWORD_TO_DINT("G_VAR".TotalCount)); ELSE "G_VAR".LossRate := 0; END_IF; *)
uint8_t parse_frame(uint8_t *rx_buf, uint8_t *tx_buf, uint8_t *dataOffset) { // for (int offset = 0; offset < SPI_FRAME_LEN; offset++) { if (rx_buf[offset] != SPI_FRAME_HEAD) continue; uint8_t len = rx_buf[(offset + 1) % SPI_FRAME_LEN]; if (len == 0 || len > SPI_FRAME_LEN) continue; uint16_t checksum_pos = (offset + 2 + len) % SPI_FRAME_LEN; uint8_t calc_sum = len; for (int i = 0; i < len; i++) { calc_sum += rx_buf[(offset + 2 + i) % SPI_FRAME_LEN]; } uint8_t recv_sum = rx_buf[checksum_pos]; if (recv_sum != calc_sum) { continue; } // now = micros(); // dt = now - last_us; // last_us = now; //printf("offset = %d\n", offset); //判断是否是第一帧数据 if(rx_buf[(offset + 6) % SPI_FRAME_LEN]) { last_seq = 0; lost_cnt = 0; total_cnt = 0; repeat_or_back_cnt = 0; first_frame = 1; //tx_ready = 1; } //处理接收到的数据 uint8_t seq_bytes[4]; seq_bytes[0] = rx_buf[(offset + 2) % SPI_FRAME_LEN]; //PLC发送过来的序列号 seq_bytes[1] = rx_buf[(offset + 3) % SPI_FRAME_LEN]; seq_bytes[2] = rx_buf[(offset + 4) % SPI_FRAME_LEN]; seq_bytes[3] = rx_buf[(offset + 5) % SPI_FRAME_LEN]; uint32_t cur_seq = parse_seqPLC(seq_bytes); if(first_frame) { first_frame = 0; last_seq = cur_seq; return 0; } else { if(cur_seq > last_seq) { uint32_t delta = cur_seq - last_seq; total_cnt += delta; if(delta > 1) { lost_cnt += (delta - 1); } } else { repeat_or_back_cnt++; } } last_seq = cur_seq; /* //回环数据 tx_buf[(offset + 2) % SPI_FRAME_LEN] = seq_bytes[0]; tx_buf[(offset + 3) % SPI_FRAME_LEN] = seq_bytes[1]; tx_buf[(offset + 4) % SPI_FRAME_LEN] = seq_bytes[2]; tx_buf[(offset + 5) % SPI_FRAME_LEN] = seq_bytes[3]; */ //memset( frame_buf_rx, 0, SPI_FRAME_LEN); //发送测试数据 // test_data_tx++; // // uint8_t test_sum = 0; // tx_buf[(offset + 2) % SPI_FRAME_LEN] = (uint8_t)((test_data_tx >> 24) & 0xFF); // tx_buf[(offset + 3) % SPI_FRAME_LEN] = (uint8_t)((test_data_tx >> 16) & 0xFF); // tx_buf[(offset + 4) % SPI_FRAME_LEN] = (uint8_t)((test_data_tx >> 8) & 0xFF); // tx_buf[(offset + 5) % SPI_FRAME_LEN] = (uint8_t)(test_data_tx & 0xFF); // for(int i = 2; i < 6; i++) // { // test_sum += tx_buf[(offset + i) % SPI_FRAME_LEN]; // } // tx_buf[(offset + 6) % SPI_FRAME_LEN] = test_sum; // for (int i = 0; i < SPI_FRAME_LEN; i++) // { // tx_buf[(offset + i) % SPI_FRAME_LEN] = rx_buf[(offset + i) % SPI_FRAME_LEN]; // //printf("tx[%d]: 0x%02Xrx:0x%02X\r\n", i, tx_buf[i],rx_buf[i]); // } *dataOffset = offset; return 0; } return 1; }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/18 5:22:57

语音识别技术的新纪元:从听懂到理解的跨越

语音识别技术的新纪元&#xff1a;从听懂到理解的跨越 【免费下载链接】FunASR A Fundamental End-to-End Speech Recognition Toolkit and Open Source SOTA Pretrained Models. 项目地址: https://gitcode.com/gh_mirrors/fu/FunASR 在智能语音技术飞速发展的今天&…

作者头像 李华
网站建设 2026/4/18 8:33:21

IP6529_Q1至为芯支持PD快充的45W车规级DC-DC芯

英集芯IP6529_Q1是一款适用于车载USB Type-C PD充电器方案的车规级DC-DC降压芯片&#xff0c;符合AEC-Q100 Grade 2标准&#xff0c;工作温度范围为-40℃至105℃&#xff0c;可在极端温度环境下稳定工作。集成PD2.0/3.1、QC2.0/3.0/3及Apple协议等所有主流快充协议。提供最大45…

作者头像 李华
网站建设 2026/4/17 9:01:12

CSDNGreener:彻底净化CSDN浏览体验的专业工具

CSDNGreener是一款专为CSDN用户设计的浏览器扩展脚本&#xff0c;通过全面的页面优化和内容过滤功能&#xff0c;为用户提供纯净高效的技术阅读环境。这款脚本经过多年持续更新&#xff0c;已经成为CSDN平台上最受用户欢迎的优化工具之一。 【免费下载链接】CSDNGreener 《专 业…

作者头像 李华
网站建设 2026/4/18 8:30:05

Kubernetes Ingress:管理集群外部访问的入口网关

在k8s之服务Service章节&#xff0c;我们详细的介绍了Service的组成以及相关的原理。Service可以将自身的服务暴露出去&#xff0c;给集群内部服务或者给外部服务去使用&#xff0c;或者将外部服务分装为一个service&#xff0c;供给集群内部服务使用。而今天介绍的ingress其实…

作者头像 李华
网站建设 2026/4/18 8:50:24

5分钟快速上手Google Apps Script OAuth2库:终极认证指南

5分钟快速上手Google Apps Script OAuth2库&#xff1a;终极认证指南 【免费下载链接】apps-script-oauth2 An OAuth2 library for Google Apps Script. 项目地址: https://gitcode.com/gh_mirrors/ap/apps-script-oauth2 Google Apps Script OAuth2库是专为Google Apps…

作者头像 李华
网站建设 2026/4/18 8:29:58

B站音频下载终极指南:用BiliFM打造你的专属音频图书馆

B站音频下载终极指南&#xff1a;用BiliFM打造你的专属音频图书馆 【免费下载链接】BiliFM 下载指定 B 站 UP 主全部或指定范围的音频&#xff0c;支持多种合集。A script to download all audios of the Bilibili uploader you love. 项目地址: https://gitcode.com/jingfel…

作者头像 李华