General approach of SPI communication between 2 µC - Page 1
Forum section: electronics / general

author: Johannsen
author_rank: Regular Contributor
timestamp: November 07, 2020, 01:43:52 am
content: Hi,I need to share data between 2 Atmega328P in both direction. Controller A has a display and buttons, controller B sensors, switches/relay and a wireless module.So µC A has to display the values collected by µC B and I need to be able to change settings which needs to be transfered back to B.µC sleeps all the time and is only responsible for user interface etc.I want to use SPI as in recent projects I2C made some problems from time to time and UART is to slow.I never dealed with a master-slave communication before. How would you do it? Do I need to define for every setting a byte variable which the master sends to the slave and gets the answer back. Or should I just sent all the settings, sensor values byte by byte?

author: rstofer
author_rank: Super Contributor
timestamp: November 07, 2020, 08:37:01 am
content: SPI is a master-slave protocol. One device is the master, all other devices are slaves. That part is easy: pick one end to be the boss.Now you need to come up with an unambiguous 2d layer protocol. The master sends a request and the slave replies when the master sends the next byte (usually 0xFF). By mutual agreement, there may need to be a bit of a delay before sending the next byte so the slave can gather up the data and place it in the transmit buffer. Some commands might not need a reply but sending a reply allows the slave to notify the master of any errors.So, in every case, the slave is sending either a reply to a command or a notice of error. Every cmd is one byte, every reply is one byte.Or, you can send a command followed by a lengthy data string as long as both ends agree. Perhaps the byte after the command contains the length of the following data string. The slave grabs all the data and then replies with success/fail.AFAIK, there is no standard way of doing this. You just have to draw it out on paper and implement it with code.The hard part: The slave code is not usually given. Most projects consider the uC to be the master and all of the slaves exist as peripheral chips and handle the protocol in hardware. Instead, the slave end is a uC and finding a library that supports this may be problematic. You will want to use interrupt driven receive code. When the SPI gadget gets a byte, it does a hardware interrupt and places it in buffer. The interrupt goes on to determine what type of command it is. Now it gets messy. If it was a command requiring a reply, the data needs to be placed in the output register. If it is the beginning of a long command (string), the interrupt routine will see every byte and either put the bytes in a circular queue or somehow buffer them. This part is hard because you can't get hung up in the interrupt handler.I suppose the slave code might be somewhat easier if the device doesn't need to do anything other than handle the SPI and get/set internal hardware settings without handling other tasks. Turn on an output, read a sensor, that kind of thing. But if the slave is involved with other time sensitive tasks, this protocol needs to be carefully thought out so the slave doesn't get jammed up in the interrupt handler.Or you add an RTOS layer to allow for separate tasks.There are a bunch of Arduino projects that have SPI slave. Maybe there will be something useful. I didn't really look.

author: voltsandjolts
author_rank: Unknown
timestamp: November 07, 2020, 08:52:40 am
content: Debugging is a pain with more than one micro.Wouldn't it be easier just to get a more powerful micro that can handle all the tasks required?

author: Siwastaja
author_rank: Super Contributor
timestamp: November 07, 2020, 10:41:31 am
content: shared header file defining packed structs containing all shared data, type-punned via union for easy raw access:Code:typedef union{struct __attribute__((packed)){uint8_t cmd;uint8_t reserved;uint16_t magic;int32_t temperature;float thing;char something;};uint8_t u8;} master_packet_t;typedef union{struct __attribute__((packed)){uint8_t response;uint16_t something_completely_different;int32_t funkiness_factor;uint8_t but_total_length_should_be_the_same;char something_else;double thing2;};uint8_t u8;} slave_packet_t;nSS = high ALWAYS RESETS STATE. Do this right; some SPI slave device manufacturers won't. In this case, state is byte counter.nSS goes low -> byte counter starts at 0.Share data of packet_t.Basically that's it.Sanity check: if nSS goes high prematurely, and new transfer starts later, does it start at 0 properly?Remember that command - response communication kinda sucks on SPI because you are receiving a response to the PREVIOUS command while giving a new command. There are ways to work around this, and sometimes you just don't need the response (i.e., commands set state which remain, no instant error codes available.) For example, I found it super-duper easy to use SPI to set motor speed setpoint and current limit for motor control MCU and return measured speed, current, etc.The pain level of debugging is exaggerated. Obviously, having to flash two devices and guaranteeing the data structure is always in sync is a minor PITA. I also recommend a single µC solution "by default", but sometimes multi-MCU is just OK or even easier, especially if they run different things requiring high CPU utilization which would be another can of worms to parallelize using interrupts on one MCU.

author: rstofer
author_rank: Super Contributor
timestamp: November 07, 2020, 04:59:23 pm
content: I would be a little cautious of structs if I were using a different compiler for each end. One would hope that if gcc worked on both ends, even if different flavors, that struct alignment would work as well but it needs to be checked.That reset condition as a result of nSS going high is an important concept. Resetting the state machine will probably be important. Hopefully the peripheral gadget honors that.Since we're talking ATmega328P (Arduino chip type), I would start with the library code. I might start with this:https://forum.arduino.cc/index.php?topic=52111.0The Arduino library code seems to be for a master only. That's not unexpected, SPI Slave is going to be awkward.https://www.arduino.cc/en/Reference/SPIHere's a replacement SPI library for master and slave. I didn't spend a lot of time with it but I don't see a case where the slave sends a response to a query from a master. This is the awkward bit because the slave needs to decode the command, get the required data to the shift register and do this before the master starts clocking the output. Yes, the master can allow a slight delay before clocking the response but the important thing to remember is that the master does ALL of the clocking.https://github.com/javifercep/SPI-Slave/tree/master/SPIThere seems to be a large number of Arduino projects using SPI so something is almost bound to be useful.

author: Siwastaja
author_rank: Super Contributor
timestamp: November 07, 2020, 05:15:41 pm
content: Quote from: rstofer on November 07, 2020, 04:59:23 pmThis is not true. Basically all decent compilers have packed attribute. The syntax may vary; you may want to wrap it in a common utility header.Packed struct is well defined and the only potential issue left is endianness. Use endianness conversion macros whenever really needed; in 99% cases today, everything's little endian. One option is to follow ataradov's good advice of ignoring big endian cpus.Incompatibility is mostly theoretical and pedantic, completely detached from practical reality.Especially in multi-µC, as discussed here, the endianness (and even the compiler version, which is completely irrelevant though) is the same on both ends and well controllable by the designer.Advantage of using a single header and struct which is accessible directly without parsing functions is so large what comes to code clarity, maintainability and avoiding human errors, that it's well worth of the theoretical "risk".It really sucks to explicitly serialize every field of your data type, then parse it back. A lot of code to write, maintain, keep in sync, and absolutely for no reason. I guess this is why many people hate multi-µC communication so much. But often this can be avoided utilizing the language and basic extensions provided by all compilers worth their penny.Same thing for type-punning through union; C11 allows this; but basically all usable compilers allowed this even before. Just do it. Don't write explicit code for moving data around. Data is just data.

author: NorthGuy
author_rank: Super Contributor
timestamp: November 07, 2020, 05:46:36 pm
content: Quote from: Johannsen on November 07, 2020, 01:43:52 amWhat speed do you need? UART can go to several MBaud. On small MCUs, max speed of UART will be roughly the same as max speed of SPI.

author: Siwastaja
author_rank: Super Contributor
timestamp: November 07, 2020, 05:52:07 pm
content: For ATMega328, the smallest SPI prescaler is 2, enabling 8Mbit/s communication (I didn't check if ATMega328 slave can handle this, but I assume so). For UART, the "double speed mode" divides the clock by 8, enabling max 2Mbaud/s communication.USART is typically a few times slower because USART is designed to sample each bit multiple times for increased robustness against noise, while SPI works just like normal digital logic, sampling once, synchronously to the clock signal.

author: rstofer
author_rank: Super Contributor
timestamp: November 07, 2020, 06:38:58 pm
content: Quote from: Siwastaja on November 07, 2020, 05:15:41 pmGiven that both ends are ATmega328p devices, this shouldn't be a problem. When I wrote the bit about being cautious I was thinking that one end was a different device.In my experiments, 'packed' has always worked but my experiments are necessarily limited.In any event, the compiler is the same from end to end so no likely issues.

author: Siwastaja
author_rank: Super Contributor
timestamp: November 08, 2020, 09:31:39 am
content: Yes, as with anything, you need to know how to do it properly and be aware of the potential risk if not done properly.

author: tggzzz
author_rank: Super Contributor
timestamp: November 08, 2020, 10:04:59 am
content: Quote from: voltsandjolts on November 07, 2020, 08:52:40 amThe more "powerful" the micro, the more difficult it is to determineworst-case latencies required forhardrealtime operation. Caches and interrupts complicate measurements, let alone predictions.From that point of view, the ideal is a simple MCU doing only one thing, when it stalls until there is something to do. If you need two things to be done, add another MCU and comms between themFortunately such simple MCUs, communications hardware and communication protocol software already exists, and you can get up to 32 of them in a single 4000MIPS chipThe comms hardware and software is the bit usually "forgotten about", but is absolutely key.

author: Psi
author_rank: Super Contributor
timestamp: November 08, 2020, 10:05:01 am
content: When doing comms between two MCUs on a single PCB I sometimes find it simpler/easier/faster to just dedicate an entire 8 or 16 bit port between the MCUs with an extra pin for clock and maybe one for direction.You can drive the clock pin from HW timer and read the 8bit data bus on pin change int.It's pretty fast even though it's not using any hardware peripheral.For bi-directional or multi-node comms it gets more complex, but not by that much.Can even do it with DMA to GPIO if you really want to.

author: ogden
author_rank: Super Contributor
timestamp: November 08, 2020, 10:39:28 am
content: Quote from: Johannsen on November 07, 2020, 01:43:52 amUART too slow? For what? Even 9600 bps is enough to update 30 (32bit) values 10 times per second. You did not tell reason why you want multi-MCU system which inevitably is added complexity and point of potential failure. Not enough memory? - Get bigger MCU. Keyboard too far from MCU? - Use port extender.

author: voltsandjolts
author_rank: Unknown
timestamp: November 08, 2020, 10:47:02 am
content: Quote from: tggzzz on November 08, 2020, 10:04:59 amWell done indeed, sir, what a brilliant solution for driving the relays, switches and sensors the OP listed in the first post.Your blinkys' must be truly something to behold.

author: tggzzz
author_rank: Super Contributor
timestamp: November 08, 2020, 10:53:37 am
content: Quote from: voltsandjolts on November 08, 2020, 10:47:02 amOh, picky picky picky.But still a single chip solution where others have considered multichip solutions

author: snarkysparky
author_rank: Frequent Contributor
timestamp: November 08, 2020, 06:04:52 pm
content: I did this recently by setting up message buffers on both ends. The master will write a null value out in order to get data back from the slave if nothing else is going on at some periodic time. Incoming data on both ends is placed in a message buffer with each message long enough to stand alone ( meaning all its info is in the message, no dependency on prior or future messages). Then each micro can process the messages from the buffers on its own time without worrying about synchronization between the two.

author: Johannsen
author_rank: Regular Contributor
timestamp: November 09, 2020, 04:15:23 pm
content: Thanks all for your posts,there are some good hints for me. The struct method would be very useful for my application.I'm using two mcu because one of them handles lora sending and receiving plus is sleeping when nothing is received. A bigger mcu that I'm capable of programming has a much higher current in sleep mode also it's not that easy or even possible for me to implement the lora lib for another mcu than the 328P.I'm trying to understand the library in the shared linkhttp://www.gammon.com.au/forum/?id=10892&reply=2#reply2and try to implement the struct method.It causes me quite a headache to simply modify it to send bytes instead of char. As soon I understood that I will try the struct thing.

author: ogden
author_rank: Super Contributor
timestamp: November 09, 2020, 05:21:17 pm
content: Consider to implement SPI slave MCU as addressable memory R/W byte array, kinda partially emulating SPI EEPROM i/o "protocol". Note that you can fully or partially read/write structures as bytes too. Important to "arrange" variables in your structure "most used first". When you will actually use partial transmission of struct - you sill see why it is good idea.

author: nctnico
author_rank: Super Contributor
timestamp: November 09, 2020, 05:44:33 pm
content: Quote from: voltsandjolts on November 07, 2020, 08:52:40 amI agree with this. This sounds like a system which can do perfectly fine with 1 microcontroller. Learning how to use a different microcontroller will be much quicker in the end than trying to glue a system with 2 microcontrollers together. Multi-processor systems are a real pain in the ass to get right. This is an excellent moment to step out of the comfort zone and learn how a screwdriver works instead of using a hammer to drive screws in.But if you have to use multiple microcontrollers: do yourself a favour and use a serial port (I don't see how a uart can be to slow for a UI) and a text based protocol. Way easier to debug and less prone for versions to go out of sync. When using structs both microcontrollers need to use the exact same version of the struct or the system will break. This will seriously hamper future extensions where a text based protocol can have extra commands or extra fields and keep full backward compatibility. Recentl I converted an inherited project from structs to text based messaging and debugging went from 'not possible' to 'easy'.

author: tggzzz
author_rank: Super Contributor
timestamp: November 09, 2020, 05:50:32 pm
content: Quote from: nctnico on November 09, 2020, 05:44:33 pmYour last point doesn't match my experience. One advantage of two processors is that you can stop/start one completely independently of the other. That's a little more problematic with two processes on one processor.But ultimately either will work, and the ease is largely determined by the degree of coupling, the implementation strategy, and the testing techniques.

author: ogden
author_rank: Super Contributor
timestamp: November 09, 2020, 05:59:26 pm
content: Quote from: nctnico on November 09, 2020, 05:44:33 pmI am afraid that this approach is not universal. Bad advice for small one-developer system where MCU's are running out of code memory long before project is even started. Good advice in case system is distributed from all points of view including time, resources and development.

author: nctnico
author_rank: Super Contributor
timestamp: November 09, 2020, 07:32:55 pm
content: Quote from: ogden on November 09, 2020, 05:59:26 pmYou'd be surprised how little code it takes to implement a command line interface. If you have some kind of UI with a display then most of the string handling functions needed are already included in the code. Also flash is cheap nowadays so there is very little savings versus the gain in making development faster. Unless you produce something in 50k units or more the development time is dominating the cost of a product.

author: nctnico
author_rank: Super Contributor
timestamp: November 09, 2020, 07:36:00 pm
content: Quote from: tggzzz on November 09, 2020, 05:50:32 pmYou are talking about multi-core systems and not multi-processor systems in which the cores are physically seperated. In the latter case you'll need to deal with the other processer being dead or not receiving a message due to interference. But even in a multi-core system you can shoot yourself in the foot in horrible ways. A single core running a round robin scheduler for low priority tasks and an interrupt for high priority stuff (and do all the processing inside the interrupt in order to minimise interaction between the processes) is a much more simpler & robust approach. But it requires some resource planning & verification.

author: ledtester
author_rank: Super Contributor
timestamp: November 09, 2020, 07:39:58 pm
content: QuoteWhat's the nature of uC B's operation? Maybe you could just attach a GPIO port expander to uC A and get rid of B.

author: Johannsen
author_rank: Regular Contributor
timestamp: November 09, 2020, 09:04:00 pm
content: QuoteuC handles the wireless stuff and switches outputs dependend on the received data. In short eg a sensor sends his ID. uC B has an array with valid IDs that can trigger a ouput. Those IDs I need to be able to change and that's what uC A does. The wireless stuff is time critical and plus I need some bigger flash to store all the programm für the GUI(menu) and some other peripherals.I see and agree that debugging could be a pain in the ** but a suitable uC could be some low power STM and I can't reprogram all the libraries I'm using plus I really don't know and that's the biggest issue how to do the communication with the lora module.That's just a hobby project, I'm not a prof. programmer and don't get it.I did a controller some years ago with an ESP8266 and an 328P connected via UART and it's working but you feel the delay when transmitting many values.

author: JOEBOBSICLE
author_rank: Regular Contributor
timestamp: November 09, 2020, 09:06:15 pm
content: You don't need two microcontrollers.Use a 32 bitter at 100MHz + and you can do almost anything on one micro. Heck you can drive 100k pixels at 60hz + on a modern cortex m

author: NorthGuy
author_rank: Super Contributor
timestamp: November 09, 2020, 09:27:09 pm
content: Quote from: Johannsen on November 09, 2020, 09:04:00 pmWhat baud rate have you used?

author: Johannsen
author_rank: Regular Contributor
timestamp: November 09, 2020, 09:28:29 pm
content: >Use a 32 bitter at 100MHz + and you can do almost anything on one micro. Heck you can drive 100k pixels at 60hz + on a modern cortex mI could use a SAMD21 but this needs much more power in sleep than a 328P>What baud rate have you used? I think it was around 9600

author: ledtester
author_rank: Super Contributor
timestamp: November 09, 2020, 09:45:42 pm
content: How much data do you need to transfer and how often?UARTs can transmit at 1 M baud and greater which is close to what you can get with SPI.

author: NorthGuy
author_rank: Super Contributor
timestamp: November 10, 2020, 03:02:19 am
content: Quote from: Johannsen on November 09, 2020, 09:28:29 pmSo, use 1 MBaud - it's 100 times faster than 9600.SPI will be slow too if you do it at 10 kHz.

author: ogden
author_rank: Super Contributor
timestamp: November 10, 2020, 05:36:47 am
content: Quote from: Johannsen on November 09, 2020, 09:28:29 pmDoes 4.6uA versus 0.9uA really matter much in your application? What power source do you use?

author: Johannsen
author_rank: Regular Contributor
timestamp: November 11, 2020, 09:28:34 am
content: Its around 200 Bytes every 100ms. As the 328P only provides 1 hardware serial I need to use software serial, a example works with 4800bd. I need hardware serial for flashing.>So, use 1 MBaud - it's 100 times faster than 9600.if you provide me a working example I will try it. Meanwhile it seems that a stable SPI connection with a 328P as slave is not possible. I used Nick Gammons example but it seems very time critical in the ISR and any other operation makes it unreliable sadly.>4.6uA versus 0.9uAI'm using AA batteries or if temperatur tests will pass also LiIon batteries. If I won't succeed I have to reconsider SAMD21 or even a new chip like STM32L but wished to get the whole thing running this year..

author: voltsandjolts
author_rank: Unknown
timestamp: November 11, 2020, 11:26:49 am
content: Buddy, I say this in a nice way, but all this is crazy talk.Don't waste your time mashing square pegs into round holes.Pick an MCU suited to the task at hand.

author: JOEBOBSICLE
author_rank: Regular Contributor
timestamp: November 11, 2020, 12:58:01 pm
content: It's not just sleep current, sounds like you'll burn a lot of energy doing all this serial transmission and processing.

author: Siwastaja
author_rank: Super Contributor
timestamp: November 11, 2020, 05:21:03 pm
content: Work with your software serial implementation. At 4800 bps, 200 bytes, say 2000 bauds, takes nearly 500ms to transmit, so obviously you can't do it every 100ms. Even if you up the speed by 10x, you would be running the CPUs at full power at 50% duty cycle, terrible for power.So do it as quickly as humanly possible, optimize the shit out of the code, so you can go back to sleep as early as possible. I don't see why you couldn't get somewhere close to 1Mbaud/s even with a bit-banged serial, but for a proper, less power hungry solution, pickanotherAVR MCU, there are those with two uarts, so you can leverage your existing knowledge and code. With a on-chip uart peripheral instead of software implementation, you can sleep the core and wake up on interrupt when a full byte has been received for processing.More generally, your problem seems to be that you are trying out example code from others but don't have enough experience to write your own, test it, and solve problems thatinevitablyarise. The good news is, you gain experienceby doing. Pick any proposed solutions (SPI slave despite your code currently being "unstable"; or the software bitbang serial despite your code currently being "slow"); then do some serious and concentrated work on it to make it work. Look at every issue: why is this happening? Add instrumentation: measure. Try a solution. Rinse & repeat.

author: ogden
author_rank: Super Contributor
timestamp: November 11, 2020, 05:25:40 pm
content: Quote from: Johannsen on November 11, 2020, 09:28:34 amAt 4.6uA cheap 2000mAh AA alkaline could last for 49.6 years. In theory. In practice AA alkaline self-discharge current exceeds sleep current ofoneten SAMD21 MCU's combined. So don't bother, just take ARM MCU.

author: bson
author_rank: Unknown
timestamp: November 12, 2020, 06:21:59 pm
content: Add an interrupt line. This way the slave can get the attention of the master without polling it for data or state changes, and the slave can indicate when an operation is finished and it has a response. The master sends a command and goes off and does whatever it normally does. When the slave is done it pull the interrupt, the master in its interrupt handler first checks if it has a pending operation and if so what (or more cleanly, a pending interrupt-context continuation). When there is no more pending work, it checks the state of the device to see if there's something that calls for action. For this reason it can make good sense that all command responses include any actionable device state (such as, for example, if there is more to be read) without requiring another command to be sent for it.

author: Siwastaja
author_rank: Super Contributor
timestamp: November 12, 2020, 06:39:38 pm
content: +1 to interrupt, but you don't even need a separate interrupt line, just use a pin capable of INTerrupt or Pin Change INTerrupt as the communication pin. Interrupt on activity on this pin, then handle the communication in the interrupt handler.For such long packet (200 bytes), you need some kind of clock synchronization mechanism if you don't want to use another line for clock. If using two IOs per link is not a problem, adding a clock signal for synchronous communication is easiest.You can experiment with this idea which should get you some 1Mbaud/s at fcpu = 16MHzSender:Change clock 0 -> 1Change clock 1 -> 0wait for a few cycles so that receiver has time to enter ISRnext_bit:Output next data bitChange Clock 0 -> 1Change Clock 1 -> 0(possibly tiny delay, maybe 1 NOP, if needed)if not all bits sent: goto next_bitreturnReceiver ISR (Triggered from sleep by clock pin change interrupt):next_bit:wait for clock = 1read next data bitif not all bits received: goto next_bitreturnPersonally, I have implemented a 40 kbaud/s (20 kbit/s) Manchester-encoded single-pin communication between ATTiny25's running at 8MHz, with 8x oversampling of each baud, in C, with some time to spare. This realigns the clocking on each bit, allowing massive frequency difference in clocks. This is a lot more complex and slower than what the OP probably wants to do, though.

author: Johannsen
author_rank: Regular Contributor
timestamp: November 14, 2020, 11:10:04 am
content: Quote from: ogden on November 11, 2020, 05:25:40 pmThat's maybe what the datasheet says. I couldn't measure a current below approx 500µA in sleep.Even when having a look at the Erratahttp://ww1.microchip.com/downloads/en/DeviceDoc/SAM-D21-Family-Silicon-Errata-and-DataSheet-Clarification-DS80000760D.pdfyou can add another 20µA for a proper operation.Quote

author: Johannsen
author_rank: Regular Contributor
timestamp: November 14, 2020, 11:16:32 am
content: Quote from: bson on November 12, 2020, 06:21:59 pmI'll give this another try but I'm not sure as Nick Gammon's code exist for a non- and a interrupt operation. I tested both.

author: ogden
author_rank: Super Contributor
timestamp: November 14, 2020, 12:41:35 pm
content: Quote from: Johannsen on November 14, 2020, 11:10:04 amIt is highly unlikely that SAMD21 consume 0.5 mA during sleep. Most likely it does not actually sleep. You shall double-check or tell what and how you measure.To restate obvious - with modern low power MCU's you do not need to worry about sleep current when using AA alkaline battery pack. SAMD21 will be fine in this application. Do you really think that two MCU's sending&processing 200 Bytes every 100ms will draw less than 20uA average current? Before you invest too much into multi-MCU design, at least calculate power budget or do some tests.

author: Siwastaja
author_rank: Super Contributor
timestamp: November 14, 2020, 12:44:42 pm
content: Quote from: Johannsen on November 14, 2020, 11:16:32 amQuite frankly, I think you should get past the idea of copy-pasting someone else's code for doing something this simple.Write your own, test and measure, adjust as necessary.Similarly, try to seewhat is wrongwith your power consumption. You justcan't avoidsolving problems that inevitably arise; if you every time give up and use another piece of copy-paste code, or change to another MCU, you may be able to "solve" that particular problem, but get ten new problems.

author: Psi
author_rank: Super Contributor
timestamp: December 01, 2020, 08:34:09 am
content: Unless someone is paying you to do it, the amount of time wasted on the interface code getting the two micros talking is not worth the bother to re-jig for a single bigger mcu.If you have to do it, always code one MCU to act like an io expander for the main MCU and put all your logic there. Trying to share logic gets super complex super fast.