Using Minidrivers for Instant Device Activation

Instant Device Activation (also known as minidriver) allows you to provide higher levels of integration for your hardware. Advanced CPUs are providing higher levels of hardware integration than ever before. For example, the main CPU can now directly control CAN, J1850, and MOST interfaces.

This approach saves on hardware costs by reducing the need for extra chips and circuitry, but it also raises concerns for the software developer. For instance, a telematics control unit must be able to receive CAN messages within 30 to 100 milliseconds from the time that it is powered on. The problem is, the complex software running on such a telematics device can easily take hundreds of milliseconds, or more, to boot up.

For example, consider the critical milestones during the boot process for an in-car telematics or infotainment unit that typically boots from a cold condition (completely powered off) or from CPU reboot condition (returning from a state where SDRAM has been turned off). The unit must be able to:

In order to address the timing requirements described above, many embedded system designs rely on a simple but expensive solution that uses an auxiliary communications processor or external power module. This auxiliary hardware can be reduced in scope and sometimes even eliminated due to innovative software from QNX Software Systems. Also called minidriver technology, the approach consists of small, highly efficient device drivers that start executing before the OS kernel is initialized.

The minidriver basics

During the normal Neutrino boot process, a driver process can't run until the OS image has been loaded into the RAM, and the kernel has been initialized. Depending on the particular hardware (processor, flash, architecture) and the OS image size, this time can be in the order of hundreds of milliseconds or even seconds. To reduce this time, a minidriver runs much earlier in the boot process to take care of the timing requirements for some bus protocols such as MOST or CAN.

Defined in the system's startup code, a minidriver runs user code before the operating system has been booted. This code could include responding to hardware power-up messages in a quick, timely fashion and ensuring that no message is lost when the OS boots up. Once the OS has booted, the minidriver may continue running, or it may pass control to a full-featured driver that can access any data the minidriver has buffered.


The minidriver


Booting process using instant device activation.

The minidriver architecture

A minidriver consists of two fundamental components:

Once a minidriver is created, its handler function is called throughout the booting process. The handler function is initially triggered from a timer (polling). Once CPU interrupts are enabled, the handler function is triggered by the real hardware interrupt. Note that the timers can also generate interrupts, allowing for a polled approach to be used for hardware that doesn't generate interrupts.

How does the minidriver work?

A minidriver is a function (piece of code) that you link to the Neutrino startup program, so that it runs Run this code before the system becomes operational and the kernel is initialized. A minidriver can access hardware and store data in a RAM buffer area where a full (process time) driver can then read this buffered information.

During system startup, a minidriver handler function (that is added to the Neutrino startup) is periodically called (or polled) . You can adapt this periodic/polled interval to suit your device's timing requirements with minor changes to the startup program. At some point in the system startup, interrupts will be available and this handler function will be interrupt driven. The handler is called with a state variable.

See below for a prototype of this minidriver handler function:

int mdriver_handler(int state, void *data);

Seamless transition

As soon as a full driver process is running in a fully operational system, transition takes place from the minidriver to a full driver. This transition is seamless and causes no blackout times. The full driver merely attaches to the device interrupt, which causes the minidriver to be notified that another process is attaching to its interrupt. The minidriver can then gracefully exit and the full driver will continue to run. The full driver also has access to any buffered data that the minidriver chooses to store.

Running multiple handler functions

The minidriver can run multiple handler functions. For devices that must do something every n milliseconds, you could attach two handler functions:

Since the timer minidriver will be polled (i.e. not invoked at a constant interval) during startup, the driver will need to use something to measure the time between the calls to get the proper interval.

This architecture will allow device drivers to start very early in the system startup and allow the device to continue to function during all boot phases. If a full driver doesn't choose to take over device control, the minidriver will continue to run while the system is operational.

Writing a minidriver

In order to write a minidriver, you must first decide on the following:

Hardware platform

Get the BSP associated with your hardware platform; it includes the source code to the board's startup program. You must link your minidriver to this startup program.

For more information, see the BSP documentation.

For information about the functions in the startup library, see the Customizing Image Startup Programs chapter of Building Embedded Systems.


Note: If you're working with an ARM platform, your minidriver handler function must be written as Position Independent Code (PIC). This means that when your handler is in the MDRIVER_KERNEL, MDRIVER_PROCESS, or MDRIVER_INTR_ATTACH state, you must not use global or static variables.

Timing requirements

Since the minidriver code is polled during the startup and kernel-initialization phases of the boot process, you need to know the timing of your device in order to verify if the poll rate is fast enough. Most of the time in startup is spent copying the boot image from flash to RAM and the minidriver is polled during this time period.

The startup contains a global variable mdriver_max, which is the size of data (in bytes) that is copied from flash to RAM between calls of your minidriver. The default size is 16 KB. The appropriate data size should be based on the timing requirements of your device, processor speed, and the flash. The file that contains this variable is called mdriver_max.c and can be found in the startup library.

In order to change this value, there are two options:

Data storage

The minidriver program requires a space to buffer the received hardware data that is later passed to the full driver at process time. You will need to determine the amount of data you require and allocate the memory.

You have the choice of specifying where the memory is located or having startup choose a safe location. In order to allocate the memory, you make the following function call:

paddr_t alloc_ram(phys_addr, size, alignment);

The address returned is the physical address of your memory area. This is used when you register your minidriver with the startup program. Since you are reserving an area of RAM, make sure you call this function after RAM has been setup, i.e. after calling init_raminfo().

This area of memory isn't internally managed by the system, it's your drivers responsibility to make sure it doesn't overwrite system memory and cause the startup to crash.

Hardware initialization

If your driver requires hardware initialization, you should place code in the MDRIVER_INIT handler of the minidriver. The MDRIVER_INIT state is the first state of the minidriver and it's set only once.

Hardware Access

The minidriver program most likely requires hardware access, meaning it needs to read and write hardware registers. In order to access hardware registers, the startup library provides function calls to map and unmap physical memory.

When the minidriver handler is called with MDRIVER_STARTUP_INIT, you call:

uintptr_t startup_io_map(size, phys_addr);
startup_io_unmap(paddr);
void * startup_memory_map(size, phys_addr);
startup_memory_unmap(paddr);

After the minidriver handler is called with MDRIVER_STARTUP_PREPARE, the above functions are no longer available and your driver must use:

uintptr_t callout_io_map(size, phys_addr);
void * callout_memory_map(size, phys_addr);

At different times in the boot process, some calls may or may not be available. If your driver requires hardware access, it must do the following:

MDRIVER_INIT
MDRIVER_STARTUP
Use the stored pointer, ptr1, to do all hardware access. No memory map calls are needed.
MDRIVER_STARTUP_PREPARE
At this point, it's safe to call callout_io_map() or callout_memory_map(), but don't use the pointer returned.

The minidriver should call one of the above functions and store the pointer in the minidriver data area or in a static variable, separate from the previously stored value (ptr2). Use the stored pointer, ptr1, to do all hardware access.

MDRIVER_STARTUP_FINI
Use the stored pointer, ptr1, to do all hardware access. Any further calls will require the minidriver to use the pointer mapped with callout_io_map() or callout_memory_map() mapped pointer, so your driver must maintain this information in the data area.
MDRIVER_KERNEL
Use the stored pointer, ptr2, to do all hardware access.
MDRIVER_PROCESS
Use the stored pointer, ptr2, to do all hardware access.
MDRIVER_INTR_ATTACH
Use the stored pointer, ptr2, to do all hardware access.

Transition to full driver

Once the kernel is running, your full driver process can run a transition from the minidriver to the full driver. The full driver can take control of the hardware device and the minidriver can then gracefully exit. In order to perform the transition, the full driver does the following:

The minidriver is called with a state of MDRIVER_INTR_ATTACH>. At this point, the minidriver should do any cleanup necessary and disable the device interrupt. The minidriver handler then can return a nonzero value, which indicates that it should exit. The successful transition occurs when:

Sample minidriver

The sample minidriver program in this example is a simple implementation that can be used for debug purposes. In this implementation, the minidriver counts the number of times it is called for each phase of the boot process and store that information in its data area. Once the system is booted, a program can then read this data area and retrieve this information.

Implementation notes

Timing data will be stored in a shared memory area. In this example, the size of this memory is set to 64 KB. If you decrease the mdriver_max value from 16 KB to a lower value, then you may need to increase this 64 KB value (e.g. to 128 KB). This is due to a larger number of callouts made when mdriver_max is decreased.

You should use the default mdriver_max value of 16 KB. So, the data storage required will be 64 KB. The assumption here is that there is no hardware access required for this implementation.

The minidriver handler function

The prototype for the minidriver handler function is as follows:

int mdriver_handler(int state, void *data);

See the description for mdriver_add() for the definition of state and the data pointer variable.

For this sample driver, the source code for the handler function would look like this:

struct mini_data
{
   uint16_t	nstartup;
   uint16_t	nstartupp;
   uint16_t	nstartupf;
   uint16_t	nkernel;
   uint16_t	nprocess;
   uint16_t	data_len;
};

/*
 * Sample minidriver handler function for debug purposes
 * 
 * Counts the number of calls for each state and
 * fills the data area with the current handler state
 */
int 
mini_data(int state, void *data)
{
   uint8_t		*dptr;
   struct mini_data	*mdata;
		
   mdata = (struct mini_data *) data;
   dptr = (uint8_t *) (mdata + 1);
	
   /* on MDRIVER_INIT, set up the data area */
   if (state == MDRIVER_INIT)
   {
      mdata->nstartup = 0;
      mdata->nstartupf = 0;
      mdata->nstartupp = 0;
      mdata->nkernel = 0;
      mdata->nprocess = 0;
      mdata->data_len = 0;
   }
	
   /* count the number of calls we get for each type */	
   if (state == MDRIVER_STARTUP)
      mdata->nstartup = mdata->nstartup + 1;
   else if (state == MDRIVER_STARTUP_PREPARE)
      mdata->nstartupp = mdata->nstartupp + 1;
   else if (state == MDRIVER_STARTUP_FINI)
      mdata->nstartupf = mdata->nstartupf + 1;
   else if (state == MDRIVER_KERNEL)
      mdata->nkernel = mdata->nkernel + 1;
   else if (state == MDRIVER_PROCESS)
      mdata->nprocess = mdata->nprocess + 1;
   else if (state == MDRIVER_INTR_ATTACH)
   {
      /* normally disable my interrupt */
      return (1);
   }
	
   /* put the state information in the data area 
   after the structure if we have room */
		
   if (mdata->data_len < 60000 ) {
      dptr[mdata->data_len] = (uint8_t) state;
   mdata->data_len = mdata->data_len + 1;
}
		
   return (0);
}

In this example, the handler function stores call information, so a structure has been created to allow easier access to the data area.

Since the data area is set as 64 KB, we ensure that we don't write any data outside this area. If your handler function writes outside of its data space, a system failure can occur, and the operating system may not boot. Always be sure to properly bound memory reads and writes.

During the MDRIVER_INIT state, the data area is initialized. this is only called once. If your handler is called with MDRIVER_INTR_ATTACH, it returns a value of 1, requesting an exit of the handler. However, due to the asynchronous nature of the system, there might be several more invocations of the handler after it has indicated that it wants to stop.

Adding your minidriver to the system

Once you have written a handler function, you need to register it with startup and allocate the required system RAM for your data area. This can be accomplished with the following functions:

paddr_t alloc_ram(phys_addr, size, alignment);
int mdriver_add(name, interrupt, handler, data_paddr, data_size);

Since you're allocating memory and passing an interrupt, these functions must be called after RAM is initialized by calling init_raminfo(), and after the interrupt information is added to the system page by calling init_intrinfo().

The main() function of startup main.c looks like as follows:

...
paddr_t		mdrvr_addr;
...

/*
* Collect information on all free RAM in the system.
*/
init_raminfo();

//
// In a virtual system we need to initialize the page tables
//
if(shdr->flags1 & STARTUP_HDR_FLAGS1_VIRTUAL) init_mmu();

/*
* The following routines have hardware or system dependencies which
* may need to be changed.
*/
init_intrinfo();
mdrvr_addr = alloc_ram(~0L, 65535, 1); /* make our 64 k  data area */
mdriver_add("mini-data",  0,  mini_data, mdrvr_addr, 65535);
...

In this example, we have allocated 64 KB of memory and registered our minidriver handler function with the name mini-data.

Build startup

Once your minidriver is complete, you must rebuild startup code and the boot image. In order to verify that the minidriver is running properly, you may want to add debug information in the handler function or write an application to read the minidriver data area and print the contents.

Test application: mini-peeker.c

Below is the source code for a test application called mini-peeker.c that maps in the minidriver data area and prints the contents:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/neutrino.h>
#include <sys/syspage.h>
#include <hw/inout.h>
#include <inttypes.h>

struct mini_data
{
   uint16_t	nstartup;
   uint16_t	nstartupp;
   uint16_t	nstartupf;
   uint16_t	nkernel;
   uint16_t	nprocess;
   uint16_t	data_len;
};

int main(int argc, char *argv[]) 
{
   int 		i, count;
   int			dump_data = 0;
   uint8_t 	*dptr;
   struct mini_data	*mdata;
	
   if (argv[1])
       dump_data = 1;
		
   ThreadCtl(_NTO_TCTL_IO, 0);

   /* map in minidriver data area */
   if ((dptr = mmap_device_memory(0, 65535, PROT_READ | 
                                  PROT_WRITE | PROT_NOCACHE, 0, 
      	     SYSPAGE_ENTRY(mdriver)->data_paddr)) == NULL)
   {
       	fprintf(stderr, "Unable to get data pointer\n");
       	return (-1);
   }

   mdata = (struct mini_data *) dptr;
   dptr = dptr + sizeof(struct mini_data);

   /* dump mini-driver data */	
   printf("---------------- MDRIVER DATA -------------------\n");
   printf("\tMDRIVER_STARTUP         calls = %d\n", mdata->nstartup);
   printf("\tMDRIVER_STARTUP_PREPARE calls = %d\n", mdata->nstartupp);
   printf("\tMDRIVER_STARTUP_FINI    calls = %d\n", mdata->nstartupf);
   printf("\tMDRIVER_KERNEL          calls = %d\n", mdata->nkernel);
   printf("\tMDRIVER_PROCESS         calls = %d\n", mdata->nprocess);
   printf("\tData Length             calls = %d\n", mdata->data_len);
   count = mdata->data_len;

   if (dump_data)
   {	
       printf("State information:\n");
       for (i = 0; i < count; i++)
	    printf("%d\n", dptr[i]);
    }
    printf("\n---------------------------------\n");

   return EXIT_SUCCESS;
}

Compile this code for your target system using regular compile technique. For example:

qcc -Vgcc_ntoppcbe mini-peeker.c -o mini-peeker

Transition from minidriver to full driver

Once the system is booted and your full driver is running, a transition must take place where the full driver takes over from the minidriver. The full driver should attach to the minidriver's interrupt and then it should read the minidriver's data area in order to retrieve any stored information.

Once your full driver does an InterruptAttach() or InterruptAttachEvent(), the kernel calls the minidriver with a state value of MDRIVER_INTR_ATTACH. When your minidriver handler receives this state, it should do any cleanup necessary and then exit.

Once your full driver is attached to the interrupt it can deal with any buffered data and continue to provide hardware access. A sample of this code would look like:

if ((id == InterruptAttachEvent(intr, event, _NTO_INTR_FLAGS_TRK_MSK)) == -1)
{
   perror("InterruptAttachEvent\n");
   return (-1);
}


if ((dptr = mmap_device_memory(0, data_size, PROT_READ | PROT_WRITE | PROT_NOCACHE, 
                               0, SYSPAGE_ENTRY(mdriver)->data_paddr)) == NULL)
{
   fprintf(stderr, "Unable to get data pointer\n");
   return (-1);
}

/* Your minidriver should now be stopped and you should have access to the interrupt and data area */ /* Enable device interrupt (intr) */

For safety, your full driver should always disable the device interrupt before doing the InterruptAttach[Event]() and then enable the interrupt upon success.

Minidriver implementation notes

Let's examine in more detail the steps for developing, running and debugging a minidriver.

These are the main phases when developing a minidriver:

Note that the last phase “making the transition to the real (full) driver” is optional. Until you turn off the minidriver by attaching an interrupt handler (InterruptAttach*()), your handler continues to receive interrupts and can be called even after the Neutrino system is booted and running. This may be a desired design.

Let's look at each of the phases of development and some tips and techniques for each phase.

Customizing the startup program that contains your minidriver code

This is the core of the minidriver development.

To implement a minidriver, the following steps are required:

  1. Modify mdriver_max.c in libstartup.

    (On your development host, this file is located in your workspace as part of your BSP files in a directory that ends with libstartup).

    By default, this value is set to 16KB (with a standard QNX 6.3.0 Board Support Package) and is the amount of data that is copied at one time when the boot image is copied from flash to RAM. The minidriver callout will be called after each copy. For example:

    minidriver callout
    copy next 16K
    minidriver callout
    copy next 16K
    etc.

    It may be necessary to modify this value. For example, on a MPC5200 running at 396 MHz, the time needed to copy 16KB is around 8 milliseconds. This will vary depending on the speed of the processor and the speed of the flash. If the mdriver_max is modified to be a 1KB copy value, then the time needed to copy this 1KB drops to less than 1 millisecond. The value of mdriver_max will need to be set based on experimentation.

    If mdriver_max.c is modified, be sure to recompile the libstartup.a library. Also, relink your startup code with this new library.

  2. Modify the necessary startup files and add in your minidriver code.

    The following files are modified in your startup code. They all exist in the same directory as your BSP startup code. For example, if you are building a BSP for the Media5200b board, you will have imported the BSP into QNX Momentics IDE (in to your workspace). The following files will change:

    See the examples in the Sample Drivers for Instant Device Activation chapter of this guide.

    Here are the highlights:

    main.c
    • Set the prototype for your minidriver callout function e.g.
      extern int mini_data(int state, void *data);		
    • Allocate a memory area for your data e.g.
      //Global
      paddr_t	mdriver_addr; // allocate 64K of memory 
                            // for use by the minidriver
      		      
      mdriver_addr = alloc_ram(~0L, 65536, 1);
    • Set your minidriver callout to be called. Do this after you call init_intrinfo() e.g.
      //Code to add a sample minidriver function 
      //called "mini-data" for irq=81 
      mdriver_add("mini-data", 81, mini_data, mdrvr_addr, 65536);	
    cpu_mdriver.c
    • Don't modify this file unless directed by QNX Software Systems.
    • Make sure this C file is included in your startup code directory
    mdriver.c
    • Don't modify this file unless directed by QNX Software Systems.
    • Make sure this C file is included in your startup code directory.
    mini-driver.c
    • This is your minidriver code. You can name this C file anything you wish. What is required is that the minidriver function defined in the mdriver_add() function be part of your startup code.
    • You may have multiple C and header files as part of your minidriver

    Try out one of the samples that is included with this package and build the new startup program (e.g. startup-mgt5200).

Making a boot image that includes your minidriver

Until this point, you will have a startup program (including your minidriver code) that has been compiled. Now include this startup program in the Neutrino boot image and try out the minidriver.

There are some basic rules to follow when building a boot image that includes a minidriver:

Compression
The boot image shouldn't be compressed. Decompression of the boot image modify the timings defined by the mdriver_max() copy size. Your boot image should have the following image type defined:
[virtual=ppcbe,binary] .bootstrap = {

Note that the keyword +compress isn't included in this line. You should change the ppcbe or binary entry to reflect your hardware and image format.

Startup program
After you compile your startup program that includes the minidriver, make sure to specify this startup program in your buildfile.

For example, for the Media5200b board, if you compile your startup program as startup-mgt5200, you should copy it to ${QNX_TARGET}/ppcbe/boot/sys/startup-mgt5200-mdriver, and then change your buildfile to include startup-mgt5200-mdriver.

For more information and sample buildfiles, see the Sample Drivers for Instant Device Activation chapter of this guide.

Debugging from within the minidriver

Use the following techniques to debug your minidriver:

Displaying information about minidriver status (after the kernel boots)

After the kernel has booted, examine the shared memory that you allocated for your minidriver, by calling alloc_ram(). Pass the paddr_t to the mdriver_add() function so that there is a link between your minidriver and the shared memory area.

See the example mini-peeker.c.

Making the transition to the real driver

The minidriver is called based on the following sequences: