Pico/mPython – smart car DIY

Time for a new robot approach: building a ‘smart car’ platform. Having some Arduino Uno experiences some years back, it’s time to learn something new:

  • Want to use Pico-W board
  • Learn micronPython
  • Learn new software environment: Visual Studio Code
  • Want learn about robot control, dead reckoning & odometry, basically driving a robot to a wanted position, that would make the robot a bit smarter, doesn’t it?

After reading this inspiring medium article: “Pico Bot” by Doug Blanding, there is no reason to wait any longer.


The Build

Bits & pieces

There are a lot of robot parts laying around, let’s use them:

Mounting

At the bottom side, the motors, caster & battery pack are installed. The motor encoder connectors pointing upwards, for easy access. Everything else is mounted on topside. The Pico is installed on the breadboard on top of the plywood, giving lot of oppertunties connecting (misplaced) wires. The motor driver is installed on it’s own PCB, saving some space on the small breadboard for future sensors.

Oepzy, the plug for recharging the battery was a bit bigger then anticipated, so had to cut a small piece of the laser cutted plywood.


Powering the robot

Although the battery shield has an “activation button” and “mode switch”, I did add an additional on/off button, to controlling the robot power. The battery shield provides charching option and could at the same time provide 3.3V and 5V, when switched on. I’m only using the 5V output.

5V power rail:

One side of the breadboard power rail is connected to the 5V from the batteries. It’s powering:

  • Powering the RPi Pico: More information, watch this excelent video: Power for the Raspberry Pi Pico. So, being able to connect both your battery and upload code via USB, you need some kind of protection. According official Raspberry Pi documentation, it’s best using a P-channel mosfet. A schottky diode (1n5817) is a good alternative, but I don’t have that either:

So I’m using a 1N4007 instead, which seems to work up till now. (This diode has a voltage drop of 0.7V, which is basically burning energy from the battery supply.) ToDo: Order some mosfets…

Now the Pico is supplied with 5V, the Pico itselves supplies some 3.3V, for connecting to sensors and IC’s. I’m using the 2nd power line on the breadboard for 3.3V supply. The ground pins of both 5V and 3.3V are also directly connected.

  • Motor driver: Needs power to drive the motors, which is coming directly from the 5V power rail.

3.3V power rail:

The 3.3V from the Pico will drive all logic, it’s connected to:

  • Motor driver (logic side).
  • Both encoders power wires.

Wires for control logic

  • Encoder pins: Each encoder has 2 outputs, these are directly connected to the Pico.
  • Driver pins: The driver also requires 2 connections per motor, which are directly connected to the Pico.

DRV8835 modes:

This DRV8835 motor controller has 2 modes:

  1. MODE pin connected to 5V: Enable/Phase mode. (The drive has an internal pulldown resistor, so should be fine.)
  2. MODE pin not connected: In/In mode.

I’m using the enable/phase mode. This requires only 1x PWM signal and 1x direction pin per motor. Having the ability to: Brake, Reverse & Forwards.
So I’m not able to use the ‘coasting’ function. The drive has another optional feature: ‘sleep’ mode, by making VCC=0 Volt.

Pins used on Pico-W:

Connection diagram:


Software preperation

Firmware update: Pico-W

Download the latest Pico-W firmware from microptyhon.org and follow procedure with boot-select etc.

Currently using: “RPI_PICO_W-20240602-v1.23.0.uf2”.

Install Visual Studio Code

Download and install Visual Studio code.

Watch YouTube: How to Use VSCode with Raspberry Pi Pico W and MicroPython.

Compared to the YouTube, the VSCode extensions seems to be upgraded and renamed: “MicroPico” extension. (Thanks Paul Ober, for making this nice extension available to all of us.)

 (The extension also requires some other packages, which is nicely documented.)

Blinky

Now the Pico is powered by the USB only, so the LED on the Pico could already be tested, see youtube movie above.

Learning to work with VSCode & Pico-W

I’m just starting and learning on the way. Some learnings sofar:

  • Starting a new project: “import machine” is not reconized:

For every new VSC/Pico project, you have to: “First of all open a folder and run > MicroPico > Configure Project command via Ctrl+Shift+P”. This will install all necessary microPython links to your project.

  • Error: “Module not found”:

Maybe it’s just me, but uploading blinky code to the Pico was quite simple, by selecting the ‘Run current file’ button. But now I like to include a module, from a seperate file (encoder_portable.py). The VSCode screen does not report an error, but pressing the ‘Run current file’ button, did not upload this additional ‘module’ on the Pico! So when using additonal microPtyhon files/modules, all project files need to be uploaded. (Tnxs Iwan!)  

(ToDo: Understand difference between: ‘running current file’ and uploading files to the Pico..)

  • How to run script remote after booting Pico, w/o USB connected:

When the Pico is connected to VSCode, it’s not running automatically the ‘main.py’ file. But if the USB cable is disconnected and the Pico is re-powered by the batteries, it first tries to run ‘boot.py’ (for setting up internal devices etc) and then continous to execute ‘main.py’ if available.

Example of working ‘main.py’, needs only 2 code lines, running a different module:

import motors
motors.run()

Running specific script/function from module (name == “main“):

While the ‘main.py’ and other code is still copied on the Pico, it’s still possible to run test code inside your module. Say the motor module needs to be tested, but this test code should not automatically starting, while running the main module. With the USB & VSCode connected, the ‘motors.py’ could be activated by ‘Run current file’.

Optional, the test code could be defined as a function inside the motors.py module:

def run_test2()
	print('test case')

At the bottom of the module, write this code:

#-----------------------------------------------------------------------
if __name__ == "__main__":
	run_test2()
  • Blocked RPi Pico:

After some (other) faulty code, while playing with USB communication, the Pico was completely bricked. 🙁 VSCode was not able to connect to the Pico at all. For unblocking your RPi Pico, upload during bootsel the ‘flash-nuke.uf2’ file, see also: How To Un-Brick Your Stuck Raspberry Pi Pico.

  • ToDo: Coding remotely on the Pico:

Wouldn’t it be fantastic programming the Pico robot remotely? Getting Started with REPL on the Raspberry Pi Pico using Rshell No USB cable needed, unless the Pico is bricked. Also enables a standard USB connection with a Paspberry Pi 4, which might control the Pico someday…


Driving motors & measure power consumption

Now hardware, firmware & software seems to run, it’s time for spinning motors. The duty cycle signals from the Pico will define the speed of the motors in a range of [0..65536].

The PWM frequency could be set between 0 and 250 kHz, according the DRV8835 datasheet. Re-using the 1000Hz setting from Doug seems to works for this driver too. This frequency setting may influence driver performance. (ToDo: Some PWM-frequency testing..)

Basic code:

# imports:
from machine import Pin, PWM

# setup motor pin:
pinMtrDir_L = Pin(6, Pin.OUT, value=0) 	#Direction pin
pinMtrPWM_L = PWM(Pin(7))			#Speed pin
pinMtrPWM_L.freq(1000)				#Frequency of the PWM pin
pinMtrPWM_L.duty_u16(0)				#Set duty cycle to zero, we don't want running motors during pin initalisation!

# drive motor opposite direction at ~30% PWM:
pinMtrDir_L.value(1)
pinMtrPWM_L.duty_u16(20_000)		#The '20_000' stands for 20000. But this '_' does not work for negative numbers or floats.

Faulty connector motor/encoder…

Unfortunately one of the provided motor connectors is very loose. Sometimes there is no connection, which will lead to unwanted controll.

Solved: The male header got somewhat loose from the pins, so these pins became very short, creating a poor connection with the female header. So just had to push this to it original position.

Duty cycle versus motor speed

Time to play with the PWM duty cycle. While increasing the duty cycle:

  • At very low PWM value’s, the motor is NOT running. There is a certain ‘cut-in’ speed required. (My case with these N20 motors: roughly 15% on table.)
  • The motor speed is increasing.
  • Also the motor torque is increasing.

With constant duty cycle value:

  • The relation between speed & torque: If the motor wheel is gently squeezed, ofc. duty cycle value remains the same, but the torque on the motor shaft increases and the speed is decreasing.

Learnings:

  • PWM duty cycle is not linear related to motor speed, due to varing dynamic influences.

Power consumption

Curious about power consumption of both N20 motors and the RPi Pico.

Current measured for different situations & PWM values:

  • Measuring motor driver current, on the 5V line into drv8835:
    • Free spinning motors
    • Robot moving on table
    • Both motors are blocked shortly
  • System current (from battery):
    • Measuring both drive + pico, in blocked mode.

Conclusion:

  • Current through RPi Pico (and both encoders) seems to be ~46 mA. (Also tested in unblocked situation.)
  • Current through 2x N20 motors:
    • Free spinning: 40 mA @ 20% duty cycle, moving up to 100 mA @ 60% and getting lower 84 mA @ 100%. (That 60% increse is a bit strange to me, maybe due to current chopping?) ToDo: Play a bit with the PWM-frequency setting…
    • Both motors are blocked: Let’s say a linear behaviour with duty cycle value, up to 560 mA @ 100% duty cycle.
    • Robot moving on table: On a flat table, there seems to be a linear relation between PWM & current consumption. (Robot weight: 250 gram.) ToDo: Meaure current & speed, while moving on table with increased mass, what is the effect?
  • Current and so the speed will depend on real world situations and are not linear to duty cycle.

Moving robot – straight line

So let’s activate both motors with same duty cycle (value: 20000, is roughly 30% power) for exact same duration of 2 seconds forwards and then backwards: Driving 2 motors with identical duty cycles.

Conclusion:

  • What is going on? Surprise: the robot is not moving a straight line at all! Did something went wrong? No, it didn’t. This is common behaviour with these type of DC motors. Within the same motor rotating CW and CCW reacts already differently.
  • We need somekind of motor contoller & calibrations, moving the robot in a wanted direction…

The encoders & wheel travel

Using a “N20 500RPM@6V” motor + “standard wheels”, with both A & B encoder signals connected to the Pico, the ’ticks’ from the motorshaft could be displayed.

Encoder+MotorGearboxWheel
Reading A & B signalsDiameter: 44.5 [mm]
28 ticks/ motorshaft_rotationRatio: 1:30Circumference:
139.79 [mm]
~8157 ticks(So ratio is 1:~29)~10 rotations
2926 ticks500 [mm]
5.852 ticks1 [mm]
1 tick0.17 [mm]

Conclusions:

  • Encoder resolution: 0.17 [mm per tick].
  • Some basic parameters could be derived:
    • TICKS_PER_M = 5852 # NrOfTicks traveling 1 meter.
    • M_PER_TICKS = 1 / TICKS_PER_M

Robot rotations

What effect will the encoder resoultion have on the robots heading? The heading (theta) is basically defined by position of both wheels. So the maximum heading error = +/- atan( 2 * encoder_resoution / wheelbase ) = +/- atan(2 * 0.17mm/90mm) = 0.216 [degrees]. So moving X: 1000 [mm] forwards, could introduce a theoretical offset in Y direction of: 3.8 [mm].

If both motors are running with same speed but opposite direction, the robot should spin around its centroid. Total amount of encoder ticks of both motors:

  • WHEEL_BASE = 0.09 #[m]
  • TICKS_PER_REV = 2 motors * (WHEEL_BASE * 3.1415) * TICKS_TO_M
    = 3309.12 #total ticks for robot rotating 360 degrees.
    So: 9.12 [ticks/deg], or 526.67 [ticks/rad].)

Motor & encoder stability.

Now all motors & encoders seem to work, it’s time to understand some limits/variations.

Variations to test:

  • Encoder reading frequency (default 10 Hz).
  • Duty cycle motors: -100…100 %.

Constants per test:

  • Constant duty cycle & direction for both motors.
  • Motor PWM-frequency: 1000 Hz.
  • Provided battery voltage: 5 V.

Known noise effects:

  • The motor connector is stil loose, which is ‘sometimes’ effecting these tests.
  • The robot is still connected to the USB. This causes a dragging effect on the (light weight) robot.

Updated messages from the Pico:

  • Pico current time: ms,
  • Encoder left motor: ticks,
  • Encoder right motor: ticks.

What to expect:

  • Stability of updated messages, is the timer function spot on? In graphs below, the ‘deltaTime’ is shown, per time interval, this should be constant, depending on timer frequency setting.
  • Stability of motor speed, should be constant value inbetween 2 intervals. For both motors the ‘deltaEncoderPosition’ per time interval is calculated. So basically this is the speed of the motor: ticks/sec.

Encoder readings: Frequency setting

What could be ideal update time for encoder readings? As quick as possible, will require a lot of calculation power on the Pico, so other features might hickup. Too low readings, are missing small deviations and end up in less precise measurements. These measurements are done with free spinning wheels: 

Conclusions:

  • All tested update frequencies (10,20,100,200Hz) seems to work fine. The response time delta’s are constant, except for the first graph.
  • While testing the first graph, the motor connector seems to be a bit loose again. This results in lot of noise and also the timer function seems to be influenced!
  • Although tests below were run at 10 Hz, the system seems to report messages nicely at 200 Hz too.
  • With a higher measurement frequency (200 Hz), the motor speed up curve is very nice visible. Seems to take 170 [msec], rotating the motor to maximum speed.
  • In most graphs, the right motor (orange line), is moving a bit quicker then the left motor.

Reading encoders: Free running wheels.

The expectation is after the wheels are up to speed to there corsponding duty cycle setting, the ticks/sec should be constant. The encoder frequency is kept to 10 Hz. Both forwards & backwards speeds are measured.


Encoder test – Free funning wheels, forwards.
Encoder test – Free funning wheels, forwards.

Conclusions:

  • Response times from the timer is still very stable.
  • At lower speeds (duty cycles: +/-20% to +40%), both motors seem to act simulary. At higher speeds the right motor reacts quicker and so the robot makes a turn.

Reading encoders: Robot (300gr) on table

Same test like before, but now the robot drives on the table, adding some friction & inertia to the wheels. These measurements are not extremly precise, since the USB cable is still connected to the robot and causes a little drag.

Encoder test – Robot 300 gram, moving forwards.
Encoder test – Robot 300 gram, moving backwards.

Conclusions:

  • Okay, these measurements are not precise at all… Speeds seem to decrease over time, most likely due to USB cable interferance, the cable is not long enough.

Reading encoders: Heavy robot (600gr) on table

What’s happening with the robot, when adding some mass, maybe a RPi 4 someday… In this case the robot weight is doubled.


Encoder test – Robot 600 gram, moving backwards.

Encoder test – Robot 600 gram, moving backwards.

Conclusions:

  • Interesting to experience the 20% duty cycle value gives trouble in ‘forward’ direction, while this worked fine with the 300gr robot.
  • ToDo: Changing motor PWMpin(1000Hz) to different frequencies, how does this effect the duty cycles?

Overall conclusions

These tests show some insights, but are far from perfect measurements.

Estimated rough motor/robot speeds, depending on duty cycles:

Duty cycle [%]:-60-40-2020406080100
free spinning:315020008408702000315043005450ticks/sec
robot (300gr)2600n/a80050011502150n/an/aticks/sec
robot (600gr)2500170065020011502000n/an/aticks/sec
free spinning:535.5340142.8147.9340535.5731926.5mm/sec
robot (300gr)442n/a13685195.5365.5n/an/amm/sec
robot (600gr)425289110.534195.5340n/an/amm/sec

Creating a reliable speed controller, the update frequency should be:

  • as low as possible, so it needs less CPU time,
  • as low as possible, so measurement resolution is good enough (encoders counting integers, not floats),
  • as high as possible, control loop has a quick response,
  • as high as possible, so imperfections (accelerations/bumps) are better controlled(?).

Let’s see what happens, when changing the controller update frequency:

Update frequency:1 Hz10 Hz20 Hz50 Hz
Period:1000 ms100 ms50 ms20 ms
NrOfTicks (100%):5500550275110
NrOfTicks (20%):11001105522
Max. encoder resolution (20%) [ticks]:440044022088

Overall conclusions:

  • Motor speeds:
    • Maximum free spinning motor speeds: ~5500 ticks per second.
      Or 5500/TICKS_PER_M = 5500/5852= 0.94 [m/sec]
  • Free spinning versus robot moving on table reduces speeds a lot, around 20 to 40%:
    • The lowest robot speed (PWM ~20%) is around 150 mm/sec.
    • Top speed of robot is estimated between 500-750 mm/sec.
  • Assumption: In general reading the encoder ticks seems to work fine:
    Update frequency between 10 and 20 Hz might be good starting point.
  • Weight of robot definately impacts cut-in speeds!
  • ToDo: Solve that anoying connector, generating noise.
  • ToDo: Program motor speed/position controller.
  • ToDo: Program remote logging, no more side effects of cables…

How to program a motor speed/position controller?

Let’s reuse the inspiring work from Doug. The code needs some changes:

  • Using different hardware: different parameters for wheel diameters, motors, encoders and wheel base etc.
  • Using different motor controller, now only 4 pins are used in a slightly different way.
  • VSCode: The ‘encoder_rpi.py’ has some ‘Viper’ coding, which is not working with VSCode/Pico extension. So switching to ‘encoder_portable.py’.
  • Pico firmware update: syntax for ‘asyncio.create_task’ is changed a little.
  • Me missing knowledge: Not able to start the web-interface…
  • Changed the PID error calculation: err = setpoint – actual. (So PID parameters are positive numbers.)

Due to the nicely organized code, changes were straight forward.

ToDo: play bit more with: PID settings, minimum running speeds, calibration e-compass.
ToDo: do more calibrations, how precise is the robot moving to a target?

Conclusions (so far):

  • With some tweaking the code works out-of-the-box.
  • Still need to update the code:
    • Include: accelerating/deaccelerating.
    • Include: remote logging.
    • Include: remote sending commands.
    • (Optimize algorithm?)

Current status (Jan. 2025)

The robot is able to follow some waypoints (hard coded).

But somehow the precision moving towards the end point is very low. (Easialy 15 cm off, when moving a 1x1m square.)

Next steps:

  • Try different N20 gearboxes, especially higher gear ratio, which will generate more pulses per wheel revolution. Does that help?
  • Try different motors, including higher resolution encoders.
    A more heavy robot, will increase friction component and should(?) have less slip.
  • Have to do a bunch more ‘remote’ coding. What medium/software to use?

Although I’m starting to understanding more about drive trains, I basically decided to pauze this project. I want to learn some more about higher level robotics on a working platform…

So decided continouing with DuckieTown MOOC, this time including the hardware.