-->
Page 1 of 7

Exploring the SPI Master interface

PostPosted: Sun Nov 30, 2014 11:54 am
by TheLastMutt
I have ESP-01 Version 2 boards and want to use them as SPI master. Of course this could be done with GPIO bit-banging, but out of curiosity I wanted to use the hardware SPI. Documentation is not really great and the spi_master.c file of the SDK is also not offering plenty of information.

The ESP8266 has 2 SPI interfaces (called "SPI" and "HSPI" in the source code and the pin descriptions). The SPI register header file and the source code suggest that both interfaces have identical register sets, which are just located at different base addresses.

"SPI" uses the SD_* pins and is connected to the external SPI flash IC. So this interface is already in use. Additionally it is in use in a high speed configuration using 4 data lines (plus clock and chip-select). Nevertheless I wanted to try this interface first because it is more easy to access than the HSPI. You just need to solder wires on pins of the SOIC8 SPI flash. That's easier than the mad soldering skillz you need to solder wires on the ESP8266 itself. Additionally it has several documented chip-select pins. My hope was that the SPI flash would only be in use at boot time and when the firmware needs to save some settings. Then my task could use it most of the time.

After having connected a scope I found out that my assumption was wrong. There is SPI traffic all the time while the board is running. I made some experiments anyway. Main result of this is that the SPI_FLASH_PIN register is used to define the chip-select. The default value is 0x1E. BIT0 is cleared in 0x1E, that means that CS0 is low when transmission is in progress. If that value is changed to e.g. 0x1A, then CS0 and CS2 are both low. Of course SPICS2 (=GPIO0 pin) has to be configured as SPI pin first.

Whenever I tried to reconfigure something on the "SPI" interface, the board crashed or got reset all the time. I did not find a way to find out if a transmission by some other task is in progress and I always crashed into an ongoing transmission with my experiments.

So I had to use my mad soldering skillz and connected wires to the "HSPI" interface. It uses the pins with the JTAG names: MTMS, MTDI etc. There is only one documented chip-select pin: MTDO/GPIO15/HSPICS. I quickly managed to transmit some data but the chip-select pin always stayed low. Then I tried to configure GPIO15 as GPIO and bit-bang the chip-select signal but nothing happened. A quick measurement with a multimeter revealed that this pin is connected to GND on the board. So this pin is not usable.

I changed the chip-select to GPIO4 and had to bit-bang it. Then stuff worked and I could explore how to use that interface.

The SPI_FLASH_CLOCK register consists of a prescaler (SPI_CLKDIV_PRE), the number of prescaled clock cycles per bit (SPI_CLKCNT_N) and two more values which somehow set the duty cycle of the SPI clock signal (SPI_CLKCNT_H, SPI_CLKCNT_L). I haven't figured out how to use them yet.
The prescaler divides the 80MHz main clock by SPI_CLKDIV_PRE+1. The highest bit (SPI_CLK_EQU_SYSCLK) is probably used to bypass the prescaler (didn't test that yet). The number of prescaled clock cycles per bit is SPI_CLKCNT_N+1.
I used a SPI_FLASH_CLOCK value of 0x403043 and got a bit more than 1MHz clock frequency: 80MHz / (16+1) / (3+1) = 1.18MHz

There is an additional setting in the PERIPHS_IO_MUX (=PERIPHS_IO_MUX_CONF_U) register. SPI1_CLK_EQU_SYS_CLK sets the HSPI clock signal to 80MHz regardless of the SPI_FLASH_CLOCK setting. This has weird results. If SPI_FLASH_CLOCK is configured to be slow and SPI1_CLK_EQU_SYS_CLK is set, the clock signal is low for the expected time, but instead of the high period there is an 80MHz signal. So you get periodic 80MHz bursts. This could be interesting for other things but it is not what you want for SPI communication.

The interface seems to support a mode in which the state machines for SPI memory chips are already included. In the SPI_FLASH_CMD register there are bits like SPI_FLASH_READ (probably "read from memory") or SPI_FLASH_WRSR (probably "write status register"). There is probably also some DMA mode for huge data transfers. The example code uses the SPI_FLASH_USR bit which enables user defined SPI communication.

The user defined sequence can consist of the following parts:
  • Command: data and bit length defined by SPI_FLASH_USER2
  • Address: data defined by SPI_FLASH_ADDR, bit length defined by SPI_USR_ADDR_BITLEN in SPI_FLASH_USER1 register
  • Data to transmit (DOUT): data defined by SPI_FLASH_C0 (and probably C1, C2 etc, when you transmit more than 32 bits), bit length defined by SPI_USR_OUT_BITLEN in SPI_FLASH_USER1 register
  • Data to receive (DIN): received data gets stored in SPI_FLASH_C0 etc. and bit length is defined by SPI_USR_DIN_BITLEN in SPI_FLASH_USER1 register

Additionally there is SPI_USR_DUMMY_CYCLELEN in SPI_FLASH_USER1 which probably adds some clock cycles in between somewhere, I did not try this.
The bit length in all registers is the actual bit length you want minus one.

Data is transmitted and received starting with the lowest byte of C0, then going to the higher bytes.
This can be changed. When SPI_RD_BYTE_ORDER is set, data is received starting with the highest byte of C0, then going to lower bytes. The SPI_WR_BYTE_ORDER bit probably does the same for outgoing data.

Any parts of the sequence can be activated or deactivated as you wish using the following bits in SPI_FLASH_USER:
  • SPI_USR_COMMAND
  • SPI_FLASH_USR_ADDR
  • SPI_FLASH_USR_DUMMY
  • SPI_FLASH_USR_DIN
  • SPI_FLASH_DOUT
So if you only set the SPI_USR_COMMAND as shown in the SDK example file, only the command part will be transmitted and nothing is received.

This sequence is normally half-duplex. Data is only received during the DIN phase.

For full-duplex operation use SPI_FLASH_DOUT only, but in combination with the SPI_DOUTDIN bit. Then you will receive the same amount of bits as you have configured in SPI_USR_OUT_BITLEN.

SPI mode is Polarity 0, Phase 0. This can probably be changed as well but I didn't try it yet. SPI_CK_OUT_EDGE and SPI_CK_I_EDGE in SPI_FLASH_USER register sound like good candidates.

Actual transmission is started by setting the SPI_FLASH_USR bit in SPI_FLASH_CMD. The bit gets reset to zero when the transmission has finished.

Finally, here is an annotated picture of an ESP-01 Version 2 board. Might be interesting for the Wiki?
pcb_top_annotated.jpg


Edit: updated information about SPI_FLASH_CLOCK register.
Edit 2: added information about byte order.

Re: Exploring the SPI Master interface

PostPosted: Tue Dec 16, 2014 6:26 pm
by fplanel
Hi,

I am looking for a way to activate / deactivate the HSPI CS pin while keeping the MOSI line active.

I tried several combinations of HSPI register configuration with no success, my CS signal is activated whenever I send a byte through the MOSI pin.

Help is welcome

Frantz

Re: Exploring the SPI Master interface

PostPosted: Wed Dec 17, 2014 8:46 am
by Sjaak
fplanel wrote:Hi,

I am looking for a way to activate / deactivate the HSPI CS pin while keeping the MOSI line active.

I tried several combinations of HSPI register configuration with no success, my CS signal is activated whenever I send a byte through the MOSI pin.

Help is welcome

Frantz


You can bitbang another pin and connect that one instead to your chip. Will cost you an gpio though

Re: Exploring the SPI Master interface

PostPosted: Wed Dec 17, 2014 12:12 pm
by TheLastMutt
fplanel wrote:I am looking for a way to activate / deactivate the HSPI CS pin while keeping the MOSI line active.

I tried several combinations of HSPI register configuration with no success, my CS signal is activated whenever I send a byte through the MOSI pin.


Hi!

Maybe I misunderstand your question, but I would say you just configure the HSPI CS pin as GPIO15 using the GPIO registers. This effectively disconnects the pin from the internal SPI logic and you can do what you want with this pin.

This should do it:
Code: Select allPIN_FUNC_SELECT(PERIPHS_IO_MUX_MTDO_U, FUNC_GPIO15);


Then you should be able to change the pin state manually e.g. to high with
Code: Select allGPIO_OUTPUT_SET(15, 1);


I can't test it because on my board this pin is grounded...