Tags
In my previous post, I started exploring alternatives to the Arduino + Red Bear Lab BLE shields that are currently part of the Ladybug Shield design. I’m noticing aggressive announcements by multiple vendors for SoCs that combine a micro controller like the Cortex m0 with BLE chips, many gpio ports, SPI, and I2C interfaces.
I am using the nRF51 DK and Arm’s Keil v5 IDE. In this post I take on the challenge of sending and receiving pH readings over i2c. Once I can do that, I will have crossed the most major chasm between the digital and analog circuits of the Ladybug Shield. This will give me a clear idea if I could use the nRF51822 as a replacement for the Arduino + Red Bear Lab BLE shield. I can’t wait to tell you…talk about a YIPPEE! moment….It works!!
The Goal
The goal of this post is to transmit data over the I2C interface. Confirmation that the data was transferred will be output of the SDA/SCL lines when my Saleae Logic Analyzer is inserted between the I2C slave (I will be using Adafruit’s ADS1015 BoB…it could be any I2C chip at this point, I just needed a destination to transmit data to).
Thanks to Those That Went Before
Adafruit played a huge role in enabling me to get this to work. Thank you Adafruit for your open source availability, excellent tutorials, support, and products. I hope/wish other companies embrace this ethos into their business model. Imagine how many makers would be empowered on the nRF51822 if there were the same level of tutorial, support, and product availability for Arm’s Keil IDE + nRF51822 BLE module.
The first person I wanted to tell I got this working was Chris Gammell. Chris has been a fantastic teacher and mentor. I highly recommend his Contextual Electronics course – which I am currently a student.
Thank you Saleae for your EXCELLENT and reasonably priced logic analyzer. I am a fan of your product and dedication to helping us easily/intuitively analyze our circuits.
The Test Setup
I used the same setup to simulate pH voltage (b) values as I did when I first started testing the pH circuit. The other components in the test include Adafruit’s ADS1015 BoB (a) to provide the ADC conversion of the voltage into a digital value that is read from either an Arduino (c) or nRF51 (d) DK over i2c.
- all shared the same ground
- the voltage divider used a separate 5V power supply.
- the Arduino or nRF52 DK used the USB power from my Mac.
- I took advantage of the nRF52 DK’s mapping to the Arduino pin out, mapping SCL and SDA to the equivalent pins that are on the Arduino. This way, I was less likely to mix up the green (SCL) and yellow (SDA) cables.
- I used:
- the Arduino’s 5V power source to power the ADS1015.
- I used my DMM and logic analyzer to analyze results outside of the software.
- I used Serial.println()s to analyze the results from the Arduino sketch.
- I used the Keil debugger to step through and analyze results from the C source used on the nRF51 DK.
Results
The output of the voltage divider supplied the voltage reading to the ADS1015’s AIN0. I used the default i2c address of the ADS1015 – 0x48.
Arduino
This image is a screen capture of the i2c traffic when the Arduino is used to get the ADC value:
nRF51822
Here are the images for the i2c write and read traffic when the nRF51 DK is used:
i2c write traffic from the nRF51822 to the ADS1015
i2c read traffic from the ADS1015 to the nRF51822
Traffic Analysis
(note: for the text below “master” could be either the Arduino or the nRF51 DK)
I found this tutorial on the I2C bus to be very helpful in understanding I2C traffic. To read the traffic, we first have to know that read/write data transfers over i2c start with the master sending a byte in which the least significant bit (LSB) is either a 0 (for a write request from the master) or 1 (for a read request). The higher 7 bits = the slave device’s address. In the sniffs above, the first write traffic byte is 0x90 = b1001 0000. Thus the i2c address = b0100 1000 = 0x48 – the address of the ADS1015. Since this is a write request, the least the least significant bit is a 0.
The ADS1015 sees the traffic is for it so it sends back an ACK. The write then occurs. The master sense 0x01, which as noted in the ADS1010 data sheet“
Second byte: 0b00000001 (points to Config register)
Third byte: 0b00000100 (MSB of the Config register configuration)..in this example: 0xCE
Fourth byte: 0b10000011 (LSB of the Config register)….in this example: 0x83
While I could go on crafting packets based on the data sheet, the Adafruit_ADS1X15 library makes writing code dramatically easier. It provides a “cheat sheet” of the ADS1015 data sheet’s discussion of I2C packet formats. The 0xC383 bytes are the result of a call to Adafruit’s readADC_SingleEnded() when m_gain = GAIN_ONE:
/**************************************************************************/
/*!
@brief Gets a single-ended ADC reading from the specified channel
*/
/**************************************************************************/
uint16_t Adafruit_ADS1015::readADC_SingleEnded(uint8_t channel) {
if (channel > 3)
{
return 0;
}
// Start with default values
uint16_t config = ADS1015_REG_CONFIG_CQUE_NONE | // Disable the comparator (default val)
ADS1015_REG_CONFIG_CLAT_NONLAT | // Non-latching (default val)
ADS1015_REG_CONFIG_CPOL_ACTVLOW | // Alert/Rdy active low (default val)
ADS1015_REG_CONFIG_CMODE_TRAD | // Traditional comparator (default val)
ADS1015_REG_CONFIG_DR_1600SPS | // 1600 samples per second (default)
ADS1015_REG_CONFIG_MODE_SINGLE; // Single-shot mode (default)
// Set PGA/voltage range
config |= m_gain;
// Set single-ended input channel
switch (channel)
{
case (0):
config |= ADS1015_REG_CONFIG_MUX_SINGLE_0;
break;
case (1):
config |= ADS1015_REG_CONFIG_MUX_SINGLE_1;
break;
case (2):
config |= ADS1015_REG_CONFIG_MUX_SINGLE_2;
break;
case (3):
config |= ADS1015_REG_CONFIG_MUX_SINGLE_3;
break;
}
// Set ‘start single-conversion’ bit
config |= ADS1015_REG_CONFIG_OS_SINGLE;
// Write config register to the ADC
writeRegister(m_i2cAddress, ADS1015_REG_POINTER_CONFIG, config);
// Wait for the conversion to complete
delay(m_conversionDelay);
// Read the conversion results
// Shift 12-bit results right 4 bits for the ADS1015
return readRegister(m_i2cAddress, ADS1015_REG_POINTER_CONVERT) >> m_bitShift;
Now that the config register is set, the i2c traffic asks the slave for the contents of the convert register.
..and there it is … both the Arduino and the nRF51 DK sent equivalent i2c traffic. My iOS app wouldn’t know whether it’s talking to an Arduino or nRF51822.
Coding on the nRF51822’s I2C Module
Getting I2C traffic to work with the nRF51 SDK was similar to getting most apis I have been clueless about. Unfortunately, while Nordic provides pretty good developer support, there is not the “geez this is easy let’s do it this weekend” type of tutorials that abound for the Arduino. Getting out of the clueless state when it came to accessing I2C took longer than it might…of course, some of the cluelessness is innate to me.
I’ll cut out the remarks and list what I found to be the most valuable things I learned:
- the term to use when looking up anything to do with I2C is TWI. This makes sense given this comment: “The name TWI was introduced by Atmel and other companies to avoid conflicts with trademark issues related to I²C…Expect TWI devices to be compatible to I²C devices except for some particularities like general broadcast or 10 bit addressing.”
- Use the latest SDK (in my case this is 8.1)
- Stay away from using the Pack installer in Keil v5. Packs make installing and maintaining drivers – like the TWI driver source – easy, peasy. The challenge is a pack has a significant amount of dependencies. Given the hours I spent trying to get my code to work by using Packs, I am not convinced the dependencies within the Nordic nRF51 DK packs are all correct. I ended up downloading the SDK 8.1 zip (note: DO NOT download the file with Pack in its name).
- use the nRF_drv_twi driver from the SDK. This driver was released in SDK 8.1. I found it simple and “just worked.” As noted in this post on the Nordic DevZone, there are previous versions. Most of the DevZone posts relate to the previous versions (and…this tricked me up for awhile since I did not have the context on why there were three…which was the one to use…).
- check out the (extremely sparse 🙂 ) TWI documentation. The example code snippet got me started on writing/reading over i2c.
- it seems there was a “oops” that was caught after SDK 8.1 was distributed. As noted in this devzone post, the following must be added to nrf_drv_config.h:
- Choose between the TWI drivers, or use both. In my case I am using the TWI0 driver. I enabled this by setting #define TWI0_ENABLED to 1.
- set the gpio pins for SCL and SDA. I chose pin 0.06 for SCL and 0.05 for SDA.
- If using Keil v5, get familiar with the (powerful) debugger. I like taking a walk with the code…by stepping through the lines within the debugger and looking at the values within variables….I am at the n00b level of understanding this debugger. Even with that, it is a far more pleasant debugging experience than within the Arduino IDE :-).
/**************************************************************************/
/*!
@brief A simple read of one of the four ADC channels. Does not use the differential
*/
/**************************************************************************/
uint16_t readADC_SingleEnded(nrf_drv_twi_t const * const p_instance,uint8_t channel) {
if (channel > 3)
{
return 0;
}
m_bitShift = 4;
// Start with default values
uint16_t config = ADS1015_REG_CONFIG_CQUE_NONE | // Disable the comparator (default val)
ADS1015_REG_CONFIG_CLAT_NONLAT | // Non-latching (default val)
ADS1015_REG_CONFIG_CPOL_ACTVLOW | // Alert/Rdy active low (default val)
ADS1015_REG_CONFIG_CMODE_TRAD | // Traditional comparator (default val)
ADS1015_REG_CONFIG_DR_1600SPS | // 1600 samples per second (default)
ADS1015_REG_CONFIG_MODE_SINGLE; // Single-shot mode (default)
// Set PGA/voltage range
config |= m_gain;
// Set single-ended input channel
switch (channel)
{
case (0):
config |= ADS1015_REG_CONFIG_MUX_SINGLE_0;
break;
case (1):
config |= ADS1015_REG_CONFIG_MUX_SINGLE_1;
break;
case (2):
config |= ADS1015_REG_CONFIG_MUX_SINGLE_2;
break;
case (3):
config |= ADS1015_REG_CONFIG_MUX_SINGLE_3;
break;
}
// Set ‘start single-conversion’ bit
config |= ADS1015_REG_CONFIG_OS_SINGLE;
// Write config register to the ADC
// Note: currently block until write happens
writeRegister(p_instance,m_i2cAddress, ADS1015_REG_POINTER_CONFIG, config);
// Read the conversion results
// Shift 12-bit results right 4 bits for the ADS1015
uint16_t valInRegister = readRegister(p_instance,m_i2cAddress, ADS1015_REG_POINTER_CONVERT);
return valInRegister >> m_bitShift;
}
/**************************************************************************/
/*!
@brief Writes 16-bits to an ADS1015 register
*/
/**************************************************************************/
void writeRegister(nrf_drv_twi_t const * const p_instance,uint8_t i2cAddress, uint8_t reg, uint16_t value) {
uint8_t tx_data[3];
uint32_t err_code;
tx_data[0] = (uint8_t)reg;
tx_data[1] = (uint8_t)(value>>8);
tx_data[2] = (uint8_t)(value & 0xFF);
err_code = nrf_drv_twi_tx(p_instance, i2cAddress, tx_data, sizeof(tx_data), false);
APP_ERROR_CHECK(err_code);
}
/**************************************************************************/
/*!
@brief Reads 16-bits from an ADS1015 register
*/
/**************************************************************************/
uint16_t readRegister(nrf_drv_twi_t const * const p_instance,uint8_t i2cAddress, uint8_t reg) {
uint32_t err_code;
// TBD: err_code (error handling) should be identical to all other nordic modules
err_code = nrf_drv_twi_tx(p_instance, i2cAddress, ®, sizeof(reg), false);
APP_ERROR_CHECK(err_code);
err_code = nrf_drv_twi_rx(p_instance,i2cAddress,data,2,false);
value = (data[0] << 8) | data[1];
return value;
}
That’s It For Now
While there were moments when my head hurt from banging it on my desk…thanks to previous efforts by Adafruit and folks on Nordic’s DevZone, I was happy to be able to get this working. Especially given my weak coding skills and n00bness to embedded systems and their tool chains. Having said all that, I am watching Apple’s WWDC 2015 videos…longing for a Swift playground that talks to all these SoCs /dev environments: Arduino, nRF51 DK….so that testing things out would be as easy as breadboard prototyping and looking at signals from a scope or DMM…well..now that Swift is open source…anyways…
Please find many things to smile about!