Designing an Electronic Speed Controller (ESC) for Brushless Motors

Designing an Electronic Speed Controller (ESC) for Brushless Motors

Some parts of a project exist because they are necessary. Others exist because you want to understand them well enough to build them yourself. This ESC was both.

It started as part of Nanolith, an open-source 3-inch drone platform I’m building around a pretty simple idea: instead of treating the frame, the flight controller, and the ESCs as separate boards and separate problems, merge them into a much more integrated system. The long-term goal is a monolithic PCB drone structure where electronics and mechanics are designed together, not stacked on top of each other as an afterthought. In that architecture, LithCore handles flight control, LithDrive handles motor power, and both eventually live inside the same broader hardware platform. For now though, I’m developing the subsystems one by one, because trying to validate everything at once is how you end up debugging five failures with one oscilloscope and no certainty about anything.

There was also a more personal reason for building this board. I like knowing how things work below the abstraction layer people usually stop at. Buying an ESC is easy. Flashing firmware onto a commercial board is easy too. Designing one, routing one, assembling one, finding out why it coggs like hell, discovering that the problem is your own stupid net label mistake, then fixing it with a microscope and a thin wire — that teaches you something real.

So this project became my way of learning brushless motor control properly, not just using it.

image

The board itself is a sensorless three-phase BLDC ESC built around an STM32F051C8T6 running AM32-compatible firmware. The general architecture is straightforward on paper. Battery voltage comes in on VBAT, local regulation generates 5 V and 3.3 V, the MCU produces the six control signals for the power stage, a DRV8300 drives the gates, and three half-bridges built from six N-channel MOSFETs switch the motor phases. Current is measured through a low-side shunt and amplifier, battery voltage is divided down for sensing, and the floating phase back-EMF is observed for zero-crossing detection. That is the clean version. Real life was less clean.

On the power side, I went with a classic three half-bridge arrangement: one high-side and one low-side MOSFET per phase, tied to the motor outputs MOTORA, MOTORB, and MOTORC. The initial revision used PSMN1R4-40YLDX-VB MOSFETs, which are honestly a bit oversized for this prototype, but they gave me margin and removed one source of uncertainty during bring-up. Each gate is driven through a 20 Ω series resistor, and the gates are controlled by a DRV8300 three-phase gate driver. The high-side bootstrap supply is generated with diodes and capacitors on each phase, which is the standard trick here: recharge when the switching node is pulled low, then use that stored charge to drive the high-side gate above the source. Elegant enough, as long as you respect the constraints.

For sensing, I added a 1 mΩ low-side shunt with an INA180 current amplifier so the MCU can see phase current indirectly, plus a resistor divider for battery voltage monitoring. The sensorless commutation side uses three back-EMF sense lines — BEMFA, BEMFB, and BEMFC — referenced against a shared BEMF_COMMON network. That part matters a lot. A sensorless ESC lives or dies by how cleanly it can observe the undriven phase and decide when to commutate. On a block diagram it looks boring. On real hardware it is where the fun begins.

SCH_Schematic1_1-LithDrive_2026-03-24

The design phase was probably the most deceptively calm part of the project. Schematic capture feels productive because everything makes sense while you’re doing it. The MCU pins are assigned, the regulators are there, the bootstrap network is there, the current sense is there, the motor connector is there, SWD is there, signal input is there. You zoom out and it looks like a finished machine. That illusion lasts right until layout and bring-up remind you that electrons do not care about your nice net labels.

I spent a lot of time thinking about routing because this board sits right at the uncomfortable boundary between power electronics and small-signal analog sensing. The high-current loops need to be short, the phase nodes need to stay away from the analog inputs, the gate traces need to be tight and clean, and the BEMF lines need enough protection from switching noise that the ADC doesn’t end up reading garbage. Later, on the revised version, I also added optional RC filtering on the BEMF inputs because I wanted a way to tame high-frequency ringing and dv/dt-related junk without making the ADC source impedance ridiculous. That balance matters more than I expected.

The first assembly work was reassuring, at least briefly. I soldered the STM32 and some decoupling capacitors, connected through SWD, and confirmed I could read memory. That was one of those small moments that feels bigger than it should. A custom board talking back to a programmer is still satisfying every single time. After that I flashed the bootloader and firmware, and eventually managed to connect to the configuration tool. That part felt like momentum.

Then the real testing began.

After soldering the MOSFETs and feeding a PWM command into the signal input, the motor actually spun in open-loop on the first try. That was a good day. Not a perfect result, but enough to prove the board was alive and the basic power stage was not a complete disaster. And frankly, getting a first custom ESC to spin a motor without instantly vaporizing itself is already a small victory. Nobody should pretend otherwise.

The next days were much messier. I burned a servo tester because I connected power where I obviously should not have. Very elegant. I replaced it with an ESP32-C3 generating a 50 Hz control signal, but that introduced another layer of uncertainty because the generated signal was not great. Testing that same signal on a commercial ESC showed weak startup behavior too, so now I had two overlapping possibilities: either my ESC had a problem, or my command source was bad, or both. That kind of ambiguity is annoying because you can waste hours debugging the wrong layer of the stack.

Once I improved the control signal generation, the motor would turn, but badly. Lots of cogging, rough behavior, unstable startup. Changing settings in the ESC configuration tool altered the behavior a little, but not enough. At that stage I was looking at several possible causes: wrong commutation timing, noisy BEMF sensing, poor zero-crossing detection, or some dumb schematic error hiding in plain sight. Usually it’s the last one, and of course it was.

The first bug I found was embarrassingly simple. During schematic capture, I duplicated the BEMFB label in the zero-crossing section and forgot to rename one of them to BEMFC. So two of the BEMF divider networks ended up tied to the same label while the actual BEMFC input on the MCU wasn’t connected correctly. That explained a lot. Sensorless commutation expects three distinct phase observations. Giving the firmware confused or missing phase information is a pretty effective way to get ugly startup behavior.

image

I prepared a corrected revision and, in parallel, started patching the existing board by hand. This is one of those moments where hardware development stops being elegant and becomes physical. Knife, magnet wire, microscope, continuity tests, swearing, repeat.

That still wasn’t the end of it.

Later I found another mistake, somehow even more ridiculous: the BEMF_COMMON network was not actually connected the way I thought it was. In the schematic, only one of the three branch resistors was truly tied into the node. The other two looked connected visually, but the actual junction was missing. The tiny schematic dot that says “yes, this is a node” was absent where it mattered. So the whole reference network for zero-crossing detection was wrong. That one hurt, because it was exactly the kind of mistake that feels obvious only after you’ve already lost time to it.

I fixed it with a small wire linking the three resistors properly.

And then it worked.

Not “kind of works.” It worked properly. Smooth rotation, no cogging, clean behavior. After all the false starts, that result felt almost suspicious. But the measurements and the bench behavior backed it up.

That debugging process also fed directly into the second revision of the board. I widened the battery power trace, added via stitching, moved passives to sizes that are less annoying to hand-assemble, improved the bootstrap diode choice, corrected the BEMF routing mistakes, added an LED for firmware status, and added the optional RC filters on the BEMF inputs. Those changes were not glamorous, but they were exactly the kind of improvements you only make after a prototype has forced you to stop guessing.

Fabrication itself was fairly standard, but it still deserves mention because this part always looks simpler in blog posts than it is on the bench. Once the PCB design was ready, I sent the revised board out for production and ordered the parts. When the boards arrived, assembly started with the low-risk section first: MCU, decoupling, regulators, and basic power validation. Only after confirming SWD access and successful flashing did I move on to the power stage. That order was deliberate. Soldering everything at once might save time if you are lucky, but it also makes fault isolation much worse when you are not.

There were a few smaller bring-up issues too. At one point I couldn’t flash properly because PB4 looked dead. PCB continuity checked out, but continuity from the resistor to the MCU pin did not, which turned out to be a simple bad solder joint on the microcontroller lead. Reflowed it, signal came back, problem gone. Annoying, but at least honest. Hardware has a nice way of reminding you that not every bug is deep.

{3EDD214D-B697-4EFB-86DB-C0887C05132B}

Once the ESC was finally running correctly, I started taking basic observations and measurements. On a 2S supply around 7.4 V, current draw looked reasonable on the bench with no load: about 0.02 A with the motor stopped, around 0.075 A at minimum running speed, roughly 0.275 A in the mid range, and about 0.386 A near maximum command. I also captured phase waveforms and gate-drive behavior for both low-side and high-side switching at different throttle positions, mostly to check that the commutation and drive signals looked coherent and to build a better intuition for what the ESC was actually doing electrically, not just functionally. Those measurements are not the end of validation, but they are the first layer of confidence. They tell me the board is no longer surviving by accident.

image

This ESC matters beyond itself because it is one of the core building blocks of Nanolith. The long-term project only makes sense if each subsystem can stand on its own before integration. A monolithic drone PCB sounds nice as a concept, but concepts are cheap. Boards that boot, switch current, detect back-EMF properly, and spin a motor smoothly are better. That is why I’m doing it in stages. First the ESC, then the flight controller, then the combined system. That approach is slower, but much less stupid.

The most useful part of the whole process was not the final smooth spin. It was the chain of small failures that got me there. The mislabeled BEMF signal. The missing node. The burned servo tester. The flaky command generation. The solder joint that looked fine until it didn’t. None of that is glamorous, but all of it is real. And honestly, if a hardware project story sounds too clean, either it is incomplete or someone is editing out the interesting parts.

This board now works, and that is satisfying. But the part I value most is that I understand it better now. Not just the theory of sensorless ESCs, not just the firmware interface, but the very practical gap between a correct-looking schematic and a board that actually behaves.

That gap is where the project became real.