uart 子系统
串口硬件储备知识:
uart 在Linux 应用层的体现及使用
uart 就是串口,它也是属于字符设备中的一种,众所周知 字符设备都会在/dev/ 目录下创建节点,串口所创建的节点名都是以tty* 为开头,例如下面这些节点:
每一个串口设备都会创建一个/dev/tty* 文件节点。
注意:/dev/tty、/dev/tty0、/dev/tty1 等等节点不是串口。
要使用串口来收发数据,我们在应用层怎么访问串口呢?
既然串口是字符设备,那么就还是用字符设备的那一套老方法来访问串口,即:open、read、write、ioctl 等等。
如下是一个串口的使用例程,主要分为三步:
- 打开串口 (设置flag:读写权限、阻塞等等);
- 设置波特率、奇偶校验位、停止位等等;
- 读写串口数据
与其他字符设备略有不同的就是多了波特率、校验位等等协议相关的。它们可以通过一个struct termios
来设置。
它的定义如下,在内核中有一个struct ktermios
与之对应。
使用tcgetattr
函数获取termios 的原始值,根据应用需求配置后,再使用tcsetattr
函数设置新的termios。(tcgetattr 与tcsetattr 其实都是对于ioctl 的封装,它们最终会调用到tty层的file_operations->unlocked_ioctl 函数来设置termios)
#define NCCS 19
struct termios {tcflag_t c_iflag; /* input mode flags */tcflag_t c_oflag; /* output mode flags */tcflag_t c_cflag; /* control mode flags */tcflag_t c_lflag; /* local mode flags */cc_t c_cc[NCCS]; /* control characters */cc_t c_line; /* line discipline (== c_cc[19]) */speed_t c_ispeed; /* input speed */ //输入波特率speed_t c_ospeed; /* output speed */ //输出波特率
};
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <stdlib.h>/* set_opt(fd,115200,8,'N',1) */
int set_opt(int fd,int nSpeed, int nBits, char nEvent, int nStop)
{struct termios newtio,oldtio;if ( tcgetattr( fd,&oldtio) != 0) { perror("SetupSerial 1");return -1;}bzero( &newtio, sizeof( newtio ) );newtio.c_cflag |= CLOCAL | CREAD; newtio.c_cflag &= ~CSIZE; newtio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); /*Input*/newtio.c_oflag &= ~OPOST; /*Output*/switch( nBits ) //设置7、8位数据位{case 7:newtio.c_cflag |= CS7;break;case 8:newtio.c_cflag |= CS8;break;}switch( nEvent ) //设置奇、偶或无校验{case 'O':newtio.c_cflag |= PARENB;newtio.c_cflag |= PARODD;newtio.c_iflag |= (INPCK | ISTRIP);break;case 'E': newtio.c_iflag |= (INPCK | ISTRIP);newtio.c_cflag |= PARENB;newtio.c_cflag &= ~PARODD;break;case 'N': newtio.c_cflag &= ~PARENB;break;}switch( nSpeed ) //设置波特率{case 2400:cfsetispeed(&newtio, B2400);cfsetospeed(&newtio, B2400);break;case 4800:cfsetispeed(&newtio, B4800);cfsetospeed(&newtio, B4800);break;case 9600:cfsetispeed(&newtio, B9600);cfsetospeed(&newtio, B9600);break;case 115200:cfsetispeed(&newtio, B115200);cfsetospeed(&newtio, B115200);break;default:cfsetispeed(&newtio, B9600);cfsetospeed(&newtio, B9600);break;}if( nStop == 1 ) //设置多少位停止位newtio.c_cflag &= ~CSTOPB;else if ( nStop == 2 )newtio.c_cflag |= CSTOPB;newtio.c_cc[VMIN] = 1; /* 读数据时的最小字节数: 没读到这些数据我就不返回! */newtio.c_cc[VTIME] = 0; /* 等待第1个数据的时间: * 比如VMIN设为10表示至少读到10个数据才返回,* 但是没有数据总不能一直等吧? 可以设置VTIME(单位是10秒)* 假设VTIME=1,表示: * 10秒内一个数据都没有的话就返回* 如果10秒内至少读到了1个字节,那就继续等待,完全读到VMIN个数据再返回*//*tcflush函数刷清(扔掉)输入缓存(终端驱动法度已接管到,但用户法度尚未读)或输出缓存(用户法度已经写,但尚未发送).int tcflush(int filedes,int quene)quene数该当是下列三个常数之一:*TCIFLUSH 刷清输入队列*TCOFLUSH 刷清输出队列*TCIOFLUSH 刷清输入、输出队列例如:tcflush(fd,TCIFLUSH);在打开串口后,串口其实已经可以开始读取 数据了 ,这段时间用户如果没有读取,将保存在缓冲区里,如果用户不想要开始的一段数据,或者发现缓冲区数据有误,可以使用这个函数清空缓冲*/tcflush(fd,TCIFLUSH);if((tcsetattr(fd,TCSANOW,&newtio))!=0){perror("com set error");return -1;}//printf("set done!\n");return 0;
}int open_port(char *com)
{int fd;//fd = open(com, O_RDWR|O_NOCTTY|O_NDELAY);fd = open(com, O_RDWR|O_NOCTTY);if (-1 == fd){return(-1);}if(fcntl(fd, F_SETFL, 0)<0) /* 设置串口为阻塞状态*/{printf("fcntl failed!\n");return -1;}return fd;
}/** ./serial_send_recv <dev>*/
int main(int argc, char **argv)
{int fd;int iRet;char c;/* 1. open *//* 2. setup * 115200,8N1* RAW mode* return data immediately*//* 3. write and read */if (argc != 2){printf("Usage: \n");printf("%s </dev/ttySAC1 or other>\n", argv[0]);return -1;}fd = open_port(argv[1]);if (fd < 0){printf("open %s err!\n", argv[1]);return -1;}iRet = set_opt(fd, 115200, 8, 'N', 1);if (iRet){printf("set port err!\n");return -1;}printf("Enter a char: ");while (1){scanf("%c", &c);iRet = write(fd, &c, 1);iRet = read(fd, &c, 1);if (iRet == 1)printf("get: %02x %c\n", c, c);elseprintf("can not get data\n");}return 0;
}
串口驱动框架
回顾前面讲的一些特征:串口是一个字符设备、它会在/dev/ 路径下创建节点、它是使用文件IO 的形式来访问。
通过这些特征,我们就可以推测出驱动中必定要做的事情:注册字符设备 cdev、实现file_operations、创建设备节点。
那么串口驱动具体是如何完成这些操作的?我们带着疑问去看uart 驱动代码。
如上图,串口驱动主要分为三层:tty层、serial core层(串口核心层)和串口硬件层。
它们对应的内核源码中的位置分别是:
drivers/tty/tty_io.c
drivers/tty/serial/serial_core.c
drivers/tty/serial/imx.c(imx6ull)、drivers/tty/serial/8250/ (8250)
tty层: 向上对应用层提供统一的tty操作接口,比如/dev/tty、/dev/tty0、/dev/ttyS1 等等(支持tty 设备不止串口一种,还有其它硬件设备和虚拟tty设备)。Linux 支持多种tty设备,它们包括串口、显示屏和虚拟设备等等,tty层则保证了我们能在应用层用相同的方法来操作各种tty设备(向下层给出统一的tty设备注册方法)。
串口核心层: Linux串口驱动可以兼容不同厂家的串口,为了适配所有串口设备,利用分层思想把串口驱动分为核心层与硬件驱动层。
核心层主要是管理各种各样的串口驱动,向硬件驱动层给出统一的注册方法,从而统一不同串口设备的操作方法(向下层给出统一的串口设备注册方法);另外,向上层将串口设备注册成为一个tty设备。
串口硬件层: 各个厂家的设计的串口控制方法不同,所以需要由厂家编写自己的串口硬件控制驱动,构造并填充struct uart_driver
和struct uart_port
,最后调用串口核心层给出的注册接口注册。
注释:驱动实际上就是就是设置寄存器,串口硬件层就是设置寄存器。串口核心层与tty层都是与硬件无关的软件层。
串口驱动中的重要数据结构
uart_driver
每家串口设备都有自己的驱动,为了管理这些各种各样的驱动程序,串口核心层用一个uart_driver 来表示一种串口的驱动:所有的串口硬件层驱动都需要构造好一个uart_drver,并向串口核心层注册它。
比如imx6ull 的串口驱动imx.c 会向serial_core.c 注册一个uart_driver,表示imx6ull上串口的驱动;
8250 串口驱动8250_core.c 会向serial_core.c 注册一个uart_driver,表示8250 串口的驱动。
一个uart_driver 可以包含多个串口端口,每一个端口都会有一个struct uart_state和struct uart_port 与之对应,也就是一个uart_driver对应多个uart_state,多个uart_port。
struct uart_driver
中有一个*state 成员,在注册uart_driver 的过程中会根据nr的申请nr * sizeof(struct uart_state)
宽度的内存,并让state指向这段内存首地址,uart_state中包含端口对应的uart_port,三者以此保存联系。
struct uart_driver {struct module *owner;const char *driver_name; //驱动名/*设备名,比如imx6ull 的串口设备名是"ttymxc",8250串口设备名是"ttyS"。这个名字最后的就是/dev/ 目录下生成的串口节点名。*/const char *dev_name; int major; //主设备号int minor; //次设备号起始值int nr; //通常一个平台上会有多个串口,uart_driver 可以兼容多个端口,nr表示这个驱动可以支持多少个串口端口struct console *cons; //与console 有关,当一个串口被设置为console 的时会用到它/** these are private; the low level driver should not* touch these; they should be initialised to NULL*/struct uart_state *state; //每一个串口端口都会有一个uart_state 与之对应struct tty_driver *tty_driver; //在底层的硬件驱动中不需要初始化它,这是预留给tty层设置的
};
串口驱动中可以调用 uart_register_driver
函数来注册一个uart_driver。
uart_state
每一个串口端口都会有一个uart_state 结构体与之对应,在注册uart_driver 时 uart_register_driver函数会根据uart_driver 支持的串口个数申请多个struct uart_state。
其中包含的tty_port 和uart_port 是重点(uart_port 表示一个串口端口,tty_port 表示一个tty端口)。uart_state、uart_port、tty_port 三者关系是唯一对应的。
struct uart_state {struct tty_port port; //一个tty_port 对应一个uart_state 和一个uart_portenum uart_pm_state pm_state;struct circ_buf xmit;atomic_t refcount;wait_queue_head_t remove_wait;struct uart_port *uart_port; //一个uart_port 对应一个uart_state 和一个tty_port
};
uart_port
通常一个平台上会有多个的串口,比如imx6ull 上就有多个串口设备,它们都是同一种串口,因此可以归属于同一个uart_driver 来管理,每一个端口用一个uart_port 来描述。
uart_port 描述一个端口的各种信息,其中包含一些硬件信息,比如irq (中断号)、membase (寄存器地址范围)等等,其它。
这些硬件信息一般保存在dtb 里,在与platform_driver 匹配后会在probe中读取硬件信息填充到uart_port 中。
此外 uart_port 中还包含该串口的硬件操作函数集uart_port->ops (struc uart_ops),有了它就可以让串口工收发数据了。
//include/linux/serial_core.h
struct uart_port {spinlock_t lock; /* port lock */unsigned long iobase; /* in/out[bwl] */unsigned char __iomem *membase; /* read/write[bwl] */unsigned int (*serial_in)(struct uart_port *, int); //用于读取硬件寄存器void (*serial_out)(struct uart_port *, int, int); //用于写入硬件寄存器void (*set_termios)(struct uart_port *,struct ktermios *new,struct ktermios *old);void (*set_ldisc)(struct uart_port *,struct ktermios *);unsigned int (*get_mctrl)(struct uart_port *);void (*set_mctrl)(struct uart_port *, unsigned int);int (*startup)(struct uart_port *port);void (*shutdown)(struct uart_port *port);void (*throttle)(struct uart_port *port);void (*unthrottle)(struct uart_port *port);int (*handle_irq)(struct uart_port *);void (*pm)(struct uart_port *, unsigned int state,unsigned int old);void (*handle_break)(struct uart_port *);int (*rs485_config)(struct uart_port *,struct serial_rs485 *rs485);unsigned int irq; /* irq number */unsigned long irqflags; /* irq flags 中断标志,在request_irq 时需要作为参数传入*/unsigned int uartclk; /* base uart clock */unsigned int fifosize; /* tx fifo size 发送FIFO 大小*/unsigned char x_char; /* xon/xoff char */unsigned char regshift; /* reg offset shift */unsigned char iotype; /* io access style */unsigned char quirks; /* internal quirks 怪癖(表示该串口硬件独有的特性)*/...... /* 省略一些东西*/int hw_stopped; /* sw-assisted CTS flow state */unsigned int mctrl; /* current modem ctrl settings */unsigned int timeout; /* character-based timeout */unsigned int type; /* port type */const struct uart_ops *ops; /* 串口硬件操作函数,这个是最重要的,有了它就可以驱动串口 (由设备厂家编写的硬件驱动提供)*/ unsigned int custom_divisor;unsigned int line; /* port index 串口需要,当设备上有多个串口设备使用同一个驱动的话会用来标序,比如 ttyS1、ttyS2*/unsigned int minor;resource_size_t mapbase; /* for ioremap */resource_size_t mapsize;struct device *dev; /* parent device */unsigned char hub6; /* this should be in the 8250 driver */unsigned char suspended;unsigned char unused[2];const char *name; /* port name 串口名*/struct attribute_group *attr_group; /* port specific attributes */const struct attribute_group **tty_groups; /* all attributes (serial core use only) */struct serial_rs485 rs485;void *private_data; /* generic platform data pointer */ //私有数据
};
串口硬件操作函数 uart_ops
这个ops 是最重要的,它代表了串口硬件的操作方法,有了这个函数集我们就可以对串口发送数据,接收数据等等,由串口厂家编写。
这些函数就是最终读写寄存器,完成功能的函数了。
struct uart_ops {unsigned int (*tx_empty)(struct uart_port *); //判断串口发送fifo 是否为空(空:代表发送完成)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 (*throttle)(struct uart_port *);void (*unthrottle)(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);int (*startup)(struct uart_port *);void (*shutdown)(struct uart_port *);void (*flush_buffer)(struct uart_port *);void (*set_termios)(struct uart_port *, struct ktermios *new,struct ktermios *old);void (*set_ldisc)(struct uart_port *, struct ktermios *);void (*pm)(struct uart_port *, unsigned int state,unsigned int oldstate);/** Return a string describing the type of the port*/const char *(*type)(struct uart_port *);/** Release IO and memory resources used by the port.* This includes iounmap if necessary.*/void (*release_port)(struct uart_port *);/** Request IO and memory resources used by the port.* This includes iomapping the port if necessary.*/int (*request_port)(struct uart_port *);void (*config_port)(struct uart_port *, int);int (*verify_port)(struct uart_port *, struct serial_struct *);int (*ioctl)(struct uart_port *, unsigned int, unsigned long);
#ifdef CONFIG_CONSOLE_POLLint (*poll_init)(struct uart_port *);void (*poll_put_char)(struct uart_port *, unsigned char);int (*poll_get_char)(struct uart_port *);
#endif
};
tty_driver
tty_driver 表示一个tty 驱动,tty driver 可以支持多种硬件设备,如串口、显示屏等等。
以串口为例,在serial_core.c 中uart_register_driver
函数会根据被注册的uart_driver 实现一个tty_driver 并调用tty_register_driver 函数向tty 层注册。
一个uart_driver 对应一个tty_driver。
tty_driver 中的成员需要注意的是 cdevs(struct cdev)、ttys(struct tty_struct)、ports(struct tty_port)和termios(struct ktermios)这4个结构体的二级指针(二级指针用来指向一个指针数组的首地址)。
在创建tty_driver 的过程中,会根据uart_driver->nr (串口端口的数量) 申请多个结构体的指针:nr * sizeof(struct cdev*); nr* sizeof(struct tty_struct*); nr* sizeof(struct tty_port*); nr* sizeof(stuct ktermios*);
并让二级指针指向它们的首地址。(只是申请了指针内存,并未申请实际结构体的内存)
cdev 代表着字符设备,每一个字符设备都会有一个struct cdev,在调用tty_port_register_device_attr
注册一个tty_port 时会为这个tty_port 创建cdev,并按照端口序号放入指针数组对应的位置。(ttyS0、ttyS1 … 每一个都是一个字符设备,它们都有一个唯一的cdev 和次设备号,因为属于同一个uart_driver 的关系它们有相同的主设备号)
tty_struct 是操作串口过程中比较重要的数据结构,它会在open ttyxx 的时候为对应的端口(tty_port) 申请一个tty_struct 内存,按序号放入指针数组对应的位置。(创建的同时会初始化tty_struct,让tty_struct->ops(const struct tty_operations *) 指向tty_driver->ops,之后就可以用tty_struct 调用到struct tty_operations 操作集)
tty_port 表示一个tty端口,在调用tty_port_register_device_attr
注册tty_port 时会将该tty_port 地址按端口序号放入数组。
ktermios 表示一个终端设备,每个tty端口对应一个,在open 过程中会每个端口创建struct ktermios并初始化它(波特率等等),按次序放入指针数组。
struct tty_driver {int magic; /* magic number for this structure */struct kref kref; /* Reference management */struct cdev **cdevs;struct module *owner;const char *driver_name;const char *name;int name_base; /* offset of printed name */int major; /* major device number */int minor_start; /* start of minor device number */unsigned int num; /* number of devices allocated */short type; /* type of tty driver */short subtype; /* subtype of tty driver */struct ktermios init_termios; /* Initial termios */unsigned long flags; /* tty driver flags */struct proc_dir_entry *proc_entry; /* /proc fs entry */struct tty_driver *other; /* only used for the PTY driver *//** Pointer to the tty data structures*/struct tty_struct **ttys;struct tty_port **ports;struct ktermios **termios;void *driver_state; //指向下层的driver结构体,比如串口就是 uart_driver (为了绑定uart_driver 和tty_driver一对一的关系)/** Driver methods 驱动方法*/const struct tty_operations *ops;struct list_head tty_drivers;
} __randomize_layout;
tty_port
tty_port 表示一个tty 端口。如果tty设备是串口的话,那么一个tty_port 对应一个uart_port。
它的成员tty_port->ops (struct tty_port_operations) 是比较重要的,在open过程中会调用到。
struct tty_port {struct tty_bufhead buf; /* Locked internally */struct tty_struct *tty; /* Back pointer */struct tty_struct *itty; /* internal back ptr */const struct tty_port_operations *ops; /* Port operations */const struct tty_port_client_operations *client_ops; /* Port client operations */spinlock_t lock; /* Lock protecting tty field */int blocked_open; /* Waiting to open */int count; /* Usage count */wait_queue_head_t open_wait; /* Open waiters */wait_queue_head_t delta_msr_wait; /* Modem status change */unsigned long flags; /* User TTY flags ASYNC_ */unsigned long iflags; /* Internal flags TTY_PORT_ */unsigned char console:1, /* port is a console */low_latency:1; /* optional: tune for latency */struct mutex mutex; /* Locking */struct mutex buf_mutex; /* Buffer alloc lock */unsigned char *xmit_buf; /* Optional buffer */unsigned int close_delay; /* Close port delay */unsigned int closing_wait; /* Delay for output */int drain_delay; /* Set to zero if no pure timebased drain is needed elseset to size of fifo */struct kref kref; /* Ref counter */void *client_data;
};
tty_struct
一个tty_struct 对应一个tty_port,里面包含端口拥有的读写缓冲区等一些重要数据。
struct tty_struct {int magic;struct kref kref;struct device *dev;struct tty_driver *driver;const struct tty_operations *ops;int index;/* Protects ldisc changes: Lock tty not pty */struct ld_semaphore ldisc_sem;struct tty_ldisc *ldisc;struct mutex atomic_write_lock;struct mutex legacy_mutex;struct mutex throttle_mutex;struct rw_semaphore termios_rwsem;struct mutex winsize_mutex;spinlock_t ctrl_lock;spinlock_t flow_lock;/* Termios values are protected by the termios rwsem */struct ktermios termios, termios_locked;......struct tty_struct *link;struct fasync_struct *fasync;int alt_speed; /* For magic substitution of 38400 bps */wait_queue_head_t write_wait;wait_queue_head_t read_wait;struct work_struct hangup_work;void *disc_data;void *driver_data;struct list_head tty_files;#define N_TTY_BUF_SIZE 4096int closing;unsigned char *write_buf;int write_cnt;/* If the tty has a pending do_SAK, queue it here - akpm */struct work_struct SAK_work;struct tty_port *port;
};
ktermios
这个就是我们在应用层初始化串口时要设置的波特率、停止位、校验位等等,都在ktermis 中。
struct ktermios {tcflag_t c_iflag; /* input mode flags */tcflag_t c_oflag; /* output mode flags */tcflag_t c_cflag; /* control mode flags */tcflag_t c_lflag; /* local mode flags */cc_t c_line; /* line discipline */cc_t c_cc[NCCS]; /* control characters */speed_t c_ispeed; /* input speed */speed_t c_ospeed; /* output speed */
};
tty_operations
除了以上的还有两个ops 是比较重要的,在open 过程中都会调用到,分别是 tty_driver->ops (struct tty_operations) 和tty_port->ops (struct tty_port_operations)
在uart_register_driver
中会创建tty_driver 并初始化,包括tty_driver->ops;在第一次open设备文件时会创建并初始化tty_struct,并将tty_driver->ops 赋值给tty_struct->ops。
操作串口时从上到下整个ops调用过程如下:
open (应用层)
-》struct file_operations //tty层注册cdev时设置
-》tty_struct->ops (struct tty_operations) //类型由tty层定义,实例由具体的设备驱动提供,如串口就有serial_core.c 提供实例uart_ops (变量名)
-》tty_port->ops (struct tty_port_operations) //类型由tty层定义,实例同样由具体的设备驱动提供,串口由serial_core.c 提供 uart_port_ops
-》uart_state->ops (struct uart_ops) //类型由串口核心层定义,实例由各自的串口厂商驱动提供,如imx.c 的imx_uart_pops
struct tty_operations {struct tty_struct * (*lookup)(struct tty_driver *driver, //查找tty_struct,对于串口来说此函数不提供struct inode *inode, int idx);int (*install)(struct tty_driver *driver, struct tty_struct *tty); //用于将tty_struct 安装到tty_driver->ttys[]void (*remove)(struct tty_driver *driver, struct tty_struct *tty); int (*open)(struct tty_struct * tty, struct file * filp);void (*close)(struct tty_struct * tty, struct file * filp);void (*shutdown)(struct tty_struct *tty);void (*cleanup)(struct tty_struct *tty);int (*write)(struct tty_struct * tty,const unsigned char *buf, int count);int (*put_char)(struct tty_struct *tty, unsigned char ch);void (*flush_chars)(struct tty_struct *tty);int (*write_room)(struct tty_struct *tty);int (*chars_in_buffer)(struct tty_struct *tty);int (*ioctl)(struct tty_struct *tty,unsigned int cmd, unsigned long arg);long (*compat_ioctl)(struct tty_struct *tty,unsigned int cmd, unsigned long arg);void (*set_termios)(struct tty_struct *tty, struct ktermios * old);void (*throttle)(struct tty_struct * tty);void (*unthrottle)(struct tty_struct * tty);void (*stop)(struct tty_struct *tty);void (*start)(struct tty_struct *tty);void (*hangup)(struct tty_struct *tty);int (*break_ctl)(struct tty_struct *tty, int state);void (*flush_buffer)(struct tty_struct *tty);void (*set_ldisc)(struct tty_struct *tty);void (*wait_until_sent)(struct tty_struct *tty, int timeout);void (*send_xchar)(struct tty_struct *tty, char ch);int (*tiocmget)(struct tty_struct *tty);int (*tiocmset)(struct tty_struct *tty,unsigned int set, unsigned int clear);int (*resize)(struct tty_struct *tty, struct winsize *ws);int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);int (*get_icount)(struct tty_struct *tty,struct serial_icounter_struct *icount);
#ifdef CONFIG_CONSOLE_POLLint (*poll_init)(struct tty_driver *driver, int line, char *options);int (*poll_get_char)(struct tty_driver *driver, int line);void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endifconst struct file_operations *proc_fops;
};
serial_core.c 提供的tty_operations 实例:
static const struct tty_operations uart_ops = {.install = uart_install,.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,.send_xchar = uart_send_xchar,.set_termios = uart_set_termios,.set_ldisc = uart_set_ldisc,.stop = uart_stop,.start = uart_start,.hangup = uart_hangup,.break_ctl = uart_break_ctl,.wait_until_sent= uart_wait_until_sent,
#ifdef CONFIG_PROC_FS.proc_show = uart_proc_show,
#endif.tiocmget = uart_tiocmget,.tiocmset = uart_tiocmset,.set_serial = uart_set_info_user,.get_serial = uart_get_info_user,.get_icount = uart_get_icount,
#ifdef CONFIG_CONSOLE_POLL.poll_init = uart_poll_init,.poll_get_char = uart_poll_get_char,.poll_put_char = uart_poll_put_char,
#endif
};
tty_port_operations
struct tty_port_operations {/* Return 1 if the carrier is raised */int (*carrier_raised)(struct tty_port *port);/* Control the DTR line */void (*dtr_rts)(struct tty_port *port, int raise);/* Called when the last close completes or a hangup finishesIFF the port was initialized. Do not use to free resources. Calledunder the port mutex to serialize against activate/shutdowns */void (*shutdown)(struct tty_port *port);/* Called under the port mutex from tty_port_open, serialized usingthe port mutex *//* FIXME: long term getting the tty argument *out* of this would begood for consoles */int (*activate)(struct tty_port *port, struct tty_struct *tty);/* Called on the final put of a port */void (*destruct)(struct tty_port *port);
};
serial_core.c 提供的struct tty_port_operations 实例:
static const struct tty_port_operations uart_port_ops = {.carrier_raised = uart_carrier_raised,.dtr_rts = uart_dtr_rts,.activate = uart_port_activate,.shutdown = uart_tty_port_shutdown,
};
uart 情景分析——注册
参考driver/tty/serial/imx.c 串口驱动,从最底层到tty 层来查看一个串口设备的注册流程。
去除一些复杂的硬件设置代码,只看与uart框架相关的:
-
注册一个uart_driver
imx_serial_init(驱动入口,只会在加载时调用一次)中调用uart_register_driver(&imx_reg),注册一个uart_driver;
uart_driver 中指定了驱动名、设备名、主设备号、次设备号起始、console(如果串口是一个console的话会用到这个结构体)。
(先忽略uart_register_driver 是怎么注册,那是serial_core.c 中的内容) -
向uart_driver 添加一个uart_port
imx_serial_init 中调用platform_driver_register() 注册一个platform_driver,并且用一个设备树节点来描述一个串口端口(包括串口硬件信息与 支持该串口的驱动的compatible),每当一个节点compatible 值与其platform_driver匹配时就会进入probe 函数。
probe 函数主要做哪些工作呢?读取设备树中的硬件信息,比如irq、reg资源等等,然后注册这些资源(关于硬件代码不详细赘述)。
还有一个最重要的就是填充uart_port (其中uart_ops 是最重要的,它是最底层、直接操作寄存器的ops),然后向uart_driver 添加uart_port。
简单看完了imx.c 的代码,其中主要做了两件事: -
注册一个 uart_driver:uart_register_driver(&imx_reg)。
-
为每个串口添加一个uart_port : uart_add_one_port(&imx_reg, &sport->port)。
接下来看看这两个结构体是怎么向上注册的,所以我们来看一下uart_register_driver、uart_add_one_port 这两个函数是如何实现的。
uart_register_driver
uart_register_driver 主要做了哪些事情:
申请与uart_driver 所支持串口数量相等的 uart_state 内存。(这里说明每一个串口端口都会对应一个uart_state)
调用alloc_tty_driver 申请一个tty_driver, 看看alloc_tty_driver 里面做了什么:
申请了一个tty_driver 内存,填充tty_driver中num = lines (lines 就是uart_driver->nr 赋值给它的)、owner、flags 等成员。
申请了与串口数量相等的 *ttys、*termios、*ports、*cdevs 一级指针。在tty_driver 中ttys、termios、ports、cdevs 都是二级指针类型,他们可以用来指向一个指针数组,申请到的指针存在这个数组中。
这里终于发现了与字符设备有关的cdev,但它只是指针,真正的cdev 内存会在哪里申请呢。
driver->ttys = kcalloc(lines, sizeof(*driver->ttys),GFP_KERNEL);
driver->termios = kcalloc(lines, sizeof(*driver->termios),GFP_KERNEL);
driver->ports = kcalloc(lines, sizeof(*driver->ports),GFP_KERNEL);
driver->cdevs = kcalloc(cdevs, sizeof(*driver->cdevs), GFP_KERNEL);
继续回到uart_register_driver
设置uart_driver->tty_driver = normal; normal->driver_state = drv;
uart_driver 与tty_driver 是一对一的,这里绑定它们的对应关系。拥有uart_driver 就可以找到tty_driver,反之亦然。
填充tty_driver 的driver_name、name(/dev/ 下的节点名就来自它)、major、minor_start(这些信息直接从uart_driver 照搬过来)、init_termios(初始波特率的值等等)、flags 等成员。
设置tty_driver->ops (struct tty_operations,在调用open、read、write 时会用到这个ops)。
初始化uart_state->tty_port,初始化tty_port buffer、等待队列、mutex、spinlock、tty_port->ops 和tty_port->client_ops (两个ops在open、read、write的过程中都会调用到)。
最重要的,在tty_driver 填充完毕后 调用tty_register_driver() 注册tty_driver。
现在我们知道调用uart_register_driver 时 会以被注册的 uart_driver 为基础生成一个tty_driver , 填充tty_driver 中的 各种信息,同时申请与串口等数量的tty_struct、ktermios、tty_port、cdev 指针,初始化uart_state 以及uart_state->port (tty_port) 最后调用tty_register_driver 注册tty_driver。
//drivers\tty\serial\serial_core.cint uart_register_driver(struct uart_driver *drv)
{struct tty_driver *normal;int i, retval;BUG_ON(drv->state);/** Maybe we should be using a slab cache for this, especially if* we have a large number of ports to handle.*///drv->nr 表示该uart_driver 能支持多少个串口,在前面注册时就已经初始化好了nr = ARRAY_SIZE(imx_ports)。//申请与drv->nr 相等的uart_driver->uart_state 内存。drv->state = kzalloc(sizeof(struct uart_state) * drv->nr, GFP_KERNEL);if (!drv->state)goto out;//申请一个tty_driver 内存 normal = alloc_tty_driver(drv->nr);if (!normal)goto out_kfree;//赋值uart_driver->tty_driver,绑定tty_driver 与uart_driver 之间的关系。drv->tty_driver = normal;//把设备名、驱动名,主次设备号等信息,从uart_driver 照搬过来到tty_driver 上。normal->driver_name = drv->driver_name;normal->name = drv->dev_name;normal->major = drv->major;normal->minor_start = drv->minor;//填充tty_driver 中的其它信息normal->type = TTY_DRIVER_TYPE_SERIAL;normal->subtype = SERIAL_TYPE_NORMAL;normal->init_termios = tty_std_termios; //初始的termiosnormal->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL; //设置串口初始的波特率等等。normal->init_termios.c_ispeed = normal->init_termios.c_ospeed = 9600;normal->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV;//将要注册的uart_driver 填充到tty_driver->driver_state。绑定tty_driver 与uart_driver 之间的关系 normal->driver_state = drv;//设置tty_driver->ops tty_set_operations(normal, &uart_ops);/** Initialise the UART state(s).*///uart_driver 所支持的每一个串口都对应一个uart_state,初始化每一个uart_driver->statefor (i = 0; i < drv->nr; i++) {struct uart_state *state = drv->state + i;struct tty_port *port = &state->port;//初始化uart_state->tty_porttty_port_init(port);port->ops = &uart_port_ops; //设置tty_port_operations}//注册一个tty_driverretval = tty_register_driver(normal);if (retval >= 0)return retval;//这里又把uart_state->tty_port 销毁了,可能时在tty_register_driver 已经利用完了state->tty_portfor (i = 0; i < drv->nr; i++)tty_port_destroy(&drv->state[i].port);put_tty_driver(normal);
out_kfree:kfree(drv->state);
out:return -ENOMEM;
}
#define tty_alloc_driver(lines, flags) \__tty_alloc_driver(lines, THIS_MODULE, flags)static inline struct tty_driver *alloc_tty_driver(unsigned int lines)
{struct tty_driver *ret = tty_alloc_driver(lines, 0);if (IS_ERR(ret))return NULL;return ret;
}struct tty_driver *__tty_alloc_driver(unsigned int lines, struct module *owner,unsigned long flags)
{struct tty_driver *driver;unsigned int cdevs = 1;int err;if (!lines || (flags & TTY_DRIVER_UNNUMBERED_NODE && lines > 1))return ERR_PTR(-EINVAL);//申请一个tty_driver 内存driver = kzalloc(sizeof(struct tty_driver), GFP_KERNEL); if (!driver)return ERR_PTR(-ENOMEM);kref_init(&driver->kref);driver->magic = TTY_DRIVER_MAGIC;/*lines:表示一个tty_driver 能支持多少个串口,这里传入的参数就是uart_driver->nr*/driver->num = lines; driver->owner = owner;driver->flags = flags;if (!(flags & TTY_DRIVER_DEVPTS_MEM)) {//申请与uart_driver->nr 数量相等的tty_struct指针driver->ttys = kcalloc(lines, sizeof(*driver->ttys), GFP_KERNEL);//申请与uart_driver->nr 数量相等的ktermios 指针driver->termios = kcalloc(lines, sizeof(*driver->termios),GFP_KERNEL);if (!driver->ttys || !driver->termios) {err = -ENOMEM;goto err_free_all;}}if (!(flags & TTY_DRIVER_DYNAMIC_ALLOC)) {//申请与uart_driver->nr 数量相等的tty_port 指针driver->ports = kcalloc(lines, sizeof(*driver->ports),GFP_KERNEL);if (!driver->ports) {err = -ENOMEM;goto err_free_all;}cdevs = lines;}//申请与uart_driver->nr 数量相等的cdev 指针driver->cdevs = kcalloc(cdevs, sizeof(*driver->cdevs), GFP_KERNEL);if (!driver->cdevs) {err = -ENOMEM;goto err_free_all;}return driver;
err_free_all:kfree(driver->ports);kfree(driver->ttys);kfree(driver->termios);kfree(driver);return ERR_PTR(err);
}
uart_add_one_port
接下去再来看看 uart_add_one_port
函数中具体做了什么:
state = drv->state + uport->line; 从uart_driver->state[] 中找到与端口对应的uart_state。(在 uart_register_driver 中申请了多个uart_state,每个串口对应一个state,可以通过串口的序号找到对应的 state地址)
将uart_state 与uart_port 互相绑定,这就完成了把uart_port 添加到uart_driver。
从uart_state 中获取到tty_port。
另外还需要设置uart_port 中一些其它重要信息,比如minor、name、struct console等等,它们都是在定义uart_driver 时初始化好的,需要把它们照搬过来。
设置uart_port->cons (struct console) ,如果该串口被设为console ,那么它是有用的。
设置串口对应的次设备号,每一个串口都有一个唯一的次设备号,每个端口的次设备号根据起始次设备号(minor_base) + 端口序号(line) 获得,在应用层也可以看到各个端口的次设备号是依序递增的。
设置串口名,设备名也是根据端口的序号 和 驱动设备名组合而来。
其它有关console 的设置,如果端口不是console的话,这些没有意义。端口被设为console时 tty_port->console == 1,否则为0。
最后调用tty_port_register_device_attr_serdev 注册tty_port 与tty_groups。
uart_port->tty_groups (const struct attribute_group **) 它是一个二级指针,在下面的代码中根据num_groups 的值申请了多个struct attribute_group * 指针内存,并设置uart_port->tty_groups,serial_core.c 中默认提供一个struct attribute_group 为tty_dev_attr_group,如果硬件层驱动有提供的话会注册两个 (imx.c 中没有提供)。
总结:uart_add_one_port 的主要做了以下三件事
1、将uart_port 填充到uart_driver 中端口对应的state,uart_state->uart_port,从而绑定uart_driver、uart_state、uart_port 三者关系,把uart_port 添加到uart_driver。 其实是4者绑定,uart_state 与tty_port 是绑定的。
2、uart_port 中console、minor、name等成员的设置,这些都是在创建uart_driver 时初始化好的,需要从uart_driver中赋值过去。其它console 的设置。
3、设置uart_port->tty_groups,调用tty_port_register_device_attr_serdev 注册uart_state->tty_port 和uart_port->tty_groups。
int uart_add_one_port(struct uart_driver *drv, struct uart_port *uport)
{struct uart_state *state;struct tty_port *port;int ret = 0;struct device *tty_dev;int num_groups;BUG_ON(in_interrupt());if (uport->line >= drv->nr) //如果串口的序号>= uart_driver支持的串口数量,就返回失败return -EINVAL;state = drv->state + uport->line; //从uart_driver 中获取uart_state,line对应的就是串口的序号port = &state->port; //从uart_state 中拿到tty_portmutex_lock(&port_mutex);mutex_lock(&port->mutex);if (state->uart_port) {ret = -EINVAL;goto out;}/* Link the port to the driver state table and vice versa */atomic_set(&state->refcount, 1);init_waitqueue_head(&state->remove_wait);state->uart_port = uport; //一对一绑定uart_state 和uart_portuport->state = state;state->pm_state = UART_PM_STATE_UNDEFINED;uport->cons = drv->cons; //设置uart_port->consoleuport->minor = drv->tty_driver->minor_start + uport->line; //设置次设备号uport->name = kasprintf(GFP_KERNEL, "%s%d", drv->dev_name, //设置串口名字drv->tty_driver->name_base + uport->line);if (!uport->name) {ret = -ENOMEM;goto out;}/** If this port is a console, then the spinlock is already* initialised.*/if (!(uart_console(uport) && (uport->cons->flags & CON_ENABLED))) {spin_lock_init(&uport->lock);lockdep_set_class(&uport->lock, &port_lock_key);}if (uport->cons && uport->dev)of_console_check(uport->dev->of_node, uport->cons->name, uport->line);tty_port_link_device(port, drv->tty_driver, uport->line); //将uart_state中的tty_port按次序赋值给 tty_driver->ports[index]uart_configure_port(drv, state, uport);port->console = uart_console(uport);num_groups = 2;if (uport->attr_group)num_groups++;uport->tty_groups = kcalloc(num_groups, sizeof(*uport->tty_groups),GFP_KERNEL);if (!uport->tty_groups) {ret = -ENOMEM;goto out;}uport->tty_groups[0] = &tty_dev_attr_group;if (uport->attr_group)uport->tty_groups[1] = uport->attr_group;/** Register the port whether it's detected or not. This allows* setserial to be used to alter this port's parameters.*/tty_dev = tty_port_register_device_attr_serdev(port, drv->tty_driver, //注册一个tty_portuport->line, uport->dev, port, uport->tty_groups);if (!IS_ERR(tty_dev)) {device_set_wakeup_capable(tty_dev, 1);} else {dev_err(uport->dev, "Cannot register tty device on line %d\n",uport->line);}/** Ensure UPF_DEAD is not set.*/uport->flags &= ~UPF_DEAD;out:mutex_unlock(&port->mutex);mutex_unlock(&port_mutex);return ret;
}
attribute_group
tty_port_register_device_attr_serdev 不仅注册了tty_port,还有uart_port->tty_groups.
那么tty_group到底是个啥,它其实是struct attribute_group 类型的,可以包含一组的 struct attribute。
struct attribute_group {const char *name;umode_t (*is_visible)(struct kobject *,struct attribute *, int);umode_t (*is_bin_visible)(struct kobject *,struct bin_attribute *, int);struct attribute **attrs;struct bin_attribute **bin_attrs;
};
那么struct attribute又是啥? 它可以用来描述一个属性。使用device_create_file 注册一个attribute 可以在/sys/class 目录下创建一个属性文件。
而tty 中的tty_port_register_device_attr_serdev 注册 attribute_group 就可以注册一组的struct attribute,创建一组属性文件。
查看/sys/class/tty/ttyS1 下的文件,正如代码中所见,有type、line、irq 等等文件,与上图tty_dev_attrs[] 中的各个属性一一对应,这样我们就可以在应用层查看串口的各种属性信息。
tty_port_register_device_attr_serdev
看看tty_port_register_device_attr_serdev 是如何注册attribute_group 和tty_port ,其它还做了些什么。
struct device *tty_port_register_device_attr_serdev(struct tty_port *port,struct tty_driver *driver, unsigned index,struct device *device, void *drvdata,const struct attribute_group **attr_grp)
{struct device *dev;tty_port_link_device(port, driver, index);dev = serdev_tty_port_register(port, device, driver, index);if (PTR_ERR(dev) != -ENODEV) {/* Skip creating cdev if we registered a serdev device */return dev;}return tty_register_device_attr(driver, index, device, drvdata,attr_grp);
}
tty_port_link_device 将uart_state->port 赋值给tty_driver->ports[index],其实就是将tty_port 安装到tty_driver 上,完成了tty_port 添加到tty_driver 的操作。
void tty_port_link_device(struct tty_port *port,struct tty_driver *driver, unsigned index)
{if (WARN_ON(index >= driver->num))return;driver->ports[index] = port;
}
在serdev_tty_port_register 函数中主要是添加了一个struct serdev_controller
,以及设置tty_port->client_ops和port->client_data = ctrl。serdev_controller 不知道干啥用的,先放着不管。重点是client_ops,在读取数据的过程中会用到它。
struct device *serdev_tty_port_register(struct tty_port *port,struct device *parent,struct tty_driver *drv, int idx)
{struct serdev_controller *ctrl;ctrl = serdev_controller_alloc(parent, sizeof(struct serport));port->client_ops = &client_ops;port->client_data = ctrl;ret = serdev_controller_add(ctrl);
}
重点在tty_register_device_attr
函数中:
tty_register_device_attr 主要分为两部分
- 创建、初始化一个struct device,向内核注册struct device
具体内容如下:
dev_t devt = MKDEV(driver->major, driver->minor_start) + index; 根据major 和minor 创建出一个设备号。
创建一个 struct device。
初始化struct device:填充设备号、类(tty_class,所有的tty设备都用的同一个类)、parent(platfrom_device->dev)、name(这个名字来自tty_driver->name_base + index,它就是/dev/ 目录下生成的节点名)、groups (struct attribute_group 它就是uart_add_one_port 中添加的一组属性文件)、drvdata(drvdata 设置为tty_port)、release (tty_device_create_release 是释放struct device的回调函数)。
最后注册struct device,注册struct device 这个结构体就会在/dev/ 下创建一个文件节点,device_register的实现是调用了device_add()。
设备节点与 属性文件的创建
问:调用 device_register() 函数会发生什么?
在 /dev/ 目录下创建设备节点。
在 /sys/class/xxx 目录下创建 attribute 属性文件。
我们在编写普通的字符设备驱动时也可以在/dev/ 目录中创建设备节点,它是如何创建的?调用了device_create()
函数。
它也是创建一个struct device,然后填充其中的信息,最终调用device_add 向内核注册,与这里的代码几乎一摸一样。(不同的是,create_device 没有传递attribute_group,不能用它来创建一些属性文件)
device_create
-> device_create_vargs
-> device_create_groups_vargs
通过这两段代码我们可以知道,创建并初始一个struct device,调用device_add 向内核注册struct device,就可以创建一个/dev/xxx 设备节点,如果你设置了device->groups 还可以创建一组属性文件。
tty_register_device_attr
前面半段的代码主要用于注册struct device,后面半段则是注册 struct cdev。
retval = tty_cdev_add(driver, devt, index, 1);
创建cdev,安装到tty_driver->cdevs[index] 数组对应的位置中。
填充cdev,包括最重要的struct file_operations,调用cdev_add 向内核注册cdev。
tty_register_device_attr 中做的两件事:1、注册struct device 会创建设备文件;2、注册struct cdev,cdev中包含file_operations。有了设备节点和cdev,就可以用文件IO 打开/dev/ 节点来访问tty 层的file_operations 了。
struct device *tty_register_device_attr(struct tty_driver *driver,unsigned index, struct device *device,void *drvdata,const struct attribute_group **attr_grp)
{char name[64];dev_t devt = MKDEV(driver->major, driver->minor_start) + index; //创建设备号struct ktermios *tp;struct device *dev;int retval;if (index >= driver->num) {pr_err("%s: Attempt to register invalid tty line number (%d)\n",driver->name, index);return ERR_PTR(-EINVAL);}if (driver->type == TTY_DRIVER_TYPE_PTY)pty_line_name(driver, index, name);elsetty_line_name(driver, index, name);dev = kzalloc(sizeof(*dev), GFP_KERNEL); //创建struct deviceif (!dev)return ERR_PTR(-ENOMEM);dev->devt = devt; //设置设备号dev->class = tty_class; //类dev->parent = device; //父设备dev->release = tty_device_create_release;dev_set_name(dev, "%s", name); //设置设备名dev->groups = attr_grp;dev_set_drvdata(dev, drvdata);dev_set_uevent_suppress(dev, 1);retval = device_register(dev); //注册deviceif (retval)goto err_put;if (!(driver->flags & TTY_DRIVER_DYNAMIC_ALLOC)) {/** Free any saved termios data so that the termios state is* reset when reusing a minor number.*/tp = = driver->termios[index];if (tp) {driver->termios[index] = NULL;kfree(tp);}retval = tty_cdev_add(driver, devt, index, 1); //这里非常关键,会创建cdev 并向内核注册cdevif (retval)goto err_del;}dev_set_uevent_suppress(dev, 0);kobject_uevent(&dev->kobj, KOBJ_ADD);return dev;err_del:device_del(dev);
err_put:put_device(dev);return ERR_PTR(retval);
}
uart 情景分析:open
在上面注册过程中,已经为每个串口注册好了struct device 和cdev,这样我们就可以通过open(“/dev/ttyS1”,XXX) 来打开串口,看看调用open打开串口设备时会发生什么。
首先,调用uart_add_one_port添加uart_port 时会为每个串口创建一个cdev,应用层调用open时自然会调用到cdev->file_operations,所以先从tty_fops->open 开始看起。
static int tty_open(struct inode *inode, struct file *filp)
{struct tty_struct *tty;dev_t device = inode->i_rdev; //inode->i_rdev 记录着设备号tty = tty_open_current_tty(device, filp); //返回NULL,所以会走if 分支if (!tty)//通过设备号查找tty_driver,并根据tty_driver创建一个tty_struct、初始化tty_struct (每个串口端口第一次打开的时候都会创建一个属于自己的tty_struct)tty = tty_open_by_driver(device, inode, filp); if (tty->ops->open) //调用tty_struct->ops->open,这里的ops 就是tty_driver->opsretval = tty->ops->open(tty, filp);......
}
tty_fops->open 即tty_open。
第一步:先通过inode 从其中获取设备号。在拥有多个次设备的驱动里,minor = minor_base + index,从minor 可以推出index,有了index 就可以找到其它与该端口对应的设备数据结构。
二:tty_open 要做的第二件事就是找到tty_struct,在前面分析的uart_register_driver > alloc_tty_driver 中申请tty_driver 时会为每个串口申请一个tty_struct 指针 (并没有为tty_struct 申请内存)。
首先调用tty_open_current_tty 来获取当前串口的tty_struct,tty_open_current_tty 只允许major=TTYAUX_MAJOR(5)、minor=0 的设备使用,所以这里返回NULL,进入if 分支调用tty_open_by_driver 来找到tty_struct。
查看tty_open_by_driver
函数,该函数的目的也是为了查找到tty_struct,事实上它是创建了一个新的tty_struct:
static struct tty_struct *tty_open_by_driver(dev_t device, struct inode *inode,struct file *filp)
{struct tty_struct *tty;struct tty_driver *driver = NULL;int index = -1;int retval;driver = tty_lookup_driver(device, filp, &index); //查找tty_driver,得到当前端口序号/* check whether we're reopening an existing tty */tty = tty_driver_lookup_tty(driver, filp, index); //获取tty_struct,串口在注册时并没有为每一个串口创建tty_struct,这里tty 返回的应该是个空指针if (IS_ERR(tty)) {mutex_unlock(&tty_mutex);goto out;}if (tty) {if (tty_port_kopened(tty->port)) {tty_kref_put(tty);mutex_unlock(&tty_mutex);tty = ERR_PTR(-EBUSY);goto out;}mutex_unlock(&tty_mutex);retval = tty_lock_interruptible(tty);tty_kref_put(tty); /* drop kref from tty_driver_lookup_tty() */if (retval) {if (retval == -EINTR)retval = -ERESTARTSYS;tty = ERR_PTR(retval);goto out;}retval = tty_reopen(tty);if (retval < 0) {tty_unlock(tty);tty = ERR_PTR(retval);}} else { /* Returns with the tty_lock held for now *///创建并初始化tty_struct:初始化tty_struct->termios, 和tty_struct 与tty_driver、uart_state、tty_port 的关系绑定,将tty_struct->ops设置为tty_driver->opstty = tty_init_dev(driver, index); mutex_unlock(&tty_mutex);}
out:tty_driver_kref_put(driver);return tty;
}
调用tty_lookup_driver 查找与uart_driver 对应的tty_driver。(想要找到tty_struct,首先得找到tty_driver)
其实内核中是有许多个tty_driver 的,比如不同厂家的串口、或者其它tty 设备,它们都会导致一个新的tty_driver 被注册,在注册时会将它们添加入一个链表,我们可以通过打开端口时获取到的设备号 遍历链表来找到该端口所属的那个tty_driver。
细看一下tty_lookup_driver
:
查看imx.c 和8250_core.c 它们的主次设备号都不与前两个分支匹配,所以串口应该会走default 分支,调用get_tty_driver 来查找tty_driver。
static struct tty_driver *tty_lookup_driver(dev_t device, struct file *filp,int *index)
{struct tty_driver *driver = NULL;switch (device) { //根据设备号查找tty_driver
#ifdef CONFIG_VTcase MKDEV(TTY_MAJOR, 0): {extern struct tty_driver *console_driver;driver = tty_driver_kref_get(console_driver);*index = fg_console;break;}
#endifcase MKDEV(TTYAUX_MAJOR, 1): {struct tty_driver *console_driver = console_device(index);if (console_driver) {driver = tty_driver_kref_get(console_driver);if (driver && filp) {/* Don't let /dev/console block */filp->f_flags |= O_NONBLOCK;break;}}if (driver)tty_driver_kref_put(driver);return ERR_PTR(-ENODEV);}default: //串口会走default 分支driver = get_tty_driver(device, index);if (!driver)return ERR_PTR(-ENODEV);break;}return driver;
}
tty 层有许多tty_driver,每次有一个uart_driver 注册就会创建一个新的tty_driver 并且注册,不光是串口其它被tty 支持的设备注册也会产生tty_driver 创建和注册的动作。
为了维护这么多个tty_driver,tty层建立了一个tty_drivers 的链表,每当有注册新的uart_driver 导致tty_register_driver 被调用时,就会将新创建的tty_driver->tty_drivers 添加到tty_drivers链表。
查找一个tty_driver时,从头到尾遍历链表得到tty_driver,tty_driver通常有多个次设备,它们有相同的主设备号和递增的次设备号,将主次设备号结构体组成base~ base+num,如果打开的文件节点设备号device在这个范围内,那么说明找到了目标tty_driver。
找到tty_driver 的同时,将设备号device - base 还可以得到当前串口端口的序号。
static struct tty_driver *get_tty_driver(dev_t device, int *index)
{struct tty_driver *p;/*tty 层有许多tty_driver,每次有一个uart_driver 注册就会创建一个新的tty_driver 并且注册,不光是串口其它被tty 支持的设备注册也会产生tty_driver 创建和注册的动作。为了维护这么多个tty_driver,tty层建立了一个tty_drivers 的链表,每当有注册新的uart_driver 导致tty_register_driver 被调用时,就会将新创建的tty_driver->tty_drivers 添加到tty_drivers链表查找一个tty_driver时,从头到尾遍历链表得到tty_driver,tty_driver通常有多个次设备,将主次设备号结构体组成base~ base+num,如果打开的文件节点设备号device在这个范围内,那么说明找到了目标tty_driver*/list_for_each_entry(p, &tty_drivers, tty_drivers) { dev_t base = MKDEV(p->major, p->minor_start);if (device < base || device >= base + p->num)continue;*index = device - base; //当前设备号-基础设备号,就是当前串口的序号return tty_driver_kref_get(p);}return NULL;
}
得到tty_driver 之后,我们继续寻找tty_struct:
调用tty_driver_lookup_tty 来查找tty_struct,在串口驱动中没有提供tty_operations->lookup 函数,所以直接返回 tty_driver->ttys[index],由于注册时没有申请tty_struct,所以这里返回的是NULL。
static struct tty_struct *tty_driver_lookup_tty(struct tty_driver *driver,struct file *file, int idx)
{struct tty_struct *tty;/*调用tty_driver->tty_operations->lookup 来查找tty_struct,串口提供的tty_operations 是在uart_register_driver 中被设置,串口核心层的tty_operations 并没有提供lookup,所以串口设备不会用此函数查找*/if (driver->ops->lookup) if (!file)tty = ERR_PTR(-EIO);elsetty = driver->ops->lookup(driver, file, idx);elsetty = driver->ttys[idx]; //根据序号找到tty_struct,串口在注册时并没有为每一个串口创建tty_struct,这里tty 应该是个空指针if (!IS_ERR(tty))tty_kref_get(tty);return tty;
}
回到tty_open_by_driver
由于tty_driver_lookup_tty 返回 tty = NULL 所以进入else 分支,调用tty_init_dev
,这个函数会创建tty_struct 并初始化它(设置tty_struct->ops= tty_driver->ops,绑定tty_struct 与tty_driver、tty_port、uart_state 的关系,设置tty_struct->termios = tty_driver->init_termios )。
查看tty_init_dev
函数:
struct tty_struct *tty_init_dev(struct tty_driver *driver, int idx)
{struct tty_struct *tty;int retval;tty = alloc_tty_struct(driver, idx); //创建tty_struct,与tty_driver绑定,并把tty_driver->ops 赋值给tty_struct->opsif (!tty) {retval = -ENOMEM;goto err_module_put;}tty_lock(tty);//安装tty_struct:1、将uart_state 赋值给tty_struct->driver_data; 2、初始化tty_struct->termios = tty_driver->init_termios、将tty_struct设置到tty_driver->ttys[]retval = tty_driver_install_tty(driver, tty); if (retval < 0)goto err_free_tty;if (!tty->port)tty->port = driver->ports[idx]; //将tty_struct 与tty_port 绑定WARN_RATELIMIT(!tty->port,"%s: %s driver does not set tty->port. This will crash the kernel later. Fix the driver!\n",__func__, tty->driver->name);retval = tty_ldisc_lock(tty, 5 * HZ); if (retval)goto err_release_lock;tty->port->itty = tty;retval = tty_ldisc_setup(tty, tty->link); //ldisc 是与行规层有关的设置if (retval)goto err_release_tty;tty_ldisc_unlock(tty);return tty;
}
alloc_tty_struct
创建一个 tty_struct,将 tty_driver赋值到tty_struct->driver,后面就可以通过tty_struct 找到tty_driver 了,并且把tty_struct->ops 设置为tty_driver->ops;
tty_driver_install_tty
会安装tty_struct。如果下层驱动提供的tty_operations 提供了install 函数,则调用install 回调函数,否则调用tty_standard_install(虚拟的tty 设备可能是用后者)。
串口驱动提供的install 函数就是uart_install,它设置tty_struct->driver_data = uart_state,之后便可以通过tty_struct 找到uart_state;然后直接调用标准的安装函数。
tty_standard_install
将tty_struct 安装到tty_driver->ttys[],下一次打开这个端口时就可以直接从tty_driver 中获取啦。除了安装tty_struct 它还调用tty_init_termios 初始化tty_struct->termios,ktermios 中主要就是设置串口的波特率、校验位、停止位等等。
tty_struct 安装完成,回到 tty_init_dev
,设置tty_struct->port = tty_driver->ports[index],之后也可以通过tty_struct 找到tty_port 了。
tty_ldisc_setup 是行规层相关的设置,跳过。
tty_init_dev
函数结束之后回到 tty_open_by_driver
函数,返回tty_open
,tty_struct 总算是找到了,接着调用tty->ops->open。
在前面初始化tty_struct 时,已经将tty_struct->ops 设置为tty_driver->ops ,所以查看 tty_driver->ops->open。
tty_driver->ops->open 对于串口来说就是uart_open,这是在serial_core.c 中定义的。
static int uart_open(struct tty_struct *tty, struct file *filp)
{struct uart_state *state = tty->driver_data;int retval;retval = tty_port_open(&state->port, tty, filp);if (retval > 0)retval = 0;return retval;
}
首先从tty_struct->driver_data 中获取到uart_state,然后调用tty_port_open。
tty_port_open 会调用tty_port->ops->activate 激活串口。(struct tty_port_operations,tty_port->ops 是调用uart_register_driver 注册uart_driver 时设置的)
int tty_port_open(struct tty_port *port, struct tty_struct *tty,struct file *filp)
{.......int retval = port->ops->activate(port, tty);.......
}
对于串口来说,tty_port->ops-activate 就是 uart_port_activate;他会调用uart_startup 启动串口;
static int uart_port_activate(struct tty_port *port, struct tty_struct *tty)
{struct uart_state *state = container_of(port, struct uart_state, port);struct uart_port *uport;int ret;uport = uart_port_check(state);if (!uport || uport->flags & UPF_DEAD)return -ENXIO;port->low_latency = (uport->flags & UPF_LOW_LATENCY) ? 1 : 0;/** Start up the serial port.*/ret = uart_startup(tty, state, 0); //启动串口if (ret > 0)tty_port_set_active(port, 1);return ret;
}uart_startup-》 uart_port_startup
这里调用到uart_port->ops->startup,(struct uart_ops)这个ops是最底层驱动提供的接口,例如imx6ull 平台上的串口startup回调函数就是imx_uart_startup,它会设置硬件寄存器来启动串口(硬件的代码略过)。
static int uart_port_startup(struct tty_struct *tty, struct uart_state *state,int init_hw)
{struct uart_port *uport = uart_port_check(state);unsigned long page;unsigned long flags = 0;int retval = 0;......retval = uport->ops->startup(uport); //调用uart_port->ops->startup 启动串口,这个ops就是最终控制硬件的uart_opsif (retval == 0) {if (uart_console(uport) && uport->cons->cflag) {tty->termios.c_cflag = uport->cons->cflag;uport->cons->cflag = 0;}/** Initialise the hardware port settings.*/uart_change_speed(tty, state, NULL);/** Setup the RTS and DTR signals once the* port is open and ready to respond.*/if (init_hw && C_BAUD(tty))uart_port_dtr_rts(uport, 1);}return retval;
}
总结:
当我们在应用层open 打开一个串口 /dev/ttyS1,首先会调用到cdev->ops(file_operations)->open 即tty_open;
在tty_open 中主要会做以下事情:
1、根据设备号在tty_drivers 链表中找到tty_driver
2、分配、设置tty_struct
3、获取到与串口对应的tty_struct 之后,就会调用tty_struct->ops->open,也就是tty_driver->ops->open
4、调用tty_port->ops(tty_port_operations)->activate
5、最终调用uart_port->ops(uart_ops)->startup 启动硬件串口
一共涉及到三个ops,tty_driver->ops (tty_operations)、tty_port->ops (tty_port_operations)、uart_port->ops (uart_ops)
uart 情景分析:tcgetattr、tcsetattr
在上述应用例程中,设置波特率等协议是通过struct termios来描述的,而设置termios 就是通过以下两个函数。
tcgetattr( fd,&oldtio) //获取原始termios 配置
... //修改termios
tcsetattr(fd,TCSANOW,&newtio) //设置新的termios
实际上在内核中有一个与struct termios 一模一样的结构体(struct ktermios),它保存在tty_struct->termios 中。
tcgetattr函数的目的是为了获得当前配置的波特率,所以它只要在内核中取得tty_struct->termios 中的数据返回即可;
tcsetattr 是为了设置波特率,所以它不仅要修改tty_struct->termios 配置,还要把新的配置(波特率、数据位、校验位和停止位)写到寄存器内。
获取termios (获取当前的波特率等配置)
->__tcgetattr->__ioctl->tty_ioctl (tty层file_operations->unlocked_ioctl)->n_tty_ioctl (ld->ops->ioctl 行规程tty_ldisc->ops->ioctl 函数)->n_tty_ioctl_helper->tty_mode_ioctl//将tty_struct->termios 数据拷贝到临时的termioscopy_termios(real_tty, &kterm); //将临时termios 中的数据拷贝到应用层termioskernel_termios_to_user_termios((struct termios __user *)arg, &kterm)) 设置termios (设置tty_struct->termios、设置硬件寄存器:波特率、停止位、校验位....)
->__tcsetattr->__ioctl->tty_ioctl (tty层file_operations->unlocked_ioctl)->n_tty_ioctl (ld->ops->ioctl 行规程tty_ldisc->ops->ioctl 函数)->n_tty_ioctl_helper->tty_mode_ioctl->set_termios//将新的配置保存到tty_struct->termiosuser_termios_to_kernel_termios(&tmp_termios,(struct termios __user *)arg)//继续向下调用设置串口寄存器->tty_set_termios/*tty_struct->ops->set_termios下层提供的tty_operations->set_termios,对于串口来说就是串口核心层(serial_core.c) 中的uart_ops->set_termios (uart_set_termios)*/->tty->ops->set_termios->uart_change_speed/*uart_port->ops(uart_ops)->set_termios 具体的串口驱动提供的设置termios 函数对于imx6ull 来说它就是imx_uart_set_termios,在这个函数里会根据termios 的配置来设置硬件寄存器*/->uport->ops->set_termios
从上面的调用流程来看,我们如果要编写一个串口驱动,想设置串口波特率的话 uart_ops->set_termios 是必不可少的
从tcgetattr、tcsetattr 两个函数入手,跟踪波特率设置流程。
tcgetattr
查看tcgetattr 源码,在 glibc-2.3.2/sysdeps/unix/bsd/sun/sunos4/tcgetattr.c 中有如下代码:
int
__tcgetattr (fd, termios_p)int fd;struct termios *termios_p;
{return __ioctl (fd, TCGETS, termios_p);
}weak_alias (__tcgetattr, tcgetattr) //weak_alias:别名,把__tcgetattr 改个名字
关键代码是这一句return __ioctl (fd, TCGETS, termios_p);
这行代码的目的是获取到原始的struct termios 内容,所以会把原始的值拷贝到termios_p 指向的内存中。
__tcgetattr 调用到了ioctl,那么就会调用到tty层的 file_operations->unlocked_ioctl 即tty_ioctl。
tty_ioctl 中有许多关于cmd的分支,但是没有TCGETS
,最终调用行规程的ioctl 函数 ld->ops->ioctl。
long tty_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{struct tty_struct *tty = file_tty(file);struct tty_struct *real_tty;void __user *p = (void __user *)arg;int retval;struct tty_ldisc *ld;......ld = tty_ldisc_ref_wait(tty); //利用tty_struct 获取行规程struct tty_ldiscif (!ld)return hung_up_tty_ioctl(file, cmd, arg);retval = -EINVAL;if (ld->ops->ioctl) {retval = ld->ops->ioctl(tty, file, cmd, arg); //调用ld->ops->ioctlif (retval == -ENOIOCTLCMD)retval = -ENOTTY;}tty_ldisc_deref(ld);return retval;
}
找到N_TTY (n_tty.c)对应的行规程ioctl:n_tty_ioctl
没有TCGETS 对应的cmd 走default分支,调用n_tty_ioctl_helper
static int n_tty_ioctl(struct tty_struct *tty, struct file *file,unsigned int cmd, unsigned long arg)
{struct n_tty_data *ldata = tty->disc_data;int retval;switch (cmd) {case TIOCOUTQ:......case TIOCINQ:......default:return n_tty_ioctl_helper(tty, file, cmd, arg);}
}
依然是走default 分支,调用tty_mode_ioctl,这个函数应该是设置串口工作模式的(波特率等等)。
int n_tty_ioctl_helper(struct tty_struct *tty, struct file *file,unsigned int cmd, unsigned long arg)
{int retval;switch (cmd) {......default:/* Try the mode commands */return tty_mode_ioctl(tty, file, cmd, arg);}
}
在内核中有一个struct ktermios 的结构体是和termios 定义一样的,参考上面的定义。它就是在内核中保存串口波特率、校验位等等这些数据的。在open 的过程中设置好了默认的配置 保存在tty_struct->termios(9600 、无校验、1位停止位等等)。
所以tty_mode_ioctl 会把tty_struct->termios中的数据拷贝到应用层传入的arg(struct termios)中,然后返回应用层,得到了旧的 termios 配置。
#define kernel_termios_to_user_termios(u, k) copy_to_user(u, k, sizeof(struct termios))int tty_mode_ioctl(struct tty_struct *tty, struct file *file,unsigned int cmd, unsigned long arg)
{struct tty_struct *real_tty;void __user *p = (void __user *)arg;int ret = 0;struct ktermios kterm;switch (cmd) {case TCGETS:copy_termios(real_tty, &kterm);if (kernel_termios_to_user_termios((struct termios __user *)arg, &kterm))ret = -EFAULT;return ret;......}
}
static void copy_termios(struct tty_struct *tty, struct ktermios *kterm)
{down_read(&tty->termios_rwsem); //应该是类似锁一样的函数*kterm = tty->termios;up_read(&tty->termios_rwsem);
}
tcsetattr
然后是设置 termios 的流程,在 glibc-2.3.2/sysdeps/unix/bsd/sun/sunos4/tcsetattr.c 中有如下代码:
例程中设置termios 时的代码是tcsetattr(fd,TCSANOW,&newtio)
,所以下面的cmd 为TCSETS。
设置termios 的目的主要是为了把我们想要的配置设置到硬件寄存器上,所以我们来看看它是怎么一步步调用到底层驱动的,又是如何设置的。
int
tcsetattr (fd, optional_actions, termios_p)int fd;int optional_actions;const struct termios *termios_p;
{unsigned long cmd;switch (optional_actions){case TCSANOW:cmd = TCSETS;break;case TCSADRAIN:cmd = TCSETSW;break;case TCSAFLUSH:cmd = TCSETSF;break;default:__set_errno (EINVAL);return -1;}return __ioctl (fd, cmd, termios_p);
}
libc_hidden_def (tcsetattr)
直接从tty_mode_ioctl 开始(前面的内容与tcgetarr 是一样的):
调用set_termios 将传入的tremios 设置为新的值。
int tty_mode_ioctl(struct tty_struct *tty, struct file *file,unsigned int cmd, unsigned long arg)
{struct tty_struct *real_tty;void __user *p = (void __user *)arg;int ret = 0;struct ktermios kterm;switch (cmd) {case TCSETS:return set_termios(real_tty, p, TERMIOS_OLD);......}
}
调用user_termios_to_kernel_termios 把应用层termios 的值拷贝到内核tty_struct->termios 中。
调用tty_set_termios 设置termios。
#define user_termios_to_kernel_termios(k, u) copy_from_user(k, u, sizeof(struct termios))static int set_termios(struct tty_struct *tty, void __user *arg, int opt)
{struct ktermios tmp_termios;struct tty_ldisc *ld;int retval = tty_check_change(tty);down_read(&tty->termios_rwsem);tmp_termios = tty->termios;up_read(&tty->termios_rwsem);if (opt & TERMIOS_TERMIO) {.......} else if (user_termios_to_kernel_termios(&tmp_termios,(struct termios __user *)arg))return -EFAULT;tty_set_termios(tty, &tmp_termios);return 0;
}
tty->termios = *new_termios; //把新的配置保存在tty_struct->termios
tty->ops->set_termios(tty, &old_termios); //调用下层提供的tty_operations->set_termios,对于串口来说就是serial_core.c 中uart_ops->set_termios
ld->ops->set_termios(tty, &old_termios); //调用行规程ld->ops->set_termios
int tty_set_termios(struct tty_struct *tty, struct ktermios *new_termios)
{struct ktermios old_termios;struct tty_ldisc *ld;down_write(&tty->termios_rwsem);old_termios = tty->termios;tty->termios = *new_termios; //把新的配置保存在tty_struct->termiosunset_locked_termios(tty, &old_termios);//调用下层提供的tty_operations->set_termios,对于串口来说就是serial_core.c 中uart_ops->set_termiosif (tty->ops->set_termios)tty->ops->set_termios(tty, &old_termios); ld = tty_ldisc_ref(tty);if (ld != NULL) {if (ld->ops->set_termios)//调用行规程ld->ops->set_termiosld->ops->set_termios(tty, &old_termios); tty_ldisc_deref(ld);}up_write(&tty->termios_rwsem);return 0;
}
tty->ops->set_termios 就是uart_set_termios
在uart_set_termios 中先判断termios 与old_termios 相对比,如果没有改变直接返回,否则就调用uart_change_speed 硬件配置
static void uart_set_termios(struct tty_struct *tty, //此时tty_struct->termios 已经被设为新的值struct ktermios *old_termios)
{struct uart_state *state = tty->driver_data;struct uart_port *uport;unsigned int cflag = tty->termios.c_cflag;unsigned int iflag_mask = IGNBRK|BRKINT|IGNPAR|PARMRK|INPCK;bool sw_changed = false;mutex_lock(&state->port.mutex);uport = uart_port_check(state);if (!uport)goto out;if (uport->flags & UPF_SOFT_FLOW) {iflag_mask |= IXANY|IXON|IXOFF;sw_changed =tty->termios.c_cc[VSTART] != old_termios->c_cc[VSTART] ||tty->termios.c_cc[VSTOP] != old_termios->c_cc[VSTOP];}if ((cflag ^ old_termios->c_cflag) == 0 &&tty->termios.c_ospeed == old_termios->c_ospeed &&tty->termios.c_ispeed == old_termios->c_ispeed &&((tty->termios.c_iflag ^ old_termios->c_iflag) & iflag_mask) == 0 &&!sw_changed) {goto out; //没有改变,直接返回}uart_change_speed(tty, state, old_termios); //修改termios 配置 ......
uart_change_speed 调用硬件驱动提供的uart_ops->set_termios 对硬件寄存器进行波特率等值的修改。对于imx6ull 它就是imx_uart_set_termios(具体的硬件操作忽略)。
static void uart_change_speed(struct tty_struct *tty, struct uart_state *state,struct ktermios *old_termios)
{struct uart_port *uport = uart_port_check(state);struct ktermios *termios;int hw_stopped;/** If we have no tty, termios, or the port does not exist,* then we can't set the parameters for this port.*/if (!tty || uport->type == PORT_UNKNOWN)return;termios = &tty->termios;//调用uart_port->ops->set_termios 设置新的 termiosuport->ops->set_termios(uport, termios, old_termios); ......
}
uart 驱动情景分析:read
串口读写会涉及到行规层,行规层有什么作用呢?在使用串口作为console的时候,我们执行命令,查看信息等等都很方便,这就是因为有行规程,它会将输入的数据回显到终端上,输入回车就会执行命令等等。
串口读过程分析:
read的过程也分为三层:应用层、行规层、串口驱动层。
在应用层调用read 函数读取串口数据,当有数据时会从行规层的buffer 中将数据拷贝到 用户空间的buffer,然后返回;如果没有数据这个读线程就会陷入休眠。
那么在什么时候会唤醒这个线程?数据来的时候。
在串口的驱动中会注册中断,当硬件上有数据到来的时候,硬件触发中断,进入串口中断处理函数。
串口中断处理函数先读取中断的状态(是否有数据可读、是否发生错误、接收的数据包统计等等),接着清理中断标志位等等一些比较紧急的事情,然后将数据从寄存器上读取到驱动(imx6ull 的串口接收数据寄存器只有32bit,其中8bit 是数据,不会很耗时)每次只会读取一个字节,读到1字节的数据后就将其插入tty_port 的buffer中。
数据读取到tty_port 的buffer完成后,需要通知行规层,有数据可读啦、可以来读啦。(这里通知并不是直接执行的,在中断处理函数中会调度一个工作队列,在工作线程中通知行规层)
行规层得到通知后将数据从驱动buffer 中拷贝出来,先把数据处理一下(比如shell中输入了删除键,他就会把字符删掉(对于非console的串口不会做处理)),接着放入到自己的buffer中,然后唤醒读线程,将数据从行规层buffer 拷贝到user buffer,应用线程返回应用空间。
大致了解read 的流程后看一下代码:
** 行规程注册**
首先是行规层的问题:行规层也需要注册,它是在哪注册的呢。
调用tty_register_ldisc 函数可以注册行规层,在内核源码中搜索该函数,看有那些地方注册了行规层。
有很多地方注册了行规层,在driver/tty/n_tty.c 中会注册n_tty 的行规层,它是内核中最通用的。
n_tty.c 函数n_tty_init 中注册了N_TTY行规层,主要是它的ops:n_tty_ops。注册完成后通过N_TTY就可以找到此行规层。
在kernel/printk/printk.c 的console_init 中调用n_tty_init 注册了行规层,它应该是内核启动时在串口注册前就注册好了。
open设备时确定行规程
那么串口是怎么获取到n_tty 常规层的?在调用open 打开设备的时候。
在open时一路调用到tty_ldisc_get 获取行规层(struct tty_ldisc),保存到tty_struct->ldisc 中。
准备工作都做完了,接着查看read 的调用过程:
应用层调用read,就会调用到cdev->ops(file_operations)->read,即tty_read。
tty_read 从tty_struct 中取出tty_ldisc,调用tty_ldisc->ops->read,即前面n_tty_ops->read,n_tty_read。
(在tty_read 中可以直接用file_tty() 来获取tty_struct,因为在tty_open 中使用tty_add_file() 向struct file 添加了tty_struct)
n_tty_read 函数定义了一个等待条目(struct wait_queue_entry),并将他添加到 read_wait 等待队列。
检查是否有数据可读,无数据则进入休眠等待,等待超时时返回timeout = 0;执行break 跳出while循环。
如果有数据或等待过程中数据到了则调用canon_copy_from_read_buf 或copy_from_read_buf 读取数据,返回。
copy_from_read_buf 函数先读取行规层buffer 地址from,然后拷贝from 中的数据到用户空间buffer。
const unsigned char *from = read_buf_addr(ldata, tail);// return &ldata->read_buf[i & (N_TTY_BUF_SIZE - 1)];retval = copy_to_user(*b, from, n);
数据源头: 中断
应用线程从行规层读取数据已经了解,接下来看看驱动如何将数据从硬件上传到行规层。参考imx6ull 平台串口驱动。
对于 imx 串口驱动,解析dtb 中串口端口的硬件信息填充uart_port (imx_uart_probe() ),硬件信息包含 irq,同时会注册irq 以及它的中断处理函数,如下:
imx_uart_int 判断中断状态标志位,调用__imx_uart_rxint 读取数据。
__imx_uart_rxint函数先判断硬件的状态、清除标志位等等,然后调用tty_insert_flip_char 将数据存入tty_port 的缓冲区,然后调用tty_flip_buffer_push通知行规程来处理。
__imx_uart_rxint// 读取硬件状态// 得到数据// 在对应的uart_port中更新统计信息, 比如sport->port.icount.rx++;// 把数据存入tty_port里的tty_buffertty_insert_flip_char(port, rx, flg)// 通知行规程来处理tty_flip_buffer_push(port);tty_schedule_flip(port);queue_work(system_unbound_wq, &buf->work); // 使用工作队列来处理// 对应flush_to_ldisc函数
tty_port->buf 的类型为struct tty_bufhead.
数据成功拷贝到tty_port->buf->tail 中后,就会调用tty_flip_buffer_push 来通知行规层读取数据。
tty_flip_buffer_push-> tty_schedule_flip
queue_work 调度工作队列,将work 任务放入工作队列执行。
那么buf->work 的工作函数是什么,要找到work 初始化的地方。既然是tty_port->buf->work,那么我们就寻找以下tty_port 初始化的位置,在uart_register_driver 函数中调用tty_port_init 初始化tty_port。
查看tty_port_init 函数定义,有一个tty_buffer_init。
tty_buffer_init 调用INIT_WORK 初始化tty_port->buf->work,工作函数为flush_to_ldisc,查看工作函数flush_to_ldisc。
flush_to_ldisc 看名字就知道它要把数据刷新到行规层。
调用tty_port->client_ops->recevie_buf 读取数据。
tty_port->client_ops 有两处设置的地方:
①是在tty_port 在 uart_register_driver->tty_port_init 中初始化tty_port 时设置为默认的tty_port_default_client_ops
②是在初始化添加uart_port 时调用的 uart_add_one_port->tty_port_register_device_attr_serdev->serdev_tty_port_register 中有设置为client_ops。
注意:如果serdev_controller 添加失败的话是会重新设置成tty_port_default_client_ops的。
这里说明一下,对于imx6ull 上的串口来说 serdev_controller_add是会返回失败的,因为imx6ull 的dtb串口节点下没有关于serdev 子节点的描述(serdev_controller_add 会检索串口节点下serdev 节点,没有则返回失败)。
所以tty_port->client_ops == tty_port_default_client_ops。
对于imx6ull 以及其它没有serdev 描述的平台来说,serdev_tty_port_register这个函数是没有意义的,可以直接忽略(在4.x 内核中没有此函数)。
回到读取数据流程,调用tty_port->client_ops->receive_buf 即tty_port_default_receive_buf。
tty_port_default_receive_buf
-> tty_ldisc_receive_buf
tty_ldisc_receive_buf 调用行规层 receive_buf或receive_buf2 接收数据。
根据前面open 时设置的tty_struct->ldisc,可以确定行规层为N_TTY,那么ldisc->ops 就是n_tty_ops。
调用n_tty_receive_buf2 从tty_port 的buffer中读取数据放入行规层buffer。
n_tty_receive_buf2
-> n_tty_receive_buf_common
-> __receive_buf
在__receive_buf 函数中读取数据,并唤醒等待线程。读线程被唤醒从行规层buffer 将数据拷贝到用户空间buffer,然后返回。
uart 驱动情景分析:write
过程描述
参考uart 驱动框架图:
应用层调用write() 发送数据,调用到tty层的file_operations->write 即tty_write,tty_write 中会调用行规程的tty_ldisc->ops->write 并且传递来自应用空间的user_buffer。
串口所用的行规程为n_tty,那么tty_ldisc->ops->write 就是n_tty_write,n_tty_write 将要发送的数据从user_buffer 拷贝到行规程 ldisc_buffer (copy_from_user)。拷贝完成之后n_tty_write 会调用tty_struct->ops->write 函数向下层发送数据,根据前面的分析我们知道tty_struct->ops 是串口核心层提供的tty_operations,那么tty_struct->ops->write 就是uart_write。uart_write是串口核心层定义的,不涉及具体硬件,所以它会调用串口硬件驱动层提供的uart_port->ops(struct uart_ops)->start_tx 开始发送(这个start_tx 函数就要根据各自的平台而定了)。
在串口硬件中有一个txFIFO 的数据管道,只要将数据放入txFIFO 数据就会自动发送,为了及时的知道硬件上数据发送完成,通常会有一个txFIFO 空的中断。
因此在start_tx 中并不会直接将数据从ldisc_buffer 拿过来放入硬件txfifo,而是使能txFIFO 空中断,在中断处理函数中将数据放入txFIFO,等到数据发送完成又会进入中断函数再次将数据放入txFIFO,如此反复,直到所有数据发送完成。
代码解析
应用代码调用write 发送数据,调用到内核空间tty层file_operations->write 即tty_write。
tty_write 调用do_tty_write(),并传入ld->ops->write、user-buffer 以及要发送的字节个数count。
do_tty_write 先将数据从user_buffer,拷贝至tty_struct->write_buf,然后循环的调用行规程write发送数据,返回已发送的字节数。
对于串口来说 ld->ops->write 就是n_tty_write.
n_tty_write 调用tty_struct->ops(struct tty_operations)->write 向下层发送数据,即serial_core.c 中的uart_write 函数。
注意,这里也有定义了一个休眠结构体,当uart_write 返回c == 0 时,会调用wait_woken 进行休眠,应该是在来不及发送的情况下会进入休眠。
uart_write 从uart_state->xmit 中获取到一个struct circ_buf 的环形缓冲区,CIRC_SPACE_TO_END 返回缓冲区中剩余可用空间长度,然后将数据从tty_struct->write_buffer 拷贝到circ_buf,调用__uart_start() 开始发送。
struct circ_buf {char *buf;int head;int tail;
};
__uart_start 调用uart_port->ops->start_tx 开始发送数据,对于imx6ull 来说,它就是imx_uart_start_tx。
imx_uart_start_tx 中使能UCR1 发送就绪的中断使能位,一旦txFIFO 中有空位置,就会产生中断。
由于初始化的时候imx6ull 设备树中没有提供txirq,所以串口的发送、接收是公用一个中断的,中断处理函数是imx_uart_int。
当串口硬件发生中断时,进入中断处理函数imx_uart_int,它判断状态寄存器USR1_TRDY 发送就绪位,是否有中断发生,或判断USR2_TXDC 位是否发送完成,如果发送完成就可以放入下一批数据。调用imx_uart_transmit_buffer 发送数据。
imx_uart_transmit_buffer 往UATX0 (发送数据寄存器)中写数据,即写入txFIFO。从(struct circ_buf) xmit->buf[xmit->tail] 开始一个一个字节写入UATX0,xmit->tail 不断++ 往后偏移字节,与上(UART_XMIT_SIZE - 1) 是为了限制范围,防止超出crc_buf 缓冲区的大小,当circ_buf 缓冲区为空(buf中的数据全部发完)后就会跳出while循环停止发送。
如果前面写的过程中circ_buf 被塞满了,但是还有数据没发完会陷入休眠,所以uart_circ_chars_pending(xmit) < WAKEUP_CHARS 会判断是否需要解除休眠。调用uart_write_wakeup 解除休眠。
最后,如果circ_buf 是空的,那么调用imx_uart_stop_tx 停止发送,停止发送函数会 禁用发送就绪中断使能位 (UCR1_TRDYEN),防止不发送数据时空的txFIFO 一直产生中断(imx_uart_stop_tx 也被设为uart_ops->stop_tx,可以从上层调用停止发送)。
uart_write_wakeup 最终会调用tty_wakeup 唤醒被休眠的线程。
uart_write_wakeup->tty_port_tty_wakeup->port->client_ops->write_wakeup //tty_port_default_client_ops->tty_port_default_wakeup->tty_wakeup
停止发送
uart 驱动一些调试方法
-
根据上面的分析在驱动中添加打印,查看接收、发送的数据是否正确
-
查看串口中断发生的次数
cat /proc/interrupts //查看系统中所有设备产生的中断次数,以及中断号。中断设备可以参照设备树来查找
-
使用 cat /proc/tty/drivers 查看系统中支持哪些tty driver
-
查看串口发送、接收字节数的统计信息
先使用 ls /proc/tty/driver 查看内核支持哪些tty driver
再cat 具体的驱动查看统计信息,如下IMX-uart 一共有3个端口0、2、5,和它们的irq、接收发送字节数
上述打印是在driver/tty/serial/serial_core.c 函数uart_line_info中打印的。
-
查看内核支持哪些行规程
-
查看驱动中支持的各种属性文件
找不到文件位置可以直接在 /sys/ 目录下搜索文件名:find -name “irq”
虚拟的串口驱动示例
以下代码为虚拟的串口驱动示例:
创建一个/dev/ttyvirt0 的虚拟串口,应用层中使用串口的方式与普通的串口相同。
在rootfs 中创建一个/proc/virt_uart_buf 虚拟文件作为与/dev/ttyvirt0 通信的另一个串口。
接收模拟:
执行 “echo xxxxxx > /proc/virt_uart_buf” 命令会调用到驱动中virtuart_proc_fops.write 函数将数据写入rxbuf,同时产生中断。驱动会响应中断,在中断处理函数中读取rxbuf的数据,上传到tty_port buffer,并刷洗到行规层buf(此时应用程序调用read可以读取到行规程buffer 中接收到的串口数据)。
发送模拟:
应用程序调用write 发送数据,会将数据存储到uart_state->xmit (struct circ_buf),并调用uart_port->ops->start_tx 函数,在start_tx 函数中将xmit 里的数据存入txbuf。(实际的串口驱动是在start_tx 使能txFIFO 空中断,在中断内将xmit数据放入txFIFO)
执行 “cat /proc/virt_uart_buf” 命令,可以读取txbuf 查看串口发送出去的数据。
(由于是虚拟串口,不涉及到任何硬件,所以驱动代码是最精简的,在驱动中uart_ops、uart_port 的配置都是必须的,否则使用过程中会出错。(实际硬件的串口驱动只会比这更复杂))
#include <linux/module.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/serial_core.h>
#include <linux/serial.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/fs.h>
#include <linux/proc_fs.h>
#include <asm/irq.h>
#include <linux/tty_flip.h>#define DRIVER_NAME "virt_uart"
#define DEV_NAME "ttyvirt"struct proc_dir_entry *proc_uart_file;
struct uart_port *uport;/*环形缓冲区* */
#define CIRC_BUF_SIZE 0xff //255字节static unsigned char txbuf[CIRC_BUF_SIZE] = {0};
static int txbuf_r; //可读位置下标
static int txbuf_w; //可写位置下标static unsigned char rxbuf[CIRC_BUF_SIZE] = {0};
static int rxbuf_r;
static int rxbuf_w;//判断缓冲区函数
static int circbuf_is_empty(int r,int w)
{return r == w ? 1 : 0;
}static int circbuf_is_full(int r,int w)
{return w+1 == r ? 1 : 0;
}//计算buf 中有效数据长度
static int circbuf_avlid_len(int r,int w)
{if(w > r)return w - r;else if(w == r)return 0;elsereturn CIRC_BUF_SIZE - r + w;
}//计算buf 中剩余可写的空闲空间
static int circbuf_free_len(int r,int w)
{if(w >= r)return (CIRC_BUF_SIZE - w) + r;elsereturn r - w;
}static int circbuf_read_data(unsigned char* circbuf,int *r_p,int *w_p,unsigned char *tmp_buf)
{int len,r = *r_p,w = *w_p;if(circbuf_is_empty(r,w)) //无数据可读return 0;//读取缓冲区中所有有效数据if(w > r){len = w - r;memcpy(tmp_buf,circbuf + r,len);}else{len = CIRC_BUF_SIZE - r;memcpy(tmp_buf,circbuf + r,len);r = 0; len = w;memcpy(tmp_buf,circbuf + r,len);}r = w; //将读下标偏移至写下标(空)*r_p = r;*w_p = w;return 0;
}static int circbuf_write_data(unsigned char* circbuf,int *r_p,int *w_p,unsigned char *tmp_buf,int size)
{int count;int r = *r_p;int w = *w_p;if(size <= 0)return size;if(size > (CIRC_BUF_SIZE - w)){memcpy(circbuf + w,tmp_buf,CIRC_BUF_SIZE - w);count = size - (CIRC_BUF_SIZE - w);w = 0;memcpy(circbuf + w,tmp_buf,count);w = count;}else{memcpy(circbuf + w,tmp_buf,size);w = w + size;}*r_p = r;*w_p = w;return size;
}static struct uart_driver virt_uart_driver = {.owner = THIS_MODULE,.driver_name = DRIVER_NAME,.dev_name = DEV_NAME, //dev_name + index组合就是设备节点的名称 (ttyvirtX).major = 0, //主设备号,写0 自动分配.minor = 64,.nr = 1,//.cons = IMX_CONSOLE,
};static const struct platform_device_id virt_uart_devtype[] = {{.name = DRIVER_NAME,}, {/* sentinel */}
};static const struct of_device_id virt_uart_dt_ids[] = {{ .compatible = DRIVER_NAME, },{ /* sentinel */ }
};/** 供应用层查看串口驱动类型。** 在 cat /proc/tty/driver/IMX-uart 时会用到,如* * # cat /proc/tty/driver/IMX-uartserinfo:1.0 driver revision:0: uart:IMX mmio:0x02020000 irq:18 tx:21113 rx:248 RTS|DTR|DSR|CD1: uart:IMX mmio:0x021E8000 irq:233 tx:0 rx:0 DSR|CD* 没有这个函数cat 会卡住,驱动卡死* */
static const char *virt_uart_type(struct uart_port *port)
{return "VIRT_UART";
}static void virt_uart_stop_tx(struct uart_port *port)
{
}static void virt_uart_start_tx(struct uart_port *port)
{struct circ_buf *xmit = &port->state->xmit;unsigned long flags;//在实际的串口驱动中,发送数据放在中断进行,发送数据时需要关闭硬件中断//spin_lock_irqsave(port->lock, flags);while(!uart_circ_empty(xmit)){if(circbuf_is_full(txbuf_r,txbuf_w))break;circbuf_write_data(txbuf,&txbuf_r,&txbuf_w,&xmit->buf[xmit->tail],1);xmit->tail = (xmit->tail + 1) & (UART_XMIT_SIZE - 1);port->icount.tx++; }//检查是否有线程需要唤醒if (uart_circ_chars_pending(xmit) < WAKEUP_CHARS)uart_write_wakeup(port);//停止发送if (uart_circ_empty(xmit))virt_uart_stop_tx(port);//spin_unlock_irqrestore(&port->lock, flags);
}static int virt_uart_startup(struct uart_port *port)
{return 0;
}static void virt_uart_shutdown(struct uart_port *port)
{
}static void virt_uart_set_termios(struct uart_port *port, struct ktermios *new,struct ktermios *old)
{
}static void virt_uart_stop_rx(struct uart_port *port)
{
}/*此函数一定要给出,不然cat /proc/tty/driver/virt_uart时会空指针* */
static unsigned int virt_uart_get_mctrl(struct uart_port *port)
{return 0;
}
/*当txFIFO 不忙时返回 TIOCSER_TEMT* */
static unsigned int virt_uart_tx_empty(struct uart_port *port)
{return TIOCSER_TEMT;
}void virt_uart_release_port(struct uart_port *port)
{}void virt_uart_set_mctrl(struct uart_port *port, unsigned int mctrl)
{}static const struct uart_ops virt_uart_pops = {.tx_empty = virt_uart_tx_empty, //判断txFIFO 是否为空.set_mctrl = virt_uart_set_mctrl,.get_mctrl = virt_uart_get_mctrl, //cts、rts 流控相关的.start_tx = virt_uart_start_tx, //开始发送.stop_tx = virt_uart_stop_tx, //停止发送.stop_rx = virt_uart_stop_rx,//.enable_ms = virt_uart_enable_ms,//.break_ctl = virt_uart_break_ctl,.startup = virt_uart_startup, //启动串口.shutdown = virt_uart_shutdown,//.flush_buffer = virt_uart_flush_buffer,.set_termios = virt_uart_set_termios, //设置波特率、停止位、校验位、数据位.release_port = virt_uart_release_port,.type = virt_uart_type,//.config_port = virt_uart_config_port,//.verify_port = virt_uart_verify_port,
};static irqreturn_t virt_uart_int(int irq,void *dev_id)
{int cnt;unsigned char tmp_buf[255] = {0};circbuf_read_data(rxbuf,&rxbuf_r,&rxbuf_w,tmp_buf);/** 调用tty_insert_flip_string 将串口数据插入tty_port buffer* */cnt = tty_insert_flip_string(&uport->state->port,tmp_buf,strlen(tmp_buf));if(cnt != strlen(tmp_buf)){printk("%s cnt %d strlen %d",__func__,cnt,strlen(tmp_buf));}uport->icount.rx += cnt;//tty_flip_buffer_push 将数据刷洗到行规程buffertty_flip_buffer_push(&uport->state->port);return IRQ_HANDLED;
}ssize_t virt_uart_buf_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{int ret = 0,cnt;unsigned char tmp_buf[255] = {0};cnt = circbuf_avlid_len(txbuf_r,txbuf_w);if(!cnt)return 0;cnt = (size > cnt) ? cnt : size;printk("%s ,cnt %d\n",__func__,cnt);circbuf_read_data(txbuf,&txbuf_r,&txbuf_w,tmp_buf);ret = copy_to_user(buf,tmp_buf,strlen(tmp_buf));if(ret){printk("copy_to_user\n");return -1;}return cnt;}ssize_t virt_uart_buf_write(struct file *filp, const char __user *buf, size_t size, loff_t *off)
{unsigned char tmp_buf[255] = {0};int ret,cnt;cnt = circbuf_free_len(rxbuf_r,rxbuf_w);cnt = (size > cnt) ? cnt : size;ret = copy_from_user(tmp_buf,buf,cnt);if(ret){printk("copy_from_user\n");return -1;}circbuf_write_data(rxbuf,&rxbuf_r,&rxbuf_w,tmp_buf,cnt);/* 模拟产生RX中断 */irq_set_irqchip_state(uport->irq, IRQCHIP_STATE_PENDING, 1);return cnt;
}static struct file_operations virtuart_proc_fops = {.read = virt_uart_buf_read,.write = virt_uart_buf_write,
};/* 在probe 函数中,需要解析设备树节点,并构造、填充一个struct uart_port,* 调用 uart_add_one_port 向uart_driver添加一个uart_port** *
*/
static int virt_uart_probe(struct platform_device *pdev)
{int irq,ret;dev_info(&pdev->dev,"%s %d\n",__func__,__LINE__);/*在/proc 创建一个虚拟文件用来保存virt_uart 发送出去的数据,以及向virt_uart 发送数据** virt_uart发送数据 ---> 存入txbuf(模拟串口发送数据) =====> cat /proc/virt_uart_buf 查看txbuf内容* echo "xxx" > /proc/virt_uart_buf ---> "xxx" 数据存入rxbuf =====> 触发中断,读出rxbuf数据,上传至行规程(模拟串口接收)*/proc_uart_file = proc_create("virt_uart_buf", 0, NULL, &virtuart_proc_fops);if(!proc_uart_file)return -1;irq = platform_get_irq(pdev,0);if(irq < 0)return irq;uport = devm_kzalloc(&pdev->dev,sizeof(*uport),GFP_KERNEL);if(!uport)return -2;//填充uart_portuport->dev = &pdev->dev;uport->type = PORT_IMX;uport->iotype = UPIO_MEM;uport->irq = irq;uport->ops = &virt_uart_pops; //关键!操作串口的函数集//注册irqret = devm_request_irq(&pdev->dev,irq,virt_uart_int,0,"virt_uart",NULL);if(ret){dev_info(&pdev->dev,"%s %d failed to reqeust irq :%d\n",__func__,__LINE__,ret);return ret;}platform_set_drvdata(pdev,uport);return uart_add_one_port(&virt_uart_driver,uport);
}static int virt_uart_remove(struct platform_device *pdev)
{int ret;dev_info(&pdev->dev,"%s %d\n",__func__,__LINE__);ret = uart_remove_one_port(&virt_uart_driver,uport);proc_remove(proc_uart_file);return ret;
}static struct platform_driver virt_uart_platform_driver = {.probe = virt_uart_probe,.remove = virt_uart_remove,.id_table = virt_uart_devtype,.driver = {.name = DRIVER_NAME,.of_match_table = virt_uart_dt_ids,},
};static int __init virt_uart_init(void)
{//注册一个uart_driverint ret = uart_register_driver(&virt_uart_driver);//注册一个platform_driver,如果有设备节点或是platfrom_device 与其匹配的话将会调用probe 函数ret = platform_driver_register(&virt_uart_platform_driver);if (ret != 0)uart_unregister_driver(&virt_uart_driver);return ret;
}static void __exit virt_uart_exit(void)
{//注销platform_driverplatform_driver_unregister(&virt_uart_platform_driver);//注销uart_driveruart_unregister_driver(&virt_uart_driver);
}module_init(virt_uart_init);
module_exit(virt_uart_exit);MODULE_LICENSE("GPL");
相关文章:
uart 子系统
串口硬件储备知识: uart 在Linux 应用层的体现及使用 uart 就是串口,它也是属于字符设备中的一种,众所周知 字符设备都会在/dev/ 目录下创建节点,串口所创建的节点名都是以tty* 为开头,例如下面这些节点:…...
SpringBoot 整合EasyExcel详解
一、概述 Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内…...
VScode+cuda编程:常见环境问题
VScodecuda:常见环境配置问题1、VScode终端问题(PS)2、编译问题(CUDA版本过低)3、nvcc编译问题(arch架构)1、VScode终端问题(PS) 问题描述: 在VScode下打开终端执行nvcc指令,发现执行不了,但是在外部终端powershell和cmd都可以。…...
简单实用的内网穿透实现教程
内网穿透,字面理解就是网络地址穿透,是一种比较常用的将内网地址转换成公网地址的方式。通过内网穿透,可以将本地内网局域网提供给外网公网上访问,在外网也能连接访问内网主机和应用,当用户有日常远程和异地外网访问的…...
makefile案例学习
makefile案例学习 很多时候, 我们在git clone完一个project之后,就会让我们使用make命令进行项目的构建。这个make命令的背后就是按照了Makefile文件定义的格式去完成项目构建。 因此Makefile的作用就是帮助程序员进行项目的构建,它按照项目…...
MySQL性能优化六 事物隔离级别与锁机制
概述 我们的数据库一般都会并发执行多个事务,多个事务可能会并发的对相同的一批数据进行增删改查操作,可能就会导致我们说的脏写、脏读、不可重复读、幻读这些问题。 这些问题的本质都是数据库的多事务并发问题,为了解决多事务并发问题&#…...
四数之和-力扣18-java排序+双指针
一、题目描述给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):…...
操作系统开发:BIOS/MBR基础与调试
这里在实验之前需要下载 Bochs-win32-2.6.11 作者使用的是Linux版本的,在Linux写代码不太舒服,所以最好在Windows上做实验,下载好虚拟机以后还需要下载Nasm汇编器,以及GCC编译器,为了能够使用DD命令实现磁盘拷贝&#…...
华为OD机试真题JAVA实现【数组合并】真题+解题思路+代码(20222023)
🔥系列专栏 华为OD机试(JAVA)真题目录汇总华为OD机试(Python)真题目录汇总华为OD机试(C++)真题目录汇总华为OD机试(JavaScript)真题目录汇总文章目录 🔥系列专栏题目输入输出示例一输入输出示例二输入输出解题思路核心知识点...
说说Real DOM和Virtual DOM的区别?优缺点?
说说Real DOM和Virtual DOM的区别?优缺点?Real DOM(真实的DOM)真实dom的优缺点?Virtual DOM(虚拟的DOM)虚拟dom的优缺点?两者的区别Real DOM(真实的DOM) 在页面渲染出的每个节点都是一个真实的DOM结构 <div class"root&…...
使用脚本以可读的 JSON 格式显示 curl 命令输出
在我们经常调试微服务或者使用 Elasticsearch API 时,经常会使用curl 来进行调试。但是有时我们的输出不尽如意。显示的不是一 pretty 格式进行输出的。我们有时还必须借助于其他的一些网站工具,比如 Best JSON Formatter and JSON Validator: Online JS…...
计算机网络9:HTTP和HTTPS的区别
1.HTTP和HTTPS的区别 (1)安全性 HTTP是超文本传输协议,信息传输存在安全问题HTTPS是安全套接字超文本传输协议,在TCP和HTTP之间加入了SSL/TLS安全协议,进行加密传输 (2)连接步骤HTTP建立相对简…...
Spring+SpringMVC+SpringBoot+MyBatis面试题
什么是Spring框架?使用Spring框架的好处是什么?Spring是一款开源的轻量级Java开发框架,可以提高开发人员的开发效率以及系统的可维护性。Spring框架是很多模块的集合,使用这些模块可以很方便地协助我们进行开发,比如说…...
ContextCapture Master 倾斜摄影测量实景三维建模技术
ContextCapture实景建模大师是一套无需人工干预,通过影像自动生成高分辨率的三维模型的软件解决方案。它集合了全球最先进数字影像处理、计算机虚拟现实以及计算机几何图形算法,在易用性、数据兼容性、运算性能、友好的人机交互及自由的硬件配置兼容性等…...
MySQL事务
文章目录MySQL事务事务的四个特性 ACID事务提交的类型事务的使用MySQL事务 事务是什么? 事务就是一组逻辑操作单元,是数据从一种状态变成另外一种状态。整个单元有一个或多个SQL语句构成,在这个操作单元中,每一个SQL语句相互依赖…...
CData Drivers for Acumatica
CData Drivers for Acumatica Acumatica的CData驱动程序为用户提供了使用AcumaticaERP数据的便捷途径,该数据来自商业智能、分析、定制应用程序、报告以及ETL。通过JDBC、ADO.NET和ODBC等标准驱动程序,以及与PowerShell、Power BI、Excel、SSIS等流行应用…...
智慧税务+数据可视化:企业财务管理告别难题
一、引言在发展社会主义市场经济的过程中,税收承担着组织财政收入、调控经济、调节社会分配的职能。中国每年财政收入的90%以上来自税收,其地位和作用越来越重要,可称之为国家经济的“晴雨表”,有效进行税务管理、充分挖掘税务大数…...
Ansible中常用的模块
目录 一、Ansible Ad-Hoc命令集 1 Ad-hoc 使用场景 2 Ansible的并发特性 3 Ansible-doc用法 4 ansible命令运行方式及常用参数 5 ansible的基本颜色代表 6 ansible中的常用模块 command模块 shell模块 script模块 copy模块 fetch模块 unarchive模块 archive模块…...
问:你是如何进行react状态管理方案选择的?
前言:最近接触到一种新的(对我个人而言)状态管理方式,它没有采用现有的开源库,如redux、mobx等,也没有使用传统的useContext,而是用useState useEffect写了一个发布订阅者模式进行状态管理&…...
【华为OD机试真题 java、python、jsNode】任务总执行时长【2022 Q4 100分】
代码请进行一定修改后使用,本代码保证100%通过率,本题提供了 java、python、JsNode三种代码 题目描述 任务编排服务负责对任务进行组合调度。参与编排的任务有两种类型,其中一种执行时长为taskA,另一种执行时长为taskB。任务一旦开始执行不能被打断,且任务可连续执行。服…...
react基础
react组件传参 父传子 父组件 < ChildA value{this.state.num}></ChildA> 子组件 {props.value}接收父组件传入参数 ChildA.defaultProps{vaue:1} defaultProps默认参数 子传父 props回调函数形式 父 setNum>v>this.setState({num:v}) v形参 < ChildA…...
【Spark分布式内存计算框架——Spark SQL】2. SparkSQL 概述(上)
第二章 SparkSQL 概述 Spark SQL允许开发人员直接处理RDD,同时可以查询在Hive上存储的外部数据。Spark SQL的一个重要特点就是能够统一处理关系表和RDD,使得开发人员可以轻松的使用SQL命令进行外部查询,同时进行更加复杂的数据分析。 2.1 前…...
Kubeadm搭建K8S
目录 一、部署步骤 1、实验环境 2、环境准备 3、所有节点安装Docker 4、 所有节点配置K8S源 5、所有节点安装kubeadm,kubelet和kubectl 6、部署 kubernetes Master 节点 7、token制作 8、k8s-node节点加入master节点 9、 master节点安装部署pod网络插件&a…...
【技术分享】搭建java项目引入外部依赖教程
文章目录引言如何在linux中编译运行java程序IDEA中新建一个简单的java工程项目并运行IDEA中如何引入外部依赖并运行maven引入log4j jar包手工引入log4j jar包如何使用命令行的方式添加外部依赖如何新建一个spring源码项目并为其添加依赖给定一个spring工程源码,如何…...
算法 ——世界 二
个人简介:云计算网络运维专业人员,了解运维知识,掌握TCP/IP协议,每天分享网络运维知识与技能。个人爱好: 编程,打篮球,计算机知识个人名言:海不辞水,故能成其大;山不辞石…...
数据治理CDGP选择题 4
5、根据DMBOK2,在实施数据治理时,要注重数据标准的建设,以下关于数据标准的描述,哪个选项是不正确的? (知识点: CDGP仿真题)A.数据标准必须得到有效沟通、监控,并被定期审查和更新;最重要的是,必须有强制手…...
动态规划之01背包问题和完全背包问题
01背包的问题描述:(内容参考代码随想录)有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。问题示例&#…...
MATLAB算法实战应用案例精讲-【图像处理】数字图像灰度化(附Java、python、matlab和opencv代码实现)
目录 前言 几个相关概念 1、RGB 2、ARGB 3、灰度化 4.图像点运算 5.线性点运算...
Linux(强大的yum命令)
yum 读 [jʌm] ,中文谐音: 样安ing。 yum( Yellow dog Updater, Modified)是一个在 Fedora 和 RedHat 以及 SUSE 中的 Shell 前端软件包管理器。 基于 RPM 包管理,能够从指定的服务器自动下载 RPM 包并且安装&#x…...
28.结语
文章目录28 Epilogue 结语28 Epilogue 结语 Don’t let it end like this. Tell them I said something. 不要让它就这样结束。 告诉他们我说了些什么 —Pancho Villa 您已到达旅程的尽头。 辛苦了 我们希望您能从本书中找到一些有价值的收获。 我们建议该列表应包括以下内…...
wordpress中文 apP/线上营销推广方式都有哪些
格式内容清洗 1.格式内容清洗产生的原因 数据是由人工收集或用户填写而来,格式内容可能存在问题不同版本的程序产生的内容或格式不一致不同数据源采集的数据内容和格式定义不一致2.时间、日期格式不一致清洗 根据实际情况,把时间/日期转换成统一的表示方式 日期格式不一致:…...
安装 wordpress多用户/广告投放网
this.p{ m:2,b:2,loftPermalink:,id:fks_087065080095089068082086086065072084084066087087095066082,blogTitle:梯度的极坐标表达式,blogAbstract:\r\n\r\n有同学问:梯度的极坐标表达式是怎么得来的? 下面给出推导详细过程。\r\n\r\n\r\n\r\n\r\n,blog…...
iis新建网站/seo群发软件
在GDB调试程序的时候,如果程序带有很长的参数列表,或者调试命令本身很长,需要频繁启动调试会话时,频繁输入参数或者命令严重拖慢调试节奏,这里记录一个GDB非常有用的参数-x,可以将调试参数和调试命令以调试…...
北京专业做网站推广/百度导航如何设置公司地址
索引是对数据库中的一列或多列进行排序的一种数据结构;下面通过单列索引和多列索引分析什么场景下可以走到索引,什么情况下又不会走到索引在进行查询时,索引列不能是表达式的一部分,也不能是函数的参数,否则无法使用索…...
加强政府网站集群建设/互联网广告平台有哪些
OpenGL 纹理是左下角(0,0) 右上角(1,1)。 需要告诉OpenGL纹理环绕方式,主要有四种:GL_REPEAT(重复纹理图像),GL_MIRRORED_REPEAT(重复纹理图像,但是每次重复图片是镜像放置的), GL_CLAMP_TO_EDGE(坐标再0-1直接,超出部分会重复纹理坐标的边缘,有边缘拉伸效果),GL…...
哪个网站做美食自媒体更好/佛山百度网站排名优化
题目实现一个医院的挂号机系统,要求:有多台挂号机同时运行,此时无论有多少患者挂号,要求都能挂到不同的号码,并且要求实现当意外断电之后,下一次恢复还能从上次结束号码继续挂号?* synchronized* 文件操作…...