UART:通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),简称串口。
调试:移植u-boot、内核时,主要使用串口查看打印信息
外接各种模块
UART的全称是Universal Asynchronous Receiver and Transmitter,即异步发送和接收。 串口在嵌入式中用途非常的广泛,主要的用途有:
打印调试信息;
外接各种模块:GPS、蓝牙;
串口因为结构简单、稳定可靠,广受欢迎。
通过三根线即可,发送、接收、地线。
TxD线把PC机要发送的信息发送给ARM开发板。 最下面的地线统一参考地。
波特率:一般选波特率都会有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来调试,其它串口来外接模块。
ARM芯片上得串口都是TTL电平的,通过板子上或者外接的电平转换芯片,转成RS232接口,连接到电脑的RS232串口上,实现两者的数据传输。
现在的电脑越来越少有RS232串口的接口,当USB是几乎都有的。因此使用USB串口芯片将ARM芯片上的TTL电平转换成USB串口协议,即可通过USB与电脑数据传输。
ARM芯片是如何发送/接收数据? 如图所示串口结构图:
要发送数据时,CPU控制内存要发送的数据通过FIFO传给UART单位,UART里面的移位器,依次将数据发送出去,在发送完成后产生中断提醒CPU传输完成。 接收数据时,获取接收引脚的电平,逐位放进接收移位器,再放入FIFO,写入内存。在接收完成后产生中断提醒CPU传输完成。
图片中描述的是串行通信中的一个典型的串行接口(Peripheral Bus)结构,特别是关于数据发送和接收的组件。以下是对图片内容的解释:
发送器(发送器):
发送FIFO寄存器:
发送缓冲寄存器:
发送保持寄存器:
发送移位器:
TXDn:
控制:
波特率:
时钟源:
单元产生器:
接收器:
接收移位器:
RXDn:
接收保持寄存器:
接收缓冲寄存器:
接收FIFO寄存器:
FIFO模式与非FIFO模式:
在串行通信中,数据通常通过发送器和接收器在设备之间传输。FIFO模式和非FIFO模式决定了数据如何被存储和检索。FIFO模式适用于数据传输速率较高且需要缓冲大量数据的情况,而非FIFO模式则适用于数据传输速率较低或不需要大量缓冲的情况。
在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
它要做的事情:
找到tty_driver
分配/设置tty_struct
行规程相关的初始化
串行通信中的"行规程":
在串行通信中,"行规程"特别指的是与串行数据传输相关的一系列设置,这些设置定义了数据如何被发送和接收。这包括:
在Linux和其他操作系统中,行规程可以通过设置串行端口的属性来配置,这些属性通常通过termios
结构体进行控制。
read过程分析:
流程为:
APP读
使用行规程来读
无数据则休眠
UART接收到数据,产生中断
中断程序从硬件上读入数据
发给行规程
行规程处理后存入buffer
行规程唤醒APP
APP被唤醒后,从行规程buffer中读入数据,返回
流程为:
APP写
使用行规程来写
数据最终存入uart_state->xmit的buffer里
硬件发送:怎么发送数据?
使用硬件驱动中uart_ops->start_tx开始发送
具体的发送方法有2种:通过DMA,或通过中断
中断方式
方法1:直接使能 tx empty中断,一开始tx buffer为空,在中断里填入数据
方法2:写部分数据到tx fifo,使能中断,剩下的数据再中断里继续发送
注册一个uart_driver:它里面有名字、主次设备号等
对于每一个port,调用uart_add_one_port,里面的核心是uart_ops,提供了硬件操作函数
uart_add_one_port由platform_driver的probe函数调用
所以:
编写设备树节点
注册platform_driver
#include // 模块化编程支持 #include // I/O端口支持 #include // 模块初始化和清理宏 #include // 控制台支持 #include // 系统请求键支持 #include // 平台设备支持 #include // TTY支持 #include // TTY翻转缓冲区支持 #include // 串行核心支持 #include // 串行硬件支持 #include // 时钟支持 #include // 延时支持 #include // 有理数支持 #include // 重置支持 #include // 内存分配 #include // 设备树支持 #include // 设备树设备支持 #include // IO操作 #include // DMA内存映射 #include // 进程文件系统 #include // 特定体系结构的中断支持 #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"); // 模块许可证
代码解释:
uart_driver
结构体,包含了驱动的名称、设备名称、设备号和设备数量。这个模块实现了一个虚拟的UART驱动程序,它可以模拟UART硬件的行为,对于嵌入式系统开发中的串行通信测试和调试非常有用。通过这种方式,开发者可以在没有实际硬件的情况下开发和测试UART相关的软件。