cancel
Showing results for 
Search instead for 
Did you mean: 

Understanding the SPI Peripheral when accepting and sending data.

JCorl.1
Senior

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.

1 ACCEPTED SOLUTION

Accepted Solutions
TDK
Guru

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".

View solution in original post

10 REPLIES 10
TDK
Guru

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".

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.

Could you explain that blocking calls part in more detail. I think this is where my problem lies. My thought was that once I enter the interrupt, my 3 separate calls are fast enough to grab all the bytes and then when I am complete, I won't enter back into the handler until the next time the CS line is pulled low.

I guess I am having trouble understanding how its possible for the EXTI handler to trigger, get cleared from the handler function, go into my callback to handle my interrupt, and when I am done, it becomes delayed. I think I am approaching this problem from the wrong end and might need to include a SPI interrupt as well.

I think this is what this person has done in their Arduino implementation:

https://github.com/mcranford13/Ps2ControllerEmulator/blob/master/Ps2ControllerEmulator.ino

TDK
Guru

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".

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
Senior

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
Senior

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
Senior

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;
}