6

General-Purpose Peripherals

Modern microcontrollers integrate several features that help in building stable and reliable embedded systems. Once the system is up and running, memory and peripherals can be accessed, and basic functionalities are in place. Only then can all the components of the system be initialized by activating the associated peripherals through the system registers, setting the correct frequencies for the clock lines, and configuring and activating interrupts. In this chapter, we will describe the interface exposed by the microcontroller to access built-in peripherals and some basic system functionalities. We will focus on the following topics:

  • The interrupt controller
  • System time
  • Generic timers
  • General-purpose input/output (GPIO)
  • The watchdog

While these peripherals are often accessible through the hardware-support libraries implemented and distributed by chip manufacturers, our approach here involves fully understanding the hardware components and the meaning of all the registers involved. This will be achieved by configuring and using the functionalities in the microcontroller straight through the interface exported by the hardware logic.

When designing drivers for a specific platform, it is necessary to study the interface provided by the microcontroller to access peripherals and CPU features. In the examples provided, the STM32F4 microcontroller is used as a reference target for implementing platform-specific features. Nevertheless, inspecting a possible implementation on our reference platform allows us to get better insight into how to interact with generic targets exposing similar functionalities using the documentation provided by the silicon manufacturer.

Technical requirements

You can find the code files for this chapter on GitHub at https://github.com/PacktPublishing/Embedded-Systems-Architecture-Second-Edition/tree/main/Chapter6.

Bitwise operations

The examples associated with this chapter make extensive use of bitwise operations for checking, setting, and clearing single bits within larger registers (in most cases, 32-bit long). You should already be familiar with bitwise logic operations in C.

The operations commonly used in the examples are the following:

  • Setting the Nth bit in the register R via the assignment R |= (1 << N): The new value of the register R will contain the result of the bitwise OR operation between its original value and a bitmask containing all zeros, except the bit corresponding to the value we want to set, which is set to the value one
  • Clearing (resetting) the Nth bit in the register R via the assignment R &= ~(1 << N): The new value of the register is the result of a bitwise AND operation between its original value and a bitmask containing all ones, except the bit in the position we want to clear, which is set to the value zero
  • Checking whether the Nth bit of the register R is set or cleared, via the condition (R & (1 << N) == (1 << N)): Returns true only if the Nth bit of the register is set

Let us quickly jump into the first topic.

The interrupt controller

Real-time systems have improved their accuracy thanks to the rapid evolution of modern embedded systems, in particular from the research on interrupt controllers. Assigning different priorities to interrupt lines guarantees a lower interrupt latency for higher-priority interrupt sources and makes the system react faster to prioritized events. Interrupts may, however, occur at any time while the system is running, including during the execution of another interrupt service routine. In this case, the interrupt controller provides a way to chain the interrupt handlers, and the order of execution depends on the priority levels assigned to the interrupt source.

One of the reasons for the popularity of the Cortex-M family of microprocessors among real-time and low-power embedded applications is perhaps the design of its programmable real-time controller—namely, the Nested Vector Interrupt Controller, or NVIC for short. The NVIC supports up to 240 interrupt sources, which can be grouped into up to 256 priority levels, depending on the bits reserved to store the priority in the microprocessor logic. These characteristics make it very flexible, as the priorities can also be changed while the system is running, maximizing the freedom of choice for the programmer. As we already know, the NVIC is connected to the vector table located at the beginning of the code region. Whenever an interrupt occurs, the current state of the executing application is pushed into the stack automatically by the processor, and the service routine associated with the interrupt line is executed.

Systems that do not have an interrupt-priority mechanism implement back-to-back interrupt handling. In these cases, chaining interrupts implies that the context is restored at the end of the execution of the first service routine in line, and then saved again while entering the following one. The NVIC implements a tail-chaining mechanism to execute nested interrupts. If one or more interrupts occur while another service routine is executing, the pull operation normally occurring at the end of the interrupt to restore the context from the stack will be canceled, and the controller will instead fetch the location of the second handler in the interrupt vector and ensure it is executed immediately after the first. Because of the increased pace of the stack save and restore operations being implemented in hardware, the interrupt latency is significantly reduced in all those cases where interrupts are chained. Thanks to its implementation, NVIC allows us to change parameters while the system is running, and is able to reshuffle the order of execution of the interrupt service routines associated with the pending signals, according to the priority levels. Moreover, the same interrupt is not allowed to run twice in the same chain of handlers, which may be caused by altering the priorities in the other handlers. This is intrinsically enforced by the NVIC logic, which ensures that no loops are possible in the chain.

Peripherals’ interrupt configuration

Each interrupt line can be enabled and disabled through the NVIC Interrupt Set/Clear Enable registers, NVIC_ISER and NVIC_ICER, located at addresses 0xE000E100 and 0xE000E180, respectively. If the target supports more than 32 external interrupts, arrays of 32-bit registers are mapped at the same locations. Each bit in the registers is used to activate a predefined interrupt line, associated with the bit position in that specific register. For example, on an STM32F4 microcontroller, in order to activate the interrupt line for the Serial Peripheral Interface (SPI) controller SPI1, which is associated with the number 35, the fourth bit should be set on the second register in the NVIC_ISER area.

The generic NVIC function, to enable the interrupt, activates the flag corresponding to the NVIC interrupt number for the source, in the associate NVIC_ISER register:

#define NVIC_ISER_BASE (0xE000E100)
static inline void nvic_irq_enable(uint8_t n)
{
  int i = n / 32;
  volatile uint32_t *nvic_iser =
    ((volatile uint32_t *)(NVIC_ISER_BASE + 4 * i));
  *nvic_iser |= (1 << (n % 32));
}

Similarly, to disable the interrupt, the nvic_irq_disable function activates the corresponding bit in the interrupt clear register:

#define NVIC_ICER_BASE (0xE000E180)
static inline void nvic_irq_disable(uint8_t n)
{
  int i = n / 32;
  volatile uint32_t *nvic_icer =
    ((volatile uint32_t *)(NVIC_ICER_BASE + 4 * i));
  *nvic_icer |= (1 << (n % 32));
}

The interrupt priorities are mapped in an array of 8-bit registers, each containing the priority value for the corresponding interrupt line, starting at address 0xE000E400 so that they can be accessed independently to change the priority at runtime:

#define NVIC_IPRI_BASE (0xE000E400)
static inline void nvic_irq_setprio(uint8_t n,
    uint8_t prio)
{
  volatile uint8_t *nvic_ipri = ((volatile uint8_t *)
   (NVIC_IPRI_BASE + n));
  *nvic_ipri = prio;
}

These functions will come in handy to route and prioritize interrupt lines whenever an interrupt is enabled for a peripheral.

System time

Timekeeping is a basic requirement for almost any embedded system. A microcontroller can be programmed to trigger an interrupt at regular intervals, which is commonly used to increment the monotonic system clock. To do so, a few configuration steps must be performed at startup in order to have a stable tick interrupt. Many processors can run at custom frequencies while using the same oscillator as the source. The input frequency of the oscillator, which can be internal or external to the CPU, is used to derive the processor’s main clock. The configurable logic integrated into the CPU is implemented by a phase-locked loop (PLL) that multiplies the input clock from an external stable source and produces the desired frequencies used by the CPU and integrated peripherals.

Adjusting the flash wait states

If the initialization code is running from flash, it might be necessary to set the wait state for the flash memory before altering the system clocks. If the microprocessor runs at high frequencies, it might require a few wait states in between two consecutive access operations to persistent memory with execute-in-place (XIP) capabilities. Failing to set the correct wait states to match the ratio between the CPU speed and the access time of the flash would most likely result in a hard fault. The configuration registers for the flash memory are located in a platform-specific location within the internal peripheral’s region. On STM32F407, the flash configuration registers are mapped starting at address 0x40023800. The Access Control Register (ACR), which is the one we need to access to set the wait states, is located at the beginning of the area:

#define FLASH_BASE (0x40023C00)
#define FLASH_ACR (*(volatile uint32_t *)(FLASH_BASE +
   0x00))

The lowest three bits in the FLASH_ACR register are used to set the number of wait states. According to the STM32F407 datasheet, the ideal number of wait states to access the flash while the system is running at 168 MHz is 5. At the same time, we can enable the data and instruction cache by activating bits 10 and 9, respectively:

void flash_set_waitstates(void) {
  FLASH_ACR = 5 | (1 << 10) | (1 << 9);
}

After the wait states are set, it is safe to run the code from the flash after setting the CPU frequency at a higher speed, so we can proceed with the actual clock configuration and distribution to the peripherals.

Clock configuration

The configuration of the clocks in Cortex-M microcontrollers happens through the Reset and Clock Control (RCC) registers, located at a specific address within the internal peripheral region. The RCC configuration is vendor-specific, as it depends on the logic of the PLL implemented in the microcontroller. The registers are described in the documentation of the microcontroller, and often, example source code is provided by the chip manufacturer demonstrating how to properly configure the clocks on the microcontroller. On our reference target, STM32F407, assuming that an external 8 MHz oscillator is used as a source, the following procedure configures a 168 MHz system clock and ensures that the clock is also distributed to each peripheral bus. The following code ensures that the PLL is initialized with the required value and that the CPU clock is ticking at the desired frequency. This procedure is common among many STM Cortex-M microcontrollers, and the values for the PLL configurations can be obtained from the chip documentation, or calculated using software tools provided by ST.

The software examples provided after this point will make use of a system-specific module, exporting the functions needed to configure the clock and set the flash memory latency. We now analyze two possible implementations for the PLL configuration, on two different Cortex-M microcontrollers.

To access the configuration of the PLL in the STM32F407-Discovery, first, we define some shortcut macros to the addresses of the registers provided by the RCC:

#define RCC_BASE (0x40023800)
#define RCC_CR (*(volatile uint32_t *)(RCC_BASE + 0x00))
#define RCC_PLLCFGR (*(volatile uint32_t *)(RCC_BASE +
    0x04))
#define RCC_CFGR (*(volatile uint32_t *)(RCC_BASE + 0x08))
#define RCC_CR (*(volatile uint32_t *)(RCC_BASE + 0x00))

For the sake of readability, and to ensure that the code is maintainable in the future, we also define the mnemonics associated with the single-bit values in the corresponding registers:

#define RCC_CR_PLLRDY (1 << 25)
#define RCC_CR_PLLON (1 << 24)
#define RCC_CR_HSERDY (1 << 17)
#define RCC_CR_HSEON (1 << 16)
#define RCC_CR_HSIRDY (1 << 1)
#define RCC_CR_HSION (1 << 0)
#define RCC_CFGR_SW_HSI 0x0
#define RCC_CFGR_SW_HSE 0x1
#define RCC_CFGR_SW_PLL 0x2
#define RCC_PLLCFGR_PLLSRC (1 << 22)
#define RCC_PRESCALER_DIV_NONE 0
#define RCC_PRESCALER_DIV_2 8
#define RCC_PRESCALER_DIV_4 9

Finally, we define the platform-specific constant values used to configure the PLL:

#define CPU_FREQ (168000000)
#define PLL_FULL_MASK (0x7F037FFF)
#define PLLM 8
#define PLLN 336
#define PLLP 2
#define PLLQ 7
#define PLLR 0

One additional macro invoking the DMB assembly instruction is defined, for brevity, as it will be used in the code to ensure that any pending memory transfer toward the configuration registers is completed before the execution of the next statement:

#define DMB() asm volatile ("dmb");

The next function will then ensure that the PLL initialization sequence is performed, in order to set the correct CPU frequency. First, it will enable the internal high-speed oscillator, and will wait until it is ready by polling the CR:

void rcc_config(void)
{
  uint32_t reg32;
  RCC_CR |= RCC_CR_HSION;
  DMB();
  while ((RCC_CR & RCC_CR_HSIRDY) == 0)
    ;

The internal oscillator is then selected as a temporary clock source:

  reg32 = RCC_CFGR;
  reg32 &= ~((1 << 1) | (1 << 0));
  RCC_CFGR = (reg32 | RCC_CFGR_SW_HSI);
  DMB();

The external oscillator is then activated in the same way:

  RCC_CR |= RCC_CR_HSEON;
  DMB();
  while ((RCC_CR & RCC_CR_HSERDY) == 0)
    ;

On this device, the clock can be distributed to all the peripherals through three system buses. Using prescalers, the frequency of each bus can be scaled by a factor of two or four. In this case, we set the clock speed for HPRE, PPRE1, and PPRE2 to be 168, 84, and 46 MHz respectively on this target:

  reg32 = RCC_CFGR;
  reg32 &= ~0xF0;
  RCC_CFGR = (reg32 | (RCC_PRESCALER_DIV_NONE << 4));
  DMB();
  reg32 = RCC_CFGR;
  reg32 &= ~0x1C00;
  RCC_CFGR = (reg32 | (RCC_PRESCALER_DIV_2 << 10));
  DMB();
  reg32 = RCC_CFGR;
  reg32 &= ~0x07 << 13;
  RCC_CFGR = (reg32 | (RCC_PRESCALER_DIV_4 << 13));
  DMB();

The PLL configuration register is set to contain the parameters to correctly scale the external oscillator frequency to the desired value:

  reg32 = RCC_PLLCFGR;
  reg32 &= ~PLL_FULL_MASK;
  RCC_PLLCFGR = reg32 | RCC_PLLCFGR_PLLSRC | PLLM |
    (PLLN << 6) | (((PLLP >> 1) - 1) << 16) |
    (PLLQ << 24);
  DMB();

The PLL is then activated, and the execution is suspended until the output is stable:

  RCC_CR |= RCC_CR_PLLON;
  DMB();
  while ((RCC_CR & RCC_CR_PLLRDY) == 0);

The PLL is selected as the final source for the system clock:

  reg32 = RCC_CFGR;
  reg32 &= ~((1 << 1) | (1 << 0));
  RCC_CFGR = (reg32 | RCC_CFGR_SW_PLL);
  DMB();
  while ((RCC_CFGR & ((1 << 1) | (1 << 0))) !=
    RCC_CFGR_SW_PLL);

The internal oscillator is no longer in use and can be disabled. The control returns to the caller, and all the clocks are successfully set.

As mentioned earlier, the procedure for clock initialization is strictly dependent on the PLL configuration in the microcontroller. To properly initialize the system clocks required for the CPU and the peripherals to operate at the desired frequencies, it is always advised to refer to the datasheet of the microcontroller provided by the silicon manufacturer. As a second example, we can verify how Quick EMUlator (QEMU) is capable of emulating the behavior of the LM3S6965 microcontroller. The emulator provides a virtual clock, which is configurable using the same initialization procedure as described on the manufacturer datasheet. On this platform, two registers are used for clock configuration, referred to as RCC and RCC2:

#define RCC (*(volatile uint32_t*))(0x400FE060)
#define RCC2 (*(volatile uint32_t*))(0x400FE070)

To reset the RCC registers to a known state, the reset value must be written to these registers at boot:

#define RCC_RESET (0x078E3AD1)
#define RCC2_RESET (0x07802810)

This microcontroller uses a raw interrupt to notify that the PLL is locked to the requested frequency. The interrupt status can be checked by reading bit 6 in the Raw Interrupt Status (RIS) register:

#define RIS (*(volatile uint32_t*))(0x400FE050)
#define PLL_LRIS (1 << 6)

The clock configuration routine in this case starts by resetting the RCC registers and setting the appropriate values to configure the PLL. The PLL is configured to generate a 400 MHz clock from an 8 MHz oscillator source:

void rcc_config(void)
{
  RCC = RCC_RESET;
  RCC2 = RCC2_RESET;
  DMB();
  RCC = RCC_SYSDIV_50MHZ | RCC_PWMDIV_64 |
    RCC_XTAL_8MHZ_400MHZ | RCC_USEPWMDIV;

The resultant 50 MHz CPU frequency is derived from this master 400 MHz clock using the system divider. The clock is pre-divided by two, and then a factor of 4 is applied:

  RCC2 = RCC2_SYSDIV2_4;
  DMB();

The external oscillators are powered on:

  RCC &= ~RCC_OFF;
  RCC2 &= ~RCC2_OFF;

And the system clock divider is powered on as well. At the same time, setting the bypass bit ensures that the oscillator is used as a source for the system clock, and the PLL is bypassed:

  RCC |= RCC_BYPASS | RCC_USESYSDIV;
  DMB();

The execution is held until the PLL is stable and has locked on the desired frequency:

 while ((RIS & PLL_LRIS) == 0)   ;

Disabling the bypass bits in the RCC registers at this point is sufficient to connect the PLL output to the system clock:

  RCC &= ~RCC_BYPASS;
  RCC2 &= ~RCC2_BYPASS;
}

Clock distribution

Once the bus clocks are available, the RCC logic can be programmed to distribute the clock to single peripherals. To do so, the RCC exposes bit-mapped peripheral clock source registers. Setting the corresponding bit in one of the registers enables the clock for each mapped peripheral in the microcontroller. Each register can control clock gating for 32 peripherals.

The order of the peripherals, and consequently the corresponding register and bit, is strictly dependent on the specific microcontrollers. The STM32F4 has three registers dedicated to this purpose. For example, to enable the clock source for the internal watchdog, it is sufficient to set the bit number 9 in the clock enable register at address 0x40021001c:

#define APB1_CLOCK_ER (*(uint32_t *)(0x4002001c))
#define WDG_APB1_CLOCK_ER_VAL (1 << 9)
APB1_CLOCK_ER |= WDG_APB1_CLOCK_ER_VAL;

Keeping the clock source off for a peripheral that is not in use saves power; thus, if the target supports clock gating, it can implement optimization and fine-tuning of power consumption by disabling the single peripherals at runtime through their clock gates.

Enabling the SysTick

Once a stable CPU frequency has been set up, we can configure the main timer on the system—the SysTick. Since the implementation of a specific system timer is not mandatory on all Cortex-M, sometimes it is necessary to use an ordinary auxiliary timer to keep track of the system time. In most cases, though, the SysTick interrupt can be enabled by accessing its configuration, which is located in the system control block within the system configuration region. In all Cortex-M microcontrollers that include a system tick, the configuration can be found starting at address 0xE000E010, and exposes four registers:

  • The control/status register (SYSTICK_CSR) at offset 0
  • The reload value register (SYSTICK_RVR) at offset 4
  • The current value register (SYSTICK_CVR) at offset 8
  • The calibration register (SYSTICK_CALIB) at offset 12

The SysTick works as a countdown timer. It holds a 24-bit value, which is decreased at every CPU clock tick. The timer reloads the same value every time it reaches 0 and triggers the SysTick interrupt if it is configured to do so.

As a shortcut to access the SysTick registers, we define their locations:

#define SYSTICK_BASE (0xE000E010)
#define SYSTICK_CSR (*(volatile uint32_t *)(SYSTICK_BASE +
    0x00))
#define SYSTICK_RVR (*(volatile uint32_t *)(SYSTICK_BASE +
    0x04))
#define SYSTICK_CVR (*(volatile uint32_t *)(SYSTICK_BASE +
    0x08))
#define SYSTICK_CALIB (*(volatile uint32_t *)(SYSTICK_BASE
    + 0x0C))

Since we know the frequency of the CPU in Hz, we can define the system tick interval by setting the value in the Reload Value Register (RVR). For a 1 ms interval in between two consecutive ticks, we simply divide the frequency by 1,000, and we subtract 1 to account for the zero-based timer value, ensuring that the next interrupt will take place after the counter has been decreased exactly N times, which corresponds to counting down from N-1 to zero. We can also set the initial value for the timer to 0 so that the first interrupt is immediately triggered after we enable the countdown. The SysTick can finally be enabled by configuring the control/status register. The meaning of the least significant three bits of the CSR is as follows:

  • Bit 0: Enables countdown. After this bit is set, the counter in the SysTick timer is automatically decreased at every CPU clock interval.
  • Bit 1: Enables interrupt. If this bit is set when the counter reaches 0, a SysTick interrupt will be generated.
  • Bit 2: Source clock selection. If this bit is reset, an external reference clock is used as the source. The CPU clock is used as the source when this bit is set.

We are going to define a custom SysTick interrupt handler, so we want to set bit 1 as well. Because we configured the CPU clock correctly, and we are scaling the system tick interval reload value on that, we also want bit 2 to be set. The last line of our systick_enable routine will enable the three bits together in the CSR:

void systick_enable(void) {
  SYSTICK_RVR = ((CPU_FREQ / 1000) - 1);
  SYSTICK_CVR = 0;
  SYSTICK_CSR = (1 << 0) | (1 << 1) | (1 << 2);
}

The system timer that we have configured is the same as that used by real-time operating systems (RTOSs) to initiate process switches. In our case, it might be helpful to keep a monotonic system wall clock, measuring the time elapsed since the clock configuration. A minimalist implementation of the interrupt service routine for the system timer could be as follows:

volatile unsigned int jiffies = 0;
void isr_systick(void)
{
  ++jiffies;
}

This simple function, and the associated global volatile variable associated, are sufficient to keep track of the time transparently while the application is running. In fact, the system tick interrupt happens independently, at regular intervals, when the jiffies variable is incremented in the interrupt handler, without altering the flow of the main application. What actually happens is that every time the system tick counter reaches 0, the execution is suspended, and the interrupt routine quickly executes. When isr_systick returns, the flow of the main application is resumed by restoring exactly the same context of execution stored in memory a moment before the interrupt occurred.

The reason why the system timer variable must be defined and declared everywhere as volatile is that its value is supposed to change while executing the application in a way that is independent of the behavior possibly predicted by the compiler for the local context of execution. The volatile keyword in this case ensures that the compiler is forced to produce code that checks the value of the variable every time it is instantiated, by disallowing the use of optimizations based on the false assumption that the variable is not being modified by the local code.

Here is an example main program that uses the previous functions to boot the system, configure the master clock, and enable the SysTick:

void main(void) {
  flash_set_waitstates();
  clock_config();
  systick_enable();
  while(1) {
    WFI();
  }
}

The shortcut for the WFI assembly instruction (short for wait for interrupt) is defined. It is used in the main application to keep the CPU inactive until the next interrupt occurs:

#define WFI() asm volatile ("wfi")

To verify that the SysTick is actually running, the program can be executed with the debugger attached and stopped after a while. If the system tick has been configured correctly, the jiffies variable should always be displaying the time in milliseconds elapsed since boot.

Generic timers

Providing a SysTick timer is not mandatory for low-end microcontrollers. Some targets may not have a system timer, but all of them expose some kind of interface to program several general-purpose timers for the program to be able to implement time-driven operations. Timers in general are very flexible and easy to configure and are generally capable of triggering interrupts at regular intervals. The STM32F4 provides up to 17 timers, each with different characteristics. Timers are in general independent from each other, as each of them has its own interrupt line and a separate peripheral clock gate. On the STM32F4, for example, these are the steps needed to enable the clock source and the interrupt line for timer 2. The timer interface is based on a counter that is incremented or decremented at every tick. The interface exposed on this platform is very flexible and supports several features, including the selection of a different clock source for input, the possibility to concatenate timers, and even the internals of the timer implementation that can be programmed. It is possible to configure the timer to count up or down, and trigger interrupt events on different values of the internal counter. Timers can be one-shot or continuous.

An abstraction of the timer interface can usually be found in support libraries provided by the silicon vendor, or in other open source libraries. However, in order to understand the interface exposed by the microcontroller, the example provided here is once again directly communicating with the peripherals using the configuration registers.

This example mostly uses the default settings for a general-purpose timer on the STM32F407. By default, the counter is increased at every tick, up to its automatic reload value, and continuously generates interrupt events on overflow. A prescaler value can be set to divide the clock source to increase the range of possible intervals. To generate interrupts spread at a constant given interval, only a few registers need to be accessed:

  • The control registers 1 and 2 (CR1 and CR2)
  • The direct memory access (DMA)/Interrupt enable register (DIER)
  • The status register (SR)
  • The prescaler counter (PSC)
  • The auto-reload register (ARR)

In general, the offsets for these registers are the same for all the timers so that, given the base address, they can be calculated using a macro. In this case, only the register for the timer in use is defined:

#define TIM2_BASE (0x40000000)
#define TIM2_CR1 (*(volatile uint32_t *)(TIM2_BASE + 0x00))
#define TIM2_DIER (*(volatile uint32_t *)(TIM2_BASE +
    0x0c))
#define TIM2_SR (*(volatile uint32_t *)(TIM2_BASE + 0x10))
#define TIM2_PSC (*(volatile uint32_t *)(TIM2_BASE + 0x28))
#define TIM2_ARR (*(volatile uint32_t *)(TIM2_BASE + 0x2c))

Also, for readability, we define some relevant bit positions in the registers that we are going to configure:

#define TIM_DIER_UIE (1 << 0)
#define TIM_SR_UIF (1 << 0)
#define TIM_CR1_CLOCK_ENABLE (1 << 0)
#define TIM_CR1_UPD_RS (1 << 2)

First of all, we are going to define a service routine. The timer interface requires us to clear one flag in the status register, to acknowledge the interrupt. In this simple case, all we do is increment a local variable so that we can verify that the timer is being executed by inspecting it in the debugger. We mark the timer2_ticks variable as volatile so that it does not get optimized out by the compiler, since it is never used in the code:

void isr_tim2(void)
{
  static volatile uint32_t timer2_ticks = 0;
  TIM2_SR &= ~TIM_SR_UIF;
  timer2_ticks++;
}

The service routine must be associated, by including a pointer to the function in the right position within the interrupt vector defined in startup.c:

isr_tim2 , // TIM2_IRQ 28

If the timer is connected to a different branch in the clock tree, as in this case, we need to account for the additional scaling factor between the clock bus that feeds the timer and the actual CPU clock frequency, while calculating the values for the prescaler and the reload threshold. Timer 2 on STM32F407 is connected to the Advanced Peripheral Bus (APB) bus, which runs at half of the CPU frequency.

This initialization is an example of a function that automatically calculates TIM2_PSC and TIM2_ARR values and initializes a timer based on the given interval, expressed in milliseconds. The clock variable must be set to the frequency of the clock source for the timer, which may differ from the CPU frequency.

The following definitions are specific to our platform, mapping the address for the clock gating configuration and the interrupt number of the device we want to use:

#define APB1_CLOCK_ER (*(volatile uint32_t *)(0x40023840))
#define APB1_CLOCK_RST (*(volatile uint32_t *)
     (0x40023820))
#define TIM2_APB1_CLOCK_ER_VAL (1 << 0)
#define NVIC_TIM2_IRQN (28)

And here is the function to invoke from main to enable a continuous timer interrupt at the desired interval:

int timer_init(uint32_t clock, uint32_t interval_ms)
{
  uint32_t val = 0;
  uint32_t psc = 1;
  uint32_t err = 0;
  clock = (clock / 1000) * interval_ms;
  while (psc < 65535) {
    val = clock / psc;
    err = clock % psc;
    if ((val < 65535) && (err == 0)) {
      val--;
      break;
    }
    val = 0;
    psc++;
  }
  if (val == 0)
    return -1;
  nvic_irq_enable(NVIC_TIM2_IRQN);
  nvic_irq_setprio(NVIC_TIM2_IRQN, 0);
  APB1_CLOCK_RST |= TIM2_APB1_CLOCK_ER_VAL;
  DMB();
  TIM2_PSC = psc;
  TIM2_ARR = val;
  TIM2_CR1 |= TIM_CR1_CLOCK_ENABLE;
  TIM2_DIER |= TIM_DIER_UIE;
  DMB();
  return 0;
}

The example presented here is only one of the possible applications of system timers. On the reference platform, timers can be used for different purposes, such as measuring intervals between pulses, synchronizing with each other, or activating signals periodically, given a chosen frequency and duty cycle. This last usage will be explained in the PWM subsection later in this chapter. For all other uses of generic timers on the target, please refer to the reference manual of the microcontroller in use. Now that our system is configured and ready to run, and we have learned how to manage time and generate synchronous events, it is finally time to introduce our first peripherals to start communicating with the outside world. In the next section, we will introduce GPIO lines in their multiple configurations, which allow driving or sensing a voltage on single microcontroller pins.

GPIO

The majority of the pins of a microcontroller chip represent configurable I/O lines. Each pin can be configured to represent a logic level by driving the voltage of the pin as a digital output or to sense the logic state by comparing the voltage as a digital input. Some of the generic pins, though, can be associated with alternate functions, such as analog input, a serial interface, or the output pulse from a timer. Pins may have several possible configurations, but only one is activated at a time. The GPIO controller exposes the configuration of all the pins and manages the association of the pins with the subsystems when alternate functions are in use.

Pin configuration

Depending on the logic of the GPIO controller, the pins can be activated all together, separately, or in groups. In order to implement a driver to set up the pins and use them as needed, it is possible to refer to the datasheet of the microcontroller or any example implementation provided by the silicon vendor.

In the case of the STM32F4, GPIO pins are divided into groups. Each group is connected to a separate clock gate, so, to use the pins associated with a group, the clock gate must be enabled. The following code will distribute the clock source to the GPIO controller for the group D:

#define AHB1_CLOCK_ER (*(volatile uint32_t *)(0x40023840))
#define GPIOD_AHB1_CLOCK_ER (1 << 3)
AHB1_CLOCK_ER |= GPIOD_AHB1_CLOCK_ER;

The configuration registers associated with the GPIO controllers are mapped to a specific area in the peripherals region as well. In the case of the GPIOD controller, the base address is at 0x40020C00. On the STM32F4 microcontrollers, there are 10 different registers for configuring and using each digital I/O group. As groups are composed of at most 16 pins, some registers may use a representation of 2 bits per pin:

  • Mode register (offset 0 in the address space) selects the mode (among digital input, digital output, alternate function, or analog input), using 2 bits per pin
  • Output type register (offset 4) selects the output signal driving logic (push-pull or open-drain)
  • Output speed register (offset 8) selects output drive speed
  • Pull-up register (offset 12) enables or disables the internal pull-up or pull-down resistor
  • Port input data (offset 16) is used to read the state of a digital input pin
  • Port output data (offset 20) containing the current value of the digital output
  • Port bit set/reset (offset 24) used to drive a digital output signal high or low
  • Port configuration lock (offset 28)
  • Alternate function low bit register (offset 32), 4 bits per pin, pins 0-7
  • Alternate function high bit register (offset 36), 4 bits per pin, pins 8-15

The pin must be configured before use, and the clock gating configured to route the source clock to the controller for the group. The configurations available on this GPIO controller can be better explained by looking at specific examples.

Digital output

Enabling a digital output is possible by setting the mode to output in the mode register bits corresponding to the given pin. To be able to control the level of pin D13, which is also connected to an LED on our reference platform, we need to access the following registers:

#define GPIOD_BASE 0x40020c00
#define GPIOD_MODE (*(volatile uint32_t *)(GPIOD_BASE +
    0x00))
#define GPIOD_OTYPE (*(volatile uint32_t *)(GPIOD_BASE +
    0x04))
#define GPIOD_PUPD (*(volatile uint32_t *)(GPIOD_BASE +
    0x0c))
#define GPIOD_ODR (*(volatile uint32_t *)(GPIOD_BASE +
    0x14))
#define GPIOD_BSRR (*(volatile uint32_t *)(GPIOD_BASE +
    0x18))

In later examples, alternate functions are used to change the pin assignment. The two registers containing the alternate function settings are shown here:

#define GPIOD_AFL (*(volatile uint32_t *)(GPIOD_BASE +
    0x20))
#define GPIOD_AFH (*(volatile uint32_t *)(GPIOD_BASE +
    0x24))

The following simple functions are meant to control the output of pin D15 connected to the blue LED on the STM32F4. The main program must call led_setup before any other function call, in order to configure the pin as output and activate the pull-up/pull-down internal resistor:

#define LED_PIN (15)
void led_setup(void)
{
  uint32_t mode_reg;

First, the clock gating is configured to enable the clock source for the GPIOD controller:

  AHB1_CLOCK_ER |= GPIOD_AHB1_CLOCK_ER;

The mode register is altered to set the mode for GPIO D15 to digital output. The operation is done in two steps. Any previous value set in the 2 bits corresponding to the position of the pin mode within the register is erased:

   GPIOD_MODE &= ~ (0x03 << (LED_PIN * 2));

In the same position, the value 1 is set, meaning that the pin is now configured as digital output:

   GPIOD_MODE |= 1 << (LED_PIN * 2);

To enable the pull-up and pull-down internal resistors, we do the same. The value to set in this case is 2, corresponding to the following:

   GPIOD_PUPD &= ~(0x03 << (LED_PIN * 2));
   GPIOD_PUPD |= 0x02 << (LED_PIN * 2);
}

After the setup function is invoked, the application and the interrupt handlers can call the functions exported, to set the value of the pin high or low, by acting on the bit set/reset register:

void led_on(void)
{
  GPIOD_BSRR |= 1 << LED_PIN;
}

The highest half of the BSRR is used to reset the pins. Writing 1 in the reset register bit drives the pin logic level to low:

void led_off(void)
{
  GPIOD_BSRR |= 1 << (LED_PIN + 16);
}

A convenience function is defined, to toggle the LED value from on to off and vice versa:

void led_toggle(void)
{
  if ((GPIOD_ODR & (1 << LED_PIN)) == (1 << LED_PIN))
    led_off();
  else
    led_on();
}

Using the timer configured in the previous section, it is possible to run a small program that blinks the blue LED on the STM32F407-Discovery. The led_toggle function can be called from inside the service routine of the timer implemented in the previous section:

void isr_tim2(void)
{
  TIM2_SR &= ~TIM_SR_UIF;
  led_toggle();
}

In the main program, the LED driver must be initialized before starting the timer:

void main(void) {
  flash_set_waitstates();
  clock_config();
  led_setup();
  timer_init(CPU_FREQ, 1, 1000);
  while(1)
  WFI();
}

The main loop of the program is empty. The led_toggle action is invoked every second to blink the LED.

PWM

Pulse Width Modulation, or PWM for brevity, is a commonly used technique to control different types of actuators, encode messages into signals with different pulse duration, and, in general, generate pulses with fixed frequency and variable duty cycles on digital output lines for different purposes.

The timer interface may allow associating pins to output a PWM signal. On our reference microcontroller, four output compare channels can be associated with general-purpose timers, and the pins connected to the OC channels may be configured to output the encoded output automatically. On the STM32F407-Discovery board, the blue LED pin PD15, used in the previous example to demonstrate digital output functionality, is associated with the OC4 that can be driven by timer 4. According to the chip documentation, selecting the alternate function 2 for the pin directly connects the output pin to OC4.

The following diagram shows the pin configuration to use alternate function 2 to connect it to the output of the timer:

Figure 6.1 – Configuring pin D15 to use alternate function 2 connects it to the output of the timer

Figure 6.1 – Configuring pin D15 to use alternate function 2 connects it to the output of the timer

The pin is initialized, and set to use the alternate configuration instead of the plain digital output, by clearing the MODE register bits and setting the value to 2:

GPIOD_MODE &= ~ (0x03 << (LED_PIN * 2));
GPIOD_MODE |= (2 << (LED_PIN * 2));

Pins from 0 to 7 in this GPIO group use 4 bits each in the AFL register of the GPIOD controller. Higher pins, in the range 8-15, use 4 bits each in the AFH register. Once the alternate mode is selected, the right alternate function number is programmed into the 4 bits associated with pin 15, so we are using the AFH register in this case:

uint32_t value;
if (LED_PIN < 8) {
   value = GPIOD_AFL & (~(0xf << (LED_PIN * 4)));
   GPIOD_AFL = value | (0x2 << (LED_PIN * 4));
} else {
   value = GPIOD_AFH & (~(0xf << ((LED_PIN - 8) * 4)));
   GPIOD_AFH = value |(0x2 << ((LED_PIN - 8) * 4));
}

The pwm_led_init() function, which we can call from the main program to configure the LED pin PD15, will look like this:

void led_pwm_setup(void)
{
  AHB1_CLOCK_ER |= GPIOD_AHB1_CLOCK_ER;
  GPIOD_MODE &= ~ (0x03 << (LED_PIN * 2));
  GPIOD_MODE |= (2 << (LED_PIN * 2));
  GPIOD_OSPD &= ~(0x03 << (LED_PIN * 2));
  GPIOD_OSPD |= (0x03 << (LED_PIN * 2));
  GPIOD_PUPD &= ~(0x03 << (LED_PIN * 2));
  GPIOD_PUPD |= (0x02 << (LED_PIN * 2));
  GPIOD_AFH &= ~(0xf << ((LED_PIN - 8) * 4));
  GPIOD_AFH |= (0x2 << ((LED_PIN - 8) * 4));
}

The function that sets up the timer for PWM generation is similar to the one used in the simple interrupt-generating timer in the digital output example, except that configuring the timer to output a PWM involves modifying the value of four additional registers:

  • The capture/compare enable register (CCER)
  • The capture/compare mode registers 1 and 2 (CCMR1 and CCMR2)
  • The capture channel 4 (CC4) configuration

The signature of the function we will use in the example to configure a PWM with the given duty cycle has the following signature:

int pwm_init(uint32_t clock, uint32_t dutycycle)
{

Enabling the clock gate to turn on timer 4 is still required:

  APB1_CLOCK_RST &= ~TIM4_APB1_CLOCK_ER_VAL;
  APB1_CLOCK_ER |= TIM4_APB1_CLOCK_ER_VAL;

Both the timer and its output compare channels are temporarily disabled to start the configuration from a clean slate:

  TIM4_CCER &= ~TIM_CCER_CC4_ENABLE;
  TIM4_CR1 = 0;
  TIM4_PSC = 0;

For this example, we can use a fixed PWM frequency of 100 kHz, by setting the automatic reload value to 1/100000 of the input clock, and enforcing no use of the prescaler:

  uint32_t val = clock / 100000;

The duty cycle is calculated according to the value that is passed as a second parameter to pwm_init(), expressed as a percentage. To calculate the corresponding threshold level, this simple formula is used so that, for example, a value of 80 means that the PWM will be active for 4/5 of the time. The resultant value is decremented by one, only if not zero to avoid underflow:

  lvl = (val * threshold) / 100;
  if (lvl != 0)
    lvl--;

Comparator value register CCR4, and auto-reload value register ARR, are set accordingly. Also, in this case, the value of ARR is decreased by 1, to account for the zero-based counter:

  TIM4_ARR = val - 1;
  TIM4_CCR4 = lvl;

In order to correctly set up a PWM signal on this platform, we first ensure that the portions of the CCMR1 register we are going to configure are correctly cleared. This includes the capture selection and the mode configuration:

  TIM4_CCMR1 &= ~(0x03 << 0);
  TIM4_CCMR1 &= ~(0x07 << 4);

The PWM1 mode selected is just one of the possible alternate configurations that are based on the capture/compare timer. To enable the mode, we set the PWM1 value in CCMR2, after clearing the relevant bits of the registers:

  TIM4_CCMR1 &= ~(0x03 << 0);
  TIM4_CCMR1 &= ~(0x07 << 4);
  TIM4_CCMR1 |= TIM_CCMR1_OC1M_PWM1;
  TIM4_CCMR2 &= ~(0x03 << 8);
  TIM4_CCMR2 &= ~(0x07 << 12);
  TIM4_CCMR2 |= TIM_CCMR2_OC4M_PWM1;

Finally, we enable the output comparator OC4. The timer is then set up to automatically reload its stored value every time the counter overflows:

  TIM4_CCMR2 |= TIM_CCMR2_OC4M_PWM1;
  TIM4_CCER |= TIM_CCER_CC4_ENABLE;
  TIM4_CR1 |= TIM_CR1_CLOCK_ENABLE | TIM_CR1_ARPE;
}

Using a PWM to drive the voltage applied on the LED modifies its brightness, according to the configured duty cycle. An example program such as the following reduces the brightness of the LED to 50% if compared to that of an LED powered by a constant voltage output, such as the one in the digital output example:

void main(void) {
  flash_set_waitstates();
  clock_config();
  led_pwm_setup();
  pwm_init(CPU_FREQ, 50);
  while(1)
    WFI();
}

The effect of the PWM on the LED brightness can be better visualized by dynamically altering the duty cycle. It is possible, for example, to set up a second timer to generate an interrupt every 50 ms. In the interrupt handler, the duty cycle factor is cycling in the range 0-80% and back, using 16 steps. In the first 8 steps, the duty cycle is increased by 10% at every interrupt, from 0 to 80%, and in the last 8 steps, it is reduced at the same rate, bringing the duty cycle back to 0:

void isr_tim2(void) {
  static uint32_t tim2_ticks = 0;
  TIM2_SR &= ~TIM_SR_UIF;
  if (tim2_ticks > 16)
    tim2_ticks = 0;
  if (tim2_ticks > 8)
    pwm_init(master_clock, 10 * (16 - tim2_ticks));
  else
    pwm_init(master_clock, 10 * tim2_ticks);
  tim2_ticks++;
}

If we initialize timer 2 in the main program to trigger interrupts spread over constant intervals, as in the previous examples, we can see the LED pulsating, rhythmically fading in and out.

In this case, timer 2 is initialized by the main program, and its associated interrupt handler updates the settings for timer 4, 20 times per second:

void main(void) {
  flash_set_waitstates();
  clock_config();
  led_pwm_setup();
  pwm_init(CPU_FREQ, 0);
  timer_init(CPU_FREQ, 1, 50);
  while(1)
    WFI();
}

Digital input

A GPIO pin configured in input mode detects the logic level of the voltage applied to it. The logic value of all the input pins on a GPIO controller can be read from the input data register (IDR). On the reference board, pin A0 is connected to the user button, so the status of the button can be read at any time while the application is running.

The GPIOA controller can be turned on by clock gating:

#define AHB1_CLOCK_ER (*(volatile uint32_t *)(0x40023830))
#define GPIOA_AHB1_CLOCK_ER (1 << 0)

The controller itself is mapped at address 0x40020000:

#define GPIOA_BASE 0x40020000
#define GPIOA_MODE (*(volatile uint32_t *)(GPIOA_BASE +
     0x00))
#define GPIOA_IDR (*(volatile uint32_t *)(GPIOA_BASE +
     0x10))

To set up the pin for input, we only ensure that the mode is set to 0, by clearing the two mode bits relative to pin 0:

#define BUTTON_PIN (0)
void button_setup(void)
{
  AHB1_CLOCK_ER |= GPIOA_AHB1_CLOCK_ER;
  GPIOA_MODE &= ~ (0x03 << (BUTTON_PIN * 2));
}

The application can now check the status of the button at any time by reading the lowest bit of the IDR. When the button is pressed, the reference voltage is connected to the pin, and the value of the bit corresponding to the pin changes from 0 to 1:

int button_is_pressed(void)
{
  return (GPIOA_IDR & (1 << BUTTON_PIN)) >> BUTTON_PIN;
}

Interrupt-based input

Having to proactively read the value of a pin by constantly polling the IDR is not convenient in many cases, where the application is supposed to react to state changes. Microcontrollers usually provide mechanisms to connect digital input pins to interrupt lines so that the application can react in real time to events related to the input because the execution is interrupted to execute the associated service routine.

On the reference microcontroller unit (MCU), pin A0 can be connected to the external interrupt and event controller, also known as EXTI. EXTI offers edge-detection triggers that can be attached to interrupt lines. The number of the pin within the GPIO group determines the number of the EXTI interrupt that is associated with it so that the EXTI 0 interrupt routine may be connected to pin 0 of any GPIO group if needed:

Figure 6.2 – EXTI0 controller associating edge detection triggers to the user button connected to PA0

Figure 6.2 – EXTI0 controller associating edge detection triggers to the user button connected to PA0

To associate PA0 to EXTI 0, the EXTI configuration register must be modified to set the number of the GPIO group in the bits associated with EXTI 0. In the STM32F4, the EXTI configuration registers (EXTI_CR) are located at address 0x40013808. Each register is used to set the interrupt controller associated with an EXTI line. The lowest four bits of the first register are relative to EXTI line 0. The number for the GPIO group A is 0, so we need to ensure that the corresponding bits are cleared in the first EXTI_CR register. The goal of the next example is to demonstrate how to enable the EXTI 0 interrupt and associate it to pin A0, so the following definitions are provided to access the first EXTI_CR register to set the GPIO group A:

#define EXTI_CR_BASE (0x40013808)
#define EXTI_CR0 (*(volatile uint32_t *)(EXTI_CR_BASE +
    0x00))
#define EXTI_CR_EXTI0_MASK (0x0F)

The EXTI0 interrupt is connected to NVIC line number 6, so we add this definition to configure the NVIC:

#define NVIC_EXTI0_IRQN (6)

The EXTI controller in STM32F4 microcontrollers is located at address 0x40013C00, and provides the following registers:

  • Interrupt mask register (IMR) at offset 0. Sets/clears the corresponding bit to enable/disable the interrupt for each of the EXTI lines.
  • Event mask register (EMR) at offset 4. Sets/clears the corresponding bit to enable/disable the event trigger for the corresponding EXTI line.
  • Rising trigger select register (RTSR) at offset 8. Sets the corresponding bit to generate events and interrupts when the associated digital input level switches from 0 to 1.
  • Falling trigger select register (FTSR) at offset 12. Sets the corresponding bit to generate events and interrupts when the associated signal falls from a logic value of 1 back to 0.
  • Software interrupt enable register (SWIER) at offset 16. If a bit is set in this register, the associated interrupt event will be immediately generated, and the service routine executed. This mechanism can be used to implement custom software interrupts.
  • Pending interrupt register (PR) at offset 20. To clear a pending interrupt, the service routine should set the bit corresponding to the EXTI line, or the interrupt will remain pending. A new service routine will be spawned until the PR bit for the EXTI line is cleared.

For convenience, we may define the registers as follows:

#define EXTI_BASE (0x40013C00)
#define EXTI_IMR (*(volatile uint32_t *)(EXTI_BASE + 0x00))
#define EXTI_EMR (*(volatile uint32_t *)(EXTI_BASE + 0x04))
#define EXTI_RTSR (*(volatile uint32_t *)(EXTI_BASE +
    0x08))
#define EXTI_FTSR (*(volatile uint32_t *)(EXTI_BASE +
    0x0c))
#define EXTI_SWIER (*(volatile uint32_t *)(EXTI_BASE +
    0x10))
#define EXTI_PR (*(volatile uint32_t *)(EXTI_BASE + 0x14))

The procedure to enable the interrupt on the rising edge of PA0, associated with the button press, is the following:

void button_setup(void)
{
  AHB1_CLOCK_ER |= GPIOA_AHB1_CLOCK_ER;
  GPIOA_MODE &= ~ (0x03 << (BUTTON_PIN * 2));
  EXTI_CR0 &= ~EXTI_CR_EXTI0_MASK;
  nvic_irq_enable(NVIC_EXTI0_IRQN);
  EXTI_IMR |= 1 << BUTTON_PIN;
  EXTI_EMR |= 1 << BUTTON_PIN;
  EXTI_RTSR |= 1 << BUTTON_PIN;
}

The ISR, IMR, and RTSR corresponding bits have been set, and the interrupt has been enabled in the NVIC. Instead of polling for the value of the digital input to change, we can now define a service routine that will be invoked every time the button is pressed:

volatile uint32_t button_presses = 0;
void isr_exti0(void)
{
  EXTI_PR |= 1 << BUTTON_PIN;
  button_presses++;
}

In this simple example, the button_presses counter is expected to increase by one at every button press event. In a real-life scenario, buttons based on mechanical contact (such as the one on the STM32F407-Discovery) are tricky to control using this mechanism. A single physical button press may in fact trigger the rising front interrupt multiple times during the transitory phase. This phenomenon, known as a button bouncing effect, can be mitigated using specific debounce techniques, which are not discussed here.

Analog input

Some pins have the possibility to measure the applied voltage dynamically and assign a discrete number to the measured value, using an analog-to-digital signal converter, or ADC. This is very useful to acquire data from a wide range of sensors, capable of conveying the information as output voltage or simply using a variable resistor.

The configuration of the ADC subsystem may vary significantly across different platforms. ADCs on modern microcontrollers offer a wide range of configuration options. The reference microcontroller equips 3 separate ADC controllers, sharing 16 input channels, each one with a resolution of 12 bits. Multiple features are available, such as DMA transfer of the acquired data, and monitoring the signals in between two watchdog thresholds.

ADC controllers are generally designed to automatically sample input values multiple times per second and provide stable results that are immediately available. The case we analyze here is simpler and consists of a one-shot read operation for a single conversion.

Associating a specific pin to a controller is possible by checking how channels are mapped on the controllers if the pin supports it and it is connected through a channel to one of the configured as analog input and reading out the value, which results from the conversion of the analog signal. In this example, pin B1 is used as analog input and can be connected to the ADB1 controller through channel 9. The following constants and registers are defined for the configuration of the ADB1 controller:

#define APB2_CLOCK_ER (*(volatile uint32_t *)(0x40023844))
#define ADC1_APB2_CLOCK_ER_VAL (1 << 8)
#define ADC1_BASE (0x40012000)
#define ADC1_SR (*(volatile uint32_t *)(ADC1_BASE + 0x00))
#define ADC1_CR1 (*(volatile uint32_t *)(ADC1_BASE +
     0x04))
#define ADC1_CR2 (*(volatile uint32_t *)(ADC1_BASE +
     0x08))
#define ADC1_SMPR1 (*(volatile uint32_t *)(ADC1_BASE +
     0x0c))
#define ADC1_SMPR2 (*(volatile uint32_t *)(ADC1_BASE +
     0x10))
#define ADC1_SQR3 (*(volatile uint32_t *)(ADC1_BASE +
     0x34))
#define ADC1_DR (*(volatile uint32_t *)(ADC1_BASE + 0x4c))
#define ADC_CR1_SCAN (1 << 8)
#define ADC_CR2_EN (1 << 0)
#define ADC_CR2_CONT (1 << 1)
#define ADC_CR2_SWSTART (1 << 30)
#define ADC_SR_EOC (1 << 1)
#define ADC_SMPR_SMP_480CYC (0x7)

These are the definitions to configure GPIO as usual, this time mapped for GPIOB:

#define AHB1_CLOCK_ER (*(volatile uint32_t *)(0x40023830))
#define GPIOB_AHB1_CLOCK_ER (1 << 1)
#define GPIOB_BASE (0x40020400)
#define GPIOB_MODE (*(volatile uint32_t *)(GPIOB_BASE +
     0x00))
#define ADC_PIN (1)
#define ADC_PIN_CHANNEL (9)

The three ADCs share a few registers for common settings, such as the clock prescale factor, so they will all operate at the same frequency. The prescale factor for the ADC must be set within the working range of the converter recommended by the datasheet—in the target platform, halving the frequency of the APB2 clock through the common prescaler. The common ADC configuration registers start at port 0x40012300:

#define ADC_COM_BASE (0x40012300)
#define ADC_COM_CCR (*(volatile uint32_t *)(ADC_COM_BASE +
    0x04))

Based on these definitions, the initialization function can be written as follows. First, we enable the clock gating for both the ADC controller and the GPIO group:

int adc_init(void)
{
  APB2_CLOCK_ER |= ADC1_APB2_CLOCK_ER_VAL;
  AHB1_CLOCK_ER |= GPIOB_AHB1_CLOCK_ER;

PB1 is set to analog input mode, corresponding to the value 3 in the mode register:

  GPIOB_MODE |= 0x03 << (ADC_PIN * 2);

ADC1 is temporarily switched off to set the desired configuration. The common clock prescaler is set to 0, meaning a divisor of 2 from the input clock. This ensures that the frequency fed to the ADC controller is within its operational range. Scan mode is disabled, and so is continuous mode, as we are not using these features in this example:

  ADC1_CR2 &= ~(ADC_CR2_EN);
  ADC_COM_CCR &= ~(0x03 << 16);
  ADC1_CR1 &= ~(ADC_CR1_SCAN);
  ADC1_CR2 &= ~(ADC_CR2_CONT);

The sampling frequency can be set using the two registers SMPR1 and SMPR2, depending on the channel in use. Each register represents one channel sample rate using 3 bits per register, so the channels 0 to 9 are configurable using SMPR1, and all the others through SMPR2. The channel for PB1 is set to 9, so in this case, the SMPR1 register is used, but to remind about this, the generic mechanism to set the sample rate on any channel is provided:

  if (ADC_PIN_CHANNEL > 9) {
    uint32_t val = ADC1_SMPR2;
    val = ADC_SMPR_SMP_480CYC << ((ADC_PIN_CHANNEL - 10) *
    3);
    ADC1_SMPR2 = val;
  } else {
    uint32_t val = ADC1_SMPR1;
    val = ADC_SMPR_SMP_480CYC << (ADC_PIN_CHANNEL * 3);
    ADC1_SMPR1 = val;
  }

Finally, the channel is enabled in the conversion sequence of the ADC controller using the sequence registers (SQRs). The mechanisms foresee that multiple channels can be added to the same sequence on the controller, by populating the registers in inverse order, from SQR3 to SQR1. Each source channel is represented in five bits, so each register contains up to six sources, except SQR1, which stores five, and reserves the higher bits to indicate the length of the stack stored in the registers, minus one. In our case, there is no need to set the length-minus-one field, as it would be zero for a single source in SQR1:

  ADC1_SQR3 |= (ADC_PIN_CHANNEL);

Finally, the ADC1 analog converter is enabled again by setting the enable bit in the CR2 control register and the initialization function successfully returns:

  ADC1_CR2 |= ADC_CR2_EN;
  return 0;
}

After the ADC has been initialized and configured to convert the analog signal on PB1, the A/D conversion can be started at any time. A simple blocking read function would initiate the conversion, wait for the conversion to be successfully started, then wait until the conversion is completed by looking at the end of conversion (EOC) bit in the status register:

int adc_read(void)
{
  ADC1_CR2 |= ADC_CR2_SWSTART;
  while (ADC1_CR2 & ADC_CR2_SWSTART)
    ;
  while ((ADC1_SR & ADC_SR_EOC) == 0)
    ;

When the conversion is completed, the corresponding discrete value is available on the lowest 12 bits of the data register, and can be returned to the caller:

  return (int)(ADC1_DR);
}

We have learned how to communicate with the outside world using GPIOs. The same GPIO setup and management interface will be useful again in the next chapter to configure more complex, local bus interfaces, using the alternate functions for the associated GPIO lines.

The upcoming section introduces the watchdog, the last of the generic system features analyzed in this chapter. Commonly present in several microcontrollers, it provides a handy emergency recovery procedure whenever, for any reason, the system is frozen and will not resume its normal execution.

The watchdog

A common feature in many microcontrollers is the presence of a watchdog timer. A watchdog ensures that the system is not stuck within an endless loop or any other blocking situation within the code. This is particularly useful in bare-metal applications that rely on an event-driven loop, where calls are required not to block, and to return to the main event loop within the allowed amount of time.

The watchdog must be seen as the very last resort to recover an unresponsive system, by triggering a forced reboot regardless of the current state of execution in the CPU.

The reference platform provides one independent watchdog timer, with a counter similar to those of the generic timers, with a 12-bit granularity and a prescaler factor. The prescaler of the watchdog, however, is expressed in multiples of 2 and has a range between 4 (represented by the value 0) and 256 (value 6).

The clock source is connected to a lower-speed oscillator, through an independent branch of the clock distribution. For this reason, clock gating is not involved in the activation of this peripheral.

The watchdog configuration area is mapped within the peripherals address region, and consists of four registers:

  • The key register (offset 0), used to trigger the three unlock, start, and reset operations by writing predefined values in the lowest 16 bits
  • The prescale register (offset 4), to set the prescale factor of the counter
  • The reload register (offset 8), containing the reload value for the counter
  • The status register (offset 12), providing the status flags to synchronize the setup operations

The registers can be referenced using shortcut macros:

#define IWDG_BASE (0x40003000)
#define IWDG_KR (*(volatile uint32_t *)(IWDG_BASE + 0x00))
#define IWDG_PR (*(volatile uint32_t *)(IWDG_BASE + 0x04))
#define IWDG_RLR (*(volatile uint32_t *)(IWDG_BASE + 0x08))
#define IWDG_SR (*(volatile uint32_t *)(IWDG_BASE + 0x0c))

The three possible operations that can be triggered via the key register are as follows:

#define IWDG_KR_RESET 0x0000AAAA
#define IWDG_KR_UNLOCK 0x00005555
#define IWDG_KR_START 0x0000CCCC

Two meaningful status bits are provided in the status, and they must be checked to ensure that the watchdog is not busy before unlocking and setting the value for prescale and reload:

#define IWDG_SR_RVU (1 << 1)
#define IWDG_SR_PVU (1 << 0)

The initialization function to configure and start the watchdog may look like the following:

int iwdt_init(uint32_t interval_ms)
{
   uint32_t pre = 0;
   uint32_t counter;

In the next line, the input value in milliseconds is scaled to the frequency of the watchdog clock, which is 32 kHz:

   counter = interval_ms << 5;

The minimum prescaler factor is 4, however, so the value should be divided again. We then look for the minimum prescaler value that results in a counter that fits the 12 bits available, by halving the counter value and increasing the prescaler factor until the counter is appropriately scaled:

   counter >>= 2;
   while (counter > 0xFFF) {
     pre++;
     counter >>= 1;
   }

The following checks ensure that the interval provided does not result in a zero counter or a value that is too large for the available scaling factor:

   if (counter == 0)
     counter = 1;
   if (pre > 6)
     return -1;

The actual initialization of the registers is done, but the device requires us to initiate the write with an unlock operation, and only after checking that the registers are available for writing:

   while(IWDG_SR & IWDG_SR_PR_BUSY);
   IWDG_KR = IWDG_KR_UNLOCK;
   IWDG_PR = pre;
   while (IWDG_SR & IWDG_SR_RLR_BUSY);
   IWDG_KR = IWDG_KR_UNLOCK;
   IWDG_RLR = counter;

Starting the watchdog simply consists of setting the START command in the key register to initiate the start operation:

   IWDG_KR = IWDG_KR_START;
   return 0;
}

Once started, the watchdog cannot be stopped and will run forever, decreasing the counter until it reaches zero, and rebooting the system.

The only way to prevent the system from being rebooted is resetting the timer manually, an operation often referred to as kicking the watchdog. A watchdog driver should export a function that allows the application to reset the counter—for example, at the end of each iteration in the main loop. Here is ours:

void iwdt_reset(void)
{
   IWDG_KR = IWDG_KR_RESET;
}

As a simple test for the watchdog driver, a watchdog counter of 2 seconds can be initialized in main():

void main(void) {
  flash_set_waitstates();
  clock_config();
  button_setup();
  iwdt_init(2000);
  while(1)
    WFI();
}

The watchdog is reset upon button press, in the interrupt service routine of the GPIO button:

void isr_exti0(void)
{
  EXTI_PR |= (1 << BUTTON_PIN);
  iwdt_reset();
}

In this test, the system will reboot if the user button is not pressed for 2 seconds in a row, so the only way to keep the system running is by repeatedly pressing the button.

Summary

The clock configuration, timers, and I/O lines are the general-purpose peripherals shown in this chapter, commonly supported by a wide range of microcontrollers. Although implementation details such as register names and placement may differ on other targets, the proposed approach is valid on most embedded platforms, and the general-purpose peripherals are the bricks for building the most basic system functionalities as well as providing a means of interaction with sensors and actuators.

In the next chapter, we will focus on serial communication channels provided by most microprocessors as communication interfaces toward other devices, and peripherals in the proximity of the target system.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset