Justin知识库 - C
type: channel
link: https://www.just1n.cn/tag/C/
items: 使用 Socket 建立 IPC 通信
items: 使用 Socket 建立 UDP 连接
items: 使用 Socket 建立多个 TCP 连接
items: Socket 编程
items: 线程同步
items: 线程处理
items: 进程通信
items: 文件 io
items: 步进电机—半步驱动
items: I2C 通信过程代码实现

使用 Socket 建立 IPC 通信
type: item
date: 2024-10-16T10:20:00+08:00
description: IPC（Inter-Process Communication）即进程间通信，是计算机科学中的一个重要概念，是指两个不同进程之间数据交换的过程。 Socket 编程原本是为了网络服务的，之前已介绍。后来逐渐发展成一种进程间通信的方式：Unix Domain Socket IPC。它允许在同一台主机上运行的进程之间进行高效的数据传输，无需经过网络协议栈，因此具有低延迟和高性能的特点。通过文件系统中的特殊文件（通常是一个套接字文件），进程可以通过套接字（socket）来进行通信，实现双向的数据传输。 Socket 编程 Socket 编程是网络通信中的一项基础而强大的技术，它允许两台计算机（或同一台计算机上的两个进程）之间进行双向通信。Socket 可以看作是网络通信中的一个端点，通过它可以发送或接收数据。在 TCP/IP 协议族中， Socke...2024-09-013 IPC通信的原理与机制 IPC 通信的原理是通过特定的机制，使得不同进程之间能够相互传递信息、协作完成任务。这些机制主要包括共享内存、消息传递、管道以及套接字等。这些机制主要包括以下几种： 1. 共享内存： 原理：通过将某一块内存区域映射到多个进程的地址空间中，实现不同进程之间的数据共享。 优点：可以高效地传递大量数据。 缺点：需要解决进程间的同步和互斥问题，以保证数据的正确性。如果多个进程同时访问共享内存区域，可能会导致数据冲突或不一致。 2. 消息传递： 原理：通过发送和接收消息的方式进行进程间通信。发送方将消息发送到一个消息队列或者直接发送给特定的进程，接收方则从消息队列中读取消息或者通过特定的接收机制接收消息。 优点：可以实现异步通信和进程间的解耦。发送方和接收方可以独立地运行，不需要知道对方的存在和状态。 缺点：需要解决消息的传递顺序和消息的可靠性问题。如果消息丢失或顺序错乱，可能会导致通信失败或数据不一致。 3. 管道： 原理：一种半双工的通信方式，用于连接一个读进程和一个写进程。管道可以使得两个进程能够单向地传递数据，其中一个进程充当写端，将数据写入管道，另一个进程充当读端，从管道中读取数据。 优点：适用于父子进程或者具有亲缘关系的进程之间的通信。 缺点：不适用于无关进程间的通信。同时，管道只能进行单向通信，且通信双方的进程必须同时存在。 4. 套接字： 原理：一种在不同主机间进行网络通信的机制，它可以在应用层与传输层之间建立连接，并通过网络传输数据。套接字可以通过TCP或UDP协议进行通信，可以在同一台主机上的不同进程间或者不同主机间进行通信。 优点：具有灵活性和可扩展性。套接字可以支持多种通信协议和数据传输方式，适用于各种复杂的网络通信场景。 缺点：需要注意网络传输的可靠性和安全性。网络传输过程中可能会出现数据丢失、延迟等问题，同时还需要考虑网络安全和身份验证等问题。 套接字实现 使用AF_UNIX通信域，可声明一个使用本地文件的 socket，其他进程可绑定该文件完成进程间通信。 socket_ipc.c： #include <stdlib.h> #include <stdio.h> #include <stddef.h> #include <sys/socket.h> #include <sys/un.h> #include <sys/stat.h> #include <errno.h> #include <string.h> #include <unistd.h> #define SOCKET_PATH "unix_domain.socket" #define SERVER_MODE 1 #define CLIENT_MODE 2 #define BUF_LEN 1024 #define OUTTEXT ("请输入要发送的信息：") #define OUTSIZE sizeof(OUTTEXT) static struct sockaddr_un socket_addr; static char *buf; void handle_error(char *err_msg) { perror(err_msg); unlink(SOCKET_PATH); // 释放 exit(-1); } void server_mode(int sockfd) { struct sockaddr_un client_addr; int client_fd, msg_len; /**** 服务端接收消息，收到 EOF 结束信息 ****/ // 1. 绑定 if (bind(sockfd, (struct sockaddr *)&socket_addr, sizeof(socket_addr)) < 0) { handle_error("bind"); }…

使用 Socket 建立 UDP 连接
type: item
date: 2024-10-09T12:00:00+08:00
description: UDP（用户数据报协议）是一种无连接的协议，相比于TCP（传输控制协议），它不需要建立连接、维护连接状态以及进行错误检查和重传，因此通常用于对实时性要求较高、但对数据完整性要求不高的场景，如视频流、在线游戏等。 本文将通过 UDP 展示如何发送和接收数据，适用于简单的应用场景。对于更复杂的需求，可能需要进一步扩展和优化代码。之前已实现 TCP 的接发数据。 Socket 编程 Socket 编程是网络通信中的一项基础而强大的技术，它允许两台计算机（或同一台计算机上的两个进程）之间进行双向通信。Socket 可以看作是网络通信中的一个端点，通过它可以发送或接收数据。在 TCP/IP 协议族中， Socke...2024-09-013 使用 Socket 建立多个 TCP 连接 使用Socket来建立多个TCP连接是一种常见的网络通信方式，在客户端和服务器之间进行数据交换。需要注意的是，服务器在处理多个客户端时，应该为每个客户端连接创建一个新的线程或进程，或者使用异步IO来避免阻塞。 2024-09-241 UDP 通讯流程 UDP通讯也使用socket，但是接收和发送的函数与 TCP 不一样。由于 UDP 不存在握手这一步骤，所以在绑定地址之后，服务端不需要 listen，客户端也不需要 connect，服务端同样不需要 accept。只要服务端绑定以后，就可以相互发消息了，由于没有握手过程，两端都不能确定对方是否收到消息，这也是 UDP 协议不如 TCP 协议可靠的地方。 编程基本步骤 服务端流程： 创建套接字，选择套接字类型（如 SOCK_DGRAM 表示数据报套接字） 绑定地址和端口 接收数据 处理数据（可选） 发送响应（可选）：如果需要，使用 sendto()发送响应数据 关闭套接字 客户端流程： 创建套接字 指定服务器地址和端口 发送数据，使用 sendto() 函数发送数据 接收响应（可选）：可以使用 recvfrom() 函数接收数据 关闭套接字 函数介绍 UDP 协议接收和发送数据不再使用 send 和 recv 方法，这两个方法一般用于 TCP 通信，UDP 通信使用 sendto 和 recvfrom方法。 需要包含的头文件： #include <sys/types.h> #include <sys/socket.h> 1. sendto ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); /** * @brief 向指定地址发送缓冲区中的数据（一般用于UDP模式） * * @param sockfd 套接字文件描述符 * @param buf 缓冲区指针 * @param len 缓冲区大小 * @param flags 通信标签，详细减send方法说明 * @param dest_addr 目标地址。如果用于连接模式，该参数会被忽略 * @param addrlen 目标地址长度 * @return ssize_t 发送的消息大小。发送失败返回-1 */ 2. recvfrom ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); /** * @brief 将接收到的消息放入缓冲区 buf 中。 * * @param sockfd 套接字文件描述符 * @param buf 缓冲区指针 * @param len 缓冲区大小 * @param flags 通信标签，详见recv方法说明 * @param src_addr 可以填NULL，如果 src_addr 不是 NULL，并且底层协议提供了消 息的源地址，则该源地址将被放置在 src_addr 指向的缓冲区中。 * @param addrlen 如果src_addr不为NULL，它应初始化为与 src_addr 关联的缓冲 区的大小。返回时，addrlen 被更新为包含实际源地址的大小。如果提供的缓冲区太小， 则返回的地址将被截断；在这种情况下，addrlen 将返回一个大于调用时提供的值。 * @return ssize_t 实际收到消息的大小。如果接收失败，返回-1 */ 示例程序 udp_server.c： #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #incl…

使用 Socket 建立多个 TCP 连接
type: item
date: 2024-09-24T21:08:00+08:00
description: 使用Socket来建立多个TCP连接是一种常见的网络通信方式，在客户端和服务器之间进行数据交换。需要注意的是，服务器在处理多个客户端时，应该为每个客户端连接创建一个新的线程或进程，或者使用异步IO来避免阻塞。 本文将以多线程和多进程两种方式实现，可以运行多个客户端实例来测试服务器是否能处理多个TCP连接。可以简单地在命令行中多次运行客户端脚本，或者写一个循环来多次创建和关闭客户端连接。 在C语言中，实现一个基本的 TCP 服务器和客户端涉及使用 socket 编程接口，这通常包括 socket(), bind(), listen(), accept(), send(), recv() 和 close() 等函数，之前有所介绍。Socket 编程 Socket 编程是网络通信中的一项基础而强大的技术，它允许两台计算机（或同一台计算机上的两个进程）之间进行双向通信。Socket 可以看作是网络通信中的一个端点，通过它可以发送或接收数据。在 TCP/IP 协议族中， Socke...2024-09-013 示例程序 以多线程和多进程两种方式实现，支持多个客户端连接服务器。需要注意的是服务器将监听一个特定的端口，并接受来自客户端的连接。一旦连接建立，它将接收来自客户端的消息，并将其回显给客户端，而客户端的端口绑定时自动分配的，当然也可以修改程序成一个指定端口。 基于多线程支持多个 TCP 连接 server.c #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> #include <pthread.h> #define PORT 6666 #define BUFFSIZE 1024 #define TYPESIZE (sizeof(char) * BUFFSIZE) #define handle_error(cmd, result) \ if(result < 0) \ { \ perror(cmd); \ return -1; \ } void * read_from_client_then_write(void * arg) { // 读取客户端发送过来的数据 回复收到 int client_fd = *(int *)arg; char * read_buf = NULL; char * write_buf = NULL; ssize_t count = 0, send_count = 0; // 初始化 read_buf = malloc(sizeof(char) * BUFFSIZE); write_buf = malloc(sizeof(char) * BUFFSIZE); if (!read_buf) { printf("服务端读缓存创建异常，断开连接\n"); close(client_fd); perror("read_buf"); return NULL; } if (!write_buf) { printf("服务端写缓存创建异常，断开连接\n"); close(client_fd); perror("read_buf"); return NULL; } while (count = recv(client_fd, read_buf, BUFFSIZE, 0)) { if (count < 0) { perror("recv"); } // 接收数据打印到控制台 printf("从_%d_客户端接收到数据:%s", client_fd, read_buf); // 把收到的消息写到写缓存 strcpy(write_buf, "收到\n"); send_count = send(client_fd, write_buf, BUFFSIZE, 0); if (send_count < 0) { perror("send"); } } // 当客户端输入 ctrl+D 退出循环 // shutdown(client_fd, SHUT_RD); close(client_fd); free(read_buf); free(write_buf); } int main(int argc, char const *argv[]) { struct sockaddr_in server_addr, client_addr; int sock_fd, tmp_res; // 清空 memset(&server_addr, 0, sizeof(…

Socket 编程
type: item
date: 2024-09-01T20:31:00+08:00
description: Socket 编程是网络通信中的一项基础而强大的技术，它允许两台计算机（或同一台计算机上的两个进程）之间进行双向通信。Socket 可以看作是网络通信中的一个端点，通过它可以发送或接收数据。在 TCP/IP 协议族中， Socket 扮演着核心角色，用于实现不同主机之间的进程间通信。 简单来说就是一种编程接口，允许在不同主机上的进程（运行中的程序）通过网络进行数据交换。 Socket 的类型 Socket 主要分为两种类型： 流式 Socket（SOCK_STREAM）：基于 TCP 协议，提供面向连接的、可靠的数据传输服务。 数据传输的顺序与发送的顺序相同，数据不重复，且接收方必须读取了发送方发送的所有数据后才能继续接收后续数据。 数据报 Socket（SOCK_DGRAM）：基于 UDP 协议，提供无连接的、不可靠的数据传输服务。 数据报是独立的，保留数据边界，发送方和接收方之间没有明确的连接，且发送方和接收方都可能不按顺序接收数据。 套接字的组成 一个套接字主要由以下三个属性组成： 网络地址：通常是 IP 地址，用于标识网络上的设备。 端口号：用于标识设备上的特定应用或进程。端口号是一个16位的数字，范围从0到65535。 协议：如 TCP（传输控制协议）或 UDP（用户数据报协议），定义了数据传输的规则和格式。 Socket 编程的基本步骤 Socket 编程的基本步骤大致相同，无论使用的是哪种 Socket 类型（TCP或UDP），但具体细节（如连接管理）会有所不同。以下是一个简化的 TCP Socket 编程步骤： 服务器端 创建Socket：使用 socket() 函数创建一个新的 Socket。 绑定Socket：使用 bind() 函数将Socket与特定的 IP 地址和端口号绑定。 监听连接：使用 listen() 函数使Socket进入被动监听状态，等待客户端的连接请求。 接受连接：使用 accept()函数接受客户端的连接请求，该函数会返回一个新的 Socket，用于与客户端进行通信。 数据收发：使用 read() / write() 或 recv() / send() 函数与客户端进行数据的发送和接收。 关闭连接：数据传输完成后，使用 close() 函数关闭Socket连接。 客户端 创建Socket：使用 socket() 函数创建一个新的 Socket。 连接服务器：使用 connect() 函数与服务器建立连接。 数据收发：使用 read() / write() 或 recv() / send() 函数与服务器进行数据的发送和接收。 关闭连接：数据传输完成后，使用 close() 函数关闭 Socket 连接。 注意事项 在进行 Socket 编程时，需要注意处理网络异常和错误，例如连接失败、数据接收不完整等。 对于 TCP 连接，要注意处理好连接的建立和断开，避免资源泄露。 对于 UDP 连接，虽然不需要建立连接，但也要注意数据包的完整性和顺序性。 在进行跨网络通信时，需要注意防火墙和网络策略的设置，确保数据能够顺利传输。 示例程序 为方便了解连接建立过程，该仅支持同时建立单个 TCP 连接，实现双方可以互相读写。对于相关函数参数并未做详细解释，如需请自行查看相关文档。目前还是很多都在采用 AF_INET（IPv4），使用时需要注意的是网络字节序和主机字节序转化。 server.c： #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> #include <pthread.h> #define PORT 6666 #define BUFFSIZE 1024 #define TYPESIZE (sizeof(char) * BUFFSIZE) #define handle_error(cmd, result) \ if(result < 0) \ { \ perror(cmd); \ return -1; \ } void * read_from_client(void * arg) { // 使用recv接收客户端发送的数据 打印到控制台 char * read_buf = NULL; int client_fd = *(int *)arg; // 得到文件描述符 read_buf = malloc(TYPESIZE); ssize_t count = 0; if (!read_buf) { perror…

线程同步
type: item
date: 2024-08-31T20:46:00+08:00
description: 线程同步的主要目的是确保多个线程在访问共享资源时能够按照一定的顺序进行，避免因为多个线程同时操作同一资源而导致的数据混乱或错误，以下解释 Linux 中线程同步的竞态条件和锁。 竞态条件和锁 当多个线程并发访问和修改同一个共享资源（如全局变量）时，如果没有适当的同步措施，就会遇到线程同步问题。这种情况下，程序最终的结果依赖于线程执行的具体时序，导致了竞态条件。 竞态条件（race condition）是一种特定的线程同步问题，指的是两个或者以上进程或者线程并发执行时，其最终的结果依赖于进程或者线程执行的精确时序。它会导致程序的行为和输出超出预期，因为共享资源的最终状态取决于线程执行的顺序和时机。为了确保程序执行结果的正确性和预期一致，需要通过适当的线程同步机制来避免竞态条件。 如何避免竞态条件 避免多线程写入一个地址。 给资源加锁，使同一时间操作特定资源的线程只有一个。 常见的锁机制 锁主要用于互斥，即在同一时间只允许一个执行单元（进程或线程）访问共享资源。包括上面的互斥锁在内，常见的锁机制共有三种： （1）互斥锁（Mutex）：保证同一时刻只有一个线程可以执行临界区的代码。 （2）读写锁（Reader/Writer Locks）：允许多个读者同时读共享数据，但写者的访问是互斥的。 （3）自旋锁（Spinlocks）：在获取锁之前，线程在循环中忙等待，适用于锁持有时间非常短的场景，一般是Linux内核使用。 后续 这里是学习中遇到的或想到的问题，以下都是个人总结。 2024-08-31 补充：关于读写锁中出现的写饥饿问题。 ① 问题描述 读写锁的写饥饿问题（Writer Starvation）是指在使用读写锁时，写线程可能无限期地等待获取写锁，因为读线程持续地获取读锁而不断地推迟写线程的执行。这种情况通常在读操作远多于写操作时出现。 ② 解决方案 Linux 提供了可以修改的属性 pthread_rwlockattr_t，默认情况下，属性中指定的策略为“读优先”，当写操作阻塞时，读线程依然可以获得读锁，从而在读操作并发较高时导致写饥饿问题。我们可以尝试将策略更改为“写优先”，当写操作阻塞时，读线程无法获取锁，避免了写线程持有锁的时间持续延长，使得写线程获取锁的等待时间显著降低，从而避免写饥饿问题。 ③ 个人总结 没有妥善管理读写操作的优先级，导致写者无限期等待资源。

线程处理
type: item
date: 2024-08-31T20:28:00+08:00
description: 线程处理是计算机编程中的一个重要概念，它涉及到多线程编程的各个方面，包括线程的创建、调度、同步、互斥、通信以及异常处理等。 线程的作用 线程处理在软件开发中扮演着重要角色，主要体现在以下几个方面： 1. 提高应用程序的响应能力：通过多线程处理，可以将耗时的操作放在后台线程中执行，从而避免阻塞主线程，使得应用程序能够更快地响应用户的操作。 2. 提高程序的吞吐量：在多处理器或多核系统上，多线程处理可以使得多个线程并行执行，从而提高程序的执行效率，减少程序的总体执行时间。 3. 实现复杂的并发操作：在需要处理多个并发任务时，如网络通信、文件操作等，多线程处理提供了一种有效的解决方案。 Linux 线程简介 Linux 中的线程是指轻量级的执行单元，相比于进程，具有以下特点： （1）进程（Process）是正在执行的程序的实例。每个进程都有自己的地址空间、代码段、数据段和打开的文件描述符等资源。线程（Thread）是进程内的一个执行单元，它共享相同的地址空间和其他资源，包括文件描述符、信号处理等，但每个线程都有自己的栈空间。 （2）由于共享地址空间和数据段，同一进程的多线程之间进行数据交换比进程间通信方便很多，但也由此带来线程同步问题。 （3）同一进程的多线程共享大部分资源，除了每个线程独立的栈空间。这代表线程的创建、销毁、切换要比进程的创建、销毁、切换的资源消耗小很多，所以多线程比多进程更适合高并发。 线程控制 线程创建 pthread_create #include <pthread.h> /** * 创建一个新线程 * * pthread_t *thread: 指向线程标识符的指针,线程创建成功时,用于存储新创建线程的线程标识符 * const pthread_attr_t *attr: pthead_attr_t结构体,这个参数可以用来设置线程的属性, * 如优先级、栈大小等。如果不需要定制线程属性,可以传入 NULL,此时线程将采用默认属性。 * void *(*start_routine)(void *): 一个指向函数的指针,它定义了新线程开始执行时的入口点。 * 这个函数必须接受一个 void * 类型的参数,并返回 void * 类型的结果 * void *arg: start_routine 函数的参数,可以是一个指向任意类型数据的指针 * return: int 线程创建结果 * 成功 0 * 失败 非0 */ int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); 每个线程都有一个唯一的标识符（即线程ID），这个标识符是通过 pthread_t 类型的变量来表示的，当pthread_create 成功创建一个线程时，它会将新线程的标识符存储在 thread 参数指向的位置。 pthread_t定义在头文件 <pthreadtypes.h> 中，实际上是 long 类型（long 和 long int 是相同类型的不同写法）的别名。 typedef unsigned long int pthread_t; 新线程执行函数的声明为 void *(*start_routine)(void *)，其入参和返回值都是 void * 指针。我们可以将传递给线程函数的参数包装为结构体，并将其指针作为入参，再在函数内部处理；同理我们可以在线程函数内部将要返回的状态码和返回值包装为结构体，并将指针作为返回值 return。 线程终止 线程终止有以下几种方法： 线程函数执行return语句； 线程函数内部调用 pthread_exit 函数； 其他线程调用 pthread_cancel 函数。 线程终止函数为 pthread_exit： #include <pthread.h> /** * 结束关闭调用该方法的线程，并返回一个内存指针用于存放结果 * void *retval: 要返回给其它线程的数据 */ void pthread_exit(void *retval); 当某个线程调用pthread_exit方法后，该线程会被关闭（相当于return）。线程可以通过retval向其它线程传递信息，retval指向的区域不可以放在线程函数的栈内。其他线程（例如主线程）如果需要获得这个返回值，需要调用pthread_join方法。 #include <pthread.h> /** * 等待指定线程结束，获取目标线程的返回值，并在目标线程结束后回收它的资源 * * pthread_t thread: 指定线程ID * void **retval: 这是一个可选参数，用于接…

进程通信
type: item
date: 2024-08-31T13:06:00+08:00
description: 进程通信是指在不同进程之间传输数据或交换信息的过程。由于进程各自拥有独立的内存地址空间，它们之间的数据不能直接通过内存地址来访问，因此需要通过特定的通信机制来实现信息交换。 进程通信的必要性 资源共享：多个进程可能需要访问共享资源，如共享内存、文件等，进程通信是实现资源共享的关键。 协同工作：多个进程可能需要协同完成某项任务，进程间的通信是协调它们工作的重要手段。 状态同步：一个进程的状态变化可能需要通知其他进程，以便它们作出相应的调整。 Linux 中进程间通信方式 只举例管道、消息队列等代码。同时不推荐进行双向操作（双工），因为比较古老，容易出现问题，比如出现混乱，一方发送的消息自己也接收。如果要实现全双工，可以使用两条消息队列（管道）分别负责两个方向的通信。 1. 管道（Pipes） 匿名管道（Anonymous Pipes）：仅存在于内存中的管道，用于具有亲缘关系的进程间通信（如父子进程）。它是一种半双工通信方式，数据只能单向流动。通过pipe()系统调用创建。 #include <sys/types.h> #include <sys/wait.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> int main(int argc, char const *argv[]) { /* 0 ： 读 1 ： 写 */ int pipefd[2]; // 将程序传递进来的第一个命令行参数 通过管道传输给子进程 if (argc != 2) { fprintf(stderr, "%s请填写需要传递的信息\n", argv[0]); exit(EXIT_FAILURE); } // 创建管道 if(pipe(pipefd) == -1) { perror("创建管道失败!\n"); exit(EXIT_FAILURE); } // 复制父子进程 pid_t cpid = fork(); if (cpid == -1) { perror("fork"); exit(EXIT_FAILURE); } else if (cpid == 0) { // 子进程：读取管道的数据，打印到控制台 close(pipefd[1]); char str[100] = {0}; sprintf(str, "子进程（%d）接收信息\n", getpid()); write(STDOUT_FILENO, str, sizeof(str)); char buf; while(read(pipefd[0], &buf, 1) > 0) { write(STDOUT_FILENO, &buf, 1); } write(STDOUT_FILENO, "\n", 1); close(pipefd[0]); exit(EXIT_SUCCESS); } else { // 父进程：写入管道数据，提供给子进程读 // 单向通信，一个写，一个读 => 先关闭读，再写 close(pipefd[0]); // 将数据写入到管道中 printf("父进程（%d）向子进程传递信息\n", getpid()); write(pipefd[1], argv[1], strlen(argv[1])); close(pipefd[1]); waitpid(cpid, NULL, 0); exit(EXIT_SUCCESS); } return 0; } 命名管道（Named Pipes或FIFOs）：也称为FIFO（First In First Out），它在文件系统中以文件的形式存在，因此允许任意进程间进行通信。通过mkfifo()或mknod()系统调用创建。命名管道具有持久性，直到被显式删除。代码如下，这里为收发两端的代码，需要打开两个控制台进行测试。 fifo_read.c #include <fcntl.h> #include <errno.h> #include <sys/stat.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #define PIPO_PATH ("/tmp/myfifo") int main(int argc, char const *argv[]) { if (mkfifo(PIPO_PATH, 0664)) { perror("mkfifo"); if (errno != EEXIST) // 文件已存在 { exit(EXIT_FAILURE); } } // 对有名管道的特殊文件 创建fd int fd = open(P…

文件 io
type: item
date: 2024-08-29T21:41:00+08:00
description: 常用的文件I/O函数及其基本用法。 函数用法 IOFILE.c #include <stdio.h> #define FILENAME_IO ("io.txt") #define FILENAME_TX ("text.txt") #define TEXT_PUTS (" litter noob..\n") #define TEXT_PRINTF ("Justin_Wu") #define BUF_MAX_SIZE (100) int main(int argc, char const *argv[]) { /* * char *__restrict __filename： 字符串表示要打开的文件名称 * char *__restrict __modes：访问模式 * （1）r：只读模式 如果没有文件会报错 * （2）w：只写模式 如果文件存在清空文件 如果不存在创建新文件 * （3）a：只追加写模式 如果文件存在末尾追加写 如果不存在创建新文件 * （4）r+：读写模式 文件必须存在 写入是从头一个一个覆盖 * （5）w+：读写模式 如果文件存在清空文件 如果不存在创建新文件 * （6）a+：读追加写模式 如果文件存在末尾追加写 如果不存在创建新文件 * return：FILE * 结构体指针 表示一个文件 * 报错返回 NULL * FILE *fopen (const char *__restrict __filename, const char *__restrict __modes) */ FILE * iofile = fopen((char *)FILENAME_IO, "a+"); if (iofile == NULL) { printf("Not File!\n"); } else { printf("Opened Success!\n"); } /** int fputc (int __c, FILE *__stream); * int __c：ASCII码对应的char * FILE *__stream：打开的一个文件 * return：成功返回对于的char 失败返回EOF * 读写权限记录在fopen方法中的参数 */ int put_result = fputc(97, iofile); if (put_result == EOF) { printf("Writed Word is File!\n"); } else { printf("Writed '%c' !\n", put_result); } /** fputs (const char *__restrict __s, FILE *__restrict __stream); * char *__restrict __s：需要写入的字符串 * FILE *__restrict __stream：需要写入的文件 * return：成功返回非法整数（0， 1） 失败返回EOF * 读写权限记录在fopen方法中的参数 */ int puts_result = fputs((char *)TEXT_PUTS, iofile); if (put_result == EOF) { printf("Writed Word is File!\n"); } else { printf("Writed Word is Success!\n"); } /** fprintf (FILE *__restrict __stream, const char *__restrict __fmt, ...) * FILE *__restrict __stream：需要打开的文件 * const char *__restrict __s：带格式化的长字符串 * ... 可变参数：填入格式化的长字符串 * return：成功返回写入的字符的个数 不包含换行符 失败返回EOF */ int printf_result = fprintf(iofile, "This is a fprintf\n\t\t%s", TEXT_PRINTF); if (printf_result == EOF) { printf("Writed Word is File!\n"); } else { printf("Writed Word is Success!\n"); } /** int fgetc (FILE *__stream); * FILE *__stream：需要打开的文件 * return：成功返回读取到的一个字节 如果失败返回EOF（出错或者文件的末尾） */ FILE * iofile_tx = fopen((char *)FILENAME_TX, "r"); while (fget…

步进电机—半步驱动
type: item
date: 2024-08-28T23:35:00+08:00
description: 环境配置 MCU：STM32F103RC 步进驱动芯片：BDR6622T IDE：STM32CubeMX + Keil5 步进芯片原理图 & MX 配置 原理图： STM32CubeMX： 实现代码 /* USER CODE BEGIN 0 */ void motor_run(uint8_t direction,uint8_t step){ if(0 == direction){ //正转 switch(step){ case 0: HAL_GPIO_WritePin(AN1_GPIO_Port,AN1_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(BN1_GPIO_Port,BN1_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(AN2_GPIO_Port,AN2_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(BN2_GPIO_Port,BN2_Pin,GPIO_PIN_RESET); break; case 1: HAL_GPIO_WritePin(AN1_GPIO_Port,AN1_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(BN1_GPIO_Port,BN1_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(AN2_GPIO_Port,AN2_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(BN2_GPIO_Port,BN2_Pin,GPIO_PIN_RESET); break; case 2: HAL_GPIO_WritePin(AN1_GPIO_Port,AN1_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(BN1_GPIO_Port,BN1_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(AN2_GPIO_Port,AN2_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(BN2_GPIO_Port,BN2_Pin,GPIO_PIN_SET); break; case 3: HAL_GPIO_WritePin(AN1_GPIO_Port,AN1_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(BN1_GPIO_Port,BN1_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(AN2_GPIO_Port,AN2_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(BN2_GPIO_Port,BN2_Pin,GPIO_PIN_SET); break; } }else{ //反转 switch(step){ case 0: HAL_GPIO_WritePin(AN1_GPIO_Port,AN1_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(BN1_GPIO_Port,BN1_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(AN2_GPIO_Port,AN2_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(BN2_GPIO_Port,BN2_Pin,GPIO_PIN_SET); break; case 1: HAL_GPIO_WritePin(AN1_GPIO_Port,AN1_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(BN1_GPIO_Port,BN1_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(AN2_GPIO_Port,AN2_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(BN2_GPIO_Port,BN2_Pin,GPIO_PIN_SET); break; case 2: HAL_GPIO_WritePin(AN1_GPIO_Port,AN1_Pin,GPIO_PIN_RESET); HAL_GPIO_WritePin(BN1_GPIO_Port,BN1_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(AN2_GPIO_Port,AN2_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(BN2_GPIO_Port,BN2_Pin,GPIO_PIN_RESET); break; case 3: HAL_GPIO_WritePin(AN1_GPIO_Port,AN1_Pin,GPIO_PIN_SET); HAL_GPIO_WritePin(BN1_GPI…

I2C 通信过程代码实现
type: item
date: 2024-08-25T16:23:00+08:00
description: 51 单片机是不支持硬件 I2C 的，所以我们要驱动 I2C 设备，只能通过代码实现软件 I2C，简单来说就是通过 IO 口模拟 I2C 的时序。 代码实现： #include <reg52.h> sbit sda = P0 ^ 1; sbit scl = P0 ^ 2; /* 50kbit/s = 1s 50k周期 = 1周期 = 1/50ks = 20us 10us发送1bit，1ms=100bit 1s = 100kbit */ void delay10us() //@11.0592MHz { unsigned char i; i = 2; while (-- i); } void i2c_start() { scl = 1; sda = 1; delay10us(); //起始信号建立时间，高电平时间持续大于4.7us sda = 0; //SDA拉低，下降沿 delay10us(); //起始信号保持时间 } void i2c_stop() { scl = 1; sda = 0; delay10us(); //停止信号建立时间，SDA高电平时间持续大于4.7us sda = 1; //SDA拉高，上升沿 delay10us(); //总线空闲时间保持 } void i2c_write_bit(unsigned char databit) { scl = 0; //开始发送前，设置scl为0 if (databit == 1) { //设置需要发送的数据 sda = 1; } else { sda = 0; } delay10us(); scl = 1; delay10us(); } void i2c_write_byte(unsigned char datasend) { /* 0x20 = 0b0010 0000 & 0b1000 0000 --> 检测第一位 0x20 = 0b0010 0000 << 1 0b0100 0000 --> 检测下一位 */ int i = 0; for (i = 0; i < 8; i ++) { if (datasend & 0x80) // 判断最高位是否为 1 { i2c_write_bit(1); // 输出高电平 } else { i2c_write_bit(0); // 输出低电平 } datasend <<= 1; //左移一位，for循环8次，直到数据发送完 } } unsigned char i2c_read_bit() { unsigned char databit = 0; scl = 0; //每bit接收前，先设置scl为0 delay10us(); //scl为低电平的时间 scl = 1; //拉高scl开始接收数据 delay10us();//等待数据稳定的时间 if (sda) { databit = 1; } else { databit = 0; } return databit; } unsigned char i2c_read_byte() { unsigned char value = 0; unsigned char i = 0; sda = 1; // 处于空闲状态 for (i = 0; i < 8; i ++) { if (i2c_read_bit() == 1) { value |= 0x01; } else { value |= 0x00; } if (i < 7) // 防止数据丢失 /* 每次向左移动一位以后，如果sda=1的时候就把最后一位置1， sda=0的时候则不用置，因为向左移动就有一个0了 */ value <<= 1; } scl = 0; //接收完成后，设置scl为0 delay10us(); return value; } // 应答信号 void i2c_ack() { scl = 0; sda = 0; //SDA拉低，发出应答信号 delay10us();; scl = 1; delay10us(); scl = 0; } // 等待应答信号 void i2c_wait_ack() { unsigned char time = 0; scl = 0; sda = 1; //sda为高电平释放总线，然后检测应答信号是否被拉低 delay10us(); scl = 1; delay10us(); while (sda) //sda为高电平，就表示没有检查到ACK， { delay10us(); time ++; if (time > 100) break; } scl = 0; delay10us(); } void i2c_nack() { scl = 0; sda = 1; //SDA拉高，发出非应…