Skip to content

Device Drivers

Daniele Lacamera edited this page Jun 10, 2018 · 7 revisions

Example device driver

After reading this sections, find a description of an example device driver here: [Example device driver] (Example-device-driver)

Device structure

Every device driver must define its own interface to communicate with the stack. This interface is accessed via the pico_device structure. Similarly to what it happens for protocols, the stack accepts the registration of this other type of module, describing a device driver, that can be attached to the stack. The pico_device structure is described in pico_device.h as follows:

    struct pico_device {
        char name[MAX_DEVICE_NAME];
        uint32_t hash;
        uint32_t overhead;
        uint32_t mtu;
        struct pico_ethdev *eth; /* Null if non-ethernet */
        struct pico_queue *q_in;
        struct pico_queue *q_out;
        int (*link_state)(struct pico_device *self);
        int (*send)(struct pico_device *self, void *buf, int len); /* Send function. Return 0 if busy */
        int (*poll)(struct pico_device *self, int loop_score);
        void (*destroy)(struct pico_device *self);
        int (*dsr)(struct pico_device *self, int loop_score);
        int __serving_interrupt;
        /* used to signal the upper layer the number of events arrived since the last processing */
        volatile int eventCnt;
      #ifdef PICO_SUPPORT_IPV6
        struct pico_nd_hostvars hostvars;
      #endif
    };

Some of the fields here are in common with the protocol structure, the name, hash, and the two queues have exactly the same meaning as for the pico_protocol descriptor. The relationship with the internal scheduler to this type of module though, is a bit different. There are two different possible strategies to implement a device driver: a poll-based implementation and an asynchronous, IRQ-based mechanism, slightly more efficient but also a bit more more difficult to implement.

Send function

In both cases (Polling and Async driver), the stack uses the send function pointer of the driver whenever a frame has to leave the stack onto the physical device, using a link associated to this very device. The send function associated by the driver during the initialization phase must implement all the mechanisms to actually put the frame out on the desired physical medium. The module can implement a generic send function for all the registered devices, as the device field will be passed as the first argument. The callback prototype is the following:

int (*send)(struct pico device *self, void *buf, int len);

If the device can immediately inject the frame at address buf of length len, it returns back to the caller the length of the frame injected. If the device is currently busy, this function can safely return 0, and the stack will retry the same operation again later.

Receive function: Polling driver

The polling strategy of the device driver requires the driver developer to implement the poll function. The poll function will be called by the stack at regular interval (normally every stack tick), to give the driver the possibility to inject newly received packets into the stack. The prototype is the following:

int (*poll)(struct pico device *self, int loop score);

The poll function must check if the device is ready to receive frames or has received new frames already. For each frame that is directed to the stack, it will call the library function pico_stack_recv(). This function will deliver the received frame to the stack. The loop_score variable represents the maximum amount of frames that the stack can process during this call, i.e. the maximum amount of calls to pico_stack_recv() that can be performed during this iteration. The device driver should loop around the packet delivery operation and decrease the loop score by one every time a frame is delivered to the stack. If during the iteration all the score was used, poll should return 0. Else, it should return the remaining loop_score.

NOTE: The poll function must return immediately and must never block on hardware-specific operations. If the device is interrupt-driven, the integration will have to provide a mechanism to defer the reception or enqueuing into the stack until the next call to the device poll function. Calling pico_stack_recv() is only allowed from inside the poll() callback, thus a two-halves interface interrupt management design is required, and any memory structure shared between the two halves must be protected against concurrent access accordingly.

This means that the smallest set of operations required to implement a device driver consists of the send and poll operations.

Receive function: Asynchronous driver

An asynchronous driver, on the other hand, might not implement the poll function (set to NULL), and instead set the __serving_interrupt flag from the interrupt handler every time a new frame is available for possible reception. This will ensure that the dsr function is called at least once to process the incoming frame when the interrupt returns. The purpose of the dsr is then the same as the poll, except that the former will only be invoked when an external, asynchronous event such as an interrupt handler has set the __serving_interrupt flag before. Note that setting and clearing this flag may cause concurrent access issues between task context and interrupt context, so be careful to implement this properly. (Use locking, or producer/consumer)

Note on memory allocation

The image above tries to illustrate how frames should be copied from and to the stack from the device drivers.

When receiving a packet from the hardware, and passing it to the stack using pico_stack_recv(), the frame will be copied into the stack. After calling pico_stack_recv(), the driver can free the buffer it used to receive the packet, if it was dynamically allocated of course.

To pass a reference to the stack, instead of copying the packet, you should use pico_stack_recv_zerocopy() instead. picoTCP will free the packet when it does not need it anymore, using PICO_FREE(). This means your driver must allocate this packet using PICO_ZALLOC()!

If you want to use zero-copy, but your buffer is not allocated using PICO_ZALLOC (e.g. because of special alignment needs for DMA buffers), you can use pico_stack_recv_zerocopy_ext_buffer_notify(), which will notify you using a callback, when picoTCP does not need the buffer any longer, so that you can free it accordingly.

For the transmitting part, picoTCP will always free the frame immediately after calling the ``device->send()``` function. This means your driver needs to take a copy of the frame for as long as the hardware needs the data to be transmitted correctly. For now, no zerocopy strategy is available for transmission.

Data Link or Network Layer

The eth field is very important for two reasons: in the first place, it provides a datalink address associated to the device. But it can also assume a NULL value to indicate that the specific device associated to the driver is exchanging pure IP packets (Network layer) instead of full frames, and if so the stack will react accordingly. This is useful in point-to-point datalink drivers, where the two endpoints don’t need any layer 2 addressing. Two examples of this kind of devices can be found in the tun driver and the PPPoS (ppp over serial line) implementation.

Adding new datalink protocols

In the future, it will be possible to add new datalink protocols (such as IEEE 802.11 or IEEE 802.15.4) as different kind of addressing strategies. In order to maintain compatibility with the existing interface, a new field will be added to struct pico_device:

enum pico_ll_mode

The value 0 will always be used for 802.3 (Ethernet) so that the compatibility with old drivers is guaranteed (i.e., if a driver does not initialize the enum field, it's assumed to be using Ethernet addressing when the .eth field is not NULL). In all other cases, the value of the .eth field assume a domain-specific meaning (e.g. a struct containing short and extended 6LoWPAN addresses in a 802.15.4 driver). For more details about this implementation, refer to the 6LoWPAN branch.

Link state

By implementing the link_state function, the driver can provide information regarding the state of the physical channel before any use of the device, or during the device activity. This function is not mandatory to implement in a driver (the stack will assume that the device is permanently connected if the function is not defined), but it might help if the physical device can sense the activity of the link and react accordingly. Furthermore, a global function pico_dev_link_state is exported by the stack to the public API, making it possible for the application developer to know about the physical state of the device.

Custom destroy function

The destroy function is not mandatory either: it is in fact used to free possible additional entries related to the device when the device is destroyed.

Overhead field

A positive integer indicating the amount of bytes required by the device driver to implement its header. This is used whenever a network layer allocates a new packet to be sent through this device. If a value is specifed here, it will be possible for the device to seek back in the frame scheduled for sending, and subsequently copy any header information in front of it. Devices dealing with pure stack frames or subparts of it (e.g. Ethernet) should have overhead set to 0.

Device driver API

A device driver can have a simple two-functions library API exported in a header file using the same name, in the modules directory. The two functions to export will be:

  • A create function, accepting any argument required for the internal device configuration, that returns a pointer to the newly allocated device struct. The function must allocate the device and internally call the library function pico_device_init() in order to register the device into the stack. The pico_device_init() function accepts the following arguments:
    • dev: the device allocated just before
    • name: a null-terminated string containing a unique device name for the device to be inserted in the system (e.g. "eth0")
    • mac: a pointer to an Ethernet address in the form of a previously allocated pico ethdev structure, containing the hardware address to be used by the stack for datalink addressing. If no hardware-specic address is provided to pico_device_init() is provided (i.e. a NULL pointer is passed), the newly created device will be directly attached to the network layer and it will have to provide and process valid IP packets without further encapsulation.
  • A destroy routine, accepting the previously allocated device pointer to free all the associated structures.

The arguments these functions should take, depend on the type of device driver implemented. Here's an example of the VDE driver API:

struct pico_device *pico_vde_create(char *sock, char *name, uint8_t *mac);
void pico_vde_destroy(struct pico_device *dev);

The way to expand the device driver interface is by simply creating a new specific structure that contains it and thus inherits all the capabilities of the standard structure but also holds the required hardware-specific information. The three callbacks will always receive a pointer to the beginning of the pico_device structure, but the memory area that follows the structure can be used to keep track of the device hardware-specific context.

Naming conventions must be followed for the two functions exposed to the user interface to cre- ate and destroy the device. The functions must be named pico_X_create() and pico_X_destroy(), where X is the unique name of the device driver.

As an example of a very simple device driver, directly attached to the networking layer using the valid naming convention for the send/poll/create/destroy interfaces are contained in the source file modules/pico_dev_null.c and its header modules/pico_dev_null.h.

In order to create a new device instance, the application needs to call the pico_X_create function exported by the device driver, which will then perform the specific initialization needed before calling the generic pico_device_init from the stack API. From the moment the device is created, it becomes part of the stack itself.

Clone this wiki locally