| /* |
| * Copyright (C) 2007 Michael Brown <mbrown@fensystems.co.uk>. |
| * |
| * This program is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU General Public License as |
| * published by the Free Software Foundation; either version 2 of the |
| * License, or any later version. |
| * |
| * This program is distributed in the hope that it will be useful, but |
| * WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. |
| */ |
| |
| FILE_LICENCE ( GPL2_OR_LATER ); |
| |
| #include <string.h> |
| #include <pxe.h> |
| #include <realmode.h> |
| #include <pic8259.h> |
| #include <biosint.h> |
| #include <pnpbios.h> |
| #include <basemem_packet.h> |
| #include <gpxe/io.h> |
| #include <gpxe/iobuf.h> |
| #include <gpxe/netdevice.h> |
| #include <gpxe/if_ether.h> |
| #include <gpxe/ethernet.h> |
| #include <undi.h> |
| #include <undinet.h> |
| #include <pxeparent.h> |
| |
| |
| /** @file |
| * |
| * UNDI network device driver |
| * |
| */ |
| |
| /** An UNDI NIC */ |
| struct undi_nic { |
| /** Assigned IRQ number */ |
| unsigned int irq; |
| /** Currently processing ISR */ |
| int isr_processing; |
| /** Bug workarounds */ |
| int hacks; |
| }; |
| |
| /** |
| * @defgroup undi_hacks UNDI workarounds |
| * @{ |
| */ |
| |
| /** Work around Etherboot 5.4 bugs */ |
| #define UNDI_HACK_EB54 0x0001 |
| |
| /** @} */ |
| |
| static void undinet_close ( struct net_device *netdev ); |
| |
| /** Address of UNDI entry point */ |
| static SEGOFF16_t undinet_entry; |
| |
| /***************************************************************************** |
| * |
| * UNDI interrupt service routine |
| * |
| ***************************************************************************** |
| */ |
| |
| /** |
| * UNDI interrupt service routine |
| * |
| * The UNDI ISR increments a counter (@c trigger_count) and exits. |
| */ |
| extern void undiisr ( void ); |
| |
| /** IRQ number */ |
| uint8_t __data16 ( undiisr_irq ); |
| #define undiisr_irq __use_data16 ( undiisr_irq ) |
| |
| /** IRQ chain vector */ |
| struct segoff __data16 ( undiisr_next_handler ); |
| #define undiisr_next_handler __use_data16 ( undiisr_next_handler ) |
| |
| /** IRQ trigger count */ |
| volatile uint8_t __data16 ( undiisr_trigger_count ) = 0; |
| #define undiisr_trigger_count __use_data16 ( undiisr_trigger_count ) |
| |
| /** Last observed trigger count */ |
| static unsigned int last_trigger_count = 0; |
| |
| /** |
| * Hook UNDI interrupt service routine |
| * |
| * @v irq IRQ number |
| */ |
| static void undinet_hook_isr ( unsigned int irq ) { |
| |
| assert ( irq <= IRQ_MAX ); |
| assert ( undiisr_irq == 0 ); |
| |
| undiisr_irq = irq; |
| hook_bios_interrupt ( IRQ_INT ( irq ), |
| ( ( unsigned int ) undiisr ), |
| &undiisr_next_handler ); |
| } |
| |
| /** |
| * Unhook UNDI interrupt service routine |
| * |
| * @v irq IRQ number |
| */ |
| static void undinet_unhook_isr ( unsigned int irq ) { |
| |
| assert ( irq <= IRQ_MAX ); |
| |
| unhook_bios_interrupt ( IRQ_INT ( irq ), |
| ( ( unsigned int ) undiisr ), |
| &undiisr_next_handler ); |
| undiisr_irq = 0; |
| } |
| |
| /** |
| * Test to see if UNDI ISR has been triggered |
| * |
| * @ret triggered ISR has been triggered since last check |
| */ |
| static int undinet_isr_triggered ( void ) { |
| unsigned int this_trigger_count; |
| |
| /* Read trigger_count. Do this only once; it is volatile */ |
| this_trigger_count = undiisr_trigger_count; |
| |
| if ( this_trigger_count == last_trigger_count ) { |
| /* Not triggered */ |
| return 0; |
| } else { |
| /* Triggered */ |
| last_trigger_count = this_trigger_count; |
| return 1; |
| } |
| } |
| |
| /***************************************************************************** |
| * |
| * UNDI network device interface |
| * |
| ***************************************************************************** |
| */ |
| |
| /** UNDI transmit buffer descriptor */ |
| static struct s_PXENV_UNDI_TBD __data16 ( undinet_tbd ); |
| #define undinet_tbd __use_data16 ( undinet_tbd ) |
| |
| /** |
| * Transmit packet |
| * |
| * @v netdev Network device |
| * @v iobuf I/O buffer |
| * @ret rc Return status code |
| */ |
| static int undinet_transmit ( struct net_device *netdev, |
| struct io_buffer *iobuf ) { |
| struct s_PXENV_UNDI_TRANSMIT undi_transmit; |
| size_t len = iob_len ( iobuf ); |
| int rc; |
| |
| /* Technically, we ought to make sure that the previous |
| * transmission has completed before we re-use the buffer. |
| * However, many PXE stacks (including at least some Intel PXE |
| * stacks and Etherboot 5.4) fail to generate TX completions. |
| * In practice this won't be a problem, since our TX datapath |
| * has a very low packet volume and we can get away with |
| * assuming that a TX will be complete by the time we want to |
| * transmit the next packet. |
| */ |
| |
| /* Copy packet to UNDI I/O buffer */ |
| if ( len > sizeof ( basemem_packet ) ) |
| len = sizeof ( basemem_packet ); |
| memcpy ( &basemem_packet, iobuf->data, len ); |
| |
| /* Create PXENV_UNDI_TRANSMIT data structure */ |
| memset ( &undi_transmit, 0, sizeof ( undi_transmit ) ); |
| undi_transmit.DestAddr.segment = rm_ds; |
| undi_transmit.DestAddr.offset = __from_data16 ( &undinet_tbd ); |
| undi_transmit.TBD.segment = rm_ds; |
| undi_transmit.TBD.offset = __from_data16 ( &undinet_tbd ); |
| |
| /* Create PXENV_UNDI_TBD data structure */ |
| undinet_tbd.ImmedLength = len; |
| undinet_tbd.Xmit.segment = rm_ds; |
| undinet_tbd.Xmit.offset = __from_data16 ( basemem_packet ); |
| |
| /* Issue PXE API call */ |
| if ( ( rc = pxeparent_call ( undinet_entry, PXENV_UNDI_TRANSMIT, |
| &undi_transmit, |
| sizeof ( undi_transmit ) ) ) != 0 ) |
| goto done; |
| |
| /* Free I/O buffer */ |
| netdev_tx_complete ( netdev, iobuf ); |
| |
| done: |
| return rc; |
| } |
| |
| /** |
| * Poll for received packets |
| * |
| * @v netdev Network device |
| * |
| * Fun, fun, fun. UNDI drivers don't use polling; they use |
| * interrupts. We therefore cheat and pretend that an interrupt has |
| * occurred every time undinet_poll() is called. This isn't too much |
| * of a hack; PCI devices share IRQs and so the first thing that a |
| * proper ISR should do is call PXENV_UNDI_ISR to determine whether or |
| * not the UNDI NIC generated the interrupt; there is no harm done by |
| * spurious calls to PXENV_UNDI_ISR. Similarly, we wouldn't be |
| * handling them any more rapidly than the usual rate of |
| * undinet_poll() being called even if we did implement a full ISR. |
| * So it should work. Ha! |
| * |
| * Addendum (21/10/03). Some cards don't play nicely with this trick, |
| * so instead of doing it the easy way we have to go to all the hassle |
| * of installing a genuine interrupt service routine and dealing with |
| * the wonderful 8259 Programmable Interrupt Controller. Joy. |
| * |
| * Addendum (10/07/07). When doing things such as iSCSI boot, in |
| * which we have to co-operate with a running OS, we can't get away |
| * with the "ISR-just-increments-a-counter-and-returns" trick at all, |
| * because it involves tying up the PIC for far too long, and other |
| * interrupt-dependent components (e.g. local disks) start breaking. |
| * We therefore implement a "proper" ISR which calls PXENV_UNDI_ISR |
| * from within interrupt context in order to deassert the device |
| * interrupt, and sends EOI if applicable. |
| */ |
| static void undinet_poll ( struct net_device *netdev ) { |
| struct undi_nic *undinic = netdev->priv; |
| struct s_PXENV_UNDI_ISR undi_isr; |
| struct io_buffer *iobuf = NULL; |
| size_t len; |
| size_t frag_len; |
| size_t max_frag_len; |
| int rc; |
| |
| if ( ! undinic->isr_processing ) { |
| /* Do nothing unless ISR has been triggered */ |
| if ( ! undinet_isr_triggered() ) { |
| /* Allow interrupt to occur */ |
| __asm__ __volatile__ ( REAL_CODE ( "sti\n\t" |
| "nop\n\t" |
| "nop\n\t" |
| "cli\n\t" ) : : ); |
| return; |
| } |
| |
| /* Start ISR processing */ |
| undinic->isr_processing = 1; |
| undi_isr.FuncFlag = PXENV_UNDI_ISR_IN_PROCESS; |
| } else { |
| /* Continue ISR processing */ |
| undi_isr.FuncFlag = PXENV_UNDI_ISR_IN_GET_NEXT; |
| } |
| |
| /* Run through the ISR loop */ |
| while ( 1 ) { |
| if ( ( rc = pxeparent_call ( undinet_entry, PXENV_UNDI_ISR, |
| &undi_isr, |
| sizeof ( undi_isr ) ) ) != 0 ) |
| break; |
| switch ( undi_isr.FuncFlag ) { |
| case PXENV_UNDI_ISR_OUT_TRANSMIT: |
| /* We don't care about transmit completions */ |
| break; |
| case PXENV_UNDI_ISR_OUT_RECEIVE: |
| /* Packet fragment received */ |
| len = undi_isr.FrameLength; |
| frag_len = undi_isr.BufferLength; |
| if ( ( len == 0 ) || ( len < frag_len ) ) { |
| /* Don't laugh. VMWare does it. */ |
| DBGC ( undinic, "UNDINIC %p reported insane " |
| "fragment (%zd of %zd bytes)\n", |
| undinic, frag_len, len ); |
| netdev_rx_err ( netdev, NULL, -EINVAL ); |
| break; |
| } |
| if ( ! iobuf ) |
| iobuf = alloc_iob ( len ); |
| if ( ! iobuf ) { |
| DBGC ( undinic, "UNDINIC %p could not " |
| "allocate %zd bytes for RX buffer\n", |
| undinic, len ); |
| /* Fragment will be dropped */ |
| netdev_rx_err ( netdev, NULL, -ENOMEM ); |
| goto done; |
| } |
| max_frag_len = iob_tailroom ( iobuf ); |
| if ( frag_len > max_frag_len ) { |
| DBGC ( undinic, "UNDINIC %p fragment too big " |
| "(%zd+%zd does not fit into %zd)\n", |
| undinic, iob_len ( iobuf ), frag_len, |
| ( iob_len ( iobuf ) + max_frag_len ) ); |
| frag_len = max_frag_len; |
| } |
| copy_from_real ( iob_put ( iobuf, frag_len ), |
| undi_isr.Frame.segment, |
| undi_isr.Frame.offset, frag_len ); |
| if ( iob_len ( iobuf ) == len ) { |
| /* Whole packet received; deliver it */ |
| netdev_rx ( netdev, iob_disown ( iobuf ) ); |
| /* Etherboot 5.4 fails to return all packets |
| * under mild load; pretend it retriggered. |
| */ |
| if ( undinic->hacks & UNDI_HACK_EB54 ) |
| --last_trigger_count; |
| } |
| break; |
| case PXENV_UNDI_ISR_OUT_DONE: |
| /* Processing complete */ |
| undinic->isr_processing = 0; |
| goto done; |
| default: |
| /* Should never happen. VMWare does it routinely. */ |
| DBGC ( undinic, "UNDINIC %p ISR returned invalid " |
| "FuncFlag %04x\n", undinic, undi_isr.FuncFlag ); |
| undinic->isr_processing = 0; |
| goto done; |
| } |
| undi_isr.FuncFlag = PXENV_UNDI_ISR_IN_GET_NEXT; |
| } |
| |
| done: |
| if ( iobuf ) { |
| DBGC ( undinic, "UNDINIC %p returned incomplete packet " |
| "(%zd of %zd)\n", undinic, iob_len ( iobuf ), |
| ( iob_len ( iobuf ) + iob_tailroom ( iobuf ) ) ); |
| netdev_rx_err ( netdev, iobuf, -EINVAL ); |
| } |
| } |
| |
| /** |
| * Open NIC |
| * |
| * @v netdev Net device |
| * @ret rc Return status code |
| */ |
| static int undinet_open ( struct net_device *netdev ) { |
| struct undi_nic *undinic = netdev->priv; |
| struct s_PXENV_UNDI_SET_STATION_ADDRESS undi_set_address; |
| struct s_PXENV_UNDI_OPEN undi_open; |
| int rc; |
| |
| /* Hook interrupt service routine and enable interrupt */ |
| undinet_hook_isr ( undinic->irq ); |
| enable_irq ( undinic->irq ); |
| send_eoi ( undinic->irq ); |
| |
| /* Set station address. Required for some PXE stacks; will |
| * spuriously fail on others. Ignore failures. We only ever |
| * use it to set the MAC address to the card's permanent value |
| * anyway. |
| */ |
| memcpy ( undi_set_address.StationAddress, netdev->ll_addr, |
| sizeof ( undi_set_address.StationAddress ) ); |
| pxeparent_call ( undinet_entry, PXENV_UNDI_SET_STATION_ADDRESS, |
| &undi_set_address, sizeof ( undi_set_address ) ); |
| |
| /* Open NIC. We ask for promiscuous operation, since it's the |
| * only way to ask for all multicast addresses. On any |
| * switched network, it shouldn't really make a difference to |
| * performance. |
| */ |
| memset ( &undi_open, 0, sizeof ( undi_open ) ); |
| undi_open.PktFilter = ( FLTR_DIRECTED | FLTR_BRDCST | FLTR_PRMSCS ); |
| if ( ( rc = pxeparent_call ( undinet_entry, PXENV_UNDI_OPEN, |
| &undi_open, sizeof ( undi_open ) ) ) != 0 ) |
| goto err; |
| |
| DBGC ( undinic, "UNDINIC %p opened\n", undinic ); |
| return 0; |
| |
| err: |
| undinet_close ( netdev ); |
| return rc; |
| } |
| |
| /** |
| * Close NIC |
| * |
| * @v netdev Net device |
| */ |
| static void undinet_close ( struct net_device *netdev ) { |
| struct undi_nic *undinic = netdev->priv; |
| struct s_PXENV_UNDI_ISR undi_isr; |
| struct s_PXENV_UNDI_CLOSE undi_close; |
| int rc; |
| |
| /* Ensure ISR has exited cleanly */ |
| while ( undinic->isr_processing ) { |
| undi_isr.FuncFlag = PXENV_UNDI_ISR_IN_GET_NEXT; |
| if ( ( rc = pxeparent_call ( undinet_entry, PXENV_UNDI_ISR, |
| &undi_isr, |
| sizeof ( undi_isr ) ) ) != 0 ) |
| break; |
| switch ( undi_isr.FuncFlag ) { |
| case PXENV_UNDI_ISR_OUT_TRANSMIT: |
| case PXENV_UNDI_ISR_OUT_RECEIVE: |
| /* Continue draining */ |
| break; |
| default: |
| /* Stop processing */ |
| undinic->isr_processing = 0; |
| break; |
| } |
| } |
| |
| /* Close NIC */ |
| pxeparent_call ( undinet_entry, PXENV_UNDI_CLOSE, |
| &undi_close, sizeof ( undi_close ) ); |
| |
| /* Disable interrupt and unhook ISR */ |
| disable_irq ( undinic->irq ); |
| undinet_unhook_isr ( undinic->irq ); |
| |
| DBGC ( undinic, "UNDINIC %p closed\n", undinic ); |
| } |
| |
| /** |
| * Enable/disable interrupts |
| * |
| * @v netdev Net device |
| * @v enable Interrupts should be enabled |
| */ |
| static void undinet_irq ( struct net_device *netdev, int enable ) { |
| struct undi_nic *undinic = netdev->priv; |
| |
| /* Cannot support interrupts yet */ |
| DBGC ( undinic, "UNDINIC %p cannot %s interrupts\n", |
| undinic, ( enable ? "enable" : "disable" ) ); |
| } |
| |
| /** UNDI network device operations */ |
| static struct net_device_operations undinet_operations = { |
| .open = undinet_open, |
| .close = undinet_close, |
| .transmit = undinet_transmit, |
| .poll = undinet_poll, |
| .irq = undinet_irq, |
| }; |
| |
| /** |
| * Probe UNDI device |
| * |
| * @v undi UNDI device |
| * @ret rc Return status code |
| */ |
| int undinet_probe ( struct undi_device *undi ) { |
| struct net_device *netdev; |
| struct undi_nic *undinic; |
| struct s_PXENV_START_UNDI start_undi; |
| struct s_PXENV_UNDI_STARTUP undi_startup; |
| struct s_PXENV_UNDI_INITIALIZE undi_initialize; |
| struct s_PXENV_UNDI_GET_INFORMATION undi_info; |
| struct s_PXENV_UNDI_GET_IFACE_INFO undi_iface; |
| struct s_PXENV_UNDI_SHUTDOWN undi_shutdown; |
| struct s_PXENV_UNDI_CLEANUP undi_cleanup; |
| struct s_PXENV_STOP_UNDI stop_undi; |
| int rc; |
| |
| /* Allocate net device */ |
| netdev = alloc_etherdev ( sizeof ( *undinic ) ); |
| if ( ! netdev ) |
| return -ENOMEM; |
| netdev_init ( netdev, &undinet_operations ); |
| undinic = netdev->priv; |
| undi_set_drvdata ( undi, netdev ); |
| netdev->dev = &undi->dev; |
| memset ( undinic, 0, sizeof ( *undinic ) ); |
| undinet_entry = undi->entry; |
| DBGC ( undinic, "UNDINIC %p using UNDI %p\n", undinic, undi ); |
| |
| /* Hook in UNDI stack */ |
| if ( ! ( undi->flags & UNDI_FL_STARTED ) ) { |
| memset ( &start_undi, 0, sizeof ( start_undi ) ); |
| start_undi.AX = undi->pci_busdevfn; |
| start_undi.BX = undi->isapnp_csn; |
| start_undi.DX = undi->isapnp_read_port; |
| start_undi.ES = BIOS_SEG; |
| start_undi.DI = find_pnp_bios(); |
| if ( ( rc = pxeparent_call ( undinet_entry, PXENV_START_UNDI, |
| &start_undi, |
| sizeof ( start_undi ) ) ) != 0 ) |
| goto err_start_undi; |
| } |
| undi->flags |= UNDI_FL_STARTED; |
| |
| /* Bring up UNDI stack */ |
| if ( ! ( undi->flags & UNDI_FL_INITIALIZED ) ) { |
| memset ( &undi_startup, 0, sizeof ( undi_startup ) ); |
| if ( ( rc = pxeparent_call ( undinet_entry, PXENV_UNDI_STARTUP, |
| &undi_startup, |
| sizeof ( undi_startup ) ) ) != 0 ) |
| goto err_undi_startup; |
| memset ( &undi_initialize, 0, sizeof ( undi_initialize ) ); |
| if ( ( rc = pxeparent_call ( undinet_entry, |
| PXENV_UNDI_INITIALIZE, |
| &undi_initialize, |
| sizeof ( undi_initialize ))) != 0 ) |
| goto err_undi_initialize; |
| } |
| undi->flags |= UNDI_FL_INITIALIZED; |
| |
| /* Get device information */ |
| memset ( &undi_info, 0, sizeof ( undi_info ) ); |
| if ( ( rc = pxeparent_call ( undinet_entry, PXENV_UNDI_GET_INFORMATION, |
| &undi_info, sizeof ( undi_info ) ) ) != 0 ) |
| goto err_undi_get_information; |
| memcpy ( netdev->hw_addr, undi_info.PermNodeAddress, ETH_ALEN ); |
| undinic->irq = undi_info.IntNumber; |
| if ( undinic->irq > IRQ_MAX ) { |
| DBGC ( undinic, "UNDINIC %p invalid IRQ %d\n", |
| undinic, undinic->irq ); |
| goto err_bad_irq; |
| } |
| DBGC ( undinic, "UNDINIC %p is %s on IRQ %d\n", |
| undinic, eth_ntoa ( netdev->hw_addr ), undinic->irq ); |
| |
| /* Get interface information */ |
| memset ( &undi_iface, 0, sizeof ( undi_iface ) ); |
| if ( ( rc = pxeparent_call ( undinet_entry, PXENV_UNDI_GET_IFACE_INFO, |
| &undi_iface, |
| sizeof ( undi_iface ) ) ) != 0 ) |
| goto err_undi_get_iface_info; |
| DBGC ( undinic, "UNDINIC %p has type %s, speed %d, flags %08x\n", |
| undinic, undi_iface.IfaceType, undi_iface.LinkSpeed, |
| undi_iface.ServiceFlags ); |
| if ( strncmp ( ( ( char * ) undi_iface.IfaceType ), "Etherboot", |
| sizeof ( undi_iface.IfaceType ) ) == 0 ) { |
| DBGC ( undinic, "UNDINIC %p Etherboot 5.4 workaround enabled\n", |
| undinic ); |
| undinic->hacks |= UNDI_HACK_EB54; |
| } |
| |
| /* Mark as link up; we don't handle link state */ |
| netdev_link_up ( netdev ); |
| |
| /* Register network device */ |
| if ( ( rc = register_netdev ( netdev ) ) != 0 ) |
| goto err_register; |
| |
| DBGC ( undinic, "UNDINIC %p added\n", undinic ); |
| return 0; |
| |
| err_register: |
| err_undi_get_iface_info: |
| err_bad_irq: |
| err_undi_get_information: |
| err_undi_initialize: |
| /* Shut down UNDI stack */ |
| memset ( &undi_shutdown, 0, sizeof ( undi_shutdown ) ); |
| pxeparent_call ( undinet_entry, PXENV_UNDI_SHUTDOWN, &undi_shutdown, |
| sizeof ( undi_shutdown ) ); |
| memset ( &undi_cleanup, 0, sizeof ( undi_cleanup ) ); |
| pxeparent_call ( undinet_entry, PXENV_UNDI_CLEANUP, &undi_cleanup, |
| sizeof ( undi_cleanup ) ); |
| undi->flags &= ~UNDI_FL_INITIALIZED; |
| err_undi_startup: |
| /* Unhook UNDI stack */ |
| memset ( &stop_undi, 0, sizeof ( stop_undi ) ); |
| pxeparent_call ( undinet_entry, PXENV_STOP_UNDI, &stop_undi, |
| sizeof ( stop_undi ) ); |
| undi->flags &= ~UNDI_FL_STARTED; |
| err_start_undi: |
| netdev_nullify ( netdev ); |
| netdev_put ( netdev ); |
| undi_set_drvdata ( undi, NULL ); |
| return rc; |
| } |
| |
| /** |
| * Remove UNDI device |
| * |
| * @v undi UNDI device |
| */ |
| void undinet_remove ( struct undi_device *undi ) { |
| struct net_device *netdev = undi_get_drvdata ( undi ); |
| struct undi_nic *undinic = netdev->priv; |
| struct s_PXENV_UNDI_SHUTDOWN undi_shutdown; |
| struct s_PXENV_UNDI_CLEANUP undi_cleanup; |
| struct s_PXENV_STOP_UNDI stop_undi; |
| |
| /* Unregister net device */ |
| unregister_netdev ( netdev ); |
| |
| /* If we are preparing for an OS boot, or if we cannot exit |
| * via the PXE stack, then shut down the PXE stack. |
| */ |
| if ( ! ( undi->flags & UNDI_FL_KEEP_ALL ) ) { |
| |
| /* Shut down UNDI stack */ |
| memset ( &undi_shutdown, 0, sizeof ( undi_shutdown ) ); |
| pxeparent_call ( undinet_entry, PXENV_UNDI_SHUTDOWN, |
| &undi_shutdown, sizeof ( undi_shutdown ) ); |
| memset ( &undi_cleanup, 0, sizeof ( undi_cleanup ) ); |
| pxeparent_call ( undinet_entry, PXENV_UNDI_CLEANUP, |
| &undi_cleanup, sizeof ( undi_cleanup ) ); |
| undi->flags &= ~UNDI_FL_INITIALIZED; |
| |
| /* Unhook UNDI stack */ |
| memset ( &stop_undi, 0, sizeof ( stop_undi ) ); |
| pxeparent_call ( undinet_entry, PXENV_STOP_UNDI, &stop_undi, |
| sizeof ( stop_undi ) ); |
| undi->flags &= ~UNDI_FL_STARTED; |
| } |
| |
| /* Clear entry point */ |
| memset ( &undinet_entry, 0, sizeof ( undinet_entry ) ); |
| |
| /* Free network device */ |
| netdev_nullify ( netdev ); |
| netdev_put ( netdev ); |
| |
| DBGC ( undinic, "UNDINIC %p removed\n", undinic ); |
| } |