Help Center
< All Topics
Print

Transmitting BME680 sensor readings to an LCD display using a 2 node CAN network

Tutorial Aim:

The aim of this project is to develop a robust environmental monitoring system using two Arduino microcontrollers connected via CAN bus. The system will consist of:

  1. Transmitter Unit: An Arduino equipped with a BME680 sensor and an MCP2515 CAN bus module. The BME680 sensor will measure environmental data including temperature, humidity, pressure, and air quality. This data will be transmitted over the CAN bus.
  2. Receiver Unit: An Arduino connected to a 20x4 LCD display and an MCP2515 CAN bus module. This unit will receive the environmental data from the transmitter unit via the CAN bus and display the real-time readings on the LCD screen.

This project aims to demonstrate the effective use of CAN bus for reliable, real-time data transmission in a sensor network, highlighting the advantages of using a message-based communication protocol for environmental monitoring applications.

Introduction:

The Controller Area Network (CAN bus) is a communication protocol that allows microcontrollers and devices to communicate with each other without a central host computer. It's especially popular in automotive and industrial applications, but you can also use it with Arduino for various projects.

Key Concepts:
  1. Communication Protocol: CAN bus is a way for different parts of a system to send messages to each other. It’s like a chat room where everyone can talk, but only one person (or device) talks at a time.
  2. Differential Signaling: CAN bus uses two wires, CAN_H (high) and CAN_L (low), to send signals. This helps reduce noise and ensures reliable data transmission even in noisy environments.
  3. Message-Based: Instead of sending data to specific addresses, CAN bus messages have identifiers. These identifiers indicate the priority and type of data. Each device (or node) listens to all messages and processes only those that are relevant.

The modules used in this tutorial communicate using the CAN 2.0B protocol

Requirements:

This tutorial makes use of the Arduino IDE 2.3

  • Arduino IDE
  • 2 x Arduino (Compatible) UNO R3 with USB cable
  • BME680 Environmental Sensor Module
  • Breadboard
  • 18 x FM Jumper Wires
  • 6 x MM Jumper Wires
  • LCD 2004 (20X4) – I2C – Blue Display
  • 2 x MCP2515 CAN Bus Module with TJA1050 Receiver

Pin Layout:

Sender Pinout:

This pinout is for the sending/transmitting Arduino. This will have the MCP2515 CANBUS module and MBE680 sensor module and will transmit the sensor values.

Receiver with MCP2515:
ArduinoMCP2515
5VVCC
GNDGND
2INT
10CS
11SI
12SO
13SCK
Sender with BME680 Sensor:
ArduinoBME680
3V3VCC
GNDGND
A4SDA
A5SCL

Receiver Pinout:

This pinout is for the receiving Arduino. This will have the MCP2515 CANBUS module and LCD to display the transmitted data.

Receiver with MCP2515:
ArduinoMCP2515
5VVCC
GNDGND
2INT
10CS
11SI
12SO
13SCK
Sender with LCD Display:
ArduinoLCD Display
3V3VCC
GNDGND
A4SDA
A5SCL

Wiring diagram:

Wire the components according to the circuit diagram shown above.

Click to expand

Code Walk Through:

For the sensor data to be transmitted & received, we'll need to code one Arduino UNO to be the transmitter, and the other to be the receiver. This code is provided in the following 2 sections.

CAN Transmitter Code Walk Through:

Libraries & Variables:

To access & use the BME680 sensor we include the Adafruit_BME680.h library by Adafruit. As the sensor uses I2C protocol to communicate with microcontrollers the Wire.h library is also needed. For information about the Adafruit_BME680.h library: https://github.com/adafruit/Adafruit_BME680

For the CAN network we use the CAN.h library by Sandeep Misty. For more information about the CAN.h: https://github.com/sandeepmistry/arduino-CAN

We define each readings message id so that we can distinguish between transmitted readings. We also create global variables to store the sensor readings for easier access and to safeguard against invalid values caused by failed sensor readings.

#include <Wire.h>
#include "Adafruit_BME680.h"
#include "string.h"
#include <CAN.h>

#define SEALEVELPRESSURE_HPA (1013.25)

/* CAN Bus packet ids */
#define TEMP_ID 0x1
#define PRESSURE_ID 0x2
#define HUMIDITY_ID 0x3
#define GAS_ID 0x4

Adafruit_BME680 bme;

// BME680 sensor readings
String temp;
String pressure;
String humidity;
String gas;
Sending A Message:

A function that iteratively sends a string message character by character with the given id. Please note that the CAN network can only transmit messages of size 8 bytes or less.

/* Transmits a packet with the given messsage and id */
void sendMsg(int id, int size, String msg) {
  // Packet info
  Serial.print("size = ");
  Serial.println(size);
  Serial.print("Sending ");
  Serial.print(msg);
  Serial.print(" ...");

  // Creating & transmitting the packet
  CAN.beginPacket(id);
  for(int i = 0; i < size; i++) {
    Serial.print(msg[i]);
    CAN.write(msg[i]);
  }
  CAN.endPacket();

  Serial.println(" done!");
  delay(2000);
}
Gathering the sensor data:

We can gather and the sensor's temperature, pressure, humidity & gas resistance readings and store them in the global variables created above. We safeguard the readings against invalid values by checking if the BME680 was able to perform readings. Therefore, if the BME680 is not able to perform readings, the sensor values are unchanged and transmitted again.

/* Updates the BME680 sensor readings */
void getReadings() {

  // Checking for valid readings
  if (! bme.performReading()) {
    Serial.println("Failed to perform reading :(");
    return;
  }

  temp = String(bme.temperature);
  pressure = String(bme.pressure / 100.0);
  humidity = String(bme.humidity);
  gas = String(bme.gas_resistance / 1000.0);
}
Transmitting the sensor data:

Sends all the sensor data with their corresponding ID's in a batch transmission.

/* Sensor reading batch transmission */
void transmitReadings() {
  sendMsg(TEMP_ID, strlen(temp.c_str()), temp);
  sendMsg(PRESSURE_ID, strlen(pressure.c_str()), pressure);
  sendMsg(HUMIDITY_ID, strlen(humidity.c_str()), humidity);
  sendMsg(GAS_ID, strlen(gas.c_str()), gas);
}
Initializing the CAN module & BME680 sensor:

We check if the BME680 was successfully initialized. Similarly, we use the CAN.h library to test if the CAN module was initialized to 500kbps. 

void setup() {
  // BME680 init
  Serial.begin(9600);
    if (!bme.begin()) {
    Serial.println("Could not find a valid BME680 sensor, check wiring!");
    while (1);
  }

  // Start & init the CAN bus at 500 kbps
  if (!CAN.begin(500E3)) {
    Serial.println("Starting CAN failed!");
    while (1);
  }

  // Set up oversampling and filter initialization
  bme.setTemperatureOversampling(BME680_OS_8X);
  bme.setHumidityOversampling(BME680_OS_2X);
  bme.setPressureOversampling(BME680_OS_4X);
  bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
  bme.setGasHeater(320, 150); // 320*C for 150 ms
}
Periodically gathering & sending the sensor readings:

Every 8 seconds the BME680 gathers new sensor readings which is then transmitted using the CAN module. 

/* Periodically updating & sending sensor readings */
void loop() {
  Serial.println("Transmit New Readings!");
  getReadings();
  transmitReadings();
  Serial.println("------------------------------");
  delay(8000);
}
Sender serial monitor output:

The sensor readings are sent in a batch transmission, where each different readings is sent individually.  The information of each batch transmission - the transmission status & size of each message - is displayed on the serial monitor so that we can oversee the sensor readings & transmission statuses.

Click to expand

CAN Receiver Code Walk Through:

Libraries & Variables:

To access & use the LCD display we include the LiquidCrystal_I2C.h library by Frank de Brabander. As the display uses I2C protocol to communicate with microcontrollers the Wire.h library is also needed. For information about the LiquidCrystal_I2C.h library: https://github.com/johnrickman/LiquidCrystal_I2C

For the CAN network, we use the CAN.h library by Sandeep Misty. For more information about the CAN.h: https://github.com/sandeepmistry/arduino-CAN

We define each reading's message ID to distinguish between transmitted readings. The maximum CAN message length in bytes is also defined. We create an LCD variable with the I2C address of 0x27, 20 columns & 4 rows. Please note that I2C addresses may vary for each LCD display, so please double-check your I2C address before continuing. This can be done using the i2c_scanner example in the Wire.h library.

The sensor readings are stored as global variables for easier access.

#include <CAN.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include "string.h"

/* CAN Bus packet ids */
#define TEMP_ID 1
#define PRESSURE_ID 2
#define HUMIDITY_ID 3
#define GAS_ID 4

/* CAN Bus msg length can only be 8 bytes*/
#define MSG_LEN 8

// LCD display with 20 columns & 4 rows
LiquidCrystal_I2C lcd(0x27,20,4);

// BME680 sensor readings
const char temp[MSG_LEN];
const char pressure[MSG_LEN];
const char humidity[MSG_LEN];
const char gas[MSG_LEN];
Reading A Message:

We first clear the sensor value to avoid any duplication, then iteratively collect the transmitted message character by character.

/* Reads & copies the message into the sensor reading*/
void readMsg(char* sensorVal) {
  // Clearing the sensor value
  strcpy(sensorVal, "");
  int i = 0;
  while (CAN.available()) {
    sensorVal[i] = (char)CAN.read();;
    i++;
  }
  Serial.print("Msg Received: ");
  Serial.println(sensorVal);
}
Message receive callback:

A callback function that is executed every time a new transmitted message is received. We display the message information before determining which sensor value was transmitted and hence which value will be updated. 

/* Receive callback that determines which sensor value will be updated */
void onReceive(int packetSize) {

  // Packet info
  Serial.print("Received packet with id 0x");
  Serial.print(CAN.packetId(), HEX);
  Serial.print(" and length ");
  Serial.println(packetSize);

  switch (CAN.packetId()) {
    case TEMP_ID:
      readMsg(temp);
      break;
    case PRESSURE_ID:
      readMsg(pressure);
      break;
    case HUMIDITY_ID:
      readMsg(humidity);
      break;
    case GAS_ID:
      readMsg(gas);
      break;
  }
}
Displaying the sensor readings on the LCD display:

Formatting the sensor readings on the LCD display.

/* Formatting & displaying the sensor values on the lcd */
void updateLCD() {
  // Clearing the screen for new readings
  lcd.clear();

  // Displaying temp
  lcd.setCursor(0, 0);
  lcd.print("Temp:");
  lcd.print(temp);
  lcd.print("C ");

  // Displaying Pressure
  lcd.setCursor(0, 1);
  lcd.print("Pressure:");
  lcd.print(pressure);
  lcd.print(" hPa");

  // Displaying Humidity
  lcd.setCursor(0, 2);
  lcd.print("Humidity:");
  lcd.print(humidity);
  lcd.print("%");

  // Displaying Gas Resistance
  lcd.setCursor(0, 3);
  lcd.print("GasRes:");
  lcd.print(gas);
  lcd.print(" KOhms");
 
  lcd.display();
}
Initializing the CAN module & BME680 sensor:

We use the CAN.h library to test if the CAN module was initialized to 500kbps as well as setup the receive callback. And initialize the LCD display & turn the backlight on.

void setup() {
  Serial.begin(9600);
  while (!Serial);

  // Start & init the CAN bus at 500 kbps
  if (!CAN.begin(500E3)) {
    Serial.println("Starting CAN failed!");
    while (1);
  }

  // Init the lcd & turn the backlight on
  lcd.init();
  lcd.backlight();

  // register the receive callback
  CAN.onReceive(onReceive);
}
Updating the LCD display:

Periodically updating the LCD display every 3 seconds to show the most recent sensor values.

/* Updating the lcd screen with the new sensor readings */
void loop() {
  updateLCD();
  delay(3000);
}
Receiver serial monitor output:

Each message's information - id, size & content - is displayed on the serial monitor for potential debugging & monitoring purposes.

Click to expand

Full Transmitter Code:

#include <Wire.h>
#include "Adafruit_BME680.h"
#include "string.h"
#include <CAN.h>

#define SEALEVELPRESSURE_HPA (1013.25)

/* CAN Bus packet ids */
#define TEMP_ID 0x1
#define PRESSURE_ID 0x2
#define HUMIDITY_ID 0x3
#define GAS_ID 0x4

Adafruit_BME680 bme;

// BME680 sensor readings
String temp;
String pressure;
String humidity;
String gas;

/* Transmits a packet with the given messsage and id */
void sendMsg(int id, int size, String msg) {
  // Packet info
  Serial.print("size = ");
  Serial.println(size);
  Serial.print("Sending ");
  Serial.print(msg);
  Serial.print(" ...");

  // Creating & transmitting the packet
  CAN.beginPacket(id);
  for(int i = 0; i < size; i++) {
    Serial.print(msg[i]);
    CAN.write(msg[i]);
  }
  CAN.endPacket();

  Serial.println(" done!");
  delay(2000);
}

/* Updates the BME680 sensor readings */
void getReadings() {

  // Checking for valid readings
  if (! bme.performReading()) {
    Serial.println("Failed to perform reading :(");
    return;
  }

  temp = String(bme.temperature);
  pressure = String(bme.pressure / 100.0);
  humidity = String(bme.humidity);
  gas = String(bme.gas_resistance / 1000.0);
}

/* Sensor reading batch transmission */
void transmitReadings() {
  sendMsg(TEMP_ID, strlen(temp.c_str()), temp);
  sendMsg(PRESSURE_ID, strlen(pressure.c_str()), pressure);
  sendMsg(HUMIDITY_ID, strlen(humidity.c_str()), humidity);
  sendMsg(GAS_ID, strlen(gas.c_str()), gas);
}

void setup() {
  // BME680 init
  Serial.begin(9600);
    if (!bme.begin()) {
    Serial.println("Could not find a valid BME680 sensor, check wiring!");
    while (1);
  }

  // Start & init the CAN bus at 500 kbps
  if (!CAN.begin(500E3)) {
    Serial.println("Starting CAN failed!");
    while (1);
  }

  // Set up oversampling and filter initialization
  bme.setTemperatureOversampling(BME680_OS_8X);
  bme.setHumidityOversampling(BME680_OS_2X);
  bme.setPressureOversampling(BME680_OS_4X);
  bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
  bme.setGasHeater(320, 150); // 320*C for 150 ms
}

/* Periodically updating & sending sensor readings */
void loop() {
  Serial.println("Transmit New Readings!");
  getReadings();
  transmitReadings();
  Serial.println("------------------------------");
  delay(8000);
}

Full Receiver Code:


#include <CAN.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include "string.h"

/* CAN Bus packet ids */
#define TEMP_ID 1
#define PRESSURE_ID 2
#define HUMIDITY_ID 3
#define GAS_ID 4

/* CAN Bus msg length can only be 8 bytes*/
#define MSG_LEN 8

// LCD display with 20 columns & 4 rows
LiquidCrystal_I2C lcd(0x27,20,4);

// BME680 sensor readings
const char temp[MSG_LEN];
const char pressure[MSG_LEN];
const char humidity[MSG_LEN];
const char gas[MSG_LEN];

/* Reads & copies the message into the sensor reading*/
void readMsg(char* sensorVal) {
  // Clearing the sensor value
  strcpy(sensorVal, "");
  int i = 0;
  while (CAN.available()) {
    sensorVal[i] = (char)CAN.read();;
    i++;
  }
  Serial.print("Msg Received: ");
  Serial.println(sensorVal);
}

/* Receive callback that determines which sensor value will be updated */
void onReceive(int packetSize) {

  // Packet info
  Serial.print("Received packet with id 0x");
  Serial.print(CAN.packetId(), HEX);
  Serial.print(" and length ");
  Serial.println(packetSize);

  switch (CAN.packetId()) {
    case TEMP_ID:
      readMsg(temp);
      break;
    case PRESSURE_ID:
      readMsg(pressure);
      break;
    case HUMIDITY_ID:
      readMsg(humidity);
      break;
    case GAS_ID:
      readMsg(gas);
      break;
  }
}

/* Formatting & displaying the sensor values on the lcd */
void updateLCD() {
  // Clearing the screen for new readings
  lcd.clear();

  // Displaying temp
  lcd.setCursor(0, 0);
  lcd.print("Temp:");
  lcd.print(temp);
  lcd.print("C ");

  // Displaying Pressure
  lcd.setCursor(0, 1);
  lcd.print("Pressure:");
  lcd.print(pressure);
  lcd.print(" hPa");

  // Displaying Humidity
  lcd.setCursor(0, 2);
  lcd.print("Humidity:");
  lcd.print(humidity);
  lcd.print("%");

  // Displaying Gas Resistance
  lcd.setCursor(0, 3);
  lcd.print("GasRes:");
  lcd.print(gas);
  lcd.print(" KOhms");
 
  lcd.display();
}

void setup() {
  Serial.begin(9600);
  while (!Serial);

  // Start & init the CAN bus at 500 kbps
  if (!CAN.begin(500E3)) {
    Serial.println("Starting CAN failed!");
    while (1);
  }

  // Init the lcd & turn the backlight on
  lcd.init();
  lcd.backlight();

  // register the receive callback
  CAN.onReceive(onReceive);
}

/* Updating the lcd screen with the new sensor readings */
void loop() {
  updateLCD();
  delay(3000);
}

Flashing and displaying the data:

  • Connect the Arduino controllers to the computer separately.
  • Flash the sender code onto the Arduino connected to the BME680 & flash the receiver code onto the Arduino connected to the LCD display. 
  • After flashing both systems successfully, connect both arduino controllers to the computer or use a portable 5VDC  power source to power the system and display the sensor data.
MB-TUT-CAN-ARD-BME680

Downloadable Content:

Please find this tutorial's code on our GitHub page.

Table of Contents