nuttx 运行 sqlite

絮絮叨叨:

上一次写 NuttX 的文章还是在 2021 年,那时我接触 NuttX 的只有几个月,当时只写了一个很简单的如何编译一个 “hello world” 的代码。

现在参与 NuttX 开发已经快有三年的时间了,这段时间里对 NuttX 的了解程度已有很大不同,自己基于 NuttX 也做了一些项目,于是乎这个系列好像可以再更新一下了。

NuttX 简介

NuttX 是一个具有丰富功能的、能够运行在小型微控制器上的实时操作系统(RTOS)。它有完整的多线程/多进程、文件系统和TCP/IP 协议栈的支持,并且实现了所有常用 POSIX API, 能够极大程度的和 Linux 环境开发保持一致。

下面这篇文章将会体现这种接口兼容性的优点,我们将会在 NuttX 上移植一个完成 SQLite 数据库,并且在 ESP32 上运行和 Linux 一样的 SQLite3 命令行工具。

快速入门

开发环境优先推荐使用 Ubuntu 20.04 及以上系统进行开发,其次可以使用 wsl2 进行开发。

下载代码

这个仓库使用 NuttX release-12.0 代码,并将常用仓库以 git submodules 方式集成,避免每次重新编译需要下载。

git clone --recursive git@github.com:Gary-Hobson/NXOS.git

下载代码后需要安装一些基础的依赖库才能进行编译。

安装依赖

sudo apt update
sudo apt install -y \
curl bison flex gettext texinfo libncurses5-dev libncursesw5-dev xxd \
gperf automake libtool pkg-config build-essential genromfs libx11-dev\
libgmp-dev libmpc-dev libmpfr-dev libisl-dev binutils-dev libelf-dev \
libexpat-dev gcc-multilib g++-multilib picocom u-boot-tools util-Linux \
kconfig-frontends gcc-arm-none-eabi binutils-arm-none-eabi zlib1g-dev
pip install pyelftools cxxfilt

编译代码

NuttX 有个很强大的功能就是它有一个完成的模拟器支持,如果代码和硬件无关,在模拟器运行成功后,无需任何修改可以运行在硬件上。

我们首先在 simulator 上编译一个简单的 demo:

./nx.sh boards/sim/configs/hello V=1

这个将会编译 boards/sim/configs/hello 这个 sim 的配置。 这个配置开启了 sim 环境中包括网络在内的常用功能,大部分与硬件无关的代码可以使用这个配置进行编译运行。

运行代码

在编译完成后,在 NuttX 目录下会出现一个 NuttX 的可执行文件,

./nuttx/nuttx

NSH 使用 在 NuttX 启动后会出现一个 Shell,它支持基本的命令,nsh 中输入 help 查看支持的所有命令。

help: 查看支持的所有命令行命令,其中 Builtin Apps 为可执行的 task,例如 hello 为一个独立的 task 可以运行,可以理解为一个进程。
ps: nsh 内置命令,查看正在运行的线程信息
ls: nsh 内置命令,查看文件系统中的文件
demo: 启动 demo 进程,运行后会执行 projects/demo/main.c 的代码
poweroff: 退出系统

运行后结果:

NuttShell (NSH)
nsh>
nsh> help
help usage:  help [-v] []
    .          break      dmesg      hexdump    mkfifo     readlink   true
    [          cat        echo       insmod     mkrd       rm         truncate
    ?          cd         env        kill       mount      rmdir      uname
    alias      cp         exec       losetup    mv         rmmod      umount
    unalias    cmp        exit       ln         nslookup   set        unset
    arp        dirname    false      ls         poweroff   sleep      uptime
    base64dec  date       free       lsmod      printf     source     usleep
    base64enc  dd         memdump    md5        ps         test       wget
    basename   df         help       mkdir      pwd        time       xd
Builtin Apps:
    demo        hello       ping        sh
    dumpstack   nsh         setlogmask  telnetd
nsh>
nsh> ping www.baidu.com
PING 110.242.68.4 56 bytes of data
56 bytes from 110.242.68.4: icmp_seq=0 time=19.9 ms
56 bytes from 110.242.68.4: icmp_seq=1 time=11.3 ms
56 bytes from 110.242.68.4: icmp_seq=2 time=22.7 ms
3 packets transmitted, 3 received, 0% packet loss, time 2322 ms
rtt min/avg/max/mdev = 11.300/17.966/22.700/4.853 ms
nsh>
nsh> ls
/:
 bin/
 data/
 dev/
 etc/
 proc/
 tmp/
nsh>
nsh> demo
Hello, World!!
nsh>
nsh> poweroff

在上面的运行示例中执行一个 demo 的进程(伪进程,没有独立地址空间),并打印了一个 hello world。

工程结构

这个代码位置在 projects/demo/main.c 里面,可以修改里面代码执行不同的任务。
其工程目录结构如下:

projects/demo
├── Kconfig
├── main.c
├── Make.defs
└── Makefile

Make.def 和 Makefile 为编译系统相关文件, PROGNAME 为进程名显示中 nsh 中的名字,PRIORITY
为任务优先级(0为最低优先级,255为最高优先级,默认为100), STACKSIZE 为进程栈大小,通过 Kconfig 的配置项决定。

cat project/demo/main.c
...
#include <stdio.h>
int main(int argc, char *argv[])
{
  printf("Hello, World!!\n");
  return 0;
}

# Demo porject
...
PROGNAME  = demo
PRIORITY  = 100
STACKSIZE = CONFIG_PROJECT_DEMO_STACKSIZE
MAINSRC = main.c

移植 SQLite3

SQLite 是一个轻量级的关系型数据库,它将数据以单个文件的方式存在与文件系统中。
我们这次将示例如何将 SQLite 移植到 NuttX 中运行。

SQLite 对跨平台做了良好的支持,官方提供了一个移植指南 https://www.SQLite.org/custombuild.html ,移植 SQLite 只需要实现内存、文件系统和锁相关的支持即可。

但使用 NuttX 这些工作都可以不需要了,因为它的接口和 Linux 完全一致,不需要做任何修改即可使用。

下载 SQLite 源码

SQLite 默认代码有 100多个文件,我们使用 SQLite 官方提供的合并后的文件https://www.SQLite.org/getthecode.html ,它只有四个文件,将所有代码都合并到一起了(合并之后一个文件20多万行,编译死慢死慢…)

SQLite(master) ✗: tree -h
[4.0K]  .
├── [862K]  shell.c
├── [8.4M]  SQLite3.c
├── [ 37K]  SQLite3ext.h
└── [614K]  SQLite3.h

然后我们参考 demo 的项目结构,将它添加到编译系统中:

├── Kconfig
├── Make.defs
├── Makefile
├── SQLite
├── SQLite_cfg.h

cat Makefile

CSRCS += SQLite/SQLite3.c
CFLAGS += -I${LIBRARIESDIR}/SQLite
CFLAGS += -D_HAVE_SQLite_CONFIG_H

ifeq ($(CONFIG_TOOLS_SQLite), y)
PROGNAME  = SQLite3
PRIORITY  = 100
STACKSIZE = ${CONFIG_TOOLS_SQLite_STACKSIZE}
MAINSRC = SQLite/shell.c
endif

SQLite 提供了一个命令行文件和一个库文件,我们将这两个分别添加到编译系统中,然后即可编译。
SQLite_cfg.h 是 SQLite 的配置文件,可以对他进行修改以进行裁剪,选择所需要的功能, 它的可选配置可以参考Compile-time Options.

运行 SQLite

同样的我们先在 sim 中运行, 先基于 board/sim/configs/hello 的配置开启下面两个配置,然后编译运行。

CONFIG_LIB_SQLITE=y
CONFIG_TOOLS_SQLITE=y

然后我们就可以在 NuttX 中和 Linux 一样的运行 SQLite 命令行工具了。
启动之后和之前一样会进入 NSH Shell,然后我们输入 sqlite3 就可以进入 SQLite 的命令行工具了。

NuttShell (NSH)
nsh>
nsh> uname -a
NuttX 0.0.0 6cdde97822 Oct  5 2023 17:11:53 xtensa ESP32-devkitc
nsh>
nsh> cd data
nsh> help
Builtin Apps:
    demo        hello       ping        sh          telnetd
    dumpstack   nsh         setlogmask  sqlite3
nsh>

我们先进入 /data 目录,以便后面的操作可以持久化到文件系统中。
在 sqlite3 的命令行中输入 .help 可以查看所有支持的命令。

使用 .open 命令打开一个数据库文件,如果文件不存在则会创建一个新的数据库文件, 然后退出 sqlite3 命令行工具,在 /data 目录下会出现一个 test.db 的文件

nsh> sqlite3
SQLite version 3.43.1 2023-09-11 12:01:27
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> .open test.db
sqlite>.quit
nsh> ls
/data:
 .
 ..
 test.db

我们再次进入 sqlite3 命令行工具,并创建一个表,使用 .tables 命令查看所有的表,可以看到我们刚才创建的 COMPANY 表。

sqlite>  CREATE TABLE COMPANY(
(x1...>    ID INT PRIMARY KEY     NOT NULL,
(x1...>    NAME           TEXT    NOT NULL,
(x1...>    AGE            INT     NOT NULL,
(x1...>    ADDRESS        CHAR(50),
(x1...>    SALARY         REAL
(x1...> );
sqlite>
sqlite> .tables
COMPANY
sqlite>

接着我们可以插入一些数据,然后使用 SELECT 命令查看数据。

sqlite> INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY)
   ...> VALUES (1, 'Paul', 32, 'California', 20000.00 );
sqlite>
INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY)
VALUES (2, 'Alsqlite>    ...> len', 25, 'Texas', 15000.00 );
sqlite>
sqlite>
sqlite> .header on
sqlite> .mode column
sqlite> SELECT * FROM COMPANY;
ID  NAME   AGE  ADDRESS     SALARY
--  -----  ---  ----------  -------
1   Paul   32   California  20000.0
2   Allen  25   Texas       15000.0

以上 sqlite3 的命令行工具的使用和 Linux 上的使用完全一致,甚至你可以将 linux 的 .db 文件拷贝到 NuttX 中使用。

ESP32 运行 SQLite

安装编译环境

pip install esptool

mkdir -p ~/.toolchain;cd ~/.toolchain
wget https://github.com/espressif/crosstool-NG/releases/download/esp-12.2.0_20230208/xtensa-esp32-elf-12.2.0_20230208-x86_64-Linux-gnu.tar.xz
tar -xf ./xtensa-esp32-elf-12.2.0_20230208-x86_64-Linux-gnu.tar.xz
rm xtensa-esp32-elf-12.2.0_20230208-x86_64-Linux-gnu.tar.xz
echo "export PATH=\"\$HOME/.toolchain/xtensa-esp32-elf/bin:\$PATH\"" >> ~/.profile
source ~/.profile

这里我使用的 ESP32 开发版是 ESP-WROOM-32,它有 4MB (32Mb) 的 SPI Flash和520KB SRAM。

编译下载

在这个配置中将将 1M 的 FALSH 作为了 代码空间,1M 的 FLASH 作为文件系统(Littlefs)。挂载到 /data 目录

# 编译 SQLite 配置
./nx.sh boards/esp32/configs/sqlite

使用上面的编译命令编译后会在 NuttX 目录下生成一个 NuttX.bin 文件, 然后我们需要将它烧录到设备中. 下面的的 /dev/ttyUSB0 需要根据实际情况填写。

# ubuntu,下载固件到设备
esptool.py -c esp32 -p /dev/ttyUSB0 -b 921600  write_flash -fs detect -fm dio -ff 40m 0x1000 boards/esp32/boot/bootloader-esp32.bin 0x8000 boards/esp32/boot/partition-table-esp32.bin 0x10000 nuttx/nuttx.bin

烧录后打开串口终端,和上面执行相同的步骤可以体验在 ESP32 上使用 SQLite 的命令行工具.(上面实际就是 ESP32 上运行的结果)

在代码中使用

在 NuttX 中使用 SQLite 也和 Linux 一样,只需要在代码中包含 sqlite3.h 头文件即可。
例如我们在 demo 项目中添加一个 sqlite3 的 demo,代码如下:

#include <sqlite3.h>
#include <stdio.h>

int callback(void *NotUsed, int argc, char **argv,
                    char **azColName) {
    NotUsed = 0;
    for (int i = 0; i < argc; i++) {
        printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL");
    }
    printf("\n");
    return 0;
}

int main(void) {
    sqlite3 *db;
    char *err_msg = 0;
    int rc = sqlite3_open("test.db", &db);
    if (rc != SQLITE_OK) {
        fprintf(stderr, "Cannot open database: %s\n",
                sqlite3_errmsg(db));
        sqlite3_close(db);
        return 1;
    }
    char *sql = "SELECT * FROM Cars";
    rc = sqlite3_exec(db, sql, callback, 0, &err_msg);
    if (rc != SQLITE_OK ) {
        fprintf(stderr, "Failed to select data\n");
        fprintf(stderr, "SQL error: %s\n", err_msg);
        sqlite3_free(err_msg);
        sqlite3_close(db);
        return 1;
    }
    sqlite3_close(db);
    return 0;
}

在上面的代码中我们使用了一个 callback 函数,这个函数是在执行 SELECT 命令后的回调函数,它会将查询的结果打印出来。

运行结果如下:

nsh> cd data
nsh> demo
Id = 1
Name = Audi
Price = 52642

Id = 2
Name = Mercedes
Price = 57127

Id = 3
Name = Skoda
Price = 9000
...

资源占用

在 ESP32 编译后比较开启 SQLite 前后的资源占用,SQLite 占用大概 600K 的 FLASH 和 70K 的 RAM
如果对 sqlite_cfg.h 进行配置后,可以进一步裁剪降低 FLASH 的占用。

NXOS(master) ✗: size hello.elf
   text    data     bss     dec     hex filename
 110672     532    9632  120836   1d804 hello.elf
NXOS(master) ✗: size sqlite.elf
   text    data     bss     dec     hex filename
 671310    7664   10680  689654   a85f6 sqlite.elf

后记

前段时间有人在公众号的后台问我,有没有关于 NuttX 的视频,我在网上搜了一下关于 NuttX 使用视频实在少得可怜。

而我在 21 年的立的 flag 中就有关于发布一个视频,这次国庆假期总算有机会可以拔了这个 flag 了。

视频的话明天会发出,如果对于这个文章有任何问题可以点击阅读原文到 github 进行评论交流(公众号注册太迟,没有留言功能😢)


nuttx 运行 sqlite
https://gary-hobson.github.io/2023/10/14/nuttx运行sqlite/
作者
非典型技术宅
发布于
2023年10月14日
许可协议