Online Version of Running Snake on WCH Chitu RISC-V MCU

In the previous tutorial, we used Chitu development board to design Snake (please visit Running Snake on WCH Chitu RISC-V MCU). At the end of the tutorial, it is suggested that the project also can be designed as an online-game. Combined with this article introducing the use of the Ethernet module of the Chitu development board (please visit Tutorial – OpenCH Chitu Development Board Ethernet Module), this episode will introduce how to design online version of Snake.

1.Project Introduction

We connect the two Chitu development boards with a network cable through an Ethernet module. One of them is used as TCP Server and the other is used as TCP Client.
TCP Server: used to monitor the network, connect to requests from the client, and execute the snake program, but the movement of the snake is controlled by the client.
TCP Client: used to connect to the server, obtain the direction control command by sampling the five to switch, and send the direction command to the server through the network cable.

This design only realizes that Chitu sends the key value to the server side through the network, and the server side will use the received information to control the movement of the snake. In order to make this tutorial easy to understand, it does not realize the control of another snake on the server side. I believe that under the guidance of this tutorial, you can complete other more interesting functions. You can post your design in the comment area for discussion.

2. TCP Server Design

1. Copy Snake to the project

Open the TCP Server sample under the ETH directory in the WCH EVT sample. This example is similar to the usage introduced in the CH32V307 tutorial [Episode IX][Ethernet] (please vistTutorial – OpenCH Chitu Development Board Ethernet Module).We now need to combine this sample with Snake by copying project into this sample.

void creatsnake();  //create snake
void printsnake();  //print snake
void creatfood();   //create food
void movesnake();   //move snake
void eatfood();     //eat food
int judge();        //judge the game

In addition, you need to copy the relevant data structures and macro definitions together.

#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;

Copy lcd.c and lcd.h in the lcd library to the User directory of the project, and use #include "lcd.h" to add the project header file in preparation for using the LCD screen.
Add the initialization function of Snake to the main function of the project main.c:

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_RNG, ENABLE); //random number generator clock
   RNG_Cmd(ENABLE);

   creatsnake();        //create snake
   printsnake();        //print snake
   creatfood();         //create food
   ```
  In the Snake example, the while part is as follows:

  ```
while(judge()){
    getkey = Basic_Key_Handle();
    if (getkey) {
        key=getkey;
    }

    movesnake();
    eatfood();
    Delay_Ms(250);     //延时 delay
    }

The reading of the key is sent through another piece of Chitu board, so this part of the code needs to be removed, and only the moving snake and eating food are left with a delay. So how should this part be added to the routine of the TCP Server? Let's first look at the code of the while part of the TCP Server routine:

while(1)
{                                    /*Ethernet library main task function, which needs to be called cyclically*/
    if(WCHNET_QueryGlobalInt())      /*Query the Ethernet global interrupt, if there is an interrupt, call the global interrupt handler*/
    {
        WCHNET_HandleGlobalInt();
    }
}

If you add it here, you need to modify it like this:

while(1)
{
    WCHNET_MainTask();              /*Ethernet library main task function, which needs to be called cyclically*/
    if(WCHNET_QueryGlobalInt())     /*Query the Ethernet global interrupt, if there is an interrupt, call the global interrupt handler*/
    {
        WCHNET_HandleGlobalInt();
    }
}

In this way, the Ethernet can understand the received button value above, and then snake control process is the same as the Snake game. But it is not possible to write this way here. The main problem is with Delay_Ms(). Let's take a look at the prototype of this function.

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)), this code will block the execution of the program in the while in the main function, that is, the program will wait in the Delay_Ms section ( This is also the role of Delay_Ms); Such blocking executes a loop of an important function in the Ethernet routine: WCHNET_MainTask(). If the delay blocking time in Delay_Ms is too long, it will be reflected as TCP connection timeout in TCP Server (TCP Timout). The purpose of this code is to make the snake run slower. Without this Delay_Ms, the snake will move too fast. The solution is to use a timer to periodically make the snake move and eat.

Refer to the example in the CH32V307 tutorial [Episode 3] [Clock] (please visithttps://verimake.com/d/151-ch32v307) , in order to show the difference, we use timer 7, initialize timer 7 and write the interrupt function as follows:

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; //抢占优先级preemption priority
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;        //子优先级subpriority
    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);//清除标志位clear flag
    if (judge()) {
         movesnake();
         eatfood();
     }
    else {
        lcd_show_string(50, 120, 32, "GAME OVER");
    }
}

In this way, the while part in the main function does not need to be modified, and only the timer initialization needs to be added in the main function.

Refresh_TIM_Init( 2500-1, (SystemCoreClock/10000)-1 );//定时器每250ms中断一次The timer is interrupted every 250ms

Execute the moving snake, eating food and decision procedure in the interrupt function.

This example also fully illustrates the need to use software delay functions such as Delay_Ms() with caution in general complex codes. This function will affect the execution efficiency of other programs.

Next, we deal with the receiving part. This routine is designed to send strings such as 'up', 'down', 'left', and 'right' (you can design and send content according to your own preferences).Then in the receiving part, you only need to press the received character to set the value of the key variable, because this global variable will control the movement of the snake in the movesnake() function. The code is shown as below:

void WCHNET_HandleSockInt(u8 sockeid,u8 initstat)
{
    u32 len;

    if(initstat & SINT_STAT_RECV)                /* socket接收中断*/socket receive interrupt
    {
       connectstat=1;
       len = WCHNET_SocketRecvLen(sockeid,NULL);          /* 获取socket缓冲区数据长度  */Get socket buffer data length
       printf("WCHNET_SocketRecvLen %d  %d\r\n",len,sockeid);
       WCHNET_SocketRecv(sockeid,MyBuf,&len);                   /* 获取socket缓冲区数据 */Get socket buffer data
       WCHNET_SocketSend(sockeid,MyBuf,&len);                   /* 演示回传数据 */show return data
       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连接成功中断*/The socket connection is successfully interrupted
    {
        connectstat=1;
        printf("TCP Connect Success\r\n");
        WCHNET_ModifyRecvBuf(sockeid, (u32)SocketRecvBuf[sockeid], RECE_BUF_LEN);
    }
    ............... //以下部分不需要修改,此处省略The following parts do not need to be modified and are omitted here

The code addition here is very simple, you only need to judge whether MyBuf receives the values in the four directions of up, down, left and right.

2.Test the TCP Server code

The code of the TCP Server part is almost written.So what is the problem with the program running and how to debug it? First, we can use the network debugging assistant on the PC side to test the function.Do not write the code of the TCP client side and debug together, so that you will not know which end is the problem at that time. Open the network debugging assistant and set the protocol type to TCP Client (the server is Chitu now). Enter the IP of Chitu for the remote host address, and fill in the remote host port according to the settings in the program. Then enter 'up', 'down', 'left', 'right' in a text file in advance, and copy and paste them directly to the sending area. Observe the movement of the snake on the Chitu screen. If you have any questions, please post them in the comments section.

3. TCP Client Design

1. Copy five-to switch to project

Start using the second Chitu development board here, and open the TCP Clinet project under the ETH directory in the WCH EVT sample. Comment out / 演示回传数据 / show return data WCHNET_SocketSend(sockeid,MyBuf,&len); in the void WCHNET_HandleSockInt(u8 sockeid,u8 initstat) function in the TCP Client project. Otherwise, when the two development boards are jointly debugged, the two development boards will keep sending and receiving between the two development boards, and then sending them to the other, which cannot be stopped. Then copy the relevant functions of the five-to switch, a GPIO initialization function and a key scanner as follows:

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;
}

The button program here has the Delay_Ms function. If it is placed in the while of the main function, it will affect the Ethernet connection. It can be seen from the code that the key is to read the state of the GPIO port once, and then delay 20Ms. If the state of the IO port does not change, it means that the key is pressed (this is to prevent the key from shaking). So we only need to set a 20Ms timer, scan the keys regularly, if there is a key pressed, make a record, and the next time it is still this key, it means that the key is pressed. In addition, if two or more consecutive inputs are the same key value, you only need to send the key value once. The GPIO initialization code remains unchanged, and the add timer code is as follows:

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; //抢占优先级preemption priority    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;        //子优先级SubPriority
    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);//清除标志位 clear flag
}

This code has three functions. First, the key reading function is modified, and all the parts that need the Delay_Ms function are removed, so that the value of the current key can be obtained every time this function is called.Then set the timer initialization function. A lot of design has been done in the interrupt function of the timer. Let's take a look at this part of the code:

void TIM7_IRQHandler(void)   __attribute__((interrupt("WCH-Interrupt-fast")));
void TIM7_IRQHandler(void)
{

    uint32_t len2=2;                //发送信息的长度2字符the length of the message sent is 2 characters
    uint32_t len4=4;                //发送信息的长度4字符the length of the message sent is 4 characters
    uint32_t len5=5;                //发送信息的长度5字符the length of the message sent is 5 characters
static uint32_t cnt=0;          //用于区分是哪一次扫描 Used to distinguish each time when it is scaned
uint8_t getkey =0;              //保存当前获得的有效按键值 Save the currently obtained valid key value
    static uint8_t getkeyprev =0;   //保存上一次发送的按键值 Save the key value of last sent
    uint8_t scankey =0;             //保存当前扫描的按键值 Save the currently scanned key value
    static uint8_t scankeyprev =0;  //保存上一次扫描的按键值 Save the key value of the last scan
    if (cnt%2) {                    
        scankey = Basic_Key_Handle();//读取按键值 read key value
    }else {
        scankeyprev = Basic_Key_Handle();//间隔20Ms读取按键值 Read the key value at an interval of 20Ms
    }
    if (scankey == scankeyprev) {   //两次扫描按键值一样则为有效按键,去抖动If the key value is the same for two scans, it is a valid key, debounce
        getkey = scankey;           //将扫描有效的按键值赋值给getkey保存 Assign the scanned valid key value to getkey to save
    }
    if (getkey!=0 & (getkeyprev != getkey)) {   //如果当前有按键按下并且和上次发送按键值不同则执行Execute if a key is currently pressed and the key value is different from the last sent key
        switch (getkey) {
            case up:
                WCHNET_SocketSend(SocketId,"up",&len2);//发送案件信息给TCP Server Send case information to 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变量Store the current sent key value to the prev variable
    }
    cnt++;                                                  
    TIM_ClearFlag(TIM7, TIM_FLAG_Update);//清除标志位 clear flag
}

Finally, remember to call the timer initialization function in the main function and set the timer to interrupt once 20Ms.

Refresh_TIM_Init( 200-1, (SystemCoreClock/10000)-1 );//初始化定期器20Ms中断一次 The initialization timer is interrupted once every 20Ms

2. Two Chitu joint debugging

To communicate the two Chitu, at the beginning, you need to configure IP for the two Chitu. Assume that the IP of the Server is 10 and the IP of the Client is 14, then the IP is set in the main.c file of Chitu as the Server. The settings are as follows:

u8 MACAddr[6];                                           /*Mac地址*/ Mac address
u8 IPAddr[4] = {192,168,1,10};                           /*IP地址*/ IP address
u8 GWIPAddr[4] = {192,168,1,1};                          /*网关*/ gateway
u8 IPMask[4] = {255,255,255,0};                          /*子网掩码*/ subnet mask
u8 DESIP[4] = {192,168,1,14};     

The IP setting part of the main.c file of Chitu as Client is as follows:

u8 MACAddr[6];                                          /*Mac地址*/Mac address
u8 IPAddr[4] = {192,168,1,14};                          /*IP地址*/IP address
u8 GWIPAddr[4] = {192,168,1,1};                         /*网关*/gateway
u8 IPMask[4] = {255,255,255,0};                         /*子网掩码*/subnet mask
u8 DESIP[4] = {192,168,1,10};                           /*目的IP地址*/destination IP address

When downloading, make sure that only one Chitu is connected to the computer, otherwise it is possible that two Chitu have downloaded the same code. After downloading the code, both Chitu observe the connection status through the serial port debugging assistant. As shown below:

Next, you can use Chitu to control the snake on another Chitu through Ethernet

4. Unfinished part

This design only realizes the use of Ethernet to connect two Chitu to communicate. In fact, you can also create another snake on the Server machine, and the two snakes will fight against each other.This part is left for you to have a try. You can also use the PC as a TCP Server to make a web version of Snake, and Chitu as a remote handle to control Snake, which can achieve a larger map and more gameplay. If you are interested, you can try it. Finally, don't forget to post your works on the forum.

Leave a Reply

Your email address will not be published. Required fields are marked *