早就应该写这篇文章了,作品完成后到现在也有一年半的时间了。虽然自己很懒,但真心觉得不能在拖着了。该作品是在大学时自学linux做出的第一个作品。喜欢玩车模或者视频的同学一起分享。
效果图:(可能不太清晰 呵呵 见谅~)
该小车基于嵌入了Linux的ARM11,利用wifi建立网络连接电脑PC端,Socket套接字TCP建立通信,ARM11采集USB摄像头图像,然后把摄像头图像进行H264硬件编码,把H264数据发送到电脑PC服务端,在PC电脑上软件解码H264文件,并且DircetDraw显示图像,同时PC电脑端可发送命令控制小车前进,后退,转向等。
注释:该作品没有使用ffmpeg项目,反而是类似于如何通过其他简单的方式,实现ffmpeg中的一点点功能(用一些粗浅的代码,只为了实现原理。在这过程中也规划代码的接口和移植,当时只为了实现原理。)
说简单点就是,小车上的摄像头采集图像,然后压缩一下,传输给电脑,电脑再显示出图像来。
好了,废话不多说。来点实在的!讲讲在读者你们的6410或者其它开发板上如何制作这样的小车…
一 .ARM11的程序编写与配置
在ARM11上,我们要用到V4l2(video 4 linux 2)摄像头驱动,MFC驱动,TCP/IP网络驱动,和电机驱动。其中除电机启动外,linux2.6.38内核已包括,在linux内核源代码框架arch-arm文件夹,中可以找到V4L2,MFC,TCP/IP的驱动源代码。( 1 )电机驱动的制作与编写:
该电机驱动可以模仿LED驱动的原理去编写。
我们至少需要4个I/O口,来驱动电机,所以查一下01- Tiny6410硬件手册,寻找4个空余管脚:
这4个管脚就不错!
CON1.3 CON1.4 CON1.5 CON1.6
然后让我们查一下ARM11 S3C6410_中文用户手册,查看GPE的寄存器配置:
关于如何根据 芯片手册 datasheet 编写自己的LED驱动,还有linux驱动结构和设备号的一些细节问题,在这里我就不在赘述了。有不懂的同学在网上随便找找,能装麻袋。我直接给出驱动驱动代码了:
1 .马达驱动:
#include <linux/miscdevice.h>
#include <mach/hardware.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/moduleparam.h>
#include <linux/ioctl.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
#include <asm/atomic.h>
#include <mach/regs-gpio.h>
#include <plat/gpio-cfg.h>
#include <mach/gpio-bank-e.h>
#define DEVICE_NAME "myleds"//驱动名称
//当用户调用该驱动的ioctl()所要处理的信息
static long sbc2440_leds_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
switch(cmd) {
unsigned tmp;
case 0:
case 1:
if (arg > 4) {
return -EINVAL;
}
tmp = readl(S3C64XX_GPEDAT);//读GPEDAT寄存器的值
tmp &= ~(1 << (arg));
tmp |= ( (!cmd) << (arg) );
writel(tmp, S3C64XX_GPEDAT);//将tmp的值写入GPEDAT
printk (DEVICE_NAME": %d %d\n", arg, cmd);
return 0;
default:
return -EINVAL;
}
}
//file_operations 结构是如下初始化
static struct file_operations dev_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = sbc2440_leds_ioctl,
};
//miscdevice 结构是如下初始化
static struct miscdevice misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEVICE_NAME,
.fops = &dev_fops,
};
static int __init dev_init(void)
{
int ret;
{
//按照ARM11 S3C6410_中文用户手册,GPE口的寄存器配置
unsigned tmp;
tmp = readl(S3C64XX_GPECON);
tmp = (tmp & ~(0xffffU << 4))|(0x1111U << 4);
writel(tmp, S3C64XX_GPECON);
tmp = readl(S3C64XX_GPEDAT);
tmp |= (0xf);
writel(tmp, S3C64XX_GPEDAT);
}
ret = misc_register(&misc);///注册设备并建立节点,此函数中会自动创建设备节点,即设备文件
printk (DEVICE_NAME"\tinitialized\n");
return ret;
}
static void __exit dev_exit(void)
{
misc_deregister(&misc);//卸载一个misc设备,设备从linux内核中移除
}
module_init(dev_init);//linux驱动的入口函数
module_exit(dev_exit);//卸载函数
MODULE_LICENSE("GPL");//模块许可证声明(必须)
MODULE_AUTHOR("GXL Inc.");//用来声明模块的作者
在当前目录下的Makfile中添加:
Makefile中添加代码:
obj-$(CONFIG_MYLED) += myled.o
在当前目录下的Kconfig中添加代码:
config MYLED
tristate "LED for MY GPIO TO CTL IO"
depends on CPU_S3C6410
default y
help
This my LEDs connected to GPIO lines
on Mini6410 boards.
在内核配置中 添加为 module 方式:
编译完成后我们会得到zImage内核文件,和myled.ko驱动模块文件。这里我们没有把驱动编写近内核,方便驱动的调试。
这样我们小车,两个马达的驱动就写好了,哈哈完全套用LED的驱动确实有点太不要脸了,还是那句话,只为实现原理,一切从简。
2 .应用程序:linux应用程序的编写:
( 1 )工程文件建立:
工程文件建立:
建立文件夹:V4L_H264_TCP ,并进入
建立源代码文件文件:V4L_H264_TCP.c
复制MFC的API头文件“MfcDrvParams.h”,“MfcDriver.h”到V4L_H264_TCP下
建立Makefile文件
如图:
“MfcDrvParams.h”,“MfcDriver.h”这两个文件是访问s3c6410 MFC硬件模块(负责H264视频压缩的模块,因为6410内部就支持硬件压缩,使用硬件压缩方案可以减轻CPU的负担)用到的必须文件。在芯片配套的资料中就可以找到,额……别问我路径,时间长了忘了那个目录了。
编写V4L_H264_TCP.c 代码及其注释如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <getopt.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <malloc.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <asm/types.h>
#include <linux/videodev2.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include<pthread.h>
#include "MfcDriver.h"
#include "MfcDrvParams.h"
#define P_WIDTH 176 //采集图片的宽带
#define P_HEIGHT 144 //采集图片的高度
#define P_MSG (P_WIDTH*P_HEIGHT*2)//数据流长度
#define CLEAR(x) memset (&(x), 0, sizeof (x))
static char scpk = 1;允许发送
///*****Y422转Y420函数声明*******
void YUV422To420(unsigned char *pYUV, unsigned char *yuv, int lWidth, int lHeight);
char y420[P_MSG]; ///*****Y422转Y420空间*******
static int sock_fd=-1; ///********TCP句柄***********
static int led_fd=-1; ///********LED句柄***********
//***************************************************
// v4l2
//***************************************************
struct buffer//v4l2 mmap映射内存接收指针
{
void * start;
size_t length;
};
static int v4l2_fd = -1;///***v4l2句柄***
struct buffer * buffers = NULL;
static unsigned int n_buffers = 0;
//***************************************************
// h264
//***************************************************
typedef struct {
int width;
int height;
int frameRateRes;
int frameRateDiv;
int gopNum;
int bitrate;
} enc_info_t;
MFC_ARGS mfc_args;///H264编码描述结构
MFC_GET_BUF_ADDR_ARG get_buf_addr;///映射结构(地址)
MFC_ENC_EXE_ARG enc_exe;///解码函数返回结构接收
int mfc_fd = -1;//MFC设备句柄
char * mfc_addr = -1;//MFC映射接受地址
char * in_buf = 0;//输入数据指针
char * out_buf = 0;//输出数据指针
int re = 0;
/****************************************************************************/
// TCP 发送图像数据函数
/****************************************************************************/
static unsigned int dd=0,cc=0;
static int sand_frame (void)
{
struct v4l2_buffer buf;
CLEAR (buf);
///出列采集命令
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
ioctl (v4l2_fd , VIDIOC_DQBUF, &buf); //出列采集的帧缓冲
///把YUV422转成YUV420(H264只能接受YUV420的数据流进行编码或解码,我的摄像头只能采集YUV422,如直接采集可省略此步)
YUV422To420( buffers[buf.index].start , y420 , P_WIDTH, P_HEIGHT);
///MFC硬件编码
assert (buf.index < n_buffers);
memcpy( in_buf, y420, 38016);
//控制MFC开始编码
re=ioctl(mfc_fd, IOCTL_MFC_H264_ENC_EXE, &enc_exe);
if(re<0){
printf("MFC ENC_exe erorr!\n");
return 0;
}
///TCP 发送图片数据的长度;
write( sock_fd, (char*)&enc_exe.out_encoded_size, 4);
printf ("len of h264msg %d,\n",enc_exe.out_encoded_size);
dd=0;
cc=0;
///TCP 发送图片的数据
while(dd<enc_exe.out_encoded_size)
{
cc = write( sock_fd, out_buf, enc_exe.out_encoded_size - dd);
dd = dd + cc;
}
ioctl (v4l2_fd , VIDIOC_QBUF, &buf); //再将其入列
return 1;
}
/****************************************************************************/
// TCP 接受数据
/****************************************************************************/
static char ser_order[6];
unsigned long noffo;
void pkled(void)
{
while(1){
read( sock_fd, ser_order, 6);///接受命令
///控制管脚
if( ser_order[0] == 1){
noffo = ser_order[1];
ioctl( led_fd, noffo, 1);
noffo = ser_order[2];
ioctl( led_fd, noffo, 2);
noffo = ser_order[3];
ioctl( led_fd, noffo, 3);
noffo = ser_order[4];
ioctl( led_fd, noffo, 4);
}
}
}
int main (int argc,char ** argv)
{
///输入IP
if(argc !=2){
printf("no input ip!");
return 0;
}
unsigned int i;
struct v4l2_capability cap;//设备的基本信息结构
struct v4l2_format fmt;//帧的格式,比如宽度,高度等
enum v4l2_buf_type type; //数据流类型
///*************** LED **************************
led_fd = open("/dev/myleds",0);
/****************************************************************************/
// TCP
/****************************************************************************/
if((sock_fd=socket(AF_INET,SOCK_STREAM,0))==-1){
printf("socket,file!");
return 0;
}
///***************
/// 配置TCP 链接参数
///***************
struct sockaddr_in server;
bzero(&server,sizeof(server));//结构清零
server.sin_family = AF_INET;///TCP/IP协议簇
server.sin_port = htons(8080);///端口
server.sin_addr.s_addr = inet_addr(argv[1]);//所连接IP
/****************************************************************************/
// MFC
/****************************************************************************/
mfc_fd = open("/dev/s3c-mfc", O_RDWR|O_NDELAY); //打开MFC设备
if(mfc_fd<0){
printf("open mfc erorr!\n");
return 0;
}
///***************
/// 获取mfc 在内核中的首地址
///***************
mfc_addr = (char *) mmap(0,BUF_SIZE,PROT_READ | PROT_WRITE,MAP_SHARED,mfc_fd,0); //mmap MFC
if(mfc_addr<0){
printf("mfc mmap erorr!\n");
return 0;
}
///***************
/// 设置mfc 编码参数
///***************
mfc_args.enc_init.in_width = P_WIDTH;//编码图片宽度
mfc_args.enc_init.in_height = P_HEIGHT;//编码图片高度
mfc_args.enc_init.in_bitrate = 1000;//比特率
mfc_args.enc_init.in_gopNum = 1;//编码图片数量
mfc_args.enc_init.in_frameRateRes = 24;
mfc_args.enc_init.in_frameRateDiv = 0;
re=ioctl(mfc_fd, IOCTL_MFC_H264_ENC_INIT, &mfc_args);
if(re<0){
printf("init MFC enc_init erorr!\n");
return 0;
}
//得到MFC输入缓冲区地址
get_buf_addr.in_usr_data = (int)mfc_addr;
re=ioctl(mfc_fd, IOCTL_MFC_GET_FRAM_BUF_ADDR, &get_buf_addr);
if(re<0){
printf("get in_addr1 erorr!\n");
return 0;
}
in_buf = (char *)get_buf_addr.out_buf_addr;
if(in_buf<0){
printf("get in_addr2 erorr!\n");
return 0;
}
if(get_buf_addr.ret_code<0){
printf("get in_addr3 erorr!\n");
return 0;
}
//得到MFC输出缓冲区地址
get_buf_addr.in_usr_data = (int)mfc_addr;
re=ioctl(mfc_fd, IOCTL_MFC_GET_LINE_BUF_ADDR, &get_buf_addr);
if(re<0){
printf("get out_addr1 erorr!\n");
return 0;
}
out_buf = (char *)get_buf_addr.out_buf_addr;
if(out_buf<0){
printf("get out_addr2 erorr!\n");
return 0;
}
if(get_buf_addr.ret_code<0){
printf("get out_addr3 erorr!\n");
return 0;
}
/****************************************************************************/
// V4L2
/****************************************************************************/
v4l2_fd = open ("/dev/video2", O_RDWR /* required */ | O_NONBLOCK, 0);//打开设备
if(v4l2_fd<0){
printf("open v4l2 file!");
return 0;
}
ioctl (v4l2_fd , VIDIOC_QUERYCAP, &cap);//获取摄像头参数
///***************
/// 设置v4l读取图像格式
///***************
CLEAR (fmt);
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//数据流形式
fmt.fmt.pix.width = P_WIDTH;//采集图片宽度
fmt.fmt.pix.height = P_HEIGHT;//高度
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;///选择采集YUV422
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
ioctl (v4l2_fd , VIDIOC_S_FMT, &fmt); //设置图像格式
///***************
/// 查看V4L2实际创建了几个帧内存数据空间,并且建立相应数量的buffers(数据描述结构)
///***************
struct v4l2_requestbuffers req;
CLEAR (req);
req.count = 4;// 缓存数量
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;// 数据流类型
req.memory = V4L2_MEMORY_MMAP;// V4L2_MEMORY_MMAP(映射)
ioctl (v4l2_fd , VIDIOC_REQBUFS, &req); //申请缓冲,count是申请的数量
if (req.count < 2)
printf("Insufficient buffer memory\n");
buffers = calloc (req.count, sizeof (*buffers));//内存中建立对应空间
///***************
/// 把V4L2在内核中申请的数据空间与buffers对应起来(这样就可以通过buffers 访问内核的帧数据了)
///***************
for (n_buffers = 0; n_buffers < req.count; ++n_buffers)
{
struct v4l2_buffer buf; //驱动中的一帧
CLEAR (buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;// 宏表示 数据流类型
buf.memory = V4L2_MEMORY_MMAP;// V4L2_MEMORY_MMAP(映射)
buf.index = n_buffers;
if (-1 == ioctl (v4l2_fd , VIDIOC_QUERYBUF, &buf)) //映射用户空间
printf ("VIDIOC_QUERYBUF error\n");
buffers[n_buffers].length = buf.length;
buffers[n_buffers].start = mmap (NULL /* start anywhere */, //通过mmap建立映射关系
buf.length,
PROT_READ | PROT_WRITE /* required */,
MAP_SHARED /* recommended */,
v4l2_fd , buf.m.offset);
if (MAP_FAILED == buffers[n_buffers].start)
printf ("mmap failed\n");
}
//缓存入列
for (i = 0; i < n_buffers; ++i)
{
struct v4l2_buffer buf;
CLEAR (buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
if (-1 == ioctl (v4l2_fd, VIDIOC_QBUF, &buf))//申请到的缓冲进入列队
printf ("VIDIOC_QBUF failed\n");
}
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (-1 == ioctl (v4l2_fd, VIDIOC_STREAMON, &type)) //开始捕捉图像数据
printf ("VIDIOC_STREAMON failed\n");
//****向服务端申请建立链接****
if(connect(sock_fd,(struct sockaddr *)&server,sizeof(server))==-1){
printf("connect file!");
return 0;
}
///开启线程,接收TCP数据并处理
scpk = 1;
pthread_t reader;
pthread_create(&reader,NULL,(void*)&pkled,NULL);
while(1)//这一段涉及到异步IO
{
fd_set fds;
struct timeval tv;
int r;
FD_ZERO (&fds);//将指定的文件描述符集清空
FD_SET (v4l2_fd, &fds);//在文件描述符集合中增加一个新的文件描述符
/* Timeout. 设定超时时间*/
tv.tv_sec = 5;
tv.tv_usec = 0;
//判断是否可读(即摄像头是否准备好),tv是定时
r = select (v4l2_fd + 1, &fds, NULL, NULL, &tv);
if (-1 == r)
{
if (EINTR == errno)
continue;
printf ("select err\n");
}
if (0 == r)
{
fprintf (stderr, "select timeout\n");
exit (EXIT_FAILURE);
}
sand_frame ();//获取YUV并且发送数据
}
unmap:
///***************
/// 程序退出释放空间释放句柄
///***************
for (i = 0; i < n_buffers; ++i){
if (-1 == munmap (buffers[i].start, buffers[i].length)){
printf ("munmap error");
}
}
close (v4l2_fd );
close(mfc_fd);
close(sock_fd);
exit (EXIT_SUCCESS);
return 0;
}
//***************************************************
//YUV422 转YUV420 函数 因为MFC硬件压缩只接受 YUV420 所以要转一下
//***************************************************
void YUV422To420(unsigned char *pYUV, unsigned char *yuv, int lWidth, int lHeight)
{
int i,j;
unsigned char *pY = yuv;
unsigned char *pU = yuv + lWidth*lHeight;
unsigned char *pV = pU + (lWidth*lHeight)/4;
unsigned char *pYUVTemp = pYUV;
unsigned char *pYUVTempNext = pYUV+lWidth*2;
for(i=0; i<lHeight; i+=2)
{
for(j=0; j<lWidth; j+=2)
{
pY[j] = *pYUVTemp++;
pY[j+lWidth] = *pYUVTempNext++;
pU[j/2] =(*(pYUVTemp) + *(pYUVTempNext))/2;
pYUVTemp++;
pYUVTempNext++;
pY[j+1] = *pYUVTemp++;
pY[j+1+lWidth] = *pYUVTempNext++;
pV[j/2] =(*(pYUVTemp) + *(pYUVTempNext))/2;
pYUVTemp++;
pYUVTempNext++;
}
pYUVTemp+=lWidth*2;
pYUVTempNext+=lWidth*2;
pY+=lWidth*2;
pU+=lWidth/2;
pV+=lWidth/2;
}
}
所有的代码都扔在这一个文件里了,额….. 确实挺难看,也不方便阅读,这里做的确实不好。不过现在也没时间改进了。同学们就凑合着看吧。不过可千万别学我啊。建议同学们可以依据代码上的功能进行分类,分散到多个源文件中。
例如: My_H264.c 存放有关视频压缩的代码;
My_v4l2.c 存放视频采集的代码;
My_tcp.c 存放用于与电脑通讯的代码;
等等,在源文件中编写统一的调用接口,在main文件中调用。这样,不仅仅是为了看着舒服。有诸多好处,在复杂的体系中不容易出错(毕竟该作品结构比较简单),代码也方便移植到其他工程上重复利用。好了,在这里我就不继续批评与自我批评了。
编写Makefile代码如下:
V4L_H264_TCP:V4L_H264_TCP.o
arm-linux-gcc -o V4L_H264_TCP V4L_H264_TCP.o -lpthread
V4L_H264_TCP.o :V4L_H264_TCP.c MfcDriver.h MfcDrvParams.h
arm-linux-gcc -c V4L_H264_TCP.c
clean:
rm V4L_H264_TCP V4L_H264_TCP.o
在V4L_H264_TCP目录下输入命令:make 编译程序
得到V4L_H264_TCP可执行文件。
还要配置一下开发板的开机启动,让linux加载完成后,通过shell文件让开发板自己主动连接 wifi 和 加载 驱动为稍后与运行的应用程序创造环境。
ARM开机启动启动项配置:
把所编译的内核文件,驱动文件,可执行文件,下载在ARM11开发板中,启动ARM11
进入/etc/init.d/目录,该目录下的rcS 文件,为linux开机自启动所要运行的脚步,我们在此添加我们所要运行的程序,通过vi编译器输入代码如下:
start-wifi wpa2 arm 123456789
insmod /lib/modules/2.6.38-FriendlyARM/myled.ko
/V4L_H264_TCP 192.168.1.105
注释:
第一行为开启wifi命令,命令ARM的wifi链接路由器名为arm 密码为123456789
第二行为电机驱动(myleds)加载到内核中
第三行为运行我们所写的linux应用程序V4L_H264_TCP 以及服务端的IP
到此,ARM段的所以工作就都完成了。
同志们,22点半了!!明天还得上班呢!白天被资本家剥削,晚上回来独守空房写博客,这是何等的屌丝生活啊。。。不说了睡觉了。明天在说 PC电脑端是软件制作过程吧!明儿见。
对了,文章中有错的地方还请告知。欢迎留言批评