本章为系列指南的第七章,讲述如何在之前的基础上,编写程序在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对以太网上数据的监听。