About 8 years ago I made up a discrete, analogue based PWM controller for a DC motor I fitted to the Z axis of my Mill. Based on a LM3524 Power Supply PWM controller, its output was single ended which meant the motor wires sat between power supply +ve and the collector of the PWm switching transistor. This meant that for the motor to reverse, the wires had to be swapped. So it ended up an electromechanical solution – cams, microswitches, relays, etc…It worked well, although I did replace one microswitch in those 8 years.
The drive uses another small DC motor as a TachoGen for clean smooth low speed control with lots of torque.
This was the circuit:

And the Old PCB’s with relays..

Meantime, I had been playing with ChatGPT on other subjects, and that same microswitch now caused me to smack an endmill into the workpiece when the Head should have gone up instead…
So I thought to replace the whole thing with some thing ‘better’..
I found a neat PWM full bridge ( no motor wire reverse relays needed..) driver – PWM and direction control inputs.

Locally it cost R400.00 – about 17 pounds.
I made up a little board carrying the MD20, and Ardunio Nano, diode bride for the TachoGen so its wires don’t have to be reversed, 5volts regulator and a few pots…
Here’s the circuit: ( the MD20 is not shown – it gets Motor Power and drives the motor. Also PWM and DIR from the Nano.

And the PCB Carrier:

The two motors shaft coupled above were a test motor and tachogen to get it working on the bench.
Now we needed software…So I decided to see what ChatGPT could deliver…
What I wanted was to use a Pot , when centered the motor would stop, have a dead band either side of center, and one way would go up, the other down, and speed would vary per pot position. So I asked ChatGPT for that :
Objective statement to ChatGPT:
generate arduino nano code for a PWM motor controller with the following requirements. Speed command is from a variable DC voltage, 0-3v . A single Pin for PWM output. PWM is 0 between 1.4v to 1.6v . When the voltage is between 0 to 1.4v a single direction pin is set logic LOW and the PWM varies from MAX at 0v to min at 1.4v. When the voltage is between 1.6v to 3v, the same direction pin is set HIGH and the PWM varies from MIN at 1.6v to MAX at 3v. PWM frequency can be between 1000Hz and 5000Hz. Motor speed regulation is by reading the voltage from a Tach-generator on the driven motor shaft. The Tachogenerator out voltage is 0v as zero speed and 3v at max speed which is where the PWM is also max
ChapGPT generated some code which I tested.
The code contained a PID loop for the motor PWM control-
I asked for these mods:
add serial monitor output for the PID loop
This gave me output data to the Arduino serial terminal so I could see the PID values in real time, and change them to get the motor smooth.
Turns out I and D were not needed at all and a P gain of 2 worked perfectly.
Then I wanted the dead-band expanded and needed a different voltage range for the TachGen output, so I asked for that:
Change the voltage ranges as follows: Speed command voltage varies from 0v to 5v. Dead band is from 2.3v to 2.7v. PWM output is 0 at 2.7v and max at 5v, and 0 at 2.3v and max at 0v. Direction pin is low between 0v to 2.4v and HIGH between 2.6v to 5v. Tachogen output is 0v a zero RPM and 5v at max RPM
The code as generated was used AS IS – I did not touch or modify anything other than to set Pgain =2 and hard code it as such.
That is quite amazing – anyone could do this, with no coding experience at all – the key is to define the requirement properly and specify the changes clearly..It works amazingly well!
This shows the ‘control’ lever – fitted to the centered pot, which it spring loaded with a zero detent – ball and spring..

The Z drive motor with the Tacho Gen slung underneath:

And Finally, the code :
/*
* Bidirectional PWM Motor Controller with Analog Speed Command and Tach Feedback
* Serial Monitor Output + Live PID Tuning + Debug Toggle
*
* Command voltage: 0–5V
* Dead band: 2.3–2.7V
* Direction pin:
* LOW for 0–2.4V
* HIGH for 2.6–5V
* Tachogenerator: 0–5V = 0–max speed
*/
const int pinCommand = A0;
const int pinTacho = A1;
const int pinPWM = 9;
const int pinDir = 8;
// — Voltage calibration —
const float Vmax = 5.0;
const float deadLow = 2.3;
const float deadHigh = 2.7;
const float dirLow = 2.4;
const float dirHigh = 2.6;
// — ADC reference constants —
const float ADCref = 5.0; // Arduino Nano analog reference
const int ADCmax = 1023;
// — PID control constants —
float Kp = 2.0; //was 80
float Ki = 0; //was 2
float Kd = 0; //was 10
float integral = 0;
float prevError = 0;
unsigned long lastTime = 0;
// // — Debug toggle flag —
// bool debugEnabled = true; // default: ON after reset
// — Debug toggle flag —
bool debugEnabled = false; // default: OFF after reset
void setup() {
pinMode(pinPWM, OUTPUT);
pinMode(pinDir, OUTPUT);
Serial.begin(115200);
Serial.println(“PWM Motor Controller Starting…”);
Serial.println(“Commands:”);
Serial.println(” Kp <val>, Ki <val>, Kd <val>”);
Serial.println(” DEBUG ON / DEBUG OFF”);
Serial.println(” SHOW”);
// Set timer1 for ~2kHz PWM on D9
TCCR1A = _BV(COM1A1) | _BV(WGM10);
TCCR1B = _BV(WGM12) | _BV(CS11); // Prescaler 8 → ~2 kHz
analogWrite(pinPWM, 0);
digitalWrite(pinDir, LOW);
}
void loop() {
handleSerialInput();
// — Read analog inputs —
float cmdRaw = analogRead(pinCommand);
float tachRaw = analogRead(pinTacho);
float cmdV = cmdRaw * (ADCref / ADCmax);
float tachV = tachRaw * (ADCref / ADCmax);
// — Direction control based on command voltage —
if (cmdV < dirLow) {
digitalWrite(pinDir, LOW);
} else if (cmdV > dirHigh) {
digitalWrite(pinDir, HIGH);
}
// — Determine target speed voltage for PID —
float target = 0;
int direction = 0;
if (cmdV < deadLow) {
direction = -1; // reverse
target = mapf(cmdV, 0.0, deadLow, 5.0, 0.0); // 0 V→max, 2.3 V→0
}
else if (cmdV > deadHigh) {
direction = 1; // forward
target = mapf(cmdV, deadHigh, Vmax, 0.0, 5.0); // 2.7 V→0, 5 V→max
}
else {
direction = 0; // stop / dead zone
target = 0;
}
if (direction == 0) {
analogWrite(pinPWM, 0);
if (debugEnabled) printStatus(cmdV, tachV, direction, target, 0, 0);
delay(50);
return;
}
// — PID Speed Regulation —
unsigned long now = millis();
float dt = (now – lastTime) / 1000.0;
if (dt <= 0) dt = 0.001;
lastTime = now;
float error = target – tachV;
integral += error * dt;
float derivative = (error – prevError) / dt;
prevError = error;
float output = Kp * error + Ki * integral + Kd * derivative;
// — Constrain and apply PWM —
int pwmVal = constrain(int(output * 51), 0, 255); // 5 V×51 ≈ 255
analogWrite(pinPWM, pwmVal);
if (debugEnabled) printStatus(cmdV, tachV, direction, target, error, pwmVal);
delay(50);
}
// — Utility map function for floats —
float mapf(float x, float in_min, float in_max, float out_min, float out_max) {
return (x – in_min) * (out_max – out_min) / (in_max – in_min) + out_min;
}
// — Serial print helper —
void printStatus(float cmdV, float tachV, int direction, float target, float error, int pwmVal) {
Serial.print(“CmdV: “); Serial.print(cmdV, 2);
Serial.print(“V | Tach: “); Serial.print(tachV, 2);
Serial.print(“V | DirPin: “);
Serial.print(digitalRead(pinDir) == HIGH ? “HIGH” : “LOW”);
Serial.print(” | Motion: “);
if (direction == 1) Serial.print(“FWD”);
else if (direction == -1) Serial.print(“REV”);
else Serial.print(“STOP”);
Serial.print(” | Target: “); Serial.print(target, 2);
Serial.print(“V | Err: “); Serial.print(error, 2);
Serial.print(” | PWM: “); Serial.print(pwmVal);
Serial.print(” | PID(Kp/Ki/Kd): “);
Serial.print(Kp, 1); Serial.print(“/”);
Serial.print(Ki, 1); Serial.print(“/”);
Serial.println(Kd, 1);
}
// — Serial input parser for PID + Debug control —
void handleSerialInput() {
static String input = “”;
while (Serial.available()) {
char c = Serial.read();
if (c == ‘\n’ || c == ‘\r’) {
input.trim();
if (input.length() > 0) processCommand(input);
input = “”;
} else {
input += c;
}
}
}
void processCommand(String cmd) {
cmd.trim();
cmd.toUpperCase();
if (cmd.startsWith(“KP “)) {
Kp = cmd.substring(3).toFloat();
Serial.print(“Kp updated to “); Serial.println(Kp, 3);
}
else if (cmd.startsWith(“KI “)) {
Ki = cmd.substring(3).toFloat();
Serial.print(“Ki updated to “); Serial.println(Ki, 3);
}
else if (cmd.startsWith(“KD “)) {
Kd = cmd.substring(3).toFloat();
Serial.print(“Kd updated to “); Serial.println(Kd, 3);
}
else if (cmd == “SHOW”) {
Serial.print(“Current PID: Kp=”); Serial.print(Kp, 3);
Serial.print(” Ki=”); Serial.print(Ki, 3);
Serial.print(” Kd=”); Serial.println(Kd, 3);
}
else if (cmd == “DEBUG ON”) {
debugEnabled = true;
Serial.println(“Debug output ENABLED.”);
}
else if (cmd == “DEBUG OFF”) {
debugEnabled = false;
Serial.println(“Debug output DISABLED.”);
}
else {
Serial.println(“Unknown command. Use: Kp <val>, Ki <val>, Kd <val>, SHOW, DEBUG ON/OFF”);
}
}