3. 静态库集成与内存布局优化

任务目标

在STM32F407裸机工程中,将CMSIS-NN编译为静态库(.a)并链接,通过调整链接脚本与变量属性,将神经网络的权重、输入/输出缓冲区分别放置到内部Flash、DTCM与AXI SRAM,最终通过map文件确认各段内存布局符合预期。

环境与依赖

  • 硬件:STM32F407VET6(192KB SRAM:128KB AXI SRAM + 64KB DTCM,1MB Flash)
  • 工具链:arm-none-eabi-gcc 10.3+,Makefile构建
  • CMSIS-NN:从CMSIS 5.9.0提取 CMSIS/NN/Source 与 CMSIS/DSP/Include(arm_math.h依赖)
  • HAL驱动:仅需启动文件与系统时钟初始化(使用STM32CubeF4固件包中的system_stm32f4xx.c)

实现步骤

1. 构建静态库

创建 lib_nn.mk 编译CMSIS-NN全部.c文件为静态库 libcmsisnn.a:


# lib_nn.mk
NN_SRC = $(wildcard $(NN_DIR)/Source/*.c)
NN_OBJ = $(patsubst %.c,%.o,$(NN_SRC))
libcmsisnn.a: $(NN_OBJ)
	arm-none-eabi-ar rcs $@ $^
%.o: %.c
	arm-none-eabi-gcc -c $< -O2 -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 \
	-I$(NN_DIR)/Include -I$(DSP_DIR)/Include -o $@

2. 内存布局定义

在链接脚本 stm32f407.ld 中显式划分三个SRAM区域:


MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 1M
  DTCM  (rw)  : ORIGIN = 0x20000000, LENGTH = 64K
  AXI   (rw)  : ORIGIN = 0x20010000, LENGTH = 128K
}
SECTIONS
{
  .nn_weights : { *(.nn_weights) } > FLASH
  .bss.dtcm   : { *(.bss.dtcm) }   > DTCM
  .bss.axi    : { *(.bss.axi) }    > AXI
}

3. 关键代码与属性标注

以下代码定义了一个2层全连接网络(输入8→隐藏16→输出4),使用CMSIS-NN的 arm_fully_connected_s8 接口,并通过变量属性控制内存放置:


#include "arm_nnfunctions.h"
#include "arm_nnsupportfunctions.h"

// 权重与偏置放入Flash(.nn_weights段)
const int8_t w1[128] __attribute__((section(".nn_weights"), aligned(16))) = {
    // 8x16 量化权重,取值范围[-3,3]
    1,-2,0,3,-1,2,-3,1, 0,2,-1,3,-2,1,0,-1,
    -3,1,2,0,-1,-2,3,1, 2,0,-3,1,-1,2,0,3,
    1,0,-2,3,-1,2,1,-3, 0,1,3,-2,2,-1,0,1,
    -2,3,0,-1,1,-3,2,0, 3,-1,1,2,0,-2,3,-1,
    0,2,-3,1,-1,3,0,-2, 1,-1,2,0,3,-2,1,0,
    -1,3,1,-2,0,2,-3,1, 2,0,-1,3,-2,1,0,-1,
    3,-1,0,2,-2,1,-3,0, 1,2,-1,0,3,-2,1,-3,
    0,-2,3,-1,1,0,2,-3, -1,3,0,2,-2,1,0,3
};
const int32_t b1[16] __attribute__((section(".nn_weights"))) = {0};
const int8_t w2[64] __attribute__((section(".nn_weights"), aligned(16))) = {
    // 16x4 量化权重
    1,-2,0,3, -1,2,-3,1, 0,2,-1,3, -2,1,0,-1,
    -3,1,2,0, -1,-2,3,1, 2,0,-3,1, -1,2,0,3,
    1,0,-2,3, -1,2,1,-3, 0,1,3,-2, 2,-1,0,1,
    -2,3,0,-1, 1,-3,2,0, 3,-1,1,2, 0,-2,3,-1
};
const int32_t b2[4] __attribute__((section(".nn_weights"))) = {0};

// 输入/输出缓冲区:输入放DTCM,中间结果与输出放AXI SRAM
static int8_t input[8]  __attribute__((section(".bss.dtcm"), aligned(16)));
static int8_t hidden[16] __attribute__((section(".bss.axi"), aligned(16)));
static int8_t output[4] __attribute__((section(".bss.axi"), aligned(16)));

// 量化参数(scale = 0.1, offset = 0)
static const int16_t in_scale = 10, out_scale = 10;

int nn_inference(const int8_t *raw_input, int8_t *result) {
    // 复制输入到DTCM缓冲区
    for (int i = 0; i < 8; i++) input[i] = raw_input[i];

    // 第1层:8→16
    arm_fully_connected_s8(input, w1, b1, 8, 16, 0, 0, in_scale, 0, out_scale, 0, hidden);
    // 第2层:16→4
    arm_fully_connected_s8(hidden, w2, b2, 16, 4, 0, 0, in_scale, 0, out_scale, 0, output);

    for (int i = 0; i < 4; i++) result[i] = output[i];
    return 0;
}

4. 主程序与验证入口


#include <stdio.h>
extern int nn_inference(const int8_t*, int8_t*);

// 通过map文件验证地址:&input应在0x20000000-0x2000FFFF,
// &hidden/output应在0x20010000-0x2002FFFF,w1/w2在0x08000000之后
int main(void) {
    int8_t test_input[8] = {1, -2, 3, 0, -1, 2, -3, 1};
    int8_t result[4];
    nn_inference(test_input, result);
    // 此处可设置断点查看result值,或通过串口打印
    while(1);
}

编译与链接


# 编译主程序并链接静态库
arm-none-eabi-gcc -c main.c -O2 -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -I$(NN_DIR)/Include -o main.o
arm-none-eabi-gcc main.o -L. -lcmsisnn -T stm32f407.ld -nostartfiles -Wl,--gc-sections -o nn_test.elf
arm-none-eabi-objdump -h nn_test.elf | grep -E "nn_weights|bss.dtcm|bss.axi"

验证方法与通过条件

  1. 段地址检查:执行上述 objdump 命令,确认输出中 .nn_weights 的地址在 0x08000000 之后(Flash区域),.bss.dtcm 在 0x20000000-0x2000FFFF,.bss.axi 在 0x20010000-0x2002FFFF。
  2. 符号地址验证:在调试器中查看 &input、&hidden、&w1 的地址,应分别落在上述三个区域范围内。
  3. 功能正确性:在 main 的 while(1) 处设置断点,单步执行后检查 result 数组值。由于权重全为0偏置(b1/b2全0),且输入非零,输出应全为0(量化后0*scale=0)。若输出非0,说明内存访问越界或链接配置错误。
预期输出说明:由于本案例权重和偏置均初始化为0,无论输入如何,两层全连接的计算结果恒为0。此设计用于隔离验证内存布局正确性——若输出非0,则说明缓冲区地址重叠或权重段未正确映射到Flash。

常见问题

  • 链接报错“region DTCM overflowed”:检查是否有其他未标注的全局变量默认放入DTCM。在链接脚本中增加 *(.bss*) 到AXI区域,或使用 -fdata-sections 配合 --gc-sections 丢弃未用变量。
  • CMSIS-NN编译警告“arm_math.h not found”:确认DSP Include路径已通过 -I 正确传入,且CMSIS版本匹配(5.9.0的NN依赖DSP 1.10.0+)。
  • 性能不达标:检查 SCB->CPACR 是否已使能FPU访问(在system_stm32f4xx.c中默认开启),并确认编译选项包含 -mfloat-abi=hard -mfpu=fpv4-sp-d16。