How to Access Chips Over the SPI on BeagleBone Black

15434

The BeagleBone Black is a Single Board Computer for $45 which runs an ARM CPU, 2Gb of flash, 512Mb of RAM and comes with many connectors allowing you to interface with electronics. Unlike the Arduino, the BeagleBone Black runs a full Linux kernel, allowing you to talk directly to your electronics from the language of your choice and with the comfort of all that RAM. In my previous article I looked at how to talk to the GPIO pins on the BeagleBone Black. This time around I’m stepping it up to talk to persistent storage in the form of an EEPROM over the Serial Peripheral Interface Bus (SPI) on the BeagleBone Black.

The SPI allows data to move in both directions from a bus master (controller) to various chips which are attached to the bus. Because there are many chips on the bus, you need to have some way to stop chips talking at the same time or reacting to commands you have sent for another chip on the bus. So each chip gets its own Chip Select line which tells it that it is the active chip and you want to talk to it (and maybe hear from it).

The normal interface might have a wire to send data from the master to the bus, a wire to get data back from the bus, a clock wire to control when a new bit is sent and received, and a chip select wire (for each chip on the bus). The wire to send from the master to the chip (slave) is called the MOSI (Master Out, Slave In) and the converse for communication from the chip (Slave) is the MISO (Master In). You send every byte a single bit at a time. To send a bit you set the MOSI line to the current bit you want to send and use the clock line to tell the chip to grab the current bit. Then send the next bit the same way and so on. Each time you send a bit, you run one clock and the chip might also send you one bit back on the MISO line.

Testing the EEPROM with Arduino

A cheap EEPROM that has an SPI interface allows for testing SPI access without the risk of damaging more expensive hardware. I choose a 512KBit EEPROM which I got for a few dollars as the first SPI device. If you are worried about the speed of sending things around a bit at a time, that EEPROM can run at up to 20Mhz clock speed. The speed of sending the data a bit at a time might not be the slowest part of the system, for example EEPROM chips take time to write data to permanent storage.

I decided to access the EEPROM from Arduino first for two reasons: the Arduino board is cheaper than a BeagleBone Black, and I could also test that how I thought the EEPROM worked from reading the datasheet was how it worked when connected and powered on. Knowing that I can read and write to the EEPROM over SPI from an Arduino was useful when I need to debug issues accessing the EEPROM from the BeagleBone Black later.

While there are 8 pins on the EEPROM, half of those are connected to power rails (3 to the 3.3v supply and one to ground). I’ve shown all power wires in red, and the light blue wires are ground wires (below). The clock wire is shown in orange, the chip select in blue, and the MOSI and MISO in black and white respectively.

arduino-eeprom

The Arduino program to read and write to the EEPROM is shown below. As you send data over the wire one bit at a time, you need to know what order the bits are to be sent in: that is, should you send the Most Significant Bit (MSB) first or last? The datasheet for the EEPROM stipulates that it wants the MSB first so the setup() function ensures that is the order. The chip select pin (10) is also set for output so we can talk to the EEPROM.

The main loop() of the program is quite simple, it just reads a byte from address 10, shows what it read to the user, and then writes the current loop iteration count to the byte at address 10. To read a byte from the EEPROM the READ instruction is sent, followed by the 2 byte address which you wish to read from. One might wonder about the bold transfer(0) line that follows the read and address, it seems that we should be reading the byte at that stage, not writing a zero. That transfer(0) call is used to send one byte, which will clock the SPI 8 times and thus have a byte of data for us from the chip. It doesn’t matter what data we sent over the SPI after the read and address have been sent, the main thing is to clock the SPI so that the chip can send back the data we want. The reading will stop once we release the chip select line, and then we have to send another instruction to the EEPROM.

To write to the EEPROM you send the write enable (INSTWREN) instruction, then a WRITE instruction, the address to write the data to, and then the byte to write. After that I write disable the IC again for completeness (INSTWRDI). You have to release the chip select after sending INSTWREN for that instruction to be acted on by the EEPROM. If you left the chip select LOW after writing INSTWREN and moved right on to issue the WRITE command, the EEPROM would likely ignore your write.

#include <SPI.h>
const byte INSTREAD  = B0000011;
const byte INSTWRITE = B0000010;
const byte INSTWREN  = B0000110;
const byte INSTWRDI  = B0000100;
const int chipSelectPin = 10;
byte loopcount = 1;
void setup() 
{
  Serial.begin(9600);
  // start the SPI library:
  SPI.begin();
  SPI.setBitOrder( MSBFIRST );
  pinMode(chipSelectPin, OUTPUT);
}
void loop()
{
  byte data = 0;
  digitalWrite(chipSelectPin, LOW);
  // Read the byte at address 10
  SPI.transfer(INSTREAD); 
  SPI.transfer(0); 
  SPI.transfer(10);
  data = SPI.transfer(0); // <- clock SPI 8 bits
  digitalWrite(chipSelectPin, HIGH);
  // show the user what we have  
  Serial.print("read data:");
  Serial.print( data, BIN );
  Serial.print("n");
  
  // Set write enable, write a byte with the current loop 
  // and disable write again
  digitalWrite(chipSelectPin, LOW);  
  SPI.transfer(INSTWREN);
  digitalWrite(chipSelectPin, HIGH);
  digitalWrite(chipSelectPin, LOW);  
  SPI.transfer(INSTWRITE);
  SPI.transfer(0);
  SPI.transfer(10);
  SPI.transfer(loopcount);
  digitalWrite(chipSelectPin, HIGH);
  digitalWrite(chipSelectPin, LOW);  
  SPI.transfer(INSTWRDI);
  digitalWrite(chipSelectPin, HIGH);
  
  loopcount++;
  delay(5000);
}

I should also mention that the EEPROM can give and get more data for each READ and WRITE instruction you send. For example, I could have clocked the SPI 8 times again to read the byte at address 11 without needing to issue another READ instruction. There are other capabilities to the EEPROM too, but for the purposes of the article the EEPROM is just used to check that one can read and write data over SPI.

On the BeagleBone Black and Linux

There are two SPI on the BeagleBone Black: SPI0 and SPI1. The four pins for SPI0 appear mid way down the P9 header and are connected in the below image. Orange is again the clock and blue for chip select. Notice that there are d0 and d1 instead of MOSI and MISO. That is because you can choose which is input or output from the BeagleBone’s perspective during software setup. See theSystem Reference Manual (SRM) for details on the pinouts of the large P8 and P9 headers on the BeagleBone Black. There are some tables on page 84, though as the document may change, it might be quicker to find by looking for the “Expansion Header P9 Pinout” table in the list of tables of the SRM. The multiplexing for the P9 header can also be seen in this table.

bbb eeprom diagram

Some chips that can be accessed over the SPI on the BeagleBone will have Linux kernel device drivers. For example, a real time clock on the SPI might be used to provide the system with /dev/rtc. You can also directly get at the SPI from your programs by using the Linux kernel spidev device driver in the Linux kernel. That will give you devices like /dev/spidev1.0 which you can use normal file access like open(2), read(2), and write(2) to get and put data on the SPI. The Linux kernel will automatically handle holding the chip select line for you, and will clock your data out at the speed you have set for the spidev.

For the EEPROM there is one slight complication. When you do a write(2) to the file the chip select is set, data is written, and then the chip select is unset. But, recall from the above that to read a byte from the EEPROM you have to hold the chip select, write the READ instruction and address, then while still holding the chip select you read from the SPI to get bytes from the EEPROM. As soon as you drop the chip select line, the EEPROM takes that as the end of your desired read operation and stops doing that. So you need a way to write to the SPI and then read from the SPI while holding the chip select the whole time from the BeagleBone Black.

This leads to the use of the SPI_IOC_MESSAGE interface to ioctl(2). That ioctl allows you to both send and receive at the same time, or perform a sequence of send and receive and nominate SPI timing and chip deselection policy between each operation in sequence of commands. The spidev_fdx.c example uses the ioctl to first send and then receive data over SPI. Though spidev_fdx is a half duplex example it gives insight into how to use this ioctl interface. You can hold the chip select pin between operations using the cs_change member of the spi_ioc_transfer structure.

The BeagleBone Black has two SPI that you can access, each having multiple chip select pins associated with it. The SPI1 shares some pins with the HDMI interface, so you will have to disable HDMI to get at both SPI. Because the pins on the headers can be used for different things, you need to tell the Linux kernel what you want to use those pins for. This is done using the device tree overlay. One advantage of this is that the kernel can manage your pins and stop two things from using the same pins at the same time.

You tell the cape manager to load an overlay by writing the overlay name to the /sys/devices/bone_capemgr.*/slots file. The cape manager looks in /lib/firmware for a matching dtbo file describing what pins the overlay uses and what mode those pins need to be in. You can create dtbo files by compiling dts source files which describe this information in a human readable text format. You can also instruct the cape manager to load some overlays on boot up, so once you have a configuration you are happy with then you don’t have to play with these overlay files anymore.

The elinux site provides files to enable the SPI0 and SPI1 with D1 as output or input for the BeagleBone Black. As outlined on elinux the procedure to enable those overlays is as follows. I also manually changed the owner of the spidev files so that I didn’t have to be root in order to talk the the EEPROM. I found that on Cloud9 GNOME Image 2013.05.27 the automatic cape loading at boot for the BB-SPI0 using the uEnv.txt optargs didn’t work for me. After a software update, automatic loading did work.

root@beaglebone:~# dtc -O dtb -o BB-SPI0-01-00A0.dtbo -b 0 -@ BB-SPI0-01-00A0.dts
root@beaglebone:~# cp BB-SPI0-01-00A0.dtbo /lib/firmware/
root@beaglebone:~# echo BB-SPI0-01 > /sys/devices/bone_capemgr.*/slots
root@beaglebone:~# ls -lh /dev/spi*
crw------- 1 root root 153, 0 Oct 16 04:12 /dev/spidev1.0
root@beaglebone:~# chown ben /dev/spi*

The below commands tell udev to always make spidev files owned by my user instead of root. The udevadm is a very handy command as it will show you not only what things you can match against in your udev rules file to select the device but also can be run again to test that your rules will match and perform the expected task.

# udevadm test /sys/devices/ocp.2/48030000.spi/spi_master/spi1/spi1.0/spidev/spidev1.0
...
SUBSYSTEM=spidev
...
# cat /etc/udev/rules.d/99-spidev.rules
SUBSYSTEM=="spidev", OWNER="ben"

Talking with EEPROM over SPI

So, now that a /dev/spidev1.0 file exists, moving on to talking with the EEPROM over SPI from the BeagleBone Black. The core of a C++ program is shown below which does the same thing that the above Arduino does. Every iteration a byte is read from a fixed address and shown and then the current iteration number is written to that same fixed address on the EEPROM.

int main( int argc, char** argv )
{
  EEPROMTest obj( argv[1] );
  address_t addr(305);
  for( uint8_t loopcount = 0; ; loopcount++ )
  {
    int d = (int)obj.read( addr );
    cerr << "MAIN: read data:"  << hex << d << endl;
    cerr << "MAIN: write data:" << hex << (int)loopcount << endl;
    obj.write( addr, loopcount );
    sleep( 1 );
  }
  return 0;
}

The EEPROMTest class has a few methods, the read() and write() being the most interesting and a constructor. The EEPROMTest::write() method is shown below. In a similar way to the Arduino program first write enable is set, then the WRITE instruction ,address, and byte are sent. This is followed by disabling write again. The tostr() method converts the instruction to a string for use with the spi_write() function shown at the end of the code block. The spi_write() is a simple wrapper of write() which sends a std::string to a file descriptor.

void write( address_t addr, uint8_t b )
{
  spi_write( m_fd, tostr(INSTWREN), true );
  {
    stringstream ss;
    ss << INSTWRITE << addr << flush;
    ss.write( (const char*)&b, sizeof(uint8_t));
    spi_write( m_fd, ss.str(), true );
  }
  spi_write( m_fd, tostr(INSTWRDI), true );
}
std::string tostr( instruction_t v )
{
  std::stringstream ss;
  ss << v;
  return ss.str();
}
static void spi_write( int fd, std::string v )
{
 write( fd, v.data(), v.size() );
}

Each call to write() will grab the chip select, put data on the bus, and release the chip select. This is fine for sending bytes to write and simple instructions which are “send only” to the EEPROM. But to read from the EEPROM you have to send the READ instruction and address and hold the chip select while you read bytes back. The EEPROMTest::read() method uses the spi_transfer() function to send and receive data on the SPI at the same time. Notice that 4 bytes are sent, the 8 bit READ, the 16 bit address and then the 8 bit value of “d” which can be any value really as the EEPROM will ignore it. Sending the 8 bits of “d” allows the SPI bus to send back the 8 bits that are contained in the address we are interested in. So the data returned from spi_transfer() will have the value of the EEPROM at address addr in its array at data[3].

uint8_t read( address_t addr )
{
  uint8_t d = 77;
  stringstream ss;
  ss << INSTREAD << addr;
  int prefixlength = ss.str().length();
  ss.write( (const char*)&d, sizeof(uint8_t));
  std::string data = spi_transfer( m_fd, ss.str() );
  data = data.substr(prefixlength);
  return data[0];
}

The spi_transfer() takes a string to send and will return a string of equal length which is what has been sent back to the BeagleBone Black over SPI while the transmission was occurring. So for the EEPROM the first 24 bits of the data we get back are useless. But after we send the 24 bits (READ instruction and 16 bit address) the next byte sent back is the byte on the EEPROM at that address. By providing the transmission buffer (tx_buf) and the receive buffer (rx_buf) of the same size, the single ioctl() will perform both read and write on the SPI at the same time.

static std::string spi_transfer( int fd, std::string v )
{
  struct spi_ioc_transfer xfer[2];
  int status;
  memset(xfer, 0, sizeof xfer);
  std::string ret;
  ret.resize( v.size() );
  
  xfer[0].tx_buf = (unsigned long)v.data();
  xfer[0].rx_buf = (unsigned long)ret.data();
  xfer[0].len = v.size();
  xfer[0].delay_usecs = 200;
  status = ioctl(fd, SPI_IOC_MESSAGE(1), xfer);
  if (status < 0) {
     perror("SPI_IOC_MESSAGE");
     return "";
  }
  return ret;
}

The program output looks like the following. In this case I had run 5 iterations the previous time I executed spibbb which is why the first read got a 5. The top lines are using a slightly modified dumpstat() from the spidev_fdx.c example to show the SPI settings for the selected spidev device file.

$ ./spibbb /dev/spidev1.0 
opened file /dev/spidev1.0
before applying settings: spi mode 0, 8 bits (msb) per word, 500000 Hz max
after applying settings: spi mode 0, 8 bits (msb) per word, 500000 Hz max
MAIN: read data:5
MAIN: write data:0
MAIN: read data:0
MAIN: write data:1
MAIN: read data:1
MAIN: write data:2

The EEPROMTest constructor shows the current SPI settings for the device and then changes things to ensure the most significant bit is sent first, the clock speed of the SPI is acceptable, and the SPI mode is something that the EEPROM IC will use.

dumpstat("before applying settings", m_fd );
uint8_t lsb = 0;
ioctl( m_fd, SPI_IOC_WR_LSB_FIRST, &lsb );
uint32_t speed = 500000;
ioctl( m_fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed );
uint32_t mode = SPI_MODE_0;
ioctl( m_fd, SPI_IOC_WR_MODE, &mode );
dumpstat("after applying settings", m_fd );

In the above, instructions and addresses are written to std::iostreams without regard to the needed 8 and 16 bit sizes that the EEPROM is expecting. This is handled by overloading the output operators for instruction_t and address_t so that data is written to the output stream as the chip expects.

enum instruction_t
{
    INSTREAD  = 0b00000011,
    INSTWRITE = 0b00000010,
    INSTWREN  = 0b00000110,
    INSTWRDI  = 0b00000100,
    INSTRDSR  = 0b00000101
};
class address_t
{
public:
    uint16_t m_v;
    address_t( int v = 0 )
        : m_v(v)
    {
    }
};
std::ostream& operator<<( std::ostream& os, const instruction_t& v )
{
    os.write( (const char*)&v, sizeof(uint8_t));
    return os;
}
std::ostream& operator<<( std::ostream& os, const address_t& v )
{
    os.write( (const char*)&v.m_v, sizeof(uint16_t));
    return os;
}

Next time I hope to use the BeagleBone Black to talk over SPI to a triple axis accelerometer and a 7-segment serial display which is itself running an ATMega328 on it’s baseboard. The accelerometer has some interrupt pins which I’ll need to monitor and react to from the Beagle.

For previous articles in this series see:

Getting Started With the BeagleBone Black: A 1GHz ARM Linux Machine for $45

BeagleBone Black Part 2: Linux Performance Tests