TCP/IP協議棧之LwIP(十一)--- LwIP協議棧移植

一、移植環境準備

前面主要是基於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_flag(dev, name, flags);
}

/* Keep old drivers compatible in RT-Thread */
rt_err_t eth_device_init_with_flag(struct eth_device *dev, const char *name, rt_uint16_t flags)
{
    struct netif* netif;

    netif = (struct netif*) rt_malloc (sizeof(struct netif));
    if (netif == RT_NULL)
    {
        rt_kprintf("malloc netif failed\n");
        return -RT_ERROR;
    }
    rt_memset(netif, 0, sizeof(struct netif));

    /* set netif */
    dev->netif = netif;
    /* device flags, which will be set to netif flags when initializing */
    dev->flags = flags;
    /* link changed status of device */
    dev->link_changed = 0x00;
    dev->parent.type = RT_Device_Class_NetIf;
    /* register to RT-Thread device manager */
    rt_device_register(&(dev->parent), name, RT_DEVICE_FLAG_RDWR);
    rt_sem_init(&(dev->tx_ack), name, 0, RT_IPC_FLAG_FIFO);

    /* set name */
    netif->name[0] = name[0];
    netif->name[1] = name[1];

    /* set hw address to 6 */
    netif->hwaddr_len   = 6;
    /* maximum transfer unit */
    netif->mtu          = ETHERNET_MTU;

    /* set linkoutput */
    netif->linkoutput   = ethernetif_linkoutput;
        
    /* get hardware MAC address */
    rt_device_control(&(dev->parent), NIOCTL_GADDR, netif->hwaddr);

#if LWIP_NETIF_HOSTNAME
    /* Initialize interface hostname */
    netif->hostname = "rtthread";
#endif /* LWIP_NETIF_HOSTNAME */

    /* if tcp thread has been started up, we add this netif to the system */
    if (rt_thread_find("tcpip") != RT_NULL)
    {
        ip4_addr_t ipaddr, netmask, gw;

#if !LWIP_DHCP
        ipaddr.addr = inet_addr(RT_LWIP_IPADDR);
        gw.addr = inet_addr(RT_LWIP_GWADDR);
        netmask.addr = inet_addr(RT_LWIP_MSKADDR);
#else        
        IP4_ADDR(&ipaddr, 0, 0, 0, 0);
        IP4_ADDR(&gw, 0, 0, 0, 0);
        IP4_ADDR(&netmask, 0, 0, 0, 0);
#endif
        netifapi_netif_add(netif, &ipaddr, &netmask, &gw, dev, eth_netif_device_init, tcpip_input);
    }

#ifdef RT_USING_NETDEV
    /* network interface device flags synchronize */
    netdev_flags_sync(netif);
#endif /* RT_USING_NETDEV */

    return RT_EOK;
}

在以太網設備初始化過程中,主要完成了以太網設備註冊rt_device_register,網卡輸出接口ethernetif_linkoutput註冊,網卡接口添加netifapi_netif_add等工作。

網卡接口添加函數netifapi_netif_add向LwIP協議棧註冊了網卡初始化接口eth_netif_device_init與網卡輸入接口tcpip_input,並將以太網設備句柄註冊到lwip網卡接口對象的state字段,實現eth_device與netif設備對象的相互訪問。我們依次看這幾個接口函數的實現代碼(限於篇幅,只節選部分):

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

static err_t eth_netif_device_init(struct netif *netif)
{
    struct eth_device *ethif;

    ethif = (struct eth_device*)netif->state;
    if (ethif != RT_NULL)
    {
        rt_device_t device;

#ifdef RT_USING_NETDEV
    /* network interface device register */
    netdev_add(netif);
#endif /* RT_USING_NETDEV */

        /* get device object */
        device = (rt_device_t) ethif;
        if (rt_device_init(device) != RT_EOK)
        {
            return ERR_IF;
        }

        /* copy device flags to netif flags */
        netif->flags = (ethif->flags & 0xff);
        netif->mtu = ETHERNET_MTU;
        
        /* set output */
        netif->output       = etharp_output;

#if LWIP_IPV6
        ......
#endif /* LWIP_IPV6 */

        /* set default netif */
        if (netif_default == RT_NULL)
            netif_set_default(ethif->netif);

#if LWIP_DHCP
        /* set interface up */
        netif_set_up(ethif->netif);
        /* if this interface uses DHCP, start the DHCP client */
        dhcp_start(ethif->netif);
#else
        /* set interface up */
        netif_set_up(ethif->netif);
#endif

        if (ethif->flags & ETHIF_LINK_PHYUP)
        {
            /* set link_up for this netif */
            netif_set_link_up(ethif->netif);
        }

        return ERR_OK;
    }

    return ERR_IF;
}

static err_t ethernetif_linkoutput(struct netif *netif, struct pbuf *p)
{
#ifndef LWIP_NO_TX_THREAD
    struct eth_tx_msg msg;
    struct eth_device* enetif;

    RT_ASSERT(netif != RT_NULL);
    enetif = (struct eth_device*)netif->state;

    /* send a message to eth tx thread */
    msg.netif = netif;
    msg.buf   = p;
    if (rt_mb_send(&eth_tx_thread_mb, (rt_uint32_t) &msg) == RT_EOK)
    {
        /* waiting for ack */
        rt_sem_take(&(enetif->tx_ack), RT_WAITING_FOREVER);
    }
#else
    struct eth_device* enetif;

    RT_ASSERT(netif != RT_NULL);
    enetif = (struct eth_device*)netif->state;

    if (enetif->eth_tx(&(enetif->parent), p) != RT_EOK)
    {
        return ERR_IF;
    }
#endif
    return ERR_OK;
}


// rt-thread\components\net\lwip-2.1.0\src\api\tcpip.c
/**
 * @ingroup lwip_os
 * Pass a received packet to tcpip_thread for input processing with
 * ethernet_input or ip_input. Don't call directly, pass to netif_add()
 * and call netif->input().
 *
 * @param p the received packet, p->payload pointing to the Ethernet header or
 *          to an IP header (if inp doesn't have NETIF_FLAG_ETHARP or
 *          NETIF_FLAG_ETHERNET flags)
 * @param inp the network interface on which the packet was received
 */
err_t tcpip_input(struct pbuf *p, struct netif *inp)
{
#if LWIP_ETHERNET
  if (inp->flags & (NETIF_FLAG_ETHARP | NETIF_FLAG_ETHERNET)) {
    return tcpip_inpkt(p, inp, ethernet_input);
  } else
#endif /* LWIP_ETHERNET */
    return tcpip_inpkt(p, inp, ip_input);
}

以太網初始化函數eth_netif_device_init最終通過調用rt_device_init完成網卡設備初始化,同時註冊了網卡輸出接口etharp_output,用於向上層傳遞數據包。

以太網鏈路輸出接口ethernetif_linkoutput最終是通過調用eth_device->eth_tx接口實現功能的,RT-Thread爲了加快網卡的傳輸速率,支持爲以太網卡分別創建一個數據發送線程與一個數據接收線程,專門處理以太網卡的數據收發,但數據包需要通過郵箱在進程間傳遞。

協議棧輸入接口tcpip_input主要是把以太網卡接收到的數據包傳遞給lwip協議棧上層進行處理,該函數被以太網卡接收線程調用,當以太網卡接收到數據包後會調用該接口函數將數據包傳遞給lwip協議棧上層處理。

以太網發送接收線程,及通過郵箱發送接收數據的過程代碼如下:

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

#ifndef LWIP_NO_TX_THREAD
/* Ethernet Tx Thread */
static void eth_tx_thread_entry(void* parameter)
{
    struct eth_tx_msg* msg;

    while (1)
    {
        if (rt_mb_recv(&eth_tx_thread_mb, (rt_ubase_t *)&msg, RT_WAITING_FOREVER) == RT_EOK)
        {
            struct eth_device* enetif;

            RT_ASSERT(msg->netif != RT_NULL);
            RT_ASSERT(msg->buf   != RT_NULL);

            enetif = (struct eth_device*)msg->netif->state;
            if (enetif != RT_NULL)
            {
                /* call driver's interface */
                if (enetif->eth_tx(&(enetif->parent), msg->buf) != RT_EOK)
                {
                    /* transmit eth packet failed */
                }
            }

            /* send ACK */
            rt_sem_release(&(enetif->tx_ack));
        }
    }
}
#endif

#ifndef LWIP_NO_RX_THREAD
/* Ethernet Rx Thread */
static void eth_rx_thread_entry(void* parameter)
{
    struct eth_device* device;

    while (1)
    {
        if (rt_mb_recv(&eth_rx_thread_mb, (rt_ubase_t *)&device, RT_WAITING_FOREVER) == RT_EOK)
        {
            struct pbuf *p;

            /* check link status */
            if (device->link_changed)
            {
                int status;
                rt_uint32_t level;

                level = rt_hw_interrupt_disable();
                status = device->link_status;
                device->link_changed = 0x00;
                rt_hw_interrupt_enable(level);

                if (status)
                    netifapi_netif_set_link_up(device->netif);
                else
                    netifapi_netif_set_link_down(device->netif);
            }

            /* receive all of buffer */
            while (1)
            {
                if(device->eth_rx == RT_NULL) break;
                
                p = device->eth_rx(&(device->parent));
                if (p != RT_NULL)
                {
                    /* notify to upper layer */
                    if( device->netif->input(p, device->netif) != ERR_OK )
                    {
                        LWIP_DEBUGF(NETIF_DEBUG, ("ethernetif_input: Input error\n"));
                        pbuf_free(p);
                        p = NULL;
                    }
                }
                else break;
            }
        }
        else
        {
            LWIP_ASSERT("Should not happen!\n",0);
        }
    }
}
#endif

#ifndef LWIP_NO_RX_THREAD
rt_err_t eth_device_ready(struct eth_device* dev)
{
    if (dev->netif)
        /* post message to Ethernet thread */
        return rt_mb_send(&eth_rx_thread_mb, (rt_uint32_t)dev);
    else
        return ERR_OK; /* netif is not initialized yet, just return. */
}
......
#endif

// rt-thread\components\drivers\spi\enc28j60.c
void enc28j60_isr(void)
{
    eth_device_ready(&enc28j60_dev.parent);
    NET_DEBUG("enc28j60_isr\r\n");
}


// libraries\HAL_Drivers\drv_eth.c
void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth)
{
    rt_err_t result;
    result = eth_device_ready(&(stm32_eth_device.parent));
    if (result != RT_EOK)
        LOG_E("RX err = %d", result);
}

從上面的代碼可以看出,eth_tx_thread_entry線程通過郵箱接收到消息後通過eth_device->eth_tx接口將數據發送出去,郵箱消息是被前面註冊的ethernetif_linkoutput接口函數發送的。

eth_rx_thread_entry線程通過郵箱接收到信號後,通過調用eth_device->eth_rx接口從以太網卡接收數據,並通過調用netif->input接口(前面註冊的tcpip_input接口函數)將數據傳遞給lwip協議棧上層處理,郵箱消息是通過以太網設備的接收中斷處理函數enc28j60_isr間接發送的。

上面調用以太網接口eth_device_ready用於發送以太網接收中斷/接收完成信號的函數有兩個,分別是enc28j60_isr與HAL_ETH_RxCpltCallback,讀者可能會疑惑這裏起作用的是哪個函數?我們使用ENC28J60以太網卡,起作用的自然是enc28j60_isr,STM32互聯網型號是支持以太網ETH MAC模塊的,對於只有PHY物理層的網卡比如DM9000,需要藉助STM32提供的ETH模塊實現MAC層的功能,自然就需要藉助STM32 ETH庫函數接口比如HAL_ETH_RxCpltCallback來發送接收完成信號便於上層處理接收到的數據了。

2.4 ENC28J60設備註冊

熟悉了eth_device設備驅動框架,接下來我們需要向eth_device設備驅動層註冊以太網設備,並實現其eth_rx與eth_tx接口函數功能。

下面先看ENC28J60以太網卡的數據結構描述:

// rt-thread\components\drivers\spi\enc28j60.h

struct net_device
{
    /* inherit from ethernet device */
    struct eth_device parent;

    /* interface address info. */
    rt_uint8_t  dev_addr[MAX_ADDR_LEN]; /* hw address   */

    rt_uint8_t emac_rev;
    rt_uint8_t phy_rev;
    rt_uint8_t phy_pn;
    rt_uint32_t phy_id;

    /* spi device */
    struct rt_spi_device *spi_device;
    struct rt_mutex lock;
};

ENC28J60網卡結構體net_device繼承自以太網設備eth_device,同時包含了MAC地址、SPI設備句柄rt_spi_device、PHY物理層的一些管理變量等。

前面已經完成了SPI2設備的註冊,接下來看看ENC28J60設備的初始化與註冊:

// rt-thread\components\drivers\spi\enc28j60.c

static struct net_device  enc28j60_dev;

rt_err_t enc28j60_attach(const char *spi_device_name)
{
    struct rt_spi_device *spi_device;

    spi_device = (struct rt_spi_device *)rt_device_find(spi_device_name);
    if (spi_device == RT_NULL)
    {
        NET_DEBUG("spi device %s not found!\r\n", spi_device_name);
        return -RT_ENOSYS;
    }

    /* config spi */
    {
        struct rt_spi_configuration cfg;
        cfg.data_width = 8;
        cfg.mode = RT_SPI_MODE_0 | RT_SPI_MSB; /* SPI Compatible Modes 0 */
        cfg.max_hz = 20 * 1000 * 1000; /* SPI Interface with Clock Speeds Up to 20 MHz */
        rt_spi_configure(spi_device, &cfg);
    } /* config spi */

    memset(&enc28j60_dev, 0, sizeof(enc28j60_dev));

    rt_event_init(&tx_event, "eth_tx", RT_IPC_FLAG_FIFO);
    enc28j60_dev.spi_device = spi_device;

    /* detect device */
    {
        uint16_t value;

        /* perform system reset. */
        spi_write_op(spi_device, ENC28J60_SOFT_RESET, 0, ENC28J60_SOFT_RESET);
        rt_thread_delay(1); /* delay 20ms */

        enc28j60_dev.emac_rev = spi_read(spi_device, EREVID);
        value = enc28j60_phy_read(spi_device, PHHID2);
        enc28j60_dev.phy_rev = value & 0x0F;
        enc28j60_dev.phy_pn = (value >> 4) & 0x3F;
        enc28j60_dev.phy_id = (enc28j60_phy_read(spi_device, PHHID1) | ((value >> 10) << 16)) << 3;

        if (enc28j60_dev.phy_id != 0x00280418)
            return RT_EIO;
    }

    /* OUI 00-04-A3 (hex): Microchip Technology, Inc. */
    enc28j60_dev.dev_addr[0] = 0x00;
    enc28j60_dev.dev_addr[1] = 0x04;
    enc28j60_dev.dev_addr[2] = 0xA3;
    /* set MAC address, only for test */
    enc28j60_dev.dev_addr[3] = 0x12;
    enc28j60_dev.dev_addr[4] = 0x34;
    enc28j60_dev.dev_addr[5] = 0x56;

    /* init rt-thread device struct */
    enc28j60_dev.parent.parent.type    = RT_Device_Class_NetIf;
#ifdef RT_USING_DEVICE_OPS
    enc28j60_dev.parent.parent.ops     = &enc28j60_ops;
#else
    enc28j60_dev.parent.parent.init    = enc28j60_init;
    enc28j60_dev.parent.parent.open    = enc28j60_open;
    enc28j60_dev.parent.parent.close   = enc28j60_close;
    enc28j60_dev.parent.parent.read    = enc28j60_read;
    enc28j60_dev.parent.parent.write   = enc28j60_write;
    enc28j60_dev.parent.parent.control = enc28j60_control;
#endif

    /* init rt-thread ethernet device struct */
    enc28j60_dev.parent.eth_rx  = enc28j60_rx;
    enc28j60_dev.parent.eth_tx  = enc28j60_tx;

    rt_mutex_init(&enc28j60_dev.lock, "enc28j60", RT_IPC_FLAG_FIFO);

    eth_device_init(&(enc28j60_dev.parent), "e0");

    return RT_EOK;
}

ENC28J60設備註冊函數enc28j60_attach完成SPI設備的配置,net_device設備的配置,最後通過調用前面介紹的接口函數eth_device_init完成eth_device設備的初始化與註冊。

根據這個過程,我們只需要調用函數enc28j60_attach即可完成ENC28J60設備的初始化與註冊,在enc28j60_port.c文件中添加ENC28J60初始化與註冊代碼如下:

// applications\enc28j60_port.c

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

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

    /* attach enc28j60 to spi. spi21 cs - PD6 */
    enc28j60_attach("spi21");
	......
    return 0;
}
INIT_COMPONENT_EXPORT(enc28j60_init);

到這裏ENC28J60網卡已經能夠初始化並註冊到RT-Thread設備管理框架中,但移植工作還沒有結束。

前面提到了ENC28J60接收中斷處理函數void enc28j60_isr(void),該函數怎麼觸發呢?ENC28J60使用的NRF WIRELESS接口是有中斷引腳NRF_IRQ的,我們只需要把該函數註冊爲NRF_IRQ引腳的外部信號觸發中斷執行函數即可。不熟悉GPIO引腳中斷配置的可以參考博客:PIN設備對象管理,在enc28j60_port.c文件中添加配置NRF_IRQ引腳並綁定中斷服務函數enc28j60_isr的代碼如下(增加條件宏定義,以免後續條件宏關閉後編譯運行錯誤):

// applications\enc28j60_port.c

#include "board.h"

#ifdef BSP_USING_ENC28J60

#include "board.h"
#include "drv_spi.h"
#include "enc28j60.h"
#include "drivers/pin.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);

    /* attach enc28j60 to spi. spi21 cs - PD6 */
    enc28j60_attach("spi21");

    /* init interrupt pin */
    rt_pin_mode(PIN_NRF_IRQ, PIN_MODE_INPUT_PULLUP);
    rt_pin_attach_irq(PIN_NRF_IRQ, PIN_IRQ_MODE_FALLING, (void(*)(void*))enc28j60_isr, RT_NULL);
    rt_pin_irq_enable(PIN_NRF_IRQ, PIN_IRQ_ENABLE);

    return 0;
}
INIT_COMPONENT_EXPORT(enc28j60_init);

#endif /* BSP_USING_ENC28J60 */

到這裏ENC28J60網卡就配置好了,在env環境中執行“scons --target=mdk5”命令生成Keil MDK工程,打開MDK工程文件project.uvprojx,編譯報錯如下:
Keil編譯報錯1
提示不能打開該文件,我們查找unistd.h文件所在路徑爲rt-thread\components\libc\compilers\armlibc\sys\unistd.h,看看包含該文件需要依賴哪些條件宏,查看該目錄下的編譯控制腳本文件rt-armlibc\SConscript,代碼如下:

// rt-thread\components\libc\compilers\armlibc\sys\unistd.h
......
#ifdef RT_USING_DFS

#define STDIN_FILENO    0       /* standard input file descriptor */
#define STDOUT_FILENO   1       /* standard output file descriptor */
#define STDERR_FILENO   2       /* standard error file descriptor */

#include <dfs_posix.h>
#else
#define _FREAD      0x0001  /* read enabled */
......
#define _FNOCTTY    0x8000  /* don't assign a ctty on this open */

#define O_RDONLY    0       /* +1 == FREAD */
......
#define O_SYNC      _FSYNC
#endif


// rt-thread\components\libc\compilers\armlibc\SConscript
......
if rtconfig.PLATFORM == 'armcc' or rtconfig.PLATFORM == 'armclang':
    group = DefineGroup('libc', src, depend = ['RT_USING_LIBC'], 
        CPPPATH = CPPPATH, CPPDEFINES = CPPDEFINES)

從上面的代碼可以看出,包含unistd.h文件所在目錄需要打開條件宏RT_USING_LIBC,我們在menuconfig中打開RT_USING_LIBC,配置界面如下:
打開LIBC條件宏
重新在env中執行“scons --target=mdk5”命令,打開MDK工程文件project.uvprojx,編譯報錯如下:
編譯報錯2
上面的警告提示是宏定義衝突,而且正好跟前面unistd.h文件中的宏定義一樣,再回頭看看unistd.h文件中的宏定義,在條件宏RT_USING_DFS開啓後,就不再重新定義這些宏定義了,宏定義衝突也就解決了,我們先在menuconfig中開啓條件宏定義RT_USING_DFS,配置界面如下:
開啓DFS文件系統配置
下面的錯誤提示是內存空間不夠用了,打開Keil MDK配置ROM與RAM的鏈接腳本文件,發現,只使用了STM32L475 SRAM2 32KB的空間,我們改爲使用SRAM1 96KB的空間,並把SRAM2的配置註釋掉(彙編語言註釋符號’;’),修改後的配置如下圖所示:

// board\linker_scripts\link.sct
......
LR_IROM1 0x08000000 0x00080000  {    ; load region size_region
  ER_IROM1 0x08000000 0x00080000  {  ; load address = execution address
   *.o (RESET, +First)
   *(InRoot$$Sections)
   .ANY (+RO)
  }
  RW_IRAM1 0x20000000 0x00018000  {  ; RW data
   .ANY (+RW +ZI)
;  RW_IRAM2 0x10000000 0x00008000  {  ; RW data
;   .ANY (+RW +ZI)
  }
}

再打開RT-Thread配置ROM與RAM的文件board\board.h,發現堆空間起始地址HEAP_BEGIN與SRAM1開始地址一致,這是有問題的,在堆之前還需要保存RW段數據與ZI段數據,如下圖所示:
RW/ZI數據段
因此我們需要重定義HEAP_BEGIN在ZI段結尾,該怎麼獲得ZI段結束地址呢?我們找到RT-Thread爲STM32提供的移植模板文件bsp\stm32\libraries\templates\stm32l4xx\board\board.h,從裏面複製出相應的內容到我們工程的board.h文件,修改代碼如下:

// board\board.h
......
#define STM32_FLASH_START_ADRESS       ((uint32_t)0x08000000)
#define STM32_FLASH_SIZE               (512 * 1024)
#define STM32_FLASH_END_ADDRESS        ((uint32_t)(STM32_FLASH_START_ADRESS + STM32_FLASH_SIZE))

#define STM32_SRAM1_SIZE               (96)
#define STM32_SRAM1_START              (0x20000000)
#define STM32_SRAM1_END                (STM32_SRAM1_START + STM32_SRAM1_SIZE * 1024)

#if defined(__CC_ARM) || defined(__CLANG_ARM)
extern int Image$$RW_IRAM1$$ZI$$Limit;
#define HEAP_BEGIN      ((void *)&Image$$RW_IRAM1$$ZI$$Limit)
#elif __ICCARM__
#pragma section="CSTACK"
#define HEAP_BEGIN      (__segment_end("CSTACK"))
#else
extern int __bss_end;
#define HEAP_BEGIN      ((void *)&__bss_end)
#endif

#define HEAP_END                       STM32_SRAM1_END
......

重新配置完MDK與RT-Thread的ROM與RAM地址及空間,在env中執行“scons --target=mdk5”命令,打開MDK工程文件project.uvprojx,編譯無報錯,將程序燒錄到我們的STM32L475 Pandora開發板中,燒錄完成界面如下:
lwip編譯並燒錄
使用putty串口工具與Pandora開發板交互,查詢設備列表,執行ifconfig命令與ping www.baidu.com命令,結果如下:
enc28j60移植結果驗證
ENC28J60網卡已正常註冊名稱爲e0的網絡接口設備,ifconfig命令查看該網卡接口的IP與DNS地址已配置,ping命令可以正常收到遠程主機的回送報文,說明網絡連通正常,到這裏基於ENC28J60移植LWIP協議棧的工作完成了。

三、LwIP示例程序驗證

這裏選擇前面使用QEMU驗證用的UDP與TCP示例程序,使用Sequential API編寫。

3.1 UDP回送示例

把前面QEMU驗證用的UDP回送程序複製過來,也即在applications目錄下新建seqapi_udp_demo.c文件,並打開該文件編輯實現代碼如下:

// applications\seqapi_udp_demo.c

#include "lwip/api.h"
#include "rtthread.h"

static void udpecho_thread(void *arg)
{
  static struct netconn *conn;
  static struct netbuf *buf;
  static ip_addr_t *addr;
  static unsigned short port;

  err_t err;
  LWIP_UNUSED_ARG(arg);

  conn = netconn_new(NETCONN_UDP);
  LWIP_ASSERT("con != NULL", conn != NULL);
  netconn_bind(conn, NULL, 7);

  while (1) {
    err = netconn_recv(conn, &buf);
    if (err == ERR_OK) {
      addr = netbuf_fromaddr(buf);
      port = netbuf_fromport(buf);
      rt_kprintf("addr: %ld, poty: %d.\n", addr->addr, port);

	    err = netconn_send(conn, buf);
      if(err != ERR_OK) {
          LWIP_DEBUGF(LWIP_DBG_ON, ("netconn_send failed: %d\n", (int)err));
      }  
      netbuf_delete(buf);
      rt_thread_mdelay(100);
    }
  }
}

static void udpecho_init(void)
{
  sys_thread_new("udpecho", udpecho_thread, NULL, 1024, 25);
  rt_kprintf("Startup a udp echo server.\n");
}
MSH_CMD_EXPORT_ALIAS(udpecho_init, seqapi_udpecho, sequential api udpecho init);

在env環境執行“scons --target=mdk5”命令,打開MDK工程文件project.uvprojx,編譯無報錯,將程序燒錄到我們的STM32L475 Pandora開發板中,示例運行結果如下:
UDP回送程序驗證lwip移植結果
UDP回送程序運行正常,說明我們移植LWIP可以正常工作,接下來再看一個TCP示例程序。

本示例工程源碼下載地址:https://github.com/StreamAI/LwIP_Projects/tree/master/stm32l475-pandora-lwip

3.2 HTTP控制設備示例

既然我們已經將lwip協議棧移植到開發板上了,開發板上不缺傳感器與執行器,這裏就在之前TCP HTTP服務程序僅展示一個網頁的基礎上,加入網頁控制LED燈亮滅的功能。

在applications目錄下新建seqapi_tcp_demo.c文件,打開該文件並編輯實現代碼如下:

// applications\seqapi_tcp_demo.c

#include "lwip/api.h"
#include "rtthread.h"
#include "board.h"
#include <stdbool.h>

/* defined the LED1 pin: PE9 */
#define LED1_PIN    GET_PIN(E, 9)

const static char http_html_hdr[] = "HTTP/1.1 200 OK\r\nContent-type: text/html\r\n\r\n";
const static char http_index_html[] = "<html><head><title>Congrats!</title></head><body><h1>Welcome to LwIP 2.1.0 HTTP server!</h1> \
                                                        <center><p>This is a test page based on netconn API.</center></body></html>";
const unsigned char LedOn_Data[] ="\
	<HTML>	\
	<head><title>LED Monitor</title></head> \
	<center>	\
	<p>   \
	<center>LED is on!!</center>\
	<form method=post action=\"off\" name=\"ledform\"> \
	<font size=\"2\">Change LED status:</font>  \
	<input type=\"submit\" value=\"off\">	\
	</form> \
	</p>  \
	</center>  \
	</HTML>";

const unsigned char LedOff_Data[] ="\
	<HTML>	\
	<head><title>LED Monitor</title></head> \
	<center>	\
	<p>   \
	<center>LED is off!!</center>\
	<form method=post action=\"on\" name=\"ledform\"> \
	<font size=\"2\">Change LED status:</font>  \
	<input type=\"submit\" value=\"on\">	\
	</form> \
	</p>  \
	</center>  \
	</HTML>";
	
static bool led_on = false;

/*send page*/
static void httpserver_send_html(struct netconn *conn, bool led_status)
{
    netconn_write(conn, http_html_hdr, sizeof(http_html_hdr)-1, NETCONN_NOCOPY);  
    /* Send our HTML page */
    netconn_write(conn, http_index_html, sizeof(http_index_html)-1, NETCONN_NOCOPY);
      
    /* Send our HTML page */
	 if(led_status == true)
        netconn_write(conn, LedOn_Data, sizeof(LedOn_Data)-1, NETCONN_NOCOPY);
	 else
	  	netconn_write(conn, LedOff_Data, sizeof(LedOff_Data)-1, NETCONN_NOCOPY);

}
/** Serve one HTTP connection accepted in the http thread */

static void httpserver_serve(struct netconn *conn)
{
  struct netbuf *inbuf;
  char *buf;
  u16_t buflen;
  err_t err;
  
  /* Read the data from the port, blocking if nothing yet there. 
   We assume the request (the part we care about) is in one netbuf */
  err = netconn_recv(conn, &inbuf);
  
  if (err == ERR_OK) {
    netbuf_data(inbuf, (void**)&buf, &buflen);
    /* Is this an HTTP GET command? (only check the first 5 chars, since
    there are other formats for GET, and we're keeping it very simple )*/
    if (buflen>=5 && buf[0]=='G' && buf[1]=='E' && buf[2]=='T' &&
        buf[3]==' ' && buf[4]=='/' ) {
      
      /* Send the HTML header 
             * subtract 1 from the size, since we dont send the \0 in the string
             * NETCONN_NOCOPY: our data is const static, so no need to copy it
       */
    httpserver_send_html(conn, led_on);
    }
	else if(buflen>=8 && buf[0]=='P' && buf[1]=='O' && buf[2]=='S' && buf[3]=='T')
	{
		if(buf[6]=='o' && buf[7]=='n'){		//請求打開LED
		    led_on = true;
            rt_pin_write(LED1_PIN, PIN_LOW);
		}else if(buf[6]=='o' && buf[7]=='f' && buf[8]=='f'){	//請求關閉LED
		    led_on = false;
            rt_pin_write(LED1_PIN, PIN_HIGH);
	    }

		httpserver_send_html(conn, led_on);
	}

	netbuf_delete(inbuf);
  }
  /* Close the connection (server closes in HTTP) */
  netconn_close(conn);
  
  /* Delete the buffer (netconn_recv gives us ownership,
   so we have to make sure to deallocate the buffer) */
}

/** The main function, never returns! */
static void httpserver_thread(void *arg)
{
  struct netconn *conn, *newconn;
  err_t err;
  LWIP_UNUSED_ARG(arg);
  
  /* Create a new TCP connection handle */
  conn = netconn_new(NETCONN_TCP);
  LWIP_ERROR("http_server: invalid conn", (conn != NULL), return;);

  led_on = true;
  rt_pin_write(LED1_PIN, PIN_LOW);
  
  /* Bind to port 80 (HTTP) with default IP address */
  netconn_bind(conn, NULL, 80);
  
  /* Put the connection into LISTEN state */
  netconn_listen(conn);
  
  do {
    err = netconn_accept(conn, &newconn);
    if (err == ERR_OK) {
      httpserver_serve(newconn);
      netconn_delete(newconn);
    }
  } while(err == ERR_OK);
  LWIP_DEBUGF(HTTPD_DEBUG, ("http_server_netconn_thread: netconn_accept received error %d, shutting down", err));
  netconn_close(conn);
  netconn_delete(conn);
}

/** Initialize the HTTP server (start its thread) */
void httpserver_init()
{
  /* set LED0 pin mode to output */
  rt_pin_mode(LED1_PIN, PIN_MODE_OUTPUT);

  sys_thread_new("http_server_netconn", httpserver_thread, NULL, 1024, TCPIP_THREAD_PRIO + 1);
  rt_kprintf("Startup a tcp web server.\n");
}
MSH_CMD_EXPORT_ALIAS(httpserver_init, seqapi_httpserver, sequential api httpserver init);

在env環境執行“scons --target=mdk5”命令,打開MDK工程文件project.uvprojx,編譯無報錯,將程序燒錄到我們的STM32L475 Pandora開發板中,示例運行結果如下:
HTTP控制設備運行結果
seqapi_httpserver運行起來後,Pandora開發板上的藍燈亮起了,在瀏覽器中輸入開發板的IP地址,可以正常訪問控制LED燈的網頁界面。點擊網頁上的off按鈕後,開發板上的LED藍燈滅了,同時網頁狀態更新爲"LED is off",界面如下:
LED燈滅
網頁可以正常控制開發板上的LED燈亮滅,也就實現了通過TCP/IP網絡遠程控制物聯網設備的功能,在ENC28J60網卡上移植LwIP協議棧運行正常。

本示例工程源碼下載地址:https://github.com/StreamAI/LwIP_Projects/tree/master/stm32l475-pandora-lwip

更多文章:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章