Непонятно приведение типов и порядок операций

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

Речь идет о этом видео о DMA на конкретном ARM-процессоре STM32F407 и преобразовании 16-битных слов в 32-битные. Раньше я считал себя экспертом в C, но здесь что-то не так или я неправильно думаю.

Сейчас около 16:46 вы увидите следующую настройку DMA:

uint16_t rxBuf[8];
uint16_t txBuf[8];
...
HAL_I2SEx_TransmitReceive_DMA(&hi2s2, txBuf, rxBuf, 4);

И около 19:05 вы увидите это в процедуре обратного вызова:

int lsample = (int) (rxBuf[0]<<16)|rxBuf[1];
int rsample = (int) (rxBuf[2]<<16)|rxBuf[3];

Дело в том, что STM32F407 может выполнять DMA только с использованием 16-битных слов, даже если данные на последовательной шине (I2S) представляют собой 24-битные или 32-битные данные. Я уверен (или предполагаю), что тип int такой же, как int32_t. Значение в rxBuf[0] содержит старшие 16 бит выборки, а rxBuf[1] — младшие 16 бит 32-битной выборки левого канала.

Моя проблема в том, что круглые скобки, окружающие rxBuf[0]<<16, не включают приведение (int) слева от него.

Как это может работать? К результату применяется приведение к 32 битам (rxBuf[0]<<16), но внутри круглых скобок находится всего лишь 16-битное беззнаковое значение, сдвинутое влево на 16 бит. В этом 16-битном значении не должно оставаться ничего, кроме нулей, когда оно преобразуется в 32 бита.

Я думаю, что две строки кода должны быть

int32_t lsample = (int32_t) ( ( (uint32_t)rxBuf[0]<<16 ) | (uint32_t)rxBuf[1] );
int32_t rsample = (int32_t) ( ( (uint32_t)rxBuf[2]<<16 ) | (uint32_t)rxBuf[3] );

Сначала вы должны преобразовать значение rxBuf[0] в (uint32_t), а затем сдвинуть его влево на 16 бит. C естественным образом преобразует 16-битное значение rxBuf[1] в 32 бита, поскольку левый операнд теперь имеет размер 32 бита, но в любом случае полезно указать явное приведение.

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

Может быть, компилятор работает неправильно и по счастливой случайности обрабатывает эти 16-битные значения как 32-битное слово? Или я (после 4 десятилетий программирования на C) просто не понимаю правильный синтаксис приведения в C? Справочное руководство C, похоже, подтверждает мое понимание правильного синтаксиса.

Целочисленные акции
Nate Eldredge 01.07.2024 04:43

Обратите внимание, что потенциальная ошибка все еще существует: если установлен старший бит rxBuf[0], то сдвиг вправо на 16 сдвинет его в знаковый бит, что означает переполнение знакового целого числа, вызывающее неопределенное поведение. Итак, вам, вероятно, понадобится (uint32_t)rxBuf[0] << 16 | rxBuf[1] и присвойте его переменной типа uint32_t.

Nate Eldredge 01.07.2024 04:51

Не ждите потрясающего кода из видеоуроков, созданных электронщиками. Большинство из них знают C ровно настолько, чтобы обойтись. Явное приведение перед переходом к более крупному типу имеет лучшую семантику самодокументирования и, как правило, является лучшим способом написания кода, выполняющего манипуляции с битами. Я поддерживаю идею использования unsigned здесь и явного указания ширины типа, поскольку int зависит от реализации.

paddy 01.07.2024 05:00

@NateEldredge, я согласен, что uint32_t следует использовать вместо int, а затем результат привести к int32_t. Так и должно быть int32_t lsample = (int32_t)(((uint32_t)rxBuf[0]<<16 )|rxBuf[1]);, если мы собираемся быть очень строгими, корректными и откровенными при кастинге. Но этот (int32_t) приведение можно не учитывать, поскольку он подразумевается в задании. Я не согласен насчет неопределенного поведения. Если было расширение знака (а это произошло бы, если бы 16-битное значение было int16_t), тогда 16-битное значение расширяется по знаку при приведении, и 16 знаковых битов выпадают за пределы края вместо 16 нулей.

robert bristow-johnson 01.07.2024 05:07

@paddy, моя проблема в том, что этот код не должен работать. Мало того, что это плохой стиль. Кстати, я согласен с вами относительно методов и стиля кодирования многих программистов-инженеров-электриков. Я стараюсь быть предельно чистым в своем коде.

robert bristow-johnson 01.07.2024 05:11

Но это uint16_t, который не расширен знаком. Я не думаю, что большинство компиляторов применяют правило против переполнения для смен, потому что, вероятно, на него полагается много плохого кода, но, например, UBSAN его перехватывает: godbolt.org/z/nzxdxj6Wq

Nate Eldredge 01.07.2024 05:17

Переполнение происходит в C всякий раз, когда математическое значение выражения не может быть представлено в его типе. Математически сдвиги определяются как умножение на степень 2. 40000 * 2^16 равно 2621440000, что больше максимального значения int32_t (2147483647), следовательно, переполнение и UB. Вам не нужно сдвигать знаковый бит, чтобы вызвать переполнение; сдвиг уже достаточно плох, потому что вы переполнили 31 бит значения.

Nate Eldredge 01.07.2024 05:21

Да, @NateEldredge, я знаю, что это uint16_t. Но это значение внутри круглых скобок по-прежнему является всего лишь 16-битным значением, и, если компилятор работает в соответствии со спецификацией, (rxBuf[0]<<16) должно быть просто нулями, будь то uint16_t или int16_t. Это сдвиг влево, поэтому слева идут только нули, а все биты, сдвинутые вправо, выпадают за край. Все должно быть нулями.

robert bristow-johnson 01.07.2024 05:21

@robertbristow-johnson На ваш первоначальный вопрос уже был дан ответ, поэтому я не стал его комментировать. Хотя вы не правы. Этот код должен работать (в основном) из-за целочисленного продвижения, и его поведение не определено, даже если вы не согласны. Исходя из предположения, что int составляет 32 бита, когда uint32_t повышается до int, он всегда будет положительным, даже если бит 15 был равен 1. Затем вы сдвигаете этот бит в позицию знака, по сути создавая отрицательное значение из первоначально положительного значения, которое не определено. поведение в соответствии с языком.

paddy 01.07.2024 05:22

@robertbristow-johnson: Эта часть уже была объяснена в ответах и ​​в ссылке, которую я дал в своем первом комментарии. Целочисленные повышения означают, что rxBuf[0] << 16 сдвигает int, несмотря на отсутствие явного приведения. Таким образом, вы действительно перемещаетесь в пределах 32-битного числа со знаком, а не 16-битного числа без знака. C на самом деле не имеет никакого способа сделать последнее.

Nate Eldredge 01.07.2024 05:23

@paddy, где в круглых скобках uint16_t повышается до 32 бит? Я этого не вижу. Это слово 16-битное, и все его биты смещены. Прежде чем оно будет преобразовано в 32-битное число, должно остаться всего 16 нулей.

robert bristow-johnson 01.07.2024 05:24

@robertbristow-johnson: Повышение до int подразумевается!!!! Здесь не на что смотреть. Это происходит автоматически, независимо от того, просите вы об этом или нет, и предотвратить его невозможно.

Nate Eldredge 01.07.2024 05:24

У вас есть значение uint16_t, смещенное на значение int. uint16_t повышается до int, а затем происходит сдвиг на это значение int. Результат тоже int.

paddy 01.07.2024 05:25

ОЙ!!! Это константа 16 и есть int!!!! Дерьмо! Дрянной код и счастливая удача. Когда он сдвигается на константу, я не думаю, что эта константа превышает 6 бит. Это даже не int.

robert bristow-johnson 01.07.2024 05:26

Нет, дело даже не в этом. rxBuf[0] << (uint16_t)16 будет то же самое. КАЖДОЕ выражение типа uint16_t повышается до int. rxBuf[0] получает повышение вне зависимости от контекста, ДО ТОГО, как произойдет сдвиг.

Nate Eldredge 01.07.2024 05:27

Нейт, я думаю, у @paddy был ответ. ответ Пэдди: почему я не сумасшедший.

robert bristow-johnson 01.07.2024 05:29

Мы с Пэдди говорим то же самое.

Nate Eldredge 01.07.2024 05:29

Нет, я думаю, что я тоже не прав! :) Меня тоже уже укусила эта штука, и я до сих пор не знаю, что лучше.

paddy 01.07.2024 05:29

Так ли char повышают до int?

robert bristow-johnson 01.07.2024 05:30

Да, они. Каждый тип ранга ниже int, то есть каждый тип, диапазон которого соответствует диапазону int.

Nate Eldredge 01.07.2024 05:30

Где это описано в справочном руководстве по C?

robert bristow-johnson 01.07.2024 05:32

@robertbristow-johnson Смотрите мой ответ.

dbush 01.07.2024 05:32

И когда вы делаете что-то вроде uint16_t x = 5000; uint16_t y = x << 7; или даже y = x << (uint16_t)7;, вы можете думать, что смещаетесь в пределах 16-битного значения, но в абстрактной машине C вы смещаетесь в пределах 32-битного значения (или любого другого размера int). be), а затем усекая результат до 16 бит. Компилятор, конечно, может оптимизировать это для сдвига 16-битного объекта, если на целевой машине есть такая инструкция, поскольку конечный результат тот же, но это не то, что делается концептуально.

Nate Eldredge 01.07.2024 05:33

А в контексте ARM это даже соответствует тому, что делает физическая машина. ldrh r0, [x] ноль расширяется до 32-битного регистра. lsl r0, r0, #7 сдвигает весь 32-битный регистр. strh r0, [y] сохраняет только младшие 16 бит. На самом деле вы вычислили старшие 16 бит 32-битного результата сдвига, но затем проигнорировали их. В отличие от x86, который может выполнять shr ax, 7 с 16-битным частичным регистром, ARM не имеет 16-битных арифметических или логических инструкций как таковых.

Nate Eldredge 01.07.2024 05:39

@robert bristow-johnson, обратите внимание, что int/unsigned может достигать 16 бит. Здесь лучше использовать uint32_t (или шире). uint32_t lsample = ((uint32_t) rxBuf[0]<<16)) | rxBuf[1];.

chux - Reinstate Monica 01.07.2024 05:57

@chux-ReinstateMonica, как только полное 32-битное слово будет собрано вместе (все с использованием инструкций по манипуляции беззнаковыми битами), мы хотим переинтерпретировать 32-битное слово как знаковое. Это аудиопроект DSP, и эти сэмплы представляют собой числа со знаком. В конце концов они будут преобразованы в float и масштабированы на 2^(-31) так, чтобы их диапазон находился в диапазоне от -1,0 до +0,99999999 .

robert bristow-johnson 01.07.2024 06:00

@robertbristow-johnson, Лучше спросить, как напрямую превратить 2 значения uint16_t в float.

chux - Reinstate Monica 01.07.2024 06:12

о, нет, @chux-ReinstateMonica, я знаю, как это сделать. Это был DMA, происходящий на оборудовании, которое имело только 16-битный буфер DMA. Значения, поступающие от аналого-цифрового преобразователя, представляют собой числа с фиксированной точкой со знаком. Поэтому C сначала увидит их как целые числа со знаком. Затем мы преобразуем их в число с плавающей запятой (это будут математические числа с плавающей запятой, имеющие то же значение, что и целое число со знаком), а затем масштабируем это число с плавающей запятой на 2^(-31), чтобы все значения с плавающей запятой находились в диапазоне от -1,0000000 до +0,99999999 .

robert bristow-johnson 01.07.2024 06:19

На мой взгляд это самый правильный код: int32_t lsample = (int32_t)( ( (uint32_t)rxBuf[0]<<16 ) | (uint32_t)rxBuf[1] );

robert bristow-johnson 01.07.2024 06:21

int точно 32-битный на этой платформе?

M.M 01.07.2024 06:40

И это объясняет, чем закончится целочисленное продвижение, @M.M. И это, и эта неявная реклама объясняют, как продукт работал в этом видео. Но, собирая слово из более мелких слов, мы должны четко указывать приведения типов. Раньше я был чертовски растерян.

robert bristow-johnson 01.07.2024 07:00

@M.M Я предположил, что int было 32 бита, потому что STM32F407 имеет шину данных шириной 32 бита. Я просто считал, что без явного приведения 16-битное число остается 16-битным до тех пор, пока оно не будет смешано с 32-битным или более длинным словом. Я не знал, что 16-битный short будет неявно преобразован (или «повышен») в int. Не раньше, чем будет операция с int.

robert bristow-johnson 01.07.2024 18:08
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
32
157
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

C неявно переводит целочисленные аргументы операторов сдвига с рангом меньше int в int. (int) там ничего не делает.

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

robert bristow-johnson 01.07.2024 05:34

В любом случае лучше рекламировать явно. (Хотя в вашем случае это без знака.) Читателю кода не нужно обращаться к неясным уголкам стандарта, чтобы узнать, что должен делать код.

Mark Adler 01.07.2024 05:36

Марк, твой ответ достаточно правильный, но я ставлю галочку @dbush.

robert bristow-johnson 01.07.2024 05:41

@Fredrik Честно говоря, нередко даже опытные программисты не знают обо всех различных неявных вещах в C. Я видел это много раз, когда вы применяете более строгий набор правил, такой как MISRA C, и тогда это становится глазом. -открывалка. До этого они обходились "странным багом, но я добавил кастинг и теперь все работает нормально - не знаю почему - пожимаю плечами". А в классах для начинающих никогда не упоминаются неявные продвижения: вместо этого они одержимы обучением умеренно полезным вещам, таким как рекурсия или строки формата printf.

Lundin 01.07.2024 08:21

@Fredrik Если вы всегда выполняете явное приведение типов там, где должны происходить повышения, чего требует любая хорошая практика кодирования, тогда вам не нужно будет знать или заботиться о неявных повышениях C.

Mark Adler 01.07.2024 16:32

@Фредрик, я всегда знал, что int и unsigned int — это естественная ширина целого числа машины. По сути, ширина шины данных. В мое время это был MC68000 (16 бит), первый микропроцессор, на котором я программировал C (и asm). long всегда был длиннее, чем short, а int мог быть любым из них, но не обоими. Это неявное преобразование целых чисел короче int в int, о котором я не знал. Мое самообучение показало, что в целом ширина слова оставалась постоянной до смешанной операции с более длинным словом или явного приведения.

robert bristow-johnson 01.07.2024 17:34

И, @Fredrik, я всегда делаю явное приведение типов. Просто я читал чужой код и не мог понять, как он мог работать при таком дрянном кастинге. Я был убежден, что если бы компилятор вел себя в соответствии со спецификацией, 16-битные слова оставались бы 16-битными при добавлении, вычитании, умножении, делении, операции OR или AND, сдвиге, операции XOR, дополнении или отрицании. Разве что в смешанной операции с более длинным словом. Или явно приводить. Это была доктрина C, которую я усвоил с самого начала.

robert bristow-johnson 01.07.2024 17:48

@robertbristow-johnson К вашему сведению, неявные продвижения от char и short до int находятся в стандарте C89.

Mark Adler 01.07.2024 20:38

Да, и я не знал этого @MarkAdler. С 1982 года я всегда думал, что длина слова остается неизменной, если только она не была явно или неявно приведена, потому что она находилась в операции с более длинным словом, а затем более короткое слово правильно расширяется до более длинного слова. И я подумал, что если более короткое слово было подписано, этот старший бит (знаковый бит) будет скопирован во все левые биты расширения, независимо от знаковости более длинного слова. А если более короткое слово было беззнаковым, то все левые биты расширения равны нулю, независимо от знака более длинного слова.

robert bristow-johnson 01.07.2024 20:54

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

Mark Adler 02.07.2024 06:27
Ответ принят как подходящий

То, что здесь происходит, является результатом целочисленного продвижения. Вообще говоря, любое выражение, использующее тип меньше int, будет переводить этот тип в int.

Точная формулировка этого изложена в разделе 6.3.1.1p2 стандарта C:

Следующие выражения могут использоваться в выражениях, где есть int или unsigned int можно использовать:

  • Объект или выражение целочисленного типа (отличного от int или unsigned int), чей целочисленный ранг преобразования меньше или равен ранг int и unsigned int.
  • Битовое поле типа _Bool, int, signed int или unsigned int.

Если int может представлять все значения исходного типа (как ограничено шириной битового поля), значение преобразуется в ан int; в противном случае он преобразуется в unsigned int. Это называются целочисленными акциями. Все остальные типы не изменяются. целочисленные акции

И побитовые сдвиги являются одним из таких операторов, где это применимо, как указано в разделе 6.5.7p3 стандарта C:

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

Итак, в этом выражении:

int lsample = (int) (rxBuf[0]<<16)|rxBuf[1];

Значение rxBuf[0] сначала повышается до int, прежде чем применяется оператор сдвига влево. Значение rxBuf[1] также повышается до int перед применением побитового оператора ИЛИ. Таким образом, в этом случае приведение фактически не имеет никакого эффекта.

Однако здесь есть ошибка. Предполагая, что int имеет размер 32 бита, если установлен старший бит rxBuf[0], то сдвиг приведет к сдвигу битового значения 1 в знаковый бит результата. Это вызовет неопределенное поведение согласно разделу 6.5.7p4 стандарта C относительно операторов побитового сдвига:

Результатом E1 << E2 является сдвиг битов E2 влево; освобожден биты заполняются нулями. Если E1 имеет беззнаковый тип, значение результат равен E1 × 2E2, уменьшенный по модулю на единицу больше, чем максимальное значение, представленное в типе результата. Если E1 имеет подписанный тип и неотрицательное значение, а E1 × 2E2 представимо в типе результата это результирующее значение; в противном случае поведение неопределенно.

Правильный способ справиться с этим — привести значение rxBuf[0] к uint32_t, чтобы сдвиг работал правильно:

int32_t lsample = ((uint32_t)rxBuf[0]<<16)|rxBuf[1];
int32_t rsample = ((uint32_t)rxBuf[2]<<16)|rxBuf[3];

Это ответ. Извините, блин, я спорил с другими ребятами и просто не увидел здесь вашего ответа.

robert bristow-johnson 01.07.2024 05:35

@robertbristow-johnson Re: ваше предложение по редактированию, я не уверен, что преобразование результата в знаковый тип имеет смысл. Обычно, когда используются битовые сдвиги, желаемые значения являются беззнаковыми.

dbush 01.07.2024 05:48

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

robert bristow-johnson 01.07.2024 05:57

Re «Однако здесь есть ошибка»: ошибки нет, если значение гарантированно не переполняется (как в случае, если, как утверждает OP, объединенное значение умещается в 24 бита) или если реализация C определяет поведение.

Eric Postpischil 01.07.2024 09:36

Существует ошибка в случае, если rxBuf[0]<<16 имеет 1 в старшем бите, потому что тогда он сместится влево на знаковый бит повышенного до 32 бита int, что явно является неопределенной ошибкой поведения.

Lundin 01.07.2024 11:00

@dbush Я действительно думаю, что тебе следует принять редактирование. lsample и rsample всегда означали числа со знаком. Просто нам предстоит собрать 32-битное слово из 16-битных блоков бит, которые, естественно, следует считать беззнаковыми. Но как только это слово собрано, оно становится числом со знаком, дополненным до двух. И к этому нужно относиться соответственно, потому что в моем приложении он сразу превращается в float. Я не хочу отправлять unsigned в число с плавающей запятой, потому что значение с плавающей запятой будет неправильным.

robert bristow-johnson 01.07.2024 18:15

@Lundin, чтобы расширение знака произошло, разве это не зависит от того, является ли операнд сдвига (в данном случае rxBuf[0]) знаковым или беззнаковым? Или это зависит только от целочисленного типа пункта назначения (в данном случае lsample)? Мне кажется, единственное логическое правило состоит в том, что расширение знака зависит только от операнда, а не от места назначения. И мне кажется, что единственное неявное целочисленное продвижение должно сохранять знаковость операнда. Таким образом, uint16_t следует повышать до unsigned int, только если sizeof(uint16_t) меньше sizeof(int).

robert bristow-johnson 01.07.2024 20:33

@robertbristow-johnson Расширение знака относится к случаям, когда у вас есть меньший знаковый и отрицательный целочисленный тип, и он преобразуется в больший, и в этом случае знак сохраняется. Здесь это не применимо. И, к сожалению, целочисленные акции (в 32/64-битной системе) превращают ваш uint16_t в int со знаком. Язык C не очень логичен, и это довольно сломанная часть языка. И когда у нас есть подписанный int32, мы не можем оставить данные сдвига более чем на 30 бит, когда вызываем UB. Или, если подписанное int32 отрицательное, мы можем вообще не сдвигать данные влево или вызывать UB.

Lundin 02.07.2024 09:03

То есть это неопределенное поведение: uint16_t x = 0x8000; x << 16; Как есть uint16_t x = 0; ~x << n;. Эмпирическое правило заключается в том, чтобы всегда приводить левый операнд сдвига к большому целочисленному типу без знака, например uint32_t.

Lundin 02.07.2024 09:05

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

robert bristow-johnson 02.07.2024 17:10

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