当前位置: 首页 > news >正文

ESP32开发——基于idf框架使用NVS操作存储设备读写

文章目录

  • 一、非易失性存储库简介
    • 1.1 底层存储
    • 1.2 键值对
    • 1.3 命名空间
  • 二、常用API函数
  • 三、代码示例
    • 3.1 nvs操作流程
    • 3.2 使用nvs读写value操作
    • 3.2 读写多个键值对
    • 3.3 使用nvs读写 BLOB 二进制大对象数据
    • 3.4 擦除数据
  • 四、自定义nvs存储分区
    • 4.1 自定义partions分区信息
    • 4.2 程序调用

一、非易失性存储库简介

非易失性存储 (NVS) 库主要用于在 flash 中存储键值格式的数据。

在这里插入图片描述

1.1 底层存储

NVS 库通过调用 esp_partition API 使用主 flash 的部分空间,即类型为 data 且子类型为 nvs 的所有分区。应用程序可调用 nvs_open() API 选择使用带有 nvs 标签的分区,也可以通过调用 nvs_open_from_partition() API 选择使用指定名称的任意分区。

如果 NVS 分区被截断(例如,更改分区表布局时),则应擦除分区内容。可以使用 ESP-IDF 构建系统中的 idf.py erase_flash 命令擦除 flash 上的所有内容。

NVS 最适合存储一些较小的数据,而非字符串或二进制大对象 (BLOB) 等较大的数据。如需存储较大的 BLOB 或者字符串,请考虑使用基于磨损均衡库的 FAT 文件系统。

1.2 键值对

NVS 的操作对象为键值对,其中键是 ASCII 字符串,当前支持的最大键长为 15 个字符。值可以为以下几种类型:

  • 整数型:uint8_t、int8_t、uint16_t、int16_t、uint32_t、int32_t、uint64_t 和 int64_t;
  • 以 0 结尾的字符串;
  • 可变长度的二进制数据 (BLOB)

键必须唯一。为现有的键写入新的值可能产生如下结果:

  • 如果新旧值数据类型相同,则更新值;
  • 如果新旧值数据类型不同,则返回错误。

读取值时也会执行数据类型检查。如果读取操作的数据类型与该值的数据类型不匹配,则返回错误。

1.3 命名空间

为了减少不同组件之间键名的潜在冲突,NVS 将每个键值对分配给一个命名空间。命名空间的命名规则遵循键名的命名规则,例如,最多可占 15 个字符。命名空间的名称在调用 nvs_open()nvs_open_from_partition 中指定,调用后将返回一个不透明句柄,用于后续调用 nvs_get_*nvs_set_*nvs_commit 函数。这样,一个句柄关联一个命名空间,键名便不会与其他命名空间中相同键名冲突。

请注意,不同 NVS 分区中具有相同名称的命名空间将被视为不同的命名空间。

二、常用API函数

参考文件: esp-idf\components\nvs_flash\include\nvs_flash.h

  • 初始化默认NVS分区
esp_err_t nvs_flash_init(void);
  • 初始化自定义NVS分区
esp_err_t nvs_flash_init_partition(const char *partition_label);
  • 从默认 NVS 分区打开指定命名空间的非易失性存储
esp_err_t nvs_open(const char* name, nvs_open_mode_t open_mode, nvs_handle_t *out_handle);
  • 从指定分区而非默认分区打开 NVS 的命名空间
esp_err_t nvs_open_from_partition(const char *part_name, const char *name, nvs_open_mode_topen_mode, nvs_handle_t *out_handle);
  • 擦除默认NVS分区
esp_err_t nvs_flash_erase(void);
  • 擦除自定义NVS分区
esp_err_t nvs_flash_erase_partition(const char *part_name);
  • 设置 int 32 类型值到键值内
esp_err_t nvs_set_i32(nvs_handle_thandle, const char *key, int32_t value);
  • 设置 uint32_t 数据类型的值到键值内
esp_err_t nvs_set_u32(nvs_handle_thandle, const char *key, uint32_t value);
  • 设置字符串值到键值内
esp_err_t nvs_set_str(nvs_handle_thandle, const char *key, const char *value);
  • 从键值内读取 int_32 类型值
esp_err_t nvs_get_i32(nvs_handle_thandle, const char *key, int32_t *out_value);
  • 从键值内读取 uint_32 类型值
esp_err_t nvs_get_u32(nvs_handle_thandle, const char *key, uint32_t *out_value);
  • 从键值内读取字符串值
esp_err_t nvs_get_str(nvs_handle_thandle, const char *key, char *out_value, size_t *length);
  • 从键值内读取二进制大对象的值
esp_err_t nvs_get_blob(nvs_handle_thandle, const char *key, void *out_value, size_t *length);
  • 将要修改的数据写入到nvs内
esp_err_t nvs_commit(nvs_handle_t handle);
  • 擦除特定键名的键值
esp_err_t nvs_erase_key(nvs_handle_t handle, const char* key);
  • 擦除命名空间内所有的键对值
esp_err_t nvs_erase_all(nvs_handle_thandle);
  • 关闭nvs存储句柄并释放所有分配的资源
void nvs_close(nvs_handle_thandle);

三、代码示例

3.1 nvs操作流程

  1. 初始化nvs flash
  2. 打开一个命名空间
  3. 读取或者写入数据
  4. 使用nvs_commit确保修改完成
  5. 关闭nvs flash

拷贝示例模板

book@100ask:~/esp$ cp esp-idf/examples/storage/nvs_rw_value/ -rfd .

在这里插入图片描述

3.2 使用nvs读写value操作

写入 nvs 命名空间为 store 的 int32 类型键对值 键名为val 键值为0 每次重启加一。

编辑nvs_value_example_main.c:

book@100ask:~/esp/nvs_rw_value$ gedit main/nvs_value_example_main.c#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"#define TAG "NVS"void app_main()
{// 延时1秒,确保系统稳定启动vTaskDelay(1000 / portTICK_PERIOD_MS);// 初始化NVS闪存分区(默认分区名为"nvs")// 若分区已满或版本不兼容,需先擦除再初始化ESP_ERROR_CHECK(nvs_flash_init());// 打开名为"store"的命名空间,使用读写模式// 若命名空间不存在,会自动创建(读写模式下)nvs_handle handle;ESP_ERROR_CHECK(nvs_open("store", NVS_READWRITE, &handle));// 读取键"val"对应的int32_t值int32_t val = 0;esp_err_t result = nvs_get_i32(handle, "val", &val);// 处理读取结果switch (result){// 键不存在的情况(两种错误码处理相同逻辑)case ESP_ERR_NVS_NOT_FOUND:case ESP_ERR_NOT_FOUND:ESP_LOGE(TAG, "Value not set yet");break;// 读取成功的情况case ESP_OK:ESP_LOGI(TAG, "Value is %d", val);break;// 其他错误(如句柄无效、内存不足等)default:ESP_LOGE(TAG, "Error (%s) opening NVS handle!\n", esp_err_to_name(result));break;}// 将值递增1(无论之前是否存在,val已有初始值0)val++;// 将递增后的值写入NVSESP_ERROR_CHECK(nvs_set_i32(handle, "val", val));// 提交更改到闪存(确保数据持久化)ESP_ERROR_CHECK(nvs_commit(handle));// 关闭NVS句柄,释放资源nvs_close(handle);
}

设置环境变量:

get_idf

设置芯片为 esp32c3:

idf.py set-target esp32c3

编译命令:

idf.py build

烧写:

idf.py -p /dev/ttyUSB0 flash

监视运行:

idf.py -p /dev/ttyUSB0 monitor

在这里插入图片描述
键值从0开始累加,每次重新启动监视器或者对板子进行复位都会累加 val。

3.2 读写多个键值对

读写几种数据类型的键值对:

#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"#define TAG "NVS"void nvs_key_write(void)
{nvs_handle handle;ESP_ERROR_CHECK(nvs_open("store", NVS_READWRITE, &handle));// 定义键名static const char *DATA = "key value1";     // 用于存储int32_tstatic const char *DATA1 = "key value2";    // 用于存储字符串static const char *DATA2 = "key value3";    // 用于存储结构体(Blob)// WiFi配置结构体初始化wifi_config_t wifi_config_sta = {.sta = {.ssid = "LjunG",.password = "666888",},};// 写入整数类型int32_t val = 123;ESP_ERROR_CHECK(nvs_set_i32(handle, DATA, val));// 写入字符串类型ESP_ERROR_CHECK(nvs_set_str(handle, DATA1, "i am LjunG"));// 写入二进制大对象(Blob) - WiFi配置结构体ESP_ERROR_CHECK(nvs_set_blob(handle, DATA2, &wifi_config_sta, sizeof(wifi_config_sta)));// 提交更改到闪存ESP_ERROR_CHECK(nvs_commit(handle));// 关闭句柄nvs_close(handle);
}void nvs_key_read(void)
{nvs_handle handle;ESP_ERROR_CHECK(nvs_open("store", NVS_READWRITE, &handle));// 定义键名(需与写入时一致)static const char *DATA = "key value1";static const char *DATA1 = "key value2";static const char *DATA2 = "key value3";// 读取整数类型int32_t val = 0;ESP_ERROR_CHECK(nvs_get_i32(handle, DATA, &val));// 读取字符串类型// 注意: str_len既是输入参数(缓冲区大小),也是输出参数(实际字符串长度)uint32_t str_len = 32;  char str_data[32] = {0};ESP_ERROR_CHECK(nvs_get_str(handle, DATA1, str_data, &str_len));// 读取二进制大对象(Blob)wifi_config_t wifi_config_sta;memset(&wifi_config_sta, 0x0, sizeof(wifi_config_sta));uint32_t wifi_len = sizeof(wifi_config_sta);ESP_ERROR_CHECK(nvs_get_blob(handle, DATA2, &wifi_config_sta, &wifi_len));// 输出读取结果printf("DATA value is: %d  \r\n", val);printf("DATA1 string is: %s len%u \r\n", str_data, str_len);printf("DATA2 wifi is:%s passwd:%s\r\n", wifi_config_sta.sta.ssid, wifi_config_sta.sta.password);// 关闭句柄nvs_close(handle);
}void app_main()
{// 延时1秒,确保系统稳定启动vTaskDelay(1000 / portTICK_PERIOD_MS);// 初始化NVS闪存ESP_ERROR_CHECK(nvs_flash_init());// 执行写入和读取操作nvs_key_write();nvs_key_read();
}

设置环境变量:

get_idf

设置芯片为 esp32c3:

idf.py set-target esp32c3

编译命令:

idf.py build

烧写:

idf.py -p /dev/ttyUSB0 flash

监视运行:

idf.py -p /dev/ttyUSB0 monitor

在这里插入图片描述

3.3 使用nvs读写 BLOB 二进制大对象数据

#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"#define TAG "NVS_BLOB_EXAMPLE"
#define STORAGE_NAMESPACE "blob_storage"  // 命名空间
#define BLOB_KEY "device_config"          // Blob数据的键名// 定义一个示例结构体(作为Blob存储的数据)
typedef struct {uint8_t device_id;          // 设备IDchar device_name[32];       // 设备名称float temperature;          // 温度数据bool is_active;             // 设备激活状态uint16_t version;           // 配置版本号
} DeviceConfig;/*** 向NVS写入Blob类型数据(结构体)*/
void nvs_blob_write() {nvs_handle_t nvs_handle;esp_err_t err;// 1. 打开命名空间(读写模式)err = nvs_open(STORAGE_NAMESPACE, NVS_READWRITE, &nvs_handle);if (err != ESP_OK) {ESP_LOGE(TAG, "打开命名空间失败: %s", esp_err_to_name(err));return;}// 2. 准备要存储的结构体数据DeviceConfig config = {.device_id = 0x01,.temperature = 25.6f,.is_active = true,.version = 0x0102,};strncpy(config.device_name, "SensorNode-001", sizeof(config.device_name)-1);// 3. 写入Blob数据err = nvs_set_blob(nvs_handle, BLOB_KEY, &config, sizeof(config));if (err != ESP_OK) {ESP_LOGE(TAG, "写入Blob失败: %s", esp_err_to_name(err));nvs_close(nvs_handle);return;}// 4. 提交更改(确保数据写入闪存)err = nvs_commit(nvs_handle);if (err != ESP_OK) {ESP_LOGE(TAG, "提交Blob失败: %s", esp_err_to_name(err));nvs_close(nvs_handle);return;}ESP_LOGI(TAG, "Blob数据写入成功,大小: %d 字节", sizeof(config));nvs_close(nvs_handle);  // 关闭句柄
}/*** 从NVS读取Blob类型数据(结构体)*/
void nvs_blob_read() {nvs_handle_t nvs_handle;esp_err_t err;// 1. 打开命名空间(只读模式即可,这里用读写模式仅作示例)err = nvs_open(STORAGE_NAMESPACE, NVS_READWRITE, &nvs_handle);if (err != ESP_OK) {ESP_LOGE(TAG, "打开命名空间失败: %s", esp_err_to_name(err));return;}// 2. 先获取Blob数据的大小(输入NULL作为缓冲区)size_t blob_size = 0;err = nvs_get_blob(nvs_handle, BLOB_KEY, NULL, &blob_size);if (err != ESP_OK) {ESP_LOGE(TAG, "获取Blob大小失败: %s", esp_err_to_name(err));nvs_close(nvs_handle);return;}ESP_LOGI(TAG, "读取到的Blob大小: %d 字节", blob_size);// 3. 分配缓冲区并读取Blob数据DeviceConfig read_config;// 检查数据大小是否匹配(防止数据损坏或版本不兼容)if (blob_size != sizeof(read_config)) {ESP_LOGE(TAG, "Blob大小不匹配!预期: %d, 实际: %d", sizeof(read_config), blob_size);nvs_close(nvs_handle);return;}err = nvs_get_blob(nvs_handle, BLOB_KEY, &read_config, &blob_size);if (err != ESP_OK) {ESP_LOGE(TAG, "读取Blob失败: %s", esp_err_to_name(err));nvs_close(nvs_handle);return;}// 4. 打印读取到的结构体数据ESP_LOGI(TAG, "Blob数据读取成功:");printf("  设备ID: 0x%02X\n", read_config.device_id);printf("  设备名称: %s\n", read_config.device_name);printf("  温度: %.1f°C\n", read_config.temperature);printf("  激活状态: %s\n", read_config.is_active ? "已激活" : "未激活");printf("  版本号: 0x%04X\n", read_config.version);nvs_close(nvs_handle);  // 关闭句柄
}void app_main() {// 初始化NVS(必须在所有NVS操作前执行)esp_err_t err = nvs_flash_init();if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {// 若分区满或版本不兼容,擦除后重新初始化ESP_ERROR_CHECK(nvs_flash_erase());err = nvs_flash_init();}ESP_ERROR_CHECK(err);// 执行Blob写入和读取操作nvs_blob_write();nvs_blob_read();
}

设置环境变量:

get_idf

设置芯片为 esp32c3:

idf.py set-target esp32c3

编译命令:

idf.py build

烧写:

idf.py -p /dev/ttyUSB0 flash

监视运行:

idf.py -p /dev/ttyUSB0 monitor

在这里插入图片描述

3.4 擦除数据

写入数据值之后进行擦除操作:

#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"#define TAG "NVS"
// 存储在NVS中的数据键名(用于标识要操作的数据)
static const char *DATA = "val";void erase(void)
{nvs_handle handle;  // NVS操作句柄,用于后续NVS操作// 延迟1秒(等待前序操作完成,单位为系统时钟周期,portTICK_PERIOD_MS为1ms)vTaskDelay(1000 / portTICK_PERIOD_MS);  // 打开NVS命名空间"store",以读写模式(NVS_READWRITE)操作// 参数:命名空间名称、操作模式、输出句柄指针ESP_ERROR_CHECK(nvs_open("store", NVS_READWRITE, &handle));// 擦除指定键(DATA即"val")的数据ESP_ERROR_CHECK(nvs_erase_key(handle, DATA));// 注释:如需擦除命名空间中所有数据,可使用以下函数// ESP_ERROR_CHECK(nvs_erase_all(handle));// 提交更改(将内存中的操作同步到闪存,NVS操作需显式提交才会生效)ESP_ERROR_CHECK(nvs_commit(handle));// 关闭NVS句柄(释放资源,避免内存泄漏)nvs_close(handle);// 输出日志提示,即将重启ESP_LOGI(TAG, "Set Restart now.\n");// 重启ESP32系统(测试数据持久性的常用方式)esp_restart();
}void write_data(void)
{nvs_handle handle;  // NVS操作句柄int32_t out_val = 0;  // 存储读取到的整数数据(32位,适配nvs_get_i32)// 打开命名空间"store",读写模式ESP_ERROR_CHECK(nvs_open("store", NVS_READWRITE, &handle));// 读取"val"键对应的整数数据到out_valesp_err_t result = nvs_get_i32(handle, DATA, &out_val);// 根据读取结果进行日志输出switch (result){case ESP_ERR_NVS_NOT_FOUND:  // 键不存在(首次运行时触发)case ESP_ERR_NOT_FOUND:      // 未找到数据(与上一条等价,兼容不同版本)ESP_LOGE(TAG, "Value not set yet");  // 提示值尚未设置break;case ESP_OK:  // 读取成功ESP_LOGI(TAG, "Value is %d", out_val);  // 输出当前值break;default:  // 其他错误ESP_LOGE(TAG, "Error (%s) opening NVS handle!\n", esp_err_to_name(result));break;}// 将读取到的值自增1(首次运行时从0变为1)out_val++;// 将自增后的值写入"val"键ESP_ERROR_CHECK(nvs_set_i32(handle, DATA, out_val));// 提交更改(同步到闪存)ESP_ERROR_CHECK(nvs_commit(handle));// 关闭句柄nvs_close(handle);
}void app_main()
{// 延迟1秒(等待系统初始化完成,避免启动阶段资源冲突)vTaskDelay(1000 / portTICK_PERIOD_MS);// 初始化NVS闪存(必须在所有NVS操作前执行,否则会报错)ESP_ERROR_CHECK(nvs_flash_init());// 执行数据写入(自增)操作write_data();// 执行数据擦除并重启erase();
}

设置环境变量:

get_idf

设置芯片为 esp32c3:

idf.py set-target esp32c3

编译命令:

idf.py build

烧写:

idf.py -p /dev/ttyUSB0 flash

监视运行:

idf.py -p /dev/ttyUSB0 monitor

在这里插入图片描述
在这里插入图片描述
板子复位后在val中写入1,自动重启后将其擦除掉。

四、自定义nvs存储分区

NVS 分区生成程序根据 CSV 文件中的键值对生成二进制文件。该二进制文件与 非易失性存储器 (NVS) 中定义的 NVS 结构兼容。NVS 分区生成程序适合用于生成二进制数据(Blob),其中包括设备生产时可从外部烧录的ODM/OEM 数据。这也使得生产制造商在使用同一个固件的基础上,通过自定义参数,如序列号等,为每个设备生成不同配置。

4.1 自定义partions分区信息

工程目录下增加 partitions.csv 文件加入自定义分区信息:

# Name,   Type, SubType, Offset,  Size, Flags
# Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild
nvs,      data, nvs,     ,        0x6000,
phy_init, data, phy,     ,        0x1000,
factory,  app,  factory, ,        1M,
MyNvs,    data, nvs,     ,        1M,

在这里插入图片描述

使用 idf.py menuconfig 来配置分区表:
在这里插入图片描述
之后我们设置一下板载SPI FLASH的大小:
在这里插入图片描述

4.2 程序调用

#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"// 日志标签,用于标识当前模块的日志输出
#define TAG "NVS"void app_main()
{// 延迟1秒(等待系统初始化完成,避免启动阶段资源冲突)vTaskDelay(1000 / portTICK_PERIOD_MS);// 初始化指定名称的NVS分区("MyNvs")// 注:需在partition.csv中预先定义该分区,否则会初始化失败ESP_ERROR_CHECK(nvs_flash_init_partition("MyNvs"));nvs_handle handle;  // NVS操作句柄// 从指定NVS分区("MyNvs")打开命名空间("store"),读写模式// 参数:分区名、命名空间、操作模式、句柄指针ESP_ERROR_CHECK(nvs_open_from_partition("MyNvs",       // 自定义NVS分区名称"store",       // 命名空间NVS_READWRITE, // 读写模式&handle        // 输出句柄));nvs_stats_t nvsStats;  // 用于存储NVS分区状态信息的结构体// 获取指定NVS分区("MyNvs")的状态信息(已用/空闲条目数等)nvs_get_stats("MyNvs", &nvsStats);// 打印NVS分区状态信息ESP_LOGI(TAG, "used: %d, free: %d, total: %d, namespace count: %d", nvsStats.used_entries,    // 已使用的条目数nvsStats.free_entries,    // 空闲的条目数nvsStats.total_entries,   // 总条目数(已用+空闲)nvsStats.namespace_count  // 已创建的命名空间数量);int32_t val = 0;  // 用于存储读取到的整数数据// 读取命名空间中"val"键对应的32位整数esp_err_t result = nvs_get_i32(handle, "val", &val);// 根据读取结果进行日志输出switch (result){case ESP_ERR_NVS_NOT_FOUND:  // 键不存在(首次运行时触发)case ESP_ERR_NOT_FOUND:      // 未找到数据(兼容不同版本的错误码)ESP_LOGE(TAG, "Value not set yet");  // 提示值尚未设置break;case ESP_OK:  // 读取成功ESP_LOGI(TAG, "Value is %d", val);  // 输出当前值break;default:  // 其他错误ESP_LOGE(TAG, "Error (%s) reading value!\n", esp_err_to_name(result));break;}val++;  // 将值自增1(首次运行时从0→1)// 将自增后的值写入"val"键ESP_ERROR_CHECK(nvs_set_i32(handle, "val", val));// 提交更改(将操作同步到闪存,必须调用才会生效)ESP_ERROR_CHECK(nvs_commit(handle));// 关闭NVS句柄(释放资源)nvs_close(handle);
}

设置环境变量:

get_idf

设置芯片为 esp32c3:

idf.py set-target esp32c3

编译命令:

idf.py build

烧写:

idf.py -p /dev/ttyUSB0 flash

监视运行:

idf.py -p /dev/ttyUSB0 monitor

在这里插入图片描述
每次重启板子或者重新启动监视器val的值会累加。

http://www.dtcms.com/a/290687.html

相关文章:

  • 家庭服务具身智能机器人体系架构
  • 一次 POI 版本升级踩坑记录
  • lesson20:Python函数的标注
  • docker nginx 部署前端踩坑记录
  • WinUI3开发_Frame用法
  • MYSQL:数据库约束
  • 【PTA数据结构 | C语言版】拓扑排序
  • 通信刚需小能手,devicenet转PROFINET网关兼容物流分拣自动化
  • 自动化计算机经过加固后有什么好处?
  • OpenAI API(2) OpenAI Responses API使用
  • 设备管理系统(MMS)如何在工厂MOM功能设计和系统落地
  • 深入解析 Linux 硬链接与软链接:原理、区别及应用场景
  • 龙虎榜——20250721
  • Linux中ELF区域与文件偏移量的关系
  • 【AI论文】EXAONE 4.0:融合非推理模式与推理模式的统一大语言模型
  • Neovim 安装与解压 tar.gz 文件
  • AXI接口学习
  • Python 模块未找到?这样解决“ModuleNotFoundError”
  • Dev C++下载安装和使用教程(图文并茂,保姆级教程)
  • dolphinscheduler中sqoop无法执行
  • 机器人工程专业本科阶段的学习分析(腾讯元宝)
  • Real-World Blur Dataset for Learning and Benchmarking Deblurring Algorithms
  • 系统分析师-计算机系统-操作系统-存储器管理设备管理
  • Oracle From查看弹性域设置
  • (3)Oracle基本语法与常用函数
  • Oracle自治事务——从问题到实践的深度解析
  • 基于MySQL实现分布式调度系统的选举算法
  • CLIP与SIGLIP对比浅析
  • RuoYi配置多数据源失效
  • vscode 使用说明二