编辑语:
技术解码栏目:是面向开发者详细解读芯片开放社区(OCC)上关于处理器、芯片、基础软件平台、集成开发环境及应用开发平台的相关技术,方便开发者学习及快速上手,提升开发效率。
作者介绍
王松:复旦大学微电子学院2020级直博生
陈布:复旦大学微电子学院2019级硕士研究生
上期我们简单介绍了五子棋AI设备的开发过程,讲解了基于博弈树的五子棋AI算法的原理,今天我们就带大家深入了解该实战案例的开发过程。
本文将分为五子棋AI核心算法的硬件实现、软硬件协同仿真两部分内容,为大家详细阐述基于wujian100 SoC设计开发五子棋AI设备的过程。
一、五子棋AI核心算法的硬件实现
本次工作中,我们使用XILINX VIVADO HLS编写C++源程序,综合出硬件Verilog代码,封装成IP核,然后将其集成于wujian100 SoC平台之中。
1.1 极大极小值搜索算法的编程实现
代码主体本质上是一个递归函数,但由于递归函数无法被综合(因为可能出现无穷无尽的迭代,硬件上无法实现),在考虑到最大搜索深度不会超过4层(否则,搜索时间太长)的前提下,实例化4个相同的函数,以供层层调用。
编程思路如下:
- 第一步,假设我方(或者对方)落子,更新棋盘状态;
- 判断棋面输赢,如果已经输了或者赢了就没必要继续搜索下去了;
- 否则,遍历棋盘上可落子的位置,假设对方(或者我方)落子,进入下一层搜索;
- 得到上一步搜索返回的权值weight,更新下层上界max(或者下层下界min);
- 当前节点为MAX_Node时,如果max ≥ alpha(此处的max是在当前节点的子节点中已搜索出的最大值,在全部搜索完成前等价于当前节点的 α 值;此处的alpha是当前节点的父节点下的其它子节点中已搜索出的最小的 α 值,在全部搜索完成前等价为上层父节点的 β 值。即满足当前节点的α ≥ 父节点的β的条件),则可以直接返回至上一层,将当前节点及其子节点全部剪枝,即 α 剪枝;
- 当前节点为MIN_Node时,如果min ≤ beta(此处的min是在当前节点的子节点中已搜索出的最小值,在全部搜索完成前等价于当前节点的 β 值;此处的beta是当前节点的父节点下的其它子节点中已搜索出的最大的 β 值,在全部搜索完成前等价为上层父节点的 α 值。即满足当前节点的β ≤ 父节点的α的条件),则可以直接返回至上一层,将当前节点及其子节点全部剪枝,即 β 剪枝;
- 否则,返回已搜索出的下层节点中的最大值max(或者是最小值min);
- 搜索到限定层数之后,评估棋面,返回权值。
相关参数定义如下:
参数 | 定义 |
ChessBoard |
用于存放棋盘状态的二维数组 |
BOARD_SIZE |
棋盘规格,默认为15 × 15 |
x , y |
当前节点的位置坐标 |
type |
当前节点的类型(MAX_Node / MIN_Node ) |
depth |
搜索深度(从0开始,递归一次,数值加1) |
alpha |
当前节点的父节点下的其它子节点中已搜索出的最小的 α 值,即当前层的下界 |
beta |
当前节点的父节点下的其它子节点中已搜索出的最大的 β 值,即当前层的上界 |
部分源代码如下:
1. for (j = 0; j < BOARD_SIZE; ++j) { 2. for (i = 0; i < BOARD_SIZE; ++i) { 3. if (ChessBoard[j][i] == EMPTY && canSearch(ChessBoard, i, j)) { 4. weight = minMax3(ChessBoard, i, j, nextType(type), depth + 1, min, max); 5. 6. if (weight > max) 7. max = weight; // 更新下层上界 8. if (weight < min) 9. min = weight; // 更新下层下界 10. 11. // alpha-beta 12. if (type == MAX_NODE) { 13. if (max >= alpha) { 14. ChessBoard[y][x] = EMPTY; 15. return max; 16. } 17. } 18. else { 19. if (min <= beta) { 20. ChessBoard[y][x] = EMPTY; 21. return min; 22. } 23. } 24. } 25. else 26. continue; 27. } 28. } c++
1.2 五子棋IP核集成方法
设计完成五子棋IP核之后,将其集成到wujian100 SoC平台之中,用Vivado综合出网表,并生成bit流,下载到平头哥的XC7A-FPGA开发板中。
然后使用配套的CDK开发软件编写C程序,通过uart串口与计算机通信,与计算机端Python程序进行联合仿真。在此之前,有必要先简单介绍一下wujian100 SoC平台。
1.2.1 wujian100 SoC平台简介
wujian100_open是一个基于MCU的SoC平台,支持通过EDA工具进行前端仿真和制作FPGA进行测试,其硬件组成结构如下图所示:
我们的IP核就是挂载于图中低速总线上的Dummy 0/1/2/3预留接口处。
1.2.2 平头哥 XC7A-FPGA开发板简介
平头哥开发的基于Xilinx Artix-7系列FPGA的开发板主要用于平头哥中低端CPU核的验证和评估,板上集成了Xilinx Artix-7 XC7A200T FPGA芯片。
1.2.3 五子棋AI IP核设计
整个IP核的顶层采用了AXI-Stream协议进行数据传输,共存在三个接口:
参数 | 定义 |
control_in |
控制端口 |
data_in |
数据输入端口 |
data_out |
数据输出端口 |
对control_in写“0”会将棋盘复位,写“1”之后即开始通过数据加载模块data_in接收输入落子坐标,经过核心计算模块计算之后,通过数据输出模块data_out输出五子棋AI的落子坐标。
我们这个IP是用Vivado HLS进行设计的,在整个程序中,对棋盘进行遍历和判断时用到了大量的for循环,Vivado HLS提供了一个流水线优化,我们对所有的循环进行了优化,综合之后得到以下Timing报告:
预估最大时钟频率可达106.22 MHz,各类资源消耗如下图所示:
在这个IP核设计中并没有用到DMA,因为这里主要涉及到的位置为坐标,不涉及到大量的数据传输,所以,我们直接将其挂载到E902上,通过AHB转AXI的IP核相连接。
E902则是在接收到PC端落子坐标的信息后,把数据传输到IP核,IP核进行落子判断后,把数据返回给E902,E902再返回给PC。
二、软硬件协同仿真
我们在使用Vivado HLS工具设计完成了五子棋AI的C++源程序之后,同时编写了C++测试程序,初步仿真算法程序的功能正确性。测试程序tb.cpp主模拟了四步落子输入:
1. int posx[4] = {7,7,8,8}; 2. int posy[4] = {7,8,6,7}; c++
仿真结果如下:
1. 6 6 7 6 6 8 6 7
下图直观地反映了仿真时的博弈过程:
从五子棋的常用战术来看,我们的五子棋AI算法的每一步“出招”都合乎预期,但是,这仅仅只能当做初步的仿真判断。
为了更准确地评估算法的智能水平,我们将IP核挂载在了wujian100 SoC平台上,并下载到FPGA开发板中,与计算机之间通过uart串口进行通信,分别使用CDK开发工具和Python完成联合仿真。
2.1 CDK C源程序
wujian100 开源项目中包含了一个uart的example程序,我们在此基础上编写完成CDK C源程序,程序的主要功能描述如下:
- 通过串口接收计算机端Python程序发送过来的落子坐标或者是重开棋局信号reset;
- 在接收到计算机端reset信号之后,通过往GB_CONTROL_IN_BADDR端口写“0”来重开棋局;
- 在接收到计算机端落子坐标之后,将其发送给五子棋AI IP核;
- 接收五子棋AI IP核返回的落子坐标,并将其发送给计算机。
(1)接收计算机端落子坐标的方法:
uart串口发送过来的数据是32位int型,但是uart在发送数据时是按一个byte接一个byte来的,先发送的是低8位数据,因此在接收的时候要将4个byte的数据拼接成一个32位数据:
1. static uint8_t posPlayer[8] = {0}; // 数组存放的是连续接收到的8个字节的坐标数据 2. ... 3. x_tmp = posPlayer[3]<<24 | posPlayer[2]<<16 | posPlayer[1]<<8 | posPlayer[0]; 4. y_tmp = posPlayer[7]<<24 | posPlayer[6]<<16 | posPlayer[5]<<8 | posPlayer[4]; 5. x_pos = *(uint32_t *) & x_tmp; // 计算机落子横坐标 6. y_pos = *(uint32_t *) & y_tmp; // 计算机落子纵坐标 c
(2)复位棋局的方法:
将计算机端发送过来的坐标x, y都等于99作为复位本局游戏的reset信号(补充:如果x, y = 15, 15,表示硬件端AI先手落子,由IP核内部相应的函数控制):
1. if(x_pos == 99 && y_pos == 99) *(volatile uint32_t *) GB_CONTROL_IN_BADDR = 0x0; c
(3)向IP核发送坐标点的方法:
先向GB_CONTROL_IN_BADDR写“1”,然后向GB_DATA_IN_BADDR写横坐标x,再向GB_DATA_IN_BADDR写纵坐标y。
1. *(volatile uint32_t *) GB_CONTROL_IN_BADDR = 0x1; 2. *(volatile uint32_t *) GB_DATA_IN_BADDR = x_pos; 3. *(volatile uint32_t *) GB_DATA_IN_BADDR = y_pos; c
(4)接收AI落子坐标的方法:
在向IP核和发送完坐标之后,C程序会等待IP核计算完成并返回落子坐标。
之前说过,IP核的数据传输方式是AXI Stream协议,我们将其valid信号接到了wujian100 SoC的中断引脚上。当IP核计算完成发回数据时,会触发MCU的中断,然后,在其中断服务程序go_bang_intr()中接收AI落子坐标,并通过串口发送到计算机端:
1. void go_bang_intr(void) 2. { 3. xy_result[0] = *(volatile uint32_t *) GB_DATA_OUT_BADDR; 4. xy_result[1] = *(volatile uint32_t *) GB_DATA_OUT_BADDR; 5. printf("%d\n", xy_result[1]); 6. printf("%d\n", xy_result[0]); 7. } c
2.2 计算机端Python源程序
前面提到过,我们的五子棋AI算法只是五子棋游戏的核心算法,相应的控制程序是通过Python来编写的,为此,我们参考了以下项目中完整的Python五子棋游戏:
参考项目中同样使用Alpha-Beta剪枝算法实现了一个五子棋的AI,只不过其估值函数与我们的硬件AI有所不同。同时项目中还包含完整的人机交互界面,我们在其基础上修改源程序,实现Python端AI与硬件端AI的即时对战。
Python端AI的落子坐标将通过串口发送到FPGA开发板:
1. posPlayer = [] 2. x, y = self.AI.findBestChess(self.map.map, self.player) 3. ... 4. posPlayer.append(struct.pack('<i', int(x)).hex()) 5. posPlayer.append(struct.pack('<i', int(y)).hex()) 6. tmp = "".join(posPlayer) 7. data_in = binascii.unhexlify(tmp) 8. ser.write(data_in) # 通过串口发送Python端落子坐标 python
然后,Python端将通过另一个线程来接收FPGA端通过串口发送回来的硬件端AI落子坐标:
1. posAI = [] 2. ... 3. def Receiving(): # 接收函数 4. global posAI 5. while True: # 循环接收数据 6. if ser.in_waiting: # 当接收缓冲区中的数据不为零时,执行下面的代码 7. strr = ser.read(ser.in_waiting).decode("gbk") 8. posAI.append(strr) 9. else: 10. if threading_end_flag: 11. break python
游戏的画面显示以及游戏胜负的最终判断都是在计算机端通过Python程序来完成。
除此之外,我们也可以让硬件端AI与人类玩家对战,实现人机对战,只需将Python AI的落子坐标替换成人类玩家的落子坐标即可,具体方法不再赘述。
2.3 硬件端AI vs 软件端AI
至此,整个软硬件协同仿真的过程可以总结如下:
- CDK C程序初始化五子棋AI IP核,等待接收计算机端Python AI落子坐标;
- Python源程序初始化,通过串口向FPGA发送Python AI落子坐标;
- FPGA端接收Python AI落子坐标,将其发送给硬件五子棋AI IP核;
- FPGA端通过中断来接收五子棋AI IP核返回的落子坐标,将其通过串口发送给计算机,再次等待接收计算机端Python AI落子坐标。
硬件端AI vs 软件端AI的记录如下图所示:
其中白方属于硬件端AI,黑方属于软件端AI,白方先手。利用timer不完全精确地记录双方运行时间后,得到以下加速比(双方AI算法不完全相同,仅作参考):
2.4 人机对战实验
由于五子棋AI使用的算法是相对固定的,两个AI的对战并不会产生多样性,因此,有必要考虑让我们的AI去与真正的人类对弈。
为此,我们特地用Python编写了一个Script,让我们的五子棋AI能够与QQ游戏大厅中广大的五子棋玩家对弈,同时记录数据(视频演示见本篇开头链接)。
在花费了两个星期的时间记录了700场对局之后,我们剔除了少部分步数不超过10步的对局,有效的对局数一共有673场,并统计了先手胜率和后手胜率,以及对局结束后总步数的分布情况,如下表所示:
先后手 | 场次 | 胜率 |
先手 | 333 | 84.08% |
后手 | 340 | 70.00% |
总共 | 673 | 76.97% |
上图是对局步数分布直方图 & 胜率分布折线图,从上图可以看出80%的对局步数都在50步以内,同时可以发现我们的AI的胜率随着对局步数的增加呈现出先下降后上升的趋势,在对局步数处于80-89步之间的时候胜率降至最低——40.0%,在考虑到玩家水平差异后分析得出以下结论:
- 大部分玩家与我们的AI“过不了几招”;
- 在90回合以后,玩家们与我们的AI“纠缠“越久越难取胜;
- 即使是高水平的玩家想要战胜我们的AI也并不容易,往往需要足够的回合数。
三、总结
我们使用Vivado HLS设计实现了基于Alpha-Beta剪枝算法的五子棋AI,并将其集成于wujian100 SoC之中,然后使用FPGA开发板通过串口与计算机相互通信完成软硬件协同仿真,仿真结果充分体现出了硬件加速的效果。
在此基础上,我们还编写脚本实现五子棋AI与广大线上玩家对弈,验证了五子棋AI的智能水平。