DesignSpark Electrical Logolinkedin
Menu Search
Ask a Question

Arduino entry - Development of WEEDINATOR Agricultural Robot

 

The project started back in the Summer of 2017 on Autodesk Fusion 360 software, but it did not start off looking like the image above! More like below:

...... As can be seen, at this stage it looks pretty rubbish ..... But would it have worked and why was this design not used 'AS IS'?

Simultaneously, I was thinking about the control system. There appeared to be a few different options, namely Arduino type MCUs, Raspberry Pi type small computers or something more elaborate running ROS (Robot Operating System). There are advantages and disadvantages to each of the 3 options, but I chose the Arduino type because it's simple, reliable, has over 50 inputs/outputs and potentially very fast. I'd seen an expensive 3 core Arduino type MCU called the 'Shield Buddy TC275' which was specifically designed for robots / autonomous vehicles and thought this would be great for controlling the motors as it would be able to run debug code and print to the serial console completely independently of the actual code running the motors. I imagined that the code for the motors would have to run on one timer at a very fast speed with no delays or pauses for anything else. Using just one timer ensures that everything is synchronous - each and every motor is in sync with the others. The main disadvantage that I could think of was compatibility with existing DIY shields such as Adafruit TFT screens and accelerometer boards ..... But more about this later!

Back to the mechanicals ....... Let's pick apart the simple steering design above. Firstly, the steering is offset from the ideal Z axis which would ideally run down through the centre of the wheel, which is quite common on farm tractors. Road going cars tend to have quite elaborate mechanisms located right inside the hub of the wheel as they go faster and need more precise control for high-speed handling. The biggest perceived problem was 'Would the steering motor be powerful enough to overcome forces induced by the drive wheels?'. At this stage, I had no real answer to this question, but later on, the problem did resolve itself ok.

Next question was 'Did I really want to use belts/chains?'. They have the inherent problem of needing to be adjusted and needing complicated panels bolted on to protect against dirt/dust/fingers/hands.

The last major criticism was that the design was not compact and looked like the mechanism might infringe too much onto the crops growing in between the wheels. Plants will tolerate quite a bit of gentle scuffing on the leaves as the machine drives slowly by, but this needed to be minimised as much as possible. Eventually, after many hours/days manipulating the design on Fusion 360, it looked more like the image below:

I actually built this mechanism as below:

Upon testing, the steering and drive system worked ok but the suspension was absolutely useless!

Rather than re-invent the proverbial wheel, I decided to copy the 'suspension' system on my old International Harvester tractor:

..... So the front wheels are now mounted on a rigid beam as shown above, which swivels on a large pin located in the centre of the beam.

This system ended up working really well and there has been no need, as yet, for joining the steering together with a tie bar.

It was important to test this mechanism before carrying on with anything more complicated like the weeding mechanism, so a PCB was put together with a 'wish list' of all the features that might be needed:

  • TFT display
  • Accelerometer and magnetic compass
  • High-resolution satellite receiver
  • GPRS module for communicating with the internet via cell phone network
  • Speech module for warning people to get out of the way and scaring off pigeons
  • Arduino Mega MCU to help with shield/software compatibility

Additionally, there should be cameras, LIDAR sensors, live wire following sensors etc.

One of the big problems was getting the Arduino Mega to communicate reliably with the TC275 without crashing. My original code attempted to parse data coming out of the I2C bus by translating the CHARACTERS to STRINGS to INTEGERS / FLOATS. It worked ok but did tend to crash quite often. Here's an excerpt from my really bad code:

void receiveEvent(int howMany) // Recieves lat and long data from FONA via TC275 for calculating bearings and distances.
{
  //Serial.println("Here is data from TC275: "); 
  while (Wire.available())
  {
    char c = Wire.read(); // receive byte as a character
   // Serial.print(c); //Serial.print("Q");          // print the character
      if (isAlpha(c))         // analyse c for letters
      {
      //Serial.print("LETTER");
      a=a+c;                // string = string + character
      if (a=="LAT")
      {
       lon="";
       lat=a;
       //Serial.print(" Trigger word LAT detected!: ");//Serial.println(b);
       a="";
      } 
      if (a=="LONG")
      {
       lat="";
       lon=a;
       //Serial.print(" Trigger word LONG detected!: ");//Serial.println(d);
       a="";
      } 
    } 
      if (lat=="LAT")
      {
        if (isDigit(c))         // analyse c for numerical digit
        {
        latString=latString+c;                // string = string + character
        }
      }
      if (lon=="LONG")
      {
        if (isDigit(c))         // analyse c for numerical digit
        {
        lonString=lonString+c;                // string = string + character
        }
      }
  }    // While Loop
  result=(latString).toInt();
  latitude = result;
  result=(lonString).toInt();
  longitude = result *-1;
  //Serial.print("latString: ");Serial.println(latString);
  //Serial.print("lonString: ");Serial.println(lonString);
  //Serial.print("latitude integer from Fona: ");Serial.println(latitude);
  //Serial.print("longitude integer from Fona: ");Serial.println(longitude);
  //Serial.println();
  //NeoGPS::Location_t base( latitude, longitude); // Llangefni
}
void lookForLettersAndDigits()
{
  lookForLetters();
  lookForDigits(); 
  lookForLatitude();
  lookForLongitude();
}
void lookForLetters()
{
    if (isAlpha(c))         // analyse c for letters
    {
      //Serial.print("LETTER");
      a=a+c;                // string = string + character
      if (a=="LAT")
      {
       lon="";
       lat=a;
       //Serial.print("Trigger word LAT detected!: ");//Serial.println(b);
       a="";
      } 
      if (a=="LON")
      {
       lat="";
       lon=a;
       //Serial.print("Trigger word LON detected!: ");//Serial.println(d);
       a="";
      } 
    } 
}
void lookForDigits()
{
  if (lat=="LAT")
  {
    if (isDigit(c))         // analyse c for numerical digit
    {
      latString=latString+c;                // string = string + character
    }
  }
  if (lon=="LON")
  {
    if (isDigit(c))         // analyse c for numerical digit
    {
      lonString=lonString+c;                // string = string + character
    }
  }
}
void lookForLatitude()
{
  if (lat=="LAT")
  {
      result=(latString).toInt();
      latitude = result;
      //Serial.println("");
      //Serial.print("Latitude: ");Serial.println(latitude);
  }
}
void lookForLongitude()
{
  if (lon=="LON")
  {
      result=(lonString).toInt();
      longitude = result;
      //Serial.println("");
      //Serial.print("Longitude: ");Serial.println(longitude);
  }
}
void characterCompileA() // For sending Ublox bearing and distance data to TC275
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This is where the data is compiled into a character ....
  dataString =  initiator  + "BEAR" + zbearing + "DIST" + zdistance + "COMP" + compass;  // Limited to 32 characters for some reason !!! ..... + ",LON" + yy .... Removed.
  int n = dataString.length();
  //Serial.print("Data string to send:     "); Serial.println(dataString);   
  //Serial.print("Size of string:  "); Serial.println(n);
  // Builds the url character:
      for (int aa=0;aa<=n;aa++)                                              
      {
          url[aa] = dataString[aa];
      }
  // DEBUG_PORT.print("Character data to send:  ");// DEBUG_PORT.println(url);
  // DEBUG_PORT.println("");
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////  
}
void characterCompileB() // For sending Ublox lat and long to TC275
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This is where the data is compiled into a character ....
  dataString =  initiator  + "LOON" + yy + "LAAT" + zz;  // Limited to 32 characters for some reason !!! ..... + ",LON" + yy .... Removed.
  int n = dataString.length();
  // DEBUG_PORT.print("Data string to send:     ");// DEBUG_PORT.println(dataString);   
  // DEBUG_PORT.print("Size of string:  ");// DEBUG_PORT.println(n);
  // Builds the url character:
      for (int aa=0;aa<=n;aa++)                                              
      {
          url[aa] = dataString[aa];
      }
  // DEBUG_PORT.print("Character data to send:  ");// DEBUG_PORT.println(url);
  // DEBUG_PORT.println("");
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////  
}

A professional code writer would despair at the above code, which is exactly what happened! ...... A stroke of good luck ...... A pro coder joined the project and re-vamped all the code for communications between the GPS, the Mega and the TC275. We now have effective communications. Full details are on Github HERE.

The first version of the PCB for the control system looked like the above but was soon superceded with version 2 below. There were far too many slots for Arduino Nanos, which just were never needed and only one Arduino MCU. The annotated diagram below looks complicated, but is essentially ten or so modules built by other people bolted onto one PCB and joined together to talk amongst themselves:

Fortunately, it all works fairly well with only the occasional jumper wire required.

At various stages in the project the question 'What are we doing this for?' arises, followed by 'Is this just an expensive toy' and 'Does it have any real use in the real world?' After applying for various UK technology grants, including one specifically for autonomous vehicles, the conclusion is that there is currently no market for this type of machine, as no other machine like it exists in the marketplace to make a good comparison. It is not similar to any high tech tractor as it is deliberately small and cheap to build. On a farm the machines would be used as, kind of, swarms rather than having just one or two very big expensive machines. The concept is extremely risky, but has a number of key advantages:

  • No drivers required, which saves cost and boredom
  • Can easily work on small farms with small fields and narrow approach roads
  • There's no 'proprietary', 'black box' software or electronics so mechanics can easily learn to fix stuff
  • Can be built, under a lenient license, by independent factories ie NOT John Beer.
  • Can work on the soil much more gently with less destruction to soil structure, soil fauna/flora

At this stage there existed a basic, working, machine that could drive around a little bit:

The main problems at this stage were:

  • The tyres would grind against the ground due to lack of differential
  • The steering motors would get overloaded when trying to steer when the machine was stationary
  • The drive motors would only work effectively at extremely low speeds

Over time, the drive motors were upgraded from stepper motors to servo motors to DC brushed motors with more basic control. The servo motors with 'inbuilt' optical encoder control were great for driving along beds of crops at high precision but terrible for general driving about on rough terrain. The answer seems to be to use 'homebrewed' optical encoder feedback systems which can be changed between 'rough terrain' and 'precision farming'.

The other problems were solved by programming the TC275 to take account of the steering geometry. There was no need to use complicated maths as using simple 'directly proportional' or 'linear' equations proved to be more than adequate. Nonetheless, it took a whole week for my small brain to work it all out. I ended up with a whole load of 'if' conditionals which are actually fairly easy to debug and reconfigure. Example:

void differential()
{
  unsigned long currentMicrosOne = micros();
  unsigned long currentMicrosTwo = micros(); 
  unsigned long currentMicrosThree = micros();
  unsigned long currentMicrosFour = micros();
  if((difference>300)&&(wheelsPosition>=0)) // Clockwise from the centre on RH lock. 300 is deadzone.
  {
    intervalOne = 2500; // Speed of RH inner wheel turn (fast) higher the value, slower the speed. Min 1250.
    intervalTwo = 2000; // Speed of LH outer wheel turn (slow) higher the value, slower the speed. Min 1250.
    clockWise();
    if (currentMicrosTwo - previousMicrosTwo >= intervalTwo)
    {
      changeStateTwo();
      digitalWrite(7,ledStateTwo); //STEP
      wheelsPosition++;
      previousFinalSteeringValue++; 
    }
    if (currentMicrosOne - previousMicrosOne >= intervalOne)
    {
      changeStateOne();
      digitalWrite(5,ledStateOne); //STEP
    }
  }
//////////////////////////////////////////////////////////////////////////////////////////////////////  
  if((difference>300)&&(wheelsPosition<0)) // Clockwise from the full LH lock position 
  {
    intervalOne = 2500; // Speed of RH outer wheel turn (slow) higher the value, slower the speed. Min 1250.
    intervalTwo = 2000; // Speed of LH inner wheel turn (fast) higher the value, slower the speed. Min 1250.
    clockWise();
    if (currentMicrosOne - previousMicrosOne >= intervalOne)
    {
      changeStateOne();
      digitalWrite(7,ledStateOne); //STEP
    }
    if (currentMicrosTwo - previousMicrosTwo >= intervalTwo)
    {
      changeStateTwo();
      digitalWrite(5,ledStateTwo); //STEP
      wheelsPosition++;
      previousFinalSteeringValue++;
    }
  }
//////////////////////////////////////////////////////////////////////////////////////////////////////
  if((difference<-300)&&(wheelsPosition>=0)) // Anti-clockwise from the full RH lock position 
  {
    intervalOne = 2500; // Speed of RH inner wheel turn (fast) higher the value, slower the speed. Min 1250.
    intervalTwo = 2000; // Speed of LH outer wheel turn (slow) higher the value, slower the speed. Min 1250.
    antiClockWise();
    if (currentMicrosTwo - previousMicrosTwo >= intervalTwo)
    {
      changeStateTwo();
      digitalWrite(7,ledStateTwo); //STEP
      wheelsPosition--;
      previousFinalSteeringValue--;
    }
    if (currentMicrosOne - previousMicrosOne >= intervalOne)
    {
      changeStateOne();
      digitalWrite(5,ledStateOne); //STEP
    }
  }
//////////////////////////////////////////////////////////////////////////////////////////////////////
  if((difference<-300)&&(wheelsPosition<0)) // Anti-clockwise from the centre 
  {
    intervalOne = 2500; // Speed of RH outer wheel turn (slow) higher the value, slower the speed. Min 1250.
    intervalTwo = 2000; // Speed of LH inner wheel turn (fast) higher the value, slower the speed. Min 1250.
    antiClockWise();
    if (currentMicrosOne - previousMicrosOne >= intervalOne)
    {
      changeStateOne();
      digitalWrite(7,ledStateOne); //STEP
    }
    if (currentMicrosTwo - previousMicrosTwo >= intervalTwo)
    {
      changeStateTwo();
      digitalWrite(5,ledStateTwo); //STEP
      wheelsPosition--;
      previousFinalSteeringValue--;
    }
  }
}    // void differential

Splitting the sketches into rows of tabs greatly helps when making adjustments and additions to the code as below:

 

The files are available on Github HERE. 

At this point in time, the machine could be driven around at a reasonable speed using manual control, but ready for upgrading to autonomous control:

Switching to autonomous control was surprisingly easy. First, I used a basic waypoint system where the machine downloaded a set of coordinates from the internet and then travelled to that point, stopped and then downloaded a new set. The obvious problem was that the machine had to stop at each waypoint. We solved this by downloading the next set of coordinates in advance of the machine arriving at the current destination. 

Accuracy for driving with satellites was about +-50mm depending on how leafy the trees were! In the middle of Summer, navigation was less reliable under the trees. Using the Pixy2 camera proved to be quite effective as it features line following and barcode recognition:

The rope does, however, get easily obscured by grass towards the end of the growing season and so navigation following a wire carrying an AC frequency of 35 kHz is now being explored with very promising results. Barcodes are still ok though.

Back to the mechanical details ........ The machine is designed to house a CNC machine which, in 2018, will perform weeding routines. The crops are carefully planted in precise grids and the machine has successfully navigated along the beds autonomously, especially when the servo drive motors were still in use and the white rope was not obscured by grass:

It's just a CNC machine, although probably more compact than would normally be seen. The biggest difference from the normal setup is that no libraries are used for controlling the 4 axes - it's all 'hard coded' from scratch, not that it's particularly complicated. Here's an example of the code:

void opZero()
{
  if(operationZero==false)                                      // current operation not yet performed.
  {
    operationZero = true;
    XZeroingStep=0;
    YZeroingStep=0;
    ZZeroingStep=0;
  }
}
void opOne()
{
  speedCNCX = 2000;                                            // 1 = one Hz.
  if(operationOne==false)                                      // current operation not yet performed.
  {
    operationNumber = "one";
    XZeroingStepsTotal = 17881;                                // one grid
    dirStateCNCX = LOW;                                        // LOW is forwards.
    if((XZeroingStep > 2000)||(XZeroingStep < 15000))          // Acceleration
    {
      speedCNCX = 3000;
    }
    if(XZeroingStepsTotal > XZeroingStep)
    {
      CNC_TIMER_X();
    }   
  }             // if(operationOne == false)
  if((XZeroingStepsTotal <= XZeroingStep)&&(operationOne==false))                    // Tells us that operationOne is complete.
  {
    operationOne=true;
    XZeroingStep=0;
    YZeroingStep=0;
    ZZeroingStep=0;
  }
}
void opTwo()
{
  if(operationOne==true)                                         // Operation one has been completed.
  {
    if(finalCurrentSensorValueRAxis<535)   // This value (530) is higher than in next operation to get claw down faster.
    {
      dirStateCNCZ = HIGH;                                     // HIGH is down.
      speedCNCZ = 1000;                                         // 1 = one Hz.
      CNC_TIMER_Z();
    }
    else
    {
      dirStateCNCZ = LOW;                                      // LOW is up.
      speedCNCZ = 1000;                                        // 1 = one Hz.
      CNC_TIMER_Z();
    } 
    
    speedCNCY = 300;                                             // 1 = one Hz. Allow time for claw to lower.
    CNC_TIMER_R();                                               // R axis motor claw starts turning.
    if(operationTwo==false)
    {
      operationNumber = "two";
      YZeroingStepsTotal = 26822;                                // One and a half grids.
      dirStateCNCY = LOW;                                        // LOW is left.
      if(YZeroingStepsTotal > YZeroingStep)
      {
        CNC_TIMER_Y();
      }   
    }     // if(operationTwo == false)
    if((YZeroingStepsTotal <= YZeroingStep)&&(operationTwo==false))                    // Tells us that operationOne is complete.
    {
      operationTwo=true;
      YZeroingStep=0;
    }
  }      //if(operationOne==true)
}
////////////////////////////////////////////////////////////////
// opThree is the start of the main weeding routine loop.
void opThree()
{
  if((operationTwo==true)&&(move2ColumnsForwards == true)&&(controlState==HIGH))      // move2ColumnsForwards is made true in op21.
  {
    //DEBUG_PORT.print("line 1765 reached. ");DEBUG_PORT.println(moveStepsForwards);
    //DEBUG_PORT.print("move2ColumnsForwards = ");DEBUG_PORT.println(moveStepsForwards);
    move2ColumnsForward();                                               // move forwards one column only after first CNC loop.
  }
  if((operationTwo==true)&&(move2ColumnsForwards == false))       // Operation two and move forwards has been completed.
  {
    //DEBUG_PORT.print("operationTwo = ");DEBUG_PORT.println(operationTwo);
    weedingBegin = true;
    CNC_TIMER_R();                                               // Enable claw rotation.
    speedCNCX = 500;                                             // 1 = one Hz.
    if(operationThree==false)                                     // current operation not yet performed.
    {
      operationNumber = "three";
      XZeroingStepsTotal = 35763;                                // 2 grids.
      if((XZeroingStep > 2000)||(XZeroingStep < 30000))          // Acceleration
      {
      speedCNCX = 1500;
      }
      dirStateCNCX = HIGH;                                        // LOW is forwards.
      if(XZeroingStepsTotal > XZeroingStep)
      {
        CNC_TIMER_X();
      }   
    }     // if(operationThree == false)
    if((XZeroingStepsTotal <= XZeroingStep)&&(operationThree==false))                    // Tells us that operationThree is complete.
    {
      operationThree=true;
      XZeroingStep=0;
    }
  }      //if(operationOne==true)
}
void opFour()
{
  if(operationThree==true)                                       // Previous operation has been completed.
  {
    CNC_TIMER_R();                                               // Enable claw rotation.
    speedCNCY = 1000;                                             // 1 = one Hz.
    if(operationFour==false)                                     // current operation not yet performed.
    {
      operationNumber = "four";
      YZeroingStepsTotal = 53645;                                // 3 grid
      dirStateCNCY = HIGH;                                        // LOW is left.
      if(YZeroingStepsTotal > YZeroingStep)
      {
        CNC_TIMER_Y();
      }   
    }     // if(operationTwo == false)
    if((YZeroingStepsTotal <= YZeroingStep)&&(operationFour==false))                    // Tells us that current operation is complete.
    {
      operationFour=true;
      YZeroingStep=0;
    }
  }      //if(operationOne==true)
}

This code, along with the code for controlling the drive and steering motors, is all located on core 0 on the TC275 and the processor breezes through it without any perceivable problems and the motors run nice and smoothly. Note that there are no 'delay' or'while' functions used which would otherwise slow things down pretty badly.

It's now September 2018 and the project has been going for just over one whole year. Currently, there's a whole load of small problems to solve before the next major task - planting - can be tackled:

  • The machine won't drive downhill due to the drive motors acting as generators, with nowhere for the generated current to go/get dissipated.
  • There's no rotary encoder facility on the drive motor for accurate positioning on the bed of vegetables.
  • Steering motors are probably slightly underpowered.
  • Satellite navigation receivers could be upgraded to Ublox version 9 sometime soon.
  • Use a small jockey wheel to keep live wire sensors at the correct height above the ground.
  • Add spring tines to the rotating claws to absorb the force of hitting a buried stone.
  • Speed up CNC mechanism and add acceleration/deceleration.

.... Plenty of work for 2019!

More details/instructions for the project are HERE.

Downloads

Ugly pirate roaming the seas in search of Treasure.

2 Nov 2018, 8:57