1.调试背景和目标
在PROFINET的设备开发中,为了验证通讯链路的完整性,做了PLC到STM32的回环测试,即PLC周期下发数据,然后STM32接收后将数据放在SPI发送区下个周期发回。
测试目标:
- 验证SPI收发是否可靠;
- 验证 PLC 周期(1ms)下 SeqBack 是否严格递增 1;
- 确认数据完整,时序稳定;
- 为下个阶段丢包率测试提供基础;
理论上,返回的数据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:
发新序号
等 STM32 回环确认
再递增 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; }