There are numerous reasons to add a second MCU to your design and numerous errors to avoid once you do. This article considers the arguments and options while pointing out the pitfalls along the way.
With the wide variety of processors available today, there are a great many reasons why you may find it to be a more cost-effective choice to add new features to an application by introducing a second microcontroller (MCU) or microprocessor (MPU) to your design rather than trying to perform every task on a single, higher performance processor:
UART, I2C, and SPI
- Cost: Migrating to a higher performance system MPU can have a higher cost differential than adding an 8- or 16-bit MCU to take on the added load.
- Power: Rather than expend the power to have the main application processor continuously turn itself on to perform a relatively simple operation such as monitoring a sensor bank or checking if a key has been pressed, an 8-bit processor can be introduced to perform these tasks. In essence, the 8-bit processor serves as a “power gate” by letting the main processor sleep and waking it only when an event occurs that requires its attention. This is especially effective with resistive and surface capacitive touchscreens.
- Determinism: User interface (UI) functions involving compute intensive tasks such as animation or audio can stress the real-time deterministic behavior of a system. Rather than risk the reliability of critical tasks such as motor control, a second MCU can assume all “real-time” UI responsibilities.
- Diversity: Many systems can benefit from a modular design. For example, a display module can be implemented with a different processor to provide additional graphics, or streaming video capabilities. By taking a modular approach, the main application code does not need to be changed to add new features to the system or to support different models.
- Simplicity: A product line may offer a wide variety of options. For example, a product may have four screens, three keyboards, and three connectivity options. When a single processor is used, the design team has to maintain, test, and verify a separate code version for each possible variant (4x3x3=36 distinct variants). When a secondary processor is used to implement a new function, maintenance is required only for each option (4+3+3=10). Note that with the first option, the entire system needs to be tested and verified with every variant, a complex and time-consuming process. When using the second option, the main application code only needs to be verified once. Each of the options can be verified separately and with significantly less complexity.
- Conservation of I/O: An MCU can appear to be several devices over an interface by responding to several addresses. For example, the main application processor could issue a command to turn on a fan which is intercepted by an MCU serving as a virtual hub. This is an effective way to conserve I/O on the main application processor since only one communication peripheral is required to read and control multiple devices.
- Certification: Changing the user interface or introducing a new interface like Wi-Fi to the main system requires the entire design to be recertified for some markets. Implementing these functions as an add-on module with a separate processor simplifies the recertification process, significantly accelerating both development and time-to-market.
There are many options available for interconnecting processors. The most basic interfaces used for this purpose are Universal Asynchronous Receiver-Transmitter (UART), Inter-Integrated Circuit (I2
C), and Serial Peripheral Interface (SPI). Today, many processors offer hardware and software support for one or more of these interfaces. For example, STMicroelectronics’ STM32 Cortex-M3 32-bit processors and 8-bit STM8L MCUs support all three. Alternatively, Cypress’ PSoC family of programmable system-on-chip processors have analog and digital blocks which can be configured to provide multiple instances of each of these interfaces, allowing the interface mix to be altered as required for different target markets. The question becomes how many interfaces you’re going to need.
A UART operates up to 250 kbps, compared to 100 to 400 kbps for I2
C. SPI, which supplies its own clock, can operate at speeds over 1 Mbps. A UART link can typically be implemented in software using an interrupt-based driver, so long as the driver doesn’t place too much load or latency on the processor based on the application’s needs. An I2
C master can often be implemented in software, but a slave needs to be implemented in hardware because of the data rates involved. SPI also needs to be hardware-based. Multi-master systems will need to be hardware-based as well.
Beyond data rate, each interface offers specific features that may make it more appropriate for a specific application. UART is extremely simple to implement and is full-duplex. I2
C is popular because it requires only two wires, can have a simple master, and offers a variety of advanced options including support for addressing, multiple masters, broadcasting, and clock stretching. Clock stretching, for example, allows an IC slave to hold the clock and halt the flow of data while it processes the data it has received, thus preventing potential overrun issues. In contrast, a SPI slave is at the mercy of the SPI master.
With all of the various options available for I2
C and SPI, many silicon vendors supply example code which you’ll need to use as a model for creating your own code. Fortunately drivers for these interfaces can be fairly simple – an I2
C driver can be 15 lines of code, allowing you to implement an interface in a short period of time. However, it is important that you determine which features you need to support, that you’ve implemented them correctly, and that the master and slave implement the same features.
For example, there are a number of parameters you’ll need to decide upon when creating an I2
C driver, such as 7- or 11-bit addressing, single or multi-master (with collision detection and recovery), data rate, whether to use ACK/NAK protocols, support for address holding, and a slew of other options. As a result, any two particular I2
C interfaces can be very different. For example, one common problem developers run into is mistaking the System Management Bus (SMBus) used in laptops as a simple I2
C bus. Even though the SMBus runs over an I2
C interface, it usually uses fixed voltages for high and low values whereas a generic I2
C interface uses threshold values which are typically based on a percentage of the MCU power supply, thus representing a potential discrepancy between the two which can create difficult-to-debug intermittent errors.
Note too, that UART, I2
C, and SPI are just physical interfaces. You’ll still need to supply a logical protocol that operates on top of the physical layer. If the interface is between two MCUs under your design, this protocol can be whatever you want it to be. If the MCU is talking to a device like a serial EEPROM, then the protocol is already defined for you. As a consequence, a driver for a master is likely to be more complex than a slave given the number of different devices and their protocols it may communicate with. In addition, if your master doesn’t support all of the protocols features a slave does (or, conversely, tries to use a feature a slave does not support), compatibility problems will eventually surface.
The simplicity of each of these interfaces is both its advantage and curse. You can implement intricate protocols, which makes the interface more efficient and powerful, but this can also make it more difficult to debug. The simplicity gives you all the rope you need to hang the system up if you aren’t careful.
For example, consider an application where the interface is controlling an analog multiplexer which needs to smoothly transition between two audio streams. Depending upon how the transition is implemented, there can be undesirable – and audible – side effects; i.e., switching from a soft to loud stream can result in a loud “Pop!” Additionally, command latency may affect the transition. Ideally, you’ll want to run the audio down, switch streams, and then run the audio back up. However, this may not always be convenient for your application. The same difficulties apply if you are working with multiple power supplies at the same time.
To facilitate initial driver design and verification, many vendors offer powerful development tools for implementing and verifying interface functionality. For example, Microchip offers the PICkit Serial Analyzer which connects to your system and to a PC which you use to configure the link as a master or slave with any of the standard interface options for UART, I2
C, and SPI. A classic problem in interface design is the need for a pre-existing master to design a slave or a pre-existing slave to design a master. The PICkit eliminates this problem by providing a reliable other half of the link to design against with visibility into what’s being sent over both sides of the link. The PICkit also has the capable of emulating a variety of I2
C peripherals like ADCs and fan controllers.
Many MCU vendors also offer a variety of full-featured development kits and libraries to facilitate the introduction of subsystems such as an LCD display, touchscreen, or other peripheral to a system. For example, TI offers the RDKIDML-35 Intelligent Display module reference design kit which makes it relatively easy to add a 3.5” QVGA touchscreen to a system connected over either an I2
C or SPI link. The kit also includes a software library with various GUI functions to speed user interface design.
Figure 1: PICKit™ Serial Analyzer.
Avoiding common pitfalls
In the end, the best way to learn to implement a UART, I2
C, or SPI link is to do it yourself. The following list of tips and tricks has been suggested by experts from a variety of MCU manufacturers to help you avoid some of the more common mistakes, misconceptions, and missteps developers often take when implementing their first processor-to-processor interface.
Introduce complexity in stages:
Do not try to implement the entire system from the start. Begin with the basic driver and verify its operation. Then add the protocol layer and verify again. Introduce the application layer. Finally, operate the entire system with all of its various concurrent tasks and interdependencies.
Design your system to abstract functionality:
For example, using a generic UI interface allows the main system to connect to mechanical buttons, a keypad, or a touchscreen without having to know which is actually connected. This approach also allows you to fundamentally modify how a function is performed without requiring any code changes to the main system. Consider a vending machine with a separate processor handling coin and bill collection. If a security problem arises (i.e., customers have figured out a way to cheat the machine), only the hardware and software used to monitor collection need to be redesigned to prevent this; the rest of the system remains unchanged.
Figure 2: RDK_IDM L35.
Recognize that peripherals are different than processors:
Connecting to a peripheral is different than connecting to another processor. When you control the software on both sides of the interface, you define the interface specifics and protocol on both sides and can therefore guarantee compatibility. When you only design one side of the interface, you need to design your interface to match the requirements of the peripheral to which you are connecting. This can give rise to many potential problems. The most common is the datasheet for the peripheral inaccurately describing how the interface has been implemented. To avoid this problem, verify how the interface has been implemented. Also be aware that various interface options such as 11-bit addressing, clock stretching, or multi-master support may not be supported by a peripheral and that if you try to implement a feature that the peripheral did not implement, the results will be unpredictable.
Specify a sufficient data rate:
Confirm that the data rate of the interface will meet both your current and future data needs. For example, a small display requires a lower data rate than a larger display or equivalent touchscreen. If you plan to support larger displays in next-generation designs, keep your interface options open.
Account for overruns: Getting an I2
C or SPI link up and going is fairly straightforward. Where you are likely to encounter difficulties is at the corner cases. For example, the interface may operate properly up to a certain frequency, at which point it begins to fail. The problem could either be from too much noise in the system or a data overrun. A data overrun may be a local problem; a series of a particular type of packet may require more processing time, resulting in the processor not emptying the buffer fast enough. More often, overruns are caused by system-level issues. For example, the DMA or FIFO used by the interface may be a shared resource. Depending upon what else is being processed by the system (i.e., the real-time operating system is managing several real-time tasks simultaneously), latencies may be greater than estimated, causing an overrun.
Verify the driver first:
When a problem arises, it can be difficult to determine the cause of the error. This is especially true in systems managing multiple interfaces or real-time tasks where errors can arise because of what else is happening in the system. Get the systems talking reliably first by verifying your drivers before introducing application-level functionality. This allows you to more confidently eliminate the driver as the source of the problem during later debugging.
Debug using code instrumentation:
Debugging both sides of an interface can be tricky because you are working with multiple processors. While there are tools for debugging multiprocessor systems, they often allow you to halt only one side of the link, making it difficult to utilize traditional debugging techniques. If you are using the same type of processor on both sides of the link, you have the option of having the one processor send and then receive its own signal; this allows you to watch both sides of the link and halt them simultaneously. If you are debugging over JTAG, you may be able to connect both processors to the same JTAG chain.
Once you mix architectures, however, you may find yourself forced to debug one side of the link at a time. To simplify debugging, limit the complexity of the other side of the link by creating a simple stub driver that allows you to test basic functionality in stages. As you begin to work with live systems on both sides of the link, you can use the debugger on one side of the link and use code instrumentation techniques on the other side of the link to facilitate debugging. The instrumented side of the link can use the link itself to send status information such as variable values, data received, and other important information back to the processor being debugged.
Exploit your debugger:
Some engineers try to solve programming issues without a debugger. When they encounter an error, they try something different in the code. Many interface issues, however, are caused at the system level and rewriting interface driver code is not the solution. Observing these system-level interactions may require a debugger.
Check your connections:
Triple-check your connections before debugging a problem. For example, an easy error to make is to hook up the control signals of the interface but fail to have a common ground. Do this and you may find yourself spending hours trying to resolve a non-existent problem.
Document your code:
Take the time to write clean code with complete comments. A simple I2
C driver might only run 15 lines and not seem to require documentation. However, if you are new to I2
C, you may have missed a critical parameter that you come to understand more about at a later time. Complete documentation gives you a reliable path for tracking your changes in thinking and not getting lost as the complexity of the application increases.
Question the spec sheet:
Because of its greater flexibility, developers typically encounter more issues with I2
C than with SPI. For example, the slave can adversely affect the master by holding the clock down, using a different addressing mode, or implementing start/stop sequences differently. Be wary even if you are connecting to a peripheral like a sensor with a detailed data sheet. The peripheral may not follow the spec, so don’t immediately assume your driver or the MCU is at fault if you can’t get an interface working. It is times like these that you may need a logic analyzer to locate the true source of the problem.
Have access to a logic analyzer:
This tool is especially useful when you have to connect to another device with an existing interface and protocol. It can also accelerate development when connecting to peripherals with a parallel interface like a display. Be sure to check if your logic analyzer supports the appropriate decode for the interface you are using. Automatic decoding can substantially simplify debugging by allowing you to observe the interface as a logical signal rather than having to convert the physical signal to a logical signal yourself.
Design for coexistence:
In a multi-master system, sometimes you will need to make sure to send data to multiple destinations without risking giving up the bus to another master. To achieve this, you can use a restart condition between each transmission to keep control of the bus. Similarly, you can use a restart to change the direction of data to receive data from a slave (i.e., EEPROM) without letting go of the bus. A master that locks the bus in this way, however, can create latency issues for other masters performing tasks that are also time-critical. Use such techniques sparingly and thoroughly test the interplay between masters to ensure that they operate well with each other.
Design slave/master-capable drivers:
Consider giving your slaves the ability to become a master. This can be useful in the case of system failure, allowing a slave to take over the bus and initiate recovery mechanisms with significantly less latency than if the slave has to wait until it is addressed before it can signal an alert.
Separate data processing from data transmission:
Often, the data collected (i.e., a voltage from a temperature sensor) is very different from the data that needs to be transferred (i.e., whether a temperature threshold has been crossed). Consider an interface to a touchscreen module where the main system, acting as the master, asks for the current finger position data. The module collects the raw data and stores it until position data is requested by the master. At this time, the module retrieves the raw data, processes it, and then transmits it.
This particular architecture can innocently create an overrun condition. Consider that to maintain responsiveness, the main system may ask for information at a higher frequency than at which the raw data actually changes. This means that with each request, the module has to recompute the position data using data-intensive algorithms, even if the data has not changed. Because the raw data is being computed multiple times, the result is tremendous overhead that can affect the responsiveness of the module.
There are several ways to eliminate such overhead. For example, a check could be made before the position data is computed as to whether the raw data has changed sufficiently or not. If the raw data has not changed, the previous calculation is used. Another approach would be to have the touchscreen module serve as the master over the link and send an update whenever the position has changed.
Keep the interface simple:
If you only have one master and one slave, there is no need to implement address collision. Of course, take into consideration future expansion and whether a deployed device may need to support such a feature to be compatible with future generations of devices.
With today’s integrated MCUs and MPUs, coupled with powerful development tools, designing your first processor-to-processor interface can be a simple process that allows you to quickly introduce a second processor to your existing design. By taking care in your design, you can avoid many common pitfalls to create a reliable and efficient interface that will meet the performance needs of your application today and into the future.