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

本章为系列指南的第七章,讲述如何在之前的基础上,编写程序在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的,所以这份代码只能尽量去看懂和借鉴,想要原封不动地使用是不可以的。

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

/**
 * 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;
}

这个函数的官方注释描述的就是用来向外发送以太网包的,函数中说要发的包在第二个参数,类型为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全栈协议,还要考虑包长度溢出的众多问题,我们精简版的协议很多不需要考虑,因此可以放弃很多繁琐的操作。

有了上述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报文了,虽然这段报文几乎没有任何意义。

我使用WireShark截图如下:

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

 

猜你喜欢

转载自blog.csdn.net/qingwufeiyang12346/article/details/84846098