Introduction to system calls with the dash16
Introduction to system calls with the dash16:
This document summarizes the usage of the dash16 device driver, and a
brief overview of device handling in general.
Unix uses device drivers as low-level interfaces for controlling system
hardware. Each device recognized by the system will have a driver which
interfaces with it and controls basic operations- on a tape drive driver
for instance, a specific call might rewind the tape.
The actual workings behind this interface are encapsulated inside the
driver, allowing the use of a standardized set of system calls to handle
virtually all device functions. These primary "system calls" are: open,
read, write, close, and ioctl.
In a sense, a device driver's job is to allow the programmer to interface
with hardware much as if he were interfacing with any other text file-
such devices are said to be character devices. As a consequence, "file"
and "device" are often used interchangably. In fact, the device is merely a
special type of file actually on the filesystem!
Each running program (process) has a number of file desriptors associated
with it which are used to access open files or devices. This descriptor is
a small integer value. By default, when a program opens it will have 3
descriptors:
(Note is is possible to use FILE pointers instead of file descriptors to
provide bufferred access to the device file. But, as will be explained later,
the file descriptor is needed for a special type of system call - ioctls. If
you are insistant on using a FILE pointer, you can obtain one by calling
fdopen() on the file descriptor. Just don't use the file descriptor for anything
but ioctls after that!)
What follows is a brief synopsis of each of the 5 "primary" system calls.
open - declared in fcntl.h
If succesful, open returns a file descriptor which can later be used
to access the file. The name of the file to be opened is passed as path
and "oflags" allows the file to be opened in a specific way.
oflags is a bitwise OR of mandatory file access modes and othel
optional modes. open MUST specify one of the following access modes:
Optional modes (which may be bitwise OR'd to the mandatory option)
include:
write - declared in unistd.h
This returned value may be lower than nbytes if their has been a
descriptor error, or if the device is particular about block sizes. The
function will return a 0 if the end of the file has been reached, and a -1
if their has been an error in the write call.
read - declared in unistd.h
close - declared in unistd.h
ioctl - declared in unistd.h
ioctl performs the function indicated by cmd on the device
referenced by the descriptor fildes. The ... refers to an
optional parameter. Wether or not ... is needed in the call is
dependent on cmd requiring an additional argument. This additional
argument can be an integer or a pointer.
More about ioctl soon- let's consider the specifics of the dash16 adc driver.
Since the dash16 driver provides an interface for accessing the hardware
like a text file, (a character device) we can perform the simplest of tasks
without concerning ourself with ioctl calls.
First off, we might open the device by making a simple open call, as
To do anything more interesting however, we will need to use a ioctl call
to first configure the card for actual use. Recall that the ioctl function
is called as
Let's first consider the macros which will expand to the usable ioctl
numbers. These macros are defined in asm/ioctl.h (which is included by
linux/ioctl.h):
Each macro defines the direction of the transfer. (read, write, or read/write)
type is simply a unique number, called a "magic" number which will be
unique to the device. Each running device (in kernel space) must have a
unique "magic" number- so the kernel may differentiate among ioctl numbers.
nr is a number which will be unique to the ioctl number with respect to
the device driver. Each ioctl define will use a succesively greater nr
value in dash16.h. Finally, size refers to the size of the data transfer
involved. One should note however, that since sizeof() is part of the
macro expansion, the programmer need only pass the data type- which
will then be evaluated for it's size.
However, as mentioned previously, dash16.h also defines a set of macros
which themselves expand to the necessary _IO, _IOR, _IORW, or _IOW macros.
Of course, this greatly simplifies the programmer's job. These macros
are:
In each define, pay special attention to the type parameter passed
to the _IO macro. The dash16 driver uses pointers as a matter of
form. Notable data types are u8 (typedef'd in sys/types.h),
struct dash16_timer_regs, and struct dash16_channel_range.
Other Pertinent #define's in dash16.h
It is unfortunate that this card has multiple registers with very simmilar
names, all for controlling things related to the interval timer. This
caused the authors much grief in trying to decide what to call various
defines and ioctls (and how to implement them!) Lets go over a few of these
now.
Defines for the control register of the interval timer:
To simplify the configuration of the control register for the interval timer,
the following macros are defined in dash16.h - when collectively OR'd together
they serve to produce a configuration byte which can then be written to
the correct register. The user utilizes these macros via the dash16_timer_regs
structure and the DASH16_TIMER_CTL(R/W)_RAW ioctls.
Note that of course, you as a (Linux) user cannot use outp, or even direct
hardware access. In this example, you would have to do something like:
Then when you finish with the rest of that control structure, you send
it off via: ioctl(filedes, DASH16_TIMER_CTLR_RAW, &control);
Note that while in CTLW ioctl, you only operate on one register at a time,
while a read in CTLR will latch and read all 3 timer registers.
If you want to reset a counter to it's default state (that is done by a select
and no write), then you simply set all 3 registers to -1 on write mode.
As Mike Perry says in dash16.h:
Defines for control of the ad start pulse (main control register):
These defines control where the start pulses for an adc come from:
For example, DOS users probably write something to the effect of:
You as a Linux user would use:
And the driver itself will handle irq and dma negotiation.
0 Standard input
1 Standard output
2 Standard error
Other file descriptors are associated with files or devices by making open
system calls. (open returns a file descriptor) Note, each descriptor is
unique to the process- even if two processes reference the same
device/file. Also, if two different process both write to a file, their
data will not be interleaved, but one will overwrite the other. Each
process independently maintains its notion of the file access offset. (how
far it is into the file)
NOTE: non-POSIX systems should also include sys/types.h and
sys/stat.h
int open(const char *path, int oflags);
int open(const char *path, int oflags, mode_t mode);
O_RDONLY Open for read-only
O_WRONLY Open for write-only
O_RDWR Open for reading and writing
O_APPEND Place written data at the end of the file.
O_TRUNC Set the length of the file to zero (effectively discard
previous contents)
O_CREAT Creates the file, if necessary.
O_EXCL Used with O_CREAT to ensure that the caller creates
the file- protecting against two processes creating
the same file at the same time.
size_t write(int fildes, const void *buf, size_t nbytes);
write arranges for the first nbytes from buf to be
written to the file associated with the file descriptor fildes. It
returns the number of bytes that were actually written to the device
associated with fildes.
size_t read(int fildes, void *buf, size_t nbytes);
read reads nbytes of data from the file associated with
fildes and places them in buf. Similar to write, it
returns the number of data bytes actually read- which also may be less than
the number of bytes requested. read returns 0 if nothing is to be read
(end of file), and a -1 if an error has occurred.
int close(int fildes);
close terminates descriptor association with a file/device and
fildes. Henceforth, the file descriptor becomes available for reuse.
0 is returned if succesful, -1 on failure.
int ioctl(int fildes, int cmd, ...);
ioctl provides a standardized (okay, maybe not...) interface for
controlling the behavior of devices which can't easily be handled by other
available calls. This might include, opening a cd, rewinding a tape, or
say, changing the frequency on a dash 16 adc/dac.
open("dash16", O_RDWR);
Similarly, since the driver defaults to sampling at 20Khz on channel 0 we
could use
read(filedes, buf, howmuch);
(where filedes, buf and howmuch correspond to file descriptor, reading buffer
and amount to read, respectively)
int ioctl(int fildes, int cmd, ...);
Since we've already considered the meaning of the file descriptor, and the
method by which it may be assigned, let us now consider cmd.
ioctl uses a set of predefined macro's (defined in dash16.h) which
expand to macro's themselves, which in turn expand to 4bit bitfields specific
to each ioctl function. These bitfields are called ioctl numbers- the
macros serve to both simplify the programmer's job and ensure the assigned
number is valid. In actuality, these ioctl numbers are what it passed
as the cmd to ioctl--fortunately, we need only concern ourselves with
the macros responsible.
_IO(type, nr)
_IOR(type, nr, size)
_IOW(type, nr, size)
_IOWR(type, nr, size)
#define DASH16_AD_TRIGGER _IO(DASH16_IOC_NUM, 0)
Start analog-digital conversion. Note this is probably not advisable,
as data will probably be dropped between the ioctl and the read.
#define DASH16_CHANNELS_SELECT _IOR(DASH16_IOC_NUM, 1, \
struct dash16_channel_range *)
Select channel start/end.
#define DASH16_CHANNELS_READ _IOW(DASH16_IOC_NUM, 2, \
struct dash16_channel_range *)
Get channel start/end.
#define DASH16_READ_STATUS _IOW(DASH16_IOC_NUM, 3, u8 *)
Read STATUS register.
#define DASH16_TIMER_ENABLE _IOR(DASH16_IOC_NUM, 4, u8 *)
Controls the timer control register. Only the low 2 bits are used.
#define DASH16_TIMER_CTLW_RAW _IOR(DASH16_IOC_NUM, 5, \
struct dash16_timer_regs *)
Timer control Write (raw access to registers)
#define DASH16_TIMER_CTLR_RAW _IOWR(DASH16_IOC_NUM, 6, \
struct dash16_timer_regs *)
Timer control Read (raw access to registers)
#define DASH16_SET_FREQ _IOR(DASH16_IOC_NUM, 6, int *)
Set the frequency on the clock. This is in hertz.
#define DASH16_RESTORE_DFL _IO(DASH16_IOC_NUM, 7)
Restore default settings. ie: sample on channel 0 at 20Khz.
#define DASH16_AD_START_CTL _IOR(DASH16_IOC_NUM, 8, int *)
Start conditions for analog/digital conversion.
dash16_timer_regs and dash16_channel_range are implemented as
struct dash16_timer_regs {
u16 ctr[3];
u8 timer_ctl;
};
struct dash16_channel_range {
u8 start; /* Start channel for conversion */
u8 end; /* End channel for conversion */
};
For instance, to set the channels start and end to 1 and 2 respectively,
we might
struct dash16_channel_range channel; /* initialize channel structure
channel.start=1; * for passing channel information
channel.end=2; * with DASH16_CHANNELS_READ */
ioctl(filedes, DASH16_CHANNELS_READ, &channels);
(ioctls: DASH16_TIMER_CTL(R/W)_RAW)
#define DASH16_TCTL_SELECT(x) (((x) & 3) << 6)
This selects a counter clock. This simply takes a counter clock (int)
as it's argument.
#define DASH16_TCTL_RL(x) (((x) & 3) << 4)
This is OR'd together with DASH16_TCTL_SELECT to help produce the byte
which is to be written. Unlike DASH16_TCTL_SELECT, DASH16_TCTL_RL(x)
takes 1 or 2 macros itself. They are:
#define DASH16_RL_MSB (1)
This tells DASH16_TCTL_RL(x) to read/load the least significant bit.
#define DASH16_RL_LSB (1<<1)
Similarly, this indicates the GSB is to be read/loaded.
NOTE: Both MSB and LSB may be read/load by OR'ing these together.
#define DASH16_TCTL_MODE(x) (((x) & 7) << 1)
This is used to select the actual configuration of the dash16. As you
might guess, a set of macros is available to pass to DASH16_TCTL_MODE(x),
they are:
#define DASH16_PTERM 0
Pulse on terminal count
#define DASH16_PROG_SHOT 1
Programmable one shot
#define DASH16_RATE_GEN 2
Rate Generator
#define DASH16_SQ_WAVE 3
Square wave generator
#define DASH16_SOFT_STRB 4
Software tiggered strobe
#define DASH16_HARD_STRB 5
Hardware triggered strobe
NOTE: Since these define the configuration of the card, DON'T
try OR'ing them together.
#define DASH16_TCTL_BCD_MODE(x) ((x) & 3)
This specifies the format for the timer counter inputs, either binary,
or binary coded decimal. (wherein each decimal place is represented in
a 4 bit segment *gag*) DASH16_TCTL_BCD_MODE(x) takes ONE of the following
macros to specity the use.
#define DASH16_USE_BCD 1
Specifying binary coded decimal.
#define DASH16_USE_BIN 0
Specifying the good <cough>, I mean binary format.
Each of these 3 main defines [DASH16_TCTL_RL(x), DASH16_TCTL_MODE(x), and
DASH16_TCTL_BCD_MODE(x)] are OR'd together to specify a byte to be
written to a register port. Where as the DOS user would want to use these
defines as follows:
outp(BASE_ADDR + 0x0f, DASH16_TCTL_SELECT(1) |
DASH16_TCTL_RL(DASH16_RL_LSB|DASH16_RL_MSB) |
DASH16_TCTL_MODE(DASH16_RATE_GEN) |
DASH16_TCTL_BCD_MODE(DASH16_USE_BIN));
outp(BASE_ADDR + 0x0d, 2);
This writes the byte configured by the rather menacing OR conglomeration
(Select counter 1 to work on, work on both the low and high bytes, use rate
generation, and use plain binary notation) to the status register, then
2 to the conrol register selected (register 1).
control.timer_ctl = (DASH16_TCTL_SELECT(1) |
DASH16_TCTL_RL(DASH16_RL_LSB|DASH16_RL_MSB) |
DASH16_TCTL_MODE(DASH16_RATE_GEN) |
DASH16_TCTL_BCD_MODE(DASH16_USE_BIN);
control.ctr[1] = 2;
"All of these macros must be ored together to build info to send to the
timer control register. This is basically for refrence. If you are cool
enough to have memorized the exact hex values that do various timer ops,
then go ahead and use them."
(ioctl: DASH16_AD_START_CTL)
#define DASH16_ENABLE_SOFTWARE (1) /* Allow software only to start A/D */
#define DASH16_ENABLE_TRIGGER (1<<1) /* Allow trigger pin to start A/D */
#define DASH16_ENABLE_TIMER (1<<1 | 1) /* Allow timer to start A/D */
outp(DASH16_BASE + 9, 0xa7);
To set the control register to use irq2, dma, and timer output controlled
starts.
u8 control = DASH16_ENABLE_TIMER;
ioctl(filedes, DASH16_AD_START_CTL, &control);