【NextPilot日志移植】Logger::run()主循环解析
📌 NextPilot 日志系统主循环逐行解析
🧱 函数作用
Logger::run()
是日志系统的主循环函数,负责:
- 控制日志启停(根据飞行状态、MAVLink 命令)
- 实时订阅传感器数据(如 IMU、电池、GPS)
- 写入日志到文件(
.ulg
)或通过 MAVLink 传输 - 管理缓冲区、处理丢包、记录性能数据
📁 移植关键性
此函数控制日志系统的整个生命周期,包括初始化、数据采集、缓冲区管理、资源清理等。移植时需重点关注:
- uORB 主题订阅机制(如
orb_subscribe()
) - 多线程安全(如
_writer.lock()
/unlock()
) - 日志文件路径与权限(如
mkdir()
、LOG_ROOT
) - 定时器与回调(如
hrt_call_every()
)
📄 逐行解析
1. 初始化日志根目录
if (_writer.backend() & LogWriter::BackendFile) {int mkdir_ret = mkdir(LOG_ROOT[(int)LogType::Full], S_IRWXU | S_IRWXG | S_IRWXO);if (mkdir_ret == 0) {PX4_INFO("log root dir created: %s", LOG_ROOT[(int)LogType::Full]);} else if (errno != -EEXIST) {PX4_ERR("failed creating log root dir: %s (%i)", LOG_ROOT[(int)LogType::Full], errno);if ((_writer.backend() & ~LogWriter::BackendFile) == 0) {return;}}
}
- 功能:创建日志存储根目录(如
/fs/microsd/log
) - 关键点:
LOG_ROOT
定义日志路径,需适配目标平台mkdir()
需支持跨平台(如 NuttX、Linux、Windows)- 若目录已存在(
errno == EEXIST
),继续执行
2. 订阅参数更新主题
uORB::Subscription parameter_update_sub(ORB_ID(parameter_update));
- 功能:创建
parameter_update
主题的订阅者,监听参数变化 - 关键点:
ORB_ID(parameter_update)
获取主题 ID- 需确保
uORB::Subscription
类已正确实现
3. 初始化所有订阅主题
if (!initialize_topics()) {return;
}
- 功能:调用
initialize_topics()
订阅所有传感器数据(如 IMU、电池) - 关键点:
initialize_topics()
需正确实现 uORB 主题的订阅逻辑- 若订阅失败(如主题未定义),函数返回
false
4. 计算最大日志消息缓冲区大小
int max_msg_size = 0;
for (int sub = 0; sub < _num_subscriptions; ++sub) {if (_subscriptions[sub].get_topic()->o_size > max_msg_size) {max_msg_size = _subscriptions[sub].get_topic()->o_size;}
}
max_msg_size += sizeof(ulog_message_data_s);
- 功能:确定缓冲区大小,确保能容纳最大日志消息
- 关键点:
_subscriptions[sub].get_topic()->o_size
是传感器数据结构体大小ulog_message_data_s
是 ULog 消息头结构体
5. 分配日志消息缓冲区
if (max_msg_size > _msg_buffer_len) {if (_msg_buffer) {delete[] (_msg_buffer);}_msg_buffer_len = max_msg_size;_msg_buffer = new uint8_t[_msg_buffer_len];if (!_msg_buffer) {PX4_ERR("failed to alloc message buffer");return;}
}
- 功能:动态分配缓冲区用于存储日志消息
- 关键点:
- 缓冲区大小需足够大,避免溢出
- 若已有缓冲区,先释放再重新分配
6. 初始化日志写入器
if (!_writer.init()) {PX4_ERR("writer init failed");return;
}
- 功能:调用
LogWriter
的init()
方法初始化底层写入器(如文件系统、MAVLink) - 关键点:
LogWriter
是抽象类,需实现init()
的具体逻辑(如文件打开、网络连接)- 若初始化失败,直接返回
7. 注册关机钩子
register_shutdown_hook(&Logger::request_stop_static);
- 功能:注册关机回调函数,确保日志在系统关闭前保存
- 关键点:
request_stop_static()
是静态函数,用于触发日志停止- 需实现
register_shutdown_hook()
的跨平台兼容性
8. 启动日志文件
if ((_log_mode == LogMode::boot_until_disarm || _log_mode == LogMode::boot_until_shutdown) && !disable_boot_logging) {start_log_file(LogType::Full);
}
- 功能:根据日志模式启动日志文件(如开机即开始记录)
- 关键点:
_log_mode
定义日志记录策略(如boot_until_disarm
表示从开机到解锁停止)start_log_file()
需正确实现日志文件名生成、目录创建、文件打开等逻辑
9. 初始化定时器回调
struct hrt_call timer_call{};
rt_sem_init(&_timer_callback_data.semaphore, "callback_lock", 0, RT_IPC_FLAG_PRIO);
- 功能:初始化高精度定时器,用于定期触发日志写入
- 关键点:
hrt_call_every()
设置定时器回调函数(timer_callback
)rt_sem
是实时信号量,需适配目标平台(如 NuttX、FreeRTOS)
10. 主循环逻辑
while (!should_exit()) {// 检查是否需要启动/停止日志const bool logging_started = start_stop_logging();// 处理 MAVLink 日志启停命令handle_vehicle_command_update();// 如果看门狗触发,初始化负载输出if (_timer_callback_data.watchdog_triggered.load()) {_timer_callback_data.watchdog_triggered.store(false);initialize_load_output(PrintLoadReason::Watchdog);}// 获取当前时间const hrt_abstime loop_time = hrt_absolute_time();// 如果日志已启动,开始记录数据if (_writer.is_started(LogType::Full)) {if (!was_started) {adjust_subscription_updates();}// 输出 CPU 负载日志if (_next_load_print != 0 && loop_time >= _next_load_print) {_next_load_print = 0;write_load_output();if (_should_stop_file_log) {_should_stop_file_log = false;stop_log_file(LogType::Full);continue;}}// 参数更新时写入日志if (!_should_stop_file_log) {if (parameter_update_sub.updated()) {parameter_update_s pupdate;parameter_update_sub.copy(&pupdate);write_changed_parameters(LogType::Full);}}// 获取缓冲区锁_writer.lock();// 遍历所有订阅主题,写入更新数据for (int sub_idx = 0; sub_idx < _num_subscriptions; ++sub_idx) {LoggerSubscription &sub = _subscriptions[sub_idx];const bool try_to_subscribe = (sub_idx == next_subscribe_topic_index);if (copy_if_updated(sub_idx, _msg_buffer + sizeof(ulog_message_data_s), try_to_subscribe)) {// 构建日志消息头const size_t msg_size = sizeof(ulog_message_data_s) + sub.get_topic()->o_size_no_padding;const uint16_t write_msg_size = static_cast<uint16_t>(msg_size - ULOG_MSG_HEADER_LEN);const uint16_t write_msg_id = sub.msg_id;// 写入日志消息头_msg_buffer[0] = (uint8_t)write_msg_size;_msg_buffer[1] = (uint8_t)(write_msg_size >> 8);_msg_buffer[2] = static_cast<uint8_t>(ULogMessageType::DATA);_msg_buffer[3] = (uint8_t)write_msg_id;_msg_buffer[4] = (uint8_t)(write_msg_id >> 8);// 写入完整日志if (write_message(LogType::Full, _msg_buffer, msg_size)) {// 记录字节数(调试用)}// 如果是任务日志,按需写入if (sub_idx < _num_mission_subs) {if (_writer.is_started(LogType::Mission)) {if (_mission_subscriptions[sub_idx].next_write_time < (loop_time / 100000)) {write_message(LogType::Mission, _msg_buffer, msg_size);}}}}}// 处理事件日志(如系统错误、警告)handle_event_updates(total_bytes);// 处理普通日志消息(如 PX4_WARN、PX4_ERR)log_message_s log_message;if (_log_message_sub.update(&log_message)) {const char *message = (const char *)log_message.text;int message_len = strlen(message);if (message_len > 0) {uint16_t write_msg_size = sizeof(ulog_message_logging_s) - sizeof(ulog_message_logging_s::message) - ULOG_MSG_HEADER_LEN + message_len;_msg_buffer[0] = (uint8_t)write_msg_size;_msg_buffer[1] = (uint8_t)(write_msg_size >> 8);_msg_buffer[2] = static_cast<uint8_t>(ULogMessageType::LOGGING);_msg_buffer[3] = log_message.severity + '0';rt_memcpy(_msg_buffer + 4, &log_message.timestamp, sizeof(ulog_message_logging_s::timestamp));rt_strncpy((char *)(_msg_buffer + 12), message, sizeof(ulog_message_logging_s::message));write_message(LogType::Full, _msg_buffer, write_msg_size + ULOG_MSG_HEADER_LEN);}}// 写入同步魔数(用于日志校验)if (loop_time - _last_sync_time > 500_ms) {uint16_t write_msg_size = static_cast<uint16_t>(sizeof(ulog_message_sync_s) - ULOG_MSG_HEADER_LEN);_msg_buffer[0] = (uint8_t)write_msg_size;_msg_buffer[1] = (uint8_t)(write_msg_size >> 8);_msg_buffer[2] = static_cast<uint8_t>(ULogMessageType::SYNC);_msg_buffer[3] = 0x2F;_msg_buffer[4] = 0x73;_msg_buffer[5] = 0x13;_msg_buffer[6] = 0x20;_msg_buffer[7] = 0x25;_msg_buffer[8] = 0x0C;_msg_buffer[9] = 0xBB;_msg_buffer[10] = 0x12;write_message(LogType::Full, _msg_buffer, write_msg_size + ULOG_MSG_HEADER_LEN);_last_sync_time = loop_time;}// 更新缓冲区统计信息for (int i = 0; i < (int)LogType::Count; ++i) {if (!_statistics[i].dropout_start && (_writer.get_buffer_fill_count_file((LogType)i > _statistics[i].high_water)) {_statistics[i].high_water = _writer.get_buffer_fill_count_file((LogType)i;}}// 发布日志状态publish_logger_status();// 释放缓冲区锁_writer.unlock();// 通知写入线程_writer.notify();// 更新订阅检查索引if (next_subscribe_topic_index != -1) {if (++next_subscribe_topic_index >= _num_subscriptions) {next_subscribe_topic_index = -1;next_subscribe_check = loop_time + TRY_SUBSCRIBE_INTERVAL;}} else if (loop_time > next_subscribe_check) {next_subscribe_topic_index = 0;}// 调试输出debug_print_buffer(total_bytes, timer_start);was_started = true;} else {// 未记录日志时尝试提前订阅新主题if (next_subscribe_topic_index != -1) {if (!_subscriptions[next_subscribe_topic_index].valid()) {_subscriptions[next_subscribe_topic_index].subscribe();}if (++next_subscribe_topic_index >= _num_subscriptions) {next_subscribe_topic_index = -1;next_subscribe_check = loop_time + TRY_SUBSCRIBE_INTERVAL;}} else if (loop_time > next_subscribe_check) {next_subscribe_topic_index = 0;}was_started = false;}// 更新参数update_params();// 等待下一次循环if (polling_topic_sub) {lockstep_progress(_lockstep_component);} else {rt_sem_take(&_timer_callback_data.semaphore, RT_WAITING_FOREVER);}rt_thread_mdelay(_log_interval / 1000 + 1);
}
🔁 循环核心逻辑流程图
[主循环开始]↓
检查日志启停条件(start_stop_logging)↓
处理 MAVLink 命令(handle_vehicle_command_update)↓
如果看门狗触发,初始化负载输出↓
若日志已启动:├─ 更新订阅索引(adjust_subscription_updates)├─ 遍历所有订阅主题:│ ├─ 检查是否有更新(copy_if_updated)│ ├─ 构建日志消息头│ └─ 写入日志(write_message)├─ 处理事件日志(handle_event_updates)├─ 处理普通日志(如 PX4_WARN)├─ 写入同步魔数(用于日志校验)├─ 更新缓冲区统计信息(丢包、高水位)├─ 发布日志状态(publish_logger_status)├─ 释放锁(_writer.unlock())└─ 通知写入线程(_writer.notify())
否则:↓
尝试提前订阅新主题(加速首次采集)↓
更新参数(update_params)↓
等待下一次循环(rt_sem_take / rt_thread_mdelay)
🧠 核心函数详解
1. start_stop_logging()
- 作用:根据飞行状态(如解锁、加锁)或 MAVLink 命令控制日志启停
- 关键逻辑:
- 如果飞行器已解锁且日志模式为
boot_until_disarm
,启动日志 - 如果飞行器加锁,停止日志
- 如果收到
VEHICLE_CMD_LOGGING_STOP
命令,停止日志
- 如果飞行器已解锁且日志模式为
2. handle_vehicle_command_update()
- 作用:处理来自 MAVLink 的日志控制命令(如
VEHICLE_CMD_LOGGING_START
) - 关键逻辑:
- 调用
ack_vehicle_command(...)
回复命令处理结果 - 根据命令类型启动或停止日志
- 调用
3. copy_if_updated()
- 作用:检查订阅的主题是否有新数据,若有则复制到缓冲区
- 关键逻辑:
orb_check()
检查是否有新数据orb_copy()
将数据复制到_msg_buffer
4. write_message()
- 作用:将日志数据写入文件或 MAVLink
- 关键逻辑:
- 调用
_writer.write_message()
实际写入 - 记录丢包和缓冲区使用情况
- 调用
5. publish_logger_status()
- 作用:发布日志状态(如缓冲区使用率、写入速度)
- 关键逻辑:
- 构造
logger_status_s
结构体 - 调用
orb_publish()
发布状态消息
- 构造
🔧 移植注意事项
步骤 | 移植建议 |
---|---|
uORB 主题订阅 | 确保 ORB_ID(...) 定义与目标平台一致 |
日志文件路径 | 适配文件系统(如 LOG_ROOT 支持 SD 卡、NAND、网络) |
多线程安全 | 使用互斥锁(如 pthread_mutex_t 或 rt_mutex ) |
缓冲区管理 | 避免缓冲区溢出,合理设置 _log_interval |
日志文件写入 | 实现 LogWriterFile::start_log_file() 的文件写入逻辑 |
MAVLink 支持 | 实现 LogWriterMavlink::start_log() 的网络传输逻辑 |
📦 相关结构体与枚举
1. ULogMessageType
enum class ULogMessageType {DATA = 'D',FORMAT = 'F',INFO = 'I',INFO_MULTIPLE = 'M',PARAMETER = 'P',SYNC = 'S',DROPOUT = 'L',LOGGING = 'T'
};
2. logger_status_s
struct logger_status_s {uint64_t timestamp;size_t buffer_used_bytes;size_t buffer_size_bytes;size_t num_messages;
};
📌 总结
功能模块 | 核心函数 | 移植难点 |
---|---|---|
日志启停控制 | start_stop_logging() | 飞行状态检测、MAVLink 命令处理 |
主题订阅 | copy_if_updated() | uORB 主题注册与数据获取 |
日志写入 | write_message() | 文件/MAVLink 写入逻辑实现 |
缓冲区管理 | _msg_buffer | 跨平台内存分配与释放 |
同步魔数 | write_message(..., ULogMessageType::SYNC) | 确保日志校验正确 |
性能监控 | write_perf_data() | 资源占用统计与分析 |
📝 扩展阅读
- uORB 主题定义:
msg/
目录下的.msg
文件 - ULog 格式定义:
src/modules/logger/ulog.h
- 日志文件分析工具:QGroundControl、PlotJuggler