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.
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.
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.
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);
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.
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.
In order to write a minidriver, you must first decide on the following:
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
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. |
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:
unsigned mdriver_max = KILO(16);
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.
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.
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:
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.
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:
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.
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 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.
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.
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.
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
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.
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.
This is the core of the minidriver development.
To implement a minidriver, the following steps are required:
(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.
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:
main.c // to call mdriver_add() for your callout cpu_mdriver.c // This file is added to your system. // QNX provides this. Don't change the file. mdriver.c // This file is added to your system. // QNX provides this. Don't change the file. mini-driver.c // The rest of the code is your mini-driver code // including the main handler function.
See the example files in the Sample Drivers for Instant Device Activation chapter of this guide.
Here are the highlights:
extern int mini_data(int state, void *data);
//Global paddr_t mdriver_addr; // allocate 64K of memory // for use by the minidriver mdriver_addr = alloc_ram(~0L, 65536, 1);
//Code to add a sample minidriver function //called "mini-data" for irq=81 mdriver_add("mini-data", 81, mini_data, mdrvr_addr, 65536);
Try out one of the samples that is included with this package and build the new startup program (e.g. startup-mgt5200).
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:
[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.
For the Media5200b board,
if you compile your startup program as startup-mgt5200, then you can do the following:
copy startup-mgt5200 to $(QNX_TARGET)/ppcbe/boot/sys/startup-mgt5200-mdriver change my build file to include this startup-mgt5200-mdriver program
See the Sample Drivers for Instant Device Activation chapter of this guide for more information and sample build files.
Use the following techniques to debug your minidriver:
If your startup code is able to print data to a serial port or other debug device, then you can use kprintf() to print any variable you wish to see, e.g. in your minidriver code:
kprintf("I am the minidriver!\n"); kprintf("Global variable mcounts=%d\n", mcounts);
Include any data that you wish to collect in the shared memory area allocated for your minidriver. After the kernel has booted, you can examine the data inside the shared memory area. See the mini-peeker.c program for an example of doing this.
Depending on your hardware, you could use JTAG. If LEDs or other diagnostics are available, your minidriver could output values to hardware registers or ports to indicate certain conditions.
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.
The minidriver is called based on the following sequences:
Usually, there is a need to do more with the hardware than what the minidriver is set up to do. There will be a transition or handoff to the real driver that will attach to the real interrupt.
To do this properly, here are the sequences of events that should be done:
The real driver attaches to the shared memory area that is owned by the minidriver. For example:
dptr = mmap_device_memory(0, 65536, PROT_READ | PROT_WRITE | PROT_NOCACHE, 0, SYSPAGE_ENTRY(mdriver)->data_paddr);
Since the minidriver is still running at this point, it continues to run whenever the interrupt is triggered until the real driver attaches to the same interrupt. Depending on the design, it may be necessary to do some processing of the existing data that has been stored by the minidriver before the real driver takes control.
The real driver calls the function InterruptAttach() or InterruptAttachEvent() (the latter is preferred over InterruptAttach()). When this call is made, the minidriver receives a message of type MDRIVER_INTR_ATTACH. The minidriver returns a value of 1.
From now onwards, the minidriver will no longer be called on the interrupt. Only the real driver will receive the interrupt. The shared memory area will continue to exist until you remove it.