一、概述:认识 UART 串口通信

1. 什么是 UART?

UART(Universal Asynchronous Receiver/Transmitter,通用异步收发器),又称串行通讯接口(Serial)或简称串口,是一种在两个设备间实现单工、半双工或全双工通信的技术。其核心特点是串行数据传输(数据以比特为单位逐位发送 / 接收)和异步通信(无需共享时钟信号,通过约定参数保持同步)。

UART 通信的基本信号包括:

  • TX(发送端):用于发送数据
  • RX(接收端):用于接收数据
  • Ground(接地):保持两个设备的公共参考电位

2. 关键通信参数

UART 通信双方必须配置相同的参数才能正常工作,主要包括:

  • 波特率:通信速率(如 9600 bps,即每秒传输 9600 比特)
  • 起始位:标记数据帧的起始(1 位)
  • 数据位:有效数据长度(常见 8 位或 9 位)
  • 奇偶校验位:错误检测机制(无校验、偶校验、奇校验)
  • 停止位:标记数据帧结束(1 位或 2 位)

UART 帧结构如下:

在这里插入图片描述

3. openvela 中的 UART 实现

在 openvela 系统中,上层应用操作串口的流程分为:

  • 上层接口调用(Upper Half):应用程序通过uart_open函数打开串口
  • 底层适配层处理(Lower Half)
    • setup函数:配置波特率、数据位等参数
    • attach函数:设置硬件中断处理

芯片厂商需适配南向接口(Lower Half),确保setup和attach函数正常执行,上层应用即可通过北向接口(Upper Half) 控制串口功能。

芯片厂商需适配南向接口(Lower Half),确保setupattach函数正常执行,上层应用即可通过北向接口(Upper Half) 控制串口功能。

二、驱动适配:在 openvela 中实现 UART 驱动

1. 适配步骤

(1)使能配置

确保启用CONFIG_SERIAL配置选项,UART 接口定义位于nuttx/include/nuttx/serial/serial.h

(2)实现驱动操作接口

定义并实现struct uart_ops_s结构体,该结构体包含 UART 底层操作的所有方法。

(3)驱动注册

up_initialize函数中调用xxx_serialinit(xxx 为体系架构,如 arm)

xxx_serialinit中调用uart_register(FAR const char *path, FAR uart_dev_t *dev)将驱动注册到系统

(4)实现设备数据结构

实现struct uart_dev_s数据结构,其中ops成员指向上述struct uart_ops_s

若支持 DMA,需额外适配 DMA 相关结构

(5)缓冲区配置

struct uart_dev_s中实现xmit(发送缓冲区)和recv(接收缓冲区),用于数据交互。

2. 核心数据结构解析

(1)struct uart_dev_sUART 设备上下文

该结构描述串口硬件的状态和功能,核心字段包括:

struct uart_dev_s
{
  uint8_t              open_count;   // 设备打开次数
  volatile bool        xmitwaiting;  // 等待发送缓冲区空间
  volatile bool        recvwaiting;  // 等待接收缓冲区数据
  sem_t                xmitsem;      // 发送同步信号量
  sem_t                recvsem;      // 接收同步信号量
  struct uart_buffer_s xmit;         // 发送缓冲区
  struct uart_buffer_s recv;         // 接收缓冲区
  FAR const struct uart_ops_s *ops;  // 操作接口集
  FAR void            *priv;         // 私有数据(硬件地址等)
  // ... 其他字段
};
  • ops:指向操作接口集,是驱动功能的核心实现
  • priv:存储硬件特定信息(如寄存器地址、FIFO 指针)
  • xmit/recv:发送 / 接收缓冲区,是数据交互的核心

(2)struct uart_ops_s:底层操作接口

该结构定义 UART 的底层操作方法,核心接口如下:

接口 功能描述 应用场景
setup() 配置波特率、数据位等硬件参数 设备首次打开时
shutdown() 禁用串口,释放资源 设备关闭时
attach() 绑定中断服务函数 启用中断驱动时
detach() 解绑中断服务函数 停用中断驱动时
ioctl() 执行特殊命令操作 非标准化控制需求
receive() 从硬件接收数据 读取设备数据
rxint() 开启 / 关闭接收中断 中断接收控制
rxavailable() 检查接收缓冲区是否有数据 判断是否需要读取数据
send() 发送数据到硬件 写入数据到设备
txint() 开启 / 关闭发送中断 中断发送控制
txready() 检查硬件是否准备好发送 发送数据前查询
txempty() 检查是否所有数据已发送 确保发送完成(如关闭前)

DMA 相关接口(硬件支持时):

  • dmasend():使用 DMA 发送数据
  • dmareceive():使用 DMA 接收数据
  • dmarxfree():通知 DMA 接收缓冲区有空间
  • dmatxavail():通知 DMA 有数据待发送

三、UART 字符设备驱动的使用流程

1. 驱动注册:uart_register

通过register_driver函数将 UART 驱动注册为系统设备节点,主要完成:

int register_driver(FAR const char *path,
                    FAR const struct file_operations *fops,
                    mode_t mode, FAR void *priv)
  • 查找或创建设备节点(inode)
  • 绑定文件操作集(struct file_operations)
  • 设置私有数据(通常为uart_dev_t *)

对应的简化流程图如下:
在这里插入图片描述

2. 文件操作集:struct file_operations

典型实现如下:

static const struct file_operations g_serialops =
{
  uart_open,  /* 打开设备 */
  uart_close, /* 关闭设备 */
  uart_read,  /* 读取数据 */
  uart_write, /* 写入数据 */
  0,          /* seek(未实现) */
  uart_ioctl, /* 控制操作 */
  uart_poll   /* 轮询操作 */
};

3. 核心操作流程解析

(1)uart_open:打开设备

流程:

nx_vopen -> file_vopen -> uart_open
  -> dev->ops->setup()  // 硬件参数配置
  -> uart_attach()      // 绑定中断(如irq_attach)
  -> uart_dmarxfree()   // 若支持DMA,配置接收

(2)uart_read:读取数据

流程:

fs/vfs/fs_read.c
|_ nx_read
   |_ file_read
      |_ inode->u.i_ops->read == uart_read(filep, buffer, buflen)
         |_ if (rxbuf->head != tail)    //如果读缓冲区里有数据就直接读走了
            |_ ch = rxbuf->buffer[tail];
            |_ *buffer++ = ch;
         |_ else
            |_ uart_dmarxfree/uart_enablerxint //如果存在读dma的话触发该操作,否则打开读中断
            |_ dev->recvwaiting = true;
            |_ ret = nxsem_wait(&dev->recvsem);  //阻塞的read
         |_ uart_enablerxint   // == dev->ops->rxint  , rxint调用的地方
         |_ uart_dmarxfree(dev);  //在读操作完成之后缓冲区肯定是存在空间的,如果存在读dma触发该操作

(3)uart_write:写入数据

流程:

fs/vfs/fs_write.cwrite
|_ nx_write
   |_ file_write
      |_ inode->u.i_ops->write(filep, buf, nbytes) == uart_write
         |_ uart_disabletxint  //txint调用的地方 
         |_ uart_putxmitchar(dev, ch, oktoblock); //把数据往缓冲区放。这个函数也会判断是否使用写dma完成发送,uart_dmatxavail。
            |_ if (nexthead != dev->xmit.tail) //如果写缓冲区有空间,把数据放入写缓冲区后返回
            |_ else //开中断,等待中断给自己腾出写空间把自己唤醒
               |_ uart_enabletxint(dev);   //txint调用的地方
               |_ nxsem_wait(&dev->xmitsem);
         #ifdef CONFIG_SERIAL_TXDMA
         |_ uart_dmatxavail(dev); //如果有写dma的话,使用dma完成发送
         #endif
         |_ uart_enabletxint(dev) == esp32c3_txint  //如果使用了上面的dma的话,这个函数会直接返回,而不产生发送数据的动作

(4)uart_poll:轮询操作

等待机制:初始化 pollfd 列表,关联驱动 poll 方法,可阻塞等待事件

fs/vfs/fs_poll.c
|_ poll_setup(kfds, nfds, &sem);
   |_ poll_fdsetup(fds[i].fd, &fds[i], true)
      |_ file_poll(filep, fds, setup);
         |_ inode->u.i_ops->poll(filep, fds, setup); == uart_poll
   |_ if(timeout<0) 
      |_ nxsem_wait(&sem);//阻塞的poll
  • 唤醒机制:
    • 写事件:发送缓冲区数据发送完成时,通过poll_notify唤醒
    • 读事件:接收缓冲区有数据时,通过poll_notify唤醒

以下为 uart_poll 的典型中断处理流程:

arch/risc-v/src/esp32c3/esp32c3_serial.c
|_ int_status = getreg32(UART_INT_ST_REG(priv->id));
|_ if (int_status & tx_mask)
   |_ uart_xmitchars
      |_ while (dev->xmit.head != dev->xmit.tail && uart_txready(dev)) // txready调用的地方
         |_ uart_send(dev, dev->xmit.buffer[dev->xmit.tail])  //send调用的地方,发完缓冲区里所有数据
         |_ uart_datasent
            |_ poll_notify(dev->fds, CONFIG_SERIAL_NPOLLWAITERS, POLLOUT)//唤醒poll等待的写者
            |_ nxsem_post(&dev->xmitsem) //唤醒write等待的写者
|_ if (int_status & rx_mask)
   |_ uart_recvchars
      |_ uart_rxavailable(dev)            //rxavailable调用的地方
      |_ uart_rxflowcontrol               //rxflowcontrol调用的地方
      |_ ch = uart_receive(dev, &status)  //receive调用的地方
      |_ uart_check_special(dev, &ch, 1)  //处理control character的地方
      |_ uart_datareceived(dev)          
         |_ poll_notify(dev->fds, CONFIG_SERIAL_NPOLLWAITERS, POLLIN); 
            |_ fds->cb(fds); == poll_default_cb
               |_ nxsem_post(pollsem)    //如果有阻塞的poll,唤醒
         |_ nxsem_post(&dev->recvsem);   //如果有阻塞的read,唤醒
      |_ nxsig_kill(dev->pid, signo);    //如果signo!=0,发signal的地方

(5)uart_close:关闭设备

流程:

fs/inode/fs_files.c
int close(int fd)
|_ nx_close(fd)
   |_ file_close(&file)
      |_ inode->u.i_ops->close(filep)  //== uart_close
         |_ uart_disablerxint(dev)
         |_ if ((filep->f_oflags & O_NONBLOCK) == 0)
            |_ uart_tcdrain  //把发送缓冲区和硬件fifo里的数据都发送完成
               |_ uart_txempty //txempty调用的地方
         |_ uart_detach(dev)       //detach调用的地方
         |_ if (!dev->isconsole)
            |_ uart_shutdown(dev)  //shutdown调用的地方
         |_ uart_datareceived(dev)

四、操作 UART 设备节点

  1. POSIX 标准接口

通过标准文件操作接口访问 UART 设备节点(如/dev/console、/dev/ttyS0):

int fd = open("/dev/console", O_RDWR);    // 打开设备
int ret = read(fd, buf, count);           // 读取数据
int ret = write(fd, buf, count);          // 写入数据
int poll(struct pollfd *fds, nfds_t nfds, int timeout);  // 轮询事件
int ioctl(int fd, unsigned long request, ...);  // 控制操作
close(fd);                                // 关闭设备
  1. 使用ioctl配置 UART 参数

通过struct termios结构体配置参数,定义如下:

struct termios
{
  tcflag_t  c_iflag;   // 输入模式
  tcflag_t  c_oflag;   // 输出模式
  tcflag_t  c_cflag;   // 控制模式
  tcflag_t  c_lflag;   // 本地模式
  cc_t      c_cc[NCCS];// 控制字符
  speed_t   c_speed;   // 波特率(非POSIX标准)
};

示例:设置波特率

struct termios term;
ioctl(fd, TCGETS, &term);       // 获取当前配置
cfsetspeed(&term, B9600);       // 设置波特率为9600
ioctl(fd, TCSETS, &term);       // 应用配置

五、测试用例验证

1. 测试程序配置

  • 源码路径:apps/testing/drivertest

在这里插入图片描述

  • 生成程序:cmocka_driver_uart
  • 需启用配置选项:
CONFIG_TESTING_CMOCKA=y
CONFIG_TESTING_DRIVER_TEST=y
CONFIG_TESTING_DRIVER_TEST_SIMPLE=y

2. 核心测试用例

  • write_default:验证写入功能,发送字符序列 0-9、a-z 等
  • read_default:验证读取功能,检查接收数据与发送数据一致性
  • burst_test:模拟快速数据传输,测试高负载场景

3. 运行示例

在这里插入图片描述

ap> cmocka_driver_uart -d /dev/console
[5.701000] [25] INFO: Running 3 test(s).
[5.701100] [25] INFO: [RUN] write default
[5.701300] [25] INFO: [OK] write default
[5.701300] [25] INFO: [RUN] read default
[14.383300] [25] INFO: [OK] read default
[14.383400] [25] INFO: [RUN] burst test
[22.179700] [25] INFO: [OK] burst test
[22.179900] [25] INFO: 3 test(s) passed.

总结

本文详细介绍了在 openvela 系统中进行 UART 驱动适配的完整流程,包括核心数据结构、驱动实现步骤、设备操作流程及测试验证方法。通过遵循本文的指南,开发者可快速完成 UART 驱动的适配与调试,确保串口通信功能的稳定可靠。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐