Введение

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

Понимание основ DSP в C

DSP включает в себя математические операции, такие как фильтрация, преобразования, свертка и спектральный анализ отобранных сигналов. В C программист управляет каждым аспектом представления данных и потока, что имеет решающее значение для детерминированного исполнения. DSP-код часто работает на микроконтроллерах или цифровых сигнальных процессорах, где аппаратное обеспечение тесно связано - например, выделенные блоки MAC (множественно-накопленные) или векторные движки SIMD. Глубокое понимание иерархии памяти целевой архитектуры, набора инструкций и периферийных возможностей имеет важное значение для написания эффективного кода C.

Ключевые характеристики кода DSP:

  • Повторяющаяся арифметика: доминируют петли с операциями с множественным добавлением (например, фильтры FIR).
  • Ограничения по времени: Каждый образец должен быть обработан в течение периода выборки.
  • Поток данных: непрерывные потоки ввода/вывода требуют эффективной буферизации и минимального копирования.
  • Связанная с памятью полоса пропускания: Многие алгоритмы DSP ограничены тем, насколько быстро могут перемещаться данные, а не арифметическими операциями.

Для базовой ссылки см. Основы DSP устройств Analog .

Фиксированная арифметика: точность без накладных расходов на плавающие точки

Многие процессоры DSP не имеют аппаратных блоков с плавающей запятой (FPU) или имеют более медленные FPU. Арифметика с фиксированной точкой использует целочисленные операции с неявной точкой радикса, обеспечивая детерминированную производительность и более низкое энергопотребление. Наиболее распространенным представлением является Q-упоминание: Qm.n, где m биты являются целочисленной частью и n биты дробной частью. Например, формат Q15 (1 знаковый бит, 15 дробных битов) повсеместно встречается в 16-битных DSP.

Внедрение операций с фиксированными точками в C

Добавление с фиксированной точкой просто (просто добавьте целые числа), но для умножения требуется настройка точки радикса. Для умножения Q15 продукту двух чисел Q15 нужен 32-битный промежуточный результат, затем вы правым смещением на 15 битов возвращаетесь к Q15. Пример:

typedef int16_t q15_t;
q15_t q15_mul(q15_t a, q15_t b) {
 int32_t temp = (int32_t)a * (int32_t)b;
 return (q15_t)(temp >> 15);
}

При накоплении (например, в фильтрах) защитные биты предотвращают переполнение. Используйте 32-битные или даже 64-битные аккумуляторы и насыщайте результаты. Библиотеки с фиксированными точками, такие как ARM CMSIS-DSP, обеспечивают оптимизированные функции с фиксированными точками, включая фильтрацию, преобразования и матричные операции.

Когда использовать Fixed-Point против Floating-Point

Современные процессоры с FPU (например, Cortex-M4/M7) могут выполнять операции с плавающей запятой так же быстро, как и с фиксированной запятой.

  • Алгоритм динамического диапазона является высоким (например, адаптивные фильтры).
  • Устойчивость кода является приоритетом (меньше масштабирования).
  • Присутствует аппаратное обеспечение FPU, и трубопровод может перекрывать добавления и умножать.

На устройствах с большим объемом данных без FPU фиксированная точка остается стандартом для приложений, чувствительных к затратам.

Оптимизация доступа к памяти для DSP

Алгоритмы DSP часто обрабатывают большие массивы данных последовательно. Промахи кэша и остановки автобусов могут убить производительность. Следуйте этим принципам:

  • Доступ к прямым данным: Поперечные массивы в непрерывном порядке (ряд-основной в C). Избегайте шаблонов шагающего доступа, если это не требуется алгоритмом (например, FFT-разворот битов).
  • Выравнивание данных: обеспечивает выравнивание массивов по границам кэш-линии. Используйте атрибуты компилятора, такие как или специальные разделы памяти.
  • Буферинг: Используйте двойную буферизацию, чтобы перекрывать передачи DMA с обработкой CPU. Пока CPU работает на одном буфере, загружается следующий блок выборки.
  • Ограниченное ключевое слово: использовать C99 на указателях, чтобы сообщить компилятору, что указатели не псевдонимы, что позволяет векторизацию и лучшее планирование инструкций.

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

void fir_lowpass(const int16_t * restrict x, int16_t * restrict y,
 const int16_t * restrict coeffs, int len, int order) {
 for (int i = 0; i < len; i++) {
 int32_t acc = 0;
 for (int j = 0; j < order; j++) {
 acc += (int32_t)x[i + j] * coeffs[j];
 }
 y[i] = (int16_t)(acc >> 15);
 }
}

Эффективный алгоритм выбора и реализации

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

  • Быстрое преобразование Фурье (FFT): Используйте радикс Кули-Туки-2 или сплит-радикс для мощности двух длин. Избегайте наивного DFT, который является O(N2). Предвычислительные факторы витков и хранить в ПЗУ.
  • ПИР-фильтры: используют полифазное разложение для децимации/интерполяции; используют симметрию для линейно-фазных фильтров для сокращения вдвое числа умножений.
  • ИИР-фильтры: используют прямую форму II, перенесённую для лучшей численной устойчивости; используют каскадные биквадные секции (стадии второго порядка) для снижения чувствительности к квантованию коэффициента.
  • Свёртывание: для длинных последовательностей используйте методы перекрытия-добавления или сохранения на основе FFT, а не прямую свертку.

Ссылка на FFTW библиотеку для ссылки на современные методы FFT (хотя и не в C, его принципы широко копируются во встроенных библиотеках DSP).

Использование функций аппаратного обеспечения: инструкции SIMD и DSP

Почти все современные микроконтроллеры включают SIMD (Single Instruction Multiple Data) или DSP-усовершенствованные инструкции.

  • ARM Cortex-M4/M7: SIMD (SADD, SMUAD и др.), насыщенная арифметика и дробные операции (QADD, QSUB). Используйте внутренние функции CMSIS-DSP.
  • TI C6000 DSP: восемь умноженных блоков, двойной MAC и программная пиплайнинг. TI DSP Optimization Guide предоставляет подробные методы.
  • RISC-V с P-расширениями: будущие ядра будут иметь DSP-подобные инструкции.

Для использования этих функций в C, напишите код, который компилятор может автоматически векторизировать (например, простые петли без зависимостей) или использовать внутренние функции компилятора. Пример использования ARM CMSIS-DSP для фильтра FIR:

#include "arm_math.h"
arm_fir_instance_f32 S;
float32_t firState[128];
arm_fir_init_f32(&S, numTaps, coeffs, firState, blockSize);
arm_fir_f32(&S, input, output, blockSize);

Такие библиотеки вручную настраиваются в сборку для максимальной производительности. Всегда профиль до и после перехода от общих функций C к библиотечным функциям.

Методы оптимизации Loop

Поскольку алгоритмы DSP имеют петлевую тяжелую структуру, оптимизация на уровне петли приносит большие дивиденды:

  • Раскрутка петли: вручную или с помощью прагм компилятора ('#pragma unroll N') для уменьшения накладных расходов на цикл и увеличения параллелизма на уровне инструкций.
  • Программная пиплайнинговая система: реструктурирует петли так, чтобы одновременно выполнялись несколько итераций. Некоторые компиляторы делают это автоматически; используют флаги «-O3» и архитектурно-специфические флаги.
  • Уменьшить ветвление: заменить условные значения арифметикой (например, мин/макс с использованием троичной), или использовать таблицы поиска для нелинейных функций.
  • Использовать локальные переменные: хранить часто доступные данные в регистрах, объявляя переменные внутри цикла или используя подсказку «регистрировать».
  • Минимизируйте деления: Замените деление постоянным умножением на взаимное; используйте сдвиг для полномочий двух.

Предвычислительные константы и таблицы поиска

Функции DSP, такие как тригонометрические значения, коэффициенты и коэффициенты вертушки, должны быть предварительно вычислены в автономном режиме и храниться в виде постоянных массивов в ПЗУ. Для запуска в нереальном времени вы можете вычислить их один раз и повторно использовать. Пример: для FFT с 1024 точками предварительно вычислить значения синус/косинус для каждого этапа. Это исключает оценку времени выполнения и снижает мощность.

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

Профилирование и настройка

Оптимизация не может быть полной без измерения. Используйте эти методы для выявления узких мест:

  • Цикло-точное профилирование: использование бортовых счетчиков циклов (например, DWT CYCCNT на Cortex-M) для измерения продолжительности функции.
  • Статистическое профилирование: выборочный счетчик программы (PC) для просмотра того, какие функции потребляют время процессора.
  • Профилирование памяти: Используйте инструменты для мониторинга промахов кэша (если таковые имеются) и транзакций шины.
  • Обратная связь с компилятором: позволяет отчетам об оптимизации компилятора ('-fopt-info-vec-optimized' в GCC) видеть, были ли циклы векторизированы.

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

Оригинальное название: Bring It All Together

Для написания эффективного кода DSP в C требуется целостный подход:

  • Выберите правильное представление данных (фиксированная точка против плавающей точки).
  • Проектирование структур данных для последовательного доступа и выравнивания.
  • Выберите алгоритмы с низкой сложностью (FFT, полифаза).
  • Используйте библиотеки DSP, когда это возможно.
  • Разверните петли и уменьшите ветвление.
  • Предвычислительные константы в ПЗУ.
  • Профиль неустанно и пусть компилятор поможет.

Применяя эти принципы, разработчики могут достичь пропускной способности обработки сигналов, сравнимой с ручной сборкой, сохраняя при этом портативность и ремонтопригодность C. Результатом являются надежные системы DSP в реальном времени, которые отвечают требованиям современных встроенных продуктов - от слуховых аппаратов до базовых станций 5G.