在嵌入式开发中,数据存储一直是个刚需:设备参数配置、传感器历史数据、运行日志记录……传统方案要么用文件系统裸奔,解析麻烦;要么上 SQLite,但移植门槛高、踩坑多。
今天分享一个基于睿擎派 RC3506的完整 SQLite 方案——从源码移植到 VFS 适配,从 DAO 层封装到 Shell 调试,手把手带你搞定嵌入式数据库。
为什么选 SQLite?
SQLite 是全世界部署最广泛的 SQL 数据库引擎,没有之一:
●零配置、无服务器:单文件数据库,无需独立进程,直接嵌入应用
●体积极小:核心代码约 250KB(可裁剪),RAM 占用 250KB 起
●全功能 SQL:支持事务、索引、触发器、视图,语法兼容标准 SQL
●可靠性极高:原子写入、故障恢复机制完善,数据安全有保障
●授权友好:公共域代码,商用免费,无需开源你的应用
从智能手机、汽车电子到工业网关,SQLite 几乎无处不在。对于 RT-Thread 嵌入式平台,它同样是大型数据持久化的常见方案。
架构总览
整个 SQLite for RT-Thread 方案分五层:

核心移植工作集中在 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+ |
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 命令
-
数据存储
+关注
关注
5文章
1038浏览量
53015 -
数据库
+关注
关注
7文章
4085浏览量
68560 -
嵌入式设备
+关注
关注
0文章
126浏览量
17750
发布评论请先 登录
从DeepSeek到Qwen,AI大模型的移植与交互实战指南-飞凌嵌入式
如何将sqlite3移植到嵌入式Linux开发板M6708上
SQLite在嵌入式Wince中的应用
明晚8点|睿擎文件系统实战:从开发到发布全流程解析
【直播预告】下周三晚8点|睿擎物联网实战:从传感器采集到MQTT上云全流程解析
明晚:睿擎物联网实战:从传感器采集到MQTT上云全流程解析|问学直播
实战 | 睿擎平台SQLite:嵌入式设备上的数据持久化方案,从移植到应用一文打通
评论