UART编程框架详解

1. UART介绍

UART:通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),简称串口。

  • 调试:移植u-boot、内核时,主要使用串口查看打印信息

  • 外接各种模块

 1.1 硬件知识_UART硬件介绍

UART的全称是Universal Asynchronous Receiver and Transmitter,即异步发送和接收。 串口在嵌入式中用途非常的广泛,主要的用途有:

  • 打印调试信息;

  • 外接各种模块:GPS、蓝牙;

串口因为结构简单、稳定可靠,广受欢迎。

通过三根线即可,发送、接收、地线。

TxD线把PC机要发送的信息发送给ARM开发板。 最下面的地线统一参考地。

1.2 串口的参数

  • 波特率:一般选波特率都会有9600,19200,115200等选项。其实意思就是每秒传输这么多个比特位数(bit)。

  • 起始位:先发出一个逻辑”0”的信号,表示传输数据的开始。

  • 数据位:可以是5~8位逻辑”0”或”1”。如ASCII码(7位),扩展BCD码(8位)。小端传输。

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

  • 停止位:它是一个字符数据的结束标志。

怎么发送一字节数据,比如‘A‘? ‘A’的ASCII值是0x41,二进制就是01000001,怎样把这8位数据发送给PC机呢?

  • 双方约定好波特率(每一位占据的时间);

  • 规定传输协议

    • 原来是高电平,ARM拉低电平,保持1bit时间;

    • PC在低电平开始处计时;

    • ARM根据数据依次驱动TxD的电平,同时PC依次读取RxD引脚电平,获得数据;

 前面图中提及到了逻辑电平,也就是说代表信号1的引脚电平是人为规定的。 如图是TTL/CMOS逻辑电平下,传输‘A’时的波形:

在xV至5V之间,就认为是逻辑1,在0V至yV之间就为逻辑0。

如图是RS-232逻辑电平下,传输‘A’时的波形:

在-12V至-3V之间,就认为是逻辑1,在+3V至+12V之间就为逻辑0。

RS-232的电平比TTL/CMOS高,能传输更远的距离,在工业上用得比较多。

市面上大多数ARM芯片都不止一个串口,一般使用串口0来调试,其它串口来外接模块。

1.3 串口电平

ARM芯片上得串口都是TTL电平的,通过板子上或者外接的电平转换芯片,转成RS232接口,连接到电脑的RS232串口上,实现两者的数据传输。

现在的电脑越来越少有RS232串口的接口,当USB是几乎都有的。因此使用USB串口芯片将ARM芯片上的TTL电平转换成USB串口协议,即可通过USB与电脑数据传输。

1.4 串口内部结构

ARM芯片是如何发送/接收数据? 如图所示串口结构图:

要发送数据时,CPU控制内存要发送的数据通过FIFO传给UART单位,UART里面的移位器,依次将数据发送出去,在发送完成后产生中断提醒CPU传输完成。 接收数据时,获取接收引脚的电平,逐位放进接收移位器,再放入FIFO,写入内存。在接收完成后产生中断提醒CPU传输完成。

图片中描述的是串行通信中的一个典型的串行接口(Peripheral Bus)结构,特别是关于数据发送和接收的组件。以下是对图片内容的解释:

  1. 发送器(发送器):

    • 负责将数据从串行接口发送出去。
  2. 发送FIFO寄存器:

    • 在FIFO(先进先出)模式下,发送FIFO寄存器用于暂存待发送的数据。
  3. 发送缓冲寄存器:

    • 这是一个64字节的缓冲区,用于存储即将通过发送器发送的数据。
  4. 发送保持寄存器:

    • 在非FIFO模式下,发送保持寄存器用于存储下一个要发送的字节。
  5. 发送移位器:

    • 负责将数据按位(bit)顺序移出,以串行方式发送。
  6. TXDn:

    • 表示发送数据线,数据通过这条线发送到外部设备。
  7. 控制:

    • 涉及串行通信的控制机制,如波特率、时钟源等。
  8. 波特率:

    • 串行通信中数据传输的速率,以比特每秒(bps)计量。
  9. 时钟源:

    • 为串行通信提供时钟信号的源,可以是PCLK(外设时钟)、FCLK(功能时钟)或UEXTCLK(外部时钟)。
  10. 单元产生器:

    • 可能是指控制单元,用于生成控制信号以管理数据传输。
  11. 接收器:

    • 负责接收来自外部设备的数据。
  12. 接收移位器:

    • 负责将接收到的串行数据按位顺序移入。
  13. RXDn:

    • 表示接收数据线,数据通过这条线接收到设备中。
  14. 接收保持寄存器:

    • 在非FIFO模式下,接收保持寄存器用于存储刚刚接收到的字节。
  15. 接收缓冲寄存器:

    • 这是一个64字节的缓冲区,用于存储接收到的数据。
  16. 接收FIFO寄存器:

    • 在FIFO模式下,接收FIFO寄存器用于暂存接收到的数据。
  17. FIFO模式与非FIFO模式:

    • FIFO模式允许使用整个64字节的缓冲寄存器作为FIFO,以暂存大量数据。
    • 非FIFO模式下,只使用缓冲寄存器中的1字节作为保持寄存器,用于存储单个数据字节。

在串行通信中,数据通常通过发送器和接收器在设备之间传输。FIFO模式和非FIFO模式决定了数据如何被存储和检索。FIFO模式适用于数据传输速率较高且需要缓冲大量数据的情况,而非FIFO模式则适用于数据传输速率较低或不需要大量缓冲的情况。

2.  Linux串口应用编程

在Linux系统中,操作设备的统一接口就是:open/ioctl/read/write。

对于UART,又在ioctl之上封装了很多函数,主要是用来设置行规程。

所以对于UART,编程的套路就是:

  • open

  • 设置行规程,比如波特率、数据位、停止位、检验位、RAW模式、一有数据就返回

  • read/write

编写驱动程序的套路:

  • 确定主设备号,也可以让内核分配

  • 定义自己的file_operations结构体

  • 实现对应的drv_open/drv_read/drv_write等函数,填入file_operations结构体

  • 把file_operations结构体告诉内核:register_chrdev

  • 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数

  • 有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev

  • 其他完善:提供设备信息,自动创建设备节点:class_create, device_create

 3. UART驱动框架分析

3.1 UART驱动情景分析_注册

 

3.2 UART驱动情景分析_open

它要做的事情:

  • 找到tty_driver

    • 分配/设置tty_struct

    • 行规程相关的初始化

3.3 UART驱动情景分析_read

串行通信中的"行规程":

在串行通信中,"行规程"特别指的是与串行数据传输相关的一系列设置,这些设置定义了数据如何被发送和接收。这包括:

  • 波特率:数据传输的速率,以比特每秒(bps)计量。
  • 数据位:每个字符的数据位长度,通常为7、8或9位。
  • 停止位:数据字符之间的可选额外位,用于标识字符边界,可以是1或2位。
  • 校验位:用于错误检测的位,可以是无校验、奇校验或偶校验。
  • 流控制:如XON/XOFF或RTS/CTS,用于控制数据流。

在Linux和其他操作系统中,行规程可以通过设置串行端口的属性来配置,这些属性通常通过termios结构体进行控制。

read过程分析:

流程为:

  • APP读

    • 使用行规程来读

    • 无数据则休眠

  • UART接收到数据,产生中断

    • 中断程序从硬件上读入数据

  • 发给行规程

    • 行规程处理后存入buffer

    • 行规程唤醒APP

  • APP被唤醒后,从行规程buffer中读入数据,返回

3.4 UART驱动情景分析_write

流程为:

  • APP写

    • 使用行规程来写

    • 数据最终存入uart_state->xmit的buffer里

  • 硬件发送:怎么发送数据?

    • 使用硬件驱动中uart_ops->start_tx开始发送

    • 具体的发送方法有2种:通过DMA,或通过中断

  • 中断方式

    • 方法1:直接使能 tx empty中断,一开始tx buffer为空,在中断里填入数据

    • 方法2:写部分数据到tx fifo,使能中断,剩下的数据再中断里继续发送

4.  编写虚拟UART驱动程序_框架

4.1  编写UART驱动要做的事

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

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

    • uart_add_one_port由platform_driver的probe函数调用

    • 所以:

      • 编写设备树节点

      • 注册platform_driver

4.2 源码分析

#include <linux/module.h>       // 模块化编程支持
#include <linux/ioport.h>       // I/O端口支持
#include <linux/init.h>        // 模块初始化和清理宏
#include <linux/console.h>     // 控制台支持
#include <linux/sysrq.h>       // 系统请求键支持
#include <linux/platform_device.h> // 平台设备支持
#include <linux/tty.h>         // TTY支持
#include <linux/tty_flip.h>    // TTY翻转缓冲区支持
#include <linux/serial_core.h> // 串行核心支持
#include <linux/serial.h>      // 串行硬件支持
#include <linux/clk.h>         // 时钟支持
#include <linux/delay.h>       // 延时支持
#include <linux/rational.h>    // 有理数支持
#include <linux/reset.h>       // 重置支持
#include <linux/slab.h>        // 内存分配
#include <linux/of.h>         // 设备树支持
#include <linux/of_device.h>   // 设备树设备支持
#include <linux/io.h>          // IO操作
#include <linux/dma-mapping.h> // DMA内存映射
#include <linux/proc_fs.h>     // 进程文件系统

#include <asm/irq.h>           // 特定体系结构的中断支持

#define BUF_LEN  1024          // 定义缓冲区长度为1024字节
#define NEXT_PLACE(i) ((i+1)&0x3FF) // 循环缓冲区的索引计算宏

// 定义全局变量
static struct uart_port	*virt_port; // 虚拟UART端口
static unsigned char txbuf[BUF_LEN]; // 发送缓冲区
static int tx_buf_r = 0;            // 发送缓冲区读索引
static int tx_buf_w = 0;            // 发送缓冲区写索引
static unsigned char rxbuf[BUF_LEN]; // 接收缓冲区
static int rx_buf_w = 0;            // 接收缓冲区写索引
static struct proc_dir_entry *uart_proc_file; // 串行控制台的proc文件

// 虚拟UART驱动结构体
static struct uart_driver virt_uart_drv = {
	.owner          = THIS_MODULE, // 驱动的模块所有者
	.driver_name    = "VIRT_UART", // 驱动名称
	.dev_name       = "ttyVIRT",   // 设备名称
	.major          = 0,          // 主设备号
	.minor          = 0,          // 次设备号
	.nr             = 1,          // 设备数量
};

// 循环缓冲区操作函数
static int is_txbuf_empty(void) // 检查发送缓冲区是否为空
{
	return tx_buf_r == tx_buf_w;
}

static int is_txbuf_full(void) // 检查发送缓冲区是否已满
{
	return NEXT_PLACE(tx_buf_w) == tx_buf_r;
}

static int txbuf_put(unsigned char val) // 向发送缓冲区添加数据
{
	if (is_txbuf_full())
		return -1;
	txbuf[tx_buf_w] = val;
	tx_buf_w = NEXT_PLACE(tx_buf_w);
	return 0;
}

static int txbuf_get(unsigned char *pval) // 从发送缓冲区读取数据
{
	if (is_txbuf_empty())
		return -1;
	*pval = txbuf[tx_buf_r];
	tx_buf_r = NEXT_PLACE(tx_buf_r);
	return 0;
}

static int txbuf_count(void) // 计算发送缓冲区中的数据量
{
	if (tx_buf_w >= tx_buf_r)
		return tx_buf_w - tx_buf_r;
	else
		return BUF_LEN + tx_buf_w - tx_buf_r;
}

// 虚拟UART的文件操作函数
ssize_t virt_uart_buf_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
	// 实现从虚拟UART读取数据到用户空间的函数
	// ...
}

static ssize_t virt_uart_buf_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
	// 实现从用户空间写入数据到虚拟UART的函数
	// ...
}

static const struct file_operations virt_uart_buf_fops = {
	.read = virt_uart_buf_read, // 读取操作
	.write = virt_uart_buf_write, // 写入操作
};

// 虚拟UART的中断处理函数和相关操作
static unsigned int virt_tx_empty(struct uart_port *port)
{
	// 检查发送缓冲区是否为空
	// ...
}

static void virt_start_tx(struct uart_port *port)
{
	// 开始发送数据
	// ...
}

// 其他虚拟UART操作函数
static void virt_set_termios(struct uart_port *port, struct ktermios *termios,
		struct ktermios *old)
{
	// 设置终端参数
	// ...
}

static int virt_startup(struct uart_port *port)
{
	// 启动虚拟UART
	// ...
}

// 虚拟UART驱动的probe和remove函数
static int virtual_uart_probe(struct platform_device *pdev)
{
	// 虚拟UART设备探测函数
	// ...
}

static int virtual_uart_remove(struct platform_device *pdev)
{
	// 虚拟UART设备移除函数
	// ...
}

// 虚拟UART驱动的入口和出口函数
static int __init virtual_uart_init(void)
{
	// 虚拟UART驱动的初始化函数
	// ...
}

static void __exit virtual_uart_exit(void)
{
	// 虚拟UART驱动的退出函数
	// ...
}

module_init(virtual_uart_init); // 注册初始化函数
module_exit(virtual_uart_exit); // 注册退出函数

MODULE_LICENSE("GPL"); // 模块许可证

代码解释:

  • 头文件包含:代码包括了处理模块化编程、I/O端口、初始化、控制台、系统请求键、平台设备、TTY、串行通信、时钟、延时、有理数、重置、内存分配、设备树、IO操作、DMA内存映射和进程文件系统等所需的头文件。
  • 宏定义:定义了缓冲区长度和循环缓冲区索引计算的宏。
  • 全局变量:定义了虚拟UART端口、发送和接收缓冲区、proc文件系统条目等全局变量。
  • 虚拟UART驱动结构体:定义了一个uart_driver结构体,包含了驱动的名称、设备名称、设备号和设备数量。
  • 循环缓冲区操作函数:实现了一组函数,用于管理发送缓冲区的状态,包括检查缓冲区是否为空或满,以及添加和读取数据。
  • 文件操作函数:实现了虚拟UART的读取和写入操作,用于与用户空间交换数据。
  • 虚拟中断处理和UART操作:实现了虚拟UART的中断处理函数和一系列UART操作函数,包括发送数据、接收数据、设置终端参数等。
  • 平台设备探测和移除函数:实现了平台设备的探测和移除函数,用于初始化和清理虚拟UART设备。
  • 模块初始化和退出函数:实现了模块的初始化和退出函数,用于注册和注销虚拟UART驱动。

这个模块实现了一个虚拟的UART驱动程序,它可以模拟UART硬件的行为,对于嵌入式系统开发中的串行通信测试和调试非常有用。通过这种方式,开发者可以在没有实际硬件的情况下开发和测试UART相关的软件。

 

Zynq是一种基于FPGA和ARM Cortex-A9处理器的SoC芯片,它可以运行Linux操作系统。下面是Zynq Linux下使用UART进行通信的基本步骤: 1. 在设备树中配置UART设备。打开设备树文件(.dts或.dtsi),找到UART节点,并配置对应的属性,如波特率、数据位、停止位、校验等。 2. 在Linux应用程序中打开UART设备。使用open()函数打开/dev/ttyPS0(PS表示processing system,PL表示programmable logic),并设置串口属性,如波特率、数据位、停止位、校验等。 3. 使用read()和write()函数进行数据读写。read()函数从串口读取数据,write()函数向串口发送数据。 下面是一段使用UART进行数据收发的示例代码: ```c #include <stdio.h> #include <fcntl.h> #include <termios.h> #include <unistd.h> int main() { int fd; struct termios options; // 打开UART设备 fd = open("/dev/ttyPS0", O_RDWR | O_NOCTTY | O_NDELAY); if (fd == -1) { perror("open"); return -1; } // 配置串口属性 tcgetattr(fd, &options); cfsetispeed(&options, B115200); cfsetospeed(&options, B115200); options.c_cflag |= (CLOCAL | CREAD); options.c_cflag &= ~PARENB; options.c_cflag &= ~CSTOPB; options.c_cflag &= ~CSIZE; options.c_cflag |= CS8; options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); options.c_iflag &= ~(IXON | IXOFF | IXANY); options.c_oflag &= ~OPOST; tcsetattr(fd, TCSANOW, &options); // 发送数据 char buf[] = "Hello, UART!"; write(fd, buf, sizeof(buf)); // 接收数据 char buf2[256]; int len = read(fd, buf2, sizeof(buf2)); buf2[len] = '\0'; printf("Received: %s\n", buf2); // 关闭UART设备 close(fd); return 0; } ``` 注意:以上代码仅供参考,实际使用时需要根据具体的硬件和应用场景进行修改。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值