Getting Started with EFM32 Giant Gecko ARM Cortex-M3

Created by Scott Schmit, last modified on Sep 18, 2014

image

Purpose

This page is meant to help you get started with Giant 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 Giant Gecko device family implements an ARM Cortex-M3 core and is available with up to 1024KB program (flash) memory, up to 128KB RAM, and CPU speeds up to 48MHz. This page will help you start a blank Giant Gecko project using IAR Embedded Workbench and provide some example code to familiarize you with the various peripherals available with this microcontroller at the register level. It also provides example code using emlib functions. The EFM32GG-STK3700 was used as the development platform for these examples.

Quick Links

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

Create an IAR Template Project for EFM32

Silicon Labs recommends using Simplicity Studio to locate example code and documentation. They recommend cloning existing projects for IAR instead of starting a blank IAR project from scratch. This is because all of the App Notes and example code written for these micros use a hierarchy that is non-typical for IAR projects. The examples also setup your include file paths and setup your debug link for you. You can then use CMSIS and em_lib libraries to call functions from your main.c file to develop your application.

Although it’s not necessarily recommended, you can start a blank IAR project by using the following procedure. This procedure parallels the example found here. However, I setup my project in a unique project directory rather than the default location used by Simplicity Studio.

Procedure for Starting a Template IAR Project for EFM32
This procedure outlines how to start a blank IAR project and setup the necessary project settings. However, we will use this process to setup a template project. Once we have a template created, we can simply copy&paste and rename the project whenever we wish to start a new EFM32 Giant Gecko project. Even if you don’t want to use the emlib libraries, you should still follow all of these steps to properly setup your project.

  1. Open IAR and navigate to Project → Create New Project
    image

  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 should pop up and you can select a location for your project.

Silicon Labs recommends placing each of your projects in the application note (an) directory that gets created when you install Simplicity Studio. However, we are going to create a unique location for all of our EFM32 projects.

For my personal use, I created a “My_EFM32_Projects” folder within an “Energy Micro” folder on my desktop. This is where I store all of my new EFM32 projects. For this demo, since we are using a Giant Gecko microcontroller, inside the “My_EFM32_Projects” folder, I’ve created a folder called “Template_EFM32GG”. You can name it whatever you wish. However, I like to be able to sort my projects by micro type. It’s an easy identifier for me to recognize which specific micro I used for that project if I include it in the name of the project folder. Alternatively, you could create another folder for individual micros if you wanted, but I like to keep my folder count down if possible. My project directory now has the following hierarchy:

→ “Energy Micro” → “My_EFM32_Projects” → “Template_EFM32GG”

Once you have your project directory setup, create an “iar” folder and save the project file as “Template_EFM32GG.ewp”.
image

  1. Now that your project has been created, you can add some groups to the project. Right-click the project in the workspace browser and select ‘Add Group’.
    image

  2. Let’s name the first group ‘source’. This is standard for all EFM32 projects.
    image

  3. Now we can create our ‘main.c’ file. Navigate to File → New → File
    image

  4. An untitled file should open. Select File → Save As
    image

  5. Navigate to your project directory (one level above the ‘iar’ folder you saved the project file in) and save the file as ‘main.c’. In my case the project directory is: → “Energy Micro” → “My_EFM32_Projects” → “Template_EFM32GG”
    image

  6. Now that the ‘main.c’ file has been created, we have to actually add it to our workspace. Right-click on the ‘source’ group you created and select Add → Add Files.
    image

  7. Select the ‘main.c’ file you just created.
    image

  8. Add two more groups to your workspace and call them ‘CMSIS’ and ‘emlib’. This is standard for EFM32 projects.

  9. Right-click the ‘CMSIS’ group and select Add → Add File.
    image

  10. When Simplicity Studio is installed, it saves all documentation as well as CMSIS, emlib, and device files in: C:\Users<user name>\AppData\Roaming\energymicro
    In the ‘energymicro’ folder, navigate to Device → EnergyMicro → EFM32GG → Source (if you aren’t using a Giant Gecko, navigate to the appropriate EFM32xx folder).Select the ‘system_efm32gg.c’ file and click ‘Open’ to add the file to your workspace. This file is included for all standard EFM32 projects.
    image

  11. Right-click the ‘CMSIS’ group and again select Add → Add File. Navigate to C:\Users<user name>\App Data\Roaming\energymicro\Device\EnergyMicro\EFM32GG\Source\IAR and open the ‘startup_efm32gg.s’ file to add it to your workspace. This file is needed in order to properly program the device.
    image

  12. Now you can add source files to the ‘emlib’ group that you previously created. You can add only the ones you need, or add all of them if you wish. Navigate to the ‘emlib’ folder which is located at C:\Users<user name>\AppData\Roaming\energymicro.
    Inside the ‘emlib’ folder you can open the ‘src’ folder and add any or all of the peripheral emlib libraries. I would recommend adding all of them to your workspace for you to easily browse through the available functions. That way you can view any file without having to navigate to each one using windows explorer. Use ‘Ctrl’+‘left mouse button’ to select individual files and click ‘Open’ to add them to your workspace.
    Note: Even if you don’t want to use the emlib libraries, you should still add the ‘em_system.c’ file to the emlib group so that you can call the Chip_Init() function at the beginning of your ‘main.c’ file. Chip_Init() addresses any known errata for your device and applies any known software fixes.
    image

  13. In order to use the functions located in these emlib files, you need to add #include for the respective header file in your ‘main.c’ file. For example, if you wanted to use any of the prewritten ADC functions in your ‘main.c’ file, you would need to include the ‘em_adc.h’ header file as shown in the following example. ‘em_system.h’ and ‘em_chip.h’ should be added to every EFM32 project. You can also add a blank main() function to your ‘main.c’ file.
    image

  14. Now let’s setup the project options. Right-click on “Template_EFM32GG.ewp” in the workspace explorer and select ‘Options’.
    image

  15. 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 EnergyMicro and select your specific device. For this demo, I selected EFM32GG990F1024 which is loaded on the EFM32GG Starter Kit.

  16. In the ‘C/C++ Compiler Settings’ category, under the ‘Preprocessor’ tab, you can add include directories for your device. You will need to add the paths to the include files for the CMSIS library and the emlib library if you wish to use the emlib API (recommended). You will also need to link to the ‘Include’ folder for the specific device. Example projects found in Energy Micro app notes use relative paths from the project directory to link to these directories. You can use hard-coded paths if you know the location of these won’t change. For my personal use, I hard code the paths directly in the Project Options (rather than using relative path). Silicon Labs recommends you use relative paths for portability. It’s completely up to you, but you need to always be aware of where the files are located and how you are linking to them.

In the ‘Defined symbols’ box, you also need to define your device symbol. For my project, I’ve defined the symbol EFM32GG990F1024. Here’s an example of hard-coding the path to the default install directory.
image

  1. In the ‘Output Converter’ category, check ‘Generate additional output’ and select ‘binary’ for output format.
    image

  2. In the ‘Linker’ category, select ‘Override default’ and navigate to the .icf file for your device. The file should be located in the IAR install directory, not the Simplicity Studio install directory:
    C:\Program Files (x86)\IAR Systems\Embedded Workbench 6.x\arm\config\linker\EnergyMicro.

  3. In the ‘Debugger’ category, under the ‘Setup’ tab, select ‘J-Link/J-Trace’ as the Driver since we are using the J-link debugger included on the EFM32GG Starter Kit. If you are using a different debugger, these settings may change.
    image

  4. Still in the ‘Debugger’ category, under the ‘Download’ tab, check the ‘Verify download’ and ‘Use flash loader(s)’ boxes.
    image

  5. In the ‘J-Link’ category, under the ‘Connection’ tab, select ‘SWD’ as the Interface. Click ‘OK’ to apply the project options.


25. Select File → Save All. Save the workspace in the ‘iar’ folder within your project directory.
image

  1. Right click on the project and click ‘Make’ to build the project
    image

  2. Now we have a template project with all of the necessary project settings. Whenever we wish to start a new EFM32 project for Giant Gecko, we can simply copy&paste the template project in whatever location we desire because we used hard-coded include paths. Once you paste a copy of the template, simply rename the project folder to whatever you wish. Also, I would recommend renaming 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
    image

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

Code Examples

The following examples were written using IAR embedded work bench with a 32KB size-limited free license. They are meant to help familiarize you with basic functionality of EFM32 Giant Gecko microcontrollers. For all of the following projects, I followed the procedure above for creating a new project (copy&paste created template). The examples are presented in order of difficulty and some build on previous examples. If you are brand new to EFM32 microcontrollers, I would recommend starting from the beginning and going through each of the examples in order. If you already have some experience with EFM32 microcontrollers, feel free to jump to any example you wish.

GPIO Example
This example demonstrates how to set pins as inputs with pull-up resistors enabled, as well as how to set and clear digital outputs. It also demonstrates how to adjust the drive strength of digital outputs. The EFM32GG allows the user to set the drive strength of a digital output with the following values:

STANDARD - 6mA drive current
LOWEST - 0.5mA drive current
HIGH - 20mA drive current
LOW - 2mA drive current
However, the EFM32GG Starter Kit uses 3 kOhm resistors which limits LED current to a max of about 1mA. Therefore, the HIGH and LOW drive settings show no effect. The LOWEST drive mode does enable the user to see visible change in LED intensity as a direct result of the drive mode setting. Therefore, LOWEST was used in the following example.

In the following example, LED0 is on by default with a standard drive strength of 6mA. LED1 is off by default. Pressing PB0 toggles LED1 and pressing PB1 toggles LED0. LED1 uses the lowest drive mode setting of 0.5mA. Even though the drive strength of LED0 is set to 6mA, the current limiting resistors reduce the current to approximately 1mA through LED0.

GPIO Example Using Direct Register Access

  • The following code demonstrates how 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 "efm32gg990f1024.h"
#include "em_chip.h"    // required for CHIP_Init() function
 
// LEDs on EFM32GG Starter Kit are connected to PortE, pin2 (LED0) and PortE, pin3 (LED1)
// Push-buttons are connected to PortB, pin9 (Push-button 0) and PortB, pin10 (Push-button 1)
 
enum {
  A,
  B,
  C,
  D,
  E
};
 
#define LED_PORT E
#define BUTTON_PORT B
#define LED0 2
#define LED1 3
#define PB0 9
#define PB1 10
 
int main() {
  CHIP_Init();                                         // This function addresses some chip errata and should be called at the start of every EFM32 application (need em_system.c)
   
  CMU->HFPERCLKEN0 = (1 << 13);                        // Enable GPIO clock
   
  // Configure LED0 pin as digital output (push-pull)
  // Configure LED1 pin as digital output (push-pull) with drive-strength set by DRIVEMODE
  GPIO->P[LED_PORT].MODEL = (5 << 12) | (4 << 8);        
                              
  GPIO->P[LED_PORT].CTRL = 1;                          // Set DRIVEMODE to lowest setting (0.5 mA) for all LEDs configured with alternate drive strength
  GPIO->P[BUTTON_PORT].MODEH = (2 << 4) | (2 << 8);    // Configure PB0 and PB1 as inputs
  GPIO->P[BUTTON_PORT].DOUT = (1 << PB1) | (1 << PB0); // Enable Pull-ups on PB0 and PB1
   
  while(1) {
    if(! (GPIO->P[BUTTON_PORT].DIN & (1 << PB0)) ) {   // If PB0 is pressed
      GPIO->P[LED_PORT].DOUTSET = 1 << LED1;           // Turn on LED1
    }else{                                             // If PB0 is released
      GPIO->P[LED_PORT].DOUTCLR = 1 << LED1;           // Turn off LED1
    }
    if(! (GPIO->P[BUTTON_PORT].DIN & (1 << PB1)) ) {   // If PB1 is pressed
      GPIO->P[LED_PORT].DOUTCLR = 1 << LED0;           // Turn off LED0
    }else{                                             // If PB1 is released
      GPIO->P[LED_PORT].DOUTSET = 1 << LED0;           // Turn on LED0
    }
  }
}

GPIO Example Using emlib API

  • The following code demonstrates how the emlib libraries can be used to implement the same GPIO example. For the following code to work, the following emlib source files must be added to the workspace:
  • em_cmu.c
  • em_gpio.c
  • em_system.c
#include "em_device.h"
#include "em_cmu.h"
#include "em_gpio.h"
#include "em_system.h"
#include "em_chip.h"    // required for CHIP_Init() function
 
#define LED_PORT gpioPortE
#define BUTTON_PORT gpioPortB
#define LED0 2
#define LED1 3
#define PB0 9
#define PB1 10
 
int main() {
  CHIP_Init();                                               // This function addresses some chip errata and should be called at the start of every EFM32 application (need em_system.c)
   
  CMU_ClockEnable(cmuClock_GPIO, true);                      // Enable GPIO peripheral clock
   
  GPIO_PinModeSet(LED_PORT, LED0, gpioModePushPull, 0);      // Configure LED0 pin as digital output (push-pull)
  GPIO_PinModeSet(LED_PORT, LED1, gpioModePushPullDrive, 1); // Configure LED1 pin as digital output (push-pull) with drive-strength to lowest setting
  GPIO_DriveModeSet(LED_PORT, gpioDriveModeLowest);          // Set DRIVEMODE to lowest setting (0.5 mA) for all LEDs configured with alternate drive strength
  GPIO_PinModeSet(BUTTON_PORT, PB0, gpioModeInputPull, 1);   // Configure PB0 as input with pull-up enabled
  GPIO_PinModeSet(BUTTON_PORT, PB1, gpioModeInputPull, 1);   // Configure PB1 as input with pull-up enabled
   
  while(1) {
    if(! (GPIO_PinInGet(BUTTON_PORT, PB0)) ) {               // If PB0 is pressed
      GPIO_PinOutSet(LED_PORT, LED1);                        // Turn on LED1
    }else{
      GPIO_PinOutClear(LED_PORT, LED1);                      // Turn off LED1
    }
    if(! (GPIO_PinInGet(BUTTON_PORT, PB1)) ) {               // If PB1 is pressed
      GPIO_PinOutClear(LED_PORT, LED0);                      // Turn off LED0
    }else{
      GPIO_PinOutSet(LED_PORT, LED0);                        // Turn on LED0
    }
  }
}

Timer with Interrupt
The following code uses a timer with interrupt to blink an LED. It uses the default clock source which is the internal high speed RC oscillator. It sets the RC oscillator to 1MHz and uses a timer to interrupt every millisecond. After 500 ms, it toggles LED0.

  • Timer with Interrupt Using Direct Register Access
    The following code demonstrates how a basic timer can be implemented using direct register access. For the following code to work, the ‘em_system.c’ file needs to be added to the workspace.
#include "efm32gg990f1024.h"
#include "em_chip.h" // required for CHIP_Init() function
 
#define LED_PORT 4 // gpioPortE
#define LED_PIN1 2
#define LED_PIN2 3
 
uint16_t ms_counter = 0;
 
void TIMER0_IRQHandler(void) {
  TIMER0->IFC = 1;                              // Clear overflow flag
  ms_counter++;                                 // Increment counter
}
 
int main() {
  CHIP_Init();                                  // This function addresses some chip errata and should be called at the start of every EFM32 application (need em_system.c)
   
  CMU->HFRCOCTRL = 0x8;                         // Set High Freq. RC Osc. to 1 MHz
  CMU->HFPERCLKEN0 = (1 << 13) | (1 << 5);      // Enable GPIO and Timer0 peripheral clocks
   
  GPIO->P[LED_PORT].MODEL = 4 << 8;             // Configure LED pin as digital output (push-pull)
   
  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(ms_counter == 500) {
      GPIO->P[LED_PORT].DOUTTGL = 1 << LED_PIN1;// Toggle LED
      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 following code to work, the following emlib source files must be added to the workspace:
  • em_cmu.c
  • em_gpio.c
  • em_timer.c
  • em_system.c
#include "em_device.h"
#include "em_cmu.h"
#include "em_gpio.h"
#include "em_system.h"
#include "em_timer.h"
#include "em_chip.h"
  
#define LED_PORT gpioPortE
#define LED_PIN  2
  
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();                               // This function addresses some chip errata and should be called at the start of every EFM32 application (need em_system.c)
  
  CMU_HFRCOBandSet(cmuHFRCOBand_1MHz);      // Set High Freq. RC Osc. to 1 MHz
  CMU_ClockEnable(cmuClock_GPIO, true);     // Enable GPIO peripheral clock
  CMU_ClockEnable(cmuClock_TIMER0, true);   // Enable TIMER0 peripheral clock
  
  GPIO_PinModeSet(LED_PORT, LED_PIN, gpioModePushPullDrive, 0); // Configure LED0 pin as digital output (push-pull)
    
  TIMER_TopSet(TIMER0, 1000);               // Set timer TOP value
  TIMER_Init_TypeDef timerInit =            // Setup Timer initialization
  {
    .enable     = true,                     // Start timer upon configuration
    .debugRun   = true,                     // Keep timer running even on debug halt
    .prescale   = timerPrescale1,           // Use /1 prescaler...timer clock = HF clock = 1 MHz
    .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, not one-shot
    .sync       = false,                    // Not synchronizing timer operation off of other timers
  };
  TIMER_IntEnable(TIMER0, TIMER_IF_OF);     // Enable Timer0 overflow interrupt
  NVIC_EnableIRQ(TIMER0_IRQn);              // Enable TIMER0 interrupt vector in NVIC
  TIMER_Init(TIMER0, &timerInit);           // Configure and start Timer0
  
  while(1)
  {
    if(ms_counter == 500) {
      GPIO_PinOutToggle(LED_PORT, LED_PIN); // Toggle LED
      ms_counter = 0;                       // Reset counter
    }
  }
}

Using the HF XTAL Oscillator
The following example is similar to the Timer with Interrupt example shown above. However, this example uses the on-board 48MHz crystal as the High Frequency Clock source. The code uses the HF clock divider to set the HF clock rate to 24MHz. The High Frequency Peripheral clock and the High Frequency Core clock prescalers were left at /1. Therefore, the Timer, GPIO peripherals along with the Core clocks all run at 24MHz in this example. The TOP value of the counter was set to 24000 so that the overflow interrupt occurs every millisecond. Every 500 ms, LED0 toggles.
HF XTAL Example Using Direct Register Access

  • The following code block shows exactly which registers to write to in order to properly configure and use the HF XTAL oscillator. For the following code to work, the ‘em_system.c’ file needs to be added to the workspace.
#include "efm32gg990f1024.h"
#include "em_chip.h"    // required for CHIP_Init() function
 
#define LED_PORT 4 // gpioPortE
#define LED_PIN1 2
#define LED_PIN2 3
 
uint16_t ms_counter = 0;
 
void TIMER0_IRQHandler(void) {
  TIMER0->IFC = 1;                              // Clear overflow flag
  ms_counter++;                                 // Increment counter
}
 
int main() {
  CHIP_Init();                                  // This function addresses some chip errata and should be called at the start of every EFM32 application (need em_system.c)
   
  CMU->CTRL |= (1 << 14);                       // Set HF clock divider to /2 to keep core frequency <32MHz
  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. 48MHz XTAL, but we divided the system clock by 2, therefore our HF clock should be 24MHz
   
  CMU->HFPERCLKEN0 = (1 << 13) | (1 << 5);      // Enable GPIO and Timer0 peripheral clocks
   
  GPIO->P[LED_PORT].MODEL = 4 << 8;             // Configure LED pin as digital output (push-pull)
   
  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
   
  while(1) {
    if(ms_counter == 500) {
      GPIO->P[LED_PORT].DOUTTGL = 1 << LED_PIN1;// Toggle LED
      ms_counter = 0;                           // Reset counter
    }
  }
}

HF XTAL Example Using emlib API

  • The following code block achieves the exact same functionality as the above example using emlib functions. 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_cmu.h"
#include "em_gpio.h"
#include "em_timer.h"
#include "em_system.h"
#include "em_chip.h"  // required for CHIP_Init() function
 
#define LED_PORT gpioPortE
#define LED_PIN 2
 
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(); // This function addresses some chip errata and should be called at the start of every EFM32 application (need em_system.c)
   
  CMU_ClockDivSet(cmuClock_HF, cmuClkDiv_2);               // Set HF clock divider to /2 to keep core frequency <32MHz
  CMU_OscillatorEnable(cmuOsc_HFXO, true, true);           // Enable XTAL Osc and wait to stabilize
  CMU_ClockSelectSet(cmuClock_HF, cmuSelect_HFXO);         // Select HF XTAL osc as system clock source. 48MHz XTAL, but we divided the system clock by 2, therefore our HF clock will be 24MHz
   
  CMU_ClockEnable(cmuClock_GPIO, true);                    // Enable GPIO peripheral clock
  CMU_ClockEnable(cmuClock_TIMER0, true);                  // Enable TIMER0 peripheral clock
   
  GPIO_PinModeSet(LED_PORT, LED_PIN, gpioModePushPull, 0); // Configure LED0 pin as digital output (push-pull)
  GPIO_PinOutClear(LED_PORT, LED_PIN);                     // Turn off LED0
   
  TIMER_TopSet(TIMER0, 24000);                             // Set timer TOP value
  TIMER_Init_TypeDef timerInit =                           // Setup Timer initialization
  {
    .enable     = true,
    .debugRun   = true,
    .prescale   = timerPrescale1,
    .clkSel     = timerClkSelHFPerClk,
    .fallAction = timerInputActionNone,
    .riseAction = timerInputActionNone,
    .mode       = timerModeUp,
    .dmaClrAct  = false,
    .quadModeX4 = false,
    .oneShot    = false,
    .sync       = false,
  };
  TIMER_IntEnable(TIMER0, TIMER_IF_OF);     // Enable Timer0 overflow interrupt
  NVIC_EnableIRQ(TIMER0_IRQn);              // Enable TIMER0 interrupt vector in NVIC
  TIMER_Init(TIMER0, &timerInit);           // Configure and start Timer0
   
  while(1) {
    if(ms_counter == 500) {
      GPIO_PinOutToggle(LED_PORT, LED_PIN); // Toggle LED
      ms_counter = 0;                       // Reset counter
    }
  }
}

PWM Example
This example demonstrates how to setup PWM output on the EFM32 Giant Gecko. It uses Capture/Compare Channel 2 of Timer3 which is available on PortE, Pin2 of the microcontroller. You may remember from the “GPIO Example” above, PE2 is connected externally to LED0 on the EFM32 Giant Gecko Starter Kit. Therefore, we can use PWM output to control the intensity of LED0.

This example uses 2 different timers. Timer3 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 Timer3 with a prescaler of /1 for each timer. Therefore, Timer0, Timer3, 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 Timer3 were both 1ms (1kHz freq).

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

#include "efm32gg990f1024.h"
#include "em_chip.h"            // required for CHIP_Init() function
 
#define LED_PORT 4              // gpioPortE
#define LED_PIN0 2              // LED0 is connected to PortE pin2
#define LED_PIN1 3              // LED1 is connected to PortE pin3
#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
 
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() {
   
  uint16_t compare_val = 0; // Initial PWM duty cycle is 0% (LED0 off)
  uint8_t inc = 1;          // Increment = true
   
  CHIP_Init();              // This function addresses some chip errata and should be called at the start of every EFM32 application (need em_system.c)
    
  CMU->HFRCOCTRL = 0x8;                               // Set High Freq. RC Osc. to 1 MHz and use as system source
  CMU->HFPERCLKEN0 = (1 << 13) | (1 << 8) | (1 << 5); // Enable GPIO, Timer0, and Timer3 peripheral clocks
  GPIO->P[LED_PORT].MODEL = (4 << 12) | (4 << 8);     // Configure LED0 and LED1 pins as digital outputs (push-pull)
  GPIO->P[LED_PORT].DOUTSET = (1 << LED_PIN1);        // Turn on LED1 (PE3)
   
  TIMER0->TOP = TOP_VAL_GP_TIMER;                     // GP Timer period will be 1ms = 1kHz freq
  TIMER3->TOP = TOP_VAL_PWM;                          // PWM period will be 1ms = 1kHz freq
   
  TIMER0->CNT = 0;                                    // Start counter at 0 (up-count mode)
  TIMER3->CNT = 0;                                    // Start counter at 0 (up-count mode)
   
  TIMER3->CC[2].CCV = compare_val;                    // Set CC2 compare value (0% duty)
  TIMER3->CC[2].CCVB = compare_val;                   // Set CC2 compare buffer value (0% duty)
   
  TIMER0->IEN = 1;                                    // Enable Timer0 overflow interrupt
  NVIC_EnableIRQ(TIMER0_IRQn);                        // Enable TIMER0 interrupt vector in NVIC
   
  TIMER3->CC[2].CTRL = 0x3;                           // Put Timer3 CC channel 2 in PWM mode
  TIMER3->ROUTE = (1 << 16) | (1 << 2);               // Connect PWM output (timer3, channel 2) to PE2 (LED0). See EFM32GG990 datasheet for details.
   
  TIMER0->CTRL = (1 << 6);                            // Allow timer to run while in debug mode
  TIMER3->CTRL = (1 << 6);                            // Allow timer to run while in debug mode
   
  TIMER0->CMD = 0x1;                                  // Start Timer0
  TIMER3->CMD = 0x1;                                  // Start Timer3
   
  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
      }
      TIMER3->CC[2].CCVB = compare_val;            // Write new value to compare buffer
      GPIO->P[LED_PORT].DOUTTGL = (1 << LED_PIN1); // 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 code block demonstrates LED0 intensity control using PWM output capability of Timer3 (channel 2). 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_cmu.h"
#include "em_chip.h"            // required for CHIP_Init() function
#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
 
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();                            // This function addresses some chip errata and should be called at the start of every EFM32 application (need em_system.c)
  CMU_HFRCOBandSet(cmuHFRCOBand_1MHz);    // Set HF oscillator to 1MHz and use as system source
  CMU_ClockEnable(cmuClock_GPIO, true);   // Start GPIO peripheral clock
  CMU_ClockEnable(cmuClock_TIMER0, true); // Start TIMER0 peripheral clock
  CMU_ClockEnable(cmuClock_TIMER3, true); // Start TIMER3 peripheral clock
   
  GPIO_PinModeSet(gpioPortE, 2, gpioModePushPull, 0); // set LED0 pin as push-pull output
  GPIO_PinModeSet(gpioPortE, 3, gpioModePushPull, 1); // set LED0 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_CU_TIMER);      // GP Timer period will be 1ms = 1kHz freq
  TIMER_TopSet(TIMER3, TOP_VAL_PWM);           // PWM period will be 1ms = 1kHz freq
   
  TIMER_CounterSet(TIMER0, 0);                 // Start counter at 0 (up-count mode)
  TIMER_CounterSet(TIMER3, 0);                 // Start counter at 0 (up-count mode)
   
  TIMER_CompareSet(TIMER3, 2, compare_val);    // Set CC2 compare value (0% duty)
  TIMER_CompareBufSet(TIMER3, 2, compare_val); // Set CC2 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(TIMER3, 2, &timerCCInit);       // apply channel configuration to Timer3 channel 2
  TIMER3->ROUTE = (1 << 16) |(1 << 2);         // connect PWM output (timer3, channel 2) to PE2 (LED0). See EFM32GG990 datasheet for details.
     
  TIMER_Init(TIMER0, &timerGPInit);            // apply general purpose configuration to timer0
  TIMER_Init(TIMER3, &timerPWMInit);           // apply PWM configuration to timer3
   
  while(1) {
    if(ms_counter == UPDATE_PERIOD) {
      if(inc) {                                    // If increment = true
        compare_val += INC_VAL;                    // Increase the compare value
      }else{                                       // increment = false
        compare_val -= INC_VAL;                    // Decrease the compare value
      }
      TIMER_CompareBufSet(TIMER3, 2, compare_val); // Write new value to compare buffer
      GPIO_PinOutToggle(gpioPortE, 3);             // 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
  }
}

UART Example
This example shows how to setup the USART in asynchronous mode (essentially a 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 Giant Gecko was setup to use the 48MHz high frequency crystal oscillator. The HF clock devider was set to /2 to achieve a system clock of 24 MHz just like in the “HF XTAL Oscillator” example above.

For this demo, the Giant Gecko UART port was setup with the following serial communication settings:

  • Baud Rate (bps): 38400
  • Data Bits: 8
  • Parity: none
  • Stop Bits: 1

The code examples found below were tested using a custom expansion board which has USART1 available on an external connector. The custom expansion board was connected to a PC using the TTL-232R-3V3 smart cable from FTDI. A terminal emulator program (Tera Term) was used to display characters transmitted to and from the Giant Gecko. The serial port of the terminal program was setup with the following settings:

  • Baud Rate (bps): 38400
  • 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 "efm32gg990f1024.h"
#include "em_chip.h"    // required for CHIP_Init() function
#include <string.h>
 
#define COM_PORT 3 // gpioPortD (USART location #1: PD0 and PD1)
#define UART_TX_pin 0
 
int main() {
  CHIP_Init();                                   // This function addresses some chip errata and should be called at the start of every EFM32 application (need em_system.c)
  uint8_t i;
  char test_string[] = "\n\rHello World!\n\r";
  char rx_char = 0;                              // Temp variable for storing received characters
   
  CMU->CTRL |= (1 << 14);                         // Set HF clock divider to /2 to keep core frequency <32MHz
  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. 48MHz XTAL, but we divided the system clock by 2, therefore our HF clock should be 24MHz
   
  CMU->HFPERCLKEN0 = (1 << 13) | (1 << 1);        // Enable GPIO, and USART1 peripheral clocks
    
  GPIO->P[COM_PORT].MODEL = (1 << 4) | (4 << 0);  // Configure PD0 as digital output and PD1 as input
  GPIO->P[COM_PORT].DOUTSET = (1 << UART_TX_pin); // Initialize PD0 high since UART TX idles high (otherwise glitches can occur)
   
  // Use default value for USART1->CTRL: asynch mode, x16 OVS, lsb first, CLK idle low
  // Default frame options: 8-none-1-none
  USART1->CLKDIV = (152 << 6);                               // 152 will give 38400 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 = 0x103;                                     // Enable TX and RX pins, use location #1 (UART TX and RX located at PD0 and PD1, see EFM32GG990 datasheet for details)
   
  // Print test string
  for(i=0; i<strlen(test_string); i++) {
    while( !(USART1->STATUS & (1 << 6)) ); // wait for TX buffer to empty
    USART1->TXDATA = test_string[i];       // print each character of the test string
  }
   
  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 char
        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_cmu.h"
#include "em_gpio.h"
#include "em_usart.h"
#include "em_system.h"
#include "em_chip.h"    // required for CHIP_Init() function
#include <string.h>     // required for strlen() function
 
#define COM_PORT gpioPortD // USART location #1: PD0 and PD1
#define UART_TX_pin 0      // PD0
#define UART_RX_pin 1      // PD1
 
int main() {
  CHIP_Init(); // This function addresses some chip errata and should be called at the start of every EFM32 application (need em_system.c)
   
  uint8_t i;
  char test_string[] = "\n\rHello World!\n\r";     // Test string
  char rx_char = 0;                                // Temp variable for storing received characters
   
  CMU_ClockDivSet(cmuClock_HF, cmuClkDiv_2);       // Set HF clock divider to /2 to keep core frequency < 32MHz
  CMU_OscillatorEnable(cmuOsc_HFXO, true, true);   // Enable XTAL Osc and wait to stabilize
  CMU_ClockSelectSet(cmuClock_HF, cmuSelect_HFXO); // Select HF XTAL osc as system clock source. 48MHz XTAL, but we divided the system clock by 2, therefore our HF clock will be 24MHz
   
  CMU_ClockEnable(cmuClock_GPIO, true);            // Enable GPIO peripheral clock
  CMU_ClockEnable(cmuClock_USART1, true);          // Enable USART1 peripheral clock
   
  GPIO_PinModeSet(COM_PORT, UART_TX_pin, gpioModePushPull, 1); // Configure UART TX pin as digital output, initialize high since UART TX idles high (otherwise glitches can occur)
  GPIO_PinModeSet(COM_PORT, UART_RX_pin, gpioModeInput, 0);    // Configure UART RX pin as input (no filter)
   
  USART_InitAsync_TypeDef uartInit =
  {
    .enable       = usartDisable,   // Wait to enable the transmitter and receiver
    .refFreq      = 0,              // Setting refFreq to 0 will invoke the CMU_ClockFreqGet() function and measure the HFPER clock
    .baudrate     = 38400,          // Desired baud rate
    .oversampling = usartOVS16,     // Set oversampling value 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 which channel we select
  };
   
  USART_InitAsync(USART1, &uartInit);                                              // Apply configuration struct to USART1
  USART1->ROUTE = UART_ROUTE_RXPEN | UART_ROUTE_TXPEN | _UART_ROUTE_LOCATION_LOC1; // Clear RX/TX buffers and shift regs, enable transmitter and receiver pins
   
  USART_IntClear(USART1, _UART_IF_MASK); // Clear any USART interrupt flags
  NVIC_ClearPendingIRQ(UART1_RX_IRQn);   // Clear pending RX interrupt flag in NVIC
  NVIC_ClearPendingIRQ(UART1_TX_IRQn);   // Clear pending TX interrupt flag in NVIC
   
  USART_Enable(USART1, usartEnable);     // Enable transmitter and receiver
   
  // Print test string
  for(i=0; i<strlen(test_string); i++) {
    while( !(USART1->STATUS & (1 << 6)) ); // wait for TX buffer to empty
    USART1->TXDATA = test_string[i];       // print each character of the test string
  }
   
  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 char
        rx_char = 0;                  // reset temp variable
      }
    }
  }
}

ADC Example
This example shows how to setup the Analog to Digital Converter (ADC) in the Giant Gecko using a custom expansion board that interfaces with the STK3700.

A potentiometer loaded on the expansion board was used as the analog source. The potentiometer provided a controllable analog voltage source varying between 0~3.3V with the center tap connected to PD6 (ADC0 Channel 6) of the Giant Gecko. Therefore, 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. The USART 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, the 48MHz crystal oscillator was used with a /2 prescaler 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 demonstrate 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 "efm32gg990f1024.h"
#include "em_chip.h"    // required for CHIP_Init() function
  
#define COM_PORT 3 // gpioPortD (USART location #1: PD0 and PD1)
#define ADC_PORT 3 // gpioPortD (ADC Channel 6 location #0: PD6)
#define TX_pin 0
#define ADC_pin 6 // ADC Channel 6
 
void set_decade(uint16_t val);
uint8_t conv_ascii(uint16_t val);
  
uint16_t ms_counter = 0;
char header[] = "\n\rEFM32 Giant Gecko - ADC Example\n\r";
uint8_t digit_array[7] = { 0x20, 0x20, 0x20, 0x20, 0x20, 'm', 'V' }; // Array for displaying ADC result, initialize to "     mV"
  
void TIMER0_IRQHandler(void) {
  TIMER0->IFC = 1; // Clear overflow flag
  ms_counter++;    // Increment counter
}
 
int main() {
  CHIP_Init();                   // This function addresses some chip errata and should be called at the start of every EFM32 application (need em_system.c)
   
  uint8_t i;
  uint16_t adc_result = 0;       // Temp variable for storing ADC conversion results
  uint32_t temp;
   
  // Initialize Clock Tree
  CMU->CTRL |= (1 << 14);        // Set HF clock divider to /2 to keep core frequency <32MHz
  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. 48MHz XTAL, but we divided the system clock by 2, therefore our HF clock should be 24MHz
  CMU->HFPERCLKEN0 = (1 << 16) | (1 << 13) | (1 << 5) | (1 << 1); // Enable GPIO, TIMER0, USART1, and ADC0 peripheral clocks
   
  // Initialize GPIO 
  GPIO->P[COM_PORT].MODEL = (1 << 24) | (1 << 4) | (4 << 0); // Configure PD0 as digital output, PD1 and PD6 as input
  GPIO->P[COM_PORT].DOUTSET = (1 << TX_pin);                 // Initialize PD0 high since UART TX idles high (otherwise glitches can occur)
   
  // Setup UART Port for asynch mode, frame format 8-none-1-none
  USART1->CLKDIV = (152 << 6);                               // 152 will give 38400 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 = 0x103;                                     // Enable TX and RX pins, use location #1 (UART TX and RX located at PD0 and PD1, see EFM32GG990 datasheet for details)
    
  // 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) | (6 << 8);
  ADC0->IEN = 0x0; // Disable ADC interrupts
   
  // Setup Timer to trigger conversions
  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(ms_counter == 500) {
       
      // 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
      temp = adc_result*3300;
      adc_result = temp/4095;
      set_decade(adc_result); // 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;
}

SPI Example
This example shows how to setup the Serial Peripheral Interface (SPI) in the Giant Gecko using a custom expansion board that interfaces with the STK3700.

A flash memory IC (W25X20CL) loaded on the expansion board was used as the SPI target. The flash IC SPI port (MOSI, MISO, SCLK, CS) was connected to USART1 (Location #1) through PortD - PD[3:0]. 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 Giant Gecko does offer automatic CS functionality). The flash IC also has active low “Hold” and “Write Protect” pins connected to the Giant Gecko. However, these pins are not used in this example, except to drive the “Hold” pin high and the “Write Protect” pin low.

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 W24X20CL, these values were 0xEF and 0x11, respectively.

After the response was received, USART0 was used to transmit the response to a terminal program on a PC. USART0 was setup in asynchronous mode with the same serial communication settings as the “UART Example” shown above. However, for this example, PC0 (USART0 Location #5) was used as the transmit pin. The receive pin at Location #5 is not available through the expansion port of the STK3700 while in full duplex mode. This isn’t a problem since we are simply transmitting results to the PC, not entering values from the PC. USART0 TX pin and GND were connected to the RX and GND pins of TTL-232R-3V3 smart cable from FTDI which was then connected to the USB port of the PC.

To provide a high accuracy clock for UART communication, the 48MHz crystal oscillator was used with a /2 prescaler to generate a 24MHz System Clock, just like the “HF XTAL Oscillator” example above.

Below is a screenshot of the terminal window while running this demo application.
image
SPI Example Using Direct Register Access

  • The following example code demonstrate how to setup SPI communication using direct register access. For the following code to work, the ‘em_system.c’ file needs to be added to the workspace.
#include "efm32gg990f1024.h"
#include "em_chip.h"    // required for CHIP_Init() function
 
#define SPI_USART USART1
#define SPI_COM_PORT 3 // gpioPortD (USART1 location #1)
#define SPI_TX_pin 0   // PD0 - MOSI
#define SPI_RX_pin 1   // PD1 - MISO
#define SPI_CS_pin 3   // PD3 - Chip Select
 
#define COM_USART USART0
#define EXT_COM_PORT 2 // gpioPortC (USART0 location #5)
#define EXT_TX_pin 0   // PC0 - TX
 
#define RX_BUFFER_SIZE 2
 
// SPI functions
void CS_pin_set(void);
void CS_pin_clr(void);
void SPI_WriteByte(uint8_t byte);
uint8_t SPI_ReadByte(void);
 
// UART function
void print_byte(uint8_t byte);
 
// Misc. functions
void get_device_id(uint8_t storage_array[], uint8_t start_index); // requires at least a 2-element storage array
void print_byte_setup(uint8_t byte);
 
// Global variable
uint8_t print_byte_array[4] = { 0x30, 0x78, 0x20, 0x20 }; // array for displaying bytes one nibble at a time
 
int main() {
  CHIP_Init(); // This function addresses some chip errata and should be called at the start of every EFM32 application (need em_system.c)
   
  uint8_t i;
  char test_string[] = "\n\rManuf/Device ID\n\r";
  uint8_t rx_buffer[RX_BUFFER_SIZE]; // array for storing the 2-byte 'Device ID' response
   
  // Clear rx buffer
  for(i=0; i<RX_BUFFER_SIZE; i++) {
    rx_buffer[i] = 0;
  }
   
  // Setup Clock Tree
  CMU->CTRL |= (1 << 14);                             // Set HF clock divider to /2 to keep core frequency <32MHz
  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. 48MHz XTAL, but we divided the system clock by 2, therefore our HF clock should be 24MHz
  CMU->HFPERCLKEN0 = (1 << 13) | (1 << 1) | (1 << 0); // Enable GPIO, USART0, and USART1 peripheral clocks
   
  // Configure GPIO
  GPIO->P[SPI_COM_PORT].MODEL = (4 << 20) | (4 << 16) | (4 << 12) | (4 << 8) | (1 << 4) | (4 << 0); // Configure PD0 (MOSI), PD2 (SCK), PD3 (CS), PD4 (nHLD), and PD5 (nWP) as digital outputs and PD1 as input
  GPIO->P[SPI_COM_PORT].DOUTSET = (1 << 4) | (1 << 3); // Initialize CS and nHLD high, nWP low
  GPIO->P[EXT_COM_PORT].MODEL = (4 << 0);              // Configure PC0 as digital output
  GPIO->P[EXT_COM_PORT].DOUTSET = (1 << EXT_TX_pin);   // Initialize PC0 high since UART TX idles high (otherwise glitches can occur)
   
  // Configure SPI Port (USART1 in sync mode)
  SPI_USART->CTRL = (1 << 30) | (1 << 10) | (1 << 0);           // Transmit MSB first, use SPI mode 0 (0,0), Disable majority voting, use manual chip-select
  SPI_USART->CLKDIV = (0 << 6);                                 // Leave CLKDIV set to 0, leads to a baud rate of 12Mbps
  SPI_USART->CMD = (1 << 11) | (1 << 10) | (1 << 4) | (1 << 2) | (1 << 0); // Clear RX and TX buffers and shift regs, enable master mode, enable transmitter/receiver
  SPI_USART->IFC = 0x1FF9;                                      // Clear USART1 interrupt flags
  SPI_USART->ROUTE = (1 << 8) | (1 << 3) | (1 << 1) | (1 << 0); // Use USART1 location #1, enable CLK, MISO, and MOSI pins
   
  // Configure COM Port (USART0 in async mode)
  COM_USART->CLKDIV = (152 << 6);         // 152 will give 38400 baud rate (using 16-bit oversampling with 24MHz peripheral clock)
  COM_USART->CMD = (1 << 10) | (1 << 2);  // Clear TX buffer and shif reg, Enable Master mode, Enable Transmitter
  COM_USART->IFC = 0x1FF9;                // clear all USART interrupt flags
  COM_USART->ROUTE = (5 << 8) | (1 << 1); // Enable TX pin, use location #5 (UART TX located at PC0, see EFM32GG990 datasheet for details)
   
  // Print test string
  i=0;
  while(test_string[i] != 0){
    print_byte(test_string[i++]);
  }
   
  // Send 'Device ID' command over SPI
  get_device_id(rx_buffer, 0);       // store result in rx_buffer starting at element 0
   
  // Display Manufacturer ID
  print_byte_setup(rx_buffer[0]);    // break up byte into individual digits (nibbles)
  for(i=0; i<4; i++) {
    print_byte(print_byte_array[i]); // Transmit high and low nibbles over UART
  }
   
  print_byte(0x20); // send whitespace 
   
  // Display Device ID
  print_byte_setup(rx_buffer[1]);    // break up byte into individual digits (nibbles)
  for(i=0; i<4; i++) {
    print_byte(print_byte_array[i]); // Transmit high and low nibbles over UART
  }
   
  print_byte('\r'); // send CR
  print_byte('\n'); // send LF    
   
  while(1);
}
 
// This function is used to display bytes over UART
void print_byte(uint8_t byte) {
  while( !(COM_USART->STATUS & (1 << 6)) ); // wait for TX buffer to empty
  COM_USART->TXDATA = byte;                 // send byte over UART
}
 
// This function drives the CS pin low
void CS_pin_clr(void) {
  GPIO->P[SPI_COM_PORT].DOUTCLR = (1 << SPI_CS_pin);
}
 
// This function drives the CS pin high
void CS_pin_set(void) {
  GPIO->P[SPI_COM_PORT].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( !(SPI_USART->STATUS & (1 << 6)) ); // wait for tx buffer to empty
  SPI_USART->TXDATA = byte;                
}
 
// This function returns the value of the RX buffer (waits for valid RX data)
uint8_t SPI_ReadByte(void) {
  while(! (SPI_USART->STATUS & (1 << 7)) ); // wait for valid RX data
  return(SPI_USART->RXDATA);         // clear RX buffer
}
 
// This function gets the manuf/device ID from the flash IC
void get_device_id(uint8_t storage_array[], uint8_t start_index) {
  uint8_t i;
   
  // drive CS pin low
  CS_pin_clr();  
   
  // send 'Device ID' command
  SPI_WriteByte(0x90);          
  storage_array[start_index] = SPI_ReadByte(); // clear RX buffer
   
  // send 3 '00' bytes
  for(i=0; i<3; i++) {
    SPI_WriteByte(0x00);          
    storage_array[start_index] = SPI_ReadByte(); // clear RX buffer each time  
  }
   
  // read manufacturer id
  SPI_WriteByte(0xFF);           // send dummy byte to receive manuf. id
  storage_array[start_index] = SPI_ReadByte(); // store manuf. id
  SPI_WriteByte(0xFF);           // send dummy byte to receive device id
  storage_array[start_index+1] = SPI_ReadByte(); // store device id     
   
  // drive CS pin high
  CS_pin_set();
}
 
// 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
This example shows how to setup the Inter-Integrated Circuit (I2C) peripheral in the Giant Gecko using a custom expansion board that interfaces with the STK3700.

An EEPROM memory IC loaded on the expansion board was used as the I2C slave device. The EEPROM was connected to I2C1 (Location #0) through PortC - PC[5:4]. I2C1 was configured in master mode using standard speed (100kHz). The auto-acknowledge feature of the Giant Gecko I2C was used for this demo to simplify the code. The EEPROM also has an active high “Write Protect” pin connected to PC3 which needs to be driven low to enable memory writes.

The example code below includes multiple functions involving interaction with the EEPROM:

  • read_byte() - reads the contents of the specified address in memory
  • write_byte() - writes a byte of data to a specific address in memory
  • read_block() - performs a sequential read of an entire block (256 bytes) of memory
  • write_page() - writes up to 16 bytes of memory in a single I2C transaction
  • chip_is_busy() - checks if the EEPROM is able to communicate

USART1 was setup to print responses in a terminal program. USART1 was setup in asynchronous mode at Location #1 with the same serial settings as the examples above. USART1 was connected to the PC using TTL-232R-3V3 smart cable from FTDI.

To provide a high accuracy clock for UART communication, the 48MHz crystal oscillator was used with a /2 prescaler to generate a 24MHz System Clock, just like the “HF XTAL Oscillator” example above.

Below is a screenshot of the terminal window while running this demo application. The following screenshot shows a portion of Block 1 after the write_byte() function was used to write 0x23 to address 0x02 of Block 1. The page_write() function was used to write 0x00 to each byte of Page 1 as seen in the terminal window below.


I2C Example Using Direct Register Access

  • The following code block demonstrates I2C communication using direct register access. For the following code to work, the ‘em_system.c’ file needs to be added to the workspace.
// All of the functions involving direct I2C communication with the EEPROM wait
// for the I2C bus to become idle and then wait until the EEPROM is ready to
// communicate. All "write" functions wait until the EEPROM completes the memory
// write before returning.
 
#include  "efm32gg990f1024.h"
#include  "em_chip.h"        // required for CHIP_Init() function
 
#define   I2C_PORT     2     // gpioPortC
#define   I2C_SDA      4     // PC4
#define   I2C_SCLK     5     // PC5
#define   I2C_WP       3     // PC3 - Write protect pin for EEPROM IC
#define   UART_PORT    3     // gpioPortD (USART location #1: PD0 and PD1)
#define   UART_TX      0     // UART TX: PD0
#define   LED_PORT     1     // gpioPortB
#define   LED_PIN0     11    // LED0: PB11
#define   LED_PIN1     12    // LED1: PB12
#define   BLOCK_SIZE   256   // 256 bytes per block
#define   PAGE_SIZE    16    // 16 bytes per page
 
// Functions involving direct I2C communication with EEPROM
uint8_t read_byte(uint8_t memory_block, uint8_t address, uint8_t* response);
uint8_t write_byte(uint8_t memory_block, uint8_t address, uint8_t data);
uint8_t read_block(uint8_t memory_block, uint8_t buffer[], uint16_t buffer_size);
uint8_t write_page(uint8_t memory_block, uint8_t page_num, uint8_t data[], uint8_t page_size);
uint8_t chip_is_busy(void);
 
// Higher level functions that implement the I2C functions above
void display_block(uint8_t memory_block);
void block_erase(uint8_t memory_block);
void check_error_status(uint8_t status);
 
// Functions involving UART communication with terminal (PC) program
void print_byte(uint8_t byte);
void print_byte_setup(uint8_t byte);
 
// Global variable
uint8_t print_byte_array[4] = { 0x30, 0x78, 0x20, 0x20 }; // array for displaying bytes one nibble at a time
 
// Error strings
char error_message1[] = "Error - unsupported block number\n\r";
char error_message2[] = "Error - buffer/array size unacceptable\n\r";
char error_message3[] = "Error - unsupported page number\n\r";
char error_message4[] = "Error - transmitted/received NACK, expected ACK\n\r";
char error_message5[] = "Error - unkown cause of error\n\r";
 
int main() {
  CHIP_Init(); // This function addresses some chip errata and should be called at the start of every EFM32 application (need em_system.c)
   
  char header[] = "\n\rEFM32 Giant Gecko - I2C Example\n\r";
  uint8_t response_byte;
  uint8_t status;
  uint8_t page_data[PAGE_SIZE];
  uint8_t i;
   
  // Clear the page_data array
  for(i=0; i<sizeof(page_data); i++) {
    page_data[i] = 0;
  }
   
  // Setup Clock Tree
  CMU->CTRL |= (1 << 14);                              // Set HF clock divider to /2 to keep core frequency <32MHz
  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. 48MHz XTAL, but we divided the system clock by 2, therefore our HF clock should be 24MHz
  CMU->HFPERCLKEN0 = (1 << 13) | (1 << 12) | (1 << 1); // Enable GPIO, USART1, and I2C1 peripheral clocks
   
  // Configure GPIO
  GPIO->P[I2C_PORT].MODEL = (11 << 20) | (11 << 16) | (4 << 12);                // Configure PC4 and PC5 as open drain output, PC3 as push-pull output
  GPIO->P[I2C_PORT].DOUTSET = (1 << I2C_SCLK) | (1 << I2C_SDA) | (1 << I2C_WP); // Initialize PC3, PC4, and PC5 high
  GPIO->P[UART_PORT].MODEL = (1 << 4) | (4 << 0);                               // Configure PD0 as push-pull output, PD1 as input
  GPIO->P[UART_PORT].DOUTSET = (1 << UART_TX);                                  // Initialize PD0 high since UART TX idles high (otherwise glitches can occur)
  GPIO->P[LED_PORT].MODEH = (4 << 16) | (4 << 12);                              // Configure PB11 and PB12 as push-pull outputs
   
  // Configure UART
  // Setup UART Port for asynch mode, frame format 8-none-1-none
  USART1->CLKDIV = (152 << 6);                               // 152 will give 38400 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 = 0x103;                                     // Enable TX and RX pins, use location #1 (UART TX and RX located at PD0 and PD1, see EFM32GG990 datasheet for details)
   
  // Configure I2C
  // Standard speed, Master mode
  I2C1->CTRL = (1 << 2) | (1 << 0);           // auto acknowledge, enable I2C module
  I2C1->CMD = (1 << 7) | (1 << 6) | (1 << 5); // Clear pending commands, clear TX buffer and shift reg, issue abort command to clear bus busy bit
  I2C1->CLKDIV = 29;                          // allows for 100kHz I2C clock
  I2C1->ROUTE = 0x3;                          // use location #0, enable SDA and SCK pins
   
  // Print Startup Header
  for(i=0; i<sizeof(header); i++) {
    print_byte(header[i]);
  }
   
  block_erase(1);                              // Erase entire block of memory
  display_block(1);                            // Dump contents of block into terminal window
   
  status = write_byte(1, 0x02, 0x23);          // Write a data byte to a specific address in memory
  check_error_status(status);                  // Check for errors
   
  status = read_byte(1, 0x02, &response_byte); // Read a specific address location, store in 'response_byte'
  print_byte_setup(response_byte);             // Print the response in the terminal window
  for(i=0; i<4; i++) {
    print_byte(print_byte_array[i]);
  }
   
  status = write_page(1, 0x1, page_data, sizeof(page_data)); // Write an entire page of memory
  check_error_status(status);                                // Check for errors
   
  display_block(1);                            // Dump contents of block into terminal window
   
  while(1);
}
 
// This function writes a single byte of 'data' to the specified 'address' of the
// specified memory block. Returns 0 if successful, 1 if unsupported memory block.
uint8_t write_byte(uint8_t memory_block, uint8_t address, uint8_t data) {
  // Initial parameter check
  if(memory_block > 3) { return 1; } // unsupported block number
   
  // Control variable
  uint8_t block_mask = 0x03; // 24AA08 only has 4 block to choose from
  // Build control byte (see 24AA08 datasheet for details)
  uint8_t control_byte = (0xA0 | ((memory_block & block_mask) << 1));
   
  // Drive WP pin low to allow memory write
  GPIO->P[I2C_PORT].DOUTCLR = (1 << I2C_WP);
   
  while(chip_is_busy());               // wait until EEPROM is ready to communicate
  while(I2C1->STATE & 0x1);            // wait for bus to become idle
  I2C1->CMD |= 0x1;                    // send START command
  while(!(I2C1->STATUS & (1 << 7)));   // wait for TX buffer to empty
  I2C1->TXDATA = control_byte;         // write control byte to TX buffer
  while(!(I2C1->STATUS & (1 << 7)));   // wait for TX buffer to empty
  while(!((I2C1->STATE >> 4) == 0x9)); // wait for address ACK/NACK and bushold
  if(I2C1->STATE & 0x8) { return 4; }  // NACK: lost communication
  I2C1->TXDATA = address;              // write address to TX buffer
  while(!(I2C1->STATUS & (1 << 7)));   // wait for TX buffer to empty
  while(!((I2C1->STATE >> 4) == 0xD)); // wait for data ACK/NACK and bushold
  if(I2C1->STATE & 0x8) { return 4; }  // NACK: lost communication
  I2C1->TXDATA = data;                 // write data to TX buffer
  while(!(I2C1->STATUS & (1 << 7)));   // wait for TX buffer to empty
  while(!((I2C1->STATE >> 4) == 0xD)); // wait for data ACK/NACK and bushold
  if(I2C1->STATE & 0x8) { return 4; }  // NACK: lost communication
  I2C1->CMD |= 0x2;                    // send STOP command
   
  while(chip_is_busy());               // wait until EEPROM has completed the memory write
   
  GPIO->P[I2C_PORT].DOUTSET = (1 << I2C_WP); // drive WP pin high to protect memory
   
  return 0;                            // successful transfer
}
 
// This function writes up to 16 bytes of memory with the data contained in the 'data[]'
// array. Returns 0 if successful, 1 if unsupported memory block, 2 if 'data[]' array is
// too large, 3 if the number of specified pages exceeds the number available in the block.
uint8_t write_page(uint8_t memory_block, uint8_t page_num, uint8_t data[], uint8_t page_size) {
  // Initial parameter check
  if(memory_block > 3) { return 1; } // unsupported memory block
  if(page_size > 16) { return 2; }   // data[] array is too large
  if(page_num > 0x0F) { return 3; }  // only 16 pages available per block
   
  // Control variables
  uint8_t block_mask = 0x03;
  uint8_t i;
   
  // Build control byte
  uint8_t control_byte = (0xA0 | ((memory_block & block_mask) << 1));
   
  // Locate page offset
  uint8_t address = 16*page_num;
   
  // Drive WP pin low to allow memory write
  GPIO->P[I2C_PORT].DOUTCLR = (1 << I2C_WP);
   
  while(chip_is_busy());               // wait until EEPROM is ready to communicate
  while(I2C1->STATE & 0x1);            // wait for bus to become idle
  I2C1->CMD |= 0x1;                    // send START command
  while(!(I2C1->STATUS & (1 << 7)));   // wait for TX buffer to empty
  I2C1->TXDATA = control_byte;         // write control byte to TX buffer
  while(!(I2C1->STATUS & (1 << 7)));   // wait for TX buffer to empty
  while(!((I2C1->STATE >> 4) == 0x9)); // wait for address ACK/NACK and bushold
  if(I2C1->STATE & 0x8) { return 4; }  // NACK: lost communication
  I2C1->TXDATA = address;              // write address to TX buffer
  while(!(I2C1->STATUS & (1 << 7)));   // wait for TX buffer to empty
  while(!((I2C1->STATE >> 4) == 0xD)); // wait for data ACK/NACK and bushold
  if(I2C1->STATE & 0x8) { return 4; }  // NACK: lost communication
   
  for(i=0; i<page_size; i++) {
    I2C1->TXDATA = data[i];              // write data to TX buffer
    while(!(I2C1->STATUS & (1 << 7)));   // wait for TX buffer to empty
    while(!((I2C1->STATE >> 4) == 0xD)); // wait for data ACK/NACK and bushold
    if(I2C1->STATE & 0x8) { return 4; }  // NACK: lost communication
  }
  I2C1->CMD |= 0x2;                      // send STOP command
   
  while(chip_is_busy()); // wait until EEPROM has completed the memory write
   
  GPIO->P[I2C_PORT].DOUTSET = (1 << I2C_WP); // drive WP pin high to protect memory
   
  return 0;                              // successful transfer
}
 
// This function reads the contents of 'address' within the specified memory block.
// The contents are stored in 'response'. Returns 0 if successful, 1 if unsupported
// memory block.
uint8_t read_byte(uint8_t memory_block, uint8_t address, uint8_t* response) {
  // Initial parameter check
  if(memory_block > 3) { return 1; } // unsupported block number
   
  // Control variable
  uint8_t block_mask = 0x03; // 24AA08 only has 4 block to choose from
     
  // Build control bytes (2 for reading a specific address)
  uint8_t control_byte[2] = { (0xA0 | ((memory_block & block_mask) << 1)),
                              (0xA0 | ((memory_block & block_mask) << 1) | 0x1) };
   
  I2C1->CMD &= ~(0x4);                 // clear TX buffer
  *response = I2C1->RXDATA;            // flush receive buffer
   
  while(chip_is_busy());               // wait until EEPROM is ready to communicate
  while(I2C1->STATE & 0x1);            // wait for bus to become idle
  I2C1->CMD |= 0x1;                    // send START command
  while(!(I2C1->STATUS & (1 << 7)));   // wait for TX buffer to empty
  I2C1->TXDATA = control_byte[0];      // write control byte[0] to TX buffer
  while(!(I2C1->STATUS & (1 << 7)));   // wait for TX buffer to empty
  while(!((I2C1->STATE >> 4) == 0x9)); // wait for address ACK/NACK and bushold
  if(I2C1->STATE & 0x8) { return 4; }  // NACK: lost communication
  I2C1->TXDATA = address;              // write address to TX buffer
  while(!(I2C1->STATUS & (1 << 7)));   // wait for TX buffer to empty
  while(!((I2C1->STATE >> 4) == 0xD)); // wait for data ACK/NACK and bushold
  if(I2C1->STATE & 0x8) { return 4; }  // NACK: lost communication
  I2C1->CMD |= 0x1;                    // send Repeated START command
  I2C1->TXDATA = control_byte[1];      // write control byte[1] to TX buffer
  while(!(I2C1->STATUS & (1 << 7)));   // wait for TX buffer to empty
  while(!((I2C1->STATE >> 4) == 0x8)); // wait for address ACK/NACK (no bushold)
  if(I2C1->STATE & 0x8) { return 4; }  // NACK: lost communication
  while(!(I2C1->STATUS & (1 << 8)));   // wait for data to arrive
  *response = I2C1->RXDATA;            // read rx buffer and store in 'response'
  I2C1->CMD |= 0x8;                    // send NACK
  while(!((I2C1->STATE >> 4) == 0xD)); // wait for data ACK/NACK and bushold
  I2C1->CMD |= 0x2;                    // send STOP command
   
  return 0;                            // successful transfer
}
 
// This function reads the specified memory block and stores the contents in 'buffer[]'.
// Returns 0 if successful, 1 if unsupported memory block, 2 if buffer is not large
// enough to store entire contents of the block.
uint8_t read_block(uint8_t memory_block, uint8_t buffer[], uint16_t buffer_size) {
    // Initial parameter check
  if(memory_block > 3) { return 1; }  // unsupported block number
  if(buffer_size < 256) { return 2; } // buffer is not large enough to store entire contents
   
  // Control variables
  uint8_t block_mask = 0x03; // 24AA08 only has 4 block to choose from
  uint8_t i;
   
  // build control bytes (2 for reading specific address)
  uint8_t control_byte[2] = { (0xA0 | ((memory_block & block_mask) << 1)),
                              (0xA0 | ((memory_block & block_mask) << 1) | 0x1) };
   
  I2C1->CMD &= ~(0x4);                 // clear TX buffer
  buffer[0] = I2C1->RXDATA;            // flush receive buffer
   
  while(chip_is_busy());               // wait until EEPROM is ready to communicate
  while(I2C1->STATE & 0x1);            // wait for bus to become idle
  I2C1->CMD |= 0x1;                    // send START command
  while(!(I2C1->STATUS & (1 << 7)));   // wait for TX buffer to empty
  I2C1->TXDATA = control_byte[0];      // write control byte[0] to TX buffer
  while(!(I2C1->STATUS & (1 << 7)));   // wait for TX buffer to empty
  while(!((I2C1->STATE >> 4) == 0x9)); // wait for address ACK/NACK and bushold
  if(I2C1->STATE & 0x8) { return 4; }  // NACK: lost communication
  I2C1->TXDATA = 0x00;                 // write address to TX buffer
  while(!(I2C1->STATUS & (1 << 7)));   // wait for TX buffer to empty
  while(!((I2C1->STATE >> 4) == 0xD)); // wait for data ACK/NACK and bushold
  if(I2C1->STATE & 0x8) { return 4; }  // NACK: lost communication
  I2C1->CMD |= 0x1;                    // send Repeated START command
  I2C1->TXDATA = control_byte[1];      // write control byte[1] to TX buffer
  while(!(I2C1->STATUS & (1 << 7)));   // wait for TX buffer to empty
  while(!((I2C1->STATE >> 4) == 0x8)); // wait for address ACK/NACK (no bushold)
  for(i=0; i<(buffer_size-1); i++) {
    while(!(I2C1->STATUS & (1 << 8)));   // wait for data to arrive
    buffer[i] = I2C1->RXDATA;            // read rx data and store in 'buffer'
    I2C1->CMD |= (1 << 2);               // send ACK
    while(!((I2C1->STATE >> 4) == 0xC)); // wait for data ACK/NACK (no bushold)
  }
   
  while(!(I2C1->STATUS & (1 << 8)));    // wait for final byte to arrive
  buffer[buffer_size-1] = I2C1->RXDATA; // read rx data and store in 'buffer'
  I2C1->CMD |= 0x8;                     // send NACK
  while(!((I2C1->STATE >> 4) == 0xD));  // wait for data ACK/NACK and bushold
  I2C1->CMD |= 0x2;                     // send STOP command
  return 0;                             // successful transfer
}
 
// This function is used to check if the EEPROM is currently performing an
// internal write process. Returns 1 for busy, 0 for ready
uint8_t chip_is_busy(void) {
   
  while(I2C1->STATE & 0x1);            // wait for bus to become idle
  I2C1->CMD |= 0x1;                    // send START command
  while(!(I2C1->STATUS & (1 << 7)));   // wait for TX buffer to empty
  I2C1->TXDATA = 0xA0;                 // write control byte to TX buffer
  while(!(I2C1->STATUS & (1 << 7)));   // wait for TX buffer to empty
  while(!((I2C1->STATE >> 4) == 0x9)); // wait for address ACK/NACK and bushold
  if(I2C1->STATE & 0x8){ // check for ACK/NACK (only valid if BUSHOLD is set)
    I2C1->CMD |= 0x2;    // send STOP command
    return 1;            // NACK received
  }else{
    I2C1->CMD |= 0x2;    // send STOP command
    return 0;            // ACK received
  }
}
 
// This function erases a page (16 bytes) of memory at a time by writing each byte '0xFF'.
void block_erase(uint8_t memory_block) {
  uint8_t j;
  uint8_t page_data[16];
   
  // load array with 0xFF
  for(j=0; j<sizeof(page_data); j++) {
    page_data[j] = 0xFF;
  }
   
  // write the array to all 16 pages of the block
  for(j=0; j<16; j++) {
      write_page(memory_block, j, page_data, sizeof(page_data));
    }
}
 
// This function reads the specified memory block and displays the contents in the terminal window
void display_block(uint8_t memory_block) {
   
  // Header Strings
  char block_header[]   = "\n\rBlock ";
  char mem_data_header[] = "| Page | Address | Data |\n\r";
  char dash_line[]        = " ------ --------- ------\n\r";
   
  // Control variables
  uint8_t rx_buffer[BLOCK_SIZE];
  uint16_t i;
  uint8_t j;
  uint8_t k=16;
   
  uint8_t status = read_block(memory_block, rx_buffer, sizeof(rx_buffer));
  check_error_status(status);
   
  // Display contents of Block
  // Print Headers ///////////////////////////////////////////
  for(i=0; i<sizeof(block_header); i++) {
      print_byte(block_header[i]);
  }
  print_byte(memory_block + 0x30); // specify which block is being dumped
  print_byte('\n');
  print_byte('\r');
  for(i=0; i<sizeof(dash_line); i++) {
      print_byte(dash_line[i]);
  }
  for(i=0; i<sizeof(mem_data_header); i++) {
      print_byte(mem_data_header[i]);
  }
  for(i=0; i<sizeof(dash_line); i++) {
      print_byte(dash_line[i]);
  }
  ////////////////////////////////////////////////////////////
   
  for(i=0; i<(sizeof(rx_buffer)); i++) { // display block data
     
    print_byte('|');
    if(k == 16) {
      for(j=0; j<3; j++) {   // print 3 whitespaces
        print_byte(' ');
      }
      print_byte_setup(i/16); // Print page number
      for(j=2; j<4; j++) {
        print_byte(print_byte_array[j]);
      }
      print_byte(' ');
      k = 0; // reset k
    }else{
      for(j=0; j<6; j++) {   // print 6 whitespaces
        print_byte(' ');
      }
    }
     
    print_byte('|');
    for(j=0; j<6; j++) {   // print 6 whitespaces
      print_byte(' ');
    }
    print_byte_setup(i); // print address of memory
    for(j=2; j<4; j++) {
      print_byte(print_byte_array[j]);
    }
    print_byte(' ');
    print_byte('|');       // separate address and data
    for(j=0; j<3; j++) {   // print 3 whitespaces
      print_byte(' ');
    }
    print_byte_setup(rx_buffer[i]); // print data located at given address
    for(j=2; j<4; j++) {
      print_byte(print_byte_array[j]);
    }
    print_byte(' ');
    print_byte('|');
    print_byte('\n'); // print newline
    print_byte('\r');
    
    k++;
  }
  // close the table
  for(i=0; i<sizeof(dash_line); i++) {
      print_byte(dash_line[i]);
  }
}
 
// This function is used to display bytes over UART
void print_byte(uint8_t byte) {
  while( !(USART1->STATUS & (1 << 6)) ); // wait for TX buffer to empty
  USART1->TXDATA = byte;                 // send byte over UART
}
 
// 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
  }
}
 
void check_error_status(uint8_t status) {
  uint8_t i;
   
  switch(status) {
    case 0:
      // successful operation
      break;
    case 1:
      // unsupported block number
      for(i=0; i<sizeof(error_message1); i++) {
        print_byte(error_message1[i]);
      }
      break;
    case 2:
      // array size error
      for(i=0; i<sizeof(error_message2); i++) {
        print_byte(error_message2[i]);
      }
      break;
    case 3:
      // unsupported page number
      for(i=0; i<sizeof(error_message3); i++) {
        print_byte(error_message3[i]);
      }
      break;
    case 4:
      // NACK transmitted/received when expected ACK
      for(i=0; i<sizeof(error_message4); i++) {
        print_byte(error_message4[i]);
      }
      break;
    default:
      // unknown error
      for(i=0; i<sizeof(error_message5); i++) {
        print_byte(error_message5[i]);
      }
      break;
  }
}

Comments from the Author

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. Take the Timer example code and try cascading multiple timers together. See if you can combine the GPIO example and the Timer example to create a debounce function for the user buttons. Try configuring different baud rates or setting up a software buffer for the UART example. The goal of this page is to familiarize you with basic functions of EFM32 Giant 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 Giant Gecko microcontrollers.

  • Scott

Questions/Comments

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