06-摄像头串口与音频.md 102 KB


title: 摄像头、串口与音频应用编程 tags: [

嵌入式Linux,
Linux应用编程,
V4L2,
摄像头,
视频采集,
串口,
UART,
termios,
ALSA,
alsa-lib,
PCM,
WAV,
音频,
IMX6ULL,

] created: 2026-09-18 updated: 2026-09-18

pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第二十五章 V4L2摄像头应用编程、第二十六章 串口应用编程、第二十八章 音频应用编程"

摄像头、串口与音频应用编程

💡 关联知识:[[03-外设与高级IO编程/01-高级IO]]、[[03-外设与高级IO编程/02-GPIO与LED应用编程]]、[[03-外设与高级IO编程/04-FrameBuffer与LCD应用编程]];延伸阅读:[[嵌入式Linux驱动开发实战/04-Linux总线与接口驱动/05-RS232与485通信]]、[[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/05-音频驱动]]。

本篇覆盖三类"数据流"外设的用户态应用编程:摄像头(V4L2 视频采集)、串口(termios 终端编程)、音频(ALSA 播放与录音)。它们的共同点是——应用层不直接碰寄存器,而是通过 /dev 设备节点与 /sys 属性文件,借助统一的驱动框架接口完成数据收发。掌握这三者,就具备了嵌入式 Linux 多媒体与外设通信的主力技能。


第一部分 V4L2 摄像头应用编程

1.1 V4L2 是什么

V4L2 是 Video for Linux Two 的简称,是 Linux 内核中视频类设备的一套驱动框架,为视频设备驱动开发和应用层提供统一的接口规范。最典型的视频类设备就是视频采集设备(各种摄像头)。

使用 V4L2 驱动框架注册的设备,会在 /dev 目录下生成设备节点,名称通常为 videoX(X 为数字编号,0、1、2……),每个 videoX 代表一个视频类设备。应用程序通过对 videoX 设备文件进行 I/O 操作来配置、使用设备。

开发板出厂系统对 ov5640、ov2640、ov7725 三款摄像头都支持,默认使能 ov5640(不能同时生效);也可直接插入 UVC USB 摄像头。USB 摄像头通常不支持 RGB565,多为 YUYV 格式。

1.2 摄像头编程流程

V4L2 摄像头应用编程有一套固定的流程,几乎所有操作都通过 ioctl() 完成,搭配不同的 V4L2 指令(request 参数)请求不同操作:

flowchart TB
    accTitle: V4L2 摄像头视频采集流程
    accDescr: 从打开设备、查询能力、枚举并设置格式,到申请帧缓冲、内存映射、入队、开启采集,最后循环出队处理再入队。
    A["open(/dev/videoX, O_RDWR)"] --> B["VIDIOC_QUERYCAP 查询设备能力"]
    B --> C{"capabilities 含 V4L2_CAP_VIDEO_CAPTURE?"}
    C -- 否 --> X["不是采集设备,退出"]
    C -- 是 --> D["VIDIOC_ENUM_FMT 枚举像素格式"]
    D --> E["VIDIOC_ENUM_FRAMESIZES / FRAMEINTERVALS 枚举分辨率与帧率"]
    E --> F["VIDIOC_S_FMT 设置帧格式"]
    F --> G["VIDIOC_REQBUFS 申请帧缓冲"]
    G --> H["VIDIOC_QUERYBUF + mmap 内存映射"]
    H --> I["VIDIOC_QBUF 帧缓冲入队"]
    I --> J["VIDIOC_STREAMON 开启采集"]
    J --> K["VIDIOC_DQBUF 出队取一帧"]
    K --> L["处理数据(显示/保存/编码)"]
    L --> M["VIDIOC_QBUF 重新入队"]
    M --> K
    K --> N["VIDIOC_STREAMOFF 结束采集"]

1.3 常用 ioctl 指令

所有指令定义在头文件 <linux/videodev2.h> 中,以宏 VIDIOC_XXX 形式提供,每个宏还携带一个 struct 类型——即调用 ioctl() 时第三个参数的类型,传入该类型变量的指针。

V4L2 指令 描述
VIDIOC_QUERYCAP 查询设备的属性/能力/功能
VIDIOC_ENUM_FMT 枚举设备支持的像素格式
VIDIOC_G_FMT 获取设备当前的帧格式信息
VIDIOC_S_FMT 设置帧格式信息
VIDIOC_REQBUFS 申请帧缓冲
VIDIOC_QUERYBUF 查询帧缓冲
VIDIOC_QBUF 帧缓冲入队操作
VIDIOC_DQBUF 帧缓冲出队操作
VIDIOC_STREAMON 开启视频采集
VIDIOC_STREAMOFF 关闭视频采集
VIDIOC_G_PARM 获取设备的流类型参数
VIDIOC_S_PARM 设置流类型参数(帧率)
VIDIOC_TRY_FMT 尝试设置帧格式,用于判断设备是否支持该格式
VIDIOC_ENUM_FRAMESIZES 枚举设备支持的视频采集分辨率
VIDIOC_ENUM_FRAMEINTERVALS 枚举设备支持的视频采集帧率

1.4 关键数据结构

1.4.1 struct v4l2_capability

struct v4l2_capability {
    __u8    driver[16];     /* 驱动的名字 */
    __u8    card[32];       /* 设备的名字 */
    __u8    bus_info[32];   /* 总线的名字 */
    __u32   version;        /* 版本信息 */
    __u32   capabilities;   /* 设备拥有的能力 */
    __u32   device_caps;
    __u32   reserved[3];    /* 保留字段 */
};

重点是 capabilities,它描述设备能力,可为多个值的位或。摄像头设备必须包含 V4L2_CAP_VIDEO_CAPTURE(0x00000001,视频采集)。其它常用值:

能力宏 含义
V4L2_CAP_VIDEO_CAPTURE 0x00000001 是视频采集设备
V4L2_CAP_VIDEO_OUTPUT 0x00000002 是视频输出设备
V4L2_CAP_VIDEO_OVERLAY 0x00000004 支持视频叠加
V4L2_CAP_READWRITE 0x01000000 支持 read/write 方式读写
V4L2_CAP_STREAMING 0x04000000 支持 streaming I/O 方式

1.4.2 struct v4l2_fmtdesc / frmsizeenum / frmivalenum

struct v4l2_fmtdesc {
    __u32 index;              /* Format number,枚举前设为 0,每次 +1 */
    __u32 type;               /* enum v4l2_buf_type */
    __u32 flags;
    __u8  description[32];    /* Description string,格式描述 */
    __u32 pixelformat;        /* Format fourcc,像素格式 */
    __u32 reserved[4];
};

struct v4l2_frmsizeenum {
    __u32 index;              /* Frame size number */
    __u32 pixel_format;       /* 像素格式 */
    __u32 type;               /* type */
    union {
        struct v4l2_frmsize_discrete discrete;
        struct v4l2_frmsize_stepwise stepwise;
    };
    __u32 reserved[2];
};

struct v4l2_frmsize_discrete {
    __u32 width;              /* Frame width [pixel] */
    __u32 height;             /* Frame height [pixel] */
};

struct v4l2_frmivalenum {
    __u32 index;              /* Frame format index */
    __u32 pixel_format;       /* Pixel format */
    __u32 width;              /* Frame width */
    __u32 height;             /* Frame height */
    __u32 type;               /* type */
    union {
        struct v4l2_fract discrete;
        struct v4l2_frmival_stepwise stepwise;
    };
    __u32 reserved[2];
};

struct v4l2_fract {
    __u32 numerator;          /* 分子 */
    __u32 denominator;        /* 分母 */
};
  • index:编号,枚举前设为 0,每次 ioctl() 后加 1,直到调用失败表示枚举完。
  • pixel_format/width/height:调用前需设置,指定枚举"哪种格式、哪个分辨率"。
  • type = V4L2_BUF_TYPE_VIDEO_CAPTUREdiscrete 生效。帧率 = denominator / numerator

常用像素格式(由 v4l2_fourcc 宏合成 32 位数据):

像素格式宏 fourcc 含义
V4L2_PIX_FMT_RGB565 'RGBP' 16 位 RGB 5-6-5
V4L2_PIX_FMT_RGB555 'RGBO' 16 位 RGB 5-5-5
V4L2_PIX_FMT_YUYV 'YUYV' 16 位 YUV 4:2:2
V4L2_PIX_FMT_MJPEG 'MJPG' Motion-JPEG
V4L2_PIX_FMT_GREY 'GREY' 8 位灰度

v4l2_buf_type 常用取值:

取值 含义
V4L2_BUF_TYPE_VIDEO_CAPTURE(1) 视频采集
V4L2_BUF_TYPE_VIDEO_OUTPUT(2) 视频输出
V4L2_BUF_TYPE_VIDEO_OVERLAY(3) 视频叠加
V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE(9) 多平面视频采集

1.4.3 struct v4l2_format / v4l2_pix_format

struct v4l2_format {
    __u32 type;
    union {
        struct v4l2_pix_format        pix;     /* V4L2_BUF_TYPE_VIDEO_CAPTURE */
        struct v4l2_pix_format_mplane pix_mp;  /* ..._MPLANE */
        struct v4l2_window            win;     /* ..._OVERLAY */
        struct v4l2_vbi_format        vbi;     /* ..._VBI_CAPTURE */
        __u8                          raw_data[200];
    } fmt;
};

struct v4l2_pix_format {
    __u32 width;          /* 视频帧宽度(像素) */
    __u32 height;         /* 视频帧高度(像素) */
    __u32 pixelformat;    /* 像素格式 */
    __u32 field;          /* enum v4l2_field */
    __u32 bytesperline;   /* for padding, zero if unused */
    __u32 sizeimage;
    __u32 colorspace;     /* enum v4l2_colorspace */
    __u32 priv;
    __u32 flags;
    union { __u32 ycbcr_enc; __u32 hsv_enc; };
    __u32 quantization;
    __u32 xfer_func;
};

1.4.4 struct v4l2_streamparm / v4l2_captureparm

struct v4l2_streamparm {
    __u32 type;               /* enum v4l2_buf_type */
    union {
        struct v4l2_captureparm capture;
        struct v4l2_outputparm  output;
        __u8 raw_data[200];
    } parm;
};

struct v4l2_captureparm {
    __u32 capability;         /* Supported modes */
    __u32 capturemode;        /* Current mode */
    struct v4l2_fract timeperframe; /* Time per frame in seconds */
    __u32 extendedmode;
    __u32 readbuffers;
    __u32 reserved[4];
};

capability 字段标志:

标志 含义
V4L2_MODE_HIGHQUALITY 0x0001 高品质成像模式
V4L2_CAP_TIMEPERFRAME 0x1000 支持设置 timeperframe 字段

只有 capability 包含 V4L2_CAP_TIMEPERFRAME 时,应用层才能通过 VIDIOC_S_PARM 设置帧率。

1.4.5 struct v4l2_requestbuffers / v4l2_buffer

struct v4l2_requestbuffers {
    __u32 count;       /* 申请帧缓冲的数量 */
    __u32 type;        /* enum v4l2_buf_type */
    __u32 memory;      /* enum v4l2_memory */
    __u32 reserved[2];
};

struct v4l2_buffer {
    __u32 index;       /* buffer 的编号 */
    __u32 type;
    __u32 bytesused;
    __u32 flags;
    __u32 field;
    struct timeval timestamp;
    struct v4l2_timecode timecode;
    __u32 sequence;
    __u32 memory;      /* memory location */
    union {
        __u32 offset;         /* 偏移量 */
        unsigned long userptr;
        struct v4l2_plane *planes;
        __s32 fd;
    } m;
    __u32 length;      /* buffer 的长度 */
    __u32 reserved2;
    __u32 reserved;
};

enum v4l2_memory

取值 含义
V4L2_MEMORY_MMAP 1 内存映射(最常用)
V4L2_MEMORY_USERPTR 2 用户指针
V4L2_MEMORY_OVERLAY 3 叠加
V4L2_MEMORY_DMABUF 4 DMA 缓冲

length 是帧缓冲长度,m.offset 是帧缓冲在内核申请的那块大内存中的偏移量;VIDIOC_REQBUFS 时内核申请"数量 × 单个大小"的连续内存,每个帧缓冲对应其中一段。

1.5 帧缓冲队列与采集方式

V4L2 读取数据有两种方式:read 方式capabilitiesV4L2_CAP_READWRITE)和 streaming 方式(含 V4L2_CAP_STREAMING)。绝大多数设备支持 streaming I/O:内核维护一个帧缓冲队列,驱动不断把采集数据填入队列中的帧缓冲;应用取走一帧叫出队,处理完再放回队列叫入队

flowchart LR
    accTitle: 帧缓冲队列的入队出队
    accDescr: 内核维护帧缓冲队列,驱动向空闲缓冲写入采集数据,应用出队取走处理后重新入队,形成循环。
    Q["内核帧缓冲队列"]
    D["摄像头驱动<br/>写入一帧"] -->|填入空闲缓冲| Q
    Q -->|VIDIOC_DQBUF 出队| A["应用程序处理"]
    A -->|VIDIOC_QBUF 入队| Q
  • 申请:VIDIOC_REQBUFS,设置 counttypememory=V4L2_MEMORY_MMAP
  • 映射:对每个缓冲 VIDIOC_QUERYBUF 拿到 length/m.offset,再 mmap() 映射到用户空间;
  • 入队:对每个缓冲 VIDIOC_QBUF 放入内核队列;开启后用 VIDIOC_DQBUF 循环取帧,处理完再 VIDIOC_QBUF
  • 帧缓冲数量不要太多,嵌入式系统内存吃紧;太少又可能丢帧,例程取 3 个。

1.6 完整源码:v4l2_camera.c

源码路径:11、Linux C应用编程例程源码/25_v4l2_camera/v4l2_camera.c。功能:在 LCD 上实时显示摄像头采集图像(要求摄像头支持 RGB565)。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <errno.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#include <linux/fb.h>

#define FB_DEV              "/dev/fb0"      //LCD设备节点
#define FRAMEBUFFER_COUNT   3               //帧缓冲数量

/*** 摄像头像素格式及其描述信息 ***/
typedef struct camera_format {
    unsigned char description[32];  //字符串描述信息
    unsigned int pixelformat;       //像素格式
} cam_fmt;

/*** 描述一个帧缓冲的信息 ***/
typedef struct cam_buf_info {
    unsigned short *start;      //帧缓冲起始地址
    unsigned long length;       //帧缓冲长度
} cam_buf_info;

static int width;                       //LCD宽度
static int height;                      //LCD高度
static unsigned short *screen_base = NULL;//LCD显存基地址
static int fb_fd = -1;                  //LCD设备文件描述符
static int v4l2_fd = -1;                //摄像头设备文件描述符
static cam_buf_info buf_infos[FRAMEBUFFER_COUNT];
static cam_fmt cam_fmts[10];
static int frm_width, frm_height;   //视频帧宽度和高度

static int fb_dev_init(void)
{
    struct fb_var_screeninfo fb_var = {0};
    struct fb_fix_screeninfo fb_fix = {0};
    unsigned long screen_size;

    /* 打开framebuffer设备 */
    fb_fd = open(FB_DEV, O_RDWR);
    if (0 > fb_fd) {
        fprintf(stderr, "open error: %s: %s\n", FB_DEV, strerror(errno));
        return -1;
    }

    /* 获取framebuffer设备信息 */
    ioctl(fb_fd, FBIOGET_VSCREENINFO, &fb_var);
    ioctl(fb_fd, FBIOGET_FSCREENINFO, &fb_fix);

    screen_size = fb_fix.line_length * fb_var.yres;
    width = fb_var.xres;
    height = fb_var.yres;

    /* 内存映射 */
    screen_base = mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);
    if (MAP_FAILED == (void *)screen_base) {
        perror("mmap error");
        close(fb_fd);
        return -1;
    }

    /* LCD背景刷白 */
    memset(screen_base, 0xFF, screen_size);
    return 0;
}

static int v4l2_dev_init(const char *device)
{
    struct v4l2_capability cap = {0};

    /* 打开摄像头 */
    v4l2_fd = open(device, O_RDWR);
    if (0 > v4l2_fd) {
        fprintf(stderr, "open error: %s: %s\n", device, strerror(errno));
        return -1;
    }

    /* 查询设备功能 */
    ioctl(v4l2_fd, VIDIOC_QUERYCAP, &cap);

    /* 判断是否是视频采集设备 */
    if (!(V4L2_CAP_VIDEO_CAPTURE & cap.capabilities)) {
        fprintf(stderr, "Error: %s: No capture video device!\n", device);
        close(v4l2_fd);
        return -1;
    }

    return 0;
}

static void v4l2_enum_formats(void)
{
    struct v4l2_fmtdesc fmtdesc = {0};

    /* 枚举摄像头所支持的所有像素格式以及描述信息 */
    fmtdesc.index = 0;
    fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FMT, &fmtdesc)) {

        // 将枚举出来的格式以及描述信息存放在数组中
        cam_fmts[fmtdesc.index].pixelformat = fmtdesc.pixelformat;
        strcpy(cam_fmts[fmtdesc.index].description, fmtdesc.description);
        fmtdesc.index++;
    }
}

static void v4l2_print_formats(void)
{
    struct v4l2_frmsizeenum frmsize = {0};
    struct v4l2_frmivalenum frmival = {0};
    int i;

    frmsize.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    frmival.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    for (i = 0; cam_fmts[i].pixelformat; i++) {

        printf("format<0x%x>, description<%s>\n", cam_fmts[i].pixelformat,
                    cam_fmts[i].description);

        /* 枚举出摄像头所支持的所有视频采集分辨率 */
        frmsize.index = 0;
        frmsize.pixel_format = cam_fmts[i].pixelformat;
        frmival.pixel_format = cam_fmts[i].pixelformat;
        while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FRAMESIZES, &frmsize)) {

            printf("size<%d*%d> ",
                    frmsize.discrete.width,
                    frmsize.discrete.height);
            frmsize.index++;

            /* 获取摄像头视频采集帧率 */
            frmival.index = 0;
            frmival.width = frmsize.discrete.width;
            frmival.height = frmsize.discrete.height;
            while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmival)) {

                printf("<%dfps>", frmival.discrete.denominator /
                        frmival.discrete.numerator);
                frmival.index++;
            }
            printf("\n");
        }
        printf("\n");
    }
}

static int v4l2_set_format(void)
{
    struct v4l2_format fmt = {0};
    struct v4l2_streamparm streamparm = {0};

    /* 设置帧格式 */
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//type类型
    fmt.fmt.pix.width = width;  //视频帧宽度
    fmt.fmt.pix.height = height;//视频帧高度
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;  //像素格式
    if (0 > ioctl(v4l2_fd, VIDIOC_S_FMT, &fmt)) {
        fprintf(stderr, "ioctl error: VIDIOC_S_FMT: %s\n", strerror(errno));
        return -1;
    }

    /*** 判断是否已经设置为我们要求的RGB565像素格式
    如果没有设置成功表示该设备不支持RGB565像素格式 */
    if (V4L2_PIX_FMT_RGB565 != fmt.fmt.pix.pixelformat) {
        fprintf(stderr, "Error: the device does not support RGB565 format!\n");
        return -1;
    }

    frm_width = fmt.fmt.pix.width;  //获取实际的帧宽度
    frm_height = fmt.fmt.pix.height;//获取实际的帧高度
    printf("视频帧大小<%d * %d>\n", frm_width, frm_height);

    /* 获取streamparm */
    streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ioctl(v4l2_fd, VIDIOC_G_PARM, &streamparm);

    /** 判断是否支持帧率设置 **/
    if (V4L2_CAP_TIMEPERFRAME & streamparm.parm.capture.capability) {
        streamparm.parm.capture.timeperframe.numerator = 1;
        streamparm.parm.capture.timeperframe.denominator = 30;//30fps
        if (0 > ioctl(v4l2_fd, VIDIOC_S_PARM, &streamparm)) {
            fprintf(stderr, "ioctl error: VIDIOC_S_PARM: %s\n", strerror(errno));
            return -1;
        }
    }

    return 0;
}

static int v4l2_init_buffer(void)
{
    struct v4l2_requestbuffers reqbuf = {0};
    struct v4l2_buffer buf = {0};

    /* 申请帧缓冲 */
    reqbuf.count = FRAMEBUFFER_COUNT;       //帧缓冲的数量
    reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    reqbuf.memory = V4L2_MEMORY_MMAP;
    if (0 > ioctl(v4l2_fd, VIDIOC_REQBUFS, &reqbuf)) {
        fprintf(stderr, "ioctl error: VIDIOC_REQBUFS: %s\n", strerror(errno));
        return -1;
    }

    /* 建立内存映射 */
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) {

        ioctl(v4l2_fd, VIDIOC_QUERYBUF, &buf);
        buf_infos[buf.index].length = buf.length;
        buf_infos[buf.index].start = mmap(NULL, buf.length,
                PROT_READ | PROT_WRITE, MAP_SHARED,
                v4l2_fd, buf.m.offset);
        if (MAP_FAILED == buf_infos[buf.index].start) {
            perror("mmap error");
            return -1;
        }
    }

    /* 入队 */
    for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) {

        if (0 > ioctl(v4l2_fd, VIDIOC_QBUF, &buf)) {
            fprintf(stderr, "ioctl error: VIDIOC_QBUF: %s\n", strerror(errno));
            return -1;
        }
    }

    return 0;
}

static int v4l2_stream_on(void)
{
    /* 打开摄像头、摄像头开始采集数据 */
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    if (0 > ioctl(v4l2_fd, VIDIOC_STREAMON, &type)) {
        fprintf(stderr, "ioctl error: VIDIOC_STREAMON: %s\n", strerror(errno));
        return -1;
    }

    return 0;
}

static void v4l2_read_data(void)
{
    struct v4l2_buffer buf = {0};
    unsigned short *base;
    unsigned short *start;
    int min_w, min_h;
    int j;

    if (width > frm_width)
        min_w = frm_width;
    else
        min_w = width;
    if (height > frm_height)
        min_h = frm_height;
    else
        min_h = height;

    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    for ( ; ; ) {

        for(buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) {

            ioctl(v4l2_fd, VIDIOC_DQBUF, &buf);     //出队
            for (j = 0, base=screen_base, start=buf_infos[buf.index].start;
                        j < min_h; j++) {

                memcpy(base, start, min_w * 2); //RGB565 一个像素占2个字节
                base += width;  //LCD显示指向下一行
                start += frm_width;//指向下一行数据
            }

            // 数据处理完之后、再入队、往复
            ioctl(v4l2_fd, VIDIOC_QBUF, &buf);
        }
    }
}

int main(int argc, char *argv[])
{
    if (2 != argc) {
        fprintf(stderr, "Usage: %s <video_dev>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* 初始化LCD */
    if (fb_dev_init())
        exit(EXIT_FAILURE);

    /* 初始化摄像头 */
    if (v4l2_dev_init(argv[1]))
        exit(EXIT_FAILURE);

    /* 枚举所有格式并打印摄像头支持的分辨率及帧率 */
    v4l2_enum_formats();
    v4l2_print_formats();

    /* 设置格式 */
    if (v4l2_set_format())
        exit(EXIT_FAILURE);

    /* 初始化帧缓冲:申请、内存映射、入队 */
    if (v4l2_init_buffer())
        exit(EXIT_FAILURE);

    /* 开启视频采集 */
    if (v4l2_stream_on())
        exit(EXIT_FAILURE);

    /* 读取数据:出队 */
    v4l2_read_data();       //在函数内循环采集数据、将其显示到LCD屏

    exit(EXIT_SUCCESS);
}

逐段解释

  • fb_dev_init():为显示服务。打开 /dev/fb0,用 FBIOGET_VSCREENINFO/FBIOGET_FSCREENINFO 取分辨率、一行字节数,mmap 显存,刷白背景。LCD 是 RGB565 显示设备。
  • v4l2_dev_init():打开 /dev/videoX 并用 VIDIOC_QUERYCAP 校验采集能力。
  • v4l2_enum_formats():循环 VIDIOC_ENUM_FMT,把每个格式的 pixelformatdescription 存入 cam_fmts[]
  • v4l2_print_formats():对每种格式再枚举分辨率和帧率,便于选型调试(部分摄像头驱动未实现该功能,只打印格式属正常)。
  • v4l2_set_format():把帧格式设为与 LCD 同分辨率的 RGB565;若驱动不支持 RGB565 则退出(USB 摄像头常不支持)。
  • v4l2_init_buffer()VIDIOC_REQBUFS 申请 3 个 MMAP 缓冲 → 逐个 VIDIOC_QUERYBUF+mmap → 逐个 VIDIOC_QBUF 入队。
  • v4l2_read_data():无限循环"出队→逐行 memcpy 到显存→再入队"。min_w/min_h 取 LCD 与视频帧的较小值,避免越界;RGB565 每像素 2 字节,所以 memcpy 长度是 min_w * 2

1.7 交叉编译与实验步骤

# 1. 设置交叉编译工具环境(路径按实际安装调整)
source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi

# 2. 编译
${CC} -o testApp v4l2_camera.c

# 3. 确认是 ARM 可执行文件
file testApp      # 应显示 32-bit ARM

实验步骤:

  1. 先装摄像头再上电(正点原子摄像头必须启动前安装;USB 摄像头可热插拔)。
  2. scptestApp 拷到开发板家目录。
  3. 运行:./testApp /dev/video0(USB 摄像头可能是 /dev/video1,可用 ls /dev/video* 确认)。
  4. LCD 上应实时显示采集画面,终端打印格式/分辨率/帧率信息。
  5. 若提示不支持 RGB565,说明该摄像头(多为 USB UVC)输出 YUYV,需要做格式转换。
  6. Ctrl+C 结束程序。

1.8 扩展:YUYV → RGB565 转换

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

USB 摄像头常输出 YUYV(YUV 4:2:2),每 4 字节表示两个像素:Y0 U Y1 V。YUV→RGB 的常用整数公式:

/* YUYV(4:2:2) -> RGB565,每 4 字节产出 2 个像素 */
static inline unsigned short yuv2rgb565(int y, int u, int v)
{
    int r, g, b;

    /* 归一化到 0~255 */
    y = y - 16;  if (y < 0)  y = 0;
    u = u - 128;
    v = v - 128;

    r = (298 * y + 409 * v + 128) >> 8;
    g = (298 * y - 100 * u - 208 * v + 128) >> 8;
    b = (298 * y + 516 * u + 128) >> 8;

    if (r < 0) r = 0; else if (r > 255) r = 255;
    if (g < 0) g = 0; else if (g > 255) g = 255;
    if (b < 0) b = 0; else if (b > 255) b = 255;

    return (unsigned short)(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3));
}

/* 用法:把一帧 YUYV 数据转成 RGB565 后写入映射区 */
static void yuyv_to_rgb565(const unsigned char *src, unsigned short *dst, int pixels)
{
    int i;
    for (i = 0; i < pixels; i += 2) {
        int y0 = src[0], u = src[1], y1 = src[2], v = src[3];
        dst[0] = yuv2rgb565(y0, u, v);
        dst[1] = yuv2rgb565(y1, u, v);
        src += 4;
        dst += 2;
    }
}

第二部分 串口应用编程

2.1 串口与终端

串口全称串行接口,数据一个接一个按顺序传输,两条线即可双向通信(一发一收)。串口通信距离远、速度相对低,是常用的工业接口。在嵌入式 Linux 中,串口常作为系统的标准输入、输出设备:系统打印信息通过串口输出,用户通过串口与系统交互。

串口在 Linux 中就是一个终端(Terminal)。终端是"处理主机输入、输出的一套设备",能接受输入、能显示输出就是终端。终端分类:

类型 说明
本地终端 连接本机键盘显示器,如 /dev/tty1~/dev/tty63
串口远程终端 开发板最常见,通过串口线连接 PC,运行 putty/MobaXterm/SecureCRT
网络远程终端(伪终端) ssh/Telnet 登录,节点在 /dev/pts/X

设备节点:

  • /dev/ttyX:本地终端,X 为 0~63;
  • /dev/pts/X:伪终端;
  • /dev/ttymxcX:I.MX6U 串口终端。ALPHA/Mini 板有 UART1、UART3 两个串口,对应 /dev/ttymxc0/dev/ttymxc2(为什么是 0 和 2?因为 I.MX6U 支持 8 个串口,出厂系统只注册 UART1 和 UART3,编号即 0 和 2)。

mxc 这个名字与驱动/硬件平台有关,换个平台可能变成 /dev/ttyPSX 等,但前缀都是 tty。可用 who 命令查看系统当前连接了哪些终端。

2.2 termios API 与 struct termios

串口应用编程可以简单理解为对终端进行"配置 + 读写"。Linux 把底层 ioctl() 封装成一套标准 API,称为 termios API——它面向所有终端设备(串口、本地键盘鼠标、伪终端),不只是串口。使用时需包含头文件 <termios.h>

描述终端配置的数据结构是 struct termios

struct termios {
    tcflag_t c_iflag;    /* input mode flags  输入模式 */
    tcflag_t c_oflag;    /* output mode flags 输出模式 */
    tcflag_t c_cflag;    /* control mode flags 控制模式 */
    tcflag_t c_lflag;    /* local mode flags  本地模式 */
    cc_t     c_line;     /* line discipline   线路规程 */
    cc_t     c_cc[NCCS]; /* control characters 特殊控制字符 */
    speed_t  c_ispeed;   /* input speed  输入速率 */
    speed_t  c_ospeed;   /* output speed 输出速率 */
};

对这些成员不要直接整体初始化,而应用"按位与/或"添加或清除标志。另外,很多标志并非对所有终端都有效——本地键盘显示器没有波特率、数据位这些硬件概念。

2.2.1 输入模式 c_iflag

控制输入数据(驱动从串口/键盘收到的字符)在交给应用前的处理方式。

标志 含义
IGNBRK 忽略输入终止条件
BRKINT 检测到输入终止条件时发送 SIGINT 信号
IGNPAR 忽略帧错误和奇偶校验错误
PARMRK 对奇偶校验错误做出标记
INPCK 对接收到的数据执行奇偶校验
ISTRIP 将所有接收数据裁剪为 7 比特位(去掉第八位)
INLCR 将接收到的 NL(换行符)转换为 CR(回车符)
IGNCR 忽略接收到的 CR(回车符)
ICRNL 将接收到的 CR(回车符)转换为 NL(换行符)
IUCLC 将接收到的大写字符映射为小写字符
IXON 启动输出软件流控
IXOFF 启动输入软件流控

2.2.2 输出模式 c_oflag

控制输出字符在传递到串口/屏幕前的处理方式。

标志 含义
OPOST 启用输出处理功能;不设置该标志则其它标志都被忽略
OLCUC 将输出字符中的大写字符转换成小写字符
ONLCR 将输出中的换行符 NL(\n)转换成回车符 CR(\r
OCRNL 将输出中的回车符 CR(\r)转换成换行符 NL(\n
ONOCR 在第 0 列不输出回车符 CR
ONLRET 不输出回车符
OFILL 发送填充字符以提供延时
OFDEL 若设置该标志,填充字符为 DEL 字符,否则为 NULL 字符

2.2.3 控制模式 c_cflag

控制终端硬件特性,对串口最重要:波特率、数据位、校验位、停止位等。

标志 含义
CBAUD 波特率的位掩码
B0/B1200/B1800/B2400/B4800/B9600/B19200/B38400/B57600/B115200/B230400/B460800/B500000/B576000/B921600/B1000000/B1152000/B1500000/B2000000/B2500000/B3000000 对应波特率
CSIZE 数据位的位掩码
CS5/CS6/CS7/CS8 5/6/7/8 个数据位
CSTOPB 2 个停止位;不设置则默认 1 个停止位
CREAD 接收使能
PARENB 使能奇偶校验
PARODD 使用奇校验,而不是偶校验
HUPCL 关闭时挂断调制解调器
CLOCAL 忽略调制解调器控制线
CRTSCTS 使能硬件流控

Linux 下波特率由 CBAUD 位掩码的若干 bit 指定,并提供 cfgetispeed()/cfsetispeed()/cfsetospeed()/cfsetspeed() 函数获取与设置。

2.2.4 本地模式 c_lflag

控制终端的本地数据处理和工作模式。

标志 含义
ISIG 收到信号字符(INTR、QUIT 等)则产生相应信号
ICANON 启用规范模式
ECHO 启用输入字符的本地回显功能
ECHOE 若设置 ICANON,则允许退格操作
ECHOK 若设置 ICANON,则 KILL 字符会删除当前行
ECHONL 若设置 ICANON,则允许回显换行符
ECHOCTL 若设置 ECHO,控制字符会显示成 ^X
ECHOPRT 若设置 ICANONIECHO,删除字符和被删除字符都显示
ECHOKE 若设置 ICANON,允许回显 ECHOE/ECHOPRT 中设定的 KILL 字符
NOFLSH 通常收到 INTR/QUIT/SUSP 会清空输入输出队列;设置该标志则不清空
TOSTOP 后台进程写控制终端时,系统向该进程组发送 SIGTTOU
IEXTEN 启用输入处理功能

2.2.5 特殊控制字符 c_cc

对应键 作用
VEOF Ctrl+D 文件结尾符 EOF;read 返回 0 表示文件结束
VEOL CR 附加行结尾符
VEOL2 LF 第二行结尾符
VERASE Backspace 删除输入行最后一个字符
VINTR Ctrl+C 向与终端相连的进程发送 SIGINT
VKILL Ctrl+U 删除整个输入行
VMIN 非规范模式下,最少读取的字符数 MIN
VQUIT Ctrl+Z 发送 SIGQUIT
VSTART Ctrl+Q 重新启动被 STOP 暂停的输出
VSTOP Ctrl+S 停止向终端的进一步输出(XON/XOFF 流控)
VSUSP Ctrl+Z 发送 SIGSUSP,挂起当前应用程序
VTIME 非规范模式下,读取每个字符之间的超时(单位:十分之一秒)

2.3 终端的三种工作模式

终端有规范模式非规范模式原始模式三种,通过 c_lflagICANON 标志区分,默认是规范模式。

  • 规范模式(canonical):所有输入基于行处理。用户输入行结束符(回车、EOF 等)之前,read() 读不到任何字符;除 EOF 外的行结束符与普通字符一样被读入缓冲区;支持行编辑,一次 read() 最多读一行。
  • 非规范模式(non-canonical):所有输入即时有效,不需要行结束符,不可行编辑。由 MINc_cc[VMIN])与 TIMEc_cc[VTIME])决定 read() 行为:
MIN TIME read() 行为
0 0 总是立即返回:有数据则读并返回字节数,否则返回 0
>0 0 阻塞直到有 MIN 个字符可读才返回;到达文件尾返回 0
0 >0 只要有数据可读,或经过 TIME 个十分之一秒,立即返回;超时无数据返回 0
>0 >0 有 MIN 个字节可读,或两字符间隔超过 TIME 个十分之一秒才返回;至少读一个字节
  • 原始模式(raw):严格说是一种特殊的非规范模式。所有输入数据以字节为单位处理,终端不回显,禁用输入输出字符的所有特殊处理。通过 cfmakeraw() 设置,其内部等价于:

    termios_p->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP
                        | INLCR | IGNCR | ICRNL | IXON);
    termios_p->c_oflag &= ~OPOST;
    termios_p->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
    termios_p->c_cflag &= ~(CSIZE | PARENB);
    termios_p->c_cflag |= CS8;
    

什么时候用原始模式? 当串口不只做人机交互(数据按字符/ASCII 传输),而是与其他设备或传感器进行二进制数据通信时,数据不应做任何特殊处理,必须用原始模式。

2.4 串口编程 API 汇总

函数 原型 作用
tcgetattr int tcgetattr(int fd, struct termios *termios_p) 获取当前配置(便于事后恢复)
cfmakeraw void cfmakeraw(struct termios *termios_p) 配置为原始模式
cfsetispeed int cfsetispeed(struct termios *, speed_t) 设置输入波特率
cfsetospeed int cfsetospeed(struct termios *, speed_t) 设置输出波特率
cfsetspeed int cfsetspeed(struct termios *, speed_t) 一次设置输入输出波特率
tcflush int tcflush(int fd, int queue_selector) 清空输入/输出缓冲区
tcdrain int tcdrain(int fd) 阻塞直到输出缓冲区数据全部发送完毕
tcflow int tcflow(int fd, int action) 暂停/重启数据收发
tcsetattr int tcsetattr(int fd, int optional_actions, const struct termios *) 写入配置使其生效
read/write 读写数据

tcflush()queue_selector

取值 含义
TCIFLUSH 清空接收到但未被读取的数据
TCOFLUSH 清空尚未传输成功的输出数据
TCIOFLUSH 前两种都清空

tcflow()action

取值 含义
TCOOFF 暂停数据输出
TCOON 重新启动暂停的输出
TCIOFF 发送 STOP 字符,停止终端设备向系统发送数据
TCION 发送 START 字符,启动终端设备向系统发送数据

tcsetattr()optional_actions(生效时机):

取值 含义
TCSANOW 配置立即生效
TCSADRAIN 在所有写入 fd 的输出传输完毕之后生效
TCSAFLUSH 所有已接收但未读取的输入在配置生效前被丢弃

配置步骤(以原始模式为例):

struct termios new_cfg;
memset(&new_cfg, 0x0, sizeof(struct termios));

cfmakeraw(&new_cfg);                 // 原始模式
new_cfg.c_cflag |= CREAD;            // 接收使能
cfsetspeed(&new_cfg, B115200);       // 波特率

new_cfg.c_cflag &= ~CSIZE;           // 清数据位
new_cfg.c_cflag |= CS8;              // 8 位数据位

/* 奇校验 */
new_cfg.c_cflag |= (PARODD | PARENB);
new_cfg.c_iflag |= INPCK;
/* 偶校验 */
new_cfg.c_cflag |= PARENB;
new_cfg.c_cflag &= ~PARODD;
new_cfg.c_iflag |= INPCK;
/* 无校验 */
new_cfg.c_cflag &= ~PARENB;
new_cfg.c_iflag &= ~INPCK;

new_cfg.c_cflag &= ~CSTOPB;          // 1 个停止位(置位则 2 个)

new_cfg.c_cc[VTIME] = 0;             // MIN/TIME 在原始模式下同样有效
new_cfg.c_cc[VMIN]  = 0;             // 均置 0:read 立即返回(非阻塞效果)

tcflush(fd, TCIOFLUSH);              // 清缓冲
tcsetattr(fd, TCSANOW, &new_cfg);    // 生效

打开串口设备时使用 O_NOCTTY,告知系统该设备不会成为进程的控制终端:

fd = open("/dev/ttymxc2", O_RDWR | O_NOCTTY);

2.5 完整源码:uart_test.c

源码路径:11、Linux C应用编程例程源码/26_uart/uart_test.c。功能:串口在原始模式下收发数据,支持命令行指定设备、波特率、数据位、校验、停止位与读写类型;读操作使用异步 I/O(信号驱动)

#define _GNU_SOURCE     //在源文件开头定义_GNU_SOURCE宏
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <termios.h>

typedef struct uart_hardware_cfg {
    unsigned int baudrate;      /* 波特率 */
    unsigned char dbit;         /* 数据位 */
    char parity;                /* 奇偶校验 */
    unsigned char sbit;         /* 停止位 */
} uart_cfg_t;

static struct termios old_cfg;  //用于保存终端的配置参数
static int fd;      //串口终端对应的文件描述符

/**
 ** 串口初始化操作
 ** 参数device表示串口终端的设备节点
 **/
static int uart_init(const char *device)
{
    /* 打开串口终端 */
    fd = open(device, O_RDWR | O_NOCTTY);
    if (0 > fd) {
        fprintf(stderr, "open error: %s: %s\n", device, strerror(errno));
        return -1;
    }

    /* 获取串口当前的配置参数 */
    if (0 > tcgetattr(fd, &old_cfg)) {
        fprintf(stderr, "tcgetattr error: %s\n", strerror(errno));
        close(fd);
        return -1;
    }

    return 0;
}

/**
 ** 串口配置
 ** 参数cfg指向一个uart_cfg_t结构体对象
 **/
static int uart_cfg(const uart_cfg_t *cfg)
{
    struct termios new_cfg = {0};   //将new_cfg对象清零
    speed_t speed;

    /* 设置为原始模式 */
    cfmakeraw(&new_cfg);

    /* 使能接收 */
    new_cfg.c_cflag |= CREAD;

    /* 设置波特率 */
    switch (cfg->baudrate) {
    case 1200: speed = B1200;
        break;
    case 1800: speed = B1800;
        break;
    case 2400: speed = B2400;
        break;
    case 4800: speed = B4800;
        break;
    case 9600: speed = B9600;
        break;
    case 19200: speed = B19200;
        break;
    case 38400: speed = B38400;
        break;
    case 57600: speed = B57600;
        break;
    case 115200: speed = B115200;
        break;
    case 230400: speed = B230400;
        break;
    case 460800: speed = B460800;
        break;
    case 500000: speed = B500000;
        break;
    default:    //默认配置为115200
        speed = B115200;
        printf("default baud rate: 115200\n");
        break;
    }

    if (0 > cfsetspeed(&new_cfg, speed)) {
        fprintf(stderr, "cfsetspeed error: %s\n", strerror(errno));
        return -1;
    }

    /* 设置数据位大小 */
    new_cfg.c_cflag &= ~CSIZE;  //将数据位相关的比特位清零
    switch (cfg->dbit) {
    case 5:
        new_cfg.c_cflag |= CS5;
        break;
    case 6:
        new_cfg.c_cflag |= CS6;
        break;
    case 7:
        new_cfg.c_cflag |= CS7;
        break;
    case 8:
        new_cfg.c_cflag |= CS8;
        break;
    default:    //默认数据位大小为8
        new_cfg.c_cflag |= CS8;
        printf("default data bit size: 8\n");
        break;
    }

    /* 设置奇偶校验 */
    switch (cfg->parity) {
    case 'N':       //无校验
        new_cfg.c_cflag &= ~PARENB;
        new_cfg.c_iflag &= ~INPCK;
        break;
    case 'O':       //奇校验
        new_cfg.c_cflag |= (PARODD | PARENB);
        new_cfg.c_iflag |= INPCK;
        break;
    case 'E':       //偶校验
        new_cfg.c_cflag |= PARENB;
        new_cfg.c_cflag &= ~PARODD; /* 清除PARODD标志,配置为偶校验 */
        new_cfg.c_iflag |= INPCK;
        break;
    default:    //默认配置为无校验
        new_cfg.c_cflag &= ~PARENB;
        new_cfg.c_iflag &= ~INPCK;
        printf("default parity: N\n");
        break;
    }

    /* 设置停止位 */
    switch (cfg->sbit) {
    case 1:     //1个停止位
        new_cfg.c_cflag &= ~CSTOPB;
        break;
    case 2:     //2个停止位
        new_cfg.c_cflag |= CSTOPB;
        break;
    default:    //默认配置为1个停止位
        new_cfg.c_cflag &= ~CSTOPB;
        printf("default stop bit size: 1\n");
        break;
    }

    /* 将MIN和TIME设置为0 */
    new_cfg.c_cc[VTIME] = 0;
    new_cfg.c_cc[VMIN] = 0;

    /* 清空缓冲区 */
    if (0 > tcflush(fd, TCIOFLUSH)) {
        fprintf(stderr, "tcflush error: %s\n", strerror(errno));
        return -1;
    }

    /* 写入配置、使配置生效 */
    if (0 > tcsetattr(fd, TCSANOW, &new_cfg)) {
        fprintf(stderr, "tcsetattr error: %s\n", strerror(errno));
        return -1;
    }

    /* 配置OK 退出 */
    return 0;
}

/**
 ** 打印帮助信息
 **/
static void show_help(const char *app)
{
    printf("Usage: %s [选项]\n"
        "\n必选选项:\n"
        "  --dev=DEVICE     指定串口终端设备名称, 譬如--dev=/dev/ttymxc2\n"
        "  --type=TYPE      指定操作类型, 读串口还是写串口, 譬如--type=read(read表示读、write表示写、其它值无效)\n"
        "\n可选选项:\n"
        "  --brate=SPEED    指定串口波特率, 譬如--brate=115200\n"
        "  --dbit=SIZE      指定串口数据位个数, 譬如--dbit=8(可取值为: 5/6/7/8)\n"
        "  --parity=PARITY  指定串口奇偶校验方式, 譬如--parity=N(N表示无校验、O表示奇校验、E表示偶校验)\n"
        "  --sbit=SIZE      指定串口停止位个数, 譬如--sbit=1(可取值为: 1/2)\n"
        "  --help           查看本程序使用帮助信息\n\n", app);
}

/**
 ** 信号处理函数,当串口有数据可读时,会跳转到该函数执行
 **/
static void io_handler(int sig, siginfo_t *info, void *context)
{
    unsigned char buf[10] = {0};
    int ret;
    int n;

    if(SIGRTMIN != sig)
        return;

    /* 判断串口是否有数据可读 */
    if (POLL_IN == info->si_code) {
        ret = read(fd, buf, 8);     //一次最多读8个字节数据
        printf("[ ");
        for (n = 0; n < ret; n++)
            printf("0x%hhx ", buf[n]);
        printf("]\n");
    }
}

/**
 ** 异步I/O初始化函数
 **/
static void async_io_init(void)
{
    struct sigaction sigatn;
    int flag;

    /* 使能异步I/O */
    flag = fcntl(fd, F_GETFL);  //使能串口的异步I/O功能
    flag |= O_ASYNC;
    fcntl(fd, F_SETFL, flag);

    /* 设置异步I/O的所有者 */
    fcntl(fd, F_SETOWN, getpid());

    /* 指定实时信号SIGRTMIN作为异步I/O通知信号 */
    fcntl(fd, F_SETSIG, SIGRTMIN);

    /* 为实时信号SIGRTMIN注册信号处理函数 */
    sigatn.sa_sigaction = io_handler;   //当串口有数据可读时,会跳转到io_handler函数
    sigatn.sa_flags = SA_SIGINFO;
    sigemptyset(&sigatn.sa_mask);
    sigaction(SIGRTMIN, &sigatn, NULL);
}

int main(int argc, char *argv[])
{
    uart_cfg_t cfg = {0};
    char *device = NULL;
    int rw_flag = -1;
    unsigned char w_buf[10] = {0x11, 0x22, 0x33, 0x44,
                0x55, 0x66, 0x77, 0x88};    //通过串口发送出去的数据
    int n;

    /* 解析出参数 */
    for (n = 1; n < argc; n++) {

        if (!strncmp("--dev=", argv[n], 6))
            device = &argv[n][6];
        else if (!strncmp("--brate=", argv[n], 8))
            cfg.baudrate = atoi(&argv[n][8]);
        else if (!strncmp("--dbit=", argv[n], 7))
            cfg.dbit = atoi(&argv[n][7]);
        else if (!strncmp("--parity=", argv[n], 9))
            cfg.parity = argv[n][9];
        else if (!strncmp("--sbit=", argv[n], 7))
            cfg.sbit = atoi(&argv[n][7]);
        else if (!strncmp("--type=", argv[n], 7)) {
            if (!strcmp("read", &argv[n][7]))
                rw_flag = 0;        //读
            else if (!strcmp("write", &argv[n][7]))
                rw_flag = 1;        //写
        }
        else if (!strcmp("--help", argv[n])) {
            show_help(argv[0]); //打印帮助信息
            exit(EXIT_SUCCESS);
        }
    }

    if (NULL == device || -1 == rw_flag) {
        fprintf(stderr, "Error: the device and read|write type must be set!\n");
        show_help(argv[0]);
        exit(EXIT_FAILURE);
    }

    /* 串口初始化 */
    if (uart_init(device))
        exit(EXIT_FAILURE);

    /* 串口配置 */
    if (uart_cfg(&cfg)) {
        tcsetattr(fd, TCSANOW, &old_cfg);   //恢复到之前的配置
        close(fd);
        exit(EXIT_FAILURE);
    }

    /* 读|写串口 */
    switch (rw_flag) {
    case 0:  //读串口数据
        async_io_init();	//我们使用异步I/O方式读取串口的数据,调用该函数去初始化串口的异步I/O
        for ( ; ; )
            sleep(1);   	//进入休眠、等待有数据可读,有数据可读之后就会跳转到io_handler()函数
        break;
    case 1:   //向串口写入数据
        for ( ; ; ) {   		//循环向串口写入数据
            write(fd, w_buf, 8); 	//一次向串口写入8个字节
            sleep(1);       		//间隔1秒钟
        }
        break;
    }

    /* 退出 */
    tcsetattr(fd, TCSANOW, &old_cfg);   //恢复到之前的配置
    close(fd);
    exit(EXIT_SUCCESS);
}

逐段解释

  • uart_init():打开设备(O_RDWR | O_NOCTTY),并 tcgetattr() 保存原配置到 old_cfg(便于退出时恢复)。
  • uart_cfg():清零 new_cfgcfmakeraw();使能接收 CREADcfsetspeed() 设波特率;~(CSIZE) 清零后按参数置 CS5~CS8;校验同时操作 c_cflagPARENB/PARODD)与 c_iflagINPCK);停止位用 CSTOPBVMIN=VTIME=0 使 read() 立即返回;tcflush() 清缓冲;最后 tcsetattr(TCSANOW) 生效。
  • io_handler():异步 I/O 信号处理函数,判断 POLL_IN 后一次性读取最多 8 字节并打印十六进制。可读数据大于 8 字节时,多余数据留待下次 read()
  • async_io_init():设置 O_ASYNC 使能异步 I/O,F_SETOWN 指定接收信号的进程,F_SETSIG 指定实时信号 SIGRTMIN,再用 sigaction(SA_SIGINFO) 注册处理函数。
  • main():解析 --dev/--brate/--dbit/--parity/--sbit/--type/--helptype=read 时初始化异步 I/O 后 sleep 等待信号;type=write 时每秒写 8 字节 0x11..0x88

2.6 交叉编译与实验步骤

source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
${CC} -o testApp uart_test.c
file testApp

实验步骤:

  1. ALPHA 板有 UART1(USB 调试串口,/dev/ttymxc0)和 UART3(RS232/RS485,/dev/ttymxc2)。板上 485 和 232 共用 UART3,不能同时使用,由 JP1 端子选择。
  2. 不能用 USB 调试串口测试(它是系统控制台),使用 RS232 接口,通过 USB 转 RS232 线连到 PC。
  3. 拷到开发板后查看帮助:./testApp --help
  4. 读测试:./testApp --dev=/dev/ttymxc2 --type=read,在 PC 串口调试助手(如 XCOM)发送 8 字节 [0x11 0x22 ... 0x88],开发板打印收到的数据。
  5. 写测试:./testApp --dev=/dev/ttymxc2 --type=write,PC 端收到开发板每秒发来的 8 字节。
  6. Ctrl+C 结束。

2.7 扩展:GPS NMEA 数据解析

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

GPS 模块通常通过串口输出 NMEA 0183 文本报文。把上面的串口初始化到 9600、8N1、原始模式,即可按行读取并解析:

/* 假设已用 uart_init()/uart_cfg() 将串口配为 9600-8-N-1 原始模式 */
static void nmea_parse(const char *line)
{
    /* $GNRMC,hhmmss.ss,A,ddmm.mmmm,N,dddmm.mmmm,E,ss.s,ccc,v*CS */
    char buf[128];
    char *p, *field[16];
    int i = 0;

    strncpy(buf, line, sizeof(buf) - 1);
    buf[sizeof(buf) - 1] = '\0';

    p = strtok(buf, ",");
    while (p && i < 16) { field[i++] = p; p = strtok(NULL, ","); }

    if (i > 6 && (!strcmp(field[0] + 3, "RMC")) && field[2][0] == 'A') {
        double lat = atof(field[3]);   /* ddmm.mmmm 格式,需换算成度 */
        double lon = atof(field[5]);
        int lat_deg = (int)(lat / 100);
        int lon_deg = (int)(lon / 100);
        double lat_min = lat - lat_deg * 100;
        double lon_min = lon - lon_deg * 100;

        printf("纬度: %d.%06d %s, 经度: %d.%06d %s\n",
               lat_deg, (int)(lat_min * 10000), field[4],
               lon_deg, (int)(lon_min * 10000), field[6]);
    }
}

实际工程建议:使用环形缓冲区按 \n 切分报文;对 $GNRMC/$GNGGA 分别处理;用校验和(* 后的两位十六进制)验证完整性。


第三部分 ALSA 音频应用编程

3.1 ALSA 与 alsa-lib

ALSA 是 Advanced Linux Sound Architecture(高级 Linux 声音体系)的缩写,是 Linux 下的主流音频体系架构,提供音频和 MIDI 支持,替代了旧的 OSS。ALSA 本身是内核中的音频驱动框架,设计复杂、采用分离分层思想;但作为应用编程,我们无需研究它。

  • 硬件层:ALSA 驱动框架注册的 sound 设备在 /dev/snd 下生成设备节点。
  • 应用层:ALSA 提供标准 API,即 alsa-lib,应用程序调用即可控制底层音频硬件(播放、录音)。

    flowchart TB
    accTitle: ALSA 音频软件栈
    accDescr: 应用程序调用 alsa-lib 库函数,alsa-lib 通过 ioctl/read/write 操作 /dev/snd 下的 sound 设备节点,最终由 ALSA 内核驱动框架控制音频硬件。
    APP["应用程序<br/>(aplay / 自研程序)"] --> LIB["alsa-lib<br/>统一 C API"]
    LIB --> NODE["/dev/snd/*<br/>pcmC0D0p / pcmC0D0c / controlC0"]
    NODE --> DRV["ALSA 内核驱动框架"]
    DRV --> HW["音频编解码芯片<br/>WM8960 / ES8388"]
    

ALPHA I.MX6U 开发板 V2.4 及之前搭载 WM8960,V2.8 以后搭载 ES8388,均支持播放与录音。Mini 板没有板载音频编解码芯片,无法测试本章例程。

3.2 sound 设备节点

/dev/snd 下常见设备节点:

设备节点 含义
controlC0 声卡控制设备(通道选择、混音器、麦克风控制等),C0 = 声卡 0
pcmC0D0c 声卡 0、设备 0 的录音 PCM 设备(c = capture)
pcmC0D0p 声卡 0、设备 0 的播放 PCM 设备(p = playback)
pcmC0D1c 声卡 0、设备 1 的录音 PCM 设备
pcmC0D1p 声卡 0、设备 1 的播放 PCM 设备
timer 定时器

/proc/asound 目录记录声卡信息:

文件/命令 作用
cat /proc/asound/cards 列出系统中注册的所有声卡
cat /proc/asound/devices 列出所有声卡注册的设备(control、pcm、timer、seq 等)
cat /proc/asound/pcm 列出所有 PCM 设备(playback 与 capture)

3.3 音频基本概念

概念 说明
样本长度(Sample) 记录音频数据最基本的单元;即采样位数/位深度,如 8bit、16bit、24bit
声道数(channel) 单声道 Mono=1,双声道/立体声 Stereo=2
帧(frame) 一个声音单元,长度 = 样本长度 × 声道数。16bit 双声道一帧 = 16×2/8 = 4 字节
采样率(Sample rate) 每秒采样次数(针对帧)。8KHz 电话、22.05KHz FM、44.1KHz CD、48KHz 数字电视
交错模式(interleaved) 数据以连续帧存放:先帧 1 左、右声道样本,再帧 2 左、右……多数情况使用交错模式
周期(period) 音频设备读写数据的单位,一个周期包含若干帧,如 1024 帧
缓冲区(buffer) 由若干周期组成的一块空间,如 16 个周期
XRUN 录音时应用读得慢导致数据被覆盖叫 overrun;播放时应用写得慢导致缓冲"饿死"叫 underrun,统称 XRUN

为什么要把 buffer 拆成多个 period? 底层驱动用 DMA 搬运数据,每搬完一个 period 触发一次中断。若一次性搬完整个 buffer,数据量越大延迟越高。周期越小延迟越低,但中断越频繁、CPU 效率越低。因此在延迟可接受的前提下,周期尽量大一些,具体依应用场合而定。

3.4 环形缓冲区与指针

播放时:应用向 buffer 写数据(write pointer 前移),音频设备从 buffer 读数据(read pointer 前移);录音时相反——设备写、应用读。两个指针到达 buffer 末尾都会回到起始位置,构成环形缓冲区

flowchart LR
    accTitle: ALSA 环形缓冲区
    accDescr: buffer 由多个 period 组成,write pointer 指向应用写位置,read pointer 指向音频设备读位置,两者循环移动。
    subgraph BUF["buffer = N × period"]
        P0["period0"]
        P1["period1"]
        P2["period2"]
        P3["period3"]
    end
    WP["write pointer<br/>应用写入位置"] --> P1
    RP["read pointer<br/>设备读取位置"] --> P3

播放数据传输:应用写多少帧,write pointer 前移多少;写满一个周期,指针前移一个周期。buffer 满时应用不能再写,必须等设备播完一个周期腾出空闲周期。设备每次从 read pointer 读一个周期,读完前移一个周期。

录音数据传输:设备(ADC 采样 + DMA)每次采集一个周期写入 buffer,write pointer 前移一个周期;应用从 read pointer 读一个周期,读完后该周期变空闲,等待设备再次写入。

3.5 PCM 播放 API

3.5.1 打开/关闭 PCM 设备

int snd_pcm_open(snd_pcm_t **pcmp, const char *name,
                 snd_pcm_stream_t stream, int mode);
int snd_pcm_close(snd_pcm_t *pcm);
参数 说明
pcmp 返回 PCM 设备句柄(snd_pcm_t *
name 逻辑设备名,非设备文件名。"hw:0,0" = 声卡 0 的 PCM 设备 0,播放对应 /dev/snd/pcmC0D0p,录音对应 pcmC0D0c;还有 "plughw:i,j""default"
stream SND_PCM_STREAM_PLAYBACK(播放)或 SND_PCM_STREAM_CAPTURE(采集)
mode 通常为 0(默认,阻塞方式);SND_PCM_NONBLOCK 为非阻塞
ret = snd_pcm_open(&pcm_handle, "hw:0,0", SND_PCM_STREAM_PLAYBACK, 0);
if (0 > ret)
    fprintf(stderr, "snd_pcm_open error: %s\n", snd_strerror(ret));

3.5.2 设置硬件参数

snd_pcm_hw_params_t 描述硬件配置:

snd_pcm_hw_params_t *hwparams = NULL;
snd_pcm_hw_params_malloc(&hwparams);   /* 或 snd_pcm_hw_params_alloca() */
snd_pcm_hw_params_any(pcm_handle, hwparams);   /* 用设备当前配置初始化 */
/* ... 设置各项参数 ... */
snd_pcm_hw_params(pcm_handle, hwparams);        /* 加载生效 */
snd_pcm_hw_params_free(hwparams);
设置函数 作用 关键说明
snd_pcm_hw_params_set_access() 访问类型 SND_PCM_ACCESS_RW_INTERLEAVED(交错,配 readi/writei)、..._RW_NONINTERLEAVED(配 readn/writen
snd_pcm_hw_params_set_format() 数据格式 常用 SND_PCM_FORMAT_S16_LE(有符号 16 位小端)
snd_pcm_hw_params_set_channels() 声道数 2 = 双声道
snd_pcm_hw_params_set_rate() 采样率 val 如 44100;dir:-1 实际≤val、0 实际=val、1 实际≥val
snd_pcm_hw_params_set_period_size() 周期大小(帧) 如 1024
snd_pcm_hw_params_set_buffer_size() buffer 大小(帧) 如 16×1024
snd_pcm_hw_params_set_periods() buffer 大小(周期数) 如 16

snd_pcm_hw_params_set_period_size()val 单位是不是字节;snd_pcm_hw_params_set_periods()val 单位是周期snd_pcm_hw_params() 内部会自动调用 snd_pcm_prepare(),使设备进入 SND_PCM_STATE_PREPARED

3.5.3 读写数据与阻塞特性

snd_pcm_sframes_t snd_pcm_writei(snd_pcm_t *pcm, const void *buffer, snd_pcm_uframes_t size);
snd_pcm_sframes_t snd_pcm_readi(snd_pcm_t *pcm, void *buffer, snd_pcm_uframes_t size);
  • snd_pcm_writei() 把应用缓冲 buffer 的数据写入驱动层播放环形缓冲区;snd_pcm_readi() 从驱动层录音环形缓冲区读到应用缓冲。
  • size为单位,通常一次写/读一个周期。
  • 成功返回实际读/写的帧数(不一定等于 size,仅当发生信号或 XRUN 时可能小于);失败返回负错误码。
  • 阻塞snd_pcm_open() 用阻塞方式时,录音无数据可读会阻塞、播放缓冲满会阻塞;非阻塞时立即返回错误。snd_pcm_readi/writei 仅适用于交错模式,非交错用 snd_pcm_readn/writen
  • PREPARED 状态下首次调用 writei/readi 会自动调用 snd_pcm_start() 开启设备,因此示例无需手动 start。

buffer应用程序的缓冲区,不要与驱动层环形缓冲区混淆。

3.6 PCM 设备状态

snd_pcm_state_t snd_pcm_state(snd_pcm_t *pcm) 获取当前状态:

状态 含义
SND_PCM_STATE_OPEN 设备已打开(snd_pcm_open() 后)
SND_PCM_STATE_SETUP 设备已初始化、参数已配置好
SND_PCM_STATE_PREPARED 设备已就绪,可开始播放/录音
SND_PCM_STATE_RUNNING 正在运行(播放/录音中)
SND_PCM_STATE_XRUN 发生 XRUN;可调 snd_pcm_prepare() 恢复
SND_PCM_STATE_DRAINING Draining(播放中/采集停止)
SND_PCM_STATE_PAUSED 暂停
SND_PCM_STATE_SUSPENDED 硬件挂起;可用 snd_pcm_resume() 精细恢复
SND_PCM_STATE_DISCONNECTED 硬件已断开

相关函数:

int snd_pcm_prepare(snd_pcm_t *pcm);          /* 使设备进入 PREPARED */
int snd_pcm_start(snd_pcm_t *pcm);            /* 启动 */
int snd_pcm_drain(snd_pcm_t *pcm);            /* 处理完挂起帧后停止 */
int snd_pcm_drop(snd_pcm_t *pcm);             /* 立即停止,丢弃挂起帧 */
int snd_pcm_pause(snd_pcm_t *pcm, int enable);/* enable=1 暂停,0 恢复 */
int snd_pcm_resume(snd_pcm_t *pcm);           /* 从挂起恢复 */
int snd_pcm_hw_params_can_pause(const snd_pcm_hw_params_t *params);
int snd_pcm_hw_params_can_resume(const snd_pcm_hw_params_t *params);

状态转换:open() → OPEN;snd_pcm_hw_params() → SETUP → PREPARED(内部调 prepare);首次读写或 snd_pcm_start() → RUNNING;drop()/drain() → SETUP;XRUN 时处于 XRUN,prepare() 可恢复;pause() 在 RUNNING 与 PAUSED 间切换。

3.7 错误处理

snd_pcm_readi/writei 返回负错误码时用 snd_strerror() 获取描述:

返回值 含义 处理建议
-EBADFD PCM 设备状态不对 需处于 PREPARED 或 RUNNING,检查状态转换
-EPIPE 发生 XRUN snd_pcm_drop() 停止,或 snd_pcm_prepare() 恢复
-ESTRPIPE 硬件挂起(SUSPENDED) snd_pcm_resume() 精细恢复;不支持则 snd_pcm_prepare()

3.8 混音器 API

混音器用于配置音量、声道、增益等。snd_mixer_t 描述混音器,配置项称为元素(element),用 snd_mixer_elem_t 描述。初始化流程:

snd_mixer_t *mixer = NULL;

snd_mixer_open(&mixer, 0);                       /* 打开一个空混音器 */
snd_mixer_attach(mixer, "hw:0");                 /* 关联声卡控制设备(对应 /dev/snd/controlC0) */
snd_mixer_selem_register(mixer, NULL, NULL);     /* 注册,options/classp 传 NULL */
snd_mixer_load(mixer);                           /* 加载 */

遍历与取值:

函数 作用
snd_mixer_first_elem(mixer) / snd_mixer_last_elem(mixer) 第一个/最后一个元素
snd_mixer_elem_next(elem) / snd_mixer_elem_prev(elem) 下一个/上一个元素
snd_mixer_selem_get_name(elem) 获取元素名字(如 "Headphone""Playback"
snd_mixer_selem_has_playback_volume(elem) / ..._has_capture_volume(elem) 是否 volume 类型
snd_mixer_selem_has_playback_switch(elem) / ..._has_capture_switch(elem) 是否 switch(bool)类型
snd_mixer_selem_has_playback_channel(elem, ch) / ..._has_capture_channel(elem, ch) 是否含指定通道
snd_mixer_selem_is_playback_mono(elem) / ..._is_capture_mono(elem) 是否单声道
snd_mixer_selem_get_playback_volume_range(elem, &min, &max) 获取音量范围
snd_mixer_selem_get_playback_volume(elem, ch, &value) 获取某声道音量
snd_mixer_selem_set_playback_volume(elem, ch, value) 设置某声道音量
snd_mixer_selem_set_playback_volume_all(elem, value) 设置所有声道音量
snd_mixer_close(mixer) 关闭混音器

声道枚举 snd_mixer_selem_channel_id_tSND_MIXER_SCHN_FRONT_LEFT(0,左前)、SND_MIXER_SCHN_FRONT_RIGHT(右前)、SND_MIXER_SCHN_REAR_LEFTSND_MIXER_SCHN_REAR_RIGHTSND_MIXER_SCHN_FRONT_CENTERSND_MIXER_SCHN_WOOFERSND_MIXER_SCHN_MONO(= 左前)。

3.9 alsa-utils 工具

出厂系统已移植 alsa-lib 与 alsa-utils,可直接使用:

工具 用途
aplay xxx.wav 播放 wav(不支持 mp3 解码)
arecord -f cd -d 10 test.wav 录音 10 秒,-f cd = 16bit little endian 44100 stereo
alsamixer 字符图形化混音器配置界面(F3 Playback / F4 Capture / F5 All,F6 选声卡)
amixer scontrols / amixer scontents 列出配置项 / 查看配置说明
amixer sset Headphone 100,100 设置耳机音量左右声道
amixer sset "Headphone Playback ZC" off 开关某项
alsactl -f /var/lib/alsa/asound.state store 保存声卡配置
alsactl -f /var/lib/alsa/asound.state restore 加载声卡配置

开机自动从 /var/lib/alsa/asound.state 加载配置,关机时保存。此文件即 WM8960 声卡配置文件,由 alsactl 完成。

3.10 WAV 文件格式与解析

WAV 是 RIFF 格式,由若干 chunk 构成。例程定义了三个紧凑结构体(__attribute__((packed)) 保证无填充字节):

typedef struct WAV_RIFF {
    char ChunkID[4];         		/* "RIFF" */
    u_int32_t ChunkSize;        	/* 从下一个地址开始到文件末尾的总字节数 */
    char Format[4];             	/* "WAVE" */
} __attribute__ ((packed)) RIFF_t;

typedef struct WAV_FMT {
    char Subchunk1ID[4];         	/* "fmt " */
    u_int32_t Subchunk1Size;      	/* 16 for PCM */
    u_int16_t AudioFormat;        	/* PCM = 1*/
    u_int16_t NumChannels;       	/* Mono = 1, Stereo = 2, etc. */
    u_int32_t SampleRate;        	/* 8000, 44100, etc. */
    u_int32_t ByteRate;           	/* = SampleRate * NumChannels * BitsPerSample/8 */
    u_int16_t BlockAlign;        	/* = NumChannels * BitsPerSample/8 */
    u_int16_t BitsPerSample;      	/* 8bits, 16bits, etc. */
} __attribute__ ((packed)) FMT_t;

typedef struct WAV_DATA {
    char Subchunk2ID[4];         	/* "data" */
    u_int32_t Subchunk2Size;      	/* data size */
} __attribute__ ((packed)) DATA_t;
字段 含义
ChunkID / Format 固定 "RIFF" / "WAVE"
Subchunk1ID 固定 "fmt "
AudioFormat 1 表示 PCM
NumChannels 声道数
SampleRate 采样率
ByteRate 每秒字节数 = 采样率 × 声道数 × 位深/8
BlockAlign 一帧字节数 = 声道数 × 位深/8(即 frame_bytes)
BitsPerSample 位深
Subchunk2ID / Subchunk2Size 数据块标识 "data" 与大小

解析思路:读 RIFF 头并校验 "RIFF"/"WAVE";读 fmt 子块并校验 "fmt ",同时拿到 SampleRateNumChannelsBlockAlign(用于配置 ALSA 参数与缓冲区);用 lseek 跳过 sizeof(RIFF_t)+8+Subchunk1Size,循环读 DATA_t"data" 块(不是则 lseek 跳过该块),找到后文件位置即 PCM 数据起点。

3.11 完整源码一:pcm_playback.c(播放 WAV)

源码路径:11、Linux C应用编程例程源码/28_alsa-lib/pcm_playback.c。功能:解析 WAV 并在 PCM 播放设备上播放(阻塞方式,一个周期一个周期写)。

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <alsa/asoundlib.h>

/************************************
 宏定义
 ************************************/
#define PCM_PLAYBACK_DEV    "hw:0,0"

/************************************
 WAV音频文件解析相关数据结构申明
 ************************************/
typedef struct WAV_RIFF {
    char ChunkID[4];         		/* "RIFF" */
    u_int32_t ChunkSize;        	/* 从下一个地址开始到文件末尾的总字节数 */
    char Format[4];             	/* "WAVE" */
} __attribute__ ((packed)) RIFF_t;

typedef struct WAV_FMT {
    char Subchunk1ID[4];         	/* "fmt " */
    u_int32_t Subchunk1Size;      	/* 16 for PCM */
    u_int16_t AudioFormat;        	/* PCM = 1*/
    u_int16_t NumChannels;       	/* Mono = 1, Stereo = 2, etc. */
    u_int32_t SampleRate;        	/* 8000, 44100, etc. */
    u_int32_t ByteRate;           	/* = SampleRate * NumChannels * BitsPerSample/8 */
    u_int16_t BlockAlign;        	/* = NumChannels * BitsPerSample/8 */
    u_int16_t BitsPerSample;      	/* 8bits, 16bits, etc. */
} __attribute__ ((packed)) FMT_t;
static FMT_t wav_fmt;

typedef struct WAV_DATA {
    char Subchunk2ID[4];         	/* "data" */
    u_int32_t Subchunk2Size;      	/* data size */
} __attribute__ ((packed)) DATA_t;

/************************************
  static静态全局变量定义
 ************************************/
static snd_pcm_t *pcm = NULL;            	//pcm句柄
static unsigned int buf_bytes;               	//应用程序缓冲区的大小(字节为单位)
static void *buf = NULL;                  	//指向应用程序缓冲区的指针
static int fd = -1;                         	//指向WAV音频文件的文件描述符
static snd_pcm_uframes_t period_size = 1024;  	//周期大小(单位: 帧)
static unsigned int periods = 16;             	//周期数(设备驱动层buffer的大小)

static int snd_pcm_init(void)
{
    snd_pcm_hw_params_t *hwparams = NULL;
    int ret;

    /* 打开PCM设备 */
    ret = snd_pcm_open(&pcm, PCM_PLAYBACK_DEV, SND_PCM_STREAM_PLAYBACK, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_open error: %s: %s\n",
                    PCM_PLAYBACK_DEV, snd_strerror(ret));
        return -1;
    }

    /* 实例化hwparams对象 */
    snd_pcm_hw_params_malloc(&hwparams);

    /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */
    ret = snd_pcm_hw_params_any(pcm, hwparams);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /**************
     设置参数
    ***************/
    /* 设置访问类型: 交错模式 */
    ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置数据格式: 有符号16位、小端模式 */
    ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置采样率 */
    ret = snd_pcm_hw_params_set_rate(pcm, hwparams, wav_fmt.SampleRate, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置声道数: 双声道 */
    ret = snd_pcm_hw_params_set_channels(pcm, hwparams, wav_fmt.NumChannels);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置周期大小: period_size */
    ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置周期数(驱动层buffer的大小): periods */
    ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 使配置生效 */
    ret = snd_pcm_hw_params(pcm, hwparams);
    snd_pcm_hw_params_free(hwparams);   //释放hwparams对象占用的内存
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
        goto err1;
    }

    buf_bytes = period_size * wav_fmt.BlockAlign; //变量赋值,一个周期的字节大小
    return 0;

err2:
    snd_pcm_hw_params_free(hwparams);   //释放内存
err1:
    snd_pcm_close(pcm); //关闭pcm设备
    return -1;
}

static int open_wav_file(const char *file)
{
    RIFF_t wav_riff;
    DATA_t wav_data;
    int ret;

    fd = open(file, O_RDONLY);
    if (0 > fd) {
        fprintf(stderr, "open error: %s: %s\n", file, strerror(errno));
        return -1;
    }

    /* 读取RIFF chunk */
    ret = read(fd, &wav_riff, sizeof(RIFF_t));
    if (sizeof(RIFF_t) != ret) {
        if (0 > ret)
            perror("read error");
        else
            fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    if (strncmp("RIFF", wav_riff.ChunkID, 4) ||//校验
        strncmp("WAVE", wav_riff.Format, 4)) {
        fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    /* 读取sub-chunk-fmt */
    ret = read(fd, &wav_fmt, sizeof(FMT_t));
    if (sizeof(FMT_t) != ret) {
        if (0 > ret)
            perror("read error");
        else
            fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    if (strncmp("fmt ", wav_fmt.Subchunk1ID, 4)) {//校验
        fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    /* 打印音频文件的信息 */
    printf("<<<<音频文件格式信息>>>>\n\n");
    printf("  file name:     %s\n", file);
    printf("  Subchunk1Size: %u\n", wav_fmt.Subchunk1Size);
    printf("  AudioFormat:   %u\n", wav_fmt.AudioFormat);
    printf("  NumChannels:   %u\n", wav_fmt.NumChannels);
    printf("  SampleRate:    %u\n", wav_fmt.SampleRate);
    printf("  ByteRate:      %u\n", wav_fmt.ByteRate);
    printf("  BlockAlign:    %u\n", wav_fmt.BlockAlign);
    printf("  BitsPerSample: %u\n\n", wav_fmt.BitsPerSample);

    /* sub-chunk-data */
    if (0 > lseek(fd, sizeof(RIFF_t) + 8 + wav_fmt.Subchunk1Size,
                SEEK_SET)) {
        perror("lseek error");
        close(fd);
        return -1;
    }

    while(sizeof(DATA_t) == read(fd, &wav_data, sizeof(DATA_t))) {

        /* 找到sub-chunk-data */
        if (!strncmp("data", wav_data.Subchunk2ID, 4))//校验
            return 0;

        if (0 > lseek(fd, wav_data.Subchunk2Size, SEEK_CUR)) {
            perror("lseek error");
            close(fd);
            return -1;
        }
    }

    fprintf(stderr, "check error: %s\n", file);
    return -1;
}

/************************************
  main主函数
 ************************************/
int main(int argc, char *argv[])
{
    int ret;

    if (2 != argc) {
        fprintf(stderr, "Usage: %s <audio_file>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* 打开WAV音频文件 */
    if (open_wav_file(argv[1]))
        exit(EXIT_FAILURE);

    /* 初始化PCM Playback设备 */
    if (snd_pcm_init())
        goto err1;

    /* 申请读缓冲区 */
    buf = malloc(buf_bytes);
    if (NULL == buf) {
        perror("malloc error");
        goto err2;
    }

    /* 播放 */
    for ( ; ; ) {

        memset(buf, 0x00, buf_bytes);   //buf清零
        ret = read(fd, buf, buf_bytes); //从音频文件中读取数据
        if (0 >= ret)   // 如果读取出错或文件读取完毕
            goto err3;

        ret = snd_pcm_writei(pcm, buf, period_size);
        if (0 > ret) {
            fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));
            goto err3;
        }
        else if (ret < period_size) {//实际写入的帧数小于指定的帧数
            //此时我们需要调整下音频文件的读位置
            //将读位置向后移动(往回移)(period_size-ret)*frame_bytes个字节
            //frame_bytes表示一帧的字节大小
            if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) {
                perror("lseek error");
                goto err3;
            }
        }
    }

err3:
    free(buf);     //释放内存
err2:
    snd_pcm_close(pcm); //关闭pcm设备
err1:
    close(fd);      //关闭打开的音频文件
    exit(EXIT_FAILURE);
}

关键逻辑buf_bytes = period_size × BlockAlign,即一个周期的字节数。每次 read() 读一个周期,再 snd_pcm_writei() 写一个周期。若实际写入帧数小于 period_size,说明有部分未写入,用 lseek 把文件读位置回退 (period_size - ret) × BlockAlign 字节,下次重发。阻塞方式下,环形缓冲区未满时 writei 立即写入返回;满了则阻塞,直到设备播完一个周期腾出空闲周期。

3.12 完整源码二:pcm_capture.c(录音)

源码路径:11、Linux C应用编程例程源码/28_alsa-lib/pcm_capture.c。功能:从 PCM 采集设备读取数据写入新建文件(16bit 双声道 44100Hz)。

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <alsa/asoundlib.h>

/************************************
 宏定义
 ************************************/
#define PCM_CAPTURE_DEV    "hw:0,0"

/************************************
  static静态全局变量定义
 ************************************/
static snd_pcm_t *pcm = NULL;            	//pcm句柄
static snd_pcm_uframes_t period_size = 1024; 	//周期大小(单位: 帧)
static unsigned int periods = 16;             	//周期数(buffer的大小)
static unsigned int rate = 44100;             	//采样率

static int snd_pcm_init(void)
{
    snd_pcm_hw_params_t *hwparams = NULL;
    int ret;

    /* 打开PCM设备 */
    ret = snd_pcm_open(&pcm, PCM_CAPTURE_DEV, SND_PCM_STREAM_CAPTURE, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_open error: %s: %s\n",
                    PCM_CAPTURE_DEV, snd_strerror(ret));
        return -1;
    }

    /* 实例化hwparams对象 */
    snd_pcm_hw_params_malloc(&hwparams);

    /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */
    ret = snd_pcm_hw_params_any(pcm, hwparams);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /**************
     设置参数
    ***************/
    /* 设置访问类型: 交错模式 */
    ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置数据格式: 有符号16位、小端模式 */
    ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置采样率 */
    ret = snd_pcm_hw_params_set_rate(pcm, hwparams, rate, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置声道数: 双声道 */
    ret = snd_pcm_hw_params_set_channels(pcm, hwparams, 2);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置周期大小: period_size */
    ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置周期数(buffer的大小): periods */
    ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 使配置生效 */
    ret = snd_pcm_hw_params(pcm, hwparams);
    snd_pcm_hw_params_free(hwparams);   //释放hwparams对象占用的内存
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
        goto err1;
    }

    return 0;

err2:
    snd_pcm_hw_params_free(hwparams);   //释放内存
err1:
    snd_pcm_close(pcm); //关闭pcm设备
    return -1;
}

/************************************
  main主函数
 ************************************/
int main(int argc, char *argv[])
{
    unsigned char *buf = NULL;
    unsigned int buf_bytes;
    int fd = -1;
    int ret;

    if (2 != argc) {
        fprintf(stderr, "Usage: %s <output_file>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* 初始化PCM Capture设备 */
    if (snd_pcm_init())
        exit(EXIT_FAILURE);

    /* 申请读缓冲区 */
    buf_bytes = period_size * 4;    //字节大小 = 周期大小*帧的字节大小 16位双声道
    buf = malloc(buf_bytes);
    if (NULL == buf) {
        perror("malloc error");
        goto err1;
    }

    /* 打开一个新建文件 */
    fd = open(argv[1], O_WRONLY | O_CREAT | O_EXCL);
    if (0 > fd) {
        fprintf(stderr, "open error: %s: %s\n", argv[1], strerror(errno));
        goto err2;
    }

    /* 录音 */
    for ( ; ; ) {

        //memset(buf, 0x00, buf_bytes);   //buf清零
        ret = snd_pcm_readi(pcm, buf, period_size);//读取PCM数据 一个周期
        if (0 > ret) {
            fprintf(stderr, "snd_pcm_readi error: %s\n", snd_strerror(ret));
            goto err3;
        }

        // snd_pcm_readi的返回值ret等于实际读取的帧数 * 4 转为字节数
        ret = write(fd, buf, ret * 4);    //将读取到的数据写入文件中
        if (0 >= ret)
            goto err3;
    }

err3:
    close(fd);  //关闭文件
err2:
    free(buf);     //释放内存
err1:
    snd_pcm_close(pcm); //关闭pcm设备
    exit(EXIT_FAILURE);
}

关键逻辑:录音参数固定为 16bit 双声道,一帧 4 字节,故 buf_bytes = period_size × 4snd_pcm_readi() 成功返回实际读取的帧数,写文件时乘以 4 转成字节数。阻塞方式下缓冲区无数据时 readi 阻塞,直到设备采集到一个周期后被唤醒。

3.13 完整源码三:混音器与播放控制(pcm_playback_mixer.c 节选)

源码路径:28_alsa-lib/pcm_playback_mixer.c。在异步方式播放的基础上加入混音器,实现按 w/s 调节音量、空格暂停、q 退出。其余(WAV 解析、异步回调、播放主循环)与前述一致,下面给出混音器初始化与音量调节的核心代码。

#define PCM_PLAYBACK_DEV    "hw:0,0"
#define MIXER_DEV           "hw:0"

static snd_mixer_t *mixer = NULL;                   	//混音器句柄
static snd_mixer_elem_t *playback_vol_elem = NULL;  	//播放<音量控制>元素

static int snd_mixer_init(void)
{
    snd_mixer_elem_t *elem = NULL;
    const char *elem_name;
    long minvol, maxvol;
    int ret;

    /* 打开混音器 */
    ret = snd_mixer_open(&mixer, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_mixer_open error: %s\n", snd_strerror(ret));
        return -1;
    }

    /* 关联一个声卡控制设备 */
    ret = snd_mixer_attach(mixer, MIXER_DEV);
    if (0 > ret) {
        fprintf(stderr, "snd_mixer_attach error: %s\n", snd_strerror(ret));
        goto err;
    }

    /* 注册混音器 */
    ret = snd_mixer_selem_register(mixer, NULL, NULL);
    if (0 > ret) {
        fprintf(stderr, "snd_mixer_selem_register error: %s\n", snd_strerror(ret));
        goto err;
    }

    /* 加载混音器 */
    ret = snd_mixer_load(mixer);
    if (0 > ret) {
        fprintf(stderr, "snd_mixer_load error: %s\n", snd_strerror(ret));
        goto err;
    }

    /* 遍历混音器中的元素 */
    elem = snd_mixer_first_elem(mixer);//找到第一个元素
    while (elem) {

        elem_name = snd_mixer_selem_get_name(elem);//获取元素的名称
        /* 针对开发板出厂系统:WM8960声卡设备 */
        if(!strcmp("Speaker", elem_name) ||  //耳机音量<对喇叭外音输出有效>
           !strcmp("Headphone", elem_name) ||//喇叭音量<对耳机输出有效>
           !strcmp("Playback", elem_name)) {//播放音量<总的音量控制,对喇叭和耳机输出都有效>
            if (snd_mixer_selem_has_playback_volume(elem)) {//是否是音量控制元素
                snd_mixer_selem_get_playback_volume_range(elem, &minvol, &maxvol);//获取音量可设置范围
                snd_mixer_selem_set_playback_volume_all(elem, (maxvol-minvol)*0.9 + minvol);//全部设置为90%

                if (!strcmp("Playback", elem_name))
                    playback_vol_elem = elem;
            }
        }

        elem = snd_mixer_elem_next(elem);
    }

    return 0;

err:
    snd_mixer_close(mixer);
    return -1;
}

/* 主循环中的音量调节(w 增加、s 减小) */
case 'w':           //音量增加
    if (playback_vol_elem) {
        snd_mixer_selem_get_playback_volume(playback_vol_elem,
                SND_MIXER_SCHN_FRONT_LEFT, &vol);
        vol++;
        snd_mixer_selem_set_playback_volume_all(playback_vol_elem, vol);
    }
    break;
case 's':           //音量降低
    if (playback_vol_elem) {
        snd_mixer_selem_get_playback_volume(playback_vol_elem,
                SND_MIXER_SCHN_FRONT_LEFT, &vol);
        vol--;
        snd_mixer_selem_set_playback_volume_all(playback_vol_elem, vol);
    }
    break;

暂停/恢复使用 snd_pcm_state() + snd_pcm_pause()

case ' ':   //空格暂停/恢复
    switch (snd_pcm_state(pcm)) {
    case SND_PCM_STATE_PAUSED:
        snd_pcm_pause(pcm, 0);   //恢复运行
        break;
    case SND_PCM_STATE_RUNNING:
        snd_pcm_pause(pcm, 1);   //暂停
        break;
    }
    break;

注意:并非所有音频硬件都支持暂停,可用 snd_pcm_hw_params_can_pause() 判断。

3.14 读写方式变体:异步 I/O 与 poll

28_alsa-lib 目录另提供了 pcm_playback_async.cpcm_capture_async.cpcm_playback_poll.cpcm_capture_poll.c,核心区别只在"何时写/读"。

异步方式:注册回调,驱动可写时自动触发:

ret = snd_async_add_pcm_handler(&async_handler, pcm, snd_playback_async_callback, NULL);

static void snd_playback_async_callback(snd_async_handler_t *handler)
{
    snd_pcm_t *handle = snd_async_handler_get_pcm(handler);
    snd_pcm_sframes_t avail = snd_pcm_avail_update(handle);
    while (avail >= period_size) {
        /* read(fd,...) + snd_pcm_writei(handle, buf, period_size) */
        avail = snd_pcm_avail_update(handle);
    }
}

poll 方式:通过 snd_pcm_poll_descriptors_count() / snd_pcm_poll_descriptors() 拿到 PCM 句柄的轮询描述符,用 poll() 等待 POLLIN/POLLOUT,再用 snd_pcm_poll_descriptors_revents() 取事件:

count = snd_pcm_poll_descriptors_count(pcm);
pfds = calloc(count, sizeof(struct pollfd));
snd_pcm_poll_descriptors(pcm, pfds, count);

for (;;) {
    poll(pfds, count, -1);
    snd_pcm_poll_descriptors_revents(pcm, pfds, count, &revents);
    if (revents & POLLERR) goto err;
    if (revents & POLLOUT) {          /* 录音侧为 POLLIN */
        avail = snd_pcm_avail_update(pcm);
        while (avail >= period_size) {
            /* read(fd,...) + snd_pcm_writei(...) 或 snd_pcm_readi + write(fd) */
            avail = snd_pcm_avail_update(pcm);
        }
    }
}

3.15 交叉编译与实验步骤

source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi

# 播放程序(链接 alsa-lib)
${CC} -o pcm_playback pcm_playback.c -lasound
# 录音程序
${CC} -o pcm_capture pcm_capture.c -lasound
# 带混音器的播放程序
${CC} -o pcm_playback_mixer pcm_playback_mixer.c -lasound

实验步骤:

  1. 拷贝可执行文件与一个 test.wav 到开发板家目录(scp)。
  2. 播放:./pcm_playback test.wav,终端打印 WAV 格式信息,喇叭/耳机出声。
  3. 录音:./pcm_capture out.pcmCtrl+C 结束),得到裸 PCM 数据(16bit 双声道 44100)。
  4. 可先用 ./mic_in_config.sh 中的 amixer 命令配置录音/播放音量与输入通道。
  5. arecord -f cd -d 10 test.wav 对比验证;aplay test.wav 播放。

3.16 ALSA 插件

user 空间的 alsa-lib 使用逻辑设备名而非设备节点。snd_pcm_open() 会加载 /usr/share/alsa/alsa.conf 并解析,其中又会加载 /etc/asound.conf~/.asoundrc。每个 pcm.name { ... } 定义了一个插件,type 字段指定插件类型:

插件 作用
hw 直接与 ALSA 内核驱动通信,无任何转换的原始通信
plughw 提供采样率转换等软件特性(硬件不支持时可用)
dmix 混音,把多个应用程序的音频数据混合
softvol 软件音量

"hw:i,j" 就是名为 hw 的插件,i 是声卡号、j 是设备号。


跨平台对比

主题 I.MX6ULL(本教程) STM32(裸机/HAL) RK3568(Linux)
摄像头 V4L2 /dev/videoX,ov5640/ov2640/ov7725/UVC,ioctl + mmap 队列 DCMI + DMA,直接操作寄存器/LL 库 V4L2 + Media Controller/V4L2 subdev,Rockchip ISP
串口 termios API,/dev/ttymxcX USART,HAL_UART_Transmit/Receive + 中断/DMA termios API,/dev/ttySX
音频 ALSA + alsa-lib,WM8960/ES8388 I2S + 外部 codec,寄存器配置 ALSA + alsa-lib,通常走 GStreamer/ASoC
编程层 用户态,统一设备框架 裸机/RTOS,直接寄存器 用户态,统一设备框架
关键差异 需掌握框架接口与设备节点 需手工配置时钟/引脚/DMA 同 V4L2/ALSA,但多平面、复杂管线常见

面试精选(5 题)

Q1. V4L2 的 MMAP 采集流程是怎样的?为什么需要"入队/出队"?

答案要点:打开设备 → QUERYCAP → ENUM_FMT/S_FMT → REQBUFS → QUERYBUF+mmap → QBUF → STREAMON → 循环 DQBUF/处理/QBUF → STREAMOFF。入队/出队是为了在应用与内核之间高效、安全地传递帧缓冲所有权。

详细解答:内核维护一个帧缓冲队列。应用 VIDIOC_QBUF 把空闲缓冲交给驱动,驱动填充一帧后缓冲变为"满";应用 VIDIOC_DQBUF 取走满缓冲(出队),处理完再 VIDIOC_QBUF 归还(入队)。这样避免了每帧内存拷贝——mmap 后应用直接读内核缓冲,即零拷贝。count 一般取 3~4,太多浪费内存、太少可能丢帧。

追问

  1. VIDIOC_DQBUF 在缓冲为空时会怎样?(阻塞直到驱动填入一帧;非阻塞打开则立即返回 EAGAIN
  2. V4L2_MEMORY_MMAPV4L2_MEMORY_USERPTR 区别?(MMAP 由内核分配、应用映射;USERPTR 由应用提供缓冲指针)

Q2. VIDIOC_S_FMT 设置后为什么要回读 struct v4l2_format

答案要点:驱动可能按硬件能力修改参数,实际值不一定等于请求值,回读才能拿到真正生效的格式。

详细解答:例如请求 800×480 RGB565,但摄像头不支持 800×480,驱动可能改成 640×480;不支持 RGB565 则保留 YUYV。典型处理:

fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 800; fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
ioctl(fd, VIDIOC_S_FMT, &fmt);
if (V4L2_PIX_FMT_RGB565 != fmt.fmt.pix.pixelformat) { /* 不支持,退化为 YUYV 等 */ }

注意 VIDIOC_TRY_FMT 只试不改,可在正式设置前探测。

追问

  1. 为什么 USB 摄像头常不支持 RGB565?(UVC 标准输出以 YUYV/MJPEG 为主)
  2. fmt.fmt.pix.sizeimage 有什么用?(一帧图像数据的总字节数,用于分配缓冲/校验)

Q3. termios 的 c_cflagc_iflagc_lflag 各控制什么?如何配置 115200-8-N-1?

答案要点c_iflag 输入处理、c_oflag 输出处理、c_cflag 硬件特性(波特率/数据位/校验/停止位)、c_lflag 本地模式(回显/规范模式等)。

详细解答:配置 115200-8-N-1 的原始模式:

struct termios new_cfg = {0};
cfmakeraw(&new_cfg);              // 原始模式
new_cfg.c_cflag |= CREAD;         // 接收使能
cfsetspeed(&new_cfg, B115200);    // 波特率
new_cfg.c_cflag &= ~CSIZE;
new_cfg.c_cflag |= CS8;           // 8 数据位
new_cfg.c_cflag &= ~PARENB;       // 无校验
new_cfg.c_iflag &= ~INPCK;
new_cfg.c_cflag &= ~CSTOPB;       // 1 停止位
tcsetattr(fd, TCSANOW, &new_cfg);

原始模式会清除 ICANON/ECHO/ISIG 等本地标志,并关闭 OPOST 输出处理——这是二进制通信必需的。

追问

  1. VMIN=VTIME=0read() 行为?(立即返回:有数据返回字节数,无数据返回 0)
  2. 为什么打开串口要加 O_NOCTTY?(避免该终端成为本进程的控制终端)

Q4. ALSA 中 frame、period、buffer 的关系是什么?为什么 buffer 要分成多个 period?

答案要点:frame = 样本长度 × 声道数;period 是设备读写的单位(若干帧);buffer 由若干 period 组成。拆分 period 是为了平衡延迟与中断开销。

详细解答:以 16bit 双声道为例,一帧 = 16/8 × 2 = 4 字节。period_size=1024 帧即 4096 字节;periods=16 则 buffer = 16×1024 帧。DMA 每搬完一个 period 触发一次中断。整块搬运延迟大,period 太小则中断频繁、CPU 开销高。buf_bytes = period_size × BlockAlign 即每次 snd_pcm_writei/readi 的数据量。

追问

  1. overrun 和 underrun 分别发生在播放还是录音?(录音读太慢 → overrun;播放写太慢 → underrun,统称 XRUN)
  2. 发生 XRUN 后如何恢复?(snd_pcm_prepare() 回到 PREPARED,或 snd_pcm_drop() 停止)

Q5. snd_pcm_readi/writei 的返回值含义与阻塞特性?出错如何处理?

答案要点:成功返回实际读/写的帧数(可能小于请求值,仅信号或 XRUN 时),失败返回负错误码;阻塞与否由 snd_pcm_open 的 mode 决定。

详细解答:阻塞方式下,录音无数据可读、播放缓冲满时调用会阻塞。错误码:-EBADFD(状态不对,需 PREPARED/RUNNING)、-EPIPE(XRUN,用 prepare 恢复)、-ESTRPIPE(硬件挂起,用 resumeprepare)。写入帧数小于请求值时,需回退文件读位置重发,见 pcm_playback.clseek(fd, (ret-period_size)*BlockAlign, SEEK_CUR)

追问

  1. 非交错模式该用什么函数?(snd_pcm_readn / snd_pcm_writen
  2. 为什么 snd_pcm_hw_params() 后设备就是 PREPARED?(其内部自动调用 snd_pcm_prepare()

内容来源:《I.MX6U嵌入式Linux C应用编程指南V1.6》第二十五章 V4L2摄像头应用编程、第二十六章 串口应用编程、第二十八章 音频应用编程;配套例程源码 25_v4l2_camera/v4l2_camera.c26_uart/uart_test.c28_alsa-lib/