SPI的高级用法
在实际的开发中,SPI有很多复杂的用法,用好这些用法,可以更加方便的进行开发。
一. 指定发送数据之前/之后的操作
在进行数据发送之前和之后,可以通过设置一个回调函数的方式。自动执行一个动作。比如在发送之前,将某个引脚设置为高或是低电平,这个过程 ,是自动进行了,不需要每次发送时手动操作。
只需要在 spi_bus_add_device
函数中,指定这个回调就可以了
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_callback
和 lcd_spi_post_transfer_callback
就是这两个函数。 这两个函数的原型可以这么写:
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
宏
// 定义一个放置在 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
- DMA 访问需求:直接内存访问(Direct Memory Access,DMA)是一种允许外部设备(如SPI、I2S等)直接与内存进行数据传输的机制,无需CPU的干预,这样可以提高数据传输的效率。但是,DMA通常只能访问特定的内存区域,如DRAM。因此,如果要使用DMA进行数据传输,相关的数据缓冲区必须位于DMA可以访问的内存区域,即DRAM中。
- 实时性要求:在某些对实时性要求较高的场景中,将数据放置在DRAM中可以减少访问延迟,因为DRAM的访问速度通常比闪存快。
如何使用DRAM_ATTR
在定义一些固定常量类型的变量时。直接添加这个宏就可以了
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
函数时,需要在传输之前先要获取总线的使用权,否则有可能会被其它的任务打乱传输顺序,造成系统崩溃。
//获取总线使用权
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接口的问题,也就不需要获取总线使用权。
// 将传输事务添加到队列中
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)中处理传输完成的事件。
// 中断服务程序
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));
}