DCMotor: two-input H-bridges brake when going backwards; add a mode with PWM on both inputs
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 35/100
- Issue type
- Feature
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- csharp
- Domain
- embedded-iot
Research direction
The proposal identifies DCMotor.cs and ExplorerHat/Motors.cs, and notes that work should start after the PR for #2613. First read that PR and the existing DCMotor2PinNoEnable implementation to understand the factory and channel lifecycle. The issue leaves API shape, braking behavior, and whether ExplorerHat should switch open for discussion; completion depends on those decisions and tests for both motor directions.
Written by the indexing model from the issue text.
Description
Problem: backwards is much slower than forwards with two-input H-bridges
With two-input H-bridges such as the DRV8833 (also L9110S, MX1508, DRV8871...), DCMotor.Create(speedPin, directionPin) (DCMotor2PinNoEnable) turns the motor much slower backwards than forwards at the same |Speed|:
Speed |
Forwards (pulses/s) | Backwards (pulses/s) | Backwards / forwards |
|---|---|---|---|
| 0.4 | 44.3 | 15.9 | ~36 % |
| 0.6 | 52.3 | 29.8 | ~57 % |
| 0.8 | 57.7 | 46.7 | ~81 % |
| 1.0 | 61.5 | 61.4 | 100 % |
Measured on a Raspberry Pi 3 B+ with the Pimoroni Explorer HAT Pro (DRV8833; Iot.Device.ExplorerHat, which uses DCMotor.Create(19, 20) and DCMotor.Create(21, 26)), Iot.Device.Bindings 4.2.0, two TT gear motors with the wheels in the air, and an LM393 slotted speed sensor on a 20-slot disc on each wheel (1 s to reach the speed, 3 s counting; both wheels gave similar values, the table shows the average).
The duty cycle math is right (this was discussed in #844): the motor gets voltage for the same fraction of the cycle in both directions. What changes is the state of the H-bridge in the off part of the cycle:
- Forwards: the direction pin is low and the PWM pin goes high/low, so in the off part both inputs are low and the DRV8833 lets the motor coast.
- Backwards: the direction pin is high and the PWM is inverted (
1.0 + val), so in the off part both inputs are high and the DRV8833 brakes the motor (it shorts its terminals: dynamic braking).
I checked it by sampling the pins with pinctrl while the motors were running. At Speed 0.4, forwards: 43 % driving, 57 % coasting; backwards: 43 % driving, 57 % braking.
A higher PWM frequency does not help. With my own SoftwarePwmChannel (both wheels, pulses per second):
| Frequency | 0.4 forwards / backwards | 0.6 forwards / backwards |
|---|---|---|
50 Hz (DCMotor default) |
43 / 17 | 52 / 31 |
| 100 Hz | 43 / 14 | 52 / 34 |
| 200 Hz | 41 / 18 | 52 / 34 |
| 1000 Hz | 45 / 29 | 44 / 29 |
Up to 200 Hz, the off time is much longer than the electrical time constant of the motor, so it brakes the same at any frequency. From ~500 Hz on, SoftwarePwmChannel on the Pi 3 no longer follows the duty cycle (at 1000 Hz, 0.4 and 0.6 give the same speed), and only a hardware PWM of several kHz would change the result.
Proposal: a DC motor with a PWM channel on each input
A DC motor mode with a PWM channel on each input, as the Pimoroni Python library for the Explorer HAT does (GPIO.PWM at 100 Hz on both pins): forwards, PWM on the first input and the second one low; backwards, PWM on the second input and the first one low. The motor coasts in the off part of the cycle in both directions. DCMotor2PinNoEnable can't do this because it only gets one PWM channel, so it would be a new factory method, for example:
// New: one PWM channel on each input of a two-input H-bridge (DRV8833, L9110S...)
public static DCMotor Create(PwmChannel forwardChannel, PwmChannel backwardChannel, bool shouldDispose = true);
Speed> 0:forwardChannel.DutyCycle = Speed,backwardChannel.DutyCycle = 0.Speed< 0:forwardChannel.DutyCycle = 0,backwardChannel.DutyCycle = -Speed(the channel of the other direction is set to 0 first).Speed= 0: both 0 (coast).
The direction is the same as DCMotor.Create(speedPin, directionPin) when forwardChannel is on speedPin, so existing wiring keeps working.
I tried this in my robot with two SoftwarePwmChannel at 100 Hz per motor (the DCMotor2PinNoEnable column comes from the first table):
Speed |
Forwards | Backwards, DCMotor2PinNoEnable |
Backwards, PWM on both inputs |
|---|---|---|---|
| 0.4 | 41.5 | 15.9 (~36 %) | 42.4 (102 %) |
| 0.6 | 51.2 | 29.8 (~57 %) | 47.7 (93 %) |
| 0.8 | 56.7 | 46.7 (~81 %) | 56.5 (100 %) |
| 1.0 | 60.5 | 61.4 (100 %) | 60.5 (100 %) |
With the pins sampled again, the inputs were never both high. The cost: with software PWM there are two threads per motor instead of one (on the Pi 3 B+, the process went from ~15 % to ~20–25 % of one core with both motors running).
Then Iot.Device.ExplorerHat could use it in Motors, as the Pimoroni library does. That changes its behavior (backwards becomes as fast as forwards, as users would expect) and needs two more PWM threads, so I'd like to know if you agree before changing it. It touches the same files as #2613 (DCMotor.cs and ExplorerHat/Motors.cs), so I would start after that PR.
Questions:
- Is the factory method above a good shape for the API, or do you prefer another one (for example, a public class or an option on an existing method)?
- Should it also allow the braking variant (both inputs high in the off part, "slow decay")? Some users may prefer it, but then both directions should brake, not only backwards.
- Should
Iot.Device.ExplorerHatswitch to the new mode?
- Dominant language
- C#
- Stars
- 2.4k
- Forks
- 630
- Avg merge
- 6d 17h
- Merged PRs (30d)
- 10
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- No contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from dotnet/iot
-
LibGpiodV2EventObserver never disposes edge events: one finalizable SafeHandle per edge causes multi-ms gen0 GC pausesPossibly taken @krwq claimed this 7 days ago. Openuntriaged
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
dotnet/iot#2626 · 1 comment · 1 reaction · 2 assignees ·
Maintainers usually reply within 1 day
-
GpioController.QueryComponentInformation() throws NotSupportedException with RaspberryPi3DriverOpenuntriaged
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
api-suggestion untriaged
Difficulty 3/5 1-2 days Newbie friendliness 35/100
dotnet/iot#2620 · 3 comments ·
Maintainers usually reply within 1 day
-
untriaged
Difficulty 4/5 3-5 days Newbie friendliness 30/100
Maintainers usually reply within 1 day
-
untriaged
Difficulty 3/5 1-2 days Newbie friendliness 76/100
dotnet/iot#2614 · 2 comments ·
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
Maintainers usually reply within 3 days
-
Difficulty 1/5 Under an hour Newbie friendliness 85/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
bug mac アプリ: コード アプリ: チャット 優先: 中 要トリアージ
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
yksr-melt/Meltype#1346 · 2 comments ·
Maintainers usually reply within 1 day