实现PS端YOLO网络前向计算函数
目的:在PS端控制PL端完成YOLO网络的前向计算
前提:已经实现了YOLO网络参数导入到DDR3的功能
创建新文件
- 在Vitis软件中新建两个文件:
yolo_accel_ctrl.c
和yolo_accel_ctrl.h
yolo_accel_ctrl.c
用于编写前向计算函数的主体代码yolo_accel_ctrl.h
用于声明前向计算函数所需的变量和函数
状态跳转图 & 流程图
graph TD A[开始] --> B[初始化状态为S_IDLE] B --> C{判断状态} C -->|S_IDLE| D[发送参数命令] D --> E[状态跳转为S_FEATURE_CONV] C -->|S_FEATURE_CONV| F[发送数据命令] F --> G[卷积计算命令] G --> H[状态跳转为S_DMA_RX] C -->|S_DMA_RX| I[接收数据命令] I --> J{判断是否完成当前8个输出通道} J -->|否| K[更新TX_CNT和TX_ADDR] K --> E J -->|是| L{判断是否完成全部16个输出通道} L -->|否| M[更新BATCH_CNT和TX_ADDR] M --> E L -->|是| N[状态跳转为S_FINISH] C -->|S_FINISH| O[结束]定义状态机
- 在
yolo_accel_ctrl.c
中定义一个状态机,用于控制前向计算的流程- 参考仿真时使用的Verilog代码中的状态机
- 状态机有四个状态:S_IDLE, S_FEATURE_CONV, S_DMA_RX, S_FINISH
- S_IDLE:初始化状态,发送参数给PL端
- S_FEATURE_CONV:数据发送和卷积计算状态,发送输入数据给PL端,并控制PL端进行卷积运算
- S_DMA_RX:数据接收状态,通过DMA从PL端读取输出数据
- S_FINISH:结束状态,跳出状态机循环
- 状态机使用switch-case语句实现
- 状态机使用两个计数器控制循环次数:tx_cnt和batch_cnt
- tx_cnt:每8个输出通道重复数据发送、卷积计算、数据接收的次数计数器
- batch_cnt:每完成8个输出通道计算的次数计数器
// 定义状态机
enum state {S_IDLE, S_FEATURE_CONV, S_DMA_RX, S_FINISH};
enum state state = S_IDLE; // 初始状态为S_IDLE
// 定义计数器
u8 tx_cnt; // 每8个输出通道重复数据发送、卷积计算、数据接收的次数计数器
u8 batch_cnt; // 每完成8个输出通道计算的次数计数器
// 定义状态机循环条件
while (state != S_FINISH) {
switch (state) {
case S_IDLE:
// 初始化状态代码
break;
case S_FEATURE_CONV:
// 数据发送和卷积计算状态代码
break;
case S_DMA_RX:
// 数据接收状态代码
break;
case S_FINISH:
// 结束状态代码
break;
default:
// 默认状态代码
break;
}
}
初始化状态
- 在S_IDLE状态中,完成以下操作:
- 定义并初始化一些变量,如命令、地址、数据长度等
- 发送参数给PL端,包括BIAS、ACT、WEIGHT等
- 跳转到S_FEATURE_CONV状态
case S_IDLE:
// 定义并初始化变量
u32 cmd; // 命令变量
u32 tx_addr; // 发送数据地址变量
u32 tx_len; // 发送数据长度变量
u32 rx_addr; // 接收数据地址变量
u32 rx_len; // 接收数据长度变量
tx_cnt = 0; // 初始化tx_cnt为0
batch_cnt = 0; // 初始化batch_cnt为0
// 发送参数给PL端
cmd = (u32)0x00000001; // BIAS参数命令
Xil_Out32(CMD_BASEADDR, cmd); // 将命令写入CMD_BASEADDR寄存器
XAxiDma_SimpleTransfer(&axiDma, (u32)BIAS_DDR_BASEADDR, BIAS_LENGTH, XAXIDMA_DMA_TO_DEVICE); // 将BIAS参数从BIAS_DDR_BASEADDR地址发送给PL端,长度为BIAS_LENGTH
cmd = (u32)0x00000002; // ACT参数命令
Xil_Out32(CMD_BASEADDR, cmd); // 将命令写入CMD_BASEADDR寄存器
XAxiDma_SimpleTransfer(&axiDma, (u32)ACT_DDR_BASEADDR, ACT_LENGTH, XAXIDMA_DMA_TO_DEVICE); // 将ACT参数从ACT_DDR_BASEADDR地址发送给PL端,长度为ACT_LENGTH
cmd = (u32)0x00000003; // WEIGHT参数命令
Xil_Out32(CMD_BASEADDR, cmd); // 将命令写入CMD_BASEADDR寄存器
XAxiDma_SimpleTransfer(&axiDma, (u32)WEIGHT_DDR_BASEADDR, WEIGHT_LENGTH, XAXIDMA_DMA_TO_DEVICE); // 将WEIGHT参数从WEIGHT_DDR_BASEADDR地址发送给PL端,长度为WEIGHT_LENGTH
// 跳转到S_FEATURE_CONV状态
state = S_FEATURE_CONV;
break;
数据发送和卷积计算状态
-
在S_FEATURE_CONV状态中,完成以下操作:
-
根据tx_cnt和batch_cnt的值,判断是否是第一次或最后一次发送数据,生成相应的命令
- 第一次发送数据:cmd = (u32)0x00000001 | (u32)0x00010000;
- 最后一次发送数据:cmd = (u32)0x00000001 | (u32)0x00100000;
- 其他情况:cmd = (u32)0x00000001 | (u32)0x00110000;
-
根据tx_cnt的值,判断是否是最后一次发送数据,生成相应的数据长度
- 最后一次发送数据:tx_len = (u32)9984;
- 其他情况:tx_len = (u32)29952;
-
根据batch_cnt的值,判断是否是第一次或第二次发送数据,生成相应的数据地址
- 第一次发送数据:tx_addr = (u32)0x10000000;
- 第二次发送数据:tx_addr = (u32)0x10000000 + (u32)(7 * IN_WIDTH * IN_CH * sizeof(float));
- 其他情况:不改变tx_addr的值;
-
将命令写入CMD_BASEADDR寄存器,将输入数据从tx_addr地址发送给PL端,长度为tx_len
-
将命令的最后4位改为4,表示开始卷积计算,并写入CMD_BASEADDR寄存器
-
等待PL端完成卷积计算,并检查是否有错误发生
-
根据tx_cnt的值,判断是否需要跳转到S_DMA_RX状态,或者增加tx_cnt的值
-
如果tx_cnt等于7,表示已经发送了8个输出通道的数据,需要跳转到S_DMA_RX状态,并将tx_cnt重置为0
-
否则,将tx_cnt加1,继续发送下一个输出通道的数据
-
-
根据batch_cnt的值,判断是否需要增加batch_cnt的值
- 如果batch_cnt等于0,并且tx_cnt等于0,表示已经完成了第一次发送8个输出通道的数据,需要将batch_cnt加1
- 否则,不改变batch_cnt的值
-
case S_FEATURE_CONV:
// 根据tx_cnt和batch_cnt的值,判断是否是第一次或最后一次发送数据,生成相应的命令
if (tx_cnt == 0 && batch_cnt == 0) {
// 第一次发送数据
cmd = (u32)0x00000001 | (u32)0x00010000;
} else if (tx_cnt == 7 && batch_cnt == 1) {
// 最后一次发送数据
cmd = (u32)0x00000001 | (u32)0x00100000;
} else {
// 其他情况
cmd = (u32)0x00000001 | (u32)0x00110000;
}
// 根据tx_cnt的值,判断是否是最后一次发送数据,生成相应的数据长度
if (tx_cnt == 7 && batch_cnt == 1) {
// 最后一次发送数据
tx_len = (u32)9984;
} else {
// 其他情况
tx_len = (u32)29952;
}
// 根据batch_cnt的值,判断是否是第一次或第二次发送数据,生成相应的数据地址
if (batch_cnt == 0 && tx_cnt == 0) {
// 第一次发送数据
tx_addr = (u32)0x10000000;
} else if (batch_cnt == 1 && tx_cnt == 0) {
// 第二次发送数据
tx_addr = (u32)0x10000000 + (u32)(7 * IN_WIDTH * IN_CH * sizeof(float));
} else {
// 其他情况
// 不改变tx_addr的值
}
// 将命令写入CMD_BASEADDR寄存器,将输入数据从tx_addr地址发送给PL端,长度为tx_len
Xil_Out32(CMD_BASEADDR, cmd);
XAxiDma_SimpleTransfer(&axiDma, tx_addr, tx_len, XAXIDMA_DMA_TO_DEVICE);
// 将命令的最后4位改为4,表示开始卷积计算,并写入CMD_BASEADDR寄存器
cmd = cmd & (u32)0xFFFFFFF0 | (u32)0x00000004;
Xil_Out32(CMD_BASEADDR, cmd);
// 等待PL端完成卷积计算,并检查是否有错误发生
while ((Xil_In32(CMD_BASEADDR + CMD_DONE_OFFSET) & CMD_DONE_MASK) != CMD_DONE_MASK);
if ((Xil_In32(CMD_BASEADDR + CMD_ERROR_OFFSET) & CMD_ERROR_MASK) != CMD_ERROR_MASK) {
printf("Error: Convolution error\n");
return XST_FAILURE;
}
// 根据tx_cnt的值,判断是否需要跳转到S_DMA_RX状态,或者增加tx_cnt的值
if (tx_cnt == 7) {
// 如果tx_cnt等于7,表示已经发送了8个输出通道的数据,需要跳转到S_DMA_RX状态,并将tx_cnt重置为0
state = S_DMA_RX;
tx_cnt = 0;
} else {
// 否则,将tx_cnt加1,继续发送下一个输出通道的数据
tx_cnt++;
state = S_FEATURE_CONV;
}
// 根据batch_cnt的值,判断是否需要增加batch_cnt的值
if (batch_cnt == 0 && tx_cnt == 0) {
// 如果batch_cnt等于0,并且tx_cnt等于0,表示已经完成了第一次发送8个输出通道的数据,需要将batch_cnt加1
batch_cnt++;
} else {
// 否则,不改变batch_cnt的值
}
break;
- 在S_DMA_RX状态中,完成以下操作:
- 根据tx_cnt和batch_cnt的值,判断是否是第一次或最后一次接收数据,生成相应的数据地址和数据长度
- 如果tx_cnt等于0,并且batch_cnt等于0,表示第一次接收数据,数据地址为0x20000000,数据长度为65536
- 如果tx_cnt等于7,并且batch_cnt等于1,表示最后一次接收数据,数据地址为0x20000000 + (u32)(7 * OUT_WIDTH * OUT_CH * sizeof(float)),数据长度为9984
- 其他情况下,数据地址为上一次接收数据的地址加上上一次接收数据的长度,数据长度为49920或65536交替出现
- 从PL端接收输出数据到相应的数据地址,长度为相应的数据长度
- 根据tx_cnt和batch_cnt的值,判断是否需要跳转到S_FINISH状态,或者跳转回S_FEATURE_CONV状态
- 如果tx_cnt等于7,并且batch_cnt等于1,表示已经完成了所有输出通道的接收,需要跳转到S_FINISH状态
- 否则,跳转回S_FEATURE_CONV状态,并根据情况增加tx_cnt或batch_cnt的值
- 根据tx_cnt和batch_cnt的值,判断是否是第一次或最后一次接收数据,生成相应的数据地址和数据长度
case S_DMA_RX:
// 根据tx_cnt和batch_cnt的值,判断是否是第一次或最后一次接收数据,生成相应的数据地址和数据长度
if (tx_cnt == 0 && batch_cnt == 0) {
// 第一次接收数据
rx_addr = (u32)0x20000000;
rx_len = (u32)65536;
} else if (tx_cnt == 7 && batch_cnt == 1) {
// 最后一次接收数据
rx_addr = (u32)0x20000000 + (u32)(7 * OUT_WIDTH * OUT_CH * sizeof(float));
rx_len = (u32)9984;
} else {
// 其他情况
// 数据地址为上一次接收数据的地址加上上一次接收数据的长度
rx_addr = rx_addr + rx_len;
// 数据长度为49920或65536交替出现
if (rx_len == (u32)49920) {
rx_len = (u32)65536;
} else {
rx_len = (u32)49920;
}
}
// 从PL端接收输出数据到相应的数据地址,长度为相应的数据长度
XAxiDma_SimpleTransfer(&axiDma, rx_addr, rx_len, XAXIDMA_DEVICE_TO_DMA);
// 等待DMA传输完成,并检查是否有错误发生
while ((XAxiDma_Busy(&axiDma, XAXIDMA_DEVICE_TO_DMA)));
if ((XAxiDma_ReadReg(axiDma.RegBase, XAXIDMA_RX_OFFSET + XAXIDMA_SR_OFFSET) & XAXIDMA_IRQ_ERROR_MASK)) {
printf("Error: DMA receive error\n");
return XST_FAILURE;
}
// 根据tx_cnt和batch_cnt的值,判断是否需要跳转到S_FINISH状态,或者跳转回S_FEATURE_CONV状态
if (tx_cnt == 7 && batch_cnt == 1) {
// 如果tx_cnt等于7,并且batch_cnt等于1,表示已经完成了所有输出通道的接收,需要跳转到S_FINISH状态
state = S_FINISH;
} else {
// 否则,跳转回S_FEATURE_CONV状态,并根据情况增加tx_cnt或batch_cnt的值
state = S_FEATURE_CONV;
if (tx_cnt == 7) {
// 如果tx_cnt等于7,表示已经完成了8个输出通道的接收,需要将tx_cnt重置为0,并将batch_cnt加1
tx_cnt = 0;
batch_cnt++;
} else {
// 否则,将tx_cnt加1
tx_cnt++;
}
}
break;
- 在S_FINISH状态中,完成以下操作:
- 打印输出数据的前16个元素,用于检查结果是否正确
- 跳出状态机的循环,结束前向计算函数
case S_FINISH:
// 打印输出数据的前16个元素,用于检查结果是否正确
printf("Output data:\n");
for (int i = 0; i < 16; i++) {
printf("%f ", ((float *)0x20000000)[i]);
}
printf("\n");
// 跳出状态机的循环,结束前向计算函数
break;
- 在S_IDLE状态中,完成以下操作:
- 根据layer_id的值,判断是哪一层的前向计算,生成相应的参数地址和数据地址
- 如果layer_id等于0,表示是第0层的前向计算,参数地址为0x10000000,数据地址为0x30000000
- 如果layer_id等于1,表示是第1层的前向计算,参数地址为0x10080000,数据地址为0x20000000
- 如果layer_id等于2,表示是第2层的前向计算,参数地址为0x10100000,数据地址为0x20000000
- 其他情况下,打印错误信息,并返回
- 从参数地址开始,发送BIAS、ACT、WEIGHT三种参数到PL端
- 跳转到S_FEATURE_CONV状态
- 根据layer_id的值,判断是哪一层的前向计算,生成相应的参数地址和数据地址
case S_IDLE:
// 根据layer_id的值,判断是哪一层的前向计算,生成相应的参数地址和数据地址
switch (layer_id) {
case 0:
// 如果layer_id等于0,表示是第0层的前向计算,参数地址为0x10000000,数据地址为0x30000000
param_addr = (u32)0x10000000;
data_addr = (u32)0x30000000;
break;
case 1:
// 如果layer_id等于1,表示是第1层的前向计算,参数地址为0x10080000,数据地址为0x20000000
param_addr = (u32)0x10080000;
data_addr = (u32)0x20000000;
break;
case 2:
// 如果layer_id等于2,表示是第2层的前向计算,参数地址为0x10100000,数据地址为0x20000000
param_addr = (u32)0x10100000;
data_addr = (u32)0x20000000;
break;
default:
// 其他情况下,打印错误信息,并返回
printf("Error: Invalid layer id\n");
return XST_FAILURE;
}
// 从参数地址开始,发送BIAS、ACT、WEIGHT三种参数到PL端
// 发送BIAS参数
Xil_Out32(ADDR_REG_1, param_addr);
Xil_Out32(ADDR_REG_2, BIAS_DATA);
Xil_Out32(ADDR_REG_3, BIAS_LEN);
Xil_Out32(ADDR_REG_4, CMD_BIAS);
// 发送ACT参数
Xil_Out32(ADDR_REG_1, param_addr + BIAS_LEN);
Xil_Out32(ADDR_REG_2, ACT_DATA);
Xil_Out32(ADDR_REG_3, ACT_LEN);
Xil_Out32(ADDR_REG_4, CMD_ACT);
// 发送WEIGHT参数
Xil_Out32(ADDR_REG_1, param_addr + BIAS_LEN + ACT_LEN);
Xil_Out32(ADDR_REG_2, WEIGHT_DATA);
Xil_Out32(ADDR_REG_3, WEIGHT_LEN);
Xil_Out32(ADDR_REG_4, CMD_WEIGHT);
// 跳转到S_FEATURE_CONV状态
state = S_FEATURE_CONV;
break;
- 在S_FEATURE_CONV状态中,完成以下操作:
- 根据tx_cnt和batch_cnt的值,判断是否是第一次或最后一次发送数据,生成相应的命令和数据长度
- 如果tx_cnt等于0,表示是第一次发送数据,命令为CMD_TX_FIRST,数据长度为TX_LEN_FIRST
- 如果tx_cnt等于TX_CNT_END,并且batch_cnt等于BATCH_CNT_END,表示是最后一次发送数据,命令为CMD_TX_LAST,数据长度为TX_LEN_LAST
- 其他情况下,表示是中间的发送数据,命令为CMD_TX_OTHER,数据长度为TX_LEN_OTHER
- 发送数据到PL端,并启动卷积计算
- 跳转到S_DMA_RX状态
- 根据tx_cnt和batch_cnt的值,判断是否是第一次或最后一次发送数据,生成相应的命令和数据长度
case S_FEATURE_CONV:
// 根据tx_cnt和batch_cnt的值,判断是否是第一次或最后一次发送数据,生成相应的命令和数据长度
if (tx_cnt == 0) {
// 如果tx_cnt等于0,表示是第一次发送数据,命令为CMD_TX_FIRST,数据长度为TX_LEN_FIRST
cmd = CMD_TX_FIRST;
tx_len = TX_LEN_FIRST;
} else if (tx_cnt == TX_CNT_END && batch_cnt == BATCH_CNT_END) {
// 如果tx_cnt等于TX_CNT_END,并且batch_cnt等于BATCH_CNT_END,表示是最后一次发送数据,命令为CMD_TX_LAST,数据长度为TX_LEN_LAST
cmd = CMD_TX_LAST;
tx_len = TX_LEN_LAST;
} else {
// 其他情况下,表示是中间的发送数据,命令为CMD_TX_OTHER,数据长度为TX_LEN_OTHER
cmd = CMD_TX_OTHER;
tx_len = TX_LEN_OTHER;
}
// 发送数据到PL端,并启动卷积计算
Xil_Out32(ADDR_REG_1, data_addr);
Xil_Out32(ADDR_REG_2, FEATURE_DATA);
Xil_Out32(ADDR_REG_3, tx_len);
Xil_Out32(ADDR_REG_4, cmd | CMD_CONV);
// 跳转到S_DMA_RX状态
state = S_DMA_RX;
break;
- 在S_DMA_RX状态中,完成以下操作:
- 根据tx_cnt和batch_cnt的值,判断是否是第一次或最后一次接收数据,生成相应的数据长度
- 如果tx_cnt等于0,表示是第一次接收数据,数据长度为RX_LEN_FIRST
- 如果tx_cnt等于TX_CNT_END,并且batch_cnt等于BATCH_CNT_END,表示是最后一次接收数据,数据长度为RX_LEN_LAST
- 其他情况下,表示是中间的接收数据,数据长度为RX_LEN_OTHER
- 从PL端接收数据,并存储到rx_addr指定的地址
- 更新tx_cnt、batch_cnt、data_addr和rx_addr的值
- 判断是否完成了当前层的所有计算,如果是,则跳转到S_FINISH状态,否则跳转到S_FEATURE_CONV状态
- 根据tx_cnt和batch_cnt的值,判断是否是第一次或最后一次接收数据,生成相应的数据长度
case S_DMA_RX:
// 根据tx_cnt和batch_cnt的值,判断是否是第一次或最后一次接收数据,生成相应的数据长度
if (tx_cnt == 0) {
// 如果tx_cnt等于0,表示是第一次接收数据,数据长度为RX_LEN_FIRST
rx_len = RX_LEN_FIRST;
} else if (tx_cnt == TX_CNT_END && batch_cnt == BATCH_CNT_END) {
// 如果tx_cnt等于TX_CNT_END,并且batch_cnt等于BATCH_CNT_END,表示是最后一次接收数据,数据长度为RX_LEN_LAST
rx_len = RX_LEN_LAST;
} else {
// 其他情况下,表示是中间的接收数据,数据长度为RX_LEN_OTHER
rx_len = RX_LEN_OTHER;
}
// 从PL端接收数据,并存储到rx_addr指定的地址
dma_rx(rx_addr, rx_len);
// 更新tx_cnt、batch_cnt、data_addr和rx_addr的值
tx_cnt++; // 增加发送计数器的值
if (tx_cnt > TX_CNT_END) {
// 如果发送计数器超过了最大值,说明已经完成了8个输出通道的计算
tx_cnt = 0; // 重置发送计数器为0
batch_cnt++; // 增加批次计数器的值
if (batch_cnt > BATCH_CNT_END) {
// 如果批次计数器超过了最大值,说明已经完成了所有输出通道的计算
batch_cnt = 0; // 重置批次计数器为0
data_addr = DATA_ADDR_INIT; // 恢复数据地址为初始值
rx_addr = RX_ADDR_INIT; // 恢复接收地址为初始值
} else {
// 否则,说明还有8个输出通道需要计算[^1^][1]
data_addr += DATA_ADDR_OFFSET; // 增加数据地址的偏移量,指向下一个8个输入通道的数据
rx_addr += RX_ADDR_OFFSET; // 增加接收地址的偏移量,指向下一个8个输出通道的存储位置
}
} else {
// 否则,说明还在同一个8个输出通道内进行计算
data_addr += DATA_ADDR_INC; // 增加数据地址的增量,指向下一个输入块的数据
rx_addr += RX_ADDR_INC; // 增加接收地址的增量,指向下一个输出块的存储位置
}
// 判断是否完成了当前层的所有计算,如果是,则跳转到S_FINISH状态,否则跳转到S_FEATURE_CONV状态
if (tx_cnt == TX_CNT_END && batch_cnt == BATCH_CNT_END) {
// 如果发送计数器和批次计数器都达到了最大值,说明当前层的所有计算都完成了
state = S_FINISH; // 跳转到S_FINISH状态
} else {
// 否则,说明还有更多的计算需要进行
state = S_FEATURE_CONV; // 跳转到S_FEATURE_CONV状态
}
break;
- 在S_FINISH状态中,完成以下操作:
- 输出当前层的计算结果,可以使用print_data函数打印rx_addr指向的数据
- 跳出状态机的循环,结束前向计算函数
case S_FINISH:
// 输出当前层的计算结果,可以使用print_data函数打印rx_addr指向的数据
print_data(rx_addr, RX_LEN_LAST);
// 跳出状态机的循环,结束前向计算函数
break;
注意:
- 在 PS 端编写代码时要注意寄存器地址、参数地址、数据地址、数据长度等值是否正确
- 在 PS 端调用 DMA 函数时,要注意以下几点:
- DMA 函数的参数要与发送或接收的数据地址、长度、方向一致
- DMA 函数要在发送或接收数据之前调用,以初始化 DMA 通道
- DMA 函数要在发送或接收命令之后调用,以启动 DMA 传输
- DMA 函数要等待传输完成后返回,以确保数据完整
- 在 PS 端进行 YOLO 网络的前向计算时,需要注意输出结果的正确性和精度。可以通过与软件模拟或硬件仿真的结果进行比对来验证输出结果是否正确,并通过调整参数、数据和命令的位宽、位数和格式来优化输出结果的精度。
- 在 PS 端实现前向计算函数时,需要注意每一层的参数、数据和命令的变化,以及循环次数和条件判断的正确性。可以通过参考仿真代码和表格来确定每一层的具体需求和控制流程,并在编写代码时进行注释和调试。