Это очень специфическая проблема, и я не получил ответа от автора, поэтому решил выбросить это и посмотреть, в порядке ли мое здравомыслие или нет.
Речь идет о этом видео о 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, похоже, подтверждает мое понимание правильного синтаксиса.
Обратите внимание, что потенциальная ошибка все еще существует: если установлен старший бит rxBuf[0]
, то сдвиг вправо на 16 сдвинет его в знаковый бит, что означает переполнение знакового целого числа, вызывающее неопределенное поведение. Итак, вам, вероятно, понадобится (uint32_t)rxBuf[0] << 16 | rxBuf[1]
и присвойте его переменной типа uint32_t
.
Не ждите потрясающего кода из видеоуроков, созданных электронщиками. Большинство из них знают C ровно настолько, чтобы обойтись. Явное приведение перед переходом к более крупному типу имеет лучшую семантику самодокументирования и, как правило, является лучшим способом написания кода, выполняющего манипуляции с битами. Я поддерживаю идею использования unsigned
здесь и явного указания ширины типа, поскольку int
зависит от реализации.
@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 нулей.
@paddy, моя проблема в том, что этот код не должен работать. Мало того, что это плохой стиль. Кстати, я согласен с вами относительно методов и стиля кодирования многих программистов-инженеров-электриков. Я стараюсь быть предельно чистым в своем коде.
Но это uint16_t
, который не расширен знаком. Я не думаю, что большинство компиляторов применяют правило против переполнения для смен, потому что, вероятно, на него полагается много плохого кода, но, например, UBSAN его перехватывает: godbolt.org/z/nzxdxj6Wq
Переполнение происходит в C всякий раз, когда математическое значение выражения не может быть представлено в его типе. Математически сдвиги определяются как умножение на степень 2. 40000 * 2^16
равно 2621440000, что больше максимального значения int32_t
(2147483647), следовательно, переполнение и UB. Вам не нужно сдвигать знаковый бит, чтобы вызвать переполнение; сдвиг уже достаточно плох, потому что вы переполнили 31 бит значения.
Да, @NateEldredge, я знаю, что это uint16_t
. Но это значение внутри круглых скобок по-прежнему является всего лишь 16-битным значением, и, если компилятор работает в соответствии со спецификацией, (rxBuf[0]<<16)
должно быть просто нулями, будь то uint16_t
или int16_t
. Это сдвиг влево, поэтому слева идут только нули, а все биты, сдвинутые вправо, выпадают за край. Все должно быть нулями.
@robertbristow-johnson На ваш первоначальный вопрос уже был дан ответ, поэтому я не стал его комментировать. Хотя вы не правы. Этот код должен работать (в основном) из-за целочисленного продвижения, и его поведение не определено, даже если вы не согласны. Исходя из предположения, что int
составляет 32 бита, когда uint32_t
повышается до int
, он всегда будет положительным, даже если бит 15 был равен 1. Затем вы сдвигаете этот бит в позицию знака, по сути создавая отрицательное значение из первоначально положительного значения, которое не определено. поведение в соответствии с языком.
@robertbristow-johnson: Эта часть уже была объяснена в ответах и в ссылке, которую я дал в своем первом комментарии. Целочисленные повышения означают, что rxBuf[0] << 16
сдвигает int
, несмотря на отсутствие явного приведения. Таким образом, вы действительно перемещаетесь в пределах 32-битного числа со знаком, а не 16-битного числа без знака. C на самом деле не имеет никакого способа сделать последнее.
@paddy, где в круглых скобках uint16_t
повышается до 32 бит? Я этого не вижу. Это слово 16-битное, и все его биты смещены. Прежде чем оно будет преобразовано в 32-битное число, должно остаться всего 16 нулей.
@robertbristow-johnson: Повышение до int
подразумевается!!!! Здесь не на что смотреть. Это происходит автоматически, независимо от того, просите вы об этом или нет, и предотвратить его невозможно.
У вас есть значение uint16_t
, смещенное на значение int
. uint16_t
повышается до int
, а затем происходит сдвиг на это значение int
. Результат тоже int
.
ОЙ!!! Это константа 16 и есть int
!!!! Дерьмо! Дрянной код и счастливая удача. Когда он сдвигается на константу, я не думаю, что эта константа превышает 6 бит. Это даже не int
.
Нет, дело даже не в этом. rxBuf[0] << (uint16_t)16
будет то же самое. КАЖДОЕ выражение типа uint16_t
повышается до int
. rxBuf[0]
получает повышение вне зависимости от контекста, ДО ТОГО, как произойдет сдвиг.
Нейт, я думаю, у @paddy был ответ. ответ Пэдди: почему я не сумасшедший.
Мы с Пэдди говорим то же самое.
Нет, я думаю, что я тоже не прав! :) Меня тоже уже укусила эта штука, и я до сих пор не знаю, что лучше.
Так ли char
повышают до int
?
Да, они. Каждый тип ранга ниже int
, то есть каждый тип, диапазон которого соответствует диапазону int
.
Где это описано в справочном руководстве по C?
@robertbristow-johnson Смотрите мой ответ.
И когда вы делаете что-то вроде uint16_t x = 5000; uint16_t y = x << 7;
или даже y = x << (uint16_t)7;
, вы можете думать, что смещаетесь в пределах 16-битного значения, но в абстрактной машине C вы смещаетесь в пределах 32-битного значения (или любого другого размера int
). be), а затем усекая результат до 16 бит. Компилятор, конечно, может оптимизировать это для сдвига 16-битного объекта, если на целевой машине есть такая инструкция, поскольку конечный результат тот же, но это не то, что делается концептуально.
А в контексте ARM это даже соответствует тому, что делает физическая машина. ldrh r0, [x]
ноль расширяется до 32-битного регистра. lsl r0, r0, #7
сдвигает весь 32-битный регистр. strh r0, [y]
сохраняет только младшие 16 бит. На самом деле вы вычислили старшие 16 бит 32-битного результата сдвига, но затем проигнорировали их. В отличие от x86, который может выполнять shr ax, 7
с 16-битным частичным регистром, ARM не имеет 16-битных арифметических или логических инструкций как таковых.
@robert bristow-johnson, обратите внимание, что int/unsigned
может достигать 16 бит. Здесь лучше использовать uint32_t
(или шире). uint32_t lsample = ((uint32_t) rxBuf[0]<<16)) | rxBuf[1];
.
@chux-ReinstateMonica, как только полное 32-битное слово будет собрано вместе (все с использованием инструкций по манипуляции беззнаковыми битами), мы хотим переинтерпретировать 32-битное слово как знаковое. Это аудиопроект DSP, и эти сэмплы представляют собой числа со знаком. В конце концов они будут преобразованы в float
и масштабированы на 2^(-31) так, чтобы их диапазон находился в диапазоне от -1,0 до +0,99999999 .
@robertbristow-johnson, Лучше спросить, как напрямую превратить 2 значения uint16_t
в float
.
о, нет, @chux-ReinstateMonica, я знаю, как это сделать. Это был DMA, происходящий на оборудовании, которое имело только 16-битный буфер DMA. Значения, поступающие от аналого-цифрового преобразователя, представляют собой числа с фиксированной точкой со знаком. Поэтому C сначала увидит их как целые числа со знаком. Затем мы преобразуем их в число с плавающей запятой (это будут математические числа с плавающей запятой, имеющие то же значение, что и целое число со знаком), а затем масштабируем это число с плавающей запятой на 2^(-31), чтобы все значения с плавающей запятой находились в диапазоне от -1,0000000 до +0,99999999 .
На мой взгляд это самый правильный код: int32_t lsample = (int32_t)( ( (uint32_t)rxBuf[0]<<16 ) | (uint32_t)rxBuf[1] );
int
точно 32-битный на этой платформе?
И это объясняет, чем закончится целочисленное продвижение, @M.M. И это, и эта неявная реклама объясняют, как продукт работал в этом видео. Но, собирая слово из более мелких слов, мы должны четко указывать приведения типов. Раньше я был чертовски растерян.
@M.M Я предположил, что int
было 32 бита, потому что STM32F407 имеет шину данных шириной 32 бита. Я просто считал, что без явного приведения 16-битное число остается 16-битным до тех пор, пока оно не будет смешано с 32-битным или более длинным словом. Я не знал, что 16-битный short
будет неявно преобразован (или «повышен») в int
. Не раньше, чем будет операция с int
.
C неявно переводит целочисленные аргументы операторов сдвига с рангом меньше int
в int
. (int)
там ничего не делает.
Пока я учусь. Я не знал, что C неявно преобразует целочисленные типы короче int
в int
. После четырех десятилетий программирования на C я этого не знал.
В любом случае лучше рекламировать явно. (Хотя в вашем случае это без знака.) Читателю кода не нужно обращаться к неясным уголкам стандарта, чтобы узнать, что должен делать код.
Марк, твой ответ достаточно правильный, но я ставлю галочку @dbush.
@Fredrik Честно говоря, нередко даже опытные программисты не знают обо всех различных неявных вещах в C. Я видел это много раз, когда вы применяете более строгий набор правил, такой как MISRA C, и тогда это становится глазом. -открывалка. До этого они обходились "странным багом, но я добавил кастинг и теперь все работает нормально - не знаю почему - пожимаю плечами". А в классах для начинающих никогда не упоминаются неявные продвижения: вместо этого они одержимы обучением умеренно полезным вещам, таким как рекурсия или строки формата printf.
@Fredrik Если вы всегда выполняете явное приведение типов там, где должны происходить повышения, чего требует любая хорошая практика кодирования, тогда вам не нужно будет знать или заботиться о неявных повышениях C.
@Фредрик, я всегда знал, что int
и unsigned int
— это естественная ширина целого числа машины. По сути, ширина шины данных. В мое время это был MC68000 (16 бит), первый микропроцессор, на котором я программировал C (и asm). long
всегда был длиннее, чем short
, а int
мог быть любым из них, но не обоими. Это неявное преобразование целых чисел короче int
в int
, о котором я не знал. Мое самообучение показало, что в целом ширина слова оставалась постоянной до смешанной операции с более длинным словом или явного приведения.
И, @Fredrik, я всегда делаю явное приведение типов. Просто я читал чужой код и не мог понять, как он мог работать при таком дрянном кастинге. Я был убежден, что если бы компилятор вел себя в соответствии со спецификацией, 16-битные слова оставались бы 16-битными при добавлении, вычитании, умножении, делении, операции OR или AND, сдвиге, операции XOR, дополнении или отрицании. Разве что в смешанной операции с более длинным словом. Или явно приводить. Это была доктрина C, которую я усвоил с самого начала.
@robertbristow-johnson К вашему сведению, неявные продвижения от char
и short
до int
находятся в стандарте C89.
Да, и я не знал этого @MarkAdler. С 1982 года я всегда думал, что длина слова остается неизменной, если только она не была явно или неявно приведена, потому что она находилась в операции с более длинным словом, а затем более короткое слово правильно расширяется до более длинного слова. И я подумал, что если более короткое слово было подписано, этот старший бит (знаковый бит) будет скопирован во все левые биты расширения, независимо от знаковости более длинного слова. А если более короткое слово было беззнаковым, то все левые биты расширения равны нулю, независимо от знака более длинного слова.
Никакого осуждения. Просто указываю на то, что это не то, что они прокрались недавно. Как я уже сказал, это не имеет никакого значения, если применять достойные методы кодирования.
То, что здесь происходит, является результатом целочисленного продвижения. Вообще говоря, любое выражение, использующее тип меньше 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];
Это ответ. Извините, блин, я спорил с другими ребятами и просто не увидел здесь вашего ответа.
@robertbristow-johnson Re: ваше предложение по редактированию, я не уверен, что преобразование результата в знаковый тип имеет смысл. Обычно, когда используются битовые сдвиги, желаемые значения являются беззнаковыми.
Верно. Эти биты, синхронизируемые в чип по последовательной шине, представляют собой 24-битные слова со знаком, поступающие от аналого-цифрового преобразователя. Сначала нам нужно рассматривать все биты как беззнаковые, пока мы собираем полное слово. Но как только все слово упаковано вместе, мы хотим считать 32-битное значение подписанным. (Со временем это станет float
.)
Re «Однако здесь есть ошибка»: ошибки нет, если значение гарантированно не переполняется (как в случае, если, как утверждает OP, объединенное значение умещается в 24 бита) или если реализация C определяет поведение.
Существует ошибка в случае, если rxBuf[0]<<16
имеет 1 в старшем бите, потому что тогда он сместится влево на знаковый бит повышенного до 32 бита int
, что явно является неопределенной ошибкой поведения.
@dbush Я действительно думаю, что тебе следует принять редактирование. lsample
и rsample
всегда означали числа со знаком. Просто нам предстоит собрать 32-битное слово из 16-битных блоков бит, которые, естественно, следует считать беззнаковыми. Но как только это слово собрано, оно становится числом со знаком, дополненным до двух. И к этому нужно относиться соответственно, потому что в моем приложении он сразу превращается в float
. Я не хочу отправлять unsigned
в число с плавающей запятой, потому что значение с плавающей запятой будет неправильным.
@Lundin, чтобы расширение знака произошло, разве это не зависит от того, является ли операнд сдвига (в данном случае rxBuf[0]
) знаковым или беззнаковым? Или это зависит только от целочисленного типа пункта назначения (в данном случае lsample
)? Мне кажется, единственное логическое правило состоит в том, что расширение знака зависит только от операнда, а не от места назначения. И мне кажется, что единственное неявное целочисленное продвижение должно сохранять знаковость операнда. Таким образом, uint16_t
следует повышать до unsigned int
, только если sizeof(uint16_t)
меньше sizeof(int)
.
@robertbristow-johnson Расширение знака относится к случаям, когда у вас есть меньший знаковый и отрицательный целочисленный тип, и он преобразуется в больший, и в этом случае знак сохраняется. Здесь это не применимо. И, к сожалению, целочисленные акции (в 32/64-битной системе) превращают ваш uint16_t
в int
со знаком. Язык C не очень логичен, и это довольно сломанная часть языка. И когда у нас есть подписанный int32, мы не можем оставить данные сдвига более чем на 30 бит, когда вызываем UB. Или, если подписанное int32 отрицательное, мы можем вообще не сдвигать данные влево или вызывать UB.
То есть это неопределенное поведение: uint16_t x = 0x8000; x << 16;
Как есть uint16_t x = 0; ~x << n;
. Эмпирическое правило заключается в том, чтобы всегда приводить левый операнд сдвига к большому целочисленному типу без знака, например uint32_t
.
Я согласен с эмпирическим правилом. Просто раньше я всегда думал, что длина слова оставалась неизменной, если только она не была явно или неявно приведена, потому что она находилась в операции с более длинным словом, а затем более короткое слово правильно расширяется до более длинного слова. И я подумал, что если более короткое слово было подписано, этот старший бит (знаковый бит) будет скопирован во все левые биты расширения, независимо от знаковости более длинного слова. А если более короткое слово было беззнаковым, то все левые биты расширения равны нулю, независимо от знака более длинного слова.