Tiny6410学习成果—视频采集小车(2012年)

早就应该写这篇文章了,作品完成后到现在也有一年半的时间了。虽然自己很懒,但真心觉得不能在拖着了。该作品是在大学时自学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电脑端是软件制作过程吧!明儿见。

对了,文章中有错的地方还请告知。欢迎留言批评

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值