4. NuttX串口驱动框架:uart_register()与uart_register_dev()、串口设备结构体定义、驱动分层(上/下/硬件层)
好,咱们今天聊点硬核的。NuttX的串口驱动,说白了就是一套分层清晰的“流水线”。我刚开始接触NuttX时,第一反应是:这玩意儿怎么比Linux还绕?但真正用起来才发现,这种分层设计,恰恰是它能在资源受限的MCU上跑得稳的关键。
今天咱们就拆开看看,这套框架到底是怎么搭起来的。重点就三个东西:注册函数、设备结构体、驱动分层。搞懂了这些,你写自己的串口驱动,心里就有谱了。
4.1 驱动分层:上/下/硬件层
NuttX的串口驱动,不是一锅粥。它分了三层,各干各的活。我画了张图,你一看就明白。
嗯,这张图就是咱们今天的主线。上层是“面子”,下层是“里子”,硬件层是“底子”。
4.2 串口设备结构体定义
在NuttX里,每个串口设备都对应一个结构体。这个结构体,就是驱动的心脏。我习惯叫它 uart_dev_s,但不同芯片厂商可能会包一层自己的结构体。
咱们直接看代码,这是NuttX官方定义的核心结构体:
struct uart_dev_s
{
/* 公共部分 */
int isconsole; /* 是不是控制台? */
uint8_t open_count; /* 打开次数计数 */
sem_t closelock; /* 关闭锁 */
/* 接收端 */
struct uart_buffer_s *recv; /* 接收缓冲区 */
struct uart_buffer_s *xmit; /* 发送缓冲区 */
/* 下层操作函数集 */
const struct uart_ops_s *ops; /* 重点!下层操作函数指针 */
/* 私有数据 */
FAR void *priv; /* 指向硬件层私有数据 */
};
这里有个关键点:ops 指针。它指向一个 uart_ops_s 结构体,里面全是函数指针。说白了,上层调用的所有底层操作,都是通过这个指针跳转的。
那 uart_ops_s 长什么样?我截取一部分:
struct uart_ops_s
{
int (*setup)(FAR struct uart_dev_s *dev);
void (*shutdown)(FAR struct uart_dev_s *dev);
int (*attach)(FAR struct uart_dev_s *dev);
void (*detach)(FAR struct uart_dev_s *dev);
int (*ioctl)(FAR struct uart_dev_s *dev, int cmd, unsigned long arg);
int (*receive)(FAR struct uart_dev_s *dev, unsigned int *status);
void (*send)(FAR struct uart_dev_s *dev, int ch);
bool (*rxflow)(FAR struct uart_dev_s *dev, bool enable);
/* ... 还有更多 */
};
你看,setup 负责初始化硬件,send 负责发一个字节,receive 负责收一个字节。每个函数都对应一个硬件操作。我在移植STM32串口时,主要就是填这个结构体。
uart_ops_s 时,别急着把所有函数都实现。先实现 setup、send、receive 这三个,能跑通再说。其他的像 ioctl、rxflow,可以后面慢慢补。我刚开始做时,一口气写了十几个函数,结果调试时发现 setup 里波特率算错了,白忙活半天。
4.3 uart_register() 与 uart_register_dev()
结构体定义好了,怎么把它注册到系统里?NuttX提供了两个注册函数,很多人搞混它们。我当年也踩过这个坑。
4.3.1 uart_register()
这个函数是“老版本”的注册方式。它直接创建一个字符设备节点,比如 /dev/ttyS0。原型如下:
int uart_register(FAR const char *path, FAR struct uart_dev_s *dev);
用法很简单:
struct uart_dev_s g_uart1_dev;
/* ... 初始化结构体 ... */
uart_register("/dev/ttyS0", &g_uart1_dev);
调用完,系统里就有了 /dev/ttyS0,应用程序可以 open() 它。
4.3.2 uart_register_dev()
这个函数是“新版本”,NuttX 7.x 以后引入的。它更灵活,支持动态分配设备号。原型:
int uart_register_dev(FAR const char *path, FAR struct uart_dev_s *dev);
用法几乎一样:
uart_register_dev("/dev/ttyS0", &g_uart1_dev);
那区别在哪?我直接说重点:
| 对比项 | uart_register() | uart_register_dev() |
|---|---|---|
| 引入版本 | 旧版(NuttX 7.x之前) | 新版(NuttX 7.x之后) |
| 设备号管理 | 静态分配 | 动态分配 |
| 内部实现 | 直接调用 register_driver() | 调用 register_driver() + 额外初始化 |
| 推荐使用 | 兼容旧代码 | 新项目首选 |
uart_register_dev()。我曾经在一个项目里用了 uart_register(),结果升级NuttX版本后,发现设备号冲突,排查了半天。后来改成 uart_register_dev(),问题就解决了。
4.4 驱动分层实战:以STM32为例
光说不练假把式。咱们拿STM32F4的串口驱动,看看这三层到底怎么配合。
4.4.1 硬件层:操作寄存器
STM32的USART外设,寄存器就那么几个。硬件层的代码,说白了就是读写这些寄存器。比如:
/* 发送一个字节 */
static void stm32_serial_send(FAR struct uart_dev_s *dev, int ch)
{
FAR struct stm32_serial_s *priv = (FAR struct stm32_serial_s *)dev->priv;
/* 等待发送数据寄存器空 */
while ((priv->usart_base->sr & USART_SR_TXE) == 0);
/* 写入数据 */
priv->usart_base->dr = (uint16_t)ch;
}
你看,这里直接操作了 usart_base->sr 和 usart_base->dr。这就是硬件层。
4.4.2 下层:组装操作函数集
下层就是把硬件层的函数,填到 uart_ops_s 结构体里:
static const struct uart_ops_s g_stm32_serial_ops =
{
.setup = stm32_serial_setup,
.shutdown = stm32_serial_shutdown,
.attach = stm32_serial_attach,
.detach = stm32_serial_detach,
.ioctl = stm32_serial_ioctl,
.receive = stm32_serial_receive,
.send = stm32_serial_send,
/* ... */
};
嗯,这里有个细节:stm32_serial_send 和 stm32_serial_receive 都是硬件层的函数,但通过这个结构体,它们被“包装”成了下层接口。
4.4.3 上层:注册到系统
最后,在初始化函数里,调用注册函数:
int stm32_serial_initialize(void)
{
/* 初始化设备结构体 */
g_uart1_dev.ops = &g_stm32_serial_ops;
g_uart1_dev.priv = &g_uart1_priv;
g_uart1_dev.recv = &g_uart1_recvbuf;
g_uart1_dev.xmit = &g_uart1_xmitbuf;
/* 注册到系统 */
uart_register_dev("/dev/ttyS0", &g_uart1_dev);
return OK;
}
到这里,一个完整的串口驱动就注册好了。应用程序调用 open("/dev/ttyS0", O_RDWR) 时,NuttX的文件系统会找到这个设备,然后通过 ops 指针,调用到硬件层的函数。
uart_ops_s 里的函数指针。下层不关心文件操作怎么实现,它只管收发字节。硬件层更纯粹,只操作寄存器。这就是分层的好处——各司其职,互不干扰。
4.5 避坑指南
最后,分享几个我实际项目中踩过的坑:
- 缓冲区大小: 默认的接收缓冲区可能只有256字节。如果你跑的是高波特率(比如921600),中断来得太快,缓冲区会溢出。我建议根据你的数据量,适当调大
CONFIG_SERIAL_RXBUFSIZE。 - 中断优先级: 串口中断优先级不要设得太低,否则在高负载下会丢数据。我曾经把优先级设成了最低,结果系统一忙,串口就丢字节,排查了两天才发现。
- DMA vs 中断: 对于低波特率(115200以下),中断模式完全够用。高波特率才考虑DMA。别一上来就上DMA,调试起来更复杂。
好了,关于NuttX串口驱动的框架,咱们就聊到这儿。分层设计、结构体定义、注册函数,这三块搞明白了,移植任何MCU的串口驱动,思路都是一样的。
公众号:蓝海资料掘金营,微信deep3321