俄罗斯方块是一个简单的小游戏,完全可以采用单片机,裸机完成。但在嵌入式linux环境下实现,可以充分感受linux应用开发,同样是写一个程序,但功力可能大不相同。由单片机转嵌入式linux应用,框架感很重要,好的框架方便扩展,修改,可移植性强。
本文实现的俄罗斯方块可以切换横屏竖屏,支持触摸屏控制,串口终端控制。会做这个其实就已经具备基本的嵌入式linux应用开发能力了。
竖屏
横屏:
动图
1. 程序框架
重点只有一个,抽象出输入设备管理器,因为我们支持触摸屏控制,串口终端控制,抽象出设备管理器后,可以继续添加其他设备如鼠标,直连键盘等,修改极其方便。
整个程序主要有四大部分组成framebuffer、input_managner、控制逻辑、display_text。游戏控制逻辑并不是重点,从github上随便下载,然后进行魔改。
整个工程结构:
2. frambuffer
可以参考嵌入式Linux入门-Framebuffer应用编程在Linux系统下画个点
2.1 打开与关闭framebuffer函数
获取lcd长宽,bpp
2.2 各种画图函数
以draw_pixel画一个像素为基础,封装出draw_line画一条线,draw_rect画框,fill_rect填充一个框
3. 输入管理器input_manager
输入管理,输入采用触摸屏或者串口终端,需要抽象出输入管理器,来管理两个设备。
#ifndef _INPUT_MANAGER_H_
#define _INPUT_MANAGER_H_
typedef struct s_input_event {
int control;//0=pause,1=change,2=move_down,3=move_left,4=move_right,5=quit
}s_input_event,*ps_input_event;
typedef struct s_input_device {
char *name;
int (*get_input_event)(ps_input_event input_enent);
int (*input_device_init)(void);
void (*input_device_exit)(void);
struct s_input_device *ps_next;
}s_input_device,*ps_input_device;
int init_input_device(char *name);
void exit_input_device(void);
int get_input_event(ps_input_event input_event);
int register_input_device(ps_input_device input_device);
#endif
抽象出输入事件s_input_event,control为控制指令,0为暂停,1为变换,2为下移,3为左移,4为右移。
抽象出输入设备s_input_device,成员为名字name,利用名字来匹配设备,每个设备必须实现三个函数:
1.get_input_device获取输入事件
2.input_device_init初始化设备
3.input_device_exit退出设备
还需要一个指针,把设备串联起来,方便切换。
input_manager需要提供register_input_device函数,让设备把自己注册进管理器
int register_input_device(ps_input_device input_device)
{
input_device->ps_next = ps_input_device_head;
ps_input_device_head = input_device;
return 0;
}
3.1 触摸屏
触摸屏直接采用tslib,Tslib是一个开源的程序,能够为触摸屏驱动获得的采样提供诸如滤波、去抖、校准等功能。关键函数如下:
static s_input_device s_input_device_ts = {
.name = "ts",
.get_input_event = get_input_event_ts,
.input_device_init = input_device_init_ts,
.input_device_exit = input_device_exit_ts,
.ps_next = NULL,
};
void register_ts(void)
{
register_input_device(&s_input_device_ts);
}
实现输入设备结构体,并实现注册进输入管理器函数。
初始化调用ts_setup,并获取一些数据,这些数据用于判定,按触摸屏的哪个位置是上下左右、暂停退出功能。
static int input_device_init_ts(void)
{
int fd_fb;
static struct fb_var_screeninfo var;
g_ts = ts_setup(NULL, 1);
if (!g_ts)
{
printf("ts_setup err\n");
return -1;
}
fd_fb = open("/dev/fb0", O_RDWR);
if (fd_fb < 0)
{
printf("can't open /dev/fb0\n");
return -1;
}
if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var))
{
printf("can't get var\n");
return -1;
}
xres = var.xres;
yres = var.yres;
one_third_x = xres/3;
one_third_y = yres/3;
two_third_x = xres*2/3;
two_third_y = yres*2/3;
quit_x = xres*4/5;
quit_y = yres/5;
close(fd_fb);
return 0;
}
获取输入事件,ts_read读取屏幕并根据位置判定是上下左右、暂停退出中的哪一个功能。
static int get_input_event_ts(ps_input_event input_event)
{
struct ts_sample samp;
int ret;
ret = ts_read(g_ts, &samp, 1);
if (ret != 1)
return -1;
if (samp.pressure == 0) {
if (samp.x<two_third_x&&samp.x>one_third_x&&samp.y<two_third_y&&samp.y>one_third_y) {
input_event->control = cmd_pause;
return 0;
} else if (samp.x<one_third_x&&samp.y<two_third_y&&samp.y>one_third_y) {
input_event->control = cmd_mv_left;
return 0;
} else if (samp.x>two_third_x&&samp.y<two_third_y&&samp.y>one_third_y) {
input_event->control = cmd_mv_right;
return 0;
} else if (samp.y<one_third_y&&samp.x<two_third_x&&samp.x>one_third_x) {
input_event->control = cmd_rotate;
return 0;
} else if (samp.y>two_third_y&&samp.x<two_third_x&&samp.x>one_third_x) {
input_event->control = cmd_mv_down;
return 0;
} else if (samp.x>quit_x&&samp.y<quit_y) {
input_event->control = cmd_quit;
return 0;
} else
return -1;
}
else
return -1;
}
3.2 串口终端
重点部分和触摸屏是一样的,因为是自己抽象出来的设备。
static s_input_device s_input_device_terminal = {
.name = "terminal",
.get_input_event = get_input_event_terminal,
.input_device_init = input_device_init_terminal,
.input_device_exit = input_device_exit_terminal,
.ps_next = NULL,
};
void register_terminal(void)
{
register_input_device(&s_input_device_terminal);
}
初始化,重定向终端输入。把键盘输入,重定向到我们写的程序。
static int input_device_init_terminal(void)
{
tcgetattr(STDIN_FILENO, &oldtermset);
newtermset = oldtermset;
newtermset.c_lflag &= ~ICANON;
newtermset.c_lflag &= ~ECHO;
newtermset.c_cc[VMIN] = 1;
tcsetattr(STDIN_FILENO, TCSANOW, &newtermset);
return 0;
}
获取输入事件
static int get_input_event_terminal(ps_input_event input_event)
{
char c;
c = getchar();
fflush(NULL);
switch(c) {
case 'w':
input_event->control = cmd_rotate;
return 0;
case 'a':
input_event->control = cmd_mv_left;
return 0;
case 's':
input_event->control = cmd_mv_down;
return 0;
case 'd':
input_event->control = cmd_mv_right;
return 0;
case 'q':
input_event->control = cmd_quit;
return 0;
case 'p':
input_event->control = cmd_pause;
return 0;
default:
break;
}
return -1;
}
4. diaplay_text显示文本
之前教过,可以用freetype矢量字体,这里采用点阵。
static void put_char(int x, int y, int c, unsigned int color, int rotation)
{
int i, j, bits,x2,y2,tmp;
for (i = 0; i < font_vga_8x8.height; i++) {
bits = font_vga_8x8.data[font_vga_8x8.height * c + i];
for (j = 0; j < font_vga_8x8.width; j++, bits <<= 1)
if (bits & 0x80) {
x2 = x+j;
y2 = y+i;
switch (rotation) {
case 0:
break;
case 1:
tmp = y2;
y2 = x2;
x2 = xres_orig - tmp - 1;
break;
default :
break;
}
lcd_draw_pixel(x2, y2, color);
}
}
}
核心函数为显示一个字符,如上,rotation为旋转标记,将横屏显示变为竖屏显示。
最底层lcd_draw_pixel(x2, y2, color);为在framebuffer中实现的函数,即在lcd上画一个点。
由此函数封装出显示字符串,显示数字函数。
5.多线程
对于俄罗斯方块,并不需要使用多线程,但为了练习,创建多线程。
嵌入式Linux入门—Linux多线程编程、互斥量、信号量、条件变量
5.1 主线程
初始化输入输出,控制逻辑,创建输入检测线程。
int main(int argc,char **argv)
{
pthread_t tid;
int control;
int a;
int b;
pthread_mutex_init(&mutex,NULL);
if (open_framebuffer() != 0)
goto error;
if(ts)
if (init_input_device("ts") != 0)
goto error;
else;
else
if (init_input_device("terminal") != 0)
goto error;
else;
input_event.control = 10;
initGame(xres_orig,yres_orig);
SetTimer(1,alarm_handler);
pthread_create(&tid,NULL,get_input,NULL);
while(1) {
pthread_mutex_lock(&mutex);
a = read;
b = write;
pthread_mutex_unlock(&mutex);
if (a != b) {
pthread_mutex_lock(&mutex);
control = buffer[read];
pthread_mutex_unlock(&mutex);
switch(control) {
case cmd_rotate:
if(game_rotate&&ts)
moveLeft();
else
rotate();
break;
case cmd_mv_left:
if(game_rotate&&ts)
moveDown();
else
moveLeft();
break;
case cmd_mv_down:
if(game_rotate&&ts)
moveRight();
else
moveDown();
break;
case cmd_mv_right:
if(game_rotate&&ts)
rotate();
else
moveRight();
break;
case cmd_quit:
printf("Game Aborted!!!\n");
printf("Your score:%d\n",getScore());
goto error;
default:
break;
}
pthread_mutex_lock(&mutex);
read = (read+1)%2;
pthread_mutex_unlock(&mutex);
}
}
error:
exit_input_device();
close_framebuffer();
exitGame();
return 0;
}
5.2 输入检测线程
获取输入事件,采用环形缓冲,读写同步。
将输入事件放入buffer[]数组,设置read,write指针。
read=(read+1)%buffer长度,构成环形,write同理。
void *get_input(void *arg)
{
while (1) {
if (get_input_event(&input_event) == 0) {
pthread_mutex_lock(&mutex);
if((write+1)%2 != read) {
buffer[write] = input_event.control;
write = (write+1)%2;
}
pthread_mutex_unlock(&mutex);
}
}
}