您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



文章 咨询 工具 课程  
会员   
   
嵌入式软件架构-高级实践
12月11-12日 北京+线上
LLM大模型与智能体开发实战
12月18-19日 北京+线上
需求分析与管理
2026年1月22-23日 北京+线上
     
   
 订阅
SPI通信技术|MCU与FLASH实战应用
 
作者:攻城狮小莫
  9   次浏览      3 次
 2025-12-3
 
编辑推荐:
本文主要介绍了SPI通信技术--MCU与FLASH实战应用相关内容。希望对您的学习有所帮助。
本文来自于微信公众号汽车电子与软件 ,由火龙果软件Alice编辑、推荐。

一、引 言

1.1 文档背景

串行外设接口(Serial Peripheral Interface,SPI)是由摩托罗拉公司开发的一种全双工同步串行通信接口,广泛应用于微控制器与外围设备之间的数据传输。本文档以MCU与FLASH存储器的通信为实例,深入阐述SPI通信的硬件设计、软件实现和实战应用。

1.2 应用场景

SPI通信在嵌入式系统中具有重要地位,主要应用于:

微控制器与存储器(FLASH、EEPROM)通信

传感器数据采集(温度、压力、加速度传感器)

显示器驱动(LCD、OLED)

ADC/DAC转换器控制

实时时钟(RTC)模块通信

1.3 技术优势

相比其他通信协议,SPI具有以下优势:

高速传输:时钟频率可达数十MHz

全双工通信:同时进行数据收发

硬件简单:仅需4根信号线

灵活性强:支持多从设备级联

二、SPI协议基础理论

2.1 协议架构

SPI采用主从(Master-Slave)架构,一个主设备可以控制多个从设备。通信基于时钟同步机制,数据传输严格按照时钟节拍进行。

2.2 信号线定义

2.3 工作时序

SPI通信的核心是时钟极性(CPOL)和时钟相位(CPHA)的配置:

2.3.1 时钟极性(CPOL)

CPOL=0:空闲状态时SCLK为低电平

CPOL=1:空闲状态时SCLK为高电平

2.3.2 时钟相位(CPHA)

CPHA=0:在SCLK的第一个边沿采样数据

CPHA=1:在SCLK的第二个边沿采样数据

2.3.3 四种工作模式

三、硬件设计方案

3.1 系统架构设计

整个系统由三个核心模块组成:MCU主控制器、SPI通信接口、FLASH存储器。

核心模块说明:

MCU模块:STM32F407作为主控制器,负责整个系统的控制逻辑

SPI模块:四线制通信接口(SCLK/MOSI/MISO/CS),实现高速数据传输

FLASH模块:W25Q64提供8MB非易失性存储空间

3.2 引脚连接方案

3.2.1 MCU端引脚配置(使用SPI1)

3.2.2 FLASH端引脚连接

3.3 电路设计要点

3.3.1 电源设计

Plain Text

VCC (3.3V) ──┬── 100nF ──┬── GND

                        │        

                        └── W25Q64 VCC

3.3.2 信号完整性设计

上拉电阻:CS信号添加10kΩ上拉电阻确保默认高电平

去耦电容:FLASH电源端添加100nF去耦电容

走线阻抗:高速信号线控制在50Ω特征阻抗

走线长度:SPI信号线长度匹配,最大差异<5mm

四、软件架构设计

4.1 系统软件架构

软件架构采用简洁的三层设计:

架构层次说明:

应用层:实现具体的业务功能,如数据记录、查询等

驱动层:封装SPI通信和FLASH操作,提供统一接口

硬件层:基于STM32 HAL库,处理底层硬件操作

4.2 模块设计

4.2.1 SPI驱动模块接口设计

基于STM32 HAL库设计的SPI驱动模块主要包含配置结构体和基础操作接口:


C
// SPI配置结构体typedef struct {uint32_t baudrate; // 波特率
uint8_t mode; // 工作模式 (0-3)uint8_t data_size; // 数据位数 (8/16)
uint8_t cs_pin; // 片选引脚
} spi_config_t;
// SPI驱动接口
HAL_StatusTypeDef SPI_Init(spi_config_t* config);
HAL_StatusTypeDef SPI_Transmit(uint8_t* data, uint16_t size);
HAL_StatusTypeDef SPI_Receive(uint8_t* data, uint16_t size);
HAL_StatusTypeDef SPI_TransmitReceive(uint8_t* tx_data, uint8_t* rx_data, uint16_t size);
HAL_StatusTypeDef SPI_DeInit(void);

接口设计要点:

配置结构体:包含波特率、工作模式、数据位数和片选引脚等关键参数

初始化接口:完成SPI外设和GPIO的配置

数据传输接口:支持发送、接收和双向传输

错误处理机制:提供统一的状态返回和错误处理

4.2.2 FLASH驱动模块接口设计

针对W25Q64设备特性设计的驱动接口:


C
// FLASH设备信息结构typedef struct {uint32_t sector_size; // 扇区大小 (4KB)
uint32_t block_size; // 块大小 (64KB)
uint32_t total_size; // 总容量 (8MB)
uint16_t manufacturer_id; // 制造商ID (0xEF)
uint16_t device_id; // 设备ID (0x4017)
uint8_t unique_id[8]; // 唯一识别码
} w25q64_info_t;
// FLASH操作接口
HAL_StatusTypeDef W25Q64_Init(void);
HAL_StatusTypeDef W25Q64_ReadID(w25q64_info_t* info);
HAL_StatusTypeDef W25Q64_ReadData(uint32_t address, uint8_t* data, uint32_t size);
HAL_StatusTypeDef W25Q64_WritePage(uint32_t address, uint8_t* data, uint32_t size);
HAL_StatusTypeDef W25Q64_EraseSector(uint32_t address);
HAL_StatusTypeDef W25Q64_EraseBlock(uint32_t address);
HAL_StatusTypeDef W25Q64_EraseChip(void);
HAL_StatusTypeDef W25Q64_ReadStatus(uint8_t* status);
uint8_t W25Q64_IsBusy(void);

接口设计要点:

设备信息结构:存储FLASH容量、扇区大小、制造商ID等信息

基础操作接口:包含初始化、读ID、数据读写、扇区擦除等功能

地址管理:支持24位地址空间的完整访问

状态检测:实现忙状态检测和写保护管理

五、驱动程序实现

5.1 SPI通信驱动实现

SPI驱动程序的核心任务就是让MCU和FLASH能够"对话"。就像两个人打电话需要遵循通话礼仪一样,MCU和FLASH的通信也需要遵循SPI协议。

5.1.1 SPI初始化 - 建立通信连接

想象MCU要给FLASH打电话,首先需要:

1. 开通电话线路(使能时钟)

2. 配置电话机(设置GPIO引脚)

3. 约定通话方式(配置SPI参数)


C
#include "stm32f4xx_hal.h"static SPI_HandleTypeDef hspi1; // SPI的"电话机"
HAL_StatusTypeDef SPI_Init(void)
{
// 步骤1:开通"电话线路" - 使能时钟
    __HAL_RCC_GPIOA_CLK_ENABLE(); // GPIO时钟
     __HAL_RCC_SPI1_CLK_ENABLE(); // SPI时钟// 步骤2:连接"电话线" - 配置引脚
    GPIO_InitTypeDef GPIO_InitStruct = {0};
// 数据线配置 (SCK, MISO, MOSI)
    GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽输出
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; // 高速模式
    GPIO_InitStruct.Alternate = GPIO_AF5_SPI1; // 选择SPI1功能
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 片选线配置 (CS)
    GPIO_InitStruct.Pin = GPIO_PIN_4;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 普通输出
    GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉,默认高电平
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 步骤3:设定"通话规则" - 配置SPI参数
    hspi1.Instance = SPI1; // 使用SPI1
    hspi1.Init.Mode = SPI_MODE_MASTER; // MCU是主机
    hspi1.Init.DataSize = SPI_DATASIZE_8BIT; // 每次传8位数据
    hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // 时钟空闲时为低电平
    hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // 第一个边沿采样
    hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; // 速度:42MHz÷4=10.5MHzreturn HAL_SPI_Init(&hspi1); // 启动"电话系统"
}

关键参数解释:

时钟分频器:就像调节通话速度,分频器越大速度越慢但越稳定

极性和相位:决定何时"说话"何时"听话"的时机

片选信号:相当于电话号码,决定和谁通话

5.1.2 数据传输 - 实际的"通话"过程

数据传输就像打电话的过程:

1. 拨号(拉低CS信号)

2. 通话(发送/接收数据)

3. 挂机(拉高CS信号)


C
// 定义"拨号"和"挂机"操作#define FLASH_CALL_START() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) // 拨号
#define FLASH_CALL_END() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) // 挂机// 双向通话 - MCU既要说也要听
    HAL_StatusTypeDef SPI_Chat(uint8_t* send_data, uint8_t* receive_data, uint16_t length)
{
    HAL_StatusTypeDef status;
    FLASH_CALL_START(); // 开始通话// 同时发送和接收数据(全双工通信)
    status = HAL_SPI_TransmitReceive(&hspi1, send_data, receive_data, length, 1000);
    FLASH_CALL_END(); // 结束通话return status;
}
// 单向发送 - 只说不听
HAL_StatusTypeDef SPI_Tell(uint8_t* data, uint16_t length)
{
    HAL_StatusTypeDef status;
    FLASH_CALL_START(); // 拨号
    status = HAL_SPI_Transmit(&hspi1, data, length, 1000); // 发送命令
    FLASH_CALL_END(); // 挂机return status;
}
// 单向接收 - 只听不说
HAL_StatusTypeDef SPI_Listen(uint8_t* data, uint16_t length)
{
    HAL_StatusTypeDef status;
    FLASH_CALL_START(); // 拨号
    status = HAL_SPI_Receive(&hspi1, data, length, 1000); // 接收数据
    FLASH_CALL_END(); // 挂机return status;
}

CS信号:就像电话的拨号/挂机,必须先拨号才能通话

超时设置:1000ms超时,避免程序"卡死"

全双工:SPI可以同时收发,就像面对面聊天

5.2 FLASH存储器驱动实现

FLASH就像一个智能储物柜,MCU需要用特定的"暗号"(命令)才能操作它。

5.2.1 FLASH的"暗号本" - 命令集

就像不同的遥控器按键有不同功能,FLASH也有自己的命令集:


C
// FLASH的"暗号本" - 各种操作命令
#define CMD_READ_ID 0x9F // "你是谁?" - 查看身份证
#define CMD_READ_DATA 0x03 // "把数据给我" - 读取数据
#define CMD_WRITE_ENABLE 0x06 // "我要写东西" - 申请写权限
#define CMD_PAGE_WRITE 0x02 // "存这些数据" - 写数据
#define CMD_SECTOR_ERASE 0x20 // "清空这一区域" - 擦除4KB
#define CMD_CHIP_ERASE 0xC7 // "全部清空" - 擦除整片
#define CMD_READ_STATUS 0x05 // "你在忙吗?" - 查询状态
// FLASH的"状态灯" - 状态寄存器各位含义
#define STATUS_BUSY 0x01 // 忙碌标志:1=正在工作,0=空闲
#define STATUS_WRITE_ENABLE 0x02 // 写使能标志:1=可以写,0=禁止写

命令分类理解:

身份类:READ_ID - 确认FLASH型号,防止认错

读取类:READ_DATA - 从指定地址读数据

写入类:WRITE_ENABLE + PAGE_WRITE - 先申请权限再写数据

擦除类:SECTOR_ERASE, CHIP_ERASE - 删除数据(必须先擦再写)

状态类:READ_STATUS - 查询FLASH是否忙碌

5.2.2 基础操作 - 与FLASH的"日常对话"

实现与FLASH沟通的基本功能,就像日常对话的常用句式:


C
// "你忙吗?" - 等待FLASH空闲static HAL_StatusTypeDef Flash_WaitFree(void)
{
    uint8_t status;
    uint32_t patience = 1000; // 耐心等待1000毫秒do {
// 问FLASH:"你在忙吗?"
        FLASH_CALL_START();
        uint8_t ask_cmd = CMD_READ_STATUS;
        HAL_SPI_Transmit(&hspi1, &ask_cmd, 1, 100);
        HAL_SPI_Receive(&hspi1, &status, 1, 100);
        FLASH_CALL_END();
// 如果FLASH回答"不忙"(BUSY位为0)if (!(status & STATUS_BUSY)) {
            return HAL_OK; // 可以继续操作
        }
        HAL_Delay(1); // 等1毫秒
        patience--;
    } while (patience > 0);
    return HAL_TIMEOUT; // 等太久了,放弃
}
// "我要写东西了" - 申请写权限 static HAL_StatusTypeDef Flash_AskPermission(void)
{
    uint8_t permit_cmd = CMD_WRITE_ENABLE;
    FLASH_CALL_START();
    HAL_StatusTypeDef result = HAL_SPI_Transmit(&hspi1, &permit_cmd, 1, 100);
    FLASH_CALL_END();
    return result;
}
// "你是谁?" - 读取FLASH身份证
HAL_StatusTypeDef Flash_CheckID(void)
{
    uint8_t ask_id_cmd = CMD_READ_ID;
    uint8_t id_card[3]; // FLASH的3字节身份证
    HAL_StatusTypeDef status;
    FLASH_CALL_START();
// 问:"你是谁?"
     status = HAL_SPI_Transmit(&hspi1, &ask_id_cmd, 1, 100);
    if (status != HAL_OK) {
        FLASH_CALL_END();
        return status;
    }
// 听FLASH回答身份
    status = HAL_SPI_Receive(&hspi1, id_card, 3, 100);
    FLASH_CALL_END();
    if (status == HAL_OK) {
// 解读身份证uint8_t manufacturer = id_card[0];
// 厂商IDuint16_t device_type = (id_card[1] << 8) | id_card[2]; // 设备型号// 验证是否为期望的W25Q64if (manufacturer == 0xEF && device_type == 0x4017) {
            printf("找到了W25Q64 FLASH!容量:8MB\n");
            return HAL_OK;
        } else {
            printf("错误:这不是W25Q64芯片\n");
            return HAL_ERROR;
        }
    }
    return status;
}

操作要点:

耐心等待:FLASH在擦除或写入时会忙碌,要耐心等待

先申请权限:写入前必须发送写使能命令

验证身份:通过ID确认芯片型号,避免误操作

5.2.3 数据读写 - 真正的"存取"操作

数据读写就像去银行存取钱,需要准确的地址信息:


C
// "给我地址X的数据" - 读取数据
HAL_StatusTypeDef Flash_ReadData(uint32_t address, uint8_t* buffer, uint32_t size)
{
    HAL_StatusTypeDef status;
     uint8_t command_packet[4]; // 命令包:1字节命令 + 3字节地址// 安全检查:地址不能超出FLASH容量
if (address + size > 0x800000) { // 8MB容量限制printf("错误:读取地址超出范围\n");
        return HAL_ERROR;
    }
// 等待FLASH空闲
    status = Flash_WaitFree();
    if (status != HAL_OK) return status;
// 准备"读取"命令包
    command_packet[0] = CMD_READ_DATA; // 命令:"给我数据"
    command_packet[1] = (address >> 16) & 0xFF; // 地址高字节
    command_packet[2] = (address >> 8) & 0xFF; // 地址中字节
    command_packet[3] = address & 0xFF; // 地址低字节
    FLASH_CALL_START();
// 先说出地址
    status = HAL_SPI_Transmit(&hspi1, command_packet, 4, 1000);
    if (status != HAL_OK) {
        FLASH_CALL_END();
        return status;
    }
// 然后接收数据
    status = HAL_SPI_Receive(&hspi1, buffer, size, 1000);
    FLASH_CALL_END();
    if (status == HAL_OK) {
        printf("成功读取%lu字节,从地址0x%06lX\n", size, address);
    }
    return status;
}

// "把这些数据存到地址X" - 写入数据(页编程)


C
HAL_StatusTypeDef Flash_WriteData(uint32_t address, uint8_t* data, uint32_t size)
{
   HAL_StatusTypeDef status;
   uint8_t command_packet[4]; // 命令包// FLASH写入规则:一次最多写256字节(一页),且不能跨页if (size > 256 || (address & 0xFF) + size > 256) {
      printf("错误:写入数据超过页边界\n");
      return HAL_ERROR;
   }
// 步骤1:等待FLASH空闲
   status = Flash_WaitFree();
   if (status != HAL_OK) return status;
// 步骤2:申请写权限(必须的礼貌)
   status = Flash_AskPermission();
   if (status != HAL_OK) return status;
// 步骤3:准备写入命令包
   command_packet[0] = CMD_PAGE_WRITE; // 命令:"我要写数据"
   command_packet[1] = (address >> 16) & 0xFF; // 目标地址高字节
   command_packet[2] = (address >> 8) & 0xFF; // 目标地址中字节
   command_packet[3] = address & 0xFF; // 目标地址低字节
   FLASH_CALL_START();
// 步骤4:发送命令和地址
   status = HAL_SPI_Transmit(&hspi1, command_packet, 4, 1000);
   if (status != HAL_OK) {
      FLASH_CALL_END();
      return status;
   }
// 步骤5:发送实际数据
   status = HAL_SPI_Transmit(&hspi1, data, size, 1000);
   FLASH_CALL_END();
// 步骤6:等待FLASH完成写入(写入需要时间)if (status == HAL_OK) {
   status = Flash_WaitFree(); // 耐心等待写入完成if (status == HAL_OK) {
          printf("成功写入%lu字节到地址0x%06lX\n", size, address);
        }
    }
    return status;
}

FLASH写入的重要规则:

先擦后写:FLASH只能将1变成0,要写入必须先擦除

页写入:一次最多写256字节,不能跨页

写权限:每次写入前都要申请写使能

耐心等待:写入完成需要几毫秒时间

六、全文总结

本文档介绍了SPI通信技术的完整实现方案:

SPI基础知识:SPI是一种4线制通信协议,包括时钟线(SCLK)、数据输出线(MOSI)、数据输入线(MISO)和片选线(CS)。

硬件连接:使用STM32F407的PA4-PA7引脚连接W25Q64 FLASH芯片,实现MCU与存储器的通信。

软件实现:通过HAL库配置SPI外设,编写驱动函数实现FLASH的读写擦除操作。

实际应用:掌握本文档内容后,可以在项目中灵活使用SPI接口连接各种外设设备。

SPI通信简单可靠,是嵌入式开发中的重要技术。通过理论学习和实际编程,能够快速掌握并应用于实际项目中。

   
9   次浏览       3 次
相关文章

中央计算的软件定义汽车架构设计
汽车电子控制系统中的软件开发过程
一文读懂汽车芯片-有线通信芯片
OTA在汽车上有哪些难点痛点?
相关文档

汽车设计-汽车的整体结构及动力系统
自动驾驶汽车软件计算框架
SysML在汽车领域的应用实践
电子电气架构-大陆汽车系统架构平台
相关课程

AutoSAR原理与实践
功能安全管理体系(基于ISO26262)
MBSE(基于模型的系统工程)
基于SOA的汽车电子架构设计与开发

最新活动计划
嵌入式软件架构设计 12-11[北京]
LLM大模型与智能体开发实战 12-18[北京]
嵌入式软件测试 12-25[北京]
AI原生应用的微服务架构 1-9[北京]
AI大模型编写高质量代码 1-14[北京]
需求分析与管理 1-22[北京]
 
 
最新文章
ASPICE中配置管理是个什么东西?
了解软件安全分析与组件鉴定
掌握Autosar ComStack的精髓!
基于整车功能的正向诊断需求开发
搞定Autosar SWC开发秘籍,码住!
汽车OTA更新的系统性威胁评估
最新课程
基于SOA的汽车电子架构设计与开发
Auto SAR原理与实践
AUTOSAR架构与实践(从CP到 AP )
AUTOSAR架构建模方法与工具(EA)
ASPICE4.0核心开发过程指南
MBSE(基于模型的系统工程)
更多...   
成功案例
某知名车企 AUTOSAR应用设计与开发
吉利汽车 MBSE工程体系汽车建模及评估
某整车企业 《功能需求分析与设计》
富奥汽车零部件 建模工具EA
零跑汽车 建模工具EA及服务
北汽福田 建模工具EA
小鹏汽车 建模工具EA
更多...