第六章 Linux UIO driver
6.1 概述
在嵌入式系统和工业控制领域, 很多场合需要和外设进行高速数据通信, 这样常常是在内存中进行, 而Linux UIO (Usersapce I/O) driver可以胜任这种情况。UIO是Linux内核提供的一种框架,旨在简化设备驱动程序的开发。UIO框架的核心思想是将设备的中断处理和内存映射等关键功能保留在内核空间,而将设备的主要操作逻辑移到用户空间, APP使用mmap直接把DMA内存映射到用户空间, 这样显著地提高效率。
6.2 UIO Feature
1). 简化开发:UIO 驱动允许大部分驱动逻辑在用户空间运行,这使得开发和调试更加简单。开发者可以使用熟悉的用户空间工具和库.
2). 提高稳定性:由于大部分驱动代码在用户空间运行,驱动中的错误不太可能导致内核崩溃。这提高了系统的稳定性.
3). 灵活性:UIO 驱动可以在不重新编译内核的情况下进行更新,这使得驱动的维护和更新更加灵活.
4). 适用于特定硬件:UIO 驱动特别适用于那些不适合现有内核子系统的硬件设备,例如工业 I/O 卡、FPGA 等.
5). 实时性:在某些实时系统中,UIO 驱动可以提供足够的性能,同时简化开发过程.
这些优势使得 UIO 驱动在某些特定应用场景中非常有用.
6.3 UIO 驱动架构
Linux UIO架构图如下:
6.4 UIO驱动设计
6.4.1 DTS定义内存
首先, 需要在DTS中定义一块DMA专用内存, 以便应用程序APP和外设硬件来使用, 这样就可以在DMA内存里直接映射到外设的各种寄存器。DTS如下定义:
/ {
reserved-memory {
#address-cells = <2>;
#size-cells = <2>;
ranges;
/* Reserve 16MB memory for demo how to swap data between APP and driver */
demo_memory: demo_memory@20000000 {
device_type = "memory";
/* reg = <high_address low_address high_size low_size>; */
reg = <0x0 (512 * 0x100000) 0x0 (16 * 0x100000)>; // 16M size
no-map;
};
};
demo_uio: uio@0 {
compatible = "neardi,demo-uio";
reg = <0x0 (512 * 0x100000) 0x0 (16 * 0x100000)>; // 16M size
memory-region = <&demo_memory>;
};
};
关于DTS的在Linux系统的作用及语法, 请参考DTS章节。
这里我们定义一个新的设备demo_uio, 此设备告诉Linux kernel去寻找"demo_uio"驱动来适配。
6.4.2 UIO驱动代码
这里我们重新设计一个自定义的UIO驱动, 由于没有实际的硬件, 因此仅仅demo如何在APP里直接使用DMA内存。 驱动代码如下:
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/uio_driver.h>
#include <linux/io.h>
#include <linux/workqueue.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/of_reserved_mem.h>
#include <linux/sysfs.h>
#include <linux/kobject.h>
#include <linux/io.h>
// Declare global variables for the workqueue, work structure, UIO info, and memory mapping
static struct workqueue_struct *uio_wq;
static struct work_struct uio_work;
static struct uio_info *global_info;
static void __iomem *mapped_mem; // Global variable to store the mapped memory address
static int ready = 0;
static ssize_t show_ready(struct device *dev, struct device_attribute *attr, char *buf)
{
return sprintf(buf, "%d\n", ready);
}
static ssize_t store_ready(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
printk("%s: buf = %s\n", __func__, buf);
sscanf(buf, "%d", &ready);
if (ready > 0) {
queue_work(uio_wq, &uio_work);
}
return count;
}
static DEVICE_ATTR(ready, 0664, show_ready, store_ready);
// Work handler function to read and write from memory
static void uio_demo_work_handler(struct work_struct *work)
{
u32 value;
u32 new_value = 0x98765432; // Example value to write
// Example read from memory
value = ioread32(mapped_mem); // Read 32-bit value from memory at offset 0
printk("Read value from 0 offset of memory: 0x%X\n", value);
// Example write to memory
iowrite32(new_value, mapped_mem + 0x4); // Write 32-bit value at offset 0x4
printk("Wrote value 0x%X to the offset 0x4 of memory\n", new_value);
ready = 0;
}
// UIO open function
static int uio_demo_open(struct uio_info *info, struct inode *inode)
{
printk("UIO demo device opened\n");
return 0;
}
// UIO release function
static int uio_demo_release(struct uio_info *info, struct inode *inode)
{
printk("UIO demo device released\n");
return 0;
}
// IRQ control function (optional for UIO)
static int uio_demo_irqcontrol(struct uio_info *info, s32 irq_on)
{
printk("UIO IRQ control: %d\n", irq_on);
return 0;
}
// Probe function for the platform driver
static int uio_demo_probe(struct platform_device *pdev)
{
struct uio_info *info;
struct resource *res;
int ret;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "Failed to get resource from DTS\n");
return -ENODEV;
}
mapped_mem = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(mapped_mem)) {
dev_err(&pdev->dev, "Failed to map memory\n");
return PTR_ERR(mapped_mem);
}
dev_info(&pdev->dev, "Memory mapped successfully at address %p\n", mapped_mem);
// Allocate and initialize UIO info structure
info = devm_kzalloc(&pdev->dev, sizeof(struct uio_info), GFP_KERNEL);
if (!info) {
dev_err(&pdev->dev, "Failed to allocate memory\n");
return -ENOMEM;
}
info->mem[0].addr = res->start;
info->mem[0].size = resource_size(res);
info->mem[0].memtype = UIO_MEM_PHYS;
info->name = "uio_demo";
info->version = "0.1";
info->irq = UIO_IRQ_NONE;
info->irqcontrol = uio_demo_irqcontrol;
info->open = uio_demo_open;
info->release = uio_demo_release;
// Register the UIO device
ret = uio_register_device(&pdev->dev, info);
if (ret) {
dev_err(&pdev->dev, "Failed to register UIO device\n");
return ret;
}
// Initialize workqueue and work handler
uio_wq = create_singlethread_workqueue("uio_wq");
INIT_WORK(&uio_work, uio_demo_work_handler);
global_info = info;
platform_set_drvdata(pdev, info);
// Create sysfs attribute
ret = device_create_file(&info->uio_dev->dev, &dev_attr_ready);
if (ret) {
dev_err(&pdev->dev, "Failed to create sysfs file: %d\n", ret);
}
printk("UIO demo driver registered successfully\n");
return 0;
}
// Remove function for the platform driver
static int uio_demo_remove(struct platform_device *pdev)
{
struct uio_info *info = platform_get_drvdata(pdev);
// Remove sysfs attribute
device_remove_file(&pdev->dev, &dev_attr_ready);
// Unregister the UIO device
uio_unregister_device(info);
destroy_workqueue(uio_wq);
printk("UIO demo driver removed\n");
return 0;
}
// Device tree match table
static const struct of_device_id uio_of_match[] = {
{ .compatible = "neardi,demo-uio", },
{},
};
MODULE_DEVICE_TABLE(of, uio_of_match);
// Platform driver structure
static struct platform_driver uio_demo_driver = {
.driver = {
.name = "uio_demo",
.of_match_table = uio_of_match,
},
.probe = uio_demo_probe,
.remove = uio_demo_remove,
};
// Register the platform driver
module_platform_driver(uio_demo_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linx zhang");
MODULE_DESCRIPTION("UIO Demo Driver with sysfs attribute");
1). 自定义驱动的配置
Linux kernel里的外设通常都是在DTS配置, 此驱动也不例外。 在设置platform_driver属性时需要与DTS保持一致, 如下所赋值"uio_demo":
// Platform driver structure
static struct platform_driver uio_demo_driver = {
.driver = {
.name = "uio_demo",
.of_match_table = uio_of_match,
},
.probe = uio_demo_probe,
.remove = uio_demo_remove,
};
2). 驱动代码有创建sysfs属性, 目的是APP向reserved memory内存写完数据时, 告知UIO驱动进一步处理数据。 关于如何在驱动里创建sysfs属性, 请参考第5章。 UIO驱动创建sysfs代码如下:
static int ready = 0;
static ssize_t show_ready(struct device *dev, struct device_attribute *attr, char *buf)
{
return sprintf(buf, "%d\n", ready);
}
static ssize_t store_ready(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
printk("%s: buf = %s\n", __func__, buf);
sscanf(buf, "%d", &ready);
if (ready > 0) {
queue_work(uio_wq, &uio_work);
}
return count;
}
static DEVICE_ATTR(ready, 0664, show_ready, store_ready);
当APP完成DMA数据后, 则向/sys/class/uio/uio0/device/ready设置1, 如下:
neardi@LPA3588:~/drivers/uio$ sudo -i
root@LPA3588:/sys/class/uio/uio0/device# ls
driver driver_override modalias of_node power ready subsystem uevent uio
root@LPA3588:/sys/class/uio/uio0# echo 1 > ready
root@LPA3588:/sys/class/uio/uio0# dmesg
[ 5503.307841] uio_demo 20000000.uio: Memory mapped successfully at address 000000000d95d1f8
[ 5503.309513] UIO demo driver registered successfully
[ 5787.413783] store_ready: buf = 1
[ 5787.414001] Read value from 0 offset of memory: 0x12345670
[ 5787.414013] Wrote value 0x98765432 to the offset 0x4 of memory
3). 驱动里使用workqueue来处理memory数据:
收到sysfs的ready属性为1时, 则使能workqueue, 如下:
if (ready > 0) {
queue_work(uio_wq, &uio_work);
}
处理函数如下:
// Work handler function to process data
static void uio_demo_work_handler(struct work_struct *work)
{
printk("%s, process data...\n", __func__);
ready = 0;
}
6.4.3 UIO框架sysfs属性
Linux UIO框架已经创建了内存sysfs的属性, 把DTS里定义的reserved memory地址, 大小都已sysfs属性呈现, 如下:
root@LPA3588:/sys/class/uio/uio0/maps/map0# ls
addr name offset size
root@LPA3588:/sys/class/uio/uio0/maps/map0# cat addr
0x0000000020000000
root@LPA3588:/sys/class/uio/uio0/maps/map0# cat name
root@LPA3588:/sys/class/uio/uio0/maps/map0# cat offset
0x0
root@LPA3588:/sys/class/uio/uio0/maps/map0# cat size
0x0000000001000000
6.5 编译驱动
在编译驱动之前, 需要先选择Linux UIO框架。 由于在第一章里的Linux Header Files没有包含UIO框架选项, 因此我们在SDK环境来编译驱动。
6.5.1 配置defconfig
在defconfig里选择如下选项:
--- a/kernel/arch/arm64/configs/rockchip_linux_defconfig
+++ b/kernel/arch/arm64/configs/rockchip_linux_defconfig
@@ -556,6 +556,9 @@ CONFIG_DMABUF_HEAPS_DEFERRED_FREE=y
CONFIG_DMABUF_HEAPS_PAGE_POOL=y
CONFIG_DMABUF_HEAPS_SYSTEM=y
CONFIG_DMABUF_HEAPS_CMA=y
+CONFIG_UIO=y
CONFIG_STAGING=y
CONFIG_FIQ_DEBUGGER=y
CONFIG_FIQ_DEBUGGER_NO_SLEEP=y
6.5.2 Makefile
修改kernel/drivers/uio目录里的Makefile文件, 如下:
diff --git a/kernel/drivers/uio/Makefile b/kernel/drivers/uio/Makefile
index c285dd2a4..e4991d616 100644
--- a/kernel/drivers/uio/Makefile
+++ b/kernel/drivers/uio/Makefile
@@ -11,3 +11,4 @@ obj-$(CONFIG_UIO_PRUSS) += uio_pruss.o
obj-$(CONFIG_UIO_MF624) += uio_mf624.o
obj-$(CONFIG_UIO_FSL_ELBC_GPCM) += uio_fsl_elbc_gpcm.o
obj-$(CONFIG_UIO_HV_GENERIC) += uio_hv_generic.o
+obj-m += demo_uio.o
6.5.3 编译驱动
在SDK根目录执行编译kernel即可, 如下:
neardi@ubuntu2004:/work/rk3588-linux$ ./build.sh kernel
processing option: kernel
============Start building kernel============
TARGET_ARCH =arm64
TARGET_KERNEL_CONFIG =rockchip_linux_defconfig
TARGET_KERNEL_DTS =rk3588-neardi-linux-lkd3588-f0
TARGET_KERNEL_CONFIG_FRAGMENT =
==========================================
#
# No change to .config
#
CALL scripts/atomic/check-atomics.sh
CALL scripts/checksyscalls.sh
CHK include/generated/compile.h
CC [M] drivers/uio/demo_uio.o
MODPOST modules-only.symvers
GEN Module.symvers
LD [M] drivers/uio/demo_uio.ko
Image: resource.img (with rk3588-neardi-linux-lkd3588-f0.dtb logo.bmp logo_kernel.bmp) is ready
Image: boot.img (with Image resource.img) is ready
Image: zboot.img (with Image.lz4 resource.img) is ready
编译成功后也在当前目录里生成了新的boot.img, 同时在kernel/drivers/uio/目录生成demo_uio.ko驱动文件。
6.5.4 加载驱动
由于需要更新DTS, 因此在加载驱动之前需要先更新boot.img。 使用Rockchip工具更新boot.img, 如下:
boot.img更新后, 把demo_uio.ko拷贝的开发板, 使用sudo insmod demo_uio.ko即可, 如下:
neardi@LPA3588:~/uio$ sudo insmod demo_uio.ko
neardi@LPA3588:~/uio$ dmesg
[ 5503.307841] uio_demo 20000000.uio: Memory mapped successfully at address 000000000d95d1f8
[ 5503.309513] UIO demo driver registered successfully
驱动加载成功后, 在/dev/会创建一个uio0的设备, 如下:
neardi@LPA3588:~/drivers/uio$ ls /dev/uio0 -l
crw------- 1 root root 238, 0 Aug 22 11:47 /dev/uio0
6.6 测试UIO驱动
6.6.1 APP测试代码
设计一个APP来直接访问reserved内存, 创建一个test.cpp文件, 如下:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <stdint.h>
#include <string.h>
#define UIO_DEV "/dev/uio0"
#define MAP_SIZE 0x1000000 // 16 MB (same as the memory reserved in DTS)
int main() {
int fd;
void *mapped_mem;
uint32_t read_value;
uint32_t write_value = 0x12345678;
// Open the UIO device
fd = open(UIO_DEV, O_RDWR);
if (fd < 0) {
perror("Failed to open UIO device");
return EXIT_FAILURE;
}
// Memory-map the UIO device memory
mapped_mem = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mapped_mem == MAP_FAILED) {
perror("Failed to mmap");
close(fd);
return EXIT_FAILURE;
}
// Write a new value at the beginning of the memory 0x0
*((volatile uint32_t *)(mapped_mem + 0x0)) = write_value;
printf("Wrote value 0x%X at the offset 0 of memory\n", write_value);
// Read a 32-bit value from the offset 0x04 of the memory
read_value = *((volatile uint32_t *)(mapped_mem + 0x4));
printf("the value read from memory offset 0x04 is: 0x%X\n", read_value);
// Read back the value from the same memory location
//read_value = *((volatile uint32_t *)(mapped_mem + 0x4));
//printf("Value read back from memory: 0x%X\n", read_value);
// Unmap the memory and close the UIO device
munmap(mapped_mem, MAP_SIZE);
close(fd);
return EXIT_SUCCESS;
}
向偏移0地址的内存写入0x12345678, 从偏移地址0x04的内存读出值。
6.6.2 编译APP代码
确保在开发板上面安装了GCC编译器(sudo apt install build-essential), 执行如下命令即可:
aarch64-linux-gnu-g++ -o test test.cpp
这样就生成了测试程序test。
6.6.3 运行测试APP
执行测试程序, 如下:
neardi@LPA3588:~/uio$ sudo ./test
Wrote value 0x12345670 at the offset 0 of memory
the value read from memory offset 0x04 is: 0x98765432
neardi@LPA3588:~/uio$ dmesg
[ 5503.307841] uio_demo 20000000.uio: Memory mapped successfully at address 000000000d95d1f8
[ 5503.309513] UIO demo driver registered successfully
[ 5787.413783] store_ready: buf = 1
[ 5787.414001] Read value from 0 offset of memory: 0x12345670
[ 5787.414013] Wrote value 0x98765432 to the offset 0x4 of memory
[ 5908.515016] UIO demo device opened
[ 5908.515594] UIO demo device released
当然, 也可以通过sysfs属性来测试驱动, 如下:
neardi@LPA3588:~/drivers/uio$ sudo -i
root@LPA3588:~# echo 1 > /sys/class/uio/uio0/device/ready
root@LPA3588:/sys/class/uio/uio0# dmesg
[ 3795.190703] uio_demo 20000000.uio: Memory mapped successfully at address 000000000d95d1f8
[ 3795.191720] UIO demo driver registered successfully
[ 3820.511562] store_ready: buf = 1
[ 3820.511743] Read value from 0 offset of memory: 0x12345678
[ 3820.511756] Wrote value 0x98765432 to the offset 0x4 of memory
[ 3876.081844] UIO demo device opened
[ 3876.082798] UIO demo device released
[ 3943.853448] UIO demo device opened
[ 3943.853999] UIO demo device released
[ 3983.537070] store_ready: buf = 1
从Log看, 上面从APP向memory写入及读取都成功了。
6.7 总结
UIO框架为开发简单设备驱动提供了一种高效且安全的解决方案。通过将大部分驱动逻辑移至用户空间,开发者可以更轻松地编写和调试驱动程序,同时减少内核代码的复杂性和风险。 避免内核copy_from_user()及copy_to_user() API需要逻辑地址映射、切换;在应用层通过UIO驱动直接访问DMA内存, 大大提高了效率及性能。