openvela之串口驱动测试
摘要:本文详细介绍了UART串口通信技术及其在openvela系统中的实现方法。UART作为异步串行通信接口,通过TX/RX信号线实现设备间数据传输,核心参数包括波特率、数据位等。在openvela中,UART驱动分为上层接口调用和底层适配层,芯片厂商需实现uart_ops_s操作集和uart_dev_s设备结构体。文章还解析了驱动注册流程、文件操作集以及核心的open/read/write操作流
一、概述:认识 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),确保setup
和attach
函数正常执行,上层应用即可通过北向接口(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_s
:UART 设备上下文
该结构描述串口硬件的状态和功能,核心字段包括:
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 设备节点
- 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); // 关闭设备
- 使用
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 驱动的适配与调试,确保串口通信功能的稳定可靠。

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