Skip to content

SPI的高级用法

在实际的开发中,SPI有很多复杂的用法,用好这些用法,可以更加方便的进行开发。

一. 指定发送数据之前/之后的操作

在进行数据发送之前和之后,可以通过设置一个回调函数的方式。自动执行一个动作。比如在发送之前,将某个引脚设置为高或是低电平,这个过程 ,是自动进行了,不需要每次发送时手动操作。

只需要在 spi_bus_add_device函数中,指定这个回调就可以了

c
spi_device_interface_config_t devcfg = {
        .clock_speed_hz = 10 * 1000 * 1000,     //Clock out at 10 MHz
        .mode = 0,                              //SPI mode 0
        .spics_io_num = PIN_NUM_CS,             //CS pin
        .queue_size = 7,                        //We want to be able to queue 7 transactions at a time
        .pre_cb = lcd_spi_pre_transfer_callback, //Specify pre-transfer callback to handle D/C line
        .post_cb = lcd_spi_post_transfer_callback,//Callback to be called after a transmission has completed.
    };
ret = spi_bus_add_device(LCD_HOST, &devcfg, &spi);

这里面的 lcd_spi_pre_transfer_callbacklcd_spi_post_transfer_callback 就是这两个函数。 这两个函数的原型可以这么写:

c
void lcd_spi_pre_transfer_callback(spi_transaction_t *t)
{
    int dc = (int)t->user;
    gpio_set_level(PIN_NUM_DC, dc);
}

这个函数的参数里面有一个 spi_transaction_t *t 可以据此引用spi传输的参数的内容。

1.说明 IRAM:

这里需要说明一下,在使用这两个回调时:此回调函数在中断上下文中调用,为了获得最佳性能,应放在IRAM中 解释一下,就是说这两个回调函数是放在中断里面执行的,为了加快处理的速度(因为中断中不能长时间处于中断中),所以最好将代码放至 iRAM中。

IRAM 即 Instruction RAM(指令随机存取存储器),在 ESP 系列芯片中,它是一种特殊的内存区域,用于存储中断服务程序(ISR)等对执行速度要求较高的代码。与普通的内存相比,访问 IRAM 中的代码速度更快,因为它不需要从外部闪存中读取指令,避免了因闪存读取延迟而导致的性能问题。类似于CPU中的“一级缓存”的概念。

如何将代码放置于IRAM中呢?

使用 IRAM_ATTR 宏

c
// 定义一个放置在 IRAM 中的回调函数
#include "esp_attr.h"

void IRAM_ATTR my_pre_cb(spi_transaction_t *trans) {
    // 回调函数的具体实现
    // ...
}

注意要引用头文件。 在函数前面加上 IRAM_ATTR 就可以将这个函数放置于IRAM段中了。

2.扩展 DRAM:

IRAM_ATTR类似的还有一个 宏 DRAM_ATTR 指令。

在ESP-IDF(Espressif IoT Development Framework)中,DRAM_ATTR 是一个宏,其作用是将变量或函数放置到数据随机存取存储器(Data Random Access Memory,DRAM)中。在ESP32等芯片中,代码和数据默认会被放置在不同的内存区域,例如代码通常存储在闪存(Flash)中,而数据可能会被放置在不同类型的内存中,这取决于芯片的架构和配置。

为什么需要DRAM_ATTR

  1. DMA 访问需求:直接内存访问(Direct Memory Access,DMA)是一种允许外部设备(如SPI、I2S等)直接与内存进行数据传输的机制,无需CPU的干预,这样可以提高数据传输的效率。但是,DMA通常只能访问特定的内存区域,如DRAM。因此,如果要使用DMA进行数据传输,相关的数据缓冲区必须位于DMA可以访问的内存区域,即DRAM中。
  2. 实时性要求:在某些对实时性要求较高的场景中,将数据放置在DRAM中可以减少访问延迟,因为DRAM的访问速度通常比闪存快。

如何使用DRAM_ATTR

在定义一些固定常量类型的变量时。直接添加这个宏就可以了

c
DRAM_ATTR static const lcd_init_cmd_t ili_init_cmds[] = {
    /* Power control B, power control = 0, DC_ENA = 1 */
    {0xCF, {0x00, 0x83, 0X30}, 3},
    .. 其它代码 ...
}

二. SPI的传输方式

1. 轮询传输(同步传输)

spi_device_polling_transmit

在 spi_device_polling_transmit 函数名里,polling 表示“轮询”的意思。在计算机编程和硬件交互领域,轮询是一种用于监控或等待某个事件发生的技术。下面详细解释 spi_device_polling_transmit 函数里 polling 的含义:

  • 持续检查状态:当调用 spi_device_polling_transmit 函数时,它会持续检查SPI设备的状态,直到数据传输完成。具体来说,在函数内部,程序会不断地询问SPI硬件设备是否已经完成了指定的数据传输任务,这个不断询问的过程就是轮询。
  • 阻塞执行:在轮询过程中,调用该函数的线程会被阻塞,也就是说CPU在这个时间段内无法去执行其他任务,只能等待SPI传输操作完成。只有当传输结束,函数才会返回,程序才会继续执行后续的代码。
  • 特别注意:由于轮询会阻塞线程,所以在使用 spi_device_polling_transmit 函数时,需要在传输之前先要获取总线的使用权,否则有可能会被其它的任务打乱传输顺序,造成系统崩溃。
c
    //获取总线使用权
    spi_device_acquire_bus(spi, portMAX_DELAY);
    // 传输数据
    esp_err_t ret = spi_device_polling_transmit(spi, &t);
    // 释放总线使用权
    spi_device_release_bus(spi);

2. SPI的异步传输

使用 spi_device_queue_trans 和 spi_device_get_trans_result 组合的异步传输方式。使用第1个函数启动传输后,不用管它了,他在后台自动传输。可以继续执行顺序的其它的代码。 当需要结果的时候,就可以执行 第2个函数。 这个函数有可以阻塞,如果有结果立刻返回,如果还没有传输完成,那就阻塞等待结果。

这个函数在执行时,只是将要传输的数据加入到了队列(queue)中,然后 ESP-IDF 会在后台自动进行数据传输,框架会自动处理SPI接口的冲突的问题,所以我们不需要担心多个设备同时访问SPI接口的问题,也就不需要获取总线使用权。

c
// 将传输事务添加到队列中
    esp_err_t ret = spi_device_queue_trans(spi, &trans, portMAX_DELAY);
     // 此时,SPI传输在后台进行,CPU可以执行其他任务
    // 例如,这里可以进行一些计算或者其他操作
    for (int i = 0; i < 10000; i++) {
        // 模拟一些计算任务
    }
    // 获取传输结果
    spi_transaction_t *rtrans;
    ret = spi_device_get_trans_result(spi, &rtrans, portMAX_DELAY);

3.SPI的中断传输

中断方式是当 SPI 传输完成时,硬件会触发一个中断信号,通知 CPU 传输已完成。CPU 在接收到中断信号后,会暂停当前正在执行的任务,跳转到中断服务程序(ISR)中处理传输完成的事件。

c
// 中断服务程序
void IRAM_ATTR spi_transfer_complete_isr(void* arg) {
    transfer_complete = true;
    // 可以在这里添加更多的处理逻辑,如清除中断标志等
}

    // 定义一个SPI传输事务
    spi_transaction_t trans;
    memset(&trans, 0, sizeof(trans));

    // 假设要发送的数据
    uint8_t tx_data[] = {0x01, 0x02, 0x03, 0x04};
    trans.length = sizeof(tx_data) * 8; // 数据长度,单位为位
    trans.tx_buffer = tx_data;

    // 配置SPI设备的中断
    configure_spi_interrupt(spi);
    // 开始SPI传输
    esp_err_t ret = spi_device_transmit(spi, &trans);

   // 这样就可以了,传输完成后,自动执行 spi_transfer_complete_isr 函数的内容

    // 或者等待传输完成
    while (!transfer_complete) {
        // 可以在这里添加一些延时或者其他任务
        vTaskDelay(pdMS_TO_TICKS(10));
    }