1. 项目概述

Sqlite µLogger(微日志器)是一个专为资源受限嵌入式系统设计的轻量级 SQLite 数据持久化库。其核心目标是在仅具备 2 KB SRAM 的微控制器(如 Arduino Uno)上,实现结构化数据的可靠写入、高效检索与断电恢复能力。该库并非 SQLite 官方嵌入式移植(如 SQLite3 for ARM),而是基于 SQLite 文件格式规范(SQLite Database File Format, version 3)自主实现的精简型日志引擎,完全绕过 SQLite 复杂的虚拟机、B-tree 索引层和事务日志(WAL/Journal)机制,转而采用线性追加(append-only)、页对齐(page-aligned)和内存映射式读取的设计哲学。

与传统文本日志(CSV/JSON)、Flash 文件系统(LittleFS、FatFS)或通用数据库驱动不同,µLogger 的本质是“ 面向日志的 SQLite 兼容二进制文件生成器 ”。它不解析 SQL 语句,不执行查询计划,不维护 B+ 树索引;它只做三件事:

  1. 将用户定义的结构化记录(含时间戳)序列化为固定长度的二进制行;
  2. 按 SQLite 页大小(默认 512 字节)对齐写入存储介质(SD 卡、SPIFFS、eMMC);
  3. 提供 O(log n) 时间复杂度的二分查找接口,直接在原始二进制文件中定位 RowID 或时间戳。

这种设计使 µLogger 在极小内存开销下达成远超通用方案的检索性能——在 Arduino Uno 上对 100 万条记录(70 MB 数据库文件)按时间戳二分查找仅需 1.6 秒 ,而同等规模的文本日志顺序扫描则需数分钟,且无法支持随机访问。

1.1 设计哲学与工程权衡

维度 传统 SQLite 嵌入式移植 Sqlite µLogger 工程目的
内存占用 ≥16 KB RAM(含页面缓存、VM栈、临时排序区) ≤ page_size + 函数调用栈 (典型值:512 B + ~200 B) 适配 ATmega328P(2 KB SRAM)等超低资源 MCU
写入模型 随机写入 + WAL 日志 + Checkpoint 严格线性追加(append-only) ,无覆盖、无擦除 消除 Flash/SD 卡写放大,规避掉电时页撕裂(page tearing)风险
索引机制 B+ Tree 索引(需额外存储空间与构建时间) 零索引 ,依赖 RowID 递增性与时间戳单调性 节省存储空间,避免索引维护开销,简化固件逻辑
数据一致性 ACID 事务(需 journal 文件) 最终一致性 ,通过页头校验与 Recovery 流程保障 在无额外存储介质前提下,以可接受的恢复延迟换取可靠性
查询能力 完整 SQL(SELECT/JOIN/ORDER BY) 仅支持主键(RowID)与时间戳二分查找 聚焦日志场景核心需求:按序写入、按 ID/时间快速定位

该库的“非标准性”恰是其价值所在:它放弃通用数据库的灵活性,换取在严苛资源约束下的确定性行为与可预测性能。所有功能均围绕一个事实展开—— 嵌入式日志的本质是时序数据流,而非关系型数据集

2. 核心架构与数据布局

2.1 数据库文件物理结构

µLogger 生成的 .db 文件是标准 SQLite v3 格式文件,可直接在 PC 端用 sqlite3 CLI 或 DB Browser for SQLite 打开并执行任意 SQL 查询。其物理布局严格遵循 SQLite File Format Specification ,关键组成部分如下:

偏移量 长度 内容 说明
0x0000 16 字节 Magic Header "SQLite format 3\0" 标准 SQLite 文件标识
0x0010 2 字节 Page Size(大端) 默认 0x0200 (512 字节),必须为 512/1024/2048/4096
0x0012 1 字节 Write Version 固定为 0x01 (legacy format)
0x0013 1 字节 Read Version 固定为 0x01
0x0014 2 字节 Reserved Bytes per Page 固定为 0x0000 (无保留字节)
0x0016 1 字节 Max Embedded Payload Fraction 0x00 (未使用)
0x0017 1 字节 Min Embedded Payload Fraction 0x00 (未使用)
0x0018 1 字节 Leaf Payload Fraction 0x00 (未使用)
0x0019 1 字节 File Change Counter 动态更新 ,每次写入后自增,用于 Recovery 检测
0x001A 4 字节 Size in Pages(大端) 当前文件总页数,初始为 0x00000001 (第 1 页为表头)
0x001E 4 字节 Page Number of 1st Freelist Trunk 0x00000000 (freelist 未启用)
0x0022 4 字节 Total Number of Freelist Pages 0x00000000
0x0026 4 字节 Schema Cookie 0x00000000 (无 schema 变更)
0x002A 4 字节 Schema Format Number 0x00000000 (legacy)
0x002E 4 字节 Default Page Cache Size 0x00000000
0x0032 4 字节 Largest Root B-Tree Page 0x00000001 (根页为第 1 页)
0x0036 4 字节 Text Encoding 0x00000001 (UTF-8)
0x003A 4 字节 User Version 0x00000000 (未使用)
0x003E 4 字节 Incremental Vacuum Mode 0x00000000
0x0042 4 字节 Application ID 0x00000000 (可自定义)
0x0046 20 字节 Reserved Space 0x00
0x005A 2 字节 Version Valid For 0x0000 (未使用)
0x005C 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (3.0.0)
0x0060 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0064 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0068 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x006C 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0070 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0074 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0078 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x007C 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0080 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0084 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0088 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x008C 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0090 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0094 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0098 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x009C 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00A0 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00A4 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00A8 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00AC 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00B0 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00B4 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00B8 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00BC 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00C0 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00C4 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00C8 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00CC 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00D0 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00D4 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00D8 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00DC 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00E0 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00E4 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00E8 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00EC 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00F0 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00F4 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00F8 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x00FC 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0100 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0104 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0108 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x010C 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0110 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0114 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0118 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x011C 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0120 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0124 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0128 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x012C 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0130 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0134 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0138 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x013C 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0140 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0144 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0148 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x014C 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0150 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0154 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0158 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x015C 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0160 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0164 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0168 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x016C 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0170 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0174 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0178 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x017C 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0180 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0184 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0188 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x018C 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0190 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0194 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0198 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x019C 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01A0 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01A4 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01A8 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01AC 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01B0 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01B4 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01B8 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01BC 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01C0 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01C4 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01C8 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01CC 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01D0 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01D4 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01D8 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01DC 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01E0 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01E4 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01E8 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01EC 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01F0 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01F4 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01F8 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x01FC 4 字节 SQLITE_VERSION_NUMBER 0x03000000 (冗余)
0x0200 512 字节 Page 1: Table Header 包含 CREATE TABLE 语句(≤ page_size - 100 字节)及元数据
0x0400 512 字节 Page 2: First Data Page 存储首条记录(RowID=1)
0x0600 512 字节 Page 3: Second Data Page 存储后续记录,按 RowID 递增顺序填充

关键洞察 :µLogger 将整个数据库视为一个 单表、单页头、多数据页 的线性结构。Table Header 页(Page 1)存储建表语句(如 CREATE TABLE logs (id INTEGER PRIMARY KEY, ts INTEGER, val REAL); ),所有数据页(Page 2+)仅存放二进制记录,无任何 B-tree 结构。RowID 由 µLogger 自动递增生成(从 1 开始),时间戳字段(若存在)要求单调递增以保证二分查找有效性。

2.2 记录序列化格式

每条记录被序列化为固定长度的二进制块,长度由 CREATE TABLE 语句中各列类型决定。µLogger 支持以下基础类型及其序列化规则:

SQLite 类型 C 类型 序列化长度(字节) 说明
INTEGER int32_t 4 小端序(Little-Endian)
REAL float 4 IEEE 754 单精度浮点,小端序
TEXT char[] 可变长(需预分配) \0 结尾,长度计入记录总长
BLOB uint8_t[] 可变长(需预分配) 原始字节流,无额外封装

示例 :对于表 CREATE TABLE sensor (id INTEGER, ts INTEGER, temp REAL, name TEXT); ,假设 name 最大长度为 16 字节,则单条记录长度 = 4( id ) + 4( ts ) + 4( temp ) + 16( name ) + 1( \0 ) = 29 字节 。实际写入时,µLogger 会将此 29 字节记录填充至页内连续空间,页内剩余空间用于存放后续记录。

重要限制 :由于 µLogger 不解析 SQL, TEXT BLOB 列的长度必须在编译时通过宏或配置确定(如 #define MAX_NAME_LEN 16 ),运行时不可变。这避免了动态内存分配,符合裸机环境约束。

3. API 接口详解

µLogger 提供 C 风格函数接口,所有操作均通过 sqlite_logger_t 句柄进行。该句柄包含底层 I/O 回调、页缓存指针及状态标志,是线程安全的(需用户确保回调函数线程安全)。

3.1 初始化与配置

// 定义 I/O 回调函数类型
typedef struct {
    int (*read)(void* ctx, uint32_t offset, uint8_t* buf, uint32_t len);
    int (*write)(void* ctx, uint32_t offset, const uint8_t* buf, uint32_t len);
    int (*sync)(void* ctx); // 刷写缓存到物理介质
    void* ctx; // 用户上下文(如 SD 卡句柄、SPIFFS 文件指针)
} sqlite_io_t;

// 初始化 logger 实例
sqlite_logger_t* sqlite_logger_init(
    const char* db_path,      // 数据库文件路径(如 "/sd/log.db")
    const char* table_sql,     // CREATE TABLE 语句(≤ page_size - 100 字节)
    uint16_t page_size,        // 页大小(512/1024/2048/4096)
    sqlite_io_t* io,           // I/O 回调结构体
    uint8_t* page_buf,         // 页缓存缓冲区(大小 = page_size)
    uint32_t buf_len           // 缓冲区长度(必须 ≥ page_size)
);

// 示例:Arduino Uno 初始化(使用 SparkFun MicroSD Shield)
#include <SPI.h>
#include <SD.h>
File db_file;

int sd_read(void* ctx, uint32_t offset, uint8_t* buf, uint32_t len) {
    db_file.seek(offset);
    return db_file.read(buf, len) == len ? 0 : -1;
}

int sd_write(void* ctx, uint32_t offset, const uint8_t* buf, uint32_t len) {
    db_file.seek(offset);
    return db_file.write(buf, len) == len ? 0 : -1;
}

int sd_sync(void* ctx) {
    db_file.flush();
    return 0;
}

sqlite_io_t sd_io = {
    .read = sd_read,
    .write = sd_write,
    .sync = sd_sync,
    .ctx = &db_file
};

uint8_t page_cache[512]; // 512 字节页缓存
sqlite_logger_t* logger = sqlite_logger_init(
    "/sd/log.db",
    "CREATE TABLE logs (id INTEGER PRIMARY KEY, ts INTEGER, voltage REAL);",
    512,
    &sd_io,
    page_cache,
    sizeof(page_cache)
);

3.2 写入操作

// 插入一条记录(自动分配 RowID)
int sqlite_logger_insert(sqlite_logger_t* logger, const void* record, uint32_t record_len);

// 插入带指定 RowID 的记录(需确保 RowID 唯一且递增)
int sqlite_logger_insert_with_id(sqlite_logger_t* logger, uint32_t rowid, const void* record, uint32_t record_len);

// 示例:记录 ADC 电压值
typedef struct {
    uint32_t ts;     // 时间戳(毫秒)
    float voltage;   // 电压值
} log_record_t;

log_record_t rec = {
    .ts = millis(),
    .voltage = analogRead(A0) * 5.0 / 1024.0
};
sqlite_logger_insert(logger, &rec, sizeof(rec));

内部流程

  1. 检查当前页是否还有足够空间容纳 record_len
  2. 若空间不足,调用 io->write 将当前页写入介质,并递增页计数器;
  3. record 复制到页缓存末尾;
  4. 更新页内记录计数;
  5. 调用 io->sync 确保数据落盘(可选,取决于可靠性要求)。

3.3 检索操作

// 按 RowID 查找(O(1) 平均,O(log n) 最坏)
int sqlite_logger_find_by_rowid(sqlite_logger_t* logger, uint32_t rowid, void* out_record, uint32_t record_len);

// 按时间戳二分查找(要求 ts 列单调递增)
int sqlite_logger_find_by_timestamp(sqlite_logger_t* logger, uint32_t target_ts, void* out_record, uint32_t record_len);

// 获取记录总数
uint32_t sqlite_logger_get_count(sqlite_logger_t* logger);

// 示例:查找最近一条记录
log_record_t latest;
if (sqlite_logger_find_by_rowid(logger, sqlite_logger_get_count(logger), &latest, sizeof(latest)) == 0) {
    Serial.printf("Latest: ts=%lu, v=%.2f\n", latest.ts, latest.voltage);
}

// 示例:查找 ts >= 1000000 的第一条记录
log_record_t found;
if (sqlite_logger_find_by_timestamp(logger, 1000000, &found, sizeof(found)) == 0) {
    Serial.printf("Found at ts=%lu\n", found.ts);
}

二分查找实现要点

  • 读取页头获取总记录数 n
  • 计算中间记录索引 mid = low + (high - low) / 2
  • 通过 rowid_to_offset() 计算 mid 对应的文件偏移量( offset = page_size + mid * record_len );
  • 调用 io->read 读取该记录的时间戳;
  • 比较后收缩搜索区间,重复至找到或区间为空。

3.4 恢复与维护

// 检测并修复因掉电导致的损坏页
int sqlite_logger_recover(sqlite_logger_t* logger);

// 获取数据库状态(用于调试)
void sqlite_logger_get_status(sqlite_logger_t* logger, sqlite_status_t* status);

// 示例:启动时自动恢复
if (sqlite_logger_recover(logger) != 0) {
    Serial.println("Recovery failed!");
}

恢复机制

  1. 读取文件末尾若干页(通常 2-3 页);
  2. 检查每页的 File Change Counter 是否与前一页一致;
  3. 找到最后一个 Change Counter 有效递增的页,截断其后的所有页;
  4. 更新 Size in Pages 字段并同步。

4. 实际应用案例分析

4.1 Arduino Uno + SparkFun MicroSD Shield

硬件配置 :ATmega328P @ 16 MHz, 2 KB SRAM, SparkFun MicroSD Shield(SPI 接口)。
挑战 :SD 卡初始化耗时约 500 ms,写入延迟波动大(1-10 ms/页),SRAM 极其紧张。

µLogger 优化实践

  • 设置 page_size = 512 page_cache 占用 512 字节;
  • 使用 sqlite_logger_insert() 替代频繁 write() ,减少 SD 卡命令开销;
  • 关闭 io->sync (依赖 SD 卡内部缓存),在关键日志后手动 flush()
  • 时间戳使用 millis() (无需 RTC,误差可接受)。

性能实测 (100 万条记录,70 MB 文件):

  • 写入速率:≈ 120 条/秒(受 SD 卡 Class 4 限制);
  • RowID 查找:0.8 ms(平均);
  • 时间戳二分查找:1.6 秒(100 万次比较,每次读取
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐