第4章 以太网CAPL编程入门:CAPL语言基础、事件处理函数(on ethernetPacket)、CAPL中的以太网数据结构

好,咱们进入以太网CAPL编程的第一课。说实话,很多从CAN转过来的工程师,第一次看到以太网CAPL代码时都会有点懵——怎么多了这么多结构体?怎么还有IP地址和端口号?别急,我带你一步步捋清楚。

4.1 CAPL语言基础——你其实已经会了大半

如果你写过CAN的CAPL脚本,那以太网的CAPL对你来说就是换了个皮。核心语法完全一样:

  • 变量声明:还是用 intfloatchar,只是多了几个以太网专用的数据类型
  • 控制结构if-elseforwhile,一个都没变
  • 函数定义:还是 void MyFunction() { ... } 这种写法

我个人习惯把以太网相关的变量名加上前缀,比如 ethPktipAddr,这样一眼就能看出是网络相关的。你在项目里也可以定个命名规范,后面维护起来会轻松很多。

核心要点:CAPL是事件驱动的。你写的大部分代码,都是在响应某个“事件”——比如收到一个报文、定时器到期、按键被按下。

4.2 事件处理函数——on ethernetPacket 是主角

在CAN里,我们最常用的是 on message。到了以太网,对应的就是 on ethernetPacket。它的写法长这样:

on ethernetPacket *
{
  // 这里写处理逻辑
  write("收到一个以太网报文,长度:%d", this.len);
}

这个 * 表示匹配所有以太网报文。你也可以指定只处理某种类型的报文,比如:

on ethernetPacket 0x0800  // 只处理IPv4报文
{
  // ...
}

嗯,这里要注意:0x0800 是以太网帧的 EtherType,IPv4 就是 0x0800,ARP 是 0x0806,IPv6 是 0x86DD。我在项目中遇到过好几次,有人把 EtherType 和 IP 协议号搞混了——这两个是完全不同的东西,千万别弄反了。

小技巧:调试阶段建议先用 on ethernetPacket * 把所有报文都抓下来,打印出基本信息,确认网络环境正常后,再缩小范围到特定类型。这样可以快速定位问题。

4.3 CAPL中的以太网数据结构——你得认识这几个“箱子”

以太网报文在CAPL里是用结构体表示的。最常用的几个,我给你列出来:

数据结构 说明 常用字段
EthernetPacket 完整的以太网帧 len, source, dest, type
Ipv4Packet IPv4报文(嵌套在EthernetPacket里) source, dest, protocol, payload
TcpPacket TCP报文段 srcPort, dstPort, seqNumber
UdpPacket UDP数据报 srcPort, dstPort, length

你想想看,这些结构体其实是嵌套的。一个完整的报文,从外到里是:

  1. 以太网帧头(MAC地址、EtherType)
  2. IP头(源IP、目的IP、协议类型)
  3. 传输层头(TCP/UDP端口号)
  4. 应用层数据(真正的有效载荷)

在CAPL里访问这些字段,就像剥洋葱一样:

on ethernetPacket *
{
  // 先拿到以太网帧
  EthernetPacket eth = this;
  
  // 如果是IPv4,再取IP层
  if (eth.type == 0x0800)
  {
    Ipv4Packet ip = eth.ipv4;
    write("源IP: %d.%d.%d.%d", ip.source[0], ip.source[1], ip.source[2], ip.source[3]);
    
    // 如果是UDP,再取UDP层
    if (ip.protocol == 17)  // UDP协议号是17
    {
      UdpPacket udp = ip.udp;
      write("目的端口: %d", udp.dstPort);
    }
  }
}

避坑指南:我曾经在项目里犯过一个低级错误——直接访问 this.ipv4.udp.dstPort,结果程序崩溃了。为什么?因为不是所有报文都是IPv4+UDP。如果收到一个ARP报文,它根本没有IP层和UDP层。所以,一定要先判断类型,再逐层访问

4.4 实战小例子——抓取并打印UDP报文

光说不练假把式。咱们写一个完整的例子,把收到的UDP报文信息打印出来:

on ethernetPacket *
{
  // 只处理IPv4报文
  if (this.type != 0x0800) return;
  
  Ipv4Packet ip = this.ipv4;
  
  // 只处理UDP报文
  if (ip.protocol != 17) return;
  
  UdpPacket udp = ip.udp;
  
  write("=== UDP报文 ===");
  write("源MAC: %02x:%02x:%02x:%02x:%02x:%02x", 
        this.source[0], this.source[1], this.source[2],
        this.source[3], this.source[4], this.source[5]);
  write("源IP: %d.%d.%d.%d:%d", 
        ip.source[0], ip.source[1], ip.source[2], ip.source[3], udp.srcPort);
  write("目的IP: %d.%d.%d.%d:%d", 
        ip.dest[0], ip.dest[1], ip.dest[2], ip.dest[3], udp.dstPort);
  write("UDP长度: %d 字节", udp.length);
  write("有效载荷: %d 字节", udp.length - 8);  // UDP头固定8字节
}

这段代码我经常用在项目调试初期。说白了,就是先确认网络报文能不能正常到达,源和目的对不对,端口号有没有配错。有一次客户说他们的设备收不到数据,我跑了一下这个脚本,发现目的端口号写反了——源和目的颠倒了。这种问题,没有抓包脚本的话,排查起来真的很头疼。

4.5 几个容易踩的坑

  • 字节序问题:以太网用的是大端序(网络字节序)。你在打印IP地址或端口号时,CAPL已经帮你转换好了,但如果你要手动拼装报文,一定要用 htons()htonl() 做转换。
  • 报文长度this.len 是整个以太网帧的长度,包括头部和CRC。但实际有效数据可能比这个短,因为网卡可能做了填充。别直接用 this.len 去访问载荷,要用 this.payload.length
  • 性能问题:如果网络流量很大(比如100Mbps),on ethernetPacket * 里别写太复杂的逻辑,否则会丢包。我一般只在里面做简单的过滤和统计,复杂的解析放到定时器里去做。

我的建议:刚开始学以太网CAPL时,先别急着写复杂的逻辑。先写一个最简单的 on ethernetPacket * { write("收到报文"); },确认环境通了,再逐步加功能。这样出了问题,你知道是环境的问题还是代码的问题。

好了,这一章的内容就这些。以太网CAPL编程其实不难,关键是理解数据结构的嵌套关系,以及养成逐层访问的习惯。下一章咱们会讲怎么用CAPL发送以太网报文,到时候你会看到,发送比接收还要简单一些。