一般情况下,设备间的通信方式可以划分为串行通行方式和并行通信方式两种。在linux字符设备、块设备、网络设备分类方式下,该外设分类划分于字符设备当中。本章节主要指导基于LINUX驱动完成串口驱动开发并调用串口与USB接口与外设完成有效通信。
按照数据传输的方向可以划分为 单工,半双工和全双工。单工通信允许数据在同一方向上进行传输,半双工则允许数据双向传输但是在同一时刻仅允许一个方向的数据传输吗,不需要独立的接收端和放松端,两者可以合并使用相同端口。全双工通信则包含两个方向上的同时传输,全双工通信是两个半双工的通信方式的拼接,从而完成的独立接收端和发送端。
而按照通信方式的不同,可以划分为同步通信和异步通信两种,同步通信是需要带时钟信号进行互相时钟同步从而解析电平信号的,如SPI,IIC,而异步通信是无需时钟同步信号的,如UART等。
在同步通讯中,收发设备的上方会使用一根信号线传输信号,在时钟信号的驱动下双方进行数据的同步,通常会在收发两端规定在时钟信号的上升沿和下降沿对数据线进行采样。
在异步通讯中,不适用时钟信号进行数据同步,直接在数据信号中穿插一些用于数据同步的信号位,或通过指定数据协议进行数据打包,以数据帧的方式传输数据,通讯中需要约束传输速率波特率,常见波特率有 4800 9600 115200等。
存在两个引脚:
在连接时如图,两个芯片的GND引脚共地。
在嵌入式开发领域通常描述串口按照电平标准划分由USB设备,RS485,RS-422,D-USB接口为主流的差分电平信号,双端电平信号包括LVDS,LVPECL等。另外一类是单片机上使用为主的单端信号,其传输电平标准为TTL,RS-232,CMOS等。普通单端信号无法连接差分信号,如上文中描述的Tx,Rx 传输的TTL电平信号无法连接LVDS信号,在使用时需要使用到转换模块。
本文中将会以讲解USB接口在Linux驱动中的使用,以及一些单端信号的使用为主。
在标准系统使用的开发板上包括了RS-485和USB2.0,USB3.0接口。
单端UART全称 通用异步收发传输器,是一种串行异步收发协议。UART的工作原理是将数据的二进制格式数据帧一位一位进行传输,在UART中使用TTL电平为主,在阈值电平以上规定为高电平1,阈值电平以下规定为低电平0.
关于串口传输速率: bps就是比特每秒,115200bps就是每秒传输115200比特(115200bit),1kb=1024bit。注意,大写的B表示字节,1[Byte]=8bit。或者说1B=8b.所以115200bps=每秒112.5kb=每秒14.0625kB。
USB,是英文Universal Serial Bus(通用串行总线)的缩写,是一个外部总线标准,用于规范电脑与的连接和通讯。是应用在[PC]领域的接口技术。
USB的电源线是5V,为USB设备提供最大500mA的电流,它与数据线上的电平无关,数据线是差分信号,通常D+和D-在+400mV~-400mV间变化,在传统的单端(Single-ended)通信中,一条线路来传输一个比特位。高电平表示1,低电平表示0。倘若在数据传输过程中受到干扰,高低电平信号完全可能因此产生突破临界值的大幅度扰动,一旦高电平或低电平信号超出临界值,信号就会出错。在差分传输电路中,输出电平为正电压时表示逻辑“1”,输出负电压时表示逻辑“0”,而输出“0”电压是没有意义的,它既不代表“1”,也不代表“0”。而差分通信中,干扰信号会同时进入相邻的两条信号线中,在信号接收端,两个相同的干扰信号分别进入差分放大器的两个反相输入端后,输出电压为0。所以说,差分信号技术对干扰信号具有很强的免疫力。对于串行传输来说,LVDS能够低于外来干扰;而对于并行传输来说,LVDS可以不仅能够抵御外来干扰,还能够抵御数据传输线之间的串扰。因为上述原因,实际电路中只要使用低压差分信号(Low Voltage Differential Signal,LVDS),350mV左右的振幅便能满足近距离传输的要求。假定负载电阻为100Ω,采用LVDS方式传输数据时,如果双绞线长度为10m,传输速率可达400 Mbps;当电缆长度增加到20m时,速率降为100 Mbps;而当电缆长度为100m时,速率只能达到10 Mbps左右。
基本串口驱动程序实现思路从底层机制大体有两种一种是通过轮训机制,不断访问串口从而实现数据的收发,但是会导致cpu占用过高,第二种是使用中断或者DMA等技术实现串口的非实时读取,但是可以保证cpu占用率低并且保证数据有效。
在上层应用层开发过程中有串口通信协议,需要进行校验位,数据位等需要进行规定。
总体上开发过程分为四步:
通常使用数据协议表格可以简单表示如下表
数据帧内容 |
长度 |
功能 |
起始位 |
1位 |
标志帧的起始 |
数据位 |
8位 (有时描述为9位) |
传输数据 |
校验位 |
无校验(1位奇校验/偶校验) |
校验本帧数据正确性和完整性 |
停止位 |
1 (0.5 、1、 1.5、 2) |
标志帧的结束 |
除了上述数据协议在通信双方需要完全一致外,还需要保证数据的传输速率一致,即波特率一致,波特率(Baud rate)是一种衡量数字通信中数据传输速率的单位,通常以每秒钟传输的比特数(bit per second,bps)为单位。它指的是在数字通信中每秒钟传输的符号数,每个符号可以携带多个比特的信息。
在串行通信中,波特率是指在传输数据时,串行线路上数据变化的速率。例如,一个波特率为9600 bps的串行通信系统,可以在一秒钟内传输9600个符号,每个符号可以携带多个比特的信息。波特率是通过调整串行通信系统中时钟信号的频率来实现的。因此,波特率也可以理解为时钟频率的一种体现。和时钟周期成倒数关系,总线时钟周期越短,单位时间传输的码元越多,串口波特率越高。
需要注意的是,波特率并不等同于数据传输速率(data rate),因为每个符号可以携带多个比特的信息。例如,一个波特率为9600 bps的串行通信系统,每个符号可以携带8个比特的信息,因此其数据传输速率为9600 bps × 8 = 76800 bps。
常见的有 115200,38400,9600,4800等。
常见的中断在前面的讲解中提到过包括定时器中断,外部硬件中断,系统异常中断,系统调用中断,信号中断,NMI中断,虚拟中断等,本节讨论的串口收发会涉及到的中断类型包括接收中断和空闲中断。在大类上归属于外部硬件中断。
使用LINUX依据空闲中断和接收中断实现串口收发的基本逻辑如下
打开串口操作会返回一个文件描述符,之后我们需要使用该文件描述符对串口进行读写操作。配置串口参数的步骤会设置串口的输入输出波特率、数据位、停止位和校验位等参数,以保证通信的正确性和稳定性。
接下来,串口硬件将接收到的数据存储在接收缓冲区中,并向内核发出中断信号。中断处理函数根据中断类型(接收中断或空闲中断)选择相应的处理方式。接收中断处理函数会将数据从接收缓冲区中读取并存储到tty缓冲区中,然后向应用程序发送SIGIO信号通知有数据可读。应用程序监听SIGIO信号并从tty缓冲区中读取数据进行处理。空闲中断处理函数类似,不同之处在于它不需要从接收缓冲区中读取数据,而是在空闲状态下触发中断并向应用程序发送SIGIO信号。
如果对比于STM32单片机实现的逻辑可能更易于理解。
中断处理函数的名称不同:Linux使用的是irq函数,而STM32使用的是HAL_UART_IRQHandler函数。STM32的中断处理函数包含了发送中断和接收中断,需要在处理函数内部进行区分,而Linux中的发送和接收分别有对应的中断处理函数。在Linux中,可以通过tty设备文件直接访问串口,而STM32需要使用串口API进行访问和操作。STM32需要手动开启和关闭中断,而Linux的中断处理函数会在内核中自动启动和停止。Linux中,数据的接收和发送是由tty设备驱动完成的,而STM32需要在中断处理函数内部实现数据的接收和发送。两者关键差异是LINUX使用内核管理中断函数的启停。
以下给出一种示例程序可以根据需要进行修改编译合入内核实现串口驱动。
#include <linux/module.h>
#include <linux/init.h>
#include <linux/serial_core.h>
#include <linux/serial.h>
#include <linux/tty.h>
#include <linux/tty_flip.h>
#define DRIVER_NAME "my_serial_driver"
static struct uart_driver my_uart_driver = {
.owner = THIS_MODULE,
.driver_name = DRIVER_NAME,
.dev_name = "ttyMY", // 设备文件名,例如 /dev/ttyMY0
.major = 0, // 自动分配主设备号
.minor = 0, // 自动分配从设备号
.nr = 1, // 支持的最大串口数量
};
// 串口 probe 函数,用于初始化串口参数和注册串口设备
static int my_serial_probe(struct uart_port *port)
{
// 设置串口参数
port->ops = &my_uart_driver.ops;
port->type = PORT_16550A;
port->iotype = UPIO_MEM;
port->ioport = 0x3f8; // 串口的 I/O 端口地址
port->irq = 4; // 串口的中断号
port->flags = UPF_BOOT_AUTOCONF;
return uart_add_one_port(&my_uart_driver, port); // 注册串口设备
}
// 串口 remove 函数,用于注销串口设备
static void my_serial_remove(struct uart_port *port)
{
uart_remove_one_port(&my_uart_driver, port); // 注销串口设备
}
// 串口操作函数表,这里只需要实现 probe 和 remove 函数
static struct uart_ops my_uart_ops = {
.tx_empty = NULL,
.set_mctrl = NULL,
.get_mctrl = NULL,
.stop_tx = NULL,
.start_tx = NULL,
.send_xchar = NULL,
.stop_rx = NULL,
.enable_ms = NULL,
.break_ctl = NULL,
.startup = NULL,
.shutdown = NULL,
.flush_buffer = NULL,
.set_termIOS = NULL,
.type = NULL,
.release_port = NULL,
.request_port = NULL,
.config_port = NULL,
.verify_port = NULL,
.ioctl = NULL,
.send_xchar_locked = NULL,
};
// 模块初始化函数,在这里注册串口驱动
static int my_serial_init(void)
{
int ret = 0;
// 注册串口驱动
ret = uart_register_driver(&my_uart_driver);
if (ret) {
printk(KERN_ERR "Failed to register UART drivern");
return ret;
}
// 设置串口操作函数表中的 probe 和 remove 函数
my_uart_ops.probe = my_serial_probe;
my_uart_ops.remove = my_serial_remove;
my_uart_driver.ops = my_uart_ops;
return ret;
}
// 模块卸载函数,在这里注销串口驱动
static void my_serial_exit(void)
{
uart_unregister_driver(&my_uart_driver);
}
module_init(my_serial_init);
module_exit(my_serial_exit);
MODULE_LICENSE("GPL");
驱动可以通过makefile编译为.ko文件后通过insmod合入内核。
串口驱动程序在新的板卡上通常由厂家进行设备树适配和驱动开发,在实际使用案例当中需要熟练掌握通过文件描述符合tty层调用串口驱动即可。以下展示串口驱动的调用方式
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#define DEVICE "/dev/ttyMY0"
int main()
{
int fd = 0;
struct termios tio;
char buf[256];
// 打开设备文件
fd = open(DEVICE, O_RDWR | O_NOCTTY | O_NDELAY);
if (fd < 0) {
perror("open");
return -1;
}
// 设置串口参数
tcgetattr(fd, &tio);
tio.c_iflag = IGNBRK | IGNPAR;
tio.c_oflag = 0;
tio.c_cflag = CS8 | CREAD | CLOCAL;
tio.c_lflag = 0;
tio.c_cc[VTIME] = 0;
tio.c_cc[VMIN] = 1;
cfsetispeed(&tio, B9600);
cfsetospeed(&tio, B9600);
tcsetattr(fd, TCSANOW, &tio);
// 读取串口数据
printf("Reading from serial port...n");
while (1) {
int n = read(fd, buf, sizeof(buf));
if (n > 0) {
buf[n] = '