AUTOSAR入门-SoAd模块和TcpIp模块
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模块。其中蓝色的部分还没讲,跟**网络协议栈**相关,本次文章全都给讲了。
- 嵌入式网络通信基础

提起**嵌入式**,我的感觉是有一定的门槛的,需要计算机软硬件很多的知识,不像搞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中:

这个data[0]是什么?首先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, ðernetif_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, ðernetif_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), ¤t_iphdr_src) &&
ip_addr_cmp(&(pcb->local_ip), ¤t_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入门
公众号:“那路谈OS与SoC嵌入式软件”,欢迎关注!
个人文章汇总:https://thatway1989.github.io
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)