今日诗词分享:该段诗词描述的是国产武侠游戏《剑侠情缘三》中的职业——天策(游戏中对唐朝军人的称呼)。

天策

长河落日东都城,铁马戍边将军坟。

尽诛宵小天策义,长枪独守大唐魂。

==========================================================================

主机操作系统:Centos 6.7 
交叉编译器环境:arm-linux-gcc-4.5.4 (可通过命令/opt/buildroot-2012.08/arm920t/usr/bin/arm-linux-gcc -v查询)
开发板平台: fl2440 
Linux内核版本: linux-3.0 .54

==========================================================================

好了,接下来进入正题。之前,我们通过两种方法移植了madplay播放器,现在,开始尝试通过应用程序来实现用按键控制madplay播放器。按键用的是我们自己写的platform的按键驱动,不是内核自带的。

基于我们的程序,fl2440有四个按键,分别设置为:

KEY1:播放/暂停

KEY2:停止

KEY3:上一首

KEY4:下一首

按键控制播放器代码:

/**********************************************************************************      Copyright:  (C) 2017 qicheng*                  All rights reserved.**       Filename:  bbb.c*    Description:  This file *                 *        Version:  1.0.0(04/27/2017)*         Author:  yangni <497049229@qq.com>*      ChangeLog:  1, Release initial version on "04/27/2017 07:49:28 PM"*                 ********************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/select.h>
#include <sys/time.h>
#include <errno.h>
#include <sys/wait.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define KEY1  0x1         //1<<1
#define KEY2  0x2         //1<<2
#define KEY3  0x4         //1<<3
#define KEY4  0x8         //1<<4  /*共享内存申请标记*/
#define PERM S_IRUSR|S_IWUSR/*双向循环列表:存放歌曲名*/
struct song
{char songname[64];struct song *prev;struct song *next;
};/*孙子进程id号*/
pid_t gradchild;/*子进程id号*/
pid_t pid;/*共享内存描述标记*/
int shmid;char *p_addr;/*播放标记*/
int first_key=1;
int play_flag=0;/*************************************************
Function name: play
Parameter    : struct song *
Description  : 播放函数
Return       : void
Argument     : void
**************************************************/
void play(struct song *currentsong)
{pid_t fd;char *c_addr;char *p;int len;char my_song[64]="/mp3/song/";while(currentsong){/*创建子进程,即孙子进程*/fd = fork();if(fd == -1){   perror("fork");exit(1);}else if(fd == 0){/*把歌曲名加上根路径*/strcat(my_song,currentsong->songname);p = my_song;len = strlen(p);/*去掉文件名最后的'\n'*/my_song[len-1]='\0';printf("THIS SONG IS %s\n",my_song);execl("/mp3/madplay","madplay",my_song,NULL);printf("\n\n\n");}else{/*内存映射*/c_addr = shmat(shmid,0,0);/*把孙子进程的id和当前播放歌曲的节点指针传入共享内存*/memcpy(c_addr,&fd,sizeof(pid_t));memcpy(c_addr + sizeof(pid_t)+1,¤tsong,4);/*使用wait阻塞孙子进程,直到孙子进程播放完才能被唤醒;当被唤醒时,表示播放MP3期间没有按键按下,则继续顺序播放下一首MP3*/if(fd == wait(NULL)){currentsong = currentsong->next;printf("THE NEXT SONG IS %s\n",currentsong->songname);}}}
}/*************************************************
Function name: creat_song_list
Parameter    : void
Description  : 创建歌曲名的双向循环链表
Return       : struct song *
Argument     : void
**************************************************/
struct song *creat_song_list(void)
{   FILE *fd;size_t size;size_t len;char *line = NULL;struct song *head;struct song *p1;struct song *p2;system("ls /mp3/song > song_list");fd = fopen("song_list","r");p1 = (struct song *)malloc(sizeof(struct song));printf("==================================song list=====================================\n");system("ls /mp3/song");   printf("\n");printf("================================================================================\n");size = getline(&line,&len,fd);strncpy(p1->songname,line,strlen(line));head = p1;while((size = getline(&line,&len,fd)) != -1){   p2 = p1;p1 = (struct song *)malloc(sizeof(struct song));strncpy(p1->songname,line,strlen(line));p2->next = p1;p1->prev = p2;  }p1->next = head;head->prev = p1;p1 = NULL;p2 = NULL;system("rm -rf song_list");return head;
}
/*************************************************
Function name: startplay
Parameter    : pid_t *,struct song *
Description  : 开始播放函数
Return       : void
Argument     : void
**************************************************/
void startplay(pid_t *childpid,struct song *my_song)
{pid_t pid;int ret;/*创建子进程*/pid = fork();if(pid > 0){*childpid = pid;play_flag = 1;sleep(1);/*把孙子进程的pid传给父进程*/memcpy(&gradchild,p_addr,sizeof(pid_t));}else if(0 == pid){   /*子进程播放MP3函数*/play(my_song);}
}
/*************************************************
Function name: my_pause
Parameter    : pid_t
Description  : 暂停函数
Return       : void
Argument     : void
**************************************************/
void my_pause(pid_t pid)
{printf("=======================PAUSE!PRESS K1 TO CONTINUE===================\n");kill(pid,SIGSTOP); //对孙子进程发送SKGSTOP信号play_flag = 0;
}/*************************************************
Function name: my_pause
Parameter    : pid_t
Description  : 停止播放函数
Return       : void
Argument     : void
**************************************************/
void my_stop(pid_t g_pid)
{printf("=======================STOP!PRESS K1 TO START PLAY===================\n");kill(g_pid,SIGKILL); //对孙子进程发送SKGKILL信号kill(pid,SIGKILL);   //对子进程发送SKGKILL信号first_key=1;}/*************************************************
Function name: conti_play
Parameter    : pid_t
Description  : 继续函数
Return       : void
Argument     : void
**************************************************/
void conti_play(pid_t pid)
{printf("===============================CONTINUE=============================\n");kill(pid,SIGCONT); //对孙子进程发送SIGCONT信号play_flag=1;
}/*************************************************
Function name: next
Parameter    : pid_t
Description  : 下一首函数
Return       : void
Argument     : void
**************************************************/
void next(pid_t next_pid)
{struct song *nextsong;printf("===============================NEXT MP3=============================\n");/*从共享内存获得孙子进程播放歌曲的节点指针*/memcpy(&nextsong,p_addr + sizeof(pid_t)+1,4);/*指向下首歌曲的节点*/nextsong = nextsong->next;/*杀死当前歌曲播放的子进程,孙子进程*/kill(pid,SIGKILL);kill(next_pid,SIGKILL);wait(NULL);startplay(&pid,nextsong);
}/*************************************************
Function name: prev
Parameter    : pid_t
Description  : 上一首函数
Return       : void
Argument     : void
Autor & date : yuanhui 09.12.08
**************************************************/
void prev(pid_t prev_pid)
{struct song *prevsong;/*从共享内存获得孙子进程播放歌曲的节点指针*/printf("===============================PRIOR MP3=============================\n");memcpy(&prevsong,p_addr + sizeof(pid_t)+1,4);/*指向上首歌曲的节点*/prevsong = prevsong->prev;/*杀死当前歌曲播放的子进程,孙子进程*/kill(pid,SIGKILL);kill(prev_pid,SIGKILL);wait(NULL);startplay(&pid,prevsong);
}/*************************************************
Function name: main
Parameter    : void
Description  : 主函数
Return       : int
Argument     : void
**************************************************/
int main(void)
{int buttons_fd;int key_value;struct song *head;/*打开设备文件*/buttons_fd = open("/dev/button", 0);if (buttons_fd < 0) {perror("open device buttons");exit(1);}/*创建播放列表*/head = creat_song_list();printf("===================================OPTION=======================================\n\n\n\n");printf("        K1:START/PAUSE     K2:STOP   K3:NEXT      K4:PRIOR\n\n\n\n");printf("================================================================================\n");/*共享内存:用于存放子进程ID,播放列表位置*/if((shmid = shmget(IPC_PRIVATE,5,PERM))== -1)exit(1);p_addr = shmat(shmid,0,0);memset(p_addr,'\0',1024);while(1) {fd_set rds;int ret;FD_ZERO(&rds);FD_SET(buttons_fd, &rds);/*监听获取键值*/ret = select(buttons_fd + 1, &rds, NULL, NULL, NULL);if (ret < 0) {perror("select");exit(1);}if (ret == 0) printf("Timeout.\n");else if (FD_ISSET(buttons_fd, &rds)){int ret = read(buttons_fd, &key_value, sizeof key_value);if (ret != sizeof key_value) {if (errno != EAGAIN)perror("read buttons\n");continue;} else{//printf("buttons_value: %d\n", key_value+1);/*首次播放,必须是按键1*/if(first_key){switch(key_value){   case KEY1:startplay(&pid,head);first_key=0;break;case KEY2:case KEY3:case KEY4:printf("=======================PRESS K1 TO START PLAY===================\n");break;default:printf("=======================PRESS K1 TO START PLAY===================\n");break;} //end switch}//end if(first_key)/*若不是首次播放,则根据不同键值处理*/else if(!first_key){switch(key_value){case KEY1://printf("play_flag:%d\n",play_flag);if(play_flag)my_pause(gradchild);elseconti_play(gradchild);break;case KEY2:my_stop(gradchild);break;case KEY3:next(gradchild);break;case KEY4:prev(gradchild);break;} //end switch}//end if(!first_key)}}}close(buttons_fd);return 0;
}

从main函数入手看,该程序首先打开我们的按键设备。然后创建播放列表,再分配一段共享内存并初始化(用于存放子进程ID,播放列表位置)。然后通过select函数监听按键,通过播放标记first_key判断是否第一次播放,若first_key=1,则调用startplay()函数并把,first_key设为0,然后程序开始播放。

用交叉编译器编译完成后,把它下载到开发板上就可以运行了。注意,先得在开发板上下载好你的歌曲,需要提前把名字改为英文,然后创建文件夹,把歌曲放在一个文件夹下。比如我在根目录下创建了一个/mp3目录,/mp3目录下建了一个song目录,并把歌曲下载到song目录下,所以我的歌曲目录就为/mp3/song/,然后把madplay工具放在/mp3目录下,总之,打开的目录一定要对。


主要函数分析:

一、创建双向循环链表:

struct song *creat_song_list(void)
{   FILE *fd;size_t size;size_t len;char *line = NULL;struct song *head;struct song *p1;struct song *p2;system("ls /mp3/song > song_list");fd = fopen("song_list","r");p1 = (struct song *)malloc(sizeof(struct song));printf("==================================song list=====================================\n");system("ls /mp3/song");   printf("\n");printf("================================================================================\n");size = getline(&line,&len,fd);strncpy(p1->songname,line,strlen(line));head = p1;while((size = getline(&line,&len,fd)) != -1){   p2 = p1;p1 = (struct song *)malloc(sizeof(struct song));strncpy(p1->songname,line,strlen(line));p2->next = p1;p1->prev = p2;  }p1->next = head;head->prev = p1;p1 = NULL;p2 = NULL;system("rm -rf song_list");return head;
}
(1)system("ls /mp3/song > song_list");

这里相当于execl()函数,用于执行系统命令,将/mp3/song下的内容重定向的song_list中。如果没有指定重定向到哪,就默认打印到屏幕上。

(2)size = getline(&line,&len,fd);

其中fd是FILE数据结构,用于表示一个文件,和文件描述符类似。fd=fopen("song_list","w");这就是说读取song_list文件的数据,将其一行的首地址给&line,line保存读到的文本,len为要读的文件的大小,fd代表文件指针。fd=fopen("song_list","w");其实就是以写的方式打开文件,并返回文件指针给fd,但是这个Line是保存的是一行内容,需要调节指针不断读取文件才能读整个文件的内容。

(3)创建双向循环列表:

         因为歌曲需要实现上一首和下一首,所以链表能够前后访问。双向链表拥有头节点和尾节点。

struct song             //创建一个双向链表
{char songname[64];struct song *prev;struct song *next;
};
    struct song *head;   //分别定义song的结构体struct song *p1;struct song *p2;
    p2 = p1;p1 = (struct song *)malloc(sizeof(struct song));strncpy(p1->songname,line,strlen(line));p2->next = p1;p1->prev = p2; 

   头结点head相当于数组中的首地址,可以通过传参head来访问该链表。

二、开始播放函数:

void startplay(pid_t *childpid,struct song *my_song)
{pid_t pid;int ret;/*创建子进程*/pid = fork();if(pid > 0){*childpid = pid;play_flag = 1;sleep(1);/*把孙子进程的pid传给父进程*/memcpy(&gradchild,p_addr,sizeof(pid_t));}else if(0 == pid){   /*子进程播放MP3函数*/play(my_song);}
}

(1)函数调用在main中:

 startplay(&pid,head);

这里head是双向链表的返回值,也是它的头节点,相当于首地址。

(2)fork()函数:

         fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
        一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

 fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:

  • 在父进程中,fork返回新创建子进程的进程ID;
  • 在子进程中,fork返回0;
  • 如果出现错误,fork返回一个负值;

知道fork()函数使用后,我们就可以开始分析这段代码了。首先,创建了一个子进程(克隆一个自己),因为返回值有两次,所以父进程和子进程各做各的事,因为父进程睡眠了1秒。所以子进程就优先完成了调用play函数的工作。接下来我们就来看看play函数。

二、play函数:

void play(struct song *currentsong)
{pid_t fd;char *c_addr;char *p;int len;char my_song[64]="/mp3/song/";    //指定歌曲存放路径,最后的“/”不能少while(currentsong){/*创建子进程,即孙子进程*/fd = fork();if(fd == -1){   perror("fork");exit(1);}else if(fd == 0){/*把歌曲名加上根路径*/strcat(my_song,currentsong->songname);p = my_song;len = strlen(p);/*去掉文件名最后的'\n'*/my_song[len-1]='\0';printf("THIS SONG IS %s\n",my_song);execl("/mp3/madplay","madplay",my_song,NULL);printf("\n\n\n");}else{/*内存映射*/c_addr = shmat(shmid,0,0);/*把孙子进程的id和当前播放歌曲的节点指针传入共享内存*/memcpy(c_addr,&fd,sizeof(pid_t));memcpy(c_addr + sizeof(pid_t)+1,&currentsong,4);/*使用wait阻塞孙子进程,直到孙子进程播放完才能被唤醒;当被唤醒时,表示播放MP3期间没有按键按下,则继续顺序播放下一首MP3*/if(fd == wait(NULL)){currentsong = currentsong->next;printf("THE NEXT SONG IS %s\n",currentsong->songname);}}}
}

      因为执行该函数之前先执行startplay()函数, startplay函数已经创建了一个子进程,本函数由子进程执行过程中调用,所以本函数创建的子进程就 相当于孙进程了。

然后孙进程要做的事就是播放当前的歌曲。而子进程则映射了一段内存,把孙进程的id和当前的歌曲节点指针放入该内存,然后用wait()函数阻塞孙进程,直到孙进程播放结束后才能唤醒,当前歌曲指针指向下一首歌。

(1)内存映射 void *shmat(int shmid, const void *shmaddr, int shmflg);

shmat()是用来允许本进程访问一块共享内存的函数。
第一个参数:shmget返回的标识符,

第二个参数:如果 shmaddr 是NULL,系统将自动选择一个合适的地址! 如果shmaddr不是NULL 并且没有指定SHM_RND 则此段连接到addr所指定的地址上 如果shmaddr非0 并且指定了SHM_RND 则此段连接到shmaddr -(shmaddr mod SHMLAB)所表示的地址上.SHM_RND命令的意思是取整,SHMLAB的意思是低边界地址的倍数,它总是2的乘方。该算式是将地址向下取最近一个 SHMLAB的倍数。除非只计划在一种硬件上运行应用程序(这在当今是不大可能的),否则不用指定共享段所连接到的地址。所以一般应指定shmaddr为0,以便由内核选择地址。

第三个参数:如果在flag中指定了SHM_RDONLY位,则以只读方式连接此段,否则以读写的方式连接此段shmat返回值是该段所连接的实际地址 如果出错返回-1

(2)  wait(等待子进程中断或结束)

当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是个异步事件,这种信号也是内核向父进程发的异步通知。父进程可以忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号处理程序)。
父进程同步等待子进程退出时则调用wait函数,此时父进程可能会有如下三种情形:
①    阻塞(如果其所有子进程都还在运行)。
②    带回子进程的终止状态立即返回(如果已有一个子进程终止,正等待父进程取其终止状态)。
③    出错立即返回(如果它没有任何子进程)。

(3)execl()函数:

execl("/mp3/madplay","madplay",my_song,NULL);

第一个参数为命令的路径,第二个参数为命名的名字,后面跟的是参数(可以为多个),NULL表示结尾。

三、暂停函数:

void my_pause(pid_t pid)
{printf("=======================PAUSE!PRESS K1 TO CONTINUE===================\n");kill(pid,SIGSTOP);  //对孙子进程发送SKGSTOP信号play_flag = 0;
}

该函数调用了kill函数,发送信号将子进程暂停,并把播放标志设置为0 。my_stop(),conti_play()的用法与之类似。

(1) my_pause(gradchild);   //调用该函数

(2)SIGKILL和SIGSTOP的区别

SIGKILL提供给管理员杀死进程的权利,SIGSTOP提供给管理员暂停进程的权利, 所以这两个信号不能被忽略和重定义。

(3)int kill(pid_t pid, int sig);

函数说明:kill()可以用来送参数sig 指定的信号给参数pid 指定的进程。参数pid 有几种情况:

  • pid>0 将信号传给进程识别码为pid 的进程.
  • pid=0 将信号传给和目前进程相同进程组的所有进程
  • pid=-1 将信号广播传送给系统内所有的进程
  • pid<0 将信号传给进程组识别码为pid 绝对值的所有进程参数 sig 代表的信号编号可参考附录D

返回值:执行成功则返回0, 如果有错误则返回-1.
错误代码:
1、EINVAL 参数sig 不合法
2、ESRCH 参数pid 所指定的进程或进程组不存在
3、EPERM 权限不够无法传送信号给指定进程

四、切换到下一曲 void next()函数:

void next(pid_t next_pid)
{struct song *nextsong;printf("===============================NEXT MP3=============================\n");/*从共享内存获得孙子进程播放歌曲的节点指针*/memcpy(&nextsong,p_addr + sizeof(pid_t)+1,4);/*指向下首歌曲的节点*/nextsong = nextsong->next;/*杀死当前歌曲播放的子进程,孙子进程*/kill(pid,SIGKILL);kill(next_pid,SIGKILL);wait(NULL);startplay(&pid,nextsong);
}

这里重点是将从共享内存获得孙子进程播放歌曲的节点指针,将指针指向下首歌曲的节点,然后停止当前进程,等待子进程退出,再进行下一轮播放。切换到上一首歌曲的函数和该函数类似,只需要将指针指向前驱即可。

五、memcpy()函数
void *memcpy(void*dest, const void *src, size_t n);

功能:从源src所指的内存地址的起始位置开始,拷贝n个字节的数据到目标dest所指的内存地址的起始位置中。

1)src和dest所指内存区域不能重叠,函数返回指向dest的指针。如果src和dest以任何形式出现了重叠,它的结果是未定义的。

2)与strcpy相比,memcpy遇到’\0’不结束,而且一定会复制完n个字节。只要保证src开始有n字节的有效数据,dest开始有n字节内存空间就行。

3)如果目标数组本身已有数据,执行memcpy之后,将覆盖原有数据(最多覆盖n个)。如果要追加数据,则每次执行memcpy()后,要将目标地址增加到要追加数据的地址。

4)source和destin都不一定是数组,任意的可读写的空间均可。

关于 shmget()和shmat()函数的用法可参考原博客:
本文参考博客:http://blog.sina.com.cn/s/blog_95268f5001016gnf.html



												

fl2440——按键控制madplay音乐播放器相关推荐

  1. Linux开发板怎么用madplay,Linux中madplay 音乐播放器移植步骤

    madplay 音乐播放器移植步骤 madplay版本: madplay-0.15.2 交叉编译器版本: arm-linux-gcc 3.4.1 操作系统平台: Linux -- Red Hat 9. ...

  2. Android怎样实现控制第三方音乐播放器暂停、播放

    1.需求 怎么控制第三方音乐播放器暂停 播放呢 2.解决思路 写一个服务,当第三方播放器打开时 ,开启这个服务,音乐暂停:关闭服务,音乐继续 3.开启和关闭服务 startService(new In ...

  3. Android实现控制第三方音乐播放器暂停/播放

    实现控制第三方音乐播放器思路: 1.参考方法:可能实现第三方音乐播放器暂停/播放/下一曲/上一曲 思路:android模拟发送键值,就像蓝牙耳机等远程设备切换歌曲操作,但是行不通.原因是现在Andro ...

  4. Android 控制第三方音乐播放器

    项目场景: 最近在做穿戴项目时,需要针对第三方播放进行控制,如:播放.暂停.上一曲.下一曲等 问题描述 在百度上发现现在大都是通过AudioManager控制 /*** 播放 暂停*/public v ...

  5. android 车载控制手机音乐播放器,【图】浅谈车载音响播放器之安卓篇

    汽车音响无疑占据着开车乐趣个部分,无论你音乐发烧友与否,陶冶灵魂玩意我想有人去抗拒它,因为毕竟它个美东西,今天本人拿些玩音乐小心跟大家分享,欢迎交流探讨,相互学习! 抛开那 ...

  6. 控制iphone音乐播放器的相关函数

    https://github.com/rono23/GlovePod 国外牛人的开源工程,其说明如下 You can control music without taking off your glo ...

  7. 计算机毕业论文乐谱播放器,原创】电子音乐播放器的设计和制作毕业论文(20201002130117...

    <原创]电子音乐播放器的设计和制作毕业论文(20201002130117>由会员分享,可在线阅读,更多相关<原创]电子音乐播放器的设计和制作毕业论文(20201002130117(1 ...

  8. (附源码)springboot+基于微信小程序音乐播放器的设计与实现 毕业设计271156

    Springboot音乐播放小程序的设计与实现 摘 要 本文设计了一种基于微信小程序的音乐播放器,系统为人们提供了方便快捷.即用即搜的音乐搜索播放服务,包括音乐资讯.音乐库推荐.交流论坛.注册登录.最 ...

  9. android 音乐播放器论文,Android音乐播放器论文-Android文档类资源

    基于android系统的音乐播放器论文.里边有详细的介绍,没有代码.只是单独的一个论文. XXX科技大学本科生毕业设计(论文) 摘要 当今社会的生活节奏越来越快,随着硬件移动设备的越来越先进,人们 对 ...

最新文章

  1. go语言实现排序算法
  2. 解决复制虚拟机时候网络不从eth0开始问题
  3. nrf52832 DFU详细步骤 SDK13
  4. Bitmasking for introspection of Objective-C object pointers i
  5. boost::hana::all用法的测试程序
  6. hdu 3501 欧拉函数
  7. spring in action 读书笔记
  8. 【渝粤题库】广东开放大学社会学概论形成性考核
  9. spring boot 配置文件加密数据库用户名/密码
  10. 大数据_Flink_Java版_数据处理_流处理API_Flink中的UDF函数类---Flink工作笔记0036
  11. 三 数据结构 --数和二叉树
  12. tf.nn的conv2d卷积与max_pool池化
  13. 苹果PD20W快充方案缺货,芯片供应不足,怎么破局?
  14. Tushare Day3——了解stock_company并与stock_basic数据规模进行比较
  15. 机器学习-凸优化理论-课堂笔记
  16. visual studio 2017 安装离线MSDN
  17. pyqt5 图片随窗口变化等比例缩放
  18. 新手程序员去哪里?避雷小技巧交给你
  19. 音叉公振频率与双臂质量的计算关系
  20. 生成android使用的BKS证书

热门文章

  1. 计算机组装 仿真交互,《计算机硬件组装》仿真交互系统设计与实现.doc
  2. 【Android App】实战项目之实现你问我答的智能语音机器人(超详细 附源码和演示视频)
  3. 终于有人将TWI(串行通讯接口)给讲通了!
  4. 动作捕捉系统用于柔性机械臂的末端定位控制
  5. DVWA系列(二)----DVWA环境搭建
  6. web前端入门到实战:HTML5 video视频播放
  7. 数字化加速时刻,天津港解锁了“天工开物”新篇
  8. Deep Learning Paper读后简记
  9. 计算机网路络课设_学生宿舍网络规划与设计
  10. XMOS软件开发入门(4) - xc语言(2)之并发机制