Accessing memory-mapped peripherals with Arm DS

Tutorial about accessing memory-mapped peripherals with Arm DS


Introduction Arm recommendations Alignment of registers Mapping of variables to specific addresses Code efficiency

Introduction

In this tutorial, learn about mapping a C variable to each register of a memory-mapped peripheral, then using a pointer to that variable to read and write the register.

Accessing memory-mapped peripherals

In most Arm embedded systems, peripherals are at specific addresses in memory. It is often convenient to map a C variable onto each register of a memory-mapped peripheral. Then, use a pointer to that variable to read and write the register. In your code, you must consider not only the size and address of the register, but also its alignment in memory.

This tutorial assumes you have installed and licensed Arm Development Studio (Arm DS). For more information, see the Arm Development Studio documentation. You can test the examples in this tutorial by using the Cortex-A53x1 FVP, Base_A53x1, with Arm Compiler 6. Both Arm Compiler 6 and the Cortex-A53x1 FVP are included with Arm DS.

Download this Arm DS example project that includes the code from this tutorial and a debug launch configuration. Use this project to modify, build, and debug the examples from this tutorial.

It is highly recommended to complete the Hello World Arm DS Tutorial before working with the examples in this tutorial.

Note: In this tutorial, the examples use a little-endian memory system.

Basic concepts

  • For 32-bit registers, unsigned int.
  • For 16-bit registers, unsigned short.
  • For 8-bit registers, unsigned char.
 

The compiler generates the appropriate single load and store instructions, that is LDR and STR for 32-bit registers, LDRH and STRH for 16-bit registers, and LDRB and STRB for 8-bit registers.

You must also ensure that the memory-mapped registers lie on appropriate address boundaries, that is either all word-aligned, or aligned on their natural size boundaries. For example, 16-bit registers must be aligned on halfword addresses.

Note: Arm recommends that all registers, whatever their size, be aligned on word boundaries.

You can also use #define to simplify your code:

 

#define PORTBASE 0xC0000000 /* Counter/Timer Base */
#define PortLoad ((volatile unsigned int *) PORTBASE) /* 32 bits */
#define PortValue ((volatile unsigned short *)(PORTBASE + 0x04)) /* 16 bits */
#define PortClear ((volatile unsigned char *)(PORTBASE + 0x08)) /* 8 bits */
 
void init_regs(void)
{
  unsigned int int_val;
  unsigned short short_val;
  unsigned char char_val;
 
  *PortLoad = (unsigned int) 0xF00FF00F;
  int_val = *PortLoad;
 
  *PortValue = (unsigned short) 0x0000;
  short_val = *PortValue;
 
  *PortClear = (unsigned char) 0x1F;
  char_val = *PortClear;
}

This code results in the following (interleaved) code:


;;;5      void init_regs(void)
000000  e59f1024 LDR r1,|L1.44|
;;;6      {
;;;7        unsigned int int_val;
;;;8        unsigned short short_val;
;;;9        unsigned char char_val;
;;;10       *PortLoad = (unsigned int) 0xF00FF00F;
000004  e3a00101 MOV r0,#0xC0000000
000008  e5801000 STR r1,[r0,#0]
;;;11       int_val = *PortLoad;
00000c  e5901000 LDR r1,[r0,#0]
;;;12       *PortValue = (unsigned short) 0x0000;
000010  e3a01000 MOV r1,#0
000014  e1c010b4 STRH r1,[r0,#4]
;;;13       short_val = *PortValue;
000018  e1d010b4 LDRH r1,[r0,#4]
;;;14       *PortClear = (unsigned char) 0x1F;
00001c  e3a0101f MOV r1,#0x1f
000020  e5c01008 STRB r1,[r0,#8]
;;;15       char_val = *PortClear;
000024  e5d00008 LDRB r0,[r0,#8]
;;;16     }
000028  e12fff1e BX lr

 

If you debug the previous code with the Cortex-A53 FVP in Arm DS, you can see the generated disassembly code in the Disassembly view.

Note: The instructions used might slightly differ from the previous interleaved code.

Code in Disassembly view 

Notice that for the instructions LDR, STR, LDRH, STRH, LDRB, and STRB, the compiler generates as type casting (unsigned int, unsigned short, and unsigned char) which is used to store the wanted values in the peripheral port registers.

It is now possible to debug the code to check the wanted values are correctly written to the wanted memory addresses that correspondent to the peripheral registers. Before executing init_regs(), open the Memory view in Arm DS and then search for the port base address of your peripheral, in this case 0xC0000000. It is observed that the relevant memory addresses only contain uninitialized values:

Uninitialized memory in Memory view

 

Stepping through the code, when *PortLoad = (unsigned int) 0xF00FF00F; is executed, the instruction STR w9,[x8,#0] is executed to store the wanted value, 0xF00FF00F, into the peripheral register PortLoad, at the address 0xC0000000:

Example project code  

STR instruction in Disassembly view

Opening the Memory view, see that the wanted value is set to the peripheral register:

Memory view after 32-bit write

 

Continue stepping until the instruction STRH wzr,[x8,#0] is reached. This instruction is executed to store the value 0x0000 in the PortValue register, mapped to the address 0xC0000004. In this case, the value set on the port register is half a word width (16-bits). When viewing memory at a word width, after the halfword write, the bottom 16-bits change and the top 16-bits remain the same. In this case, the bottom 16-bits change to 0x0000 and the top 16-bits remain 0xCFDF like the following:

Memory view after 16-bit write

 

Continue stepping to the next relevant instruction of STRB w9,[x8,#0]. This instruction is executed to store the value 0x1F in the PortClear register, mapped to the address 0xC0000008. In this case, the value set on the port register is a byte wide (8-bits). When viewing memory at a word width, after the byte write, the bottom 8-bits change and the top 24 bits (3 bytes) remain the same. In this case, the bottom 8-bits change to 0x1F and the top 3 bytes remain 0xDFDFDF like the following:

Memory view after 8-bit write

 

To check the variables are stored to the correct peripheral addresses, in the Variables view, look at the local variables int_val, short_val, and char_val. Look at the values, types, and sizes of the three variables. Remember that, even if the sizes are different, word-alignment (32-bits) is respected when storing the values in the peripheral registers. Notice that the locations of these variables are not the locations of the peripheral registers. These variables are only local variables to check that the contents of the peripheral registers are the expected values.

Variables in Variables view
Next