精华内容
下载资源
问答
  • LwIP串口当网卡
    2020-10-17 15:52:31

    《嵌入式网络那些事——STM32物联实战》第6.1节:“在LwIP运行的目标系统上可能存在多个网络接口,比如可能有多个网卡,也可能有串行网络接口(串口),还可能有环回接口。”
    这就说明可以用串口作为网卡使用。

    找到一篇帖子实现了“用串口当网卡”,用到了SLIP协议。

    更多相关内容
  • :网络通信技术的迅速发展,电子设备要求远程数据传输功能越来越普遍。现在对于智能仪表的通信,都是通过个工业总线、串口通信来完成的,以后...为了推动产品的网络化,本文在LwIP协议栈的基础上,提出PCP协议的应用。
  • 前面主要是基于QEMU虚拟机环境进行LwIP协议栈开发调试的,如果手头没有开发板可以先在个人电脑上运行QEMU虚拟机以便学习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协议栈在操作系统网络分层架构中的位置,及其各层的调用关系,可以参考博客:网络分层结构 + netdev/SAL原理

    三、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灯亮灭的功能(不熟悉HTTP协议和HTML语法可参考博客:Web三大技术要素)。

    在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>LED Monitor</title></head> \
    									  <body><h1>Welcome to LwIP 2.1.0 HTTP server!</h1> \
                                          <center><p>This is a test page based on netconn API. \
                                          </p></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

    更多文章:

    展开全文
  • 软件主要实现5个任务:时钟初始化/操作系统初始化任务,修改IP、子网掩码、网关的菜单任务,LwIP初始化任务,网口初始化任务,网口向串口串口向网口发送数据任务。给出了主函数和主程序流程图。实际应用效果良好。
  • 关于开启lwIP协议栈的调试输出LWIP_DEBUGF[复制链接]我们在分析lwIP协议栈的时候,会经常看到LWIP_DEBUGF()这个函数的身影。我想lwIP的作者可能为了便于人们去学习和使用lwIP而花了不少时间添加的。其实对于初学者来...

    关于开启lwIP协议栈的调试输出LWIP_DEBUGF

    [复制链接]

    我们在分析lwIP协议栈的时候,会经常看到LWIP_DEBUGF()这个函数的身影。我想lwIP的作者可能为了便于人们去学习和使用lwIP而花了不少时间添加的。

    其实对于初学者来说,要把lwIP协议栈分析清楚不是一件容易的事情,尤其是对TCP/IP协议原理不是很了解的人。文件较多,函数较多,宏较多,调用关系相比一般的C程序来说较复杂。

    我个人认为,有些时候开启一下lwIP的调试信息输出功能,无论是对于我们学习还是查找以太网通信中的故障都是有帮助的。它能够lwIP协议栈中的一些内部函数调用关系,变量值,追踪信息等通过串口输出来。

    总之,

    1.可以查看函数的调用关系,跟踪程序流程。

    2.查看各种协议的调试信息,关键变量的值。

    3.通过以上掌握的很有针对性调试信息,我们可以以此为依据进一步地去优化我们的工程,保证各种资源的分配合理,了解到底是通信过程中的哪一个环节限制了网络的性能,然后加以改善。

    其实,我觉得lwIP协议栈的调试上,作者也是花了不少心思的。把要输出地调试信息分为按协议类型(TCP,UDP,ICMP,ARP....),调试信息类型(LWIP_DBG_TRACE,LWIP_DBG_STATE,LWIP_DBG_FRESH),调试信息级别(LWIP_DBG_LEVEL_OFF,LWIP_DBG_LEVEL_WARNING,LWIP_DBG_LEVEL_SERIOUS,LWIP_DBG_LEVEL_SEVERE)

    之所以要这么做,其实就是对要输出的调试信息有一个更好的管理,当我们开启调试功能后,只输出相关的调试信息,无关的信息就不要输出了。

    接下来以一个简单的例子,说一下,如何开启lwIP的调试功能。

    1.找到debug.h,添加下面这个define。

    #define LWIP_DEBUG

    #ifdef LWIP_DEBUG

    /** print debug message only if debug message type is enabled...

    *  AND is of correct type AND is at least LWIP_DBG_LEVEL

    */

    #define LWIP_DEBUGF(debug, message) do { \

    if ( \

    ((debug) & LWIP_DBG_ON) && \

    ((debug) & LWIP_DBG_TYPES_ON) && \

    ((s16_t)((debug) & LWIP_DBG_MASK_LEVEL) >= LWIP_DBG_MIN_LEVEL)) { \

    LWIP_PLATFORM_DIAG(message); \

    if ((debug) & LWIP_DBG_HALT) { \

    while(1); \

    } \

    } \

    } while(0)

    #else  /* LWIP_DEBUG */

    #define LWIP_DEBUGF(debug, message)

    #endif /* LWIP_DEBUG */

    #endif /* __LWIP_DEBUG_H__ */

    2.在lwIPopts.h的debug options部分,按如下设置,要调试什么就把前边的“//”注释去掉即可。

    //*****************************************************************************

    //

    // ---------- Debugging options ----------

    //

    //*****************************************************************************

    #if 1

    #define U8_F "c"

    #define S8_F "c"

    #define X8_F "x"

    #define U16_F "u"

    #define S16_F "d"

    #define X16_F "x"

    #define U32_F "u"

    #define S32_F "d"

    #define X32_F "x"

    extern void UARTprintf(const char *pcString, ...);

    #define LWIP_PLATFORM_DIAG(x) {UARTprintf x;}

    #define LWIP_DEBUG

    #endif

    #define LWIP_DBG_MIN_LEVEL              LWIP_DBG_LEVEL_OFF

    //#define LWIP_DBG_MIN_LEVEL              LWIP_DBG_LEVEL_WARNING

    //#define LWIP_DBG_MIN_LEVEL              LWIP_DBG_LEVEL_SERIOUS

    //#define LWIP_DBG_MIN_LEVEL              LWIP_DBG_LEVEL_SEVERE

    //#define LWIP_DBG_TYPES_ON               LWIP_DBG_ON

    #define LWIP_DBG_TYPES_ON               (LWIP_DBG_ON|LWIP_DBG_TRACE|LWIP_DBG_STATE|LWIP_DBG_FRESH)

    //#define ETHARP_DEBUG                    LWIP_DBG_ON

    //#define NETIF_DEBUG                     LWIP_DBG_ON

    //#define PBUF_DEBUG                      LWIP_DBG_ON

    //#define API_LIB_DEBUG                   LWIP_DBG_ON

    //#define API_MSG_DEBUG                   LWIP_DBG_ON

    //#define SOCKETS_DEBUG                   LWIP_DBG_ON

    //#define ICMP_DEBUG                      LWIP_DBG_ON

    //#define IGMP_DEBUG                      LWIP_DBG_ON

    //#define INET_DEBUG                      LWIP_DBG_ON

    #define IP_DEBUG                        LWIP_DBG_ON

    //#define IP_REASS_DEBUG                  LWIP_DBG_ON

    //#define RAW_DEBUG                       LWIP_DBG_ON

    //#define MEM_DEBUG                       LWIP_DBG_ON

    //#define MEMP_DEBUG                      LWIP_DBG_ON

    //#define SYS_DEBUG                       LWIP_DBG_ON

    #define TCP_DEBUG                       LWIP_DBG_ON

    //#define TCP_INPUT_DEBUG                 LWIP_DBG_ON

    //#define TCP_FR_DEBUG                    LWIP_DBG_ON

    //#define TCP_RTO_DEBUG                   LWIP_DBG_ON

    //#define TCP_CWND_DEBUG                  LWIP_DBG_ON

    //#define TCP_WND_DEBUG                   LWIP_DBG_ON

    #define TCP_OUTPUT_DEBUG                LWIP_DBG_ON

    //#define TCP_RST_DEBUG                   LWIP_DBG_ON

    //#define TCP_QLEN_DEBUG                  LWIP_DBG_ON

    //#define UDP_DEBUG                       LWIP_DBG_ON

    //#define TCPIP_DEBUG                     LWIP_DBG_ON

    //#define PPP_DEBUG                       LWIP_DBG_ON

    //#define SLIP_DEBUG                      LWIP_DBG_ON

    //#define DHCP_DEBUG                      LWIP_DBG_ON

    //#define AUTOIP_DEBUG                    LWIP_DBG_ON

    //#define SNMP_MSG_DEBUG                  LWIP_DBG_ON

    //#define SNMP_MIB_DEBUG                  LWIP_DBG_ON

    //#define DNS_DEBUG                       LWIP_DBG_ON

    #endif /* __LWIPOPTS_H__ */

    3.当然通过串口输出的话,在main()函数里边还要初始化串口。

    4.通过以上三步基本就可以了,如果没有,看是否在用到>LWIP_DEBUGF()函数的C文件中,#include "lwip/debug.h",或者是include了包含此头文件的其它头文件,如opt.h。这是我调试打开一个网页时输出的调试信息:

    [本帖最后由 academic 于 2011-3-14 17:12 编辑]

    展开全文
  • 前段时间一直在调试lwip协议栈的问题,在stm32F107上实现一个C/S 架构的通信程序。项目初期的时候设计的是B/S架构的控制,然后在使用过程中发现了些限制,因为芯片自身的RAM有限,所以跑B/S的server端略显压力,为了...

    前段时间一直在调试lwip协议栈的问题,在stm32F107上实现一个C/S 架构的通信程序。项目初期的时候设计的是B/S架构的控制,然后在使用过程中发现了些限制,因为芯片自身的RAM有限,所以跑B/S的server端略显压力,为了处理类似动态网页内容,开辟一个5K的缓冲区,然后一次tcp_write就可以将内容发送给浏览器了,当然网页内容也是比较简单,考虑到后续可能会有更多的数据处理,故决定开发一个C/S架构的控制。

    上位机client倒是没什么太多可说的,自己封装下基本的winsock操作。考虑到用TCP协议传输简单地封装了下数据封包和拆包的协议,然后MFC作为图形界面。在stm32端主要采用lwip的RAW API,然后利用callback的方式处理接收上位机命令、数据后的处理,初始化服务器的代码如下:

    void Server_init(void)

    {

    struct tcp_pcb *pcb;

    pcb = tcp_new(); // 动态创建一个pcb

    tcp_bind(pcb, IP_ADDR_ANY, 8082);  // 绑定端口8082

    pcb = tcp_listen(pcb); // 开始监听

    tcp_accept(pcb, Server_accept); // accept成功时的回调函数

    }

    然后在Server_accept中也主要是初始化一些回调函数,

    static err_t Server_accept(void *arg, struct tcp_pcb *pcb, err_t err)

    {

    tcp_err(pcb, Server_conn_err); // 错误时的回调函数

    tcp_recv(pcb, Server_recv); // 接收到数据后的回调函数

    tcp_sent(pcb, Server_sent); // tcp_write数据成功发送后的回调函数

    gRemoteIp = pcb->remote_ip; // 获取远程客户端的地址

    return ERR_OK;

    }

    最重要的函数就是Server_recv()了,在这个函数中,根据客户端不同的命令,然后处理相应的数据发送给客户端,但这是问题就暴露出来了。截取一段发送数据的简化代码:

    for(i = 0; i < WMFlag.WM_Record_Num; ++i) {

    SendCharBuff(pcb, WMTempData, strlen(WMTempData), PT_TEXT);

    }

    其中SendCharBuff主要是调用tcp_write函数,这个当WM_Record_Num这个数值很大时,客户端总是接收不全,后来经过反复地进行实验发现,然来是tcp_write这个函数在循环到12次的时候会返回ERR_MEM的内存错误,这个问题让我百思不得其解,然后通过网上的一些资料,很多人说是lwip协议栈有BUG,然后我姑且相信了这个结论,但是有BUG也得继续调呀。于是便想到了winsock里面有个WSAGetLastError()这个函数,但是lwip里面却没有,网上找了找说可以开启lwip的调试功能,于是乎就开始设置lwip的调试功能了。

    关于开启LWIP的调试功能主要的设置如下:

    1、  在src/include/lwip目录中找到debug.h这个文件,然后在里面添加如下代码

    // Add By 风格独特 2012-06-28

    // 增加串口调试功能

    // 声明外部的USART2_Printf串口输出调试信息函数

    extern void USART2_Printf(const char *format, ...);

    // 定义格式化内容的宏

    // PS 通过调试这个宏发现了以前不知道的一个小知识,那就是字符串可以这样表示

    // “aaaa” “bbb” == “aaaabbb”

    #define U8_F "c"

    #define S8_F "c"

    #define X8_F "x"

    #define U16_F "u"

    #define S16_F "d"

    #define X16_F "x"

    #define U32_F "u"

    #define S32_F "d"

    #define X32_F "x"

    // 设置调试的宏,注释这个宏可以关闭调试功能

    #define LWIP_DEBUG

    #define LWIP_PLATFORM_DIAG(x) USART2_Printf x

    // Add End

    2、  在src/include/lwip目录中找到opt.h这个文件,然后找到如下代码

    #ifndef TCP_OUTPUT_DEBUG

    #define TCP_OUTPUT_DEBUG                LWIP_DBG_OFF

    #endif

    其中将LWIP_DBG_OFF改为LWIP_DBG_ON,即开启了TCP_OUT_DEBUG的调试,当然如果想开启其他的调试输出,就可以将相应的地方改为LWIP_DBG_ON即可。

    3、  实现USART2_Printf这个函数,代码如下

    void USART2_Printf(const char *format,...)

    {

    int iOutLen;

    char buf[256];

    va_list arg_ptr;

    va_start(arg_ptr, format);

    iOutLen = vsprintf(buf, format, arg_ptr);

    va_end(arg_ptr);

    USART_SendStr(buf, iOutLen);

    }

    开启调试后得到如下的调试输出信息:

    tcp_enqueue: 12 (after enqueued)

    tcp_write(pcb=20009c1c, data=08003a24, len=6, apiflags=1)

    tcp_enqueue(pcb=20009c1c, arg=08003a24, len=6, flags=0, apiflags=1)

    tcp_enqueue: queuelen: 12

    tcp_enqueue: too long queue 12 (max 12)

    tcp_output_segment: 6848:6920

    State: ESTABLISHED

    可以看出在tcp_enqueue第12次的时候输出了too long queue 12 (max 12),超出最大的列队次数,于是在工程中搜索too long queue这句话,在tcp_out.c文件中找到了如下代码:

    if ((queuelen > TCP_SND_QUEUELEN) || (queuelen > TCP_SNDQUEUELEN_OVERFLOW)) {

    LWIP_DEBUGF(TCP_OUTPUT_DEBUG | 2, ("tcp_enqueue: queue too long %"U16_F" (%"U16_F")\n", queuelen, TCP_SND_QUEUELEN));

    goto memerr;

    }

    然后发现TCP_SND_QUEUELEN的值为12,跟到TCP_SND_QUEUELEN的定义代码:

    #define TCP_SND_BUF             (2*TCP_MSS)  // 发送缓冲区,为两个MSS的大小

    // 此参数限制了tcp_write的次数,系数默认为6, 改为3000

    #define TCP_SND_QUEUELEN        (3000 * TCP_SND_BUF)/TCP_MSS

    于是将那个默认的系数由6改为3000,再次测试时发现可以连续tcp_write超过12次了,于是解决了tcp_write的连续多次调用后失败的问题。

    最后补充一下tcp_write这个函数的最后一个参数的说明,该函数的声明如下

    err_t tcp_write(struct tcp_pcb *pcb, void *dataptr, u16_t len, u8_t copy);

    其中第四个参数是一个copy参数,当为0时为不拷贝数据,也就是在dataptr所指的缓冲区里面发送数据,因为调用tcp_write成功后数据并不会立即发送,所以要确保dataptr所指的缓冲区内容保持不变,如果调用tcp_write成功后,再改变dataptr缓冲区可能就会和预期发送的数据不相符,当时我也碰到过这个问题,后来将最后的参数改为1,为1时即拷贝缓冲区内容,当执行tcp_write时,会将dataptr所指向的缓冲区内容先拷贝到发送的缓冲区中,这样的话执行tcp_write之后再改变dataptr所指的内容是不影响数据的正确发送的。

    展开全文
  • 本程序是TI的32位单品机LM3S9D96官方开发板上,基于IAR平台的,用C语言编写的,基于LWIP协议的UDP客户端的通信完整工程文件。从电脑传的数据经网口可在液晶屏幕及串口上同时显示。已经调通。改程序综合了,串口,...
  • stm32f407 raw, LWIP长时间跑死,原因是 pcb == pcb->next ,while(pcb != NULL) 死循环。
  • 网络通讯采用4G转以太网和wifi,这两种通讯并不是采用的串口透传,而是采用驱动加上TCP/IP网络协议栈(lwip)。 文档主要讨论TCPIP技术,内容参考了TCPIP详解、老衲五木(朱升林)的微博,朱老师写的微博让我...
  • 本程序是TI的32位单品机LM3S9D96官方开发板上,基于IAR平台的,用C语言编写的,基于LWIP协议的TCP客户端的通信完整工程文件。从电脑传的数据经网口可在液晶屏幕及串口上同时显示。已经调通。改程序综合了,串口,...
  • 主流的物联网产品及芯片主要支持lwip协议栈,这款专为嵌入式开发的轻量级协议栈为flash和ram都不是很强大cpu提供了一个可靠的数据传输,本人目前主要是在嵌入式网络通信方面做开发工作,今天开始与大家一起探讨lwip...
  • 文中介绍一种基于高性能处理器STM32的嵌入式串口设备联网技术的系统设计,该系统充分利用UCOS-II实时操作系统和LwIP轻量型网络协议栈的特点,采用DM9161快速以太网PHY和STM32微处理器等组成,通过以太网实现上位机...
  • 创建互斥信号量函数xSemaphoreCreateMutex()FREERTOS系统移植LWIP协议介绍LWIP协议移植 本次项目开发平台为STM32战舰版 (也是对上一博客说明) 前言 FREERTOS是由safeRTOS衍生的一套操作系统,由Richard Ba
  • 关于STM32使用LWIP协议栈二次初始化时无法成功初始化TCP服务器----内存碎片化问题以及解决方法 关于LWIP协议栈的话后期再出一个相关的系列文章吧,关于使用LAN8720芯片断网线重连的问题可以参考:我的这篇博客 这里...
  • Zynq-7000基于zynq平台裸跑LWIP协议栈的详解(万字长文)

    千次阅读 多人点赞 2020-12-13 17:59:47
    1. LWIP协议栈·· 1 1.1 LWIP库·· 2 1.2 LwIP原理分析·· 3 1.2.1 动态内存管理·· 3 1.2.2 数据包pbuf 4 1.2.3 网络接口·· 7 1.3 PS的千兆以太网控制器·· 7 2. 硬件部署·· 9 2.1 Ethernet硬件设计·· 9...
  • 2、在SDK中,创建一个lwIP Echo Server工程,什么都不改,直接编译,下载,需要注意,因为没有调用任何PL资源,因此可以不用下载FPGA 3、全速运行,打开串口CTR,观察串口打印信息,发现卡在网络..
  • 一、基本概念 1、网络协议 GPRS无线数据传输的最低层,即物理层是通过RS232串口及GPRS模块组成的,然后是数据链路层。...针对LWIP来看,硬件结构可以分为网卡或者串口两种物理层架构,如果用到网卡将使用
  • 1.1 TCP/IP 协议简介 1.2 lwIP 简介 1.2.1 目录结构 lwip-2.1.2 1.2.2 lwip-2.1.2/src 2 目录结构详解 2.1 /doc 2.2 /test 2.3 /src 2.3.1 APIs 2.3.2 /src/api 2.3.3 /src/apps 2.3.4 /src/core 2.3.5 ...
  • 文中提出了一种低成本、高性能的嵌入式串口服务器的硬软件设计方案。该服务器以ARM7芯片LPC2210为核心...对轻便TCP/ IP协议栈LW IP在μC/OS - Ⅱ实时操作系统中进行了移植, 并对16路串行通道设计了实时多任务方案。
  • 野火串口助手协议发送文件通讯协议 修订历史 日期 版本 更新内容 2020/6/22 0.0.1 首次发布 XMODEM协议 上位机是现实了XModem-CRC16和XModem-1K; XModem-CRC16使用CRC-16校验方式,数据长度为128字节 ...
  • 引言:本节我们继续使用Xilinx SDK自带的LwIP协议测试例程测试电路板千兆网接口,验证电路板PHY硬件设计是否正确。 1.实验系统框图 本实验系统框图如图1所示。图1中PHY在电路图上连接至PS侧BANK501 MIO接口,UART...
  • DP83848本身没有MAC地址(网卡地址),所以需要我们自己生成一个MAC地址,同时告诉STM32 ETH和lwip协议栈。这里的生成方式是,前三位固定为00:80:E1,后三位由STM32的3个32位器件ID值(HAL_GetUIDw0~2)相加生成。...
  • 1.LWIP协议栈配合串口调试助手测试 调试助手发送数据为字符串格式,在LWIP协议中可使用strcmp()函数进行比对。 如:if(strcmp(recv_data,p-&gt;payload) == 0){} lwip发送数据tcp_write();在网络调试...
  • 通常串口服务器采用ARM9微处理器和带TCP/IP协议栈的付费操作系统,而文中提出的串口服务器是通过移植LWIP协议栈到代码开源的μC/OS-Ⅱ中实现,这样不但降低成本,而且代码的编写更加透明、灵活。
  • stm32f407移值rt_thread加lwip协议,DHCP获取IP地址成功后想在程序中使用获取到的IP地址 找了很久没有找到相关文档,串口调试输入ifconfig倒是可以看到IP地址,具体在程序中哪个函数获取到的IP地址却找不到。 ...
  • 由于STM32CudeMX内集成LWIP(TCP/IP协议栈),不需要我们进行复杂的移植,只需简单的配置。 1.选择时钟源 在前文已经讲过了,这里使用MCO,所以HSE选择BYPASS旁路,也就是ST-Link输入的时钟源,时钟输入为8M。 2....
  • LWIP在STM32上的移植

    2019-10-08 13:47:24
    文章标题:STM32使用LWIP实现DHCP客户端 http://www.cnblogs.com/dengxiaojun/p/4379545.html 该文章介绍了几点,LWIP源码的内容。关键点:1、include下新建arch文件夹,在arch文件夹下的sys_arch.c多任务定时...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 911
精华内容 364
关键字:

串口lwip协议