前言

此篇文章为学习 Linux系统编程01:文件I/O 部分的笔记

1. open/close函数

1.1 open

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

flags

flags 用于指定文件的打开/创建模式,这个参数可由以下三个互斥的常量(定义于 fcntl.h)通过逻辑或(|)连接:

O_RDONLY      只读模式 
O_WRONLY      只写模式 
O_RDWR        读写模式

其他可选常量:

常量含义
O_APPEND每次写操作都写入文件的末尾
O_CREAT如果指定文件不存在,则创建这个文件
O_EXCL如果要创建的文件已存在,则返回 -1,并且修改 errno 的值
O_TRUNC如果文件存在,并且以只写/读写方式打开,则清空文件全部内容
O_NOCTTY如果路径名指向终端设备,不要把这个设备用作控制终端
O_NONBLOCK如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O设置为非阻塞模式(nonblocking mode)

以下用于同步输入输出

常量含义
O_DSYNC等待物理 I/O 结束后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新
O_RSYNCread 等待所有写入同一区域的写操作完成后再进行
O_SYNC等待物理 I/O 结束后再 write,包括更新文件属性的 I/O

 

mode

mode 和 fopen() 函数的 mode 参数相同。mode 指定文件的打开模式:

r = 4,w = 2, x = 1

  • r:只读方式打开一个文本文件(该文件必须存在)
  • r+:可读可写方式打开一个文本文件(该文件必须存在)
  • w:只写方式打开一个文本文件(若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件)
  • w+:可读可写方式创建一个文本文件(若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件)
  • a:追加方式打开一个文本文件(若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留))
  • a+:可读可写追加方式打开一个文本文件(若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾后,即文件原先的内容会被保留。 (原来的EOF符不保留))

a 和 a+ 的区别:a 不能读,a+ 可以读

  • rb:只读方式打开一个二进制文件(使用法则同r)
  • rb+:可读可写方式打开一个二进制文件(使用法则同r+)
  • wb:只写方式打开一个二进制文件(使用法则同w)
  • wb+:可读可写方式生成一个二进制文件(使用法则同w+)
  • ab:追加方式打开一个二进制文件(使用法则同a)
  • ab+:可读可写方式追加一个二进制文件(使用法则同a+)

需要注意的是,当 flags 为 O_CREAT 或 O_TMPFILE 时,必须提供 mode 参数;否则 mode 参数将不起作用。

返回值
open() 的返回值是一个 int 类型的文件描述符,打开失败返回 -1。

 

1.2 close

NAME
close - 关闭一个文件描述符

总览

 #include <unistd.h> 
int close(int fd);

描述
close 关闭 一个文件描述符 , 使不在指向任何文件和可以在新的文件操作中被再次使用

任何与此文件相关联的以及程序所拥有的锁, 都会被删除 (忽略那些持有锁的文件描述符)

假如 fd 是最后一个文件描述符与此资源相关联 , 则这个资源将被释放.  
若此描述符是最后一个引用到此文件上的, 则文件将使用 unlink(2) 删除.

返回值

close 返回 0 表示 成功 , 或者 -1 表示 有 错误 发生 .

错误信息

EBADF  fd 不是 一个 有效 的 已 被 打开 的 文件 的 描述符

EINTR  The close() 调用 被 一 信号 中断.

EIO    I/O 有 错误 发生

 

1.3 文件权限

在Linux系统中,文件的最终权限是由文件的创建模式(mode)和用户掩码(umask)共同决定的。

文件的创建模式(mode)是指在使用系统调用如open()creat()创建文件时,通过指定一个八进制数来表示文件的权限。通常使用三个数字来表示权限,分别对应所有者、所属组和其他用户的权限。每个数字由三个位组成,分别代表读(r)、写(w)和执行(x)权限。例如,权限为 rw-r--r-- 的文件模式用八进制表示就是 644

用户掩码(umask)是用来屏蔽(取消)文件创建模式中的某些权限。默认情况下,umask 设置为 022,表示屏蔽掉写权限(w)和执行权限(x)对其他用户。所以如果文件的模式是 644,经过 umask 的处理,其他用户的权限就变为 444,即只有读权限。

计算文件的最终权限可以使用如下公式:

最终权限 = 文件模式(mode) & (~用户掩码(umask))

例如,假设文件的模式是 755,umask 是 022,则最终权限为:

755 & (~022) = 755 & 755 = 755

所以最终权限仍然是 755

另外,需要注意的是,umask 的值通常是用八进制数表示的。在设置 umask 时,一般使用四个数字,例如 umask 022

root@freecho:/opt/C/file_IO/test# umask
0022  # 755

 

1.4 代码样例

open1.c

读取文件,文件存在则以只读 O_RDONLY 方式打开,并且清空文件全部内容 O_TRUNC,如果不存在则创建文件 O_CREAT

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main()
{
    int fd;
    // 0644 第一个0表示8进制, 644为权限
    fd = open("./dict.cp", O_RDONLY | O_CREAT | O_TRUNC, 0644); // 644 rw-r--r--

    printf("fd = %d\n", fd);

    close(fd); // 关闭文件描述符  成功0 失败-1

    return 0;
}

/*
root@freecho:/opt/C/file_IO/test# ./open1
fd = 3
*/
root@freecho:/opt/C/file_IO/test# ./open1
fd = 3

 

open2.c

打开不存在的文件

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main()
{
    int fd;
    fd = open("./dict1.cp", O_RDONLY); 

    printf("fd = %d, errno = %d:%s\n", fd, errno, strerror(errno));

    close(fd); // 关闭文件描述符  成功0 失败-1

    return 0;
}
root@freecho:/opt/C/file_IO/test# ./open2
fd = -1, errno = 2:No such file or directory

 

open3.c

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main()
{
    int fd;

    fd = open("mydir", O_WRONLY); // 打开文件夹

    printf("fd = %d, errno = %d:%s\n", fd, errno, strerror(errno));

    close(fd); // 关闭文件描述符  成功0 失败-1

    return 0;
}
root@freecho:/opt/C/file_IO/test# ./open3
fd = -1, errno = 21:Is a directory

 

1.5 错误及处理

open 常见错误:

  1. 打开文件不存在
  2. 以写方式打开只读文件(权限问题)
  3. 以只写方式打开目录
  4. 当 open 出错时,程序会自动设置 errno,可以通过 strerror(errno)来查看报错数字的含义

错误处理函数

  1. printf("xxx error: %d\n", errno);
    
  2. char *strerror(int errnum);
    	printf("xxx error: %s\n", strerror(errno));
    
  3. void perror(const char *s);
    	perror("xxx error");
    

 

2. read/write函数

2.1 read

Man手册

NAME
read - 在文件描述符上执行读操作

概述

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

描述

read( ) 从文件描述符 fd 中读取 count 字节的数据并放入从 buf 开始的缓冲区中.

如果 count 为零,read( )返回0,不执行其他任何操作. 如果 count 大于SSIZE_MAX,那么结果将不可预料.

返回值

成功时返回读取到的字节数(为零表示读到文件描述符), 此返回值受文件剩余字节数限制

当返回值小于指定的字节数时 并不意味着错误;这可能是因为当前可读取的字节数小于指定的 字节数(比如已经接近文件结尾,或者正在从管道或者终端读取数 据,或者read( )被信号中断)

发生错误时返回-1,并置 errno 为相应值.在这种情况下无法得知文件偏移位置是否有变化.

错误代码

EINTR  在读取到数据以前调用被信号所中断.

EAGAIN 使用 O_NONBLOCK 标志指定了非阻塞式输入输出,但当前没有数据可读.

EIO    输入输出错误.可能是正处于后台进程组进程试图读取其 控制终端,但读操作无效,或者被信号SIGTTIN所阻塞, 或者其进程组是孤儿进程组.也可能执行的是读磁盘或者 磁带机这样的底层输入输出错误.

EISDIR fd 指向一个目录.

EBADF  fd 不是一个合法的文件描述符,或者不是为读操作而打开.

EINVAL fd 所连接的对象不可读.

EFAULT buf 超出用户可访问的地址空间.

也可能发生其他错误,具体情况和 fd 所连接的对象有关.  POSIX 允许  read  在读取了一定量的数据后被信号所中断,并返回  -1(且  errno  被设置为EINTR),或者返回已读取的数据量.

2.2 write

Man手册

NAME
write -在一个文件描述符上执行写操作

概述

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

描述
write 向文件描述符 fd 所引用的文件中写入 从 buf 开始的缓冲区中 count 字节的数据.

POSIX规定,当使用了write()之后再使用 read(),那么读取到的应该是更新后的数据. 但请注意并不是所有的文件系统都是 POSIX兼容的.

返回值
成功时返回所写入的字节数(若为零则表示没有写入数据).

错误时返回-1,并置errno为相应值.

若count为零,对于普通文件无任何影响,但对特殊文件将产生不可预料的后果.

错误代码

EBADF  fd 不是一个合法的文件描述符或者没有以写方式打开.

EINVAL fd 所指向的对象不可写.

EFAULT buf 不在用户可访问地址空间内.

EPIPE  fd  连接到一个管道,或者套接字的读方向一端已关闭.此时写进程  将接收到  SIGPIPE  信号;如果此信号被捕获,阻塞或忽略,那么将返回错误EPIPE.

EAGAIN 读操作阻塞,但使用 O_NONBLOCK 指定了非阻塞式输入输出.

EINTR  在写数据以前调用被信号中断.

ENOSPC fd 指向的文件所在的设备无可用空间.

EIO    当编辑一个节点时发生了底层输入输出错误.

可能发生了其他错误,取决于 fd 所连接的对象.

2.3 代码样例

mycp.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    char buf[1024];

    int n =0;

    int fd1 = open(argv[1], O_RDONLY); // read
    if (fd1 == -1) {
        perror("open argv1 error");
        exit(1);
    }

    int fd2 = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0664); // rw-rw--r
    if (fd1 == -1) {
        perror("open argv2 error");
        exit(1);
    }

    // read 返回为读取到的字节数
    while ((n = read(fd1, buf, 1024)) != 0) { // 0:读到文件结尾结束循环
        if (n < 0) {
            perror("read error");
            break;
        }
        write(fd2, buf, n);
    }

    close(fd1);
    close(fd2);

    return 0;
}
root@freecho:/opt/C/file_IO/rw_test# make
gcc mycp.c -o mycp
root@freecho:/opt/C/file_IO/rw_test# ls
makefile  mycp  mycp.c  test1.txt
root@freecho:/opt/C/file_IO/rw_test# ./mycp test1.txt test2.txt
root@freecho:/opt/C/file_IO/rw_test# ls
makefile  mycp  mycp.c  test1.txt  test2.txt
root@freecho:/opt/C/file_IO/rw_test# cat test2.txt
Hello 这是一个cp测试文件
root@freecho:/opt/C/file_IO/rw_test# ./mycp 123
open argv1 error: No such file or directory

 

2.4 strace命令

strace是一个用于跟踪和分析程序系统调用的工具。它在Linux系统上广泛使用,可用于诊断程序执行时的问题,分析程序与操作系统之间的交互,以及定位程序的错误和性能瓶颈。strace可以帮助开发人员和系统管理员深入了解程序的运行情况,包括系统调用、信号、文件操作、网络通信等。

使用strace命令时,它会启动被跟踪的目标程序,并输出程序执行过程中的系统调用和信号等相关信息。可以用于追踪应用程序、脚本、甚至是其他系统命令的执行。

常见用法:

strace <command>

示例:

strace -o output.txt ls  # 将输出重定向到文件
strace -e trace=open,read,write ls  # 只跟踪指定的系统调用
strace -p <PID>  # 跟踪正在运行的进程

strace的输出会显示程序执行过程中每个系统调用的结果、参数和返回值,以及相应的错误信息。通过分析strace的输出,可以帮助定位程序运行时的问题,识别潜在的错误或性能瓶颈,进而进行适当的优化和调试。

 

2.5 预读入缓输出

通过比较 fgetc/fputcread/write 的执行速度,了解预读入缓输出

库函数: getc_cmp_read.c

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *fp, *fp_out;
    int n;

    fp = fopen("test.txt", "r");
    if (fp == NULL) {
        perror("fopen error");
        exit(1);
    }

    fp_out = fopen("test.cp", "w");
    if (fp_out == NULL) {
        perror("fopen error");
        exit(1);
    }

    while ((n = fgetc(fp)) != EOF) {
        fputc(n, fp_out);
    }

    fclose(fp);
    fclose(fp_out);

    return 0;
}

系统调用:read_cmp_getc.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

#define N 1

int main(int argc, char *argv[])
{
    int fd, fd_out;
    int n;
    char buf[N];

    fd = open("test.txt", O_RDONLY); // read
    if (fd < 0) {
        perror("open test.txt error");
        exit(1);
    }

    fd_out = open("test.cp", O_WRONLY | O_CREAT | O_TRUNC, 0664); // rw-rw--r
    if (fd_out < 0) {
        perror("open test.cp error");
        exit(1);
    }

    // read 返回为读取到的字节数
    while ((n = read(fd, buf, N))) { // 0:读到文件结尾结束循环
        if (n < 0) {
            perror("read error");
            break;
        }
        write(fd_out, buf, n);
    }

    close(fd);
    close(fd_out);

    return 0;
}
strace -o read.txt ./read_cmp_getc
strace -o getc.txt ./getc_cmp_read
  1. readwrite系统调用是无用户缓冲区的,它们直接在用户空间和内核空间之间传递数据,每次操作只处理一个字节或一组字节,需要频繁地在用户空间和内核空间之间切换,因此在处理大量数据时可能会比较慢。
  2. fgetcfputc是C标准库提供的函数,它们使用了用户缓冲区。C标准库在打开文件时会为文件分配一个缓冲区,当使用fgetc读取一个字符或使用fputc写入一个字符时,实际上是将数据读取到用户缓冲区或从用户缓冲区写入数据。当用户缓冲区满了或遇到fflushfclose等情况时,数据才会被传输到内核。
  3. 在大多数情况下,fgetcfputc的性能相对于readwrite会慢一些,因为它们多了一层用户缓冲区的处理。但在某些情况下,特别是频繁读写小量数据时,使用C标准库提供的函数可以减少系统调用的次数,从而提高性能。

综上所述,readwrite是直接的系统调用,没有用户缓冲区,适用于大量数据的读写;而fgetcfputc是C标准库提供的函数,使用了用户缓冲区,适用于频繁读写小量数据。选择使用哪种方法取决于具体的应用场景和性能需求。

3. 文件描述符

此部分主要由 chatGPT 生成讲解

3.1 PCB进程控制块

PCB(Process Control Block,进程控制块)是操作系统中用于管理和维护进程信息的 数据结构。每个运行的进程都对应一个唯一的PCB,PCB记录了进程的状态、标识符、优先级、寄存器值、程序计数器、内存分配信息、打开的文件、CPU占用时间等信息。

PCB是操作系统实现多道程序设计和进程调度的基础。当一个进程被调度执行时,操作系统会将当前进程的上下文(包括寄存器状态等)保存在该进程的PCB中,然后加载要执行的进程的上下文,使得新进程可以继续执行。当操作系统决定暂停或切换进程时,会再次保存当前进程的上下文到其PCB中,同时加载下一个要执行的进程的上下文。

PCB 实际上是一个结构体:struct task_struct { 结构体

PCB 通常包含以下字段:

  1. 进程标识符(Process ID,PID):用于唯一标识进程的整数值。
  2. 程序计数器(Program Counter,PC):指向当前执行指令的地址。
  3. 寄存器值:保存进程的寄存器状态,包括通用寄存器、程序状态字(PSW)等。
  4. 进程状态:表示进程的当前状态,如运行、就绪、阻塞等。
  5. 进程优先级:用于进程调度时确定进程的优先级顺序。
  6. 内存分配信息:记录进程占用的内存地址空间。
  7. 打开的文件列表:记录进程打开的文件及其 文件描述符
  8. CPU占用时间:记录进程在CPU上执行的时间。
  9. 其他控制信息:可能包含信号量、消息队列、进程间通信等信息。

PCB在操作系统中起着重要的作用,它是实现多任务、多进程的基础,也是操作系统对进程进行管理和调度的核心数据结构。每个进程都有一个对应的PCB,当进程从运行态切换到阻塞态或就绪态时,PCB中的信息会被更新和保存,以便在合适的时候重新调度和恢复进程的执行。

 

3.2 文件描述符表

文件描述符

文件描述符是在操作系统中用于标识打开文件的整数值。在Unix-like系统中,包括Linux,每个打开的文件都会被分配一个文件描述符,用于标识该文件。文件描述符是一个非负整数,通常由操作系统管理。

在C语言中,使用int类型来表示文件描述符。文件描述符的值为0、1和2通常有特殊意义:

  • 文件描述符0表示标准输入(stdin),它是程序从终端接收输入数据的文件描述符。
  • 文件描述符1表示标准输出(stdout),它是程序向终端输出数据的文件描述符。
  • 文件描述符2表示标准错误(stderr),它用于向终端输出错误信息。

除了标准输入、标准输出和标准错误外,程序还可以通过系统调用(例如openreadwrite等)打开其他文件,从而得到相应的文件描述符。每个新打开的文件都会获得一个尚未使用的最小的非负整数值作为其文件描述符。

文件描述符的主要作用是用于标识文件,从而在程序中进行文件的读取、写入和关闭等操作。在C语言中,通常使用文件描述符来操作文件,例如使用readwrite来读写文件内容,使用close来关闭文件。文件描述符是一个重要的概念,它使得程序可以方便地管理多个打开的文件,并进行相应的文件操作。

文件描述符表

文件描述符表是操作系统中用于管理进程打开的文件的数据结构。在Unix-like操作系统中,每个进程都有一个文件描述符表,它是一个数组,其中的 每个元素都是一个文件描述符

数组下标可以看成指针,指向文件结构体struct file

struct file {
    
}

操作系统隐藏文件结构体内容,只暴露 数组下标,即文件描述符给用户

文件描述符表的大小是由操作系统预先定义的,并且有一定的限制。在Linux系统中,通常默认情况下,每个进程最多可以打开 1024 个文件,这个限制可以通过修改系统参数来调整。

当进程执行一个新程序时,它会继承原有的文件描述符表。这意味着在新程序中也可以继续使用原有的文件描述符来操作文件。

文件描述符表是操作系统管理进程文件操作的重要机制,它使得进程可以方便地访问和操作打开的文件,从而实现了进程与文件之间的交互和通信。

 

3.3 最大打开文件数

最大打开文件数是操作系统对一个进程能够同时打开的文件数量进行限制的值。这个限制是为了保证系统资源的合理分配和控制,防止某个进程滥用资源导致系统资源耗尽。

在Unix-like系统中,包括Linux和macOS等,最大打开文件数由系统参数ulimit来控制。ulimit命令用于设置或显示用户的资源限制,其中包括最大打开文件数。

要查看当前用户的最大打开文件数,可以在终端中运行以下命令:

ulimit -n

通常情况下,ulimit -n 的默认值为1024,即每个进程最多可以同时打开1024个文件。这个值对于普通用户来说已经足够了,但对于某些特殊应用或服务器程序来说可能会不够。

如果需要增加最大打开文件数的限制,可以使用 ulimit 命令进行设置,但是普通用户通常只能增加到一定的限制,超过系统默认值需要管理员权限。

在Linux系统中,还可以通过修改系统配置文件来增加最大打开文件数的限制。这个配置文件通常是 /etc/security/limits.conf,可以在其中添加类似下面的配置:

*       soft    nofile  65535
*       hard    nofile  65535

这里的 65535 是新的最大打开文件数限制值。设置后需要重新登录或重启系统才能生效。

需要注意的是,增加最大打开文件数的限制会占用更多的系统资源,因此在修改配置时要慎重考虑,确保系统有足够的资源支持。不当的配置可能会导致系统性能下降或资源耗尽问题。

 

3.4 FILE结构体

FILE结构体是C标准库中用于文件操作的重要数据结构。它定义在 stdio.h 头文件中,并由C库提供文件读写函数所使用。

在标准C库中,FILE结构体的定义如下:

typedef struct _IO_FILE FILE;

具体的FILE结构体的定义会因不同的编译器和操作系统而有所不同,但通常会包含用于管理文件操作的各种成员变量。

FILE结构体的主要作用是用于维护文件的状态信息,例如文件指针位置、缓冲区、读写模式等。通过FILE结构体,C库能够实现对文件的高效读写操作,并且对于程序员来说,无需直接处理底层的文件描述符,只需使用FILE指针即可。

C标准库中提供了许多基于FILE结构体的文件读写函数,例如:

  • fopen:打开文件并返回一个FILE指针。
  • fclose:关闭文件。
  • fgetc、getc:从文件中读取一个字符。
  • fgets:从文件中读取一行字符串。
  • fputc、putc:将一个字符写入文件。
  • fputs:将字符串写入文件。
  • fprintf:格式化输出到文件。
  • fread、fwrite:二进制读写数据。
  • fseek、ftell:文件指针定位。

使用这些函数,可以方便地进行文件的读写操作,而无需直接操作文件描述符。当然,底层仍然是通过文件描述符来进行实际的文件读写操作,但这一过程对于程序员来说是透明的。

需要注意的是,在使用标准C库的文件读写函数时,要注意及时关闭文件以释放资源,并检查函数返回值以处理可能出现的错误情况。

 

4. 阻塞、非阻塞

4.1 概念

读常规文件是不会阻塞的,不管读多少字节,read 一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用 read 读终端设备就会阻塞,如果网络上没有接收到数据包,调用 read 从网络读就会阻塞,至于会阻塞多长时间 也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的, 而向终端设备或网络写则不一定。

现在明确一下阻塞(Block)这个概念。当进程调用一个阻塞的系统函数时,该进程被置于睡(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用 sleep 指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running)状态,在 Linux 内核中,处于运行状态的进程分为两种情况:

  1. 正在被调度执行:CPU 处于该进程的上下文环境中,程序计数器(eip)里保存着该进 程的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令, 正在读写该进程的地址空间。
  2. 就绪状态:该进程不需要等待什么事件发生,随时都可以执行,但 CPU 暂时还在执行 另一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的 进程,那么该调度谁执行呢?内核的调度算法是基于优先级和时间片的,而且会根据每个进 程的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同 时要兼顾用户体验,不能让和用户交互的进程响应太慢。

阻塞、非阻塞是设备文件、网络文件的属性

产生阻塞的场景:读设备文件、读网络文件。(读常规文件无阻塞概念) 补:/dev/tty --- 终端文件

注意,阻塞与非阻塞是对于文件而言的。而不是 read、write 等的属性。read 终端,默认阻塞读。

 

4.2 代码样例

block_readtty.c:阻塞阻塞读终端

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    char buf[10];
    int n;

    // #define STDIN_FILENO 0  STDOUT_FILENO 1  STDERR_FILENO 2
    n = read(STDIN_FILENO, buf, 10);
    if (n < 0) {
        perror("read STDIN_FILENO");
        // printf("%d", errno);
        exit(1);
    }
    write(STDOUT_FILENO, buf, n);

    return 0;
}

nonblock_readtty.c:非阻塞读终端,通过 open 函数 改变文件状态

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>

int main()
{
    char buf[10];
    int fd, n;

    fd = open("/dev/tty", O_RDONLY | O_NONBLOCK); // 设置非阻塞状态
    if (fd < 0) {
        perror("open /dev/tty");
        exit(1);
    }

tryagain:

    n = read(fd, buf, 10);

    // 如果read返回-1,并且 errno = EAGIN 或 EWOULDBLOCK,
    // 说明不是read失败,而是read在以非阻塞方式读一个设备或网络文件,并且文件无数据
    if (n < 0) {
        if (errno != EAGAIN) {        // if (errno != EWOULDBLOCK)
            perror("read /div/tty");
            exit(1);
        } else {
            write(STDOUT_FILENO, "try again\n", strlen("try again\n"));
            sleep(2);
            goto tryagain;
        }
    }

    write(STDOUT_FILENO, buf, n);
    close(fd);

    return 0;
}

nonblock_timeout.c:非阻塞读终端和等待超时

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>

#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "time out\n"

int main()
{
    char buf[10];
    int fd, n, i;

    fd = open("/dev/tty", O_RDONLY | O_NONBLOCK); // 设置非阻塞状态
    if (fd < 0) {
        perror("open /dev/tty");
        exit(1);
    }
    printf("open /dev/tty ok ... %d\n", fd);

    for (i = 0; i < 5; i++) {
        n = read(fd, buf, 10);
        if (n > 0) {               // 说明读到了东西
            break;
        }
        if (errno != EAGAIN) {          // if (errno != EWOULDBLOCK)
            perror("read /dev/tty");
            exit(1);
        } else {
            write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
            sleep(2);
        }
    }

    if (i == 5) {
            write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));
    } else {
        write(STDOUT_FILENO, buf, n);
    }

    close(fd);

    return 0;
}

 

5. fcntl 函数

5.1 概念

fcntl 函数是用于操作文件描述符的系统调用,它可以用来执行各种与文件描述符相关的操作。在C语言中,我们可以通过 fcntl 函数来调用这个系统调用。

函数原型

#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

参数说明

  • fd:要操作的文件描述符。

  • cmd:指定要执行的操作,可以是下列常量之一:


    • F_DUPFD:复制文件描述符。
    • F_GETFD:获取文件描述符标志。
    • F_SETFD:设置文件描述符标志。
    • F_GETFL:获取文件状态标志。
    • F_SETFL:设置文件状态标志。
    • F_GETOWN:获取异步I/O所有权。
    • F_SETOWN:设置异步I/O所有权。
    • F_GETLK:获取文件锁信息。
    • F_SETLK:设置文件锁。
    • F_SETLKW:设置文件锁,但如果锁不可用,则阻塞。
  • arg:根据操作类型 cmd 的不同,可能需要传入其他参数。

返回值

  • 如果执行成功,则根据操作类型 cmd 的不同,返回值也不同。
  • 如果发生错误,则返回 -1,并设置 errno 来指示错误类型。

 

5.2 代码样例

下面以 F_GETFLF_SETFL 操作为例,演示如何使用 fcntl 函数来获取和设置文件状态标志:

fcntl_1.c

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    int fd = open("example.txt", O_RDONLY);  // 打开文件 example.txt,只读模式

    // 获取文件状态标志
    int flags = fcntl(fd, F_GETFL);
    if (flags == -1) {
        perror("fcntl");
        return 1;
    }

    printf("File flags before modification: %d\n", flags);

    // 添加 O_APPEND 标志
    flags |= O_APPEND;
    
    // 设置文件状态标志
    int result = fcntl(fd, F_SETFL, flags);
    if (result == -1) {
        perror("fcntl");
        return 1;
    }

    printf("File flags after modification: %d\n", flags);

    close(fd);
    return 0;
}

在这个例子中,我们首先打开了一个文件 example.txt,然后使用 fcntl 函数获取文件状态标志,接着添加 O_APPEND 标志,最后再次使用 fcntl 函数将修改后的标志设置回去。运行此程序,可以看到文件状态标志的变化。

下面再来一个例子

fcntl_2.c:将文件改为非阻塞状态

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>

#define MSG_TRY "try again\n"

int main(void)
{
    char buf[10];
    int flags, n;

    flags = fcntl(STDIN_FILENO, F_GETFL); // 获取 stdin 属性信息

    if (flags == -1) {
        perror("fcntl error");
        exit(1);
    }

    flags |= O_NONBLOCK; // 添加上非阻塞状态
    int ret = fcntl(STDIN_FILENO, F_SETFL, flags);

    if (ret == -1) {
        perror("fcntl error");
        exit(1);
    }

tryagain:
    n = read(STDIN_FILENO, buf, 10);

    if (n < 0) {
        if (errno != EAGAIN) {
            perror("read /dev/tty");
            exit(1);
        }
        sleep(3);
        write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
        goto tryagain;
    }

    write(STDOUT_FILENO, buf, n);

    return 0;
}

 

6. lseek函数

6.1 概念

lseek 函数用于在打开的文件中定位文件指针的位置。它在 C 语言中是一个系统调用,提供了对文件读写位置的控制。

函数原型

#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);

参数说明

  • fd:文件描述符,是文件在进程中的标识符。

  • offset:指定了文件指针的偏移量。这个值可以为正、负或零,具体取决于 whence 参数。

  • whence:用于确定 offset 如何解释,它可以取以下值:


    • SEEK_SET:从文件开头开始偏移。
    • SEEK_CUR:从当前文件指针位置开始偏移。
    • SEEK_END:从文件末尾开始偏移。

返回值

lseek 函数返回新的文件指针位置,若出错则返回 -1,并设置全局变量 errno 来表示错误类型。

特别的:lseek 允许超过文件结尾设置偏移量,文件会因此被拓展。

注意文件“读”和“写”使用同一偏移位置。

 

6.2 代码样例

lseek1.c :文件读写使用同一偏移位置

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main()
{
    int fd, n;
    char msg[] = "It's a test for lseek\n";
    char ch;

    fd = open("test1.txt", O_RDWR | O_CREAT, 0644);
    if (fd < 0) {
        perror("open lseek.txt error");
        exit(1);
    }

    write(fd, msg, strlen(msg)); // 使用fd对打开的文件进行写操作,读写位置位于文件结尾处

    lseek(fd, 0, SEEK_SET);   // 修改文件读写位置,位于文件开头

    while ((n = read(fd, &ch, 1))) {
        if (n < 0) {
            perror("read error");
            exit(1);
        }
        write(STDOUT_FILENO, &ch, n); // 将文件内容按字节读出,写出到屏幕
    }

    close(fd);

    return 0;
}

lseek2.c :获取文件大小

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    int fd = open(argv[1], O_RDWR);
    if (fd == -1) {
        perror("open error");
        exit(1);
    }

    int lenth = lseek(fd, 0, SEEK_END);
    printf("file size: %d\n", lenth);

    close(fd);

    return 0;
}

lseek3.c :拓展文件大小:要想使文件大小真正拓展,必须引起 IO 操作

也可以使用 truncate函数 直接拓展文件

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    int fd = open(argv[1], O_RDWR);
    if (fd == -1) {
        perror("open error");
        exit(1);
    }

    int lenth = lseek(fd, 110, SEEK_END);
    printf("file size: %d\n", lenth);

    write(fd, "\0", 1);


    close(fd);

    return 0;
}

补充:

od -tcx filename # 查看文件的 16 进制表示形式 
od -tcd filename # 查看文件的 10 进制表示形式

 

7. ioctl.c 函数 (嵌入式)

7.1 概念

ioctl 是一个在 UNIX/Linux 系统中用于设备控制的函数。它用于与设备驱动程序进行通信,进行设备的配置、状态查询和控制等操作。

函数原型

#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);

参数解释

  • fd:文件描述符,用于指定要控制的设备。
  • request:一个无符号长整型参数,表示控制请求的编号。通常使用预定义的常量或者自定义的控制码。
  • ...:可选参数,如果 request 需要传递额外的数据,可以使用可变参数传递。

工作原理

ioctl 函数的作用是向设备驱动程序发送特定的控制命令(request),然后设备驱动程序根据接收到的命令执行相应的操作。这些操作可能包括设备的配置、状态查询、性能优化等。不同的设备驱动程序支持的控制命令是不同的,因此在使用 ioctl 函数时,需要参考设备驱动程序的文档或者相关头文件来了解可用的控制命令。

使用场景

ioctl 函数通常用于与特殊设备进行交互,例如硬件设备(如串口、USB 设备等)、网络设备、字符设备、以及其他类型的外设等。通过 ioctl 函数,用户可以向设备驱动程序发送自定义的控制命令,从而实现设备的灵活控制和配置。

注意事项

使用 ioctl 函数需要小心,因为它是一个较为底层的接口,如果使用不当,可能导致系统崩溃或不稳定。在使用 ioctl 函数时,建议查阅相关的文档和资料,了解设备驱动程序支持的控制命令和参数,以及正确的使用方法。

 

7.2 代码样例

ioctl.c

#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>

int main() {
    int fd = open("/dev/ttyS0", O_RDWR); // 打开串口设备文件

    if (fd < 0) {
        perror("open");
        return 1;
    }

    // 设置串口波特率为 9600
    int baud_rate = 9600;
    if (ioctl(fd, TIOCSBRSRATE, &baud_rate) < 0) {
        perror("ioctl");
        return 1;
    }

    // 关闭设备文件
    close(fd);
    return 0;
}

 

8. 传入传出参数

在编程中,函数的参数可以分为 传入参数(Input Parameter)传出参数(Output Parameter)传入传出参数(Input/Output Parameter) 三种类型。

这些参数的区别在于函数如何使用它们以及对它们的修改和返回。

8.1 传入参数

  1. 指针作为函数
  2. 通常有 const 关键字修饰
  3. 指针指向有效区域,在函数内部做读操作

8.2 传出参数

  1. 指针作为函数
  2. 在函数调用之前,指针指向的空间可以无意义,但必须有效
  3. 在函数内部,做写操作
  4. 函数调用结束后,充电函数返回值

8.3 传入传出参数

  1. 指针作为函数
  2. 在函数调用之前,指针指向的空间有实际意义
  3. 在函数内部,先做读操作,后做写操作
  4. 函数调用结束后,充当函数返回值

 

9. 扩展阅读

关于虚拟 4G 内存的描述和解析

此部分内容可以在学到 进程 时观看

一个进程用到的虚拟地址是由内存区域表来管理的,实际用不了 4G。而用到的内存区域,会通过页表映射到物理内存

所以每个进程都可以使用同样的虚拟内存地址而不冲突,因为它们的物理地址实际上是不同的。内核用的是 3G 以上的 1G 虚拟内存地址

其中896M 是直接映射到物理地址的,128M 按需映射 896M 以上的所谓高位内存。各进程使用的是同一个内核。

首先要分清 可以寻址实际使用 的区别。

其实我们讲的每个进程都有 4G 虚拟地址空间,讲的都是“可以寻址”4G,意思是虚拟地址的 0-3G 对于一个进程的用户态和内核态来说是可以访问的,而 3-4G 是只有进程的内核态可以访问的。并不是说这个进程会用满这些空间。

其次,所谓“独立拥有的虚拟地址”是指对于每一个进程,都可以访问自己的 0-4G 的 虚拟地址。虚拟地址是“虚拟”的,需要转化为“真实”的物理地址。

好比你有你的地址簿,我有我的地址簿。你和我的地址簿都有 1、2、3、4 页,但是每 页里面的实际内容是不一样的,我的地址簿第 1 页写着 3 你的地址簿第 1 页写着 4,对于你、 我自己来说都是用第 1 页(虚拟),实际上用的分别是第 3、4 页(物理),不冲突。

内核用的 896M 虚拟地址是直接映射的,意思是只要把虚拟地址减去一个偏移量(3G) 就等于物理地址。同样,这里指的还是寻址,实际使用前还是要分配内存。而且 896M 只是 个最大值。如果物理内存小,内核能使用(分配)的可用内存也小。

 

相关链接

教程视频:Linux系统编程哔哩哔哩bilibili

Linux系列文章:Linux – Echo (liveout.cn)

Linux基础命令:Linux学习资料分享 – Echo (liveout.cn)

GCC、GDB、Makefile:GCC、GDB、Makefile学习笔记 – Echo (liveout.cn)

Vim配置:Linux学习资料分享 – Echo (liveout.cn)

GitHub仓库,包含教程讲义、代码以及笔记:https://github.com/PGwind/LinuxSystem

--> 前言此篇文章为学习 Linux系统编程01:文件I/O 部分的笔记1. open/close函数1.1 open#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>int open(const char *pathname, int flags);int open(const char ...

前言

关于Linux系统编程的前导课程学习笔记,共涉及 GCC GDB Makefile 三个部分

1. GCC

介绍

GCC(英文全拼:GNU Compiler Collection)是 GNU 工具链的主要组成部分,是一套以 GPL 和 LGPL 许可证发布的程序语言编译器自由软件,由 Richard Stallman 于 1985 年开始开发。

GCC 原名为 GNU C语言编译器,因为它原本只能处理 C 语言,但如今的 GCC 不仅可以编译 C、C++ 和 Objective-C,还可以通过不同的前端模块支持各种语言,包括 Java、Fortran、Ada、Pascal、Go 和 D 语言等等。

GCC 的编译过程可以划分为四个阶段:预处理(Pre-Processing)、编译(Compiling)、汇编(Assembling)以及链接(Linking)。

1.1 相关参数

4个步骤 :预处理、编译、汇编、链接

-E:仅执行预处理           # hello.i  源文件 i  
-S:只编译                # hello.s  汇编文件 s 
-c:编译和汇编,但不链接。   # hello.o  目标文件 o
-o <file>:指定输出文件。   # a.out  可执行文件
-I:  指定头文件位置         # gcc -I ./inc hello.c -o hello
-g:  编译时增加调试文件
-Wall: 显示所有警告信息
-D:    向程序中“动态“注册宏定义

1.2 静态库和动态库

静态库

空间大,速度块,基本淘汰

1. gcc -c foo.c -o foo.o    # 生成 foo.o 目标文件
2. ar rcs libfoo.a foo.o    # 生成 libfoo.a 静态库
3. gcc hello.c -static libfoo.a -o hello    # 编译 并链接静态库 libfoo.a
4. gcc test.c -static ./lib/libmymath.a -I ./inc -o b.out  # 文件+库+头文件

动态库

空间小,速度慢,最多使用

1. gcc -c add.c -o add.o -fPIC          # 生成于位置无关的代码 -fPIC
2. gcc -shared -o libmymath.so add.o sub.o   #  制作动态库
3. -l: 指定库名      -L:指定库路径
   gcc test.c -lmymath -L./lib -I./inc -o a.out  # 编译可执行程序

执行报错
原因

  1. 链接器:工作于链接阶段,工作时需要 -l 和 -L
  2. 动态链接器:工作于程序运行阶段,工作时需要提供动态库所在目录位置解决

解决 :

  1. export LD_LIBRARY_PATH=动态库路径:$LD_LIBRARY_PATH (一次进程里,临时)

  2. 移动到 /usr/lib (永久)

  3. .bashrc 中 写入 环境变量 (永久)

  4. vim /etc/ld.so.conf  # 写入动态库的路径
    ldconfig  #  刷新  (永久)
    

ldd是Linux系统下的一个命令行工具,用于查看可执行程序或共享库所依赖的动态链接库(Dynamic Linking Library,简称DLL)。它会列出指定的可执行程序或共享库所使用的共享库,并显示它们的完整路径。

root@freecho:/opt/C/gcc/lib/dynamicLib# ldd a.out
        linux-vdso.so.1 (0x0000ffffb6b6d000)
        libmymath.so => /lib/libmymath.so (0x0000ffffb6af0000)
        libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000ffffb6940000)
        /lib/ld-linux-aarch64.so.1 (0x0000ffffb6b34000)
root@freecho:/opt/C/gcc/lib/dynamicLib#

 

2. GDB

介绍

GDB 全称“GNU symbolic debugger”,从名称上不难看出,它诞生于 GNU 计划(同时诞生的还有 GCC、Emacs 等),是 Linux 下常用的程序调试器。发展至今,GDB 已经迭代了诸多个版本,当下的 GDB 支持调试多种编程语言编写的程序,包括 C、C++、Go、Objective-C、OpenCL、Ada 等。实际场景中,GDB 更常用来调试 C 和 C++ 程序

2.1 基础指令

gcc gdbtest.c -o a.out -g   # -g 使用该参数编译可执行文件,得到调试表
gdb a.out # 开始
l 1       # 从第一行开始显示
l 		  # 继续往下显示   (lsit)
b 52	  # 52行设置断点	  (breakout)
r		  # 运行 		(run)
n   	  # 逐过程单步执行,越过函数,系统函数用 n              (next)
s 		  # 逐语句单步执行,进入函数,其他情况与n无区别,执行下一步 		(step)
until 16  # 运行程序直到达到指定的行号并停止
p i       # 打印变量 i 的值  (print)
continue  # 继续执行断点后续指令
quit      # 退出gdb调试

2.2 断点指令

(gdb) b 23 # 设置断点
(gdb) b 30
(gdb) b 41
(gdb) b 41 if i=5 # 设置条件断点
(gdb) info b # 查看断点信息表
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00000000000009f8 in select_sort at gdbtest2.c:23
2       breakpoint     keep y   0x0000000000000a7c in select_sort at gdbtest2.c:30
3       breakpoint     keep y   0x0000000000000b14 in print_arr at gdbtest2.c:41
4       breakpoint     keep y   0x0000000000000b14 in print_arr at gdbtest2.c:41
        stop only if i=5
(gdb) delete 3 # 删除指定断点

delete  # 删除所有断点
disable	# 禁用断点
enable  # 启用断点

2.3 其他指令

  1. 使用 r 查找段错误出现位置
  2. start :用于开始执行程序并停在main函数的第一条语句上
  3. finish :当你在调试一个函数时,你可以使用finish命令来执行该函数的剩余部分,并停在函数调用的地方
  4. ser args :设置 main 函数命令行参数,在start run 命令之前
  5. run 字符串1字符串2 ... : 设置 main 函数命令行参数
  6. info b:查看断点信息表 information breakout
  7. ptype i:查看变量类型
  8. backtrace :查看函数的调用的栈帧和层级关系 缩写:bt
  9. frame 编号:切换函数的栈帧 缩写:f
  10. display i :设置跟踪变量
  11. undisplay 编号:取消跟踪变量

补:栈帧

栈帧(Stack Frame) 是在程序执行期间,用于管理函数调用和局部变量的一种数据结构。在函数调用时,会创建一个栈帧来存储函数的参数、局部变量以及保存调用函数的返回地址和调用函数的寄存器状态等信息。

每个函数调用都会创建一个新的栈帧,将其添加到函数调用栈(Call Stack)的顶部。当函数执行结束后,其对应的栈帧会被销毁,栈帧也会被从调用栈中移除,程序会返回到调用该函数的上一个栈帧继续执行。

栈帧通常包含以下几个重要的部分:

  1. 返回地址:指向调用该函数的下一条指令的地址,用于函数执行完后返回到正确的位置。
  2. 函数参数:将函数的参数值存储在栈帧中,以便在函数内部使用。
  3. 本地变量:函数中定义的局部变量也被存储在栈帧中,它们只在函数执行期间存在,函数结束后将被销毁。
  4. 保存的寄存器状态:为了保护调用函数时的寄存器状态,可能需要将某些寄存器的值保存在栈帧中,在函数执行完后再恢复寄存器的值。

栈帧的使用使得函数调用和返回的过程更加高效和可控,同时提供了函数间局部变量的隔离,避免了函数之间的冲突。栈帧在计算机体系结构和操作系统中发挥着重要作用,为函数调用提供了必要的支持。

 

3. Makefile

介绍

Makefile是一个用于构建和管理项目的工程文件,它定义了项目中的源文件、依赖关系以及构建规则等信息,用于自动化编译和链接程序。

Makefile通常使用make工具来执行其中定义的规则和命令。通过Makefile,开发人员可以在项目中定义编译、链接、清理等操作的规则,以及指定项目中的文件之间的依赖关系。这样,当项目的源文件发生变化时,make工具可以根据Makefile中的规则,自动检测变化并重新编译需要更新的文件,从而加快项目的开发和构建过程。

文件命令:makefile Makefile

1个规则

目标:依赖条件
	(一个tab缩进) 命令
  1. 目标的时间必须晚于依赖条件的时间,否则,更新目标
  2. 依赖条件如果不存在,找寻新的规则去产生依赖

ALL:指定makefile的最终目标,否则最终目标写在文件开头

步骤

root@freecho:/opt/C/make# vim makefile
root@freecho:/opt/C/make# cat makefile
hello:hello.c
        gcc hello.c -o a.out
root@freecho:/opt/C/make# ls
hello.c  makefile
root@freecho:/opt/C/make# make
gcc hello.c -o a.out
root@freecho:/opt/C/make# ls
a.out  hello.c  makefile

makefile文件1

ALL:a.out  # 指定makefile的最终目标,否则最终目标写在文件开头

hell.o:hello.c
        gcc -c hello.c -o hello.o
add.o:add.c
        gcc -c add.c -o add.o
sub.o:sub.c
        gcc -c sub.c -o sub.o
mul.o:mul.c
        gcc -c mul.c -o mul.o
div1.o:div1.c
        gcc -c div1.c -o div1.o
a.out:hello.o add.o sub.o mul.o div1.o
        gcc hello.o add.o sub.o mul.o div1.o -o a.out

2个函数

两个函数分别为:wildcard(通配符)patsubst(匹配替换)

2.1 $(wildcard ./*.c)

src = $(wildcard *.c) 
# src = add.c sub.c mul.c div1.c

匹配当前目录下的所有后缀为 .c 的文件,将文件名组成列表,赋值给变量 src

2.2 $(patsubst %.c, %.o, $(src))

obj = $(patsubst %.c, %.o, $(src))
# obj = add.o sub.o mul.o div1.o

参数3 中,包含 参数1 的部分,替换为 参数2,即把 src 变量里所有后缀为 .c 的文件替换为 .o

makefile文件2、3

src = $(wildcard *.c)               # src = add.c sub.c mul.c div1.c
obj = $(patsubst %.c, %.o, $(src))  # obj = add.o sub.o mul.o div1.o

ALL:a.out

hell.o:hello.c
        gcc -c hello.c -o hello.o
add.o:add.c
        gcc -c add.c -o add.o
sub.o:sub.c
        gcc -c sub.c -o sub.o
mul.o:mul.c
        gcc -c mul.c -o mul.o
div1.o:div1.c
        gcc -c div1.c -o div1.o
a.out: $(obj)
        gcc $(obj) -o a.out

clean:  # 终端输入 make clean 才会生效
        -rm -rf $(obj)  # -rm 前面的 '-'  表示出错依然执行

步骤

clean: (没有依赖)

make clean -n 会演示清除命令,不会实际生效

make clean 则会实际生效

root@freecho:/opt/C/make# make
gcc -c add.c -o add.o
gcc -c div1.c -o div1.o
gcc -c -o hello.o hello.c
gcc -c mul.c -o mul.o
gcc -c sub.c -o sub.o
gcc  add.o  div1.o  hello.o  mul.o  sub.o   -o a.out
root@freecho:/opt/C/make# ls
add.c  add.o  a.out  div1.c  div1.o  hello.c  hello.o  m1  m2  makefile  mul.c  mul.o  sub.c  sub.o
root@freecho:/opt/C/make# make clean -n
rm -rf  add.o  div1.o  hello.o  mul.o  sub.o
root@freecho:/opt/C/make# ls
add.c  add.o  a.out  div1.c  div1.o  hello.c  hello.o  m1  m2  makefile  mul.c  mul.o  sub.c  sub.o
root@freecho:/opt/C/make# make clean
rm -rf  add.o  div1.o  hello.o  mul.o  sub.o
root@freecho:/opt/C/make# ls
add.c  a.out  div1.c  hello.c  m1  m2  makefile  mul.c  sub.c

3个自动变量

$@ :在规则的命令中,表示规则中的目标

$< :在规则的命令中,表示规则中的第一个条件,如果将该变量应用于 模式规则 中,它可将依赖列表中的依赖依次取出,套用规则模式

$^ :在规则的命令中,表示规则中的所有依赖条件,组成一个列表,以空格隔开,如果这个列表中有重复的项则消除重复

makefile文件4

src = $(wildcard *.c)               # src = add.c sub.c mul.c div1.c
obj = $(patsubst %.c, %.o, $(src))  # obj = add.o sub.o mul.o div1.o

ALL:a.out

hell.o:hello.c
        gcc -c $< -o $@
add.o:add.c
        gcc -c $< -o $@
sub.o:sub.c
        gcc -c $< -o $@
mul.o:mul.c
        gcc -c $< -o $@
div1.o:div1.c
        gcc -c $< -o $@
a.out: $(obj)
        gcc $^ -o $@

clean:
        -rm -rf $(obj)

模式规则

编译 .c 文件 生成 .o 文件

%.o:%.c
        gcc -c $< -o $@

makefile文件5

src = $(wildcard *.c)               # src = add.c sub.c mul.c div1.c
obj = $(patsubst %.c, %.o, $(src))  # obj = add.o sub.o mul.o div1.o

ALL:a.out

a.out: $(obj)
        gcc $^ -o $@

%.o:%.c
        gcc -c $< -o $@

clean:
        -rm -rf $(obj) a.out

拓展

静态模式规则

表示对哪一个依赖条件套用这个规则

$(obj):%.o:%.c
        gcc -c $< -o $@

伪目标

声明了 .PHONY: clean ALL,它告诉 Make 不会去检查是否存在名为 cleanALL 的文件,而是直接执行它们后面的命令块。这样,运行 make cleanmake ALL 时,Make 将执行相应的命令块。

.PHONY: clean ALL

最终形态 makefile

src = $(wildcard *.c)
obj = $(patsubst %.c, %.o, $(src))

myArgs = -Wall -g

ALL:a.out

a.out: $(obj)
        gcc $^ -o $@ $(myArgs)

$(obj):%.o:%.c
        gcc -c $< -o $@ $(myArgs)

clean:
        -rm -rf $(obj) a.out

.PHONY: clean ALL

相关参数

-n:模拟执行make、make clean 命令
-f:指定文件执行 make 命令

 

相关链接

教程视频:Linux系统编程哔哩哔哩bilibili

Linux系列文章:Linux – Echo (liveout.cn)

Linux基础命令:Linux学习资料分享 – Echo (liveout.cn)

Vim配置:Linux学习资料分享 – Echo (liveout.cn)

GitHub仓库,包含教程讲义、代码以及笔记:https://github.com/PGwind/LinuxSystem

--> 前言关于Linux系统编程的前导课程学习笔记,共涉及 GCC GDB Makefile 三个部分1. GCC介绍GCC(英文全拼:GNU Compiler Collection)是 GNU 工具链的主要组成部分,是一套以 GPL 和 LGPL 许可证发布的程序语言编译器自由软件,由 Richard Stallman 于 1985 年开始开发。GCC 原名为 GNU C语言编译器,因为它原本...

前言

在 Linux 系统中,查找文件是日常工作中常用的操作之一。Linux 提供了多个命令来帮助我们快速定位和查找文件。本文将介绍 Linux 下查找文件的几个常用命令。

 

1. find

find 命令是 Linux 中用于查找文件和目录的强大工具。它可以通过指定不同的选项来满足各种查找需求。

基本语法

find [路径] [选项] [匹配条件]

示例

查找当前目录及其子目录下所有名称为 example.txt 的文件:

find . -name "example.txt"

查找 /home/user 目录下所有以 .log 结尾的文件:

find /home/user -name "*.log"

 

2. locate

locate 命令用于快速查找文件,它基于系统维护的文件数据库进行搜索。由于它直接查询数据库,速度通常比 find 命令更快。

基本语法

locate [文件名]

示例

查找系统中所有以 .conf 结尾的配置文件:

locate *.conf

 

3. grep

grep 命令是用于在文件中搜索指定的字符串的工具,它也可以用来查找文件名。

基本语法

grep [搜索字符串] [文件名]

示例

在当前目录及其子目录下查找包含 example 字符串的文件:

grep -r "example" .

 

4. which

which 命令用于查找指定命令在系统中的安装路径。

基本语法

which [命令名]

示例

查找 ls 命令在系统中的路径:

which ls

 

5. whereis

whereis 命令用于查找二进制文件、源代码文件和帮助文档的位置。

基本语法

whereis [文件名]

示例

查找 bash 命令的二进制文件、源代码文件和帮助文档位置:

whereis bash

 

以上是 Linux 下查找文件的几个常用命令,通过灵活使用这些命令,你可以更高效地管理和查找文件,提升工作效率。

--> 前言在 Linux 系统中,查找文件是日常工作中常用的操作之一。Linux 提供了多个命令来帮助我们快速定位和查找文件。本文将介绍 Linux 下查找文件的几个常用命令。 1. findfind 命令是 Linux 中用于查找文件和目录的强大工具。它可以通过指定不同的选项来满足各种查找需求。基本语法find [路径] [选项] [匹配条件]示例查找当前目录及其子目录下所有名称为 e...

前言

这篇文章是关于ESP32的一个小Demo,名称为 智能垃圾桶

涉及到的模块为 SG90舵机和HC-SR04超声波, 使用的编程烧录软件为 Arduino

一开始打算直接扔在ESP32基础教程 – Echo (liveout.cn)这篇文章里,不做过多介绍,因为这个小Demo只用到了舵机和超声波模块。

后来感觉模块虽少,但是涉及到的知识其实挺多的,有PWM、外部中断(硬件)、硬件定时器和二值信号量,所以就单独写了一篇文章。

如果想要再进化一下,可以尝试添加摄像头模块,进行深度学习,分别垃圾种类,这方面还没涉及到,就不多说了。

如果十分感兴趣,可以参考此篇文章:STEAM案例 | 《智能垃圾桶》项目的设计 - 知乎 (zhihu.com)

PS:这个Demo非本人原创,我加了些注释额外代码,并且整理优化成了此篇文章,相关链接会在文章结尾部分给出。

 

接线图

 

Demo流程

超声波模块每隔200ms发出一次信号进行测距,如果测量到的物体距离在范围内,则信号为 open_semaphore

舵机旋转打开盖子,板载灯变亮,串口打印相关信息。

当打开盖子时,记录打开时间,并启动计时器进行定时检测,即每隔500ms进行检测。

如果检测到盖子关闭时间超过了阈值,则重置打开时间,并设置二值信号量状态为关闭。

得到关闭 close_semaphore 信号后,舵机转动进行关盖。

 

 

 

代码部分

代码需要用到的库: ESP32Servo

主体部分

#include "sonar.h"
#include "cover.h"
#include "servo.h"
int ledPin = 2;//板载灯
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; // 自旋锁

// 打开盖子
void open_cover()
{
  bool shouldAct = false; // 开盖行为
  
  /*
     临界区是一段代码片段,用于在多任务环境下保护共享资源,以确保对资源的访问不会被并发任务中断或干扰。
     临界区的作用是提供一种互斥机制,使得同一时间只有一个任务可以访问共享资源,避免并发访问导致的数据竞争和不一致性。
  */
  portENTER_CRITICAL(&mux); // 进入临界区
  if (openTime == 0) // 打开时间为0时
    shouldAct = true;
  openTime = micros(); // 记录当前打开盖子的时间
  portEXIT_CRITICAL(&mux);  // 离开临界区

  if (shouldAct)
    servo.write(145); // 舵机旋转145,即开盖°
}

void close_cover()
{
  servo.write(0); // 舵机为0°,即关盖
}

void setup() {
  pinMode(ledPin,OUTPUT); //输出模式
  Serial.begin(115200);
  sonar_init(&mux); // 超声波模块初始化
  cover_detect_init(&mux); // 初始化盖子检测相关的设置
  servo_init(); // 舵机初始化
  close_cover(); // 刚开始为盖上盖子状态
}

void loop() {
  // 超声波有信号回来
  if (xSemaphoreTake(open_semaphore, 0) == pdTRUE) // 打开信号为true
  {
    Serial.println("open"); // 串口打印信息
    open_cover();
    digitalWrite(ledPin, HIGH); // 灯亮
  }

  if (xSemaphoreTake(close_semaphore, 0) == pdTRUE) // 关闭信号为true
  {
    Serial.println("close");
    close_cover();
    digitalWrite(ledPin, LOW); // 灯灭
  }
}

关盖处理

cover.h

#pragma once //预处理指令,用于确保头文件只被编译一次

#include <freertos/FreeRTOS.h>
#include <esp32-hal-timer.h>
#include <freertos/semphr.h>

extern volatile unsigned long openTime;
extern volatile SemaphoreHandle_t close_semaphore;

void cover_detect_init(portMUX_TYPE *mux);

cover.cpp

#include "cover.h"

hw_timer_t *cover_timer = NULL; // 定时器
static portMUX_TYPE *_mux = NULL;
volatile SemaphoreHandle_t close_semaphore; // 关盖信号量
volatile unsigned long openTime = 0; // 打开盖子的时间

// 中断服务程序ISR:检测盖子是否关闭
void IRAM_ATTR close_detect()
{
  portENTER_CRITICAL_ISR(_mux);
  auto now = micros();
  if (openTime != 0 && (now - openTime) >= 4000000) // 打开盖子时间大于等于4s则关闭
  { 
    openTime = 0;
    xSemaphoreGiveFromISR(close_semaphore, NULL);
  }
  portEXIT_CRITICAL_ISR(_mux);
}

void cover_detect_init(portMUX_TYPE *mux)
{
  _mux = mux;
  close_semaphore = xSemaphoreCreateBinary();

  // 检测到关闭部分,0.5秒检测一次
  cover_timer = timerBegin(2, 80, true);  // 初始化计时器2,分频系数80,使能中断
  timerAttachInterrupt(cover_timer, close_detect, true);  // 附加中断处理函数 close_detect 到计时器
  timerAlarmWrite(cover_timer, 500000, true);  // 设置计时器的定时时间为500000微秒(0.5秒),并使能重复触发
  timerAlarmEnable(cover_timer);  // 启动计时器
}

舵机模块

servo.h

#pragma
#include <ESP32Servo.h>

extern Servo servo;

void servo_init();

servo.cpp

#include "myservo.h"

// 舵机部分
Servo servo;
int minUs = 500;
int maxUs = 2500;
int servoPin = 13;

void servo_init()
{
  // 舵机
  ESP32PWM::allocateTimer(1);
  servo.setPeriodHertz(50);
  servo.attach(servoPin, minUs, maxUs);
}

超声波模块

sonar.h

#pragma once
#include <freertos/FreeRTOS.h>
#include <esp32-hal-timer.h>
#include <freertos/semphr.h>

extern volatile SemaphoreHandle_t open_semaphore; // 信号量
void sonar_init(portMUX_TYPE *mux);

sonar.cpp

#include "sonar.h"

volatile SemaphoreHandle_t open_semaphore; // 信号量

// 超声波测距部分
const int trigPin = 17; 
const int echoPin = 18;
int distance = 0;

static portMUX_TYPE *_mux = NULL;
hw_timer_t *sonar_timer = NULL; // 定时器
volatile unsigned long startTime = 0; // 发出超声波时间
volatile unsigned long endTime = 0; // 收到超声波时间

// 硬件定时器ISR
void IRAM_ATTR ping()
{
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(15);
  digitalWrite(trigPin, LOW);
}

// ECHO 引脚ISR
void IRAM_ATTR changeISR() 
{
  auto now = micros(); // 当前时间
  auto state = digitalRead(echoPin);

  portENTER_CRITICAL_ISR(_mux);
  if (state) // 高电平,即刚发出超声波
    startTime = now;
  else
    endTime = now;
// 变成低电平时表示已经收到回声
// 如果 < 10cm 就发信号开盖
  if (!state) {
    auto t = endTime - startTime;
    auto dis = t * 0.01715;
    if (dis <= 10)
    {
      xSemaphoreGiveFromISR(open_semaphore, NULL); // 给一个开盖信号量发送信号
    }
  }
  portEXIT_CRITICAL_ISR(_mux);
}

void sonar_init(portMUX_TYPE* mux)
{
  _mux = mux;
  pinMode(trigPin, OUTPUT);
  pinMode(echoPin, INPUT);
  open_semaphore = xSemaphoreCreateBinary();

  //测距定时器部分
  sonar_timer = timerBegin(0, 80, true);
  timerAttachInterrupt(sonar_timer, ping, true);
  timerAlarmWrite(sonar_timer, 200000, true); // 定时时间为 0.2s

  // echo引脚的中断
  attachInterrupt(digitalPinToInterrupt(echoPin), changeISR, CHANGE);

  // 开始周期测量
  timerAlarmEnable(sonar_timer);
}

相关链接

Demo教程视频:ESP32之智能垃圾桶制作讲解—哔哩哔哩_bilibili

GitHub仓库:https://github.com/PGwind/esp32project

ESP32代码记录:ESP32基础教程 – Echo (liveout.cn)

PS:写程序难免会有问题,而且国内的ChatGPT访问受限

这里就给个朋友搭建的链接吧:chuanwen智能

 

--> 前言这篇文章是关于ESP32的一个小Demo,名称为 智能垃圾桶涉及到的模块为 SG90舵机和HC-SR04超声波, 使用的编程烧录软件为 Arduino。一开始打算直接扔在ESP32基础教程 – Echo (liveout.cn)这篇文章里,不做过多介绍,因为这个小Demo只用到了舵机和超声波模块。后来感觉模块虽少,但是涉及到的知识其实挺多的,有PWM、外部中断(硬件)、硬件定时器和二值...

前言

此篇文章为有关 ESP32 的学习期间的代码记录,并且加上了自己的注释,非教学文章。

使用开发板全称ESP32 DEVKILTv1(devkitv1) ,搭载芯片为 ESP32D0WDQ6,使用软件为 Arduino

 

 

参考链接

如果是小白并且想要学习单片机相关知识,建议移步此篇文章:51单片机入门教程(上篇)(代码+个人理解) – Echo (liveout.cn)

此篇文章参考教程视频:小鱼创意的个人空间哔哩哔哩bilibili

GitHub代码样例链接:https://github.com/PGwind/ESP32code

开发板详细讲解:ESP32 DEVKILTv1(devkitv1)开发板全解析

 

1. 点亮LED

1.1 点亮第一个LED

int ledPin = 2; //定义引脚,一般为板载蓝色灯

void setup() {
  // put your setup code here, to run once:
  pinMode(ledPin,OUTPUT); //输出模式
}

void loop() {
  // put your main code here, to run repeatedly:
  digitalWrite(ledPin, HIGH); //引脚高电平,即等效于 digitalWrite(ledPin, 1);
}

1.2 LED闪烁

int ledPin = 2;

void setup() {
  pinMode(ledPin,OUTPUT);

}

void loop() {
  digitalWrite(ledPin, HIGH);
  delay(2000); //延迟
  digitalWrite(ledPin, LOW);
  delay(2000);
}

1.3 不同闪烁周期LED

int ledPin2 = 2; 
int ledStatus2 = 0;  //现在的状态
unsigned int prevTime2 = 0; //改变状态时的时间

int ledPin4 = 4;
int ledStatus4 = 0;  
unsigned int prevTime4 = 0;

void setup() {
  pinMode(ledPin2, OUTPUT);
  digitalWrite(ledPin2, HIGH);
  ledStatus2 = HIGH;
  prevTime2 = millis(); //millis(): 本程序已经运行的时间(ms) micros()微秒us

  pinMode(ledPin4, OUTPUT);
  digitalWrite(ledPin4, HIGH);
  ledStatus4 = HIGH;
  prevTime4 = millis(); //millis(): 本程序已经运行的时间(ms) micros()微秒us
}

void loop() {
  unsigned int now = millis(); //程序运行的时间

  if (now - prevTime2 > 3000) //上次改变状态后已经过了3s
  {
    int status  = ledStatus2 == HIGH ? LOW: HIGH;
    digitalWrite(ledPin2, status);
    ledStatus2 = status;
    prevTime2 = now;
  }

  if (now - prevTime4 > 1000) //上次改变状态后已经过了1s
  {
    int status  = ledStatus4 == HIGH ? LOW: HIGH;
    digitalWrite(ledPin4, status);
    ledStatus4 = status;
    prevTime4 = now;
  }
}

 

2. 按键

2.1 按键控制LED

int switchPin = 25; //按键所接GPIO口
int ledPin = 4; //LED接口
int ledStatus = 0; //LED目前状态

void setup() {
  pinMode(switchPin, INPUT_PULLUP);//INPUT_PULLUP上拉,低电平有效,检测到低电平表明按键已经按下
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, HIGH);
  ledStatus = HIGH;
}

void loop() {
  int val = digitalRead(switchPin); //读取开关引脚的电平状态
  if (val == LOW) //低电平有效
  {
    ledStatus = !ledStatus;
    digitalWrite(ledPin, ledStatus);
  }
}

2.2 软件消除抖

使用 RBD_ Button 库进行消抖,在 库管理 处进行安装

#include <RBD_Timer.h>
#include <RBD_Button.h>

int switchPin = 25;
int ledPin = 4;
int ledStatus = 0;

//创建一个可以消除拉动的按键对象
RBD::Button button(switchPin, INPUT_PULLUP);

void setup() {
  pinMode(ledPin, OUTPUT);
  button.setDebounceTimeout(20); // 消除抖动时间是20ms
}

void loop() {
  //检测按键按下去的事件(下降沿)
  if (button.onPressed()) //按键已经按下
  {
    ledStatus = !ledStatus;
    digitalWrite(ledPin, ledStatus);
  }
}

 

3. PWM

LED控制(LEDC)外围设备主要用于控制LED的强度,尽管它也可以用于生成PWM信号用于其他目的。它具有16个通道,可以生成独立的波形,这些波形可以用于驱动RGB LED器件。

3.1 LEDC(PWM)

void setup() {
  int ret = 0; //状态
  Serial.begin(115200);
  int ch0 = 0; //通道
  int gpio4 = 4; //引脚
  ret = ledcSetup(ch0, 5000, 12); //设置ledc通道0,频率5000HZ,精度12 

  delay(200);
  if (ret == 0)
    Serial.println("Error Setup");
  else 
    Serial.println("Success Setup");
    
  ledcAttachPin(gpio4, ch0); //设置引脚和通道
  ledcWrite(ch0, pow(2, 11)); //占空比50%   2^11 / 2^12 = 1/2 

}

void loop() {
  // put your main code here, to run repeatedly:

}

3.2 LED呼吸灯

每秒钟固定调整占空比 50 次。 T 为呼吸周期,光从灭到最亮经过半个周期T/2。

半个周期进行 50*T/2 调整占空比

count 表示占空比为 100% 时等分的格子

step 为每次调整时要加上的增量 step = count / (50 * T/2) = 2 * count / (50 * T)

3.2.1 使用 delay() ,呼吸周期偏长

/* 每秒钟固定调整占空比50次。T为呼吸周期,光从灭到最亮经过半个周期T/2。
   半个周期进行 50*T/2 调整占空比
   count表示占空比为100%时等分的格子
   step为每次调整时要加上的增量  step = count / (50 * T/2) = 2 * count / (50 * T)
*/

int gpio4 = 4;
int ch1 = 1; //ledc通道号
int duty = 0; //目前信号的占空比
int count = 0; //100%占空比时的格子
int step = 0; //占空比步进值(增量)
int breathTime = 3; //呼周期,单位s

void setup() {
  ledcSetup(ch1, 1000, 12); //建立ledc通道
  count = pow(2, 12); //计算占空比为100%时共几份
  step = 2 * count / (50 * breathTime); //计算一次增加多少格子
  ledcAttachPin(gpio4, ch1); //绑定 ch1 和 GPIO4
}

void loop() {
  ledcWrite(ch1, duty);
  duty += step;
  if (duty > count)
  {
    duty = count;
    step = -step; //step变为负数,duty递减
  }
  else if (duty < 0)
  {
    duty = 0;
    step = -step; //step变为正数,duty递增
  }
  delay(20); //阻塞20ms
}

3.2.2 使用 millis

int prevTime = 0;
int gpio4 = 4;
int ch1 = 1; //ledc通道号
int duty = 0; //目前信号的占空比
int count = 0; //100%占空比时的格子
int step = 0; //占空比步进值(增量)
int breathTime = 3; //呼周期,单位s

void setup() {
  ledcSetup(ch1, 1000, 12); //建立ledc通道
  count = pow(2, 12); //计算占空比为100%时共几份
  step = 2 * count / (50 * breathTime); //计算一次增加多少格子
  ledcAttachPin(gpio4, ch1); //绑定 ch1 和 GPIO4
}

void loop() {
  int now =  millis(); 
  if (now - prevTime >= 20) //上次改变状态后已经过了 20ms
  {
    ledcWrite(ch1, duty);
    duty += step;
    if (duty > count)
    {
      duty = count;
      step = -step;
    } 
    else if (duty < 0)
    {
      duty = 0;
      step = -step;
    }
    prevTime = now;
  }
}

 

4. 软件定时器

使用 AsyncTimer 库进行定时操作,在 库管理 处进行安装。

定时器主要模式:

  1. 等待多长时间触发一个事件
  2. 每个多久时间触发一个事件
  3. 到某个时间点触发一个事件

定时器类型:

  1. 硬件定时器:ESP32只有4个
  2. 软件定时器:精度低,数量多

4.1 单次定时任务

串口定时打印信息

#include <AsyncTimer.h>

AsyncTimer t; //定义一个定时器

void myfun()
{
  Serial.println("the second");
}
void setup() {
  Serial.begin(115200);
  delay(200);
  
  //setTimeout(回调函数, 超时时间(ms)),回调函数可以无参无返回值
  auto id = t.setTimeout([](){ //第一个单次定时任务:2s 打印 the first
    Serial.println("the first");
  }, 2000);
  Serial.print("First:");
  Serial.println(id);

  id = t.setTimeout(myfun, 4000); //第二个单次定时任务:4s 打印 the second
  Serial.print("Second:");
  Serial.println(id);
}

void loop() {
  t.handle(); //执行有关定时器的操作,精度与loop()函数里面操作时间有关
}
First:62510
Second:36048
the first
the second

4.2 周期定时任务

#include <AsyncTimer.h>

AsyncTimer t;

void myfun()
{
  Serial.println("the second");
}

void setup() {
  Serial.begin(115200);
  delay(200);
  
  //setInterval(回调函数, 超时时间(ms)),回调函数可以无参无返回值
  auto id = t.setInterval([](){ //第一个周期定时任务:每 2s 打印 the first
    Serial.println("the first");
  }, 2000);
  Serial.print("First:");
  Serial.println(id);

  id = t.setInterval(myfun, 4000); //第二个周期定时任务:每 4s 打印 the second
  Serial.print("Second:");
  Serial.println(id);
}

void loop() {
  t.handle();
}
the first
the first
the second

4.3 闪烁LED改造

LED灯刚启动以1s周期进行闪烁,按键按下去后在1s和3s的周期进行切换

// LED灯刚启动以1s周期进行闪烁,按键按下去后在1s和3s的周期进行切换
#include <RBD_Button.h>
#include <AsyncTimer.h>

int switchPin = 25; // 按钮
int ledPin = 4; // led
int ledStatus = HIGH;
int t = 1; // 闪烁周期

// 软件消抖
RBD::Button button(switchPin, INPUT_PULLUP);

AsyncTimer timer;
int taskId = 0;

void ChangeLedStatus() // 改变LED状态函数
{
  ledStatus = !ledStatus; // 状态取反
  digitalWrite(ledPin, ledStatus); // 改变状态
}

void setup() {
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, HIGH); // 点亮
  button.setDebounceTimeout(20);
  // 创建周期任务
  taskId = timer.setInterval(ChangeLedStatus, t*1000);
}

void loop() {
  timer.handle();

  if (button.onPressed())
  {
    t = t == 1?3:1; // 周期定时时间为:1s或3s
    timer.changeDelay(taskId, t*1000);
  }
}

4.4 相关函数讲解

  1. 停止单个定时任务:

    cancel(intervalOrTimeoutId) ,intervalOrTimeoutid即定时任务的编号

  2. 停止多个定时任务:

    cancelAll(includeIntervals) ,参数默认值为true。

    true:取消所有定时任务 fasle:只取消单次定时任务

  3. 改变定时任务周期:

    changeDelay(intervalOrTimeoutId, delaylnMs) ,参数分别为定时任务的编号 和 新的超时时间(ms)

  4. 重置定时任务:

    reset(intervalOrTimeoutId),只能重置还没有停止的定时任务,重置完从0重新计时

  5. 额外延时一个定时任务:

    delay(intervalOrTimeoutId, delaylnMs) ,参数分别为定时任务的编号 和 额外延时时间(ms)

  6. 获取定时任务剩余时间:

    getRemaining(intervalOrTimeoutId) ,获取指定定时任务本轮还剩多久时间超时

    unsigned long remaining = getRemaining(timeoutId);
    

     

5. ADC模数转换

5.1 样例

void setup() {
  Serial.begin(115200);
  analogReadsolution(12); // 设置读取精度(位宽)

  //设置通道衰减值(不设置默认为11db)
  /*
  analogSetAttenuation(ADC_ATTEN_DB_11); // 设置所有通道
  analogSetPinAttenuation(2, ADC_ATTEN_DB_11); // 设置指定GPIO口的衰减值
  */
}

void loop() {
  int analogValue = analogRead(2); // 读取DAC值
  int analogVolts = analogReadMilliVolts(2); // 读取电压值(c)

  Serial.printf("ADC analog value = %d\n", analogValue);
  Serial.printf("ADC millivolts value = %d\n", analogVlots);

  delay(100);
}

5.2 电位器控制LED亮度

/* ADC + LEDC + 定时器(软件)
  通过更改定位器阻值控制LED亮度
*/
#include <AsyncTimer.h>

int pmPin = 32; // 电位器GPIO接口
int ledPin = 4; // LED
int ch0 = 0; // ledc通道

AsyncTimer timer;
int taskId = 0;

void ChangeLedLightness()
{
  int val = analogRead(pmPin);
  Serial.printf("%d:", val);

  auto vol = analogReadMilliVolts(pmPin);
  Serial.println(vol);

  int duty = val / 4095.0 * 1024;
  ledcWrite(ch0, duty);
}

void setup() {
  Serial.begin(115200);
  analogReadResolution(12); // 确定analogRead() 函数返回的值的分辨率(以位为单位)
  analogSetAttenuation(ADC_11db); // 设置所有通道衰减值

  ledcSetup(ch0, 1000, 10); // 设置ledc通道0,频率1000HZ,精度10
  ledcAttachPin(ledPin, ch0); 

  taskId = timer.setInterval(ChangeLedLightness, 20); //周期定时任务
}

void loop() {
  timer.handle();
}

 

6. I2C协议

6.1 I2C及Wire库使用

主机

// 主机Master
#include <Wire.h>

#define I2C_DEV_ADDR 0x55  // I2C设备地址

uint32_t i = 0;

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true); // 启用串口调试输出
  Wire.begin(); // 初始化I2C总线
}

void loop() {
  delay(5000);

  Wire.beginTransmission(I2C_DEV_ADDR); // 开始I2C传输
  Wire.printf("Hello World! %u", i++); // 向I2C设备发送数据
  uint8_t error = Wire.endTransmission(true); // 结束I2C传输并检查错误
  Serial.printf("endTransmission:%u\n", error);

  uint8_t bytesReceived = Wire.requestFrom(I2C_DEV_ADDR, 16); // 从I2C设备读取数据并返回接收到的字节数
  Serial.printf("requestFrom:%u\n", bytesReceived);
  if ((bool)bytesReceived) {
    uint8_t temp[bytesReceived];
    Wire.readBytes(temp, bytesReceived); // 读取接收到的字节
    log_print_buf(temp, bytesReceived); // 打印接收到的数据
  }
}

从机

// 从机Slave
#include "Wire.h"

#define I2C_DEV_ADDR 0x55

uint32_t i = 0;

/*
  onRequest()函数:用于处理主机的请求,在每次请求时,
  向主机发送递增的数据包计数,并打印调试信息。
*/
void onRequest(){
  Wire.print(i++);
  Wire.print("Packets.");
  Serial.println("onRequest");
}

//  onReceive()函数:用于处理主机发送的数据,在接收到数据时,打印接收到的数据内容和长度。
void onReceive(int len){
  Serial.printf("onReceived[%d]: ", len);
  while (Wire.available()){
    Serial.write(Wire.read());
  }
  Serial.println();
}

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Wire.onReceive(onReceive); // 注册接收回调函数
  Wire.onRequest(onRequest); // 注册请求回调函数
  Wire.begin((uint8_t)I2C_DEV_ADDR); // 初始化I2C从机

  // 如果是ESP系列芯片,可以使用slaveWrite函数发送初始消息
#if CONFIG_IDF_TARGET_ESP#@
  char message[64];
  snprintf(message, 64, "%u Packets.", i++);
  Wire.slaveWrite((uint8_t *)message, strlen(message));
#endif
}

void loop() {

}

6.2 ESP32双机通信

主机每秒2秒向从机发送递增的数字,

从机在收到主机的数据后LED闪烁0.5秒,并在收到的数字后加上OK字符发送给主机

主机收到从机发来的数据后打印在串口上

主机

主机程序使用了Wire库进行I2C通信。在setup函数中,初始化串口并加入I2C总线。在loop函数中,通过Wire.beginTransmissionWire.endTransmission向从机发送数字字符串,并通过Wire.requestFrom从从机接收数据。收到数据后,将其打印在串口上。

/* 
  主机每秒2秒向从机发送递增的数字,
  从机在收到主机的数据后LED闪烁0.5秒,并在收到的数字后加上OK字符发送给主机
  主机收到从机发来的数据后打印在串口上
*/

// 主机程序
#include <Wire.h>

int num = 1; // 发送给从机
int address = 33; // 从机地址

void setup() {
  Serial.begin(115200);

  if (Wire.begin()) // 主机加入I2C总线
    Serial.println("I2C Success");
  else 
    Serial.println("I2C Failed");
}

void loop() {
  char tmp[32];
  itoa(num++, tmp, 10); // 将数字转换成字符串

  Wire.beginTransmission(address);
  Wire.write(tmp); // 传输数字字符串
  int ret = Wire.endTransmission();
  if (ret != 0) // 判断状态
  {
    Serial.printf("Send failed:%d\r\n", ret);
    return;
  }

  delay(100); // 从机处理时间
    
  /*
  	Wire.requestFrom(address, quantity, stop);
  	requestFrom返回值代表了从机发来多少字节的数据,实际上是错误的,
  	返回值永远是等于你传进去的欲读取数据的数量值(quantity)
  	若 接收的数据量 > 从机发送的数据量,超出部分全部为 0x3f
  */
  int len = Wire.requestFrom(address, 32); // 发出请求,最多不超过32字节
  if (len > 0)
  {
    // 打印出来收到从机发来的数据
    Serial.print("Receive data size:");
    Serial.println(len);

    Wire.readBytes(tmp, 32);
    Serial.println(tmp);

    // 打印出收到数据的16进制值
    for (int i=0; i<32; i++)
    {
      Serial.printf("%2x, ", tmp[i]);
      if (i % 8 == 7)
        Serial.println();
    }
    Serial.println();
  }
  delay(1900);
}

从机

从机程序使用了Wire库进行I2C通信,并使用AsyncTimer库来控制LED闪烁。在onReceive函数中,当接收到数据时,将数据存储到缓冲区buf中,并让LED闪烁。在onRequest函数中,向主机发送带有"OK"字符的数据。

/* 
  主机每秒2秒向从机发送递增的数字,
  从机在收到主机的数据后LED闪烁0.5秒,并在收到的数字后加上OK字符发送给主机
  主机收到从机发来的数据后打印在串口上
*/

// 从机程序
#include <Wire.h>
#include <AsyncTimer.h>

char buf[32]; // 接受缓冲区
int ledPin = 4;

AsyncTimer timer;

void onReceive(int len) {
  // 接受数据,将数字存到缓冲区,并让led闪烁
  if (len > 0)
  {
    // 从I2C总线读取最多32个字节的数据,并将其存储到buf缓冲区中。函数返回实际读取到的字节数
    int sz = Wire.readBytes(buf, 32);
    if (sz > 0)
    {
      buf[sz] = 0;
      digitalWrite(ledPin, HIGH);

      // 注册定时事件,500ms后关闭led灯
      timer.setTimeout([](){
        digitalWrite(ledPin, LOW);
      }, 500);
    }
  }
}

void onRequest() {
   // 向主机发送数据
  strcat(buf, "OK"); // 拼接
  Wire.write(buf); // 发送缓冲区数据(包括"OK"字符)
  Wire.write(0);
}
void setup() {
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT); 
  Wire.onReceive(onReceive); // 注册接受事件
  Wire.onRequest(onRequest); // 注册发送事件
  Wire.begin(33);
}

void loop() { 
 timer.handle();
}

6.3 I2C操控1602LCD

需要下载 LiquidCrystal_I2C 库,地址为:https://github.com/mrkaleArduinoLib/LiquidCrystal_I2C

主要用到的文件为 LiquidCrystal_I2C.hLiquidCrystal_I2C.cpp 这两个文件

使用时移动到项目文件根目录并调用

#include <Arduino.h>
#include <Wire.h>
#include "LiquidCrystal_I2C.h"

LiquidCrystal_I2C lcd(0x27, 16, 2); // LiquidCrystal_I2C lcd(显示器地址, 行数, 列数);

void setup() {
  lcd.init(); // 初始化 LCD 显示器
  lcd.backlight(); // 打开背光
  lcd.print("Hello World!"); // 在第一行打印 "Hello World!"
  // lcd.setCursor(列号, 行号)
  lcd.setCursor(0, 1); // 设置光标位置为第二行第一列
  lcd.print("I am a fish, I am a fish, I am a fish."); // 在第二行打印 "I am a fish, I am a fish, I am a fish."

  // 将第二行的 "am" 改成大写 "AM"
  lcd.setCursor(2, 1); // 设置光标位置为第二行第三列
  lcd.write('A'); // 写入大写字母 'A'
  lcd.write('M'); // 写入大写字母 'M'

  lcd.clear(); // 清空显示器

  // 字幕不停向左滚动
  for (int i = 0; i < 100; i++) {
    lcd.scrollDisplayLeft(); // 向左滚动显示内容
    delay(1000); // 延迟1秒
  }
}

void loop() {

}

 

7. 外部中断(硬件)

中断服务程序要求:

  • 要尽量地短,减少执行时间
  • 不要使用 delay() 函数
  • 不要使用 Serial 打印
  • 和主程序共享的变量要加_上 volatile 关键字
  • 不要使用 millis() 函数,它的值将不会增长
  • 可以使用 micros 函数来获取时间
  • 外部中断最高频率手册没说,但达到几M是没有问题的

7.1 按键开关LED

IRAM_ATTR 是一个ESP32的特殊属性,用于指定函数在IRAM(内部RAM)中运行,而不是默认的闪存(Flash)中运行。在ESP32中,IRAM是位于处理器内部的高速随机访问存储器,执行速度更快。

使用 IRAM_ATTR 属性可以将函数加载到IRAM中,从而提高函数的执行速度和响应性能。在中断服务程序(ISR)中使用 IRAM_ATTR 属性可以确保ISR在最短的时间内得到执行,从而更及时地响应中断事件。

因此,IRAM_ATTR 修饰符常常用于将中断服务程序(ISR)函数加载到IRAM中,以提高性能。

const byte LED = 4;
const byte BUTTON = 25;

// ISR
IRAM_ATTR void switchPressed()
{
  // 按钮松开高电平亮,按钮按下低电平灭
  if (digitalRead(BUTTON) == HIGH)
    digitalWrite(LED, HIGH);
  else 
    digitalWrite(LED, LOW);
}

void setup() {
  pinMode(LED, OUTPUT);
  pinMode(BUTTON, INPUT_PULLUP);
  // 设置和执行ISR(中断服务程序)
  attachInterrupt(digitalPinToInterrupt(BUTTON), switchPressed, CHANGE);
}

void loop() {

}

7.2 简单PWM测量仪

临界区 是一段代码片段,用于在多任务环境下保护共享资源,以确保对资源的访问不会被并发任务中断或干扰。临界区的作用是提供一种互斥机制,使得同一时间只有一个任务可以访问共享资源,避免并发访问导致的数据竞争和不一致性。

#include "LiquidCrystal_I2C.h" // 包含 LiquidCrystal_I2C 库,用于LCD显示器
// 共享变量
volatile unsigned long raiseTime = 0; // 前一次上升沿时间
volatile unsigned long fallTime = 0; // 前一次下降沿时间
volatile double duty = 0; // 占空比
volatile double fre = 0; // 频率

int pwmPin = 27; // 信号输入接口

// 显示器初始化
LiquidCrystal_I2C lcd(0x27, 16, 2);

// 自旋锁
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;

// ISR:中断服务程序
void changeISR()
{
  auto now = micros();
  if (digitalRead(pwmPin)) // 现在是高
  {
    /*
     临界区是一段代码片段,用于在多任务环境下保护共享资源,以确保对资源的访问不会被并发任务中断或干扰。
     临界区的作用是提供一种互斥机制,使得同一时间只有一个任务可以访问共享资源,避免并发访问导致的数据竞争和不一致性。
    */
    portENTER_CRITICAL_ISR(&mux); // 进入临界区
    auto total = now - raiseTime; // 周期 us
    fre = 1e6 / (double)total; // 频率
    auto h = fallTime - raiseTime; // 脉宽
    duty = h / (double)total; // 占空比 = 脉宽 / 周期
    portEXIT_CRITICAL_ISR(&mux); // 离开临界区
    raiseTime = now;
  }
  else
  {
    fallTime = now;
  }
}

void setup() { 
 lcd.init(); // 初始化 LCD 显示器
  lcd.backlight(); // 打开背光
  lcd.setCursor(0, 0); // 设置光标位置为第一行第一列
  lcd.print("fre: "); // 在 LCD 上打印 "fre: "
  lcd.setCursor(0, 1); // 设置光标位置为第二行第一列
  lcd.print("duty: "); // 在 LCD 上打印 "duty: "
  pinMode(pwmPin, INPUT); // 将 pwmPin 设置为输入模式
  attachInterrupt(digitalPinToInterrupt(pwmPin), changeISR, CHANGE); // 注册中断服务程序来响应 pwmPin 引脚状态变化的事件
}

void loop() {
  delay(1000); // 延迟1秒

  portENTER_CRITICAL(&mux); // 进入临界区
  double f = fre; // 读取频率值
  double d = duty; // 读取占空比值
  portEXIT_CRITICAL(&mux); // 离开临界区

  lcd.setCursor(5, 0); // 设置光标位置为第一行第五列
  lcd.print(f); // 在 LCD 上打印频率值
  lcd.setCursor(6, 1); // 设置光标位置为第二行第六列
  lcd.print(d); // 在 LCD 上打印占空比值
} 

 

8. 硬件定时器及二值信号量

分频数越大,周期越长,频率越低。分频数最大是 65525

流程 :初始化 -> 绑定ISR -> 设置触发ISR的计数值 -> 启动定时器

硬件定时器流程

#include <esp32-hal-timer.h>

hw_timer_t *timer = NULL;

void IRAM_ATTR timerISR() {
  // 硬件定时器中断服务程序
}

void setup() {
  timer = timerBegin(0, 80, true); // 创建硬件定时器,使用定时器 0,预分频因子 80,设置为自动重载模式
  timerAttachInterrupt(timer, &timerISR, true); // 将定时器中断服务程序与硬件定时器关联
  timerAlarmWrite(timer, 1000000, true); // 设置定时器定时周期为 1 秒,自动重载,即周期循环
  timerAlarmEnable(timer); // 启用定时器定时中断"
  // timerEnd(timer); // 结束
}

void loop() {
  // 主循环代码
}

8.1 硬件定时器样例

每 1s 打印一次当前迭代数和时间

// 每 1s 打印一次当前迭代数和时间
#include <esp32-hal-timer.h>
volatile int count = 0;
volatile unsigned long tim = 0;

hw_timer_t *timer1 = NULL; // 1s 1次
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

// ISR
void IRAM_ATTR onTimer1() {
  portENTER_CRITICAL_ISR(&timerMux); // 进入临界区
  count ++;
  tim = micros();
  portEXIT_CRITICAL_ISR(&timerMux); // 离开临界区
}

void setup() {
  Serial.begin(115200);
  // 初始化定时器,80分频,1us计数一次
  timer1 = timerBegin(0, 80, true);
  // 附加中断
  timerAttachInterrupt(timer1, onTimer1, true);
  // 计数到 1000000(1s) 时触发中断
  timerAlarmWrite(timer1, 1000000, true);
  // 开启定时器
  timerAlarmEnable(timer1);
}

void loop() {
  portENTER_CRITICAL(&timerMux);
  auto c = count;
  auto t = tim;
  portEXIT_CRITICAL(&timerMux);

  Serial.println(c);
  Serial.println(t);
}

loop() 函数 中的 portENTER_CRITICAL(&timerMux) 会启用自旋锁,并且禁用掉了CPU的中断。

loop()函数执行速度很快,中断被屏蔽时间会非常长,外部如果有两个或以上中断进来无法及时检测到。

想要解决这个问题,这时候就需要 二值信号量了。

8.2 二值信号量

// 每 1s 打印一次当前迭代数和时间
#include <esp32-hal-timer.h>
volatile int count = 0;
volatile unsigned long tim = 0;
volatile SemaphoreHandle_t timerSemaphore; // 信号量

hw_timer_t *timer1 = NULL; // 1s 1次
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

// ISR
void IRAM_ATTR onTimer1() {
  portENTER_CRITICAL_ISR(&timerMux); // 进入临界区
  count ++;
  tim = micros();
  portEXIT_CRITICAL_ISR(&timerMux); // 离开临界区

  /*
    从中断服务程序(ISR)中给予一个二值信号量它会将二值信号量的计数值增加,
    并唤醒等待该信号量的任务。第二个参数为 NULL 表示不需要唤醒任何任务。
  */
  // 设置完共享变量后发送信号 
  xSemaphoreGiveFromISR(timerSemaphore, NULL);
}

void setup() {
  Serial.begin(115200);

  timerSemaphore = xSemaphoreCreateBinary(); // 创建一个二值信号量
  // 初始化定时器,80分频,1us计数一次
  timer1 = timerBegin(0, 80, true);
  // 附加中断
  timerAttachInterrupt(timer1, onTimer1, true);
  // 计数到 1000000(1s) 时触发中断
  timerAlarmWrite(timer1, 1000000, true);
  // 开启定时器
  timerAlarmEnable(timer1);
}

void loop() {
  if (xSemaphoreTake(timerSemaphore, 0) == pdTRUE)
  {
    portENTER_CRITICAL(&timerMux);
    auto c = count;
    auto t = tim;
    portEXIT_CRITICAL(&timerMux);

    Serial.println(c);
    Serial.println(t);
  }
  
}

 

9. 超声波测距

HC-SR04 模块测量错误的情况:

  1. 物体体积太小,无法反射超声波
  2. 物体在探头15°范围之外
  3. 物体表面材质是吸收超声波的,比如毛绒绒的物体
  4. 物体与探头的夹角不对

9.1 距离测量

const int trigPin = 4;
const int echoPin = 16;

void setup() {
  Serial.begin(115200);
  delay(200);
  pinMode(trigPin, OUTPUT);
  pinMode(echoPin, INPUT);
}

void loop() {
  // 在Trig引脚发送15us脉冲
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(15); // 15us
  digitalWrite(trigPin, LOW);

  // 读取Echo引脚脉冲时长
  auto t = pulseIn(echoPin, HIGH);
  double dis = t * 0.01715; // 单位:CM
  Serial.print(dis);
  Serial.println(" cm");

  delay(200);
}

此程序阻塞过长,下面将使用中断方式优化

9.2 距离测量(中断优化)

中断测距原理:

  • 外部中断(change) 附加到 ECHO 的引脚上
  • 使用硬件定时器每 500msTrigger 一个 15us 的脉冲 (1s测量2次)
  • 在上升沿中断的时候记当前时间 t1
  • 在下降沿中断的时候记当前时间 t2,并发 信号(Semaphore) 给任务
  • Loop函数在收到信号后获取 t2和t1 的值,并计算出距离
// 中断测距
/* - 将 外部中断(change) 附加到 ECHO 的引脚上
- 使用硬件定时器每 500ms 给 Trigger 一个 15us  的脉冲 (1s测量2次)
- 在上升沿中断的时候记当前时间 t1 
- 在下降沿中断的时候记当前时间 t2,并发 信号(Semaphore)`  给任务
- Loop函数在收到信号后获取 t2和t1 的值,并计算出距离
*/

const int trigPin = 4;
const int echoPin = 16;
double distance = 0; // 单位cm

hw_timer_t *timer1 = NULL; // 定时器
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; // 自旋锁
volatile unsigned long startTime = 0; // 发出超声波时间
volatile unsigned long endTime = 0; // 收到超声波时间
volatile SemaphoreHandle_t semaphore; // 信号量

// 硬件定时器ISR
void IRAM_ATTR ping()
{
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(15);
  digitalWrite(trigPin, LOW);
}

// ECHO 引脚ISR
void IRAM_ATTR changeISR() 
{
  auto now = micros(); // 当前时间
  auto state = digitalRead(echoPin);

  portENTER_CRITICAL_ISR(&mux);
  if (state) // 高电平,即刚发出超声波
    startTime = now;
  else
    endTime = now;
  portEXIT_CRITICAL_ISR(&mux);

  // 变成低电平时表示已经收到回声
  if (!state)
    xSemaphoreGiveFromISR(semaphore, NULL); // 给一个信号量发送信号
}


void setup() {
  pinMode(trigPin, OUTPUT);
  pinMode(echoPin, INPUT);
  Serial.begin(115200);

  semaphore = xSemaphoreCreateBinary(); // 创建二值信号量

  // 定时器部分
  timer1 = timerBegin(0, 80, true);
  timerAttachInterrupt(timer1, ping, true);
  timerAlarmWrite(timer1, 500000, true); // 定时时间为 0.5s
 
  // echo引脚的中断
  attachInterrupt(digitalPinToInterrupt(echoPin), changeISR, CHANGE);

  // 开始周期测量
  timerAlarmEnable(timer1);

}

void loop() {
  if (xSemaphoreTake(semaphore, 0) == pdTRUE)
  {
    // 收到信号,准备工作
    portENTER_CRITICAL(&mux);
    auto t = endTime - startTime;
    portEXIT_CRITICAL(&mux);

    double dis = t * 0.01715;
    if (dis < 350)
    {   
        distance = dis;
        Serial.print("Distance: "); 
        Serial.print(distance, 1); // 小数点后1位
        Serial.println(" cm");
    }
  }
}

 

10. 舵机

10.1 Servo库操控舵机

库名称为 ESP32Servo

#include <ESP32Servo.h>

Servo servo1; // 定义对象
Servo servo2;

int minUs = 500; // 0°时的脉宽,单位us
int maxUs = 2500; // 180°时的脉宽,单位us

int servo1Pin = 15;
int servo2Pin = 16;
int pos = -1; // 舵机角度
bool up = true; // 计数方向

void setup() {
  ESP32PWM::allocateTimer(1); // 指定使用的硬件定时器

  servo1.setPeriodHertz(50); // 指定PWM的频率
  servo2.setPeriodHertz(50); // 指定PWM的频率

  servo1.attach(servo1Pin, minUs, maxUs);
  servo2.attach(servo2Pin, minUs, maxUs);

}

void loop() {
  if (pos == 181)
    up = false;
  else if (pos == -1)
    up = true;
  
  if (up)
    pos ++;
  else
    pos --;
  
  servo1.write(pos);
  servo2.write(180 - pos);

  //servo1.write(pos); // 转到指定的角度(0° - 180°)
  //servo1.detach(); // 不需要的时候将引脚和ledc分离

  delay(15);
}

10.2 智能垃圾桶

使用超声波测距配合舵机实现智能垃圾桶,因为懒得弄模型,所以垃圾桶开闭直接用串口打印信息。

其相关流程及代码部分见此篇文章:ESP32Demo:智能垃圾桶 – Echo (liveout.cn)

 

11. WiFi连接

#include<WiFi.h>
const char* ssid = "WiFi名称";
const char* password = "WiFi密码";
void setup() {
  //初始化串口
  Serial.begin(115200);
  delay(10);
    
  // 进行WiFi连接
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  //连接WIFI
  WiFi.begin(ssid, password);
  //等待WIFI连接成功
  while (WiFi.status() != WL_CONNECTED) { //WiFi.status()函数用于获取WiFi连接的状态
    //WL_CONNECTED,即连接状态
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");

}

void loop() {
    
}

 

 

--> 前言此篇文章为有关 ESP32 的学习期间的代码记录,并且加上了自己的注释,非教学文章。使用开发板全称ESP32 DEVKILTv1(devkitv1) ,搭载芯片为 ESP32D0WDQ6,使用软件为 Arduino 。  参考链接如果是小白并且想要学习单片机相关知识,建议移步此篇文章:51单片机入门教程(上篇)(代码+个人理解) – Echo (liveout.cn...