4

We are developing for the STM32F103 MCU. We use bare-metal C++ code with the ARM GCC toolchain. After some hours of struggling with a suspicious expression, we found that the constant keyword triggers different results of that expression. When testing the same piece of code with the x86 GCC toolchain, the problem is nonexistent.
We are using the STM's GPIOS for debugging.
This is the code that that fully reproduces the problem:

#include "stm32f10x.h"
#include "system_stm32f10x.h"

#include "stdlib.h"
#include "stdio.h"

  const unsigned short RTC_FREQ = 62500;
  unsigned short prescaler_1ms = RTC_FREQ/1000;

int main()
{

//********** Clock Init **********
   RCC->CFGR |= RCC_CFGR_ADCPRE_0 | RCC_CFGR_ADCPRE_1; // ADC prescaler
   RCC->APB2ENR |= RCC_APB2ENR_AFIOEN; // Alternate Function I/O clock enable
   RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; // I/O port C clock enable
   RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // I/O port A clock enable
   RCC->APB2ENR |= RCC_APB2ENR_ADC1EN; // ADC 1 interface clock enable
   RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // Timer 2 clock enable 
   RCC->AHBENR = RCC_AHBENR_DMA1EN; // DMA1 clock enable
   RCC->CSR = RCC_CSR_LSION; // Internal Low Speed oscillator enable
//********************************

/* GPIO Configuration */
   GPIOC->CRH = GPIO_CRH_MODE12_0; //GPIO Port C Pin 12 
   GPIOC->CRH |= GPIO_CRH_MODE13_1 | GPIO_CRH_MODE13_0;
   GPIOC->CRH |= GPIO_CRH_MODE10_0;
   GPIOC->CRH |= GPIO_CRH_MODE9_0;
   GPIOC->CRH |= GPIO_CRH_MODE8_0;
   GPIOC->CRL = GPIO_CRL_MODE7_0;
   GPIOC->CRL |= GPIO_CRL_MODE6_0;
   GPIOC->CRL |= GPIO_CRL_MODE4_0;
   GPIOC->CRL |= GPIO_CRL_MODE3_0;


   while(1){

      if(prescaler_1ms & (1<<0))GPIOC->BSRR |= GPIO_BSRR_BR13;
      else GPIOC->BSRR |= GPIO_BSRR_BS13;
      if(prescaler_1ms & (1<<1))GPIOC->BSRR |= GPIO_BSRR_BR12;
      else GPIOC->BSRR |= GPIO_BSRR_BS12;
      if(prescaler_1ms & (1<<2))GPIOC->BSRR |= GPIO_BSRR_BR10;
      else GPIOC->BSRR |= GPIO_BSRR_BS10;
      if(prescaler_1ms & (1<<3))GPIOC->BSRR |= GPIO_BSRR_BR9;
      else GPIOC->BSRR |= GPIO_BSRR_BS9;
      if(prescaler_1ms & (1<<4))GPIOC->BSRR |= GPIO_BSRR_BR8;
      else GPIOC->BSRR |= GPIO_BSRR_BS8;
      if(prescaler_1ms & (1<<5))GPIOC->BSRR |= GPIO_BSRR_BR7;
      else GPIOC->BSRR |= GPIO_BSRR_BS7;
      if(prescaler_1ms & (1<<6))GPIOC->BSRR |= GPIO_BSRR_BR6;
      else GPIOC->BSRR |= GPIO_BSRR_BS6;
      if(prescaler_1ms & (1<<7))GPIOC->BSRR |= GPIO_BSRR_BR4;
      else GPIOC->BSRR |= GPIO_BSRR_BS4;
      if(prescaler_1ms & (1<<8))GPIOC->BSRR |= GPIO_BSRR_BR3;
      else GPIOC->BSRR |= GPIO_BSRR_BS3;

   }
   return 0;
}

When that code compiles, we are expecting the result 0b111110 at the GPIOS. When we change

const unsigned short RTC_FREQ = 62500;  

to

unsigned short RTC_FREQ = 62500;  

we get 0b111111111.
This is the Makefile that we use:

EABI_PATH=$(ROOT_DIR)"arm_toolchain/gcc-arm-none-eabi-6-2017-q2-update/arm-none-eabi/"
CMSIS_INC_PATH=$(ROOT_DIR)"STMLib/STM32F10x_StdPeriph_Lib_V3.5.0/Libraries/CMSIS/CM3/"
PROJECT_INC=$(ROOT_DIR)

CXXINCS = -I$(EABI_PATH)"include" -I$(CMSIS_INC_PATH)"CoreSupport" -I$(CMSIS_INC_PATH)"DeviceSupport/ST/STM32F10x" -I$(PROJECT_INC)"Source" -I$(PROJECT_INC)"Includes"

CXXLIBS = -L$(EABI_PATH)"lib" -L$(EABI_PATH)"6.3.1"
CXXFLAGS = --specs=nosys.specs -DSTM32F10X_MD -DVECT_TAB_FLASH -fdata-sections -ffunction-sections -fno-exceptions -mthumb -mcpu=cortex-m3 -march=armv7-m -O2
LDFLAGS = -lstdc++ -Wl,--gc-sections

CC = $(EABI_PATH)"../bin/arm-none-eabi-gcc"
CXX = $(EABI_PATH)"../bin/arm-none-eabi-g++"
LD = $(EABI_PATH)"../bin/arm-none-eabi-ld"
STRIP = $(EABI_PATH)"../bin/arm-none-eabi-strip"

all:
        $(CC) $(CXXINCS) -c $(PROJECT_INC)"Source/syscalls.c" $(PROJECT_INC)"Source/startup.c" $(CXXFLAGS)
        $(CXX) $(CXXINCS) -c $(PROJECT_INC)"Source/main.cpp" $(CMSIS_INC_PATH)"DeviceSupport/ST/STM32F10x/system_stm32f10x.c" $(CXXFLAGS)
        $(CXX) $(CXXLIBS) -o main syscalls.o main.o startup.o -T linker.ld system_stm32f10x.o $(LDFLAGS)
        $(STRIP) --strip-all main
        $(EABI_PATH)"bin/objcopy" -O binary main app
        $(EABI_PATH)"bin/objdump" -b binary -m arm_any -D app > app_disasm
        rm -f *.o main adc timer task solenoid dma startup syscalls system_stm32f10x

Does anybody have a clue what can cause a problem like that? Is that a compiler bug? Have we missed something?

slaviber
  • 358
  • 2
  • 10
  • 1
    Note: I rolled back a bogus edit to the language tag – M.M Nov 12 '17 at 02:09
  • In the makefile is PROJECT_INT meant to be PROJECT_INC ? – M.M Nov 12 '17 at 02:19
  • 1
    What's in Source/startup.c and the linker script? – A.K. Nov 12 '17 at 02:32
  • 3
    My theory is that C++ initialization code, which is supposed to copy 62 into `prescaler_1ms`, is not called. When you define `RTC_FREQ` as `const`, the result of this computation is known at compile time, 62 lives in the flash and needs no initialization. – A.K. Nov 12 '17 at 02:35
  • @M.M Yes, it should be PROJECT_INC. – slaviber Nov 12 '17 at 09:34
  • @A.K. That could be it. The startup.c and linker.ld files are taken from http://www.downloads.seng.de/HowTo_ToolChain_STM32_Ubuntu.pdf pages **20** and **17** but I don't know what exactly happens there. Very strange problem, though. And the lack of a compiler warning is scary. – slaviber Nov 12 '17 at 09:42
  • You may want to disassemble and compare the resulting binaries. – n. m. could be an AI Nov 12 '17 at 10:05
  • If startup code is missing, the compiler can not give a warning. So there is nothing obscure there. Simply use an arm simulator for debugging or debug with jtag if possible. Having wrong content in vars which is fully reproducible is the simplest problem to solve in embedded designs... – Klaus Nov 12 '17 at 11:12

1 Answers1

5

Promoting my theory to an answer because it is confirmed by the startup code and LD script.

C++ initialization code, which is supposed to copy 62 into prescaler_1ms, is not called. When you define RTC_FREQ as const, the result of this computation is known at compile time, 62 lives in the flash and needs no initialization.

C++ initialization is performed by a number of generated functions, with names like _Z41__static_initialization_and_destruction_0ii. Pointers to these functions are collected by the compiler in the .init_array and .pre_init_array sections. Before main() is called, the startup code should iterate over these pointers and call each of them. The boundaries of these pointer arrays are known to the startup code because these special symbols are defined by the linker script:

__preinit_array_start, __preinit_array_end
__init_array_start, __init_array_end

The distinction between _preinit_array and __init_array is not yet clear to me. The former section is called before calling the _init function and the latter are called after that. In my project the _init function provided by gcc does not seem to be a valid function, so I do not call it.

There is a symmetrical procedure at the program termination, when C++ destructors of global objects are called using __fini_array_start and __fini_array_end. However, for embedded systems, it is likely not relevant.

The minimal steps to make a project call C++ initialization stuff are:

  1. Include the .init_array section in your linker script.

From the document you provided, it seems the .init_array section is already defined as:

. = ALIGN(4);
__preinit_array_start = .;
KEEP(*(.preinit_array))
__preinit_array_end = .;

. = ALIGN(4);
__init_array_start = .;
KEEP(*(SORT(.init_array.*)))
KEEP(*(.init_array))
__init_array_end = .;
  1. Have the code that calls those pointers at program startup. This part seems to be absent from your setup, which is the actual cause of the problem.

You could add the following code (or similar) to the __Init_Data() function in startup.c:

// usually these are defined with __attribute__((weak)) but I prefer to get errors when required things are missing
extern void (*__preinit_array_start[])(void);
extern void (*__preinit_array_end[])(void);
extern void (*__init_array_start[])(void);
extern void (*__init_array_end[])(void);

void __Init_Data(void) {

    // copying initialized data from flash to RAM
    ...

    // zeroing bss segment
    ...

    // calling C++ initializers
    void (**f)(void);
    for (f = __preinit_array_start; f != __preinit_array_end; f++)
        (*f)();
    // init(); // _init and _fini do not work for me
    for (f = __init_array_start; f != __init_array_end; f++)
        (*f)();
}

Again, I am not sure about the _init function, so it is commented out here. I may ask my own question some time later.

A.K.
  • 839
  • 6
  • 13
  • When using newlib, one can just call the `_start()` function, which calls all the init_array stuff, and jumps to `main()`. It takes care of zeroing `bss`, but initialized data must be copied beforehand.`void Reset_Handler() { SystemInit(); SystemClock_Config(); memcpy(&_sdata, &_sidata, &_edata - &_sdata); _start(); }` – followed Monica to Codidact Nov 12 '17 at 19:01