Нюансы оптимизации

При настройке таймеров на STM32F4 , внезапно на ровном месте столкнулся со странным глюком - вроде меняешь код на пару строк, никак не связанных с настройкой ШИМ - а фаза ШИМ одного из каналов таймера инвертируется. Еще пару строк поменял - инвертировалось обратно… Что за фигня?
Долго вылавливал часть кода, которая дает этот эффект, пока не сократил код до чуть более 70 строк:

Смотреть код
const timer_dev* MAIN_TIMER = TIMER4;
const timer_dev* OE_TIMER = TIMER3;
uint8_t ADD_NUM = 228;
uint8_t TIM3_PERIOD = 8;
uint8_t oe_channel;
uint8_t pin_DMD_nOE = PB0;
uint8_t pin_CLK = PA6;

// ===============================================
void initialize_timers() {

// setup MAIN TIMER (Master)
  timer_init(MAIN_TIMER);
  timer_pause(MAIN_TIMER);
  timer_set_prescaler(MAIN_TIMER, 0);
  timer_set_reload(MAIN_TIMER, (TIM3_PERIOD * ADD_NUM + 4));
// Set master mode  
  (MAIN_TIMER->regs).gen->SMCR = TIMER_SMCR_MSM;  
// use CC1 as trigger output to control slave  
  (MAIN_TIMER->regs).gen->CR2 = TIMER_CR2_MMS_COMPARE_OC1REF;         
  timer_oc_set_mode(MAIN_TIMER, 1, TIMER_OC_MODE_PWM_1, 0);
// CC1 to control slave on/off 
  timer_set_compare(MAIN_TIMER, 1, TIM3_PERIOD);     

// Setup OE Timer (slave)
  timer_init(OE_TIMER);
  timer_pause(OE_TIMER);
  timer_set_prescaler(OE_TIMER, 0);
  timer_set_reload(OE_TIMER, TIM3_PERIOD - 1);

// connect slave OE_TIMER to Master
  (OE_TIMER->regs).gen->SMCR = TIMER_SMCR_TS_ITR3 | TIMER_SMCR_SMS_GATED;

// set PWM mode for output
  timer_oc_set_mode(OE_TIMER, oe_channel, TIMER_OC_MODE_PWM_1, 0);
  timer_cc_enable(OE_TIMER, oe_channel);
  timer_cc_set_pol(OE_TIMER, oe_channel, 0);
  timer_set_compare(OE_TIMER, oe_channel, TIM3_PERIOD / 2);

// ==== Start timers  
  timer_set_count(OE_TIMER, 0);
  timer_set_count(MAIN_TIMER, 0);
  timer_resume(OE_TIMER);
  timer_resume(MAIN_TIMER);
}
// ===============================================
void setup() {
// define timer CH for nOE pin
  oe_channel = timer_map[pin_DMD_nOE].channel;
  pinMode(pin_DMD_nOE, PWM);
 
// setup pin for time marks  
  pinMode(pin_CLK, OUTPUT);
// ==== 1st time mark  
  digitalWrite(pin_CLK, HIGH);
  digitalWrite(pin_CLK, LOW);
  
// setup and start the timers 
  initialize_timers();

  delayMicroseconds(200);

// stop OE generation by set CC1 of Main timer = 0
  timer_set_compare(MAIN_TIMER, 1, 0);
  delayMicroseconds(20);

// ==== 2nd time mark    
  digitalWrite(pin_CLK, HIGH);
  digitalWrite(pin_CLK, LOW);

// ++++++ problem lines +++++
//  timer_resume(MAIN_TIMER);
//  timer_resume(OE_TIMER);
}
// ===============================================
void loop() { }

В детали настройки таймеров в процедуре initialize_timers() вникать не обязательно. Кратко суть в том, что мы настраиваем два таймера - Master (MAIN_TIMER) и slave (OE_TIMER) таким образом, чтобы высокий уровень сигнала на ШИМ выходе Мастер таймера разрешал работу слейва.

Все нехитрое действие происходит в setup(). В строке 58 мы запускаем таймеры и разрешаем работу слейв таймера, потом ждем 200 мкс и в строке 64 запрещаем генерацию сигнала для слейва.
Еще я добавил вывод коротких импульсов на свободный пин, чтобы они служили временными метками на осцилограмме и показывали начало и конец процесса.
Правильная картинка логик-анализаторе выглядит так:

Красные пики - сигналы слейв-таймера, зеленые - отметки начала и конца.
Но стоит раскомментировать строчки 72 и 73, картина меняется:

Обратите внимание, что в программе “нехорошие” строки стоят только ПОСЛЕ второй временной отметки. А влияют на фазу ШИМ с самого начала!
Причем по хорошему эти команды, вообще-то, никак на ситуацию влиять не должны. Функция timer_resume() в ядре STM32F4 всего лишь выставляет в контрольном регистре таймера бит, разрещающий его работу. Но к моменту 72 строки этот бит в обоих таймерах и так выставлен, поскольку я их не останавливал.

Честно говоря, я потратил много вечеров, вылавливая эту бяку. Причин я ее так и не знаю, но заметил, что этот баг наблюдается только при сборке с опцией оптимизации -Os (то есть при оптимизации по размеру). Если включить оптимизацию -O3 - баг пропадает и код одинаково работает и со строками 72-73 и без них, выдавая на ЛА первую картинку.

Подозреваю, это как-то связано с тем, что функция timer_resume() определена как inline:

/**
 * @brief Start a timer's counter.
 *
 * Does not affect the timer's mode or other settings.
 */
static inline void timer_resume(const timer_dev *dev) {
    *bb_perip(&(dev->regs).bas->CR1, TIMER_CR1_CEN_BIT) = 1;
}

Смотреть файл Arduino_STM32/blob/master/STM32F4/cores/maple/libmaple/timer.h

Стараясь не допустить дублирования кода, оптимизатор, похоже, связывает вызовы этой функции в строчках 72-73 с таким же вызовом в момент инициализации таймеров в строках 43-44 - поэтому мы и видим, что добавление строк в конце программы портит работу таймеров с самого старта.
Но это только предположение. Так ли это и что именно там портится - может кто подскажет?

1 лайк

Так может тогда отключить оптимизацию именно этих строк?

В проекте несколько десятков тысяч строк. И эта функция вызывается тоже десятки, если не сотни раз.
Кроме того, я сильно не уверен, что проблема в ней (или только в ней). В управлении таймерами почти все методы - inline, выключишь один - заглючит другой.

На мой взгляд, налицо какая-то ошибка проектирования пакета. Может в данном случае инлайн нельзя использовать?
Кто бы подсказал…

Хотя я понимаю, что для ответа нужно уж слишком плотное погружение в проблему. К сожалению, сделать пример совсем простым, чтобы было буквально 10 строк - мне не удалось. А значит, например, на Stack Overflow с этим вопросом не пойдешь :frowning:

ИМХО : Самое правильное в данной ситуации будет отказаться от фреймворка Ардуино.
Что то мне подсказывает, что там этих кривых “тонкостей” больше чем кажется.

1 лайк

Слишком глубоко копаете :slight_smile: Мне бы что попроще… например пакет Ардуино-СТМ32 поправить :slight_smile:

Отказаться от Ардуино не могу, по условиям задачи нет возможности.
Проект имеет смысл только в экосистеме Ардуино.

Ну тогда, как ты иногда пишешь, разбираться только тебе, что там в этом фремворке натворил писатель.
Я без наездов, понимаешь, датчик опросить и т.д. можно, специфичные задачи, тем более учитывая сложность смт, надо решать на более низком уровне.
Да думаю, сам ты все понимаешь.

#pragma Не_оптимизировать_тут
надо поискать в описании, когда-то попадалось подобное, для ПИКового компилятора, там их тыщщи были, всяких этих прагм - т.е. указаний компилятору.

а что будет, если вместо нехороших строк навставлять , например, нопов? Размер кода может быть влияет, опосредованно.

Потом, я бы еще скомпилировал прошивку с -O0 (проверил бы идею с инлайнами и пр. оптимизацией)

Если бы разбирался в их ассемблере, то наверно бы сравнил еще, что там кодогенератор генерирует. -S, что-ли, опция у GCC. Но я не разбираюсь.

А состояние регистров таймеров (ну, т.е. конфигурация их) сравнивалась? Когда хорошая картинка и когда плохая - прочитать регистры, посмотреть чо там на самом деле.

аналогично

я перепробовал столько вариантов этого кода, пока его сокращал, что точно могу сказать. что дело не в размере. Дело именно в наличии этих строк.

Причем им даже не обязательно выполнятся, главное чтобы они были в коде.
Если вставить в код такую конструкцию

 if (flag) {
// ++++++ problem lines +++++
  timer_resume(MAIN_TIMER);
  timer_resume(OE_TIMER);
}

где значение flag = false - то ШИМ уже испортится, несмотря на то. что строчки даже не запускаются в коде.
Именно этот факт навел меня на мысль. что тут дело в оптимизации, а не в реальном влиянии этих строк на регистры таймера.

PS Конечно же, значение flag не должно быть заведо false - иначе этот блок просто будет выброшен из программы.

Если твоя гипотеза про оптимизатор верна, то может, не обязательно, но может помчь такая штука:

 __attribute__((always_inline))

Идея такая: оптимизатор игнорит декрарацию “инлайн” ради экономии места и где-то в цепочке портится порядок запуска таймера. GCC аттрибут оптимизатор не сможет игнорировать… Насколько помню - не сможет, но наизусть GCC опции только “мой добрый коллега” ЕП знает - у него спроси. Я - только избранные места помню и то в пересказе ;).

1 лайк

Можно вовсе выключить все попытки оптимизации опцией компилятора -O0.

Разумеется. Я так понял что таки хочется пооптимизировать, тогда можно подёргаться с этими прагмами ИМХО.

Жизнь показала что сокращать код самое геморойное дело, лучше взять проц с памятью потолще, к слову.

не то чтобы хочется.
Просто выключение оптимизации - это очевидный костыль, необходимость в котором возникла из-за ошибок в коде - моем или библиотечном.
Вместо использования костыля хотелось бы найти причину.

В данном случае я не стеснен размером, текущий код занимает примерно 20% от доступного обьема программной памяти.
К тому же, сокращать код для меня - это не проблема, я это даже люблю.

спасибо, попробую