目录
前言
主要介绍linux系统c应用开发,基于IMX6ULL平台,有待完善
一、GPIO应用编程
1.1 应用层操控硬件的两种方式
- /dev/目录下的设备文件(设备节点);
- /sys/目录下设备的属性文件。
一般简单地设备会使用 sysfs 方式操控,其设备驱动在实现时会将设备的一些属性导出到用户空间 sysfs 文件系统,以属性文 件的形式为用户空间提供对这些数据、属性的访问支持,譬如 LED、GPIO 等。但对于一些较复杂的设备通常会使用设备节点的方式,譬如 LCD 等、触摸屏、摄像头等。
2)sysfs 文件系统
sysfs 是一个基于内存的文件系统,同 devfs、proc 文件系统一样,称为虚拟文件系统;它的作用是将内核信息以文件的方式提供给应用层使用,sysfs 文件系统挂载在/sys 目录下
/sys 下的子目录 | 说明 |
---|---|
/sys/devices | 这是系统中所有设备存放的目录,也就是系统中的所有设备在 sysfs 中的呈现、表达,也是 sysfs 管理设备的最重要的目录结构。 |
/sys/block | 块设备的存放目录,这是一个过时的接口,按照 sysfs 的设计理念,系统所有的设备都存放在/sys/devices 目录下,所以/sys/block 目录下的文件通常是链接到/sys/devices 目录下的文件。 |
/sys/bus | 这是系统中的所有设备按照总线类型分类放置的目录结 构,/sys/devices 目录下每一种设备都是挂在某种总线下 的,譬如 i2c 设备挂在 I2C 总线下。同样,/sys/bus 目录下的文件通常也是链接到了/sys/devices 目录。 |
/sys/class | 这是系统中的所有设备按照其功能分类放置的目录结构, 同样该目录下的文件也是链接到了/sys/devices 目录。按照设备的功能划分组织在/sys/class 目录下,譬如/sys/class/leds 目录中存放了所有的 LED 设备,/sys/class/input 目录中存放了所有的输入类设备。 |
/sys/dev | 这是按照设备号的方式放置的目录结构,同样该目录下的文件也是链接到了/sys/devices 目录。该目录下有很多以主设备号:次设备号(major:minor)命名的文件,这些文件都是链接文件,链接到/sys/devices 目录下对应的设备。 |
/sys/firmware | 描述了内核中的固件。 |
/sys/fs | 用于描述系统中所有文件系统,包括文件系统本身和按文件系统分类存放的已挂载点。 |
/sys/kernel | 这里是内核中所有可调参数的位置。 |
/sys/module | 这里有系统中所有模块的信息。 |
/sys/power | 这里是系统中电源选项,有一些属性可以用于控制整个系统的电源状态。 |
系统中所有的设备(对象)都会在/sys/devices 体现出来,是 sysfs 文件系统中最重要的目录结构;而/sys/bus、/sys/class、/sys/dev 分别将设备按照挂载的总线类型、功能分类以及设备号的形式将设备组织存放在这些目录中,这些目录下的文件都是链接到了/sys/devices 中。
1.2 LED灯控制
如果 LED 设备使用的是 Linux 内核标准 LED 驱动框架注册而成,在/dev 目录下并没有其对应的设备节点,其实现使用 sysfs 方式控制
/sys/class/leds 目录下便存放了所有的 LED 类设备, 进入sys-led 文件夹,发现其内有brightness、max_brightness 以及 trigger 三个文件
- brightness:该属性文件可读可写;这个属性文件是用于设置 LED的亮度等级或者获取当前 LED 的亮度等级
- max_brightness:该属性文件只能被读取,不能写,用于获取 LED 设备的最大亮度等级。
- trigger:触发模式,该属性文件可读可写,读表示获取 LED 当前的触发模式,写表示设置 LED 的触发模式
1.2.1 命令控制
- cat 命令:可以用来查看文件内容
- echo命令:用来输出控制
echo timer > trigger //将 LED 触发模式设置为 timer
echo 1 > brightness //点亮 LED echo 0 > brightness//熄灭 LED
1.2.2 LED 应用程序
具体可以参考后一小节GPIO输出
1.3 GPIO控制
与 LED 设备一样,GPIO 同样也是通过 sysfs 方式进行操控,进入到/sys/class/gpio 目录下
- gpiochipX:当前 SoC 所包含的 GPIO 控制器。我们知道I.MX6ULL 一共包含了 5 个 GPIO控制器,分别为 GPIO1、GPIO2、GPIO3、GPIO4、GPIO5,在这里分别对应 gpiochip0、gpiochip32、gpiochip64、gpiochip96、gpiochip128 这 5 个文件夹,随便进入到其中某个目录下
- base:与 gpiochipX 中的 X 相同,表示该控制器所管理的这组 GPIO 引脚中最小的编号
- label:该组 GPIO 对应的标签,也就是名字
- ngpio:该控制器所管理的 GPIO 引脚的数量(所以引脚编号范围是:base ~ base+ngpio-1)
- export:用于将指定编号的 GPIO 引脚导出。在使用 GPIO 引脚之前,需要将其导出,导出成功之后才能使用它。注意 export 文件是只写文件,不能读取,导出成功之后会发现在/sys/class/gpio 目录下生成了一个名为 gpioX 的文件夹,譬如:
echo 0 > export # 导出编号为 0 的 GPIO 引脚(对于 I.MX6UL/I.MX6ULL 来说,也就是GPIO1_IO0)
在gpioX 的文件夹中
- direction:配置 GPIO 引脚为输入或输出模式。该文件可读、可写
- value:在 GPIO 配置为输出模式下,向 value 文件写入"0"控制 GPIO 引脚输出低电平,写入"1"则控制 GPIO 引脚输出高电平。在输入模式下,读取 value 文件获取 GPIO 引脚当前的输入电平状态
- active_low:这个属性文件用于控制极性,可读可写,默认情况下为 0
- edge:控制中断的触发模式,该文件可读可写
- unexport:将导出的 GPIO 引脚删除。当使用完 GPIO 引脚之后,我们需要将导出的引脚删除,同样该文件也是只写文件、不可读,譬如:
echo 0 > unexport # 删除导出的编号为 0 的 GPIO 引脚
1.4 GPIO应用编程
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
static char gpio_path[100];
static int gpio_config(const char *attr, const char *val)
{
char file_path[100];
int len;
int fd;
sprintf(file_path, "%s/%s", gpio_path, attr);
if (0 > (fd = open(file_path, O_WRONLY))) {
perror("open error");
return fd;
}
len = strlen(val);
if (len != write(fd, val, len)) {
perror("write error");
close(fd);
return -1;
}
close(fd); //关闭文件
return 0;
}
int main(int argc, char *argv[])
{
/* 校验传参 */
if (3 != argc) {
fprintf(stderr, "usage: %s <gpio> <value>\n", argv[0]);
exit(-1);
}
/* 判断指定编号的GPIO是否导出 */
sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]);
if (access(gpio_path, F_OK)) {//如果目录不存在 则需要导出
int fd;
int len;
if (0 > (fd = open("/sys/class/gpio/export", O_WRONLY))) {
perror("open error");
exit(-1);
}
len = strlen(argv[1]);
if (len != write(fd, argv[1], len)) {//导出gpio
perror("write error");
close(fd);
exit(-1);
}
close(fd); //关闭文件
}
/* 配置为输出模式 */
if (gpio_config("direction", "out"))
exit(-1);
/* 极性设置 */
if (gpio_config("active_low", "0"))
exit(-1);
/* 控制GPIO输出高低电平 */
if (gpio_config("value", argv[2]))
exit(-1);
/* 退出程序 */
exit(0);
}
1.5 程序运行
注意:以下可以将Ubuntu当开发板实验
1.5.1 环境搭建
- 安装 Poky 交叉编译工具链
https://download.csdn.net/download/weixin_44567668/87701219
①修改权限
chmod u+x fsl-imx-x11-glibc-x86_64-meta-toolchain-qt5-cortexa7hf-neon-toolchain-4.1.15-2.1.0.sh
②使能环境变量
source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
③使用 env 指令查看生效的环境变量,使用 arm-poky-linux-gnueabi-gcc -v 指令可以查看 gcc 版本
④要使用此编译器编译内核和 U-boot 还需要安装以下软件
sudo apt-get update // 先更新软列表
sudo apt-get install lzop // 安装 lzop 工具,用于生成压缩或解压镜像
sudo apt-get install libncurses* // 安装 ncurese 相关库,U-boot 或者内核菜单显示时需要
1.5.2 编译测试
①环境设置
source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
②使用交叉编译工具编译应用程序
${CC} -o testApp testApp.c
③将testApp.c拷贝到开发板跟文件系统中,可使用 scp 命令
④执行该应用程序控制开发板上的 GPIO1_IO01 引脚输出高或低电平
./testApp 1 1 #控制 GPIO1_IO01 输出高电平
二、输入设备应用编程
2.1 输入设备介绍
输入设备(也称为 input 设备),常见的输入设备有鼠标、键盘、触摸屏、遥控器、电脑画图板等,用户通过输入设备与系统进行交互。
Linux 系统为了统一管理这些输入设备,实现了一套能够兼容所有输入设备的框架,那么这个框架就是 input 子系统。驱动开发人员基于 input 子系统开发输入设备的驱动程序,input 子系统可以屏蔽硬件的差异,向应用层提供一套统一的接口。
基于 input 子系统注册成功的输入设备,都会在/dev/input目录下生成对应的设备节点(设备文件),设备节点名称通常为 eventX(X 表示一个数字编号 0、1、2、3 等),譬如/dev/input/event0、/dev/input/event1、 /dev/input/event2 等,通过读取这些设备节点可以获取输入设备上报的数据。
2.1.1 读取数据的流程
假设触摸屏设备对应的设备节点为/dev/input/event0,那么数据读取流程如下:
①应用程序打开/dev/input/event0 设备文件;
②应用程序发起读操作(譬如调用 read),如果没有数据可读则会进入休眠(阻塞 I/O 情况下);
③当有数据可读时,应用程序会被唤醒,读操作获取到数据返回;
④应用程序对读取到的数据进行解析。
当无数据可读时,程序会进入休眠状态(也就是阻塞),譬如应用程序读触摸屏数据,如果当前并没有去触碰触摸屏,自然是无数据可读;当我们用手指触摸触摸屏或者在屏上滑动时,此时就会产生触摸数据、应用程序就有数据可读了,应用程序会被唤醒,成功读取到数据。那么对于其它输入设备亦是如此,无数据可读时应用程序会进入休眠状态(阻塞式 I/O 方式下),当有数据可读时才会被唤醒。
2.1.2 数据的解析
应用程序打开输入设备对应的设备文件,向其发起读操作,那么这个读操作获取到的是什么样的数据呢?其实每一次 read 操作获取的都是一个 struct input_event 结构体类型数据,该结构体定义在<linux/input.h>头文件中,它的定义如下:
struct input_event {
struct timeval time;
__u16 type;
__u16 code;
__s32 value;
};
- 结构体中的 time 成员变量是一个 struct timeval 类型的变量,内核会记录每个上报的事件其发生的时间,并通过变量 time 返回给应用程序
- type:type 用于描述发生了哪一种类型的事件(对事件的分类),Linux 系统所支持的输入事件类型如下所示:
#define EV_SYN 0x00 //同步类事件,用于同步事件
#define EV_KEY 0x01 //按键类事件
#define EV_REL 0x02 //相对位移类事件(譬如鼠标)
#define EV_ABS 0x03 //绝对位移类事件(譬如触摸屏)
#define EV_MSC 0x04 //其它杂类事件
#define EV_SW 0x05
#define EV_LED 0x11
#define EV_SND 0x12
#define EV_REP 0x14
#define EV_FF 0x15
#define EV_PWR 0x16
#define EV_FF_STATUS 0x17
#define EV_MAX 0x1f
#define EV_CNT (EV_MAX+1)
- code:表示该类事件中的哪一个具体事件,如按键类事件、相对位移事件等
- value:内核每次上报事件都会向应用层发送一个数据 value,对 value 值的解释随着 code 的变化而变化
2.1.3 数据同步
应用程序如何得知本轮已经读取到完整的数据了呢?其实这就是通过同步事件来实现的,内核将本轮需要上报、发送给接收者的数据全部上报完毕后,接着会上报一个同步事件,以告知应用程序本轮数据已经完整、可以进行同步了。同步类事件中也包含了多种不同的事件,如下所示:
#define SYN_REPORT 0
#define SYN_CONFIG 1
#define SYN_MT_REPORT 2
#define SYN_DROPPED 3
#define SYN_MAX 0xf
#define SYN_CNT (SYN_MAX+1)
所有的输入设备都需要上报同步事件,上报的同步事件通常是 SYN_REPORT,而 value 值通常为 0。
2.2 按键应用编程
在 for 循环中,调用 read()读取输入设备上报的数据,当按键按下或松开(以及长按)动作发生时,read()会读取到输入设备上报的数据,首先判断此次上报的事件是否是按键类事件(EV_KEY),如果是按键类事件、接着根据 value 值来判断按键当前的状态是松开、按下还是长按。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>
int main(int argc, char *argv[])
{
struct input_event in_ev = {0};
int fd = -1;
int value = -1;
/* 校验传参 */
if (2 != argc) {
fprintf(stderr, "usage: %s <input-dev>\n", argv[0]);
exit(-1);
}
/* 打开文件 */
if (0 > (fd = open(argv[1], O_RDONLY))) {
perror("open error");
exit(-1);
}
for ( ; ; ) {
/* 循环读取数据 */
if (sizeof(struct input_event) !=
read(fd, &in_ev, sizeof(struct input_event))) {
perror("read error");
exit(-1);
}
if (EV_KEY == in_ev.type) { //按键事件
switch (in_ev.value) {
case 0:
printf("code<%d>: 松开\n", in_ev.code);
break;
case 1:
printf("code<%d>: 按下\n", in_ev.code);
break;
case 2:
printf("code<%d>: 长按\n", in_ev.code);
break;
}
}
}
}
2.3 触摸屏应用编程
2.3.1 解析触摸屏设备上报的数据
触摸屏设备是一个绝对位移设备,可以上报绝对位移事件
触摸屏设备除了上报绝对位移事件之外,还可以上报按键类事件和同步类事件。同步事件很好理解,因为几乎每一个输入设备都会上报同步事件、告知应用层本轮数据是否完整;当手指点击触摸屏或手指从触摸屏离开时,此时就会上报按键类事件,用于描述按下触摸屏和松开触摸屏;具体的按键事件为BTN_TOUCH(code=0x14a,也就是 330),当然,手指在触摸屏上滑动不会上报 BTN_TOUCH 事件。
- 单点触摸设备
触摸屏分为多点触摸设备和单点触摸设备。单点触摸设备只支持单点触摸,一轮(笔者把一个同步事件称为一轮)完整的数据只包含一个触摸点信息;单点触摸设备以 ABS_XXX 事件承载、上报触摸点的信息,譬如 ABS_X(value 值对应的是 X 轴坐标值)、ABS_Y(value 值对应的是 Y 轴坐标值)等绝对位移事件,而有些设备可能还支持 Z 轴坐标(通过 ABS_Z 事件上报、value 值对应的便是 Z 轴坐标值)、按压力大小(通过 ABS_PRESSURE 事件上报、value 值对应的便是按压力大小)以及接触面积等属性。大部分的单点触摸设备都会上报 ABS_X 和 ABS_Y 事件,而其它绝对位移事件则根据具体的设备以及驱动的实现而定!
单点触摸设备事件上报的流程大概如下所示:
#点击触摸屏时
BTN_TOUCH
ABS_X
ABS_Y
SYN_REPORT
#滑动
ABS_X
ABS_Y
SYN_REPORT
#松开
BTN_TOUCH
SYN_REPORT
- 多点触摸设备
在 Linux 内核中,多点触摸设备使用多点触摸(MT)协议上报各个触摸点的数据,MT 协议分为两种类型:Type A 和 Type B,Type A 协议实际使用中用的比较少,我们重点来看看 Type B 协议。
Type B协议的重点是通过 ABS_MT_SLOT 事件上报各个触摸点信息的更新!能够追踪并区分触摸点的设备通常在硬件上能够区分不同的触摸点,譬如对于一个 5 点触摸设备来说,硬件能够为每一个识别到的触摸点与一个 slot 进行关联,这个 slot 就是一个编号,触摸点 0、触摸点 1、触摸点 2 等。底层驱动向应用层上报 ABS_MT_SLOT 事件,此事件会告诉接收者当前正在更新的是哪个触摸点的数据,ABS_MT_SLOT 事件中对应的 value 数据存放的便是一个 slot、以告知应用层当前正在更新 slot关联的触摸点对应的信息。
每个识别出来的触摸点分配一个 slot,与该 slot 关联起来,利用这个 slot 来传递对应触点的变化。除了ABS_MT_SLOT 事 件 之 外 , Type B 协 议 还 会 使 用 到 ABS_MT_TRACTKING_ID 事 件 ,
ABS_MT_TRACTKING_ID 事件则用于触摸点的创建、替换和销毁工作,ABS_MT_TRACTKING_ID 事件携带的数据 value 表示一个 ID,一个非负数的 ID(ID>=0)表示一个有效的触摸点,如果 ID 等于-1 表示该触摸点已经不存在、被移除了;一个以前不存在的 ID 表示这是一个新的触摸点。
Type B 协议可以减少发送到用户空间的数据,只有发生了变更的数据才会上报,譬如某个触摸点发生了移动,但仅仅只改变了 X 轴坐标、而未改变 Y 轴坐标,那么内核只会将改变后的 X 坐标值通过ABS_MT_POSITION_X 事件发送给应用层。
Type B 协议下多点触摸设备上报数据的流程列举如下:
ABS_MT_SLOT 0
ABS_MT_TRACKING_ID 10
ABS_MT_POSITION_X
ABS_MT_POSITION_Y
ABS_MT_SLOT 1
ABS_MT_TRACKING_ID 11
ABS_MT_POSITION_X
ABS_MT_POSITION_Y
SYN_REPORT
- 触摸屏上报数据分析
使用命令"cat /proc/bus/input/devices",确定触摸屏对应的设备节点,如下所示:
2.3.2 获取触摸屏的信息
- 通过 ioctl()函数可以获取到触摸屏支持的最大触摸点数、触摸屏 X、Y 坐标的范围等,使用方式如下:
char name[100];
ioctl(fd, EVIOCGNAME(sizeof(name)), name);
- 通过EVIOCGABS(abs)这个宏可以获取到触摸屏 slot(slot<0>表示触摸点 0、slot<1>表示触摸点 1、slot<2>表示触摸点 2,以此类推!)的取值范围
#define EVIOCGABS(abs) _IOR('E', 0x40 + (abs), struct input_absinfo)
2.3.3 单点触摸应用程序
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>
int main(int argc, char *argv[])
{
struct input_event in_ev;
int x, y; //触摸点x和y坐标
int down; //用于记录BTN_TOUCH事件的value,1表示按下,0表示松开,-1表示移动
int valid; //用于记录数据是否有效(我们关注的信息发生更新表示有效,1表示有效,0表示无效)
int fd = -1;
/* 校验传参 */
if (2 != argc) {
fprintf(stderr, "usage: %s <input-dev>\n", argv[0]);
exit(EXIT_FAILURE);
}
/* 打开文件 */
if (0 > (fd = open(argv[1], O_RDONLY))) {
perror("open error");
exit(EXIT_FAILURE);
}
x = y = 0; //初始化x和y坐标值
down = -1; //初始化<移动>
valid = 0;//初始化<无效>
for ( ; ; ) {
/* 循环读取数据 */
if (sizeof(struct input_event) !=
read(fd, &in_ev, sizeof(struct input_event))) {
perror("read error");
exit(EXIT_FAILURE);
}
switch (in_ev.type) {
case EV_KEY: //按键事件
if (BTN_TOUCH == in_ev.code) {
down = in_ev.value;
valid = 1;
}
break;
case EV_ABS: //绝对位移事件
switch (in_ev.code) {
case ABS_X: //X坐标
x = in_ev.value;
valid = 1;
break;
case ABS_Y: //Y坐标
y = in_ev.value;
valid = 1;
break;
}
break;
case EV_SYN: //同步事件
if (SYN_REPORT == in_ev.code) {
if (valid) {//判断是否有效
switch (down) {//判断状态
case 1:
printf("按下(%d, %d)\n", x, y);
break;
case 0:
printf("松开\n");
break;
case -1:
printf("移动(%d, %d)\n", x, y);
break;
}
valid = 0; //重置valid
down = -1; //重置down
}
}
break;
}
}
}
三、LCD应用开发
3.1 FrameBuffer设备
Frame 是帧的意思,buffer 是缓冲的意思,所以 Framebuffer 就是帧缓冲,这意味着 Framebuffer 就是一块内存,里面保存着一帧图像。帧缓冲(framebuffer)是 Linux 系统中的一种显示驱动接口,它将显示设备(譬如 LCD)进行抽象、屏蔽了不同显示设备硬件的实现,对应用层抽象为一块显示内存(显存),它允许上层应用程序直接对显示缓冲区进行读写操作,而用户不必关心物理显存的位置等具体细节,这些都由Framebuffer 设备驱动来完成。
所以在 Linux 系统中,显示设备被称为 FrameBuffer 设备(帧缓冲设备),所以 LCD 显示屏自然而言就是 FrameBuffer 设备。FrameBuffer 设备对应的设备文件为/dev/fbX(X 为数字,0、1、2、3 等),Linux下可支持多个 FrameBuffer 设备,最多可达 32 个,分别为/dev/fb0 到/dev/fb31,开发板出厂系统中,/dev/fb0设备节点便是 LCD 屏。
应用程序读写/dev/fbX 就相当于读写显示设备的显示缓冲区(显存),譬如 LCD 的分辨率是 800 * 480,每一个像素点的颜色用 24 位(譬如 RGB888)来表示,那么这个显示缓冲区的大小就是 800 x 480 x 24 / 8 = 1152000 个字节。譬如执行下面这条命令将 LCD 清屏,也就是将其填充为黑色(假设 LCD 对应的设备节点是/dev/fb0,分辨率为 800*480,RGB888 格式):
3.2 LCD 应用编程介绍
对 FrameBuffer 设备(譬如 LCD)进行应用编程,通过对 LCD 设备节点/dev/fb0(假设 LCD 对应的设备节点是/dev/fb0)进行 I/O 操作即可实现对 LCD 的显示控制,实质就相当于读写了 LCD 的显存,而显存是 LCD 的显示缓冲区,LCD 硬件会从显存中读取数据显示到 LCD 液晶面板上,步骤如下:
①首先打开/dev/fbX 设备文件。
②使用 ioctl()函数获取到当前显示设备的参数信息,譬如屏幕的分辨率大小、像素格式,根据屏幕参数计算显示缓冲区的大小。
③通过存储映射 I/O 方式将屏幕的显示缓冲区映射到用户空间(mmap)。
④映射成功后就可以直接读写屏幕的显示缓冲区,进行绘图或图片显示等操作了。
⑤完成显示后,调用 munmap()取消映射、并调用 close()关闭设备文件。
3.2.1 使用 ioctl()获取屏幕参数信息
通 过 ioctl() 函 数 来 获 取 屏 幕 参 数 信息, 对 于 Framebuffer 设备来说, 常 用 的 request 包 括FBIOGET_VSCREENINFO、FBIOPUT_VSCREENINFO、FBIOGET_FSCREENINFO。三个宏定义以及2 个数据结构 struct fb_var_screeninfo 和 struct fb_fix_screeninfo 都定义在<linux/fb.h>头文件中
- FBIOGET_VSCREENINFO:表示获取 FrameBuffer 设备的可变参数信息,可变参数信息使用 struct fb_var_screeninfo 结 构 体 来 描 述 , 所 以 此 时 ioctl() 需 要 有 第 三 个 参 数 , 它 是 一 个 struct fb_var_screeninfo *指针,指向 struct fb_var_screeninfo 类型对象,调用 ioctl()会将 LCD 屏的可变参数信息保存在 struct fb_var_screeninfo 类型对象中,如下所示:
struct fb_var_screeninfo fb_var;
ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
- FBIOPUT_VSCREENINFO:表示设置 FrameBuffer 设备的可变参数信息,既然是可变参数,那说明应用层可对其进行修改、重新配置,当然前提条件是底层驱动支持这些参数的动态调整,譬如在我们的 Windows 系统中,用户可以修改屏幕的显示分辨率,这就是一种动态调整。同样此时 ioctl()需要有第三个参数,也是一个 struct fb_var_screeninfo *指针,指向 struct fb_var_screeninfo 类型对象,表示用 struct fb_var_screeninfo 对象中填充的数据设置 LCD
- FBIOGET_FSCREENINFO:表示获取 FrameBuffer 设备的固定参数信息,既然是固定参数,那就意味着应用程序不可修改。固定参数信息使用struct fb_fix_screeninfo结构体来描述,所以此时ioctl()需要有第三个参数,它是一个 struct fb_fix_screeninfo *指针,指向 struct fb_fix_screeninfo 类型对象,调用 ioctl()会将 LCD 的固定参数信息保存在 struct fb_fix_screeninfo 对象中
3.2.2 使用 mmap()将显示缓冲区映射到用户空间
存储映射 I/O 这种高级 I/O 方式,它的一个非常经典的使用场景便是用在 Framebuffer 应用编程中。通过 mmap()将显示器的显示缓冲区(显存)映射到进程的地址空间中,这样应用程序便可直接对显示缓冲区进行读写操作。
为什么这里需要使用存储映射 I/O 这种方式呢?其实使用普通的 I/O 方式(譬如直接 read、write)也是可以的,只是,当数据量比较大时,普通 I/O 方式效率较低。假设某一显示器的分辨率为 1920 * 1080,像
素格式为 ARGB8888,针对该显示器,刷一帧图像的数据量为 1920 x 1080 x 32 / 8 = 8294400 个字节(约等于 8MB),这还只是一帧的图像数据,而对于显示器来说,显示的图像往往是动态改变的,意味着图像数
据会被不断更新。
在这种情况下,数据量是比较庞大的,使用普通 I/O 方式必然导致效率低下,所以才会采用存储映射I/O 方式。
3.3 LCD 应用编程
#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 <sys/mman.h>
#include <linux/fb.h>
#define argb8888_to_rgb565(color) ({ \
unsigned int temp = (color); \
((temp & 0xF80000UL) >> 8) | \
((temp & 0xFC00UL) >> 5) | \
((temp & 0xF8UL) >> 3); \
})
static int width; //LCD X分辨率
static int height; //LCD Y分辨率
static unsigned short *screen_base = NULL; //映射后的显存基地址
/********************************************************************
* 函数名称: lcd_draw_point
* 功能描述: 打点
* 输入参数: x, y, color
* 返 回 值: 无
********************************************************************/
static void lcd_draw_point(unsigned int x, unsigned int y, unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);//得到RGB565颜色值
/* 对传入参数的校验 */
if (x >= width)
x = width - 1;
if (y >= height)
y = height - 1;
/* 填充颜色 */
screen_base[y * width + x] = rgb565_color;
}
/********************************************************************
* 函数名称: lcd_draw_line
* 功能描述: 画线(水平或垂直线)
* 输入参数: x, y, dir, length, color
* 返 回 值: 无
********************************************************************/
static void lcd_draw_line(unsigned int x, unsigned int y, int dir,
unsigned int length, unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);//得到RGB565颜色值
unsigned int end;
unsigned long temp;
/* 对传入参数的校验 */
if (x >= width)
x = width - 1;
if (y >= height)
y = height - 1;
/* 填充颜色 */
temp = y * width + x;//定位到起点
if (dir) { //水平线
end = x + length - 1;
if (end >= width)
end = width - 1;
for ( ; x <= end; x++, temp++)
screen_base[temp] = rgb565_color;
}
else { //垂直线
end = y + length - 1;
if (end >= height)
end = height - 1;
for ( ; y <= end; y++, temp += width)
screen_base[temp] = rgb565_color;
}
}
/********************************************************************
* 函数名称: lcd_draw_rectangle
* 功能描述: 画矩形
* 输入参数: start_x, end_x, start_y, end_y, color
* 返 回 值: 无
********************************************************************/
static void lcd_draw_rectangle(unsigned int start_x, unsigned int end_x,
unsigned int start_y, unsigned int end_y,
unsigned int color)
{
int x_len = end_x - start_x + 1;
int y_len = end_y - start_y - 1;
lcd_draw_line(start_x, start_y, 1, x_len, color);//上边
lcd_draw_line(start_x, end_y, 1, x_len, color); //下边
lcd_draw_line(start_x, start_y + 1, 0, y_len, color);//左边
lcd_draw_line(end_x, start_y + 1, 0, y_len, color);//右边
}
/********************************************************************
* 函数名称: lcd_fill
* 功能描述: 将一个矩形区域填充为参数color所指定的颜色
* 输入参数: start_x, end_x, start_y, end_y, color
* 返 回 值: 无
********************************************************************/
static void lcd_fill(unsigned int start_x, unsigned int end_x,
unsigned int start_y, unsigned int end_y,
unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);//得到RGB565颜色值
unsigned long temp;
unsigned int x;
/* 对传入参数的校验 */
if (end_x >= width)
end_x = width - 1;
if (end_y >= height)
end_y = height - 1;
/* 填充颜色 */
temp = start_y * width; //定位到起点行首
for ( ; start_y <= end_y; start_y++, temp+=width) {
for (x = start_x; x <= end_x; x++)
screen_base[temp + x] = rgb565_color;
}
}
int main(int argc, char *argv[])
{
struct fb_fix_screeninfo fb_fix;
struct fb_var_screeninfo fb_var;
unsigned int screen_size;
int fd;
/* 打开framebuffer设备 */
if (0 > (fd = open("/dev/fb0", O_RDWR))) {
perror("open error");
exit(EXIT_FAILURE);
}
/* 获取参数信息 */
ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
ioctl(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_WRITE, MAP_SHARED, fd, 0);
if (MAP_FAILED == (void *)screen_base) {
perror("mmap error");
close(fd);
exit(EXIT_FAILURE);
}
/* 画正方形方块 */
int w = height * 0.25;//方块的宽度为1/4屏幕高度
lcd_fill(0, width-1, 0, height-1, 0x0); //清屏(屏幕显示黑色)
lcd_fill(0, w, 0, w, 0xFF0000); //红色方块
lcd_fill(width-w, width-1, 0, w, 0xFF00); //绿色方块
lcd_fill(0, w, height-w, height-1, 0xFF); //蓝色方块
lcd_fill(width-w, width-1, height-w, height-1, 0xFFFF00);//黄色方块
/* 画线: 十字交叉线 */
lcd_draw_line(0, height * 0.5, 1, width, 0xFFFFFF);//白色线
lcd_draw_line(width * 0.5, 0, 0, height, 0xFFFFFF);//白色线
/* 画矩形 */
unsigned int s_x, s_y, e_x, e_y;
s_x = 0.25 * width;
s_y = w;
e_x = width - s_x;
e_y = height - s_y;
for ( ; (s_x <= e_x) && (s_y <= e_y);
s_x+=5, s_y+=5, e_x-=5, e_y-=5)
lcd_draw_rectangle(s_x, e_x, s_y, e_y, 0xFFFFFF);
/* 退出 */
munmap(screen_base, screen_size); //取消映射
close(fd); //关闭文件
exit(EXIT_SUCCESS); //退出进程
}
四、串口应用开发
4.1 串口简介
串口全称叫做串行接口,串行接口指的是数据一个一个的按顺序传输,通信线路简单。使用两条线即可实现双向通信,一条用于发送,一条用于接收。串口通信距离远,但是速度相对会低,串口是一种很常用的工业接口。
4.1.1 终端 Terminal
终端就是处理主机输入、输出的一套设备,它用来显示主机运算的输出,并且接受主机要求的输入。分类如下:
- 本地终端:例如对于我们的个人 PC 机来说,PC 机连接了显示器、键盘以及鼠标等设备,这样的一个显示器/键盘组合就是一个本地终端
- 用串口连接的远程终端:对于嵌入式 Linux 开发来说,这是最常见的终端—串口终端。譬如我们的开发板通过串口线连接到一个带有显示器和键盘的 PC 机,在 PC 机通过运行一个终端模拟程序
- 基于网络的远程终端:譬如我们可以通过 ssh、Telnet 这些协议登录到一个远程主机。注意,不要把物理网卡当成终端关联的物理设备,它们与终端并不直接相关,所以这类不直接关联物理设备的终端叫做伪终端。
4.1.2 终端对应的设备节点
每一个终端在/dev 目录下都有一个对应的设备节点
- /dev/ttyX(X 是一个数字编号,譬如 0、1、2、3 等)设备节点:ttyX(teletype 的简称)是最令人熟悉的了,在 Linux 中,/dev/ttyX 代表的都是上述提到的本地终端,包括/dev/tty1~/dev/tty63 一共63 个本地终端,也就是连接到本机的键盘显示器可以操作的终端
- /dev/pts/X(X 是一个数字编号,譬如 0、1、2、3 等)设备节点:这类设备节点是伪终端对应的设备节点,也就是说,伪终端对应的设备节点都在/dev/pts 目录下、以数字编号命令。如ssh 或 Telnet 这些远程登录协议登录到开发板主机,那么开发板 Linux 系统会在/dev/pts 目录下生成一个设备节点
- 串口终端设备节点/dev/ttymxcX:对于 ALPHA/Mini I.MX6U 开发板来说,有两个串口,也就是有两个串口终端,对应两个设备节点
注意:mxc 这个名字不是一定的,这个名字的命名与驱动有关系(与硬件平台有关)
4.2 串口应用配置
串口设备节点为/dev/ttymxc0(UART1)和/dev/ttymxc2(UART3)。因为 Linux 为上层用户做了一层封装,将这些 ioctl()操作封装成了一套标准的 API。要使用 termios API,需要在我们的应用程序中包含termios.h头文件。对于配置来说,一个很重要的数据结构便是 struct termios 结构体:
4.2.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_iflag 成员的宏如下所示:
宏 | 功能 |
---|---|
IGNBRK | 忽略输入终止条件 |
BRKINT | 当检测到输入终止条件时发送 SIGINT 信号 |
IGNPAR | 忽略帧错误和奇偶校验错误 |
PARMRK | 对奇偶校验错误做出标记 |
INPCK | 对接收到的数据执行奇偶校验 |
ISTRIP | 将所有接收到的数据裁剪为 7 比特位、也就是去除第八位 |
INLCR | 将接收到的 NL(换行符)转换为 CR(回车符) |
IGNCR | 忽略接收到的 CR(回车符) |
ICRNL | 将接收到的 CR(回车符)转换为 NL(换行符) |
IUCLC | 将接收到的大写字符映射为小写字符 |
IXON | 启动输出软件流控 |
IXOFF | 启动输入软件流控 |
- 输出模式:c_oflag
输出模式控制输出字符的处理方式,即由应用程序发送出去的字符数据在传递到串口或屏幕之前是如何处理的。可用于 c_oflag 成员的宏如下所示:
宏 | 功能 |
---|---|
OPOST | 启用输出处理功能,如果不设置该标志则其他标志都被忽略 |
OLCUC | 将输出字符中的大写字符转换成小写字符 |
ONLCR | 将输出中的换行符(NL ‘\n’)转换成回车符(CR ‘\r’) |
OCRNL | 将输出中的回车符(CR ‘\r’)转换成换行符(NL ‘\n’) |
ONOCR | 在第 0 列不输出回车符(CR) |
ONLRET | 不输出回车符 |
OFILL | 发送填充字符以提供延时 |
OFDEL | 如果设置该标志,则表示填充字符为 DEL 字符,否则为 NULL字符 |
- 控制模式:c_cflag
可设置串口波特率、数据位、校验位、停止位等硬件特性。可用于 c_cflag 成员的标志如下所示:
CBAUD | 波特率的位掩码 |
---|---|
B0 | 波特率为 0 |
…… | …… |
B1200 | 1200 波特率 |
B1800 | 1800 波特率 |
B2400 | 2400 波特率 |
B4800 | 4800 波特率 |
B9600 | 9600 波特率 |
B19200 | 19200 波特率 |
B38400 | 38400 波特率 |
B57600 | 57600 波特率 |
B115200 | 115200 波特率 |
B230400 | 230400 波特率 |
B460800 | 460800 波特率 |
B500000 | 500000 波特率 |
B576000 | 576000 波特率 |
B921600 | 921600 波特率 |
B1000000 | 1000000 波特率 |
B1152000 | 1152000 波特率 |
B1500000 | 1500000 波特率 |
B2000000 | 2000000 波特率 |
B2500000 | 2500000 波特率 |
B3000000 | 3000000 波特率 |
…… | …… |
CSIZE | 数据位的位掩码 |
---|---|
CS5 | 5 个数据位 |
CS6 | 6 个数据位 |
CS7 | 7 个数据位 |
CS8 | 8 个数据位 |
CSTOPB | 2 个停止位,如果不设置该标志则默认是一个停止位 |
CREAD | 接收使能 |
PARENB | 使能奇偶校验 |
PARODD | 使用奇校验、而不是偶校验 |
HUPCL | 关闭时挂断调制解调器 |
CLOCAL | 忽略调制解调器控制线 |
CRTSCTS | 使能硬件流控 |
- 本地模式:c_lflag
本地模式用于控制终端的本地数据处理和工作模式。
模式 | 功能 |
---|---|
ISIG | 若收到信号字符(INTR、QUIT 等),则会产生相应的信号 |
ICANON | 启用规范模式 |
ECHO | 启用输入字符的本地回显功能。当我们在终端输入字符的时候,字符会显示出来,这就是回显功能 |
ECHOE | 若设置 ICANON,则允许退格操作 |
ECHOK | 若设置 ICANON,则 KILL 字符会删除当前行 |
ECHONL | 若设置 ICANON,则允许回显换行符 |
ECHOCTL | 若设置 ECHO,则控制字符(制表符、换行符等)会显示成“^X”, 其中 X 的 ASCII 码等于给相应控制字符的 ASCII 码加上 0x40。例如,退格字符(0x08)会显示为“^H”('H’的 ASCII 码为 0x48) |
ECHOPRT | 若设置 ICANON 和 IECHO,则删除字符(退格符等)和被删除的字符都会被显示 |
ECHOKE | 若设置 ICANON,则允许回显在 ECHOE 和 ECHOPRT 中设定的 KILL字符 |
NOFLSH | 在通常情况下,当接收到 INTR、QUIT 和 SUSP 控制字符时,会清空输入和输出队列。如果设置该标志,则所有的队列不会被清空 |
TOSTOP | 若一个后台进程试图向它的控制终端进行写操作,则系统向该后台进程的进程组发送 SIGTTOU 信号。该信号通常终止进程的执行 |
IEXTEN | 启用输入处理功能 |
- 特殊控制字符:c_cc
特殊控制字符是一些字符组合,如 Ctrl+C、Ctrl+Z 等,当用户键入这样的组合键,终端会采取特殊处理方式。struct termios 结构体中 c_cc数组将各种特殊字符映射到对应的支持函数。每个字符位置(数组下标)由对应的宏定义的,如下所示:
- VEOF:文件结尾符 EOF,对应键为 Ctrl+D;该字符使终端驱动程序将输入行中的全部字符传递给正在读取输入的应用程序。如果文件结尾符是该行的第一个字符,则用户程序中的 read 返回 0,表示文件结束。
- VEOL:附加行结尾符 EOL,对应键为 Carriage return(CR);作用类似于行结束符。
- VEOL2:第二行结尾符 EOL2,对应键为 Line feed(LF);
- VERASE:删除操作符 ERASE,对应键为 Backspace(BS);该字符使终端驱动程序删除输入行中的最后一个字符;
- VINTR:中断控制字符 INTR,对应键为 Ctrl+C;该字符使终端驱动程序向与终端相连的进程发送SIGINT 信号;
- VKILL:删除行符 KILL,对应键为 Ctrl+U,该字符使终端驱动程序删除整个输入行;
- VMIN:在非规范模式下,指定最少读取的字符数 MIN;
- VQUIT:退出操作符 QUIT,对应键为 Ctrl+Z;该字符使终端驱动程序向与终端相连的进程发送SIGQUIT 信号。
- VSTART:开始字符 START,对应键为 Ctrl+Q;重新启动被 STOP 暂停的输出。
- VSTOP:停止字符 STOP,对应键为 Ctrl+S;字符作用“截流”,即阻止向终端的进一步输出。用于支持 XON/XOFF 流控
- VSUSP:挂起字符 SUSP,对应键为 Ctrl+Z;该字符使终端驱动程序向与终端相连的进程发送SIGSUSP 信号,用于挂起当前应用程序。
- VTIME:非规范模式下,指定读取的每个字符之间的超时时间(以分秒为单位)TIME。
在以上所列举的这些宏定义中,TIME 和 MIN 值只能用于非规范模式,可用于控制非规范模式下 read()调用的一些行为特性
4.2.2 终端的三种工作模式
终端有三种工作模式,分别为规范模式(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()函数的调用方式。
(1)MIN = 0 和 TIME = 0:在这种情况下,read()调用总是会立即返回。若有可读数据,则读取数据并返回被读取的字节数;否则读取不到任何数据并返回 0。
(2)MIN > 0 和 TIME = 0:在这种情况下,read()函数会被阻塞,直到有 MIN 个字符可以读取时才返回,返回值是读取的字符数量。到达文件尾时返回 0。
(3)MIN = 0 和 TIME > 0:在这种情况下,只要有数据可读或者经过 TIME 个十分之一秒的时间,read()函数则立即返回,返回值为被读取的字节数。如果超时并且未读到数据,则 read()函数返回 0。
(4)MIN > 0 和 TIME > 0:在这种情况下,当有 MIN 个字节可读或者两个输入字符之间的时间间隔超过 TIME 个十分之一秒时,read()函数才返回。因为在输入第一个字符后系统才会启动定时器,所以,在这种情况下,read()函数至少读取一个字节后才返回。 - 原始模式是一种特殊的非规范模式。在原始模式下,所有的输入数据以字节为单位被处理。在这个模式下,终端是不可回显的,并且禁用终端输入和输出字符的所有特殊处理。在我们的应用程序中,可以通过调用 cfmakeraw()函数将终端设置为原始模式。
4.3 串口应用函数
4.3.1 打开串口设备:open()函数
int fd;
fd = open("/dev/ttymxc2", O_RDWR | O_NOCTTY);
if (0 > fd) {
perror("open error");
return -1;
}
4.3.2 获取终端当前的配置参数:tcgetattr()函数
#include <termios.h>
#include <unistd.h>
int tcgetattr(int fd, struct termios *termios_p);
调用 tcgetattr 函数之前,我们需要定义一个 struct termios 结构体变量,将该变量的指针作为 tcgetattr()函数的第二个参数传入;tcgetattr()调用成功后,会将终端当前的配置参数保存到 termios_p 指针所指的对象中。
函数调用成功返回 0;失败将返回-1,并且会设置 errno 以告知错误原因。
4.3.3 对串口终端进行配置
- 配置串口终端为原始模式
调用<termios.h>头文件中申明的 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(),这两个函数在<termios.h>头文件中申明。除了之外,我们还可以直接使用 cfsetspeed()函数一次性设置输入和输出波特率
cfsetispeed(&new_cfg, B115200);
cfsetospeed(&new_cfg, B115200);
cfsetspeed(&new_cfg, B115200);
- 设置数据位大小
首先将 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 设置为 0,这样则在任何情况下 read()调用都会立即返回,此时对串口的 read 操作会设置为非阻塞方式
new_cfg.c_cc[VTIME] = 0;
new_cfg.c_cc[VMIN] = 0;
4.3.4 缓冲区的处理
#include <termios.h>
#include <unistd.h>
int tcdrain(int fd);
int tcflush(int fd, int queue_selector);
int tcflow(int fd, int action);
- 调用 tcdrain()函数后会使得应用程序阻塞,直到串口输出缓冲区中的数据全部发送完毕为止!
- 调用 tcflow()函数会暂停串口上的数据传输或接收工作,具体情况取决于参数 action,参数 action 可取值如下:
- TCOOFF:暂停数据输出(输出传输);
- TCOON:重新启动暂停的输出;
- TCIOFF:发送 STOP 字符,停止终端设备向系统发送数据;
- TCION:发送一个 START 字符,启动终端设备向系统发送数据;
- 调用tcflush()函数会清空输入/输出缓冲区中的数据,具体情况取决于参数queue_selector,参数 queue_selector 可取值如下:
- TCIFLUSH:对接收到而未被读取的数据进行清空处理;
- TCOFLUSH:对尚未传输成功的输出数据进行清空处理;
- TCIOFLUSH:包括前两种功能,即对尚未处理的输入/输出数据进行清空处理。
- 通常我们会选择 tcdrain()或 tcflush()函数来对串口缓冲区进行处理。譬如直接调用 tcdrain()阻塞,或者调用 tcflush()清空缓冲区:
tcdrain(fd);
tcflush(fd, TCIOFLUSH);
4.3.5 写入配置、使配置生效:tcsetattr()函数
#include <termios.h>
#include <unistd.h>
int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);
调用该函数会将参数 termios_p 所指 struct termios 对象中的配置参数写入到终端设备中,使配置生效!而参数 optional_actions 可以指定更改何时生效,其取值如下:
- TCSANOW:配置立即生效。
- TCSADRAIN:配置在所有写入 fd 的输出都传输完毕之后生效。
- TCSAFLUSH:所有已接收但未读取的输入都将在配置生效之前被丢弃。
该函数调用成功时返回 0;失败将返回-1,、并设置 errno 以指示错误类型。譬如,调用 tcsetattr()将配置参数写入设备,使其立即生效:
tcsetattr(fd, TCSANOW, &new_cfg);
4.4 串口应用编程示例
#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;
}
/***
--dev=/dev/ttymxc2
--brate=115200
--dbit=8
--parity=N
--sbit=1
--type=read
***/
/**
** 打印帮助信息
**/
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);
}
五、网络应用编程
5.1 网络通信概述
网络通信本质上是一种进程间通信,是位于网络中不同主机上的进程之间的通信,属于 IPC 的一种,通常称为 socket IPC.所以网络通信是为了解决在网络环境中,不同主机上的应用程序之间的通信问题。大概可以分为三个层次:
- 硬件层:网卡设备,收发网络数据
- 驱动层:网卡驱动(Linux 内核网卡驱动代码)
- 应用层:上层应用程序(调用 socket 接口或更高级别接口实现网络相关应用程序)
5.1.1 网络互连模型
- 应用层(Application Layer)是 OSI 参考模型中的最高层,是最靠近用户的一层,为上层用户提供应用接口,也为用户直接提供各种网络服务。我们常见应用层的网络服务协议有:HTTP、FTP、TFTP、SMTP、SNMP、DNS、TELNET、HTTPS、POP3、DHCP。
- 表示层(Presentation Layer)提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别。如果必要,该层可提供一种标准表示形式,用于将计算机内部的多种数据格式转换成通信中采用的标准表示形式。数据压缩/解压缩和加密/解密(提供网络的安全性)也是表示层可提供的功能之一。
- 会话层(Session Layer)对应主机进程,指本地主机与远程主机正在进行的会话。会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成。将不同实体之间表示层的连接称为会话。因此会话层的任务就是组织和协调两个会话进程之间的通信,并对数据交换进行管理。
- 传输层(Transport Layer)定义传输数据的协议端口号,以及端到端的流控和差错校验。该层建立了主机端到端的连接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括差错校验处理和流控等问题。我们通常说的,TCP、UDP 协议就工作在这一层,端口号既是这里的“端”。
- 网络层进行逻辑地址寻址,实现不同网络之间的路径选择。本层通过 IP 寻址来建立两个节点之间的连接,为源端发送的数据包选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。网络层(Network Layer)也就是通常说的 IP 层。该层包含的协议有:IP(Ipv4、Ipv6)、ICMP、IGMP 等。
- 数据链路层(Data Link Layer)是 OSI 参考模型中的第二层,负责建立和管理节点间逻辑连接、进行硬件地址寻址、差错检测等功能。将比特组合成字节进而组合成帧,用 MAC 地址访问介质,错误发现但不能纠正。
数据链路层又分为 2 个子层:逻辑链路控制子层(LLC)和媒体访问控制子层(MAC)。MAC 子层的主要任务是解决共享型网络中多用户对信道竞争的问题,完成网络介质的访问控制;LLC 子层的主要任务是建立和维护网络连接,执行差错校验、流量控制和链路控制。
数据链路层的具体工作是接收来自物理层的位流形式的数据,并封装成帧,传送到上一层;同样,也将来自上层的数据帧,拆装为位流形式的数据转发到物理层;并且,还负责处理接收端发回的确认帧的信息,以便提供可靠的数据传输。 - 物理层(Physical Layer)是 OSI 参考模型的最低层,物理层的主要功能是:利用传输介质为数据链路层提供物理连接,实现比特流的透明传输,物理层的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。使数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的。
5.1.2 数据的封装与拆封
网络通信中,数据从上层到下层交付时,要进行封装;同理,当目标主机接收到数据时,数据由下层传递给上层时需要进行拆封。这就是数据的封装与拆封。
当用户发送数据时,将数据向下交给传输层,但是在交给传输层之前,应用层相关协议会对用户数据进行封装,譬如 MQTT、HTTP 等协议,其实就是在用户数据前添加一个应用程序头部,这是处于应用层的操作,最后应用层通过调用传输层接口来将封装好的数据交给传输层。
传输层会在数据前面加上传输层首部(此处以 TCP 协议为例,图中的传输层首部为 TCP 首部,也可以是 UDP 首部),然后向下交给网络层。
同样地,网络层会在数据前面加上网络层首部(IP 首部),然后将数据向下交给链路层,链路层会对数据进行最后一次封装,即在数据前面加上链路层首部(此处使用以太网接口为例,对应以太网首部),然后将数据交给网卡。
最后,由网卡硬件设备将数据转换成物理链路上的电平信号,数据就这样被发送到了网络中。这就是网络数据的发送过程,从图中可以看到,各层协议均会对数据进行相应的封装,可以概括为 TCP/IP 模型中的各层协议对数据进行封装的过程。
5.2 TCP/IP 协议
TCP/IP 协议它其实是一个协议族,包含了众多的协议,譬如应用层协议 HTTP、FTP、MQTT…以及传输层协议 TCP、UDP 等这些都属于 TCP/IP 协议
5.2.1 IP 地址
Internet 依靠 TCP/IP 协议,在全球范围内实现不同硬件结构、不同操作系统、不同网络系统的主机之间的互联。在 Internet 上,每一个节点都依靠唯一的 IP 地址相互区分和相互联系,IP 地址用于标识互联网中的每台主机的身份,设计人员为每个接入网络中的主机都分配一个 IP 地址(Internet Protocol Address),只有合法的 IP 地址才能接入互联网中并且与其他主机进行网络通信,IP 地址是软件地址,不是硬件地址,硬件 MAC 地址是存储在网卡中的,应用于局域网中寻找目标主机。
-
IP 地址的分类
根据 IP 地址中网络地址和主机地址两部分分别占多少位的不同,将 IP 地址划分为 5 类,分别为 A、B、C、D、E 五类,如下所示:
A 类地址的第一组数字为 1~126。 B 类地址的第一组数字为 128~191。 C 类地址的第一组数字为 192~223。
A 类地址的表示范围为:0.0.0.0~126.255.255.255,默认网络掩码为:255.0.0.0;A 类地址分配给规模特别大的网络使用。A 类地址用第一组数字表示网络地址,后面三组数字作为连接于网络上的主机对应的地址。分配给具有大量主机而局域网络个数较少的大型网络,譬如 IBM 公司的网络。
B 类地址的表示范围为:128.0.0.0~191.255.255.255,默认网络掩码为:255.255.0.0;B 类地址分配给一般的中型网络。B 类地址用第一、二组数字表示网络地址,后面两组数字代表网络上的主机地址。
C类地址的表示范围为:192.0.0.0~223.255.255.255,默认网络掩码为:255.255.255.0;C 类地址分配给小型网络,如一般的局域网和校园网,它可连接的主机数量是最少的,采用把所属的用户分为若干的网段进行管理。C 类地址用前三组数字表示网络地址,最后一组数字作为网络上的主机地址。 -
特殊的 IP 地址
①其中在 A、B、C 三类地址中,各保留了一个区域作为私有地址:
A 类地址:10.0.0.0~10.255.255.255
B 类地址:172.16.0.0~172.31.255.255
C 类地址:192.168.0.0~192.168.255.255
②直接广播(Direct Broadcast Address):向某个网络上所有的主机发送报文
A 类地址的广播地址为:XXX.255.255.255
B 类地址的广播地址为:XXX.XXX.255.255
C 类地址的广播地址为:XXX.XXX.XXX.255
③受限广播地址:255.255.255.255
④多播地址
多播地址用在一对多的通信中,即一个发送者,多个接收者,不论接受者数量的多少,发送者只发送一次数据包。多播地址属于 D 类地址,D 类地址只能用作目的地址,而不能作为主机中的源地址。
⑤环回地址
环回地址(Loopback Address)是用于网络软件测试以及本机进程之间通信的特殊地址。把 A 类地址中的 127.XXX.XXX.XXX 的所有地址都称为环回地址,主要用来测试网络协议是否工作正常的作用。比如在电脑中使用 ping 命令去 ping 127.1.1.1 就可以测试本地 TCP/IP 协议是否正常。
⑥0.0.0.0 地址
IP 地址 32bit 全为 0 的地址(也就是 0.0.0.0)表示本网络上的本主机,只能用作源地址。0.0.0.0 是不能被 ping 通的,在服务器中,0.0.0.0 并不是一个真实的的 IP 地址,它表示本机中所有的IPv4 地址。监听 0.0.0.0 的端口,就是监听本机中所有 IP 的端口。 -
如何判断 2 个 IP 地址是否在同一个网段内
可通过网络标识来进行判断,网络标识定义:
网络标识 = IP 地址 & 子网掩码
2 个 IP 地址的网络标识相同,那么它们就处于同一网络。譬如 192.168.1.50 和 192.168.1.100,这 2 个都是 C 类地址,对应的子网掩码为 255.255.255.0,很明显,这两个 IP 地址与子网掩码进行按位与操作时得到的结果(网络标识)是一样的,所以它们处于同一网络。
5.2.2 HTTP 协议
HTTP 超文本传输协议(英文:HyperText Transfer Protocol,缩写:HTTP)是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP 是万维网数据通信的基础。HTTP 的应用最为广泛,譬如大家经常会打开网页浏览器查询资料,通过浏览器便可开启 HTTP 通信。
HTTP 协议工作于客户端(用户)、服务器端(网站)模式下,浏览器作为 HTTP 客户端通过 URL 向HTTP 服务端即 WEB 服务器发送请求。Web 服务器根据接收到的请求后,向客户端发送响应信息。借助这种浏览器和服务器之间的 HTTP 通信,我们能够足不出户地获取网络中的各种信息。
5.2.3 FTP 协议
FTP 协议的英文全称为 File Transfer Protocol,简称为 FTP,它是一种文件传输协议,从一个主机向一个主机传输文件的协议。FTP 协议同样也是基于客户端-服务器模式,在客户端和服务器之间进行文件传输,譬如我们通常会使用 FTP 协议在两台主机之间进行文件传输,譬如一台 Ubuntu 系统主机和一台 Windows系统主机,将一台主机作为 FTP 服务器、另一台主机作为 FTP 客户端,建立 FTP 连接之后,客户端可以从服务器下载文件,同样也可以将文件上传至服务器。
FTP 除了基本的文件上传/下载功能外,还有目录操作、权限设置、身份验证等机制,许多网盘的文件传输功能都是基于 FTP 实现的。
5.2.4 UDP 协议
UDP 是 User Datagram Protocol 的简称,中文名是用户数据报协议,是一种无连接、不可靠的协议,同样它也是工作在传顺层。它只是简单地实现从一端主机到另一端主机的数据传输功能,这些数据通过 IP 层发送,在网络中传输,到达目标主机的顺序是无法预知的,因此需要应用程序对这些数据进行排序处理,这就带来了很大的不方便,此外,UDP 协议更没有流量控制、拥塞控制等功能,在发送的一端,UDP 只是把上层应用的数据封装到 UDP 报文中,在差错检测方面,仅仅是对数据进行了简单的校验,然后将其封装到 IP 数据报中发送出去。而在接收端,无论是否收到数据,它都不会产生一个应答发送给源主机,并且如果接收到数据发送校验错误,那么接收端就会丢弃该UDP 报文,也不会告诉源主机,这样子传输的数据是无法保障其准确性的,如果想要其准确性,那么就需要应用程序来保障了。UDP 协议的特点:
- 无连接、不可靠;
- 尽可能提供交付数据服务,出现差错直接丢弃,无反馈;
- 面向报文,发送方的 UDP 拿到上层数据直接添加个 UDP 首部,然后进行校验后就递交给 IP 层,而接收的一方在接收到 UDP 报文后简单进行校验,然后直接去除数据递交给上层应用;
- 速度快,因为 UDP 协议没有 TCP 协议的握手、确认、窗口、重传、拥塞控制等机制,UDP 是一个无状态的传输协议,所以它在传递数据时非常快,即使在网络拥塞的时候 UDP 也不会降低发送的数据。
UDP 虽然有很多缺点,但也有自己的优点,所以它也有很多的应用场合,因为在如今的网络环境下,UDP 协议传输出现错误的概率是很小的,并且它的实时性是非常好,常用于实时视频的传输,比如直播、网络电话等,因为即使是出现了数据丢失的情况,导致视频卡帧,这也不是什么大不了的事情,所以,UDP协议还是会被应用与对传输速度有要求,并且可以容忍出现差错的数据传输中。
5.3 TCP 协议
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于 IP 的传输协议。TCP 协议工作在传输层,对上服务 socket 接口,对下调用 IP 层(网络层)。关于 TCP 协议我们需要理解的重点如下:
- TCP 协议工作在传输层,对上服务 socket 接口,对下调用 IP 层;
- TCP 是一种面向连接的传输协议,通信之前必须通过三次握手与客户端建立连接关系后才可通信;
- TCP 协议提供可靠传输,不怕丢包、乱序。
5.3.1 TCP 报文格式
当数据由上层发送到传输层时,数据会被封装为 TCP 数据段,我们将其称为 TCP 报文(或 TCP 报文段),TCP 报文由 TCP 首部+数据区域组成,一般 TCP 首部通常为 20 个字节大小
- 源端口号和目标端口号
各占 2 个字节,一个 4 个字节,关于端口号的概念会在 29.5.3 小节进行介绍。每个 TCP 报文都包含源主机和目标主机的端口号,用于寻找发送端和接收端应用进程,这两个值加上 IP 首部中的源 IP 地址和目标 IP 地址就能确定唯一一个 TCP 连接。有时一个 IP 地址和一个端口号也称为 socket(插口)。 - 序号
占 4 个字节,用来标识从 TCP 发送端向 TCP 接收端发送的数据字节流,它的值表示在这个报文段中的第一个数据字节所处位置码,根据接收到的数据区域长度,就能计算出报文最后一个数据所处的序号,因为TCP 协议会对发送或者接收的数据进行编号(按字节的形式),那么使用序号对每个字节进行计数,就能很轻易管理这些数据。
在 TCP 传送的数据流中,每一个字节都有一个序号。例如,一报文段的序号为 300,而且数据共 100 字节,则下一个报文段的序号就是 400;序号是 32bit 的无符号数,序号到达 2^32-1 后从 0 开始。 - 确认序号
确认序号占 4 字节,是期望收到对方下次发送的数据的第一个字节的序号,也就是期望收到的下一个报文段的首部中的序号;确认序号应该是上次已成功收到数据字节序号+1。只有 ACK 标志为 1 时,确认序号才有效。TCP 为应用层提供全双工服务,这意味数据能在两个方向上独立地进行传输,因此确认序号通常会与反向数据(即接收端传输给发送端的数据)封装在同一个报文中(即捎带),所以连接的每一端都必须保持每个方向上的传输数据序号准确性。 - 首部长度
首部长度字段占 4 个 bit 位,它指出了 TCP 报文段首部长度,以字节为单位,最大能记录 15*4=60 字节的首部长度,因此,TCP 报文段首部最大长度为 60 字节。在字段后接下来有 6bit 空间是保留未用的,供以后应用,现在置为 0。 - 6 个标志位:URG/ACK/PSH/RST/SYN/FIN
- URG:首部中的紧急指针字段标志,如果是 1 表示紧急指针字段有效。
- ACK:只有当 ACK=1 时,确认序号字段才有效。
- PSH:当PSH=1 时,接收方应该尽快将本报文段立即传送给其应用层。
- RST:当 RST=1时,表示出现连接错误,必须释放连接,然后再重建传输连接。复位比特还用来拒绝一个不法的报文段或拒绝打开一个连接。
- SYN:SYN=1,ACK=0 时表示请求建立一个连接,携带 SYN 标志的 TCP 报文段为同步报文段。
- FIN:为 1表示发送方没有数据要传输了,要求释放连接。
- 窗口大小
占用 2 个字节大小,表示从确认号开始,本报文的发送方可以接收的字节数,即接收窗口大小,用于流量控制。 - 校验和
对整个的 TCP 报文段,包括 TCP 首部和 TCP 数据,以 16 位字进行计算所得。这是一个强制性的字段。 - 紧急指针
本报文段中的紧急数据的最后一个字节的序号。 - 选项
选项字段的大小是不确定的,最多 40 字节。
5.3.2 建立TC连接:三次握手
- 第一次握手:客户端将 TCP 报文标志位 SYN 置为 1,随机产生一个序号值 seq=J,保存在 TCP 首部的序列号(Sequence Number)字段里,指明客户端打算连接的服务器的端口,并将该数据包发送给服务器端,发送完毕后,客户端进入 SYN_SENT 状态,等待服务器端确认。
- 第二次握手:服务器端收到数据包后由标志位 SYN=1 知道客户端请求建立连接,服务器端将 TCP 报文标志位 SYN和 ACK 都置为 1,ack=J+1,随机产生一个序号值 seq=K,并将该数据包发送给客户端以确认连接请求,服务器端进入 SYN_RCVD 状态。
- 第三次握手:客户端收到确认后,检查 ack 是否为 J+1,ACK 是否为 1,如果正确则将标志位 ACK 置为 1,ack=K+1,并将该数据包发送给服务器端,服务器端检查 ack 是否为 K+1,ACK 是否为 1,如果正确则连接建立成功,客户端和服务器端进入 ESTABLISHED 状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。
5.3.3 关闭 TCP 连接:四次挥手
- 第一次挥手:Client 端发起挥手请求,向 Server 端发出一个 FIN 报文段主动进行关闭连接,此时报文段的 FIN 标志位被设置为 1。此时,Client 端进入 FIN_WAIT_1 状态,这表示 Client 端没有数据要发送给 Server 端了。
- 第二次挥手:Server 端收到了 Client 端发送的 FIN 报文段,向 Client 端返回一个 ACK 报文段,此时报文段的 ACK标志位被设置为 1。ack 设为 seq 加 1,Client 端进入 FIN_WAIT_2 状态,Server 端告诉 Client 端,我确认并同意你的关闭请求。
- 第三次挥手:Server 端向 Client 端发送一个 FIN 报文段请求关闭连接,此时报文段的 FIN 标志位被设置为 1,同时
Client 端进入 LAST_ACK 状态。 - 第四次挥手:Client 端收到 Server 端发送的 FIN 报文段后,向 Server 端发送 ACK 报文段(此时报文段的 ACK 标志位被设置为 1),然后 Client 端进入 TIME_WAIT 状态。Server 端收到 Client 端的 ACK 报文段以后,就关闭连接。此时,Client 端等待 2MSL 的时间后依然没有收到回复,则证明 Server 端已正常关闭,那好,Client端也可以关闭连接了。
5.4 端口号的概念
互联网中的每一台主机都需要一个唯一的 IP 地址以标识自己的身份,网络中传输的数据包通过 IP 地址找到对应的目标主机;一台主机通常只有一个 IP 地址,但主机上运行的网络进程却通常不止一个,譬如 Windows 电脑上运行着 QQ、微信、钉钉、网页浏览器等,这些进程都需要进行网络连接,它们都可通过网络发送/接收数据,那么这里就有一个问题?主机接收到网络数据之后,如何确定该数据是哪个进程对应的接收数据呢?其实就是通常端口号来确定的。
端口号本质上就是一个数字编号,用来在一台主机中唯一标识一个能上网(能够进行网络通信)的进程,端口号的取值范围为 0~65535。一台主机通常只有一个 IP 地址,但是可能有多个端口号,每个端口号表示一个能上网的进程。一台拥有 IP 地址的主机可以提供许多服务,比如 Web 服务、FTP 服务、SMTP 服务等,这些服务都是能够进行网络通信的进程,IP 地址只能区分网络中不同的主机,并不能区分主机中的这些进程,显然不能只靠 IP 地址,因此才有了端口号。通过“IP 地址+端口号”来区分主机不同的进程。
很多常见的服务器它都有特定的端口号,具体详情如下表所示:
服务 | 端口号 | 说明 |
---|---|---|
HTTP服务 | 80 | 超文本传输协议 |
FTP 服务 | 21 | 文件传输协议,使得主机间可以共享文件 |
SMTP 服务 | 25 | 简单邮件传输协议,它帮助每台计算机在发送或中转信件时找到下一个目的地。 |
TFTP 服务 | 69 | 简单文件传输协议,主机之间进行简单文件传输 |
SSH 服务 | 22 | 安全外壳协议,专为远程登录会话和其他网络服务提供安全性的协议 |
Telnet 服务 | 23 | 终端远程登录协议,它为用户提供了在本地计算机上完成远程主机工作的能力。 |
POP3 服务 | 110 | 邮局协议版本 3,本协议主要用于支持使用客户端远程管理在服务器上的电子邮件 |
5.5 socket 编程基础
5.5.1 socket 简介
套接字(socket)是 Linux 下的一种进程间通信机制(socket IPC),在前面的内容中已经给大家提到过,使用 socket IPC 可以使得在不同主机上的应用程序之间进行通信(网络通信),当然也可以是同一台主机上的不同应用程序。socket IPC 通常使用客户端<—>服务器这种模式完成通信,多个客户端可以同时连接到服务器中,与服务器之间完成数据交互。
内核向应用层提供了 socket 接口,对于应用程序开发人员来说,我们只需要调用 socket 接口开发自己的应用程序即可!socket 是应用层与 TCP/IP 协议通信的中间软件抽象层,它是一组接口。在设计模式中,socket 其实就是一个门面模式,它把复杂的 TCP/IP 协议隐藏在 socket 接口后面,对用户来说,一组简单的接口就是全部,让 socket 去组织数据,以符合指定的协议。所以,我们无需深入的去理解 tcp/udp 等各种复杂的 TCP/IP 协议,socket 已经为我们封装好了,我们只需要遵循 socket 的规定去编程,写出的程序自然遵循 tcp/udp 标准的。
当前网络中的主流程序设计都是使用 socket 进行编程的,因为它简单易用,它还是一个标准(BSD socket),能在不同平台很方便移植,比如你的一个应用程序是基于 socket 接口编写的,那么它可以移植到任何实现 BSD socket 标准的平台,譬如 LwIP,它兼容 BSD Socket;又譬如 Windows,它也实现了一套基于socket 的套接字接口,更甚至在国产操作系统中,如 RT-Thread,它也实现了 BSD socket 标准的 socket 接口。
5.5.2 编程接口介绍
使用 socket 接口需要在我们的应用程序代码中包含两个头文件:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
- socket()函数
类似于 open()函数,它用于创建一个网络通信端点(打开一个网络通信),如果成功则返回一个网络文件描述符
int socket(int domain, int type, int protocol);
- 参数 domain :用于指定一个通信域;这将选择将用于通信的协议族,对于 TCP/IP 协议来说,通常选择 AF_INET 就可以了,当然如果你的 IP 协议的版本支持 IPv6,那么可以选择 AF_INET6。可选的协议族如下表所示:
协议族名字 | 说明 | 帮助信息 |
---|---|---|
AF_UNIX, AF_LOCAL | Local communication | unix(7) |
AF_INET | IPv4 Internet protocols | ip(7) |
AF_INET6 | IPv6 Internet protocols | ipv6(7) |
AF_IPX | IPX - Novell protocols | |
AF_NETLINK | Kernel user interface device | netlink(7) |
AF_X25 | ITU-T X.25/ISO-8208 protocol | x25(7) |
AF_AX25 | Amateur radio AX.25 protocol | |
AF_ATMPVC | Access to raw ATM PVCs | |
AF_APPLETALK | AppleTalk | ddp(7) |
AF_PACKET | Low level packet interface | packet(7) |
AF_ALG | Interface to kernel crypto API |
- 参数 type :指定套接字的类型
type | 说明 |
---|---|
SOCK_STREAM | 提供有序的、可靠的、双向的、基于连接的字节流,能保证数据正确传送到对方,用于 TCP 协议;可以支持带外数据传输机制。 |
SOCK_DGRAM | 固定长度的、无连接的、不可靠的报文传递,用于 UDP 协议 |
SOCK_SEQPACKET | 固定长度的、有序的、可靠的、面向连接的报文传递 |
SOCK_RAW | 表示原始套接字,它允许应用程序访问网络层的原始数据包,这个套接字用得比较少,暂时不用理会它。 |
SOCK_RDM | 提供不保证排序的可靠数据报层。 |
SOCK_PACKET | 已过时,不应在应用程序中使用 |
- 参数 protocol:通常设置为 0,表示为给定的通信域和套接字类型选择默认协议。当对同一域和套接字类型支持多个协议时,可以使用 protocol 参数选择一个特定协议。在 AF_INET 通信域中,套接字类型为SOCK_STREAM 的默认协议是传输控制协议(Transmission Control Protocol,TCP 协议)。在 AF_INET 通信域中,套接字类型为 SOCK_DGRAM 的默认协议时 UDP。
- bind()函数
用于将一个 IP 地址或端口号与一个套接字进行绑定,成功返回 0,失败情况下返回- 1,并设置 errno 以提示错误原因。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 参数 addr:是一个指针,指向一个 struct sockaddr 类型变量。里面有sa_data数组,一共 14 个字节,在这 14 个字节中就包括了 IP 地址、端口号等信息
- listen()函数
只能在服务器进程中使用,让服务器进程进入监听状态,等待客户端的连接请求。无法在一个已经连接的套接字上执行 listen()。
int listen(int sockfd, int backlog);
- 参数 backlog 用来描述 sockfd 的等待连接队列能够达到的最大值
- accept()函数
获取客户端的连接请求并建立连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数 addr 是一个传出参数,参数 addr 用来返回已连接的客户端的 IP 地址与端口号等这些信息。参数addrlen 应设置为 addr 所指向的对象的字节长度,如果我们对客户端的 IP 地址与端口号这些信息不感兴趣,可以把 arrd 和 addrlen 均置为空指针 NULL。
accept()函数通常只用于服务器应用程序中,为了能够正常让客户端能正常连接到服务器,服务器必须遵循以下处理流程:
- 调用 socket()函数打开套接字;
- 调用 bind()函数将套接字与一个端口号以及 IP 地址进行绑定;
- 调用 listen()函数让服务器进程进入监听状态,监听客户端的连接请求;
- 调用 accept()函数处理到来的连接请求。
- connect()函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
该函数用于客户端应用程序中,客户端调用 connect()函数将套接字 sockfd 与远程服务器进行连接,参数 addr 指定了待连接的服务器的 IP 地址以及端口号等信息,参数 addrlen 指定了 addr 指向的 struct sockaddr对象的字节大小。
客户端通过 connect()函数请求与服务器建立连接,对于 TCP 连接来说,调用该函数将发生 TCP 连接的握手过程,并最终建立一个 TCP 连接,而对于 UDP 协议来说,调用这个函数只是在 sockfd 中记录服务器IP 地址与端口号,而不发送任何数据。
- 发送和接收函数
一旦客户端与服务器建立好连接之后,我们就可以通过套接字描述符来收发数据了(对于客户端使用socket()返回的套接字描述符,而对于服务器来说,需要使用 accept()返回的套接字描述符),这与我们读写普通文件是差不多的操作,譬如可以调用 read()或 recv()函数读取网络数据,调用 write()或 send()函数发送数据。当不再需要套接字描述符时,可调用 close()函数来关闭套接字,释放相应的资源。
- recv()函数
可以通过指定 flags 标志来控制如何接收数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
标志 | 描述 |
---|---|
MSG_CMSG_CLOEXEC | 为 UNIX 域套接字上接收的文件描述符设置执行时关闭标志 |
MSG_DONTWAIT | 启动非阻塞操作(相当于 O_NONBLOCK) |
MSG_ERRQUEUE | 接收错误信息作为辅助数据 |
MSG_OOB | 如果协议支持,获取带外数据 |
MSG_PEEK | 返回数据包内容而不真正取走数据包 |
MSG_TRUNC | 即使数据包被截断,也返回数据包的长度 |
MSG_WAITALL | 等待知道所有的数据可用(仅 SOCK_STREAM) |
- send()函数
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
标志 | 描述 |
---|---|
MSG_CONFIRM | 提供链路层反馈以保持地址映射有效 |
MSG_DONTROUTE | 勿将数据包路由出本地网络 |
MSG_DONTWAIT | 允许非阻塞操作(等价于使用 O_NONBLOCK) |
MSG_EOR | 如果协议支持,标志记录结束 |
MSG_MORE | 延迟发送数据包允许写更多数据 |
MSG_NOSIGNAL | 在写无连接的套接字时不产生 SIGPIPE 信号 |
MSG_OOB | 如果协议支持,发送带外数据 |
5.6 IP 地址格式转换函数
对于人来说,我们更容易阅读的是点分十进制的 IP 地址,譬如 192.168.1.110、192.168.1.50,这其实是一种字符串的形式,但是计算机所需要理解的是二进制形式的 IP 地址,所以我们就需要在点分十进制字符串和二进制地址之间进行转换。
- 点分十进制字符串和二进制地址之间的转换函数主要有:inet_aton、inet_addr、inet_ntoa它们需要包含头文件<sys/socket.h>以及<netinet/in.h>,这些函数已经淘汰了
- inet_ntop、inet_pton 函数只需包含<arpa/inet.h>头文件即可!
int inet_pton(int af, const char *src, void *dst)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
5.6.1 inet_pton()函数
inet_pton()函数将点分十进制表示的字符串形式转换成二进制 Ipv4 或 Ipv6 地址,inet_ntop()函数执行与 inet_pton()相反的操作
将字符串 src 转换为二进制地址,参数 af 必须是 AF_INET 或 AF_INET6,AF_INET 表示待转换的 Ipv4地址,AF_INET6 表示待转换的是 Ipv6 地址;并将转换后得到的地址存放在参数 dst 所指向的对象中,如果参数 af 被指定为 AF_INET,则参数 dst 所指对象应该是一个 struct in_addr 结构体的对象;如果参数 af 被指定为 AF_INET6,则参数 dst 所指对象应该是一个 struct in6_addr 结构体的对象。
inet_pton()转换成功返回 1(已成功转换)。如果 src 不包含表示指定地址族中有效网络地址的字符串,则返回 0。如果 af 不包含有效的地址族,则返回-1 并将 errno 设置为 EAFNOSUPPORT。
5.6.2 inet_ntop()函数
参数 af 与 inet_pton()函数的 af 参数意义相同。
参数 src 应指向一个 struct in_addr 结构体对象或 struct in6_addr 结构体对象,依据参数 af 而定。函数inet_ntop()会将参数 src 指向的二进制 IP 地址转换为点分十进制形式的字符串,并将字符串存放在参数 dts
所指的缓冲区中,参数 size 指定了该缓冲区的大小。
inet_ntop()在成功时会返回 dst 指针。如果 size 的值太小了,那么将会返回 NULL 并将 errno 设置为
ENOSPC。
5.7 socket 编程实战
5.7.1 编写服务器程序
①调用 socket()函数打开套接字,得到套接字描述符;
②调用 bind()函数将套接字与 IP 地址、端口号进行绑定;
③调用 listen()函数让服务器进程进入监听状态;
④调用 accept()函数获取客户端的连接请求并建立连接;
⑤调用 read/recv、write/send 与客户端进行通信;
⑥调用 close()关闭套接字。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define SERVER_PORT 8888 //端口号不能发生冲突,不常用的端口号通常大于5000
int main(void)
{
struct sockaddr_in server_addr = {0};
struct sockaddr_in client_addr = {0};
char ip_str[20] = {0};
int sockfd, connfd;
int addrlen = sizeof(client_addr);
char recvbuf[512];
int ret;
/* 打开套接字,得到套接字描述符 */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
/* 将套接字与指定端口号进行绑定 */
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERVER_PORT);
ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (0 > ret) {
perror("bind error");
close(sockfd);
exit(EXIT_FAILURE);
}
/* 使服务器进入监听状态 */
ret = listen(sockfd, 50);
if (0 > ret) {
perror("listen error");
close(sockfd);
exit(EXIT_FAILURE);
}
/* 阻塞等待客户端连接 */
connfd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);
if (0 > connfd) {
perror("accept error");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("有客户端接入...\n");
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip_str, sizeof(ip_str));
printf("客户端主机的IP地址: %s\n", ip_str);
printf("客户端进程的端口号: %d\n", client_addr.sin_port);
/* 接收客户端发送过来的数据 */
for ( ; ; ) {
// 接收缓冲区清零
memset(recvbuf, 0x0, sizeof(recvbuf));
// 读数据
ret = recv(connfd, recvbuf, sizeof(recvbuf), 0);
if(0 >= ret) {
perror("recv error");
close(connfd);
break;
}
// 将读取到的数据以字符串形式打印出来
printf("from client: %s\n", recvbuf);
// 如果读取到"exit"则关闭套接字退出程序
if (0 == strncmp("exit", recvbuf, 4)) {
printf("server exit...\n");
close(connfd);
break;
}
}
/* 关闭套接字 */
close(sockfd);
exit(EXIT_SUCCESS);
}
5.7.2 编写客户端程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define SERVER_PORT 8888 //服务器的端口号
#define SERVER_IP "192.168.1.150" //服务器的IP地址
int main(void)
{
struct sockaddr_in server_addr = {0};
char buf[512];
int sockfd;
int ret;
/* 打开套接字,得到套接字描述符 */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
/* 调用connect连接远端服务器 */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT); //端口号
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);//IP地址
ret = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (0 > ret) {
perror("connect error");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("服务器连接成功...\n\n");
/* 向服务器发送数据 */
for ( ; ; ) {
// 清理缓冲区
memset(buf, 0x0, sizeof(buf));
// 接收用户输入的字符串数据
printf("Please enter a string: ");
fgets(buf, sizeof(buf), stdin);
// 将用户输入的数据发送给服务器
ret = send(sockfd, buf, strlen(buf), 0);
if(0 > ret){
perror("send error");
break;
}
//输入了"exit",退出循环
if(0 == strncmp(buf, "exit", 4))
break;
}
close(sockfd);
exit(EXIT_SUCCESS);
}
六、CAN应用编程
can工作原理可以参照单片机
6.1 SocketCan 应用编程
6.1.1 创建 socket 套接字
CAN 总线套接字的创建采用标准的网络套接字操作来完成,网络套接字在头文件<sys/socket.h>中定义
int sockfd = -1;
/* 创建套接字 */
sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if(0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
socket 函数在之前介绍过,第一个参数用于指定通信域,在 SocketCan 中,通常将其设置为PF_CAN,指定为CAN通信协议;第二个参数用于指定套接字的类型,通常将其设置为SOCK_RAW;第三个参数通常设置为 CAN_RAW。
6.1.2 将套接字与 CAN 设备进行绑定
strcpy(ifr.ifr_name, "can0"); //指定名字
ioctl(sockfd, SIOCGIFINDEX, &ifr);
can_addr.can_family = AF_CAN; //填充数据
can_addr.can_ifindex = ifr.ifr_ifindex;
/* 将套接字与 can0 进行绑定 */
ret = bind(sockfd, (struct sockaddr *)&can_addr, sizeof(can_addr));
if (0 > ret) {
perror("bind error");
close(sockfd);
exit(EXIT_FAILURE);
}
struct ifreq 和 struct sockaddr_can,其中 struct ifreq 定义在<net/if.h>头文件中,而 struct sockaddr_can 定义在<linux/can.h>头文件中
6.1.3 设置过滤规则
如果没有设置过滤规则,应用程序默认会接收所有 ID 的报文;如果我们的应用程序只需要接收某些特定 ID 的报文(亦或者不接受所有报文,只发送报文),则可以通过 setsockopt 函数设置过滤规则
struct can_filter rfilter[2]; //定义一个 can_filter 结构体对象
// 填充过滤规则,只接收 ID 为(can_id & can_mask)的报文
rfilter[0].can_id = 0x60A;
rfilter[0].can_mask = 0x7FF;
rfilter[1].can_id = 0x60B;
rfilter[1].can_mask = 0x7FF;
// 调用 setsockopt 设置过滤规则
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));
struct can_filter 结构体中只有两个成员,can_id 和 can_mask。
如果应用程序不接收所有报文,在这种仅仅发送数据的应用中,可以在内核中省略接收队列,以此减少CPU 资源的消耗。此时可将 setsockopt()函数的第 4 个参数设置为 NULL,将第 5 个参数设置为 0
6.1.4 数据发送/接收
每一次通信都采用 struct can_frame 结构体将数据封装成帧
struct can_frame {
canid_t can_id; /* CAN 标识符 */
__u8 can_dlc; /* 数据长度(最长为 8 个字节) */
__u8 __pad; /* padding */
__u8 __res0; /* reserved / padding */
__u8 __res1; /* reserved / padding */
__u8 data[8]; /* 数据 */
};
- can_id 为帧的标识符,如果是标准帧,就使用 can_id 的低 11 位;如果为扩展帧,就使用0~28 位。can_id的第29、30、31位是帧的标志位,用来定义帧的类型,定义如下:
/* special address description flags for the CAN_ID */
#define CAN_EFF_FLAG 0x80000000U /* 扩展帧的标识 */
#define CAN_RTR_FLAG 0x40000000U /* 远程帧的标识 */
#define CAN_ERR_FLAG 0x20000000U /* 错误帧的标识,用于错误检查 */
/* mask */
#define CAN_SFF_MASK 0x000007FFU /* <can_id & CAN_SFF_MASK>获取标准帧 ID */
#define CAN_EFF_MASK 0x1FFFFFFFU /* <can_id & CAN_EFF_MASK>获取标准帧 ID */
#define CAN_ERR_MASK 0x1FFFFFFFU /* omit EFF, RTR, ERR flags */
- 错误帧的符号位在头文件<linux/can/error.h>中定义
/* error class (mask) in can_id */
#define CAN_ERR_TX_TIMEOUT 0x00000001U /* TX timeout (by netdevice driver) */
#define CAN_ERR_LOSTARB 0x00000002U /* lost arbitration / data[0] */
#define CAN_ERR_CRTL 0x00000004U /* controller problems / data[1] */
#define CAN_ERR_PROT 0x00000008U /* protocol violations / data[2..3] */
#define CAN_ERR_TRX 0x00000010U /* transceiver status / data[4] */
#define CAN_ERR_ACK 0x00000020U /* received no ACK on transmission */
#define CAN_ERR_BUSOFF 0x00000040U /* bus off */
#define CAN_ERR_BUSERROR 0x00000080U /* bus error (may flood!) */
#define CAN_ERR_RESTARTED 0x00000100U /* controller restarted */
- 数据发送
使用 write()函数来实现,譬如要发送的数据帧包含了三个字节数据 0xA0、0xB0 以及0xC0,帧 ID 为 123,可采用如下方法进行发送:
struct can_frame frame; //定义一个 can_frame 变量
int ret;
frame.can_id = 123;//如果为扩展帧,那么 frame.can_id = CAN_EFF_FLAG | 123;
frame.can_dlc = 3; //数据长度为 3
frame.data[0] = 0xA0; //数据内容为 0xA0
frame.data[1] = 0xB0; //数据内容为 0xB0
frame.data[2] = 0xC0; //数据内容为 0xC0
ret = write(sockfd, &frame, sizeof(frame)); //发送数据
if(sizeof(frame) != ret) //如果 ret 不等于帧长度,就说明发送失败
perror("write error");
如果要发送远程帧(帧 ID 为 123),可采用如下方法进行发送:
struct can_frame frame;
frame.can_id = CAN_RTR_FLAG | 123;
write(sockfd, &frame, sizeof(frame));
- 数据接收数据接收使用 read()函数来实现
struct can_frame frame;
int ret = read(sockfd, &frame, sizeof(frame));
6.1.5 回环功能设置
在默认情况下,CAN 的本地回环功能是开启的,在本地回环功能开启的情况下,所有的发送帧都会被回环到与 CAN 总线接口对应的套接字上,可以使用下面的方法关闭或开启本地回环功能:
int loopback = 0; //0 表示关闭,1 表示开启(默认)
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_LOOPBACK, &loopback, sizeof(loopback));
6.2 CAN 应用编程
在 Linux 系统中,CAN 总线设备作为网络设备被系统进行统一管理。在控制台下,CAN 总线的配置和以太网的配置使用相同的命令。使用 ifconfig 命令查看 CAN 设备
6.2.1CAN数据发送实例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <net/if.h>
int main(void)
{
struct ifreq ifr = {0};
struct sockaddr_can can_addr = {0};
struct can_frame frame = {0};
int sockfd = -1;
int ret;
/* 打开套接字 */
sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if(0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
/* 指定can0设备 */
strcpy(ifr.ifr_name, "can0");
ioctl(sockfd, SIOCGIFINDEX, &ifr);
can_addr.can_family = AF_CAN;
can_addr.can_ifindex = ifr.ifr_ifindex;
/* 将can0与套接字进行绑定 */
ret = bind(sockfd, (struct sockaddr *)&can_addr, sizeof(can_addr));
if (0 > ret) {
perror("bind error");
close(sockfd);
exit(EXIT_FAILURE);
}
/* 设置过滤规则:不接受任何报文、仅发送数据 */
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);
/* 发送数据 */
frame.data[0] = 0xA0;
frame.data[1] = 0xB0;
frame.data[2] = 0xC0;
frame.data[3] = 0xD0;
frame.data[4] = 0xE0;
frame.data[5] = 0xF0;
frame.can_dlc = 6; //一次发送6个字节数据
frame.can_id = 0x123;//帧ID为0x123,标准帧
for ( ; ; ) {
ret = write(sockfd, &frame, sizeof(frame)); //发送数据
if(sizeof(frame) != ret) { //如果ret不等于帧长度,就说明发送失败
perror("write error");
goto out;
}
sleep(1); //一秒钟发送一次
}
out:
/* 关闭套接字 */
close(sockfd);
exit(EXIT_SUCCESS);
}
6.2.2 CAN数据接收实例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <net/if.h>
int main(void)
{
struct ifreq ifr = {0};
struct sockaddr_can can_addr = {0};
struct can_frame frame = {0};
int sockfd = -1;
int i;
int ret;
/* 打开套接字 */
sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if(0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
/* 指定can0设备 */
strcpy(ifr.ifr_name, "can0");
ioctl(sockfd, SIOCGIFINDEX, &ifr);
can_addr.can_family = AF_CAN;
can_addr.can_ifindex = ifr.ifr_ifindex;
/* 将can0与套接字进行绑定 */
ret = bind(sockfd, (struct sockaddr *)&can_addr, sizeof(can_addr));
if (0 > ret) {
perror("bind error");
close(sockfd);
exit(EXIT_FAILURE);
}
/* 设置过滤规则 */
//setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);
/* 接收数据 */
for ( ; ; ) {
if (0 > read(sockfd, &frame, sizeof(struct can_frame))) {
perror("read error");
break;
}
/* 校验是否接收到错误帧 */
if (frame.can_id & CAN_ERR_FLAG) {
printf("Error frame!\n");
break;
}
/* 校验帧格式 */
if (frame.can_id & CAN_EFF_FLAG) //扩展帧
printf("扩展帧 <0x%08x> ", frame.can_id & CAN_EFF_MASK);
else //标准帧
printf("标准帧 <0x%03x> ", frame.can_id & CAN_SFF_MASK);
/* 校验帧类型:数据帧还是远程帧 */
if (frame.can_id & CAN_RTR_FLAG) {
printf("remote request\n");
continue;
}
/* 打印数据长度 */
printf("[%d] ", frame.can_dlc);
/* 打印数据 */
for (i = 0; i < frame.can_dlc; i++)
printf("%02x ", frame.data[i]);
printf("\n");
}
/* 关闭套接字 */
close(sockfd);
exit(EXIT_SUCCESS);
}