This is a full guide on controlling an electric motor from a phone (iOS or Android) with a Raspberry PI H-Bridge and Pulse Width Modulation.
The video version of this article is here:
Let’s go through electric motor basics and H-Bridge.
When the current is applied to a motor, it starts to rotate.
If we reverse the direction of the current, the motor will rotate in the opposite direction. It’s very straightforward.
To switch the direction of the current, we use an H-bridge.
H-bridgeSimply speaking, H-Bridge - it’s 4 electrical switchers (transistors) that can change the direction of current flow through the motor by turning on diagonal switchers.
This pair will enable rotation in a counterclockwise direction.
And the opposite swithers will rotate in a clockwise direction.
How can it change the speed of the motor?
And the answer is using pulse-width modulation.
Pulse Width Modulation (PWM)When we don’t have current, the motor is static. Nothing happens, nothing rotates.
When we send tiny pulses of current for a short time - we make the motor rotate. When there is no current - the motor rotates by insertion. We can see that voltage is applied to the motor only 25% of the time, and in this case, we will have only 25% of power.
Next, we increase pulse length and reduce gaps when we don’t have the current. Now, we have identical periods when the voltage is applied and gaps when it doesn’t. The motor is pushed half the time so that the power will equal 50%.
Going further, we can apply voltage during 75% of the time. The power will increase accordingly.
And finally, when the current is flowing constantly, we have 100% of the power.
Let’s zoom out and review it together. We can see that power or speed is controlled by current impulses. We apply the same voltage but for a different amount of time. The longer the impulses, the more speed and power the motor will reach.
- No current, no power.
- When pulse length is 25% of a period - 25% of the power.
- We have half the power when the gaps are equal to the current flow periods.
- When pulse length is 75% of a period - 75% of the power.
- And finally, full power when the current is constant.
Combining the H-Bridge with the Pulse Width Modulation allows us to control the electric motor's speed and rotation direction.
And this tiny board can do both.
The connection schema- 12V battery. We connect the H-Bridge module to it. We need to use POWER plus and POWER minus slots.
- We connect a motor to the motor slots. In this case, polarity doesn’t matter.
- And finally, we connect tiny pins of the module to Rasberry PI:
- Ground to Ground,
- Input 1 (IN1) of the H-Bridge to the GPIO 13
- Input 2 (IN2) to the GPIO 12.
We are using these pins because they support HARDWARE Pulse Width Modulation. This modulation type has a more precise output and works better than software PWM.
And that’s how it looks like in real life.
The hardware is ready, and it’s time to review the software.
System DesignThe Raspberry PI and phone are connected to the same Wi-Fi so that I can use a local network. I will use WebSockets because it’s easier to build reliable communication using this TCP connection. If you don’t know what WebSockets is, you can find a video about different types of internet connections on my channel.
Now, let’s review a system design. We have a phone connected to a Wi-Fi router and a Raspberry Pi on the same network with a WebSocket server on a board.
We will establish a TCP connection to the WebSocket server and send a command that contains a JSON object. I am sending Y from -100 to 100 to determine the rotation speed and direction.
- -100 means full speed in the counterclockwise direction.
When Raspberry PI receives a command, it sends a pulse-width-modulation signal to input 1 of the H-Bridge. The H-Bridge powers the motor, and it rotates.
- 100 means full speed in the clockwise direction.
When the phone sends a command with a positive 100 value, the Raspberry PI stops sending any current to Input1 and starts sending it to Input2. The H-Bridge reverses the current flow.
I am using a Raspberry PI 5 and 64-bit Debian OS.
Next, I need to connect to Raspberry Pi using SSH
ssh USERNANME@RASBPERRY_PI_IP_ADDRESS
And clone the repository.
git clone https://github.com/Nerdy-Things/raspberry-pi-5-pwm-motor-control.git
Next, we are going deeper into the Raspberry folder of the repo and running script. Later, I will explain what it does.
cd raspberry-pi-5-pwm-motor-control/raspberry
./install.sh
In the end, the Raspberry will reboot itself.
After reconnection, we need to go to the same folder and run the command:
cd raspberry-pi-5-pwm-motor-control/raspberry
python websocket.py
The command printed that it operates with both GPIO and that it created a WebSocket server to port 8765
The Raspberry PI is ready and waiting for the connection.
Raspberry PI codebaseIt’s time to open VisualStudioCode and review what we just launched on the Raspberry PI.
install.sh script will do the following:
#!/bin/bash
sudo apt-get install -y git python3
# https://github.com/Pioreactor/rpi_hardware_pwm
pip3 install rpi-hardware-pwm --break-system-packages
pip3 install websockets --break-system-packages
sudo bash -c 'echo "dtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4" >> /boot/firmware/config.txt'
# Get the current kernel version
current_kernel=$(uname -r)
# https://github.com/Pioreactor/rpi_hardware_pwm/issues/14
if [[ "$current_kernel" == "6.6.20+rpt-rpi-2712" ]]; then
echo "Kernel version has issues. Updating..."
sudo apt install raspberrypi-kernel
sudo rpi-update
else
echo "Kernel is OK"
fi
sudo reboot now
- First, it will check and install Git and Python. They should be preinstalled on the system, but it’s better to recheck.
- Install two important Python libraries. Hardware PWM and Websocket server.
- Enable Pulse Width Modulation on pins 12 and 13. So, we add this line dtoverlay=pwm-2chan, pin=12, func=4, pin2=13, func2=4 to this file /boot/firmware/config.txt.
- It's an ugly hack. In the current version of Debian OS, hardware pulse width modulation doesn’t work. And we need to update the kernel. If a kernel version is 6.6.20, we force an update on the Linux kernel.
Last but not least, reboot the Raspberry PI.
websocket.py
PING_INTERVAL = 5
PING_MESSAGE = "PING"
PONG_TIMEOUT = 3
ip = "0.0.0.0"
port = 8765
motor_handler = MotorHandler()
motor_handler.init()
ping_tasks = {}
async def handler(websocket):
ping_tasks[websocket] = asyncio.create_task(ping_sender(websocket))
try:
while True:
message = await websocket.recv()
print(f'Received a message {message}')
try:
if message == "pong":
continue
data_dict = json.loads(message)
command = WebsocketCommand(**data_dict)
print(f"Command parsed ${command}")
print(f"Type == {command.type} {type(command.type)}")
if command.type is WebsocketCommandType.MOTOR:
print(f"Motor change")
motor_handler.set(command.x, command.y)
except JSONDecodeError:
print("Incorrect JSON")
except ConnectionClosed:
print("Connection closed!")
motor_handler.stop()
finally:
if websocket in ping_tasks: # Check if websocket exists before cancellation
ping_tasks[websocket].cancel()
ping_tasks.pop(websocket)
async def ping_sender(websocket):
print("Ping started")
while True:
await asyncio.sleep(PING_INTERVAL)
try:
await websocket.send(PING_MESSAGE)
await asyncio.wait_for(websocket.recv(), timeout=PONG_TIMEOUT)
print("Ping-Pong done")
except (asyncio.TimeoutError, ConnectionClosed):
print("Ping timeout, connection might be closed")
break
async def main():
print(f"Starting server on {ip}:{port}")
async with serve(handler, ip, port):
await asyncio.Future() # run forever
asyncio.run(main())
- At the top of it, we have some constants. Like ping intervals, ip, and port.
- Then, the main method runs a WebSocket server on the defined port.
- If a phone or computer connects to the Raspberry PI, we will receive a WebSocket using a handler method.
- Next, we are starting pings to keep this connection alive.
- Then, an infinite loop reads messages from the WebSocket, parses them, and sends them to the motor handler.
- The next file is the Motor Handler class.
- It has defined two fields—motor_forward and motor_backward. We assigned a channel (or pin) to each of them, 12 and 13, accordingly.
- We init both pins.
- Then, we set a value to one of the pins or zero to another. It’s important not to send a PWM signal to both pins simultaneously. The motor can’t rotate in both directions at the same time.
pwm_control.py file:
class PwmControl:
_pwms = {}
def init(self, channel: Channel):
if channel in self._pwms:
raise AlreadyStartedException("Already initialized.")
else:
print(f"Init {channel}")
pwm = HardwarePWM(pwm_channel=channel.value, hz=frequency, chip=2)
pwm.start(0)
self._pwms[channel] = pwm
def set(self, channel: Channel, value: int):
pwm = self._pwms[channel]
if not pwm:
raise ChannelNotFoundException("Channel not found. Did you init it first?")
value = min(value, 100)
value = max(value, 0)
print(f"Set {channel} {value}")
pwm.change_duty_cycle(value)
def stop(self, channel: Channel):
pwm = self._pwms[channel]
if not pwm:
raise ChannelNotFoundException("Channel not found. Should it be stopped?")
- In the init method, we start with a zero value.
- In the set method, we correct the value. We guarantee that it will be from zero to one hundred, and we change the duty cycle of the pin. This will increase or decrease gaps in Pulse Width Modulation and adjust the motor's speed.
That’s it for the Raspberry PI code.
Mobile cross-platform codeIt’s time to move on to the cross-platform mobile application.
Let’s open Android Studio.
It’s essential to open a mobile folder in the Studio. It has a flutter project inside.
This project can be built for iOS, Android, and the web. I will show you an Android example.
All the magic happens in the lib folder.
I have main.dart file, which is an entry point.
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Raspberry PI 5 PWM Motor',
theme: ThemeData(
brightness: Brightness.dark,
),
home: const SliderScreenWidget(),
);
}
}
I have a websocket_client.dart file that connects to the Raspberry PI and sends commands:
class WebSocketClient {
.....................................
WebSocketChannel? _channel = null;
void connect() async {
try {
print("connect");
_channel = WebSocketChannel.connect(
Uri.parse('ws://192.168.1.73:8765'),
);
_listen();
} catch (error) {
print("Caught");
_scheduleReconnect();
}
}
.....................................
Future<void> send(WebsocketCommand command) async {
var channel = _channel;
if (channel != null) {
await channel.ready;
channel.sink.add(command.toJson());
}
}
.....................................
}
Here, I have a hardcoded IP address for the Pi server.
In my case, I have an IP 192.168.1.73, and I need to put it into that file.
Last but not least, a slider_screen.dart file. It’s a file with the UI:
We can connect the phone using a USB cable and run the project.
We can see that GPIO 13 receives positive values in a terminal window, and the motor starts to rotate.
The results:
That’s it. We can control this motor simply by dragging a slider.
Let me know in the comments if you have any questions.
Thank you for reading!
Comments