Hi again everybody! Once again, I let a bunch of time elapse before writing another article in my microcontroller programming series. I left off last time by mentioning that most microcontrollers have a built-in SPI peripheral that handles the SPI communication protocol for you. Today, I’m going to talk about how such a peripheral would work. I’m going to do it differently from I have done in the past, though. This time, instead of making up a theoretical peripheral, I’m going to actually walk through an actual peripheral built-in to a real microcontroller! I’m going to be using the AVR ATmega328P, which is also the chip used in the Arduino Uno. Note: I don’t actually have an Arduino Uno (nor anything else with an ATmega328P for that matter), but I figured I would review the peripheral and talk about how to make it work.
We’re going to pretend to talk to some random SPI slave device, and I’ll show you how to write (and read) a byte to it. In this situation, the ATmega328P will be the master device.
As you may (or may not) remember, SPI uses four pins: MISO, MOSI, CLK, and CS. Before we do anything, we need to initialize these four pins as inputs or outputs. Let’s take a second to figure out how the pins need to be configured. MISO is master in, slave out. This means it will be an input on the master, since data is coming from the slave to the master. MOSI is the opposite, so it will be an output. CLK and CS are both controlled by the master, so they will also be outputs. So the first line of business is to set MISO as an input and the other three pins as outputs.
On the AVR we’re using, port B is where the SPI functions are located. On port B, pin 2 is chip select (they call it slave select, but it means the same thing), pin 3 is MOSI, pin 4 is MISO, and pin 5 is CLK. So using the AVR’s data direction register, let’s make sure that port B, pin 4 is configured as an input, while port B, pins 2, 3, and 5 are outputs.
DDRB |= ((1 << 2) | (1 << 3) | (1 << 5)); DDRB &= ~(1 << 4);
This will turn on bits 2, 3, and 5 of the port B’s DDR register, and turn off bit 4.
Now, before we do any more register reading/writing, let’s look at the SPI registers available in the ATmega328P’s datasheet. This is more or less going to be the same information available in the datasheet, but I’d like to walk through it to describe which registers are important and which ones are just small configuration things that aren’t really relevant to understanding how the peripheral works.
The first register is SPCR, the SPI control register.
- Bit 7 is SPIE — the SPI interrupt enable. If this bit is turned on, you will get an interrupt from the SPI peripheral whenever a transmission completes. For now, we don’t want to worry about interrupts, so we will leave this off.
- Bit 6 is SPE — SPI enable. This bit actually turns on the SPI peripheral, so we will definitely want to turn it on.
- Bit 5 is DORD — data order. When it’s 1, data is transmitted serially least-significant bit first. 0 means most-significant bit first. What you set here depends on the slave you will be talking to. For our purposes, let’s assume the slave we will be communicating with expects to receive data least-significant bit first, so we will set it to 1.
- Bit 4 is MSTR — master/slave select. We will be setting this to a 1 to ensure that the AVR will act as a master rather than a slave.
- Bit 3 is CPOL — clock polarity. This is another one that’s dependent on the slave you’re talking to. Some slaves expect you to keep the clock line high when you’re not communicating to the slave, and others expect you to leave it low. The slave datasheet will tell you which one you’re supposed to use. For our purposes, let’s assume we are supposed to set CPOL to 1.
- Bit 2 is CPHA — clock phase. This one goes hand-in-hand with CPOL, and is another one that you will have to determine by looking at the slave’s datasheet. It has to do with when data is sampled. We will assume CPHA is supposed to be 0.
- Quick note: CPOL and CPHA are sometimes treated together as a 2-bit value called the SPI mode. In our case, with CPOL 1 and CPHA 0, we are using SPI mode 2 (binary 10). Some SPI slave datasheets will specify a mode number rather than CPOL or CPHA, or might only show a signal diagram in which you will have to figure out for yourself what CPOL and CPHA are. See the datasheet for more info on this, but it’s not really important for understanding how to use the SPI peripheral.
- Bits 1 and 0 (SPR1 and SPR0) together determine the SPI clock rate divider. They will allow you to divide the CPU clock by 4, 16, 64, or 128 to determine an SPI clock rate (how fast the CLK pin will toggle from low to high and low again while you are sending data to the slave). The datasheet also mentions that if you set the SPI2X bit of the SPI status register, you can divide the clock by 2, 8, 32, or 64 instead. For our purposes, let’s assume the CPU clock rate is 8 MHz and our SPI peripheral’s maximum clock rate is 500 KHz. Thus, we can set the divider to 16 (so the divider bits will be 01 and SPI2X will be 0), which will give us an exact SPI clock rate of 500 KHz (0.5 MHz).
I’ll also go through the SPI status register (SPSR) really quickly since I have already mentioned it. Usually, status registers are read-only, and they exist to tell you the status of the peripheral. In this case, the SPI2X bit is a special exception.
- Bit 7 (SPIF) is the SPI interrupt flag, which is just a flag that gets set to 1 whenever a transfer has completed. Even though we’re not using interrupts in this simple example, we will still look at this bit to determine when an SPI transfer is complete.
- Bit 6 (WCOL) is the write collision flag, which lets you know if you wrote to the data register while a transfer was still in progress (you’re not supposed to do that). I consider this a fairly useless bit because you should ensure your code doesn’t try to write to the data register while a transfer is already in progress.
- Bit 0 is the SPI2X bit that I mentioned earlier — it changes the clock rate as I mentioned.
There is one more register: the SPI data register (SPDR). This is the register you write to in order to begin an SPI transmission. If you want to send 0x52 over SPI, you would write 0x52 to this register, and then wait for the transmission to complete. Then, you can read from this same SPI data register to see the eight bits that the slave sent back to you while you were sending the 0x52 to it.
That’s it! That’s all there is to the SPI peripheral in the AVR. You’ll notice that it doesn’t provide any options for 16- or 32-bit transmissions, but you can do it yourself by sending 2 or 4 successive 8-bit transmissions. Also, there’s one other thing I should point out. The datasheet says that the SPI interface does not automatically control the SS (chip select) line. So you need to handle it yourself before starting a transmission and after a transmission is complete. Let’s assume that the chip select line should be high when idle, and low when you’re talking to the chip. OK, so let’s write some code!
We have already initialized the port direction registers, so now let’s turn on the SPI peripheral:
SPCR = (1 << SPE) | (1 << DORD) | (1 << MSTR) | (1 << CPOL) | (1 << SPR0);
I went ahead and left out the bits I set to zero, but you could insert them as (0 << BITNAME) if you want it to be completely clear.
We should probably also ensure that the SPI2X bit in the status register is off:
SPSR &= ~(1 << SPI2X);
OK, so now the SPI peripheral is pretty much ready for action! Let’s do one last part of preparation and ensure that the chip select pin is high, which it should be while idle:
PORTB |= (1 << 2);
We don’t have to worry about any of the other pins, because the AVR’s SPI peripheral has taken control of them at this point.
Now, let’s send the 0x52 byte to the slave:
// Pull chip select low to assert it (activating the slave) PORTB &= ~(1 << 2); // Send 0x52 to the slave SPDR = 0x52;
At this point, we have started to send 0x52 to the slave. But the instruction will complete before the peripheral has finished sending the data to the slave. So now, before we do anything else (such as trying to send another byte or reading what the slave sent to us), we need to wait for the transmission to complete.
If we had enabled interrupts and created an interrupt routine, we could go on to doing other stuff and an interrupt would occur as soon as the transmission finished. But since this example is not interrupt-driven, we will now poll the status register until we know that the transmission has completed:
while ((SPSR & (1 << SPIF)) == 0);
This code waits until the SPIF bit of the status register becomes 1. This means that the transmission has completed, so we can now read the 8 bits that the chip sent to us:
uint8_t result = SPDR;
This act of waiting until the SPIF bit becomes set and then reading the SPDR register will clear the SPIF bit (the datasheet for the AVR says so). So next time we begin a transfer, we can do the same thing — wait until the SPIF bit goes high again, and then read the SPDR register.
The last thing we should do, now that we’re done, is pull chip select high to complete the transfer. Some slaves might prefer for chip select to stay low until several bytes have been sent and received, but we will assume that this chip only wants chip select to go low for a single byte and then return high.
PORTB |= (1 << 2);
There you go. You now know how to use the SPI peripheral in an AVR microcontroller. You’ll find that most of the 8-bit AVRs have an SPI controller very similar to this one. You’ll also find that this is basically how the SPI peripheral works in all microcontrollers. The toughest part is getting all of the setup values correct, particularly the CPHA and CPOL values. Once you have that in place, the rest of it is really, really simple.
I didn’t cover handling SPI with interrupts, but it’s not much more difficult than this — in the SPI interrupt handler, you can read back the data and either begin another transfer or pull chip select high to finish the transfer. If you have a big list of bytes that need to be sent as quickly as possible, interrupt-driven SPI would be ideal to take care of that.
That’s all I have for SPI. Tune in next time for a discussion of some other yet-to-be-determined microcontroller peripheral!