Report RX errors via netdev_rx_err()
[people/holger/gpxe.git] / src / arch / i386 / drivers / net / undinet.c
index d54429b..03515fd 100644 (file)
@@ -22,6 +22,7 @@
 #include <pic8259.h>
 #include <biosint.h>
 #include <pnpbios.h>
+#include <basemem_packet.h>
 #include <gpxe/iobuf.h>
 #include <gpxe/netdevice.h>
 #include <gpxe/if_ether.h>
 
 /** An UNDI NIC */
 struct undi_nic {
-       /** Entry point */
-       SEGOFF16_t entry;
        /** 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 );
 
 /*****************************************************************************
@@ -133,7 +144,7 @@ static union u_PXENV_ANY __data16 ( undinet_params );
  * Used as the indirection vector for all UNDI API calls.  Resides in
  * base memory.
  */
-static SEGOFF16_t __data16 ( undinet_entry_point );
+SEGOFF16_t __data16 ( undinet_entry_point );
 #define undinet_entry_point __use_data16 ( undinet_entry_point )
 
 /**
@@ -154,7 +165,6 @@ static int undinet_call ( struct undi_nic *undinic, unsigned int function,
        /* Copy parameter block and entry point */
        assert ( params_len <= sizeof ( undinet_params ) );
        memcpy ( &undinet_params, params, params_len );
-       undinet_entry_point = undinic->entry;
 
        /* Call real-mode entry point.  This calling convention will
         * work with both the !PXE and the PXENV+ entry points.
@@ -209,7 +219,8 @@ static int undinet_call ( struct undi_nic *undinic, unsigned int function,
                DBGC ( undinic, "UNDINIC %p parameters at %04x:%04x length "
                       "%#02x, entry point at %04x:%04x\n", undinic,
                       rm_params.segment, rm_params.offset, params_len,
-                      undinic->entry.segment, undinic->entry.offset );
+                      undinet_entry_point.segment,
+                      undinet_entry_point.offset );
                DBGC ( undinic, "UNDINIC %p parameters provided:\n", undinic );
                DBGC_HDA ( undinic, rm_params, params, params_len );
                DBGC ( undinic, "UNDINIC %p parameters returned:\n", undinic );
@@ -232,43 +243,39 @@ static int undinet_call ( struct undi_nic *undinic, unsigned int function,
 /**
  * UNDI interrupt service routine
  *
- * The UNDI ISR simply increments a counter (@c trigger_count) and
- * exits.
+ * The UNDI ISR increments a counter (@c trigger_count) and exits.
  */
-extern void undinet_isr ( void );
+extern void undiisr ( void );
 
-/** Dummy chain vector */
-static struct segoff prev_handler[ IRQ_MAX + 1 ];
+/** 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 */
-static volatile uint8_t __text16 ( trigger_count ) = 0;
-#define trigger_count __use_text16 ( 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
- *
- * The UNDI ISR specifically does @b not chain to the previous
- * interrupt handler.  BIOSes seem to install somewhat perverse
- * default interrupt handlers; some do nothing other than an iret (and
- * so will cause a screaming interrupt if there really is another
- * interrupting device) and some disable the interrupt at the PIC (and
- * so will bring our own interrupts to a shuddering halt).
  */
 static void undinet_hook_isr ( unsigned int irq ) {
 
        assert ( irq <= IRQ_MAX );
+       assert ( undiisr_irq == 0 );
 
-       __asm__ __volatile__ ( TEXT16_CODE ( "\nundinet_isr:\n\t"
-                                            "incb %%cs:%c0\n\t"
-                                            "iret\n\t" )
-                              : : "p" ( & __from_text16 ( trigger_count ) ) );
-
+       undiisr_irq = irq;
        hook_bios_interrupt ( IRQ_INT ( irq ),
-                             ( ( unsigned int ) undinet_isr ),
-                             &prev_handler[irq] );
-
+                             ( ( unsigned int ) undiisr ),
+                             &undiisr_next_handler );
 }
 
 /**
@@ -281,8 +288,9 @@ static void undinet_unhook_isr ( unsigned int irq ) {
        assert ( irq <= IRQ_MAX );
 
        unhook_bios_interrupt ( IRQ_INT ( irq ),
-                               ( ( unsigned int ) undinet_isr ),
-                               &prev_handler[irq] );
+                               ( ( unsigned int ) undiisr ),
+                               &undiisr_next_handler );
+       undiisr_irq = 0;
 }
 
 /**
@@ -291,11 +299,10 @@ static void undinet_unhook_isr ( unsigned int irq ) {
  * @ret triggered      ISR has been triggered since last check
  */
 static int undinet_isr_triggered ( void ) {
-       static unsigned int last_trigger_count = 0;
        unsigned int this_trigger_count;
 
        /* Read trigger_count.  Do this only once; it is volatile */
-       this_trigger_count = trigger_count;
+       this_trigger_count = undiisr_trigger_count;
 
        if ( this_trigger_count == last_trigger_count ) {
                /* Not triggered */
@@ -314,13 +321,6 @@ static int undinet_isr_triggered ( void ) {
  *****************************************************************************
  */
 
-/** Maximum length of a packet transmitted via the UNDI API */
-#define UNDI_IOB_LEN 1514
-
-/** UNDI I/O buffer */
-static char __data16_array ( undinet_iob, [UNDI_IOB_LEN] );
-#define undinet_iob __use_data16 ( undinet_iob )
-
 /** UNDI transmit buffer descriptor */
 static struct s_PXENV_UNDI_TBD __data16 ( undinet_tbd );
 #define undinet_tbd __use_data16 ( undinet_tbd )
@@ -339,10 +339,20 @@ static int undinet_transmit ( struct net_device *netdev,
        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 ( undinet_iob ) )
-               len = sizeof ( undinet_iob );
-       memcpy ( &undinet_iob, iobuf->data, len );
+       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 ) );
@@ -357,7 +367,7 @@ static int undinet_transmit ( struct net_device *netdev,
        undinet_tbd.ImmedLength = len;
        undinet_tbd.Xmit.segment = rm_ds;
        undinet_tbd.Xmit.offset 
-               = ( ( unsigned ) & __from_data16 ( undinet_iob ) );
+               = ( ( unsigned ) & __from_data16 ( basemem_packet ) );
 
        /* Issue PXE API call */
        if ( ( rc = undinet_call ( undinic, PXENV_UNDI_TRANSMIT,
@@ -376,7 +386,6 @@ static int undinet_transmit ( struct net_device *netdev,
  * Poll for received packets
  *
  * @v netdev           Network device
- * @v rx_quota         Maximum number of packets to receive
  *
  * Fun, fun, fun.  UNDI drivers don't use polling; they use
  * interrupts.  We therefore cheat and pretend that an interrupt has
@@ -393,8 +402,17 @@ static int undinet_transmit ( struct net_device *netdev,
  * 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, unsigned int rx_quota ) {
+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;
@@ -407,26 +425,6 @@ static void undinet_poll ( struct net_device *netdev, unsigned int rx_quota ) {
                if ( ! undinet_isr_triggered() )
                        return;
 
-               /* See if this was our interrupt */
-               memset ( &undi_isr, 0, sizeof ( undi_isr ) );
-               undi_isr.FuncFlag = PXENV_UNDI_ISR_IN_START;
-               if ( ( rc = undinet_call ( undinic, PXENV_UNDI_ISR, &undi_isr,
-                                          sizeof ( undi_isr ) ) ) != 0 )
-                       return;
-
-               /* Send EOI to the PIC.  In an ideal world, we'd do
-                * this only for interrupts which the UNDI stack
-                * reports as "ours".  However, since we don't (can't)
-                * chain to the previous interrupt handler, we have to
-                * acknowledge all interrupts.  See undinet_hook_isr()
-                * for more background.
-                */
-               send_eoi ( undinic->irq );
-
-               /* If this wasn't our interrupt, exit now */
-               if ( undi_isr.FuncFlag != PXENV_UNDI_ISR_OUT_OURS )
-                       return;
-               
                /* Start ISR processing */
                undinic->isr_processing = 1;
                undi_isr.FuncFlag = PXENV_UNDI_ISR_IN_PROCESS;
@@ -436,7 +434,7 @@ static void undinet_poll ( struct net_device *netdev, unsigned int rx_quota ) {
        }
 
        /* Run through the ISR loop */
-       while ( rx_quota ) {
+       while ( 1 ) {
                if ( ( rc = undinet_call ( undinic, PXENV_UNDI_ISR, &undi_isr,
                                           sizeof ( undi_isr ) ) ) != 0 )
                        break;
@@ -455,6 +453,7 @@ static void undinet_poll ( struct net_device *netdev, unsigned int rx_quota ) {
                                       "allocate %zd bytes for RX buffer\n",
                                       undinic, len );
                                /* Fragment will be dropped */
+                               netdev_rx_err ( netdev, NULL, -ENOMEM );
                                goto done;
                        }
                        if ( frag_len > iob_tailroom ( iobuf ) ) {
@@ -466,9 +465,14 @@ static void undinet_poll ( struct net_device *netdev, unsigned int rx_quota ) {
                                         undi_isr.Frame.segment,
                                         undi_isr.Frame.offset, frag_len );
                        if ( iob_len ( iobuf ) == len ) {
+                               /* Whole packet received; deliver it */
                                netdev_rx ( netdev, iobuf );
                                iobuf = NULL;
-                               --rx_quota;
+                               /* 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:
@@ -489,7 +493,7 @@ static void undinet_poll ( struct net_device *netdev, unsigned int rx_quota ) {
        if ( iobuf ) {
                DBGC ( undinic, "UNDINIC %p returned incomplete packet\n",
                       undinic );
-               netdev_rx ( netdev, iobuf );
+               netdev_rx_err ( netdev, iobuf, -EINVAL );
        }
 }
 
@@ -575,6 +579,29 @@ static void undinet_close ( struct net_device *netdev ) {
        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
  *
@@ -588,6 +615,7 @@ int undinet_probe ( struct undi_device *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;
@@ -597,11 +625,12 @@ int undinet_probe ( struct undi_device *undi ) {
        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 ) );
-       undinic->entry = undi->entry;
+       undinet_entry_point = undi->entry;
        DBGC ( undinic, "UNDINIC %p using UNDI %p\n", undinic, undi );
 
        /* Hook in UNDI stack */
@@ -645,11 +674,20 @@ int undinet_probe ( struct undi_device *undi ) {
        DBGC ( undinic, "UNDINIC %p is %s on IRQ %d\n",
               undinic, eth_ntoa ( netdev->ll_addr ), undinic->irq );
 
-       /* Point to NIC specific routines */
-       netdev->open     = undinet_open;
-       netdev->close    = undinet_close;
-       netdev->transmit = undinet_transmit;
-       netdev->poll     = undinet_poll;
+       /* Get interface information */
+       memset ( &undi_iface, 0, sizeof ( undi_iface ) );
+       if ( ( rc = undinet_call ( undinic, PXENV_UNDI_GET_IFACE_INFO,
+                                  &undi_iface,
+                                  sizeof ( undi_iface ) ) ) != 0 )
+               goto err_undi_get_iface_info;
+       DBGC ( undinic, "UNDINIC %p has type %s and link speed %ld\n",
+              undinic, undi_iface.IfaceType, undi_iface.LinkSpeed );
+       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;
+       }
 
        /* Register network device */
        if ( ( rc = register_netdev ( netdev ) ) != 0 )
@@ -659,6 +697,7 @@ int undinet_probe ( struct undi_device *undi ) {
        return 0;
 
  err_register:
+ err_undi_get_iface_info:
  err_bad_irq:
  err_undi_get_information:
  err_undi_initialize:
@@ -675,7 +714,8 @@ int undinet_probe ( struct undi_device *undi ) {
        undinet_call ( undinic, PXENV_STOP_UNDI, &stop_undi,
                       sizeof ( stop_undi ) );
  err_start_undi:
-       free_netdev ( netdev );
+       netdev_nullify ( netdev );
+       netdev_put ( netdev );
        undi_set_drvdata ( undi, NULL );
        return rc;
 }
@@ -709,8 +749,12 @@ void undinet_remove ( struct undi_device *undi ) {
                       sizeof ( stop_undi ) );
        undi->flags &= ~UNDI_FL_STARTED;
 
+       /* Clear entry point */
+       memset ( &undinet_entry_point, 0, sizeof ( undinet_entry_point ) );
+
        /* Free network device */
-       free_netdev ( netdev );
+       netdev_nullify ( netdev );
+       netdev_put ( netdev );
 
        DBGC ( undinic, "UNDINIC %p removed\n", undinic );
 }