|
|
@@ -0,0 +1,2328 @@
|
|
|
+---
|
|
|
+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 参数)请求不同操作:
|
|
|
+
|
|
|
+```mermaid
|
|
|
+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
|
|
|
+
|
|
|
+```c
|
|
|
+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
|
|
|
+
|
|
|
+```c
|
|
|
+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) | 多平面视频采集 |
|
|
|
+
|
|
|
+### 1.4.3 struct v4l2_format / v4l2_pix_format
|
|
|
+
|
|
|
+```c
|
|
|
+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
|
|
|
+
|
|
|
+```c
|
|
|
+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
|
|
|
+
|
|
|
+```c
|
|
|
+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 方式**(`capabilities` 含 `V4L2_CAP_READWRITE`)和 **streaming 方式**(含 `V4L2_CAP_STREAMING`)。绝大多数设备支持 streaming I/O:内核维护一个帧缓冲队列,驱动不断把采集数据填入队列中的帧缓冲;应用取走一帧叫**出队**,处理完再放回队列叫**入队**。
|
|
|
+
|
|
|
+```mermaid
|
|
|
+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`。
|
|
|
+- 帧缓冲数量不要太多,嵌入式系统内存吃紧;太少又可能丢帧,例程取 3 个。
|
|
|
+
|
|
|
+## 1.6 完整源码:v4l2_camera.c
|
|
|
+
|
|
|
+> 源码路径:`11、Linux C应用编程例程源码/25_v4l2_camera/v4l2_camera.c`。功能:在 LCD 上实时显示摄像头采集图像(要求摄像头支持 RGB565)。
|
|
|
+
|
|
|
+```c
|
|
|
+#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.7 交叉编译与实验步骤
|
|
|
+
|
|
|
+```bash
|
|
|
+# 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. 用 `scp` 把 `testApp` 拷到开发板家目录。
|
|
|
+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 的常用整数公式:
|
|
|
+
|
|
|
+```c
|
|
|
+/* 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`:
|
|
|
+
|
|
|
+```c
|
|
|
+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` | 若设置 `ICANON` 和 `IECHO`,删除字符和被删除字符都显示 |
|
|
|
+| `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_lflag` 的 `ICANON` 标志区分,默认是规范模式。
|
|
|
+
|
|
|
+- **规范模式(canonical)**:所有输入基于行处理。用户输入行结束符(回车、EOF 等)之前,`read()` 读不到任何字符;除 EOF 外的行结束符与普通字符一样被读入缓冲区;支持行编辑,一次 `read()` 最多读一行。
|
|
|
+- **非规范模式(non-canonical)**:所有输入即时有效,不需要行结束符,不可行编辑。由 `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()` 设置,其内部等价于:
|
|
|
+
|
|
|
+```c
|
|
|
+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` | 所有已接收但未读取的输入在配置生效前被丢弃 |
|
|
|
+
|
|
|
+**配置步骤**(以原始模式为例):
|
|
|
+
|
|
|
+```c
|
|
|
+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`,告知系统该设备不会成为进程的控制终端:
|
|
|
+
|
|
|
+```c
|
|
|
+fd = open("/dev/ttymxc2", O_RDWR | O_NOCTTY);
|
|
|
+```
|
|
|
+
|
|
|
+## 2.5 完整源码:uart_test.c
|
|
|
+
|
|
|
+> 源码路径:`11、Linux C应用编程例程源码/26_uart/uart_test.c`。功能:串口在原始模式下收发数据,支持命令行指定设备、波特率、数据位、校验、停止位与读写类型;读操作使用**异步 I/O(信号驱动)**。
|
|
|
+
|
|
|
+```c
|
|
|
+#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`。
|
|
|
+
|
|
|
+## 2.6 交叉编译与实验步骤
|
|
|
+
|
|
|
+```bash
|
|
|
+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、原始模式**,即可按行读取并解析:
|
|
|
+
|
|
|
+```c
|
|
|
+/* 假设已用 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**,应用程序调用即可控制底层音频硬件(播放、录音)。
|
|
|
+
|
|
|
+```mermaid
|
|
|
+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 末尾都会回到起始位置,构成**环形缓冲区**。
|
|
|
+
|
|
|
+```mermaid
|
|
|
+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 设备
|
|
|
+
|
|
|
+```c
|
|
|
+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` 为非阻塞 |
|
|
|
+
|
|
|
+```c
|
|
|
+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` 描述硬件配置:
|
|
|
+
|
|
|
+```c
|
|
|
+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 读写数据与阻塞特性
|
|
|
+
|
|
|
+```c
|
|
|
+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` | 硬件已断开 |
|
|
|
+
|
|
|
+相关函数:
|
|
|
+
|
|
|
+```c
|
|
|
+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` 描述。初始化流程:
|
|
|
+
|
|
|
+```c
|
|
|
+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`(= 左前)。
|
|
|
+
|
|
|
+## 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))` 保证无填充字节):
|
|
|
+
|
|
|
+```c
|
|
|
+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 数据起点。
|
|
|
+
|
|
|
+## 3.11 完整源码一:pcm_playback.c(播放 WAV)
|
|
|
+
|
|
|
+> 源码路径:`11、Linux C应用编程例程源码/28_alsa-lib/pcm_playback.c`。功能:解析 WAV 并在 PCM 播放设备上播放(阻塞方式,一个周期一个周期写)。
|
|
|
+
|
|
|
+```c
|
|
|
+#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)。
|
|
|
+
|
|
|
+```c
|
|
|
+#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` 阻塞,直到设备采集到一个周期后被唤醒。
|
|
|
+
|
|
|
+## 3.13 完整源码三:混音器与播放控制(pcm_playback_mixer.c 节选)
|
|
|
+
|
|
|
+> 源码路径:`28_alsa-lib/pcm_playback_mixer.c`。在异步方式播放的基础上加入混音器,实现按 `w`/`s` 调节音量、空格暂停、`q` 退出。其余(WAV 解析、异步回调、播放主循环)与前述一致,下面给出混音器初始化与音量调节的核心代码。
|
|
|
+
|
|
|
+```c
|
|
|
+#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()`:
|
|
|
+
|
|
|
+```c
|
|
|
+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.c`、`pcm_capture_async.c`、`pcm_playback_poll.c`、`pcm_capture_poll.c`,核心区别只在"何时写/读"。
|
|
|
+
|
|
|
+**异步方式**:注册回调,驱动可写时自动触发:
|
|
|
+
|
|
|
+```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()` 取事件:
|
|
|
+
|
|
|
+```c
|
|
|
+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 交叉编译与实验步骤
|
|
|
+
|
|
|
+```bash
|
|
|
+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.pcm`(`Ctrl+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_MMAP` 与 `V4L2_MEMORY_USERPTR` 区别?(MMAP 由内核分配、应用映射;USERPTR 由应用提供缓冲指针)
|
|
|
+
|
|
|
+### Q2. `VIDIOC_S_FMT` 设置后为什么要回读 `struct v4l2_format`?
|
|
|
+
|
|
|
+**答案要点**:驱动可能按硬件能力修改参数,实际值不一定等于请求值,回读才能拿到真正生效的格式。
|
|
|
+
|
|
|
+**详细解答**:例如请求 800×480 RGB565,但摄像头不支持 800×480,驱动可能改成 640×480;不支持 RGB565 则保留 YUYV。典型处理:
|
|
|
+
|
|
|
+```c
|
|
|
+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_cflag`、`c_iflag`、`c_lflag` 各控制什么?如何配置 115200-8-N-1?
|
|
|
+
|
|
|
+**答案要点**:`c_iflag` 输入处理、`c_oflag` 输出处理、`c_cflag` 硬件特性(波特率/数据位/校验/停止位)、`c_lflag` 本地模式(回显/规范模式等)。
|
|
|
+
|
|
|
+**详细解答**:配置 115200-8-N-1 的原始模式:
|
|
|
+
|
|
|
+```c
|
|
|
+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=0` 时 `read()` 行为?(立即返回:有数据返回字节数,无数据返回 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`(硬件挂起,用 `resume` 或 `prepare`)。写入帧数小于请求值时,需回退文件读位置重发,见 `pcm_playback.c` 的 `lseek(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.c`、`26_uart/uart_test.c`、`28_alsa-lib/`。
|