3. SoftDevice内存资源规划
在nRF5 SDK工程中,SoftDevice并不是一个可选的库,而是一段预先烧录、拥有固定Flash与RAM地址范围的协议栈固件。应用程序能否正常启动、能否通过蓝牙协议栈收发数据,首先取决于链接脚本是否把代码和数据放到了SoftDevice“让出来”的区域。理解Flash与RAM的分区关系,是nRF52系列BLE工程能否稳定运行的第一道门槛。
Flash分区:从地址0开始的固定占用
以nRF52832搭配S132 SoftDevice为例,Flash从0x00000000开始,最前面是SoftDevice固件本体,紧接着是应用程序区,最后是引导程序(Bootloader)与MBR区域。SoftDevice的结束地址由芯片型号和协议栈版本共同决定,例如S132 v7.2.0在nRF52832上占用约0x00000000~0x00026000,即152 KB。应用程序的起始地址必须大于等于这个值,否则链接器会把代码写进SoftDevice区域,烧录后直接跑飞。
SDK通过sdk_config.h中的NRF_DFU_APP_START或链接脚本中的FLASH ORIGIN来体现这一约束。以Keil为例,Options for Target → Target → IROM1的Start应设置为0x26000,Size为0x52000(假设Flash总容量256 KB,Bootloader预留0x78000~0x80000)。若使用GCC,则修改.ld文件中的FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0x52000。
RAM分区:SoftDevice的“保留区”与应用程序的可用区
RAM的分区比Flash更隐蔽。SoftDevice在RAM中占用两部分:一是协议栈自身的静态数据与堆栈,二是它需要的一块“应用可见但不可随意使用”的保留区。以S132为例,SoftDevice从RAM起始地址0x20000000开始占用约0x00000000~0x00001628(约5.5 KB),随后是一段由RAM_START和RAM_SIZE定义的应用程序可用区。但真正容易出错的是:SoftDevice会在运行时使用应用程序RAM区顶部的一段空间作为协议栈的“共享内存”,这段空间的大小由sd_ble_enable()的p_app_ram_base参数决定。
典型做法是:在main()中调用nrf_sdh_enable_request()之前,先声明一个uint32_t变量app_ram_base,然后调用sd_ble_enable(&app_ram_base)。如果传入的app_ram_base为0,协议栈会返回NRF_ERROR_NO_MEM,并在app_ram_base中写回它实际需要的RAM起始地址。开发者需要把这个值填入链接脚本的RAM ORIGIN,或者使用SDK提供的nrf_sdh_ble_default_cfg_set()自动计算。
链接脚本配置:把分区约束写进工程
在Keil MDK中,IROM1与IRAM1的配置直接决定链接结果。假设使用S132 v7.2.0、nRF52832、无Bootloader,则IROM1 Start=0x26000、Size=0x5A000(256 KB减去152 KB),IRAM1 Start=0x20001628、Size=0x10000-0x1628=0xE9D8。若启用DFU,Bootloader会占用Flash末尾约32 KB,应用程序Size需再减去0x8000。
在GCC/SEGGER Embedded Studio中,链接脚本nrf52832_xxaa.ld的修改更为直观:
MEMORY
{
FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0x5A000
RAM (rwx) : ORIGIN = 0x20001628, LENGTH = 0xE9D8
}
注意:RAM ORIGIN不能简单写成0x20000000 + 0x1628,因为不同SDK版本、不同SoftDevice配置下这个偏移量会变化。更稳妥的做法是使用SDK提供的nrf_sdh_ble_default_cfg_set(),它会在运行时根据ble_cfg结构体计算出ram_start,并写入全局变量m_ram_start,链接脚本只需引用该符号即可。
内存冲突排查:从现象到根因
最常见的冲突现象有三种:一是程序下载后无法运行,调试器停在HardFault_Handler;二是蓝牙初始化返回NRF_ERROR_NO_MEM;三是运行一段时间后随机死机。第一种通常是因为Flash ORIGIN设小了,应用程序覆盖了SoftDevice;第二种是RAM ORIGIN设大了,协议栈共享内存与应用程序变量重叠;第三种往往是栈溢出或堆碎片导致。
排查时可按以下顺序操作:首先用nrfjprog --memrd 0x00000000 --n 4读取Flash起始地址,确认SoftDevice是否完整;其次在sd_ble_enable()后打印app_ram_base,与链接脚本中的RAM ORIGIN对比;最后用Keil的.map文件查看RW/ZI段结束地址是否超过RAM顶部。若app_ram_base大于链接脚本的RAM ORIGIN,说明应用程序占用了协议栈保留区,必须调大RAM ORIGIN或减少全局变量。
优化策略:在有限RAM中挤出空间
当NRF_ERROR_NO_MEM出现时,优先调整ble_cfg中的连接数与事件长度。例如将BLE_CONN_CFG_GAP的conn_cfg.conn_count从4降到2,ble_cfg.conn_cfg.params.gap_conn_cfg.event_length从320降到160,可显著减少共享内存占用。其次,检查sdk_config.h中的NRF_SDH_BLE_GATT_MAX_MTU_SIZE,从247降到23可节省约2 KB。最后,把大数组从.bss移到.noinit或使用__attribute__((section(".ram2")))手动放置,避免与协议栈保留区冲突。
Flash方面,若应用程序超出可用空间,可开启-Os优化、移除未使用的SDK模块、将常量表放入.rodata并启用链接器--gc-sections。对于必须保留的DFU功能,可考虑使用外部Flash存储升级包,而不是扩大Bootloader分区。
- Flash ORIGIN = SoftDevice结束地址(查SDK版本对应的
SD_SIZE宏) - RAM ORIGIN =
sd_ble_enable()返回的app_ram_base - IRAM1 Size = 总RAM - RAM ORIGIN - 协议栈共享内存
- 编译后查看
.map,确认ZI段结束地址 < RAM顶部 - 运行时打印
app_ram_base,与链接脚本对比
内存规划不是一次性的配置,而是随连接数、MTU、DFU需求动态调整的过程。每次修改ble_cfg或sdk_config.h后,都应重新确认app_ram_base,并同步更新链接脚本。只有Flash与RAM两个分区都严格对齐SoftDevice的边界,应用程序才能在协议栈之上稳定运行。