CNC Router‎ > ‎

Spindle controller

For the CNC milling machine, I'm using a Kress FM 530 spindle. This is the most basic model from Kress and consists of just a 530W universal motor and an on-off switch. The more expensive models use a knob and a triac to allow varying the speed. Mine just spins up to 28000 RPM upon turning it on.

It would be very easy to add such a triac based speed control circuit, but I would like to be able to control the spindle from Mach3 or EMC2. Additionally, it would be nice if a feedback mechanism ensures that the spindle speed is maintained during milling.
I found that all this is offered by Super-PID, but as it did not seem that hard, I decided to build a speed controller from scratch.

So... what is needed?
  • AC zero-cross detection: To control a triac, you need to know when an AC zero crossing happens, in order to trigger it at the right time. This is called triac phase control. The bigger the phase angle, the more the universal motor will receive of the AC half wave.
  • RPM sensor: Next, we require the actual speed of the spindle to be able to adjust this phase angle. Thus we need some kind of RPM sensor.
  • Triac driver circuit: We want to use a microcontroller to drive the triac, preferably completely isolated from the triac for safety reasons.
  • Interface: Ways to read current and desired speed and a knob to manually adjust the speed. Oh and some emergency stop switch.

Zero-cross detection

Detecting the zero-passing of the AC sine can be done in various ways. The easiest is to directly connect the mains to a 1M resistor to the input pin of a uC and have the chip's internal EMC protection diodes limit the voltage.Very simple and explained in this AVR application note.

Of course, if cost is not paramount and you want some safety, you'll want to isolate the mains completely from the microcontroller part. Then, some more noise immunity would not hurt either. So looking for an alternative, I found this and this. Both circuits are very similar and use an optocoupler to isolate the detection pulses. You can read the full circuit descripton by following the links.

The circuit generates a short pulse around the zero crossing, which can be easily used as GPIO interrupt input for the STM8S I'm using here.

RPM sensor

Measuring the speed of the spindle is done using an IR sensor. The hard part is finding a suitable location inside the router where the IR receiver can look at the spinning axle. In the Kress, there is a small black plastic "wheel" at the top end of the axle, just before the bearing. It has small indentations, so it is possible that it is was intended for measuring the speed, if it were painted.

The sensor/diode combination used is a Vishay TCRT5000. I tried a TCRT1000 as wel, but its range was insufficient to obtain a reliable pulse signal. In the picture below, you can see the sensor mounted on a piece of protoboard and the white dot (corrector fluid) on the plastic wheel, just above the motor's armature.

Installing IR speed sensor in the spindle

To keep things simple, I'm using a single pulse per revolution. For a range between 2000 to 30.000 rpm, this translates to pulses with a frequency of 33 Hz to 500 Hz.

Triac driver

The application of triacs as a dimmer or motor controller is widespread; they can be found in appliances such as vacuum cleaners and light dimmers. If no specific control is necessary, a potmeter can be used to control the triac's phase angle, but we want to have a feedback loop through the microcontroller.

For the triac, I chose a BTA08-600, which can handler 8A at 600Vrms. Should be plenty.

It is possible to drive the triacs gate directly from a microcontroller if the triac is logic-level compatible. This means that the gate is more sensitive and can trigger faster (with a smaller current). But if this is not the case, there exist triac drivers that include both the driver circuit and an optocoupler to isolate the circuit parts, such as the MOC3022. Below is an application example from the datasheets, which fits perfectly for what we have in mind.

Microcontroller and peripherals

The STM8S is an 8 bit microcontroller from ST. Its major point is that it includes lots of onboard peripherals such as timers of various complexity. The timers can be configured extensively so that almost no support code is necessary during their operation.

In this case, we want to count the amount of time between pulses from the IR sensor to derive the router speed (an alternative is counting pulses in a certain interval but this is less accurate at low speeds). It involves using TIMER1 in Slave-reset mode, so that it resets on an interrupt on its input channel. The current counter value is then stored in its capture-compare register. I have already covered the code here.

RPM Timer

To cover the possible range of speeds with adequate accuracy, the timer is run at 2Mhz. At 30.000rpm this relates to 4000 pulses and at 2000 rpm 60.000 pulses:

TIM1_TimeBaseInit(7, TIM1_COUNTERMODE_UP, 65535, 0);

The measured intervals are stored in an array with moving index, and then averaged when the RPM value is calculated. To limit computation time with the division involved, everything is kept as an integer.

const u32 MultFactor = 120000000;

for (i=0;i<NBSAMPLES;i++){

    avg = avg + measuredValues[i];
avg = avg>>3; //Divide by 8
rpm = MultFactor/avg; //Convert to rpm as integer (accurate enough)

Triac phase angle timer

AC zero crossings are detected with a GPIO rising interrupt. At that point,TIMER2 is configured to count until the current phase angle setting, and started. As we only have to deal with intervals up to 25 Hz or 10ms, we choose the prescaler at 16. The phase angle value can therefore move in steps of 1us.

void EXTI_PORTA_IRQHandler(void) interrupt 3{
    TIM2->ARRH = (u8)(triacDelay >> 8);//Set the Repetition Counter value to the ignition wait-time of the triac

    TIM2->ARRL = (u8)(triacDelay);
    TIM2_SetCounter(0);//Ensure timer is zero
    TIM2_Cmd(ENABLE);//Start timer

At the TIMER2 update interrupt, the timer is stopped and an ignition pulse is created. Some nop()'s were inserted to ensure the pulse is long enough.
if (outputEnabled){
    GPIO_WriteHigh(GPIOG,GPIO_PIN_1);//Trigger triac
TIM2_Cmd(DISABLE);//Stop timer
TIM2_ClearITPendingBit(TIM2_IT_UPDATE);//Clear interrupt
nop(); nop(); nop();
GPIO_WriteLow(GPIOG,GPIO_PIN_1);//Stop trigger

Display and input

To display the current RPM, a 7 segment LED display was chosen. A normal character LCD would also work, but I find 7 segment displays to be more readable from a distance and under varying light conditions. I made a 5-segment display using 75HCN595 shift registers. The problem with this is that the shift registers have only a limited output current, which has to be distributed among the lit segments. The solution to continuously switch between individual segments works fine, but takes a lot of effort on the part of the microcontroller, while the segments are 1/8 of the maximum brightness.

So, this is where the TM1638 comes in. These ICs are available via Chinese webshops and typically combine 8 leds, 8 buttons and 8 segment blocks on a single PCB, for prices around €7-8. Libraries exists for the Arduino, so I adapted it to work on the STM8S.

Auxiliary timer

Periodically, we need to read the setpoint value from the speed control knob, execute the PID code, update the display and read the settings buttons. We use TIMER4 to set flag-variables that signal the main loop to execute certain tasks:

void TIM4_UPD_OVF_IRQHandler(void) interrupt 23{
    //Timer fires every 2ms
    if (tim4count % 40 == 0)//Calculate PID on 0,40,80,120,160,200
        pidFlag = 1;
    if (tim4count % 165 == 0)//Shortly after calculating PID (5*2ms to be exactly), trigger display update. Display is updated every 240ms
        displayFlag = 1;
    if (tim4count == 220)
        setPointFlag = 1;//Read ADC for target RPM
    if (tim4count == 100)
        buttonFlag = 1;
    if (tim4count>249)//Max 250ms
        tim4count = 0;

A/D converter & setpoint input

When setPointFlag is set in the code above, the A/D converter reads the voltage from a potentiometer knob:

while (ADC1_GetFlagStatus(ADC1_FLAG_EOC) == RESET);//Wait for conversion finish

measuredInputs[inIdx] = ADC1_GetConversionValue();

//Calculate average of input samples
avg = 0;
for (i=0;i<NBSAMPLESINPUT;i++){
    avg = avg + measuredInputs[i];
avg = avg>>2;//Divide by 4

//Rescale to maxSpeed
setPoint = avg << (maxSpeed-10);

This value (10 bit) is averaged and rescaled to a range between 0 and 32768 (max rpm).

During testing, we saw that the a sudden change in setpoint caused the spindle to accelerate so hard that it jumped of the table, leading to major mayhem to the cables connected :) So it was changed to "buffer" the setpoint and ramp the real speed towards the setpoint. But only for rising values of the setpoint.

PID loop

The important part of course is the PI feedback loop.

#define KP    0.21f
#define KI    0.18f
#define MAX_I_ERR 100000
#define MIN_I_ERR -100000
float Ierr = 0;

void runPid(void){

    /* Update error value */
    error = targetSpeed - rpm;
    if (outputEnabled){
        float Pterm = 0;
        float Iterm = 0;
        float sum = 0;
        float err = ((float)error);
        /* Update P value */
        Pterm = KP * err;
        /* Update I value */
        Ierr = Ierr + err;
        if (Ierr > MAX_I_ERR){//Integrator windup protection
            Ierr = MAX_I_ERR;
        } else if (Ierr < MIN_I_ERR){
            Ierr = MIN_I_ERR;
        Iterm = Ierr * KI;
        /* Summate */
        sum = Pterm + Iterm;
        /* Translate output to something within the valid range of the phase angle */
        if (sum>MAX_PHASE_ANGLE){
            sum = MAX_PHASE_ANGLE;
        } else if (sum<0){
            sum = 0;
        triacDelay = MAX_PHASE_ANGLE -((u16) sum);
        if (triacDelay<PHASE_ANGLE_OFFSET_BOT){
            triacDelay = PHASE_ANGLE_OFFSET_BOT;
     } else {
        Ierr = 0;//Ensure I controller is reset

Because of the very low inertia of the armature, speed can vary very fast, so only a small proportional action can be used. The values of Kp and Ki were determined experimentally by applying a step to the speed setpoint and monitoring the measured RPM. This is the theoretical ideal way to do it... In other words, you'll have to fiddle with the values manually because it is not possible to measure the RPM without delay or apply a good setpoint step (motor reacts too violently).

Final schematic and code

Under construction

Test video