Skip to main content
JCorl.1
Senior
November 30, 2021
Solved

Understanding the SPI Peripheral when accepting and sending data.

  • November 30, 2021
  • 6 replies
  • 14244 views

I have my STM32F407VE successfully configured as a slave device. Goal is to emulate a PlayStation 2 controller.

Where I am at right now is that I am trying to send a specific order of bytes so I can be sure that before I read and act on the command byte, I am able to properly send the required 5 byte payload. Basically, the console sends 5 bytes with a certain game I use when a controller is connected: 0x01, 0x42, 0x00, 0x00, 0x00. For this game, it never changes since it doesn't need other commands like for analog joystick read and such.

So all I do is receive a byte and then pulse the acknowledge line (required so the consoles knows I read it). For each exchange, I send the proper bytes that a real controller would which are: 0xFF, 0x41, 0x5A, 0xFF, 0xFF. Sounds easy enough.

When I send with one byte to begin, everything looks good. When I send with two bytes, still looks good. But when I try a 3rd byte, it seems as though the interrupt I have that is triggered from the chip select line is not sync'd correctly. The byte order also becomes messed up because of this. There must be something fundamentally wrong on how I understand the SPI module behaves but I can't quite understand why yet.

I have my interrupt code snippet and the byte screenshots attached.

This topic has been closed for replies.
Best answer by TDK

You would need to capture the transactions at the point it gets out of sync to understand how it's getting out of sync. From the logic analyzer plot you showed, the first byte in the transaction is the second byte sent in your ISR, so it's already out of sync at that point.

The concept however is pretty simple. If it's already in the interrupt, then CS cycles again, it will re-enter the interrupt immediately.

There are a number of ways to solve this. I would set a flag when CS goes high. When it does, cancel the SPI transfer and start a new one. I would set up the SPI peripheral to be always on and just send data into SPIx->DR as needed, and watch for the TXE/RXNE flags to know when to send an ACK and more data.

6 replies

TDK
November 30, 2021

Is the CS line hooked up to SPI_NSS? It would be a lot simpler to use a single call to HAL_SPI_TransmitReceive() with a transaction size of 3 bytes rather than 3 separate calls. Disabling/enabling the peripheral between bytes is not necessary.

Blocking calls within an interrupt can cause the next interrupt to be delayed. No doubt the EXTI flag is set mid-interrupt and is called again immediately after exiting.

"If you feel a post has answered your question, please click ""Accept as Solution""."
JCorl.1
JCorl.1Author
Senior
November 30, 2021

The CS line is internally software controlled since I only have one slave and master. But I interrupt on the CS line anyway as I want to hardware the NSS later on. I wish I could send 3 bytes at the same, that actually works quite well with HAL_SPI_TransmitReceive(). But the PlayStation wants to see an Acknowledge signal every time a byte is sent. Even though I can send all 5 bytes easily (3 was sent to work my way up to fish for errors) with HAL_SPI_TransmitReceive(), the console will now respond because there is no ACK signal. From what I understand, its SPI bus but with the requirement of this ACK line.

This is why I send one byte a time in my code, because I need to ACK plus I need to see what that command byte is to act upon it before the next byte from the master is sent.

TDK
TDKBest answer
December 1, 2021

You would need to capture the transactions at the point it gets out of sync to understand how it's getting out of sync. From the logic analyzer plot you showed, the first byte in the transaction is the second byte sent in your ISR, so it's already out of sync at that point.

The concept however is pretty simple. If it's already in the interrupt, then CS cycles again, it will re-enter the interrupt immediately.

There are a number of ways to solve this. I would set a flag when CS goes high. When it does, cancel the SPI transfer and start a new one. I would set up the SPI peripheral to be always on and just send data into SPIx->DR as needed, and watch for the TXE/RXNE flags to know when to send an ACK and more data.

"If you feel a post has answered your question, please click ""Accept as Solution""."
JCorl.1
JCorl.1Author
Senior
December 1, 2021

So to clarify, when I enter the handler, and the interrupt is cleared, my CS line is already low. That would set off an interrupt again? I thought an interrupt only occurs when its a falling edge. Sorry if I am not getting quite yet as its my first time using interrupts as well.

Going with your idea of approaching this problem, how would you flag when CS goes high in the middle of a transfer? I had a similar idea but I couldn't figure out a way to do this after I detect it going low and serving my interrupt. When I was thinking of how to do that, I was using polling only methods. Maybe I set another interrupt to to cancel the transfer sequence?

I actually did try to use SPIx-> and tried to watch the flags and it seemed to work okay. The problem was that I could not figure out how to capture the CS line while in the middle of my byte sequencing. And its actually important because I think the console will vary the number of bytes it sends during a payload, I think to sniff out illegitimate controllers? If you look at my attachments, you can see what I mean.

After a 5 byte sequence, the console went back to sending 1 byte. If I could figure out how to cancel out of that, that would be great. Maybe I can do it in one of my while loops? I think I tried that but something went wrong? I will have to try again.

	while(1)
	{
		// Wait for chip select to enable
		while( ((ButtonState_t)HAL_GPIO_ReadPin(PS_PS2_CHIP_SELECT_PORT, PS_PS2_CHIP_SELECT_PIN_HAL)) == PUSHED ) {};
		while( ((ButtonState_t)HAL_GPIO_ReadPin(PS_PS2_CHIP_SELECT_PORT, PS_PS2_CHIP_SELECT_PIN_HAL)) == RELEASED ) {};
 
		// Trigger for logic analyzer in debug mode
		PlayerLed_SetState(P1, LED_OFF);
		PlayerLed_SetState(P1, LED_ON);
		PlayerLed_SetState(P1, LED_OFF);
 
		// Enable SPI3
		SPI3->CR1 |= SPI_CR1_SPE;
 
		/* 1st byte */
		// Prepare data to send before the master starts clockint
		SPI3->DR = 0xFF;
 
		// Wait until the transaction between master and slave is complete
		while( (SPI3->SR & (SPI_SR_BSY)) ) {};
 
		// Grab the command master sent
		command = SPI3->DR;
 
		// Wait for rx buffer to be empty
		while( !(SPI3->SR & (SPI_SR_RXNE)) ) {};
 
		// Tell the master we acknowledge this exchange
		///if( (command == 0x01) )
		//{
			PsPs2ControllerEmulation_SendAcknowledge();
		//}
 
		/* 2nd byte */
		// Prepare data to send before the master starts clockint
		SPI3->DR = 0x41;
 
		// Wait until the transaction between master and slave is complete
		while( (SPI3->SR & (SPI_SR_BSY)) ) {};
 
		// Grab the command master sent
		command = SPI3->DR;
 
		// Wait for rx buffer to be empty
		while( !(SPI3->SR & (SPI_SR_RXNE)) ) {};
 
		// Tell the master we acknowledge this exchange
		///if( (command == 0x01) )
		//{
			PsPs2ControllerEmulation_SendAcknowledge();
		//}
 
		/* 3rd byte */
		// Prepare data to send before the master starts clockint
		SPI3->DR = 0x5A;
 
		// Wait until the transaction between master and slave is complete
		while( (SPI3->SR & (SPI_SR_BSY)) ) {};
 
		// Grab the command master sent
		command = SPI3->DR;
 
		// Wait for rx buffer to be empty
		while( !(SPI3->SR & (SPI_SR_RXNE)) ) {};
 
		// Tell the master we acknowledge this exchange
		///if( (command == 0x01) )
		//{
			PsPs2ControllerEmulation_SendAcknowledge();
		//}
 
		/* 4th byte */
		// Prepare data to send before the master starts clockint
		SPI3->DR = 0xFF;
 
		// Wait until the transaction between master and slave is complete
		while( (SPI3->SR & (SPI_SR_BSY)) ) {};
 
		// Grab the command master sent
		command = SPI3->DR;
 
		// Wait for rx buffer to be empty
		while( !(SPI3->SR & (SPI_SR_RXNE)) ) {};
 
		// Tell the master we acknowledge this exchange
		///if( (command == 0x01) )
		//{
			PsPs2ControllerEmulation_SendAcknowledge();
		//}
 
		/* 5th byte */
		// Prepare data to send before the master starts clockint
		SPI3->DR = 0xFF;
 
		// Wait until the transaction between master and slave is complete
		while( (SPI3->SR & (SPI_SR_BSY)) ) {};
 
		// Grab the command master sent
		command = SPI3->DR;
 
		// Wait for rx buffer to be empty
		while( !(SPI3->SR & (SPI_SR_RXNE)) ) {};
 
		// Tell the master we acknowledge this exchange
		///if( (command == 0x01) )
		//{
			PsPs2ControllerEmulation_SendAcknowledge();
		//}
 
		// Disable SPI3
		SPI3->CR1 &= ~SPI_CR1_SPE;
}

JCorl.1
JCorl.1Author
Senior
December 1, 2021

Here is the snippet where I try to un-comment the previous code's if...then blocks where try to read the data from the rx buffer. When I do this, I send the bytes properly (well without a method to detect the CS line go high) but I no longer ACK properly because even though I see the proper bytes being sent to me, I fail to capture it properly from the SPIx->DR. It is really strange to me and is the reason why I started to try other ways. Also thanks for discussing this with me. It is hard to find someone to bounce ideas off of as I know no one else who does firmware on their free time lol.

while(1)
	{
		// Wait for chip select to enable
		while( ((ButtonState_t)HAL_GPIO_ReadPin(PS_PS2_CHIP_SELECT_PORT, PS_PS2_CHIP_SELECT_PIN_HAL)) == PUSHED ) {};
		while( ((ButtonState_t)HAL_GPIO_ReadPin(PS_PS2_CHIP_SELECT_PORT, PS_PS2_CHIP_SELECT_PIN_HAL)) == RELEASED ) {};
 
		// Trigger for logic analyzer in debug mode
		PlayerLed_SetState(P1, LED_OFF);
		PlayerLed_SetState(P1, LED_ON);
		PlayerLed_SetState(P1, LED_OFF);
 
		// Enable SPI3
		SPI3->CR1 |= SPI_CR1_SPE;
 
		/* 1st byte */
		// Prepare data to send before the master starts clockint
		SPI3->DR = 0xFF;
 
		// Wait until the transaction between master and slave is complete
		while( (SPI3->SR & (SPI_SR_BSY)) ) {};
 
		// Grab the command master sent
		command = SPI3->DR;
 
		// Wait for rx buffer to be empty
		while( !(SPI3->SR & (SPI_SR_RXNE)) ) {};
 
		// Tell the master we acknowledge this exchange
		if( (command == 0x01) )
		{
			PsPs2ControllerEmulation_SendAcknowledge();
		}
 
		/* 2nd byte */
		// Prepare data to send before the master starts clockint
		SPI3->DR = 0x41;
 
		// Wait until the transaction between master and slave is complete
		while( (SPI3->SR & (SPI_SR_BSY)) ) {};
 
		// Grab the command master sent
		command = SPI3->DR;
 
		// Wait for rx buffer to be empty
		while( !(SPI3->SR & (SPI_SR_RXNE)) ) {};
 
		// Tell the master we acknowledge this exchange
		if( (command == 0x42) )
		{
			PsPs2ControllerEmulation_SendAcknowledge();
		}
 
		/* 3rd byte */
		// Prepare data to send before the master starts clockint
		SPI3->DR = 0x5A;
 
		// Wait until the transaction between master and slave is complete
		while( (SPI3->SR & (SPI_SR_BSY)) ) {};
 
		// Grab the command master sent
		command = SPI3->DR;
 
		// Wait for rx buffer to be empty
		while( !(SPI3->SR & (SPI_SR_RXNE)) ) {};
 
		// Tell the master we acknowledge this exchange
		if( (command == 0x00) )
		{
			PsPs2ControllerEmulation_SendAcknowledge();
		}
 
		/* 4th byte */
		// Prepare data to send before the master starts clockint
		SPI3->DR = 0xFF;
 
		// Wait until the transaction between master and slave is complete
		while( (SPI3->SR & (SPI_SR_BSY)) ) {};
 
		// Grab the command master sent
		command = SPI3->DR;
 
		// Wait for rx buffer to be empty
		while( !(SPI3->SR & (SPI_SR_RXNE)) ) {};
 
		// Tell the master we acknowledge this exchange
		if( (command == 0x00) )
		{
			PsPs2ControllerEmulation_SendAcknowledge();
		}
 
		/* 5th byte */
		// Prepare data to send before the master starts clockint
		SPI3->DR = 0xFF;
 
		// Wait until the transaction between master and slave is complete
		while( (SPI3->SR & (SPI_SR_BSY)) ) {};
 
		// Grab the command master sent
		command = SPI3->DR;
 
		// Wait for rx buffer to be empty
		while( !(SPI3->SR & (SPI_SR_RXNE)) ) {};
 
		// Tell the master we acknowledge this exchange
		if( (command == 0x00) )
		{
			PsPs2ControllerEmulation_SendAcknowledge();
		}
 
		// Disable SPI3
		SPI3->CR1 &= ~SPI_CR1_SPE;
}

JCorl.1
JCorl.1Author
Senior
December 1, 2021

As an update, I receive the bytes in the following order with the more "manual" code: 0x00, 0x01, 0x42, 0x00, 0x00. It should be 0x01, 0x42, 0x00, 0x00, 0x00.

JCorl.1
JCorl.1Author
Senior
December 1, 2021

Ok so I think that BUSY flag was messing me up. I can now receive bytes in the correct order! I think I am able to sense the CS line as well.

uint8_t PsPs2ControllerEmulation_Run(void)
{
	// Wait for chip select to enable
	while( ((ButtonState_t)HAL_GPIO_ReadPin(PS_PS2_CHIP_SELECT_PORT, PS_PS2_CHIP_SELECT_PIN_HAL)) == PUSHED ) {};
	while( ((ButtonState_t)HAL_GPIO_ReadPin(PS_PS2_CHIP_SELECT_PORT, PS_PS2_CHIP_SELECT_PIN_HAL)) == RELEASED ) {};
 
	// Trigger for logic analyzer in debug mode
	PlayerLed_SetState(P1, LED_OFF);
	PlayerLed_SetState(P1, LED_ON);
	PlayerLed_SetState(P1, LED_OFF);
 
	// Enable SPI3
	SPI3->CR1 |= SPI_CR1_SPE;
 
	/* 1st byte */
	// Prepare data to send before the master starts clocking
	SPI3->DR = 0xFF;
 
	// Wait for rx buffer to be empty
	while( !(SPI3->SR & (SPI_SR_RXNE)) )
	{
		if(((ButtonState_t)HAL_GPIO_ReadPin(PS_PS2_CHIP_SELECT_PORT, PS_PS2_CHIP_SELECT_PIN_HAL)) == RELEASED)
		{
			// Disable SPI3
			SPI3->CR1 &= ~SPI_CR1_SPE;
			return 0;
		}
		PlayerLed_SetState(P1, LED_OFF);
		PlayerLed_SetState(P1, LED_ON);
		PlayerLed_SetState(P1, LED_OFF);
	}
	// Grab the command master sent
	command1 = SPI3->DR;
	// Tell the master we acknowledge this exchange
	if( (command1 == 0x01) )
	{
		PsPs2ControllerEmulation_SendAcknowledge();
	}
 
	/* 2nd byte */
	// Prepare data to send before the master starts clocking
	SPI3->DR = 0x41;
 
	// Wait for rx buffer to be empty
	while( !(SPI3->SR & (SPI_SR_RXNE)) )
	{
		if(((ButtonState_t)HAL_GPIO_ReadPin(PS_PS2_CHIP_SELECT_PORT, PS_PS2_CHIP_SELECT_PIN_HAL)) == RELEASED)
		{
			// Disable SPI3
			SPI3->CR1 &= ~SPI_CR1_SPE;
			return 0;
		}
		PlayerLed_SetState(P1, LED_OFF);
		PlayerLed_SetState(P1, LED_ON);
		PlayerLed_SetState(P1, LED_OFF);
	}
	// Grab the command master sent
	command2 = SPI3->DR;
	// Tell the master we acknowledge this exchange
	if( (command2 == 0x42) )
	{
		PsPs2ControllerEmulation_SendAcknowledge();
	}
 
	/* 3rd byte */
	// Prepare data to send before the master starts clocking
	SPI3->DR = 0x5A;
 
	// Wait for rx buffer to be empty
	while( !(SPI3->SR & (SPI_SR_RXNE)) )
	{
		if(((ButtonState_t)HAL_GPIO_ReadPin(PS_PS2_CHIP_SELECT_PORT, PS_PS2_CHIP_SELECT_PIN_HAL)) == RELEASED)
		{
			// Disable SPI3
			SPI3->CR1 &= ~SPI_CR1_SPE;
			return 0;
		}
		PlayerLed_SetState(P1, LED_OFF);
		PlayerLed_SetState(P1, LED_ON);
		PlayerLed_SetState(P1, LED_OFF);
	}
	// Grab the command master sent
	command3 = SPI3->DR;
	// Tell the master we acknowledge this exchange
	if( (command3 == 0x00) )
	{
		PsPs2ControllerEmulation_SendAcknowledge();
	}
 
	/* 4th byte */
	// Prepare data to send before the master starts clocking
	SPI3->DR = 0xF7;
 
	// Wait for rx buffer to be empty
	while( !(SPI3->SR & (SPI_SR_RXNE)) )
	{
		if(((ButtonState_t)HAL_GPIO_ReadPin(PS_PS2_CHIP_SELECT_PORT, PS_PS2_CHIP_SELECT_PIN_HAL)) == RELEASED)
		{
			// Disable SPI3
			SPI3->CR1 &= ~SPI_CR1_SPE;
			return 0;
		}
		PlayerLed_SetState(P1, LED_OFF);
		PlayerLed_SetState(P1, LED_ON);
		PlayerLed_SetState(P1, LED_OFF);
	}
	// Grab the command master sent
	command4 = SPI3->DR;
	// Tell the master we acknowledge this exchange
	if( (command4 == 0x00) )
	{
		PsPs2ControllerEmulation_SendAcknowledge();
	}
 
	/* 5th byte */
	// Prepare data to send before the master starts clocking
	SPI3->DR = 0xFF;
 
	// Wait for rx buffer to be empty
	while( !(SPI3->SR & (SPI_SR_RXNE)) )
	{
		if(((ButtonState_t)HAL_GPIO_ReadPin(PS_PS2_CHIP_SELECT_PORT, PS_PS2_CHIP_SELECT_PIN_HAL)) == RELEASED)
		{
			// Disable SPI3
			SPI3->CR1 &= ~SPI_CR1_SPE;
			return 0;
		}
		PlayerLed_SetState(P1, LED_OFF);
		PlayerLed_SetState(P1, LED_ON);
		PlayerLed_SetState(P1, LED_OFF);
	}
	// Grab the command master sent
	command5 = SPI3->DR;
	// Tell the master we acknowledge this exchange
	if( (command5 == 0x00) )
	{
		PsPs2ControllerEmulation_SendAcknowledge();
	}
	return 0;
}

JCorl.1
JCorl.1Author
Senior
December 1, 2021

Ok yeah that was def the way to do it! The game finally went to another menu when I decided to spam the START bit. Thanks again for the suggestions!

0693W00000GZgWIQA1.png