Как компилятор C или препроцессор по-разному обрабатывают MACRO с аргументами?

Я работаю над кодом для микроконтроллера Atmel и использую ATMEL Studio.

Вы можете проверить набор инструментов и версию студии здесь.

*\Atmel\Studio\7.0\toolchain\avr8\avr8-gnu-toolchain\lib\gcc\avr\5.4.0*

У меня есть код в двух моих программах.

ДЕЛО 1:

#define USART_BAUD_RATE(BAUD_RATE) ((float)(5000000 * 64 / (16 * (float)BAUD_RATE)) + 0.5)
USART1.BAUD = (uint16_t)USART_BAUD_RATE(300);

СЛУЧАЙ_2:

#define USART_BAUD_RATE(BAUD_RATE) ((float)(5000000 * 64 / (16 * (float)BAUD_RATE)) + 0.5)
/* uint32_t my_BaudRate = 300; //I set this value in the program, normally it's 115200, 2400, but sometimes it can be 300*/
USART1.BAUD = (uint16_t)USART_BAUD_RATE(my_BaudRate );

Пример примера:

#include <stdio.h>
#include <stdint.h>

#define USART_BAUD_RATE(BAUD_RATE) ((float)(5000000 * 64 / (16 * (float)(BAUD_RATE))) + 0.5)

uint16_t getValue_1() {
    return (uint16_t)USART_BAUD_RATE(300);
}

uint16_t getValue_2(uint32_t inVal) {
    return (uint16_t)USART_BAUD_RATE(inVal);
}

int main()
{
    printf("Macro with Constant argument: %d\r\n", getValue_1());
    printf("Macro with variable argument: %d\r\n", getValue_2(300));
    return 0;
}

// Output
/* 
Macro with Constant argument: 65535
Macro with variable argument: 1131
*/

Я обнаружил, что USART.BAUD имеет разные значения в обеих программах, хотя они должны быть одинаковыми, если я установлю my_BaudRate равным 300.

В программе 1: USART1.BAUD имеет значение 0xFFFF, хотя оно должно было быть переполнено и иметь значение 0x046B. Я попытался изменить значение BAUD_RATE на 280 или меньше, оно всегда устанавливает его на 0xFFFF. Только после того, как я установил значение 306, фактическое значение становится меньше 0xFFFF.

В программе 2: работает как положено, USART1.BAUD имеет значение 0x046B.

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

Буду благодарен за любую информацию.

С наилучшими пожеланиями.

Макросы — это простые операции поиска и замены. USART_BAUD_RATE(300) будет заменен на (float)(5000000 * 64 / (16 * (float)300)) + 0.5). Аналогично USART_BAUD_RATE(my_BaudRate) будет заменено на (float)(5000000 * 64 / (16 * (float)my_BaudRate)) + 0.5)

Some programmer dude 06.06.2024 13:26

Скорее всего, здесь происходит не переполнение, а просто отключение. Результатом (5000000 * 64 / (16 * 300)) + 0.5 является 66667,166666667. Затем оно преобразуется в значение int66667, а явное преобразование в uint16_t отбросит верхние биты и оставит 65535. Что действительно 0xffff. Если вы разделите выражение на более мелкие части и используете временные переменные для хранения промежуточных результатов, его будет легче увидеть.

Some programmer dude 06.06.2024 13:32

То, что второй пример с переменной работает, может быть связано с множеством разных причин, о которых мы не сможем догадаться, пока вы не покажете нам правильный минимально воспроизводимый пример.

Some programmer dude 06.06.2024 13:34

@BetaEngineer – В вашем вопросе отсутствует определение my_BaudRate.

Armali 06.06.2024 13:35
USART1.BAUD = (uint16_t)(uint32_t)USART_BAUD_RATE(300); должен дать вам завернутое значение 0x46B, избегая неопределенного поведения. (Замените uint32_t на uint_fast32_t, uint_least32_t или unsigned long, если uint32_t не реализовано.)
Ian Abbott 06.06.2024 14:27

Я считаю, что именно определение my_Baudrate как uint32_t имеет здесь значение. Второй пример проходит через uint32 и берет младшее слово. Полагаться на переполнение при преобразовании типов всегда плохая идея. Первый выполняет насыщенное преобразование из (float) 66666,667 в uint16_t, ближайший — 65535, также известный как 0xffff. Кстати, BAUD_RATE должен быть в скобках на случай, если кто-то вызовет его с выражением внутри скобок, например. USART_BAUD_RATE(300+300). Я сомневаюсь, что кто-нибудь в наши дни использует скорость 300 бод, так что вам, вероятно, это сойдет с рук!

Martin Brown 06.06.2024 14:32

@Someprogrammerdude, спасибо, я думаю, преобразование плавающей точки в беззнаковое целое число не так просто, как int-int.

BetaEngineer 06.06.2024 16:25

@MartinBrown Я думал, что он должен был отреагировать так же, не имеет значения, является ли это константой или передается переменная uint32. Но в обоих случаях расчет какой-то, просто конвертация в uint16 разная. ПС. спасибо за предложение о брекетах. И да, я читаю показания внешнего счетчика, поддерживающего скорость 300 бод. Раньше он работал нормально, потому что я использовал (uint16_t)USART_BAUD_RATE(300), но когда я изменил его на (uint16_t)USART_BAUD_RATE(someVariable), я не смог прочитать показания счетчика, а позже обнаружил, что фактический набор бод отличается.

BetaEngineer 06.06.2024 16:30

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

Martin Brown 06.06.2024 16:54
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
9
89
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

… он должен был переполниться и иметь адрес 0x046B.

Учитывая, что аргумент BAUD_RATE равен 300, ((float)(5000000 * 64 / (16 * (float)BAUD_RATE)) + 0.5) оценивается как 66 667,166…

Затем (uint16_t)USART_BAUD_RATE(300) пытается преобразовать 66 667,166… в uint16_t. Поскольку 66,667.166… в шестнадцатеричном виде — это 1046B.2AA…16, вы, по-видимому, ожидаете, что это преобразование завершится по модулю 216 и будет усечено, в результате чего будет получено 46B16.

Однако C 2018 6.3.1.4 1 говорит:

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

Поскольку целая часть 1046B.2AA…16 больше, чем FFFF16, ее нельзя представить в uint16_t, и поэтому поведение не определяется стандартом C. Компилятор нередко генерирует разные результаты для неопределенных выражений с использованием констант и неопределенных выражений с использованием переменных, поскольку компилятор может оценивать выражения с использованием констант во время компиляции, используя свою собственную встроенную арифметику, но генерировать инструкции для выражений с использованием переменных, которые используют разные арифметика.

Если вы хотите, чтобы это преобразование было переносным, вы должны реализовать это самостоятельно, проверив значение, пока оно все еще находится в типе float, и вычитая при необходимости. Если вы хотите, чтобы он не переполнялся и не переносился, вам необходимо использовать более широкий целочисленный тип или перепроектировать код, чтобы использовать другие единицы измерения, чтобы значение помещалось в uint16_t.

Спасибо за ссылку на стандарт C. В моем тестировании использование (uint16_t)USART_BAUD_RATE(300) всегда приводит к 0xFFFF, а (uint16_t)USART_BAUD_RATE(var), здесь var — это uint32 и имеет значение 300, всегда приводит к 0x046B. Вот почему я подумал, что, возможно, это как-то связано с оптимизацией препроцессора или компилятора. Но теперь я исправлю это, убедившись, что мое значение правильное.

BetaEngineer 06.06.2024 16:33

Я бы, наверное, улучшил ваш макрос USART_BAUD_RATE с помощью нескольких преобразований. Начиная с этого:

#define USART_BAUD_RATE(BAUD_RATE) ((float)(5000000 * 64 / (16 * (float)BAUD_RATE)) + 0.5)

, я бы сначала объединил целочисленные константы 64 в числителе и 16 в знаменателе. Деление точное, в том числе и с плавающей запятой, и это не только упрощает выражение, но и уменьшает возможности целочисленного переполнения. Это дает нам следующее:

#define USART_BAUD_RATE(BAUD_RATE) ((float)(5000000 * 4 / ((float)BAUD_RATE)) + 0.5)

Я не вижу особого преимущества 5000000 * 4 перед 20000000, поэтому я бы объединил их, чтобы получить последнее:

#define USART_BAUD_RATE(BAUD_RATE) ((float)(20000000 / ((float)BAUD_RATE)) + 0.5)

Кроме того, я бы заметил, что первое приведение не является необходимым. Его операнд уже имеет тип float, поскольку операнды в вычисляемом выражении представляют собой смесь float и целых чисел. Удаление этого дает:

#define USART_BAUD_RATE(BAUD_RATE) ((20000000 / ((float)BAUD_RATE)) + 0.5)

Однако,

  1. Цель оставшегося приведения, по-видимому, состоит в том, чтобы обеспечить деление FP вместо целочисленного деления, и этого было бы лучше достичь, используя константу FP в числителе, чем путем приведения знаменателя; и

  2. Аргументы макроса обычно следует заключать в круглые скобки там, где они появляются в замещающем тексте макроса.

Оба из них предполагают преобразование в

#define USART_BAUD_RATE(BAUD_RATE) ((20000000f / (BAUD_RATE)) + 0.5)

. Обратите внимание, что 2000000f — это константа типа float, поэтому выполняется деление с плавающей запятой, и частное также имеет тип float. Но почему float? Константа 0.5 имеет тип double, поэтому вы преобразуете результат float в double и получаете окончательный результат типа double. В большинстве реализаций C единственное преимущество float перед double — это размер хранилища, и вы все равно не сохраняете значение (в макросе), поэтому вместо этого я бы сделал это:

#define USART_BAUD_RATE(BAUD_RATE) ((20000000.0 / (BAUD_RATE)) + 0.5)

Однако, что касается вашей реальной проблемы, вам, похоже, нужно это целое число без знака, а не double или float (см. @Eric ответ), поэтому я бы, вероятно, поместил это в макрос. Если вы готовы предположить, что BAUD_RATE никогда не будет меньше 1, то это подойдет и в значительной степени не зависит от поведения, зависящего от реализации:

#define USART_BAUD_RATE(BAUD_RATE) ((uint_least32_t)((20000000.0 / (BAUD_RATE)) + 0.5))

Однако что касается использования этого макроса, если вы хотите извлечь наименее значимые 16 бит, то было бы яснее сделать это явно, путем маскировки. Поскольку окончательная версия макроса (см. выше) расширяется до выражения целочисленного типа, это не представляет особой проблемы:

USART1.BAUD = USART_BAUD_RATE(my_BaudRate) & 0xffff;

// or

USART1.BAUD = USART_BAUD_RATE(300) & 0xffff;

Что касается вашего предложения относительно определения МАКРО, я учту это в своем проекте, поскольку оно упрощает расчет, а также снижает вероятность переполнения. Единственное препятствие заключается в том, что (в моем полном проекте) в формуле 5000000 и 16 также определены макросом, но они могут иметь разные значения (хотя всегда постоянные), поэтому для разных проектов я могу выполнить расчеты самостоятельно и поставить упрощенную версию формул, как вы предложили.

BetaEngineer 06.06.2024 16:43
Ответ принят как подходящий

Я почти уверен, что разница между этими двумя случаями заключается в том, что вызов

USART_BAUD_RATE(300)

Будет скомпилирован оптимизатором для загрузки постоянного значения, которое вычисляется во время компиляции (потенциально более точно и лучше контролируемо, чем во время выполнения). Это контрастирует со вторым примером передачи переменной, когда система времени выполнения обязана выполнять все вычисления. Было бы полезно взглянуть на листинг ассемблерного кода, чтобы убедиться, что это так.

Стоит прокомментировать утраченное искусство эффективной целочисленной арифметики для встраиваемых систем. Приведение к плавающему состоянию не является необходимым (и может принести много лишнего груза во встроенных системах, в которых отсутствует аппаратная поддержка FP).

#define USART_BAUD_RATE(BAUD_RATE) ((float)(5000000 * 64 / (16 * (float)(BAUD_RATE))) + 0.5)

Первое число с плавающей запятой, вероятно, тоже хочет находиться внутри числителя (в некоторых системах оно может переполниться).

Это можно без особого труда переписать в чисто целочисленной форме.

#define USART_BAUD_RATE(B) (( 5000000 * 64 + 8*(B)) / (16 * (B))

Или, если вы уверены, что множитель знаменателя всегда будет кратен 8, его можно упростить до

#define USART_BAUD_RATE(B) (( 5000000 * 8 + (B)) / (2 * (B))

Если вам нужен ответ по модулю 0xffff, выполните побитовое &, чтобы получить результат в правильном диапазоне, а если вам нужно насыщение, то min(x,0xffff). Присвоение значения, выходящего за пределы диапазона, меньшему типу — это неопределенное поведение (хотя во многих случаях может показаться, что оно делает то, что вы ожидаете, — пока однажды это не произойдет). Я предполагаю, что некоторые из этих констант — это тактовая частота и коэффициенты аппаратного делителя, так что все умножения необходимы,

Другие вопросы по теме