Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Creating and Operating Device Files for Linux PCIe Drivers

Tech 1

PCIe devices in Linux are implemented as character devices, wich expose hardware functionality via a device node under the /dev directory. User applications interact with PCIe hardware using standard file I/O operations on this node.

Creating the Character Device Node

Folow these steps to register a character device and create its device node:

/* 1. Allocate dynamic device number */
ret = alloc_chrdev_region(&pcie_demo_data.dev_num, 0, 1, "pcie_demo");

/* 2. Initialize character device structure */
pcie_demo_data.cdev.owner = THIS_MODULE;
cdev_init(&pcie_demo_data.cdev, &pcie_dev_fops);

/* 3. Add character device to kernel */
cdev_add(&pcie_demo_data.cdev, pcie_demo_data.dev_num, 1);

/* 4. Create device class for udev auto node creation */
pcie_demo_data.dev_class = class_create(THIS_MODULE, "pcie_demo");
if (IS_ERR(pcie_demo_data.dev_class)) {
    return PTR_ERR(pcie_demo_data.dev_class);
}

/* 5. Create device node in /dev */
pcie_demo_data.device = device_create(pcie_demo_data.dev_class, NULL, pcie_demo_data.dev_num, NULL, "pcie_demo");
if (IS_ERR(pcie_demo_data.device)) {
    return PTR_ERR(pcie_demo_data.device);
}

We first define an empty device operation struct as a placeholder:

/* Device file operations structure */
static struct file_operations pcie_dev_fops = {
    .owner = THIS_MODULE,
};

Add the creation code above to your driver's initialization entry, and add corresponding cleanup logic to the driver exit function:

static void __exit pcie_demo_exit(void)
{
	if(pcie_demo_data.pci_dev != NULL) {
		cdev_del(&pcie_demo_data.cdev);
		unregister_chrdev_region(pcie_demo_data.dev_num, 1);

		device_destroy(pcie_demo_data.dev_class, pcie_demo_data.dev_num);
		class_destroy(pcie_demo_data.dev_class);
	}
	
	pci_unregister_driver(&pcie_demo_driver);
}

After compiling and loading the driver module, you can see the newly created pcie_demo device in /dev.

Implementing Device File Operations

Next, we add implementations for open, release, read, and write operations, plus the full PCIe driver framework that interacts with a sample factorial computation PCIe peripheral:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/pci.h>
#include <linux/init.h>
#include <linux/errno.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/wait.h>
#include <linux/atomic.h>
#include <linux/io.h>

#define PCIE_DEMO_VENDOR_ID     0x1234
#define PCIE_DEMO_DEVICE_ID     0x11e8
#define PCIE_DEMO_REVISION_ID   0x10

/* PCIe device private data structure */
typedef struct {
    dev_t dev_num;
    struct cdev cdev;
    struct class *dev_class;
    struct device *device;
    struct pci_dev *pci_dev;
    void __iomem *bar0_base;
    atomic_t calc_in_progress;
    wait_queue_head_t calc_wait_queue;
} pcie_dev_data_t;

static pcie_dev_data_t pcie_demo_data;

/* PCIe device ID match table */
static struct pci_device_id pcie_demo_ids[] = {
    { PCI_DEVICE(PCIE_DEMO_VENDOR_ID, PCIE_DEMO_DEVICE_ID), },
    { 0 , }
};
MODULE_DEVICE_TABLE(pci, pcie_demo_ids);

/* Interrupt handler for computation completion */
static irqreturn_t pcie_demo_irq_handler(int irq, void *dev_private)
{
    pcie_dev_data_t *dev_data = (pcie_dev_data_t *)dev_private;
    uint32_t irq_status;

    /* Read interrupt status from BAR0 */
    irq_status = ioread32(dev_data->bar0_base + 0x24);
    /* Clear pending interrupt */
    iowrite32(irq_status, dev_data->bar0_base + 0x64);

    /* Verify interrupt clearing */
    irq_status = ioread32(dev_data->bar0_base + 0x24);
    if(irq_status != 0){
        dev_err(&dev_data->pci_dev->dev, "Failed to clear interrupt\n");
        return IRQ_NONE;
    }

    /* Wake up waiting read process */
    atomic_set(&dev_data->calc_in_progress, 0);
    wake_up_interruptible(&dev_data->calc_wait_queue);

    return IRQ_HANDLED;
}

/* Open device file */
static int pcie_dev_open(struct inode *inode, struct file *filp)
{
    init_waitqueue_head(&pcie_demo_data.calc_wait_queue);
    dev_info(pcie_demo_data.pci_dev, "Device file opened\n");
    return 0;
}

/* Close/release device file */
static int pcie_dev_release(struct inode *inode, struct file *filp)
{
    dev_info(pcie_demo_data.pci_dev, "Device file closed\n");
    return 0;
}

/* Write data to device: accept input value for factorial computation */
static ssize_t pcie_dev_write(struct file *filp, const char __user *buf, size_t count, loff_t *offset)
{
    int ret;
    uint32_t input;
    uint8_t tmp_buf[4] = {0};

    ret = copy_from_user(tmp_buf, buf, count);
    if(ret < 0) {
        dev_err(&pcie_demo_data.pci_dev->dev, "Copy from user failed\n");
        return -EFAULT;
    }

    /* Assemble 32-bit input value */
    input = tmp_buf[0] | (tmp_buf[1] << 8) | (tmp_buf[2] << 16) | (tmp_buf[3] << 24);
    /* Start computation on hardware */
    iowrite32(input, pcie_demo_data.bar0_base + 0x08);
    atomic_set(&pcie_demo_data.calc_in_progress, 1);

    return count;
}

/* Read data from device: get computation result after interrupt */
static ssize_t pcie_dev_read(struct file *filp, char __user *buf, size_t count, loff_t *offset)
{
    int ret;
    uint32_t result;

    /* Wait for computation completion interrupt */
    ret = wait_event_interruptible(pcie_demo_data.calc_wait_queue, 
        0 == atomic_read(&pcie_demo_data.calc_in_progress));
    if(ret)
        return ret;

    /* Read result from hardware BAR0 */
    result = ioread32(pcie_demo_data.bar0_base + 0x08);
    /* Copy result back to user space */
    ret = copy_to_user(buf, &result, sizeof(uint32_t));

    return sizeof(uint32_t);
}

/* Device operations struct */
static struct file_operations pcie_dev_fops = {
    .owner      = THIS_MODULE,
    .open       = pcie_dev_open,
    .release    = pcie_dev_release,
    .read       = pcie_dev_read,
    .write      = pcie_dev_write,
};

/* PCIe probe: run when device is matched */
static int pcie_demo_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
    int ret;
    int bar = 0;
    resource_size_t bar_len;

    ret = pci_enable_device(dev);
    if(ret) {
        dev_err(&dev->dev, "Failed to enable PCIe device\n");
        return ret;
    }

    /* Map BAR0 region to kernel virtual address */
    bar_len = pci_resource_len(dev, bar);
    pcie_demo_data.bar0_base = pci_iomap(dev, bar, bar_len);
    pcie_demo_data.pci_dev = dev;

    /* Register interrupt handler */
    ret = request_irq(dev->irq, pcie_demo_irq_handler, IRQF_SHARED, "pcie_demo", &pcie_demo_data);
    if(ret) {
        dev_err(&dev->dev, "Failed to request IRQ\n");
        return ret;
    }

    /* Enable hardware interrupt */
    iowrite32(0x80, pcie_demo_data.bar0_base + 0x20);

    return 0;
}

/* PCIe remove: run when driver is unloaded */
static void pcie_demo_remove(struct pci_dev *dev)
{
    /* Disable hardware interrupt */
    iowrite32(0x01, pcie_demo_data.bar0_base + 0x20);

    free_irq(dev->irq, &pcie_demo_data);
    pci_iounmap(dev, pcie_demo_data.bar0_base);
    pci_disable_device(dev);
}

/* PCIe driver structure */
static struct pci_driver pcie_demo_driver = {
    .name       = "pcie_demo",
    .id_table   = pcie_demo_ids,
    .probe      = pcie_demo_probe,
    .remove     = pcie_demo_remove,
};

/* Driver initialization entry */
static int __init pcie_demo_init(void)
{
    int ret = pci_register_driver(&pcie_demo_driver);

    if(pcie_demo_data.pci_dev == NULL){
        pr_err("pcie_demo: Failed to probe PCIe device\n");
        return ret;
    }

    /* Create character device node */
    ret = alloc_chrdev_region(&pcie_demo_data.dev_num, 0, 1, "pcie_demo");
    if(ret != 0)
        return ret;

    pcie_demo_data.cdev.owner = THIS_MODULE;
    cdev_init(&pcie_demo_data.cdev, &pcie_dev_fops);
    cdev_add(&pcie_demo_data.cdev, pcie_demo_data.dev_num, 1);

    pcie_demo_data.dev_class = class_create(THIS_MODULE, "pcie_demo");
    if (IS_ERR(pcie_demo_data.dev_class)) {
        unregister_chrdev_region(pcie_demo_data.dev_num, 1);
        return PTR_ERR(pcie_demo_data.dev_class);
    }

    pcie_demo_data.device = device_create(pcie_demo_data.dev_class, NULL, pcie_demo_data.dev_num, NULL, "pcie_demo");
    if (IS_ERR(pcie_demo_data.device)) {
        class_destroy(pcie_demo_data.dev_class);
        unregister_chrdev_region(pcie_demo_data.dev_num, 1);
        return PTR_ERR(pcie_demo_data.device);
    }

    return 0;
}

static void __exit pcie_demo_exit(void)
{
    if(pcie_demo_data.pci_dev != NULL) {
        cdev_del(&pcie_demo_data.cdev);                     
        unregister_chrdev_region(pcie_demo_data.dev_num, 1);

        device_destroy(pcie_demo_data.dev_class, pcie_demo_data.dev_num);
        class_destroy(pcie_demo_data.dev_class);
    }
    
    pci_unregister_driver(&pcie_demo_driver);
}

module_init(pcie_demo_init);
module_exit(pcie_demo_exit);
MODULE_LICENSE("GPL");
MODULE_INFO(intree, "Y");

Writing User Space Test Program

Create a user space test application test_pcie.c to interact with the driver:

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    int fd;
    uint32_t input = 6;
    uint32_t result;
    char *dev_path = "/dev/pcie_demo";

    /* Open device node */
    fd = open(dev_path, O_RDWR);
    if(fd < 0){
        perror("Failed to open device node");
        return EXIT_FAILURE;
    }

    /* Send input value to hardware */
    if(write(fd, &input, sizeof(uint32_t)) != sizeof(uint32_t)){
        perror("Failed to write to device");
        close(fd);
        return EXIT_FAILURE;
    }

    /* Read computation result */
    if(read(fd, &result, sizeof(uint32_t)) != sizeof(uint32_t)){
        perror("Failed to read from device");
        close(fd);
        return EXIT_FAILURE;
    }

    printf("Factorial of %d = %d\n", input, result);

    close(fd);
    return EXIT_SUCCESS;
}

Testing the Driver

Compile and load the kernel driver, then compile the test application with the following command:

gcc test_pcie.c -o pcie_test

Run test application, you will get the output Factorial of 6 = 720, which matches the expected result of 6! = 6 * 5 * 4 * 3 * 2 * 1 = 720.

Related Articles

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

SBUS Signal Analysis and Communication Implementation Using STM32 with Fus Remote Controller

Overview In a recent project, I utilized the SBUS protocol with the Fus remote controller to control a vehicle's basic operations, including movement, lights, and mode switching. This article is aimed...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.