CH32V307教程[第十集][又见贪吃蛇——网络版]
在上一版介绍用赤兔开发板设计贪吃蛇(https://verimake.com/d/156-ch32v3071)的文章里面最后建议可以将项目设计成网络游戏。结合这一篇介绍赤兔开发板以太网模块使用的文章(https://verimake.com/d/169-ch32v307),今天就介绍下将贪吃蛇设计成网络版。
一、项目简介
使用两块赤兔开发板,通过以太网模块用网线将两块赤兔连接起来。其中一台作为TCP Server,另一台作为TCP Client。
TCP Server:用来监听网络,连接来自Client端的请求,此外执行贪吃蛇程序,但是蛇的运动由Client端来控制。
TCP Client:用来连接Server,通过采样五相开关获得方向控制指令,将方向指令通过网线发送给Server。
本设计只实现了一台赤兔通过网络将按键值发送到Server端,并且Server端将利用收到的信息来控制蛇的运动。为了本教程简单易懂并没有实现Server端另一条蛇的控制,相信在本教程的引导下各位看官能够完成其他更有意思的功能,大家可以将自己的设计发布在评论区讨论哦。
二、TCP Server端设计
1、移植贪吃蛇到工程
打开沁恒EVT样例里面的ETH目录下的TCP Server样例。该样例类似于CH32V307教程[第九集][以太网] (https://verimake.com/d/169-ch32v307)介绍的使用。我们现在需要将该样例和贪吃蛇相结合。即将贪吃蛇的程序复制到该样例中。我们需要将贪吃蛇里如下函数拷贝过来:
void creatsnake(); //创建蛇
void printsnake(); //打印蛇
void creatfood(); //创建食物
void movesnake(); //移动蛇
void eatfood(); //吃食物
int judge(); //判定游戏
此外还需要把相关的数据结构和宏定义一并拷贝过来。(当然高手的话我相信已经把贪吃蛇封装成.c和.h文件了,只需要include "snacke.h"就可以了。我们这里还是使用比较基础的方式来实现,单一文件复制粘贴的方式。啊哈哈哈)。
#define map_row 198
#define map_col 234
#define x_min 3
#define x_max 236
#define y_min 3
#define y_max 200
#define up 1
#define down 2
#define left 3
#define right 4
#define sel 5
#define sw1 6
#define sw2 7
uint8_t key = 0;
uint8_t dir = 0;
uint64_t score =0;
typedef struct Snakes
{
int x;
int y;
struct Snakes *next;
}snake;
snake *head;
struct Foods
{
int x;
int y;
}food;
将lcd库里的lcd.c和lcd.h拷贝到工程的User目录下。并使用#include "lcd.h"添加工程头文件,以备使用液晶屏。
在工程main.c的main函数中添加贪吃蛇的初始化函数:
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_RNG, ENABLE); //随机数发生器时钟
RNG_Cmd(ENABLE);
creatsnake(); //创建蛇
printsnake(); //打印蛇
creatfood(); //创建食物
在贪吃蛇的样例中,在while部分如下:
while(judge()){
getkey = Basic_Key_Handle();
if (getkey) {
key=getkey;
}
movesnake();
eatfood();
Delay_Ms(250); //延时
}
其中按键的读取是通过另一块赤兔发送过来,所以这部分代码需要去掉,只剩下移动蛇和吃食物加延时了。那么这部分该怎么加入到TCP Server的例程中呢?我们先来看下TCP Server例程中while部分的代码:
while(1)
{ /*以太网库主任务函数,需要循环调用*/
if(WCHNET_QueryGlobalInt()) /*查询以太网全局中断,如果有中断,调用全局中断处理函数*/
{
WCHNET_HandleGlobalInt();
}
}
如果添加进这里,需要这样修改:
while(1)
{
WCHNET_MainTask(); /*以太网库主任务函数,需要循环调用*/
if(WCHNET_QueryGlobalInt()) /*查询以太网全局中断,如果有中断,调用全局中断处理函数*/
{
WCHNET_HandleGlobalInt();
}
}
这样编写可以理解上面的以太网接收按键值,然后控制蛇和我们的贪吃蛇流程一样。但是这里如果这样编写是不可以的。主要问题出在Delay_Ms()上。我们来看下这个函数的原型。
void Delay_Ms(uint32_t n)
{
uint32_t i;
SysTick->CTLR = (1<<4);
i = (uint32_t)n*p_ms;
SysTick->CMP = i;
SysTick->CTLR |= (1<<5)|(1<<0);
while((SysTick->SR & (1<<0)) != (1<<0));
SysTick->SR &= ~(1<<0);
}
这段代码中while((SysTick->SR & (1<<0)) != (1<<0));
这段代码会照成main函数中的while里面程序执行的阻塞,即程序会在Delay_Ms这段等待(这也是Delay_Ms的作用),这样的阻塞会使得以太网例程中一个重要函数的循环执行:WCHNET_MainTask(); 如果在Delay_Ms中的延时阻塞时间过长就会照成TCP Server中的TCP连接超时(TCP Timout)。那么这段代码在贪吃蛇中是为了让蛇跑的慢点,如果没有这个Delay_Ms会使得蛇运动过快人眼都看不见嗖就没影子了。那么我们解决方法就是使用一个定时器,定时让蛇移动和吃食物就解决了。
参照CH32V307教程 [第三集] [时钟] (https://verimake.com/d/151-ch32v307)教程里面的例子,为了以示区别我们使用定时器7,初始化定时器7以及中断函数编写如下:
void Refresh_TIM_Init( u16 arr, u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
RCC_APB1PeriphClockCmd( RCC_APB1Periph_TIM7, ENABLE );
TIM_TimeBaseInitStructure.TIM_Period = arr;
TIM_TimeBaseInitStructure.TIM_Prescaler = psc;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Down;
TIM_TimeBaseInit( TIM7, &TIM_TimeBaseInitStructure);
TIM_ITConfig(TIM7, TIM_IT_Update, ENABLE);
TIM_ARRPreloadConfig( TIM7, ENABLE );
TIM_Cmd( TIM7, ENABLE );
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM7_IRQn ;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; //子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
void TIM7_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void TIM7_IRQHandler(void)
{
TIM_ClearFlag(TIM7, TIM_FLAG_Update);//清除标志位
if (judge()) {
movesnake();
eatfood();
}
else {
lcd_show_string(50, 120, 32, "GAME OVER");
}
}
这样main函数中的while部分就不用做任何修改,只需要在main函数中添加定时器初始化。
Refresh_TIM_Init( 2500-1, (SystemCoreClock/10000)-1 );//定时器每250ms中断一次
在中断函数中执行移动蛇、吃食物和判定程序。
这个样例也充分说明了在一般复杂代码中需要谨慎使用Delay_Ms()这样软件延时函数。这样函数会影响其他程序的执行效率。<font color=#FF0000 size=5 face="黑体">重要!!</font>
接下来我们处理接收部分,本例程设计是发送'up'、'down'、'left'、'right'这样的字符串(个人可以按自己喜好设计发送内容)。那么在接收部分只需要按接收的字符,设置key这个变量的值,因为这个全局变量会在movesnake()函数中控制蛇的移动。代码如下:
void WCHNET_HandleSockInt(u8 sockeid,u8 initstat)
{
u32 len;
if(initstat & SINT_STAT_RECV) /* socket接收中断*/
{
connectstat=1;
len = WCHNET_SocketRecvLen(sockeid,NULL); /* 获取socket缓冲区数据长度 */
printf("WCHNET_SocketRecvLen %d %d\r\n",len,sockeid);
WCHNET_SocketRecv(sockeid,MyBuf,&len); /* 获取socket缓冲区数据 */
WCHNET_SocketSend(sockeid,MyBuf,&len); /* 演示回传数据 */
if(strncmp(MyBuf,"up",len)==0)
{
key = up;
}
else if(strncmp(MyBuf,"down",len)==0)
{
key = down;
}
else if(strncmp(MyBuf,"left",len)==0)
{
key = left;
}
else if(strncmp(MyBuf,"right",len)==0)
{
key = right;
}
}
if(initstat & SINT_STAT_CONNECT) /* socket连接成功中断*/
{
connectstat=1;
printf("TCP Connect Success\r\n");
WCHNET_ModifyRecvBuf(sockeid, (u32)SocketRecvBuf[sockeid], RECE_BUF_LEN);
}
............... //以下部分不需要修改,此处省略
此处代码添加非常简单,只需要判断MyBuf接收的是不是上下左右四个方向的值就行了。
2、测试TCP Server代码
到此TCP Server部分的代码就编写的差不多了。那么程序运行会有什么问题,如何调试呢?这里我们是先使用PC端的网络调试助手测试功能。(千万不要编写好TCP Client端的代码一起联调,这样到时候就不知道到底哪一端出问题了)。打开网络调试助手,设置协议类型为TCP Client(现在赤兔是Server哦),远程主机地址输入赤兔的IP,远程主机端口按照程序里的设置填写。然后事先在一个text文件里输入好'up'、'down'、'left'、'right',直接复制粘贴到发送区。观察赤兔屏幕上的蛇的移动。有问题的欢迎在评论区里发帖讨论哦。
三、TCP Client端设计
1、移植五相开关到工程
这里开始使用第二块赤兔开发板,打开沁恒EVT样例中ETH目录下的TCP Clinet工程,第一步将TCP Client工程中void WCHNET_HandleSockInt(u8 sockeid,u8 initstat)
函数中的WCHNET_SocketSend(sockeid,MyBuf,&len); /* 演示回传数据 */
这个代码注释掉,不然两个开发板联调的时候就会两个开发板之间不停的一发一收收了又发给了对方停不下来(尴尬)。然后复制五相开关的相关函数,一个GPIO初始化函数和一个按键扫描程序。如下:
void GPIO_INIT(){
GPIO_InitTypeDef GPIO_InitTypdefStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOD|RCC_APB2Periph_GPIOE,ENABLE);
GPIO_InitTypdefStruct.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5;
GPIO_InitTypdefStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitTypdefStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOE, &GPIO_InitTypdefStruct);
GPIO_InitTypdefStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitTypdefStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitTypdefStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitTypdefStruct);
GPIO_InitTypdefStruct.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_13;
GPIO_InitTypdefStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitTypdefStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOD, &GPIO_InitTypdefStruct);
}
uint8_t Basic_Key_Handle( void )
{
uint8_t keyval = 0;
if( ! GPIO_ReadInputDataBit( GPIOE, GPIO_Pin_4 ) )
{
Delay_Ms(20);
if( ! GPIO_ReadInputDataBit( GPIOE, GPIO_Pin_4 ) )
{
keyval = sw1;
}
}
else {
if( ! GPIO_ReadInputDataBit( GPIOE, GPIO_Pin_5 ) )
{
Delay_Ms(20);
if( ! GPIO_ReadInputDataBit( GPIOE, GPIO_Pin_5 ) )
{
keyval = sw2;
}
}
else {
if( ! GPIO_ReadInputDataBit( GPIOE, GPIO_Pin_1 ) )
{
Delay_Ms(20);
if( ! GPIO_ReadInputDataBit( GPIOE, GPIO_Pin_1 ) )
{
keyval = up;
}
}
else {
if( ! GPIO_ReadInputDataBit( GPIOE, GPIO_Pin_2 ) )
{
Delay_Ms(20);
if( ! GPIO_ReadInputDataBit( GPIOE, GPIO_Pin_2 ) )
{
keyval = down;
}
}
else {
if( ! GPIO_ReadInputDataBit( GPIOE, GPIO_Pin_3 ) )
{
Delay_Ms(20);
if( ! GPIO_ReadInputDataBit( GPIOE, GPIO_Pin_3 ) )
{
keyval = right;
}
}
else {
if( ! GPIO_ReadInputDataBit( GPIOD, GPIO_Pin_6 ) )
{
Delay_Ms(20);
if( ! GPIO_ReadInputDataBit( GPIOD, GPIO_Pin_6 ) )
{
keyval = left;
}
}
else {
if( ! GPIO_ReadInputDataBit( GPIOD, GPIO_Pin_13 ) )
{
Delay_Ms(20);
if( ! GPIO_ReadInputDataBit( GPIOD, GPIO_Pin_13 ) )
{
keyval = sel;
}
}
}
}
}
}
}
}
return keyval;
}
这里的按键程序程序里面有Delay_Ms函数,如果放在main函数的while中会影响以太网的连接。通过代码我们不难看出按键是读取一次gpio口状态,然后延时20Ms看如果这个IO口的状态不变说明这个按键按下了(这样是防止按键抖动)。所以我们只需要设置一个20Ms的定时器,定时扫描按键,如果有按键按下做好记录,等下一次依然是这个按键说明该按键按下了。除此之外如果检测到两次按键,并且两次按键值相同的话说明两次输入的键值一样,这样的情况下是没有必要发送两次数据,只需要一次即可,即如果两次或两次以上连续的输入是同一个按键值只需要发送一次按键值即可。GPIO初始化代码不变,添加定时器代码如下:
uint8_t Basic_Key_Handle( void )
{
uint8_t keyval = 0;
if( ! GPIO_ReadInputDataBit( GPIOE, GPIO_Pin_4 ) )
{
keyval = sw1;
}
else {
if( ! GPIO_ReadInputDataBit( GPIOE, GPIO_Pin_5 ) )
{
keyval = sw2;
}
else {
if( ! GPIO_ReadInputDataBit( GPIOE, GPIO_Pin_1 ) )
{
keyval = up;
}
else {
if( ! GPIO_ReadInputDataBit( GPIOE, GPIO_Pin_2 ) )
{
keyval = down;
}
else {
if( ! GPIO_ReadInputDataBit( GPIOE, GPIO_Pin_3 ) )
{
keyval = right;
}
else {
if( ! GPIO_ReadInputDataBit( GPIOD, GPIO_Pin_6 ) )
{
keyval = left;
}
else {
if( ! GPIO_ReadInputDataBit( GPIOD, GPIO_Pin_13 ) )
{
keyval = sel;
}
}
}
}
}
}
}
return keyval;
}
void Refresh_TIM_Init( u16 arr, u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
RCC_APB1PeriphClockCmd( RCC_APB1Periph_TIM7, ENABLE );
TIM_TimeBaseInitStructure.TIM_Period = arr;
TIM_TimeBaseInitStructure.TIM_Prescaler = psc;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Down;
TIM_TimeBaseInit( TIM7, &TIM_TimeBaseInitStructure);
TIM_ITConfig(TIM7, TIM_IT_Update, ENABLE);
TIM_ARRPreloadConfig( TIM7, ENABLE );
TIM_Cmd( TIM7, ENABLE );
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM7_IRQn ;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; //子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
void TIM7_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void TIM7_IRQHandler(void)
{
uint32_t len2=2;
uint32_t len4=4;
uint32_t len5=5;
static uint32_t cnt=0;
uint8_t getkey =0;
static uint8_t getkeyprev =0;
uint8_t scankey =0;
static uint8_t scankeyprev =0;
if (cnt%2) {
scankey = Basic_Key_Handle();
}else {
scankeyprev = Basic_Key_Handle();
}
if (scankey == scankeyprev) {
getkey = scankey;
}
if (linkstatus) {
if (getkey!=0 & (getkeyprev != getkey)) {
switch (getkey) {
case up:
WCHNET_SocketSend(SocketId,"up",&len2);
break;
case down:
WCHNET_SocketSend(SocketId,"down",&len4);
break;
case left:
WCHNET_SocketSend(SocketId,"left",&len4);
break;
case right:
WCHNET_SocketSend(SocketId,"right",&len5);
break;
default:
break;
}
getkeyprev =getkey;
}
}
cnt++;
TIM_ClearFlag(TIM7, TIM_FLAG_Update);//清除标志位
}
该段代码有三个函数,首先修改了按键读取函数,将原来需要Delay_Ms函数部分全部去掉了,这样每次调用该函数就可以获得当前按键的值。然后设置了定时器初始化函数。在定时器的中断函数中做了很多设计,我们接下来看下这部分代码:
void TIM7_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void TIM7_IRQHandler(void)
{
uint32_t len2=2; //发送信息的长度2字符
uint32_t len4=4; //发送信息的长度4字符
uint32_t len5=5; //发送信息的长度5字符
static uint32_t cnt=0; //用于区分是哪一次扫描
uint8_t getkey =0; //保存当前获得的有效按键值
static uint8_t getkeyprev =0; //保存上一次发送的按键值
uint8_t scankey =0; //保存当前扫描的按键值
static uint8_t scankeyprev =0; //保存上一次扫描的按键值
if (cnt%2) {
scankey = Basic_Key_Handle();//读取按键值
}else {
scankeyprev = Basic_Key_Handle();//间隔20Ms读取按键值
}
if (scankey == scankeyprev) { //两次扫描按键值一样则为有效按键,去抖动
getkey = scankey; //将扫描有效的按键值赋值给getkey保存
}
if (getkey!=0 & (getkeyprev != getkey)) { //如果当前有按键按下并且和上次发送按键值不同则执行
switch (getkey) {
case up:
WCHNET_SocketSend(SocketId,"up",&len2);//发送案件信息给TCP Server
break;
case down:
WCHNET_SocketSend(SocketId,"down",&len4);
break;
case left:
WCHNET_SocketSend(SocketId,"left",&len4);
break;
case right:
WCHNET_SocketSend(SocketId,"right",&len5);
break;
default:
break;
}
getkeyprev =getkey; //存储当前发送按键值到prev变量
}
cnt++;
TIM_ClearFlag(TIM7, TIM_FLAG_Update);//清除标志位
}
最后记得在main函数中调用定时器初始化函数,设置定时器为20Ms中断一次。
Refresh_TIM_Init( 200-1, (SystemCoreClock/10000)-1 );//初始化定期器20Ms中断一次
2、测试TCP Client代码
打开网络调试助手,设置协议类型为TCP Server(现在赤兔是Client哦),本地主机地址输入PC主机的IP,本地主机端口按照程序里的设置填写。连接成功后,拨动五相开关观察接收的信息,出现'up'、'down'、'left'、'right'就正确了,而且如果两次按键一样不会重复发送哦。有问题的欢迎在评论区里发帖讨论哦。
三、两台联调
终于可以两台赤兔通信了,开始时候需要给两台赤兔配置IP,首先假设Server的IP尾数为10,Client的IP尾数为14,则在作为Server的赤兔的main.c文件中IP设置部分设置如下:
u8 MACAddr[6]; /*Mac地址*/
u8 IPAddr[4] = {192,168,1,10}; /*IP地址*/
u8 GWIPAddr[4] = {192,168,1,1}; /*网关*/
u8 IPMask[4] = {255,255,255,0}; /*子网掩码*/
u8 DESIP[4] = {192,168,1,14}; /*目的IP地址*/
作为Client的赤兔的main.c文件中IP设置部分如下:
u8 MACAddr[6]; /*Mac地址*/
u8 IPAddr[4] = {192,168,1,14}; /*IP地址*/
u8 GWIPAddr[4] = {192,168,1,1}; /*网关*/
u8 IPMask[4] = {255,255,255,0}; /*子网掩码*/
u8 DESIP[4] = {192,168,1,10}; /*目的IP地址*/
在下载的时候注意保证只有一个赤兔连接在电脑上,不然有可能两个赤兔下载了同一个代码哦。不要问我怎么知道的(尴尬),下载好代码后两个赤兔都通过串口调试助手观察连接状态。如下图:
接下来就可以使用赤兔通过以太网控制另一台赤兔上的贪吃蛇了。
四、未完成部分
本设计只实现了利用以太网将两台赤兔连接通信。其实还可以在Server机器上再创建一条蛇,两条蛇进行对战,这部分就不往下写了,留点大家试一试的空间啊。
还可以利用PC作为TCP Server,做一个网页版贪吃蛇,赤兔作为远程手柄控制贪吃蛇,这样可以实现更大的地图,更多的玩法实现哦。有兴趣的也可以尝试了,最后不要忘了来论坛发布你的作品哦。