同步通信协议(I2C/SPI)驱动OLED/EEPROM/传感器实战

在嵌入式开发中,设备间的通信是核心环节之一,同步通信协议凭借时钟同步的优势,成为芯片间短距离数据传输的首选。其中I2C(Inter-Integrated Circuit)SPI(Serial Peripheral Interface) 是最常用的两种同步通信协议,广泛应用于OLED显示、EEPROM存储、传感器数据采集等场景。

一、同步通信基础:为什么选择I2C和SPI?

同步通信的核心是时钟信号同步,主设备产生时钟(SCL/SCLK),从设备根据时钟节拍接收/发送数据,相比异步通信(如UART),同步通信速率更高、时序更精准,适合芯片间近距离通信。

I2C和SPI作为同步通信的代表,各有特点,适用场景不同:

特性 I2C协议 SPI协议
总线引脚 2根(SDA数据线、SCL时钟线) 4根(SCLK时钟、MOSI主发从收、MISO主收从发、CS片选)
通信模式 多主多从 单主多从
地址机制 7位/10位从机地址 片选信号(CS)选通从机
通信速率 标准模式100Kbps,快速模式400Kbps 最高可达几十Mbps(由硬件决定)
硬件复杂度 需上拉电阻 无需上拉电阻
适用场景 低速、多设备通信(如OLED、EEPROM) 高速、单设备/少设备通信(如传感器、Flash)

简单来说:I2C用最少的引脚实现多设备通信,适合低速场景;SPI以更多引脚换取更高速率,适合高速数据采集

二、I2C协议详解:原理、时序与硬件连接

I2C协议由飞利浦公司开发,也叫“两线式串行总线”,是嵌入式开发中最常用的低速通信协议,几乎所有微控制器都集成了I2C控制器。

1. I2C总线结构

I2C总线仅需两根线:

  • SDA(Serial Data):双向数据线,主从设备均可收发数据;
  • SCL(Serial Clock):时钟线,由主设备产生,控制通信节奏。

此外,SDA和SCL必须外接上拉电阻(通常4.7KΩ~10KΩ),因为I2C总线采用“开漏输出”设计,空闲时总线被上拉为高电平,通信时通过拉低总线传输数据。

I2C支持多主多从架构,总线上可挂载多个主设备和从设备,每个从设备拥有唯一的7位/10位地址,主设备通过地址选择通信的从设备。

2. I2C核心通信时序

I2C的通信过程由起始信号、地址帧、数据帧、应答信号、停止信号组成,时序是协议的核心,必须严格遵守。

(1)起始信号(Start)

当SCL为高电平时,SDA由高电平拉低,产生起始信号,标志着通信开始。

// STM32 HAL库模拟I2C起始信号示例
void I2C_Start(void) {
    I2C_SDA_HIGH();  // SDA置高
    I2C_SCL_HIGH();  // SCL置高
    delay_us(2);
    I2C_SDA_LOW();   // SDA拉低
    delay_us(2);
    I2C_SCL_LOW();   // SCL拉低,准备发送数据
}

(2)停止信号(Stop)

当SCL为高电平时,SDA由低电平拉高,产生停止信号,标志着通信结束。

void I2C_Stop(void) {
    I2C_SDA_LOW();   // SDA置低
    I2C_SCL_HIGH();  // SCL置高
    delay_us(2);
    I2C_SDA_HIGH();  // SDA拉高
    delay_us(2);
}

(3)应答信号(ACK/NACK)

I2C采用“应答机制”保证通信可靠性:

  • 主设备发送1字节数据后,释放SDA,等待从设备拉低SDA产生应答(ACK)
  • 若从设备未拉低SDA(高电平),则为非应答(NACK),主设备需终止通信。
// 等待从设备应答
uint8_t I2C_Wait_Ack(void) {
    uint8_t retry = 0;
    I2C_SDA_HIGH();  // 释放SDA
    delay_us(1);
    I2C_SCL_HIGH();  // SCL置高,检测SDA
    delay_us(1);
    while(I2C_SDA_READ()) {  // 检测SDA电平
        retry++;
        if(retry > 250) {
            I2C_Stop();
            return 1;  // 无应答,返回错误
        }
    }
    I2C_SCL_LOW();  // SCL拉低,继续通信
    return 0;       // 有应答,返回成功
}

(4)数据传输

I2C以字节为单位传输数据,高位先行,每个数据位对应SCL的一个时钟周期:SCL高电平时,SDA数据稳定;SCL低电平时,SDA可以改变数据。

// 发送1字节数据
void I2C_Send_Byte(uint8_t data) {
    uint8_t i;
    I2C_SCL_LOW();  // SCL拉低,准备发送
    for(i=0; i<8; i++) {
        // 发送高位
        if(data & 0x80) I2C_SDA_HIGH();
        else I2C_SDA_LOW();
        data <<= 1;
        delay_us(2);
        I2C_SCL_HIGH();  // SCL置高,从设备读取数据
        delay_us(2);
        I2C_SCL_LOW();   // SCL拉低,准备下一位
        delay_us(2);
    }
}

3. I2C硬件连接要点

以STM32与I2C设备(如OLED)为例,硬件连接需注意:

  1. STM32的I2C引脚(如PB6=SCL、PB7=SDA)分别连接设备的SCL、SDA引脚;
  2. SDA和SCL引脚串联4.7KΩ上拉电阻到VCC(3.3V);
  3. 所有I2C设备的SDA/SCL共地,电源电压需匹配(如3.3V)。

三、SPI协议详解:原理、时序与硬件连接

SPI协议由摩托罗拉公司开发,是一种高速同步串行通信协议,采用“主从式”架构,适合高速数据传输(如传感器、Flash、触摸屏)。

1. SPI总线结构

SPI总线通常包含4根线(部分场景可简化为3根):

  • SCLK(Serial Clock):时钟线,由主设备产生;
  • MOSI(Master Out Slave In):主设备发送、从设备接收数据;
  • MISO(Master In Slave Out):主设备接收、从设备发送数据;
  • CS/SS(Chip Select):片选线,主设备拉低CS选通对应的从设备(SPI为单主多从,通过CS选择从设备)。

SPI无地址机制,通过CS信号选通从设备,硬件连接比I2C更简单,且无需上拉电阻。

2. SPI核心通信时序

SPI的时序由时钟极性(CPOL)时钟相位(CPHA) 决定,共4种通信模式,主从设备必须使用相同的模式。

  • CPOL:决定SCLK空闲时的电平(0=空闲低电平,1=空闲高电平);
  • CPHA:决定数据采样的时刻(0=第一个时钟沿采样,1=第二个时钟沿采样)。

最常用的是模式0(CPOL=0,CPHA=0)

  1. SCLK空闲时为低电平;
  2. 主设备在SCLK上升沿发送数据,从设备在上升沿采样;
  3. 主设备在SCLK下降沿接收数据,从设备在下降沿发送数据。
// STM32 HAL库SPI模式0发送1字节数据示例
uint8_t SPI_Send_Byte(SPI_HandleTypeDef *hspi, uint8_t data) {
    uint8_t recv_data;
    // 发送并接收1字节(SPI为全双工)
    HAL_SPI_TransmitReceive(hspi, &data, &recv_data, 1, 100);
    return recv_data;
}

3. SPI硬件连接要点

以STM32与SPI传感器(如ADXL345)为例,硬件连接需注意:

  1. STM32的SPI引脚(如PA5=SCLK、PA7=MOSI、PA6=MISO、PA4=CS)分别连接传感器的对应引脚;
  2. CS引脚需由主设备控制,通信前拉低CS选通从设备,通信结束后拉高;
  3. SPI为全双工通信,若仅需单向传输,可省略MISO/MOSI引脚(如仅主发从收时,去掉MISO)。

四、实战1:I2C驱动0.96寸OLED显示屏

0.96寸OLED显示屏是嵌入式开发中最常用的显示设备,多数采用I2C接口,下面以STM32F103为例,讲解I2C驱动OLED的完整流程。

1. 硬件连接

STM32引脚 OLED引脚 说明
PB6 SCL I2C时钟线
PB7 SDA I2C数据线
3.3V VCC 供电(勿接5V)
GND GND 共地

同时,SDA和SCL引脚需外接4.7KΩ上拉电阻到3.3V。

2. OLED I2C通信原理

0.96寸I2C OLED的从机地址通常为0x78(或0x7A,由ADDR引脚电平决定),通信分为命令写入数据写入

  • 写入命令:主设备发送地址帧0x78→应答→发送控制字节0x00(表示后续为命令)→应答→发送命令字节→应答;
  • 写入数据:主设备发送地址帧0x78→应答→发送控制字节0x40(表示后续为数据)→应答→发送数据字节→应答。

3. 核心驱动代码(STM32 HAL库)

(1)OLED初始化

OLED初始化需配置显示模式、对比度、扫描方向等命令:

#include "stm32f1xx_hal.h"
#include "delay.h"

// 定义I2C引脚
#define OLED_SCL_PIN GPIO_PIN_6
#define OLED_SDA_PIN GPIO_PIN_7
#define OLED_GPIO_PORT GPIOB

#define OLED_I2C_ADDR 0x78  // OLED从机地址

// I2C引脚操作函数
#define OLED_SCL_HIGH() HAL_GPIO_WritePin(OLED_GPIO_PORT, OLED_SCL_PIN, GPIO_PIN_SET)
#define OLED_SCL_LOW()  HAL_GPIO_WritePin(OLED_GPIO_PORT, OLED_SCL_PIN, GPIO_PIN_RESET)
#define OLED_SDA_HIGH() HAL_GPIO_WritePin(OLED_GPIO_PORT, OLED_SDA_PIN, GPIO_PIN_SET)
#define OLED_SDA_LOW()  HAL_GPIO_WritePin(OLED_GPIO_PORT, OLED_SDA_PIN, GPIO_PIN_RESET)

// 发送I2C起始信号(同前文I2C_Start)
void OLED_I2C_Start(void) { ... }
// 发送I2C停止信号(同前文I2C_Stop)
void OLED_I2C_Stop(void) { ... }
// 发送1字节数据(同前文I2C_Send_Byte)
void OLED_I2C_SendByte(uint8_t data) { ... }
// 等待应答(同前文I2C_Wait_Ack)
uint8_t OLED_I2C_WaitAck(void) { ... }

// 向OLED写入命令
void OLED_WriteCmd(uint8_t cmd) {
    OLED_I2C_Start();
    OLED_I2C_SendByte(OLED_I2C_ADDR);  // 发送从机地址
    OLED_I2C_WaitAck();
    OLED_I2C_SendByte(0x00);           // 控制字节:写命令
    OLED_I2C_WaitAck();
    OLED_I2C_SendByte(cmd);            // 发送命令
    OLED_I2C_WaitAck();
    OLED_I2C_Stop();
}

// 向OLED写入数据
void OLED_WriteData(uint8_t data) {
    OLED_I2C_Start();
    OLED_I2C_SendByte(OLED_I2C_ADDR);  // 发送从机地址
    OLED_I2C_WaitAck();
    OLED_I2C_SendByte(0x40);           // 控制字节:写数据
    OLED_I2C_WaitAck();
    OLED_I2C_SendByte(data);           // 发送数据
    OLED_I2C_WaitAck();
    OLED_I2C_Stop();
}

// OLED初始化函数
void OLED_Init(void) {
    delay_ms(100);  // 上电延时
    // 发送初始化命令
    OLED_WriteCmd(0xAE);  // 关闭显示
    OLED_WriteCmd(0x00);  // 设置列地址低位
    OLED_WriteCmd(0x10);  // 设置列地址高位
    OLED_WriteCmd(0x40);  // 设置显示起始行
    OLED_WriteCmd(0xB0);  // 设置页地址
    OLED_WriteCmd(0x81);  // 设置对比度
    OLED_WriteCmd(0xFF);  // 对比度最大值
    OLED_WriteCmd(0xA1);  // 段重映射(水平翻转)
    OLED_WriteCmd(0xA6);  // 正常显示(0xA7为反显)
    OLED_WriteCmd(0xA8);  // 设置多路复用率
    OLED_WriteCmd(0x3F);  // 1/64 Duty
    OLED_WriteCmd(0xC8);  // COM扫描方向(垂直翻转)
    OLED_WriteCmd(0xD3);  // 设置显示偏移
    OLED_WriteCmd(0x00);  // 偏移为0
    OLED_WriteCmd(0xD5);  // 设置时钟分频
    OLED_WriteCmd(0x80);  // 分频因子
    OLED_WriteCmd(0xD9);  // 设置预充电周期
    OLED_WriteCmd(0xF1);
    OLED_WriteCmd(0xDA);  // 设置COM引脚配置
    OLED_WriteCmd(0x12);
    OLED_WriteCmd(0xDB);  // 设置VCOMH
    OLED_WriteCmd(0x40);
    OLED_WriteCmd(0x8D);  // 使能电荷泵
    OLED_WriteCmd(0x14);
    OLED_WriteCmd(0xAF);  // 开启显示
}

(2)OLED显示字符

OLED的显存为8页×128列,每个字符由8×16点阵组成,需先制作字模,再将字模数据写入显存:

// 字模:以字符'A'为例(8×16点阵,由字模软件生成)
const uint8_t OLED_Font_A[] = {
    0x00,0x7C,0x12,0x11,0x12,0x7C,0x00,0x00,
    0x00,0x3F,0x40,0x40,0x40,0x3F,0x00,0x00
};

// 设置光标位置(页地址,列地址)
void OLED_SetPos(uint8_t page, uint8_t col) {
    OLED_WriteCmd(0xB0 + page);  // 设置页地址
    OLED_WriteCmd(col & 0x0F);   // 列地址低位
    OLED_WriteCmd(0x10 + (col >> 4));  // 列地址高位
}

// 显示单个字符
void OLED_ShowChar(uint8_t page, uint8_t col, uint8_t ch) {
    uint8_t i;
    OLED_SetPos(page, col);
    // 显示上8行
    for(i=0; i<8; i++) {
        OLED_WriteData(OLED_Font_A[i]);
    }
    OLED_SetPos(page+1, col);
    // 显示下8行
    for(i=8; i<16; i++) {
        OLED_WriteData(OLED_Font_A[i]);
    }
}

// 主函数中调用
int main(void) {
    HAL_Init();
    SystemClock_Config();
    // 初始化I2C引脚为输出模式
    GPIO_InitTypeDef gpio_init;
    gpio_init.Pin = OLED_SCL_PIN | OLED_SDA_PIN;
    gpio_init.Mode = GPIO_MODE_OUTPUT_PP;
    gpio_init.Pull = GPIO_NOPULL;
    gpio_init.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(OLED_GPIO_PORT, &gpio_init);
    
    OLED_Init();          // 初始化OLED
    OLED_ShowChar(0, 0, 'A');  // 在第0页第0列显示字符'A'
    while(1) {
    }
}

五、实战2:I2C驱动AT24C02 EEPROM

AT24C02是256字节的I2C接口EEPROM(电可擦除可编程只读存储器),用于存储掉电不丢失的数据(如设备参数、校准数据),下面讲解其读写方法。

1. 硬件连接

STM32引脚 AT24C02引脚 说明
PB6 SCL I2C时钟线
PB7 SDA I2C数据线
3.3V VCC 供电
GND GND 共地

SDA和SCL同样需外接4.7KΩ上拉电阻,AT24C02的从机地址为0xA0(可通过A0/A1/A2引脚修改)。

2. AT24C02读写原理

  • 写操作:AT24C02支持字节写入和页写入(每页8字节),写入后需等待约5ms的擦写时间;
  • 读操作:支持随机读和连续读,需先发送要读取的地址,再接收数据。

3. 核心驱动代码

#define AT24C02_ADDR 0xA0  // AT24C02从机地址
#define AT24C02_PAGE_SIZE 8  // 页大小8字节

// 向AT24C02指定地址写入1字节
void AT24C02_WriteByte(uint8_t addr, uint8_t data) {
    I2C_Start();
    I2C_Send_Byte(AT24C02_ADDR);  // 发送从机地址
    I2C_Wait_Ack();
    I2C_Send_Byte(addr);          // 发送存储地址
    I2C_Wait_Ack();
    I2C_Send_Byte(data);          // 发送数据
    I2C_Wait_Ack();
    I2C_Stop();
    delay_ms(5);  // 等待擦写完成
}

// 从AT24C02指定地址读取1字节
uint8_t AT24C02_ReadByte(uint8_t addr) {
    uint8_t data;
    I2C_Start();
    I2C_Send_Byte(AT24C02_ADDR);  // 发送从机地址(写)
    I2C_Wait_Ack();
    I2C_Send_Byte(addr);          // 发送存储地址
    I2C_Wait_Ack();
    
    I2C_Start();                  // 重复起始信号
    I2C_Send_Byte(AT24C02_ADDR+1);  // 发送从机地址(读)
    I2C_Wait_Ack();
    data = I2C_Read_Byte();       // 接收数据(需实现I2C_Read_Byte)
    I2C_Send_Nack();              // 发送非应答
    I2C_Stop();
    return data;
}

// 主函数中测试
int main(void) {
    uint8_t temp_data;
    HAL_Init();
    // 初始化I2C引脚(同OLED)
    
    AT24C02_WriteByte(0x00, 0x55);  // 向地址0x00写入0x55
    temp_data = AT24C02_ReadByte(0x00);  // 从地址0x00读取数据
    if(temp_data == 0x55) {
        OLED_ShowChar(0, 0, 'O');  // 读取成功,OLED显示'O'
    }
    while(1) {
    }
}

六、实战3:SPI驱动ADXL345加速度传感器

ADXL345是SPI/I2C接口的三轴加速度传感器,可采集X/Y/Z轴的加速度数据,下面以SPI接口为例,讲解驱动方法。

1. 硬件连接

STM32引脚 ADXL345引脚 说明
PA5 SCLK SPI时钟线
PA7 MOSI 主发从收
PA6 MISO 主收从发
PA4 CS 片选线
3.3V VCC 供电
GND GND 共地

2. ADXL345 SPI通信原理

ADXL345的SPI通信为模式0(CPOL=0,CPHA=0),通过读写寄存器配置传感器(如量程、采样率),并读取加速度数据。核心寄存器:

  • 0x2D(POWER_CTL):电源控制,设置测量模式;
  • 0x31(DATA_FORMAT):数据格式,设置量程;
  • 0x32~0x37(DATAX0~DATAZ1):X/Y/Z轴加速度数据寄存器(16位)。

3. 核心驱动代码

#include "stm32f1xx_hal.h"

#define ADXL345_CS_PIN GPIO_PIN_4
#define ADXL345_CS_PORT GPIOA

// SPI句柄(需在CubeMX中配置SPI1)
extern SPI_HandleTypeDef hspi1;

// CS引脚操作
#define ADXL345_CS_HIGH() HAL_GPIO_WritePin(ADXL345_CS_PORT, ADXL345_CS_PIN, GPIO_PIN_SET)
#define ADXL345_CS_LOW()  HAL_GPIO_WritePin(ADXL345_CS_PORT, ADXL345_CS_PIN, GPIO_PIN_RESET)

// 向ADXL345寄存器写入数据
void ADXL345_WriteReg(uint8_t reg, uint8_t data) {
    ADXL345_CS_LOW();  // 选通从设备
    // 发送寄存器地址(最高位为0表示写)
    HAL_SPI_Transmit(&hspi1, &reg, 1, 100);
    // 发送数据
    HAL_SPI_Transmit(&hspi1, &data, 1, 100);
    ADXL345_CS_HIGH(); // 取消选通
}

// 从ADXL345寄存器读取数据
uint8_t ADXL345_ReadReg(uint8_t reg) {
    uint8_t data;
    reg |= 0x80;  // 最高位为1表示读
    ADXL345_CS_LOW();  // 选通从设备
    // 发送寄存器地址
    HAL_SPI_Transmit(&hspi1, &reg, 1, 100);
    // 接收数据
    HAL_SPI_Receive(&hspi1, &data, 1, 100);
    ADXL345_CS_HIGH(); // 取消选通
    return data;
}

// 初始化ADXL345
void ADXL345_Init(void) {
    ADXL345_WriteReg(0x2D, 0x08);  // 测量模式
    ADXL345_WriteReg(0x31, 0x0B);  // 量程±16g,13位分辨率
}

// 读取三轴加速度数据
void ADXL345_ReadAcc(int16_t *x, int16_t *y, int16_t *z) {
    uint8_t buf[6];
    // 连续读取6个寄存器(DATAX0~DATAZ1)
    buf[0] = ADXL345_ReadReg(0x32);
    buf[1] = ADXL345_ReadReg(0x33);
    buf[2] = ADXL345_ReadReg(0x34);
    buf[3] = ADXL345_ReadReg(0x35);
    buf[4] = ADXL345_ReadReg(0x36);
    buf[5] = ADXL345_ReadReg(0x37);
    // 拼接16位数据
    *x = (int16_t)(buf[1] << 8 | buf[0]);
    *y = (int16_t)(buf[3] << 8 | buf[2]);
    *z = (int16_t)(buf[5] << 8 | buf[4]);
}

// 主函数中调用
int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_SPI1_Init();  // 初始化SPI1
    // 初始化CS引脚为输出模式
    GPIO_InitTypeDef gpio_init;
    gpio_init.Pin = ADXL345_CS_PIN;
    gpio_init.Mode = GPIO_MODE_OUTPUT_PP;
    gpio_init.Pull = GPIO_NOPULL;
    gpio_init.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(ADXL345_CS_PORT, &gpio_init);
    
    ADXL345_Init();  // 初始化传感器
    int16_t x, y, z;
    while(1) {
        ADXL345_ReadAcc(&x, &y, &z);  // 读取加速度数据
        // 将数据显示到OLED(结合前文OLED驱动)
        delay_ms(100);
    }
}

七、同步通信协议开发注意事项

  1. 上拉电阻选择:I2C的上拉电阻需根据通信速率选择,100Kbps用10KΩ,400Kbps用4.7KΩ;
  2. 时序匹配:主从设备的时钟速率需一致,SPI的CPOL/CPHA模式必须匹配;
  3. 应答检测:I2C通信中必须检测从设备的应答信号,避免数据传输失败;
  4. 片选信号控制:SPI的CS信号需在通信前拉低、通信后拉高,多从设备时避免CS冲突;
  5. 擦写延时:EEPROM、Flash等存储设备写入后需等待擦写完成,否则会写入失败;
  6. 抗干扰处理:总线较长时,需在SDA/SCL/SCLK引脚添加电容滤波,减少电磁干扰。
posted @ 2025-12-25 22:17  python农工  阅读(6)  评论(0)    收藏  举报