深度分析Linux的UART子系统,实现串口通信的完美解决方案 (linux uart子系统)

串口通信作为一种最传统的通信方式,在工业自动化、通讯、控制等领域得到广泛使用。而Linux系统,作为一种流行的开源操作系统,也广泛应用于这些领域。本文将深度分析Linux的UART子系统,介绍如何实现一个完美的串口通信解决方案。

一、UART子系统概述

UART(Universal Asynchronous Receiver/Tranitter)是一种常见的串行通信协议,支持异步传输。UART接口被应用于所有的计算机上,从微控制器到工控机、服务器等设备。

Linux内核提供了UART子系统作为串行设备的核心实现。UART子系统的主要作用是管理和控制串行设备的读写,包括接口的标准化和通信流程的控制。它是Linux内核和用户空间之间交互的接口。在Linux内核中,UART子系统也是串行驱动的基础。

UART子系统有四个重要的组成部分,包括内核中的UART驱动程序、串口子系统、TTY驱动程序以及用户空间的工具。

1. 内核中的UART驱动程序

在Linux内核中,UART驱动程序掌管与串行设备的通信。它提供了串口通信的底层操作函数,可以调用串口驱动的开关、初始化、写、读、控制等各种操作。Linux中有许多UART驱动程序,每种驱动程序可以用于支持不同的串行设备。

2. 串口子系统

串口子系统提供了一个抽象的串行接口,它允许控制和管理所有共享同一个串口的设备。串口子系统通过设备文件/dev/ttySx或/dev/ttyUSBx访问。串口驱动程序控制串口设备的行为,而TTY驱动程序提供了编码和解码机制来解决通用字符设备驱动程序和串口驱动程序之间的差异。

3. TTY驱动程序

TTY驱动程序是一个通用字符设备驱动程序,它将技术细节屏蔽在内部并将其公开为标准接口,与不同设备的驱动程序兼容。对于串口设备,TTY驱动程序通过串口子系统中的解码器和编码器提供了UART协议支持。这种方式简化了通讯编程,让应用程序员可以更加轻松地与串行设备交互。

4. 用户空间工具

用户空间应用程序可通过读写串口设备的文件来与串口设备通信。可以使用标准C库函数和API调用,如open()、close()、read()和write()等。

二、实现串口通信

实现串口通信需要广泛的应用程序知识,具体步骤如下:

1. 打开串口设备文件

首先需要通过open()函数打开串口设备文件。设备名称如/dev/ttyS0或/dev/ttyS1等。

“`C

int fd = open(“/dev/ttyS0”, O_RDWR | O_NOCTTY);

“`

2. 配置串口设备参数

通过TCgetattr()和TCsetattr()等函数可以从控制终端获取或设置端口的属性。常用的属性包括波特率、字符大小、校验位和停止位等。

“`C

struct termios options;

tcgetattr(fd, &options);

options.c_cflag |= (CLOCAL | CREAD);

options.c_cflag &= ~CSIZE;

options.c_cflag |= CS8;

options.c_cflag &= ~PARENB;

options.c_cflag &= ~CSTOPB;

cfsetispeed(&options, B115200);

cfsetospeed(&options, B115200);

tcsetattr(fd, TCSANOW, &options);

“`

以上是一种常见的串口设备属性设置,包括字符长度为8bits,无校验位,1个停止位,波特率为115200等。

3. 读/写数据

读写串口设备数据时,可以使用read()和write()函数。这些函数可以在指定的时间内等待读/写完成。

“`C

char buf[255];

int len = read(fd, buf, sizeof(buf));

write(fd, buf, len);

“`

4. 关闭串口设备

当完成串口通信后,应该使用close()函数关闭串口设备。

“`C

close(fd);

“`

通过上述步骤,我们可以轻松地实现串口通信。但是,由于串口技术的专业性,调试和优化过程可能会变得繁琐和昂贵。在此处,我们建议使用常见的串口调试工具,在Linux中有许多优秀的串口调试工具可用。

三、使用Debug工具

调试串口通信时,有几个重要的参数,包括波特率、数据位、停止位、校验位等。通过串口调试工具,可以更加直观地查看回应数据,调整参数等。

1. minicom

Minicom是一款开源的串口通信程序,可用于调试、监视和控制串口设备。它是一个命令行串口调试工具,提供了许多强大的功能,包括自动发送和断线重联等。

使用以下命令安装和启动minicom:

“`bash

sudo apt-get install minicom

sudo minicom -s

“`

在配置中,需要选择正确的串口设备,设置波特率和其他参数。

2. cutecom

Cutecom是一款基于GUI的串口调试工具,适合初学者使用。与minicom类似,cutecom也支持串行设备调试、参数配置和命令传输等功能。

使用以下命令安装和启动cutecom:

“`bash

sudo apt-get install cutecom

cutecom

“`

在图形化界面中,可以设置端口、波特率、流控等,并发送/接收数据。此外,还可以使用定时器计算/记录串口数据速率。Cutecom也提供了十六进制和八进制显示功能等。

四、

通过本文,我们深度分析了Linux的UART子系统,介绍了实现串口通信的完美解决方案。在实现和调试串口通信时,可以使用Linux内核的UART驱动程序、串口子系统、TTY驱动程序以及用户空间工具。通过调整和控制串口设备的参数,我们可以实现可靠的串口通信。

此外,在使用Linux进行串口通信时,我们还可以使用许多优秀的Debug工具,如minicom和cutecom等。这些工具使得串口通信更加直观和高效。那么,现在,我们可以尝试使用Linux中的UART子系统来实现工业自动化、通讯、控制等方面的任务了。

相关问题拓展阅读:

linux 内核 uart driver 只有fifo满才向用户buf传递数据吗

对于串口驱动的移植准备自己分析一下源代码的,但是发现自己好多地方都只知道一 些皮毛,不明白其中的道理,所以我上网搜的时候发现有好多人写了很多很好的文章了,下面我转载的这篇就非常不错,一个困恼我好久的问题是驱动代码中只是注 册了游信platform驱动,而platform设备注册在哪里?这个问题困恼我好久,源代码中一直没找到,下面文章就解决了这个问题。当然文章中详细了讲 述了很多细节的知识。

原文地址

(1)串口移植

S3C2440共有3个串口,在DK2440平台上串口0和串口1都作为普通串口使用,串口2工作在红外收发模式。TQ2440开发板将它们都作为普通串口,目前所需要的只有串口0,作为控制终端,所以此处不作修改。

在文件 linux/arch/arm/plat-s3c24xx/devs.c中定义了三个串口的硬件资源。

static struct resource s3c2410_uart0_resource = {

………………………………

};

static struct resource s3c2410_uart1_resource = {

………………………………

};

static struct resource s3c2410_uart2_resource = {

………………………………

};

在文件linux/arch/arm/plat-samsung/dev-uart.c中定义了每个串口对应的平台设备。

static struct platform_device s3c24xx_uart_device0 = {

.id = 0,

};

static struct platform_device s3c24xx_uart_device1 = {

.id = 1,

};

static struct platform_device s3c24xx_uart_device2 = {

.id = 2,

};

在文件linux/arch/arm/mach-s3c2440/mach-dk2440.c中有串口一些寄存器的初始化宴陪配置。

static struct s3c2410_uartcfg dk2440_uartcfgs __initdata = {

= {

…………………………

},

= {

…………………………

},

/* IR port */

= {

…………………………

}

};

在文件linux/arch/arm/mach-s3c2440/mach-dk2440.c中将调用函数

s3c24xx_init_uarts()最神祥轮终将上面的硬件资源,初始化配置,平台设备整合到一起。

在文件 linux/arch/arm/plat-s3c/init.c中有

static int __init s3c_arch_init(void)

{

………………………………

ret = platform_add_devices(s3c24xx_uart_devs, nr_uarts);

return ret;

}

这个函数将串口所对应的平台设备添加到了内核。

(2)串口设备驱动原理浅析

我认为任何设备在linux中的实现就“两条线”。一是设备模型的建立,二是读写数据流。串口驱动也是这样。

串口设备模型建立:

串口设备驱动的核心结构体在文件linux/drivers/serial/samsuing.c中如下

static struct uart_driver s3c24xx_uart_drv = {

.owner = THIS_MODULE,

.dev_name = “s3c2410_serial”,

.nr = CONFIG_SERIAL_SAMSUNG_UARTS,

.cons = S3C24XX_SERIAL_CONSOLE,

.driver_name = S3C24XX_SERIAL_NAME,

.major = S3C24XX_SERIAL_MAJOR,

.minor = S3C24XX_SERIAL_MINOR,

};

串口驱动的注册

static int __init s3c24xx_serial_modinit(void)

{

………………………………

ret = uart_register_driver(&s3c24xx_uart_drv);

………………………………

}

串口驱动其实是一个典型的tty驱动

int uart_register_driver(struct uart_driver *drv)

{

………………………………

//每一个端口对应一个state

drv->state = kzalloc(sizeof(struct uart_state) * drv->nr, GFP_KERNEL);

………………………………

normal = alloc_tty_driver(drv->nr); //分配该串口驱动对应的tty_driver

………………………………

drv->tty_driver = normal; //让drv->tty_driver字段指向这个tty_driver

………………………………

normal->driver_name = drv->driver_name;

normal->name = drv->dev_name;

normal->major = drv->major;

normal->minor_start = drv->minor;

………………………………

//设置该tty驱动对应的操作函数集tty_operations (linux/drivers/char/core.c)

tty_set_operations(normal, &uart_ops);

………………………………

retval = tty_register_driver(normal); //将tty驱动注册到内核

………………………………

}

其实tty驱动的本质是一个字符设备,在文件 linux/drivers/char/tty_io.c中

int tty_register_driver(struct tty_driver *driver)

{

………………………………

cdev_init(&driver->cdev, &tty_fops);

driver->cdev.owner = driver->owner;

error = cdev_add(&driver->cdev, dev, driver->num);

………………………………

}

它所关联的操作函数集tty_fops在文件linux/drivers/char/tty_io.c中实现

static const struct file_operations tty_fops = {

.llseek = no_llseek,

.read = tty_read,

.write = tty_write,

………………………………

.open = tty_open,

………………………………

};

到此串口的驱动作为tty_driver被注册到了内核。前面提到串口的每一个端口都是作为平台设备被添加到内核的。那么这些平台设备就对应着有它们的平台设备驱动。在文件linux/drivers/serial/s3c2440.c中有:

static struct platform_driver s3c2440_serial_driver = {

.probe = s3c2440_serial_probe,

.remove = __devexit_p(s3c24xx_serial_remove),

.driver = {

.name = “s3c2440-uart”,

.owner = THIS_MODULE,

},

};

当其驱动与设备匹配时就会调用他的探测函数

static int s3c2440_serial_probe(struct platform_device *dev)

{

return s3c24xx_serial_probe(dev, &s3c2440_uart_inf);

}

每一个端口都有一个描述它的结构体s3c24xx_uart_port 在 文件linux/drivers/serial/samsuing.c

static struct s3c24xx_uart_port s3c24xx_serial_ports = {

= {

.port = {

.lock = __SPIN_LOCK_UNLOCKED(s3c24xx_serial_ports.port.lock),

.iotype = UPIO_MEM,

.irq = IRQ_S3CUART_RX0, //该端口的中断号

.uartclk = 0,

.fifosize = 16,

.ops = &s3c24xx_serial_ops, //该端口的操作函数集

.flags = UPF_BOOT_AUTOCONF,

.line = 0, //端口编号

}

},

………………………………

上面探测函数的具体工作是函数s3c24xx_serial_probe()来完成的

int s3c24xx_serial_probe(struct platform_device *dev,

struct s3c24xx_uart_info *info)

{

………………………………

//根据平台设备提供的硬件资源等信息初始化端口描述结构体中的一些字段

ret = s3c24xx_serial_init_port(ourport, info, dev);

//前面注册了串口驱动,这里便要注册串口设备

uart_add_one_port(&s3c24xx_uart_drv, &ourport->port);

………………………………

}

int uart_add_one_port(struct uart_driver *drv, struct uart_port *uport)

{

………………………………

//前面说串口驱动是tty_driver,这里可以看到串口设备其实是tty_dev

tty_dev = tty_register_device(drv->tty_driver, uport->line, uport->dev);

………………………………

}

串口数据流分析:

在串口设备模型建立中提到了三个操作函数集,uart_ops ,tty_fops,s3c24xx_serial_ops数据的流动便是这些操作函数间的调用,这些调用关系如下:

在对一个设备进行其他操作之前必须先打开它,linux/drivers/char/tty_io.c

static const struct file_operations tty_fops = {

………………………………

.open = tty_open,

………………………………

};

static int tty_open(struct inode *inode, struct file *filp)

{

………………………………

dev_t device = inode->i_rdev;

………………………………

driver = get_tty_driver(device, &index); //根据端口设备号获取它的索引号

………………………………

if (tty) {

………………………………

} else

tty = tty_init_dev(driver, index, 0); //创建一个tty_struct 并初始化

………………………………

}

struct tty_struct *tty_init_dev(struct tty_driver *driver, int idx,int first_ok)

{

………………………………

tty = alloc_tty_struct(); //分配一个tty_struct结构

//一些字段的初始化,

initialize_tty_struct(tty, driver, idx);

//完成的主要工作是driver->ttys = tty;

retval = tty_driver_install_tty(driver, tty);

………………………………

/*

下面函数主要做的就是调用线路规程的打开函数ld->ops->open(tty)。

在这个打开函数中分配了一个重要的数据缓存

tty->read_buf = kzalloc(N_TTY_BUF_SIZE, GFP_KERNEL);

*/

retval = tty_ldisc_setup(tty, tty->link);

}

void initialize_tty_struct(struct tty_struct *tty,struct tty_driver *driver, int idx)

{

………………………………

//获取线路规程操作函数集tty_ldisc_N_TTY,并做这样的工作tty->ldisc = ld;

tty_ldisc_init(tty);

………………………………

/*

下面函数的主要工作是INIT_DELAYED_WORK(&tty->buf.work, flush_to_ldisc);

初始化一个延时tty->buf.work 并关联一个处理函数flush_to_ldisc(),这个函数将在

数据读取的时候用到。

*/

tty_buffer_init(tty);

………………………………

tty->driver = driver;

tty->ops = driver->ops; //这里的ops就是struct tty_operations uart_ops

tty->index = idx; //idx就是该tty_struct对应端口的索引号

tty_line_name(driver, idx, tty->name);

}

端口设备打开之后就可以进行读写操作了,这里只讨论数据的读取,在文件 linux/drivers/char/tty_io.c中,

static const struct file_operations tty_fops = {

………………………………

.read = tty_read,

………………………………

};

static ssize_t tty_read(struct file *file, char __user *buf, size_t count,

loff_t *ppos)

{

………………………………

ld = tty_ldisc_ref_wait(tty); //获取线路规程结构体

if (ld->ops->read) //调用线路规程操作函数集中的n_tty_read()函数

i = (ld->ops->read)(tty, file, buf, count);

else

………………………………

}

在linux/drivers/char/N_tty.c中:

struct tty_ldisc_ops tty_ldisc_N_TTY = {

………………………………

.open= n_tty_open,

………………………………

.read= n_tty_read,

………………………………

};

static ssize_t n_tty_read(struct tty_struct *tty, struct file *file,

unsigned char __user *buf, size_t nr)

{

………………………………

while (nr) {

………………………………

if (tty->icanon && !L_EXTPROC(tty)) {

//如果设置了tty->icanon 就从缓存tty->read_buf中逐个数据读取,并判断读出的每一个数//据的正确性或是其他数据类型等。

eol = test_and_clear_bit(tty->read_tail,tty->read_flags);

c = tty->read_bufread_tail>;

………………………………

} else {

………………………………

//如果没有设置tty->icanon就从缓存tty->read_buf中批量读取数据,之所以要进行两次读

//取是因为缓存tty->read_buf是个环形缓存

uncopied = copy_from_read_buf(tty, &b, &nr);

uncopied += copy_from_read_buf(tty, &b, &nr);

………………………………

}

}

………………………………

}

用户空间是从缓存tty->read_buf中读取数据读的,那么缓存tty->read_buf中的数据有是从那里来的呢?分析如下:

回到文件 linux/drivers/serial/samsuing.c中,串口数据接收中断处理函数实现如下:

这是串口最原始的数据流入的地方

static irqreturn_t s3c24xx_serial_rx_chars(int irq, void *dev_id)

{

………………………………

while (max_count– > 0) {

………………………………

ch = rd_regb(port, S3C2410_URXH); //从数据接收缓存中读取一个数据

………………………………

flag = TTY_NORMAL; //普通数据,还可能是其他数据类型在此不做讨论

………………………………

/*

下面函数做的最主要工作是这样

struct tty_buffer *tb = tty->buf.tail;

tb->flag_buf_ptrused> = flag;

tb->char_buf_ptrused++> = ch;

将读取的数据和该数据对应标志插入 tty->buf。

*/

uart_insert_char(port, uerstat, S3C2410_UERSTAT_OVERRUN, ch, flag);

}

tty_flip_buffer_push(tty); //将读取到的max_count个数据向上层传递。

out:

return IRQ_HANDLED;

}

void tty_flip_buffer_push(struct tty_struct *tty)

{

………………………………

if (tty->low_latency)

flush_to_ldisc(&tty->buf.work.work);

else

schedule_delayed_work(&tty->buf.work, 1);

//这里这个延时work在上面串口设备打开中提到过,该work的处理函数也是flush_to_ldisc。

}

static void flush_to_ldisc(struct work_struct *work)

{

………………………………

while ((head = tty->buf.head) != NULL) {

………………………………

char_buf = head->char_buf_ptr + head->read;

flag_buf = head->flag_buf_ptr + head->read;

………………………………

//刚才在串口接收中断处理函数中,将接收到的数据和数据标志存到tty->buf中,现在将

//这些数据和标志用char_buf 和flag_buf指向进一步向上传递。

disc->ops->receive_buf(tty, char_buf,flag_buf, count);

spin_lock_irqsave(&tty->buf.lock, flags);

}

}

上面调用的函数disc->ops->receive_buf在文件linux/drivers/char/N_tty.c中实现

struct tty_ldisc_ops tty_ldisc_N_TTY = {

………………………………

.receive_buf = n_tty_receive_buf,

………………………………

};

static void n_tty_receive_buf(struct tty_struct *tty, const unsigned char *cp, char *fp, int count)

{

………………………………

//现在可以看到缓冲区tty->read_buf 中数据的由来了。

if (tty->real_raw) {

//如果设置了tty->real_raw将上面讲到的些传入数据批量拷贝到tty->read_head中。

//对环形缓存区的数据拷贝需要进行两次,之一次拷贝从当前位置考到缓存的末尾,如果还//有没考完的数据而且缓存区开始出处还有剩余空间,就把没考完的数据考到开始的剩余空

//间中。

spin_lock_irqsave(&tty->read_lock, cpuflags);

i = min(N_TTY_BUF_SIZE – tty->read_cnt,N_TTY_BUF_SIZE – tty->read_head);

i = min(count, i);

memcpy(tty->read_buf + tty->read_head, cp, i);

tty->read_head = (tty->read_head + i) & (N_TTY_BUF_SIZE-1);

tty->read_cnt += i;

cp += i;

count -= i;

i = min(N_TTY_BUF_SIZE – tty->read_cnt,

N_TTY_BUF_SIZE – tty->read_head);

i = min(count, i);

memcpy(tty->read_buf + tty->read_head, cp, i);

tty->read_head = (tty->read_head + i) & (N_TTY_BUF_SIZE-1);

tty->read_cnt += i;

spin_unlock_irqrestore(&tty->read_lock, cpuflags);

} else {

for (i = count, p = cp, f = fp; i; i–, p++) {

//如果没有设置tty->real_raw,就根据传入数据标志分类获取数据。

………………………………

}

………………………………

}

………………………………

}

linux uart子系统的介绍就聊到这里吧,感谢你花时间阅读本站内容,更多关于linux uart子系统,深度分析Linux的UART子系统,实现串口通信的完美解决方案,linux 内核 uart driver 只有fifo满才向用户buf传递数据吗的信息别忘了在本站进行查找喔。


数据运维技术 » 深度分析Linux的UART子系统,实现串口通信的完美解决方案 (linux uart子系统)