5.31 SN序列号设备
5.31 SN序列号设备
📚 本节导读
学习时长: 约 20 分钟
难度级别: ⭐⭐☆☆☆
前置知识: C 语言基础、5.1 设备驱动框架、5.17 FAL Flash 抽象层
本节讲解 OneOS 的 SN(Serial Number,序列号)设备。在量产物联网设备时,每台设备通常需要烧录一个唯一序列号(用于身份识别、防伪、售后溯源),而序列号必须掉电不丢失,所以要存到 Flash 里。
OneOS 的 SN 设备通过 FAL 分区把序列号存储在名为 "cfg" 的 Flash 分区中,并提供一套 Shell 命令(sn read / sn write / sn erase / get_device_info)让产线工具和开发者读写。本节会带你:
- 看懂序列号在 FAL 分区中的存储约定(分区名、偏移、长度);
- 逐个拆解
sn_read/sn_write/sn_erase三个内部函数的完整源码; - 掌握
sn与get_device_infoShell 命令的用法; - 通过实际产线命令掌握序列号的烧录与读取流程。
说明:本节所有源码均取自项目
drivers/sn/sn_cfg.c,与项目源码逐字一致(含原始英文注释)。
🎯 学习目标
- 理解 SN 设备基于 FAL 分区存储序列号的设计
- 掌握序列号存储的核心配置(分区名、偏移、长度)
- 逐个拆解
sn_read/sn_write/sn_erase的完整源码与执行流程 - 掌握
sn与get_device_infoShell 命令的交互用法 - 了解产线烧录序列号的完整流程
一、概述
1.1 SN 设备解决什么问题
一句话核心:SN 设备适用于需要在 Flash 中持久化存储一段"每台设备唯一"的序列号,并通过命令行烧录/读取/擦除的场景。它让你"在产线敲一条命令,就能把 20 位序列号写进 Flash 并随时读回"。
为什么用 FAL 分区存储:
- 序列号必须掉电不丢失,所以要放 Flash。
- 直接操作 Flash 底层(裸 flash 读写擦除)复杂且易错,OneOS 用 FAL(Flash Abstraction Layer) 把 Flash 抽象成分区,SN 设备只需告诉 FAL"我要读写
cfg分区",不用关心底层是哪颗 Flash、怎么擦除。 - 序列号放在独立分区
"cfg",与应用固件分区隔离,升级固件不会误删序列号。
1.2 核心配置
SN 设备的存储位置由 sn_cfg.c 顶部的三个宏决定:
来源:drivers/sn/sn_cfg.c
#define SN_PARTITION_NAME "cfg"
#define SN_ADDR_OFFSET 0
#define SN_LENGTH 20| 宏 | 值 | 含义 |
|---|---|---|
SN_PARTITION_NAME | "cfg" | 序列号存储在 FAL 的 "cfg" 分区中 |
SN_ADDR_OFFSET | 0 | 从分区起始偏移 0 字节处开始存储 |
SN_LENGTH | 20 | 序列号长度为 20 个字符(20 字节) |
关键理解:这三个宏在
sn命令和get_device_info命令中都被反复使用。例如sn read实际执行的就是"从cfg分区偏移 0 读 20 字节"。
1.3 SN 设备整体结构
sn_cfg.c 主要包含三部分:
| 部分 | 函数/宏 | 作用 |
|---|---|---|
| 内部辅助函数 | char_dump / sn_read / sn_write / sn_erase | 序列号的显示、读、写、擦除底层实现 |
sn 命令 | sn 函数 + SH_CMD_EXPORT | 提供 sn read/write/erase 命令行 |
| 板卡信息 | slot_no_read / board_no_read / board_no_pin_init / get_device_info | 读取板卡号/槽位号、显示设备信息 |
1.4 SN 设备使用场景
1.4.1 一句话核心
SN 设备适用于量产阶段给每台设备烧录并持久化唯一序列号、以及运行阶段查询设备身份信息的场景。它让你"通过串口命令行即可完成序列号读写与板卡信息查询"。
1.4.2 典型应用场景
场景一:产线烧录序列号
产线通过串口连接设备,在初始化阶段执行:
> sn write 12345678901234567890 ← 写入 20 位序列号到 Flash
write success
> sn read ← 读回验证
sn:12345678901234567890为什么用 SN 设备:SN 命令封装了 FAL 分区查找、解锁、擦除、写入、加锁的完整流程,产线工具只需敲一条命令,不用理解底层 Flash 操作。
场景二:设备身份信息展示(板卡号/槽位号/型号)
多板卡系统中,通过 get_device_info 查看当前设备属于哪块板、插在哪个槽位、SoC 型号、固件编译时间等:
> get_device_info
sn:12345678901234567890
type:STM32F407VE
version:Aug 18 2026-10:30:00
back_bd_no:3
core_bd_slot:1为什么用 SN 设备:get_device_info 读取板卡号引脚(board_no_pin_tab)和槽位号引脚(slot_no_pin_tab)的电平组合来得出板卡/槽位编号,配合序列号和型号,方便产线识别与售后定位。
1.4.3 选型边界表(何时不该用它)
| 场景 | 该用什么 | 为什么 |
|---|---|---|
| 需要存储大量结构化数据(键值对/配置项) | 专门的配置系统(如 KVDB)或文件系统 | SN 设备只存 20 字节序列号 |
| 需要断电保存任意长度的用户数据 | FAL 分区 + 自定义读写 | SN 固定 20 字节长度 |
| 不需要命令行、只需代码里读写序列号 | 直接调 FAL API(fal_part_read/write) | 若不需要产线命令交互,可绕过 SN 的 Shell 封装 |
1.4.4 结合源码的使用注意点
- 写之前自动擦除整块:
sn_write先fal_part_erase(part, offset, block_size)擦除一个块,再写。因为 Flash 只能把 1 写为 0,不能直接覆盖,必须先擦除。 - 序列号长度固定 20:
sn write命令会检查strlen(argv[2]) == 20,否则提示SN char number is not euqal 20!。 - 读写必须配对:
sn_write内部用fal_part_unlock/fal_part_lock包裹,避免并发冲突;sn_erase同理。 - 读取结果显示为
?:char_dump把非字母数字的字节(如0xff、未擦除的数据)显示为?,这是 Flash 擦除后的默认值。
二、核心 API 详解
说明:以下函数均为
sn_cfg.c中的static函数,由sn命令间接调用。完整源码与项目逐字一致。
2.1 内部辅助函数 char_dump —— 格式化显示序列号
一句话定位:把读到的 20 字节数据格式化打印成序列号字符串,非可打印字符显示为 ?。
完整源码(drivers/sn/sn_cfg.c,与项目一致):
static void char_dump(unsigned char *buff, int count)
{
int i;
os_kprintf("sn:");
for (i = 0; i < count; i++)
{
if (buff[i] == 0xff)
os_kprintf("?");
#ifdef SERIES_CH32V30X
else if (buff[i] == 0x39)
{
if(i < count - 1)
{
if (buff[i + 1] == 0xe3)
{
os_kprintf("?");
}
else
{
os_kprintf("%c", buff[i]);
}
}
else if (buff[i - 1] == 0xe3)
{
os_kprintf("?");
}
else
{
os_kprintf("%c", buff[i]);
}
}
#endif
else if (isalnum(buff[i]))
os_kprintf("%c", buff[i]);
else
os_kprintf("?");
}
os_kprintf("\r\n");
}分步详解:
第 1 步:打印 sn: 前缀(无条件执行)
int i;
os_kprintf("sn:");详细解释:先打印 sn: 作为输出前缀,标识这是一段序列号内容。
第 2 步:遍历每个字节并判断显示方式(条件 buff[i] == 0xff)
for (i = 0; i < count; i++)
{
if (buff[i] == 0xff)
os_kprintf("?");详细解释:
- 遍历
count个字节。 buff[i] == 0xff:Flash 擦除后的默认值是0xff,说明该位置没有写入有效数据(或为空),显示为?。
第 3 步:特殊芯片的兼容处理(条件 SERIES_CH32V30X 且字节为 0x39)
#ifdef SERIES_CH32V30X
else if (buff[i] == 0x39)
{
if(i < count - 1)
{
if (buff[i + 1] == 0xe3)
{
os_kprintf("?");
}
else
{
os_kprintf("%c", buff[i]);
}
}
else if (buff[i - 1] == 0xe3)
{
os_kprintf("?");
}
else
{
os_kprintf("%c", buff[i]);
}
}
#endif详细解释:这段是针对 CH32V30X 系列芯片的兼容处理(条件编译 SERIES_CH32V30X)。它处理字节 0x39(ASCII 数字 '9')与多字节字符编码 0xe3 相邻的特殊情况,避免把 UTF-8 中文编码的字节误显示。非该系列芯片时整段不编译。
第 4 步:可打印字符直接显示,否则显示 ?(条件 isalnum(buff[i]))
else if (isalnum(buff[i]))
os_kprintf("%c", buff[i]);
else
os_kprintf("?");
}
os_kprintf("\r\n");
}详细解释:
isalnum(buff[i]):判断是否字母或数字。是则用%c原样打印;否则(其他不可打印字节)打印?。- 循环结束后打印换行
\r\n。
条件走向总结:
char_dump() 条件走向总结(针对每个字节 buff[i]):
├── buff[i] == 0xff → 打印 "?"(Flash 擦除默认值)
├── (SERIES_CH32V30X)buff[i] == 0x39 → 按相邻字节 0xe3 判断打印 "?" 或 "%c"
├── isalnum(buff[i]) 为真 → 打印 "%c"(字母/数字)
└── 否则 → 打印 "?"2.2 内部辅助函数 sn_read —— 从分区读取序列号
一句话定位:查找 cfg 分区,从偏移处读取 length 字节并打印。
完整源码(drivers/sn/sn_cfg.c,与项目一致):
static int sn_read(const char *name, os_off_t offset, uint32_t length)
{
os_size_t count;
uint8_t *data;
fal_part_t *part;
part = fal_part_find(name);
if (part == OS_NULL)
{
LOG_E(DBG_TAG, "invalide fal part!\r\n");
return OS_FAILURE;
}
data = os_malloc(length);
if (data == OS_NULL)
{
LOG_E(DBG_TAG, "out of memory!\r\n");
return OS_NOMEM;
}
memset(data, 0xff, length);
count = fal_part_read(part, offset, data, length);
if (count == length)
{
char_dump(data, length);
}
else
{
LOG_E(DBG_TAG, "read failed %d/%d\r\n", count, length);
}
os_free(data);
return OS_SUCCESS;
}分步详解:
第 1 步:查找 FAL 分区(条件 part == OS_NULL)
part = fal_part_find(name);
if (part == OS_NULL)
{
LOG_E(DBG_TAG, "invalide fal part!\r\n");
return OS_FAILURE;
}详细解释:fal_part_find(name) 按名字(如 "cfg")在 FAL 分区表中查找分区。若找不到(part == OS_NULL,可能分区未定义),打印错误并返回 OS_FAILURE。
第 2 步:申请读取缓冲区(条件 data == OS_NULL)
data = os_malloc(length);
if (data == OS_NULL)
{
LOG_E(DBG_TAG, "out of memory!\r\n");
return OS_NOMEM;
}详细解释:os_malloc(length) 申请 length(20)字节的临时缓冲区。申请失败(内存不足)返回 OS_NOMEM。
第 3 步:把缓冲区填成 Flash 默认值(无条件执行)
memset(data, 0xff, length);详细解释:先把缓冲区全部填成 0xff。因为 Flash 擦除后是 0xff,这样如果分区某段未写入,读出来就是 0xff,char_dump 会显示为 ?。
第 4 步:调用 FAL 读取数据(条件 count == length)
count = fal_part_read(part, offset, data, length);
if (count == length)
{
char_dump(data, length);
}
else
{
LOG_E(DBG_TAG, "read failed %d/%d\r\n", count, length);
}
os_free(data);
return OS_SUCCESS;
}详细解释:
fal_part_read(part, offset, data, length):从分区offset处读取length字节到data,返回实际读取字节数count。- 若
count == length(读满),调用char_dump打印序列号;否则打印read failed。 - 释放缓冲区,返回
OS_SUCCESS。
条件走向总结:
sn_read() 条件走向总结:
├── part = fal_part_find(name)
├── part == OS_NULL(分区不存在)→ LOG_E + 返回 OS_FAILURE
├── data = os_malloc(length)
├── data == OS_NULL(内存不足)→ LOG_E + 返回 OS_NOMEM
├── memset(data, 0xff, length) ← 填 Flash 默认值
├── count = fal_part_read(part, offset, data, length)
│ ├── count == length(读满)→ char_dump(data, length) 打印
│ └── count != length(读失败)→ LOG_E "read failed"
├── os_free(data)
└── 返回 OS_SUCCESS2.3 内部辅助函数 sn_write —— 写入序列号
一句话定位:先擦除 cfg 分区的一个块,再把序列号写入。
完整源码(drivers/sn/sn_cfg.c,与项目一致):
static int sn_write(const char *name, os_off_t offset, char *buf, uint32_t length)
{
os_size_t count;
os_size_t block_size;
fal_part_t *part;
part = fal_part_find(name);
if (part == OS_NULL)
{
LOG_E(DBG_TAG, "invalide fal part!\r\n");
return OS_FAILURE;
}
fal_part_unlock(part);
block_size = fal_part_block_size(part);
count = fal_part_erase(part, offset, block_size);
if (count != block_size)
{
LOG_E(DBG_TAG, "erase failed %d/%d\r\n", count, block_size);
fal_part_lock(part);
return OS_FAILURE;
}
count = fal_part_write(part, offset, (uint8_t *)buf, length);
fal_part_lock(part);
if (count == length)
{
LOG_D(DBG_TAG, "write success\r\n");
return OS_SUCCESS;
}
else
{
LOG_E(DBG_TAG, "write failed %d/%d\r\n", count, length);
return OS_FAILURE;
}
}分步详解:
第 1 步:查找分区并解锁(条件 part == OS_NULL)
part = fal_part_find(name);
if (part == OS_NULL)
{
LOG_E(DBG_TAG, "invalide fal part!\r\n");
return OS_FAILURE;
}
fal_part_unlock(part);详细解释:与 sn_read 相同的分区查找;找到后用 fal_part_unlock(part) 给分区解锁,允许后续擦除/写入(防止并发冲突)。
第 2 步:获取块大小并擦除一个块(条件 count != block_size)
block_size = fal_part_block_size(part);
count = fal_part_erase(part, offset, block_size);
if (count != block_size)
{
LOG_E(DBG_TAG, "erase failed %d/%d\r\n", count, block_size);
fal_part_lock(part);
return OS_FAILURE;
}详细解释:
fal_part_block_size(part):获取该分区的块大小(擦除的最小单位,通常为 4KB 等)。fal_part_erase(part, offset, block_size):擦除从offset开始的一个块。- 为什么必须先擦除:Flash 的物理特性是只能把 1 写成 0,不能把 0 写成 1。要写入新数据,必须先擦除(把所有位变成 1),再写。若擦除字节数不等于块大小,说明擦除失败,重新加锁并返回
OS_FAILURE。
第 3 步:写入序列号并加锁(条件 count == length)
count = fal_part_write(part, offset, (uint8_t *)buf, length);
fal_part_lock(part);
if (count == length)
{
LOG_D(DBG_TAG, "write success\r\n");
return OS_SUCCESS;
}
else
{
LOG_E(DBG_TAG, "write failed %d/%d\r\n", count, length);
return OS_FAILURE;
}
}详细解释:
fal_part_write(part, offset, (uint8_t *)buf, length):把buf的length字节写到分区offset处。- 写完后
fal_part_lock(part)重新加锁。 - 若实际写入字节数等于
length,返回OS_SUCCESS;否则打印write failed返回OS_FAILURE。
条件走向总结:
sn_write() 条件走向总结:
├── part = fal_part_find(name)
├── part == OS_NULL(分区不存在)→ LOG_E + 返回 OS_FAILURE
├── fal_part_unlock(part)
├── block_size = fal_part_block_size(part)
├── count = fal_part_erase(part, offset, block_size)
│ ├── count != block_size(擦除失败)→ LOG_E + fal_part_lock + 返回 OS_FAILURE
│ └── count == block_size(擦除成功)→ 继续
├── count = fal_part_write(part, offset, buf, length)
├── fal_part_lock(part)
│ ├── count == length(写满)→ LOG_D + 返回 OS_SUCCESS
│ └── count != length(写失败)→ LOG_E + 返回 OS_FAILURE2.4 内部辅助函数 sn_erase —— 擦除序列号
一句话定位:擦除 cfg 分区的一个块,清空序列号。
完整源码(drivers/sn/sn_cfg.c,与项目一致):
static int sn_erase(const char *name, os_off_t offset)
{
os_size_t count;
os_size_t block_size;
fal_part_t *part;
part = fal_part_find(name);
if (part == OS_NULL)
{
LOG_E(DBG_TAG, "invalide fal part!\r\n");
return OS_FAILURE;
}
fal_part_unlock(part);
block_size = fal_part_block_size(part);
count = fal_part_erase(part, offset, block_size);
fal_part_lock(part);
if (count == block_size)
{
LOG_D(DBG_TAG, "erase success\r\n");
return OS_SUCCESS;
}
else
{
LOG_E(DBG_TAG, "erase failed %d/%d\r\n", count, block_size);
return OS_FAILURE;
}
}分步详解:
第 1 步:查找分区并解锁(条件 part == OS_NULL)
part = fal_part_find(name);
if (part == OS_NULL)
{
LOG_E(DBG_TAG, "invalide fal part!\r\n");
return OS_FAILURE;
}
fal_part_unlock(part);详细解释:与 sn_read/sn_write 相同的分区查找与解锁逻辑。
第 2 步:获取块大小并擦除(条件 count == block_size)
block_size = fal_part_block_size(part);
count = fal_part_erase(part, offset, block_size);
fal_part_lock(part);
if (count == block_size)
{
LOG_D(DBG_TAG, "erase success\r\n");
return OS_SUCCESS;
}
else
{
LOG_E(DBG_TAG, "erase failed %d/%d\r\n", count, block_size);
return OS_FAILURE;
}
}详细解释:擦除一个块,随后加锁。擦除字节数等于块大小则成功返回 OS_SUCCESS;否则返回 OS_FAILURE。
条件走向总结:
sn_erase() 条件走向总结:
├── part = fal_part_find(name)
├── part == OS_NULL → LOG_E + 返回 OS_FAILURE
├── fal_part_unlock(part)
├── count = fal_part_erase(part, offset, block_size)
├── fal_part_lock(part)
│ ├── count == block_size(擦除成功)→ LOG_D + 返回 OS_SUCCESS
│ └── count != block_size(擦除失败)→ LOG_E + 返回 OS_FAILURE2.5 sn 命令 —— Shell 命令行
一句话定位:注册 sn 命令,解析 read / write / erase 子命令并调用对应函数。
完整源码(drivers/sn/sn_cfg.c,与项目一致):
static void sn(int argc, char **argv)
{
if ((argc > 3) || (argc == 1))
{
help:
os_kprintf("\r\n");
os_kprintf("sn [OPTION] [PARAM ...]\r\n");
os_kprintf(" read Read <%d> Bytes from <%d> of <%s>\r\n",
SN_LENGTH,
SN_ADDR_OFFSET,
SN_PARTITION_NAME);
os_kprintf(" write Write <%d> Bytes from <%d> of <%s>\r\n",
SN_LENGTH,
SN_ADDR_OFFSET,
SN_PARTITION_NAME);
return;
}
if (!strcmp(argv[1], "read"))
{
if (argc != 2)
{
os_kprintf("The input parameters are not correct!\r\n");
goto help;
}
sn_read(SN_PARTITION_NAME, SN_ADDR_OFFSET, SN_LENGTH);
}
else if (!strcmp(argv[1], "write"))
{
if (argc != 3)
{
os_kprintf("The input parameters are not correct!\r\n");
goto help;
}
if (strlen(argv[2]) == 20)
{
if (OS_SUCCESS == sn_write(SN_PARTITION_NAME, SN_ADDR_OFFSET, argv[2], SN_LENGTH))
{
os_kprintf("write success\r\n");
}
else
{
os_kprintf("write fail\r\n");
}
}
else
{
os_kprintf("SN char number is not euqal 20!\r\n");
}
}
else if (!strcmp(argv[1], "erase"))
{
if (argc != 2)
{
os_kprintf("The input parameters are not correct!\r\n");
goto help;
}
if (OS_SUCCESS == sn_erase(SN_PARTITION_NAME, SN_ADDR_OFFSET))
{
os_kprintf("erase success\r\n");
}
else
{
os_kprintf("erase erase\r\n");
}
}
else
{
os_kprintf("Input parameters are not supported!\r\n");
goto help;
}
}
SH_CMD_EXPORT(sn, sn, "sn config");分步详解:
第 1 步:参数个数预检(条件 argc > 3 或 argc == 1)
if ((argc > 3) || (argc == 1))
{
help:
os_kprintf("\r\n");
os_kprintf("sn [OPTION] [PARAM ...]\r\n");
os_kprintf(" read Read <%d> Bytes from <%d> of <%s>\r\n",
SN_LENGTH,
SN_ADDR_OFFSET,
SN_PARTITION_NAME);
os_kprintf(" write Write <%d> Bytes from <%d> of <%s>\r\n",
SN_LENGTH,
SN_ADDR_OFFSET,
SN_PARTITION_NAME);
return;
}详细解释:argc 是参数个数(含命令本身)。合法范围是 2 或 3(sn read、sn write xxx、sn erase)。若参数过多(>3)或过少(==1,只有 sn),跳到 help 标签打印用法(read/write 的帮助,各含 SN_LENGTH/SN_ADDR_OFFSET/SN_PARTITION_NAME)后返回。
第 2 步:处理 read 子命令(条件 argv[1] == "read")
if (!strcmp(argv[1], "read"))
{
if (argc != 2)
{
os_kprintf("The input parameters are not correct!\r\n");
goto help;
}
sn_read(SN_PARTITION_NAME, SN_ADDR_OFFSET, SN_LENGTH);
}详细解释:sn read 必须恰好 2 个参数。合法则调用 sn_read("cfg", 0, 20) 读取并打印序列号。
第 3 步:处理 write 子命令(条件 argv[1] == "write")
else if (!strcmp(argv[1], "write"))
{
if (argc != 3)
{
os_kprintf("The input parameters are not correct!\r\n");
goto help;
}
if (strlen(argv[2]) == 20)
{
if (OS_SUCCESS == sn_write(SN_PARTITION_NAME, SN_ADDR_OFFSET, argv[2], SN_LENGTH))
{
os_kprintf("write success\r\n");
}
else
{
os_kprintf("write fail\r\n");
}
}
else
{
os_kprintf("SN char number is not euqal 20!\r\n");
}
}详细解释:sn write <sn> 需要 3 个参数(含命令)。必须恰好 20 个字符(strlen(argv[2]) == 20),否则提示 SN char number is not euqal 20!。合法则调用 sn_write,成功打印 write success,失败打印 write fail。
第 4 步:处理 erase 子命令(条件 argv[1] == "erase")
else if (!strcmp(argv[1], "erase"))
{
if (argc != 2)
{
os_kprintf("The input parameters are not correct!\r\n");
goto help;
}
if (OS_SUCCESS == sn_erase(SN_PARTITION_NAME, SN_ADDR_OFFSET))
{
os_kprintf("erase success\r\n");
}
else
{
os_kprintf("erase erase\r\n");
}
}详细解释:sn erase 必须恰好 2 个参数。调用 sn_erase,成功打印 erase success,失败打印 erase erase(源码原样,属于项目现状的笔误文案)。
第 5 步:未知子命令(条件 其它)
else
{
os_kprintf("Input parameters are not supported!\r\n");
goto help;
}
}
SH_CMD_EXPORT(sn, sn, "sn config");详细解释:子命令不是 read/write/erase 时,提示不支持并跳到 help 打印用法。末尾 SH_CMD_EXPORT(sn, sn, "sn config") 把函数注册为 Shell 命令。
sn() 条件走向总结:
sn() 条件走向总结:
├── argc > 3 或 argc == 1(参数过多或过少)→ help: 打印用法 → return
├── argv[1] == "read"
│ ├── argc != 2 → 提示参数错误 → goto help
│ └── argc == 2 → sn_read("cfg", 0, 20)
├── argv[1] == "write"
│ ├── argc != 3 → 提示参数错误 → goto help
│ └── argc == 3
│ ├── strlen(argv[2]) != 20 → 提示 "SN char number is not euqal 20!"
│ └── strlen(argv[2]) == 20 → sn_write(...)
│ ├── 成功 → 打印 "write success"
│ └── 失败 → 打印 "write fail"
├── argv[1] == "erase"
│ ├── argc != 2 → 提示参数错误 → goto help
│ └── argc == 2 → sn_erase(...)
│ ├── 成功 → 打印 "erase success"
│ └── 失败 → 打印 "erase erase"
└── 其它子命令 → 提示 "Input parameters are not supported!" → goto help2.6 get_device_info 命令 —— 显示板卡信息
一句话定位:读取板卡号/槽位号引脚,并打印序列号、SoC 型号、编译时间等信息。
完整源码(drivers/sn/sn_cfg.c,与项目一致):
static int slot_no_read(void)
{
int ret = 0;
int i;
for (i = 0; i < slot_no_pin_tab_size; i++)
{
ret = ret << 1;
ret |= os_pin_read(slot_no_pin_tab[i]);
}
return ret;
}
static int board_no_read(void)
{
int ret = 0;
int i;
for (i = 0; i < board_no_pin_tab_size; i++)
{
ret = ret << 1;
ret |= os_pin_read(board_no_pin_tab[i]);
}
return ret;
}
static os_err_t board_no_pin_init(void)
{
int i;
for (i = 0; i < board_no_pin_tab_size; i++)
{
os_pin_mode(board_no_pin_tab[i], PIN_MODE_INPUT);
}
for (i = 0; i < slot_no_pin_tab_size; i++)
{
os_pin_mode(slot_no_pin_tab[i], PIN_MODE_INPUT);
}
return OS_SUCCESS;
}
OS_INIT_CALL(board_no_pin_init, OS_INIT_LEVEL_DEVICE, OS_INIT_SUBLEVEL_HIGH);
static os_err_t get_device_info(int32_t argc, char **argv)
{
int board_no;
int slot_no;
board_no = board_no_read();
slot_no = slot_no_read();
sn_read(SN_PARTITION_NAME, SN_ADDR_OFFSET, SN_LENGTH);
os_kprintf("type:%s\r\n", SOC_MODEL);
os_kprintf("version:%s-%s\r\n", __DATE__, __TIME__);
os_kprintf("back_bd_no:%d\r\n", board_no);
os_kprintf("core_bd_slot:%d\r\n", slot_no);
#ifdef BSP_DATA_SECTION_SIZE
os_kprintf("ram_size:%d\r\n", BSP_DATA_SECTION_SIZE);
#endif
return OS_SUCCESS;
}
SH_CMD_EXPORT(get_device_info, get_device_info, "show board info");分步详解:
第 1 步:读取槽位号引脚(无条件执行)
static int slot_no_read(void)
{
int ret = 0;
int i;
for (i = 0; i < slot_no_pin_tab_size; i++)
{
ret = ret << 1;
ret |= os_pin_read(slot_no_pin_tab[i]);
}
return ret;
}详细解释:slot_no_pin_tab[] 是板级定义的槽位号引脚表(在 board 头文件中定义,这里直接引用)。循环里每读一个引脚就 ret << 1 左移一位再 |= 该引脚电平,把多个引脚的 0/1 电平拼接成一个二进制数,即槽位号。
数值演算(假设 2 个槽位引脚都读回高电平):
| 计算项 | 代入计算 | 结果 |
|---|---|---|
| 第 1 个引脚 | ret = 0<<1 | PIN_HIGH = 0 | 1 | 1 |
| 第 2 个引脚 | ret = 1<<1 | PIN_HIGH = 2 | 1 | 3 |
| 槽位号 | 返回 ret | 3 |
第 2 步:读取板卡号引脚(无条件执行)
static int board_no_read(void)
{
int ret = 0;
int i;
for (i = 0; i < board_no_pin_tab_size; i++)
{
ret = ret << 1;
ret |= os_pin_read(board_no_pin_tab[i]);
}
return ret;
}详细解释:与 slot_no_read 逻辑完全一致,只是用 board_no_pin_tab[] 引脚表,读出的二进制数是板卡号。
第 3 步:初始化板卡号/槽位号引脚(无条件执行,自动初始化)
static os_err_t board_no_pin_init(void)
{
int i;
for (i = 0; i < board_no_pin_tab_size; i++)
{
os_pin_mode(board_no_pin_tab[i], PIN_MODE_INPUT);
}
for (i = 0; i < slot_no_pin_tab_size; i++)
{
os_pin_mode(slot_no_pin_tab[i], PIN_MODE_INPUT);
}
return OS_SUCCESS;
}
OS_INIT_CALL(board_no_pin_init, OS_INIT_LEVEL_DEVICE, OS_INIT_SUBLEVEL_HIGH);详细解释:把板卡号引脚和槽位号引脚都配置为输入模式(PIN_MODE_INPUT),这样 board_no_read/slot_no_read 才能读到电平。OS_INIT_CALL(board_no_pin_init, OS_INIT_LEVEL_DEVICE, OS_INIT_SUBLEVEL_HIGH) 让它在设备初始化阶段自动执行。
第 4 步:打印设备信息(无条件执行)
static os_err_t get_device_info(int32_t argc, char **argv)
{
int board_no;
int slot_no;
board_no = board_no_read();
slot_no = slot_no_read();
sn_read(SN_PARTITION_NAME, SN_ADDR_OFFSET, SN_LENGTH);
os_kprintf("type:%s\r\n", SOC_MODEL);
os_kprintf("version:%s-%s\r\n", __DATE__, __TIME__);
os_kprintf("back_bd_no:%d\r\n", board_no);
os_kprintf("core_bd_slot:%d\r\n", slot_no);
#ifdef BSP_DATA_SECTION_SIZE
os_kprintf("ram_size:%d\r\n", BSP_DATA_SECTION_SIZE);
#endif
return OS_SUCCESS;
}
SH_CMD_EXPORT(get_device_info, get_device_info, "show board info");详细解释:依次读取板卡号、槽位号,打印序列号(sn_read),再打印:
type:%s:SoC 型号(SOC_MODEL宏)。version:%s-%s:固件编译日期和时间(__DATE__、__TIME__编译器内置宏)。back_bd_no:板卡号。core_bd_slot:槽位号。ram_size(条件BSP_DATA_SECTION_SIZE定义了才打印):RAM 大小。
SH_CMD_EXPORT(get_device_info, get_device_info, "show board info") 注册为命令。
三、完整使用示例
示例:产线烧录并验证序列号
以下是在 OneOS Shell(串口终端)中执行的完整产线操作流程:
# 1. 写入 20 位序列号到 cfg 分区(sn write 命令,自动完成解锁→擦除→写入→加锁)
> sn write 12345678901234567890
write success
# 2. 读回验证(sn read 命令,从 cfg 分区偏移 0 读 20 字节并打印)
> sn read
sn:12345678901234567890
# 3. 查看设备完整信息(型号、版本、板卡号、槽位号)
> get_device_info
sn:12345678901234567890
type:STM32F407VE
version:Aug 18 2026-10:30:00
back_bd_no:3
core_bd_slot:1
# 4. (可选)擦除序列号,出厂复位
> sn erase
erase success说明:示例展示了
sn命令与get_device_info命令的产线用法。实际效果:sn write会把序列号写入 Flash 的cfg分区并掉电保存;sn read读回验证;get_device_info显示设备身份;sn erase清空序列号。
注意:
sn write要求序列号恰好 20 个字符,且只接受字母/数字。若输入长度不对会提示SN char number is not euqal 20!。
五、SN 的适配(接入新板卡)
5.1 先认识适配层:SN 设备怎么"长"到新板卡上
drivers/sn/sn_cfg.c 是板级配置型设备(非抽象框架),它通过宏和引脚表与具体板卡耦合。接入新板卡,只需配置三处:① 序列号分区宏(SN_PARTITION_NAME/SN_ADDR_OFFSET/SN_LENGTH);② 板卡号/槽位号引脚表(board_no_pin_tab/slot_no_pin_tab);③ 确保 cfg 分区在 FAL 分区表中定义。函数逻辑不用改。
适配全景图:
┌──────────────────────────────────────────────────────────┐
│ 应用/产线(sn / get_device_info 命令) │
└──────────────────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ SN 设备(drivers/sn/sn_cfg.c,配置型,改宏/引脚表) │
│ sn_read/write/erase + get_device_info + 引脚拼接 │
└───────────────┬──────────────────────────┬────────────────┘
│ ①配置宏/引脚表 │ ②FAL cfg 分区
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ FAL 分区层 + 板级引脚表(board 头文件)★需配置★ │
│ cfg 分区定义 + board_no_pin_tab/slot_no_pin_tab │
└───────────────┬──────────────────────────┬────────────────┘
│ fal / os_pin │
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ 硬件(Flash 存储 + 板卡号/槽位号跳线 GPIO) │
└──────────────────────────────────────────────────────────┘适配的关键洞察:SN 设备逻辑固定,接入新板卡只需改配置(宏 + 引脚表),不用改代码。核心依赖:FAL 的 cfg 分区必须存在、板卡号/槽位号引脚必须配置为输入。
5.2 接口契约:接入新板卡需要提供什么
| 层 | 内容 | 职责 | 说明 |
|---|---|---|---|
| 宏 | SN_PARTITION_NAME | 序列号所在 FAL 分区名 | 默认 "cfg" |
| 宏 | SN_ADDR_OFFSET/SN_LENGTH | 分区内偏移 / 序列号长度 | 默认 0 / 20 |
| 引脚表 | board_no_pin_tab/slot_no_pin_tab | 板卡号/槽位号 GPIO 列表 | board 头文件定义 |
| 分区 | cfg 分区 | FAL 分区表里定义 | 无则 sn_* 报 invalide fal part |
| 引脚初始化 | board_no_pin_init | 配引脚为输入 | OS_INIT_CALL 自动调用 |
5.3 接入新板卡的标准流程
第 1 步:确认 Flash 与分区——确认板上 Flash 已通过 FAL 注册,且 FAL 分区表(fal_cfg.c)里有 cfg 分区(放序列号)。
第 2 步:配置序列号宏——若序列号长度/位置不同,改 sn_cfg.c 顶部的 SN_LENGTH/SN_ADDR_OFFSET/SN_PARTITION_NAME。
第 3 步:定义板卡号/槽位号引脚表——在 board 头文件定义 board_no_pin_tab/slot_no_pin_tab(数组 + *_size),列出读板卡号/槽位号的 GPIO。
第 4 步:测试——sn write <20位> 写入并 sn read 验证;get_device_info 验证板卡号/槽位号。
5.4 适配检查清单(快速自查)
| 检查项 | 说明 |
|---|---|
cfg 分区在 FAL 表里? | 无则 sn_read/write/erase 报 invalide fal part |
SN_LENGTH 与实际一致? | 序列号长度改后,sn write 的长度检查(strlen==20)需同步 |
| 板卡号/槽位号引脚表对? | board_no_pin_tab/slot_no_pin_tab 与跳线 GPIO 对应,错则编号错 |
| 引脚配成输入了? | board_no_pin_init 配 PIN_MODE_INPUT,未配读到错误电平 |
| 依赖 FAL/GPIO 启用? | 未启用 fal/os_pin 则无法读写/读引脚 |
📝 本节小结
核心要点回顾
本节围绕 SN 序列号设备展开,核心要点如下:
1. 存储机制
- 序列号存储在 FAL 的
"cfg"分区,从偏移 0 开始,长度 20 字节。 - 用 FAL 分区隔离序列号与固件分区,升级固件不丢序列号。
2. 三个核心内部函数
| 函数 | 作用 | 关键流程 |
|---|---|---|
sn_read | 读取序列号并打印 | 找分区 → 申请缓冲 → fal_part_read → char_dump |
sn_write | 写入序列号 | 找分区 → 解锁 → 擦除一块 → fal_part_write → 加锁 |
sn_erase | 擦除序列号 | 找分区 → 解锁 → 擦除一块 → 加锁 |
3. Shell 命令
sn read/sn write <20位>/sn erase:产线烧录与验证。get_device_info:显示序列号、SoC 型号、编译时间、板卡号、槽位号。
4. 关键技术点
- Flash 只能把 1 写成 0,写入前必须先擦除(
sn_write内部先fal_part_erase)。 - 板卡号/槽位号通过多个 GPIO 引脚电平拼接成二进制数读出。
5. 硬件适配
- 接入新板卡 = 配置序列号宏 + 定义板卡号/槽位号引脚表 + 确保 FAL
cfg分区存在,函数逻辑不用改。 - 依赖
fal、os_pin子系统,cfg分区必须有。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
sn write 输入长度不是 20 | sn write 必须恰好 20 个字符,否则报 SN char number is not euqal 20! |
忘记定义 cfg FAL 分区 | sn_read/write/erase 依赖 cfg 分区存在,否则返回 invalide fal part |
| 混淆擦除与写入 | Flash 写入前必须先擦除(sn_write 内部先擦除一块再写);直接覆盖会失败 |
误解 ? 显示 | char_dump 把 0xff(擦除后默认值)和不可打印字节显示为 ?,表示该位置无有效数据 |
| 板卡号引脚未配置 | get_device_info 依赖 board_no_pin_init 把引脚配成输入;未配置会读到错误电平 |
忘配 cfg FAL 分区 | sn_* 依赖 cfg 分区,FAL 分区表无它则报 invalide fal part |
| 引脚表与跳线不对应 | board_no_pin_tab/slot_no_pin_tab 顺序/引脚错则读出的板卡/槽位号错 |
下一步
掌握了 SN 序列号设备后,接下来学习 Cache 缓存设备:
- 5.32 Cache设备:了解 OneOS 数据缓存清理/无效化操作与 DMA 缓存一致性原理。