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

Mapping variables to specific addresses

Memory mapped registers can be accessed from C in two ways:

  • Forcing an array or struct variable to a specific address.
  • Using a pointer to an array or struct.

Both previous methods generate efficient code. Choose the method that you prefer.

  1. Forcing an array or struct variable to a specific address.

    The array or struct variable is declared in a file on its own. When it is compiled, the object code for this file only contains data. If using the Arm Compiler, this data can be placed at a specified address using the Arm scatter-loading mechanism. This scatter-loading mechanism is the recommended method for placing all scatter-loading AREAs at required locations in the memory map.

    To test using a struct at a specific address, do the following:

    A. In the example project, open src> iovar.c.

    The iovar.c file contains a declaration of the array or struct variable:

    
        struct{
          volatile unsigned reg1;
          volatile unsigned reg2;
        } mem_mapped_reg;
        

     

    B. Open scatter.txt.

    The scatter.txt file contains the following:

    
        ALL 0x80000000
        {
          ALL 0x80000000
          {
            * (+RO,+RW,+ZI)
          }
        }
        IO 0xC0000000
        {
          IO 0xC0000000
           {
            iovar.o (+ZI)
          }
        }
        

    The scatter-loading description file must be specified at link time to the linker using the --scatter scatter.txt command-line option. This description creates two different load regions in your image: ALL and IO. The zero-initialized area (ZI) from iovar.o, that contains the struct, goes into the I/O area at 0xC0000000. All code (RO) and data areas (RW and ZI) from other object files go into the ALL region which starts at 0x80000000.

    You can add the scatter-loading description easily to an Arm DS project. In the Project Explorer view, right-click on your project folder and click Properties. Then go to C/C++ Build > Settings> Tool Settings. Then click Image Layout under Arm Linker 6. Click Browse to select the scatter file and then click Apply and Close to save the changes:

    Adding a scatter-loading file to a project

    Note: In the example project, the scatter-loading file has already been added to the project.

    If you have more than one set of memory mapped registers, you must define each group of variables as a separate execution region. It is possible that all the memory mapped registers could lie within a single load region. To define each variable group as a separate execution region, each group of variables must be defined in a separate module.

    The benefit of using a scatter-loading description file is that all the (target-specific) absolute addresses chosen for your devices, code, and data are in one file. Because everything is in one file, maintenance is easy. Furthermore, if you decide to change your memory map, for example, if peripherals are moved, you do not need to rebuild your entire project. You just run the link step on the existing object files.

    For more documentation on scatter-loading, check the following links:


    Alternatively, use the #pragma arm section pragma to place the data into a specific section. Then, use scatter-loading to place that data at an explicit location. See Pragmas documentation in Arm Developer.

     

    C. In the example project DocMemMappedPeri.c, uncomment the following code:

    
        extern struct{
            volatile unsigned reg1;
            volatile unsigned reg2;
          } mem_mapped_reg;
         
        int main(void) {
        ...
           mem_mapped_reg.reg1 = (unsigned int) 0xF00FF00F;
           mem_mapped_reg.reg2 = (unsigned int) 0x100CF00F;
        ...
        }
        

    And comment the following line in main():

     //init_regs();

    D. Clean and build the example project.

    E. Launch the Cortex-A53 FVP.

    F. Step through the code.

    The fields reg1 and reg2 in the struct variable mem_mapped_reg are now mapped to the addresses of the peripheral registers. When writing to these variables, you are directly writing to the peripheral registers. Enter the address of the peripheral registers, 0xC0000000, into the Memory view. Observe how the data, 0xF00FF00F and 0x100CF00F, is correctly written by using the memory-mapped variables:

    Memory view after struct write 
  2. Using a pointer to an array or struct.
    
        struct PortRegs {
          unsigned short ctrlreg; /* offset 0 */
          unsigned short dummy1;
          unsigned short datareg; /* offset 4 */
          unsigned short dummy2;
          unsigned int data32reg; /* offset 8 */
        };
        volatile struct PortRegs *iospace = (struct PortRegs *)0xC0000000;
        x = iospace->ctrlreg;
        iospace->ctrlreg = newval;
        

    The pointer can be either local or global. If global, to avoid having the base pointer reloaded after function calls, make iospace a constant pointer to the struct by changing its definition to:

    volatile struct PortRegs * const iospace = (struct PortRegs *)0xC0000000;
Previous Next