尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Zephyr开发实战:用Shell和Settings实现设备用户管理
zephyr 这个系列总算写到第2篇了。第1篇我们把基础工程跑通能看见串口打印一个 hello这一篇要加点真正能用的东西——在设备上做一个“添加用户”的应用程序。说白了就是在 Zephyr 里实现一套用户管理通过串口命令行往设备里添加用户、列出用户、删除用户而且重启之后数据不丢。这个功能听起来简单但它几乎覆盖了 Zephyr 应用层开发的几个核心环节shell 子系统、settings 持久化存储、devicetree 分区配置还有 Kconfig 裁剪。把这套流程走通后面再做设备配置管理、权限白名单、简单账户体系都是同一个套路。适合刚把 Zephyr 环境搭起来、想往实际应用走的开发者参考也适合那种“板子在手但不知道写什么项目”的情况。我用的环境是 Zephyr 3.x qemu_cortex_m3 模拟不需要真实硬件也能完整跑起来。1. 需求拆解嵌入式设备上的“用户”到底是什么先说个很多人会问的问题嵌入式设备要“用户管理”干嘛云平台上有账号体系很正常但一个跑 RTOS 的 MCU 上维护用户列表看起来有点大材小用。其实不是。我见过不少实际项目里都需要这种本地账户列表门禁控制器的白名单、工业仪表的操作员权限表、开发板的调试接口访问控制甚至是一个物联网网关上的设备所有者信息。这些场景的共同点是——设备本身要记住“谁被允许使用它”而不是每次开机都靠上位机重新下发一份名单。所以设备端需要一张轻量的、能掉电保存的用户表支持最基本的增删查。这正好是这篇应用要解决的需求。功能边界我也提前划清楚只做用户管理本体包括添加用户、按名字查找、列出全部用户、删除指定用户、清空全部用户以及持久化到片上 flash。不做登录认证、不做密码加密、不做权限策略校验那些属于下一层的业务逻辑。这一篇先把“用户数据能存得住、能查得到”这个地基打好。从第1篇延续下来的工程结构也不用改还是那个最小应用工程只新增一个 src/main.c加上 prj.conf 和 overlay 文件。Zephyr 的应用开发模型大致就是这样配置写 prj.conf硬件布局用 devicetree overlay业务逻辑放 main.c 和自定义模块。实现时我刻意把“存储细节”和“业务操作”分开用户表在内存里是普通结构体数组存储层通过 settings API 读写 flash。这样后面想换成数据库或者文件系统上层逻辑基本不用动。2. 为什么选 Shell Settings 这套组合2.1 交互方式对比实现用户管理的交互方式有好几种我实测过不同的路子。最简单的是写死代码调试时手动改数组那根本不算应用复杂的是自定义一套串口协议比如0xAA 0x01 0x00...这种帧格式适合和上位机联调但前期成本高而且调试非常痛苦。折中方案就是 Zephyr 自带的 Shell 子系统。它只需要注册几条命令参数解析、补全、提示符这些全都现成交互体验还接近 Linux 终端。我这边加一条user add alice就能完成新增操作对嵌入式应用来说已经足够专业。Shell 的代价是占一点 flash 和 RAM但对绝大多数带 shell 需求的板子来说完全可以接受。如果资源实在紧张也可以只用纯串口回调去解析字符串但那就得自己维护环形缓冲、状态机没必要。站在开发效率的角度我建议别重复造轮子。2.2 持久化方案选型用户表需要掉电保存Zephyr 下常见方案有三个底层 NVS、settings 子系统、文件系统。文件系统对这种 key-value 型数据太重还要考虑文件描述符和磨损均衡直接操作 NVS 又不方便得自己处理键值编码和读取流程。真正顺手的是 settings 子系统。它是 Zephyr 的配置管理框架底层支持 NVS 或 FCB对外只提供键值读写接口自带存储校验和磨损均衡。对我来说最大的爽点是 settings 和 Shell 一样属于官方高质量模块不需要我自己维护底层细节。代码里一行settings_set_str(users/alice, 1,1)就能把数据写进 flash重启后再用回调把数据读回来。这个抽象层级正好合适——比裸 NVS 高一层又没高到文件系统那种夸张程度。2.3 数据模型设计用户表模型我设计成固定数组 结构体#define MAX_USERS 8 #define NAME_LEN 16 #define ROLE_LEN 8 struct user { char name[NAME_LEN]; uint8_t role_id; bool active; };字段刻意保持最小用户名、角色、启用状态。MAX_USERS 设成 8一是嵌入式设备上本地账户数量本来就不多二是固定数组可以避免堆分配也方便预估内存占用。这段数组 8 个元素加起来不到 200 字节在 MCU 上非常划算。角色用一个数字表示而不是直接存字符串也是为了省空间。显示的时候再去映射成admin / operator / viewer / guest这样存储和展示解耦。active 字段表示用户是否启用可以先留着后面做锁定功能时直接就能用。2.4 这套设计的扩展性前面这些选择其实都留了扩展口。Shell 命令是一条条注册的后面加user disable、user passwd只是多写一个命令函数的问题settings 存储的键值格式是users/name后面加字段的话改 value 格式的序列化和解析就行旧数据和新代码也能兼容只要解析函数做好缺省值处理。这也是我为什么强调“先把框架搭对”一个用户管理功能并不难难的是它能不能平滑长成权限系统的一部分。3. 动手实现前的关键配置3.1 prj.conf 配置先把需要的功能模块打开。我这份 prj.conf 是跑通的完整版本CONFIG_SHELLy CONFIG_SHELL_LOG_BACKENDn CONFIG_SETTINGSy CONFIG_SETTINGS_NVSy CONFIG_SETTINGS_NVS_PARTITION_LABELstorage CONFIG_SETTINGS_SHELLy CONFIG_NVSy CONFIG_FLASHy CONFIG_FLASH_MAPy CONFIG_MAIN_STACK_SIZE2048几个关键点说一下。CONFIG_SETTINGS_NVS_PARTITION_LABEL告诉 settings 后端去哪个分区找存储区域这个 label 必须和 devicetree overlay 里定义的分区 label 一致写成storage。CONFIG_SETTINGS_SHELL可以额外提供一组 settings 调试命令启动后用settings show能直接看到 flash 里存了哪些键值排障神器建议开着。CONFIG_MAIN_STACK_SIZE我设成 2048因为 settings 加载和 shell 命令处理都会用到 main 线程栈给足一点避免莫名其妙栈溢出。3.2 devicetree 分区配置存储要放在专门的 flash 分区里不能随随便便写到一个和应用程序代码重叠的地址。qemu_cortex_m3 的 flash 一共 256 KB代码占用很小我把存储分区放在最后 16 KB也就是地址0x3c000到0x40000/ { chosen { zephyr,console uart0; }; }; flash0 { partitions { compatible fixed-partitions; #address-cells 1; #size-cells 1; storage_partition: partition3c000 { label storage; reg 0x3c000 0x4000; }; }; };分区大小不是随便拍的。我在前面算过NVS 一条记录大约要十几字节头部用户数据写最坏情况users/abcdefghijklmno 3,0也就 30 字节左右8 个用户全部存满也不到 500 字节。但 NVS 本身需要至少两个扇区做磨损均衡qemu 模拟的 flash 扇区是 4 KB两个就是 8 KB我干脆给 16 KB余量充足以后加字段也不会紧张。注意如果你用真实开发板千万不要照抄0x3c000这个地址。要先确认板子的 flash 总大小、应用分区占用情况以及有没有 bootloader 占用。否则容易把存储分区覆盖到代码段轻则数据写坏重则程序起不来。我在这上面吃过亏稍后讲排查时细说。3.3 工程目录结构整个应用结构保持 Zephyr 推荐的标准布局user_app/ ├── CMakeLists.txt ├── prj.conf ├── boards/ │ └── qemu_cortex_m3.overlay └── src/ └── main.cCMakeLists.txt 和上一篇一样不用改cmake_minimum_required(VERSION 3.20.0) find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(user_app) target_sources(app PRIVATE src/main.c)board overlay 文件名要严格对应目标板qemu_cortex_m3 板就叫qemu_cortex_m3.overlay放在boards/目录下。这样west build -b qemu_cortex_m3时会自动加载。4. 核心代码实现详解4.1 用户表与辅助函数main.c 先把基础结构写出来。我定义了角色名列表和几个操作函数#include zephyr/kernel.h #include zephyr/shell/shell.h #include zephyr/settings/settings.h #include zephyr/storage/flash_map.h #include ctype.h #include errno.h #include stdio.h #include string.h #define SETTINGS_PREFIX users #define SETTINGS_KEY_MAX 64 #define USER_ENTRY_MAX 32 static const char * const role_names[] { admin, operator, viewer, guest }; static struct user { char name[NAME_LEN]; uint8_t role_id; bool active; } user_table[MAX_USERS]; static int user_count; static const struct user *find_user(const char *name) { for (int i 0; i MAX_USERS; i) { if (user_table[i].name[0] \0) { continue; } if (strcmp(user_table[i].name, name) 0) { return user_table[i]; } } return NULL; } static struct user *find_free_slot(void) { for (int i 0; i MAX_USERS; i) { if (user_table[i].name[0] \0) { return user_table[i]; } } return NULL; }这里有个小设计空的槽位用name[0] \0来判断。添加用户时会在name里写入字符串所以全零初始化的数组天然全是“空格”。删除用户时只要把整个结构体清零就自动变成可用槽位。数组遍历顺序查找用户最多 8 个线性查找的性能完全不是问题。4.2 Settings 回调注册settings 的工作方式是这样的应用启动时调用settings_load()它从 flash 里逐个读出键值然后根据键的前缀路由到对应的处理函数。所以我要注册一个users前缀的 handler当读到users/xxx时由这个 handler 负责把数据填进内存表。static int user_settings_set(const char *key, size_t len, settings_read_cb read_cb, void *cb_arg) { if (key NULL) { return 0; } char buf[USER_ENTRY_MAX] {0}; char *comma; unsigned int role; unsigned int active; struct user *slot; if (len sizeof(buf)) { return -EINVAL; } if (read_cb(cb_arg, buf, len) 0) { return -EINVAL; } slot find_free_slot(); if (slot NULL) { return -ENOMEM; } comma strchr(buf, ,); if (comma NULL) { return -EINVAL; } *comma \0; role strtoul(buf, NULL, 10); active strtoul(comma 1, NULL, 10); memset(slot, 0, sizeof(*slot)); strncpy(slot-name, key, NAME_LEN - 1); slot-role_id (uint8_t)role; slot-active (active ! 0); return 0; } static int user_settings_commit(void) { int count 0; for (int i 0; i MAX_USERS; i) { if (user_table[i].name[0] ! \0) { count; } } user_count count; return 0; } SETTINGS_HANDLER_DEFINE(user_handler, SETTINGS_PREFIX, NULL, user_settings_set, user_settings_commit, NULL);user_settings_set里的 key 是去掉前缀后的部分比如完整键是users/alice回调拿到的 key 就是alice这正好就是用户名。value 格式我存的是角色编号,启用状态比如1,1表示 operator 且启用。解析时用strchr找逗号然后分别转数字。这个存储格式简单归简单但可读性也还行settings show输出里一眼能看懂。user_settings_commit是在所有键都加载完后调用的我在这里统一统计一次用户数量避免在set回调里反复加减计数导致出错。这种“先填表再 commit”的流程是 settings 标准用法别跳过。4.3 添加用户命令完整链路添加用户是整个应用的灵魂。我把这块代码贴出来然后每一步拆开讲static int is_valid_name(const char *name) { const char *p name; if (*p \0) { return 0; } while (*p) { if (!(isalnum((unsigned char)*p) || *p _)) { return 0; } p; } return 1; } static int cmd_user_add(const struct shell *sh, size_t argc, char **argv) { const char *name argv[2]; char key[SETTINGS_KEY_MAX]; char value[32]; unsigned int role 3; bool active true; struct user *slot; if (argc 3 || argc 5) { shell_print(sh, usage: user add name [role] [active]); return -EINVAL; } if (strlen(name) NAME_LEN) { shell_error(sh, name too long, max %d chars, NAME_LEN - 1); return -EINVAL; } if (!is_valid_name(name)) { shell_error(sh, invalid name: only letters, digits and _ allowed); return -EINVAL; } if (find_user(name) ! NULL) { shell_error(sh, user already exists: %s, name); return -EALREADY; } if (argc 4) { for (int i 0; i 4; i) { if (strcmp(argv[3], role_names[i]) 0) { role i; break; } } } if (argc 5) { active (strcmp(argv[4], 1) 0); } slot find_free_slot(); if (slot NULL) { shell_error(sh, user table full, max %d users, MAX_USERS); return -ENOMEM; } memset(slot, 0, sizeof(*slot)); strncpy(slot-name, name, NAME_LEN - 1); slot-role_id (uint8_t)role; slot-active active; snprintf(key, sizeof(key), %s/%s, SETTINGS_PREFIX, name); snprintf(value, sizeof(value), %u,%u, role, active ? 1u : 0u); if (settings_set_str(key, value) ! 0) { shell_error(sh, failed to save user %s, name); memset(slot, 0, sizeof(*slot)); return -EIO; } user_count; shell_print(sh, added user: %s (role%s, active%d), name, role_names[role], active ? 1 : 0); return 0; }这里有几个坑值得说。第一名字校验必须放在查重之前不然脏数据也会被拿去比较。第二settings_set_str失败后要把槽位清掉保持内存和 flash 的一致性。我最开始没做这个回滚结果有一次 flash 写入失败内存表里却多了个用户后面怎么删都删不掉调试半天才发现是内存和存储状态不一致导致的。第三写入 key 时用snprintf拼SETTINGS_PREFIX/name不要手动拼字符串避免越界。4.4 列表与删除命令列表命令主要注意排版我用固定宽度格式化输出串口终端看起来像表格static int cmd_user_list(const struct shell *sh, size_t argc, char **argv) { shell_print(sh, %-16s %-10s %s, NAME, ROLE, ACTIVE); shell_print(sh, ----------------------------------------); for (int i 0; i MAX_USERS; i) { if (user_table[i].name[0] \0) { continue; } shell_print(sh, %-16s %-10s %s, user_table[i].name, role_names[user_table[i].role_id], user_table[i].active ? yes : no); } shell_print(sh, %d user(s), user_count); return 0; }删除命令相对简单但顺序同样重要static int cmd_user_rm(const struct shell *sh, size_t argc, char **argv) { struct user *slot; char key[SETTINGS_KEY_MAX]; if (argc ! 3) { shell_print(sh, usage: user rm name); return -EINVAL; } slot (struct user *)find_user(argv[2]); if (slot NULL) { shell_error(sh, user not found); return -ENOENT; } snprintf(key, sizeof(key), %s/%s, SETTINGS_PREFIX, slot-name); if (settings_delete(key) ! 0) { shell_error(sh, failed to delete %s from storage, argv[2]); return -EIO; } memset(slot, 0, sizeof(*slot)); user_count--; shell_print(sh, removed user: %s, argv[2]); return 0; }顺序是先删 flash 里的记录再清内存槽位。反过来不行——如果先清了内存flash 删除失败的话重启后用户又会“复活”而且你还不知道删失败了。先删 flash 再清内存至少失败时内存还能保持一致可以继续重试。4.5 Shell 命令注册最后把上面几个命令挂到 shell 子命令树里SHELL_STATIC_SUBCMD_SET_CREATE(user_commands, SHELL_CMD(add, NULL, add user: user add name [role] [active], cmd_user_add), SHELL_CMD(list, NULL, list all users, cmd_user_list), SHELL_CMD(rm, NULL, remove user: user rm name, cmd_user_rm), SHELL_SUBCMD_SET_END ); SHELL_CMD_REGISTER(user, user_commands, user management commands);这里用的是SHELL_STATIC_SUBCMD_SET_CREATE它把add/list/rm挂在顶级命令user下面。执行效果就是uart:~$ user add alice uart:~$ user list uart:~$ user rm alice这种子命令树结构适合管理“一个功能点触发多个子操作”的场景。Zephyr 还支持别的命令注册方式但子命令集在用户管理这类功能里最清晰。4.6 初始化加载main 函数里负责初始化 settings 并加载数据void main(void) { int rc; rc settings_subsys_init(); if (rc ! 0) { printk(settings subsystem init failed: %d\n, rc); return; } rc settings_load(); if (rc ! 0) { printk(settings load returned: %d\n, rc); } printk(user app ready, %d user(s) loaded\n, user_count); }settings_subsys_init()负责把 NVS 后端挂起来然后settings_load()会把 flash 里所有键扫一遍触发我们注册的 handler。首次烧录时 flash 里没有数据settings_load()可能返回非 0 或者空跑这个打印只是提示不影响后续使用。5. 构建、烧录与实测过程5.1 构建命令在 Zephyr 工作区目录下执行west build -b qemu_cortex_m3 -d build/user_app path/to/user_app如果之前构建过可以加-p自动清理并重建west build -b qemu_cortex_m3 -d build/user_app -p path/to/user_app-b qemu_cortex_m3指定模拟的板卡-d指定构建目录。构建完成后启动 QEMU 交互west build -d build/user_app -t run这时会进入串口终端提示符一般是uart:~$。5.2 交互实测记录下面是一次完整的实测会话uart:~$ user list NAME ROLE ACTIVE ---------------------------------------- 0 user(s) uart:~$ user add alice added user: alice (roleviewer, active1) uart:~$ user add bob operator added user: bob (roleoperator, active1) uart:~$ user add carol admin 1 added user: carol (roleadmin, active1) uart:~$ user list NAME ROLE ACTIVE ---------------------------------------- alice viewer yes bob operator yes carol admin yes 3 user(s) uart:~$ user add bob user already exists: bob uart:~$ user add some one invalid name: only letters, digits and _ allowed uart:~$ settings show users/alice: 3,1 users/bob: 1,1 users/carol: 0,1注意settings show输出的就是 flash 里的原始键值对。alice默认是3,1因为代码里 role 默认值是 3对应 guestactive 默认 1bob指定了 operator 角色所以是1,1。这份原始数据能直接验证存储层是否正常工作。5.3 持久化验证关键一步是验证重启后数据不丢。在 QEMU 里重启应用再执行user listuart:~$ user list NAME ROLE ACTIVE ---------------------------------------- alice viewer yes bob operator yes carol admin yes 3 user(s)数据完整回来了。这就证明了这不仅仅是内存操作而是真正写进了 flash。这时候再删除一个用户试试uart:~$ user rm alice removed user: alice uart:~$ user list NAME ROLE ACTIVE ---------------------------------------- bob operator yes carol admin yes 2 user(s) uart:~$ settings show users/bob: 1,1 users/carol: 0,1settings show里已经看不到users/alice说明删除同时清掉了 flash 里的记录。5.4 观察资源占用跑起来之后我习惯看一眼 RAM 和 flash 占用。可以在构建输出里找到Memory region Used Size Region Size %age Used FLASH: 62.8 KB 256 KB 24.5% RAM: 11.4 KB 64 KB 17.9%这个结果说明 shell settings nvs 整套下来大概增加了几十 KB flash 和几 KB RAM对大部分板卡来说可接受。如果实际项目里资源紧张可以从CONFIG_SHELL关掉列表补全、关掉 shell 的 log backend能省下不少空间。6. 常见问题与排查技巧实录做完这个小项目我积了一批真实踩过的坑整理成张速查表希望各位少走弯路现象可能原因解决办法settings_subsys_init()返回非 0NVS 分区 label 和 devicetree 不一致或分区不存在检查 overlay 中label storage与CONFIG_SETTINGS_NVS_PARTITION_LABELstorage是否一致重启后用户数据丢失写入后未真正持久化或分区和代码段重叠被覆盖确认 settings 后端是 NVS而不是仅内存检查分区地址是否在 flash 末尾安全区域user add报 name too long名字超过NAME_LEN - 1缩短名字或调大NAME_LEN并同步调整存储键值长度名字能加进去但 settings show 看不到键名非法包含/或特殊符号名字限制为字母数字下划线settings_set_str的键里不要出现多余/删除用户后重启又回来先清了内存、后删 flashflash 删除失败但没感知严格按“先删 flash 后清内存”的顺序并检查返回值用户表满但明明没加几个删掉的用户槽位没有清零或者 user_count 计数漂移删除时memset(slot, 0, sizeof(*slot))commit 回调里重新统计启动时卡在 flash 相关初始化分区地址与 bootloader 或应用代码重叠查看目标板的 flash 布局把 storage 分区放到链路末尾6.1 settings_load 返回非 0 不一定是错误第一次烧录新板子时settings_load()返回一个非 0 值我当时第一反应是慌了。后来排查发现空 NVS 分区首次加载时没有可读记录某些版本就会返回-ENOENT之类的错误码。这个问题不大打印日志里能看到不会影响后续使用。真正要担心的是settings_subsys_init()失败那多半是分区没找到属于配置层面的硬错误。提示调试 settings 相关问题时settings show是你的第一工具。它直接输出 flash 里所有键值能立刻判断是“根本没存进去”还是“存了但读出来不对”。6.2 分区重叠是最隐蔽的坑我之前在一款真实板卡上调这个功能应用一直起不来反复检查代码也没发现毛病。最后查了 devicetree 才知道板卡自带的分区表里已经有 bootloader 占用了 flash 起始区域我只看到 flash0 总大小 256 KB 就把存储分区放在了低位地址结果直接把引导代码覆盖了。这种问题不会报编译错误只会在启动阶段神秘崩溃。经验是给存储分区分配地址之前先打开板卡的.dts文件确认 flash 从哪个地址开始被谁占用。如果板卡已经有partitions节点就在现有节点里追加自己的分区而不是另起炉灶。放在 flash 末尾是最稳妥的做法因为应用代码从低地址开始增长高地址通常空闲。6.3 字符串边界和缓冲区溢出这段代码里有三个容易越界的字符串用户名、键名、value。我都做了防护用户名限制NAME_LEN-1键名用SETTINGS_KEY_MAX的缓冲区加snprintf组装value 缓冲区 32 字节对3,1这种格式绰绰有余。但如果你后面要加密码字段、加备注字段务必同步检查这些缓冲区大小。嵌入式开发里字符串越界不报错后果是诡异的覆盖和时好时坏的行为。6.4 小心频繁写入 flashNVS 虽然做了磨损均衡但也不是无限寿命。我这个应用只是靠人手工输入命令写入频率很低没关系。但如果你把这个模块改成循环写入状态量flash 很快会写穿。用户管理这类配置型数据正确使用姿势是“添加时写一次删除时写一次”而不是每次状态变化都写。如果一定要高频写建议换外部存储或者加大分区容量。一个小建议把存储格式文档化最后分享一个小经验。虽然users/alice: 3,1这种格式人能看懂但时间一长你就会忘记3代表什么、1代表什么。我在项目里加了一个注释头把这个格式的文档直接写在 main.c 顶部/* * settings key: users/name * settings value: role_id,active * role_id: 0admin, 1operator, 2viewer, 3guest * active: 0disabled, 1enabled */这个习惯是在吃了好几次“过一个月看不懂自己代码”的亏之后养成的。嵌入式项目经常要跨几个月维护一份清晰的存储格式说明比任何花哨注释都管用。这篇应用本身不难真正值钱的是这套“能存、能查、能删、能重启恢复”的骨架拿它去套设备配置管理、白名单过滤、甚至是简单的账户系统都很顺手。
RELATED

相关推荐

无电感电荷泵D类功放CS5250:原理、选型、PCB布局与避坑指南

无电感电荷泵D类功放CS5250:原理、选型、PCB布局与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/17 1:28:53
两串电池组主动均衡方案:MP2642GR-Z与ATmega644P实战

两串电池组主动均衡方案:MP2642GR-Z与ATmega644P实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/17 1:28:53
kubectl-ssh:一条命令轻松登录任意Kubernetes节点

kubectl-ssh:一条命令轻松登录任意Kubernetes节点

如果让我列一个 Kubernetes 日常排障里最磨人的环节,登录节点一定排在前三。节点 NotReady、Pod 卡在 Pending、容器运行时抽风……很多问题不跑到节点上看一眼根本定位不了。而问题恰恰就出在“到节点上看一眼”这几个字上:你要先查出节点对应的 IP&…

📅 2026/10/17 1:28:53
MORE NEWS

更多资讯

📰

Ascend C Matmul GEMV 实战:用 CubeFormat::VECTOR 在 asc-devkit 中实现矩阵向量乘

人工智能深度学习算子库CANNAscend 【免费下载链接】asc-devkit 本项目是CANN 推出的昇腾AI处理器专用的算子程序开发语言,原生支持C和C标准规范,主要由类库和语言扩展层构成,提供多层级API,满足多维场景算子开发诉求。 项目地址&…

📰

REFramework 完整指南:RE Engine 游戏模组框架与 Lua 脚本平台快速跑通

REFramework 完整指南:RE Engine 游戏模组框架与 Lua 脚本平台快速跑通 【免费下载链接】REFramework Mod loader, scripting platform, and VR support for all RE Engine games 项目地址: https://gitcode.com/GitHub_Trending/re/REFramework REFramework…

📰

DeepFilterNet ONNX 导出与跨平台部署:从导出命令到三条集成路径的完整教程

DeepFilterNet ONNX 导出与跨平台部署:从导出命令到三条集成路径的完整教程 【免费下载链接】DeepFilterNet Noise supression using deep filtering 项目地址: https://gitcode.com/GitHub_Trending/de/DeepFilterNet DeepFilterNet 是一个面向 48kHz 全频带…

📰

CloudCompare点云处理实战:从激光雷达数据到OBJ建模全流程

简介:CloudCompare是一款面向三维点云处理领域的开源跨平台软件,适用于测绘、逆向工程、计算机视觉及地理信息系统(GIS)等方向的研究者与工程师,可高效完成法向量计算与优化、泊松曲面重建、点云滤波、配准与可视化等核…

📰

Flutter开发OpenHarmony离线Web开发助手App:JavaScript参考模块实战

如果你手里同时握着几张牌:一段还算扎实的Flutter开发经验、一台能跑OpenHarmony的设备,以及“写JavaScript时总得翻语法”的老毛病,那么把这三种需求压成一个离线App,会是件相当顺手的事。这篇就是我从零搭一个Web开发助手App的实…

📰

SQL Developer 4.0.3.16.84 配置与避坑实战指南

简介:Oracle SQL Developer 4.0.3.16.84-x64 是 Oracle 官方推出的免费数据库图形化管理工具,面向开发人员、DBA 及数据分析人员,适用于连接、查询、建模、迁移、调优等日常操作,界面直观、对新手友好。压缩包共 2000 个文件&…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬