summaryrefslogblamecommitdiffstats
path: root/src/drivers/net/efi/snpnet.c
blob: 79b4946c4297723cc41f7df4da1ef4bc3ff190e7 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  
                                                              












                                                                      

                                                                



                               

                   
                   
                  

                           
                          
                          

                                            

                                



                   
                 


   




                                      
                                         








                                                               
 



                                      

  


                                                  
   


























































                                                                             



















                                                                             








                                                        
                                                     

                         
 



                                                                             
         
 







                                                                           
         

                           
                 


   





































                                                                                



                                      

                                                          
                  
                           

                         
 









                                                            

                 




                                                                          
 




                                                                      
 
                                                       
                                             


                                                                     


                              



                                                 



         
                             
  

















                                                       


                                                      


                                                              

                         
 





                                                                                

         

                                                                         
                                     
                                            


                                                                

         





                                                                                
         






                                                                       
                                                                             
                                                                      
                                     


                                                                         

         


                                                   



                 
                       
  
                                      

                                                        
                                           
                         
               
 





                                                                 
         
 




                                                                          
 




                                                   



                                                         



                                    


   
                                        
  

                                                 

                                          

                                                                              
                                           




                                               
                              
                              
                         

               












                                                                                

                                                                        
                                                                                
                                     

                                                                 
                                       


                                        


                                                                               
                                     





                                                                             





                                                                   









                                                                              
                                           





                                                            

                                                                     












                                                         
                                  


                            

               







                                                                                
                                                                             

                                                                       

                                       
 





                                                    
                                                   



















                                                                         
                                                                     





                                                                             
                                                                     
                                  

         


                                                                        
                                                                         







                                                                          
                                                                         


                                             
         



                                                        
 

                                                       
                                         

                                                                  






                                                      
 

                 







                                        

                                  



                                                                      



                  
                            
  





                                                                  
                                           

                         
 

                                       
 


                                                             

                                                                        
                                                  

         
                                 
                                        


                                  
                                

                                                                      
 
/*
 * Copyright (C) 2014 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., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

FILE_LICENCE ( GPL2_OR_LATER );

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <ipxe/iobuf.h>
#include <ipxe/netdevice.h>
#include <ipxe/ethernet.h>
#include <ipxe/vsprintf.h>
#include <ipxe/efi/efi.h>
#include <ipxe/efi/Protocol/SimpleNetwork.h>
#include <ipxe/efi/efi_driver.h>
#include <ipxe/efi/efi_pci.h>
#include "snpnet.h"

/** @file
 *
 * SNP NIC driver
 *
 */

/** An SNP NIC */
struct snp_nic {
	/** EFI device */
	struct efi_device *efidev;
	/** Simple network protocol */
	EFI_SIMPLE_NETWORK_PROTOCOL *snp;
	/** Generic device */
	struct device dev;

	/** Maximum packet size
	 *
	 * This is calculated as the sum of MediaHeaderSize and
	 * MaxPacketSize, and may therefore be an overestimate.
	 */
	size_t mtu;

	/** Current transmit buffer */
	struct io_buffer *txbuf;
	/** Current receive buffer */
	struct io_buffer *rxbuf;
};

/** Maximum number of received packets per poll */
#define SNP_RX_QUOTA 4

/**
 * Format SNP MAC address (for debugging)
 *
 * @v mac		MAC address
 * @v len		Length of MAC address
 * @ret text		MAC address as text
 */
static const char * snpnet_mac_text ( EFI_MAC_ADDRESS *mac, size_t len ) {
	static char buf[ sizeof ( *mac ) * 3 /* "xx:" or "xx\0" */ ];
	size_t used = 0;
	unsigned int i;

	for ( i = 0 ; i < len ; i++ ) {
		used += ssnprintf ( &buf[used], ( sizeof ( buf ) - used ),
				    "%s%02x", ( used ? ":" : "" ),
				    mac->Addr[i] );
	}
	return buf;
}

/**
 * Dump SNP mode information (for debugging)
 *
 * @v netdev		Network device
 */
static void snpnet_dump_mode ( struct net_device *netdev ) {
	struct snp_nic *snp = netdev_priv ( netdev );
	EFI_SIMPLE_NETWORK_MODE *mode = snp->snp->Mode;
	size_t mac_len = mode->HwAddressSize;
	unsigned int i;

	/* Do nothing unless debugging is enabled */
	if ( ! DBG_EXTRA )
		return;

	DBGC2 ( snp, "SNP %s st %d type %d hdr %d pkt %d rxflt %#x/%#x%s "
		"nvram %d acc %d mcast %d/%d\n", netdev->name, mode->State,
		mode->IfType, mode->MediaHeaderSize, mode->MaxPacketSize,
		mode->ReceiveFilterSetting, mode->ReceiveFilterMask,
		( mode->MultipleTxSupported ? " multitx" : "" ),
		mode->NvRamSize, mode->NvRamAccessSize,
		mode->MCastFilterCount, mode->MaxMCastFilterCount );
	DBGC2 ( snp, "SNP %s hw %s", netdev->name,
		snpnet_mac_text ( &mode->PermanentAddress, mac_len ) );
	DBGC2 ( snp, " addr %s%s",
		snpnet_mac_text ( &mode->CurrentAddress, mac_len ),
		( mode->MacAddressChangeable ? "" : "(f)" ) );
	DBGC2 ( snp, " bcast %s\n",
		snpnet_mac_text ( &mode->BroadcastAddress, mac_len ) );
	for ( i = 0 ; i < mode->MCastFilterCount ; i++ ) {
		DBGC2 ( snp, "SNP %s mcast %s\n", netdev->name,
			snpnet_mac_text ( &mode->MCastFilter[i], mac_len ) );
	}
	DBGC2 ( snp, "SNP %s media %s\n", netdev->name,
		( mode->MediaPresentSupported ?
		  ( mode->MediaPresent ? "present" : "not present" ) :
		  "presence not supported" ) );
}

/**
 * Check link state
 *
 * @v netdev		Network device
 */
static void snpnet_check_link ( struct net_device *netdev ) {
	struct snp_nic *snp = netdev_priv ( netdev );
	EFI_SIMPLE_NETWORK_MODE *mode = snp->snp->Mode;

	/* Do nothing unless media presence detection is supported */
	if ( ! mode->MediaPresentSupported )
		return;

	/* Report any link status change */
	if ( mode->MediaPresent && ( ! netdev_link_ok ( netdev ) ) ) {
		netdev_link_up ( netdev );
	} else if ( ( ! mode->MediaPresent ) && netdev_link_ok ( netdev ) ) {
		netdev_link_down ( netdev );
	}
}

/**
 * Transmit packet
 *
 * @v netdev		Network device
 * @v iobuf		I/O buffer
 * @ret rc		Return status code
 */
static int snpnet_transmit ( struct net_device *netdev,
			     struct io_buffer *iobuf ) {
	struct snp_nic *snp = netdev_priv ( netdev );
	EFI_STATUS efirc;
	int rc;

	/* Defer the packet if there is already a transmission in progress */
	if ( snp->txbuf ) {
		netdev_tx_defer ( netdev, iobuf );
		return 0;
	}

	/* Transmit packet */
	if ( ( efirc = snp->snp->Transmit ( snp->snp, 0, iob_len ( iobuf ),
					    iobuf->data, NULL, NULL,
					    NULL ) ) != 0 ) {
		rc = -EEFI ( efirc );
		DBGC ( snp, "SNP %s could not transmit: %s\n",
		       netdev->name, strerror ( rc ) );
		return rc;
	}
	snp->txbuf = iobuf;

	return 0;
}

/**
 * Poll for completed packets
 *
 * @v netdev		Network device
 */
static void snpnet_poll_tx ( struct net_device *netdev ) {
	struct snp_nic *snp = netdev->priv;
	UINT32 irq;
	VOID *txbuf;
	EFI_STATUS efirc;
	int rc;

	/* Get status */
	if ( ( efirc = snp->snp->GetStatus ( snp->snp, &irq, &txbuf ) ) != 0 ) {
		rc = -EEFI ( efirc );
		DBGC ( snp, "SNP %s could not get status: %s\n",
		       netdev->name, strerror ( rc ) );
		netdev_rx_err ( netdev, NULL, rc );
		return;
	}

	/* Do nothing unless we have a completion */
	if ( ! txbuf )
		return;

	/* Sanity check */
	if ( ! snp->txbuf ) {
		DBGC ( snp, "SNP %s reported spurious TX completion\n",
		       netdev->name );
		netdev_tx_err ( netdev, NULL, -EPIPE );
		return;
	}

	/* Complete transmission */
	netdev_tx_complete ( netdev, snp->txbuf );
	snp->txbuf = NULL;
}

/**
 * Poll for received packets
 *
 * @v netdev		Network device
 */
static void snpnet_poll_rx ( struct net_device *netdev ) {
	struct snp_nic *snp = netdev->priv;
	UINTN len;
	unsigned int quota;
	EFI_STATUS efirc;
	int rc;

	/* Retrieve up to SNP_RX_QUOTA packets */
	for ( quota = SNP_RX_QUOTA ; quota ; quota-- ) {

		/* Allocate buffer, if required */
		if ( ! snp->rxbuf ) {
			snp->rxbuf = alloc_iob ( snp->mtu );
			if ( ! snp->rxbuf ) {
				/* Leave for next poll */
				break;
			}
		}

		/* Receive packet */
		len = iob_tailroom ( snp->rxbuf );
		if ( ( efirc = snp->snp->Receive ( snp->snp, NULL, &len,
						   snp->rxbuf->data, NULL,
						   NULL, NULL ) ) != 0 ) {

			/* EFI_NOT_READY is just the usual "no packet"
			 * status indication; ignore it.
			 */
			if ( efirc == EFI_NOT_READY )
				break;

			/* Anything else is an error */
			rc = -EEFI ( efirc );
			DBGC ( snp, "SNP %s could not receive: %s\n",
			       netdev->name, strerror ( rc ) );
			netdev_rx_err ( netdev, NULL, rc );
			break;
		}

		/* Hand off to network stack */
		iob_put ( snp->rxbuf, len );
		netdev_rx ( netdev, snp->rxbuf );
		snp->rxbuf = NULL;
	}
}

/**
 * Poll for completed packets
 *
 * @v netdev		Network device
 */
static void snpnet_poll ( struct net_device *netdev ) {

	/* Process any TX completions */
	snpnet_poll_tx ( netdev );

	/* Process any RX completions */
	snpnet_poll_rx ( netdev );

	/* Check for link state changes */
	snpnet_check_link ( netdev );
}

/**
 * Open network device
 *
 * @v netdev		Network device
 * @ret rc		Return status code
 */
static int snpnet_open ( struct net_device *netdev ) {
	struct snp_nic *snp = netdev->priv;
	EFI_MAC_ADDRESS *mac = ( ( void * ) netdev->ll_addr );
	UINT32 filters;
	EFI_STATUS efirc;
	int rc;

	/* Try setting MAC address (before initialising) */
	if ( ( efirc = snp->snp->StationAddress ( snp->snp, FALSE, mac ) ) !=0){
		rc = -EEFI ( efirc );
		DBGC ( snp, "SNP %s could not set station address before "
		       "initialising: %s\n", netdev->name, strerror ( rc ) );
		/* Ignore error */
	}

	/* Initialise NIC */
	if ( ( efirc = snp->snp->Initialize ( snp->snp, 0, 0 ) ) != 0 ) {
		rc = -EEFI ( efirc );
		snpnet_dump_mode ( netdev );
		DBGC ( snp, "SNP %s could not initialise: %s\n",
		       netdev->name, strerror ( rc ) );
		return rc;
	}

	/* Try setting MAC address (after initialising) */
	if ( ( efirc = snp->snp->StationAddress ( snp->snp, FALSE, mac ) ) !=0){
		rc = -EEFI ( efirc );
		DBGC ( snp, "SNP %s could not set station address after "
		       "initialising: %s\n", netdev->name, strerror ( rc ) );
		/* Ignore error */
	}

	/* Set receive filters */
	filters = ( EFI_SIMPLE_NETWORK_RECEIVE_UNICAST |
		    EFI_SIMPLE_NETWORK_RECEIVE_MULTICAST |
		    EFI_SIMPLE_NETWORK_RECEIVE_BROADCAST |
		    EFI_SIMPLE_NETWORK_RECEIVE_PROMISCUOUS |
		    EFI_SIMPLE_NETWORK_RECEIVE_PROMISCUOUS_MULTICAST );
	if ( ( efirc = snp->snp->ReceiveFilters ( snp->snp, filters, 0, TRUE,
						  0, NULL ) ) != 0 ) {
		rc = -EEFI ( efirc );
		DBGC ( snp, "SNP %s could not set receive filters: %s\n",
		       netdev->name, strerror ( rc ) );
		/* Ignore error */
	}

	/* Dump mode information (for debugging) */
	snpnet_dump_mode ( netdev );

	return 0;
}

/**
 * Close network device
 *
 * @v netdev		Network device
 */
static void snpnet_close ( struct net_device *netdev ) {
	struct snp_nic *snp = netdev->priv;
	EFI_STATUS efirc;
	int rc;

	/* Shut down NIC */
	if ( ( efirc = snp->snp->Shutdown ( snp->snp ) ) != 0 ) {
		rc = -EEFI ( efirc );
		DBGC ( snp, "SNP %s could not shut down: %s\n",
		       netdev->name, strerror ( rc ) );
		/* Nothing we can do about this */
	}

	/* Discard transmit buffer, if applicable */
	if ( snp->txbuf ) {
		netdev_tx_complete_err ( netdev, snp->txbuf, -ECANCELED );
		snp->txbuf = NULL;
	}

	/* Discard receive buffer, if applicable */
	if ( snp->rxbuf ) {
		free_iob ( snp->rxbuf );
		snp->rxbuf = NULL;
	}
}

/** SNP network device operations */
static struct net_device_operations snpnet_operations = {
	.open = snpnet_open,
	.close = snpnet_close,
	.transmit = snpnet_transmit,
	.poll = snpnet_poll,
};

/**
 * Get underlying PCI device information
 *
 * @v efidev		EFI device
 * @v dev		Generic device to fill in
 * @ret rc		Return status code
 */
static int snpnet_pci_info ( struct efi_device *efidev, struct device *dev ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	EFI_HANDLE device = efidev->device;
	union {
		EFI_DEVICE_PATH_PROTOCOL *path;
		void *interface;
	} path;
	EFI_DEVICE_PATH_PROTOCOL *devpath;
	struct pci_device pci;
	EFI_HANDLE pci_device;
	EFI_STATUS efirc;
	int rc;

	/* Get device path */
	if ( ( efirc = bs->OpenProtocol ( device,
					  &efi_device_path_protocol_guid,
					  &path.interface,
					  efi_image_handle, device,
					  EFI_OPEN_PROTOCOL_GET_PROTOCOL ))!=0){
		rc = -EEFI ( efirc );
		DBGC ( device, "SNP %p %s cannot open device path: %s\n",
		       device, efi_handle_name ( device ), strerror ( rc ) );
		goto err_open_device_path;
	}
	devpath = path.path;

	/* Check for presence of PCI I/O protocol */
	if ( ( efirc = bs->LocateDevicePath ( &efi_pci_io_protocol_guid,
					      &devpath, &pci_device ) ) != 0 ) {
		rc = -EEFI ( efirc );
		DBGC ( device, "SNP %p %s is not a PCI device\n",
		       device, efi_handle_name ( device ) );
		goto err_locate_pci_io;
	}

	/* Get PCI device information */
	if ( ( rc = efipci_info ( pci_device, &pci ) ) != 0 ) {
		DBGC ( device, "SNP %p %s could not get PCI information: %s\n",
		       device, efi_handle_name ( device ), strerror ( rc ) );
		goto err_efipci_info;
	}

	/* Populate SNP device information */
	memcpy ( &dev->desc, &pci.dev.desc, sizeof ( dev->desc ) );
	snprintf ( dev->name, sizeof ( dev->name ), "SNP-%s", pci.dev.name );

 err_efipci_info:
 err_locate_pci_io:
	bs->CloseProtocol ( device, &efi_device_path_protocol_guid,
			    efi_image_handle, device );
 err_open_device_path:
	return rc;
}

/**
 * Get underlying device information
 *
 * @v efidev		EFI device
 * @v dev		Generic device to fill in
 * @ret rc		Return status code
 */
static int snpnet_dev_info ( struct efi_device *efidev, struct device *dev ) {
	EFI_HANDLE device = efidev->device;
	int rc;

	/* Try getting underlying PCI device information */
	if ( ( rc = snpnet_pci_info ( efidev, dev ) ) == 0 )
		return 0;

	DBGC ( device, "SNP %p %s could not get underlying device "
	       "information\n", device, efi_handle_name ( device ) );
	return -ENOTTY;
}

/**
 * Attach driver to device
 *
 * @v efidev		EFI device
 * @ret rc		Return status code
 */
int snpnet_start ( struct efi_device *efidev ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	EFI_HANDLE device = efidev->device;
	EFI_SIMPLE_NETWORK_MODE *mode;
	struct net_device *netdev;
	struct snp_nic *snp;
	void *interface;
	EFI_STATUS efirc;
	int rc;

	/* Open SNP protocol */
	if ( ( efirc = bs->OpenProtocol ( device,
					  &efi_simple_network_protocol_guid,
					  &interface, efi_image_handle, device,
					  ( EFI_OPEN_PROTOCOL_BY_DRIVER |
					    EFI_OPEN_PROTOCOL_EXCLUSIVE )))!=0){
		rc = -EEFI ( efirc );
		DBGC ( device, "SNP %p %s cannot open SNP protocol: %s\n",
		       device, efi_handle_name ( device ), strerror ( rc ) );
		DBGC_EFI_OPENERS ( device, device,
				   &efi_simple_network_protocol_guid );
		goto err_open_protocol;
	}

	/* Allocate and initialise structure */
	netdev = alloc_etherdev ( sizeof ( *snp ) );
	if ( ! netdev ) {
		rc = -ENOMEM;
		goto err_alloc;
	}
	netdev_init ( netdev, &snpnet_operations );
	snp = netdev->priv;
	snp->efidev = efidev;
	snp->snp = interface;
	mode = snp->snp->Mode;
	efidev_set_drvdata ( efidev, netdev );

	/* Populate underlying device information */
	if ( ( rc = snpnet_dev_info ( efidev, &snp->dev ) ) != 0 )
		goto err_info;
	snp->dev.driver_name = "SNP";
	snp->dev.parent = &efidev->dev;
	list_add ( &snp->dev.siblings, &efidev->dev.children );
	INIT_LIST_HEAD ( &snp->dev.children );
	netdev->dev = &snp->dev;

	/* Bring to the Started state */
	if ( ( mode->State == EfiSimpleNetworkStopped ) &&
	     ( ( efirc = snp->snp->Start ( snp->snp ) ) != 0 ) ) {
		rc = -EEFI ( efirc );
		DBGC ( device, "SNP %p %s could not start: %s\n", device,
		       efi_handle_name ( device ), strerror ( rc ) );
		goto err_start;
	}
	if ( ( mode->State == EfiSimpleNetworkInitialized ) &&
	     ( ( efirc = snp->snp->Shutdown ( snp->snp ) ) != 0 ) ) {
		rc = -EEFI ( efirc );
		DBGC ( device, "SNP %p %s could not shut down: %s\n", device,
		       efi_handle_name ( device ), strerror ( rc ) );
		goto err_shutdown;
	}

	/* Populate network device parameters */
	if ( mode->HwAddressSize != netdev->ll_protocol->hw_addr_len ) {
		DBGC ( device, "SNP %p %s has invalid hardware address "
		       "length %d\n", device, efi_handle_name ( device ),
		       mode->HwAddressSize );
		rc = -ENOTSUP;
		goto err_hw_addr_len;
	}
	memcpy ( netdev->hw_addr, &mode->PermanentAddress,
		 netdev->ll_protocol->hw_addr_len );
	if ( mode->HwAddressSize != netdev->ll_protocol->ll_addr_len ) {
		DBGC ( device, "SNP %p %s has invalid link-layer address "
		       "length %d\n", device, efi_handle_name ( device ),
		       mode->HwAddressSize );
		rc = -ENOTSUP;
		goto err_ll_addr_len;
	}
	memcpy ( netdev->ll_addr, &mode->CurrentAddress,
		 netdev->ll_protocol->ll_addr_len );
	snp->mtu = ( snp->snp->Mode->MaxPacketSize +
		     snp->snp->Mode->MediaHeaderSize );

	/* Register network device */
	if ( ( rc = register_netdev ( netdev ) ) != 0 )
		goto err_register_netdev;
	DBGC ( device, "SNP %p %s registered as %s\n",
	       device, efi_handle_name ( device ), netdev->name );

	/* Set initial link state */
	if ( snp->snp->Mode->MediaPresentSupported ) {
		snpnet_check_link ( netdev );
	} else {
		netdev_link_up ( netdev );
	}

	return 0;

	unregister_netdev ( netdev );
 err_register_netdev:
 err_ll_addr_len:
 err_hw_addr_len:
 err_shutdown:
 err_start:
	list_del ( &snp->dev.siblings );
 err_info:
	netdev_nullify ( netdev );
	netdev_put ( netdev );
 err_alloc:
	bs->CloseProtocol ( device, &efi_simple_network_protocol_guid,
			    efi_image_handle, device );
 err_open_protocol:
	return rc;
}

/**
 * Detach driver from device
 *
 * @v efidev		EFI device
  */
void snpnet_stop ( struct efi_device *efidev ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	struct net_device *netdev = efidev_get_drvdata ( efidev );
	struct snp_nic *snp = netdev->priv;
	EFI_HANDLE device = efidev->device;
	EFI_STATUS efirc;
	int rc;

	/* Unregister network device */
	unregister_netdev ( netdev );

	/* Stop SNP protocol */
	if ( ( efirc = snp->snp->Stop ( snp->snp ) ) != 0 ) {
		rc = -EEFI ( efirc );
		DBGC ( device, "SNP %p %s could not stop: %s\n", device,
		       efi_handle_name ( device ), strerror ( rc ) );
		/* Nothing we can do about this */
	}

	/* Free network device */
	list_del ( &snp->dev.siblings );
	netdev_nullify ( netdev );
	netdev_put ( netdev );

	/* Close SNP protocol */
	bs->CloseProtocol ( device, &efi_simple_network_protocol_guid,
			    efi_image_handle, device );
}