news 2026/4/30 2:02:06

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/24 20:53:28

基于springboot + vue酒店管理系统(源码+数据库+文档)

酒店管理 目录 基于springboot vue酒店管理系统 一、前言 二、系统功能演示 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 基于springboot vue酒店管理系统 一、前言 博主介绍&#xff1a;✌️大…

作者头像 李华
网站建设 2026/4/29 3:47:41

25、Linux 下卡拉 OK 系统搭建与文件处理全解析

Linux 下卡拉 OK 系统搭建与文件处理全解析 1. TiMidity 运行与配置 在尝试使用标准包 TiMidity v2.13.2 - 40.1 运行接口时,程序在内存释放调用中崩溃。由于代码经过剥离,很难追踪崩溃原因,而且也不确定该包编译时所依赖的库和代码版本。 为了解决这个问题,可以从源代码…

作者头像 李华
网站建设 2026/4/18 5:27:57

非结构化数据的隐私性较低吗?

从听过任何关于人工智能讨论的调查来看&#xff0c;我们都知道隐私很重要。我们一次又一次地听到人们谈论如何实现某种类型的人工智能系统&#xff0c;但他们担心涉及的隐私问题。有时候&#xff0c;从整体格局的细致角度来看&#xff0c;能让我们看到如何做得更好。例如&#…

作者头像 李华
网站建设 2026/4/23 14:43:17

29、基于 Java Sound 的卡拉 OK 应用与字幕处理

基于 Java Sound 的卡拉 OK 应用与字幕处理 1. SequenceInformation 类 SequenceInformation 类是一个便利类,被多个其他类使用。它存储了序列、歌词行和旋律音符的副本,用于通过用户界面展示歌词和旋律,还存储了歌曲标题、设置音符显示范围的最大和最小音符,以及旋律所…

作者头像 李华
网站建设 2026/4/23 14:39:33

QMCDecode音频格式转换终极指南:Mac音乐解密完整教程

QMCDecode音频格式转换终极指南&#xff1a;Mac音乐解密完整教程 【免费下载链接】QMCDecode QQ音乐QMC格式转换为普通格式(qmcflac转flac&#xff0c;qmc0,qmc3转mp3, mflac,mflac0等转flac)&#xff0c;仅支持macOS&#xff0c;可自动识别到QQ音乐下载目录&#xff0c;默认转…

作者头像 李华