中山市网站建设_网站建设公司_响应式开发_seo优化
2025/12/18 18:53:44 网站建设 项目流程

目录

一、模拟SPI核心原理(模式0)

二、前期准备

1. 硬件材料

2. 软件环境

三、核心实现:代码编写与解析

1. 通用引脚定义(主机与从机一致)

2. 主机代码实现

3. 从机代码实现

四、硬件连接步骤

五、测试与调试

1. 程序上传

2. 查看通信结果

六、常见问题与解决方案

1. 时序同步错位(核心问题)

2. 引脚选择不当

3. 缺少共地或接触不良

4. 从机进入死循环


在嵌入式开发中,SPI(串行外设接口)是常用的同步通信协议,广泛用于传感器、存储芯片、显示屏等外设的通信。ESP32自带硬件SPI外设,但在某些场景下(如硬件SPI引脚被占用、需要自定义通信引脚),模拟SPI(软件SPI)就成为了刚需。本文将详细讲解在Arduino环境下,如何实现ESP32模拟SPI的主机与从机通信,包含完整代码、硬件连接、时序分析及常见问题解决。


一、模拟SPI核心原理(模式0)

SPI通信有4种模式,由时钟极性(CPOL)和时钟相位(CPHA)定义,本文选择最常用的模式0(CPOL=0,CPHA=0),其核心时序规则如下:

  • 时钟空闲状态:SCLK为低电平(CPOL=0);

  • 数据采样时机:SCLK的上升沿(第一个时钟沿)采样数据;

  • 数据更新时机:SCLK的下降沿(第二个时钟沿)更新数据;

  • 通信逻辑:主机产生SCLK时钟和CS片选信号,通过MOSI发送数据,从机同步通过MISO返回数据(全双工通信)。

模拟SPI的本质是通过软件控制GPIO口的电平变化,模拟上述时序,无需依赖硬件SPI外设,灵活性更高,但通信速度低于硬件SPI。


二、前期准备

1. 硬件材料

  • ESP32开发板 2块(1块作为主机,1块作为从机);

  • 杜邦线 若干(至少4根,用于连接SCLK、MOSI、MISO、CS);

  • USB数据线 2根(用于给开发板供电和上传程序);

  • 面包板 1块(可选,用于规范接线,避免接触不良)。

2. 软件环境

Arduino IDE(已安装ESP32开发板支持包,若未安装,可参考官方指南:https://docs.espressif.com/projects/arduino-esp32/en/latest/installing.html)。


三、核心实现:代码编写与解析

本次实现“主机发送固定数据0x54,从机接收后返回固定数据0x77”的全双工通信,引脚定义优先选择ESP32通用GPIO(避免使用SDIO复用引脚,减少电平干扰)。

1. 通用引脚定义(主机与从机一致)

选择4个通用GPIO口,对应关系如下(可根据实际需求调整,但需保证主机与从机一一对应):

#define SPI_SCLK 9 // 串行时钟引脚 #define SPI_MOSI 10 // 主机输出从机输入引脚 #define SPI_MISO 11 // 主机输入从机输出引脚 #define SPI_CS 12 // 片选引脚(低有效)

2. 主机代码实现

主机核心功能:初始化GPIO口、产生SCLK时钟和CS信号、通过MOSI发送数据、通过MISO接收从机数据。

/* * ESP32 模拟 SPI 主机(模式0:CPOL=0,CPHA=0) */ #define SPI_SCLK 9 // 串行时钟引脚 #define SPI_MOSI 10 // 主机输出从机输入引脚 #define SPI_MISO 11 // 主机输入从机输出引脚 #define SPI_CS 12 // 片选引脚(低有效) #define CPOL 0 // 时钟极性:空闲低 #define CPHA 0 // 时钟相位:上升沿采样 void spi_soft_init(); uint8_t spi_soft_transfer(uint8_t tx_data); void setup() { Serial.begin(115200); spi_soft_init(); Serial.println("SPI主机初始化完成!"); } void loop() { uint8_t send_data = 0x01; // 测试数据 uint8_t recv_data = spi_soft_transfer(send_data); Serial.printf("主机发送:0x%02X,主机接收(从机返回):0x%02X\n", send_data, recv_data); delay(1000); } // 主机SPI初始化 void spi_soft_init() { pinMode(SPI_SCLK, OUTPUT); pinMode(SPI_MOSI, OUTPUT); pinMode(SPI_MISO, INPUT); pinMode(SPI_CS, OUTPUT); digitalWrite(SPI_CS, HIGH); digitalWrite(SPI_SCLK, CPOL); digitalWrite(SPI_MOSI, LOW); } // 主机全双工传输一个字节 uint8_t spi_soft_transfer(uint8_t tx_data) { uint8_t rx_data = 0; digitalWrite(SPI_CS, LOW); // 选中从机 // 增加短暂延时,确保CS拉低后从机有时间响应 delayMicroseconds(1); for (int i = 7; i >= 0; i--) { // 【关键】先输出MOSI数据,等待电平稳定(提前于SCLK上升沿) digitalWrite(SPI_MOSI, (tx_data >> i) & 0x01); delayMicroseconds(1); // 电平稳定延时 // 模式0:上升沿采样,先拉SCLK高 digitalWrite(SPI_SCLK, HIGH); delayMicroseconds(1); // 等待从机响应并稳定MISO电平 // 【关键】延时后再采样MISO,避免时序错位 rx_data |= (digitalRead(SPI_MISO) << i); // 拉低SCLK,恢复空闲电平 digitalWrite(SPI_SCLK, LOW); delayMicroseconds(1); // 可选:增加延时降低时钟速度 } digitalWrite(SPI_CS, HIGH); // 取消选中从机 return rx_data; }

3. 从机代码实现

从机核心功能:初始化GPIO口、持续监听CS信号(等待主机选中)、同步主机SCLK时钟、采样MOSI数据(接收主机数据)、通过MISO返回数据。

/* * ESP32 模拟 SPI 从机(模式0:CPOL=0,CPHA=0) * 引脚需与主机一一对应:SCLK/MOSI/CS 为输入,MISO 为输出 */ #define SPI_SCLK 9 // 主机产生的时钟,从机作为输入 #define SPI_MOSI 10 // 主机输出的数据,从机作为输入 #define SPI_MISO 11 // 从机输出的数据,主机作为输入 #define SPI_CS 12 // 主机的片选,从机作为输入 #define CPOL 0 #define CPHA 0 uint8_t slave_recv_data = 0; // 从机接收的主机数据 uint8_t slave_send_data = 0x02; // 从机要返回给主机的数据 void spi_slave_init(); void spi_slave_listen(); // 从机监听主机的通信请求 void setup() { Serial.begin(115200); spi_slave_init(); Serial.println("SPI从机初始化完成!"); } void loop() { spi_slave_listen(); // 从机持续监听主机的通信 } // 从机SPI初始化 void spi_slave_init() { pinMode(SPI_SCLK, INPUT); pinMode(SPI_MOSI, INPUT); pinMode(SPI_MISO, OUTPUT); pinMode(SPI_CS, INPUT); digitalWrite(SPI_MISO, LOW); // 初始化MISO电平 } // 从机核心:监听主机的CS和SCLK,完成数据接收和发送 void spi_slave_listen() { // 等待主机拉低CS(选中当前从机) if (digitalRead(SPI_CS) != LOW) { slave_recv_data = 0; digitalWrite(SPI_MISO, LOW); // 空闲时MISO拉低 return; } uint8_t recv_data = 0; uint8_t send_data = slave_send_data; // 从机要发送的预存数据 // 逐位处理主机的时钟和数据 for (int i = 7; i >= 0; i--) { // 【关键】在等待SCLK上升沿前,先输出当前位到MISO(提前准备数据) digitalWrite(SPI_MISO, (send_data >> i) & 0x01); delayMicroseconds(1); // 电平稳定延时 // 等待主机产生SCLK上升沿(模式0:上升沿采样) while (digitalRead(SPI_SCLK) != HIGH) { if (digitalRead(SPI_CS) == HIGH) { // 防止CS意外拉高导致死循环 return; } } // 上升沿:从机采样MOSI(接收主机的数据) recv_data |= (digitalRead(SPI_MOSI) << i); delayMicroseconds(1); // 可选:稳定延时 // 等待主机拉低SCLK(恢复空闲电平) while (digitalRead(SPI_SCLK) != LOW) { if (digitalRead(SPI_CS) == HIGH) { // 防止CS意外拉高导致死循环 return; } } } // 通信完成:更新从机的接收数据,并打印调试 slave_recv_data = recv_data; Serial.printf("从机接收主机数据:0x%02X,从机发送给主机数据:0x%02X\n", slave_recv_data, send_data); // 可选:从机发送数据自增,用于测试连续通信 // slave_send_data++; }

四、硬件连接步骤

按照以下对应关系连接主机和从机的GPIO口,确保接线牢固(面包板连接需注意杜邦线接触良好):

ESP32主机

ESP32从机

信号说明

GPIO18(SCLK)

GPIO9(SCLK)

时钟信号(主机→从机)

GPIO23(MOSI)

GPIO10(MOSI)

主机发送→从机接收

GPIO19(MISO)

GPIO11(MISO)

从机发送→主机接收

GPIO5(CS)

GPIO12(CS)

片选信号(主机→从机)

GND

GND

共地(必须连接,否则电平不稳定)

注意:主机和从机必须共地,否则会因电平参考点不一致导致通信失败!

五、测试与调试

1. 程序上传

  • 将主机代码上传到其中一块ESP32开发板;

  • 将从机代码上传到另一块ESP32开发板;

  • 上传完成后,给两块开发板通电。

2. 查看通信结果

打开Arduino IDE的“串口监视器”,分别选择两块开发板的串口(波特率设为115200),正常通信时应显示以下日志:

主机日志:

从机日志:


六、常见问题与解决方案

在实际测试中,最常见的问题是“从机能接收主机数据,但主机接收从机数据错误(如0xBB、0xFF)”,以下是核心问题及解决方法:

1. 时序同步错位(核心问题)

问题原因:主机在SCLK上升沿瞬间采样MISO,但从机未及时输出数据(从机在上升沿后才更新MISO电平),导致主机采样到旧电平。

解决方案:从机需在SCLK上升沿前输出MISO数据,主机采样前增加短暂延时(如delayMicroseconds(1)),确保电平稳定。本文提供的代码已优化此问题。

2. 引脚选择不当

问题原因:ESP32的GPIO9、10、11、12等属于SDIO复用引脚,若用于模拟SPI,可能因硬件复用导致电平异常。

解决方案:优先选择通用GPIO(如18、19、23、5、4等),避免使用SDIO、UART等复用引脚。

3. 缺少共地或接触不良

问题原因:主机与从机未共地,电平参考点不一致;面包板或杜邦线接触不良,导致信号丢失。

解决方案:确保主机和从机的GND相连;检查杜邦线是否插紧,必要时更换杜邦线或面包板。

4. 从机进入死循环

问题原因:从机的while循环(等待SCLK沿)中,若CS意外拉高,会一直循环无法退出。

解决方案:在从机的while循环中增加CS状态判断,若CS拉高则直接返回(本文代码已添加此保护)。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询