本帖最后由 xhackerustc 于 2024-5-4 17:57 编辑
esp32c6集成了3个SPI控制器:SPI0, SPI1和通用SPI2。其中SPI0与SPI1合称MSPI主要芯片内部使用以访问外部的flash和psram(看esp32c6的datasheet框图也会发现有个有意思的地方:SPI0和SPI1合用一套spi信号,之间通过一个总线仲裁器,另外SPI0接了cache,猜测cache硬件操作SPI0,而esptool、idf中的代码读写flash是操作的SPI1),通用SPI2可当通用SPI控制器来使用。通用SPI2支持1线、2线、4线等spi模式,支持DMA传输,最高时钟频率80MHZ,时钟极性和相位可配置。笔者有一个8MB的PSRAM模块,这次就以它来测试下ESP32C6的通用SPI2。不过这一次idf没现成代码,需要手搓一个操作手里这个PSRAM的代码,但是可以照葫芦画瓢,在idf的spi例子上(examples/peripherals/spi_master/hd_eeprom/)改。
io mux与GPIO交换矩阵
esp32c6 datasheet说明通过iomux与GPIO交换矩阵,信号可以接到任意针脚,但是对于高速信号,可仅仅经由iomux而"旁路GPIO 交换矩阵以实现更好的高频数字特性”,是以一定的针脚灵活性换取了更好的高频数字特性。经阅读datasheet第7.12章节IO MUX 管脚功能列表中的表 73. IO MUX 管脚功能, 笔者想直接经由iomux的FSPID, FSPICLK, FSPIQ以及FSPICS2这四个信号, 对比FireBettle 2原理图,这次笔者不再遵循板子丝印的针脚安排,而是使用如下针脚映射:
- MOSI: MTDO
- MISO: GPIO2
- CS: SDIO_CMD
- SCLK: MTCK
esp idf中的spi api简介
idf中spi使用基本上这个套路:调用spi_bus_initialize()初始化spi host,再调用spi_bus_add_device(),初始化流程完成。然后每一次spi传输都是通过spi_transaction_t或spi_transaction_ext_t结构体控制的,其中后者用于地址或cmd长度会变,或者需要传输dummy bits的场景。spi传输的API比较灵活,选用哪个结构体取决于spi传输需要。设置完spi传输结构体后调用spi_device_polling_transmit()或spi_device_tranmit(),前者顾名思义不用中断使用poll方式,延迟和spi本身的传输性能比较好,因为没有context切换开销;后者整体系统cpu利用占优,选用哪个API也是灵活机变根据需要选择。
psram操作原则
任何操作都是发起一次spi传输,每次发spi传输前拉低CS信号,结束后拉高CS信号。
psram的初始化
根据笔者这款psram模块的datasheet,上电后自动初始化进入standby模式,不过笔者初始化后还是发起了RESET_EN和RESET命令,发命令的函数如下所示:
static esp_err_t psram_send_cmd(spi_device_handle_t h, const uint8_t cmd)
{
spi_transaction_ext_t t = { };
t.base.flags = SPI_TRANS_VARIABLE_ADDR;
t.base.cmd = cmd;
t.base.length = 0;
t.command_bits = 8U;
t.address_bits = 0;
return spi_device_polling_transmit(h, (spi_transaction_t*)&t);
}
psram的读取
int psram_read(uint32_t addr, void *buf, int len)
{
esp_err_t ret;
spi_transaction_ext_t t = { };
t.base.cmd = CMD_FAST_READ;
t.base.addr = addr;
t.base.rx_buffer = buf;
t.base.length = len * 8;
t.base.flags = SPI_TRANS_VARIABLE_DUMMY;
t.dummy_bits = 8;
gpio_set_level(GPIO_CS, 0);
ret = spi_device_polling_transmit(handle, (spi_transaction_t*)&t);
gpio_set_level(GPIO_CS, 1);
if (ret != ESP_OK) {
printf("psram_read failed %lx %d\n", addr, len);
return -1;
}
return len;
}
psram的写入
int psram_write(uint32_t addr, void *buf, int len)
{
esp_err_t ret;
spi_transaction_t t = {};
t.cmd = CMD_WRITE;
t.addr = addr;
t.tx_buffer = buf;
t.length = len * 8;
gpio_set_level(GPIO_CS, 0);
ret = spi_device_polling_transmit(handle, &t);
gpio_set_level(GPIO_CS, 1);
if (ret != ESP_OK) {
printf("psram_write failed %lx %d\n", addr, len);
return -1;
}
return len;
}
综合测试代码
void app_main(void)
{
int i;
uint64_t t1;
uint8_t testbuf[64];
usleep(2000000);
if (psram_init())
printf("failed to init psram");
memset(testbuf, 0x5a, sizeof(testbuf));
t1 = esp_timer_get_time();
for (i = 0; i < 10000; ++i) {
psram_write(i * 64, &testbuf, 64);
}
t1 = esp_timer_get_time() - t1;
t1 /= 1000;
printf("PSRAM write speed: %lld B/s.\n", 64 * 10000 * 1000 / t1);
fflush(stdout);
t1 = esp_timer_get_time();
for (i = 0; i < 10000; ++i) {
psram_read(i * 64, &testbuf, 64);
}
t1 = esp_timer_get_time() - t1;
t1 /= 1000;
printf("PSRAM read speed: %lld B/s.\n", 64 * 10000 * 1000 / t1);
fflush(stdout);
for (;;) {
printf("PSRAM test done\n");
usleep(1000000);
}
}
编译烧录和测试
可参见笔者前面的测评贴,这里不再赘述,测试结果如下:
I (2066) gpio: GPIO[18]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
spi_bus_initialize = 0
spi_bus_add_device = 0
PSRAM ID: 0d5d52d26fd0
PSRAM write speed: 3091787 B/s.
PSRAM read speed: 3004694 B/s.
SPI传输性能影响因素
据笔者测试,每次SPI传输数据大小和用不用poll方式影响比较大,其它因素影响比较小。实验如下:
1.将每次传输大小从64字节改成128字节,相比使用64字节传输大小,速度提升了38.2%
PSRAM write speed: 4723247 B/s.
PSRAM read speed: 4654545 B/s.
2.在此基础上,把传输方式改成非poll方式即spi_device_polling_transmit()改成spi_device_transmit(),传输大小仍然维持128字节,相比poll方式速度下降了26.6%
PSRAM write speed: 3137254 B/s.
PSRAM read speed: 3099273 B/s.