QNX Neutrino RTOS Building Embedded Systems
QNX Neutrino RTOS Building Embedded Systems
QNX Neutrino RTOS Building Embedded Systems
QNX, QNX CAR, Neutrino, Momentics, Aviage, and Foundry27 are trademarks
of BlackBerry Limited that are registered and/or used in certain jurisdictions,
and used under license by QNX Software Systems Limited. All other trademarks
belong to their respective owners.
Table of Contents
About This Book ......................................................................................................................11
Typographical conventions ...............................................................................................12
Technical support ...........................................................................................................14
int15_copy() .......................................................................................................103
print_byte() .........................................................................................................103
print_char() ........................................................................................................103
print_long() .........................................................................................................103
print_sl() ............................................................................................................103
print_string() .......................................................................................................103
print_var() ..........................................................................................................104
print_word() ........................................................................................................104
protected_mode() ................................................................................................104
uart_hex8 ...........................................................................................................104
uart_hex16 .........................................................................................................105
uart_hex32 .........................................................................................................105
uart_init .............................................................................................................105
uart_put .............................................................................................................106
uart_string ..........................................................................................................106
uart32_hex8 .......................................................................................................106
uart32_hex16 .....................................................................................................107
uart32_hex32 .....................................................................................................107
uart32_init .........................................................................................................107
uart32_put .........................................................................................................108
uart32_string ......................................................................................................108
chip_read16() .....................................................................................................157
chip_read32() .....................................................................................................157
chip_write8() ......................................................................................................157
chip_write16() ....................................................................................................157
chip_write32() ....................................................................................................157
copy_memory() ....................................................................................................157
del_typed_string() ................................................................................................157
falcon_init_l2_cache() .........................................................................................158
falcon_init_raminfo() ...........................................................................................158
falcon_system_clock() ..........................................................................................158
find_startup_info() ...............................................................................................158
find_typed_string() ..............................................................................................158
handle_common_option() .....................................................................................158
hwi_add_device() ................................................................................................160
hwi_add_inputclk() ..............................................................................................160
hwi_add_irq() ......................................................................................................160
hwi_add_location() ..............................................................................................160
hwi_add_nicaddr() ...............................................................................................161
hwi_add_rtc() ......................................................................................................161
hwi_alloc_item() ..................................................................................................161
hwi_alloc_tag() ....................................................................................................161
hwi_find_as() ......................................................................................................161
hwi_find_item() ...................................................................................................162
hwi_find_tag() .....................................................................................................162
hwi_off2tag() ......................................................................................................162
hwi_tag2off() ......................................................................................................163
init_asinfo() ........................................................................................................163
init_cacheattr() ...................................................................................................163
init_cpuinfo() ......................................................................................................163
init_hwinfo() .......................................................................................................163
init_intrinfo() ......................................................................................................164
init_mmu() .........................................................................................................164
init_pminfo() .......................................................................................................164
init_qtime() ........................................................................................................164
init_qtime_sa1100() ............................................................................................165
init_raminfo() ......................................................................................................165
init_smp() ...........................................................................................................165
init_syspage_memory() (deprecated) ......................................................................165
init_system_private() ............................................................................................166
jtag_reserve_memory() .........................................................................................166
kprintf() ..............................................................................................................166
openbios_init_raminfo() .......................................................................................166
pcnet_reset() ......................................................................................................166
print_syspage() ....................................................................................................167
rtc_time() ...........................................................................................................168
Table of Contents
startup_io_map() .................................................................................................169
startup_io_unmap() .............................................................................................169
startup_memory_map() ........................................................................................169
startup_memory_unmap() .....................................................................................169
tulip_reset() ........................................................................................................169
uncompress() ......................................................................................................170
x86_cpuid_string() ..............................................................................................170
x86_cputype() .....................................................................................................170
x86_enable_a20() ...............................................................................................170
x86_fputype() .....................................................................................................171
x86_init_pcbios() ................................................................................................171
x86_pcbios_shadow_rom() ...................................................................................171
x86_scanmem() ..................................................................................................172
Writing your own kernel callout ......................................................................................173
Find out who's gone before ...................................................................................173
Why are they in assembly language? ......................................................................174
Starting off .........................................................................................................175
“Patching” the callout code .................................................................................175
Getting some R/W storage ....................................................................................177
The exception that proves the rule .........................................................................178
QNX Neutrino runs on several processor families (e.g., ARM, x86). For
information on getting started with QNX Neutrino on a particular board, refer
to the appropriate BSP (Board Support Package) documentation for your board.
Topic Chapter(s)
Getting started with your board support Working with a BSP (p. 29)
package
We assume that you've already installed QNX Neutrino and that you're familiar
with its architecture. For a detailed overview, see the System Architecture
manual.
For information about programming in QNX Neutrino, see Get Programming with the
QNX Neutrino RTOS and the QNX Neutrino Programmer's Guide.
Typographical conventions
Throughout this manual, we use certain typographical conventions to distinguish
technical terms. In general, the conventions we use conform to those found in IEEE
POSIX publications.
Reference Example
Commands make
Parameters parm1
You'll find the Other... menu item under Perspective ➝ Show View .
Cautions tell you about commands or procedures that may have unwanted
or undesirable side effects.
Technical support
Technical assistance is available for all supported products.
To obtain technical support for any QNX product, visit the Support area on our website
(www.qnx.com). You'll find a wide range of support options, including community
forums.
In this chapter, we'll take a “high-level” look at the steps necessary to build a complete
QNX Neutrino-based embedded system, with pointers to the appropriate chapters for
the lower-level details.
First we'll see what a QNX Neutrino system needs to do in order to run. Then we'll
look at the components and how they operate. Finally, we'll do an overview of the steps
you may need to follow when customizing certain portions.
Introduction
From the software perspective, the following steps occur when the system starts up:
1. Processor begins executing at the reset vector. The Initial Program Loader (IPL)
locates the OS image and transfers control to the startup program in the image.
2. Startup program configures the system and transfers control to the microkernel
and process manager (procnto).
3. The procnto module loads additional drivers and application programs.
After we look at the software aspects in some more detail, we'll consider the impact
that the hardware has on this startup process.
The first step performed by the software is to load the OS image. This is done by a
program called the Initial Program Loader (IPL).
The IPL's initial task is to minimally configure the hardware to create an environment
that will allow the startup program, and consequently the microkernel, to run.
Specifically, this task includes at least the following steps:
The IPL is described in detail in the chapter on Writing an IPL Program (p. 77).
Startup
procnto
Other
files
...
There are two general types of IPL: warm-start and cold-start. Warm-start IPL is typically
invoked by a ROM-monitor or BIOS; some aspects of the hardware and processor
configuration will have already been set up.
With cold-start IPL, on the other hand, nothing has been configured or initialized —
the CPU and hardware have just been reset. Naturally, the work that needs to be done
within a warm-start IPL will be a subset of the work required in a cold-start IPL.
We'll approach the discussion of the IPL's responsibilities starting at the end, describing
the goal or final state that everything should be in just before the first component of
the image is started. Then we'll take a look at the steps necessary to get us to that
final state.
Depending on the design of your target, you may have to take a number of steps,
ranging from none (e.g. you're running on a standard platform with a ROM monitor or
BIOS, and have performed a warm-start IPL via disk or network boot; the boot ROM
has done all the work described below for you) to many (e.g. you have a custom
embedded system without firmware and the image is stored on a specialized piece of
hardware).
The final state (just before the first component of the image is started) is characterized
by the following:
• The memory controller has been configured to give access to the memory present
on the system.
• Minimal hardware configuration has been performed (e.g. chip selects to map
EPROMs have been programmed).
• The entire image is now located in linearly addressable memory.
• The first part of the image, the startup code, is now in RAM. (Note that the startup
code is relatively small and that the RAM area is reclaimed when the startup code
is finished.)
Either the IPL or the BIOS/ROM monitor code is responsible for transferring the image
to linearly addressable memory. The OS image must have been built in a format that
the IPL or ROM monitor code understands so that it can know where to place the
image in memory and to what address to pass control after the image has been loaded.
For example, an IBM PC BIOS system typically loads a raw binary and then jumps to
the first address. Other systems may accept an image in ELF format, using the ELF
header information to determine the location to place the image as well as the starting
address. Refer to the documentation that came with your hardware to find out what
image formats the IPL code can accept.
Once the IPL has located the image, and the entire image is now in linearly addressable
memory, control is transferred to the startup program. At that point, the IPL is done
and is out of the picture.
The second step performed by the software is to configure the processor and hardware,
detect system resources, and start the OS. This is done by the startup program. (For
details, see the chapter on Customizing Image Startup Programs (p. 109).)
While the IPL did the bare minimum configuration necessary to get the system to a
state where the startup program can run, the startup program's job is to “finish up”
the configuration. If the IPL detected various resources, it would communicate this
information to the startup program (so it wouldn't have to redetect the same resources.)
To keep QNX Neutrino as configurable as possible, we've given the startup program
the ability to program such things as the base timers, interrupt controllers, cache
controllers, and so on. It can also provide kernel callouts, which are code fragments
that the kernel can call to perform hardware-specific functions. For example, when a
hardware interrupt is triggered, some piece of code must determine the source of the
interrupt, while another piece of code must be able to clear the source of the interrupt.
Note that the startup program does not configure such things as the baud rate of serial
ports. Nor does it initialize standard peripheral devices like an Ethernet controller or
EIDE hard disk controller — these are left for the drivers to do themselves when they
start up later.
Once the startup code has initialized the system and has placed the information about
the system in the system page area (a dedicated piece of memory that the kernel will
look at later), the startup code is responsible for transferring control to the QNX
Neutrino kernel and process manager (procnto), which perform the final loading
step.
Startup's responsibilities
Let's take a look at the overall responsibilities and flow of the startup code:
If the image isn't in its final destination in RAM, the startup code copies it there. If
the image is compressed, the startup code automatically decompresses the image.
Compression is optional; you can create an image file that isn't compressed, in which
case the startup code won't bother trying to decompress it.
The main task here is to set up the minimum required to be able to determine the
system configuration (and then perform the system configuration).
The details of what needs to be configured during the hardware configuration phase
depend on your particular hardware.
Depending on the nature of the embedded system, you may wish to dynamically
determine the configuration on startup or (in the case of a deeply embedded system)
simply “hardcode” the configuration information.
Regardless of the source of the information, the configuration part of the startup code
needs to store this information into a set of well-defined data structures that the OS
will then look at when it starts. Collectively known as the system page area, these data
structures contain information about:
• memory configuration
• hardware device configuration
• processor type
• time of day
Establishing callouts
To keep the QNX Neutrino kernel as portable as possible (not only to different
processors, but also to different hardware configurations of those processors), a number
of callouts must be supplied by the startup code. Not all of the callouts require that
you write code — we have a library that provides many of these.
The following classes of callout functions can be provided for QNX Neutrino:
• debug interface
• clock/timer interface
• interrupt controller interface
• cache controller interface
• power management
• miscellaneous
The callouts are described in detail in the chapter on Customizing Image Startup
Programs (p. 109).
Starting the OS
The final step that the startup code performs is to start the operating system.
If all of the above sounds like a lot of work, well, it is! Note, however, that we've
provided source code for some common startup programs and have created a library
that performs most of the above functions for you.
If you have one of the many platforms that we support, then you don't have to do any
of this work — we've already done it for you.
To find out what processors and boards we currently support, please refer to the
following sources:
If you have a nonstandard embedded system, you can look at the source for the system
that most closely resembles yours and “clone” the appropriate functionality from the
examples provided.
This issue is discussed in detail in the chapter on Customizing Image Startup Programs
(p. 109).
The third step performed by the software is to start any executables that you want to
be running. The OS does this by reading and processing information stored in the
startup script — a sequence of commands stored within the image. The format of the
startup script, as well as the buildfile that it's part of, is documented in detail in a
variety of places in this guide:
• Making an OS Image (p. 43) chapter — describes the steps required to build a
QNX Neutrino-based system, including discussions of the script file and buildfile.
• Sample Buildfiles appendix in this guide — describes common “tricks” used within
the buildfile and also contains complete examples of sample configurations.
• mkifs doc — describes the mkifs utility, which is used to create the image from
the description passed to it in the buildfile. See the Utilities Reference for details.
• Building OS and Flash Images chapter in the IDE User's Guide — describes the
how the OS and flash images are created in the IDE.
Basically, the OS processes the startup script file, which looks like a shell script. In
the startup script file, you'd specify which executables should be started up (and their
order), the command-line options that they should run with, and so on.
Hardware aspects
From the hardware point of view, the following components form the system:
• processor
• source of initialization and configuration info
• storage media
• I/O devices
Choice of processor
Generally, the simplest development system is one in which you have to do the least
amount of work. If we've already done the work, meaning that the board that you're
using is a standard, supported hardware platform, there's very little work required from
you in this regard; you can instead focus on your software that's going to run on that
board.
If a 3rd party supplies just the BIOS or ROM monitor, then your responsibilities are
increased by having to write the software that starts the operating system. As mentioned
earlier, we call this a “warm-start,” (p. 16) because the system is already “warmed-up”
— various devices are configured and initialized.
If you're supplying a custom IPL, then your responsibilities are further increased by
also having to deal with configuration issues for the hardware. This we call a
“cold-start,” (p. 16) because you are responsible for everything to do with initialization
and configuration.
Choice of filesystems
Once you've sorted out how the system is going to boot, you may still have additional
decisions to make regarding the system's storage capabilities:
• none
• read-only
• read/write nonpersistent
• read/write persistent
Is a
filesystem No
Done
needed?
Yes
Is
write access No procnto image
required? filesystem
Yes
Is
persistent
storage No procnto memory
required? objects
Yes
Will Is a
a rotating No network No Flash
medium filesystem filesystem
be used? used?
Yes Yes
QNX Network
filesystem filesystem
If you don't require any additional storage (i.e. your system is entirely self-contained
and doesn't need to access any other files once it's running), then your work in this
regard is done.
The simplest filesystem scenario is one where read-only access is required. There's
no work for you to do—QNX Neutrino provides this functionality as part of the OS
itself. Simply place the files that you wish to access/execute directly into the image
(see the chapter on Making an OS Image (p. 43)), and the OS will be able to access
them.
If you require write access (perhaps for temporary files, logs, etc.), and the storage
doesn't have to be persistent in nature (meaning that it doesn't need to survive a reset),
then once again the work is done for you.
The QNX Neutrino RTOS allows the RAM in your system to be used as a RAM-disk,
without any additional coding or device drivers. The RAM-disk is implemented via the
Process Manager — you simply set up a Process Manager link (using the ln command).
For example, to mount the /tmp directory as a RAM-disk, execute the following
command:
Or place the following line in your buildfile (we'll talk about buildfiles over the next
few chapters):
[type=link] /tmp=/dev/shmem
This instructs the Process Manager to take requests for any files under /tmp and
resolve them to the shared memory subsystem. For example, /tmp/AAA4533.tmp
becomes a request for /dev/shmem/AAA4533.tmp.
In order to minimize the size of the RAM filesystem code inside the Process
Manager, the shared memory filesystem specifically doesn't include “big
filesystem” features such as file locking and directory creation.
If you do require storage that must survive a power failure or processor reset, then
you'll need to run an additional driver. We supply these classes of filesystems:
• flash filesystems
• rotating disk filesystems
• network filesystems
All of these filesystems require additional drivers. The Sample Buildfiles appendix in
this guide gives detailed examples showing how to set up these filesystem drivers.
The flash driver can interface to the flash memory devices (boot block and regular) in
all combinations of bus widths (8, 16, and 32 bits) and interleave factors (1, 2, and
4).
To find out what flash devices we currently support, please refer to the following
sources:
Using the source code provided, you may be able to tailor one of our filesystems (e.g.
devf-generic) to operate on your particular embedded system (if it isn't currently
supported).
The QNX Neutrino RTOS currently supports several filesystems, including DOS, Linux,
Macintosh HFS and HFS Plus, Windows NT, QNX 4, Power-Safe, Universal Disk Format
(UDF), and more. For details, see the fs-* entries in the Utilities Reference.
Drivers are available for many block-oriented devices. For up-to-date information, see
the devb-* entries in the Utilities Reference as well as the Community area of our
website, www.qnx.com.
In addition to its own transparent distributed processing system (Qnet), The QNX
Neutrino RTOS also supports network filesystems such as CIFS (SMB), NFS 2, and
NFS 3.
Drivers are available for the several Ethernet controllers. For details, see the devn-*
and devnp-* entries in the Utilities Reference as well as the Community area of our
website, www.qnx.com.
I/O devices
Ultimately, your QNX Neutrino-based system will need to communicate with the outside
world. Here are some of the more common ways to do this:
• serial/parallel port
• network (described above)
• data acquisition/generation
• multimedia
For standard serial ports, QNX Neutrino supports several devices (8250 family,
Signetics, etc.) For details, see the devc-* entries in the Utilities Reference, as well
as the Community area of our website, www.qnx.com.
Special/custom devices
One design issue you face is whether you can get off-the-shelf drivers for the hardware
or whether you'll have to write your own. If it turns out that you need to write your
own, then the Writing a Resource Manager guide can help you do that.
Getting started
Depending on the ultimate system you'll be creating, you may have a ton of work to
do or you may have very little. In any case, we recommend that you start with a
supported evaluation board. This approach minimizes the amount of low-level work
that you have to do initially, thereby allowing you to focus on your system rather than
on implementation details.
Start with an evaluation platform that most closely resembles your target platform —
there are many supported evaluation platforms from various vendors.
Once you're comfortable with the development environment and have done a very
rudimentary “proof of concept,” you can move on to such development efforts as
creating your own hardware, writing your own IPL and startup code, writing drivers for
your hardware, and so on.
Once these are addressed, you can then decide on your plan of attack.
Hardware design
There are a number of ways of designing your hardware. We've seen many boards come
in from the field and have documented some of our experiences with them in the
System Design Considerations appendix in this book. You may be able to realize certain
savings (in both cost and time) by reading that appendix first.
Ideally, the system you're designing will look identical to a supported evaluation
platform. In reality, this isn't always the case, so you'll need to customize some of the
components in that system.
We've provided the source code to a large number of the “customizable” pieces of the
OS. This diagram gives you the high-level view of the directory structure for the source
tree we ship:
bsp_working_dir/src/hardware
startupipl flash
Figure 3: The three main branches of the QNX Neutrino source tree.
As you can see, we've divided the source tree into three major branches: ipl,
startup, and flash. Each branch consists of further subdirectories:
bsp_working_dir/src/hardware
The following table relates the source tree branches to the individual chapters in this
book:
For detailed information on the format of the Makefile present in these directories,
see Conventions for Recursive Makefiles and Directories in the QNX Neutrino
Programmer's Guide.
Once you've installed the QNX Neutrino RTOS, you can download processor-specific
Board Support Packages (BSPs) from our website, http://www.qnx.com/. These
BSPs are designed to help you get the QNX Neutrino RTOS running on certain
platforms.
• IPL
• startup
• default buildfile
• networking support
• board-specific device drivers, system managers, utilities, etc.
The BSP is contained in an archive named after the industry-recognized name of the
board and/or reference platform that the BSP supports. BSP packages are available
for QNX Neutrino, Windows, or Linux hosts.
The BSP components are provided in source (p. 34) code form, unless there are
restrictions on the source code, in which case the component is provided only in binary
form. BSPs are provided in a zip archive. The same archive applies to all hosts.
The QNX community website, Foundry27, has more information about BSPs; see
http://community.qnx.com/sf/sfmain/do/viewProject/projects.bsp.
You can also check out BSPs from a Subversion repository on Foundry27.
To use a BSP, you must either unzip the archive and build it on the command line,
or import it into the IDE.
Before working with a BSP in the IDE, you must first import it. When you import the
BSP source, the IDE creates a System Builder project.
If you answer Yes, the IDE will start the build process. If you decide to build at a
later time, you can do a Rebuild All from the main Project menu when you're ready
to build.
When you import a QNX BSP, the IDE opens the QNX BSP Perspective. This
perspective combines the minimum elements from the C\C++ Development
Perspective and the System Builder Perspective.
For more information, see the IDE User's Guide in your documentation
set.
If you aren't using the IDE and you want to manually install a BSP archive, we
recommend that you create a default directory with the same name as your BSP and
unzip the archive from there:
1. Change the directory to where you want to extract the BSP (e.g. /home/joe). The
archive will extract to the current directory, so you should create a directory
specifically for your BSP.
For example:
mkdir /home/joe/bspname
cd /home/joe/bspname
unzip bspname.zip
Each BSP is rooted in whatever directory you copy it to. If you type make within this
directory, you'll generate all of the buildable entities within that BSP no matter where
you move the directory.
When you build a BSP, everything it needs, aside from standard system headers, is
pulled in from within its own directory. Nothing that's built is installed outside of the
BSP's directory. The makefiles shipped with the BSPs copy the contents of the
prebuilt directory into the install directory. The binaries are built from the source
using include files and link libraries in the install directory.
Structure of a BSP
After you unzip a BSP archive, the resulting directory structure looks something like
this:
bsp_working_dir
imagesprebuiltinstallsrc
bin devc
include devn
lib flash
sbin ipl
startup
In our documentation, we refer to the directory where you've installed a BSP (e.g.
/home/myID/my_BSPs/integrator) as the bsp_working_dir. This directory includes
the following subdirectories:
• src
• prebuilt
• install
• images
The images subdirectory is where the resultant boot images are placed. It contains
(as a minimum) the Makefile needed to build the image(s). Other files that could
reside in this directory include:
prebuilt subdirectory
The prebuilt subdirectory contains prebuilt binaries, and header files that are
shipped with the BSP.
Before the BSP is built, all of the files from the prebuilt directory are copied into
the install directory, maintaining the path structure.
In order to handle dependencies, the libraries, headers, and other files found in the
./prebuilt directory need to be copied correctly to your ./install directory. To
do this, you'll need to run make at the bsp_working_dir directory level.
The “root” of the prebuilt directory requires the same structure as the system root.
The target-specific and usr directories mirror the structure of /.
All processor-specific binaries are located under the directory named for that
processor type.
prebuilt
eth.h util.ahnic.h
mdi.h platform.h
support.h types.h
...
install subdirectory
The install directory gets populated at the beginning of the BSP build process. All
the files in the prebuilt directory are copied, then all generated binaries are installed
here as they're compiled. The files stored in the install directory are taken first
when mkifs executes.
Before you make any components for your particular board, you must first make the
BSP sources at the top level:
cd bsp_working_dir
make
This builds everything under ./src and sets up the ./install and ./images
subdirectories correctly.
After this initial build is complete, you can build any of the source files individually.
src subdirectory
The BSP-specific source code is stored in this directory. Refer to the BSP release
notes to find the location of the source code for a specific driver.
The hardware directory contains separate directories for character, flash, and network
drivers, IPL, startup code, and so on, depending on the BSP.
The lib directory contains separate directories for libraries that are required by driver
and other utilities that are included with the BSP.
Some drivers, such as the network drivers or USB host controller drivers, are
implemented as shared objects, but the source code for them is located under
the hardware directory.
The utils directory contains separate directories for minor utilities that are required
on the board. Some hardware-specific utilities can also be found in
hardware/support.
The services directory contains separate directories for additional services that
aren't included in the base installation.
In order to build a BSP from the command line, you must go to the root directory for
the BSP.
Use the make command to build the source code. The Makefile defines the following
targets:
all
prebuilt
install
Invokes the prebuilt target, and then performs the following in the src
directory:
• make hinstall to copy all public headers from src into the install
directory.
• make install to build all binaries in src and copy the results into
the install directory. This target also copies the buildfile from
src/hardware/startup/boards/board/build and renames it
board.build.
links
images
Changes to the images directory and runs the Makefile there. This
Makefile creates an IFS file based on the buildfile linked in during the
make links target. Any extra work required (e.g. IPL padding, conversion
to an alternate format) is also handled from within this Makefile.
We recommend that you use make to build the OS image. If you use mkifs
directly, you need to use the -r option to specify where to find the binaries.
For more information, see the entry for mkifs in the Utilities Reference.
All boards have some devices, whether they're input, serial, flash, or PCI. Every BSP
includes a buildfile that you can use to generate an OS image that will run on the
board it was written for. The buildfile is in the
bsp_working_dir/src/hardware/startup/boards/board directory.
A BSP's buildfile contains the commands — possibly commented out — for starting
the drivers associated with the devices. You will need to edit the buildfile to modify
or uncomment these commands. If you uncomment a command, make sure you
uncomment the lines that add any required binaries to the image.
For more information, see the documentation for each BSP, as well as the buildfile
itself; for general information about buildfiles, see the entry for mkifs in the Utilities
Reference.
Once you've modified the buildfile, follow the instructions given earlier in this chapter
for building an OS image.
The IDE lets you communicate with your target and download your OS image using
either a serial connection, or a network connection using the Trivial File Transfer
Protocol (TFTP). If your board doesn't have a ROM monitor, you probably can't use
the download services in the IDE; you'll have to get the image onto the board some
other way (e.g. JTAG).
Transferring an OS image
Burn both the IPL and the OS image into IPL and OS
the flash boot ROM, then boot entirely
from flash
Burn an IPL (Initial Program Loader) into IPL and boot ROM
the flash boot ROM, then load the OS
image serially
The method you use to transfer an OS image depends on what comes with the board.
The BSP contains information describing the method that you can use for each
particular board. Each board will have all or some of these options for you to use.
1. Connect your target and host machine with a serial cable. Ensure that both machines
properly recognize the connection.
2. Specify the device (e.g.COM1) and the communications settings (e.g. the baud
rate, parity, data bits, stop bits, and flow control) to match your target machine's
capabilities. You can now interact with your target by typing in the view.
1. Using either the serial terminal view or another method (outside the IDE), configure
your target so that it's ready to receive an image.
2. In the serial terminal view, click Send File.
3. In the Select File to Send dialog, enter the name or your file (or click Browse).
4. Select a protocol (e.g. sendnto).
5. Click OK. The Builder transmits your file over the serial connection.
The flash filesystem drivers implement a POSIX-like filesystem on NOR flash memory
devices. The flash filesystem drivers are standalone executables that contain both the
flash filesystem code and the flash device code. There are versions of the flash
filesystem driver for different embedded systems hardware as well as PCMCIA memory
cards.
The naming convention for the drivers is devf-system, where system describes the
embedded system.
To find out what flash devices we currently support, please refer to the following
sources:
The flash filesystem drivers support one or more logical flash drives. Each logical drive
is called a socket, which consists of a contiguous and homogeneous region of flash
memory. For example, in a system containing two different types of flash device at
different addresses, where one flash device is used for the boot image and the other
for the flash filesystem, each flash device would appear in a different socket.
Each socket may be divided into one or more partitions. Two types of partitions are
supported:
• raw partitions
• flash filesystem partitions
Raw partitions
A raw partition in the socket is any partition that doesn't contain a flash filesystem.
The flash filesystem driver doesn't recognize any filesystem types other than the flash
filesystem. A raw partition may contain an image filesystem or some application-specific
data.
The flash filesystem uses a raw mountpoint to provide access to any partitions on the
flash that aren't flash filesystem partitions. Note that the flash filesystem partitions
are available as raw partitions as well.
A flash filesystem partition contains the POSIX-like flash filesystem, which uses a
QNX-proprietary format to store the filesystem data on the flash devices. This format
isn't compatible with either the Microsoft FFS2 or PCMCIA FTL specification.
The flash filesystem allows files and directories to be freely created and deleted. It
recovers space from deleted files using a reclaim mechanism similar to garbage
collection.
The flash filesystem supports all the standard POSIX utilities such as ls, mkdir, rm,
ln, mv, and cp. There are also some QNX Neutrino utilities for managing the flash
filesystem:
flashctl
deflate
mkefs
The flash filesystem supports all the standard POSIX I/O functions such as open(),
close(), read(), and write(). Special functions such as erasing are supported using the
devctl() function.
Each BSP contains the binary and the source code for the appropriate flash filesystem
driver, but the QNX Neutrino RTOS contains the associated header files and libraries.
Typing make in the bsp_working_dir generates the flash filesystem binary. Normally,
you won't need to remake the flash filesystem driver unless you've changed the size
or configuration of the flash on the board — this can include the number of parts,
size of parts, type of parts, interleave, etc.
Regardless of which BSP you're working with, the procedure requires that you:
For example, this is what you might have to do for a board than can be booted from
DMON or flash:
1. To boot from DMON, enter the following command to start the flash filesystem
driver:
2. To boot from flash, enter the following command to start the flash system driver:
devf-generic -s0x0,32M
3. To prepare the area for the partition, you must erase the entire flash. Enter the
following command:
flashctl -p/dev/fs0p0 -f
slay devf-generic
devf-generic &
Entry Description
You can test QNX Neutrino simply by executing any shell builtin command or any
command residing within the OS image. For example, type:
ls
You'll see a directory listing, since the ls command has been provided in the default
system image.
Now that you have a better understanding of how BSPs work in an embedded system,
you'll want to start working on your applications. The following table contains references
to the QNX Neutrino documentation that may help you find the information you'll need
to get going.
Filename conventions
In QNX Neutrino BSPs, we use the following conventions for naming files:
In this chapter, we'll take a look at the steps necessary to build an OS image. Then
we'll examine the steps required to get that image to the target, whether it involves
creating a boot disk/floppy, a network boot, or burning the image into an EPROM or
flash device. We'll also discuss how to put together some sample systems to show you
how to use the various drivers and resource managers that we supply.
For more information on using the various utilities described in this chapter, see the
Utilities Reference.
In the embedded QNX Neutrino world, an “image” can mean any of the following:
What is an OS image?
When you've created your executables (programs) that you want your embedded system
to run, you need to place them somewhere where they can be loaded from. An OS
image is simply a file that contains the OS, your executables, and any data files that
might be related to your programs. Actually, you can think of the image as a small
“filesystem” — it has a directory structure and some files in it.
An image can be bootable or nonbootable. A bootable image is one that contains the
startup code that the IPL can transfer control to (see the chapter on customizing IPL
programs (p. 77) in this book). Generally, a small embedded system will have only the
one (bootable) OS image.
# cd /proc/boot
# ls
.script ping cat data1 pidin
The above example actually demonstrates two aspects of having the OS image function
as a filesystem. When we issued the ls command, the OS loaded ls from the image
filesystem (pathname /proc/boot/ls). Then, when we issued the cat command,
the OS loaded cat from the image filesystem as well, and opened the file data1.
Let's now take a look at how we configure the image to contain files.
Configuring an OS image
The OS image is created by a program called mkifs (make image filesystem), which
accepts information from two main sources: its command line and a buildfile.
A simple buildfile
Let's look at a very simple buildfile, the one that generated the OS image used in the
example above:
# A simple "ls", "ping", and shell.
# This file is "shell.bld"
[virtual=armle-v7,srec] .bootstrap = {
startup-abc123
PATH=/proc/boot procnto -vv
}
[+script] .script = {
procmgr_symlink ../../proc/boot/libc.so.3 /usr/lib/ldqnx.so.2
[type=link] /dev/console=/dev/ser1
[type=link] /tmp=/dev/shmem
libc.so.2
libc.so
[data=copy]
devc-ser8250-abc123
ksh
ls
cat
data1
ping
ftp
pidin
Inline files
Although the three sections in the buildfile above seem to be distinct, in reality all
three are similar in that they're lists of files.
[virtual=armle-v7,srec] .bootstrap = {
The rest of the line specifies an inline file (as indicated by the open brace) named
“.bootstrap”, which consists of the following:
startup-abc123
PATH=/proc/boot procnto -vv
If you set the value of PATH in the bootstrap file, procnto sets the _CS_PATH
configuration string. Similarily, if you set LD_LIBRARY_PATH, procnto sets
the _CS_LIBPATH configuration string. It doesn't pass these environment
variables on to the script, but you can set environment variables in the script
itself.
You can bind in optional modules to procnto by using the [module=...] attribute.
For example, to bind in the adaptive partitioning scheduler, change the procnto line
to this:
The actual name of the bootstrap file is irrelevant. However, nowhere else in the
buildfile did we specify the bootstrap or script files—they're included automatically
when specified by a [virtual] or [physical] attribute.
The “virtual” attribute (and its sibling the “physical” attribute) specifies the
target processor (in our example, the armle-v7 part) and the bootfile (the srec
part), a very small amount of code between the IPL and startup programs. The target
processor is put into the environment variable $PROCESSOR and is used during
pathname expansion. You can omit the target processor specification, in which case
it defaults to the same as the host processor. For example:
[virtual=bios] .bootstrap = {
...
While we're looking at the bootstrap specification, it's worth mentioning that you can
apply the +compress attribute to compress the entire image. The image is
automatically uncompressed before being started. Here's what the first line would look
like:
The second section of the buildfile starts with the [+script] attribute — this tells
mkifs that the specified file is a script file, a sequence of commands that you want
procnto to execute when it's completed its own startup.
Script files look just like regular shell scripts, except that:
In order to run a command, its executable must be available when the script
is executed. You can add the executable to the image or get it from a filesystem
that's started before the executable is required. The latter approach results in
a smaller image.
In this case, the script file is an inline file (again indicated by the open brace). The
file (which happens to be called “.script”) contains the following:
Next the script starts a serial driver (the fictional devc-ser8250-abc123) in edited
mode with hardware flow control disabled at a baud rate of 115200 bps at a particular
physical memory address. The script then does a reopen to redirect standard input,
output, and error. The last line simply displays a message.
As mentioned above, the bootstrap file can set the _CS_PATH and _CS_LIBPATH
configuration strings. You can set PATH, LD_LIBRARY_PATH, and other environment
variables if the programs in your script need them.
You can specify which CPU to bind processes to when launching processes from the
startup script through the [CPU=] modifier.
The [CPU=] is used as any other modifier, and specifies the CPU on which to launch
the following process (or, if the attribute is used alone on a line without a command,
sets the default CPU for all following processes). Specify the CPU as a zero-based
processor number:
[cpu=0] my_program
[cpu=*] my_program
At boot time, if there isn't a processor with the given index, a warning message is
displayed, and the command is launched without any runmask restriction.
Due to a limitation in the boot image records, this syntax allows only the
specification of a single CPU and not a more generic runmask. Use the on
utility to spawn a process within a fully specified runmask.
The script file stored on the target isn't the same as the original specification of the
script file within the buildfile. That's because a script file is “special” — mkifs parses
the text commands in the script file and stores only the parsed output on the target,
not the original ASCII text. The reason we did this was to minimize the work that the
process manager has to do at runtime when it starts up and processes the script file
— we didn't want to have to include a complete shell interpreter within the process
manager!
Let's return to our example. Notice the “list of files” (i.e. from “[type=link]
/dev/console=/dev/ser1” to “pidin”).
As mentioned earlier, this list specifies the files and libraries that you want to include
in the image. What you include in the image depends on what you want your embedded
system to do; for some tips on choosing these files, see the Sample Buildfiles appendix.
In the example above, we specified that the files at the end were to be part of the
image, and mkifs somehow magically found them. Actually, it's not magic — mkifs
simply looked for the environment variable MKIFS_PATH. This environment variable
contains a list of places to look for the files specified in the buildfile. If the environment
variable doesn't exist, then the following are searched in this order:
1. current working directory if the filename contains a slash (but doesn't start with
one).
2. ${QNX_TARGET}/${PROCESSOR}/sbin
3. ${QNX_TARGET}/${PROCESSOR}/usr/sbin
4. ${QNX_TARGET}/${PROCESSOR}/boot/sys
5. ${QNX_TARGET}/${PROCESSOR}/bin
6. ${QNX_TARGET}/${PROCESSOR}/usr/bin
7. ${QNX_TARGET}/${PROCESSOR}/lib
8. ${QNX_TARGET}/${PROCESSOR}/lib/dll
9. ${QNX_TARGET}/${PROCESSOR}/usr/lib
(The ${PROCESSOR} component is replaced with the name of the CPU, e.g. arm.)
Since none of the filenames that we used in our example starts with the “/” character,
we're telling mkifs that it should search for files (on the host) within the path list
specified by the MKIFS_PATH environment variable as described above. Regardless
of where the files came from on the host, in our example they'll all be placed on the
target under the /proc/boot directory (there are a few subtleties with this, which
we'll come back to).
For our example, devc-con will appear on the target as the file
/proc/boot/devc-con, even though it may have come from the host as
${QNX_TARGET}/armle-v7/sbin/devc-con.
To include files from locations other than those specified in the MKIFS_PATH
environment variable, you have a number of options:
• Change the MKIFS_PATH environment variable (use the shell command export
MKIFS_PATH=newpath on the host).
• Modify the search path with the [search=] attribute.
• Specify the pathname explicitly (i.e. with a leading “/” character).
• Create the contents of the file in line.
[search=${MKIFS_PATH}:/mystuff]
Let's assume that one of the files used in the example is actually stored on your
development system as /release/data1. If you simply put /release/data1 in
the buildfile, mkifs would include the file in the image, but would call it
/proc/boot/data1 on the target system, instead of /release/data1.
Sometimes this is exactly what you want. But at other times you may want to specify
the exact pathname on the target (i.e. you may wish to override the prefix of
/proc/boot). For example, specifying /etc/passwd would place the host
filesystem's /etc/passwd file in the target's pathname space as
/proc/boot/passwd — most likely not what you intended. To get around this, you
could specify:
/etc/passwd = /etc/passwd
This tells mkifs that the file /etc/passwd on the host should be stored as
/etc/passwd on the target.
On the other hand, you may in fact want a different source file (let's say
/home/joe/embedded/passwd) to be the password file for the embedded system.
In that case, you would specify:
/etc/passwd = /home/joe/embedded/passwd
For our tiny data1 file, we could just as easily have included it in line — that is to
say, we could have specified its contents directly in the buildfile itself, without the
need to have a real data1 file reside somewhere on the host's filesystem. To include
the contents in line, we would have specified:
data1 = {
This is a data file, called data1, contained in the image.
Note that this is a convenient way of associating data
files with your programs.
}
A few notes. If your inline file contains the closing brace (“}”), then you must escape
that closing brace with a backslash (“\”). This also means that all backslashes must
be escaped as well. To have an inline file that contains the following:
you would have to specify this file (let's call it data2) as follows:
data2 = {
This includes a {, a \}, and a \\ character.
}
Note that since we didn't want the data2 file to contain leading spaces, we didn't
supply any in the inline definition. The following, while perhaps “better looking,”
would be incorrect:
If the filename that you're specifying has “weird” characters in it, then you must quote
the name with double quote characters ("). For example, to create a file called I
"think" so (note the spaces and quotation marks), you would have to specify it as
follows:
But naming files like this is discouraged, since the filenames are somewhat awkward
to type from a command line (not to mention that they look goofy).
The files that we included (in the example above) had the owner, group ID, and
permissions fields set to whatever they were set to on the host filesystem they came
from. The inline files (data1 and data2) got the user ID and group ID fields from
the user who ran the mkifs program. The permissions are set according to the user's
umask.
If we wanted to explicitly set these fields on particular files within the buildfile, we
would prefix the filenames with an attribute:
This marks the first file (file1) as being owned by root (the user ID 0), group zero,
and readable and writable by all (the mode of octal 666). The second file (file2) is
marked as being owned by user ID 5, group ID 1, and executable and readable by all
(the a+xr permissions).
When running on a Windows host, mkifs can't get the execute (x), setuid
(“set user ID”), or setgid (“set group ID”) permissions from the file. Use the
perms attribute to specify these permissions explicitly. You might also have
to use the uid and gid attributes to set the ownership correctly. To determine
whether or not a utility needs to have the setuid or setgid permission set, see
its entry in the Utilities Reference.
Notice how when we combine attributes, we place all of the attributes within one
open-square/close-square set. The following is incorrect:
If we wanted to set these fields for a bunch of files, the easiest way to do that would
be to specify the uid, gid, and perms attributes on a single line, followed by the
list of files:
/release_1.0
This would put all the files that reside under /release_1.0 into /proc/boot on
the target. If there were subdirectories under /release_1.0, then they too would
be created under /proc/boot, and all the files in those subdirectories would also
be included in the target.
Again, this may or may not be what you intend. If you really want the /release_1.0
files to be placed under /, you would specify:
/=/release_1.0
This tells mkifs that it should grab everything from the /release_1.0 directory
and put it into a directory called /. As another example, if we wanted everything in
the host's /release_1.0 directory to live under /product on the target, we would
specify:
/product=/release_1.0
To generate the image file from our sample buildfile, you could execute the command:
This tells mkifs to use the buildfile shell.bld to create the image file shell.ifs.
You can also specify command-line options to mkifs. Since these command-line
options are interpreted before the actual buildfile, you can add lines before the buildfile.
You would do this if you wanted to use a makefile to change the defaults of a generic
buildfile.
The following sample changes the address at which the image starts to 64 KB (hex
0x10000):
If you'd like to see the contents of an image, you can use the dumpifs utility. The
output from dumpifs might look something like this:
Offset Size Name
0 100 Startup-header flags1=0x1 flags2=0 paddr_bias=0x80000000
100 a008 startup.*
a108 5c Image-header mountpoint=/
a164 264 Image-directory
---- ---- Root-dirent
---- 12 usr/lib/ldqnx.so.2 -> /proc/boot/libc.so
---- 9 dev/console -> /dev/ser1
a3c8 80 proc/boot/.script
b000 4a000 proc/boot/procnto
55000 59000 proc/boot/libc.so.3
---- 9 proc/boot/libc.so -> libc.so.3
ae000 7340 proc/boot/devc-ser8250
b6000 4050 proc/boot/esh
bb000 4a80 proc/boot/ls
c0000 14fe0 proc/boot/data1
d5000 22a0 proc/boot/data2
Checksums: image=0x94b0d37b startup=0xa3aeaf2
The more -v (“verbose”) options you specify to dumpifs, the more data you'll see.
For more information on dumpifs, see its entry in the Utilities Reference.
If your application requires a writable filesystem and you have flash memory devices
in your embedded system, then you can use a QNX Neutrino flash filesystem driver
to provide a POSIX-compatible filesystem. The flash filesystem drivers are described
in the Filesystems chapter of the System Architecture guide. The chapter on
customizing the flash filesystem (p. 179) in this book describes how you can build a
flash filesystem driver for your embedded system.
• Create a flash filesystem image file on the host system and then write the image
into the flash on the target.
• Run the flash filesystem driver for your target system, and then copy files into the
flash filesystem on the target.
In this section we describe how to create a flash filesystem image file using the mkefs
(for make embedded filesystem) utility and a buildfile. How to transfer the flash
filesystem image onto your target system is described in the “Embedding an image”
(p. 62) section. For details on how to use the flash filesystem drivers, see the Utilities
Reference.
Using mkefs
The mkefs utility takes a buildfile and produces a flash filesystem image file. The
buildfile is a list of attributes and files to include in the filesystem.
mkefs buildfile
The syntax of the buildfile is similar to that for mkifs, but mkefs supports a different
set of attributes, including the following:
block_size=bsize
Specifies the block size of the flash device being used; defaults to 64 KB.
We'll talk about interleave considerations for flash devices below.
max_size=msize
Specifies the maximum size of the flash device; is used to check for
overflows. The default is 4 Gbytes.
spare_blocks=sblocks
Specifies the number of spare blocks to set aside for the flash filesystem;
see “Spare blocks (p. 59),” below.
min_size=tsize
Refer to the Utilities Reference for a complete description of the buildfile syntax and
attributes supported by mkefs.
In this example, the attributes specify that the flash devices have a block size of 128
KB, that there should be one spare block, and that all the files should be processed
using the deflate utility, which compresses the files. A single directory is given.
Just as with mkifs, when we specify a directory, all files and subdirectories beneath
it are included in the resulting image. Most of the other filename tricks shown above
for mkifs also apply to mkefs.
Block size
The value you should specify for the block_size attribute depends on the physical
block size of the flash device given in the manufacturer's data sheet and on how the
flash device is configured in your hardware (specifically the interleave).
Notice that you don't have to specify any details (other than the block size) about the
actual flash devices used in your system.
Spare blocks
The spare_blocks attribute indicates how many blocks should be left as spare. A
value of 0 implies a “read/write” (or “write-once”) flash filesystem, whereas a value
greater than 0 implies a “read/write/reclaim” filesystem.
The default is 1, but the number of spare blocks you'll need depends on the amount
of writing you'll do. You should specify an odd number of spare blocks, usually 1 or
3.
The filesystem doesn't use a spare block until it's time to perform a reclaim operation.
A nonspare block is then selected for “reclamation”, and the data contained in that
block is coalesced into one contiguous region in the spare block. The nonspare block
is then erased and becomes the new spare block. The former spare block takes the
place of the reclaimed block.
If you don't set aside at least one spare block (i.e. the spare_blocks attribute
is 0), then the flash filesystem driver won't be able to reclaim space — it won't
have any place to put the new copy of the data. The filesystem will eventually
fill up since there's no way to reclaim space.
Compressing files
The file compression mechanism provided with our flash filesystem is a convenient
way to cut flash memory costs for customers. The flash filesystem uses popular
deflate/inflate algorithms for fast and efficient compression/decompression.
You can use the deflate utility to compress files in the flash filesystem, either from
a shell or as the filter attribute to mkefs. The deflate algorithm provides excellent
lossless compression of data and executable files.
The flash filesystem drivers use the inflator utility to transparently decompress
files that have been compressed with deflate, which means that you can access
compressed files in the flash filesystem without having to decompress them first.
Compressing files can result in significant space savings. But there's a trade-off:
it takes longer to access compressed files. Always consider the slowdown of
compressed data access and increased CPU usage when designing a system.
We've seen systems with restricted flash budget increase their boot time by
large factors when using compression.
The first method is the high-runner case. You can use the deflate utility as a filter
for mkefs to compress the files that get built into the flash filesystem. For example,
you could use this buildfile to create a 16-megabyte filesystem with compression:
You can also precompress the files by using deflate directly. If mkefs detects a
compression signature in a file that it's putting into the filesystem, it knows that the
file is precompressed, and so it doesn't compress the file again. In either case, mkefs
puts the data on the flash filesystem and sets a simple bit in the metadata that tells
the flash filesystem that the file needs to be decompressed.
The second method is to use deflate to compress files and store them directly in
the flash filesystem. For example, here's how to use deflate at the command line
to compress the ls file from the image filesystem into a flash filesystem:
Abstraction layer
The flash filesystem never compresses any files. It detects compressed files on the
media and uses inflator to decompress them as they're accessed. An abstraction
layer in inflator achieves efficiency and preserves POSIX compliance. Special
compressed data headers on top of the flash files provide fast seek times.
This layering is quite straightforward. Specific I/O functions include handling the three
basic access calls for compressed files:
• read()
• lseek()
• lstat()
Two sizes
This is where compression gets tricky. A compressed file has two sizes:
Virtual size
This is, for the end user, the real size of the decompressed data, such as
stat() would report.
Media size
For instance, running the disk usage utility du would be practically meaningless under
a flash directory with data that is decompressed on the fly. It wouldn't reflect flash
media usage at all.
As a convenience, inflator supports a naming convention that lets you access the
compressed file: simply add .~~~ (a period and three tildes) to the file name. If you
use this extension, the file isn't decompressed, so read operations yield raw compressed
data instead of the decompressed data. For example, to get the virtual size of a
compressed file, type:
ls -l my_file
ls -l my_file.~~~
Compression rules
If you read a file with the .~~~ extension, the file isn't decompressed for you, as it
would be normally. Now this is where we start talking about rules. All this reading and
getting the size of files is fairly simple; things get ugly when it's time to write those
files.
• You can't write all over the place! Although the flash filesystem supports random
writes in uncompressed files, the same isn't true for compressed files.
• Compressed files are read-only; you can replace a compressed file, but you can't
modify it in place.
• The flash filesystem never transparently compresses any data.
• If compressed data needs to be put on the flash during the life of a product, this
data has to be precompressed.
The exception
So those are the rules, and here is the exception: truncation. If a compressed file is
opened with O_TRUNC from the regular virtual namespace, the file status will become
just as if it were created from this namespace. This gives you full POSIX capabilities
and no compression with accompanying restrictions.
By the way, the ftruncate() functionality isn't provided with compressed files, but is
supported with regular files.
Embedding an image
After you've created your bootable OS image on the host system, you'll want to transfer
it to the target system so that you can boot the QNX Neutrino RTOS on the target. The
various ways of booting the OS on a target system are described in the chapter on
customizing IPL programs (p. 77) in this guide.
If you're booting the OS from flash, then you'll want to write the image into the flash
devices on the target. The same applies if you have a flash filesystem image — you'll
want to write the image into flash on the target.
Use mkifs
to create Neutrino
image
No
Are
flash
filesystem image
Yes Use mkimage
and Neutrino image
to combine images
to go in the same
flash
device?
No
Done
Figure 7: Flash configuration options for your QNX Neutrino-based embedded systems.
Depending on your requirements and the configuration of your target system, you may
want to embed:
• the IPL
• the boot image
• the boot image and other image filesystem
• the boot image and flash filesystem
• some other combination of the above.
Also, you may wish to write the boot image and the flash filesystem on the same flash
device or different devices. If you want to write the boot image and the flash filesystem
on the same device, then you can use the mkimage utility to combine the image files
into a single image file.
During the initial development stages, you'll probably need to write the image into
flash using a programmer or a download utility. Later on if you have a flash filesystem
running on your target, you can then write the image file into a raw flash partition.
If your programmer requires the image file to be in some format other than binary,
then you can use the mkrec utility to convert the image file format.
The mkimage utility combines multiple input image files into a single output image
file. It recognizes which of the image files contains the boot image and will place this
image at the start. Note that instead of using mkimage, some developers rely on a
flash programmer to burn the separate images with appropriate alignment.
For example:
will take the nto.ifs and fs.ifs image files and output them to the flash.ifs
file.
If you want more control over how the image files are combined, you can use other
utilities, such as:
• cat
• dd
• mkrec
• objcopy
You'll use the System Builder to generate OS images for your target board's RAM or
flash. You can create:
• an OS image
• a Flash image
• a combined image.
For more information about this process, please see the documentation that comes
with the QNX Momentics IDE.
The mkrec utility takes a binary image file and converts it to either Motorola S records
or Intel hex records, suitable for a flash or EPROM programmer.
For example:
will convert the image file flash.ifs to an S-record format file called flash.srec.
The -s 256k option specifies that the EPROM device is 256 KB in size.
If you have multiple image files that you wish to download, then you can first use
mkimage to combine the image files into a single file before downloading. Or, your
flash/EPROM programmer may allow you to download multiple image files at different
offsets.
There are many ways to transfer your image into your flash:
The details on how to transfer the image with anything other than the last method is
beyond the scope of this document. Using the raw mountpoint is a convenient way
that comes bundled with your flash filesystem library. You can actually read and write
raw partitions just like regular files, except that when the raw mountpoint is involved,
remember to:
For the sake of this discussion, we can use the devf-ram driver. This driver simulates
flash using regular memory. To start it, log in as root and type:
# devf-ram &
You can use the flashctl command to erase a partition. You don't need to be root
to do this. For instance:
$ flashctl -p /dev/fs0 -e
Be careful when you use this command. Make sure you aren't erasing
something important on your flash — like your BIOS!
On normal flash, the flashctl command on a raw partition should take a while
(about one second for each erase block). This command erases the /dev/fs0 raw
flash array. Try the hd command on this newly erased flash array; everything should
be 0xFF:
$ hd /dev/fs0
0000000: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
*
Of course, this IPL won't work for real — it's just for trying out the flash filesystem.
In any event, an IPL wouldn't be very useful in RAM. Let's make a dummy flash
filesystem for the purpose of this example (the ^D means Ctrl–D):
$ mkefs -v - flash_image
[block_size=128k spare_blocks=1 min_size=384k]
/bin/ls
/bin/cat
^D
writing directory entry ->
writing file entry -> ls **
writing file entry -> cat *
Filesystem size = 384K
block size = 128K
1 spare block(s)
This flash filesystem actually works (unlike the IPL). Now, the flash partition images
can be transferred to the flash using any file-transfer utility (such as cp or ftp). We
have an IPL image created with mkrec (and properly padded to an erase block
boundary) and a flash image created with mkefs, so we can use cat to combine and
transfer both images to the flash:
If you use the hd utility on the raw mountpoint again, you'll see that your flash that
had initially all bits set to ones (0xFF) now contains your partition images. To use the
flash filesystem partition, you need to slay the driver and start it again so it can
recognize the partitions and mount them. For instance, with devf-ram:
$ slay devf-ram
$ devf-ram &
From this point, you have a /fs0p1 mountpoint that's in fact a directory and contains
the files you specified with mkefs to create your flash image. There's no /fs0p0,
because the boot image isn't recognized by the flash filesystem. It's still accessible
as a raw mountpoint via /dev/fs0p0. You can do the same operations on
/dev/fs0p0 that you could do with /dev/fs0. Even /dev/fs0p1 is accessible,
but be careful not to write to this partition while applications are using the flash
filesystem at /fs0p1. Try:
$ /fs0p1/ls /fs0p1
You've just executed ls from your flash filesystem and you've listed its contents. To
conclude, let's say that what we did in this example is a good starting point for when
you customize the flash filesystem to your own platforms. These baby steps should be
the first steps to using a full-blown filesystem on your target.
System configuration
In this section, we'll look at some of the ways you can configure QNX Neutrino systems.
Refer to the Sample Buildfiles appendix in this guide for more detailed examples.
What you want to do will, of course, depend on the type of system you're building. Our
purpose in this section is to offer some general guidelines and to help clarify which
executables should be used in which circumstances, as well as which shared libraries
are required for their respective executables.
One of the very first things to do in a buildfile is to start a driver that you then redirect
standard input, output, and error to. This allows all subsequent drivers and applications
to output their startup messages and any diagnostics messages they may emit to a
known place where you can examine the output.
Generally, you'd start either the console driver or a serial port driver. The console driver
is used when you're developing on a fairly complete “desktop” type of environment;
the serial driver is suitable for most “embedded” environments.
But you may not even have any such devices in your deeply embedded system, in
which case you would omit this step. Or you may have other types of devices that you
can use as your output device, in which case you may require a specialized driver (that
you supply). If you don't specify a driver, output will go to the debug output driver
provided by the startup code.
This example starts the standard console driver in edited mode (the -e option, which
is the default). To set up the output device, you would include the driver in your startup
script (the [+script] file). For example:
devc-con -e &
reopen /dev/con1
The following starts the 8250 serial port driver in edited mode (the -e option), with
an initial baud rate of 115200 baud (the -b option):
In both cases, the reopen command causes standard input, output, and error to be
redirected to the specified pathname (either /dev/con1 or /dev/ser1 in the above
examples). This redirection holds until otherwise specified with another reopen
command.
The reopen used above is a mkifs internal command, not the shell builtin
command of the same name.
Running drivers/filesystems
The next thing you'll want to run are the drivers and/or filesystems that will give you
access to the hardware. Note that the console or serial port that we installed in the
previous section is actually an example of such a driver, but it was a special case in
that it should generally be the first one.
Which one you install first is generally driven by where your executables reside. One
of the goals for the image is to keep it small. This means that you generally don't put
all the executables and shared libraries you plan to load directly into the image —
instead, you place those files into some other medium (whether a flash filesystem,
rotating disk, or a network filesystem). In that case, you should start the appropriate
driver to get access to your executables. Once you have access to your executables on
some medium, you would then start other drivers from that medium.
The alternative, which is often found in deeply embedded systems, is to put all the
executables and shared libraries directly into the image. You might want to do this if
there's no secondary storage medium or if you wanted to have everything available
immediately, without the need to start a driver.
Let's examine the steps required to start the disk, flash, and network drivers. All these
drivers share a common feature: they rely on one process that loads one or more .so
files, with the particular .so files selected either via the command line of the process
or via automatic configuration detection.
Since the various drivers we're discussing here use .so files (not just their
own driver-specific ones, but also standard ones like the C library), these .so
files must be present before the driver starts. Obviously, this means that the
.so file cannot be on the same medium as the one you're trying to start the
driver for! We recommend that you put these .so files into the image
filesystem.
Disk drivers
The first thing you need to determine is which hardware you have controlling the disk
interface. We support a number of interfaces, including various flavors of SCSI
controllers and the EIDE controller. For details on the supported interface controllers,
see the various devb-* entries in the Utilities Reference.
The only action required in your buildfile is to start the driver (e.g. devb-aha7). The
driver will then dynamically load the appropriate modules (in this order):
The CAM .so files are documented under cam-* in the Utilities Reference. Currently,
we support CD-ROMs (cam-cdrom.so), hard disks (cam-disk.so), and optical
disks (cam-optical.so).
The fs-* modules are responsible for providing the high-level knowledge about how
a particular filesystem is structured. We currently support the following:
Filesystem Module
MS-DOS fs-dos.so
Linux fs-ext2.so
Windows NT fs-nt.so
QNX 4 fs-qnx4.so
Power-Safe fs-qnx6.so
Flash filesystems
To run a flash filesystem, you need to select the appropriate flash driver for your target
system. For details on the supported flash drivers, see the various devf-* entries in
the Utilities Reference.
The flash filesystem drivers don't rely on any flash-specific .so files, so the only
module required is the standard C library (libc.so).
Since the flash filesystem drivers are written for specific target systems, you can
usually start them without command-line options; they'll find the flash for the specific
system they were written for.
Network drivers
Network services are started from the io-pkt* command, which is responsible for
loading in the required .so files.
For dynamic control of network drivers, you can simply use mount and umount
to start and stop drivers at the command line. For example:
Two levels of .so files are started, based on the command-line options given to
io-pkt*:
The -d option lets you choose the hardware driver that knows how to talk to a particular
card. For example, choosing -d ne2000 will cause io-pkt* to load devn-ne2000.so
to access an NE-2000-compatible network card. You may specify additional
command-line options after the -d, such as the interrupt vector to be used by the card.
The -p option lets you choose the protocol driver that deals with a particular protocol.
As with the -d option, you would specify command-line options after the -p for the
driver, such as the IP address for a particular interface.
For more information about network services, see the devn-*, and io-pkt entries
in the Utilities Reference.
Network filesystems
• NFS (fs-nfs2, fs-nfs3), which allows file access over a network to a UNIX or
other system running an NFS server.
• CIFS (fs-cifs), which allows file access over a network to a Windows 98 or NT
system or to a UNIX system running an SMB server.
Although NFS is primarily a UNIX-based filesystem, you may find some versions of
NFS available for Windows.
Running applications
There's nothing special required to run your applications. Generally, they'll be placed
in the script file after all the other drivers have started. If you require a particular
driver to be present and “ready,” you would typically use the waitfor command in
the script.
Here's an example. An application called peelmaster needs to wait for a driver (let's
call it driver-spud) to be ready before it should start. The following sequence is
typical:
driver-spud &
waitfor /dev/spud
peelmaster
This causes the driver (driver-spud) to be run in the background (specified by the
ampersand character). The expectation is that when the driver is ready, it will register
the pathname /dev/spud. The waitfor command tries to stat() the pathname
/dev/spud periodically, blocking execution of the script until the pathname appears
or a predetermined timeout has occurred. Once the pathname appears in the pathname
space, we assume that the driver is ready to accept requests. At that point, the
waitfor will unblock, and the next program in the list (in our case, peelmaster)
will execute.
Without the waitfor command, the peelmaster program would run immediately
after the driver was started, which could cause peelmaster to miss the /dev/spud
pathname and fail.
When you're developing embedded systems under some operating systems, you often
need to use a hardware debugger, a physical device that connects to target hardware
via a JTAG (Joint Test Action Group) interface. This is necessary for development of
drivers, and possibly user applications, because they're linked into the same memory
space as the kernel. If a driver or application crashes, the kernel and system may crash
as a result. This makes using software debuggers difficult, because they depend on a
running system.
Debugging target systems with QNX Neutrino is different because its architecture is
significantly different from other embeddable realtime operating systems:
• All QNX Neutrino applications (including drivers) run in their own memory-protected
virtual address space. This has the advantage that the software is more reliable
and fault tolerant. However, conventional hardware debuggers rely on decoding
physical memory addresses, making them incompatible with debugging user
applications based in a virtual memory environment.
• QNX Neutrino lets you develop multithreaded applications, which hardware
debuggers generally don't support.
In other words, you rarely have to use a JTAG hardware debugger, especially if you're
using one of our board support packages.
We provide a software debugging agent called pdebug that makes it easier for you to
debug system drivers and user applications. The pdebug agent runs on the target
system and communicates with the host debugger over a serial or Ethernet connection.
For more information, see “The process-level debug agent” in the Compiling and
Debugging chapter of the Programmer's Guide.
The major constraint of using pdebug is that the kernel must already be running on
the target. In other words, you can't use pdebug until the IPL and startup have
successfully started the kernel.
However, the IPL and startup program run with the CPU in physical mode, so you can
use conventional hardware debuggers to debug them. This is the primary function of
the JTAG debugger throughout the QNX Neutrino software development phase. You
use the hardware debugger to debug the BSP (IPL and startup), and pdebug to debug
drivers and applications once the kernel is running. You can also use a hardware
debugger to examine registers and view memory while the kernel and applications are
running, if you know the physical addresses.
If hardware debuggers, such as SH or AMC have builtin QNX Neutrino awareness, you
can use a JTAG to debug applications. These debuggers can interpret kernel information
as well as perform the necessary translation between virtual and physical memory
addresses to view application data.
You can use hardware debuggers to debug QNX Neutrino IPL and startup programs
without any extra information. However, in this case, you're limited to assembly-level
debugging, and assembler symbols such as subroutine names aren't visible. To perform
full source-level debugging, you need to provide the hardware debugger with the symbol
information and C source code.
This section describes the steps necessary to generate the symbol and debug
information required by a hardware debugger for source-level debugging.
The examples below assume that you're logged in on the development host with root
privileges.
To generate symbol information for the IPL, you must recompile both the IPL library
and the board's IPL with debug information. The general procedure is as follows:
Be sure to synchronize the source code, the IPL burned into flash, and the IPL
debug symbols.
# cd bsp_working_dir/src/hardware/ipl/lib/target/a.le
# make clean
# make CCOPTS=-g
# cp libipl.a bsp_working_dir/board_name/install/processor/lib
# make install
The above steps recompile the target-specific IPL library (libipl.a) with DWARF
debug information and copy this library to the board's install directory, assuming
that the BSP is configured to look for this library first in this directory. The make
install is optional, and copies libipl.a to /processor/usr/lib.
Some BSPs have been set up to work with SREC format files. However, to generate
debug and symbol information to be loaded into the hardware debugger, you must
generate ELF-format files.
TARGET(elf32-target)
OUTPUT_FORMAT(srec)
ENTRY(entry_vec)
to:
TARGET(elf32-target)
OUTPUT_FORMAT(elf32-target)
ENTRY(entry_vec)
You can now rebuild the board's IPL to produce symbol and debug information in ELF
format. To build the board's IPL with debug information:
# cd bsp_working_dir/board_name/src/hardware/ipl/boards/board_name/target/le
# make clean
# make CCOPTS=-g
The ipl-board_name file is now in ELF format with debug symbols from both the
IPL library and the board's IPL.
To rebuild the BSP, you need to change the board_name.lnk file back to
outputting SREC format. It's also important to keep the IPL that's burned into
the board's flash memory in sync with the generated debug information; if you
modify the IPL source, you need to rebuild the BSP, burn the new IPL into
flash, and rebuild the IPL symbol and debug information.
You can use the objdump utility to view the ELF information. For example, to view
the symbol information contained in the ipl-board_name file:
You can now import the ipl-board_name file into a hardware debugger to provide
the symbol information required for debugging. In addition, the hardware debugger
needs the source code listings found in the following directories:
• bsp_working_dir/board_name/src/hardware/ipl/boards/board_name
• bsp_working_dir/src/hardware/ipl/lib
• bsp_working_dir/src/hardware/ipl/lib/target
To generate symbol information for startup, you must recompile both the startup library
and the board's startup with debug information. The general procedure is as follows:
The above steps recompile the startup library (libstartup.a) with DWARF debug
information and copy this library to the install directory, assuming that the BSP
is configured to look for this library first in this directory. The make install is
optional, and copies libstartup.a to /processor/usr/lib.
The above steps generate the file startup-board_name with symbol and debug
information. Again, you can use the -gstabs+ debug option instead of -g. The make
install is necessary, and copies startup-board_name into the install directory,
bsp_working_dir/board_name/install/processor/boot/sys.
You can't load the startup-board_name ELF file into the hardware debugger
to obtain the debug symbols, because the mkifs utility adds an offset to the
addresses defined in the symbols according to the offset specified in the build
file.
Modify the build file to include the +keeplinked attribute for startup:
# cd bsp_working_dir/board_name/images
[image=0x10000]
[virtual=processor,binary +compress] .bootstrap = {
[+keeplinked] startup-board_name -vvv -D8250
PATH=/proc/boot procnto -vv
}
The +keeplinked option makes mkifs generate a symbol file that represents the debug
information positioned within the image filesystem by the specified offset.
# cd bsp_working_dir/board_name/images
# make clean
# make all
otherwise:
These commands create the symbol file, startup-board_name.sym. You can use
the objdump utility to view the ELF information.
You can now import the startup-board_name.sym file into a hardware debugger
to provide the symbol information required for debugging startup. In addition, the
hardware debugger needs the source code listings found in the following directories:
• bsp_working_dir/src/hardware/startup/lib
• bsp_working_dir/src/hardware/startup/lib/public/target
• bsp_working_dir/src/hardware/startup/lib/public/sys
• bsp_working_dir/src/hardware/startup/lib/target
• bsp_working_dir/board_name/src/hardware/startup/boards/board_name
In this section, we'll examine the IPL program in detail, including how to customize
it for your particular hardware, if you need to.
The initial task of the IPL is to minimally configure the hardware to create an
environment that allows the startup program (e.g. startup-bios,
startup-ixdp425, etc.), and consequently the microkernel, to run. This includes
at least the following:
The IPL's initialization part is written entirely in assembly language (because it executes
from ROM with no memory controller). After initializing the hardware, the IPL then
calls the main() function to initiate the C-language environment.
Once the C environment is set up, the IPL can perform different tasks, depending on
whether the OS is booting from a linearly mapped device or a bank-switched device:
Linearly mapped
Bank-switched
Note that we use the term “ROM” generically to mean any nonvolatile memory device
used to store the image (Flash, RAM, ROM, EPROM, flash, battery-backed SRAM,
etc.).
• ROM
Bank-switched images
In conjunction with the above, we have the following processors and configurations:
Let's assume we're booting from a bank-switched or paged device (e.g. paged flash,
disk device, network, etc.), and that the image is uncompressed. The IPL needs to
handle these main tasks:
1. The IPL must first use a C function to talk to the device in question. We'll use a
serial download for this discussion. For serial downloads, the IPL uses
image_download_8250(), a function that specifically knows how to configure and
control the 8250 class of serial controllers.
Once the controller is set up, the function's task is to copy the image via the serial
controller to a location in RAM.
2. We now have an OS image in RAM. The IPL then uses the image_scan() function,
which takes a start address and end address as its parameters. It returns the address
at which it found the image:
• Scans for a valid OS signature over the range provided. Note that this can be
multiple OS images.
• Copies the startup header from the image to a struct startup_header variable.
• Authenticates the startup signature (STARTUP_HDR_SIGNATURE).
3. Once the OS image has been found and validated, the IPL's next function to call
is image_setup(), which takes the address of the image as its parameter and always
returns 0:
At this phase, the startup program has been copied to RAM (and it must always
execute from RAM), and the startup header has been patched with the address of
the OS image.
Since the startup program is responsible for copying the image filesystem
to its final destination in RAM, the IPL must copy the image to a location
that's linearly accessible by the startup program, which has no knowledge
of paged devices (serial, disk, parallel, network, etc.).
Note also that if the image is compressed, then the IPL can copy the
compressed image to a location that won't interfere with startup's
decompression of the image to its final destination in RAM. When the image
lives in flash (or ROM or whatever linear storage device), this isn't an issue.
But when the image is stored on a paged device, more care must be taken
in placing the image in a RAM location that won't interfere with startup's
decompression of the image. Here are the rules:
Uncompressed
If the image is uncompressed, then the IPL can copy the image
from the paged device directly to its destined location. Startup
will compare the addresses and realize that the image doesn't
need to be copied.
Compressed
4. The last phase is to jump to the startup entry point. This is accomplished by calling
image_start():
The function jumps to the startup_vaddr address as defined in the startup header.
For a system that boots from a linearly mapped device (e.g. linear flash, ROM, etc.),
the IPL's tasks are the same as in the paged-device scenario above, but with one
notable exception: the IPL doesn't need to concern itself with copying a full OS image
from the device to RAM.
Your IPL code may be quite simple or fairly elaborate, depending on how your
embedded system is configured. We'll use the terms warm start and cold start to
describe the different types of IPL:
Warm-start IPL
If there's a BIOS or ROM monitor already installed at the reset vector, then
your IPL code is simply an extension to the BIOS or ROM monitor.
Cold-start IPL
The system doesn't have (or doesn't use) a BIOS or ROM monitor program.
The IPL must be located at the reset vector.
Warm-start IPL
In this case, the IPL doesn't get control immediately after the reset, but instead gets
control from the BIOS or ROM monitor.
The x86 PC BIOS allows extensions, as do various ROM monitors. During the power-up
memory scan, the BIOS or ROM monitor attempts to detect extensions in the address
space. To be recognized as an extension, the extension ROM must have a well-defined
extension signature (e.g. for a PC BIOS, this is the sequence 0x55 and then 0xAA as
the first two bytes of the extension ROM). The extension ROM must be prepared to
receive control at the extension entry offset (e.g. for a PC BIOS, this is an offset of
0x0003 into the extension ROM).
Note that this method is used by the various PC BOOTP ROMs available. The ROM
presents itself as an extension, and then, when control is transferred to it, gets an
image from the network and loads it into RAM.
Cold-start IPL
When power is first applied to the processor (or whenever the processor is reset), some
of its registers are set to a known state, and it begins executing from a known memory
location (i.e. the reset vector).
Your IPL software must be located at the reset vector and must be able to:
For example, on an x86 system, the reset vector is located at address 0xFFFFFFF0.
The device that contains the IPL must be installed within that address range. In a
typical x86 PC BIOS, the reset vector code contains a JMP instruction that then
branches to the code that performs diagnostics, setup, and IPL functionality.
Regardless of the processor being used, once the IPL code is started, it has to load
the image in a manner that meets the requirements of the microkernel as described
above. The IPL code may also have to support a backup way of loading the image (e.g.
an .altboot in the case of a hard/floppy boot). This may also have to be an automatic
fallback in the case of a corrupted image.
Note, however, that the amount of work your IPL code has to do really depends on the
location of the image; there may be only a small amount of work for the IPL or there
may be a lot.
This is the simplest scenario. In this case, the entire image is stored in some form of
directly addressable storage — either a ROM device or a form of PC-Card device that
maps its entire address space into the processor's address space. All that's required
is to copy the startup code into RAM. This is ideal for small or deeply embedded
systems.
Note that on x86 architectures, the device isn't required to be addressable within the
first megabyte of memory. The startup program also needn't be in the first megabyte
of RAM.
Note also that for PC-Card devices, some form of setup may be required before the
entire PC-Card device's address space will appear in the address space of the processor.
It's up to your IPL code to perform this setup operation. (We provide library routines
for several standard PC-Card interface chips.)
RAM
Low memory
Flash/ROM
IPL jmp
jmp
Flash ROM
Startup
procnto
Prog1
Prog2
File
High memory
In this scenario, the image is stored in a device that isn't directly mapped into linear
memory. An additional factor needs to be considered here — how will your IPL code
get at the image stored in the device?
• ROM
• Network boot
• Serial or parallel port
• Traditional disk
Let's look at the common characteristics. In such systems, the IPL code knows how
to fetch data from some piece of hardware. The process is as follows:
RAM
Flash/ROM Low memory
IPL jmp
jmp
Paged ROM,
Network,
Serial/Parallel port,
or Disk
Startup
procnto
Prog1
Prog2
File
High memory
ROM devices
In this scenario, a solid-state storage device (ROM, EPROM, flash, etc.) contains the
image, but the processor can see only a small portion of the contents of the device.
How is this implemented? The hardware has a small window (say 32 KB) into the
address space of the processor; additional hardware registers control which portion of
the device is manifested into that window.
20M
storage
Bottom of address space
medium
FFFC 8000
Bottom of window
Window
FFFC FFFF
Top of window
Window
mapping
hardware
In order to load the image, your IPL code must know how to control the hardware that
maps the window. Your IPL code then needs to copy the image out of the window into
RAM and transfer control.
Network boot
To boot a QNX Neutrino system using BOOTP, you'll need a BOOTP ROM for your OS
client and a BOOTP server (e.g. bootpd) for your server. Since the TFTP protocol is
used to move the image from the server to the client, you'll also need a TFTP
server—this is usually provided with a BOOTP server on most systems (QNX Neutrino,
UNIX, Windows 95/98/NT).
Serial port
A serial port on the target can be useful during development for downloading an image
or as a failsafe mechanism (e.g. if a checksum fails, you can simply reload the image
via the serial port).
A serial loader can be built into the IPL code so that the code can fetch the image
from an external hardware port. This generally has a minimal impact on the cost of
an embedded system; in most cases, the serial port hardware can be left off for final
assembly. Evaluation boards supplied by hardware chip vendors often have serial ports.
We supply source code for an embedded serial loader for the 8250 chip.
The IPL process in this case is almost identical to the one discussed above for the
Network boot, except that the serial port is used to fetch the image.
Traditional disk
In a traditional PC-style embedded system with a BIOS, this is the simplest boot
possible. The BIOS performs all the work for you — it fetches the image from disk,
transfers it to RAM, and starts it.
On the other hand, if you don't have a BIOS but you wish to implement this kind of a
boot, then this method involves the most complicated processing discussed so far.
This is because you'll need a driver that knows how to access the disk (whether it's a
traditional rotating-medium hard disk or a solid-state disk). Your IPL code then needs
to look into the partition table of the device and figure out where the contents of the
image reside. Once that determination has been made, the IPL then needs to either
map the image portions into a window and transfer bytes to RAM (in the case of a
solid-state disk) or fetch the data bytes from the disk hardware.
It's entirely conceivable that none of the above adequately describes your particular
embedded system. In that case, the IPL code you'll write must still perform the same
basic steps as described above — handle the reset vector, fetch the image from some
medium, and transfer control to the startup routine.
Once the image has either been loaded into RAM or is available for execution in ROM,
we must transfer control to the startup code (copied from the image to RAM).
For detailed information about the different types of startup programs, see the chapter
on Customizing Image Startup Programs (p. 109).
Once the startup code is off and running, the work of the IPL process is done.
Customizing IPLs
This section describes in detail the steps necessary to write the IPL for an embedded
system that boots from ROM or Flash.
Systems that boot from disk or over the network typically come with a BIOS or ROM
monitor, which already contains a large part of the IPL within it. If your embedded
system fits this category, you can probably skip directly to the chapter on Customizing
Image Startup Programs (p. 109).
Your IPL loader gets control at reset time and performs the following main functions:
Initialize hardware
Basic hardware initialization is done at this time. This includes gaining access to the
system RAM, which may not be addressable after reset. The amount of initialization
done here will depend on what was done by any code before this loader gained control.
On some systems, the power-on-reset will point directly to this code, which will have
to do everything. On other systems, this loader may be called by an even more primitive
loader, which may have already performed some of these tasks.
Note that it's not necessary to initialize standard peripheral hardware such as an IDE
interface or the baud rate of serial ports. This will be done by the OS drivers when
they're started later. Technically, you need to initialize only enough hardware to allow
control to be transferred to the startup program in the image.
The startup program is written in C and is provided in full source-code format. The
startup code is structured in a readily customizable manner, providing a simple
environment for performing further initializations, such as setting up the system page
in-memory data structure.
The IPL code must locate the boot image (made with the mkifs utility) and copy part
or all of it into memory.
The loader uses information in the header to copy the header and startup into RAM.
The loader would be responsible for copying the entire image into RAM if the image
weren't located in linearly addressable memory.
The boot header structure struct startup_header is defined in the include file
<sys/startup.h>. It is 256 bytes in size and contains the following members,
which are examined by the IPL and/or startup code:
A valid image (for bootable images) is detected by performing a checksum (via the
function call checksum()) over the entire image, as follows:
signature
This is the first 32 bits in the header and always contains 0x00FF7EEB in native byte
order. It's used to identify the header. On a machine that can be either big-endian or
little-endian (a bi-endian machine), there's typically a hardware strap that gets set on
the board to specify the endianness.
version
The following flags are defined for flags1 (flags2 is currently not used):
STARTUP_HDR_FLAGS1_VIRTUAL
If this flag is set, the operating system is to run with the Memory Management
Unit (MMU) enabled.
For this release of the QNX Neutrino RTOS, you should always specify
a virtual system (by specifying the virtual= attribute in your
buildfile, which then sets the STARTUP_HDR_FLAGS1_VIRTUAL
flag).
STARTUP_HDR_FLAGS1_BIGENDIAN
STARTUP_HDR_FLAGS1_COMPRESS_NONE
STARTUP_HDR_FLAGS1_COMPRESS_ZLIB
STARTUP_HDR_FLAGS1_COMPRESS_LZO
STARTUP_HDR_FLAGS1_COMPRESS_UCL
The image is compressed with libucl. This is the format chosen when
using the [+compress] attribute in the mkifs build script.
Note that both flag flags1 and flags2 are single-byte; this ensures that they're
endian-neutral.
header_size
machine
startup_vaddr
paddr_bias
Value to add to physical address to get a value to put into a pointer and indirect
through.
image_paddr
The physical address of the image. This can be in ROM or RAM, depending on the
type of image; for more information, see “Relationship of struct startup_header
fields (p. 94),” later in this chapter.
ram_paddr
The physical address in RAM to copy the image to. You should copy startup_size bytes
worth of data.
ram_size
The number of bytes the image will occupy when it's loaded into RAM. This value is
used by the startup code in the image and isn't currently needed by the IPL code.
This size may be greater than stored_size if the image was compressed. It may also
be smaller than stored_size if the image is XIP.
startup_size
This is the size of the startup code. Copy this number of bytes from the start of the
image into RAM. Note that the startup code is never compressed, so this size is true
in all cases.
stored_size
This is the size of the image including the header. The stored_size member is also
used in the copy/decompress routines for non-XIP images.
imagefs_paddr
Set by the IPL to the physical address of the image filesystem. Used by the startup.
imagefs_size
preboot_size
Contains the number of bytes from the beginning of the loaded image to the startup
header. Note that this value will usually be zero, indicating that nothing precedes the
startup portion. On an x86 with a BIOS, it will be nonzero, because there's a small
piece of code that gets data from the BIOS in real mode and then switches into
protected mode and performs the startup.
info
Note that the info is declared as an array of longs — this is purely to allocate the
storage space. In reality, the info storage area contains a set of structures, each
beginning with this header:
struct startup_info_hdr {
unsigned short type;
unsigned short size;
};
STARTUP_INFO_SKIP
Ignore this field. If the corresponding size member is 0, it means that this
is the end of the info list.
STARTUP_INFO_MEM
STARTUP_INFO_DISK
STARTUP_INFO_TIME
STARTUP_INFO_BOX
Note that the struct startup_info_hdr header (containing the type and size
members) is encapsulated within each of the above mentioned struct
startup_info* structures as the first element.
struct startup_info_skip
These structures contain an address and size pair defining a chunk of memory that
should be added to procnto's free memory pool.
struct startup_info_mem {
struct startup_info_hdr hdr;
unsigned long addr;
unsigned long size;
};
The addr and size fields are 32 bits long, so memory is limited to 4 GB. For larger
memory blocks, the startup_info_mem_extended structure is used:
struct startup_info_mem_extended {
struct startup_info_mem mem;
unsigned long addr_hi;
unsigned long size_hi;
};
For the extended structure, determine the address and size from the addr_hi and
size_hi members and the encapsulated startup_info_mem structure as follows:
struct startup_info_disk
struct startup_info_disk {
struct startup_info_hdr hdr;
unsigned char drive;
unsigned char zero;
unsigned short heads;
unsigned short cylinders;
unsigned short sectors;
unsigned long blocks;
};
Contains information about any hard disks detected (on a PC with a BIOS). The
members are as follows:
drive
Drive number.
zero
heads
cylinders
sectors
blocks
struct startup_info_time
struct startup_info_time {
struct startup_info_hdr hdr;
unsigned long time;
};
The time member contains the current time as the number of seconds since 1970 01
01 00:00:00 GMT.
struct startup_info_box
struct startup_info_box {
struct startup_info_hdr hdr;
unsigned char boxtype;
unsigned char bustype;
unsigned char spare [2];
};
Contains the boxtype and bustype information. For valid values, please see the chapter
on Customizing Image Startup Programs (p. 109).
The following explains some of the fields used by the IPL and startup for various types
of boot. These fields are stuffed by mkifs.
Note that we've indicated which steps are performed by the IPL and which are done
by the startup.
image_paddr
ram_paddr
ROM
Startup
Startup header
startup_vaddr
header Startup
startup_size
Startup
ram_size
stored_size Reserved
Imagefs for
header imagefs
data
imagefs_size Imagefs
High memory
image_paddr
ram_paddr
ROM
Startup
Startup header
startup_vaddr
header Startup
startup_size
Startup
Imagefs ram_size
stored_size Compressed header
imagefs Imagefs
header
Imagefs
imagefs_size
High memory
image_paddr
ram_paddr
ROM
Startup
Startup header
startup_vaddr
header Startup
startup_size
Startup
Imagefs ram_size
stored_size Imagefs header
header Imagefs imagefs_size
Imagefs
imagefs_size
High memory
In this case our full IPL isn't involved. An existing BIOS IPL loads the image into
memory and transfers control to our IPL. Since the existing IPL doesn't know where
in startup to jump, it always jumps to the start of the image. On the front of the image
we build a tiny IPL that jumps to startup_vaddr:
RAM
Low memory
jump ipl
image_paddr,
ram_paddr
Startup
header
startup_size startup_vaddr
Startup
stored_size, Imagefs
ram_size header
Imagefs
imagefs_size
High memory
jump (startup_vaddr)
This is identical to the previous case, except that we need to decompress the image
in the startup:
RAM
Low memory
jump ipl
image_paddr
startup_vaddr
Startup RAM
header
startup_size ram_paddr
Startup
High memory
The case of a bank-switched ROM is much like a disk/network boot except you get to
write the code that copies the image into RAM using the following steps in the IPL:
You'll need to map the physical addresses and sizes into bank-switching as needed.
Have fun and next time don't bank-switch your rom! Make it linear in the address
space.
IPL structure
In this section, we'll examine the structure of the IPL source tree directory, and also
the structure of a typical IPL source file.
bsp_working_dir/src/hardware
boards
The IPL source code for a particular board is stored in a directory under
bsp_working_dir/src/hardware/ipl/boards.
The IPL code is structured in two stages. The first stage is written in assembly language;
it sets up just enough of an environment for the second stage, written in C, to run.
Generally, the minimum work done here is to set up the DRAM controllers, initialize
the various registers, and set up the chip selects so that you can address your hardware.
Generally, the IPL assembly-language source name begins with “init” (e.g.
init8xx.s for the MPC8xxFADS board); the C file is always called main.c.
Once your assembly-language routine has set up the minimum amount required to
transfer control to the C language portion, the main() program calls the following
functions in order:
image_download_8250()
This function is responsible for getting the image from wherever it may be
located. If the image is located in linear memory, this function isn't required
(the image is already “downloaded”).
If you're downloading the image from a custom piece of hardware, you should
call your function image_download_hw(), where the hw part is replaced with
a descriptive name for the hardware, e.g. image_download_x25().
image_scan()
This function is given a start and an end address to search for a boot image.
If successful, it returns a pointer to the start of the image. It's possible to
search within an address range that contains more than one image. If there
are multiple images, and one of them has a bad checksum, then the next
image is used. If there are multiple images with good checksums, the startup
header is examined, and the one with the higher version number is used.
Note that the scan will occur only between the specified addresses.
image_setup()
This function does the work of copying the necessary part of the image into
RAM.
image_start()
This function will jump to the start of the image loaded into RAM, which
will turn control over to the startup program.
An example
#include "ipl.h"
int
main (void)
{
/*
* Image is located at 0x2840000
* Therefore, we don't require an image_download_8250 function
*/
image = image_scan (0x2840000, 0x2841000);
/*
* Copy startup to ram; it will do any necessary work on the image
*/
image_setup (image);
/*
* Set up link register and jump to startup entry point
*/
image_start (image);
return (0);
}
In this case, we have a linearly addressable flash memory device that contains the
image — that's why we don't need the image_download_8250() function.
The next function called is image_scan(), which is given a very narrow range of
addresses to scan for the image. We give it such a small range because we know where
the image is on this system — there's very little point searching for it elsewhere.
Then we call image_setup() with the address that we got from the image_scan(). This
copies the startup code to RAM.
Finally, we call image_start() to transfer control to the startup program. We don't expect
this function to return — the reason we have the return (0); statement is to keep
the C compiler happy (otherwise it would complain about “Missing return value from
function main”).
To create a new IPL, it's best to start with one we've provided that's similar to the type
of CPU and board you have in your design.
The IPL library contains a set of routines for building a custom IPL. Here are the
available library functions:
Function Description
Function Description
enable_cache
enable_cache
image_download_8250()
Downloads an image from the specified serial port (port) to the specified address
(address) using a custom protocol. On the host side, this protocol is implemented via
the utility sendnto (you may need a NULL-modem cable — the protocol uses only
TX, RX, and GND). The span parameter indicates the offset from one port to the next
port on the serial device.
image_scan()
The image_scan() function scans memory for a valid system image. It looks on 4 KB
boundaries for the image identifier bytes and then does a checksum on the image.
The function scans between start and end. If a valid image is found, image_scan()
returns the image's address. If no valid image is found, it returns -1.
Note that image_scan() will search for all images within the given range, and will pick
the “best” one as described above (in the “IPL code structure” (p. 97) section).
image_scan_ext()
image_setup()
The image_setup() function prepares an image for execution. It copies the RAM-based
startup code from ROM.
The function takes the image's address as its parameter and always returns 0.
image_setup_ext()
image_start()
The image_start() function starts the image by jumping to the startup_vaddr address
as defined in the startup header.
image_start_ext()
int15_copy()
The int15_copy() function is intended for an x86 system with a BIOS running in real
mode. The function lets you copy data from high memory (above 1 MB) to a buffer or
to low memory (below 1 MB).
print_byte()
print_char()
print_long()
print_sl()
Using int10, this function displays to video a string, followed by a long (x86 only).
print_string()
print_var()
print_word()
protected_mode()
This assembly call switches the x86 processor into protected mode. The function is
for non-BIOS systems.
Upon return, the DS and ES registers will be set to selectors that can access the entire
4 GB address space. This code is designed to be completely position-independent.
This routine must be called with a pointer to a 16-byte area of memory that's used to
store the GDT. The pointer is in ds:ax.
16
uart_hex8
This assembly call outputs an 8-bit hex number to the UART. The function is set up
for a 16-bit real-mode environment (x86 only).
On entry:
DX
AL
Value to output.
uart_hex16
This assembly call outputs a 16-bit hex number to the UART. The function is set up
for a 16-bit real-mode environment (x86 only).
On entry:
DX
AX
Value to output.
uart_hex32
This assembly call outputs a 32-bit hex number to the UART. The function is set up
for a 16-bit real-mode environment (x86 only).
On entry:
DX
EAX
Value to output.
uart_init
This assembly call initializes the on-chip UART to 8 data bits, 1 stop bit, and no parity
(8250 compatible). The function is set up for a 16-bit real-mode environment (x86
only).
On entry:
EAX
Baud rate.
EBX
ECX
DX
uart_put
This assembly call outputs a single character to the UART. The function is set up for
a 16-bit real-mode environment (x86 only).
On entry:
AL
Character to output.
DX
uart_string
This assembly call outputs a NULL-terminated string to the UART. The function is set
up for a 16-bit real-mode environment (x86 only).
On entry:
DX
For example:
uart32_hex8
This assembly call outputs an 8-bit hex number to the UART. The function is set up
for a 32-bit protected-mode environment (x86 only).
On entry:
DX
AL
Value to output.
uart32_hex16
This assembly call outputs a 16-bit hex number to the UART. The function is set up
for a 32-bit protected-mode environment (x86 only).
On entry:
DX
AX
Value to output.
uart32_hex32
This assembly call outputs a 32-bit hex number to the UART. The function is set up
for a 32-bit protected-mode environment (x86 only).
On entry:
DX
EAX
Value to output.
uart32_init
This assembly call initializes the on-chip UART to 8 data bits, 1 stop bit, and no parity
(8250 compatible). The function is set up for a 32-bit protected-mode environment
(x86 only).
On entry:
EAX
Baud rate.
EBX
ECX
DX
uart32_put
This assembly call outputs a single character to the UART. The function is set up for
a 32-bit protected-mode environment (x86 only).
On entry:
AL
Character to output.
DX
uart32_string
This assembly call outputs a NULL-terminated string to the UART. The function is set
up for a 32-bit protected-mode environment (x86 only).
On entry:
DX
For example:
The first program in a bootable QNX Neutrino image is a startup program whose purpose
is to:
You do basic hardware initialization at this time. The amount of initialization done
here will depend on what was done in the IPL loader.
Note that you don't need to initialize standard peripheral hardware such as an IDE
interface or the baud rate of serial ports. This will be done by the drivers that
manage this hardware when they're started.
2. Initialize the system page.
Information about the system is collected and placed in an in-memory data structure
called the system page (p. 112). This includes information such as the processor
type, bus type, and the location and size of available system RAM.
The kernel as well as applications can access this information as a read-only data
structure. The hardware/system-specific code to interrogate the system for this
information is confined to the startup program. This code doesn't occupy any system
RAM after it has run.
3. Initialize callouts.
Another key function of the startup code is that the system page callouts are bound
in. These callouts are used by the kernel to perform various hardware- and
system-specific functions that must be specified by the systems integrator.
4. Load and transfer control to the next program in the image.
You can customize QNX Neutrino for different embedded-system hardware by changing
the startup program.
Each release of the QNX Neutrino RTOS ships with a growing number of startup
programs for many boards. To find out what boards we currently support, please refer
to the following sources:
bsp_working_dir/src/hardware
startupipl flash
boards bootfile
Generally speaking, the following directory structure applies in the startup source for
the startup-boardname module:
bsp_working_dir/src/hardware/startup/boards/boardname
Each startup program consists of a main() with the following structure (in pseudo
code):
Global variables
main()
{
Call add_callout_array (p. 146)()
You should examine the commented source for each of the functions within
the library to see if you need to replace a library function with one of your own.
To create a new startup program, you should make a new directory under
bsp_working_dir/src/hardware/startup/boards and copy the files from one of
the existing startup program directories. For example, to create something close to
the Intel PXA250TMDP board, called my_new_board, you would:
1. cd bsp_working_dir/src/hardware/startup/boards
2. mkdir my_new_board
3. cp -r pxa250tmdp/* my_new_board
4. cd my_new_board
5. make clean
For descriptions of all the startup functions, see “The startup library” (p. 143) section
in this chapter.
The system page structure struct syspage_entry is defined in the include file
<sys/syspage.h>. The structure contains a number of constants, references to
other structures, and a union shared between the various processor platforms supported
by the QNX Neutrino RTOS.
It's important to realize that there are two ways of accessing the data within the system
page, depending on whether you're adding data to the system page at startup time or
reading data from the system page later (as would be done by an application program
running after the system has been booted). Regardless of which access method you
use, the fields are the same.
/*
* contains at least the following:
*/
struct syspage_entry {
uint16_t size (p. 112);
uint16_t total_size (p. 113);
uint16_t type (p. 113);
uint16_t num_cpu (p. 113);
syspage_entry_info system_private (p. 113);
syspage_entry_info asinfo (p. 113);
syspage_entry_info hwinfo (p. 116);
syspage_entry_info cpuinfo (p. 122);
syspage_entry_info cacheattr (p. 124);
syspage_entry_info qtime (p. 127);
syspage_entry_info callout (p. 129);
syspage_entry_info callin (p. 129);
syspage_entry_info typed_strings (p. 129);
syspage_entry_info strings (p. 130);
syspage_entry_info intrinfo (p. 130);
syspage_entry_info smp (p. 137);
syspage_entry_info pminfo (p. 138);
union {
struct x86_syspage_entry x86 (p. 136);
struct arm_syspage_entry arm (p. 137);
} un (p. 136);
};
Note that some of the fields presented here may be initialized by the code provided
in the startup library, while some may need to be initialized by code provided by you.
The amount of initialization required really depends on the amount of customization
that you need to perform.
size
The size of the system page entry. This member is set automatically by the library.
total_size
The size of the system page entry plus the referenced substructures; effectively the
size of the entire system-page database. This member is set automatically by the
library and adjusted later (grown) as required by other library calls.
type
This is used to indicate the CPU family for determining which union member in the
un (p. 136) element to use. Can be one of:
SYSPAGE_ARM, or SYSPAGE_X86.
num_cpu
The num_cpu member indicates the number of CPUs present on the given system.
This member is initialized to the default value 1 in the library and adjusted by the
library call init_smp() (p. 165) if additional processors are detected.
system_private
The system_private area contains information that the operating system needs to know
when it boots. This is filled in by the startup library's init_system_private() (p. 166)
function.
Member Description
asinfo
The asinfo section consists of an array of the following structure. Each entry describes
the attributes of one section of address space on the machine.
struct asinfo_entry {
uint64_t start;
uint64_t end;
uint16_t owner;
uint16_t name;
uint16_t attr;
uint16_t priority;
int (*alloc_checker)(struct syspage_entry *__sp,
uint64_t *__base,
uint64_t *__len,
size_t __size,
size_t __align);
uint32_t spare;
};
Member Description
The alloc_checker isn't currently used. When implemented, it will let you
provide finer-grain control over how the system allocates memory (e.g. making
sure that ISA memory used for DMA doesn't cross 64 KB boundaries).
Address range can be cached (this bit should be off if you're using device
memory).
Indicates that there are other entries that use this one as their owner. Note
that the library turns on this bit automatically; you shouldn't specify it when
creating the section.
Indicates that there are multiple entries being used to describe one “logical”
address range. This bit will be on in all but the last one. Note that the library
turns on this bit and uses it internally; you shouldn't specify it when creating
the section.
The asinfo section contains trees describing address spaces (where RAM, ROM, flash,
etc. are located).
/memory/memclass/....
Or:
/io/memclass/....
Or:
/memory/io/memclass/....
The memclass is something like: ram, rom, flash, etc. Below that would be further
classifications, allowing the process manager to provide typed memory support.
hwinfo
The hwinfo area contains information about the hardware platform (type of bus, devices,
IRQs, etc). This is filled in by the startup library's init_hwinfo() function.
This is one of the more elaborate sections of the QNX Neutrino system page. The
hwinfo section doesn't consist of a single structure or an array of the same type.
Instead, it consists of a sequence of symbolically “tagged” structures that as a whole
describe the hardware installed on the board. The following types and constants are
all defined in the <hw/sysinfo.h> file.
The hwinfo section doesn't have to describe all the hardware. For instance,
the startup program doesn't have to do PCI queries to discover what's been
plugged into any slots if it doesn't want to. It's up to you as the startup
implementor to decide how full to make the hwinfo description. As a rule, if
a component is hardwired on your board, consider putting it into hwinfo.
Tags
Each structure (or tag) in the section starts the same way:
struct hwi_prefix {
uint16_t size;
uint16_t name;
};
The size field gives the size, in 4-byte quantities, of the structure (including the
hwi_prefix).
The name field is an offset into the strings section of the system page, giving a
zero-terminated string name for the structure. It might seem wasteful to use an ASCII
string rather than an enumerated type to identify the structure, but it actually isn't.
The system page is typically allocated in 4 KB granularity, so the extra storage required
by the strings doesn't cost anything. On the upside, people can add new structures to
the section without requiring QNX Software Systems to act as a central repository for
handing out enumerated type values. When processing the section, code should ignore
any tag that it doesn't recognize (using the size field to skip over it).
Items
struct hwi_item {
struct hwi_prefix prefix;
uint16_t itemsize;
uint16_t itemname;
uint16_t owner;
uint16_t kids;
};
The itemsize field gives the distance, in 4-byte quantities, until the start of the next
item tag.
The itemname gives an offset into the strings section of the system page for the name
of the item being described. Note that this differs from the prefix.name field, which
tells what type of the structure the hwi_item is buried in.
The owner field gives the offset, in bytes, from the start of the hwinfo section to the
item that this item is owned by. This field allows groups of items to be organized in
a tree structure, similar to a filesystem directory hierarchy. We'll see how this is used
later. If the item is at the root of a tree of ownership, the owner field is set to
HWI_NULL_OFF.
The kids field indicates how many other items call this one “daddy.”
The code currently requires that the tag name of any item structure must start
with an uppercase letter; nonitem tags have to start with a lowercase letter.
Device trees
The hwinfo section contains trees describing the various hardware devices on the
board.
/hw/bus/devclass/device
where:
hw
bus
devclass
device
Two basic calls in the startup library are used to add things to the hwinfo section:
• hwi_alloc_tag()
• hwi_alloc_item()
This call allocates a tag of size size with the tag name of name. If the structure contains
any 64-bit integer fields within it, the align field should be set to 8; otherwise, it
should be 4. The function returns a pointer to memory that can be filled in as
appropriate. Note that the hwi_prefix fields are automatically filled in by the
hwi_alloc_tag() function.
This call allocates an item structure. The first three parameters are the same as in the
hwi_alloc_tag() function.
The itemname and owner parameters are used to set the itemname and owner fields
of the hwi_item structure. All hwi_alloc_tag() calls done after a hwi_alloc_item() call
are assumed to belong to that item and the itemsize field is adjusted appropriately.
1. Call hwi_alloc_item() to build a top-level item (one with the owner field to be
HWI_NULL_OFF).
2. Add whatever other tag structures you want in the item.
3. Use hwi_alloc_item() to start a new item. This item could be either another top-level
one or a child of the first.
Note that you can build the items in any order you wish, provided that the parent is
built before the child.
When building a child item, suppose you've remembered its owner in a variable or you
know only its item name. In order to find out the correct value of the owner parameter,
you can use the hwi_find_item() function (which is defined in the C library, since it's
useful for people processing the section):
The start parameter indicates where to start the search for the given item. For an initial
call, it should be set to HWI_NULL_OFF. If the item found isn't the one wanted, then
the return value from the first hwi_find_item() is used as the start parameter of the
second call. The search will pick up where it left off. This can be repeated as many
times as required (the return value from the second call going into the start parameter
of the third, etc). The item being searched is identified by a sequence of char *
parameters following start. The sequence is terminated by a NULL. The last string
before the NULL is the bottom-level itemname being searched for, the string in front
of that is the name of the item that owns the bottom-level item, etc.
For example, this call finds the first occurrence of an item called “foobar”:
The following call finds the first occurrence of an item called “foobar” that's owned
by “sam”:
Other functions
The following functions are in the C library for use in processing the hwinfo section:
hwi_tag2off()
Given a pointer to the start of a tag, return the offset, in bytes, from the
beginning of the start of the hwinfo section.
hwi_off2tag()
void *hwi_off2tag(unsigned);
Given an offset, in bytes, from the start of the hwinfo section, return a pointer
to the start of the tag.
hwi_find_tag()
Find the tag named tagname. The start parameter works the same as the
one in hwi_find_item(). If curr_item is nonzero, the search stops at the end
of the current item (whatever item the start parameter points into). If
curr_item is zero, the search continues until the end of the section. If the
tag isn't found, HWI_NULL_OFF is returned.
hwi_next_item()
Get the offset of the next item after the given offset from the start of the
hwinfo section.
hwi_next_tag()
Get the offset of the next tag after the given offset from the start of the
hwinfo section. As it is for hwi_find_tag(), the curr_item restricts the search
to the current item.
For more information about these functions, see the QNX Neutrino C Library Reference.
Defaults
Before main() is invoked in the startup program, the library adds some initial entries
to serve as a basis for later items.
void
hwi_default() {
hwi_tag *tag;
hwi_tag *tag;
hwi_alloc_item(HWI_TAG_INFO(group), HWI_ITEM_ROOT_AS,
HWI_NULL_OFF);
tag = hwi_alloc_item(HWI_TAG_INFO(group), HWI_ITEM_ROOT_HW,
HWI_NULL_OFF);
hwi_alloc_item(HWI_TAG_INFO(bus), HWI_ITEM_BUS_UNKNOWN,
hwi_tag2off(tag));
tag = hwi_alloc_item(HWI_TAG_INFO(addrspace),
HWI_ITEM_AS_MEMORY, loc);
tag->addrspace.base = 0;
tag->addrspace.len = (uint64_t)1 << 32;
#ifndef __X86__
loc = hwi_tag2off(tag);
#endif
tag = hwi_alloc_item(HWI_TAG_INFO(addrspace), HWI_ITEM_AS_IO,
loc);
tag->addrspace.base = 0;
#ifdef __X86__
tag->addrspace.len = (uint64_t)1 << 16;
#else
tag->addrspace.len = (uint64_t)1 << 32;
#endif
}
These are the items defined in the hw/sysinfo.h file. Note that you're free to create
additional items — these are just what we needed for our own purposes. You'll notice
that all things are defined as HWI_TAG_NAME_*, HWI_TAG_ALIGN_*, and struct
hwi_*. The names are chosen that way so that the HWI_TAG_INFO() macro in startup
works properly.
Group item
The Group item is used when you wish to group a number of items together. It serves
the same purpose as a directory in a filesystem. For example, the devclass level of the
/hw tree would use a Group item.
Bus item
The Bus item tells the system about a hardware bus. Item names can be (but are not
limited to):
Device item
The Device item tells the system about an individual device (the device level from the
“Trees” section — the devclass level is done with a “Group” tag). The pnpid field is
the Plug and Play device identifier assigned by Microsoft.
location tag
Note that location is a simple tag, not an item. It gives the location of the hardware
device's registers, whether in a separate I/O space or memory-mapped. There may be
more than one of these tags in an item description if there's more than one grouping
of registers.
The base field gives the physical address of the start of the registers. The len field
gives the length, in bytes, of the registers. The regshift tells how much each register
access is shifted by. If a register is documented at offset of a device, then the driver
will actually access offset offset2^regshift to get to that register.
The addrspace field is an offset, in bytes, from the start of the asinfo section. It should
identify either the memory or io address space item to tell whether the device registers
are memory-mapped.
irq tag
Note that this is a simple tag, not an item. The vector field gives the logical interrupt
vector number of the device.
diskgeometry tag
Note that this is a simple tag, not an item. This is an x86-only mechanism used to
transfer the information from the BIOS about disk geometry.
pad tag
Note that this is a simple tag, not an item. This tag is used when padding must be
inserted to meet the alignment constraints for the subsequent tag.
cpuinfo
The cpuinfo area contains information about each CPU chip in the system, such as
the CPU type, speed, capabilities, performance, and cache sizes. There are as many
elements in the cpuinfo structure as the num_cpu (p. 113) member indicates (e.g. on
a dual-processor system, there will be two cpuinfo entries).
This table is filled automatically by the library function init_cpuinfo() (p. 163).
Member Description
The flags member contains a bitmapped indication of the capabilities of the CPU chip.
Note that the prefix for the manifest constant indicates which CPU family it applies
to (e.g. ARM_ indicates this constant is for use by the ARM family of processors). In
the case of no prefix, it indicates that it's generic to any CPU.
syspage_entry cacheattr
The cacheattr area contains information about the configuration of the on-chip and
off-chip cache system. It also contains the control() callout used for cache control
operations. This entry is filled by the library routines init_cpuinfo() (p. 163) and
init_cacheattr() (p. 163).
Note that init_cpuinfo() (p. 163) deals with caches implemented on the CPU itself;
init_cacheattr() handles board-level caches.
Member Description
Member Description
The cacheattr entries are organized in a linked list, with the next member indicating
the index of the next lower cache entry. This was done because some architectures
will have separate instruction and data caches at one level, but a unified cache at
another level. This linking allows the system page to efficiently contain the information.
Note that the entry into the cacheattr tables is done through the cpuinfo (p. 122)'s
ins_cache and data_cache. Since the cpuinfo (p. 122) is an array indexed by the CPU
number for SMP systems, it's possible to construct a description of caches for CPUs
with different cache architectures. Here's a diagram showing a two-processor system,
with separate L1 instruction and data caches as well as a unified L2 cache:
CPU 1CPU 2
L1 L1 L1 L1
instruction data instruction data
cache cache cache cache
L2 unified L2 unified
Memory
Figure 13: Two-processor system with separate L1 instruction and data caches.
Given the above memory layout, here's what the cpuinfo (p. 122) and cacheattr (p. 124)
fields will look like:
/*
* CPUINFO
*/
cpuinfo [0].ins_cache = 0;
cpuinfo [0].data_cache = 1;
cpuinfo [1].ins_cache = 0;
cpuinfo [1].data_cache = 1;
/*
* CACHEATTR
*/
cacheattr [0].next = 2;
cacheattr [0].linesize = linesize;
cacheattr [0].numlines = numlines;
cacheattr [0].flags = CACHE_FLAG_INSTR;
cacheattr [1].next = 2;
cacheattr [1].linesize = linesize;
cacheattr [1].numlines = numlines;
cacheattr [1].flags = CACHE_FLAG_DATA;
Note that the actual values chosen for linesize and numlines will, of course, depend
on the actual configuration of the caches present on the system.
syspage_entry qtime
The qtime area contains information about the timebase present on the system, as
well as other time-related information. The library routine init_qtime() (p. 164) fills
these data structures.
Member Description
Member Description
• disable interrupts
or:
• get the value(s) twice and make sure that they haven't changed between
the first and second read.
The parameters timer_rate and timer_scale relate to the external counter chip's input
frequency, in Hz, as follows:
1
timer_scale
timer_rate x 10
Yes, this does imply that timer_scale is a negative number. The goal when expressing
the relationship is to make timer_rate as large as possible in order to maximize the
number of significant digits available during calculations.
For example, on an x86 PC with standard hardware, the values would be 838095345UL
for the timer_rate and -15 for the timer_scale. This indicates that the timer value is
specified in femtoseconds (the -15 means “ten to the negative fifteen”); the actual
value is 838,095,345 femtoseconds (approximately 838 nanoseconds).
If the clock on your system drifts, you should make sure that the startup code specifies
the correct clock frequency. You can use the -f option in the startup command to
override the setting in the code.
callout
The callout area is where various callouts get bound into. These callouts allow you to
“hook into” the kernel and gain control when a given event occurs. The callouts operate
in an environment similar to that of an interrupt service routine — you have a very
limited stack, and you can't invoke any kernel calls (such as mutex operations, etc.).
On standard hardware platforms (x86-PC compatibles), you won't have to supply any
functionality — it's already provided by the startup code we supply.
Member Description
For details about the characteristics of the callouts, please see the sections “Callout
information (p. 139)” and “Writing your own kernel callout (p. 173)” later in this chapter.
callin
typed_strings
The typed_strings area consists of several entries, each of which is a number and a
string. The number is 4 bytes and the string is NULL-terminated as per C. The number
in the entry corresponds to a particular constant from the system include file
<confname.h> (see the C function confname() for more information).
Generally, you wouldn't access this member yourself; the various init_*() library
functions put things into the typed strings literal pool themselves. But if you need to
add something, you can use the function call add_typed_string() (p. 146) from the
library.
strings
This member is a literal pool used for nontyped strings. Users of these strings would
typically specify an index into strings (for example, cpuinfo (p. 122)'s name member).
Generally, you wouldn't access this member yourself; the various init_*() library
functions put things into the literal pool themselves. But if you need to add something,
you can use the function call add_string() (p. 146) from the library.
intrinfo
The intrinfo area is used to store information about the interrupt system. It also contains
the callouts used to manipulate the interrupt controller hardware.
On a multicore system, each interrupt is directed to one (and only one) CPU, although
it doesn't matter which. How this happens is under control of the programmable
interrupt controller chip(s) on the board. When you initialize the PICs at startup, you
can program them to deliver the interrupts to whichever CPU you want to; on some
PICs you can even get the interrupt to rotate between the CPUs each time it goes off.
For the startups we write, we typically program things so that all interrupts (aside from
the one(s) used for interprocessor interrupts) are sent to CPU 0. This lets us use the
same startup for both procnto and procnto-smp. According to a study that Sun
did a number of years ago, it's more efficient to direct all interrupts to one CPU, since
you get better cache utilization.
The intrinfo area is automatically filled in by the library routine init_intrinfo() (p. 164).
If you need to override some of the defaults provided by init_intrinfo() (p. 164), or if
the function isn't appropriate for your custom environment, you can call
add_interrupt_array() (p. 146) directly with a table of the following format:
Member Description
Member Description
Member Description
Each group of callouts (i.e. id, eoi, mask, unmask) for each level of interrupt
controller deals with a set of interrupt vectors that start at 0 (zero-based). Set
the callouts for each level of interruption accordingly.
Interrupt vector numbers are passed without offset to the callout routines. The
association between the zero-based interrupt vectors the callouts use and the
system-wide interrupt vectors is configured within the startup-intrinfo structures.
These structures are found in the init_intrinfo() routine of startup.
Processor Interpretation
Processor Interpretation
The flags member takes two sets of flags. The first set deals with the characteristics
of the interrupts:
INTR_FLAG_NMI
The code in the kernel needs to differentiate between normal interrupts and
NMIs, because with an NMI the kernel needs to know that it can't protect
(mask) the interrupt (hence the “N” in NonMaskable Interrupt). We strongly
discourage the use of the NMI vector in x86 designs; we don't support it on
any non-x86 platforms.
INTR_FLAG_CASCADE_IMPLICIT_EOI
Indicates that an EOI to the primary interrupt controller is not required when
handling a cascaded interrupt (e.g. it's done automatically). Only used if
this entry describes a cascaded controller.
INTR_FLAG_CPU_FAULT
Indicates that one or more of the vectors described by this entry is not
connected to a hardware interrupt source, but rather is generated as a result
of a CPU fault (e.g. bus fault, parity error). Note that we strongly discourage
designing your hardware this way. The implication is that a check needs to
be inserted for an exception into the generated code stream; after the
interrupt has been identified, an EOI needs to be sent to the controller. The
EOI code burst has the additional responsibility of detecting what address
caused the fault, retrieving the fault type, and then passing the fault on.
The primary disadvantage of this approach is that it causes extra code to be
inserted into the code path.
INTR_GENFLAG_LOAD_SYSPAGE
INTR_GENFLAG_LOAD_INTRINFO
INTR_GENFLAG_LOAD_INTRMASK
Used only by EOI routines for hardware that doesn't automatically mask at
the chip level. When the EOI routine is about to reenable interrupts, it should
reenable only those interrupts that are actually enabled at the user level
(e.g. managed by the functions InterruptMask() and InterruptUnmask()).
When this flag is set, the existing interrupt mask is stored in a register for
access by the EOI routine. A zero in the register indicates that the interrupt
should be unmasked; a nonzero indicates it should remain masked.
INTR_GENFLAG_NOGLITCH
INTR_GENFLAG_LOAD_CPUNUM
INTR_GENFLAG_ID_LOOP
In the ID callout, you need to allocate read-write storage as per the usual
procedures. This storage is initially set to zero (done by default). When the
callout runs, the first thing it does is check the storage area:
The config callout may return zero or more of the following flags:
INTR_CONFIG_FLAG_PREATTACH
INTR_CONFIG_FLAG_DISALLOWED
Prevents user code from attaching to this interrupt level. Generally used
with INTR_CONFIG_FLAG_PREATTACH, but could be used to prevent user
code from attaching to any interrupt in general.
INTR_CONFIG_FLAG_IPI
syspage_entry union un
The un union is where processor-specific system page information is kept. The purpose
of the union is to serve as a demultiplexing point for the various CPU families. It is
demultiplexed based on the value of the type (p. 113) member of the system page
structure.
un.x86
smpinfo
gdt
idt
pgdir
real_addr
un.x86.smpinfo (deprecated)
The members of this field are filled automatically by the function init_smp() (p. 165)
within the startup library.
un.arm
L1_vaddr
Virtual address of the MMU level 1 page table used to map the kernel.
L1_paddr
Physical address of the MMU level 1 page table used to map the kernel.
startup_base
startup_size
cpu
page_flush
page_flush_deferred
smp
pminfo
The pminfo area is a communication area between the power manager and
startup/power callout.
The pminfo area contains the following elements which are customizable in the power
manager structure and are power-manager dependent:
Callout information
• coded in assembler
• position-independent
• no static read/write storage
Callouts are basically binding standalone pieces of code for the kernel to invoke without
having to statically link them to the kernel.
The requirement for coding the callouts in assembler stems from the second
requirement (i.e. that they must be written to be position-independent). This is because
the callouts are provided as part of the startup code, which will get overwritten when
the kernel starts up. In order to circumvent this, the startup program will copy the
callouts to a safe place — since they won't be in the location that they were loaded
in, they must be coded to be position-independent.
We need to qualify the last requirement (i.e. that callouts not use any static read/write
storage). There's a mechanism available for a given callout to allocate a small amount
of storage space within the system page, but the callouts cannot have any static
read/write storage space defined within themselves.
Debug interface
• display_char()
• poll_key()
• break_detect().
These three callouts are used by the kernel when it wishes to interact with a serial
port, console, or other device (e.g. when it needs to print out some internal debugging
information or when there's a fault). Only the display_char() is required; the others
are optional.
Clock/timer interface
• timer_load()
• timer_reload()
• timer_value().
The kernel uses these callouts to deal with the hardware timer chip.
The timer_load() callout is responsible for stuffing the divisor value passed by the
kernel into the timer/counter chip. Since the kernel doesn't know the characteristics
of the timer chip, it's up to the timer_load() callout to take the passed value and
validate it. The kernel will then use the new value in any internal calculations it
performs. You can access the new value in the qtime_entry element of the system
page as well as through the ClockPeriod() function call.
The timer_reload() callout is called after the timer chip generates an interrupt. It's
used in two cases:
• Reloading the divisor value (because some timer hardware doesn't have an automatic
reload on the timer chip — this type of hardware should be avoided if possible).
• Telling the kernel whether the timer chip caused the interrupt or not (e.g. if you
had multiple interrupt sources tied to the same line used by the timer — not the
ideal hardware design, but…).
The timer_value() callout is used to return the value of the timer chip's internal count
as a delta from the last interrupt. This is used on processors that don't have a
high-precision counter built into the CPU (e.g. 80486).
• mask()
• unmask()
• config()
• id
• eoi
The mask() and unmask() perform masking and unmasking of a particular interrupt
vector.
For more information about these callouts, refer to the intrinfo (p. 130) structure in the
system page above.
Depending on the cache controller circuitry in your system, you may need to provide
a callout for the kernel to interface to the cache controller.
On the x86 architecture, the cache controller is integrated tightly with the CPU,
meaning that the kernel doesn't have to talk to the cache controller. On other
architectures, the cache controllers need to be told to invalidate portions of the cache
when certain functions are performed in the kernel.
The callout for cache control is control(). This callout gets passed:
The callout is responsible for returning the number of cache lines that it affected —
this allows the caller (the kernel) to call the control() callout repeatedly at a higher
level. A return of 0 indicates that the entire cache was affected (e.g. all cache entries
were invalidated).
The miscellaneous callout, reboot(), gets called whenever the kernel needs to reboot
the machine.
The reboot() callout is responsible for resetting the system. This callout lets developers
customize the events that occur when proc needs to reboot — such as turning off a
watchdog, banging the right registers etc. without customizing proc each time.
A “shutdown” of the binary will call sysmgr_reboot(), which will eventually trigger the
reboot() callout.
The power() callout gets called whenever power management needs to be activated.
This callout is specific to the CPU and target.
Active or Running
Idle
The system isn't running applications; the CPU is halted. Code is all or
partially resident in memory.
Standby
The system isn't running applications; the CPU is halted. Code isn't resident
in memory.
Shutdown
Minimal or zero-power state. CPU, memory, and devices are all powered off.
These definitions are a guideline only; you can define multiple subsets for each state
(e.g. Idle1, Idle2, etc.). Furthermore, not all these CPU power modes may be required
or even possible for a specific board or CPU.
The startup library contains a rich set of routines consisting of high-level functions
that are called by your main() through to utility functions for interrogating the hardware,
initializing the system page, loading the next process in the image, and switching to
protected mode. Full source is provided for all these functions, allowing you to make
local copies with minor modifications in your target startup directory.
The available library functions include the following (in alphabetical order):
add_cache()
Add an entry to the cacheattr section of the system page structure. Parameters map
one-to-one with the structure's fields. The return value is the array index number of
the added entry. Note that if there's already an entry that matches the one you're trying
to add, that entry's index is returned — nothing new is added to the section.
add_callout()
Add a callout to the callout_info section of the system page. The offset parameter
holds the offset from the start of the section (as returned by the offsetof() macro) that
the new routine's address should be placed in.
add_callout_array()
Add the callout array specified by slots (for size bytes) into the callout array in the
system page.
add_interrupt()
struct intrinfo_entry
*add_interrupt(const struct startup_intrinfo
*startup_intr);
Add a new entry to the intrinfo section. Returns a pointer to the newly added entry.
add_interrupt_array()
Add the interrupt array callouts specified by intrs (for size bytes) into the interrupt
callout array in the system page.
add_ram()
Tell the system that there's RAM available starting at physical address start for size
bytes.
add_string()
Add the string specified by name into the string literal pool in the system page and
return the index.
add_typed_string()
Add the typed string specified by name (of type type_index) into the typed string literal
pool in the system page and return the index.
alloc_qtime()
Allocate space in the system page for the qtime section and fill in the epoch, boot_time,
and nsec_tod_adjust fields. Returns a pointer to the newly allocated structure so that
user code can fill in the other fields.
alloc_ram()
Allocate memory from the free memory pool initialized by the call to init_raminfo().
The RAM is not cleared.
armv_cache
struct armv_cache {
const struct arm_cache_config *dcache_config;
const struct callout_rtn *dcache_rtn;
const struct arm_cache_config *icache_config;
const struct callout_rtn *icache_rtn;
};
The armv_cache structure describes the CPU caches. The members include:
dcache_config
Describes the data cache. It's required only when a CPU doesn't implement
the CP15 cache-type register.
When a CPU does implement the CP15 cache-type register, set this to 0,
so that the startup library will use arm_add_cache() to determine the cache
register configuration based on the CP15 cache-type register.
dcache_rtn
icache_config
Describes the instruction cache. This is required only if the CPU doesn't
implement the CP15 cache type register. When a CPU does implement the
CP15 cache-type register, set this to 0, so that the startup library will use
arm_add_cache() to determine the cache register configuration based on
the CP15 cache-type register.
icache_rtn
armv_chip
struct armv_chip {
unsigned cpuid;
const char *name;
unsigned mmu_cr_set;
unsigned mmu_cr_clr;
int cycles;
const struct armv_cache *cache;
const struct callout_rtn *power;
const struct callout_rtn *flush;
const struct callout_rtn *deferred;
const struct armv_pte *pte;
const struct armv_pte *pte_wa;
const struct armv_pte *pte_wb;
const struct armv_pte *pte_wt;
void (*setup)(struct cpuinfo_entry *cpu, unsigned cpuid);
const struct armv_chip *(*detect)(void);
unsigned short ttb_attr;
unsigned short pte_attr;
};
The ARMv7 processors use the WFI instruction to enter “wait for interrupt”
mode.
cpuid
name
mmu_cr_set
Specifies which bits to set in the MMU control register when the MMU is
enabled in vstart().
mmu_cr_clr
Specifies which bits to clear in the MMU control register when the MMU is
enabled in vstart().
cycles
cache
power
The flush callout is used to flush the cache and TLB when unmapping a
page. This is called for each page in a region being unmapped.
The deferred callout is used after all pages in a region have been unmapped,
and can be used to perform any actions that the flush callout didn't perform.
For example, if the MMU doesn't support flushing the instruction cache by
virtual address, the deferred callout can be used to flush the instruction
cache after all pages have been unmapped, to reduce the cost of flushing.
pte
pte_wa
If you specify the -wa option, the pte_wa configuration is used. If the CPU
doesn't support write-allocate caching, set pte_wa to 0, and the default pte
values will be used instead.
pte_wb
If you specify the -wb compile option, the pte_wb configuration is used. If
the CPU doesn't support write-back caching, set pte_wb to 0, and the default
pte values will be used instead.
pte_wt
If you specify the -wt compile option, the pte_wt configuration is used. If
the CPU doesn't support write-through caching, set pte_wt to 0, and the
default pte values will be used instead.
setup
detect
A pointer to a function that checks for various configurations for Cortex A-8
and Cortex A-9 processors.
ttb_attr
pte_attr
Cacheability attributes for page table mappings used by the memory manager
to manipulate L1/L2 page table entries.
armv_chip_detect()
The armv_chip_detect() function checks for various configurations for Cortex A-8 and
Cortex A-9 processors.
This function checks for a NULL name and non-NULL detect function to invoke the
CPU-specific detect function that returns the appropriate armv_chip().
armv_pte
struct armv_pte {
unsigned short upte_ro;
unsigned short upte_rw;
unsigned short kpte_ro;
unsigned short kpte_rw;
unsigned short mask_nc;
unsigned short l1_pgtable;
unsigned kscn_ro;
unsigned kscn_rw;
unsigned kscn_cb;
};
The armv_pte structure describes the MMU page table encodings. Its members
include:
upte_ro
upte_rw
kpte_ro
kpte_rw
mask_nc
Non-cacheable mappings.
l1_pgtable
kscn_ro
kscn_rw
kscn_cb
armv_setup_v7()
For ARMv7, there is a generic function, armv_setup_v7(), that performs generic ARMv7
initialization:
• checks for VFP (vector floating point) functionality and sets the CPU_FLAG_FPU,
if necessary
• sets up the MMU for the ARMv7 variant of procnto
The armv_setup_v7() function must be called by any CPU-specific setup function for
an ARMv7 CPU after it has performed its CPU-specific actions.
as_add()
Add an entry to the asinfo section of the system page. Parameters map one-to-one
with field names. Returns the offset from the start of the section for the new entry.
For more information and an example, see “Typed memory” in the Interprocess
Communication (IPC) chapter of the System Architecture guide.
as_add_containing()
Add new entries to the asinfo section, with the owner field set to whatever entries are
named by the string pointed to by container. This function can add multiple entries
because the start and end values are constrained to stay within the start and end of
the containing entry (e.g. they get clipped such that they don't go outside the parent).
If more than one entry is added, the AS_ATTR_CONTINUED bit will be turned on in
all but the last. Returns the offset from the start of the section for the first entry added.
For more information and an example, see “Typed memory” in the Interprocess
Communication (IPC) chapter of the System Architecture guide.
as_default()
unsigned as_default(void);
Add the default memory and io entries to the asinfo section of the system page.
as_find()
The start parameter indicates where to start the search for the given item. For an initial
call, it should be set to AS_NULL_OFF. If the item found isn't the one wanted, then
the return value from the first as_find_item() is used as the start parameter of the
second call. The search will pick up where it left off. This can be repeated as many
times as required (the return value from the second call going into the start parameter
of the third, etc). The item being searched is identified by a sequence of char *
parameters following start. The sequence is terminated by a NULL. The last string
before the NULL is the bottom-level itemname being searched for, the string in front
of that is the name of the item that owns the bottom-level item, etc.
For example, this call finds the first occurrence of an item called “foobar”:
The following call finds the first occurrence of an item called “foobar” that's owned
by “sam”:
as_find_containing()
Find an asinfo entry with the name pointed to by container that at least partially covers
the range given by start and end. Follows the same rules as as_find() to know where
the search starts. Returns the offset of the matching entry or AS_NULL_OFF if none
is found. (The as_add_containing() function uses this to find what the owner fields
should be for the entries it's adding.)
as_info2off()
Given a pointer to an asinfo entry, return the offset from the start of the section.
as_off2info()
Given an offset from the start of the asinfo section, return a pointer to the entry.
as_set_checker()
Set the checker callout field of the indicated asinfo entry. If the AS_ATTR_CONTINUED
bit is on in the entry, advance to the next entry in the section and set its priority as
well (see as_add_containing() (p. 152) for why AS_ATTR_CONTINUED would be on).
Repeat until an entry without AS_ATTR_CONTINUED is found.
as_set_priority()
Set the priority field of the indicated entry. If the AS_ATTR_CONTINUED bit is on in
the entry, advance to the next entry in the section and set its priority as well (see
as_add_containing() (p. 152) for why AS_ATTR_CONTINUED would be on). Repeat
until an entry without AS_ATTR_CONTINUED is found.
avoid_ram()
Make startup avoid using the specified RAM for any of its internal allocations. Memory
remains available for procnto to use. This function is useful for specifying RAM that
the IPL/ROM monitor needs to keep intact while startup runs. Because it takes only
a paddr32_t, addresses can be specified in the first 4 GB. It doesn't need a full
paddr_t because startup will never use memory above 4 GB for its own storage
requirements.
calc_time_t()
Given a struct tm (with values appropriate for the UTC timezone), calculate the
value to be placed in the boot_time field of the qtime section.
calloc_ram()
Allocate memory from the free memory pool initialized by the call to init_raminfo().
The RAM is cleared.
callout_io_map(), callout_io_map_indirect()
The return value is for use in the CPU's equivalent of in/out instructions (regular moves
on all but the x86). The value is for use in any kernel callouts (i.e. they live beyond
the end of the startup program and are maintained by the OS while running).
callout_memory_map(), callout_memory_map_indirect()
callout_register_data()
This function lets you associate a pointer to arbitrary data with a callout. This data
pointer is passed to the patcher routine (see “Patching the callout code (p. 175),”
below.
The rp argument is a pointer to the pointer where the callout address is stored in the
system page you're building. For example, say you have a pointer to a system page
section that you're working on called foo. In the section there's a field bar that points
to a callout when the system page is finished. Here's the code:
callout_register_data(&foo->bar, &some_interesting_data_for_patcher);
When the patcher is called to fix up the callout that's pointed at by foo->bar,
&some_interesting_data_for_patcher is passed to it.
chip_access()
Get access to a hardware chip at physical address base with a register shift value of
reg_shift (0 if registers are one byte apart; 1 if registers are two bytes apart, etc. See
devc-ser8250 for more information).
chip_done()
void chip_done(void);
chip_read8()
Read one byte from the device specified by chip_access(). The off parameter is first
scaled by the reg_shift value specified in chip_access() before being used.
chip_read16()
chip_read32()
chip_write8()
Write one byte from the device specified by chip_access(). The off parameter is first
scaled by the reg_shift value specified in chip_access() before being used.
chip_write16()
chip_write32()
copy_memory()
del_typed_string()
Find the string in the typed_strings section of the system page indicated by the type
type_index and remove it. Returns the offset where the removed string was, or -1 if
no such string was present.
falcon_init_l2_cache()
Enable the L2 cache on a board with a Falcon system controller chip. The base physical
address of the Falcon controller registers are given by base.
falcon_init_raminfo()
On a system with the Falcon system controller chip located at falcon_base, determine
how much/where RAM is installed and call add_ram() (p. 146) with the appropriate
parameters.
falcon_system_clock()
On a system with a Falcon chipset located at physical address falcon_base, return the
speed of the main clock input to the CPU (in Hertz). This can then be used in turn to
set the cpu_freq, timer_freq, and cycles_freq variables.
find_startup_info()
Attempt to locate the kind of information specified by type in the data area used by
the IPL code to communicate such information. Pass start as NULL to find the first
occurrence of the given type of information. Pass start as the return value from a
previous call in order to get the next information of that type. Returns 0 if no
information of that type is found starting from start.
find_typed_string()
Return the offset from the beginning of the type_strings section of the string with the
type_index type. Return -1 if no such string is present.
handle_common_option()
Take the option identified by opt (a single ASCII character) and process it. This function
assumes that the global variable optarg points to the argument string for the option.
Reboot switch. If set, an OS crash will cause the system to reboot. If not
set, an OS crash will cause the system to hang.
f [cpu_freq][,[cycles_freq][,timer_freq]]
• cpu_freq — the CPU clock frequency. Also sets the speed field in the
cpuinfo section of the system page.
• cycles_freq — the frequency at which the value returned by ClockCycles()
increments. Also sets the cycles_per_sec field in the qtime section of
the system page.
• timer_freq — the frequency at which the timer chip input runs. Also sets
the timer_rate and timer_scale values of the qtime section of the system
page.
Add the hostname specified to the typed name string space under the
identifier _CS_HOSTNAME.
hwi_add_device()
Add an hwi_device item to the hwinfo section. The bus and class parameters are used
to locate where in the device tree the new device is placed.
hwi_add_inputclk()
hwi_add_irq()
Add an irq tag structure to the hwinfo section. The logical vector number for the
interrupt will be set to vector.
hwi_add_location()
Add a location tag structure to the hwinfo section. The fields of the structure will be
set to the given parameters.
hwi_add_nicaddr()
hwi_add_rtc()
Add an hwi_device item describing the realtime clock to the hwinfo section. The name
of the device is name. The hwi_location tag items are given by base, reg_shift, len,
and mmap. The mmap parameter indicates if the device is memory-mapped or
I/O-space-mapped and is used to set the addrspace field.
If the cent_reg parameter is not -1, it's used to add an hwi_regname tag with the offset
field set to its value. This indicates the offset from the start of the device where the
century byte is stored.
hwi_alloc_item()
hwi_alloc_tag()
hwi_find_as()
Given a physical address of base and mmap (indicating 1 for memory-mapped and 0
for I/O-space-mapped), return the offset from the start of the asinfo section indicating
the appropriate addrspace field value for an hwi_location tag.
hwi_find_item()
Search for a given item in the hwinfo section of the system page. If start is
HWI_NULL_OFF, the search begins at the start of the hwinfo section. If not, it starts
from the item after the offset of the one passed in (this allows people to find multiple
tags of the same type; it works just like the find_startup_info() function). The var args
portion is a list of character pointers, giving item names; the list is terminated with a
NULL. The order of the item names gives ownership information. For example:
also searches for “foobar,” but this time it has to be owned by an item called
“mumblyshwartz.”
If the item can't be found, HWI_NULL_OFF is returned; otherwise, the byte offset
within the hwinfo section is returned.
hwi_find_tag()
Search for a given tagname in the hwinfo section of startup. The start parameter works
just like in hwi_find_item(). If curr_item is nonzero, the tagname must occur within
the current item. If zero, the tagname can occur anywhere from the starting point of
the search to the end of the section. If the tag can't be found, then HWI_NULL_OFF
is returned; otherwise, the byte offset within the hwinfo section is returned.
hwi_off2tag()
Given a byte offset from the start of the hwinfo section, return a pointer to the hwinfo
tag structure.
hwi_tag2off()
Given a pointer to the start of a hwinfo tag instruction, convert it to a byte offset from
the start of the hwinfo system page section.
init_asinfo()
Initialize the asinfo section of the system page. The mem parameter is the offset of
the memory entry in the section and can be used as the owner parameter value for
as_add() (p. 152)s that are adding memory.
init_cacheattr()
Initialize the cacheattr (p. 124) member. For all platforms, this is a do-nothing stub.
init_cpuinfo()
Initialize the members of the cpuinfo (p. 122) structure with information about the
installed CPU(s) and related capabilities. Most systems will be able to use this function
directly from the library.
init_hwinfo()
Initialize the appropriate variant of the hwinfo structure in the system page.
init_intrinfo()
x86
You would need to change this only if your hardware doesn't have the
standard PC-compatible dual 8259 configuration.
ARM
• the interrupt controller hardware as appropriate (e.g. on the x86 it should program
the two 8259 interrupt controllers)
• the intrinfo structure with the details of the interrupt controller hardware.
This initialization of the structure is done via a call to the function add_interrupt_array()
(p. 146).
init_mmu()
Sets up the processor for virtual addressing mode by setting up page-mapping hardware
and enabling the pager.
On the x86 family, it sets up the page tables as well as special mappings to “known”
physical address ranges (e.g. sets up a virtual address for the physical address ranges
0 through 0xFFFFF inclusive).
On the ARM family, this function simply sets up the page tables.
init_pminfo()
Initialize the pminfo section of the system page and set the number of elements in
the managed storage array.
init_qtime()
Initialize the qtime (p. 127) structure in the system page. Most systems will be able to
use this function directly from the library.
This function doesn't exist for ARM. Specific functions exist for ARM processors with
on-chip timers; currently, this includes only init_qtime_sa1100().
init_qtime_sa1100()
Initialize the qtime (p. 127) structure and kernel callouts in the system page to use the
on-chip timer for the SA1100 and SA1110 processors.
init_raminfo()
Determine the location and size of available system RAM and initialize the asinfo (p.
113) structure in the system page.
If you know the exact amount and location of RAM in your system, you can replace
this library function with one that simply hard-codes the values via one or more
add_ram() (p. 146) calls.
x86
If the RAM configuration is known (e.g. set by the IPL code, or the multi-boot
IPL code gets set by the gnu utility), then the library version of init_raminfo()
will call the library routine find_startup_info() (p. 158) to fetch the information
from a known location in memory. If the RAM configuration isn't known,
then a RAM scan (via x86_scanmem() (p. 172)) is performed looking for valid
memory between locations 0 and 0xFFFFFF, inclusive. (Note that the VGA
aperture that usually starts at location 0xB0000 is specifically ignored.)
ARM
There's no library default. You must supply your own init_raminfo() function.
init_smp()
Initialize the SMP functionality of the system, assuming the hardware (e.g. x86)
supports SMP.
init_syspage_memory() (deprecated)
Initialize the system page structure's individual member pointers to point to the data
areas for the system page substructures (e.g. typed_strings). The base parameter is a
pointer to where the system page is currently stored (it will be moved to the kernel's
address space later); the size indicates how big this area is. On all platforms, this
routine shouldn't require modification.
init_system_private()
Find all the boot images that need to be started and fill a structure with that
information; parse any -M options used to specify memory regions that should be
added; tell QNX Neutrino where the image filesystem is located; and finally allocate
room for the actual storage of the system page. On all platforms, this shouldn't require
modification.
jtag_reserve_memory()
kprintf()
Display output using the put_char() function you provide. It supports a very limited
set of printf() style formats.
openbios_init_raminfo()
void openbios_init_raminfo(void);
On a system that contains an OpenBIOS ROM monitor, add the system RAM
information.
pcnet_reset()
Ensure that a PCnet-style Ethernet controller chip at the given physical address (either
I/O or memory-mapped as specified by mmap) is disabled. Some ROM monitors leave
the Ethernet receiver enabled after downloading the OS image. This causes memory
to be corrupted after the system starts and before QNX Neutrino's Ethernet driver is
run, due to the reception of broadcast packets. This function makes sure that no
further packets are received by the chip until the QNX Neutrino driver starts up and
properly initializes it.
print_syspage()
Print the contents of all the structures in the system page. The global variable
debug_level is used to determine what gets printed. The debug_level must be at least
2 to print anything; a debug_level of 3 will print the information within the individual
substructures.
Note that you can set the debug level at the command line by specifying multiple -v
options to the startup program.
You can also use the startup program's -S command-line option to select which entries
are printed from the system page: -Sname selects name to be printed, whereas -S~name
disables name from being printed. The name can be selected from the following list:
rtc_time()
x86
ARM
rtc_time_ds1386()
rtc_time_m48t5x()
rtc_time_mc146818()
rtc_time_rtc72423()
There's also a “none” version to use if your board doesn't have RTC hardware:
If you're supplying the rtc_time() routine, you should call one of the chip-specific
routines or write your own. The chip-specific routines all share the same parameter
list:
The base parameter indicates the physical base address or I/O port of the device. The
reg_shift indicates the register offset as a power of two.
0
A typical value would be 0 (meaning 2 , i.e. 1), indicating that the registers of the
device are one byte apart in the address space. As another example, a value of 2
2
(meaning 2 , i.e. 4) indicates that the registers in the device are four bytes apart.
If the mmap variable is 0, then the device is in I/O space. If mmap is 1, then the
device is in memory space.
Finally, cent_reg indicates which register in the device contains the century byte (-1
indicates no such register). If there's no century byte register, then the behavior is
chip-specific. If the chip is year 2000-compliant, then we will get the correct time.
If the chip isn't compliant, then if the year is less than 70, we assume it's in the range
2000 to 2069; else we assume it's in the range 1970 to 1999.
startup_io_map()
startup_io_unmap()
startup_memory_map()
startup_memory_unmap()
tulip_reset()
Ensure that a Tulip Ethernet chip (Digital 21x4x) at the given physical address (either
I/O or memory-mapped as specified by mem_mapped) is disabled. Some ROM monitors
leave the Ethernet receiver enabled after downloading the OS image. This causes
memory to be corrupted after the system starts and before QNX Neutrino's Ethernet
driver is run, due to the reception of broadcast packets. This function makes sure that
no further packets are received by the chip until the QNX Neutrino driver starts up
and properly initializes it.
uncompress()
This function resides in the startup library and is responsible for expanding a
compressed OS image out to full size (this is invoked before main() gets called). If
you know you're never going to be given a compressed image, you can replace this
function with a stub version in your own code and thus make a smaller startup program.
x86_cpuid_string()
Place a string representation of the CPU in the string buf to a maximum of max
characters. The general format of the string is:
This information is determined using the cpuid instruction. If it's not supported, then
a subset (typically only the part) will be placed in the buffer (e.g. 486).
x86_cputype()
An x86 platform-only function that determines the type of CPU and returns the number
(e.g. 486).
x86_enable_a20()
Enable address line A20, which is often disabled on many PCs on reset. It first checks
if address line A20 is enabled and if so returns 0. Otherwise, it sets bit 0x02 in port
0x92, which is used by many systems as a fast A20 enable. It again checks to see if
A20 is enabled and if so returns 0. Otherwise, it uses the keyboard microcontroller to
enable A20 as defined by the old PC/AT standard. It again checks to see if A20 is
enabled and if so returns 0. Otherwise, it returns -1.
If cpu is a 486 or greater, it issues a wbinvd opcode to invalidate the cache when
doing a read/write test of memory to see if A20 is enabled.
In the rare case where setting bit 0x02 in port 0x92 may affect other hardware, you
can skip this by setting only_keyboard to 1. In this case, it will attempt to use only
the keyboard microcontroller.
x86_fputype()
An x86-only function that returns the FPU type number (e.g. 387).
x86_init_pcbios()
void x86_init_pcbios(void);
x86_pcbios_shadow_rom()
Given the physical address of a ROM BIOS extension, this function makes a copy of
the ROM in a RAM location and sets the x86 page tables in the _syspage_ptr-
>un.x86.real_addr range to refer to the RAM copy rather than the ROM version.
When something runs in V86 mode, it'll use the RAM locations when accessing the
memory.
The amount of ROM shadowed is the maximum of the size parameter and the size
indicated by the third byte of the BIOS extension.
if you're starting the system in physical mode and there's no MMU to make
a RAM copy be referenced
if everything works.
x86_scanmem()
An x86-only function that scans memory between beg and end looking for RAM, and
returns the total amount of RAM found. It scans memory performing a R/W test of 3
values at the start of each 4 KB page. Each page is marked with a unique value. It
then rescans the memory looking for contiguous areas of memory and adds them to
the asinfo (p. 113) entry in the system page.
A special check is made for a block of memory between addresses 0xB0000 and
0xBFFFF, inclusive. If memory is found there, the block is skipped (since it's probably
the dual-ported memory of a VGA card).
The call x86_scanmem (0, 0xFFFFFF) would locate all memory in the first 16
megabytes of memory (except VGA memory). You may make multiple calls to
x86_scanmem() to different areas of memory in order to step over known areas of
dual-ported memory with hardware.
In order for the microkernel to work on all boards, all hardware-dependent operations
have been factored out of the code. Known as kernel callouts, these routines must be
provided by the startup program.
The startup can actually have a number of different versions of the same callout
available — during hardware discovery it can determine which one is appropriate for
the board it's running on and make that particular instance of the callout available to
the kernel. Alternatively, if you're on a deeply embedded system and the startup knows
exactly what hardware is present, only one of each callout might be present; the startup
program simply tells the kernel about them with no discovery process.
The callout code is copied from the startup program into the system page and after
this, the startup memory (text and data) is freed.
The patch code is run during execution of the startup program itself, so regular calls
work as normal.
Once copied, your code must be completely self-contained and position independent.
The purpose of the patch routines is to allow you to patch up the code with constants,
access to RW data storage etc. so that your code is self-contained and contains all
the virtual-physical mappings required.
The startup library provides a number of different callout routines that we've already
written. You should check the source tree (originally installed in
bsp_working_dir/src/hardware/startup/lib/) to see if a routine for your
device/board is already available before embarking on the odyssey of writing your own.
This directory includes generic code, as well as processor-specific directories.
In the CPU-dependent level of the tree for all the source files, look for files that match
the pattern:
callout_*.[sS]
Those are all the callouts provided by the library. Whether a file ends in .s or .S
depends on whether it's sent through the C preprocessor before being handed off to
an assembler. For our purposes here, we'll simply refer to them as .s files.
callout_category_device.s
cache
debug
interrupt
timer
reboot
The device identifies the unique hardware that the callouts are for. Typically, all the
routines in a particular source file would be used (or not) as a group by the kernel.
For example, the callout_debug_8250.s file contains the display_char_8250(),
poll_key_8250(), and break_detect_8250() routines for dealing with an 8250-style
UART chip.
Since the memory used by the startup executable is reclaimed by the OS after startup
has finished, the callouts that are selected for use by the kernel can't be used in place.
Instead, they must be copied to a safe location (the library takes care of this for you).
Therefore, the callout code must be completely position-independent, which is why
callouts have to be written in assembly language. We need to know where the callout
begins and where it ends; there isn't a portable way to tell where a C function ends.
The other issue is that there isn't a portable way to control the preamble/postamble
creation or code generation. So if an ABI change occurs or a build configuration issue
occurs, we could have a very latent bug.
For all but two of the routines, the kernel invokes the callouts with the normal
function-calling conventions. Later we'll deal with the two exceptions (interrupt_id()
and interrupt_eoi()).
Starting off
Find a callout source file of the appropriate category that's close to what you want and
copy it to a new filename. If the new routines will be useful on more than one board,
you might want to keep the source file in your own private copy of the startup library.
If not, you can just copy to the directory where you've put your board-specific files.
Now edit the new source file. At the top you'll see something that looks like this:
#include "callout.ah"
Or:
.include "callout.ah"
This include file defines the CALLOUT_START and CALLOUT_END macros. The
CALLOUT_START macro takes three parameters and marks the start of one callout.
The first parameter is the name of the callout routine (we'll come back to the two
remaining parameters later).
The CALLOUT_END macro indicates the end of the callout routine source. It takes
one parameter, which has to be the same as the first parameter in the preceding
CALLOUT_START. If this particular routine is selected to be used by the kernel, the
startup library will copy the code between the CALLOUT_START and CALLOUT_END
to a safe place for the kernel to use. The exact syntax of the two macros depends on
exactly which assembler is being used on the source. Two common versions are:
CALLOUT_START(timer_load_8254, 0, 0)
CALLOUT_END(timer_load_8254)
Or:
CALLOUT_START timer_load_8254, 0, 0
CALLOUT_END timer_load_8254
Just keep whatever syntax is being used by the original file you started from. The
original file will also have C prototypes for the routines as comments, so you'll know
what parameters are being passed in. Now you should replace the code from the
original file with what will work for the new device you're dealing with.
You may need to write a callout that deals with a device that may appear in different
locations on different boards. You can do this by “patching” the callout code as it is
copied to its final position. The third parameter of the CALLOUT_START macro is
either a zero or the address of a patcher() routine. This routine has the following
prototype:
This routine is invoked immediately after the callout has been copied to its final resting
place. The parameters are as follows:
paddr
vaddr
Virtual address of the system page that allows read/write access (usable only
by the kernel).
rtn_offset
Offset from the beginning of the system page to the start of the callout's
code.
rw_offset
See the section on “Getting some R/W storage (p. 177)” below.
data
src
The data and src arguments were added in the QNX Neutrino Core OS 6.3.2.
Earlier patcher functions can ignore them.
patch_debug_8250:
movl 0x4(%esp),%eax // get paddr of routine
addl 0xc(%esp),%eax // ...
movl 0x14(%esp),%edx // get base info
CALLOUT_START(display_char_8250, 0, patch_debug_8250)
movl $0x12345678,%edx // get serial port base (patched)
movl $0x12345678,%ecx // get serial port shift (patched)
....
CALLOUT_END(display_char_8250)
Your callouts may need to have access to some static read/write storage. Normally this
wouldn't be possible because of the position-independent requirements of a callout.
But you can do it by using the patcher routines and the second parameter to
CALLOUT_START. The second parameter to CALLOUT_START is the address of a
four-byte variable that contains the amount of read/write storage the callout needs.
For example:
rw_interrupt:
.long 4
patch_interrupt:
add a1,a1,a2
j ra
sh a3,0+LOW16(a1)
/*
* Mask the specified interrupt
*/
CALLOUT_START(interrupt_mask_my_board, rw_interrupt, patch_interrupt)
/*
* Input Parameters :
* a0 - syspage_ptr
* a1 - Interrupt Number
* Returns:
* v0 - error status
*/
...
CALLOUT_END(interrupt_mask_my_board)
The rw_interrupt address as the second parameter tells the startup library that the
routine needs four bytes of read/write storage (since the contents at that location is a
4). The startup library allocates space at the end of the system page and passes the
offset to it as the rw_offset parameter of the patcher routine. The patcher routine then
modifies the initial instruction of the callout to the appropriate offset. While the callout
is executing, the t3 register will contain a pointer to the read/write storage. The
question you're undoubtedly asking at this point is: Why is the CALLOUT_START
parameter the address of a location containing the amount of storage? Why not just
pass the amount of storage directly?
That's a fair question. It's all part of a clever plan. A group of related callouts may
want to have access to shared storage so that they can pass information among
themselves. The library passes the same rw_offset value to the patcher routine for all
routines that share the same address as the second parameter to CALLOUT_START.
In other words:
will all get the same rw_offset parameter value passed to patch_interrupt() and thus
will share the same read/write storage.
To clean up a final point, the interrupt_id() and interrupt_eoi() routines aren't called
as normal routines. Instead, for performance reasons, the kernel intermixes these
routines directly with kernel code — the normal function-calling conventions aren't
followed. The callout_interrupt_*.s files in the startup library will have a
description of what registers are used to pass values into and out of these callouts for
your particular CPU. Note also that you can't return from the middle of the routine as
you normally would. Instead, you're required to “fall off the end” of the code.
Introduction
The QNX Neutrino RTOS ships with a small number of prebuilt flash filesystem drivers
for particular embedded systems. For the currently available drivers, look in the
${QNX_TARGET}/${PROCESSOR}/sbin directory. The flash filesystem drivers are
named devf-system, where system is derived from the name of the embedded system.
You'll find a general description of the flash filesystem in the System Architecture
book and descriptions of all the flash filesystem drivers in the Utilities Reference.
If a driver isn't provided for your particular target embedded system, you should first
try our “generic” driver (devf-generic). This driver often — but not always — works
with standard flash hardware. The driver assumes a supported memory technology
driver (MTD) and linear memory addressing.
If none of our drivers works for your hardware, you'll need to build your own driver.
We provide all the source code needed for you to customize a flash filesystem driver
for your target. After installation, look in the
bsp_working_dir/src/hardware/flash/boards directory — you'll find a
subdirectory for each board we support.
Besides the boards directory, you should also refer to the following sources to find
out what boards/drivers we currently support:
• QNX Neutrino docs (BSP docs as well as devf-* entries in Utilities Reference)
• the Community area of our website, www.qnx.com
Note that we currently support customizing a driver only for embedded systems with
onboard flash memory (also called a resident flash array or RFA). If you need support
for removable media like PCMCIA or compact or miniature memory cards, then please
contact us.
Driver structure
When customizing the flash filesystem driver for your system, you'll be modifying the
main() routine for the flash filesystem and providing an implementation of the socket
services component. The other components are supplied as libraries to link into the
driver.
Applications
/fs0p0 /dev/fs0p0
dispatch*,
resmgr*, iofunc*
Flash filesystem
Flash Probe
services routine
Socket services
Hardware
Like all QNX Neutrino device managers, the flash filesystem uses the standard
resmgr/iofunc interface and accepts the standard set of resource manager messages.
The flash filesystem turns these messages into read, write, and erase operations on
the underlying flash devices.
For example, an open message would result in code being executed that would read
the necessary filesystem data structures on the flash device and locate the requested
file. A subsequent write message will modify the contents of the file on flash. Special
functions, such as erasing the flash device, are implemented using devctl messages.
The flash filesystem itself is the “personality” component of the flash filesystem driver.
The filesystem contains all the code to process filesystem requests and to manage the
filesystem on the flash devices. The socket and flash services components are used
by the flash filesystem to access the flash devices.
The code for the flash filesystem component is platform-independent and is provided
in the libfs-flash3.a library.
Before reading/writing the flash device, other components will use socket services to
make sure the required address range can be accessed. On systems where the flash
device is linearly mapped into the processor address space, addressability is trivial.
On systems where the flash is either bank-switched or hidden behind some other
interface (such as PCMCIA), addressability is more complicated.
The socket services component is the one that will require the most customization for
your system.
The flash services component contains the device-specific code required to write and
erase particular flash devices. This component is also called the memory technology
driver (MTD).
The probe routine uses a special algorithm to estimate the size of the flash array.
Since the source code for the probe routine is available, you should be able to readily
identify any failures in the sizing algorithm.
Before you start customizing your own flash filesystem driver, you should examine the
source of all the sample drivers supplied. Most likely, one of the existing drivers can
be easily customized to support your system. If not, the devf-ram source provides
a good template to start with.
bsp_working_dir /src/hardware
boards mtd-flash
Pathname Description
For example, to create a driver called myboard based on an existing board, you would:
cd bsp_working_dir/hardware/flash/boards
mkdir myboard
cp -cRv existing_board myboard
cd myboard
make clean
The copy command (cp) specifies a recursive copy (the -R option). This will copy all
files from the specified source directory including the subdirectory indicating which
CPU this driver should be built for. For example, if the existing_board directory has a
arm subdirectory, then the new driver (myboard in our example) will be built for ARM.
The Makefile
When you go to build your new flash filesystem driver, you don't need to change the
Makefile. Our recursive makefile structure ensures you're linking to the appropriate
libraries.
The main() function for the driver, which you'll find in the main.c file in the sample
directories, is the first thing that needs to be modified for your system. Let's look at
the main.c file for the 800FADS board example:
/*
** File: main.c for 800FADS board
*/
#include <sys/f3s_mtd.h>
#include "f3s_800fads.h"
/* v1 Read/Write/Erase/Suspend/Resume/Sync (Unused) */
NULL, NULL, NULL, NULL, NULL, NULL,
f3s_a29f040_v2write, /* v2 Write */
f3s_a29f040_v2erase, /* v2 Erase */
f3s_a29f040_v2suspend, /* v2 Suspend */
f3s_a29f040_v2resume, /* v2 Resume */
f3s_a29f040_v2sync, /* v2 Sync */
{
/* mandatory last entry */
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
}
};
/* init f3s */
f3s_init(argc, argv, flash);
/* start f3s */
error = f3s_start(service, flash);
return error;
}
The flash array contains one or more f3s_flash_t structures, depending on how
many different types of flash device your driver has to support. The f3s_flash_t
structure, defined in <fs/f3s_flash.h>, contains function pointers to the flash
services routines.
The f3s_init() and f3s_start() functions are defined in the <fs/f3s_api.h> header
file.
f3s_init()
This function passes the command-line arguments to the flash filesystem component,
which then initializes itself.
f3s_start()
This function passes the service and flash arrays to the filesystem component so it
can make calls to the socket and flash services, and then starts the driver. This function
returns only when the driver is about to exit.
• the socket services functions for each socket in the service array
• the flash services functions for each flash device in the flash array.
If you have a system with only one socket consisting of the same flash devices, then
there will be only a single entry in each array.
f3s_open()
f3s_page()
This function is called to access a window_size sized window at address offset from
the start of the device; it must be provided for both bank-switched and linearly mapped
flash devices. If the size parameter is non-NULL, you should set it to the size of the
window. The function must return a pointer suitable for accessing the device at address
offset. On error, it should return NULL and set errno to ERANGE.
f3s_status()
This function is called to get the socket status. It's used currently only for interfaces
that support dynamic insertion and removal. For onboard flash, you should simply
return EOK.
f3s_close()
This function is called to close the socket. If you need to, you can disable the flash
device and remove any programming voltage, etc.
The following flags are defined for the flags parameter in the socket functions:
F3S_POWER_VCC
F3S_POWER_VPP
F3S_OPER_SOCKET
F3S_OPER_WINDOW
The socket parameter is used for passing arguments and returning results from the
socket services and for storing information about each socket. To handle complex
interfaces such as PCMCIA, the structure has been defined so that there can be more
than one socket; each socket can have more than one window. A simple linear flash
array would have a single socket and no windows.
/*
* these fields are initialized by the socket services and later
* referenced by the flash file system
*/
uint8_t *name; /* name of driver */
_Paddr64t address; /* physical address 0 for allocated */
uint32_t window_size; /* size of window power of 2 mandatory */
uint32_t array_offset; /* offset of array 0 for based */
uint32_t array_size; /* size of array 0 for window_size */
uint32_t unit_size; /* size of unit 0 for probed */
uint32_t flags; /* flags for capabilities */
uint16_t bus_width; /* width of bus */
uint16_t window_num; /* number of windows 0 for not windowed */
/*
* these fields are initialized by the socket services and later
* referenced by the socket services
*/
uint8_t* memory; /* access pointer for window memory */
void *socket_handle; /* socket handle pointer for external
library */
void *window_handle; /* window handle pointer for external
library */
/*
* this field is modified by the socket services as different window
* pages are selected
*/
uint32_t window_offset; /* offset of window */
}
f3s_socket_t;
option
socket_index
Current socket.
window_index
Current window.
name
address
window_size
array_size
unit_size
flags
bus_width
window_num
memory
Free for use by socket services; usually stores current window address.
socket_handle
Free for use by socket services; usually stores pointer to any extra data for
socket.
window_handle
Free for use by socket services; usually stores pointer to any extra data for
window.
window_offset
Options parsing
The socket services should parse any applicable options before initializing the flash
devices in the f3s_open() function. Two support functions are provided for this:
f3s_socket_option()
Parse the driver command-line options that apply to the socket services.
where:
baseaddress
windowsize
arrayoffset
arraysize
buswidth
interleave
Number of physical chips interleaved to form a larger logical chip (e.g. two
16-bit chips interleaved to form a 32-bit logical chip).
f3s_socket_syspage()
The syspage options allow the socket services to get any information about the flash
devices in the system that is collected by the startup program and stored in the syspage.
See the chapter on Customizing Image Startup Programs (p. 109) for more information.
The flash services interface, defined in the <fs/f3s_flash.h> header file, consists
of the following functions:
The values for the flags parameter are defined in <fs/s3s_flash.h>. The
most important one is F3S_VERIFY_WRITE. If this is set, the routine must
perform a read-back verification after the write as a double check that the write
succeeded. Occasionally, however, the hardware reports success even when
the write didn't work as expected.
f3s_ident()
Identifies the flash device at address text_offset and fills in the dbase structure with
information about the device type and geometry.
f3s_reset()
Resets the flash device at address text_offset into the default read-mode after calling
the fs3_ident() function or after a device error.
f3s_v2read()
This optional function is called to read buffer_size bytes from address text_offset into
buffer. Normally the flash devices will be read directly via memcpy().
On success, it should return the number of bytes read. If an error occurs, it should
return -1 with errno set to one of the following:
EIO
Recoverable I/O error (e.g. failed due to low power, but corruption is localized
and block will be usable after erasing).
EFAULT
EINVAL
ERANGE
ENODEV
ESHUTDOWN
f3s_v2write()
On success, it should return the number of bytes written. If an error occurs, it should
return -1 with errno set to one of the following:
EIO
Recoverable I/O error (e.g. failed due to low power or write failed, but
corruption is localized and block will be usable after erasing).
EFAULT
EROFS
EINVAL
ERANGE
ENODEV
ESHUTDOWN
f3s_v2erase()
This function begins erasing the flash block containing the text_offset. It can optionally
determine if an error has already occurred, or it can just return EOK and let f3s_v2sync()
detect any error.
On success, it should return EOK. If an error occurs, it should return one of the
following:
EIO
Recoverable I/O error (e.g. failed due to low power or erase failed, but
corruption is localized and block will be usable after an erase)
EFAULT
EROFS
EINVAL
EBUSY
ERANGE
ENODEV
ESHUTDOWN
f3s_v2suspend()
This function suspends an erase operation, when supported, for a read or for a write.
On success, it should return EOK. If an error occurs, it should return one of the
following:
EIO
Recoverable I/O error (e.g. failed due to low power or erase failed, but
corruption is localized and block will be usable after erasing).
EFAULT
EINVAL
ECANCELED
ERANGE
ENODEV
ESHUTDOWN
f3s_v2resume()
This function resumes an erase operation after a suspend command has been issued.
On success, it should return EOK. If an error occurs, it should return one of the
following:
EIO
Recoverable I/O error (e.g. failed due to low power or erase failed, but
corruption is localized and block will be usable after erasing).
EFAULT
EINVAL
ERANGE
ENODEV
ESHUTDOWN
f3s_v2sync()
This function determines whether an erase operation has completed and returns any
detected error.
On success, it should return EOK. If an error occurs, it should return one of the
following:
EAGAIN
Still erasing.
EIO
Recoverable I/O error (e.g. failed due to low power or erase failed, but
corruption is localized and block will be usable after an erase).
EFAULT
EROFS
EINVAL
ERANGE
ENODEV
ESHUTDOWN
f3s_v2islock()
This function determines whether the block containing the address text_offset can be
written to (we term it as success) or not.
On success, it should return EOK. If the block cannot be written to, it should return
EROFS. Otherwise, an error has occurred and it should return one of the following:
EIO
Recoverable I/O error (e.g. failed due to low power or lock failed, but
corruption is localized and block will be usable after an erase).
EFAULT
EINVAL
ERANGE
ENODEV
ESHUTDOWN
f3s_v2lock()
This function write-protects the block containing the address text_offset (if supported).
If the block is already locked, it does nothing.
On success, it should return EOK. If an error occurs, it should return one of the
following:
EIO
Recoverable I/O error (e.g. failed due to low power or lock failed, but
corruption is localized and block will be usable after an erase).
EFAULT
EINVAL
ERANGE
ENODEV
ESHUTDOWN
f3s_v2unlock()
This function clears write-protection of the block containing the address text_offset
(if supported). If the block is already unlocked, it does nothing. Note that some devices
do not support unlocking of arbitrary blocks. Instead all blocks must be unlocked at
the same time. In this case, use f3s_v2unlockall() instead.
On success, it should return EOK. If an error occurs, it should return one of the
following:
EIO
Recoverable I/O error (e.g. failed due to low power or unlock failed, but
corruption is localized and block will be usable after an erase).
EFAULT
EINVAL
ERANGE
ENODEV
ESHUTDOWN
f3s_v2unlockall()
This function clears all write-protected blocks on the device containing the address
text_offset. Some boards use multiple chips to form one single logical device. In this
situation, each chip will have f3s_v2unlockall() invoked on it separately.
On success, it should return EOK. If an error occurs, it should return one of the
following:
EIO
Recoverable I/O error (e.g. failed due to low power or unlock failed, but
corruption is localized and block will be usable after an erase).
EFAULT
EINVAL
ERANGE
ENODEV
ESHUTDOWN
• f3s_ident()
• f3s_reset()
• f3s_v2write()
• f3s_v2erase()
• f3s_v2suspend()
• f3s_v2resume()
• f3s_v2sync()
• f3s_v2islock()
• f3s_v2lock()
• f3s_v2unlock()
• f3s_v2unlockall().
For example, if you have a 16-bit Intel device and you want to use f3s_v2erase(), you'd
use the f3s_iCFI_v2erase() routine.
For more information, see the technical note Choosing the correct MTD Routine for
the Flash Filesystem.
bsp_working_dir/src/hardware/flash/mtd-flash/public/sys/f3s_mtd.h.
This driver uses main memory rather than flash for storing the flash filesystem.
Therefore, the filesystem is not persistent — all data is lost when the system reboots
or /dev/shmem/fs0 is removed. This driver is used mainly for test purposes.
main()
In the main() function, we declare a single services array entry for the socket services
functions and a null entry for the flash services functions.
/*
** File: f3s_ram_main.c
**
** Description:
**
** This file contains the main function for the f3s
** flash filesystem
**
*/
#include "f3s_ram.h"
{
/* mandatory last entry */
0, 0, 0, 0, 0
}
};
{
/* mandatory last entry */
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
}
};
/* init f3s */
f3s_init(argc, argv, (f3s_flash_t *)flash);
/* start f3s */
error = f3s_start(service, (f3s_flash_t *)flash);
return (error);
}
f3s_ram_open()
In the socket services open() function, we assign a name for the driver and then process
any options. If no options are specified, a default size is assigned and the memory for
the (virtual) flash is allocated.
/*
** File: f3s_ram_open.c
**
** Description:
**
** This file contains the open function for the ram library
**
*/
#include "f3s_ram.h"
if (flag)
{
close(fd);
return (errno);
}
if (!memory)
{
close(fd);
return (errno);
}
f3s_ram_page()
In the socket services page() function, we first check that the given offset doesn't
exceed the bounds of the allocated memory, and then assign the window size if
required. The function returns the offset address modulo the window size.
/*
** File: f3s_ram_page.c
**
** Description:
**
** This file contains the page function for the ram library
**
*/
#include "f3s_ram.h"
The socket services status() and close() don't do anything interesting in this driver.
Since QNX Neutrino is a protected-mode 32-bit operating system, many limiting design
considerations won't apply (particularly on the x86 platform, which is steeped in DOS
and 8088 legacy concerns). By noting the various “do's” and “don'ts” given in this
appendix, you'll be able to design and build an embedded system tailored for QNX
Neutrino.
You may also be able to realize certain savings, in terms of design time, hardware
costs, and software customization effort.
Before you begin designing your system, here are some typical questions you might
consider:
Naturally, your particular system will dictate whether all of these (or others) are relevant.
But for the purposes of this discussion, we'll assume all these considerations apply.
Processor speed
Although QNX Neutrino is a realtime operating system, this fact alone doesn't
necessarily mean that any given application will run quickly. Graphical user
interface applications can consume a reasonable amount of CPU and are
particularly sensitive to the end-user's perception of speed.
If at all possible, try to prototype the system on either a standard PC (in the
case of x86-based designs) or a supported evaluation board (in the case of
x86 and ARM designs). This will very quickly give you a “feel” for the speed
of a particular processor.
Memory requirements
During initial prototyping, you should plan on more memory on the target
than during the final stages. This is because you'll often be running
debugging versions of software, which may be larger. Also, you'll want to
include diagnostics and utility programs, which again will consume more
memory than expected. Once your prototype system is up and running, you
can then start thinking about how much memory you “really” need.
Peripherals
Given a choice, you should use peripherals that are listed as supported by
QNX Neutrino. This includes such items as disk controllers, network cards,
PC-Card controllers, flash memory chips, and graphics controllers. For lists
of supported hardware, see the Community area of our website,
http:www.qnx.com; for information about third-party products, see the
Download area.
Graphics controllers are one of the particularly delicate areas in the design
of an embedded system, often because a chip may be very new when it's
selected and we may not yet have a driver for it. Also, if you're using a
graphics controller in conjunction with an LCD panel, beware that this is
perhaps the most complicated setup because of the many registers that
must be programmed to make it work.
Note that QNX Software Systems can do custom development work for you;
for more information, contact your sales representative. Other consulting
houses offer similar services to the QNX community.
Debugging
This debug port can be left off for final assembly or a slightly modified
“final” version of the board can be created. The cost savings in terms of
software development time generally pay for the hardware modifications
many times over.
Field upgrades
You can handle the issue of field upgrades in various ways, depending on
the nature of your particular target system:
• a JTAG port
• socketed Flash/EPROM devices
• a communications port
You may need such a vehicle for your update software even during your initial
software development effort. At this early phase, you'll effectively be
performing “field upgrades” as your software is being developed.
The target requirements vary depending on your intended usage. The bare minimum
requirements for generic x86 target systems are:
We also support non-BIOS IPL for x86 on specific processors and chipsets, but you
should ask us about your exact target configuration.
For requirements for non-x86 targets, see the Board Support Packages that we provide;
the target boards that we support represent a cross section of the minimum hardware
(in terms of DRAM, CPU/SoC performance, storage space, etc.) that we require. If
you're considering another board, ask us to help analyze it.
There are other design considerations that relate to both the hardware and software
development process. In this section, we'll discuss some of the more common ones.
Solid-state mass storage can be located anywhere in the address space — it should
be linearly mapped. In legacy designs (particularly x86), the mass storage device was
often forced into a window of some size (typically from 8 KB to 64 KB), with additional
hardware being required to map that window into the processor's address space.
Additionally, this window was traditionally located in the first 1 MB of memory.
With a modern, 32-bit processor, the physical address space of the processor is usually
sufficient to address the entire mass storage device. In fact, this makes the software
easier by not having to worry about how to address the window-mapping hardware.
The two driving factors to be considered in the hardware design of solid-state media
are cost and compatibility. If the medium is to be soldered onto the board, then there's
little chance that it may need to be compatible with other operating systems. Therefore,
simply map the entire medium into the address space of the processor and don't add
additional hardware to perform windowing or bank switching.
Adhering to standards (e.g. PCMCIA, FFS2, etc.) for solid-state memory is also
unnecessary — our Flash filesystem drivers know how to address and use just a raw
Flash device.
When the time comes to decide on the logical layout of the flash memory chips, the
tradeoff will be between the size of the erase block and the speed of access. By taking
four flash devices and organizing them into a 32-bit wide bus, you gain speed. However,
you also increase the erase block size by a factor of four (e.g. 256 KB erase blocks).
Note that we don't recommend trying to XIP out of flash memory that's being used for
a flash filesystem. This is because the flash filesystem may need to erase a particular
block of memory. While this erase operation is in progress, depending on the particular
type of flash memory device you have, the entire device may be unusable. If this is
also the device containing the code that the processor is actively executing from, you'll
run into problems. Therefore, we recommend that you use at least two independent
sets of flash devices: one set for the filesystem and one set for the code.
IPL location
Under QNX Neutrino, the only location requirement is that the ROM boot device that
performs the IPL be addressable at the processor's reset vector. No special hardware
is required to be able to “move” the location of the boot ROM.
Graphics cards
All the drivers under QNX Neutrino can be programmed to deal with graphics hardware
at any address—there's no requirement to map the VGA video aperture below 1 MB.
A20 gate
On the x86 platform, another vestige of the legacy 1 MB address limitation is usually
found in something called an A20 gate. This is a piece of hardware that would force
the A20 address line to zero, regardless of the actual setting of the A20 address line
on the processor.
The justification for this was for legacy software that would depend on the ability to
wrap past location 0xFFFFF back to 0x00000. The QNX Neutrino RTOS doesn't have
such a requirement. As a result, the OS doesn't need any A20 gate hardware to be
installed. Note that some embedded x86 processors have the A20 gate hardware built
right into the processor chip itself—the IPL will disable the A20 gate as soon as
possible after startup.
If your system requires a standard BIOS, there's a small chance that the BIOS
will make use of the A20 gate. To find out for certain, consult your BIOS
supplier.
QNX Neutrino doesn't require the external ISA bus to be mapped into the usual x86
0x00000-to-0xFFFFF address range. This simplifies the hardware design, eliminating
issues such as shadow RAM and the requirement to move a portion of the RAM (usually
0xA0000 through 0xFFFFF) to some other location.
But if your hardware needs to run with a standard BIOS and to support BIOS extensions,
then this optimization can't be implemented, because the BIOS expects extensions
at 0xA0000 through 0xEFFFF (typically).
In QNX Neutrino, all PCI drivers interface to a PCI resource manager (e.g. pci-bios,
pci-p5064, pci-raven), which handles the hardware on behalf of the drivers.
External clocks
The QNX Neutrino RTOS can be driven with an external clock. In some systems there's
a “standard” clock source supplied as part of the system or of the highly integrated
CPU chip itself. For convenience, the OS can operate with an external clock source
that's not generated by this component. However, keep two things in mind:
• The timing resolution for software timers will be no better than the timing resolution
of the external clock.
• The hardware clock will be driving a software interrupt handler.
Therefore, keep the rates down to a reasonable number. Almost all modern processors
can handle clock interrupts at 1 kHz or lower — processors with higher CPU clock
rates (e.g. Pentium-class, 300 MHz RISC processors, etc.) can handle faster clock
interrupts.
Note that there's no requirement to keep the clock frequency to some “round number.”
If it's convenient to derive the clock interrupt from a baud rate generator or other
crystal, the OS will be able to accurately scale the incoming clock rate for use in its
internal timers and time-of-day clocks.
On an x86 design, the default startup supports two Programmable Interrupt Controllers
(PICs). These must be 8259-compatible, with the standard configuration of a secondary
8259 connected to the IRQ2 line of the primary interrupt controller.
Beware of hanging devices off IRQ7 and IRQ15 on an 8259 chip — these are
generally known as the “glitch interrupts” and can be unreliable.
If your x86 hardware design differs, there's no constraint about the PICs, but you must
write the code to handle them.
On non-x86 designs, be aware that there may be only one interrupt line going to the
processor and that a number of hardware devices may be sharing that one line. This
is generally accomplished in one of two ways:
• wire-OR
• PIC chip
In either case, the relevant design issue is to determine the ordering and priority of
interrupts from hardware sources. You'll want to arrange the hardware and software
to give highest priority (and first order) to the interrupt source that has the most
stringent latency requirements. (For more details, see the chapter on Writing an
Interrupt Handler in the Programmer's Guide, along with the InterruptAttach() and
InterruptAttachEvent() function calls in the QNX Neutrino C Library Reference.)
Serial and parallel ports are certainly desirable—and highly recommended—but not
required. The 16550 component with 16-byte FIFOs is suitable for QNX Neutrino.
If you're going to support multiple serial ports on your device, you can have the multiple
devices share the same interrupt. It's up to the software to decide which device
generated the interrupt and then to handle that interrupt. The standard QNX Neutrino
serial port handlers are able to do this.
Although the serial driver can be told to use a “nonstandard” clock rate when
calculating its divisor values, this can cause the baud rate to deviate from the standard.
Try to run DTR, DSR, RTS, CTS if possible, because hardware flow control will help
on slower CPUs.
Generally, the parallel port does not require an interrupt line — this isn't used by our
standard parallel port drivers.
Avoid the Non-Maskable Interrupt (NMI) in x86 designs. ARM doesn't even support
it. An NMI is an interrupt which can't be disabled by clearing the CPU's interrupt
enable flag, unlike most normal interrupts. Non-Maskable interrupts are typically used
to signal events that require immediate action, such as a parity error, a hardware
failure, or imminent loss of power.
The problem with NMIs is that they can occur even when interrupts have been disabled.
This is important because sometimes it's assumed that interrupts can be masked to
avoid being interrupted. NMIs undermine this assumption and this can lead to
unexpected behaviour if an NMI fires during a period in which that software expects
to be operating without interruption.
For this reason, NMIs are normally only used when the subsequent condition of the
machine is not a relevant consideration; for instance, when the machine is about to
shut down, or when an unrecoverable hardware error has occurred.
Anytime an NMI is used, any software may experience unexpected behavior and there's
no good way to predict what the behavior may be.
Before you commit to a design, take a look at the following tips — you may save
yourself some grief. Although some of these points assume you're relying on our Custom
Engineering services, the principles behind all of them are sound.
Do:
Don't:
• Don't decide that no-BIOS is the way to go just because it sounds cool.
• Don't use a $2.00 chip instead of a $3.00 chip and expect the
performance of a $10.00 chip.
• Don't build your first run of boards without leaving a way to debug the
system.
• Don't build your first run of boards with only 1 MB of RAM on board.
• Don't send us anything without correct schematics that match what you
send.
• Don't program the flash and then solder it on, leaving us with no option
to reprogram it.
• Don't build just one prototype that must be shipped back and forth several
times.
In this appendix, we'll look at some typical buildfiles you can use with mkifs or import
into the IDE's System Builder to get your system up and running. This appendix is
divided into two main parts:
We finish with a section for each of the supported processor platforms, showing you
differences from the x86 samples and noting things to look out for.
Note that you should read both the section for your particular processor as well as the
section on generic samples, because things like shared objects (which are required
by just about everything) are documented in the generic section.
Generic examples
In this section, we'll look at some common buildfile examples that are applicable
(perhaps with slight modifications, which we'll note) to all platforms. We'll start with
some fragments that illustrate various techniques, and then we'll wrap up with a few
complete buildfiles. In the “Processor-specific notes” section, we'll look at what needs
to be different for the various processor families.
Shared libraries
The first thing you'll need to do is to ensure that the shared objects required by the
various drivers you'll be running are present.
All drivers require at least the standard C library shared object (libc.so). Since the
shared object search order looks in /proc/boot, you don't have to do anything
special, except include the shared library into the image. This is done by simply
specifying the name of the shared library on a line by itself, meaning “include this
file.”
How do you determine which shared objects you need in the image? There are several
ways:
• The easiest method (which gives the most concise output) is to use the ldd (“list
dynamic dependencies”) utility. For example:
• You can use the objdump utility to display information about the executables
you're including in the image; look for the objects marked as NEEDED. For example:
The ping executable needs libsocket.so.3 and libc.so.3. You need to use
objdump recursively to see what these shared objects need:
• You can set the DL_DEBUG environment variable. This causes the shared library
loader to display debugging information about the libraries as they're opened:
# export DL_DEBUG=libs
# ping 10.42.110.235
load_object: attempt load of libsocket.so.3
load_elf32: loaded lib at addr b8200000(text) b822bccc(data)
dlopen("nss_files.so.0",513)
load_object: attempt load of nss_files.so.0
dlopen: Library cannot be found
dlopen("nss_dns.so.0",513)
load_object: attempt load of nss_dns.so.0
dlopen: Library cannot be found
For more information about the values for DL_DEBUG, see the entry for dlopen()
in the QNX Neutrino C Library Reference.
If you want to be able to run executables more than once, you'll need to specify the
[data=copy] attribute for those executables. If you want it to apply to all executables,
just put it on a line by itself before the executables. This causes the data segment to
be copied before it's used, preventing it from being overwritten by the first invocation
of the program.
Multiple consoles
For systems that have multiple consoles or multiple serial ports, you may wish to have
the shell running on each of them. Here's an example showing you how that's done:
[+script] .script = {
# start any other drivers you need here
devc-con -e -n4 &
reopen /dev/con1
[+session] esh &
reopen /dev/con2
[+session] esh &
...
1. Start the console driver with the -n option to ask for more than one console (in this
case, we asked for four virtual consoles).
2. Redirect standard input, output, and error to each of the consoles in turn.
3. Start the shell on each console.
It's important to run the shell in the background (via the ampersand character “&”)
— if you don't, then the interpretation of the script will suspend until the shell exits!
Generally speaking, this method can be used to start various other programs on the
consoles (that is to say, you don't have to start the shell; it could be any program).
To do this for serial ports, start the appropriate serial driver (e.g. devc-ser8250),
and redirect standard input, output, and error for each port (e.g. /dev/ser1,
/dev/ser2). Then run the appropriate executable (in the background!) after the
redirection.
The [+session] directive makes the program the session leader (as per POSIX) —
this may not be necessary for arbitrary executables.
Redirection
You can do the reopen on any device as many times as you want. You would do this,
for example, to start a program on /dev/con1, then start the shell on /dev/con2,
and then start another program on /dev/con1 again:
[+script] .script = {
...
reopen /dev/con1
prog1 &
reopen /dev/con2
[+session] esh &
reopen /dev/con1
prog2 &
...
/tmp
To create the /tmp directory on a RAM-disk, you can use the following in your buildfile:
This will establish /tmp as a symbolic link in the process manager's pathname table
to the /dev/shmem directory. Since the /dev/shmem directory is really the place
where shared memory objects are stored, this effectively lets you create files on a
RAM-disk — files created are, in reality, shared memory objects living in RAM.
Note that the line containing the link attribute (the [type=link] line) should be
placed outside of the script file or boot file — after all, you're telling mkifs that it
should create a file that just happens to be a link rather than a “real” file.
This configuration file does the bare minimum necessary to give you a shell prompt
on the first serial port:
[virtual=processor,srec] .bootstrap = {
startup-rpx-lite -Dsmc1.115200.64000000.16
PATH=/proc/boot procnto
}
[+script] .script = {
devc-serversion -e -F -c64000000 -b115200 smc1 &
reopen
[type=link] /dev/console=/dev/ser1
[type=link] /usr/lib/ldqnx.so.2=/proc/boot/libc.so
libc.so
[data=copy]
devc-serversion
esh
# specify executables that you want to be able
# to run from the shell: echo, ls, pidin, etc...
echo
ls
pidin
cat
cp
Let's now examine a complete buildfile that starts up the flash filesystem:
[+script] .script = {
devc-con -e -n5 &
reopen /dev/con1
devf-i365sl -r -b3 -m2 -u2 -t4 &
waitfor /fs0p0
[+session] TERM=qansi PATH=/proc/boot:/bin esh &
}
[type=link] /tmp=/dev/shmem
[type=link] /bin=/fs0p0/bin
[type=link] /etc=/fs0p0/etc
libc.so
[type=link] /usr/lib/ldqnx.so.2=/proc/boot/libc.so
libsocket.so
[data=copy]
devf-i365sl
devc-con
esh
The buildfile's .bootstrap specifies the usual startup-bios and procnto (the
startup program and the kernel). Notice how we set the PATH environment variable
to point not only to /proc/boot, but also to /bin — the /bin directory is a link
(created with the [type=link]) to the flash filesystem's /fs0p0/bin path.
In the .script file, we started up the console driver with five consoles, reopened
standard input, output, and error for /dev/con1, and started the flash filesystem
driver devf-i365sl. Let's look at the command-line options we gave it:
-r
Enable fault recovery for dirty extents, dangling extents, and partial reclaims.
-b3
-u2
Specify the highest update level (2) to update files and directories.
-t4
The devf-i365sl will automatically mount the flash partition as /fs0p0. Notice
the process manager symbolic links we created at the bottom of the buildfile:
[type=link] /bin=/fs0p0/bin
[type=link] /etc=/fs0p0/etc
In this example, we'll look at a filesystem for rotating media. Notice the shared libraries
that need to be present:
[+script] .script = {
pci-bios &
devc-con &
reopen /dev/con1
# Disk drivers
devb-eide blk cache=2m,automount=hd0t79:/,automount=cd0:/cd &
# We use the C shared lib (which also contains the runtime linker)
libc.so
# Just in case someone needs floating point and our CPU doesn't
# have a floating point unit
fpemu.so.2
# Include the hard disk shared objects so we can access the disk
libcam.so
io-blk.so
# Copy code and data for all executables after this line
[data=copy]
For this release of the QNX Neutrino RTOS, you can't use the floating-point
emulator (fpemu.so) in statically linked executables.
In this buildfile, we see the startup command line for the devb-eide command:
This line indicates that the devb-eide driver should start and then pass the string
beginning with the cache= through to the end (except for the ampersand) to the block
I/O file (io-blk.so). This will examine the passed command line and then start up
with a 2-megabyte cache (the cache=2m part), automatically mount the partition
identified by hd0t79 (the first QNX filesystem partition) as the pathname /hd, and
automatically mount the CD-ROM as /cd.
Once this driver is started, we then need to wait for it to get access to the disk and
perform the mount operations. This line does that:
waitfor /x86 10
This waits for the pathname /x86 to show up in the pathname space. (We're assuming
a formatted hard disk that contains a valid QNX filesystem with ${QNX_TARGET}
copied to the root.)
Now that we have a complete filesystem with all the shipped executables installed,
we run a few common executables, like the Pipe server.
Finally, the list of shared objects contains the .so files required for the drivers and
the filesystem.
Here's an example of a buildfile that starts up an Ethernet driver, the TCP/IP stack,
and the network filesystem:
[+session] sh &
}
# Include the network files so we can access files across the net
devn-tulip-abc123.so
For this release of the QNX Neutrino RTOS, you can't use the floating-point
emulator (fpemu.so.2) in statically linked executables.
This buildfile is very similar to the previous one shown for the disk. The major difference
is that instead of starting devb-eide to get a disk filesystem driver running, we
started io-pkt-v4 to get the network drivers running. The -d specifies the driver
that should be loaded, in this case the driver for a DEC 21x4x (Tulip)-compatible
Ethernet controller.
Once the network manager is running, we need to synchronize the script file
interpretation to the availability of the drivers. That's what the waitfor /dev/socket
is for — it waits for the network manager to initialize itself. The ifconfig en0
10.0.0.1 command then specifies the IP address of the interface.
The next thing started is the NFS filesystem module, fs-nfs3, with options telling
it that it should mount the filesystem present on 10.0.0.2 in two different places:
${QNX_TARGET} should be mounted in /, and /etc should be mounted as /etc.
Since it may take some time to go over the network and establish the mounting, we
see another waitfor, this time ensuring that the filesystem on the remote has been
correctly mounted (here we assume that the remote has a directory called
${QNX_TARGET}/armle-v7/bin — since we've mounted the remote's
${QNX_TARGET} as /, the waitfor is really waiting for armle-v7/bin under the
remote's ${QNX_TARGET} to show up).
Processor-specific notes
Let's look at what's different from the generic files listed above for each processor
family. Since almost everything that's processor- and platform-specific in QNX Neutrino
is contained in the kernel and startup programs, there's very little change required to
go from an x86 with standard BIOS to, for example, an ARM evaluation board.
The first obvious difference is that you must specify the processor that the
buildfile is for. This is actually a simple change; in the [virtual=…] line,
substitute the x86 specification with armle-v7.
For example:
Another difference is that the startup program is tailored not only for the
processor family, but also for the actual board the processor runs on. If
you're not running an x86 with a standard BIOS, you should replace the
startup-bios command with one of the many startup-* programs we
supply.
To find out what startup programs we currently provide, refer to the following
sources:
The examples listed previously provide support for the 8250 family of serial
chips. Some non-x86 platforms support the 8250 family as well, but others
have their own serial port chips.
A20 gate
adaptive
adaptive partitioning
application ID
A number that identifies all processes that are part of an application. Like
process group IDs, the application ID value is the same as the process id of
the first process in the application. A new application is created by spawning
with the POSIX_SPAWN_NEWAPP or SPAWN_NEWAPP flag. A process created
without one of those inherits the application ID of its parent. A process needs
the PROCMGR_AID_CHILD_NEWAPP ability in order to set those flags.
The SignalKill() kernel call accepts a SIG_APPID flag ORed into the signal
number parameter. This tells it to send the signal to all the processes with
an application ID that matches the pid argument. The DCMD_PROC_INFO
devctl() returns the application ID in a structure field.
atomic
thread could preempt the original thread and move the file position to a
different location, thus causing the original thread to read data from the
second thread's position.
attributes structure
bank-switched
Convenient set of library calls for writing resource managers. These calls all
start with resmgr_*(). Note that while some base layer calls are unavoidable
(e.g. resmgr_pathname_attach()), we recommend that you use the POSIX
layer calls where possible.
A certain sequence of bytes indicating to the BIOS or ROM Monitor that the
device is to be considered an “extension” to the BIOS or ROM Monitor —
control is to be transferred to the device by the BIOS or ROM Monitor, with
the expectation that the device will perform additional initializations.
On the x86 architecture, the two bytes 0x55 and 0xAA must be present (in
that order) as the first two bytes in the device, with control being transferred
to offset 0x0003.
block-integral
bootable
bootfile
The part of an OS image that runs the startup code and the microkernel.
budget
buildfile
A text file containing instructions for mkifs specifying the contents and
other details of an image, or for mkefs specifying the contents and other
details of an embedded filesystem image.
canonical mode
Also called edited mode or “cooked” mode. In this mode the character device
library performs line-editing operations on each received character. Only
when a line is “completely entered” — typically when a carriage return (CR)
is received — will the line of data be made available to application processes.
Contrast raw mode.
channel
chid
CIFS
Common Internet File System (also known as SMB) — a protocol that allows
a client workstation to perform transparent file access over a network to a
Windows 95/98/NT server. Client file access calls are converted to CIFS
protocol requests and are sent to the server over the network. The server
receives the request, performs the actual filesystem operation, and sends a
response back to the client.
CIS
coid
combine message
connect message
connection
The key thing to note is that connections and file descriptors (FD) are one
and the same object. See also channel and FD.
context
cooked mode
core dump
critical section
A code passage that must be executed “serially” (i.e. by only one thread at
a time). The simplest from of critical section enforcement is via a mutex.
deadlock
device driver
A process that allows the OS and application programs to make use of the
underlying hardware in a generic way (e.g. a disk drive, a network interface).
Unlike OSs that require device drivers to be tightly bound into the OS itself,
device drivers for the QNX Neutrino RTOS are standard processes that can
be started and stopped dynamically. As a result, adding device drivers doesn't
affect any other part of the OS — drivers can be developed and debugged
like any other application. Also, device drivers are in their own protected
address space, so a bug in a device driver won't cause the entire OS to shut
down.
DNS
dynamic bootfile
dynamic linking
The process whereby you link your modules in such a way that the Process
Manager will link them to the library modules before your program runs. The
word “dynamic” here means that the association between your program and
the library modules that it uses is done at load time, not at linktime. Contrast
static linking. See also runtime loading.
edge-sensitive
edited mode
EOI
End Of Interrupt — a command that the OS sends to the PIC after processing
all Interrupt Service Routines (ISR) for that particular interrupt source so
that the PIC can reset the processor's In Service Register. See also PIC and
ISR.
EPROM
event
FD
FIFO
flash memory
FQNN
Fully Qualified Node Name — a unique name that identifies a QNX Neutrino
node on a network. The FQNN consists of the nodename plus the node
domain tacked together.
garbage collection
HA
handle
A pointer that the resource manager base library binds to the pathname
registered via resmgr_attach(). This handle is typically used to associate
some kind of per-device information. Note that if you use the iofunc_*()
POSIX layer calls, you must use a particular type of handle — in this case
called an attributes structure.
image
inherit mask
A bitmask that specifies which processors a thread's children can run on.
Contrast runmask.
interrupt
interrupt handler
See ISR.
interrupt latency
See ISR.
I/O message
A message that relies on an existing binding between the client and the
resource manager. For example, an _IO_READ message depends on the
client's having previously established an association (or context) with the
resource manager by issuing an open() and getting back a file descriptor.
See also connect message, context, combine message, and message.
I/O privileges
A particular right, that, if enabled for a given thread, allows the thread to
perform I/O instructions (such as the x86 assembler in and out
instructions). By default, I/O privileges are disabled, because a program with
it enabled can wreak havoc on a system. To enable I/O privileges, the thread
must be running as root, and call ThreadCtl().
IPC
IPL
Initial Program Loader — the software component that either takes control
at the processor's reset vector (e.g. location 0xFFFFFFF0 on the x86), or
is a BIOS extension. This component is responsible for setting up the
machine into a usable state, such that the startup program can then perform
further initializations. The IPL is written in assembler and C. See also BIOS
extension signature and startup code.
IRQ
ISR
kernel
See microkernel.
level-sensitive
linearly mapped
message
Message passing not only allows processes to pass data to each other, but
also provides a means of synchronizing the execution of several processes.
As they send, receive, and reply to messages, processes undergo various
“changes of state” that affect when, and for how long, they may run.
microkernel
A part of the operating system that provides the minimal services used by
a team of optional cooperating processes, which in turn provide the
higher-level OS functionality. The microkernel itself lacks filesystems and
many other services normally expected of an OS; those services are provided
by optional processes.
mount structure
multicore system
A chip that has one physical processor with multiple CPUs interconnected
over a chip-level bus.
mutex
name resolution
In a QNX Neutrino network, the process by which the Qnet network manager
converts an FQNN to a list of destination addresses that the transport layer
knows how to get to.
name resolver
nd
NDP
network directory
NFS
you use for listing and managing files (e.g. ls, cp, mv) operate on the remote
files exactly as they do on your local files.
NMI
See NDP.
node domain
A character string that the Qnet network manager tacks onto the nodename
to form an FQNN.
nodename
nonbootable
OCB
Open Control Block (or Open Context Block) — a block of data established
by a resource manager during its handling of the client's open() function.
This context block is bound by the resource manager to this particular
request, and is then automatically passed to all subsequent I/O functions
generated by the client on the file descriptor returned by the client's open().
package filesystem
partition
pathname prefix
See mountpoint.
persistent
When applied to storage media, the ability for the medium to retain
information across a power-cycle. For example, a hard disk is a persistent
storage medium, whereas a ramdisk is not, because the data is lost when
power is lost.
PIC
PID
POSIX
Convenient set of library calls for writing resource managers. The POSIX
layer calls can handle even more of the common-case messages and functions
than the base layer calls. These calls are identified by the iofunc_*() prefix.
In order to use these (and we strongly recommend that you do), you must
also use the well-defined POSIX-layer attributes (iofunc_attr_t), OCB
(iofunc_ocb_t), and (optionally) mount (iofunc_mount_t) structures.
preemption
The act of suspending the execution of one thread and starting (or resuming)
another. The suspended thread is said to have been “preempted” by the
new thread. Whenever a lower-priority thread is actively consuming the CPU,
and a higher-priority thread becomes READY, the lower-priority thread is
immediately preempted by the higher-priority thread.
prefix tree
priority inheritance
priority inversion
process
A nonschedulable entity, which defines the address space and a few data
areas. A process must have at least one thread running in it — this thread
is then called the first thread.
process group
process group ID
process ID (PID)
pty
pulses
Qnet
QoS
RAM
raw mode
replenishment
reset vector
The address at which the processor begins executing instructions after the
processor's reset line has been activated. On the x86, for example, this is
the address 0xFFFFFFF0.
resource manager
A user-level server program that accepts messages from other programs and,
optionally, communicates with hardware. QNX Neutrino resource managers
are responsible for presenting an interface to various types of devices,
whether actual (e.g. serial ports, parallel ports, network cards, disk drives)
or virtual (e.g. /dev/null, a network filesystem, and pseudo-ttys).
RMA
round robin
runmask
A bitmask that indicates which processors a thread can run on. Contrast
inherit mask.
runtime loading
The process whereby a program decides while it's actually running that it
wishes to load a particular function from a library. Contrast static linking.
scheduling latency
The amount of time that elapses between the point when one thread makes
another thread READY and when the other thread actually gets some CPU
time. Note that this latency is almost always at the control of the system
designer.
scoid
session
session leader
A process whose death causes all processes within its process group to
receive a SIGHUP signal.
software interrupts
Similar to a hardware interrupt (see interrupt), except that the source of the
interrupt is software.
sporadic
startup code
The software component that gains control after the IPL code has performed
the minimum necessary amount of initialization. After gathering information
about the system, the startup code transfers control to the OS.
static bootfile
An image created at one time and then transmitted whenever a node boots.
Contrast dynamic bootfile.
static linking
The process whereby you combine your modules with the modules from the
library to form a single executable that's entirely self-contained. The word
“static” implies that it's not going to change — all the required modules
are already combined into one.
An area in the kernel that is filled by the startup code and contains
information about the system (number of bytes of memory, location of serial
ports, etc.) This is also called the SYSPAGE area.
thread
The schedulable entity under the QNX Neutrino RTOS. A thread is a flow of
execution; it exists within the context of a process.
tid
timer
timeslice
TLB
TLS
Index
_CS_LIBPATH 48 BIOS 16, 17, 82, 122, 171, 224
in bootstrap files 48 extension 82, 171
_CS_PATH 48 if you don't have one 224
in bootstrap files 48 block_size buildfile attribute 57, 58
.~~~ filename extension 61 Board Support Packages, See BSPs
/dev/shmem 23 boot header 88
/tmp, creating on a RAM-disk 218 bootfile 47
BOOTP 82, 85
bootstrap file (.bootstrap) 48, 49
A bound multiprocessing (BMP) 50
A20 171, 208 break_detect() 139
adaptive partitioning 48 bsp_working_dir 32
add_cache() 145 BSPs 29, 30, 31, 33, 34
add_callout_array() 146 content 29, 31
add_callout() 145 obtaining 29
add_interrupt_array() 130, 146, 164 source code 30, 31, 33, 34
add_interrupt() 146 command line 31, 34
add_ram() 146, 165 importing into the IDE 30
add_string() 130, 146 buildfile 47, 48, 49, 50, 52, 53, 54, 57, 58, 59, 74, 89,
add_typed_string() 129, 146 216, 217, 218, 224
alloc_qtime() 147 attributes 48, 49, 52, 54, 57, 58, 59, 74, 89, 217,
alloc_ram() 147 218, 224
ARM 132, 137, 164, 165, 168, 224 block_size 57, 58
ARM_CPU_FLAG_NEON 123 combining 54
ARM_CPU_FLAG_SMP 123 compress 49, 89
ARM_CPU_FLAG_V7 123 data 217
ARM_CPU_FLAG_WMMX2 123 filter 58, 59
armv_cache 147 gid 54
armv_chip 148 keeplinked 74
armv_chip_detect() 150 max_size 57
armv_pte 151 min_size 58
armv_setup_v7() 152 module 48
as_add_containing() 152 newpath 52
as_add() 152 perms 54
AS_ATTR_CACHABLE 115 physical 49
AS_ATTR_CONTINUED 115 script 49
AS_ATTR_KIDS 115 search 52
AS_ATTR_READABLE 115 spare_blocks 57, 59
AS_ATTR_WRITABLE 115 type 218
as_default() 153 uid 54
as_find_containing() 153 virtual 48, 49, 89, 224
as_find() 153 complete examples of 216
as_info2off() 153 including lots of files in 54
AS_NULL_OFF 114 inline files 47, 53
as_off2info() 154 modifiers 50
AS_PRIORITY_DEFAULT 114 CPU 50
as_set_checker() 154 simple example of 47
as_set_priority() 154 specifying a processor in 224
avoid_ram() 154 syntax 47
Bus item (system page) 121
B
C
bank-switched 78
See also image cache 122, 123, 124, 140
defined 78 CACHE_FLAG_CTRL_PHYS 125
See also image CACHE_FLAG_DATA 125