Introduction: Pulse Generator
When you need pulses of any sort, long, short and with variable frequency you can use one or two 555 timers. That works, is easy to set up and is cheap. But it also limited to a certain range in frequency and pulse length.
If you want a precise pulse length you will need an oscilloscope to check it. The same goes for when you want an exact frequency.
I needed pulses of exactly 1us, 5us, 1ms and 5ms, I decided that trying to do that with 555 timers was not what I wanted.
So I build a versatile pulse generator with a microcontroller and a crystal to produce exact results and a display to show the current values for frequency and pulse length.
EDIT: The code is now available on GitLab
STM8S103F3 on development board
16 MHz crystal
ST7735 1.8 inch TFT display
rotary encoder (with button)
STMicroelectronics ST-Visual_Develop (IDE)
Cosmic Software STM8 compiler
ST-Link-V2 (or a clone)
manuals about the STM8S103
Step 1: The STM8S103F3
Usually I pick a STMicroelectronics STM32 or an Atmel (now Microchip) ATMEGA or even ATTINY microcontroller. This time I decided to use a STM8 microcontroller, the STM8S103F3. You can buy them on a breakout board for less than a euro.
While it is less capable than even the lowest of the STM32 series, it is somewhat comparable with the ATMEGA328 in speed and number of pins. But it only has a quarter of flash memory, just 8kByte.
The peripherals on the other hand are more capable than those in the ATMEGA328, esspecially the timers are awesome. And those are exactly what is needed for a pulse generator.
Step 2: IDE and Compiler
I program the STM8 in C with STMicroelectronics free "ST Visual Develop" (STVD) and the, also free, STM8 compilers from Cosmic-Software. (to download and use these programs you do need to register with their respective companies) Both programs feel rather old fashioned but they work well.
Most (all?) microcontrollers have pins with many alternate uses, they can be standard inputs or outputs, but can also be connected to one or more peripherals in the microcontroller. The selection of what alternate use a pin can have is dne with "Option Bytes" in a STM8S103F3. It can be done when the code is uploaded to the microcontroller but I prefer to use a separate program to do that called ST Visual Programmer, the option byte needed here is AFR0 (alternate function bye 0) see the screen print of the option bytes.
To upload the code into the microcontroller and to program the option bytes I have an official ST-Link-V2 but you can just as well use an incredibly cheap clone (3 euro).
Then there is STM8CubeMX. If you know the STM32 version of this program you will be disappointed with this program. The only real use of it is to make a layout of what pins get what function. You cannot produce any code with it as you can with STM32CubeMX.
Step 3: Standard Peripheral Library / Bare Metal
At first I used the Standard Peripheral Library (SPL) from STMicroelectronics, but I ended up coding on "the bare metal" as it is called. This means that instead of writing
GPIOC->ODR |= GPIO_PIN_0; .
It does not make the code much smaller as the SPL is well written, but when the code was getting bigger, it didn't fit in the available flash of the microcontroller when I used the SPL, but it did fit when written without the SPL. (in the finished program there are just a few bytes left)
The pictures show the configuration of the clock via the SPL and the Bare_Metal method. It takes a little time to get used to it, but it isn't hard.
Step 4: Change of Mind
What also changed during development was the choice of display. I started with a LCD16x2 display but changed to a 1.8 inch ST7735 TFT. The main reason for this change was the size of the display, the LCD16x2 did fit in the project box I had in mind, but it would have used most of the frontpanel of it. The ST7735 is smaller so it made the layout of the frontpanel (if you can call it that) easier. As a bonus you can display a more text on it. Porting the drivers for the ST7735 from another project, with another type of microcontroller, proved to be easy.
Step 5: User Interface
A pulse generator doesn't need a complicated user interface, in this case a single rotary encoder is enough. When the generator is switched on for the first time, the rotary encoder manages the frequency, press it once and the pulse width is changed.
There is a problem with being able to set the frequency and pulse width separately. Many combinations simply cannot work, you cannot produce pulses of 1 second length 50 times per second. I chose to make the maximum pulse length dependent on the frequency. (the other way round is also possible). This maximum possible pulse length is always shown. When you try to set a longer pulse length it ignores it. When you set a pulse length and increase the frequency it will do so until it reaches the maximum possible frequency at that pulse length and it changes the function of the rotary encoder from frequency to pulse length. There you can then decide to shorten it or leave it as it is, lengthening isn't possible of course.
Step 6: Timers
The STM8S103F3 has three timers, a basic timer, and general purpose timer and an advanced timer. The advanced timer (TIM1) can be used as a One-Pulse timer, just what is needed. When a One-Pulse timer is triggered it counts from zero to the value set in the Auto Reload Register (ARR) and then stops, until it receives another trigger.
TIM1 has four input/output channels of which one is an input used to trigger the timer and the second channel is the output for the pulse-output. Unfortunately it does not have the option to trigger it iternally from another timer, that option is reserved for bigger versions of this microcontroller, that have more timers. Not that it matters in this project, it would only have made one connection from one pin to the next pin unneccessary, but I like the idea of keeping as much as possible inside a microcontroller.
The general purpose timer (TIM2) is used to generate the frequency that triggers TIM1. While TIM1 has a prescaler that divides the clock frequency (16MHz) by any integer from 1 to 65536, TIM2 only has a prescaler that can be set to powers of two (1, 2, 4, 8, 16, 32 .. 32768). Only one output of TIM2 is used and is (as said) on the outside of the microcontroller connected to the input of TIM1.
Many frequencies can be generater with good accuracy or even exact values, but not all. One example: to create a frequency of 700 kHz from a clock of 16 MHz you need to divide it by 22.857, that simply isn't possible with TIM2, you can divide by 23 to produce 695 kHz or divide by 22 which gives 727 kHz, I chose to produce 695 kHz as that is closest by. The next frequency, 800 kHz, can be produced exactly again. In the low frequencies ranges the error for "odd" frequencies, such as 30 Hz, is so small you can ignore it.
With the pulse length this problem is not there as both the prescaler and the Auto Reload Register of TIM1 can be set to any integer from 1 to 65536. But there is another problem with the shortest pulse lengths, the timer isn't infinitely fast internally, it takes time to switch the output from 0 to 1 and back. This time is short, just 1 clock cycle, but that is 62.5ns and when the pulse you want is 300ns it is a big error. My solution is very simple, for the pulse lengths shorter than 1us the display shows the selected value plus 63ns. This way the display and the real pulse length are almost exactly equal.
Step 7: Calculations
In LibreOffice Calc I made tables with the needed prescalers (PSCR) and highest numbers the timers can count to (ARR). I also made a table to find what the maximum pulse length could be. These tables you can find in the files tables.h and tables.c . The maximum settings I did not put into these files but handled in a switch-case statement, as it turns out the be very regular how much the maximum pulse length needs to be relative to the current frequency.
I won't bother you with the calculations, the result can be seen in the file tables.c
Step 8: Code and How I Handle the Hardware
The rotary encoder and it button are read via GPIO ports that are configured as external interrupt sources (EXTI). The interrupt routines are very simple, as soon as they are called they disable themselves to prevent bounce, they set a timeout after which they are re-enabled and set variables "button" and "encoder" with the current value. For button that is a simple "1" when it is pressed, for the rotary encoder it is a "-1" or "1" depending on the rotation.
The third timer (TIM4) is used for handling timeouts, it generates 1000 interrupts per second, it counts down the timeout set in the iterrupt routines of the rotary encoder and its button and does the re-enabling of those interrupts.
As the timer TIM1 and TIM2 use some GPIO that cannot be changed to other pins, the SPI peripheral (for sending data to the display) could no longer be used. This meant that instead of passing on the data to the SPI peripheral I had to make the SPI signals by bitbanging them. Bitbanging SPI is easy but it isn't as fast as using a dedicated peripheral. You can see that when you switch on the generator, it takes some time to clear the screen. Fortunately the amount of data that is send to the display in normal use is small so the performance is acceptable.
The output of the microcontroller is fed into a 74HC14, a hex inverter, the first inverter is connected to the microcontroller, its output goes to the inputs of all five other inverters. Of those five, three have the outputs connected with 150 ohm resistors to the output connector. This gives a nice low impedance output signal and at the same time protects the 74HC14 from a shorted output. The other two inverter ouputs are not-connected
The schematic doesn't show the power supply, it is a 3.3V power supply made with a Holtek Semiconductor 7333 low dropout regulator and a lithium ion battery. The battery is "hidden" under the display.
Step 9: Result and Video
The result is a generator that can produce pulses from as high as 2 MHz down to 1 pulse every 100 seconds.
The pulse length can be set from 250 ns up to 90 seconds. Of course the limitations mentioned above apply, you can set a pulse with length 250 ns to appear every 100 seconds, but you cannot make a pulse of 10 seconds appear a 1000 times per second.
As the timers, once set, are independent of any other code running on the microcontroller, the signals are absolutely clean and regular.
The picture shows an extreme setting, one pulse of 250ns every 100s.
Runner Up in the
Build a Tool Contest