This is a C-language tutorial for the 8-core Propeller microcontroller. It features the Propeller Activity Board but other Propeller development boards will work.
Many projects utilize devices that communicate with microcontrollers using a protocol. Some examples of common communication protocols are: half- and full-duplex serial, inter-integrated circuit (I2C), and serial peripheral interface (SPI). In many cases, you’ll use a library to talk with a device, and that library in turn includes a protocol library to perform the actual communication. An example of this is the HMC5883L compass. The library uses the simpletools i2c_in and i2c_out functions to communicate with the device, and the functions you call at the application level simply supply the measured number of degrees from magnetic north.
Instead of using pre-written libraries for devices, this tutorial series’ activities demonstrate how to communicate with devices, starting with the protocol information that comes in the product documents and/or datasheets. These activities can be good practice for when you find a device you want to use that doesn’t already have a pre-written library, and also for wrapping the communication layer into a Simple Library.
If you have no experience with C programming or have not completed the Simple Devices tutorial series, we advise completing all of the following steps before beginning Simple Protocols:
Half-duplex serial communication is what the the Propeller microcontroller sends to the Activity Board’s (original or WX version) serial-to-USB converter chip every time you use a print statement to send a message to the SimpleIDE Terminal. The term “half-duplex” means that the system is either sending or receiving, not both. In contrast, “full-duplex” means that the system can send and receive at the same time, which is covered in a different activity.
Half-duplex serial communication is also useful for communicating with a variety of devices. The Parallax Serial LCD and RFID Reader are two examples. This tutorial will show you how to use serial communication to send messages to the Parallax Serial LCD.
Baud rate indicates the number of 1/0 values per second a serial signal transmits and/or receives. The code example that comes with SimpleIDE sends values commands to the Parallax Serial LCD at a baud rate of 9600 bits per second (bps). The Parallax serial LCD has 3 baud rate settings, 2400, 9600, and 19200 with a chart underneath that indicates the switch settings for each.
The code example that comes with SimpleIDE happens to use P12 for communication. Keep in mind that the Propeller chip can use any I/O pin for either half or full-duplex serial communication.
If you haven’t already installed the latest USB driver, SimpleIDE, or Learn folder, go to Propeller C – Set up SimpleIDE and Propeller C – Start Simple.
The test code turns the display on, clears it, and then displays the message “Hello LCD!!!”
The simpletools library has a #include in it for simpletext, which has built-in half-duplex serial functions. To use these functions, your application has to declare serial *deviceNickname for every different serial device the application needs to communicate with. In this example, there’s just one device that’s declared with serial *lcd. In the main function, lcd = serial_open(12, 12, 0, 9600) sets the Propeller chip’s receive pin (12), its transmit pin (also 12), the communication mode (0), and the baud rate (9600 bits per second). The serial_open function also allocates memory for a serial data structure that stores all these settings as well as the actual serial data. Last but not least, serial_open returns the starting address of this memory, which gets stored in lcd. From this point forward, code can use lcd with functions like dprint and writeChar to specify which serial connection to send/receive data. For example, writeChar(lcd, ON) sends the value 22 to the lcd. Likewise, print(lcd, “Hello LCD!!!”) sends the “Hello LCD!!!” string to the lcd.
/* Hello Serial LCD.c */ #include "simpletools.h" serial *lcd; const int ON = 22; const int CLR = 12; int main() { lcd = serial_open(12, 12, 0, 9600); writeChar(lcd, ON); writeChar(lcd, CLR); pause(5); dprint(lcd, "Hello LCD!!!"); }
If this code looks different from what came with your SimpleIDE, just copy from here and paste over the code provided in your SimpleIDE folder.
Make it Global — The serial *lcd declaration is global since it is made before any functions. This is useful because it allows any function in the application to pass the lcd device nickname in calls to simpletext functions like dprint and putChar.
More simpletext functions — Any simpletext function with a text_t *device parameter can be used with half-duplex serial peripherals like the Parallax Serial LCD. For a complete list of simpletext functions, see …DocumentsSimpleIDELearnSimple LibrariesText DeviceslibsimpletextDocumentation simpletext Library.html.
More serial functions — The serial_open function is part of the serial library, which is included by the simpletext library, which is in turn included by the simpletools library. For a full list of (half-duplex) serial functions, see … DocumentsSimpleIDELearnSimple LibrariesText DeviceslibsimpletextDocumentation serial Library.html.
LCD commandsproduct page. This document has a list of values that can be sent to the LCD to make it perform a variety of tasks. In addition to turning the display on (22) and clearing it (12), there are commands for cursor positioning, custom character definition and display, and even playing musical notes on the LCD’s built-in piezospeaker.
— The two constant declarations are values from the Parallax Serial LCD’s product documentation, which is available through the Downloads section on itsHalf-duplex input instead — If your application is set up so the Propeller chip only receives and does not send serial data, you can put a -1 in the serial_open function’s transmit pin parameter, like this: serial someInputDevice = serial_open(7, -1, 0, 9600).
Sending a value to a serial device can be almost as easy as it is with the SimpleIDE terminal. Here is an example that places the cursor on the fourth character over in the second line, with writeChar(lcd, LINE2 + 4); and then prints n = 123.
Here is an excerpt from a program that sends similar messages to the serial LCD and terminal using the device functions. The simpleterm_close function shuts down all the services that are provided by default, so print, putChar and other functions stop working after this call. Next, this code manually creates a second half-duplex serial connection to the terminal. The example here uses serial *term and term = serial_open(31, 30, 115200). So now there are two device nicknames, lcd and term, and they can be used to tell functions like dprint, writeChar, writeStr and writeDec to communicate with either the terminal or the LCD. For example, if you want to send a serial message to the LCD, you can use dprint(lcd, …. If you instead want to send a message to the terminal, use dprint(term, ….
Full-duplex serial communication allows both outgoing and incoming messages to be sent and received at the same time. Examples of devices where full-duplex serial communication can be useful include XBee radios, other microcontrollers, and even the SimpleIDE Terminal. Although the Propeller uses half-duplex serial communication with the SimpleIDE Terminal by default, that can be disabled and replaced with full-duplex serial communication. This example demonstrates how to use full-duplex serial communication with XBee radios, and also with the SimpleIDE Terminal.
Before you start, let’s make sure that the Propeller Chip is not sending an output signal to the XBee module’s DO output pin. If one is trying to send a high signal and the other is trying to send a low signal at opposite ends of a wire, it could damage either the Propeller I/O pin or the XBee.
int main() { input(9); }
Now, we can safely build the circuit, without having to worry about some old program in the Propeller chip’s EEPROM sending signals to the XBee module’s DO pin.
This activity uses two XBee radios and an XBee to USB adapter.
If you haven’t already installed the latest USB driver, SimpleIDE, or Learn folder, go to Propeller C – Set up SimpleIDE and Propeller C – Start Simple.
The test code is a loopback connection, meaning it sends back what it receives. After setting up the SimpleIDE terminal to talk with the XBee adapter, you’ll be able to type characters in to send them to the XBee you connected to the adapter. That XBee will broadcast those characters on its radio frequency. The XBee on the Propeller will receive those characters from the airwaves, and send them to the Propeller as serial messages. The Propeller program then sends “You typed: + the character” back to the Activity Board’s XBee, which broadcasts that message to the XBee connected to the USB adapter. The Xbee on the adapter will receive that broadcast and send it to the PC, which then displays it on the SimpleIDE terminal.
If you want to modify and re-run your program, follow these steps:
The library for full-duplex serial communication is called fdserial, and #include “fdserial.h” makes its functions available to the application. Then, fdserial *xbee sets up a full-duplex serial device identifier. In the main function, xbee = fdserial_open(9, 8, 0, 9600) configures the serial connection to receive messages on the P9 I/O pin, send with the P8 pin, set the mode to 0, and the baud rate to 9600 bits per second. The fdserial_open function also returns a memory address to the xbee pointer variable. Now that xbee has that pointer, it can be used as a device identifier with any fdserial function with fdserial *term parameters as well as any simpletext function that has a text_t *device parameters. Keep in mind that there could be more than one full-duplex serial device connected to the Propeller, so we use the device identifier to tell functions like dprint and fdserial_rxChar which serial device they are communicating with. That’s why we see xbee in function calls like dprint(xbee, “Click this terminal, \n”), and c = fdserial_rxChar(xbee).
/* XBee UART Loopback.c */ #include "simpletools.h" #include "fdserial.h" fdserial *xbee; int main() { xbee = fdserial_open(9, 8, 0, 9600); writeChar(xbee, CLS); dprint(xbee, "Click this terminal, \n"); dprint(xbee, "and type on keyboard...\n\n"); char c; while(1) { c = fdserial_rxChar(xbee); if(c != -1) { dprint(xbee, "You typed: %c\n", c); } } }
As with half-duplex serial, full-duplex serial identifiers can be used with dprint, which gives you formatting options that are much like print with the SimpleIDE Terminal. For example, dprint(xbee, “Click this terminal, \n”) sends the message followed by the \n newline character. You can also use %d for sending the text representation of integer values, and even %f for floating point representations.
The fdserial library runs code in another cog that accumulates bytes it receives into a buffer array. If there’s nothing in it, the fdserial_rxReady function returns -1. If there are one or more characters in the buffer, fdserial_rxReady returns the oldest character. After repeated calls to fetch all the characters, the function returns -1 again to indicate that there’s nothing in the buffer.
Inside the while(1) loop, c = fdserial_rxChar(xbee) either gets -1, or the oldest character in the buffer. When it gets -1, the code inside the if(c != -1) block gets skipped because c is equal to -1. When c contains a character, it’s not equal to -1, and in that case, the dprint(xbee, “You typed: %c\n”, c) code does get executed.
The XBee manual is available from digi.com.
The serial *xbee declaration is global since it is made before any functions. This is useful because it allows any function in the application to pass Xbee device nicknames in calls to simpletext functions like dprint and putChar.
Any simpletext function with a text_t *device parameter can be used with full-duplex serial peripherals like the XBee. For a complete list of simpletext functions, see …Documents\SimpleIDE\Learn\Simple Libraries\Text Devices\libsimpletext\Documentation simpletext Library.html.
The fdserial_open function is part of the fdserial library. For a full list of (half-duplex) serial functions, see Documents\SimpleIDE\Learn\Simple Libraries\Text Devices\libfdserial\Documentation fdserial Library.html.
The fdserial library’s receive buffer is considered FIFO. That’s an abbreviation of first-in-first-out, meaning the oldest byte that was received is the first one to be returned by fdserial_rxChar(xbee).
The fdserial_open function’s mode parameters expects a value with binary 1s and 0s. The mode 0 in xbee = fdserial_open(9, 8, 0, 9600) is the most common one, but other variations include:
You can even set the multiple mode bits. For example, you could pass a mode of 0b0011 to invert both the transmit and receive signals.
You might expect to see a function like xbcmd in the code below in an XBee library. This function simplifies putting the XBee into command mode for configuration. In its default mode, the XBee radio broadcasts the serial data it receives for other xbees. When it is switched to command mode, it treats the serial data it receives as configuration data. You can use this mode to set its network address, baud rate, and many other features.
In the XBee Command Mode example (below), the main function sends +++ to the XBee module to put it into command mode. So long as the XBee doesn’t receive anything else from the Propeller for a couple seconds, it switches to command mode and replies with OK. If the Propeller gets the OK message, it then sends ATBD to find out what baud rate the XBee is set to. According to the XBee manual from Digi, since it’s at 9600, the XBee will reply with 3. Last, but not least, it sends XBCN to exit command mode, and the XBee replies with OK. After exiting command mode, the XBee returns to its normal mode of radio broadcasting the serial messages it receives.
Does the reply make sense to you? Try adding this code right below the xbcmd you just modified: if(!strcmp(“ERROR”, response)) print(“The XBee didn’t understand!”);
To drive home the point that you can have multiple full-duplex serial connections, let’s update the Try This example so that it uses fdserial to communicate with SimpleIDE terminal, and another instance of fdserial running in another cog to communicate with the XBee. Here is a start on modifications to the code from Try This:
We created another fdserial identifier named term, which is short for terminal. The simpleterm_close function call shuts down the default half-duplex communication, which will cause print calls to stop working. But that’s fine because we can use full-duplex serial.
Note the last command in this example was modified from print(“cmd = +++”) to dprint(term, “cmd = +++”). If you do that with all the print statements, it’ll work the same as before, but will be running a full-duplex serial connection (instead of half-duplex serial) under the hood.
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. 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 and Propeller C – Start Simple.
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 1×16 + 6×1 = 22. In 6-bit binary, that’s 010110 = 0x32 + 1×16 + 0x8 + 1×4 + 1×2 + 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 + 1×64 + 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.
I2C (also: I2C) is a circuit arrangement and communication protocol. It allows one or more processors to communicate with one or more special-purpose ICs using a single pair of wires. A group of wires for communication is called a bus, and so this pair is called an I2C bus.
The simpletools library has functions for common I2C tasks. The Propeller microcontroller “master” orchestrates communication with I2C device “subordinates.” Here, we’ll prototype some code to talk with the 64 KB I2C EEPROM built into the Propeller Activity Board (original or WX version). The simpletools library already has functions to do just that, but this activity will give you a starting point for prototyping with other I2C devices that do not already have library support.
Need a quick way to save data to your Activity Board’s EEPROM (original or WX version)? Try these simpletools functions: ee_putByte, ee_getByte, ee_putInt, ee_getInt, ee_putFloat32, ee_getFloat32, ee_putStr, and ee_getStr.
Need more I2C options? The simplei2c library provides access to lower-level communication details with more flexibility. In SimpleIDE, click Help and select Simple Library Reference. Propeller GCC has even more I2C functions with advanced features and faster communication rates.
Many Propeller development boards already have a 64 KB I2C EEPROM connected to P28 and P29:
If you are using a different Propeller board, you will need:
If the EEPROM circuit is already built into your Propeller development board, you are good to go. Just use the code examples as-is with SCL = P28 and SDA = P29.
If you are using a different Propeller set-up, connect the 24FC512 EEPROM using the schematic shown below. You can use a different pair of I/O pins (other than P28 and P29). Just make sure to update the code examples to reflect the different pair of pins.
Let’s look at the connections that the EEPROM on the Propeller Activity Board (original or WX version) needs, in addition to Vcc (3.3 V) and Vss (0 V).
Address pins A2…A0
Each subordinate device on an I2C bus needs its own address. The lowest three digits in EEPROM’s address are determined by applying voltage to the address pins: 3.3 V = 1, and 0 V = 0. In our example, A2…A0 are connected to ground, so those digits will be 000. Up to eight similar EEPROMs could be placed on the same I2C bus, each with a different pattern of 3.3 V and 0 V applied to A2…A0.
SCL
The I2C bus’ SCL (serial clock) line is used to coordinate communication between devices. The EEPROM’s SCL pin connects Propeller I/O pin P28. A pull-up resistor holds the voltage on the SCL line at Vdd (3.3 V in our example below) when no devices are talking.
SDA
The SDA (serial data) line is used to transmit data from one device to another. The EEPROM’s SDA pin connects Propeller I/O pin P29. The SDA line also has a pull-up resistor.
WP
WP stands for write protect, and when it is enabled with a high signal, the data stored inside the EEPROM cannot be changed. Since it is receiving a GND (0 V) low signal here, write protect is disabled. To enable write protect, you could change that connection to 3.3 V.
This test program stores a string of seven character values “abcdefg” to the EEPROM. Then, it retrieves them back and prints them in the SimpleIDE Terminal. To do so, the code performs these steps:
Library Alert! This example relies on Simple Libraries in the (5/14/2014) Learn folder or later. Update your Learn Folder.
The i2c_in function will still be able to fetch and display the string, since the EEPROM is designed to hold data without power.
/* Test 24LC512 with I2C.c Test writes data to I2C EEPROM, then reads it back and displays it. */ #include "simpletools.h" // Include simpletools header i2c *eeBus; // I2C bus ID int main() // Main function { eeBus = i2c_newbus(28, 29, 0); // Set up I2C bus, get bus ID // Use eeBus to write to device i2c_out(eeBus, 0b1010000, // with I2C address 0b1010000, 32768, 2, "abcdefg", 8); // send 2 byte address of 32768 // and 8 byte data string. while(i2c_busy(eeBus, 0b1010000)); // Wait for EEPROM to finish char testStr[] = {0, 0, 0, 0, 0, 0, 0, 0}; // Set up test string // Use eeBus to read from device i2c_in(eeBus, 0b1010000, // with I2C address 0b1010000, 32768, 2, testStr, 8); // send 2 byte address of 32768 // & store data in 8 byte array. print("testStr = %s \n", testStr); // Display result }
On this page, we’ll go through the Test 24LC512 with I2C.c code a few lines at a time, to see how the i2c_out and i2c_in functions communicate with the I2C device.
First, let’s get the EEPROM datasheet. You would need the datasheet to prototype code for any I2C device that doesn’t already have library support.
The i2c_out and i2c_in functions we’ll be using have six parameters:
First, the simpletools library is included, to let us use its i2c_in and i2c_out functions. It also includes other libraries, like simpletext, which has the print function.
#include "simpletools.h"
Each I2C bus you declare needs a bus identifier. This identifier keeps the address in Propeller memory where information about the bus is stored. It is declared globally — above main and not inside a function — so it can be used by any function in the program.
i2c *eeBus;
Next, inside main, use the identifier to set up the I2C bus. The i2c_newbus function has three parameters: sclPin, sdaPin, mode.
int main() { eeBus = i2c_newbus(28, 29, 0);
Setting sclPin to 28 and sdaPin to 29 corresponds to the circuit we are using. Mode = 0 is for our circuit’s normal I2C configuration, with a pull-up resistor on the SCL line. Although less common, some boards do not have a pull-up resistor on the SCL line and let the microcontroller drive it, which is when mode = 1 would be used. The Propeller Demo Board and PE Kit platform are examples.
The i2c_newbus function call returns information for the I2C bus ID, which gets stored in eeBus. From this point forward, your code can pass eeBus to the i2c_in and i2c_out function’s busID parameter to select this bus (and not some other I2C bus a project might have) for communication.
Sending data to an I2C device with i2c_out involves telling it to find some memory address and store a value there. Or, in the case of our EEPROM, find a memory address, and store eight values starting from that memory address. Let’s look at how the i2c_out function parameters are used here:
Why 8 bytes for 7 characters? The string are zero terminated, so there’s a 0 that follows those characters that doesn’t show. The print function needs that zero to properly display strings. So, make sure to add 1 to the size of your character strings contained in quotes.
i2c_out(eeBus, 0b1010000, 32768, 2, "abcdefg", 8);
So, how would you go about finding out details like the EEPROM’s I2C address? Or the number of bytes that you have to send it to pick one of its internal memory addresses? Read the device’s datasheet. They are usually written for industry professionals, so you might need to read and re-read certain sections and write pieces of test code to check how the device replies. But with time and persistence, you will succeed, and it gets easier with practice.
Read some selections from the I2C datasheet to understand the I2C address above. As you read, the main tasks are to focus on the I2C address, how to select one of the EEPROM’s memory addresses, and how to send it data bytes. The i2c_out and i2c_in functions take care of details like start conditions, read/write bits, and ACK/NACK signals, so don’t worry about those portions.
Some I2C devices don’t also need a memory address, just data. If that’s the case for your device, use NULL in place of the memory address, and 0 for the number of bytes in the memory address. Also use NULL and 0 in transactions that involve just issuing a memory address without data, in which case, NULL and 0 would be in place of *data and dataCount.
Some I2C devices stop responding while they are processing, such as our EEPROM while it is storing data it just received. Devices like this can be polled for availability with the i2c_busy function, which returns 1 if the device is not responding (because it’s busy), or 0 if it responds (which means it’s ready for more). Putting the i2c_busy function call in a while loop lets the program execution pause until the I2C device is available again.
while(i2c_busy(eeBus, 0b1010000));
To verify that the i2c_out function actually stored the data in the EEPROM, we’ll use i2c_in to retrieve it. But first, we need to make a character array full of zeroes as a place to store the data that’s about to be retrieved from the EEPROM.
char testStr[] = {0, 0, 0, 0, 0, 0, 0, 0};
(An alternate approach to this string of zeroes would be char testStr[8]; followed by memset(testStr, 0, 8);)
Now the program is ready for the read operation. This i2c_in call selects the same I 2 C bus, I2 C chip address, and points to the EEPROM’s 32768 memory address. The difference is the *data parameter. Instead of sending a pointer to “abcdefg” string, this call passes a pointer to the testStr array that was just created.
i2c_in(eeBus, 0b1010000, 32768, 2, testStr, 8);
Last, this print statement displays the data in the testStr array with the SimpleIDE Terminal. If testStr contains “abcdefg” then mission accomplished!
print("testStr = %s \n", testStr); // Display result
The term I2C comes from the name Inter-Integrated Circuit, which would be abbreviated IIC. If those three letters were variables in an equation multiplied together, the two I terms would be I x I or I2. So, that’s where I2C comes from. It’s often pronounced I-squared-C, though I-two-C is also understood.
The Load EEPROM button copies the example Propeller program to your board’s EEPROM, filling addresses 0 to 32767. So, 32768 is the first available memory address for data storage. Some Simple Libraries keep calibration data in the EEPROM. By convention, these libraries use the highest available EEPROM addresses, so if your application is logging data, it’s best to start from 32768 and allow data to accumulate upward to higher addresses.
Libraries that use EEPROM this way list the memory locations they use in their documentation. For an example, open …DocumentsSimpleIDELearnSimple LibrariesConvertlibabvoltsDocumentation abvolts Library.html, scroll to the Detailed Description heading, and check the EEPROM Usage info. You might also notice these addresses in the Macros section.
A typical I2C packet has a first byte that begins with the microcontroller master (the Propeller in this case) sending a start condition signal that takes the SDA line low, while the SCL line is high. After that, the Propeller repeatedly pulls the SCL line low and then releases it, allowing the resistor to pull it up to 3.3 V; this sends a low…high…low…high… clock signal. Each time SCL is high, the Propeller transmits the next binary data value by either pulling the SDA line low to send a binary 0, or leaving it pulled high by the resistor to send a binary 1.
The first seven bits sent are the target chip’s I2C address, transmitted most significant bit first. For example, if the address is 0b1010000, it will send the 1 first, then the 0, another 1, and four zeroes. The last data bit in the first byte is called the read/write bit. If this bit is 1 (read), it tells the I2C chip to respond with data. If it’s 0, it means the I2C chip is about to receive data. In this case, the read/write bit is set to 0, so the Propeller is going to send more information to the EEPROM. When the Propeller sends the 9th CLK pulse, it releases control of the SDA line so that the I2C chip can either respond with a low value to acknowledge (ACK) that it is ready to continue, or a high value to not acknowledge (NACK).
Note: These timing diagrams assume that the program used i2c_out(eeBus, 0b1010000, 32771, 2, “abcdefg”, 8); and are showing the timing for i2c_in(eeBus, 0b1010000, 32771, 2, testStr, 8); The memAddr parameter has been changed from 32768 (0b10000000000000000) to 32771 (0b10000000000000011) to help make the start and end of the values more apparent in the diagrams.
Next, the Propeller sends two more bytes to select the memory address in the chip for reading data from or writing data to. Many I2C devices only need one memory address byte. The 24LC512 has 65536 memory addresses, so it takes two bytes to describe all the values from 0 to 65535. Although the Propeller chip controls the both the SCL and SDA lines during the first 8 pulses, it releases the SDA line on each 9th pulse to allow the I2C device to send either an ACK or NACK reply.
In the case of our EEPROM, that command byte tells the chip what memory address to set its pointer to. An I2C sensor might be pointed to a memory address to receive configuration values, or one that stores its sensor measurements for reading. Other devices like I/O expanders might be pointing to a memory address that contains the input or output pin states. After the command, one or more data bytes are sent to (write) or received from (read) the chip. Whether the data is one byte or many bytes depends on how the chip is designed, and it’s one of many details that get explained the device’s datasheet.
At this point, the i2c_in function has set the EEPROM’s memory address pointer. Now, it’s time to read the data bytes from the EEPROM. This is initiated by another byte with the start condition, I2C address, and read/write bit and ACK from the I2C device. This time, the read/write bit is set to 1 for a read operation, meaning the I2C device will have to start replying with data.
For a read operation, the Propeller continues supplying SCL pulses, but now the I2C chip controls the SDA line for 8 pulses as it replies with data, and the Propeller has to reply with an ACK or NACK on SDA during every 9th SCL pulse.
Now that we have done some basic communication with simple byte data and one I2C device, let’s expand on that. First, let’s modify the original code to work with other data types besides bytes. Then, if you are interested in going a bit further, the Advanced Topic section will show you a method you could use to put two additional EEPROM DIP chips (plugged into your breadboard) on a second I2C bus.
Data is written to and read from I2C devices in the form of one or more eight-bit bytes. In many cases the data written to an I2C chip originated as a different data type, like maybe a 32-bit int or even a float.
Likewise, data bytes read from I2C chips may need to be reassembled back into the data those bytes actually represent. For example, a sensor might be returning a signed 16-bit short value with two bytes, or an EEPROM or other memory device might be storing and returning ints, floats, or some other type.
So, here is an example that shows how an int variable can be converted to bytes, stored in EEPROM, retrieved from EEPROM, and reassembled back into an int value. It does this process with the number 5280, stored in an int variable.
Along with storing and retrieving an int value, this program gives names to some of the constant values from the previous example. The main trick for the variable-to-bytes conversion is to pass the address of the variable in question, cast as a char pointer, and make sure to specify the correct number of bytes for the data type.
So, this program starts with int val = 5280. Notice how i2c_out(eeBus, eeAddr, memAddr, 2, (char*) &val, 4) uses (char*) &val to cast the address of the val int variable as a pointer to a character variable. The dataCount parameter following it is also set to 4, since an int variable has four bytes. The reverse of this process is accomplished with i2c_in(eeBus, eeAddr, memAddr, 2, (char*) &val, 4).
This is meant to be a theoretical exercise, not a hands-on activity. If you have the two additional EEPROMs required for this example and wish to try adding a second bus to your board, then the information provided here can be used as a guide.
Below is an example of an I2C bus you could add to your Activity Board’s breadboard (original or WX version). It has two more I2C EEPROMs just like the one on the Activity Board’s P28/P29 bus.
Please Note: The top 24LC512 shown in the schematic has its A0, A1 and A2 pins set the same as the one on P28 and P29. But keep in mind, it’s on a completely different bus, so it’s okay for it to have the same address. Since the bottom 24LC512 is on the same bus, it has to have a different address.
(2) 24LC512 EEPROM
It’s easy to expand your program in order to communicate with all three EEPROMs; these two and the one built into your Propeller board. The steps are pretty much the same:
i2c *eeBus2;
eeBus2 = i2c_newbus(7, 6, 0);
// Write to upper EEPROM on second bus i2c_out(eeBus2, 0b1010000, cmd, 2, "mnop", 5); // Write to lower EEPROM on second bus i2c_out(eeBus2, 0b1010001, cmd, 2, "qrstuvw", 8); ...