1. Linux下四大通信协议概述
在嵌入式Linux开发中,SPI、I2C、UART和CAN是四种最常用的硬件通信协议。它们各自具有独特的特点和适用场景:
- SPI(Serial Peripheral Interface):同步串行通信协议,采用主从架构,四线制(MOSI/MISO/SCLK/CS),支持全双工通信,最高速率可达数十MHz
- I2C(Inter-Integrated Circuit):两线制同步串行总线(SDA/SCL),支持多主多从,标准模式100kbps,快速模式400kbps
- UART(Universal Asynchronous Receiver/Transmitter):异步串行通信,只需TX/RX两根线,无时钟信号,依赖波特率同步
- CAN(Controller Area Network):差分信号总线,具有强大的错误检测和处理机制,广泛应用于汽车和工业领域
在Linux内核中,这些协议都有对应的驱动框架实现。开发者既可以使用内核提供的标准接口,也可以根据具体硬件特性编写定制驱动。
2. SPI协议深度解析与编码实践
2.1 SPI硬件连接与工作原理
SPI总线包含以下信号线:
- SCLK:串行时钟,由主机产生
- MOSI:主机输出,从机输入
- MISO:主机输入,从机输出
- SS/CS:片选信号(低电平有效)
SPI支持四种工作模式,由CPOL(时钟极性)和CPHA(时钟相位)组合决定:
- 模式0:CPOL=0,CPHA=0
- 模式1:CPOL=0,CPHA=1
- 模式2:CPOL=1,CPHA=0
- 模式3:CPOL=1,CPHA=1
2.2 Linux SPI驱动框架
Linux内核提供了完善的SPI子系统,主要包含以下核心结构体:
c复制struct spi_device {
struct device dev;
struct spi_master *master;
u32 max_speed_hz;
u8 chip_select;
u8 bits_per_word;
u16 mode;
// ...
};
struct spi_driver {
const struct spi_device_id *id_table;
int (*probe)(struct spi_device *spi);
int (*remove)(struct spi_device *spi);
struct device_driver driver;
};
2.3 SPI设备驱动开发实例
下面是一个完整的SPI设备驱动示例:
c复制#include <linux/module.h>
#include <linux/spi/spi.h>
#define DRIVER_NAME "my_spi_device"
static int my_spi_probe(struct spi_device *spi)
{
int ret;
u8 tx_buf[2] = {0x01, 0x02};
u8 rx_buf[2] = {0};
struct spi_transfer tr = {
.tx_buf = tx_buf,
.rx_buf = rx_buf,
.len = 2,
};
// 配置SPI参数
spi->mode = SPI_MODE_0;
spi->bits_per_word = 8;
ret = spi_setup(spi);
if (ret < 0) {
dev_err(&spi->dev, "SPI setup failed\n");
return ret;
}
// 执行SPI传输
ret = spi_sync_transfer(spi, &tr, 1);
if (ret < 0) {
dev_err(&spi->dev, "SPI transfer failed\n");
return ret;
}
dev_info(&spi->dev, "Received data: 0x%02x 0x%02x\n",
rx_buf[0], rx_buf[1]);
return 0;
}
static const struct of_device_id my_spi_of_match[] = {
{ .compatible = "vendor,my-spi-device" },
{}
};
MODULE_DEVICE_TABLE(of, my_spi_of_match);
static struct spi_driver my_spi_driver = {
.driver = {
.name = DRIVER_NAME,
.of_match_table = my_spi_of_match,
},
.probe = my_spi_probe,
};
module_spi_driver(my_spi_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
2.4 SPI用户空间访问
除了内核驱动,Linux还提供了通过用户空间访问SPI设备的接口:
bash复制# 加载SPI设备
echo "my_spi_device 0 0" > /sys/bus/spi/devices/spi0.0/driver/unbind
echo "my_spi_device 0 0" > /sys/bus/spi/devices/spi0.0/driver/bind
# 使用spidev工具
spidev_test -D /dev/spidev0.0 -s 1000000 -p "\x01\x02"
3. I2C协议实现详解
3.1 I2C总线特性
I2C总线的主要特点:
- 两线制:SDA(数据线)和SCL(时钟线)
- 支持多主多从架构
- 7位或10位设备地址
- 标准模式(100kbps)、快速模式(400kbps)、高速模式(3.4Mbps)
- 开漏输出,需要上拉电阻(通常4.7kΩ)
3.2 Linux I2C子系统
Linux I2C核心提供了以下关键结构体:
c复制struct i2c_adapter {
struct module *owner;
const struct i2c_algorithm *algo;
// ...
};
struct i2c_client {
unsigned short flags;
unsigned short addr;
char name[I2C_NAME_SIZE];
struct i2c_adapter *adapter;
// ...
};
struct i2c_driver {
int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
int (*remove)(struct i2c_client *client);
const struct i2c_device_id *id_table;
struct device_driver driver;
};
3.3 I2C设备驱动开发
完整I2C设备驱动示例:
c复制#include <linux/module.h>
#include <linux/i2c.h>
#define DRIVER_NAME "my_i2c_device"
static int my_i2c_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
int ret;
u8 reg = 0x00;
u8 val;
// 读取寄存器
ret = i2c_smbus_read_byte_data(client, reg);
if (ret < 0) {
dev_err(&client->dev, "Failed to read register\n");
return ret;
}
val = ret;
// 写入寄存器
ret = i2c_smbus_write_byte_data(client, reg, val | 0x01);
if (ret < 0) {
dev_err(&client->dev, "Failed to write register\n");
return ret;
}
dev_info(&client->dev, "Device initialized\n");
return 0;
}
static const struct i2c_device_id my_i2c_id[] = {
{ "my_i2c_device", 0 },
{}
};
MODULE_DEVICE_TABLE(i2c, my_i2c_id);
static const struct of_device_id my_i2c_of_match[] = {
{ .compatible = "vendor,my-i2c-device" },
{}
};
MODULE_DEVICE_TABLE(of, my_i2c_of_match);
static struct i2c_driver my_i2c_driver = {
.driver = {
.name = DRIVER_NAME,
.of_match_table = my_i2c_of_match,
},
.probe = my_i2c_probe,
.id_table = my_i2c_id,
};
module_i2c_driver(my_i2c_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
3.4 I2C用户空间工具
Linux提供了i2c-tools工具包用于用户空间访问:
bash复制# 扫描I2C总线上的设备
i2cdetect -y 1
# 读取寄存器
i2cget -y 1 0x50 0x00
# 写入寄存器
i2cset -y 1 0x50 0x00 0x55
4. UART串口通信实现
4.1 UART硬件特性
UART关键参数:
- 波特率:常见有9600, 19200, 38400, 57600, 115200等
- 数据位:5-9位(通常8位)
- 停止位:1或2位
- 校验位:无、奇校验、偶校验
- 流控:硬件流控(RTS/CTS)、软件流控(XON/XOFF)
4.2 Linux TTY子系统
Linux将UART设备抽象为TTY设备,主要结构体:
c复制struct uart_driver {
struct module *owner;
const char *driver_name;
int major;
int minor;
struct tty_driver *tty_driver;
// ...
};
struct uart_port {
struct uart_state *state;
unsigned long iobase;
unsigned int irq;
unsigned int uartclk;
// ...
};
4.3 UART驱动开发
串口驱动通常已经由内核提供,开发者主要关注应用层编程:
c复制#include <stdio.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
int main()
{
int fd;
struct termios options;
// 打开串口设备
fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY);
if (fd < 0) {
perror("open serial port failed");
return -1;
}
// 配置串口参数
tcgetattr(fd, &options);
cfsetispeed(&options, B115200);
cfsetospeed(&options, B115200);
options.c_cflag |= (CLOCAL | CREAD);
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
tcsetattr(fd, TCSANOW, &options);
// 发送数据
write(fd, "Hello UART!\n", 12);
// 接收数据
char buf[32];
int n = read(fd, buf, sizeof(buf));
if (n > 0) {
printf("Received: %.*s", n, buf);
}
close(fd);
return 0;
}
4.4 串口调试工具
Linux下常用的串口工具:
bash复制# 使用minicom
minicom -D /dev/ttyS0 -b 115200
# 使用screen
screen /dev/ttyS0 115200
# 发送文件
sz <filename> > /dev/ttyS0 < /dev/ttyS0
# 接收文件
rz < /dev/ttyS0 > /dev/ttyS0
5. CAN总线通信实现
5.1 CAN协议特点
CAN总线关键特性:
- 差分信号(CAN_H/CAN_L),抗干扰能力强
- 多主架构,总线仲裁机制
- 报文ID标识优先级(11位或29位)
- 错误检测与处理机制
- 最高1Mbps传输速率(40米内)
5.2 Linux SocketCAN框架
Linux通过SocketCAN子系统提供CAN支持:
c复制#include <linux/can.h>
#include <linux/can/raw.h>
int setup_can_socket(const char *ifname)
{
int s;
struct sockaddr_can addr;
struct ifreq ifr;
// 创建CAN套接字
s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if (s < 0) {
perror("socket");
return -1;
}
// 绑定到指定CAN接口
strcpy(ifr.ifr_name, ifname);
ioctl(s, SIOCGIFINDEX, &ifr);
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind");
close(s);
return -1;
}
return s;
}
5.3 CAN数据收发示例
c复制int can_send_frame(int sock, canid_t id, const void *data, size_t len)
{
struct can_frame frame;
if (len > CAN_MAX_DLEN) {
fprintf(stderr, "Data too long\n");
return -1;
}
frame.can_id = id;
frame.can_dlc = len;
memcpy(frame.data, data, len);
if (write(sock, &frame, sizeof(frame)) != sizeof(frame)) {
perror("write");
return -1;
}
return 0;
}
int can_receive_frame(int sock, struct can_frame *frame)
{
int nbytes;
nbytes = read(sock, frame, sizeof(*frame));
if (nbytes < 0) {
perror("read");
return -1;
}
if (nbytes != sizeof(*frame)) {
fprintf(stderr, "Incomplete CAN frame\n");
return -1;
}
return 0;
}
5.4 CAN工具集
Linux提供了一系列CAN工具:
bash复制# 设置CAN接口参数
ip link set can0 type can bitrate 500000
ip link set can0 up
# 查看CAN接口状态
ip -details link show can0
# 使用cansend发送帧
cansend can0 123#1122334455667788
# 使用candump接收帧
candump can0
# 使用canplayer回放日志
canplayer -I can_log.log
6. 协议对比与选型指南
6.1 四大协议关键参数对比
| 特性 | SPI | I2C | UART | CAN |
|---|---|---|---|---|
| 线数 | 4+ (CS线) | 2 | 2 (3带流控) | 2 (差分对) |
| 速度 | 可达50MHz+ | 100k-3.4Mbps | 通常<3Mbps | 可达1Mbps |
| 通信方式 | 全双工 | 半双工 | 全双工 | 半双工 |
| 拓扑结构 | 主从 | 多主多从 | 点对点 | 多主 |
| 同步方式 | 同步 | 同步 | 异步 | 同步 |
| 错误检测 | 无 | ACK/NACK | 奇偶校验 | CRC+ACK |
| 传输距离 | <1m | <1m | <15m | 可达1km |
| 典型应用 | 高速外设 | 低速传感器 | 调试接口 | 汽车/工业 |
6.2 协议选型建议
- 需要高速数据传输:选择SPI,特别是对于显示屏、存储器等需要高速数据交换的设备
- 设备数量多且速度要求不高:选择I2C,适合连接多个传感器或外围芯片
- 简单点对点通信:使用UART,特别是与PC或其他微控制器的通信
- 工业或汽车环境:必须使用CAN总线,其可靠性和抗干扰能力最适合严苛环境
- 长距离通信:考虑使用UART转RS-485或直接使用CAN总线
6.3 混合使用场景
在实际项目中,经常需要组合使用多种协议:
- 主控通过SPI连接高速设备(如存储器)
- 通过I2C连接多个传感器
- 通过UART提供调试接口
- 通过CAN与其它节点通信
例如,一个典型的汽车ECU可能:
- 使用CAN与整车网络通信
- 使用SPI连接高速ADC采集传感器数据
- 使用I2C连接温度传感器
- 使用UART连接诊断接口
7. 调试技巧与常见问题
7.1 通用调试方法
- 逻辑分析仪:使用Saleae等逻辑分析仪捕获总线信号
- 示波器:检查信号质量和时序
- 内核日志:
dmesg查看驱动加载和错误信息 - sysfs接口:通过
/sys/class/下的文件节点查询设备状态
7.2 协议特定问题
SPI常见问题:
- 模式不匹配(CPOL/CPHA设置错误)
- 片选信号问题(极性或时序不正确)
- 时钟速度过高导致信号失真
I2C常见问题:
- 上拉电阻不合适(通常4.7kΩ)
- 地址冲突(确保每个设备地址唯一)
- 总线锁死(需要复位I2C控制器)
UART常见问题:
- 波特率不匹配(两端必须完全相同)
- 流控配置错误
- 线路干扰(长距离时需使用RS-232/RS-485转换)
CAN常见问题:
- 终端电阻缺失(总线两端需120Ω电阻)
- 波特率配置不一致
- 过滤器设置不当导致收不到报文
7.3 性能优化建议
-
SPI优化:
- 使用DMA传输减少CPU开销
- 合理设置时钟分频
- 使用双缓冲技术提高吞吐量
-
I2C优化:
- 启用快速模式(400kHz)或高速模式(3.4MHz)
- 合并多次读写为单次传输
- 减少总线上的设备数量
-
UART优化:
- 使用硬件流控避免数据丢失
- 增大缓冲区减少中断频率
- 考虑使用DMA(如果支持)
-
CAN优化:
- 合理分配报文ID优先级
- 使用CAN FD提高带宽(如果硬件支持)
- 优化过滤器设置减少不必要的中断
8. 进阶主题与扩展阅读
8.1 设备树配置
现代Linux内核使用设备树描述硬件,以下是各协议的典型设备树节点:
SPI设备节点:
dts复制&spi0 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&spi0_pins>;
flash@0 {
compatible = "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <50000000>;
};
};
I2C设备节点:
dts复制&i2c1 {
status = "okay";
clock-frequency = <400000>;
sensor@48 {
compatible = "ti,tmp102";
reg = <0x48>;
};
};
UART设备节点:
dts复制&uart0 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart0_pins>;
};
CAN控制器节点:
dts复制&can0 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&can0_pins>;
bitrate = <500000>;
};
8.2 用户空间API
除了设备驱动,Linux还提供了用户空间访问这些总线的API:
- SPI:通过
/dev/spidevX.Y设备节点和ioctl接口 - I2C:通过
/dev/i2c-X设备节点和ioctl(I2C_RDWR) - UART:通过标准TTY设备节点和
termios接口 - CAN:通过SocketCAN接口(PF_CAN)
8.3 相关内核文档
- SPI:
Documentation/spi/ - I2C:
Documentation/i2c/ - TTY:
Documentation/serial/ - CAN:
Documentation/networking/can.rst
8.4 推荐硬件工具
-
SPI/I2C调试:
- Logic Analyzer (Saleae, DSLogic)
- Bus Pirate
- CH341A编程器
-
UART调试:
- FT232/FT2232调试器
- CP2102 USB转串口
- PL2303 USB转串口
-
CAN调试:
- PCAN-USB适配器
- USB-CAN分析仪
- CANable开源CAN适配器
9. 实战经验分享
在实际项目开发中,我总结了以下宝贵经验:
-
SPI实战技巧:
- 对于高速SPI设备(如Flash),启用DMA可以显著提高性能
- 片选信号切换间加入微小延迟,某些设备需要此等待时间
- 使用
spi_message结构组织复杂传输序列
-
I2C避坑指南:
- 总线锁死时,可以通过重新初始化I2C控制器恢复
- 对于地址冲突的设备,可以使用I2C多路复用器(PCA9548)
- SMBus协议是I2C的子集,注意兼容性差异
-
UART调试心得:
- 长距离传输时,改用RS-485并启用终端电阻
- 遇到数据丢失时,首先检查流控设置
- 对于不稳定连接,实现自定义心跳和重传机制
-
CAN开发经验:
- 设计阶段合理规划CAN ID分配方案
- 使用candump记录总线流量分析异常
- 实现接收超时处理,避免阻塞
10. 未来发展趋势
-
更高速度的变种:
- SPI → QSPI/OSPI(用于高速存储器)
- I2C → I3C(引入更高速度和新特性)
- CAN → CAN FD(灵活数据速率)
-
无线化演进:
- 蓝牙/WiFi替代部分UART应用
- Wireless M-Bus替代部分I2C应用
-
安全性增强:
- 总线加密(如CAN加密网关)
- 身份认证(特别是I2C和SPI设备)
-
工具链完善:
- 更强大的总线分析工具
- 自动化测试框架集成
- 云端监控和诊断
通过深入理解这四种通信协议的特性和Linux下的实现方式,开发者可以更高效地完成嵌入式系统设计,构建稳定可靠的硬件通信架构。
