Home > Electronics > Modis – Closed loop motor control with display (Benham disc)

Modis – Closed loop motor control with display (Benham disc)

Information about closed loop motor control is somewhat hard to come by on the Internet. Of course there is a vast body of literature about closed loop controls, but most of these books or articles focus on the mathematical methods of modeling and calculating motor controls. In practice, it is often not easy to apply these methods to a given real-world situation. Consequently these books require an amount of study that can easily overwhelm a hobby constructor who "just needs a simple motor control". On the other hand, simple instructions often are too specific or leave out a lot of useful information, making it difficult to recognize their limitations or adapt them to a given mechanical setup.

Front view of the Benham disc rotator

Front view of the Benham disc rotator

I am attempting here to provide a fairly detailed description of a closed loop engine controller. This includes a description of the mechanical parts, the circuit boards and finally the software, written in C for an Atmel Atmega 8, as a WinAVR (Programmer’s Notepad) project. You can use this description for projects of your own, and you can use my schematics, layouts and source code freely (where indicated; for exceptions see the descriptions and/or source). However, while I am trying to explain the details as well as I can, you should expect that you need to invest some work of your own to fit your needs and possibilities. Please check everything carefully as neither this description nor the provided files are guaranteed error free!

Download the project files

Overview

I built the "Machine" for an exhibition my wife was involved in. The purpose of the Machine is to demonstrate the appearance of “physiological colors” (i. e. colors that appear under some conditions due to the physiological structure of our eyes) on a rotating disc that contains a printed pattern of black and white. Such a disc is commonly known as “Benham disc”. The requirements are as follows:
–    Control the speed of a rotating disc of about 50 cm diameter according to a user-defined target value.
–    Speed range should be between 0 and about 400 rotations per minute.
–    Rotation must be clockwise or counterclockwise as selected by the user.
–    The target value and the actual rotation speed should be displayed.
–    There should be an overload protection in case someone stops the rotating disc.
–    The setup must be very robust because it might be handled roughly by exhibition visitors. At the same time it should be as simple as possible.

Rear view of the colour disc rotator

Rear view. Most wiring has been shielded to avoid EMI.

Here’s a video of the thing in action:

Mechanical setup of the "Machine"

To keep the user interface simple there is only one control to preset the disc speed, namely a rotary encoder. While of course any rotary encoder (such as Reichelt’s) could be used, a cheap stepper motor makes a very robust and reliable rotary encoder. It also has a good size that makes it easy to fit a handle to it. All that is needed is a small interface board to shape the input for the microcontroller.

Control_Handle (Medium) Stepper_Encoder (Medium)

The stepper motor has a small sprocket at the end of its output shaft which I fit into an aluminum axle of about 25 mm diameter, holding it in place with a small grub screw. The axle is seated in a plastic tube to avoid contact with the surrounding wood. Use plastic with a low friction coefficient here; aluminum isn’t the ideal axle material here either because it sticks easily. Some day I have to try a better solution.

The end of the axle is tapped and a there’s a wheel attached to it. The wheel comes from my milling machine (an Optimum BF 20); its center bore cracked when I had to remove it and I got a replacement – however, it’s still good enough to use on the Machine. The horizontal handle in the picture was removed for the second exhibition because some people thought they should propel the disc using the wheel, resulting in a rather harsh treatment.

The motor that drives the disc is a small DC motor that already contains a slotted disc for measuring motor speed. To improve the signal I used a comparator on a small extra board.

Motor_detailed (Medium)

This type of small DC motor might be found in video tape recorders. It can sometimes be cheaply bought from scrap dealers such as second-re-use.com. However, some kind of gear is necessary to match the motor speed to the desired speed of the output shaft. In most cases, the motor’s speed will be too high while its torque is too small. The large spinning disc has quite a large moment of inertia, so a gear is required here to provide a higher force at lesser speed.

There are a number of options for gears. Sprockets, for example, are widely used, but do have several disadvantages. Clearings must be relatively exact, requiring high precision in machining the gears and bearings. They make some noise and wear down, possibly requiring some kind of lubrication. Additionally, they can be hard to find in the required sizes or might be expensive, especially if larger. It is possible to make them on your own, but that’s a time consuming task requiring special tools and a lot of skill.

Cogged belt drives, such as I have chosen, do not have so many disadvantages. They also have the ability to balance slight imprecisions in axial trueness (wobbling) which makes building and adjusting the gears a lot easier.

First I determined the speed of my motor at given voltages. As my motor already contained a slotted disc (with the number 120 stamped on it) and an optical sensor it was easy to measure the idle speed using a counter. When the assembly is fitted and the disk runs at full speed the motor has to overcome only the amount of friction that is lost in the belts and bearings. Some part of the maximum speed can be deducted to account for these losses, say about 10 to 20%. The gear ratio that results from dividing the engine speed by the desired output speed determines the sizes and distances of the belt pulleys.
In my case I needed a ratio of about 1:9. As I couldn’t find two belt wheels with this ratio I chose two belt drives with identical pulleys of 1:3 each. To calculate sizes, distances and the resulting length of the belts you can use online calculators which are provided by manufacturers.

I got my cogged belts and pulleys from the Mädler web shop. They have a large variety of mechanical components plus a useful belt length calculator.

Gears (Medium)

To build a mechanical assembly like the one I describe here you’ll need a lathe and a mill. Without them it seems to be very difficult to me. While, in theory, you can have the parts manufactured elsewhere, it is of great practical value to have the machines ready in case you need to adjust something if it doesn’t fit as planned. I also assume that you have some basic mechanical experience so I’m not going to be very specific how to actually machine the parts that I’m describing.

Lathe_Friedrich_See (Medium)

My trusty old lathe, a 1967 Friedrich See model, still in good condition

The pulley carrier consists of two strong aluminum panels that are held together by rods. The rods are threaded on both sides. The front panel contains a tapped hole to accept one side of the rod. The other side’s thread is smaller in diameter than the rod itself, giving a little ledge; the second panel is put in place by pushing the threaded end through holes that rest against the ledge and the nuts are then screwed on, fastening the panels together. For the rods you can use aluminum but steel is better because it’s more rigid. If you look closely at the above picture you will see that I mixed aluminum and steel.

Here’s a CAD draft of the panel assembly (connecting rods not shown):

mechanics_schema

This drawing shows only the motor and one intermediate shaft. Imagine the output shaft to the further right. Such a CAD drawing is very useful in determining the approximate dimensions of the assembly and whether everything fits together. You can experiment with different designs and later on take the measurements for the drill holes etc. from the drawing.

Originally I intended to fasten the motor to the panel directly as shown in the drawing. Later on I decided to put it on an extra aluminum sheet that can be moved with respect to the panels to make the belt play easily adjustable. As a general rule, you should give much thought to how to make the components adjustable. I have found it easiest to mill parallel slots into the panels in which the bolts that hold bearings and motor in place can be moved a centimeter or so upwards or downwards.

The bearing holders in the drawing (there are two for each shaft) are made from aluminum panel the same size as the side panels. A round pocket is milled into one side; this pocket will contain a ball bearing that accepts the shaft. The pocket should be milled as precisely as possible, both in diameter as in depth. Ideally the ball bearing’s outer shell is pressed tightly to the panel when the bearing holder is fastened in place. This will prevent the outer bearing shell from rotating, which you want to avoid. The hole in the middle must be large enough to allow free rotation of the inner bearing shell.

Here’s a test specimen which is yet missing the bore holes for the panel screws. The bearing I’ve put in is just an example as it’s too thick. The final bearing holders I used are smaller (because the bearings were smaller) and have their pocket in the middle (please see the full assembly view below).

Bearing_holder (Medium) Bearing_holder_with_bearing (Medium)

To secure the rotating shafts in the bearings I suggest that you take material with a slightly larger diameter than the inner bearing diameter. The parts of the shaft that go through the bearings are then turned down to fit into the bearings. This needs to be done fairly exact so that when the assembly is finally put together the shafts do not jam the bearings because they are squeezed too tightly. On the other hand you don’t want the shaft to move to and fro because that creates noises. If you make the diameter too small the shaft will rotate in the inner bearing so better see that it’s a tight fit. It pays to be exact here but it’s not as hard as it might seem now. You can also apply a drop of thread locking compound to the inner bearing shell if the shaft has too much clearance.

I made the shafts out of an easy to machine steel, such as 9SMn28 because I felt that aluminum might not be stiff and robust enough. The pulley wheels can easily be fitted to the shafts using grub screws. Do this after assembling the whole thing so that you can adjust the belt positions.

To manufacture bearing holders like this you can use a milling machine with a round table. Here’s my Optimum BF 20 with a round table mounted in a vertical position:

Mill_Optimum_FB_20_with round table (Medium)(BTW, do you notice the improvised handle on the Z axis wheel? The original handle was used on the Machine.)

The belt assembly can be put together and tested on the workbench. Later on I attached it to wooden support struts in the box. As the whole thing is likely to vibrate it is advisable to fix the wooden frame to the box with a flexible material, e. g. rubber dampers:

Strut_Support(Medium)

Below the angular metal bracket as well as around the screw there’s a (hard to see) thick patch of rubber that has been made out of a broken cooling hose of a car. Though not perfect it reduces the hum from the vibrations.

Here’s a picture of the complete mechanical part plus support struts:

Panel_assembly_fixed (Medium)

It is advisable to use self-locking nuts on all threaded connections.

 

Electronics

The schematics and board layouts are available in Cadsoft Eagle format. The free version of Eagle can be used to view and print them out.

Download the project files

First of all, here’s a block diagram of the different components of the engine controller:

modis_components

The power in my case is supplied from a large transformer-type wall wart that delivers 18V AC at a current of 1A. I guess the average power consumption of the Machine during operation is not more than 15W.

Encoder board

The encoder board is required when a stepper motor is used to set the target value. If you want to use a standard rotary encoder it is not required. The board is connected to both of the stepper motor’s coils. It senses the current that is induced by a rotation and outputs pulses with steep slopes that can be counted by a microcontroller.

The encoder board circuit has been adapted from Julien Thomas’ page http://www.jtxp.org/tech/schrittmotordrehgeber.htm. According to the author it is free to use for personal, educational and artistic use so I guess I’m fine, but of course I can’t repost it here, so please have a look at it yourself (sorry, German only).

Display board

The display consists of four LED seven-segment displays of type LTS3403 (of which I happened to have several lying around). The display is multiplexed using a serial shift register 74HC164 which drives the LED anodes using a source driver array UDN2981. The cathodes are driven using NPN transistors. The software takes care of the multiplexing. At each display’s turn the shift register is cleared and filled serially with the appropriate pattern of that display’s position. During this time all transistors are off. The proper transistor is then switched on causing the LEDs to light up and show the pattern. After some time the software repeats the process for the next display position.

I’ve chosen the current for the segment LEDs to be 50 mA, which is quite a lot. The reason is that we need the LEDs to shine rather bright so that they can be seen well even in daylight. As there are four segments, though, it means that the maximum duty cycle for each segment is 25% so the current averages to 12,5 mA which is half the continuous maximum current according to the data sheet, so with four segments we could even go up to 100 mA but would then need larger transistors.

7segment_sch

There is a solder jumper that determines which of the two dots is used. The board layout looks like this:

7segment_brd

The LTS3403 is defined in an extra Eagle library that you can add to the eagle library folder. The board is quite simple and straightforward to solder (with some SMD soldering experience), except the segment displays: Make sure that you have them all nicely aligned. The other components partly go under the segment displays, so be sure to solder them in first ;-)

Sensor amplifier (comparator)

The sensor amplifier is just a comparator with Schmitt trigger characteristic that shapes the speed signal for the microcontroller. You could use any comparator with an open collector output. I have used an LM393. The amplifier board should be as close to the speed sensor as possible.

Here’s the circuit:

speed_comparator_sch

The motor connectors are just wired through for convenience. How the sensor is connected depends on the sensor type. In my case it was a photocell sensor consisting of an IR diode and a photo transistor, wired like this:

speed_comparator_sensorAs you can see the LED requires a current limiting resistor. This is R1 in the comparator schematic, again provided for convenience. A typical value for 5 volts VCC would be 470 Ohms. T1 is open collector and will pull down the input line when a slot in the disc passes by the transistor surface. Therefore, it requires a pullup resistor R7 in the comparator circuit, whereas R6 can be omitted. A typical value for R7 would be 10 kOhms. R4 is the pullup resistor for the comparator, also typically 10 kOhms. R2 and R5 determine the threshold voltage. With a sensor that pulls down the input they can be chosen to be equal, thus setting the reference voltage to 1/2 VCC, e. g. 20 kOhms each. The hysteresis resistor R3 should be set to twice or three times the value of R2, but you can start with 100 kOhms and adjust it if it doesn’t work well.

Here’s a little more information on how to calculate the resistor values.

As you see this comparator is quite versatile. I have designed it to be used in other situations as well, not just for this project. Here’s the board layout:

speed_comparator_brd

At the time I made the project I was still experimenting with different configurations so I built it using striped circuit board:

Sensor_Comparator (Medium)

Note that the panel holding the motor in place is slightly tilted. With this tilt I can adjust the belt tension.

Main board

The main board is where most of the action takes place. It contains:

  • A rectifier with a fuse and smoothing capacitor for the motor voltage
  • A voltage regulator to provide the boards with 5 volts
  • The microcontroller that runs the closed loop control program
  • A motor driver L298
  • connectors and support components

Let’s have a look at the circuit:

modis_mainboard_sch

    This circuit is able to drive two DC motors independently, or one stepper motor. It can perform current sensing on both motors (or phases). Additionally there’s a speed sensor input on the INT1 pin of the µC.
    Cautionary note: The speed sensor of the other motor is connected to ADC4 in this circuit. I haven’t tried whether this actually works – I’ve only driven one motor with this circuit. It might if ADC4 is configured as an analog comparator input but this requires different code than for INT1 or INT0. I’m going to put SPEED1 on INT0 some day but unfortunately this requires changes to the libraries as one additional pin of PORTD is needed, so for the time being speed sensing can only be done on motor 2. End of cautionary note.

The board has two six-pin connectors for the motors, an encoder input, two solder dots to connect an optional switch, and two ten-pin connectors for the in-system-programmer and the display board respectively. (I really like these tub-type connectors because they are reliable and easy to experiment with.)

The L298 is a standard chip for motor appliances. Usually it is used with its brother L297 for simplified control of stepper motors. In this circuit I do the L297’s work in the software with the additional advantage that I can drive plain old DC motors as well. In any case you must provide protector diodes (D1 to D8). Usually it is said that you should use high speed switching diodes (Schottky types or whatever). These diodes are more expensive than the standard garden variety 1N4007 or similar which I have found to work equally well as long as the PWM frequency is not too high. A few kHz should be ok.

The L298 has two power inputs: VIN, which is unregulated rectified power, and VCC which is 5 volts. The higher VIN is, the faster the motors will go, provided that they can withstand the voltage. The voltage the motor sees also depends on the PWM signal. You could use 40 volts on a motor rated at 10 V provided you never exceed a PWM duty cycle of 25%. You will have to enforce this rule in the software. The L298 can handle up to 46 volts. In any case it should be connected to a heat sink for all but very small motors.

The L298 implements an H bridge whose operation is explained nicely in this Wikipedia article. Basically it allows us to run the motor in four modes: free running, left, right, and braking. We are going to use all of these modes in the software. The mode is selected by setting the logic state on the inputs INPUT1 to INPUT4 (two for each motor respectively).

R1 and R2 are current sensing resistors (aka shunts). The voltage drop across these resistors is measured by ADC inputs 0 and 1 (smoothed by low pass filters R3/C6 and R4/C7 respectively). The higher the ADC value is the more current is being drawn by a motor. We can experimentally determine a threshold that we use in our software to provide an emergency cutoff in case a motor is blocked or overloaded.

The shunt resistors are typically small, about 0.47 ohms, though this depends on the supply voltage VIN and the motor wattage.

Here is my layout for the main board:

modis_mainboard_brd

The red wires are wire bridges that are necessary to keep the board one-sided.

Here is a view of the bottom (what you see is a previous, still experimental version):

Mainboard_bottom (Medium)

Here’s a close up of the component side:

Mainboard_top (Medium) 

I apologize for both the poor picture and soldering quality ;-) The first version of the board has required some corrections and experiments (the capacitor and the blue wires underneath).

Also notice the shielding using self-adhesive aluminum foil and ferrite cores which I found necessary to reduce some issues I had with EMI in the vicinity of fluorescent lamps.

The board is attached to the rear support panel via two components: The L298 and the voltage regulator. As the board is quite light this setup is mechanically stable enough. Here’s a close up of the L298:

L298_closeup (Medium)

Note the green shunt resistor (0.22 ohms in this case). The (already somewhat worn) screw goes into a tapped hole. It holds the circuit board on one side.

The voltage regulator is attached to the panel using a stripe of aluminum plate. It holds the circuit board on another side:

voltage_regulator (Medium)

It is absolutely necessary to provide a heat sink for the voltage regulator. The LED display alone requires 9*50 mA in the worst case (if all digits and the dots are lighting continuously) which is 450 mA. Suppose we need 500 mA altogether with a supply voltage of 18 VAC. Then we have Veff at about 25 V minus 5 V = 20 V times 0,5 A to burn, this is about 10 W. 10 W is not too much for a 7805 provided there’s a good heat sink (fortunately we have a large aluminum panel here which is a good heat conductor).

In practice I don’t expect that the current is always that high. After all, not all digits are always on. It may be about 100 mA, which brings power dissipation down to 2 W. In any case, even 2 W are too much for a 7805 without a heat sink. So better prepare for the worst case.

If you think the board should be properly attached to a support, you are probably right ;-)

The software

The project is divided into several modules that partly rely on each other. The project structure looks like this in WinAVR’s Programmer’s Notepad:

software_project_structure

Here’s a short description of the modules:

  • segdisp: Drives the seven-segment display. Defines the characters and how they are mapped to the display segments. Supports text and numeric output (based on format) and blinking and scrolling (“marquee”). Uses timer0.
  • timer0: Provides a “system clock” with 1ms resolution and an interrupt service that allows other modules to have functions called on a regular time basis.
  • pwm: Provides functions to generate PWM output.
  • utils: Convenience and utility functions.
  • encoder: Supports evaluation of a rotary encoder with a number of options. Uses timer0.
  • format: Provides integer to string conversion without printf’s overhead.
  • modis.c: The main program.

timer0 is probably the most interesting module apart from main. It sets up a more or less stable interrupt driven time pulse of 1ms and counts these pulses as “ticks”. Other modules can “hook” themselves into this pulse to be called in regular intervals. For example, encoder queries the encoder pins every millisecond to determine the rotation via this mechanism. segdisp hooks into timer0 to implement its multiplexing, blinking and scrolling, all of which must be done on a regular time basis. The timer0 module relieves these modules of setting up their own timer interrupts which could lead to conflicts. The timer ticks serve as a “system clock” for easy measurement of time intervals.

The other modules are quite straightforward and I invite you to take a look at the code yourself. They are all reasonably well commented so it should not be a problem to understand what’s going on.

Let’s have a high-level overview of the main program:

software_control_flow

This is just a rough overview of what’s going on in the program. If you are new to projects like this you might be overwhelmed. However, keep in mind that this program has not been developed “top-down”, but evolved over several steps.

Next, I’m presenting the most important parts of the source plus an explanation where necessary.

Variable declaration

I usually declare the most important variables and #defines at the beginning of the file, outside of the main() method. It pays off to comment these well, so it should be sufficient if I just give you the source code here:

/***** Variables and constants *****/

// maximum speed set value in EEPROM (user can’t set more than this)
// this value can be configured at startup
uint16_t EEMEM max_rot_ee;

// maximum output shaft speed; depends on voltage, motor and gears
#define ABSOLUTE_MAX_ROT        450
static int16_t max_rot = ABSOLUTE_MAX_ROT;

// measuring and adjustment interval (control cycles) in Hz
// The more cycles are used, the quicker is the control feedback.
// At the same time accuracy suffers because of calculation overhead.
#define CONTROL_INTERVAL    10

// ratio of motor RPM to output shaft RPM
#define GEAR_FACTOR        9

// current speed
static volatile uint16_t speed;

// smooth rotation history buffer
static uint16_t speedHist[8];
int16_t smoothRot = 0;

// smooth ADC history buffer
static uint16_t adcHist[8];
uint16_t smoothAdc = 0;

// control state
enum {NORMAL, BRAKE_TO_ZERO};
static uint8_t state = NORMAL;

// user selected set speed
int16_t speedSet = 0;

// user selected set speed in the previous control cycle
int16_t oldspeedSet = 0;

// number of remaining cycles to display the set speed instead current speed
uint8_t displaySetCycles = 0;
// number of cycles the set value is shown on user activity
#define DEFAULT_DISPLAY_CYCLES     10

// current pwm value
int16_t pwm = 0;

// current that is drawn by the motor
uint16_t current;

// maximum current value before the brake kicks in
#define OVERCURRENT 800

// counts subsequent cycles where current > overcurrent
uint16_t overCurrentCount;

// counts cycles during which nothing is done (loop control pauses)
// this increases the effectiveness of measures like e.g. braking, as
// the brake is not released immediately but is active for several cycles
int16_t doNothingCounter;

// debug: display pwm value instead of current speed
bool showPulse = false;
int pulseOn = 0;
#define PULSE_DURATION 20    // 20 cycles display ratio pwm/speed

// idle time in milliseconds. If the user does not perform any action during this
// time, the machine stops turning.
#define IDLE_TIME 180000L

// system ticks since last user action occurred
long lastActionTime;

Interrupt routine

The interrupt routine that counts the speed impulses is very simple:

/***** Interrupt handlers *****/

// Speed counter interrupt handler
ISR(INT1_vect) {
    speed++;
}

Initialization

The initialization phase sets up the port directions, interrupts and A/D converters. It initializes the internal data structures of the timer0, segment, encoder and pwm modules. During this time we don’t want to be interrupted, so we wrap the section in cli() and sei() commands. This code is right at the beginning of the main() method.

cli();

// initialize the common timer and interrupt handler chain
timer0_init();

// interrupt setup for INT1 (speed impulse counter)
// trigger at rising slope
MCUCR |= (1 << ISC11) | (1 << ISC10);
// enable INT1
GICR |= 0b10000000;

// initialize segment display
// configure port D as output
// except PD3 which is used as interrupt input (INT1)
DDRD = ~(1 << PD3);
segment_init(4);

// initialize encoder
DDRC = 0;
encoder_init();

// motor control output
DDRB = 0xFF;
// setup motor control PWM interrupt
pwm_init();

// motor current sensing ADC setup
ADMUX = (1 << REFS1) | (1 << REFS0) | (1 << MUX0);  // ADC Ref is internal, select ADC1
ADCSRA = (1 << ADEN) | (1 << ADSC) | (1 << ADPS1) | (1 << ADPS0); // ADC enabled

// start running
sei();

The second part of the initialization phase lets the user configure the maximum target value. There’s a short so-called “edit loop” that we enter if an encoder movement is detected during the first second of startup. The edit loop is exited if no movement can be detected for some time, e.g. five seconds.

If the eeprom value is read at the first time it will be –1. This is a case that can only occur when the program has not written a value, so we assume the maximum. During the edit loop the end value is limited to the range [200, ABSOLUTE_MAX_ROT].

// read config from eeprom
max_rot = eeprom_read_word(&max_rot_ee);
if (max_rot == -1)
    max_rot = ABSOLUTE_MAX_ROT;

// show that we’re initializing
segment_set_text("INIT");

long init_ticks = timer0_ticks();

// check encoder within the first second
while (timer0_ticks() – init_ticks < 1000) {
    // encoder moved?
    if (encoder_read() != 0) {
        // let the user adjust the maximum value
        segment_set_text("END");
        delay_ms(500);
        segment_set_int(max_rot);
        #define INIT_MENU_TIMEOUT 3000
        init_ticks = timer0_ticks();
        // edit loop
        while (true) {
            int delta = encoder_read();
            // encoder moved?
            if (delta != 0) {
                // timer reset
                init_ticks = timer0_ticks();
                max_rot += delta;
                if (max_rot < 200)
                    max_rot = 200;
                if (max_rot > ABSOLUTE_MAX_ROT)
                    max_rot = ABSOLUTE_MAX_ROT;
                segment_set_int(max_rot);
            }
            // end edit on timeout
            if (timer0_ticks() – init_ticks > INIT_MENU_TIMEOUT) {
                // save end value in eeprom
                eeprom_write_word(&max_rot_ee, max_rot);
                break;
            }
        }
    }
}

After the initialization phase is complete we enter the main loop.

Program state

The main loop performs the repeated calculations that are necessary for controlling the speed of the disc. One loop iteration is referred to as a “cycle”. At a given time the “program state” affects the way the program operates, and the program changes the state if necessary. You can think of the main loop as a more or less complicated state machine. To design and understand a program like this you need to understand the different states that affect the program, and the transitions from one state to another.

It can be difficult to understand the states and transitions from the source code alone. I usually take care to make it easier by following these rules:

  • Put global state variables before the main program and comment them well.
  • Local helper variables that do not represent global state are defined locally.
  • Important state variables should be of an enum type with self-explaining names.

Let’s again have a look at some important state variables.

// control state
enum {NORMAL, BRAKE_TO_ZERO};
static uint8_t state = NORMAL;

This variable contains the most important distinction: Are we running in “normal” closed loop controller mode or do we brake until the disc stops to spin. The later will be the case if the user wants to change the direction of the disc.

// user selected set speed
int16_t speedSet = 0;

// user selected set speed in the previous control cycle
int16_t oldspeedSet = 0;

// number of remaining cycles to display the set speed instead current speed
uint8_t displaySetCycles = 0;
// number of cycles the set value is shown on user activity
#define DEFAULT_DISPLAY_CYCLES     10

displaySetCycles affects the state of the display. If this number is positive we display the set value instead of the current speed. If it is greater than zero we decrease it by 1. This is a kind of “sub-state” that affects only the display output, not the behavior of the controller. We can set the value to a positive integer from anywhere in the main loop to cuase the set value to be displayed.

// counts cycles during which nothing is done (loop control pauses)
// this increases the effectiveness of measures like e.g. braking, as
// the brake is not released immediately but is active for several cycles
int16_t doNothingCounter;

This variable works similar to displaySetCycles. It bypasses the closed loop controller for some time.

Main loop

As described before, the main loop contains code that is repeatedly executed. Let’s have a closer look:

// remember previous set speed
oldspeedSet = speedSet;

// evaluate encoder
speedSet += encoder_read();
// limit speed value to maximum
if (speedSet > max_rot) {
    speedSet = max_rot;
    displaySetCycles = DEFAULT_DISPLAY_CYCLES;
    lastActionTime = timer0_ticks();
}
// limit speed value to maximum (opposite direction)
if (speedSet < -1 * max_rot) {
    speedSet = -1 * max_rot;
    displaySetCycles = DEFAULT_DISPLAY_CYCLES;
    lastActionTime = timer0_ticks();
}

// set value changed?
if (oldspeedSet != speedSet) {
    displaySetCycles = DEFAULT_DISPLAY_CYCLES;
    // remember ticks of last user activity
    lastActionTime = timer0_ticks();
    // Are we starting from 0?
    if (smoothRot == 0) {
        // set a high PWM value to overcome initial inertia and friction
        // the value will be quickly regulated down, but this makes the disc
        // more responsive (user feedback)
        pwm = 300;
    }
}

// Has the user been inactive for the ticks defined in IDLE_TIME?
if ((state == NORMAL) && (timer0_ticks() – lastActionTime > IDLE_TIME)) {
    // stop the motor without hard braking (let the disc roll out)
    speedSet = 0;
    oldspeedSet = 0;
    pwm = 0;
}

Main state switch

Now follows the “main state switch”. First we check whether the sign of the old set speed value and the current set speed value differ. If this is the case the user has told us that the direction of the disc should be changed. Now we can’t do this right away for a simple reason: As there is no sensor for the direction we have to apply the sign of the set speed to the measured speed for display. If the disc is still rotating in one direction, we can’t simply switch the sign to negative if the speed is still 100, for example – this would look very odd. We therefore force the user to wait until the rotation has stopped altogether. They can then enter either a positive or negative value and the disc spins accordingly.

// Are we in normal running mode?
if (state == NORMAL) {
    // Has the direction changed?
    // Are the sign bits different?
    if ((speedSet >> 15) ^ (oldspeedSet >> 15)) {
        // We have to return to 0 to change direction.
        // Brake hard to stop the disk as quickly as possible.
        state = BRAKE_TO_ZERO;
    }
    // Setup the H-bridge
    if (speedSet == 0)
        // L298: Open the H-bridge (let the disc spin freely)
        PORTB = 0;
    else
    if ((speedSet > 0) && (doNothingCounter <= 0)) {
        // L298: Set the H-bridge to spin the motor right
        PORTB |= (1 << PB4);
        PORTB &= ~(1 << PB5);
    }
    else
    if ((speedSet < 0) && (doNothingCounter <= 0)) {
        // L298: Set the H-bridge to spin the motor left
        PORTB |= (1 << PB5);
        PORTB &= ~(1 << PB4);
    }
} else if (state == BRAKE_TO_ZERO) {
    // L298: Short-circuit the H-bridge for braking.
    // The current induced in the motor coils is "burnt" in the L298’s semiconductors.
    // That’s why we need a heat sink to prevent L298 burnout.
    PORTB &= ~(1 << PB5);
    PORTB &= ~(1 << PB4);       

    // While braking, the set speed is always 0 and can’t be changed.
    speedSet = 0;
}

Setting state = BRAKE_TO_ZERO takes effect from the next cycle when the L298 short-circuits the motor coil to brake. In the normal running mode we setup the direction of the H bridge; for convenience this is done in each cycle. Strictly speaking we would have to set the direction only at a state transition; however, this code is much simpler. Note how we do not set the direction if doNothingCounter is greater than 0. This allows us to keep braking for the some cycles if disc speed is too high (this is determined a little below).

Measurements

Now it’s time to measure the actual speed and current drawn by the motor:

// Speed measurement

// Reset the speed counter
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
    speed = 0;
}

// Delay for e. g. 100 ms if the control interval is 10
delay_ms(1000 / CONTROL_INTERVAL);

// now the number of slotted disc pulses has been written to speed
// by the interrupt routine.

The define CONTROL_INTERVAL determines how many cycles we intend to run per second. More cycles per second require more CPU cycles which means that there is an upper limit how far you can go. More cycles will also mean that the speed measurement becomes more inaccurate because less speed impulses will be counted during each cycle. Less cycles, on the other hand, will make the control loop less responsive. This value should be determined experimentally.

As you may note I do not disable interrupts during the next code part. This means that the value in the speed variable may increase during the code execution, until it is finally taken into account for the next calculation. I regard this error as negligible, though.

// Current measurement

uint16_t sample = 0;       
// Measure current 8 times and take the mean value
for (int i = 0; i < 8; i++)
{
  ADCSRA |= (1 << ADSC);        // start conversion
  while ((ADCSRA & (1 << ADSC)) == (1 << ADSC));  // wait for conversion to finish
  sample += ADCW;  // sum up the sample values
}
current = sample /= 8;  // mean value

// Smoothen ADC value again over the last 8 cycles (low pass filter)
smoothAdc = 0;
for (int i = 0; i < 8; i++) {
    smoothAdc +=adcHist[i];
    if (i > 0)
        adcHist[i - 1] = adcHist[i];
}
adcHist[7] = current;
current = smoothAdc / 8;

The current measurement part takes the mean of eight ADC measurements and smoothes it further over the last eight cycles’ values. It’s better to have a little more filtering here (analog R/C, eight measurements plus smoothing) because there can be quite a lot of PWM noise on the measurement line. In fact I’m not sure if this is really the best way to do this, but it seems to work reasonably well.

Closed loop control

The following code block is a little larger. It contains the most important part of the closed loop controller.

// Closed loop control

// Calculate output shaft speed.
// The order of calculation is important to avoid integer over-/underflow.
// The factor (speed * CONTROL_INTERVAL) should be safely divided by 120 (slot count).
uint16_t speedOut = speed * CONTROL_INTERVAL
    / 120     // pulses per motor rotation (slotted disc slots)
    * 60     // per 1 minute
    / GEAR_FACTOR;

// Did we arrive at 0 by braking?
if ((state == BRAKE_TO_ZERO)  && (speedOut == 0)) {
    // Switch back to normal so that the machine becomes responsive again.
    state = NORMAL;
}

// determine absolute difference of the actual and set speed

#define speedSetAbs    speedSet * (speedSet < 0 ? -1 : 1)
int16_t delta = speedSetAbs – speedOut;

// There is a counter for cycles in which we do not change the settings.
// Decrement the counter.
doNothingCounter–;
// Have we reached 0?
if (doNothingCounter < 0) {
    // now we may do something
    doNothingCounter = 0;

    // This is the implementation of the closed loop control.
    // As the result of our calculations we modify the PWM value.
    // The pwm value determines the current the motor will get over time,
    // and thus the power exerted by the motor. More power means the
    // motor turns faster.
    // If delta, the difference between the set speed and the actual speed,
    // is less than 0, it means that we should decrease the pwm value and brake.
    // Is it greater than 0 it means that we should increase the pwm value.
    // For larger differences we react faster. For small differences we react
    // slower. This has the effect that the machine responds faster to greater
    // changes. In a PID controller, this corresponds to the differential
    // (or derivative) value.
    // 20 is the maximum difference we can react to
    if (delta < -20)
        delta = -20;
    if (delta > 20)
        delta = 20;
    if (delta < -15)  {
        pwm -= 4;
        // brake if below 400 rpm
        if ((speedOut < 400) | (delta < -40)) {
            PORTB &= ~(1 << PB5);
            PORTB &= ~(1 << PB4);   
        }
        doNothingCounter += 4;
    } else 
    if (delta < -10) {
        pwm += 3;
        // brake if below 400 rpm
        if (speedOut < 400) {
            PORTB &= ~(1 << PB5);
            PORTB &= ~(1 << PB4);   
        }
        doNothingCounter += 13;
    } else
    if (delta < -5) {
        pwm -= 2;
        // brake if below 400 rpm
        if (speedOut < 400) {
            PORTB &= ~(1 << PB5);
            PORTB &= ~(1 << PB4);   
        }
        doNothingCounter += 2;
    } else
    if (delta < 0) {
        pwm–;
        // brake if below 400 rpm
        if (speedOut < 400) {
            PORTB &= ~(1 << PB5);
            PORTB &= ~(1 << PB4);   
        }
        doNothingCounter += 1;
    } else
    if (delta > 15) {
        pwm += 4;
        doNothingCounter += 4;
    } else
    if (delta > 10) {
        pwm += 3;
        doNothingCounter += 3;
    } else
    if (delta > 5) {
        pwm += 2;
        doNothingCounter += 2;
    } else
    if (delta > 0) {
        pwm++;
        doNothingCounter += 1;
    }
    // Limit the pwm value
    if (pwm > 511)
        pwm = 511;
    if (pwm < 0)
        pwm = 0;
}       
// End of the closed loop control

The most important thing to know is the difference between set speed and actual speed, in rotations per minute of the output shaft. This value is stored in delta. As you see in the large nested ifs we are reacting differently to different ranges delta may fall into.

This code is full of magic numbers. It’s important to understand that all of these numbers have been determined by experiments. It’s an approximation of a mathematical formula that you could derive from the PID controller’s equations. The basic thing to start with is the following code:

if (delta > 0)
    pwm++;
else if (delta < 0)
    pwm–;

This code works but is not very fast for large differences. I therefore added different branches to let pwm change faster for larger deltas. This worked better but still not good enough: The large disc at the output shaft has quite a large moment of inertia, so delta would stay large while pwm would continue to grow. When finally the disc had spun up pwm was far too high, resulting in what’s called overshoot. A good closed loop controller will have little overshoot. Now the overshoot depends mostly on mechanical factors such as the disc inertia and the reaction of the motor to higher pwm values. I found it difficult to derive these values from the mechanical properties, simply because I don’t know that much physics, so I tweaked the controller code until I found it satisfactory. The introduction of the doNothingCounter improved overshoot as it gives the mechanics some “empty” cycles to react to the pwm changes. Still the loop control wasn’t good especially for small target values.

One addition which improved this very much was the braking. Applying the brakes greatly helps to stabilize the speed very fast. To give the brakes more effect they are also using the doNothingCounter. One peculiarity with my mechanical construction is that at speeds above 400 the braking effect kicked in so hard that the belt began to make loud knocking noises. This occurred at speeds over 400 (or slightly below) so I disabled braking for these speeds (applying the brakes for a shorter time would have also helped, but I would have had to increase the cycles per second).

If you are making your own controller my advice for you is: Start with a simple linear controller and see how well the system reacts at different set speeds, especially with regard to overshoot. Then tweak the code by introducing a few ifs for different value of delta. Try to find out how fast pwm needs to grow and how long you should delay to make the system more responsive and less prone to overshoot. Experimentation is key here. If you have the ability to brake (not all electronics would perhaps be able to do so) apply it with caution to fine-tune the system. Finally you could turn your ifs into a short and elegant mathematical formula.

Setting the value

Now is the time to act on the values we determined above.

// Set the PWM value

// normal operation=?
if (state == NORMAL) {
    // stop blinking in case it was on
    segment_blink_off();

    // does the motor draw too much current?
    if (sample > OVERCURRENT) {
        // stop the pwm immediately, the motor gets nothing
        pwm_set(0);
        // Delay for some time to let things cool down
        // increase the delay if the overcurrent lasts longer
        if (overCurrentCount < 80)
            overCurrentCount += 20;
        // indicate that we had to stop the pwm
        segment_set_text("STOP");
        delay_ms(overCurrentCount * 10);
        // reduce the pwm value to reduce the risk of overcurrent
        // in the next iteration
        pwm *= .8;
    }
    else {
        // no overcurrent. Set the pwm value
        pwm_set(pwm);
        // Decrease overcurrent delay
        if (overCurrentCount > 0)
            overCurrentCount -= 2;
    }
} else
if (state == BRAKE_TO_ZERO) {
    // full brake -> short-circuit the H-bridge
    PORTB &= ~(1 << PB5);
    PORTB &= ~(1 << PB4);    
    pwm_set(511);
    if (pwm > 0) {
        pwm = 0;
        segment_blink_on(50);
        segment_set_text("0000");
    }
}

In the normal state we react to a possible overcurrent condition or set the new pwm value if everything is ok. At the first cycle of BRAKE_TO_ZERO (pwm will be > 0) the display is set to a blinking “0000”. The brake short-circuits the engine coil to make the disc stop faster.

Speed display

One thing left to do is to display the set speed or the current speed to the user. We only do this in the normal state in order to not override a possible “0000” we have set above when braking to zero. If we display the output speed as calculated in each cycle it changes much too fast which is hard to read. I therefore applied a smoothing filter. The variable displaySetCycles contains the number of cycles we still display the set value after a user interaction. While this value is positive the set speed is displayed and a dot per segment is added to indicate that this is the set value.

If showPulse is true the pwm value is displayed instead of the speed value. This helps with debugging and tweaking the loop controller code.

// Calculate rotation for display

smoothRot = 0;
for (int i = 0; i < 8; i++) {
    smoothRot += speedHist[i];
    if (i > 0)
        speedHist[i - 1] = speedHist[i];
}
speedHist[7] = speedOut;
smoothRot /= 8;

if (state == NORMAL) {
    if (displaySetCycles > 0) {
        displaySetCycles–;
        segment_set_int(speedSet);
        // dots indicate preset value
        segment_add_dot(0);
        segment_add_dot(1);
        segment_add_dot(2);
        segment_add_dot(3);
    } else {
        if (showPulse && (pulseOn > 0)) {
            pulseOn–;
            // display pwm value
            segment_set_int(pwm);
            segment_add_dot(0);
        } else {
            // show actual speed
            segment_set_int(smoothRot * (speedSet < 0 ? -1 : 1));
            // decrease pwm display duration counter
            pulseOn–;
            // in the range of -PULSE_DURATION to 0 the actual speed is displayed
            // in the range of 0 to PULSE_DURATION the pwm value is displayed
            // ==> display ratio 1:1
            if (pulseOn < -PULSE_DURATION) {
                pulseOn = PULSE_DURATION;
            }
        }
    }    // display speed
}    // state == NORMAL

Final words

The Machine worked quite well so far and is now used in the second exhibition. We have determined that the speed error is about 2% which is quite good actually.

I think the design will give you a good starting point if you want to make your own similar machine. If you have any questions or if I have forgotten something, just ask. Of course there might be errors or bugs, so please let me know if you find one.

Download the project files

  1. No comments yet.
  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: