Driver Development Under Linux Device Tree

Overview

This article introduces the development process and methods of device drivers under the platform framework, mainly including the development of device trees, drivers, and applications. Taking the random number driver as an example, it implements the process of the application calling the library function, entering the kernel through a system call, and finally executing the hardware driver to obtain true random numbers.

Note: Actually, the Linux kernel has a random number driver framework, and specific driver adaptation can refer to Adding SMC to Obtain Random Numbers Between Linux and BL31.

Adding Device Tree Nodes

Add a child node named trng under the soc node, with the following content:

trng: trng@0x53030000 { compatible = "acme,trng"; reg = <0x00 0x53030000 0x00 0x1000>; interrupts = <0x34 IRQ_TYPE_LEVEL_HIGH>; interrupt-parent = <&plic>; };

Compile the device tree dts to generate the corresponding dtb file:

make dtbs

Use the new dtb to boot the Linux kernel. After Linux starts successfully, check if the trng node exists:

ls /proc/device-tree/soc
# trng@0x53030000

Enter the <span>trng</span> directory to check the attribute-related files:

/proc/device-tree/soc/trng@0x53030000# ls
compatible        interrupts        phandle
interrupt-parent  name              reg

Writing Device Drivers

Makefile

Create a new <span>trng/driver</span> directory and create a <span>Makefile</span> with the following content:

# Kernel architecture
ARCH := riscv
# Cross toolchain
CROSS_COMPILE := /path/to/riscv32-linux-
# Kernel directory
KERNELDIR := /path/to/linux/linux-6.1
# Current directory
PWD := $(shell pwd)
# Target file
obj-m := trng.o

# Target
build: kernel_modules

# Compile module
kernel_modules:
 $(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNELDIR) M=$(PWD) modules

# Clean
clean:
 $(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNELDIR) M=$(PWD) clean

Driver

Create a new <span>trng/driver</span> file named <span>trng.c</span> with the following content:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/fs.h>
#include <linux/of.h>
#include <linux/cdev.h>
#include <linux/io.h>
#include <linux/iopoll.h>
#include <linux/platform_device.h>

/* TRNG registers */
#define TRNG_CTRL           (0x0)
    #define TRNG_CTRL_CMD_MASK      (0x07)
    #define TRNG_CTRL_CMD_RNG       (0x01)
    #define TRNG_CTRL_CMD_SEED      (0x02)
#define TRNG_STAT           (0x4)
    #define TRNG_STAT_SEEDED        BIT(9)
#define TRNG_MODE           (0x8)
    #define TRNG_MODE_R256          BIT(3)
#define TRNG_ISTAT          (0x14)
    #define TRNG_ISTAT_RAND_RDY     BIT(0)
    #define TRNG_ISTAT_SEED_DONE    BIT(1)
#define TRNG_RAND0          (0x20)


#define TRNG_TIMEOUT        (50000)

#define DRIVER_NAME         "trng"

struct trng_dev {
    dev_t devid;            /* Device number */
    struct cdev cdev;       /* cdev */
    struct class *class;    /* Class */
    struct device *dev;     /* Device */
    int major;              /* Major device number */
    int minor;              /* Minor device number */
    int irq;                /* Interrupt number */
    void __iomem *base;     /* Base address */
};

static int trng_init(void *base)
{
    int ret;
    unsigned int value;

    /* Mode */
    value = readl(base + TRNG_MODE);
    value |= TRNG_MODE_R256;
    writel(value, base + TRNG_MODE);    

    /* Seeding */
    value = readl(base + TRNG_CTRL);
    value &= ~TRNG_CTRL_CMD_MASK;
    value |= TRNG_CTRL_CMD_SEED;
    writel(value, base + TRNG_CTRL);
    /* Wait for seeding to complete */
    ret = readl_relaxed_poll_timeout_atomic(base + TRNG_ISTAT,
                        value, (value & TRNG_ISTAT_SEED_DONE),
                        10, TRNG_TIMEOUT);
    if (ret == 0) {
        value |= TRNG_ISTAT_SEED_DONE;
        writel(value, base + TRNG_ISTAT);
    }
    return ret;
}

static int trng_generate_random(void *base, unsigned char *buf)
{
    int ret;
    unsigned int value;
    
    /* Start generating random numbers */
    value = readl(base + TRNG_CTRL);
    value &= ~TRNG_CTRL_CMD_MASK;
    value |= TRNG_CTRL_CMD_RNG;
    writel(value, base + TRNG_CTRL);
    /* Wait for random number to be ready */
    ret = readl_relaxed_poll_timeout_atomic(base + TRNG_ISTAT,
                        value, (value & TRNG_ISTAT_RAND_RDY),
                        10, TRNG_TIMEOUT);    
    if (ret) {
        return ret;
    }
    /* Clear ready flag */
    value = readl(base + TRNG_ISTAT);
    value &= ~TRNG_ISTAT_RAND_RDY;
    writel(value, base + TRNG_ISTAT);
    
    /* Read random number */
    for (int i = 0; i < 8; i++) {
        *(unsigned int*)buf = readl(base + TRNG_RAND0 + i*4);
        buf += 4;
    }

    return 0;
}

static irqreturn_t trng_irq_handler(int irq, void *dev_id)
{
    struct trng_dev *trng;

    trng = (struct trng_dev*)dev_id;
    
    dev_dbg(trng->dev, "TRNG interrupt received\n");
    return IRQ_HANDLED;
}

static int trng_open(struct inode *inode, struct file *filp) 
{
    int ret;
    struct trng_dev *trng;
    
    trng = container_of(inode->i_cdev, struct trng_dev, cdev);
    filp->private_data = trng;

    dev_dbg(trng->dev, "Open trng\n");

    ret = trng_init(trng->base);
    if (ret) {
        dev_err(trng->dev, "Failed to init trng, ret=%d\n", ret);
        return ret;
    }

    return 0;
}

static int trng_release(struct inode *inode, struct file *filp) 
{
    struct trng_dev *trng;

    trng = filp->private_data;
    
    dev_dbg(trng->dev, "Release trng\n");

    return 0;
}

static ssize_t trng_read(struct file *filp, char __user *buffer, size_t len, loff_t *offset)
{  
    int ret;
    unsigned char random[32];
    size_t copyed_len, len_to_copy;
    struct trng_dev *trng;

    trng = filp->private_data;

    dev_info(trng->dev, "Read trng\n");

    copyed_len = 0;
    while (len) {
        ret = trng_generate_random(trng->base, random);
        if (ret) {
            dev_err(trng->dev, "Failed to generate random, ret=%d\n", ret);
            return ret;
        }
        // print_hex_dump(KERN_INFO, "random: ", DUMP_PREFIX_NONE, 16, 1, random, sizeof(random), false);

        len_to_copy = (len < sizeof(random)) ? len : sizeof(random);
        ret = copy_to_user(buffer, random, len_to_copy);
        if (ret) {
            dev_err(trng->dev, "Failed to copy to user\n");
            return -EFAULT;
        }
        copyed_len += len_to_copy;
        buffer += len_to_copy;
        len -= len_to_copy;
    }

    return copyed_len;  
}  

static ssize_t trng_write(struct file *filp, const char __user *buffer, size_t len, loff_t *offset) 
{
    struct trng_dev *trng;

    trng = filp->private_data;
    
    dev_dbg(trng->dev, "Write trng\n");

    return len;
}


static long trng_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    struct trng_dev *trng;

    trng = filp->private_data;

    dev_dbg(trng->dev, "Ioctl trng\n");

    switch (cmd) {
    default:
     dev_err(trng->dev, "Unknown ioctl = 0x%x\n", cmd);
        break;
    }

 return -ENOTTY;
}

static int trng_mmap(struct file *filp, struct vm_area_struct *vma) 
{
    struct trng_dev *trng;

    trng = filp->private_data;

    dev_dbg(trng->dev, "Mmap trng\n");

    return 0;
}

static const struct file_operations trng_fops = {
    .owner = THIS_MODULE,
    .open = trng_open,
    .release = trng_release,
    .read = trng_read,
    .write = trng_write,
    .unlocked_ioctl = trng_ioctl,
    .mmap = trng_mmap,
};

static int trng_probe(struct platform_device *pdev) 
{
 struct device *dev = &pdev->dev;
 struct trng_dev *trng;
 int ret;

    /* Allocate memory */
    trng = devm_kzalloc(dev, sizeof(*trng), GFP_KERNEL);
    if (!trng) {
        dev_err(dev, "Failed to allocate memory\n");
        return -ENOMEM;
    }

    /* Map device resources to memory space */
 trng->base = devm_platform_ioremap_resource(pdev, 0);
 if (IS_ERR(trng->base)) {
        dev_err(dev, "Failed to map device registers\n");
        return PTR_ERR(trng->base);
    }

    /* Get device interrupt number */
    trng->irq = platform_get_irq(pdev, 0);
    if (trng->irq <= 0) {
  dev_err(dev, "Failed to get irq %d\n", trng->irq);
  return trng->irq;        
    }

    /* Request interrupt */
    ret = devm_request_irq(dev, trng->irq, trng_irq_handler, 0,
                           DRIVER_NAME, trng);
    if (ret) {
        dev_err(dev, "Failed to request IRQ\n");
        return ret;
    }

    /* Allocate device number */
    ret = alloc_chrdev_region(&trng->devid, 0, 1, DRIVER_NAME);  
    if (ret < 0) {  
        dev_err(dev, "Failed to allocate device number\n");  
        return ret;  
    }
    trng->major = MAJOR(trng->devid); 
    trng->minor = MINOR(trng->devid); 

    /* Initialize cdev */
    trng->cdev.owner = THIS_MODULE;
    cdev_init(&trng->cdev, &trng_fops);

    /* Add a cdev */
    ret = cdev_add(&trng->cdev, trng->devid, 1);
    if (ret < 0) {
        dev_err(dev, "Failed to add cdev\n");
        unregister_chrdev_region(trng->devid, 1);
        return ret;
    } 

    /* Create class */
    trng->class = class_create(THIS_MODULE, DRIVER_NAME);
    if (IS_ERR(trng->class)) {
        cdev_del(&trng->cdev);
        unregister_chrdev_region(trng->devid, 1);
        dev_err(dev, "Failed to create class\n");
        return PTR_ERR(trng->class);
    }

    /* Create device */
    trng->dev = device_create(trng->class, NULL, trng->devid, NULL, DRIVER_NAME);
    if (IS_ERR(trng->dev)) {
        cdev_del(&trng->cdev);
        unregister_chrdev_region(trng->devid, 1);
        class_destroy(trng->class);
        dev_err(dev, "Failed to create device\n");
        return PTR_ERR(trng->dev);
    }

    /* Save device private structure */
    platform_set_drvdata(pdev, trng);

    dev_info(dev, "TRNG platform driver probed\n");
    return 0;
}

static int trng_remove(struct platform_device *pdev) 
{
    struct trng_dev *trng;
    
    /* Get device private structure */
    trng = platform_get_drvdata(pdev);

    /* Delete cdev */
    cdev_del(&trng->cdev);
    /* Free device number */
    unregister_chrdev_region(trng->devid, 1);
    /* Delete device */
    device_destroy(trng->class, trng->devid);
    /* Delete class */
    class_destroy(trng->class);
    /* Free device memory */
    // devm_kfree(&pdev->dev, trng);    // Memory allocated by devm_kzalloc will be automatically released when the device is removed

    dev_info(&pdev->dev, "TRNG platform driver removed\n");
    return 0;
}

static const struct of_device_id trng_of_match[] = {
 { .compatible = "acme,trng" },
 { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, trng_of_match);

static struct platform_driver trng_driver = {
 .driver = {
  .name = DRIVER_NAME,
  .of_match_table = trng_of_match,
 },
 .probe = trng_probe,
 .remove = trng_remove,
};

module_platform_driver(trng_driver);

MODULE_AUTHOR("Author");
MODULE_DESCRIPTION("Trng driver");
MODULE_LICENSE("GPL");

Using the platform driver device model to write the driver program for <span>trng</span>.

  • When the node in the device tree matches the driver successfully, the <span>trng_probe</span> function will be executed to complete the driver loading.

  • When the application needs to obtain random numbers, it reads this <span>trng</span> device, entering the kernel calling function <span>trng_read</span>, which in turn calls the function <span>trng_generate_random</span> to obtain random numbers from the hardware.

  • If the device needs to be released, the <span>trng_remove</span> function will be called to unload the device driver.

Execute <span>make</span> to compile the driver program, and if the compilation is successful, it generates <span>trng.ko</span>.

After successfully starting Linux, you can mount <span>nfs</span> and copy <span>trng.ko</span> to the <span>trng</span> directory:

mkdir trng
mount -t nfs -o nolock xx.xx.xx.xx:/nfs/trng /root/trng

Execute the following command to load the device driver:

insmod trng.ko
# [  177.821055] trng 53030000.trng: TRNG platform driver probed

If the device driver loads successfully, you can find the device under <span>/dev</span>:

ls /dev/trng

Additionally, you can check the device number of <span>trng</span>:

cat /proc/devices
# 249 trng

If you need to unload the device driver, execute:

rmmod trng.ko
# [ 2947.495906] trng 53030000.trng: TRNG platform driver removed

Application App

Makefile

Create a new <span>trng/app</span> directory and create a <span>Makefile</span> with the following content:

# Cross toolchain
CROSS_COMPILE ?= /opt/andestech/nds32le-linux-glibc-v5d/bin/riscv32-linux-
 
# Specify C compiler
CC := $(CROSS_COMPILE)gcc
 
# Target file name
TARGET := trng
 
# Source file name
SRC := trng.c
 
# Default target
all: $(TARGET)
 
# Compile and link
$(TARGET): $(SRC)
 $(CC) $(SRC) -o $(TARGET)
 
# Clean
clean:
 rm -f $(TARGET)

Application

Create a new <span>trng/app</span> file named <span>trng.c</span> with the following content:

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h> 

#define TRNG_DEVICE                       "/dev/trng"

static void hexdump(const char *name, const unsigned char *buffer, unsigned int len)
{
    printf("****************%s****************\n", name);
    for (unsigned int i = 0; i < len; i++) {
        printf("%02x ", buffer[i]);
        if ((i + 1) % 16 == 0) {
            printf("\n");
        }
    }
    if (len % 16 ) {
        printf("\n");
    }
}

int main(int argc, char *argv[])
{
    uint8_t *buf = NULL;
    size_t num;
    int ret, fd;

    if (argc < 2) {
        printf("Usage: trng <num>\n");
        return -1;
    }
    num = atoi(argv[1]);

    buf = malloc(num);
    if (buf == NULL) {
        printf("Failed to malloc\n");
        return -1;
    }

    /* Open device */
    fd = open(TRNG_DEVICE, O_RDONLY);
    if (fd < 0) {
        printf("Failed to open trng device\n");
        goto exit;
    }
    
    /* Read random number */
    ret = read(fd, buf, num);
    if (ret < 0) {
        printf("Failed to read random, ret=%d\n", ret);
        goto exit;
    }

    hexdump("random", buf, num);
exit:
    close(fd);
    free(buf);

    return ret;
}

Execute <span>make</span> to compile the application program, and if the compilation is successful, it generates <span>trng</span>.

Similarly, copy the <span>trng</span> application to the <span>trng</span> directory and execute:

./trng 16
# ****************random****************
# 6c 95 ea 3c a0 1f e8 c2 03 db 66 f6 19 4b 07 e3 
# c0 96 a3 93 20 a9 68 c5 9f 1f a1 55 c0 9c 24 c9 
# 5f 06 47 45 be 2c 21 b5 11 23 23 e6 36 94 3f d6 
# 9a 30 68 91 da c4 6d ff af 46 26 c9 ab f8 79 7c 

If the random number is successfully obtained, it indicates that the driver and application program are running normally.

Compile Into Kernel

In the early stage of development, drivers are generally compiled into modules for easier debugging. After the driver development is completed, it can be compiled into the kernel.

Driver

Create a new <span>trng</span> directory under <span>linux-6.1/drivers</span> and create a <span>Makefile</span> and <span>Kconfig</span> file with the following content:

# SPDX-License-Identifier: GPL-2.0
#
# Makefile for the TRNG device drivers.
#

obj-$(CONFIG_TRNG) := trng.o
# SPDX-License-Identifier: GPL-2.0-only
#
# TRNG device configuration
#
config TRNG
    tristate "TRNG support"
    help
      This driver provides support for TRNG in SoCs. To compile this driver as a module, choose M here: the module will be called acme-trng. If unsure, say Y.

Add the following line in <span>linux/drivers/Makefile</span>:

obj-$(CONFIG_TRNG) += trng/

Add the following line in <span>linux/drivers/Kconfig</span>:

source "drivers/trng/Kconfig"

Copy the <span>trng.c</span> driver file to the <span>trng</span> directory, and the final directory structure is as follows:

$ tree linux-6.1/drivers/trng/
├── Kconfig
├── Makefile
└── trng.c

Kernel Configuration

Configure the kernel, enter the command:

make menuconfig

Select <span>Device Drivers->TRNG support</span> and choose to compile <span>trng</span> into the kernel. There are three options:

  • *: Compile this feature into the kernel
  • Empty: Do not compile this feature
  • M: Compile this feature as a module in the kernel

Run

Compile Linux and start it. In the startup log, if the following message is printed, it indicates that the TRNG driver is running normally:

[    4.974450] trng 53030000.trng: TRNG platform driver probed

You can execute the command to obtain random numbers:

cat /dev/trng | hexdump -n 32
0000000 df01 f5bc de33 2509 8d16 7b5f 8868 8bea
0000010 40f3 00f2 97a4 324d 03c2 10c8 b943 3d6d
0000020

Troubleshooting

  1. Loading KO reports an exception <span>trng: loading out-of-tree module taints kernel.</span>

    The reason is that this driver module has not been added to <span>Kconfig</span>.

  2. Using the function <span>devm_kzalloc</span> for memory allocation for the device will automatically release it when the device is removed, so explicit release <span>devm_kfree</span> is not required.

  3. The macro <span>container_of</span> is used to derive the address of the entire structure from the address of a member of the structure. Note especially that the first parameter must be the address of the member; if the member is a pointer variable, the address of that pointer variable needs to be taken.

Leave a Comment