linux 应用开发学习笔记2

自旋锁

自旋锁本质上也是锁,互斥锁就是通过自旋锁实现的
在获取自旋锁时,如果未上锁,则会加锁,如果本身有锁,则会原地“自旋”直到持有者释放锁,
而互斥锁则是在有锁时阻塞
  自旋:就是一直循环查看该自旋锁的持有者是否释放,但是这会占用CPU,
  1、同一自旋锁加锁两次,必然死锁,而互斥锁不一定因为互斥锁有一种类型可以检查错误 
   ----------- 笔记1提及过

在这里插入图片描述

#include <pthread.h>
int pthread_spin_destroy(pthread_spinlock_t *lock); //销毁
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);//初始化
参数 lock 指向了需要进行初始化或销毁的自旋锁对象,参数 pshared 表示自旋锁的进程共享属性,可以
取值如下:
⚫ PTHREAD_PROCESS_SHARED: 共享自旋锁。该自旋锁可以在多个进程中的线程之间共享;
⚫ PTHREAD_PROCESS_PRIVATE: 私有自旋锁。只有本进程内的线程才能够使用该自旋锁。
这两个函数在调用成功的情况下返回 0;失败将返回一个非 0 值的错误码。

注意=======

可以使用 pthread_spin_lock()函数或 pthread_spin_trylock()函数对自旋锁进行加锁,前者在未获取到锁时
一直“自旋”;对于后者,如果未能获取到锁,就立刻返回错误,错误码为 EBUSY。不管以何种方式加锁,
自旋锁都可以使用 pthread_spin_unlock()函数对自旋锁进行解锁。其函数原型如下所示:
#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
使用这些函数需要包含头文件<pthread.h>。
参数 lock 指向自旋锁对象,调用成功返回 0,失败将返回一个非 0 值的错误码。
如果自旋锁处于未锁定状态,调用 pthread_spin_lock()会将其锁定(上锁),如果其它线程已经将自旋
锁锁住了,那本次调用将会“自旋”等待;如果试图对同一自旋锁加锁两次必然会导致死锁
void *producer(void *arg)
{
    for (int i = 0; i < BUFFER_SIZE * 2; i++)
    {
        pthread_spin_lock(&buffer_lock);

        // 等待缓冲区有空位
        while (buffer_index == BUFFER_SIZE)
        {
            pthread_spin_unlock(&buffer_lock);
            usleep(100); // 100微秒
            pthread_spin_lock(&buffer_lock);
        }

        // 生产数据并放入缓冲区
        buffer[buffer_index++] = i;
        printf("Produced: %d\n", i);

        pthread_spin_unlock(&buffer_lock);
    }

    return NULL;
}

void *consumer(void *arg)
{
    for (int i = 0; i < BUFFER_SIZE * 2; i++)
    {
        pthread_spin_lock(&buffer_lock);

        // 等待缓冲区有数据
        while (buffer_index == 0)
        {
            pthread_spin_unlock(&buffer_lock);
            usleep(100); // 100微秒
            pthread_spin_lock(&buffer_lock);
        }

        // 消费数据并从缓冲区移除
        int data = buffer[--buffer_index];
        printf("Consumed: %d, i %d\n", data, i);

        pthread_spin_unlock(&buffer_lock);
    }

    return NULL;
}
void test()
{
    pthread_spin_init(&buffer_lock, PTHREAD_PROCESS_PRIVATE);

    pthread_t producer_thread, consumer_thread;

    pthread_create(&producer_thread, NULL, producer, NULL);
    pthread_create(&consumer_thread, NULL, consumer, NULL);

    pthread_join(producer_thread, NULL);
    pthread_join(consumer_thread, NULL);

    pthread_spin_destroy(&buffer_lock);
}

读写锁

 互斥锁和自旋锁 两种状态:加锁和不加锁,一次只有一个线程可以加锁

 读写锁:读加锁状态、写加锁状态、不加锁状态
 一次一个线程可以占用写的状态、多个线程读的状态,读写锁比互斥锁更有并行性

当读写锁是读加锁状态,其他试图读加锁的线程都能加锁成功,但是写加锁线程会被阻塞,
当是写加锁状态,其他线程不管是读、写加锁状态的都阻塞

 1、读写锁初始化
 读写锁的初始化可以使用宏 PTHREAD_RWLOCK_INITIALIZER 或者函数
pthread_rwlock_init(),其初始化方式与互斥锁相同,
2、销毁
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
pthread_rwlock_destroy(&rwlock);
3、加锁 解锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock)
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

4、当读写锁处于写模式加锁状态时,其它线程调用 pthread_rwlock_rdlock()或 pthread_rwlock_wrlock()函数
均会获取锁失败,从而陷入阻塞等待状态;当读写锁处于读模式加锁状态时,其它线程调用
pthread_rwlock_rdlock()函数可以成功获取到锁,如果调用 pthread_rwlock_wrlock()函数则不能获取到锁,从
而陷入阻塞等待状态。
如果线程不希望被阻塞,可以调用 pthread_rwlock_tryrdlock()和 pthread_rwlock_trywrlock()来尝试加锁,
如果不可以获取锁时。这两个函数都会立马返回错误,错误码为 EBUSY。

void *read_thread(void *arg)
{
    int num = *(int *)arg;
    for (size_t i = 0; i < 10; i++)
    {
        pthread_rwlock_rdlock(&rwlock);
        printf("read thread %lu, num %d g_count %d\n", pthread_self(), num, g_count);
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
    return NULL;
}
void *write_thread(void *arg)
{
    int num = *(int *)arg;
    for (size_t i = 0; i < 10; i++)
    {
        pthread_rwlock_wrlock(&rwlock);
        printf("write thread num %lu, g_count %d\n", num, g_count += 10);
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
    return NULL;
}
void pthread_cond_test()
{
    pthread_t tid[5];
    pthread_t w_tid[5];
    int ret;
    int pthread_num[5] = {0, 1, 2, 3, 4}; // 此处为了区分线程,

    pthread_rwlock_init(&rwlock, NULL);
    for (size_t i = 0; i < sizeof(tid) / sizeof(pthread_t); i++)
    {
    	//ret = pthread_create(&tid[i], NULL, read_thread, &i); 
    	/*&i 是错误的,主线程可能创建五个线程,  
    	但是这个五线程并未执行到获取arg这行代码时,将CPU让给主线程执行,也就是主线程很有可能将循环执行完毕,
       五个线程采取获取arg 的值导致信号号都是5 .也有可能是别的情况,固有此数组*/
        ret = pthread_create(&tid[i], NULL, read_thread, &pthread_num[i]);
        if (ret)
        {
            perror("pthread create failed");
        }
    }
    // sleep(10);
    for (size_t j = 0; j < 5; j++)
    {
        ret = pthread_create(&w_tid[j], NULL, write_thread, &pthread_num[j]);
        if (ret)
        {
            perror(" write pthread create failed");
        }
    }

    for (size_t i = 0; i < 5; i++)
    {
        ret = pthread_join(tid[i], NULL);
        if (ret)
        {
            perror("pthread join failed");
        }
    }
    for (size_t i = 0; i < 5; i++)
    {
        ret = pthread_join(w_tid[i], NULL);
        if (ret)
        {
            perror(" w pthread join failed");
        }
    }

    pthread_rwlock_destroy(&rwlock);
}

读写锁属性

pthread_rwlockattr_t
pthread_rwlock_t rwlock; //定义读写锁
pthread_rwlockattr_t attr; //定义读写锁属性
/* 初始化读写锁属性对象 /
pthread_rwlockattr_init(&attr);
/
将进程共享属性设置为 PTHREAD_PROCESS_PRIVATE /
pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_PRIVATE);
/
初始化读写锁 /
pthread_rwlock_init(&rwlock, &attr);

/
使用完之后 */
pthread_rwlock_destroy(&rwlock); //销毁读写锁
pthread_rwlockattr_destroy(&attr); //销毁读写锁属性对象

高级I/O

 阻塞:就是进入休眠,让出CPU控制权
普通文件的读写是不会阻塞的,使用普通文件本身决定的
管道设备、设备文件:可以用非阻塞IO或者阻塞IO

阻塞IO 能提高CPU的利用率,当条件不满足时,阻塞让出CPU,而非阻塞则则不断轮询,占用CPU利用率很高,如果不轮询占有率也不会高
阻塞式:无法并发实现读取数据,假设鼠标和键盘都需要读取,鼠标时设备文件,如果鼠标不点击,则一直阻塞,即便是键盘有输入,也会获取不到数据,而且这样也只能先动鼠标在输入键盘输入
在这里插入图片描述

int main(void)
{
    char buf[100];
    int fd, ret, flag;
    /* 打开鼠标设备文件 */
    fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
    if (-1 == fd)
    {
        perror("open error");
        exit(-1);
    }
    /* 将键盘设置为非阻塞方式 */
    flag = fcntl(0, F_GETFL); // 先获取原来的 flag
    flag |= O_NONBLOCK;       // 将 O_NONBLOCK 标准添加到 flag
    fcntl(0, F_SETFL, flag);  // 重新设置 flag
    for (;;)
    {
        /* 读鼠标 */
        ret = read(fd, buf, sizeof(buf));
        if (0 < ret)
            printf("鼠标: 成功读取<%d>个字节数据\n", ret);
        /* 读键盘 */
        ret = read(0, buf, sizeof(buf));
        if (0 < ret)
            printf("键盘: 成功读取<%d>个字节数据\n", ret);
    }
    /* 关闭文件 */
    close(fd);
    exit(0);
}
// 但是这是通过轮询实现的,则非常浪费CPU,那就使用多路IO复用

多路IO复用

I/O 多路复用(IO multiplexing) 它通过一种机制,可以监视多个文件描述符,一旦某个文件描述符(也
就是某个文件) 可以执行 I/O 操作时, 能够通知应用程序进行相应的读写操作。 I/O 多路复用技术是为了解
决:在并发式 I/O 场景中进程或线程阻塞到某个 I/O 系统调用而出现的技术,使进程不阻塞于某个特定的
I/O 系统调用。
由此可知, I/O 多路复用一般用于并发式的非阻塞 I/O,也就是多路非阻塞 I/O譬如程序中既要读取鼠
标、又要读取键盘,多路读取。

我们可以采用两个功能几乎相同的系统调用来执行 I/O 多路复用操作,分别是系统调用 select()和 poll()。
这两个函数基本是一样的,细节特征上存在些许差别!
I/O 多路复用存在一个非常明显的特征:外部阻塞式, 内部监视多路 I/O。
在这里插入图片描述


重点 循环调用 select


#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

在这里插入图片描述
在这里插入图片描述

int main(void)
{
    char buf[100];
    int fd, ret = 0, flag;
    fd_set rdfds;
    int loops = 5;
    /* 打开鼠标设备文件 */
    fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
    if (-1 == fd)
    {
        perror("open error");
        exit(-1);
    }
    /* 将键盘设置为非阻塞方式 */
    flag = fcntl(0, F_GETFL); // 先获取原来的 flag
    flag |= O_NONBLOCK;       // 将 O_NONBLOCK 标准添加到 flag
    fcntl(0, F_SETFL, flag);  // 重新设置 flag
    /* 同时读取键盘和鼠标 */
    while (loops--)
    {
        FD_ZERO(&rdfds);
        FD_SET(0, &rdfds);  // 添加键盘
        FD_SET(fd, &rdfds); // 添加鼠标
        ret = select(fd + 1, &rdfds, NULL, NULL, NULL);
        if (0 > ret)
        {
            perror("select error");
            goto out;
        }
        else if (0 == ret)
        {
            fprintf(stderr, "select timeout.\n");
            continue;
        }
        /* 检查键盘是否为就绪态 */
        if (FD_ISSET(0, &rdfds))
        {
            ret = read(0, buf, sizeof(buf));
            if (0 < ret)
                printf("键盘: 成功读取<%d>个字节数据\n", ret);
        }
        /* 检查鼠标是否为就绪态 */  www.openedv.com/forum.php
        if (FD_ISSET(fd, &rdfds))
        {
            ret = read(fd, buf, sizeof(buf));
            if (0 < ret)
                printf("鼠标: 成功读取<%d>个字节数据\n", ret);
        }
    }
out:
    /* 关闭文件 */
    close(fd);
    exit(ret);
}

poll

系统调用 poll()与 select()函数很相似,但函数接口有所不同

在 select()函数中,我们提供三个 fd_set 集合,在每个集合中添加我们关心的文件描述符;

而在 poll()函数中,则需要构造一个 struct pollfd 类型的数组,每个数组元素指定一个文件描述符以及我们对该文件描述符所关心的条件(数据可读、可写或异常情况)。

poll()函数原型如下所示:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

int main(void)
{
    char buf[100];
    int fd, ret = 0, flag;
    int loops = 5;
    struct pollfd fds[2];
    /* 打开鼠标设备文件 */
    fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
    if (-1 == fd)
    {
        perror("open error");
        exit(-1);
    }
    /* 将键盘设置为非阻塞方式 */
    flag = fcntl(0, F_GETFL); // 先获取原来的 flag
    flag |= O_NONBLOCK;       // 将 O_NONBLOCK 标准添加到 flag
    fcntl(0, F_SETFL, flag);  // 重新设置 flag
    /* 同时读取键盘和鼠标 */
    fds[0].fd = 0;
    fds[0].events = POLLIN; // 只关心数据可读
    fds[0].revents = 0;
    fds[1].fd = fd;
    fds[1].events = POLLIN; // 只关心数据可读
    fds[1].revents = 0;
    while (loops--)
    {
        ret = poll(fds, 2, -1); // -1 :阻塞 0:不阻塞, >0 表示阻塞时间
        if (0 > ret)
        {
            perror("poll error");
            goto out;
        }
        else if (0 == ret)
        {
            fprintf(stderr, "poll timeout.\n");
            continue;
        }
        /* 检查键盘是否为就绪态 */
        if (fds[0].revents & POLLIN)
        {
            ret = read(0, buf, sizeof(buf));
            if (0 < ret)
                printf("键盘: 成功读取<%d>个字节数据\n", ret);
        }
        /* 检查鼠标是否为就绪态 */
        if (fds[1].revents & POLLIN)
        {
            ret = read(fd, buf, sizeof(buf));
            if (0 < ret)
                printf("鼠标: 成功读取<%d>个字节数据\n", ret);
        }
    }
out:
    /* 关闭文件 */
    close(fd);
    exit(ret);
}

这里是引用

异步IO

 也称驱动IO,异步 I/O 中,当文件描述符上可以执行 I/O 操作时,进程可以请求内核为自己发送一个信号。 之后进程
 就可以执行任何其它的任务,直到文件描述符可以被执行 I/O 操作为止,此时内核会发送信号给进程。

要使用异步 I/O,程序需要按照如下步骤来执行:
⚫ 通过指定 O_NONBLOCK 标志使能非阻塞 I/O。
⚫ 通过指定 O_ASYNC 标志使能异步 I/O。
⚫ 设置异步 I/O 事件的接收进程。也就是当文件描述符上可执行 I/O 操作时会发送信号通知该进程,
通常将调用进程设置为异步 I/O 事件的接收进程。
⚫ 为内核发送的通知信号注册一个信号处理函数。默认情况下, 异步 I/O 的通知信号是 SIGIO,所以
内核会给进程发送信号 SIGIO。在 8.2 小节中简单地提到过该信号。
⚫ 以上步骤完成之后,进程就可以执行其它任务了,当 I/O 操作就绪时,内核会向进程发送一个 SIGIO
信号,当进程接收到信号时,会执行预先注册好的信号处理函数,我们就可以在信号处理函数中进
行 I/O 操作。

O_ASYNC 用于使能文件描述符的异步 I/O 事件,当文件描述符可执行 I/O 操作时,内核会向异
步 I/O 事件的接收进程发送 SIGIO 信号(默认情况下)

int sig_io_fd;
void sigio_handler(int sig)
{
    int loop = 5;
    char buf[100];
    int ret;

    if (SIGIO != sig)
    {
        return;
    }

    ret = read(sig_io_fd, buf, sizeof(buf));
    if (0 < ret)
    {
        printf(" mouse");
    }

    loop--;
    if (loop <= 0)
    {
        close(sig_io_fd);
    }
}
void signal_io()
{
    int flag;
    if ((sig_io_fd = open(MOUSE, O_RDONLY | O_NONBLOCK) < 0))
    {
        perror("file open fail");
    }
    // 使能异步IO
    flag = fcntl(sig_io_fd, F_GETFL);
    flag |= O_ASYNC;
    fcntl(sig_io_fd, F_SETFL, flag);

    // 设置异步IO所有者
    fcntl(sig_io_fd, F_SETOWN, getpid());

    signal(SIGIO, sigio_handler);

    for (;;)
    {
        sleep(1);
    }
}

优化异步IO EPOLL

而对于 select()或 poll()函数来说,内部实现原理其实是通过轮训的方式来检查多个文件描述符是否可执
行 I/O 操作,所以,当需要检查的文件描述符数量较多时,随之也将会消耗大量的 CPU 资源来实现轮训检
查操作。 当需要检查的文件描述符并不是很多时,使用 select()或 poll()是一种非常不错的方案!
在性能表现上, epoll 与异步 I/O 方式相似,但是 epoll 有一些胜过异步 I/O 的优点

在这里插入图片描述
解决办法
1、

在这里插入图片描述
2、
在这里插入图片描述
通过以上两步才能解决

设备文件:是硬件文件向应用层提供的API接口

设备文件通常在dev目录下,dev目录也称设备节点
除此之外还可以通过sysfs文件系统对硬件设备操作
同 devfs、proc文件系统一样,被称为虚拟文件系统,proc:可以获取系统信息和进程相关

标准接口、非标准接口

linux系统的设备驱动框架提供同意了接口规范,驱动程序使用内核提供设备驱动框架就是标准接口
杂项类设备:基本都是非标

GPIO

在这里插入图片描述

gpiochipX:当前SOC 支持的GPIO控制器,IMX6UL/IMX6ULL 5个GPIO控制器,GPIO1、2、3、4、5,
分别对应 GPIO0、32、64、96、128,每一个gpiochipX管理一组GPIO,
在这里插入图片描述
base:该组gpio基地址,等于GPIOX,
label:该组GPIO的名称
ngpio:该控制器控制的GPIO数量,所以引脚编号范围是: base ~ base+ngpio-1
这三者是可读设备权限,不可写,
在这里插入图片描述
GPIO4_IO16对应的编号是(4- 132+ 16,也可以 GPIO4 对应96, 96+16 = 112,(1和32固定计算公式)*
将GPIO0_IO0导出、删除:echo GPIO编号 > export echo GPIO编号 >unexport
在这里插入图片描述
导出之后,生产一个文件夹,包含GPIO的输入、输出、输出高低电平

Tips:需要注意的是,并不是所有 GPIO 引脚都可以成功导出, 如果对应的 GPIO 已经在内核中被使用
了, 那便无法成功导出,

这里是引用
active_low:控制极性
direction: 输入输出 out,in
value:0:active_low默认为0 时:低。1:高,active_low为1时,则反之
edge:触发方式 非中断触发(none)、上升沿(rising)、下降沿(falling),边沿触发(both),设置为中断触发可以poll监控电平变化
都是字符、字符串写入

# 获取 GPIO 引脚的输入电平状态
echo "in" > direction
cat value
# 控制 GPIO 引脚输出高电平
echo "out" > direction
echo "1" > value

# active_low 等于 0
echo "0" > active_low
echo "out" > direction
echo "1" > value #输出高
echo "0" > value #输出低
# active_low 等于 1
$ echo "1" > active_low
$ echo "out" > direction
$ echo "1" > value #输出低
$ echo "0" > value #输出高

非中断引脚: echo "none" > edge
上升沿触发: echo "rising" > edge
下降沿触发: echo "falling" > edge
边沿触发: echo "both" > edge
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#define GPIO_PATH_BASE "/sys/class/gpio"

int gpio_config(const char *name, const char *value, const char *gpio_num)
{
    char file_path[100];
    int fd, ret;

    // Construct the file path
    snprintf(file_path, sizeof(file_path), "%s/gpio%s/%s", GPIO_PATH_BASE, gpio_num, name);

    if ((fd = open(file_path, O_RDWR)) < 0)
    {
        perror("gpio config fail");
        return 0;
    }

    printf("%s ---%s\n", file_path, value);

    if ((ret = write(fd, value, strlen(value))) < 0)
    {
        perror("write value fail");
        close(fd);
        return 0;
    }

    printf("ret %d\n", ret);
    close(fd);
    return 1;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        fprintf(stderr, "Usage: %s GPIO_NUM VALUE\n", argv[0]);
        return 1;
    }

    const char *gpio_num = argv[1];
    const char *value = argv[2];

    // Check if the GPIO directory exists, and if not, export it
    char gpio_export_path[50];
    snprintf(gpio_export_path, sizeof(gpio_export_path), "%s/gpio%s", GPIO_PATH_BASE, gpio_num);

    if (access(gpio_export_path, F_OK))
    {
        int fd;
        if ((fd = open(GPIO_PATH_BASE "/export", O_WRONLY)) < 0)
        {
            perror("export open fail");
            return 1;
        }

        if ((write(fd, gpio_num, strlen(gpio_num))) < 0)
        {
            perror("write data fail");
            close(fd);
            return 1;
        }

        close(fd);
    }

    if (!gpio_config("direction", "out", gpio_num))
    {
        perror("direction fail");
    }

    if (!gpio_config("active_low", "0", gpio_num))
    {
        perror("active_low fail");
    }

    if (!gpio_config("value", value, gpio_num))
    {
        perror("value fail");
    }

    return 0;
}

#输入设备:能产生输入事件的设备

Linux 系统为了统一管理这些输入设备,实现了一套能够兼容所有输入设备的框架,那么这个框架就
是 input 子系统。驱动开发人员基于 input 子系统开发输入设备的驱动程序, input 子系统可以屏蔽硬件的差
异,向应用层提供一套统一的接口。
基于 input 子系统注册成功的输入设备,都会在/dev/input 目录下生成对应的设备节点(设备文件), 设
备节点名称通常为 eventX(X 表示一个数字编号 0、 1、 2、 3 等),譬如/dev/input/event0、 /dev/input/event1、
/dev/input/event2 等, 通过读取这些设备节点可以获取输入设备上报的数据

触摸屏

如果我们要读取触摸屏的数据,假设触摸屏设备对应的设备节点为/dev/input/event0,那么数据读取流程
如下:
①、应用程序打开/dev/input/event0 设备文件;
②、应用程序发起读操作(譬如调用 read),如果没有数据可读则会进入休眠(阻塞 I/O 情况下);
③、 当有数据可读时,应用程序会被唤醒,读操作获取到数据返回;
④、应用程序对读取到的数据进行解析。

其实每一次 read 操作获取的都是一个 struct input_event 结构体类型数据, 该结构体定
义在<linux/input.h>头文件中,它的定义如下:
struct input_event 结构体
struct input_event {
struct timeval time;
__u16 type;
__u16 code;
__s32 value;
};

/* type:事件类型

  • Event types
    */
    #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:该类事件的具体事件,

type:鼠标事件,code:点击鼠标按键(左键、右键,或鼠标上的其它按键)时会上报按键
类事件,移动鼠标时则会上报相对位移类事件
或者键盘,键盘字母w、z tab等

#define KEY_RESERVED 0
#define KEY_ESC 1 //ESC 键
#define KEY_1 2 //数字 1 键
#define KEY_2 3 //数字 2 键
#define KEY_TAB 15 //TAB 键
#define KEY_Q 16 //字母 Q 键
#define KEY_W 17 //字母 W 键
#define KEY_E 18 //字母 E 键
#define KEY_R 19 //字母 R 键

相对位移事件

#define REL_X 0x00 //X 轴
#define REL_Y 0x01 //Y 轴
#define REL_Z 0x02 //Z 轴
#define REL_RX 0x03
#define REL_RY 0x04

绝对位移事件
触摸屏事件

触摸屏设备是一种绝对位移设备,它能够产生绝对位移事件; 譬如对于触摸屏来说,一个触摸点所包含 的信息可能有多种,譬如触摸点的 X 轴坐标、
Y 轴坐标、 Z 轴坐标、按压力大小以及接触面积等, 所以 code 变量告知应用程序当前上报的是触摸点的哪一种信息(X 坐标还是 Y
坐标、亦或者其它);绝对位移事件 如下

#define ABS_X 0x00 //X 轴
#define ABS_Y 0x01 //Y 轴
#define ABS_Z 0x02 //Z 轴
#define ABS_RX 0x03
#define ABS_RY 0x04
#define ABS_RZ 0x05
#define ABS_THROTTLE 0x06
#define ABS_RUDDER 0x07
#define ABS_WHEEL 0x08
#define ABS_GAS 0x09
#define ABS_BRAKE 0x0a
#define ABS_HAT0X 0x10
#define ABS_HAT0Y 0x11
#define ABS_HAT1X 0x12
#define ABS_HAT1Y 0x13
#define ABS_HAT2X 0x14
#define ABS_HAT2Y 0x15
#define ABS_HAT3X 0x16
#define ABS_HAT3Y 0x17
#define ABS_PRESSURE 0x18
#define ABS_DISTANCE 0x19
#define ABS_TILT_X 0x1a
#define ABS_TILT_Y 0x1b
#define ABS_TOOL_WIDTH 0x1c

value:随着code的变化而变化,内核每次上报事件都会向应用层发送一个数据

数据同步

同步事件类型EV_SYN:实现同步操作,告知接收者本轮数据已完整,
应用程序读取输入设备上报的数据时,一次 read 操作只能读取一个 struct input_event 类型数据,譬
如对于触摸屏来说,一个触摸点的信息包含了 X 坐标、 Y 坐标以及其它信息, 对于这样情况,应用程序需
要执行多次 read 操作才能把一个触摸点的信息全部读取出来, 这样才能得到触摸点的完整信息。
那么应用程序如何得知本轮已经读取到完整的数据了呢?其实这就是通过同步事件来实现的, 内核将
本轮需要上报、发送给接收者的数据全部上报完毕后,接着会上报一个同步事件,以告知应用程序本轮数据
已经完整、 可以进行同步了。

同步类事件中也包含了多种不同的事件:
*

* Synchronization events.
*/
#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

key input 子系统验证

通过od 指令判断输入设备或者 /proc/bus/input/devices

在这里插入图片描述

在这里插入图片描述

void key_test()
{
    struct input_event in_ev = {0};
    int fd = -1;
    int ret = 0;
    char path[100];
    printf("Enter input event device path: ");
    scanf("%s", path);
    printf("%s\n", path);
    if ((fd = open(path, O_RDONLY)) < 0)
    {
        perror(" open fail");
    }

    for (;;)
    {
        if ((ret = read(fd, &in_ev, sizeof(struct input_event))) < 0)
        {
            perror("read data fail");
        }
        printf("type: %d, code: %d, value: %d\n", in_ev.type, in_ev.code, in_ev.value);
    }
    close(fd);
}
root@ATK-IMX6U:/home/app# ./main
Enter input event device path: /dev/input/event2
/dev/input/event2
type: 1, code: 114, value: 1  //按键 KEY_VOLUMEDOWN被按下
type: 0, code: 0, value: 0  //上报了 EV_SYN 同步类事件(type=0)中的 SYN_REPORT 事件(code=0)数据完整
type: 1, code: 114, value: 0 //按键 KEY_VOLUMEDOWN被松开
type: 0, code: 0, value: 0  //上报了 EV_SYN 同步类事件(type=0)中的 SYN_REPORT 事件(code=0)数据完整
type: 1, code: 114, value: 1
type: 0, code: 0, value: 0
type: 1, code: 114, value: 0
type: 0, code: 0, value: 0
type: 1, code: 114, value: 1
type: 0, code: 0, value: 0
type: 1, code: 114, value: 2
type: 0, code: 0, value: 1
type: 1, code: 114, value: 2
type: 0, code: 0, value: 1
type: 1, code: 114, value: 2
type: 0, code: 0, value: 1
type: 1, code: 114, value: 2
type: 0, code: 0, value: 1
type: 1, code: 114, value: 0
type: 0, code: 0, value: 0  

type: 1, code: 114, value: 1 下图解释

在这里插入图片描述
在这里插入图片描述

判断按键长按短按

void key_test()
{
    struct input_event in_ev = {0};
    int fd = -1;
    int ret = 0;
    char path[100];
    printf("Enter input event device path: ");
    scanf("%s", path);
    printf("%s\n", path);
    if ((fd = open(path, O_RDONLY)) < 0)
    {
        perror(" open fail");
    }

    for (;;)
    {
        if ((ret = read(fd, &in_ev, sizeof(struct input_event))) < 0)
        {
            perror("read data fail");
        }
        // printf("type: %d, code: %d, value: %d\n", in_ev.type, in_ev.code, in_ev.value);
        if (in_ev.type == EV_KEY)
        {

            switch (in_ev.value)
            {
            case 0:
                printf(" key up\n");
                break;
            case 1:
                printf(" key down\n");
                break;
            case 2:
                printf(" key still down\n");
                break;
            default:
                break;
            }
        }
    }
    close(fd);
}
root@ATK-IMX6U:/home/app# ./main
Enter input event device path: /dev/input/event2
/dev/input/event2
 key down
 key up
 key down
 key up
 key down
 key still down
 key still down
 key still down
 key still down
 key still down
 key up

注意 判断事件类型和fd记得关闭

在这里插入图片描述

显示屏 单点测试程序 tslib库

void tslib_screen_test()
{
    struct tsdev *ts = NULL;
    struct ts_sample_mt *mt_ptr = NULL;
    struct ts_sample samp;
    struct input_absinfo slot;
    int max_slots;
    int pressure = 0;
    int ret;

    // unsigned int pressure[12] = {0};
    // printf(" 11111111 \n");
    int i;

    ts = ts_setup(NULL, 0);
    if (ts == NULL)
    {
        printf(" setup failed");
    }
    // printf(" 11111111 \n");
    for (;;)
    {
        // printf(" 11111111 \n");
        if (ts_read(ts, &samp, 1) < 0)
        {
            printf(" get data failed");
            ts_close(ts);
            break;
        }
        if (samp.pressure)
        {
            if (pressure)
            {
                printf(" 移动(%d,%d)\n", samp.x, samp.y);
            }
            else
            {
                printf("按下 (%d,%d)\n", samp.x, samp.y); //记得将log后面加上换行符,刷新输出缓存,否则看不到输出
            }
        }
        else
        {
            printf(" 松下(%d,%d)\n", samp.x, samp.y);
        }
        pressure = samp.pressure;
    }
    ts_close(ts);
}

framebuffer

帧缓冲:是linux 一种显示驱动接口, 所以linux系统下显示设备被称为framebuffer设备, framebuffer设备 对应 /dev/fbX(X,0 ,1,2)
应用程序通过对 LCD 设备节点/dev/fb0(假设 LCD 对应的设备节点是
/dev/fb0)进行 I/O 操作即可实现对 LCD 的显示控制,实质就相当于读写了 LCD 的显存,而显存是 LCD 的
显示缓冲区, LCD 硬件会从显存中读取数据显示到 LCD 液晶面板上。

在应用程序中,操作/dev/fbX 的一般步骤如下:
①、首先打开/dev/fbX 设备文件。
②、 使用 ioctl()函数获取到当前显示设备的参数信息,譬如屏幕的分辨率大小、像素格式,根据屏幕参
数计算显示缓冲区的大小。
③、通过存储映射 I/O 方式将屏幕的显示缓冲区映射到用户空间(mmap)。
④、映射成功后就可以直接读写屏幕的显示缓冲区,进行绘图或图片显示等操作了。
⑤、完成显示后, 调用 munmap()取消映射、并调用 close()关闭设备文件。

每个像素点使用多少个 bit 来描述,也就是像素深度 bpp
bmp格式的图片不会失真,所以数据量大

mmap 将显存映射到进程的地址空间

为什么使用存储映射IO?
数据量较大是,普通IOde 效率不如映射
假设某一显示器的分辨率为 1920 * 1080,像
素格式为ARGB8888,针对该显示器,刷一帧图像的数据量为 1920 x 1080 x 32 / 8 = 8294400 个字节(约等
于 8MB),这还只是一帧的图像数据,而对于显示器来说,显示的图像往往是动态改变的,意味着图像数
据会被不断更新。

代码
#define argb8888_torgb565(color) ({                                                 \
    unsigned int temp = color;                                                      \
    ((temp & 0xF80000UL) >> 8) | ((temp & 0xFC00UL) >> 5) | ((temp & 0xF8UL) >> 3); \
})
int width;
int height;
static unsigned short *screen_base = NULL;
static void lcd_draw_point(unsigned int x, unsigned int y, unsigned int color)
{
    unsigned short rgb565 = argb8888_torgb565(color);

    if (x >= width)
    {
        x = width - 1;
    }
    if (y >= height)
    {
        y = height - 1;
    }

    screen_base[y * width + x] = rgb565;
}
static void lcd_draw_line(uint32_t x, uint32_t y, int32_t dir, uint32_t length, uint32_t color)
{
    uint16_t rgb565 = argb8888_torgb565(color);
    uint32_t end;
    uint64_t 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 - 1)
        {
            end = width - 1;
        }

        for (; x <= end; x++, temp++)
            screen_base[temp] = rgb565;
    }
    else
    {
        end = y + length - 1;
        if (end >= height - 1)
        {
            end = height - 1;
        }
        for (; y <= end; y++, temp += width)
            screen_base[temp] = rgb565;
    }
}

static void lcd_draw_rectangle(uint32_t start_x, uint32_t end_x, uint32_t start_y, uint32_t end_y, uint32_t color)
{

    uint32_t x_len = end_x - start_x + 1;
    uint32_t 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);
}
static void lcd_fill(uint32_t start_x, uint32_t end_x, uint32_t start_y, uint32_t end_y, uint32_t color)
{
    uint16_t rgb565 = argb8888_torgb565(color);
    uint64_t temp = 0;
    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;
    }
}
int frame_buf()
{
    struct fb_fix_screeninfo fb_fix;
    struct fb_var_screeninfo fb_var;

    unsigned int screen_size;
    int fd, ret;

    if ((fd = open("/dev/fb0", O_RDWR)) < 0)
    {
        perror(" open failed");
        return -1;
    }

    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);
        return 0;
    }

    int w = height * 0.25;

    lcd_fill(0, width - 1, 0, height - 1, 0x00);
    lcd_fill(0, w, 0, w, 0xFF0000);
    lcd_fill(width - w, width - 1, 0, w, 0xFF0000);
    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);

    uint32_t s_x, e_x, s_y, 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);
}

一般常见的图像都是以 16 位(R、 G、 B 三种颜色分别使用 5bit、 6bit、 5bit 来表示)、 24 位(R、 G、
B 三种颜色都使用 8bit 来表示) 色图像为主,我们称这样的图像为真彩色图像, 真彩色图像是不需要调色板
的,即位图信息头后面紧跟的就是位图数据了

移植

在这里插入图片描述

错误处理

使用 libjpeg 库函数的时候难免会产生错误,所以我们在使用 libjpeg 解码之前,首先要做好错误处理。
在 libjpeg 库中,实现了默认错误处理函数,当错误发生时, 譬如如果内存不足、文件格式不对等, 则会 libjpeg
实现的默认错误处理函数, 默认错误处理函数将会调用 exit()结束束整个进程;当然,我们可以修改错误处
理的方式, libjpeg 提供了接口让用户可以注册一个自定义错误处理函数。

void my_error_exit(struct jpeg_decompress_struct *cinfo)
{
/* ... */
}
cinfo.err.error_exit = my_error_exit;

看依赖库

在这里插入图片描述
在这里插入图片描述

串口

ls /dev/pts/* 查看远程终端 SSH
ls /dev/ttymxc* 查看串口中断
who who 命令来查看计算机系统当前连接了哪些终端(一个终端就表示有
一个用户使用该计算机)远程和串口加起来

mxc 这个名字不是一定的,这个名字的命名与驱动有关系(与硬件平台有关) ,如果 你换一个硬件平台,那么它这个串口对应的设备节点就不一定是 mxcX 了; 譬如 ZYNQ 平台,它的系统中 串口对应的设备节点就是/dev/ttyPSX(X 是一个数字编号),所以说这个名字它不是统一的,但是名字前缀 都是以“tty”开头,以表明它是一个终端

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 */
};
如上定义所示,影响终端的参数按照不同模式分为如下几类:
⚫ 输入模式;
⚫ 输出模式;
⚫ 控制模式;
⚫ 本地模式;
⚫ 线路规程;
⚫ 特殊控制字符;
⚫ 输入速率;
⚫ 输出速率
man 3 termios 查询以下宏
c_iflag:
IGNBRK 忽略输入终止条件
BRKINT 当检测到输入终止条件时发送 SIGINT 信号
IGNPAR 忽略帧错误和奇偶校验错误
PARMRK 对奇偶校验错误做出标记
INPCK 对接收到的数据执行奇偶校验
ISTRIP 将所有接收到的数据裁剪为 7 比特位、也就是去除第八位
INLCR 将接收到的 NL(换行符)转换为 CR(回车符)
IGNCR 忽略接收到的 CR(回车符)
ICRNL 将接收到的 CR(回车符)转换为 NL(换行符)
IUCLC 将接收到的大写字符映射为小写字符
IXON 启动输出软件流控
IXOFF 启动输入软件流控

c_oflag:输出模式控制输出字符方式
OPOST 启用输出处理功能,如果不设置该标志则其他标志都被忽略
OLCUC 将输出字符中的大写字符转换成小写字符
ONLCR 将输出中的换行符(NL '\n')转换成回车符(CR '\r')
OCRNL 将输出中的回车符(CR '\r')转换成换行符(NL '\n')
ONOCR 在第 0 列不输出回车符(CR)
ONLRET 不输出回车符
OFILL 发送填充字符以提供延时
OFDEL 如果设置该标志,则表示填充字符为 DEL 字符,否则为 NULL、

c_cflag:控制模式, 波特率,数据位,停止位  B115200 这些都是宏
B1200 1200 波特率
B1800 1800 波特率
B2400 2400 波特率
B4800 4800 波特率
B9600 9600 波特率
B19200 19200 波特率
B38400 38400 波特率
B57600 57600 波特率
B115200 115200 波特率

CSIZE 数据位的位掩码
CS5 5 个数据位
CS6 6 个数据位
CS7 7 个数据位
CS8 8 个数据位
CSTOPB 2 个停止位,如果不设置该标志则默认是一个停止位
CREAD 接收使能
PARENB 使能奇偶校验
PARODD 使用奇校验、而不是偶校验
HUPCL 关闭时挂断调制解调器
CLOCAL 忽略调制解调器控制线
CRTSCTS 使能硬件流控字符


有一个 c_ispeed 成员变量和 c_ospeed 成员变量,在其它一些系统中,可能
会使用这两个变量来指定串口的波特率;在 Linux 系统下, 则是使用 CBAUD 位掩码所选择的几个 bit 位来
指定串口波特率。事实上, termios API 中提供了 cfgetispeed()cfsetispeed()函数分别用于获取和设置串口
的波特率。

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 启用输入处理功能

**> 注:当 ICANON 标志被设置时表示启用终端的规范模式,什么规范模式?这里给大家简单地说明一下。

> 终端有三种工作模式,分别为规范模式(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()函数的调用方式。 上一小节给大家提到过,
> TIME 和 MIN 的值只能用于非规范模式,两者结合起来可以控制对输入数据 的读取方式。 根据 TIME 和 MIN 的取值不同,会有以下4 种不同情况

> MIN = 0 和 TIME = 0: 在这种情况下, read()调用总是会立即返回。若有可读数据,则读取数据并返回被读取的字节数; 否则读取不到任何数据并返回 0> MIN > 0 和 TIME = 0:在这种情况下, read()函数会被阻塞, 直到有 MIN 个字符可以读取时才返回,返回值是读取的字符数量。到达文件尾时返回 0> MIN = 0 和 TIME > 0:在这种情况下, 只要有数据可读或者经过 TIME 个十分之一秒的时间, read() 函数则立即返回,返回值为被读取的字节数。如果超时并且未读到数据,则 read()函数返回 0。 ⚫
> MIN > 0 和 TIME > 0:在这种情况下, 当有 MIN 个字节可读或者两个输入字符之间的时间间隔超 过 TIME个十分之一秒时, read()函数才返回。因为在输入第一个字符后系统才会启动定时器,所 以,在这种情况下,read()函数至少读取一个字节后才返回

 1. 原始模式(Raw mode) 按照严格意义来讲,原始模式是一种特殊的非规范模式。在原始模式下,所有的输入数据以字节为单位
    被处理。在这个模式下,终端是不可回显的, 并且禁用终端输入和输出字符的所有特殊处理。 在我们的应用 程序中,可以通过调用
    cfmakeraw()函数将终端设置为原始模式。 cfmakeraw()函数内部其实就是对 struct termios
    结构体进行了如下配置: 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;
   什么时候会使用原始模式?串口在 Linux 系统下是作为一种终端设备存在,终端通常会对用户的输入、
输出数据进行相应的处理,如前所述!
但是串口并不仅仅只扮演着人机交互的角色(数据以字符的形式传输、也就数说传输的数据其实字符对
应的 ASCII 编码值);串口本就是一种数据串行传输接口, 通过串口可以与其他设备或传感器进行数据传
输、通信,譬如很多 sensor 就使用了串口方式与主机端进行数据交互。 那么在这种情况下,我们就得使用原
始模式,意味着通过串口传输的数据不应进行任何特殊处理、不应将其解析成 ASCII 字符。



特殊控制字符: 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 流控。
打开串口设备
  1. 调用 open()函数时,使用了 O_NOCTTY 标志,该标志用于告知系统/dev/ttymxc2 它不会成为进程的控 制终端 fd = open(“/dev/ttymxc2”, O_RDWR | O_NOCTTY);

  2. 获取当前的配置参数:tcgetattr函数 man 3 tcgetattr" int tcgetattr(int fd, struct termios termios_p);
    成功返回 0;失败将返回-1,并且会设置 errno 以告知错误原因。
    struct termios old_cfg; if (0 > tcgetattr(fd, &old_cfg)) { /
    出错处理 */
    do_something(); }

  3. 假设我们需要采用原始模式进行串口数据通信。

  4. 调用<termios.h>头文件中申明的 cfmakeraw()函数可以将终端配置为原始模式:
    struct termios new_cfg;
    memset(&new_cfg, 0x0, sizeof(struct termios)); //配置为原始模式
    cfmakeraw(&new_cfg); 这个函数没有返回值

  5. new_cfg.c_cflag |= CREAD; //接收使能
    cfsetispeed(&new_cfg, B115200);

  6. 与设置波特率不同,设置数据位大小并没有现成可用的函数, 我们需要自己通过位掩码来操作、设置数 据位大小。 设置方法也很简单, 首先将c_cflag 成员中 CSIZE 位掩码所选择的几个 bit 位清零,然后再设置 数据位大小 new_cfg.c_cflag &=
    ~CSIZE; new_cfg.c_cflag |= CS8; //设置为 8 位数据位

  7. //奇校验使能 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;

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

  9. // 将停止位设置为 2 个比特
    new_cfg.c_cflag |= CSTOPB;

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

  11. 缓冲区的处理
    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: 包括前两种功能,即对尚未处理的输入/输出数据进行清空处理。
    以上这三个函数,调用成功时返回 0;失败将返回-1、并且会设置 errno 以指示错误类型。 通常我们会选择 tcdrain()或
    tcflush()函数来对串口缓冲区进行处理。譬如直接调用 tcdrain()阻塞:

  12. 写入配置
    #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);

#include "uart.h"

static int fd;
static struct termios old_cfg, new_cfg; // 该结构体用于串口的配置、读写

int uart_init(const uint8_t *device)
{
    fd = open(device, O_RDWR | O_NOCTTY);
    if (fd < 0)
    {
        perror("open fail");
    }

    if (tcgetattr(fd, &old_cfg))
    {
        perror(" old cfg fail");
        close(fd);
        return -1;
    }
    return 0;
}

int uart_cfg(uart_cfg_t *cfg)
{

    struct termios new_flag = {0};

    speed_t speed;

    cfmakeraw(&new_flag); // 终端配置为原始模式

    new_cfg.c_cflag |= CREAD; // 接受使能

    switch (cfg->baudrate)
    {
    case 1200:
        speed = B1200;
        break;
    case 9600:
        speed = B9600;
        break;
    case 115200:
        speed = B115200;

    default:
        speed = B115200;
        break;
    }

    if (0 > cfsetspeed(&new_cfg, speed)) // 波特率写入
    {
        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 */ // 规定read函数读取时具体操作
    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", app);
    printf("必选选项:\n");
    printf("  --dev=DEVICE     指定串口终端设备名称, 譬如--dev=/dev/ttymxc2\n");
    printf("  --type=TYPE      指定操作类型, 读串口还是写串口, 譬如--type=read(read 表示读、 write 表示写、其它值无效)\n\n");
    printf("可选选项:\n");
    printf("  --brate=SPEED    指定串口波特率, 譬如--brate=115200\n");
    printf("  --dbit=SIZE      指定串口数据位个数, 譬如--dbit=8(可取值为: 5/6/7/8)\n");
    printf("  --parity=PARITY  指定串口奇偶校验方式, 譬如--parity=N(N 表示无校验、 O 表示奇校验、 E 表示偶校验)\n");
    printf("  --sbit=SIZE       指定串口停止位个数, 譬如--sbit=1(可取值为: 1/2)\n");
    printf("  --help            查看本程序使用帮助信息\n\n");
}
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");
    }
}
static void async_io_init(void)
{
    struct sigaction sigact;
    memset(&sigact, 0, sizeof(sigact));

    sigact.sa_sigaction = io_handler; // Set the handler function
    sigact.sa_flags = SA_SIGINFO;

    if (sigaction(SIGRTMIN, &sigact, NULL) < 0)
    {
        perror("sigaction error");
        tcsetattr(fd, TCSANOW, &old_cfg);
        close(fd);
        exit(EXIT_FAILURE);
    }

    // Set the file descriptor to be asynchronous
    int flags = fcntl(fd, F_GETFL);
    if (flags < 0)
    {
        perror("fcntl F_GETFL error");
        tcsetattr(fd, TCSANOW, &old_cfg);
        close(fd);
        exit(EXIT_FAILURE);
    }
    //注意打开
    // if (fcntl(fd, F_SETFL, flags | O_ASYNC) < 0)
    // {
    //     perror("fcntl F_SETFL error");
    //     tcsetattr(fd, TCSANOW, &old_cfg);
    //     close(fd);
    //     exit(EXIT_FAILURE);
    // }

    // Set the process to receive SIGIO signals when data is available
    if (fcntl(fd, F_SETOWN, getpid()) < 0)
    {
        perror("fcntl F_SETOWN error");
        tcsetattr(fd, TCSANOW, &old_cfg);
        close(fd);
        exit(EXIT_FAILURE);
    }
}
void ttymxc2_test()
{
    // int fd;
    // // struct termios old_tty, new_tty;
    // fd = open("/dev/ttymxc2", O_RDWR | O_NOCTTY);
    // if (fd < 0)
    // {
    //     perror("open error");
    // }
    // if (tcgetattr(fd, &old_tty) < 0)
    // {
    //     do_something();
    // }
    // memset(&new_tty, 0, sizeof(new_tty));
    // cmakerraw(&new_tty);
    // new_tty.c_cflag |= CREAD;
    // cfsetospeed(&new_tty, B1152000);

    // new_tty.c_cflag &= ~CSIZE;
    // new_tty.c_cflag |= CS8;

    // new_tty.c_cflag |= (PARODD | PARENB);
    // new_tty.c_iflag |= INPCK;

    // new_tty.c_cflag |= PARENB;
    // new_tty.c_cflag &= ~PARODD;
    // new_tty.c_iflag |= INPCK;

    // new_tty.c_cflag &= ~PARENB;
    // new_tty.c_cflag &= ~INPCK;

    // new_tty.c_cflag |= ~CSTOPB;
    // new_tty.c_cflag |= CSTOPB;

    // new_tty.c_cc[VTIME] = 0;
    // new_tty.c_cc[VMIN] = 0;

    // tcdrain(fd);

    // tcsetattr(fd, TCSANOW, &new_tty);

    uart_cfg_t uart = {0};
    uint8_t *device = NULL;

    int rw_flag = -1;

    uint8_t w_buf[] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99};
    int n;
    uint8_t *param[255] = {0};
    for (size_t i = 0; i < n; i++)
    {
        if (!strncmp("--dev=", param[i], 6))
        {
            device = &param[i][6];
        }
        else if (!strncmp("--brate=", param[i], 8))
        {
            uart.baudrate = atoi(&param[i][8]);
        }
        else if (!strncmp("--dbit=", param[i], 7))
        {
            uart.dbit = atoi(&param[i][7]);
        }
        else if (!strncmp("--parity=", param[i], 9))
        {
            uart.parity = &param[i][9];
        }
        else if (!strncmp("--sbit=", param[i], 7))
        {
            uart.sbit = atoi(&param[i][7]);
        }
        else if (!strncmp("--type=", param[i], 7))
        {
            if (!strcmp("read", &param[i][7]))
            {
                rw_flag = 0;
            }
            else if (!strcmp("write", &param[i][7]))
            {
                rw_flag = 1;
            }
        }
        else if (!strcmp("--help", param[n]))
        {
            show_help(param[0]);
            exit(0);
        }
    }

    if (device == NULL && rw_flag == -1)
    {
        fprintf(stderr, "Error");
    }

    if (uart_init(device))
        exit(EXIT_FAILURE);

    if (uart_cfg(&uart))
    {
        tcsetattr(fd, TCSANOW, &old_cfg);
        close(fd);
        exit(EXIT_FAILURE);
    }

    switch (rw_flag)
    {
    case 1:
        async_io_init();
        for (;;)
        {
            sleep(1);
        }
        break;
    case 2:
        for (;;)
        {
            write(fd, w_buf, 8);
            sleep(1);
        }

    default:
        break;
    }

    tcsetattr(fd, TCSANOW, &old_cfg);
    close(fd);
    exit(EXIT_SUCCESS);
}
看门狗

重复计数的计数器,防止程序跑偏

I.MX6UL/I.MX6ULL SoC 集成了两个看门狗定时器(WDOG): WDOG1 和 WDOG2; WDOG2 用于安
全目的,而 WDOG1 则是一个普通的看门狗,支持产生中断信号以及复位 CPU。
Linux 系统中所注册的看门狗外设,都会在/dev/目录下生成对应的设备节点(设备文件),设备节点名
称通常为 watchdogX(X 表示一个数字编号 0、 1、 2、 3 等),譬如/dev/watchdog0、 /dev/watchdog1 等,通
过这些设备节点可以控制看门狗外设

在这里插入图片描述

这个 watchdog0 其实就是 I.MX6U 的 WDOG1 所对应的设备节点, 从图中可知,除了/dev/watchdog0 之
外,还有一个 watchdog 设备节点,这个设备节点的名称没有后面的数字编号,这个又是什么意思呢?因为
系统中可能注册了多个看门狗设备, /dev/watchdog 设备节点则代表系统默认的看门狗设备; 通常这指的就
是 watchdog0, 所以, 上图中, /dev/watchdog 其实就等于/dev/watchdog0,也就意味着它俩代表的是同一个
硬件外设

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

首先在我们的应用程序中,需要包含头文件<linux/watchdog.h>头文件, 该头文件中定义了一些 ioctl 指
令宏,每一个不同的指令宏表示向设备请求不同的操作
比 较 常 用 指 令 包 括 :
在这里插入图片描述

cmake

Makefile 带来的好处就是——“自动化编译”,一旦写好,只需要一个 make 命令,整个工程完全按照
Makefile 文件定义的编译规则进行自动编译,极大的提高了软件开发的效率。 大都数的 IDE 都有这个工具,
譬如 Visual C++的 nmake、 linux 下的 GNU make、 Qt 的 qmake 等等, 这些 make 工具遵循着不同的规范和
标准, 对应的 Makefile 文件其语法、 格式也不相同, 这样就带来了一个严峻的问题:如果软件想跨平台,必
须要保证能够在不同平台下编译, 而如果使用上面的 make 工具,就得为每一种标准写一次 Makefile,这将
是一件让人抓狂的工作。
而 cmake 就是针对这个问题所诞生, 允许开发者编写一种与平台无关的 CMakeLists.txt 文件来制定整个
工程的编译流程, 再根据具体的编译平台,生成本地化的 Makefile 和工程文件,最后执行 make 编译

因此,对于大多数项目, 我们应当考虑使用更自动化一些的 cmake 或者 autotools 来生成 Makefile

开放源代码。我们可以直接从 cmake 官网 https://cmake.org/下载到它的源代码;
⚫ 跨平台。 cmake 并不直接编译、构建出最终的可执行文件或库文件, 它允许开发者编写一种与平台
无关的 CMakeLists.txt 文件来制定整个工程的编译流程, cmake 工具会解析 CMakeLists.txt 文件语
法规则,再根据当前的编译平台,生成本地化的 Makefile 和工程文件,最后通过 make 工具来编译
整个工程;所以由此可知, cmake 仅仅只是根据不同平台生成对应的 Makefile,最终还是通过 make
工具来编译工程源码,但是 cmake 却是跨平台的。
⚫ 语法规则简单。 Makefile 语法规则比较复杂,对于一个初学者来说,通常并不那么友好, 并且
Makefile 语法规则在不同平台下往往是不一样的;而 cmake 依赖的是 CMakeLists.txt 文件,该文件
的语法规则与平台无关,并且语法规则简单、容易理解! cmake 工具通过解析 CMakeLists.txt 自动
帮我们生成 Makefile,这样就不需要我们自己手动编写 Makefile 了。

在这里插入图片描述

sudo apt-get install cmake
安装完成之后可以通过 cmake --version 命令查看 cmake
的版本号
在这里插入图片描述
通过 file 命令可以查看到 hello 是一个 x86-64 架构下的可执行文件

MQTT

MQTT 是一种基于客户端服务端架构的发布/订阅模式的消息传输协议。它的设计思想是轻巧、开放、
简单、规范,易于实现。这些特点使得它对很多场景来说都是很好的选择,特别是对于受限的环境如机器与
机器的通信(M2M)以及物联网环境(IoT)
可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服
务。作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的

MQTT 协议是为工作在低带宽、不可靠网络的远程传感器和控制设备之间的通讯而设计的协议,它具
有以下主要的几项特性:
①、 使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合。
②、基于 TCP/IP 提供网络连接。
主流的 MQTT 是基于 TCP 连接进行数据推送的,但是同样也有基于 UDP 的版本,叫做 MQTT-SN。这
两种版本由于基于不同的连接方式,优缺点自然也就各有不同了。
③、支持 QoS 服务质量等级。
根据消息的重要性不同设置不同的服务质量等级。
④、 小型传输, 开销很小,协议交换最小化,以降低网络流量。
这就是为什么在介绍里说它非常适合"在物联网领域,传感器与服务器的通信,信息的收集",要知道嵌
入式设备的运算能力和带宽都相对薄弱,使用这种协议来传递消息再适合不过了, 在手机移动应用方面,
MQTT 是一种不错的 Android 消息推送方案。
⑤、使用 will 遗嘱机制来通知客户端异常断线。
⑥、基于主题发布/订阅消息,对负载内容屏蔽的消息传输。
⑦、支持心跳机制

这种通讯协议必
须满足以下条件:
⚫ 易于实现,服务器必须要实现成千上万个客户端的接入
⚫ 数据传输的服务质量可控,根据数据的重要性和特性,设置不同等级的服务质量
⚫ 占用带宽小,单次数据量小,但不能出错
⚫ 必须能够适应高延迟、掉线、断网等网络通信不可靠的风险
⚫ 设备连接状态可知,云端与设备端保持长连接
通过以上几个条件可知
MQTT 服务器可以连接大量的远程传感器和控制设备,与远程客户端保持长连接,具有一定的实
时性。
⚫ 云端向设备端发送消息,设备端可以在最短的时间内接收到并作出回应。
⚫ MQTT 更适合需要实时控制的场合,尤其适合执行器。
⚫ 云端与客户端需要保持长连接,要能够获取到设备的连接状态,就需要时不时地发送心跳包,这就
不会省电,所以, MQTT 并不适合低功耗场合。

目前 MQTT 主流版本有两个,分别是 MQTT3.1.1 和 MQTT5
但是 MQTT3.1.1作为一个经典的版本, 目前仍然是主流版本, 能够满足大部分实际需求。
MQTT5 是在 MQTT3.1.1 的基础上进行了升级,因此 MQTT5 是完全兼容 MQTT3.1.1 的

MQTT 是一种基于客户端-服务端架构的消息传输协议,所以在 MQTT 协议通信中,有两个最为重要的
角色,它们便是服务端和客户端

MQTT 服务端通常是一台服务器(broker), 它是 MQTT 信息传输的枢纽,负责将 MQTT 客户端发送
来的信息传递给 MQTT 客户端; MQTT 服务端还负责管理 MQTT 客户端,以确保客户端之间的通讯顺畅,
保证 MQTT 信息得以正确接收和准确投递

MQTT 客户端可以向服务端发布信息,也可以从服务端收取信息; 我们把客户端发送信息的行为称为
“发布”信息。而客户端要想从服务端收取信息,则首先要向服务端“订阅”信息。“订阅”信息这一操作
很像我们在使用微信时“关注”了某个公众号, 当公众号的作者发布新的文章时, 微信官方会向关注了该公
众号的所有用户发送信息,告诉他们有新文章更新了,以便用户查看

上面我们讲到了,客户端想要从服务器获取信息,首先需要订阅信息,那客户端如何订阅信息呢?这里
我们要引入“主题(Topic)”的概念,“主题”在 MQTT 通信中是一个非常重要的概念,客户端发布信息
以及订阅信息都是围绕“主题”来进行的,并且 MQTT 服务端在管理 MQTT 信息时,也是使用“主题”来
控制的。
客户端发布消息时需要为消息指定一个“主题”, 表示将消息发布到该主题;而对于订阅消息的客户端
来说,可通过订阅“主题” 来订阅消息,这样当其它客户端或自己(当前客户端)向该主题发布消息时,
MQTT 服务端就会将该主题的信息发送给该主题的订阅者(客户端)

正是因为有了服务端对 MQTT 信息的接收、储存、处理和发送,客户端在发布和订阅信息时,可以相
互独立、 且在空间上可以分离、 时间上可以异步, 这就是 MQTT 发布/订阅的特性: 客户端相互独立、空间
上可分离、 时间上可异步, 具体介绍如下:
⚫ 客户端相互独立: MQTT 客户端是一个个独立的个体, 它们无需了解彼此的存在,依然可以实现
信息交流。 譬如在上面的实例中,开发板客户端在发布“芯片温度”信息时, 开发板客户端本身完
全不知道有多少个 MQTT 客户端订阅了“芯片温度”这一主题; 而订阅了“芯片温度”主题的手
机和电脑客户端也完全不知道彼此的存在, 大家只要订阅了“芯片温度” 这一主题, MQTT 服务端
就会在每次收到新信息时,将信息发送给订阅了“芯片温度”主题的客户端。
⚫ 空间上分离: 空间上分离相对容易理解, MQTT 客户端以及 MQTT 服务端它们在通信时是处于同
一个通信网络中的, 这个网络可以是互联网或者局域网; 只要客户端联网,无论他们远在天边还是
近在眼前,都可以实现彼此间的通讯交流;其实网络通信本就是如此,所以并不是 MQTT 通信所
特有的。
⚫ 时间上可异步: MQTT 客户端在发送和接收信息时无需同步。这一特点对物联网设备尤为重要,
前面我们也介绍了, MQTT 从诞生之初就是专为低带宽、高延迟或不可靠的网络而设计的,高延
迟和不可靠网络必然就会导致时间上的异步; 物联网设备在运行过程中发生意外掉线是非常正常
的情况, 我们使用上面的实例二的场景来作说明, 当开发板在运行过程中,可能会由于突然断电
(假设开发板是通过电源适配器供电的)导致掉线,这时开发板会断开与 MQTT 服务端的连接。
假设此时我们的手机客户端向开发板客户端所订阅的“LED 控制”主题发布了信息,而开发板恰
恰不在线, 这时, MQTT 服务端可以将“LED 控制”主题的新信息保存,待开发板客户端再次上
线后,服务端再将“LED 控制”信息推送给开发板。 所以这就必然导致了,手机发送信息与开发
板接收信息在时间上是异步的

移植mqtt 的源码
构建编译
mqtt 代码示例
typedef struct
{
int payloadlen; //负载长度
void* payload; //负载
int qos; //消息的 qos 等级
int retained; //消息的保留标志
int dup; //dup 标志(重复标志)
int msgid; //消息标识符,也就是前面说的 packetId
......
} MQTTClient_message;

当客户端发布消息时就需要实例化一个 MQTTClient_message 对象,同理,当客户端接收到消息时,其实也就是接收
 到了 MQTTClient_message 对象。 通常在实例化MQTTClient_message对象时会使用MQTTClient_message_initializer 宏对其进行初始化

在连接服务端之前,需要创建一个客户端对象,使用 MQTTClient_create 函数创建:
int MQTTClient_create(MQTTClient *handle,
const char *serverURI,
const char *clientId,
int persistence_type,
void *persistence_context
);
handle: MQTT 客户端句柄;
serverURL: MQTT 服务器地址;
clientId: 客户端 ID;
persistence_type: 客户端使用的持久化类型:

MQTTCLIENT_PERSISTENCE_NONE: 使用内存持久性。如果运行客户端的设备或系统出现故障
或关闭,则任何传输中消息的当前状态都会丢失,并且即使在 QoS1 和 QoS2 下也可能无法传递某
些消息。
⚫ MQTTCLIENT_PERSISTENCE_DEFAULT: 使用默认的(基于文件系统)持久性机制。传输中消
息的状态保存在文件系统中,并在意外故障的情况下提供一些防止消息丢失的保护。
⚫ MQTTCLIENT_PERSISTENCE_USER: 使用特定于应用程序的持久性实现。使用这种类型的持久
性可以控制应用程序的持久性机制。应用程序必须实现 MQTTClient_persistence 接口。
persistence_context: 如果使用 MQTTCLIENT_PERSISTENCE_NONE 持久化类型,则该参数应设置为
NULL。如果选择的是 MQTTCLIENT_PERSISTENCE_DEFAULT 持久化类型,则该参数应设置为持久化目
录的位置,如果设置为 NULL,则持久化目录就是客户端应用程序的工作目录。
返回值: 客户端对象创建成功返回 MQTTCLIENT_SUCCESS,失败将返回一个错误码。

MQTTClient client;
int rc;

/* 创建 mqtt 客户端对象 */
if (MQTTCLIENT_SUCCESS !=
(rc = MQTTClient_create(&client, "tcp://iot.ranye-iot.net:1883",
"dt_mqtt_2_id",
MQTTCLIENT_PERSISTENCE_NONE, NULL))) {
printf("Failed to create client, return code %d\n", rc);
return EXIT_FAILURE;
}

 "tcp://iot.ranye-iot.net:1883"地址中,第一个冒号前面的 tcp 表示我们使用的是 TCP 连接;后面的
1883 表示 MQTT 服务器对应的端口号。
连接服务端
int MQTTClient_connect(MQTTClient handle,
MQTTClient_connectOptions *options
);

handle: 客户端句柄;
options: 一个指针。指向一个 MQTTClient_connectOptions 结构体对象。 MQTTClient_connectOptions 结
构体中包含了 keepAlive、 cleanSession 以及一个指向 MQTTClient_willOptions 结构体对象的指针 will_opts;
MQTTClient_willOptions 结构体包含了客户端遗嘱相关的信息, 遗嘱主题、遗嘱内容、遗嘱消息的 QoS 等
级、遗嘱消息的保留标志等。
返回值: 连接成功返回 MQTTCLIENT_SUCCESS,是否返回错误码:
⚫ 1: 连接被拒绝。不可接受的协议版本,不支持客户端的 MQTT 协议版本
⚫ 2: 连接被拒绝:标识符被拒绝
⚫ 3: 连接被拒绝:服务器不可用
⚫ 4: 连接被拒绝:用户名或密码错误
⚫ 5: 连接被拒绝:未授权
⚫ 6-255: 保留以备将来使用

typedef struct
{
int keepAliveInterval; //keepAlive
int cleansession; //cleanSession
MQTTClient_willOptions *will; //遗嘱相关
const char *username; //用户名
const char *password; //密码
int reliable; //控制同步发布消息还是异步发布消息
...... 
......
} MQTTClient_connectOptions;

typedef struct
{
const char *topicName; //遗嘱主题
const char *message; //遗嘱内容
int retained; //遗嘱消息的保留标志
int qos; //遗嘱消息的 QoS 等级
......
......
} MQTTClient_willOptions;


MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
MQTTClient_willOptions will_opts = MQTTClient_willOptions_initializer;
......
/* 连接服务器 */
will_opts.topicName = "dt2914/willTopic"; //遗嘱主题
will_opts.message = "Abnormally dropped"; //遗嘱内容
will_opts.retained = 1; //遗嘱保留消息
will_opts.qos = 0; //遗嘱 QoS 等级
conn_opts.will = &will_opts;
conn_opts.keepAliveInterval = 30; //客户端 keepAlive 间隔时间
conn_opts.cleansession = 0; //客户端 cleanSession 标志
conn_opts.username = "dt_mqtt_2"; //用户名
conn_opts.password = "dt291444"; //密码
if (MQTTCLIENT_SUCCESS !=
(rc = MQTTClient_connect(client, &conn_opts))) {
printf("Failed to connect, return code %d\n", rc);
return EXIT_FAILURE;

通常在定义 MQTTClient_connectOptions 对象时会使用 MQTTClient_connectOptions_initializer 宏对其进
行初始化操作;而在定义 MQTTClient_willOptions 对象时使用 MQTTClient_willOptions_initializer 宏对其初
始化。
}

调用 MQTTClient_setCallbacks 函数为应用程序设置回调函数, MQTTClient_setCallbacks 可设置多个回
调函数,包括: 断开连接时的回调函数 cl (当客户端检测到自己掉线时会执行该函数,如果将其设置为 NULL
表示应用程序不处理断线的情况) 、 接收消息的回调函数 ma(当客户端接收到服务端发送过来的消息时执
行该函数,必须设置此函数否则客户端无法接收消息)、 发布消息的回调函数 dc(当客户端发布的消息已经确认发送时执行该回调函数,如果你的应用程序采用同步方式发布消息或者您不想检查是否成功发送时,
您可以将此设置为 NULL)

int MQTTClient_setCallbacks(MQTTClient handle,
void *context,
MQTTClient_connectionLost *cl,
MQTTClient_messageArrived *ma,
MQTTClient_deliveryComplete *dc
);


> handle: 客户端句柄; context: 执行回调函数的时候,会将 context 参数传递给回调函数,因为每一个回调函数都设置了一个
> 参数用来接收 context 参数。 cl: 一个 MQTTClient_connectionLost 类型的函数指针,如下:
> typedef void MQTTClient_connectionLost(void *context, char *cause);
> ma: 一个 MQTTClient_messageArrived 类型的函数指针,如下:
typedef int MQTTClient_messageArrived(void *context, char *topicName,
int topicLen, MQTTClient_message *message);
参 数 topicName 表 示消息的主题名 , topicLen 表示主题名的长 度 ;参数 message 指向一 个
MQTTClient_message 对象,也就是客户端所接收到的消息。
dc: 一个 MQTTClient_deliveryComplete 类型的函数指针,如下:

typedef void MQTTClient_deliveryComplete(void* context, MQTTClient_deliveryToken dt);
> typedef void MQTTClient_connectionLost(void *context, char *cause); 参数
> cause 表示断线的原因,是一个字符串。 ma: 一个 MQTTClient_messageArrived 类型的函数指针,如下:
> typedef int MQTTClient_messageArrived(void *context, char *topicName,
> int topicLen, MQTTClient_message *message); 参 数 topicName 表 示消息的主题名 ,
> topicLen 表示主题名的长 度 ;参数 message 指向一 个 MQTTClient_message
> 对象,也就是客户端所接收到的消息。 dc: 一个 MQTTClient_deliveryComplete 类型的函数指针,如下:
> typedef void MQTTClient_deliveryComplete(void* context,
> MQTTClient_deliveryToken dt); 参 数 dt 表 示 MQTT 消 息 的 值 , 将 其 称 为 传 递 令
> 牌 。 发 布 消 息 时 ( 应 用 程 序 通 过 MQTTClient_publishMessage 函数发布消息) , MQTT
> 协议会返回给客户端应用程序一个传递令牌; 应用程 序可以通过将调用
> MQTTClient_publishMessage()返回的传递令牌与传递给此回调的令牌进行匹配来检查消 息是否已成功发布。
> 前面提到了“同步发布消息”这个概念,既然有同步发布,那必然有异步发布,确实如何!那如何控制 是同步发布还是异步发布呢? 就是通过
> MQTTClient_connectOptions 对象中的 reliable 成员控制的,这是一 个布尔值,当 reliable=1
> 时使用同步方式发布消息,意味着必须完成当前正在发布的消息(收到确认) 之后 才能发布另一个消息;如果 reliable=0
> 则使用异步方式发布消息。 当使用 MQTTClient_connectOptions_initializer 宏对
> MQTTClient_connectOptions 对象进行初始化时, reliable 标志被初始化为 1,所以默认是使用了同步方式。
> 返回值: 成功返回 MQTTCLIENT_SUCCESS,失败返回 MQTTCLIENT_FAILURE。 注意:调用
> MQTTClient_setCallbacks 函数设置回调必须在连接服务器之前完成!


static void delivered(void *context, MQTTClient_deliveryToken dt)
{
printf("Message with token value %d delivery confirmed\n", dt);
}
static int msgarrvd(void *context, char *topicName, int topicLen,
MQTTClient_message *message)
{
printf("Message arrived\n");
printf("topic: %s\n", topicName);
printf("message: <%d>%s\n", message->payloadlen, (char *)message->payload);
MQTTClient_freeMessage(&message); //释放内存
MQTTClient_free(topicName); //释放内存
return 1;
}
static void connlost(void *context, char *cause)
{
printf("\nConnection lost\n");
printf(" cause: %s\n", cause);
}
int main(void)
{
......
/* 设置回调 */
if (MQTTCLIENT_SUCCESS !=
(rc = MQTTClient_setCallbacks(client, NULL, connlost,
msgarrvd, delivered))) {
printf("Failed to set callbacks, return code %d\n", rc);
return EXIT_FAILURE;
}

对于 msgarrvd 函数有两个点需要注意:
⚫ 退出函数之前需要释放消息的内存空间,必须调用 MQTTClient_freeMessage 函数;同时也要释放
主题名称占用的内存空间,必须调用 MQTTClient_free。
⚫ 函数的返回值。此函数的返回值必须是 01,返回 1 表示消息已经成功处理; 返回 0 则表示消息
处理存在问题,在这种情况下,客户端库将重新调用 MQTTClient_messageArrived()以尝试再次将
消息传递给客户端应用程序,所以返回 0 时不要释放消息和主题所占用的内存空间,否则重新投
递失败
......
}

当 客 户 端 成 功 连 接 到 服 务 端 之 后 , 便 可 以 发 布 消 息 或 订 阅 主 题 了 , 应 用 程 序 通 过

MQTTClient_publishMessage 库函数来发布一个消息:
int MQTTClient_publishMessage(MQTTClient handle,
const char *topicName,
MQTTClient_message *msg,
MQTTClient_deliveryToken *dt
handle: 客户端句柄;
topicName: 主题名称。向该主题发布消息。
msg: 指向一个 MQTTClient_message 对象的指针。
dt: 返回给应用程序的传递令牌。
返回值: 成功返回 MQTTCLIENT_SUCCESS,失败返回错误码。
使用示例
MQTTClient_message pubmsg = MQTTClient_message_initializer;
MQTTClient_deliveryToken token;
......
/* 发布消息 */
pubmsg.payload = "online"; //消息内容
pubmsg.payloadlen = 6; //消息的长度
pubmsg.qos = 0; //QoS 等级
pubmsg.retained = 1; //消息的保留标志
if (MQTTCLIENT_SUCCESS !=
(rc = MQTTClient_publishMessage(client, "dt2914/testTopic", &pubmsg, &token))) {
printf("Failed to publish message, return code %d\n", rc);
return EXIT_FAILURE;
}
);

客户端应用程序调用 MQTTClient_subscribe 函数来订阅主题:
int MQTTClient_subscribe(MQTTClient handle,
const char *topic,
int qos
);
handle: 客户端句柄;
topic: 主题名称。客户端订阅的主题。
qos: QoS 等级。
返回值: 成功返回 MQTTCLIENT_SUCCESS,失败返回错误码。
使用示例

if (MQTTCLIENT_SUCCESS !=
(rc = MQTTClient_subscribe(client, "dt2914/testTopic", 0))) {
printf("Failed to subscribe, return code %d\n", rc);
return EXIT_FAILURE;

当客户端想取消之前订阅的主题时,可调用 MQTTClient_unsubscribe 函数,如下所示:
int MQTTClient_unsubscribe(MQTTClient handle,
const char *topic
);
当客户端需要主动断开与客户端连接时,可调用 MQTTClient_disconnect 函数:
int MQTTClient_disconnect(MQTTClient handle,
int timeout
);
handle: 客户端句柄;
timeout: 超时时间。 客户端将断开连接延迟最多 timeout 时间(以毫秒为单位),以便完成正在进行中
的消息传输。
返回值: 如果客户端成功从服务器断开连接,则返回 MQTTCLIENT_SUCCESS; 如果客户端无法与服
务器断开连接,则返回错误代码。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值