[STM32U5] 【NUCLEO-U575ZI-Q测评】使用轻量级AI推理框架TinyMaix实现手写数字识别

[复制链接]
4928|36
 楼主| xusiwei1236 发表于 2023-3-13 21:39 | 显示全部楼层 |阅读模式
本帖最后由 xusiwei1236 于 2023-3-13 22:02 编辑

【NUCLEO-U575ZI-Q测评】使用轻量级AI推理框架TinyMaix实现手写数字识别
一、TinyMaix简介
TinyMaix是国内sipeed团队开发一个轻量级AI推理框架,官方介绍如下:
TinyMaix 是面向单片机的超轻量级的神经网络推理库,即 TinyML 推理库,可以让你在任意单片机上运行轻量级深度学习模型。
根据官方介绍,在仅有2K RAM的 Arduino UNO(ATmega328, 32KB Flash, 2KB RAM) 上,都可以基于 TinyMaix 进行手写数字识别。对,你没有看错,2K RAM + 32K Flash的设备上都可以使用TinyMaix进行手写数字识别!TinyMaix官网提供了详细介绍,可以在本文末尾的参考链接中找到。
首先,我们看看这次试用的NUCLEO-U575ZI-Q开发板主控芯片——STM32U575ZIT6QU的主要参数:
  • CPU: Cortex-M33内核,160 MHz
  • Flash: 2 MB
  • RAM: 786 KB

所以,在我们这次试用的主角NUCLEO-U575ZI-Q开发板上运行TinyMaix完全是没有任何压力的。
接下来,我将介绍如何为STM32U575移植TinyMaix框架,以及如何运行手写数字识别示例。

1.1 TinyMaix开源项目
TinyMaix项目源码时以 Apache-2.0协议开源的,
GitHub代码仓:https://github.com/sipeed/tinymaix

1.2 TinyMaix核心API
TinyMaix框架对上层应用程序提供的核心API主要位于代码仓的tinymaix.h文件中,核心API如下:
  1. /******************************* MODEL FUNCTION ************************************/
  2. tm_err_t tm_load  (tm_mdl_t* mdl, const uint8_t* bin, uint8_t*buf, tm_cb_t cb, tm_mat_t* in);   //load model
  3. void     tm_unload(tm_mdl_t* mdl);                                      //remove model
  4. tm_err_t tm_preprocess(tm_mdl_t* mdl, tm_pp_t pp_type, tm_mat_t* in, tm_mat_t* out);            //preprocess input data
  5. tm_err_t tm_run   (tm_mdl_t* mdl, tm_mat_t* in, tm_mat_t* out);         //run model

  6. /******************************* UTILS FUNCTION ************************************/
  7. uint8_t TM_WEAK tm_fp32to8(float fp32);
  8. float TM_WEAK tm_fp8to32(uint8_t fp8);

  9. /******************************* STAT FUNCTION ************************************/
  10. #if TM_ENABLE_STAT
  11. tm_err_t tm_stat(tm_mdlbin_t* mdl);                    //stat model
  12. #endif

主要分为三类:
  • 模型函数,包括模型加载、卸载、预处理、推理;
  • 工具函数,包含FP32和uint8的互转;
  • 统计函数,用于输出模型中间层信息;

这里的模型,通常是预训练模型经过脚本转换生成的TinyMaix格式的模型;

另外,TinyMaix还提供了单独的层函数,用于实现单层计算功能,可以通过这些函数,用C代码的形式编写出一个模型。
  1. /******************************* LAYER FUNCTION ************************************/
  2. tm_err_t tml_conv2d_dwconv2d(tm_mat_t* in, tm_mat_t* out, wtype_t* w, btype_t* b, \
  3.     int kw, int kh, int sx, int sy, int dx, int dy, int act, \
  4.     int pad_top, int pad_bottom, int pad_left, int pad_right, int dmul, \
  5.     sctype_t* ws, sctype_t in_s, zptype_t in_zp, sctype_t out_s, zptype_t out_zp);
  6. tm_err_t tml_gap(tm_mat_t* in, tm_mat_t* out, sctype_t in_s, zptype_t in_zp, sctype_t out_s, zptype_t out_zp);
  7. tm_err_t tml_fc(tm_mat_t* in, tm_mat_t* out,  wtype_t* w, btype_t* b, \
  8.     sctype_t* ws, sctype_t in_s, zptype_t in_zp, sctype_t out_s, zptype_t out_zp);
  9. tm_err_t tml_softmax(tm_mat_t* in, tm_mat_t* out, sctype_t in_s, zptype_t in_zp, sctype_t out_s, zptype_t out_zp);
  10. tm_err_t tml_reshape(tm_mat_t* in, tm_mat_t* out, sctype_t in_s, zptype_t in_zp, sctype_t out_s, zptype_t out_zp);
  11. tm_err_t tml_add(tm_mat_t* in0, tm_mat_t* in1, tm_mat_t* out, \
  12.     sctype_t in_s0, zptype_t in_zp0, sctype_t in_s1, zptype_t in_zp1, sctype_t out_s, zptype_t out_zp);



1.3 TinyMaix底层依赖
TinyMaix可以简单理解为一个矩阵和向量计算库,目前已支持如下几种计算硬件:
  1. #define TM_ARCH_CPU         (0) //default, pure cpu compute
  2. #define TM_ARCH_ARM_SIMD    (1) //ARM Cortex M4/M7, etc.
  3. #define TM_ARCH_ARM_NEON    (2) //ARM Cortex A7, etc.
  4. #define TM_ARCH_ARM_MVEI    (3) //ARMv8.1: M55, etc.
  5. #define TM_ARCH_RV32P       (4) //T-head E907, etc.
  6. #define TM_ARCH_RV64V       (5) //T-head C906,C910, etc.
  7. #define TM_ARCH_CSKYV2      (6) //cskyv2 with dsp core
  8. #define TM_ARCH_X86_SSE2    (7) //x86 sse2

对于ARM-Cortex系列MCU,可以支持纯CPU计算和SIMD计算。其中CPU计算部分无特殊依赖(计算代码均使用标准C实现)。SIMD部分,部分计算代码使用了C语言内嵌汇编实现,需要CPU支持相应的汇编指令,才可以正常编译、运行。
TinyMaix的示例代码依赖于精准计时打印输出能力,具体是项目的tm_port.h中的几个宏定义:
  1. #define  TM_GET_US()       ((uint32_t)((uint64_t)clock()*1000000/CLOCKS_PER_SEC))

  2. #define TM_DBGT_INIT()     uint32_t _start,_finish;float _time;_start=TM_GET_US();
  3. #define TM_DBGT_START()    _start=TM_GET_US();
  4. #define TM_DBGT(x)         {_finish=TM_GET_US();\
  5.                             _time = (float)(_finish-_start)/1000.0;\
  6.                             TM_PRINTF("===%s use %.3f ms\n", (x), _time);\
  7.                             _start=TM_GET_US();}



二、计时和打印支持
2.1 创建CubeMX项目
首先,打卡STM32CubeMX,通过“Access to board selector”进入开发板选择界面。
image-20230312120656248.png
在开发板选择界面中,首先在左上角Commercial Part Number中输入NUCLEO-U575ZI-Q,此时右下角的开发列表将会只有一个NUCLEO-U575ZI-Q开发板。然后,鼠标点击NUCLEO-U575ZI-Q开发板所在行,选中此款开发板。最后,点击右上角的Start Project按钮。
image-20230312121634069.png

弹出“Initialize all peripherals with their default Mode?”对话框,选择Yes继续。然后,弹出的TrustZone选择对话框,选择without TrustZone actived,点击OK继续。
此时,已进入CubeMX的配置界面,按Ctrl+S或通过File->Save Project菜单可将当前CubeMX配置保存为独立的ioc文件。
image-20230312122636910.png

2.2 使用CubeMX生成Keil项目
选择NUCLEO-U575ZI-Q开发板后,可以在CubeMX中看到,默认已经将主控芯片STM32U575ZIT6的PA9、PA10引脚分别配置为USART1_TX、USART1_RX功能,如下图所示:
image-20230312202931372.png
查阅原理图,可以看到板载STLink的串口和主控芯片的PA9、PA10已连接:
image-20230312202641056.png
因此,我们可以通过板载STLink接收USART1的输入和输出。

在CubeMX中,切换到Project Manager标签页,将Minimum Heap Size和Minimum Stack Size的值分别修改为:0x8000(32K)和0x2000(8KB)。
image-20230312203429688.png
接下来,点击Code Generator,然后:
  • STM32Cube MCU packages and embedded software packs中,选择Copy only the necessary library files(只拷贝必要的库文件);
  • Generated files中,选择Generate peripheral initialization as a pair of '.c/.h' filers per peripheral(每个外设的初始化生成独立的.c/.h文件对);

接下来,点击右上角的Generate Code,开始生成代码。
image-20230312203742210.png

生成的Keil项目中,文件结构如下:
image-20230313194823462.png
为了方便调试,在调试器设置中,勾选Reset and Run,如下图所示:
image-20230313194536404.png

2.3 添加printf打印支持
生成代码后,在项目的Target配置界面中,勾选Use MicroLIB,如下图所示:
image-20230313194411303.png
然后,在项目的usart.c代码文件中,添加如下代码:
  1. int fputc(int ch, FILE* f)
  2. {
  3.   (void) f;

  4.   uint8_t data[] = {ch};
  5.   if (HAL_UART_Transmit(&huart1, data, sizeof(data), 1000) != HAL_OK)
  6.   {
  7.     return EOF;
  8.   }

  9.   return ch;
  10. }
添加完如上代码后,将项目可以支持printf打印。


2.4 基本功能测试
接下来,修改main.c文件,将其中的while循环代码修改为:
  1.   /* Infinite loop */
  2.   /* USER CODE BEGIN WHILE */
  3.   while (1)
  4.   {
  5.     HAL_GPIO_WritePin(LED_RED_GPIO_Port, LED_RED_Pin, GPIO_PIN_RESET);
  6.     HAL_Delay(1000);
  7.     printf("Low\r\n");

  8.     HAL_GPIO_WritePin(LED_RED_GPIO_Port, LED_RED_Pin, GPIO_PIN_SET);
  9.     HAL_Delay(1000);
  10.     printf("High\r\n");
  11.     /* USER CODE END WHILE */

  12.     /* USER CODE BEGIN 3 */
  13.   }
  14.   /* USER CODE END 3 */
重新编译整个项目,烧录,将会看到红色LED闪烁。

CubeMX中,默认的USART1参数配置为:
image-20230313200042275.png
使用串口助手,打开STLINK的虚拟串口,波特率设置为115200,既可以看到printf的打印输出:
image-20230313200341804.png


三、TinyMaix移植3.1 添加TinyMaix源码
接下来,克隆TinyMaix源码:
git clone https://github.com/sipeed/TinyMaix.git
在CubeMX生成的文件夹中创建TinyMaix,并将TinyMaix开源代码的include、src、example目录拷贝到该目录中。

然后在Keil的项目视图中,使用Add Group菜单添加源码组,如下图所示:
image-20230313202524875.png
然后,将新创建的组重命名为TinyMaix,如下图所示:
image-20230313202835027.png
然后,通过右键Add Exists Files to Group 'TinyMaix'菜单,将刚刚拷贝过来的src目录的.c文件添加到该组中,如下图所示:
image-20230313203043465.png


3.2 解决TinyMaix编译问题
此时直接编译源码,会出现一些找不到tinymaix.h的编译报错,需要按照如下步骤处理:
  • 右键TinyMaix-U575打开Options for Target 'TinyMaix-U575'配置窗口;
  • 点击C/C++标签页,找到Include paths栏;
  • 点击Include paths栏右侧的“...”按钮,弹出Setup compiler include paths界面;
  • 在此界面中,将项目的TinyMaix/include子目录添加到搜索路径列表中。

完成上述步骤后,仍然无法编译通过,会有如下编译报错:
image-20230313203649054.png
我们需要修改这段代码:
image-20230313203742879.png
将其修改为如下代码段:
  1. #include "stm32u5xx_hal.h"
  2. #define TM_GET_MS()         HAL_GetTick()
  3. #define TM_DBGT_INIT()      uint32_t _start, _finish; int _time; _start = TM_GET_MS();
  4. #define TM_DBGT_START()     _start = TM_GET_MS();
  5. #define TM_DBGT(x)          _finish = TM_GET_MS(); \
  6.                             _time = (_finish - _start); \
  7.                             TM_PRINTF("===%s use %d ms\n", (x), _time); \
  8.                             _start = TM_GET_MS();
PS:CubeMX默认生成的项目的Tick频率为1000Hz,1个Tick即为1毫秒;

3.3 添加手写数字识别示例
TinyMaix项目的examples目录下,每个目录是一个独立的示例程序。其中,minst目录即为手写数字识别示例的代码。
由于TinyMaix的examples目录内有多个main.c文件,并且每个main.c中都有一个main函数。另外,CubeMX生成的项目本身已经有了main.c文件,以及对应的main函数。
因此,如果直接将这些文件添加到项目中,会导致编译错误。所以,接下来首先需要进行如下修改:
  • 将mnist目录内的main.c重命名为mnist_main.c;
  • 将该文件中的main函数重命名为mnist_main;

完成以上修改之后,使用类似前面的方法,将mnist_main.c文件添加到TinyMaix源码组中:
image-20230313205235102.png
最后,修改main.c,在while循环之前添加两行代码,分别声明和调用minst_main函数:
  1.   int mnist_main(int argc, char** argv);
  2.   mnist_main(0, 0);
完成以上修改后,编译、烧录、运行,将会在STLink虚拟串口中看到如下输出:
image-20230313210522062.png
可以看到,成功识别了手写数字。

四、原理解读4.1 手写数字识别示例源码
mnist_main.c文件中,开始的几行用于根据tm_port.h中定义的数据使用对应的模型:
  1. #if TM_MDL_TYPE == TM_MDL_INT8
  2. #include "../../tools/tmdl/mnist_valid_q.h"
  3. //#include "../../tools/tmdl/mnist_resnet_q.h"
  4. #elif TM_MDL_TYPE == TM_MDL_FP32
  5. #include "../../tools/tmdl/mnist_valid_f.h"
  6. //#include "../../tools/tmdl/mnist_resnet_f.h"
  7. #elif TM_MDL_TYPE == TM_MDL_FP16
  8. #include "../../tools/tmdl/mnist_valid_fp16.h"
  9. #elif TM_MDL_TYPE == TM_MDL_FP8_143
  10. #include "../../tools/tmdl/mnist_fp8_143.h"
  11. #elif TM_MDL_TYPE == TM_MDL_FP8_152
  12. #include "../../tools/tmdl/mnist_fp8_152.h"
  13. #endif
这些.h文件是由tflite2tmdl.py脚本生成的TinyMaix模型,mnist_valid_f模型的转换命令为:
python3 tflite2tmdl.py tflite/mnist_valid_f.tflite tmdl/mbnet_fp8.tmdl fp8_152 1 28,28,1 10
接下来定义了一个数组,uint8_t mnist_pic[28*28],保存一张测试图片,数组每个元素对应一个像素的灰度值。
image-20230313211621713.png
接下来,mnist_main中使用模型,主要使用了一下几个TinyMaix的API:
  • tm_stat 打印模型结构等信息;
  • tm_load 将模型加载到内存;
  • tm_preprocess 输入数据预处理;
  • tm_run 模型推理,得到输出;
  • tm_unload 模型卸载,释放内存;

使用起来还是非常简单的,具体接口参数和返回值可以参考TinyMaix代码注释。

本篇内容就到这里了,感谢你的阅读,下次再会。
完整移植代码仓(感兴趣的同学可以下载下来自行实验):https://gitee.com/swxu/tiny-maix-u575.git

五、参考链接
  • 【ST官网】NUCLEO-U575ZI-Q开发板产品页: https://www.st.com/en/evaluation-tools/nucleo-u575zi-q.html
  • 【ST官网】NUCLEO-U575ZI-Q开发板原理图:https://www.st.com/resource/en/schematic_pack/mb1549-u575ziq-c03_schematic.pdf
  • 【ST官网】STM32U575ZI芯片产品页: https://www.st.com/en/microcontrollers-microprocessors/stm32u575zi.html
  • 【Keil官网】Pack包下载:https://www.keil.com/dd2/pack/
  • Keil中如何重定向printf/scanf到UART: http://www.ocfreaks.com/retarget-redirect-printf-scanf-uart-keil/













朝生 发表于 2023-3-14 14:41 | 显示全部楼层
不错,TinyMaix确实好用。
LLGTR 发表于 2023-3-14 14:41 | 显示全部楼层
就是不知道TinyMaix怎么支持NPU加速。
芯路例程 发表于 2023-3-14 14:42 | 显示全部楼层
这个Demo还是软件比较复杂,硬件还好。
ljm9823 发表于 2023-4-11 16:27 | 显示全部楼层
厉害呀
土司Apple 发表于 2023-4-11 19:43 | 显示全部楼层
这个谁有触摸屏上写的例子呀?
chenjun89 发表于 2023-4-11 20:16 来自手机 | 显示全部楼层
实际可以应用到哪些方面
Undshing 发表于 2023-4-11 21:19 | 显示全部楼层
TinyMaix支持NPU加速吗?
hearstnorman323 发表于 2023-4-12 20:32 | 显示全部楼层
单片机也能玩神经网络              
olivem55arlowe 发表于 2023-4-12 20:44 | 显示全部楼层
这个计算能力这么强悍吗              
cashrwood 发表于 2023-4-12 21:39 | 显示全部楼层
TinyMaix 是针对小算力小内存的芯片设计的轻量级推理框架,甚至能在2KB内存的Arduino ATmega328单片机上运行MNIST
jonas222 发表于 2023-4-13 20:47 | 显示全部楼层
TinyMaix是识别手写的吗              
mickit 发表于 2023-4-13 21:25 | 显示全部楼层
需要做神经网络学习吗              
saservice 发表于 2023-4-13 22:31 | 显示全部楼层
移植轻量级AI推理框架               
mattlincoln 发表于 2023-4-16 22:16 | 显示全部楼层
Arduino都能跑的超轻量级TinyML推理框架
mollylawrence 发表于 2023-4-16 22:44 | 显示全部楼层
TinyMaix 是面向单片机的超轻量级的神经网络推理库
saservice 发表于 2023-4-18 16:47 | 显示全部楼层
这个链接摄像头了吗
              
wangdezhi 发表于 2023-4-23 13:34 | 显示全部楼层
这个是基于哪个架构做的?              
ousj 发表于 2023-6-7 21:30 | 显示全部楼层
不错,TinyMaix确实好用。
wenfen 发表于 2023-6-7 21:34 | 显示全部楼层
这个Demo还是软件比较复杂,硬件还好。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

5

主题

37

帖子

1

粉丝
快速回复 在线客服 返回列表 返回顶部