正点原子linux应用编程——提高篇3

PWM应用编程

应用层操控PWM

与LED设备一样,PWM同样也是通过sysfs方式进行操控。

进入到/sys/class/pwm目录下,会有形如pwmchipX的文件,就是对应的PWM控制器。出厂的话就是pwmchip0和pwmchip4,pwmchip0对应TIM4,pwmchip4对应TIM1。

进入pwmchip4目录,重点是export、npwm以及unexport三个属性文件:

  • npwm:制度文件,读取该文件可获得PWM控制器下共有几路PWM输出;
  • export:同样,需要通过该文件,在使用PWM前将其导出,导出的编号就是从0到3对应的4个通道,导出成功,就会在pwmchipX下生成pwmY的目录;
  • unexport:将导出的PWM通道删除。

控制PWM,就是在export导出之后,进入生成的pwmY目录操作文件,重点关注的是duty_cycle、enable、period以及polarity四个属性文件:

  • enable:可读写,写入“0”表示禁用,写入“1”使能;
  • polarity:可读写,设置极性,可输入“normal”或“inversed”;
  • period:可读写,配置PWM周期,以ns为单位,通过输入字符串数字来配置;
  • duty_cycle:可读写,配置PWM占空比,以ns为单位,通过输入字符串数字来配置。

编写应用程序

主要就是编写pwm_config函数,里面通过open打开PWM设备文件,然后就是通过write写入参数就可以了。

main函数中,通过access来导出pwm2目录(access(pwm_path,F_OK),这样的话目录不存在就会导出),然后open卡开export文件检查一下,存在就是顺利导出了;之后write把TIM1_CH3写入export文件,以此来完成该通道的导出操作。

以上导出完成后,调用pwm_config,填入文件名以及对应的参数来完成PWM的相关配置。

V4L2摄像头应用编程

MP157开发板配套支持正点原子的ov5640(500W像素)摄像头,在开发板出厂系统上,可以使用该摄像头;当然,除此之外还可以使用USB摄像头,直接将USB摄像头插入到开发板上的USB接口即可!

V4L2简介

V4L2是Video for linux two的简称,是Linux内核中视频类设备的一套驱动框架,为视频类设备驱动开发和应用层提供了一套统一的接口规范。

使用V4L2设备驱动框架注册的设备会在Linux系统/dev/目录下生成对应的设备节点文件,设备节点的名称通常为videoX(X表示一个数字编号,0、1、2、3……),每一个videoX设备文件就代表一个视频类设备。

V4L2摄像头应用程序

对于摄像头设备来说,其编程模式如下所示:

  1. 首先是打开摄像头设备;
  2. 查询设备的属性或功能;
  3. 设置设备的参数,譬如像素格式、帧大小、帧率;
  4. 申请帧缓冲、内存映射;
  5. 帧缓冲入队;
  6. 开启视频采集;
  7. 帧缓冲出队、对采集的数据进行处理;
  8. 处理完后,再次将帧缓冲入队,往复;
  9. 结束采集。

摄像头操作的流程如下:

正点原子linux应用编程——提高篇3_第1张图片

从上图可看出,几乎对摄像头的所有操作都是通过ioctl()来完成,搭配不同的V4L2指令(request参数)请求不同的操作,这些指令定义在头文件linux/videodev2.h 中,在摄像头应用程序代码中,需要包含头文件linux/videodev2.h。

针对视频采集类设备,常用指令如下:

正点原子linux应用编程——提高篇3_第2张图片

打开摄像头

视频类设备对应的设备节点为/dev/videoX,X为数字编号,通常从0开始;摄像头应用编程的第一步便是打开设备,调用open打开,得到文件描述符fd。需要用O_RDWR制定读权限和写权限。

查询设备属性/能力/功能

查询设备的属性,使用的指令为VIDIOC_QUERYCAP,如下所示:

ioctl(int fd, VIDIOC_QUERYCAP, struct v4l2_capability *cap);

此时通过ioctl()将获取到一个struct v4l2_capability类型数据,struct v4l2_capability数据结构描述了设备的一些属性。该结构体中重点关注capabilities字段,其中对于摄像头设备,必须要包含V4L2_CAP_VIDEO_CAPTURE表示支持视频采集功能

设置帧格式、帧率

使用VIDIOC_ENUM_FMT指令,可获取摄像头支持的像素格式

ioctl(int fd, VIDIOC_ENUM_FMT, struct v4l2_fmtdesc *fmtdesc);

需要传入一个struct v4l2_fmtdesc *指针,ioctl()会将获取到的数据写入到v4l2_fmtdesc指针所指向的对象中。struct v4l2_fmtdesc 结构体描述了像素格式相关的信息:

  • index:编号,枚举前设置为0,每调用一次就会加1;
  • description:描述性字符串;
  • pixelformat:像素格式编号,无符号32位数据;
  • type:类型,表示获取设备某种功能对应的像素格式;如果要使用,需要在ioctl()之前设置,摄像头就是设置为V4L2_BUF_TYPE_VIDEO_CAPTURE

使用VIDIOC_ENUM_FRAMESIZES指令可以枚举出设备所支持的所有视频采集分辨率,用法如下所示:

ioctl(int fd, VIDIOC_ENUM_FRAMESIZES, struct v4l2_frmsizeenum *frmsize);

需要传入一个struct v4l2_frmsizeenum *指针,ioctl()会将获取到的数据写入到frmsize 指针所指向的对象中。struct v4l2_frmsizeenum 结构体描述了视频帧大小相关的信息:

  • index:同样也是编号;
  • pixel_format:像素格式;
  • type:对应功能的像素格式。

其中还有一个union共用体,当type=V4L2_BUF_TYPE_VIDEO_CAPTURE情况下,descrete生效,描述了视频帧的宽度和高度。

使用VIDIOC_ENUM_FRAMEINTERVALS指令可以枚举出设备所支持的所有帧率,使用方式如下:

ioctl(int fd, VIDIOC_ENUM_FRAMEINTERVALS, struct v4l2_frmivalenum *frmival);

需要传入一个struct v4l2_frmivalenum *指针,ioctl()会将获取到的数据写入到frmival指针所指向的对象中。struct v4l2_frmivalenum 结构体描述了视频帧率相关的信息:

  • index:编号;
  • type:对应功能的像素格式;
  • width、height:制定视频帧大小;
  • pixel_format:像素格式。

该结构体也有一个union共用体,当type=V4L2_BUF_TYPE_VIDEO_CAPTURE时,discrete生效,描述了视频的帧率,numerator是分子,denominator是分母,用numerator/denominator表示图像采集的周期,帧率就是其倒数。

可以使用VIDIOC_G_FMT指令查看设备当期的格式,用法如下所示:

int ioctl(int fd, VIDIOC_G_FMT, struct v4l2_format *fmt);

需要传入一个 truct v4l2_format *指针,ioctl()会将获取到的数据写入到fmt指针所指向的对象中,struct v4l2_format结构体描述了格式相关的信息。

使用VIDIOC_S_FMT指令设置设备的格式,用法如下所示:

int ioctl(int fd, VIDIOC_S_FMT, struct v4l2_format *fmt);

ioctl()会使用fmt所指对象的数据去设置设备的格式。

使用指令VIDIOC_S_FMT设置格式时,实际设置的参数并不一定等于指定的参数,因为摄像头可能不支持该格式,所以设置完成后,还需要检查返回的struct v4l2_format类型变量。使用示例如下:

struct v4l2_format fmt;

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;
if (0 > ioctl(fd, VIDIOC_S_FMT, &fmt)) { //设置格式
	perror("ioctl error");
	return -1;
}
if (800 != fmt.fmt.pix.width || 480 != fmt.fmt.pix.height){
	do_something();
}
if (V4L2_PIX_FMT_RGB565 != fmt.fmt.pix.pixelformat) {
	do_something();
}

使用VIDIOC_G_PARM指令可以获取设备的流类型相关参数(Stream type-dependent parameters),使用方式如下:

ioctl(int fd, VIDIOC_G_PARM, struct v4l2_streamparm *streamparm);

需要传入一个struct v4l2_streamparm *指针,ioctl()会将获取到的数据写入到streamparm指针所指向的对象中,struct v4l2_streamparm 结构体描述了流类型相关的信息。

使用VIDIOC_S_PARM指令设置设备的流类型相关参数,用法如下所示:

ioctl(int fd, VIDIOC_S_PARM, struct v4l2_streamparm *streamparm);

ioctl()会使用streamparm所指对象的数据去设置设备的流类型相关参数:

type与之前一样是对应功能的像素格式。当type=V4L2_BUF_TYPE_VIDEO_CAPTURE时,union共用体中capture变量生效,它是一个struct v4l2_captureparm类型变量,struct v4l2_captureparm结构体描述了摄像头采集相关的一些参数。该结构体中,capability表示设备支持的模式,capturemode表示当前模式,timeperframe描述了视频采集周期。

在设置之前,先通过VIDIOC_G_PARM命令获取到设备的流类型相关参数,判断capability字段是否包含V4L2_CAP_TIMEPERFRAME,如下所示:

struct v4l2_streamparm 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;
	}
}
else
	fprintf(stderr, "不支持帧率设置");

申请帧缓冲、内存映射

读取摄像头数据的方式有两种,一种是read方式,也就是直接通过read()系统调用读取摄像头采集到的数据;另一种则是streaming方式。

使用VIDIOC_QUERYCAP指令查询设备的属性,得到一个struct v4l2_capability类型数据,其中capabilities字段记录了设备拥有的能力,当该字段包含V4L2_CAP_READWRITE时,表示设备支持read I/O方式读取数据;当该字段包含V4L2_CAP_STREAMING时,表示设备支持streaming I/O方式。使用streaming I/O方式,需要向设备申请帧缓冲,并将帧缓冲映射到应用程序进程地址空间

帧缓冲就是用于存储一帧图像数据的缓冲区,使用VIDIOC_REQBUFS指令可申请帧缓冲,使用方式如下所示:

ioctl(int fd, VIDIOC_REQBUFS, struct v4l2_requestbuffers *reqbuf);

需要传入一个struct v4l2_requestbuffers *指针,struct v4l2_requestbuffers结构体描述了申请帧缓冲的信息,ioctl()会根据reqbuf所指对象填充的信息进行申请:

  • type:对应功能的像素格式;
  • count:申请帧缓冲的数量;
  • memory:通常设置为V4L2_MEMORY_MMAP。

示例如下:

struct v4l2_requestbuffers reqbuf;

reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.count = 3; // 申请 3 个帧缓冲
reqbuf.memory = V4L2_MEMORY_MMAP;

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

streaming I/O方式会在内核空间中维护一个帧缓冲队列,驱动程序会将从摄像头读取的一帧数据写入到队列中的一个帧缓冲,接着将下一帧数据写入到队列中的下一个帧缓冲;当应用程序需要读取一帧数据时,需要从队列中取出一个装满一帧数据的帧缓冲,这个取出过程就叫做出队;当应用程序处理完这一帧数据后,需要再把这个帧缓冲加入到内核的帧缓冲队列中,这个过程叫做入队。

使用VIDIOC_REQBUFS指令申请帧缓冲,该缓冲区实质上是由内核所维护的,应用程序不能直接读取该缓冲区的数据,需要将其映射到用户空间中。在映射之前,需要查询帧缓冲的信息,譬如帧缓冲的长度、偏移量等信息,使用VIDIOC_QUERYBUF指令查询,使用方式如下所示:

ioctl(int fd, VIDIOC_QUERYBUF, struct v4l2_buffer *buf);

需要传入一个struct v4l2_buffer *指针,struct v4l2_buffer结构体描述了帧缓冲的信息,ioctl()会将获取到的数据写入到buf指针所指的对象中:

  • index:编号,与之前一样的使用方法;
  • type:同上;
  • memory:同上;
  • length:帧缓冲长度,union中offset是帧缓冲的偏移量;通过VIDIOC_REQBUFS指令申请帧缓冲时,内核会向操作系统申请一块内存空间作为帧缓冲区,这块内存空间的大小就等于申请的帧缓冲数量 * 每一个帧缓冲的大小,每一个帧缓冲对应到这一块内存空间的某一段,所以它们都有一个地址偏移量。

使用示例:

struct v4l2_requestbuffers reqbuf;
struct v4l2_buffer buf;
void *frm_base[3];

reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.count = 3; // 申请 3 个帧缓冲
reqbuf.memory = V4L2_MEMORY_MMAP;

/* 申请 3 个帧缓冲 */
if (0 > ioctl(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 < 3; buf.index++) {
	ioctl(fd, VIDIOC_QUERYBUF, &buf);
	frm_base[buf.index] = mmap(NULL, buf.length,
				PROT_READ | PROT_WRITE, MAP_SHARED,
				fd, buf.m.offset);
	if (MAP_FAILED == frm_base[buf.index]) {
		perror("mmap error");
		return -1;
	}
}

入队

使用VIDIOC_QBUF指令将帧缓冲放入到内核的帧缓冲队列中,使用方式如下:

ioctl(int fd, VIDIOC_QBUF, struct v4l2_buffer *buf);

调用ioctl()之前,需要设置struct v4l2_buffer类型对象的memory、type字段。示例如下:

struct v4l2_buffer buf;

/* 入队操作 */
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
for (buf.index = 0; buf.index < 3; buf.index++) {
	if (0 > ioctl(fd, VIDIOC_QBUF, &buf)) {
		perror("ioctl error");
		return -1;
	}
}

开启视频采集

将所有帧缓冲放入到队列中之后,接着打开摄像头、开启图像采集,使用VIDIOC_DQBUF指令开启视频采集,使用方式如下所示:

ioctl(int fd, VIDIOC_STREAMON, int *type); //开启视频采集
ioctl(int fd, VIDIOC_STREAMOFF, int *type); //停止视频采集

type就是一个enum v4l2_buf_type *指针。通常会如下方法使用:

enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (0 > ioctl(fd, VIDIOC_STREAMON, &type)) {
	perror("ioctl error");
	return -1;
}

读取、处理数据

直接读取每一个帧缓冲的在用户空间的映射区即可读取到摄像头采集的每一帧图像数据。当然读取之前,需要先帧缓冲出队。

使用VIDIOC_DQBUF指令执行出队操作,使用方式如下:

ioctl(int fd, VIDIOC_DQBUF, struct v4l2_buffer *buf);

例如将摄像头的图像显示到LCD,数据处理完成后再将帧缓冲入队,讲下一个帧缓冲出队,如此往复操作。使用示例如下:

struct v4l2_buffer buf;

buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
for ( ; ; ) {
	for(buf.index = 0; buf.index < 3; buf.index++) {
		ioctl(fd, VIDIOC_DQBUF, &buf); //出队
		
		// 读取帧缓冲的映射区、获取一帧数据
		// 处理这一帧数据
		do_something();
		
		// 数据处理完之后、将当前帧缓冲入队、接着读取下一帧数据
		ioctl(fd, VIDIOC_QBUF, &buf);
	}
}

结束视频采集

使用VIDIOC_STREAMOFF指令结束视频采集,使用示例如下:

enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (0 > ioctl(fd, VIDIOC_STREAMOFF, &type)) {
	perror("ioctl error");
	return -1;
}

V4L2摄像头应用编程实战

摄像头的应用编程基本都是如下流程:打开设备、查询设备、设置格式、申请帧缓冲、内存映射、入队、开启视频采集、出队、对采集到的数据进行处理。每一个步骤基本都是通过ioctl()来实现,搭配不同请求指令。

首先需要定义保存摄像头像素格式以及帧缓冲的两个结构体:cam_fmt中保存unsigned char description[32]字符串数组以及unsigned int pixelformat存储像素格式;cam_buf_info中保存unsigned char* start保存帧缓冲起始地址以及unsigned long length保存帧缓冲长度。然后定义一些LCD相关的参数。

编写fb_dev_init函数,这个函数就是LCD的启动初始化,与之前的LCD实验是一样的,就不再赘述了,大致就是open打开后ioctl获取参数然后保存到对应的全局变量,最后mmap映射一下再memset初始化成0xFF。

编写v4l2_dev_init函数,来初始化摄像头:首先定义v4l2_capability结构体变量cap来保存摄像头的属性,open打开摄像头,然后ioctl查询设备功能,通过V4L2_CAP_VIDEO_CAPTURE判断cap是否支持视频采集即可。

编写v4l2_enum_formats函数,定义v4l2_fmtdesc结构体变量fmtdesc,然后设置fmtdesc的index=0以及type取为V4L2_BUF_TYPE_VIDEO_CAPTURE,之后直接while枚举所有摄像头的像素格式并保存到全局的数组中。

编写v4l2_print_formats函数,定义v4l2_frmsizeenum结构体变量frmsize以及v4l2_frmivalenum结构体变量frmival,之后定义两者的type成员为V4L2_BUF_TYPE_VIDEO_CAPTURE,通过for循环枚举cam_fmts[i].pixelformat(上一个函数存入的保存信息的数组),全部printf打印出来,之后把frmsize的index=0,frmsize和frmival的pixel_format全传入cam_fmts[i].pixelformat,然后进入while循环遍历,VIDIOC_ENUM_FRAMESIZES,把尺寸全部printf打印出来,然后设置frmival的index=0,frmival的尺寸全部通过frmsize.discrete传入,再次while遍历VIDIOC_ENUM_FRAMEINTERVALS把帧率printf出来。

编写v4l2_set_format函数,定义v4l2_format结构体变量fmt以及v4l2_streamparm结构体变量streamparm,之后通过fmt的各个成员变量设置帧格式然后ioctl的VIDIOC_S_FMT给设置进去;之后判断一下是否把pixelformat设置成了V4L2_PIX_FMT_RGB565(LCD屏幕是rgb565的),并通过自定义的全局变量保存一下实际的帧宽度和帧高度,最后通过streamparm的type设为=V4L2_BUF_TYPE_VIDEO_CAPTURE并通过ioctl的VIDIOC_G_PARM命令获取一下streamparm,判断是或否支持帧率的设置并设置帧率(V4L2_CAP_TIMEPERFRAME判断并通过streamparm中的成员变量设置)。

编写v4l2_init_buffer函数,定义v4l2_requestbuffers结构体变量reqbuf以及v4l2_buffer结构体变量buf,通过reqbuf来申请帧缓冲,然后通过buf建立内存映射,建立完成后通过for循环中ioctl的VIDIOC_QBUF入队。(这一部分上面是有使用示例的展示的)

编写v4l2_stream_on函数,这个打开摄像头并开启采集,上面有使用示例展示。

编写v4l2_read_data函数,这里需要先进行width和height的校验并对应设置min_h和min_w,之后设置buf的type和memory,在for死循环中,通过for循环buf.index,ioctl由VIDIOC_DQBUF出队,然后再for一行行把数据memcpy拷贝到LCD,拷贝完成后ioctl再次通过VIDIOC_QBUF入队。这个流程上文有展示。

最后是main函数,调用fb_dev_init初始化LCD,v4l2_dev_init初始化摄像头,然后v4l2_enum_formats和v4l2_print_formats把像素格式以及分辨率帧率全部打印出来,之后调用v4l2_set_format设置格式,调用v4l2_init_buffer初始化帧缓冲,最后调用v4l2_stream_on开启视频采集并v4l2_read_data读取数据。

串口应用编程

串口应用编程介绍

串口(UART)在嵌入式Linux系统中常作为系统的标准输入、输出设备,系统运行过程产生的打印信息通过串口输出;同理,串口也作为系统的标准输入设备,用户通过串口与Linux系统进行交互。

所以串口在Linux系统就是一个终端。

终端Terminal

终端就是处理主机输入、输出的一套设备,它用来显示主机运算的输出,并且接受主机要求的输入。

分类如下:

  • 本地终端:主机直接连接的设备;
  • 串口连接的远程终端:嵌入式Linux最常见的中断,开发板和PC连接并在PC运行终端模拟程序来交换信息;
  • 基于网络的远程终端:例如ssh、Telnet登录远程主机。

终端对应的设备节点:

  • /dev/ttyX:/dev/ttyX代表的都是本地终端,包括/dev/tty1-/dev/tty63一共63个本地终端;
  • /dev/pts/X:伪终端对应的设备节点,通过ssh或Telnet这些远程登录协议登录到开发板主机,那么开发板Linux系统会在/dev/pts目录下生成一个设备节点;
  • /dev/ttySTMX:MP157开发板,串口设备的节点名称。

可以通过“who”命令来查看当前的终端。

串口应用编程

就是通过ioctl()对串口进行配置,调用read()读取串口的数据、调用write()向串口写入数据。

当然,可以通过Linux的封装好的标准API进行编程,都是C库函数,可通过man手册查询。这里就把这些接口称为termios API,使用时需要包含termios.h头文件

struct termios结构体

该结构体描述了终端的配置信息。如下所示:

示例代码 26.1.1 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 */
};

可以看出,这些参数分别就对应了不同的模式。具体的含义可以直接去看教程,这里不做赘述。

主要关注的就是c_iflag成员(输入模式)、c_oflag成员(输出模式)、c_cflag成员(控制模式)以及c_lflag成员(本地控制)这四个参数,这些参数能够分别控制、影响终端的行为特性。

终端的三种工作模式

分别为规范模式(canonical mode)、非规范模式(non-canonical mode)和原始模式(raw mode)。通过在struct termios结构体的c_lflag成员中设置ICANNON标志来定义终端是以规范模式(设置ICANNON标志)还是以非规范模式(清除ICANNON标志)工作,默认情况为规范模式

在规范模式下,所有的输入是基于行进行处理的。在用户输入一个行结束符(回车符、EOF 等)之前,系统调用read()函数是读不到用户输入的任何字符的。除了EOF之外的行结束符(回车符等)与普通字符一样会被read()函数读取到缓冲区中。在规范模式中,行编辑是可行的,而且一次read()调用最多只能读取一行数据。如果在read()函数中被请求读取的数据字节数小于当前行可读取的字节数,则read()函数只会读取被请求的字节数,剩下的字节下次再被读取。

在非规范模式下,所有的输入是即时有效的,不需要用户另外输入行结束符,而且不可进行行编辑。在非规范模式下,对参数MIN(c_cc[VMIN])和TIME(c_cc[VTIME])的设置决定read()函数的调用方式。

原始模式是一种特殊的非规范模式。在原始模式下,所有的输入数据以字节为单位被处理。在这个模式下,终端是不可回显的,并且禁用终端输入和输出字符的所有特殊处理。例如将串口与其他设备或传感器进行数据通信时,就需要用原始模式。

打开串口设备

使用open()函数打开串口的设备节点文件,得到文件描述符:

int fd;
fd = open("/dev/ttySTM2", O_RDWR | O_NOCTTY);
if (0 > fd) {
	perror("open error");
	return -1;
}

获取终端当前配置

tcgetattr()函数可以获取到串口终端当前的配置参数,函数原型如下所示:

#include 
#include 

int tcgetattr(int fd, struct termios *termios_p);

第一个参数对应串口终端设备的文件描述符fd。

调用前,需要定义一个struct termios结构体变量,将该变量的指针作为tcgetattr()函数的第二个参数传入;tcgetattr()调用成功后,会将终端当前的配置参数保存到termios_p指针所指的对象中。

函数调用成功返回0;失败将返回-1,并且会设置errno以告知错误原因。

示例如下:

struct termios old_cfg;
if (0 > tcgetattr(fd, &old_cfg)) {
	/* 出错处理 */
	do_something();
}

配置串口终端

  • 配置原始模式

调用头文件中申明的cfmakeraw()函数可以将终端配置为原始模式:

struct termios new_cfg;

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

//配置为原始模式
cfmakeraw(&new_cfg);
  • 接收使能

只需在struct termios结构体的c_cflag成员中添加CREAD标志即可:

new_cfg.c_cflag |= CREAD; //接收使能
  • 设置串口波特率

设置波特率的主要函数有cfsetispeed()和cfsetospeed(),这两个函数在头文件中申明,使用方法很简单,如下所示:

cfsetispeed(&new_cfg, B115200);
cfsetospeed(&new_cfg, B115200);

B115200就是设置波特率为115200。

cfsetispeed()函数设置数据输入波特率,而cfsetospeed()函数设置数据输出波特率。

还可以直接使用cfsetspeed()函数一次性设置输入和输出波特率,该函数也是在头文件中申明,使用方式如下:

cfsetspeed(&new_cfg, B115200);

这几个函数在成功时返回 0,失败时返回-1。

  • 设置数据位大小

首先将c_cflag成员中CSIZE位掩码所选择的几个bit位清零,然后再设置数据位大小,如下所示:

new_cfg.c_cflag &= ~CSIZE;
new_cfg.c_cflag |= CS8; //设置为 8 位数据位
  • 设置奇偶校验位

串口的奇偶校验位配置一共涉及到struct termios结构体中的两个成员变
量:c_cflag和c_iflag。首先对于c_cflag成员,需要添加PARENB标志以使能串口的奇偶校验功能;同时对于c_iflag成员来说,还需要添加INPCK标志,这样才能对接收到的数据执行奇偶校验,代码如下所示:

//奇校验使能
new_cfg.c_cflag |= (PARODD | PARENB);
new_cfg.c_iflag |= INPCK;

//偶校验使能
new_cfg.c_cflag |= PARENB;
new_cfg.c_cflag &= ~PARODD; /* 清除 PARODD 标志,配置为偶校验 */
new_cfg.c_iflag |= INPCK;

//无校验
new_cfg.c_cflag &= ~PARENB;
new_cfg.c_iflag &= ~INPCK;

  • 设置停止位

通过设置c_cflag成员的CSTOPB标志而实现的。若停止位为一个比特,则清除CSTOPB标志;若停止位为两个,则添加CSTOPB标志即可:

// 将停止位设置为一个比特
new_cfg.c_cflag &= ~CSTOPB;

// 将停止位设置为 2 个比特
new_cfg.c_cflag |= CSTOPB;
  • 设置MIN和TIME

MIN和TIME的取值会影响非规范模式下read()调用的行为特征,原始模式是一种特殊的非规范模式,所以MIN和TIME在原始模式下也是有效的。

对接收字符和等待时间没有特别要求的情况下,可以将MIN和TIME设置为0,这样则在任何情况下read()调用都会立即返回,此时对串口的read操作会设置为非阻塞方式,如下所示:

new_cfg.c_cc[VTIME] = 0;
new_cfg.c_cc[VMIN] = 0;

缓冲区处理

可以调用中声明的tcdrain()、tcflow()、tcflush()等函数来处理目前串口缓冲中的数据,它们的函数原型如下所示:

#include 
#include 

int tcdrain(int fd);
int tcflush(int fd, int queue_selector);
int tcflow(int fd, int action);

调用tcdrain()函数后会使得应用程序阻塞,直到串口输出缓冲区中的数据全部发送完毕为止!

调用tcflow()函数会暂停串口上的数据传输或接收工作,具体情况取决于参数action取值。

调用tcflush()函数,会清空输入/输出缓冲区中的数据,具体情况取决于参数queue_selector取值。

以上这三个函数,调用成功时返回0;失败将返回-1、并且会设置errno以指示错误类型。

通常会选择tcdrain()或tcflush()函数来对串口缓冲区进行处理,直接调用tcdrain()阻塞或调用tcflush()清空缓冲区:

tcdrain(fd);
tcflush(fd,TCIOFLUSH);

写入配置,使配置生效

需要将配置参数写入到终端设备(串口硬件),使其生效。通过tcsetattr()函数将配置参数写入到硬件设备,其函数原型如下所示:

#include 
#include 

int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);

调用该函数会将参数termios_p所指struct termios对象中的配置参数写入到终端设备中,使配置生效。参数optional_actions来指定更改的生效时刻。

立即生效示例如下:

tcsetattr(fd, TCSANOW, &new_cfg);

读写数据

这里就是直接调用read()和write()就可以了。

串口应用编程实战

定义一个uart_cfg_t来保存串口的参数:波特率、数据位、奇偶校验以及停止位;定义termios结构体变量old_cfg以及fd设备文件描述符。

编写uart_init函数,open打开设备然后tcgetattr获取当前参数。

编写uart_cfg函数,定义一个termios结构体变量new_cfg,cfmakeraw设置为原始模式,然后new_cfg.c_cflag或上CREAD使能接收,通过switch判断cfg->baudrate来设置波特率给到speed_t结构体变量speed,然后调用cfsetspeen来设置波特率;之后先通过new_cfg.c_cflag&=~CSIZE清除数据位相关配置,然后switch判断cfg->dbit,通过或来配置数据位;switch判cfg->parity来对应设置奇偶校验位,这个上文有示例;之后设置MIN和TIME为0,tcflush清空缓冲区,然后tcsetattr写入配置并使能。

编写io_handler函数,作为信号处理函数,如果未接收到SIGRTMIN就直接return,判断info->si_code==POLLIN,为真则有数据,就read8个字节然后for循环printf打印出来。

编写async_io_init函数,通过fcntl使能串口的异步I/O功能,然后flag或上O_ASYNC再fcntl设置进去;fcntl的F_SETOWN来设置异步I/O为自己;fcntl设定SIGRTMIN实时信号为异步I/O通知信号;然后就是sigantion添加io_handler为信号处理函数。

最后就是main函数,可以unsigned char预定义好要传输的数据,然后通过for循环传参argc,通过strncmp比较传入的是什么参数然后传到对应的结构体成员中;然后调用uart_init初始化串口,uart_cfg配置串口,switch判断rw_flag来判断读写,并分别调用read和write传输数据,最后tcsetattr恢复串口配置并close设备文件。

开发板测试

这里是通过RS232来测试,所以还需要USB转RS232才能完成测试。

看门狗应用编程

在产品化的嵌入式系统中,为了使系统在异常情况下能自动复位,一般都需要引入看门狗。看门狗其实就是一个可以在一定时间内被复位的计数器。当看门狗启动后,计数器开始自动计数,经过一定时间,如果没有被复位,计数器溢出就会对CPU产生一个复位信号使系统重启(俗称“被狗咬”)。系统正常运行时,需要在看门狗允许的时间间隔内对看门狗计数器清零(俗称“喂狗”),不让复位信号产生。如果系统不出问题,程序保证按时“喂狗”,一旦程序跑飞,没有“喂狗”,系统“被咬”复位。

看门狗应用程序介绍

Linux系统中所注册的看门狗外设,都会在/dev/目录下生成对应的设备节点(设备文件),设备节点名称通常为watchdogX。

应用层控制看门狗其实非常简单,通过ioctl()函数即可做到!

首先在应用程序中,需要包含头文件头文件,该头文件中定义了一些ioctl指令宏。常用指令有:WDIOC_GETSUPPORT 、 WDIOC_SETOPTIONS 、 WDIOC_KEEPALIVE 、
WDIOC_SETTIMEOUT、WDIOC_GETTIMEOUT。

正点原子linux应用编程——提高篇3_第3张图片

打开设备

首先通过open获取看门狗设备的文件描述符。

获取设备支持功能

使用WDIOC_GETSUPPORT指令获取看门狗设备支持哪些功能,使用方式如下:

ioctl(int fd, WDIOC_GETSUPPORT, struct watchdog_info *info);

需要传入一个struct watchdog_info
*指针,ioctl()会将获取到的数据写入到info指针所指向的对象中。struct watchdog_info结构体描述了看门狗设备的信息。该结构体常见的值包括:WDIOF_SETTIMEOUT、WDIOF_KEEPALIVEPING;WDIOF_SETTIMEOUT表示设备支持设置超时时间;WDIOF_KEEPALIVEPING表示设备支持“喂狗”操作,也就是重置看门狗计时器。

使用示例如下:

struct watchdog_info info;

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

printf("identity: %s\n", info.identity);
printf("version: %u\n", firmware_version);

if (0 == (WDIOF_KEEPALIVEPING & info.options))
	printf("设备不支持喂狗操作\n");
if (0 == (WDIOF_SETTIMEOUT & info.options))
	printf("设备不支持设置超时时间\n");

获取/设置超时时间

使用WDIOC_GETTIMEOUT指令可获取设备当前设置的超时时间,使用方式如下:

ioctl(int fd, WDIOC_GETTIMEOUT, int *timeout);

使用WDIOC_SETTIMEOUT指令可设置看门狗的超时时间,使用方式如下:

ioctl(int fd, WDIOC_SETTIMEOUT, int *timeout);

超时时间是以秒为单位,设置超时时间,不可超过其最大值、否则ioctl()调用将会失败,使用示例如下所示:

int timeout;

/* 获取超时时间 */
if (0 > ioctl(fd, WDIOC_GETTIMEOUT, &timeout)) {
	fprintf(stderr, "ioctl error: WDIOC_GETTIMEOUT: %s\n", strerror(errno));
	return -1;
}

printf("current timeout: %ds\n", timeout);

/* 设置超时时间 */
timeout = 10; //10 秒钟
if (0 > ioctl(fd, WDIOC_SETTIMEOUT, &timeout)) {
	fprintf(stderr, "ioctl error: WDIOC_SETTIMEOUT: %s\n", strerror(errno));
	return -1;
}

开启/关闭看门狗

使用WDIOC_SETOPTIONS指令可以开启看门狗计时或停止看门狗计时,使用方式如下:

ioctl(int fd, WDIOC_SETOPTIONS, int *option);

option指针内容,WDIOS_DISABLECARD表示停止看门狗计时,WDIOS_ENABLECARD则表示开启看门狗计时。

这里需要注意,close()关闭设备时,看门狗会自动启动;所以,当打开设备之后,需要使用WDIOC_SETOPTIONS指令停止看门狗计时,等所有设置完成之后再开启看门狗计时器

喂狗

通过WDIOC_KEEPALIVE指令喂狗,使用方式如下:

ioctl(int fd, WDIOC_KEEPALIVE, NULL);

看门狗应用编程实战

这个函数就比较简单,只有一个main函数。

首先定义watchdog_info结构体变量info,然后open打开看门狗设备文件,打开后先通过WDIOS_DISABLECARD,使用ioctl的WDIOC_SETOPTIONS配置看门狗计时器停止,然后ioctl调用WDIOC_SETTIMEOUT配置超时事件,之后通过WDIOS_ENABLECARD,调用ioctl的WDIOC_SETOPTIONS使能看门狗计时器,然后for死循环中通过ioctl的WDIOC_KEEPALIVE命令喂狗。

你可能感兴趣的:(linux学习,linux,学习,笔记)