Arm Ethos‑U65:YOLOv8n 仿真验证与命令流执行

发布时间:2026/7/18 23:27:10
Arm Ethos‑U65:YOLOv8n 仿真验证与命令流执行 本文是 Arm Ethos‑U65 学习笔记的第五篇也是 YOLOv8n 目标检测实例的下篇。上篇完成了模型准备与 Vela 编译生成命令流本篇将命令流导入芯片仿真环境执行推理验证整个示例以芯片验证仿真为主线同时对裸机环境下的软件开发也有一定参考价值。数据准备tflite转bin将vela.tflite转换为命令流和权重的 bin 文件。convert_vela2bin.pyimportosimporttflitedefextract_ethos_u_stream(tflite_path):withopen(tflite_path,rb)asf:buff.read()modeltflite.Model.GetRootAsModel(buf,0)subgraphmodel.Subgraphs(0)found_npu_opFalse# 遍历所有算子寻找 Ethos-U 的 CUSTOM 算子foriinrange(subgraph.OperatorsLength()):opsubgraph.Operators(i)opcode_idxop.OpcodeIndex()opcodemodel.OperatorCodes(opcode_idx)ifopcode.BuiltinCode()tflite.BuiltinOperator.CUSTOM:custom_codeopcode.CustomCode()ifcustom_code:custom_code_strcustom_code.decode(utf-8)else:continueifethos-uincustom_code_str:print(f 找到 Ethos-U Custom 算子 (Index:{i}) )found_npu_opTrue# Index 0: 包含 cmd0/cmd1 的指令流 (Command Stream)# Index 1: 包含 权重 (Weights) 和 偏置 (Biases)cmd_tensor_idxop.Inputs(0)weight_tensor_idxop.Inputs(1)cmd_tensorsubgraph.Tensors(cmd_tensor_idx)weight_tensorsubgraph.Tensors(weight_tensor_idx)cmd_buf_objmodel.Buffers(cmd_tensor.Buffer())weight_buf_objmodel.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())])returnbcmd_bufferget_raw_bytes(cmd_buf_obj)weight_bufferget_raw_bytes(weight_buf_obj)# 保存为原始二进制文件cmd_outethos_u_cmd_stream.binweight_outethos_u_weights.binwithopen(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.tfliteextract_ethos_u_stream(model_filename)bin转hex字符将 bin 文件转化为 verilog $readmemh 可以读取的16进制文件需要注意命令流需要删除前 32B 数据。convert_bin2vhex.pydefbin_to_hex_32bit(bin_path,hex_path,skip_bytes0,reverse_bytesTrue): 将二进制 .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_LINE4# 32位宽对应 4 字节withopen(bin_path,rb)asf_in,open(hex_path,w)asf_out:# 如果需要跳过前几个字节例如 Vela 命令流头部元数据ifskip_bytes0:droppedf_in.read(skip_bytes)print(f已跳过{bin_path}头部的{len(dropped)}字节数据)whileTrue:# 每次读取 4 字节32位chunkf_in.read(BYTES_PER_LINE)ifnotchunk:break# 如果最后剩余字节不足 4 字节用 0x00 补齐iflen(chunk)BYTES_PER_LINE:chunkchunkb\x00*(BYTES_PER_LINE-len(chunk))# 字节序反转小端序# 确保 bin 文件中的低地址字节映射到 Verilog 32位寄存器的低 8 位 [7:0] if reverse_bytes:chunkchunk[::-1]# 转换为 8 位的十六进制字符串4字节 8个16进制字符例如 ffffffffhex_linechunk.hex()# 写入文件并换行f_out.write(hex_line\n)if__name____main__:# 【指令流转换】# 如果你发现硬件的第一行指令报错即包含了 Vela Header可以尝试将 skip_bytes 改为 4 或 8 bin_to_hex_32bit(bin_pathethos_u_cmd_stream.bin,hex_pathethos_u_cmd_stream_32.hex,skip_bytes32,# 如果需要剔除第一行不能用的数据可以根据具体字节数修改此处例如改为 4reverse_bytesTrue)# 【权重流转换】bin_to_hex_32bit(bin_pathethos_u_weights.bin,hex_pathethos_u_weight_stream_32.hex,skip_bytes0,reverse_bytesTrue)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~0x2fe80sub存放地址 0x2fe80~0x30900quantize 存放地址 0x30900~0x44ad00输出均在区域1所以最终需要 dump 的数据是 区域1 0x2f400~0x4ad00 偏移。此外也可以得知数据是按 1344 维度顺序存。寄存器配置寄存器配置可以参见 IP验证的前文档主要区别在修改地址。write_reg(NPU_REG_AXI_LIMIT0, 32h0f0f0001, error); // axi-0配置 write_reg(NPU_REG_AXI_LIMIT1, 32h0f0f0001, error); write_reg(NPU_REG_AXI_LIMIT2, 32h0f0f0001, error); // axi-1配置 write_reg(NPU_REG_AXI_LIMIT3, 32h0f0f0001, 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, 32h00000022, 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, dtypenp.int8).reshape((1344, 84), orderF)导入数据可以看到前4列为坐标后80列为置信度-128 对应 int8 格式下的最小值。最后结果和 Python 调用 Tensorflow lite 运行模型推理程序完全一致。