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 = <®_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 驱动开发的整体思维。