AUTOSAR入门-SoAd模块和TcpIp模块

原创 thatway 那路谈OS与SoC嵌入式软件 2022-05-25 08:03

AUTOSAR入门-SoAd模块和TcpIp模块

有一句话叫做“  

All is in the code
”,网上好像没找到是谁说的,算我杜撰的
。这也是我坚信的一个观点:一切都在代码里。
不管你做什么技术,什么业务,只要你跟软件相关有代码,直接看代码就可以反推
出来技术是怎么设计实现的。比如一个高境界的C程序员,那么别管什么技术,比如操作系统、各种应用app、驱动、无线/网络协议等,只要是C写的,有代码,就都可以搞,而且可以迅速上手,看海量C代码如喝白水
。因为万变不离其宗,都是C语言的逻辑在支撑,正所谓“无招胜有招
”。

按照这个思路,我们先不看SoAd模块和TcpIP模块是什么,直接分析代码来实践下。在[AUTOSAR入门-基于以太网诊断](http://mp.weixin.qq.com/s?__biz=MzUzMDMwNTg2Nw==&mid=2247483815&idx=1&sn=8603d2994641cbbb02d48ac0313d074b&chksm=fa528783cd250e9524080071c93ed2d7db49a074824f264587bd317f3fda28e746657c7161de&scene=21#wechat_redirect)  

实验中,报文交互的流程为:
网卡驱动-》网络接口-》网络协议栈-》SoAd模块
-》
DoIP模块-》PduR模块-》Dcm模块
。其中
蓝色
的部分还没讲,跟
网络协议栈
相关,本次文章全都给讲了。

  1. 嵌入式网络通信基础

提起  

嵌入式
,我的感觉是有一定的门槛的,需要计算机软硬件很多的知识,不像搞java应用,非科班学几个月也可以找工作。大概十年前当时的嵌入式多么的火爆,而今却成了全民AI
的互联网,也是让人感慨三十年河东三十年河西。下面是我的一点偏见:
那些深度学习AI
的东西的确是能产生很大的经济效益
,但是在技术方面除了设计算法的大牛,大多数从业者只是调参做标记
等的底层劳动力,
调得一手好参
,但是真正的技术学到了多少,国家被卡脖子的技术又懂得多少,技术壁垒有多高,能算得上多高档次的人才。
当互联网被整治(国家不让互联网躺着挣钱,把国家的事干了),这股热潮可能会褪去。有一句狠话:当潮水褪去,才知道谁在裸泳

下面扯点我走上嵌入式这条路的过程,记得十几年前大二的时候我电脑上就安装了ubuntu,当时流行里面的四个桌面围成鱼缸养鱼,觉得很炫酷。然后计算机专业学校各种软硬件知识的课都有,杂而不精,对那些硬件理论挺有兴趣,特别是软硬件结合的东西。对界面软件倒是兴趣不大。然后毕业前来到了北京,在清华前面的小民房里面的亚嵌培训班,我还听过一周的课,也没有疫情还可以经常去清华校园逛。一晃很多年,亚嵌的老板老早也出国移民了,后来就成了华清远见的天下。岁月流逝,还是留下来了一些东西,比如这本书:《**Linux c一站式编程**  


http://staff.ustc.edu.cn/~guoyan/os12/LinuxC.pdf

首先进入Linux c编程,熟悉gcc和makefile。然后主要是  

一些网络编程的基础知识,这一部分是嵌入式开发的基本功。我们翻到36章**TCP/IP协议基础**

书上举例的应用层协议是**FTP**  

,这里我们替换为我们的DoIP
进行分析,DoIP也是一个应用层协议
,可以基于TCP进行传输。理论就是上面的这个图,客户端和服务器通过网线或者无线网络进行通信。就像A给B送一封信
,但是A写完信后还要弄个信封
装起来,然后帖个邮票等操作,然后邮递员把信送到B手里,B还要拆信
封,才能看到信的内容。DoIP报文可以看做是信,那么
TCP/IP
这一套东西就是信封
,以太网就是邮政送信的。

一条报文可以看成一块内存buffer,里面都是按顺序排列的**二进制0和1**  

,和代码里面的
结构体
是一一对应的,人操作报文的时候是通过代码里面的结构体,机器传输报文的时候是通过二进制的0和1。我们写代码的时候就可以把一个报文看成一个结构体
就可以了。我看了下代码,我们as上的客户端和服务器DoIP模块的代码都没有用结构体(
编程水平
有点low**)**
,我修改了下client程序的代码,添加了DoIP报文的结构体(
已上
库),在as/socket_tool/doip.h中:

这个data0
是什么?首先data在这个结构体中不占空间,是一个指针,可以指向另一个报文结构体,这里我们指向UDS报文。这个是报文套报文
的一个典型用法,在代码里面解析或者组装报文
的时候,就知道它的便利性了。

接下来看,**第37 章****socket编程**  

。这个就是上学的时候学的《计算机网络
》课程,现在想想那时老师真是太扯了,只讲理论,
为啥不讲一下实践
,还是说那时水平太菜,老师怕学生实践不了。近来我看清华的一些本科生OS教程,感觉老师真的太重要了,陈渝老师那书
直接上代码实践,
理论知识根本就是小菜一碟不值得专门讲的,随意就穿插进去了
。真是感叹清华老师的底蕴实在太厚了,而很多高校只讲课本理论,是不是因为那老师之前都没见过那门课,自己找个课本学习下就可以教学了,自己都不知道怎么实践的。这样这样看来上个好大学很重要
啊,不然上个电子类的大学
也是可以的。

上面扯的有点多,下面就开始  

Show me the code
! 先来看个下面的TCP协议通信流程图
,里面直接放了要执行的函数,分为客户端和服务器端,妥妥的操作指导。

2. Client程序

Client程序是在Linux下用c语言写的,代码路径为:  

as/socket_tool/client.c

这里为什么可以在Linux下写一个程序,为啥不是在AS内部的代码里面实现clinet。这里是因为
网络交互是以01二进制报文的形式,通过以太网交互的
,而以太网的两端的软件不论
什么设备、平台、语言
都可以,基于此可以实现万物互联。所谓的分布式软总线
,就是这么个概念,不论什么设备什么平台什么语言通过网络接入进来就可以通信。下面具体看c实现的代码:

memset(&sockaddr,0,sizeof(sockaddr));
sockaddr.sin_port= htons(port);
sockaddr.sin_family= AF_INET;
socketfd= socket(AF_INET,SOCK_STREAM,0);//建立socket

//连接服务器
inet_pton(AF_INET,servInetAddr,&sockaddr.sin_addr);
if((connect(socketfd,(structsockaddr*)&sockaddr,sizeof(sockaddr))) < 0 )
{
       printf("connecterror %s errno: %d\n",strerror(errno),errno);
       exit(0);
}
//发送数据
send_num =send(socketfd,sendline,send_len,0);

socketfd = socket
(AF_INET,SOCK_STREAM,0);是建立一个socket,是建立在tcp/ip协议上的,SOCK_STREAM
表示面向字节流是TCP
SOCK_DGRAM
表示数据流是UDP
。建立socket后,clinet会调用connnect
函数连接服务器,sockaddr参数里面回携带服务器的ip和端口。
连接成功的话就会调用send
函数发送DoIP报文。报文的组装可以自己查看代码。

3. AS网络协议栈初始化和运行

再看下这个图,上面client是在**linux**  

上发出去的,linux把剩下的事情都包了,然后通过以太网发送给了as
,在as里面,第一个程序就是
以太网驱动程序
,就是我们这里的pci网卡驱动。这个网卡是我们通过qemu添加上去的,见building.py里面代码。

驱动是在系统启动的时候进行初始化的,下面先看下网卡驱动程序初始化的过程。系统启动后大概会执行下面的过程:  

TASK(SchM_Startup)-》EcuM_StartupTwo-》EcuM_AL_DriverInitTwo-》SoAd_Init-》TcpIp_Init-》LwIP_Init-》tcpip_init

tcpip_init
在as/release/download/lwip开源软件中实现。
具体的as里面
的os和初
始化
单独一篇文
章再说下。

3.1 SoAd_Init

这里我们关注SoAd_Init
函数,代码在

com/as.infrastructure/communication/SoAd/SoAd.c

SocketAdminList[i].SocketState = SOCKET_INIT;
SocketAdminList[i].SocketConnectionRef = &SoAd_Config.SocketConnection[i];

SoAd_Config
是配置文件,在

com/as.application/common/config/SoAd_Cfg.c
中定义

const SoAd_ConfigType SoAd_Config =
{
   .SocketConnection = SoAd_SocketConnection,
    .SocketRoute =SoAd_SocketRoute,
   .DoIpTargetAddresses = SoAd_DoIpTargetAddresses,
    .DoIpTesters=SoAd_DoIpTesters,
   .DoIpRoutingActivations = SoAd_DoIpRoutingActivations,
   .DoIpRoutingActivationToTargetAddressMap =SoAd_DoIpRoutingActivationToTargetAddressMap,
    .PduRoute =SoAd_PduRoute
};

static const SoAd_SocketConnectionType SoAd_SocketConnection [SOAD_SOCKET_COUNT] =
{
        {       /* for DCM */
               .SocketId= 0,
               .SocketLocalIpAddress= "172.18.0.200",
               .SocketLocalPort= 13400,
               .SocketProtocol= SOAD_SOCKET_PROT_TCP,
               .AutosarConnectorType= SOAD_AUTOSAR_CONNECTOR_DOIP,
        }, 
};

可以看到id 172.18.0.200是DoIP使用的,端口号是13400,使用的TCP协议。要修改了可以在这里修改。

接下来就是服务器端socket的类型操作了:

系统启动后TASK(SchM_BswService)
会循环调用

SoAd_MainFunction
(void),主要执行scanSockets
();进入状态机

初始化的的状态是SOCKET_INIT 执行socketCreate(i);

sockFd = SoAd_CreateSocketImpl(AF_INET, sockType, 0);
--》lwip_socket(domain, type,protocol);
    --》onn = netconn_new_with_callback(NETCONN_TCP,event_callback);
        --》#define netconn_new_with_callback(t, c)netconn_new_with_proto_and_callback(t, 0, c)
        --》msg.function = do_newconn;-》pcb_new(msg);#分配pcb

SoAd_BindImpl(sockFd,SocketAdminList[sockNr].SocketConnectionRef->SocketLocalPort, SocketAdminList[sockNr].SocketConnectionRef->SocketLocalIpAddress);
    --》lwip_ioctl(s, FIONBIO, &on);
SoAd_ListenImpl(sockFd, 20) == 0
    --》lwip_listen(s, backlog);
SocketAdminList[sockNr].SocketState = SOCKET_TCP_LISTENING;

经过设置socket,状态改为监听,会执行socketAccept(i);函数

clientFd =SoAd_AcceptImpl(SocketAdminList[sockNr].SocketHandle, &RemoteIpAddress,&Rem     otePort);
    --》lwip_accept(s, (struct sockaddr*)&client_addr, (socklen_t *)&addrlen);
if( clientFd != (-1)){
    SocketAdminList[sockNr].SocketState= SOCKET_TCP_READY;
}

如果有数据则状态改为TCP接收:SOCKET_TCP_READY

没有数据,打印lwip_accept(0): returning EWOULDBLOCK

可以看到上面是server端创建了socket,等待client的连接。

3.2 LwIP_Init

TcpIp_Init
函数里面,主要执行了LwIP_Init
函数,下面主要看这个函数,位置在

com/as.infrastructure/arch/common/lwip/sys_arch.c

ethernet_configure(); #没找到定义的地方
re_sys_init(); #线程个数信息初始化
tcpip_init(tcpip_init_done, NULL); #初始化运行lwip的tcpip线程
GET_BOOT_IPADDR; #获取网卡的IP地址
GET_BOOT_NETMASK;
GET_BOOT_GW;

ethernet_set_mac_address(macaddress); #设置网卡的mac地址
/* Add network interface to the netif_list */
netif_add(&netif,&ipaddr, &netmask, &gw, NULL, &ethernetif_init, &tcpip_input);
/*  Registers thedefault network interface.*/
netif_set_default(&netif);#注册默认网络,跟路由相关
netif_set_addr(&netif,&ipaddr , &netmask, &gw); #设置地址

/* netif is configured */
netif_set_up(&netif);
ethernet_enable_interrupt();
netbios_init();

return &netif;

tcpip_init拉起来了lwip
GET_BOOT_IPADDR获取到写死的IP地址为:
#define LWIP_AS_LOCAL_IP_ADDR    "172.18.0.200"
#define LWIP_AS_LOCAL_IP_NETMASK"255.255.255.0"
#define LWIP_AS_LOCAL_IP_GATEWAY "172.18.0.1"
define GET_BOOT_IPADDR ipaddr.addr =ipaddr_addr(LWIP_AS_LOCAL_IP_ADDR)

netif_add(&netif,&ipaddr, &netmask, &gw, NULL, &ethernetif_init,&tcpip_input);

添加网卡,把网卡的ip等信息设置好,初始化执行ethernetif_init,

有包来驱动调用tcpip_input

ethernetif_init函数在

as/com/as.infrastructure/communication/Pci/pci_asnet.c

PciNet_Init(netif->gw.addr, netif->netmask.addr,netif->hwaddr,&mtu);中

pdev = find_pci_dev_from_id(0xcaac,0x0002); #找到网卡设备,0x0001是can用的,1是网卡

__iobase = pci_get_memio(pdev, 1); #获取寄存器地址
Irq_Save(imask);
enable_pci_resource(pdev);
pci_register_irq(pdev->irq_num,Eth_Isr);
enable_pci_interrupt(pdev);
writel(__iobase+REG_GW, gw);
writel(__iobase+REG_NETMASK, netmask);
writel(__iobase+REG_CMD, 0);

Irq_Restore(imask);

irq_num的中断号为11,Eth_Isr是中断发生后处理函数,这里靠lwip进程轮询执行,中断没触发。

寄存器偏移地址定义如下:

enum{
    REG_MACL      = 0x00,
    REG_MACH      = 0x04,
    REG_MTU       = 0x08,
    REG_DATA      =0x0C,
    REG_LENGTH    = 0x10,
    REG_NETSTATUS = 0x14,
    REG_GW        = 0x18,
    REG_NETMASK   = 0x1C,
    REG_CMD       = 0x20,
    REG_ADAPTERID =0x24,
};

网卡驱动和ip 端口已经绑定好了,

3.3 TaskLwip激活

系统启动的时候,在EcuM_Init里面调用KSM_INIT() ,然后执行KsmLwipIdle_Init

在as/com/as.infrastructure/system/kernel/Os.c中,LwipIdle初始化之后就会轮询执行功能函数。

static const KsmFunction_Type KsmLwipIdle_FunctionList[4] = 
{ 
    KsmLwipIdle_Init                   , 
    KsmLwipIdle_Start                  ,    
    KsmLwipIdle_Stop                   ,    
    KsmLwipIdle_Running                ,    
}; 

const KSM_Type KSM_Config[KSM_NUM] =                                                  { 
    { /* LwipIdle */ 
        4,   
        KsmLwipIdle_FunctionList 
    },   
    { /* CANIdle */ 
        4,   
        KsmCANIdle_FunctionList 
    },   
}; 

void KsmStart(void)
{
  KsmID_Type i;
  for(i=0;i<KSM_NUM;i++)
  {
    KSM_Config[i].Ksm[KSM_S_START]();
  }
}

4. PCI网卡驱动程序读取数据

激活lwip任务后,会定时执行KsmLwipIdle_Running
函数,定义如下:

KSM(LwipIdle,Running)
{
#ifdef USE_LWIP
    Eth_Isr();
#endif
}

Eth_Isr()在
com/as.infrastructure/communication/Pci/pci_asnet.c
中实现

之前网卡寄存器的地区获取保存到__iobase =pci_get_memio(pdev, 1);

flag =readl(__iobase+REG_NETSTATUS);
if(flag&FLG_RX)
{
    struct pbuf *p = low_level_input();
    ethhdr = (struct eth_hdr *)p->payload;
    witch(htons(ethhdr->type)) {
        case ETHTYPE_IP:
        if (ethernet_input(p,netif) != ERR_OK) {

            LWIP_DEBUGF(NETIF_DEBUG,("ethernetif_input: IP input error\n"));
            pbuf_free(p);
            p = NULL;
}                 

low_level_input函数从寄存器里面读取数据
len = len2 =readl(__iobase+REG_LENGTH);
pkbuf[pos] = readl(__iobase+REG_DATA);

总结下驱动处理数据的主要过程如下:

Eth_Isr
  struct pbuf *p = low_level_input();
  ethhdr = (struct eth_hdr*)p->payload;
  switch(htons(ethhdr->type)){
    case ETHTYPE_IP:
      ethernet_input(p,netif)
  }

5. Lwip程序

ethernet_input把读取的数据送入入lwip开源软件处理,
release/download/lwip/src/netif/etharp.c

这里需要注意lwip的代码在release/download/lwip
下,但是做了一个软链接到com里面了,例如:

com/as.infrastructure/system/net/lwip
/lwip/src/core/ipv4/ip.c这个文件软链接到:

release/download/lwip/src/core/ipv4/ip.c

ethernet_input传入的是eth报文,然后分类处理

#define ETHTYPE_ARP       0x0806U
#define ETHTYPE_IP        0x0800U
#define ETHTYPE_VLAN      0x8100U
#define ETHTYPE_PPPOEDISC0x8863U  /* PPP Over Ethernet DiscoveryStage */
#define ETHTYPE_PPPOE     0x8864U /* PPP Over Ethernet Session Stage */

如果是IP类型:

ip_input(p, netif);
#define IP_PROTO_ICMP    1
#define IP_PROTO_IGMP    2
#define IP_PROTO_UDP     17
#define IP_PROTO_UDPLITE 136
#define IP_PROTO_TCP     6

Lwip模块具体处理流程为:

ethernet_input
ethhdr = (struct eth_hdr *)p->payload;
type = ethhdr->type;
switch (type) {
case PP_HTONS(ETHTYPE_IP):
ip_input(p, netif);
}

ip_input
#define IPH_PROTO(hdr) ((hdr)->_proto)
iphdr = (struct ip_hdr *)p->payload;
switch (IPH_PROTO(iphdr)) {
case IP_PROTO_TCP:
tcp_input(p, inp);
}

tcp_input
     struct tcp_pcb*pcb;
for(pcb = tcp_active_pcbs; pcb != NULL; pcb =pcb->next) {
if (pcb->remote_port == tcphdr->src &&
      pcb->local_port == tcphdr->dest &&
      ip_addr_cmp(&(pcb->remote_ip), &current_iphdr_src) &&
      ip_addr_cmp(&(pcb->local_ip), &current_iphdr_dest)) {
break;
}
}

tcp_process(pcb);
  switch (pcb->state) {
  case SYN_RCVD:
    tcp_receive(pcb); //收到报文后存储在lwip协议栈中
  }

6. SoAd模块处理

SoAd模块初始化后,TCP的13400端口会处于监听状态,

会循环执行scanSockets中socketAccept()函数来监听,如果lwip协议栈有数据则会返回clientFd

socketAccept
clientFd =SoAd_AcceptImpl(SocketAdminList[sockNr].SocketHandle, &RemoteIpAddress,&Rem     otePort);                                                                                  
if( clientFd != (-1)){
SocketAdminList[sockNr].SocketState= SOCKET_TCP_READY;
}
SOCKET_TCP_READY在状态机中会执行socketTcpRead(i);函数

socketTcpRead
switch(SocketAdminList[sockNr].SocketConnectionRef->AutosarConnectorType) {
case SOAD_AUTOSAR_CONNECTOR_DOIP:
  DoIp_HandleTcpRx(sockNr);
}

这里服务器端是自己搞的一个接受的socket逻辑,大体上也是跟上面图里的一个步骤。初始化socket,然后等待连接,有连接后读取数据。

DoIp_HandleTcpRx
函数这里就接上了DoIP模块的代码。

7. SoAd规范

官网文档名字为  

AUTOSAR_SWS_SocketAdaptor.pdf
》,SoAd的意思就是SocketAdaptor,就是适配socket
的,提供网络的socket编程服务。

SoAd模块的功能描述如下:

可以看到我们
用lwip开源软件直接替代了TcpIp模块
,TcpIp就是提供网络协议栈的解析的,还能提供DHCP、ICMP、ARP等报文的服务。

其他SoAd的函数的解释自己可以看下规范。对照
as.infrastructure/communication/SoAd/SoAd_LWIP.c
代码自己看下。

8. TcpIp规范

规范文档为:《
AUTOSAR_SWS_TcpIp.pdf》,目前的as平台上由于Arccore的代码比较老,TcpIp没独立出来一个模块,用Lwip代替了。最新的开源代码里面是有TcpIp模块的,可以参考:
https://github.com/openAUTOSAR/classic-platform
进行一个移植。主要功能如下图:

后记:

到此**网络协议栈**  

相关的AUTOSAR模块都讲完了,如下:
网卡驱动-》网络接口-》网络协议栈-》SoAd模块-》DoIP模块-》PduR模块-》Dcm模块
。完成了AUTOSAR一小半的主要模块,有兴趣的朋友可以研究下Can相关
的协议和模块,基本也是这么个
套路
,并且can协议比DoIP简单一些。也有can的客户端可以发can报文,qemu模拟can的驱动等。

最近有很多对自研**代码
下一代**汽车软件
感兴趣的朋友,这里可以加我微信thatway1989
,备注进群
。然后拉你进本公众号的交流群:OS与AUTOSAR研究-交流群,可以讨论汽车软件最新技术,一起学习。

Talk is cheap  

,show me the code
,后续会继续更新,
纯干货
分析,无广告,不打赏,欢迎
转载
,欢迎
评论交流

往期见话题:
AUTOSAR入门

results matching ""

    No results matching ""