Does anybody has schematics and code example on this 20A Ali board?
I need those schematics and code example too! I just burnt my second board and I’m a little hopeless in discovering how to use it.
Hi, on this page https://aliexpress.ru/item/1005009953049031.html?sku_id=12000050670260828&spm=a2g2x.productlist.search_results.1.65073eb5Olvntz has some information about the use, this is all I could find. As I understand it, you need to use pins as in the examples from the library
Have you managed to get this driver working?
Hello,
I’ve managed to get it working, here are some documentation and the code I’ ve used.
Just FYI, I’m using a fairly large BLDC with an incremental encoder (China ZLTECH 5.5inch 24V 150W 270RPM encoder DC in wheel hub motor for mobile robot Manufacturer and Supplier | Zhongling)
Dependencies
arduino-cli lib install ESP32Encoder@0.11.8
Pinout
Version: FOC_ESP32_V1.1
P2
| Header Pin | ESP Pin | ESP GPIO | - | Header Pin | ESP Pin | ESP GPIO |
|---|---|---|---|---|---|---|
| 1 - MOSI | 37 | GPIO_23 | - | 2 - SCL | 36 | GPIO_22 |
| 3 - MISO | 31 | GPIO_19 | - | 4 - SDA | 33 | GPIO_21 |
| 5 - SCLK | 30 | GPIO_18 | - | 6 - 3V3 | - | - |
| 7 - CS | 16 | GPIO_13 | - | 8 - GND | - | - |
| 9 - GND | - | - | - | 10 - GND | - | - |
| 11 - 3V3 | - | - | - | 12 - 3V3 | - | - |
P1
| Pin | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| Function | IO0 | GND | GND | RXD | TXD | 3V3 |
Driver
| Phase A (GPIO) | Phase B (GPIO) | Phase C (GPIO) | Enable (GPIO) |
|---|---|---|---|
| GPIO_16 | GPIO_17 | GPIO_5 | GPIO_4 |
Current Sense
- Shunt:
0.01 - Gain:
50
| Phase A (GPIO) | Phase B (GPIO) |
|---|---|
| GPIO_39 | GPIO_36 |
I didn’t managed to get current control working yet, but closed loop voltage control seems okay.
This is the code I’ve used,
code.ino
//------------------------------------------------------------ CLOSED LOOP Losing Count
// Open loop motor control example
#include <SimpleFOC.h>
#include "sd_encoder.h"
// BLDC motor & driver instance
// BLDCMotor( pp number , phase resistance, KV rating)
BLDCMotor motor = BLDCMotor(15 , 0.55, 2.0*7.518796992);
// BLDCDriver3PWM(phA, phB, phC, en)
BLDCDriver3PWM driver = BLDCDriver3PWM(16, 17, 5, 4);
// InlineCurrentSensor constructor
// - shunt_resistor - shunt resistor value
// - gain - current-sense op-amp gain
// - phA - A phase adc pin -> SENSOR_VN
// - phB - B phase adc pin -> SENSOR_VP
// - phC - C phase adc pin (optional)
InlineCurrentSense *current_sense;// = InlineCurrentSense(0.01, 50, GPIO_NUM_39, GPIO_NUM_36, _NC); // when measuring A and B phase currents and not measuring C
SDEncoderFOC encoder(GPIO_NUM_21, GPIO_NUM_19, 4096, encType::full);
// instantiate the commander
Commander command = Commander(Serial);
void doMotor(char* cmd) { command.motor(&motor, cmd); }
hw_timer_t *timer = NULL;
unsigned long loopTime = 0;
TaskHandle_t foc_task_handle;
SemaphoreHandle_t foc_semaphore;
void IRAM_ATTR onTimer() {
xSemaphoreGiveFromISR(foc_semaphore, NULL);
}
void focTask(void *pvParameters) {
for (;;) {
if (xSemaphoreTake(foc_semaphore, portMAX_DELAY) == pdTRUE) {
loopTime = _micros();
motor.loopFOC();
motor.move();
loopTime = _micros() - loopTime;
}
}
}
void setup() {
Serial.begin(115200);
SimpleFOCDebug::enable(&Serial);
setCpuFrequencyMhz(240);
uint32_t Freq = getCpuFrequencyMhz();
Serial.print("CPU Freq = ");
Serial.print(Freq);
Serial.println(" MHz");
encoder.init();
// encoder.enableInterrupts(doA, doB);
motor.linkSensor(&encoder);
// driver config
// power supply voltage [V]
Serial.println("Driver Init!");
driver.voltage_power_supply = 24;
driver.voltage_limit = 12;
driver.pwm_frequency = 40000;
driver.init();
// link the current sense and the driver
Serial.println("Link Driver CS!");
current_sense = new InlineCurrentSense(0.01, 50, GPIO_NUM_39, GPIO_NUM_36, _NC); // when measuring A and B phase currents and not measuring C
// link current sense and driver
motor.linkDriver(&driver);
// current_sense->linkDriver(&driver);
// velocity loop PID
motor.PID_velocity.P = 5.0;
motor.PID_velocity.I = 2.0;
motor.PID_velocity.D = 0.05;
motor.PID_velocity.output_ramp = 20.0;
motor.PID_velocity.limit = 12.0;
// Low pass filtering time constant
motor.LPF_velocity.Tf = 0.05;
// angle loop PID
motor.P_angle.P = 5.0;
motor.P_angle.I = 0.0;
motor.P_angle.D = 0.0;
motor.P_angle.output_ramp = 0.0;
// motor.P_angle.limit = 10.0;
// Low pass filtering time constant
// motor.LPF_angle.Tf = 0.0;
// Limits
motor.velocity_limit = 10.0;
motor.voltage_limit = 12.0;
motor.current_limit = 9.0; // 9 -> 3.6A
// pwm modulation settings
motor.foc_modulation = FOCModulationType::SpaceVectorPWM;
motor.modulation_centered = 1.0;
motor.torque_controller = TorqueControlType::voltage;
motor.controller = MotionControlType::velocity;
motor.voltage_sensor_align = 4; // 3 para current_sense
motor.sensor_direction = Direction::CW; // Pula detecção automática
motor.init();
// Serial.println("CS Init!");
// current_sense->skip_align = true;
if (current_sense->init())
{
Serial.println("Current sense init success!");
}
else{
Serial.println("Current sense init failed!");
return;
}
// link motor and current sense
// Serial.println("Link Motor CS!");
// motor.linkCurrentSense(current_sense);
// motor.useMonitoring(Serial);
// motor.monitor_downsample = 0;
// motor.monitor_downsample = 5000;
// motor.monitor_variables = _MON_TARGET | _MON_VEL | _MON_ANGLE;
// motor.monitor_variables = _MON_TARGET | _MON_VEL | _MON_ANGLE | _MON_CURR_Q | _MON_CURR_D;
// motor.monitor_variables = _MON_TARGET | _MON_CURR_Q | _MON_CURR_D;
// link the motor and the driver
Serial.println("Link Driver Motor!");
motor.initFOC();
Serial.println("Zero elétrico: ");
Serial.println(motor.zero_electric_angle, 6);
// add target command
command.add('M', doMotor, "Motor");
Serial.println("Motor ready!");
Serial.println("Set target velocity [rad/s]");
_delay(1000);
foc_semaphore = xSemaphoreCreateBinary();
xTaskCreatePinnedToCore(
focTask,
"FOC Task",
4096,
NULL,
5,
&foc_task_handle,
1
);
timer = timerBegin(1'000'000); // Timer 1MHz
timerAttachInterrupt(timer, &onTimer);
timerAlarm(timer, 1000, true, 0);
encoder.debug = false;
}
PhaseCurrent_s currents;
float current_magnitude;
unsigned long velTimer = 0;
void loop()
{
if (millis() - velTimer > 100)
{
// currents = current_sense->getPhaseCurrents();
// current_magnitude = current_sense->getDCCurrent(); // Read absolute DC current
// motor.monitor();
velTimer = millis();
// Serial.printf("V:%.3f A:%.3f\n", motor.shaftVelocity(), motor.shaftAngle());
Serial.printf("V:%.3f A:%.3f SA:%.3f T:%ld\n", motor.shaftVelocity(), encoder.getAngle(), motor.shaftAngle(), loopTime);
// Serial.printf("%.3f,%.3f,%.3f\n", motor.shaftVelocity(), current_magnitude, currents.a);
// user communication
command.run();
}
}
And for the incremental encoder:
sd_encoder.h
#ifndef INCLUDE_ESP32_FOC_SD_ENCODER_H_
#define INCLUDE_ESP32_FOC_SD_ENCODER_H_
#include <ESP32Encoder.h>
#include <SimpleFOC.h>
class SDEncoderFOC : public Sensor {
public:
SDEncoderFOC(int _pinA, int _pinB, int64_t _ppr, encType _type);
~SDEncoderFOC();
void init();
// Get current shaft angle from the sensor hardware, and
// return it as a float in radians, in the range 0 to 2PI.
// - This method is pure virtual and must be implemented in subclasses.
// Calling this method directly does not update the base-class internal
// fields. Use update() when calling from outside code.
float getSensorAngle() override;
/**
* @brief Current angular velocity (rad/s)
*
* @return Angular Velocity
*/
float getVelocity() override;
virtual void update() override;
int needsSearch() override;
int64_t pulses();
bool debug;
private:
ESP32Encoder sensor;
encType type;
int64_t ppr;
int64_t cpr;
int pinA;
int pinB;
// velocity calculation variables
int64_t lastCount;
float prev_Th, pulse_per_second;
float velocity;
long currentDt;
constexpr static long SAMPLING = 5000; // us
volatile long prev_pulse_counter, prev_timestamp_us, pulse_timestamp, sampling_timer;
inline void updateVelocity(long _dt);
};
#endif // INCLUDE_ESP32_FOC_SD_ENCODER_H_
And
sd_encoder.cpp
#include "sd_encoder.h"
SDEncoderFOC::SDEncoderFOC(int _pinA, int _pinB, int64_t _ppr, encType _type) {
pinA = _pinA;
pinB = _pinB;
ppr = _ppr;
type = _type;
lastCount = 0;
velocity = 0;
prev_Th = 0;
pulse_per_second = 0;
prev_pulse_counter = 0;
prev_timestamp_us = _micros();
pulse_timestamp = _micros();
currentDt = 0;
switch (type) {
case encType::single: {
cpr = ppr;
} break;
case encType::half: {
cpr = 2 * ppr;
} break;
case encType::full: {
cpr = 4 * ppr;
} break;
}
debug = true;
}
SDEncoderFOC::~SDEncoderFOC() {}
void SDEncoderFOC::init() {
ESP32Encoder::useInternalWeakPullResistors = puType::up;
// FullQuad: Counts on 4 edges
// HalfQuad: Rising and falling edge of single channel
// Single: Only rising edge of channel A
switch (type) {
case encType::single: {
sensor.attachSingleEdge(pinA, pinB);
} break;
case encType::half: {
sensor.attachHalfQuad(pinA, pinB);
} break;
case encType::full: {
sensor.attachFullQuad(pinA, pinB);
} break;
}
// sensor.attachSingleEdge(GPIO_NUM_19, GPIO_NUM_21);
sensor.setCount(0);
}
float SDEncoderFOC::getSensorAngle() {
return _2PI * (sensor.getCount() % cpr) / cpr;
}
float SDEncoderFOC::getVelocity() { return velocity; }
void SDEncoderFOC::update() {
float val = getSensorAngle();
if (debug)
Serial.printf("%f\n", val);
angle_prev_ts = _micros();
float d_angle = val - angle_prev;
// if overflow happened track it as full rotation
if (abs(d_angle) > (0.8f * _2PI))
full_rotations += (d_angle > 0) ? -1 : 1;
angle_prev = val;
if (pulses() - lastCount != 0) {
pulse_timestamp = _micros();
}
lastCount = pulses();
long dt = _micros() - sampling_timer;
if (dt >= SAMPLING) {
sampling_timer = _micros();
updateVelocity(dt);
}
}
int SDEncoderFOC::needsSearch() { return false; }
int64_t SDEncoderFOC::pulses() { return sensor.getCount(); }
inline void SDEncoderFOC::updateVelocity(long _dt) {
long copy_pulse_counter = sensor.getCount();
long copy_pulse_timestamp = pulse_timestamp;
// timestamp
long timestamp_us = _micros();
// sampling time calculation
// float Ts = (timestamp_us - prev_timestamp_us) * 1e-6f;
float Ts = _dt * 1e-6f;
// quick fix for strange cases (micros overflow)
if (Ts <= 0 || Ts > 0.5f)
Ts = 1e-3f;
// time from last impulse
long dN = copy_pulse_counter - prev_pulse_counter;
// if (dN == 0)
// {
// currentDt += _dt;
// }
// else if (currentDt != 0)
// {
// Ts = (currentDt + _dt) * 1e-6f;
// currentDt = 0;
// }
float Th = (timestamp_us - copy_pulse_timestamp) * 1e-6f;
// Pulse per second calculation (Eq.3.)
// dN - impulses received
// Ts - sampling time - time in between function calls
// Th - time from last impulse
// Th_1 - time form last impulse of the previous call
// only increment if some impulses received
float dt = Ts; // + prev_Th - Th;
pulse_per_second = (abs(dN) > 1 && dt > Ts / 2) ? dN / dt : pulse_per_second;
// if more than 0.05f passed in between impulses
if (Th > 0.1f)
pulse_per_second = 0;
// velocity calculation
velocity = pulse_per_second / ((float)cpr) * (_2PI);
// save variables for next pass
prev_timestamp_us = timestamp_us;
// save velocity calculation variables
prev_Th = Th;
prev_pulse_counter = copy_pulse_counter;
}
Thanks a lot for sharing this
I think it’s going to be helpful to other users.
Is your impression that the board is working well?
Yes, It gets the job done pretty well, at least for voltage control mode.
I didn’t test it all the way to 20A, so I’m not sure if it can handle this current on continuous operation (I kind of doubt it, given the size of the mosfets, and I’m not sure if the PCB can dissipate all this power). From the tests I’ve made, something around 5A (at 24V) should be fine, but it starts to smell “hot”, so I didn’t push it too far.
In my application I’ve limited the current to around 1.6A which is more than enough for my robot to carry myself on top of it ![]()

