Getting Started with EFM32 Zero Gecko ARM Cortex-M0+

Created by Scott Schmit, last modified on Jul 25, 2014

image

Purpose

This page is meant to help you get started with Zero Gecko microcontrollers which are part of the EFM32 device family from Silicon Labs. The EFM32 family of microcontrollers are some of the lowest power 32-bit microcontrollers available. The Zero Gecko device family implements an ARM Cortex-M0+ core and is available with up to 32KB program (flash) memory, up to 4KB RAM, and CPU speeds up to 24MHz. This page will help you start a blank IAR template project for Zero Gecko and also provide some example code to familiarize you with the various peripherals available with this microcontroller. Each tutorial includes example code for direct register manipulation as well as the free emlib API provided by SiLabs.

Quick Links

Purchase Links
Reference Documentation
Software Downloads
  • Simplicity Studio - Provides extensive documentation for all EFM32 devices and example code using emlib libraries.
  • IAR Embedded Workbench - Firmware development environment (CD also included with kit). Time-limited licenses, or code size-limited licenses are available for free. The 32KB size-limited version can be used for all of the examples found on this page.
  • Keil uVision - Firmware development environment. Similar free licenses are also available for this software.

Reference Design

Take a look at the following schematic and you will see some helpful information regarding board design with the Zero Gecko (or download a pdf version Schematic.pdf (168.4 KB) ). The following reference design was made for the full-featured Zero Gecko: EFM32ZG222F32 in the 48-pin QFP package.

Board Power/Decoupling

An on-board LDO regulates 5V from the USB bus to 3.3V which powers the entire board. Alternatively, the LDO can be bypassed and the board can be powered directly through the expansion header J1. Recommended decoupling capacitors were used for each power pin of the Zero Gecko. A ferrite bead and 1-ohm resistor were used to isolate noise in the digital power plane from the analog power plane.

Programming

The Debug connector (J6) shows you exactly which connections to make in order to program the MCU with an external programmer/debugger using the standard 20-pin ARM connector. Alternatively, instead of using an external debugger to program the device, you can use the UART bootloader which comes pre-flashed to every EFM32 device. For the Zero Gecko, the Low Energy UART (location #3) is used for the bootloader COM port which shares pins with the SW Debug port. The schematic shows which pin connections need to be made to properly communicate with the bootloader. A pull-up resistor on SWCLK is needed to actually invoke the bootloader. In the reference design, a push-button was used to connect a pull-up resistor to SWCLK. Therefore, the bootloader will only start if the button is pressed on power-up (or power-on-reset). See the SiLabs Application Note AN0003 for more information on the UART Bootloader.

External Crystals

The reference design also shows how to connect external crystals for the system clock as well as the necessary load capacitance for each crystal. The Zero Gecko has a max system clock rate of 24MHz. A 32.768kHz crystal can be connected for RTC applications.

Digital Communication Peripherals

The reference design includes a number of digital communication ports. An on-board light sensor was included as an I2C slave. The I2C bus was also broken out on the J1 expansion header for adding additional slaves to the bus. An on-board serial flash chip was included as a SPI target. The SPI bus was also broken out on the J1 expansion header for adding additional SPI targets to the bus. UART communication is available in a few different places. USART1 (location #0) and Low Energy UART (location #0) are both broken out on the expansion header J1. USART1 (location #0) and the Low Energy UART (location #3) are both availabe through the USB port via the USB/UART bridge IC from FTDI. A slide switch (SW1) is used to determine which UART port is connected to the FTDI chip.

The design includes a USB reference design using the FT230X USB/UART bridge IC from FTDI. It includes the necessary communication connections as well as decoupling, ESD protection, and VBUS power (bus-powered device). By having an on-board USB/UART bridge IC, you can power the board and program the device using the pre-installed UART bootloader using only a USB mini-B cable connected to your PC (SW1 must be set to Low Energy UART). The FT230X device must be configured as a Bus-Powered device and the CBUS pin must be configured for desired LED operation. The FT230X can be configured using the FTProg Utility from FTDI. Note: This reference was not USB certified, but communication was established and verified.

Analog Peripherals

An analog potentiometer was connected to PD4 as an on-board analog source along with voltage scaling resistors. PD4 and PD5 were both broken out on the expansion header J1. Therefore the user has the option of connecting 2 single-ended analog sources to the Zero Gecko, or these pins can be used as a differential pair of inputs to the ADC. There is no on-board target for the DAC. However, PB11 is broken out to the expansion header J1 for access to IDAC0 (location #0). PC0, PC1, PC2, and PE13 are all available on the expansion header J1 for access to the analog comparator.

User Interface

The design includes a push-button, 2 LEDs, and a thumbwheel potentiometer for the user interface. The push-button can be debounced with hardware using the proper RC components. It is connected to PC9 which can be used as a wake-up source from the EM4 sleep mode. The LEDs are active-high and can be controlled with PWM using TIMER1 (location #1), Compare Channels 0 and 1.

Bill of Materials

The following attachment lists all components used in the above reference design including purchase links:

Create an IAR Template Project for EFM32 Zero Gecko

This procedure outlines how to setup an IAR project for Zero Gecko from scratch. When we are done, it will have all of the necessary files included and have all of the project settings setup. We will save it as “EFM32ZG_Template” in a directory of our choosing, so for future Zero Gecko projects, you can simply copy and paste the template project, rename it, and it will already have all of the necessary settings done for you. As a prerequisite, Simplicity Studio must be installed prior to starting this demo.

Procedure

  1. Open IAR and navigate to Project → Create New Project
  2. In the “Create New Project” dialog box, ensure “ARM” is selected as the Toolchain. Select “Empty Project” and click “OK”.
    image
  3. Now a “Save As” dialog will pop up and you can select your project directory. You can setup your project directory wherever you wish. For the purposes of this demo, we will simply setup our project directory on the C:/ drive.
    Within the “Save As” dialog box, navigate to the C:/ drive and create a folder called “My_EFM32_Projects”. Within that newly created folder, create another folder called “EFM32ZG_Template”. This is the folder where our template project will reside. Once we are completely finished, this is the folder we will copy and paste for future Zero Gecko Projects.
    Within the “Save As” dialog box, open the newly created “EFM32ZG_Template” folder, create another folder called “iar”.
    In the “Save As” dialog box, type “EFM32ZG_Template.ewp” as the File Name. Save the file in the “iar” folder you just created.
    image
  4. Now that the blank project is created, we will add the necessary files to the project. Right-click the project in the workspace browser and select “Add Group”.
  5. Name the group “source” and click “OK”.
    image
  6. Now create the main.c file. Navigate to File → New → File.
    image
  7. An untitled file should open. Select File → Save As
    image
  8. In the “Save As” dialog box, navigate to the “EFM32ZG_Template” folder (one level above the iar folder) and save the file as “main.c”.
    image
  9. Now that the main.c file has been created, we have to actually add it to the workspace. Right-click on the “source” group you created and select Add → Files.
  10. Select the main.c file you just created.
    image
  11. Add two more groups to your project and call them “CMSIS” and “emlib”.
  12. Right-click the “CMSIS” group and select Add → Add File.
  13. At the time of this writing, Version 2 was the most recent version of Simplicity Studio. When Simplicity Studio v2 installs, it saves all documentation, as well as the CMSIS and emlib libraries here:
    C:\SiliconLabs\SimplicityStudio\v2\developer\sdks\efm32\v2
    note: If you have a different version of Simplicity Studio, the directory may change.

For Zero Gecko projects, the system_efm32zg.c file needs to be added to the workspace. At the time of this writing, the file is located here:
C:\SiliconLabs\SimplicityStudio\v2\developer\sdks\efm32\v2\Device\EnergyMicro\EFM32ZG\Source

:white_check_mark:If you can’t locate the file manually, try using the Windows Explorer integrated search tool.

Once you’ve located system_efm32zg.c , select it and click “Open”.


14. Now add the startup_efm32zg.s file to the “CMSIS” group. Right-click on “CMSIS” and select Add-> Add File. At the time of this writing the file is located here:
C:\SiliconLabs\SimplicityStudio\v2\developer\sdks\efm32\v2\Device\EnergyMicro\EFM32ZG\Source\IAR

Select startup_efm32zg.s and click “Open”.


15. Now we can add source files to the “emlib” group that was previously created. Since this is a template project, try to add files that you think would be relevant to save you time in the future. Add them all if you want, but as a bare minimum, you should add em_system.c to the group.

At the time of this writing, the emlib source libraries were located here:
C:\SiliconLabs\SimplicityStudio\v2\developer\sdks\efm32\v2\emlib\src

Select whichever files you wish (use Ctrl+left click to select multiple files at the same time) and click “Open”.


16. In order to use functions from the emlib libraries we just added, we need to add #include for each file that we wish to use in our main.c file. For example, if we wanted to use any of the prewritten ADC functions, we would need to include the em_adc.h header file. Since this is a template project, we will simply add em_device.h, em_system.h, and em_chip.h . As a minimum, these 3 files should be added to the main.c file in every EFM32 project.

If it’s not open already, open main.c and add the 3 includes shown below. You’ll also need to add a blank “main” function for the project to build properly.
image
17. Now let’s setup the project options. Right-click on “EFM32ZG_Template” in the workspace explorer and select “Options”.
image
18. In the “General Options” category, under the “Target” tab, make sure the “Device” button is selected and click the “list” icon on the right. Navigate to Silicon Labs → EFM32ZG and select the device you are using. For this demo, I selected EFM32ZG222F32.


19. In the “C/C++ Compiler Settings” category, under the “Preprocessor” tab, you can add include directories for your project. You will need to add paths for the CMSIS and emlib libraries as well as the “include” folder for your specific device. You can either type them in directly or click the “…” button to Browse to the necessary folders. You can hard-code the full path or use relative paths from the project directory to the libraries. For this example, I hard-coded the full path to the libraries.

Also under the “Preprocessor” tab, you need to define your device symbol. For this template project, we’ll define the symbol EFM32ZG222F32.



20. In the “Output Converter” category, check “Generate additional output” and select “binary” for the output format.

21. In the “Linker” category, under the “Config” tab, select “Override default” and navigate to the .icf file for your device. This file is located in the IAR install directory, not the Simplicity Studio install directory. IAR Embedded Workbench for ARM v7.10 was used for this demo. Therefore, the linker file was located here:
C:\Program Files (x86)\IAR Systems\Embedded Workbench 7.0\arm\config\linker\SiliconLaboratories

Note: If you are using a different version of IAR, the linker file may be in another location.

Once you’ve located the file, select it and click “Open”. Note: If you are planning on using a bootloader, you will instead need to use the linker file for the bootloader. For the SiLabs UART and USB bootloaders, the appropriate linker files are included in the App Notes.


22. In the “Debugger” category, you can setup the properties for the debugger you are using. If you are using the EFM32ZG-STK3200 starter kit, select “J-Link/J-Trace” as the Driver under the “Setup” tab. If you are using an external debugger, you may have different settings. If you are using a bootloader to program the device instead of a debugger, you don’t need to modify anything in the “Debugger” category.

23. Still in the “Debugger” category, under the “Download” tab, check the “Verify download” and “Use flash loader(s)” boxes. Again, if you are using a bootloader to program the device, you will not need to worry about these settings.

24. In the “J-Link” category, under the “Connection” tab, ensure “SWD” is selected as the Interface.

That should be the last step for configuring project options. Click “OK” to apply your changes.
image
25. The project is fully setup and configured as a template project. Now we need to save the workspace in the proper location. Select File-> Save All. Save the workspace as “EFM32ZG_Template.eww” in the “iar” folder within your project directory.
image
26. Right-click on the project and click “Make” to build the project. Building the project compiles the program and places the output files in the “Debug” folder of your project directory.
image
27. We have now finished creating our Zero Gecko template project. Whenever we wish to start a new IAR project for Zero Gecko, we can simply copy and paste this template project in the same directory and everything is setup for us. Once you paste a copy of the template, simply rename the project folder to whatever you wish. You can also rename the workspace file to match the project name. All other files can keep “Template” in the file name and the project will still build properly.


image

You are on your way! Now let’s look at some code examples…

Code Examples

The following examples will help familiarize you with the internal registers for a few different peripherals available in the Zero Gecko. For each example, an IAR template project was copy&pasted and renamed using the procedure above. Each example was compiled and tested on the Reference Design board mentioned above. However, the code can easily be applied to any Zero Gecko device (defined by the startup_efm32zg.s file) on any board while paying special attention to pin assignments for various package types. The examples are presented in order of difficulty with the first one being the easiest to understand. Some examples build on each other. Therefore, if you are new to EFM32 devices, I would recommend starting with the first example and working your way through in order.

  • GPIO Example
    This example demonstrates basic GPIO functions on the Zero Gecko. For GPIO inputs, each pin has an internal pull-up and pull-down resistor, or the user can leave the pin floating. For GPIO outputs, pins can be set as “Open Drain” or “Push-Pull”. The user has the option of setting the drive strength for digital output pins. The following drive strength options are available on the Zero Gecko:
  • High - 20mA
  • Standard - 6mA
  • Low - 2mA
  • Lowest - 0.5mA
    For this example, PORTC pin 9 was setup as an input with the internal pull-up resistor enabled. PORTE pin 10 was setup as an output with a standard drive mode of 6mA. PORTE pin 11 was setup as an output with the lowest drive mode setting of 0.5mA. A pushbutton was connected between PC9 and ground. When the button is pressed, a logical ‘0’ is seen by the PC9 input. When the button is released, the internal pull-up resistor ties PC9 high which reads logical ‘1’. PE10 and PE11 were each connected to an LED with a 550 Ohm series resistor.
    This example was written so that when the button is pressed, the LED connected to PE10 toggles.
    GPIO Example Using Direct Register Access
    The following code demonstrates basic GPIO functionality using direct register access. For the following code to work, the em_system.c file needs to be added to the workspace.
#include "em_device.h"
#include "em_system.h"
#include "em_chip.h"
 
#define BUTTON_PORT 2  // PORTC
#define BUTTON_PIN  9
#define LED_PORT    4  // PORTE
#define LED0_PIN    10
#define LED1_PIN    11
 
int main () {
   
  CHIP_Init();
   
  CMU->HFPERCLKEN0 = (1 << 7); // enable GPIO peripheral clock
   
  // configure LED0 pin as push-pull output with standard drive strength
  // configure LED1 pin as push-pull output with alternate drive strength
  GPIO->P[LED_PORT].MODEH   = (5 << 12) | (4 << 8);
  GPIO->P[LED_PORT].CTRL    = (1 << 0);                          // set alternate drive strength to lowest setting (0.5mA)
  GPIO->P[LED_PORT].DOUTSET = (1 << LED1_PIN) | (1 << LED0_PIN); // turn on both LEDs
   
  GPIO->P[BUTTON_PORT].MODEH = (2 << 4);          // set BUTTON_PIN to input
  GPIO->P[BUTTON_PORT].DOUT  = (1 << BUTTON_PIN); // enable pull-up on push button
   
  while(1) {
    if(!(GPIO->P[BUTTON_PORT].DIN & (1 << BUTTON_PIN))) { // if button is pressed
      GPIO->P[LED_PORT].DOUTCLR = (1 << LED0_PIN);        // Turn off LED0
    }else{                                                // if button is released
      GPIO->P[LED_PORT].DOUTSET = (1 << LED0_PIN);        // Turn on LED0
    }
  }
}

GPIO Example Using emlib API
The following code demonstrates basic GPIO functionality using emlib libraries provided by Silicon Labs. For the following code to work, the following emlib source files must be added to the workspace:

  • em_cpu.c
  • em_gpio.c
  • em_system.c
#include "em_device.h"
#include "em_system.h"
#include "em_chip.h"
#include "em_cmu.h"
#include "em_gpio.h"
 
#define BUTTON_PORT gpioPortC
#define BUTTON_PIN  9
#define LED_PORT    gpioPortE
#define LED0_PIN    10
#define LED1_PIN    11
 
int main () {
   
  CHIP_Init();
   
  CMU_ClockEnable(cmuClock_GPIO, true);                           // enable GPIO peripheral clock
   
  GPIO_PinModeSet(LED_PORT, LED0_PIN, gpioModePushPull, 0);       // configure LED0 pin as push-pull output with standard drive strength
  GPIO_PinModeSet(LED_PORT, LED1_PIN, gpioModePushPullDrive, 1);  // configure LED1 pin as push-pull output with alternate drive strength
  GPIO_DriveModeSet(LED_PORT, gpioDriveModeLowest);               // set alternate drive strength to lowest setting (0.5mA)
  GPIO_PinOutSet(LED_PORT, LED0_PIN);                             // turn on LED0
  GPIO_PinOutSet(LED_PORT, LED1_PIN);                             // turn on LED1
   
  GPIO_PinModeSet(BUTTON_PORT, BUTTON_PIN, gpioModeInputPull, 1); // configure BUTTON_PIN as input with pull-up enabled
   
  while(1) {
    if(!(GPIO_PinInGet(BUTTON_PORT, BUTTON_PIN))) { // if button is pressed
      GPIO_PinOutClear(LED_PORT, LED0_PIN);         // turn off LED0
    }else{                                          // if button is released
      GPIO_PinOutSet(LED_PORT, LED0_PIN);           // turn on LED0
    }
  }
}
  • 16-bit Timer with Interrupt
    The following code demonstrates how to setup a 16-bit timer with interrupt. It uses the default system clock source which is the internal high frequency RC Oscillator. On power up, the HF RC runs at 14MHz. To make the timing easy, the system clock frequency was changed from 14MHz to 1MHz. The undivided system clock was used as the clock source for TIMER0. The top value of TIMER0 was set to 1000 which makes the timer overflow every millisecond. The TIMER0 Overflow Interrupt was enabled and a temporary variable was used to count the number of milliseconds. After a defined time period (500 ms) passed an LED was toggled.
    Timer with Interrupt Using Direct Register Access
    The following code demonstrates how to setup a 16-bit timer with interrupt using direct register access. For the following code to work, the em_system.c file needs to be added to the workspace.
#include "em_device.h"
#include "em_system.h"
#include "em_chip.h"
 
#define LED_PORT     4   // PORTE
#define LED0_PIN     10
#define LED1_PIN     11
#define BLINK_PERIOD 500 // ms
 
volatile uint16_t ms_counter = 0;
 
void TIMER0_IRQHandler(void) {
  TIMER0->IFC = 1; // clear overflow flag
  ms_counter++;    // increment counter
}
 
int main () {
   
  CHIP_Init();
   
  CMU->HFRCOCTRL   = 0x8;                 // set HF RC oscillator to 1MHz
  CMU->HFPERCLKEN0 = (1 << 7) | (1 << 0); // enable GPIO and TIMER0 peripheral clocks
   
  GPIO->P[LED_PORT].MODEH = (4 << 8);     // set LED0 pin as push-pull output with standard drive strength
   
  TIMER0->TOP = 1000;                     // set top value for TIMER0
  TIMER0->IEN = 1;                        // enable TIMER0 overflow interrupt
   
  NVIC_EnableIRQ(TIMER0_IRQn);            // enable TIMER0 interrupt vector in NVIC
   
  TIMER0->CMD = 0x1;                      // start TIMER0
   
  while(1) {
    if(BLINK_PERIOD == ms_counter) {
      GPIO->P[LED_PORT].DOUTTGL = (1 << LED0_PIN); // toggle LED0
      ms_counter = 0;                              // reset counter
    }
  }
}

Timer with Interrupt Using emlib API
The following code demonstrates how the emlib libraries can be used to implement the same Timer with Interrupt example. For the code to work, the following emlib source files must be added to the workspace:

  • em_cpu.c
  • em_gpio.c
  • em_timer.c
  • em_system.c
#include "em_device.h"
#include "em_system.h"
#include "em_chip.h"
#include "em_cmu.h"
#include "em_gpio.h"
#include "em_timer.h"
 
#define LED_PORT     gpioPortE
#define LED0_PIN     10
#define BLINK_PERIOD 500
 
volatile uint16_t ms_counter = 0;
 
void TIMER0_IRQHandler(void) {
  TIMER_IntClear(TIMER0, TIMER_IF_OF); // clear overflow flag
  ms_counter++;                        // increment counter
}
int main () {
   
  CHIP_Init();
   
  CMU_HFRCOBandSet(cmuHFRCOBand_1MHz);    // set HF RC oscillator to 1MHz
  CMU_ClockEnable(cmuClock_GPIO, true);   // enable GPIO peripheral clock
  CMU_ClockEnable(cmuClock_TIMER0, true); // enable TIMER0 peripheral clock
   
  GPIO_PinModeSet(LED_PORT, LED0_PIN, gpioModePushPullDrive, 0); // set LED0 pin as push-pull output with standard drive strength
   
  TIMER_Init_TypeDef timer0_init =
  {
    .enable     = true,                 // start timer upon configuration
    .debugRun   = true,                 // keep timer running even on debug halt
    .prescale   = timerPrescale1,       // use /1 prescaler; TIMER0 clock = HF clock = 1MHz
    .clkSel     = timerClkSelHFPerClk,  // set HF peripheral clock as clock source
    .fallAction = timerInputActionNone, // no action on falling edge
    .riseAction = timerInputActionNone, // no action on rising edge
    .mode       = timerModeUp,          // use up-count mode
    .dmaClrAct  = false,                // not using DMA
    .quadModeX4 = false,                // not using quad decoder
    .oneShot    = false,                // using continuous mode, not one-shot
    .sync       = false,                // not synchronizing with other timers
  };
  TIMER_TopSet(TIMER0, 1000);           // set TIMER0 top value
  TIMER_IntEnable(TIMER0, TIMER_IF_OF); // enable TIMER0 overflow interrupt
  NVIC_EnableIRQ(TIMER0_IRQn);          // enable TIMER0 interrupt vector in NVIC
  TIMER_Init(TIMER0, &timer0_init);     // apply configuration and start TIMER0
   
  while(1) {
    if(BLINK_PERIOD == ms_counter) {
      GPIO_PinOutToggle(LED_PORT, LED0_PIN); // toggle LED
      ms_counter = 0;                        // reset counter
    }
  }
}
  • Using the HF XTAL Oscillator
    The following example is similar to the Timer with Interrupt example above. However, this example uses a 24MHz crystal oscillator as the system clock source instead of the internal HF RC oscillator. The HF peripheral clock and the TIMER0 prescaler were left at /1. Therefore, the clock for TIMER0 was the same as the system clock (24MHz). The TOP value of the counter was set to 24000 so that the overflow interrupt occurs every millisecond. The TIMER0 Overflow Interrupt was enabled and a temporary variable was used to count the number of milliseconds. After a defined time period (500 ms) passed an LED was toggled.
    The point of this example is to assign the High Frequency Crystal Oscillator as the High Frequency clock source. Once the HF crystal is selected as the clock source, the default internal RC oscillator is disabled to conserve power.
    :exclamation:Use caution when disabling oscillators!
    The CMU STATUS register should always be checked before disabling an oscillator. You may inadvertently disable the currently selected clock source. In which case your microcontroller can become unusable. The following example code shows how to properly check the STATUS register before switching and disabling clock sources.

HF XTAL Example Using Direct Register Access

#include "em_device.h"
#include "em_system.h"
#include "em_chip.h"
 
#define LED_PORT     4   // PORTE
#define LED0_PIN     10
#define BLINK_PERIOD 500 // ms
 
volatile uint16_t ms_counter = 0;
 
void TIMER0_IRQHandler(void) {
  TIMER0->IFC = 1; // clear overflow flag
  ms_counter++;    // increment counter
}
 
int main () {
   
  CHIP_Init();
   
  CMU->OSCENCMD |= 0x4;                   // enable XTAL oscillator
  while(!(CMU->STATUS & 0x8));            // wait for XTAL osc to stabilize
  CMU->CMD |= 0x2;                        // select HF XTAL osc as system clock source (24MHz)
  if(!(CMU->STATUS & (1 << 10))) {        // make sure HFRCO is no longer the selected clock
    CMU->OSCENCMD |= 0x2;                 // disable HFRCO to save power
  }
 
  CMU->HFPERCLKEN0 = (1 << 7) | (1 << 0); // enable GPIO and TIMER0 peripheral clocks
   
  GPIO->P[LED_PORT].MODEH = (4 << 8);     // set LED0 pin as push-pull output with standard drive strength
   
  TIMER0->TOP = 24000;                    // set top vlaue for TIMER0
  TIMER0->IEN = 1;                        // enable TIMER0 overflow interrupt
   
  NVIC_EnableIRQ(TIMER0_IRQn);            // enable TIMER0 interrupt vector in NVIC
   
  TIMER0->CMD = 0x1;                      // start TIMER0
   
  while(1) {
    if(BLINK_PERIOD == ms_counter) {
      GPIO->P[LED_PORT].DOUTTGL = (1 << LED0_PIN); // toggle LED0
      ms_counter = 0;                              // reset counter
    }
  }
}

HF XTAL Example Using emlib API
The following code uses the emlib API to select the HF crystal oscillator as the system clock source and implement the same Timer with Interrupt example from above. The following files need to be added to the workspace for the code to work:

  • em_cmu.c
  • em_emu.c
  • em_gpio.c
  • em_timer.c
  • em_system.c
#include "em_device.h"
#include "em_system.h"
#include "em_chip.h"
#include "em_cmu.h"
#include "em_gpio.h"
#include "em_timer.h"
 
#define LED_PORT gpioPortE
#define LED0_PIN 10
#define BLINK_PERIOD 500 // ms
 
volatile uint16_t ms_counter = 0;
 
void TIMER0_IRQHandler(void) {
  TIMER_IntClear(TIMER0, TIMER_IF_OF); // clear overflow flag
  ms_counter++;                        // increment counter
}
 
int main () {
   
  CHIP_Init();
   
  CMU_OscillatorEnable(cmuOsc_HFXO, true, true);            // enable XTAL osc and wait for it to stabilize
  CMU_ClockSelectSet(cmuClock_HF, cmuSelect_HFXO);          // select HF XTAL osc as system clock source (24MHz)
  if(cmuSelect_HFRCO != CMU_ClockSelectGet(cmuClock_HF)) {  // make sure HFRCO is no longer the selected clock source
    CMU_OscillatorEnable(cmuOsc_HFRCO, false, false);       // disable it to save power
  }
   
  CMU_ClockEnable(cmuClock_GPIO, true);                     // enable GPIO peripheral clock
  CMU_ClockEnable(cmuClock_TIMER0, true);                   // enable TIMER0 peripheral clock
   
  GPIO_PinModeSet(LED_PORT, LED0_PIN, gpioModePushPull, 0); // set LED0 pin as push-pull output
   
  TIMER_Init_TypeDef timer0_init =
  {
    .enable     = true,                 // start timer upon configuration
    .debugRun   = true,                 // keep timer running even on debug halt
    .prescale   = timerPrescale1,       // use /1 prescaler; TIMER0 clock = HF clock = 1MHz
    .clkSel     = timerClkSelHFPerClk,  // set HF peripheral clock as clock source
    .fallAction = timerInputActionNone, // no action on falling edge
    .riseAction = timerInputActionNone, // no action on rising edge
    .mode       = timerModeUp,          // use up-count mode
    .dmaClrAct  = false,                // not using DMA
    .quadModeX4 = false,                // not using quad decoder
    .oneShot    = false,                // using continuous mode, not one-shot
    .sync       = false,                // not synchronizing with other timers
  };
  TIMER_TopSet(TIMER0, 24000);          // set TIMER0 top value
  TIMER_IntEnable(TIMER0, TIMER_IF_OF); // enable TIMER0 overflow interrupt
  NVIC_EnableIRQ(TIMER0_IRQn);          // enable TIMER0 interrupt vector in NVIC
  TIMER_Init(TIMER0, &timer0_init);     // configure and start TIMER0
   
  while(1) {
    if(BLINK_PERIOD == ms_counter) {
      GPIO_PinOutToggle(LED_PORT, LED0_PIN); // toggle LED0
      ms_counter = 0;                        // reset counter
    }
  }
}
  • LF XTAL - Real Time Counter Example
    This example demonstrates usage of the Low Frequency Crystal Oscillator and how it can be used as the clock source for the 24-bit Real Time Counter. A 32.768 kHz crystal was connected to the LF XTAL input pins of the Zero Gecko. The LF XTAL was set as the input to the RTC with a prescaler of /1. The RTC was set to interrupt on compare match and clear the counter. The compare value was set to 32768. Therefore, the compare match interrupt executed once every second and toggled an LED connected to PE10.
    The LFXO startup time was left at the default value of 32768 cycles. Therefore, there is an initial 1-sec delay before code continues executing.
    This example also demonstrates how to access asynchronous registers. The 32.768kHz crystal was used as the clock source for the RTC. However, the core was still clocked from the High Frequency Oscillator. This is nice because it allows you to use the RTC without the core running. However, any interaction between the core and an asynchronous peripheral requires special attention. When writing to an asynchronous peripheral register, you must monitor the SYNCBUSY register to make sure the write completes. All of the Low Energy peripherals in the Zero Gecko have asynchronous registers.

RTC Example Using Direct Register Access
The following example code demos the RTC using direct register access. For the following code to work, the em_system.c file needs to be added to the workspace.

#include "em_device.h"
#include "em_system.h"
#include "em_chip.h"
 
#define LED_PORT     4   // PORTE
#define LED0_PIN     10  // PE10
 
// overflow ISR executes every second
void RTC_IRQHandler(void) {
  RTC->IFC = 2;                                // clear COMP0 flag
  GPIO->P[LED_PORT].DOUTTGL = (1 << LED0_PIN); // toggle LED
}
 
int main () {
   
  CHIP_Init();
  CMU->OSCENCMD |= (1 << 8);          // enable LFXO
  while(!(CMU->STATUS & (1 << 9)));   // wait for LF XTAL osc to stabilize
  CMU->LFCLKSEL = (2 << 0);           // select LFXO as clock source to LFA
  CMU->LFACLKEN0 |= 0x1;              // enable RTC peripheral clock
  CMU->HFCORECLKEN0 = (1 << 2);       // enable the Low Energy Peripheral Interface clock
  CMU->HFPERCLKEN0 = (1 << 7);        // enable GPIO peripheral clock
   
  GPIO->P[LED_PORT].MODEH = (4 << 8); // set LED0 pin as push-pull output with standard drive strength
   
  RTC->CNT = 0;                       // clear the counter (optional)
  RTC->COMP0 = 32768;                 // set compare value (asynch reg)
  while(RTC->SYNCBUSY & 0x2);         // wait for COMP0 reg write to complete
  RTC->CTRL = (1 << 2) | (1 << 0);    // use COMP0 value as TOP value, enable RTC (asynch reg)
  while(RTC->SYNCBUSY & 0x1);         // wait for CTRL reg write to complete
  RTC->IFC = 0x7;                     // clear RTC interrupt flags
  RTC->IEN = 0x2;                     // enable COMP0 interrupt
   
  NVIC_EnableIRQ(RTC_IRQn);           // enable RTC interrupt vector in NVIC
   
  while(1);                           // loop to allow interrupt to execute continuously
}

RTC Example Using emlib API
The following code executes the RTC demo while utilizing the prewritten emlib functions. For the code to work the following files need to be added to the workspace:

  • em_cmu.c
  • em_emu.c
  • em_gpio.c
  • em_rtc.c
  • em_system.c
#include "em_device.h"
#include "em_system.h"
#include "em_chip.h"
#include "em_cmu.h"
#include "em_gpio.h"
#include "em_rtc.h"
 
#define LED_PORT gpioPortE // PORTE
#define LED0_PIN 10        // PE10
 
// overflow ISR executes every second
void RTC_IRQHandler(void) {
  RTC_IntClear(0x2);                     // clear COMP0 flag
  GPIO_PinOutToggle(LED_PORT, LED0_PIN); // toggle LED
}
 
int main () {
   
  CHIP_Init();
   
  CMU_OscillatorEnable(cmuOsc_LFXO , true, true);   // enable LFXO and wait for it to stabilize
  CMU_ClockSelectSet(cmuClock_LFA, cmuSelect_LFXO); // select LFXO as clock source to LFA
  CMU_ClockEnable(cmuClock_LFA, true);              // (optional) LFA clock is enabled by default
  CMU_ClockEnable(cmuClock_RTC, true);              // enable RTC peripheral clock
  CMU_ClockEnable(cmuClock_CORELE, true);           // enable the Low Energy Peripheral Interface clock
  CMU_ClockEnable(cmuClock_GPIO, true);             // enable GPIO peripheral clock
   
  GPIO_PinModeSet(LED_PORT, LED0_PIN, gpioModePushPull, 0);
   
  RTC_CounterReset();       // clear the counter (optional)
  RTC_CompareSet(0, 32768); // set compare value (asynch reg)
  RTC_IntClear(0x7);        // clear all RTC interrupt flags
  RTC_IntEnable(0x2);       // enable COMP0 interrupt
   
  NVIC_EnableIRQ(RTC_IRQn); // enable RTC interrupt vector in NVIC
   
  RTC_Init_TypeDef rtc_init =
  {
    .enable = true,    // enable counter upon initialization
    .debugRun = false, // halt counter on debug halt
    .comp0Top = true,  // clear counter on compare match
  };
   
  RTC_Init(&rtc_init);  // initiallize RTC and start the counter
   
  while(1);             // loop to allow interrupt to execute continuously
}
  • Pulse-Width-Modulation (PWM) Example
    This example demonstrates how to setup PWM output on the EFM32 Zero Gecko. It uses Capture/Compare Channel 0 of TIMER1 which is available on PORTE, Pin10 of the microcontroller (TIMER1 location #1). For this example, PWM output was to control the intensity of an LED.
    This example uses 2 different timers. TIMER1 was used as the PWM generator. The PWM period was set to 1ms (frequency of 1kHz) and the duty cycle was adjusted by updating the output compare register at a defined UPDATE_PERIOD. The second timer – TIMER0 – was used to generate the update period of 250ms. This example uses TIMER0 with its overflow interrupt just like in the “Timer with Interrupt” example above.
    Whenever the Update Period is reached the intensity of LED0 is updated with the new compare value and LED1 toggles.
    The system clock for this example was the High Frequency RC Oscillator, set to 1MHz. The High Frequency Peripheral clock was used as the source for TIMER0 and TIMER1 with a prescaler of /1 for each timer. Therefore, TIMER0, TIMER1, and the System Clock all share the same frequency. The TOP value of each timer was set to 1000, so the overflow interrupt period for TIMER0 and the PWM period for TIMER1 were both 1ms (1kHz freq).

PWM Example Using Direct Register Access
The following example code demonstrates how to achieve LED intensity control using PWM output functionality of TIMER1 with direct register access. For the following code to work, the em_system.c file needs to be added to the workspace.

#include "em_device.h"
#include "em_system.h"
#include "em_chip.h"
 
#define LED_PORT         4      // gpioPortE
#define LED0_PIN         10
#define LED1_PIN         11
#define TOP_VAL_PWM      1000   // sets PWM frequency to 1kHz (1MHz timer clock)
#define TOP_VAL_GP_TIMER 1000   // sets general purpose timer overflow frequency to 1kHz (1MHz timer clock)
#define UPDATE_PERIOD    250    // update compare value and toggle LED1 every 250ms
#define INC_VAL (TOP_VAL_PWM/4) // adjust compare value amount
  
volatile uint16_t ms_counter = 0; // global variable to count general purpose timer overflow events
  
// TIMER ISR, executes every ms
void TIMER0_IRQHandler(void) {
  TIMER0->IFC = 1; // clear source of interrupt
  ms_counter++;    // increment Counter
}
  
int main() {
    
  CHIP_Init();
   
  uint16_t compare_val = 0; // initial PWM duty cycle is 0% (LED0 off)
  uint8_t inc = 1;          // increment = true
    
  CMU->HFRCOCTRL = 0x8;                               // set High Freq. RC Osc. to 1 MHz and use as system source
  CMU->HFPERCLKEN0 = (1 << 7) | (1 << 1) | (1 << 0);  // enable GPIO, TIMER0, and TIMER3 peripheral clocks
  GPIO->P[LED_PORT].MODEH = (4 << 12) | (4 << 8);     // configure LED0 and LED1 pins as digital outputs (push-pull)
  GPIO->P[LED_PORT].DOUTSET = (1 << LED1_PIN);        // Turn on LED1
    
  TIMER0->TOP = TOP_VAL_GP_TIMER;                     // GP Timer period will be 1ms = 1kHz freq
  TIMER1->TOP = TOP_VAL_PWM;                          // PWM period will be 1ms = 1kHz freq
    
  TIMER0->CNT = 0;                                    // start counter at 0 (up-count mode)
  TIMER1->CNT = 0;                                    // start counter at 0 (up-count mode)
    
  TIMER1->CC[0].CCV = compare_val;                    // set CC0 compare value (0% duty)
  TIMER1->CC[0].CCVB = compare_val;                   // set CC0 compare buffer value (0% duty)
    
  TIMER0->IEN = 1;                                    // enable TIMER0 overflow interrupt
  NVIC_EnableIRQ(TIMER0_IRQn);                        // enable TIMER0 interrupt vector in NVIC
    
  TIMER1->CC[0].CTRL = 0x3;                           // put TIMER1 CC channel 0 in PWM mode
  TIMER1->ROUTE = (1 << 16) | (1 << 0);               // connect PWM output to PE10 (TIMER1 location #1) See EFM32ZG222 datasheet for details.
    
  TIMER0->CTRL = (1 << 6);                            // allow timer to run while in debug mode
  TIMER1->CTRL = (1 << 6);                            // allow timer to run while in debug mode
    
  TIMER0->CMD = 0x1;                                  // start TIMER0
  TIMER1->CMD = 0x1;                                  // start TIMER1
    
  while(1) {
    if(UPDATE_PERIOD == ms_counter) {
      if(inc) {                                    // if increment = true
        compare_val += INC_VAL;                    // increase the compare value
      }else{                                       // if increment = false
        compare_val -= INC_VAL;                    // decrease the compare value
      }
      TIMER1->CC[0].CCVB = compare_val;            // write new value to compare buffer
      GPIO->P[LED_PORT].DOUTTGL = (1 << LED1_PIN); // toggle LED1
      ms_counter = 0;                              // reset counter
    }
    if(compare_val > (TOP_VAL_PWM-1)) { inc = 0; } // if compare value is at max, start decrementing
    if(compare_val < 1) { inc = 1; }               // if compare value is at min, start incrementing
  }
}

PWM Example Using emlib API
The following example code implements the same PWM demo using the emlib API functions. The following files need to be added to the workspace for the code to work:

  • em_cmu.c
  • em_gpio.c
  • em_timer.c
  • em_system.c
#include "em_device.h"
#include "em_system.h"
#include "em_chip.h"
#include "em_cmu.h"
#include "em_gpio.h"
#include "em_timer.h"
 
#define TOP_VAL_PWM      1000            // sets PWM frequency to 1kHz (1MHz timer clock)
#define TOP_VAL_GP_TIMER 1000            // sets general purpose timer overflow frequency to 1kHz (1MHz timer clock)
#define UPDATE_PERIOD    250             // update compare value, toggle LED1 every 1/4 second (250ms)
#define INC_VAL          (TOP_VAL_PWM/4) // adjust compare value amount
 
#define LED_PORT         gpioPortE
#define LED0_PIN         10              // PE10
#define LED1_PIN         11              // PE11
 
volatile uint16_t ms_counter = 0;        // global variable to count general purpose timer overflow events
 
// TIMER ISR executes every ms
void TIMER0_IRQHandler(void) {
  TIMER_IntClear(TIMER0, TIMER_IF_OF);   // clear interrupt source
  ms_counter++;                          // increment counter
}
 
int main () {
   
  uint32_t compare_val = 0;               // initial PWM duty cycle is 0% (LED0 off)
  uint8_t inc = 1;                        // increment = true
   
  CHIP_Init();
   
  CMU_HFRCOBandSet(cmuHFRCOBand_1MHz);    // Set HF oscillator to 1MHz and use as system source
  CMU_ClockEnable(cmuClock_GPIO, true);   // enable GPIO peripheral clock
  CMU_ClockEnable(cmuClock_TIMER0, true); // enable TIMER0 peripheral clock
  CMU_ClockEnable(cmuClock_TIMER1, true); // enable TIMER3 peripheral clock
   
  GPIO_PinModeSet(LED_PORT, LED0_PIN, gpioModePushPull, 0); // set LED0 pin as push-pull output
  GPIO_PinModeSet(LED_PORT, LED1_PIN, gpioModePushPull, 1); // set LED1 pin as push-pull output
   
  // Setup Timer Channel Configuration for PWM
  TIMER_InitCC_TypeDef timerCCInit = {
    .eventCtrl  = timerEventEveryEdge,    // this value will be ignored since we aren't using input capture
    .edge       = timerEdgeNone,          // this value will be ignored since we aren't using input capture
    .prsSel     = timerPRSSELCh0,         // this value will be ignored since we aren't using PRS
    .cufoa      = timerOutputActionNone,  // no action on underflow (up-count mode)
    .cofoa      = timerOutputActionSet,   // on overflow, we want the output to go high, but in PWM mode this should happen automatically
    .cmoa       = timerOutputActionClear, // on compare match, we want output to clear, but in PWM mode this should happen automatically
    .mode       = timerCCModePWM,         // set timer channel to run in PWM mode
    .filter     = false,                  // not using input, so don't need a filter
    .prsInput   = false,                  // not using PRS
    .coist      = false,                  // initial state for PWM is high when timer is enabled
    .outInvert  = false,                  // non-inverted output
  };
   
  // Setup Timer Configuration for PWM
  TIMER_Init_TypeDef timerPWMInit =
  {
    .enable     = true,                 // start timer upon configuration
    .debugRun   = true,                 // run timer in debug mode
    .prescale   = timerPrescale1,       // set prescaler to /1
    .clkSel     = timerClkSelHFPerClk,  // set clock source as HFPERCLK
    .fallAction = timerInputActionNone, // no action from inputs
    .riseAction = timerInputActionNone, // no action from inputs
    .mode       = timerModeUp,          // use up-count mode
    .dmaClrAct  = false,                // not using DMA
    .quadModeX4 = false,                // not using Quad Dec. mode
    .oneShot    = false,                // not using one shot mode
    .sync       = false,                // not syncronizing timer3 with other timers
  };
   
  //Setup Timer Configuration for general purpose use
  TIMER_Init_TypeDef timerGPInit =
  {
    .enable     = true,                 // start timer upon configuration
    .debugRun   = true,                 // run timer in debug mode
    .prescale   = timerPrescale1,       // set prescaler to /1
    .clkSel     = timerClkSelHFPerClk,  // set clock source as HFPERCLK
    .fallAction = timerInputActionNone, // no action from inputs
    .riseAction = timerInputActionNone, // no action from inputs
    .mode       = timerModeUp,          // use up-count mode
    .dmaClrAct  = false,                // not using DMA
    .quadModeX4 = false,                // not using Quad Dec. mode
    .oneShot    = false,                // not using one shot mode
    .sync       = false,                // not syncronizing timer3 with other timers
  };
   
  TIMER_TopSet(TIMER0, TOP_VAL_GP_TIMER);      // GP Timer period will be 1ms = 1kHz freq
  TIMER_TopSet(TIMER1, TOP_VAL_PWM);           // PWM period will be 1ms = 1kHz freq
   
  TIMER_CounterSet(TIMER0, 0);                 // start counter at 0 (up-count mode)
  TIMER_CounterSet(TIMER1, 0);                 // start counter at 0 (up-count mode)
   
  TIMER_CompareSet(TIMER1, 0, compare_val);    // set CC0 compare value (0% duty)
  TIMER_CompareBufSet(TIMER1, 0, compare_val); // set CC0 compare buffer value (0% duty)
   
  TIMER_IntEnable(TIMER0, TIMER_IF_OF);        // enable Timer0 overflow interrupt
  NVIC_EnableIRQ(TIMER0_IRQn);                 // enable Timer0 interrupt vector in NVIC
   
  TIMER_InitCC(TIMER1, 0, &timerCCInit);       // apply channel configuration to Timer1 channel 0 (PE10)
  TIMER1->ROUTE = (1 << 16) |(1 << 0);         // connect PWM output (timer1, channel 0) to PE10 (LED0). See EFM32ZG222 datasheet for details.
     
  TIMER_Init(TIMER0, &timerGPInit);            // apply general purpose configuration to timer0
  TIMER_Init(TIMER1, &timerPWMInit);           // apply PWM configuration to timer1
   
  while(1) {
    if(ms_counter == UPDATE_PERIOD) {
      if(inc) {                                    // if increment = true
        compare_val += INC_VAL;                    // increase the compare value
      }else{                                       // if increment = false
        compare_val -= INC_VAL;                    // decrease the compare value
      }
      TIMER_CompareBufSet(TIMER1, 0, compare_val); // write new value to compare buffer
      GPIO_PinOutToggle(LED_PORT, LED1_PIN);       // toggle LED1 to indicate an update event on LED0
      ms_counter = 0;                              // reset counter
    }
    if(compare_val > (TOP_VAL_PWM-1)) { inc = 0; } // if compare value is at max, start decrementing
    if(compare_val < 1) { inc = 1; }               // if compare value is at min, start incrementing
  }
}
  • Universal Asynchronous Receiver/Transmitter (UART) Example
    This example shows how to setup USART1 in asynchronous mode (UART) for basic serial communication. UART communication is very popular in embedded designs due to the low number of required pins (Transmit Data line, Receive Data line, and Ground). There are even some “single-wire” UARTs available (shared TX/RX line), however these still require a common ground to operate properly. UARTs work asynchronously, meaning there is no dedicated clock line between the 2 devices. Therefore, each device needs to have a predefined matching data rate and frame format to actually communicate.
    The data rate is defined by the clock for each individual device. Any error in the clock rate will effect the baud rate of the UART and could cause loss of sync between the devices. Because of this, it’s normally recommended to use a high accuracy clock source such as a crystal oscillator. An RC oscillator may work, although it should involve some sort of calibration process to increase accuracy.
    For this demo, the Zero Gecko was setup to use a 24MHz high frequency crystal oscillator as the system clock source. The UART port was setup with the following serial communication settings:
  • Baud Rate (bps): 115200
  • Data Bits: 8
  • Parity: none
  • Stop Bits: 1
    A terminal emulator program (Tera Term) was used to display characters transmitted to and from the Zero Gecko. The serial port of the terminal program was setup with the following settings:
  • Baud Rate (bps): 115200
  • Data Bits: 8
  • Parity: none
  • Stop Bits: 1
  • Flow Control: none
    The following figure shows the printed characters in the terminal window while running the applications found below.
    image

UART Example Using Direct Register Access
The following example code shows how to use direct register access to implement this simple UART application. For the following code to work, the em_system.c file needs to be added to the workspace.

#include "em_device.h"
#include "em_system.h"
#include "em_chip.h"
 
#define COM_PORT 2 // PORTC
#define TX_PIN   0
#define RX_PIN   1
 
int main () {
   
  CHIP_Init();
   
  uint8_t i;
  const char test_string[] = "\n\rHello World!\n\r";
  char rx_char = 0;
   
  CMU->OSCENCMD |= 0x4;                          // enable XTAL oscillator
  while(!(CMU->STATUS & 0x8));                   // wait for XTAL osc to stabilize
  CMU->CMD = 0x2;                                // select HF XTAL osc as system clock source (24MHz)
   
  CMU->HFPERCLKEN0 = (1 << 7) | (1 << 3);        // enable GPIO and USART1 peripheral clocks
   
  GPIO->P[COM_PORT].MODEL = (1 << 4) | (4 << 0); // configure PC0 as output and PC1 as input
  GPIO->P[COM_PORT].DOUTSET = (1 << TX_PIN);     // initialize PC0 high
   
  USART1->CLKDIV = (48 << 6);                                // 115200 baud
  USART1->CMD = (1 << 11) | (1 << 10) | (1 << 2) | (1 << 0); // clear buffers, enable transmitter and receiver
  USART1->IFC = 0x1FF9;                                      // clear all USART interrupt flags
  USART1->ROUTE = 0x3;                                       // enable RX/TX pins
   
  for(i=0; i<sizeof(test_string); i++) {
    while(!(USART1->STATUS & (1 << 6))); // wait for TX buffer to empty
    USART1->TXDATA = test_string[i];     // send character
  }
   
  while(1) {
    if(USART1->STATUS & (1 << 7)) {      // if RX buffer contains valid data
      rx_char = USART1->RXDATA;          // store the data
    }
    if(rx_char) {                        // if we have a valid character
      if(USART1->STATUS & (1 << 6)) {    // check if TX buffer is empty
        USART1->TXDATA = rx_char;        // echo received character
        rx_char = 0;                     // reset temp variable
      }
    }
  }
}

UART Example Using emlib API
The following example code shows how to implement the UART example using emlib functions. For the following code to work, the following files need to be added to the workspace:

  • em_cmu.c
  • em_emu.c
  • em_gpio.c
  • em_system.c
  • em_usart.c
#include "em_device.h"
#include "em_system.h"
#include "em_chip.h"
#include "em_cmu.h"
#include "em_gpio.h"
#include "em_usart.h"
 
#define COM_PORT gpioPortC
#define TX_PIN   0
#define RX_PIN   1
 
const char test_string[] = "\n\rHello World!\n\r";
char rx_char;
 
int main () {
   
  uint8_t i;
  CHIP_Init();
   
  CMU_OscillatorEnable(cmuOsc_HFXO, true, true);          // enable HF XTAL osc and wait for it to stabilize
  CMU_ClockSelectSet(cmuClock_HF, cmuSelect_HFXO);        // select HF XTAL osc as system clock source (24MHz)
   
  CMU_ClockEnable(cmuClock_GPIO, true);                   // enable GPIO peripheral clock
  CMU_ClockEnable(cmuClock_USART1, true);                 // enable USART1 peripheral clock
   
  GPIO_PinModeSet(COM_PORT, TX_PIN, gpioModePushPull, 1); // set TX pin to push-pull output, initialize high (otherwise glitches can occur)
  GPIO_PinModeSet(COM_PORT, RX_PIN, gpioModeInput, 0);    // set RX pin as input (no filter)
   
  USART_InitAsync_TypeDef uartInit =
  {
    .enable = usartDisable,     // wait to enable transmitter and receiver
    .refFreq = 0,               // setting refFreq to 0 will invoke the CMU_ClockFreqGet() function and measure the HFPER clock
    .baudrate = 115200,         // desired baud rate
    .oversampling = usartOVS16, // set oversampling to x16
    .databits = usartDatabits8, // 8 data bits
    .parity = usartNoParity,    // no parity bits
    .stopbits = usartStopbits1, // 1 stop bit
    .mvdis = false,             // use majority voting
    .prsRxEnable = false,       // not using PRS input
    .prsRxCh = usartPrsRxCh0,   // doesn't matter what channel we select
  };
  USART_InitAsync(USART1, &uartInit);      // apply configuration to USART1
  USART1->ROUTE = USART_ROUTE_RXPEN | USART_ROUTE_TXPEN | _USART_ROUTE_LOCATION_LOC0; // clear buffers, enable transmitter and receiver pins
   
  USART_IntClear(USART1, _USART_IF_MASK);  // clear all USART interrupt flags
  NVIC_ClearPendingIRQ(USART1_RX_IRQn);    // clear pending RX interrupt flag in NVIC
  NVIC_ClearPendingIRQ(USART1_TX_IRQn);    // clear pending TX interrupt flag in NVIC
   
  USART_Enable(USART1, usartEnable);       // enable transmitter and receiver
   
  for(i=0; i<sizeof(test_string); i++) {
    while(!(USART1->STATUS & (1 << 6)));   // wait for TX buffer to empty
    USART1->TXDATA = test_string[i];       // send character
  }
     
  while(1) {
    if(USART1->STATUS & (1 << 7)) {        // if RX buffer contains valid data
      rx_char = USART1->RXDATA;            // store the data
    }
    if(rx_char) {                          // if we have a valid character
      while(!(USART1->STATUS & (1 << 6))); // wait for TX buffer to empty
      USART1->TXDATA = rx_char;            // echo received char
      rx_char = 0;                         // clear temp variable
    }
  }
}
  • Low Energy Universal Asynchronous Receiver/Transmitter (LEUART) Example
    The following example demonstrates the same UART example from above. However, this time, the Low Energy UART was used as the communication port. The LEUART is special because it can run asynchronously to the core, which means it can run in some sleep modes. With a 32.768kHz crystal as the clock source, it can achieve baud rates up to 9600 bps while using very little power. Optionally, the LEUART can be clocked from the HF Core clock to achieve much higher baud rates. However, it would no longer be an asynchronous peripheral and therfore could not be used in sleep mode.
    For this demo, the Zero Gecko was setup to use a 32.768kHz crystal oscillator as the LEUART clock source. The LEUART port was setup with the following serial communication settings:
  • Baud Rate (bps): 9600
  • Data Bits: 8
  • Parity: none
  • Stop Bits: 1
    LEUART Location #3 (PF0, PF1) was used for TX/RX.
    A terminal emulator program (Tera Term) was used to display characters transmitted to and from the Zero Gecko. The serial port of the terminal program was setup with the following settings:
  • Baud Rate (bps): 9600
  • Data Bits: 8
  • Parity: none
  • Stop Bits: 1
  • Flow Control: none

The following figure shows the printed characters in the terminal window while running the applications found below.

LEUART Example Using Direct Register Access
The following code demonstrates the LEUART example using direct register access. For the following code to work the em_system.c file needs to be added to the workspace.

#include "em_device.h"
#include "em_system.h"
#include "em_chip.h"
 
#define LEUART_PORT   5  // PORTF - LEUART (location #3)
#define LEUART_TX_PIN 0  // PF0
#define LEUART_RX_PIN 1  // PF1
 
const char test_string[] = "\n\rHello World!\n\r";
 
int main () {
   
  CHIP_Init();
   
  uint8_t i;
  char rx_char = 0;
   
  CMU->OSCENCMD |= (1 << 8);          // enable LFXO
  while(!(CMU->STATUS & (1 << 9)));   // wait for LF XTAL osc to stabilize
  CMU->LFCLKSEL = (2 << 2);           // select LFXO as clock source to LFB
  CMU->LFBCLKEN0 |= 0x1;              // enable LEUART peripheral clock
  CMU->HFCORECLKEN0 = (1 << 2);       // enable the Low Energy Peripheral Interface clock
  CMU->HFPERCLKEN0 = (1 << 7);        // enable GPIO peripheral clock
   
  GPIO->P[LEUART_PORT].MODEL = (1 << 4) | (4 << 0);    // configure PF0 as output and PF1 as input
  GPIO->P[LEUART_PORT].DOUTSET = (1 << LEUART_TX_PIN); // initialize PF0 high
   
  LEUART0->CLKDIV = 618;                            // 9600 baud with 32.768kHz source clock
  while(LEUART0->SYNCBUSY & (1 << 2));              // wait for CLKDIV reg write to complete
  LEUART0->CMD = (1<<7) | (1<<6) | (1<<2) | (1<<0); // clear buffers, enable TX/RX
  while(LEUART0->SYNCBUSY & (1 << 1));              // wait for CMD reg write to complete
  LEUART0->IFC = (0xFF << 3) | 0x1;                 // clear interrupt flags
  LEUART0->ROUTE = (3 << 8) | 0x3;                  // use Location #3 (PF0, PF1)
   
  for(i=0; i<sizeof(test_string); i++) {
    while(!(LEUART0->STATUS & (1 << 4))); // wait for TX buffer to empty
    LEUART0->TXDATA = test_string[i];     // send character
    while(LEUART0->SYNCBUSY & (1 << 6));  // wait for TXDATA reg write to complete
  }
   
  while(1) {
    if(LEUART0->STATUS & (1 << 5)) {        // if RX buffer contains valid data
      rx_char = LEUART0->RXDATA;            // store the data
    }
    if(rx_char) {                           // if we have a valid character
      while(!(LEUART0->STATUS & (1 << 4))); // wait for TX buffer to empty
      LEUART0->TXDATA = rx_char;            // echo received character
      rx_char = 0;                          // reset temp variable
    }
  }
}

LEUART Example Using emlib API
The following code executes the LEUART demo with the prewritten emlib API. For the code to work, the following files need to be added to the workspace:

  • em_cmu.c
  • em_emu.c
  • em_gpio.c
  • em_leuart.c
  • em_system.c
#include "em_device.h"
#include "em_system.h"
#include "em_chip.h"
#include "em_cmu.h"
#include "em_gpio.h"
#include "em_leuart.h"
 
#define LEUART_PORT   gpioPortF // PORTF - LEUART (location #3)
#define LEUART_TX_PIN 0         // PF0
#define LEUART_RX_PIN 1         // PF1
 
const char test_string[] = "\n\rHello World!\n\r";
 
int main () {
   
  CHIP_Init();
   
  uint8_t i;
  char rx_char = 0;
   
  CMU_OscillatorEnable(cmuOsc_LFXO, true, true);    // enable LFXO and wait for it to stabilize
  CMU_ClockSelectSet(cmuClock_LFB, cmuSelect_LFXO); // select LFXO as clock source to LFB
  CMU_ClockEnable(cmuClock_LEUART0, true);          // enable LEUART peripheral clock
  CMU_ClockEnable(cmuClock_CORELE, true);           // enable the Low Energy Peripheral Interface clock
  CMU_ClockEnable(cmuClock_GPIO, true);             // enable GPIO peripheral clock
   
  GPIO_PinModeSet(LEUART_PORT, LEUART_TX_PIN, gpioModePushPull, 1); // configure PF0 as output, initialize high
  GPIO_PinModeSet(LEUART_PORT, LEUART_RX_PIN, gpioModeInput, 0);    // configure PF1 as input, no filter
   
  LEUART_IntClear(LEUART0, (0xFF << 3) | 0x1); // clear interrupt flags
  LEUART0->ROUTE = (3 << 8) | 0x3;             // use Location #3 (PF0, PF1)
   
  LEUART_Init_TypeDef leuart_init =
  {
    .enable   = leuartEnable,    // enable Receiver and Transmitter
    .refFreq  = 0,               // measure reference clock
    .baudrate = 9600,            // 9600 bps is max baud rate when using 32.768kHz LFXO clock source
    .databits = leuartDatabits8, // 8 data bits
    .parity   = leuartNoParity,  // no parity bits
    .stopbits = leuartStopbits1, // 1 stop bit
  };
   
  LEUART_Init(LEUART0, &leuart_init); // configure Low Energy UART and enable TX/RX
   
  /* print test string */
  /* LEUART_Tx waits for the TX hardware buffer to empty before writing new data */
  for(i=0; i<sizeof(test_string); i++) {
    LEUART_Tx(LEUART0, test_string[i]);
  }
   
  while(1) {
    rx_char = LEUART_Rx(LEUART0);  // get character from RX hardware buffer
                                   // LEUART_Rx waits until RX hardware buffer has valid data
    if(rx_char) {                  // if we have a valid character
      LEUART_Tx(LEUART0, rx_char); // echo received character
      rx_char = 0;                 // reset temp variable
    }
  }
}
  • Analog to Digital Converter (ADC) Example
    This example shows how to setup the 12-bit Analog to Digital Converter (ADC) in the Zero Gecko.
    A potentiometer was connected to PORTD, pin 4 and used as the analog source for the ADC. The potentiometer provided a controllable analog voltage source varying between 0~1.5V with the center tap connected to PD4 (ADC0 Channel 4) of the Zero Gecko. VDD (3.3V) was used as the reference voltage of the ADC. The ADC was setup in single ended mode and provided single conversions at a predefined interval. The results were set to 12-bit, right-justified within the 16-bit result register. The ADC clock was set to 12MHz with x2 oversampling enabled. An input filter was not implemented for this demo.
    After the ADC completed each conversion, USART1 was used to transmit the results to a terminal program on a PC. USART1 was setup in asynchronous mode with the same serial communication settings as the “UART Example” shown above.
    To provide a high accuracy clock for UART communication, a 24MHz crystal oscillator was used with a to generate a 24MHz System Clock, just like the “HF XTAL Oscillator” example above.
    TIMER0 was used as a general purpose timer to trigger single ADC conversions every 500ms, similar to the “Timer with Interrupt” example above. The potentiometer value was updated onscreen every half second.
    Below is a screenshot of the terminal window while running this demo application.
    image

ADC Example Using Direct Register Access
The following example code demonstrates how to setup the ADC in single conversion mode using direct register access. For the following code to work, the em_system.c file needs to be added to the workspace.

#include "em_device.h"
#include "em_system.h"
#include "em_chip.h"
 
#define COM_PORT 2 // PORTC (USART1 location #0: PC0 and PC1
#define ADC_PORT 3 // PORTD (ADC Channel 4 location #0: PD4
#define TX_PIN   0 // PC0
#define RX_PIN   1 // PC1
#define ADC_PIN  4 // PD4
 
void set_decade(uint16_t val);
uint8_t conv_ascii(uint16_t val);
 
volatile uint16_t ms_counter     = 0;
const    char     header[]       = "\n\rEFM32 Zero Gecko - ADC Example\n\r";   // header string
volatile uint8_t  digit_array[7] = { 0x20, 0x20, 0x20, 0x20, 0x20, 'm', 'V' }; // array for displaying ADC result
 
void TIMER0_IRQHandler(void) {
  TIMER0->IFC = 1; // clear the overflow flag
  ms_counter++;    // increment counter
}
 
int main () {
   
  CHIP_Init();
   
  uint8_t  i;
  uint32_t adc_result = 0; // temp variable for storing ADC conversion results
   
  /* Initialize Clock Tree */
  CMU->OSCENCMD |= 0x4;          // enable XTAL Oscillator
  while(! (CMU->STATUS & 0x8) ); // wait for XTAL osc to stabilize
  CMU->CMD = 0x2;                // select HF XTAL osc as system clock source (24MHz)
  CMU->HFPERCLKEN0 = (1 << 10) | (1 << 7) | (1 << 3) | (1 << 0); // Enable GPIO, TIMER0, USART1, and ADC0 peripheral clocks
   
  /* Initialize GPIO */
  GPIO->P[COM_PORT].MODEL = (1 << 4) | (4 << 0); // set PC0 as push-pull output, PC1 as input
  GPIO->P[COM_PORT].DOUTSET = (1 << TX_PIN);     // initialze PC0 high (otherwise glitches can occur)
  GPIO->P[ADC_PORT].MODEL = (1 << 16);           // set PD4 as input
   
  /* Setup USART port for asynch mode, frame format 8-none-1-none */
  USART1->CLKDIV = (48 << 6);                                // 48 will give 115200 baud rate (using 16-bit oversampling with 24MHz peripheral clock)
  USART1->CMD = (1 << 11) | (1 << 10) | (1 << 2) | (1 << 0); // Clear RX/TX buffers and shif regs, Enable Transmitter and Receiver
  USART1->IFC = 0x1FF9;                                      // clear all USART interrupt flags
  USART1->ROUTE = (1 << 1) | (1 << 0);                       // Enable TX and RX pins, use location #0 (UART TX and RX located at PC0 and PC1)
   
  /* Setup ADC */
  // Timebase bit field = 24, defines ADC warm up period (must be greater than or equal to number of clock cycles in 1us)
  // Prescaler setting = 1: ADC clock = HFPERCLK/2 = 12MHz (ADC clock should be between 32kHz and 13MHz)
  // Oversampling set to 2, no input filter, no need for Conversion Tailgating
  // Warm-up mode = NORMAL (ADC is not kept warmed up between conversions)
  ADC0->CTRL = (24 << 16) | (1 << 8);
    
  // Don't use PRS as input
  // Can use single-cycle acquisition time since we are spacing out our conversions using a timer
  // Use buffered Vdd as reference, use Channel 6 as input to single conversion
  // 12-bit resolution, right-justified, single-ended input, single conversion
  ADC0->SINGLECTRL = (2 << 16) | (4 << 8);
  ADC0->IEN = 0x0; // Disable ADC interrupts
    
  /* Setup TIMER0 */
  TIMER0->TOP = 24000;                     // set TOP value for TIMER0
  TIMER0->IEN = 1;                         // enable TIMER0 overflow interrupt
  NVIC_EnableIRQ(TIMER0_IRQn);             // enable TIMER0 interrupt vector in NVIC
  TIMER0->CMD = 0x1;                       // start TIMER0
    
  // Print Startup Header
  i=0;
  while(header[i] != 0) {
    while( !(USART1->STATUS & (1 << 6)) ); // wait for TX buffer to empty
    USART1->TXDATA = header[i++];          // print each character of the header
  }
   
  while(1) {
    if(500 == ms_counter) {
        
      // Move cursor to start of line
      while( !(USART1->STATUS & (1 << 6)) ); // wait for TX buffer to empty
      USART1->TXDATA = 0x0D;                 // send carriage return
       
      ADC0->CMD = 0x1;                       // Start Single Conversion
      while(!(ADC0->STATUS & (1 << 16)));    // Wait for single conversion data to become valid
      adc_result = ADC0->SINGLEDATA;         // Store conversion result
        
      // Change hex result to decimal result in mV
      adc_result = adc_result*3300;
      adc_result = adc_result/4095;
      set_decade(adc_result & 0xFFFF); // Divide result into individual characters to be transmitted over UART
        
      // Transmit result characters over UART
      for(i=0; i<7; i++) {
        while( !(USART1->STATUS & (1 << 6)) ); // wait for TX buffer to empty
        USART1->TXDATA = digit_array[i];       // print each character of the test string
      }
        
      ms_counter = 0; // reset counter
    }
  }
}
  
// This function is used to divide a 16-bit value into 5 individual digits
void set_decade(uint16_t val)
{
  uint16_t tmp;
  tmp = val / 10000;
  if (tmp) {
    digit_array[0] = conv_ascii(tmp);
  } else
  digit_array[0] = ' ';
  val = val - tmp*10000;
  tmp = val / 1000;
  if (tmp) {
    digit_array[1] = conv_ascii(tmp);
  } else
  digit_array[1] = ' ';
  val = val - tmp*1000;
  tmp = val / 100;
  if (tmp || digit_array[1]!=' ') {
    digit_array[2] = conv_ascii(tmp);
  } else digit_array[2] = ' ';
  val = val - tmp*100;
  tmp = val / 10;
  if (tmp  || digit_array[2]!=' ' || digit_array[1]!=' ') {
    digit_array[3] = conv_ascii(tmp);
  } else digit_array[3] = ' ';
  val = val - tmp*10;
  digit_array[4] = conv_ascii(val);
}
  
// This function is used to covert the character to its ASCII value.
uint8_t conv_ascii(uint16_t val)
{
  if (val<= 0x09) {
    val = val + 0x30;
  }
  if ((val >= 0x0A) && (val<= 0x0F)) {
    val = val + 0x37;
  }
  return val;
}

ADC Example Using emlib API
The following example code executes the same demo using the emlib API functions. For the following code to work, the following files need to be added to the workspace:

  • em_cmu.c
  • em_emu.c
  • em_system.c
  • em_gpio.c
  • em_timer.c
  • em_usart.c
  • em_adc.c
#include "em_device.h"
#include "em_system.h"
#include "em_chip.h"
#include "em_cmu.h"
#include "em_gpio.h"
#include "em_usart.h"
#include "em_adc.h"
#include "em_timer.h"
 
#define COM_PORT gpioPortC // PORTC (USART1 location #0: PC0 and PC1
#define ADC_PORT gpioPortD // PORTD (ADC Channel 4 location #0: PD4
#define TX_PIN        0    // PC0
#define RX_PIN        1    // PC1
#define ADC_PIN       4    // PD4
#define SAMPLE_PERIOD 500  // ms
 
void set_decade(uint16_t val);
uint8_t conv_ascii(uint16_t val);
 
volatile uint16_t ms_counter     = 0;
const    char     header[]       = "\n\rEFM32 Zero Gecko - ADC Example\n\r";   // header string
volatile uint8_t  digit_array[7] = { 0x20, 0x20, 0x20, 0x20, 0x20, 'm', 'V' }; // array for displaying ADC result
 
void TIMER0_IRQHandler(void) {
  TIMER_IntClear(TIMER0, TIMER_IF_OF); // clear the overflow flag
  ms_counter++;                        // increment counter
}
 
int main () {
   
  CHIP_Init();
   
  uint8_t  i = 0;
  uint32_t adc_result = 0; // temp variable for storing ADC conversion results
   
  /* Initialize Clock Tree */
  CMU_OscillatorEnable(cmuOsc_HFXO, true, true);            // enable XTAL osc and wait for it to stabilize
  CMU_ClockSelectSet(cmuClock_HF, cmuSelect_HFXO);          // select HF XTAL osc as system clock source (24MHz)
  if(cmuSelect_HFRCO != CMU_ClockSelectGet(cmuClock_HF)) {  // make sure HFRCO is no longer the selected clock source
    CMU_OscillatorEnable(cmuOsc_HFRCO, false, false);       // disable it to save power
  }
    
  CMU_ClockEnable(cmuClock_GPIO, true);                     // enable GPIO peripheral clock
  CMU_ClockEnable(cmuClock_USART1, true);                   // enable USART1 peripheral clock
  CMU_ClockEnable(cmuClock_ADC0, true);                     // enable ADC0 peripheral clock
  CMU_ClockEnable(cmuClock_TIMER0, true);                   // enable TIMER0 peripheral clock
   
  /* Initialize GPIO */
  GPIO_PinModeSet(COM_PORT, TX_PIN, gpioModePushPull, 1); // set TX pin as push-pull output (initialize high)
  GPIO_PinModeSet(COM_PORT, RX_PIN, gpioModeInput, 0);    // set RX pin as input (no filter or pull resistor)
  GPIO_PinModeSet(ADC_PORT, ADC_PIN, gpioModeInput, 0);   // set ADC input pin as input (no filter or pull resistor)
   
  /* Setup USART port for asynch mode, frame format 8-none-1-none */
  USART_InitAsync_TypeDef uartInit =
  {
    .enable = usartDisable,     // wait to enable transmitter and receiver
    .refFreq = 0,               // setting refFreq to 0 will invoke the CMU_ClockFreqGet() function and measure the HFPER clock
    .baudrate = 115200,         // desired baud rate
    .oversampling = usartOVS16, // set oversampling to x16
    .databits = usartDatabits8, // 8 data bits
    .parity = usartNoParity,    // no parity bits
    .stopbits = usartStopbits1, // 1 stop bit
    .mvdis = false,             // use majority voting
    .prsRxEnable = false,       // not using PRS input
    .prsRxCh = usartPrsRxCh0,   // doesn't matter what channel we select
  };
  USART_InitAsync(USART1, &uartInit);      // apply configuration to USART1
  USART1->ROUTE = USART_ROUTE_RXPEN | USART_ROUTE_TXPEN | _USART_ROUTE_LOCATION_LOC0; // clear buffers, enable transmitter and receiver pins
    
  USART_IntClear(USART1, _USART_IF_MASK);  // clear all USART interrupt flags
  NVIC_ClearPendingIRQ(USART1_RX_IRQn);    // clear pending RX interrupt flag in NVIC
  NVIC_ClearPendingIRQ(USART1_TX_IRQn);    // clear pending TX interrupt flag in NVIC
    
  USART_Enable(USART1, usartEnable);       // enable transmitter and receiver
   
  /* Setup ADC */
  ADC_Init_TypeDef adcCommonInit =
  {
    .ovsRateSel = adcOvsRateSel2,              // lowest oversampling option
    .lpfMode = adcLPFilterBypass,              // no input filter for ADC
    .warmUpMode = adcWarmupNormal,             // ADC is not kept warmed up between conversions
    .timebase = ADC_TimebaseCalc(0),           // calculate the ADC timebase to generate 1us warm-up time
    .prescale = ADC_PrescaleCalc(12000000, 0), // 12MHz ADC clock derived from HFCLK
    .tailgate = false,                         // no need for conversion tailgating
  };
   
  ADC_InitSingle_TypeDef adcSingleInit =
  {
    .prsSel = adcPRSSELCh0,    // not using PRS so this option doesn't matter
    .acqTime = adcAcqTime1,    // 1 clock cycle for acquisition
    .reference = adcRefVDD,    // use buffered Vdd as reference
    .resolution = adcRes12Bit, // 12-bit resolution
    .input = adcSingleInpCh4,  // use Channel 4 as input
    .diff = false,             // use single-ended mode instead of differential
    .prsEnable = false,        // not using PRS
    .leftAdjust = false,       // right-justify result
    .rep = false,              // deactivate conversion after one scan
  };
   
  ADC_Init(ADC0, &adcCommonInit);       // apply global settings to ADC
  ADC_InitSingle(ADC0, &adcSingleInit); // apply settings to single channel
  ADC_IntDisable(ADC0, _ADC_IF_MASK);   // disable ADC interrupts
   
  /* Setup Timer */
  TIMER_Init_TypeDef timer0_init =
  {
    .enable     = true,                 // start timer upon configuration
    .debugRun   = true,                 // keep timer running even on debug halt
    .prescale   = timerPrescale1,       // use /1 prescaler; TIMER0 clock = HF clock = 24MHz
    .clkSel     = timerClkSelHFPerClk,  // set HF peripheral clock as clock source
    .fallAction = timerInputActionNone, // no action on falling edge
    .riseAction = timerInputActionNone, // no action on rising edge
    .mode       = timerModeUp,          // use up-count mode
    .dmaClrAct  = false,                // not using DMA
    .quadModeX4 = false,                // not using quad decoder
    .oneShot    = false,                // using continuous mode, not one-shot
    .sync       = false,                // not synchronizing with other timers
  };
  TIMER_TopSet(TIMER0, 24000);          // set TIMER0 top value
  TIMER_IntEnable(TIMER0, TIMER_IF_OF); // enable TIMER0 overflow interrupt
  NVIC_EnableIRQ(TIMER0_IRQn);          // enable TIMER0 interrupt vector in NVIC
  TIMER_Init(TIMER0, &timer0_init);     // configure and start TIMER0
   
  /* Print Startup Header */
  while(!(NULL == header[i])) {
    while(!(USART1->STATUS & (1 << 6))); // wait for TX buffer to empty
    USART1->TXDATA = header[i++];        // print each character of the header string
  }
   
  while(1) {
    if(SAMPLE_PERIOD == ms_counter) {    // if the sample period has elapsed
        
      // Move cursor to start of line
      while( !(USART1->STATUS & (1 << 6)) ); // wait for TX buffer to empty
      USART1->TXDATA = 0x0D;                 // send carriage return
       
      ADC_Start(ADC0, adcStartSingle);            // start Single Conversion
      while(ADC0->STATUS & ADC_STATUS_SINGLEACT); // wait for single conversion data to become valid
      adc_result = ADC_DataSingleGet(ADC0);       // store conversion result
        
      // Change hex result to decimal result in mV
      adc_result = adc_result*3300;
      adc_result = adc_result/4095;
      set_decade(adc_result & 0xFFFF); // Divide result into individual characters to be transmitted over UART
        
      // Transmit result characters over UART
      for(i=0; i<7; i++) {
        while( !(USART1->STATUS & (1 << 6)) ); // wait for TX buffer to empty
        USART1->TXDATA = digit_array[i];       // print each character of the test string
      }
        
      ms_counter = 0; // reset counter
    }
  }
}
// This function is used to divide a 16-bit value into 5 individual digits
void set_decade(uint16_t val)
{
  uint16_t tmp;
  tmp = val / 10000;
  if (tmp) {
    digit_array[0] = conv_ascii(tmp);
  } else
  digit_array[0] = ' ';
  val = val - tmp*10000;
  tmp = val / 1000;
  if (tmp) {
    digit_array[1] = conv_ascii(tmp);
  } else
  digit_array[1] = ' ';
  val = val - tmp*1000;
  tmp = val / 100;
  if (tmp || digit_array[1]!=' ') {
    digit_array[2] = conv_ascii(tmp);
  } else digit_array[2] = ' ';
  val = val - tmp*100;
  tmp = val / 10;
  if (tmp  || digit_array[2]!=' ' || digit_array[1]!=' ') {
    digit_array[3] = conv_ascii(tmp);
  } else digit_array[3] = ' ';
  val = val - tmp*10;
  digit_array[4] = conv_ascii(val);
}
  
// This function is used to convert the character to its ASCII value.
uint8_t conv_ascii(uint16_t val)
{
  if (val<= 0x09) {
    val = val + 0x30;
  }
  if ((val >= 0x0A) && (val<= 0x0F)) {
    val = val + 0x37;
  }
  return val;
}
  • Serial Peripheral Interface (SPI) Example
    This example shows how to setup the Serial Peripheral Interface (SPI) in the Zero Gecko. The flash memory IC loaded on the board was used as the SPI target (see reference design section above). The flash IC SPI port (MOSI, MISO, SCLK, CS) was connected to USART1 (Location #3). USART1 was configured in synchronous (SPI) master mode with the SCLK idle low, data setup on trailing edge of SCLK, and data sampled on the rising edge of SCLK. The USART1 clock divider was left at 0 which generated a SCLK frequency of 12MHz. Due to the requirements of the flash IC, USART1 was setup to transmit msb first and the CS pin was handled manually (although the Zero Gecko does offer automatic CS functionality). The flash IC also has active low “Hold” and “Write Protect” pins connected to the Zero Gecko. However, these pins are not used in this example, except to drive the “Hold” pin high and the “Write Protect” pin low, which enables a read access only.
    For this demo, a simple ‘Device ID’ command was sent to the flash IC which returns a 2-byte response – the Manufacturer ID and the Device ID. For the W25X40CL, these values were 0xEF and 0x12, respectively.
    After the response was received, LEUART0 was used to transmit the response to a terminal program on a PC. Unlike the LEUART example above, this time the LEUART0 was setup as a high speed peripheral using the HFCORECLK/2 as the clock source. With a high speed clock source, much higher baud rates are achievable. A common baud rate of 115200 bps was used for this demo. The LFBPRESC0 register was used to scale the system clock down to create a 3MHz clock source for the LEUART. To provide a high accuracy clock for UART communication, the 24MHz crystal oscillator was used to generate a 24MHz System Clock, just like the “HF XTAL Oscillator” example above. With a 3MHz clock source, 115200 baud is achievable with 0.005% error, plus any slop from the crystal oscillator (±50 ppm).
    The example code includes some low level SPI driver functions like:
CS_pin_clr();    // assert chip-select pin
CS_pin_set();    // de-assert chip-select pin
SPI_WriteByte(); // transmit single data byte over SPI
SPI_ReadByte();  // read single data byte over SPI

Note: In full-duplex mode, a dummy byte must be placed in the transmitter in order to receive a byte from the slave. When you use the USART in SPI mode, you have a couple options for checking to see if all of the received data bits have been shifted in from the slave. You can use the RXDATAV bit in the USART STATUS register to indicate valid data exists in the receive buffer, which is what this example uses. Alternatively, since the transmitter is actually used to initiate reception, you could also use the TXC bit within the USART STATUS register to indicate all of the dummy bits have been shifted out from the master. The latter is only a valid option when using the USART in full-duplex SPI master mode.
The following image shows a screenshot of the terminal window while running this application. It’s displaying the Manufacturer and Device ID as it should (0xEF and 0x12). The extra characters above the ID strings are due to the fact that I used the UART bootloader to program the chip. Those characters can be disregarded. Also, since I used the UART bootloader to program the device and used the same UART port for communication during my application (LEUART0), I needed to reset the LEUART peripheral at the beginning of my main() function. If you are using an external debugger to program the device, you won’t need to reset the LEUART peripheral.

SPI Example Using Direct Register Access
The following example code demonstrates SPI functionality using direct register access. For the following code to work, the em_system.c file needs to be added to the workspace.

#include "em_device.h"
#include "em_system.h"
#include "em_chip.h"
 
#define SPI_PORTD     3  // PORTD - USART1 (location #3) MISO and MOSI are on PORTD
#define SPI_PORTC     2  // PORTC - USART1 (location #3) SS and SCLK are on PORTC
#define SPI_MISO_PIN  6  // PD6
#define SPI_MOSI_PIN  7  // PD7
#define SPI_CS_PIN    14 // PC14
#define SPI_SCLK_PIN  15 // PC15
#define SPI_nHLD_PIN  13 // PC13
#define SPI_nWP_PIN   11 // PC11
 
#define LEUART_PORT   5  // PORTF - LEUART (location #3)
#define LEUART_TX_PIN 0  // PF0
#define LEUART_RX_PIN 1  // PF1
 
#define LED_PORT     4   // PORTE
#define LED0_PIN     10  // PE10
 
#define RX_BUFFER_SIZE 2
 
/* SPI functions */
void CS_pin_clr(void);
void CS_pin_set(void);
void SPI_WriteByte(uint8_t byte);
uint8_t SPI_ReadByte(void);
 
/* UART functions */
void print_byte(uint8_t byte);
void print_byte_setup(uint8_t byte);
 
/* Global variables */
const char header[] = "\n\rManuf/Device ID\n\r";
uint8_t rx_buffer[RX_BUFFER_SIZE];
uint8_t print_byte_array[4] = { 0x30, 0x78, 0x20, 0x20 }; // array for displaying bytes one nibble at a time
 
int main () {
   
  CHIP_Init();
   
  uint8_t i;
   
  // Clear rx buffer
  for(i=0; i<RX_BUFFER_SIZE; i++) {
    rx_buffer[i] = 0;
  }
   
  /* Setup clock tree */
  /* Use 24MHz crystal as HF system clock source */
  /* Run Low Energy Peripheral B system as a high speed peripheral (use CORECLCK/2 as source) */
  CMU->OSCENCMD |= 0x4;                   // enable HF XTAL oscillator (24MHz)
  while(!(CMU->STATUS & 0x8));            // wait for HF XTAL osc to stabilize
  CMU->CMD = 0x2;                         // select HF XTAL osc as system clock source (24MHz)
  if(!(CMU->STATUS & (1 << 10))) {        // make sure HFRCO is no longer the selected clock
    CMU->OSCENCMD |= 0x2;                 // disable HFRCO to save power
  }
  CMU->LFCLKSEL = (3 << 2);               // select HFCORECLK/2 as clock source to LFB
  CMU->HFCORECLKEN0 = (1 << 2);           // enable the Low Energy Peripheral Interface clock
  CMU->LFBCLKEN0 = 0x1;                   // enable LEUART peripheral clock
  while(CMU->SYNCBUSY & (1 << 4));        // wait for LFBCLKEN0 reg write to complete
  CMU->LFBPRESC0 = 0x2;                   // set LFB prescaler to /4 (LEUART source clock will be 3MHz)
  while(CMU->SYNCBUSY & (1 << 6));        // wait for LFBPRESC0 reg write to complete
  CMU->HFPERCLKEN0 = (1 << 7) | (1 << 3); // enable GPIO and USART1 peripheral clocks
   
  /* Configure GPIO */ 
  GPIO->P[LED_PORT].MODEH = (4 << 8);                                       // set LED0 pin as push-pull output with standard drive strength
  GPIO->P[LEUART_PORT].MODEL = (1 << 4) | (4 << 0);                         // configure PF0 as output and PF1 as input
  GPIO->P[LEUART_PORT].DOUTSET = (1 << LEUART_TX_PIN);                      // initialize PF0 high
  GPIO->P[SPI_PORTD].MODEL = (4 << 28) | (1 << 24);                         // PD6 input, PD7 output
  GPIO->P[SPI_PORTC].MODEH = (4 << 28) | (4 << 24) | (4 << 20) | (4 << 12); // PC11, PC13, PC14, PC15 all outputs
  GPIO->P[SPI_PORTC].DOUTSET = (1 << SPI_CS_PIN) | (1 << SPI_nHLD_PIN);     // initialize CS, nHLD high
  GPIO->P[SPI_PORTC].DOUTCLR = (1 << SPI_nWP_PIN);                          // initialize nWP low
   
  /* Configure SPI Port (USART1 in sync mode) */
  USART1->CTRL = (1 << 10) | (1 << 0);                      // sync. mode, MSB first
  USART1->CLKDIV = 0;                                       // SPI baud will be at max value of HFPERCLK/2 (12 Mbps)
  USART1->IFC = 0x1FF9;                                     // clear all USART1 interrupt flags
  USART1->ROUTE = (3 << 8) | (1 << 3) | (3 << 0);           // use location #3, enable TX/RX/CLK pins
  USART1->CMD = (3 << 10) | (1 << 4) | (1 << 2) | (1 << 0); // clear RX/TX buffers, set to SPI Master, enable TX/RX
   
  /* Configure COM Port (LEUART0 asynch) */
  LEUART0->CLKDIV = 6411;                           // 115200 baud with 3MHz source clock (0.005% error)
  while(LEUART0->SYNCBUSY & (1 << 2));              // wait for CLKDIV reg write to complete
  LEUART0->CMD = (1<<7) | (1<<6) | (1<<2) | (1<<0); // clear buffers, enable TX/RX
  while(LEUART0->SYNCBUSY & (1 << 1));              // wait for CMD reg write to complete
  LEUART0->IFC = (0xFF << 3) | 0x1;                 // clear interrupt flags
  LEUART0->ROUTE = (3 << 8) | 0x3;                  // use Location #3 (PF0, PF1)
   
  /* Print Header */
  for(i=0; i<sizeof(header); i++) {
    while(!(LEUART0->STATUS & (1 << 4))); // wait for TX buffer to empty
    LEUART0->TXDATA = header[i];          // send character
    while(LEUART0->SYNCBUSY & (1 << 6));  // wait for TXDATA reg write to complete
  }
   
  /* Get Device ID from SPI Slave (see W25X40CL datasheet for details) */
  CS_pin_clr();                    // drive CS pin low
  SPI_WriteByte(0x90);             // send 'Device ID' command (see W25X40CL datasheet for details)
  rx_buffer[0] = SPI_ReadByte();   // clear RX buffer
  for(i=0; i<3; i++) {             // send 3 '00' bytes (see W25X40CL datasheet for details)
    SPI_WriteByte(0x00);
    rx_buffer[0] = SPI_ReadByte(); // clear RX buffer each time 
  }
  SPI_WriteByte(0xFF);             // send dummy byte to receive manuf. id
  rx_buffer[0] = SPI_ReadByte();   // store manuf id
  SPI_WriteByte(0xFF);             // send dummy byte to receive device id
  rx_buffer[1] = SPI_ReadByte();   // store device id
  CS_pin_set();                    // drive CS pin high
   
  /* Print response on-screen */
  print_byte_setup(rx_buffer[0]);    // print manuf id
  for(i=0; i<4; i++) {
    print_byte(print_byte_array[i]);
  }
  print_byte(0x20);                  // send whitespace
  print_byte_setup(rx_buffer[1]);    // print device id
  for(i=0; i<4; i++) {
    print_byte(print_byte_array[i]);
  }
  print_byte('\r');                  // send CR
  print_byte('\n');                  // send LF 
   
  while(1);
}
 
// This function drives the CS pin low
void CS_pin_clr(void) {
  GPIO->P[SPI_PORTC].DOUTCLR = (1 << SPI_CS_PIN);
}
 
// This function drives the CS pin high
void CS_pin_set(void) {
  GPIO->P[SPI_PORTC].DOUTSET = (1 << SPI_CS_PIN);
}
 
// This function writes a byte to the TX buffer (waits for buffer to clear)
void SPI_WriteByte(uint8_t byte) {
  while( !(USART1->STATUS & (1 << 6)) ); // wait for tx buffer to empty
  USART1->TXDATA = byte;                 // transmit data byte   
}
 
// This function returns the value of the RX buffer (waits for valid RX data)
uint8_t SPI_ReadByte(void) {
  while(! (USART1->STATUS & (1 << 7)) ); // wait for valid RX data
  return(USART1->RXDATA);                // clear RX buffer
}
 
// This function is used to display bytes over LEUART
void print_byte(uint8_t byte) {
  while(!(LEUART0->STATUS & (1 << 4))); // wait for TX buffer to empty
  LEUART0->TXDATA = byte;               // send character
  while(LEUART0->SYNCBUSY & (1 << 6));  // wait for TXDATA reg write to complete
}
 
// This function breaks up 'byte' into high and low nibbles (ASCII characters) to be transmitted over UART
void print_byte_setup(uint8_t byte) {
                
  if (((byte & 0xF0) >> 4) <= 0x09) {                      // if high nibble is less than 0xA
    print_byte_array[2] = ((byte & 0xF0) >> 4) + 0x30;     // store ASCII char
  }
  if ((((byte & 0xF0) >> 4) >= 0x0A) && (((byte & 0xF0) >> 4)<= 0x0F)) { // if high nibble is between 0xA and 0xF
    print_byte_array[2] = ((byte & 0xF0) >> 4) + 0x37;     // store ASCII char
  }
  if ((byte & 0x0F) <= 0x09) {                             // if low nibble is less than 0xA
    print_byte_array[3] = (byte & 0x0F) + 0x30;            // store ASCII char
  }
  if (((byte & 0x0F) >= 0x0A) && ((byte & 0x0F)<= 0x0F)) { // if low nibble is between 0xA and 0xF
    print_byte_array[3] = (byte & 0x0F) + 0x37;            // store ASCII char
  }
}

SPI Example Using emlib API
The following example code demonstrates how to use the emlib functions to implement the SPI example. For the following code to work, the following files need to be added to the workspace:

  • em_cmu.c
  • em_emu.c
  • em_gpio.c
  • em_leuart.c
  • em_system.c
  • em_usart.c
#include "em_device.h"
#include "em_system.h"
#include "em_chip.h"
#include "em_cmu.h"
#include "em_gpio.h"
#include "em_usart.h"
#include "em_leuart.h"
 
#define SPI_PORTD     gpioPortD // USART1 (location #3) MISO and MOSI are on PORTD
#define SPI_PORTC     gpioPortC // USART1 (location #3) SS and SCLK are on PORTC
#define SPI_MISO_PIN  6  // PD6
#define SPI_MOSI_PIN  7  // PD7
#define SPI_CS_PIN    14 // PC14
#define SPI_SCLK_PIN  15 // PC15
#define SPI_nHLD_PIN  13 // PC13
#define SPI_nWP_PIN   11 // PC11
 
#define LEUART_PORT   gpioPortF // LEUART (location #3)
#define LEUART_TX_PIN 0  // PF0
#define LEUART_RX_PIN 1  // PF1
 
#define RX_BUFFER_SIZE 2
 
void print_byte_setup(uint8_t byte);
 
/* SPI functions */
void CS_pin_clr(void);
void CS_pin_set(void);
 
/* Global variables */
const char header[] = "\n\rManuf/Device ID\n\r";
uint8_t rx_buffer[RX_BUFFER_SIZE];
uint8_t print_byte_array[4] = { 0x30, 0x78, 0x20, 0x20 }; // array for displaying bytes one nibble at a time
 
int main () {
   
  CHIP_Init();
  uint16_t i;
   
  // Clear software buffer (optional)
  for(i=0; i<RX_BUFFER_SIZE; i++) {
    rx_buffer[i] = 0;
  }
   
  /* Setup clock tree */
  /* Use 24MHz crystal as HF system clock source */
  /* Run Low Energy Peripheral B system as a high speed peripheral (use CORECLCK/2 as source) */
  CMU_OscillatorEnable(cmuOsc_HFXO, true, true);           // enable XTAL osc and wait for it to stabilize
  CMU_ClockSelectSet(cmuClock_HF, cmuSelect_HFXO);         // select HF XTAL osc as system clock source (24MHz)
  if(cmuSelect_HFRCO != CMU_ClockSelectGet(cmuClock_HF)) { // make sure HFRCO is no longer the selected clock source
    CMU_OscillatorEnable(cmuOsc_HFRCO, false, false);      // disable HFRCO to save power
  }
  CMU_ClockSelectSet(cmuClock_LFB, cmuSelect_CORELEDIV2);  // select HFCORECLK/2 as clock source to LFB
  CMU_ClockEnable(cmuClock_CORELE, true);                  // enable the Low Energy Peripheral Interface clock
   
  /* LEUART reset (if using bootloader to program) */
  LEUART0->ROUTE = 0;                                      // reset the LEUART route register
  CMU_ClockEnable(cmuClock_LEUART0, false);                // disable LEUART peripheral clock
  for(i=0; i<2000; i++);                                   // brief delay to keep TX line low at least one frame period
  /* End LEUART reset */
   
  CMU_ClockEnable(cmuClock_LEUART0, true);                 // enable LEUART peripheral clock
  CMU_ClockDivSet(cmuClock_LFB, 2);                        // set LFB prescaler to /4 (LEUART souorce clock will be 3MHz)
  CMU_ClockEnable(cmuClock_GPIO, true);                    // enable GPIO peripheral clock
  CMU_ClockEnable(cmuClock_USART1, true);                  // enable USART1 peripheral clock
   
  /* Configure GPIO */
  GPIO_PinModeSet(LEUART_PORT, LEUART_TX_PIN, gpioModePushPull, 1); // configure UART TX pin as output, initialize high
  GPIO_PinModeSet(LEUART_PORT, LEUART_RX_PIN, gpioModeInput, 0);    // configure UART RX pin as input, no filter
  GPIO_PinModeSet(SPI_PORTD, SPI_MISO_PIN, gpioModeInput, 0);       // configure MISO pin as input, no filter
  GPIO_PinModeSet(SPI_PORTD, SPI_MOSI_PIN, gpioModePushPull, 1);    // configure MOSI pin as output, initialize high
  GPIO_PinModeSet(SPI_PORTC, SPI_CS_PIN, gpioModePushPull, 1);      // configure CS pin as output, initialize high
  GPIO_PinModeSet(SPI_PORTC, SPI_SCLK_PIN, gpioModePushPull, 0);    // configure SCLK pin as output, initialize low
  GPIO_PinModeSet(SPI_PORTC, SPI_nHLD_PIN, gpioModePushPull, 1);    // configure nHLD pin as output, initialize high
  GPIO_PinModeSet(SPI_PORTC, SPI_nWP_PIN, gpioModePushPull, 0);     // configure nWP pin as output, initialize low
   
  /* Configure SPI Port (USART1 in sync mode) */
  USART_InitSync_TypeDef spi_init = {
    .enable = usartEnable,        // enable bidirectional data (TX and RX)
    .refFreq = 0,                 // measure source clock
    .baudrate = 12000000,         // 12Mbps is max data rate with 24MHz clock source
    .databits = usartDatabits8,   // 8 data bits per frame
    .master = true,               // configure as SPI master
    .msbf = true,                 // transmit msb first (requirement of W25X40CL)
    .clockMode = usartClockMode0, // clock idles low, data setup on rising edge, sampled on falling edge
  };
  USART_IntClear(USART1, 0x1FF9);                 // clear interrupt flags (optional)
  USART_InitSync(USART1, &spi_init);              // apply configuration to USART1
  USART1->ROUTE = (3 << 8) | ( 1 << 3)| (3 << 0); // use location #3, enable TX/RX/CLK pins
   
  /* Configure COM Port (LEUART0 asynch) */
  LEUART_Init_TypeDef leuart_init = {
    .enable = leuartEnable,                    // enable Receiver and Transmitter
    .refFreq = 0,                              // measure reference clock
    .baudrate = 115200,                        // 115200 bps
    .databits = leuartDatabits8,               // 8 data bits
    .parity = leuartNoParity,                  // no parity bits
    .stopbits = leuartStopbits1,               // 1 stop bit
  };
  LEUART_IntClear(LEUART0, (0xFF << 3) | 0x1); // clear interrupt flags (optional)
  LEUART_Init(LEUART0, &leuart_init);          // apply configuration to LEUART0
  LEUART0->ROUTE = (3 << 8) | 0x3;             // use Location #3 (PF0, PF1) enable TX/RX pins
  /* Print Header */
  for(i=0; i<sizeof(header); i++) {
    LEUART_Tx(LEUART0, header[i]);
  }
   
  /* Get Device ID from SPI Slave (see W25X40CL datasheet for details) */
  CS_pin_clr();                                     // select slave device
   
  rx_buffer[0] = USART_SpiTransfer(USART1, 0x90);   // send 'Device ID' command and clear RX buffer
  for(i=0; i<3; i++) {
    rx_buffer[0] = USART_SpiTransfer(USART1, 0x00); // send 3 '00' bytes
  }
  rx_buffer[0] = USART_SpiTransfer(USART1, 0xFF);   // send dummy byte '0xFF' to receive manuf. id
  rx_buffer[1] = USART_SpiTransfer(USART1, 0xFF);   // send dummy byte '0xFF' to receive device id
   
  CS_pin_set();                                     // deselect slave device
   
  /* Print response on-screen */
  print_byte_setup(rx_buffer[0]);                   // break up manuf. id into individual characters
  for(i=0; i<4; i++) {
    LEUART_Tx(LEUART0, print_byte_array[i]);        // display manuf. id
  }
  LEUART_Tx(LEUART0, 0x20);                         // send whitespace
  print_byte_setup(rx_buffer[1]);                   // break up device id into individual characters
  for(i=0; i<4; i++) {
    LEUART_Tx(LEUART0, print_byte_array[i]);        // display device id
  }
   
  while(1);
}
 
// This function drives the CS pin low
void CS_pin_clr(void) {
  GPIO_PinOutClear(SPI_PORTC, SPI_CS_PIN);
}
 
// This function drives the CS pin high
void CS_pin_set(void) {
  GPIO_PinOutSet(SPI_PORTC, SPI_CS_PIN);
}
 
// This function breaks up 'byte' into high and low nibbles (ASCII characters) to be transmitted over UART
void print_byte_setup(uint8_t byte) {
                
  if (((byte & 0xF0) >> 4) <= 0x09) {                      // if high nibble is less than 0xA
    print_byte_array[2] = ((byte & 0xF0) >> 4) + 0x30;     // store ASCII char
  }
  if ((((byte & 0xF0) >> 4) >= 0x0A) && (((byte & 0xF0) >> 4)<= 0x0F)) { // if high nibble is between 0xA and 0xF
    print_byte_array[2] = ((byte & 0xF0) >> 4) + 0x37;     // store ASCII char
  }
  if ((byte & 0x0F) <= 0x09) {                             // if low nibble is less than 0xA
    print_byte_array[3] = (byte & 0x0F) + 0x30;            // store ASCII char
  }
  if (((byte & 0x0F) >= 0x0A) && ((byte & 0x0F)<= 0x0F)) { // if low nibble is between 0xA and 0xF
    print_byte_array[3] = (byte & 0x0F) + 0x37;            // store ASCII char
  }
}
  • Inter-Integrated Circuit (I2C) Example
    The I2C protocol is a very popular serial communication protocol in embedded designs due to the low number of pins required and the large number of devices that can be connected to a single bus. A single data line (SDA) and a single clock line (SCL) plus common ground are the only required connections between an I2C master and slave. SMBus and PMBus are similar protocols that have more restrictive message format requirements. The Zero Gecko supports SMBus as well as I2C. The following figure (taken from the I2C wikipedia page) shows a simplified timing diagram for I2C transactions. The first yellow box indicates a START signal. A START condition is the only time the SDA line will transition low while the clock line is high. During the transcaction, SDA transitions are made while SCL is low (blue) and SDA is sampled while SCL is high (green). The second yellow box indicates a STOP signal. A STOP condition is the only time the SDA line will transition high while the clock line is high. Open-drain outputs with external pull-up resistors are used for the data and clock lines. Once a device stops actively driving the bus lines, the external resistors pull each line high. I would strongly recommend doing additional research on the I2C protocol if you are not already familiar with it.

    I2C Timing Example

I2C is an address-based protocol, meaning each slave device has a unique I2C address. I2C supports multiple masters and multiple slaves on a single bus. A device’s datasheet will specify its I2C slave address. Typically, a device will use a 7-bit I2C slave address, although some variations to the I2C protocol also support 10 and 16-bit addressing modes. A typical I2C transaction would begin with a master issuing a START signal, followed by a single byte consisting of the slave’s 7-bit address and a single read/write bit. After the slave address byte, the message format varies a bit from device to device. There is no limit to the number of bytes that can be transmitted in a single transaction. However, each byte must be acknowledged by the receiving device before the next byte is sent. After the last byte and acknowledge is received, the master will end the transaction by issuing a STOP signal. For devices that support larger addressing schemes, the initial byte format may also change. The specific device’s datasheet will describe the required I2C message format.

For this example, the Zero Gecko was used as an I2C master and the TSL2569 light sensor was used as the I2C slave device (see the reference design section above). The following image shows the I2C read and write formats for communication with the TSL2569 (taken from datasheet).


The TSL2569 uses 7-bit addressing mode. The second byte is used to specify which register you are trying to access within the device. The “Byte Count” byte is not used for I2C communication. It is included in the above images since it is required in the SMBus protocol (supported by the TSL2568). The grey boxes indicate bits sent from the slave to the master, while the white boxes indicate bits sent from the master to the slave. Each transaction starts with the START (S) signal and ends with the STOP (P) signal. As mentioned before, ACKNOWLEDGE (A) signals are required after every byte and can be sent from either a slave or a master. A single WRITE (Wr) bit (set to ‘0’) is required for I2C write transactions. For I2C read transactions, a WRITE (Wr) bit (set to ‘0’) is initially sent in order to write the proper command code to the internal Command register of the TSL2569. After the TSL2569 acknowledges that byte, a REPEATED START (Sr) signal is sent from the master followed by the slave address with a READ (Rd) bit included (set to ‘1’).

100kbps is the standard I2C data rate while more recent I2C variations support 400kbps fast mode , 1Mbps fast mode plus , and 3.4Mbps high speed mode . For this example, the Zero Gecko implemented 400kbps fast mode as that was a requirement of the TSL2569 device. In order to generate the 400kbps data rate, the internal RC oscillator was used as the 14MHz clock source along with a 6:3 bit ratio on the I2C bus (see section 14.3.4 of the Zero Gecko reference manual for details).

For this example, the Zero Gecko simply reads the internal Device ID register within the TSL2569 over I2C. The result is displayed on-screen over UART. The LEUART was used as a high speed peripheral to print characters in a terminal window. The following image shows the Tera Term output window while running this application. The upper nibble 0xB (b’1101’) indicates that this is a TSL2569 while the lower nibble 0x0 indicates that this is the first die revision of the part.


The program was flashed to the Zero Gecko using the EFM32 UART bootloader. The bootloader changes the RC oscillator frequency to 21MHz to increase speed, but never adjusts it back to the default setting of 14MHz. Therefore, a small clock rate adjustment was made at the beginning of main () . Also, since I used the UART bootloader to program the device and used the same UART port for communication during my application (LEUART0), I needed to reset the LEUART peripheral at the beginning of my main() function. If you are using an external debugger to program the device, you won’t need to reset the LEUART peripheral.

I2C Example Using Direct Register Access
The following code demonstrates I2C usage with direct register access. For the following code to work, the em_system.c file needs to be added to the workspace.

#include "em_device.h"
#include "em_system.h"
#include "em_chip.h"
 
#define I2C_PORT    4  // PORTE - I2C0 Location #6
#define I2C_SDA_PIN 12 // PE12
#define I2C_SCL_PIN 13 // PE13
 
#define LEUART_PORT   5 // PORTF - LEUART0 Location #3
#define LEUART_TX_PIN 0 // PF0
#define LEUART_RX_PIN 1 // PF1
 
#define SENSOR_SLAVE_ADDR 0x39 // see TSL2569 datasheet for details
 
typedef struct {
  uint8_t com_code;
  uint8_t byte_cnt;
  uint8_t data[];
}i2c_message_typeDef;
 
/* UART functions */
void print_byte(uint8_t byte);
void print_byte_setup(uint8_t byte);
void print_string(const char string[], uint8_t length);
 
/* I2C functions */
void I2C_Write(uint8_t addr, i2c_message_typeDef *message);
void I2C_Read(uint8_t addr, i2c_message_typeDef *message);
uint8_t check_for_nack(uint8_t expected_ack_type);
 
/* Global variables */
volatile uint8_t print_byte_array[4] = { 0x30, 0x78, 0x20, 0x20 }; // array for displaying bytes one nibble at a time
const char header[] = "\n\rEFM32 Zero Gecko I2C Example\n\r";
const char id_string[] = "Part# RevID: ";
const char nack_error_string[] = "NACK Received\n\r";
 
int main () {
   
  CHIP_Init();
  uint8_t i;
  i2c_message_typeDef sensor_message;
   
  /* Setup clock tree */
  /* Use 14MHz RC oscillator as HF clock source */
  /* Bootloader Clock Correction */
  // If using uart bootloader to program, the system boots with 21MHz internal RC as
  // the HF clock source. Change to 14MHz, but don't overwrite reset tuning value.
  CMU->HFRCOCTRL &= ~(7 << 8);      // clear HFCLK band
  CMU->HFRCOCTRL |= (3 << 8);       // set HFCLK to 14MHz
  /* End Bootloader Clock Correction */
   
  CMU->LFCLKSEL = (3 << 2);                // select HFCORECLK/2 as clock source to LFB
  CMU->HFCORECLKEN0 = (1 << 2);            // enable the Low Energy Peripheral Interface clock
  CMU->LFBCLKEN0 = 0x1;                    // enable LEUART peripheral clock
  while(CMU->SYNCBUSY & (1 << 4));         // wait for LFBCLKEN0 reg write to complete
  CMU->LFBPRESC0 = 0x1;                    // set LFB prescaler to /2: LEUART clock freq = 14MHz/2/2 = 3.5MHz
  while(CMU->SYNCBUSY & (1 << 6));         // wait for LFBPRESC0 reg write to complete
  CMU->HFPERCLKDIV = (1 << 8);             // enable HF peripheral clock
  CMU->HFPERCLKEN0 = (1 << 11) | (1 << 7); // enable GPIO and I2C0 peripheral clocks
   
  /* Configure GPIO */
  GPIO->P[I2C_PORT].MODEH      = (8 << 20) | (8 << 16);                   // configure SDA and SCL as open drain output
  GPIO->P[I2C_PORT].DOUTSET    = (1 << I2C_SCL_PIN) | (1 << I2C_SDA_PIN); // initialize SDA and SCL high
  GPIO->P[LEUART_PORT].MODEL   = (1 << 4) | (4 << 0);                     // configure PF0 (TX) as output and PF1 (RX) as input
  GPIO->P[LEUART_PORT].DOUTSET = (1 << LEUART_TX_PIN);                    // initialize PF0 high
     
  /* Configure COM Port (LEUART0 asynch) */
  LEUART0->CLKDIV = 7522;                           // 115200 baud with 3.5MHz source clock (0.0029% error)
  while(LEUART0->SYNCBUSY & (1 << 2));              // wait for CLKDIV reg write to complete
  LEUART0->CMD = (1<<7) | (1<<6) | (1<<2) | (1<<0); // clear buffers, enable TX/RX
  while(LEUART0->SYNCBUSY & (1 << 1));              // wait for CMD reg write to complete
  LEUART0->IFC = (0xFF << 3) | 0x1;                 // clear interrupt flags
  LEUART0->ROUTE = (3 << 8) | 0x3;                  // use Location #3 (PF0, PF1)
   
  /* Configure I2C - Fast mode (400kHz) Master */
  I2C0->CTRL = (1 << 8) | (1 << 2) | (1 << 0);      // enable I2C, 6:3 ratio, auto ack enabled
  I2C0->CMD = (7 << 5);                             // clear pending commands, clear TX buffer and shift reg, issue abort to clear bus busy bit
  I2C0->CLKDIV = 2;                                 // generates 400kHz clock with 6:3 ratio
  i = I2C0->RXDATA;                                 // flush RX buffer
  I2C0->IFC = 0x1FFCF;                              // clear interrupt flags (optional)
  I2C0->ROUTE = (6 << 8) | 0x3;                     // use location #6 (PE11, PE12), enable SDA and SCL
   
  /* Print Header */
  print_string(header, sizeof(header));
   
  /* Send power-up command to sensor (see TSL2569 datasheet for details) */
  sensor_message.com_code = 0xD0;                   // access control register
  sensor_message.byte_cnt = 1;                      // required for SMBus protocol compliance, ignored for I2C applications
  sensor_message.data[0]  = 0x3;                    // power-up command
  I2C_Write(SENSOR_SLAVE_ADDR, &sensor_message);    // start I2C write transaction with sensor
   
  /* Read part# and rev ID from sensor (see TSL2569 datasheet for details) */
  sensor_message.com_code = 0xDA;                   // access ID register
  sensor_message.byte_cnt = 1;                      // TSL2569 doesn't transmit the byte count, therefore need to specify #of bytes manually
  I2C_Read(SENSOR_SLAVE_ADDR, &sensor_message);     // start I2C read transaction with sensor
   
  print_byte_setup(sensor_message.data[0]);         // break result up into individual characters
   
  /* Print Part# RevID */
  print_string(id_string, sizeof(id_string));
  for(i=0; i<sizeof(print_byte_array); i++) {
    print_byte(print_byte_array[i]);
  }
  while(1);
}
 
/* This function sends a single character over UART */
void print_byte(uint8_t byte) {
  while(!(LEUART0->STATUS & (1 << 4))); // wait for TX buffer to empty
  LEUART0->TXDATA = byte;               // send character
  while(LEUART0->SYNCBUSY & (1 << 6));  // wait for TXDATA reg write to complete
}
 
/* This function sends a string of characters over UART */
void print_string(const char string[], uint8_t length) {
  uint8_t i;
  for(i=0; i<length; i++) {
    print_byte(string[i]);
  }
}
 
/* This function handles I2C write transactions */
void I2C_Write(uint8_t addr, i2c_message_typeDef *message) {
   
  uint8_t i;
   
  /* wait for bus to become idle */
  while(I2C0->STATE & 0x1);
   
  /* send START command */
  I2C0->CMD |= 0x1;
   
  /* write I2C slave address */
  while(!(I2C0->STATUS & (1 << 7)));   // wait for TX buffer to empty
  I2C0->TXDATA = addr << 1;            // write slave address with WRITE bit (lsb=0)
  if(check_for_nack(0)) {              // wait for slave to ack
    // handle addr nack error
  }
   
  /* write the sensor command code */
  while(!(I2C0->STATUS & (1 << 7)));   // wait for TX buffer to empty
  I2C0->TXDATA = message->com_code;    // write the command code to the sensor
  if(check_for_nack(1)) {              // wait for slave to ack
    // handle data nack error
  }
   
  /* send each data byte over I2C */
  for(i=0; i<message->byte_cnt; i++) {
    while(!(I2C0->STATUS & (1 << 7))); // wait for TX buffer to empty
    I2C0->TXDATA = message->data[i];   // send data byte to slave
    if(check_for_nack(1)) {            // wait for slave to ack
      // handle data nack error
    }
  }
   
  /* send stop command */
  I2C0->CMD |= 0x2;
}
 
/* This function handles I2C read transactions */
void I2C_Read(uint8_t addr, i2c_message_typeDef *message) {
  uint8_t i;
   
  /* flush buffer if needed */
  if(I2C0->STATUS & (1 << 8)) {
    i = I2C0->RXDATA;
  }
   
  /* wait for I2C bus to become idle */
  while(I2C0->STATE & 0x1);             
   
  /* send START command */
  I2C0->CMD |= 0x1;
   
  /* write I2C slave address */
  while(!(I2C0->STATUS & (1 << 7)));   // wait for TX buffer to empty
  I2C0->TXDATA = addr << 1;            // write slave address with WRITE bit (lsb=0)
  if(check_for_nack(0)) {              // wait for slave to ack
    // handle addr nack error
  }
   
  /* write the sensor command code */
  while(!(I2C0->STATUS & (1 << 7)));   // wait for TX buffer to empty
  I2C0->TXDATA = message->com_code;    // write command code to the sensor
  if(check_for_nack(1)) {              // wait for slave to ack
    // handle data nack error
  }
   
  /* send repeated START command */
  I2C0->CMD |= 0x1;
   
  /* write I2C slave address with 'read' bit set */
  while(!(I2C0->STATUS & (1 << 7)));   // wait for TX buffer to empty
  I2C0->TXDATA = (addr << 1) | 0x1;    // write slave address with READ bit (lsb=1)
  if(check_for_nack(2)) {              // wait for slave to ack
    // handle addr nack error
  }
   
  /* read each data byte */
  for(i=0; i<message->byte_cnt; i++) {
    while(!(I2C0->STATUS & (1 << 8))); // wait for valid data to be received
    message->data[i] = I2C0->RXDATA;   // read data from slave
    /* no manual ACK signal required since we are using AUTO-ACK feature */
  }
   
  /* send STOP command */
  I2C0->CMD |= 0x2;
}
 
/* This function checks for an expected acknowledge signal based on the message type (READ/WRITE) and byte number (ADDR/DATA) */
uint8_t check_for_nack(uint8_t expected_ack_type) {
   
  if(0 == expected_ack_type) {
    while(!((I2C0->STATE >> 4) == 0x9)) {
      // wait for address ACK/NACK and bushold
      // check for timeout (optional)
    }
     
    if(I2C0->STATE & 0x8) {
      // Handle NACK: lost communication
      print_string(nack_error_string, sizeof(nack_error_string));
      return 1; // nack received
    }else{
      return 0; // ack received
    }
  }else if (1 == expected_ack_type){
    while(!((I2C0->STATE >> 4) == 0xD)) {
      // wait for data ACK/NACK and bushold
      // check for timeout (optional)
    }
    if(I2C0->STATE & 0x8) {
      // Handle NACK: lost communication
      print_string(nack_error_string, sizeof(nack_error_string));
      return 1; // nack received
    }else{
      return 0; // ack received
    }
  }else if(2 == expected_ack_type) {
    while(!((I2C0->STATE >> 4) == 0xC)) {
      // wait for data ACK/NACK (no bushold)
      // check for timeout (optional)
    }
    if(I2C0->STATE & 0x8) {
      // Handle NACK: lost communication
      print_string(nack_error_string, sizeof(nack_error_string));
      return 1; // nack received
    }else{
      return 0; // ack received
    }
  }else{
    /* unsupported ack type */
    return 2;
  }
}
 
/* This function breaks up 'byte' into high and low nibbles (ASCII characters) to be transmitted over UART */
void print_byte_setup(uint8_t byte) {
                
  if (((byte & 0xF0) >> 4) <= 0x09) {                      // if high nibble is less than 0xA
    print_byte_array[2] = ((byte & 0xF0) >> 4) + 0x30;     // store ASCII char
  }
  if ((((byte & 0xF0) >> 4) >= 0x0A) && (((byte & 0xF0) >> 4)<= 0x0F)) { // if high nibble is between 0xA and 0xF
    print_byte_array[2] = ((byte & 0xF0) >> 4) + 0x37;     // store ASCII char
  }
  if ((byte & 0x0F) <= 0x09) {                             // if low nibble is less than 0xA
    print_byte_array[3] = (byte & 0x0F) + 0x30;            // store ASCII char
  }
  if (((byte & 0x0F) >= 0x0A) && ((byte & 0x0F)<= 0x0F)) { // if low nibble is between 0xA and 0xF
    print_byte_array[3] = (byte & 0x0F) + 0x37;            // store ASCII char
  }
}

I2C Example Using emlib API
The following example code demonstrates I2C functionality using emlib functions. For the code to work, the following files need to be added to the workspace:

  • em_cmu.c
  • em_emu.c
  • em_gpio.c
  • em_i2c.c
  • em_leuart.c
  • em_system.c
#include "em_device.h"
#include "em_system.h"
#include "em_chip.h"
#include "em_cmu.h"
#include "em_emu.h"
#include "em_gpio.h"
#include "em_i2c.h"
#include "em_leuart.h"
 
#define I2C_PORT    gpioPortE // PORTE - I2C0 Location #6
#define I2C_SDA_PIN 12 // PE12
#define I2C_SCL_PIN 13 // PE13
 
#define LEUART_PORT   gpioPortF // PORTF - LEUART0 Location #3
#define LEUART_TX_PIN 0 // PF0
#define LEUART_RX_PIN 1 // PF1
 
#define SENSOR_SLAVE_ADDR 0x39 // see TSL2569 datasheet for details
 
void print_byte_setup(uint8_t byte);
 
/* Global variables */
volatile uint8_t print_byte_array[4] = { 0x30, 0x78, 0x20, 0x20 }; // array for displaying bytes one nibble at a time
const char header[] = "\n\rEFM32 Zero Gecko I2C Example\n\r";
const char id_string[] = "Part# RevID: ";
const char nack_error_string[] = "NACK Received\n\r";
 
int main () {
   
  CHIP_Init();
  uint16_t i;
  uint8_t tx_buffer[3];           // software tx buffer
  uint8_t rx_buffer[1];           // software rx buffer
  I2C_TransferReturn_TypeDef ret; // I2C state tracker
   
  /* Setup clock tree */
  /* Use 14MHz RC oscillator as HF clock source */
  /* Bootloader Clock Correction */
  // If using uart bootloader to program, the system boots with 21MHz internal RC as
  // the HF clock source. Change to 14MHz, but don't overwrite reset tuning value.
  CMU_HFRCOBandSet(cmuHFRCOBand_14MHz); // set HFCLK to 14MHz
  /* End Bootloader Clock Correction */
   
  CMU_ClockSelectSet(cmuClock_LFB, cmuSelect_CORELEDIV2);           // select HFCORECLK/2 as clock source to LFB
  CMU_ClockEnable(cmuClock_CORELE, true);                           // enable the Low Energy Peripheral Interface clock
   
  /* LEUART reset (if using bootloader to program) */
  LEUART0->ROUTE = 0;                                               // reset the LEUART route register
  CMU_ClockEnable(cmuClock_LEUART0, false);                         // disable LEUART peripheral clock
  for(i=0; i<2000; i++);                                            // brief delay to keep TX line low at least one frame period
  /* End LEUART reset */
   
  CMU_ClockEnable(cmuClock_LEUART0, true);                          // enable LEUART peripheral clock
  CMU_ClockDivSet(cmuClock_LFB, 1);                                 // set LFB prescaler to /2: LEUART clock freq = 14MHz/2/2 = 3.5MHz
  CMU_ClockEnable(cmuClock_HFPER, true);                            // enable HF peripheral clock
  CMU_ClockEnable(cmuClock_GPIO, true);                             // enable GPIO peripheral clock
  CMU_ClockEnable(cmuClock_I2C0, true);                             // enable I2C0 peripheral clock
   
  /* Configure GPIO */ 
  GPIO_PinModeSet(LEUART_PORT, LEUART_TX_PIN, gpioModePushPull, 1); // configure UART TX pin as output, initialize high
  GPIO_PinModeSet(LEUART_PORT, LEUART_RX_PIN, gpioModeInput, 0);    // configure UART RX pin as input, no filter
  GPIO_PinModeSet(I2C_PORT, I2C_SDA_PIN, gpioModeWiredAnd, 0);      // configure SDA pin as open drain output
  GPIO_PinModeSet(I2C_PORT, I2C_SCL_PIN, gpioModeWiredAnd, 0);      // configure SCL pin as open drain output
   
  GPIO_PinModeSet(gpioPortD, 5, gpioModePushPull, 0);
   
  /* Configure COM Port (LEUART0 asynch) */
  LEUART_Init_TypeDef leuart_init =
  {
    .enable   = leuartEnable,                    // enable Receiver and Transmitter
    .refFreq  = 0,                               // measure reference clock
    .baudrate = 115200,                          // 115200 bps
    .databits = leuartDatabits8,                 // 8 data bits
    .parity   = leuartNoParity,                  // no parity bits
    .stopbits = leuartStopbits1,                 // 1 stop bit
  };
  LEUART_IntClear(LEUART0, (0xFF << 3) | 0x1);   // clear interrupt flags (optional)
  LEUART_Init(LEUART0, &leuart_init);            // apply configuration to LEUART0
  LEUART0->ROUTE = (3 << 8) | 0x3;               // use Location #3 (PF0, PF1) enable TX/RX pins
   
  /* Configure I2C - Fast mode (400kHz) Master */
  I2C_Init_TypeDef i2c_init =
  {
    .enable = true,                              // enable I2C
    .master = true,                              // I2C master
    .refFreq = 0,                                // measure reference clock
    .freq = 400000,                              // 400kbps fast mode
    .clhr = i2cClockHLRAsymetric,                // use 6:3 bit ratio
  };
  I2C_Init(I2C0, &i2c_init);                     // apply configuration to I2C0
  I2C0->CTRL |= (1 << 2);                        // enable AUTO-ACK feature
  I2C0->ROUTE = (6 << 8) | 0x3;                  // use location #6 (PE11, PE12), enable SDA and SCL
   
  /* Print Header */
  for(i=0; i<sizeof(header); i++) {
    LEUART_Tx(LEUART0, header[i]);
  }
   
  /* Send power-up command to sensor (see TSL2569 datasheet for details) */
  tx_buffer[0] = 0xD0;                           // access control register
  tx_buffer[1] = 0x3;                            // power-up command
     
  I2C_TransferSeq_TypeDef sensor_message =
  {
    .addr = (SENSOR_SLAVE_ADDR << 1),            // set sensor slave address
    .flags = I2C_FLAG_WRITE,                     // indicate basic write
    .buf[0].data = tx_buffer,                    // point to tx_buffer
    .buf[0].len = 2,                             // specify number of bytes
  };
  ret = I2C_TransferInit(I2C0, &sensor_message); // start I2C write transaction with sensor
  while(ret == i2cTransferInProgress) {          // continue until all data has been sent
    ret = I2C_Transfer(I2C0);
  }
   
  /* Read part# and rev ID from sensor (see TSL2569 datasheet for details) */
  tx_buffer[0] = 0xDA;                           // access ID register
   
  sensor_message.flags = I2C_FLAG_WRITE_READ;    // indicate combined write/read
  sensor_message.buf[0].len = 1;                 // specify # bytes to be written
  sensor_message.buf[0].data = tx_buffer;        // point to tx_buffer
  sensor_message.buf[1].len = 1;                 // specify # bytes to be read
  sensor_message.buf[1].data = rx_buffer;        // point to rx_buffer
   
  ret = I2C_TransferInit(I2C0, &sensor_message); // start I2C write/read transaction with sensor
  while(ret == i2cTransferInProgress) {          // continue until all data has been received
    ret = I2C_Transfer(I2C0);
  }
   
  print_byte_setup(rx_buffer[0]);                // break result up into individual characters
   
  /* Print Part# RevID */
  for(i=0; i<sizeof(id_string); i++) {
    LEUART_Tx(LEUART0, id_string[i]);
  }
  for(i=0; i<sizeof(print_byte_array); i++) {
    LEUART_Tx(LEUART0, print_byte_array[i]);
  }
   
  while(1);
}
 
// This function breaks up 'byte' into high and low nibbles (ASCII characters) to be transmitted over UART
void print_byte_setup(uint8_t byte) {
                
  if (((byte & 0xF0) >> 4) <= 0x09) {                      // if high nibble is less than 0xA
    print_byte_array[2] = ((byte & 0xF0) >> 4) + 0x30;     // store ASCII char
  }
  if ((((byte & 0xF0) >> 4) >= 0x0A) && (((byte & 0xF0) >> 4)<= 0x0F)) { // if high nibble is between 0xA and 0xF
    print_byte_array[2] = ((byte & 0xF0) >> 4) + 0x37;     // store ASCII char
  }
  if ((byte & 0x0F) <= 0x09) {                             // if low nibble is less than 0xA
    print_byte_array[3] = (byte & 0x0F) + 0x30;            // store ASCII char
  }
  if (((byte & 0x0F) >= 0x0A) && ((byte & 0x0F)<= 0x0F)) { // if low nibble is between 0xA and 0xF
    print_byte_array[3] = (byte & 0x0F) + 0x37;            // store ASCII char
  }
}

Comments from the Author

The Zero Gecko microcontroller is one of the most energy efficient microcontrollers available. It has a ton of appealing features and is very low cost. The code found in the examples above can be used freely. I encourage you to build on the examples presented on this page and add functionality to the applications. The goal of this page was to familiarize you with basic functions of EFM32 Zero Gecko microcontrollers so that you can apply each peripheral in more complex applications. I hope you find it useful. I hope you are successful with Zero Gecko microcontrollers!

  • Scott

Questions/Comments

Any questions or comments please go to Digi-Key’s TechForum