Linux学习笔记--UART子系统
set_opt() - 串口参数设置
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 ){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(fd,TCIFLUSH);if((tcsetattr(fd,TCSANOW,&newtio))!=0){perror("com set error");return -1;}//printf("set done!\n");return 0;
}
参数说明:
fd: 串口文件描述符nSpeed: 波特率(2400,4800,9600,115200)nBits: 数据位(7或8)nEvent: 校验位('N'无校验,'O'奇校验,'E'偶校验)nStop: 停止位(1或2)
功能特点:
设置原始模式(非规范模式)
禁用回显和信号处理
配置数据位、停止位、校验位
设置最小读取字节数和超时
open_port() - 打开串口
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;
}
以读写方式打开串口设备
设置为阻塞模式
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, 9600, 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;
}
fd = open_port(argv[1]); // 打开指定的串口设备配置串口参数
iRet = set_opt(fd, 115200, 8, 'N', 1); // 115200,8N1配置数据收发循环
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");
}VMIN和VTIME组合
| VMIN | VTIME | 行为 |
|---|---|---|
| 0 | 0 | 立即返回,读取可用数据 |
| 1 | 0 | 阻塞直到收到1个字节 |
| 1 | 1 | 最多等待0.1秒,收到1个字节立即返回 |
| 0 | 1 | 最多等待0.1秒读取数据 |
读取GPS数据
read_gps_raw_data() - 读取GPS原始数据
int read_gps_raw_data(int fd, char *buf)
{int i = 0;int iRet;char c;int start = 0;while (1){iRet = read(fd, &c, 1); // 每次读取1个字符if (iRet == 1){if (c == '$') // 检测到NMEA语句起始符start = 1;if (start) // 开始记录数据{buf[i++] = c;}if (c == '\n' || c == '\r') // 检测到行结束符return 0;}else{return -1; // 读取失败}}
}工作流程:
逐个字符读取串口数据
遇到
$符号开始记录(NMEA语句起始符)持续记录直到遇到换行符
\n或回车符\r返回完整的NMEA语句
parse_gps_raw_data() - 解析GPS数据
int parse_gps_raw_data(char *buf, char *time, char *lat, char *ns, char *lng, char *ew)
{char tmp[10];if (buf[0] != '$') // 检查起始符return -1;else if (strncmp(buf+3, "GGA", 3) != 0) // 检查是否为GPGGA语句return -1;else if (strstr(buf, ",,,,,")) // 检查数据是否有效{printf("Place the GPS to open area\n");return -1;}else {// 解析逗号分隔的字段sscanf(buf, "%[^,],%[^,],%[^,],%[^,],%[^,],%[^,]", tmp, time, lat, ns, lng, ew);return 0;}
}解析逻辑:
验证数据格式正确性
只处理
GPGGA语句(全球定位系统固定数据)检查数据有效性(避免解析无效定位数据)
使用
sscanf和%[^,]格式提取逗号分隔的字段
NMEA GPGGA数据格式
示例数据:
$GPGGA,082559.00,4005.22599,N,11632.58234,E,1,04,3.08,14.6,M,-5.6,M,,*76
各字段含义:
$GPGGA: 语句头082559.00: UTC时间(08:25:59.00)4005.22599: 纬度(40度05.22599分)N: 北纬11632.58234: 经度(116度32.58234分)E: 东经1: 定位质量指示器04: 使用的卫星数量3.08: 水平精度因子14.6: 海拔高度M: 单位(米)-5.6: 大地水准面高度M: 单位(米)*76: 校验和
经纬度格式转换
原始格式 → 十进制度格式
纬度转换:
/* 纬度格式: ddmm.mmmm */
sscanf(Lat+2, "%f", &fLat); // 提取分钟部分(跳过前2位度数)
fLat = fLat / 60; // 分钟转换为度
fLat += (Lat[0] - '0')*10 + (Lat[1] - '0'); // 加上度数部分示例计算:
/* 经度格式: dddmm.mmmm */
sscanf(Lng+3, "%f", &fLng); // 提取分钟部分(跳过前3位度数)
fLng = fLng / 60; // 分钟转换为度
fLng += (Lng[0] - '0')*100 + (Lng[1] - '0')*10 + (Lng[2] - '0'); // 加上度数原始数据:
4005.22599度数: 40
分钟: 05.22599
转换: 40 + 5.22599/60 = 40.0870998°
UART驱动
static struct uart_port *virt_port; // 虚拟串口端口
static unsigned char txbuf[1024]; // 发送缓冲区
static int tx_buf_r = 0; // 缓冲区读指针
static int tx_buf_w = 0; // 缓冲区写指针UART驱动结构体
static struct uart_driver virt_uart_drv = {.owner = THIS_MODULE,.driver_name = "VIRT_UART", // 驱动名称.dev_name = "ttyVIRT", // 设备节点名称(/dev/ttyVIRT).major = 0, // 动态分配主设备号.minor = 0, // 动态分配次设备号.nr = 1, // 支持1个端口
};UART操作函数集
static const struct uart_ops virt_pops = {.tx_empty = virt_tx_empty, // 检查发送是否完成.start_tx = virt_start_tx, // 启动发送.set_termios = virt_set_termios, // 设置终端参数
};核心函数
1. virt_tx_empty() - 发送空检查
static unsigned int virt_tx_empty(struct uart_port *port)
{return 1; // 总是返回"空",因为数据瞬间存入buffer
}这个函数告诉上层数据已经发送完成
对于虚拟设备,数据立即进入缓冲区,所以总是返回"空"
2. virt_start_tx() - 启动发送
static void virt_start_tx(struct uart_port *port)
{struct circ_buf *xmit = &port->state->xmit;while (!uart_circ_empty(xmit) && !uart_tx_stopped(port)) {// 从环形缓冲区读取数据到发送缓冲区txbuf[tx_buf_w++] = xmit->buf[xmit->tail];xmit->tail = (xmit->tail + 1) & (UART_XMIT_SIZE - 1);port->icount.tx++; // 更新发送计数}if (uart_circ_chars_pending(xmit) < WAKEUP_CHARS)uart_write_wakeup(port); // 唤醒等待的写进程
}关键概念说明:
环形缓冲区 (circ_buf):
xmit->buf: 缓冲区数组xmit->head: 写指针xmit->tail: 读指针UART_XMIT_SIZE: 通常为4096,缓冲区大小
发送流程:
检查环形缓冲区是否有数据
将数据从环形缓冲区复制到虚拟发送缓冲区
更新读指针(环形缓冲区)
更新写指针(发送缓冲区)
统计发送字节数
3. virt_set_termios() - 设置终端参数
static void virt_set_termios(struct uart_port *port, struct ktermios *termios,struct ktermios *old)
{return; // 虚拟设备,忽略参数设置
}平台设备驱动实现
1. 探测函数
static int virtual_uart_probe(struct platform_device *pdev)
{ int rxirq;// 从设备树获取中断号(虚拟设备可能不需要)rxirq = platform_get_irq(pdev, 0);// 分配uart_port结构体virt_port = devm_kzalloc(&pdev->dev, sizeof(*virt_port), GFP_KERNEL);// 初始化uart_portvirt_port->dev = &pdev->dev;virt_port->iotype = UPIO_MEM; // 内存映射IOvirt_port->irq = rxirq; // 中断号virt_port->fifosize = 32; // FIFO大小virt_port->ops = &virt_pops; // 操作函数集virt_port->flags = UPF_BOOT_AUTOCONF; // 自动配置标志// 注册UART端口return uart_add_one_port(&virt_uart_drv, virt_port);
}2. 设备树匹配
static const struct of_device_id virtual_uart_of_match[] = {{ .compatible = "100ask,virtual_uart", }, // 设备树兼容性字符串{ },
};模块初始化和退出
1. 初始化函数
static int __init virtual_uart_init(void)
{ int ret;printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);// 1. 注册UART驱动ret = uart_register_driver(&virt_uart_drv);if (ret)return ret;// 2. 注册平台驱动return platform_driver_register(&virtual_uart_driver);
}驱动工作流程
用户空间 → 内核空间的数据流
用户写入数据
echo "hello" > /dev/ttyVIRTTTY子系统处理
数据通过
tty_write()进入TTY核心调用UART驱动的写函数
UART驱动处理
数据存入环形缓冲区
xmit调用
virt_start_tx()启动发送数据从环形缓冲区复制到
txbuf
用户空间|v
tty_struct (TTY核心)|v
uart_state (UART状态)|-- xmit (环形缓冲区)|v
uart_port (UART端口)|-- ops->start_tx() // 调用virt_start_tx()|v
txbuf[1024] // 虚拟发送缓冲区
/{virtual_uart: virtual_uart_100ask {compatible = "100ask,virtual_uart";interrupt-parent = <&intc>;interrupts = <GIC_SPI 99 IRQ_TYPE_LEVEL_HIGH>;};};
控制台支持
1. 控制台支持 (Console Support)
控制台结构体定义
static struct console virt_uart_console = {.name = "ttyVIRT", // 控制台名称.write = virt_uart_console_write, // 控制台写入函数.device = virt_uart_console_device, // 控制台设备函数.flags = CON_PRINTBUFFER, // 控制台标志.index = -1, // 自动分配索引.data = &virt_uart_drv, // 关联的UART驱动
};控制台写入函数
static void virt_uart_console_write(struct console *co, const char *s, unsigned int count)
{int i;for (i = 0; i < count; i++)if (txbuf_put(s[i]) != 0) // 将控制台输出存入发送缓冲区return;
}控制台设备函数
struct tty_driver *virt_uart_console_device(struct console *co, int *index)
{struct uart_driver *p = co->data;*index = co->index;return p->tty_driver; // 返回对应的tty驱动
}在UART驱动中注册控制台
static struct uart_driver virt_uart_drv = {// ... 其他字段.cons = &virt_uart_console, // 注册控制台
};2. 完整的UART操作函数集
UART操作函数
static int virt_startup(struct uart_port *port)
{return 0; // 启动UART端口
}static void virt_set_mctrl(struct uart_port *port, unsigned int mctrl)
{// 设置调制解调器控制线(空实现)
}static unsigned int virt_get_mctrl(struct uart_port *port)
{return 0; // 获取调制解调器控制线状态
}static void virt_stop_tx(struct uart_port *port)
{// 停止发送(空实现)
}static void virt_stop_rx(struct uart_port *port)
{// 停止接收(空实现)
}static void virt_shutdown(struct uart_port *port)
{// 关闭UART端口(空实现)
}static const char *virt_type(struct uart_port *port)
{return "100ASK_VIRT_UART"; // 返回驱动类型名称
}
static const struct uart_ops virt_pops = {.tx_empty = virt_tx_empty,.set_mctrl = virt_set_mctrl,.get_mctrl = virt_get_mctrl,.stop_tx = virt_stop_tx,.start_tx = virt_start_tx,.stop_rx = virt_stop_rx,.startup = virt_startup,.shutdown = virt_shutdown,.set_termios = virt_set_termios,.type = virt_type,
};3. 改进的中断处理函数
使用tty_port结构
static irqreturn_t virt_uart_rxint(int irq, void *dev_id)
{struct uart_port *port = dev_id;struct tty_port *tport = &port->state->port; // 获取tty_portunsigned long flags;int i;spin_lock_irqsave(&port->lock, flags);for (i = 0; i < rx_buf_w; i++) {port->icount.rx++;/* 使用tty_port而不是uart_port */tty_insert_flip_char(tport, rxbuf[i], TTY_NORMAL);}rx_buf_w = 0;spin_unlock_irqrestore(&port->lock, flags);tty_flip_buffer_push(tport); // 推送数据到tty_portreturn IRQ_HANDLED;
}4. 改进的端口配置
端口类型和基地址设置
virt_port->flags = 0; // 清除自动配置标志
virt_port->type = PORT_8250; // 设置为8250兼容类型
virt_port->iobase = 1; /* 为了让uart_configure_port能执行 */完整的数据流分析
1. 内核打印输出流
printk() 或内核日志↓
console_write() [控制台核心]↓
virt_uart_console_write() [虚拟控制台]↓
数据存入txbuf环形缓冲区↓
用户读取/proc/virt_uart_buf获取内核输出2. 应用程序数据流
应用程序写入/dev/ttyVIRT↓
tty_write() [TTY核心]↓
uart_write() [串口核心]↓
数据存入circular buffer (xmit)↓
virt_start_tx()被调用↓
数据从xmit复制到txbuf环形缓冲区3. 接收数据流
用户写入/proc/virt_uart_buf↓
数据存入rxbuf接收缓冲区↓
模拟产生RX中断↓
virt_uart_rxint()中断处理函数↓
数据从rxbuf推送到TTY flip buffer↓
tty_flip_buffer_push()通知TTY核心↓
应用程序从/dev/ttyVIRT读取数据