An input method is the software that converts keyboard events from one language into another. It is a translator of sorts: it enables the user to "type" in a language that doesn't mesh well with the traditional 100+ key keyboards.
Take Japanese, for example. The Japanese language consists of a number of writing systems (sort of like alphabets). When combined, these alphabets contain literally many thousands of characters. Imagine a keyboard with that many keys!
Input methods were created to work around this impracticality. For example, Be's new Japanese input method—an installation option in R4 -- transliterates the Roman alphabet (the keys on your keyboard) into Japanese. The process for doing this is somewhat complicated, but that's another subject. This article is about what you need to do in order to support languages that require input methods.
So, what do you need to do in order to capture the hearts of all those BeOS users out there in the rest of the world?
The short answer: You don't need to do a thing! Sort of... If your
application's text entry needs are satisfied with BTextView
(and
BTextControl
), then you really are set. Be's kit classes are all
input-method aware straight out of the box. However, if you're rolling
out your own text engine, you need to do a minimal amount of work to be
fully input-method aware.
The first step is in the BView
constructor. Every input method- aware
view must have the B_INPUT_METHOD_AWARE
flag set (take a look in
View.h
). If you don't set this
flag, the OS will intervene as needed
and pop up a proxy window (commonly known as the "offline" or
"bottomline" window) that will handle the input method's needs for you.
The window goes away when the current input transaction is complete. The
resulting text is then converted into simulated B_KEY_DOWN
messages and
sent to the non-savvy view, thus ensuring that all views are capable of
receiving the user's input.
When the B_INPUT_METHOD_AWARE
flag is present, it is the view's
responsibility to do what the bottomline window would otherwise do for
you. This style of input is called "inline" input, and it is by far
preferred to bottomline input, as everything occurs locally in the
context of the current view. There are no windows popping up and
disappearing; it provides a far more seamless experience to the user.
An input method-aware view receives key down messages in one of two ways:
Through the standard B_KEY_DOWN
messages that invoke
your KeyDown()
method, or
Via the new B_INPUT_METHOD_EVENT
message that you must
catch in MessageReceived()
.
All B_INPUT_METHOD_EVENT
messages contain
int32 opcodes under the name of be:opcode
. Currently there are four
opcodes defined (in Input.h
):
B_INPUT_METHOD_STARTED
B_INPUT_METHOD_STOPPED
B_INPUT_METHOD_CHANGED
B_INPUT_METHOD_LOCATION_REQUEST
The B_INPUT_METHOD_STARTED
opcode signals to your view that a new input
transaction has begun. There is a be:reply_to
BMessenger
stashed in these
messages. Keep that messenger around; it's your only way to talk to the
input method while this transaction is in effect.
B_INPUT_METHOD_STOPPED
, surprisingly, is the opposite of
B_INPUT_METHOD_STARTED
. By the time you receive this message, the user is
done with the current transaction. The be:reply_to
messenger that you
kept around is now officially stale. Throw it out.
B_INPUT_METHOD_CHANGED
and
B_INPUT_METHOD_LOCATION_REQUEST
messages
happen in between the started and stopped messages.
B_INPUT_METHOD_CHANGED
is where most of the work is done. It contains the
following data:
be:string
(a char*)
be:selection
(two int32s)
be:clause_start
(n-numbers of int32s)
be:clause_end
(the same number of int32s as
be:clause_start
)
be:confirmed
(a boolean)
The string is what the user is currently entering. This is what you want
to display at the current insertion point. BTextView
highlights this text
in blue to show that it is a part of a transitory transaction.
There may be a selection within the blue portion. This is expressed as a
pair of be:selection
int32s that
are offsets within be:string
. BTextView
highlights the selection in red instead of blue.
In languages such as Japanese, a single sentence or phrase is often
separated into numerous clauses; be:clause_start
and be:clause_end
pairs
delimit these clauses, also as offsets within be:string. BTextView
separates the blue/red highlighting wherever there is a clause boundary.
Finally, be:confirmed
is true
when
the user has entered and "confirmed"
the current string, and wishes to either close the transaction or start a
new one directly. BTextView
unhighlights the blue/red at this point, and
waits for either a B_INPUT_METHOD_STOPPED
(to close the transaction) or
another B_INPUT_METHOD_CHANGED
(to start a new transaction directly).
B_INPUT_METHOD_LOCATION_REQUEST
is the input method's way of asking you
for the screen coordinates of each character in your representation of
be:string
. This information is used by the input method to pop up
additional windows that give the user an opportunity to select certain
characters from a list and so on. When you receive this event, simply
reply to the be:reply_to
messenger with a
B_INPUT_METHOD_EVENT
as such:
BMessage
reply
(B_INPUT_METHOD_EVENT
);reply
.AddInt32
("be:opcode",B_INPUT_METHOD_LOCATION_REQUEST
);BPoint
screenDelta
=ConvertToScreen
(B_ORIGIN
);screenDelta
-=B_ORIGIN
; for (int32i
=inlineStartOffset
;i
<inlineEndOffset
;i
=NextUTF8Character
(i
)) {reply
.AddPoint
("be:location_reply",LocalLocationOfCharacter
(i
) +screenDelta
);reply
.AddFloat
("be:height_reply", HeightOfCharacter(i
)); }theBeReplyToMessenger
.SendMessage
(&reply
);
The input method will take care of the rest for you.
That was a quick overview of what you need in order to get started. Taking the time to implement an input method-aware view is an excellent first step towards making your product ready for the markets around the world. Good luck!
This is second in a series of Developer Workshop articles to help people program with the new Media Kit. It was written by Be's Director of Media Technology. The article first appeared on <http://www.b500.com/bepage/>, and possible future updates will be posted there.
Rather than use a BeOS device driver directly, user-level applications instead use some higher-level API which calls a user-level add-on, which calls the driver. There's nothing, however, that prevents an application from talking directly to a driver just as an appropriate add-on would. Indeed, in cases where there is no appropriate add-on API you have to talk to the driver directly from an application. It's also useful to talk directly to the driver while developing and testing it. In this article, we'll call the entity (application or add-on) that is using the driver a "client" of the driver.
The first thing you need to do is to find the device. The driver will
export one or more devices in subdirectories of the
/dev
directory. For
instance, the sonic_vibes audio card driver exports in
/dev/audio/raw/sonic_vibes/
as well as in other locations.
Because most BeOS drivers support handling more than one installed card
of the same kind, the convention is to number the installed cards,
starting at 1. Thus, the first installed sonic_vibes card is found as
/dev/audio/raw/sonic_vibes/1
.
You can use the BDirectory
class or the
opendir()
C function to look through a directory for available devices.
Once you know what device you want to use, you should open it using the
open()
C call:
intfd
=open
("/dev/audio/raw/sonic_vibes/1",O_RDWR
);
You'll use this file descriptor to refer to the open device from now on.
The file descriptor should be closed with close()
when you're done with
it. If the process (team) that opened the device crashes or otherwise
goes away without closing the file descriptor, it will be garbage
collected and closed by the kernel.
Many devices implement the read()
and write()
protocols. Thus, to record
some audio from the default input device, you just do this:
short *data
= (short *)malloc
(200000); ssize_trd
=read
(fd
,data
, 200000);
rd
will contain the number of bytes actually read, or -1 if an error
occurred (in which case the thread-local variable errno
will contain the
error code).
The format of the data returned by the device varies with the device; the
default format of the sonic_vibes driver is stereo 16-bit signed
native-endian 44.1 kHz PCM sample data. To play back this data using the
write()
call, do this:
ssize_twr
=write
(fd
,data
,rd
);
wr
will contain the actual number of bytes written, or -1 for error, in
which case errno
contains the error code.
Many devices do not work well with the simple read()
and write()
protocol; for instance, video capture cards often require a contiguous
locked area of memory, which typically is not found in a buffer passed in
by the user to read()
or write()
.
Then you can implement your protocol as
ioctl()
selectors. There are a number of
well-defined ioctl()
values that
your device can implement if they make sense for the class of device
you're dealing with; specific subdirectories of
/dev
may require certain
ioctl()
protocols to be implemented (such as
/dev/joystick
,
/dev/midi
, or
/dev/audio
).
Suppose we're using a video capture driver which implements the following protocol:
enum {drvOpSetBuffers
=B_DEVICE_OP_CODES_END
+10001,drvOpStart
,drvOpStop
,drvOpWaitForFrame
, }; struct drv_buffer_info { color_spacein_space
; intin_width
; intin_height
; intin_rowbytes
; void *in_buffers
[2]; /* even, odd */ }; struct drv_frame_info { intout_frame_number
; };
The client could then configure the driver like so:
drv_buffer_infobuf_info
;buf_info
.in_space
=B_YUV422
;buf_info
.in_width
= 640;buf_info
.in_height
= 240;buf_info
.in_rowbytes
= 640; area_idbuf_area
=create_area
("capture buffers", &buf_info
.in_buffers
[0],B_ANY_KERNEL_ADDRESS
,buf_info
.in_rowbytes
*buf_info
.in_height
*2,B_CONTIGUOUS
,B_READ_AREA
|B_WRITE_AREA
);buf_info
.in_buffers
[1] = ((char *)buf_info
.in_buffers
[0])+buf_info
.in_rowbytes
*buf_info
.in_height
; interr
= ioctl(fd
,drvOpSetBuffers
, &buf_info
); if (err
== -1)err
=errno
;
It would start video capture like so:
interr
=ioctl
(fd
,drvOpStart
); interr
=ioctl
(fd
,drvOpStart
);
It would wait for each frame to arrive like so:
while (running
) { drv_frame_infofrm_info
; interr
=ioctl
(fd
,drvOpWaitForFrame
, &frm_info
); if (err
== -1)err
=errno
;process_frame
(frm_info
.out_frame_number
,buf_info
.in_buffers
[frm_info
.out_frame_number
& 1]); }
Last, it would stop the capture like so:
interr
=ioctl
(fd
,drvOpStop
); if (err
== -1)err
=errno
;
In real life, a typical protocol is more capable, and thus more complicated, than shown here, but it should be enough to give you an idea of how the protocol between a user-level client and a driver can be structured.
OK, now that you know how to use a device driver, and have some idea how to structure the protocol between the client and the driver, it's time to get down and dirty with the actual process of creating a driver. Creating a driver on BeOS is done using ANSI C; the C++ language requires certain support which is not available in the BeOS kernel environment.
If you already have a large C++ library that talks to your hardware device and want to port it to BeOS, we suggest that you make your driver very shallow and use it just to read/write card registers and service interrupts, and put all your C++ code in a user-level add-on. Some readers may know "interrupts" by the name "IRQ"; we'll call them "interrupts" because that's the terminology used by the BeOS kernel kit.
A driver is a loadable shared library (add-on) which exports certain
well-known function names such as init_driver()
and publish_devices()
.
The driver gets loaded by the "devfs" file system (which runs in the
kernel) in response to some client calling file system functions
opendir()
, open()
, and others. A driver may get loaded and unloaded
several times, not necessarily being opened just because it's loaded. It
will, however, never be unloaded while it is open. The moral of this
story is that you cannot expect global or static variables to retain
their values after uninit_driver()
has been called, or before
init_driver()
is called.
First, you have to decide what to call your driver and your devices.
Typically, one driver will service any number of installed cards of the
same type, and each of those cards may cause the driver to publish
multiple device names under
/dev
. These device names will be referred to
as "devices"; the actual binary add-on will be called the "driver"; and
the pieces of hardware serviced by the driver will be called the
"hardware."
Typically, you'll name your driver something similar to the name of the main chip serviced by the driver. The sonic_vibes driver drives the S3 Sonic Vibes chip; the bt848 driver drives the Brooktree Bt848/878 chips; the awe64 driver drives the Creative Labs SoundBlaster AWE32/64 cards; etc.
Your device names will then be derived from the protocols they implement,
as well as the driver name. Thus,
sonic_vibes publishes devices in
/dev/audio/raw/sonic_vibes
,
/dev/audio/old/sonic_vibes
,
/dev/audio/mix/sonic_vibes
,
/dev/audio/mux/sonic_vibes
,
/dev/midi/sonic_vibes
,
and /dev/joystick/sonic_vibes
,
each device implementing the protocol that's defined for that part of the
/dev
directory tree. If there is no protocol defined for your device, you can
implement whatever protocol you wish. Try to be consistent in your
naming, though. For instance, a video capture driver for a chip named
Pixtor might publish devices in
/dev/video/pixtor/
. By convention, each
card will be numbered from 1 and up, so the first "pixtor" device would
be called
/dev/video/pixtor/1
.
If your device is of some irregular kind, you can always publish in
/dev/misc/your-name
.
Please avoid publishing directly under
/dev
and
avoid inventing new classes of devices under
/dev/
. If you feel you have
to, contact BeOS developer support or your favorite Be engineer first, to
check that your scheme will work well with the rest of the system.
The first function called in your driver, if you implement and export it,
is the init_hardware()
hook. Please refer to the skeleton driver for the
C function prototype of each driver function.
init_hardware()
will only be called the first time your driver is loaded,
to find and reset your hardware and get it into some known state, if
necessary. Many drivers can do without implementing this hook at all. If
you implement this hook, but don't find any of your cards installed, you
should return a negative error code, such as ENODEV
.
Note: on BeOS, all the POSIX error codes (EXXX
) are negative numbers, so
you should return them as-is to signify error.
The next driver hook being called is init_driver()
, which definitely
should be implemented by all drivers. If your device is an ISA card,
you'll want to call get_module()
on the ISA bus manager module to
initialize a global variable to refer to that module for easy access
(typically, this variable will be named "isa"). For PCI cards, use the
PCI bus manager module, found in
PCI.h
.
Then, use the bus manager module to iterate over available hardware,
looking for instances of the hardware you support. For each piece of
hardware, make sure you enable its PCI bus interface in the configuration
registers if it isn't already. Then allocate whatever memory you need to
keep track of the hardware and the devices that hardware will cause to be
published, and make sure the hardware is in some safe, well-behaved state
and not generating spurious interrupts or other bad behavior. To allocate
memory, use malloc()
. To later deallocate this memory,
use free()
.
/* a global variable for the PCI module */ pci_module_info *pci
; /* in init_driver() */ pci_infoinfo
; intix
= 0; intcards_found
= 0; if (get_module(B_PCI_MODULE_NAME
, (module_info **)&pci
) get_nth_pci_info)(ix
, &info
)) { if (info
.vendor_id
==MY_VENDOR
&&info
.device_id
==MY_DEVICE
) {cards_found
++;my_card_array
[ix
].info
=info
; }ix
++; if (cards_found
==MAX_CARDS
) break; } if (cards_found
< 1) returnENODEV
;names
[cards_found
] =NULL
; /* in uninit_driver() */put_module
(B_PCI_MODULE_NAME
);
If you find no hardware, return ENODEV
. If you find hardware, but
something is wrong and you're not prepared to publish any devices, return
ENOSYS
or ENOENT
. If all is OK,
return B_OK
.
Next, the hook publish_devices()
will be called. It should return a
pointer to an array of C string pointers, one per device you want to
publish, and terminated by a NULL
pointer. For a hypothetical "Pixtor"
driver which publishes one device per installed hardware card, up to a
maximum of four installed cards, you'll typically have a global variable
"names", like so:
static char *names
[5] = { "video/pixtor/1", "video/pixtor/2", "video/pixtor/3", "video/pixtor/4",NULL
/* init_driver() sets unavailable slots to NULL */ };
In init_driver()
you will allocate a name string per device you find
(unless a static array will work, as shown), and make the corresponding
slot in names
point to that string. Then you can just return the
names
array in publish_devices()
:
const char **publish_devices
() { if (names
[0] ==NULL
) returnNULL
; returnnames
; }
Note that the names assume they live under
/dev/
and thus should NOT
contain that part; a typical name may be
video/pixtor/1
.
How does the devfs file system know which driver to open when a program
asks for the device named
/dev/foo/bar/1
? Under R3, devfs opened all
drivers when the system booted and called their publish_devices()
function, so it could know what devices were available. However, this
mechanism doesn't scale well with an increasing number of drivers
available for BeOS, and a new mechanism was introduced in R4.
Inside /system/add-ons/kernel/drivers
(and
~/config/add-ons/kernel/drivers
)
there are now two folders,
dev
and
bin
.
All driver binaries go into
bin
,
and symlinks to the drivers go
into the appropriate subdirectory of
dev
.
Thus the hypothetical Pixtor driver would put the driver in
...kernel/drivers/bin
,
and put a symlink to that driver in
...kernel/drivers/dev/video
.
The symlink has to be put there by the installation program or script for the driver, or, for
development purposes, by the driver build process.
Thus, when a client calls
open("/dev/video/pixtor/1")
or
opendir("/dev/video/")
,
devfs will scan all symlinks found in
...kernel/drivers/dev/video
(and subdirectories thereof) and open the
referenced drivers to call their init_driver()
and publish_devices()
functions, in order to figure out which driver(s) publish devices that
would interest the client. Devfs is reasonably smart about only doing
this once, and it uses the modification date of the driver in
.../bin
to
do that, so when you replace your driver with a newer copy, subsequent
open()
calls for your driver will cause devfs to load the new version
(once all the old clients have closed the old driver).
Not having to reboot for the new driver to be found is one of my favorite features of BeOS for driver development.
When a client decides to open one of your devices, the kernel calls your
find_device()
hook with the name in question. It's up to you to map this
name (which you previously published in publish_devices()
) to the right
device type within your driver. If you support only one device type, this
is easy; even if you support more than one, a simple strncmp()
is
typically sufficient.
A "device type" consists of a set of function pointers that define the
interface for a device. In
Drivers.h
you'll find the struct
device_hooks, which is what should be returned from find_device()
. The
hooks for open
, close
, free
,
read
, write
, and control
must be
implemented; the hooks for select
, deselect
,
readv
, and writev
are
optional.
If you don't implement
readv
/writev
, the kernel will
emulate these functions by repeatedly calling your
read
/write
hooks, which may be
less efficient than if you supported the
readv
/writev
functions directly.
Don't confuse the hook name select()
with the Net Kit
function select()
; currently they have nothing to do
with each other.
Once you've returned a device_hooks structure, the kernel calls the open
hook therein, letting you turn the device name into a unique "cookie"
which your other hooks will use to find the open device in other hook
calls. The open mode is O_RDONLY
, O_WRONLY
,
or O_RDWR
. Depending on your
driver's capabilities, you might want to ensure exclusive access to
reading and writing respectively, and return a EPERM
error if someone
tries to open the same device with the same mode twice in a row. It may,
however, make sense to allow one open()
for
O_RDONLY
and another open()
for O_WRONLY
.
The kernel never dereferences the "cookie" value, so it can be a pointer
to some private data you malloc()
, or a pointer to an element in a global
array, or just an index of some sort. Suffice to say that you must be
able to get all necessary state information for the open device, and the
hardware associated with it, when given this cookie in later hook
function calls.
Your open()
hook will typically need to call
install_io_interrupt_handler()
to install an interrupt service routine
for the hardware in question the first time it is opened, if you didn't
already do that in init_driver()
. For PCI devices, you find the values to
pass to this function in the pci_info struct for your hardware. The
"data" value will be passed to your interrupt handler, and thus is
typically your "cookie" value.
Note that the device_hooks structure may acquire more
functions in later versions of BeOS. To tell the kernel what version of the
interface you were compiled with, you should export an int32
variable named api_version
, it should be initialized to
B_CUR_DRIVER_API_VERSION
. Assuming you put your
device_hooks structs in static or global memory, the compiler
will clear out any slots you don't define at the end to
NULL
for the version of the device_hooks
struct you compile with; thus the value of
B_CUR_DRIVER_API_VERSION
changes when the size of the
device_hooks struct changes. Just adding this line to your
driver is enough, as long as you include Drivers.h
before it:
int32api_version
=B_CUR_DRIVER_API_VERSION
;
When the user is done with your device, he calls
close()
on the file descriptor that references it.
When the file descriptor is closed (or when the last file descriptor is
closed, if the user uses dup()
), the kernel calls your
close()
hook. You should start shutting down the
device; set a status bit so that future read()
,
write()
, and control()
hook calls
will return an error, and preferably un-wedge any outstanding blocking I/O
requests and have them return EINTR
. One technique for
doing this is to simply delete the semaphores you use for synchronizing
I/O. The acquire_sem()
calls in your driver hooks
should then detect the B_BAD_SEM_ID
error and take
that to mean that the device is being shut down, and return
EINTR
to the calling client.
Once all outstanding I/O requests have returned from your driver, the
free()
hook is called. Here is where you can
deallocate all memory you allocated in open()
or
during the course of dealing with the specific open device (as indicated by
the cookie), and re-set your driver to accept a future
open()
for that device name. Note that there will be
exactly one call to the free()
hook for each call to
the open()
hook, and that a call to
free()
for the cookie returned by
open()
will always come after a call to
close()
for that cookie. There is no relation between
different cookies returned by different calls to
open()
; as far as the kernel knows they are
independent.
The free()
hook is a good place to call
remove_io_interrupt_handler()
to remove the interrupt
handler for your device if you installed it in open()
.
If you allow multiple open()
s, it's easier to install
the handler (once) in init_driver()
and remove it in
uninit_driver()
; don't install a handler more than
once for the same hardware! Pass the same "data" value as you
passed to install_io_interrupt_handler()
in
open()
(i.e., for most devices, your
"cookie" value).
Your interrupt handler is called whenever an interrupt on your interrupt
number occurs. Because of interrupt sharing, your hardware may not be the
hardware that generated the interrupt. Your interrupt handler will be
called on to figure out whether the interrupt was caused by your
hardware, and if so, to handle it. The first thing you do in the
interrupt handler should be to read the appropriate status register on
your hardware, and if the interrupt was not generated by your hardware,
immediately return B_UNHANDLED_INTERRUPT
. This lets the kernel move on to
other interrupt handlers installed for the same interrupt number and see
if they can handle the interrupt.
If the interrupt was indeed generated by your hardware, you can go ahead
and handle the interrupt, and then return B_HANDLED_INTERRUPT
.
While your interrupt handler runs, interrupts are turned off. Thus, threads
cannot be rescheduled, and other interrupts cannot be handled. This means
that your interrupt handler should run as fast as possible. A typical
interrupt handler just acquires a spinlock (for mutual exclusion with
user-level threads), adjusts some internal data structure, and quite
possibly releases a semaphore which the user thread
(read()
, write()
, or
control()
) is waiting for. Because rescheduling with
interrupts disabled can cause a total system hang, you should release
semaphores using release_sem_etc()
and pass the
B_DO_NOT_RESCHEDULE
flag, like so:
release_sem_etc
(my_cookie
->some_semaphore
, 1,B_DO_NOT_RESCHEDULE
);
The scheduling quantum on BeOS is 3000 microseconds. Thus, if you release
a semaphore without rescheduling, the longest you may have to wait before
a reschedule happens, and the scheduler gets a chance to notice that your
semaphore has become available, and thus be able to schedule the thread
waiting on the semaphore, is 3 milliseconds. If this is too long (for
low-latency media devices like audio and MIDI, for example) your
interrupt handler routine can return the special value
B_INVOKE_SCHEDULER
, which means that you handled the interrupt, and want
a thread reschedule to happen at the earliest possible time. The kernel
then calls resched()
as soon as it leaves interrupt level, which gives
the scheduler a chance to notice that your semaphore has been released
and your waiting thread is now ready to run.
Note that, because of multithreading and thread priorities, your thread may not be the thread chosen to run just because a reschedule happens. If you have really low latency requirements, and can't afford to have lower-priority threads come between your interrupt handler and your waiting thread getting scheduled, you have to use real-time priority for the thread waiting for the interrupt. Using real-time priority for threads is dangerous, however, because they may completely lock out other threads from the system, including the graphics threads that draw to the screen, making the system appear "hung" if your real-time thread does too much work without synchronizing with a blocking primitive (like a semaphore).
Now that you know how your device is loaded and unloaded, and how to handle interrupts generated by your hardware, you can design the rest of your device API to be used by user-level clients.
The read()
hook is called in response to a call to the user-level
function read()
on a file descriptor that references your device. The
cookie for your device will be passed to the read()
hook, as well as the
current position, as maintained by the kernel file descriptor layer. If
your device does not support positioning (seeking) you can ignore the
position parameter.
Your job inside the read()
hook is to transfer data into the buffer
passed into the read()
hook. The buffer has a size of *numBytes
. You
should transfer at most that many bytes, and then set *numBytes
to the
number of bytes transferred. If any bytes were transferred, return B_OK
.
If an error occurred and/or no data was transferred, set *numBytes
to 0
and return a negative error code.
Please note that the buffer pointed at by "data" will typically be in the
user space of the team calling read()
. It will typically be in
discontiguous memory, and it will not be locked in physical RAM. Thus, it
is not accessible from an interrupt service routine, nor can you DMA
directly into it without first locking the buffer and getting the
physical memory mapping for it:
status_tread_hook
(void *cookie
, off_tposition
, void *buffer
, size_t *numBytes
) { longentries
= 2+*numBytes
/B_PAGE_SIZE
; physical_entry *pe
= (physical_entry *)malloc
(sizeof
(physical_entry)*entries
); status_terr
;lock_memory
(buffer
, *numBytes
,B_DMA_IO
);entries
=get_memory_map
(buffer
, *numBytes
,pe
,entries
); /* set up and start your DMA here */ ... /* assume your interrupt handler will release this semaphore when DMA done */err
=acquire_sem
(my_dma_semaphore
);unlock_memory
(buffer
, *numBytes
,B_DMA_IO
); if (err
<B_OK
) { *numBytes
= 0; }free
(pe
); returnerr
; }
The same rules apply for the write()
hook, except that the data transfer
direction is from the buffer passed by the client to your hardware.
An alternative is to use a contiguous buffer in kernel space that you
allocate and copy to/from in read()
and
write()
. If you have a sound card
that uses a cyclic auto-repeat DMA buffer, this is often a good solution,
for example. However, if the data rate is high, such as for live video or
fast mass storage devices, you want to avoid copies. You might choose to
just have read()
and write()
return
an error, and use ioctl()
exclusively
for communicating with your device. Another option is to make ioctl()
the
preferred protocol, but have read()
and write()
call the appropriate
ioctl()
functions for convenience.
These kinds of decisions are easier if you're implementing a device for which Be has defined a protocol, because then you just follow the protocol. However, if you're implementing a driver for a device for which there is no predefined protocol, or if your device will have significantly better performance using some other protocol, you'll have to design the driver protocol on your own.
The control()
hook is called in response to the user-level client calling
the ioctl()
function:
struct the_args { inta
; int *b
; }; intfoo
; struct the_argsargs
;args
.a
= 1;args
.b
= &foo
;err
=ioctl
(fd
,SOME_CONSTANT
, &args
); if (err
== -1)err
=errno
;
The control()
hook receives the integer constant passed to ioctl()
, as
well as the pointer argument. Currently, the size
argument will always
be 0 when passed to the hook, so you can ignore it. Assume that the
pointer argument is correct for the integer constant in question.
You can start numbering your own operation constants from
B_DEVICE_OP_CODES_END
+1 (in
Drivers.h
). If you want to avoid the risk
of clashing with someone trying to use a protocol you do not know about
on your device, you can choose an arbitrary larger number to start
numbering from, such as your birthday or something. As long as the
numbers (when read as signed 32-bit integers) are larger than
B_DEVICE_OP_CODES_END
.
In the example above, your device control hook can look like this:
status_tcontrol_hook
(void *cookie
, uint32operation
, void *data
, size_tlength
) { my_device *md
= (my_device *)cookie
; status_terr
=B_OK
; switch (operation
) { caseSOME_CONSTANT
: { struct the_args *ta
= (struct the_args *)data
; inti
; if (ta
->a
>MAX_INDEX_FOR_MY_DEVICE
) {ta
->a
=MAX_INDEX_FOR_MY_DEVICE
; } if (ta
->b
==NULL
) {err
=B_BAD_VALUE
; } else {err
=acquire_sem
(md
->lock_sem
); if (err
<B_OK
) { returnerr
; } for (i
=0;ia
;i
++) {ta
->b
[i
] =md
->some_value
[i
];release_sem
(md
->lock_sem
); } } break; default:err
=B_DEV_INVALID_IOCTL
; break; } returnerr
; }
Semaphores may cause a reschedule to another thread when released. Thus,
you should not release a semaphore from an interrupt handler, or with
interrupts disabled, without passing the B_DO_NOT_RESCHEDULE
flag (using
release_sem_etc()
).
It is generally a good idea to put as much code as possible at the user
level, and make your driver as shallow as possible even if you aren't
forced to by porting C++ code. The less code there is in the driver, the
less locked memory will be used, and the less code there is that may
crash the kernel. All of your driver's code and global/static data, as
well as all memory returned by malloc()
called from a driver, is locked
(and thus safe to access from an interrupt handler). Be gentle on the
system.
Disabling interrupts is NOT sufficient to guarantee atomicity, because on an SMP system, the other CPU may be calling into your driver at the same time. For synchronization with data accessed by interrupt handlers, you have to use a spinlock. Spinlocks are the most primitive synchronization mechanism available; basically they use some atomic memory operation to test-and-set a variable. When the test-and-set fails, the calling thread just keeps trying (busy-waiting) until it succeeds. Thus, contention for spinlocks can be quite CPU intensive. Therefore, they should be used sparingly, and only to synchronize data that really has to be touched by an interrupt handler (since semaphores cannot be used by interrupt handlers).
A spinlock is simply an int32 value in some permanent storage (a global,
or some memory you malloc()
as part of opening your device) that is
initialized to 0 before being used the first time. To acquire a spinlock,
you turn off interrupts and then call acquire_spinlock()
:
/* these are global variables */ int32my_spinlock
= 0; charprotected_data
[128]; intprotected_ctr
= 0; /* Acquire spinlock. */ cpu_statuscp
=disable_interrupts
();acquire_spinlock
(&my_spinlock
); /* Do protected operations -- this should be fast and not cause */ /* any reschedule, so don't call malloc() or any semaphore operations */ /* or any function that may call these functions. */protected_data
[protected_ctr
++] = 0;protected_ctr
=protected_ctr
& 127; /* Release spinlock. */release_spinlock
(&my_spinlock
);restore_interrupts
(cp
); /* in your interrupt handler */ /* serialize with user code, possibly on other CPUs */acquire_spinlock
(&my_spinlock
); /* Do protected operations like hardware register access */release_spinlock
(&my_spinlock
);
If you fail to disable interrupts before acquiring the spinlock, you'll
deadlock on single-CPU machines, because your interrupt handler may then
be called (and try to acquire_spinlock()
your spinlock) while the regular
thread is holding the spinlock. That would be bad.
Many people find it convenient to wrap spin-locking into two
general-purpose lock()
and unlock()
routines to not forget to turn off
interrupts. You can use the same routines inside your interrupt handler,
because calling disable_interrupts()
and later
restore_interrupts()
is OK
even inside an interrupt handler (even though interrupt handlers run with
interrupts already turned off). Spinlocks, like semaphores, don't nest
like that, however, so think about what you're doing and don't call
functions that may lock a spinlock from some function that already holds
the same spinlock.
/* Assuming you keep state information about your hardware */ /* in a struct named my_card, with a 0-initialized */ /* spinlock named hardware_lock */ cpu_status lock_hardware(my_card *card
) { cpu_statusret
=disable_interrupts
();acquire_spinlock
(&card
->hardware_lock
); returnret
; } voidunlock_hardware
(my_card *card
, cpu_statusprevious
) {release_spinlock
(&card
->hardware_lock
);restore_interrupts
(previous
); }
It's important to not disable interrupts for a long time. As a general rule, no more than 50 microseconds is allowable. If you disable interrupts for longer, you'll jeopardize the overall performance of the BeOS and the machine it's running on. Similarly, your interrupt service routine should not run for more than 50 microseconds (and less is, of course, better).
You may find the lack of deferred procedure calls disturbing if you come from some other driver architecture. However, the time it takes for BeOS to service an interrupt, release a semaphore, and cause a reschedule into a user-level real-time thread is often less than the time it takes for other operating systems just to handle the interrupt and get to the deferred procedure call level. Thus, we prefer to do what needs to be done in the user-level client threads that call into the device hooks.
If at all possible, let the user of your device spawn whatever threads
your device needs. Kernel threads are very tricky and live by different,
undocumented rules. If you find a need for a periodic task, look into
using timer interrupts (available as of BeOS R4.1). Look for add_timer()
and cancel_timer()
in
KernelExport.h
.
If you wish to use a kernel thread in your driver, there are several
pitfalls that make doing this a bad idea. The kernel team is a team just
like any other team, and your kernel thread will have a stack in the
upper half of the kernel team address space. This stack (and stacks of
other kernel threads) is not accessible from user programs, and thus is
not accessible from device hooks called by user programs. Only the lower
half of the kernel team address space (0x0–0x7fffffff
) is accessible to
all teams when they enter the kernel.
You also cannot use wait_for_thread()
in your
close()
or free()
hooks,
because doing so causes a deadlock with the psycho_killer thread, which
is responsible both for reaping dead threads and for freeing file
descriptors and their associated devices. Thus, it is impossible for you
to be perfectly sure that your kernel thread has terminated before your
free()
hook returns. This is a big enough problem that you should
reconsider using kernel threads at all in your driver, if there is some
other possibility. This specific problem will be fixed in a future
version of the BeOS, but all the other problems with kernel threads will
still remain; also, R4 will be the baseline BeOS for some time to come,
so a design without kernel threads is thus more widely compatible.
Again: Put the threads you need in the user-level client (add-on,
application, whatever). If the driver needs to perform periodic tasks not
in response to hardware interrupts, use timer interrupts. They are even
more lightweight than threads, and have fewer of the problems mentioned
above. As with any interrupt routine, timer interrupts still cannot
access memory that is not in the kernel space; thus if you need to write
into user-supplied buffers, do it in your driver read()
,
write()
and control()
hooks.
Good luck!