开发环境的搭建
Keil的下载
Keil软件的官网下载地址如下:
https://www.keil.com/download/product/
由不同版本的文字提示可知,应下载C51(development tools for all 8051 devices)。并且,要获得Keil C51的全部功能,需要current license,否则只能运行其Lite/Evaluation edition。
STC-ISP烧录软件的下载
使用STC-ISP软件进行烧录。STC-ISP6.87S版本(来自STC公司)的官网下载地址如下:
http://www.stcmcudata.com/STCISP/stc-isp-15xx-v6.87S.zip
(若使用Google Chrome打开,会有安全警告)
硬件介绍
LED阵列
LED阵列是由发光二极管与限流电阻有规律地排布而成,示意图如下。
发光二极管的亮灭通过ROW与COL的电平关系决定,仅当ROW为HIGH且COL为LOW时,二极管导通。
此外,LED阵列还包括两颗串-并变换、SOIC-16封装的74HC595D,、2颗0603封装的电源去耦电容、2个5管脚直插的连接器,示意图如下。
其中第一个74HC595D的输出通过npn型三极管S9013的基级与集电极与COL耦合(如下图),第二个74HC595D的输出直接耦合ROW。
可知,第一个74HC595D的输出C通过三极管后反相后得到COL,即当C为1,ROW为1时,有COL为LOW,ROW为HIGH,此时发光二极管导通。
8051最小系统
8051最小系统(下文简称为最小系统)的主要硬件包括:基于STC的STC15W204内核为8051的8位单片机,MicroUSB供电并基于沁恒的CH340E(USB转UART), LED灯(电源状态指示)。
最小系统的电路原理图如下:
最小系统的3D效果图的俯视图如下:
连接
由于软件部分是基于王安然老师提供的全亮测试工程,故保留了王安然老师对寄存器与引脚的分配。
代码部分:
sbit SRCLK=P5^4;
sbit RCLK=P3^3;
sbit SER=P5^5;
参照阵列与最小系统的原理图,将二者引脚通过杜邦线连接。
可知,阵列的P1的1号引脚(3V3)应与最小系统的VCC相连,阵列P1的2号引脚(GND)应与最小系统的GND相连,阵列P1的3号引脚(SER1)应与最小系统的P55相连,阵列P1的4号引脚(SCK)应与最小系统的P54相连,阵列P1的5号引脚(RCK)应与最小系统的P33相连。
本项目说明:
在实现了基础的图形滚动之后,我们团队期望做出一个可以进行交互的LED点阵效果,但是由于本次使用的最小系统板上没有包含任何输入按键,我们最终决定采用串口通信的方式,通过PC机向51单片机发送不同的字符来控制LED点阵的变化。
最终的方案是设计一个经典的魂斗罗像素战士形象,并且可以通过串口控制该魂斗罗战士在LED点阵中移动、跳跃、举枪和射击。特别的,由于硬禾学堂提供的LED点阵板可以通过跳帽进行级联,从而形成更大的LED点阵,我们为了项目的故事性和可玩性,增加了可爱又迷人的反派角色—谷歌小恐龙(也就是Chrome连不上网络的时候出现的那一只)。
这个注定悲剧的故事主线是,谷歌小恐龙被突然闯入的魂斗罗战士不分青红皂白了连射五枪,悲怆跪地了。
形象设计
此项目中的两个形象仿照了经典像素游戏魂斗罗的主角以及谷歌浏览器的404像素恐龙(如下图)。
魂斗罗:
平举着枪的小人,用外侧腿的两种姿态来模拟走路。
代码:
u8 walk_row_one[]={0xbc,0x7c,0x10,0x10,0x00,0x00,0x00,0x00};
u8 walk_row_two[]={0x7c,0xbc,0x10,0x10,0x00,0x00,0x00,0x00};
抬举着枪的小人,用外侧腿的两种姿态来模拟走路。
代码:
u8 gun_up_row_one[]={0xbc,0x7c,0x08,0x04,0x00,0x00,0x00,0x00};
u8 gun_up_row_two[]={0x7c,0xbc,0x08,0x04,0x00,0x00,0x00,0x00};
跳起来的小人,平举着枪与抬举着枪。
代码:
u8 jump_normal_row[]={0x2f,0x1f,0x04,0x04,0x00,0x00,0x00,0x00};
u8 jump_gun_up_row[]={0x2f,0x1f,0x02,0x01,0x00,0x00,0x00,0x00};
谷歌浏览器的404像素恐龙
站立状态。
代码:
u8 standing_tyr_row[]={0x06,0x16,0x8e,0xfe,0xb8,0xf8,0x10,0x08};
被魂斗罗战士击败后的跪姿。
代码:
u8 kneeing_tyr_row[]={0x18,0x58,0xB8,0xf8,0xe0,0xe0,0x40,0x80};
代码说明:
代码主要包含三部分:
第一部分是LED点阵点亮和扫描的基本原理。
第二部分是串口通信。
第三部分最为要命,是如何实现串口数据对LED点阵效果的控制和更改。(也就是我们怎么实现摁一个’D’键魂斗罗战士就向前移动一步的等等)
亮灯控制
为避免对角线所在矩形上的其他LED被点亮,采取逐行刷新的方式来进行控制。
void main()
{
while(1)
{
//data:输入的数据 roll:数据读取的偏移值(如果roll=1,则从data[1]作为data[0],以此类推)
Hc595ColScan(data,roll)
}
}
所调用函数的代码:
void Hc595ColScan(u8 dat[8],u8 m)//扫描和刷新一个8x8LED点阵板
{
u8 i;
u8 col=0x01;
for(i=0;i<8;i++)
{
// scan and refresh the second LED board by sending two times 8 bit data
Hc595SendByte(dat[(i+8-m)%8]);
Hc595SendByte(col);
RCLK=1;
_nop_();
_nop_();
RCLK=0;
//Delay100ms(3);
col<<=1;
}
}
下面展示的子函数代码和上面的实现相同的功能,但是用于两个LED点阵板级联时同步刷新。
void Hc595ColScan_2board(u8 dat1[8],u8 dat2[8],u8 m, u8 n)//扫描和刷新两个LED板
{
u8 i;
u8 col=0x01;
for(i=0;i<8;i++)
{
// scan and refresh the second LED board by sending two times 8 bit data
Hc595SendByte(dat2[(i+8-n)%8]);
Hc595SendByte(col);
// scan and refresh the first LED board by sending two times 8 bit data
Hc595SendByte(dat1[(i+8-m)%8]);
Hc595SendByte(col);
//Latch in 锁存数据
RCLK=1;
_nop_();
_nop_();
RCLK=0;
//Delay100ms(3);
col<<=1;
}
}
串口通信
串口通信在代码中主要体现为三部分:串口的初始化、数据的接收和数据的发送。
详细的深层的串口知识我将不细致展开,只是简单的从概念层面帮助大家梳理一下,方便大家理解代码。
串口的初始化其实就是对串口通信的一些参数进行配置(比如单片机的时钟频率、波特率、停止位校验位这些),这些参数的配置涉及到相应的寄存器配置,需要根据每个芯片的不同参考数据手册进行查看。其次,为了方便数据的接收和处理,我们需要使能一个内部中断,每当有新数据进来我们就进入中断处理函数。最后,有些时候我们还会使能一个定时器用于在通信出现问题时及时停止而不是一直等待数据。
void UartInit(void) //9600bps@33MHz
{
SCON = 0x50;
AUXR |= 0x01;
AUXR |= 0x04;
T2L = 0xA5;
T2H = 0xFC;
AUXR |= 0x10;
ES=1;
EA=1;
}
串口发送数据就是我们把我们想要发送的内容传递到一个内部的缓冲区SBUFF,一旦这个缓冲区有数据串口就会自动将其按照我们刚刚初始化时配置好的模式发送出去,一旦全部发送完毕,标志位TI会被置高位。
在我们的程序里,串口数据发送用于将我们目前接收到的指令和将要执行的操作发送给PC一方面方便我们调试,一方面也可以增加交互体验。
void UART1_SendData(unsigned char dat)//发送一个字符
{
SBUF=dat;
while(!TI);
TI=0;
}
void UART1_SendString(unsigned char *s)//发送字符串
{
while(*s != 0)
{
UART1_SendData(*s++);
}
}
串口接收数据就是当有数据从PC机发送过来,数据会被缓存在同样的缓存区SBUFF,一旦SBUFF接收到数据,标志位RI就会被置高位。这时我们就会进入到中断处理函数,在这里我们可以提取接收到的数据并执行相应的操作。
在我们的程序里,我们通过根据接收的数据不同,执行不同的操作从而使LED产生不同的效果。具体的实现方法可以看下面的代码:(总体就是用if语句进行判断,然后给不同的变量赋值来在main函数中执行相应的指令)
void UART_INT(void) interrupt 4
{
if(RI == 1) { //如果收到.
RI = 0; //清除标志.
New_rec = 1; //接收到新数据
UART_buff = SBUF; //接收.
if(UART_buff == '1') start = 1;
if(UART_buff == '0')
{ start = 0;
fire=0;
life=4;
pTyr=standing_tyr_row;
}
if(UART_buff == 'a') move = 2;
if(UART_buff == 'd') move = 1;
if(UART_buff == 'w') jump = 1;
if(UART_buff == 'j') weapon = 1;
if(UART_buff == 'k') weapon = 0;
if(UART_buff == 'l') fire = 1;
}
指令控制
现在,重点在于,我们知道指令是什么了,如何在满足LED点阵不间断扫描的过程中改变LED的点亮模式实现图案的变化。它的难点在于,LED的变化模式只在我们想让他改变的时候改变,而没有新指令的时候要维持原来的模式。具体到我们这个项目就是,在上电之后LED全灭,串口发送‘1’,魂斗罗战士出现,并且以一定的频率进行原地踏步运动。只有当串口发送‘A’、’D’、‘W‘、’J’、‘K’、‘L’这几个字符的时候,模式才会被改变。
要实现这个最重要的就是尽管LED的变化模式中存在变化的周期,也就是延时,但是这个周期或者延时的实现不能够打断LED点阵的扫描和刷新。我们只能通过延时改变输入数据,来实现LED点阵某一种图案的保持。因此我们可以利用单片机内部的定时器,或者简单的每完成一次主循环某个变量加一,通过判断变量的值来改变数据。
void main()
{
u8 m=0,n=0;// data input offset
u8 k=4;// a variable used to indicate the maximum length a bullet can move
u8 bullrt_move=256;// bullet speed. namely, how long it takes for a bullet to move for one colloum to next
u16 ju=1000;//jump duration
u16 s=0;//a hand made timer
//Two array pointes
pTyr=standing_tyr_row;//point to data array of current Google_Tyrannosaurus
pHero=walk_row_one;//point to data array of current Contra hero
Delay100ms(5);
UartInit();
UART1_SendString("STC15W204S\r\nUart is ok !\r\n");//发送字符串检测是否初始化成功
Delay100ms(10);
while(1)
{ //each time serial port receive a character, send the corresponding command description feedback to PC
if(New_rec) {
if(UART_buff == '1') UART1_SendString("Game Start!\r\n");
if(UART_buff == '0') UART1_SendString("Game Over!\r\n");
if(UART_buff == 'a') UART1_SendString("Move Backward\r\n");
if(UART_buff == 'd') UART1_SendString("Move Forward\r\n");
if(UART_buff == 'w') UART1_SendString("Junp\r\n");
if(UART_buff == 'j') UART1_SendString("Weapon On\r\n");
if(UART_buff == 'k') UART1_SendString("Weapon Down\r\n");
if(move==1)
{
if(m==4) m=4;
else m++;
move=0;
}
else if(move==2)
{
if(m==0) m=0;
else m--;
move=0;
}
New_rec=0;
}
//Start the game, hero shows up
if(start==1)
{
if(fire) // judge whether hero is firing
{
for(k=4;k<8-m;k++){
*(pHero+k)=bullet;// u8 bullet =0x10;
while(bullrt_move--)
Hc595ColScan_2board(pHero,pTyr,m,0);
*(pHero+k)=0x00;
bullrt_move=250;
}
//when the bullet touch the Tyrannosaurus four times, Tyrannosaurus gets to his knees.
if(life==0) pTyr=kneeing_tyr_row;
else life--;
fire=0;
}
else{
if(!jump){ // judge whether hero is jumping
if(!weapon){ // judge whether hero holds the weapon up
if(n) pHero=walk_row_one;
else pHero=walk_row_two;
}
else{
if(n) pHero=gun_up_row_one;
else pHero=gun_up_row_two;
}
Hc595ColScan_2board(pHero,pTyr,m,0);
}
else{
if(!weapon) pHero=jump_normal_row;
else pHero=jump_gun_up_row;
while (ju--) Hc595ColScan_2board(pHero,pTyr,m,0);
jump=0;
ju=1500;
}
}
s++;
if(s==800)
{
n=~n;
s=0;
}
}
else
{
Hc595ColScan_2board(clear,pTyr,0,0);
}
}
最终效果展示:(两块LED点阵板)