Author Topic: Setting up C++ on a microcontroller  (Read 6462 times)

0 Members and 2 Guests are viewing this topic.

Offline SimonTopic starter

  • Global Moderator
  • *****
  • Posts: 18911
  • Country: gb
  • Did that just blow up? No? might work after all !!
    • Simon's Electronics
Re: Setting up C++ on a microcontroller
« Reply #25 on: February 11, 2026, 11:10:59 am »
... or should I ditch the C stuff and just have it treated as C++?

I would never attempt to use two different compilers to compile sections and then link them into to a single program. It can be done, but there are some gotchas (which are beyond my level of expertise). But a C++ compiler can handle C code perfectly well, so there is no need to make it more complicated.

And C++ is a different language. You may get different (and probably more) warnings and error messages from the compiler.

Well that is what I asked early in the thread. I think it was copilot that did all these separate flags and calls. The c files are all CMSIS / DFPack files which tend to have the bit of text that tells a C++ compiler that this is C.
 

Offline Doctorandus_P

  • Super Contributor
  • ***
  • Posts: 5389
  • Country: nl
Re: Setting up C++ on a microcontroller
« Reply #26 on: February 11, 2026, 12:18:55 pm »
Copying some AI hallucinations without verifying what it does is a recipe for disaster.
 

Offline az1

  • Regular Contributor
  • *
  • Posts: 61
  • Country: us
Re: Setting up C++ on a microcontroller
« Reply #27 on: February 11, 2026, 12:27:41 pm »
Well that is what I asked early in the thread. I think it was copilot that did all these separate flags and calls. The c files are all CMSIS / DFPack files which tend to have the bit of text that tells a C++ compiler that this is C.

Strictly speaking unlike Objective-C/Objective-C++, C++ is not a superset of C.  A C++ compiler will handle most C, especially as things have converged a bit with recent standards but things may break.  Assuming you're passing enough of the same compiler flags (especially things like board/MCU related defines and architecture flags) to each you should be fine to let gcc compile the C files.

What won't work (or won't work well) is linking your final binary with gcc.  Use g++ so the linker can find the C++ specific gunk.

The linker has no say in whether you call __libc_init_array() or not, and will not care either way. For C++ of course it would be a major problem if not called in startup code (after data/bss init). Linker does nothing extraordinary for C++.

That's a bit pedantic.  The compiler will direct the linker to include the appropriate blobs and in the C++ case that means you need to be sure to invoke the C++ compiler for linking.  While it seems unlikely that a person would knowingly (and successfully) link their C++ program with the C compiler, it is entirely within the realm of possibility for an AI hallucination to attempt such shenanigans.

Well that is what I asked early in the thread. I think it was copilot that did all these separate flags and calls. The c files are all CMSIS / DFPack files which tend to have the bit of text that tells a C++ compiler that this is C.

Unless the question is "how do I make an elon musk style fart noise generator" the answer is almost never AI.

Disclaimer: I left C++ behind back when there were only five types of constructors and variadic templates were the sort of thing you didn't discuss in polite company.

It's not a mistake to pass values into constructors, the bigger problem is relying on objects to be statically initialized. Static initialization has always been fragile in C++ because order isn't guaranteed.  Initializing things at runtime makes life a lot more predictable, and avoiding globals where you can makes things easier to test and generally easier to reason about.  But in general if your compiler is throwing warnings, pay attention to them.  Treating warnings as errors (-Werror) is a decent way to keep to best practices.

Me? I went all in on Rust for desktop stuff a few years ago and am taking a second crack at it for embedded stuff.  I've exactly two MCU related compiler directives (target = thumbv7em-none-eabihf, no-std) and if it's not initialized before use it won't compile.  Support for the D2x and 5x is decent and actively maintained and there's a somewhat active Matrix room. The AI slop is better too (so far as that stuff is ever usable).
 

Offline Pseudobyte

  • Frequent Contributor
  • **
  • Posts: 358
  • Country: us
  • Embedded Systems Engineer / PCB Designer
Re: Setting up C++ on a microcontroller
« Reply #28 on: February 11, 2026, 01:00:05 pm »
I am pretty sure that you are just using node_id before you initialize it.

node_id is simply an U32 value being passed to the object on instantiation. All other variables passed also have the same warning.

^ This is objectively an incorrect statement based on the code you provided. Really it should read:
new_node_n is simply an U32 value being passed to the object on instantiation.

If we look at your constructor, you use node_id (a class member) before you give it a value that comes from new_node_n.


io_board_1_class::io_board_1_class(device_type deviceType, can_open_class *new_bus, uint32_t new_node_n,
                                   encoder_class * encoder0ptr, encoder_class * encoder1ptr)
{

    bus->addNode(deviceType, node_id, object);

    encoder[0] = encoder0ptr;
    encoder[1] = encoder1ptr;
    bus = new_bus;
    node_id = new_node_n;
    node_status.od_size = sizeof(object) / sizeof(od_type);
    node_status.NMT_state = HB_OPERATIONAL;
    node_status.heartbeat_time_stamp = 0;


node_id needs to have a value before you use it or the behavior is undefined. While you're at it you should probably move the initialization for bus, before it is used for the first time.


io_board_1_class::io_board_1_class(device_type deviceType, can_open_class *new_bus, uint32_t new_node_n,
                                   encoder_class * encoder0ptr, encoder_class * encoder1ptr)
{
    node_id = new_node_n;
    bus = new_bus;

    bus->addNode(deviceType, node_id, object);
    encoder[0] = encoder0ptr;
    encoder[1] = encoder1ptr;
   
    node_status.od_size = sizeof(object) / sizeof(od_type);
    node_status.NMT_state = HB_OPERATIONAL;
    node_status.heartbeat_time_stamp = 0;

“They Don’t Think It Be Like It Is, But It Do”
 

Offline gpr

  • Contributor
  • Posts: 43
  • Country: gb
Re: Setting up C++ on a microcontroller
« Reply #29 on: February 11, 2026, 01:41:52 pm »
... or should I ditch the C stuff and just have it treated as C++?

I would never attempt to use two different compilers to compile sections and then link them into to a single program. It can be done, but there are some gotchas (which are beyond my level of expertise). But a C++ compiler can handle C code perfectly well, so there is no need to make it more complicated.

And C++ is a different language. You may get different (and probably more) warnings and error messages from the compiler.

There is nothing totally wrong in using both C and C++ compilers in one project as long as you don't forget about things like "extern C" for things which are comiled in one language but used from another language. Happens all the time when you are using some external libraries.

But if everything is in one project, and you are not reusing C library from C++ or something like that, it is more convenient to just use C++ for everything.
 

Offline cv007

  • Super Contributor
  • ***
  • Posts: 1061
Re: Setting up C++ on a microcontroller
« Reply #30 on: February 11, 2026, 02:52:46 pm »
Quote
that means you need to be sure to invoke the C++ compiler for linking.
Invoking gcc for the link produces the same results as g++ for me, example below. I can also do the same on bigger projects and nothing changes.

Code: [Select]
//main.cpp- turn on led on PA5 using minimal code, but make global constructor do the work
using u32 = unsigned int;
extern "C" void __libc_init_array();

[[ using gnu : section(".vectors"), used ]]
struct { u32 stack; void(*vfunc)(); } flashVector { 0x20002000, __libc_init_array };

struct PA5 {
    u32& regMode{ *reinterpret_cast<u32*>(0x5000'0000) };
    u32& regBssr{ *reinterpret_cast<u32*>(0x5000'0018) };
    u32& rccIopenr{ *reinterpret_cast<u32*>(0x4002'1034) };
    PA5 (){ rccIopenr = 1; regMode = 1<<(5*2); regBssr = 1<<5; while(1){} }
    };
PA5 pa5;

arm-none-eabi-g++ -c main.cpp -mcpu=cortex-m0plus -Os -fdata-sections -ffunction-sections -Wall -Wextra -fno-exceptions -fno-rtti
arm-none-eabi-gcc -mcpu=cortex-m0plus -Tlinker-script.txt --specs=nano.specs -Wl,--gc-sections main.o
arm-none-eabi-objcopy -O binary a.out a.bin
copy a.bin to dev board using virtual drive programming

led lights up.
« Last Edit: February 11, 2026, 02:58:12 pm by cv007 »
 

Offline SimonTopic starter

  • Global Moderator
  • *****
  • Posts: 18911
  • Country: gb
  • Did that just blow up? No? might work after all !!
    • Simon's Electronics
Re: Setting up C++ on a microcontroller
« Reply #31 on: February 11, 2026, 04:09:22 pm »
I am pretty sure that you are just using node_id before you initialize it.

node_id is simply an U32 value being passed to the object on instantiation. All other variables passed also have the same warning.

^ This is objectively an incorrect statement based on the code you provided. Really it should read:
new_node_n is simply an U32 value being passed to the object on instantiation.

If we look at your constructor, you use node_id (a class member) before you give it a value that comes from new_node_n.


io_board_1_class::io_board_1_class(device_type deviceType, can_open_class *new_bus, uint32_t new_node_n,
                                   encoder_class * encoder0ptr, encoder_class * encoder1ptr)
{

    bus->addNode(deviceType, node_id, object);

    encoder[0] = encoder0ptr;
    encoder[1] = encoder1ptr;
    bus = new_bus;
    node_id = new_node_n;
    node_status.od_size = sizeof(object) / sizeof(od_type);
    node_status.NMT_state = HB_OPERATIONAL;
    node_status.heartbeat_time_stamp = 0;


node_id needs to have a value before you use it or the behavior is undefined. While you're at it you should probably move the initialization for bus, before it is used for the first time.


io_board_1_class::io_board_1_class(device_type deviceType, can_open_class *new_bus, uint32_t new_node_n,
                                   encoder_class * encoder0ptr, encoder_class * encoder1ptr)
{
    node_id = new_node_n;
    bus = new_bus;

    bus->addNode(deviceType, node_id, object);
    encoder[0] = encoder0ptr;
    encoder[1] = encoder1ptr;
   
    node_status.od_size = sizeof(object) / sizeof(od_type);
    node_status.NMT_state = HB_OPERATIONAL;
    node_status.heartbeat_time_stamp = 0;



Yes you are right, I've sorted that out, so now it complains only about:

warning: '*this.io_board_1_class::encoder[0]' is used uninitialized [-Wuninitialized]
warning: '*this.io_board_1_class::encoder[1]' is used uninitialized [-Wuninitialized]

these are pointers  inside the io_board_1_class class object to encoder_class objects. maybe I need to rethink how this is all done.
 

Offline SimonTopic starter

  • Global Moderator
  • *****
  • Posts: 18911
  • Country: gb
  • Did that just blow up? No? might work after all !!
    • Simon's Electronics
Re: Setting up C++ on a microcontroller
« Reply #32 on: February 11, 2026, 04:41:45 pm »
Trying to compile with just C++ results in a slew of errors on the startup file along the lines of:
error: 'void CAN1_Handler()' aliased to undefined symbol 'Dummy_Handler'

This file is not enclosed with ifdef C++ extern C.

So it looks like I may as well keep compilation to the language of each file. All files in the DFP pack from microchip are *.c and *.h. All my C++ files are *.cpp and *.hpp
 

Offline Siwastaja

  • Super Contributor
  • ***
  • Posts: 11333
  • Country: fi
Re: Setting up C++ on a microcontroller
« Reply #33 on: February 11, 2026, 05:07:31 pm »
Yes you are right, I've sorted that out, so now it complains only about:

warning: '*this.io_board_1_class::encoder[0]' is used uninitialized [-Wuninitialized]
warning: '*this.io_board_1_class::encoder[1]' is used uninitialized [-Wuninitialized]

So now you are using them uninitialized. Your issue is lack of logical thinking, essential to programming, and wandering off to weirdest possible explanations to obvious and simple problems.

You miss the fundamental fact that C++ (like C, and like >90% of programming languages) are imperative languages where things go in sequence. Programs written in C++ are list of steps for computer (in case of C and C++, a virtual imaginary computer) to execute in order. You may also miss the fact that compilers are exact about variable names. If you have variables new_node_id, and node_id, those are two totally different things.

Your errors follow the same pattern:
Code: [Select]
1) Put cake in oven
2) Mix flour with the egg and sugar
3) Set oven to 150degC
4) Add baking soda to the flour

You need to start paying closer attention to writing the code, and reading the code. You write too much code in too little time, and AI is going to make this much worse for you.
 

Offline SimonTopic starter

  • Global Moderator
  • *****
  • Posts: 18911
  • Country: gb
  • Did that just blow up? No? might work after all !!
    • Simon's Electronics
Re: Setting up C++ on a microcontroller
« Reply #34 on: February 11, 2026, 05:14:01 pm »
I'm not listening to AI, I've seen the bullshit it suggested for code comments.

the pointers are variables in the class I am instantiating, so I would assume that these exist at the point I say, I want an object of this class, then the constructor would be called and I am trying to assign addresses to these pointers of existing objects. but apparently simple pointer variables that are part of the class definition are not available, so now who is trying to mix the ingredients after the cake is in the oven?

This is the class, the contention is now over " encoder_class * encoder[2]" is this not the correct way to setup an array of pointers to class type objects? or have I accidentally created 2 class instances ?

Code: [Select]
class io_board_1_class {

public:   
    can_open_class* bus;
   
    uint32_t node_id;
   
    uint16_t short_press;
   
    uint16_t long_press;
   
    volatile encoder_command_type encoder_command[2];
   
    encoder_class * encoder[2];
   
    node_status_type node_status;

    uint32_t save_parameters = 0x65766173;
   
    uint32_t restore_parameters = 0x64616F6C;
   
    uint32_t producer_heartbeat_time = 500; // Heart beat in ms

    od_type object[COMMS_OD_SIZE + 10u] = // index number must be the amount of entries as the array sizeof() is used to calculate the number of object entries
    {
        &node_status, STRUCTURE, 0x00, 0x0060, 0x0,

        // RPDO stuff
        &node_status.pdo[0].highest_sub_index_supported, UNSIGNED8, 0x00, 0x1400, RO,
        &node_status.pdo[0].cob_id, UNSIGNED32, 0x01, 0x1400, RW,
        &node_status.pdo[0].transmission_type, UNSIGNED8, 0x02, 0x1400, RW,
        &node_status.pdo[0].inhibit_time, UNSIGNED16, 0x03, 0x1400, RW,
        &node_status.pdo[0].event_timer, UNSIGNED16, 0x05, 0x1400, RW,
        &node_status.pdo[0].start_sync, UNSIGNED8, 0x06, 0x1400, RW,
        &node_status.pdo[1].highest_sub_index_supported, UNSIGNED8, 0x00, 0x1401, RO,
        &node_status.pdo[1].cob_id, UNSIGNED32, 0x01, 0x1401, RW,
        &node_status.pdo[1].transmission_type, UNSIGNED8, 0x02, 0x1401, RW,
        &node_status.pdo[1].inhibit_time, UNSIGNED16, 0x03, 0x1401, RW,
        &node_status.pdo[1].event_timer, UNSIGNED16, 0x05, 0x1401, RW,
        &node_status.pdo[1].start_sync, UNSIGNED8, 0x06, 0x1401, RW,
        &node_status.pdo[2].highest_sub_index_supported, UNSIGNED8, 0x00, 0x1402, RO,
        &node_status.pdo[2].cob_id, UNSIGNED32, 0x01, 0x1402, RW,
        &node_status.pdo[2].transmission_type, UNSIGNED8, 0x02, 0x1402, RW,
        &node_status.pdo[2].inhibit_time, UNSIGNED16, 0x03, 0x1402, RW,
        &node_status.pdo[2].event_timer, UNSIGNED16, 0x05, 0x1402, RW,
        &node_status.pdo[2].start_sync, UNSIGNED8, 0x06, 0x1402, RW,
        &node_status.pdo[3].highest_sub_index_supported, UNSIGNED8, 0x00, 0x1403, RO,
        &node_status.pdo[3].cob_id, UNSIGNED32, 0x01, 0x1403, RW,
        &node_status.pdo[3].transmission_type, UNSIGNED8, 0x02, 0x1403, RW,
        &node_status.pdo[3].inhibit_time, UNSIGNED16, 0x03, 0x1403, RW,
        &node_status.pdo[3].event_timer, UNSIGNED16, 0x05, 0x1403, RW,
        &node_status.pdo[3].start_sync, UNSIGNED8, 0x06, 0x1403, RW,

       
        &node_status.pdo[0].mapped_objects_n, UNSIGNED8, 0x00, 0x1600, RW,
        &node_status.pdo[0].map[0], UNSIGNED32, 0x01, 0x1600, RW,
        &node_status.pdo[0].map[1], UNSIGNED32, 0x02, 0x1600, RW,
        &node_status.pdo[0].map[2], UNSIGNED32, 0x03, 0x1600, RW,
        &node_status.pdo[0].map[3], UNSIGNED32, 0x04, 0x1600, RW,
        &node_status.pdo[1].mapped_objects_n, UNSIGNED8, 0x00, 0x1601, RW,
        &node_status.pdo[1].map[0], UNSIGNED32, 0x01, 0x1601, RW,
        &node_status.pdo[1].map[1], UNSIGNED32, 0x02, 0x1601, RW,
        &node_status.pdo[1].map[2], UNSIGNED32, 0x03, 0x1601, RW,
        &node_status.pdo[1].map[3], UNSIGNED32, 0x04, 0x1601, RW,
        &node_status.pdo[2].mapped_objects_n, UNSIGNED8, 0x00, 0x1602, RW,
        &node_status.pdo[2].map[0], UNSIGNED32, 0x01, 0x1602, RW,
        &node_status.pdo[2].map[1], UNSIGNED32, 0x02, 0x1602, RW,
        &node_status.pdo[2].map[2], UNSIGNED32, 0x03, 0x1602, RW,
        &node_status.pdo[2].map[3], UNSIGNED32, 0x04, 0x1602, RW,
        &node_status.pdo[3].mapped_objects_n, UNSIGNED8, 0x00, 0x1603, RW,
        &node_status.pdo[3].map[0], UNSIGNED32, 0x01, 0x1603, RW,
        &node_status.pdo[3].map[1], UNSIGNED32, 0x02, 0x1603, RW,
        &node_status.pdo[3].map[2], UNSIGNED32, 0x03, 0x1603, RW,
        &node_status.pdo[3].map[3], UNSIGNED32, 0x04, 0x1603, RW,

        // TPDO stuff
        &node_status.pdo[4].highest_sub_index_supported, UNSIGNED8, 0x00, 0x1800, RW,
        &node_status.pdo[4].cob_id, UNSIGNED32, 0x01, 0x1800, RW,
        &node_status.pdo[4].transmission_type, UNSIGNED8, 0x02, 0x1800, RW,
        &node_status.pdo[4].inhibit_time, UNSIGNED16, 0x03, 0x1800, RW,
        &node_status.pdo[4].event_timer, UNSIGNED16, 0x05, 0x1800, RW,
        &node_status.pdo[4].start_sync, UNSIGNED8, 0x06, 0x1800, RW,
        &node_status.pdo[5].highest_sub_index_supported, UNSIGNED8, 0x00, 0x1801, RW,
        &node_status.pdo[5].cob_id, UNSIGNED32, 0x01, 0x1801, RW,
        &node_status.pdo[5].transmission_type, UNSIGNED8, 0x02, 0x1801, RW,
        &node_status.pdo[5].inhibit_time, UNSIGNED16, 0x03, 0x1801, RW,
        &node_status.pdo[5].event_timer, UNSIGNED16, 0x05, 0x1801, RW,
        &node_status.pdo[5].start_sync, UNSIGNED8, 0x06, 0x1800, RW,
        &node_status.pdo[6].highest_sub_index_supported, UNSIGNED8, 0x00, 0x1802, RW,
        &node_status.pdo[6].cob_id, UNSIGNED32, 0x01, 0x1802, RW,
        &node_status.pdo[6].transmission_type, UNSIGNED8, 0x02, 0x1802, RW,
        &node_status.pdo[6].inhibit_time, UNSIGNED16, 0x03, 0x1802, RW,
        &node_status.pdo[6].event_timer, UNSIGNED16, 0x05, 0x1802, RW,
        &node_status.pdo[6].start_sync, UNSIGNED8, 0x06, 0x1800, RW,
        &node_status.pdo[7].highest_sub_index_supported, UNSIGNED8, 0x00, 0x1803, RW,
        &node_status.pdo[7].cob_id, UNSIGNED32, 0x01, 0x1803, RW,
        &node_status.pdo[7].transmission_type, UNSIGNED8, 0x02, 0x1803, RW,
        &node_status.pdo[7].inhibit_time, UNSIGNED16, 0x03, 0x1803, RW,
        &node_status.pdo[7].event_timer, UNSIGNED16, 0x05, 0x1803, RW,
        &node_status.pdo[7].start_sync, UNSIGNED8, 0x06, 0x1803, RW,

       
        &node_status.pdo[4].mapped_objects_n, UNSIGNED8, 0x00, 0x1A00, RW,
        &node_status.pdo[4].map[0], UNSIGNED32, 0x01, 0x1A00, RW,
        &node_status.pdo[4].map[1], UNSIGNED32, 0x02, 0x1A00, RW,
        &node_status.pdo[4].map[2], UNSIGNED32, 0x03, 0x1A00, RW,
        &node_status.pdo[4].map[3], UNSIGNED32, 0x04, 0x1A00, RW,
        &node_status.pdo[5].mapped_objects_n, UNSIGNED8, 0x00, 0x1A01, RW,
        &node_status.pdo[5].map[0], UNSIGNED32, 0x01, 0x1A01, RW,
        &node_status.pdo[5].map[1], UNSIGNED32, 0x02, 0x1A01, RW,
        &node_status.pdo[5].map[2], UNSIGNED32, 0x03, 0x1A01, RW,
        &node_status.pdo[5].map[3], UNSIGNED32, 0x04, 0x1A01, RW,
        &node_status.pdo[6].mapped_objects_n, UNSIGNED8, 0x00, 0x1A02, RW,
        &node_status.pdo[6].map[0], UNSIGNED32, 0x01, 0x1A02, RW,
        &node_status.pdo[6].map[1], UNSIGNED32, 0x02, 0x1A02, RW,
        &node_status.pdo[6].map[2], UNSIGNED32, 0x03, 0x1A02, RW,
        &node_status.pdo[6].map[3], UNSIGNED32, 0x04, 0x1A02, RW,
        &node_status.pdo[7].mapped_objects_n, UNSIGNED8, 0x00, 0x1A03, RW,
        &node_status.pdo[7].map[0], UNSIGNED32, 0x01, 0x1A03, RW,
        &node_status.pdo[7].map[1], UNSIGNED32, 0x02, 0x1A03, RW,
        &node_status.pdo[7].map[2], UNSIGNED32, 0x03, 0x1A03, RW,
        &node_status.pdo[7].map[3], UNSIGNED32, 0x04, 0x1A03, RW,

       
        &save_parameters, UNSIGNED32, 0x01, 0x1010, 0x0,
       
        &restore_parameters, UNSIGNED32, 0x01, 0x1011, 0x0,
       
        &producer_heartbeat_time, UNSIGNED16, 0x00, 0x1017, 0x0,
       
        // End coms section

        &encoder[0]->counts, INTEGER32, 0x01, 0x2000, RW,
        &encoder[0]->high_limit, INTEGER32, 0x02, 0x2000, RW,
        &encoder[0]->low_limit, INTEGER32, 0x03, 0x2000, RW,
        &encoder_command[0], UNSIGNED64, 0x04, 0x2000, RW,
       
        &encoder[1]->counts, INTEGER32, 0x01, 0x2001, RW,
        &encoder[1]->high_limit, INTEGER32, 0x02, 0x2001, RW,
        &encoder[1]->low_limit, INTEGER32, 0x03, 0x2001, RW,
        &encoder_command[1], UNSIGNED64, 0x04, 0x2001, RW,
       
        &short_press, UNSIGNED16, 01, 0x2010, RW,       
        &long_press, UNSIGNED16, 02, 0x2010, RW, 
    };
   
   
   io_board_1_class(device_type deviceType, can_open_class* new_bus, uint32_t new_node_n,
        encoder_class * encoder0ptr, encoder_class* encoder1ptr );
   
    void interrupt_routine(void) ;
   
    void encoder_config_update(void) ;
   
    void encoder_config(uint32_t n);

};
 

Online negativ3

  • Frequent Contributor
  • **
  • Posts: 618
  • Country: th
Re: Setting up C++ on a microcontroller
« Reply #35 on: February 11, 2026, 05:32:02 pm »
Initialize the object array in the constructor (after setting the pointers):

io_board_1_class::io_board_1_class(...) {
    encoder[0] = encoder0ptr;
    encoder[1] = encoder1ptr;
   
    // Now initialize object array
    object[...] = {&encoder[0]->counts, INTEGER32, 0x01, 0x2000, RW};
    // etc.
}
 

Online ejeffrey

  • Super Contributor
  • ***
  • Posts: 4852
  • Country: us
Re: Setting up C++ on a microcontroller
« Reply #36 on: February 11, 2026, 05:54:13 pm »
Quote
This is the class, the contention is now over " encoder_class * encoder[2]" is this not the correct way to setup an array of pointers to class type objects? or have I accidentally created 2 class instances ?

That's correct, the problem is your initializer:

Code: [Select]

class io_bard_1_class {
...
encoder_class * encoder[2];
...
od_type object[COMMS_OD_SIZE + 10u] = {
...
        &encoder[0]->counts, INTEGER32, 0x01, 0x2000, RW,
        &encoder[0]->high_limit, INTEGER32, 0x02, 0x2000, RW,
        &encoder[0]->low_limit, INTEGER32, 0x03, 0x2000, RW,
        &encoder_command[0], UNSIGNED64, 0x04, 0x2000, RW,
...
}
}

This initialization will be executed _before_ the constructor body is entered, and so before the encoder member variable has been initialized.  You need to move it until after the encoder member variable has been set, or initialize it directly from the constructor parameter.  If you are using the in-class initalization syntax because you want this large initializer construct to be shared between multiple constructors, consider putting it in a

Initialization is a relatively complicated part of C++ given that it's something that happens so often, there are multiple ways to initialize variables and member variables, and keeping track of what order they happen in can be admittedly confusing.  In general it is optimal if you can arrange it so that each member can be initialized independently so that it's order independent.

The C++ core guidelines have some guidelines here:

https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#rc-order

C.47: Define and initialize data members in the order of member declaration
C.48: Prefer default member initializers to member initializers in constructors for constant initializers
C.51: Use delegating constructors to represent common actions for all constructors of a class
 

Offline hans

  • Super Contributor
  • ***
  • Posts: 1997
  • Country: 00
Re: Setting up C++ on a microcontroller
« Reply #37 on: February 11, 2026, 07:09:34 pm »
... or should I ditch the C stuff and just have it treated as C++?

I would never attempt to use two different compilers to compile sections and then link them into to a single program. It can be done, but there are some gotchas (which are beyond my level of expertise). But a C++ compiler can handle C code perfectly well, so there is no need to make it more complicated.

And C++ is a different language. You may get different (and probably more) warnings and error messages from the compiler.
Yes... C++ and C undefined behaviour are not the same.

For example; in C++, if you forget a return statement from a non-void function, thats UB and can remove said code:
https://godbolt.org/z/KrE4obaT6

As you can see, in C++ the program "hangs" in execution. Of course, not returning data when the type is non-void is also not pretty code in C, but as C is a language with dementia (oh those lovely implicit type casts!), this was deemed to be OK behaviour.

Unfortunately I forgot a scenario where C and C++ would compile a seemingly simple program (<10 LOC), with legal code, but show different computed results. Anyhow, these languages are not simply dialects anymore.. but should be treated individually.

Trying to compile with just C++ results in a slew of errors on the startup file along the lines of:
error: 'void CAN1_Handler()' aliased to undefined symbol 'Dummy_Handler'

This file is not enclosed with ifdef C++ extern C.

So it looks like I may as well keep compilation to the language of each file. All files in the DFP pack from microchip are *.c and *.h. All my C++ files are *.cpp and *.hpp

Presumably, that Dummy_Handler is maybe in a assembler startup file? Is there also a function prototype enclosed in extern "C"? Often this is enough to bump the C++ part of a program to use the C variant of that function, and not look for the C++ mangled naming.
If you use -flto you may also want to add __attribute__((used)) in there.
 

Offline SimonTopic starter

  • Global Moderator
  • *****
  • Posts: 18911
  • Country: gb
  • Did that just blow up? No? might work after all !!
    • Simon's Electronics
Re: Setting up C++ on a microcontroller
« Reply #38 on: February 11, 2026, 08:35:09 pm »
Quote
This is the class, the contention is now over " encoder_class * encoder[2]" is this not the correct way to setup an array of pointers to class type objects? or have I accidentally created 2 class instances ?

That's correct, the problem is your initializer:

Code: [Select]

class io_bard_1_class {
...
encoder_class * encoder[2];
...
od_type object[COMMS_OD_SIZE + 10u] = {
...
        &encoder[0]->counts, INTEGER32, 0x01, 0x2000, RW,
        &encoder[0]->high_limit, INTEGER32, 0x02, 0x2000, RW,
        &encoder[0]->low_limit, INTEGER32, 0x03, 0x2000, RW,
        &encoder_command[0], UNSIGNED64, 0x04, 0x2000, RW,
...
}
}

This initialization will be executed _before_ the constructor body is entered, and so before the encoder member variable has been initialized.  You need to move it until after the encoder member variable has been set, or initialize it directly from the constructor parameter.  If you are using the in-class initalization syntax because you want this large initializer construct to be shared between multiple constructors, consider putting it in a

Initialization is a relatively complicated part of C++ given that it's something that happens so often, there are multiple ways to initialize variables and member variables, and keeping track of what order they happen in can be admittedly confusing.  In general it is optimal if you can arrange it so that each member can be initialized independently so that it's order independent.

The C++ core guidelines have some guidelines here:

https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#rc-order

C.47: Define and initialize data members in the order of member declaration
C.48: Prefer default member initializers to member initializers in constructors for constant initializers
C.51: Use delegating constructors to represent common actions for all constructors of a class


Ah, bollocks, yes I see what you mean. So I need to treat the encoder class objects the same as the button ones and not have anything to do with them in the io_board class.

My alternatives are to put the encoder class code into the io_board code or create objects of encoder type inside the io_board class which means I don't duplicate the code. I may as well put the button class objects into the io_board class as well.

One thing I will say is that I never got this level of detail from the compiler when using MPLABX. No wonder it just crashed in short order.
 

Offline SimonTopic starter

  • Global Moderator
  • *****
  • Posts: 18911
  • Country: gb
  • Did that just blow up? No? might work after all !!
    • Simon's Electronics
Re: Setting up C++ on a microcontroller
« Reply #39 on: February 11, 2026, 08:38:35 pm »
... or should I ditch the C stuff and just have it treated as C++?

I would never attempt to use two different compilers to compile sections and then link them into to a single program. It can be done, but there are some gotchas (which are beyond my level of expertise). But a C++ compiler can handle C code perfectly well, so there is no need to make it more complicated.

And C++ is a different language. You may get different (and probably more) warnings and error messages from the compiler.
Yes... C++ and C undefined behaviour are not the same.

For example; in C++, if you forget a return statement from a non-void function, thats UB and can remove said code:
https://godbolt.org/z/KrE4obaT6

As you can see, in C++ the program "hangs" in execution. Of course, not returning data when the type is non-void is also not pretty code in C, but as C is a language with dementia (oh those lovely implicit type casts!), this was deemed to be OK behaviour.

Unfortunately I forgot a scenario where C and C++ would compile a seemingly simple program (<10 LOC), with legal code, but show different computed results. Anyhow, these languages are not simply dialects anymore.. but should be treated individually.

Trying to compile with just C++ results in a slew of errors on the startup file along the lines of:
error: 'void CAN1_Handler()' aliased to undefined symbol 'Dummy_Handler'

This file is not enclosed with ifdef C++ extern C.

So it looks like I may as well keep compilation to the language of each file. All files in the DFP pack from microchip are *.c and *.h. All my C++ files are *.cpp and *.hpp

Presumably, that Dummy_Handler is maybe in a assembler startup file? Is there also a function prototype enclosed in extern "C"? Often this is enough to bump the C++ part of a program to use the C variant of that function, and not look for the C++ mangled naming.
If you use -flto you may also want to add __attribute__((used)) in there.

non of the start up file code is enclosed in extern C. So what should I do? return to the separate compilers or enclose all C code in extern C ?
 

Offline az1

  • Regular Contributor
  • *
  • Posts: 61
  • Country: us
Re: Setting up C++ on a microcontroller
« Reply #40 on: February 12, 2026, 01:30:04 am »
non of the start up file code is enclosed in extern C. So what should I do? return to the separate compilers or enclose all C code in extern C ?

If you're calling C code (or code that uses the C ABI) from C++ you need to wrap the declaration of the C code with an extern C block.  However extern C isn't valid C so that needs to be wrapped with the proper preprocessor directives when you use those declarations in C code.
 

Offline cv007

  • Super Contributor
  • ***
  • Posts: 1061
Re: Setting up C++ on a microcontroller
« Reply #41 on: February 12, 2026, 05:22:42 am »
Quote
return to the separate compilers or enclose all C code in extern C ?
You can base your compiler usage by source file naming (via makefile)- gcc for .c (C only), g++ for .cpp. Source code designed for a C compiler such as your startup file, its simpler to just let gcc do the compiling so no need to modify someone else's code. Normal header files provided to you will most likely have C++ in mind (#ifdef __cplusplus), so no changes needed there. Your own code will most likely want to be cpp files whether there is C++ inside or not, as you will want to remain in the mangled name world, and will end up using C++ features without realizing it.

Your startup file is going to be somewhat unique in that a function alias is used, and would require the use of a C++ mangled alias name if compiling with g++. Rather than do that and to allow for C source files to override these weak functions, these function names should remain in the normal C-style naming. Since this is the case for the startup file, and no C++ code is in it, the most logical thing to do is let gcc compile it.

Your C++ code that wants to override a weak vector entry will then have to declare the function as C-style naming-
extern "C" void CAN1_INT0_Handler(){ /*code*/ }


Quote
For example; in C++, if you forget a return statement from a non-void function, thats UB and can remove said code:
You would also be required to ignore the warnings produced. This is an example why using -Werror is a good idea.
« Last Edit: February 12, 2026, 07:05:48 am by cv007 »
 

Offline SimonTopic starter

  • Global Moderator
  • *****
  • Posts: 18911
  • Country: gb
  • Did that just blow up? No? might work after all !!
    • Simon's Electronics
Re: Setting up C++ on a microcontroller
« Reply #42 on: February 12, 2026, 08:41:30 am »
Copying some AI hallucinations without verifying what it does is a recipe for disaster.

To be clear, other than the compiler flags, all the code is mine! I've already spotted that copilot has absolutely no clue about what it is talking about or doing. It clearly spots patterns and tries to fit in with what it has seen other write but it has no notion of why it is doing what it is doing. It can be helpful when I am doing repetitive editing but I still have to go back and finish changing the bits it did not understand.
 

Offline Doctorandus_P

  • Super Contributor
  • ***
  • Posts: 5389
  • Country: nl
Re: Setting up C++ on a microcontroller
« Reply #43 on: February 12, 2026, 01:44:22 pm »
And how is you ai supposed to know which compiler flags are suitable for your project?
Sure, an ai can probably generate a makefile for you that can be used to compile source for an arm cortex X uC into a binary for your particular uC, but there is no guarantee at all that all those flags are what you want, or even work together.

To get some confidence, at least you'd have to look up what those flags do in the manual, and then decide whether they are correct or not.

But overall, I'd rather search for an example makefile written by a knowledgeable human and use that as a start point for my own projects.
 

Offline SimonTopic starter

  • Global Moderator
  • *****
  • Posts: 18911
  • Country: gb
  • Did that just blow up? No? might work after all !!
    • Simon's Electronics
Re: Setting up C++ on a microcontroller
« Reply #44 on: February 12, 2026, 01:58:46 pm »
And how is you ai supposed to know which compiler flags are suitable for your project?
Sure, an ai can probably generate a makefile for you that can be used to compile source for an arm cortex X uC into a binary for your particular uC, but there is no guarantee at all that all those flags are what you want, or even work together.

To get some confidence, at least you'd have to look up what those flags do in the manual, and then decide whether they are correct or not.

But overall, I'd rather search for an example makefile written by a knowledgeable human and use that as a start point for my own projects.

I think all it really did was duplicate the existing C flags and call them CXX flags. I posted the make file and I have queried one of the flags I can't even find any reference to but it seems to have an effect on the resulting elf file.
 

Online ejeffrey

  • Super Contributor
  • ***
  • Posts: 4852
  • Country: us
Re: Setting up C++ on a microcontroller
« Reply #45 on: February 12, 2026, 04:06:36 pm »
non of the start up file code is enclosed in extern C. So what should I do? return to the separate compilers or enclose all C code in extern C ?

I would generally recommend using separate compilers.  It's not really a problem.  Here is what you need to do:

1) use g++ for .cpp files and gcc for .c files
2) use g++ to invoke the linker (gcc will work too, just don't call ld directly)
3) all C header files should be enclosed with #ifdef cplusplus extern "C" {

Alternately since it's all your code, you can instead convert everything to "C++" even if not using any of the c++ specific features.  To do that:

1) rename all .c files to .cpp
2) do not use extern "C" anywhere (*)
3) compile everythig with g++
4) fix any compiler issues due to the differences between C and C++

(*) You may still need to use extern "C" on specific things that interact with the runtime.  For instance, depending on how it's set up, interrupt handlers may need to be declared extern C.
 

Offline SimonTopic starter

  • Global Moderator
  • *****
  • Posts: 18911
  • Country: gb
  • Did that just blow up? No? might work after all !!
    • Simon's Electronics
Re: Setting up C++ on a microcontroller
« Reply #46 on: February 12, 2026, 06:08:59 pm »
All header files are in C with extern C in them. I'd have to take them out and rearrange all of the struct definitions for C++. This is pointless.

The only strictly C file is the start up and I don't know if it is a good idea messing with that.
 

Offline gpr

  • Contributor
  • Posts: 43
  • Country: gb
Re: Setting up C++ on a microcontroller
« Reply #47 on: February 12, 2026, 09:12:35 pm »
All header files are in C with extern C in them. I'd have to take them out and rearrange all of the struct definitions for C++. This is pointless.

The only strictly C file is the start up and I don't know if it is a good idea messing with that.

You can add one define at the beginning and at the end of .h files to make them both C and C++ compatible.

Like this:
https://github.com/STMicroelectronics/cmsis-core/blob/b7487de1303e2eb77c402432e368691e9dcfb5f0/Include/core_cm85.h#L38-L40
https://github.com/STMicroelectronics/cmsis-core/blob/b7487de1303e2eb77c402432e368691e9dcfb5f0/Include/core_cm85.h#L4666-L4668

 

Offline az1

  • Regular Contributor
  • *
  • Posts: 61
  • Country: us
Re: Setting up C++ on a microcontroller
« Reply #48 on: February 13, 2026, 12:07:17 am »
Quote
that means you need to be sure to invoke the C++ compiler for linking.
Invoking gcc for the link produces the same results as g++ for me, example below. I can also do the same on bigger projects and nothing changes.

That's because you're not statically initializing anything or using anything from the C++ standard library.  The latter isn't something you're likely to encounter on an embedded project, but the former is and whether or not the "C" runtime will call C++ constructors depends on the compiler.  The most predictable thing to do is just use the C++ compiler to link your C++ projects.
 

Offline cv007

  • Super Contributor
  • ***
  • Posts: 1061
Re: Setting up C++ on a microcontroller
« Reply #49 on: February 13, 2026, 12:59:36 am »
I do the link with g++, but do not know what additional information g++ is providing over gcc. The -v output is identical. The object file seems to have all the required info- it will produce .data/.bss info as needed, will create _GLOBAL__sub_i_ functions in the .text section (which the linker I assume because of naming will see it as needing to add to .init_array).

So I make some changes to the previous example to use static vars from the class. As long as I init the .data section, because that's where these static vars get initialized, this also works when linking via gcc.
Code: [Select]
//main.cpp- turn on led on PA5 using minimal code, but make global constructor do the work
using u32 = unsigned int; using u8 = unsigned char;
extern "C" void __libc_init_array();
//symbols from linker script
extern "C" u32 _etext       []; //end of text
extern "C" u8  _srelocate   []; //data (initialized)
extern "C" u8  _erelocate   [];

[[ using gnu : section(".vectors"), used ]]
struct { u32 stack; void(*vfunc)(); } flashVector { 0x20002000, __libc_init_array };

struct PA5 {
    u32& regMode{ *reinterpret_cast<u32*>(0x5000'0000) };
    u32& regBssr{ *reinterpret_cast<u32*>(0x5000'0018) };
    u32& rccIopenr{ *reinterpret_cast<u32*>(0x4002'1034) };
    static u32 a,b,c;
    PA5 (){
    __builtin_memcpy( _srelocate, _etext, _erelocate - _srelocate ); //init .data section
    rccIopenr = a; regMode = b; regBssr = c; while(1){} }
    };
PA5 pa5;
u32 PA5::a{1};
u32 PA5::b{1<<10};
u32 PA5::c{1<<5};

From C++17 on, I create class static vars 'static inline' as most of my code is in headers. In the above example, those static vars if done inline would have optimized away the need for the .data section, which is why I defined them outside the class for this example.

I also use the C++ standard library, although only to a point where the impact on size is minimal- chrono, array, string_view to name just a few.

I am not saying the use of g++ to link is never required, but have not seen it myself. If a simple example could be provided that can show a need to use g++, then I could see what additional information g++ is providing to the linker over what gcc is providing.

edit-
On a pc, I can also link via gcc (a simple pc app compiled with g++ which uses iostream, which needs stdc++) but by default gcc is not passing -lstdc++ to the linker so manually passing in this option is required when using gcc- which then works ok because now the needed symbols can be resolved as they are in stdc++.  In this case I am using all defaults in the compile- g++ -c main.cpp, and for the link- gcc -lstdc++ main.o, and can see the difference in what gcc/g++ is passing to ld via the -v option.

It still appears to me the linker does not care whether the objects files are C or C++, as its doing the same job it always does in both cases. Using gcc/g++ to get options to the linker is of course wanted, but that appears to be all they are doing- essentially providing command line options to the linker. I would assume, although not much fun, you could also run ld directly given the correct options, and gcc/g++ would have no involvement. It may be more of advantage on the pc to use g++ to link, and there is no reason not to in any case, but on the mcu if you mistakenly used gcc to link you will most likely never know the difference, and there will be no difference (and just like C, if you didn't give the linker enough info to resolve all symbols the link will fail).
« Last Edit: February 13, 2026, 05:37:41 am by cv007 »
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf

 

- - -
-->