STM32F103C8T6 SPI协议完整教程:从入门到精通
适用芯片: STM32F103C8T6
难度等级: 入门到进阶
📋 目录
1. SPI协议基础理论
1.1 什么是SPI?
**SPI(Serial Peripheral Interface,串行外设接口)**是一种高速、全双工、同步的通信总线,由Motorola公司提出,广泛应用于嵌入式系统中与外设(如Flash存储器、传感器、显示屏驱动器等)进行通信。
1.2 SPI协议特点
- 全双工通信: 数据可以同时双向传输
- 高速传输: 传输速度可达几十MHz
- 简单硬件: 只需要4根线(最少3根)
- 主从模式: 采用主从架构
- 同步通信: 使用时钟信号同步数据传输
1.3 SPI信号线定义
SPI协议通常使用以下4根信号线:
| 信号线名称 | 全称 | 方向 | 说明 |
|---|---|---|---|
| MOSI | Master Out Slave In | 主机→从机 | 主机数据输出线 |
| MISO | Master In Slave Out | 主机←从机 | 从机数据输出线 |
| SCK | Serial Clock | 主机→从机 | 同步时钟信号 |
| CS/SS | Chip Select/Slave Select | 主机→从机 | 片选信号(低电平有效) |
1.4 SPI工作原理
主机 (Master) 从机 (Slave)
┌──────────────┐ ┌──────────────┐
│ │ │ │
│ MOSI ─────┼─────────────>│ MOSI │
│ │ │ │
│ MISO │<─────────────┼──── MISO │
│ │ │ │
│ SCK ─────┼─────────────>│ SCK │
│ │ │ │
│ CS ─────┼─────────────>│ CS │
│ │ │ │
└──────────────┘ └──────────────┘
工作流程:
- 主机通过拉低CS信号选通从机
- 主机产生时钟信号SCK
- 在SCK的边沿,主机通过MOSI发送数据给从机
- 同时,从机通过MISO发送数据给主机
- 传输完成后,主机拉高CS信号结束通信
1.5 SPI时序模式
SPI有4种时钟模式(Mode),由CPOL和CPHA决定:
| 模式 | CPOL | CPHA | 时钟极性 | 数据采样 |
|---|---|---|---|---|
| Mode 0 | 0 | 0 | 空闲为低电平 | 上升沿采样 |
| Mode 1 | 0 | 1 | 空闲为低电平 | 下降沿采样 |
| Mode 2 | 1 | 0 | 空闲为高电平 | 下降沿采样 |
| Mode 3 | 1 | 1 | 空闲为高电平 | 上升沿采样 |
注意: 通信双方必须使用相同的时序模式!
2. STM32F103 SPI硬件资源
2.1 STM32F103C8T6 SPI外设概述
STM32F103C8T6共有3个SPI外设:
- SPI1: 高速SPI(最高18MHz)
- SPI2: 标准SPI(最高18MHz)
- SPI3: 标准SPI(不支持直接映射,需重映射)
2.2 SPI引脚资源表
SPI1标准配置:
| 引脚 | 功能 | 重映射功能 |
|---|---|---|
| PA4 | NSS | 保留为NSS |
| PA5 | SCK | SPI1_SCK |
| PA6 | MISO | SPI1_MISO |
| PA7 | MOSI | SPI1_MOSI |
SPI1重映射配置:
| 引脚 | 功能 | 说明 |
|---|---|---|
| PA15 | NSS | 重映射后的片选 |
| PB3 | SCK | 重映射后的时钟 |
| PB4 | MISO | 重映射后的输入 |
| PB5 | MOSI | 重映射后的输出 |
SPI2标准配置:
| 引脚 | 功能 |
|---|---|
| PB12 | NSS |
| PB13 | SCK |
| PB14 | MISO |
| PB15 | MOSI |
2.3 SPI时钟源配置
STM32F103的APB总线时钟配置关系:
系统时钟 → APB2 → SPI1 (最大18MHz)
系统时钟 → APB1 → SPI2/SPI3 (最大18MHz)
波特率计算公式:
SPI速度 = 系统时钟 / 分频系数
3. SPI寄存器详解
3.1 控制寄存器1 (SPIx_CR1)
SPI控制寄存器1关键位说明:
- CPHA: 时钟相位
- CPOL: 时钟极性
- MSTR: 主/从模式选择
- BR: 波特率选择
- SPE: SPI使能
- LSBFIRST: 帧格式(LSB/MSB)
- SSI: 内部从机选择
- SSM: 软件从机管理
- RXONLY: 只接收模式
- DFF: 数据帧格式
- CRCEN: CRC使能
- BIDIMODE: 双向数据模式使能
3.2 状态寄存器 (SPIx_SR)
SPI状态寄存器位说明:
- RXNE: 接收缓冲区非空
- TXE: 发送缓冲区空
- CHSIDE: 通道侧
- UDR: 下溢标志
- CRCERR: CRC错误标志
- MODF: 模式错误
- OVR: 溢出错误
- BSY: 忙标志
3.3 数据寄存器 (SPIx_DR)
双向数据寄存器,根据DFF位配置,可以是8位或16位数据。
4. SPI主从模式配置
4.1 主机模式配置要点
主机模式特点:
- 产生时钟信号
- 控制CS片选信号
- 主动发起通信
配置步骤:
- 设置MSTR位为1
- 配置时钟频率(BR位)
- 配置时序模式(CPOL/CPHA)
- 使能SPI(SPE位)
- 控制NSS信号(硬件或软件)
4.2 从机模式配置要点
从机模式特点:
- 接收主机时钟
- 响应CS信号
- 被动接收/发送数据
配置步骤:
- 设置MSTR位为0
- 配置时序模式(与主机一致)
- 使能SPI(SPE位)
- 配置NSS(通常使用SSM软件模式)
5. 硬件电路连接
5.1 典型连接示例
STM32F103C8T6 (主机) 连接 SPI Flash (从机)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PA4 (NSS) ───────────> CS
PA5 (SCK) ───────────> CLK
PA7 (MOSI) ───────────> DI
PA6 (MISO) <─────────── DO
GND ───────────> GND
3.3V ───────────> VCC
5.2 注意事项
⚠️ 重要提示:
- 电源匹配: 确保主机和从机使用相同的电源电压(通常3.3V)
- 电平匹配: STM32F103工作电压为3.3V,注意电平兼容性
- 上拉电阻: 根据器件需求添加适当的上拉/下拉电阻
- PCB布线: 高速信号线尽量短,避免干扰
5.3 多从机连接
方式一:独立片选(推荐)
主机 (STM32) 多个从机
PA4 (NSS1) ─────────> FLASH1_CS
PA5 (SCK) ─────────> FLASH1_CLK
PA7 (MOSI) ─────────> FLASH1_DI
PA6 (MISO) <───────── FLASH1_DO
PA8 (NSS2) ─────────> FLASH2_CS
PA5 (SCK) ─────────> FLASH2_CLK
PA7 (MOSI) ─────────> FLASH2_DI
PA6 (MISO) <───────── FLASH2_DO
6. 标准库开发实战
6.1 SPI引脚配置
/**
* SPI GPIO初始化配置
* 使用SPI1,标准引脚配置:PA4(NSS), PA5(SCK), PA6(MISO), PA7(MOSI)
*/
void SPI1_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 1. 使能GPIOA和SPI1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE);
// 2. 配置PA5(SCK), PA7(MOSI) 为复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 高速输出
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3. 配置PA6(MISO) 为浮空输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 4. 配置PA4(NSS) 为推挽输出,用于片选控制
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 5. 初始状态:NSS拉高(未选中)
GPIO_SetBits(GPIOA, GPIO_Pin_4);
}
6.2 SPI参数配置
/**
* SPI1初始化配置
* 配置为主机模式,8位数据,Mode0模式
*/
void SPI1_Config(void)
{
SPI_InitTypeDef SPI_InitStructure;
// 初始化SPI1结构体参数
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 主机模式
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8位数据
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // CPOL=0
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // CPHA=0 (Mode0)
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 软件NSS
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_64; // 72MHz/64≈1.125MHz
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // MSB先发
SPI_InitStructure.SPI_CRCPolynomial = 7; // CRC多项式
// 初始化SPI1
SPI_Init(SPI1, &SPI_InitStructure);
// 使能SPI1
SPI_Cmd(SPI1, ENABLE);
}
6.3 片选控制函数
/**
* SPI1片选信号控制
* @param state: 1=选中从机(拉低), 0=释放从机(拉高)
*/
void SPI1_CS_Control(uint8_t state)
{
if(state == 1)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_4); // 拉低,选中从机
}
else
{
GPIO_SetBits(GPIOA, GPIO_Pin_4); // 拉高,释放从机
}
}
6.4 数据收发函数
/**
* SPI1发送一个字节
* @param byte: 要发送的数据
* @return: 接收到的数据
*/
uint8_t SPI1_SendByte(uint8_t byte)
{
// 等待发送缓冲区空
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
// 发送数据
SPI_I2S_SendData(SPI1, byte);
// 等待接收完成
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
// 返回接收到的数据
return SPI_I2S_ReceiveData(SPI1);
}
/**
* SPI1接收一个字节
* @return: 接收到的数据
*/
uint8_t SPI1_ReceiveByte(void)
{
// SPI主机模式下,发送时钟信号,同时接收数据
return SPI1_SendByte(0xFF); // 发送0xFF(无效数据)以产生时钟
}
6.5 主函数测试代码
#include "stm32f10x.h"
// 函数声明
void SPI1_GPIO_Config(void);
void SPI1_Config(void);
void SPI1_CS_Control(uint8_t state);
uint8_t SPI1_SendByte(uint8_t byte);
uint8_t SPI1_ReceiveByte(void);
/**
* 主函数:SPI通信测试
*/
int main(void)
{
uint8_t send_data = 0x55; // 发送的数据
uint8_t recv_data; // 接收的数据
// 1. 系统时钟配置(使用外部8MHz晶振,PLL到72MHz)
SystemInit();
// 2. SPI1初始化
SPI1_GPIO_Config(); // GPIO配置
SPI1_Config(); // SPI配置
while(1)
{
// 选中从机
SPI1_CS_Control(1);
// 发送数据并接收
recv_data = SPI1_SendByte(send_data);
// 释放从机
SPI1_CS_Control(0);
// 延时,便于观察
for(volatile int i = 0; i < 1000000; i++);
}
}
7. HAL库开发实战
7.1 HAL库SPI初始化
#include "main.h"
#include "stm32f1xx_hal.h"
SPI_HandleTypeDef hspi1;
/**
* SPI1初始化函数(HAL库版本)
*/
void MX_SPI1_Init(void)
{
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER; // 主机模式
hspi1.Init.Direction = SPI_DIRECTION_2LINES; // 全双工
hspi1.Init.DataSize = SPI_DATASIZE_8BIT; // 8位数据
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0 (Mode0)
hspi1.Init.NSS = SPI_NSS_SOFT; // 软件NSS
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_64; // 分频系数
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; // MSB先发
hspi1.Init.TIMode = SPI_TIMODE_DISABLE; // 禁用TI模式
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; // 禁用CRC
// 初始化SPI1
if(HAL_SPI_Init(&hspi1) != HAL_OK)
{
Error_Handler(); // 错误处理
}
}
/**
* SPI1底层驱动函数(由HAL库调用)
*/
void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(hspi->Instance == SPI1)
{
// 使能SPI1和GPIOA时钟
__HAL_RCC_SPI1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置PA5(SCK), PA7(MOSI)
GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置PA6(MISO)
GPIO_InitStruct.Pin = GPIO_PIN_6;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置PA4(NSS)为软件控制
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);
// 初始状态:NSS拉高
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
}
}
7.2 HAL库数据收发
/**
* HAL库:SPI发送数据
*/
HAL_StatusTypeDef SPI1_Transmit(uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
// 选中从机
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
// 发送数据
HAL_StatusTypeDef status = HAL_SPI_Transmit(&hspi1, pData, Size, Timeout);
// 释放从机
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
return status;
}
/**
* HAL库:SPI接收数据
*/
HAL_StatusTypeDef SPI1_Receive(uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
// 选中从机
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
// 接收数据
HAL_StatusTypeDef status = HAL_SPI_Receive(&hspi1, pData, Size, Timeout);
// 释放从机
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
return status;
}
/**
* HAL库:SPI发送并接收数据(全双工)
*/
HAL_StatusTypeDef SPI1_TransmitReceive(uint8_t *pTxData, uint8_t *pRxData,
uint16_t Size, uint32_t Timeout)
{
// 选中从机
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
// 发送并接收数据
HAL_StatusTypeDef status = HAL_SPI_TransmitReceive(&hspi1, pTxData, pRxData, Size, Timeout);
// 释放从机
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
return status;
}
8. 高级应用与优化
8.1 16位数据传输
/**
* SPI1配置为16位数据模式
*/
void SPI1_Config_16bit(void)
{
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_16b; // 16位数据
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_64;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
}
8.2 软件模拟SPI
// 定义模拟SPI引脚
#define SIM_SPI_MOSI_PIN GPIO_Pin_7
#define SIM_SPI_MISO_PIN GPIO_Pin_6
#define SIM_SPI_SCK_PIN GPIO_Pin_5
#define SIM_SPI_CS_PIN GPIO_Pin_4
#define SIM_SPI_PORT GPIOA
#define SIM_SPI_MOSI_HIGH() GPIO_SetBits(SIM_SPI_PORT, SIM_SPI_MOSI_PIN)
#define SIM_SPI_MOSI_LOW() GPIO_ResetBits(SIM_SPI_PORT, SIM_SPI_MOSI_PIN)
#define SIM_SPI_MISO_READ() GPIO_ReadInputDataBit(SIM_SPI_PORT, SIM_SPI_MISO_PIN)
#define SIM_SPI_SCK_HIGH() GPIO_SetBits(SIM_SPI_PORT, SIM_SPI_SCK_PIN)
#define SIM_SPI_SCK_LOW() GPIO_ResetBits(SIM_SPI_PORT, SIM_SPI_SCK_PIN)
/**
* 软件SPI发送一个字节(Mode0模式)
*/
uint8_t SoftwareSPI_Transfer(uint8_t data)
{
uint8_t i;
uint8_t recv_data = 0;
for(i = 0; i < 8; i++)
{
// 设置MOSI数据位
if(data & 0x80)
SIM_SPI_MOSI_HIGH();
else
SIM_SPI_MOSI_LOW();
data <<= 1;
// 产生时钟上升沿
SIM_SPI_SCK_HIGH();
// 读取MISO数据位
recv_data <<= 1;
if(SIM_SPI_MISO_READ())
recv_data |= 0x01;
// 产生时钟下降沿
SIM_SPI_SCK_LOW();
}
return recv_data;
}
9. 常见问题与调试技巧
9.1 常见问题排查
问题1: 通信无响应
可能原因:
- 时序模式不匹配
- 引脚配置错误
- 片选信号未正确控制
解决方法:
- 检查时序模式必须一致(CPOL、CPHA)
- 确保引脚配置为复用功能(GPIO_Mode_AF_PP)
- 检查片选信号是否正确控制
问题2: 数据传输错误
可能原因:
- MSB/LSB顺序不匹配
- 数据帧格式不一致
- 波特率设置过高
解决方法:
- 确保MSB/LSB顺序一致
- 确保数据宽度一致(8位或16位)
- 降低波特率进行测试
9.2 调试工具与方法
使用逻辑分析仪
推荐工具:
- Saleae Logic Analyzer
- DSView
- 示波器
检查要点:
- 时钟信号是否正确产生
- MOSI/MISO数据是否符合预期
- 时序模式是否正确
- 片选信号是否正常
使用串口输出调试信息
void SPI_Debug_Print(uint8_t *tx_data, uint8_t *rx_data, uint16_t length)
{
uint16_t i;
printf("SPI Debug Info:\r\n");
printf("TX: ");
for(i = 0; i < length; i++)
{
printf("0x%02X ", tx_data[i]);
}
printf("\r\nRX: ");
for(i = 0; i < length; i++)
{
printf("0x%02X ", rx_data[i]);
}
printf("\r\n");
}
9.3 实际应用案例
案例1: 与SPI Flash通信
/**
* W25Q64 Flash ID读取命令
*/
uint32_t W25Q64_ReadID(void)
{
uint8_t id_data[3];
// 发送读取ID命令(0x9F)
SPI1_CS_Control(1);
SPI1_SendByte(0x9F); // 命令
id_data[0] = SPI1_SendByte(0xFF); // 制造商ID
id_data[1] = SPI1_SendByte(0xFF); // 设备ID高字节
id_data[2] = SPI1_SendByte(0xFF); // 设备ID低字节
SPI1_CS_Control(0);
return (id_data[0] << 16) | (id_data[1] << 8) | id_data[2];
}
📚 总结
本文档详细介绍了STM32F103C8T6的SPI协议通信,包括:
- 理论基础:SPI协议原理、时序模式、信号线定义
- 硬件资源:STM32F103的SPI外设特点和引脚资源
- 寄存器配置:详细解释了关键寄存器的用法
- 实战编程:提供了标准库和HAL库的完整代码示例
- 高级应用:16位数据、软件SPI等高级技巧
- 调试技巧:常见问题排查和性能优化方法
希望本文档能帮助大家快速掌握STM32F103的SPI通信!
写在最后:
如果本文档对您有帮助,欢迎点赞、收藏、转发!
如有问题或建议,欢迎在评论区留言讨论!
祝您学习愉快,开发顺利! 🎉

457

被折叠的 条评论
为什么被折叠?



