Hey kids! Today we’re going to take a look at the SiFive HiFive1 Rev B and Freedom Metal I2C API.
I am going to be using a classic EEPROM from National Semiconductor, the NM24C17. The NM24C17 is a 16 kilobit (2K) EEPROM that can be written to and read from using I2C.
If you have one of these EEPROMs lying around (and who doesn’t?) and want to use it with your HiFive1 board, you’ll also need:
- the datasheet
- a breadboard
- breadboard wires
- 2 4.7k pull-up resistors
What you might also want to have handy a digital logic analyzer such as the Logic 8 from Saleae.
The I2C circuit is a simple one, but it is important to note that the NM24C17 EEPROM does not come with I2C pull-up resistors, so we need to add them in our circuit.
Assembling everything with the HiFive1 I2C pins and providing power.
Reading the Datasheet
When working with I2C devices it is so important to read through the datasheet once or twice. Or ten times. Datasheets can be dense and intimidating, but I have rarely come across an issue I was troubleshooting that didn’t end up being caused by not reading the datasheet closely.
Writing
Okay, let’s start coding some I2C with Freedom Metal. We’ll start with a basic shell.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <stdio.h> #include <metal/i2c.h> int main(void) { struct metal_i2c* i2c_device = metal_i2c_get_device(0); if (!i2c_device) { printf("Unable to obtain I2C device\n"); return -1; } metal_i2c_init(i2c_device, 100000, METAL_I2C_MASTER); printf("Exit\n"); return 0; } |
Working with I2C in Freedom Metal starts with including the <metal/i2c.h>
header file and obtaining a pointer to the I2C device with metal_i2c_get_device
. For the HiFive1 Rev B board there is only one device to get, and it’s at index 0. Once you have a pointer to the I2C device, initialize it with metal_i2c_init
. We’ll configure our device for 100 kbits/sec (I2C “full speed”) and as the master.
Now, let’s look at our first write function, which will be to write a sequence of bytes to the EEPROM at a given address. This code is very specific to the way the NM24C17 EEPROM functions. We will be using the metal_i2c_write
function which takes as its arguments:
- a pointer to the I2C controller device on the RISC-V chip
- the address of the I2C bus device to talk to
- the length of the message to send to the bus device
- the message to send
- a flag indicating whether or not to send the I2C stop bit
The first argument will be our struct metal_i2c* i2c_device
variable, but the address of the EEPROM on the bus is interesting.
At first blush it appears the address would be 0xa0
to account for the first four bits to transmit are 1 0 1 0
, and for the NM24C17 device the 3 page address bits appear to be all 0 (appear is the operative word). That leaves us with the R/W bit, which for a write would be 0. 1010 0000b
, or 0xa0
, right? Wrong. The R/W bit is not a part of the device address here, which leaves us with 1010000b, which is 0x50.
The message to send to the EEPROM consists of two bytes: the memory address in the EEPROM to write to and the value to write. For simplicity we’ll just write the value 0xab at the address 0x00.
1 2 3 |
unsigned int addr = 0x50; unsigned char buf[] = {0x00, 0xab}; metal_i2c_write(i2c_device, addr, 2, buf, METAL_I2C_STOP_ENABLE); |
The final argument to metal_i2c_write
is to indicate whether or not to signal an I2C stop bit upon completion. Since the stop bit is required for us to write this data to the EEPROM we will use METAL_I2C_STOP_ENABLE
.
One thing I’ve found to be true about I2C is that if it works, it works. If it doesn’t, you better have a digital logic analyzer on hand to look at things.
Reading
Now let’s read the data that we wrote back in. This requires two function calls: metal_i2c_write
and metal_i2c_read
.
Notice above that the first step to reading a byte from the EEPROM is to write out the address to be read from, followed by a read. There is only one stop bit in this sequence:
1 2 3 4 5 6 7 |
unsigned char readbuf[1] = {0x00}; if (!metal_i2c_write(i2c_device, addr, 1, readbuf, METAL_I2C_STOP_DISABLE)) { if (!metal_i2c_read(i2c_device, addr, 1, readbuf, METAL_I2C_STOP_ENABLE)) { printf("Data read = %x\n", readbuf[0]); } } |
Notice the use of METAL_I2C_STOP_DISABLE
; this instructs the I2C controller not to signal a stop bit at the conclusion of the write.
We make double use of the readbuf
array by initializing it to the address in the EEPROM we want to read from, and then to hold the data read in.
Writing and Reading Multiple Bytes
Writing and reading one byte at a time to our EEPROM is a bit tedious, so let’s make use of the multiple-byte write.
In this example we send the device address (again, 0x50 for the EEPROM), followed by an address to write to in the EEPROM, and then up to 16 bytes of data.
Before we get to writing again, I’ve made mention that 0x50 is the I2C device address of the EEPROM. While it is, that isn’t the whole story. That is the address for page block 0 of the device, but it supports 8 page blocks. Selecting the page block is done with the lower nibble of the device address. For example, page block 1 can be addressed at 0x51, page block 2 at 0x52, and so on. Each page block is 2 kilobits, or 256 bytes.
At any rate, let’s stick with page block 0 for now and write 16 bytes:
1 2 3 4 5 6 7 |
unsigned int addr = 0x50; unsigned char writebuf[17] = {0x00}; srand(time(NULL)); for (int i = 1; i <= 16; i++) { writebuf[i] = rand() % 0xff; } metal_i2c_write(i2c_device, addr, 17, writebuf, METAL_I2C_STOP_ENABLE); |
Of course, we are actually writing 17 bytes out to the EEPROM, the first of which is the address we want to write to.
Reading the data back in can look like this:
1 2 3 4 |
unsigned int addr = 0x50; unsigned char readbuf[17] = {0x00}; metal_i2c_write(i2c_device, addr, 1, readbuf, METAL_I2C_STOP_DISABLE); metal_i2c_read(i2c_device, addr, 16, readbuf+1, METAL_I2C_STOP_ENABLE); |
A Smarter API
After understanding the basics of reading and writing to the NM24C17 it’s time to write an API to encapsulate the nuts and bolts. Our header file looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <stdint.h> typedef struct nm24c17_msg { uint8_t pageblock; // Page block within NM24C17 uint8_t len; // Length of the buffer uint8_t addr; // EEPROM address within page uint8_t buf[16]; // Data } nm24c17_msg; // Write to the NM24C17 int nm24c17_write(nm24c17_msg* msg); // Read from the NM24C17 int nm24c17_read(nm24c17_msg* msg); |
Our structure is arranged such that the addr
byte is positioned immediately prior to the buf
. This organization allows us to take advantage of the C memory layout of the data and writing to the EEPROM. For example:
1 2 3 4 5 6 7 8 9 |
int nm24c17_write(nm24c17_msg* msg) { struct metal_i2c* i2c = i2c_setup(); if (!i2c) return -1; unsigned int device_addr = NM24C17_BASE_ADDR | msg->page; uint8_t len = 1 + msg->len; return metal_i2c_write(i2c, device_addr, len, &msg->addr, METAL_I2C_STOP_ENABLE); } |
Our device address is the base address of the EEPROM (0x50) ORed with the page block number. The length of the message to write is the length of the data buffer the user wants to write plus the address byte. Writing starts at the address byte and continues into the buffer.
Remember the exhortation to read the datasheet, and read it several more times? Here is what happens if you start a write on an address not evenly divisible by 16. Notice that our write address starts at 0x22 in page block 1. Sixteen bytes are presumably written, but when trying to read them back in, the last two bytes read are 0xff. Hmm.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Page Addr Wrote Read 01 22 63 63 01 23 7c 7c 01 24 77 77 01 25 7b 7b 01 26 f2 f2 01 27 6b 6b 01 28 6f 6f 01 29 c5 c5 01 2a 30 30 01 2b 01 01 01 2c 67 67 01 2d 2b 2b 01 2e fe fe 01 2f d7 d7 01 30 ab ff 01 31 76 ff |
From the National Semiconductor datasheet definitions:
- PAGE – 16 sequential addresses (one byte each) that may be programmed during a “Page Write” programming cycle.
- PAGE BLOCK – 2,048 (2K) bits organized into 16 pages of addressable memory. (8 bits) x (16 bytes) x (16 pages) = 2,048 bits”
But wait! The Fairchild-printed version of this EEPROM’s datasheet says a bit more:
To minimize write cycle time, NM24C16/17 offer Page Write feature, by which, up to a maximum of 16 contiguous bytes locations can be programmed all at once (instead of 16 individual byte writes). To facilitate this feature, the memory array is organized in terms of “Pages.” A Page consists of 16 contiguous byte locations starting at every 16-Byte address boundary (for example, starting at array address 0x00, 0x10, 0x20 etc.)
Like I said, always read the datasheet and sometimes you have to read two of them to get the whole story.
Recap
There are four basic functions needed to use I2C with Freedom Metal:
- metal_i2c_get_device – obtain a pointer to the underlying I2C device on the microcontroller
- metal_i2c_init – initialize the I2C device speed and mode
- metal_i2c_write – address and send data on the bus
- metal_i2c_read – address and read data from the bus
It really is that simple!
Get the Code
I’ve recently start using PlatformIO to develop on my HiFive1. If you haven’t checked it out I recommend you doing so; it’s very easy to install and start making use of your board right away without having to manually download toolchains and JTAG interfaces. I’ve posted a PlatformIO-based project on GitHub for working with the NM24C17.