2

I'm writing some program for a stm32l0 microcontroller. I define some arrays as global variables:

volatile uint32_t     synth_counter  [N_CHANNELS];
volatile uint32_t     synth_pitch    [N_CHANNELS];
volatile uint32_t     synth_envelope [N_CHANNELS];
volatile int_fast8_t  synth_key      [N_CHANNELS];
volatile uint32_t     synth_active   [N_CHANNELS];
volatile uint32_t     synth_state    [N_CHANNELS];
volatile uint32_t     synth_envl_add [N_CHANNELS];
volatile uint32_t     synth_envl_sub [N_CHANNELS];
volatile uint32_t     synth_envl_goal[N_CHANNELS];
volatile int_fast8_t  synth_prev     [N_CHANNELS];
volatile int_fast8_t  synth_next     [N_CHANNELS];

...

         uint8_t      CH_ACTIVE [N_NOTES];
         int_fast8_t  CH_INDEX  [N_NOTES];

volatile uint32_t     PITCH     [N_NOTES];

volatile int8_t       SAMPLE    [N_SAMPLE] = DEFAULT_SAMPLE;

As you can see, the size of the array is a constant defined in a header file:

#define N_NOTES   97               // = (9 - 1) * 12 + 1

#define N_CHANNELS 12 //EVEN NUMBER
#define N_SAMPLE   256

I want those arrays have an initial value which can be overwritten later. For one of them I could do:

volatile int8_t       SAMPLE    [N_SAMPLE] = DEFAULT_SAMPLE;

because I know that N_SAMPLE will always be 256. So I could define something like this:

#define DEFAULT_SAMPLE \
{ \
    0x7F, 0x7F, 0x7F, 0x7F, 0x7E, 0x7E, 0x7E, 0x7D, \
    0x7D, 0x7C, 0x7B, 0x7A, 0x7A, 0x79, 0x78, 0x76, \
    0x75, 0x74, 0x73, 0x71, 0x70, 0x6E, 0x6D, 0x6B, \
    0x6A, 0x68, 0x66, 0x64, 0x62, 0x60, 0x5E, 0x5C, \
    0x5A, 0x57, 0x55, 0x53, 0x50, 0x4E, 0x4B, 0x49, \
    0x46, 0x44, 0x41, 0x3E, 0x3C, 0x39, 0x36, 0x33, \
    0x30, 0x2D, 0x2A, 0x27, 0x25, 0x22, 0x1E, 0x1B, \
    0x18, 0x15, 0x12, 0x0F, 0x0C, 0x09, 0x06, 0x03, \
    0x00, 0xFC, 0xF9, 0xF6, 0xF3, 0xF0, 0xED, 0xEA, \
    0xE7, 0xE4, 0xE1, 0xDD, 0xDA, 0xD8, 0xD5, 0xD2, \
    0xCF, 0xCC, 0xC9, 0xC6, 0xC3, 0xC1, 0xBE, 0xBB, \
    0xB9, 0xB6, 0xB4, 0xB1, 0xAF, 0xAC, 0xAA, 0xA8, \
    0xA5, 0xA3, 0xA1, 0x9F, 0x9D, 0x9B, 0x99, 0x97, \
    0x95, 0x94, 0x92, 0x91, 0x8F, 0x8E, 0x8C, 0x8B, \
    0x8A, 0x89, 0x87, 0x86, 0x85, 0x85, 0x84, 0x83, \
    0x82, 0x82, 0x81, 0x81, 0x81, 0x80, 0x80, 0x80, \
    0x80, 0x80, 0x80, 0x80, 0x81, 0x81, 0x81, 0x82, \
    0x82, 0x83, 0x84, 0x85, 0x85, 0x86, 0x87, 0x89, \
    0x8A, 0x8B, 0x8C, 0x8E, 0x8F, 0x91, 0x92, 0x94, \
    0x95, 0x97, 0x99, 0x9B, 0x9D, 0x9F, 0xA1, 0xA3, \
    0xA5, 0xA8, 0xAA, 0xAC, 0xAF, 0xB1, 0xB4, 0xB6, \
    0xB9, 0xBB, 0xBE, 0xC1, 0xC3, 0xC6, 0xC9, 0xCC, \
    0xCF, 0xD2, 0xD5, 0xD8, 0xDA, 0xDD, 0xE1, 0xE4, \
    0xE7, 0xEA, 0xED, 0xF0, 0xF3, 0xF6, 0xF9, 0xFC, \
    0xFF, 0x03, 0x06, 0x09, 0x0C, 0x0F, 0x12, 0x15, \
    0x18, 0x1B, 0x1E, 0x22, 0x25, 0x27, 0x2A, 0x2D, \
    0x30, 0x33, 0x36, 0x39, 0x3C, 0x3E, 0x41, 0x44, \
    0x46, 0x49, 0x4B, 0x4E, 0x50, 0x53, 0x55, 0x57, \
    0x5A, 0x5C, 0x5E, 0x60, 0x62, 0x64, 0x66, 0x68, \
    0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, \
    0x75, 0x76, 0x78, 0x79, 0x7A, 0x7A, 0x7B, 0x7C, \
    0x7D, 0x7D, 0x7E, 0x7E, 0x7E, 0x7F, 0x7F, 0x7F  \
}

I can not know this for other arrays. I would want to create for example a #define DEFAULT_SYNTH_NEXT which will become:
{ 1, 2, 3, 4, 5, 6, 7, -1} when N_CHANNELS equals 8,
{ 1, 2, 3, 4, 5, -1} when N_CHANNELS equal 6, and so on.
Create a #define DEFAULT_CH_ACTIVE which is an array filled with N_NOTES values of -1. and so on and so on.

Is it possible, using the C preprocessor to achieve a #define which depends on previously defined number in a way described above? Somehow (recursively?) (iterably?) produce the expected result?

I found an article http://jhnet.co.uk/articles/cpp_magic but this does not look very readable and understandable and I didn't manage to figure it out yet. I'm hoping for something more clear to read.

I see 2 alternatives to what I want, but they are not ideal:

Alternative 1 is statically defining like:

#define DEFAULT_SYNTH_PREV {-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
#define DEFAULT_SYNTH_NEXT {1, 2, 3, 4 ,5, 6, 7, 8, 9, 10, 11, -1}

and so on, but then I have to manually edit it every time I update one of those values.

Alternative 2 is initialising the arrays in the main() function, in for() loops before entering the main loop. This is what I'm currently doing,

    set_tuning(DEFAULT_TUNING);
    
    for(i=0; i<N_CHANNELS; ++i)
    {
        synth_counter[i] = 0;
        synth_pitch[i] = 0;
        synth_envelope[i] = 0;
        synth_envl_add[i] = 0;
        synth_envl_sub[i] = 0;
        synth_envl_goal[i] = 0;
        synth_active[i] = 0;
        synth_state[i] = ENVL_0;
        if (i==0)
            synth_prev[i] = -1;
        else
            synth_prev[i] = i-1;
        if (i==(N_CHANNELS - 1))
            synth_next[i] = -1;
        else
            synth_next[i] = i+1;
        synth_key[i] = -1;
    }
    for (i=0; i<N_NOTES; ++i)
    {
        CH_ACTIVE[i] = 0;
        CH_INDEX[i] = -1;
    }

This does prefill the array with required values but it is less optimal code than the code which prefills predefined variables by copying from FLASH to RAM in the reset handler in the startup code which is included to the project by default:

Reset_Handler:  
  ldr   r0, =_estack
  mov   sp, r0          /* set stack pointer */

/* Copy the data segment initializers from flash to SRAM */
  movs  r1, #0
  b  LoopCopyDataInit

CopyDataInit:
  ldr  r3, =_sidata
  ldr  r3, [r3, r1]
  str  r3, [r0, r1]
  adds  r1, r1, #4

LoopCopyDataInit:
  ldr  r0, =_sdata
  ldr  r3, =_edata
  adds  r2, r0, r1
  cmp  r2, r3
  bcc  CopyDataInit
  ldr  r2, =_sbss
  b  LoopFillZerobss
/* Zero fill the bss segment. */
FillZerobss:
  movs  r3, #0
  str  r3, [r2]
  adds r2, r2, #4


LoopFillZerobss:
  ldr  r3, = _ebss
  cmp  r2, r3
  bcc  FillZerobss

/* Call the clock system intitialization function.*/
  bl  SystemInit
/* Call static constructors */
    bl __libc_init_array
/* Call the application's entry point.*/
  bl  main

This is why I'm still hoping to be able to "dynamically" #define the array initialisers

15
  • 3
    Forget the preprocessor. Write a code generator. Commented Aug 10, 2021 at 20:07
  • 2
    #define N_NOTES 97 // = (9 - 1) * 12 + 1 - do not do this. If a number makes sense as a result of a math operation, put the math operation into number definition. I have seen some errors with unpleasant consequences where there was a math error like that. Commented Aug 10, 2021 at 20:15
  • 1
    Answering to the question as asked - it could be possible, but it would be a terribly looking code. Sophisticated PP constructs are frown upon, are a nightmare to troubleshoot and produce cryptic errors. Just codegen. Commented Aug 10, 2021 at 20:17
  • 1
    @Neil - static variable preserves value when out of scope. But these are global variables so always in scope. Global because interrupt handling function has to see them and they represent global state of device. preprocessor macro defines only initial value. Commented Aug 10, 2021 at 20:20
  • 1
    @SergeyA "just codegen" - I was kind of hoping to avoid this in this project as it didn't seem to be complex enough but this might be the solution after all. Commented Aug 10, 2021 at 20:32

3 Answers 3

2

For your first requirement, you can simply do something like this:

#if N_CHANNELS == 8
    #define DEFAULT_SYNTH_NEXT { 1, 2, 3, 4, 5, 6, 7, -1 }
#endif

#if N_CHANNELS == 6
    #define DEFAULT_SYNTH_NEXT { 1, 2, 3, 4, 5, -1 }
#endif

and so on.

Not sure how to do the second one.


Or, as per @chux's comment, you could do:

#if N_CHANNELS == 8
    #define DEFAULT_SYNTH_NEXT { 1, 2, 3, 4, 5, 6, 7, -1 }
#elif N_CHANNELS == 6
    #define DEFAULT_SYNTH_NEXT { 1, 2, 3, 4, 5, -1 }
...
#else
    #error "Unsupported N_CHANNELS value"
#endif
Sign up to request clarification or add additional context in comments.

3 Comments

This could be the solution in my specific case as the possible values for N_CHANNELS are limited. as mentioned in my previous comment, possible values are 2, 4, 6, 8, 10, 12, 14, 16. For all my other arrays I can use "designated initialisers" gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Designated-Inits.htm volatile uint32_t synth_state [N_CHANNELS] = {[0 ... (N_CHANNELS-1)] = ENVL_0};
A #if ... #elif .... #elif .... #else .... #endif allows for a #error "useful message" in the #else.
With limited N_CHANNELS, this is probably the easiest and least-bug-prone solution.
1

Thats really really ugly, but you can make use of designated initializers:

#include <assert.h>
#include <stdio.h>

#define VAL(max, x) [(x <= max) ? (x - 1) : 0] = (x <= max) ? x : 1,

#define MAKE_ARRAY(name, max)           \
static_assert((max > 0) && (max < 9));  \
volatile int name[] = {                 \
    VAL(max, 1)                         \
    VAL(max, 2)                         \
    VAL(max, 3)                         \
    VAL(max, 4)                         \
    VAL(max, 5)                         \
    VAL(max, 6)                         \
    VAL(max, 7)                         \
    VAL(max, 8)                         \
    [max - 1] = -1                      \
}

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Woverride-init"

MAKE_ARRAY(synth, 3);
// More arrays here

#pragma GCC diagnostic pop

int main(void)
{
    size_t size = sizeof(synth) / sizeof(*synth);

    printf("array size = %zu\nvalues = {", size);
    for (size_t i = 0; i < size; i++)
    {
        printf(" %d,", synth[i]);
    }
    printf(" }\n");
    return 0;
}

MAKE_ARRAY(synth, 3) expands to:

static_assert((3 > 0) && (3 < 9));
volatile int synth[] = {[0]=1,[1]=2,[2]=-1,[0]=1,[0]=1,[0]=1,[0]=1,[0]=1};

The ouput is:

array size = 3
values = { 1, 2, -1, }

I'm using

#pragma GCC diagnostic ignored "-Woverride-init"

to silence the compiler due to the repetition in: ,[0]=1,[0]=1,[0]=1 ...

Otherwise I get:

warning: initialized field overwritten [-Woverride-init]

You can also use:

#include <stdio.h>
#include <assert.h>

#define VAL1 -1
#define VAL2 1
#define VAL3 VAL2, 2
#define VAL4 VAL3, 3
#define VAL5 VAL4, 4
#define VAL6 VAL5, 5
#define VAL7 VAL6, 6
#define VAL8 VAL7, 7

#define MAKE_ARRAY(name, max)           \
static_assert((max > 0) && (max < 9));  \
volatile int name[] = {VAL##max, -1}

MAKE_ARRAY(synth, 3);
// More arrays here

int main(void)
{
    size_t size = sizeof(synth) / sizeof(*synth);

    printf("array size = %zu\nvalues = {", size);
    for (size_t i = 0; i < size; i++)
    {
        printf(" %d,", synth[i]);
    }
    printf(" }\n");
    return 0;
}

In this case MAKE_ARRAY(synth, 3) expands to:

static_assert((3 > 0) && (3 < 9));
volatile int synth[] = {1, 2, -1};

But then you can not use:

#define NELEMS 3
MAKE_ARRAY(synth, NELEMS);

nor

enum {NELEMS = 3};
MAKE_ARRAY(synth, NELEMS);

while first version allows you to do it.

Comments

0
I found an article http://jhnet.co.uk/articles/cpp_magic but this does not look very readable and understandable and I didn't manage to figure it out yet.

Rather than untangle how the magic works, you can just use a library with the magic done for you.

Using boost preprocessor:

#include <boost/preprocessor/arithmetic/dec.hpp>
#include <boost/preprocessor/repetition/enum_shifted.hpp>

#define IOTA_FROM_0(Z_, COUNT_, _) \
    BOOST_PP_DEC(COUNT_)
#define DEFAULT_SYNTH_PREV \
    { -1, BOOST_PP_ENUM_SHIFTED(N_CHANNELS, IOTA_FROM_0, ) }

#define IOTA(Z_, COUNT_, _) COUNT_
#define DEFAULT_SYNTH_NEXT \
    { BOOST_PP_ENUM_SHIFTED(N_CHANNELS, IOTA, ) , -1 }

...and yes, boost preprocessor works in C.

This just uses BOOST_PP_ENUM_SHIFTED, which is almost everything you want. For your PREV macro, this just adds BOOST_PP_DEC. Since that saturates at 0 we plug the -1 in manually (not so different than what you do in your loop, though for the loop case 0-1 does equal -1!)

Demo

On coliru.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.