一、移植環境準備
前面主要是基於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-pandora-lwip已經基於STM32L475 Pandora移植好了,可以再次基礎上開發新的功能。如果想了解RT-Thread系統啓動過程和移植過程,可以參考博客:《RT-Thread啓動過程》與《RT-Thread移植過程》,本文的重點是移植LwIP協議棧,這部分就略去了。
stm32l475-pandora-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網卡包括PHY與MAC模塊,具有TX/RX緩衝器,使用SPI接口與MCU通信,支持中斷引腳觸發。我手頭的ENC28J60網卡是從正點原子官方旗艦店採購的,通過NRF Wireless接口插到STM32L475 Pandora開發板上。
查詢STM32L475 Pandora開發板I / O引腳分配表可知,NRF Wireless相關的接口如下:
把ENC28J60模塊插到STM32L475 Pandora開發板上,圖示如下:
STM32L475 SPI接口通訊我在之前的博客:《STM32L4 SPI + QSPI + HAL》與《RT-Thread SPI設備對象管理》中已經詳細介紹過了,本文就不再贅述了。
我們先把底層的SPI2接口配置好,打開board\CubeMX_Config\STM32L475VE.ioc文件,可以看到SPI2已經配置好了,不需要我們再重新配置,SPI2配置界面如下(注意引腳號與上表要一致,這裏只需要配置SPI通信的三個引腳,片選CS由軟件配置):
在env環境中執行menuconfig命令打開圖形化配置界面,使能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網卡驅動,配置界面如下:
在保存配置時彈出了警告窗口:
這個主要是因爲啓用LwIP協議棧條件依賴宏,LwIP協議棧配置中有一項跟ping命令相關的宏RT_LWIP_USING_PING依賴netdev模塊,而netdev模塊並沒有啓動導致的,netdev模塊是RT-Thread提供的一套網卡接口管理層,作用主要是向上提供統一的網卡接口,方便協議棧的移植。
我們進入LwIP模塊配置界面,默認選擇的LwIP協議棧版本是2.0.2,我們選擇最新的2.1.0版本作爲移植對象,配置界面如下:
爲了在移植LwIP後驗證移植是否成功,我們需要使用ping命令,同時爲了方便後續更好物理網卡方便,我們使用RT-Thread提供的網卡接口管理層netdev模塊,該模塊還提供了ifconfig命令用於查看網卡信息,使能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(ð_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(ð_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(ð_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(ð_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,編譯報錯如下:
提示不能打開該文件,我們查找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,配置界面如下:
重新在env中執行“scons --target=mdk5”命令,打開MDK工程文件project.uvprojx,編譯報錯如下:
上面的警告提示是宏定義衝突,而且正好跟前面unistd.h文件中的宏定義一樣,再回頭看看unistd.h文件中的宏定義,在條件宏RT_USING_DFS開啓後,就不再重新定義這些宏定義了,宏定義衝突也就解決了,我們先在menuconfig中開啓條件宏定義RT_USING_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段數據,如下圖所示:
因此我們需要重定義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開發板中,燒錄完成界面如下:
使用putty串口工具與Pandora開發板交互,查詢設備列表,執行ifconfig命令與ping www.baidu.com命令,結果如下:
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可以正常工作,接下來再看一個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開發板中,示例運行結果如下:
seqapi_httpserver運行起來後,Pandora開發板上的藍燈亮起了,在瀏覽器中輸入開發板的IP地址,可以正常訪問控制LED燈的網頁界面。點擊網頁上的off按鈕後,開發板上的LED藍燈滅了,同時網頁狀態更新爲"LED is off",界面如下:
網頁可以正常控制開發板上的LED燈亮滅,也就實現了通過TCP/IP網絡遠程控制物聯網設備的功能,在ENC28J60網卡上移植LwIP協議棧運行正常。
本示例工程源碼下載地址:https://github.com/StreamAI/LwIP_Projects/tree/master/stm32l475-pandora-lwip