5.19 Console 设备
5.19 Console 设备
📚 本节导读
学习时长: 约 20 分钟
难度级别: ⭐⭐☆☆☆
前置知识: 5.1 设备驱动框架、5.4 Serial 设备
🎯 学习目标
- 理解 Console 控制台设备在 OneOS 中的角色与架构
- 掌握控制台设备的注册与切换方法
- 理解
os_kprintf输出的底层重定向机制 - 能够将日志输出重定向到不同的物理设备(串口、USB、网络等)
一、概述
Console(控制台)设备是 OneOS 中用于系统日志输出的抽象设备。它不直接对应某个硬件外设,而是作为 os_kprintf 等内核日志函数的输出目标,通过一个 "Console 设备指针" 将所有日志输出重定向到实际的物理设备(如串口、USB CDC 等)。
1.1 Console 在输出体系中的位置
┌─────────────────────────────────┐
│ os_kprintf() / KLOG_xxx() │ ← 内核/应用日志 API
├─────────────────────────────────┤
│ os_hw_console_output() │ ← Console 输出接口
├─────────────────────────────────┤
│ gs_console_device (设备指针) │ ← 当前控制台设备
├──────────┬──────────┬───────────┤
│ Serial │ USB CDC │ ... │ ← 实际物理设备
└──────────┴──────────┴───────────┘1.2 核心特点
- 设备无关:Console 不绑定具体硬件,通过设备指针实现输出重定向
- 热切换:运行时可通过
os_console_set_device()切换输出设备 - 弱符号回退:当没有注册 Console 设备时,通过
__os_hw_console_output弱符号提供默认输出 - 自动初始化:通过
OS_INIT_CALL机制在设备初始化阶段自动注册
1.3 使用场景
一句话核心:Console 是"日志输出总开关"——它不直接对应硬件,而是通过一个设备指针把 os_kprintf 的输出去向统一管起来,让日志可以自由重定向到任意能输出的设备(串口/USB/网络)。
与"直接调某个串口输出"对比:
| 维度 | 用 Console | 应用直接调串口 |
|---|---|---|
| 输出目标切换 | 改一个设备名即可热切换 | 每处代码都要改 |
| 与日志系统解耦 | 内核/应用日志统一走 os_kprintf | 需自己封装 |
| 设备未就绪 | 弱符号兜底,早期日志可见 | 早期无输出 |
| 多设备 | 可切到 USB/网络等 | 绑定单一串口 |
结论:日志/调试信息要统一管理、且可能要切换输出设备时用 Console;极简单、只用一个固定串口的场景可省去。
典型应用场景:
| 场景 | 谁在用 | 实际工作情况 |
|---|---|---|
| 调试串口输出 | os_kprintf/KLOG | 启动时 os_console_init 把 uart0 设为 Console,所有日志打到调试串口 |
| USB 虚拟串口 | 日志 + 交互 | 连接 USB 后 os_console_set_device("vcom") 把日志改到 USB CDC |
| 网络日志 | 远程调试 | 把 Console 切到网口对应的设备,远程看日志 |
| 早期启动日志 | BSP 弱符号 | __os_hw_console_output 直接操作寄存器,保证从第一条日志就可见 |
选型边界表:
| 场景 | 该用什么 | 为什么 |
|---|---|---|
| 设备不支持 write | 不用它做 Console | Console 输出靠 os_device_write_*,无 write 则输出无效 |
| 极简单串口调试 | 直接调串口 | 引入 Console 设备指针的意义不大 |
| 需要复杂日志分级 | KLOG/DLOG 后端 | Console 只是输出目标,分级过滤在 DLOG 层 |
结合源码的使用注意点:
- 切换设备旧设备会被 close:
os_console_set_device会关闭旧设备,返回的old指针仅供查看,不要再读写; - 设备须先注册:
os_console_set_device("xxx")内部os_device_open_s,设备未注册则切换不生效; - 中断中输出走非阻塞:
DLOG_USING_ASYNC_OUTPUT开启时自动按中断状态选阻塞/非阻塞写,避免中断阻塞。
二、核心数据结构
2.1 全局控制台设备指针
static os_device_t *gs_console_device = OS_NULL;整个系统只有一个 Console 设备指针,指向当前用于输出的设备。
2.2 源文件位置
| 文件 | 路径 | 说明 |
|---|---|---|
console.h | drivers/console/console.h | Console 接口头文件 |
console.c | drivers/console/console.c | Console 实现源码 |
三、API 参考
3.1 设置 Console 设备
os_device_t *os_console_set_device(const char *name);参数:
name:要设置为 Console 设备的设备名称(如"uart0")
返回值:旧的 Console 设备指针(OS_NULL 表示之前没有设置)
功能:通过设备名称查找并打开设备,将其设置为新的 Console 输出目标。如果之前已有 Console 设备,会自动关闭旧设备。
完整源码(drivers/console/console.c,与项目逐字一致):
os_device_t *os_console_set_device(const char *name)
{
os_device_t *new;
os_device_t *old;
old = gs_console_device;
new = os_device_open_s(name);
if (OS_NULL != new)
{
gs_console_device = new;
if (OS_NULL != old)
{
os_device_close(old);
}
}
return old;
}分步详解:
第 1 步:记录旧设备(无条件执行)
old = gs_console_device;详细解释:先保存当前 Console 设备指针 old,用于最后返回。
第 2 步:打开新设备(无条件执行)
new = os_device_open_s(name);详细解释:按名字 os_device_open_s 查找并打开设备(阻塞打开)。若设备不存在或打开失败,new == OS_NULL,此时 old 未变,gs_console_device 仍是旧设备,切换不生效。
第 3 步:切换并关闭旧设备(条件 new 非空)
if (OS_NULL != new)
{
gs_console_device = new;
if (OS_NULL != old)
{
os_device_close(old);
}
}详细解释:只有新设备打开成功才更新 gs_console_device = new,并把旧的(若存在)os_device_close(old)。注意:旧设备被关闭,之后不能再直接读写,所以返回值 old 只是给你看"之前是哪个设备",不要再去读写/关闭它。
os_console_set_device() 条件走向总结:
os_console_set_device(name) 条件走向总结:
├── old = gs_console_device(记录旧设备)
├── new = os_device_open_s(name) 打开新设备
│ ├── new == OS_NULL(设备不存在/打开失败)
│ │ └── 返回 old(切换不生效,仍用旧设备)
│ └── new != OS_NULL(打开成功)
│ ├── gs_console_device = new(更新为新设备)
│ ├── old != OS_NULL(之前有旧设备)
│ │ └── os_device_close(old)(关闭旧设备)
│ └── 返回 old(旧设备指针)3.2 获取 Console 设备
os_device_t *os_console_get_device(void);返回值:当前 Console 设备指针(OS_NULL 表示未设置)
os_device_t *console = os_console_get_device();
if (console != OS_NULL)
{
os_kprintf("Console device: %s\n", console->name);
}3.3 控制台硬件输出
void os_hw_console_output(char *log_buff);功能:将日志缓冲区内容输出到当前 Console 设备。如果 Console 设备未设置,则调用弱符号 __os_hw_console_output(BSP 层可覆盖)。
四、实现原理
4.1 初始化流程
Console 设备通过 OS_INIT_CALL 机制在设备初始化阶段自动注册:
完整源码(drivers/console/console.c,与项目逐字一致):
os_err_t os_console_init(void)
{
os_console_set_device(OS_CONSOLE_DEVICE_NAME);
return OS_SUCCESS;
}
OS_INIT_CALL(os_console_init, OS_INIT_LEVEL_PRE_DEVICE, OS_INIT_SUBLEVEL_MIDDLE);分步详解:
第 1 步:设置默认 Console 设备(无条件执行)
os_console_set_device(OS_CONSOLE_DEVICE_NAME);详细解释:调用 os_console_set_device(见 3.1)把 OS_CONSOLE_DEVICE_NAME 指定的设备(如 "uart0")设为 Console。注意:os_console_set_device 内部 os_device_open_s,若该设备此刻还没注册会打开失败(new == OS_NULL),Console 暂不生效,os_kprintf 走弱符号回退。
第 2 步:返回成功(无条件执行)
return OS_SUCCESS;
}详细解释:始终返回 OS_SUCCESS(即使设备没打开成功也返回成功,因为弱符号能兜底,不阻塞启动流程)。
第 3 步:注册初始化钩子(无条件执行)
OS_INIT_CALL(os_console_init, OS_INIT_LEVEL_PRE_DEVICE, OS_INIT_SUBLEVEL_MIDDLE);详细解释:用 OS_INIT_CALL 在 OS_INIT_LEVEL_PRE_DEVICE(设备初始化之前)级别注册 os_console_init。为什么要在设备前:这样后续各设备驱动的初始化日志(os_kprintf)能立即有 Console 输出目标。但:此时 OS_CONSOLE_DEVICE_NAME 对应的串口设备往往还没注册(它在 OS_INIT_LEVEL_DEVICE 级别才注册),所以早期靠弱符号 __os_hw_console_output 打印。
OS_CONSOLE_DEVICE_NAME由oneos_config.h或 BSP 的board.h定义- 初始化级别为
OS_INIT_LEVEL_PRE_DEVICE,在设备初始化之前执行,确保 Console 就绪后其他设备初始化日志可以正常输出
os_console_init() 条件走向总结:
os_console_init() 条件走向总结:
├── os_console_set_device(OS_CONSOLE_DEVICE_NAME)
│ ├── 设备已注册 → 打开成功,gs_console_device 指向该设备
│ └── 设备未注册 → 打开失败(new==NULL),Console 暂空(走弱符号)
├── return OS_SUCCESS
└── OS_INIT_CALL 在 PRE_DEVICE 级别注册(启动时自动调用)4.2 输出流程
os_hw_console_output 函数的输出流程:
void os_hw_console_output(char *log_buff)
{
os_device_t *console = os_console_get_device();
if (console == OS_NULL)
{
__os_hw_console_output(log_buff);
}
else
{
int count = 0;
int send_index = 0;
int size = strlen(log_buff);
while (send_index < size)
{
#ifdef DLOG_USING_ASYNC_OUTPUT
if (os_is_irq_disabled())
count = os_device_write_nonblock(console, 0, log_buff + send_index, size - send_index);
else
count = os_device_write_block(console, 0, log_buff + send_index, size - send_index);
#else
count = os_device_write_nonblock(console, 0, log_buff + send_index, size - send_index);
#endif
if (count <= 0)
{
break;
}
send_index += count;
}
}
}分步详解:
第 1 步:取当前 Console 设备并判断(条件 console == OS_NULL)
os_device_t *console = os_console_get_device();
if (console == OS_NULL)
{
__os_hw_console_output(log_buff);
}详细解释:取 gs_console_device。若未设置 Console 设备(返回 OS_NULL),调用弱符号 __os_hw_console_output(log_buff) 兜底输出(BSP 覆盖版本直接操作寄存器,保证早期日志可见)。若已设置则进入 else 用设备框架输出。
第 2 步:初始化变量(无条件执行)
int count = 0;
int send_index = 0;
int size = strlen(log_buff);详细解释:count 记录单次实际写入字节数;send_index 是已发送偏移;size 是日志总长度(strlen,不含 \0)。注意 count 必须初始化,否则 #ifdef 不同分支下若 os_device_write_* 未赋值就有未初始化风险。
第 3 步:循环分块写入直到发完(条件 send_index < size)
while (send_index < size)
{
#ifdef DLOG_USING_ASYNC_OUTPUT
if (os_is_irq_disabled())
count = os_device_write_nonblock(console, 0, log_buff + send_index, size - send_index);
else
count = os_device_write_block(console, 0, log_buff + send_index, size - send_index);
#else
count = os_device_write_nonblock(console, 0, log_buff + send_index, size - send_index);
#endif
if (count <= 0)
{
break;
}
send_index += count;
}详细解释:关键设计 —— 异步输出模式:当 DLOG_USING_ASYNC_OUTPUT 开启时,判断当前是否关中断——若 os_is_irq_disabled()(在中断上下文或关中断区)用非阻塞写(os_device_write_nonblock,不等待),否则用阻塞写(os_device_write_block,保证发完),避免中断中阻塞。未开启该宏时始终用非阻塞写。关键设计 —— 分块写入:设备驱动可能只接收部分字节(count),用 send_index += count 推进偏移,循环直到发完;count <= 0(失败或暂不可写)则 break 停止,防止死循环。
关键设计点:
- 弱符号回退:
__os_hw_console_output是弱符号,BSP 可覆盖实现(如直接操作 UART 寄存器打印字符),在 Console 设备未就绪时也能输出 - 异步输出模式:
DLOG_USING_ASYNC_OUTPUT宏启用时,在关中断状态下使用非阻塞写入,开中断时使用阻塞写入,避免中断中阻塞 - 分块写入:支持设备驱动返回部分写入长度,通过循环确保全部数据输出
4.3 弱符号回退
来源:drivers/console/console.c,与项目逐字一致:
OS_WEAK void __os_hw_console_output(char *str)
{
}详细解释:这是框架提供的弱符号默认实现(空函数,什么都不做)。BSP 在别处定义同名函数即可覆盖它——链接器优先取强符号版本。为什么需要弱符号:Console 设备(gs_console_device)在系统启动早期可能还没被设置,此时 os_kprintf 若依赖设备框架会丢日志。BSP 覆盖 __os_hw_console_output 直接操作 UART 寄存器逐字符打印,保证从第一条启动日志开始就可见。
BSP 覆盖示例(在 board.c 中,示意用法,非框架源码):
void __os_hw_console_output(char *str)
{
/* 直接通过 UART 寄存器逐字符输出,不依赖设备框架 */
while (*str != '\0')
{
while (!(USART1->SR & USART_SR_TXE));
USART1->DR = *str++;
}
}五、使用示例
5.1 查看当前 Console 设备
os_device_t *console = os_console_get_device();
if (console)
{
os_kprintf("当前控制台设备: %s\n", console->name);
}
else
{
os_kprintf("未设置控制台设备\n");
}5.2 切换 Console 设备
/* 场景:系统启动时使用串口1,连接 USB 后切换到虚拟串口 */
void switch_to_usb_console(void)
{
os_device_t *old_dev;
os_kprintf("切换到 USB 虚拟串口...\n");
old_dev = os_console_set_device("vcom");
if (old_dev)
{
os_kprintf("已从 %s 切换到 vcom\n", old_dev->name);
}
}5.3 多设备日志输出
/* 同时向多个设备输出日志 */
void multi_output_log(const char *msg)
{
os_device_t *uart0 = os_device_open_s("uart0");
os_device_t *vcom = os_device_open_s("vcom");
os_size_t len;
if (uart0)
{
len = os_device_write_nonblock(uart0, 0, msg, strlen(msg));
if (len != strlen(msg))
{
os_kprintf("uart0 write %d/%d bytes\r\n", len, (int)strlen(msg));
}
os_device_close(uart0);
}
if (vcom)
{
len = os_device_write_nonblock(vcom, 0, msg, strlen(msg));
if (len != strlen(msg))
{
os_kprintf("vcom write %d/%d bytes\r\n", len, (int)strlen(msg));
}
os_device_close(vcom);
}
}六、Console 的适配(把日志输出到新设备)
6.1 先认识 Console 的"适配":根本不需要移植框架
Console 框架本身非常简单(console.c 只有一个全局指针 + 4 个 API),几乎没有硬件依赖,换任何平台都不需要改框架。所谓"适配"实际是让某个设备能作为 Console 输出,核心是弱符号 __os_hw_console_output 的覆盖(保证早期日志)和串口设备的注册(让 os_console_set_device 能打开它)。
适配全景图:
┌──────────────────────────────────────────────────────────┐
│ 应用/内核:os_kprintf() / KLOG_xxx() │
└──────────────────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ Console 框架层(drivers/console/console.c,通用不改) │
│ os_console_set_device / get_device / os_hw_console_output│
└──────────────────────────────┬───────────────────────────┘
│ ①弱符号 __os_hw_console_output
│ ②os_console_set_device("uart0")
▼
┌──────────────────────────────────────────────────────────┐
│ BSP 适配层(board.c / drv_console.c)★需实现★ │
│ ①覆盖 __os_hw_console_output(寄存器直打) │
│ ②串口设备驱动注册(可被 os_device_open_s 打开) │
└──────────────────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ 硬件(UART / USB CDC / 网口等) │
└──────────────────────────────────────────────────────────┘适配的关键洞察:做两件事就能让 Console 工作:① 覆盖弱符号 __os_hw_console_output(直接操作 UART 寄存器逐字符打印);② 确保目标串口设备已注册(os_console_set_device("uart0") 能打开它)。框架层不用改。
6.2 以 STM32 为例,看一整套适配
① 覆盖弱符号 —— 早期日志直打寄存器(drivers/hal/st/drivers/drv_console.c 或 board.c,示意):
#include <board.h>
#include <os_console.h>
void __os_hw_console_output(char *str)
{
extern UART_HandleTypeDef huart1;
/* 直接操作串口寄存器逐字符输出,不依赖设备框架 */
while (*str != '\0')
{
while (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TXE) == RESET);
huart1.Instance->DR = (uint8_t)(*str++);
}
}要点:只要定义同名
__os_hw_console_output强符号,就覆盖了框架的弱符号默认空实现。它不依赖设备框架,在os_console_init之前(OS_CONSOLE_DEVICE_NAME设备还没注册)也能打印,保证启动早期日志可见。
② 串口设备注册:drv_uart.c 用 OS_DRIVER_INFO/probe 注册串口设备(名字如 "uart0"),os_console_set_device("uart0") 才能 os_device_open_s 打开它。这一步与 5.4 Serial 设备适配相同。
6.3 接口契约:适配 Console 需要实现什么
| 层 | 接口 | 职责 | 对应 6.2 代码 |
|---|---|---|---|
| 弱符号 | __os_hw_console_output(char *str) | 早期日志直打寄存器 | BSP 同名强符号覆盖 |
| 设备 | 串口设备驱动 | 可被 os_device_open_s 打开、支持 write | drv_uart.c |
| 配置 | OS_CONSOLE_DEVICE_NAME | 指定默认 Console 设备名 | board.h 定义 |
6.4 适配新平台的标准流程
第 1 步:确认调试串口——选定一块 UART 作为控制台输出口,确认其寄存器访问方式。
第 2 步:覆盖弱符号——新建/修改 drv_console.c(或 board.c),定义 __os_hw_console_output 强符号,直接操作该 UART 寄存器逐字符输出。
第 3 步:注册串口设备——确保该 UART 有完整 Serial 设备驱动(OS_DRIVER_INFO 注册、实现 write),名字如 "uart0"。
第 4 步:配置默认设备名——在 board.h/oneos_config.h 定义 OS_CONSOLE_DEVICE_NAME "uart0"。
第 5 步:测试——启动后 os_kprintf("hello"),串口能看到输出;用 os_console_set_device("vcom") 验证热切换。
6.5 适配检查清单(快速自查)
| 检查项 | 说明 |
|---|---|
| 弱符号是否覆盖? | 未覆盖则早期日志(设备就绪前)无输出 |
| 目标串口是否注册且支持 write? | 未注册则 os_console_set_device 打开失败 |
OS_CONSOLE_DEVICE_NAME 是否配置? | 未定义则编译期报错 |
| 切换设备后是否误用旧指针? | os_console_set_device 返回的 old 已被 close,不可再读写 |
七、注意事项
- Console 设备必须支持写入操作:用作 Console 的设备必须实现
write方法(字符设备类型) - 初始化顺序:Console 初始化在
OS_INIT_LEVEL_PRE_DEVICE级别,早于设备驱动初始化,因此用作 Console 的设备需要尽早注册 - 中断安全:在中断上下文中调用
os_kprintf时,Console 输出使用非阻塞模式,避免中断阻塞 - 弱符号覆盖:BSP 必须覆盖
__os_hw_console_output弱符号,确保在 Console 设备未就绪时也能输出早期日志(如启动阶段的调试信息)
📝 本节小结
核心要点回顾
本节围绕 Console 控制台设备展开,核心要点如下:
- 抽象角色:Console 不直接对应硬件,而是通过
gs_console_device设备指针把所有日志输出重定向到实际物理设备。 - 设置/获取:
os_console_set_device(name)打开设备设为 Console(成功才切换、失败保留旧设备)并关闭旧设备;os_console_get_device()取当前设备。 - 输出机制:
os_hw_console_output优先用 Console 设备分块非阻塞写入;未设置时走弱符号__os_hw_console_output回退。 - 自动初始化:
os_console_init在OS_INIT_LEVEL_PRE_DEVICE级别用OS_CONSOLE_DEVICE_NAME提前配置 Console。 - 异步输出:
DLOG_USING_ASYNC_OUTPUT开启时,关中断用非阻塞写、开中断用阻塞写,避免中断阻塞。 - 弱符号回退:
__os_hw_console_output是弱符号空实现,BSP 覆盖它直打寄存器,保证早期启动日志可见。 - 硬件适配:适配 Console ≈ 覆盖弱符号 + 注册串口设备 + 配
OS_CONSOLE_DEVICE_NAME,框架层不用改。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
| Console 设备不支持 write | 用作 Console 的设备必须实现字符设备的 write 方法,否则输出无效 |
| 弱符号未覆盖 | BSP 需覆盖 __os_hw_console_output,否则 Console 设备未就绪时早期日志无输出 |
| 切换设备后旧设备引用 | os_console_set_device 返回旧设备指针(已自动 close),不要再重复关闭/读写 |
| 在中断里阻塞输出 | 中断上下文应使用非阻塞写(DLOG_USING_ASYNC_OUTPUT 已处理),避免阻塞中断 |
| 初始化顺序错误 | Console 在 PRE_DEVICE 级别初始化,用作 Console 的设备需更早注册才可见 |
| 设备未注册就切换 | os_console_set_device("xxx") 内部 os_device_open_s,设备未注册则切换不生效 |
下一步
掌握了 Console 控制台设备后,接下来学习 Net 网络设备:
- 5.20 Net设备:了解 OneOS 的网络设备驱动、网卡注册与数据收发。