среда, 21 сентября 2022 г.

GF2p8 Кодирование Рида-Соломона в изоморфном представлении поля

Коды Рида-Соломона считаются через экспоненты и логарифмы по таблице. А у нас с внедренеием инструкций Intel GFNI появилось две другие возможности: использовать инструкцию умножения и аффинные преобразования.

Для примера я взял коды РС из проекта библиотеки генеации QR-кодов. Там используется полином поля G(2^8) P = 0x11D. Кодирование производится в цикле. При переходе к операциям умножения в поле 0x11D, цикл кодрования выглядит так.


	for(i = 0; i < data_length; i++) {
		uint8_t fb =  data[i] ^ Z[0];
		for(j = 0; j < ecc_length-1; j++) {
			Z[j] = Z[j+1] ^ gf2p8_mul(fb, g[j], 0x11D);
		}
		Z[j] = gf2p8_mul(fb, g[j], 0x11D);
	}
	for(i = 0; i < ecc_length; i++) {
		ecc[i] = Z[i];
	}

Преобразование алгоритма для расчета в изоморфном представлении поля с образующим полиномом 0x11B:


	const uint64_t M = 0xFFAACC88F0A0C080;
	const uint64_t Mr= 0xFFAACC88F0A0C080;

	for(i = 0; i < data_length; i++) {
		uint8_t fb =  affine(M,data[i]) ^ Z[0];
		for(j = 0; j < ecc_length-1; j++) {
			Z[j] = Z[j+1] ^ gf2p8_mul(fb, g[j], 0x11B);
		}
		Z[j] = gf2p8_mul(fb, g[j], 0x11B);
	}
	for(i = 0; i < ecc_length; i++) {
		ecc[i] = affine(Mr, Z[i]);
	}

Данное преобразование предполагает так же изоморфные преобразования компонент вектора генератора кодов (g). Векторизация алгоритма может быть выполнена по вложенному циклу.

[1]The comeback of Reed Solomon codes N.Drucker, Sh.Gueron, V.Krasnov

Приложение

Довеском идет код для расчета остатка от деления на 255. Когда в контроллере нет полиномиального умножения, приходится использовать способ расчета умножения и инверсии с использованием таблиц логарифмов и экспонент.
// Multiplication in Galua Fields
int gf_mul(int x, int y) {
    if (x == 0 || y == 0) return 0;
    return exp_[(log_[x] + log_[y])%255];
}
// Inversion in Galua Fields
int gf_inv(int x) {
    return exp_[255 - log_[x]];
}
// Division in Galua Fields
int gf_div(int x, int y) {
    if (x == 0) return 0;
    return exp_[(log_[x] + 255 - log_[y]) % 255];
}
Самая неудобная операция в этом случае - вычисление остатка от деления на 255. Избавился от деления.
// Остаток от деления на 255
uint8_t mod255(unsigned v) {
	uint32_t q = (v*0x1010102UL)>>(24);
	return q;
}
// Остаток от деления на 255
uint8_t mod255(unsigned v) {
	return v + (v>>8);
}

А этот вариант самый простой, применим для сложения логарифмов.

Такая вот оптимизация.

[2] Исходники на Github

вторник, 20 сентября 2022 г.

Крипто-Рубль. Изоморфные преобразования при вычислении Хеш функции Stribog

Для расчета хеш функции ГОСТ Р 34.11-2012 "Stribog" выполняется табличная подстановка в цикле - это общепринятая практика.

Если вернуться к определению алгоритма, его описывают последовательностью матричных операций S, P, L, X:
S - подстановка Sbox для каждого элемента 8x8 байт.
P - транспонирование матрицы 8х8 байт
L - линейное преобразование.
X - исключающее ИЛИ.

Линейное преобразование может быть представлено операцией умножения матрицы коэффициентов 8x8 на матрицу состояния 8x8 в поле GF(2^8) с полиномом 0x171.

LM = {
0x71, 0x05, 0x09, 0xB9, 0x61, 0xA2, 0x27, 0x0E,
0x04, 0x88, 0x5B, 0xB2, 0xE4, 0x36, 0x5F, 0x65,
0x5F, 0xCB, 0xAD, 0x0F, 0xBA, 0x2C, 0x04, 0xA5,
0xE5, 0x01, 0x54, 0xBA, 0x0F, 0x11, 0x2A, 0x76,
0xD4, 0x81, 0x1C, 0xFA, 0x39, 0x5E, 0x15, 0x24,
0x05, 0x71, 0x5E, 0x66, 0x17, 0x1C, 0xD0, 0x02,
0x2D, 0xF1, 0xE7, 0x28, 0x55, 0xA0, 0x4C, 0x9A,
0x0E, 0x02, 0xF6, 0x8A, 0x15, 0x9D, 0x39, 0x71,
Пояснение, как оно соотносится с с матрицей A. Колонка 1 матрицы M - это элемент A[0] = 0x8e20faa72ba0b470,
для которого выполнен разворот порядка следования бит.
8E=>71 20=>04 FA=>5F
Колонка 2 матрицы M - это элемент A[8],
. . .
Колонка 8 матрицы M - это элемент A[56],

Я независимо вывел эту таблицу, из представленнй таблицы подстановки. О возможности подобного представления читал в статье автора [1]. Продемонструю, как выполнить декомпозицию таблиц. Берем два числа: первоя строка, вторая и третья колонка:
A[1] = 0x47107ddd9b505a38
A[2] = 0xad08b0e0c3282d1c
Выделяем по маске 7 бит в каждом байте (A[1] & ~0x0101010101010101) >> 1)^A[2] видим числа 0x8E008E8E8E000000. В каждом байте стоит полином, по которуму производится редуцирование 0x171, с отраженным порядком бит будет 0x8E. Перед нами просто таблица умножения в поле GF(2^8)/[0x171] с отраженным порядком следования бит в байте.

Линейное преобразование может быть представлено таблично.
Cвойство L(a+b) = L(a) + L(b), как и свойство L(a*b) = L(a)*L(b) - действует и для таблицы.

Мы рассматриваем возможность реализации без таблицы, с использованием афинных преобазований и операци умножения в поле GF(2^8). Операция умножения может быть выполнена с использованием векторных инструкций умножения без переносов (Intel), полиномиального умножения (Arm Neon). Афинные преобразования могут выполняться с использованием векторных инструкций без обращения к таблицам.

Оптимизация под векторные инсрукции Arm Neon. 16 байт составляют вектор, используя вектора можно в два действия эмулировать работу операции умножения на константу. Для эмуляции используется свойство линейности преобразования.

Оптимизация под GF-NI:

Парадокс в том, что отечественные алгоритмы Stribog и Kuznyechik можно считать с использованием американской криптографии. Американская криптография использует умножение в поле GF(2^8) по полиному 0x11B. Отечетсвенная криптография использует умножение с редуцированием по полиному 0x171 в алгоритме Stribog, 0x1C3 в алгоритме Kuznyechik. Можно использовать афинные изоморфные преобразования, чтобы отобразить все матрицы и коэффициенты для использования умножения в поле 0x11b.

Матрицы изоморфного преобразования мы научились находить[2]. Для преобразования 0x171=> 0x11B можно использовать матрицы

матрицы изоморфного преобразования 171 => 11B "Stribog"
171=>11B 02 0E M =49A24EE22C984CF0 M-1 =FB44BAF0CC5A0A1C
171=>11B 02 17 M =C30E560C5240D828 M-1 =6D0A141C3A9C2046
171=>11B 02 52 M =B1A27CD0765CD234 M-1 =F5481A5CBE24D86E
171=>11B 02 54 M =29903A0892D47ABC M-1 =E5126608F2EC44F0
171=>11B 02 A0 M =C154448014AEDCC6 M-1 =1B8C164A06F81208
171=>11B 02 B4 M =1590FECC3E8E8CE6 M-1 =9960C6DA5E32485C
171=>11B 02 F6 M =090EFAA096722E1A M-1 =6FD8B46E36428C4A
171=>11B 02 FD M =A7541EDA2602426A M-1 =CB20B646D48660DA
Изоморфное представление матрицы LM в поле 0x11B:
unsigned char LM_11b = {
	0xF6,0x55,0x74,0xE4,0x56,0x3E,0xC1,0x2F,
	0x54,0xDF,0x17,0x9E,0xA9,0x60,0x43,0x02,
	0x43,0x1D,0x10,0x2E,0xEB,0xBB,0x54,0x65,
	0xA8,0x01,0x39,0xEB,0x2E,0xA1,0xE1,0xAD,
	0x93,0xAB,0x81,0x26,0x4E,0x42,0xF5,0xCE,
	0x55,0xF6,0x42,0x0D,0xFB,0x81,0xC7,0x0E,
	0xBA,0x5C,0xA6,0xEF,0x38,0x30,0xEC,0x71,
	0x2F,0x0E,0x07,0xD1,0xF5,0x2A,0x4E,0xF6,

Необходимо таже преобразовать таблицу Sbox в изоморфное представление Sbox_iso = MR·S·RM-1, где MR и RM-1 - матрицы прямого и обратного изоморфного преобразования из поля образованного полиномом 0x171 в поле 0x11b. R - отражение порядка следования бит.


void SPLX_11b(const uint8_t* s, uint8_t* r)
{
	int i,j,k;
	for (i=0; i<8; i++)
	for (j=0; j<8; j++) {
		uint8_t v= 0;
		for (k=0; k<8; k++) 
			v ^= gf2p8_mul(LM_11b[j*8+k], Sbox_iso[s[k*8+i]]);
		r[i*8+j] ^= v;
	}
}

Полученный алгоритм выполняет операции S,P,L,X. В цике выполняется операция умножения в поле GF(2^8)/0x11B. Оптимизация под ARM Neon может использовать полиномиальное умножение с последующим редуцированием.

Я озаглавил сообщение "Крипто-рубль..". Все алгоритмы добычи крипто-валют основаны на вычислении хеш-функций, этот алгоритм удалось выполнить исключительно в 8-битных числах. Это открывает возможность векторизовать алгоритм для параллельного вычисления до 64 хешей. Другая оптимизация - изоморфное преобразование в композитное поле позволяет получить эффективную реализацию алгоритма аппаратно в ASIC или на FPGA.


[1] Algebraic Aspects of the Russian Hash Standard GOST R 34.11-2012, O. Kazymyrov, V. Kazymyrova

среда, 24 августа 2022 г.

Представление системного времени

При вычислении интервалов времении, в POSIX и С11 используется структура timespec (секунды, наносекунды), получаем такую проверку.

ts.tv_nsec+=timeout;
if (ts.tv_nsec>=1000000000){
  ts.tv_nsec  = ts.tv_nsec % 1000000000;
  ts.tv_sec  += ts.tv_nsec / 1000000000;
}

В этом методе есть недостаток. Мы не можем применять такой подход для контроллеров. На архитектуре типа Cortex-M23 нет деления. Эмуляция деления будет вычисляться удивительно неэффективно. Надо исключить операцию деления и операцию взятия остатка от деления.
Идеально было бы исключить и проверку, чтобы код стал линейным.

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

Есть способ проведения вычислений с двоично-десятичными числами (Binary-coded decimal, BCD), когда при сложении выполняется коррекция операции сложения - добавление константы, чтобы заработать перенос между разрядами десятичного числа. В случае двоично-десятичных чисел к десятичному разряду добавляется дополнение (число 6), если разряд больше 9. Число 6 получается, как инверсия (0x9^0xF) или в нашей математике (16-10). Аналогично мы расчитываем дополнение для миллиарда в двоичном представлении ((1<<30) - 1000000000) или (0x3B9ACA00^0xFFFFFFFF).

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

// коррекция сложения интервалов времени
if ((uint32_t)t64 > 999999999UL) t64 += (uint32_t)~999999999UL; 

Полусумматор и входящий перенос

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


ci = carry_in -- входящий перенос: 0 или 1
s  = a^b^ci    -- результат сумма разрядов
сo = ci? (a|b) : (a&b); -- исходящий перенос

Нас интересуют входящие переносы (сi) по маске двоично-десятичного числа 0x11111110 при сложении с дополнением (a+b+D). Сложение с дополнением формирует правильную цепочку переносов между десятичными разрядами. При этом коррекция результата сложения требуется, если между десятичными разрядами был перенос.

s1 = a+b; -- формирует правильные результаты в каждом разряде, если переноса не было,
s2 = a+b+D; -- формирует правильную цепочку переносов между разрядами.
s = co? s2: s1; -- коррекция результата сложения

Расчет BСD времени в упакованном формате
struct _BCD_Time {
  unsigned int susec:8;   //сотые
  unsigned int   sec:8;   //секунды
  unsigned int  mins:8;   //минуты
  unsigned int  hour:8;   //часы
}; 

#define BCD_TIME_CORRECT 0x66A6A666UL 
#define BCD_TIME_CARRYIN 0x11111110UL
time_add(a, b) {
  s1 = a + b;
  s2 = a + b + BCD_TIME_CORRECT;  // коррекция BCD для вычисления переносов
  m  =(a ^ b ^ s2) & BCD_TIME_CARRYIN; // из переносов делаем маску
  m  = m - (m>>4); // маска для выделения десятичных разрядов
  return s1 + (m & BCD_TIME_CORRECT); // коррекция сложения
}

Вот в таком виде можно применять к вычислению времени и временных интервалов независимо от архитектуры ядра процессора.


вторник, 23 августа 2022 г.

Осваиваем GigaDevice GD32

Мы делаем свою операционную систему... которая поддерживает функционал C11 и собирается с использованием GCC.
GigaDevice не помог с этим. Загрузчик и примеры GD ориентированы на Keil и JAR. Постараюсь конспективно изложить, что нужно для разработки HAL с нуля и как действовать при виде нового контроллера с несовместимым API.

1.2 Индентификация системы

Порядок индентификации: определить ядро процессора (SCB_CPUID), определить к какому семейству контроллеров относится, размер памяти и уникальный идентификатор. При прочих равных в плату можно запаять совместимые по выводам контроллеры нескольких производителей. Устройство должно выдавать отладочную информацию: микроконтроллер и размер памяти. Это необходимо для внутрисхемной диагностики и обслуживания, внутрисхемной прошивки.
Идентификация ядра CPUID_CORE_ID BITS(4,15):
	CORTEX_M0   = 0xC20, ARMv6-M (0xC)
	CORTEX_M0P  = 0xC60, ARMv6-M
	CORTEX_M1   = 0xC21, ARMv6-M
	CORTEX_M3   = 0xC23, 
	CORTEX_M4   = 0xC24,
	CORTEX_M7   = 0xC27, ARMv7-M (0xF)
	CORTEX_M23  = 0xD20, ARMv8-M baseline (0xC)
	CORTEX_M33  = 0xD21, ARMv8‑M with Main Extension (0xF)
	CORTEX_M35P = 0xD31,
	CORTEX_M55  = 0xD22,
	CORTEX_M85  = 0xD23, ARMv8.1‑M with Main Extension (0xF)
Идентификация контроллера начинается с выбора адреса регистра отладки DBG/DBGMCU. Адрес регистра отладки зависит от ядра микроконтроллера.
ID code register (DBG_ID)
DBGMCU_IDCODE    0xE004 2000
DBG_ID_GD32E10X  0xE004 2000
DBGMCU_IDCODE_G4 0xE004 2000
DBGMCU_IDCODE_F7 0xE004 2000

DBGMCU_IDCODE_L0 0x4001 5800
DBG_ID_GD32E23X  0x4001 5800
DBGMCU_IDCODE_G0 0x4001 5800

DBGMCU_IDCODE_L5 0xE004 4000
DBGMCU_IDCODE_U5 0xE004 4000
DBG_ID_GD32E50X  0xE004 4000

DBGMCU_IDCODE_H7 0x5C00 1000
Глядя на таблицу, кажется адреса архитектурно зависимы. Если ядро Cortex-M0 или Cortex-M23 -- один адрес, если CM3/CM4 - другой, CM33.. . Размер флеш памяти определяется в зависимости от производителя и архитектуры, см. Memory density information. Для контроллеров GD32 находим два регистра...
Base address: 0x1FFF F7E0
SRAM_DENSITY 	BITS(16,31)
FLASH_DENSITY	BITS( 0,15)
Уникальный идентификатор Unique device ID (96 bits)

MCU         CPUID  DEV_ID  REV_ID 	UNIQUE_ID    MEM.DENSITY
GD32F1X0	0xC23  0x410   0x1303	0x1FFF F7AC  0x1FFF F7E0
GD32F10X	0xC23  0x410			0x1FFF F7E8  0x1FFF F7E0
GD32E10X	0xC24  0x410   0x1712	0x1FFF F7E8  0x1FFF F7E0
GD32F3X0	0xC24  0x410   0x1704	0x1FFF F7AC  0x1FFF F7E0
GD32F30X	0xC24					0x1FFF F7E8  0x1FFF F7E0
GD32F4XX	0xC24					0x1FFF 7A20  0x1FFF 7A10
STM32F75X		   0x449			0x1FF0 F420  0x1FF0 F442
STM32F72X          0x452            0x1FF0 7A10  0x1FF0 7A22
GD32E23X	0xD20  0x410   0x1909	0x1FFF F7AC  0x1FFF F7E0
GD32E50X 	0xD21  0x444			0x1FFF F7E8  0x1FFF F7E0
STM32U5xx   0x	   0x482   0x2001   0x0BFA 0700  0x0BFA 07A0
GD32VF10X                   		0x1FFF F7E8  0x1FFF F7E0

Часть 1.3. Программатор

Попробовал использовать ST-Link/V2 Programmer для прошивки GD32E10X -- работает, размер флеша определился корректно. Ревизия не известная, пишит, что найден "STM32F103 medium-density". Для GD32E23X и GD32E50X ST-Link не подходит. Прошивалку делаем из отладочной платы GD32E103R. Дополнительно изучаем тему изготовления CMSIS-DAP/DAPLink адаптера согласно рекомендациям и спецификациям ARM CMSIS-DAP.

Что-то работает не корректно. Основная проблема идентификации, что не распознаются признаки защиты памяти, OptionBytes -- выставление опций вызвает непоправимые изменения. Программатор должен знать, где располагаются, как и за что отвечают OptionBytes. Осознание проблемы приводит к некоторой модели идентификации контроллера программатором. У программатора должна быть база известных контроллеров, со своими адресами для идентификации. Идентификация начинается с CPUID, далее необходимо распознать опциональное оборудование ядра CoreSight, далее распознается DEV_ID и REV_ID. По трем параметрам (CPUID,DEV_ID,REV_ID) находим в базе микроконтроллер. Далее находим файл конфигурации периферии, например в формате CMSIS-SVD. ARM предлагает производителю контроллера cтандартный подход для встраивания поддержки в свою экосистему. Этот же подход нужен для создания инструмента программирования контроллеров.

  • Создание файлов начальной загрузки startup_{device}.s -- Reset_Handler и таблица векторов прерываний.
  • Создание файлов system_{device}.c/h -- начальная конфигурация контроллера
  • Создание файла описания аппаратуры в формате SVD и генерация на его снове заголовков, используется утилита для генерации кода. Это позволяет в какой-то мере унифицировать HAL
  • Описание и распознавание средсв отладки CoreSight встроенных в контроллер.
  • Описание конфигурации флеш-памяти: размер памяти, размер сектора, разрядность и быстродействие операций.
  • Создание и компиляция алгоритма прошивки флеш памяти, состоящего из ряда вызовов: EraseSector, ProgramPage, Verify..[CMSIS-Pack]. Скомпилированный код помещается в оперативную память и испольняется в режиме отладки. Функция программатора сводится к загрузке бутлодера и фрагментов прошиваемого кода в оперативную память и манипуляция точками запуска и останова.

Часть 1.4. Пакет описания аппаратуры DFP

Дополнительно можно изучить тему где взять подобные описания. Я нашел источник информации, помимо библиотек в пакетах DFP от производителя. Технология создания пакета описана на в стандарте CMSIS-Pack и поддерживается производителями контроллеров, см. Open-CMSIS-Pack.

К сожелению, поддержка пакетов CMSIS-Pack не является первичной. Пакеты Firmware Library обновляются раньше чем пакеты CMSIS-Pack и содержат более доставерную информацию об аппаратуре, они не полные, не сождержат описания сегментов памяти OptionBytes. Однако, это тот путь, который позволяет выполнять генерацию кода по описанию аппаратуры и может использоваться при отладке.

Структура директории

  • Device/include/{CHIPX}.h -- описание аппаратуры
  • Flash/{CHIPX}.flm -- бутлоадеры, elf формат
  • svd/{CHIPX}.svd -- описание аппаратуры в формате CMSIS-SVD

Часть 1. Нужно создать совместимый Sturtup

C учетом правил сборки проекта и рекомендаций ARM у нас должно образоваться несколько файлов с определенным именованием.
  • Device/source/gcc/gd32e10x.ld -- правила линковки. Скрипт включает размещение сегментов памяти и карту памяти. В тоже время генерация скрипта возможна по шаблону: перечислить встроенную память.
  • Device/include/gd32e10x.h -- описание периферии. Стараемся придерживаться исходников производителя. В этом файле только базовые адреса устройств.
  • Device/source/gcc/startup_gd32e10x.s -- вот этот файл линкуется первым. Он на асемблере, содержит таблицу векторов прерываний и инициализацию переменных. В нем выполняется инициализация сегментов памяти RAM data (заполнение начальными значениями) и bss (обнуление). Код производит два вызова: низкоуровневую настройку периферии (low_level_init), без которой операционка не может стартовать и после этого запускает основную функцию (main) или загружает модули ядра операционной системы. В нашем случае до функции main выполняется старт операционной системы, которая после запуска и инициализации мультизадачности, инициализации драйверов, запутит main.
  • Device/source/system_gd32e10x.c (low_level_init) -- содержит настройку частоты ядра, ресет оборудования, вызвает начальную конфигурацию периферии, вызывает настройку работы внешней памяти, если есть.
  • Кое-что еще нужно для нормальной работы программ на Си, не очевидное -- статические конструкторы и деструкторы, нужно запустить все конструкторы по списку. Мы пишем код на Си, но поддержка статический конструкторов нужна для функционала модулей ядра операционной системы. Инициализацию памяти и перемещение сегментов мы выполняем до запуска кода, в коде начальной загрузки.

Области памяти .bss, .data и таблица статических конструкторов определяеся в линкерном скрипте. В линкерном скрипте задается также размер памяти и указатель стека.

Совместимый startup это всегда один и тот же код, для Cortex-M. Отличием является таблица векторов, которая своя для каждого микроконтроллера. Метод создания - скопировать код, скопировать таблицу векторов от производителя и подправить синтакстис ассемблера, чтобы нравился GCC.

Часть 2.

Нужно создать код низкоуровневой инициализации (low_level_init). Пример нужно взять из файла startup_gd32e10x.c от производителя, но дописать и подправить придется. Тут всегда один стандартный вызов SystemInit(). SystemCoreClock -- глобальная переменная, на выходе инициализации должна содержать частоту ядра -- это требование совместимости с CMSIS-RTOS.

Часть 3.

Где-то между инициализацией периферии до запуска прикладных программ нужно настроить по списку все ноги, порты ввода-вывода. Это вероятно происходит до запуска main, до операционной системы. В нашей концепции все ноги по списку должны быть настроены до запуска приложения. Приложение может менять настройки периферии, но все выводы и порты должны быть в определенном состоянии на момент включения и на момент выключения. Нужно описать ноги согласно принятому API, в совместимом виде, чтобы один и тот же код можно было перенести между контроллерами разных произовдителей. Понятно что настройка происходит иначе, но описание ног должно быть унифицировано меду контроллерами разных производителей. Для этого надо составить файл hal/gpio.h и hal/gpio.c. Описание таблицы ног производится в заголовке board.h. Мы определяем ноги, группы ног по портам, с атрибутами: PIO_ANALOG, PIO_INPUT, PIO_OUTPUT, PIO_OPENDRAIN и т.д. Сложность в том, чтобы использовать одинаковый набор атрибутов на разных платформах, невзирая на то, что на одной нужно использовать Remap, а на другой - нет.

Часть 4.

Надо описать хотя бы один стандартный последовательный порт, который служит для отладки. На STM32 мы используем SWO для вывода отладочной информации. Тут этого нет. Но надежда умирает последней я расчитываю найти SWO даже если про него ничего не говорят. Признаком того, что SWO реализован аппаратно, является ITM (Instrumentation Trace Macrocell) и возможность настроить TRACE через DBGMCU.

Среди настроек регистра DBG_CTL находим TRACE_IOEN (включение трассировки) и TRACE_MODE=0 (Asyncronous).

Если в контроллере все же отсутсвует ITM и поддержка SWO, изучаем возможность использовать UART вместо SWO. Обращаю внимание, что аппаратура трассировки через SWO активируется со стороны программатора, программатор может не поддерживать канал отладки.

Первое что нужно при запуске нового контроллера - помыргать лампочкой, изменить логический урвоень на любой тестовой точке. Нам нужен хоть какой-то терминал, который покажет, что устройство загрузилось и работает. Это может быть последовательный порт типа UART, одноногий дебаггер -- осциллограф или логический анализатор. Теримнал и канал отладки (трассировки) - это функция операционной системы. Мы поддерживаем два режима трассировки. В одном режиме исполнение кода приостанавливается пока не выполнится вывод сообщения (debug) - блокирующий вызов, этот режим нужен для отладки драйверов и обработчиков прерываний. Второй режим -- ассинхронный (printf, puts), вывод отладочной информации не должен препятствовать работе прикладной программы, без блокировки, без задержки -- вывод сообщений выполняется в фоновом процессе.

Часть 5. Перенос ядра RTOS на иную архитектуру

Завязок на аппаратуру в операционной системе не так уж много, но они очень существенные. От этой зависимости можно абстрагироваться.

  1. SVC -- системное прерывание, обращение к системной функции. Кусочек кода на ассемблере.
  2. ContextSwitch -- выполняется при обращении к PendSW прерыванию. Так происходит на всех ядрах Cortex. В разных моделях различным образом используется стек при входе и выходе из прерывания. Есть модели с защитой памяти, с поддержкой FPU, с ленивыми переключениями, с поддержкой двух стеков MSP PSP, да и система команд зачастую различаются.

Таким образом, для нового контроллера надо написать две фукнции: SVC_Handler и PendSW_Handler. Еще есть функция, связанная с механизмом переключения задач, -- заполнение стека при запуске нового треда. Заполнение в частности включает EXEC_RETURN -- флаги способа возврата из обработчика прерывания. Эти флаги - псевдо-адрес возврата сообщают ядру, как восстановить стек. Количество флагов и значение меняется в разных ядрах Cortex, так что заполнение стека является аппаратно-зависимой фукнцией. В нашей реализации аппаратно зависимым от архитектуры оказался именно набор флагов EXEC_RETURN.

Ниже привожу примеры реализации обработчика системного вызова

SVC_Handler:
   mrs  r0, psp
   ldr  r1, [r0, #24]
   ldrb r1, [r1, #-2]
   ldr  pc, [r3, r1, LSL #2]

В нашей операционке SVC вызывается только из прикладных задач (Thread mode), в Handler mode вызовы SVC не используются. Код обработки SVC запросов не меняется для архитектур armv7m-e и armv8m.main, а вот ядро cortex-m23 (архитектура armv8m.base) такие инструкции со сдвигами и отрицательными смещениями не поддерживает, нужна альтернативная реализация.

Переключение c основного стека (Main Stack Pointer, MSP) на стек прикладных программ (Process Stack Pointer, PSP) происходит в процессе начальной загрузки. Мы выполняем это переключение в ассемблерном коде, чтобы исключить использование стека до момента переключения.

// переключение стека с MSP на PSP
	mov r0, sp
	msr psp, r0;
    ... настройка размера стека
	mrs r0, control;
	orr r0, #2
	msr control, r0
	sub sp, #256

Основной чертой нашей операционки должна быть лаконичность. Мы хотим минимизировать задержку на обработку данных. Если получен пакет данных в драйвере, то обработка в приложении должна произойти с минимальными и фиксированными задержками на переключение задач.

В обработчике прерывания мы принимаем решение о переключении задач. Переключение задач происходит путем вызова программного прерывания PendSW следом за обработчиком аппаратного прерывания, выставляем флаг с использованием инструкции __YIELD().

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

PendSW_Handler: // переключение задач
   push {r4-r11,lr}
   . . .// сохраняем контекст (sp) и переходим к следующей задаче
   pop  {r4-r11,pc}

В этом примере переключение стеков (контектсорв задач) очень лаконично сформулировано. Однако, не все так гладко происходит на смом деле. На самом деле, находясь в прерывании PendSW мы используем Handler Stack Pointer (MSP), а возврат происходит в Thread mode с использованием Process Stack Pointer (PSP)

PendSW_Handler: // переключение задач
   mrs   r0, psp
   stmdb r0!, {r4-r11,lr}
   . . .// сохраняем контекст (r0) и переходим к следующей задаче
   ldmia r0!, {r4-r11,lr}
   msr	 psp, r0
   bx    lr

Далее мы усложняем процесс переключения, если используется FPU, и если используется исключение безопасности. Для каждого ядра и каждого варианта оборудования, с FPU, c MPU, c Security Extension возникает необходимость дописать или переписать код переключения контекстов задач.


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

Программатор DAP-Link для прошивки и тестирования Хеш-плат

Программатор

Новый контроллер -- это, возможно, и новый программатор. Все совместимые программаторы строятся на референсном коде ARM CMSIS-DAP/DAP-Link. Ядро ARM содержит CoreSight отладчик с поддежкой JTAG или SWD. GD32E103 определяется как STM32F103 и может прошиваться программатором STM ST-Link. А вот модели контроллеров GD32E23x и GD32E50x определяются с ошибкой и не шьются. Программатор GD-Link-OB содержится на отладочной плате. Программатор для GD32E23x и GD32E50x мы сделали из стартового набора GD32E103R-Start, отпаяв от платы отлаживаемый контроллер. Вероятно, вам удасться, где-то заказать GD-Link-S или GD-Link-Light. Говорят, можно использовать nanoDAP или еще какой-то CMSIS-DAP/DAP-Link эмулятор. Я прикинул, заказ программатора - минимум неделя. Заказал в производство собственный программатор совместимый с GD-Link-OB...

воскресенье, 19 июня 2022 г.

Расчет CRC-5 и CRC-24 в контроллерах GigaDevice

Аппаратно контроллер GigaDevice GD32E23X или GD32E50X может расчитывать 8, 16 и 32 битные полиномы. А какже расчитать аппаратно полиномы другой размерности? Для нас актуально расчитывать в контроллерах CRC-5 и CRC-24.

Привожу решение...


#define CRC5BTM_POLY 0x05
uint8_t crc5_btm(uint8_t crc, uint8_t *data, size_t data_len) 
{
	CRC_IDATA = (uint32_t)(crc<<3);
	CRC_CTL   = CRC_CTL_PS_8|CRC_CTL_RST;// REV_I REV_O
	CRC_POLY  = (uint32_t)(CRC5BTM_POLY<<3);
	int i;
	for (i=0; i< data_len; i++) 
		REG8(CRC) = data[i];
	return REG8(CRC)>>3;
}

Я выравниваю разрядность сдвигом, CRC5 считается в старших разрядах 8-битного регистра. Точно так же можно посчитать 24 битный CRC-24 с выравниванием на 32 бита.

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

воскресенье, 5 июня 2022 г.

Эмуляция инструкций CLZ и CTZ для Cortex-M23

Вылезла проблема эффективной реализации операции CLZ, потому что на ядре Cortex-M23 она не поддерживатся. В тоже время есть тонны кода для Cortex-M3/M4 с использованием инструкции CLZ.
Я изучил в доступных источниках и не нашел хорошей реалиазации. Мне нужна эффективная реализация этой операции, потому что на ней основана одна из основных функций -- выделение памяти и работа с флагами. Решил, что надо самому подобрать такую реализацию.

static	const uint8_t lut[16]= {4, 3, 2, 2, 1, 1, 1, 1};
int clz_u32(uint32_t a)
{
	int i = 28;
	if((a>>16)!=0) a>>=16,i-=16;
	if((a>> 8)!=0) a>>=8, i-=8;
	if((a>> 4)!=0) a>>=4, i-=4;
	return i + lut[a];
}
Еще одна реализация. Можно таблицу подстановки представить конструкцией switch.

int _clz_u32(uint32_t r0)
{
	int i=28;
	uint32_t b;
	if ((b = r0>>16) != 0) r0=b, i-=16;
	if ((b = r0>>8 ) != 0) r0=b, i-=8;
	if ((b = r0>>4 ) != 0) r0=b, i-=4;
	switch(r0 & 0xF) {
	case 0: b=4; break;
	case 1: b=3; break;
	case 2 ... 3: b=2; break;
	case 4 ... 7: b=1; break;
	default:b=0; break;
	}
	return i + b;//lut[r0];
}
Дальше занялся исследованием кода, который производися компилятором (#gcc -O3 -S).
Руками выполнил оптимизацию за компияторм, чтобы код стал компактнее. Оптимизация - обращене к таблице подстановки с использованием инструкции ADR.
__clzsi2:
        movs    r3, #28 // i=28

        lsrs    r2, r0, #16
        cbz     r2, 1f
        movs    r0, r2
        subs    r3, r3, #16
1:
        lsrs    r2, r0, #8
        cbz     r2, 1f
        movs    r0, r2
        subs    r3, r3, #8
1:
        lsrs    r2, r0, #4
        cbz     r2, 1f
        movs    r0, r2
        subs    r3, r3, #4
1:
        adr     r2, 1f
        ldrb    r0, [r2, r0]
        adds    r0, r0, r3
        bx      lr
	.align 2
1:
	.byte 4, 3, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0
Это пожалуй самая эффективная реализация. Затем подобрал аналогичным образом реализацию для CTZ.

static	const uint8_t lut_ctz[16]= {4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0};
int _ctz_u32(uint32_t a)
{
	int i = 28;
	if((a<<16)!=0) a<<=16,i-=16;
	if((a<< 8)!=0) a<<=8, i-=8;
	if((a<< 4)!=0) a<<=4, i-=4;
	return i + lut_ctz[a>>28];
}
__ctzsi2:
        movs    r3, #28

        lsls    r2, r0, #16
        cbz     r2, 1f
        movs    r0, r2
        subs    r3, r3, #16
1:
        lsls    r2, r0, #8
        cbz     r2, 1f
        movs    r0, r2
        subs    r3, r3, #8
1:
        lsls    r2, r0, #4
        cbz     r2, 1f
        movs    r0, r2
        subs    r3, r3, #4
1:
        lsrs    r0, r0, #28
        adr     r2, 1f
        ldrb    r0, [r2, r0]
        adds    r0, r0, r3
        bx      lr
	.align 2
1:
	.byte 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0
[ARM DDI0550] ARM Cortex-M23 Processor Technical Reference Manual

пятница, 27 мая 2022 г.

CRC-15/CAN

Решил закрепить навык синтеза алгоритмов CRC с числом бит не кратным 8. Вот что получилось для CRC-15/CAN.
CRC-15/CAN
width=15 poly=0x4599 init=0x0000 refin=false refout=false xorout=0x0000 check=0x059e name="CRC-15"
Таблицы и коды при вычислении выравниваются по старшему биту.
// Табличный алгоритм CRC-15 для таблиц 16 значений
CRC16	CRC15_update(CRC16 crc, uint8_t val){
	crc<<=1;
	crc^= (val<<8);
	crc = (crc << 4) ^ CRC15_Lookup[(crc>>12) & 0xF];
	crc = (crc << 4) ^ CRC15_Lookup[(crc>>12) & 0xF];
	return (crc>>1);
}
// Табличный алгоритм CRC-15 для таблиц 256 значений
CRC16	CRC15_update(CRC16 crc, uint8_t val){
	crc<<=1;
	crc^= (val<<8);
	crc = (crc << 8) ^ CRC15_Lookup[crc>>8];
	return (crc>>1);
}

static const uint16_t CRC15_Lookup[] = {
0x0000, 0x8B32, 0x9D56, 0x1664, 
0xB19E, 0x3AAC, 0x2CC8, 0xA7FA,
0xE80E, 0x633C, 0x7558, 0xFE6A, 
0x5990, 0xD2A2, 0xC4C6, 0x4FF4,
. . .
};

понедельник, 23 мая 2022 г.

CRC-5/BITMAIN Алгоритм для расчета и проверки контрольных сумм

CRC-5 обнаружил в проекте cgminer в ужасной реализации. Вычисления производятся по битам и далеко не самым оптимальным образом. Решил разобраться, почему и что это вообще за CRC такой. Из кода выяснил, что алгоритм похоже использует полином x^5+x^2+1(0x05). В таблицах нашел алгоритм CRC-5/USB с полиномом 0x05. Это тот же полином, только не ясно, как сделать алгоритм одинаково эффективно работающий с байтами и битами. В коде встречаются варианты применения алгоритма как к битовым строкам так и байтам. Решение такое: расчитать таблицу умножения для полинома и использовать табличные подстановки. Чтобы "подобрать" алгоритм для CRC5 сначала реализовал расчет CRC-5/USB, потому что для него есть точное определение в базе и прверочные числа.
CRC-5/USB
width=5 poly=0x05 init=0x1f refin=true refout=true xorout=0x1F check=0x19 name="CRC-5/USB"
CRC-5/BITMAIN
width=5 poly=0x05 init=0x1f refin=false refout=false xorout=0x0 check=0x0F name="CRC-5/BITMAIN"
Формат записи характеристик алгоритма: разрядность, полином, маска инициализации, отражение бит на входе и выходе алгоритма, маска инверсии на выходе и контрольная сумма от строки "123456789". Данных по CRC-5/BITMAIN не было, я даже не был уверен что это возможно. Сразу не получилось подобрать алгоритм, потому что неясно было, как выполнять выравнивание данных при пяти битах. Я начал с реализации табличного алгоритма для CRC-5/USB. Сделал алгоритм с табицей 16 элементов (умножение на 4 бита).
 Таблица подстановки для CRC-5/USB
0x00, 0x16, 0x05, 0x13,
0x0A, 0x1C, 0x0F, 0x19,
0x14, 0x02, 0x11, 0x07,
0x1E, 0x08, 0x1B, 0x0D,
Структура алгоритма точно такая же, как и для CRC-8.

uint8_t    CRC5USB_update(uint8_t crc, uint8_t data) 
{
  crc = crc ^ data;
  crc = (crc>>4) CRC5USB_Lookup4[crc&0xF];
  crc = (crc>>4) CRC5USB_Lookup4[crc&0xF];
  return crc&0x1F;
}
Если таблица имеет размер 256 элементов, алгоритм еще упроститься

uint8_t    CRC5USB_update(uint8_t crc, uint8_t data) 
{
  crc = crc ^ data;
  crc = CRC5USB_Lookup[crc];
  return crc;
}
Для CRC-5/BITMAIN сгенерировал таблицу умножения 256 элементов (умножение на 8 бит). В таблице и в алгоритме число 5 бит выравниывается по старшему разряду. Алгоритм работает над байтами,

uint8_t    CRC5_update(uint8_t crc, uint8_t data) 
{
  crc = (crc<<3) ^ data;
  crc = CRC5_Lookup[crc];
  return (crc>>3);
}
Для реализации в микроконтроллере мы обычно используем маленькие табицы по 16 элементов

uint8_t    CRC5_update(uint8_t crc, uint8_t data) 
{
  crc = (crc<<3) ^ data;
  crc = (crc<<4) ^ CRC5_Lookup[crc>>4];
  crc = (crc<<4) ^ CRC5_Lookup[crc>>4];
  return (crc>>3);
}
Алгоритм CRC можно дробить на любое число бит 1...8.
// шаг алгоритма для 1..8 бит
crc = crc ^ data;
crc = (crc << bits) ^ CRC5_Lookup[crc>>(8-bits)];
Таким образом можно посчитать CRC5, если число бит в битовой строке не кратно восьми. Данные при этом должны выравниваться по старшему биту.
 Таблица подстановки для CRC5
0x00, 0x28, 0x50, 0x78, 0xA0, 0x88, 0xF0, 0xD8,
0x68, 0x40, 0x38, 0x10, 0xC8, 0xE0, 0x98, 0xB0,
0xD0, 0xF8, 0x80, 0xA8, 0x70, 0x58, 0x20, 0x08,
0xB8, 0x90, 0xE8, 0xC0, 0x18, 0x30, 0x48, 0x60,
0x88, 0xA0, 0xD8, 0xF0, 0x28, 0x00, 0x78, 0x50,
0xE0, 0xC8, 0xB0, 0x98, 0x40, 0x68, 0x10, 0x38,
0x58, 0x70, 0x08, 0x20, 0xF8, 0xD0, 0xA8, 0x80,
0x30, 0x18, 0x60, 0x48, 0x90, 0xB8, 0xC0, 0xE8,
0x38, 0x10, 0x68, 0x40, 0x98, 0xB0, 0xC8, 0xE0,
0x50, 0x78, 0x00, 0x28, 0xF0, 0xD8, 0xA0, 0x88,
0xE8, 0xC0, 0xB8, 0x90, 0x48, 0x60, 0x18, 0x30,
0x80, 0xA8, 0xD0, 0xF8, 0x20, 0x08, 0x70, 0x58,
0xB0, 0x98, 0xE0, 0xC8, 0x10, 0x38, 0x40, 0x68,
0xD8, 0xF0, 0x88, 0xA0, 0x78, 0x50, 0x28, 0x00,
0x60, 0x48, 0x30, 0x18, 0xC0, 0xE8, 0x90, 0xB8,
0x08, 0x20, 0x58, 0x70, 0xA8, 0x80, 0xF8, 0xD0,
0x70, 0x58, 0x20, 0x08, 0xD0, 0xF8, 0x80, 0xA8,
0x18, 0x30, 0x48, 0x60, 0xB8, 0x90, 0xE8, 0xC0,
0xA0, 0x88, 0xF0, 0xD8, 0x00, 0x28, 0x50, 0x78,
0xC8, 0xE0, 0x98, 0xB0, 0x68, 0x40, 0x38, 0x10,
0xF8, 0xD0, 0xA8, 0x80, 0x58, 0x70, 0x08, 0x20,
0x90, 0xB8, 0xC0, 0xE8, 0x30, 0x18, 0x60, 0x48,
0x28, 0x00, 0x78, 0x50, 0x88, 0xA0, 0xD8, 0xF0,
0x40, 0x68, 0x10, 0x38, 0xE0, 0xC8, 0xB0, 0x98,
0x48, 0x60, 0x18, 0x30, 0xE8, 0xC0, 0xB8, 0x90,
0x20, 0x08, 0x70, 0x58, 0x80, 0xA8, 0xD0, 0xF8,
0x98, 0xB0, 0xC8, 0xE0, 0x38, 0x10, 0x68, 0x40,
0xF0, 0xD8, 0xA0, 0x88, 0x50, 0x78, 0x00, 0x28,
0xC0, 0xE8, 0x90, 0xB8, 0x60, 0x48, 0x30, 0x18,
0xA8, 0x80, 0xF8, 0xD0, 0x08, 0x20, 0x58, 0x70,
0x10, 0x38, 0x40, 0x68, 0xB0, 0x98, 0xE0, 0xC8,
0x78, 0x50, 0x28, 0x00, 0xD8, 0xF0, 0x88, 0xA0,

среда, 11 мая 2022 г.

Хеш платы для Майнинга

С начала проекта мы наплодили множество версий хеш плат. Некоторые из них не нашли своего заказчика. Ищут.

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

Основные направления: 1) Совместимость. В совместимой версии платы мы оставляем то же количество чипов. 2) Производительность. Производительность оцениваем по числу гигахеш в секунду. 3) Эффективность.

S19 Hyd. Совместимые

Размер платы: 210мм х 96мм, 76 ASIC Bitmain BM1398, водяное охлаждение одностороннее. 1790 электронных компонент. 8100 контактных площадок, 4600 отверстий. Производительнсть 35-40 ТН/s

S19 Pro Hyd. Производительные

Размер платы: 210мм х 126, 114 ASIC Bitmain BM1398, водяное охлаждение двустороннее. 2290 электронных компонент. 10700 контактных площадок, 8400 отверстий. Производительнсть 50-60 ТН/s

S19 XP Hyd. Перспективные

Данные не раскрываются

L3+ Hyd. Экспериментальные

Размер платы: 150мм х 120мм, 96 ASIC Bitmain BM1485, водяное охлаждение. 1300 электронных компонент. 5660 контактных площадок, 8990 отверстий.

четверг, 2 декабря 2021 г.

Хеш-плата для майнера

Плата после Ковидных коникул и балансировки питания наконец показала стаблиьную работу. Выглядит это так...

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

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

Это самая ранняя ревизия платы, первая. Мы переделывали платы, чтобы побороть брак сборки. Оптимизировали работу сборочной линии.., выпустили платы на толстой фольге, изучали тепловизором разогрев элементов платы, изучали на рентгене качество пайки, оптимизировали технологию.

Результат скромный: 35ГигаХеш/сек с одной платы. Ожидали 40 ГигаХещ/сек. Надо оптимизировать питание на плате, чтобы добиться стабильной работы на большей частоте.

четверг, 18 ноября 2021 г.

ПЛК контроллер для гидравлики

Который месяц не можем обеспечить производство контроллерами. Заказал контроллеры STM32G0, потому что они есть на складах. Изначально проект выполнен под контроллер STM32F3, но из этой серии невозможно добыть ни одного контроллера по разумным ценам. Делаем контроллер управления гидравлической системой на 24 релейных выхода. Это позволяет реализовать различные гидравлические установки с насосами и цилиндрами. В настоящее время на них выпускается гидравлическая платформа с четрьмя лапами и телескопической мачтой. В проекте переработка платы под маслостанции различного применения. Маслостанции - это общее название включающее один или несколько насосов, управление приводами заслонок трубопроводов, и гидрораспределительными клапанами, датчики давления и температуры. Для примера - промывочная масловстанция, с переключением режимов и поддержкой температуры промывочной жидкости. Установка применяется для промывки трубопроводов и промышленных устсновок, выпускается (Гидромашцентр, GMC.SU).

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

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

среда, 29 сентября 2021 г.

Сборочное производство

Собрали линию на новой площадке. Принимаем заказы на автоматический монтаж.

В состав линии входят: трафаретный принтер, чип шутер с приставкой для паллет с процессорами, длинная печка на десять зон и станция контроля.

вторник, 21 сентября 2021 г.

Хеш плата для Майнинга

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

Я поставил рекорд по плотности монтажа, все сигналы и сила разведены по одной стороне. Искусство схемотехники!

Плата должна выдерживать токи в 40-60А через кристалл размером 8х8мм. Дорожки усилены медной полосой. Питание радается каскадом: ток через один каскад чипов питает следующий каскад.

Размер платы всего 210x90мм. Успейте купить!

среда, 1 сентября 2021 г.

Цифровая подпись ГОСТ -- быстрое редуцирование по модулю простого числа

Настал момент поделиться ноу-хау, как ускорять криптографию ГОСТ. Вернее одна маленькая деталь, но самая важная. Как быстро выполнять редуцирование - модуль простого числа. Как вообще строится цифровая подпись. Есть математика на бублике, в поле целых чисел по модулю простого числа. На этом строится и RSA и ECDSA и ГОСТ. Можно сказать модуль числа - это самая критическая операция - остаток от деления.

Любая операция на бублике: это умножение большого числа (256 или 512 бит) или сложение большого числа сопровождается последующем редуцированием(взятием по модулю).
Простые числа в криптографии часто выбирваются исходя из простоты восприятия человеком и с возможностью оптимизации вычислений, например в ГОСТ представлены простые числа вида P = 2^(n-1)+p0 и P = 2^n - p0 где n=256 или n=512 бит.

Все операции в поле выполняются по модулю. Например, умножение по модулю - это умножение и модуль числа.

// Умножение с редуцированием по модулю простого числа.
void bn_mulm(bn* r, bn* a, bn* b)
{
	int cy = bn_mul(r, a, b);
	bn_mod(r, cy, Prime);
}

Вычисление модуля можно выполнять оптом, т.е. сначала выполнить все операции умножения и сложения, а потом выпонить редуцирование результата по модулю. Но это может быть накладно с точки зрения числа операций умножения и сложения. Т.е. если на входе число 512 бит, то умножение таких чисел даст 1024 бита. И последующие операции нужно будет выполнять с разрядностью 1024 бита. По этой причине все алгоритмы стрим по принципу: умножение - редуцирование. Редуцирование надо выполнять после каждого умножения больших чисел.

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


Что в таком случае редуцирование?



Представим результат операции - как перенос (сx- число, которое вылезло за пределы 512 бит) и число X, которое осталось в пределах 512 бит {cx, x}. При этом число x имеет разрядность 512 бита, а перенос - 32 или 64 бита, зависит от архитектуры процессора, ориентируемся на 64 бита.

Наша задача сводится к тому, чтобы вычесть из числа {cx,x} простое число несколько раз, чтобы перенос стал нулевым. При этом остаток помещается в 512 бит, но может требовать дальнейших операций по редуцированию. Нам важно только одно - результат редуцирования помещается в заданные 512 бит. Операцию можно выполнить в два этапа.

Алгоритм 1. Быстрое редуцирование в поле P = 2^N - p0
1. {cy,x} : = {cx, x} - (2^512-p0)*cx;
В результате cy может быть равен нулю или единице.
2. if cy>0 then {cy,x} : = {cy, x} - P;

Чтобы это работало на P надо наложить некоторые ограничения p0 < 2^(n-m), где m - разрядность переноса.

Алгоритм можно упростить:

Алгоритм 1.1. Быстрое редуцирование P = 2^N - p0
1. {cy,x} : = x + p0*cx;
2. if cy>0 then x : = x + p0;

Быстрое редуцирование не является взятием по модулю. Редуцирование возвращает число в заданную разрядность 2^n. Операция взятия модуля выполняется всего один раз на множество операций, только при сравнении чисел.

Теперь рассмотрим второй случай, когда простое число представленно суммой P=2^(N-1) + p0, где N -- разрядность поля, 256 или 512.

Алгоритм 2. Быстрое редуцирование P = 2^(N-1) + p0
1. {cy,x} : = {cx, x} - 2*(2^511+p0)*cx = x - 2*p0;
В результате cy может быть равен нулю или (-1). 
2. if cy<0 then {cy,x} : = {cy, x} + P;

При значении cy == (-1) x находится в интервале [-2*p0; -1], в старшем бите единица (1).

Полагаю равносильно заменить P на втором шаге на 2P. Cуть операции редуцирования не меняется.

Алгоритм 2.1. Быстрое редуцирование P = 2^(N-1) + p0
1. {cy,x} : = {cx, x} - 2*(2^511+p0)*cx = x - 2*p0*cx;
В результате cy может быть равен нулю или (-1).
2. if cy<0 then {cy,x} : = {cy, x} + 2*P = x + 2*p0;

Отдельно рассматриваем редуцирование при сложении и вычитании

Алгоритм 3. Сложение с редуцированием
1. {cy, x} := a+b
В результате cy принимает значения 0 или 1.
2. if (cy>0) {cy,x} := {cy,x} - (2^N - p0) = x + p0
Может понадобится третий шаг, очень мало вероятно:
3. if (cy>0) {cy,x} := {cy,x} - (2^N - p0) = x + p0

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

// Алгоритм 4. Умножение с накоплением (со сложением)
uint64_t bn_mla_512(uint64_t* r, uint64_t *a, uint64_t d)
{
    unsigned __int128 ac = 0;
    int i;
    for (i=0;i<8; i++) {
        ac +=  (unsigned __int128)d*a[i] + (unsigned __int128)r[i];
        r[i] = (uint64_t)ac;
        ac = ac>>64;
    }
    return (uint64_t)ac;
}

Операция "Умножение с накоплением" используется в реализации Алгоритма 1.1, быстрого редуцирования.

// Алгоритм 5. Умножение с вычитанием
 int64_t bn_mls_512(uint64_t* r, uint64_t *a, uint64_t d)
{
    __int128 ac = 0;
    int i;
    for (i=0; i<8; i++) {
        ac += (unsigned __int128)r[i] - (unsigned __int128)a[i]*d;
        r[i] = (uint64_t)ac;
        ac = ac>>64;
    }
    return (int64_t)ac;
}

Операция "Умножение с вычитанием" используется в реализации Алгоритма 2.1, быстрого редуцирования.

Эффективность описания операции (Alg.5) сомнительна, в рабочем проекте я использую ассемблер. Данное описание мне понадобилось для отладки под GCC 10 на платформе Intel x86_64.

Сжатие методом Хаффмана (Huffman Codes)

На картинке изображен метод кодирования Хаффмана, предложенный еще в 1952 году, 70 лет назад. Глядя на эту схему, приходит в голову сортированный список или стек(приоритеная очередь) с сортировкой по весовому коэффциенту.
Алгоритм построения кодов состоит из двух этапов. На первом этапе мы составляем и сортируем очередь указанным на картинке способом. На каждом шаге создаем композитный элемент очереди, включающий ссылки на два последних элемента и тем самым формируем из списка дерево -- еще одна динамическая структура. На втором этапе совершаем обход дерева, степень вложенности элементов в дерево будет давать длину кода. Данный метод используется для вычисления длин кодов, одновременно с эти можно вычислять сами коды, приняв направление ветвления, право - лево за ноль или единицу. Расчет кодов предлагается выполнять методом обхода дерева, а это -- рекурсия.

Привожу таблицу выданную моей реализацией алгоритма, алгоритм составлен только из созерцания картиники. Как?! -- Очередь с приоритетами и построение дерева. Коды можно считать несколькими способами, один из которых считается Каноническим.

No |wght|bits|code
 0 | 20 |  2 |00
 1 | 18 |  3 |010
 2 | 10 |  3 |011
 3 | 10 |  3 |100
 4 | 10 |  3 |101
 5 |  6 |  4 |1100
 6 |  6 |  5 |11010
 7 |  4 |  5 |11011
 8 |  4 |  5 |11100
 9 |  4 |  5 |11101
10 |  4 |  5 |11110
11 |  3 |  6 |111110
12 |  1 |  6 |111111

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

Как избавиться от дерева. Предлагаю не думать о дереве вовсе. Давайте представим, что мы не удаляем из очереди эдементы, а только добавляем композитные элементы с суммарным весом. Тогда проход по списку сверху вниз, с увеличением счетчика длины кода на каждом композитном элементе, дает длину следующих кодов. Алгоритм - мой, сам придумал, глядя на картинку. Но возможно за последние 70 лет, эта идея приходила не только мне.

Как избавиться от приоритетной очереди. От нее не избавиться, но мы могли бы использовать сортировку массива или создавать динамические данные из массива. Сортировка массива дает слишком много циклов и много операций копирования. В общем случае мы используем объект типа список из массива элементов и методы: "вставить с приоритетом"(list_insert_sorted), "получить следующий", и "выделить новый элемент из массива" (list_new). Используем процедуру обхода списка, в одном направлении -- односвязный список.

Это мы сейчас обсуждаем генерацию кодов, которая востребована на этапе упаковки данных, сжатия.

Декомпрессия выполнятся иначе. Для декомпрессии на вход подается таблица битовых длинн кодов, только третья колонка в нашем примере, вторая и четвертая колонки таблицы: частота использования и коды -- не заполнены.

В первой реализации метода декодирования (см. предыдущее сообщение), я использовал генерацию кодов из длин налету, без использования кодовых таблиц. Однако, теоретический максимум производительности такого метода - один бит за такт процессора. Медлено.

Метод декодирования с использованием дерева ветвления - тоже сравнительно медленный. Чтобы понять насколько медленно скажем так: символы основного алфавита кодируются длинами от 1 до 15 бит. Если разброс длин таблицы кодирования большой, то может понадобиться 15 циклов, чтобы найти нужный код. Однако, тут надо иметь ввиду, что длинные коды встречаются реже, чем короткие. Так что, берем оценку по среднему - восемь циклов на декодирование каждого символа.

Метод декодирования с использованием кодовых таблиц в теории будет работать быстрее, потому что разбор ведется сразу по несколько бит, в среднем по 8 бит для литералов и длин, и по пять бит для смещения. Т.е. ожидаем производительность табличных методов в несколько раз выше. Однако, для декодирования кодов с максимальной длиной 15 бит нужны таблицы декодирования размером в 32-128 кБайт, а их генерировать возможно дольше, чем разбирать поток по одному биту. По этой причине мы рассматриваем методы, которые используют частичное декодирование с помощью таблиц, а затем вычисляют остаток.

Ставим задачу: получить производительность декодирования - один цикл на символ, в среднем. Как это сделать. Для начала надо научиться по префиксу кода, находить индекс в таблице. Готовим коды с выравниванием по левому краю, как указано в таблице, чтобы можно было выполнять сравнение чисел.

 0 | 20 |  2 |00XXX
 1 | 18 |  3 |010XX
 2 | 10 |  3 |011XX
 3 | 10 |  3 |100XX
 4 | 10 |  3 |101XX
 5 |  6 |  4 |1100X
 6 |  6 |  5 |11010
 . . .

В таком виде можно вычислять индекс по кодам. Привожу алгоритм описанный в обзорной статье [ACM Comput. Surv., Vol. 1, No. 1, Article 1. Publication date: June 2019].

    1) Найти такое ℓ чтобы first_code_l[ℓ] ≤ code < first_code_l[ℓ + 1];
-- Коды упорядочены по убыванию частоты использования, найти какому диапазону принадлежит код.
    2) пусть offset ← (code − first_code_l[ℓ]) >> (L − ℓ);
-- находим смещение в таблице - номер символа. 
    3) Находим сам символ s ← first_symbol[ℓ] + offset;
Для поиска в таком режиме применяем следующий цикл, именно он будет определять производительность метода:
// Алгоритм поиска кода по таблице.
    set ℓ ← search_start[code >> (L − t)];
    while code ≥ first_code_l[ℓ + 1] do
        set ℓ ← ℓ + 1.

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

Сначала из таблицы некоторой фиксированной длины мы выбираем величину минимальной длины кода (t). Затем подбираем код по таблице. Казалось бы выигрыша особенного нет, потому что получаем цикл от минимальной до максимальной длины кода.

Для отладки и исследования алгоритма я ввел еще одну таблицу - search_end - максимальная длина кода с заданным префиксом, которая ограничивает глубину поиска кода.

// Предложенный алгоритм поиска
    set ℓ ← search_start[code >> (L − t)];
    set u ← search_end  [code >> (L − t)];
    while (ℓ < u) && code ≥ first_code[ℓ + 1] do
        set ℓ ← ℓ + 1.

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

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

И тут пришла в голову мысль, а не посмотреть ли как другие решают эту задачу. Посмотрел. Опять нашел код с исходниками Марка Адлера и отказом от права контролировать копии, с 1996 года в составе GNU, gzip -- этот код есть на каждом сервере. Код тридцатилетней давности, 30 лет, в коде упомянаются проблемы компиляции под MSDOS и OS/2, с ипсользованием Turbo C. В файле ТОДО рекомендуется использовать оптимизации CRC от Росса Вильямса, который уже пятнадцать лет, как на пенсии.

Есть давно забытое искусство написания декодеров, по принципу "магического кристалла": крутим-вертим кристалл, а из него выпадают: то форматированные данные, то инструкции, как их исполнять - декодер команд. Давайте представим, что наш формат -- это поток инструкций для некоторого процессора, которые надо декодировать. Для декодирования используется конечный автомат - переход по состояниям, и таблицы - таблицы - таблицы. При этом декодирование отделено от исполнения команд, рассматриваем декодер, как устройство или функцию, которая из одного потока данных делает другой поток данных иначе сформатированный и выровненный. Попробуем представить не то, как оптимизировать существующий алгоритм, а как описать произвольный табличный декодер минимальным числом инструкций.

// Алгоритм декодирования
    do{
        t ← t.data + (взять_из_потока(t.n_bits) & маска_выборки(t.extra)) 
    } while нужно_продолжить(t.extra);
    сдвинуть_поток(t.n_bits);
    выполнить_обработку_данных(t.data, t.extra).

Примерно такой шаблон кода должен решать задачу декодирования. Более того, если этот фрагмент выполняется в цикле, таблицами (t) и переходами внутри таблицы (t.data) можно организовать контектстно-зависимый разбор. Маска выборки может быть линейная или не линейная. Допустим нам нужно декодировать программу из системы команд одного процессора в систему команд другого процессора. Или скажем, нужно написать итератор по системе команд Intel x86, т.е. по префиксам выделять длину инструкции -- тот же алгоритм декодирования, тот же шаблон. Или понадобилось разбирать синтаксис и грамматику в интерпретаторе команд, - тоже самое.

Конкретно, нам нужно выбрать инструкцию, на которой будет все работать. Для пионеров можем предложить реализацию на инструкции Intel AVX512_BITALG:

Synopsis
   __mmask16 _mm_bitshuffle_epi64_mask (__m128i b, __m128i c)

Instruction: vpshufbitqmb k, xmm, xmm
CPUID Flags: AVX512_BITALG + AVX512VL

Т.е на одной такой инструкции можно декодировать входной поток с использованием перестановки бит. Комбинация бит во входном потоке приводит к переходу по таблице декодирования. Результат - маска.

Мы выбираем простой способ формирования масок ((1<<(n))-1), который можно реализовать в одну инструкцию Intel BMI или BMI2;

В зависимости от целевой платформы, мы можем с таким алгоритмом достичь производительности в один такт(цикл) на кодовое слово. И тут происходит основное расстройство - мы живем в золотом веке программирования - этот подход уже реализован в gzip. С той лишь разницей, что реализован он средствами 30 летней давности, в инструкциях ANSI С. Более современная интерпретация алгоритма пристуствует в проекте zlib-ng.

Это сильно расстраивает, но наша цель -- предложить самый быстрый и компактный алгоритм декодирования.

Я потратил выходные на то чтобы научиться составлять таблицы декодирования и подобрать функцию декодера. И вот что получилось.

// Алгоритм декодирования
#define BIT_MASK(n) ((1<<(n))-1)

    t = t + (stream & BIT_MASK(N));
    if (t->extra) {
        t = te + t->n + ((stream>>N) & BIT_MASK(t->extra));
    }
    stream >>= t->bits;
    return t->n;

Функция использует две таблицы: (t) - основаная с разрядностью индекса N и дополнительная (te) - для декодирования кодов с длиной больше N. Декодирование выполняется не более чем в два этапа. На выходе алгоритм выдает декодированное значение. Параметр N выбирается разный для таблиц декодирования литералов (N=8) и для декодирования дистанций (N=5). При данных значениях параметра вторая таблица используется редко. Операции извлечения бит из потока могут выполняться с использованием инструкций Intel BEXTR (BMI1). Полагаю данная реализация является оптимальной с точки зрения производительности.Если при этом удасться сохранить лаконичность кода, то и самая быстрая в своем роде (быстрее gzip, zlib и zlib-ng).

Суммараная производительность алгоритма будет складываться из времени подготовки таблиц, времени работы алгоритма декодирования (inflate), и времени подгрузки данных.