本文是 Arm Ethos‑U65 学习笔记的第五篇,也是 YOLOv8n 目标检测实例的下篇。上篇完成了模型准备与 Vela 编译生成命令流,本篇将命令流导入芯片仿真环境执行推理验证,整个示例以芯片验证仿真为主线,同时对裸机环境下的软件开发也有一定参考价值。

数据准备

tflite转bin

将vela.tflite转换为命令流和权重的 bin 文件。
convert_vela2bin.py

import os  
import tflite  
  
def extract_ethos_u_stream(tflite_path):  
    with open(tflite_path, "rb") as f:  
        buf = f.read()  
  
    model = tflite.Model.GetRootAsModel(buf, 0)  
    subgraph = model.Subgraphs(0)  
  
    found_npu_op = False  
  
    # 遍历所有算子,寻找 Ethos-U 的 CUSTOM 算子  
    for i in range(subgraph.OperatorsLength()):  
        op = subgraph.Operators(i)  
        opcode_idx = op.OpcodeIndex()  
        opcode = model.OperatorCodes(opcode_idx)  
  
        if opcode.BuiltinCode() == tflite.BuiltinOperator.CUSTOM:  
            custom_code = opcode.CustomCode()  
            if custom_code:  
                custom_code_str = custom_code.decode('utf-8')  
            else:  
                continue  
  
            if "ethos-u" in custom_code_str:  
                print(f"==== 找到 Ethos-U Custom 算子 (Index: {i}) ====")  
                found_npu_op = True  
                # Index 0: 包含 cmd0/cmd1 的指令流 (Command Stream)                
                # Index 1: 包含 权重 (Weights) 和 偏置 (Biases)                
                cmd_tensor_idx = op.Inputs(0)  
                weight_tensor_idx = op.Inputs(1)  
  
                cmd_tensor = subgraph.Tensors(cmd_tensor_idx)  
                weight_tensor = subgraph.Tensors(weight_tensor_idx)  
  
                cmd_buf_obj = model.Buffers(cmd_tensor.Buffer())  
                weight_buf_obj = model.Buffers(weight_tensor.Buffer())  
  
                # --- 安全提取 Buffer 数据 (兼容不同 Flatbuffers 版本) ---  
                def get_raw_bytes(buf_obj):  
                    # 通过 DataLength 逐字节提取  
                    if hasattr(buf_obj,'DataLength') and buf_obj.DataLength() > 0:  
                        return bytes([buf_obj.Data(j) for j in range(buf_obj.DataLength())])  
                    return b""  
  
                cmd_buffer = get_raw_bytes(cmd_buf_obj)  
                weight_buffer = get_raw_bytes(weight_buf_obj)  
  
                # 保存为原始二进制文件  
                cmd_out = "ethos_u_cmd_stream.bin"  
                weight_out = "ethos_u_weights.bin"  
  
                with open(cmd_out, "wb") as f_cmd:  
                    f_cmd.write(cmd_buffer)  
                with open(weight_out, "wb") as f_weight:  
                    f_weight.write(weight_buffer)  
  
                print(f"成功导出指令流 -> {cmd_out} ({len(cmd_buffer)} 字节)")  
                print(f"成功导出权重数据 -> {weight_out} ({len(weight_buffer)} 字节)")  
                break  # 找到 NPU 核心算子后即可退出  
  
  
if __name__ == "__main__":  
    # 替换为你的 vela tflite 模型文件名  
    model_filename = "./tfile_model/output/yolov8n_256_quant_pc_ui_od_coco_vela.tflite"  
    extract_ethos_u_stream(model_filename)

bin转hex字符

将 bin 文件转化为 verilog $readmemh 可以读取的16进制文件,需要注意命令流需要删除前 32B 数据。
convert_bin2vhex.py

def bin_to_hex_32bit(bin_path, hex_path, skip_bytes=0, reverse_bytes=True):  
    """  
    将二进制 .bin 文件转换为 32位宽(4字节)Verilog $readmemh 可读的 .hex 文件  
  
    :param bin_path: 输入的 .bin 文件路径  
    :param hex_path: 输出的 .hex 文件路径  
    :param skip_bytes: 需要跳过的头部字节数(如 Vela 的 Header),默认为 0    :param reverse_bytes: 是否反转每行的字节序(小端序系统设为 True)  
    """    BYTES_PER_LINE = 4  # 32位宽对应 4 字节  
  
    with open(bin_path, 'rb') as f_in, open(hex_path, 'w') as f_out:  
        # 如果需要跳过前几个字节(例如 Vela 命令流头部元数据)  
        if skip_bytes > 0:  
            dropped = f_in.read(skip_bytes)  
            print(f"已跳过 {bin_path} 头部的 {len(dropped)} 字节数据")  
  
        while True:  
            # 每次读取 4 字节(32位)  
            chunk = f_in.read(BYTES_PER_LINE)  
            if not chunk:  
                break  
  
            # 如果最后剩余字节不足 4 字节,用 0x00 补齐  
            if len(chunk) < BYTES_PER_LINE:  
                chunk = chunk + b'\x00' * (BYTES_PER_LINE - len(chunk))  
  
            # 字节序反转(小端序):  
            # 确保 bin 文件中的低地址字节,映射到 Verilog 32位寄存器的低 8 位 [7:0]            if reverse_bytes:  
                chunk = chunk[::-1]  
  
            # 转换为 8 位的十六进制字符串(4字节 = 8个16进制字符,例如 ffffffff)  
            hex_line = chunk.hex()  
  
            # 写入文件并换行  
            f_out.write(hex_line + '\n')  
  
  
if __name__ == "__main__":  
    # 【指令流转换】  
    # 如果你发现硬件的第一行指令报错(即包含了 Vela Header),可以尝试将 skip_bytes 改为 4 或 8    bin_to_hex_32bit(  
        bin_path="ethos_u_cmd_stream.bin",  
        hex_path="ethos_u_cmd_stream_32.hex",  
        skip_bytes=32,  # 如果需要剔除第一行不能用的数据,可以根据具体字节数修改此处(例如改为 4)  
        reverse_bytes=True  
    )  
  
    # 【权重流转换】  
    bin_to_hex_32bit(  
        bin_path="ethos_u_weights.bin",  
        hex_path="ethos_u_weight_stream_32.hex",  
        skip_bytes=0,  
        reverse_bytes=True  
    )  
  
    print("32位宽转换完成!已生成对应的 .hex 文件。")

转换完成后可以和 register-command-stream 对比一下,应当是完全一致的
在这里插入图片描述

仿真testbench

从命令流获取地址

首先通过命令流获取地址信息,查看 register-command-stream,通过IFM_REGION、OFM_REGION、DMA0_SRC_REGION 可以获取输入数据、缓存数据和权重需要存放的地址。下图中可以看到输入数据存放在区域1+偏移地址0x0、权重放在区域0+偏移地址 0x0,缓存数据起始地址是区域2+偏移地址 0x0。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

输出数据地址获取就麻烦一些,tflite 网络最后一步为 concat,但命令流中是没有concat,再往上追可以看到 concat 有3个来源,前 2 个 “2” 是框坐标,最后的 “80” 是框置信度,然后查看 register-command-stream,可以看到mul存放地址 0x2f400~0x2fe80,sub存放地址 0x2fe80~0x30900,quantize 存放地址 0x30900~0x44ad00,输出均在区域1,所以最终需要 dump 的数据是 区域1 + 0x2f400~0x4ad00 偏移。此外也可以得知数据是按 1344 维度顺序存。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

寄存器配置

寄存器配置可以参见 IP验证的前文档,主要区别在修改地址。

write_reg(NPU_REG_AXI_LIMIT0, 32'h0f0f0001, error); // axi-0配置
write_reg(NPU_REG_AXI_LIMIT1, 32'h0f0f0001, error);
write_reg(NPU_REG_AXI_LIMIT2, 32'h0f0f0001, error); // axi-1配置
write_reg(NPU_REG_AXI_LIMIT3, 32'h0f0f0001, error);

write_reg('h10,'h0, error); // 命令流地址
write_reg('h20,'h9850, error);  // 命令流大小,register-command第一行可以看

write_reg('h80,'h10000, error);    // region0  权重
write_reg('h88,'h40_0000, error);  // region1  输入数据、中间变量、输出数据
write_reg('h90,'h80_0000, error);  // region2  中间变量
write_reg(NPU_REG_REGIONCFG, 32'h00000022, error);

write_reg('h8 ,'h1, error);    // 触发NPU工作
    
wait(ethosu65_exetb.u_wrapper.u_dut.IRQ) #1ns; //等待NPU运算完成

...  // 导出 区域1地址+0x30900~0x4ad00偏移 数据

仿真结果

仿真波形

运行仿真,等待仿真结束,可以看到,3.2MB 的YOLO v8n 模型使用 Vela 默认配置编译,Ethos U65 运行时钟 1GHz,花费 6.16ms 运算完成。

在这里插入图片描述

指令应当和波形的行为完全对应,例如第1次权重搬运,从区域0地址偏移 0x0 搬运到区域2地址偏移 0x30000,波形中基地址为 APB 配置的区域 0、2地址。

在这里插入图片描述

在这里插入图片描述

在运算期间可以通过内部信号 u_ethosu65_cc.u_ethosu65_cc_reg.qread_i 判断当前执行命令执行到哪里,结合 register-command-stream 调试。

导出数据打印

U65 输出为中间运算结果,还需要继续进行后处理,程序参照前文章 tfile_model_run_yolov8n.py 后半部分,其实搞清 U65 输出是按1344 遍历即可,其余部分没有改动。

arr = np.array(data, dtype=np.int8).reshape((1344, 84), order='F')

导入数据可以看到前4列为坐标,后80列为置信度,-128 对应 int8 格式下的最小值。
在这里插入图片描述
最后结果和 Python 调用 Tensorflow lite 运行模型推理程序完全一致。
在这里插入图片描述

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐