Introduction: 3D Printed Automatic Pet Feeder

This instructable was created in fulfillment of the project requirement of the Makecourse at the University of South Florida (www.makecourse.com).

The benefit of this design is that it will dispense exactly one cup each interval, making it superior over other auger or open-close freefall style feeders.

This project is designed to dispense dry kibble in one cup increments two times a day. The feeding times and number of increments are adjustable but the cup size is not. If you have a Chihuahua or another small breed that requires 1/2 cup, you'll have to get another dog.

The "feed-now" feature was designed for training in mind. I firmly believe for obedience training dogs should work for their food. So if you catch the timer before it goes off you can make your dog do a trick or two and feed them manually. If you forget or miss it, that's okay too- they won't go hungry.

3D printed parts:

  1. Housing
  2. Top Funnel
  3. Internal Funnel
  4. Bottom Funnel
  5. Cylinder
  6. Cap 1
  7. Cap 2 (with notch)

Materials needed:

  1. Arduino Uno R3
  2. Female to female wires
  3. IR receiver
  4. IR remote
  5. 10k ohm resistor
  6. Super Glue
  7. AC/DC wall adapter
  8. Flexible cutting board (dollar store)
  9. 4 digit 7 segment display
  10. Hot glue sticks
  11. USB cable
  12. Stepper motor
  13. ULN2003 motor driver module
  14. Touch sensor
  15. 7x 470 ohm resistors
  16. SPDT switch (Radioshack #: 2750016)
  17. Pipe-clamp
  18. Water cooler bottle
  19. Zipties
  20. Wood to mount the bottle
  21. Electrical tape
  22. Touch Latch [I used Amerock Chrome Double Roller Catch And Latch from Ace 10/Pack (1888373)]
  23. L-Bracket
  24. U-Bracket
  25. Toggle bolt

Equipment Needed:

  1. 3D printer
  2. Screwdriver/Drill
  3. Computer with Inventor
  4. Soldering iron, flux and solder
  5. Hot glue gun
  6. Dremel

Step 1: 3D Printed Parts

  1. Gather all of the 3D printed parts. The zip file below contains all the parts necessary to 3D print this project.
  2. Housing part:
    • The 4 digit 7 segment display should fit snugly into the front hole of the (I've done this project twice: this worked perfectly my first printing, but I needed to use a Dremel to widen out the hole the second time I printed).
  3. Cylinder and Caps:
    • Attach Cap 1 to one side of the cylinder.
    • Attach Cap 2 to the other side, making sure that the protruding notch is in line with the opening of the cylinder.
    • Slide the fully assembled cylinder into the housing with the notch on the side of the arduino compartment.
    • Once the cylinder is in, you can slide the lever switch into the slot of the inter housing so that when the cylinder rotates, the notch on the cap will activate the switch. Note that when looking at the housing from the left side, the cylinder will rotate clockwise, so face the lever switch accordingly.
  4. Internal Funnel:
    • Using the cheap cutting board, cut two squares to fit on top of this funnel (this creates a slippery service for food to fall into the cylinder.
  5. Top Funnel:
    • Line up the top funnel with the solid-bottom covering the Arduino compartment of the Housing.
    • Using two hinges, align them in a way the top will be able to open
    • Drill pilot holes using the Dremel, and then attach the hinges with screws.
    • Take a Touch Latch roller and using the Dremel, cut part of the metal off so that it can fit on the front left wall of the housing without interfering with the cylinder.
    • Attach the corresponding latch to the bottom of the Top Funnel and screw it in, so that it will keep the top closed snugly
  6. Bottom Funnel:
    • For convenience, set this aside for now. It will be easier to work with a square bottom and to have access to the bottom opening of the internal funnel.

Step 2: Electrical Components- 4 Digit 7 Segment Display

  1. To begin, let's look at the fritzing diagram above. This is a screenshot from Dr. Rudy Schlaf's excellent video on the theory behind the display, and how to wire it. Link:
  2. If you're going to use my code, make note of these few adjustments: Move the contact at pin 8 to pin 1, and pin 9 to pin 14.
  3. Once you've verified your display works, use the female connectors directly to the pins on the display. Due to the claustrophobic nature of this project, I needed to solder the resistors where applicable instead of using a breadboard. I wrapped each resistor in electrical tape to avoid shorts.
  4. The cords can fit in the slit between the Arduino compartment and cylinder compartment.

Step 3: Electrical Components: Touch Sensor and Limiting Switch

  1. Take the touch sensor and slide it into the slit on the right side of the housing.
  2. Follow the diagram above and attach it to the arduino accordingly
  3. For the limiting switch, follow the schematic above for a normally closed connection. The correct pin will say NC on it. Note that this should be inputted to pin 8 to correlate with my code.

Step 4: Electrical Components: Stepper Motor

1. Take the stepper motor and attach the end into the tip of the cylinder. It should fit snuggly on the two nubs on the inside of the housing. I used moldable plastic I had on hand to lodge it in between the walls of the housing. Glue should work fine though.

2. Following the schematic above, attach the ribbon cables from In1,2,3,4 to arduino pins A1,A2,A3, and A4 respectively.

3. Attach the power rails from the arduino. I used a breadboard for the power rails as multiple 5V sources are needed.

4. Finally attach the cables from the stepper motor to the driver

Step 5: Electrical Components: IR Sensor

  1. Attach the IR sensor according to the diagram above with OUT going to A0.
  2. Download the following libraries from GitHub
  3. Open the IR decoder file and upload the code, making sure you change the IR pin to A0.
  4. Press the serial montor button on the arduino (magnifying glass in the corner) and begin pressing the buttons you'd like to use for your feeder.
  5. You will take these numbers and replace the numbers in my code which correlate to my remote.
  6. Drill a hole anywhere in the front of the housing for your IR receiver. I had enough of a cap between the housing and the top funnel to receive a signal so this may not be necessary.
  7. You will notice that I have a reset function in my code. This can be replicated by shorting pin 9 to the reset pin on the arduino. This was mostly for testing purposes but I have found it to be useful so I've kept it. Note that when uploading your code, you must disconnect this pin first or it will not upload correctly.

Step 6: Arduino Code

In order for our project to run we need to upload code to the Arduino to control the electrical components. Libraries are included in the zip file below, and the folders inside must be placed in the main Arduino library folder on your computer after unzipping. Included are libraries for the IR sensor, stepper motor, and the seven segment display. The attached zip file titled PetFeeder contains the folder with the Arduino sketch needed to run the code. This simply needs to be flashed to the Arduino in order for the code to run.

<br><p>#include <br>#include 
#include 
#include 
#include "SevSeg.h"
SevSeg sevseg;
#define STEPS_PER_MOTOR_REVOLUTION 32   
#define STEPS_PER_OUTPUT_REVOLUTION 32 * 64 // Sets the motor for one revolution
Stepper small_stepper(STEPS_PER_MOTOR_REVOLUTION, A1, A4, A2, A3);
const int touchPin = A5;         // Touch sensor assigned here
int touchState;                  // Reading of the touch sensor
int touched;                     // Condition if the touch sensor was pressed
int disableTouch;                // Disables multiple touches 
int feed;                        // Runs the feeding loop
int AMorPM=LOW;                  // Decides if feeding cycle is in the morning or evening used for setting the time
int justFed;                     // Condition used to set the time
int minutes;                     // For the timer
int i=0;                         // Used in formula for deciding if it's AM or PM
int disableMinutes;              // How long the touch sensor is disabled
const int limPin = 8;            // Limit sensor assigned here
int limState = HIGH;              // Limit state
int pressed = LOW;                 // How many times the limit sensor was pressed
int upright;
int IRpin = A0;  // pin for the IR sensor
IRrecv irrecv(IRpin);
decode_results results;
int singlecup;
int resetPin = 9;</p>

This initalizes the variables and sets certain states. If you're beginning the code in the evening, you will want to int AMorPM as HIGH.

<br><p>void setup()<br>{
  digitalWrite(resetPin, HIGH);
  delay(200);
  pinMode(limPin, INPUT);
  pinMode(touchPin, INPUT);
  pinMode(resetPin, OUTPUT);
  //Serial.begin(9600);
  Serial.println("reset");//print reset to know the program has been reset and 
  byte numDigits = 4;                                                 
  byte digitPins[] = {10, 11, 12, 13};                 //Set up for the 4 digit seven segment display
  byte segmentPins[] = {2, 3, 4, 5, 6, 7, 1, 14};</p><p>  sevseg.begin(COMMON_CATHODE, numDigits, digitPins, segmentPins);
  sevseg.setBrightness(90);
    irrecv.enableIRIn(); // Start the receiver</p><p>}</p>

This setup runs once and sets the pins as inputs or outputs. It also sets up the brightness of the seven segment display which can be adjusted.

<br><p>void loop() {<br>  static unsigned long startTime = millis();
  static int minutes = 480;                                           // Initializes for 480 minutes or 8 hours, since it is AM to                                                                                               begin with (9 am feeding -> 8 hours -> 5pm feeding)
  int justFed = LOW;
  touchState = digitalRead(touchPin);                                 // Checks if touchpin was pressed
  limState = digitalRead(limPin);
  while(upright == LOW && limState == LOW){        // This loop does a one-time check to see if the cylinder is    

											aligned, if not it will rotate until it's upright
  limState = digitalRead(limPin);
  small_stepper.setSpeed(380);                                        
  small_stepper.step(10);
    if (limState == HIGH){
             small_stepper.step(-60);</p><p>     upright = HIGH;
     Serial.println("switch hit");</p><p>     
    }
   
}</p><p>  if(touchState == LOW && disableTouch == LOW){                       
    touched=HIGH;
    feed=HIGH;
    delay(10);
        // Serial.println("Pad was touched");</p><p>  }</p>

This is the beginning of the main code. It begins by initializing how many minutes until the next feeding. If you're starting in the evening and have set AMorPM to HIGH, then you will also need to change the minutes here to 960 (or 16 hours until the next feeding). This also does an initial check to make sure the cylinder is upright when you turn the system on- the cylinder will rotate until the switch is hit. The bottom of the code states that if the capacitance of the touch sensor goes low (i.e. you pressed it) then it sets the feed loop high, and the touch will be disabled to avoid multiple feedings.

<p>if (irrecv.decode(&results)) {//has a transmission been received?<br>    Serial.println(results.value);//If yes: interpret the received commands...
if (results.value == 3810010651){
          feed = HIGH;
          touched = HIGH;
        }
        //Mark's refresh button 5316027
        //Joe's - digital zoom button
if (results.value == 2885434849){
disableTouch = LOW;
        }
if (results.value == 810318687){
  Serial.println("resetting");
  delay(10);
  digitalWrite(resetPin, LOW);
  Serial.println("this never happens");
}
       // Mark's one button 2534850111
       //Joe's timer on button
if (results.value == 1429432985){
 small_stepper.setSpeed(300);                      
  small_stepper.step(2000);
  pressed = LOW;
delay(3000);  // Initially has a large step to get off the limiting sensor (any less and the sensor registers multiple presses)
  while(pressed == LOW){
  small_stepper.setSpeed(450);                                        // This loop continues until the limiting sensor is pressed once (meaning the cylinder went around once for one cup)
  small_stepper.step(30);
  delay(200);                                                         // It will move forward 50 steps and back 20, to minimize jamming
  small_stepper.step(-10);
  limState = digitalRead(limPin);
    if (limState == HIGH){
     pressed = HIGH;                                           // Reads the limit pin and counts a press
     Serial.println("switch hit");
     delay(4500);
    }
  }
    small_stepper.setSpeed(500);
    small_stepper.step(-20);
    delay(10);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
    small_stepper.step(50);
    delay(500);
    pressed = LOW;
        }</p><p>    irrecv.resume(); // Receive the next value
  }</p>

This code correlates to the remote controls. These numbers will be replaced by the values you received from the IRdecoder. There is a function for running a feeding cycle, running an extra cup (for days where you think your pet needs the extra calories), and a function for resetting the whole system.

<p>if(feed == HIGH){                                    //Feeds Lydia<br>  
  small_stepper.setSpeed(300);                      
  small_stepper.step(2000);
  pressed = LOW;
delay(3000);  // Initially has a large step to get off the limiting sensor (any less and the sensor registers multiple presses)
  while(pressed == LOW){
  small_stepper.setSpeed(450);                                        // This loop continues until the limiting sensor is pressed once (meaning the cylinder went around once for one cup)
  small_stepper.step(30);
  delay(200);                                                         // It will move forward 50 steps and back 20, to minimize jamming
  small_stepper.step(-10);
  limState = digitalRead(limPin);
    if (limState == HIGH){
     pressed = HIGH;                                           // Reads the limit pin and counts a press
     Serial.println("switch hit");
     delay(4500);
    }
  }</p><p>    small_stepper.setSpeed(500);
    small_stepper.step(-20);
    delay(10);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
    small_stepper.step(50);
    delay(500);
  small_stepper.setSpeed(300);
  small_stepper.step(2000);
  delay(3000);
  pressed = LOW;
  while(pressed == LOW){</p><p>  small_stepper.setSpeed(450);   
  small_stepper.step(30);
  delay(200);
  small_stepper.step(-10);
  limState = digitalRead(limPin);
    if (limState == HIGH){
     pressed = HIGH;
     delay(500);
     //Serial.println("Pad was touched");
          Serial.println("switch hit");</p><p>    }
  }
    small_stepper.setSpeed(500);
    small_stepper.step(-25);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    delay(500);
        small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    small_stepper.setSpeed(500);
    small_stepper.step(-50);
    small_stepper.setSpeed(500);
    small_stepper.step(50);
    delay(500);
    pressed = LOW;
  feed = LOW;
  i = i + 1;                                                           // Tells the feeder it's moving on to the next cycle (AMorPM)
  justFed = HIGH;
  }</p>

This is the most important part of the code- the feeding cycle. It begins by taking a huge "step" to move most of the way around the system. It then sets pressed to low, which correlates to the limiting switch. If it doesn't make it all the way around then there was a jam, so it runs through an un-jamming while-loop. It will begin to take 30 steps forward and 10 steps back until the limiting switch it hit. Once at the top, it will move back and forth to help shake food down into the cylinder. This cycle will then begin again, to feed my pet a total of two cups. This can be adjusted for your pet by deleting the second cycle.

<p>if(disableTouch == HIGH && minutes == 960 && AMorPM == HIGH){        // If the touch sensor is ever pressed, it becomes disabled. This will allow it to be pressed again once a new feeding cycle has begun.<br>  disableTouch = LOW;
}
  if(disableTouch == HIGH && minutes == 480 && AMorPM == LOW){
  disableTouch = LOW;
}</p>

If the touch sensor was pressed for an early feeding, then the disableTouch feature is activated. I call this my "Greg-proof feature." I have a friend named Greg who, I know, would spend the time pressing the touch sensor over and over again until all 20 lbs of dog food would be scattered across my floor. The touch sensor will be activated again once the early-feeding time has elapsed, and it goes back to it's regular scheduling.

<p>if (millis() >= startTime) {<br>    minutes--;                                                         // 60000 seconds is equal to 1 min
    startTime += 60000; 
    if (minutes == 0) {                                                // Runs the feeding cycle if the timer reaches zero
      feed = HIGH;
      disableTouch=LOW;                                                
      touched = LOW;
    }
    sevseg.setNumber(minutes, 1);
  }</p>

This if- function is the timer function. It will countdown by the minute, and if the minutes equal zero, it will run the feed function, and disable the Greg-proof feature.

<p>if(i%2 == 1){                                                          // Conditional statement to decide if it is AM or PM odd cycles a                                                                                     re PM<br>  AMorPM = HIGH;
}
else if(i%2 == 0){
  AMorPM=LOW;}</p>

This is the part of the code that switches between the two feeding cycles. Each time the feeding cycle runs "i" will increase by one. This states that when "i" is divided by and and has a remainer (odd), it will switch to the evening feed cycle and vice-versa. This correlates to resetting the timer.

<p>if(justFed == HIGH && touched == LOW && AMorPM == LOW){                // Conditions for setting the time and disabling the touch sensor.<br>  minutes = 480;
  justFed = LOW;
 }
else if(justFed == HIGH && touched == LOW && AMorPM == HIGH){
  minutes = 960;
  justFed = LOW;
}
else if(justFed == HIGH && touched == HIGH && AMorPM == LOW){
  minutes = 480 + minutes;
  justFed = LOW;
  disableTouch = HIGH;</p><p>sevseg.refreshDisplay();</p><p>
}
else if(justFed == HIGH && touched == HIGH && AMorPM == HIGH){
  minutes = 960 + minutes;
  justFed = LOW;
  disableTouch = HIGH;</p><p>sevseg.refreshDisplay();</p><p>}
sevseg.refreshDisplay(); // Must run repeatedly
}</p>

This is the end of the code which resets the timer to the appropriate time. If the touch sensor was pressed, it will take the time that was left and add it to the next feeding time. This is so the feeder is never off schedule. If the timer runs down to zero, the time will simply be set to the next feeding time. The sevseg.refreshDisplay(); is what tells the timer to refresh.

Step 7: Assembly

  1. Take the Zephyrhills water bottle and the wood you'd like to mount it on. Drill four holes in the board so that you can run zipties through the wood and around the handle of the bottle.
  2. Put a pipe-clamp around the Top Funnel.
  3. I found it's best to wrap a couple layers of duct tape around the nozzle of the bottle, to add grip and make the fitting snug.
  4. Slide the feeder onto the nozzle and secure with a pipe-clamp by tightening with a flat-head.
  5. If you haven't attached the bottom funnel, this may be done now. It does not bear much weight, so hotglue will suffice.
  6. Attach the L-Bracket to the top of the wood.
  7. Using a toggle bolt attach a U-bracket to the wall, any variation of a french cleat will do though. The L-bracket will then attach to the U-bracket, making it removable so you can refill the container with food.