5.4 Serial 设备
5.4 Serial 设备
📚 本节导读
学习时长: 约 50 分钟
难度级别: ⭐⭐⭐⭐☆
前置知识: C 语言基础、MCU 串口基础知识、5.1 设备驱动框架
🎯 学习目标
- 理解 OneOS 中 Serial 设备的框架设计与 FIFO 缓冲机制
- 掌握串口配置参数(波特率、数据位、停止位、校验位)的设置方法
- 掌握串口数据的阻塞/非阻塞读写操作
- 理解串口事件机制与 DMA 支持
- 能够编写完整的串口数据收发程序
一、概述
Serial(串口/UART)设备是嵌入式系统中最常用的通信接口之一。OneOS 将串口抽象为字符设备(OS_DEVICE_TYPE_CHAR),在标准设备框架之上增加了 FIFO 缓冲区、DMA 收发、事件通知等机制,提供高效可靠的串口通信能力。Serial 设备驱动位于 drivers/serial/ 目录下,核心头文件为 serial.h。
Serial 设备框架的特点:
- FIFO 缓冲:收发各配备独立的 FIFO 环形缓冲区,有效避免数据丢失
- DMA 支持:支持 DMA 收发模式,适用于大数据量传输场景
- 事件机制:通过设备通知机制,支持 RX 到达、TX 完成、DMA 完成、超时等事件
- 中断驱动:基于中断的收发模式,不阻塞 CPU
- POSIX 兼容:可选支持 POSIX termios 接口,兼容标准串口编程模型
二、核心数据结构
2.1 串口设备结构体 os_serial_device
struct os_serial_device
{
struct os_device parent; /* 继承自 os_device_t */
const struct os_uart_ops *ops; /* 底层 UART 操作函数表 */
struct serial_configure config; /* 串口配置参数 */
struct os_serial_rx_fifo *rx_fifo; /* 接收 FIFO 缓冲区 */
struct os_serial_tx_fifo *tx_fifo; /* 发送 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; /* 自旋锁 */
};
typedef struct os_serial_device os_serial_t;os_serial_device 继承自 os_device_t,通过 parent 成员接入通用设备框架。config 保存了串口的全部配置参数,rx_fifo 和 tx_fifo 分别管理接收和发送缓冲区。
2.2 串口配置结构体 serial_configure
struct serial_configure
{
uint32_t baud_rate; /* 波特率 */
uint32_t data_bits : 4; /* 数据位 (5/6/7/8/9) */
uint32_t stop_bits : 2; /* 停止位 (1/2/3/4) */
uint32_t parity : 2; /* 校验位 (NONE/ODD/EVEN) */
uint32_t bit_order : 1; /* 位顺序 (LSB/MSB) */
uint32_t invert : 1; /* 编码模式 (NORMAL/INVERTED) */
uint32_t rx_bufsz : 16; /* 接收缓冲区大小 */
uint32_t tx_bufsz : 16; /* 发送缓冲区大小 */
uint32_t reserved : 6; /* 保留位 */
};该结构体使用 C 语言位域(bit-field)来紧凑存储配置信息,总大小为 96 位(12 字节)。位域顺序和宽度由源码精确定义。
2.3 接收 FIFO 结构体 os_serial_rx_fifo
struct os_serial_rx_fifo
{
dma_ring_t ring; /* DMA 环形缓冲区 */
};接收 FIFO 使用 DMA 环形缓冲区(dma_ring_t),支持 DMA 硬件直接填充数据。
2.4 发送 FIFO 结构体 os_serial_tx_fifo
struct os_serial_tx_fifo
{
rb_ring_buff_t rbuff; /* 环形缓冲区 */
uint8_t *line_buff; /* 发送行缓冲区 */
uint32_t line_buff_count; /* 行缓冲区数据计数 */
};发送 FIFO 采用两层缓冲设计:rbuff 是环形缓冲区用于缓存待发送数据,line_buff 是行缓冲区用于当前正在发送的数据块。
2.5 UART 操作函数表 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 | 轮询模式发送(阻塞发送),返回实际发送的字节数 |
三、串口配置参数详解
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 的常用波特率,覆盖了低速到高速串口通信的全部需求。
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 /* 偶校验 */3.5 位顺序与编码
#define BIT_ORDER_LSB 0 /* 低位先传 */
#define BIT_ORDER_MSB 1 /* 高位先传 */
#define NRZ_NORMAL 0 /* NRZ 标准编码 */
#define NRZ_INVERTED 1 /* NRZ 反向编码 */3.6 默认配置
#define OS_SERIAL_CONFIG_DEFAULT \
{ \
BAUD_RATE_115200, \
DATA_BITS_8, \
STOP_BITS_1, \
PARITY_NONE, \
BIT_ORDER_LSB, \
NRZ_NORMAL, \
OS_SERIAL_RX_BUFSZ, \
OS_SERIAL_TX_BUFSZ, \
0 \
}默认配置为:115200-8-N-1(最常用的串口配置),收发缓冲区大小默认为 64 字节(可通过 OS_SERIAL_RX_BUFSZ 和 OS_SERIAL_TX_BUFSZ 宏自定义)。
3.7 缓冲区大小
#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 字节,可在 oneos_config.h 中覆盖定义。
四、串口事件与 DMA
4.1 事件类型
#define OS_SERIAL_EVENT_RX_IND 0x01 /* 接收数据到达指示 */
#define OS_SERIAL_EVENT_TX_DONE 0x02 /* 发送完成 */
#define OS_SERIAL_EVENT_RX_DMADONE 0x03 /* RX DMA 传输完成 */
#define OS_SERIAL_EVENT_TX_DMADONE 0x04 /* TX DMA 传输完成 */
#define OS_SERIAL_EVENT_RX_TIMEOUT 0x05 /* 接收超时 */| 事件 | 说明 |
|---|---|
OS_SERIAL_EVENT_RX_IND | 接收 FIFO 中有新数据到达,应用层可读取 |
OS_SERIAL_EVENT_TX_DONE | 发送缓冲区数据全部发送完毕 |
OS_SERIAL_EVENT_RX_DMADONE | 一次 DMA 接收传输完成 |
OS_SERIAL_EVENT_TX_DMADONE | 一次 DMA 发送传输完成 |
OS_SERIAL_EVENT_RX_TIMEOUT | 接收超时,在指定时间内未收到数据 |
4.2 DMA 模式
#define OS_SERIAL_DMA_RX 0x01 /* 使能 DMA 接收 */
#define OS_SERIAL_DMA_TX 0x02 /* 使能 DMA 发送 */4.3 中断模式
#define OS_SERIAL_RX_INT 0x01 /* 使能接收中断 */
#define 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 /* 校验错误 */4.5 发送数据队列
#define OS_SERIAL_TX_DATAQUEUE_SIZE 2048 /* 发送数据队列大小 */
#define OS_SERIAL_TX_DATAQUEUE_LWM 30 /* 发送数据队列低水位 */五、核心 API 详解
5.1 注册串口设备
os_err_t os_hw_serial_register(struct os_serial_device *serial, const char *name, void *data);- 注册串口设备到设备框架,通常由 BSP 层在初始化时调用
serial:串口设备结构体指针,必须已填充ops和config字段name:设备名称,如"uart0"、"uart1"data:用户自定义数据指针,存储到device->user_data- 返回值:
OS_SUCCESS成功,其他值失败 - 内部操作:设置设备类型为
OS_DEVICE_TYPE_CHAR,绑定serial_ops操作函数表,初始化自旋锁,创建信号量
5.2 ISR 接收完成通知
void os_hw_serial_isr_rxdone(struct os_serial_device *serial);- 底层驱动在接收中断中调用,通知上层有数据到达
- 更新
dev->rx_count为当前 FIFO 中的数据量 - 调用
os_device_recv_notify()唤醒等待读取的任务
5.3 ISR 发送完成通知
void os_hw_serial_isr_txdone(struct os_serial_device *serial);- 底层驱动在发送完成中断中调用
- 从发送环形缓冲区取出下一块数据继续发送
- 若缓冲区为空,调用
os_device_send_notify()唤醒等待写入的任务
5.4 通过设备框架操作串口
Serial 设备注册为字符设备,可使用标准设备框架 API 操作:
/* 打开设备 */
os_device_t *dev = os_device_open_s("uart0");
/* 阻塞读取 */
char buf[64];
int len = os_device_read_block(dev, 0, buf, sizeof(buf));
/* 非阻塞读取 */
len = os_device_read_nonblock(dev, 0, buf, sizeof(buf));
/* 阻塞写入 */
const char *msg = "Hello OneOS\r\n";
os_device_write_block(dev, 0, msg, strlen(msg));
/* 非阻塞写入 */
os_device_write_nonblock(dev, 0, msg, strlen(msg));
/* 动态修改配置 */
struct serial_configure cfg = OS_SERIAL_CONFIG_DEFAULT;
cfg.baud_rate = BAUD_RATE_921600;
os_device_control(dev, OS_DEVICE_CTRL_CONFIG, &cfg);
/* 关闭设备 */
os_device_close(dev);5.5 设备控制命令
| 命令 | 说明 |
|---|---|
OS_DEVICE_CTRL_CONFIG | 动态修改串口配置,传入 serial_configure 结构体指针 |
OS_DEVICE_CTRL_SET_RX_TIMEOUT | 设置接收超时时间 |
OS_DEVICE_CTRL_SET_TX_TIMEOUT | 设置发送超时时间 |
OS_DEVICE_CTRL_FLUSH | 刷新缓冲区 |
OS_DEVICE_CTRL_RESUME | 恢复设备 |
OS_DEVICE_CTRL_SUSPEND | 挂起设备 |
六、使用示例
示例 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;
/* 打开串口设备 */
dev = os_device_open_s(UART_NAME);
if (dev == OS_NULL)
{
os_kprintf("Failed to open %s\r\n", UART_NAME);
return;
}
/* 发送欢迎信息 */
os_device_write_block(dev, 0, "Serial Demo Started!\r\n", 22);
while (1)
{
/* 阻塞读取串口数据 */
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);
/* 回显数据 */
os_device_write_block(dev, 0, "Echo: ", 6);
os_device_write_block(dev, 0, rx_buf, len);
}
}
}示例 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;
/* 读取当前配置 */
cfg = OS_SERIAL_CONFIG_DEFAULT;
/* 修改为 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;
/* 应用新配置 */
os_device_control(dev, OS_DEVICE_CTRL_CONFIG, &cfg);
os_kprintf("Baudrate changed to 921600\r\n");
os_device_close(dev);
}示例 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)
{
/* 非阻塞读取,立即返回 */
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);
}
}
}示例 4:注册自定义串口设备
#include <serial.h>
#include <device.h>
/* 底层 UART 操作实现 */
static int my_uart_init(struct os_serial_device *serial, struct serial_configure *cfg)
{
/* 初始化硬件 UART */
return 0;
}
static int my_uart_deinit(struct os_serial_device *serial)
{
/* 反初始化硬件 UART */
return 0;
}
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)
{
/* 轮询发送(阻塞) */
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)
{
/* 配置串口参数 */
my_serial.config = (struct serial_configure)OS_SERIAL_CONFIG_DEFAULT;
my_serial.ops = &my_uart_ops;
/* 注册到设备框架 */
os_hw_serial_register(&my_serial, "uart0", OS_NULL);
}示例 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);
}
}
}
/* 串口 DMA 初始化 */
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;
}
/* 配置 DMA 接收 */
os_device_control(dev, OS_DEVICE_CTRL_SET_RX_TIMEOUT,
(void *)(os_uint32_t)100); /* 100ms 超时 */
g_serial_dev = dev;
os_kprintf("Serial DMA mode initialized\r\n");
}
/* BSP 层中断处理示例(HAL 层实现) */
void USART1_IRQHandler(void)
{
os_interrupt_enter();
/* 检查接收中断 */
if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
/* 通知上层有数据到达 */
os_hw_serial_isr_rxdone(&uart1_device);
}
/* 检查 DMA 传输完成中断 */
if (DMA_GetITStatus(DMA1_Stream5, DMA_IT_TCIF5) != RESET)
{
DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_TCIF5);
/* DMA 传输完成,通知上层 */
os_hw_serial_isr_rxdone(&uart1_device);
}
os_interrupt_leave();
}📝 本节小结
本节介绍了 OneOS 中 Serial 设备的使用方法,包括:
- 设备结构:
os_serial_device继承自os_device_t,通过os_uart_ops操作函数表实现底层硬件抽象 - 配置参数:
serial_configure使用位域紧凑存储波特率、数据位、停止位、校验位、位顺序、缓冲区大小等参数 - FIFO 缓冲:收发各配备独立的环形缓冲区,接收使用 DMA 环形缓冲区,发送采用两层缓冲设计
- 事件机制:支持 RX 到达、TX 完成、DMA 完成、RX 超时等五种事件类型
- DMA 支持:支持 DMA 收发模式,适合大数据量传输场景
- 标准接口:通过设备框架的
os_device_read_block、os_device_write_block等 API 进行读写操作
Serial 设备是嵌入式系统中最常用的通信接口,掌握其配置与使用方法是进行嵌入式开发的基础。