Projects Research & Design Hub

A Multipurpose Signal Generator

Written by Brian Millier

Using the ESP32

The ESP32 wireless MCU has powered a variety of Brian’s projects. Here, he shares the details of his efforts to use the device to build a multipurpose signal generator. The project illustrates how rich the ESP32’s set of on-chip peripherals is, beyond its wireless capabilities—which he doesn’t even use this time!

  • How to build a multipurpose signal generator using the ESP32 MCU
  • How to define the signal options
  • How to develop the user interface
  • How to design the sine wave generator
  • How to design the clock generators
  • How to design the pulse generator
  • How to use the 3-phase PWM generator
  • How to understand the dead-time options
  • How to craft the supporting circuitry
  • How to develop the software using the ESP-IDF
  • Espressif Systems’ ESP32 MCU
  • Espressif IoT Development Framework,
  • ConnectEVE 4.3” display from Mikroelektronika
  • FTDI’s FT800 display controller
  • Silicon Labs Si5351A
  • Texas Instruments (TI) INA128 Instrumentation amplifier
  • Princeton Technology’s PT8211 I2S DAC
  • TI’s DAC7611U DAC
  • TI’s DCP010512DBP-U DC/DC bipolar power module
  • 3W AC/ DC power module—the RAC03-05SK from RECOM
  • Microchip MCP42010 dual 8-bit digital potentiometer

Back when the ESP32 from Espressif Systems was first introduced, I wrote an article about it in Circuit Cellar (“Exploring the ESP32’s Peripheral Blocks,” Circuit Cellar 332, March, 2018). Instead of focusing on its Wi-Fi/BLE capabilities, I explored some of its unique built-in peripherals. Since then, I’ve been using it in Wi-Fi projects, some of which have been described in subsequent Circuit Cellar articles.

For this month’s article, I decided to take another look at the ESP32’s unique peripheral blocks, with an eye on building a multipurpose signal generator. Before I retired from Dalhousie University (Nova Scotia, Canada), I had access to some professional signal and pulse generators for both my work-related and personal projects. Now, I don’t personally do such demanding work as to require fancy signal generators with professional specifications. But I still run into situations where I need a variety of different signal generator functions for design and troubleshooting purposes.

For any readers old enough to have read Circuit Cellar issue 78 back in January, 1997, I wrote an article, “Pulse’05 Pulse Generator.” This project generated a clock and two separate, synchronized pulses with a wide range of period/width/delay times. This was accomplished using a highly specialized timer chip, the AM9513, and a tiny NXP (formerly Motorola) 68HC05 microcontroller (MCU). It was still in use occasionally when I decided to design this more ambitious signal generator project.


Because many of my projects are mixed signal (as the name of my column suggests), I wanted to generate both analog and digital signal types. Starting with analog signals, I wanted the following:

1) A sine wave generator covering the low Hertz range to beyond the normal human hearing range. I wanted a wide range of signal amplitudes up to 18VP-P with an adjustable DC offset capability.
2) An accurate, adjustable DC voltage reference in the ±4.0V range.

All the other signals of interest to me were digital signals of one form or another:


Advertise Here

1) A square-wave clock signal covering the frequency range of 7Hz to 3MHz. This is handled by the ESP32 itself, using the PLL-controlled I2S function block.
2) A square-wave clock signal covering 100kHz to 100MHz. This signal is not generated by the ESP32 itself. Instead, it uses the ubiquitous Si5351 clock generator chip by Silicon Labs.
3) A pulse generator capable of generating three discrete pulses. All three pulses would share a common, user-adjustable period. Each individual pulse is adjustable in width and delay time with respect to pulse #1.
4) A 3-phase PWM (pulse width modulation) generator. This provides a user-adjustable frequency, user-adjustable pulse width and dead time. Both an active high and a complementary output are provided for each of the three phases.

Before discussing each of these generators in detail, I’d like to describe what I settled on for a user interface.


With so many different types of signals—covering a wide range in terms of period/frequency/width/delay—several parameters must be entered and displayed. To enter such a wide range of numbers—some integers, some floating point—it seemed like there was no choice but to use a numeric keypad of some type. Also, there would need to be several switches to select among the various signal types, and additional switches to select which of the many parameters were being set. The only feasible display would be an LCD with enough rows to simultaneously display all the adjustable parameters for a given generator function.

A physical keypad and all the other necessary switches would have required a lot of wiring and used a lot of the ESP32’s GPIO lines. The ESP32 is a versatile MCU, but it has only 23 GPIO lines available. Many of these would be needed for the various signal outputs, and a common alphanumeric LCD display would need six more.

Instead, I decided to use a TFT LCD display with touchscreen capability. In place of a physical numeric keypad, the display, itself, would provide a virtual keypad (Figure 1). Rather than all the signal function and parameter selection switches, the touchscreen display would display all the necessary “buttons” needed to configure any particular signal-generation function. There were a few parameter adjustments that I thought would be better served by using a rotary encoder instead of the on-screen keypad. The entire user interface is handled by the TFT touchscreen display and a single rotary encoder.

FIGURE 1 – The Multi-Generator display, showing the touchscreen numeric keypad

I chose a 4.3″ TFT touchscreen, based on the FT800 display controller made by FTDI (known for its popular USB bridge chips). Considering how many switches and how much wiring it eliminates, I felt that the cost (about $72) of this display was justified. The FT800 controller is very sophisticated and provides many high-level “widgets” that make designing the GUI somewhat easier—at least in my case, since I had climbed the learning curve for this controller on earlier projects. Figure 2 shows the initial menu screen on this display. In the sections that follow I’ll describe both the hardware and software for each of the signal generation functions.

FIGURE 2 – The main menu for the Multi-Generator, showing the six separate functions

Although the ESP32 contains two, 8-bit DACs that can be coupled with an internal cosine generator block, I felt that the 8-bit resolution was not adequate. I decided to generate the sine wave using a software-based Direct Digital Synthesizer (DDS). In simple terms, this DDS uses a sine wave look-up table in SRAM with a 16-bit resolution. It scans through this look-up table with a variable phase increment value, which determines the frequency of the sine wave. The 16-bit amplitude values taken from this look-up table are fed to the I2S output port. The data stream to the I2S DAC must be at a constant rate. This is handled transparently by the ESP32 firmware. You merely keep an SRAM buffer loaded, and an ESP32 DMA routine looks after sending these data to the I2S output port at the user-selected sample rate.

I chose a sample rate of 214,873Hz. This seems like an odd value, but it was a value greater than 200,000Hz that the I2S clock generator could produce exactly. The exact sample rate value isn’t critical; as long as the DDS routine knows exactly what it is, it can produce accurate sine waves at the selected frequency.

I decided to use the I2S port for the sine wave generator for a couple of reasons:


Advertise Here

1) I could have used a 16-bit SPI DAC, but they are expensive, and I was already using the ESP32’s SPI port for the TFT touchscreen display. It would have been difficult or impossible to maintain the DDS output stream at a constant sample rate, while also handling the TFT screen-refresh data stream. Actually, the ESP32 contains two available SPI ports, but I needed the second SPI port’s GPIO pins for other purposes.
2) The Princeton Technology PT8211 stereo I2S DAC is inexpensive. Note that while these are available for almost nothing in China, the only place I know of to buy them in North America, in unit quantities, is

The PT8211 I2S DAC is stereo. I was only planning on providing a single sine wave output, so one of the DAC outputs could have been left unused. However, the nature of the I2S protocol requires that you send out both channels, whether or not you are using both channels.

Figure 3 is a block diagram of the sine wave generator. I decided to send the sine wave value to the left DAC, and a 180 degrees out-of-phase signal to the right DAC—basically giving me a differential signal with an output amplitude that was twice that of the PT8211’s standard 1.0V full-scale output. The main reason I did this was that the PT8211’s zero output rests at VCC/2, and the 1.0V full-scale output range is centered on this 1.65V level. By using both DAC channels in a differential mode, this 1.65V common-mode voltage is eliminated. There is a schematic for the system too, but it’s not printed in this article. Go to Circuit Cellar’s article code and files webpage to download it.

FIGURE 3 – The block diagram of the Audio Sine Wave Generator. The wide range of amplitudes is handled by the MCP42010 digital potentiometer and the two gain ranges provided by the INA128’s switchable gain setting.

The two PT8211 outputs are fed to a Microchip MCP42010 dual 8-bit digital potentiometer. This allows the 2.0VP-P sine wave to be attenuated by various factors, according to the user-selected output voltage amplitude setting. The two (possibly) attenuated signals are then fed into a Texas Instruments (TI) INA128 instrumentation amplifier. This amplifier’s gain is set by a single 12.5kΩ resistor (R1 in the schematic), and is equal to the following:

When R1 is in circuit, the gain is 1 + 50kΩ/12.5kΩ or 5. Relay K1 switches R1 in and out of circuit. When it is out of circuit, the INA128’s gain is reduced to 1. Therefore, the INA128 provides both a low- and a high-amplitude range, with a ratio of 5:1. Combined with the MCP42010 digital potentiometer, a wide range of sine wave amplitudes can be generated.

I also wanted the ability to offset the sine wave signal from its nominal zero-volt reference. This was easily accomplished by feeding a variable offset voltage into the INA128 amplifier’s “ref” pin. To produce the variable offset voltage, I used the 8-bit DAC1 of the ESP32, which produces an output voltage from zero to VCC (3.3V). To allow for both positive and negative offset voltages, the DAC0 output is fed to section 1 of a TI NE5532 op amp and mixed with a fixed negative voltage derived from D1, a TI LM385Z-2.5 2.5V reference chip. Pot R15 is adjusted at calibration time to produce a zero offset when the ESP32 DAC0 is set to one half of full scale. The sine wave offset voltage can basically be set anywhere within the ±9V range of the INA128 instrumentation amplifier.

The sine wave generator is the only function that requires ongoing service by the ESP32. That is, the DDS function requires a 512-sample buffer (256 samples × 2 channels) to be filled with sine wave values every 1.2ms. The DDS routine takes only about 53µs to accomplish this. The rest of the 1.2ms time is spent checking the rotary encoder for movement, and checking to see if any buttons have been pressed on the TFT touchscreen. The rotary encoder provides a smooth adjustment of either amplitude or offset, depending on which button is pressed on the touchscreen.

While not a signal in itself, I added an accurate voltage reference source to the project. A 12-bit TI DAC7611 is connected to the ESP32 via the SPI bus. It provides a voltage output of 0 to 4.096V. Using the remaining section of the NE5532 op-amp (U7), this variable voltage is mixed with a fixed negative voltage from D1, the minus 2.5V reference device. Pot R1 is set at calibration time to produce 0V output when the DAC7611 is at half scale. Overall, the voltage reference circuit provides ±4.00V output at a resolution of 12 bits. Now let’s look at the various digital signal generators.


The clock generator is made up of two sections:

1) A low-range generator capable of clock frequencies from 7Hz to 3MHz
2) An RF clock generator covering a range of 8kHz up to 125MHz

The low-range generator is handled using the ESP32’s clock generator dedicated to its I2S function block. In the I2S protocol, there is a bus clock signal (BCLK) and an LRCLK signal (which defines which of the two channels is being sent at the time). The BCLK signal is much faster than the LRCLK (32:1 for 16-bit samples × 2 channels). For the low-range generator to handle such a wide range, I needed to use the LRCLK signal for the lower frequencies, and the BCLK for the frequencies up to 3MHz. However, the I2S pins can be assigned to different GPIO lines, so I used two different I2S configuration structures to map either the BCLK or the LRCLK signal to the IO26 output pin, depending on the frequency called for. The code implementing this is shown in Listing 1.

LISTING 1 - The I2S pins can be assigned to different GPIO lines, so I used two different I2S configuration structures to map either the BCLK or the LRCLK signal to the IO26 output pin, depending on the frequency. The code implementing this is shown here.

i2s_pin_config_t pin_config1 = { // puts BCLK on IO26 (for high frequencies)
     .bck_io_num = 26, //this is BCK pin
     .ws_io_num = 32, // this is LRCK pin
     .data_out_num = 27, // this is DATA output pin
     .data_in_num = -1 //Not used

i2s_pin_config_t pin_config2 = { // puts LRCLK on IO26 (for low frequencies)
     .bck_io_num = 32, //this is BCK pin
     .ws_io_num = 26, // this is LRCK pin
     .data_out_num = 27, // this is DATA output pin
     .data_in_num = -1 //Not used

The clock source for the I2S function block can be specified in software. I chose the dedicated audio PLL to generate this clock signal. It has a range of 350MHz-500MHz, and has a variable modulo counter in its feedback loop. The PLL output is followed by a fractional-modulus divider. The combination of these two circuits provides an extremely high-resolution clock signal. This is required if one is using the I2S block at industry standard sample rates (such as 22,500, 44,100, 48,000,96,000, 192,000 samples/s). For the low-range clock generator, this provides a very high resolution.

While not mentioned above, an additional clock signal is a part of the I2S protocol. This is the master clock signal (MCLK) and is used by many I2S Codec chips. The MCLK signal is generally 256× the desired BCLK signal. So, for a 44,100Hz sample rate, the MCLK signal would be 44,100 × 256 or 11,289,600Hz. (You see why a high-resolution clock source is needed from the odd frequency just mentioned.)

The ESP32 can provide this MCLK signal, but due to the high frequencies involved, the MCLK signal cannot be assigned to many different GPIO lines via the internal pin MUX circuit. It can only be switched among three GPIO pins, and the only one of the three not already assigned to the USB serial UART port is IO00. I was using all the ESP32 GPIO pins for the various functions, and IO00 was not available. Therefore, I couldn’t extend the low-range clock generator’s frequency range beyond 3MHz by using the I2S MCLK signal.

Instead, I incorporated a Silicon Labs Si5351 into the design. These ubiquitous clock generator chips only cost a few dollars and can provide three discrete RF clock signals in the 2.5kHz to 200MHz range (with limitations). Since the Si5351 comes in a tiny MSOP-10 package, I used a tiny Adafruit Si5351 breakout module shown in Figure 4.


Advertise Here

FIGURE 4 – The Adafruit breakout board for the tiny Si5351A Clock Generator chip

The Si5351 is interfaced to the ESP32 via its I2C port and is the only device on that bus. I won’t discuss the internal operation of the Si5351 because it’s complex and requires many registers to be set to provide an output frequency that can be specified to a sub-Hertz resolution. For my purposes, I used a simplified algorithm to produce frequencies in the 0.1 to 100MHz range to a resolution of 1kHz. You can find the frequency-setting algorithm in my ESP32 program named setSI5351Freq routine (line 766). If you are interested in using the Si5351 at its full-frequency resolution, check out the source code for the Si5351A Synthesizer by QRP-Labs (see RESOURCES at the end of the article).

All the digital clock/signal sources in this project are made available via a single 9-pin D connector. Due to the higher RF frequencies involved with the Si5351’s output signal, it is sent to a BNC connector. The analog sine wave and voltage reference signals are also sent to BNC connectors.


The pulse generator can provide three discrete pulses. All three pulses share a common repetition frequency, which is adjustable and will be described later. The time delay between the second and third pulses—both with respect to pulse #1—is also adjustable.

The pulse generator function is handled by three sections of the LED PWM block within the ESP32. Each section consists of the blocks shown in Figure 5. The ESP32 contains eight separate, high-speed PWM blocks and eight low-speed blocks. I’m not sure how useful the 16 PWM sections are in the ESP32 (as packaged now), because there aren’t enough free GPIO pins to handle all 16 PWM signals. I am only using three of the high-speed blocks for the pulse generator function.

FIGURE 5 – The internal configuration of the LED controller block in the ESP32. The high resolution available for the Pulse Generator’s period is due to the 18-bit fractional divider used after the 80MHz clock.

Two parameters are related—the pulse frequency and the number of bits of PWM resolution. The higher the frequency you specify, the lower the resolution you will be able to choose. The high-speed clock rate is 80MHz, so the highest pulse repetition rate is 40MHz, with a PWM resolution of 1 bit—essentially a 40MHz square wave. The relationship is as follows:

Once you have chosen a frequency and determined a suitable bit-resolution, you then define both the width and the delay parameters in terms of this bit resolution. For example, choosing a repetition frequency of 10kHz, you could choose up to 12-bit resolution, per equation (2) above. 12-bit resolution is 4,096. Therefore, for example, if you chose a width of 16 counts, that would correspond to a width of:

It follows that each count = 0.39µs/16 or 0.0244µs granularity. The delay calculations would be the same. You would enter the number of counts equal to the desired delay interval. Three such LED PWM blocks are used for the pulse generator, and all three pulses are synchronized with each other by configuration commands in the software.

I should mention how the selected frequency is generated. It is not merely a standard integer-divider chain following the 80MHz clock. Instead it is a fractional divider, with a division ratio made up of a 10-bit integer value, “A,” plus an 8-bit fractional portion, “B.” So, for example, if you wanted to divide the 80MHz clock by 61.25, you would use a value of 61 for “A” and 64 for “B” (64/256 = 0.25). This fractional divider allows the user to choose the repetition rate with reasonably high accuracy. The “actual” repetition rate is displayed on the TFT display after you have entered all the desired parameters, and it may differ slightly from what you have specified.

The three pulses are mapped to GPIO pins IO12, 13 and 14. Because the ESP32 runs on 3.3V, these pulses are present at 3.3V logic levels. I wanted to buffer these pulses before they left the unit, to protect the ESP32’s GPIO pins from getting burnt out if there were shorts or connection errors made with the external device being tested. I chose TI SN74LV125 quad tri-state buffers for this. By switching the VCC of the SN74LV125 between 3.3V and 5V, I was able to get both 3.3V and 5V logic level outputs from the pulse generator. I still use some 5V logic parts, so this was a useful option. Figure 6 is a block diagram of the Pulse Generator.

FIGURE 6 – The Pulse Generator’s three pulses are fed to a 74LV125 buffer chip powered by either 5V or 3.3V, giving two different logic-level outputs.

The LED PWM block in the ESP32 is somewhat more versatile than what I have just described. It contains logic that allows you to program-in a ramp for the PWM signal. That is, it can be made to ramp the PWM value, from a starting value to an ending value, at a selectable rate (without any MCU intervention). This could be useful for implementing LED brightness fades. I did not need or implement this feature in the pulse generator.


The ESP32 contains two fully-featured 3-phase, motor-control PWM function blocks. The 3-phase PWM generators provide three main and three complementary PWM signals, and allow programmable dead-time intervals between the main and complementary signal pairs, as required in H-bridge applications. Three input capture blocks can be used, for example, to synchronize the PWMs with Hall-sensors mounted on the motor, itself. Finally, there are three fault inputs that can shut down or modify the PWM operation in response to over-current/stall conditions.

I’m not sure why Espressif included two motor-control PWM blocks, since it seems that there aren’t anywhere near enough GPIO pins to handle two 3-phase motor controllers, especially if you needed the input capture pins for motor position/speed feedback and/or the fault-sensing inputs. In any case, I decided to implement one generic 3-phase PWM generator with main and complementary outputs for each phase (Figure 7). The user can select the PWM frequency, the duty cycle and the dead time.

FIGURE 7 – All six PWM outputs from the ESP32 Motor Control PWM block are fed to 74LV125’s to provide both 3.3V and 5V logic-level outputs. The Low-Frequency Clock Generator uses a remaining section of the second 74LV125 to achieve the same two logic-levels.

As in my pulse generator function, the user-selected PWM frequency is derived from a high-frequency clock. In this case however, it is a 160MHz clock, and it feeds a simple 8-stage integer divider (not a fractional one as is found in the LCD PWM controller). The clock frequency for the MCPWM block cannot be specified with as high a resolution as the LED PWM block. This is offset, however, by the fact that the counter used by the PWM operator can have its terminal value set to any value, whereas the LED PWM operator could only be specified by the number of bits in the counter. Therefore, both the LED and motor control PWM blocks can have their PWM frequency set with a fine granularity.

The ESP32 MCPWM API contains a call where you provide the PWM frequency and PWM mode. The PWM mode can be count-up or count-up/down. The latter provides phase-correct PWM signals. The user just provides the desired frequency and PWM mode, and the API sets both the clock prescaler and the PWM terminal value, to produce a frequency close to what was selected. Similarly, there is an API call to set the duty cycle. Here, you just specify the percentage you want, and the API figures out the necessary count with which to load the PWM operator.

I struggled with the MCPWM’s dead-time function. Figure 8 shows a standard, active-high PWMa output, with its complement shown as PWMb. The two necessary dead times are specified by the values DTRED (dead time rising-edge delay) and DTFED (dead-time falling-edge delay).

FIGURE 8 – The dead-time waveforms for the 3-Phase PWM Generator

There is an ESP32 register with eight different modes, some of which specify various dead-time options, and two other registers, which specify the length of the rising/falling dead times in 100ns increments. It seemed to me that once you selected the dead-time amount, and picked the proper mode, that either (or both) of the complementary outputs would be lengthened/shortened to provide the two complementary signals, as shown in Figure 5. In other words, the switching points would be modified such that the two (complementary) outputs would never switch simultaneously, but would have a dead-time delay at both the rising and falling edges of the PWM signals.

I was not able to achieve this using the MCPWM APIs alone, regardless of what dead-time mode I selected. Instead, I could only get one-half of the necessary dead times directly, and had to lengthen the duty cycle of one of the two complementary signals (using the duty-cycle API) separately, to achieve the other required dead time. This “pseudo-dead time” was then present, but not with the same granularity that the “true” dead-time operator produced. It felt like I would “wear out” my ‘scope during all of the messing around I did trying to get this to work!

The 3-phase PWM generator produces six discrete signals. Three of these are mapped to the same GPIO pins used by the pulse generator section (IO12,13,14). The last three signals are mapped to IO15,16,17. Like the pulse generator section, all six signals are buffered by two 74LV125 buffers and are output at either 3.3V or 5V logic levels, depending on the position of switch S2. With so many digital signals available, I decided to use a 9-pin “D” socket for them, rather than the BNC sockets used for the analog output signals.


Most of the analog and mixed-signal circuitry was described in the previous sections covering each generator function. The full schematic of the unit is complex. As mentioned earlier, rather than duplicating it here, it is included along with the software on Circuit Cellar’s article code and files webpageFigure 9 shows the Vector board containing all the circuitry apart from the touchscreen and rotary encoder.

FIGURE 9 – The complete circuit built on a Vector 8001 protoboard. The small black block at the right is a RECOM 5V 3W power supply. The chip closest to it is the TI DC/DC convertor that provides ±12V.

The power supply for the project had to provide several discrete voltages. I wanted to mount the whole power supply on the Vector 8001 protoboard that I used for the rest of the project. I decided to use a small, 3W AC/ DC power module—the RAC03-05SK from RECOM, which provides 5V at 600mA. I power the ESP32 DevkitC module with this 5V supply. The DevkitC has a built-in LDO regulator to reduce this to the 3.3V needed by the ESP32 MCU. The DevkitC contains a pin providing a regulated 3.3V to external circuitry. Since the ConnectEVE TFT touchscreen display requires about 125mA at 3.3V, and there are other chips also using 3.3V, I decided to use a separate MCP1702T-3302 LDO regulator to supply 3.3V for everything apart from the ESP32 DevkitC module. More about the ConnectEVE touchscreen later.

The analog generators needed a bipolar power supply of at least ±10V. Rather than using another AC/DC power module fed from the 120VAC line, I used an older DC/ DC converter module I had on hand—the TI DCP010512DBP-U. This is very small—the size of a 14-pin DIP IC. It provides ±12V (unregulated) from an input source of 5V. Without a load, it produces more than ±20V, which is a bit beyond the specifications of the INA128 and NE5532 amplifiers.

I knew it would drop when the amplifiers were connected, but I didn’t want to take a chance exceeding their ratings. I like to have an LED on any board to indicate that power is present, so I used an LED on both the plus and minus 12V rails. I sized the current-limiting resistors such that the DCP010512DBP’s 20V unloaded outputs dropped below 18V—the limit of the INA128 and NE5532 amplifiers. With both amplifiers wired up, the unregulated ±12V power supplies run at around 12V, so I guess I was being overly cautious.

I use a single rotary encoder to adjust parameters that don’t readily lend themselves to the on-screen keypad entry method. I only use it for the Audio generator function—to adjust the amplitude and DC offset of the signal in a smooth fashion. Numeric keypad entry would not have been good for this purpose.


Espressif provides the Espressif IoT Development Framework, which it calls the ESP-IDF, for developing programs on the ESP32. It also provides the SDK bundle of utilities and low-level APIs. Although these tools are likely the most comprehensive, Espressif also provides an Arduino core for the ESP32. Because I am accustomed to using the Arduino IDE, I did my program development in the Arduino environment. Adding the ESP32 core to the Arduino IDE is most easily handled using Arduino’s board manager function. I provide a link to comprehensive instructions for doing this i RESOURCES below.

The touchscreen display that I used is the ConnectEVE 4.3” display made by Mikroelektronika. It contains FTDI’s FT800 touchscreen display controller chip. FTDI also sells similar display modules under its Bridgetek brand. The FT800 touchscreen controller is very powerful, but doesn’t work anything like any other TFT screen display controller you might have run into. For starters, it doesn’t require a RAM buffer in your MCU’s memory space to store all the pixel data being displayed. For earlier projects I had built, using 8-bit Microchip Technology AVR MCUs (with limited RAM memory), this was a big plus.

Instead, the FT800 controller accepts a series of high-level commands and associated parameters, and puts them in a circular buffer. When it receives what is essentially an “execute” command, it parses these commands and, on a line-by-line basis, generates the pixel data needed to draw the desired display pattern. An SPI interface is used between the MCU and the FT800.

I am using the default 8MHz SPI clock rate of the ESP32, but because only high-level commands need to be sent, and not a large, bitmap data stream, the screen update rate is perfectly adequate. While this project’s GUI is basic, I have done some reasonably complex GUI’s using the FT800, and its display response is fully sufficient (particularly when I have cranked-up the SPI clock rate to 24MHz when using the Teensy 3.6 module).

All the low-level details of handling a touchscreen display are performed transparently by the FT800. That is, whenever you specify a widget that you want displayed (button, slider and so on), you assign it a TAG number. Then, whenever that widget is touched (or a slider is moved), the FT800 will report which widget was touched, or the new value for the slider.

Handling the FT800 controller requires a complicated API, which FTDI provides in the form of a C++ class library. Because FTDI expected the FT800 to be used with numerous MCUs, they wrote this library in two layers: a hardware abstraction layer (HAL), and a layer that matches the HAL to the specific SPI port characteristics of the MCU being used. Using this library with the ESP32 in the Arduino environment was fairly simple. I only had to specify the IO pin numbers that I had connected to the SPI *CS line and the FT800 module’s *PD line. The Arduino compiler already knew what pins were used by the ESP32 for its primary SPI port, so the SCK, MISO and MOSI lines didn’t need to be defined.

Along with the project software posted on the Circuit Cellar article code and files webpage, I have included the seven files that make up the FTDI library. Unless you are using the FT800 with other programs, these files should be placed in the same folder as the project’s Arduino sketch.

Although Arduino libraries are available that contain a rotary encoder class, I decided to use a library that I wrote for my earlier article (“Exploring the ESP32’s Peripheral Blocks,” Circuit Cellar 332, March, 2018). This library utilizes the ESP32’s hardware counter block, configured to handle a quadrature encoder. It doesn’t require any MCU interrupts or ISRs to handle the encoder; you simply read an ESP32 counter register to see the current position of the encoder. I have included the ESP32encoder.h and .cpp files with the rest of the source code for the project. These two files must be placed in the same sketch folder as the project.

You can choose a few different flash programming options for the ESP32. Mostly they center around how much flash memory you want to devote to your program, and how much you want to allocate for either or both of the ESP32’s SPI file system and the over-the-air (OTA) re-programming utility. Neither of these applies in this program, so the default flash mapping option is fine, and needs no user intervention.


I’ve done several Wi-Fi-enabled projects using the ESP32, and played around a bit with its Bluetooth functionality. It felt a little strange using a Wi-Fi-enabled MCU in a project that had absolutely no need for wireless communications. However, you’re getting a 240MHz, 32-bit MCU, complete with a lot of useful peripheral blocks for about $10. The ESP32 firmware is readily developed and the device programmed from the Arduino IDE, which I find advantageous.

I am still surprised that Espressif included such a variety of internal peripheral functions, beyond its Wi-Fi capability. In addition to the ones that I used or described in this article, there are several other unique functions, such as the RMT block. This block contains eight channels, each of which can send or receive the unique data stream that is used by IR remote controls. There is also an Ethernet MAC controller, a Real-Time Clock module and two 16-bit pulse counters (one of which I used for the rotary encoder).

I find it curious that there are nowhere near enough GPIO pins on the ESP32 to handle even half of the available peripheral blocks. I also wonder how likely it is that one would design a 3-phase motor controller that is controlled over Wi-Fi, using the ESP32 chip alone. Don’t misunderstand me—I am pleased to be able to build Wi-Fi-enabled projects using a module that is easy to mount on a Vector board, easy to program, and costs only about $10 here in Canada! 


The Full Schematic and the Software for this project is available for download from the Circuit Cellar article code and files webpage.


Instructions for adding the ESP32 core to the Arduino IDE using the Arduino’s board manage function can be found here:

Silicon Labs Si5351A :

Si5351A    Source code for high-resolution Si5351 operation:
ConnectEVE FT800 display:

Texas Instruments INA128 Instrumentation amplifier:

PT8211 I2S DAC:

Burr-Brown Digital-to-Analog Converter DAC7611U

Texas Instruments DCP010512DBP-U DC/DC bipolar power module

Adafruit |
Espressif Systems |
Microchip Technology |
MikroElektronika |
NXP Semiconductors |
Princeton Technology |
Silicon Labs |
Texas Instruments |


Keep up-to-date with our FREE Weekly Newsletter!

Don't miss out on upcoming issues of Circuit Cellar.

Note: We’ve made the May 2020 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.

Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

Sponsor this Article
+ posts

Brian Millier runs Computer Interface Consultants. He was an instrumentation engineer in the Department of Chemistry at Dalhousie University (Halifax, NS, Canada) for 29 years.

Supporting Companies

Upcoming Events

Copyright © 2021 KCK Media Corp.

A Multipurpose Signal Generator

by Brian Millier time to read: 24 min