A microcontroller with restricted access to its interfaces or with limited resources pleads for support of external circuitry.
Most MCU chips you find today are characterised by the diversity of roles assigned to their interface pins realised by their internal multiplexing facility. Using a particular pin or set of pins for a specific role excludes the usability of any other role they are capable of doing. That is to say: roles are mutually exclusive on each interface pin.
For example, the ATmega328P1 MCU, the heart of the Arduino UNO2, has 14 digital input/output pins (PortD 0-7 and PortB 0-5), and 6 analog inputs (PortC 0-5). Six out of the digital IO pins can be configured as PWM, i.e. analog, outputs. Furthermore, 2 of those PWM pins have another role as part of the SPI interface, the MOSI and the SS pins.
The remaining 8 digital IO pins also have multiple roles. Two of them provide the USART serial TXD and RXD connectivity. Another 2 pins out of the 8 provide the MISO and SCK connectivity of SPI interface.
As for external interrupts, this MCU supports 3 of them, one assigned to the TXD pin, another one assigned to a PWM pin, and the third is assigned to a free digital IO pin. This leaves us 3 pure digital IO pins that do not interfere with other roles of the MCU.
As for the analog input pins, 2 of them provide I2C connectivity as a second role. Figure 1 illustrates the secondary assigned role of each interface pin of this MCU besides being a general purpose I/O (GPIO) digital or analog pin. Accordingly, an embedded system designer would have to face the consequences of role contingency in most MCUs.
From my experience as such, I find 3 alternatives to follow in order to resolve MCU interface conflicts which can eventually prohibit choosing a particular MCU that fails satisfying the application requirements.
The first is to alter MCU pin assignments defaulted by a peripheral library. Here, I am referring to Arduino IDE3 specifically. For example, if a peripheral requires a particular interface pin that is already configured for another role such as SPI, then declaring the peripheral's class instance in the sketch may be adjusted to avoid disrupting the SPI, otherwise the hardcoded defaults in the peripheral library may need to be altered.
On the other hand, if the MCU supports the assignment of SPI interface to user's chosen pins, then this would be another way out.
The second alternative is to use interface extension modules, such as I2C accessible digital GPIOs, or SPI and I2C expanders, or I2C based ADC or DAC. This alternative becomes handy when all or some of the original assigned pins for a role are deliberately configured for a different role in the user's application leaving not enough specialised interface pins for a role. Such extension modules provide the advantage of relying solely on I2C or SPI interfaces that are supposedly available, so they do not require additional interface pins for their operation. The downside of this alternative is the cost overhead of the modules and burden of coding them.
The final alternative is to provide circuitry for splitting a single interface pin into several external pins playing the same role. This requires the implementation of multiplexing external interface signals and the allocation of GPIO interface pins for controlling the switching mechanism between them. This alternative becomes handy, and even mandatory, whenever the previous two alternatives are not applicable.
In this article, I present three scenarios that demonstrate those alternatives; two of them mandate the adoption of external circuitry for driving specific types of peripherals. The challenging aspects in resolving the problematic scenarios are to keep the circuitry simple and cheap.
Exciting enough, I did use the IC74 series chips to do just that.
I present in the table shown here the list of such ICs I used to build what I called "auxiliary circuits" around Arduino UNO to resolve the upcoming contingency scenarios.
CHIP NO. |
FUNCTION |
MCU ROLE |
741484 |
8-line to 3-line priority encoder |
Linking an interrupt GPIO pin to 8 external prioritised channels in scenario #2. |
74045 |
Hex inverter |
Adjusting the inverted output code of the 74148. |
7440516 |
8-channel analog multiplexer / demultiplexer |
Linking an analog input pin to 8 external analog channels, and also linking a PWM output pin to 8 external PWM channels in scenario #2. |
742437 |
Quad bus transceiver with noninverted three-state outputs |
Improving the fan-out driving current of UNO's SPI interface in scenario #2. |
PCF85748 |
Remote 8-bit I/O expander for I2C bus |
Partially implemented in SimulIde as |
NE5559 |
Precision timer |
Partially implemented in |
AD722410 |
8-bit DAC with output amplifiers |
Used in conjunction with NE555 & PCF8574 in scenario #3 to generate PWM for extended GPIOs. |
The SimulIDE development tool
Even though the auxiliary circuitry I am building are based on basic digital and analog ICs, the reader can imagine the cumbersome of implementing them on breadboard for a proof of concept (POC). The practical and flexible alternative is to use a circuit simulator where alterations are easy and wiring is just a matter of dragging the cursors.
I chose SimulIDE11 for its good ranking and for its support for components used in my scenarios: the UNO, the IC74 family, and some basic I2C devices.
The main structure of the simulator window (see Figure 2a) is composed of 3 main areas: the component repository, the circuit canvas, and message area.
Building a circuit becomes a simple task, assuming some hands-on has been done.
Click on new circuit icon on the top toolbar to create an empty canvas, and then drag the required components into it. Some examples are shown below in Figure 2b.
Left-click the mouse (in Windows OS), on the pins of the components to be interconnected, and drag the cursor to the destination pins to stretch a wire, and then left-click the mouse again to finish the wire drawing.
Note: Modifying wires paths can be frustrating at the beginning of using the simulator, but practising will familiarise you with the best way to draw a circuit. I tend to remove wires, by right-clicking the mouse on them and left-clicking on the remove popup menu, and redraw them when wiring becomes cluttered and dragging them becomes cumbersome, as you can see in my simulator illustrations.
The simulator provides a bunch of debugging versatile tools: a voltage probe, an oscilloscope, a logic analyser, a set of meters, and a couple of signal generators.
I used most of them for the sake of illustration and for resolving weird behaviour of some components. I found out that some ICs require fixed voltage feed to floating input pins that in real life assume logic "1" unless grounded. The argument applies to inverted input pins, if they need to be deactivated. Some good features that simplify your circuit can be found in the properties of the components you may be using. For example, the LEDs can be set to "Grounded" instead of wiring them to the Ground component. You also find LED bar and resistor DIP components that simplify wiring to buses.
Some Arduino modules are supported and you can load realistic sketches into them and even launch the serial monitor just as if you were using Arduino IDE. The following screenshots illustrate the steps to follow in order to prepare the executable binary and load it into the MCU. You will use the Arduino IDE as usual for preparing the sketch, but rather than compiling and uploading it, you will only choose "Export Compiled Binary".
Open Arduino IDE and go to Tools in the drop-down menu.
Select Arduino Uno board.
Export compiled binary to the Build folder within the sketch folder. This generates a "build" folder containing the loadable binary, found in a hex file, and the Arduino IDE role ends here.
The following screenshots show the steps of uploading the compiled sketch into the MCU. Those steps are done within the simulator by right clicking on the MCU component and choose "Load Firmware" from the popup window. There you will find also the option of starting the "Serial Monitor".
Right-click within the UNO block (Found in canvas of Fig 7) and choose "Load Firmware" from the popup window.
In the Sketch folder, select the build folder.
Expand the arduino.avr.uno folder.
Find the uno-display-simple.ino.hex file.
Clicking on the red button at the top toolbar starts and stops the simulator. During a simulation run, you can start and stop controllable components such as switches, power supplies, and signal generators. By doing this, you can master the operation of your circuit and check it thoroughly.
Scenario 1:
UNO driving a parallel TFT display alongside an I2C peripheral
Configuration: UNO + Parallel TFT + I2C + UART enabled.
Penalty: No SPI, no external interrupts, only one PWM interface.
In this scenario, a parallel TFT display KS010812 is wired to the UNO.
The display has an interface of 14 pins (see Figure 5), 5 of which should be wired to the UNO's analog input pins
In this case, I applied the first alternative discussed earlier and changed the display pin assignment to GPIO2 (D2 in Figure 6) in that file. Another problem arises for pin RST of the display module
The adjusted simulator circuitry for this scenario is shown in Figure 7 in which the LCD library is modified to optimise UNO's pin allocation. Note that I inserted an I2C RAM just to mark the utilisation of analog pins A4 and A5 for I2C interface.
// all includes are in Glcd_v3 library
#include <glcd.h>
#include <fonts/allFonts.h>
#include <bitmaps/allBitmaps.h>
Code Snippet 1: Header files required for driving the KS0108 display.
Scenario 2:
UNO driving SPI and I2C peripherals alongside handling 8 external interrupts and PWM peripherals and analog inputs
Configuration: UNO + I2C + SPI + UART enabled.
Requirement: 8 external interrupt handling, driving 8 PWM peripherals intermittently, and analog input for 12 channels.
Imagine an UNO application in which we need SPI and I2C connectivity to some peripherals in addition to monitoring UNO's operation using the serial port of Arduino IDE. Add to this the need for 8 external interrupts triggering some actions in the UNO, as well as some extra analog input interfaces.
The solution I propose to these hyper requirements would be based on the 3rd alternative in which I use multiplexer and demultiplexer and encoder chips controlled by some UNO pins for addressing the external channels (see Figure 8).
For the PWM extension, I used demultiplexer for switching one PWM interface from the UNO along 8 external channels. I connected a DC motor to the 4th channel for illustration.
As for the additional analog input interfaces, I used a multiplexer connected to A0 pin of the UNO.
I used a signal generator and a potentiometer to feed 2 of the extension channels.
For the extended interrupt channels, I used a priority encoder that produces 3-bit code corresponding to the highest priority interrupt signal on the encoder's 8 input channels.
An aggregated interrupt signal is fed from the encoder to pin 2 of the UNO responsible for INT0 role.
Now to summarise the required pins of the UNO to drive the 3 chips, we need 3 selection output pins for the PWM and the analog extension, and 3 input pins for the encoded interrupts. Add to this a specialised pin on the UNO for each chip for delivering the extended signal type. Such a circuit requires a clever understanding of how the UNO works. It normally runs a single task at a time, so dedicating only 3 interface pins for the selection signals of both PWM and analog chips should work flawlessly. However, this technique would make the PWM extension momentary rather than continuous. If the UNO's application requires sustained PWM signal to peripherals, then scenario #3 should be the best solution.
Now to summarise the pin requirements for the 3 extension circuits, we shall need 6 selection/encoding pins and 3 specialised interface pins, totalling to 9 pins.
As depicted in Figure 8, UNO's pins 6, 7, 8 are used for PWM and analog channel selection, pins 3, 4, 5 are used for input encoded interrupts, pin 9 for PWM feed to extension circuit, pin A0 for input aggregated analog extension signal, and finally pin 2 for the aggregated extension interrupt.
// declare outputs of interrupt priority encoder
const int trigger_extInt = 2; // extended interrupt pin
const int encoded_intPin1 = 3; // encoded interrupt pin
const int encoded_intPin2 = 4; // encoded interrupt pin
const int encoded_intPin3 = 5; // encoded interrupt pin
// declare selection pins for both extended PWM and analog interfaces
const int sel_extPin1 = 6; // extender selection pin
const int sel_extPin2 = 7; // extender selection pin
const int sel_extPin3 = 8; // extender selection pin
const int aggr_pwmPin = 9; // extenderfed PWM pin from UNO
int aggr_analogPin = A0; // aggegated extended analog to UNO's A0
int val = 0; // variable to store the read aggregated analog
void setup() {
Serial.begin(9600);
Serial.println("Handling multiple interrupts!");
Serial.println("Extending IO analog pins!");
// initialize inputs to UNO from interrupt priority encoder outputs
pinMode(trigger_extInt, INPUT);
pinMode(encoded_intPin1, INPUT);
pinMode(encoded_intPin2, INPUT);
pinMode(encoded_intPin3, INPUT);
// initialize selection pins for extended PWM and analog interfaces
pinMode(sel_extPin1, OUTPUT);
pinMode(sel_extPin2, OUTPUT);
pinMode(sel_extPin3, OUTPUT);
// initialize extender fed PWM pin from UNO
pinMode(aggr_pwmPin, OUTPUT);
// Attach aggregated external interrupt to the ISR vector
attachInterrupt(0, pin_ISR, FALLING);
}
void loop() {
}
void pin_ISR() {
// get source external interrupt number (0 - 7)
digitalWrite(sel_extPin1, digitalRead(encoded_intPin1));
digitalWrite(sel_extPin2, digitalRead(encoded_intPin2));
digitalWrite(sel_extPin3, digitalRead(encoded_intPin3));
// print interrupt number
Serial.print(digitalRead(encoded_intPin1));
Serial.print(" ");
Serial.print(digitalRead(encoded_intPin2));
Serial.print(" ");
Serial.println(digitalRead(encoded_intPin3));
val = analogRead(aggr_analogPin); // read input potentiometer setting
Serial.print("Potentiometer setting: ");
Serial.print(val); Serial.println(" ohm");
Serial.print("Duty cycle: ");
Serial.print(val/10); // assuming 1 Kohm potentiometer
Serial.println(" %");
analogWrite(aggr_pwmPin, val/4); // send to DC motor a momentary PWM
// whose duty cycle is set by potentiometer
}
The code above illustrates how the 3 extension circuits are driven and made accessible. I introduced a sequence for demonstrating the mechanism of this scenario. I first enable the interrupt circuit, using the switchable voltage source, and toggle interrupt switch no. 4 (encoded 0B011). The interrupt service routine < pin_ISR()> passes the encoded interrupt to pins 6, 7, 8 thus enabling the 4th channel in both PWM and analog extension chips. The ISR guarantees that the reading of the potentiometer is read via pin A0 and its recorded voltage controls the PWM duty cycle of the PWM signal sent out to the DC motor. When running the simulator for this scenario, the reader will observe the continuous run of the motor, and it will react instantaneously to changes made to the potentiometer. Such sustained reaction can be maintained as long as no interrupts other than the one coming on extension channel 4 is triggered.
The code snippet shown involves optimising UNO pins allocations by sharing them for driving PWM and analog multiplexers' channels. Pin settings are derived from the encoded external interrupts. So, an external interrupt is mapped to a PWM channel and an analog channel at the same time.
Scenario 3:
UNO lacking PWM and digital GPIO interfaces
Configuration: UNO + Parallel TFT + analog inputs + I2C + UART enabled.
Requirement: Continuously drive external PWM peripherals.
In this scenario, I assume not enough or non PWM-capable interfaces of the UNO are available to continuously drive a DC motor, for instance.
The extender circuit presented in the previous scenario provided a momentary PWM driving since a change in the selected multiplexer output channel disables the motor's channel. The circuit I am presenting here does not suffer this flaw.
I assume that I2C is the only available interface of the UNO to generate the PWM output, thus I use I2C-to-Parallel chip in a pipeline which latches the byte sent over I2C to be converted to analog signal using a DAC chip.
The final stage in the pipeline is to generate the PWM signal from a timer chip tuned for either 490 or 980KHz, the UNO's standard PWM frequencies. The voltage out of the DAC controls the width, or duty cycle, of the timer output. I used 75 and 37KΩ resistors to adjust the timer for the relevant frequency. In order to land on those values, I temporarily used a potentiometer of 100KΩ in the simulated circuit to tune the timer.
Figure 10 illustrates the PWM pipeline at 490KHz and
Figure 11 illustrates the modification of the resistor for tuning PWM to 980KHz.
The simulator's oscilloscope became handy for this tuning task, and required some setup as shown in Figure 11 a & b for steady display of PWM waveform.
I also demonstrate the extension of digital GPIO interfaces by using another I2C-to-Parallel chip to latch the state of 8 extended interfaces and drive 8 LEDs for demonstration purposes.
The sketch loops around reading the potentiometer produced voltage on to pin A0 of the UNO. The read value is proportionally sent out to both I2C-to-Parallel chips, thus controlling the speed of the motor and lighting LEDs according to the decoded 8 bits sent to their driving chip.
Scenario #3 code snipped for driving a DC motor and 8 LEDs based on the setting of a potentiometer is shown here.
Conclusion
Three alternatives were discussed for optimising MCU interfaces to cope with user-selected peripherals. The alternatives range from simple, by just modifying the default peripheral assigned pins or fixing voltage for them.
A standard alternative was to use SPI or I2C based extension devices to free GPIO and analog pins for their specialised roles. The final alternative was to introduce basic digital and analog chips, such as multiplexers and encoders, to extend specialised interface pins of the MCU. Such an alternative comes to the rescue whenever SPI or I2C interfaces are forcibly unavailable.