CMSIS-NN 使用方式与典型场景:两个可运行示例
使用方式与典型场景:两个可运行示例
下面两个例子都不依赖 TFLM,直接调用 CMSIS-NN 算子,适合「模型已经量化、参数已经由转换器导出、只想在固件里手工跑一遍」的场景。
示例一:一层 int8 全连接的完整调用
场景:把 32 维特征映射到 8 类输出,输入输出都是 int8。关键是四组 dims 与两组量化参数都要给全。
#include <arm_nnfunctions.h>
#define IN 32
#define OUT 8
static int8_t input_data[IN];
static int8_t weight_data[OUT * IN]; /* 按输出通道优先的布局排列 */
static int32_t bias_data[OUT];
static int8_t output_data[OUT];
/* 由转换器导出的 per-channel 量化参数 */
static const int32_t fc_mult[OUT] = { /* ... */ };
static const int32_t fc_shift[OUT] = { /* ... */ };
void fc_infer(void)
{
cmsis_nn_context ctx = { NULL, 0 }; /* 全连接不需要 scratch */
cmsis_nn_fc_params fc_params;
fc_params.input_offset = -1; /* 输入零点取负 */
fc_params.filter_offset = 0; /* 权重零点 */
fc_params.output_offset = 3; /* 输出零点 */
fc_params.activation.min = -128;
fc_params.activation.max = 127;
cmsis_nn_per_channel_quant_params quant_params;
quant_params.multiplier = (int32_t *)fc_mult;
quant_params.shift = (int32_t *)fc_shift;
cmsis_nn_dims input_dims = { 1, 1, 1, IN };
cmsis_nn_dims filter_dims = { OUT, 1, 1, IN };
cmsis_nn_dims bias_dims = { 1, 1, 1, OUT };
cmsis_nn_dims output_dims = { 1, 1, 1, OUT };
arm_fully_connected_s8(&ctx, &fc_params, &quant_params,
input_dims, input_data,
filter_dims, weight_data,
bias_dims, bias_data,
output_dims, output_data);
}
注意两点:input_offset 是零点取负,不是直接填零点;cmsis_nn_fc_params 在不同版本里的字段有过增删(例如批量支持、filter_offset 的取舍),换版本时第一件事是打开 arm_nn_types.h 核对字段名。
示例二:深度可分离卷积加 Softmax 的推理片段
场景:关键词唤醒一类的音频前端,输入是单通道的特征图,先做逐通道卷积,再用一次 Softmax 出分类概率。
#include <arm_nnfunctions.h>
#define IN_H 16
#define IN_W 10
#define CH 8
#define KH 3
#define KW 3
#define OUT_H (IN_H - KH + 1)
#define OUT_W (IN_W - KW + 1)
static int8_t in_tensor[IN_H * IN_W * CH];
static int8_t dw_weight[KH * KW * CH];
static int32_t dw_bias[CH];
static int8_t dw_out[OUT_H * OUT_W * CH];
static int8_t prob_out[OUT_H * OUT_W * CH];
void dw_infer(void)
{
cmsis_nn_context ctx = { NULL, 0 }; /* 非 _opt 版本不用 scratch */
cmsis_nn_conv_params conv_params;
conv_params.stride.h = 1;
conv_params.stride.w = 1;
conv_params.padding.h = 0;
conv_params.padding.w = 0;
conv_params.dilation.h = 1;
conv_params.dilation.w = 1;
conv_params.input_offset = -1;
conv_params.output_offset = 2;
conv_params.activation.min = -128;
conv_params.activation.max = 127;
cmsis_nn_per_channel_quant_params quant_params;
quant_params.multiplier = (int32_t *)dw_mult;
quant_params.shift = (int32_t *)dw_shift;
cmsis_nn_dims in_dims = { 1, IN_H, IN_W, CH };
cmsis_nn_dims flt_dims = { 1, KH, KW, CH }; /* 逐通道:滤波个数为 1 */
cmsis_nn_dims bias_dims = { 1, 1, 1, CH };
cmsis_nn_dims out_dims = { 1, OUT_H, OUT_W, CH };
arm_depthwise_conv_s8(&ctx, &conv_params, &quant_params,
in_dims, in_tensor,
flt_dims, dw_weight,
bias_dims, dw_bias,
out_dims, dw_out);
/* 分类头:把特征图按行展开后做 Softmax */
arm_softmax_s8(dw_out, 1, OUT_H * OUT_W * CH,
softmax_mult, softmax_shift, softmax_diff_min,
prob_out);
}
改用 arm_depthwise_conv_s8_opt 时,ctx.buf 与 ctx.size 必须按库给出的缓冲区大小辅助函数填好,否则算子会越界写。另外,arm_softmax_s8 的乘子与移位是 Softmax 自己的一套,不是上一层的,阈值参数用于把过小的输入直接判零、避免指数运算溢出——这些值同样从转换器导出,不要自己拍。
适合与不适合的场景。
- 适合:Cortex-M 上的分类、关键词唤醒、简单手势识别、异常检测这类小模型;模型已经量化到 int8;用到的算子落在 CMSIS-NN 的覆盖范围内(卷积、深度可分离卷积、全连接、池化、Softmax、逐元素运算)。
- 不适合:需要在设备上训练或微调;模型里有大量 CMSIS-NN 未覆盖的算子(LSTM、注意力、动态 shape 操作);精度不允许 int8 量化的场合。这些情况要么换更完整的推理框架,要么自己补算子实现。

