复盘一下之前做的一个小项目,温习一下c语言和Linux的知识,唔,就是这样子。
所用软件以及工具如下:
1、VMware-workstation-full搭配Ubuntu18.04的Linux操作系统,VMware是桌面虚拟计算机软件,提供用户可在单一的桌面上同时运行不同的操作系统和进行开发、测试 、部署新的应用程序。
2、Vsode代码编写软件,配置c语言开发环境
3、CH341/340和PL2303 串口驱动软件
4、SecureCRT串口调试软件
5、GEC6818开发板如下图
上述环境搭建百度一下就有很多很多教程,这里不在赘述。真的要说配置环境,那得拉老长的文章了。
开发流程如下:
联通开发板与电脑:
接通电源,插上开发板后,安装CH341/340和PL2303串口驱动,
在计算机右键点击计算机管理中的设备管理器查看对应端口
打开SecureCRT串口调试软件,点击快速连接
设置SSH为serial,端口为查看的端口,波特率115200,流控全部关闭,点击连接即可
文件上传开发板:
Linux文件IO
小练习:德国国旗的显示
终于简单的归纳了一下部分内容到这里,可以开始做个小Demo练习一下熟练知识点啦!显示德国国旗在开发板上。(不要在意条条的颜色这些细节,问就是酱样紫)
在开发板上显示德国国旗代码如下:
#include
#include
#include
#include
#include
#include
int main()
{
// 1.打开lcd驱动
int lcd_fd;
lcd_fd = open("/dev/fb0", O_RDWR);
if(-1 == lcd_fd)
{
perror("open lcd failed!\n");
return -1;
}
// 2.处理颜色数据
// 定义颜色数据缓冲区
int col_buf[800*480];
int x,y;
for(y=0; y<160; y++)
{
for(x=0; x<800; x++)
col_buf[800*y+x] = 0x000000;
}
for(y=160; y<320; y++)
{
for(x=0; x<800; x++)
col_buf[800*y+x] = 0xff0000;
}
for(y=320; y<480; y++)
{
for(x=0; x<800; x++)
col_buf[800*y+x] = 0xffff00;
}
// 3.将颜色数据写入lcd
write(lcd_fd, col_buf, sizeof(col_buf));
// 4.关闭lcd
close(lcd_fd);
return 0;
}
tips:猜猜界面中的背景图是谁鸭
最终电子钢琴界面效果如下图:
不同的图片格式有着不同的压缩算法,所以需要将对应格式的图片文件压缩算法库引入项目,这里为了简单一点使用通用的24位图格式BMP文件存储图片,当然在GEC6818开发板的LCD显示屏却是32位图,为了在开发板的系统上正常显示需要进行原色移位即24转32位。
开发板系统的图片像素点是A、R、G、B共4个字节32位,BMP格式的图片像素点是B、G、R共3个字节24位,以B为基准,一个字节是八位,所以将R左移16位,G左移8位,B不动,A可以左移24事实上不管也行默认就可以,然后通过C语言的按位或,配合左移,实现数据的拼接大功告成啦!
例 二进制: 1101 0101
1010 1101<<8
1101 1010<<16
0000 0000<<24
结果 0000 0000 1101 1010 1010 1101 1101 0101
//int 4字节 buf是char 1字节 透明度是0不管
bmp_buf[i] = buf[3*i+2]<<16 | buf[3*i+1]<<8 | buf[3*i]; //其实写成代码也就一行搞定
注意:
图片显示代码如下:
#include
#include
#include
#include
#include
#include
int main()
{
// 1.打开lcd驱动
int lcd_fd;
lcd_fd = open("/dev/fb0", O_RDWR);
if(-1 == lcd_fd)
{
perror("open lcd failed!\n");
return -1;
}
// 2.打开图片
int bmp_fd;
bmp_fd = open("/LeiMu.bmp", O_RDONLY);
if(-1 == bmp_fd)
{
perror("open bmp failed!\n");
return -1;
}
char head[54] = {0};
read(bmp_fd, head, 54);
int w = *((int *)&head[18]);
int h = *((int *)&head[22]);
printf("w=%d, h=%d\n", w,h);
// 补齐4字节
int n_add; // 需要补的字节数
int add_a; // 补齐后的字节数
n_add = (4-w*3%4)%4;
add_a = w*3+n_add;
char bmp_add[add_a*h];
lseek(bmp_fd, 54, SEEK_SET);
read(bmp_fd, bmp_add, sizeof(bmp_add));
// 3.读取图片像素数据
char bmp_buf[w*h*3];
for(int j=0; j<h; j++)
memcpy(&bmp_buf[w*3*j], &bmp_add[add_a*j], w*3);
// 3.1 24--->32
int bmp_32[w*h];
for(int i=0; i<w*h; i++)
{// b g r a
bmp_32[i] = bmp_buf[3*i+0]<<0 | bmp_buf[3*i+1]<<8 | bmp_buf[3*i+2]<<16 | 0x00<<24;
}
// 3.2 翻转
int buffz[800*480];
int x0=0, y0=0;
for(int y=0; y<h; y++)
{
for(int x=0; x<w; x++)
{
buffz[800*(y+y0)+x+x0] = bmp_32[w*(h-1-y)+x];
}
}
// 4.把图片像素数据写入lcd
write(lcd_fd, buffz, sizeof(bmp_32));
// 5.关闭lcd,关闭图片
close(lcd_fd);
close(bmp_fd);
return 0;
}
图片显示效果如下:
猜到背景板上的人物了吗?OK,现在背景搞定了,接下来就是把按键以及头部和底部添上去了,和之前操作差不多其实,无非就是改了改参数而已(正常开发中,这些界面参数都会由UI设计师给出),难道我们要copy这些代码一个个费劲儿的调参吗?不,这里可以封装一下显示的函数,将需要显示的图片长宽以及起始点位置x、y和图片文件路径传给它就好了。
除了需要封装显示函数,这里我们采用效率更高的mmap映射的方式来实现用户空间和内核空间的数据直接交互从而省去了空间不同数据不通的繁琐过程。
mmap介绍:
#include
FB = mmap(NULL, 800*480*4, PROT_READ | PROT_WRITE, MAP_SHARED, lcd_fd, 0);
返回值为void型万能指针,可以和浮点型、整型等兼容。关键看定义的存储数据数组是什么类型就用什么类型;
共有6个参数:
munmap(FB, 800*480*4); //释放虚例内存函数
封装的显示图片函数代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#define LCD_PATH "/dev/fb0"
int lcd_fd;
int *FB;
//1,LCD初始化函数
void Lcd_Init(void)
{
//1,打开LCD文件
lcd_fd = open(LCD_PATH, O_RDWR);
if(-1 == lcd_fd)
{
perror("open lcd failed");
return;
}
//2,lcd映射到用户空间
FB = mmap(NULL, 800*480*4, PROT_READ | PROT_WRITE, MAP_SHARED, lcd_fd, 0);
if(MAP_FAILED == FB)
{
perror("mmap lcd failed");
return;
}
}
//2,LCD释放函数
void Lcd_Uninit(void)
{
//4,解除映射,关闭文件
munmap(FB, 800*480*4);
close(lcd_fd);
}
//3,显示宽度为win,高度为high的bmp图片 在起点坐标为(x_s, y_s)这个点开始显示图片
void Show_Bmp(int win, int high, int x_s, int y_s, char *picname)
{
int i, j;
int tmp;
char buf[win*high*3]; //存放图片的原始数据
int bmp_buf[win*high]; //存放转码之后的ARGB数据
//1,lcd初始化
Lcd_Init();
//2,读取图片数据,并且进行转码 RGB -> ARGB
//打开图片
FILE *fp = fopen(picname, "r");
if(NULL == fp)
{
perror("fopen failed");
return;
}
//读取图片像素点原始数据
fseek(fp, 54, SEEK_SET);
fread(buf, 3, win*high, fp);
//将读取的数据进行转码 24-->32
for(i=0; i<win*high; i++)
{
//ARGB R G B
bmp_buf[i] = buf[3*i+2]<<16 | buf[3*i+1]<<8 | buf[3*i];
}
//将转码的数据进行倒转 把第i行,第j列的点跟第479-i行,第j列的点进行交换
for(i=0; i<high/2; i++) //0~239行
{
for(j=0; j<win; j++) //0~799列
{
//第i行,第j列的点跟第479-i行,第j列的点进行交换
tmp = bmp_buf[win*i+j];
bmp_buf[win*i+j] = bmp_buf[win*(high-1-i)+j];
bmp_buf[win*(high-1-i)+j] = tmp;
}
}
//3,将转码之后的数据写入LCD (写入到LCD的区域由 (0,0) --> (100, 20))
for(i=y_s; i<high+y_s && i<480; i++) // 0 ~ high-1行 20 ~ high+20-1
{
for(j=x_s; j<win+x_s && j<800; j++) // 0~win-1列 100 ~ win+100-1
{
//FB[800*i+j] = bmp_buf[win*i+j];(图片的数组中第i行,第j列的点)
FB[800*i+j] = bmp_buf[win*(i-y_s)+j-x_s];
}
}
//4,lcd资源销毁,关闭图片
fclose(fp);
Lcd_Uninit();
}
int main()
{
Show_Bmp(800,480,0,0, "LeiMu.bmp");
Show_Bmp(800,80,0,0,"logo.bmp");//top部图片
for (int i = 0; i < 12; i++)
Show_Bmp(60,280,40+60*i,100,"key_off.bmp");
Show_Bmp(800,80,0,400,"bar.bmp");//bootom部图片
return 0;
}
到这里,以上代码就可以实现,钢琴界面的显示啦!效果见最开始的图哦。目前只是静态的显示,接下来要做的是获取点击的位置,判断区域,播放对应音乐,按键换图。
使用Linux中的输入子系统,可以轻松获取触摸屏的被触摸的坐标,引入#include
struct input_event
{
struct timeval time;时间戳,精确到微秒
_u16 type;输入事件类型
_u16 code;具体事件描述,比如触摸屏的EV_ABS中的ABS_X和ABS_Y;
_u16 value;具体动作描述
}
EV_SYN:事件分割标志
EV_ABS:发生了触摸屏事件,触摸屏坐标值ABS_X,ABS_Y;
EV_REL:发生了鼠标事件
EV_KEY:发生了键盘事件,设备的状态发生变化
BTN_TOUCH:点击事件
struct timeval{
_time_t tv_sec;秒
long int tv_usec;微秒
}
当用户点击时,只需要根据输入事件类型为触摸屏,就可以去具体事件描述里获取EV_ABS,这也就是触摸屏坐标值ABS_X,ABS_Y,废话不多说,直接上代码!
#include
#include
#include
#include
#include
#include
#include
#include
#include //输入子系统的头文件
#define LCD_PATH "/dev/fb0"
#define TS_PATH "/dev/input/event0"
int lcd_fd;
int *FB;
int ts_x, ts_y; //存放点击屏幕的横纵坐标
//获取一次点击触摸屏的坐标信息,存入ts_x,ts_y
int get_ts(void)
{
//1,打开触摸屏文件
int fd = open(TS_PATH, O_RDWR);
if(-1 == fd)
{
perror("open ts failed");
return -1;
}
//2,读取触摸屏文件数据
struct input_event xy;
int flag = 0; //记录当前获取坐标的信息
while(1)
{
read(fd, &xy, sizeof(xy));
if(xy.type == EV_ABS && xy.code == ABS_X && flag == 0)
{
ts_x = xy.value * 800 / 1024; //获取点击的时候X轴坐标的值 (0~1024)--> (0~800)
flag = 1;
}
if(xy.type == EV_ABS && xy.code == ABS_Y && flag == 1)
{
ts_y = xy.value * 480 / 600; //获取点击的时候Y轴坐标的值 (0~600)-->(0~480)
flag = 2;
}
//设置条件:每读取一次完整的坐标,就打印一次坐标
if(flag == 2)
{
flag = 0;
printf("(%d,%d)\n", ts_x, ts_y);
break; //获取一次坐标就跳出循环
}
}
//3,关闭触摸屏文件
close(fd);
}
这里qmy_lhl博主的一个比较好的地方,定义一个flag来控制获取函数的结束,同时需要注意获取点击时候坐标的值,注意一个细节,ts_x,ts_y是定义的全局变量便于拿到主函数里去用。
最后,再坚持一下下,这个项目接近实现啦!
现在我们有了用户按压了琴键,这时候需要播放对应的声音,我们借助Alsa(Advanced Linux Sound Architecture)库(官方去下载即可,搜一下就有)来实现播放,这里记得复制libasound.so.2加上混响并配置一下全局变量就好了
现在,我们在代码中执行下面的代码即可播放对应路径的声音!
execlp("aplay","aplay",”./mp3/d1.wav”,NULL);
琴键那么多声音,难道我们要一个个手戳重复的代码来播放吗?不,这里封装一下下播放函数,只需要传一个编号就能播放一个MP3目录下对应的声音;
void play(int num){
char str[32] = {0};
printf("%d\n",num);
sprintf(str,"./mp3/d%d.wav",num);//打印字符到str中
execlp("aplay","aplay",str,NULL);
return;
}
秋豆麻袋,目前用户点击一下就会播放一次对应编号的声音,这是一个线程,多点多放对应着多线程的问题,下面是一个很形象的比喻:
多线程的问题是多个人同时吃一道菜的时候容易发生争抢,例如两个人同时夹一个菜,一个人刚伸出筷子,结果伸到的时候已经被夹走菜了。。。此时就必须等一个人夹一口之后,在还给另外一个人夹菜,也就是说资源共享就会发生冲突争抢。
1、对于 Windows 系统来说,【开桌子】的开销很大,因此 Windows 鼓励大家在一个桌子上吃菜。因此 Windows 多线程学习重点是要大量面对资源争抢与同步方面的问题。
2、对于 Linux 系统来说,【开桌子】的开销很小,因此 Linux 鼓励大家尽量每个人都开自己的桌子吃菜。这带来新的问题是:坐在两张不同的桌子上,说话不方便。因此,Linux 下的学习重点大家要学习进程间通讯的方法。
这里,我们用fork()函数执行子进程实现播放多个声音,解决这个问题。
下面放上完整代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include //输入子系统的头文件
#define LCD_PATH "/dev/fb0"
#define TS_PATH "/dev/input/event0"
int lcd_fd;
int *FB;
int ts_x, ts_y; //存放点击屏幕的横纵坐标
//1,LCD初始化函数
void Lcd_Init(void)
{
//1,打开LCD文件
lcd_fd = open(LCD_PATH, O_RDWR);
if(-1 == lcd_fd)
{
perror("open lcd failed");
return;
}
//2,lcd映射到用户空间
FB = mmap(NULL, 800*480*4, PROT_READ | PROT_WRITE, MAP_SHARED, lcd_fd, 0);
if(MAP_FAILED == FB)
{
perror("mmap lcd failed");
return;
}
}
//2,LCD释放函数
void Lcd_Uninit(void)
{
//4,解除映射,关闭文件
munmap(FB, 800*480*4);
close(lcd_fd);
}
//3,显示宽度为win,高度为high的bmp图片 在起点坐标为(x_s, y_s)这个点开始显示图片
void Show_Bmp(int win, int high, int x_s, int y_s, char *picname)
{
int i, j;
int tmp;
char buf[win*high*3]; //存放图片的原始数据
int bmp_buf[win*high]; //存放转码之后的ARGB数据
//1,lcd初始化
Lcd_Init();
//2,读取图片数据,并且进行转码 RGB -> ARGB
//打开图片
FILE *fp = fopen(picname, "r");
if(NULL == fp)
{
perror("fopen failed");
return;
}
//读取图片像素点原始数据
fseek(fp, 54, SEEK_SET);
fread(buf, 3, win*high, fp);
//将读取的数据进行转码 24-->32
for(i=0; i<win*high; i++)
{
//ARGB R G B
bmp_buf[i] = buf[3*i+2]<<16 | buf[3*i+1]<<8 | buf[3*i];
}
//将转码的数据进行倒转 把第i行,第j列的点跟第479-i行,第j列的点进行交换
for(i=0; i<high/2; i++) //0~239行
{
for(j=0; j<win; j++) //0~799列
{
//第i行,第j列的点跟第479-i行,第j列的点进行交换
tmp = bmp_buf[win*i+j];
bmp_buf[win*i+j] = bmp_buf[win*(high-1-i)+j];
bmp_buf[win*(high-1-i)+j] = tmp;
}
}
//3,将转码之后的数据写入LCD (写入到LCD的区域由 (0,0) --> (100, 20))
for(i=y_s; i<high+y_s && i<480; i++) // 0 ~ high-1行 20 ~ high+20-1
{
for(j=x_s; j<win+x_s && j<800; j++) // 0~win-1列 100 ~ win+100-1
{
//FB[800*i+j] = bmp_buf[win*i+j];(图片的数组中第i行,第j列的点)
FB[800*i+j] = bmp_buf[win*(i-y_s)+j-x_s];
}
}
//4,lcd资源销毁,关闭图片
fclose(fp);
Lcd_Uninit();
}
//4,获取一次点击触摸屏的坐标信息,存入ts_x,ts_y
int get_ts(void)
{
//1,打开触摸屏文件
int fd = open(TS_PATH, O_RDWR);
if(-1 == fd)
{
perror("open ts failed");
return -1;
}
//2,读取触摸屏文件数据
struct input_event xy;
int flag = 0; //记录当前获取坐标的信息
while(1)
{
read(fd, &xy, sizeof(xy));
if(xy.type == EV_ABS && xy.code == ABS_X && flag == 0)
{
ts_x = xy.value * 800 / 1024; //获取点击的时候X轴坐标的值 (0~1024)--> (0~800)
flag = 1;
}
if(xy.type == EV_ABS && xy.code == ABS_Y && flag == 1)
{
ts_y = xy.value * 480 / 600; //获取点击的时候Y轴坐标的值 (0~600)-->(0~480)
flag = 2;
}
//设置条件:每读取一次完整的坐标,就打印一次坐标
if(flag == 2)
{
flag = 0;
printf("(%d,%d)\n", ts_x, ts_y);
break; //获取一次坐标就跳出循环
}
}
//3,关闭触摸屏文件
close(fd);
}
void play(int num){
char str[32] = {0};
printf("%d\n",num);
sprintf(str,"./mp3/d%d.wav",num);//打印字符到str中
execlp("aplay","aplay",str,NULL);
return;
}
int main()
{
Show_Bmp(800,480,0,0, "LeiMu.bmp");
Show_Bmp(800,80,0,0,"logo.bmp");//top
for (int i = 0; i < 12; i++)
Show_Bmp(60,280,40+60*i,100,"key_off.bmp");
Show_Bmp(800,80,0,400,"bar.bmp");//bootom
while (1)
{
get_ts();
int num;long stime;
num = (ts_x-40)/60;
if(ts_y > 100 && ts_y < 380 && ts_x > 40+num*60 && ts_x < 100+num*60)//如果点击对应区域就创建一条子进程播放音乐
{
Show_Bmp(60,280,40+num*60,100,"key_on.bmp");//改变按下的键
if(0 == fork()){
play(num+1);
}
stime = 200000;
usleep(stime);
Show_Bmp(60,280,40+num*60,100,"key_off.bmp");//恢复按下的键
}
}
return 0;
}
弄了个《小星星》代码播放的,毫无感情,全是技巧,播放即社死,千万别轻易尝试!
void Playstar(){
int star[4][7]={{1,1,5,5,6,6,5} //一闪一闪亮晶晶
,{4,4,3,3,2,2,1} //满天都是小星星
,{5,5,4,4,3,3,2} //挂在天空放光明,
,{5,5,4,4,3,3,2}}; //好像许多小眼睛
int stime;
for(int i=0;i<4;i++)
{
for(int j=0;j<7;j++)
{
if(0 == fork())
{
play(star[i][j]);
}
printf("%d\t",star[i][j]);
stime = 200000;
usleep(stime);
}
stime = 500000;
usleep(stime);
}
}
文章中省略了一些细节,但基本上完整复盘了该项目过程,如有错漏,欢迎指出哦!
参考文章:
一些GEC6818的小项目链接: