Chapter 06 · ARM 嵌入式 C 开发
I²C 与 SPI 总线
掌握 I2C 开漏时序与 SPI 四种模式的硬件原理,驱动 SSD1306 OLED 显示屏和 W25Q128 Flash 存储器。
掌握 I2C 开漏时序与 SPI 四种模式的硬件原理,驱动 SSD1306 OLED 显示屏和 W25Q128 Flash 存储器。
#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)); }
#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 */ }
NOR Flash 的写操作只能将 bit 从 1 改为 0。若目标地址已有数据(非全 FF),必须先执行扇区擦除(将整个 4KB 变为 0xFF),再写入新数据。写入前必须先发送 Write Enable 命令(0x06),每次页写后写使能自动清除。频繁擦写会磨损 Flash,W25Q128 擦写寿命约 10 万次。
上拉电阻值影响 I2C 速度和功耗。电阻太大(>10kΩ):上升沿太慢,高速模式无法工作。电阻太小(<1kΩ):静态电流大,驱动能力要求高。经验值:100kHz 用 10kΩ,400kHz 用 4.7kΩ,Fast-Mode Plus(1MHz)用 1kΩ。总线上接多个设备时,上拉电阻并联,等效值变小,可能需要调整。