TinyWireM vs. Wire for ATtinys

About this post

In the last post I had reported about programming ATtinys with ATTinyCore. This package allows using the USI interface of the ATtinys for I2C use with the Wire library. Special libraries for the I2C implementation, such as TinyWireM, TinyI2C or USIWire are thus no longer needed. However, since they are leaner, their use on the ATtinys with their limited program memory can still be useful.

In this article, I use Adafruit’s TinyWireM library as an example to explore how big the savings are compared to Wire. I also introduce TinyWireM compatible libraries for some I2C based devices. And finally, I show with two examples that you can modify many I2C based libraries to make them compatible with TinyWireM without much effort. 

TinyWireM and TinyWireS

The”M” in TinyWireM stands for Master. That means the library allows to make the ATtiny the I2C master, but not the slave. For the latter, there are several other libraries like TinyWireS. For example, if you wanted two ATtinys to communicate with each other via I2C, you would need both variants. I will confine myself here to the use of the ATtinys as masters in this post.

Preparations and requirements

I assume that you have installed the ATTinyCore package from Spence Konde. I also assume you know how to burn bootloaders and upload sketches with it. If that’s not the case, just check out my last post, where I explained it all in detail. 

You should as well have TinyWireM installed. You can get the library via the library manager of the Arduino IDE. Alternatively, you can download the library directly from GitHub here.

For this article, I use an ATtiny85. You can just as well take another ATtiny which is compatible with ATTinyCore. However, it should be a model with 8 kByte program memory. Otherwise, it will be too small for some examples.

Pinout ATtiny85 / 45 / 25
Pinout ATtiny85 / 45 / 25

Using TinyWireM and Wire for the MPU6050

In most cases you probably use libraries for I2C devices which do it for you to address the devices on register level using the wire functions. However, to understand the differences between TinyWireM and Wire, let’s now look at an example where we don’t use a component library.

I have chosen the 6-axis gyroscope and accelerometer module MPU6050 as a demonstration object. Those interested in the details of this component can read my post on it. Here and now, it’s all about seeing TinyWireM and Wire in action.

Since I want to output the readings of the MPU6050 on the serial monitor, I burned the Optiboot bootloader to the ATtiny85 and uploaded the sketches with a USB-to-TTL Serial Adapter. The following circuit was used:

Circuit for the control of the MPU6050
Circuit for the control of the MPU6050

MPU6050 Control with Wire

The following sketch uses the wire library for I2C communication. The MPU6050 is “woken up” in the setup. The three acceleration values, the temperature and the three gyroscope values are then read out one after the other in the main loop. Since these are 16-bit values, two registers must be read at a time, for a total of fourteen. Then the values are output on the serial monitor.

#include<Wire.h>
#define MPU6050_ADDR 0x68  

int16_t AcX,AcY,AcZ,Tmp,GyX,GyY,GyZ;
void setup(){
  Wire.begin();
  Wire.beginTransmission(MPU6050_ADDR);
  Wire.write(0x6B);  // PWR_MGMT_1 register
  Wire.write(0);     // set to zero (wakes up the MPU6050)
  Wire.endTransmission(true);
  Serial.begin(9600);
}
void loop(){
  Wire.beginTransmission(MPU6050_ADDR);
  Wire.write(0x3B);  // starting with register 0x3B (ACCEL_XOUT_H)
  Wire.endTransmission(false);
  Wire.requestFrom(MPU6050_ADDR,14,true);  // request a total of 14 registers
  AcX=Wire.read()<<8|Wire.read();  // 0x3B (ACCEL_XOUT_H) & 0x3C (ACCEL_XOUT_L)    
  AcY=Wire.read()<<8|Wire.read();  // 0x3D (ACCEL_YOUT_H) & 0x3E (ACCEL_YOUT_L)
  AcZ=Wire.read()<<8|Wire.read();  // 0x3F (ACCEL_ZOUT_H) & 0x40 (ACCEL_ZOUT_L)
  Tmp=Wire.read()<<8|Wire.read();  // 0x41 (TEMP_OUT_H) & 0x42 (TEMP_OUT_L)
  GyX=Wire.read()<<8|Wire.read();  // 0x43 (GYRO_XOUT_H) & 0x44 (GYRO_XOUT_L)
  GyY=Wire.read()<<8|Wire.read();  // 0x45 (GYRO_YOUT_H) & 0x46 (GYRO_YOUT_L)
  GyZ=Wire.read()<<8|Wire.read();  // 0x47 (GYRO_ZOUT_H) & 0x48 (GYRO_ZOUT_L)
  delay(2000);
  Serial.println();
  Serial.print(F("AcX = ")); Serial.print(AcX);
  Serial.print(F(" | AcY = ")); Serial.print(AcY);
  Serial.print(F(" | AcZ = ")); Serial.print(AcZ); 
  Serial.print(F(" | Tmp = ")); Serial.print(Tmp/340.00+36.53);  //equation for temperature in degrees C from datasheet
  Serial.print(F(" | GyX = ")); Serial.print(GyX);
  Serial.print(F(" | GyY = ")); Serial.print(GyY);
  Serial.print(F(" | GyZ = ")); Serial.println(GyZ);
}

 

The sketch requires 4058 bytes. This is ~53% of the available program memory (7616 bytes). Reminder: Actually, 8192 bytes are available, but the Optiboot bootloader needs 576 bytes.

Memory requirement of mpu6050_minimum_wire.ino with Wire
Memory requirement of mpu6050_minimum_wire.ino

For completeness, here is the output:

Output of mpu6050_minimum_wire.ino
Output of mpu6050_minimum_wire.ino

By the way, the ugly special characters come from the fact that pin PB0 is both SDA connector and serial output to the adapter. If this bothers you, you can also do the output via SoftwareSerial, for example with the pins PB3 and PB4. However, you then have to rewire after uploading and use more program memory (4898 bytes in total).

MPU6050 Control with TinyWireM

Even though TinyWireM is internally very different from Wire, the usage is comparable. However, some TinyWireM functions have different names:

Wire vs. TinyWireM functions
Wire vs. TinyWireM functions

Here is the “translation” of the sketch for TinyWireM:

#include <TinyWireM.h>
#define MPU6050_ADDR 0x68  

int16_t AcX,AcY,AcZ,Tmp,GyX,GyY,GyZ;
void setup(){
  TinyWireM.begin();
  TinyWireM.beginTransmission(MPU6050_ADDR);
  TinyWireM.send(0x6B);  // PWR_MGMT_1 register
  TinyWireM.send(0);     // set to zero (wakes up the MPU-6050)
  TinyWireM.endTransmission(true);
  Serial.begin(9600);
}
void loop(){
  TinyWireM.beginTransmission(MPU6050_ADDR);
  TinyWireM.send(0x3B);  // starting with register 0x3B (ACCEL_XOUT_H)
  TinyWireM.endTransmission(false);
  TinyWireM.requestFrom(MPU6050_ADDR,14);  // request a total of 14 registers
  AcX=TinyWireM.receive()<<8|TinyWireM.receive();  // 0x3B (ACCEL_XOUT_H) & 0x3C (ACCEL_XOUT_L)    
  AcY=TinyWireM.receive()<<8|TinyWireM.receive();  // 0x3D (ACCEL_YOUT_H) & 0x3E (ACCEL_YOUT_L)
  AcZ=TinyWireM.receive()<<8|TinyWireM.receive();  // 0x3F (ACCEL_ZOUT_H) & 0x40 (ACCEL_ZOUT_L)
  Tmp=TinyWireM.receive()<<8|TinyWireM.receive();  // 0x41 (TEMP_OUT_H) & 0x42 (TEMP_OUT_L)
  GyX=TinyWireM.receive()<<8|TinyWireM.receive();  // 0x43 (GYRO_XOUT_H) & 0x44 (GYRO_XOUT_L)
  GyY=TinyWireM.receive()<<8|TinyWireM.receive();  // 0x45 (GYRO_YOUT_H) & 0x46 (GYRO_YOUT_L)
  GyZ=TinyWireM.receive()<<8|TinyWireM.receive();  // 0x47 (GYRO_ZOUT_H) & 0x48 (GYRO_ZOUT_L)
  delay(2000);
  Serial.println();
  Serial.print(F("AcX = ")); Serial.print(AcX);
  Serial.print(F(" | AcY = ")); Serial.print(AcY);
  Serial.print(F(" | AcZ = ")); Serial.print(AcZ); 
  Serial.print(F(" | Tmp = ")); Serial.print(Tmp/340.00+36.53);  //equation for temperature in degrees C from datasheet
  Serial.print(F(" | GyX = ")); Serial.print(GyX);
  Serial.print(F(" | GyY = ")); Serial.print(GyY);
  Serial.print(F(" | GyZ = ")); Serial.println(GyZ);
}

 

This sketch needs only 3538 bytes, which is 520 bytes less. This corresponds to about one sixteenth of the 8 kByte program memory. With an ATtiny45 it would be already one eighth. This may not sound like much, but it could be just the bytes that decide which ATtiny you can use.

Memory requirements of mpu6050_minimum_tinywirem.ino with TinyWireM
Memory requirement of mpu6050_minimum_tinywirem.ino

TinyWireM compatible libraries

As you have just seen, you can save valuable program memory with TinyWireM. But you have also seen that some function names of TinyWireM and Wire are different. So for controlling an I2C based device, you can’t just include TinyWireM.h instead of Wire.h. The library must either be written for TinyWireM or allow its use as an option. I would like to show some examples of this. You can find all libraries I use for the examples on GitHub, or install them via the library manager of the Arduino IDE.

1. OLED display control with Tiny4kOLED

My first example is the library Tiny4kOLED, which is used to control small I2C-based OLED displays. The displays are available with 128 x 64 or 128 x 32 pixels. This is not lush, but sufficient to display a few measured values, for example.

Since we don’t need the serial monitor anymore, I programmed the ATtiny85 via ISP without bootloader in all upcoming examples.

The following circuit was used to operate the display:

I2C with the ATtiny85 - OLED Display
SSD1306 controlled OLED display connected to the ATtiny85

I won’t get into the details of the Tiny4kOLED library. There are many good example sketches included. Here is just a small “Hello World” sketch for illustration and as a test:

#include <TinyWireM.h>
#include <Tiny4kOLED.h>

uint8_t width = 128;
uint8_t height = 64;

void setup() {
  TinyWireM.begin();
  oled.begin(width, height, sizeof(tiny4koled_init_128x64br), tiny4koled_init_128x64br);
  oled.clear();
  oled.setFont(FONT6X8);
  oled.on();
  oled.setCursor(30, 4); // x: pixel / y: line
  oled.print("Hello world");
}

void loop() {
}

 

The sketch requires 2682 bytes, which is ~32% of the program memory.

Memory requirements of hello_world_tinywirem_tiny4koled.ino
Memory requirements of hello_world_tinywirem_tiny4koled.ino

And this is how the output looks on the display:

hello_world_tinywirem_tiny4koled.ino with TinyWireM
Output from hello_world_tinywirem_tiny4koled.ino

For comparison: Control with SSD1306Acii and Wire

For comparison, I tried a lean Wire.h-based library, namely SSD1306Ascii:

#include <Wire.h>
#include "SSD1306Ascii.h"
#include "SSD1306AsciiWire.h"
#define I2C_ADDRESS 0x3C

SSD1306AsciiWire oled;

void setup() {
  Wire.begin();
  oled.begin(&Adafruit128x64, I2C_ADDRESS);
  oled.setFont(System5x7);
  oled.clear();
  oled.setCursor(30,4);
  oled.print("Hello world!");
}

void loop() {}

 

The difference to the Tiny4kOLED solution was still 442 bytes:

Memory requirements of hello_world_wire_ssd1306ascii.ino
Memory requirements of hello_world_wire_ssd1306ascii.ino

2. control of the MCP23017 Port Expander

Switch between Wire and TinyWireM

I have rewritten three of my libraries to work with both Wire and TinyWireM. For this purpose I added a file named component_config.h (e.g. MCP23017_config.h) to each of the libraries, in which you only have to uncomment the line #define USE_TINY_WIRE_M_ to switch to the TinyWireM library. You can find the config files in the Arduino “libraries” folder → library name → src. 

The preprocessor directives #ifdef (if defined), #ifndef (if not defined), #else and #endif control which code is compiled and which is ignored. Here is a snippet:

#ifndef USE_TINY_WIRE_M_
        _wire->beginTransmission(I2C_Address);
        _wire->write(reg);
        _wire->endTransmission(false);
        _wire->requestFrom(I2C_Address, 1);
        regVal = _wire->read();
#else
        TinyWireM.beginTransmission(I2C_Address);
        TinyWireM.send(reg);
        TinyWireM.endTransmission();
        TinyWireM.requestFrom(I2C_Address, 1);
        regVal = TinyWireM.receive();
#endif

 

Just don’t forget to comment out #define USE_TINY_WIRE_M_ again when you change the microcontroller and want to change back to Wire.

By the way, I had initially tried to simply write #define USE_TINY_WIRE_M_ into the sketch. In the process I learned that the preprocessor does not proceed line by line when reading the sketch, but apparently reads the include files first. As a result, USE_TINY_WIRE_M_ was initially undefined, which led to corresponding error messages. Hence, the cumbersome solution with the configuration file.

And now for the library MCP23017_WE

The library MCP23017_WE controls the MCP23017 port expander, which provides you with additional 16 GPIOs. For details, check out my post on this component. For testing the control via ATtiny85 (in principle) the following circuit was used:

MCP23017 connected to the ATtiny85 for LED control
MCP23017 connected to the ATtiny85 for LED control

Being convenient, I used LED bars with common cathode and integrated resistors:

MCP23017 connected to the ATtiny85 in action
MCP23017 connected to the ATtiny85 in action

And here is the example sketch:

#include <TinyWireM.h>
#include <MCP23017.h>
#define MCP_ADDRESS 0x20 // (A2/A1/A0 = LOW)
#define RESET_PIN 4 

MCP23017 myMCP = MCP23017(MCP_ADDRESS, RESET_PIN);

int wT = 1000; // wT = waiting time

void setup(){ 
  TinyWireM.begin();
  myMCP.Init();
  myMCP.setPortMode(0b11111101, A);  // Port A: all pins are OUTPUT except pin 1
  myMCP.setPortMode(0b11111111, B);  // Port B: all pins are OUTPUT
  delay(wT);
  myMCP.setAllPins(A, ON); // alle LEDs switched on except A1
  delay(wT);
  myMCP.setPinX(1, A, OUTPUT, HIGH); // A1 switched on 
  delay(wT); 
  myMCP.setPort(0b11110000, B); // B4 - B7 switched on
  delay(wT);
  myMCP.setPort(0b01011110, A); // A0,A5,A7 switched off
  delay(wT);
  myMCP.setPinX(0,B,OUTPUT,HIGH); // B0 switched on
  delay(wT);
  myMCP.setPinX(4,B,OUTPUT,LOW); // B4 switched off
  delay(wT);
  myMCP.setAllPins(A, HIGH); // A0 - A7 all on
  delay(wT);
  myMCP.setPin(3, A, LOW); // A3 switched off
  delay(wT);
  myMCP.setPortX(0b11110000, 0b01101111,B); // at port B only B5,B6 are switched on
  delay(wT);
  myMCP.setPinMode(0,B,OUTPUT); // B0 --> OUTPUT
  for(int i=0; i<5; i++){  // B0 blinking
    myMCP.togglePin(0,B); 
    delay(200);
    myMCP.togglePin(0,B);
    delay(200);
  }
  for(int i=0; i<5; i++){ // B7 blinking
    myMCP.togglePin(7,B);
    delay(200);
    myMCP.togglePin(7,B);
    delay(200);
  }
}

void loop(){ 
} 

 

The memory requirement of the sketch is 2106 bytes, which is about 25 percent.

3. Controlling the INA219 current sensor

The library INA219_WE is used to controlling the current sensor INA219, which I have reported about in detail here.

INA219 modules

This is the circuit that was used:

INA219 and OLED display connected to the ATtiny85
INA219 and OLED display connected to the ATtiny85

The component “Load” stands for any consumer.

And here is the example sketch, which needs 72 percent of the program memory of the ATtiny85:

#include <TinyWireM.h>
#include <Tiny4kOLED.h>
#include <INA219_WE.h>
#define I2C_ADDRESS 0x40

uint8_t width = 128;
uint8_t height = 64;

/* There are several ways to create your INA219 object:
 * INA219_WE ina219 = INA219_WE()              -> uses I2C Address = 0x40
 * INA219_WE ina219 = INA219_WE(ICM20948_ADDR) -> define I2C_ADDRESS
 */
INA219_WE ina219 = INA219_WE(I2C_ADDRESS);

void setup() {
  TinyWireM.begin();
  oled.begin(width, height, sizeof(tiny4koled_init_128x64br), tiny4koled_init_128x64br);
  oled.setFont(FONT6X8);
  oled.clear();
  oled.on();
  
  oled.setCursor(0, 0);
  if(!ina219.init()){
     oled.print("INA219 not connected!");
     while(1){}
  }
  else{
    oled.print("INA219 connected");
    delay(1000);
    oled.clear();
  }

  /* Set ADC Mode for Bus and ShuntVoltage
  * Mode *            * Res / Samples *       * Conversion Time *
  BIT_MODE_9        9 Bit Resolution             84 µs
  BIT_MODE_10       10 Bit Resolution            148 µs  
  BIT_MODE_11       11 Bit Resolution            276 µs
  BIT_MODE_12       12 Bit Resolution            532 µs  (DEFAULT)
  SAMPLE_MODE_2     Mean Value 2 samples         1.06 ms
  SAMPLE_MODE_4     Mean Value 4 samples         2.13 ms
  SAMPLE_MODE_8     Mean Value 8 samples         4.26 ms
  SAMPLE_MODE_16    Mean Value 16 samples        8.51 ms     
  SAMPLE_MODE_32    Mean Value 32 samples        17.02 ms
  SAMPLE_MODE_64    Mean Value 64 samples        34.05 ms
  SAMPLE_MODE_128   Mean Value 128 samples       68.10 ms
  */
  //ina219.setADCMode(SAMPLE_MODE_128); // choose mode and uncomment for change of default
  
  /* Set measure mode
  POWER_DOWN - INA219 switched off
  TRIGGERED  - measurement on demand
  ADC_OFF    - Analog/Digital Converter switched off
  CONTINUOUS  - Continuous measurements (DEFAULT)
  */
  // ina219.setMeasureMode(CONTINUOUS); // choose mode and uncomment for change of default
  
  /* Set PGain
  * Gain *  * Shunt Voltage Range *   * Max Current (if shunt is 0.1 ohms) *
   PG_40       40 mV                    0.4 A
   PG_80       80 mV                    0.8 A
   PG_160      160 mV                   1.6 A
   PG_320      320 mV                   3.2 A (DEFAULT)
  */
  // ina219.setPGain(PG_320); // choose gain and uncomment for change of default
  
  /* Set Bus Voltage Range
   BRNG_16   -> 16 V
   BRNG_32   -> 32 V (DEFAULT)
  */
  // ina219.setBusRange(BRNG_32); // choose range and uncomment for change of default

  /* If the current values delivered by the INA219 differ by a constant factor
     from values obtained with calibrated equipment you can define a correction factor.
     Correction factor = current delivered from calibrated equipment / current delivered by INA219
  */
  // ina219.setCorrectionFactor(0.98); // insert your correction factor if necessary
  
  /* If you experience a shunt voltage offset, that means you detect a shunt voltage which is not 
     zero, although the current should be zero, you can apply a correction. For this, uncomment the 
     following function and apply the offset you have detected.   
  */
  // ina219.setShuntVoltOffset_mV(0.5); // insert the shunt voltage (millivolts) you detect at zero current 
}

void loop() {
  float shuntVoltage_mV = 0.0;
  float loadVoltage_V = 0.0;
  float busVoltage_V = 0.0;
  float current_mA = 0.0;
  float power_mW = 0.0; 
  bool ina219_overflow = false;
  
  shuntVoltage_mV = ina219.getShuntVoltage_mV();
  busVoltage_V = ina219.getBusVoltage_V();
  current_mA = ina219.getCurrent_mA();
  power_mW = ina219.getBusPower();
  //loadVoltage_V  = busVoltage_V + (shuntVoltage_mV/1000);
  //ina219_overflow = ina219.getOverflow();

  oled.setCursor(0,0);
  oled.print("Current [mA]: ");
  oled.print(current_mA);
  oled.clearToEOL();
  
  oled.setCursor(0,2); //10
  oled.print("Power [mW]:   ");
  oled.print(power_mW);
  oled.clearToEOL();
  
  oled.setCursor(0,4); 
  oled.print("Bus [V]:      ");
  oled.print(busVoltage_V);
  oled.clearToEOL();
  
  oled.setCursor(0,6); 
  oled.print("Shunt [mV]:   ");
  oled.print(shuntVoltage_mV);
  oled.clearToEOL();
  
  delay(3000);
}

 

Before you apply the sketch, you have to uncomment the line #define USE_TINY_WIRE_M_ in the file INA219_config.h again.

The output looked like this:

Output of INA219_attiny85.ino
Output of INA219_attiny85.ino

4. Controlling the ADS1115 A/D converter

Finally, I customized my ADS1115_WE   library, which can be used to control the ADS1115 16-bit, 4-channel A/D converter. I have reported about this nice component here.

The following circuit was used:

ADS1115 and OLED display connected to the ATtiny85
ADS1115 and OLED display connected to the ATtiny85

The four potentiometers represent the voltage inputs to be converted.

And that was my test sketch, which took up 73 percent of the program memory:

#include <TinyWireM.h>
#include <Tiny4kOLED.h>
#include<ADS1115_WE.h> 
#define ADS1115_I2C_ADDR 0x48
uint8_t width = 128;
uint8_t height = 64;

/* There are two ways to create your ADS1115_WE object:
 * ADS1115_WE adc = ADS1115_WE()             -> uses Wire / I2C Address = 0x48
 * ADS1115_WE adc = ADS1115_WE(I2C_ADDRESS)  -> uses Wire / I2C_ADDRESS
 */
ADS1115_WE adc = ADS1115_WE(ADS1115_I2C_ADDR);

void setup() {
  TinyWireM.begin();
  oled.begin(width, height, sizeof(tiny4koled_init_128x64br), tiny4koled_init_128x64br);
  oled.clear();
  oled.setFont(FONT6X8);
  oled.on();
  
  if(!adc.init()){
     oled.print("ADS1115 not connected");
     while(1){}
  }
  else{
    oled.print("ADS1115 connected");
    delay(1000);
    oled.clear();
  }

  /* Set the voltage range of the ADC to adjust the gain
   * Please note that you must not apply more than VDD + 0.3V to the input pins!
   * 
   * ADS1115_RANGE_6144  ->  +/- 6144 mV
   * ADS1115_RANGE_4096  ->  +/- 4096 mV
   * ADS1115_RANGE_2048  ->  +/- 2048 mV (default)
   * ADS1115_RANGE_1024  ->  +/- 1024 mV
   * ADS1115_RANGE_0512  ->  +/- 512 mV
   * ADS1115_RANGE_0256  ->  +/- 256 mV
   */
  adc.setVoltageRange_mV(ADS1115_RANGE_6144); //comment line/change parameter to change range

  /* Set the inputs to be compared
   *  
   *  ADS1115_COMP_0_1    ->  compares 0 with 1 (default)
   *  ADS1115_COMP_0_3    ->  compares 0 with 3
   *  ADS1115_COMP_1_3    ->  compares 1 with 3
   *  ADS1115_COMP_2_3    ->  compares 2 with 3
   *  ADS1115_COMP_0_GND  ->  compares 0 with GND
   *  ADS1115_COMP_1_GND  ->  compares 1 with GND
   *  ADS1115_COMP_2_GND  ->  compares 2 with GND
   *  ADS1115_COMP_3_GND  ->  compares 3 with GND
   */
  adc.setCompareChannels(ADS1115_COMP_0_GND); //comment line/change parameter to change channel

  /* Set number of conversions after which the alert pin asserts
   * - or you can disable the alert 
   *  
   *  ADS1115_ASSERT_AFTER_1  -> after 1 conversion
   *  ADS1115_ASSERT_AFTER_2  -> after 2 conversions
   *  ADS1115_ASSERT_AFTER_4  -> after 4 conversions
   *  ADS1115_DISABLE_ALERT   -> disable comparator / alert pin (default) 
   */
  //adc.setAlertPinMode(ADS1115_ASSERT_AFTER_1); //uncomment if you want to change the default

  /* Set the conversion rate in SPS (samples per second)
   * Options should be self-explaining: 
   * 
   *  ADS1115_8_SPS 
   *  ADS1115_16_SPS  
   *  ADS1115_32_SPS 
   *  ADS1115_64_SPS  
   *  ADS1115_128_SPS (default)
   *  ADS1115_250_SPS 
   *  ADS1115_475_SPS 
   *  ADS1115_860_SPS 
   */
  // adc.setConvRate(ADS1115_8_SPS); //uncomment if you want to change the default

  /* Set continuous or single shot mode:
   * 
   *  ADS1115_CONTINUOUS  ->  continuous mode
   *  ADS1115_SINGLE     ->  single shot mode (default)
   */
  adc.setMeasureMode(ADS1115_CONTINUOUS); //comment line/change parameter to change mode

   /* Choose maximum limit or maximum and minimum alert limit (window) in Volt - alert pin will 
   *  assert when measured values are beyond the maximum limit or outside the window 
   *  Upper limit first: setAlertLimit_V(MODE, maximum, minimum)
   *  In max limit mode the minimum value is the limit where the alert pin assertion will be  
   *  cleared (if not latched)  
   * 
   *  ADS1115_MAX_LIMIT
   *  ADS1115_WINDOW
   * 
   */
  //adc.setAlertModeAndLimit_V(ADS1115_MAX_LIMIT, 3.0, 1.5); //uncomment if you want to change the default
  
  /* Enable or disable latch. If latch is enabled the alert pin will assert until the
   * conversion register is read (getResult functions). If disabled the alert pin assertion will be
   * cleared with next value within limits. 
   *  
   *  ADS1115_LATCH_DISABLED (default)
   *  ADS1115_LATCH_ENABLED
   */
  //adc.setAlertLatch(ADS1115_LATCH_ENABLED); //uncomment if you want to change the default

  /* Sets the alert pin polarity if active:
   *  
   * ADS1115_ACT_LOW  ->  active low (default)   
   * ADS1115_ACT_HIGH ->  active high
   */
  //adc.setAlertPol(ADS1115_ACT_LOW); //uncomment if you want to change the default
 
  /* With this function the alert pin will assert, when a conversion is ready.
   * In order to deactivate, use the setAlertLimit_V function  
   */
  //adc.setAlertPinToConversionReady(); //uncomment if you want to change the default
}

  /* If you change the compare channels you can immediately read values from the conversion 
   * register, although they might belong to the former channel if no precautions are taken. 
   * It takes about the time needed for two conversions to get the correct data. In single 
   * shot mode you can use the isBusy() function to wait for data from the new channel. This 
   * does not work in continuous mode. 
   * To solve this issue the library adds a delay after change of channels if you are in contunuous
   * mode. The length of the delay is adjusted to the conversion rate. But be aware that the output 
   * rate will be much lower that the conversion rate if you change channels frequently. 
   */

void loop() {
  float voltage = 0.0;

  adc.setCompareChannels(ADS1115_COMP_0_GND);
  voltage = adc.getResult_V();
  oled.setCursor(0,0);
  oled.print("Channel 0 [V]: ");
  oled.print(voltage);
  oled.clearToEOL();

  adc.setCompareChannels(ADS1115_COMP_1_GND);
  voltage = adc.getResult_V();
  oled.setCursor(0,2);
  oled.print("Channel 1 [V]: ");
  oled.print(voltage);
  oled.clearToEOL();

  adc.setCompareChannels(ADS1115_COMP_2_GND);
  voltage = adc.getResult_V();
  oled.setCursor(0,4);
  oled.print("Channel 2 [V]: ");
  oled.print(voltage);
  oled.clearToEOL();

  adc.setCompareChannels(ADS1115_COMP_3_GND);
  voltage = adc.getResult_V();
  oled.setCursor(0,6);
  oled.print("Channel 3 [V]: ");
  oled.print(voltage);
  oled.clearToEOL();
  
  delay(2000);
}

 

Here is the output on the OLED display:

Output of ADS1115_attiny85.ino

Making libraries TinyWireM compatible

You want to use TinyWireM, but the library of your I2C device is incompatible with TinyWireM? Then adjust it! I will give two examples of this.

I would like to say in advance that this is not as easy with every library as described here. Some libraries are more complex, so that larger interventions are necessary, others are simply too large and do not fit into the 8 kByte program memory.  

And, of course, there is a disadvantage: if the author of the library releases an update, you will have to stay with the old version or adapt the new version again.

I2C with the ATtiny85 – MPU6050

For the first example, I come back to the MPU6050. I have chosen the MPU6050_light library from rfetick as the demonstration object.

The relevant library files are MPU6050_light.h and MPU6050_light.cpp. First, we turn to the header file (“.h”).

But first the link to the license, which must be referred to if you publish the library in whole or in part. And that’s what I’m doing here.

Modifying MPU6050_light.h

For editing, it is best to use a file editor that highlights program code. I recommend the free Notepad++. Use the editor to open the header file and search for “Wire” and “TwoWire”. Wire is an object of the TwoWire class. All lines that contain these words need your attention.

In this case, the wire object is passed to the MPU6050 object as a reference (&w). You can recognize this by the line MPU6050(TwoWire &w);. We change this procedure and work without handover. Sounds complicated? But it is not. Only a few changes are necessary. I marked them with /////// Take out!!!! and with /////// Add !!!!:

#ifndef MPU6050_LIGHT_H
#define MPU6050_LIGHT_H

#include "Arduino.h"
/////// #include "Wire.h" /////// Take out!!!!
#include<TinyWireM.h>  /////// Add!!!!

#define MPU6050_ADDR                  0x68
#define MPU6050_SMPLRT_DIV_REGISTER   0x19
#define MPU6050_CONFIG_REGISTER       0x1a
#define MPU6050_GYRO_CONFIG_REGISTER  0x1b
#define MPU6050_ACCEL_CONFIG_REGISTER 0x1c
#define MPU6050_PWR_MGMT_1_REGISTER   0x6b

#define MPU6050_GYRO_OUT_REGISTER     0x43
#define MPU6050_ACCEL_OUT_REGISTER    0x3B

#define RAD_2_DEG             57.29578 // [deg/rad]
#define CALIB_OFFSET_NB_MES   500
#define TEMP_LSB_2_DEGREE     340.0    // [bit/celsius]
#define TEMP_LSB_OFFSET       12412.0

#define DEFAULT_GYRO_COEFF    0.98

class MPU6050{
  public:
    // INIT and BASIC FUNCTIONS
  /////// MPU6050(TwoWire &w); Take out !!!!
  MPU6050(); /////// Add!!!!
    byte begin(int gyro_config_num=1, int acc_config_num=0);
  
  byte writeData(byte reg, byte data);
    byte readData(byte reg);
  
  void calcOffsets(bool is_calc_gyro=true, bool is_calc_acc=true);
  void calcGyroOffsets(){ calcOffsets(true,false); }; // retro-compatibility with v1.0.0
  void calcAccOffsets(){ calcOffsets(false,true); }; // retro-compatibility with v1.0.0
  
  void setAddress(uint8_t addr){ address = addr; };
  uint8_t getAddress(){ return address; };
  
  // MPU CONFIG SETTER
  byte setGyroConfig(int config_num);
  byte setAccConfig(int config_num);
  
    void setGyroOffsets(float x, float y, float z);
  void setAccOffsets(float x, float y, float z);
  
  void setFilterGyroCoef(float gyro_coeff);
  void setFilterAccCoef(float acc_coeff);

  // MPU CONFIG GETTER
  float getGyroXoffset(){ return gyroXoffset; };
    float getGyroYoffset(){ return gyroYoffset; };
    float getGyroZoffset(){ return gyroZoffset; };
  
  float getAccXoffset(){ return accXoffset; };
  float getAccYoffset(){ return accYoffset; };
  float getAccZoffset(){ return accZoffset; };
  
  float getFilterGyroCoef(){ return filterGyroCoef; };
  float getFilterAccCoef(){ return 1.0-filterGyroCoef; };
  
  // DATA GETTER
    float getTemp(){ return temp; };

    float getAccX(){ return accX; };
    float getAccY(){ return accY; };
    float getAccZ(){ return accZ; };

    float getGyroX(){ return gyroX; };
    float getGyroY(){ return gyroY; };
    float getGyroZ(){ return gyroZ; };
  
  float getAccAngleX(){ return angleAccX; };
    float getAccAngleY(){ return angleAccY; };

    float getAngleX(){ return angleX; };
    float getAngleY(){ return angleY; };
    float getAngleZ(){ return angleZ; };

  // INLOOP UPDATE
  void fetchData(); // user should better call 'update' that includes 'fetchData'
    void update();
  
  // UPSIDE DOWN MOUNTING
  bool upsideDownMounting = false;


  private:
    /////// TwoWire *wire;  /////// Take out!!!!
  uint8_t address = MPU6050_ADDR; // 0x68 or 0x69
  float gyro_lsb_to_degsec, acc_lsb_to_g;
    float gyroXoffset, gyroYoffset, gyroZoffset;
  float accXoffset, accYoffset, accZoffset;
    float temp, accX, accY, accZ, gyroX, gyroY, gyroZ;
    float angleAccX, angleAccY;
    float angleX, angleY, angleZ;
    long preInterval;
    float filterGyroCoef; // complementary filter coefficient to balance gyro vs accelero data to get angle
};

#endif

 

Modifying MPU6050_light.cpp

Then we modify the “.cpp” file. Again, we need to look for “Wire” and “TwoWire”. As already mentioned, Wire is passed as a reference in the original version of the library. The reference is then assigned to the variable wire with wire = &w. Since this is a reference, the arrow operator is used here instead of the point operator, e.g. wire->beginTransmission() instead of the perhaps more familiar Wire.beginTransmission(). We replace all of that with the TinyWireM counterparts.

#include "MPU6050_light.h"
#include "Arduino.h"

/* Wrap an angle in the range [-limit,+limit] (special thanks to Edgar Bonet!) */
static float wrap(float angle,float limit){
  while (angle >  limit) angle -= 2*limit;
  while (angle < -limit) angle += 2*limit;
  return angle;
}

/* INIT and BASIC FUNCTIONS */

/////// MPU6050::MPU6050(TwoWire &w){ ///////Take out!!!!
///////  wire = &w; /////// Take out!!!!
MPU6050::MPU6050(){  /////// Add
  setFilterGyroCoef(DEFAULT_GYRO_COEFF);
  setGyroOffsets(0,0,0);
  setAccOffsets(0,0,0);
}

byte MPU6050::begin(int gyro_config_num, int acc_config_num){
  // changed calling register sequence [https://github.com/rfetick/MPU6050_light/issues/1] -> thanks to augustosc
  byte status = writeData(MPU6050_PWR_MGMT_1_REGISTER, 0x01); // check only the first connection with status
  writeData(MPU6050_SMPLRT_DIV_REGISTER, 0x00);
  writeData(MPU6050_CONFIG_REGISTER, 0x00);
  setGyroConfig(gyro_config_num);
  setAccConfig(acc_config_num);
  
  this->update();
  angleX = this->getAccAngleX();
  angleY = this->getAccAngleY();
  preInterval = millis(); // may cause lack of angular accuracy if begin() is much before the first update()
  return status;
}

byte MPU6050::writeData(byte reg, byte data){
///////  wire->beginTransmission(address);/////// Take out!!!!
///////  wire->write(reg);/////// Take out!!!!
///////  wire->write(data);/////// Take out!!!!
///////  byte status = wire->endTransmission();/////// Take out!!!!
  TinyWireM.beginTransmission(address);/////// Add
  TinyWireM.send(reg);/////// Add
  TinyWireM.send(data);/////// Add
  byte status = TinyWireM.endTransmission();/////// Add
  return status; // 0 if success
}

// This method is not used internaly, maybe by user...
byte MPU6050::readData(byte reg) {
///////  wire->beginTransmission(address);/////// Take out!!!!
///////  wire->write(reg);/////// Take out!!!!
///////  wire->endTransmission(true);/////// Take out!!!!
///////  wire->requestFrom(address,(uint8_t) 1);/////// Take out!!!!
///////  byte data =  wire->read();/////// Take out!!!!
  TinyWireM.beginTransmission(address);/////// Add
  TinyWireM.send(reg);/////// Add
  TinyWireM.endTransmission(true);/////// Add
  TinyWireM.requestFrom(address,(uint8_t) 1);/////// Add
  byte data =  TinyWireM.receive();/////// Add
  return data;
}

/* SETTER */

byte MPU6050::setGyroConfig(int config_num){
  byte status;
  switch(config_num){
    case 0: // range = +- 250 deg/s
    gyro_lsb_to_degsec = 131.0;
    status = writeData(MPU6050_GYRO_CONFIG_REGISTER, 0x00);
    break;
  case 1: // range = +- 500 deg/s
    gyro_lsb_to_degsec = 65.5;
    status = writeData(MPU6050_GYRO_CONFIG_REGISTER, 0x08);
    break;
  case 2: // range = +- 1000 deg/s
    gyro_lsb_to_degsec = 32.8;
    status = writeData(MPU6050_GYRO_CONFIG_REGISTER, 0x10);
    break;
  case 3: // range = +- 2000 deg/s
    gyro_lsb_to_degsec = 16.4;
    status = writeData(MPU6050_GYRO_CONFIG_REGISTER, 0x18);
    break;
  default: // error
    status = 1;
    break;
  }
  return status;
}

byte MPU6050::setAccConfig(int config_num){
  byte status;
  switch(config_num){
    case 0: // range = +- 2 g
    acc_lsb_to_g = 16384.0;
    status = writeData(MPU6050_ACCEL_CONFIG_REGISTER, 0x00);
    break;
  case 1: // range = +- 4 g
    acc_lsb_to_g = 8192.0;
    status = writeData(MPU6050_ACCEL_CONFIG_REGISTER, 0x08);
    break;
  case 2: // range = +- 8 g
    acc_lsb_to_g = 4096.0;
    status = writeData(MPU6050_ACCEL_CONFIG_REGISTER, 0x10);
    break;
  case 3: // range = +- 16 g
    acc_lsb_to_g = 2048.0;
    status = writeData(MPU6050_ACCEL_CONFIG_REGISTER, 0x18);
    break;
  default: // error
    status = 1;
    break;
  }
  return status;
}

void MPU6050::setGyroOffsets(float x, float y, float z){
  gyroXoffset = x;
  gyroYoffset = y;
  gyroZoffset = z;
}

void MPU6050::setAccOffsets(float x, float y, float z){
  accXoffset = x;
  accYoffset = y;
  accZoffset = z;
}

void MPU6050::setFilterGyroCoef(float gyro_coeff){
  if ((gyro_coeff<0) or (gyro_coeff>1)){ gyro_coeff = DEFAULT_GYRO_COEFF; } // prevent bad gyro coeff, should throw an error...
  filterGyroCoef = gyro_coeff;
}

void MPU6050::setFilterAccCoef(float acc_coeff){
  setFilterGyroCoef(1.0-acc_coeff);
}

/* CALC OFFSET */

void MPU6050::calcOffsets(bool is_calc_gyro, bool is_calc_acc){
  if(is_calc_gyro){ setGyroOffsets(0,0,0); }
  if(is_calc_acc){ setAccOffsets(0,0,0); }
  float ag[6] = {0,0,0,0,0,0}; // 3*acc, 3*gyro
  
  for(int i = 0; i < CALIB_OFFSET_NB_MES; i++){
    this->fetchData();
  ag[0] += accX;
  ag[1] += accY;
  ag[2] += (accZ-1.0);
  ag[3] += gyroX;
  ag[4] += gyroY;
  ag[5] += gyroZ;
  delay(1); // wait a little bit between 2 measurements
  }
  
  if(is_calc_acc){
    accXoffset = ag[0] / CALIB_OFFSET_NB_MES;
    accYoffset = ag[1] / CALIB_OFFSET_NB_MES;
    accZoffset = ag[2] / CALIB_OFFSET_NB_MES;
  }
  
  if(is_calc_gyro){
    gyroXoffset = ag[3] / CALIB_OFFSET_NB_MES;
    gyroYoffset = ag[4] / CALIB_OFFSET_NB_MES;
    gyroZoffset = ag[5] / CALIB_OFFSET_NB_MES;
  }
}

/* UPDATE */

void MPU6050::fetchData(){
///////  wire->beginTransmission(address); /////// Take out!!!!
///////  wire->write(MPU6050_ACCEL_OUT_REGISTER); /////// Take out!!!!
///////  wire->endTransmission(false); /////// Take out!!!!
///////  wire->requestFrom(address,(uint8_t) 14); /////// Take out!!!!
  TinyWireM.beginTransmission(address); /////// Add!!!!
  TinyWireM.send(MPU6050_ACCEL_OUT_REGISTER); /////// Add!!!!
  TinyWireM.endTransmission(false); /////// Add!!!!
  TinyWireM.requestFrom(address,(uint8_t) 14); /////// Add!!!!

  int16_t rawData[7]; // [ax,ay,az,temp,gx,gy,gz]

  for(int i=0;i<7;i++){
///////	rawData[i]  = wire->read() << 8; /////// Take out!!!!
///////    rawData[i] |= wire->read();   /////// Take out!!!!
    rawData[i]  = TinyWireM.receive() << 8; /////// Add!!!!
    rawData[i] |= TinyWireM.receive();   /////// Add!!!!
  }

  accX = ((float)rawData[0]) / acc_lsb_to_g - accXoffset;
  accY = ((float)rawData[1]) / acc_lsb_to_g - accYoffset;
  accZ = (!upsideDownMounting - upsideDownMounting) * ((float)rawData[2]) / acc_lsb_to_g - accZoffset;
  temp = (rawData[3] + TEMP_LSB_OFFSET) / TEMP_LSB_2_DEGREE;
  gyroX = ((float)rawData[4]) / gyro_lsb_to_degsec - gyroXoffset;
  gyroY = ((float)rawData[5]) / gyro_lsb_to_degsec - gyroYoffset;
  gyroZ = ((float)rawData[6]) / gyro_lsb_to_degsec - gyroZoffset;
}

void MPU6050::update(){
  // retrieve raw data
  this->fetchData();
  
  // estimate tilt angles: this is an approximation for small angles!
  float sgZ = (accZ>=0)-(accZ<0); // allow one angle to go from -180 to +180 degrees
  angleAccX =   atan2(accY, sgZ*sqrt(accZ*accZ + accX*accX)) * RAD_2_DEG; // [-180,+180] deg
  angleAccY = - atan2(accX,     sqrt(accZ*accZ + accY*accY)) * RAD_2_DEG; // [- 90,+ 90] deg

  unsigned long Tnew = millis();
  float dt = (Tnew - preInterval) * 1e-3;
  preInterval = Tnew;

  // Correctly wrap X and Y angles (special thanks to Edgar Bonet!)
  // https://github.com/gabriel-milan/TinyMPU6050/issues/6
  angleX = wrap(filterGyroCoef*(angleAccX + wrap(angleX +     gyroX*dt - angleAccX,180)) + (1.0-filterGyroCoef)*angleAccX,180);
  angleY = wrap(filterGyroCoef*(angleAccY + wrap(angleY + sgZ*gyroY*dt - angleAccY, 90)) + (1.0-filterGyroCoef)*angleAccY, 90);
  angleZ += gyroZ*dt; // not wrapped (to do???)
}

 

Example sketch

Now we need another sketch to test. For this, I have modified one of the example sketches:

#include <TinyWireM.h>
#include <Tiny4kOLED.h>
#include <MPU6050_light.h>

MPU6050 mpu = MPU6050();
uint8_t width = 128;
uint8_t height = 64;

void setup() {
  TinyWireM.begin();
  oled.begin(width, height, sizeof(tiny4koled_init_128x64br), tiny4koled_init_128x64br);
  oled.clear();
  oled.on();
  oled.setFont(FONT6X8);
  
  byte status = mpu.begin();
  while(status!=0){ } // stop everything if could not connect to MPU6050
  oled.setCursor(0,2);
  oled.print("Don't move!");
  delay(1000);
  mpu.calcOffsets(true,true); // gyro and accelero
  oled.clear();
}

void loop() {
  mpu.update();
  oled.setCursor(0,0); oled.print("AcX = "); oled.print(mpu.getAccX()); oled.clearToEOL();
  oled.setCursor(0,1); oled.print("AcY = "); oled.print(mpu.getAccY()); oled.clearToEOL();
  oled.setCursor(0,2); oled.print("AcZ = "); oled.print(mpu.getAccZ()); oled.clearToEOL();
  oled.setCursor(0,3); oled.print("tmp = "); oled.print(mpu.getTemp()); oled.clearToEOL();
  oled.setCursor(0,4); oled.print("GyX = "); oled.print(mpu.getGyroX()); oled.clearToEOL();
  oled.setCursor(0,5); oled.print("GyY = "); oled.print(mpu.getGyroY()); oled.clearToEOL();
  oled.setCursor(0,6); oled.print("GyZ = "); oled.print(mpu.getGyroZ()); oled.clearToEOL();
  delay(2000);
}

 

I had to limit the sketch to the most necessary because it was getting tight on the ATtiny85:

Memory requirements of MPU6050_light_attiny85.ino
Memory requirements of MPU6050_light_attiny85.ino

It would not work that way with Wire instead of TinyWireM. The available memory would not be enough.

And here is the output:

Output of MPU6050_light_attiny85.ino
Output of MPU6050_light_attiny85.ino

I2C with the ATtiny85 – SHT31 temperature and humidity sensor

The second library I modified is Grove_SHT31_Temp_Humi_Sensor from Seeed Studio, version 1.0.0. It is used to control a SHT31 temperature and humidity sensor.

Here, too, the reference to the license.

SHT31 module
SHT31 module

Modifying SHT31.h

Modifying the header file is even easier because the wire object is not passed in this library. You just have to replace #include "Wire.h" with #include<TinyWireM.h> in SHT31.h.

#ifndef _SHT31_H_
#define _SHT31_H_

#include "Arduino.h"
//////// #include "Wire.h" /////// take out
#include<TinyWireM.h> /////// Add!

#define SHT31_ADDR    0x44
#define SHT31_MEAS_HIGHREP_STRETCH 0x2C06
#define SHT31_MEAS_MEDREP_STRETCH  0x2C0D
#define SHT31_MEAS_LOWREP_STRETCH  0x2C10
#define SHT31_MEAS_HIGHREP         0x2400
#define SHT31_MEAS_MEDREP          0x240B
#define SHT31_MEAS_LOWREP          0x2416
#define SHT31_READSTATUS           0xF32D
#define SHT31_CLEARSTATUS          0x3041
#define SHT31_SOFTRESET            0x30A2
#define SHT31_HEATEREN             0x306D
#define SHT31_HEATERDIS            0x3066

class SHT31 {
  public:
    SHT31();    
    boolean begin(uint8_t i2caddr = SHT31_ADDR);
    float getTemperature(void);
    float getHumidity(void);
    uint16_t readStatus(void);
    void reset(void);
    void heater(boolean);
    uint8_t crc8(const uint8_t *data, int len);

  private:
    boolean getTempHum(void);
    void writeCommand(uint16_t cmd);

    uint8_t _i2caddr;
    float humidity, temp;
    
};

#endif

 

Modifying SHT31.cpp

There is a little more to change in SHT31.cpp, but it is not complicated. Keep an eye out for “Wire” again.

#include "SHT31.h"

SHT31::SHT31() {
}

boolean SHT31::begin(uint8_t i2caddr) {
  /////// Wire.begin(); //////// Take out!!!!
  //TinyWireM.begin(); -> not necessary to put in!
  _i2caddr = i2caddr;
  reset();
  /////// return (readStatus() == 0x40); /////// Take out!!!!
  return true; //////// Add!!!!
}

float SHT31::getTemperature(void) {
  if (! getTempHum()) return NAN;
  return temp;
}


float SHT31::getHumidity(void) {
  if (! getTempHum()) return NAN;
  return humidity;
}

uint16_t SHT31::readStatus(void) {
  return 42; /////// I added this line. Without this, it compiles 
             /////// with an Arduino Nano, but not with an ATtiny85
}

void SHT31::reset(void) {
  writeCommand(SHT31_SOFTRESET);
  delay(10);
}

void SHT31::heater(boolean h) {
  if (h)
    writeCommand(SHT31_HEATEREN);
  else
    writeCommand(SHT31_HEATERDIS);
}

uint8_t SHT31::crc8(const uint8_t *data, int len) {
  const uint8_t POLYNOMIAL(0x31);
  uint8_t crc(0xFF);
  
  for ( int j = len; j; --j ) {
      crc ^= *data++;

      for ( int i = 8; i; --i ) {
  crc = ( crc & 0x80 )
    ? (crc << 1) ^ POLYNOMIAL
    : (crc << 1);
      }
  }
  return crc; 
}


boolean SHT31::getTempHum(void) {
  uint8_t readbuffer[6];

  writeCommand(SHT31_MEAS_HIGHREP);
  
  delay(50);
  /////// Wire.requestFrom(_i2caddr, (uint8_t)6); /////// Take out!!!!
  TinyWireM.requestFrom(_i2caddr,6); //////// Add!!!!
  //if (Wire.available() != 6) /////// Take out!!!! 
  if (TinyWireM.available() != 6) 
    return false;
  for (uint8_t i=0; i<6; i++) {
    //readbuffer[i] = Wire.read(); /////// Take out!!!!
  readbuffer[i] = TinyWireM.receive(); /////// Add!!!!
  }
  uint16_t ST, SRH;
  ST = readbuffer[0];
  ST <<= 8;
  ST |= readbuffer[1];

  if (readbuffer[2] != crc8(readbuffer, 2)) return false;

  SRH = readbuffer[3];
  SRH <<= 8;
  SRH |= readbuffer[4];

  if (readbuffer[5] != crc8(readbuffer+3, 2)) return false;
 
  double stemp = ST;
  stemp *= 175;
  stemp /= 0xffff;
  stemp = -45 + stemp;
  temp = stemp;
  
  double shum = SRH;
  shum *= 100;
  shum /= 0xFFFF;
  
  humidity = shum;
  
  return true;
}

void SHT31::writeCommand(uint16_t cmd) {
  /* Wire.beginTransmission(_i2caddr); /////// Take out 4 lines !!!!
  Wire.write(cmd >> 8);
  Wire.write(cmd & 0xFF);
  Wire.endTransmission();     */  
  TinyWireM.beginTransmission(_i2caddr); /////// Add the following lines!!!!
  TinyWireM.send((uint8_t)(cmd >> 8));
  TinyWireM.send((uint8_t)(cmd & 0xFF));
  TinyWireM.endTransmission();
}

 

Sketch sht31

Then there is still a test sketch to create, which is not difficult. To do this, simply find the relevant functions from the example sketch. The result could then look like this:

#include <TinyWireM.h>
#include <Tiny4kOLED.h>
#include <SHT31.h>

uint8_t width = 128;
uint8_t height = 64;

SHT31 sht31 = SHT31();

void setup() {
  TinyWireM.begin();
  oled.begin(width, height, sizeof(tiny4koled_init_128x64br), tiny4koled_init_128x64br);
  oled.setFont(FONT6X8);
  oled.clear();
  oled.on();
  
  oled.setCursor(0, 0);
  sht31.begin();
  sht31.heater(false); // heater off, with "true" it's on -> can remove condensated water, 
                       // but gives false temperature values (about 3 degrees too high);
}

void loop() {
  float temp = sht31.getTemperature();
  float hum = sht31.getHumidity();
  
  oled.setCursor(0,0);
  oled.print("Temp.   [*C]: ");
  oled.print(temp);
  oled.clearToEOL();
  
  oled.setCursor(0,2); //10
  oled.print("Humidity [%]: ");
  oled.print(hum);
  oled.clearToEOL();
  
  delay(2000);
}

 

By the way, this sketch requires only 62% of the program memory. And here is the output:

Output of sht31_attiny85.ino
Output of sht31_attiny85.ino

Acknowledgement

I would like to thank the authors of the libraries TinyWireM, Tiny4kOLED, MPU6050_light and Grove_SHT31_Temp_Humi_Sensor.

10 thoughts on “TinyWireM vs. Wire for ATtinys

  1. is there a possibility to access to I2C address of ssd1306 128×64 display under libraries TinyWireM, and tiny4koled.
    Thanks

    1. is there a possibility to access to I2C address of ssd1306 128×64 display under libraries TinyWireM, and tiny4koled.
      Thanks

        1. I connected an ATtiny85 to SSD Oled 128×32 with the following pins :
          Gnd, Vcc, D0, D1, Res, DC, and CS .
          I2c communication was configured by moving R3 to R1 , and R8 shorten.
          The ATtinyCore package was installed via Ide 1.8
          libraries TinyWireM, and Tiny4koled installed
          PB0 –>D1
          PB2–>CLK
          Res –> Vcc
          DC , and CS –> Gnd
          in such configuration is the address (0x3C) still always correct?

  2. Another excellent article that stimulates thoughts of how the ATtiny devices could be used. I already use your MCP23017 and INA219 libraries on my ESP projects and I find they work really well. I use platformIO to build my projects. I am aout to use your MCP23017 library with an ESP-Now project in which an ESP32 will be a bridge to MQTT and five ESP8266 devices communicate with the bridge with bi-directional control. The bridge does ESP-Now and WiFi simulataneously.
    Keep up the great work you do, I really like the style of your writing and how well you describe the subject material.

Leave a Reply

Your email address will not be published. Required fields are marked *