xhackerustc 发表于 2024-5-4 16:46

[FireBeetle 2 ESP32C6开发板] SPI测试

<div class='showpostmsg'> 本帖最后由 xhackerustc 于 2024-5-4 17:57 编辑

<p>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/)改。</p>

<p>&nbsp;</p>

<p><strong>io mux与GPIO交换矩阵</strong></p>

<p>esp32c6 datasheet说明通过iomux与GPIO交换矩阵,信号可以接到任意针脚,但是对于高速信号,可仅仅经由iomux而&quot;旁路GPIO 交换矩阵以实现更好的高频数字特性&rdquo;,是以一定的针脚灵活性换取了更好的高频数字特性。经阅读datasheet第7.12章节IO MUX 管脚功能列表中的表 7&shy;3. IO MUX 管脚功能, 笔者想直接经由iomux的FSPID, FSPICLK, FSPIQ以及FSPICS2这四个信号, 对比FireBettle 2原理图,这次笔者不再遵循板子丝印的针脚安排,而是使用如下针脚映射:</p>

<pre>
<code class="language-bash">    - MOSI: MTDO
    - MISO: GPIO2
    - CS: SDIO_CMD
    - SCLK: MTCK</code></pre>

<p><strong>esp idf中的spi api简介</strong></p>

<p>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也是灵活机变根据需要选择。</p>

<p>&nbsp;</p>

<p><strong>psram操作原则</strong></p>

<p>任何操作都是发起一次spi传输,每次发spi传输前拉低CS信号,结束后拉高CS信号。</p>

<p>&nbsp;</p>

<p><strong>psram的初始化</strong></p>

<p>根据笔者这款psram模块的datasheet,上电后自动初始化进入standby模式,不过笔者初始化后还是发起了RESET_EN和RESET命令,发命令的函数如下所示:</p>

<pre>
<code class="language-cpp">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*)&amp;t);
}
</code></pre>

<p><strong>psram的读取</strong></p>

<pre>
<code class="language-cpp">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*)&amp;t);
      gpio_set_level(GPIO_CS, 1);

      if (ret != ESP_OK) {
                printf("psram_read failed %lx %d\n", addr, len);
                return -1;
      }

      return len;
}</code></pre>

<p><strong>psram的写入</strong></p>

<pre>
<code class="language-cpp">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, &amp;t);
      gpio_set_level(GPIO_CS, 1);

      if (ret != ESP_OK) {
                printf("psram_write failed %lx %d\n", addr, len);
                return -1;
      }

      return len;
}</code></pre>

<p>综合测试代码</p>

<pre>
<code class="language-cpp">void app_main(void)
{
      int i;
      uint64_t t1;
      uint8_t testbuf;
               
                usleep(2000000);
      if (psram_init())
                printf("failed to init psram");

      memset(testbuf, 0x5a, sizeof(testbuf));
      t1 = esp_timer_get_time();
      for (i = 0; i &lt; 10000; ++i) {
                psram_write(i * 64, &amp;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 &lt; 10000; ++i) {
                psram_read(i * 64, &amp;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);
      }
}
</code></pre>

<p><strong>编译烧录和测试</strong></p>

<p>可参见笔者前面的测评贴,这里不再赘述,测试结果如下:</p>

<pre>
<code class="language-bash">I (2066) gpio: GPIO| 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.
</code></pre>

<p>&nbsp;</p>

<p><strong>SPI传输性能影响因素</strong></p>

<p>据笔者测试,每次SPI传输数据大小和用不用poll方式影响比较大,其它因素影响比较小。实验如下:</p>

<p>1.将每次传输大小从64字节改成128字节,相比使用64字节传输大小,速度提升了38.2%</p>

<pre>
<code class="language-bash">PSRAM write speed: 4723247 B/s.
PSRAM read speed: 4654545 B/s.
</code></pre>

<p>2.在此基础上,把传输方式改成非poll方式即spi_device_polling_transmit()改成spi_device_transmit(),传输大小仍然维持128字节,相比poll方式速度下降了26.6%</p>

<pre>
<code class="language-bash">PSRAM write speed: 3137254 B/s.
PSRAM read speed: 3099273 B/s.
</code></pre>

<p>&nbsp;</p>
</div><script>                                        var loginstr = '<div class="locked">查看本帖全部内容,请<a href="javascript:;"   style="color:#e60000" class="loginf">登录</a>或者<a href="https://bbs.eeworld.com.cn/member.php?mod=register_eeworld.php&action=wechat" style="color:#e60000" target="_blank">注册</a></div>';
                                       
                                        if(parseInt(discuz_uid)==0){
                                                                                                (function($){
                                                        var postHeight = getTextHeight(400);
                                                        $(".showpostmsg").html($(".showpostmsg").html());
                                                        $(".showpostmsg").after(loginstr);
                                                        $(".showpostmsg").css({height:postHeight,overflow:"hidden"});
                                                })(jQuery);
                                        }                </script><script type="text/javascript">(function(d,c){var a=d.createElement("script"),m=d.getElementsByTagName("script"),eewurl="//counter.eeworld.com.cn/pv/count/";a.src=eewurl+c;m.parentNode.insertBefore(a,m)})(document,523)</script>

xhackerustc 发表于 2024-5-4 16:48

对于spi0和spi1的使用,这个链接里的回答非常清楚:https://github.com/espressif/esp-idf/issues/11663

Here're few ways to access the Flash:

By default most of the ESP-IDF program is placed in the Flash. Accessing code placed in Flash is done by hardware (SPI0).

And we provide the ESP FLASH driver for you to access the Flash for other purpose (e.g. storage). This is via SPI1.

We provide SPI FLASH MMAP driver and MMU MMAP driver for you to access (via SPI0, e.g. by a pointer) those you write to the Flash via ESP FLASH driver.

lugl4313820 发表于 2024-5-4 20:42

<p>相比poll方式速度下降了26.6%。阻塞式还要快一点吗?</p>

xhackerustc 发表于 2024-5-5 08:32

lugl4313820 发表于 2024-5-4 20:42
相比poll方式速度下降了26.6%。阻塞式还要快一点吗?

<p>poll方式更快,esp-idf官方文档说过poll方式能减少上下文切换。实际上这个道理和linux中的NAPI原理是一致的</p>

lugl4313820 发表于 2024-5-5 08:45

xhackerustc 发表于 2024-5-5 08:32
poll方式更快,esp-idf官方文档说过poll方式能减少上下文切换。实际上这个道理和linux中的NAPI原理是一致 ...

<p>这跟在单片机上的运行有点区别呢。</p>
页: [1]
查看完整版本: [FireBeetle 2 ESP32C6开发板] SPI测试