请稍侯

Uboot的网络

7. net

uboot 支持 tcp/ip 网络协议,但是作为一个 bootloader 它并没有把协议栈作为一个后台线程长时间运行,而是在使用到网络功能时才会初始化协议栈、使用网络功能。

7.1. 协议栈主循环

net_loopnet/net.c) 是网络协议栈的主循环,所有的网络操作最终都会进入这里。

int net_loop(enum proto_t protocol)
{
   ...
    if (eth_is_on_demand_init() || protocol != NETCONS) {
        eth_halt();
        eth_set_current();
        ret = eth_init();
        if (ret < 0) {
            eth_halt();
            return ret;
        }
    } else {
        eth_init_state_only();
    }
restart:
...
    switch (net_check_prereq(protocol)) {
    case 1:
        /* network not configured */
        eth_halt();
        return -ENODEV;

    case 2:
        /* network device not configured */
        break;

    case 0:
        net_dev_exists = 1;
        net_boot_file_size = 0;
        switch (protocol) {
        case TFTPGET:
#ifdef CONFIG_CMD_TFTPPUT
        case TFTPPUT:
#endif
            /* always use ARP to get server ethernet address */
            tftp_start(protocol);
            break;
#ifdef CONFIG_CMD_TFTPSRV
        case TFTPSRV:
            tftp_start_server();
            break;
#endif
#if defined(CONFIG_CMD_DHCP)
        case DHCP:
            bootp_reset();
            net_ip.s_addr = 0;
            dhcp_request();     /* Basically same as BOOTP */
            break;
#endif

        case BOOTP:
            bootp_reset();
            net_ip.s_addr = 0;
            bootp_request();
            break;

#if defined(CONFIG_CMD_RARP)
        case RARP:
            rarp_try = 0;
            net_ip.s_addr = 0;
            rarp_request();
            break;
#endif
#if defined(CONFIG_CMD_PING)
        case PING:
            ping_start();
            break;
#endif
#if defined(CONFIG_CMD_NFS)
        case NFS:
            nfs_start();
            break;
#endif
#if defined(CONFIG_CMD_CDP)
        case CDP:
            cdp_start();
            break;
#endif
#if defined(CONFIG_NETCONSOLE) && !(CONFIG_SPL_BUILD)
        case NETCONS:
            nc_start();
            break;
#endif
#if defined(CONFIG_CMD_SNTP)
        case SNTP:
            sntp_start();
            break;
#endif
#if defined(CONFIG_CMD_DNS)
        case DNS:
            dns_start();
            break;
#endif
#if defined(CONFIG_CMD_LINK_LOCAL)
        case LINKLOCAL:
            link_local_start();
            break;
#endif
        default:
            break;
        }

        break;
    }

#if defined(CONFIG_MII) || defined(CONFIG_CMD_MII)
#if defined(CONFIG_SYS_FAULT_ECHO_LINK_DOWN)    && \
    defined(CONFIG_STATUS_LED)          && \
    defined(STATUS_LED_RED)
    /*
     * Echo the inverted link state to the fault LED.
     */
    if (miiphy_link(eth_get_dev()->name, CONFIG_SYS_FAULT_MII_ADDR))
        status_led_set(STATUS_LED_RED, STATUS_LED_OFF);
    else
        status_led_set(STATUS_LED_RED, STATUS_LED_ON);
#endif /* CONFIG_SYS_FAULT_ECHO_LINK_DOWN, ... */
#endif /* CONFIG_MII, ... */
#ifdef CONFIG_USB_KEYBOARD
    net_busy_flag = 1;
#endif

    /*
     *  Main packet reception loop.  Loop receiving packets until
     *  someone sets `net_state' to a state that terminates.
     */
    for (;;) {
        WATCHDOG_RESET();
#ifdef CONFIG_SHOW_ACTIVITY
        show_activity(1);
#endif
        if (arp_timeout_check() > 0)
            time_start = get_timer(0);

        /*
         *  Check the ethernet for a new packet.  The ethernet
         *  receive routine will process it.
         *  Most drivers return the most recent packet size, but not
         *  errors that may have happened.
         */
        eth_rx();

        /*
         *  Abort if ctrl-c was pressed.
         */
        if (ctrlc()) {
            /* cancel any ARP that may not have completed */
            net_arp_wait_packet_ip.s_addr = 0;

            net_cleanup_loop();
            eth_halt();
            /* Invalidate the last protocol */
            eth_set_last_protocol(BOOTP);

            puts("\nAbort\n");
            /* include a debug print as well incase the debug
               messages are directed to stderr */
            debug_cond(DEBUG_INT_STATE, "--- net_loop Abort!\n");
            ret = -EINTR;
            goto done;
        }

        /*
         *  Check for a timeout, and run the timeout handler
         *  if we have one.
         */
        if (time_handler &&
            ((get_timer(0) - time_start) > time_delta)) {
            thand_f *x;

#if defined(CONFIG_MII) || defined(CONFIG_CMD_MII)
#if defined(CONFIG_SYS_FAULT_ECHO_LINK_DOWN)    && \
    defined(CONFIG_STATUS_LED)          && \
    defined(STATUS_LED_RED)
            /*
             * Echo the inverted link state to the fault LED.
             */
            if (miiphy_link(eth_get_dev()->name,
                    CONFIG_SYS_FAULT_MII_ADDR))
                status_led_set(STATUS_LED_RED, STATUS_LED_OFF);
            else
                status_led_set(STATUS_LED_RED, STATUS_LED_ON);
#endif /* CONFIG_SYS_FAULT_ECHO_LINK_DOWN, ... */
#endif /* CONFIG_MII, ... */
            debug_cond(DEBUG_INT_STATE, "--- net_loop timeout\n");
            x = time_handler;
            time_handler = (thand_f *)0;
            (*x)();
        }

        if (net_state == NETLOOP_FAIL)
            ret = net_start_again();

        switch (net_state) {
        case NETLOOP_RESTART:
            net_restarted = 1;
            goto restart;

        case NETLOOP_SUCCESS:
            net_cleanup_loop();
            if (net_boot_file_size > 0) {
                printf("Bytes transferred = %d (%x hex)\n",
                       net_boot_file_size, net_boot_file_size);
                setenv_hex("filesize", net_boot_file_size);
                setenv_hex("fileaddr", load_addr);
            }
            if (protocol != NETCONS)
                eth_halt();
            else
                eth_halt_state_only();

            eth_set_last_protocol(protocol);

            ret = net_boot_file_size;
            debug_cond(DEBUG_INT_STATE, "--- net_loop Success!\n");
            goto done;

        case NETLOOP_FAIL:
            net_cleanup_loop();
            /* Invalidate the last protocol */
            eth_set_last_protocol(BOOTP);
            debug_cond(DEBUG_INT_STATE, "--- net_loop Fail!\n");
            goto done;

        case NETLOOP_CONTINUE:
            continue;
        }
    }

done:
#ifdef CONFIG_USB_KEYBOARD
    net_busy_flag = 0;
#endif
#ifdef CONFIG_CMD_TFTPPUT
    /* Clear out the handlers */
    net_set_udp_handler(NULL);
    net_set_icmp_handler(NULL);
#endif
    return ret;
}

从上面的代码可以看出 uboot 网络处里流程和一般的协议栈类似(如 lwip 所有的操作都是放在一个循环,在一个线程中运行)。

流程可以分解为 3 步:

  1. 初始化协议栈
    net_init();
    net_init_loop();
    
  2. 区分不同的子协议(如 ping 、 ICMP 、 arp 等)
    case DHCP:
     ...
    case PING:
     ...
    case DNS:
     ...
    ...
    
  3. 关闭协议栈和网络设备
    ...
    eth_halt();
    ...
    net_cleanup_loop();
    ...
    

7.2. 协议栈和驱动

协议栈收发数据都是通过函数 eth_send()eth_rx() 完成的。

发包:

int eth_send(void *packet, int length)                         
{                                                              
...                                   
    current = eth_get_dev();                                   
...
    ret = eth_get_ops(current)->send(current, packet, length); 
    if (ret < 0) {                                             
        /* We cannot completely return the error at present */ 
        debug("%s: send() returned error %d\n", __func__, ret);
    }                                                          
    return ret;                                                
}                                                              

收包:

int eth_rx(void)
{
...
    current = eth_get_dev();
...  
    for (i = 0; i < 32; i++) {
        ret = eth_get_ops(current)->recv(current, flags, &packet);
        flags = 0;
        if (ret > 0)
            net_process_received_packet(packet, ret);
        if (ret >= 0 && eth_get_ops(current)->free_pkt)
            eth_get_ops(current)->free_pkt(current, packet, ret);
        if (ret <= 0)
            break;
    }
...
    return ret;
}

两者最终都是调用实际的驱动函数 cpsw_send()cpsw_recv() 进行收发包

7.3. ping

uboot 网络支持多种操作,但是都有一个特点就是必须 uboot 首先发起操作,然后相应对端的操作,并不能像一般的系统那样时时刻刻接受对端的网络请求。

ping 的实现也只是向外发送 ping 包,然后接收响应,并不会响应对端发起的 ping 操作。

ping 是作为一个命令(ping)存在于 uboot 的,它的函数实现是 do_ping()cmd/net.c) :

static int do_ping(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{
...
    if (net_loop(PING) < 0) {
        printf("ping failed; host %s is not alive\n", argv[1]);
        return CMD_RET_FAILURE;
    }
...
}

然后进入 net_loop() ,首先向外发 ping 包 :

...
switch (protocol) {
    ...
    case PING:
        ping_start();
        break;
    ...
}

调用 ping 的发包函数

void ping_start(void)
{
    printf("Using %s device\n", eth_get_name());
    net_set_timeout_handler(10000UL, ping_timeout_handler);

    ping_send();
}
static int ping_send(void)
{
...
    arp_request();
    return 1;   /* waiting */
}

arp_request() 最终会执行 eth_send() 调用网卡驱动发送数据包。

接下来,net_loop() 会等待对端发送的响应包,并进行处理:

for (;;) {
    ...
    eth_rx();
    ...
}

eth_rx() 调用网卡驱动,而网卡驱动会调用函数 net_process_received_packet() 处理协议栈相关的操作,此处就是执行 ping_receive() 完成 ping 操作的阶段:

void ping_receive(struct ethernet_hdr *et, struct ip_udp_hdr *ip, int len)
{
    struct icmp_hdr *icmph = (struct icmp_hdr *)&ip->udp_src;
    struct in_addr src_ip;
    int eth_hdr_size;

    switch (icmph->type) {
    case ICMP_ECHO_REPLY:
        src_ip = net_read_ip((void *)&ip->ip_src);
        if (src_ip.s_addr == net_ping_ip.s_addr)
            net_set_state(NETLOOP_SUCCESS);
        return;
    ...
/*  default:
        return;*/
    }
}

通过函数 net_set_state() 告知 net_loop() ping 操作成功,否则 ping 失败,即网络有问题。

7.4. tftp

tftp 分两部分:向服务器发送请求和从服务器接收数据,所以在 net_loop() 中 tftp 协议有两组判断条件:

int net_loop(enum proto_t protocol)
{
    ...
        case TFTPGET:
#ifdef CONFIG_CMD_TFTPPUT
        case TFTPPUT:
#endif
            /* always use ARP to get server ethernet address */
            tftp_start(protocol);
            break;
    ...

}

发送请求(TFTPGETTFTPPUT),向对端发送 tftp 请求,比如 GET 和 PUT ,调用 tftp_start() 发送请求报文:

void tftp_start(enum proto_t protocol)
{
...
    net_set_udp_handler(tftp_handler);
...
    tftp_send();
...
}

其中 net_set_udp_handler() 会注册 tftp_handler() 到钩子函数 static rxhand_f *udp_packet_handler;,而 tftp_handler() 会调用 tftp_complete() 检查接收文件是否结束。

接下来 net_loop() 会循环调用 eth_rx() 接收数据包,直到所有数据都收完(钩子函数 udp_packet_handler 会不断的检查已收到的文件长度和实际文件长度是否一直)。

7.5. 小结

uboot 的网络协议栈可以说是麻雀虽小肝胆俱全,网络协议栈所需要的功能它基本都实现了,比如 ping 、 tftp 、tftp server 、DHCP 、 bootp 、 arp/rarp 、DNS 等,它主要侧重于功能的实现,对实时性、并发性等要求不高,所以都是由用户输入命令主动发起操作,并且是单线程操作。