電子鋼琴專案
覆盤一下之前做的一個小專案,溫習一下c語言和Linux的知識,唔,就是這樣子。
一、環境搭建
所用軟體以及工具如下:
1、VMware-workstation-full搭配Ubuntu18.04的Linux作業系統,VMware是桌面虛擬計算機軟體,提供使用者可在單一的桌面上同時執行不同的作業系統和進行開發、測試 、部署新的應用程式。
2、Vsode程式碼編寫軟體,配置c語言開發環境
3、CH341/340和PL2303 串列埠驅動軟體
4、SecureCRT串列埠除錯軟體
5、GEC6818開發板如下圖
上述環境搭建百度一下就有很多很多教程,這裡不在贅述。真的要說配置環境,那得拉老長的文章了。
二、開發板的使用與Linux檔案IO
開發流程如下:
- 透過Vscode中進行程式碼編輯,進行開發後放入共享資料夾
- Ubantu中切換到共享目錄,使用交叉編譯器出可執行檔案
- 開啟SecureCRT將可執行檔案上傳開發板
- 在開發板上除錯執行
聯通開發板與電腦:
-
接通電源,插上開發板後,安裝CH341/340和PL2303串列埠驅動,
-
在計算機右鍵點選計算機管理中的裝置管理器檢視對應埠
-
開啟SecureCRT串列埠除錯軟體,點選快速連線
-
設定SSH為serial,埠為檢視的埠,波特率115200,流控全部關閉,點選連線即可
檔案上傳開發板:
- 可執行檔案(小):rx demo命令後點選傳輸選擇檔案上傳,百k以下
- 程式執行資源(大):如音訊、影像檔案等,透過隨身碟上傳,在 /mnt/udisk目錄下cp -r demo /就行;
Linux檔案IO
- open函式開啟檔案
- 定義資料緩衝區,write函式寫入資料
- lseek函式調整檔案位置偏移量
- 定義資料緩衝區,read函式存放讀到的資料
- 列印出讀到的資料
- 關閉對應檔案
小練習:德國國旗的顯示
終於簡單的歸納了一下部分內容到這裡,可以開始做個小Demo練習一下熟練知識點啦!顯示德國國旗在開發板上。(不要在意條條的顏色這些細節,問就是醬樣紫)
在開發板上顯示德國國旗程式碼如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
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]; //其實寫成程式碼也就一行搞定
注意:
- 邊界顯示:bmp圖片的寬所佔的位元組數如果不能被4整除,windows在儲存的時候,會在每一行的後面新增垃圾數湊夠4整除
- 影像偏移:BMP圖片有54位元組的標頭檔案儲存圖片的bmp格式圖片的長度和寬度,色深,大小……,所以需要用lseek進行偏移
- 圖形翻轉:因為BMP獨特的編碼方式:它的畫素點的編碼方式是上下顛倒的,我們在開發板的螢幕上顯示需要從最下面一行開始向上進行顯示,如下圖方便理解,左圖顯示的開發板,右圖緩衝區的圖片
圖片顯示程式碼如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
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 <sys/mman.h>
FB = mmap(NULL, 800*480*4, PROT_READ | PROT_WRITE, MAP_SHARED, lcd_fd, 0);
返回值為void型萬能指標,可以和浮點型、整型等相容。關鍵看定義的儲存資料陣列是什麼型別就用什麼型別;
共有6個引數:
- 第一個 對映記憶體的起始地址,我們一般用NULL,系統會自動尋找一個合適的起始地址。
- 第二個 對映記憶體的大小,就是我們要把一個多大的檔案對映到記憶體中,mmap對映後,會返回 給我們一個記憶體對映的起始地址,這個len就是我們檔案的大小,8004804。
- 第三個 對映記憶體的保護許可權,一般給可讀可寫就行。
- 第四個 我們要選共享也就是map-shared。
- 第五個 檔案描述符,把lcd檔案描述符給他就可以。
- 第六個 檔案對映的開始區域偏移量,那麼在螢幕上來說,要從左上角,也就是0開始。
munmap(FB, 800*480*4); //釋放虛例記憶體函式
- 第一個引數 釋放記憶體地址
- 第二個引數 釋放記憶體長度
封裝的顯示圖片函式程式碼如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <signal.h>
#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 <linux/input.h>即可開始使用,其實它是定義了一個結構體,如下所示
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 <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <signal.h>
#include <linux/input.h> //輸入子系統的標頭檔案
#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 <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <signal.h>
#include <linux/input.h> //輸入子系統的標頭檔案
#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);
}
}
文章中省略了一些細節,但基本上完整覆盤了該專案過程,如有錯漏,歡迎指出哦!
參考文章:
- 交叉編譯器arm-linux-gcc - 知乎 (zhihu.com)
- 認真分析mmap:是什麼 為什麼 怎麼用 - 胡瀟 - 部落格園 (cnblogs.com)
- (28條訊息) GEC6818開發板上音樂播放器_qmy_lhl的部落格-CSDN部落格_6818開發板
- 多程式和多執行緒的概念 - fengMisaka - 部落格園 (cnblogs.com)
一些GEC6818的小專案連結: