2026/1/17 4:15:49
网站建设
项目流程
网站开发团队架构,快懂百科登录入口,国外域名。国内网站,新兴县做网站的目录 一、模拟SPI核心原理#xff08;模式0#xff09;
二、前期准备
1. 硬件材料
2. 软件环境
三、核心实现#xff1a;代码编写与解析
1. 通用引脚定义#xff08;主机与从机一致#xff09;
2. 主机代码实现
3. 从机代码实现
四、硬件连接步骤
五、测试与调试…目录一、模拟SPI核心原理模式0二、前期准备1. 硬件材料2. 软件环境三、核心实现代码编写与解析1. 通用引脚定义主机与从机一致2. 主机代码实现3. 从机代码实现四、硬件连接步骤五、测试与调试1. 程序上传2. 查看通信结果六、常见问题与解决方案1. 时序同步错位核心问题2. 引脚选择不当3. 缺少共地或接触不良4. 从机进入死循环在嵌入式开发中SPI串行外设接口是常用的同步通信协议广泛用于传感器、存储芯片、显示屏等外设的通信。ESP32自带硬件SPI外设但在某些场景下如硬件SPI引脚被占用、需要自定义通信引脚模拟SPI软件SPI就成为了刚需。本文将详细讲解在Arduino环境下如何实现ESP32模拟SPI的主机与从机通信包含完整代码、硬件连接、时序分析及常见问题解决。一、模拟SPI核心原理模式0SPI通信有4种模式由时钟极性CPOL和时钟相位CPHA定义本文选择最常用的模式0CPOL0CPHA0其核心时序规则如下时钟空闲状态SCLK为低电平CPOL0数据采样时机SCLK的上升沿第一个时钟沿采样数据数据更新时机SCLK的下降沿第二个时钟沿更新数据通信逻辑主机产生SCLK时钟和CS片选信号通过MOSI发送数据从机同步通过MISO返回数据全双工通信。模拟SPI的本质是通过软件控制GPIO口的电平变化模拟上述时序无需依赖硬件SPI外设灵活性更高但通信速度低于硬件SPI。二、前期准备1. 硬件材料ESP32开发板 2块1块作为主机1块作为从机杜邦线 若干至少4根用于连接SCLK、MOSI、MISO、CSUSB数据线 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 主机模式0CPOL0CPHA0 */ #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 从机模式0CPOL0CPHA0 * 引脚需与主机一一对应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从机信号说明GPIO18SCLKGPIO9SCLK时钟信号主机→从机GPIO23MOSIGPIO10MOSI主机发送→从机接收GPIO19MISOGPIO11MISO从机发送→主机接收GPIO5CSGPIO12CS片选信号主机→从机GNDGND共地必须连接否则电平不稳定注意主机和从机必须共地否则会因电平参考点不一致导致通信失败五、测试与调试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拉高则直接返回本文代码已添加此保护。