LOADING
3644 字
18 分钟
ChibiOS/RT 调试与数据结构

ChibiOS/RT 调试与数据结构#

一、调试支持概述#

ChibiOS/RT提供了丰富的调试支持机制,帮助开发者在开发阶段发现和定位问题。这些调试功能通过 chdebug.h 头文件中的宏定义进行控制,大部分功能在发布版本中可以完全禁用,零开销运行。

调试功能的启用通过 chconf.h 配置文件中的宏定义进行控制:

/* chconf.h 调试配置示例 */
/* 系统状态检查 - 验证API调用时的系统状态 */
#define CH_DBG_SYSTEM_STATE_CHECK TRUE
/* API参数检查 - 验证API参数的有效性 */
#define CH_DBG_ENABLE_CHECKS TRUE
/* 断言检查 - 启用chDbgAssert断言 */
#define CH_DBG_ENABLE_ASSERTS TRUE
/* 调试追踪掩码 - 控制追踪输出的类别 */
#define CH_DBG_TRACE_MASK CH_DBG_TRACE_MASK_ALL
/* 栈溢出检查 - 检测线程栈溢出 */
#define CH_DBG_ENABLE_STACK_CHECK TRUE
/* 线程栈填充 - 用魔术值填充栈空间便于检测 */
#define CH_DBG_FILL_THREADS TRUE
/* 性能测量钩子 - 启用性能测量回调 */
#define CH_DBG_ENABLE_HOOKS TRUE

二、调试宏详解#

2.1 系统状态检查(CH_DBG_SYSTEM_STATE_CHECK)#

此选项启用对系统状态的检查,确保API调用在正确的上下文中进行。例如,在中断处理器中调用某些不允许的API时会触发断言。

/* 启用后,以下代码会触发断言失败 */
void ISR_handler(void) {
/* 在中断中调用chThdSleep会触发断言 */
chThdSleep(MS2ST(100)); /* 错误!中断中不能睡眠 */
}

2.2 API参数检查(CH_DBG_ENABLE_CHECKS)#

此选项启用对API参数有效性的检查。如果传递了无效参数(如NULL指针、超出范围的值),会触发断言失败。

/* 启用后,以下代码会触发断言失败 */
void bad_example(void) {
/* 传递NULL指针 */
chSemWait(NULL); /* 错误!NULL指针 */
/* 传递超出范围的优先级 */
chThdCreateStatic(wa_thread, sizeof(wa_thread),
256, /* 错误!最大优先级为255 */
thread_func, NULL);
}

2.3 断言检查(CH_DBG_ENABLE_ASSERTS)#

断言是最常用的调试工具,用于验证程序逻辑的正确性。ChibiOS/RT提供了 chDbgAssert 宏用于自定义断言。

/* 自定义断言示例 */
void process_data(uint8_t *data, size_t len) {
/* 断言数据指针不为空 */
chDbgAssert(data != NULL, "data pointer is NULL");
/* 断言数据长度有效 */
chDbgAssert(len > 0 && len <= MAX_DATA_SIZE, "invalid data length");
/* 断言数据内容有效 */
chDbgAssert(validate_checksum(data, len), "data checksum mismatch");
/* 正常处理 */
do_processing(data, len);
}
/* 条件断言 */
void process_sensor(sensor_t *sensor) {
chDbgCheck(sensor != NULL);
/* 检查传感器状态 */
if (sensor->calibrated) {
chDbgAssert(sensor->offset != 0, "sensor not properly calibrated");
}
}

三、栈溢出检查#

栈溢出是嵌入式系统中最常见也最难调试的问题之一。ChibiOS/RT提供了多种机制来检测和预防栈溢出。

3.1 栈水印检查(CH_DBG_ENABLE_STACK_CHECK)#

栈水印(Stack Watermark)是一种高效的栈使用检测方法。它在线程栈的底部填充特定的魔术值,运行时检查这些值是否被破坏。

/* chconf.h 配置 */
#define CH_DBG_ENABLE_STACK_CHECK TRUE
#define CH_DBG_STACK_LIMIT 64 /* 栈溢出警告阈值(字节) */
/* 检查线程栈使用情况 */
void check_stack_usage(void) {
thread_t *tp = chThdGetSelfX();
/* 获取栈使用量 */
size_t used = chThdGetUsedStack(tp);
size_t limit = chThdGetLimitStack(tp);
chprintf((BaseSequentialStream *)&SD1,
"Thread: %s, Stack used: %d/%d bytes\r\n",
tp->name, (int)used, (int)limit);
/* 检查是否接近栈溢出 */
if (used > limit - CH_DBG_STACK_LIMIT) {
chprintf((BaseSequentialStream *)&SD1,
"WARNING: Stack almost full!\r\n");
}
}

3.2 栈填充(CH_DBG_FILL_THREADS)#

启用此选项后,ChibiOS/RT会在创建线程时用魔术值(0x55)填充整个栈空间。这使得栈使用情况更加直观,也便于检测栈溢出。

/* 栈填充后的内存布局示意 */
/*
+------------------+ 栈顶
| 线程栈帧 |
| 局部变量 |
| 函数调用 |
+------------------+ <-- SP(栈指针)
| 未使用区域 | <-- 填充了 0x55
| 0x55 0x55 ... |
+------------------+ 栈底
*/
/* 检查栈填充是否被破坏 */
void check_stack_filling(thread_t *tp) {
uint8_t *stack_base = (uint8_t *)tp->wabase;
uint8_t *stack_limit = stack_base + chThdGetLimitStack(tp);
/* 检查栈底区域的填充值 */
for (uint8_t *p = stack_base; p < stack_limit; p++) {
if (*p != 0x55) {
chprintf((BaseSequentialStream *)&SD1,
"Stack corruption detected at %p\r\n", p);
break;
}
}
}

3.3 栈溢出检测实践#

/* 完整的栈溢出检测方案 */
#define STACK_SIZE 512
static THD_WORKING_AREA(wa_worker, STACK_SIZE);
static THD_FUNCTION(worker_thread, arg) {
(void)arg;
/* 获取当前线程信息 */
thread_t *tp = chThdGetSelfX();
size_t initial_sp = (size_t)tp->sp;
chprintf((BaseSequentialStream *)&SD1,
"Worker thread started, SP: 0x%08X\r\n", initial_sp);
/* 模拟可能导致栈溢出的操作 */
while (true) {
/* 定期检查栈使用情况 */
size_t current_sp;
__asm volatile("mov %0, sp" : "=r"(current_sp));
size_t stack_used = initial_sp - current_sp;
if (stack_used > STACK_SIZE - 64) {
chprintf((BaseSequentialStream *)&SD1,
"Stack overflow imminent! Used: %d bytes\r\n",
(int)stack_used);
}
chThdSleepMilliseconds(100);
}
}

四、性能测量#

ChibiOS/RT提供了强大的性能测量工具,帮助开发者分析系统性能瓶颈。

4.1 时间测量(chTM)#

时间测量宏用于精确测量代码段的执行时间:

#include "ch.h"
void performance_measurement_example(void) {
time_measurement_t tm;
/* 初始化时间测量结构 */
chTMObjectInit(&tm);
/* 开始测量 */
chTMStartMeasurementX(&tm);
/* 要测量的代码 */
for (int i = 0; i < 1000; i++) {
process_data(i);
}
/* 停止测量 */
chTMStopMeasurementX(&tm);
/* 输出测量结果 */
chprintf((BaseSequentialStream *)&SD1,
"Elapsed: %d cycles, %d ticks\r\n",
(int)tm.n, (int)tm.best);
}

4.2 性能钩子#

通过启用性能钩子,可以在每次上下文切换、空闲循环等事件时执行自定义代码:

/* chconf.h 配置 */
#define CH_DBG_ENABLE_HOOKS TRUE
/* 上下文切换钩子 */
void chDbgContextSwitchHook(thread_t *ntp, thread_t *otp) {
/* 记录上下文切换 */
trace_context_switch(otp->name, ntp->name);
}
/* 空闲循环钩子 */
void chDbgIdleHook(void) {
/* 更新空闲计数器 */
idle_counter++;
/* 测量空闲时间 */
idle_start = chVTGetSystemTimeX();
}
/* 锁入口钩子 */
void chDbgLockEnterHook(void) {
lock_entry_time = chVTGetSystemTimeX();
}
/* 锁退出钩子 */
void chDbgLockExitHook(void) {
lock_exit_time = chVTGetSystemTimeX();
lock_duration += lock_exit_time - lock_entry_time;
}

4.3 CPU使用率测量#

/* 基于空闲钩子的CPU使用率测量 */
static volatile uint32_t idle_counter = 0;
static volatile uint32_t total_counter = 0;
/* 定期采样的定时器回调 */
static void cpu_usage_timer_cb(virtual_timer_t *vtp, void *param) {
(void)vtp;
(void)param;
/* 计算CPU使用率 */
uint32_t idle = idle_counter;
uint32_t total = total_counter;
uint32_t used = total - idle;
uint32_t usage = (used * 100) / total;
chprintf((BaseSequentialStream *)&SD1,
"CPU Usage: %d%% (idle: %d, total: %d)\r\n",
(int)usage, (int)idle, (int)total);
/* 重置计数器 */
idle_counter = 0;
total_counter = 0;
}
void cpu_usage_init(void) {
static virtual_timer_t vt;
/* 启动定时器,每1秒采样一次 */
chVTSetContinuous(&vt, S2ST(1), cpu_usage_timer_cb, NULL);
}
/* 空闲钩子中递增计数器 */
void chDbgIdleHook(void) {
idle_counter++;
}

五、系统检查#

ChibiOS/RT提供了系统级的检查函数,用于验证内核对象的状态和完整性。

5.1 对象验证(chDbgCheck)#

chDbgCheck 用于验证对象是否有效,检查对象的魔数(magic number)是否正确:

/* 验证信号量对象 */
void validate_semaphore(semaphore_t *sp) {
chDbgCheck(sp != NULL);
chDbgAssert(chSemGetStateI(sp) >= 0, "invalid semaphore state");
}
/* 验证互斥锁对象 */
void validate_mutex(mutex_t *mp) {
chDbgCheck(mp != NULL);
chDbgAssert(chMtxGetStateX(mp) >= 0, "invalid mutex state");
}
/* 验证线程对象 */
void validate_thread(thread_t *tp) {
chDbgCheck(tp != NULL);
chDbgAssert(tp->state >= THD_STATE_READY &&
tp->state <= THD_STATE_WTONE,
"invalid thread state");
}

5.2 系统完整性检查#

/* 完整的系统健康检查 */
typedef struct {
bool_t threads_valid;
bool_t timers_valid;
bool_t semaphores_valid;
bool_t memory_valid;
uint32_t error_count;
} system_health_t;
system_health_t check_system_health(void) {
system_health_t health = {0};
/* 检查所有线程 */
thread_t *tp;
chSysLock();
tp = ch.rlist.rlink_predecessor;
while (tp != (thread_t *)&ch.rlist) {
if (tp->state == THD_STATE_WTFINALIZE) {
health.threads_valid = FALSE;
health.error_count++;
}
tp = tp->rlist.rlink_predecessor;
}
chSysUnlock();
/* 检查内存池 */
memory_heap_t *heapp = NULL;
/* ... 遍历所有堆 ... */
/* 检查虚拟定时器 */
chSysLock();
virtual_timer_t *vtp = ch.vtlist.vt_next;
while (vtp != (virtual_timer_t *)&ch.vtlist) {
if (vtp->vt.func == NULL) {
health.timers_valid = FALSE;
health.error_count++;
}
vtp = vtp->vt_next;
}
chSysUnlock();
return health;
}

六、内核数据结构#

6.1 线程结构体(thread_t)#

thread_t 是ChibiOS/RT中最核心的数据结构,代表一个线程的完整上下文:

struct thread {
/* 内核链表 */
ch_list_t rlist; /* 就绪列表节点 */
/* 栈信息 */
void *wabase; /* 工作区基地址 */
void *sp; /* 栈指针 */
void *limit; /* 栈限制 */
/* 线程属性 */
tstate_t state; /* 线程状态 */
tprio_t prio; /* 优先级 */
bool umode; /* 用户模式标志 */
/* 等待信息 */
msg_t msg; /* 等待消息 */
sysinterval_t utime; /* 延迟时间 */
sysinterval_t vttim; /* 虚拟定时器 */
/* 扩展数据 */
void *user_data; /* 用户数据指针 */
const char *name; /* 线程名称 */
};

6.2 线程状态#

typedef enum {
THD_STATE_READY = 0, /* 就绪状态 */
THD_STATE_CURRENT, /* 当前运行状态 */
THD_STATE_WTSTART, /* 等待启动 */
THD_STATE_WTSEMAPHORE, /* 等待信号量 */
THD_STATE_WTCOND, /* 等待条件变量 */
THD_STATE_WTMTX, /* 等待互斥锁 */
THD_STATE_WTMSG, /* 等待消息 */
THD_STATE_WTQUEUE, /* 等待队列 */
THD_STATE_SNDNXT, /* 发送下一条消息 */
THD_STATE_WT Events, /* 等待事件 */
THD_STATE_WTOREVT, /* 等待或事件 */
THD_STATE_WTANDEVT, /* 等待与事件 */
THD_STATE_SLEEPEVT, /* 事件睡眠 */
THD_STATE_FINAL, /* 终止状态 */
THD_STATE_WTFINALIZE /* 等待终止 */
} tstate_t;

6.3 虚拟定时器(virtual_timer_t)#

虚拟定时器用于实现软件定时器,支持单次和周期性定时:

typedef struct virtual_timer {
virtual_timer_t *vt_next; /* 下一个定时器 */
virtual_timer_t *vt_prev; /* 上一个定时器 */
sysinterval_t vt_delta; /* 增量值 */
vtfunc_t vt_func; /* 回调函数 */
void *vt_par; /* 回调参数 */
} virtual_timer_t;
/* 定时器回调函数类型 */
typedef void (*vtfunc_t)(virtual_timer_t *vtp, void *param);
/* 使用示例 */
static void timer_callback(virtual_timer_t *vtp, void *param) {
(void)vtp;
int *counter = (int *)param;
(*counter)++;
chprintf((BaseSequentialStream *)&SD1, "Timer fired: %d\r\n", *counter);
}
void timer_example(void) {
static virtual_timer_t vt;
static int counter = 0;
/* 启动单次定时器,1秒后触发 */
chVTSet(&vt, S2ST(1), timer_callback, &counter);
/* 启动周期定时器,每500ms触发一次 */
chVTSetContinuous(&vt, MS2ST(500), timer_callback, &counter);
}

6.4 信号量(semaphore_t)#

信号量用于线程同步和资源计数:

typedef struct {
cnt_t cnt; /* 计数器 */
/* ... 等待队列 ... */
} semaphore_t;
/* 使用示例 */
static SEMAPHORE_DECL(sem, 0); /* 声明并初始化信号量 */
void producer(void) {
/* 产生资源 */
chSemSignal(&sem); /* 释放信号量 */
}
void consumer(void) {
chSemWait(&sem); /* 等待信号量 */
/* 使用资源 */
}

6.5 互斥锁(mutex_t)#

互斥锁用于保护共享资源,支持优先级继承协议:

typedef struct {
tcnt_t cnt; /* 锁计数 */
tprio_t owner_prio; /* 持有者原始优先级 */
tprio_t orgprio; /* 原始优先级 */
thread_t *owner; /* 持有者线程 */
/* ... 等待队列 ... */
} mutex_t;
/* 使用示例 */
static MUTEX_DECL(mtx);
void protected_function(void) {
chMtxLock(&mtx); /* 获取互斥锁 */
/* 临界区代码 */
chMtxUnlock(&mtx); /* 释放互斥锁 */
}

6.6 事件源(event_source_t)#

事件源用于异步事件通知:

typedef struct {
eventmask_t mask; /* 事件掩码 */
/* ... 等待线程列表 ... */
} event_source_t;
/* 使用示例 */
static event_source_t es_source;
void event_source_init(void) {
chEvtObjectInit(&es_source);
}
void event_handler(eventid_t id) {
switch (id) {
case 0:
chprintf((BaseSequentialStream *)&SD1, "Event 0\r\n");
break;
case 1:
chprintf((BaseSequentialStream *)&SD1, "Event 1\r\n");
break;
}
}
void event_thread(void) {
event_listener_t el;
/* 注册事件监听器 */
chEvtRegister(&es_source, &el, 0);
while (true) {
eventmask_t mask = chEvtWaitAny(ALL_EVENTS);
chEvtDispatch(event_handler, mask);
}
}

七、基础数据结构#

7.1 双向链表(ch_list_t)#

双向链表是ChibiOS/RT中最基础的数据结构,用于组织各种内核对象:

typedef struct ch_list {
struct ch_list *next; /* 下一个节点 */
struct ch_list *prev; /* 上一个节点 */
} ch_list_t;
/* 链表操作宏 */
#define CH_LIST_INIT(p) { (p)->next = (p); (p)->prev = (p); }
#define CH_LIST_IS_EMPTY(p) ((p)->next == (p))
#define CH_LIST_INSERT_AFTER(l, n) { \
(n)->next = (l)->next; \
(n)->prev = (l); \
(l)->next->prev = (n); \
(l)->next = (n); \
}
#define CH_LIST_INSERT_BEFORE(l, n) { \
(n)->prev = (l)->prev; \
(n)->next = (l); \
(l)->prev->next = (n); \
(l)->prev = (n); \
}
#define CH_LIST_REMOVE(n) { \
(n)->prev->next = (n)->next; \
(n)->next->prev = (n)->prev; \
}
/* 使用示例 */
typedef struct {
ch_list_t list_node; /* 链表节点必须是第一个成员 */
int data;
char name[32];
} my_item_t;
void list_example(void) {
ch_list_t list;
CH_LIST_INIT(&list);
my_item_t item1 = {.data = 1, .name = "item1"};
my_item_t item2 = {.data = 2, .name = "item2"};
/* 插入节点 */
CH_LIST_INSERT_BEFORE(&list, &item1.list_node);
CH_LIST_INSERT_BEFORE(&list, &item2.list_node);
/* 遍历链表 */
ch_list_t *node = list.next;
while (node != &list) {
my_item_t *item = (my_item_t *)node;
chprintf((BaseSequentialStream *)&SD1, "%s: %d\r\n",
item->name, item->data);
node = node->next;
}
}

7.2 队列(ch_queue_t)#

ChibiOS/RT内部使用的队列数据结构:

typedef struct {
ch_list_t *head; /* 队列头 */
ch_list_t *tail; /* 队列尾 */
cnt_t cnt; /* 元素数量 */
} ch_queue_t;
/* 队列操作 */
#define CH_QUEUE_INIT(q) { (q)->head = NULL; (q)->tail = NULL; (q)->cnt = 0; }
#define CH_QUEUE_IS_EMPTY(q) ((q)->head == NULL)
/* 入队操作 */
static inline void ch_queue_insert(ch_queue_t *qp, ch_list_t *item) {
item->next = NULL;
item->prev = qp->tail;
if (qp->tail != NULL) {
qp->tail->next = item;
} else {
qp->head = item;
}
qp->tail = item;
qp->cnt++;
}
/* 出队操作 */
static inline ch_list_t *ch_queue_remove_head(ch_queue_t *qp) {
ch_list_t *item = qp->head;
if (item != NULL) {
qp->head = item->next;
if (qp->head != NULL) {
qp->head->prev = NULL;
} else {
qp->tail = NULL;
}
qp->cnt--;
}
return item;
}

7.3 优先级队列(ch_priority_queue_t)#

优先级队列用于按优先级排序的线程调度:

typedef struct {
ch_list_t *head; /* 队列头 */
cnt_t cnt; /* 元素数量 */
} ch_priority_queue_t;
/* 优先级队列操作 */
#define CH_PRIORITY_QUEUE_INIT(q) { (q)->head = NULL; (q)->cnt = 0; }
/* 插入节点(按优先级排序) */
static inline void ch_priority_queue_insert(ch_priority_queue_t *pq,
ch_list_t *item,
tprio_t prio) {
(void)prio;
ch_list_t *node = pq->head;
ch_list_t *prev = NULL;
/* 找到插入位置 */
while (node != NULL) {
/* 这里简化了比较逻辑,实际实现更复杂 */
prev = node;
node = node->next;
}
/* 插入节点 */
if (prev != NULL) {
prev->next = item;
item->prev = prev;
} else {
pq->head = item;
item->prev = NULL;
}
item->next = node;
if (node != NULL) {
node->prev = item;
}
pq->cnt++;
}
/* 移除最高优先级节点 */
static inline ch_list_t *ch_priority_queue_remove_highest(ch_priority_queue_t *pq) {
ch_list_t *item = pq->head;
if (item != NULL) {
pq->head = item->next;
if (pq->head != NULL) {
pq->head->prev = NULL;
}
pq->cnt--;
}
return item;
}

7.4 增量链表(ch_delta_list_t)#

增量链表用于实现虚拟定时器的高效管理:

typedef struct {
ch_list_t *head; /* 链表头 */
cnt_t cnt; /* 节点数量 */
} ch_delta_list_t;
/* 增量链表操作 */
#define CH_DELTA_LIST_INIT(dl) { (dl)->head = NULL; (dl)->cnt = 0; }
/* 插入定时器节点(按增量值排序) */
static inline void ch_delta_list_insert(ch_delta_list_t *dl,
ch_list_t *item,
sysinterval_t delta) {
(void)delta;
ch_list_t *node = dl->head;
ch_list_t *prev = NULL;
/* 找到插入位置(按增量值排序) */
while (node != NULL) {
/* 实际实现中会检查增量值 */
prev = node;
node = node->next;
}
/* 插入节点 */
if (prev != NULL) {
prev->next = item;
item->prev = prev;
} else {
dl->head = item;
item->prev = NULL;
}
item->next = node;
if (node != NULL) {
node->prev = item;
}
dl->cnt++;
}
/* 移除头节点 */
static inline ch_list_t *ch_delta_list_remove_head(ch_delta_list_t *dl) {
ch_list_t *item = dl->head;
if (item != NULL) {
dl->head = item->next;
if (dl->head != NULL) {
dl->head->prev = NULL;
}
dl->cnt--;
}
return item;
}

八、类型别名#

ChibiOS/RT定义了一系列类型别名,提高代码的可读性和可移植性:

/* 基本类型 */
typedef int32_t msg_t; /* 消息类型 */
typedef int32_t cnt_t; /* 计数器类型 */
typedef uint32_t tprio_t; /* 线程优先级 */
typedef uint32_t sysinterval_t; /* 系统间隔时间 */
typedef systime_t sysinterval_t; /* 系统时间间隔 */
/* 线程状态 */
typedef uint8_t tstate_t; /* 线程状态 */
/* 事件相关 */
typedef uint32_t eventmask_t; /* 事件掩码 */
typedef uint32_t eventid_t; /* 事件ID */
/* 时间测量 */
typedef struct {
rtcnt_t best; /* 最佳时间 */
rtcnt_t worst; /* 最差时间 */
rtcnt_t cumulated; /* 累计时间 */
rtcnt_t last; /* 上次时间 */
cnt_t n; /* 测量次数 */
} time_measurement_t;
/* 内存池 */
typedef struct {
void *free; /* 空闲列表 */
/* ... 其他字段 ... */
} memory_pool_t;
/* 堆 */
typedef struct {
void *next; /* 下一个堆块 */
size_t size; /* 堆块大小 */
} memory_heap_t;

使用示例#

/* 类型安全的函数原型 */
static THD_FUNCTION(worker_thread, void *arg) {
(void)arg;
/* 使用正确的类型 */
msg_t result = MSG_OK;
cnt_t count = 0;
tprio_t my_prio = chThdGetPriorityX();
sysinterval_t timeout = TIME_MS2I(100);
/* 类型安全的API调用 */
result = chSemWaitTimeout(&sem, timeout);
if (result == MSG_OK) {
count++;
}
chprintf((BaseSequentialStream *)&SD1,
"Priority: %d, Count: %d\r\n",
(int)my_prio, (int)count);
}

九、总结#

ChibiOS/RT提供了完善的调试支持和高效的基础数据结构:

调试支持#

  • 系统状态检查:验证API调用上下文
  • 参数检查:验证API参数有效性
  • 断言:验证程序逻辑正确性
  • 栈溢出检查:检测和预防栈溢出
  • 性能测量:分析系统性能瓶颈

内核数据结构#

  • thread_t:线程核心结构
  • virtual_timer_t:软件定时器
  • semaphore_t:信号量同步
  • mutex_t:互斥锁保护
  • event_source_t:事件通知

基础数据结构#

  • ch_list_t:双向链表
  • ch_queue_t:队列
  • ch_priority_queue_t:优先级队列
  • ch_delta_list_t:增量链表

合理利用这些调试工具和数据结构,可以显著提高嵌入式系统的开发效率和运行可靠性。

ChibiOS/RT 调试与数据结构
/posts/chibios-rt-debug/
作者
JJZBQA
发布于
2024-12-22
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时