Help writing I2C library

I’m trying to write a baremetal I2C library for the FE310-0002 and have found some information:

  • I2C address @ 0x10016000
  • Has registers PRERlo, PRERhi, CTR, TXR, RXR, CR, & SR
  • No official I2C library from SiFive
  • Library available from Zephyr

What I am stuck on is writing and reading registers to the I2C address in C. I’m trying to to use some op codes like ldi to load any value into that register. Yet, every time I read the disassembly, that address always has 0xff stored. Are there some protection circuits on this chip that prevent me from reading and writing to registers? I’m trying to understand this whole picture a bit more. Thanks :slight_smile:

All of the i2c hardware is described in this repo
You’ve found the base as 0x10016000
The registers are
prescaler_lo 0x00
prescaler_hi 0x04
control 0x08
data 0x0C
cmd_status 0x10 //bit0=ack, …

From C you might try something like this
#define i2c0_data (*(volatile uint32_t *) (0x10016000 + 0x0C))
#define i2c0_data_tx(x) ( i2c0_data = (uint32_t) (( i2c0_data & ~(0xFF << 0)) | (( (x) & 0xFF) << 0)) )
#define i2c0_data_rx() ( ((i2c_data >> 0) & 0xFF) )

You might then do something like
i2c0_data_tx( 0x55 ); // write to the data reg
uint8_t foo = i2c0_data_rx(); // read from the data reg

Thanks to @Seif for suggestion of this clever #define r-m-w paradigm

1 Like

At times I have also seen the compiler do strange things with my code. Seems like there is overzealous optimizations going on, by default. For example, when I code a simple spin-loop with an empty body as a crude delay, there is no loop in the assembly at all. Only when I put a call to some ‘dummy’ function (doing some non-trivial things like inc or dec) in the spin-loop will the loop show up in the assembly.
Keep trying, and be creative with the C while watching the lst output.

I use the following options:
riscv32-unknown-elf-as -march=rv32imac -mabi=ilp32 foo.s -o foo.o
riscv32-unknown-elf-gcc -march=rv32imac -mabi=ilp32 -Wall -O2 -nostdlib -nostartfiles -ffreestanding -c bar.c -o bar.o
riscv32-unknown-elf-ld foo.o ... -T -o foo.elf -Map
riscv32-unknown-elf-objdump -D foo.elf > foo.lst
riscv32-unknown-elf-objcopy foo.elf -O ihex foo.hex
riscv32-unknown-elf-objcopy foo.elf -O binary foo.bin

1 Like

Thank you so much for the reply. Can you explain why this code has the registers declared at different hex values when compared to the spec sheet from OpenCores? (RXR and TXT ar 0x03 in the OpenCores spec sheet, but the code has it aligned to 0x0C) Is it aligning it to the lower/upper word?


The 32-bit implementation here keeps registers, and executable op codes, on a four-byte boundary, as in
0x00 = 0 * 4
0x04 = 1 * 4
0x08 = 2 * 4
0x0C = 3 * 4
0x10 = 4 * 4
It’s byte-wise little endian so alignment is on the low order byte.

There is also an interesting discussion related to E310 here.

1 Like

Hi :wave:

As a matter of fact I’ve just finished up with a C++ HAL driver for the I2C core of the FE310-G002. The high-level usage of the code looks like this:

#include <cassert>
#include <snowfox/hal/riscv64/FE310/Io.h>
#include <snowfox/hal/riscv64/FE310/Clock.h>
#include <snowfox/hal/riscv64/FE310/I2cMaster.h>
/* ... */
using namespace snowfox::hal;
/* ... */
static uint32_t const HFXOSCIN_FREQ_Hz     =  16'000'000UL;
static uint32_t const CORECLK_FREQ_Hz      = 200'000'000UL;

static uint8_t  const M24LR_ADDRESS_SYSTEM = (0x57 << 1);
static uint16_t const M24LR_REG_ICREF      = 0x091C;
static uint8_t  const M24LR_IC_REF         = 0x5A;
/* ... */
uint8_t m24lr_readByte(uint16_t const reg_addr)
  i2c_master.begin(M24LR_ADDRESS_SYSTEM, false);

  uint8_t const reg_addr_msb = static_cast<uint8_t>((reg_addr & 0xFF00) / 256);
  uint8_t const reg_addr_lsb = static_cast<uint8_t>((reg_addr & 0x00FF));


  uint8_t data = 0;
  i2c_master.requestFrom(M24LR_ADDRESS_SYSTEM, &data, 1);

  return data;
/* ... */
/* ... */
int snowfox_main()
  clock.setClockFreq(static_cast<uint8_t>(FE310::ClockId::coreclk), CORECLK_FREQ_Hz);


  uint8_t const m24lr_ref_id = m24lr_readByte(M24LR_REG_ICREF);
  assert(m24lr_ref_id == M24LR_IC_REF);

  for(;;) { }
  return 0;

The full example code can be found here.

If you want to compile it for your HiFive 1 Rev. B you’ve got to follow the following steps:

  • Download/Install RISCV64 Compiler (you probably already did that)
sudo wget
sudo tar -xzvf riscv64-unknown-elf-gcc-8.2.0-2019.02.0-x86_64-linux-ubuntu14.tar.gz
export PATH=$PATH:/opt/riscv64-unknown-elf-gcc-8.2.0-2019.02.0-x86_64-linux-ubuntu14/bin
  • Clone the Snowfox Repository with all examples
git clone --recurse-submodules
cd snowfox
  • Build the SiFive FE310-G002 I2C example (the compiled binary is located in build/bin/hal-fe310-i2c )
.ci/script/ examples/hal/FE310/hal-fe310-i2c

If you want to take a look at the code you’ll find it here. The code is contained in files I2CMaster.cpp, I2CMasterBase.cpp and I2CMasterLowLevel.cpp .

One thing that’s a bit tricky is that the command and status register are found at the same address and you access the command register by writing to it and the status register by reading from it. So if you are used to setting bits the usual way, that is reg |= (1<<bit_pos) you’ll be in for a surprise since that will be expanded to reg = reg | (1<<bit_pos) and I think the problem is now obvious :wink:


At times I have also seen the compiler do strange things with my code. Seems like there is overzealous optimizations going on, by default. For example, when I code a simple spin-loop with an empty body as a crude delay, there is no loop in the assembly at all.

Unless you use volatile when declaring the counter it will most likely get optimized away with anything other than -O0.
This is not “overzealous” - it is expected as the code is doing nothing useful.
Anyway delay loops such as thatt are generally not a great idea.

I really appreciate the response. I am just trying to learn how to write (unsuccessfully so far) an I2C driver as a hobby in my free time. This code looks excellent and I’ll take a look at what you did :slight_smile:

#include <stdio.h>
#include <metal/cpu.h>
#include <metal/led.h>
#include <metal/button.h>
#include <metal/switch.h>

#define I2C_ADDRESS						0x10016000
#define PRESLo 							0x10016000
#define PRESHi 							0x10016004
#define CTR 							0x10016008
#define STATUS							0x10016010
#define PRESCALER						0x3f

#define READ_I2C_ADDRESS()      		(*(volatile uint32_t *) I2C_ADDRESS)
#define I2C_WRITE(val) 					((*(volatile uint32_t *) I2C_ADDRESS) = (val))
#define I2C_WRITE_PRESCALER(val) 		((*(volatile uint32_t *) PRESHi) = (val))
#define I2C_WRITE_CTR(val) 				((*(volatile uint32_t *) CTR) = (val))

int main(void)
	// Set EN bit to 0 in CTR Register (CTR == 00000000)

	// Set Prescale Register

	// Reenable I2C
    printf("%d", 62);
    return 0;


This is my progress so far. I can’t seem to get a clock going on the SCK line. I think the prescaler is 3F according to the I2C documentation, so I’m just hard coding it to test it.

Hi @c0ntrarian :wave:

Well the prescaler really depends on the system clock you are using. In my example I’m running a core clock of 200 MHz which results in a prescaler of 399 (0x18F) for 100 kHz I2C clock.

One thing that immediately is obvious to me is that you are just enabling the I2C core (btw. writing 0x80 to the control register is enough since its set’s bit #7 which enables the I2C core). In any way - in order to get data to be clocked out on the bus you need to generate a start condition by writing to the command register (and first filling the slave address into the data register), e.g.

DATA = my_slave_addr;
COMMAND_STATUS = (1<<7) | (1<<4); /* STA (Bit 7) + WR (Bit 4) */

Also the GPIO special function register GPIO_IOF_EN and GPIO_IOF_SEL for the I2C must be configured to route I2C to the actual MCU pins and not the GPIO ports (see here) with

I2C0_SDA = 12, /* I2C0 SDA */
I2C0_SCL = 13, /* I2C0 SCL */

Have fun :wink:

1 Like

Please see:

Jim thank you, but the <metal/i2c.h> library is not currently available for the HiFive1 Rev B from what I understand. This is exactly why I’m writing this I2C library in the first place, praying I could learn micros a bit better in the process. See here

Hi c0ntrarian,

There is a new release of the freedom-e-sdk. With the v20.05.00.00 tag of freedom-e-sdk I’m able to target the HiFive1 rev B board, compile, and run the example-i2c program. The console reports an error “Failed to detect PmodTmp2 module” because I don’t have the I2C temp sensor and ADC hardware the example is targeting.

YES! That was my issue. I was using the prebuilt FreedomStudio download package from here. I just recursively cloned the git one and I am up and running with the example I2C. (I would still like to learn how to write my own I2C driver :slight_smile: Thank you all for your help :slight_smile:

Could someone explain what the deal is with prescale high and low? I understand that the prescaler max is 0xFFFF which is 2 bytes. Since the HiFive Rev. B (I think according to the schematic) a 16MHz clock… although I see 320MHz on the website, I used the formula in the I2C datasheet and came out with decimal 31 as my prescaler (100khz.) I’m supposed to write 0x1F to the prescale low and 0 out the high prescaler? Is that how it works?

You should use a correct frequency as your base frequency. Base frequency for the I2C peripheral is tlclk frequency. According to the picture on 6.1 Clock Generation Overview G002 manual it equals to the coreclk frequency. So if you are running off 320MHz, you need 639 divider: 0x27F - 0x02 high, 0x7F low. If you are running off 16MHz core clock, then your calculation is correct: 0x00 high and 0x1F low.

1 Like