Chapter 06 · ARM 嵌入式 C 开发

I²C 与 SPI 总线

掌握 I2C 开漏时序与 SPI 四种模式的硬件原理,驱动 SSD1306 OLED 显示屏和 W25Q128 Flash 存储器。

课程进度60%

关键概念

I2C 协议基础
Inter-Integrated Circuit,两线制总线:SCL(时钟)+ SDA(数据),均为开漏输出,需要上拉电阻(通常 4.7kΩ)。支持多主多从,每个从设备有唯一 7 位地址(0x00-0x7F)。速率:标准模式 100kHz,快速模式 400kHz,高速模式 3.4MHz。
I2C 开漏总线原理
SDA 和 SCL 线上所有设备都只能拉低,高电平靠上拉电阻实现。任何设备可以在任意时刻将总线拉低(线与逻辑)。当主机发送数据后,若从机不回应 ACK(拉低 SDA),主机读到高电平(NACK),得知通信失败。
I2C ACK/NACK
每传输 8 bit 数据后,接收方在第 9 个时钟拉低 SDA 表示 ACK(确认),不拉低表示 NACK(否认/无应答)。NACK 可能意味着:设备地址错误、设备忙、数据不被接受、或传输结束。
SPI 四线制
Serial Peripheral Interface,四线:SCLK(时钟)、MOSI(主发从收)、MISO(主收从发)、CS(片选,低有效)。全双工,同时收发。速度快(可达数十 MHz),无应答机制,适合高速短距离通信(Flash、显示屏、ADC)。
SPI 极性与相位(CPOL/CPHA)
CPOL(时钟极性):0=空闲低,1=空闲高。CPHA(时钟相位):0=第一个边沿采样,1=第二个边沿采样。四种组合形成 Mode 0-3。Mode 0(CPOL=0,CPHA=0)最常用。使用前必须查阅从设备数据手册确认 SPI 模式。
SSD1306 OLED
常见 0.96 寸单色 OLED 显示屏控制芯片,支持 I2C(地址 0x3C 或 0x3D)和 SPI 接口。分辨率 128×64 像素。通过发送命令字节配置显示参数,发送数据字节填充显存(GDDRAM)。驱动时需先发送初始化命令序列。
W25Q128 Flash
华邦 128Mbit(16MB)SPI NOR Flash。页大小 256 字节,扇区 4KB,块 64KB。写操作必须先擦除(置 FF),再写入(只能将 1 写成 0)。SPI Mode 0 或 Mode 3,最高 104MHz。常用作固件存储、配置数据存储。

I2C 时序图解

I2C 完整写时序(主机写数据到从机寄存器) ════════════════════════════════════════════════════════════════ SCL ─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌── │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ SDA ─┐└─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └ │ ← 7位地址(MSB先) → │R/W│ A │←── 数据8位 ──→│ A │ ↑ ↑ ↑ START ACK ACK/NACK (SCL高时SDA下降) (从机拉低) (STOP或继续) START 条件: SCL 高时,SDA 产生下降沿 STOP 条件: SCL 高时,SDA 产生上升沿 数据有效: SCL 高期间 SDA 必须稳定(上升/下降沿只在 SCL 低时) 典型读流程(读 SSD1306 状态): START → [0x3C + W=0] → ACK → [寄存器地址] → ACK → ReSTART → [0x3C + R=1] → ACK → [数据] → NACK → STOP

I2C 驱动 SSD1306 OLED

#include "stm32f4xx_hal.h"
#include "string.h"

#define SSD1306_ADDR    0x3C   /* 7位地址,SA0接GND时为0x3C */
#define SSD1306_CMD     0x00   /* 控制字节:命令 */
#define SSD1306_DATA    0x40   /* 控制字节:数据 */
#define SSD1306_W  128
#define SSD1306_H   64

extern I2C_HandleTypeDef hi2c1;

static uint8_t oled_buf[SSD1306_W * SSD1306_H / 8];  /* 1024 字节显存缓冲 */

/* 发送单个命令 */
static void SSD1306_SendCmd(uint8_t cmd) {
  uint8_t buf[2] = {SSD1306_CMD, cmd};
  HAL_I2C_Master_Transmit(&hi2c1, SSD1306_ADDR << 1, buf, 2, 10);
}

/* 初始化序列(参考 SSD1306 数据手册)*/
void SSD1306_Init(void) {
  static const uint8_t init_cmds[] = {
    0xAE,          /* Display OFF */
    0xD5, 0x80,   /* 时钟分频 */
    0xA8, 0x3F,   /* MUX 比率 = 64 */
    0xD3, 0x00,   /* 显示偏移 = 0 */
    0x40,          /* 起始行 0 */
    0x8D, 0x14,   /* 电荷泵使能 */
    0x20, 0x00,   /* 水平寻址模式 */
    0xA1,          /* SEG 映射翻转 */
    0xC8,          /* COM 扫描反向 */
    0xDA, 0x12,   /* COM 引脚配置 */
    0x81, 0xCF,   /* 对比度 */
    0xD9, 0xF1,   /* 预充电周期 */
    0xDB, 0x40,   /* VCOMH 电平 */
    0xA4,          /* 正常显示(非全亮)*/
    0xA6,          /* 正相显示 */
    0xAF           /* Display ON */
  };
  for (uint8_t i = 0; i < sizeof(init_cmds); i++)
    SSD1306_SendCmd(init_cmds[i]);
  memset(oled_buf, 0, sizeof(oled_buf));
}

/* 刷新全屏(将 oled_buf 写入 GDDRAM)*/
void SSD1306_Refresh(void) {
  /* 设置列地址范围 0-127,页地址范围 0-7 */
  SSD1306_SendCmd(0x21); SSD1306_SendCmd(0); SSD1306_SendCmd(127);
  SSD1306_SendCmd(0x22); SSD1306_SendCmd(0); SSD1306_SendCmd(7);

  uint8_t ctrl = SSD1306_DATA;
  /* 先发控制字节(0x40),再发 1024 字节显存数据 */
  HAL_I2C_Mem_Write(&hi2c1, SSD1306_ADDR << 1,
                    SSD1306_DATA, 1,
                    oled_buf, sizeof(oled_buf), 100);
}

/* 画像素点(x=0-127,y=0-63,color=0/1)*/
void SSD1306_DrawPixel(uint8_t x, uint8_t y, uint8_t color) {
  if (x >= SSD1306_W || y >= SSD1306_H) return;
  if (color)  oled_buf[x + (y / 8) * SSD1306_W] |=  (1 << (y % 8));
  else        oled_buf[x + (y / 8) * SSD1306_W] &= ~(1 << (y % 8));
}

SPI 模式图解

SPI 四种模式(CPOL / CPHA) Mode 0: CPOL=0(空闲低), CPHA=0(第1沿采样) SCLK: ─┐ ┌─┐ ┌─┐ ┌─┐ ┌─ └─┘ └─┘ └─┘ └─┘ MOSI: ──X───────X───────X── (上升沿前稳定,上升沿采样) ↑ 采样 ↑ 采样 Mode 1: CPOL=0(空闲低), CPHA=1(第2沿采样) SCLK: ─┐ ┌─┐ ┌─┐ ┌─┐ ┌─ └─┘ └─┘ └─┘ └─┘ MOSI: ─X───────X───────X─── (下降沿采样) ↑ 采样 ↑ 采样 Mode 3: CPOL=1(空闲高), CPHA=1(第2沿采样) ← W25Q128 使用此模式 SCLK: ─┐ ┌─┐ ┌─┐ ┌─┐ ┌─ → 实际上空闲是高,脉冲向下 └─┘ └─┘ └─┘ └─┘ 常用器件 SPI 模式: SSD1306: Mode 0 或 Mode 3(查具体批次手册) W25Q128: Mode 0 或 Mode 3 MPU6050: I2C(不是SPI) ADS1115: I2C

SPI 读写 W25Q128 Flash

#define W25Q_CS_LOW()   HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET)
#define W25Q_CS_HIGH()  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)

#define W25Q_CMD_READ_ID    0x9F
#define W25Q_CMD_WRITE_EN   0x06
#define W25Q_CMD_READ_DATA  0x03
#define W25Q_CMD_PAGE_PROG  0x02
#define W25Q_CMD_SECTOR_ERS 0x20
#define W25Q_CMD_READ_SR    0x05

extern SPI_HandleTypeDef hspi1;

/* 发送单字节并接收响应 */
static uint8_t SPI_TxRx(uint8_t tx) {
  uint8_t rx;
  HAL_SPI_TransmitReceive(&hspi1, &tx, &rx, 1, 10);
  return rx;
}

/* 等待 Flash 内部操作完成(读状态寄存器 BUSY 位)*/
static void W25Q_WaitBusy(void) {
  uint8_t sr;
  do {
    W25Q_CS_LOW();
    SPI_TxRx(W25Q_CMD_READ_SR);
    sr = SPI_TxRx(0xFF);
    W25Q_CS_HIGH();
  } while (sr & 0x01);  /* bit0 = BUSY */
}

/* 读取 JEDEC ID(设备识别)*/
uint32_t W25Q_ReadID(void) {
  uint32_t id = 0;
  W25Q_CS_LOW();
  SPI_TxRx(W25Q_CMD_READ_ID);
  id |= (uint32_t)SPI_TxRx(0xFF) << 16;  /* MF ID */
  id |= (uint32_t)SPI_TxRx(0xFF) << 8;   /* Memory Type */
  id |= (uint32_t)SPI_TxRx(0xFF);        /* Capacity */
  W25Q_CS_HIGH();
  return id;  /* W25Q128 = 0xEF4018 */
}

/* 读取数据(任意地址,任意长度)*/
void W25Q_Read(uint32_t addr, uint8_t *buf, uint32_t len) {
  W25Q_CS_LOW();
  SPI_TxRx(W25Q_CMD_READ_DATA);
  SPI_TxRx((addr >> 16) & 0xFF);
  SPI_TxRx((addr >> 8)  & 0xFF);
  SPI_TxRx( addr         & 0xFF);
  HAL_SPI_Receive(&hspi1, buf, len, 1000);
  W25Q_CS_HIGH();
}

/* 写入一页(最多256字节,必须先扇区擦除!)*/
void W25Q_PageProgram(uint32_t addr, uint8_t *data, uint16_t len) {
  W25Q_CS_LOW(); SPI_TxRx(W25Q_CMD_WRITE_EN); W25Q_CS_HIGH();

  W25Q_CS_LOW();
  SPI_TxRx(W25Q_CMD_PAGE_PROG);
  SPI_TxRx((addr >> 16) & 0xFF);
  SPI_TxRx((addr >> 8)  & 0xFF);
  SPI_TxRx( addr         & 0xFF);
  HAL_SPI_Transmit(&hspi1, data, len, 1000);
  W25Q_CS_HIGH();
  W25Q_WaitBusy();
}

/* 4KB 扇区擦除(整扇区变为 0xFF)*/
void W25Q_SectorErase(uint32_t sector_addr) {
  W25Q_CS_LOW(); SPI_TxRx(W25Q_CMD_WRITE_EN); W25Q_CS_HIGH();
  W25Q_CS_LOW();
  SPI_TxRx(W25Q_CMD_SECTOR_ERS);
  SPI_TxRx((sector_addr >> 16) & 0xFF);
  SPI_TxRx((sector_addr >> 8)  & 0xFF);
  SPI_TxRx( sector_addr         & 0xFF);
  W25Q_CS_HIGH();
  W25Q_WaitBusy();  /* 扇区擦除约需 45-400ms */
}
Flash 写操作注意事项

NOR Flash 的写操作只能将 bit 从 1 改为 0。若目标地址已有数据(非全 FF),必须先执行扇区擦除(将整个 4KB 变为 0xFF),再写入新数据。写入前必须先发送 Write Enable 命令(0x06),每次页写后写使能自动清除。频繁擦写会磨损 Flash,W25Q128 擦写寿命约 10 万次。

I2C 上拉电阻选择

上拉电阻值影响 I2C 速度和功耗。电阻太大(>10kΩ):上升沿太慢,高速模式无法工作。电阻太小(<1kΩ):静态电流大,驱动能力要求高。经验值:100kHz 用 10kΩ,400kHz 用 4.7kΩ,Fast-Mode Plus(1MHz)用 1kΩ。总线上接多个设备时,上拉电阻并联,等效值变小,可能需要调整。