Using Parallel Ports to Control Devices

0
11919
parallel port

Computer cables connecting on globe. Abstract digital background

Peripheral devices are connected to a computer through interfaces called ports, which could be serial or parallel. A parallel port is a parallel physical communication interface. Let’s find out how to use a parallel port to control our devices. The code mentioned in this article can be used easily by anyone familiar with the Linux system and with writing a character device driver.

Parallel ports were first introduced around 1970 by Centronics. The company used a 36-pin interface at that time. Later, in the 80s, IBM started its own parallel port, which was in the form we know today. It had a 25-pin interface and the connector was called DB-25. Originally, only printers were used on this port but, eventually, scanners, zip-drives, joysticks, CD ROM drives, etc, all came to be used on this port.
In this article, we are going to use a parallel port to control our devices. Out of a total of 25 pins, we can use 12 pins for our devices. Please refer to the schematic in Figure 1.

Figure 1
Figure 1: Schematic to connect devices

The pins that are marked O are output pins and can be used to control devices. Just to show you how to use them, this article covers the use of two devices. One device is connected to the data pins (D3) and the other device is connected to one of the control pins (Strobe), which is active-low. I have given a relay in the schematic so that you can use any device you want. Just keep in mind that the data pins will give you 3V output and the control pins will give you 5V output.
On Linux systems, we will be using the parallel port in two ways:
1. From user space using ioctl system calls;
2. With a device driver, which will create device nodes in the /dev folder; then, from the user space, we just need to write 1 or 0 to control the devices.

Table 1

User space code
On a Linux system, the parallel port is a device node in the
/dev folder with the name parport0. If you have more than one parallel port, then the other ports will be named parport1, parport2, … parportN.
To use this parallel port from a user space program, we need to open the parallel port:

int fd;

fd=open(“/dev/parport0”,O_RDWR);

if(fd==-1)
{
perror(“open”);
exit(0);
}

So, let’s try to open the port and if it fails, we print an error message and exit. Before controlling the port pins, we should first claim the port. Linux will not let us modify the port status unless we have successfully claimed it. Claiming the port requires just an ioctl call:

if(ioctl(fd,PPCLAIM) !=0)
{
perror(“ioctl”);
close(fd);
exit(0);
}

If ioctl fails, that means some other program might have already claimed it or some other error has occurred, so we print the error message, close the parport that we have opened, and exit.
Just as we need to claim the port, we should also release it when we have finished working with the parallel port:

if(ioctl(fd,PPRELEASE) !=0)
perror(“"ioctl”");-

But this release should be at the end of the program.
Now that we have claimed the port, controlling the pins again requires just some simple ioctl calls.
The ioctl commands that we will be using are:
1. PPRCONTROL – reads the control pins and gives us the data
2. PPWCONTROL – sets the control pins
3. PPRDATA – reads the data pins
4. PPWDATA – writes to the data pins

unsigned char r;
if(ioctl(fd,PPRCONTROL,&r)!=0)
{
perror(“ioctl”);
if(ioctl(fd,PPRELEASE) !=0)
perror(“ioctl”);

close(fd);
exit(0);
}

We read the control lines and put the value in the variable ‘r’, as our device is connected to STROBE; so we need to set/reset bit 0 to control the STROBE pin.

unsigned int inp;

do {
printf(“Enter 1 to set, 0 to reset:\n”);
scanf(“%u”,&inp);
}while(inp>1); //stay in loop until user enters 0 or 1

r &= ~(0x01); //STROBE is first bit of control – reset the bit
r |= (!inp);

if(ioctl(fd, PPWCONTROL, &r) !=0)
perror(“ioctl”);

After reading the control bits, we need to modify the bit 0 and again write that to the parallel port. All the other control pins will remain the same. Only our STROBE pin will change. r &= ~(0x01) will clear the first bit and r |= (!inp) will set/reset the first bit. We have used the ‘!’ sign as STROBE is active-low, and when users enter 1, they will expect to see the pin go high.

If you want to add your devices to other control pins, then:

  • Line-Feed is bit 1
  • Reset is bit 2
  • Select is bit 3

Similarly, to control the data pins, first read them, modify the particular pin and write again to the port.

if(ioctl(fd,PPRDATA,&r)!=0)
{
perror(“ioctl”);
if(ioctl(fd,PPRELEASE) !=0)
perror(“ioctl”);

close(fd);
exit(0);
}
r &= ~(1<<3); // reset D3 pin
r |= (inp << 3); //set D3 if inp is 1

if(ioctl(fd, PPWDATA, &r) !=0) // write to the port again
perror(“ioctl”);

You can connect your devices to all the data port pins and control them. The ID of bit 0 is D0, bit 1 is D1 ….., bit 7 is D7, etc. At the end, remember to release and close the port:

if(ioctl(fd,PPRELEASE) !=0)
perror(“ioctl”);

close(fd);

Note: This user space code has to be executed with root permissions.

Using a parallel port from a device driver
Now, let’s look at how to control devices connected to a parallel port from a device driver. If you have worked with char device and misc device, you will know that the latter is the easiest way to create a character device. For those who are new to device drivers, let me give you a few details. When you register a char driver, you will need to have a major number and minor number that you have to combine using MKDEV to get a dev_t value. Then you will need to register this dev_t with register_chrdev_region() and after that, you will need to call cdev_init() and cdev_add(). Finally, you have to create the device node in the /dev folder, manually. We can avoid all this hard work if we use misc_device. We just need to call misc_register() and all the tasks are done automatically, including the device node creation in the /dev folder. But for that we need to define a structure_miscdevice.
Let’s look at the basic parts of our driver with just the header files, init and exit.

#include<linux/kernel.h>
#include<linux/module.h>
#include<linux/miscdevice.h>
#include<linux/device.h>
#include<linux/fs.h>
#include<linux/slab.h>
#include <linux/uaccess.h>
#include <linux/parport.h>

int __init init_m(void)
{
return parport_register_driver(&mydevice_driver);
}

void __exit cleanup_m(void)
{
parport_unregister_driver(&mydevice_driver);
}

module_init(init_m);
module_exit(cleanup_m);
MODULE_LICENSE(“GPL”);

In the init function, we are just registering our driver with the parallel port. But we have not yet defined our driver.

static struct parport_driver mydevice_driver = {
.name = “mydevice”,
.attach = mydevice_attach,
.detach = mydevice_detach,
};

mydevice_attach and mydevice_detach are the entry and exit points for our driver. Whenever Linux detects any parallel port, it will call the drivers and attach a function where we register our device with the parallel port, and try to claim it. The remaining part of the code will work only if we claim it.

Assuming that we have claimed the parallel port successfully, let’s register miscdevices:

static void mydevice_attach(struct parport *port)
{
int i,err;

if (port->number != 0) // we connect to lpt1
return;
if (pprt) {
pr_err(“port->number=%d already registered!\n”,port->number);
return;
}

pprt = parport_register_device(port, “mydevice”, NULL, NULL, NULL, 0, (void *)&pprt);
if (pprt == NULL) {
pr_err(“port->number=%d, parport_register_device() failed\n”, port->number);
return;
}

if (parport_claim(pprt)) {
pr_err(“could not claim access to parport. Aborting.\n”);
goto err_unreg_device;
}

for (i=0;i<2;i++) {
err = misc_register(&mydevice[i]);
if(err < 0)
goto error;
}
return;

error:
for(--i;i>=0;i--)
misc_deregister(&mydevice[i]);

err_unreg_device:
parport_unregister_device(pprt);
pprt = NULL;
}

static void mydevice_detach(struct parport *port)
{
int i;

if (port->number != 0)
return;

if (!pprt) {
pr_err(“nothing to unregister.\n”);
return;
}

for(i=0;i<2;i++)
misc_deregister(&mydevice[i]);

parport_release(pprt);
parport_unregister_device(pprt);
pprt = NULL;
}

Since we are using only two devices, we are registering two miscdevices here. If you plan to use more than two, you need to make the required modifications in the code. mydevice, which we have used with misc_register, is just an array of two elements defining the miscdevice. (If you have more than two devices, you need to add elements in this array of mydevice.)

static struct miscdevice mydevice[2] = {
{
.minor = MISC_DYNAMIC_MINOR,
.name = “mydev1”,
.fops = &mydevice_fops,
.nodename = “mydev1”,
.mode = 0666, // pin 0 = STROBE
},
{
.minor = MISC_DYNAMIC_MINOR,
.name = “mydev2”,
.fops = &mydevice_fops,
.nodename = “mydev2”,
.mode = 0666, // pin 5 = D3
}
};

Instead of using the names mydev1 or mydev2, you can easily use the names fan and light, in which case your device nodes will become /dev/fan and /dev/light but here, in this example, let’s keep the nodes as /dev/mydev1 and /dev/mydev2:

static const struct file_operations mydevice_fops = {
.owner = THIS_MODULE,
.write= mydevice_write,
.open = mydevice_open,
.release = mydevice_release,
};

This is the definition of mydevice_fops as we just need to open and write to the device. If you want to know the state of the device, you can also add a read call.
Now, we have a small problem here. We have created two device nodes and may choose to open one of them. Since we have a common open function for all the devices, how would we know which device was opened? Again, when you are writing to the device, how will the right function know which port pin to modify? Let’s look at the open callback function to find out how we can solve this problem.

static int mydevice_open(struct inode *inode, struct file *file)
{
int i,minor = iminor(inode);
int *deviceno = kmalloc(sizeof(int),GFP_KERNEL);
for (i=0;i<2;i++) {
dev_t dvt = (mydevice[i].this_device)->devt;
if( minor == MINOR(dvt)) {
*deviceno = i;
break;
}
}

file->private_data = deviceno;
return 0;
}

The struct file has a member called private_data which is of type void *. You can store any kind of data there, which you think will be required in your driver. So, in our case, we find out the minor number of the device from the inode, and then compare it with the array of devices to find out the index of the device which has called the open function. We store the array index in that private_data. When we open both our devices, we will have two separate struct files; so we put the index into private_data to know later which struct file is for which device.

Let’s now look at the write callback function.

static ssize_t mydevice_write(struct file *filp, const char *buff, size_t len, loff_t *off)
{
char temp;
int retval = len;
int devicepin;

if(copy_from_user(&temp, buff, 1)) //only take the first byte
return -EFAULT;
if(temp<48 || temp>49) //ascii code of 0 and 1
return -EINVAL;

temp -=48;
devicepin = *(int *)(filp->private_data);
spin_lock_irq(&pprt_lock);
switch(devicepin) {
case 0: //STROBE is the first bit
control = parport_read_control(pprt->port);
control &= ~(0x01); //reset the bit
control |= !temp; // change the bit, ! because STROBE is activelow
parport_write_control(pprt->port, control);
break;

case 1: // second element of array is D3
data = parport_read_data(pprt->port);
data &= ~(1<<3);
data |= (temp<<3);
parport_write_data(pprt->port, data);
break;

default:
retval = -EINVAL;
break;
}

spin_unlock_irq(&pprt_lock);
return retval;
}

In the write call, we just accept values ‘0’ and ‘1’, get the array index from the private_data and based on the switch-case, we control the particular port pin. parport_read_control() and parport_write_control() are the functions to read and write to the parallel port control pins. Similarly, parport_read_data() and parport_write_data() are the functions to read and write to the parallel port data pins. Just like the user space code, bit0 is to be changed to control the Strobe pin.

In the write call, there are chances of one more problem occurring. Nowadays, there are multiple processors in a system, so what will happen if two different processes from two different processors try to control our devices at the same time? The devices will be left in an inconsistent state. To prevent that, we use spinlock. Before modifying the port pins, we use the lock so that any other process cannot enter the same code block that this process is executing. After the pins are modified, we release the lock. To use the lock, we need to define it first:

static DEFINE_SPINLOCK(pprt_lock);

We have not yet defined our close callback function. Whenever any device is closed, we just free the memory that was allocated in the open call to store the index number.

static int mydevice_release(struct inode *inode, struct file *file)
{
if(file->private_data) {
kfree(file->private_data);
file->private_data = NULL;
}
return 0;
}

When we insmod this driver, if all goes well, we will have two devices: /dev/mydev1 and /dev/mydev2. To turn dev1 on or off, we can simply echo 1 or 0 to the device.

To turn on the device, type:

echo 1 > /dev/mydev1

To turn off the device, type:

echo 0 > /dev/mydev1

Alternately, you can write a user space program that opens this device and turns it on or off according to your requirement.
You can get the full user space code discussed earlier along with this driver from https://github.com/sudipm-mukherjee/osfy.

LEAVE A REPLY

Please enter your comment!
Please enter your name here