|
汽车零部件采购、销售通信录 填写你的培训需求,我们帮你找 招募汽车专业培训老师
AUTOSAR入门-SoAd模块和TcpIp模块
有一句话叫做“All is in the code”,网上好像没找到是谁说的,算我杜撰的
。这也是我坚信的一个观点:一切都在代码里。不管你做什么技术,什么业务,只要你跟软件相关有代码,直接看代码就可以反推出来技术是怎么设计实现的。比如一个高境界的C程序员,那么别管什么技术,比如操作系统、各种应用app、驱动、无线/网络协议等,只要是C写的,有代码,就都可以搞,而且可以迅速上手,看海量C代码如喝白水。因为万变不离其宗,都是C语言的逻辑在支撑,正所谓“无招胜有招”。
按照这个思路,我们先不看SoAd模块和TcpIP模块是什么,直接分析代码来实践下。在AUTOSAR入门-基于以太网诊断实验中,报文交互的流程为:网卡驱动-》网络接口-》网络协议栈-》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.SocketState = SOCKET_INIT;SocketAdminList.SocketConnectionRef = &SoAd_Config.SocketConnection;
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};staticconst 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);#分配pcbSoAd_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拉起来了lwipGET_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初始化之后就会轮询执行功能函数。
staticconst 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 }, };
voidKsmStart(void){ KsmID_Type i;for(i=0;i<KSM_NUM;i++) { KSM_Config.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_Isrstruct 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_inputethhdr = (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
socketAcceptclientFd =SoAd_AcceptImpl(SocketAdminList[sockNr].SocketHandle, &RemoteIpAddress,&Rem otePort); if( clientFd != (-1)){SocketAdminList[sockNr].SocketState= SOCKET_TCP_READY;}SOCKET_TCP_READY在状态机中会执行socketTcpRead(i);函数socketTcpReadswitch(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入门 |
|