实战 | 睿擎平台SQLite:嵌入式设备上的数据持久化方案,从移植到应用一文打通

2026.05.01 | 焜财商富 | 32410次围观

嵌入式开发中,数据存储一直是个刚需:设备参数配置、传感器历史数据、运行日志记录……传统方案要么用文件系统裸奔,解析麻烦;要么上 SQLite,但移植门槛高、踩坑多。

今天分享一个基于睿擎派 RC3506的完整 SQLite 方案——从源码移植到 VFS 适配,从 DAO 层封装到 Shell 调试,手把手带你搞定嵌入式数据库。

为什么选 SQLite?

SQLite 是全世界部署最广泛的 SQL 数据库引擎,没有之一:

零配置、无服务器:单文件数据库,无需独立进程,直接嵌入应用

体积极小:核心代码约 250KB(可裁剪),RAM 占用 250KB 起

全功能 SQL:支持事务、索引、触发器、视图,语法兼容标准 SQL

可靠性极高:原子写入、故障恢复机制完善,数据安全有保障

授权友好:公共域代码,商用免费,无需开源你的应用

智能手机汽车电子到工业网关,SQLite 几乎无处不在。对于 RT-Thread 嵌入式平台,它同样是大型数据持久化的常见方案。

架构总览

整个 SQLite for RT-Thread 方案分五层:

a35cf6ce-43bd-11f1-ab55-92fbcf53809c.png

核心移植工作集中在 VFS 适配层,而 dbhelper 中间层则大幅简化了应用开发难度。

移植实战:VFS 适配层详解

1. 编译配置(sqlite_config_rtthread.h)

首先需要告诉 SQLite:“我在 RT-Thread 上运行”。关键宏定义:

#defineSQLITE_OS_OTHER 1// 不使用默认 OS 层

#defineSQLITE_OS_RTTHREAD 1 // 启用 RT-Thread VFS

#defineSQLITE_THREADSAFE 1 // 启用线程安全

#defineSQLITE_OMIT_WAL 1 // 禁用 WAL(减少资源占用)

#defineSQLITE_OMIT_LOAD_EXTENSION 1 // 禁用动态加载扩展

SQLITE_OMIT_WAL=1 是嵌入式场景的常见选择:WAL(Write-Ahead Logging)虽然提升并发性能,但会增加文件数量和内存占用。对于单线程写入场景,回滚日志模式足够。

2. VFS 注册(rtthread_vfs.c)

SQLite 要求所有 VFS 实现 sqlite3_vfs 结构体:

staticsqlite3_vfs _rtthread_vfs={

3,// iVersion:VFS 版本号

sizeof(RTTHREAD_SQLITE_FILE_T),// szOsFile:文件句柄大小

RTTHREAD_MAX_PATHNAME,// mxPathname:最大路径长度(256)

0,// pNext:链表指针(内部使用)

"rt-thread",// zName:VFS 名称

0,// pAppData:应用数据指针

_rtthread_vfs_open,// xOpen:打开文件

_rtthread_vfs_delete,// xDelete:删除文件

_rtthread_vfs_access,// xAccess:检查文件权限

_rtthread_vfs_fullpathname,// xFullPathname:获取绝对路径

// ... 其他接口

};

SQLITE_APIintsqlite3_os_init(void)

{

sqlite3_vfs_register(&_rtthread_vfs,1);// 注册为默认 VFS

returnSQLITE_OK;

}

3. 文件 IO 实现(rtthread_io_methods.c)

核心的读写接口,直接调用 DFS 的 read()/write()/lseek():

staticint_rtthread_io_read(sqlite3_file*file_id,void*pbuf,intcnt,sqlite3_int64 offset)

{

RTTHREAD_SQLITE_FILE_T*file=(RTTHREAD_SQLITE_FILE_T*)file_id;

// 定位到指定偏移

if(lseek(file->fd,offset,SEEK_SET)!=offset){

returnSQLITE_IOERR_READ;

}

// 循环读取直到完成(处理 EINTR 中断)

intr_cnt;

do{

r_cnt=read(file->fd,pbuf,cnt);

if(r_cnt==cnt)break;

if(r_cnt<0&&errno!=EINTR)returnSQLITE_IOERR_READ;

// 处理部分读取

if(r_cnt>0){

cnt-=r_cnt;

pbuf=(void*)(r_cnt+(char*)pbuf);

}

}while(r_cnt>0);

returnSQLITE_OK;

}

staticint_rtthread_io_write(sqlite3_file*file_id,constvoid*pbuf,intcnt,sqlite3_int64 offset)

{

RTTHREAD_SQLITE_FILE_T*file=(RTTHREAD_SQLITE_FILE_T*)file_id;

if(lseek(file->fd,offset,SEEK_SET)!=offset){

returnSQLITE_IOERR_WRITE;

}

intw_cnt;

do{

w_cnt=write(file->fd,pbuf,cnt);

if(w_cnt==cnt)break;

if(w_cnt<0&&errno!=EINTR)returnSQLITE_IOERR_WRITE;

if(w_cnt>0){

cnt-=w_cnt;

pbuf=(void*)(w_cnt+(char*)pbuf);

}

}while(w_cnt>0);

returnSQLITE_OK;

}

特别注意 EINTR 处理:在实时系统中,系统调用可能被信号中断,必须重试。

4. 文件锁实现(rtthread_io_methods.c)

SQLite 的并发控制依赖文件锁。RT-Thread 没有文件锁,我们用信号量模拟

typedefstruct{

sqlite3_io_methodsconst*pMethod;

intfd;// 文件描述符

inteFileLock;// 锁状态:NO_LOCK/SHARED/EXCLUSIVE

structrt_semaphoresem;// 信号量

}RTTHREAD_SQLITE_FILE_T;

staticint_rtthread_io_lock(sqlite3_file*file_id,inteFileLock)

{

RTTHREAD_SQLITE_FILE_T*file=(RTTHREAD_SQLITE_FILE_T*)file_id;

rt_sem_tpsem=&file->sem;

// 已持有锁,直接升级级别

if(file->eFileLock>NO_LOCK){

file->eFileLock=eFileLock;

returnSQLITE_OK;

}

// 尝试获取信号量

if(rt_sem_trytake(psem)!=RT_EOK){

returnSQLITE_BUSY;// 其他线程持有锁

}

file->eFileLock=eFileLock;

returnSQLITE_OK;

}

staticint_rtthread_io_unlock(sqlite3_file*file_id,inteFileLock)

{

RTTHREAD_SQLITE_FILE_T*file=(RTTHREAD_SQLITE_FILE_T*)file_id;

if(eFileLock==NO_LOCK){

rt_sem_release(&file->sem);// 释放信号量

}

file->eFileLock=eFileLock;

returnSQLITE_OK;

}

注意:这是进程内锁,只能防止同一进程内的多线程并发访问。如果多个进程同时访问同一数据库文件,仍需依赖文件系统的锁机制或外部协调。

5. 随机数与时间戳

SQLite 内部需要随机数(生成临时文件名)和当前时间戳:

staticint_rtthread_vfs_randomness(sqlite3_vfs*pvfs,intnByte,char*zOut)

{

chartick8=(char)rt_tick_get();

chartick16=(char)(rt_tick_get()>>8);

for(inti=0;i<nByte;i++){

zOut[i]=(char)(i^tick8^tick16);

tick8=zOut[i];

tick16=~(tick8^tick16);

}

returnnByte;

}

staticint_rtthread_vfs_current_time_int64(sqlite3_vfs*pvfs,sqlite3_int64*pnow)

{

time_tt;

time(&t);

*pnow=((sqlite3_int64)t)*1000+24405875*(sqlite3_int64)8640000;

returnSQLITE_OK;

}

中间层封装:dbhelper 设计

直接调用 SQLite 原生 API 门槛较高:需要手动管理 sqlite3_stmt、处理错误码、编写回滚逻辑。dbhelper 封装了常用操作,让应用层代码更简洁。

初始化

intdb_helper_init(void)

{

sqlite3_initialize();// 初始化 SQLite 内部状态

db_mutex_lock=rt_mutex_create("dbmtx",RT_IPC_FLAG_FIFO);

returnRT_EOK;

}

INIT_APP_EXPORT(db_helper_init);// 自动初始化

数据库创建

intdb_create_database(constchar*sqlstr)

{

returndb_nonquery_operator(sqlstr,0,0);

}

// 使用示例

constchar*sql="CREATE TABLE student("

"id INTEGER PRIMARY KEY AUTOINCREMENT,"

"name VARCHAR(32) NOT NULL,"

"score INT NOT NULL);";

db_create_database(sql);

带数据绑定的批量插入

intdb_nonquery_operator(constchar*sqlstr,

int(*bind)(sqlite3_stmt*,int,void*),

void*param);

关键优化:一条 SQL 语句只编译一次,然后循环绑定数据执行,大幅提升批量操作性能。

// 插入学生数据

staticintstudent_insert_bind(sqlite3_stmt*stmt,intindex,void*arg)

{

rt_list_t*h=arg;

student_t*s;

rt_list_for_each_entry(s,h,list){

sqlite3_reset(stmt);

sqlite3_bind_text(stmt,1,s->name,strlen(s->name),NULL);

sqlite3_bind_int(stmt,2,s->score);

sqlite3_step(stmt);

}

returnSQLITE_OK;

}

db_nonquery_operator("INSERT INTO student(name,score) VALUES(?,?);",

student_insert_bind,&student_list);

内部已开启事务,失败自动回滚。

注意:db_nonquery_by_varpara() 是单条 SQL 执行,不支持事务。如需事务,请使用 db_nonquery_operator() 或 db_nonquery_transaction()。

带变参的查询

intdb_query_by_varpara(constchar*sql,

int(*create)(sqlite3_stmt*,void*),

void*arg,

constchar*fmt,...);

// 查询指定 ID 的学生

staticintstudent_create(sqlite3_stmt*stmt,void*arg)

{

student_t*s=arg;

if(sqlite3_step(stmt)!=SQLITE_ROW)return0;

s->id=db_stmt_get_int(stmt,0);

db_stmt_get_text(stmt,1,s->name);

s->score=db_stmt_get_int(stmt,2);

returnSQLITE_OK;

}

student_ts;

db_query_by_varpara("SELECT * FROM student WHERE id=?;",

student_create,&s,"%d",student_id);

实战示例:学生成绩管理系统

示例工程 12_data_parsers_sqlite 实现了一个完整的学生成绩管理 DAO 层。

表结构设计

CREATETABLEstudent(

idINTEGERPRIMARYKEYAUTOINCREMENT,

nameVARCHAR(32)NOTNULL,

scoreINTEGERNOTNULL

);

DAO 层核心接口

// 增:批量插入学生

intstudent_add(rt_list_t*h);

// 删:按 ID 删除或删除全部

intstudent_del(intid);

intstudent_del_all(void);

// 改:更新学生信息

intstudent_update(student_t*s);

// 查:按 ID 查询单个,或查询全部

intstudent_get_by_id(student_t*s,intid);

intstudent_get_all(rt_list_t*q);

// 条件查询:按分数区间查询,支持升序/降序

intstudent_get_by_score(rt_list_t*h,intlow,inthigh,enumorder_typeorder);

Shell 命令调试

示例提供了完整的 Shell 命令,方便调试:

msh />create_student_tbl # 创建数据库和表

Database path: /data/stu_info.db

msh />stuadd100# 批量插入 100 条记录

Insert100record(s): 125ms, speed: 1ms/record

msh />stu # 查询全部

id:1 name:Student1234 score:87

id:2 name:Student5678 score:92

...

record(s):100

msh />stu score60100-d # 查询 60-100 分,降序排列

id:88 name:Student9999 score:99

id:42 name:Student1234 score:95

...

msh />stu update1Alice100# 更新记录

update record success!

msh />stu del1# 删除指定记录

Del record success with id:1

msh />stu del # 删除全部

Del all record success!

资源占用

资源

最小配置

典型配置

RAM

250KB

500KB+

ROM

310KB

500KB+

栈空间

4KB

8KB

睿擎派 RC3506(RK3506,512MB DDR)完全满足需求。如果资源紧张,可通过 SQLITE_OMIT_* 宏裁剪不需要的功能。

总结

SQLite 为嵌入式设备提供了企业级的数据管理能力,而 RT-Thread 的 VFS 适配层让移植工作变得简单。通过 dbhelper 中间层封装,应用开发者无需深入了解 SQLite API,就能快速实现数据持久化功能。

关键要点回顾:

VFS 适配层是移植核心,实现文件 IO、锁、随机数、时间戳

dbhelper 封装了事务、数据绑定、错误处理,大幅简化应用代码

Prepared Statement + 事务是批量操作的性能关键

单写者模型:多线程读,单线程写,避免锁竞争

完整示例代码已集成到睿擎 SDK V2604,欢迎体验!

配套资料包

想在自己的项目里复现 SQLite 移植?我们整理了完整资料包,助你快速上手:

SQLite 移植源码包(含 VFS 适配层、dbhelper 封装)

交叉编译脚本与集成示例(RuiChing Studio 可直接导入)

学生成绩管理 DAO 层完整代码

本篇文章涉及的全部 SQL 脚本与 Shell 命令