Weird behaviour in record mapping

I am trying to map a register (SYSCFG_EXTICR1) on a STM32F42xxx board and have come across some odd behaviour with the actual problem being that the desired pin isn’t mapped to external board interrupt.

Consider the following mapping:

with Common_Types;
with System;

package SYSCFG is
   
   Base_Address : constant := 16#40013800#;
   EXTICR_Offset : constant := 16#08#;
   
   type EXTI_Port is (PA,
                      PB,
                      PC,
                      PD,
                      PE,
                      PF,
                      PG,
                      PH,
                      PI) with size => 4;
   for EXTI_Port use (
                      PA => 2#0000#,
                      PB => 2#0001#,
                      PC => 2#0010#,
                      PD => 2#0011#,
                      PE => 2#0100#,
                      PF => 2#0101#,
                      PG => 2#0110#,
                      PH => 2#0111#,
                      PI => 2#1000#);
   
   type SYSCFG_EXTICR_Register is
      record
         EXTIO : EXTI_Port;
         EXTI1 : EXTI_Port;
         EXTI2 : EXTI_Port;
         EXTI3 : EXTI_Port;
         Unmapped1 : Common_Types.Arbitrary_Unmapped_Space (1 .. 15);
         EXTI4 : EXTI_Port;
         EXTI5 : EXTI_Port;
         EXTI6 : EXTI_Port;
         EXTI7 : EXTI_Port;
         Unmapped2 : Common_Types.Arbitrary_Unmapped_Space (1 .. 15);
         EXTI8 : EXTI_Port;
         EXTI9 : EXTI_Port;
         EXTI10 : EXTI_Port;
         EXTI11 : EXTI_Port;
         Unmapped3 : Common_Types.Arbitrary_Unmapped_Space (1 .. 15);
         EXTI12 : EXTI_Port;
         EXTI13 : EXTI_Port;
         EXTI14 : EXTI_Port;
         EXTI15 : EXTI_Port;
         Unmapped4 : Common_Types.Arbitrary_Unmapped_Space (1 .. 15);
      end record with Volatile, Object_Size => 128;
   for SYSCFG_EXTICR_Register use
      record
         EXTIO at 0 range 0 .. 3;
         EXTI1 at 0 range 4 .. 7;
         EXTI2 at 0 range 8 .. 11;
         EXTI3 at 0 range 12 .. 15;
         Unmapped1 at 0 range 16 .. 31;
         EXTI4 at 4 range 0 .. 3;
         EXTI5 at 4 range 4 .. 7;
         EXTI6 at 4 range 8 .. 11;
         EXTI7 at 4 range 12 .. 15;
         Unmapped2 at 4 range 16 .. 31;
         EXTI8 at 8 range 0 .. 3;
         EXTI9 at 8 range 4 .. 7;
         EXTI10 at 8 range 8 .. 11;
         EXTI11 at 8 range 12 .. 15;
         Unmapped3 at 8 range 16 .. 31;
         EXTI12 at 12 range 0 .. 3;
         EXTI13 at 12 range 4 .. 7;
         EXTI14 at 12 range 8 .. 11;
         EXTI15 at 12 range 12 .. 15;
         Unmapped4 at 12 range 16 .. 31;
      end record;
         
   --   Access points
   SYSCFG_EXTICR_Reg : SYSCFG_EXTICR_Register with Address => System'To_Address (Base_Address + EXTICR_Offset);
   SYSCFG_EXTICR_Reg_Unmapped : SYSCFG_EXTICR_Register;
   
end SYSCFG;

where Common_Types is just some package with well, common types so Common_Types.Arbitrary_Unmapped_Space is:

type Arbitrary_Unmapped_Space is array (Positive range <>) of Boolean with Component_Size => 1, Unreferenced;

Based on the Reference Manual the mapping must be something like:

and the base address:

(This configuration is to do with mapping external pins to board interrupts and there are four registers available which all only use the lower 16 bits to configure a specific pin against an external interrupt. I’ve mapped all of these registers in one go using a single record)

Now consider the following code:

1. SYSCFG.SYSCFG_EXTICR_Reg.EXTI4 := SYSCFG.PB; --   After running this EXTI4 should be set to PB! (i.e. the pin we're trying to map is PB4)
   
2. SYSCFG.SYSCFG_EXTICR_Reg_Unmapped.EXTI4 := SYSCFG.PB;
   
3. SYSCFG.SYSCFG_EXTICR_Reg.Unmapped4 (1) := True;
   
4. SYSCFG.SYSCFG_EXTICR_Reg_Unmapped.EXTI4 := SYSCFG.PB; --   Not relevant just needed a hook to put a breakpoint to.

Putting a breakpoint in line 1 and printing the value of EXTI4 and dumping the raw memory in gdb, I’m getting:

(gdb) set lang ada
(gdb) p SYSCFG.SYSCFG_EXTICR_Reg.EXTI4
$1 = pa

which is correct as the line hasn’t been executed yet.
Now, executing line 1 and priting the value still gives PA! :

(gdb) n
(gdb) p SYSCFG.SYSCFG_EXTICR_Reg.EXTI4
$2 = pa

Dumping the raw bytes also shows the same (using byte-size chunks as I think gdb will print scalars with a size greater than a byte in big-endian, but might be wrong):

(gdb) x/16tb 0x40013808
0x40013808:     00000000        00000000        00000000        00000000        00000000        00000000        00000000        00000000
0x40013810:     00000000        00000000        00000000        00000000        00000000        00000000        00000000        00000000

My initial thought was that there’s something up with the way the compiler has laid out the record (i.e. maybe in big-endian rather than little-endian and setting the variable hits the register area which should be left untouched) so I set out to prove this theory but initially I wanted to prove that an unmapped record behaves as expected hence the SYSCFG_EXTICR_Reg_Unmapped variable in the SYSCFG package above. So, prior to executing line 2 above I printed the EXTI4 value:

(gdb) p SYSCFG.SYSCFG_EXTICR_Reg_Unmapped.EXTI4
$3 = pa

and then executing line 2 and printing the value and dumping the raw values yields the correct result:

(gdb) n
(gdb) p SYSCFG.SYSCFG_EXTICR_Reg_Unmapped.EXTI4
$4 = pb
(gdb)  x/16tb SYSCFG.SYSCFG_EXTICR_Reg_Unmapped'Address
0x20001548 <syscfg__syscfg_exticr_reg_unmapped>:        00000000        00000000        00000000        00000000        00000001        00000000        00000000        00000000
0x20001550 <syscfg__syscfg_exticr_reg_unmapped+8>:      00000000        00000000        00000000        00000000        00000000        00000000        00000000        00000000

where the first bit of the second word has been set correctly (this is little-endian, mind you).

So, clearly, something is wrong with the memory-mapping…As I said, I tried proving that by attempting to set a bit in the unmapped area shown above and executed line 3 but still got the same result (i.e. all bits zero) though come to think of it, if the record had been laid out in big-endian then setting any bit in the unmapped area should have no effect at all.

I am building this in Alire and some bits of the configuration are
(aaa.gpr):

for Target use "arm-eabi";
for Runtime ("ada") use "ravenscar-full-stm32f429disco";

package Compiler is
   for Default_Switches ("ada") use Microcontrollers_Config.Ada_Compiler_Switches;
end Compiler;
............

(aaa_config.gpr):

Ada_Compiler_Switches := External_As_List ("ADAFLAGS", " ");
   Ada_Compiler_Switches := Ada_Compiler_Switches &
          (
            "-Og" -- Optimize for debug
           ,"-ffunction-sections" -- Separate ELF section for each function
           ,"-fdata-sections" -- Separate ELF section for each variable
           ,"-g" -- Generate debug info
           .... rest of the formatting switches

the configured toolchain is:

gnat_native=14.2.1
gprbuild=22.0.1

Interestingly enough, the exact same mapping when passed through an older gprbuild i.e.:

gprbuild --version
GPRBUILD Community 2021 (20210519) (x86_64-pc-linux-gnu)

works fine and the interrupt handler is triggered indeed when the voltage on the pin drops below a certain level using a potentiometer (i.e. falling-edge).

Any ideas what might be going on or any further troubleshooting tips?

Apologies for the long post.

Thanks.

Hi @sidisyom,

Here are my two cents (which may fully help you or may get you closer to a solution).

I would mark the variable SYSCFG_EXTICR_Reg as aliased just in case. Aliased just tells the compiler that the variable needs to live somewhere in memory but not the heap. It may not be needed, but still. Secondly, if you think there is some endianess issues… Tell the compiler to follow the convention you want! See Gem #140: Bridging the Endianness Gap | AdaCore

type Arbitrary_Unmapped_Space is array (Positive range <>) of Boolean with Component_Size => 1, Unreferenced; I would add the Pack attribute or even the Size too, just for good measure.

If you sourced the Community package from 2021, you are not just using an older GPRbuild, but an entirely different toolchain too! I believe Alire already has the newer GPRbuild v24, so you should be able to update to it using the toolchain system of Alire. Additionally, what happens if you build your code in release mode in Alire? What happens if you do an assembly comparison between the different generated binaries from the different toolchains?

Best regards,
Fer

I think this is generating accesses to APB memory that are not 32-bits wide, which is a bug that has bitten me more than once.

When a 16- or an 8-bit access is performed on an APB register, the access is transformed into a 32-bit access: the bridge duplicates the 16- or 8-bit data to feed the 32-bit vector
RM0090 2.11.1 (pg.64)

You need to create 32-bit wide types and apply the Volatile_Full_Access aspect/pragma. This tells the compiler that reads/write must only be 32-bits wide.

type EXTICR_Register is array (0 .. 3) of EXTI_Port
    with Component_Size => 4, Object_Size => 32, Volatile_Full_Access;
type EXTICR_Array is array (1 .. 4) of EXTICR_Register
   with Component_Size => 32, Volatile;

EXTICR : EXTICR_Array
   with Import, Address => System'To_Address (SYSCFG_BASE + 16#08#);

This does make the code harder to read and requires calculating indices in the calling code, but there’s really no other way to tell gcc that this memory region requires word-sized accesses.

Regarding Pack: This is an optimization hint only. It does not guarantee anything about the memory layout of the type. However, Component_Size applied to an array does eliminate padding between elements.

Your code is very naive :slight_smile: The best way to understand what is happens is to debug it at assembler level.

You need to provide much more information to compiler, like to disable some optimizations, use access of appropriate width (byte/half/word).

You can take a look what svd2ada generates, for example for STM32F401

PS. Magic thing is pragma Volatile_Full_Access, it requires to use read-modify-store sequence as 32bit value without any optimizations.

Thanks very much @Irvise for your suggestions!

Interesting about aliased and how it can be used as a hint to the compiler for stack-allocated objects.

Thanks, sounds useful, will go through that. (did try to specify the bit order but that didn’t make any difference)

As @JeremyGrosser mentioned, I was too under the impression that this is just a hint to the compiler i.e. a confirmation that the compiler can lay the record out in at least that many bits (maybe fewer and certainly more) but the deterministic aspects are the *_Size ones.

Hmm, I see…with my current Alire setup, I am using the latest versions of gnat_native and gprbuild that are shown when I run an alr toolchain --select but what about the cross-compiler? Where’s that coming from? :thinking:

For some assembly dumps, please see below in my main answer.

Oh, right, I see! So, if I get this right, with byte accesses say, the APB will effectively load the first byte and if that’s zero will wipe-out the entire word…Interesting, thanks for sharing!

Did give your suggestion a try but the compiler complains with:
atomic access to component of EXTICR_Array cannot be guaranteed. I wonder if that means the compiler is trying to view access to the entire array as atomic? I did try a variation of that (to be posted below) but that didn’t work either.

Out of curiosity @JeremyGrosser, what is the purpose of the Import on the mapped variable? (i.e. I know the semantics of the aspect in general, I’m just wondering what effect that has on the compiler regarding memory accesses. I.e. isn’t Volatile sufficient?)

So, I tried the following changes:

type SYSCFG_EXTICR_Register is
   record
      EXTIO : EXTI_Port;
      EXTI1 : EXTI_Port;
      EXTI2 : EXTI_Port;
      EXTI3 : EXTI_Port;
      Unmapped : Common_Types.Arbitrary_Unmapped_Space (1 .. 15);
   end record with 
     Volatile, 
     Object_Size => 32, 
     Bit_Order => System.Low_Order_First,        
     Volatile_Full_Access;
   
for SYSCFG_EXTICR_Register use
   record
      EXTIO at 0 range 0 .. 3;
      EXTI1 at 0 range 4 .. 7;
      EXTI2 at 0 range 8 .. 11;
      EXTI3 at 0 range 12 .. 15;
      Unmapped at 0 range 16 .. 31;
   end record;
   
type SYSCFG_EXTICR_Bank is array (1 .. 4) of SYSCFG_EXTICR_Register with Volatile, Component_Size => 32;
   
SYSCFG_EXTICR1_Reg : aliased SYSCFG_EXTICR_Register with Address => System'To_Address (Base_Address + EXTICR_Offset);
   
SYSCFG_EXTICR_Regs : aliased SYSCFG_EXTICR_Bank with Address => System'To_Address (Base_Address + EXTICR_Offset);

but that didn’t have any effect i.e. executing this line:
SYSCFG.SYSCFG_EXTICR1_Reg.EXTI1 := SYSCFG.PB;
or this:
SYSCFG.SYSCFG_EXTICR_Regs (2).EXTI1 := SYSCFG.PB;
refuses to change the EXTI1 to PB.

However, dumping some disassembled code confirms the suggestions regarding the size accesses.

When no Volatile_Full_Access is specified then the compiler produces:

(gdb) disas 0x8000026, 0x8000032
Dump of assembler code from 0x8000026 to 0x8000032:
=> 0x08000026 <_ada_main+18>:   ldr     r2, [pc, #40]   ; (0x8000050 <_ada_main+60>)
   0x08000028 <_ada_main+20>:   ldrb    r3, [r2, #8]
   0x0800002a <_ada_main+22>:   movs    r1, #1
   0x0800002c <_ada_main+24>:   bfi     r3, r1, #4, #4
   0x08000030 <_ada_main+28>:   strb    r3, [r2, #8]
End of assembler dump.

which shows that the loads (0x08000028) and stores (0x08000030) are done in bytes indeed. (not sure what the bfi instruction does).

When Volatile_Full_Access is specified, then accesses are in words:

(gdb) disas 0x8000026, 0x8000036
Dump of assembler code from 0x8000026 to 0x8000036:
   0x08000026 <_ada_main+18>:   ldr     r2, [pc, #44]   ; (0x8000054 <_ada_main+64>)
   0x08000028 <_ada_main+20>:   ldr.w   r3, [r2, #2056] ; 0x808
   0x0800002c <_ada_main+24>:   movs    r1, #1
   0x0800002e <_ada_main+26>:   bfi     r3, r1, #4, #4
   0x08000032 <_ada_main+30>:   str.w   r3, [r2, #2056] ; 0x808
End of assembler dump.

This seems to be the case when all registers are bundled together in one array too (i.e. when using the variable SYSCFG_EXTICR_Regs above)

(gdb) disas 0x8000026, 0x8000032
Dump of assembler code from 0x8000026 to 0x8000032:
   0x08000026 <_ada_main+18>:   ldr     r2, [pc, #52]   ; (0x800005c <_ada_main+72>)
   0x08000028 <_ada_main+20>:   ldr     r3, [r2, #0]
   0x0800002a <_ada_main+22>:   movs    r1, #1
   0x0800002c <_ada_main+24>:   bfi     r3, r1, #4, #4
   0x08000030 <_ada_main+28>:   str     r3, [r2, #0]
End of assembler dump.

(I believe the unqualified ldr translates to word?)

But again, this doesn’t seem to work.

Thanks, will take a look at the svd-generated code. Interesting how a variant record is generated even though As_Array => False is used for all register declarations (didn’t fully go through that, tbh). I’ll try to mimic this approach and see if that makes any difference.

Would you be able to share any specific examples?

Thanks!

The declaration of an imported object shall not include an explicit initialization expression. Default initializations are not performed.
ARM B.1.24

Import ensures that the compiler doesn’t try to zero the memory during elaboration.

1 Like

I suppose you don’t need more examples, you have find why it doesn’t work.

Pragma/aspect Volatile_Full_Access is actually combination of Volatile and Full_Access_Only.

Volatile means that value can be changed at any time, thus disable some optimizations.

Full_Access_Only means that all components of the object should be read/written by single operation, object doesn’t allow byte/half word access.

Array of the elements is a bit orthogonal. You can try to declare register type to be Volatile_Full_Access, or use Volatile_Component. You can find more information in RM

https://docs.adacore.com/live/wave/arm22/html/arm22/arm22-C-6.html

1 Like

Just a note to add some further findings.

I read the behaviour of the APB bus a bit more carefully and I started to think that zeroing-out of all bits shouldn’t actually be happening. If anything, more bits should be set!
Screenshot from 2025-02-19 08-26-15

Indeed, that is what is happening (well, nearly that) when I build the project using plain gprbuild which is using the configured cross-compiler for the given Target.

Given this:

type SYSCFG_EXTICR_Register is
      record
         EXTIO: EXTI;
         EXTI1: EXTI;
         EXTI2: EXTI;
         EXTI3: EXTI;
         Unmapped: Arbitrary_Unmapped_Space(1 .. 15);
      end record with Volatile, Object_Size => 32;

for SYSCFG_EXTICR_Register use
   record
      EXTIO at 0 range 0 .. 3;
      EXTI1 at 0 range 4 .. 7;
      EXTI2 at 0 range 8 .. 11;
      EXTI3 at 0 range 12 .. 15;
      Unmapped at 0 range 16 .. 31;
   end record;

type SYSCFG_EXTICR_Arr is array (1 .. 4) of SYSCFG_EXTICR_Register with Component_Size => 32;

SYSCFG_EXTICR_Reg : aliased SYSCFG_EXTICR_Arr with Import, Address => System'To_Address (Base_Address); --   Base address includes offset here

and executing that:

SYSCFG_EXTICR_Reg(2).EXTIO := PB;

the assembly is:

Dump of assembler code from 0x8000256 to 0x8000262:
=> 0x08000256 <pin_interrupt__setup_interrupts+10>:     ldr     r2, [pc, #56]   ; (0x8000290 <pin_interrupt__setup_interrupts+68>)
   0x08000258 <pin_interrupt__setup_interrupts+12>:     ldrb    r3, [r2, #4]
   0x0800025a <pin_interrupt__setup_interrupts+14>:     movs    r1, #1
   0x0800025c <pin_interrupt__setup_interrupts+16>:     bfi     r3, r1, #0, #4
   0x08000260 <pin_interrupt__setup_interrupts+20>:     strb    r3, [r2, #4]
End of assembler dump.

(so, byte accesses are issued) and dumping the memory gives:

(gdb) x/8tb SYSCFG_EXTICR_Reg'Address
0x40013808:     00000000        00000000        00000000        00000000        00000001        00000001        00000000        00000000

so the “1” from the actual changing byte has been duplicated onto the second byte only, which I think is kinda half-consistent with the RM i.e. I’d expect that duplication to take place across all bytes of the word and result in:
00000001 00000001 00000001 00000001
but perhaps a separate issue.

Now, setting Volatile_Full_Access on the record results in the following assembly:

Dump of assembler code from 0x8000256 to 0x8000268:
   0x08000256 <pin_interrupt__setup_interrupts+10>:     ldr     r3, [pc, #64]   ; (0x8000298 <pin_interrupt__setup_interrupts+76>)
   0x08000258 <pin_interrupt__setup_interrupts+12>:     adds    r3, #4
   0x0800025a <pin_interrupt__setup_interrupts+14>:     ldr     r3, [r3, #0]
   0x0800025c <pin_interrupt__setup_interrupts+16>:     movs    r2, #1
   0x0800025e <pin_interrupt__setup_interrupts+18>:     bfi     r3, r2, #0, #4
   0x08000262 <pin_interrupt__setup_interrupts+22>:     ldr     r2, [pc, #52]   ; (0x8000298 <pin_interrupt__setup_interrupts+76>)
   0x08000264 <pin_interrupt__setup_interrupts+24>:     adds    r2, #4
   0x08000266 <pin_interrupt__setup_interrupts+26>:     str     r3, [r2, #0]

so, word accesses are issued (aka unqualified instructions) and dumping the memory gives:

(gdb) x/8tb SYSCFG_EXTICR_Reg'Address
0x40013808:     00000000        00000000        00000000        00000000        00000001        00000000        00000000        00000000

and all is good here.

None of that is happening in the Alire project. I tried using the exact same compiler flags as the gprbuild project and although the assembly is now identical there’s no change in the behaviour.

What’s more interesting is that the same happens if I use a single scalar for the entire register, i.e. given this:

SYSCFG_EXTICR1_Reg : aliased Interfaces.Unsigned_32 with
     Import,
     Volatile_Full_Access,
     Address => System'To_Address (Base_Address + EXTICR_Offset);

and executing:
SYSCFG.SYSCFG_EXTICR1_Reg := 1;

the assembly is:

(gdb) disas 0x8000022, 0x8000028
Dump of assembler code from 0x8000022 to 0x8000028:
   0x08000022 <_ada_main+14>:   ldr     r3, [pc, #36]   ; (0x8000048 <_ada_main+52>)
   0x08000024 <_ada_main+16>:   str.w   r2, [r3, #2056] ; 0x808
End of assembler dump.

so, just plain word loads and stores (didn’t check but I assume r2 is set to “1” a bit further up) but yet SYSCFG.SYSCFG_EXTICR1_Reg remains stubbornly stuck to “0” as if that instruction were a nop.

So, it looks like something is wrong with my Alire setup.

Perhaps someone with more in-depth knowledge of all the components involved might be able to shed some light. I’m going to revert back to gprbuild for now and continue to investigate.

Thanks everyone for all the helpful comments.

Let me fix that for you.

1 Like

A quick update on this, fwiw.

As it is so often the case, this was down to user error. After banging my self hard in the head with the STM32F42 Reference Manual over the past couple of days, it finally hit me (apologies for the terrible pun).

If the system configuration controller clock isn’t enabled then interacting with any register under that memory range is effectively a nop. The reason it works in my other gprbuild project is naturally because the controller clock is turned-on in some other part of the flow. So, no issues with my Alire setup.

What I’m keeping from this is that Volatile_Full_Access is crucial when interacting with this board as the behaviour will most likely be non-deterministic (i.e. setting random bits may or may not have any program side-effects).

The downside, as previously mentioned, is that it some implementation details from the register mapping, do leak into the client (i.e. offset calculation etc). Maybe, a small mapping function will add an extra layer of encapsulation (albeit the register array still exposes it’s index structure):

subtype Exti_Range is Natural range 0 .. 15;
function Exti_To_Offset (Exti : Exti_Range) return Natural is (Exti / 4);

I’m sure other solutions can be applied (i.e. private types etc) but that might be taking it a bit too far.

Thanks all for looking into it and apologies for the confusion.

2 Likes

Private types are not “too far”.
Private types are perfectly appropriate for many things, but particularly for separating implementation from interface. One feature that, in recent years, has been neglected, is the Separate body.

You can use Separate to take a large subprogram and isolate it, like say the parser for reading a file (esp if you’re doing multi-format, auto-detect reads), but you can also use them for isolating/implementing system dependency. (Now, to be fair, this latter usage is often tied to your build-system and is not exclusively a language issue.)

Package Paths is
  Type Path(<>) is limited private;
   Function To_Path( Object : String ) return Path;
   Function To_String( Object : Path ) return String;
   -- Other functions.
Private
   Package Path_Vector is Ada.Containers.Indefinite_Vector
    (Index => Positive, Element => String, "=" => Ada.Strings.Case_Insensitive_Equals);
   Type Path is new Path_Vector.Vector with null record;

   -- This is used to set the character used for path separation.
   Function Separator return Character is Separate;
End Paths;
Windows Unix
src\win\paths.separator.adb src/unix/paths.separator.adb
Separate(Paths) Function Separator return Character renames '\'; Separate(Paths) Function Separator return Character renames '/';

Thus, whether you include src\win or src/unix in your build process, determines how your to-path parses a given string, while keeping that completely from anyone using the package, as the implementation is completely hidden. (Granted, it’s a trivial and stupid little example, but shows how you can leverage the feature to isolate system/architecture dependencies from leaking through your interface.) – Ada is robust enough that very often it is beneficial to sit down, describe your problem in the type-system, write out how you want the interface to be, and then implement.

Don’t be afraid to completely isolate all implementation details into the BODY or PRIVATE parts. I’ve had times where I’ve completely changed the implementation of a type and only had to recompile that package (and its children).

1 Like