5.4 Serial 设备
5.4 Serial 设备
📚 本节导读
学习时长: 约 90 分钟
难度级别: ⭐⭐⭐⭐☆
前置知识: C 语言基础、MCU 串口基础知识、5.1 设备驱动框架、FIFO/环形缓冲区概念
Serial(串口/UART)设备是嵌入式系统中最常用的通信接口——调试日志、传感器数据、通信协议、固件升级几乎都离不开它。本节讲解 OneOS 中 Serial 设备的完整用法。OneOS 将串口抽象为字符设备(OS_DEVICE_TYPE_CHAR),在标准设备框架之上增加了 FIFO 环形缓冲区、DMA 收发、事件通知等机制,提供高效可靠的串口通信能力。Serial 设备框架位于 drivers/serial/ 目录,核心文件为 serial.h 与 serial.c。
本节会带你:
- 看懂 Serial 设备如何基于 5.1 的设备框架二次封装(
os_serial_device内嵌os_device); - 掌握
os_serial_device、serial_configure、os_serial_rx_fifo、os_serial_tx_fifo、os_uart_ops等核心结构体的每个字段(含位域存储演算); - 掌握串口配置参数(波特率、数据位、停止位、校验位、位顺序、缓冲区大小);
- 逐个拆解
os_hw_serial_register、os_hw_serial_isr_rxdone、os_hw_serial_isr_txdone及框架层os_serial_init/deinit/read/write/control的完整源码; - 通过多个可运行示例掌握阻塞/非阻塞读写、动态改配置、事件接收的实际开发。
说明:本节所有源码均取自项目
drivers/serial/serial.c与drivers/serial/serial.h,与项目源码逐字一致(含原始英文注释)。
🎯 学习目标
- 理解 OneOS 中 Serial 设备框架的设计理念与 FIFO 缓冲机制
- 掌握
os_serial_device、serial_configure、os_serial_rx_fifo、os_serial_tx_fifo、os_uart_ops各字段含义与来源 - 掌握串口配置参数(波特率、数据位、停止位、校验位、位顺序)的设置方法
- 掌握框架层读写流程(
os_serial_read/os_serial_write与内部辅助函数_serial_rx/_serial_tx/_serial_poll_tx) - 理解串口事件机制(RX 到达、TX 完成、DMA 完成、超时)与 DMA 支持
- 理解更换 MCU 时如何移植 BSP 层串口驱动(认识
drv_usart.c/drv_uart.c、实现os_uart_ops、接入三层选择机制) - 能够编写完整的串口数据收发程序
一、概述
Serial(串口/UART)设备是嵌入式系统中最常用的通信接口之一。OneOS 将串口抽象为字符设备(OS_DEVICE_TYPE_CHAR),在标准设备框架之上增加了 FIFO 缓冲区、DMA 收发、事件通知等机制,提供高效可靠的串口通信能力。Serial 设备驱动位于 drivers/serial/ 目录下,核心头文件为 serial.h。
1.1 Serial 设备的分层架构
Serial 设备是基于 5.1 设备驱动框架的二次封装,它在通用 os_device_t 的基础上扩展出串口特有的字段,并与底层 BSP 串口驱动解耦:
┌─────────────────────────────────────────────────────────────┐
│ 应用层(Application) │
│ os_device_open_s / os_device_read_block / os_device_write_ │
│ block / os_device_control(改配置)/ os_device_close │
└──────────────────────────────┬──────────────────────────────┘
│ os_device_read/write/control
▼
┌─────────────────────────────────────────────────────────────┐
│ Serial 框架层(drivers/serial/serial.c) │
│ os_serial_init/deinit/read/write/control │
│ _serial_rx / _serial_tx / _serial_poll_tx(内部辅助函数) │
│ os_hw_serial_register / os_hw_serial_isr_rxdone/txdone │
│ os_serial_rx_fifo / os_serial_tx_fifo(FIFO 缓冲) │
└──────────────────────────────┬──────────────────────────────┘
│ 调用 serial->ops->init/start_send/poll_send
▼
┌─────────────────────────────────────────────────────────────┐
│ BSP 厂商串口驱动(如 stm32 的 drv_usart.c) │
│ 实现 struct os_uart_ops 的四个函数指针 │
│ 操作具体 MCU 的 UART 外设寄存器/HAL │
└──────────────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 硬件(MCU UART 外设) │
└─────────────────────────────────────────────────────────────┘关键理解:Serial 框架层不直接操作硬件。它负责"FIFO 缓冲管理 + 设备框架适配 + 调用 ops",真正操作寄存器的是 BSP 厂商驱动实现的 struct os_uart_ops。这让上层应用与具体 MCU 平台解耦。
1.2 Serial 设备使用场景
一句话核心
Serial 设备适用于任务与外部设备(或另一颗芯片)之间逐字节流式、点对点、异步收发数据的场景,如调试日志、传感器数据上报、通信协议交互。
与其他总线/外设的机制对比
| 维度 | Serial(UART) | SPI | I2C | USB |
|---|---|---|---|---|
| 接线 | 2~3 根(TX/RX/GND) | 3~4 根 | 2 根 | 4 根(D+/D-) |
| 总线拓扑 | 点对点为主 | 一主多从 | 多主多从 | 主机-设备 |
| 是否按地址寻址 | 否 | 片选 CS | 7/10 位地址 | 端点/地址 |
| 数据格式 | 异步帧(起止位) | 同步 | 同步+ACK | 分组 |
| 典型场景 | 日志、调试、传感器上报 | 高吞吐外设 | 低速多从机 | 大容量/热插拔 |
为什么用 Serial 而不是别的:调试/日志场景需要逐字节流式输出,UART 接线最少(TX/RX 两根)、协议最简单;SPI/I2C 需要额外时钟和寻址,USB 需要协议栈和驱动,都较重。对于点对点、速率要求不高的通信,UART 是首选。
典型应用场景
场景 1:调试日志输出(console)
┌────────────┐ TX ───────▶ ┌────────────┐
│ MCU 主控 │ │ PC 串口 │
│ os_kprintf │ RX ◀─────── │ 调试助手 │
└────────────┘ └────────────┘为什么用 Serial:os_kprintf 底层走 console 串口,把调试信息实时发到 PC 串口助手。OneOS 的 os_serial_write 对 console 设备有特殊处理(走轮询发送,见 4.4),保证即使在中断/关调度场景也能可靠输出日志。
实际工作:任务 A 调用 os_kprintf("state=%d\r\n", state),数据进入 console 串口 → BSP 的 poll_send 逐字节发送 → PC 端串口助手按 115200-8-N-1 解析显示。
场景 2:传感器数据上报(定长帧协议)
为什么用 Serial:许多 GPS、姿态传感器通过 UART 输出定长/不定长数据帧。接收任务用 os_device_read_block 阻塞等待,收到完整帧后解析。
实际工作:GPS 模块每 1s 通过 TX 发一帧 $GPRMC NMEA 数据。MCU 的接收任务 os_device_read_block 阻塞在 rx_sem 上,当 UART 接收中断调用 os_hw_serial_isr_rxdone → os_device_recv_notify 唤醒接收任务,任务把数据从 FIFO 读出并解析。
场景 3:AT 指令交互(与 WiFi/4G 模组通信)
为什么用 Serial:WiFi/4G 模组通常通过 UART 与 MCU 交互,MCU 发 AT 指令、模组回响应。这种"你问我答"非常适合阻塞读 + 超时判断。
实际工作:MCU 任务发 AT+CWMODE=1\r\n,然后 os_device_read_block 等模组回 OK 或 ERROR。若用 OS_DEVICE_CTRL_SET_RX_TIMEOUT 设置接收超时,可防止一直阻塞。
选型边界表(何时不该用 Serial)
| 场景 | 该用什么 | 为什么 |
|---|---|---|
| 需要高速大吞吐(如 SPI Flash、LCD) | SPI | UART 速率低、逐字节异步开销大 |
| 需要一主多从按地址寻址 | I2C/SPI | UART 无地址概念 |
| 需要大数据量/热插拔/枚举 | USB | UART 无枚举和热插拔机制 |
| 只需要传开关状态 | GPIO | 无需协议开销 |
| 极长距离通信 | RS485/RS232 或无线 | 标准 TTL UART 抗干扰和距离有限 |
结合源码的使用注意点
- 收发有独立 FIFO:
rx_fifo(DMA 环形缓冲)与tx_fifo(环形缓冲 + 行缓冲)分开管理,读写互不干扰(见 2.3/2.4)。 - 读写是否阻塞由设备框架决定:
os_device_read_block会等rx_sem(rx_count==0时阻塞),os_device_read_nonblock立即返回(见 4.5)。 - console 特殊路径:
os_serial_write对 console 设备走_serial_poll_tx(轮询),避免在中断里用信号量同步(见 4.4)。 - 配置变更会重新初始化:
OS_DEVICE_CTRL_CONFIG改配置后,若设备已打开(ref_count非 0),会先os_serial_deinit再os_serial_init(见 4.3)。
二、核心数据结构(字段详解)
本节对照 drivers/serial/serial.h 讲解串口框架涉及的五个核心结构体,以及它们所依赖的环形缓冲结构。来源:drivers/serial/serial.h。
2.1 串口配置结构体 struct serial_configure
struct serial_configure
{
uint32_t baud_rate;
uint32_t data_bits : 4;
uint32_t stop_bits : 2;
uint32_t parity : 2;
uint32_t bit_order : 1;
uint32_t invert : 1;
uint32_t rx_bufsz : 16;
uint32_t tx_bufsz : 16;
uint32_t reserved : 6;
};| 字段 | 类型/宽度 | 含义 |
|---|---|---|
baud_rate | uint32_t | 波特率,如 115200、9600 |
data_bits | 位域 4 位 | 数据位(5/6/7/8/9) |
stop_bits | 位域 2 位 | 停止位(STOP_BITS_1=0,STOP_BITS_2=1) |
parity | 位域 2 位 | 校验位(PARITY_NONE=0,PARITY_ODD=1,PARITY_EVEN=2) |
bit_order | 位域 1 位 | 位顺序(BIT_ORDER_LSB=0,BIT_ORDER_MSB=1) |
invert | 位域 1 位 | 编码模式(NRZ_NORMAL=0,NRZ_INVERTED=1) |
rx_bufsz | 位域 16 位 | 接收缓冲区大小 |
tx_bufsz | 位域 16 位 | 发送缓冲区大小 |
reserved | 位域 6 位 | 保留位 |
关键理解——位域存储演算:该结构体用 C 位域(bit-field)紧凑存储多个小配置项。存储分布如下:
| 存储单元 | 占用位 | 内容 |
|---|---|---|
第 1 个 uint32_t | 32 位 | baud_rate |
第 2 个 uint32_t | 前 10 位 | data_bits(4) + stop_bits(2) + parity(2) + bit_order(1) + invert(1) |
第 2 个 uint32_t | 次 16 位 | rx_bufsz(16)(10+16 = 26 位,仍在 32 位内) |
第 3 个 uint32_t | 22 位 | tx_bufsz(16) + reserved(6)(26 位 + 22 位超出 32 位边界,需换新字) |
数值演算(为什么占 3 个
uint32_t即 12 字节):
计算项 代入计算 结果 baud_rate独立 uint32_t32 位 = 4 字节 位域合计 4+2+2+1+1+16+16+6 48 位 位域跨字分配 第 2 个字装 26 位,剩余 22 位放入第 3 个字 2 个字 = 64 位 总大小 32 + 64 96 位 = 12 字节
所以 sizeof(struct serial_configure) 为 12 字节。位域顺序和宽度由源码精确定义,BSP 驱动按位域访问。
2.2 接收 FIFO 结构体 os_serial_rx_fifo
struct os_serial_rx_fifo
{
dma_ring_t ring;
};| 字段 | 类型 | 含义 |
|---|---|---|
ring | dma_ring_t | DMA 环形缓冲区 |
接收 FIFO 内部就是一个 dma_ring_t 环形缓冲区。来源:drivers/serial/serial.h;dma_ring_t 定义于 drivers/dma/soft_dma.h:
typedef struct dma_ring
{
uint8_t *buff;
int size;
int head;
int tail;
} dma_ring_t;| 字段 | 含义 |
|---|---|
buff | 环形缓冲区基址(DMA 可直接写入) |
size | 缓冲区大小(字节) |
head | 写索引(DMA/ISR 写入的位置) |
tail | 读索引(应用读取的位置) |
关键理解:head 与 tail 组成一个环形指针——数据写入位置由 head 指示,读取位置由 tail 指示;当任一条追上另一条时做回绕。框架层用 ring_count() 计算当前可用数据量(head - tail 取模 size)。接收用 DMA 环形缓冲,底层 DMA 中断可直接往 buff 写数据,无需 CPU 逐字节搬移,适合大数据量接收。
2.3 发送 FIFO 结构体 os_serial_tx_fifo
struct os_serial_tx_fifo
{
rb_ring_buff_t rbuff;
uint8_t *line_buff;
uint32_t line_buff_count;
};| 字段 | 类型 | 含义 |
|---|---|---|
rbuff | rb_ring_buff_t | 发送环形缓冲区,缓存待发送数据 |
line_buff | uint8_t * | 发送行缓冲区,存放当前正在发送的数据块 |
line_buff_count | uint32_t | 行缓冲区中待发送的数据字节数 |
关键理解——两层缓冲设计:发送采用"环形缓冲 rbuff(缓存)→ 行缓冲 line_buff(当前块)"两层:
rbuff负责缓存尚未排到的数据(生产者往这里写);line_buff只存正在通过start_send交给底层的那一块数据;line_buff_count为 0 表示当前无发送在进行,可以启动下一块发送(见 3.3_serial_tx)。
rb_ring_buff_t 定义于 common/include/ring_buff.h:
struct rb_ring_buff
{
uint8_t *buff; /* The buffer */
uint32_t buff_size; /* The buffer size */
uint32_t read_index; /* Current read index */
uint32_t write_index; /* Current write index */
os_bool_t read_mirror; /* Read mirror flag */
os_bool_t write_mirror; /* Write mirror flag */
};
typedef struct rb_ring_buff rb_ring_buff_t;| 字段 | 含义 |
|---|---|
buff | 缓冲区基址 |
buff_size | 缓冲区大小 |
read_index | 读索引 |
write_index | 写索引 |
read_mirror/write_mirror | 读写镜像标志,用于判断环形回绕(区分"满"与"空") |
环形缓冲区"空/满"判断(镜像标志详解):环形缓冲最棘手的问题是"满/空歧义"——普通实现只用
read_index == write_index判断空,但缓冲区写满时read_index也会等于write_index(写追上了读),于是"空"和"满"两种状态索引完全相同、无法区分。OneOS 用一对read_mirror/write_mirror镜像标志解决这个问题。判断逻辑来自common/source/ring_buff.c的rb_ring_buff_state():来源:
common/source/ring_buff.cstatic rb_ring_buff_state_t rb_ring_buff_state(rb_ring_buff_t *rb) { if (rb->read_index == rb->write_index) { if (rb->read_mirror == rb->write_mirror) { return RB_RING_BUFF_EMPTY; } else { return RB_RING_BUFF_FULL; } } return RB_RING_BUFF_HALF_FULL; }判空/判满的核心:
read_index != write_index→ 半满(有数据可读);read_index == write_index且read_mirror == write_mirror→ 空;read_index == write_index且read_mirror != write_mirror→ 满。镜像标志何时翻转:读写索引跨过缓冲末尾回绕(从
buff_size-1回到 0)时,对应镜像翻转一次(rb_ring_buff_put/rb_ring_buff_get中read_mirror = !read_mirror/write_mirror = !write_mirror)。这样"写回绕次数"与"读回绕次数"的奇偶关系就能区分满与空。具体数值演算(
buff_size = 8):设 8 字节环形缓冲,依次写入 A~H(8 个字节写满),再依次读出 A~H(读空)。各阶段状态如下:
阶段 操作 read_indexwrite_indexread_mirrorwrite_mirror索引关系 rb_ring_buff_state结果① 初始化 rb_ring_buff_init0 0 F F 相等 镜像相同 → 空 ② 写 A 写入 buff[0] 0 1 F F 不等 半满 ③ 写 B~G 依次写入 buff[1]~buff[6] 0 7 F F 不等 半满 ④ 写 H 写入 buff[7], write_index从 7 回绕到 00 0 F T 相等 镜像不同 → 满 ⑤ 读 A 从 buff[0] 读出 1 0 F T 不等 半满 ⑥ 读 B~G 依次读出 buff[1]~buff[6] 7 0 F T 不等 半满 ⑦ 读 H 从 buff[7] 读出, read_index从 7 回绕到 00 0 T T 相等 镜像相同 → 空 关键观察:阶段 ④(满)和阶段 ⑦(空)时,
read_index == write_index == 0完全相同,但——
- 阶段 ④:
write_mirror = T(写回绕了 1 次)、read_mirror = F(读还没回绕)→ 镜像不同 → 满;- 阶段 ⑦:
read_mirror = T(读也回绕了 1 次)、write_mirror = T→ 镜像相同 → 空。为什么能区分:
write_mirror记录"写方是否比读方多回绕了一次"。若写回绕次数与读回绕次数奇偶相同(镜像相同)且索引相等 → 是空(读追上了写,把数据读完了);若奇偶不同(镜像不同)且索引相等 → 是满(写追上了读,把缓冲区写满了)。这就是镜像标志解决"满/空歧义"的原理,同时不需要牺牲一个缓冲区槽位(传统做法会留一格空位区分)。传统做法对比:
方案 判空判满方式 代价 传统(留一格) 始终留一格空位, read_index == write_index判空,(write_index+1)%size == read_index判满最大只能存 buff_size-1字节,浪费 1 槽OneOS(镜像标志) read_index == write_index且镜像相同判空、镜像不同判满需维护 2 个镜像位,可存满 buff_size字节
2.4 串口设备结构体 os_serial_device
struct os_serial_device
{
struct os_device parent;
const struct os_uart_ops *ops;
struct serial_configure config;
struct os_serial_rx_fifo *rx_fifo;
struct os_serial_tx_fifo *tx_fifo;
#ifdef OS_SERIAL_DELAY_CLOSE
os_semaphore_id delay_sem;
#endif
os_semaphore_id sem;
uint16_t rx_count;
#ifdef OS_DEVICE_SUPPORT_PLUG
os_plug_t plug;
#endif
os_spinlock_t lock; /* Spin lock. */
};
typedef struct os_serial_device os_serial_t;| 字段 | 类型 | 含义 |
|---|---|---|
parent | struct os_device | 内嵌的通用设备对象,用于接入 5.1 设备框架 |
ops | const struct os_uart_ops * | 底层 UART 操作函数表(BSP 驱动填充) |
config | struct serial_configure | 串口全部配置参数 |
rx_fifo | struct os_serial_rx_fifo * | 接收 FIFO 指针(os_serial_init 时分配) |
tx_fifo | struct os_serial_tx_fifo * | 发送 FIFO 指针(仅 start_send 存在时分配) |
delay_sem | os_semaphore_id | 延迟关闭信号量(OS_SERIAL_DELAY_CLOSE 开启时存在),用于防止发送未完就关闭 |
sem | os_semaphore_id | 设备操作信号量(初值 1,os_serial_init/deinit 互斥用) |
rx_count | uint16_t | 接收数据计数(已收到但未读的字节数) |
plug | os_plug_t | 热插拔插件(OS_DEVICE_SUPPORT_PLUG 开启时存在) |
lock | os_spinlock_t | 自旋锁,保护 FIFO 的并发访问 |
关键理解:parent 内嵌 os_device,使串口能注册进设备框架,应用层用 os_device_read/write/control 操作它。sem 与 delay_sem 是信号量(初值 1),用于串口初始化的互斥与延迟关闭同步;lock 是自旋锁,在 _serial_rx/_serial_tx 中保护 FIFO 并发读写。
2.5 UART 操作函数表 struct os_uart_ops
struct os_uart_ops
{
int (*init)(struct os_serial_device *serial, struct serial_configure *cfg);
int (*deinit)(struct os_serial_device *serial);
int (*start_send)(struct os_serial_device *serial, const uint8_t *buff, os_size_t size);
int (*poll_send)(struct os_serial_device *serial, const uint8_t *buff, os_size_t size);
};| 函数指针 | 说明 |
|---|---|
init | 初始化底层 UART 硬件,应用配置参数 |
deinit | 反初始化 UART 硬件 |
start_send | 启动中断模式发送,返回实际接收的字节数 |
poll_send | 轮询模式发送(阻塞发送),返回实际发送的字节数 |
关键理解:init/deinit/poll_send 是 os_hw_serial_register 中必须非空的三个(见 4.1 的断言);start_send 可空——若为空,os_serial_init 不分配 tx_fifo,且 os_serial_write 退化为轮询发送(见 4.4)。这套 ops 表是 BSP 驱动与框架层之间的"契约",也是移植的核心(见第七章)。
三、串口配置参数详解
本节对照 drivers/serial/serial.h 讲解串口配置宏。来源:drivers/serial/serial.h。
3.1 波特率
#define BAUD_RATE_2400 2400
#define BAUD_RATE_4800 4800
#define BAUD_RATE_9600 9600
#define BAUD_RATE_19200 19200
#define BAUD_RATE_38400 38400
#define BAUD_RATE_57600 57600
#define BAUD_RATE_115200 115200
#define BAUD_RATE_230400 230400
#define BAUD_RATE_460800 460800
#define BAUD_RATE_921600 921600
#define BAUD_RATE_2000000 2000000
#define BAUD_RATE_3000000 3000000支持从 2400 到 3,000,000 bps 的常用波特率,覆盖了低速到高速串口通信的全部需求。最常用的是 BAUD_RATE_115200(默认配置,见 3.6)。
3.2 数据位
#define DATA_BITS_5 5
#define DATA_BITS_6 6
#define DATA_BITS_7 7
#define DATA_BITS_8 8
#define DATA_BITS_9 9| 宏 | 说明 |
|---|---|
DATA_BITS_5 | 5 位数据位 |
DATA_BITS_6 | 6 位数据位 |
DATA_BITS_7 | 7 位数据位 |
DATA_BITS_8 | 8 位数据位(最常用) |
DATA_BITS_9 | 9 位数据位(含校验位场景) |
3.3 停止位
#define STOP_BITS_1 0
#define STOP_BITS_2 1
#define STOP_BITS_3 2
#define STOP_BITS_4 3| 宏 | 值 | 说明 |
|---|---|---|
STOP_BITS_1 | 0 | 1 个停止位(最常用) |
STOP_BITS_2 | 1 | 2 个停止位 |
STOP_BITS_3 | 2 | 3 个停止位 |
STOP_BITS_4 | 3 | 4 个停止位 |
3.4 校验位
#define PARITY_NONE 0
#define PARITY_ODD 1
#define PARITY_EVEN 2| 宏 | 值 | 说明 |
|---|---|---|
PARITY_NONE | 0 | 无校验 |
PARITY_ODD | 1 | 奇校验 |
PARITY_EVEN | 2 | 偶校验 |
注意:源文件中
PARITY_*宏位于#ifdef _WIN32/#else分支内(#else分支定义),Windows 平台下因windows.h已有同名定义而不重复定义。实际嵌入式中使用上述值。
3.5 位顺序与编码
#define BIT_ORDER_LSB 0
#define BIT_ORDER_MSB 1
#define NRZ_NORMAL 0 /* Non Return to Zero : normal mode */
#define NRZ_INVERTED 1 /* Non Return to Zero : inverted mode */| 宏 | 值 | 说明 |
|---|---|---|
BIT_ORDER_LSB | 0 | 低位先传(LSB first) |
BIT_ORDER_MSB | 1 | 高位先传(MSB first) |
NRZ_NORMAL | 0 | NRZ 标准编码 |
NRZ_INVERTED | 1 | NRZ 反向编码 |
3.6 默认配置
来源:drivers/serial/serial.h
#define OS_SERIAL_CONFIG_DEFAULT \
{ \
BAUD_RATE_115200, /* 115200 bits/s */ \
DATA_BITS_8, /* 8 databits */ \
STOP_BITS_1, /* 1 stopbit */ \
PARITY_NONE, /* No parity */ \
BIT_ORDER_LSB, /* LSB first sent */ \
NRZ_NORMAL, /* Normal mode */ \
OS_SERIAL_RX_BUFSZ, /* Tx buffer size */ \
OS_SERIAL_TX_BUFSZ, /* Tx buffer size */ \
0 \
}| 字段 | 默认值 | 说明 |
|---|---|---|
baud_rate | BAUD_RATE_115200 | 115200 bps |
data_bits | DATA_BITS_8 | 8 位数据位 |
stop_bits | STOP_BITS_1 | 1 个停止位 |
parity | PARITY_NONE | 无校验 |
bit_order | BIT_ORDER_LSB | LSB 先传 |
invert | NRZ_NORMAL | 正常编码 |
rx_bufsz | OS_SERIAL_RX_BUFSZ | 接收缓冲 64 字节 |
tx_bufsz | OS_SERIAL_TX_BUFSZ | 发送缓冲 64 字节 |
reserved | 0 | 保留 |
默认配置即 115200-8-N-1(最常用的串口配置),收发缓冲区大小默认 64 字节(可通过 OS_SERIAL_RX_BUFSZ / OS_SERIAL_TX_BUFSZ 宏自定义)。
3.7 缓冲区大小
来源:drivers/serial/serial.h
#ifndef OS_SERIAL_RX_BUFSZ
#define OS_SERIAL_RX_BUFSZ 64
#endif
#ifndef OS_SERIAL_TX_BUFSZ
#define OS_SERIAL_TX_BUFSZ 64
#endif收发缓冲区大小默认均为 64 字节。这两个宏未定义时由 #ifndef 保护给出默认值;可在 oneos_config.h 中先定义再覆盖。
数值演算(
os_serial_init中 rx 缓冲区实际分配大小):源码里rx_bufsz = OS_ALIGN_UP(config.rx_bufsz + 1, 4)(见 4.2)。默认rx_bufsz = 64:
计算项 代入计算 结果 传入值 config.rx_bufsz64 加 1(预留满标志) 64 + 165 4 字节对齐 OS_ALIGN_UP(65, 4)68 即实际分配 68 字节的 DMA 接收环形缓冲(多出的 +1 用于区分"满"与"空",对齐到 4 字节)。
四、串口事件、DMA 与中断模式
本节对照 drivers/serial/serial.h 讲解事件类型、DMA 模式、中断模式、错误类型与发送数据队列宏。来源:drivers/serial/serial.h。
4.1 事件类型
#define OS_SERIAL_EVENT_RX_IND 0x01 /* Rx indication */
#define OS_SERIAL_EVENT_TX_DONE 0x02 /* Tx complete */
#define OS_SERIAL_EVENT_RX_DMADONE 0x03 /* Rx DMA transfer done */
#define OS_SERIAL_EVENT_TX_DMADONE 0x04 /* Tx DMA transfer done */
#define OS_SERIAL_EVENT_RX_TIMEOUT 0x05 /* Rx timeout */| 事件 | 值 | 说明 |
|---|---|---|
OS_SERIAL_EVENT_RX_IND | 0x01 | 接收数据到达指示,应用层可读取 |
OS_SERIAL_EVENT_TX_DONE | 0x02 | 发送完成 |
OS_SERIAL_EVENT_RX_DMADONE | 0x03 | 一次 DMA 接收传输完成 |
OS_SERIAL_EVENT_TX_DMADONE | 0x04 | 一次 DMA 发送传输完成 |
OS_SERIAL_EVENT_RX_TIMEOUT | 0x05 | 接收超时 |
注意:这些事件宏在
serial.c的框架逻辑中不直接作为switch分支出现,而是供 BSP 驱动在中断回调里通知上层时使用(结合os_device_recv_notify等)。框架层的核心同步用的是设备框架的rx_sem/tx_sem(见 4.5)。
4.2 DMA 模式
#define OS_SERIAL_DMA_RX 0x01
#define OS_SERIAL_DMA_TX 0x02| 宏 | 值 | 说明 |
|---|---|---|
OS_SERIAL_DMA_RX | 0x01 | 使能 DMA 接收 |
OS_SERIAL_DMA_TX | 0x02 | 使能 DMA 发送 |
DMA 模式用于大数据量传输:接收时 DMA 直接把数据写入 rx_fifo->ring.buff,发送时 start_send 可用 DMA 搬移数据。OS_SERIAL_DMA_RX / OS_SERIAL_DMA_TX 是独立的位(bit0/bit1),可组合。
4.3 中断模式
#define OS_SERIAL_RX_INT 0x01
#define OS_SERIAL_TX_INT 0x02| 宏 | 值 | 说明 |
|---|---|---|
OS_SERIAL_RX_INT | 0x01 | 使能接收中断 |
OS_SERIAL_TX_INT | 0x02 | 使能发送中断 |
4.4 错误类型
#define OS_SERIAL_ERR_OVERRUN 0x01
#define OS_SERIAL_ERR_FRAMING 0x02
#define OS_SERIAL_ERR_PARITY 0x03| 宏 | 值 | 说明 |
|---|---|---|
OS_SERIAL_ERR_OVERRUN | 0x01 | 接收溢出错误 |
OS_SERIAL_ERR_FRAMING | 0x02 | 帧错误 |
OS_SERIAL_ERR_PARITY | 0x03 | 校验错误 |
4.5 发送数据队列
#define OS_SERIAL_TX_DATAQUEUE_SIZE 2048
#define OS_SERIAL_TX_DATAQUEUE_LWM 30| 宏 | 值 | 说明 |
|---|---|---|
OS_SERIAL_TX_DATAQUEUE_SIZE | 2048 | 发送数据队列大小 |
OS_SERIAL_TX_DATAQUEUE_LWM | 30 | 发送数据队列低水位(当队列低于该水位时唤醒生产者) |
补充:
serial.h中还有#define OS_SERIAL_FLAG_MASK 0xff00,用于屏蔽串口设备标志位的高 8 位(保留给设备框架/驱动使用)。
五、核心 API 详解
本节逐个拆解 drivers/serial/serial.c 中的核心函数。来源:drivers/serial/serial.c。
5.1 os_hw_serial_register —— 注册串口设备
函数签名
os_err_t os_hw_serial_register(struct os_serial_device *serial, const char *name, void *data)参数 / 返回值说明
| 参数 | 类型 | 说明 |
|---|---|---|
serial | struct os_serial_device * | 串口设备结构体指针,必须已填充 ops 和 config 字段 |
name | const char * | 设备名称,如 "uart0"、"uart1" |
data | void * | 用户自定义数据指针,存储到 device->user_data |
| 返回值 | 说明 |
|---|---|
OS_SUCCESS | 注册成功 |
| 其他 | 注册失败(os_device_register 的返回值) |
完整源码
os_err_t os_hw_serial_register(struct os_serial_device *serial, const char *name, void *data)
{
os_err_t ret;
struct os_device *device;
OS_ASSERT(serial != OS_NULL);
OS_ASSERT(serial->ops != OS_NULL);
OS_ASSERT(serial->ops->init != OS_NULL);
OS_ASSERT(serial->ops->deinit != OS_NULL);
OS_ASSERT(serial->ops->poll_send != OS_NULL);
device = &(serial->parent);
device->type = OS_DEVICE_TYPE_CHAR;
device->ops = &serial_ops;
device->user_data = data;
#ifdef OS_DEVICE_SUPPORT_PLUG
serial->plug.upper = &device->plug;
serial->plug.halt = serial_halt;
serial->plug.release = serial_release;
os_plug_in(&serial->plug, "serial", name);
#endif
os_spin_lock_init(&serial->lock);
/* Register a character device */
ret = os_device_register(device, name);
/* sem */
serial->sem = os_semaphore_create(OS_NULL, OS_NULL, 1, 1);
#ifdef OS_SERIAL_DELAY_CLOSE
serial->delay_sem = os_semaphore_create(OS_NULL, OS_NULL, 1, 1);
#endif
return ret;
}分步详解
第一步:参数校验(条件 serial 及 ops 表各函数非空)
OS_ASSERT(serial != OS_NULL);
OS_ASSERT(serial->ops != OS_NULL);
OS_ASSERT(serial->ops->init != OS_NULL);
OS_ASSERT(serial->ops->deinit != OS_NULL);
OS_ASSERT(serial->ops->poll_send != OS_NULL);为什么断言 init/deinit/poll_send 必须非空,而不断言 start_send:
init/deinit在设备打开/关闭时必然调用(见 4.2os_serial_init/deinit),若为空会空指针崩溃;poll_send是保底发送路径(start_send为空时走它),必须存在;start_send是可选的优化——若 BSP 不支持中断/DMA 发送,可只实现poll_send,框架会退化为轮询发送。因此这里不强制start_send非空。
第二步:配置父设备类型、ops、user_data(无条件执行)
device = &(serial->parent);
device->type = OS_DEVICE_TYPE_CHAR;
device->ops = &serial_ops;
device->user_data = data;- 取内嵌
parent作为设备框架对象; type = OS_DEVICE_TYPE_CHAR:标记为字符设备;ops = &serial_ops:挂上框架层操作表(serial_ops,见下方);user_data = data:存用户数据。
serial_ops 表的来源:
/* clang-format off */
const static struct os_device_ops serial_ops =
{
.init = os_serial_init,
.deinit = os_serial_deinit,
.read = os_serial_read,
.write = os_serial_write,
.control = os_serial_control
};
/* clang-format on */第三步:热插拔插件挂载(条件 OS_DEVICE_SUPPORT_PLUG)
#ifdef OS_DEVICE_SUPPORT_PLUG
serial->plug.upper = &device->plug;
serial->plug.halt = serial_halt;
serial->plug.release = serial_release;
os_plug_in(&serial->plug, "serial", name);
#endif若启用热插拔,配置 plug 的 halt/release 回调并挂入 "serial" 插件体系(本框架还实现了 serial_halt/serial_release 用于释放信号量)。
第四步:初始化自旋锁并注册到设备框架(无条件执行)
os_spin_lock_init(&serial->lock);
/* Register a character device */
ret = os_device_register(device, name);初始化 serial->lock 自旋锁(供 FIFO 并发保护),调用 os_device_register 把父设备以 name 注册到设备管理器,ret 保存结果。
第五步:创建信号量(无条件执行)
/* sem */
serial->sem = os_semaphore_create(OS_NULL, OS_NULL, 1, 1);
#ifdef OS_SERIAL_DELAY_CLOSE
serial->delay_sem = os_semaphore_create(OS_NULL, OS_NULL, 1, 1);
#endif
return ret;创建初值 1 的 serial->sem(os_serial_init/deinit 互斥),若开启延迟关闭再创建 delay_sem,最后返回 ret。
条件走向总结
os_hw_serial_register() 条件走向总结:
├── OS_ASSERT 校验 serial/ops/init/deinit/poll_send(任一为空则断言失败)
├── 配置 device->type/ops/user_data
├── OS_DEVICE_SUPPORT_PLUG 已定义
│ └── 配置 plug 回调并 os_plug_in 挂载
├── os_spin_lock_init(&serial->lock)
├── ret = os_device_register(device, name)
├── 创建 serial->sem(初值 1)
├── OS_SERIAL_DELAY_CLOSE 已定义
│ └── 创建 serial->delay_sem(初值 1)
└── 返回 ret(OS_SUCCESS 成功 / 其他失败)5.2 os_hw_serial_isr_rxdone —— 接收完成通知
函数签名
void os_hw_serial_isr_rxdone(struct os_serial_device *serial)参数 / 返回值说明
| 参数 | 类型 | 说明 |
|---|---|---|
serial | struct os_serial_device * | 串口设备指针 |
| 返回值 | 说明 |
|---|---|
| 无 | 该函数无返回值,仅做通知 |
完整源码
void os_hw_serial_isr_rxdone(struct os_serial_device *serial)
{
os_device_t *dev;
OS_ASSERT(serial);
OS_ASSERT(serial->rx_fifo != OS_NULL);
dev = &serial->parent;
OS_ASSERT(dev != OS_NULL);
dev->rx_count = ring_count(&serial->rx_fifo->ring);
OS_ASSERT(dev->rx_count > 0);
os_device_recv_notify(dev);
}分步详解
第一步:取父设备指针并断言(条件 serial 与 rx_fifo 非空)
os_device_t *dev;
OS_ASSERT(serial);
OS_ASSERT(serial->rx_fifo != OS_NULL);
dev = &serial->parent;
OS_ASSERT(dev != OS_NULL);为什么断言 rx_fifo 非空:os_hw_serial_isr_rxdone 在接收中断里被调用,此时 rx_fifo 必须已由 os_serial_init 分配(见 4.2)。若未分配就调用,ring_count(&serial->rx_fifo->ring) 会访问空指针。这通常说明 BSP 在设备未初始化完成时就触发了接收中断。
第二步:更新接收计数(无条件执行)
dev->rx_count = ring_count(&serial->rx_fifo->ring);
OS_ASSERT(dev->rx_count > 0);用 ring_count() 计算接收环形缓冲里当前可读字节数,写入 dev->rx_count。为什么断言 rx_count > 0:rxdone 表示"有新数据到达",若计数为 0 说明调用时机错误(例如被误调用或数据已被读完),尽早暴露。
第三步:通知等待读取的任务(无条件执行)
os_device_recv_notify(dev);调用 os_device_recv_notify 唤醒阻塞在 os_device_read_block(等待 rx_sem)的任务,让应用层来读数据。
关键理解:
os_hw_serial_isr_rxdone由 BSP 驱动在接收中断/DMA 完成中断中调用(如 ST 的HAL_UART_RxCpltCallback),它本身不搬移数据(DMA 已把数据写入rx_fifo->ring.buff),只负责"更新计数 + 唤醒读者"。真正的数据读取发生在应用层调用os_device_read→_serial_rx时(见 5.6)。
5.3 os_hw_serial_isr_txdone —— 发送完成通知
函数签名
void os_hw_serial_isr_txdone(struct os_serial_device *serial)参数 / 返回值说明
| 参数 | 类型 | 说明 |
|---|---|---|
serial | struct os_serial_device * | 串口设备指针 |
| 返回值 | 说明 |
|---|---|
| 无 | 该函数无返回值,仅做通知 |
完整源码
void os_hw_serial_isr_txdone(struct os_serial_device *serial)
{
int count;
os_device_t *dev;
struct os_serial_tx_fifo *tx_fifo;
dev = &serial->parent;
tx_fifo = (struct os_serial_tx_fifo *)serial->tx_fifo;
OS_ASSERT(tx_fifo->line_buff_count != 0);
tx_fifo->line_buff_count = 0;
count = rb_ring_buff_get(&tx_fifo->rbuff, tx_fifo->line_buff, serial->config.tx_bufsz);
if (count == 0)
{
#ifdef OS_SERIAL_DELAY_CLOSE
os_semaphore_post(serial->delay_sem);
#endif
dev->tx_count = 0;
os_device_send_notify(dev);
}
else
{
tx_fifo->line_buff_count = serial->ops->start_send(serial, tx_fifo->line_buff, count);
OS_ASSERT(tx_fifo->line_buff_count > 0);
if (tx_fifo->line_buff_count < count)
{
rb_ring_buff_put(&tx_fifo->rbuff,
tx_fifo->line_buff + tx_fifo->line_buff_count,
count - tx_fifo->line_buff_count);
}
dev->tx_count = rb_ring_buff_data_len(&tx_fifo->rbuff);
}
}分步详解
第一步:取父设备与发送 FIFO(无条件执行)
os_device_t *dev;
struct os_serial_tx_fifo *tx_fifo;
dev = &serial->parent;
tx_fifo = (struct os_serial_tx_fifo *)serial->tx_fifo;
OS_ASSERT(tx_fifo->line_buff_count != 0);取 parent 与 tx_fifo。为什么断言 line_buff_count != 0:txdone 表示"当前行缓冲那块数据已发完",此时 line_buff_count 必然非 0;若为 0 说明发送完成中断来得异常(可能硬件误触发),尽早暴露。
第二步:清零行缓冲计数并取下一块数据(无条件执行)
tx_fifo->line_buff_count = 0;
count = rb_ring_buff_get(&tx_fifo->rbuff, tx_fifo->line_buff, serial->config.tx_bufsz);先把 line_buff_count 清零(表示上一块已发完),再从 rbuff 环形缓冲取出下一块待发数据放入 line_buff。count 为取到的字节数。
第三步:判断是否还有待发数据(条件 count == 0 或 count > 0)
if (count == 0)
{
#ifdef OS_SERIAL_DELAY_CLOSE
os_semaphore_post(serial->delay_sem);
#endif
dev->tx_count = 0;
os_device_send_notify(dev);
}子分支 A(count == 0,缓冲区已空):所有数据发送完毕。若开启延迟关闭,os_semaphore_post(delay_sem) 允许关闭;dev->tx_count = 0 表示发送队列空;os_device_send_notify 唤醒等待写入的任务(阻塞在 os_device_write_block)。
第四步:继续发送下一块数据(条件 count > 0)
else
{
tx_fifo->line_buff_count = serial->ops->start_send(serial, tx_fifo->line_buff, count);
OS_ASSERT(tx_fifo->line_buff_count > 0);
if (tx_fifo->line_buff_count < count)
{
rb_ring_buff_put(&tx_fifo->rbuff,
tx_fifo->line_buff + tx_fifo->line_buff_count,
count - tx_fifo->line_buff_count);
}
dev->tx_count = rb_ring_buff_data_len(&tx_fifo->rbuff);
}子分支 B(count > 0,还有数据):调用 start_send 启动下一块发送。若 start_send 只接收了部分字节(< count),把剩余字节退回 rbuff(rb_ring_buff_put 放回队首),最后更新 dev->tx_count。
条件走向总结
os_hw_serial_isr_txdone() 条件走向总结:
├── 取 dev、tx_fifo
├── OS_ASSERT(tx_fifo->line_buff_count != 0)(异常则为 0,断言失败)
├── tx_fifo->line_buff_count = 0(上一块已发完)
├── count = rb_ring_buff_get(rbuff, line_buff, tx_bufsz)(取下一块)
├── count == 0(发送队列已空)
│ ├── OS_SERIAL_DELAY_CLOSE:os_semaphore_post(delay_sem)
│ ├── dev->tx_count = 0
│ └── os_device_send_notify(dev)(唤醒写者)
└── count > 0(还有数据)
├── line_buff_count = start_send(serial, line_buff, count)(启动下一块)
├── line_buff_count < count:把剩余字节 rb_ring_buff_put 退回 rbuff
└── dev->tx_count = rb_ring_buff_data_len(rbuff)5.4 框架层初始化/反初始化(os_serial_init / os_serial_deinit)
函数签名
static os_err_t os_serial_init(struct os_device *dev)
static os_err_t os_serial_deinit(struct os_device *dev)这两个 static 函数被 serial_ops 表绑定,当设备打开/关闭时由设备框架调用。os_serial_init 负责分配收发 FIFO 并初始化底层硬件,os_serial_deinit 负责释放 FIFO 并反初始化硬件。
完整源码(os_serial_init)
static os_err_t os_serial_init(struct os_device *dev)
{
os_err_t result = OS_SUCCESS;
uint32_t rx_bufsz = 0;
struct os_serial_device *serial;
uint8_t *tx_rbuff_alloc;
OS_ASSERT(dev != OS_NULL);
serial = (struct os_serial_device *)dev;
os_semaphore_wait(serial->sem, OS_WAIT_FOREVER);
OS_ASSERT(serial->rx_fifo == OS_NULL);
OS_ASSERT(serial->tx_fifo == OS_NULL);
/* rx buff */
rx_bufsz = OS_ALIGN_UP(serial->config.rx_bufsz + 1, 4);
serial->rx_fifo = (struct os_serial_rx_fifo *)os_calloc(1, sizeof(struct os_serial_rx_fifo));
OS_ASSERT(serial->rx_fifo != OS_NULL);
serial->rx_fifo->ring.buff = (uint8_t *)os_dma_malloc_align(rx_bufsz, BSP_CACHE_LINE_SIZE);
serial->rx_fifo->ring.size = rx_bufsz;
/* tx buff */
if (serial->ops->start_send != OS_NULL)
{
serial->tx_fifo = (struct os_serial_tx_fifo *)os_calloc(1, sizeof(struct os_serial_tx_fifo));
OS_ASSERT(serial->tx_fifo != OS_NULL);
serial->tx_fifo->line_buff = (uint8_t *)os_dma_malloc_align(serial->config.tx_bufsz, BSP_CACHE_LINE_SIZE);
serial->tx_fifo->line_buff_count = 0;
tx_rbuff_alloc = (uint8_t *)os_calloc(1, serial->config.tx_bufsz);
rb_ring_buff_init(&serial->tx_fifo->rbuff, tx_rbuff_alloc, serial->config.tx_bufsz);
}
dev->rx_size = serial->config.rx_bufsz;
dev->rx_count = 0;
dev->tx_size = serial->config.tx_bufsz;
dev->tx_count = 0;
/* lowlevel initialization */
result = serial->ops->init(serial, &serial->config);
#ifdef OS_SERIAL_DELAY_CLOSE
os_semaphore_post(serial->delay_sem);
#endif
os_semaphore_post(serial->sem);
#ifdef OS_DEVICE_SUPPORT_PLUG
os_plug_t *plug = os_plug_get("serial", dev->name);
OS_ASSERT(&serial->plug == plug);
#endif
return result;
}分步详解(os_serial_init)
第一步:获取串口指针并加互斥锁(无条件执行)
OS_ASSERT(dev != OS_NULL);
serial = (struct os_serial_device *)dev;
os_semaphore_wait(serial->sem, OS_WAIT_FOREVER);os_semaphore_wait(serial->sem, OS_WAIT_FOREVER) 加信号量锁,防止并发初始化(sem 初值 1)。
第二步:断言 FIFO 未分配(无条件执行)
OS_ASSERT(serial->rx_fifo == OS_NULL);
OS_ASSERT(serial->tx_fifo == OS_NULL);为什么断言 FIFO 为空:os_serial_init 只在设备首次打开(或重配后)调用,此时 FIFO 应尚未分配。若已非空,说明重复初始化会内存泄漏或覆盖已有缓冲。
第三步:分配接收 FIFO(无条件执行)
/* rx buff */
rx_bufsz = OS_ALIGN_UP(serial->config.rx_bufsz + 1, 4);
serial->rx_fifo = (struct os_serial_rx_fifo *)os_calloc(1, sizeof(struct os_serial_rx_fifo));
OS_ASSERT(serial->rx_fifo != OS_NULL);
serial->rx_fifo->ring.buff = (uint8_t *)os_dma_malloc_align(rx_bufsz, BSP_CACHE_LINE_SIZE);
serial->rx_fifo->ring.size = rx_bufsz;计算 rx_bufsz = OS_ALIGN_UP(config.rx_bufsz + 1, 4)(见 3.7 数值演算),用 os_calloc 分配 FIFO 结构,用 os_dma_malloc_align 分配DMA 对齐的缓冲区(BSP_CACHE_LINE_SIZE 对齐,保证 DMA 与 cache 一致)。
第四步:分配发送 FIFO(条件 serial->ops->start_send != OS_NULL)
/* tx buff */
if (serial->ops->start_send != OS_NULL)
{
serial->tx_fifo = (struct os_serial_tx_fifo *)os_calloc(1, sizeof(struct os_serial_tx_fifo));
OS_ASSERT(serial->tx_fifo != OS_NULL);
serial->tx_fifo->line_buff = (uint8_t *)os_dma_malloc_align(serial->config.tx_bufsz, BSP_CACHE_LINE_SIZE);
serial->tx_fifo->line_buff_count = 0;
tx_rbuff_alloc = (uint8_t *)os_calloc(1, serial->config.tx_bufsz);
rb_ring_buff_init(&serial->tx_fifo->rbuff, tx_rbuff_alloc, serial->config.tx_bufsz);
}只有 start_send 存在时才分配发送 FIFO。因为若走纯轮询发送(_serial_poll_tx),不需要 rbuff/line_buff 缓冲,直接逐字节发即可。这体现了"按需分配、节省内存"。
第五步:初始化设备框架的收发计数(无条件执行)
dev->rx_size = serial->config.rx_bufsz;
dev->rx_count = 0;
dev->tx_size = serial->config.tx_bufsz;
dev->tx_count = 0;把 rx_size/tx_size 设为配置的缓冲大小,rx_count/tx_count 清零。设备框架的阻塞读写依赖这些字段判断"空/满"(见 5.7)。
第六步:调用底层 init 并释放锁(无条件执行)
/* lowlevel initialization */
result = serial->ops->init(serial, &serial->config);
#ifdef OS_SERIAL_DELAY_CLOSE
os_semaphore_post(serial->delay_sem);
#endif
os_semaphore_post(serial->sem);
#ifdef OS_DEVICE_SUPPORT_PLUG
os_plug_t *plug = os_plug_get("serial", dev->name);
OS_ASSERT(&serial->plug == plug);
#endif
return result;调用 serial->ops->init(BSP 底层硬件初始化),释放锁,返回结果。
完整源码(os_serial_deinit)
static os_err_t os_serial_deinit(struct os_device *dev)
{
struct os_serial_device *serial;
OS_ASSERT(dev != OS_NULL);
serial = (struct os_serial_device *)dev;
os_semaphore_wait(serial->sem, OS_WAIT_FOREVER);
#ifdef OS_SERIAL_DELAY_CLOSE
os_semaphore_wait(serial->delay_sem, OS_WAIT_FOREVER);
#endif
serial->ops->deinit(serial);
/* rx fifo */
if (serial->rx_fifo->ring.buff != OS_NULL)
{
os_dma_free_align(serial->rx_fifo->ring.buff);
serial->rx_fifo->ring.buff = OS_NULL;
}
if (serial->rx_fifo != OS_NULL)
{
os_free(serial->rx_fifo);
serial->rx_fifo = OS_NULL;
}
/* tx fifo */
if (serial->tx_fifo != OS_NULL)
{
if (serial->tx_fifo->line_buff != OS_NULL)
{
os_dma_free_align(serial->tx_fifo->line_buff);
serial->tx_fifo->line_buff = OS_NULL;
}
if (serial->tx_fifo->rbuff.buff != OS_NULL)
{
os_free(serial->tx_fifo->rbuff.buff);
serial->tx_fifo->rbuff.buff = OS_NULL;
}
os_free(serial->tx_fifo);
serial->tx_fifo = OS_NULL;
}
os_semaphore_post(serial->sem);
#ifdef OS_DEVICE_SUPPORT_PLUG
os_plug_put(&serial->plug);
#endif
return OS_SUCCESS;
}分步详解(os_serial_deinit)
第一步:加锁(条件开启延迟关闭时等待 delay_sem)
OS_ASSERT(dev != OS_NULL);
serial = (struct os_serial_device *)dev;
os_semaphore_wait(serial->sem, OS_WAIT_FOREVER);
#ifdef OS_SERIAL_DELAY_CLOSE
os_semaphore_wait(serial->delay_sem, OS_WAIT_FOREVER);
#endif为什么开启 OS_SERIAL_DELAY_CLOSE 时要等 delay_sem:delay_sem 初值 1,发送进行时 _serial_tx 会 os_semaphore_wait(delay_sem, OS_NO_WAIT) 把它减到 0(见 5.6),发送完成后 os_hw_serial_isr_txdone 再 os_semaphore_post 加回。因此 os_serial_deinit 等待 delay_sem 就是等待当前发送完全结束,避免关闭时还有数据在硬件里没发完。
第二步:反初始化底层硬件并释放收发 FIFO(无条件执行)
serial->ops->deinit(serial);
/* rx fifo */
if (serial->rx_fifo->ring.buff != OS_NULL)
{
os_dma_free_align(serial->rx_fifo->ring.buff);
serial->rx_fifo->ring.buff = OS_NULL;
}
if (serial->rx_fifo != OS_NULL)
{
os_free(serial->rx_fifo);
serial->rx_fifo = OS_NULL;
}
/* tx fifo */
if (serial->tx_fifo != OS_NULL)
{
if (serial->tx_fifo->line_buff != OS_NULL)
{
os_dma_free_align(serial->tx_fifo->line_buff);
serial->tx_fifo->line_buff = OS_NULL;
}
if (serial->tx_fifo->rbuff.buff != OS_NULL)
{
os_free(serial->tx_fifo->rbuff.buff);
serial->tx_fifo->rbuff.buff = OS_NULL;
}
os_free(serial->tx_fifo);
serial->tx_fifo = OS_NULL;
}调用 deinit 关硬件,然后逆序释放:先释放 DMA 缓冲,再释放 FIFO 结构体,并把指针置 OS_NULL 防止悬垂。tx_fifo 同样释放 line_buff、rbuff.buff 和结构体。
第三步:释放锁(无条件执行)
os_semaphore_post(serial->sem);
#ifdef OS_DEVICE_SUPPORT_PLUG
os_plug_put(&serial->plug);
#endif
return OS_SUCCESS;释放信号量锁,返回 OS_SUCCESS。
条件走向总结(os_serial_init)
os_serial_init() 条件走向总结:
├── 取 serial,os_semaphore_wait(sem)(互斥锁)
├── OS_ASSERT(rx_fifo == OS_NULL && tx_fifo == OS_NULL)
├── 计算 rx_bufsz = OS_ALIGN_UP(config.rx_bufsz + 1, 4)
├── 分配 rx_fifo 及 DMA 缓冲
├── ops->start_send 非空
│ └── 分配 tx_fifo、line_buff、rbuff(rb_ring_buff_init)
├── ops->start_send 为空
│ └── 不分配 tx_fifo(走轮询发送)
├── 设置 dev->rx_size/rx_count/tx_size/tx_count
├── result = ops->init(serial, &config)(底层初始化)
├── 释放 sem 锁
└── 返回 result条件走向总结(os_serial_deinit)
os_serial_deinit() 条件走向总结:
├── 取 serial,os_semaphore_wait(sem)
├── OS_SERIAL_DELAY_CLOSE:os_semaphore_wait(delay_sem)(等发送结束)
├── ops->deinit(serial)
├── 释放 rx_fifo 的 DMA 缓冲与结构体
├── tx_fifo 非空
│ └── 释放 line_buff、rbuff.buff、tx_fifo 结构体
├── 释放 sem 锁
└── 返回 OS_SUCCESS5.5 框架层读写入口(os_serial_read / os_serial_write)
函数签名
static os_ssize_t os_serial_read(struct os_device *dev, os_off_t pos, void *buffer, os_size_t size)
static os_ssize_t os_serial_write(struct os_device *dev, os_off_t pos, const void *buffer, os_size_t size)这两个 static 函数被 serial_ops 绑定,是设备框架 os_device_read/write(含阻塞/非阻塞变体)最终调用的入口。它们做了简单校验后,把实际逻辑委托给内部辅助函数 _serial_rx / _serial_tx / _serial_poll_tx。
完整源码(os_serial_read)
static os_ssize_t os_serial_read(struct os_device *dev, os_off_t pos, void *buffer, os_size_t size)
{
struct os_serial_device *serial;
OS_ASSERT(dev != OS_NULL);
if (size == 0)
return 0;
serial = (struct os_serial_device *)dev;
return _serial_rx(serial, (uint8_t *)buffer, size);
}完整源码(os_serial_write)
static os_ssize_t os_serial_write(struct os_device *dev, os_off_t pos, const void *buffer, os_size_t size)
{
struct os_serial_device *serial;
OS_ASSERT(dev != OS_NULL);
if (size == 0)
return 0;
serial = (struct os_serial_device *)dev;
#ifdef OS_USING_CONSOLE
if (os_console_get_device() == dev)
{
#ifdef DLOG_USING_ASYNC_OUTPUT
if (os_is_irq_disabled())
return _serial_poll_tx(serial, (const uint8_t *)buffer, size);
#else
return _serial_poll_tx(serial, (const uint8_t *)buffer, size);
#endif
}
#endif
if (serial->ops->start_send != OS_NULL)
return _serial_tx(serial, (const uint8_t *)buffer, size);
return _serial_poll_tx(serial, (const uint8_t *)buffer, size);
}分步详解(os_serial_read)
第一步:校验长度非 0(条件 size != 0)
OS_ASSERT(dev != OS_NULL);
if (size == 0)
return 0;size == 0 时无需读取,直接返回 0。
第二步:委托给接收辅助函数(无条件执行)
serial = (struct os_serial_device *)dev;
return _serial_rx(serial, (uint8_t *)buffer, size);把 dev 转成 serial,调用 _serial_rx 从接收 FIFO 拷贝数据。_serial_rx 的完整源码与详解见下方"5.8 核心辅助函数 _serial_rx"。
分步详解(os_serial_write)
第一步:校验长度非 0(条件 size != 0)
OS_ASSERT(dev != OS_NULL);
if (size == 0)
return 0;第二步:判断是否 console 设备(条件 os_console_get_device() == dev)
#ifdef OS_USING_CONSOLE
if (os_console_get_device() == dev)
{
#ifdef DLOG_USING_ASYNC_OUTPUT
if (os_is_irq_disabled())
return _serial_poll_tx(serial, (const uint8_t *)buffer, size);
#else
return _serial_poll_tx(serial, (const uint8_t *)buffer, size);
#endif
}
#endif为什么 console 设备走轮询发送:console 是调试输出,os_kprintf 可能在中断、关调度、异常等不能睡眠/等信号量的上下文调用。_serial_poll_tx 是纯轮询(while 等 TXE 标志 + 逐字节发),不依赖信号量同步,可在任意上下文使用。若开了异步输出(DLOG_USING_ASYNC_OUTPUT)且当前关中断,同样必须走轮询。
第三步:判断是否支持中断发送(条件 start_send != OS_NULL)
if (serial->ops->start_send != OS_NULL)
return _serial_tx(serial, (const uint8_t *)buffer, size);
return _serial_poll_tx(serial, (const uint8_t *)buffer, size);非 console 设备:若 start_send 存在(支持中断/DMA 发送),用 _serial_tx(入队 + 启动发送,不阻塞);否则退化为 _serial_poll_tx 轮询发送。
关键理解:
os_serial_write的_serial_tx是非阻塞的(数据先入rbuff,由中断/DMA 后台发送);真正让写者阻塞等待的是os_device_write_block在设备框架层对tx_sem的等待(见 5.7)。_serial_tx本身只入队并尝试启动发送,立即返回。
条件走向总结(os_serial_write)
os_serial_write() 条件走向总结:
├── OS_ASSERT(dev != OS_NULL);size == 0 → 返回 0
├── OS_USING_CONSOLE 且 os_console_get_device() == dev(console 设备)
│ ├── DLOG_USING_ASYNC_OUTPUT 且 os_is_irq_disabled() → 返回 _serial_poll_tx
│ ├── 否则(非异步输出)→ 返回 _serial_poll_tx
├── 非 console 设备
│ ├── ops->start_send 非空 → 返回 _serial_tx(入队启动发送)
│ └── ops->start_send 为空 → 返回 _serial_poll_tx(轮询发送)5.6 框架层控制入口(os_serial_control)
函数签名
static os_err_t os_serial_control(struct os_device *dev, int cmd, void *args)参数 / 返回值说明
| 参数 | 类型 | 说明 |
|---|---|---|
dev | struct os_device * | 设备指针 |
cmd | int | 控制命令 |
args | void * | 命令参数指针 |
| 返回值 | 说明 |
|---|---|
OS_SUCCESS | 命令处理成功 |
OS_NOSYS | 命令不支持 |
OS_INVAL | 参数无效(如 termios 传入空指针) |
OS_EIO | 输入/输出错误 |
完整源码
static os_err_t os_serial_control(struct os_device *dev, int cmd, void *args)
{
os_err_t ret = OS_SUCCESS;
struct os_serial_device *serial;
OS_ASSERT(dev != OS_NULL);
serial = (struct os_serial_device *)dev;
switch (cmd)
{
case OS_DEVICE_CTRL_CONFIG:
if (args)
{
serial->config = *(struct serial_configure *)args;
if (serial->parent.ref_count)
{
os_serial_deinit(dev);
ret = os_serial_init(dev);
os_task_msleep(1);
os_task_msleep(1);
return ret;
}
}
break;
#ifdef OS_USING_POSIX_TERMIOS
case TCGETA:
{
struct termios *tio = (struct termios *)args;
if (tio == OS_NULL)
return OS_INVAL;
tio->c_iflag = 0;
tio->c_oflag = 0;
tio->c_lflag = 0;
/* Update oflag for console device */
#ifdef OS_USING_CONSOLE
if (os_console_get_device() == dev)
tio->c_oflag = OPOST | ONLCR;
#endif
/* Set cflag */
tio->c_cflag = 0;
if (serial->config.data_bits == DATA_BITS_5)
tio->c_cflag = CS5;
else if (serial->config.data_bits == DATA_BITS_6)
tio->c_cflag = CS6;
else if (serial->config.data_bits == DATA_BITS_7)
tio->c_cflag = CS7;
else if (serial->config.data_bits == DATA_BITS_8)
tio->c_cflag = CS8;
if (serial->config.stop_bits == STOP_BITS_2)
tio->c_cflag |= CSTOPB;
if (serial->config.parity == PARITY_EVEN)
tio->c_cflag |= PARENB;
else if (serial->config.parity == PARITY_ODD)
tio->c_cflag |= (PARODD | PARENB);
cfsetospeed(tio, _get_speed(serial->config.baud_rate));
}
break;
case TCSETAW:
case TCSETAF:
case TCSETA:
{
int baudrate;
struct serial_configure config;
struct termios *tio = (struct termios *)args;
if (tio == OS_NULL)
return OS_INVAL;
config = serial->config;
baudrate = _get_baudrate(cfgetospeed(tio));
config.baud_rate = baudrate;
switch (tio->c_cflag & CSIZE)
{
case CS5:
config.data_bits = DATA_BITS_5;
break;
case CS6:
config.data_bits = DATA_BITS_6;
break;
case CS7:
config.data_bits = DATA_BITS_7;
break;
default:
config.data_bits = DATA_BITS_8;
break;
}
if (tio->c_cflag & CSTOPB)
config.stop_bits = STOP_BITS_2;
else
config.stop_bits = STOP_BITS_1;
if (tio->c_cflag & PARENB)
{
if (tio->c_cflag & PARODD)
config.parity = PARITY_ODD;
else
config.parity = PARITY_EVEN;
}
else
config.parity = PARITY_NONE;
if (serial->ops->init)
serial->ops->init(serial, &config);
}
break;
case TCFLSH:
{
int queue = (int)args;
_tc_flush(serial, queue);
}
break;
case TCXONC:
break;
#endif
default:
/* Control device */
ret = OS_NOSYS;
break;
}
return ret;
}分步详解
第一步:按命令分发(条件 cmd 的值)
switch (cmd)
{
case OS_DEVICE_CTRL_CONFIG:
if (args)
{
serial->config = *(struct serial_configure *)args;
if (serial->parent.ref_count)
{
os_serial_deinit(dev);
ret = os_serial_init(dev);
os_task_msleep(1);
os_task_msleep(1);
return ret;
}
}
break;
#ifdef OS_USING_POSIX_TERMIOS
case TCGETA:
case TCSETAW:
case TCSETAF:
case TCSETA:
case TCFLSH:
case TCXONC:
/* 见 5.6 完整源码:POSIX termios 命令 */
break;
#endif
default:
/* Control device */
ret = OS_NOSYS;
break;
}
return ret;OS_DEVICE_CTRL_CONFIG 是框架层支持动态改配置的核心命令(见下方第二步);OS_USING_POSIX_TERMIOS 下的 TCGETA/TCSETAW/TCSETA/TCFLSH/TCXONC 是 POSIX termios 命令,用于兼容标准串口编程模型(其完整实现见上方"完整源码");其余命令走 default 返回 OS_NOSYS。
第二步:OS_DEVICE_CTRL_CONFIG —— 动态改配置(条件 args 非空)
case OS_DEVICE_CTRL_CONFIG:
if (args)
{
serial->config = *(struct serial_configure *)args;
if (serial->parent.ref_count)
{
os_serial_deinit(dev);
ret = os_serial_init(dev);
os_task_msleep(1);
os_task_msleep(1);
return ret;
}
}
break;- 把
args指向的serial_configure拷贝到serial->config; - 若设备已打开(
ref_count非 0):先os_serial_deinit(释放旧 FIFO)再os_serial_init(按新配置重新分配),中间os_task_msleep(1)两次等待旧发送完成;为什么:改波特率等参数会影响硬件时序,必须重新初始化硬件与缓冲,否则会出错。 - 若设备未打开,仅更新
config,等下次打开时应用。
第三步:termios 命令(条件 OS_USING_POSIX_TERMIOS)
TCGETA 读取当前配置填入 struct termios;TCSETAW/TCSETAF/TCSETA 从 termios 解析出波特率/数据位/停止位/校验位并调用 ops->init 应用;TCFLSH 调用 _tc_flush 清空 FIFO;TCXONC 空处理。这些是 POSIX 兼容层,普通串口使用可忽略。
第四步:默认分支返回不支持(条件 cmd 不匹配上述命令)
default:
/* Control device */
ret = OS_NOSYS;
break;
}
return ret;未知命令返回 OS_NOSYS。
条件走向总结(os_serial_control)
os_serial_control() 条件走向总结:
├── switch(cmd)
│ ├── OS_DEVICE_CTRL_CONFIG
│ │ ├── args 非空
│ │ │ ├── serial->config = *(serial_configure*)args
│ │ │ ├── ref_count 非 0(设备已打开)
│ │ │ │ └── os_serial_deinit → os_serial_init → 返回 ret
│ │ │ └── ref_count 为 0(未打开)
│ │ │ └── 仅更新 config,break
│ │ └── args 为空 → break
│ ├── TCGETA → 读配置到 termios(tio 为空 → 返回 OS_INVAL)
│ ├── TCSETAW/TCSETAF/TCSETA → 从 termios 应用配置(tio 为空 → 返回 OS_INVAL)
│ ├── TCFLSH → _tc_flush 清 FIFO
│ ├── TCXONC → 空处理
│ └── default → ret = OS_NOSYS
└── 返回 ret5.7 设备框架的阻塞/非阻塞读写(关联:os_device_read_block 等)
Serial 是字符设备,应用层实际通过设备框架的 os_device_read_block/write_block/read_nonblock/write_nonblock 操作,它们内部会调用 serial_ops 的 read/write(即 os_serial_read/os_serial_write)。这里简述阻塞语义:
来源:drivers/device.c(阻塞读的关键片段)
os_ssize_t os_device_read_block(os_device_t *dev, os_off_t pos, void *buffer, os_size_t size)
{
os_ssize_t count;
if (dev == OS_NULL)
{
return OS_INVAL;
}
OS_ASSERT(dev->ref_count != 0);
if (dev->ops == OS_NULL || dev->ops->read == OS_NULL)
return 0;
if (size == 0)
return 0;
if (dev->status == OS_DEVICE_STATUS_REMOVED)
return 0;
/* block */
os_semaphore_wait(dev->sem, OS_WAIT_FOREVER);
while ((dev->rx_size != 0) && (dev->rx_count == 0) && (dev->status == OS_DEVICE_STATUS_NORMAL))
{
os_semaphore_post(dev->sem);
if (os_semaphore_wait(dev->rx_sem, dev->rx_timeout) != OS_SUCCESS)
return 0;
os_semaphore_wait(dev->sem, OS_WAIT_FOREVER);
}
if (dev->status == OS_DEVICE_STATUS_REMOVED)
{
os_semaphore_post(dev->sem);
return 0;
}
/* read */
count = dev->ops->read(dev, pos, buffer, size);
os_device_notify(dev, ION_GENERIC_READ_BLOCK, count);
/* wake up other thread */
if (dev->rx_count != 0)
{
os_semaphore_post(dev->rx_sem);
}
os_semaphore_post(dev->sem);
return count;
}分步详解(阻塞读关键逻辑)
第一步:参数与状态校验(无条件执行)
if (dev->ops == OS_NULL || dev->ops->read == OS_NULL)
return 0;
if (size == 0)
return 0;
if (dev->status == OS_DEVICE_STATUS_REMOVED)
return 0;设备被移除、无读能力、或请求大小为 0 时返回 0。
第二步:无数据时阻塞等待(条件 rx_count == 0 且设备正常)
while ((dev->rx_size != 0) && (dev->rx_count == 0) && (dev->status == OS_DEVICE_STATUS_NORMAL))
{
os_semaphore_post(dev->sem);
if (os_semaphore_wait(dev->rx_sem, dev->rx_timeout) != OS_SUCCESS)
return 0;
os_semaphore_wait(dev->sem, OS_WAIT_FOREVER);
}关键循环:当"接收缓冲有大小(rx_size != 0)"且"当前无数据(rx_count == 0)"且"设备正常"时,阻塞等待 rx_sem。这个 rx_sem 由 os_hw_serial_isr_rxdone 里 os_device_recv_notify 唤醒。若超时返回 0。
第三步:读取数据并唤醒其他读者(无条件执行)
count = dev->ops->read(dev, pos, buffer, size);
os_device_notify(dev, ION_GENERIC_READ_BLOCK, count);
/* wake up other thread */
if (dev->rx_count != 0)
{
os_semaphore_post(dev->rx_sem);
}
os_semaphore_post(dev->sem);
return count;调用 ops->read(即 _serial_rx)读数据,os_device_notify 发读完成通知;若还有剩余数据则 os_semaphore_post(rx_sem) 唤醒其他等待读者;最后释放 dev->sem 并返回 count。
关联理解:
os_device_read_block阻塞在rx_sem;非阻塞版本os_device_read_nonblock在rx_count == 0时直接返回 0 不等待。二者最终都调用_serial_rx读数据。
5.8 核心辅助函数 _serial_rx(接收核心)
函数定位:_serial_rx 是 os_serial_read 调用的接收核心辅助函数,负责在自旋锁保护下,从 rx_fifo 环形缓冲取出数据。它在 os_serial_read 中被调用,本身无分支的主要逻辑但含条件判断。
完整源码
static int _serial_rx(struct os_serial_device *serial, uint8_t *data, int length)
{
int count;
os_ubase_t level;
if (length <= 0)
return 0;
OS_ASSERT(serial != OS_NULL);
os_spin_lock_irq(&serial->lock, &level);
OS_ASSERT(serial->rx_fifo != OS_NULL);
count = min(ring_count(&serial->rx_fifo->ring), length);
if (count != 0)
{
copy_ring_to_line(&serial->rx_fifo->ring, data, count);
}
serial->parent.rx_count = ring_count(&serial->rx_fifo->ring);
os_spin_unlock_irq(&serial->lock, level);
return count;
}分步详解
第一步:长度校验(条件 length > 0)
if (length <= 0)
return 0;
OS_ASSERT(serial != OS_NULL);length <= 0 无需读取,直接返回 0;断言 serial 非空。
第二步:加自旋锁保护(无条件执行)
os_spin_lock_irq(&serial->lock, &level);为什么用自旋锁而非常量/信号量:_serial_rx 可能被中断上下文打断(接收中断在改 rx_fifo),需要关中断 + 自旋锁保护临界区,防止与中断处理竞争。信号量在中断里无法使用。
第三步:计算可读字节数(无条件执行)
OS_ASSERT(serial->rx_fifo != OS_NULL);
count = min(ring_count(&serial->rx_fifo->ring), length);count = min(当前FIFO可读字节数, 请求长度)——取两者较小者,避免越界读。
数值演算:设
rx_fifo->ring中已有 20 字节,请求读 64 字节:
计算项 代入计算 结果 ring_count(ring)当前 head-tail 20 请求长度 length64 min(20, 64)取较小值 20 本次最多拷出 20 字节(FIFO 里只有 20 字节)。若请求读 8 字节,则
min(20, 8) = 8。
第四步:拷贝数据(条件 count != 0)
if (count != 0)
{
copy_ring_to_line(&serial->rx_fifo->ring, data, count);
}count != 0 时,用 copy_ring_to_line 从环形缓冲拷贝 count 字节到用户 data。
第五步:更新计数并解锁(无条件执行)
serial->parent.rx_count = ring_count(&serial->rx_fifo->ring);
os_spin_unlock_irq(&serial->lock, level);
return count;读取后更新 rx_count(剩余可读字节数),解锁,返回本次拷贝的字节数。
条件走向总结(_serial_rx)
_serial_rx() 条件走向总结:
├── length <= 0 → 返回 0
├── OS_ASSERT(serial != OS_NULL)
├── os_spin_lock_irq(关中断)
├── count = min(ring_count, length)
├── count != 0
│ └── copy_ring_to_line 拷贝数据
├── count == 0 → 不拷贝
├── 更新 parent.rx_count
├── os_spin_unlock_irq
└── 返回 count(实际拷贝字节数)5.9 核心辅助函数 _serial_tx(发送核心)
函数定位:_serial_tx 是 os_serial_write 调用的发送核心辅助函数,负责把数据入队 rbuff,并在空闲时启动第一块发送。它被 os_serial_write 调用(当 start_send 存在时)。
完整源码
static int _serial_tx(struct os_serial_device *serial, const uint8_t *data, int length)
{
int count;
os_ubase_t level;
struct os_serial_tx_fifo *tx_fifo;
os_spin_lock_irq(&serial->lock, &level);
tx_fifo = serial->tx_fifo;
length = rb_ring_buff_put(&tx_fifo->rbuff, data, length);
if (length == 0)
goto end;
if (tx_fifo->line_buff_count != 0)
goto end;
count = rb_ring_buff_get(&tx_fifo->rbuff, tx_fifo->line_buff, serial->config.tx_bufsz);
#ifdef OS_SERIAL_DELAY_CLOSE
os_semaphore_wait(serial->delay_sem, OS_NO_WAIT);
#endif
tx_fifo->line_buff_count = serial->ops->start_send(serial, tx_fifo->line_buff, count);
OS_ASSERT(tx_fifo->line_buff_count > 0);
if (tx_fifo->line_buff_count < count)
{
rb_ring_buff_put(&tx_fifo->rbuff,
tx_fifo->line_buff + tx_fifo->line_buff_count,
count - tx_fifo->line_buff_count);
}
end:
OS_ASSERT(tx_fifo->line_buff_count != 0);
serial->parent.tx_count = rb_ring_buff_data_len(&tx_fifo->rbuff);
os_spin_unlock_irq(&serial->lock, level);
return length;
}分步详解
第一步:加锁并取发送 FIFO(无条件执行)
os_spin_lock_irq(&serial->lock, &level);
tx_fifo = serial->tx_fifo;关中断加自旋锁,取 tx_fifo。
第二步:数据入队(无条件执行)
length = rb_ring_buff_put(&tx_fifo->rbuff, data, length);
if (length == 0)
goto end;
if (tx_fifo->line_buff_count != 0)
goto end;用 rb_ring_buff_put 把数据写入 rbuff,length 变为实际入队字节数。若入队 0(缓冲满)跳转 end;若当前已有发送在进行(line_buff_count != 0)也跳转 end——因为数据已入队,等 txdone 中断取下一块时自然会发。
第三步:启动第一块发送(条件缓冲空闲且取到数据)
count = rb_ring_buff_get(&tx_fifo->rbuff, tx_fifo->line_buff, serial->config.tx_bufsz);
#ifdef OS_SERIAL_DELAY_CLOSE
os_semaphore_wait(serial->delay_sem, OS_NO_WAIT);
#endif
tx_fifo->line_buff_count = serial->ops->start_send(serial, tx_fifo->line_buff, count);
OS_ASSERT(tx_fifo->line_buff_count > 0);
if (tx_fifo->line_buff_count < count)
{
rb_ring_buff_put(&tx_fifo->rbuff,
tx_fifo->line_buff + tx_fifo->line_buff_count,
count - tx_fifo->line_buff_count);
}走到这里说明缓冲空闲,从 rbuff 取出一块到 line_buff,调用 start_send 启动发送。line_buff_count 记录本块待发字节数;若 start_send 只接收部分,剩余退回 rbuff。
第四步:更新计数并解锁(无条件执行)
end:
OS_ASSERT(tx_fifo->line_buff_count != 0);
serial->parent.tx_count = rb_ring_buff_data_len(&tx_fifo->rbuff);
os_spin_unlock_irq(&serial->lock, level);
return length;为什么 end: 处断言 line_buff_count != 0:发送缓冲不可能完全空闲(要么刚入队的数据在 rbuff,要么正在发送的块在 line_buff)。若为 0 说明逻辑异常。最后更新 tx_count 并返回实际入队字节数。
条件走向总结(_serial_tx)
_serial_tx() 条件走向总结:
├── os_spin_lock_irq
├── length = rb_ring_buff_put(rbuff, data, length)(数据入队)
├── length == 0(缓冲满)→ goto end
├── line_buff_count != 0(已有发送在进行)→ goto end
├── 否则(缓冲空闲)
│ ├── count = rb_ring_buff_get(rbuff, line_buff, tx_bufsz)
│ ├── OS_SERIAL_DELAY_CLOSE:os_semaphore_wait(delay_sem, NO_WAIT)
│ ├── line_buff_count = start_send(line_buff, count)(启动发送)
│ ├── line_buff_count < count:剩余字节退回 rbuff
│ └── end:
├── OS_ASSERT(line_buff_count != 0)
├── 更新 parent.tx_count
├── os_spin_unlock_irq
└── 返回 length(入队字节数)5.10 核心辅助函数 _serial_poll_tx(轮询发送)
函数定位:_serial_poll_tx 是轮询发送辅助函数,被 os_serial_write(console 设备、或 start_send 为空时)调用。它不依赖中断/DMA,逐字节等待 TXE 标志后发送,可在任何上下文使用。
完整源码
static int _serial_poll_tx(struct os_serial_device *serial, const uint8_t *data, int length)
{
int count;
int send_index = 0;
OS_ASSERT(serial != OS_NULL);
while (send_index < length)
{
count = serial->ops->poll_send(serial, data + send_index, length - send_index);
if (count <= 0)
{
break;
}
send_index += count;
}
return send_index;
}分步详解
第一步:循环发送(条件 send_index < length)
while (send_index < length)
{
count = serial->ops->poll_send(serial, data + send_index, length - send_index);
if (count <= 0)
{
break;
}
send_index += count;
}循环调用 poll_send 发送剩余数据。poll_send 返回本次实际发送字节数;若 count <= 0(发送出错)跳出循环,避免死循环。
第二步:返回累计发送字节数(无条件执行)
return send_index;返回实际发送的总字节数(可能小于 length,若中途出错)。
条件走向总结(_serial_poll_tx)
_serial_poll_tx() 条件走向总结:
├── 循环(send_index < length)
│ ├── count = poll_send(data+send_index, length-send_index)
│ ├── count <= 0(发送出错)→ break
│ └── count > 0 → send_index += count
└── 返回 send_index(实际发送字节数)六、使用示例
以下示例基于项目头文件
drivers/serial/serial.h与设备框架接口,均含返回值检查。
示例 1:串口基本收发(阻塞读写)
#include <serial.h>
#include <device.h>
#include <os_task.h>
#include <string.h>
#define UART_NAME "uart0"
void serial_demo_task(void *arg)
{
os_device_t *dev;
char rx_buf[128];
int len;
/* 1. 打开串口设备 */
dev = os_device_open_s(UART_NAME);
if (dev == OS_NULL)
{
os_kprintf("Failed to open %s\r\n", UART_NAME);
return;
}
/* 2. 发送欢迎信息(阻塞写,注意长度 22 需与字符串一致) */
const char *welcome = "Serial Demo Started!\r\n";
if (os_device_write_block(dev, 0, welcome, strlen(welcome)) != (os_ssize_t)strlen(welcome))
{
os_kprintf("Write welcome failed\r\n");
os_device_close(dev);
return;
}
while (1)
{
/* 3. 阻塞读取串口数据(无数据时任务阻塞等待) */
len = os_device_read_block(dev, 0, rx_buf, sizeof(rx_buf) - 1);
if (len > 0)
{
rx_buf[len] = '\0';
os_kprintf("Received: %s\r\n", rx_buf);
/* 4. 回显数据 */
os_device_write_block(dev, 0, "Echo: ", 6);
os_device_write_block(dev, 0, rx_buf, len);
}
}
}实际效果:打开 uart0 后,先发欢迎串;进入循环,os_device_read_block 在有数据时返回读到的字节数并回显,无数据时阻塞等待(由接收中断 os_hw_serial_isr_rxdone → os_device_recv_notify 唤醒)。
示例 2:动态修改串口配置
#include <serial.h>
#include <device.h>
void change_baudrate_demo(void)
{
os_device_t *dev;
struct serial_configure cfg;
dev = os_device_open_s("uart0");
if (dev == OS_NULL)
return;
/* 1. 基于默认配置构造新配置 */
cfg = OS_SERIAL_CONFIG_DEFAULT;
/* 2. 修改为 921600-8-N-1 */
cfg.baud_rate = BAUD_RATE_921600;
cfg.data_bits = DATA_BITS_8;
cfg.stop_bits = STOP_BITS_1;
cfg.parity = PARITY_NONE;
/* 3. 应用新配置(设备已打开,会 deinit 再 init) */
os_err_t ret = os_device_control(dev, OS_DEVICE_CTRL_CONFIG, &cfg);
if (ret != OS_SUCCESS)
{
os_kprintf("Change config failed: %d\r\n", ret);
}
else
{
os_kprintf("Baudrate changed to 921600\r\n");
}
os_device_close(dev);
}实际效果:把 uart0 配置动态改为 921600-8-N-1。因设备已打开(ref_count 非 0),os_serial_control 会先 os_serial_deinit 再 os_serial_init,重新按新波特率初始化硬件。
示例 3:非阻塞读取
#include <serial.h>
#include <device.h>
void nonblock_read_demo(void)
{
os_device_t *dev;
char buf[64];
int len;
dev = os_device_open_s("uart0");
if (dev == OS_NULL)
return;
while (1)
{
/* 非阻塞读取,无数据立即返回 0 */
len = os_device_read_nonblock(dev, 0, buf, sizeof(buf));
if (len > 0)
{
os_kprintf("Got %d bytes: %.*s\r\n", len, len, buf);
}
else
{
/* 没有数据,做其他事情,避免空转 */
os_task_msleep(10);
}
}
}实际效果:os_device_read_nonblock 在 rx_count == 0 时直接返回 0(不阻塞)。任务轮询读取,无数据时 os_task_msleep(10) 让出 CPU,避免忙等。
示例 4:注册自定义串口设备
#include <serial.h>
#include <device.h>
/* 底层 UART 操作实现 */
static int my_uart_init(struct os_serial_device *serial, struct serial_configure *cfg)
{
/* 根据 cfg 配置硬件波特率/数据位/停止位/校验位 */
return OS_SUCCESS;
}
static int my_uart_deinit(struct os_serial_device *serial)
{
/* 关闭硬件 UART */
return OS_SUCCESS;
}
static int my_uart_start_send(struct os_serial_device *serial, const uint8_t *buff, os_size_t size)
{
/* 启动中断发送,返回实际接收的字节数 */
return size;
}
static int my_uart_poll_send(struct os_serial_device *serial, const uint8_t *buff, os_size_t size)
{
/* 轮询发送(阻塞),逐字节发 */
int i;
for (i = 0; i < size; i++)
{
/* 等待 TXE,发送 buff[i] */
}
return size;
}
static const struct os_uart_ops my_uart_ops =
{
.init = my_uart_init,
.deinit = my_uart_deinit,
.start_send = my_uart_start_send,
.poll_send = my_uart_poll_send,
};
static struct os_serial_device my_serial;
void my_serial_init(void)
{
/* 1. 配置串口参数 */
my_serial.config = (struct serial_configure)OS_SERIAL_CONFIG_DEFAULT;
my_serial.ops = &my_uart_ops;
/* 2. 注册到设备框架 */
os_err_t ret = os_hw_serial_register(&my_serial, "uart0", OS_NULL);
if (ret != OS_SUCCESS)
{
os_kprintf("Serial register failed: %d\r\n", ret);
return;
}
os_kprintf("Serial uart0 registered\r\n");
}实际效果:实现 os_uart_ops 四个函数后,填充 my_serial.ops 与 config,os_hw_serial_register 注册为字符设备 "uart0"。注册成功后应用层可用 os_device_open_s("uart0") 操作它。
示例 5:DMA + 中断接收高级用法
此示例展示如何使用 DMA 接收 + 中断通知实现高效的不定长数据接收:
#include <serial.h>
#include <device.h>
#include <os_task.h>
#define UART_NAME "uart0"
#define RX_BUFFER_SIZE 256
static os_device_t *g_serial_dev = OS_NULL;
static char g_rx_buffer[RX_BUFFER_SIZE];
/* 串口接收任务 */
void serial_rx_task(void *parameter)
{
int len;
while (1)
{
/* 等待接收事件(阻塞等待中断/DMA 通知) */
len = os_device_read_block(g_serial_dev, 0, g_rx_buffer, sizeof(g_rx_buffer) - 1);
if (len > 0)
{
g_rx_buffer[len] = '\0';
os_kprintf("[Serial RX] %d bytes: %s\r\n", len, g_rx_buffer);
}
}
}
/* 串口初始化 */
void serial_dma_init(void)
{
os_device_t *dev;
dev = os_device_open_s(UART_NAME);
if (dev == OS_NULL)
{
os_kprintf("Failed to open %s\r\n", UART_NAME);
return;
}
g_serial_dev = dev;
os_kprintf("Serial DMA mode initialized\r\n");
}说明:DMA 接收的底层驱动在中断中把数据写入
rx_fifo->ring.buff后调用os_hw_serial_isr_rxdone,再经os_device_recv_notify唤醒serial_rx_task。应用层代码与中断/轮询模式完全一致,体现框架解耦价值。
七、更换 MCU(Serial 驱动移植)—— 全局视角
本节从全局视角讲解:当你的项目要换一颗新的 MCU 时,OneOS 的 Serial 设备驱动要做什么才能在新平台上跑起来。核心结论与 5.2/5.3 一样:应用层与框架层完全不用动,只需要新写/移植 BSP 层的串口驱动。
在动手移植之前,先认识 BSP 层驱动文件到底承担了什么职责——这是理解移植步骤的前提。
7.1 认识 BSP 层驱动:drv_usart.c / drv_uart.c
BSP 层的串口驱动文件名为 drv_usart.c 或 drv_uart.c(各厂商命名不同),承担"实现 struct os_uart_ops + 中断处理 + 注册"三件事:
| 文件 | 作用 |
|---|---|
drv_usart.c / drv_uart.c | 实现 struct os_uart_ops 四个函数 + UART 中断处理 + probe 注册 |
(部分厂商)drv_usart.h | 声明 UART 实例结构体、引脚/时钟配置宏 |
以 STM32 为例(drivers/hal/st/drivers/drv_usart.c):
(1)定义"UART 实例"结构体 struct stm32_uart
struct stm32_uart
{
struct os_serial_device serial;
soft_dma_t sdma;
UART_HandleTypeDef *huart;
os_list_node_t list;
uint8_t *rx_buff;
uint32_t rx_size;
#ifdef OS_DEVICE_SUPPORT_PLUG
os_plug_t plug;
#endif
};serial:内嵌的struct os_serial_device,是注册进设备框架的"公共部分";huart:指向 STM32 HAL 的 UART 句柄(对应具体 UART 外设);sdma:软 DMA 控制块(用于 DMA 接收);list:把多个 UART 实例挂到全局链表(供中断回调按huart找到实例)。
(2)实现 struct os_uart_ops 的四个函数并聚合
static const struct os_uart_ops stm32_uart_ops = {
.init = stm32_uart_init,
.deinit = stm32_uart_deinit,
.start_send = stm32_uart_start_send,
.poll_send = stm32_uart_poll_send,
};(3)写中断回调并调用 os_hw_serial_isr_rxdone/txdone
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
struct stm32_uart *uart;
os_list_for_each_entry(uart, &stm32_uart_list, struct stm32_uart, list)
{
if (uart->huart == huart)
{
os_hw_serial_isr_txdone((struct os_serial_device *)uart);
break;
}
}
}发送完成中断回调里调用框架的 os_hw_serial_isr_txdone,通知框架"上一块发完,可以发下一块"。
(4)写 probe 函数并注册
static int stm32_usart_probe(const os_driver_info_t *drv, const os_device_info_t *dev)
{
struct serial_configure config = OS_SERIAL_CONFIG_DEFAULT;
os_err_t result = 0;
os_ubase_t level;
struct stm32_uart *uart = os_calloc(1, sizeof(struct stm32_uart));
OS_ASSERT(uart);
uart->huart = (UART_HandleTypeDef *)dev->info;
if (HAL_UART_DeInit(uart->huart) != HAL_OK)
{
return OS_FAILURE;
}
struct os_serial_device *serial = &uart->serial;
serial->ops = &stm32_uart_ops;
serial->config = config;
stm32_usart_parse_configs_from_stm32cube(uart);
OS_ASSERT(uart->huart->hdmatx == OS_NULL || uart->huart->hdmatx->Init.Mode == DMA_NORMAL);
os_spin_lock_irqsave(&gs_device_lock, &level);
os_list_add_tail(&stm32_uart_list, &uart->list);
os_spin_unlock_irqrestore(&gs_device_lock, level);
#ifdef OS_DEVICE_SUPPORT_PLUG
uart->plug.upper = &serial->plug;
uart->plug.halt = stm32_uart_halt;
uart->plug.release = stm32_uart_release;
os_plug_in(&uart->plug, "uart", dev->name);
#endif
result = os_hw_serial_register(serial, dev->name, NULL);
OS_ASSERT(result == OS_SUCCESS);
return result;
}
OS_DRIVER_INFO stm32_usart_driver = {
.name = "UART_HandleTypeDef",
.probe = stm32_usart_probe,
};
OS_DRIVER_DEFINE(stm32_usart_driver, OS_INIT_LEVEL_PRE_DEVICE, OS_INIT_SUBLEVEL_HIGH);说明:上面 probe 片段仅摘取与注册主线相关的语句(
stm32_usart_parse_configs_from_stm32cube把 CubeMX 生成的huart->Init参数映射到serial->config等中间步骤未在此展开),完整实现见项目drivers/hal/st/drivers/drv_usart.c。移植时关键是把握"取硬件信息 → 填 ops 与 config →os_hw_serial_register"这条主线。
(5)不同 MCU 的同类文件
| 厂商 | 路径 | 区别 |
|---|---|---|
| ST | drivers/hal/st/drivers/drv_usart.c | 用 STM32 HAL(HAL_UART_Transmit_IT/DMA、LL_USART_*) |
| GD | drivers/hal/gd/drivers/f403/drv_usart.c | 用 GD32 库 |
| NRF | drivers/hal/nrf/drivers/nrf52/drv_uart.c | 用 nRF5 SDK 的 UARTE 模块 |
关键理解:这些 drv_usart.c/drv_uart.c 是并列的"平级实现",都实现同一个 struct os_uart_ops 契约。它们的存在意义是:同一套应用代码,通过换一个 BSP 驱动文件,就能跑在不同 MCU 上——这正是移植/跨平台的核心。
(6)"根据所选 MCU 决定"到底是怎么决定的?—— 构建系统的三层选择机制
这是理解 OneOS 跨平台编译的关键。项目不是把所有串口驱动都编译进去,而是通过 Kconfig 配置 + weave.yaml 条件编译,只编译当前所选 MCU 的那一份(与 5.2/5.3 的机制完全一致,只是把 drv_gpio.c/drv_i2c.c 换成 drv_usart.c):
第一层:选择"厂商宏"(templates/configs/Kconfig 的 choice)
choice
prompt "MANUFACTOR"
default MANUFACTOR_STM32
config MANUFACTOR_STM32
bool "STM32"
config MANUFACTOR_GD32
bool "GD32"
# 其他厂商选项依此类推
endchoice第二层:SRC_HAL 决定加载哪家目录(templates/configs/SConscript 的映射 + drivers/hal/Kconfig 的 source)
inq_table['MANUFACTOR_STM32'] = 'st'
inq_table['MANUFACTOR_GD32'] = 'gd'第三层:weave.yaml 决定该厂商下哪些源文件被编译(ST 的 drivers/hal/st/drivers/weave.yaml)
group_name: hal/drivers
# 依赖宏控:只有定义了 MANUFACTOR_STM32 才编译整个组
depend_macro:
- MANUFACTOR_STM32
source_file:
- drv_usart.c ? {is_define(['OS_USING_SERIAL', 'HAL_UART_MODULE_ENABLED'])}drv_usart.c ? {is_define(['OS_USING_SERIAL', 'HAL_UART_MODULE_ENABLED'])} 意思是:只有当同时定义了 OS_USING_SERIAL(使能 Serial 设备组件)和 HAL_UART_MODULE_ENABLED(使能 UART 硬件模块)时,才编译 drv_usart.c。
完整链路图(以"选 STM32 + 开 Serial 组件"为例):
① 配置阶段
templates/configs/Kconfig
选择 MANUFACTOR_STM32 → 定义宏 MANUFACTOR_STM32
② 目录选择
SConscript: inq_table['MANUFACTOR_STM32'] = 'st' → SRC_HAL = st
drivers/hal/Kconfig: source "$OS_ROOT/hal/$(SRC_HAL)/Kconfig" → 加载 hal/st/Kconfig
③ 文件级条件编译
drivers/hal/st/drivers/weave.yaml
depend_macro: MANUFACTOR_STM32 (组级:必须选 STM32)
drv_usart.c ? {is_define(['OS_USING_SERIAL',
'HAL_UART_MODULE_ENABLED'])} (文件级:还要开 Serial + UART)
↓ 条件都满足才编译
drv_usart.c → 链接进固件结论:换 MCU 厂商 → 厂商宏变 → SRC_HAL 变 → 编译的 drv_usart.c 换成新厂商那份。
7.2 移植前的全局视角:分层全景图
回顾 Serial 设备的完整分层(本书 1.1 节已详解):
┌─────────────────────────────────────────────────────────────┐
│ 【应用层】 用户代码(serial_demo_task / serial_rx_task 等) │
│ 用 os_device_open_s / os_device_read_block / write_block │
└──────────────────────────────┬──────────────────────────────┘
│ os_device_read/write/control
▼
┌─────────────────────────────────────────────────────────────┐
│ 【框架层】 drivers/serial/serial.c(与 MCU 无关,通用代码) │
│ os_serial_init/deinit/read/write/control │
│ _serial_rx / _serial_tx / _serial_poll_tx │
│ os_hw_serial_register / isr_rxdone / isr_txdone │
│ rx_fifo / tx_fifo(FIFO 缓冲) │
└──────────────────────────────┬──────────────────────────────┘
│ 调用 serial->ops->init/start_send/poll_send
▼
┌─────────────────────────────────────────────────────────────┐
│ 【BSP 层】 drivers/hal/xxx/drivers/drv_usart.c(需按 MCU 实现)│
│ 实现 struct os_uart_ops 四个函数 + 中断处理 + probe 注册 │
│ 真正操作寄存器(HAL_UART_* / LL_USART_*) │
└──────────────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 【硬件层】 具体 MCU 的 UART 外设寄存器 │
└─────────────────────────────────────────────────────────────┘移植的关键洞察:
- 只有 BSP 层和 MCU 强相关(依赖具体寄存器、HAL 库、引脚分配、时钟树、中断)。
- 框架层和应用层与 MCU 无关(只依赖
serial.h定义的抽象接口)。 - 所以"换 MCU"本质上 = 重写/移植 BSP 层的
drv_usart.c。
7.3 移植需要提供的"接口契约"
要移植,必须先满足 BSP 层与框架层之间的契约——也就是 struct os_uart_ops 的四个函数(见 2.5 节)必须全部实现,并映射到新 MCU 的硬件能力:
| ops 函数 | 需实现的能力 | 新 MCU 上对应什么 |
|---|---|---|
init | 初始化 UART 硬件,应用波特率/数据位/停止位/校验位 | 配置 UART 控制寄存器、波特率寄存器、使能外设 |
deinit | 反初始化 UART 硬件 | 关闭外设、释放引脚 |
start_send | 启动中断/DMA发送,返回实际接收的字节数 | 配置 TX 中断或 DMA 搬移,启动发送 |
poll_send | 轮询发送(阻塞),返回实际发送字节数 | while(!TXE) 等发送寄存器空,写数据寄存器 |
此外还需要:
- UART 实例结构体
struct xxx_uart:内嵌struct os_serial_device,扩展硬件句柄、DMA 控制块、链表节点(如 ST 的struct stm32_uart)。 - 中断回调:接收完成中断调用
os_hw_serial_isr_rxdone,发送完成中断调用os_hw_serial_isr_txdone。 - probe/初始化函数:取硬件信息、填
ops与config,最后调用os_hw_serial_register,并通过OS_DRIVER_DEFINE让系统自动调用。
7.4 移植的标准流程(5 步)
下面以"把 Serial 驱动从 STM32 移植到一颗新 MCU"为例,给出通用流程:
第 1 步:定义 UART 实例结构体与硬件配置
/* 内嵌框架层 os_serial_device,扩展本 MCU 硬件信息 */
struct my_uart
{
struct os_serial_device serial; /* 框架层公共部分 */
MY_UART_HandleTypeDef huart; /* 新 MCU 的 UART 句柄 */
os_list_node_t list; /* 挂到实例链表 */
};第 2 步:实现四个 ops 函数
以 start_send 与 poll_send 为例:
static int my_uart_start_send(struct os_serial_device *serial, const uint8_t *buff, os_size_t size)
{
struct my_uart *uart = os_container_of(serial, struct my_uart, serial);
/* 用新 MCU 的中断/DMA 发送,返回实际接收字节数(成功返回 size) */
if (MY_UART_Transmit_IT(uart->huart, buff, size) == OK)
return size;
return 0;
}
static int my_uart_poll_send(struct os_serial_device *serial, const uint8_t *buff, os_size_t size)
{
struct my_uart *uart = os_container_of(serial, struct my_uart, serial);
int i;
for (i = 0; i < size; i++)
{
/* 等待 TXE 空,发送一个字节 */
while (!MY_UART_IsActiveFlag_TXE(uart->huart));
MY_UART_TransmitData8(uart->huart, buff[i]);
}
return size;
}关键点:
- 用
os_container_of(serial, struct my_uart, serial)从框架层的serial反查到 BSP 实例(因为serial是内嵌在my_uart里的)。 start_send返回"本次启动发送的字节数"(通常就是size,除非硬件限制)。poll_send必须循环逐字节发完,等待 TXE 标志,返回实际发送数。
第 3 步:定义并导出 ops 操作表
static const struct os_uart_ops my_uart_ops = {
.init = my_uart_init,
.deinit = my_uart_deinit,
.start_send = my_uart_start_send,
.poll_send = my_uart_poll_send,
};第 4 步:写中断回调 + probe 注册
/* 发送完成中断回调:通知框架发下一块 */
void MY_UART_IRQHandler_TxDone(void)
{
struct my_uart *uart = &g_my_uart; /* 根据硬件中断上下文找到对应实例 */
os_hw_serial_isr_txdone((struct os_serial_device *)uart);
}
/* 接收中断回调:通知框架有数据 */
void MY_UART_IRQHandler_RxDone(void)
{
struct my_uart *uart = &g_my_uart; /* 数据已由 DMA/ISR 写入 rx_fifo->ring.buff */
os_hw_serial_isr_rxdone((struct os_serial_device *)uart);
}
static int my_uart_probe(const os_driver_info_t *drv, const os_device_info_t *dev)
{
struct my_uart *uart = os_calloc(1, sizeof(struct my_uart));
OS_ASSERT(uart);
uart->huart = (MY_UART_HandleTypeDef *)dev->info;
struct os_serial_device *serial = &uart->serial;
serial->ops = &my_uart_ops;
serial->config = OS_SERIAL_CONFIG_DEFAULT;
/* 可选:把硬件默认参数映射到 serial->config(如波特率) */
os_list_add_tail(&my_uart_list, &uart->list);
return os_hw_serial_register(serial, dev->name, NULL);
}
OS_DRIVER_INFO my_uart_driver = {
.name = "MY_UART_HandleTypeDef",
.probe = my_uart_probe,
};
OS_DRIVER_DEFINE(my_uart_driver, OS_INIT_LEVEL_PRE_DEVICE, OS_INIT_SUBLEVEL_HIGH);关键点:
- 中断回调里用
os_hw_serial_isr_rxdone/txdone与框架交互,不要直接操作 FIFO(那是框架职责)。 - probe 里填
ops与config,os_hw_serial_register完成注册。
第 5 步:验证
移植完成后,用本节"示例 1(串口基本收发)"跑通即可验证:
dev = os_device_open_s("uart0");
if (dev == OS_NULL) return;
os_device_write_block(dev, 0, "Hello\r\n", 7); /* 发送 */
int len = os_device_read_block(dev, 0, buf, sizeof(buf)); /* 接收 */- 能收到自己发的数据 → 发送路径(
start_send/poll_send)正确。 - 能从外部串口发来并收到 → 接收中断 +
os_hw_serial_isr_rxdone+_serial_rx链路正确。 - 连续大块发送正确 →
_serial_tx+start_send+txdone的流水线正确。
7.5 芯片未适配时该怎么办?(新建 BSP 还是补型号)
与 5.2/5.3 的判断完全一致:已有厂商(补型号)vs 全新厂商(新建 BSP)。
7.5.1 判断标准
| 情况 | 判断标准 | 工作量大不大 |
|---|---|---|
| 情况 A:已有厂商 | 厂商目录已存在(如 st/),只是具体型号没列 | 小,通常不改串口驱动 |
| 情况 B:全新厂商 | 厂商在 drivers/hal/ 完全没有目录 | 大,需整套新建 BSP + 移植串口驱动 |
7.5.2 情况 A:已有厂商,只是具体型号没适配
同厂商同系列 UART 寄存器结构高度一致,drv_usart.c 的 struct os_uart_ops 实现通常直接复用:
- 第 1 步:确认
drv_usart.c的引脚/时钟/中断配置覆盖你的型号。 - 第 2 步:在厂商 Kconfig 加"芯片型号"选项,使其
select正确的时钟树与HAL_UART_MODULE_ENABLED。 - 第 3 步:用厂商工具生成外设配置(
peripherals.c,含 UART 引脚复用)。 - 第 4 步:写板级文件并确认
OS_DRIVER_DEFINE的 probe 在应用使用前被调用。 - 第 5 步:验证。
要点:情况 A 的核心是"型号配置 + 串口引脚复用",串口驱动代码基本不用动。
7.5.3 情况 B:全新厂商,需要从零新建 BSP
这才是真正需要"移植串口驱动"的情况:
- 第 1 步:新建
drivers/hal/my_chip/目录(含drv_usart.c、Kconfig、weave.yaml、board 文件、启动文件)。 - 第 2 步:按 7.4 的 5 步实现串口驱动。
- 第 3 步:接入三层选择机制——Kconfig choice 加
MANUFACTOR_MYCHIP、SConscript 加inq_table['MANUFACTOR_MYCHIP']='my_chip'、weave.yaml 加drv_usart.c ? {is_define(['OS_USING_SERIAL', 'HAL_UART_MODULE_ENABLED'])}。 - 第 4 步:验证。
7.5.4 两种情况的对比速查
| 维度 | 情况 A(已有厂商补型号) | 情况 B(全新厂商新建 BSP) |
|---|---|---|
| 是否新建目录 | 否,复用厂商目录 | 是,新建 drivers/hal/my_chip/ |
是否改 drv_usart.c | 通常不改(或改引脚/时钟配置) | 从零实现四个 ops |
是否改厂商宏/SRC_HAL | 否 | 是 |
| 是否改 weave.yaml | 否 | 是(新增 drv_usart.c 的 is_define) |
| 工作量 | 小 | 大 |
7.6 移植检查清单(快速自查)
| 检查项 | 是否满足 |
|---|---|
struct os_uart_ops 四个函数全部实现并绑定 | ☐ |
init/deinit/poll_send 必须非空(os_hw_serial_register 会断言) | ☐ |
接收中断回调调用 os_hw_serial_isr_rxdone | ☐ |
发送完成中断回调调用 os_hw_serial_isr_txdone | ☐ |
os_container_of 从 serial 反查实例正确 | ☐ |
os_hw_serial_register 在应用使用前被调用(OS_DRIVER_DEFINE) | ☐ |
| 多 UART 外设时,每个外设独立实例并各注册一次 | ☐ |
DMA 缓冲对齐(os_dma_malloc_align + cache 一致) | ☐ |
7.7 移植的价值与注意事项
价值:一旦 BSP 层移植完成,应用层代码一行不改即可在多个 MCU 间复用。
注意事项:
init/deinit/poll_send必须实现:os_hw_serial_register会OS_ASSERT这三个非空,缺一个注册即失败。- 中断回调别直接操作 FIFO:接收/发送完成只调
os_hw_serial_isr_rxdone/txdone,FIFO 由框架管理,否则破坏一致性。 - DMA 缓冲要对齐并维护 cache:接收用
os_dma_malloc_align分配,DMA 与 CPU 访问需 cache 一致(os_invalid_dcache)。 - console 特殊路径:若你的串口是 console,
os_serial_write会走_serial_poll_tx,需确保poll_send实现正确,否则os_kprintf无法输出。 - 多 UART 外设各注册一次:每颗 MCU 往往有多个 UART,每个实例结构体独立、各调一次
os_hw_serial_register(如uart0、uart1)。
📝 本节小结
核心要点回顾
- 分层抽象设计:Serial 设备在 5.1 设备框架上二次封装,
os_serial_device内嵌os_device,通过os_uart_ops与底层 BSP 驱动解耦。 - FIFO 缓冲:接收用 DMA 环形缓冲(
rx_fifo),发送用"环形缓冲 + 行缓冲"两层(tx_fifo)。 - 位域配置存储:
serial_configure用位域紧凑存储配置参数,共占 3 个uint32_t(12 字节),BSP 按位域访问。 - 配置参数:默认 115200-8-N-1,收发缓冲默认 64 字节(
OS_ALIGN_UP(rx_bufsz+1,4)实际分配)。 - 框架读写流程:
os_serial_read→_serial_rx(自旋锁保护,从 rx_fifo 拷贝);os_serial_write→_serial_tx(入队启动发送)或_serial_poll_tx(轮询);console 走轮询。 - 事件机制:接收中断
os_hw_serial_isr_rxdone、发送完成中断os_hw_serial_isr_txdone,配合设备框架rx_sem/tx_sem实现阻塞读写。 - 注册流程:
os_hw_serial_register设置字符设备类型、挂serial_ops、创建信号量、注册设备框架;OS_DEVICE_CTRL_CONFIG可动态改配置(重新 init)。 - 更换 MCU 移植(见第七章):移植核心 = 实现
struct os_uart_ops四个函数 + 中断回调 + probe 注册;框架层与应用层不用改;换 MCU 本质是换SRC_HAL对应的那份drv_usart.c。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
os_hw_serial_register 前 init/deinit/poll_send 未实现 | 三个断言必挂 → 三个函数都必须实现(start_send 可空) |
在中断里调用阻塞读写(os_device_read_block) | 会等信号量无法睡眠 → 数据收发应在任务上下文 |
console 串口配置错了 poll_send | os_kprintf 走 _serial_poll_tx → 必须正确实现 poll_send |
中断回调里直接操作 rx_fifo/tx_fifo | FIFO 由框架管理 → 回调只调 os_hw_serial_isr_rxdone/txdone |
| 改波特率后不重新 init | 硬件时序错乱 → 用 OS_DEVICE_CTRL_CONFIG(设备已打开会自动 deinit+init) |
| 接收缓冲取实际大小弄错 | 实际分配 OS_ALIGN_UP(rx_bufsz+1, 4),非裸 rx_bufsz |
| 位域字段访问越界 | data_bits 等是位域,需按宏赋值(如 DATA_BITS_8),不能随便赋大数 |
| 移植时 DMA 缓冲未对齐/未刷 cache | DMA 与 CPU 数据不一致 → 用 os_dma_malloc_align + cache 维护 |
| 移植时漏掉中断回调 | 数据接收/发送无法驱动框架 → 接收/发送中断回调必须调 isr_rxdone/txdone |
下一步
- 5.5 SPI设备:了解 OneOS SPI 总线/设备的配置、传输 API 与使用示例。