stc-b单片机项目《基于485通信的双人井字棋对战》

计算机系统设计与创新基础训练创新设计《基于485通信的双人井字棋对战》

这是《计算机系统设计与创新基础训练创新设计》课程设计,实现的是一个《基于485通信的双人井字棋对战》小游戏,你需要两根杜邦线和两块STCb学习板。今天花了十分钟写了这篇博客用于<开源>,时间比较仓促,希望程序能够带给读者快乐,而不是压在我电脑里尘封数年直至消失。

一.设计概述:

1.设计目的

为了进一步熟悉单片机开发,学习单片机项目的流程与设计方法,培养自己的动手能力和创新能力,利用现有的STC-B学习板设备,融合结合数码管滚动显示,流水灯功能,电子音乐功能,导航键功能和蜂鸣器功能,485通信功能,将3x3的棋盘滚动地显示在一行地数码管上,实现井字棋单机小游戏和双人对战功能。

2.功能概述:

  • ①数码管布局:

在数码管中央棋盘的一行,第一颗数码管显示行号,第二颗和第8颗数码管显示对战双方A或B,3和7颗数码管显示棋盘左右边界,456三颗数码管显示3x3棋盘。通过导航键可以上下滚动查看井字棋任意行,上下左右移动闪烁光标确定位置,按下则落子下棋。

在这里插入图片描述

  • ②游戏开始:

游戏开始前,播放《超级玛丽》开机音乐, 数码管滚动显示字母《PLAY CHESS》表示游戏名称,K2可以选择玩家A或B,按下K1游戏开始,进入游戏棋盘界面。

  • ③游戏中:

通过控制导航键上下滚动选择数码管的任意一行,行号显示在第一个数码管上,操作的玩家信息将会用小数点标注;左右控制光标位置,光标闪烁,显示被选中的位置,按下导航键落子下棋;操作过程配有按键提示音效,并且A,B双方玩家的按键提示音不同,加以区分。

  • ④游戏结束:

当棋盘存在三点一线或者平局将会触发游戏结束,数码管闪烁,显示赢家信息,胜利一方播放胜利的音乐,表示游戏胜利,失败一方播放失败的音乐,表示游戏失败。游戏全程按下K2键将会触发得分显示,将双方的得分显示在数码管上。

  • ⑤双人对战功能:

用杜邦线连接两块板子,双方可以用A,B两个符号在棋盘上下棋,两人通过485通信进行数据交互,实现实时双人对战。下棋过程中除查看比分外,所有的操作动作都会被传输到对手,实时更新棋盘。游戏采取三局两胜利制,当三局游戏结束,会自动关闭游戏循环,显示滚动字样《PLAY CHESS》

二.模块实现:

1.数码管布局和动态扫描:

  • (1)整体布局

在这里插入图片描述
好的布局往往给编码带来方便,我将8颗数码管布局从左到右编码为0,4,6,1,2,3,7,5 ,这样有两个好处:
①棋盘Bord的的数组下标为1,2,3,这样在以后的处理中会更加便捷
②位选数组可以方便的设置编码顺序,比如说uchar dig[]={0,4,6,1,2,3,7,5};数码管位选,实现上没有难度,给代码编写带来方便
在这里插入图片描述
在这里插入图片描述
因此根据这种编码方式,可以得到右上图的映射方法,从而设计:
数码管位选数组:

uchar code dig[]={
    
    0,3,4,5,1,7,2,6};                        //数码管位选

棋盘数组:

uchar Board[4][8]={
    
                                     //棋盘
		{
    
    0,0,0,0,0x0a,0xb,0x13,0x14},
		{
    
    1,0,0,0,0x0a,0xb,0x13,0x14},
		{
    
    2,0,0,0,0x0a,0xb,0x12,0x12},
		{
    
    3,0,0,0,0x0a,0xb,0x10,0x11}};

比分板

uchar scoreBoard[8]={
    
    0,0,0x12,0xa,0xb,0x12,0,0};              //比分板
  • (2)动态扫描显示棋盘/比分板/滚动字样

数码管显示功能易于实现,主要是设置动态扫描,扫描的同时选定位选和段选信号即可,所以说,实现的时候重点在于如何切换显示内容,也就是段选信号输入?我在前面已经设计了三个布局,分别是棋盘/比分板/滚动字样,他们存在三个数组中,当检测到控制信号有效的时候,按条件切换显示内容:

digIndex++;                         //数码管的数组下标
if(digIndex==8){
    
    
     digIndex=0;                    //下标归零
     count++;                                                  
}
if(sbtKey2 == 0 && (start||over||gameNum))  //开始或者结束的时候可以查看棋盘                                  
     displayScore();                //3局开始后,被按下,显示比分板  
else if((!start&&!over&&gameNum==0&&!isSelect)||(start&&over))  //未开始或者已经全局结束
     displayRoll(arrRoll);          //显示滚动字母             
else 
     displayChessBoard();           //显示棋盘

2.按键功能实现

(1)Key1游戏开始功能:

在T0定时器中监听Key1,当按键按下并且start=0并且已经选择了游戏玩家角色,则可以提前开始开始:

void T0_Process() interrupt 1                  //中断
{
    
    
     //检测按键Key1游戏开始
     if(sbtKey1==0 && (!start) && (!over))        //按键按下并且start=0并且已经选择了游戏玩家角色,则可以提前开始开始
     {
    
    
          tick4();
          if(sbtKey1==0){
    
    
              while( !sbtKey1 );
              start=1;                  //key1按下,游戏开始
              datas=0x01;               //设置Key1数据,记住,玩家信息在sendData中添加
              sendDatas();               //发送数据               
          }
     }
}

(2)Key2游戏角色选择功能:

游戏选择功能难点在于,如何根据控制信号来选择玩家,当游戏未开始,且游戏局数为0的时候,Key2将会选择玩家,这个时候才能去调用函数selectCharacter();选择玩家,此时将会更新棋盘第二行,显示所选择的玩家信息。如果选择信号设置错误了,那么将会导致棋盘错误更新,显示棋盘落子信息,而不是所选角色。

if( sbtKey2 == 0 && gameNum==0 && (!start) && (!over))    //当游戏未开始,且游戏局数为0的时候,Key2将会选择玩家
{
    
    
    Delay5ms();                                       		//延时消抖
    tick4();
    if( sbtKey2 == 0 )
    {
    
    
        while( !sbtKey2 );                                        //等待K1放开
        selectCharacter();
        isSelect=1;
        datas=0x02;                                                //发送key2
        sendDatas();                                           //直接发送数据
    }
}

(3)导航键控制方向和落子:

导航按键的功能是移动光标,控制上下左右,用于定位,当按下确定键的时候可以选中他的位置。导航键上下左右实现方法:

①只需要改变(x,y)的值就可以改变棋子坐标,随着数码管动态扫描就自动改变了
②当导航按键被按下的时候,获取此时的ADC值
③用一个case语句进行分支,对ADC值进行判断,从而可以判断处方向,从而改变<x,y>的值:

左:x--,右: x++,上:y--,下:y++

④注意要对x,y的范围进行限定,他们都因该在范围<1,2,3>,为了更加接近显示体验,我不设置滚动,相反,当x,y到达边界的时候,按下导航键试图越界将不被允许
⑤将整个导航检测函数放在中断中处理还是放到 while(1){} 循环内,核心功能如下:

switch( ucNavKeyPast )
{
    
    
   case 0x01:                      //右
   if(y!=3) y++;break;
   case 0x02:                      //下if(x!=3) x++;break;
   case 0x03:                     //里break;
   case 0x04:if(y!=1) y--;break;          //左
   case 0x05:if(x!=1) x--;break;
}

3.游戏开始和结束

(1)游戏开始:

游戏开始的程序需要完成播放结束音乐,等待玩家再次开始游戏,闪烁数码管和更新棋盘的功能。对于播放音乐,可以直接调用PlayMusic函数即可,对于等待游戏结束可以用while(1)循环直接判断start是否为1

/*---------游戏开始----------------*/
void gameStart(){
    
    PlayMusic(190,arrMusicBM);                        //播放开机音乐while(!start);                                  //等待游戏开始Delay5ms();tick3();//闪烁数码管//重新定时为45ms
​     TH0 = ( 65535 - 45000 ) / 256;                 //定时器0的高八位设置
​     TL0 = ( 65535 - 45000 ) % 256;                 //定时器0的低八位设置,这里总体就是设置定时器0的初始值是1ms//重新定时为45ms
​     TH0 = ( 65535 - 1000 ) / 256;                  //定时器0的高八位设置
​     TL0 = ( 65535 - 1000 ) % 256;                  //定时器0的低八位设置,这里总体就是设置定时器0的初始值是1ms //更新棋盘refreshChessBoard();
}

4.音乐播放

电子音乐通过蜂鸣器发声,根据音谱对音调和节拍进行编码,编码的时候

①音符的十位代表是低中高八度,1代表高八度,2代表中八度,3代表高八度

②个位代表简谱的音符,例如0x15代表低八度的S0,0x21代表中八度的DO。

③节拍则是代表音长,例如:0x10代表一拍,0x20代表两拍,0x08代表1/2拍

④音符的十位代表是低八度,中八度还是高八度,1代表低八度,2代表中八度,3代表高八度

⑤个位代表简谱的音符,例如0x15代表低八度的S0,0x21代表中八度的DO。

⑥节拍则是代表音长,例如:0x10代表一拍,0x20代表两拍,0x0代表1/2拍,根据音谱对电子音乐进行编码:

(1)超级玛丽结束音乐:

uchar code arrMusicFail[] ={
    
    
0x21,0x08,0x24,0x10,0x24,0x08,0x24,0x08,0x24,0x08,0x23,0x08,0x22,0x09,0x21,0x08,0x00,0x00
};

(2)超级玛丽BM音乐:

       0x23,0x04,0x23,0x08,0x23,0x08,0x21,0x04,0x23,0x08,0x25,0x10,0x15,0x10,0x21,0x08,0xff,0x04,0x15,0x04,0xff,0x08,0x13,0x08,0xff,0x04,0x16,0x08,0x17,0x04,0xff,0x04,0x17,0x04,0x16,0x08,0x15,0x06,0x23,0x05,0x25,0x05,0x26,0x08,0x24,0x04,0x25,0x04,0xff,0x04,0x23,0x08,0x21,0x04,0x22,0x04,0x17,0x08,0xff,0x04,0x21,0x08,0xff,0x04,0x15,0x04,0xff,0x08,0x13,0x08,0xff,0x04,0x16,0x08,0x17,0x04,0xff,0x04,0x17,0x04,0x16,0x08,0x15,0x06,0x23,0x05,0x25,0x05,0x26,0x08,0x24,0x04,0x25,0x04,0xff,0x04,0x23,0x08,0x21,0x04,0x22,0x04,0x17,0x08,0xff,0x04,0xff,0x08,0x25,0x04,0x24,0x04,0x24,0x04,0x23,0x08,0x23,0x04,0xff,0x04,0x15,0x04,0x16,0x04,0x21,0x04,0xff,0x04,0x16,0x04,0x21,0x04,0x22,0x04,0xff,0x08,0x25,0x04,0x24,0x04,0x24,0x04,0x23,0x08,0x23,0x04,0xff,0x04,0x31,0x08,0x31,0x04,0x31,0x08,0xff,0x08,0xff,0x08,0x25,0x04,0x24,0x04,0x24,0x04,0x23,0x08,0x23,0x04,0xff,0x04,0x15,0x04,0x16,0x04,0x21,0x04,0xff,0x04,0x16,0x04,0x21,0x04,0x22,0x04,0xff,0x08,0x25,0x08,0xff,0x04,0x24,0x08,0xff,0x04,0x23,0x08,0xff,0x08,0xff,0x10,0x00,0x00

(3)超级玛丽吃蘑菇音乐:

uchar code arrMusicSuccess[] ={
    
    0x23,0x04,0x23,0x08,0x23,0x04,0xff,0x04,0x21,0x04,0x23,0x08,0x25,0x10,0x00,0x00
};

5.485 通信数据发送和接收

(1)传输信息表示:

这里为了减少数据传输的量,我将按键进行编码发送。data=<tag,K1,K2,右,下,确认,左,上>,一共8位,可以封装成一个uchar向量,从而大大减小发送量:

Tag 按下 Key2 Key1
  • Tag:标识位,用来确定显示的来源机器,如果是来源于0x0a,则Tag=0,否则Tag=0
  • 上下左右确认表示导航内容
  • Key1,Key2分别表示按键Key1,Key2被按下

将按键进行编码带来了许多便利,但是也有一些问题,类似于网页Web技术中的ajax,可以局部刷新页面,但是我只传输1个8位的向量编码,而不是整个状态,一旦某个过程缺失将导致严重后果。

(2)角色身份选择:

传输一个数据,右移7位选出标识位tag,当这个tag与自身的myPlayer的最低为相同的时候,说明要进行互斥操作,因此,将myPlayer进行取反操作,因此,可以设置自己的角色与对方互斥

if(!((datas>>7)^(myPlayer&0x01))){
    
    
  if(start) return;      //如果交互数据来自自己,那么拒绝接收
  else{
    
    
	   myPlayer^=0x01;   //异或,保持玩家互斥
  }
}

(3)角色选择同步:

在一开始,我没有合理地设置角色同步,导致一方设置角色,另一方无法看到,这个时候体验感将会极差,所以我将代码进行了改进,设置Key2数据接收地时候,更新数码管数组,显示玩家选择信息:

同样地在一开始,我一直被游戏结束功能所困扰,在getData函数接收到数据的时候,我判断棋盘是否导致游戏结束,并且设置over标志位,根据start和over进行gameOver调用,但是这里出现了两个进程的冲突:

①接收函数调用getData

/*---------串口2中断处理程序,数据接收---------*/
void Uart2_Process( void ) interrupt 8 using 1
{
    
    
  if( S2CON & cstUart2Ri )                      //无校验&接收中断请求标志位
  {
    
    
    datas = S2BUF ;                                  //从串口中接收数据暂存
    S2CON &= ~cstUart2Ri;              //接收中断标志位清0
          getData();
  }
  if( S2CON & cstUart2Ti )                      //无校验&发送中断请求标志位
  {
    
    
    btSendBusy = 0 ;       //清除忙信号
    S2CON &= ~cstUart2Ti ;       //发送中断标志位清0
  }
}

②getData调用gameOver

void getData()
{
    
    
    gameOver();
}

③我们忽略了一个问题,就是串口会占用计时器T2,但是,gateData中gameOver会调用函数PlayMusic和设置T0的即使时间,在网上查找相关资料得到,串口和定时器2是不能同时工作的,所以说进程PlayMusic和getData冲突,导致程序出错。因此我想到的方法是在NavKey_Process中判断:

/*---------导航按键处理子函数--------*/
void NavKey_Process()
{
    
    
     ............
     if(over&&start){
    
                                    

          gameOver();
     }    
  Delay100ms();
}

④实现效果:

两块单片机在游戏结束的时候同时发出结束音乐

三.程序功能测试

(1)数码管显示功能:

在数码管中央棋盘的一行,第一颗数码管显示行号,第二颗和第8颗数码管显示对战双方A或B,3和7颗数码管显示棋盘左右边界,456三颗数码管显示3x3棋盘。通过导航键可以上下滚动查看井字棋任意行,上下左右移动闪烁光标确定位置,按下则落子下棋。

在这里插入图片描述

(2)玩家选择功能

当游戏开始的时候,按下Key2选择角色,被选择的角色被显示在了数码管中央,此是左侧的玩家信息保持和选择玩家一致,也就是说,左侧的玩家,是本地玩家的信息。按下Key1键确认选择,按下Key1键游戏开始。
在这里插入图片描述

(3)双人对战功能:

用杜邦线连接两块板子,双方可以用A,B两个符号在棋盘上下棋,两人通过485通信进行数据交互,实现实时双人对战。下棋过程中除查看比分外,所有的操作动作都会被传输到对手,实时更新棋盘。游戏采取三局两胜利制,当三局游戏结束,会自动关闭游戏循环,显示滚动字样《PLAY CHESS》
在这里插入图片描述

(4)数码管滚动显示功能:

在程序下载好之后,可以看到PLAY CHESS字样在数码管上平稳滚动显示,显示当前游戏名称。
在这里插入图片描述

(5)比分显示功能

程序下载好之后,点击K1游戏开始,按下KEY2可以看到显示当前比分信息。
在这里插入图片描述

四.程序设计总结:

这一次设计我进一步熟悉单片机开发,学习单片机项目的流程与设计方法,培养了自己的动手能力和创新能力,利用现有的STC-B学习班设备,融合结合数码管滚动显示,流水灯功能,电子音乐功能,导航键功能和蜂鸣器功能,485通信功能,将3x3的棋盘滚动地显示在一行地数码管上,实现了井字棋单机小游戏和双人对战功能。其中收获的新知识主要是:

(1)定时器组成

  • MCS51系列的单片机通常有2个16位可编程定时/计数器,即定时器0和1。(T0/T1)
  • MCS52系列还有一个定时/计数器2。可编程的意思是指其功能(如工作模式、定时时间、启动方式等)可由指令来确定和改变。通常都是赋值指令给相关的寄存器。
  • 与定时/计数器相关的有两个特殊功能寄存器(模式控制寄存器TMOD和控制寄存器TCON)。
  • 且定时器往往在中断中使用,以便当时间到了完成相应处理。
  • MCS51单片机定时/计数器工作原理示意图如图
    在这里插入图片描述

(2)定时器赋初始值计算:
如果需要需要定时为50MS,则计算方式如下
如果晶振是12MHZ:
则机器周期为12MHz除以12,就是1MHz,每秒1000000次机器周期,那么50ms就是50000次机器周期。
65536-50000=15536(3cb0),TH0=0x3c,TL0=0xb0。1ms就是1000次机器周期。(65536-1000)/256是高位,(65536-1000)%256是低位。
TMOD寄存器的设置依据如图所示:
在这里插入图片描述
(3)中断工作原理

中断的概念: CPU在处理某一事件A时,发生了另一事件B请求CPU迅速去处理(中断发生);CPU暂时中断当前的工作,转去处理事件B(中断响应和中断服务)待CPU将事件B处理完毕后,再回到原来事件A被中断的地方继续处理事件A(中断返回),这一过程称为中断。MCS51中断信息如表2所示。

MCS51单片机中断信息表
在这里插入图片描述

与中断相关的寄存器包括:
在这里插入图片描述

① 中断允许控制寄存器IE:

在用到中断时,必须要开总中断EA,即EA=1。

  • EX0(EX1):外部中断允许控制位。EX0=1开外部0号中断,EX0=0关闭外部0号中断。
  • ET0(ET1):定时中断允许控制位。ET0=1,开内部定时器0号中断;ET0=0关闭定时器中断0号开关.
  • ES: 串口中断允许控制位。ES=1,开串口中断;ES=0 关闭串口中断。

② 中断优先控制寄存器IP
在这里插入图片描述

说明:

  • PS:串行口中断优先级控制位。PS=1设定串行口为高优先级中断;PS=0为低优先级中断。
  • PT1:T1中断优先级控制位。PT1=1设定定时器T1为高优先级中断;PT1=0为低优先级中断。
  • 外部中断1优先级控制位。PX1=1设定定时器外部中断1为高优先级中断;PX1=0为低优先级中断
  • PT0:T0中断优先级控制位。PT0=1设定定时器T0为高优先级中断;PT0=0为低优先级中断。
  • PX0:外部中断0优先级控制位。PX0=1设定定时器外部中断0为高优先级中断;PX0=0为低优先级中断。

(3)传输信息表示创新:

这里为了减少数据传输的量,我将按键进行编码发送。data=<tag,K1,K2,右,下,确认,左,上>,一共8位,可以封装成一个uchar向量,从而大大减小发送量:

Tag 按下 Key2 Key1
  • Tag:标识位,用来确定显示的来源机器,如果是来源于0x0a,则Tag=0,否则Tag=0
  • 上下左右确认表示导航内容
  • Key1,Key2分别表示按键Key1,Key2被按下

将按键进行编码带来了许多便利,但是也有一些问题,类似于网页Web技术中的ajax,可以局部刷新页面,但是我只传输1个8位的向量编码,而不是整个状态,一旦某个过程缺失将导致严重后果。

(4)设计感悟:

本次实验中我用到了融合结合数码管滚动显示,流水灯功能,电子音乐功能,导航键功能和蜂鸣器功能,485通信功能,将3x3的棋盘滚动地显示在一行地数码管上,整个游戏花费了一个星期,从一开始看不懂定时器,到完成作品,到帮别人debug都是一步步过来。体会了从陌生,到认识,到熟知,到应用的过程,最让我印象深刻的是学习过程中,利用现有资源,快速总结方法。我认为最有效的方法是看到什么不懂的,就立马总结起来写到Typora里面,不会的时候拿出来查阅,时间一久,就立马知道该从哪去找方法,去哪里找知识,很快就能熟练掌握。也很感谢我的舍友帮我解决了电子音乐的编码问题。超级马里奥的失败胜利音乐没有电子谱,所以只能通过人耳翻译,仔细听音符,音调和设置节拍,最后才整合得到了电子普,将其编码写入,通过蜂鸣器播放。我想,这门课的设计目的我应该达到了,熟悉了整套的单片机设计方法,知道一块芯片的生产,零件如何焊接,到编码应用,很感谢课程的教学老师。

猜你喜欢

转载自blog.csdn.net/weixin_44307065/article/details/108688969