打印
[应用相关]

STM32开发笔记53:STM32F4+DP83848以太网通信指南系列(七):...

[复制链接]
671|5
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主

本章为系列指南的第七章,讲述如何在之前的基础上,编写程序在STM32上发送一个网络包,并使用WireShark进行验证。

先回顾一下之前的章节我们做好的准备工作,在《STM32F4+DP83848以太网通信指南第五章:MAC+DMA配置》结束时我们封装了一个DP83848的初始化函数,该函数完成了PHY的配置,MAC层的配置,DMA的配置,并且启用了以太网中断,函数命名为DP83848Init(),那么今天,我们要做的主要任务就是编写一个类似的DP83848Send(u8* data, u16 length)函数。

可以在本章的一开始跟大家剧透一个好消息,有了《STM32F4+DP83848以太网通信指南第四章:PHY配置》 和 《STM32F4+DP83848以太网通信指南第五章:MAC+DMA配置》 的基础,我们本章最终实现的DP83848Send(u8* data, u16 length)函数,只有两行代码,非常非常简单。这两行代码我暂时先不贴出来,我们来顺着原来的思路,根据相关文档和官方示例代码,顺藤摸瓜,一步一步深入了解以太网发包的流程,最终理解体系结构后,也就水到渠成能够写出来了。

在 《STM32F4+DP83848以太网通信指南第五章:MAC+DMA配置》 最后一部分提到在LWIP官方样例中,路径为STM32F4x7_ETH_LwIP_V1.1.1\Utilities\Third_Party\lwip-1.4.1\port\STM32F4x7\Standalone\ethernetif.c的文件中,第76行有个low_level_init()函数,该函数调用ETH库函数对MAC底层及DMA进行了初始化。同样的,这份文件的138行,有个名为low_level_output(struct netif *netif, struct pbuf *p)的函数,疑似是向外输出网络包的函数,下面就对这部分代码进行分析,并试着用其中的核心逻辑进行测试。

因为ethernetif.c这份代码本身隶属于LWIP,而我们是不使用LWIP的,所以这份代码只能尽量去看懂和借鉴,想要原封不动地使用是不可以的。


使用特权

评论回复
沙发
keaibukelian|  楼主 | 2019-7-4 09:36 | 只看该作者

我们先完整地贴出这个函数:



  • /**



  • * This function should do the actual transmission of the packet. The packet is



  • * contained in the pbuf that is passed to the function. This pbuf



  • * might be chained.



  • *



  • * @param netif the lwip network interface structure for this ethernetif



  • * @param p the MAC packet to send (e.g. IP packet including MAC addresses and type)



  • * @return ERR_OK if the packet could be sent



  • *         an err_t value if the packet couldn't be sent



  • *



  • * @NOTE Returning ERR_MEM here if a DMA queue of your MAC is full can lead to



  • *       strange results. You might consider waiting for space in the DMA queue



  • *       to become availale since the stack doesn't retry to send a packet



  • *       dropped because of memory failure (except for the TCP timers).



  • */







  • static err_t low_level_output(struct netif *netif, struct pbuf *p) {



  •     err_t errval;



  •     struct pbuf *q;



  •     u8 *buffer =  (u8 *)(DMATxDescToSet->Buffer1Addr);



  •     __IO ETH_DMADESCTypeDef *DmaTxDesc;



  •     uint16_t framelength = 0;



  •     uint32_t bufferoffset = 0;



  •     uint32_t byteslefttocopy = 0;



  •     uint32_t payloadoffset = 0;







  •     DmaTxDesc = DMATxDescToSet;



  •     bufferoffset = 0;







  •     /* copy frame from pbufs to driver buffers */



  •     for(q = p; q != NULL; q = q->next) {



  •         /* Is this buffer available? If not, goto error */



  •         if((DmaTxDesc->Status & ETH_DMATxDesc_OWN) != (u32)RESET) {



  •             errval = ERR_BUF;



  •             goto error;



  •         }







  •         /* Get bytes in current lwIP buffer */



  •         byteslefttocopy = q->len;



  •         payloadoffset = 0;







  •         /* Check if the length of data to copy is bigger than Tx buffer size*/



  •         while( (byteslefttocopy + bufferoffset) > ETH_TX_BUF_SIZE ) {



  •             /* Copy data to Tx buffer*/



  •             memcpy( (u8_t *)((u8_t *)buffer + bufferoffset), (u8_t *)((u8_t *)q->payload + payloadoffset), (ETH_TX_BUF_SIZE - bufferoffset) );







  •             /* Point to next descriptor */



  •             DmaTxDesc = (ETH_DMADESCTypeDef *)(DmaTxDesc->Buffer2NextDescAddr);







  •             /* Check if the buffer is available */



  •             if((DmaTxDesc->Status & ETH_DMATxDesc_OWN) != (u32)RESET) {



  •                 errval = ERR_USE;



  •                 goto error;



  •             }







  •             buffer = (u8 *)(DmaTxDesc->Buffer1Addr);







  •             byteslefttocopy = byteslefttocopy - (ETH_TX_BUF_SIZE - bufferoffset);



  •             payloadoffset = payloadoffset + (ETH_TX_BUF_SIZE - bufferoffset);



  •             framelength = framelength + (ETH_TX_BUF_SIZE - bufferoffset);



  •             bufferoffset = 0;



  •         }







  •         /* Copy the remaining bytes */



  •         memcpy( (u8_t *)((u8_t *)buffer + bufferoffset), (u8_t *)((u8_t *)q->payload + payloadoffset), byteslefttocopy );



  •         bufferoffset = bufferoffset + byteslefttocopy;



  •         framelength = framelength + byteslefttocopy;



  •     }







  •     /* Note: padding and CRC for transmitted frame



  •        are automatically inserted by DMA */







  •     /* Prepare transmit descriptors to give to DMA*/



  •     ETH_Prepare_Transmit_Descriptors(framelength);







  •     errval = ERR_OK;







  • error:







  •     /* When Transmit Underflow flag is set, clear it and issue a Transmit Poll Demand to resume transmission */



  •     if ((ETH->DMASR & ETH_DMASR_TUS) != (uint32_t)RESET) {



  •         /* Clear TUS ETHERNET DMA flag */



  •         ETH->DMASR = ETH_DMASR_TUS;







  •         /* Resume DMA transmission*/



  •         ETH->DMATPDR = 0;



  •     }



  •     return errval;



  • }



使用特权

评论回复
板凳
keaibukelian|  楼主 | 2019-7-4 09:36 | 只看该作者

这个函数的官方注释描述的就是用来向外发送以太网包的,函数中说要发的包在第二个参数,类型为pbuf结构体指针的参数p中,并且说了p可能是个链表,我们看到函数的两个入参都是结构体参数,这两个结构体的定义我们不需要管,是LWIP自己封装的一个结构体。我们去寻迹参数p的用法,在代码片段的30行,使用q变量和for循环遍历p,因此我们能够确定p就是个头尾相接的pbuf链表。继续观察遍历体中的操作逻辑,我们看到整个for循环的主要目的就是在尝试将q->payload中的byte,利用函数memcopy()向buffer变量中堆,并且做了一些长度的校验,我们继而去观察一下buffer变量的定义,第19行的u8 *buffer = (u8 *)(DMATxDescToSet->Buffer1Addr);是一个比较重要的线索,由此我们可以抽丝剥茧出整体的逻辑,应该就是将首尾相接的p遍历出来,取其中每个元素的payload区域,向DMATxDescToSet->Buffer1Addr中压。最后,第73行的ETH_Prepare_Transmit_Descriptors(framelength);调用了ETH库中的函数,实现了最终的结局,将网络包发出去,入参的framelength应该就是需要发出去的包长度,包内容应该就是通过DMA技术,将内存中的DMATxDescToSet->Buffer1Addr发出去了。

有了以上针对low_level_output()函数的分析,我们来做实验印证一下,因为我们从零开始构建的项目没有LWIP,也没有ethernetif.c,更没有low_level_output()函数,因此,函数内部的逻辑都需要我们自己手动实现,慢着,不要一看到「手动实现」就头疼,你以为手动实现就很复杂吗?不,LWIP把事情搞复杂了,又是pbuf又是链表的,还有长度判断导致的Buffer2NextDescAddr切换(详见第43-62行一整段,不过不重要),如果我们手动写这段逻辑,放弃一些异常处理,再放弃那些跟LWIP强相关的结构体,我们整个发包函数只要两行就行:



  • void DP83848Send(u8* data, u16 length){



  •     memcpy((u8 *)DMATxDescToSet->Buffer1Addr, data, length);







  •     /* Prepare transmit descriptors to give to DMA*/



  •     ETH_Prepare_Transmit_Descriptors(length);



  • }


这里附带说明一下,并不是LWIP原版代码又臭又长,LWIP要做一个TCP/IP全栈协议,还要考虑包长度溢出的众多问题,我们精简版的协议很多不需要考虑,因此可以放弃很多繁琐的操作。


使用特权

评论回复
地板
keaibukelian|  楼主 | 2019-7-4 09:38 | 只看该作者

有了上述DP83848Send()函数,下面来做个小程序试验一下:



  • int main() {



  •     u8 MyMacAddr[6] = {0x08, 0x00, 0x06, 0x00, 0x00, 0x09};



  •     /* 下面是一段60byte大小的ARP报文,手动构建的 */



  •     u8 mydata[60] = {    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,



  •                      0x00, 0x01, 0x08, 0x06, 0x00, 0x01, 0x08, 0x00, 0x06, 0x04,



  •                      0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc0, 0xa8,



  •                      0x02, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xa8,



  •                      0x02, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,



  •                      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};



  •     u32 clock;







  •     /* 默认调用SystemInit,系统时钟168MHz */



  •     NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);        //4位抢占,0位响应







  •     DP83848Init(MyMacAddr);







  •     while(1){



  •         DP83848Send(mydata, 60);







  •         clock = 42000000;    //1s延时,while中每个步进需要4个周期



  •         while(clock--);



  •     }







  • }


使用Keil编译,用JLink下载到STM32F407中,给开发板接上网线,用WireShark就可以在网口中观察到STM32每隔1秒钟向外发送ARP报文了,虽然这段报文几乎没有任何意义。


使用特权

评论回复
5
keaibukelian|  楼主 | 2019-7-4 09:38 | 只看该作者

我使用WireShark截图如下:


使用特权

评论回复
6
keaibukelian|  楼主 | 2019-7-4 09:39 | 只看该作者
总结一下,这一章我们完成了一个DP83848Send()发包函数,这个函数可以接受一个字节buffer,一个字节buffer的长度,将这个buffer通过以太网发送出去,buffer内部的内容全部需要我们手工构建。DP83848Send()函数的设计思路来自于分析LWIP官网示例,主要是ethernetif.c中的代码。下一章我们同样根据这份代码,分析收包逻辑,实现STM32对以太网上数据的监听。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

63

主题

4095

帖子

5

粉丝