本次主讲:CAN、UART、RS485 / RS232、USB、I2C、SPI
比如:
嵌入式 Linux 应用开发本质上不是直接访问寄存器,而是基于内核已经暴露出来的抽象工作。
常见抽象有:
/dev/ttyS*、/dev/i2c-*、/dev/spidev*can0 can1/dev/video*、/dev/ttyUSB*、wlan0用户态程序不是“凭感觉”访问,而是通过固定 API:
open / ioctl / read / writesocket / bind / send / recvtermiosi2c-devspidevlibusb你可以把 CAN 想成:
一条公共发言总线,很多设备都能说话,但大家同时开口时,不是乱成一团,而是“优先级高的人先说”。
普通串口像两个人打电话,一对一。
CAN 像一个会议系统,所有节点都连在同一总线上。
但这个会议系统很聪明:
这就是为什么说 CAN 是非破坏性仲裁。
控制系统很看重三件事:
CAN 恰好都比较强:
因为它是差分信号:
外界同时对两根线造成的干扰,在接收端大概率会被抵消。
因为 CAN 有:
因为优先级高的报文可以先发。
这就是为什么:
CAN ID 不只是“报文编号”,它还是总线仲裁优先级。
ID 越小,优先级越高。
为什么?
因为 CAN 仲裁时是逐位比较的,“0” 会压过 “1”,所以二进制更小的 ID 会赢。
can总线通信速率。
例如:
同一条 CAN 总线上的节点,bitrate 必须一致。
如果一个节点 500k,另一个节点 250k,基本不可能正常通信。
真实 CAN 总线通常在两端各放一个 120 欧电阻。
为什么要加?
因为传输线不是理想导线,信号沿着线传播时会产生反射。 终端电阻的作用是做阻抗匹配,减少反射。
设计思想:
Linux 把 CAN 当成一种“网络接口”来管理,而不是普通字符设备。
所以会看到:
can0而不是:
/dev/can0因为 Linux 希望统一网络类通信接口。
这样做的好处:
所以 CAN 用户态编程很像网络编程:
socket()bind()read()write()ip link show can0 | ||
ip -details link show can0 | ||
ip link set can0 up | ||
ip link set can0 down | ||
candump can0 | ||
cansend can0 123#11223344 | ||
cangen can0 |
#include<stdio.h> // printf, perror#include<string.h> // strcpy#include<unistd.h> // close, read, write#include<sys/socket.h> // socket, bind#include<sys/ioctl.h> // ioctl#include<net/if.h> // struct ifreq#include<linux/can.h> // struct can_frame#include<linux/can/raw.h> // CAN_RAW/* * 文件名:can_send_verbose.c * 编译命令: * gcc can_send_verbose.c -o can_send_verbose * * 功能: * 发送一帧标准 CAN 报文到 can0 */intmain(void){int sockfd; // CAN 套接字文件描述符structifreqifr;// 用于通过接口名获取接口索引structsockaddr_canaddr;// CAN socket 地址结构structcan_frameframe;// CAN 报文结构体/* * 第 1 步:创建一个原始 CAN socket * * PF_CAN : 协议族为 CAN * SOCK_RAW : 原始套接字 * CAN_RAW : 原始 CAN 协议 */ sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW);if (sockfd < 0) { perror("socket");return1; }/* * 第 2 步:指定我们要使用的接口名 * 这里假设接口名是 can0 */strcpy(ifr.ifr_name, "can0");/* * 第 3 步:通过 ioctl 获取 can0 的接口索引 * * 为什么要拿索引? * 因为 bind 的时候不是直接绑定字符串 "can0", * 而是绑定它对应的接口编号。 */if (ioctl(sockfd, SIOCGIFINDEX, &ifr) < 0) { perror("ioctl SIOCGIFINDEX"); close(sockfd);return1; }/* * 第 4 步:设置 CAN 地址结构 * * can_family : 地址族,固定 AF_CAN * can_ifindex : 接口索引,对应 can0 */ addr.can_family = AF_CAN; addr.can_ifindex = ifr.ifr_ifindex;/* * 第 5 步:把 socket 绑定到 can0 * * 绑定后,后续 read/write 都针对 can0 收发 */if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { perror("bind"); close(sockfd);return1; }/* * 第 6 步:构造一帧 CAN 报文 */ frame.can_id = 0x123; // 标准帧 ID = 0x123 frame.can_dlc = 8; // 数据长度为 8 字节/* * 给数据区填入 8 个测试字节 */for (int i = 0; i < 8; i++) { frame.data[i] = i; // 依次写入 0,1,2,3,4,5,6,7 }/* * 第 7 步:发送报文 * * write 的数据对象是整个 struct can_frame */if (write(sockfd, &frame, sizeof(frame)) != sizeof(frame)) { perror("write"); close(sockfd);return1; }/* * 第 8 步:打印提示 */printf("CAN frame sent successfully.\n");/* * 第 9 步:关闭 socket */ close(sockfd);return0;}#include<stdio.h> // printf, perror#include<string.h> // strcpy#include<unistd.h> // close, read#include<sys/socket.h> // socket, bind#include<sys/ioctl.h> // ioctl#include<net/if.h> // struct ifreq#include<linux/can.h> // struct can_frame#include<linux/can/raw.h> // CAN_RAW/* * 文件名:can_recv_verbose.c * 编译: * gcc can_recv_verbose.c -o can_recv_verbose * * 功能: * 阻塞等待接收一帧 CAN 报文 */intmain(void){int sockfd; // CAN 套接字structifreqifr;// 用于获取接口索引structsockaddr_canaddr;// CAN 地址结构structcan_frameframe;// 接收报文缓冲区/* * 创建 CAN 原始 socket */ sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW);if (sockfd < 0) { perror("socket");return1; }/* * 指定要监听的接口名 */strcpy(ifr.ifr_name, "can0");/* * 获取 can0 对应的接口索引 */if (ioctl(sockfd, SIOCGIFINDEX, &ifr) < 0) { perror("ioctl"); close(sockfd);return1; }/* * 设置地址结构 */ addr.can_family = AF_CAN; addr.can_ifindex = ifr.ifr_ifindex;/* * 绑定到 can0 */if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { perror("bind"); close(sockfd);return1; }printf("Waiting for CAN frame on can0...\n");/* * read 会阻塞,直到收到一帧 CAN 报文 */if (read(sockfd, &frame, sizeof(frame)) < 0) { perror("read"); close(sockfd);return1; }/* * 打印接收到的报文 ID 和数据 */printf("Received CAN ID = 0x%X, DLC = %d, DATA =", frame.can_id, frame.can_dlc);for (int i = 0; i < frame.can_dlc; i++) {printf(" %02X", frame.data[i]); }printf("\n"); close(sockfd);return0;}/dev/can0?答:因为 Linux 使用 SocketCAN,把 CAN 设备抽象为网络接口,统一纳入 socket 体系和 netdev 管理框架。
答:因为 CAN 具有总线仲裁、错误检测、自动重发和多节点共享能力,更适合分布式控制。
答:为了阻抗匹配,减少信号反射,保证差分总线的波形质量。
UART 像什么?
像两个人不带节拍器打暗号。 他们事先约定:
于是即使没有时钟线,也能通信。
这就是“异步串口”的核心。
因为它不是同步通信。 它靠的是:
所以 UART 虽然简单,但对参数匹配要求非常高。
8N1 =
一帧一般长这样:
为什么会有起始位?
因为接收方平时不知道什么时候开始来数据。 起始位就像“我开始说了”的信号。
为什么需要停止位?
因为接收方要知道“一帧结束了,可以准备下一帧”。
因为 Linux 的串口、终端、伪终端,本质上都走 tty 子系统。
所以会看到:
/dev/ttyS0/dev/ttyS1/dev/ttyUSB0建立认知:
ttyS* 多数是 SoC 自带 UARTttyUSB* 多数是 USB 转串口termios 可以理解成:
Linux 给 tty 设备的“通信规则配置器”
你通过它告诉系统:
#include<stdio.h> // printf, perror#include<unistd.h> // close, read, write#include<fcntl.h> // open#include<termios.h> // termios 串口配置接口#include<string.h> // strlen/* * 函数名:uart_config * 功能:把一个串口 fd 配置成 115200 8N1 原始模式 */staticintuart_config(int fd){structtermiostty;// 保存串口参数的结构体/* * 先读取当前串口参数 * 为什么先读? * 因为通常是在当前基础上修改,避免把某些字段意外清掉。 */if (tcgetattr(fd, &tty) != 0) { perror("tcgetattr");return-1; }/* * 设置输入波特率为 115200 */ cfsetispeed(&tty, B115200);/* * 设置输出波特率为 115200 */ cfsetospeed(&tty, B115200);/* * 先清除数据位掩码 * CSIZE 代表数据位相关配置位 */ tty.c_cflag &= ~CSIZE;/* * 设为 8 位数据位 */ tty.c_cflag |= CS8;/* * 关闭校验位 * PARENB = parity enable */ tty.c_cflag &= ~PARENB;/* * 设置为 1 个停止位 * 如果要 2 个停止位,需要打开 CSTOPB */ tty.c_cflag &= ~CSTOPB;/* * 关闭硬件流控 RTS/CTS */ tty.c_cflag &= ~CRTSCTS;/* * CLOCAL:忽略调制解调器控制线 * CREAD :使能接收 */ tty.c_cflag |= CLOCAL | CREAD;/* * 输入模式设置为原始 * 不做特殊处理 */ tty.c_iflag = 0;/* * 输出模式设置为原始 */ tty.c_oflag = 0;/* * 本地模式设置为原始 * 不做行编辑、不做回显 */ tty.c_lflag = 0;/* * VMIN = 1 * 表示 read 至少等到 1 个字节才返回 */ tty.c_cc[VMIN] = 1;/* * VTIME = 1 * 表示超时单位为 0.1 秒,这里是 0.1 秒 */ tty.c_cc[VTIME] = 1;/* * 立即生效配置 */if (tcsetattr(fd, TCSANOW, &tty) != 0) { perror("tcsetattr");return-1; }return0;}intmain(void){/* * 打开串口设备 * * O_RDWR : 可读可写 * O_NOCTTY : 不把该串口当作控制终端 */int fd = open("/dev/ttyS1", O_RDWR | O_NOCTTY);if (fd < 0) { perror("open");return1; }/* * 配置串口 */if (uart_config(fd) != 0) { close(fd);return1; }/* * 要发送的字符串 */constchar *msg = "hello uart\r\n";/* * 发送数据 */if (write(fd, msg, strlen(msg)) < 0) { perror("write"); close(fd);return1; }printf("UART configured and data sent.\n");/* * 关闭串口 */ close(fd);return0;}#include<stdio.h> // printf, perror#include<unistd.h> // close, read#include<fcntl.h> // openintmain(void){/* * 打开串口 */int fd = open("/dev/ttyS1", O_RDWR | O_NOCTTY);if (fd < 0) { perror("open");return1; }/* * 接收缓冲区 * 多留一个字节,用于字符串结尾 '\0' */char buf[128];/* * 阻塞读取串口数据 * 实际是否阻塞取决于 termios 配置 */int n = read(fd, buf, sizeof(buf) - 1);if (n < 0) { perror("read"); close(fd);return1; }/* * 手工补 '\0',便于按字符串打印 */ buf[n] = '\0';/* * 打印接收到的内容 */printf("recv: %s\n", buf); close(fd);return0;}read/write因为实际项目中,串口数据几乎总是协议数据,而不是“人类一句话”。
所以必须考虑:
这就是为什么真正的串口项目里,通常会有:
答:因为没有单独时钟线,双方依赖预先约定的波特率和帧格式完成同步。
答:8 位数据位、无校验、1 位停止位。
答:用于配置 tty 设备的通信参数,如波特率、数据位、停止位、校验和读写行为。
很多人把 UART、TTL、RS232、RS485 混在一起讲。
是异步串行通信逻辑 / 控制器层
是逻辑电平形式通常 3.3V 或 5V
是单端串口电气标准
是差分串口电气标准
一句话总结:
UART 解决“怎么按位发”,RS232/RS485 解决“电气上怎么把这位发出去”。
RS232 像传统的一对一电话线通信。
特点:
它不是为多节点总线设计的。
所以它更像“设备 A 和设备 B 点对点串口通信”。
RS485 像工业现场的一条公共差分通信线。
特点:
为什么工业现场喜欢它?
因为工厂很吵,电机很多,继电器很多,地线环境也复杂。 普通单端信号容易受干扰,差分更稳。
半双工 RS485 的核心难点不是 read/write,而是:
什么时候发,什么时候收,什么时候切换方向。
如果方向切换太早:
这就是为什么 tcdrain() 很关键。
因为:
于是 Modbus RTU 很自然成为常见组合:
#include<stdio.h> // printf, perror#include<string.h> // memset#include<unistd.h> // close#include<fcntl.h> // open#include<sys/ioctl.h> // ioctl#include<linux/serial.h> // struct serial_rs485/* * 文件名:rs485_verbose.c * 编译: * gcc rs485_verbose.c -o rs485_verbose * * 作用: * 尝试把一个 tty 串口配置成 RS485 模式 * * 注意: * 该示例是否成功,取决于内核驱动是否支持 TIOCSRS485 */intmain(void){/* * 打开底层串口 * 注意:RS485 在 Linux 应用层常常仍然是从 tty 进入 */int fd = open("/dev/ttyS1", O_RDWR | O_NOCTTY);if (fd < 0) { perror("open");return1; }/* * 定义 RS485 配置结构体 */structserial_rs485rs485;/* * 清零,避免脏数据 */memset(&rs485, 0, sizeof(rs485));/* * 启用 RS485 模式 */ rs485.flags |= SER_RS485_ENABLED;/* * 发送时拉高 RTS * 某些平台用这个来控制发送方向 */ rs485.flags |= SER_RS485_RTS_ON_SEND;/* * 发送结束后,不保持发送状态 * 即准备回到接收方向 */ rs485.flags &= ~SER_RS485_RTS_AFTER_SEND;/* * 把 RS485 配置下发给驱动 */if (ioctl(fd, TIOCSRS485, &rs485) < 0) { perror("TIOCSRS485"); close(fd);return1; }/* * 如果成功,说明驱动接受了 RS485 模式配置 * 真正业务中,还需要: * 1. termios 配串口参数 * 2. write() 发送协议帧 * 3. tcdrain() 等待真正发完 * 4. read() 读响应 * 5. 校验 CRC16 * 6. 超时重试 */printf("RS485 mode configured successfully.\n"); close(fd);return0;}答:UART 是异步串行控制逻辑,RS485 是差分电气标准,二者常组合使用。
答:因为它采用差分传输,对共模噪声不敏感。
答:因为多个节点共享同一对线,常通过方向控制在发送和接收之间切换。
答:因为 RS485 适合长距离多节点抗干扰传输,而 Modbus RTU 提供统一工业协议格式。
USB 不像 UART / I2C / SPI 那样只是“一个简单外设接口”。
USB 更像一个完整的小系统:
这就是 USB 的核心味道。
因为 USB 设计里,主机负责:
设备不会自己突然乱发。
记住一句:
USB 是 host-driven bus。
枚举可以理解为:
主机插上一个 USB 设备后,先问它:你是谁?你有哪些功能?我该怎么跟你说话?
设备就通过各种描述符回答:
然后 Linux 再决定绑定哪个驱动。
因为 USB 只是底层总线。
真正给应用用的,通常是驱动绑定后的抽象。
例如:
/dev/video0/dev/ttyUSB0wlan0/dev/sda这一步必须吃透,因为这正是 Linux 应用开发和底层协议开发的边界。
因为现实项目中,如果已有成熟类驱动,应用通常直接使用更高层抽象:
只有在没有合适类驱动,或设备协议非常定制化时,才会直接用 libusb。
#include<stdio.h> // printf, fprintf#include<libusb-1.0/libusb.h> // libusb API/* * 文件名:usb_libusb_verbose.c * 编译: * gcc usb_libusb_verbose.c -lusb-1.0 -o usb_libusb_verbose * * 作用: * 演示 libusb 的最小初始化流程 */intmain(void){/* * libusb 上下文指针 * 可理解为 libusb 运行环境句柄 */ libusb_context *ctx = NULL;/* * 返回值 * libusb 大多数函数返回 0 表示成功,负值表示错误 */int ret;/* * 初始化 libusb * 如果成功,ctx 会被设置 */ ret = libusb_init(&ctx);if (ret < 0) {fprintf(stderr, "libusb_init failed\n");return1; }/* * 到这里说明 libusb 初始化成功 */printf("libusb initialized successfully.\n");/* * 实际项目中,后续常见步骤包括: * * 1. 枚举设备列表 * 2. 根据 VID/PID 查找目标设备 * 3. 打开设备句柄 * 4. claim interface * 5. 发 control/bulk/interrupt 传输 * 6. 关闭设备 *//* * 退出 libusb,释放上下文 */ libusb_exit(ctx);return0;}答:因为设备的枚举、配置和通信调度都由主机发起并管理。
答:用于唯一标识 USB 设备型号,是驱动匹配的重要依据。
/dev/videoX?答:因为内核中的 UVC 类驱动绑定后,把它暴露为 V4L2 视频设备。
答:当没有现成类驱动,或者设备协议需要用户态直接访问 USB 端点时。
I2C 像什么?
像一条公共走廊,很多房间共用这条走廊,但每个房间门口有门牌号。
主机想找谁,就先喊门牌号:
谁的地址对,谁就应答。
所以 I2C 的核心味道是:
因为它把控制简化到了极致:
时钟由主机提供。 数据在线上双向流动。
这也是它很适合板级低速外设的原因:
因为主机不是在和空气说话,它需要知道“有人应不应”。
所以每个关键字节后,会有一位 ACK/NACK:
可以把它想成“点名答到”。
这是 I2C 最容易被问的点之一。
I2C 的线通常是开漏结构:
所以必须靠上拉电阻把线恢复成高电平。
如果没有上拉:
因为很多 I2C 芯片内部是寄存器模型。
主机真正想做的是:
“我要读你 0x10 寄存器里的值。”
所以必须先告诉设备寄存器地址,然后再读数据。
这就是为什么 I2C 读寄存器常表现为:
#include<stdio.h> // printf, perror#include<stdint.h> // uint8_t#include<fcntl.h> // open#include<unistd.h> // close, read, write#include<sys/ioctl.h> // ioctl#include<linux/i2c-dev.h> // I2C_SLAVE/* * 文件名:i2c_verbose.c * 编译: * gcc i2c_verbose.c -o i2c_verbose * * 作用: * 演示最简单的“先写寄存器地址,再读 1 字节数据” */intmain(void){/* * 打开 I2C 总线设备节点 * 这里示例使用 /dev/i2c-1 */int fd = open("/dev/i2c-1", O_RDWR);if (fd < 0) { perror("open");return1; }/* * 指定目标从设备地址 * 这里用 0x50 作为示例 */int addr = 0x50;/* * 告诉内核:接下来这个 fd 要访问地址为 0x50 的 I2C 从设备 */if (ioctl(fd, I2C_SLAVE, addr) < 0) { perror("ioctl I2C_SLAVE"); close(fd);return1; }/* * 假设我们想读设备内部 0x00 这个寄存器 */uint8_t reg = 0x00;/* * 先把寄存器地址写给设备 * 注意:这里不是写数据,而是“告诉设备我要读哪个寄存器” */if (write(fd, ®, 1) != 1) { perror("write reg"); close(fd);return1; }/* * 用于保存读回来的数据 */uint8_t data = 0;/* * 从该寄存器读 1 个字节 */if (read(fd, &data, 1) != 1) { perror("read data"); close(fd);return1; }/* * 打印结果 */printf("Read value: 0x%02X\n", data); close(fd);return0;}#include<stdio.h> // printf, perror#include<stdint.h> // uint8_t#include<fcntl.h> // open#include<unistd.h> // close#include<sys/ioctl.h> // ioctl#include<linux/i2c-dev.h> // i2c-dev 接口#include<linux/i2c.h> // struct i2c_msg/* * 文件名:i2c_rdwr_verbose.c * 编译: * gcc i2c_rdwr_verbose.c -o i2c_rdwr_verbose * * 作用: * 使用 I2C_RDWR 进行标准组合事务 */intmain(void){/* * 打开 I2C 总线节点 */int fd = open("/dev/i2c-1", O_RDWR);if (fd < 0) { perror("open");return1; }/* * 要访问的寄存器地址 */uint8_t reg = 0x00;/* * 保存读回来的数据 */uint8_t data = 0;/* * msgs[0]:写消息 * 用来把寄存器地址发给从设备 */structi2c_msgmsgs[2]; msgs[0].addr = 0x50; // 从设备地址 msgs[0].flags = 0; // 0 表示写 msgs[0].len = 1; // 写 1 字节 msgs[0].buf = ® // 数据内容是寄存器地址/* * msgs[1]:读消息 * 从同一个从设备读 1 字节数据 */ msgs[1].addr = 0x50; // 同一从设备 msgs[1].flags = I2C_M_RD; // 表示读 msgs[1].len = 1; // 读 1 字节 msgs[1].buf = &data; // 读到 data/* * ioctl 所需封装结构 */structi2c_rdwr_ioctl_dataioctl_data; ioctl_data.msgs = msgs; // 指向消息数组 ioctl_data.nmsgs = 2; // 一共 2 个消息/* * 执行组合事务 */if (ioctl(fd, I2C_RDWR, &ioctl_data) < 0) { perror("ioctl I2C_RDWR"); close(fd);return1; }/* * 打印读到的数据 */printf("Read value: 0x%02X\n", data); close(fd);return0;}答:因为 SDA、SCL 常为开漏结构,设备只能主动拉低,必须依赖上拉恢复高电平。
答:主机发出了地址和方向位,但没有从设备应答。
答:因为它低速、线少、总线共享,特别适合传感器、EEPROM、RTC 等按寄存器访问的器件。
SPI 像什么?
像主设备手里拿着节拍器,同时点名某一个从设备讲话。
它的关键味道:
因为 SPI 更直接:
代价就是:
主设备提供的时钟线。 没有它,从设备不知道什么时候采样。
Master Out Slave In 主设备发给从设备的数据线。
Master In Slave Out 从设备回给主设备的数据线。
Chip Select 片选信号,用于指明“我现在在跟谁说话”。
很多人死记 mode 0/1/2/3,容易乱。
可以这样理解:
决定时钟空闲时是高还是低。
决定在第几个边沿采样。
于是四种组合就出来了。
真正工程里你通常不是自己推,而是:
这个问题特别有面试价值。
很多人以为:
“SPI 控制器驱动起来了,就应该有 /dev/spidev0.0。”
这是错的。
真正关系是:
所以:
控制器存在 ≠ 用户态泛化节点一定存在
#include<stdio.h> // printf, perror#include<stdint.h> // uint8_t, uint32_t#include<fcntl.h> // open#include<unistd.h> // close#include<sys/ioctl.h> // ioctl#include<linux/spi/spidev.h> // SPI_IOC_* 宏定义/* * 文件名:spi_config_verbose.c * 编译: * gcc spi_config_verbose.c -o spi_config_verbose * * 作用: * 配置一个 spidev 设备的模式、字宽和时钟 */intmain(void){/* * 打开 spidev 设备节点 * 注意:只有系统已经导出 spidev 时,这里才会成功 */int fd = open("/dev/spidev0.0", O_RDWR);if (fd < 0) { perror("open");return1; }/* * SPI_MODE_0: * CPOL = 0, CPHA = 0 */uint8_t mode = SPI_MODE_0;/* * 每个数据单元 8 位 */uint8_t bits = 8;/* * SPI 时钟 1MHz */uint32_t speed = 1000000;/* * 设置 SPI 模式 */if (ioctl(fd, SPI_IOC_WR_MODE, &mode) < 0) { perror("SPI_IOC_WR_MODE"); close(fd);return1; }/* * 设置每字数据位数 */if (ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0) { perror("SPI_IOC_WR_BITS_PER_WORD"); close(fd);return1; }/* * 设置最大时钟频率 */if (ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed) < 0) { perror("SPI_IOC_WR_MAX_SPEED_HZ"); close(fd);return1; }printf("SPI configured successfully.\n"); close(fd);return0;}#include<stdio.h> // printf, perror#include<stdint.h> // uint8_t#include<fcntl.h> // open#include<unistd.h> // close#include<sys/ioctl.h> // ioctl#include<linux/spi/spidev.h> // spi_ioc_transfer#include<string.h> // memset/* * 文件名:spi_transfer_verbose.c * 编译: * gcc spi_transfer_verbose.c -o spi_transfer_verbose * * 作用: * 演示一次最小 SPI 传输 */intmain(void){/* * 打开 SPI 设备节点 */int fd = open("/dev/spidev0.0", O_RDWR);if (fd < 0) { perror("open");return1; }/* * tx 为要发出去的数据 * 这里用 0x9F 作为示例命令(很多 SPI Flash 用它读 JEDEC ID) */uint8_t tx[3] = {0x9F, 0x00, 0x00};/* * rx 用于接收返回数据 * 先清零 */uint8_t rx[3] = {0};/* * 描述一次 SPI 传输的结构体 */structspi_ioc_transfertr;/* * 清零,避免未初始化字段带来问题 */memset(&tr, 0, sizeof(tr));/* * 发送缓冲区地址 * 注意这里传的是用户空间缓冲区指针 */ tr.tx_buf = (unsignedlong)tx;/* * 接收缓冲区地址 */ tr.rx_buf = (unsignedlong)rx;/* * 一次传输的长度:3 字节 */ tr.len = 3;/* * 本次传输使用的时钟频率 */ tr.speed_hz = 1000000;/* * 每个数据单元 8 位 */ tr.bits_per_word = 8;/* * 执行一次 SPI 传输 * * SPI_IOC_MESSAGE(1) 表示发送 1 个 transfer 结构 */if (ioctl(fd, SPI_IOC_MESSAGE(1), &tr) < 0) { perror("SPI_IOC_MESSAGE"); close(fd);return1; }/* * 打印接收到的数据 */printf("RX: %02X %02X %02X\n", rx[0], rx[1], rx[2]); close(fd);return0;}答:因为 SPI 协议更简单,没有地址仲裁和 ACK 开销,且是同步时钟、独立收发路径。
答:因为主设备可能连接多个从设备,需要用片选信号决定当前和谁通信。
答:因为控制器驱动和用户态泛化从设备节点不是一回事,是否出现 spidev 取决于设备树和驱动绑定。