Arm Ethos‑U65:YOLOv8n 仿真验证与命令流执行
2026/7/18 23:26:51 网站建设 项目流程

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

数据准备

tflite转bin

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

importosimporttflitedefextract_ethos_u_stream(tflite_path):withopen(tflite_path,"rb")asf:buf=f.read()model=tflite.Model.GetRootAsModel(buf,0)subgraph=model.Subgraphs(0)found_npu_op=False# 遍历所有算子,寻找 Ethos-U 的 CUSTOM 算子foriinrange(subgraph.OperatorsLength()):op=subgraph.Operators(i)opcode_idx=op.OpcodeIndex()opcode=model.OperatorCodes(opcode_idx)ifopcode.BuiltinCode()==tflite.BuiltinOperator.CUSTOM:custom_code=opcode.CustomCode()ifcustom_code:custom_code_str=custom_code.decode('utf-8')else:continueif"ethos-u"incustom_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 版本) ---defget_raw_bytes(buf_obj):# 通过 DataLength 逐字节提取ifhasattr(buf_obj,'DataLength')andbuf_obj.DataLength()>0:returnbytes([buf_obj.Data(j)forjinrange(buf_obj.DataLength())])returnb""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"withopen(cmd_out,"wb")asf_cmd:f_cmd.write(cmd_buffer)withopen(weight_out,"wb")asf_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

defbin_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 字节withopen(bin_path,'rb')asf_in,open(hex_path,'w')asf_out:# 如果需要跳过前几个字节(例如 Vela 命令流头部元数据)ifskip_bytes>0:dropped=f_in.read(skip_bytes)print(f"已跳过{bin_path}头部的{len(dropped)}字节数据")whileTrue:# 每次读取 4 字节(32位)chunk=f_in.read(BYTES_PER_LINE)ifnotchunk:break# 如果最后剩余字节不足 4 字节,用 0x00 补齐iflen(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 运行模型推理程序完全一致。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询