Bafang Headlight Circuit Current Limit

Having just replaced Rev 1 of the amber running light with Rev 3 (about which, more later) on Mary’s Tour Easy, both the front and rear lights began blinking erratically. Given that they have completely independent circuitry, this strongly suggests a power problem.

Herewith, the headlight circuit voltage:

Bafang headlight voltage - two 1 W running lights
Bafang headlight voltage – two 1 W running lights

The voltage should be a constant 6 or 6.3 V, depending on which description you most recently read. That is the case with only one light attached, so the problem occurs only when running both lights.

The four pulses come from the amber LED’s Morse code “b” (dah-dit-dit-dit) with a 85 ms dits; the first dah pulse should be three times longer than the dits and definitely isn’t. The rear light’s red LED stays on continuously, except for two dark dits, so it draws a constant current and does not produce any changes in this trace.

Both lights have 2.0 Ω sense resistors setting the LED current to 400 mA, which corresponds to 250 mA each from the Bafang controller’s 6.3 V headlight circuit. The headlight circuit’s total of 500 mA should work fine, although the “spec” seems to be basically whatever the OEM headlight requires.

The Rev 1 amber light ran the LED at 360 mA with a supply current around 450 mA. That light and the rear light on the back ran fine, so the supply seems to have a hard maximum current limit at (a bit less than?) 500 mA.

The least-awful solution seems to be backing off both LED currents to 360 mA to keep the total supply current well under 500 mA.

Running Light Waveforms: A Closer Look

A test setup on the bench allows a bit more room for probes:

1 W Amber LED - MP1584 pulse setup
1 W Amber LED – MP1584 pulse setup

Some heatsink tape holds the LED to the far side of that oversize heatsink.

The input signal (top trace) arrives from a function generator set to blip the MP1584 regulator’s Enable input at 4 Hz with a 7 ms pulse:

Amber 1 w LED - pulse 200 mA-div
Amber 1 w LED – pulse 200 mA-div

The purple trace is the voltage across the 2 Ω sense resistor. The MP1584 datasheet says the regulator soft-starts for (typically) 1.5 ms, during which the output ramps upward at 600 mV/ms to 800 mV , whereupon the actual regulation commences. The amber LED forward drop adds 2.5 V to the sense voltage, so the regulator produces 3.3 V from the 6.3 V bench supply input.

The cyan trace is the output current through the LED and sense resistor, also ramping up to 800 mV/2 Ω = 400 mA to drive the LED at 1 W.

The furry section shows when the regulator is actively regulating, with the output voltage rising and falling over a small range to maintain the average current (via the sense voltage). Successive Enable pulses may have longer, shorter, or completely missing fur, with no predictable pattern. Increasing the duty cycle doesn’t affect the results, with the fur sometimes extending for the entire pulse and sometimes being completely missing.

I think the regulator can settle in one of two metastable states. The best case has a constant voltage producing a constant LED current, with the sense voltage remaining within whatever deadband keeps the error amplifier happy. When something knocks the sense voltage out of the deadband, the error amp starts the usual regulation cycle, which will stop when the minimum or maximum voltage of a cycle remains within the deadband:

Amber 1 w LED - pulse - detail - 200 mA-div
Amber 1 w LED – pulse – detail – 200 mA-div

The ripple shows the regulator running at three cycles per 20 µs division = 150 kHz, far lower then the MP1584 datasheet’s maximum 1.5 MHz and the typical 500 kHz in the test circuits. Perhaps a low frequency lets the designers use a cheap PCB and not worry about pesky EMI issues.

In any event, during this pulse the ripple amplitude gradually decreased as the output voltage settled at the point where the error voltage variation stayed within the deadband. The typical amp gain is only 200 V/V, so it’s definitely less fussy than something build around an op amp.

For whatever it’s worth, a 7 ms flash from a 1 W amber LED at 4 Hz is way attention-getting in a dim Basement Laboratory. You wouldn’t need an Arduino to produce that signal, even though I like the Morse capability.

Tour Easy Rear Running Light: Current Waveforms

There’s just enough slack in the LED wiring to clip a Tek current probe in there:

Tour Easy Rear Running Light - regulator wiring
Tour Easy Rear Running Light – regulator wiring

Which reveals the LED current waveform:

Red LED - LED current - 100 mA-div
Red LED – LED current – 100 mA-div

The LED is on continuously, except for the two 75 ms Morse code dits in the upper trace.

The lower trace shows the current ramping up at the end of the first dit, from zero to 400 mA in 1.5 ms.

Clamping the probe around the 6.3 V power supply lead:

Red LED - power supply - 100 mA-div
Red LED – power supply – 100 mA-div

The supply current includes maybe 20 mA for the Arduino running the Morse code program and the current ramps up from there to about 250 mA when the LED is on.

The LED drops 2.6 V at 400 mA, so it dissipates a smidge over 1 W. The 2.0 Ω current sense resistor (3.3 Ω in parallel with 5.1 Ω) dissipates 800 mV × 400 mA = 320 mW.

The dissipation from the Bafang headlight output, including the Arduino, is 1.6 W.

The running light ticks along at the hot side of comfortably warm on the Electronics Workbench and runs barely warm in free air out on the bike, so I’ll define it to be Good Enough™.

Tour Easy 1 W Amber Running Light: Holder and First Light

Wrapping a left-side ball mount around the PVC case produced a holder:

Fairing 1 W LED Mount - Left side - show view
Fairing 1 W LED Mount – Left side – show view

Which looks like this in real life:

1 W Amber Running Light - installed front
1 W Amber Running Light – installed front

The support structure under the arch required a bit more cleanup than it got, so the clamp didn’t quite close around the ball on the first full test:

1 W Amber Running Light - installed side
1 W Amber Running Light – installed side

Both the phone camera and the eyeballometer report the 1 W amber LED isn’t quite as bright as the 400 lumen Anker flashlight on its low setting:

1 W Amber Running Light - First Light
1 W Amber Running Light – First Light

Stir the unusual (for a bike) amber color together with some blinkiness, though, and it’s definitely attention-getting.

The OpenSCAD source code as a GitHub Gist:

// Tour Easy Fairing Flashlight Mount
// Ed Nisley KE4ZNU - July 2017
// August 2017 -
// August 2020 - add reinforcing columns under mount cradle
// August 2021 - 1 W Amber LED
/* [Build Options] */
FlashName = "1WLED"; // [AnkerLC40,AnkerLC90,J5TactV2,InnovaX5,Sidemarker,Clearance,Laser,1WLED]
Component = "BallClamp"; // [Ball, BallClamp, Mount, Plates, Bracket, Complete]
Layout = "Build"; // [Build, Show]
Support = true;
MountSupport = true;
/* [Hidden] */
ThreadThick = 0.25; // [0.20, 0.25]
ThreadWidth = 0.40; // [0.40]
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
Protrusion = 0.01; // [0.01, 0.1]
HoleWindage = 0.2;
/* [Fairing Mount] */
Side = "Right"; // [Right,Left]
ToeIn = -10; // inward from ahead
Tilt = 20; // upward from forward (M=20 E=10)
Roll = 0; // outward from top
//- Screws and inserts
/* [Hidden] */
ID = 0;
OD = 1;
/* [Hidden] */
ClampInsert = [3.0,4.2,8.0];
ClampScrew = [3.0,5.9,35.0]; // thread dia, head OD, screw length
ClampScrewWasher = [3.0,6.75,0.5];
ClampScrewNut = [3.0,6.1,4.0]; // nyloc nut
/* [Hidden] */
F_NAME = 0;
LightBodies = [
//- Fairing Bracket
// Magic numbers taken from the actual fairing mount
/* [Hidden] */
inch = 25.4;
BracketHoleOD = 0.25 * inch; // 1/4-20 bolt holes
BracketHoleOC = 1.0 * inch; // fairing hole spacing
// usually 1 inch, but 15/16 on one fairing
Bracket = [48.0,16.3,3.6 - 0.6]; // fairing bracket end plate overall size
BracketHoleOffset = (3/8) * inch; // end to hole center
BracketM = 3.0; // endcap arc height
BracketR = (pow(BracketM,2) + pow(Bracket[1],2)/4) / (2*BracketM); // ... radius
//- Base plate dimensions
Plate = [100.0,30.0,6*ThreadThick + Bracket[2]];
PlateRad = Plate[1]/4;
RoundEnds = true;
echo(str("Base plate thick: ",Plate[2]));
//- Select flashlight data from table
echo(str("Flashlight: ",FlashName));
FlashIndex = search([FlashName],LightBodies,1,0)[F_NAME];
//- Set ball dimensions
BallWall = 5.0; // max ball wall thickness
echo(str("Ball wall: ",BallWall));
BallOD = IntegerMultiple(LightBodies[FlashIndex][F_GRIPOD] + 2*BallWall,1.0);
echo(str(" OD: ",BallOD));
BallLength = IntegerMultiple(min(sqrt(pow(BallOD,2) - pow(LightBodies[FlashIndex][F_GRIPOD],2)) - 2*4*ThreadThick,
echo(str(" length: ",BallLength));
BallSides = 8*4;
//- Set clamp ring dimensions
//ClampOD = 50;
ClampOD = BallOD + 2*5;
echo(str("Clamp OD: ",ClampOD));
ClampLength = min(20.0,0.75*BallLength);
echo(str(" length: ",ClampLength));
ClampScrewOC = IntegerMultiple((ClampOD + BallOD)/2,1);
echo(str(" screw OC: ",ClampScrewOC));
TiltMirror = (Side == "Right") ? [0,0,0] : [0,1,0];
//- Adjust hole diameter to make the size come out right
module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes
Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
FixDia = Dia / cos(180/Sides);
cylinder(r=(FixDia + HoleWindage)/2,h=Height,$fn=Sides);
//- Fairing Bracket
// This part of the fairing mount supports the whole flashlight mount
// Centered on screw hole
module Bracket() {
difference() {
translate([(Bracket[0]/2 - BracketHoleOffset),0,0])
intersection() {
union() {
for (i=[-1,0,1]) // middle circle fills gap
translate([i*(Bracket[0]/2 - BracketR),0])
circle(d=BracketHoleOD/cos(180/8),$fn=8); // dead center at the origin
//- General plate shape
// Centered in the middle of the plate
module PlateBlank() {
difference() {
intersection() {
translate([0,0,Plate[2]/2]) // select upper half of spheres
if (RoundEnds)
for (i=[-1,1])
translate([i*(Plate[0]/2 - PlateRad),0,0])
sphere(r=PlateRad); // nice round ends!
for (i=[-1,1], j=[-1,1])
translate([i*(Plate[0]/2 - PlateRad),j*(Plate[1]/2 - PlateRad),0])
sphere(r=PlateRad); // nice round corners!
translate([BracketHoleOC,0,-Protrusion]) // punch screw holes
//- Inner plate
module InnerPlate() {
difference() {
translate([-BracketHoleOC,0,Plate[2] - Bracket[2] + Protrusion]) // punch fairing bracket
//- Outer plate
// With optional legend for orientation and parameters
module OuterPlate(Legend = true) {
TextRotate = (Side == "Left") ? 0 : 180;
difference() {
if (Legend)
linear_extrude(height=3*ThreadThick + Protrusion) {
translate([BracketHoleOC + 15,0,0])
translate([-BracketHoleOC,8,0]) rotate(TextRotate)
text(text=str("Toe ",ToeIn),size=5,spacing=1.20,font="Arial",halign="center",valign="center");
translate([-BracketHoleOC,-8,0]) rotate(TextRotate)
text(text=str("Tilt ",Tilt),size=5,spacing=1.20,font="Arial",halign="center",valign="center");
translate([BracketHoleOC,-8,0]) rotate(TextRotate)
translate([BracketHoleOC,8,0]) rotate(TextRotate)
text(text=str("Roll ",Roll),size=5,spacing=1.20,font="Arial",halign="center",valign="center");
//- Slotted ball around flashlight
// Print with brim to ensure adhesion!
module SlotBall() {
NumSlots = 8*2; // must be even, half cut from each end
SlotWidth = 2*ThreadWidth;
SlotBaseThick = 10*ThreadThick; // enough to hold finger ends together
RibLength = (BallOD - LightBodies[FlashIndex][F_GRIPOD])/2;
translate([0,0,(Layout == "Build") ? BallLength/2 : 0])
rotate([0,(Layout == "Show") ? 90 : 0,0])
difference() {
intersection() {
sphere(d=BallOD,$fn=2*BallSides); // basic ball
cube([2*BallOD,2*BallOD,BallLength],center=true); // trim to length
PolyCyl(LightBodies[FlashIndex][F_GRIPOD],2*BallOD,BallSides); // remove flashlight body
for (i=[0:NumSlots/2 - 1]) { // cut slots
SlotCutterLength = LightBodies[FlashIndex][F_GRIPOD];
rotate(a + 360/NumSlots)
if (Support && (Layout == "Build")) {
for (i=[0:NumSlots-1]) {
a = i*360/NumSlots;
rotate(a + 180/NumSlots)
translate([(LightBodies[FlashIndex][F_GRIPOD] + RibLength)/2 + ThreadWidth,0,BallLength/(2*4)])
//- Clamp around flashlight ball
BossLength = ClampScrew[LENGTH] - 1*ClampScrewWasher[LENGTH];
BossOD = ClampInsert[OD] + 2*(6*ThreadWidth);
module BallClamp(Section="All") {
difference() {
union() {
intersection() {
sphere(d=ClampOD,$fn=BallSides); // exterior ball clamp
cube([ClampLength,2*ClampOD,2*ClampOD],center=true); // aiming allowance
for (j=[-1,1])
sphere(d=(BallOD + 1*ThreadThick),$fn=BallSides); // interior ball with minimal clearance
for (j=[-1,1]) {
translate([0,j*ClampScrewOC/2,-ClampOD]) // screw clearance
translate([0,j*ClampScrewOC/2, // insert clearance
-0*(BossLength/2 - ClampInsert[LENGTH] - 3*ThreadThick) + Protrusion])
translate([0,j*ClampScrewOC/2, // insert transition
-(BossLength/2 - ClampInsert[LENGTH] - 3*ThreadThick)])
if (Section == "Top")
else if (Section == "Bottom")
if (Support) { // ad-hoc supports
NumRibs = 6;
RibLength = 0.5 * BallOD;
RibWidth = 1.9*ThreadWidth;
SupportOC = ClampLength / NumRibs;
if (Section == "Top") // base plate for adhesion
cube([ClampLength + 6*ThreadWidth,RibLength,2*ThreadThick],center=true);
else if (Section == "Bottom")
cube([ClampLength + 6*ThreadWidth,RibLength,2*ThreadThick],center=true);
intersection() {
sphere(d=BallOD - 0*ThreadWidth); // cut at inner sphere OD
cube([ClampLength + 2*ThreadWidth,RibLength,BallOD],center=true);
if (Section == "Top") // select only desired section
else if (Section == "Bottom")
union() { // ribs for E-Z build
for (j=[-1,0,1])
for (i=[0:NumRibs]) // allow NumRibs + 1 to fill the far end
translate([i*SupportOC - ClampLength/2,0,0])
cylinder(d=BallOD - 2*ThreadThick,
//- Mount between fairing plate and flashlight ball
// Build with support for bottom of clamp screws!
module Mount() {
MountShift = [ClampOD*sin(ToeIn/2),0,ClampOD/2];
mirror(TiltMirror) {
intersection() {
translate([0,0,Plate.x/2 + 3*ThreadThick])
if (MountSupport) // anchor outer corners at worst overhang
color("Yellow") {
RibWidth = 1.9*ThreadWidth;
SupportOC = 0.1 * ClampLength;
intersection() {
difference() {
translate([(ClampOD - BallOD)*sin(ToeIn/2),0,3*ThreadThick]) // Z = avoid legends
for (i=[-4.5,-2.5,0,2.0,4.5])
translate([i*SupportOC - 0.0,0,(5 + Plate[2])/2])
cube([RibWidth,0.7*ClampOD,(5 + Plate[2])],center=true);
sphere(d=ClampOD - 2*ThreadWidth,$fn=BallSides);
//- Build things
if (Component == "Bracket")
if (Component == "Ball")
if (Component == "BallClamp")
if (Layout == "Show")
else if (Layout == "Build")
if (Component == "Mount")
if (Component == "Plates") {
OuterPlate(Legend = false);
if (Component == "Complete") {
mirror(TiltMirror) {
translate([0,0,ClampOD/2 + BossOD*abs(sin(ToeIn))]) {

Tour Easy 1 W Amber Running Light: Firmware

Rather than conjure a domain specific language to blink an LED, it’s easier to use Morse code:

Herewith, Arduino source code using Mark Fickett’s Morse library to blink an amber running light:

// Tour Easy Running Light
// Ed Nisley - KE4ZNU
// September 2021

#include <morse.h>

#define PIN_OUTPUT	13

LEDMorseSender Morser(PIN_OUTPUT,(float)10.0);

void setup()

    Morser.setMessage(String("qst de ke4znu "));

//    Morser.setWPM((float)3.0);
	Morser.setMessage(String("s   "));

void loop()
	if (!Morser.continueSending())


Bonus: a trivially easy ID string.

A dit time of 50 ms produces a brief flash that’s probably about as fast as it can be, given that the regulator must ramp the LED current up from zero after its Enable input goes high. In round numbers, a 50ms dit corresponds to 24 WPM Morse.

Each of the three blanks after the “s” produces a seven element word space to keep the blinks from running together.

Sending “b ” (two blanks) with a 75 ms dit time may be more noticeable. You should tune for maximum conspicuity on your rides.

1 W Amber Running Light - installed front
1 W Amber Running Light – installed front

On our first ride, Mary got a friendly wave from a motorcyclist, an approving toot from a driver, and several “you go first” gestures at intersections.

Works for us …

Tour Easy 1 W Amber Running Light: Circuitry

With the internal slab attached to the 1 W LED heatsink, some double-sided foam tape affixes an Arduino Nano to one side of the slab:

1 W LED Running Light - Arduino side
1 W LED Running Light – Arduino side

The MP1584 regulator and its 2.2 Ω current-sensing resistor (tacked down with acrylic adhesive) go on the other side:

1 W LED Running Light - Regulator side
1 W LED Running Light – Regulator side

The Arduino and regulator draw power from the Bafang motor controller’s 6.3 V headlight circuit. The 2.2 Ω resistor sets the LED current to 360 mA = 900 mW. The blue wire connects the Arduino’s default LED output pin (D13) to the regulator’s Enable input (pin 2) to allow programmatic blinkiness.

The end view shows everything Barely Fits™:

1 W LED Running Light - internal assembly
1 W LED Running Light – internal assembly

All it needs is a rear cover of some sort …

Tour Easy 1 W Amber Running Light: Internal Plate

A semi-scaled doodle laying out an Arduino Nano and the MP1584 regulator board suggested they might fit behind the heatsink with the 1 W LED:

Amber running light - board layout doodle - side
Amber running light – board layout doodle – side

A somewhat more detailed doodle of the end view prompted me to bore the PVC pipe out to 23 mm:

Amber running light - board layout doodle - end
Amber running light – board layout doodle – end

The prospect of designing a 3D printed holder for the boards suggested Quality Shop Time combined with double-stick foam tape would ensure a better outcome.

So I bandsawed the remains of a chunky angle bracket into a pair of rectangles, flycut All The Sides to square them up, and tapped a pair of M3 holes along one edge of each:

1 W LED Running Light - baseplate tapping
1 W LED Running Light – baseplate tapping

The other long edges got the V groove that killed the Sherline’s Y axis nut:

Sherline Y-Axis Nut Mishap - setup
Sherline Y-Axis Nut Mishap – setup

The groove holds a length of 4 mm OD (actually 5/32 inch, but don’t tell anybody) brass tubing:

1 W LED Running Light - baseplate trial fit
1 W LED Running Light – baseplate trial fit

The M3 button head screws are an admission of defeat, as I could see no way of controlling the width + thickness of the aluminum slabs to get a firm push fit in the PVC tube. The screws let me tune for best picture after everything else settled out.

A little more machining opened up the top of the groove:

1 W LED Running Light - baseplate dry assembly
1 W LED Running Light – baseplate dry assembly

A short M3 button head screw (with its head turned down to 4 mm) drops into the slot and holds the slab to the threaded hole in the LED heatsink. The long screw is holding the threaded insert in place for this dry fit.

I doodled a single long screw through the whole thing, but having it fall off the heatsink when taking the rear cover off seemed like a Bad Idea™. An M3 button head screw uses a 2 mm hex key that fits neatly through the threaded insert, thereby making it work.

Butter it up with epoxy, scrape off the excess, and let things cure:

1 W LED Running Light - baseplate curing
1 W LED Running Light – baseplate curing

This was obviously made up as I went along …