My first 3D printer was a CTC generation 2 clone of MakerBot ‘The Replicator Dual’, circa 2012. It died a little while back and has since been replaced with a Prusa i3 MK3S+. Not one to waste any bits, I disassembled/broke down the 3D printer into component pieces, looking for inspiration for a project - one made mainly from these bits.
And I came up with the Roverling. I’m attempting to make a wheeled platform, mainly from these bits, that can be used in a multitasking environment and for further development. I’m designing and making it on the fly so I don’t have an overall picture or plan.
The basic design consists of 4 independently driven wheels using stepper motors. The plan is to use the differential drive to maneuver and steer (untested). I would use servos for steering, except I’m trying to use just what’s available as much as possible. I intend to develop some MicroPython modules that will allow easy control via an RC remote. I’ll probably stick a bunch of sensors on board too.
- Disassemble an old 3D printer or otherwise procure these parts:
- 8x Silver steel 8mm rods (mine: 2x 420mm, 2x 300mm, 4x 100mm)
- 4x NEMA17 Stepper motors and cables
- M3 bolts, washers, spring washers & nuts of various sizes (35, 20)
- Cable ties: 100 x 25mm
Frame
- The base size of my design was driven by the rods available, yours will vary.
Print the corner brackets and motor mounts. I print them solid and it takes 24 hours on my setup to do a full set of 4. Make sure you have enough filament (mine ran out in the early hours of the morning, which is why they are bi-coloured). - Design files are in FreeCAD format + 3D models in step format + 3mf Prusa format. My printer is a Prusa i3 and I use generic branded filaments.
-
You will need to use lots of supports, which are a pain to clean up. All the 8mm holes also have an opposite 4mm hole. After picking off what you can drill these holes (4 on the corner bracket, one on the motor bracket) using a 4mm bit. It helps to loosen it up. I then drill, by hand, to 7.5 mm at the opening, being really careful not to go through. I don’t pull the trigger on the drill, I only rotate the part back and forth by hand until all the muck is out. Also, I find that drilling in reverse works well.
-
The rods need to fit tightly, but not so much that it cracks the bracket. If there is no muck inside it will work well as there are expansion slots built in.
-
Test each piece to ensure the rod can be pushed in all the way to the end. You can easily tell by looking into the 4mm push-out holes.
- These are the parts you should have ready at this stage (not all steppers are shown)
-
Test each piece to ensure the rod can be pushed in all the way to the end. You can easily tell by looking into the 4mm push-out holes.
-
Now for the first assembly test prior to gluing.
-
First, insert the small rod into the bracket hole that did not have any supports printed, this is the ‘vertical’ hole.
-
The horizontal rods can now be pushed in - all the way until you hear them hit the vertical rod.
- Repeat for all corners. Lay flat on a level surface and square up. Measure corner to corner to be certain.
-
If it all works out, undo it carefully and glue it all back together.
-
I have used CA (super glue) as all the fits are tight and CA works best in these situations. I used medium but would have preferred light if I had it on hand.
-
The short rods are fastened first. Put glue in the hole and around the edge and get the rod in quickly. Use a hammer. Wear safety glasses in case of spray from glue. I have suffered super glue in the eye in exactly the same type of fixing situation. You don't want it to happen to you.
-
Then each short rod is between two corners. Once the second piece is pressed in, quickly flatten (on the bench/table top) so the tops are parallel. If you don't do it quickly enough the glue will set in the incorrect position.
-
Then fix two long rods to one of the short ends.
-
Put glue into the two holes on the remaining short side.
-
Insert two rods from the other assembly into the holes quickly. Use a hammer if you need to and then immediately flatten out on the bench top to ensure all 4 tops are parallel.
-
Let the glue cure.
- First print the 4 wheels. No supports are necessary, I used minimal fill except for the center section which is solid. On my printer, it takes around 8 hours per wheel. I’ve never printed in flexible materials, but will learn if I find that I need to to improve wheels in this way.
- Fitting the motor to the wheel is tricky as it is a very tight fit. Once the keys are aligned I use a punch and a small hammer on the back shaft of the stepper to drive the shaft into the wheel.
- It is seated correctly once the shaft is flush with the front of the wheel.
- Remove 4 bolts from steppers. Mine are only 25mm long and need to be replaced with M3 x 35mm. Attach the bracket using washers and spring washers.
- The cable can be fitted later (with some difficulty) or now (but getting in the way)
Finished wheel assembly. (stepper cable entry from top)
- First test fit to ensure all parts fit well. Make sure everything is in alignment and then glue with CA, the same way you did with the corner brackets.
- If in practice the wheels aren’t quite parallel, use a shim or washers at the mounting bolt/s to rectify.
- Use heat shrink and cable ties to secure the cable, will design a clip later.
- BTW, nothing to stop you from mounting the frame higher using longer silver steel rods, or turning all the wheels 180 degrees for a smaller overall footprint.
- Now print the 4 platform pieces, there are two required of each type. Carefully snap them onto the rods. I used PETG in a ‘natural’ colour. If you attach them straight after the print, whilst it’s still hot, you’ll have great success.
- The sides can be secured more firmly with cable ties. The panels should be secured to each other using the slots provided in the center ribs, with appropriate nuts, bolts, and washers, being M3x25, two washers, and one nut.
- I considered gluing, but haven’t at this stage as I believe some flexibility in the frame would be beneficial.
- This is my first attempt ever at printing with FLEX. Same as designing a tyre.
- Using Bilby 1.75mm black FLEX.
- Originally I used the ‘SemiFlex’ profile, however, my first one took 17 hours to print at a volumetric rate of 1.35 m/s/s with a 25% fill.
- I now use ‘SemiFlex’ and ‘0.3mm draft’ settings on my Prusa with only the followings mods:
- Bottom solid layers: 5
- Top Solid layers: 7 (because of the internal invisible bridge)
- Perimeters: 3
- Fill: 0%. Note the hollow cross section in the simulation pic - important
- Max volumetric speed: 2.7 mm3/sec - This is the critical parameter, speed will take care of itself.
- Just press tyres onto the wheels, I use CA to secure them.
Power Source
- In the past, I have used many different types of fixed and portable power sources. For this project, I decided to use what I have already available.
- I have a plethora of power tools in the farm shed, workshop, and office. With these, collected over many years of renovating, comes a number of battery capacities at 18V, ranging from 2 - 6 Ah, as well as numerous chargers. Why not use these handy power sources for other projects - including this one? So I designed and made this adapter
- All design files and build notes can be found here
-
My initial idea was to use a power source of just below 30V, mainly for efficiency reasons, but now I’ll design with a nominal 18V in mind. I found that the battery packs I’m using actually contain 5 x 16650. So expect voltages between 20.5V and no load, down to 15V, at which point the batteries are considered exhausted and any load should be removed.
-
Install your power source, somewhere near the center of the platform. I used 2x M3x20 bolts, nuts, and washers. Tie down, and clean up motor wiring. Bring the harness to a point near the power supply where the driver board will be mounted.
Stepper Drivers
-
The motherboard from my printer contained 5 stepper motor drivers plugged into the sockets at the top right of the MB. You will need to unplug 4.
- They contain an A4988 driver chip, however, there is no trim pot to set Vref and the corresponding motor current. It took me a bit to figure out that the mode pin MS3 is not wired to the corresponding pin on the A4988 but instead to the Vref pin. So leaving it floating means low current and no torque.
- Testing has confirmed that the current limit per phase, in mA, is equal to (Vref * 315). I’m using a PWM 100 kHz output piped through a 10 Hz low pass filter to produce Vref. Extrapolating further, PWM duty = 65000 * ILIMIT (A) is about right.
- During testing, I ran one unloaded motor for about 5 minutes at 1 kHz (1000 steps/sec, 200 steps per rotation, 5 rotations per second, 5 * 60 = 300 RPM, I think). I used the maximum current limit during the entire time, around an amp per phase. It ran nice and quietly for a while and then started vibrating due to the motor mount melting. Something that needs to be monitored and managed carefully.
Power & Driver Board Parts
- 4x A4988 driver module
- 4x 100uF electrolytic (min, I used 470u)
- 2x 47uF electrolytic
- 1x CE08607 RP-2040-zero
- 1x DFR0568 DC-DC Automatic Step Up-down Power Module (2.5~15V to 3.3V 600mA)
- 1x DFR0569 DC-DC Automatic Step Up-down Power Module (3~15V to 5V 600mA)
- 9x 100n monolithic capacitors
- 1x 160k resistor
- 1x Power switch, SPST 10A (with LED optional)
- 1x Fuse holder and Fuse, 10A (final value TBA)
- 2x 8-pin screw connectors
- 1x 2-pin screw terminal
- 6x 3pin for RC rx channels
- 1x Sockets for modules, if you want (I cut mine up from old ones)
- 1x 12 x 18cm protoboard.
- 1x Mounting bracket (printed) for main power switch and fuse.
- 6x 3-pin Servo cables
- 1x RC transmitter & receiver
The switch and fuse holder is press fit and/or glued to the platform.
- Wire up the battery, switch, fuse, and power cables to PCB. Use fat wires. Then test for correct voltage/polarity. Also, wire up the LED switch (if there is one).
- Then use heat shrink and a lot of hot glue to ensure all exposed wires are fully insulated.
Protoboard Layout - Tips
- I’ll be doing free-form wiring on a solderable protoboard.
- Find suitable sites for the modules. Best done once you’ve roughed in the wiring to make connections easier.
- At about the same time figure out when you want your PCB standoffs / mounting bolts. I used plastic standoffs and M3 bolts to secure the PCB to the platform. I drilled 4mm holes. And make sure you are clear of any wiring.
- I have used sockets for most modules.
- I’ve left a ton of room for future expansion on this board.
- After marking out wiring positions, remove PCB from the platform so we can wire & solder up and remove all modules from sockets
- I prefer to wire all power nets first, on the underside of the board.
- Then using a regulated and current-limited supply, check for the correct voltage on all pins connected to a power net.
- Wire & solder up all remaining nets.
- Install the completed board on the platform and plug in the motors
- Install and connect up the RC receiver. I have used an old 6-channel RC receiver for testing and future learning functions.
- I’ve used hot glue on the screw connectors and MCU so that disconnecting/reconnecting during testing won’t dislodge/break anything.
- The schematic is pretty straightforward.
- VREF is produced through 100kHz PWM output through LPF made from R15 and 100n caps.
- If your stepper driver has an onboard pot for Vref, then don’t wire these VREF nets and instead tie MS2 low.
- 6x RC inputs through voltage divider (4k7 / 10k) to translate 5V down to 3V3
- 2 regulators, 3 bus voltages available. 18V 10A, 5V 600mA, 3.3V 600mA
- I’ll probably change the MCU dev board format as I have only 1 pin left
-
Flash your MCU with Micropython
-
Load up the code StepperTest.py to /pyboard/main.py
-
Plug in the battery, power up and it should start moving, like so….
main.py
############################################################################### # Roverling Motor Control Module # # v1.01 15-07-2023 Starting # v1.02 20-07-2023 Adding RC control from machine import Pin, Signal, PWM, Timer from time import sleep_ms, ticks_ms, ticks_us from math import pi import neopixel # Stepper Driver Control Pins # [FL] 0 ----- 1 [FR] # | | # | | # | | # [BL] 2 ----- 3 [BR] mEN = [Signal(Pin(11,Pin.OUT), invert = True), Signal(Pin( 5,Pin.OUT), invert = True), Signal(Pin( 8,Pin.OUT), invert = True), Signal(Pin( 2,Pin.OUT), invert = True)] for i in mEN: i.off() # disable motor drivers mDIR = [Signal(Pin(9,Pin.OUT), invert = False), Signal(Pin(3,Pin.OUT), invert = True), Signal(Pin(6,Pin.OUT), invert = False), Signal(Pin(0,Pin.OUT), invert = True)] for i in mDIR: i.off() # direction forward mSTEP = [PWM(Pin(10,Pin.OUT)), PWM(Pin( 4,Pin.OUT)), PWM(Pin( 7,Pin.OUT)), PWM(Pin( 1,Pin.OUT))] for i in mSTEP: i.duty_u16(0) # FIXED at 50% when running, 0% to hold i.freq(50) # sets steps per second StepsPerM = int(1 / (pi * 0.12 ) * 200) # 200 steps per rev. 120mm diam mVREF = [PWM(Pin(15,Pin.OUT)), PWM(Pin(13,Pin.OUT)), PWM(Pin(14,Pin.OUT)), PWM(Pin(12,Pin.OUT))] for i in mVREF: i.freq(100000) # FIXED at 100kHz i.duty_u16(0) # Indicators import neopixel numPixels = 1 NeoPin = Pin(16,Pin.OUT) Neo = neopixel.NeoPixel(NeoPin,numPixels) Neo[0] = (10,0,0) neopixel.NeoPixel.write(Neo) ##################################### # Motor Control # 17/7/2023 With platform fully loaded, level surface, stall recovery at: # 2WD(F) 750mA 0.25 m/s (132Hz) # 4WD 500mA 0.25 m/s # 4WD 1000mA 0.60 m/s (318Hz) # crazy tests # 1WD(no load) 1000mA 16.5 m/s (accel 0.25 m/s/s) (8745Hz) (60km/hr)!! # 4WD(no load) 1000mA 5.0 m/s (accel 0.1 m/s/s) vibrations rule # computed velocity/PWM for Current, Target and required Step Vcur = [0, 0, 0, 0] Vtarget = [0, 0, 0, 0] Vstep = [0, 0, 0, 0] PWMcur = [0, 0, 0, 0] PWMstep = [0, 0, 0, 0] PWMtarget = [0, 0, 0, 0] # set velocity target and acceleration steps def SetVel(i, vel, accel): global Vstep global Vtarget global PWMstep global PWMtarget Vtarget[i] = vel if accel != 0 and Vtarget[i] != Vcur[i]: TimeToTarget = abs((Vtarget[i] - Vcur[i]) / accel) NumStepsReqd = (1000 / MotionPeriod) * TimeToTarget Vstep[i] = (Vtarget[i] - Vcur[i]) / NumStepsReqd # delta V per period PWMtarget[i] = int(Vtarget[i] * StepsPerM) PWMstep[i] = int(StepsPerM * Vstep[i]) # delta signed PWM per period #print('TimeToTarget', TimeToTarget, 'NumStepsReqd', NumStepsReqd) #print(i, 'Vcur', Vcur[i], 'Vtarget', Vtarget[i], 'Vstep', Vstep[i], # 'PWMstep', PWMstep[i], 'PWMtarget', PWMtarget[i]) def SetCurrentmA(mA): # Current Limit in mA. 250mA ~ 1000mA multiplier = 65 for i in mVREF: i.duty_u16(65 * max(min(mA,1000),250)) # at a regular interval increase velocity until target reached def cbMotionTimer(MotionTimer): global PWMcur for i in range(4): if PWMstep[i] != 0 : PWMcur[i] = PWMcur[i] + PWMstep[i] # Target Reached if ((PWMstep[i] > 0 and PWMcur[i] > PWMtarget[i]) # pos accel or (PWMstep[i] < 0 and PWMcur[i] < PWMtarget[i])): # neg accel PWMcur[i] = PWMtarget[i] PWMstep[i] = 0 #print('target on ', i, ' reached','PWM: ', mSTEP[i].freq()) if PWMcur[i] == 0: mSTEP[i].duty_u16(0) # HOLD when vely reaches zero Vcur[i] = 0 # Fix for illegal PWM freq if abs(PWMcur[i]) < 8: # PWM freq must be greater than 8Hz mSTEP[i].freq(8) # or set new PWM freq & DIR else: if PWMcur[i] >= 0: # set DIR according to sign of PWM mDIR[i].off() else: mDIR[i].on() mSTEP[i].freq(abs(PWMcur[i])) # remove PWM sign mSTEP[i].duty_u16(50) # RUN, testing shows 50% best Vcur[i] = PWMcur[i] / StepsPerM # update current velocity #if i == 0: print('freq',mSTEP[i].freq(),'duty',mSTEP[i].duty_u16()) ################################################################################ # Start RC Interface from RCinterface import GetRC sleep_ms(200) # allow time for sample collection # Use this first to set RC endpoints and subtrim #while True: # sleep_ms(50) # print(GetRC(1),GetRC(2),GetRC(3),GetRC(4),GetRC(5),GetRC(6)) # Start locomotion processing MotionTimer = Timer() MotionPeriod = 100 MotionTimer.init(period=MotionPeriod, mode=Timer.PERIODIC, callback=cbMotionTimer) sleep_ms(200) # enable stepper drivers SetCurrentmA(1000) for i in range(4): mEN[i].on() RCmaxVel = 2.0 RCaccel = 0.6 while True: sleep_ms(100) if GetRC(6) > 50: for i in range(4): mEN[i].off() else: for i in range(4): mEN[i].on() midVel = RCmaxVel * (GetRC(1) / 100) offset = RCmaxVel * (GetRC(2) - 50) / 100 # range -0.5 ~ +0.5 if midVel != 0: if offset > 0: leftVel = midVel + offset rightVel = midVel else: leftVel = midVel rightVel = midVel - offset else: leftVel = offset rightVel = -offset if GetRC(5) > 50: # GEAR switch = reverse leftVel = -leftVel rightVel = -rightVel print(leftVel, rightVel) SetVel(0, leftVel, RCaccel) SetVel(1, rightVel, RCaccel) SetVel(2, leftVel, RCaccel) SetVel(3, rightVel, RCaccel)
- A timer is used to incrementally update stepper frequencies based on precalculated steps.
- Use the function SetVel(n, velocity, acceleration), where n is the stepper number, velocity in m/s signed, and acceleration in m/s/s unsigned. This will then automatically accelerate (or decelerate) to the target velocity. There is no feedback and no detection of stall/stepper missing.
- Use GetRC(ch) to get the RC value in % (0 ~ 100) for channel ch.
- Tested stall recovery, no guarantee of recovery outside these parameters
-
- # 17/7/2023 With platform fully loaded, level surface, stall recovery at:
- # 2WD(F) 750mA 0.25 m/s (132Hz)
- # 4WD 500mA 0.25 m/s
- # 4WD 1000mA 0.60 m/s (318Hz)
- # crazy tests
- # 1WD(no load) 1000mA 16.5 m/s (accel 0.25 m/s/s) (8745Hz) (60km/hr)!!
- # 4WD(no load) 1000mA 5.0 m/s (accel 0.1 m/s/s) vibrations rule!!
RCinterface.py
############################################################################### # Roverling - RC Interface (mine: Spektrum AR6200 Rx, DX6i Tx) # Interrupt on both edges to mark tick_us points, process later at a reduced # rate, as required, to keep these IRQ routines as short as possible # # v1.01 20-07-2023 Starting from machine import Pin from time import ticks_us ch1 = Pin(27,Pin.IN) ch2 = Pin(13,Pin.IN) ch3 = Pin(14,Pin.IN) ch4 = Pin(28,Pin.IN) ch5 = Pin(15,Pin.IN) ch6 = Pin(26,Pin.IN) tt = ticks_us() CHtimes = [[tt,tt], [tt,tt], [tt,tt], [tt,tt], [tt,tt], [tt,tt]] def cbIntCh1(ch1): global CHtimes if ch1.value() == 1: CHtimes[0][0] = ticks_us() else: CHtimes[0][1] = ticks_us() def cbIntCh2(ch2): global CHtimes if ch2.value() == 1: CHtimes[1][0] = ticks_us() else: CHtimes[1][1] = ticks_us() def cbIntCh3(ch3): global CHtimes if ch3.value() == 1: CHtimes[2][0] = ticks_us() else: CHtimes[2][1] = ticks_us() def cbIntCh4(ch4): global CHtimes if ch4.value() == 1: CHtimes[3][0] = ticks_us() else: CHtimes[3][1] = ticks_us() def cbIntCh5(ch5): global CHtimes if ch5.value() == 1: CHtimes[4][0] = ticks_us() else: CHtimes[4][1] = ticks_us() def cbIntCh6(ch6): global CHtimes if ch6.value() == 1: CHtimes[5][0] = ticks_us() else: CHtimes[5][1] = ticks_us() ch1.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=cbIntCh1) ch2.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=cbIntCh2) ch3.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=cbIntCh3) ch4.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=cbIntCh4) ch5.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=cbIntCh5) ch6.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=cbIntCh6) LastRC = [0,0,0,0,0,0] RCavg = [[0,0,0,0,0], [0,0,0,0,0], [0,0,0,0,0], [0,0,0,0,0], [0,0,0,0,0], [0,0,0,0,0]] def GetRC(ch): # get raw times, convert to %, limit: 0 ~ 100 # too reduce gitter (some caused by other interrupts) use a 5 element median filter global LastRC global RCavg if CHtimes[ch-1][1] > CHtimes[ch-1][0]: # in case we sample mid pulse, ignore NewVal = int(min(max(((CHtimes[ch-1][1] - CHtimes[ch-1][0]) - 1000) / 10, 0), 100)) RCavg[ch-1].append(NewVal) RCavg[ch-1].pop(0) # use median of last 5 samples tmpList = [] tmpList.extend(RCavg[ch-1]) tmpList.sort() LastRC[ch-1] = tmpList[2] # centre position implies 2 values above and 2 below return LastRC[ch-1]
- I have created an RC interface from scratch, operational details are in the code. It takes up to 6 raw servo PWM signals and converts to a percentage and is non-blocking.
- Load up main.py and RCinterface.py and you will be able to control speed with throttle input, turns with aileron input, direction with gear input, and disable steppers with flaps.
- You will see from the code it is very simple to modify these input channels.
All files are available on my GitHub.
- FreeCAD design file which includes ALL bits (currently v9)
- 3D Models and Prusa project files: wheel, stepper bracket, corner, platform left and right, tyres, battery bracket & clip, switch & fuse holder, tyres
- Schematic: Roverling102.kicad_sch, Roverling102-schematic.pdf
- Test Code: StepperTest.py.
- RC Code: main.py, RCinterface.py
The challenge I set for myself was to make something out of the old 3D printer, rather than fully design, simulate and fabricate a proper robotics platform - to that end, I have succeeded. However, there are numerous problems I will need to address before getting onto the next steps:
- Power to weight ratio is not good, however, properly specced steppers & drivers would go a long way to addressing this.
- Without wheel feedback or shaft encoders, steppers are probably not the best choice. I’ve got a couple of 25-year-old Maxon DC motors with gearing and encoders which I may try.
- Way too heavy. Next, I would get rid of the steel and use aluminum instead, if really needed.
- I was overconfident that differential steering would work. I’ll need to rethink steering as it is very ineffective.
- The tyres didn’t work as expected. Great on a smooth floor, but I need it to go up and down my steep 200m unsealed drive. Next, I’ll go for thinner perimeters and greatly increase the section height.
- I should probably use a metal hub adapter, as hot motor shafts affect the very tight-fitting PLA wheel.
When I get around to fixing the above issues, I’ll probably look at the following challenges with an aim to make it useful, doing something, not sure what, on the farm.
- Characterise vehicle dynamics (especially outdoors)
- Localisation awareness - GPS, compass, gyro, accelerometer
- Telemetry will try playing with LoRa for the first time
- Add some autonomy
- Object awareness - sonar, PIR, uWave, camera (maybe some AI)
- Mapping and tracking
- Object manipulation - maybe an arm or so
- Integration with my property-wide MQTT-based infrastructure.