TOC PREV NEXT INDEX

Writing Device Drivers for LynxOS


Porting Linux Drivers to LynxOS

Due to the popularity of Linux, and the growing population of open source developers, a great deal of device driver code is freely accessible. Though it may not be practical for use directly on most real-time operating systems, many of these drivers can provide important benefits in understanding how a particular device must be manipulated.

For LynxOS developers, however, these device drivers are similar in structure to a LynxOS device driver, with the primary difference being the inherent real-time conditions. If a LynxOS developer needs a device driver and a Linux driver already exists, it is a relatively straightforward process to port it.

This appendix examines the differences and requirements of Linux and LynxOS at the device driver level.

GPL Issues

It is important to take note of GPL issues before using any GPL driver source code. Note that:

Driver Installation

Driver installation between Linux and LynxOS systems are similar. The following table provides a comparison of static and dynamic installs for both systems.

Driver Installation Differences
Install Type
Linux
LynxOS
Static
Link Driver with kernel from the directory linux/drivers with Makefile.
Link driver with kernel from directory sys/drivers and sys/devices with Makefile
Dynamic
With insmod, execute init_module()
With mmod, execute cleanup_module()
module_init()
module_exit()
Use drinstall and devinstall -c/-b
Or, use drvinstall() to install
Use drinstall, devinstall -u or drvuninstall() to uninstall.

Using a Device

For both Linux and LynxOS, devices are accessed through a special file called a node, typically located in the /dev directory. Nodes can be created with the mknod utility. The following is an example listing of two devices. Note that the com1 device node is a character device (leading "c" character), and the scsi drive is a block device (leading "b" character):

crw--ww- 1 root 6,0    Sep 3 12:01  /dev/com1
brw----- 1 root 1, 16  Sep 29 19:58 /dev/sd0a

Once a node is opened with the open() call, it can be accessed with any standard file operation (read(), write(), ioctl()).

Major and Minor Numbers

Major and Minor numbers are used to identify a device. For LynxOS, the order of the driver .cfg pointed to in the /sys/lynx.os/CONFIG.TBL file specifies the major number of the device.

For Linux, drivers can be statically or dynamically assigned major and minor numbers, however the proc/devices directory should be checked for number availability.

Accessing a Device

The facilities for accessing a device are described below for both LynxOS and Linux:

Device Access Differences
Function
Linux Facilities
LynxOS Facilities
mmap()
mmap() from application
mmap() from application
I/O port access
insb(), outsb(), readb/writeb
insw(), outsw(), readw/writew
instl(), outsl(), readl/writel
__inb(), __outb()
__inw(), __outw()
__inl(), __outl()
Physical to virtual address translation
vremap() can be used to map a physical address into a kernel address. The function
ioremap() can also be used.
permap() can be used to map a physical address into a kernel virtual address.
PCI autoconfig access
PCI functions are used to handle PCI devices.
DRM (Device Resource Manager) is a set of functions used to access PCI devices.

Driver Entry Points

The driver APIs for both Linux and LynxOS are fairly standard. They provide entry points from user space to driver space through a series of system calls. The following table shows the correlating calls between Linux and LynxOS.

Linux and LynxOS Entry Points  
Linux Call
LynxOS Call
setup(), module_init()
install()
init(), init_module()
install()
cleanup_module(), module_exit()
uninstall()
open()
open()
release()
close()
read()
read()
write()
write()
ioctl()
ioctl()
select()
select()
lseek()
ioctl()
mmap() ioremap()
mmap()
readdir(), fsync(), fasync()
strategy()
check_media(), change(), revalidate()
N/A

System Call Processing

The processing of system calls is similar on both LynxOS and Linux systems. The following table describes the differences.:

System Call Action Differences
Action
Linux
LynxOS
System call executed
When a task performs a system call, it is run in the task's context. The state changes from user to system.
When a task performs a system call, it is run in the task's context. The state changes from user to system.
Running process
A running process is called a task and can be a process or thread.
A running process is executed by at least the initial thread or the main thread.
Scheduling
The entities that get scheduled according to their priority are tasks
The entities that get scheduled according to their priority are user threads or kernel threads.
ISRs (Interrupt Service Request)
ISRs will run automatically in the current thread context when an interrupt occurs.
ISRs will run automatically in the current thread context when an interrupt occurs
ID Changes
One PID value for both threads and processes is used.
PID for processes and TID (Thread ID) for threads.)

Preemption

System calls can be preempted on both systems, but the degree of preemption is different

Preemption Differences
Linux
LynxOS
The running system call can be preempted by another task or by a slow interrupt.
The running thread can be preempted in the middle of its system call by another, higher priority thread, or by an interrupt.
The running system call can be preempted when it goes to sleep through the use of interruptible_sleep_on() option.
Preemption is implicit and can be disabled and restored with the functions sdisable() and srestore() (although there are non-reentrant areas in the kernel where this should not be done.)
The structure task_struct contains the priority value of the running task
The system call can get its priority with the function _getpriority().

Signal Handling

Signal handling also differs between the two systems

Signal Handling Comparison
Linux
LynxOS
The state of the task defines how systems are handled while blocking. The states TASK_INTERRUPTABLE and TASK_UNINTERRUPTIBLE are used to define the state of the task.
The flag's semaphore is used in the system call to define how signals are handled during blocking:
   IGNORE_SIGS
   DELIVER_SIGS
   ABORT_ON_SIGS
While blocking, these functions define if the task is interruptible:
   Interruptible_sleep_on()
   Sleep_on()
   Wake_up_interruptible()
   Wake_up()
For interrupt handling during blocking, the swait() or tswait() calls are used to affect behavior.

Error Handling

LynxOS and Linux handle error returns from system calls differently

Error Handling Comparison
Linux
LynxOS
The error value returned by the last system call can be stored in a global variable called errno.
The error value of the last system call can be set with pseterr() or retrieved with pgeterr(). The errno value in user context will then contain the appropriate value.

Interrupts

When porting Linux device drivers to LynxOS, it is important to understand the differences in how both operating systems handle interrupt requests. Linux has a multi-stage mechanism used to prioritize tasks. LynxOS uses a similar mechanism, but it uses the priority of the interrupt as the basis for prioritization. Later processing within the kernel thread that handles the interrupt provides another level of prioritization.

How Linux Handles Interrupts

Linux provides two types of interrupt service routines: Slow and Fast. Slow interrupt routines can be interrupted by fast routines. Fast routines can only be interrupted if it is enabled in the ISR (Interrupt Service Routine). Linux uses the cli() and sti() calls to disable and restore interrupts.

How LynxOS Handles Interrupts

LynxOS provides a single interrupt routine, which is prioritized exclusively by the hardware. Interrupts can be interrupted by other interrupts of a higher priority only. LynxOS uses the functions disable() and restore() to disable and restore interrupts. In addition, LynxOS drivers can use kernel threads for devices with unbounded interrupt latency to create bounded interrupt response.

Registering Interrupts

Interrupts are registered differently on LynxOS and Linux systems.

Registering Interrupts for Linux

Linux uses request_irq() function to register both fast and slow interrupt routines. An example prototype of an ISR is:

void do_irq(int irq, void *dev_id, struct pt_regs *regs);

The function to clear the ISR is called free_irq(). Interrupts can also be linked with the SA_SHIRQ flag when calling request_irq().

Registering Interrupts for LynxOS

LynxOS registers interrupts with the iointset() function. The prototype of the ISR is:

void do_irq(char *s);

ISRs are cleared with the iointclr() function. The address passed to the ISR is generally the address of the static structure of the device driver installed. Interrupts can be linked under LynxOS with ioint_link().

Blocking and Non-Blocking I/O

Both Linux and LynxOS support devices that include blocking and non-blocking I/O. The following table describes the differences between how the two systems handle these features.

Blocking & Non-Blocking I/O Differences
Action
Linux
LynxOS
Ability to select blocking or non-blocking
Read and write can be blocking or non-blocking. This is set when the device is opened.
Read and write can be blocking or non-blocking. This is set when the device is opened.
Non-blocking setting
Set through O_NONBLOCK flag passed through read or write in struct of file.
Set through O_NONBLOCK flag passed through read or write in struct of file.
Behavior if data is not available
The function interruptible_sleep_on() can be set to wait synchronously for data.
A semaphore can be set to wait synchronously with the functions wait() or tswait().
Behavior during wait
Task is put into the waiting queue.
The thread is put into a waiting state.
Behavior during data available
The interrupt can call wake_up() to allow the task to continue.
The ISR can post the semaphore with ssignal() and the thread continues according to its priority.

Bottom-Halves and Kernel Threads

This section outlines the behaviors used in handling prioritized interrupts.

Linux uses a bottom-half handler to process an interrupt that requires extensive processing, but is not time critical. Bottom half drivers are installed using init_bh() and removed with remove_bh(). An ISR can mark the bottom half of an ISR that needs to execute by using the function mark_bh(). Once marked, every bottom half handler runs automatically after slow interrupts occur, as well as whenever the scheduler invokes them.

LynxOS uses kernel threads to handle prioritized interrupt processing. Whenever an ISR is too long, its code can be placed into a kernel thread. The ISR can then signal the kernel thread to execute with an ssignal() semaphore. Kernel threads can be created with the ststart() call and removed with the stremove() call.

Kernel threads in LynxOS are scheduled for execution by prioritizing them at half a priority higher than the process which is using them. This is possible because although LynxOS has 256 user priorities, it really has 512 internal priorities to allow for this half-priority increase. The reason for this is to allow thread processing to complete before the user process that requested it runs.

Kernel Support

Memory allocation within the kernel is performed in similar ways.

Memory Allocation Differences
Action
Linux
LynxOS
Get a page
unsigned long get_free_page(int priority)
alloc_page
char *get1page();
void free1page(char *p);
Get free memory
void *kmalloc(size_t size, int priority)
void * vmalloc(unsigned long size);
char *sysbrk(long size);
void sysfree(char *p, long size)
Allocate contiguous physical memory
kmalloc() with priority set to GFP_DMA with GFP_KERNEL or GFP_ATOMIC
char *alloc_cmem(int size);
void free_cmem(char *p, int size);

Kernel Timer Support

Kernel timer support is provided under both systems with the following calls:.

Kernel Timer Support Differences
Action
Linux
LynxOS
Adding a timer
void add_timer(struct
timer_list *timer);
int timeout(int (*func()),
char *arg, int interval);
Removing a timer
int del_timer(struct
timer_list *timer);
cancel_timeout(int num);

Semaphore Support

Semaphores are available on both systems.

Linux Semaphore Support

Under Linux, a semaphore is waited on with the down system call:

void down(struct semaphore *sem);

It is signaled with the up call:

void up (struct semaphore *sem);

Tasks blocked that are waiting on a semaphore can be woken up with the wake_up call:

void wake_up(struct wait_queue *p)

LynxOS Semaphore Support

Under LynxOS, semaphores are waited on and signaled with the swait(), ssignal(), ssignaln() (for signaling multiple time) and sreset() using the following calls:

int swait(int *sem, int flag);
int ssignal(int *sem);
int ssignaln(int *sem, int count);
int sreset(int *sem)

Threads and processes waiting on a semaphore can be woken up in priority order with the sreset() call. Also, semaphores in LynxOS can be set to operate as priority inheritance semaphores to alleviate problems with priority inversion.

Address Translation

Address Translation for Linux

For Linux, when accessing a virtual user address from within a system call or any interrupt routine, the data needs to be copied from one address space to another. The functions used to copy to and from the kernel are:

The functions used to copy to and from user space to system space are:

The address of the process context can be retrieved from the current pointer of the current process context.

To validate addresses, the function access_ok() can be used with the VERIFY_WRITE flag to check for write permission. The access_ok() function with the VERIFY_READ flag can be used to check for read permission.

Address Translation for LynxOS

LynxOS allows direct access to user address space from entry points, as well as from within the system call, without the need for address translation. Interrupts and kernel threads do need to translate a user address to the kernel virtual address space with get_phys(). In all cases, user code can never directly access the kernel address space. Additionally, the currtptr pointer within the current system call contains the address of the process context.

Address validation under LynxOS is performed with the rbounds() and wbounds() calls. Users can also use the NOT_ALIGNED() function to see if an address is valid for use as anything other than a character pointer (returns nonzero value if this is true).

Driver Problem Reporting

Linux allows you to report problems within device drivers with sprintf() and vsprintf() to send strings to the console device. LynxOS uses cprintf() and kkprintf() to perform this same function.

A convention is to use cprintf() for error message reporting and kkprintf() for debug output.

Communications with Applications

Both Linux and LynxOS allow signals to be sent to user applications from within kernel space. Linux provides send_sig(), which can send one of 32 different signals. LynxOS uses _kill() or _killpg() (for a group of processes). For LynxOS, 64 different signals are supported (required by the POSIX API.)

Scheduling Differences

LynxOS differs from Linux in the way that it schedules the handling of interrupts. There is also a difference in the way schedules and threads are processed. These differences can affect the way in which drivers must be written to respond to user applications.

Linux Scheduling

Linux is designed as a "fair share" scheduling model. Linux tasks can have a priority associated with them, but this is not used as an absolute determinant of the process priority. The "real" priority of a process (what the scheduler uses to determine what to schedule next) is completely dynamic on a Linux system. The scheduler keeps track of the processes that have been running, and which processes have been denied running. The scheduler then attempts to balance the execution time for each. For example, if a task is running for a length of time, the scheduler lowers its "real" priority, allowing other waiting tasks to run.

Linux also distinguishes between tasks that perform different kinds of activities and attempts to grant them CPU time accordingly.

For interactive processes (ones that interact with users), the wakeup time must be short. This is important, because these kinds of processes spend much of their time waiting for input from mice, command shells, etc. Typically, the average delay in waking these kinds of processes up must fall between 50 and 150 ms to keep up with users.

For batch processes, a rapid wakeup time is not required, as these typically run in the background. Because these processes can afford to wait, they are often the first ones to be penalized by the scheduler to maintain responsiveness for the interactive processes.

For real-time processes, extremely strong scheduling requirements are enforced. These processes can never be blocked by lower-priority processes and must always be responded to in a very short time. Also, the variance of the response time should be minimal. Examples of these kinds of tasks include sound applications, and data collection and control.

The Linux scheduler generally behaves in the following way:

  1. Initialization, static priority is assigned by the user and the dynamic priority is equal to the static priority.

  2. For each clock tick (occurring at 10 ms):

- The dynamic priority is decremented.
- The "goodness"value is computed (it is equal to the sum of the static and dynamic priorities).
- If the dynamic priority decreases to 0, than the goodness value decreases to 0 as well.
  1. When the scheduler is invoked, it gives the CPU time to the task with the highest "goodness" (need_resched(), dynamic = 0, block/yield)

  2. When all tasks reach Dynamic = 0, all dynamic priorities are re-initialized to their static value and all tasks have the chance to run their time quantum.

  3. When the real-time flag is set for a task, it implies that its goodness value is always be kept high.

At the heart of this scheduling algorithm, the notion of a "goodness" value for each task is what controls what tasks run when. As previously mentioned, Linux behaves differently based on the type of task it is running. The goodness value determines this behavior. Here are the different actions taken when the value of goodness changes (c is the value returned by the goodness() call):

This task must never be selected. This value is returned when the run queue list contains only the init_task.

The task has exhausted its run time quantum. Unless this task is the first task in the run queue list and all the other runnable tasks have exhausted their quantum, it will not be selected for execution.

The task is a conventional task that has not exhausted its quantum. Note that a higher value of c denotes a higher level of goodness.

The task is a real-time process because the goodness value is very high.

LynxOS Scheduling

LynxOS uses a strict round-robin scheduler with fixed priority levels (there is a slight exception to this rule for priority inheritance scheduling). There are 256 priority levels in comparison to Linux's 99 (although Linux can also have negative priority levels). Kernel threads are scheduled in the global scheduling space along with user processes and user threads. Priority tracking and priority inheritance are also supported.

In general, LynxOS schedules processes and threads (tasks) in a strict priority sense. There is no other scheduling criteria for a process other than its priority. Tasks with a high priority ready to run are allowed to run immediately, preempting lower-priority running tasks. Also, the period of time that a high priority task takes to begin running is guaranteed to be bounded.

If a priority inheritance semaphore is used, the scheduler will alter the priority of the tasks involved by temporarily incrementing the priority of a high priority task that is waiting on a resource locked by a priority inheritance semaphore in the possession of a lower priority task. This keeps the high priority task from being denied by a medium priority task that preempts the lower priority task to keep it from completing its use of the resource.

The LynxOS scheduler runs each task within a priority level in turn for the period of time specified by that level's quantum (this is user-modifiable). If all the tasks within a particular priority level are waiting for I/O, tasks from the next lower priority queue are run.

Like Linux, tasks can voluntarily yield the CPU by using the yield() call.

Differences in Setting up a Driver

Setup

For Linux, the setup() function can be used to pass device-specific data to a driver for initialization. For LynxOS, the convention is to declare for every driver a structure that contains all the values that the device needs to be initialized with. The LynxOS install() entry point allocates memory for the driver and performs device initialization. The address of the driver info structure will then be passed automatically by the kernel to all the other entry points.

Installation

For Linux, a driver is registered within the kernel with the register_chrdev() or register_blkdev(). These functions are used by the Linux init() function. The major number can be choose or the kernel will find the highest free available one. The init() call also should check to see if the device is present.

LynxOS allows the device driver to be performed either statically or dynamically. The major device number cannot be choose, it is dictated by the kernel that finds the highest free available one. The LynxOS install() routine checks to see if the device is present and available.

Device Access: open() and close()

Linux passes the open() call the following information:

int open(struct inode *inode, struct file *file);

Here, the inode value contains the node information for device access.

The file variable contains the access mode, position in the device and the functions that can be used in the inodes.

open() should return a 0 (for success) or an error value on failure.

The release() call is used to close the device:

void release(struct inode *inode; struct file *file);

LynxOS operates similarly, but the data structure for the device is passed to the open() and close() calls directly:

int open(char *s, struct file *file);

Here, s is passed by the kernel automatically and is the address for the data structure returned from install().

The file variable contains the access mode, position in the device and the major and minor numbers of the device.

LynxOS device drivers are closed with a close() call:

int close(char *s, struct file *file);

Both open() and close() should return OK or SYSERR.

Device Access: read() and write():

Reading and writing are performed with similar calls on both systems. For Linux, read() is called as follows:

int read(struct inode *inode, struct file *file, char *buffer, int count);

This call should return the number of bytes read or an error.

write() is called as follows:

int write(struct inode *inode, struct file *file, char *buffer, int count);

It should return the number of bytes written or an error.

Data in both cases needs to be copied from one address space (user) to another (system or kernel space of the driver).

LynxOS operates similarly, with the read() call declared as follows:

int read(char *s, struct file *file, char *buffer, int count);

This call should return the number of bytes read or an error.

For the write() call:

int write(char *s, struct file *file, char *buffer, int count);

This call should return the number of bytes written or an error.

The entry points can directly use the buffer address to access count data.

Device Access: Control

The ioctl() and select() calls are universal in both LynxOS and Linux, as well as the calls for device control. The ioctl() call allows control of a device and select() allows you to wait on multiple channels of the device.

For Linux, ioctl() is called as follows:

int ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg);

This call should return a 0 or an error.

The select() call is used as follows:

int select(struct inode *inode, struct file *file, int sel_type, slect_table *wait);

It should return a 0 or 1 when one of the devices becomes available.

LynxOS is similar, with the exception of passing the control structure first for ioctl():

int ioctl(char *s, struct file *file, int cmd, char *arg);

This should return OK or SYSERR.

For select():

int select(char *s, struct file *file, int which, struct sel *ffs);

Return value should be 0 or SYSERR.



LynuxWorks, Inc.
855 Branham Lane East
San Jose, CA 95138
http://www.lynuxworks.com
1.800.255.5969
TOC PREV NEXT INDEX