深入浅出UART驱动开发与调试:从基础调试到虚拟驱动实现

发布于:2024-11-29 ⋅ 阅读:(9) ⋅ 点赞:(0)

往期内容

本专栏往期内容:Uart子系统

  1. UART串口硬件介绍
  2. 深入理解TTY体系:设备节点与驱动程序框架详解
  3. Linux串口应用编程:从UART到GPS模块及字符设备驱动
  4. 解UART 子系统:Linux Kernel 4.9.88 中的核心结构体与设计详解
  5. IMX 平台UART驱动情景分析:注册篇
  6. IMX 平台UART驱动情景分析:open篇
  7. IMX 平台UART驱动情景分析:read篇–从硬件驱动到行规程的全链路剖析
  8. IMX 平台UART驱动情景分析:write篇–从 TTY 层到硬件驱动的写操作流程解析

interrupt子系统专栏:

  1. 专栏地址:interrupt子系统
  2. Linux 链式与层级中断控制器讲解:原理与驱动开发
    – 末片,有专栏内容观看顺序

pinctrl和gpio子系统专栏:

  1. 专栏地址:pinctrl和gpio子系统

  2. 编写虚拟的GPIO控制器的驱动程序:和pinctrl的交互使用

    – 末片,有专栏内容观看顺序

input子系统专栏:

  1. 专栏地址:input子系统
  2. input角度:I2C触摸屏驱动分析和编写一个简单的I2C驱动程序
    – 末片,有专栏内容观看顺序

I2C子系统专栏:

  1. 专栏地址:IIC子系统
  2. 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客
    – 末篇,有专栏内容观看顺序

总线和设备树专栏:

  1. 专栏地址:总线和设备树
  2. 设备树与 Linux 内核设备驱动模型的整合-CSDN博客
    – 末篇,有专栏内容观看顺序

img

1.UART驱动调试方法

img

1.1 怎么得到UART硬件上收发的数据

1.1.1 接收到的原始数据(收)

可以在接收中断函数里把它打印出来,这些数据也会存入UART对应的tty_port的buffer里:

img

imx_rxint
    // 读取硬件状态
    // 得到数据
    // 在对应的uart_port中更新统计信息, 比如sport->port.icount.rx++;
        ------添加打印---------
    // 把数据存入tty_port里的tty_buffer
    tty_insert_flip_char(port, rx, flg)
        ------添加打印,确保是否接收到数据---------
    // 通知行规程来处理
    tty_flip_buffer_push(port);
    	tty_schedule_flip(port);
			queue_work(system_unbound_wq, &buf->work); // 使用工作队列来处理
				// 对应flush_to_ldisc函数

1.1.2 发送出去的数据(发)

所有要发送出去的串口数据,都会通过uart_write函数发送,所有可以在uart_write中把它们打印出来:

imgimg

1.2 proc文件

1.2.1 /proc/interrupts

查看中断次数。

img

1.2.2 /proc/tty/drivers

img

1.2.3 /proc/tty/driver(非常有用)

img

1.2.4 /proc/tty/ldiscs

img

1.3 sys文件

drivers\tty\serial\serial_core.c中,有如下代码:

img

这写代码会在/sys目录中创建串口的对应文件,查看这些文件可以得到串口的很多参数。

怎么找到这些文件?在开发板上执行:

cd /sys
find -name uartclk  // 就可以找到这些文件所在目录

2.编写虚拟UART驱动程序

2.1 要做的事

img

  • 注册一个uart_driver:它里面有名字、主次设备号等

  • 对于每一个port,调用uart_add_one_port,里面的核心是uart_ops,提供了硬件操作函数

    • uart_add_one_port由platform_driver的probe函数调用

    • 所以:

      • 编写设备树节点
      • 注册platform_driver

2.2 虚拟的UART

img为了做实验,还要创建一个虚拟文件:/proc/virt_uart_buf

  • 要发数据给虚拟串口时,执行:echo “xxx” > /proc/virt_uart_buf
  • 要读取虚拟串口的数据时,执行:cat /proc/virt_uart_buf

2.3 编程

📎virtual_uart.c

📎serial_send_recv.c – 测试程序

# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH,          比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH,          比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin 
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
#       请参考各开发板的高级用户使用手册

KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88

all:
	make -C $(KERN_DIR) M=`pwd` modules 

clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order

obj-m   += virtual_uart.o
/{
	virtual_uart: virtual_uart_100ask {
		compatible = "100ask,virtual_uart";
		
		interrupt-parent = <&intc>;
		interrupts = <GIC_SPI 99 IRQ_TYPE_LEVEL_HIGH>;
		
	};
	
	
};

无非就是实现uart_driver、uart_ops、file_operations virt_uart_buf_fops、/proc/virt_uart_buf,采用plataform_driver

2.3.1 代码说明

1. 基本结构和宏定义

  • BUF_LEN 1024:定义了环形缓冲区的长度,1024字节。
  • NEXT_PLACE(i):计算缓冲区的下一个位置,这里通过位与操作实现循环数组(环形缓冲区)。

2. 环形缓冲区相关

代码定义了两个环形缓冲区:

  • txbuf:发送缓冲区,用于存储要发送的数据。
  • rxbuf:接收缓冲区,用于存储接收的数据。

并定义了如下指针和变量:

  • tx_buf_r, tx_buf_w:发送缓冲区的读写位置。
  • rx_buf_w:接收缓冲区的写位置。

环形缓冲区的相关操作:

  • is_txbuf_empty():判断发送缓冲区是否为空。
  • is_txbuf_full():判断发送缓冲区是否已满。
  • txbuf_put():向发送缓冲区放入一个字节。
  • txbuf_get():从发送缓冲区取出一个字节。
  • txbuf_count():计算缓冲区中的有效数据字节数。

3. UART驱动结构体

  • uart_driver virt_uart_drv:表示一个UART驱动结构体,其中包括驱动名称、设备名称、设备数量等信息。
struct uart_driver virt_uart_drv = {
    .owner = THIS_MODULE,
    .driver_name = "VIRT_UART",
    .dev_name = "ttyVIRT",  //最后设备节点的名字:/dev/ttyVIRTx
    .major = 0, // 动态分配主设备号
    .minor = 0, // 动态分配次设备号
    .nr = 1,    // UART设备数量为1
};
  • uart_port virt_port:表示虚拟串口硬件信息(包含硬件资源配置),如I/O地址、IRQ、FIFO大小、操作集等。

4. proc文件系统的创建

  • proc_create():创建一个proc文件,virt_uart_buf用来与用户空间交互。
uart_proc_file = proc_create("virt_uart_buf", 0, NULL, &virt_uart_buf_fops);

通过 /proc/virt_uart_buf 文件,可以读写虚拟UART的缓冲区。virt_uart_buf_fops是文件操作集,定义了read和write方法。

5. 文件操作函数

  • virt_uart_buf_read():从虚拟UART的发送缓冲区读取数据,拷贝给用户空间的缓冲区。
ssize_t virt_uart_buf_read(struct file *file, char __user *buf, size_t size, loff_t *ppos) {
    int cnt = txbuf_count();
    int i;
    unsigned char val;
    cnt = (cnt > size) ? size : cnt; // 限制读取字节数
    for (i = 0; i < cnt; i++) {
        txbuf_get(&val); // 从环形缓冲区获取数据
        copy_to_user(buf + i, &val, 1); // 复制数据到用户空间
    }
    return cnt;
}
  • virt_uart_buf_write():从用户空间接收数据,存入接收缓冲区,并模拟触发RX中断。
static ssize_t virt_uart_buf_write(struct file *file, const char __user *buf, size_t size, loff_t *off) {
    copy_from_user(rxbuf, buf, size); // 从用户空间拷贝数据到接收缓冲区
    rx_buf_w = size; // 更新接收缓冲区写指针
    irq_set_irqchip_state(virt_port->irq, IRQCHIP_STATE_PENDING, 1); // 模拟RX中断
    return size;
}

6. UART操作函数

这些函数定义了UART操作,如启动、停止传输等:

  • virt_tx_empty():判断发送缓冲区是否为空,这里总是返回1,因为数据在缓冲区瞬间发送完毕。
  • virt_start_tx():开始发送数据。它从UART内部的环形缓冲区读取数据并写入txbuf,表示发送操作。
  • virt_set_termios():配置UART波特率、停止位等参数,这里未实现。
  • virt_startup():启动UART设备,这里返回0,表示不需要额外初始化。
  • virt_set_mctrl()virt_get_mctrl():控制UART调制解调器状态,暂未实现。
  • virt_shutdown():关闭UART设备。
  • virt_type():返回虚拟UART类型的字符串。

7. 中断处理函数

  • virt_uart_rxint():虚拟的RX中断处理函数,处理接收的数据,将接收到的数据放入TTY层。
static irqreturn_t virt_uart_rxint(int irq, void *dev_id) {
    struct uart_port *port = dev_id;
    struct tty_port *tport = &port->state->port;
    unsigned long flags;
    int i;
    
    spin_lock_irqsave(&port->lock, flags);
    for (i = 0; i < rx_buf_w; i++) {
        port->icount.rx++; // 增加接收计数
        tty_insert_flip_char(tport, rxbuf[i], TTY_NORMAL); // 插入TTY缓冲区 / put data to ldisc
    }
    rx_buf_w = 0;
    spin_unlock_irqrestore(&port->lock, flags);
    tty_flip_buffer_push(tport); // 推送到用户空间
    return IRQ_HANDLED;
}

8. 平台设备驱动

  • virtual_uart_probe():平台设备的探测函数,用于初始化UART设备并请求中断。这个函数负责:
static int virtual_uart_probe(struct platform_device *pdev) {
    rxirq = platform_get_irq(pdev, 0); // 获取中断号
    virt_port = devm_kzalloc(&pdev->dev, sizeof(*virt_port), GFP_KERNEL); // 分配port结构体
    virt_port->irq = rxirq; // 设置中断号
    ret = devm_request_irq(&pdev->dev, rxirq, virt_uart_rxint, 0, dev_name(&pdev->dev), virt_port); // 注册中断
    return uart_add_one_port(&virt_uart_drv, virt_port); // 添加一个UART端口
}
  1. 创建proc文件。
  2. 从设备树中获取中断号并注册中断处理函数。
  3. 分配并初始化uart_port结构体,注册UART设备。
  • virtual_uart_remove():用于清理和移除UART设备,包括删除proc文件和反注册UART端口。
static int virtual_uart_remove(struct platform_device *pdev) {
    uart_remove_one_port(&virt_uart_drv, virt_port);
    proc_remove(uart_proc_file);
    return 0;
}

9. 设备树匹配

  • of_device_id virtual_uart_of_match[]:定义设备树匹配表,用于匹配“100ask,virtual_uart”兼容字符串。

10. 平台驱动结构体

  • platform_driver virtual_uart_driver:定义平台驱动结构体,其中包含proberemove函数,以及设备名称和设备树匹配表。

11. 模块初始化与退出

  • virtual_uart_init():模块初始化函数,注册UART驱动并注册平台驱动。
  • virtual_uart_exit():模块退出函数,反注册平台驱动和UART驱动。

调用关系总结:

  • 模块加载时,module_init()调用virtual_uart_init(),注册UART驱动并调用platform_driver_register()注册平台驱动。
  • virtual_uart_probe()会被调用,分配和初始化uart_port,注册中断处理函数并将UART端口注册到系统中。
  • 中断处理函数virt_uart_rxint()会在接收中断时被调用,处理接收的数据。
  • 用户可以通过/proc/virt_uart_buf文件读取和写入虚拟UART缓冲区,触发相关操作。

2.3.2 /proc文件

参考/proc/cmdline,怎么找到它对应的驱动?在Linux内核源码下执行以下命令搜索:

grep "cmdline" * -nr | grep proc

得到:

fs/proc/cmdline.c:26:   proc_create("cmdline", 0, NULL, &cmdline_proc_fops);

2.3.3. 触发中断

使用如下函数:

int irq_set_irqchip_state(unsigned int irq, enum irqchip_irq_state which,
              bool val);

怎么找到它的?在中断子系统中,我们知道往GIC寄存器GICD_ISPENDRn写入某一位就可以触发中断。内核代码中怎么访问这些寄存器?
drivers\irqchip\irq-gic.c中可以看到irq_chip中的"irq_set_irqchip_state"被用来设置中断状态:

static struct irq_chip gic_chip = {
    .irq_mask		= gic_mask_irq,
    .irq_unmask		= gic_unmask_irq,
    .irq_eoi		= gic_eoi_irq,
    .irq_set_type		= gic_set_type,
    .irq_get_irqchip_state	= gic_irq_get_irqchip_state,
    .irq_set_irqchip_state	= gic_irq_set_irqchip_state, /* 2. 继续搜"irq_set_irqchip_state" */
    .flags			= IRQCHIP_SET_TYPE_MASKED |
                  IRQCHIP_SKIP_SET_WAKE |
                  IRQCHIP_MASK_ON_SUSPEND,
};

static int gic_irq_set_irqchip_state(struct irq_data *d,
                     enum irqchip_irq_state which, bool val)
{
    u32 reg;

    switch (which) {
    case IRQCHIP_STATE_PENDING:
        reg = val ? GIC_DIST_PENDING_SET : GIC_DIST_PENDING_CLEAR; /* 1. 找到寄存器 */
        break;

    case IRQCHIP_STATE_ACTIVE:
        reg = val ? GIC_DIST_ACTIVE_SET : GIC_DIST_ACTIVE_CLEAR;
        break;

    case IRQCHIP_STATE_MASKED:
        reg = val ? GIC_DIST_ENABLE_CLEAR : GIC_DIST_ENABLE_SET;
        break;

    default:
        return -EINVAL;
    }

    gic_poke_irq(d, reg);
    return 0;
}

继续搜"irq_set_irqchip_state",在drivers\irqchip\irq-gic.c中可以看到:

int irq_set_irqchip_state(unsigned int irq, enum irqchip_irq_state which,
              bool val)
{
    ......
}

EXPORT_SYMBOL_GPL(irq_set_irqchip_state);

以后就可与使用如下代码触发某个中断:

irq_set_irqchip_state(irq, IRQCHIP_STATE_PENDING, 1);

2.4 调试

装载驱动程序后,可以知道其设备节点是:/dev/ttyVIRT0
运行测试程序后,出现了input/output error之类的错误,如何去调试查看呢?
>>>strace -o log.txt ./serial send recv /dev/ttyVIRT0
该命令会将输出信息保存到log.txt中,方便我们去查看

img