TCP/IP协议栈之LwIP(十一)--- LwIP协议栈移植

本文详细介绍了如何在STM32L475 Pandora开发板上移植LwIP协议栈,使用ENC28J60有线网卡。首先,准备工作涉及IoT-OS(RT-Thread)的配置和网络卡的选择。然后,通过配置SPI接口、注册网卡设备和实现以太网设备对象管理,完成了LwIP协议栈的移植。最后,通过UDP回送和HTTP控制设备的示例程序验证了移植的成功。
摘要由CSDN通过智能技术生成

一、移植环境准备

前面主要是基于QEMU虚拟机环境进行LwIP协议栈开发调试的,如果手头没有开发板可以先在个人电脑上运行QEMU虚拟机以便学习LwIP协议栈的实现原理或者开发调试过程。在实际产品中,就需要在真实的开发板上移植LwIP协议栈,并在此基础上进行开发调试了。

1.1 IoT-OS准备

现在物联网设备越来越需要操作系统支持,所以本文在有操作系统的基础上移植LwIP协议栈,选择的操作系统环境是RT-Thread,选择的开发板是STM32L475 Pandora。

.\rt-thread-4.0.1\bsp\stm32\stm32l475-atk-pandora目录下启动env环境执行scons --dist命令,获得工程文件目录dist,将其复制出来,得到我们移植LwIP协议栈的基础环境。

复制出来的工程,修改工程总目录名为stm32l475-pandora-lwip,在该目录下打开env环境(在博客QEMU开发环境RT-Thread系统启动中介绍过),执行“scons --target=mdk5”命令生成MDK5工程,使用Keil MDK打开project.uvprojx工程文件,编译无报错,将其烧录到STM32L475 Pandora开发板中,开发板上的红色LED灯周期性闪烁,启动串口助手putty,打开开发板的串口,执行list_device命令可以看到目前开发板上启动的设备,结果如下:
STM32L475开发板移植成功
说明工程stm32l475-pandora-lwip已经基于STM32L475 Pandora移植好了,可以再次基础上开发新的功能。如果想了解RT-Thread系统启动过程和移植过程,可以参考博客:《RT-Thread启动过程》与《RT-Thread移植过程》,本文的重点是移植LwIP协议栈,这部分就略去了。

stm32l475-pandora-lwip的工程目录如下:
移植lwip的工程目录
stm32l475-pandora-lwip工程源码下载地址:https://github.com/StreamAI/LwIP_Projects/tree/master/stm32l475-pandora-lwip

1.2 Network Card准备

LwIP协议栈偏上层,要想让协议栈正常工作还需要网卡提供硬件支持。网卡可以分为有线和无线两种,常见的有线网卡一般是以太网卡比如ENC28J60,常见的无线网卡一般是WI-FI网卡比如AP6181。Wi-Fi网卡还涉及到Wi-Fi协议栈的移植,这里选择有线网卡ENC28J60为LwIP协议栈的运行提供硬件支持,Wi-Fi协议栈待后续再专门介绍。

首先看看ENC28J60的典型电路:
ENC28J60典型电路
ENC28J60网卡包括PHY与MAC模块,具有TX/RX缓冲器,使用SPI接口与MCU通信,支持中断引脚触发。我手头的ENC28J60网卡是从正点原子官方旗舰店采购的,通过NRF Wireless接口插到STM32L475 Pandora开发板上。

查询STM32L475 Pandora开发板I / O引脚分配表可知,NRF Wireless相关的接口如下:
NRF Wireless接口
把ENC28J60模块插到STM32L475 Pandora开发板上,图示如下:
ENC28J60插口
STM32L475 SPI接口通讯我在之前的博客:《STM32L4 SPI + QSPI + HAL》与《RT-Thread SPI设备对象管理》中已经详细介绍过了,本文就不再赘述了。

我们先把底层的SPI2接口配置好,打开board\CubeMX_Config\STM32L475VE.ioc文件,可以看到SPI2已经配置好了,不需要我们再重新配置,SPI2配置界面如下(注意引脚号与上表要一致,这里只需要配置SPI通信的三个引脚,片选CS由软件配置):
SPI2配置界面
在env环境中执行menuconfig命令打开图形化配置界面,使能SPI2外设并保存配置,配置界面如下:
使能SPI2外设

二、LwIP协议栈移植

2.1 工程中加入网卡与协议栈代码

从上面的工程目录可以看出,RT-Thread驱动框架中包含enc28j60的驱动,我们只需要启用相应的条件依赖宏就可以了,从编译控制脚本文件rt-thread\components\drivers\spi\SConscript可知,enc28j60驱动的条件依赖宏为RT_USING_ENC28J60,我们据此在菜单配置脚本文件board\Kconfig文件中新增ENC28J60网卡的配置选项如下:

// board\Kconfig
......
menu "Board extended module Drivers"
    config BSP_USING_ENC28J60
        bool "Enable ENC28J60"
        select BSP_USING_SPI2
        select RT_USING_ENC28J60
        default n
......

保存配置项,在env环境中执行menuconfig命令,打开图形化配置界面,使能刚才配置的ENC28J60网卡驱动,配置界面如下:
使能ENC28J60配置
在保存配置时弹出了警告窗口:
保存ENC28J60警告窗口
这个主要是因为启用LwIP协议栈条件依赖宏,LwIP协议栈配置中有一项跟ping命令相关的宏RT_LWIP_USING_PING依赖netdev模块,而netdev模块并没有启动导致的,netdev模块是RT-Thread提供的一套网卡接口管理层,作用主要是向上提供统一的网卡接口,方便协议栈的移植。

我们进入LwIP模块配置界面,默认选择的LwIP协议栈版本是2.0.2,我们选择最新的2.1.0版本作为移植对象,配置界面如下:
选择LwIP协议栈版本V2.1.0
为了在移植LwIP后验证移植是否成功,我们需要使用ping命令,同时为了方便后续更好物理网卡方便,我们使用RT-Thread提供的网卡接口管理层netdev模块,该模块还提供了ifconfig命令用于查看网卡信息,使能netdev模块的配置界面如下:
使能netdev模块
保存配置,刚才的警告消失了。到这里SPI2接口、ENC28J60网卡驱动、LwIP V2.1.0协议栈代码都已经使能了,接下来需要把各模块衔接起来,让其协调配合,完成网络数据的处理。

2.2 网卡SPI设备注册

前面的配置只是把ENC28J60网卡驱动与LwIP协议栈的代码加入的stm32l475-pandora-lwip工程中了,要想让其正常工作,还需要添加相应的移植代码。

由博客SPI设备对象管理可知,要想使用SPI设备,需要调用rt_hw_spi_device_attach函数完成SPI设备的绑定,该函数原型及实现代码如下:

// libraries\HAL_Drivers\drv_spi.c

/**
  * Attach the spi device to SPI bus, this function must be used after initialization.
  */
rt_err_t rt_hw_spi_device_attach(const char *bus_name, const char *device_name, GPIO_TypeDef *cs_gpiox, uint16_t cs_gpio_pin)
{
   
    RT_ASSERT(bus_name != RT_NULL);
    RT_ASSERT(device_name != RT_NULL);

    rt_err_t result;
    struct rt_spi_device *spi_device;
    struct stm32_hw_spi_cs *cs_pin;

    /* initialize the cs pin && select the slave*/
    GPIO_InitTypeDef GPIO_Initure;
    GPIO_Initure.Pin = cs_gpio_pin;
    GPIO_Initure.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_Initure.Pull = GPIO_PULLUP;
    GPIO_Initure.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(cs_gpiox, &GPIO_Initure);
    HAL_GPIO_WritePin(cs_gpiox, cs_gpio_pin, GPIO_PIN_SET);

    /* attach the device to spi bus*/
    spi_device = (struct rt_spi_device *)rt_malloc(sizeof(struct rt_spi_device));
    RT_ASSERT(spi_device != RT_NULL);
    cs_pin = (struct stm32_hw_spi_cs *)rt_malloc(sizeof(struct stm32_hw_spi_cs));
    RT_ASSERT(cs_pin != RT_NULL);
    cs_pin->GPIOx = cs_gpiox;
    cs_pin->GPIO_Pin = cs_gpio_pin;
    result = rt_spi_bus_attach_device(spi_device, device_name, bus_name, (void *)cs_pin);

    if (result != RT_EOK)
    {
   
        LOG_E("%s attach to %s faild, %d\n", device_name, bus_name, result);
    }

    RT_ASSERT(result == RT_EOK);

    LOG_D("%s attach to %s done", device_name, bus_name);

    return result;
}

我们在使用SPI2设备前,也需要先调用该函数,我们现在applications目录下新建ENC28J60移植代码文件enc28j60_port.c,并在该文件中新增绑定SPI2设备的代码如下:

// applications\enc28j60_port.c

#include "board.h"
#include "drv_spi.h"

// WIRELESS
#define PIN_NRF_IRQ   GET_PIN(D, 3)        // PD3 :  NRF_IRQ      --> WIRELESS
#define PIN_NRF_CE    GET_PIN(D, 4)        // PD4 :  NRF_CE       --> WIRELESS
#define PIN_NRF_CS    GET_PIN(D, 5)        // PD5 :  NRF_CS       --> WIRELESS

int enc28j60_init(void)
{
   
    __HAL_RCC_GPIOD_CLK_ENABLE();
    rt_hw_spi_device_attach("spi2", "spi21", GPIOD, GPIO_PIN_5);

    ......

    return 0;
}
INIT_COMPONENT_EXPORT(enc28j60_init);

到这里SPI2设备就绑定到STM32L475的SPI总线上了,STM32L475可以通过SPI总线接口函数正常访问该SPI设备了。最后使用INIT_COMPONENT_EXPORT命令可以让RT-Thread启动过程中自动调用enc28j60_init函数,以完成ENC28J60网卡的初始化,这里只完成了SPI2设备的初始化,下面继续添加ENC28J60驱动模块的初始化。

2.3 以太网设备对象管理

在博客网络接口管理中谈到LwIP网络接口管理层需要用户实现网络接口初始化、输入、输出等函数,相关函数原型如下:

// rt-thread\components\net\lwip-2.1.0\src\include\lwip\netif.h

/** Function prototype for netif init functions. Set up flags and output/linkoutput
 * callback functions in this function.
 *
 * @param netif The netif to initialize
 */
typedef err_t (*netif_init_fn)(struct netif *netif);
/** Function prototype for netif->input functions. This function is saved as 'input'
 * callback function in the netif struct. Call it when a packet has been received.
 *
 * @param p The received packet, copied into a pbuf
 * @param inp The netif which received the packet
 * @return ERR_OK if the packet was handled
 *         != ERR_OK is the packet was NOT handled, in this case, the caller has
 *                   to free the pbuf
 */
typedef err_t (*netif_input_fn)(struct pbuf *p, struct netif *inp);

#if LWIP_IPV4
/** Function prototype for netif->output functions. Called by lwIP when a packet
 * shall be sent. For ethernet netif, set this to 'etharp_output' and set
 * 'linkoutput'.
 *
 * @param netif The netif which shall send a packet
 * @param p The packet to send (p->payload points to IP header)
 * @param ipaddr The IP address to which the packet shall be sent
 */
typedef err_t (*netif_output_fn)(struct netif *netif, struct pbuf *p,
       const ip4_addr_t *ipaddr);
#endif /* LWIP_IPV4*/

#if LWIP_IPV6
/** Function prototype for netif->output_ip6 functions. Called by lwIP when a packet
 * shall be sent. For ethernet netif, set this to 'ethip6_output' and set
 * 'linkoutput'.
 *
 * @param netif The netif which shall send a packet
 * @param p The packet to send (p->payload points to IP header)
 * @param ipaddr The IPv6 address to which the packet shall be sent
 */
typedef err_t (*netif_output_ip6_fn)(struct netif *netif, struct pbuf *p,
       const ip6_addr_t *ipaddr);
#endif /* LWIP_IPV6 */

/** Function prototype for netif->linkoutput functions. Only used for ethernet
 * netifs. This function is called by ARP when a packet shall be sent.
 *
 * @param netif The netif which shall send a packet
 * @param p The packet to send (raw ethernet packet)
 */
typedef err_t (*netif_linkoutput_fn)(struct netif *netif, struct pbuf *p);
/** Function prototype for netif status- or link-callback functions. */
typedef void (*netif_status_callback_fn)(struct netif *netif);

从LwIP协议栈对网卡接口的需求可知,ENC28J60网卡至少也需要提供初始化、输入、输出与配置接口,RT-Thread为以太网设备提供了一个驱动管理框架如下:
以太网设备管理框架
RT-Thread在网卡驱动层(比如下文介绍的ENC28J60驱动层)与LwIP协议栈间提供了一个网络设备层,该层对于以太网数据的收发采用了独立的双线程结构,erx 线程和 etx 线程在正常情况下,两者的优先级设置成相同,用户可以根据自身实际要求进行微调以侧重接收或发送。

网络设备层为以太网设备提供了一个数据管理结构eth_device,该数据结构描述与接口函数原型如下:

// rt-thread\components\net\lwip-2.1.0\src\include\netif\ethernetif.h

struct eth_device
{
   
    /* inherit from rt_device */
    struct rt_device parent;

    /* network interface for lwip */
    struct netif *netif;
    struct rt_semaphore tx_ack;

    rt_uint16_t flags;
    rt_uint8_t  link_changed;
    rt_uint8_t  link_status;

    /* eth device interface */
    struct pbuf* (*eth_rx)(rt_device_t dev);
    rt_err_t (*eth_tx)(rt_device_t dev, struct pbuf* p);
};

rt_err_t eth_device_ready(struct eth_device* dev);
rt_err_t eth_device_init(struct eth_device * dev, const char *name);
rt_err_t eth_device_init_with_flag(struct eth_device *dev, const char *name, rt_uint16_t flag);
rt_err_t eth_device_linkchange(struct eth_device* dev, rt_bool_t up);

int eth_system_device_init(void);

结构体eth_device继承自基设备rt_device,同时包含前面介绍的网卡接口结构体指针netif及LwIP协议栈需要的网卡状态与标志字段,最后是以太网卡的发射与接收函数指针eth_rx / eth_tx。

以太网设备的初始化过程如下:

// rt-thread\components\net\lwip-2.1.0\src\netif\ethernetif.c

rt_err_t eth_device_init(struct eth_device * dev, const char *name)
{
   
    rt_uint16_t flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP;

#if LWIP_IGMP
    /* IGMP support */
    flags |= NETIF_FLAG_IGMP;
#endif

    return eth_device_init_with
lwip是瑞士计算机科学院的一个开源的TCP/IP协议实现.   lwIPTCP/IP协议的一个实现。lwIP协议栈主要关注的是怎么样减少内存的使用和代码的大小,这样就可以让lwIP适用于资源有限的小型平台例如嵌入式系统。为了简化处理过程和内存要求,lwIP对API进行了裁减,可以不需要复制一些数据。   LwIP是Light Weight (轻型)IP协议,有无操作系统的支持都可以运行。LwIP实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用,一般它只需要几百字节的RAM和40K左右的ROM就可以运行,这使LwIP协议栈适合在低端的嵌入式系统中使用。   其主要特性如下:   ①支持多网络接口下的IP转发;   ②支持ICMP协议;   ③包括实验性扩展的UDP(用户数据报协议);   ④包括阻塞控制、RTT 估算、快速恢复和快速转发的TCP(传输控制协议);   ⑤提供专门的内部回调接口(Raw API),用于提高应用程序性能;   ⑥可选择的Berkeley接口API (在多线程情况下使用) 。   (1) 信号量   LwIP中需要使用信号量进行通信,所以在sys_arch中应实现相应的信号量结构体 struct sys_semt和处理函数sys_sem_new() 、sys_sem_free() 、sys_sem_signal ( ) 和sys_arch_sem_wait ( ) 。由于μC/OS已经实现了信号量OSEVENT的各种操作,并且功能和LwIP上面几个函数的目的功能是完全一样的,所以只要把μC/OS的函数重新包装成上面的函数,就可直接使用。   (2) 消息队列   LwIP 使用消息队列来缓冲、传递数据报文,因此要实现消息队列结构sys_mbox_t ,以及相应的操作函数:sys_mbox_new() 、sys_mbox_free () 、sys_mbox _post () 和sys_arch_mbox_fetch() 。μC/OS实现了消息队列结构及其操作,但是μC/OS没有对消息队列中的消息进行管理,因此不能直接使用,必须在μC/OS的基础上重新实现。具体实现时,对队列本身的管理利用μC/OS自己的OSQ操作完成,然后使用μC/OS中的内存管理模块实现对消息的创建、使用、删除和回收,两部分综合起来形成了LwIP的消息队列功能。   (3) 定时器函数   LwIP中每个和TCP/IP相关的任务的一系列定时事件组成一个单向链表,每个链表的起始指针存在lwip_timeouts 的对应表项中,如图2所示。移植时需要实现struct sys_timeouts * sys_arch_timeouts (void) 函数,该函数返回目前正处于运行态的线程所对应的timeout 队列指针。   (4) 创建新线程函数   在μC/OS 中,没有线程(thread) 的概念,只有任务(Task) 。它提供了创建新任务的系统API调用OSTaskCreate,因此只要把OSTaskCreate封装一下,就可以实现 sys_thread_new。需要注意的是LwIP中的thread并没有μC/OS 中优先级的概念,实现时要由用户事先为LwIP中创建的线程分配好优先级。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流云IoT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值