arduino / ArduinoCore-megaavr

Arduino Core for the ATMEGA4809 CPU
103 stars 62 forks source link

Using Arduino NANO Every as SLAVE in SPI communication #67

Open bridystone opened 4 years ago

bridystone commented 4 years ago

Hi,

i'm trying to establish an MASTER/SLAVE Communication between 2 Arduinos.

The current idea for testing is that i want the NANO to generate random numbers and send it back to the UNO. Slave-NANO Every Master-UNO

This is now done with a lot of register setting and bit analyzing.

I'm just wondering if this is not planned in the implementation of SPI? Is Arduino always planned as a MASTER? now even AttachInterrupt and DetachInterrupt are hidden in private...

using Begin / BeginTransaction & Transfer did not really help.

Have i overlooked something?

robamu commented 4 years ago

I have the same problem right now and I am glad I found someone with the same problem. I had a solution for the Uno and the MEGA (found on google) but it did not work anymore because the registers are different for the ATmega4809. I hope I can get it working with your code. I was also very confused that there is a need for register setting to make the Arduino work as a SPI slave.

SpenceKonde commented 4 years ago

The implementation of SPI on the megaAVR 0-series parts specifically precludes slave mode.

I suspect the reason for this that 99.5+% of arduino users want to use the arduino as an SPI master, and the behavior of the SS pin on classic AVRs was one of the most-hated "traps" for users. It was a true blessing that the new parts allowed us to turn off the perverse behavior of the SS pin when used as input (which also precludes it's use as a slave). Bringing back slave without a plan to preserve that behavior for the much more common use case would be setting the trap for unwary users that Microchip has graciously provided us with a way to disarm.

Whether or not the official core has an interest in dealing with this, I'd love to get a better SPI implementation into megaTinyCore that would support slave mode, and I suspect @MCUdude would also for MegaCoreX (his excellent core for the megaAVR 0-series processors). We love pull requests - it sounds like you've done enough digging to sort out how to do this. In addition to everything else , you'd also want to deal with the behavior of the SS pin, (which would mean setting the SS pin as INPUT_PULLUP, and not disabling the SS pin, in addition to the interrupt stuff ), but only if slave mode was to be enabled...

Worth noting that the attachInterrupt() and detachInterrupt() methods even for classic AVRs just flip the bit enable or disable interrupts; presumably part of the problem was that there are now four interrupts associated with SPI, instead of one (frankly, the way the single interrupt was used on the classic AVRs for both signaling that a byte has transferred and calling the slave handler was a nightmare) - but the SPI implementation on the 0-series parts (as well as the tinyAVR 0-series and 1-series parts) gets byzantine when you move out of the well-behaved, easy to understand, world of SPI master, making a nice library that someone could just pick up and use difficult to achieve...

If you want to create an issue for it over in my megaTinyCore and work towards an improved version of SPI.h, I'd be happy to discuss. Be sure to start your work with our version of SPI.h - it's diverged a bit from the implementation on the official core in order to support a wider variety of parts with the whole pinswapping stuff (actually, you'll probably want to start with the version in MegaCoreX, not one of mine, because you're using those parts, and the pinswap stuff is different because we're targeting different boards)

tomalmy commented 3 years ago

I've used Uno boards in slave mode. I would expect that the megaAVR will do better. The two issues I've had is the lack of double buffering can lead to data loss and there is no interrupt for SS going high, requiring the use of a pin change interrupt. Well the ATmega4809 has the double buffering. It didn't provide an interrupt for SS going high however there's a pin change interrupt for every pin, so that is covered. Might not fit in with the Arduino library way of doing things but it can be done. I'm tackling the ATmega4809 SPI this coming week. If I can't get it to work I'll report back here. If I can I'll post some example code.

tomalmy commented 3 years ago

OK, here is some code for operating in slave mode with buffering on. Runs entirely within ISRs, which I consider a necessity for handling asynchronous events. Doesn't really integrate with the "Arduino way" of doing things, but consider this as a proof of concept that Slave mode works.

When in SPI slave mode, if there are slave devices in series (such that the number of bytes shifted is unknown to the slave) I haven't figured out a way to run with buffering turned on. I haven't given up yet, but am giving it a rest for now. It will work unbuffered but there is a possibility of data loss if there is insufficient time between bytes being transferred (the ISR must execute, and there can be some latency if other interrupt routines intervene). On the other hand, If I know exactly how many bytes will be shifted buffered mode works fine.

I put SS on pin 8. This is PE3 on the microcontroller. I've got macros to access the GPIO registers, ddr accesses the data direction register, intflag the interrupt flag register. I think that's the only nonstandard code here. This slave receives/sends 4 bytes. The received bytes are processed and sent back in the following transfer.

Setup is:

  PORTMUX.TWISPIROUTEA |= PORTMUX_SPI0_ALT2_gc;
  ddr.SLAVE_MISO = 1; // Config output pins
  PORTE.PIN3CTRL = PORT_ISC_RISING_gc; // Slave SS pin interrupts on rising edge
  SPI0_CTRLA = 0x41; // Enable in slave mode, LSB first
  SPI0_CTRLB = 0xc0; // Enable buffering
  SPI0_INTCTRL = 0xA0; // Interrupt when receive buffer has content or send buffer empty
// Data
volatile uint8_t receiveBuffer[4];
volatile uint8_t sendBuffer[4];
volatile uint8_t receiveIndex, sendIndex; // Index into above of next byte
volatile uint8_t dummy;

The ISR for the SPI receives and sends all the data it can. All data transfers are initiated in this ISR.

ISR(SPI0_INT_vect) { // We might be able to send or receive
  if ((SPI0_INTFLAGS & 0x20) != 0) { // transmit data register empty
    if (sendIndex < 4) { // more to send
      SPI0_DATA = sendBuffer[sendIndex];
      sendIndex++;
    } else { // All sent, disable interrupt
      SPI0_INTCTRL &= ~0x20;
    }
  }
  while ((SPI0_INTFLAGS & 0x80) != 0) { // data has arrived
    if (receiveIndex < 4) { // we have a place for it
      receiveBuffer[receiveIndex] = SPI0_DATA;
      receiveIndex++;
    } else { // Why is this here? just throw it out
      dummy = SPI0_DATA;
    }
  }
}

The interrupt on the rising edge of SS "processes" the data received by adding one to each byte and storing in the array for data to send. It is possible for the interrupt to occur before the final SPI interrupt, so it first reads any incoming data. Finally it resets and initializes for the next transmission. Cycling the enable bit for the SPI allows it to recover from glitches.

ISR(PORTE_PORT_vect) { // Interrupt vector for SS rising
  if (intflg.SLAVE_SS != 0) { // SS rising
    while ((SPI0_INTFLAGS & 0x80) != 0) {
      // there is a pending SPI interrupt, so do what it would have done
      // It can only be data received at this point
      if (receiveIndex < 4) { // we have a place for it
        receiveBuffer[receiveIndex] = SPI0_DATA;
        receiveIndex++;
      } else { // Why is this here? just throw it out
        dummy = SPI0_DATA;
      }
    }
    // Values here are correct!
    // Our values are in receiveBuffer. Let's "process" them
    for (uint8_t i = 0; i < 4; i++) {
      sendBuffer[i] = receiveBuffer[i] + 1;
    }
    // Reset indices and enable data register empty interrupt
    receiveIndex = 0;
    sendIndex = 0;
    SPI0_CTRLA &= ~1; // turn off
    SPI0_INTCTRL |= 0x20;
    SPI0_CTRLA |= 1; // turn back on

    intflg.SLAVE_SS = 1; // reset pin interrupt flag
  }
}
JustHomematic commented 2 years ago

Hi, I have a new project, I´want to connect the Raspberry Pi with a Nano Every (Mega 4809) per SPI. I have done ist with my Mini Pro, but the program does not work with the Nano Every, because of the library. Does someone has a solution?

tomalmy commented 2 years ago

If what I've posted just above doesn't do it for you, I can't really help you. Buffered operation (really needed for reliable use as an SPI slave) is possible but not directly supported by any existing Arduino library for the 4809 that I've seen, not saying there isn't one somewhere. I suggest people connect via I2C if the performance is adequate rather than using SPI slave mode. For long distances BACNET over RS422 is very reliable, is an industrial standard, and has readily available libraries.

SpenceKonde commented 2 years ago

Do you need to act as SPI master too?

If you do, that's going to be very hard to do with the SPI library getting in he way.

If not, the problem way easier... because you don't have to involve the SPI library at all

Also, on further reflection and experimentation, I found implementing SPI slave to be so straightforward if you didn't have a master to worry about that I don't even know that it's worth a library - even with buffered mode, it's one of the simplest peripherals on the part. a buffered slavbe library would just have a couple of functions to to like, turn it on and off.... and a macro\ to get tdata out annd put it in.