Tag Archives: Hardware Access

Accessing x86-specific I/O mapped hardware in Linux

This eighth article, which is part of the series on Linux device drivers, continues on talking about accessing hardware in Linux.

<< Seventh Article

Second day in the Linux device drivers laboratory was expected to be quite different from the typical software oriented classes. Apart from accessing & programming the architecture-specific I/O mapped hardware in x86, it had lot to offer for first timers in reading hardware device manuals (commonly referred as data-sheets) and to understand them for writing device drivers.

Contrast this with the previous laboratory session, which taught about the generic architecture-transparent hardware interfacing. It was all about mapping and accessing memory-mapped devices in Linux, without any device specific detail.

x86-specific hardware interfacing

Unlike most other architectures, x86 has an additional hardware accessing mechanism through a direct I/O mapping. It is a direct 16-bit addressing scheme and doesn’t need a mapping to virtual address for its accessing. These addresses are referred to as port addresses, or in short – ports. As x86 has this as an additional accessing mechanism, it calls for additional set of x86 (assembly/machine code) instructions. And yes, there are the input instructions inb, inw, inl for reading an 8-bit byte, a 16-bit word, and a 32-bit long word respectively, from the I/O mapped devices through the ports. And the corresponding output instructions are outb, outw, outl, respectively. And the equivalent C functions/macros are as follows (available through the header <asm/io.h>):

u8 inb(unsigned long port);
u16 inw(unsigned long port);
u32 inl(unsigned long port);
void outb(u8 value, unsigned long port);
void outw(u16 value, unsigned long port);
void outl(u32 value, unsigned long port);

The basic question may arise, as to which all devices are I/O mapped and what are the port addresses of these devices. The answer is pretty simple. As per x86-specific, all these devices & their mappings are x86 standard and hence pre-defined. Figure 13 shows a snippet of these mappings through the kernel window /proc/ioports. The listing includes pre-defined DMA, timer, RTC, serial, parallel, PCI bus interfaces to name a few.

Figure 13: x86-specific I/O ports

Figure 13: x86-specific I/O ports

Simplest the serial on x86 platform

For example, the first serial port is always I/O mapped from 0x3F8 to 0x3FF. But what does this mapping mean? What do we do with this? How does it help us to use the serial port?
That is where a data-sheet of the device controlling the corresponding port needs to be looked up. Serial port is controlled by the serial controller device, commonly known as an UART (Universal Asynchronous Receiver/Transmitter) or at times a USART (Universal Synchronous/Asynchronous Receiver/Transmitter). On PCs, the typical UART used is PC16550D. The data-sheet (uart_pc16550d.pdf) for the same has also been included in the self-extracting LDDK-Package.sh, used for the Linux device driver kit. Figure 14 shows the relevant portion of it.

In general, from where & how do we get these device data-sheets? Typically, an on-line search with the corresponding device number should yield their data-sheet links. And how does one get the device number? Simple, by having a look at the device. If it is inside a desktop, open it up and check it out. Yes, this is the least you may have to do to get going with the hardware for writing device drivers. Assuming all this hacking has been done, it is time to peep into the data-sheet of UART PC16550D.

For a device driver writer, the usual sections of interest in a data-sheet are the ones related to registers of the device. Why? As, it is these registers, which a device driver writer need to read from and/or write in to finally use the device. Page 14 of the data-sheet (also shown in Figure 14) shows the complete table of all the twelve 8-bit registers present in the UART PC16550D. Each of the 8 rows corresponds to the respective bit of the registers. Also, note that the register addresses start from 0 and goes till 7. The interesting thing to note about this is that a data-sheet always gives the register offsets, which then need to be added to the base address of the device, to get the actual register addresses. Who decides the base address and where is it obtained from? Base addresses are typically board/platform specific, unless they are dynamically configurable like in the case of PCI devices. In the case here, i.e. serial device on x86, it is dictated by the x86 architecture – and that is what precisely was the starting serial port address mentioned above – 0x3F8. And the eight register offsets 0 to 7 are the ones exactly mapping to the eight port addresses 0x3F8 to 0x3FF. So, these are the actual addresses to be read or written for reading or writing the corresponding serial registers, to achieve the desired serial operations, as per the register descriptions.

Figure 14: Registers of UART PC16550D

Figure 14: Registers of UART PC16550D

All the serial register offsets and the register bit masks are defined in the header <linux/serial_reg.h>. So, rather than hard coding these values from the data-sheet, the corresponding macros could be used instead. All the following code uses these macros along with the following:

#define SERIAL_PORT_BASE 0x3F8

Operating on the device registers

To summarize all these decoding of UART PC16550D data-sheet, here are a few examples of how to do read and write operations of the serial registers and their bits.

Reading and writing the “Line Control Register (LCR)”:

u8 val;

val = inb(SERIAL_PORT_BASE + UART_LCR /* 3 */);
outb(val, SERIAL_PORT_BASE + UART_LCR /* 3 */);

Setting and clearing the “Divisor Latch Access Bit (DLAB)” in LCR:

u8 val;

val = inb(SERIAL_PORT_BASE + UART_LCR /* 3 */);

/* Setting DLAB */
val |= UART_LCR_DLAB /* 0x80 */;
outb(val, SERIAL_PORT_BASE + UART_LCR /* 3 */);

/* Clearing DLAB */
val &= ~UART_LCR_DLAB /* 0x80 */;
outb(val, SERIAL_PORT_BASE + UART_LCR /* 3 */);

Reading and writing the “Divisor Latch”:

u8 dlab;
u16 val;

dlab = inb(SERIAL_PORT_BASE + UART_LCR);
dlab |= UART_LCR_DLAB; // Setting DLAB to access Divisor Latch
outb(dlab, SERIAL_PORT_BASE + UART_LCR);

val = inw(SERIAL_PORT_BASE + UART_DLL /* 0 */);
outw(val, SERIAL_PORT_BASE + UART_DLL /* 0 */);

Blinking an LED

To get a real experience of the low-level hardware access and Linux device drivers, the best way would be to play with the Linux device driver kit (LDDK). However, just for the feel of low-level hardware access, a blinking light emitting diode (LED) may be tried as follows:

  • Connect a light emitting diode (LED) with a 330 ohm resistor in series across the pin 3 (Tx) & pin 5 (Gnd) of the DB9 connector of your PC.
  • Pull up & down the transmit (Tx) line with a 500 ms delay, by loading the blink_led driver using insmod blink_led.ko, and then unloading the driver using rmmod blink_led, before reloading.

Below is the blink_led.c, to be compiled into the blink_led.ko driver, by running make using the usual driver Makefile:

#include <linux/module.h>
#include <linux/version.h>
#include <linux/types.h>
#include <linux/delay.h>
#include <asm/io.h>

#include <linux/serial_reg.h>

#define SERIAL_PORT_BASE 0x3F8

int __init init_module()
{
	int i;
	u8 data;

	data = inb(SERIAL_PORT_BASE + UART_LCR);
	for (i = 0; i < 5; i++)
	{
		/* Pulling the Tx line low */
		data |= UART_LCR_SBC;
		outb(data, SERIAL_PORT_BASE + UART_LCR);
		msleep(500);
		/* Defaulting the Tx line high */
		data &= ~UART_LCR_SBC;
		outb(data, SERIAL_PORT_BASE + UART_LCR);
		msleep(500);
	}
	return 0;
}

void __exit cleanup_module()
{
}

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Anil Kumar Pugalia <email@sarika-pugs.com>");
MODULE_DESCRIPTION("Blinking LED Hack");

Summing up

Are you wondering as where has Shweta gone today? She has bunked all the classes. Watch out for the next article to find out why.

Ninth Article >>

Notes

  1. The above example is to demonstrate how bare bone easy the low level access could get. However, to make it more perfect, one should use the APIs request_region() and release_region(), respectively before and after the accesses of the I/O port addresses, respectively to acquire and release the range of I/O port addresses to access.
  2. Also, you might have observed that there is no module_init() & module_exit() in the above driver. But nonetheless, insmod & rmmod do work. How is that? That is because init_module() & cleanup_module() are the predefined names for the constructor & the destructor, respectively. Hence, you do not need module_init() & module_exit() to translate your other function names to these predefined ones. Caution: Since kernel 2.6 onwards, if you are building the driver into the kernel, you should define your own function names & use module_init() & module_exit().
   Send article as PDF   

Generic Hardware Access in Linux

This seventh article, which is part of the series on Linux device drivers, talks about accessing hardware in Linux.

<< Sixth Article

Shweta was all jubilant about her character driver achievements, as she entered the Linux device drivers laboratory on the second floor of her college. Why not? Many of her classmates had already read her blog & commented on her expertise. And today was a chance for show-off at an another level. Till now, it was all software. Today’s lab was on accessing hardware in Linux. Students are expected to “learn by experimentation” to access various kinds of hardware in Linux on various architectures over multiple lab sessions here.

As usual, the lab staff are a bit skeptical to let the students directly get onto the hardware, without any background. So to build their background, they have prepared some slide presentations, which can be accessed from SysPlay’s website.

Generic hardware interfacing

As every one settled in the laboratory, lab expert Priti started with the introduction to hardware interfacing in Linux. Skipping the theoretical details, the first interesting slide was about the generic architecture-transparent hardware interfacing. See Figure 11.

Figure 11: Hardware mapping

Figure 11: Hardware mapping

The basic assumption being that the architecture is 32-bit. For others, the memory map would change accordingly. For 32-bit address bus, the address/memory map ranges from 0 (0x00000000) to ‘232 – 1′ (0xFFFFFFFF). And an architecture independent layout of this memory map would be as shown in the Figure 11 – memory (RAM) and device regions (registers & memories of devices) mapped in an interleaved fashion. The architecture dependent thing would be what these addresses are actually there. For example, in an x86 architecture, the initial 3GB (0x00000000 to 0xBFFFFFFF) is typically for RAM and the later 1GB (0xC0000000 to 0xFFFFFFFF) for device maps. However, if the RAM is less, say 2GB, device maps could start from 2GB (0x80000000).

Type in cat /proc/iomem to list the memory map on your system. cat /proc/meminfo would give you an approximate RAM size on your system. Refer to Figure 12 for a snapshot.

Figure 12: Physical & bus addresses on an x86 system

Figure 12: Physical & bus addresses on an x86 system

Irrespective of the actual values, the addresses referring to RAM are termed as physical addresses. And the addresses referring to device maps are termed as bus addresses, as these devices are always mapped through some architecture-specific bus. For example, PCI bus in x86 architecture, AMBA bus in ARM architectures, SuperHyway bus in SuperH (or SH) architectures, GX bus on PowerPC (or PPC), etc.

All the architecture dependent values of these physical and bus addresses are either dynamically configurable or are to be obtained from the datasheets (i.e. hardware manuals) of the corresponding architecture processors/controllers. But the interesting part is that, in Linux none of these are directly accessible but are to be mapped to virtual addresses and then accessed through that. Thus, making the RAM and device accesses generic enough, except just mapping them to virtual addresses. And the corresponding APIs for mapping & unmapping the device bus addresses to virtual addresses are:

#include <asm/io.h>

void *ioremap(unsigned long device_bus_address, unsigned long device_region_size);
void iounmap(void *virt_addr);

These are prototyped in <asm/io.h>. Once mapped to virtual addresses, it boils down to the device datasheet, as to which set of device registers and/or device memory to read from or write into, by adding their offsets to the virtual address returned by ioremap(). For that, the following are the APIs (prototyped in the same header file <asm/io.h>):

#include <asm/io.h>

unsigned int ioread8(void *virt_addr);
unsigned int ioread16(void *virt_addr);
unsigned int ioread32(void *virt_addr);
unsigned int iowrite8(u8 value, void *virt_addr);
unsigned int iowrite16(u16 value, void *virt_addr);
unsigned int iowrite32(u32 value, void *virt_addr);

Accessing the video RAM of “DOS” days

After this first set of information, students were directed for the live experiments. They were suggested to do an initial experiment with the video RAM of “DOS” days to understand the usage of the above APIs. Shweta got onto the system – displayed the /proc/iomem window – one very similar to as shown in Figure 12. From there, she got the video RAM address ranging from 0x000A0000 to 0x000BFFFF. And with that she added the above APIs with appropriate parameters into the constructor and destructor of her already written null driver to convert it into a vram driver. Then, she added the user access to the video RAM through read & write calls of the vram driver. Here’s what she coded in the new file video_ram.c:

#include <linux/module.h>
#include <linux/version.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <asm/io.h>

#define VRAM_BASE 0x000A0000
#define VRAM_SIZE 0x00020000

static void __iomem *vram;
static dev_t first;
static struct cdev c_dev;
static struct class *cl;

static int my_open(struct inode *i, struct file *f)
{
	return 0;
}
static int my_close(struct inode *i, struct file *f)
{
	return 0;
}
static ssize_t my_read(struct file *f, char __user *buf, size_t len, loff_t *off)
{
	int i;
	u8 byte;

	if (*off >= VRAM_SIZE)
	{
		return 0;
	}
	if (*off + len > VRAM_SIZE)
	{
		len = VRAM_SIZE - *off;
	}
	for (i = 0; i < len; i++)
	{
		byte = ioread8((u8 *)vram + *off + i);
		if (copy_to_user(buf + i, &byte, 1))
		{
			return -EFAULT;
		}
	}
	*off += len;

	return len;
}
static ssize_t my_write(
		struct file *f, const char __user *buf, size_t len, loff_t *off)
{
	int i;
	u8 byte;

	if (*off >= VRAM_SIZE)
	{
		return 0;
	}
	if (*off + len > VRAM_SIZE)
	{
		len = VRAM_SIZE - *off;
	}
	for (i = 0; i < len; i++)
	{
		if (copy_from_user(&byte, buf + i, 1))
		{
			return -EFAULT;
		}
		iowrite8(byte, (u8 *)vram + *off + i);
	}
	*off += len;

	return len;
}

static struct file_operations vram_fops =
{
	.owner = THIS_MODULE,
	.open = my_open,
	.release = my_close,
	.read = my_read,
	.write = my_write
};

static int __init vram_init(void) /* Constructor */
{
	int ret;
	struct device *dev_ret;

	if ((vram = ioremap(VRAM_BASE, VRAM_SIZE)) == NULL)
	{
		printk(KERN_ERR "Mapping video RAM failed\n");
		return -ENOMEM;
	}
	if ((ret = alloc_chrdev_region(&first, 0, 1, "vram")) < 0)
	{
		return ret;
	}
	if (IS_ERR(cl = class_create(THIS_MODULE, "chardrv")))
	{
		unregister_chrdev_region(first, 1);
		return PTR_ERR(cl);
	}
	if (IS_ERR(dev_ret = device_create(cl, NULL, first, NULL, "vram")))
	{
		class_destroy(cl);
		unregister_chrdev_region(first, 1);
		return PTR_ERR(dev_ret);
	}

	cdev_init(&c_dev, &vram_fops);
	if ((ret = cdev_add(&c_dev, first, 1)) < 0)
	{
		device_destroy(cl, first);
		class_destroy(cl);
		unregister_chrdev_region(first, 1);
		return ret;
	}
	return 0;
}

static void __exit vram_exit(void) /* Destructor */
{
	cdev_del(&c_dev);
	device_destroy(cl, first);
	class_destroy(cl);
	unregister_chrdev_region(first, 1);
	iounmap(vram);
}

module_init(vram_init);
module_exit(vram_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Anil Kumar Pugalia <email@sarika-pugs.com>");
MODULE_DESCRIPTION("Video RAM Driver");

Summing up

Then, Shweta repeated the following steps:

  • Build the vram driver (video_ram.ko file) by running make with the same Makefile changed to build this driver.
  • Usual load of the driver using insmod video_ram.ko.
  • Usual write into /dev/vram, say using echo -n “0123456789” > /dev/vram.
  • Read the /dev/vram contents using xxd /dev/vram | less. The usual cat /dev/vram also can be used but that would give all binary content. xxd shows them up as hexadecimal in centre with the corresponding ASCII along the right side.
  • Usual unload the driver using rmmod video_ram.

Note 1: Today’s systems typically use separate video cards having their own video RAM. So, the video RAM used in the “DOS days”, i.e. the one mentioned in this article is unused and many a times not even present. Hence, playing around with it, is safe, without any effect on the system, or the display.

Note 2: Moreover, if the video RAM is absent, the read/write may not be actually reading/writing, but just sending/receiving signals in the air. In such a case, writes would not do any change, and reads would keep on reading the same value – thus ‘xxd’ showing the same values.

It was yet half an hour left for the practical class to be over and a lunch break. So, Shweta decided to walk around and possibly help somebody in their experiments.

Eighth Article >>

Notes:

  1. When a pointer is tagged with __iomem, it enables that pointer for compiler checks &/or optimizations, relevant for I/O mapped memory.

Other References:

  1. Translating addresses in Kernel Space on different architectures
  2. Addressing Concepts in Linux
  3. Linux Memory Management Overview
   Send article as PDF