![]() |
|
||||
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:
- Any modification to the Linux source must be made freely available to the rest of the world under the GPL license.
- You cannot distribute the driver statically linked to the LynxOS kernel. The ported driver must be dynamically installed, as LynxOS is not GPL code.
- No copyright text can be changed.
- If you use the driver source as a reference and write a completely new driver, there are no restrictions on what you may do with the final product, it is yours, not subject to the GPL.
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.
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):
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:
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
System Call Processing
The processing of system calls is similar on both LynxOS and Linux systems. The following table describes the differences.:
Preemption
System calls can be preempted on both systems, but the degree of preemption is different
Signal Handling
Signal handling also differs between the two systems
Error Handling
LynxOS and Linux handle error returns from system calls differently
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:
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:
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.
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 Get free memory Allocate contiguous physical memory kmalloc() with priority set to GFP_DMA with GFP_KERNEL or GFP_ATOMIC
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 Removing a timer
Semaphore Support
Semaphores are available on both systems.
Linux Semaphore Support
Under Linux, a semaphore is waited on with the down system call:
It is signaled with the up call:
Tasks blocked that are waiting on a semaphore can be woken up with the wake_up call:
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:
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:
- put_user() - Copies data from system space to user space
- get_user() - Copies data from user space to system space
The functions used to copy to and from user space to system space are:
- copy_from_user() - Copy from user space to system space
- copy_to_user() - Copies data from system space to user space
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:
- Initialization, static priority is assigned by the user and the dynamic priority is equal to the static priority.
- For each clock tick (occurring at 10 ms):
- When the scheduler is invoked, it gives the CPU time to the task with the highest "goodness" (need_resched(), dynamic = 0, block/yield)
- 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.
- 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:
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:
LynxOS operates similarly, but the data structure for the device is passed to the open() and close() calls directly:
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:
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:
This call should return the number of bytes read or an error.
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:
This call should return the number of bytes read or an error.
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:
This call should return a 0 or an error.
The select() call is used as follows:
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():
![]() LynuxWorks, Inc. 855 Branham Lane East San Jose, CA 95138 http://www.lynuxworks.com 1.800.255.5969 |
![]() |
![]() |
![]() |
![]() |