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

一、UART介绍

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

1. 通信协议

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

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

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

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

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

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

  • 空闲位:当时钟线和数据新都处于逻辑“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流控制分析,分析主机发送/接收流程:

    • 物理连接

      图片
      • 主机的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流程如下图所示:

https://mmbiz.qpic.cn/mmbiz_png/icRxcMBeJfcicKYmjrJA5eiaJJff6iav81VVc1HRDaTWCiaFzuRJNExicgKpEBPKqxic4fHKf2VyIMcicKtzic5dkU0QJdQ/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1

2. 数据收发流程分析

(1)打开设备

图片

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

图片

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

图片

(4)关闭设备(close)
图片

3. 注销流程

(1) 移除uart_port

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

图片
(2) 注销uart_driver

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

图片

三、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)

  • 8
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 使用Verilog语言来实现UART,需要实现以下几个步骤:1.定义UART的基本参数,如波特率;2.编写UART的模块,包括接收和发送模块;3.实现接收和发送模块之间的控制逻辑;4.编写驱动程序,控制UART进行数据传输。 ### 回答2: 使用Verilog语言实现UART(通用异步收发器)是一项将串行数据转换为并行数据或将并行数据转换为串行数据的重要任务。以下是使用Verilog实现UART的步骤: 1. 首先,定义UART的数据宽度和波特率等参数。数据宽度指的是并行数据的位数,波特率指的是串行通信时每秒传输的比特数。 2. 创建一个有限状态机(FSM)来控制UART的发送和接收过程。该状态机可以使用状态寄存器来表示各个状态。 3. 对于发送过程,需要为数据和校验位(如奇偶校验位)创建并行数据输入端口,并定义一个控制信号来启动发送过程。 4. 在发送模块中使用一个计数器来跟踪并行数据的位数,并将其转换为串行数据。在每个时钟周期中,将相应的并行数据位发送到串行数据输出端口。 5. 对于接收过程,需要定义一个控制信号来启动接收过程,并使用一个计数器来跟踪接收到的串行数据位数。 6. 在接收模块中,使用一个移位寄存器来接收串行数据位,并在每个时钟周期中将其转换为并行数据位发送到输出端口。 7. 实现校验功能,根据校验位的设置对发送和接收的数据进行校验。 8. 最后,将发送和接收模块结合在一起,实现完整的UART模块。 需要注意的是,以上只是基本的框架和思路,实际实现中可能还需要考虑其他细节,如时钟同步、数据传输协议等。 使用Verilog实现UART可以实现串行通信功能,广泛应用于各种通信领域,如网络通信、嵌入式系统和通信接口等。 ### 回答3: 使用Verilog编程语言可以很方便地实现UART(Universal Asynchronous Receiver Transmitter,通用异步收发器)。UART用于串行数据通信,可以通过该模块实现与外部设备的数据传输。 首先,在Verilog中实现UART需要定义模块的输入输出端口。常见的UART端口包括时钟信号,输入数据,输出数据以及控制信号等。根据需要,可以进一步增加奇偶校验等功能。 接下来,需要实现UART的核心逻辑部分。这包括时钟分频逻辑,接收缓冲区和发送缓冲区的FIFO(First-In First-Out,先进先出)逻辑等。 对于接收端,可以设置一个有限状态机来接收和处理串行数据。根据接收缓冲区的状态,可以解析出所接收到的数据,并进行相应的处理。同时,可以设置中断信号以通知主控制器数据的到达。 对于发送端,可以设置一个有限状态机来发送数据。根据发送缓冲区的状态,可以将数据发送至串行端口,并处理相关的时序问题。 最后,需要对UART模块进行仿真和验证。可以利用Verilog的仿真工具,如ModelSim等,进行功能验证,确保UART模块的正确性。 综上所述,通过使用Verilog编程语言,并结合适当的逻辑设计,可以实现UART模块。这样,我们就能够与外部设备进行串行数据通信,实现数据的传输和交换。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Leon_George

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值