Once more: esp32, ILI9341 + xpt2046 + sdcard on same spi

I know there are several discussions about esp32 + display + touch + sdcard. But they are filled with different tips and solutions and there doesn’t seem to be a nice and clean solution.

And imho one possible solution isn’t mentioned at all: The ili and xp2046 already share the same spi in the default esp32 lvgl µP setup. Display and touch run nicely on the same spi in my test setup. The sd card also works on the same bus as long is I don’t try to initialize the display or the touch first. So physically everything should be fine.

My question: Does it make sense to write an sd card driver that uses the same spi techniques the current ili and touch drivers use?

Imho the “lvgl µP drivers to use” are the driver/esp32/ili9XXX.py and driver/esp32/xpt2046.py and i think these are the ones i am using. Both use a similar (pretty complex) way of accessing the SPI.

Would it make sense if I write a similar driver for the sdcard? One problem i see is the fact that the sd card driver want’s to write a few hundred ff bytes to the sd card before cs is asserted. This imho somewhat contradicts the fact that cs is totally controlled by the SPI/DMA logic in ili9XXX.py and xp2046.py

So does this make sense? Any reason not to do this? Has anyone done this already?

Hi @Till_Harbaum!

Yes it’s a possible solution.
But instead, why not change/fix the existing SD card driver to work nicely with other drivers? Is it necessary to write a new driver?
After fixing the driver you can try to push it to upstream Micropython. If for some reason they don’t accept it, we can consider pushing these fixes to lv_micropython directly.

That’s weird. The idea of a shared bus is that the SPI slave reads data from the bus only when CS is asserted. It might still work as long as CS is not asserted on the display/touch drivers but I never tried something like that.

In general, did you find out exactly why SD card driver doesn’t work with LVGL drivers?

One thing that I noticed is that Micropython’s SPI driver is not as flexible as it could have been. For example, it does not allow the user to set max_transfer_sz and it does not allow other drivers to initialize the bus (on LVGL drivers you can pass -1 to miso/mosi/clk arguments and it would assume someone else initialized the SPI bus).
If the Micropython SPI driver either allowed initializing max_transfer_sz or allowed other drivers to initialize the bus, it would probably work together with LVGL drivers. (well, I didn’t try that, there could be additional problems)
More details on: Trying to use external SPI device + lvgl

The sd card driver itself is pretty simple. I’d just combine your SPI code form the ILI and XPT drivers with the sd card specific code from sdcard.py or the like.

That’s pretty common. SPI devices are often very simple and don’t come with builtin clock source. So they use the SPI clock as their only clock source for internal state machines or the like. By spec SD cards want nearly 100 spi clock cycles as part of their power-up sequence. See e.g. http://elm-chan.org/docs/mmc/i/sdinit.png

It may be sufficient here:

  1. The clocks generated by a previous display initialization, or
  2. Dummy clocks generated by sending 0xff (or anything else) to the sd card with cs low. I actually don’t see that cs high is a must. The docs just say there’s a need for 70-something clocks at power up

No. But I’d thought that the current way the ILI and XPT drivers work is “the right” way to do it. If the normal SPI could be fixed then why hasn’t this been fixed and used for the ILI and XPT as well?

My reasoning is: If the ILI and XPT drivers are doing it “the way it should be done” then it makes sense to do it the same way in the sdcard driver. If approach of the ILI and XPT drivers are not the way to go then they should be fixed. But I wouldn’t understand why the ILI and XPT should be treated different than then SD card.

The ILI and XPT drivers are Python drivers that use ESP-IDF directly through the espidf module.
The espidf module is only available on lv_micropython today.
So, if you write an SD card driver in the same way, you would only be able to run it where espidf module is present - on lv_micropython forks.

It might be good enough, but a more generic solution would be to create an agnostic driver that could run on any micropython fork. An SD driver, unlike ILI and XPT drivers, is not directly related to LVGL GUI and could be used without LVGL. The ILI and XPT drivers were designed to work with LVGL and not used for other purposes.

You might ask, why do the ILI and XPT drivers rely on the espidf module?
It was possible to implement them in C, such that they would only rely on ESP-IDF C API and work anywhere (wherever ESP32 is supported). But I chose to implement them in Python and the existing SPI (and other) python APIs provided by Micropython don’t cover what I needed. So espidf module is the way to access the ESP-IDF C API directly from Python.

I did some tests based upon the XPT driver. One major issue is the fact that the SD commands are 6 bytes long. In theory I should be able to send these via 1 command byte and 5 address bytes since command is max 16 bits and address is max 64 bits. But the µP bindings expect at most a 16 bit integer for the address limiting me to 4 bytes cmd and addr in total. This doesn’t work, so i tried a full 6 byte write + x bytes read transfer. This only works in full duplex since a write+read transfer is not supported in half duplex DMA.

So a full duplex version based on the XPT code sort of works and I can send some of the initial card commands and see their results. But: For some yet unknown reason this stops working once i initialize the ILI first. I even tried to lock the entire bus to avoid the ILI driver to do anything in the background. Still the transfer is broken. Time to power up the logic analyzer to see what’s different if the ILI has been initialized first.

But still if that can be made to work, SD card support will be rather ugly. There are e.g. times in a transfer where you read 0xff until you get a valid status byte. So you cannot know beforehand how many bytes you need to read. This can IMHO barely be implemented with these DMA engines without releasing CS in between.

Is this a µP binding limitation or ESP-IDF limitation? Could you point me to the API you are referring to?
The µP binding is generated from ESP-IDF headers so I would like to know where is the discrepancy.

Maybe a DMAable RAM allocation problem? The ILI driver allocates lots of DMAable RAM. You can try to minimize it by lowering the factor parameter.

Anyway, I’m very curious about your findings on this.

So how is it implemented in the original SD card driver? How did they work around this issue?

It’s an ESP-IDF thing. It’s this field: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/spi_master.html#_CPPv4N17spi_transaction_t4addrE

They are doing everything manually byte by byte and they are controlling cs also manually and keep it asserted as long as they want. See https://github.com/micropython/micropython/blob/master/drivers/sdcard/sdcard.py

I am not surprised that this collides with some DMA spi going on in the background.

A related question: The docs several times mention this default/example mapping:
ILI9341: miso=5, mosi=18, clk=19, cs=13, dc=12, rst=4, power=14, backlight=15, spihost=esp.HSPI_HOST, mhz=40, factor=4, hybrid=True

What confuses me here is that this uses HSPI but the default pin numbers are for VSPI pins. This is possible as there’s the pin multiplexer but IMHO it doesn’t make too much sense. Why is this using HSPI?

Where did you see that the address is limited to 16 bit?
In the docs it says address_bits 0-64 bits.

Probably a mistake.
It doesn’t matter too much because, as you said, almost any pins can be used and the multiplexer would handle this. (On the other hand it probably limits the maximum frequency because of the multiplexer timing).

Feel free to send a PR to change that.

When i try to write anything > 65535 into that field within µP i get this error:
OverflowError: overflow converting long int to machine word

For some reason µP thinks this is an integer.

Actually none of this works for me as my version of µP doesn’t even have esp.HSPI_HOST and I am just using constants 1 and 2. So i don’t have an idea what should be used there.

So I did fire up the logic analyzer under slight protest from the kids as they had rewired the sd card to the second spi.

I have this code i wrote an basis of the xpt driver which just sends a few commands to the sd card full duplex and reads a few bytes back and i see that this sort of makes sense and i see the expected reply to several commands. I see this in the logic analyzer and the esp32 reports these as well. Once i load the ili driver beforehand the esp32 reports unexpected vakues that don’t make much sense. And here’s the interesting part: In the logic analyzer i still see the correct bytes.

So the transfer itself is still working. The result just doesn’t reach memory. Since the xpt driver does work this may be related to the fact I need to do full duplex transfers and the ili does half duplex and this won’t coexist.

Anyway. For the sd card full duplex is not the way to go, anyway. But as pointed out before half duplex does not work as i cannot write the address field as i’d need.

Biggest problem here: The setup using the second spi for the sd card is working and the kids are satisfied :slight_smile: So they don’t want me to go one step backwards and try to understand what’s going in with the shared bus.

I’d like to do similar with the ili driver and the original sdcard.py (or its equivalent in machine.sdcard). But i cannot manage that to load at all. Whatever I try either the ili or the sdcard complains that the spi bus is already in use.

Actually… this might be a bug in the LVGL micropython bindings.
I noticed that it tries to convert 64bit integers with mp_obj_new_int/mp_obj_get_int, while the correct functions are probably mp_obj_new_int_from_ll and I’m not sure what is the other one to convert mp integer back to long long.
I’ve opened an issue to track this.

That’s strange.
Macros are not automatically converted, but HSPI/VSPI is handled specially here with an enum, which is converted automatically.

That’s great! I can’t wait until my kids are old enough to wire anything on my projects. Currently they barely wire their own shoe laces…

Both ili and xpt can be configure to work either half duplex or full duplex. There’s a half_duplex argument in the constructor.

I can’t remember seeing anything in the docs that prevents half-duplex and full-duplex coexistence on the same SPI bus. But I think I’ve seen issues (this, this) mentioning problems when mixing half/full duplex. Maybe some esp-idf problem? here is a post in esp forum.

If you provide “-1” to ili miso/mosi/clk, it will not try to initialize the bus. But you need to make sure that whichever code that initializes the bus also sets a large enough max_transfer_sz.
Another option is to let ili initialize the bus and remove the bus initialization code from the sdcard driver.

That’s indeed great. I am doing this for a small toy controller where they can select and run their own python scripts from sd card.

Uhm … and there it reads #if ... && ESP_IDF_VERSION_MAJOR >= 4 which explains it as I am using IDF 3 since 4 is still marked as experimental.

Thinks like you are all using version 4 …

I finally found it. a) IDF 4 seems to be needed for that and b) then this shows up as esp.HSPI_HOST only after a import espidf as esp. This is somewhat confusing as a module named esp actually exists and also one named esp32 which many examples import esp32 as esp

Well, both esp32 and espidf modules provide µP API to the same ESP-IDF C API.
esp32 is manually-written and provided by Micropython while espidf is auto generated from ESP-IDF header files and specific to lv_micropython. espidf is provided in addition to the original esp32 module.

I agree it’s a bit confusing, but that’s how it is right now.

Update: This problem is resolved on latest lv_binding_micropython.