1

I have some code that I now wish to run inside a timer-based interrupt on a Teensy 3.6 microcontroller. The code accesses a [global] array of objects of a class. I have marked that array and all of the member variables as volatile, which I believe is the first step for correctly dealing with interrupts.

One of the member variables which I have marked volatile is an std::bitset and I would like to call it's non-volatile methods, which I can't do as

"passing 'volatile std::bitset<16u>' as 'this' argument discards qualifiers [-fpermissive]"

I think I could just duplicate the bitset library and switch everything to volatile, but I don't think that should be required, so I think there is either a better solution, or I am thinking about things incorrectly.

Please let me know what should be done.

These answers seem to recommend the use of volatile when accessing global variables in an ISR: C 'Volatile' keyword in ISR and multithreaded program?,

Why is volatile needed in C?,

What is the correct way of using C++ objects (and volatile) inside interrupt routines?,

Is volatile needed when variable is only read during interrupt

volatile keyword usage in ISR function in micro-controller programing

This is in addition to many external sources recommending the use. Maybe my original message was not clear or maybe my situation differs from these.

user4913118
  • 105
  • 1
  • 8
  • 4
    What are you hoping `volatile` should do? – πάντα ῥεῖ Aug 14 '19 at 17:30
  • 3
    You only need volatile when writing and reading to something like a pin and other things considered to have a side effect. – Guillaume Racicot Aug 14 '19 at 17:33
  • 1
    Expand on this "correctly dealing with interrupt" notion. I don't think `volatile` does what you think it does, or if it does, you're going to be SOL – Lightness Races in Orbit Aug 14 '19 at 17:33
  • Also worth noting that there's a reason C++ is uncommon on embedded :P (Or used to be, at least) – Lightness Races in Orbit Aug 14 '19 at 17:35
  • My understanding of volatile is to ensure the variables are not optimised. I don't know what kind of optimisations might occur, but from what I understand, the optimiser never assumes that a variable has been untouched. – user4913118 Aug 14 '19 at 17:37
  • 3
    Actually, C++ is becoming increasingly popular for embedded systems. See https://www.embedded.com/design/programming-languages-and-tools/4438660/Modern-C--in-embedded-systems---Part-1--Myth-and-Reality – Alecto Irene Perez Aug 14 '19 at 17:37
  • You only need to use `volatile` on variables where the hardware can change the value of the variable because the compiler can't know about that. This is what `volatile` is for, letting the compiler know this is a special variable that could be changed in a manner it doesn't know about. If the hardware cant change the value on you, then you don't need volatile. – NathanOliver Aug 14 '19 at 17:38
  • 1
    Be *very* careful with `volatile`. Most people don't understand what it *actually* does and what it *does* is rarely what they want. In almost all cases, use of `volatile` is a bug and there is something else you *actually* want to be using. Most importantly; `volatile` does *Not* mean "thread safe". It will also, effectively, disable your compilers optimizer. See also: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/volatile-considered-harmful.txt?h=v5.3-rc4&id=27641b953c54643acfd28fcd9ebbe03cdc724605 , https://en.cppreference.com/w/cpp/language/cv – Jesper Juhl Aug 14 '19 at 17:42
  • 1
    @light _"Also worth noting that there's a reason C++ is uncommon on embedded :P"_ I have to disagree. I've been using c++ with embedded programming for a long time and very successfully. There are good reasons if you simply know what you're doing. – πάντα ῥεῖ Aug 14 '19 at 18:00
  • @πάνταῥεῖ Me too, just saying it's more common to stick with C in that environment for various reasons. Although decreasingly so in the Arduino/Pi era. Outside of that sphere... still not so much. – Lightness Races in Orbit Aug 14 '19 at 18:01
  • 1
    @πάνταῥεῖ "know what you're doing" - That, right there, really is the clincher. Unfortunately it's so often ignored :( – Jesper Juhl Aug 14 '19 at 18:03
  • @light Well, that might be the difference of tinkering vs. doing serious software development and architectures. – πάντα ῥεῖ Aug 14 '19 at 18:03
  • @Jesper You'll need to grasp the whole picture 1st. – πάντα ῥεῖ Aug 14 '19 at 18:04
  • @πάνταῥεῖ Of course. – Jesper Juhl Aug 14 '19 at 18:05
  • @πάνταῥεῖ Yes, I think so - and if you're doing _serious_ embedded work a lot of the time it'll be critical and it will be C, even now. Despite: _(cont.)_ – Lightness Races in Orbit Aug 14 '19 at 18:06
  • @J.AntonioPerez I did say "used to be" ;) – Lightness Races in Orbit Aug 14 '19 at 18:06
  • @light Well, sometimes working on _sugar coating_ c, I agree thus far. – πάντα ῥεῖ Aug 14 '19 at 18:08

1 Answers1

7

You should not be setting everything to volatile. Volatile has a specific purpose, and that's to prevent the compiler from optimizing out reads and writes to memory. Let's look at a really simple example.

int regular_sum(int* ptr) {
    int a = *ptr;
    int b = *ptr;
    return a + b;
}
int volatile_sum(int volatile* ptr) {
    int a = *ptr;
    int b = *ptr;
    return a + b; 
}

When we look at the assembly, we see that in regular_sum, the compiler realizes you're dereferencing the same pointer twice, and it optimizes it to just one dereference. But in volatile_sum, the compiler inserts both dereferences:

regular_sum(int*):
        mov     eax, DWORD PTR [rdi]
        add     eax, eax
        ret
volatile_sum(int volatile*):
        mov     eax, DWORD PTR [rdi]
        mov     edx, DWORD PTR [rdi]
        add     eax, edx
        ret

Optimizations are good, and most of the time, you won't need to use volatile. If you're doing memory-mapped IO, or you're writing values to pins as though they were a pointer, that's where you use volatile. To reiterate what Nathan Oliver said,

You only need to use volatile on variables where the hardware can change the value of the variable because the compiler can't know about that. This is what volatile is for, letting the compiler know this is a special variable that could be changed in a manner it doesn't know about. If the hardware cant change the value on you, then you don't need volatile.

But if you're doing computations on an object, don't use volatile. Do the computations on a normal object, and then copy the result to your volatile pointer.

Volatile and Interrupt Service Routines.

It is appropriate to use volatile on global variables that might be modified by Interrupt Service Routines. That being said, volatile cannot be used with objects like std::bitset because std::bitset does not have support for volatile operations, and std::bitset is not trivially copyable.

In this regard, you have two options:

  • Use a container that contains volatile primitives (e.g, std::vector<volatile bool>
  • Write your own class, with support for volatile.

If you have a class that is trivially copyable, then you can do something like the following. First, we have to define functions to allow us to copy to and from volatile types:

template<class T>
T volatile_copy(T const volatile& source) {
    static_assert(std::is_trivially_copyable_v<T>, "Input must be trivially copyable");
    T dest;
    auto* dest_ptr = dynamic_cast<char*>(&dest);
    auto* source_ptr = dynamic_cast<char const volatile*>(&source);

    for(int i = 0; i < sizeof(T); i++) {
        dest_ptr[i] = source_ptr[i];
    }

    return dest;
}

template<class T>
void volatile_assign(T volatile& dest, T const& source) {
    static_assert(std::is_trivially_copyable_v<T>, "Input must be trivially copyable");
    auto* source_ptr = dynamic_cast<char*>(&source);
    auto* dest_ptr   = dynamic_cast<char volatile*>(&dest);

    for(int i = 0; i < sizeof(T); i++) {
        dest_ptr[i] = source_ptr[i];
    }
}

Then, we may write a class normally, and as long as it's trivially copyable we can create a copy from a volatile version:

struct MyBitset {
    uint64_t bits;
    // Logic

    void flip() {
        bits = ~bits;
    }
    void addOne() {
        bits++;
    }
};

volatile MyBitset flags;

void interrupt_handler() {
    auto local = volatile_copy(flags);

    // Do stuff to local

    volatile_assign(flags, local); 
};

We can also encapsulate this behavior in a class, so that we can "check out" volatile variables:

template<class T>
struct Checkout {
    T local;
    T volatile& source;
    Checkout(T volatile& source)
      : local(volatile_copy(source))
      , source(source) {}
    void save() {
        volatile_assign(source, local);
    }
    ~Checkout() {
        save();
    }
};

Using this allows us to create local copies of volatile variables, make modifications to them, and the result will be saved automatically:

volatile MyBitset flags;

void interrupt_handler() {
    auto f = Checkout(::flags);

    f.local.flip(); //We can call whatever member functions we want on the local

    // When the function exits, changes made to the local are automatically assigned to the volatile global
}
Alecto Irene Perez
  • 10,321
  • 23
  • 46
  • Pretty much everywhere else says that global variables accessed/written within an ISR need to be marked as volatile. I don't understand how my situation is different. – user4913118 Aug 14 '19 at 18:19
  • Can you specify: everywhere? I suspect you have some articles or books? – JVApen Aug 14 '19 at 18:28
  • @JVApen, https://learn.microsoft.com/en-us/cpp/cpp/volatile-cpp?view=vs-2019, https://barrgroup.com/Embedded-Systems/How-To/C-Volatile-Keyword as well as the SO answers linked in my original post. – user4913118 Aug 14 '19 at 18:34
  • `be aware that the C++11 ISO Standard volatile keyword is different and is supported in MSVC when the /volatile:iso compiler option is specified` -> Microsoft is by default not standard compliant – JVApen Aug 14 '19 at 18:38
  • That said, it doesn't state that you should make globals volatile. However, it does mention that the non-standard behavior works as atomics – JVApen Aug 14 '19 at 18:40
  • I haven't read the other article, it's from 2001, C++ has evolved a lot. What by accident happened by compilers back than, can be completely different now – JVApen Aug 14 '19 at 18:42
  • Check the compiler specs for how it handles volatile. Embedded systems often use compilers with extensions to support interrupts and hardware I/o. Much of this precedes Modern, C11 and later where the memory model was ad hoc with no idea about multi-threading. – doug Aug 14 '19 at 18:44
  • @JVApen The site for my specific microcontroller recommends it here: https://www.pjrc.com/teensy/interrupts.html – user4913118 Aug 14 '19 at 19:05
  • The only global it mentions are global interrupts – JVApen Aug 14 '19 at 19:12
  • @JVApen "Shared variables must be declared with the "volatile" keyword, which instructs the compiler to always access the variable. Without volatile, the compiler may apply optimizations which assume the variable can not change on its own." – user4913118 Aug 14 '19 at 19:17
  • Sounds like that should be `std::atomic` instead – JVApen Aug 14 '19 at 19:19
  • @JVApen std::atomic is not related to optimisation; it's to ensure that the type will always hold valid data and won't be read in the middle of a multi-byte write, from what I undestand. That's my next step. You seem to know a lot about this topic, so would you personally recommend not marking variables as volatile, even if they a are accessed from an ISR? – user4913118 Aug 14 '19 at 19:27
  • 1
    Volatile only makes sense when you have an address mapped to hardware to which to write, all other cases are bugs – JVApen Aug 14 '19 at 19:29
  • I’m the context of your problem, it seems appropriate to make global variables that are modified from an ISR as volatile. What I would *not* do is try to invoke complex member functions on a volatile variable. You should get better performance by creating a local copy of the volatile variable, modifying the local copy as needed, and then writing it back to memory – Alecto Irene Perez Aug 14 '19 at 19:31
  • @user4913118 I updated the answer with some proposals on how to make using volatile simpler for your code. – Alecto Irene Perez Aug 14 '19 at 20:14
  • @user4913118 The problem with marking a shared variable as `volatile` is that it does not work with threads. So only single threaded code with signal handlers can use volatile for "sharing" variables. The std::atomic is the modern version for sharing that works across threads and multiple CPU cores. So modern use of volatile is pretty much limited to accessing memory-mapped-IO registers on hardware, which must additionally be marked uncached by the system to avoid the synchronization issues between cores and to ensure a timely write and correct write. – Goswin von Brederlow Mar 04 '23 at 19:46