SPI stands for serial peripheral interface, and it’s a communication protocol commonly used for exchanging data between special purpose integrated circuits (ICs) and computing devices (like your Propeller microcontroller). A wide variety of sensor chips use SPI, including ambient temperature sensors, A/D and D/A converters, and the 3-axis accelerometer module in this example.
Some SPI devices will have libraries that support them, with pre-written functions, documentation, and example code to make them easier to use. If the SPI device you need for your project doesn’t have a library, incorporating it into your project will involve reading the device’s datasheet and using that information to write code to make your microcontroller communicate with the device.
This example uses a datasheet to develop test code that communicates with the MMA7455 3-Axis Accelerometer module.
WAIT! What do you want to do?
If you just want to get a quick start using the 3-Axis Accelerometer Module, try the handy mma7455 library in the MMA7455 3-Axis Accelerometer Simple Devices tutorial [1]. If you want to see an example of communicating with an SPI device using shift_out and shift_in functions, keep reading!
For this activity, we’ll use the MMA7455 3-Axis Accelerometer Module and some jumper wires to connect it to 3.3 V, ground, and Propeller I/O pins.
In addition to 3.3 V and GND, this circuit has three connections for SPI communication:
CLK — it’s the clock line, connected to P8 in our example. The Propeller microcontroller sends series of high/low signals on this line, to tell the device it’s time to check for (or send) a data bit on the DATA line. (The CLK line might be labeled SCLK or SCK on other devices.)
DATA — Binary values are sent and received on this line, which is connected to P7 here. Data is sent with each repetition of the CLK line’s signal.
CS — this stands for “chip select” line, which is connected to P6 in this circuit. The microcontroller sets a given device’s CS line low during data exchanges, and takes it high again when done. (You might see this line labeled nCS, CSB, CNS, NSS, STE, or SYNC on other devices.)
Let's connect the Propeller microcontroller and an SPI device — the MMA7455 3-Axis Accelerometer module — to form an SPI bus so they can communicate.
Note from the schematic that the DATA line, connected to P7, is bidirectional. Other SPI chips may need buses with separate data-out and data-in lines. The chip’s documentation might label a data-out line DOUT, DO, or MISO (microcontroller-in-serial-out). Likewise, the chip’s data-in line might be labeled DIN, DI, or MOSI (microcontroller-out-serial-in). A bidirectional line might be labeled DIO.
If you haven't already installed the latest USB driver, SimpleIDE, or Learn folder, go to Propeller C - Set up SimpleIDE [3] and Propeller C - Start Simple [4].
For simplicity, this test code only monitors the accelerometer’s z-axis. Setting the board flat will align the z-axis with the earth’s gravitational field. This gives a measurement of 64, representing 1 g of gravitational pull on the mass inside the accelerometer. Turn the board upside-down, and you’ll get a -64; and setting it on its edge should result in a measurement near zero.
/* MMA7455 Test Z Axis.c */ #include "simpletools.h" // Include simpletools lib signed char z; // Z-axis value int main() // Main function { high(6); // CS line high (inactive) low(8); // CLK line low low(6); // CS -> low start SPI shift_out(7, 8, MSBFIRST, 7, 0b1010110); // Write MCTL register shift_out(7, 8, MSBFIRST, 1, 0b0); // Send don't-care bit shift_out(7, 8, MSBFIRST, 8, 0b01100101); // Value for MCTL register high(6); // CS -> high stop SPI pause(1); while(1) // Main loop { low(6); // CS low selects chip shift_out(7, 8, MSBFIRST, 7, 0b0001000); // Send read register address shift_out(7, 8, MSBFIRST, 1, 0b0); // Send don't-care value z = shift_in(7, 8, MSBPRE, 8); // Get value from register high(6); // De-select chip print("%c z = %d%c", HOME, z, CLREOL); // Display measurement pause(500); // Wait 0.5 s before repeat } }
On this page, we'll go through MMA7455 Test Z Axis.c a little bit at a time to see how the shift_out and shift_in functions communicate with the SPI device.
This is the part where you’ll want to go get the MMA7455 datasheet. Each time you run into a term like MCTL or ZOUT8, use your .pdf reader’s search function to find and read about all instances of each term. As you do so, it will help build a picture of how to use a device’s datasheet to write code to communicate with the device.
The program includes the simpletools library since it uses pause, print and a number of other simpletools functions. The simpletools library also has functions named shift_in for receiving SPI data and shift_out for transmitting it.
#include "simpletools.h" // Include simpletools lib
An int variable named z is declared for storing the z-axis accelerometer measurement.
signed char z; // Z-axis value
The first two statements in the main function initialize the I/O pins connected to the SPI bus, getting them ready for the communication signaling to come. The high(6) call sets the chip’s chip select (CS) line high, which tells the chip that it doesn’t have to communicate at the moment. The low(8) call sets the I/O pin connected to the chip’s CLK line low, preparing it to send brief high signals called clock pulses.
int main() // Main function { high(6); // CS line high (inactive) low(8); // CLK line low
The accelerometer’s datasheet has instructions for configuring the chip’s mode of operation, which must be done before reading measurements from it. Configuring the mode entails sending several values to the chip using SPI signaling. But, before sending any values, the microcontroller has to set the chip’s /CS line low, which tells the accelerometer chip to wake up and pay attention to signals that it’s about to receive on the CLK and DATA lines. Since Propeller I/O pin P6 is connected to the chip’s /CS pin, low(6) does the job.
Next, the microcontroller has to send a set of three values on the data line. Each value has to have a certain number of bits, that is, binary 1/0 digits. The first value has 7 bits, followed by a 1-bit value, and then an 8-bit value. A brief high pulse to the CLK line tells the chip to get each bit one at a time from the DATA line. If the DATA line is high, the chip interprets that bit as a binary 1. If it’s low, the chip interprets that as a binary 0.
You can see these signals on the CS, CLK, and DATA lines below.
The simpletools library has a function called shift_out that takes care of setting DATA line values and sending pulses to the CLK line. In the drawing above, you can see how a separate shift_out call was used for the 7-bit, 1-bit, and 8-bit values needed for configuring the sensor.
The shift_out function has five parameters: pinDat, pinClk, mode, bits, and value. Let’s look at what’s happening with each parameter in the first call: shift_out(7, 8, MSBFIRST, 7, 0b1010000).
Let’s look at each parameter:
After that, two more shift_out commands send two more binary values. The first is a single-bit value 0b0, and the second is the 8-bit value 0b01100101.
So, how did we know to send those values? The answer is, someone had to read the MMA7455 datasheet, understand how the device needed to be configured, and then write code to send those configuration messages. Each device will be different, so it’s a necessary step in writing SPI drivers. A chip’s datasheet will include descriptions of its registers (small bits of memory) that your program will need to read or write to in order to use the device. A register typically has a name to identify its purpose, and an address to use when interacting with it.
The MMA7455 datasheet explains that the first step in configuration is to send a 7-bit command. This command has to tell the chip whether your program will send a value to be stored in one of the chip’s registers (write operation) or if the chip is going to have to tell you what value a certain register stores (read operation). The leftmost digit in the binary command value has to be set to 1 for write or 0 for read. The next six binary digits have to contain the address of the chip’s memory where this value is going to get stored.
The configuration register is named MCTL, and its address is 0x16 (hexadecimal 16). Converting to decimal, that’s 1x16 + 6x1 = 22. In 6-bit binary, that’s 010110 = 0x32 + 1x16 + 0x8 + 1x4 + 1x2 + 0x1. The 7th binary digit (counting from the right) has to be a 1 for a write operation. That gives us 1010110. With the 0b prefix added, our command for the value parameter is 0b1010110. (Don't worry, we’ll take a look at an easy way to let your code handle the hexadecimal-to-binary conversion later).
shift_out(7, 8, MSBFIRST, 7, 0b1010110); // Write MCTL register
Next, the datasheet says that the chip needs to receive a “don’t care” value. Basically, that means it needs to receive a pulse on the CLK line, with either a 1 or 0 (we don’t care) on the DATA line. This shift_out call does the job, sending 0b0 for the value parameter, though we could have used 0b1.
shift_out(7, 8, MSBFIRST, 1, 0b0); // Send don't-care value
Now that the program is done telling the chip to get ready to receive a value that it has to write to its MCTL register (at address 0x16), it’s time to actually send that value. The value that makes it measure acceleration in the +/- 2g range is 0b01100101.
shift_out(7, 8, MSBFIRST, 8, 0b01100101); // Value for MCTL register
The datasheet explains each binary value in the 8-bit MCTL register, and the effect those values have on the chip’s operation. Here is what the summary table for that register looks like in the datasheet:
The top row is the binary digit number or bit number, from 7 on the left to 0 on the right. The second row is the name given to each the bit. It uses these names to discuss each binary digit and what setting it to 1 or 0 does to the chip’s configuration. Our 0b01100101 command sets the unnamed D7 bit to 0; it is another “don’t-care” bit. It also sets DPRD to 1, SPI3W to 1, STON to 0, GLVL[1] and GLVL[0] to 01, and mode[0] and mode[1] to 01. The datasheet goes on to explain what setting each bit to 1 or 0 does. We will have to leave you to research those, since it is part of getting an SPI device to work.
The high(6) call ends the SPI exchange by setting the P6 I/O pin that controls the chip’s /CS input high.
high(6); // CS -> high stop SPI
Now that the chip is configured, the program enters a loop that repeatedly gets z-axis measurements from the chip and displays them.
while(1) // Main loop {
Getting the z-axis value requires another command, again using a set of 7-bit, 1-bit, and 8-bit values. The datasheet says the chip’s ZOUT8 register stores the signed 8-bit version of the z-axis measurement, so we want to read the values in that register.
As before, the first step is to bring the chip’s /CS pin low, with low(6). The ZOUT8 register’s address in the chip is 0x08 (hexadecimal 8), which expressed as a 6-bit binary number is 001000. Remember that the chip’s datasheet said for a read operation, we want to set bit 6 (the 7th binary digit from the right) to 0. That, with the prefix, makes our 7-bit command 0b0001000 for the value parameter in the call shift_out(7, 8, MSBFIRST, 7, 0b0001000). This command must also be followed by a “don’t care” bit, done with shift_out(7, 8, MSBFIRST, 1, 0b0).
Now that the sensor chip received the read ZOUT8 register command, it has to reply with the value stored in its ZOUT8 register. To receive this reply, the Propeller chip still has to apply pulses to the CLK line. But now it has to listen for values the accelerometer chip sends on the DATA line. The counterpart of shift_out is shift_in. This function still outputs pulses to the CLK line, but it sets its I/O pin on the DATA line to input and monitors for reply values from the chip. So, z = shift_in(7, 8, MSBPRE, 8), reads data on P7, sends clock pulses on P8, in MSBPRE mode, and it reads 8 bits.
MSBPRE is one of four options the shift_in function’s mode parameter can use. The MSB part is similar to MSBFIRST — it takes the first binary digit, and loads it into the function result value’s leftmost binary digit, and works its way to the right. Change it to LSB, and it would load the first binary digit into the result variable’s rightmost binary digit and work its way left. The PRE part tells shift_in to check for a value on the DATA line before each clock pulse. If that were changed to POST, it would check after each pulse. The result in the figure is 01000000, which converted from binary is 0x128 + 1x64 + 0x32 + 0x16 + 0x8 + 0x4 + 0x2 + 0x1 = 64 (same as the SimpleIDE display). The high(6) call sets /CS high again, disabling the chip and ending the SPI transaction.
Before repeating the while(1) loop, the program displays the value returned by the shift_in function call, which was stored in the z variable.
print("%c z = %d%c", HOME, z, CLREOL); // Display measurement pause(500); // Wait 0.5 s before repeat } }
Congrats for making it this far! The next page shows you some techniques to make the coding easier.
This third page in the SPI Example tutorial starts with some backround on bitwise operators, and then uses them with coding techniques to improve the original example program.
An OR operation applied to two binary values works like this:
0 OR 0 = 0
0 OR 1 = 1
1 OR 0 = 1
1 OR 1 = 1
Notice that only 0 OR 0 gives a result of 0.
Bitwise OR applies the OR operation to every pair of bits corresponding by position in two different values. The two Bit 0 values are OR’ed together, the two Bit 1 values get OR’d, and so on, to form a result value. Because of the way OR and bitwise OR work, you can use it create a bit mask to make sure particular bits in the result value are set to 1. Any bit in the mask that is 0 will allow the binary digit in the other value to be unchanged in the result. But, any bit in the mask that is a 1 will cause that bit in the result to store a 1.
Imagine your code has values named MCTL, writeMask, and cmd. If writeMask has just its 6th bit set to 1, the illustration below shows the effect that MCTL OR writeMask will have on the final result stored in cmd. Notice that cmd bits 5…0 are all the same as MCTL, but bit 6 changed from 0 to 1 because writeMask’s bit 6 is 1. You can see how the 0’s in the mask let the original bits fall through to the result, but 1’s in the mask block them, and always put a 1 in the result.
Bit# 6543210 ||||||| MCTL = 0b0010110 writeMask = 0b1000000 (OR) -------------------------- cmd = 0b1010110
Also note that if MCTL bit 6 were already a 1, the OR operation would still have put a 1 in cmd bit 6, because 1 OR 1 is 1. So, this operation would be useful for setting a particular bit in the result to 1 even if we do not know whether the bit is a 0 or 1 in the original value.
A C language shortcut for writing 0b1000000 is 1 << 6. You can use it to initialize a bit mask value like this:
writeMask = 1 << 6;
This statement takes a 1 (0b0000001), and shifts it left by 6 digit positions (0b1000000).
A C language operator for bitwise OR is the pipe symbol: |
cmd = MCTL | writeMask
An AND operation applied to two binary values works like this:
0 AND 0 = 0
0 AND 1 = 0
1 AND 0 = 0
1 AND 1 = 1
Notice that only 1 AND 1 gives a result of 1.
Similar to Bitwise OR, Bitwise AND can be used to perform an AND operation between corresponding pairs of bits in two values, to form a result value. In a bit mask, Bitwise AND can be used to make sure particular bits in the result value are set to 0. The trick is to put a 1 in the mask for any bit you do not want changed in the result, and a 0 in the mask for any bit that you want to make sure is a 0 in the result.
In the illustration below, we have a ZOUT8 address with bit 6 set to 1, but we need to set it to 0. By using the mask 0b0111111 (named readMask this time) ith bitwise AND, the cmd result looks just like ZOUT8 with only bit 6 changed to 0.
Bit# 6543210 ||||||| ZOUT8 = 0b1001000 readMask = 0b0111111 (AND) --------------------------- cmd = 0b0001000
Again, note that if ZOUT8 bit 6 had been 0 in the first place and did not need to be changed, the Bitwise AND operation would still have put a 0 in cmd bit 6, since 0 AND 0 is 0. So, this technique can set a particular bit in the result to 0 even if we do not know whether it’s a 0 or 1 in the original value.
A C language shortcut for creating a mask with all 1s and a zero in bit 6 would be:
readMask = ~(1 << 6);
The value 0b1000000 gets created in the parentheses. Then, the bitwise NOT operator ~ is applied, making the result 0b0111111.
NOT 0 = 1
NOT 1 = 0
The bitwise AND is the ampersand symbol: &, and a C language example for applying this mask is:
cmd = ZOUT8 & readMask
The MMA7455 datasheet supplies register addresses as hexadecimal values. Let’s assign those values hex values to constants. Then, we can use the print formatter %07b to view them as binary numbers. We can also use the bit mask techniques from the Did You Know section above to set bit 6 in these values to a 1 or 0, as they would need to be for read or write operations to communicate with the accelerometer.
#include "simpletools.h" // Include simpletools header const int MCTL = 0x16; // = 0b0100101 // Control register address const int ZOUT8 = 0x08; // = 0b0001000 // 8-bit z register address const int writeMask = 1 << 6; // Write mask for setting bit 6 const int readMask = ~(1 << 6); // Read mask for clearing bit 6 signed char z; // Z-axis int cmd; // Variable for storing command int main() // Main function { print("MCTL = %07b \n", MCTL); // Display binary MCTL address print("writeMask = %07b (OR)\n", writeMask); // Display binary writeMask cmd = MCTL | writeMask; // MCTL OR writeMask to cmd print("-------------------------\n"); // Draw line print("cmd = %07b \n\n", cmd); // Display cmd result print("ZOUT8 = %07b \n", ZOUT8); // Display binary ZOUT address print("readMask = %07b (AND)\n", // Display binary readMask readMask & 0b1111111); // Bits 31...7 -> 0 for display cmd = ZOUT8 & readMask; // ZOUT8 AND readMask to cmd print("-------------------------\n"); // Draw line print("cmd = %07b \n", cmd); // Display cmd result }
Why is & 0b1111111 added to the statement that prints readMask? The %07 flag only prints 7 digits if all the digits to the left are zero. Without the mask, it would have printed 11111111111111111111111110111111. Performing an AND operation with 0b00000000000000000000000001111111 changes all those leading ones to zeroes so that only 7 digits in readMask get printed.
Let’s use what we’ve learned to update our SPI code example. This version declares a constant for the address of each MMA7455 register used so far. It also declares constants writeMask and readMask for setting or clearing bit 6 of the register values. Then there will be no need to manually convert from hexadecimal to binary for the shift_out and shift_in value parameters (hooray!). The modified example also uses names in place of numbers for the I/O pins. This makes a driver much easier to re-use in a different project.
Why are we encouraging you to do all this? Because these are techniques you are likely to see (or use yourself) in other SPI drivers.
Links
[1] https://learn.parallax.com/propeller-c-simple-devices/mma7455-three-axis-accelerometer
[2] http://www.parallax.com/product/28526
[3] https://learn.parallax.com/propeller-c-set-simpleide
[4] https://learn.parallax.com/propeller-c-start-simple
[5] http://www.parallax.com