This is the description of one of my pet projects, a simpler version of LIRC. I did this project to learn a bit about electronics, kernel programming and inter-process communication
LIRC stands for Linux Infrared Remote Control, and is a collection of electronics schematics, drivers and user-space software to control your Linux session using a remote control. As I usually use my computer as a multimedia station, it would be great to be able to play, stop and skip songs from a distance.
My first idea was to create a serial port receiver, just as it was described on the LIRC website (www.lirc.org). For it, I'd need all sorts of cool electronical components. For example, you need a diode, a voltage regulater (a 78L05), a resistor, a capacitor, a RS-232 DB-9 or DB-25 socket, and, of course, a photo-transistor to get the data from the remote control.
As this was my first attempt at an electronics project, I had some problems buying the material. I could find everything, except for the photo-transistor. (I also bought the wrong DB-9 socket. I bought a female and it should be a male, or something like that)
I also bought some wire, a standard soldering board and other cool stuff (like a cheap digital multimeter). I already had a soldering iron, so I didn't have to buy one.
After a lot of times going to lousy and dark stores, I finally found a photo-transistor. However, the one I bought (and the only one available at the store) was a QT948 L14G1. I'm not really sure of what it means, but something tells me that it won't work. Why I say that? Because, even if there is a faint light in my room, my multimeter says that we have a open circuit. If I go to a completely dark room, then I have some big resistance. But only by lightening it with some faint light and, bam!, it is open again. And the bigger problem... it just ignores my remote controls signals. If I point at it with one of them and press any button, I can see the resistance falling down a little, but not even close to an open circuit. And I must point at from a very short distance, or else it won't work either.
After being a bit depressed by that, I decided to change my project and build a simpler remote control. It wouldn't be wireless and it would have only one button. You would send signals to it using some sort of morse code. The reason for it is very simple... this project would be really easy to build. It would require just some long wire, a DB-9 connector (I got one from a old serial mouse), and a simple button. (I also got it from the old mouse)
How would it work? The hardware is very simple. See, the serial port on the back of the computer has 9 pins, each one with a special function. We have a GND pin, that carries the (duh) ground. We have a RD and TD pin, that carry the data, etc. For more information on the pins and their location, check out the serial port pinout. We are interested in only two pins here: the RTS and the DSR. Both are used only for handshaking purposes, but that's not the reason of why we are using them. First we need to learn how the serial port transmits information
All the data that is transmitted on the serial port is transmitted serially. (that's why it is called a serial port, after all). So, we have only one way for data to go... either throught the RD pin (Receive Data) or the TD pin (Transmit Data). These pins are relative to the computer. So, when your program tries to send data, it goes through the TD pin. When you move your serial mouse, the computer receives its data on the RD pin.
So, why we won't use the RD/TD pins for our morse signals? For a simple reason... there is a whole protocol for the data that goes throught these pins. First of all, they can have only two states, either high or low (relative to the GND pin). Then, when you want to send a byte (usually 8 bits, but you can configure the port to use another number), the voltage on the pin first goes up for a short while (also called the start bit), then goes up and down accordingly to your byte (low means 1, high means 0. The data is also sent in LSB order, or right-to-left). Then we have a stop bit, or maybe some parity bits. (For more information, check the RS-232 protocol)
As you can see, this is not a very simple protocol... if we wanted our remote control to use these pins, we would have to use a IC to code our button press to this protocol (adding a start and stop bit, etc)
So, we are using a alternate approach here (the same used by the LIRC folks). We are going to change the voltage in the DSR pin. This pin isn't really important, as it is used only for handshaking. As our device isn't going to use any kind of flow control, we can change it's value as we want. And the main advantage is that we can, through software, get not only the current value of the pin, but also an interrupt everytime it changes voltage.
The schematics for our hardware are very simple. Take a look at this:
RTS ----------------- \
(serial port) BUTTON (our device)
DSR ----------------- /
The RTS is kept constant at a high state (we do it using software), and we constantly monitor the state of the DSR pin (also via software). Whenever the button is pressed, we close the RTS/DSR circuit and the voltage in the DSR pin changes. Our software catches this voltage change and builds our pulse. For example... the DSR is usually low. Then we press the button, the DSR goes up and our software gets an interrupt. It then registers that we are now high and starts a counter. One second later we release the button, the circuit is opened, and the voltage goes down. Our software gets another interrupt, realizes that we are now back at the low state and sends a message telling for how long we pressed the button.
Pretty simple, huh? We have another program that gets this message and does all sort of cool stuff with them (like changing the current playing music on Noatun)
Now let's take a look at the source code. All code here is distributed under the GPL licensed, and comes with no warranties whatsoever
This was my first attempt at kernel coding, so any comments are welcomed :)
Serport.c (Device Driver)
/* serport.c Play around with the serial port */
#include <linux/module.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <linux/sched.h>
#include <linux/tqueue.h>
#include <linux/interrupt.h>
/* proc fs */
#include <linux/proc_fs.h>
/* Char devices */
#include <linux/fs.h>
#define DEVICE_NAME "serport_dev"
static int Device_Open = 0;
#define COM1 0x3f8
static unsigned int port_state;
static int count = 0;
static int icr_status = 0;
static char msr_status = 0;
static char data[5120];
int shutdown = 0;
DECLARE_WAIT_QUEUE_HEAD(wq);
#define BUFFER_SIZE 128
static char buffer[BUFFER_SIZE];
static char *rp, *wp;
/* Timer */
struct timer_list space_timer;
#define SPACE_DELAY 30
/* Used when reading the /proc/serport file */
int procfile_read(char *buffer,
char **buffer_location,
off_t offset,
int buffer_length)
{
int len;
char status = inb(COM1+6);
static char my_buffer[5280];
if (offset > 0)
return 0;
len = sprintf(my_buffer, "Count: %d\nICR: %d\nMCR: %d\nStatus: %d\nCTS: %d\nDSR: %d\nRI: %d\nDCD: %d\n%s",
count,
icr_status,
msr_status,
status,
status & (1<<4),
status & (1<<5),
status & (1<<6),
status & (1<<7),
data);
*buffer_location = my_buffer;
return len;
}
/* Used when the character device is opened */
static int device_open( struct inode *inode,
struct file *file)
{
printk("Device opened\n");
if (Device_Open)
return -EBUSY;
Device_Open++;
MOD_INC_USE_COUNT;
shutdown = 0;
add_timer(&space_timer);
rp = wp = buffer;
return 0;
}
/* Used when the character device is closed */
static int device_release( struct inode *inode,
struct file *file)
{
printk ("Device closed\n");
/* Delete the timer */
shutdown = 1;
del_timer_sync(&space_timer);
Device_Open--;
MOD_DEC_USE_COUNT;
return 0;
}
/* Used when the character device is read.
If we don't have waiting data, the proccess
is put to sleep */
static int device_read( struct file *file,
char *buf,
size_t length,
loff_t *offset)
{
/* Check if we have something to read */
while (rp == wp) {
/* We don't have anything to read.
* Go to sleep! */
if (wait_event_interruptible(wq, (rp != wp)))
return -ERESTARTSYS;
}
if (wp > rp)
count = wp - rp;
else /* Pointer wrapped */
count = buffer + BUFFER_SIZE - rp;
copy_to_user(buf, rp, count);
rp+=count;
if (rp == buffer + BUFFER_SIZE)
rp = buffer;
return count;
}
#define CLICK_TIMER (unsigned long)10
static unsigned long last_jiffies;
/* Pulse from 0-10 Jiffies -> '0'
* Each 10 jiffies later, +1
* Until 'z' */
/* This is the tasklet for the interrupt handler */
void serial_bh(unsigned long value) {
static unsigned long current_state = 0;
static unsigned long pulse_start = 0;
if (value != current_state) {
/* Pulse boundary! */
if (value) {
/* Pulse starting */
pulse_start = last_jiffies;
current_state = value;
/* Delete the timer */
shutdown = 1;
del_timer_sync(&space_timer);
} else {
/* Pulse finishing */
current_state = value;
*wp = '0' + (last_jiffies - pulse_start)/10;
if (*wp > 'z')
*wp = 'z';
wp++;
if (wp == buffer + BUFFER_SIZE)
wp = buffer;
/* Awake everyone */
wake_up_interruptible(&wq);
/* Re add the timer */
shutdown = 0;
space_timer.expires = jiffies + SPACE_DELAY;
add_timer(&space_timer);
}
}
}
DECLARE_TASKLET(serial_tasklet, serial_bh, 0);
/* Interrupt handler */
void irq_handler(int irq,
void *dev_id,
struct pt_regs *regs)
{
unsigned char status;
/* Run while bit 1 is not set */
while ( (status = inb(COM1+2)) != 0x01 ) {
if ( (status & 0x0E) == 0) {
/* It is exactly what we want!
* service it! */
if (last_jiffies != jiffies) {
last_jiffies = jiffies;
serial_tasklet.data = inb(COM1+6) & 0x20;
if (Device_Open)
tasklet_schedule(&serial_tasklet);
}
} else {
/* It is not what we want!
* break to avoid a infinite loop
* and use icr_status to tell it */
icr_status++;
break;
}
}
}
/* Device number */
static int Major;
struct file_operations Fops = {
read: device_read,
open: device_open,
release: device_release };
/* Used to mark a space (time without pulse) */
void space_new(unsigned long data) {
*wp = ' ';
wp++;
if (wp == buffer + BUFFER_SIZE)
wp = buffer;
if (!shutdown) {
space_timer.expires = jiffies + SPACE_DELAY;
add_timer(&space_timer);
}
wake_up_interruptible(&wq);
}
int init_module(void)
{
struct proc_dir_entry *ent;
/* Register the char dev */
Major = register_chrdev(0, DEVICE_NAME, &Fops);
/* Init the timer */
init_timer(&space_timer);
space_timer.function = space_new;
if (Major < 0) {
printk ("Can't register the device: %d\n", Major);
return Major;
}
printk ("Registered the device with major number %d\n", Major);
/* Change RTS
Here we make the RTS go up */
port_state = inb(COM1+0x04);
outb(0x02, COM1+0x04);
if ((ent = create_proc_entry("serport", S_IRUGO | S_IWUSR, NULL)) != NULL) {
ent->get_info = procfile_read;
}
/* Install IRQ handler */
free_irq(0x04, NULL);
request_irq(0x04,
irq_handler,
0,
"serial port IRQ handler",
NULL);
/* Enable interrupts */
outb(0x08, COM1+0x01);
return 0;
}
void cleanup_module(void)
{
remove_proc_entry("serport", NULL);
unregister_chrdev(Major, DEVICE_NAME);
unregister_chrdev(254, DEVICE_NAME);
unregister_chrdev(253, DEVICE_NAME);
unregister_chrdev(252, DEVICE_NAME);
/* Change back */
outb(port_state, COM1+0x04);
/* Disable interrupts */
outb(0, COM1+0x01);
free_irq(0x04, NULL);
}
And this is the source code for the user application. It will monitors a character device (major number 254, minor number 0, probably... it must be created with "mknod c 254 0 device_name") and sends commands to Noatun when it gets data (it is configured to skip the current song with a short press on the remote control button
If you want to take a look at how the device driver works, just do a "cat serport.dev", where serport.dev is the name of the character device. You will see that, whenever there is a pulse, we get a non-space value. The value of the pulses depend on the duration of them (starting at character '0' and going up, until 'z')
/* Software to use the PMLIRC */
#include <stdio.h>
#include <fcntl.h>
#define BUFFER_SIZE 3
int main(int argc, char *argv[]) {
char *devname;
char buffer[BUFFER_SIZE];
char cmd[] = " ! ";
char *rp = buffer;
char *wp = buffer;
char *cp = cmd;
int fork_result;
int fd;
int len = BUFFER_SIZE;
if (argc < 2) {
printf("Usage: pmlirc <device>\n");
return -1;
}
devname = argv[1];
printf("Using device %s\n", devname);
fd = open(devname, O_RDONLY);
if (fd < 0) {
printf("Can't open device!\n");
return -1;
}
while( (len = read(fd, wp, len)) != 0 ) {
/* Get the number of bytes read and update the wp */
wp += len;
if (wp == buffer + BUFFER_SIZE) {
/* Wraps the pointer */
wp = buffer;
len = BUFFER_SIZE;
} else {
/* Don't wrap */
len = buffer + BUFFER_SIZE - wp;
}
while (rp != wp) {
/* While we don't have to get more data
* Check if this data matches */
if (*cp == '!' && *rp >= '0' && *rp <= '9') {
/* Matches! Increase the cp */
cp++;
} else if (*cp == ' ' && *rp == ' ') {
cp++;
} else {
/* No match! Reset the cp */
cp = cmd;
}
/* Increase the rp */
rp++;
/* Check if all have matched */
if (*cp == 0) {
/* Matched! Fork and execute the dcopclient
* TODO: A nice dcop C binding would be cool here */
fork_result = fork();
if (fork_result == -1) {
printf("Fork failure!\n");
return -1;
}
else if (fork_result == 0) {
/* Make the exec call */
printf("Matched!\n");
execlp("dcop", "dcop", "noatun", "Noatun", "fastForward", (char *)0);
return -1;
}
cp = cmd;
}
/* Wraps the rp, if that's the case */
if (rp == buffer + BUFFER_SIZE)
rp = buffer;
}
}
return 0;
}
That's it... both programs could use a lot of improvements... :)