Table of Contents
BE ENGINEERING INSIGHTS: BeOS Kernel Programming Part V: Interfacing With ISA and PCI By Dmitriy Budko dmitriy@be.com
The goal of this article is to describe how BeOS device drivers communicate with devices on the most common busses -- ISA and PCI. There are many types of hardware busses in the current PCs and Macs: ISA, ADB, SCSI, PCI, USB, AGP, VLB, IEEE 1394, I2C, etc., but the majority of devices are located on ISA and PCI busses, so this article will focus on them. The question I'll answer is how a BeOS device driver can access a hardware device on ISA and PCI busses. But before going into the BeOS details I'll briefly describe the software-visible characteristics of ISA and PCI. ISA appeared on the first IBM PC (8088), and was slightly refined in the later IMB PC AT (80286). Since then there have been no changes and ISA has become old and difficult to use. Fortunately, it's begun to be replaced. The latest Intel chipset i810 doesn't directly support ISA. There are two address spaces on ISA: one is a 64kB I/O space and another is a 16 MB memory space. An ISA device may occupy portions of both address spaces and respond to I/O and memory read/write cycles initiated by another device on the bus, usually a CPU. In this case the device is passive and the CPU has to read/write data from/to the device to/from system RAM. An ISA device can be more active and can transfer data without constant attention from a CPU by using an 8237-style DMA controller (as the majority of ISA sound cards do) or by being an ISA bus master (a few SCSI and LAN adapters do this). ISA DMA is obsolete and convoluted; this article does not discuss it. The PCI bus is a modern, sane replacement for ISA and has multiple advantages over it. A system can have multiple PCI busses, PCI has *good* PnP, it's faster (132 MB/s theoretical bandwidth, 100 MB/s throughput with the real hardware: Matrox Millennium II and Intel 440LX chipset), etc. In general PCI is a 32-bit bus. There are 64-bit data transfer and 64-bit addressing versions of PCI but they are currently used only in high-end servers. PCI (32-bit version) has three address spaces:
Many PCI devices, especially high-performance ones, can be PCI bus masters, which lets them transfer data from/to RAM or another PCI device without direct intervention from the CPU. How then, does a BeOS kernel device driver interact with ISA and PCI? By way of BeOS ISA and PCI kernel modules, defined in <ISA.h> and <PCI.h>. The relevant portion of these modules is as follows: typedef struct isa module info isa module info; struct isa module info { .............................. uint8 (*read io 8) (int mapped io addr); void (*write io 8) (int mapped io addr, uint8 value); uint16 (*read io 16) (int mapped io addr); void (*write io 16) (int mapped io addr, uint16 value); uint32 (*read io 32) (int mapped io addr); void (*write io 32) (int mapped io addr, uint32 value); void * (*ram address) (const void * physical address in system memory); ........................ }; struct pci module info { bus manager info binfo; uint8 (*read io 8) (int mapped io addr); void (*write io 8) (int mapped io addr, uint8 value); uint16 (*read io 16) (int mapped io addr); void (*write io 16) (int mapped io addr, uint16 value); uint32 (*read io 32) (int mapped io addr); void (*write io 32) (int mapped io addr, uint32 value); long (*get nth pci info) ( long index, /* index into pci device table */ pci info *info /*caller-supplied buffer for info*/ ); uint32 (*read pci config) ( uchar bus, /* bus number */ uchar device, /* device # on bus*/ uchar function, /* function # in device */ uchar offset, /* offset in configuration space */ uchar size /* # bytes to read (1, 2 or 4) */ ); void (*write pci config) ( uchar bus, /* bus number */ uchar device, /* device # on bus */ uchar function, /* function # in device */ uchar offset, /* offset in configuration space */ uchar size, /* # bytes to write (1, 2 or 4) */ uint32 value /* value to write */ ); void* (*ram address) ( const void *physical address in system memory); }; The general API of BeOS modules has already been described in a previous newsletter article, "BeOS Kernel Programming Part IV: Bus Managers," by Brian Swetland <http://www-classic.be.com/aboutbe/benewsletter/volume III/Issue28.html>, so I'll focus on ISA and PCI specifics. 1. First, how do find your device? Usually this is done in init hardware() and/or init driver() hooks. For ISA, however, there is no easy way to find or detect a device, so an ISA driver has to:
Finding a PCI device is easy. Use get nth pci info() to iterate through the list of all PCI devices in the system to find your device. For example, the following code shows how to find a PCI USB UHCI controller and check what specific version it is: bool uhci present(void) { pci info info; int i; for (i = 0; ; i++) { if (pcim->get nth pci info (i, &info) != B OK) return FALSE; /* Error or end of device list */ /* do not support PIIX3 - too many HW bugs */ if (info.vendor id == 0x8086 && info.device id == 0x7020) continue; if (info.class base == PCI serial bus && info.class sub == PCI usb && info.class api == PCI usb uhci ) break; } return TRUE; /* Device was found */ } 2. Next, how do you find the resources the device uses (I/O and/or memory addresses, IRQs, etc). This is done in init driver() hook. For ISA you have to use the same methods as you would for finding the device. For PCI use the pci info structure that you use to find the device. Remember that init hardware() is called only once and the driver can be unloaded afterwards, so the driver can't easily remember the information from init hardware(). For example: /* find PCI bus, device, function, IO, IRQ */ for (i = 0; ; i++) { if (pcim->get nth pci info (i, &info) != B OK) return B ERROR; /* Error or end of device list*/ if (info.class base == PCI serial bus && info.class sub == PCI usb && info.class api == PCI usb uhci ) break; } /* Handle broken devices that violate PCI spec and don't use base register 0. */ for( base reg num=0; (base reg num < 6) && (info.u.h0.base registers[base reg num] == 0); base reg num++) ; /* refuse to find the controller and don't load the driver if the controller is disabled in BIOS. */ if( (base reg num == 6) || (info.u.h0.interrupt line == 0) || (info.u.h0.interrupt line == 0xFF) ) { dprintf("USB HC is disabled by BIOS\n"); return B ERROR; } /* remember the resources */ access range.range start = info.u.h0.base registers[base reg num]; access range.range length = info.u.h0.base register sizes[base reg num]; access range.range in memory space = !(info.u.h0.base register flags[base reg num] & PCI address space); irq = info.u.h0.interrupt line; 3. Now you enable and map registers. To do this, set the appropriate bits in the control registers of the PCI device, including I/O access enable, memory access enable, and bus master enable. For example: command reg = pcim->read pci config( bus, device, function, PCI command, 2); command reg |= PCI command io | PCI command memory | PCI command master; pcim->write pci config( bus, device, function, PCI command, 2, command reg); 4. If the device registers are located in memory space, the device driver has to map this memory by map physical memory() with the appropriate flags, then use the returned virtual address of the area as a pointer to the registers. For example, (without error handling) from the generic graphics driver, frame buffer in [0], control registers in [1] (complete source code is on BeOS CD): sprintf(buffer, "%04X %04X %02X%02X%02X regs", di->pcii.vendor id, di->pcii.device id, di->pcii.bus, di->pcii.device, di->pcii.function); si->regs area = map physical memory( buffer, (void *) di->pcii.u.h0.base registers[1], di->pcii.u.h0.base register sizes[1], B ANY KERNEL ADDRESS, 0, /* B READ AREA + B WRITE AREA, */ /* neither read nor write, to hide it from user space apps */ (void **)&(di->regs)); sprintf(buffer, "%04X %04X %02X%02X%02X framebuffer", di->pcii.vendor id, di->pcii.device id, di->pcii.bus, di->pcii.device, di->pcii.function); si->fb area = map physical memory( buffer, (void *) di->pcii.u.h0.base registers[0], di->pcii.u.h0.base register sizes[0], B ANY KERNEL BLOCK ADDRESS | /* BLOCK - try to use special features of the CPU like BAT or large pages */ B MTR WC, /* use write combining */ B READ AREA + B WRITE AREA, &(si->framebuffer)); 5. Now use read/write io xx() functions to read/write 1/2/4 bytes from/to a device register if the register is in the I/O space of ISA or PCI. Example from the USB HC driver: uint16 frame number = pcim->read io 16( access range.range start + 6); Use pointers to read/write data if the registers are located in memory space. Writing four bytes to the beginning of the frame buffer:
6. The purpose, arguments, and use of all the functions above should be clear to anyone who is familiar with ISA and PCI. But what does void* ram address( const void *physical address in system memory); do? If the device is using bus mastering, the driver has to lock memory() and get memory map() for all data buffers and tell the device to use returned physical RAM addresses. However, this is not enough. On some systems, like the BeBox, the RAM address ! = PCI address, so the driver has to convert the RAM address to a PCI address for each physical entry by calling ram address(). Here's an example, with no error handling: status t foo write(void *cookie, off t position, const void *data, size t *numBytes) { int i; physical entry sg list[MAX FOO SG ENTRIES]; lock memory(data, *numBytes, B DMA IO | B READ DEVICE); /* flags for cache coherency on some systems */ get memory map( data, *numBytes, &sg list, MAX FOO SG ENTRIES); for(i=0; sg list.size != 0; i++) sg list[i].address = pcim-> ram address(sg list[i].address); send sg list to foo(&sg list); start foo bus master read(); block until foo interrupt(); return check status(numBytes); } So... now you have enough information to *properly* access PCI and ISA devices. By the way, AGP is a superset of PCI, so you also now know how to use the basic functionality of AGP. BE ENGINEERING INSIGHTS: Farewell BSound and BSoundFile (All Hail BGameSound and BMediaFile) By Jon Watte hplus@be.com
Among other important improvements in BeOS Release 4.5 is the BMediaFile, which gives access to various kinds of media file formats, and BGameSound (with subclasses), which allows simple but efficient playback of sound effects and background sounds. The BSoundFile class has been with us for a long time, and was starting to show its age. Many older programs that still run on PowerPC depend on idiosyncrasies of this class, so rather than make it use the same mechanism as BMediaFile to access data, which would break the previous semantic of the file (we tried this), we decided to stay compatible, and suggest that all newer applications use BMediaFile for all their media reading/writing needs. However, if you have an application which uses BSoundFile, you may need some features that BMediaFile and BMediaTrack don't provide. Most notably, BMediaTrack reads audio frames in blocks of a predetermined size, whereas BSoundFile lets you read any number of frames at any time. BMediaTrack also may not be precise in seeking to a specified frame location (because of compression algorithm constraints). I present here a simple wrapper for BMediaTrack, known as ATrackReader. It lets you treat a generic media file, accessed internally through a BMediaFile object, much like a BSoundFile. It's also a good introduction to using BMediaFile/BMediaTrack to read data in general. If you use a BSoundPlayer with a number of BSounds to play sound effects, you'll probably want to change over to the new BGameSound system the next time you overhaul your code. BGameSound is designed to allow for hardware acceleration in a future version of BeOS (when this will happen is TBD), and it's also designed to be really simple to use! If you used BSound with a chunk of data in memory as your data, you now create a BSimpleGameSound object. If you use BSound with a large-ish sound file on disk for background music or other something similar, you now create a BFileGameSound. BSimpleGameSound can be created either with a pointer to data and a description of the data pointed to (it should be uncompressed PCM sample data), or with an entry ref, in which case it will load the sound file from disk (uncompressing, if necessary) into memory so it's always readily available to be played. The BGameSound system makes a copy of the data you provide it, so you can free that memory as soon as the object is created. If you need more than one copy of the same sound running, you can call Clone() to get a second BSimpleGameSound which references the same data as the first. When you make a Clone(), that clone references the same internal copy with a reference count, so no extra memory is wasted. The Be Book accidentally documents an earlier behaviour where data was copied inside Clone(). To play the sound, just call StartPlaying() on it. BFileGameSound is created with an entry ref as argument, and can optionally be set to looping or non-looping mode. When you call StartPlaying(), it will start playing, and keep going until you stop it with StopPlaying(), or, if it's not looping, until it reaches the end of the file. It's important to note that the first BGameSound instance you create determines the format of the connection between BGameSound and the Audio Mixer. In our sample program, we create a dummy 44 kHz stereo sound and immediately delete it to establish the connection in a known format, since otherwise the first file the user drags into the program will determine the format that all files will be played back as. All BGameSound instances that are playing are mixed into one connection to the Audio Mixer; this connection is currently named after your application with no way of changing it. In some future version of the API, we may let you create more than one connection, and name these connections. That's what the BGameSoundDevice argument is for in the constructors for these classes; however, we currently only support the default (NULL) device, so you can leave it to the default value without worrying about it. If you want to set the pan position or gain (volume) of a BGameSound, you do that by calling SetPan() and SetGain(). The "duration" parameter (which is optional) allows you to specify that the change should take place over some amount of time, if the sound is currently playing. Thus, if a file was playing, and you wanted to fade it out over the course of two seconds for a soft ending, you could call SetGain(0.0, 2000000LL). You can also change the effective sampling rate of a BGameSound. This changes both the pitch and duration of the sound. The BGameSound system contains a built-in software resampler which uses a fast, reasonable quality 0-th order resampler. There is no additional overhead of playing a sound at some other sampling frequency than the one you initially specify. However, there is no SetSamplingRate() function; instead, you have to use the low-level SetAttributes() function to change the B GS SAMPLING RATE attribute. Again, you can specify a duration during which the change ramps in. Thus, if you're playing a sound at a 22000 Hz sampling rate, and ramp it to 12000 Hz with a duration of 500000, it will take approximately half a second for the full change to take effect. The resulting sound effect is similar to a tape deck or record slowing down. Specific details are found in the gameplay.cpp file in the source code that goes with this article: <ftp://ftp.be.com/pub/samples/game_kit/gameplay.zip>. To build the sample code, just "cd" to the directory where you unpacked it in Terminal, and type "make". Run it with obj.x86/gameplay (or obj.ppc/gameplay) from the command line, or double-click it. You can drag a sound file into the lower half of the window, and the program will load it as a BSimpleGameSound or a BFileGameSound. You can then start playing with the "Start" button, and play with volume/sampling rate with the sliders in the window. Note that the pop-up menu for sound kind (SimpleGameSound or FileGameSound) only decides what kind of object to create the next time you drag a file to the window; it does not change the type of the current sound. DEVELOPERS' WORKSHOP: The Magic of Messages Part 1: The Sending By Owen Smith orpheus@be.com "Developers' Workshop" is a weekly feature that provides
answers to our developers' questions, or topic requests.
To submit a question, visit
http://www.be.com/developers/suggestion_box.html.
In the next couple of installments, we'll delve into the
gory details of what happens when you send a BMessage in the
BeOS. I won't be discussing archival or other ancillary uses
of BMessages here. We'll just look at pure, simple messaging
-- BeOS-style.
The Whole Enchilada
Let's start with an overview of the messaging process. To
begin, let's say I have a message that I want to deliver to
a particular messaging target (called a "handler" in BeOS).
That messaging target lives in a process, called a "looper,"
somewhere in my system. The looper's job is to receive
incoming messages and reroute them to the appropriate
messaging target.
To send a message to a handler, I create an object which
acts as the delivery mechanism for the message, called a
messenger. I set this messenger up to point at my handler,
and tell it to send my message, also specifying a place
where replies to this message can go. The messenger, in
turn, turns my message into a flattened stream of data and
writes it to a low-level data queue called a port. Once the
writing is done, the message has been delivered.
On the destination end, the port serves as the mailbox of
the looper with whom my target resides. The looper reads the
data from the port and reconstructs a message from the data.
It then passes the message through a series of steps that
determine who the final handler of the message should be.
Finally, once the handler has been determined, the looper
tells the handler to handle the message. The handler does
whatever is necessary to respond to the message, including
the option to send back a reply to the message, or to claim
ownership of the message for later processing. Once the
handler is done with the message, the looper gets rid of the
message (unless it's been detached), and goes back to look
for any other incoming messages.
Now that you've seen what the whole enchilada looks like,
let's get down to business.
Step 1: Create
The first step to sending a message, of course, is creating
it. As you probably know, a BMessage contains a "what" field
that briefly identifies the contents of the BMessage, and a
number of labeled fields that contain the data.
Generally, when you're creating BMessages to send to
somebody, it works extremely well to allocate them on the
stack. You retain ownership of the message when you send it,
and the message is automatically cleaned up for you when
you're done.
One exception to this is if you're creating a "model
message" for some other object to use for sending messages,
such as BInvoker-derived classes. In these cases, you'll be
handing the messages off to somebody else, so you'll need to
allocate them on the free store:
There are a host of functions that let you throw all kinds
of data into a BMessage, including raw data if you need to.
There is a similar set of functions for retrieving stuff
from a BMessage. One stumbling point that I regularly see
has to do with ownership of the data that gets stashed in
the BMessage. When you use the BMessage::Add* functions, the
data is always copied into the message for you, so you're
responsible for cleaning up anything that you add to the
message. For example, let's say you had an array of data you
wanted to stuff into a BMessage. You can't just add the data
into a BMessage and
forget about it; you have to clean it up afterwards:
The ownership rules for retrieving data are trickier, and
are a common source of errors. Most of the time when you
retrieve data, the data is copied into whatever you pass
into the BMessage::Find* functions, so there are no
complications. However, in the special cases of FindData and
FindString, the pointer you get back actually points to data
inside the BMessage, and you have to copy it out yourself!
For example, let's say you're going to retrieve a string
from a message. Be careful that you're doing the right
thing...
A BMessage can technically store as much data as you have
memory for, though you'll see that it's probably not a
good idea to stash megabytes of data into a BMessage for
purposes of messaging.
Step 2: Target
Handlers and Loopers
Once we've created the BMessage, we need to know where to
send it. All potential targets of a message derive from a
class called BHandler. Many Application and Interface Kit
classes derive from BHandler: applications, windows, views,
controls, and so forth.
One interesting fact about the BeOS is that you cannot send
messages directly to a BHandler. The only objects capable of
actually receiving a BMessage are objects called BLoopers.
Applications and windows are both examples of BLoopers.
A BLooper is an object whose job is to receive messages and
dispatch them as they arrive. This behavior makes BLoopers
the "Grand Central Stations" of the messaging world.
BLoopers maintain a list of targets (i.e., BHandlers). When
you send a message to a target, it actually makes its way to
the BLooper that owns the target. The BLooper finds the
target among its list of BHandlers and dispatches the
message to the target. Because of this, your target must
belong to some BLooper; you cannot just send a message to a
BHandler floating in free space. (BLooper also derives from
BHandler, so you can also send messages to the BLooper
itself.)
Messengers
The fundamental delivery mechanism of the BeOS messaging
system is BMessenger. BMessengers are lightweight objects
that identify a message target in your system. Let's examine
how to use BMessengers to specify various kinds of targets:
* Local Targets
The easiest case is sending a message to a target in our own
application (what I call "app-local targets"). In this case,
we can create the BMessenger and target the recipient
directly.
If you look at the BMessenger constructor, you'll see that
there are three useful ways you can construct the messenger
for delivery to app-local targets:
BMessenger msgr(NULL, window);
If there is no preferred handler, this will target the
looper itself.
Note that BWindows have a special interpretation for the
preferred handler. In a BWindow, the preferred handler
is the view that currently has the focus.
BMessenger msgr(view, NULL);
The looper in this case is assumed to be the handler's
owner, but you can redundantly specify the looper if you
want; the BMessenger will perform a sanity check for you.
BMessenger msgr(window, NULL);
Although this looks similar to (1), there's a big
difference in behavior! Be sure that you recognize this
distinction.
For app-local targets, there are also ways to send a message
that don't require us to create a BMessenger; I'll talk
about those a little later.
* Remote Targets
Now, what if we want to send a message to a target in some
other application? In this case, the BHandler lives in a
different address space, so we unfortunately can't create a
BMessenger to target that BHandler directly. What we *can*
do, however, is target the remote application itself, and
ask the application to create the messenger for us. Here's
what you do:
This technique requires you to work out a messaging protocol
between yourself and the remote application for identifying
the target. Scripting is a great way to do this if the
application supports it (as many Be apps do). For example,
using Attila Mezei's "hey" command line tool (available on
BeWare), I can do the following:
hey Tracker get Window 0
This sends a message off to the Tracker application, and
receives in return a messenger that targets the first window
in the Tracker's window list. If you want to learn more
about scripting, take a gander at:
http://www-classic.be.com/developers/developer_library/scripting.html
* Extremely Remote Targets
Finally, let's entertain the possibility that we want to
send a message to a target on some other machine.
Interestingly, there are a few third-party developers that
have created solutions for this, providing
BMessenger-derived classes that allow you to specify targets
on machines across a network. See BeWare
(http://www.be.com/beware/) for more details.
Step 3: Send
Now that you have a BMessage and a target BHandler, how do
you send the message? There are two approved ways of doing
this, and one sneaky shortcut. I'll discuss the approved
ways first; the sneaky shortcut will have to wait until next
week.
* BMessenger::SendMessage can send a message to either
app-local or remote targets. It can either take a BMessage
or just a "what" code (which it quickly wraps a BMessage
around). It can also deliver messages in one of two ways:
* BLooper::PostMessage is a method you can use to send
messages to app-local targets. It effectively does the
work of creating a BMessenger and calling SendMessage for
you. You call it on the BLooper that owns your target.
Here are three ways you can identify targets with
PostMessage:
As you can see from the above, there is an important, and
often confusing, distinction to make between passing a NULL
handler and passing no handler at all!
Like SendMessage, PostMessage allows you to pass a "what"
code instead of a full-fledged BMessage. Unlike SendMessage,
PostMessage does NOT allow you to do a synchronous send:
replies go to your application object, or to a reply handler
if you've specified one.
Pay No Attention to the Man Behind the Curtain
Some of you may be wondering how messages actually travel
from one application to another. I'll break the magician's
creed of secrecy and tell you that no voodoo is involved. In
fact, the underlying mechanism for passing messages in the
BeOS is the port. If you've ever run 'listport' from a
Terminal, this will probably come as little surprise to you.
For those of you who think the Kernel Kit is a package of
frozen corn, a little orientation here may be in order. A
port is a kernel primitive that implements a "message
queue." At this low level, a "message" is little more than a
buffer of raw data. There are two basic operations you can
do with a port:
* Write to the port. You provide a buffer of data. This data
is copied into the port's queue as a brand-new message --
in other words, each time you write to the port, the data
is treated as a new entity. When you create the port, you
tell it the maximum number of items that it can contain. If
you try to write a new item when the port is full, you
generally wait until items are removed before placing your
item in the queue (with the option to just give up if a
specified amount of time has elapsed, and the queue is
still full).
* Read from the port. Again, you provide a buffer of data.
If there are any items in the queue, the oldest item's
data is copied into your buffer, and the item is removed
from the queue. If you try to read from an empty port,
you generally wait until an item arrives before reading it
(again, with the option to bail out if you feel that
you've spent too long waiting).
One nice thing about ports is that they work extremely well
in multithreaded situations. Generally, you use ports by
having one thread read from the port, and many threads write
data to the port. By using ports to send data between
threads, you can avoid the Evil Deadlocks that direct data
access can cause. Even better, ports can be accessed from
any address space, so inter-application communication is a
snap with them as well.
How are ports used in the messaging system? Well, each
BLooper maintains its own port, which serves as the delivery
repository for incoming messages. The looper's thread then
repeatedly reads items from this port and handles them as it
sees fit.
So, here's what happens behind the scenes when you send the
message:
Because of this delivery mechanism, the message you hand off
to SendMessage is NOT the same message that the target
receives! Instead, a copy of the data is sent to the
destination. Because the flattening and copying of data
takes a certain amount of time, it's definitely a good idea
to keep the size of your BMessage contents down. If you must
throw around large amounts of data between applications,
consider using shared memory areas to store the bulk of your
data instead.
Another important detail is that the looper's port has a
limited size. Most of the time, the port is more than big
enough for your messaging needs, but under heavy load, that
port can fill up -- and if you're not ready to handle this
case, there's a subtle bug just waiting to bite you when you
need it least!
If a looper's port is chock full of messages when you try to
send a message, you'll have to wait until the port has
emptied a bit before you can write the message data to the
port. This will affect you in different ways, depending on
whether you're using PostMessage or SendMessage. If you're
using PostMessage, the function will immediately return with
an error (B WOULD BLOCK) if it can't write the message data,
and the message won't be sent. If you're using SendMessage,
you specify a "send timeout" value to indicate how long
you're willing to wait for the message to be delivered. If
you don't specify a timeout, the BMessenger will patiently
wait forever for the write to succeed.
The very important corollary to this behavior is that, if
you use PostMessage or specify a send timeout in
SendMessage, there's a possibility that your message won't
be delivered because the function has timed out. Many people
don't take this detail into account in their code, and
during crunch time, they'll sometimes get bit by this subtle
problem. So, if your message absolutely has to get
delivered, make sure you check the return value from
PostMessage and SendMessage, and do something appropriate if
the function times out!
That's it for this week. Next week, we'll examine what
happens on the other side of the connection...
Concerned readers have written to ask why I stopped writing
my weekly column. The cheeky answer is that I've been
suspended by the SEC, but the truth is quite the contrary,
as you'll read in a moment.
For the past four weeks, we've been involved in what is
ritually called an IPO Road Show, touring the U.S. and
Europe to meet with institutional investors. During that
time, and for 25 days following today's IPO, we're in what
is called a "quiet period." This means that we cannot make
any comments that could be construed as "promoting" our
public offering. The company's only allowable public
statement on this matter is contained in the prospectus, a
document filed under SEC supervision.
Under such conditions, given my occasional recourse to
poetic license, I decided that temporary silence was the
safest choice of words. Also, the Road Show process
undeniably consumed a great deal of time and psychic energy.
But to return to the SEC -- I come from a different culture.
Not so long ago in France and other European countries,
insider trading wasn't a crime. In any case, Europe's
clubby, opaque business culture makes enforcement of
prohibitions on such things difficult. Publicly traded
companies publish their numbers months, not days after the
close, and shareholders are subjects, not bosses.
Consequently, I like the climate of greater trust the SEC
fosters in its watchdog and, I might add, guide dog role
here in the U.S. I've read many complaints about the "Plain
English" rule. From my perspective, however, purging
prospectuses and other filings of "whereases" and
"foregoings" can only benefit normal humans who want to
trust the investment process. In our case, the SEC was
extremely punctual, helpful, civil, and service oriented,
right through the very last nervous moments of the process.
I'll be back soon with road stories and other anecdotes --
stand by.
1997 Be Newsletters | 1995 & 1996 Be Newsletters Copyright © 1999 by Be, Inc. All rights reserved. Legal information (includes icon usage info). Comments, questions, or confessions about our site? Please write the Webmaster. |