CH32V003 does not exit while loop? - Page 1
Forum section: electronics / general

author: cubeflake
author_rank: Contributor
timestamp: July 10, 2024, 06:20:00 pm
content: Hi everyone,I am having a very strange problem. I must be doing something very stupid, but I'm not seeing it.I'm using the CH32V003 on a project, and in general everything seems to be working completely fine. I have already written some code on it, debugger works totally fine, no problems.. until..I am using the SysTick timer in order to make a delay_ms() function. I've set the SysTick counter to count every ms. I have checked that the timer does indeed run at this speed (by toggling a pin).Now, I have made a function that looks like this:Code:void delay_ms(u32 ms){u32 start = uwTick;// we do +1 because it's possible that the tick increments right after// entering this functionu32 end = start + ms + 1;while (uwTick < end){// wait}return;}The weird thing is that the micro stays in the while loop. When I check what's going on with the debugger, I see thatuwTickis larger thanend. Nevertheless, the micro will not exit the while loop and will just wait forever.Any ideas as to what is going on?Jim

author: cubeflake
author_rank: Contributor
timestamp: July 10, 2024, 06:29:56 pm
content: Found it! I had to declareuwTickas volatile. Still don't understand why, because I sawuwTickincreasing while in the loop...

author: pcprogrammer
author_rank: Super Contributor
timestamp: July 10, 2024, 07:03:52 pm
content: To understand why you should check out the assembler generated by the compiler for both options. With and without the volatile. It might show that with the volatile on it the code reloads the value from memory for every while iteration and without it loads the register on startup of the function and never reloads it from memory again.

author: cv007
author_rank: Frequent Contributor
timestamp: July 10, 2024, 07:46:55 pm
content: You should redo the function to handle overflow of the counter (where tick + ms overflows a 32bit value). You can work out why by using high systick values which will cause your 'end' value to overflow and your delay returning without delay.Code://0xFFFFFFFF = systick now//ms = 10void delay_ms(u32 ms){u32 start = uwTick; //0xFFFFFFFFwhile( (uwTick - start) < ms ){//0xFFFFFFFF - 0xFFFFFFFF = 0, 0<10 ? true//0x00000000 - 0xFFFFFFFF = 1, 1<10 ? true//to//0x00000009 - 0xFFFFFFFF = 10, 10 < 10 ? false//can do low power while waiting here if wanted}}

author: HwAoRrDk
author_rank: Super Contributor
timestamp: July 10, 2024, 10:09:18 pm
content: To expand on what pcprogrammer said, or to put it in other words:Because uwTick is non-volatile, the compiler is assuming that its value won't change during execution of the while loop. So therefore it is generating machine code that grabs the value of uwTick once upon entry to the loop (or even maybe at start of function), and then compares that same value every time to the value of end. And of course if the former is less than the latter once, it will be every time, and you get stuck in an infinite loop.Also, when you're looking at uwTick in the debugger, it appears to change value because the debugger is querying that location in memory directly, rather than whatever value is being cached in a CPU register during execution of the delay_ms function.

author: DavidAlfa
author_rank: Super Contributor
timestamp: July 10, 2024, 10:44:51 pm
content: Quote from: cubeflake on July 10, 2024, 06:29:56 pmBut the compiler isn't aware about this, that variable will never change in that loop.Declaring it as volatile tells the compiler "This value might change unexpectedly elsewhere, anytime".

author: SiliconWizard
author_rank: Super Contributor
timestamp: July 10, 2024, 10:56:55 pm
content: A very common issue that bites many embedded developers.The reason is that from a program flow perspective, the compiler has no means of knowing that this variable is actually changing, so it optimizes out reading it. The reason behind it is that, if said variable is only modified inside an ISR, the compiler basically has no means of knowing the ISR function ever gets called, as it's not part of the normal program flow from its perspective.The volatile qualifier instructs the compiler never to optimize out neither a write nor a read for the qualified object. So, it honors all of them verbatim.One may think that the compiler may be smart enough to detect code that potentially interacts concurrently, but C doesn't define anything like this, and always favors performance, generally speaking.If C compilers handled all variables by default as "volatile" - which is something that I bet many would expect - C compiled code would end up very inefficient.

author: cubeflake
author_rank: Contributor
timestamp: July 11, 2024, 11:37:25 am
content: That makes a lot of sense, thanks!

author: Tation
author_rank: Regular Contributor
timestamp: July 11, 2024, 12:02:53 pm
content: Cannot remember where, but once I read that missing 'volatile' qualifiers is 1st in the list of culprits for embedded systems misbehavior.

author: martinribelotta
author_rank: Regular Contributor
timestamp: July 11, 2024, 04:44:10 pm
content: I prefer to wrap the volatile keyword into an access macro and avoid the declaration of any volatile variable (in fact, the volatile is the access not the variable)This approach is the new recomended usage for the new C++ and others languages (like rust and zig) and, at least for me, prevent many missusages of volatile declarations (many juniors declare me an array as volatile when something goes wrong)https://godbolt.org/z/z1jYjczx8Code:#define VLOAD(type, rvalue) (*((const volatile type *) (&(rvalue))))#define VSTORE(type, lvalue, expr) do { *((volatile type *) (&(lvalue))) = (expr); } while(0)typedef unsigned int u32;u32 tick_counter;void increment_tick(void){VSTORE(u32, tick_counter, VLOAD(u32, tick_counter) + 1);}void delay_tick(u32 ticks){u32 start_tick = VLOAD(u32, tick_counter);while ((VLOAD(u32, tick_counter) - start_tick) < ticks) {}}If the code avoid macros, the other trick is to wrap the volatile access as anstatic inlinefunction like this:https://godbolt.org/z/P7ansfzbaCode:typedef unsigned int u32;static inline u32 vload_u32(const u32 *ptr){return *((volatile u32*) ptr);}static inline void vstore_u32(u32 *ptr, u32 val){*((volatile u32*) ptr) = val;}u32 tick_counter;void increment_tick(void){vstore_u32(&tick_counter, vload_u32(&tick_counter) + 1);}void delay_tick(u32 ticks){u32 start_tick = vload_u32(&tick_counter);while ((vload_u32(&tick_counter) - start_tick) < ticks) {}}Many people say that atomic operations are equivalents but in some cases I found bugs or missunderstands of atomic access usages and I found this approach less prone to errors.

author: mikeselectricstuff
author_rank: Super Contributor
timestamp: July 11, 2024, 06:05:07 pm
content: The classic symptom of a variable not being declared volatile when it should be, is if the problem goes away if optimisation level is set to minimum - this can be a useful quick diagnostic when looking for strange problems.QuoteWhy is that better than declaring volatile variables correctly?

author: martinribelotta
author_rank: Regular Contributor
timestamp: July 12, 2024, 03:33:53 pm
content: QuoteBecause is more expresive... when you have a complex expression accessing a volatile variable, the number of load/store will explode. Obviously, it's a human bug for missunderstad of the problem but for a team of developers with junior experience, is a big win when your API prevent bad usages.If you force to access the volatile variables as a load/store semantic, your code is expresive about the memory access, when you put a volatile variable in a complex expression, is not evident when the compiler emit a load or store. In the most cases, the intention is perform a load-modify-store operationBy example, the next code (really bad wirten) perform a 3 load operations in diferent times. This is valid usage of volatile variable but may not obtain the correct result:Code:volatile unsigned int counter = 0;void f(int i){counter = (counter + i) * 2 - (counter >> 1) + (i << 2) / counter;}When you make explicit the load/store of your volatile variable (preferible with a sematic wrapper likeint load_my_awesome_counter(void);) the code screams at you what it does.

author: mikeselectricstuff
author_rank: Super Contributor
timestamp: July 12, 2024, 03:42:51 pm
content: Quote from: martinribelotta on July 12, 2024, 03:33:53 pmIf you're using complex expressions in an interrupt routine, you're probably doing it wrong.The developers still need to understand how to use the load/store where appropriate, in the same way they need to know where to declare variables volatile.

author: DavidAlfa
author_rank: Super Contributor
timestamp: July 12, 2024, 04:00:21 pm
content: But it's bad programming practice then.If you know a variable can change anytime, you should copy it and work with the buffered value.Quote from: martinribelotta on July 12, 2024, 03:33:53 pmCode:volatile unsigned int counter = 0;void f(int i){unsigned int t = counter;counter = (t + i) * 2 - (t >> 1) + (i << 2) / t;}

author: cv007
author_rank: Frequent Contributor
timestamp: July 12, 2024, 04:54:00 pm
content: QuoteI doubt anyone in the C++ world is going to recommend macros/defines. The language itself has better options and you eliminate the obfuscation of the preprocessor doing find/replace on your code. I create no defines at all, unless I'm pushed into a corner where there is no other good choice in the language such as compile time string creationIf C++ is in use, you hide the details in a class, where the class can easily handle the little details such as volatile requirements, the need to interrupt protect, keeping things private outsiders have no need to access, etc.In this example, the Systick class can easily handle dealing with the volatile and any other details without extra help, and the user of this class only has to know what public functions are available. No 'outsider' is accessing the volatile var directly so the class creator (you) can easily understand there is no irq protection needed for the increment/write or for a read-https://godbolt.org/z/ovqaveEGTYou will have to understand any need for volatile and atomic protection in any case, so those 'helper macros' do nothing but add an extra layer which you still have to remember to use- if you use them you already know what is needed and can just use a volatile var instead and skip the macros-https://godbolt.org/z/GdPnYa8aPand as already stated, if you do not want multiple reads due to the volatile, you simply make a copy to work with.

author: martinribelotta
author_rank: Regular Contributor
timestamp: July 12, 2024, 05:27:37 pm
content: QuoteYes, all about it is how to deal with bad programmers/bad use cases for your api

author: martinribelotta
author_rank: Regular Contributor
timestamp: July 12, 2024, 05:46:05 pm
content: QuoteC++ is other lang/other entire world... the approach of C++ is deprecate volatile (although there is a proposal to de-deprecate volatile on c++26:https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2866r0.pdf)In a language with templates, the prefered way will be like it:Code:template<typename T>inline T volatile_load(const T& v){return *reinterpret_cast<const volatile T*>(&v);}template<typename T>inline void volatile_store(T& v, const T& val){*reinterpret_cast<volatile T*>(&v) = val;}https://godbolt.org/z/rnhEeGz4dQuoteSure, the abstraction is the big tool here... but classes or other tricks is only tools, if you use compiltion units and external functions with struct as first parameter is the same level of abstraction...The big cuestion is what is the best way to deal with a moron users for your api...Obviously, the tick counter problem is a oversimplificated volatile usage example and the simple way is aceptable for it... but when you have hardware sincronization (like DMA pointers or flags) the usage of volatile will be turn very tricky

author: martinribelotta
author_rank: Regular Contributor
timestamp: July 12, 2024, 06:01:03 pm
content: QuoteYes but... the usage of volatile is normally more common in non interrupt contextQuoteThe big problem here is how to deal with not-so-good developers and maintain the code robust... the usage of volatile variables, in my experience, probes to be a bad combination with unexperimented programmers, and I share my solutions (clearly, partials and not free of fails) for it.

author: cv007
author_rank: Frequent Contributor
timestamp: July 12, 2024, 06:03:58 pm
content: QuoteYes, they seem to have forgotten that microcontrollers are a thing. Though a compiler option will restore what they took away.QuoteSo instead of giving a var an attribute at creation time (one time), you are forced to revisit its need to be a volatile on every use. That is a recipe for mistakes of omission and it replaces something that really has no need to be replaced.edit-What they actually took away (for now, starting in C++20), is the ++, += type of things on volatile vars-ms_++;ms_ += 1;so you still have volatile, just not these specific operators which you can 'manually' write if you want-ms_ = ms_ + 1;

author: SiliconWizard
author_rank: Super Contributor
timestamp: July 12, 2024, 09:36:13 pm
content: Quote from: martinribelotta on July 11, 2024, 04:44:10 pmBoth can be defended, and both have their own merits. But at least with the declaration, you only have to do it once, and the "semantic" is attached to the variable, which usually makes more sense.Quote from: martinribelotta on July 11, 2024, 04:44:10 pmCan you point us to this "new recommended usage"?Also, I don't see how it would prevent anything, as if one forgets to declare some variable volatile when it should be, one can as easily forget to decorate the access "volatile" when it's needed. Developers are much more likely to be confused as to when they need to access some variable in this way, and when they don't, and if they're going to decorate *all* accesses to some variable like this, a single qualifier at a single point (volatile declaration) is way less prone to errors.

author: mikeselectricstuff
author_rank: Super Contributor
timestamp: July 12, 2024, 10:17:58 pm
content: Of course this is something that a compiler ought to be able to easily detect, and issue a warning if a variable is used in both an interrupt routine and a foreground task.

author: cv007
author_rank: Frequent Contributor
timestamp: July 13, 2024, 02:40:35 am
content: QuoteExcept for the mcu such as cortex-m's where there is nothing special about these interrupt functions, so unless they are given some kind of additional attribute the compiler will not know anything about interrupt functions. You also have the option to move the vector table to ram, populate it at runtime (which I do), and again the compiler will have no idea what is going on. In addition, you have multiple interrupt levels where you may spend all your working time inside these 'run levels' and your foreground task could essentially be doing nothing (at least as far as shared vars are concerned).The volatile is not a hard thing to figure out and every mcu user has to simply learn the when and why just like any other language specific feature. The need for atomic operations is also something that should be given some thought and is often forgotten. Getting the former wrong typically results is 'not working' (which is good and is usually the first step to figuring out volatile), forgetting about the latter usually results in problems that show up infrequently so you may be in for some troubleshooting time and eventually you realize atomic operations are a thing to consider. Both usually require a user to experience the problem firsthand, and after that one learns to consider these things when creating code.I also compile at -Os optimization from the start, so in addition to dealing with the same generated code and timing throughout the project, if I do forget the need for volatile I find about it when the code is created and not a month later when I decide its time to go -Os (and now have multiple things 'broke' that were working before).

author: mikeselectricstuff
author_rank: Super Contributor
timestamp: July 13, 2024, 08:54:43 am
content: Quote from: cv007 on July 13, 2024, 02:40:35 amBy "compiler" I meant the whole toochain. At some point it needs to know to do whatever is needed to make the processor treat a procedure as an interrupt, and in most cases this will be fairly straightforward,e.g. an attribute.Anyone doing more complex things like moving vector tables will likely be skilled enough to not benefit from the "this should be declared volatile" warning.

author: glenenglish
author_rank: Frequent Contributor
timestamp: July 13, 2024, 08:20:25 pm
content: I find that on some IDE/compilers, like AVR codevision, that all global variables are treated as volatile.As a result, each time the compiler manipulates a global variable, it loads it out of memory, performes some operation on or using it, and then, if modified, writes it back to memory. This results in the code producing (usually) needless read-modify-writes, and lots of code bloat and speed cost.Compared to most compilers, GCC etc, the volatile keyword is required to ensure the compiler makes no assumptions to the value of a variable.For the newbie -Without the volatile keyword, there is assumption by the compiler, that a variable is not being modified elsewhere, so when it loads a (global) variable from memory say at the start of a function, that it doesnt need to reload it to ensure it has the update to date version of it.the compiler has no idea if the variable you reference in some part of the program will be modified somewhere else . And this might happen when an interrupt handler is executed.If a variable may be modified outside a function , and if it is modified, if the behaviour of the function would be modified as a result ----, for example during an interrupt handler event might modify a counter variable , (while the 'base' function is being executed) , the use of the variable must be guarded by , usually, disabling interrupts around the use of the variable, so it cannot change while you are making decisions based on the value.The codevision read modify write method doesnt prevent this because an interrupt may be executed between the 'read - modify - write' steps. I must ask Pavel why he does this.Many use global variables when they need variable state persistence across function calls. The better method is to use static variables in functions. The compiler does not have to guess if it is being used outside the function because it's private (in a C sense).Golly I have realised that this is quite a subject, not the original two line reply I wanted to make !When working with hardware, where peripheral registers may change at any time, like say a UART serial buffer bit telling you there is more data, the definitions for a device in the header file will (likely) be using the volatile keyword to tell the compiler ' do not assume anything'.Volatile requirements around peripheral interaction, and interrupt handlers are always new traps for new embedded programmers.

author: ejeffrey
author_rank: Super Contributor
timestamp: July 13, 2024, 08:30:44 pm
content: Quote from: cv007 on July 12, 2024, 06:03:58 pmThey didn't. C++ continues to completely support volatile for memory mapped IO, which is the main purpose, as well as other used for volatile such as globals accessed by ISRs or async signal handlers.What they have deprecated or proposed to deprecate is mostly stuff that doesn't make sense or has unclear semantics. For instance volatile function parameters don't really make sense. Volatile compoind assignments like x += 1 are well defined but might be misinterpreted as an atomic read-modify-write, which is incorrect. Of course some people do use volatile compoind assignments in a way that is OK, so it's not free from breakage but easy to fix by writing x = x + 1.Ideally it would be best to avoid using volatile for ISRs and signal handlers. Atomics are much better in these applications: more efficient, easier to get correct, and can work correctly in multi core situations where volatile alone is not enough. More and more microcontrollers are becoming multi core so that's going to continue to be an issue. But atomics are not always a drop in replacement for volatile globals you may need to rework logic to transition. On top of that C11/C++11 atomics don't always actually translate to atomic operations, they will fall back on locks when in some situations. You can't use a lock in an ISR so you need to ensure that your platform supports lock free atomics for the types you need.

author: brucehoult
author_rank: Super Contributor
timestamp: July 14, 2024, 03:25:42 am
content: Quote from: ejeffrey on July 13, 2024, 08:30:44 pmISRs are inherently locked against code running on the same CPU core. The problem there is that the lock might be already held by some normal application code, and then you've painted yourself into a corner.If you think you need a lock in an ISR then that's a sign you're doing too much work in the ISR. You need to stash your data somewhere private and signal some non-ISR code to handle it and then return from the interrupt pronto -- that might be unblocking a thread, or it might be a global variable the main program polls.

author: ejeffrey
author_rank: Super Contributor
timestamp: July 14, 2024, 08:38:41 pm
content: Quote from: brucehoult on July 14, 2024, 03:25:42 amMy point was just that if you use C11 atomics in an ISR to communicate back to the main code you could end up with something that actually locks a mutex which will 99% of the time work but then deadlock of the ISR fires while the lock is held.The object size where this happens on any given platform is likely to be the same threshold where volatile variables stop being effectively atomic, so it's really pick your poison: deadlock or tearing. But it's part of why you can't just go blindly replace volatile variables with atomics.

author: mikeselectricstuff
author_rank: Super Contributor
timestamp: July 14, 2024, 09:16:55 pm
content: Quote from: glenenglish on July 13, 2024, 08:20:25 pmA reason that new embedded programmers may be unaware of the volatile keyword is that although it applies to most peripheral registers, they will usually never see it used, as it's buried, often multiple files deep, in the manufacturers' supplied header files.

author: SiliconWizard
author_rank: Super Contributor
timestamp: July 14, 2024, 10:18:42 pm
content: Quote from: mikeselectricstuff on July 14, 2024, 09:16:55 pmAnd the root reason, as I have often said, is that C is not taught/not learned properly anywhere, in a majority of cases. That's a major issue.

author: glenenglish
author_rank: Frequent Contributor
timestamp: July 14, 2024, 10:33:25 pm
content: and "not being taught C properly" is an excellent point.I imagine many do C++ in school then try to write embedded C... without the low level knowledge.I always feel my first 10 years in microcontroller/microprocessor programming being in 100% assembler with Rodney Zaks' book in hand, prepared me better than say, learning c++ writing programs on a PC then making C work on a embedded platform, and then learning assembler when they get short of cycles or need to write vector intrinsics.

author: SiliconWizard
author_rank: Super Contributor
timestamp: July 14, 2024, 11:31:27 pm
content: Yeah, from my experience, looks like most embedded C developers out there had learn some at uni, but really just the basics and not "learned" it as a programming language, as it should be. Then they just discover things as they go, in the professional world. Some will learn along the way, many will just apply some "recipes" and go on like this for years.One key reason is that C looks so simple that everyone assumes it's like the BASIC of current programming languages and doesn't need any real learning.Even if I don't buy it for a number of reasons that don't belong in this thread, I think that's one thing for which much more complex languages like Rust do better, regardless of their own technical merits: they just are too complex to be used without learning them. So, you're forced to learn, and that's a good thing.Languages that seem simple enough almost not to need learning are, in the end, a plague. The idea looks all nice - like, make programming more accessible to everyone - but the reality is harsh. They just promote bad habits, quick recipes and cargo cult programming. Just my opinion.

author: westfw
author_rank: Super Contributor
timestamp: July 15, 2024, 08:20:04 am
content: QuoteDoesn't have to be "complex." Check out this (very old: v0012) implementation of the Timer0 ISR for Arduino:Code:volatile unsigned long timer0_clock_cycles = 0;volatile unsigned long timer0_millis = 0;SIGNAL(SIG_OVERFLOW0){// timer 0 prescale factor is 64 and the timer overflows at 256timer0_clock_cycles += 64UL * 256UL;while (timer0_clock_cycles > clockCyclesPerMicrosecond() * 1000UL) {timer0_clock_cycles -= clockCyclesPerMicrosecond() * 1000UL;timer0_millis++;}}How many loads of timer0_clock_cycles does that do? (Each one being 4 slow, long instructions, on an AVR!)