I don't know about you, but my childhood was all about RC cars. As I grew older they were somewhere along the road replaced with video games, where I had the chance to actually (for once in my life mom !) go over the speed limit. Now, instead of just reminiscing on some old memories, in this project we're taking this matter into our own hands. To cut to the chase: in this project, we are going to combine the controls known from video games with my childhood's greatest hobby. (*Drumroll*): we are going to control an RC rover with a Dualshock 4 (PS4) controller.
Of course, this doesn't have to be a rover as well; this project is perfect for reviving your old toy car (the dusty one laying in your basement for more than 3 years because you forgot where you kept its remote control). The main concept revolves around wirelessly controlling a DC motor so feel free to unleash the boundaries to your creativity.
Let's talk about the hardware we're dealing with. First of all, we have the Raspberry Pi model B. This model has built-in Bluetooth support, which will be useful in connecting with the Playstation controller. Moreover, we will be using the Infineon TLE 94112 Pi HAT to control our DC motors. (ThePi HAT is compatible with any 40 pin layout Raspberry Pi version). This could also be used for LED control. However we will only be controlling motors in this project. For the human component of control, we will use a Dualshock 4 controller. For the rover's skeleton, we will use a custom 3 wheeled lego set using omniwheels and three DC Motors with all their respective wires out ready to be connected.
Taking a closer look at the Pi HATThis Pi HAT is essentially a multi-half-bridge circuit, where to keep this as simple as possible we are having control over the direction and amount of power the DC motor receives; in other words: we have the capacity to change the motor's speed and direction. (For further details, please refer to the theory section in the TLE94112 Hackster Protip)
To use the Pi HAT: all you have to do is supply it with power (recommended range between 5.5V and 20V) and connect it to your motors.
To keep this project as simple as possible we will be using the Raspberrypi's GPIO pins to power the Pi HAT, however, if speed is what you desire then I would definitely recommend powering the Pi HAT with something that has more juice.
Pi HAT basic testEnough introductions let's drive straight in! This section is where we get a hint of how we're actually going to program our project. Mount the Pi HAT onto the Raspberry Pi and connect the HATs power connections as well as a DC motor to outputs 1 and 5.
Onto the raspberry pi itself through a command line:
- Make sure you have the package manager for Python packages by entering:
sudo apt-get install python3-pip
- Install the Infineon multi half-bridge library by entering:
sudo pip3 install multi-half-bridge
- clone Infineon's multi-half-bridge repository by entering:
git clone https://github.com/Infineon/multi-half-bridge.git
At this point, you have everything you need to control the Pi HAT (i.e control the motors), installed onto your Pi. Fortunately, the multi-half-bridge circuit library comes with some pre-written code examples to showcase how to use the product. You can find these examples in the directory:
multi-half-bridge/src/framework/raspberrypi/examples_py/
The same examples are also written in c++ but in this project, I will just stick to python. Let's try running the basicTest.py script by navigating to the examples_py directory and in the command line entering:
sudo python3 basicTest.py
If you followed the previous instructions, then your motor should start as soon as you run the script for three seconds and then stop. Congratulations you're officially done with 30% percent of the project! I know it was hard getting here but this was legitimately the hardest part. What remains is just understanding the code used in the test and then writing your own program.
Basic Test Code AnalysisBefore getting into any details on the code specialties, I want you first to think about the code as a compound structure made up of a skeleton and an active component. In the skeleton component: you basically explain your setup to the program, i.e you tell the program which motors are connected to which pins and how do you want these connections to operate. This component involves no active control whatsoever to the motors (i.e it has nothing to do with accelerating, decelerating, moving, or stopping); it is just you basically telling the software how the hardware is assembled. Now onto the active component; this is where all the bells and whistles lie: this is the part where you instruct the motors exactly what to do and when.
"""
# name basicTest
# author Infineon Technologies AG
# copyright 2021 Infineon Technologies AG
# brief This example shows how to switch two half bridge outputs with minimal code.
# details
It will switch on two outputs (one to Vsup and one to GND), wait 3 seconds
and switch off both outputs (both to floating state).
# SPDX-License-Identifier: MIT
"""
import multi_half_bridge_py as mhb
from time import sleep
# Create a Tle94112Rpi instance for each motor controller
controller = mhb.Tle94112Rpi()
# Create a Tle94112Motor instance for each connected load
motor = mhb.Tle94112Motor(controller)
# Enable MotorController
# Note: Required to be done before starting to configure the motor
# controller is set to default CS0 pin
controller.begin()
# Clear all errors to start clean
controller.clearErrors()
# Let the library know that a load is connected to HB1 (high side)
# and HB5 (low side).
motor.connect(motor.HIGHSIDE, controller.TLE_HB1)
motor.connect(motor.LOWSIDE, controller.TLE_HB5)
motor.setPwm(motor.LOWSIDE, controller.TLE_NOPWM)
# Initialize the motor
motor.begin()
# Switch the load on
motor.start(255)
sleep(3)
# Switch the load off (set outputs to floating state)
motor.coast()
This is the code for the basic test (it would occupy only 12 lines if it wasn't for the comments). Let's start dissecting!
import multi_half_bridge_py as mhb
from time import sleep
The code starts with importing the Infineon multi-half-bridge library that we have already installed. It also imports the function from the time module (we will see why shortly).
Skeleton component
controller = mhb.Tle94112Rpi()
motor = mhb.Tle94112Motor(controller)
Thereafter a controller structure is instantiated and a motor structure is also instantiated under this controller structure. (This is the first part of the skeleton component that we were talking about earlier.)
Remember, we are just telling the software how the hardware is assembled.
controller.begin()
controller.clearErrors()
the controller is enabled through the begin function and the error Registers which are used for debugging purposes are cleared using the clear errors function. (we will not be using the Pi HAT's debugging features in this project but if you want to have a deeper look into them refer to section 5 of the TLE94112 Hackster Protip).
motor.connect(motor.HIGHSIDE, controller.TLE_HB1)
motor.connect(motor.LOWSIDE, controller.TLE_HB5)
motor.setPwm(motor.LOWSIDE, controller.TLE_NOPWM)
motor.begin()
To conclude the skeleton component, the motor connections are specified in the motor.connect() function, where we specify to the program the terminals that the DC motor is connected to. I personally do not give the HIGHSIDE/LOWSIDE pin selection much thought; all I need to do is have one connection on HIGHSIDE and the other on LOWSIDE because I know that I can change the direction of the motor's motion from the program to my preference. After specifying the motor's connections, all you have to do is tell the program, whether or not you want to use Pulse width modulation (if you are not familiar with this term read the next CRASH COURSE subsection).
CRASH COURSE: PULSEWIDTH MODULATION
Pulse width modulation is switching power ON and OFF with a high frequency with the aim of controlling the power delivered to the devices. The ratio of ON and OFF time is called duty-cycle and this indicates the amount of power delivered to the device (in our case motors). To keep everything simple: it is just a way of quantifying the power that we are giving to the motors, where at 100% duty-cycle the motor is getting full power (we could use this when we want the motor to move at maximum speed), if we want to decelerate then we would reduce the duty-cycle and if we want to accelerate we would increase it.
Back to the basic test: in the basic test the motor is not assigned a pulse width modulation channel: i.e it will either move at full power or it won't move at all (regardless of the direction). If we would want to change that we would change the setPwm function to:
motor.setPwm(motor.LOWSIDE, controller.TLE_PWM1)
Where we would be assigning a pulse width modulation channel, named PWM1 to this motor.
Note: You could assign multiple motors to the same PWM channel.
motor.begin()
Last but not least we end the skeleton component with the begin function, which essentially enables the motor structure and initializes it.
Active Control Component
It's action time.
motor.start(255)
And now for the MVP of this script: we have the start function, which is the function that actually runs the motor. If you're using pulse width modulation then you can set the duty cycle between the range of 0 to 255. Where at 0 the motor is not running at all and 255 is the motor running at full power. To change the direction of the motor all you have to do is add a negative sign right before the value, so if we would want the motor in this example code to take the other direction we would change the command to:
motor.start(-255)
sleep(3)
Remember the sleep function? This line is the one in charge of keeping the motor working for 3 seconds before stopping. We will not be using it in our project but you can always use it in any application, where you want to power your motors differently depending on the amount of time that goes by.
motor.coast()
The coast() function is in charge of stopping the delivery of power to the motor in order to stop the motor, however without any form of active braking.
A closer look at the active control programming componentIn the past section, we saw how simple it was to write a program, that runs a motor at full power for three seconds and then coasts it. Let's get a deeper look into other active control functionalities besides moving and coasting.
The active control component consists mainly of the following functions:
Now that we're pretty familiar with coding the Pi HAT we could now move forward in our project. Time to start using the PS4 controller; so let's begin by firstly pairing it with our Raspberry Pi. Connecting the PS4 controller with the Pi could be done in multiple ways. To keep it simple here we will choose the easiest one; we will pair with it through the Raspbian graphical user interface.
Initially, we will have to put the controller in pairing mode by simply holding the Playstation button and the share button at the same time. Keep holding until the LED at the back of the controller starts blinking.
Now that the controller is running in pairing mode let's hop into the Raspberry Pi side. Head to the right corner and left-click on the Bluetooth icon. Click on add device and scroll through the list until you find the controller. Click on it and pair. And voila now you're connected!
Using the pyPS4 libraryThe pyPS4 library allows us to use a paired Dualshock 4 controller as an input device to a python script. To install the library; in a command line enter:
sudo pip3 install pyPS4Controller
Let's take a look at how to use it. Thankfully there is a well-written guide on how to use it but to make a long story short all we have to do is copy this code from the repository and under the new Class define functions that resemble the response we want to be associated with certain events.
from pyPS4Controller.controller import Controller
class MyController(Controller):
def __init__(self, **kwargs):
Controller.__init__(self, **kwargs)
def on_x_press(self):
print("Hello world")
def on_x_release(self):
print("Goodbye world")
controller = MyController(interface="/dev/input/js0", connecting_using_ds4drv=False)
# you can start listening before controller is paired, as long as you pair it within the timeout window
controller.listen(timeout=60)
for example, this code snippet prints Hello world when we press the x button on the controller and prints Goodbye world upon releasing it. A list of all events is also available at the repository itself:
on_x_press
on_x_release
on_triangle_press
on_triangle_release
on_circle_press
on_circle_release
on_square_press
on_square_release
on_L1_press
on_L1_release
on_L2_press
on_L2_release
on_R1_press
on_R1_release
on_R2_press
on_R2_release
on_up_arrow_press
on_up_down_arrow_release
on_down_arrow_press
on_left_arrow_press
on_left_right_arrow_release
on_right_arrow_press
on_L3_up
on_L3_down
on_L3_left
on_L3_right
on_L3_x_at_rest # L3 joystick is at rest after the joystick was moved and let go off on x axis
on_L3_y_at_rest # L3 joystick is at rest after the joystick was moved and let go off on y axis
on_L3_press # L3 joystick is clicked. This event is only detected when connecting without ds4drv
on_L3_release # L3 joystick is released after the click. This event is only detected when connecting without ds4drv
on_R3_up
on_R3_down
on_R3_left
on_R3_right
on_R3_x_at_rest # R3 joystick is at rest after the joystick was moved and let go off on x axis
on_R3_y_at_rest # R3 joystick is at rest after the joystick was moved and let go off on y axis
on_R3_press # R3 joystick is clicked. This event is only detected when connecting without ds4drv
on_R3_release # R3 joystick is released after the click. This event is only detected when connecting without ds4drv
on_options_press
on_options_release
on_share_press # this event is only detected when connecting without ds4drv
on_share_release # this event is only detected when connecting without ds4drv
on_playstation_button_press # this event is only detected when connecting without ds4drv
on_playstation_button_release # this event is only detected when connecting without ds4drv
Let's try and use both code snippets to come up with our own code in order to have a better grasp of how to use the library and tailor it to our own application. Let's say we want to print some messages on the screen if we press some buttons on the controller. For example, let's write a script that prints "going up" if we press the forwards' arrow key and prints "not going up anymore" if we release the upwards arrow key:
- The first step would be to copy the hello world code from above and then remove the x button functions like this:
from pyPS4Controller.controller import Controller
class MyController(Controller):
def __init__(self, **kwargs):
Controller.__init__(self, **kwargs)
controller = MyController(interface="/dev/input/js0", connecting_using_ds4drv=False)
# you can start listening before controller is paired, as long as you pair it within the timeout window
controller.listen(timeout=60)
- The second step would be to copy (from the code snippet above) the events that we want our program to respond to:
first being the up arrow button being pressed on_up_arrow_press
we then use it to create our own function as follows
from pyPS4Controller.controller import Controller
class MyController(Controller):
def __init__(self, **kwargs):
Controller.__init__(self, **kwargs)
def on_up_arrow_press(self):
print("going up")
controller = MyController(interface="/dev/input/js0", connecting_using_ds4drv=False)
controller.listen(timeout=60)
Now let's add the other function for releasing the up-arrow: we will do the exact same process by copying the event from the snippet and then using it to write the function in your code
Event: on_up_down_arrow_release
from pyPS4Controller.controller import Controller
class MyController(Controller):
def __init__(self, **kwargs):
Controller.__init__(self, **kwargs)
def on_up_arrow_press(self):
print("going up")
def on_up_down_arrow_release(self):
print("not going up anymore")
controller = MyController(interface="/dev/input/js0", connecting_using_ds4drv=False)
controller.listen(timeout=60)
To run the script you just wrote, simply navigate in a command line to the directory containing your script and then enter
sudo python3 "nameOfYourScript".py
Rover Software BreakdownIn the following part we use the 12 line basicTest code and tailor it to our application. (You most probably do not have the exact same setup I am having, this should not matter at all at the end of the day you will just modify, add or remove functionalities to the code, the structure is essentially the same)
import multi_half_bridge_py as mhb
from pyPS4Controller.controller import Controller
The only two libraries we need for this particular project are the ones that we installed, which are the Infineon multi-half-bridge library and the pyPS4Controller.
Skeleton Code Component:Instances:
controller = mhb.Tle94112Rpi()
motor1 = mhb.Tle94112Motor(controller)
motor2 = mhb.Tle94112Motor(controller)
motor3 = mhb.Tle94112Motor(controller)
Analog to the basic test we first instantiate a controller and then a motor In my setup, the lego kit came with three motors so I will make three motor instances and name them motor1, motor2, and motor3.
Controller configuration:
controller.begin()
controller.clearErrors()
Before doing any motor configuration it is critical to enable the controller and clear its error registers.
Motor configuration:
I am using three different motors in this setup so I will be configuring each one of them on their own
Motor1 configuration:
motor1.connect(motor1.HIGHSIDE, controller.TLE_HB1)
motor1.connect(motor1.LOWSIDE, controller.TLE_HB5)
motor1.setPwm(motor1.HIGHSIDE, controller.TLE_PWM1)
motor1.begin()
This motor is physically connected to outputs 1 and 5 so all I did was specify that in the connect function. The choice of which side to be used as HIGHSIDE and which is to be used for LOWSIDE was completely arbitrary (because I get to control the motor's direction through code so I did not really give it that much thought). I am not using pulse width modulation because I want to give the motor as much power possible, however, I did assign the motor a pulse width modulation channel just in case I change my mind in the future. The last step in the motor configuration is to enable it.
Motor 2 Configuration:
motor2.connect(motor2.HIGHSIDE, controller.TLE_HB7)
motor2.connect(motor2.LOWSIDE, controller.TLE_HB9)
motor2.setPwm(motor2.HIGHSIDE, controller.TLE_PWM2)
motor2.begin()
The second and third motor configurations were completely analog to the configuration of the first, the only difference are the outputs these motors are connected to.
Motor 3 Configuration:
motor3.connect(motor2.HIGHSIDE, controller.TLE_HB6)
motor3.connect(motor2.LOWSIDE, controller.TLE_HB4)
motor2.setPwm(motor3.HIGHSIDE, controller.TLE_PWM3)
motor3.begin()
This concludes the skeleton component of the code.
Active Control Code Component:I try to keep my code as organized and as readable as possible through the use of functions. For example, instead of asking motors 1 and 3 to start every time I want to ask the program to make the rover move forward. I will simply write a function that does that and name it move_forward(). The next subsection is purely dedicated to making such functions.
Custom Functions:
Before getting into the function I want to give a hint on how I want to control my setup. My setup consists of three motors, I want to dedicate two of them to moving forward and backward (namely motors 1 and 3). Motor2 will be in charge of turning left or right.
def motors_stop():
motor3.stop(255)
motor2.stop(255)
motor1.stop(255)
As the name of the function suggests this custom function breaks all three motors to stop the rover's motion. The braking is performed with maximum force. ( If you want to implement softer braking feel free to reduce the stop functions input to something below 255)
def move_forward():
motor1.start(255)
motor3.start(-255)
To move my rover forwards all I have to do is move motor1 and motor3. (If I did not want motor3 to have a contradicting direction to the motion of motor 1 I could have switched the physical pin connections of motor3 and that would have done the job).
def move_backward():
motor1.start(-255)
motor3.start(255)
Analog to the move_forward() function, the move_backwards() is the exact same with just different motor motion directions.
def turn_left():
motor2.start(255)
Motor2 is the only motor in charge of turning the rover. Note that the rover is not a finite state machine: i.e it could turn left while moving forward or backward.
def turn_right():
motor2.start(-255)
The turn_right() function does exactly what the turn_left function does, but with a different direction of motor motion.
The rest of the functions are kind of intuitive.
def coast_vertical():
motor1.coast()
motor3.coast()
def coast_horizontal():
motor2.coast()
def rotate_clockwise():
motor1.start(-255)
motor2.start(-255)
motor3.start(-255)
def rotate_anticlockwise():
motor1.start(255)
motor2.start(255)
motor3.start(255)
def motors_coast():
motor1.coast()
motor3.coast()
motor2.coast()
Dualshock 4 Controller Component:Now for the final part of this project's code. We will copy the "hello world" code snippet and add the events we want from the list above and associate them with the functions we just created.
class MyController(Controller):
def __init__(self, **kwargs):
Controller.__init__(self, **kwargs)
def on_x_press(self):
motors_stop()
def on_up_arrow_press(self):
move_forward()
def on_left_arrow_press(self):
turn_left()
def on_right_arrow_press(self):
turn_right()
def on_down_arrow_press(self):
move_backward()
def on_up_down_arrow_release(self):
coast_vertical()
def on_left_right_arrow_release(self):
coast_horizontal()
def on_R1_press(self):
rotate_clockwise()
def on_R1_release(self):
motors_coast()
def on_L1_press(self):
rotate_anticlockwise()
def on_L1_release(self):
motors_coast()
def on_R2_press(self, value):
move_forward()
return super().on_R2_press(value)
def on_R2_release(self):
coast_vertical()
return super().on_R2_release()
def on_L2_press(self, value):
move_backward()
return super().on_L2_press(value)
def on_L2_release(self):
coast_vertical()
return super().on_L2_release()
controller = MyController(interface="/dev/input/js0", connecting_using_ds4drv=False)
controller.listen(timeout=60)
Annnnnd Voila! Now we're done!
For my rover to work I need to run this python script, but let's say we want to experiment with my driving skills outside. I can't go everywhere with my monitor, keyboard, and mouse. It is more realistic to make the Raspberry Pi run our script on startup.
Running our script on Startup (Autostart)If you've reached this stage, you have a functioning.py file.
Open a command line and enter:
sudo nano /etc/profile
scroll to the end of the window and enter
sudo python3 "the directories leading to your file"/"yourfile".py
in my case it was
sudo python3 /home/pi/Desktop/project_christmas.py
Press ctrl+o and enter to save and then ctrl+x to exit and then you're done. Now your Raspberry Pi will run your script as soon as it starts. Your DualShock 4 will connect automatically to the Raspberry Pi if it is paired as well.
And that's all there is to the project. I hope that you now possess all the knowledge you need to revive that old toy car of yours. Feel free to check out the other projects as well if you liked this one. In the meantime drive safe ;)
Comments