当前位置: 首页 > news >正文

保姆级教程:用一块STM32F103开发板同时玩转SPI Flash和IIC OLED屏

保姆级教程:用一块STM32F103开发板同时玩转SPI Flash和IIC OLED屏

在嵌入式开发中,SPI和IIC是最常用的两种通信协议。对于初学者来说,单独使用一种协议可能已经足够挑战,但实际项目中往往需要同时管理多种外设。本文将带你用一块常见的STM32F103开发板,同时驱动SPI接口的W25Qxx Flash芯片和IIC接口的SSD1306 OLED屏,实现从Flash读取数据并显示的功能。

这个项目不仅能让你掌握两种通信协议的实际应用,还能学习如何在同一个工程中协调管理不同外设。我们会从硬件连接开始,一步步讲解CubeMX配置、驱动编写,直到最终实现功能。过程中还会分享一些实战中的小技巧和常见坑点。

1. 硬件准备与连接

1.1 所需材料清单

在开始之前,请确保你已准备好以下硬件:

  • STM32F103C8T6最小系统板(蓝色药丸开发板)
  • W25Qxx系列SPI Flash模块(如W25Q64)
  • SSD1306 0.96寸IIC OLED显示屏
  • 杜邦线若干(建议使用不同颜色区分功能)
  • USB转TTL模块(用于串口调试)
  • 面包板(可选,但推荐使用)

1.2 引脚连接指南

正确连接硬件是项目成功的第一步。下面是具体的引脚连接方案:

SPI Flash连接(W25Qxx)

Flash引脚STM32引脚功能说明
CSPA4片选信号
DOPA6MISO
DIPA7MOSI
CLKPA5SCLK
VCC3.3V电源正极
GNDGND地线

IIC OLED连接(SSD1306)

OLED引脚STM32引脚功能说明
SDAPB7数据线
SCLPB6时钟线
VCC3.3V电源正极
GNDGND地线

提示:STM32的IIC引脚是固定的,SCL必须接PB6,SDA必须接PB7。如果使用其他引脚,需要软件模拟IIC。

1.3 连接注意事项

  1. 电源选择:W25Qxx和SSD1306都使用3.3V供电,切勿接5V,否则可能损坏设备。
  2. 线长控制:杜邦线不宜过长,特别是时钟信号线,过长可能导致信号失真。
  3. 接触可靠:确保所有连接牢固,接触不良是最常见的调试难题。
  4. 上拉电阻:IIC总线需要上拉电阻(通常4.7kΩ),如果模块上已集成,则无需额外添加。

2. 开发环境配置

2.1 CubeMX工程创建

  1. 打开STM32CubeMX,新建工程,选择STM32F103C8Tx系列芯片。
  2. 配置时钟源:
    • HSE选择Crystal/Ceramic Resonator
    • 在Clock Configuration中将系统时钟设置为72MHz
  3. 配置SPI1:
    • Mode: Full-Duplex Master
    • Hardware NSS Signal: Disable
    • Prescaler: 分频系数设为8(SPI时钟9MHz)
    • CPOL: Low
    • CPHA: 1 Edge
  4. 配置I2C1:
    • Mode: I2C
    • Speed: Standard Mode (100kHz)
  5. 配置USART1用于调试输出(可选但推荐):
    • Mode: Asynchronous
    • Baud Rate: 115200
  6. 生成代码,选择MDK-ARM或你喜欢的IDE。

2.2 关键代码结构

生成的工程包含以下重要文件:

├── Core │ ├── Inc │ │ ├── main.h │ │ ├── spi.h │ │ └── i2c.h │ └── Src │ ├── main.c │ ├── spi.c │ └── i2c.c ├── Drivers └── MDK-ARM

我们需要在工程中添加以下驱动文件:

  • ssd1306.c/.h:OLED显示屏驱动
  • w25qxx.c/.h:SPI Flash驱动

3. SPI Flash驱动实现

3.1 W25Qxx基础驱动

首先实现Flash的基本读写功能。在w25qxx.c中添加以下核心函数:

// Flash初始化 void W25Qxx_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; // CS引脚初始化 GPIO_InitStruct.Pin = GPIO_PIN_4; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); W25Qxx_CS_HIGH(); // 初始时取消片选 // 读取Flash ID验证连接 uint16_t id = W25Qxx_ReadID(); if(id != W25Q64_ID) { printf("Flash ID error: 0x%X\r\n", id); } else { printf("W25Q64 detected\r\n"); } } // 读取Flash ID uint16_t W25Qxx_ReadID(void) { uint8_t cmd[4] = {0x90, 0x00, 0x00, 0x00}; uint8_t id[2] = {0}; W25Qxx_CS_LOW(); HAL_SPI_Transmit(&hspi1, cmd, 4, 100); HAL_SPI_Receive(&hspi1, id, 2, 100); W25Qxx_CS_HIGH(); return (id[0] << 8) | id[1]; } // 扇区擦除 void W25Qxx_EraseSector(uint32_t addr) { uint8_t cmd[4]; W25Qxx_WriteEnable(); cmd[0] = 0x20; // Sector Erase指令 cmd[1] = (addr >> 16) & 0xFF; cmd[2] = (addr >> 8) & 0xFF; cmd[3] = addr & 0xFF; W25Qxx_CS_LOW(); HAL_SPI_Transmit(&hspi1, cmd, 4, 100); W25Qxx_CS_HIGH(); W25Qxx_WaitForWriteEnd(); }

3.2 Flash数据读写优化

为了提高读写效率,我们可以实现页编程和连续读取功能:

// 页编程(写入一页256字节) void W25Qxx_WritePage(uint8_t* buf, uint32_t addr, uint16_t len) { uint8_t cmd[4]; W25Qxx_WriteEnable(); cmd[0] = 0x02; // Page Program指令 cmd[1] = (addr >> 16) & 0xFF; cmd[2] = (addr >> 8) & 0xFF; cmd[3] = addr & 0xFF; W25Qxx_CS_LOW(); HAL_SPI_Transmit(&hspi1, cmd, 4, 100); HAL_SPI_Transmit(&hspi1, buf, len, 1000); W25Qxx_CS_HIGH(); W25Qxx_WaitForWriteEnd(); } // 连续读取数据 void W25Qxx_ReadData(uint8_t* buf, uint32_t addr, uint16_t len) { uint8_t cmd[4]; cmd[0] = 0x03; // Read Data指令 cmd[1] = (addr >> 16) & 0xFF; cmd[2] = (addr >> 8) & 0xFF; cmd[3] = addr & 0xFF; W25Qxx_CS_LOW(); HAL_SPI_Transmit(&hspi1, cmd, 4, 100); HAL_SPI_Receive(&hspi1, buf, len, 1000); W25Qxx_CS_HIGH(); }

注意:Flash写入前必须先擦除,且擦除最小单位是扇区(4KB)。写入最小单位是页(256字节),跨页写入需要分多次操作。

4. SSD1306 OLED驱动实现

4.1 OLED初始化与基础函数

ssd1306.c中实现OLED的基本显示功能:

// OLED初始化 void SSD1306_Init(void) { // 初始化命令序列 const uint8_t init_cmds[] = { 0xAE, // 关闭显示 0xD5, 0x80, // 设置时钟分频 0xA8, 0x3F, // 设置多路复用率 0xD3, 0x00, // 设置显示偏移 0x40, // 设置起始行 0x8D, 0x14, // 电荷泵设置 0x20, 0x00, // 内存地址模式 0xA1, // 段重映射 0xC8, // 扫描方向 0xDA, 0x12, // COM引脚配置 0x81, 0xCF, // 对比度设置 0xD9, 0xF1, // 预充电周期 0xDB, 0x40, // VCOMH设置 0xA4, // 全亮显示 0xA6, // 正常显示 0xAF // 开启显示 }; // 发送初始化命令 for(uint8_t i = 0; i < sizeof(init_cmds); i++) { SSD1306_WriteCommand(init_cmds[i]); } // 清屏 SSD1306_Clear(); } // 写命令 void SSD1306_WriteCommand(uint8_t cmd) { uint8_t buf[2] = {0x00, cmd}; // Co=0, D/C#=0表示命令 HAL_I2C_Master_Transmit(&hi2c1, SSD1306_I2C_ADDR, buf, 2, 100); } // 写数据 void SSD1306_WriteData(uint8_t* data, uint16_t size) { uint8_t buf[size + 1]; buf[0] = 0x40; // Co=0, D/C#=1表示数据 memcpy(&buf[1], data, size); HAL_I2C_Master_Transmit(&hi2c1, SSD1306_I2C_ADDR, buf, size + 1, 100); }

4.2 图形显示功能实现

实现基本的图形和文字显示功能:

// 清屏 void SSD1306_Clear(void) { uint8_t zeros[128] = {0}; for(uint8_t page = 0; page < 8; page++) { SSD1306_SetPosition(0, page); SSD1306_WriteData(zeros, 128); } } // 设置显示位置 void SSD1306_SetPosition(uint8_t x, uint8_t page) { SSD1306_WriteCommand(0xB0 + page); // 设置页地址 SSD1306_WriteCommand(x & 0x0F); // 设置列地址低4位 SSD1306_WriteCommand(0x10 | (x >> 4)); // 设置列地址高4位 } // 显示字符 void SSD1306_PutChar(uint8_t x, uint8_t page, char ch) { if(ch < 32 || ch > 127) ch = ' '; // 只支持可打印ASCII字符 SSD1306_SetPosition(x, page); SSD1306_WriteData(&font_8x16[(ch - 32) * 16], 8); SSD1306_SetPosition(x, page + 1); SSD1306_WriteData(&font_8x16[(ch - 32) * 16 + 8], 8); } // 显示字符串 void SSD1306_PutString(uint8_t x, uint8_t page, const char* str) { while(*str) { SSD1306_PutChar(x, page, *str++); x += 8; if(x >= 128) { x = 0; page += 2; if(page >= 8) break; } } }

5. 系统集成与功能实现

5.1 主程序逻辑设计

main.c中实现主控制逻辑:

int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); MX_I2C1_Init(); MX_USART1_UART_Init(); printf("System init done\r\n"); // 外设初始化 W25Qxx_Init(); SSD1306_Init(); // 检查Flash中是否有数据,如果没有则写入测试数据 if(W25Qxx_CheckEmpty(0x000000, 512)) { printf("Flash is empty, writing test data...\r\n"); uint8_t test_data[512]; memset(test_data, 0, sizeof(test_data)); strcpy((char*)test_data, "Hello from SPI Flash!"); W25Qxx_WriteSector(test_data, 0x000000); } // 主循环 while(1) { // 从Flash读取数据 uint8_t read_buf[512]; W25Qxx_ReadData(read_buf, 0x000000, sizeof(read_buf)); // 在OLED上显示 SSD1306_Clear(); SSD1306_PutString(0, 0, (char*)read_buf); // 添加一些动态效果 static uint8_t counter = 0; char temp_str[20]; sprintf(temp_str, "Count: %d", counter++); SSD1306_PutString(0, 4, temp_str); HAL_Delay(1000); } }

5.2 性能优化技巧

  1. SPI速度优化

    • 在CubeMX中将SPI时钟分频设为2(36MHz)
    • 使用DMA传输减少CPU开销
    // 使用DMA的SPI传输示例 HAL_SPI_Transmit_DMA(&hspi1, data, size);
  2. IIC速度优化

    • 将IIC时钟提高到400kHz(Fast Mode)
    • 使用复合命令减少通信次数
    // 一次性发送命令和数据 uint8_t buf[3] = {0x80, 0xAE, 0x00}; // 复合命令 HAL_I2C_Master_Transmit(&hi2c1, SSD1306_I2C_ADDR, buf, 3, 100);
  3. 双缓冲技术

    • 在内存中维护两个显示缓冲区
    • 后台准备数据,前台显示,减少闪烁

5.3 常见问题排查

SPI通信失败

  1. 检查硬件连接是否正确,特别是CS引脚
  2. 确认SPI模式(CPOL/CPHA)与从设备匹配
  3. 使用逻辑分析仪抓取SPI波形分析

IIC通信失败

  1. 确认SCL和SDA引脚是否正确
  2. 检查总线上是否有上拉电阻(通常4.7kΩ)
  3. 使用示波器检查IIC总线是否有信号

OLED显示异常

  1. 检查供电是否稳定(3.3V)
  2. 确认初始化命令序列是否正确
  3. 检查显示缓冲区数据是否正确

Flash读写错误

  1. 确保写入前已擦除相应扇区
  2. 检查写入地址是否对齐(页对齐)
  3. 验证Flash ID是否正确识别

6. 进阶功能扩展

6.1 实现图形界面

在OLED上实现简单的GUI元素:

// 绘制直线 void SSD1306_DrawLine(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1) { int dx = abs(x1 - x0); int dy = abs(y1 - y0); int sx = (x0 < x1) ? 1 : -1; int sy = (y0 < y1) ? 1 : -1; int err = dx - dy; while(1) { SSD1306_DrawPixel(x0, y0); if(x0 == x1 && y0 == y1) break; int e2 = 2 * err; if(e2 > -dy) { err -= dy; x0 += sx; } if(e2 < dx) { err += dx; y0 += sy; } } } // 绘制位图 void SSD1306_DrawBitmap(uint8_t x, uint8_t y, const uint8_t* bitmap, uint8_t w, uint8_t h) { for(uint8_t j = 0; j < h; j++) { for(uint8_t i = 0; i < w; i++) { if(bitmap[j * w + i]) { SSD1306_DrawPixel(x + i, y + j); } } } }

6.2 文件系统集成

在Flash上实现FAT文件系统,方便管理大量数据:

  1. 添加FatFS中间件到工程
  2. 实现Flash的底层驱动接口:
DSTATUS disk_initialize(BYTE pdrv) { W25Qxx_Init(); return RES_OK; } DRESULT disk_read(BYTE pdrv, BYTE* buff, LBA_t sector, UINT count) { W25Qxx_ReadData(buff, sector * W25QXX_SECTOR_SIZE, count * W25QXX_SECTOR_SIZE); return RES_OK; }
  1. 使用示例:
FATFS fs; FIL file; UINT bw; f_mount(&fs, "", 0); f_open(&file, "data.txt", FA_READ); f_read(&file, buffer, sizeof(buffer), &bw); f_close(&file);

6.3 多任务调度

使用FreeRTOS管理多个外设任务:

void SPI_Task(void const * argument) { while(1) { // 处理SPI Flash操作 osDelay(100); } } void IIC_Task(void const * argument) { while(1) { // 处理OLED显示更新 osDelay(50); } } void Main_Task(void const * argument) { // 初始化外设 W25Qxx_Init(); SSD1306_Init(); // 创建任务 osThreadDef(spi_task, SPI_Task, osPriorityNormal, 0, 128); osThreadCreate(osThread(spi_task), NULL); osThreadDef(iic_task, IIC_Task, osPriorityNormal, 0, 128); osThreadCreate(osThread(iic_task), NULL); while(1) { // 主协调任务 osDelay(1000); } }

7. 项目实战技巧

7.1 调试技巧

  1. 串口打印调试

    printf("SPI Flash ID: 0x%04X\r\n", W25Qxx_ReadID());
  2. LED指示灯

    HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); // 翻转LED状态
  3. 逻辑分析仪使用

    • 抓取SPI波形验证时序
    • 检查IIC起始/停止信号

7.2 性能测试

  1. SPI Flash速度测试

    uint32_t start = HAL_GetTick(); W25Qxx_ReadData(buffer, 0, 4096); uint32_t elapsed = HAL_GetTick() - start; printf("Read speed: %.2f KB/s\r\n", 4.0 / elapsed * 1000);
  2. OLED刷新率测试

    uint32_t frames = 0; uint32_t last_time = HAL_GetTick(); while(1) { SSD1306_Clear(); SSD1306_PutString(0, 0, "FPS Test"); frames++; if(HAL_GetTick() - last_time >= 1000) { printf("FPS: %lu\r\n", frames); frames = 0; last_time = HAL_GetTick(); } }

7.3 电源管理

  1. 低功耗模式

    // 进入停止模式 HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); // 唤醒后重新初始化时钟 SystemClock_Config();
  2. 外设电源控制

    // 控制OLED电源 HAL_GPIO_WritePin(OLED_PWR_GPIO_Port, OLED_PWR_Pin, GPIO_PIN_RESET); // 关闭 HAL_Delay(100); HAL_GPIO_WritePin(OLED_PWR_GPIO_Port, OLED_PWR_Pin, GPIO_PIN_SET); // 开启 SSD1306_Init(); // 重新初始化

8. 项目扩展思路

8.1 物联网数据记录仪

  1. 使用Flash存储传感器历史数据
  2. OLED显示实时数据和简单图表
  3. 通过串口或无线模块上传数据

8.2 嵌入式图形终端

  1. 实现更复杂的GUI界面
  2. 支持触摸屏输入
  3. 从Flash加载多页面内容

8.3 bootloader应用

  1. 使用Flash存储固件更新包
  2. 实现IAP(在应用编程)功能
  3. OLED显示升级进度
// 简单的IAP示例 void JumpToApp(uint32_t app_addr) { typedef void (*pFunction)(void); pFunction Jump_To_Application; // 检查栈指针是否有效 if(((*(__IO uint32_t*)app_addr) & 0x2FFE0000) == 0x20000000) { // 设置跳转地址 Jump_To_Application = (pFunction)(*(__IO uint32_t*)(app_addr + 4)); // 初始化用户应用程序的堆栈指针 __set_MSP(*(__IO uint32_t*)app_addr); // 跳转到应用程序 Jump_To_Application(); } }

在实际项目中,我发现同时管理SPI和IIC设备时,最重要的是时序协调。特别是在没有RTOS的情况下,合理的状态机设计可以避免总线冲突。例如,可以将SPI Flash操作放在主循环中,而将OLED刷新放在定时器中断中,两者通过标志位通信。

http://www.jsqmd.com/news/853789/

相关文章:

  • Gem5实战:从零构建与调试自定义片上网络(NoC)
  • 阶,原根
  • 改背景颜色、固定定位+锚记(复习) - -王心雨
  • 喜马拉雅FM音频下载器:跨平台VIP专辑下载完整指南
  • 融合ArcGIS、InVEST和RUSLE的水土流失动态模拟与空间格局分析
  • WCHUsbSerTest:串口批量自动化测试工具的原理、配置与生产实践
  • 2026年上海长途搬家公司最新推荐排行榜 - 品牌推广大师
  • 2023B卷,第N个排列
  • 别再手动转换时间了!用Jackson和Spring的这两个注解,搞定Java日期序列化所有坑
  • 为什么92%的DeepSeek AWS部署失败?资深架构师拆解3大隐性成本陷阱与4步合规加固法
  • QiWe 免费开源微信机器人:从零到一的完整开发与部署指南
  • 告别手动发送:用TSMaster诊断控制台实现自动化测试脚本(Python/C# API调用教程)
  • MSP430F5438 RTC模块配置与低功耗应用实战指南
  • 2026年1月实测:10款免费好用的降ai率工具 收藏必备 - 降AI实验室
  • 保姆级教程:用Docker一键部署OnlyOffice,再给Cloudreve装上在线预览插件
  • 2026医疗建筑设计公司推荐:专业机构实力解析 靠谱选型指南 - 资讯速览
  • 3个月销50万碗:即食黑芝麻糊厂家案例解析 - 资讯速览
  • 团队冲刺每日总结5.20
  • 为什么92%的DeepSeek RAG Pipeline在迭代3轮后崩溃?真相藏在这份DRY反模式检查清单里(附Git Hooks自动拦截脚本)
  • 5大核心功能重塑NGA论坛浏览体验:从基础优化到高级定制的完整指南
  • 如何从零打造一台开源六足机器人:新手终极指南
  • 保姆级教程:在Ubuntu 22.04上为DCU-Z100(ZiFang)安装ROCm 4.5.2驱动及完整工具链
  • AUTOSAR Ea模块深度剖析:从原理到实战的EEPROM抽象层配置与优化
  • 数据库连接池详解
  • 广州小出口企业找谁做财税?2026年实操指南(附5个决定成败的关键动作) - 欢欢在创业
  • 实战分享:为6个同地址光模块编写Linux I2C驱动(Zynq平台)
  • 2026装配式钢管桩施工服务推荐:专业团队实力解析 权威选型指南 - 资讯速览
  • 深入浅出DPCM与DAPM:图解高通音频架构如何实现动态功耗管理与低延迟播放
  • 【紧急预警】Midjourney团队功能强制迁移启动:现有个人账户在2024年10月15日后将自动降权至只读模式?
  • Google I/O 2026 第二天:Gemini 3.5 实测性能深度解析与 Android XR 生态全景