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

描述

在嵌入式开发中,数据存储一直是个刚需:设备参数配置、传感器历史数据、运行日志记录……传统方案要么用文件系统裸奔,解析麻烦;要么上 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 上运行”。关键宏定义:

#define SQLITE_OS_OTHER        1// 不使用默认 OS 层
#define SQLITE_OS_RTTHREAD     1    // 启用 RT-Thread VFS
#define SQLITE_THREADSAFE      1    // 启用线程安全
#define SQLITE_OMIT_WAL        1    // 禁用 WAL(减少资源占用)
#define SQLITE_OMIT_LOAD_EXTENSION 1  // 禁用动态加载扩展
 

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


 

2. VFS 注册(rtthread_vfs.c)

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

static sqlite3_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_API intsqlite3_os_init(void)
{
sqlite3_vfs_register(&_rtthread_vfs,1);// 注册为默认 VFS
return SQLITE_OK;
}
 


 

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

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

staticint_rtthread_io_read(sqlite3_file *file_id,void*pbuf,int cnt, sqlite3_int64 offset)
{
    RTTHREAD_SQLITE_FILE_T *file =(RTTHREAD_SQLITE_FILE_T*)file_id;

// 定位到指定偏移
if(lseek(file->fd, offset,SEEK_SET)!= offset){
return SQLITE_IOERR_READ;
}

// 循环读取直到完成(处理 EINTR 中断)
int r_cnt;
do{
        r_cnt =read(file->fd, pbuf, cnt);
if(r_cnt == cnt)break;
if(r_cnt <0&& errno != EINTR)return SQLITE_IOERR_READ;
// 处理部分读取
if(r_cnt >0){
            cnt -= r_cnt;
            pbuf =(void*)(r_cnt +(char*)pbuf);
}
}while(r_cnt >0);

return SQLITE_OK;
}

staticint_rtthread_io_write(sqlite3_file *file_id,constvoid*pbuf,int cnt, sqlite3_int64 offset)
{
    RTTHREAD_SQLITE_FILE_T *file =(RTTHREAD_SQLITE_FILE_T*)file_id;

if(lseek(file->fd, offset,SEEK_SET)!= offset){
return SQLITE_IOERR_WRITE;
}

int w_cnt;
do{
        w_cnt =write(file->fd, pbuf, cnt);
if(w_cnt == cnt)break;
if(w_cnt <0&& errno != EINTR)return SQLITE_IOERR_WRITE;
if(w_cnt >0){
            cnt -= w_cnt;
            pbuf =(void*)(w_cnt +(char*)pbuf);
}
}while(w_cnt >0);

return SQLITE_OK;
}
 

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


 

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

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

typedefstruct{
    sqlite3_io_methods const*pMethod;
int fd;// 文件描述符
int eFileLock;// 锁状态:NO_LOCK/SHARED/EXCLUSIVE
structrt_semaphore sem;// 信号量
} RTTHREAD_SQLITE_FILE_T;

staticint_rtthread_io_lock(sqlite3_file *file_id,int eFileLock)
{
    RTTHREAD_SQLITE_FILE_T *file =(RTTHREAD_SQLITE_FILE_T*)file_id;
rt_sem_t psem =&file->sem;

// 已持有锁,直接升级级别
if(file->eFileLock > NO_LOCK){
        file->eFileLock = eFileLock;
return SQLITE_OK;
}

// 尝试获取信号量
if(rt_sem_trytake(psem)!= RT_EOK){
return SQLITE_BUSY;// 其他线程持有锁
}

    file->eFileLock = eFileLock;
return SQLITE_OK;
}

staticint_rtthread_io_unlock(sqlite3_file *file_id,int eFileLock)
{
    RTTHREAD_SQLITE_FILE_T *file =(RTTHREAD_SQLITE_FILE_T*)file_id;

if(eFileLock == NO_LOCK){
rt_sem_release(&file->sem);// 释放信号量
}
    file->eFileLock = eFileLock;
return SQLITE_OK;
}
 

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


 

5. 随机数与时间戳

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

staticint_rtthread_vfs_randomness(sqlite3_vfs *pvfs,int nByte,char*zOut)
{
char tick8 =(char)rt_tick_get();
char tick16 =(char)(rt_tick_get()>>8);

for(int i =0; i < nByte; i++){
        zOut[i]=(char)(^ tick8 ^ tick16);
        tick8 = zOut[i];
        tick16 =~(tick8 ^ tick16);
}
return nByte;
}

staticint_rtthread_vfs_current_time_int64(sqlite3_vfs *pvfs, sqlite3_int64 *pnow)
{
time_t t;
time(&t);
*pnow =((sqlite3_int64)t)*1000+24405875*(sqlite3_int64)8640000;
return SQLITE_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);
return RT_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,int index,void*arg)
{
rt_list_t*= 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);
}
return SQLITE_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*= 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);
return SQLITE_OK;
}

student_t s;
db_query_by_varpara("SELECT * FROM student WHERE id=?;",
                    student_create,&s,"%d", student_id);
 

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

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

表结构设计

CREATETABLE student(
    id INTEGERPRIMARYKEY AUTOINCREMENT,
    name VARCHAR(32)NOTNULL,
    score INTEGERNOTNULL
);
 

DAO 层核心接口

// 增:批量插入学生
intstudent_add(rt_list_t*h);

// 删:按 ID 删除或删除全部
intstudent_del(int id);
intstudent_del_all(void);

// 改:更新学生信息
intstudent_update(student_t*s);

// 查:按 ID 查询单个,或查询全部
intstudent_get_by_id(student_t*s,int id);
intstudent_get_all(rt_list_t*q);

// 条件查询:按分数区间查询,支持升序/降序
intstudent_get_by_score(rt_list_t*h,int low,int high,enumorder_type order);
 

Shell 命令调试

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

msh /> create_student_tbl          # 创建数据库和表
Database path: /data/stu_info.db

msh /> stu add100# 批量插入 100 条记录
Insert 100 record(s): 125ms, speed: 1ms/record

msh /> stu                         # 查询全部
id:1     name:Student1234   score:87
id:2     name:Student5678   score:92
...
record(s)100

msh /> stu score 60100 -d         # 查询 60-100 分,降序排列
id:88    name:Student9999   score:99
id:42    name:Student1234   score:95
...

msh /> stu update 1 Alice 100# 更新记录
update record success!

msh /> stu del 1# 删除指定记录
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 命令

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分