> 技术文档 > 深入浅出UART驱动开发与调试:从基础调试到虚拟驱动实现_uart调试

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


往期内容

本专栏往期内容: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博客
    – 末篇,有专栏内容观看顺序

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

目录

  • 往期内容
  • 1.UART驱动调试方法
    • 1.1 怎么得到UART硬件上收发的数据
      • 1.1.1 接收到的原始数据(收)
      • 1.1.2 发送出去的数据(发)
    • 1.2 proc文件
      • 1.2.1 /proc/interrupts
      • 1.2.2 /proc/tty/drivers
      • 1.2.3 /proc/tty/driver(非常有用)
      • 1.2.4 /proc/tty/ldiscs
    • 1.3 sys文件
  • 2.编写虚拟UART驱动程序
    • 2.1 要做的事
    • 2.2 虚拟的UART
    • 2.3 编程
      • 2.3.1 代码说明
      • 2.3.2 /proc文件
      • 2.3.3. 触发中断
    • 2.4 调试

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

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

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

img

1.2.4 /proc/tty/ldiscs

img

1.3 sys文件

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

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

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

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

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

2.编写虚拟UART驱动程序

2.1 要做的事

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

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

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

    • uart_add_one_port由platform_driver的probe函数调用

    • 所以:

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

2.2 虚拟的UART

深入浅出UART驱动开发与调试:从基础调试到虚拟驱动实现_uart调试为了做实验,还要创建一个虚拟文件:/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.88all:make -C $(KERN_DIR) M=`pwd` modules clean:make -C $(KERN_DIR) M=`pwd` modules cleanrm -rf modules.orderobj-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