尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Nordic nRF54L15 UICR寄存器应用指南:存储关键数据的嵌入式解决方案

Nordic nRF54L15 UICR寄存器应用指南:存储关键数据的嵌入式解决方案 1. 项目缘起为什么需要UICR寄存器在嵌入式开发尤其是基于Nordic nRF系列芯片的项目中我们经常会遇到一个看似简单却至关重要的需求如何让设备记住一些关键信息即使在完全断电、重新烧录程序之后这些信息依然能够保留下来你可能会立刻想到几种常见方案使用外部EEPROM、挂载一个SPI Flash芯片或者利用芯片内部的Flash模拟EEPROM。这些方案当然可行但它们都引入了额外的复杂度——需要驱动、占用GPIO、增加BOM成本或者消耗宝贵的Flash擦写寿命。对于Nordic nRF系列芯片特别是其新一代的nRF Connect SDKNCS开发环境所支持的nRF54L15等型号其实提供了一个被许多开发者忽略的“宝藏”区域用户信息配置寄存器。UICR全称User Information Configuration Registers是芯片内部一块特殊的非易失性存储区域。它的“特殊”之处在于它和程序代码存储的Flash区域是物理隔离的但同样具备掉电不丢失的特性。你可以把它想象成芯片出厂时自带的一个“小本子”专门留给开发者写一些“用户信息”。这个“小本子”的容量不大通常只有几十到几百个字节但对于存储设备序列号、蓝牙MAC地址、硬件版本号、生产校准参数、启动配置标志等关键数据来说已经绰绰有余。我最近在一个基于nRF54L15的多传感器节点项目中就遇到了这个需求。设备出厂前需要写入唯一的设备ID和一组经过校准的传感器偏移量。如果使用外部存储不仅增加成本在紧凑的PCB布局上也难以实现。而使用主Flash模拟存储又担心在固件升级OTA时擦写操作误伤这些数据。这时UICR就成了最优雅的解决方案。它独立于应用程序区常规的固件烧录和擦除操作不会影响到UICR中的数据除非你明确地对其进行编程。2. 深入理解UICR不仅仅是寄存器在开始动手之前我们需要先厘清一个概念UICR虽然名字里带着“寄存器”但它和我们常说的CPU通用寄存器R0, R1...或外设控制寄存器有着本质区别。2.1 UICR的物理本质与访问特性UICR在物理上属于非易失性存储器更具体地说它是芯片内部Flash存储器的一个特殊分区。这意味着掉电数据不丢失写入的数据会永久保存直到下一次被擦除并重新编程。按字/双字编程你不能像操作RAM一样随意修改某个字节。对UICR的写入必须以“字”32位或“双字”64位取决于芯片为单位进行并且目标地址必须对齐。需要先擦后写Flash存储器的特性决定了在写入新数据前目标存储单元必须处于已擦除状态通常所有位为1即0xFFFFFFFF。如果该位置已有数据非0xFFFFFFFF直接写入会导致硬件错误或数据错误。擦除粒度大与细粒度的写入不同UICR的擦除通常以“页”或整个UICR区域为单位。这意味着更新一个数据可能需要擦除一大片区域在设计数据结构时需要谨慎。2.2 nRF54L15的UICR内存映射以nRF54L15为例我们查看其芯片手册或NCS中的内存映射头文件如zephyr/include/dt-bindings/clock/nrf54l15_cpuapp_peripherals.h或类似文件可以找到UICR的基地址和结构定义。通常UICR区域被划分为多个预定义字段和用户可用字段。预定义字段可能被芯片用于存储工厂校准值、保密配置等。用户可用字段才是我们可以自由使用的部分。这些字段在内存映射中表现为固定的地址偏移。例如UICR-CUSTOMER[0]到UICR-CUSTOMER[N]这是一组专门留给用户使用的32位寄存器。其他如XTALFREQ、HFCLKSRC等则用于配置芯片启动时的时钟源不建议用户随意更改。在编程时我们通过访问这些绝对内存地址来读写数据。例如*(volatile uint32_t *)0x10001080可能就指向UICR-CUSTOMER[0]。注意直接使用魔术数字地址如0x10001080是极不推荐的这会导致代码可移植性差且难以维护。正确做法是使用NCS提供的设备树DTS绑定和头文件宏定义来获取这些地址。2.3 UICR与NV存储的对比为了更清晰地理解UICR的定位我们将其与其它常见的非易失性存储方案做个对比特性UICRSettings子系统 (Zephyr)外部EEPROMFlash模拟EEPROM (FS/NVS)存储介质芯片内部Flash特殊区域芯片内部Flash通常外部I2C/SPI芯片芯片内部主Flash区容量很小 (几十~几百字节)中等 (几KB ~ 几十KB)可配置 (几KB ~ 几MB)较大 (取决于分区大小)读写速度快 (直接内存访问)中等 (经过文件系统/管理层)慢 (受总线速度限制)慢 (需擦除、写入)擦写寿命与Flash相同 (约1万次)与Flash相同 (约1万次)很高 (百万次级)与Flash相同 (约1万次)功耗极低 (无需外设)低较高 (需总线供电)低数据持久性极高 (独立分区OTA安全)高 (但OTA时需特殊处理)高中高 (OTA可能覆盖)使用复杂度低(直接地址访问)中 (需学习Settings API)中 (需驱动、寻址)中高 (需处理磨损均衡、坏块)典型应用设备唯一ID、校准值、启动标志系统配置、用户偏好大量日志、配置文件频繁更新的小数据、系统参数从这个对比可以看出UICR的核心优势在于极简和安全。它省去了所有中间层和外部器件提供了最直接、最可靠的小数据存储方案尤其适合那些在设备生命周期内几乎不变的关键数据。3. 实战在NCS中读取与写入UICR理论讲完我们进入实战环节。在NCS基于Zephyr RTOS环境下操作UICR不能像在裸机程序中那样直接进行位操作。我们需要遵循Zephyr的设备驱动模型通过正确的API来访问。3.1 环境准备与设备树配置首先确保你的NCS项目配置正确。UICR的访问通常由nrfx_nvmc驱动或类似的Flash驱动提供支持。在大多数情况下对于标准用户UICR字段NCS已经做好了底层抽象我们无需在设备树中显式配置。但是为了以最规范、可移植的方式访问UICR我强烈建议使用设备树绑定来定义你的自定义数据位置。这允许你将数据地址的“硬编码”转移到设备树文件中使代码与具体硬件解耦。创建自定义设备树绑定 在项目目录下创建dts/bindings/foo/your-company,uicr-customer.yaml文件。description: Your Company UICR customer registers compatible: your-company,uicr-customer include: base.yaml properties: reg: type: array description: Address and size of the UICR customer registers required: true在设备树源文件中引用 在你的板级设备树文件如boards/arm/your_board/your_board.dts或应用覆盖层文件app.overlay中添加节点。/ { your_uicr_data: your-uicr-data { compatible your-company,uicr-customer; reg 0x10001080 0x10; /* 例如使用CUSTOMER[0], CUSTOMER[1], CUSTOMER[2], CUSTOMER[3] 共4个寄存器16字节*/ }; };这里0x10001080是起始地址0x10是长度16字节。你需要根据芯片手册确定可用的UICR-CUSTOMER基地址。在C代码中通过设备树获取地址#include zephyr/device.h #include zephyr/devicetree.h #define UICR_DATA_NODE DT_NODELABEL(your_uicr_data) // 或 DT_COMPAT_GET_ANY_STATUS_OKAY static const uint32_t *uicr_customer_regs (const uint32_t *)DT_REG_ADDR(UICR_DATA_NODE); static const size_t uicr_customer_regs_count DT_REG_SIZE(UICR_DATA_NODE) / sizeof(uint32_t);现在uicr_customer_regs就是一个指向你定义的UICR内存区域的指针数组uicr_customer_regs_count是寄存器的数量。3.2 安全读取UICR数据读取操作是安全的不会改变存储内容。你可以像读取常量数组一样读取UICR。uint32_t read_uicr_customer_reg(size_t index) { if (index uicr_customer_regs_count) { printk(Error: UICR register index out of bounds!\n); return 0xFFFFFFFF; // 或其它错误标识 } // 直接解引用指针即可。注意UICR在内存中是只读的必须使用const指针。 uint32_t value uicr_customer_regs[index]; printk(UICR CUSTOMER[%d] 0x%08lx\n, index, value); return value; } void read_all_uicr_data(void) { printk(Reading UICR customer registers:\n); for (size_t i 0; i uicr_customer_regs_count; i) { uint32_t val read_uicr_customer_reg(i); if (val 0xFFFFFFFF) { printk( [%d]: 0xFFFFFFFF (Erased)\n, i); } else { printk( [%d]: 0x%08lx\n, i, val); } } }3.3 谨慎写入UICR数据写入是危险操作必须严格遵守“先擦后写”且“只能写一次”的规则。在NCS中我们使用Flash写操作相关的API。通常nrfx_nvmc驱动提供了nrfx_nvmc_uicr_word_write或类似的函数。但在Zephyr框架下更通用的方式是使用Flash Map API和Flash Driver API。不过对于UICR这种特殊区域Nordic提供了更直接的封装。查找NCS中是否有类似include/nrfx/nrfx_nvmc.h的头文件。一个典型的写入流程如下#include nrfx_nvmc.h int write_uicr_customer_reg(size_t index, uint32_t data) { if (index uicr_customer_regs_count) { return -EINVAL; } // 1. 获取目标地址 uint32_t *target_address (uint32_t *)uicr_customer_regs[index]; // 注意需要去掉const但操作需谨慎 // 更安全的方式是直接计算地址 uint32_t target_addr DT_REG_ADDR(UICR_DATA_NODE) (index * sizeof(uint32_t)); // 2. 检查是否已擦除全为1 uint32_t current_value *(volatile uint32_t *)target_addr; if (current_value ! 0xFFFFFFFF) { printk(Error: UICR register at 0x%08lx is not erased (value0x%08lx). Cannot write.\n, target_addr, current_value); return -EACCES; // 权限错误表示需要先擦除 } // 3. 执行写入操作 // 注意此函数可能因芯片和SDK版本而异请查阅对应版本的文档 nrfx_err_t err nrfx_nvmc_uicr_word_write(target_addr, data); if (err ! NRFX_SUCCESS) { printk(Failed to write UICR at 0x%08lx, error: %d\n, target_addr, err); return -EIO; } printk(Successfully wrote 0x%08lx to UICR[%d] at 0x%08lx\n, data, index, target_addr); return 0; }关键点擦除操作如果目标位置不是0xFFFFFFFF你需要擦除整个UICR页或区域。擦除操作通常通过nrfx_nvmc_uicr_erase或nrfx_nvmc_page_erase针对特定地址完成。擦除UICR会导致所有用户UICR数据丢失且可能影响芯片的启动配置务必在明确知晓后果的情况下进行最好在芯片首次编程或工厂生产环节完成。写入时机UICR写入通常需要在系统初始化早期、中断未启用时进行因为Flash操作可能会暂时阻塞CPU。在Zephyr中可以考虑在main()函数开始或一个高优先级、单次执行的线程中完成。4. 生产环节使用nrfutil工具离线编程UICR在工厂量产时我们不可能为每个设备都运行一次完整的应用程序来写入UICR。这时就需要借助Nordic提供的强大命令行工具——nrfutil在烧录固件的同时将UICR数据一并写入。nrfutil可以生成一个包含应用程序、引导加载程序、以及UICR设置等所有内容的“合并hex文件”或直接通过DFU设备固件更新包进行编程。4.1 准备UICR数据文件首先你需要创建一个文本文件例如uicr_data.hex来定义要写入UICR的内容。格式遵循标准的Intel HEX格式但通常我们使用更简单的方式用nrfutil命令生成。更常见的做法是创建一个.json配置文件来指定UICR内容{ uicr: { uicr_customer: { 0: 0x12345678, 1: 0xABCDEF00, 2: 0xA5A5A5A5 } } }这个JSON文件定义了要写入UICR-CUSTOMER[0],[1],[2]的值。4.2 使用nrfutil生成并烧录完整镜像假设你已经编译生成了应用程序的hex文件app.hex和引导程序的hex文件bl.hex。生成包含UICR设置的Hex文件nrfutil settings generate --family NRF54L15 --application app.hex --application-version 1 --bootloader bl.hex --bl-settings-version 2 uicr_data.hex # 注意上述命令主要生成引导加载程序设置。对于自定义UICR可能需要使用nrfutil pkg generate并指定--uicr-json-file参数具体请参考nrfutil最新文档。 # 一个更直接的方式是使用mergehex工具合并 mergehex --merge app.hex bl.hex uicr_custom.hex --output combined.hex其中uicr_custom.hex是你通过其他方式如手动编写或脚本生成创建的只包含UICR数据的hex文件。使用nrfjprog烧录nrfjprog --family NRF54L15 --program combined.hex --sectorerase --reset这条命令会将合并后的hex文件烧录到芯片并擦除相应扇区最后复位设备。实操心得在生产线上通常会将“应用程序”、“引导程序”、“UICR数据”、“蓝牙设备地址”等所有静态内容合并成一个最终的“生产镜像”factory_image.hex。烧录器只需要烧录这一个文件极大简化了生产流程也避免了多次烧录可能带来的顺序错误。可以使用mergehex工具或编写脚本自动化完成此合并过程。4.3 验证烧录结果烧录完成后可以通过nrfjprog读取内存来验证nrfjprog --family NRF54L15 --memrd 0x10001080 --n 4这个命令会读取从地址0x10001080开始的4个字16字节内存并显示其值确保与你期望写入的UICR数据一致。5. 避坑指南与高级技巧在实际使用UICR的过程中我踩过不少坑也总结出一些让方案更稳健的技巧。5.1 坑一误擦除与数据完整性问题UICR区域与Flash主存储区是分开的但使用一些全芯片擦除命令如nrfjprog --eraseall时可能会一并擦除UICR。此外如果应用程序错误地调用了Flash擦除API并传入了UICR的地址范围也会导致数据丢失。对策代码保护在应用程序中将对UICR的写/擦除操作封装在独立的、条件编译严格的模块中。默认的应用程序构建不包含写UICR的代码仅包含读操作。生产流程隔离将写入UICR的步骤作为工厂生产流程的独立一环与固件烧录分开或严格排序。使用独立的、经过验证的生产脚本。添加数据校验在UICR中存储数据时不要只存原始数据。可以增加CRC32校验和或简单的魔法数字Magic Number。每次读取时先验证校验和如果无效则使用默认值或触发错误恢复流程。typedef struct { uint32_t magic; // 例如 0xDEADBEEF uint32_t device_id; float calibration_factor; uint32_t crc; // 前面所有字段的CRC32值 } uicr_data_t;5.2 坑二地址对齐与跨字数据问题Flash写入要求地址对齐32位或64位。如果你想存储一个8位的序列号或一个字符串直接写入可能会遇到对齐问题。对策数据打包将小于32位的数据打包到一个32位字中。例如将四个8位的版本号主、次、修订、构建打包uint32_t version_packed (major 24) | (minor 16) | (revision 8) | (build); write_uicr_customer_reg(0, version_packed);使用联合体利用C语言的union来方便地在不同数据类型和存储格式间转换。union uicr_entry { uint32_t word; uint8_t bytes[4]; struct { uint16_t param1; uint16_t param2; } halves; };5.3 技巧实现一个简易的UICR管理器为了提升代码的可用性和安全性我建议抽象出一个简单的UICR管理器模块uicr_mgr.c/uicr_mgr.h。这个模块提供以下功能初始化读取所有配置的UICR寄存器验证数据有效性如CRC并将有效数据加载到RAM中的结构体。只读访问为应用程序提供干净的API来获取设备ID、校准值等隐藏底层地址细节。工厂编程模式通过编译开关如CONFIG_UICR_FACTORY_PROGRAMMING启用写功能。在此模式下模块可以提供API来擦除和编程UICR并自动计算和写入校验码。状态查询提供接口查询UICR数据是否有效、是否已被编程过。这样应用程序其他部分完全不需要关心数据存在哪里、如何读取只需要调用uicr_mgr_get_device_id()这样的函数即可实现了良好的解耦。5.4 与Zephyr Settings子系统的结合思考你可能会想Zephyr已经有了强大的Settings子系统来管理非易失性配置为什么还要直接用UICR它们并不冲突而是可以协作。Settings用于“可变数据”存储网络配置、用户偏好、运行状态等可能改变的数据。Settings底层可能使用Flash模拟的EEPROMNVS它处理了磨损均衡、掉电保护等复杂问题。UICR用于“固化数据”存储设备身份标识、工厂校准参数、硬件版本等“只写一次、永久使用”的数据。在系统启动时可以先从UICR中读取设备唯一ID和校准参数然后将这些参数作为只读常量提供给应用程序或者甚至作为Settings的默认值。这种分层存储策略兼顾了灵活性和可靠性。6. 案例为nRF54L15设备注入唯一身份让我们用一个完整的微型案例来串联上述知识。目标在nRF54L15的UICR-CUSTOMER[0]和CUSTOMER[1]中存储一个64位的唯一设备标识符。步骤一设计数据结构我们决定用两个32位寄存器存储一个64位ID。CUSTOMER[0]存储ID的高32位CUSTOMER[1]存储低32位。同时我们在CUSTOMER[2]存储一个魔法数字如0x55AA55AA用于简单验证。步骤二生产编程脚本编写一个Python脚本调用nrfutil/mergehex将应用程序hex、引导程序hex和一个包含特定设备ID的UICR hex文件合并。设备ID可以从数据库或序列号生成器中获取。步骤三应用程序读取与使用在应用程序早期初始化代码中#include uicr_mgr.h void main(void) { // 初始化UICR管理器 if (uicr_mgr_init() ! 0) { // 初始化失败可能是UICR数据无效进入安全模式或使用默认值 printk(Warning: Invalid UICR data. Using default ID.\n); // 可以尝试从备份存储读取或生成一个运行时ID } // 获取设备唯一ID uint64_t device_id uicr_mgr_get_device_id(); printk(Device Unique ID: 0x%016llx\n, device_id); // 将ID用于蓝牙设备名称、网络标识等 char dev_name[32]; snprintf(dev_name, sizeof(dev_name), Sensor-%016llX, device_id); // ... 设置蓝牙设备名称 ... // 后续应用程序逻辑 while (1) { // ... } }步骤四处理异常情况在uicr_mgr_init()函数内部如果发现魔法数字不匹配或者读取到的ID是全0/全F等非法值则判定为UICR未编程或已损坏。管理器可以返回错误码并提供一个备用的ID生成方案例如读取芯片内部唯一的FICR地址如DEVICEID[0]和DEVICEID[1]来组合成一个ID。通过这个案例你将UICR从一个生硬的技术概念转变为了支撑产品关键功能设备标识的可靠基石。它简洁、高效并且与生产流程完美融合。
返回列表