title: 摄像头、串口与音频应用编程 tags: [
嵌入式Linux,
Linux应用编程,
V4L2,
摄像头,
视频采集,
串口,
UART,
termios,
ALSA,
alsa-lib,
PCM,
WAV,
音频,
IMX6ULL,
] created: 2026-09-18 updated: 2026-09-18
💡 关联知识:[[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 是 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 格式。
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 结束采集"]
所有指令定义在头文件 <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 |
枚举设备支持的视频采集帧率 |
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 方式 |
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_CAPTURE 时 discrete 生效。帧率 = 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) |
多平面视频采集 |
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;
};
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 设置帧率。
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 时内核申请"数量 × 单个大小"的连续内存,每个帧缓冲对应其中一段。
V4L2 读取数据有两种方式:read 方式(capabilities 含 V4L2_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,设置 count、type、memory=V4L2_MEMORY_MMAP;VIDIOC_QUERYBUF 拿到 length/m.offset,再 mmap() 映射到用户空间;VIDIOC_QBUF 放入内核队列;开启后用 VIDIOC_DQBUF 循环取帧,处理完再 VIDIOC_QBUF。源码路径:
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,把每个格式的 pixelformat 与 description 存入 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. 设置交叉编译工具环境(路径按实际安装调整)
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
实验步骤:
scp 把 testApp 拷到开发板家目录。./testApp /dev/video0(USB 摄像头可能是 /dev/video1,可用 ls /dev/video* 确认)。Ctrl+C 结束程序。⚠️ 来源说明:本节不属于《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;
}
}
串口全称串行接口,数据一个接一个按顺序传输,两条线即可双向通信(一发一收)。串口通信距离远、速度相对低,是常用的工业接口。在嵌入式 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命令查看系统当前连接了哪些终端。
串口应用编程可以简单理解为对终端进行"配置 + 读写"。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 输出速率 */
};
对这些成员不要直接整体初始化,而应用"按位与/或"添加或清除标志。另外,很多标志并非对所有终端都有效——本地键盘显示器没有波特率、数据位这些硬件概念。
控制输入数据(驱动从串口/键盘收到的字符)在交给应用前的处理方式。
| 标志 | 含义 |
|---|---|
IGNBRK |
忽略输入终止条件 |
BRKINT |
检测到输入终止条件时发送 SIGINT 信号 |
IGNPAR |
忽略帧错误和奇偶校验错误 |
PARMRK |
对奇偶校验错误做出标记 |
INPCK |
对接收到的数据执行奇偶校验 |
ISTRIP |
将所有接收数据裁剪为 7 比特位(去掉第八位) |
INLCR |
将接收到的 NL(换行符)转换为 CR(回车符) |
IGNCR |
忽略接收到的 CR(回车符) |
ICRNL |
将接收到的 CR(回车符)转换为 NL(换行符) |
IUCLC |
将接收到的大写字符映射为小写字符 |
IXON |
启动输出软件流控 |
IXOFF |
启动输入软件流控 |
控制输出字符在传递到串口/屏幕前的处理方式。
| 标志 | 含义 |
|---|---|
OPOST |
启用输出处理功能;不设置该标志则其它标志都被忽略 |
OLCUC |
将输出字符中的大写字符转换成小写字符 |
ONLCR |
将输出中的换行符 NL(\n)转换成回车符 CR(\r) |
OCRNL |
将输出中的回车符 CR(\r)转换成换行符 NL(\n) |
ONOCR |
在第 0 列不输出回车符 CR |
ONLRET |
不输出回车符 |
OFILL |
发送填充字符以提供延时 |
OFDEL |
若设置该标志,填充字符为 DEL 字符,否则为 NULL 字符 |
控制终端硬件特性,对串口最重要:波特率、数据位、校验位、停止位等。
| 标志 | 含义 |
|---|---|
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() 函数获取与设置。
控制终端的本地数据处理和工作模式。
| 标志 | 含义 |
|---|---|
ISIG |
收到信号字符(INTR、QUIT 等)则产生相应信号 |
ICANON |
启用规范模式 |
ECHO |
启用输入字符的本地回显功能 |
ECHOE |
若设置 ICANON,则允许退格操作 |
ECHOK |
若设置 ICANON,则 KILL 字符会删除当前行 |
ECHONL |
若设置 ICANON,则允许回显换行符 |
ECHOCTL |
若设置 ECHO,控制字符会显示成 ^X |
ECHOPRT |
若设置 ICANON 和 IECHO,删除字符和被删除字符都显示 |
ECHOKE |
若设置 ICANON,允许回显 ECHOE/ECHOPRT 中设定的 KILL 字符 |
NOFLSH |
通常收到 INTR/QUIT/SUSP 会清空输入输出队列;设置该标志则不清空 |
TOSTOP |
后台进程写控制终端时,系统向该进程组发送 SIGTTOU |
IEXTEN |
启用输入处理功能 |
| 宏 | 对应键 | 作用 |
|---|---|---|
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 |
— | 非规范模式下,读取每个字符之间的超时(单位:十分之一秒) |
终端有规范模式、非规范模式、原始模式三种,通过 c_lflag 的 ICANON 标志区分,默认是规范模式。
read() 读不到任何字符;除 EOF 外的行结束符与普通字符一样被读入缓冲区;支持行编辑,一次 read() 最多读一行。MIN(c_cc[VMIN])与 TIME(c_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 传输),而是与其他设备或传感器进行二进制数据通信时,数据不应做任何特殊处理,必须用原始模式。
| 函数 | 原型 | 作用 |
|---|---|---|
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);
源码路径:
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_cfg 后 cfmakeraw();使能接收 CREAD;cfsetspeed() 设波特率;~(CSIZE) 清零后按参数置 CS5~CS8;校验同时操作 c_cflag(PARENB/PARODD)与 c_iflag(INPCK);停止位用 CSTOPB;VMIN=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/--help;type=read 时初始化异步 I/O 后 sleep 等待信号;type=write 时每秒写 8 字节 0x11..0x88。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
实验步骤:
/dev/ttymxc0)和 UART3(RS232/RS485,/dev/ttymxc2)。板上 485 和 232 共用 UART3,不能同时使用,由 JP1 端子选择。./testApp --help。./testApp --dev=/dev/ttymxc2 --type=read,在 PC 串口调试助手(如 XCOM)发送 8 字节 [0x11 0x22 ... 0x88],开发板打印收到的数据。./testApp --dev=/dev/ttymxc2 --type=write,PC 端收到开发板每秒发来的 8 字节。Ctrl+C 结束。⚠️ 来源说明:本节不属于《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 是 Advanced Linux Sound Architecture(高级 Linux 声音体系)的缩写,是 Linux 下的主流音频体系架构,提供音频和 MIDI 支持,替代了旧的 OSS。ALSA 本身是内核中的音频驱动框架,设计复杂、采用分离分层思想;但作为应用编程,我们无需研究它。
/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 板没有板载音频编解码芯片,无法测试本章例程。
/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) |
| 概念 | 说明 |
|---|---|
| 样本长度(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 效率越低。因此在延迟可接受的前提下,周期尽量大一些,具体依应用场合而定。
播放时:应用向 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 读一个周期,读完后该周期变空闲,等待设备再次写入。
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));
用 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。
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指应用程序的缓冲区,不要与驱动层环形缓冲区混淆。
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 间切换。
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() |
混音器用于配置音量、声道、增益等。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_t:SND_MIXER_SCHN_FRONT_LEFT(0,左前)、SND_MIXER_SCHN_FRONT_RIGHT(右前)、SND_MIXER_SCHN_REAR_LEFT、SND_MIXER_SCHN_REAR_RIGHT、SND_MIXER_SCHN_FRONT_CENTER、SND_MIXER_SCHN_WOOFER、SND_MIXER_SCHN_MONO(= 左前)。
出厂系统已移植 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完成。
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 ",同时拿到 SampleRate、NumChannels、BlockAlign(用于配置 ALSA 参数与缓冲区);用 lseek 跳过 sizeof(RIFF_t)+8+Subchunk1Size,循环读 DATA_t 找 "data" 块(不是则 lseek 跳过该块),找到后文件位置即 PCM 数据起点。
源码路径:
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 立即写入返回;满了则阻塞,直到设备播完一个周期腾出空闲周期。
源码路径:
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 × 4。snd_pcm_readi() 成功返回实际读取的帧数,写文件时乘以 4 转成字节数。阻塞方式下缓冲区无数据时 readi 阻塞,直到设备采集到一个周期后被唤醒。
源码路径:
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()判断。
28_alsa-lib 目录另提供了 pcm_playback_async.c、pcm_capture_async.c、pcm_playback_poll.c、pcm_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);
}
}
}
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
实验步骤:
test.wav 到开发板家目录(scp)。./pcm_playback test.wav,终端打印 WAV 格式信息,喇叭/耳机出声。./pcm_capture out.pcm(Ctrl+C 结束),得到裸 PCM 数据(16bit 双声道 44100)。./mic_in_config.sh 中的 amixer 命令配置录音/播放音量与输入通道。arecord -f cd -d 10 test.wav 对比验证;aplay test.wav 播放。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,但多平面、复杂管线常见 |
答案要点:打开设备 → QUERYCAP → ENUM_FMT/S_FMT → REQBUFS → QUERYBUF+mmap → QBUF → STREAMON → 循环 DQBUF/处理/QBUF → STREAMOFF。入队/出队是为了在应用与内核之间高效、安全地传递帧缓冲所有权。
详细解答:内核维护一个帧缓冲队列。应用 VIDIOC_QBUF 把空闲缓冲交给驱动,驱动填充一帧后缓冲变为"满";应用 VIDIOC_DQBUF 取走满缓冲(出队),处理完再 VIDIOC_QBUF 归还(入队)。这样避免了每帧内存拷贝——mmap 后应用直接读内核缓冲,即零拷贝。count 一般取 3~4,太多浪费内存、太少可能丢帧。
追问
VIDIOC_DQBUF 在缓冲为空时会怎样?(阻塞直到驱动填入一帧;非阻塞打开则立即返回 EAGAIN)V4L2_MEMORY_MMAP 与 V4L2_MEMORY_USERPTR 区别?(MMAP 由内核分配、应用映射;USERPTR 由应用提供缓冲指针)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 只试不改,可在正式设置前探测。
追问
fmt.fmt.pix.sizeimage 有什么用?(一帧图像数据的总字节数,用于分配缓冲/校验)c_cflag、c_iflag、c_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 输出处理——这是二进制通信必需的。
追问
VMIN=VTIME=0 时 read() 行为?(立即返回:有数据返回字节数,无数据返回 0)O_NOCTTY?(避免该终端成为本进程的控制终端)答案要点: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 的数据量。
追问
snd_pcm_prepare() 回到 PREPARED,或 snd_pcm_drop() 停止)snd_pcm_readi/writei 的返回值含义与阻塞特性?出错如何处理?答案要点:成功返回实际读/写的帧数(可能小于请求值,仅信号或 XRUN 时),失败返回负错误码;阻塞与否由 snd_pcm_open 的 mode 决定。
详细解答:阻塞方式下,录音无数据可读、播放缓冲满时调用会阻塞。错误码:-EBADFD(状态不对,需 PREPARED/RUNNING)、-EPIPE(XRUN,用 prepare 恢复)、-ESTRPIPE(硬件挂起,用 resume 或 prepare)。写入帧数小于请求值时,需回退文件读位置重发,见 pcm_playback.c 的 lseek(fd, (ret-period_size)*BlockAlign, SEEK_CUR)。
追问
snd_pcm_readn / snd_pcm_writen)snd_pcm_hw_params() 后设备就是 PREPARED?(其内部自动调用 snd_pcm_prepare())内容来源:《I.MX6U嵌入式Linux C应用编程指南V1.6》第二十五章 V4L2摄像头应用编程、第二十六章 串口应用编程、第二十八章 音频应用编程;配套例程源码 25_v4l2_camera/v4l2_camera.c、26_uart/uart_test.c、28_alsa-lib/。