Linux 驱动开发学习笔记:UART、触摸屏与 ADC 驱动实战

1. 引言

在嵌入式 Linux 开发中,字符设备驱动是最基础也最常用的驱动类型。UART、触摸屏和 ADC 三个外设虽然功能各异,但它们的驱动框架有着相似的脉络:设备树描述硬件、platform 驱动匹配、file_operations 提供用户态接口、中断或轮询处理数据。本文以这三类驱动为切入点,梳理 Linux 驱动开发的核心思路与实战要点。

2. 驱动开发基础准备

在动手编写驱动之前,需要先准备好开发环境并理解几个关键概念。

2.1 环境准备

  • 开发板:推荐使用 i.MX6ULL、STM32MP157 等常见 Linux 开发板,它们对 UART、触摸屏和 ADC 都有完善的设备树支持。
  • 交叉编译工具链:例如 arm-linux-gnueabihf-gcc,用于编译内核模块。
  • 内核源码:需要与开发板运行的内核版本一致,并完成内核配置。
  • NFS 或 TFTP:用于将编译好的内核模块和应用程序下载到开发板。

2.2 字符设备驱动框架

字符设备驱动的核心结构包括:

  • file_operations:定义 open、read、write、ioctl 等操作函数。
  • cdev:字符设备对象,负责将设备与文件系统关联。
  • 设备号:主设备号标识驱动类型,次设备号标识具体设备。
  • platform_driver:通过设备树匹配硬件资源。

一个典型的字符设备驱动注册流程如下:

c 复制代码
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>

static int major = 0;
static struct cdev test_cdev;
static struct class *test_class;

static int test_open(struct inode *inode, struct file *filp)
{
    return 0;
}

static ssize_t test_read(struct file *filp, char __user *buf,
                         size_t count, loff_t *off)
{
    return 0;
}

static struct file_operations test_fops = {
    .owner = THIS_MODULE,
    .open = test_open,
    .read = test_read,
};

static int __init test_init(void)
{
    dev_t devid;
    alloc_chrdev_region(&devid, 0, 1, "test");
    major = MAJOR(devid);
    cdev_init(&test_cdev, &test_fops);
    cdev_add(&test_cdev, devid, 1);
    test_class = class_create("test_class");
    device_create(test_class, NULL, devid, NULL, "test");
    return 0;
}

static void __exit test_exit(void)
{
    cdev_del(&test_cdev);
    unregister_chrdev_region(MKDEV(major, 0), 1);
    class_destroy(test_class);
}

module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");

3. UART 驱动学习笔记

UART(通用异步收发器)是嵌入式开发中最常用的串行通信接口,常用于调试串口、蓝牙模块、GPS 模块等设备通信。

3.1 UART 驱动框架

Linux 内核中,UART 驱动通常基于 tty 子系统实现。核心结构体是 uart_driver 和 uart_port,前者描述整个串口驱动,后者描述具体的串口端口。

UART 驱动的层次结构如下:

  • tty 核心层:负责与用户空间的交互,提供 read、write、ioctl 等接口。
  • uart 层:封装了串口驱动的通用逻辑,如波特率设置、数据格式配置。
  • 具体硬件驱动层:实现底层的寄存器读写、中断处理。

3.2 设备树配置

在设备树中,UART 节点通常如下描述:

dts 复制代码
&uart3 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart3>;
    status = "okay";
};

其中 pinctrl 节点用于配置引脚复用功能,将 GPIO 复用为 UART 的 TX 和 RX 引脚。

3.3 关键数据结构

编写 UART 驱动时,需要重点理解以下结构体:

  • uart_driver:包含驱动名称、设备号、ops 等字段。
  • uart_port:描述端口的内存地址、中断号、波特率等。
  • uart_ops:定义 start_tx、stop_tx、startup、shutdown 等回调函数。

3.4 数据收发流程

UART 数据接收通常采用中断方式:当接收寄存器非空时触发中断,中断处理函数读取数据并放入接收缓冲区,然后唤醒等待数据的进程。数据发送则通过写发送寄存器触发,发送完成后产生发送完成中断。

一个简化的接收中断处理流程如下:

c 复制代码
static irqreturn_t uart_isr(int irq, void *dev_id)
{
    struct uart_port *port = dev_id;
    unsigned int status = readl(port->membase + UART_SR);
    if (status & UART_SR_RX_READY) {
        unsigned char ch = readl(port->membase + UART_RBR);
        // 将数据放入接收缓冲区
        tty_insert_flip_char(port, ch, TTY_NORMAL);
        tty_flip_buffer_push(port);
    }
    return IRQ_HANDLED;
}

3.5 应用层使用

用户态程序通过标准文件操作访问串口设备节点,例如 /dev/ttymxc3:

c 复制代码
int fd = open("/dev/ttymxc3", O_RDWR | O_NOCTTY);
struct termios opt;
tcgetattr(fd, &opt);
cfsetispeed(&opt, B115200);
cfsetospeed(&opt, B115200);
opt.c_cflag |= (CLOCAL | CREAD);
tcsetattr(fd, TCSANOW, &opt);
write(fd, "Hello UART", 10);

4. 触摸屏驱动学习笔记

触摸屏是嵌入式设备的重要人机交互接口。Linux 内核使用 input 子系统 来管理触摸屏等输入设备。

4.1 触摸屏驱动框架

触摸屏驱动主要分为两部分:

  • 触摸屏控制器驱动:负责初始化硬件、读取触摸坐标、上报事件。
  • input 子系统:将触摸事件标准化为 input_event 结构,通过 /dev/input/eventX 提供给应用层。

4.2 设备树配置

以电阻式触摸屏为例,设备树节点通常包含以下信息:

dts 复制代码
&tsc {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_tsc>;
    interrupt-parent = <&gpio1>;
    interrupts = <13 IRQ_TYPE_LEVEL_LOW>;
    touchscreen-max-x = <1024>;
    touchscreen-max-y = <600>;
    status = "okay";
};

4.3 核心数据结构

触摸屏驱动中最重要的结构体是 input_dev,它描述了一个输入设备的能力和事件类型。

c 复制代码
struct input_dev *input_dev;
input_dev = input_allocate_device();
input_dev->name = "touchscreen";
input_dev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_ABS);
input_dev->keybit[BIT_WORD(BTN_TOUCH)] = BIT_MASK(BTN_TOUCH);
input_set_abs_params(input_dev, ABS_X, 0, 1024, 0, 0);
input_set_abs_params(input_dev, ABS_Y, 0, 600, 0, 0);
input_register_device(input_dev);

4.4 触摸事件上报

当触摸屏检测到触摸时,驱动需要上报坐标和压力事件:

c 复制代码
input_report_key(input_dev, BTN_TOUCH, 1);
input_report_abs(input_dev, ABS_X, x);
input_report_abs(input_dev, ABS_Y, y);
input_sync(input_dev);

其中 input_sync 表示一次完整事件的结束,应用层通过读取 input_event 结构获取触摸数据。

4.5 应用层读取触摸数据

c 复制代码
int fd = open("/dev/input/event0", O_RDONLY);
struct input_event ev;
while (1) {
    read(fd, &ev, sizeof(ev));
    if (ev.type == EV_ABS) {
        if (ev.code == ABS_X) printf("X = %d\n", ev.value);
        if (ev.code == ABS_Y) printf("Y = %d\n", ev.value);
    }
}

5. ADC 驱动学习笔记

ADC(模数转换器)用于将模拟电压信号转换为数字量,常用于采集温度、光照、电池电压等传感器数据。

5.1 ADC 驱动框架

Linux 内核使用 IIO(Industrial I/O)子系统 来管理 ADC、DAC、传感器等设备。IIO 子系统提供了统一的用户态访问接口 /dev/iio:deviceX。

5.2 设备树配置

ADC 设备树节点通常包含通道信息:

dts 复制代码
&adc1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_adc1>;
    vref-supply = <&reg_vref_3v3>;
    #address-cells = <1>;
    #size-cells = <0>;
    channel@0 {
        reg = <0>;
        label = "adc_ch0";
    };
    status = "okay";
};

5.3 核心数据结构

IIO 驱动中,iio_dev 是核心结构体,它描述了 ADC 设备的通道、属性和回调函数。

c 复制代码
struct iio_dev *indio_dev;
indio_dev = iio_device_alloc(sizeof(struct adc_priv));
indio_dev->name = "myadc";
indio_dev->info = &adc_info;
indio_dev->channels = adc_channels;
indio_dev->num_channels = ARRAY_SIZE(adc_channels);
iio_device_register(indio_dev);

5.4 数据读取流程

ADC 数据读取通常通过 read_raw 回调实现,驱动在该函数中启动一次转换并读取结果:

c 复制代码
static int adc_read_raw(struct iio_dev *indio_dev,
                        struct iio_chan_spec const *chan,
                        int *val, int *val2, long mask)
{
    struct adc_priv *priv = iio_priv(indio_dev);
    unsigned int value;
    // 启动 ADC 转换
    writel(1, priv->base + ADC_CR);
    // 等待转换完成
    while (!(readl(priv->base + ADC_SR) & ADC_SR_EOC));
    value = readl(priv->base + ADC_DR) & 0xFFF;
    *val = value;
    return IIO_VAL_INT;
}

5.5 应用层读取 ADC 数据

用户态可以通过 sysfs 接口读取 ADC 原始值:

bash 复制代码
cat /sys/bus/iio/devices/iio:device0/in_voltage0_raw

也可以使用 C 程序读取:

c 复制代码
int fd = open("/sys/bus/iio/devices/iio:device0/in_voltage0_raw", O_RDONLY);
char buf[16];
read(fd, buf, sizeof(buf));
int raw = atoi(buf);
float voltage = raw * 3.3f / 4096.0f;
printf("Voltage = %.2f V\n", voltage);

6. 三类驱动的对比与总结

通过以上学习,可以发现 UART、触摸屏和 ADC 驱动虽然面向不同的硬件,但整体框架高度相似。

对比项 UART 驱动 触摸屏驱动 ADC 驱动
子系统 tty 子系统 input 子系统 IIO 子系统
核心结构体 uart_driver / uart_port input_dev iio_dev
用户态接口 /dev/ttySX /dev/input/eventX /dev/iio:deviceX 或 sysfs
数据交互方式 中断 + 缓冲区 事件上报 轮询或触发转换
典型应用 调试串口、蓝牙模块 人机交互界面 传感器数据采集

学习这三类驱动,核心是掌握设备树描述硬件、platform 驱动匹配、子系统注册与回调实现这几个关键环节。建议读者在开发板上实际编译运行示例代码,通过阅读内核源码加深理解,逐步建立起 Linux 驱动开发的整体思维。

相关推荐
先知后行。1 小时前
各种文件系统
linux·服务器·数据库
youdexiang2 小时前
如何建立个人学习资料库?
学习
人工智能培训2 小时前
具身智能环境交互与学习范式原理
大数据·linux·人工智能·学习·算法·机器学习
Tj8105232 小时前
企业培训如何真正落地:从组织能力建设到培训ROI评估的完整指南
学习·学习方法
~kiss~2 小时前
LangGraph 和 ToT,Tree of Thoughts 同一步骤多条路线并行探索,用LLM做评判和减枝
学习
Sayai2 小时前
Java 进程 Unable to create new native thread 排查实战:Linux 线程数五层限制与 JVM -Xss 调优
java·linux·运维·jvm·性能优化
CS_Zero2 小时前
解决模型开发板ping不通模型终端设备问题
linux·网络
wuminyu2 小时前
Continuation::freeze与thaw的x86_64汇编Stack Chunk拆解源码分析
java·linux·c语言·汇编·jvm·c++
catino2 小时前
SDD:规范驱动开发(Spec-Driven Development)
驱动开发·ai