0

Arduino tečaj: Raspberry Pi

8. lesson

Ovo je dio cjelovitog tečaja Arduino-a.
  1. Štampajte dijelove koristeći ovaj Sketchup model RescueLine.zip.
  2. Sastavite robota koristeći upute za sastavljanje.
  3. Spojite kablove koristeći plan spajanja.
  4. Programirajte robota.
    1. Osnove.
    2. Praćenje crte.
    3. Naredbe.
    4. LIDARi.
    5. Prekidač, IMU, 8x8 LED.
    6. Praćenje zida.
    7. Srebro.
    8. 8x8 LED programiranje.
    9. Robotska ruka.
    10. Raspberry Pi (ova stranica).
    11. Prepoznavanje kamerom, OpenCV.
    12. Arduino i RPI surađuju.

Purpose

In our robot a microcontroller (Teensy 3.2) works together with Raspberry Pi, a microprocessor. None of them can complete all the tasks alone. RPI cannot control motors and read sensors well and Arduino cannot process images at all. Therefore, You will have to make 2 programs, one for each of them. The environments are quite different and programming is, despite they use the same language, different. Obviously, the the programs have to communicate in some way. There are different options. If You need complex data to be exchanged (and You do), the 2 choices will be I2C and UART. I2C would use less pins but it is always master (RPI) - initiated, so a bidirectional information flow gets more complicated. Furthermore, a stuck I2C bus will stop Arduino - a bad thing. So, we choose UART.

You will learn how to exchange information. How You will implement the rest, it is up to You. One solution is to regularly check for new messages on both sides and act accordingly. For example, first Arduino sends a message to RPI to start looking for green markers. After that Arduino should be checking for the result often. Otherwise there can be too many unread messages and the buffer will overflow. On the RPI side, the program should report every marker (with a message), when it detects one. It should also listen for new Arduino commands as Teensy may issue a new command to stop looking for markers and start searching white balls.

Arduino part

This lesson continues building the program that we interrupted in lesson 7.

Download UART library from https://www.github.com/PribaNosati/MRMS and install it as before. Enter the orange changes:

#include <Commands.h> //ML-R library You use to issue commands to Your robot.
#include <Displays.h> //Libraray for 8x8 3-color display
#include <IMUBoschBNO055.h> //IMU
#include <Motors.h> //Library Motors will be used. It contain motor-control functions.
#include <PID.h> //Library that implements a very simple proportional–integral–derivative controller. Check Wikipedia.
#include <ReflectanceSensors.h>
#include <Switches.h>
#include <UART.h> //Library used to communicate to Raspberry Pi 
#include <VL53L0Xs.h> //STM VL53L0X is the name of LIDAR chip.

Commands commands(&Serial2); // Object for commands is created.
Displays displays;
IMUBoschBNO055 imu(&Serial2);
Motors motors(true, &Serial2); //motors is an C++ object of class Motors. If You do not understand this, never mind. Just alwasys use motors with this line.
PID pidLine(20, 15, 0.01, &Serial2); //PID object: pidLine. After studying Wikipedia, alter the 3 parameters.
PID pidWallAngle(10, 1, 0, &Serial2); //Angle error. 10 and 1 are arbitrary parameters: find the correct values for Your robot.
PID pidWallDistance(10, 1, 0, &Serial2); // Distance to wall error. 10 and 1 are arbitrary.
ReflectanceSensors reflectanceSensors(0, 0.25, &Serial2);
Switches switches;
UART uart(&Serial1); //Raspberry Pi is connected via Serial1.
VL53L0Xs lidars(&Serial2); //LIDARs

void setup() {
  Serial.begin(115200);    // Serial (USB) communication to our computer is established. You call function begin of the object Serial with 1 argument: 115200.
  Serial2.begin(9600);     // Start serial communication with Bluetooth adapter.
  Wire.begin();            //Start I2C bus.
  delay(1000);             // Do nothing for 1000 ms (1 sec.) - wait for the USB connection.
  Serial.println("Start"); // Display "Start" on PC's screen. Function println() prints the argument ("Start") and jumps to a new line.
  
  //The following 2 lines will create 2 commands. First parameter is shortcut that invokes the command. The second 
  //is a function pointer. Again, if that term doesn't ring a bell, it will be no problem. Just write here the name of the function You 
  //want to invoke, for example, after typing "lin" in your mobile phone. The third parameter is an explanation.
  commands.add("dis", displayTest, "Display test");
  commands.add("imu", imuTest, "IMU test");
  commands.add("cal", calibrate, "Calibrate black");
  commands.add("caa", calibrateAll, "Calibrate all");
  commands.add("lid", lidarsTest, "Test LIDARs");
  commands.add("lin", line, "Follow line", 33); //Pressing the switch (connected to pin 33) will execute this command.
  commands.add("swi", switchesTest, "Switches test");
  commands.add("uar", uartTest, "UART test");
  commands.add("wal", wall, "Follow left wall");
  
  //Display 8x8
  displays.add(0x70);

  //IMU, false as parameter sets its I2C address to 0x28 instead of 0x29.
  imu.add(false);
  
  //LIDARs. Each add command uses 2 parameters: digital pin number, and a chosen I2C (hexadecimal) address.
  //Prefix "0x" precedes hexadecimal number. As every LIDAR uses the same I2C address, 0x29, there must be a way to resolve the conficts, and there is. During boot phase the library enables all the LIDARs, one by one, using 0x29, and changes their addresses into the ones indicated as the second parameter.
  lidars.add(13, 0x22);    
  lidars.add(17, 0x24);
  lidars.add(29, 0x23); 
  lidars.add(31, 0x26);
  lidars.add(30, 0x25); 
  lidars.add(12, 0x27);
  lidars.begin();

  //In the following 4 lines we will add 4 motors. First 2 parameters, numbers (like 3, 5), are pins that are connected
  //to motor controller. Third parameter (true or false) is true when it is a left motor, otherwise false. There is an optional fourth parameter in the
  //first 2 lines and there can be even more of them. Optional (default) parameters do not have to be mentioned.
  motors.add(3, 5, true, true);
  motors.add(23, 4, false);
  
  //10 reflectance sensors will be added. ML-R libraries always include new elements (like sensors and motors) using the function add().
  //First parameter is analog pin's name, second millimeters from the central longitudinal axis (negative numbers for left sensors).
  //The last parameter is always true and enables that sensor for the libary's line following algorithm.
  reflectanceSensors.add(A11, -49.5, true); 
  reflectanceSensors.add(A10, -38.5, true);
  reflectanceSensors.add(A13, -27.5, true);
  reflectanceSensors.add(A12, -16.5, true);
  reflectanceSensors.add(A1, -5.5, true);
  reflectanceSensors.add(A14, 5.5, true);
  reflectanceSensors.add(A0, 16.5, true);
  reflectanceSensors.add(A2, 27.5, true);
  reflectanceSensors.add(A17, 38.5, true);
  reflectanceSensors.add(A15, 49.5, true);
  reflectanceSensors.eepromRead();//Reading EEPROM calibration data into RAM.
  
  switches.add(33);
  
  uart.add(); // The communication to RPI starts here.

  displays.fillScreen(0, LED_GREEN);
  displays.writeDisplay(0);
}

void loop() {
  //List all the available commands and wait for user's input.
  do {
    commands.list();
  } while (commands.prompt());
}

/** Function for reflectance sensors calibration
*/
bool calibrate() {
  reflectanceSensors.calibrate();
  return true;
}

/* Calibrate white, black and silver
@return true
*/
bool calibrateAll()
{
  reflectanceSensors.calibrate(5, ALL);
  return true;
}

/** Any key pressed?
@return - true if so.
*/
bool commandAvailable() {
  return commands.available();
}

bool displayTest() {
  displays.test(0, commandAvailable);
  return true;
}

/** The next 6 functions convert mm to cm and introduce some more meaningful names. First of the last 2 letters (L, F or R) is robot's side and the one following determines the sensor on that side. For example LB - left side of the robot (L), back (rear sensor, B). F is front, L is left, R is right, B is back (rear). 
@return - distance in cm
*/
float distanceLB() { return lidars.distance(0) / 10.0; }
float distanceLF() { return lidars.distance(1) / 10.0; }
float distanceFL() { return lidars.distance(2) / 10.0; }
float distanceFR() { return lidars.distance(3) / 10.0; }
float distanceRF() { return lidars.distance(4) / 10.0; }
float distanceRB() { return lidars.distance(5) / 10.0; }

/** All the ML-R libraries invoke this function when something goes wrong. Here we stop the motors, display the error
message which enters the function as the only argument ("message") and then enter an endless loop. while(1) never ends as the condition
(1) is always true. The only command that is in the loop is ";" (empty command - does nothing).
@param message
*/
void error(String message){
  motors.go(0, 0);         //stop the motors to avoid harm
  Serial.println(message); //display error message
  while(1)                 //never stop
    ;                      //do nothing
}

bool imuTest() {
  imu.test(commandAvailable);
  return true;
}

/** Test LIDARs
@return - always true
*/
bool lidarsTest() {
  lidars.test(commandAvailable);
  return true;
}

/* A line-following function, can invoked by a command.
*/
bool line(){
  while (!commandAvailable()) { // There is a way to stop the line following - press a key.
    bool found, nonLineFound;
    //The following function returns position of the black line against the longitudinal centre of the robot. found and notFound are output
    //parameters. found is true if a line is found (at least one sensor black). nonLineFound not found (that at least one sensor doesn't detect black).
    float centerMm = reflectanceSensors.findLine(found, nonLineFound);
    //centerMm is in fact error in line following. It should be 0. PID algorithm uses it as an input and calculates error correction:
    float correction = pidLine.calculate(centerMm);

    const int speedAhead = 60; //Average speed of the left and right motor. You can change this number.
    if (found)
      motors.go(speedAhead + correction, speedAhead - correction);//Here we control the motors.
    else
      motors.go(50, 50); //If no line, go straight ahead.
  }
  motors.go(0, 0);
  return true;
}

bool switchesTest() {
  switches.test(commandAvailable);
  return true;
}

/** Print to 2 serial ports: Arduino monitor and Your mobile (Bluetooth).
@param message
@param eol - end of line
*/
void print(String message, bool eol = false); //Function declaration enables implicit parameter eol - if omitted, the value will be false.
void print(String message, bool eol) {
	if (eol) {
		Serial.println(message);
		Serial2.println(message);
	}
	else {
		Serial.print(message);
		Serial2.print(message);
	}
}

bool uartTest() {
  uint32_t lastSent = 0; //The time last message was sent to RPI
  
  while (!commandAvailable()) {
  
    //The next 2 lines receive data from RPI and print it. They are not used in this test.
    if (uart.available())
      print(uart.read());

    if (millis() - lastSent > 1000) { // When time difference (in milliseconds) exeeds 1000 (ms), send a new message.
      char data[] = "Arduino message "; //Message text.
	  //Numbers, like millis() and data, must be cast ((String)) to enable string concatination (sign +). Otherwise 3 program lines would be
      //be needed to print the 3 items: millis(), "ms, send: ", and data.
      print((String)millis() + " ms, send: " + (String)data, true); // Function print() prints both to Arduino monitor and to Your mobile
      uart.write(data); //Here the data is actually sent.
      lastSent = millis(); //Last send time is recorded.
    }
  }
  return true;
}

/** Follow left wall
@return - always true
*/
bool wall() {
  while (!commandAvailable())
    wall(true, 10); // true - left wall, 10 - follow it at 10 cm.
  motors.go(0, 0);
  return true;
}

/** Follow wall
@param isLeftWall - left wall (true) or right (false)
@param wallDistanceNeeded - wall following distance in cm.
*/
void wall(bool isLeftWall, float wallDistanceNeeded) {

  //Determine front and back distances to the wall
  float wallDistanceFront = isLeftWall ? distanceLF() : distanceRF();
  float wallDistanceRear = isLeftWall ? distanceLB() : distanceRB();
  const float betweenLIDARs = 13.5; //This number is needed to calculate distance to the wall.

  //Check Your knowledge of primary school math to prove that this calculation yields distance to the wall:
  float distanceToWallMeasured = betweenLIDARs *(wallDistanceFront + wallDistanceRear) / 2.0 /
	hypot(betweenLIDARs, wallDistanceFront - wallDistanceRear);
  float tooFar = distanceToWallMeasured - wallDistanceNeeded; //This is the error in distance - we will try to make it 0.
  float correction = pidWallDistance.calculate(tooFar) * (isLeftWall ? -1 : 1); //PID calculates the correction for position

  //This is the second error, direction (should be parallel to the wall)
  float clockwiseTooMuch = (wallDistanceFront - wallDistanceRear) * (isLeftWall ? -1 : 1); // 2nd error.
  correction += pidWallAngle.calculate(clockwiseTooMuch); //PID calculates correction for the 2nd error and we add it to the first correction.

  const int speedAhead = 60; // Average speed ahead.
  motors.go(speedAhead + correction, speedAhead - correction);//Here we control the motors, by applying the cumulative correction for 2 errors.
}

Raspberry Pi part

Any Raspberry Pi board can be used, although Your life will be much more pleasant with the fastest models. Connect Raspberry Pi board as depicted in connection schematics . RPI board is powered using ML-R Raspberry Pi Power Supply (mrm-rpi). A worse solution is to connect the 2 RPI pins to a 5 V KK 254 connector using 2 (advisable thicker) Dupont 0.1" cables.

The easiest way to configure Raspeberry Pi software is to use a complete SD's image. Here is the link (click to download). Unzip the downloaded file. Take a 16 GB micro SD card and copy the image, by running, for example, Etcher or Win32DiskImager. If Your SD card is smaller than the image (although is rated 16 GB), the operation will fail. In that case You have an option, but only in Win32DiskImager, to clone the content only, without additional empty space. Another solution is to use a bigger 16 GB card or use a 32 GB. After successful copying, put the card into Your RPI.

If You do not want to use the complete SD, please install CodeBlocks and OpenCV in Your Raspibian.

Power on the board.

There are 3 ways to access Raspberry Pi operating system.

  • Connect an USB keyboard, mouse and HDMI monitor. This is a staightforward way. There is no configuration, so chances for mistakes are the lowest. However, this is not a viable way to connect to a moving robot and also all the connectors may not be accessible in a completed robot. As You have to configure the next 2 options somehow, it is usually a must to connect to RPI this way and prepare the configuration.
  • Start RealVNC client and connect to RPI via Ethernet cable. Your computer must have a RJ45 (Ethernet) connector. Use a common Ethernet patch cable to connect it to Raspberry Pi. First You have to establish Ethernet connection. Check http://www.circuitbasics.com/how-to-connect-to-a-raspberry-pi-directly-with-an-ethernet-cable/. In short, configure your PC's networking like this:
    • click with the right mouse's button to Windows logo (lower left corner of Windows 10),
    • choose "Network connections", "Network and Sharing Center",
    • click on "Local Area Connection", "Properties",
    • click on "Internet Protocol Version 4 (TCP/IPv4)",
    • click on "Properties",
    • enter the following data:
      .
    Now, VNC. You can find the instructions here: https://www.realvnc.com/en/connect/docs/raspberry-pi.html.
  • Start RealVNC client and connect to RPI via WiFi. If You use our SD image, the network name will be "WiFi" and password "12345". First join Your (home) WiFi network. Then follow VNC instructions as in the previous paragraph. A variation of this version would be to use the previous one (wired Ethernet), and change WiFi settings by entering the command (after clicking on a black terminal window icon on the top of the RPI screen):

    sudo nano /etc/wpa_supplicant/wpa_supplicant.conf

    This will open the editor:



    Change ssid and psk, click on Ctrl-X, Y, Enter. Disconnect Ethernet cable. Reboot.

Start CodeBlocks by clicking on raspberry image, Programming, Code::Blocks IDE:



CodeBlocks IDE will open:

There are 2 possibilities:

  1. If You haven't the got SD image, You will have to enter this program into CodeBlock's editor. Choose "Create a new project":



    Choose "Empty project" and "Go":

    Follow the wizard and make the new project.
  2. If You made the SD using the supplied image, just open the project ArduinoHelper by clicking on "/home/pi/Cpp/ArduinoHelper/ArduinoHelper.cbp", listed under "Recent projects":



    ArduinoHelper project will open:



    Change "FIND_CIRCLES" in line 10 into "TEST_UART". Skip entering the 3 files, described below (main.cpp,...) and jump to "Run" paragraph.

Enter main.cpp (if You don't have SD image):

#include <iostream>
#include "UART.h" // ML-R supplied library

using namespace std;

int main(int argc, char *argv[])
{
    UART uart; // Define uart object, for the communication to Arduino
    while (true){ //Never stop
        if (uart.available()){ // If a message from Arduino is available
            cout << uart.read(); // Read a charater (byte) and print it to console
            cout.flush(); //Refresh console (force display).
        }
    }

    return 0;
}



End of code! Then UART.h:
#ifndef UART_H
#define UART_H

#include <stdint.h>
#include <string>

class UART
{
    private:
        int _handle; /// Reference to the serial port.

    public:
        UART();
        virtual ~UART();

        /**Returns the number of characters available for reading, or -1 for any error condition, in which case errno will be set appropriately.
        */
        int available();

        /**Reads first byte of the incoming serial data.
        @return - the first byte of incoming serial data available (or -1 if no data is available).
        */
        uint8_t read();

        /** Reads characters from the serial port into a buffer. The function terminates if the determined length has been read, or it times out.
        @param buffer - the buffer to store the bytes in.
        @param length - the number of bytes to read.
        @return - the number of bytes placed in the buffer.
        */
        int read(uint8_t size, uint8_t *bytes);

        /** Writes a single byte to the serial port.
        @param byte - a byte to send.
        */
        void write(uint8_t byte);

        /** Sends the nul-terminated string to the serial device identified by the given file descriptor.
        */
        void write(char *string);

        /** Writes series of bytes to the serial port; to send the characters representing the digits of a number use the print() function instead.
        @param size - buffer's size
        @param buffer - data
        */
        void write(uint8_t size, uint8_t *bytes) ;
};

#endif // UART_H



End of code! Finally UART.cpp:
#include "UART.h"
#include <fcntl.h>
#include <stdint.h>
#include <string>
#include <string.h>
#include <iostream>
#include <unistd.h>
#include <wiringPi.h>
#include <wiringSerial.h>

using namespace std;

UART::UART()
{
    char dev[] = "/dev/serial0";
	if ((_handle = serialOpen(dev, 115200)) < 0) {
		cout << "Error opening " << dev << ". Rights?" << endl;
	}
	else
		cout << dev << " opened." << endl;
}

UART::~UART()
{
	serialClose(_handle);
}

/**Returns the number of characters available for reading, or -1 for any error condition, in which case errno will be set appropriately.
*/
int UART::available() {
	return serialDataAvail(_handle) > 0;
}

/**Reads first byte of the incoming serial data.
@return - the first byte of incoming serial data available (or -1 if no data is available).
*/
uint8_t UART::read()
{
	try {
		uint8_t ch = serialGetchar(_handle);
		return ch;
	}
	catch (...) {
		cerr << " Error in readBlock().";
		throw;
	}
}

/** Reads characters from the serial port into a buffer. The function terminates if the determined length has been read, or it times out.
@param buffer - the buffer to store the bytes in.
@param length - the number of bytes to read.
@return - the number of bytes placed in the buffer.
*/
int UART::read(uint8_t size, uint8_t * data)
{

	try {
		int bytesReadCount = ::read(_handle, data, size);
		return bytesReadCount;
	}
	catch (...) {
		cerr << " Error in readBlock().";
		throw;
	}
}

/** Writes a single byte to the serial port.
@param byte - a byte to send.
*/
void UART::write(uint8_t byte)
{
	try {
		serialPutchar(_handle, byte);
	}
	catch (...) {
		cerr << " Error in write().";
		throw 86;
	}
}

/** Sends the nul-terminated string to the serial device identified by the given file descriptor.
*/
void UART::write(char *string){
    try {
		serialPuts(_handle, string);
	}
	catch (...) {
		cerr << " Error in write().";
		throw 89;
	}
}

/** Writes series of bytes to the serial port; to send the characters representing the digits of a number use the print() function instead.
@param size - buffer's size
@param buffer - data
*/
void UART::write(uint8_t size, uint8_t *bytes) {
    try{
		::write(_handle, bytes, size);
    }
	catch (...) {
		cerr << " Error in write().";
		throw;
	}
}

Run

Start Arduino code. Output should be like this:

Build and run CodeBlocks program by clicking on the button with a yellow gear and green triangle:



Wait a minute as RPI is not very fast. After the program starts, the output will be:

Utility programs

Besides RealVNC client, download and install WinScp in order to move files between Your PC and RPI. Famous PuTTY can be also handy as it enables You to access Your RPI remotely using a command prompt, in case You have any problems with VNC. Win32DiskImager is a good tool to make images of Your SD occasionally. Do not skip that chore!

Previous lesson
Next lesson