CC Blog Design Solutions Research & Design Hub

A Potentiostat for Performing Electrochemical Experiments

Figure 8 This is the display and the unit's circuit board. The Teensy 3.5's uSD socket has a flex extension cable plugged into it to extend the socket so it can be accessed from the outside of the unit's enclosure. The cable here is much longer than needed—about 18”—but my first choice of a shorter cable was defective when it arrived.
Written by Brian Millier

The powerful Teensy 3.5 microcontroller (MCU) module, combined with a Bridgetek-controlled thin-film transistor (TFT) touchscreen display and some analog components, makes for a low-cost Potentiostat instrument.

  • How can the Teensy 3.5 MCU be used in chemistry?
  • How can I build a potentiostat?
  • How are MCUs used in academic research??
  • T.I. OPA344P
  • Teensy 3.5 MCU

Over the years I’ve designed and built a number of different potentiostats to be used mainly for teaching labs, as well as a few specialized ones for researchers at the Dalhousie University Department of Chemistry where I worked. My job and interests were in electronics instrumentation, not chemistry, so my knowledge of electrochemistry lies more in its measurement than in the construction of electrochemical cells, or the ultimate goal of their measurement.

Basically, electrochemistry examines the relationships between electrical potential and chemical phenomena. This is done by placing specialized electrodes into a chemical solution and measuring the relationship between the applied voltage and the resulting current flow. Figure 1 is a drawing of such a cell, with the electrodes labeled. Note that the electrode naming convention wouldn’t be familiar to an electronics person, but this is how electrochemists label them. What I refer to in the article as “cell bias” goes to the working electrode, and the “current sensing” is connected to the counter electrode. I’ll explain the reference electrode later. The instrument that performs this measurement was called a voltammograph by the faculty members with whom I worked, but the term potentiostat is synonymous and more commonly used today.

Figure 1
This is a diagram of an electrochemical cell. The electrode naming convention is strange to an electronics person, and is described in the text.
Figure 1
This is a diagram of an electrochemical cell. The electrode naming convention is strange to an electronics person, and is described in the text.

A potentiostat is an instrument that provides a linear voltage ramp with a starting potential and a final potential. In some cases, it provides for a two-part ramp where the intersection of the two ramps is defined as the switching potential. The definition of these three voltages is a bit of puzzle for an electronics person like myself. When an electrochemist talks about a starting voltage of positive 1.0V, for example, the ramp voltage must be programmed to put out a negative 1.0V. The same applies for the switching and final voltage polarities. I take care of this inversion in my software.

The “ramp speed” is specified in volts (or millivolts) per second. A “step” value, in volts (or millivolts) is defined as the ∆V between each cell current measurement. Cell current can vary widely, so the current-measuring circuit must be capable of measuring currents less than 1µA and up to 10mA. The 10mA upper range would not apply to all electrochemical measurements, but it does handle all of the situations that one would run into in a teaching lab, which is generally where my instruments were used.

For some measurements, it is necessary to superimpose a square wave (of a user-selected amplitude value) upon the linear voltage ramp. The period of this square wave is fixed: the upper portion is applied for one current sample and the negative portion is applied for the next sample.

The ramp voltage is generated by a digital-to-analog converter (DAC). The time between when the DAC has been set to a new voltage and when the cell current is actually measured by the analog-to-digital converter (ADC) is called the “measurement delay.” This is chosen by the operator, and is measured in milliseconds. The execution of a full single-segment or two-segment ramp is called a “cycle.” The operator enters the number of cycles to be run and the current vs. voltage data is collected and stored in a file, separated out into cycles. The final parameter that can be set is the “cell relaxation” time (seconds). The potentiostat contains a relay which connects and disconnects the counter electrode from the potentiostat’s circuitry. The cell relaxation time is the interval between the connection of the counter electrode to the potentiostat’s current-sensing circuitry and when the voltage ramp actually begins.

In electrochemistry, the range of ramp voltages that are needed doesn’t exceed ±2V. Not being a chemist, I don’t know why this is, but it’s a nice coincidence. Since the total range is less than 5V, the analog circuitry required can be implemented with devices that work on a single 5V power source. This is the same voltage supplied by the USB specification and is also the voltage used to power the microcontroller (MCU) module that I use.


When I started building potentiostats for both teaching labs and researchers, good-quality op-amps were not available with single-supply, rail-to-rail specifications. At the time, good-quality op-amps were generally powered by ±12V power supplies. Even though the voltage range needed in electrochemistry was less than ±2V, in my earlier designs, I used quality op-amps, mentioned above, which operated on ±12V power supplies. A side effect of this was that several of the op-amps had to be able to dissipate a significant amount of power, due to the large differential between the power supply and the normal working range of potentiostat output voltages.

Today, with the advent of single-supply op-amps having rail-to-rail inputs and outputs, the design can be a lot simpler. Although the potentiostat must be capable of providing a bipolar output range, the electrochemical cell itself is not grounded. Therefore, the potentiostat can generate cell bias voltages, and measure the resulting current, using a “virtual” ground (common) with a potential of 2.5V. The resulting ±2V cell bias voltages will then fall into the 0.5V to 4.5V range. This allows for the use of a single 5V power supply for the potentiostat’s analog circuitry—assuming that one uses op-amps that are capable of rail-to-rail operation and have a 5V power source. This 5V power is readily supplied by a USB charger which connects to the micro-USB socket on the Teensy 3.5 MCU module which runs the potentiostat. The 5V USB power is pinned-out on the Teensy 3.5’s VIN pin. The Teensy 3.5 module contains a 3.3V LDO regulator which powers the MK64FX512VMD12 ARM MCU on that module, and this 3.3V is also used to power a few things on the external mixed-signal board.

With the power supply requirements out of the way, let’s look at the potentiostat’s analog circuitry. Please refer to Figure 2. To provide a voltage ramp in the ±2V range with a resolution of 1mV would require a 12-bit DAC (that is, a range of 0V to 4.096V in 1mV intervals). Because the voltage ramp might have a variable amplitude square wave added to the ramp value, under user control, I decided to instead use a 16-bit DAC—the Maxim MAX5541. This DAC can be powered by 5V and it requires an external reference. I use an LM385Z-2.5 2.50V reference device for this purpose. The DAC output, over its full 16-bit range, will be 0V to 2.50V. For convenience, I wanted the resolution of the DAC to be 10 counts per millivolt. As mentioned earlier, I am using a virtual ground set at 2.50V (also provided by the LM385Z-2.5 reference). I use an amplifying buffer (U5B) with an adjustable voltage gain, set by pot R5. During initial calibration, the MAX5541 DAC is sent a value of 25000, and R5 is adjusted to provide a 2.50V output at the U5B buffer’s output pin. Given that I’ve chosen a “virtual” ground of 2.50V, this 25000 reading corresponds to a “zero-volt“ cell bias voltage. Similarly, a DAC value of 5000 represents a DAC output of -2V, and a DAC value of 45000 represents a +2V DAC output. So, each DAC count represents 0.1mV. The virtual ground node is labeled Vzero in Figure 2.

Figure 2
This is the schematic diagram of the potentiostat. Power for the unit is obtained via the USB port on the Teensy using a USB 5V 1A charger.
Figure 2
This is the schematic diagram of the potentiostat. Power for the unit is obtained via the USB port on the Teensy using a USB 5V 1A charger.

The bias voltage is fed to the non-inverting input of U5A (a TLV2461 dual op-amp). This op-amp has rail-to-rail inputs and outputs. The TLV2461 couldn’t source/sink the 10mA currents needed for the potentiostat all the way out to each rail. But, the voltage range used by the potentiostat is only 0.5V to 4.5V, and this op-amp can provide the necessary current over that reduced voltage range, since it never gets within 0.5V of either rail.

As mentioned earlier in the article, the cell potential is not actually the voltage between the working electrode (bias voltage) and the counter electrode (current sense). Instead, there is a third electrode called the reference electrode, and the cell potential is the voltage between it and the counter electrode. To accomplish this, the inverting input of U5A is connected to the reference terminal, and this feedback loop adjusts the voltage going to the working electrode (cell bias) accordingly. The DAC, dual op-amp and software make up the ramp voltage generator (with provision for a square wave modulation of the ramp, in software, under user control).

As mentioned at the start of the article, how one specifies the cell bias voltage (connected to the working electrode) is different from how electronics people would think. In electrochemistry, when one specifies a cell bias voltage of +1V (for example), the actual voltage that the potentiostat must supply to the working electrode is -1V. Since that’s the way electrochemists express the cell bias voltage, my software inverts the polarity of the operator-selected ramp voltages before sending the applicable values to the DAC.

All that’s left is to provide circuitry to measure the cell current. This could be accomplished by placing a low-value shunt resistor in series with the cell’s counter electrode, to the “virtual ground” node and then measuring the voltage across it. This would result in a small discrepancy between the ramp generator’s output voltage and the true cell potential, due to the small voltage drop across the shunt resistor. Instead, a potentiostat generally incorporates a trans-impedance amplifier (TIA). A TIA converts a current into a voltage output without a shunt resistor, so there is virtually no error due to a voltage drop across it.

Figure 3
This is the basic trans-impedance amplifier (TIA) circuit that I use to measure cell current.
Figure 3
This is the basic trans-impedance amplifier (TIA) circuit that I use to measure cell current.

Figure 3 shows a basic TIA circuit. Note that the non-inverting op-amp input is connected to a virtual ground, which in this project is 2.50V. Due to the presence of negative feedback resistor RF, and the extremely high open-loop gain of the op-amp, the voltage between the inverting and non-inverting inputs will be virtually zero. Therefore, the current coming in will be measured with respect to the 2.5V virtual ground potential. Since the sum of the currents at the TIA’s inverting input node must be zero, the op-amp’s output voltage will equal:


I chose a T.I. OPA344P op-amp for the TIA. It’s a rail-to-rail op-amp that operates on 5V. The TIA’s output pin must be capable of sinking the 10mA maximum cell current that the instrument is designed to measure. The OPA344P is capable of ±15mA short-circuit output current, so it’s just powerful enough to handle the maximum current being sensed in this design.

To handle a wide range of currents from the low µA range to 10mA, two methods were used. For the TIA itself, I provide three different feedback resistors with ratios of 16 between them: 200Ω, 3,200Ω and 51,200Ω. The 74CBTLV3253 multiplexer switches have a resistance of about 5Ω, so the 3,200 and 51,200 values are trimmed slightly by pots R6,R7 to maintain 16:1 and 256:1 ratios. The 200Ω resistor’s slightly higher effective value is due to the multiplexer’s switch resistance.

The TIA voltage output is measured by a ADS1115 16-bit ADC. This ADC has an internal PGA providing six full-scale ranges—I use the 256, 512, 1,024 and 2,048 millivolt ranges in this project.

The combination of the above two scaling methods results in nine current ranges with full-scale values between 10µA and 10mA. With a 16-bit ADC, accurate measurements below 1µA are possible.

For each of the three TIA current ranges, there is a separate zero offset adjust using the second half of the 74CBTLV3253 multiplexer and pots R19 through R21. There is a software routine “offset_Adjust_Screen()” which I call during initial calibration. It switches among the three multiplexer settings and displays the raw ADC output. With nothing connected to the current sense pin, you adjust these pots, one per TIA range, to produce ADC outputs as close to zero as possible.

The ADS1115 is a real gem of an ADC. It is capable of 16-bit readings at sample rates up to 860sps. The PGA ranges all have exactly a 2.000 ratio between adjacent ranges, so software trimming and calibration is not required when using the various PGA ranges. It uses an I2C interface to the MCU, which is handy in this project as both the DAC and the touchscreen display use the SPI interface. I run the I2C interface at 400KHz. The ADS1115 chip is housed in a tiny 10-pin MSOP package which would be tough for me to mount. Luckily, Adafruit (and many other vendors) sell this chip on a small breakout board with a 0.1 pin header, costing virtually the same price as the chip itself.

Since the analog circuitry feeding the ADS1115 is capable of providing signals up to 5V, I use 5V to power the ADS1115 module. The I2C lines on the Teensy 3.5 operate at 3.3V, so I removed the two 10k I2C pull-up resistors on the Adafruit ADS1115 breakout board and added pull-up resistors to the 3.3V power supply on my main board (R12,R13).


I had a Matrix Orbital Eve3-50G display on hand which I had used for my two-part Circuit Cellar article “Building a Touchscreen Display Using a Bridgetek BT81x” (Circuit Cellar May 2022 issue #382 and July 2022 issue #384) that covered the Bridgetek BT81x series of touchscreen controllers. This display has a 5″ screen with 800 x 480 pixel resolution and a capacitive touchscreen. A Bridgetek BT815 controller chip handles the display and also provides an interface to the capacitive touch controller chip. All communication between the host MCU and the display occur over a high speed SPI bus. I use two separate SPI ports on the Teensy 3.5—one for the display and a secondary one for the MAX5541 DAC.

Note that in this article, references to the Bridgetek BT815 controller may involve references to FT81x as well as BT81x. The reason for this is that the FTDI company originally designed this line of display controllers and later sold or spun off the product line to Bridgetek. Therefore, even the latest available Bridgetek documentation and library/function names use a mix of the FT and BT naming conventions. This dual naming convention is carried through into this article.

Having spent a lot of time working with this display for my aforementioned articles, I decided to use this display for the potentiostat project. For a potentiostat, you need at least an 800 x 480 pixel display to properly graph the data. There are also many user-entered parameters which must be routinely entered or changed. I consider a 5” touch screen display the smallest practical display for this project. Having used resistive touch screens often in the past, I knew that the capacitive touch screen found on the Eve3-50G display was much more accurate and user-friendly.

The Matrix Orbital Eve3-50G display is meant to connect to the host MCU using an FFC-20P cable. These 0.5mm pitch flex cables/sockets don’t lend themselves to the Vector protoboards that I use to build projects. The Eve3-50G also contains a PCB footprint (on 0.1” centers) containing all the necessary signals (likely there for factory tests). I was able to solder a header for a 10-conductor ribbon cable to the Eve3-50G board, and connect it to my host board this way. However, while working with this display for the earlier Circuit Cellar articles, I ran into a strange anomaly. The Eve3-50G would work fine when connected to a Teensy 3.5 module, but wouldn’t work when I connected it to any other member of the Teensy family (Teensy 3.2, 3.6, 4.0, 4.1). All of these Teensy modules are pin-compatible and I tried all of them out using the same Vector protoboard, wiring and interconnect cable. I ruled out that the Teensy’s execution speed could be too high, since the Teensy 3.2 is a much slower 72MHz Arm Cortex M4. The Teensy 3.6 is identical in execution speed to the Teensy 3.5, and the Teensy 4.0 and 4.1 are much faster 600MHz Arm Cortex M6 MCUs. When I connected my logic analyser to the SPI port lines on the display, I could see no difference in the data being sent to the Eve3-50G display, regardless of what Teensy MCU I had connected. Neither could I see any meaningful differences in the SPI signals on my ‘scope either, using all four Teensy MCUs. In the end, I was never able to solve this issue, and I spent a lot of time at it.

While the Eve3-50G display module works well with the Teensy 3.5 used in this project, I would recommend using the newer Matrix Orbital Eve3x-50G module. I was in contact with Matrix Orbital for some technical information while writing the earlier articles. They sent me a sample of the newer Eve3x-50G module and it worked fine with all of the Teensy MCUs mentioned above. It also contains a separate 20-pin socket for a standard ribbon cable, in addition to the FFC socket.

The Eve3x-50G display comes with some added features:

  • Haptic Feedback—This is a tiny “shaker” motor which you can turn on/off to act as haptic feedback when you touch the screen, for example.
  • A low-pass filter, low-power amplifier and a small loudspeaker—The BT815 controller itself contains a basic sound synthesizer function. Its audio output is a Sigma-Delta signal with a carrier frequency of about 30MHz. The Eve3 display sends this raw sigma/delta signal to the FFC-20P socket, but only the Eve3x modules contain this added circuitry.

The Eve3x models contain a significantly brighter screen than the EVE3 models—1000 nit Sunlight readable versus 300-600 nit for the Eve3. This higher brightness requires a more powerful LED backlight. Therefore, the Eve3x displays require more power. Since I wanted to use the USB port on the Teensy 3.5 to supply the power for the whole project, I was limited to the 500mA that the USB specification calls for. I was able to keep the project’s entire current draw to roughly 500mA by dropping the Eve3x-50G LED backlight power to 50% using the following line of code:

wr8(REG_PWM_DUTY + RAM_REG, 64); // Backlight PWM duty 50 %

The REG_PWM_DUTY register expects values between 0 and 128 for duty cycles between 0 and 100%. The RAM_REG constant is the base address that must be added to all register address definitions to form the true memory address. Even at 50% backlight intensity, the display is perfectly readable indoors. Figure 4A and Figure 4B show the front and back of the Eve3x-50G display.

Although I covered the topic of BT815 graphics programming in depth in my earlier Circuit Cellar articles, there is one graphics feature that was a prerequisite for this project: fast real-time plotting of the data to an X-Y graph. The BT815 controllers use a display list architecture to render graphics to the screen. Using this method, and a collection of high-level graphics “widgets,” it’s possible to render sophisticated GUI screens. The BT815 also contains high-level hardware handling of touch events, which greatly simplifies the handling of touch events by the host MCU’s code.

The BT815 contains the equivalent of a “draw-line” command (VERTEX) which can be used to draw a graph box and place axes markers, etc. The same command could also be used to plot the data points on the graph—assuming that all of the data points were available in advance.

In a potentiostat, however, the cell voltage is ramped up/down over time, and the current is constantly monitored. The cell voltage is plotted on the X-axis and the cell current is on the Y-axis. For measurements involving fast ramps, one could just wait until the complete ramp up/down was finished and perform one plot of the complete dataset. However, collection runs with very slow ramps are also common, and the operator will want to see the partial data as it is being collected. That way, if there is something wrong in the electrochemical cell configuration, they won’t wait until the end of a possibly long cycle to see the problem.

In a conventional LED display controller, there would be a large block of RAM called the display buffer which contains all of the pixel data to be displayed. The display controller would constantly read sequentially through this buffer and transfer that data stream to the LED panel’s pixel elements. To plot a graph in real time, all one needs to do is to write to a specific location in that RAM array, to turn a specific display pixel on/off. For a plot that is evolving in real-time, at any time you are only changing the specific RAM locations corresponding to the last data point received—the rest of the RAM array remains unchanged. Other parts of that array which would remain static would include the graph box, axes and labels.

The BT815’s display list method is very different from the above. Standard high-level graphics widgets and primitives are used to render the screen, including the plotting of graphical data. Plots which evolve over time would normally require you to send a new display list to completely redraw the whole display screen, every time a new data point arrives. That is, clearing the screen, drawing a new graph box, inserting the axes and labels, and plotting the available data, point-by-point. The BT815 controller, coupled with a 16MHz SPI clock rate, is fast, but not nearly fast enough to perform all this at the fastest 50sps rate that I wanted to achieve in my design.

After several attempts at coding a routine to perform this real-time plotting, using the standard functions described in the BT815’s programming manual, I wasn’t able to achieve anywhere near the screen refresh rate that I wanted. Finally, I came up with a scheme that worked beautifully at the fastest data acquisition rates expected in the project.


While the BT815 uses a display list methodology, and a relatively small 8k FIFO buffer memory to store the display list, it also contains 1024k of general purpose RAM labeled RAM_G. This RAM memory is normally meant to hold the following:

  • Images—either un-compressed bitmaps or JPEG, PNG types
  • Fonts—over and above the ROM fonts contained in the BT815 (that is, larger or more specialized fonts)
  • Sound wave files—for use with the sound-synthesizer section of the BT815

In the case of images, these image arrays would normally be loaded into RAM_G from either the Host MCU (program flash or SD card) or from a Flash memory chip located on the Eve3-50G board itself (this is a purchase option that can add a 32 or 256 megabit Flash device to the board).

The “trick” that I eventually came up with involves setting up a display list which contains instructions and parameters to display a bitmap with dimensions equal to the X and Y sizes of the rectangle enclosing the plot. I then define a bitmap header which contains these X,Y dimensions, the type of bitmap, and a pointer to the position in RAM_G memory that contains the bitmap image. The type of bitmap is “L1”, which is black/white and maps eight pixels (horizontally) into 1 byte of memory. Prior to collecting any data, I issue a CMD_MEMZERO command with the proper parameters to zero out the area of RAM_G that is used for this bitmap. Thereafter, as data points arrive, this data is incrementally plotted by manipulating bytes in this bitmap array, as needed to form the required plot trace.

Also included in this display list are a series of high-level commands or widgets which:

  • Draw the rectangle defining the graph area on the screen
  • Draw axes ticks and descriptive labels
  • Insert Min, Max value labels for the axes
  • Provide an “Exit” button to halt the data collection
  • Provide any necessary status message(s)

At the start of the run, the display list described above is sent to the BT815 controller. Once sent, it will continuously execute that list, at the display’s refresh rate—giving what would normally be a static image.

What I discovered however, is that the host MCU can issue instructions to write data directly to the RAM_G area assigned to the bitmap. Whatever changes are made to this RAM_G memory are seen in the graph area of the screen, on the next screen refresh.


Advertise Here

Therefore, as data comes in, I calculate a line connecting the newest data point with the previous data point. I use Breshenham’s algorithm to break this line up into a number of X,Y points. I then do a calculation to map each point’s X,Y coordinate values into the proper bitmap memory location in RAM_G. Due to the nature of a potentiostat run, the X value of a data point will be incrementing/decrementing slowly as the ramp progresses. The Y value of the data point (cell-current) will also involve only gradual changes. Therefore, for each new data point, there will be only a short line segment needed, and very few RAM_G memory locations will need to be modified. Even if there were large value changes from data point to data point, transferring those new memory values to RAM_G is done very efficiently using the BT815’s memory read/write commands, coupled with the 16MHz SPI bus speed.

The routine that does the above is a routine labeled FT81x_DG(parameter list) and is located in my BT81x.cpp library file. The routine which takes two adjacent data points and converts them into points on the graph is called BresenhamLine(parameter list). The routine which maps a point’s X,Y coordinate into the bitmap RAM_G memory is putPixel(parameter list). Both of these routines are in the main “C” program file.

Listing 1 is the putPixel routine which maps the X,Y coordinates of a point on the graph into the bitmap array in RAM_G. It is quite simple since the L1 bitmap format maps eight horizontal pixels into 1 byte in memory. Also, I chose the graph’s X dimension to be 640, which is evenly divisible by eight. Since any given byte represents eight adjacent horizontal pixels, one has to read in the proper RAM_G memory byte (using the BT815’s rd8 routine), or in the new pixel bit, and then write it back to RAM_G (using wr8).

This is the snippet of code that converts a pixel with co-ordinates X,Y into a pointer to the BT815's RAM_G bit-map memory, and modifies that byte to light the pixel.

void putPixel(int x1, int y1) {
    int graphOffset;
    uint8_t rem;
    uint8_t l;
    uint8_t tmp;
    graphOffset = x1 >> 3; // div by 8 as 8 pixels per byte = number of bytes across the full width of graph
    rem = x1 % 8;
    l = 128 >> rem;
    graphOffset += y1 * GraphX>>3;
    tmp = rd8(graphOffset);
    l = l | tmp;
    wr8(graphOffset, l);

Using my scheme allows one to get all of the advantages of the BT815’s display list architecture and high-level widgets, while utilizing the much faster and more efficient graphics frame memory buffer concept for the plotting routine. I’ll also mention that the 8kb size of the BT815’s display list memory, while sufficient for lots of high-level graphics commands and widgets, is not big enough to allow you to plot more than a few hundred data points—using the BT815’s lower-level Vertex commands (the equivalent of draw-line commands).


There are quite a few parameters that are needed to define a potentiostat data collection run. The potentiostats that I built in the past all interfaced to a PC computer. I wrote a Visual Basic application which simplified entering these parameters using a GUI screen that contained text box fields for each parameter.

The 5” Eve3x-50G display has a high resolution of 800 x 480 pixels, and the capacitive touch screen is very accurate and sensitive to user touch. However, I had to separate the parameter entry process into two separate screens to allow the necessary controls to be large enough for easy manipulation. I broke the run configuration process up into two screens:

  • Voltage Screen (Figure 5). This includes sliders to select starting, switching and final voltages. If you don’t activate the Cyclic Mode button, you only have to specify start and final voltages, and the run will consist of just one cycle. Otherwise, you must also specify a switching value and the run will consist of multiple cycles (the number of cycles are set on a separate screen). You also specify the voltage step that you want between taking current readings. Finally, if you want to superimpose a square wave onto the voltage ramp, you can select the SQ Wave Mode button, and enter the square wave’s amplitude using a slider.
  • Current/Time Screen (Figure 6). This screen allows you to select the speed of the voltage ramp in mV/s. The measurement delay is the time in milliseconds between the setting of the DAC at a new voltage value and the triggering of the ADC to measure the cell current value. Cell relaxation time is the time between the closing of the TIA input relay (K1) and the start of the ramp. This happens at the beginning of each cycle of the run. The number of cycles is set on this screen using the +, – buttons. The full-scale current setting for the TIA/ADC combination is also selected on this screen and ranges between 0.01mA and 10.24mA in 9 separate ranges.
Figure 5
This is the screen which is used to enter all of the voltage-related parameters.
Figure 5
This is the screen which is used to enter all of the voltage-related parameters.
Figure 6
This is the screen which is used to enter all of the timing and cycle-related parameters. Because the time parameters can vary over a wide range, a pre-defined array of values is included in the program, and the + and – buttons cycle through these choices.
Figure 6
This is the screen which is used to enter all of the timing and cycle-related parameters. Because the time parameters can vary over a wide range, a pre-defined array of values is included in the program, and the + and – buttons cycle through these choices.

For some of the above settings, you are selecting from a limited, achievable list of values. For others, there are limits defined. However, there are combinations of parameters which could result in “impossible” run configurations. After using both screens to set the parameters, you must return to the Main Menu screen and hit the “Store Run Profile” button. This will result in a check of all the parameters as they interact with each other, and an error message will be issued if there are problems. Assuming things are OK, they will be stored as a data structure in the Teensy 3.5’s EEPROM memory. That is, they will remain in force even after the power is cycled off/on.

Figure 7 is a photo of the potentiostat screen after performing a run. Due to COVID-19, the teaching lab that I work with is closed, and I did not have access to a real electrochemical cell. So, I used a resistor to simulate the cell, for this run.

Figure 7
This is a photo of the display when it has completed a cycle. The faint outline of square blocks in the photo are not visible to the eye, in actual use.
Figure 7
This is a photo of the display when it has completed a cycle. The faint outline of square blocks in the photo are not visible to the eye, in actual use.

One valuable feature of the Bridgetek BT815 display controller is its high-level handling of touches and gestures on the thin-film transistor (TFT) display screen. With resistive touch screen controllers, you generally get an indication when the screen has been touched or released, and an X,Y coordinate where the touch occurred. You would have to interpret this coordinate to determine what button was pressed. Evaluating what the user has done with a slider, for example, is a more complicated software routine.

The Eve3x-50G display contains a capacitive touch screen, and a Goodix GT911 touchscreen controller. It’s not necessary for a programmer using a Eve3x-50G display to have any knowledge of the operation of the GT911. Rather, when the display is initialized (using the FT81x_Init routine), a block of data is loaded from the host MCU’s flash memory to the BT815 controller chip. This block of data is not described in Matrix Orbital’s documentation or as comments in the driver library they supply. Whatever it does, it allows the BT815 to communicate with the GT911 touch controller, and it transfers touch status and X,Y coordinates to the BT815’s touch screen registers.

There are two main ways to get information about whether and where the screen has been touched:

  • Low-level access to the touch screen registers—If you read the 32-bit register REG_CTOUCH_TOUCH_XY it will return the X coordinate of the touch in the MS 16-bits and the Y coordinate in the LS 16-bits. A 32-bit value of 0x80008000 is returned when the screen is not being touched.
  • Higher-level commands—When you place any widgets on the screen, you would normally use the TAG instruction ahead of the command that places that widget. A parameter of that TAG command is a single character/byte that you wish to associate with that widget.

For example, the following code will place an “exit” button on the screen and assign it a TAG value of ‘e’:

// assign TAG ID for following widget(s)

Cmd_Button(444, 366, 141, 59, 27, 0, “Exit”);

If you then read the 8-bit register REG_TOUCH_TAG it will return a value of 0x65 (ASCII value of ‘e’) whenever that button is touched. The valid TAG IDs are byte values from 1 to 255, and a zero is returned if the screen has not been touched.

Reading sliders is only a bit more complicated. For example, you can place a slider on the screen as follows:


Cmd_Slider(243, 164, 24, 265, 0, vf, 200);

Cmd_Track(243, 164, 24, 265, ‘F’);

This tags the slider with the TAG ID ‘F,’ sets its default value at vf and its range as 0-200. The Cmd_TRACK statement defines the area of the screen occupied by the Slider, tracks any touches in that area and assigns them to the TAG ‘F.’ To read the value of a slider at any time, the following statement is used:

uint32_t tracker = rd32(REG_TRACKER + RAM_REG);


Advertise Here

tracker = tracker >> 16;

The tracker value is a 16-bit value scaled such that the minimum slider value is 0 and the maximum slider value is 65,535 (0xFFFF). This returned value has the same range regardless of what you define to be the range in the Cmd_Slider command. Therefore you will have to scale this value to map it into the range that you want the slider to operate within. This is somewhat inconsistent, since you specify a “real world” range using a parameter of the slider command and assign it a default value within this range. However, subsequent monitoring of the slider returns a 16-bit value that must be scaled.


It’s not emphasized in the BT815 controller’s Programming Guide that you must calibrate the touch screen prior to using it. Fortunately, Bridgetek provides the Cmd_Calibrate() command which handles most of the process for you. This command places three target dots on the screen, one at a time, which you have to press. That results in the BT815 controller generating a six-member transform array containing the calibration constants. There is some additional code required to use this command, and this can be found in the MakeScreen_Calibrate() function, found in my BT81x.cpp file.

You could run this routine at every startup, but that would be inconvenient. Therefore, I save these six 32-bit values to the Teensy 3.5’s EEPROM memory. At startup, I call the loadTouchMatrix() function, which reads this EEPROM array. If the six 32-bit values are all 0xffffffff, that indicates that the EEPROM has not yet been written to, and I therefore call the MakeScreenCalibrate() function. Otherwise, I take these six values and transfer them to the BT815 at the memory location reserved for these values:


The Eve3x-50’s capacitive touch screen is very stable—once you have performed this calibration, you won’t have to do it again. This may not be true if you were operating the display over a wide temperature range, but I have not tried this. What I have tried, by accident, is swapping between the Eve3-50G and Eve3X-50G displays without remembering to do a re-calibration. In this case, the touch screen appears to be non-responsive, as the transform array values are definitely way off.

Here, I have covered a few relevant topics regarding BT815 controller programming. A trade-off for its powerful graphics capabilities is the fact that its architecture and command set are rather unusual. If you are considering using the Eve3 or Eve3x series of displays, I’d recommend that you refer to my two-part article in Circuit Cellar issues #382 and #384, as I previously mentioned.


All of my earlier potentiostat designs were interfaced to a PC computer via a serial port. Therefore the run data, which could be large if the cycle count was high, could simply be stored on the PC’s hard disk. For this stand-alone unit I had to provide some form of permanent/removable storage. The Teensy 3.5, 3.6 and 4.1 modules all contain a uSD socket which is connected to the MCU’s SDIO port. This provides much quicker access than you can achieve if you were to use the SPI port, which SD cards also support.

All of the Teensy modules are meant to be programmed in C++ using the Arduino IDE. There are a number of libraries available to handle SD cards. The early libraries were targeted for the 8-bit Atmel AVR MCUs that were mounted on Arduino boards. When the Arduino IDE was extended to work with 32-bit ARM MCUs, these libraries were ported to the more powerful ARM MCUs, with much the same functionality. That is, they only handled 8.3 DOS format file names, and did not work with newer high capacity (HC) SD cards.

Today there are more powerful SD card libraries available for the 32-bit ARM MCUs. I am using the SdFat library (written by Bill Greiman) which is now included with the latest version of Teensyduino (V1.56). This library works with the fast SDIO port that Teensy 3.5 uses for its SD card socket. It allows for long filenames—it’s not limited to the 8.3 DOS format. It handles FAT16, FAT32 and exFAT disk formatting.

Midway through the setup() function, I call the following function: SD.sdfs.begin(SdioConfig(FIFO_SDIO)).

This routine will return a false value if a formatted SD card is not detected. The display will show an “SD Card not inserted” message, and then go into an endless loop. Power must be cycled off and a card inserted, to proceed.

The run data that is collected is stored in the Teensy 3.5’s RAM memory until a cycle is complete. It is then sent to the SD card as a text file with a filename of Cyclexxx.txt, where xxx is a leading zero-padded cycle number. The data format is a simple comma-separated table:

Voltage (mV), current (mA)

I do not include the user-specified run parameters in this file.


Advertise Here

Since I am using the on-board uSD Card socket, it wasn’t possible to provide access to a uSD card from the external enclosure. To get around this issue, I bought a flexible uSD extension cable. Figure 8 shows the potentiostat circuit board with the flex extension cable attached to the Teensy 3.5’s uSD socket.

Figure 8
This is the display and the unit's circuit board. The Teensy 3.5's uSD socket has a flex extension cable plugged into it to extend the socket so it can be accessed from the outside of the unit's enclosure. The cable here is much longer than needed—about 18”—but my first choice of a shorter cable was defective when it arrived.
Figure 8
This is the display and the unit’s circuit board. The Teensy 3.5’s uSD socket has a flex extension cable plugged into it to extend the socket so it can be accessed from the outside of the unit’s enclosure. The cable here is much longer than needed—about 18”—but my first choice of a shorter cable was defective when it arrived.

The Eve3x-50G display is the first reasonably-priced TFT touch screen display that is usable for instruments that require a graphics display of data in any significant amount or resolution. Its capacitive touchscreen and 5” size also make it relatively easy to provide a GUI which allows for easy entry of parameters, and so on. Since I obtained the newer Eve3x-50G display after the project was substantially finished, I did not make use of the Eve3x-50G haptic feedback or its audio synthesizer/speaker. There are occasions, when using a touch screen display, where it would be better to use haptic feedback rather than waiting until the user stopped touching the screen, before proceeding. Alternately, a short “beep” from the speaker would help in this respect. 

Teensy 3.5 ARM MCU module:
Teensyduino add-in for Arduino IDE:
Matrix Orbital Eve3x-50G Touch Screen Display:
MAX5541 16-bit DAC:
Adafruit ADS1115 16-bit ADC breakout module:

Code and Supporting Files


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

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

Note: We’ve made the Dec 2022 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 © KCK Media Corp.
All Rights Reserved

Copyright © 2024 KCK Media Corp.

A Potentiostat for Performing Electrochemical Experiments

by Brian Millier time to read: 28 min