SPI_master


前言

用FPGA实现SPI主机


一、SPI 特性

1.1同步

发送数据时钟+发送数据

1.2速率

I2C协议v2.1规定了100K,400K和3.4M三种速率(bps)。

具体到产品中SPI的速率为系统时钟频率的1/2。

1.3全双工

两根数据线

二、数据接口

2.1主从机连接

在这里插入图片描述

2.2传输模式

MODECPLOCPHL
000
101
210
311

二、难点

2.1 难点1:SPI数据是在下降沿变化,上升沿保持

解决思路:对系统时钟产生二分频计数器,既可以指示spi_clk上升下降沿,又可以在spi_clk时钟下降沿改变数据,上升沿数据保持。

2.2 难点2:串并转换

思路:串转并移位寄存器往里填数,并转串移位移位寄存器往外溢数

always@(posedge i_clk)begin
    if(i_rst)
        ro_spi_mosi <= 'd0;
    else if(w_user_active)
        ro_spi_mosi <= i_user_data[cnt];//并转串
    else 
        ro_spi_mosi <= ro_spi_mosi;
end 


always@(posedge ro_spi_clk,posedge i_rst)begin
    if(i_rst)
        user_data <= 'd0;
    else
        user_data <= {user_read_data[6 : 0],i_spi_miso};//串转并
end

3 仿真

简单仿真验证一下逻辑,输出信号下降沿变化,对应从机就是上升沿读取数据,没啥问题,ok
在这里插入图片描述


备注

仅用于记录日常学习,侵权必究

#include "spi_master_server.h" // SPI 头文件 #include <SPI.h> #include "helper.h" #include <Adafruit_SPIDevice.h> // Adafruit SPIDevice 封装类对象 Adafruit_SPIDevice *spi_dev = nullptr; #define SPI_MASTER_ENABLE 1 // 自定义引脚定义(与原从机对应) #define MY_CS 12 #define MY_SCK 13 #define MY_MOSI 14 #define MY_MISO 11 // 超时时间 #define SPI_READ_TIMEROUT 999999 // us // 缓冲区大小:32 字节 static constexpr size_t SPI_BUFFER_SIZE = 16 * 2; static constexpr size_t QUEUE_SIZE = 1; // 保留兼容字段,实际主机不用队列 // 发送/接收缓冲区 static uint8_t spi_tx_buf[SPI_BUFFER_SIZE] = {1, 2, 3, 4, 5, 6, 7, 8}; // 测试数据 static uint8_t spi_rx_buf[SPI_BUFFER_SIZE] = {0}; // 命令发送缓冲区 static uint8_t spi_tx_cmd_buf[SPI_BUFFER_SIZE] = {0}; // 发送缓存配置 #define SPI_TX_CACHE_BUF_LEN 1024 #define SPI_TX_PAGE_BUF_LEN (SPI_BUFFER_SIZE - 3) // 协议头占3字节: 0xAB + len + seq // 用户命令定义 #define SPI_USER_CMD_NULL 0 #define SPI_USER_CMD_READ 1 #define SPI_USER_CMD_WRITE 2 static volatile int spi_current_cmd = 0; // 全局缓存及状态 static volatile uint8_t spi_tx_cache_buf[SPI_TX_CACHE_BUF_LEN] = {0}; static volatile int spi_tx_cache_buf_cnt = 0; static volatile int spi_current_cmd = 0; static volatile int spi_current_cmd_len = 0; // 标志位 static uint8_t spi_send_busy = 0; static uint8_t spi_send_mode = 1; // 默认使用 SPI 发送 int spi_master_data_cache_add(uint8_t *data, uint16_t len) { if (len > 1024) len = 1024; memcpy((void*)spi_tx_cache_buf, data, len); spi_tx_cache_buf_cnt = len; return 0; } void IRAM_ATTR my_pre_transfer_cb() { int page_len = SPI_BUFFER_SIZE - 3; int total_pages = (spi_tx_cache_buf_cnt + page_len - 1) / page_len; int current_page = spi_current_cmd_len; memset(spi_tx_buf, 0, SPI_BUFFER_SIZE); spi_tx_buf[0] = 0xAB; if (current_page >= total_pages) { spi_tx_buf[1] = 0; // no data spi_tx_buf[2] = current_page; return; } int offset = current_page * page_len; int data_len = min(page_len, spi_tx_cache_buf_cnt - offset); spi_tx_buf[1] = data_len; spi_tx_buf[2] = current_page; for (int i = 0; i < data_len; i++) { spi_tx_buf[3 + i] = spi_tx_cache_buf[offset + i]; } spi_current_cmd_len++; } static void spi_master_task(void *pvParameters) { while (1) { if (digitalRead(MY_CS) == LOW) { // 从机请求通信 memset(spi_rx_buf, 0, SPI_BUFFER_SIZE); // 使用 writeThenRead 模拟全双工传输 bool success = spi_dev->write_then_read( spi_tx_buf, SPI_BUFFER_SIZE, // 发送的数据和长度 spi_rx_buf, SPI_BUFFER_SIZE, // 接收缓冲区和长度 0 // 命令/地址字节个数(无) ); if (success) { printf_log_hex("RX from slave", spi_rx_buf, SPI_BUFFER_SIZE); printf_log_hex("TX to slave", spi_tx_buf, SPI_BUFFER_SIZE); if (spi_rx_buf[1] == 0xAA && spi_rx_buf[2] == 0x01) { spi_current_cmd = SPI_USER_CMD_READ; } } else { printf("SPI transfer failed!\n"); } } vTaskDelay(5 / portTICK_PERIOD_MS); } } void spi_master_client_init(void) { #if SPI_MASTER_ENABLE // Step 1: 配置硬件 SPI 总线(HSPISPIClass hspi(HSPI); // 创建 HSPI 实例 hspi.begin(MY_SCK, MY_MISO, MY_MOSI, MY_CS); // 设置引脚 // Step 2: 创建 Adafruit_SPIDevice 对象 spi_dev = new Adafruit_SPIDevice( MY_CS, // CS pin 1000000, // 频率 1MHz SPI_BITORDER_MSBFIRST, SPI_MODE3, // CPOL=1, CPHA=1 &hspi // 使用自定义 SPIClass ); if (!spi_dev->begin()) { printf("SPI Device init failed!\n"); return; } // 创建任务 xTaskCreate(spi_master_task, "spi_master_task", 8192, NULL, 2, NULL); #endif } 在主函数调用该代码
09-26
你提供的代码是一个 **ESP-IDF 或 FreeRTOS 环境下的 SPI 主机(Master)驱动程序**,使用了 `Adafruit_SPIDevice` 库来封装底层 SPI 操作,并通过一个任务(`spi_master_task`)轮询从机是否拉低 CS 引脚以发起通信。整个逻辑是:**当从机拉低 CS 时,主机进行一次全双工传输(writeThenRead),发送缓存数据并接收响应**。 --- ### ✅ 目标 你想知道如何在 **主函数中调用这段代码**,让其正常运行。 我们将: 1. 完整解释关键部分; 2. 给出可以在 ESP-IDF 或 Arduino-ESP32 环境中使用的完整示例(主函数调用方式); 3. 提供必要的补充说明和潜在问题修复建议。 --- ## ✅ 一、代码核心功能解析 | 功能 | 说明 | |------|------| | `spi_master_client_init()` | 初始化 SPI 主机设备,创建任务 | | `spi_master_task()` | 轮询 CS 引脚,检测从机请求,执行 SPI 通信 | | `my_pre_transfer_cb()` | 在每次发送前准备数据包(分页发送) | | `spi_master_data_cache_add()` | 将用户数据写入待发送缓存 | > ⚠️ 注意:当前 `my_pre_transfer_cb` 是预处理回调,但并未被自动调用 —— 需手动触发或集成进流程。 但在你的代码中,`my_pre_transfer_cb()` 实际上没有注册给 `Adafruit_SPIDevice`,所以它不会自动执行! --- ## ✅ 二、修正与完善:添加 pre-transfer 回调支持 `Adafruit_SPIDevice` 支持设置前置回调函数(用于设置 TX 缓冲区),我们需要正确绑定它。 ```cpp // 修改 spi_master_client_init() 中的 spi_dev 创建后加入: spi_dev->setCallback(my_pre_transfer_cb); ``` 否则 `my_pre_transfer_cb()` 不会被调用。 --- ## ✅ 三、主函数调用示例(适用于 ESP-IDF) 如果你使用的是 **ESP-IDF + C++** 环境(推荐 IDF 版本 ≥ v4.4): ```cpp // main.cpp extern "C" void app_main(void) { // 初始化串口日志 std::cout.sync_with_stdio(true); printf("SPI Master Starting...\n"); // 初始化 SPI 主机 spi_master_client_init(); // 添加测试数据到缓存 uint8_t test_data[] = "Hello from SPI Master!"; spi_master_data_cache_add(test_data, strlen((char*)test_data)); // 主循环什么都不做,任务已由 FreeRTOS 托管 while (true) { vTaskDelay(pdMS_TO_TICKS(1000)); printf("System alive...\n"); } } ``` --- ## ✅ 四、Arduino-ESP32 环境中的调用方式(更常见) 如果你是在 Arduino IDE 或 PlatformIO 使用 ESP32: ```cpp void setup() { Serial.begin(115200); delay(1000); Serial.println("SPI Master Start"); // 初始化 SPI Master spi_master_client_init(); // 添加要发送的数据 uint8_t test_data[] = "Hello Slave via SPI!"; spi_master_data_cache_add(test_data, strlen((char*)test_data)); } void loop() { // 可以定期更新数据 static int cnt = 0; if (++cnt % 10 == 0) { char buf[64]; sprintf(buf, "Counter: %d", cnt); spi_master_data_cache_add((uint8_t*)buf, strlen(buf)); } delay(1000); } ``` --- ## ✅ 五、关键问题修复建议 ### 🔧 1. `my_pre_transfer_cb()` 没有被注册! 必须在初始化后显式设置回调: ```cpp // 在 spi_master_client_init() 函数内,new spi_dev 后添加: if (!spi_dev->begin()) { printf("SPI Device init failed!\n"); return; } // 注册发送前回调 spi_dev->setCallback(my_pre_transfer_cb); // <<< 关键! ``` 否则 `spi_tx_buf` 始终是旧数据。 --- ### 🔧 2. `printf_log_hex()` 是什么? 你用了这个函数打印 HEX 数据,但它不是标准函数。你需要自己实现: ```cpp void printf_log_hex(const char* tag, const uint8_t* buf, int len) { printf("%s: ", tag); for (int i = 0; i < len; i++) { printf("%02X ", buf[i]); } printf("\n"); } ``` 记得声明在头文件或上方。 --- ### 🔧 3. `IRAM_ATTR` 的注意事项 `my_pre_transfer_cb()` 加了 `IRAM_ATTR`,意味着它必须放在 IRAM 中(中断上下文调用)。确保其调用的所有函数也都在 IRAM 或 ROM 中。 避免在其中调用 `printf`, `malloc`, `memset` 等非 IRAM-safe 函数。 👉 替换 `memset(spi_tx_buf, 0, ...)` 为 `bzero(spi_tx_buf, ...)` 或确认编译器将其优化为 IRAM 安全版本。 或者移除 `IRAM_ATTR`(如果只是普通发送前回调而非中断级)。 --- ### 🔧 4. CS 引脚方向配置 虽然你用 `digitalRead(MY_CS)` 判断从机是否请求,这意味着 **CS 是输入引脚(从机控制)** —— 这其实是“被动模式”,不符合典型 SPI 主机行为。 通常: - 主机控制 CS 输出 → 主动发起通信 - 你现在是等待从机拉低 CS → 属于 **从机发起请求,主机响应** 这类似于 **SPI Slave Request Protocol**,可以工作,但需注意: - `MY_CS` 必须配置为输入 - 上拉电阻建议开启 添加如下代码到 `spi_master_client_init()`: ```cpp pinMode(MY_CS, INPUT_PULLUP); // 从机主动拉低 ``` --- ## ✅ 六、最终精简版可运行结构总结 ```cpp // spi_master_server.h #ifndef SPI_MASTER_SERVER_H #define SPI_MASTER_SERVER_H void spi_master_client_init(void); int spi_master_data_cache_add(uint8_t *data, uint16_t len); #endif ``` ```cpp // spi_master_server.cpp #include "spi_master_server.h" #include <SPI.h> #include "helper.h" #include <Adafruit_SPIDevice.h> // ... (其余定义保持不变) // 必须实现这个日志函数 void printf_log_hex(const char* tag, const uint8_t* buf, int len) { printf("%s: ", tag); for (int i = 0; i < len; i++) { printf("%02X ", buf[i]); } printf("\n"); } // ... (其他变量定义) void IRAM_ATTR my_pre_transfer_cb() { int page_len = SPI_BUFFER_SIZE - 3; int total_pages = (spi_tx_cache_buf_cnt + page_len - 1) / page_len; int current_page = spi_current_cmd_len; bzero((void*)spi_tx_buf, SPI_BUFFER_SIZE); // IRAM-safe spi_tx_buf[0] = 0xAB; if (current_page >= total_pages) { spi_tx_buf[1] = 0; spi_tx_buf[2] = current_page; return; } int offset = current_page * page_len; int data_len = min(page_len, spi_tx_cache_buf_cnt - offset); spi_tx_buf[1] = data_len; spi_tx_buf[2] = current_page; for (int i = 0; i < data_len; i++) { spi_tx_buf[3 + i] = spi_tx_cache_buf[offset + i]; } spi_current_cmd_len++; } static void spi_master_task(void *pvParameters) { while (1) { if (digitalRead(MY_CS) == LOW) { memset(spi_rx_buf, 0, SPI_BUFFER_SIZE); bool success = spi_dev->write_then_read( spi_tx_buf, SPI_BUFFER_SIZE, spi_rx_buf, SPI_BUFFER_SIZE, 0 ); if (success) { printf_log_hex("RX from slave", spi_rx_buf, SPI_BUFFER_SIZE); printf_log_hex("TX to slave", spi_tx_buf, SPI_BUFFER_SIZE); if (spi_rx_buf[1] == 0xAA && spi_rx_buf[2] == 0x01) { spi_current_cmd = SPI_USER_CMD_READ; } } else { printf("SPI transfer failed!\n"); } } vTaskDelay(5 / portTICK_PERIOD_MS); } } void spi_master_client_init(void) { #if SPI_MASTER_ENABLE pinMode(MY_CS, INPUT_PULLUP); // 注意:监听从机拉低 CS SPIClass hspi(HSPI); hspi.begin(MY_SCK, MY_MISO, MY_MOSI, MY_CS); spi_dev = new Adafruit_SPIDevice( MY_CS, 1000000, SPI_BITORDER_MSBFIRST, SPI_MODE3, &hspi ); if (!spi_dev->begin()) { printf("SPI Device init failed!\n"); return; } spi_dev->setCallback(my_pre_transfer_cb); // <<< 注册回调! xTaskCreate(spi_master_task, "spi_master_task", 8192, NULL, 2, NULL); #endif } ``` --- ## ✅ 七、结论:如何调用? 只要你在主程序中调用: ```cpp spi_master_client_init(); // 启动 SPI 主机服务 spi_master_data_cache_add(data, len); // 写入要发送的数据 ``` 然后从机会在拉低 CS 时收到分片数据包(每包最多 13 字节有效载荷,协议头 3 字节)。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

徕卡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值