Wednesday, December 5, 2018

SOSS-S8: Simple Open Source Servo STM8S


SOSS-S8 is a DIY servo drive using feedback position control for DC motors with an incremental encoder. It is based on the low cost STM8S103F3 processor with all the functions handled properly by peripherals. Its main puropse is education, by allowing anyone to implement a closed loop position servo using very low cost components and completely open source software tools. You can learn many things with SOSS-S8:
  • Basic concepts in feedback control systems implementation (PID etc),
  • Convert continuous time designs into discrete time (digital control),
  • High precision fixed point math implementation in a tight control loop timing,
  • Writing code for real-time execution and using simple tools to check (LED, cheap logic analyser etc.),
  • Using sdcc, GNU make and standard peripheral library to write a software project for STM8S processor,
  • Simple digital filter implementation,
  • And others...

Currently a textbook PID algorithm is implemented. It is very satisfactory see the motor follow the reference signal, observe overshoot, steady state error, instabilities etc. or tune the control loop using textbook methods like Ziegler Nichols to achieve fast response and zero steady state error. But the real value comes when you connect the SOSS-S8 to your computer and see the control signals on your computer, in real-time, using the excellent SerialPlot software, which is also open source. The development was done in Linux, but it is perfectly valid for Windows or MacOS-X also.

It looks like this...
SOSS-S8 prototype. A PCB may follow someday...

Video here:


Specifications:

  • Control sampling frequency: 2kHz (can be an order of magnitude higher)
  • PWM frequency: 8kHz which is barely audible.
  • PWM resolution at 8kHz: 2000 counts
  • 16bit PID coefficients with 1/32 precision (scaling implemented for speed)
  • Hardware quadrature encoder reading suitable for high resolution encoders (500ppr, 1000ppr etc.)
  • 16 bit control parameters sent in real-time at 500 samples/second for graphing
  • Hardware PWM and timebase generation.
  • Generates its own time based step reference signals, or reads from a potentiometer using ADC.

Hardware:

SOSS-S8 is designed to use very low cost, off the shelf components:
Most of the components are already laying around in a typical workshop, but even if you buy them all, the maximum cost is less than $20. The circuit connections are very simple and can be done on a breadboard using the common "DuPont" patch cables in a few minutes.

I was too lazy to draw a proper schematic, especially since most people will probably be building it on a breadboard. So I drew a connection diagram instead.Next to each module there is a box with the names of connections of that module. Match the names of the connections in the box that is at the other end of the line.

Connection diagram. Match the same pin names at the same position at both ends of the lines.
(Sorry about the raster graphics -JPG- Blogspot does not allow vector graphics...)

How to install and run: Firmware

The firmware source code can be downloaded from my GitHub repository: SOSS-S8. The circuit connections can be found in the file motor.c as comments at the begining of the file. To build the firmware, you need to have sdcc installed as well as STM8S Standard Peripheral Library (one simple modification to the library is needed to make it sdcc compatible), a programmer software for the device programmer and  GNU make. I discuss how to install the development environment, in another blog post. You should test a few simple firmware projects to make sure they work before attempting this one. You can try the timebase project for example. About the only modification to the source is to modify both 'Makefile's, one in the main directory, and the other within 'libs' directory, to tell the compiler your path to the ST libraries. Windows users beware backslash '\' should replace forward slash '/' in the path values! Finally, all you should need to do is:
 $ make flash
and it should automatically compile first the files in the 'libs' folder, produce the local library file 'projectlib.lib', then continue with motor.c, and finally, if SOSS-S8 is connected to your computer, it will be uploaded and ran.

Remember that since this is a control system, the motor rotation direction is important. I took clockwise as viewed from the front of the motor as positive. That means if you apply a positive control signal to the motor, it should rotate in the positive direction, and the measured motor angle must be increasing, otherwise the control loop becomes positive feedback. If either of them are opposite, you should correct them: If the motor turns in the wrong direction, swap its terminals to the power amplifier, or if it turns in the right direction but the encoder reading is decreasing, swap the A and B leads of the encoder. If both are opposite, it will still work, but in the opposite direction to your reference commands. Of course you can also choose counterclockwise as the positive direction.

The communication settings

SOSS-S8 periodically sends measured values over the serial port. Currently it sends the measued motor angle, position reference and control signal at 500 samples/sec. This is quite fast even for the small and fast motor that I used in the prototype. To get the highest sample rate from the microprocessor at a given baud rate, we must minimize the number of bytes that are sent for each sample. The data is packed in the following way:
  • A constant synchronization sequence: 0xAA 0x55,
  • Parameter 1 formatted as 16 bit int, MSB first,
  • Parameter 2, 3, etc. in the same format,
  • No end delimiter or checksum.
For example, using 115200bps, to send 3 measurements, we need to send 8 bytes/sample. That means about 1400 samples/second can be sent, which is quite respectable! Roughly, it should be possible to send about 10 variables at 500 samples/sec before communication speed bottoms out. To add more measurements to the sent data, see the file motor.c. It is trivial; just type-cast the data to 16bit int, separate into two 8 bit u_int's and add them to the print buffer. The rest is taken care of automatically.

SOSS-S8 sends the data in binary form to minimize the processor overhead of formatting and to minimize the amount of data transferred over the serial connection. The serial data transfer is done in a non-blocking fashion: There is a print buffer, a buffer pointer, a message length variable and a "printing in progress" flag. A short code after the control routine periodically checks if the flag is set, and if the UART transmit buffer is empy, puts the next byte out. I did not bother writing a circular buffer for this because of the unnecessary computation overhead of pushing and popping data from the circular buffer. If you need, I have a sample circular buffer, let me know.

Going to the receiving end; we use the excellent program SerialPlot by hyOzd. The measurement data streaming from the serial port of SOSS-S8 in real-time is displayed on the PC. SerialPlot should be configured to synchronize to SOSS-S8 synchronization sequence, and accept int16 values in big endian format. This is configured in the "Data Format" tab of SerialPlot. Do the following settings:

  • "Custom Frame"
  • "Frame Start:" AA 55 (this is taken as hex without 0x prefix).
  • "# Channels:" 3
  • "Frame Size:" Fixed Size: 6 (use 2* # Channels)
  • "Number Type:" int16
  • "Endianness:" Big Endian
  • "Checksum:" unclick enabled.
That's it. Connect your USB-Serial converter, determine the port name (top dropdown list), click "Open" and you should see the motor parameters floating by, similar to an oscilloscope. For best results, adjust the sample size, graph size etc, from the "Plot" tab. I use 2000 samples for a good display. The graph here belongs to the motor shown in the top photo, with a reference that changes repeatedly every second, between 0 and 300 pulses.

You can take a snapshot of the data and save it as a csv file, to be plotted using your preferred graphing software (I like gnuplot, or perhaps Scilab, Octave, Matlab etc.). You can also pause the data, zoom into it and many other things.

Program structure:

The program is made up of the main part in motor.c, and the peripheral drivers in the individual files inside the libs folder. Here I will describe how the peripherals are used. I have first written the code using the functions provided by the STM8 Peripheral Libraries from the manufacturer. However, the code size quickly inflated. I designed the project in several parts (control, communication, timing etc.) and each part on its own quickly reached the ROM size limit. So I re-wrote the code by taking the individual register operations from the libraries and discarded everything else. So the code no longer depends on the library and the ROM size is now under 5kb. If you look at the source code, you will see the original library function calls commented out above their replacements.

Timer 1:

TIM1 is used as the hardware incremental encoder counter. It is initialized in the encoder mode. There is no interrupt associated with TIM1; it is read at control sampling times to calculate the control algorithm. The pins for A, B encoder inputs are PC6 and PC7. To be able to use PC6 and PC7 as TIM1 encoder inputs, it is necessary to program AFR0. To set it up you can write this into the configuration register:

 $ echo "00 00 ff 01 fe 00 ff 00 ff 00 ff" | xxd -r -p > TIM1CH1_Options.bin
 $ stm8flash -c stlinkv2 -p stm8s103f3 -s opt -w TIM1CH1_Options.bin
The associated files are: tim1.c and tim1.h.

Timer 2:

TIM2 is used as the hardware PWM generator for the power amplifier. I used it in asimple way where either TIM2 CH1 or CH2 generate the PWM waveform and the other is kept at ground level. After the calculation of the control signal, the PWM compare registers are adjusted. The PWM frequency is a compromise between frequency and resolution. We want high frequency so that it is not audible but at the same time we want the PWM count limit to be high for high control signal resolution. However their product equals the processor clock frequency. As a reasonable compromise, I selected 8kHz PWM frequency and 2000 count limit. The pins for PWM output are TIM2 CH1: PC5 and CH2: PD3. Again, PC5 can only be used as TIM2 output after the same modification of AFR0 explained above (no need to do anything else). The associated files are: tim2.c and tim2.h.

Timer 4:

TIM4 is used as a timebase interrupt generator. It is a simple 8 bit counter with an inflexible prescaler that can be set only to powers of 2 to divide the processor main clock. I set the prescaler to 1/32 and the count limit to 250 to obtain 2kHz INT. There is a further counter which raises a global flag at every millisecond to generate a ms event for easy timing. The MS event is counted in main() for several jobs. The associated files are: tim4_tbase.c and tbase.h.

ADC 1 CH2:

ADC is used as a reference measurement. A potentiometer can be conected here as a voltage divider. One minor simplification is that a new reading is initiated at the end of control signal calculation rather than at the begining. This is to reduce overhead before control calculation. We use ADC1 CH2 (shared with PC4).

main() triggers the ADC conversion. At EOC (End Of Conversion) an INT is produced. The conversion value is stored in the global variable ADC_RES to be used at the earliest convenience... Conversion is right aligned.

UART:

UART1 transmits variables to a PC. It is initialized to 115200Baud. Some simple string send functions are provided. At the moment, only transmit is used, but command receive methods will be later implemented.

ETC:

This rounds up the more significant peripherals. Of course there is more to it; the clock configuration, GPIO configuration and 7 segment LCD interfaces:

The files clock.c and clock.h manage the clock settings. HSI (High Speed Internal) clock is used and the processor clocked ad 16MHz. It also switches on the peripherals which are needed. The other peripherals are left powered off. Please check clock.c if you want to add more peripherals.

The files gpio.c and gpio.h are used to initialize the GPIO ports (LEDs etc.). Fairly straightforward.

The files HT1621_NoLib.c and HT1621_NoLib.h are for driving the LCD display. I think I had ported them from Arduino library to STM32, and then to STM8 here. The source was further modified to remove dependency on the STM Libraries.

Main function:

Let me explain a little about the main() last but not least. It is in motor.c. It starts typically with the peripheral and variable initializations. Especially the control variables structure ControlValues_t  and EncoderValues_t are initialized. Here the control gains are also set. I wanted a simple way of assigning square wave periodic reference. it is stored in the array references[]

Particularly, the define "ANALOG_REFERENCE" drives the control from ADC. If this is commented out, square wave reference is produced.

The main loop has the following functions:

Check the MS event flag if(MS_TickEventFlag==TRUE) and increment various counters. This makes it simple to generate timed events. For example, the reference value is updated every second.

If the flag for control signal reference is set, the motor encoder is read and motor position is displayed on the LCD.  If analog reference is asserted, first a low pass filter is used to smooth out the voltage reading (most pots are noisy!). The filter is implemented using int manipulation; we shift left, do the arithmetic and shift right 3 times which gives 1/8 precision. Then error calculation is done and control function is called. Finally the next ADC reading is triggered.

If the reference switch time has come if(REF_count>=REF_PERIOD), the next reference value is selected from the array references[].

If the time has come to transmit the next control values to the PC (I call that print): if(PRINT_count>=PRINT_PERIOD), the print buffer is populated. First the synchronization byets 0xAA and 0x55, then the variables are written to the buffer. I typecast the values to int16_t, and then write them to the buffer in big endian format. The printing state variable is also set to TRUE so that the non blocking print routine can send the characters.

Finally the non blocking print routine sends the characters one by one as soon as the UART transmit buffer is empty.

Finaly, the apply_control() function. It calculates the control algorithm and applies it to TIM2 for PWM generation. A simple anti-windup is also performed here. The time spent in the control algorithm can be used to check the processor utilizatin using an oscilloscope. I am hoping to find time to implement other control algorithms for demonstration in this function.


So, go ahead and build yourself a SOSS-S8 and try your hand at closed loop control implementation!