Friday, March 2, 2018

Cheap and powerful 50 cents microcontroller | EFM8 Family

This is a step-by-step tutorial on how to setup and program an EFM8BB10F8G-A microcontroller or any other EFM8 Busy Bee microcontroller family from Silicon Labs. It is assumed that the reader knows the C programming language and basics about microcontrollers.
EFM8 Busy Bee microcontroller family from Silicon Labs
Why would you want to learn to use another microcontroller when there are many tutorials and libraries on popular MCU's such as AVR and PIC? Well, because they are much more cheaper but performant nonetheless - on Farnell you can buy them for less than 50 cents - and also are pre-programed with a bootloader making easy to program them using any USB to serial converter.  Compare that to Attiny13A which is around the same price but with half the pin count, less speed and peripherals. Just see the features bellow.

 EFM8BB1 Feature List:

Memory:
- EFM8BB10F8G-A
Up to 8 KB flash memory 
Up to 512 bytes RAM (including 256 bytes standard 8051 RAM and 256 bytes on-chip XRAM)  
- EFM8BB31F32I-B-QFP32
32KB flash memory and 2.25KB RAM

Clock Sources:
- EFM8BB10F8G-A
Internal 24.5 MHz oscillator with ±2% accuracy  
Internal 80 kHz low-frequency oscillator 
External CMOS clock option  
- EFM8BB31F32I-B-QFP32 
Internal 49MHz and 24.5MHz oscillators accurate to ±2%

Timers/Counters and PWM:
- EFM8BB10F8G-A
3-channel programmable counter array (PCA) supporting PWM, capture/compare, and frequency output modes  
4 x 16-bit general-purpose timers  
Independent watchdog timer, clocked from the low frequency oscillator  
- EFM8BB31F32I-B-QFP32 
6-channel programmable counter array supporting PWM, 6 x 16-bit general-purpose timers & independent watchdog timer

Communications and Digital Peripherals:
- EFM8BB10F8G-A
UART, SPI, SMBus/I2C, 16-bit CRC unit, supporting automatic CRC of flash at 256- byte boundaries  
- EFM8BB31F32I-B-QFP32 
2 x UART, SPI, SMBus/I2C and I2C high speed slave interfaces

Analog:
- EFM8BB10F8G-A
12-Bit Analog-to-Digital Converter (ADC)  
2 x Low-current analog comparators with adjustable reference 
On-chip Temperature sensor
- EFM8BB31F32I-B-QFP32
2 x 12bits DACs
12-Bit Analog-to-Digital Converter (ADC)
2 x Low-current analog comparators with adjustable reference
On-chip Temperature sensor

On-Chip, Non-Intrusive C2D Debugging:
- EFM8BB10F8G-A and EFM8BB31F32I-B-QFP32 
Full memory and register inspection
Four hardware breakpoints, single-stepping
EFM8BB1 SOIC16 Pinout
EFM8BB3 QFP32 pinout

Simple prototyping board for the EFM8BB1 microcontroller

First thing is to make a prototyping board for learning how to program the MCU and testing the code. Very few external components are required.
EFM8BB1 SOIC16 Breakout Board Schematic
EFM8BB10F8G-A-SOIC16 Breakout Board Schematic
C1 and C2 are decoupling capacitors across the power supply. D1 is a 3.3V Zener diode to protect the MCU in case a svoltage higher than 3.3V is applied. EFM8 microcontroller is a 3.3V device.
R3 is a 10k pull-up resistor to keep the reset pin at VCC. C3 is there to reset the MCU while programming. When the code is downloaded, the DTR pin will be pulled LOW making the reset pin to became low while the C3 capacitor is charging. The RESET pin must stay low at least 50us for the bootloader to start. After the capacitor C3 is charged the RESET pin will be at VCC level again. During this time the pin 7 (C2D) must be low either by holding down the S1 switch or by permanently shorting the JP3 jumper.
Note that if you are using the UART with a PC terminal and the jumper is shorted, the microcontroller will be reset when you plug in the USB and will stay in bootload mode; that is why the button is there. Must be a SMD button mounted on top.
LED0 and R1 is connected to pin 0 and it's used for debugging.

EFM8BB10F8G-A-SOIC16 Breakout Board PCB Top
EFM8BB10F8G-A-SOIC16 Breakout Board PCB Top

EFM8BB10F8G-A-SOIC16 Breakout Board PCB Bottom
EFM8BB10F8G-A-SOIC16 Breakout Board PCB Bottom
You can find here how to make a PCB yourself using toner transfer method
I used two receptacle headers and each has 8 pins. This way the board can be interfaced using jumper wires or some male headers can be connected and then connect them to a breadboard. The distance between the two headers must be multiple of 2.54 mm to fit on a breadboard.

Programming the EFM8 microcontroller

You have two options for programming - using a dedicated programmer from Silicon Labs or an USB to UART converter. The first option is better because of the debugging feature. I'm using the second option. Check this link if you want a tutorial on how to build one yourself (Build a USB to serial adapter).


Installing Simplicity Studio IDE Software

Silicon Labs have a great development environment for writing firmware for their microcontrollers - meet Simplicity Studio. This is equivalent to Atmel Studio, for AVR guys. Download Simplicity Studio. During the download you will need to create a free acount on Silicon Labs.

If you are new to this it could take some time to setup the software and familiarize yourself with it, but you will get to appreciate it after some time.
The nice thing about this software is that it has a configurator that helps you to setup your MCU by generating the code automatically. Even if I prefer to write my code from scratch, I still use the configurator to copy snippets of code in another project.

Simplicity Studio Configuration Interface
Simplicity Studio Configuration Interface
After installing you need to register the Keil compiler and you will get a free license.

Video on how to install Simplicity Studio IDE, installing a device, license the Keil compiler, creating a project and linking a external library in a Simplicity Studio project



To test that everything works fine, use the bellow code that will blink an LED connected on port 0 pin/bit 0 (P0_B0 ). I keep this code in a file as a template for future projects.
delay.h is header file that includes two functions that generate a time delay and can be downloaded from here. The file needs to be included in the project as shown in the video.

//-----------------------------------------------------------------------------
// Defines
//-----------------------------------------------------------------------------
#define F_CPU       24500000 // CPU FREQUENCY IN Hz

//-----------------------------------------------------------------------------
// Includes
//-----------------------------------------------------------------------------
#include <SI_EFM8BB1_Register_Enums.h>  // SFR declarations
#include "delay.h"

//-----------------------------------------------------------------------------
// Global CONSTANTS
//-----------------------------------------------------------------------------
SI_SBIT(LED, SFR_P0, 0);   // LED on Port 0: Bit 0

//-----------------------------------------------------------------------------
// Global VARIABLES
//-----------------------------------------------------------------------------


//-----------------------------------------------------------------------------
// Function PROTOTYPES
//-----------------------------------------------------------------------------
void Port_Init(void);

//-----------------------------------------------------------------------------
// SiLabs_Startup() Routine
// ----------------------------------------------------------------------------
// This function is called immediately after reset, before the initialization
// code is run in SILABS_STARTUP.A51 (which runs before main() ). This is a
// useful place to disable the watchdog timer, which is enable by default
// and may trigger before main() in some instances.
//-----------------------------------------------------------------------------
void SiLabs_Startup (void){
 // Disable Watchdog with key sequence
 WDTCN = 0xDE; // First key
 WDTCN = 0xAD; // Second key
}

//-----------------------------------------------------------------------------
// main() Routine
// ----------------------------------------------------------------------------
// Note: the software watchdog timer is not disabled by default in this
// example, so a long-running program will reset periodically unless
// the timer is disabled or your program periodically writes to it.
//
// Review the "Watchdog Timer" section under the part family's datasheet
// for details. To find the datasheet, select your part in the
// Simplicity Launcher and click on "Data Sheet".
//-----------------------------------------------------------------------------
int main(void){
  Port_Init();

  while(1){
   LED = 1;
   _delay_ms(500);
   LED = 0;
   _delay_ms(500);
  }
}


void Port_Init (void){
 // -------------------------------------------------------------
 // 1. Select the input mode (analog or digital) for all port pins
 // using the Port Input Mode register (PnMDIN)
 // -------------------------------------------------------------

 // --- Configure a pin as a digital input
 // -------------------------------------------------------------
 // 1.1 Set the bit associated with the pin in the PnMDIN register to 1.
 // This selects digital mode for the pin.
 //P0MDIN |= (1<<0);

 // 2.1 Clear the bit associated with the pin in the PnMDOUT register to 0.
 // This configures the pin as open-drain.
 //P0MDOUT &= ~(1<<0);

 // 3.1 Set the bit associated with the pin in the Pn register to 1.
 // This tells the output driver to “drive” logic high. Because the pin is
 // configured as open-drain, the high-side driver is disabled,
 // and the pin may be used as an input.
 //P0 |= (1<<0);


 // --- To configure a pin as a digital, push-pull output:
 // -------------------------------------------------------------
 // 1.1 Set the bit associated with the pin in the PnMDIN register to 1.
 // This selects digital mode for the pin.
 P0MDIN |= (1<<0);


 // 2.1 Set the bit associated with the pin in the PnMDOUT register to 1.
 // This configures the pin as push-pull.
 P0MDOUT |= (1<<0);


 // --- To configure a pin as analog, the following steps should be taken:
 // -------------------------------------------------------------
 // 1.1 Clear the bit associated with the pin in the PnMDIN register to 0.
 // This selects analog mode for the pin.
 //P0MDIN &= ~(1<<0);

 // 2.1 Set the bit associated with the pin in the Pn register to 1.
 //P0 |= (1<<0);

 // 3.1 Skip the bit associated with the pin in the PnSKIP register
 // to ensure the crossbar does not attempt to assign a function to the pin.
 //P0SKIP = P0SKIP_B0__SKIPPED;

 // -------------------------------------------------------------
 // 2. Select any pins to be skipped by the I/O crossbar
 // using the Port Skip registers (PnSKIP)
 // -------------------------------------------------------------


 // -------------------------------------------------------------
 // 3. Assign port pins to desired peripherals
 // -------------------------------------------------------------

 // -------------------------------------------------------------
 // 4. Enable crossbar and weak pull-ups
 // -------------------------------------------------------------
 XBR2 |= 0x40;

 // SYSCLK - CPU speed
 CLKSEL = CLKSEL_CLKSL__HFOSC | CLKSEL_CLKDIV__SYSCLK_DIV_1;
}

If everything compiled correctly, now it's time to download the code to the microcontroller.

If you use a programmer from Silicon Labs then you can compile and upload the code directly form the IDE interface. But when using a USB to serial converter this can't be done from Simplicity Studio. But have no fear, a workaround is here.



Uploading code to EFM8BB1 without Simplicity Studio

You need two files for this: hex2boot.exe and efm8load.exe They are provided by Silicon Labs at this link Bootloader Software. Click on AN945: EFM8 Factory Bootloader User Guide Software and download the zip file. In the zip file go to Tools > Windows and copy the two executables to the folder where your project files are located. Or download them at the end of this page.

To find where the project folder is located right click on Release or Build folder inside Project Explorer (see image below) and then Browse Files Here.

Simplicity Studio IDE interface

If you are interested on how to use this files with a dedicated programmer or the list of commands available read AN945: EFM8 Factory Bootloader User Guide.
These two executables are meant to be used by passing arguments to them. So after opening the Release folder and copying the two executables, hold the Shift key and right click on an empty spot inside Windows Explorer and then click Open command window here. Make sure no file is selected during this.

Hex2Boot — Hex to Bootload Record Converter:
The hex2boot tool converts a standard Intel hex file that is output upon a successful build by 8051 build tools like Keil to a bootload record. The bootload record is a pure binary file composed of the bootloader commands.
In the Command window you first enter the name of the executable hex2boot.exe then the name of the hex file generated using Simplicity Studio, in my case the project name is EFM8BB1-prototyping. After the -o command is the output file name that can be anything you want with efm8 extension. This efm8 file will be used by the efm8load.exe to upload it to microcontroller.
hex2boot.exe EFM8BB1-prototyping.hex -o EFM8BB10F8G-A-SOIC16.efm8
EFM8Load — Bootload Record Downloader:
This file is used to download the previously generated bootload record to the microcontroller. Like before efm8load.exe is the name of the executable, the -p command is the port where the USB to serial converter is; in most cases COM3. The -t trace parameter enables verbose output for the download process. This output will indicate a successful set of data is programmed with a [@] symbol. Errors in the download will also be highlighted for each data set. Last is the filename to be downloaded.
efm8load.exe -p COM3 -t EFM8BB10F8G-A-SOIC16.efm8
To simplify this steps I have created two .bat files and I name them
1 - Hex to Bootload Record Converter.bat
2 - Bootload Record Downloader.bat

The commands for the first file are:
@echo Generating bootloader record from hex

@hex2boot.exe EFM8BB1-prototyping.hex -o EFM8BB10F8G-A-SOIC16.efm8

@pause
For the second file:
@echo Downloading bootload record using COM3

@efm8load.exe -p COM3 -t EFM8BB10F8G-A-SOIC16.efm8

@pause
If you want to use my method, create two text files using Microsoft's Notepad and paste the commands above, making sure to rename the file names according to your project. Then rename the files with .bat extension. Or download them at the end of this page.
From now on to program your microcontroller, simply use Build command in Simplicity Studio and then go to this folder and open first bat file and then the second one and that's it.

The Bootloader

On first use, because the flash memory is empty the bootloader will start up and the microcontroller can be programmed using UART communication. However after that in order to boot in bootloader mode a certain pin must be low for at least 50us when the MCU starts. When the MCU sees this pin low it will go to bootloader vector. With this particular MCU the pin number is 7/C2D. On other types of MCU's the pin is usually C2D the debugging pin. Consult the AN945 for more details.

General aspects 

By convention, all initial setup code is included in a function called Port_Init.
The setup code must be in a certain order like in the example code above. Even when I'm not using certain pin functions I still keep an example code commented out just as a reference.

Configure a pin as a digital input

Set the bit associated with the pin in the PnMDIN register to 1.This selects digital mode for the pin. Replace the PIN_NR with the pin you want to setup. In this case we use a pin on port 0 since the P0MDIN register. For port 1, use P1MDIN and so on.
PnMDIN is a SFR (Special Function Register) that has an hex address assigned to this name. If you don't know what this do, search online for bitwise operations to learn about it.
P0MDIN |= (1<<PIN_NR);
Clear the bit associated with the pin in the PnMDOUT register to 0. This configures the pin as open-drain.
P0MDOUT &= ~(1<<PIN_NR);
Set the bit associated with the pin in the Pn register to 1. This tells the output driver to “drive” logic high. Because the pin is configured as open-drain, the high-side driver is disabled, and the pin may be used as an input.
P0 |= (1<<PIN_NR);
P0 represents the Port 0 register. When in open-drain, this activates some weak pull-up resistor to keep the digital logic at a predefined level (High). If the pin was left floating then the logic level could be fluctuating depending on the electrical noise nearby.
When the bit is cleared like this:
P0 &= ~(1<<PIN_NR);
then the pin will be connected to GND. Do not connect logic High to the pin when doing this or you will damage the MCU if there are no resistors.

Configure a pin as a digital output 

Set the bit associated with the pin in the PnMDIN register to 1.This selects digital mode for the pin.
P0MDIN |= (1<<PIN_NR);
Set the bit associated with the pin in the PnMDOUT register to 1. This configures the pin as push-pull.
P0MDOUT |= (1<<PIN_NR);

Configure a pin as analog

Clear the bit associated with the pin in the PnMDIN register to 0.  This selects analog mode for the pin.
P0MDIN &= ~(1<<PIN_NR);
Set the bit associated with the pin in the Pn register to 1.
P0 |= (1<<PIN_NR);
Skip the bit associated with the pin in the PnSKIP register to ensure the crossbar does not attempt to assign a function to the pin.
P0SKIP |= (1<<PIN_NR);

Enable crossbar and weak pull-ups

Configuration complete; enable the crossbar.
XBR2 |= 0x40;
The  Crossbar is  a  multiplexer that maps internal digital signals to Port I/O pins on the device. On an AVR MCU for example, the peripherals like ADC or PWM are mapped to certain pins and cannot be changed. However in this architecture using the crossbar the pin functions can change depending of what peripherals you enable, like Timers, UART, ADC. UART pins are an exception because they are always on pins 4 and 5 on port 0.
It can be confusing at first to understand the crossbar priority and how it maps pins. I just use the configurator inside Simplicity Studio by making a project just for this. I enable lets say the timer, ADC and UART and see the generated code and what pins it uses and copy to my own project.

To learn more about the crossbar visit AN101 - Configuring the Port IO Crossbar Decoder.

On every file you need to include the SFR (Special Function Registers) declarations.
#include <SI_EFM8BB1_Register_Enums.h>
This contains the register names and their addresses. More on them later.
The globals.h header is a file that contains all common functions and macros used across multiple files. You can download it down below.

This function is a special one and is executed before main(). Since the watchdog is enabled by default, if you don't reset the watchdog timer in software, the microcontroller will be reset after a while. If you don't use the watchdog, here you can disable it.
void SiLabs_Startup(void){
 // Disable Watchdog with key sequence
 WDTCN = 0xDE; // First key
 WDTCN = 0xAD; // Second key
}

Selecting the CPU clock and prescaler

CLKSEL = CLKSEL_CLKSL__HFOSC | CLKSEL_CLKDIV__SYSCLK_DIV_1;

CLKSEL_CLKSL__HFOSC represents the high frequency oscillator 24.5MHz and CLKSEL_CLKDIV__SYSCLK_DIV_1 is the prescaler. As the name suggest the clock is divided by 1. You can replace the 1 in the name with 8 or other prescale values specified in the datasheet.
There is a low frequency oscillator, 80KHz I think that is used by the watchdog. It could be useful on ultra low power devices.

Using or creating a SFR (Special Function Register)

To make a pin high or low you can use predefined SFRs like so:
Suppose you want to toggle pin 2 on port 1. You would do like this:
P1_B2 = 1;
P1 represents Port 1 and B2 bit 2, or pin 2. Don't use bitwise operators because this represents a bit not a byte.
P1_B2 = 0;
makes it low.

Using SI_SBIT():
SI_SBIT(LED, SFR_P0, 0);   // LED on Port 0: Bit 0
you can make your own SFR. In this example we create a SFR with the name LED. The led is on port 0, so we use SFR_P0, and the pin number is 0. Now you can do this:
LED = 1; // LED On

Reading an input pin or a button

Using the example above, to read pin 2 on port 1
pinState = P1_B2;
This will return either 0 or 1. So to check if a button is pressed or not, connect the pin to one side of the button and the other side to GND. Then:
// If pin is low means the button is pressed
if(P1_B2 == 0){
        // Light the LED on port 0, pin 0
        P0_B0 = 1;
}else{
        // If the button is not pressed, turn off the LED
        P0_B0 = 0;
}



Memory Organization

Similar to 8051 architecture, the microcontroller has two types of data memory: internal RAM and external XRAM, each one having 256 bytes. After compiling the code in Simplicity Studio, you will see something like this in the console:
Program Size: data=109.1 xdata=234 const=0 code=4206
data - represents the number of bytes that occupy the RAM memory (max 256 bytes)
xdata - likewise but represents the XRAM external memory (max 256 bytes)
const - i don't know about this
code - is the flash memory being used by the program code (max 8 KB on this particular chip)

All variables will be stored in 'data' (RAM) by default.
To place a variable in XRAM simply declare it using the xdata memory type:
static volatile uint8_t xdata UART_TX_BUFFER[UART_TX_BUFFER_SIZE] = {'\0'};
Notice that xdata is placed just before the variable name. xdata cannot be used  for functions.
Also for constants - variables that don't need to be changed - use the memory type code instead of xdata. This will help if let's say you have large flash memory size and less data memory (RAM and XRAM) and you have some URL's; these can be stored using code memory type.

Bit-Addressable Objects

Another great thing about 8051 architecture, are the Bit-Addressable Objects. Often you need some flags that are either 0 or 1, true or false. In other microcontroller architectures, you would have to use an entire byte or use some workarounds, but here you can have a variable that uses just 1 bit.

The Cx51 Compiler places variables declared with the bdata memory type into the bit-addressable area. Furthermore, variables declared with the bdata memory type must be global (declared outside the scope of a function). You may declare these variables as shown below:
int bdata ibase;        /* Bit-addressable int */

char bdata bary [4];    /* Bit-addressable array */
The variables ibase and bary are bit-addressable. To access individual bits inside the bytes, use the sbit keyword when creating new variables.
sbit mybit0 = ibase ^ 0;      /* bit 0 of ibase */
sbit mybit15 = ibase ^ 15;    /* bit 15 of ibase */

sbit Ary07 = bary[0] ^ 7;     /* bit 7 of bary[0] */
sbit Ary37 = bary[3] ^ 7;     /* bit 7 of bary[3] */
Now you can modify the bits as follows:
mybit0 = 1
mybit0 = 0
Read more on Keil's website Bit-Addressable Objects.

That's it folks. If you have any questions, leave them in the comments below.

Download

Eagle schematic EFM8BB10F8G-A breakout board v1.0
globals file header
delay file header
Programming apps for EFM8

External Resources

Simplicity Studio software
Bootloader software on silabs site
EFM8BB1 (Reference Manual)
EFM8BB1 Data Sheet


2 comments: