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

Вводные данные

В данный момент заканчиваю разработку цифрового синтезатора Kaleidoscope собственного авторства. Синтезатор работает на базе микроконтроллера stm32f446, процессор которого имеет ряд очень интересных и полезных инструкций, описанных в Programming Manual.

Вывод всей информации осуществляется на черно-белый OLED дисплей с разрешением 128x64 точки. Или 8 строк по 128 байтов. На экране нельзя задавать яркость отдельного пикселя, можно писать только байтами.

Особенность дисплея в том, что нижнему пикселю байта соответствует наиболее значащий бит записываемого байта. Вы пишете в память 0b11001010, а на экране рисуется

низ.11001010.верх

Мне необходимо вывести бинарно кодированное число 8 бит на шкалу из 24 вертикальных пикселей.

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

Параметр MSK - это и есть 8-бит паттерн
Параметр MSK - это и есть 8-бит паттерн

Например, число 0xAE = 0b10101110 должно отображаться на 24 пикселя экрана так:

низ .00011111.11110001.11000111. верх

Для начала я получаю маску 24 битов, которые надо нарисовать.

uint32_t t = 0;
uint8_t  b_num = 0xAE;

for(int i = 0; i < 8; i++)
{
  if(b_num & (1<<i) != 0)
  {
    t |= 0b111 << (i*3);  
  }
  // 0xAE = 0b10101110 -> t = 0b00000000111000111000111111111000
}

// отобразить надо
// низ .00011111.11110001.11000111. верх

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

0b00011111. Биты [0:7] числа t в обратном порядке.

0b11110001. Биты [8:15] числа t в обратном порядке.

0b11000111. Биты [16:23] числа t в обратном порядке.

Конечно, всё это можно сделать софтверно.

Через циклы.

// t уже хранит в себе маску битов
uint8_t b[3];
for(int j = 0; j < 3; j++)
{
  uint8_t b, bj;
  b  = 0;
  bj = (t>>(8*j)) & 0xFF;    //сдвигаем, а потом маскируем нижние 8 бит

  for(int i = 0; i < 8; i++)
  {
    if(0!= (1<<i) & bj)
      b |= 1<<(7-i);
  }
  b[j] = b;
}

Через LUT на 256 значений, где каждому i будет соответствовать его число с обратным порядком битов.

// t уже хранит в себе маску битов
uint8_t b[3];
for(int j = 0; j < 3; j++)
{
  uint8_t bj;
  bj = (t>>(8*j)) & 0xFF;    //сдвигаем, а потом маскируем нижние 8 бит

  b[j] = rev_bit_lut_256[bj]; 
  //лут хранит заранее вычисленные числа с обратным порядком битов
}

А можно воспользоваться специальными ассемблерными инструкциями процессора:

RBIT - берет слово и возвращает за один цикл процессора число с обратным порядком битов.

REV - берет слово и возвращает за один цикл процессора слово с обратным порядком байтов.

	.global rev_bit_word
	.text
rev_bit_word:
	//r0 - address of x     uint32
	//r1 - address of res 	uint32

	ldr		r2, [r0] 	// load x to r2 register
  	rbit    r2, r2      // reversed bit  order
  	rev     r2, r2      // reversed byte order
	str     r2, [r1]    // store x to destination address

	bx 	    lr          // return

Дальше мы просто отрезаем нужные биты из отредактированного t и отправляем их в память дисплея.

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

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

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

Комментарии (17)


  1. SIISII
    01.01.2025 14:29

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


    1. vadjuse Автор
      01.01.2025 14:29

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

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


      1. HardWrMan
        01.01.2025 14:29

        Насколько медленнее/быстрее будет подобный инлайн?

        asm("rbit %0,%1" : "=r" (buf[i]) : "r" (buf[i]));

        asm("rev %0,%1" : "=r" (buf[i]) : "r" (buf[i]));


        1. vadjuse Автор
          01.01.2025 14:29

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

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


          1. SIISII
            01.01.2025 14:29

            Их вызовы выглядят подобно вызовам функций, но не более того. Собственно, компилятор и обычные функции, тела которых ему доступны, может встроить (причём, кажется, уже без всякого явного указания inline).


      1. fshp
        01.01.2025 14:29

        Иногда компилятор все же умнее

        https://godbolt.org/z/Tc5aerW7a


      1. emusic
        01.01.2025 14:29

        вызов каждого интринсика это еще и накладные расходы на подготовку регистров с операндами

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

        или стекового кадра

        Зачем для intrinsic стековый кадр? У ARM есть команды, работающие только над кадром?


  1. VT100
    01.01.2025 14:29

    Сдаётся мне, что контроллер [дисплея] имеет возможность аппаратно настраивать порядок связи пикселей с битами.

    Шрифт - вырвиглазомозг, 145%.

    P.S. Не рассмотрен ещё "алгоритмический трюк" по реверсу битов вместо таблицы. Не разбирался, что там за магия с константами, но он - работает.


    1. vadjuse Автор
      01.01.2025 14:29

      Специально ещё раз посмотрел датащит на ssd1309 и там есть только ремап колонок, но не битов относительно пикселей.

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

      И да, шрифт на 145% вырвиглазмозг в первые пять минут работы, а потом нормально адаптируется и совершенно не замечает.


      1. VT100
        01.01.2025 14:29

        Нуштош... раз нет ремапа - напомню книгу Уоррена "Алгоритмические трюки". Для тех, у кого rbit'а нет.


        1. vadjuse Автор
          01.01.2025 14:29

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

          А я так и написал, что хочу про ассемблер.


  1. HardWrMan
    01.01.2025 14:29

    RBIT - берет слово и возвращает за один цикл процессора число с обратным порядком битов.
    REV - берет слово и возвращает за один цикл процессора слово с обратным порядком байтов.

    Касаемо REV я понимаю: можно менять тупоконечный формат на остроконечный и обратно. Касаемо RBIT не понятно: у вас по тексту меняется порядок битов в слове, т.е. D0 обменивается с D31 и т.д., но тогда последующий REV как бы и не нужен. Хотя вот в доке так описано:


  1. AbitLogic
    01.01.2025 14:29

    Меня как то занимал вопрос почему в x86 на асме есть инструкции ror/rol, даже в Паскале есть, а в С/С++ нет и нужно корячиться с двумя сдвигами и лог.сложением


    1. HardWrMan
      01.01.2025 14:29

      И к какому выводу вы пришли?


      1. AbitLogic
        01.01.2025 14:29

        Если использовать конструкцию в виде одной длинной строки, то компилятор Clang всё таки её превращает в rol/ror, поэтому просто имею файл с набором подобных макросов

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


        1. HardWrMan
          01.01.2025 14:29

          Что касается циклического сдвига в ЯВУ вообще прям беда да. Только арифметические.


          1. AbitLogic
            01.01.2025 14:29

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

            Впрочем нужно шагать в ногу со временем, в Rust таки эти операции появились, пусть через метод, но всё же в явном виде