old-www/LDP/LG/issue96/pramode.html

753 lines
24 KiB
HTML

<!--startcut ==============================================-->
<!-- *** BEGIN HTML header *** -->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
<HTML><HEAD>
<title>Servo motors, Linux/TRAI and other fun stuff LG #96</title>
</HEAD>
<BODY BGCOLOR="#FFFFFF" TEXT="#000000" LINK="#0000FF" VLINK="#0000AF"
ALINK="#FF0000">
<!-- *** END HTML header *** -->
<!-- *** BEGIN navbar *** -->
<A HREF="artime.html">&lt;&lt;&nbsp;Prev</A>&nbsp;&nbsp;|&nbsp;&nbsp;<A HREF="index.html">TOC</A>&nbsp;&nbsp;|&nbsp;&nbsp;<A HREF="../index.html">Front Page</A>&nbsp;&nbsp;|&nbsp;&nbsp;<A HREF="http://www.linuxgazette.com/cgi-bin/talkback/all.py?site=LG&article=http://www.linuxgazette.com/issue96/pramode.html">Talkback</A>&nbsp;&nbsp;|&nbsp;&nbsp;<A HREF="../faq/index.html">FAQ</A>&nbsp;&nbsp;|&nbsp;&nbsp;<A HREF="dorgan1.html">Next&nbsp;&gt;&gt;</A>
<!-- *** END navbar *** -->
<!--endcut ============================================================-->
<TABLE BORDER><TR><TD WIDTH="200">
<A HREF="http://www.linuxgazette.com/">
<IMG ALT="LINUX GAZETTE" SRC="../gx/2002/lglogo_200x41.png"
WIDTH="200" HEIGHT="41" border="0"></A>
<BR CLEAR="all">
<SMALL>...<I>making Linux just a little more fun!</I></SMALL>
</TD><TD WIDTH="380">
<CENTER>
<BIG><BIG><STRONG><FONT COLOR="maroon">Servo motors, Linux/TRAI and other fun stuff</FONT></STRONG></BIG></BIG>
<BR>
<STRONG>By <A HREF="../authors/pramode.html">Pramode C.E</A></STRONG>
</CENTER>
</TD></TR>
</TABLE>
<P>
<!-- END header -->
<html>
<head>
<title>Servo motors, Linux/RTAI and other fun stuff</title>
</head>
<body>
<h2>Introduction</h2>
<p>
In last month's <a href="http://www.linuxgazette.com/issue95/pramode.html">article</a>, I had explored the problems
associated with writing timing sensitive code under Linux and looked at
how <a href="http://www.aero.polimi.it/projects/rtai">RTAI</a> solves them in an elegant manner.
In this issue, I present an interesting application developed by
a few of my students as part of a Computer Vision project. I would
also like to share with you a few programs which I wrote in my
effort to understand RTAI better:
<ul>
<li>A simple program which measures pulse width of a PWM signal
<li>A frequency counting program which uses interrupts under RTAI
<li>A program which tries to demonstrate the effect of priority
inversion in multithreaded code and the use of resource semaphores
under RTAI (The reader is expected to have an understanding of basic semaphore
operations to follow this part).
</ul>
</p>
<h2>The Project</h2>
<p>
In his article <a href="http://www.fsmlabs.com/articles/camera/rtlcam.html">Creating a web page controlled 2-axis movable camera with
RTLlinux/Pro</a>, Cort Dougan presents the software and hardware involved in the
construction of an interesting device which allows him to always keep an eye on his pet cat,
Kepler. An ordinary web cam is controlled by two servo motors - one makes the cam
move up and down and the other makes it sweep 180 degrees. The movement of both the
servos is controlled by real time tasks.
<p>
Motivated by the article, a few of my students tried their hand at
designing such a system. Here is what they came up with:
<p>
<img src="misc/pramode/cam1.jpg">
<p>
The top and bottom servo's should be clearly visible - the one at
the bottom serves to rotate the platform resting on its axis (the
platform on which the webcam is mounted). The
whole thing is made out of transparent plastic. Here is a closer view
of the bottom part:
<p>
<img src="misc/pramode/cam2.jpg">
<h3>Controlling the servo</h3>
<p>
A hobby <a href="http://www.seattlerobotics.org/guide/servos.html">servo motor</a>(like the Futaba S2003 used in this project)
runs off 5V DC, has high torque for its size and can be positioned
at points along a 180 degree arc (or a little bit more - the servo
doesn't rotate the full 360 degrees, there is some kind of
mechanical `stop' built into it - which can of course be removed
if you are seriously into <a href="http://www.seattlerobotics.org/guide/servohack.html">servo hacking</a>).
The control wire (normally white colour)
of the servo should be continuously fed with a digital signal whose
period is about 20ms - the period need not be very accurate,
and can be lesser than 20ms, but the on time should be precisely
controlled, it is what decides where the servo will move to - in
this case, it was seen that the servo moves for about 170 degrees
for an on time from 2.2ms down to 0.5ms. Here is a picture of the
control pulse:
<p>
<img src="misc/pramode/pwm.png">
<p>
Generating this control pulse with `normal' Linux is a difficult
issue - other activities going on in the system can seriously
disturb the waveform - with the result that the servo will start
reacting violently. So, hard real time RTAI tasks are used.
<p>
Two real time tasks are used to control the servos - one
controlling the top servo and the other one, the bottom
servo. The servo position is communicated to the real time
tasks from user space with the help of two real time fifos.
Let's first look at some macro/variable definitions (the actual
rotation angles have not been accurately measured - so users
of the Futaba S2003 need not be worried about any numerical
discrepancy):
<pre>
#define BOTTOM_SERVO_FIFO 0
#define TOP_SERVO_FIFO 1
#define FIFO_SIZE 1024
#define BOTTOM_SERVO 0
#define TOP_SERVO 1
#define BOTTOM_SERVO_PIN 2 /* Bit D1 of parport o/p register */
#define TOP_SERVO_PIN 4 /* Bit D2 of parport o/p register */
#define TICK_PERIOD 1000000 /* 1 ms */
#define STACK_SIZE 4096
#define MIN_ON_PERIOD 500000 /* .5 ms */
#define NSTEPS 35
#define STEP_PERIOD 50000 /* 50 micro seconds */
#define TOTAL_PERIOD 20000000 /* 20ms */
#define ONE_SHOT
RTIME on_time[2], off_time[2];
RT_TASK my_task1, my_task2;
</pre>
The bottom servo is connected to pin 3 of the parallel port and the
other one, to pin 4 - the macro's BOTTOM_SERVO_PIN and TOP_SERVO_PIN
define the values to be written to the parallel port data register
to set these pins (bit D0 of parallel port data register controls pin
number 2, bit D1 pin number 3 and so on - the macro's are definitely
poorly named and meant to confuse readers). The minimum on period of the
control pulse is defined to be 500000 nano seconds (.5 ms). Because
we get a servo rotation of 170 degree for pulse widths from 0.5ms to
2.2ms, a 50 micro second change in pulse width gives us 5 degree of
rotation. We define the `zeroth step' of the servo as the position
to which it moves when it sees a pulse of 0.5ms duration and the
`thirty-fourth step' as the position it moves to when it sees a
pulse whose on time is 0.5ms + (50 micro second * 34), ie, 2.2ms. So, in
a total of 35 steps, each step about 5 degree, we get full 170 degree
rotation. What the user program communicates with the real time tasks
through the fifos is this step number.
<p>
The total period is defined to be 20ms. The zeroth element of the on_time
array stores the current on time of the pulse which controls the
bottom servo and the first element, that of the top servo. Similar
is the case with the off_time array.
<p>
Let's now look at the code for the tasks which control the servo's:
<pre>
static void servo_task(int pin)
{
while(1) {
outb(inb(0x378) | pin, 0x378);
rt_sleep(on_time[(pin &gt;&gt; 1) - 1]);
outb(inb(0x378) &amp; ~pin, 0x378);
rt_sleep(off_time[(pin &gt;&gt; 1) - 1]);
}
}
</pre>
The argument to the task is BOTTOM_SERVO_PIN or TOP_SERVO_PIN,
depending on which servo it controls. The servo number can be
obtained from these values by shifting them right once and subtracting
one. First, the corresponding parallel port pin is made high and
a sleep is executed - then, the pin is made low and the task goes
to sleep once again.
<p>
Let's now look at the fifo handler code - user programs communicate
with the real time tasks through two fifo's - one for each servo. The
fifo handler code, which gets executed when either of them is written
to from a user space program, is the same. User programs communicate
a `step number', ie, an integer in the range 0 to 34.
<pre>
int fifo_handler(unsigned int fifo)
{
int n, r;
unsigned int on;
r = rtf_get(fifo, &amp;n, sizeof(n));
rt_printk("fifo = %d, r = %u, n = %u\n", fifo, r, n);
if((n &lt; 0) &amp;&amp; (n &gt; 34)) return -1;
on = MIN_ON_PERIOD + n*STEP_PERIOD;
on_time[fifo] = nano2count(on);
off_time[fifo] = nano2count(TOTAL_PERIOD - on);
return 0;
}
</pre>
The code should be easy to understand, it simply reads a step number,
converts it into a corresponding `on time' and stores it into the proper
slot in the on_time array. The argument to the handler is the number
of the fifo to which data was written to from the user space program.
<p>
We now come to the module initialization part, the code should be easy
to understand.
<pre>
int init_module(void)
{
RTIME tick_period;
RTIME now;
rtf_create(BOTTOM_SERVO_FIFO, FIFO_SIZE);
rtf_create_handler(BOTTOM_SERVO_FIFO, fifo_handler);
rtf_create(TOP_SERVO_FIFO, FIFO_SIZE);
rtf_create_handler(TOP_SERVO_FIFO, fifo_handler);
#ifdef ONE_SHOT
rt_set_oneshot_mode();
#endif
rt_task_init(&amp;my_task1, servo_task, BOTTOM_SERVO_PIN, STACK_SIZE, 0, 0, 0);
rt_task_init(&amp;my_task2, servo_task, TOP_SERVO_PIN, STACK_SIZE, 0, 0, 0);
tick_period = start_rt_timer(nano2count(TICK_PERIOD));
on_time[BOTTOM_SERVO] = nano2count(MIN_ON_PERIOD);
on_time[TOP_SERVO] = on_time[BOTTOM_SERVO];
off_time[BOTTOM_SERVO] = nano2count(TOTAL_PERIOD - MIN_ON_PERIOD);
off_time[TOP_SERVO] = off_time[BOTTOM_SERVO];
now = rt_get_time() + tick_period;
rt_task_make_periodic(&amp;my_task1, now, tick_period);
rt_task_make_periodic(&amp;my_task2, now, tick_period);
return 0;
}
</pre>
<p>
And here comes the module cleanup:
<pre>
void cleanup_module(void)
{
stop_rt_timer();
rt_busy_sleep(10000000);
rtf_destroy(BOTTOM_SERVO_FIFO);
rtf_destroy(TOP_SERVO_FIFO);
rt_task_delete(&amp;my_task1);
rt_task_delete(&amp;my_task2);
}
</pre>
<p>
The real time tasks were found to control the servo's perfectly. The
system was heavily loaded by running multiple kernel compiles, copying and untarring
large files and several other tricks - but the real time tasks were found
to perform satisfactorily.
<h2>Using Interrupts</h2>
<p>
It is easy to measure the frequency of a low-frequency square
wave. Simply apply it to the parallel port interrupt pin, write
an interrupt service routine and increment a count within the
routine. This count may be transferred to a user space program
through a real time fifo.
<p>
A real time application may not tolerate interrupts getting
missed or the interrupt handling code getting executed a long
time after the interrupt is asserted. With a `normal' Linux
kernel, this is a real problem. The Linux kernel may disable
interrupts when it executes critical sections of code - during
this time, the system remains unresponsive to external events.
In an RTAI patched kernel, even if Linux asks for interrupts
to be disabled, interrupts don't really get disabled; only thing
is RTAI does not let the Linux kernel see the interrupt. A
real time task will still be able to handle interrupts undisturbed.
<p>
Here is a small program which counts the number of interrupts
coming on the parallel port interrupt input pin. I tested it
out by using a function generator to generate a square wave
at various low frequencies. A simple 555 timer based circuit
should also do the job.
<pre>
#include &lt;linux/module.h&gt;
#include &lt;rtai.h&gt;
#include &lt;rtai_sched.h&gt;
#include &lt;rtai_fifos.h&gt;
#include &lt;asm/io.h&gt;
#define FIFO_R 0
#define FIFO_W 1
#define TIMERTICKS 1000000000 /* 1 second */
#define STACK_SIZE 4096
#define FIFO_SIZE 1024
static int prev_total_count = 0;
static int new_total_count = 0;
static RT_TASK my_task;
static void fun(int t)
{
while(1) {
prev_total_count = new_total_count;
new_total_count = 0;
rt_task_wait_period();
}
}
int fifo_handler(unsigned int fifo)
{
char c;
rtf_get(FIFO_R, &amp;c, sizeof(c));
rtf_put(FIFO_W, &amp;prev_total_count, sizeof(prev_total_count));
return 0;
}
static void handler(void)
{
new_total_count++;
}
int init_module(void)
{
RTIME tick_period, now;
rt_set_periodic_mode();
rt_task_init(&amp;my_task, fun, 0, STACK_SIZE, 0, 0, 0);
tick_period = start_rt_timer(nano2count(TIMERTICKS));
now = rt_get_time();
rt_task_make_periodic(&amp;my_task, now + tick_period, tick_period);
rtf_create(FIFO_R, FIFO_SIZE);
rtf_create(FIFO_W, FIFO_SIZE);
rtf_create_handler(FIFO_R, fifo_handler);
rt_request_global_irq(7, handler);
rt_enable_irq(7);
outb(0x10, 0x37a);
return 0;
}
void cleanup_module(void)
{
stop_rt_timer();
rt_busy_sleep(10000000);
rt_task_delete(&amp;my_task);
rt_disable_irq(7);
rt_free_global_irq(7);
rtf_destroy(FIFO_R);
rtf_destroy(FIFO_W);
}
</pre>
<p>
The rt_request_global_irq and rt_enable_irq functions together
instruct the RTAI kernel to service IRQ 7 (which is the parallel
port interrupt). The interrupt handler simply increments a count.
Every 1 second (the frequency of our input doesn't change
fast, and we are only interested in observing a long cumulative count)
a real time task wakes up and stores this count
value to another variable (called prev_total_count) and also clears
the counter. User programs can access prev_total_count through
a FIFO.
<p>
A user program reads the count by writing a dummy value to
FIFO_R and reading from FIFO_W. Writing to FIFO_R will result
in fifo_handler getting executed, which will place the
count onto FIFO_W; it can then be read by the user program. There
should surely be a better way to do this - only trouble is,
I don't know how. Here is the user program:
<pre>
/* User space test program */
#include &lt;sys/types.h&gt;
#include &lt;sys/stat.h&gt;
#include &lt;fcntl.h&gt;
#include &lt;assert.h&gt;
#define FIFO_W "/dev/rtf0"
#define FIFO_R "/dev/rtf1"
main()
{
int fd1, fd2, dat,r;
fd1 = open(FIFO_W, O_WRONLY);
fd2 = open(FIFO_R, O_RDONLY);
assert(fd1 &gt; 2);
assert(fd2 &gt; 2);
write(fd1, "a", 1);
r = read(fd2, &amp;dat, sizeof(dat));
assert(r == 4);
printf("interrupt count = %d\n", dat);
}
</pre>
<h2>Measuring Pulse Width</h2>
<p>
Let's say we wish to measure the off-time of a pulse
of total width 3ms with an accuracy of not more than
0.1ms. We start a periodic task with period 0.1 ms. At
each `awakening' of this periodic task, it checks whether
the signal is low or high. The number of times the signal
is low, out of a total of 30 samples, is recorded.
Here is the code which implements this procedure:
<pre>
#include &lt;linux/module.h&gt;
#include &lt;rtai.h&gt;
#include &lt;rtai_sched.h&gt;
#define LPT1_BASE 0x378
#define LPT1_STATUS 0x379
#define ACK 6
#define STACK_SIZE 4096
#define TIMERTICKS 100000 /* 0.1 milli second */
#define TOTAL_SAMPLES 30 /* Take 30 samples at 0.1 ms each */
static RT_TASK my_task;
static int old_off_samples, new_off_samples;
static void fun(int t)
{
static int count = 0;
while(1) {
new_off_samples = new_off_samples + ((inb(LPT1_STATUS) &gt;&gt; ACK) &amp; 0x1);
if(++count == TOTAL_SAMPLES) {
count = 0;
old_off_samples = new_off_samples;
new_off_samples = 0;
}
rt_task_wait_period();
}
}
int init_module(void)
{
RTIME tick_period, now;
rt_set_periodic_mode();
rt_task_init(&amp;my_task, fun, 0, STACK_SIZE, 0, 0, 0);
tick_period = start_rt_timer(nano2count(TIMERTICKS));
now = rt_get_time();
rt_task_make_periodic(&amp;my_task, now + tick_period, tick_period);
return 0;
}
void cleanup_module(void)
{
stop_rt_timer();
rt_printk("old = %u, new = %u\n", old_off_samples, new_off_samples);
rt_busy_sleep(10000000);
rt_task_delete(&amp;my_task);
}
</pre>
<p>
The periodic task checks the D6th bit of the parallel port
status register. It is 1 if the input signal on the 10th
pin is low. The count could be, as usual, transferred to a
user space program through a fifo.
<h2>Semaphores and Priority inversion</h2>
<p>
Programmer's use semaphore's to synchronize the activity of
multiple threads. RTAI has a function:
<pre>
void rt_sem_init(SEM *sem, int value);
</pre>
Which can be used to create and initialize semaphore objects.
There is an interesting problem called `priority inversion'
associated with the use of locking mechanisms in an an environment
which allows preemptive execution of tasks with varying priorities.
It seems that the problem was brought to the attention of the
real time design community by certain glitches encountered during
the Mars Pathfinder Mission (a Google search would yield more
information). I shall try to describe the problem first (the description
is based on information obtained from the Net on the Pathfinder mission
failure).
<p>
Suppose we have a very high priority task, Task-H which periodically
accesses a buffer to write data to it (or read data from it). Another
task, a very low priority and very infrequently running task, which
always runs only for a short amount of time (let's call it Task-L), also
might need to access the buffer to write to or read from it. Both
these tasks would have to successfully grab a lock before one of
them is able to access the buffer; the lock can be acquired by only
one task at a time. Let's say Task-L grabs the lock and starts accessing
the buffer. In between, suppose an interrupt causes the scheduling of
the very high priority task, Task-H. Now, Task-H also would try to grab
the lock, but blocks because it is currently held by Task-L. In the normal
case, Task-L would finish very soon and release the lock, allowing Task-H
to continue. But suppose a medium priority task, Task-M gets scheduled. Now,
as long as Task-M does not finish, the OS will not allow Task-L to continue.
Only if Task-L finishes doing whatever it has to do and releases the lock
will Task-H be able to continue - the result is Task-H gets delayed by
the time Task-M would take to complete. Suppose the system designer overlooks
this and builds a watchdog timer which would time out and reboot the
system if Task-H does not run for a short amount of time - the result would
be system reboots whenever Task-M comes in between Task-H and Task-L.
<p>
Here is a small program which tries to demonstrate this problem.
<pre>
#include &lt;linux/module.h&gt;
#include &lt;rtai.h&gt;
#include &lt;rtai_sched.h&gt;
#define LPT1_BASE 0x378
#define STACK_SIZE 4096
#define TIMERTICKS 100000000 /* .1 second */
#define HIGH_PRIO 0
#define MEDIUM_PRIO 1
#define LOW_PRIO 2
#define PORTB 0x61
#define PIT_CTRL 0x43
#define PIT_DATA 0x42
static RT_TASK my_task1, my_task2, my_task3;
SEM flag;
RTIME tick_period;
unsigned char c = 0;
void speaker_on(void)
{
unsigned char c;
c = inb(PORTB)|0x3;
outb(c, PORTB);
}
void speaker_off(void)
{
unsigned char c;
c = inb(PORTB)&amp;~0x3;
outb(c, PORTB);
}
void generate_tone(void)
{
/* Counter 2, low and high, mode 3, binary */
outb(0xb6, PIT_CTRL);
outb(152, PIT_DATA);
outb(10, PIT_DATA);
}
static void my_delay(unsigned int i)
{
while(i--);
}
/* Highest priority */
static void info_bus_task(int t)
{
speaker_on();
rt_sem_wait(&amp;flag);
rt_printk("info bus task got mutex...\n");
rt_sem_signal(&amp;flag);
speaker_off();
}
/* Medium Priority */
static void comm_task(int t)
{
my_delay(0xffffffff);
my_delay(0xffffffff);
my_delay(0xffffffff);
}
/* Low priority */
static void weather_task(int t)
{
rt_sem_wait(&amp;flag);
rt_sleep(30*tick_period);
rt_sem_signal(&amp;flag);
}
int init_module(void)
{
RTIME now;
//rt_typed_sem_init(&amp;flag, 1, RES_SEM);
rt_sem_init(&amp;flag, 1);
rt_set_periodic_mode();
generate_tone();
rt_task_init(&amp;my_task1, info_bus_task, 0, STACK_SIZE, HIGH_PRIO, 0, 0);
rt_task_init(&amp;my_task2, comm_task, 0, STACK_SIZE, MEDIUM_PRIO, 0, 0);
rt_task_init(&amp;my_task3, weather_task, 0, STACK_SIZE, LOW_PRIO, 0, 0);
tick_period = start_rt_timer(nano2count(TIMERTICKS));
now = rt_get_time();
rt_task_make_periodic(&amp;my_task1, now + 2*tick_period, tick_period);
rt_task_make_periodic(&amp;my_task2, now + 3*tick_period, tick_period);
rt_task_make_periodic(&amp;my_task3, now + tick_period, tick_period);
return 0;
}
void cleanup_module(void)
{
stop_rt_timer();
rt_busy_sleep(10000000);
rt_sem_delete(&amp;flag);
rt_task_delete(&amp;my_task1);
rt_task_delete(&amp;my_task2);
rt_task_delete(&amp;my_task3);
}
</pre>
The information bus, communication and weather tasks are respectively
the high, medium and low priority tasks. RTAI lets us set priorities
to tasks during rt_task_init. The weather task starts first. It grabs
a semaphore (the semaphore is initialized to 1 in rt_sem_init) and
then goes to sleep for 30 ticks (3 seconds, as each tick is 0.1 second).
The information bus task runs at the next timer tick (the second argument
to rt_task_make_periodic is the point of time at which the task is to be
started); it turns on the PC speaker and then attempts to do a `down'
operation on the semaphore and in the process, gets blocked. We expect the
weather task to complete its sleep, release the semaphore and let the
information bus task run to completion, thereby stopping the noise coming
out of the speaker. But the trouble is that before the weather task comes
out of its sleep, a medium priority `communication' task gets scheduled and
starts executing a series of busy loops (which on my Athlon XP system generates a
combined delay of about 17 seconds when compiled with -O2 - it would be better if you time the delay
loop in a userland program before you plug it into the kernel - remember, as
long as that delay loop is running, your machine will be in an unusable state -
so be careful with what you do). The operating system can't bring the weather
taks out of its sleep even after 3 seconds is over because the medium priority
task is busy executing a loop - after about 17 seconds, the communication task
would end, thereby letting the weather task continue with its execution. The
weather task perform an `up' operation on the semaphore bringing the information
bus task out of the block and letting it stop the speaker. We note that the
high priority information bus task is getting delayed by the communication task.
<p>
RTAI supports `resource semaphores' - they can be used to solve the above
problem. Had we initialized our semaphore like this:
<pre>
rt_typed_sem_init(&amp;flag, 1, RES_SEM);
</pre>
the task which originally `acquired' the semaphore (the weather task) would
have `inherited' the priority of the high priority information bus task blocked on it.
This would have resulted in the RTAI scheduler preempting the communication task
and giving control back to the weather task exactly after 3 seconds of sleep.
<h2>Acknowledgements</h2>
<p>
The webcam-servo motor setup was implemented by <b>Krishna Prasad</b>, <b>Mahesh</b> and
friends as part of a Computer Vision project; they wish to acknowledge
the influence of <b>Cort Dougan's</b> implementation of the idea on RTLinux.
RTAI comes with good documentation, and lots of example code. I would like
to thank all those people who took the pains not only to build a great system,
but also document it well.
<h2>About the Author</h2>
<p>
I have been teaching GNU/Linux and elementary Computer Science since
1997. If you are sure that you really wish to waste your time, you might
drop in on my home page <a href="http://pramode2.tripod.com">pramode2.tripod.com</a>
</body>
</html>
<!-- *** BEGIN author bio *** -->
<P>&nbsp;
<P>
<!-- *** BEGIN bio *** -->
<P>
<img ALIGN="LEFT" ALT="[BIO]" SRC="../gx/2002/note.png">
<em>
I am an instructor working for IC Software in Kerala, India. I would have loved
becoming an organic chemist, but I do the second best thing possible, which is
play with Linux and teach programming!
</em>
<br CLEAR="all">
<!-- *** END bio *** -->
<!-- *** END author bio *** -->
<!-- *** BEGIN copyright *** -->
<hr>
<CENTER><SMALL><STRONG>
Copyright &copy; 2003, Pramode C.E.
Copying license <A HREF="../copying.html">http://www.linuxgazette.com/copying.html</A><BR>
Published in Issue 96 of <i>Linux Gazette</i>, November 2003
</STRONG></SMALL></CENTER>
<!-- *** END copyright *** -->
<HR>
<!--startcut ==========================================================-->
<CENTER>
<!-- *** BEGIN navbar *** -->
<A HREF="artime.html">&lt;&lt;&nbsp;Prev</A>&nbsp;&nbsp;|&nbsp;&nbsp;<A HREF="index.html">TOC</A>&nbsp;&nbsp;|&nbsp;&nbsp;<A HREF="../index.html">Front Page</A>&nbsp;&nbsp;|&nbsp;&nbsp;<A HREF="http://www.linuxgazette.com/cgi-bin/talkback/all.py?site=LG&article=http://www.linuxgazette.com/issue96/pramode.html">Talkback</A>&nbsp;&nbsp;|&nbsp;&nbsp;<A HREF="../faq/index.html">FAQ</A>&nbsp;&nbsp;|&nbsp;&nbsp;<A HREF="dorgan1.html">Next&nbsp;&gt;&gt;</A>
<!-- *** END navbar *** -->
</CENTER>
</BODY></HTML>
<!--endcut ============================================================-->