Arduino Home

An Open-Source platform to create digital devices and interactive objects that sense and control physical devices.

Arduino Tutorials
Arduino Projects

 

1、On-line battery bank monitor

This project, On-line battery bank monitor, can trace voltage, current and battery temperature with MKR VIDOR 4000.

SAMD core runs like MKR 1010, which is ARM-M0 core. The performance is not enough for fast ADC conversion,but in draft design it shall be used. Then the ADC function is done with FPGA and the low-power supervision part is done by SAMD. MKR VIDOR 4000 appears good for such hybrid Project binding low-power and high performance in one board.

2. Introduction to the MKR VIDOR 4000

Arduino MKR Vidor 4000 features on board FPGA using the Intel Altera Cyclone 10 L016 FPGA, a Microchip SAMD21 microcontroller, WiFi and BLE with a U-blox NINA 102 module, 8 MB of SDRAM, 2 MB of QSPI Flash (1 MB for user applications), a Micro HDMI connector, and a MiPi connector.  The FPGA contains 16K Logic Elements, 504Kbit of embedded RAM and 56 18x18 bit HW multipliers for high-speed DSP.

In Arduino IDE, the Atmel SAMD Core for Vidor Shall be added into the Boards Manager, unlike other MKR series like MKR 1100, the  SAMD Core SAMD21 can do little to Ports and Pins. It plays more roles as Interconnected Bridge between Developers and FPGA cores.USB Blaster is center part of the  MKR VIDOR 4000  kit, I assume.

Since Programming the FPGA is possible in various ways:

  1. flashing the image along with Arduino code creating a library which incorporates the ttf file
  2. programming the image in RAM through USB Blaster (this requires mounting the FPGA JTAG header). this can be done safely only when SAM D21 is in bootloader mode as in other conditions it may access JTAG and cause a contention
  3. programming the image in RAM through the emulated USB Blaster via SAM D21 (this component is pending release)

 

Here is how it works.

#include "Blaster.h"
static bool activityLed = true;
extern void enableFpgaClock();

void setup() {
  USBBlaster.begin(activityLed);
  // also start the 48MHz clock feed for the FPGA, in case we need to run the bitstream and need this clock
  enableFpgaClock();
}

void loop() {
  USBBlaster.loop();
}

Like rest of the MKR borads,  MKR Vidor 4000 runs at 3.3V. Applying more than 3.3V on any pin will damage the board. The VIN pin may be used to power the board but the voltage supplied must be 5V.

What make it unique is Pins, The standard MKR arduino pins of Ax and Dxx can  be routed through the FPGA as well. This is the flexibility of the FPGA technology In depth knowledge of both are required, to quickest startup is using a basic set of FPGA based functionalities, grouped into two main libraries: VidorIPeripherals and VidorGraphics. It can be sample for developers as well.

The MKR Vidor 4000 deploys different Development Cycle and it is really innovative. But need time to prove the Robustness, at least the Upload from Arduino IDE shall take longer than I expected, and fails several time.After all, the FPGA lib shall be refresh at the same time.

 

3. Function Blocks

To make the logic clean, three  ScheduleTable is used , including <ScheduleTable.h>

#include <ScheduleTable.h>
SchedTable<1> tsreadCycle(10,1000);
SchedTable<1> tswriteCycle(20,2000);
SchedTable<1> bmsCycle(20,100);

The channel in  thingspeak.com is created.

tsreadCycle for BMS data read with 10s  cycle,

tswriteCycle for data write with 20s cycle,

bmsCycle for Battery Supervised value fetching with 2s cycle, it can be tuned faster

For batter monitor, three value is enough for most cases, voltage,  current,  and temperature for batteries.

 

 

4. The Hardware

4.1 MKR VIDOR 4000

4.2 TLP3547 optical Relay

4.3 LM35 temp sensor

4.4 Resistors and switches

 

5.  First DEMO, SAMD play ADC functions

5.1  Hardware wiring demo as follows, SAMD with ADC functions

 

5.2 The software, demo project with basic supervision functions.

#include <SPI.h>
#include <WiFiNINA.h>
#include "ThingSpeak.h"
#include "VidorPeripherals.h"
#include "VidorEncoder.h"
#include "arduino_secrets.h" 
// Set scheduleTable to 1000ms base and 10s for read and 20s for write.
#include <ScheduleTable.h>
SchedTable<1> tsreadCycle(10,1000);
SchedTable<1> tswriteCycle(20,2000);
SchedTable<1> bmsCycle(20,100);


#define ADC_TEMPERATURE_PIN      A0
#define ADC_VOLTAGE_PIN          A1
#define ADC_CURRENT_PIN          A2
#define ADC_RESOLUTION_BITS      12
#define D_PROTECTION_PIN         0
#define D_CONTROL_PIN            1
#define TEMHIGH     100
#define VOLHIGH     100
#define CURHIGH     100

///////please enter your sensitive data in the Secret tab/arduino_secrets.h
char ssid[] = SECRET_SSID;        // your network SSID (name)
char pass[] = SECRET_PASS;    // your network password (use for WPA, or use as key for WEP)

int status = WL_IDLE_STATUS;     // the Wifi radio's status
WiFiClient  client;
unsigned long myChannelNumber = SECRET_BMS_CH_ID;
const char * myWriteAPIKey = SECRET_BMS_WRITE_APIKEY;
const char * myReadAPIKey = SECRET_BMS_READ_APIKEY;
int keyIndex = 0;            // your network key Index number (needed only for WEP)
unsigned int bmsFieldNumber = 3;
int ts_number = random(0,100);
int bms_voltage,  bms_current,  bms_temperature=0;
bool Fault=false;

void FPGA_on_off(){  
 // Let's start by initializing the FPGA
  if (!FPGA.begin()) { Serial.println("Initialization failed!");while (true) {} }
  Serial.print("Vidor bitstream version: ");
  Serial.println(FPGA.version(), HEX);
  FPGA.pinMode(33, OUTPUT);
  FPGA.digitalWrite(33, HIGH);
  pinMode(A0, INPUT);
  // WireFPGA1 object provides an extra I2C interface
  WireFPGA1.begin();
  FPGA.end();
  return;  
}


void thingspeak_on_scheduleTable(){  
  // check for the WiFi module:
  if (WiFi.status() == WL_NO_MODULE) {
    Serial.println("Communication with WiFi module failed!\n");
    while (true);
  }
  while (status != WL_CONNECTED) {
    Serial.print("Attempting to connect to WPA SSID: ");
    Serial.println(ssid);
    status = WiFi.begin(ssid, pass);
    delay(10000); 
  }
    Serial.print("You're connected to the network");
  //Serial.print( "IP Address: "+WiFi.localIP() );
   ThingSpeak.begin(client);  // Initialize ThingSpeak
    tsreadCycle.at(0,ts_read);
    tsreadCycle.start();
    tswriteCycle.at(0,ts_write);
    //tswriteCycle.start();
    bmsCycle.at(0,bms_update);
    bmsCycle.start(1);
    return;
}


void ts_read() {
    int statusCode = 0;
          bmsCycle.stop();
    long bms_temperature = ThingSpeak.readLongField(myChannelNumber, bmsFieldNumber, myReadAPIKey);  
    statusCode = ThingSpeak.getLastReadStatus();
    if(statusCode == 200){
      Serial.println("bms_temperature : " + String(bms_temperature) + " deg F");
      }
     else{
      Serial.println("Problem reading channel. HTTP error code " + String(statusCode)); 
      } 
       bmsCycle.start(1);
       return ;
}


void ts_write() {      
      String statusStr = "";      
      // set the fields with the values
      ThingSpeak.setField(1, bms_voltage); 
      ThingSpeak.setField(2, bms_current);
      ThingSpeak.setField(3, bms_temperature);
      statusStr = String("Three fields set.");
      int x = ThingSpeak.writeFields(myChannelNumber, myWriteAPIKey);
      if(x == 200){
        Serial.println("Channel update successful. With number of "+String(ts_number) );
        }
        else{
          Serial.println("Problem updating channel. HTTP error code " + String(x));
          }

      return; 
}

void bms_update() {
  digitalWrite(D_PROTECTION_PIN, HIGH);
  bms_voltage=analogRead(ADC_VOLTAGE_PIN);
  delay(10);
  bms_current=analogRead(ADC_CURRENT_PIN);;
  delay(10);
  bms_temperature=analogRead(ADC_TEMPERATURE_PIN);
  delay(10);
  Fault =  ( (bms_voltage > VOLHIGH)or (bms_current > CURHIGH)  or (bms_temperature> TEMHIGH) ) ? true : false  ;
  Serial.println("\nbms_voltage "+String(bms_voltage)+"bms_current "+String(bms_current)+"bms_temperaturetage "+String(bms_temperature) );
  if ( Fault ) {
    digitalWrite(D_PROTECTION_PIN, LOW); // Trip off the circuit.
    Serial.println("Problem on Voltage, current or temperature!");
    tswriteCycle.stop(); tsreadCycle.stop(); bmsCycle.stop();Serial.println("Loop Off.\n");
    }
}

void setup() {
  //Initialize serial and wait for port to open:
  Serial.begin(9600);
  while (!Serial) {; }
  // PINs 
  analogReadResolution(ADC_RESOLUTION_BITS);
  pinMode(D_PROTECTION_PIN, OUTPUT);
  pinMode(D_CONTROL_PIN, OUTPUT);
  FPGA_on_off();
  thingspeak_on_scheduleTable();
}


void loop() {    
   //Serial.println("\nLoop On.");
    ScheduleTable::update();
}

 

It shall be noted. The FPGA is still now used, only SAMD core is running.

I would like to improve the project with two choice, using extra ADC IC to read potential of resistor and temperatures, or customized FPGA firmware for fast ADC conversion.

 

 

6 . REMOTE sensing with I2C connection

6.1 This part of work use FPGA peripheral library, fetching analogue data from remote MCU. MKR4000 runs with Nucleo board remotely.

MKR 4000 in I2C master mode and Nucleo-STM32F410 in slave mode.

The nucleo-connector according to,

 

6.2 Remote part running on Nucleo-STM32F410RB, coding in mbed OS5,


// BMS_I2C_SLAVE
# include <stdio.h>
# include <stdlib.h>
#include "mbed.h"
#include "TextLCD.h"    // LCD1602


#define VOL_REF 3300 * 10  //With convert RATIO in shun resistors
#define CUR_REF 1000 / 10     //With 10ohm Resistor
#define TEM_REF 100
#define MEASURESPAN  1          // in 1s


Ticker mticker;


DigitalOut led1(LED1);
AnalogIn voltage_value(A1);
AnalogIn current_value(A2);
AnalogIn temperature_value(A0);


TextLCD lcd(D8, D9, D4, D5, D6, D7);
I2CSlave bmsslave(D14, D15);


// display text on LCD
void textLCD(char *text, int line) {
    char tmpBuf[16];
    for (int i = 0; i < 16; i++) tmpBuf[i] = 0x20;
    for (int i = 0; i < strlen(text); i++) {
        if (i < 16) tmpBuf[i] = text[i];
        lcd.locate(i, line);
        lcd.putc(tmpBuf[i]);
    }
}


void digitLCD(int val, int line) {
    char tmpBuf[8];
    //printf("bms = %d\n",  val);
    for (int i = 4; i < 12; i++) tmpBuf[i] = 0x20;
    int i=12;
    while (val){
        tmpBuf[i] = (val % 10) + '0';  
        val =val /10;
        lcd.locate(i, line);
        lcd.putc(tmpBuf[i]);
        i--;
        }           
} // end  of digitLCD
          
void measuretick() {
    char meas[24];
    //float ana_read_a1,ana_read_a2,ana_read_a0;
    int ana_read_a1,ana_read_a2,ana_read_a0;
    
    ana_read_a1 = int ( 0.5+ VOL_REF* (voltage_value.read()) ); // Read the analog input value (value from 0.0 to 1.0 = full ADC conversion range) and Converts value in the 0V-3.3V range
    ana_read_a2 = int ( 0.5 +CUR_REF* (current_value.read()) );
    ana_read_a0 = int ( 0.5 +TEM_REF* (temperature_value.read())  );
        //printf("Voltage = %f: Current = %f: Temperature = %f.\n",  ana_read_a1,ana_read_a2,ana_read_a0);
    int length = snprintf( meas, sizeof(meas), "%d,%d,%d", ana_read_a1,ana_read_a2,ana_read_a0 );
    printf("V,I,T measures = %s and length =%d\n",  meas,length);
    // I2C Write.  
    if ( bmsslave.receive()==I2CSlave::ReadAddressed ) {
               bmsslave.write(meas, strlen(meas) + 1);
       }
    bmsslave.stop(); // Reset I2C status ready.       
    // LCD update.
       digitLCD( ana_read_a1, 0 );
       digitLCD( ana_read_a2, 1 );    
}


void BMS_ON() {
            textLCD("Ready to go.    ", 1);
            wait(2);
           textLCD("VOL=          mV", 0);
           textLCD("CUR=          mA", 1);
           mticker.attach(&measuretick, MEASURESPAN);  
}


int main(){
        // Setting I2C slave 
    bmsslave.address(0x60);
    bmsslave.frequency(100000);
    
    lcd.cls();
    textLCD("BMS Slave is On.", 0);
    wait(2);
    BMS_ON();    
            
    while(1){    } 
    
}    // end of main

 

On-line battery bank monitor ADC Ticker every 1 second. Set on MEASURESPAN.

6.3 The FPGA part shall be used.

Here is how it wired.

VidorTwoWire WireFPGA0(1,9,10);

VidorTwoWire WireFPGA1(2,11,12);

VidorTwoWire WireFPGA2(3,13,14);

VidorTwoWire WireFPGA3(4,A0,A1);

 

6.4 Here  is the code for FPGA1 used for I2C, mostly there are changes in bms_update()

#include <SPI.h>
#include <WiFiNINA.h>
#include "ThingSpeak.h"
#include "VidorPeripherals.h"
#include "VidorEncoder.h"
#include "arduino_secrets.h" 


// Set scheduleTable to 1000ms base and 10s for read and 20s for write.
#include <ScheduleTable.h>
SchedTable<1> tsreadCycle(10,1000);
SchedTable<1> tswriteCycle(20,2000);
SchedTable<1> bmsCycle(20,100);


#define D_PROTECTION_PIN         0
#define D_CONTROL_PIN            1
#define TEMHIGH     100
#define VOLHIGH     100
#define CURHIGH     100


#define SLAVE_ID  0x60
#define BMS_BUFF  24




///////please enter your sensitive data in the Secret tab/arduino_secrets.h
char ssid[] = SECRET_SSID;        // your network SSID (name)
char pass[] = SECRET_PASS;    // your network password (use for WPA, or use as key for WEP)


int status = WL_IDLE_STATUS;     // the Wifi radio's status
WiFiClient  client;
unsigned long myChannelNumber = SECRET_BMS_CH_ID;
const char * myWriteAPIKey = SECRET_BMS_WRITE_APIKEY;
const char * myReadAPIKey = SECRET_BMS_READ_APIKEY;


unsigned int bmsFieldNumber = 3;
int ts_number = random(0,100);
int bms_voltage,  bms_current,  bms_temperature=0;
bool Fault=false;


void thingspeak_on_scheduleTable(){  
  // check for the WiFi module:
  if (WiFi.status() == WL_NO_MODULE) {
    Serial.println("Communication with WiFi module failed!\n");
    while (true);
  }


  while (status != WL_CONNECTED) {
    Serial.print("Attempting to connect to WPA SSID: ");
    Serial.println(ssid);
    status = WiFi.begin(ssid, pass);
    delay(10000); 
  }
  
  Serial.print("You're connected to the network");
   ThingSpeak.begin(client);  // Initialize ThingSpeak


    tsreadCycle.at(0,ts_read);
    tsreadCycle.start();
    
    tswriteCycle.at(0,ts_write);
    //tswriteCycle.start();


    bmsCycle.at(0,bms_update);
    bmsCycle.start(1);
    return;
}


void ts_read() {
    int statusCode = 0;
    
      bmsCycle.stop();


  //ts_number++; Serial.print("In loop.\n");
  
    long bms_temperature = ThingSpeak.readLongField(myChannelNumber, bmsFieldNumber, myReadAPIKey);  
    statusCode = ThingSpeak.getLastReadStatus();
    if(statusCode == 200){
      Serial.println("bms_temperature : " + String(bms_temperature) + " deg F");
      }
     else{
      Serial.println("Problem reading channel. HTTP error code " + String(statusCode)); 
      } 
      
       bmsCycle.start(1);
       
      return ;
}


void ts_write() {      
      String statusStr = "";      
      // set the fields with the values
      ThingSpeak.setField(1, bms_voltage); 
      ThingSpeak.setField(2, bms_current);
      ThingSpeak.setField(3, bms_temperature);
      statusStr = String("Three fields set.");
      
      //int x = ThingSpeak.writeField(myChannelNumber, 1, ts_number, myWriteAPIKey);
      int x = ThingSpeak.writeFields(myChannelNumber, myWriteAPIKey);
      if(x == 200){
        Serial.println("Channel update successful. With number of "+String(ts_number) );
        }
        else{
          Serial.println("Problem updating channel. HTTP error code " + String(x));
          }


       return; 
}


void bms_update() {
      String statusStr = "";     
     String comdata = "";
     int i=0;
     int index;
     
  digitalWrite(D_PROTECTION_PIN, HIGH);
  /***************************************
  bms_voltage=analogRead(ADC_VOLTAGE_PIN);
  delay(10);
  bms_current=analogRead(ADC_CURRENT_PIN);;
  delay(10);
  bms_temperature=analogRead(ADC_TEMPERATURE_PIN);
  delay(10);
  **************************************/
      // Same for SPIFPGA1, but with SPI of course
  WireFPGA1.begin();
  WireFPGA1.requestFrom(SLAVE_ID, BMS_BUFF);    // request 6 bytes from slave device #2


  while(WireFPGA1.available())    // slave may send less than requested
  { 
   char c = WireFPGA1.read(); // receive byte as a character
   comdata += c;
   i++;
   if (i>BMS_BUFF) { break; }
   Serial.print(c);         // print the character
  }
 // First data,
 index = comdata.indexOf(',');
 bms_voltage=comdata.substring(0,index).toInt();
 comdata=comdata.substring(index+1,comdata.length()); 
 // 2nd and 3rd data,
 index = comdata.indexOf(',');
 bms_current=comdata.substring(0,index).toInt();
 bms_temperature=comdata.substring(index+1,comdata.length()).toInt(); 

  
  Fault =  ( (bms_voltage > VOLHIGH)or (bms_current > CURHIGH)  or (bms_temperature> TEMHIGH) ) ? true : false  ;
  Serial.println("\nbms_voltage "+String(bms_voltage)+"bms_current "+String(bms_current)+"bms_temperaturetage "+String(bms_temperature) );
  Fault=0;
  if ( Fault ) {
    digitalWrite(D_PROTECTION_PIN, LOW); // Trip off the circuit.
    Serial.println("Problem on Voltage, current or temperature!");
    tswriteCycle.stop(); tsreadCycle.stop(); bmsCycle.stop();Serial.println("Loop Off.\n");
      // shutdown the FPGA
    FPGA.end();
    }
  


  }


  


void setup() {
  //Initialize serial and wait for port to open:
  Serial.begin(9600);
  while (!Serial) {; }
  // PINs 
  pinMode(D_PROTECTION_PIN, OUTPUT);
  pinMode(D_CONTROL_PIN, OUTPUT);


  
    // Let's start by initializing the FPGA Wire
  if (!FPGA.begin()) {
    Serial.println("Initialization failed!");
    while (1) {}
  }
    // WireFPGA1 object provides an extra I2C interface
  WireFPGA1.begin();
  //WireFPGA1.VidorTwoWire


  
  // THINGSPEAD start.
  thingspeak_on_scheduleTable();
  
}


void loop() {    
   //Serial.println("\nLoop On.");
    ScheduleTable::update();
    // Table not shown.
    if (false) {
      tswriteCycle.stop(); tsreadCycle.stop(); bmsCycle.stop();Serial.println("Loop Off.\n");
      }
  //delay(20000);   // check the network connection once every 20 seconds:
}

 

6.5 Application of thingspeak

Here is the screenshot from thingspeak with BMS project settings.

 

7. Final part

On-line battery bank monitor can get battery running status and send the data remotely.

Plug the usb connector for Nucleo and MKR4000 board,

8. Remarks,

8.1 It is really amazing experience in running MKR4000 Vidor board. The clever design of PinMux of SAMD and FPGA provide ampliy choice and perfect compatible with previous Arduino boards and Extension Shield boards.

It is by far perfect, such as I2C in master mode only and a lit bit high running power cunsumption

It inspires more idea and complex alogrithm in one simple and compact board. It definitely supports hotest-topic Deep Learing and even more.

Hope the USB blast can be next standard PORT for FPGA integration.