STM8 - interesting compiler non-optimisation - Page 1
Forum section: electronics / general

author: Saimoun
author_rank: Frequent Contributor
timestamp: October 31, 2020, 04:13:03 pm
content: Hi!Just wanted to share something I found rather interesting. I have a simple code on my STM8 uC where I store the EEPROM values in a struct.To optimise EEPROM usage (write cycles) I separated 16-bit values into two 8-bit values.Code:typedef struct EEPROM_Struct {__IO uint8_t param1HIGH;__IO uint8_t RESERVED1; // separate each value 4 bytes apart to get minimum amount of erase/write cycles__IO uint8_t param1LOW;__IO uint8_t RESERVED2;__IO uint8_t param2;}EEPROM_TypeDef;EEPROM EEPROM_TypeDef EEPROM_ACCESS = { // initial values10,// param1HIGH{0},// rsvd230,// param1LOW{0},// rsvd4// param2};Then to read param1 I wrote a simple code which I was hoping the compiler will understand and simply move each byte into where it should be:Code:uint16_t currentParam1 = 0; // global variable in RAMvoid main(void) {currentParam1 = (uint16_t)EEPROM_ACCESS.param1HIGH << 8 | (uint16_t)EEPROM_ACCESS.param1LOW;}I was expecting the compile to simply move the high byte into the high byte of currentParam1 and the low byte into the low byte. But it actually did the whole shifting and the OR operation!Code:353 0007 c60004lda,_EEPROM_ACCESS+4354 000a 97ldxl,a355 000b 1f01ldw(OFST-10,sp),x357 000d c60000lda,_EEPROM_ACCESS358 0010 5fclrwx359 0011 97ldxl,a360 0012 7b02lda,(OFST-9,sp)361 0014 01rrwax,a362 0015 1a01ora,(OFST-10,sp)363 0017 01rrwax,a364 0018 bf00ldw_currentParam1,xIt turns out that the problem were my (uint16_t) that prevented it from optimising correctly! If I change the main to:Code:void main(void) {currentParam1 = EEPROM_ACCESS.param1HIGH << 8 | EEPROM_ACCESS.param1LOW;}then I get more what I was expected:Code:353 0007 c60000lda,_EEPROM_ACCESS354 000a 97ldxl,a355 000b c60004lda,_EEPROM_ACCESS+4356 000e 02rlwax,a357 000f bf00ldw_currentParam1,xTricky!! A trap for young players like someone would say ha haSimonPS: I still did not get why it is not using simply two mov instructions - I guess mov memory to memory is quite slow?I can get it to do it by doing a strange workaround:Code:void main(void) {uint8_t *tmp = (uint8_t*)&currentParam1;tmp = EEPROM_ACCESS.param1HIGH; // STM8 is big-endiantmp = EEPROM_ACCESS.param1LOW;}Then I get:Code:365 0007 5500000000mov_currentParam1,_EEPROM_ACCESS368 000c 5500040001mov_currentParam1+1,_EEPROM_ACCESS+4

author: GromBeestje
author_rank: Frequent Contributor
timestamp: October 31, 2020, 05:37:59 pm
content: What compiler are you using? SDCC?

author: Saimoun
author_rank: Frequent Contributor
timestamp: October 31, 2020, 06:27:41 pm
content: Forgot to mention that yesI am using Cosmic compiler with the following options (in ST Visual Develop):Code:cxstm8 +warn +mods0 +debug -pxp +strict -pp -pc99 -l -iinc $(ToolsetIncOpts) -cl$(IntermPath) -co$(IntermPath) $(InputFile)

author: Nominal Animal
author_rank: Super Contributor
timestamp: October 31, 2020, 06:47:48 pm
content: You could rely on type punning via union, too. It was standardized in C11, but supported in practice by earlier C compilers as well:Code:/* TODO: Wrap the following in endianness detection for portable code */#define pack_u16 pack_u16bestatic inline uint16_t pack_u16be(const uint8_t high, const uint8_t low){union {uint16_t u16;uint8_t u8;} data = { .u8 = { high, low } };return data.u16;}static inline uint16_t pack_u16le(const uint8_t high, const uint8_t low){union {uint16_t u16;uint8_t u8;} data = { .u8 = { low, high } };return data.u16;}

author: Siwastaja
author_rank: Super Contributor
timestamp: November 01, 2020, 01:12:55 pm
content: The fields are defined __IO, if you grep for the headers you'll find __IO is defined as a short-hand forvolatile(+ possibly something else).This is highly important; usually you want the IO port operations to happenexactly as you write them, not to be combined. This is significant with many peripherals: for example, a status register readout may clear flags, and you want to do that exactly once. Or reading a data register pops one from the fifo. You want to do that exactly as many times as you have written it.It appears your particular use case would be fine with more optimized behavior. You still can't redefine this particular IO port without the volatile qualifier, because then the compiler might optimizetoo much, i.e., not just combine the two operations, but also perform the operation at a different place altogether (so that it has been performed when the function returns!)The solution is indeed to type-pun via union, giving all possible access patterns you would like to use, yet still keep the volatile qualifier. If you don't want to modify the headers, you can also just type cast.Often you need to write out read-modify-write explicitly, which isn't a bad thing:Code:volatile type_t volatile_thing;type_t tmp = volatile_thing;tmp |= 0x123;tmp += 42;tmp *= 3;volatile_thing = tmp;this code reads and writes volatile_thing once, and only once, yet you can do multiple operations on it and let compiler optimisethose, because the tmp variableisn'tvolatile.