超详细Uart驱动框架及编程方法

一、UART介绍

UART(Universal Asynchronous Receiver/Transmitter),中文全称为通用异步收发传输器,是一种异步收发传输器,它将要传输的数据通过并行到串行转换后再进行传输。该总线双向通信,可以实现全双工传输和接收。在嵌入式设备中,UART 用于主机与辅助设备通信。

1. 通信协议

UART通信协议的工作原理是将传输数据的每个比特位一位接一位地传输。其中各比特的意义如下:

  • 起始位:在时钟线为高电平时,数据线发出一个逻辑”0”的信号,表示传输字符的开始。

  • 数据位:紧接着起始位之后。数据位的个数可以是5、6、7、8等,构成一个字符。通常采用ASCII码。从最低位开始传送,靠时钟定位。

  • 奇偶校验位:数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验),以此来校验数据传送的正确性 。

  • 停止位:在时钟线为高电平时,数据线发出一个逻辑”1”的信号。可以是1位、1.5位、2位的高电平。

由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会。适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢。

  • 空闲位:当时钟线和数据新都处于逻辑“1”状态,表示当前线路上没有数据传送。

超详细Uart驱动框架及编程方法_第1张图片

2. 波特率

波特率是衡量数据传送快慢的指标。表示每秒钟传送的符号数(symbol)。一个符号代表的信息量(比特数)与符号的阶数有关。例如传输使用256阶符号,每8bit代表一个符号,数据传送速率为120字符/秒,则波特率就是120 baud,比特率是120*8=960bit/s。这两者的概念很容易搞错。

UART 的接收和发送是按照相同的波特率进行收发的。波特率发生器产生的时钟频率不是波特率时钟频率,而是波特率时钟频率的16倍,目的是为在接收时进行精确的采样,以提取出异步的串行数据。根据给定的晶振时钟和要求的波特率,可以算出波特率分频计数值。

3. 工作原理

  • 发送数据过程:空闲状态,线路处于高电位;当收到发送数据指令后,拉低线路一个数据位的时间T,接着数据位按低位到高位依次发送,数据发送完毕后,接着发送奇偶检验位和停止位(停止位为高电位),一帧数据发送结束。

  • 接收数据过程: 空闲状态,线路处于高电位;当检测到线路的下降沿(线路电位由高电位变为低电位)时说明线路有数据传输,按照约定的波特率从低位到高位接收数据,数据接收完毕后,接着接收并比较奇偶检验位是否正确,如果正确则通知则通知后续设备准备接收数据或存入缓存。

  • 采用率:UART是异步传输,没有传输同步时钟。为了能保证数据传输的正确性,UART采用16倍数据波特率的时钟进行采样。每个数据有16个时钟采样,取中间的采样值,以保证采样不会滑码或误码。一般UART一帧的数据位为8,这样即使每一个数据有一个时钟的误差,接收端也能正确地采样到数据。

  • 接收数据时序为:当检测到数据下降沿时,表明线路上有数据进行传输,这时计数器CNT开始计数,当计数器为8时,采样的值为“0”表示开始位;当计数器为24=16*1+8时,采样的值为bit0数据;当计数器的值为40=16*2+8时,采样的值为bit1数据;依次类推,进行后面6个数据的采样。如果需要进行奇偶校验位,则当计数器的值为152=16*9+8时,采样的值为奇偶位;当计数器的值为168=16*10+8时,采样的值为“1”表示停止位,一帧数据收发完成。

4. 流控

数据在两个串口传输时,常常会出现丢失数据的现象,或者两台计算机的处理速度不同,如台式机与单片机之间的通讯,接收端数据缓冲区以满,此时继续发送的数据就会丢失,流控制能解决这个问题,当接收端数据处理不过来时,就发出“不再接收”的信号,发送端就停止发送,直到收到“可以继续发送”的信号再发送数据。因此流控制可以控制数据传输的进程,防止数据丢失。

PC机中常用的两种流控为:硬件流控(包括RTS/CTS、DTR/CTS等)和软件流控制XON/XOFF(继续/停止)。

(1)硬件流控
  • 硬件流控制常用的有RTS/CTS流控制和DTR/DSR流控制两种。

    • **DTR–数据终端就绪(Data Terminal Ready)**低有效,当为低时,表示本设备自身准备就绪。此信号输出对端设备,使用对端设备决定能否与本设备通信。
    • **DSR-数据装置就绪(Data Set Ready)**低有效,此信号由本设备相连接的对端设备提供,当为低时,本设备才能与设备端进行通信。
    • **RTS - 请求发送(数据)(Request To Send)**低有效,此信号由本设备在需要发送数据给对端设备时设置。当为低时,表示本设备有数据需要向对端设备发送。对端设备能否接收到本方的发送数据,则通过CTS信号来应答。
    • **CTS - 接收发送(请求)(Clear To Send)**低有效,对端设备能否接收本方所发送的数据,由CTS决定。若CTS为低,则表示对端的以准备好,可以接收本端发送数据。
  • 以RTS/CTS流控制分析,分析主机发送/接收流程:

    • 物理连接

      超详细Uart驱动框架及编程方法_第2张图片
      • 主机的RTS(输出信号),连接到从机的CTS(输入信号)。主机是CTS(输入信号),连接到从机的RTS(输入信号)。
    • 主机的发送过程:主机查询主机的CTS脚信号,此信号连接到从机的RTS信号,受从机控制。如果主机CTS信号有效(为低),表示从机的接收FIFO未满,从机可以接收,此时主机可以向从机发送数据,并且在发送过程中要一直查询CTS信号是否为有效状态。主机查询到CTS无效时,则中止发送。主机的CTS信号什么时候会无效呢?从机在接收到主机发送的数据时,从机的接收模块的FIFO如果满了,则会使从机RTS无效,也即主机的CTS信号无效。主机查询到CTS无效时,主机发送中止。

    • 主机接收模式:如果主机接收FIFO未满,那么使主机RTS信号有效(为低),即从机的CTS信号有效。此时如果从机要发送,发送前会查询从机的CTS信号,如果有效,则开始发送。并且在发送过程中要一直查询从机CTS信号的有效状态,如果无效则终止发送。是否有效由主机的RTS信号决定。如果主机FIFO满了,则使主机的RTS信号无效,也即从机CTS信号无效,主机接收中止。

(2)软件流控
  • 由于电缆的限制,在普通的控制通讯中一般不采用硬件流控制,而是使用软件流控制。一般通过XON/XOFF来实现软件流控制。

  • 常用方法是:

    • 当接收端的输入缓冲区内数据量超过设定的高位时,就向数据发送端发送XOFF字符后就立即停止发送数据。
    • 当接收端的输入缓冲区内数据量低于设定的低位时,就向数据发送端发送XON字符(十进制的17或Control-Q),发送端收到XON字符后就立即开始发送数据。
  • 一般可从设备配套源程序中找到发送端收到XON字符后就立即发送数据。一般可以从设备配套源程序中找到发送的是什么字节。应注意,若传输的是二进制的数据,标志字符也可能在数据流中出现而引起误操作,这是软件流控的缺陷,而硬件流控不会出现这样的问题。

二、UART驱动编程

首先,带大家简单回顾一下tty架构情况,详情可参考一文彻底讲清Linux tty子系统架构及编程实例

https://img-blog.csdnimg.cn/img_convert/ec709699ec1680d05f8b1a915baef7a5.png

  • 整个 tty架构大概的样子如上图所示,简单来分的话可以说成两层:
    • 一层是下层我们的串口驱动层,它直接与硬件相接触,我们需要填充一个 struct uart_ops 的结构体;
    • 另一层是 tty 层,包括 tty 核心以及线路规程,它们各自都有一个 Ops 结构,用户空通过间是 tty 注册的字符设备节点来访问。
  • 发送数据的流程为:tty核心从一个用户获取将要发送给一个tty设备的数据,tty核心将数据传递给tty线路规程驱动,接着数据被传到tty驱动,tty驱动将数据转换为可以发给硬件的格式。
  • 接收数据的流程为:从tty硬件接收到的数据向上交给tty驱动,接着进入tty线路规程驱动,再进入tty核心,在这里它被一个用户获取。

1. UART驱动编写

(1) 注册uart_driver
  • 在uart driver的初始阶段(module_init()),需要将我们的struct uart_driver结构变量注册到内核,其注册流程大致为:
    1. uart_register_driver
    2. 申请n个uart_state结构的空间根据driver支持的最大设备数,申请n个uart_state空间,每一个uart_state都有一个uart_port。
    3. 分配及初始化tty_driver结构分配一个tty_driver,设置默认波特率、检验方式等,并将uart_driver->tty_driver指向它
    4. 设置tty_driver的操作集——tty_operations(它是tty核心与串口驱动通信的接口
    5. 设置tty_port的操作集——tty_port_operations(初始化每一个uart_state的tty_port
    6. 注册tty_driver注册uart_driver实际上就是注册tty_driver,与用户空间打交道的工作完全交给tty_driver,这一部分是内核实现好的不需要修改
    7. 完毕
(2) 添加uart_port
  • uart_add_one_port接口用于注册一个uart port 到uart driver上。此后uart driver就可以访问对应的uart port进行数据收发。该接口在uart driver中的probe函数中调用,所以必须保证晚于uart_register_driver的注册过程。

  • uart driver在调用接口前,要手动设置uart_port的操作集uart_ops,使得通过调用uart_add_one_port接口后驱动完成硬件的操作接口注册。

  • uart添加port流程如下图所示:

超详细Uart驱动框架及编程方法_第3张图片

2. 数据收发流程分析

(1)打开设备

超详细Uart驱动框架及编程方法_第4张图片

(2)数据发送流程(write)

超详细Uart驱动框架及编程方法_第5张图片

(3) 数据接收流程(read)

超详细Uart驱动框架及编程方法_第6张图片

(4)关闭设备(close)
超详细Uart驱动框架及编程方法_第7张图片

3. 注销流程

(1) 移除uart_port

此接口用于从uart driver上注销一个uart port,该接口在uart driver中的remove函数中调用。uart移除port的流程如图3-9所示:

超详细Uart驱动框架及编程方法_第8张图片
(2) 注销uart_driver

此接口在uart driver中调用,用来从kernel中注销uart_driver,调用阶段在uart driver的退出阶段,例如:module_exit(),uart driver的注销流程如图3.10所示

超详细Uart驱动框架及编程方法_第9张图片

三、UART驱动中的数据结构

  • 串口驱动数据结构总图

image-20201102212833734

  • 以s3c2440开发板为例,讲述其中UART驱动的数据结构构成:
    • 因为我们和开发板的人机交互的接口是Windows下的串口控制台。这就是上面所说的控制台终端。但是我们用了console = ttySAC0。即把串口终端当做控制台终端。所以我们要研究具体的代码需要cd到serial子目录下。即串口终端目录。ls显示serial下的文件结点。如图所示:

  • 我们主要关心的是两类文件:
    • 一类是与体系结构和板载资源无关的通用串口操作文件(samsung.c)。
    • 一类是与体系结构相关的硬件操作文件(s3c2440.c s3c2410.c s5pv210.c等)。我们为了得到具体的调用链。在具体的发送函数中加入回溯。如图所示。

  • 我们得到的函数调用链是这样的(以发送函数。即文件的写操作为例)

write-> sys_write-> vfs_write-> redirected_tty_write-> tty_write-> n_tty_write-> uart_write-> uart_start-> s3c24xx_serial_start_tx

  • 从具体代码上来看。这些函数基本上都是通过结构体中的函数指针调用。我们可以把这个调用链分为三个部分。即tty子系统核心、tty链路规程、tty驱动:
  1. tty核心:是对整个tty设备的抽象,对用户提供统一的接口,包括sys_write->vfs_write

  2. tty线路规程:是对传输数据的格式化,在tty_ldisc_N_TTY变量中描述,包括redirected_tty_write-> tty_write->n_tty_write

  3. tty驱动:是面向tty设备的硬件驱动,这里面真正的对硬件进行操作,包括uart_write-> uart_start-> s3c24xx_serial_start_tx

这是从具体函数的角度来看的调用链。下面为了从数据结构的角度来分析调用链,介绍linux内核中针对于这一个串口硬件的主要数据结构。

(1) uart_driver

就是uart驱动程序结构,封装了tty_driver,使得底层的UART驱动无需关心tty_driver,具体定义如下:

struct uart_driver{
    struct module	*owner;
    const char		*driver_name;
    const char		*dev_name;
    int				major;
    int				minor;
    int 			nr;
    struct console  *cons;
    /* these are private;the low level driver should not
     * touch these; they should be initialised to NULL
     */
    struct uart_state *state;
    ...
}
(2) uart_port

uart_port用于描述一个UART端口(直接对应于一个串口)的I/O端口或者IO内存地址等信息。

typedef unsigned int __bitwise__ upf_t;
struct uart_port{
    spinlock_t		lock;
    unsigned long	iobase;
    unsigned char __iomem *membase;
    unsigned int 	(*serial_in)(struct uart_port *, int);
    void 	(*serial_out)(struct uart_port *, int);
    void 	(*set_termios)(struct uart_port *, 
                           struct ktermios *new,
                           struct ktermios *old);
    void    (*pm)(struct uart_port *, unsigned int state, unsigned int old);
    unsigned int	irq;
    unsigned long	irqflags;
    unsigned int    uartclk;
    ...
}
(3) uart_ops

uart_ops定义了针对UART的一系列操作。注意这里不要把uart_ops结构和uart_ops变量混淆。uart_ops结构是我们这里的数据结构。而uart_ops变量则是一个tty_operations的变量。

在serial_core.c中定义了tty_operations的实例。即uart_ops变量,包含uart_open();uart_close();uart_send_xchar()等成员函数,这些函数借助uart_ops结构体中的成员函数来完成具体的操作:

struct uart_ops{
    unsigned int (*tx_empty)(struct uart_port *);
    void (*set_mctrl)(struct uart_port *, unsigned int mctrl);
    unsigned int (*get_mctrl)(struct uart_port *);
    void (*stop_tx)(struct uart_port *);
    void (*start_tx)(struct uart_port *);
    void (*send_xchar)(struct uart_port *, char ch);
    void (*stop_rx)(struct uart_port *);
    void (*enable_ms)(struct uart_port *);
    void (*break_ctl)(struct uart_port *, int ctl);
    void (*startup)(struct uart_port *);
    void (*shutdown)(struct uart_port *);
}

uart_ops变量是tty_operations型的一个变量。如下图所示:

static const struct tty_operations uart_ops = {
    .open			= uart_open,
    .close			= uart_close,
    .write			= uart_write,
    .put_char		= uart_put_char,
    .flush_chars	= uart_flush_chars,
    .write_room		= uart_write_room,
    .chars_in_buffer= uart_chars_in_buffer,
    .flush_buffer	= uart_flush_buffer,
    .ioctl			= uart_ioctl,
    .throttle		= uart_throttle,
    .unthrottle		= uart_unthrottle,
}
(4) uart_state

uart_state是uart的状态结构

struct uart_state{
    struct tty_port		port;
    int					pm_state;
    struct circ_buf		xmit;
    struct tasklet_struct tlet;
    struct uart_port	*uart_port;
};
#define UART_XMIT_SIZE	PAGE_SIZE
(5) uart_info

uart_info是uart的信息结构,在这个体系结构下定义为s3c24xx_uart_info:

struct s3c24xx_uart_info{
    char		*name;
    unsigned int 	type;
    unsigned int 	fifosize;
    unsigned long 	rx_fifomask;
    unsigned long 	rx_fifoshift;
    unsigned long 	rx_fifofull;
}
  • 所以很显然,用数据结构来描述函数调用链就是:

uart_driver -> uart_state-> uart_port-> uart_ops-> 特定的函数指针。

(6) 发送函数及中断服务例程

使能发送并没有真正的发送,而只是使能发送中断(enable_irq(ourport->tx_irq);)而已

static void s3c24xx_serial_start_tx(struct uart_port *port)
{
    struct s3c24xx_uart_port *ourport = to_ourport(port);
    if(!tx_enabled(port)){
        if(port->flags & UPF_CONS_FLOW)
            s3c24xx_serial_rx_disable(port);
        enable_irq(ourport->tx_irq);
        tx_enabled(port) = 1;
    }
}

这是因为ARM9处理器上有一个循环缓冲。用户从write系统调用传下来的数据就会写入这个UTXH0寄存器。发送完事之后处理器会产生一个内部中断。我们通过这个内部中断就可以实现流控过程,我们打开芯片手册可以看到如下字样:

如下才是发送中断的ISR(Interrupt Service Routine)中断服务例程。一个irqreturn_t类型的handler。

static irqreturn_t s3c24xx_serial_tx_chars(int irq, void *id)
{
    struct s3c24xx_uart_port *ourport = id;
    struct uart_port *port = &ourport->port;
    struct cirt_buf *xmit = &port->state->xmit;
    int count = 256;
    
    if(port->x_char){
        wr_regb(port,S3C2410_UTXH, port->x_char);
        port->icount.tx++;
        port->x_char = 0;
        goto out;
    }
    ...
}

这个wr_regb(port, S3C2410_UTXH, port->x_char);就是往特定寄存器写的过程。


至此我们的分析已经结束。相信读者对于Linux下的tty子系统已经有一个概观了。下面是这个uart驱动的总图。结合数据结构的调用链。Linux内核完成了驱动模型和特定硬件的分离:

在下一篇文章中,将继续讲解如何实际编写一个串口模块,欢迎点赞加关注!

【参考】

嵌入式大杂烩.原文

基于Linux的tty架构及UART驱动详解 (qq.com)

你可能感兴趣的:(Linux3.4.2驱动开发,1024程序员节,Uart,串口框架,驱动编程)