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)结构,特别是关于数据发送和接收的组件。以下是对图片内容的解释:
-
发送器(发送器):
- 负责将数据从串行接口发送出去。
-
发送FIFO寄存器:
- 在FIFO(先进先出)模式下,发送FIFO寄存器用于暂存待发送的数据。
-
发送缓冲寄存器:
- 这是一个64字节的缓冲区,用于存储即将通过发送器发送的数据。
-
发送保持寄存器:
- 在非FIFO模式下,发送保持寄存器用于存储下一个要发送的字节。
-
发送移位器:
- 负责将数据按位(bit)顺序移出,以串行方式发送。
-
TXDn:
- 表示发送数据线,数据通过这条线发送到外部设备。
-
控制:
- 涉及串行通信的控制机制,如波特率、时钟源等。
-
波特率:
- 串行通信中数据传输的速率,以比特每秒(bps)计量。
-
时钟源:
- 为串行通信提供时钟信号的源,可以是PCLK(外设时钟)、FCLK(功能时钟)或UEXTCLK(外部时钟)。
-
单元产生器:
- 可能是指控制单元,用于生成控制信号以管理数据传输。
-
接收器:
- 负责接收来自外部设备的数据。
-
接收移位器:
- 负责将接收到的串行数据按位顺序移入。
-
RXDn:
- 表示接收数据线,数据通过这条线接收到设备中。
-
接收保持寄存器:
- 在非FIFO模式下,接收保持寄存器用于存储刚刚接收到的字节。
-
接收缓冲寄存器:
- 这是一个64字节的缓冲区,用于存储接收到的数据。
-
接收FIFO寄存器:
- 在FIFO模式下,接收FIFO寄存器用于暂存接收到的数据。
-
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相关的软件。