| 编辑推荐: |
本文主要介绍了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通信简单可靠,是嵌入式开发中的重要技术。通过理论学习和实际编程,能够快速掌握并应用于实际项目中。 |