Микроконтроллер + DAC + быстрый синус
[Home] [< Prev: Микроконтроллер + DAC + табличный синус] [Next: ADC: аналого-цифровой преобразователь микроконтроллера >]

Микроконтроллер + DAC + быстрый синус

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

Оглавление
Варианты реализации DDS на MCU
Быстрое вычисление синуса суммированием ряда
Практическая реализация
Вычисление синуса как функции от индекса фазы
Пример программы для микроконтроллера



Варианты реализации DDS на MCU

Когда используется прямой цифровой синтез синусоидального сигнала (DDS), требуется вычислять величину сигнала в моменты времени, определяемые тактовым сигналом. Это означает, что необходимо с высокой скоростью вычислять значения синуса. Для чего можно использовать несколько методов, предъявляющих различные требования к производительности процессора и объёму используемой памяти.

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

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

С помощью формул приведения, тригонометрическая функция может быть выражена через тригонометрическую функцию другого аргумента, в определённых ситуациях меньшего (по модулю), чем исходный. Благодаря этому, полная таблица синусов, расcчитанная для значений аргументов из полного периода [0, 2π), может быть заменена таблицей для четвёртой части периода, т.е. может быть уменьшена по размеру в 4 раза. Тем самым мы экономим значительный объём памяти, но теряем в быстродействии из-за необходимости выполнять дополнительные вычисления.

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

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

Быстрое вычисление синуса суммированием ряда

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

Однако, для 12-разрядного DAC совершенно не требуется вычислять тригонометрические функции с той высокой точностью, которая обеспечивается библиотечными функциями поддержки математики. Вполне достаточно точности, которую дают вычисления с фиксированной точкой при суммировании первых двух-трёх членов ряда Маклорена для синуса или косинуса.

Разложение функций sin, cos в ряд Маклорена имеет вид: $$ \sin x=x-\frac{x^3}{3!}+\frac{x^5}{5!}-\frac{x^7}{7!}+\ldots+(-1)^{n+1}\frac{x^{2n-1}}{(2n-1)!}+\ldots, \tag{1} $$ $$ \cos x=1-\frac{x^2}{2!}+\frac{x^4}{4!}-\frac{x^6}{6!}+\ldots+(-1)^{n+1}\frac{x^{2n-2}}{(2n-2)!}+\ldots. \tag{2} $$

Данные ряды сходятся и имеют в качестве суммы функцию sin x, cos x соответственно при любом x. Однако, чем меньше x, тем сходимость более быстрая. Допустим, если |x|<1, то ряды удовлетворяют условию теоремы Лейбница, начиная с первого члена. Следовательно, если мы заменим сумму ряда частичной суммой, то допустим ошибку, по модулю не превосходящую абсолютной величины первого из отброшенных членов: $$ \sin x \approx x-\frac{x^3}{3!}+\frac{x^5}{5!}, \tag{3}\\ |err| \le \left|\frac{x^7}{7!}\right|; $$ $$ \cos x \approx 1-\frac{x^2}{2!}+\frac{x^4}{4!}-\frac{x^6}{6!}, \tag{4}\\ |err| \le \frac{x^8}{8!}. $$

Синус и косинус любого аргумента можно выразить через синус и косинус числа, не превосходящего по абсолютной величине π/4<1.* Значит, пользуясь формулами (3), (4), можем вычислять sin с точностью не хуже 4e-5 и cos с точностью до 4e-6. А это даже более высокая точность, чем требуется при управлении 12-разрядным DAC с его 1LSB=1/4096 (порядка 2e-4).

* На самом деле тригонометрические функции любого аргумента можно выразить через sin, cos ещё меньших углов, но в данном случае это не приведёт к упрощению вычислений.

Теперь о том, как выразить тригонометрическую функцию произвольного аргумента \(\phi\), через sin, cos величины, по модулю не превосходящей π/4. Представим аргумент в виде $$ \phi=\frac{n\pi}2+\alpha, \\ -\pi/4 \le \alpha \lt \pi/4, $$ где n - целое число. Тогда, например, функция sin в соответствии с формулой синуса от суммы может быть выражена следующим образом $$ \sin \phi=\sin\frac{n\pi}2 \cos\alpha+\cos\frac{n\pi}2 \sin\alpha, $$ $$ \sin\phi=\sin\alpha, \text{ если } n=4z \text{ или } (n\&3==0), \\ \sin\phi=\cos\alpha, \text{ если } n=4z+1 \text{ или } (n\&3==1), \\ \sin\phi=-\sin\alpha, \text{ если } n=4z+2 \text{ или } (n\&3==2), \\ \sin\phi=-\cos\alpha, \text{ если } n=4z+3 \text{ или } (n\&3==3), $$ здесь z - некоторое целое число.

Осталось для данного \(\phi\) подобрать подходящие значения \(\alpha\) и n. Заметим, что $$ \phi=\frac{n\pi}2+\alpha=\frac{\pi}2\left(n+\frac{\alpha}{\pi/2}\right), \tag{5} \\ -1/2 \le \frac{\alpha}{\pi/2} \lt 1/2, $$ тогда $$ 0 \le \frac{\alpha}{\pi/2}+\frac 1 2 \lt 1. $$ Следовательно, в соответствии со свойствами функции - целой части числа и с учётом того, что n - целое, можно записать: $$ floor\left(\frac{\alpha}{\pi/2}+\frac 1 2 \right)=0 \Rightarrow \\ floor\left(n+\frac{\alpha}{\pi/2}+\frac 1 2 \right)=n. $$ Но из (5) следует, что $$ n+\frac{\alpha}{\pi/2}=\frac{\phi}{\pi/2}, $$ значит, $$ n=floor\left(\frac{\phi}{\pi/2}+\frac 1 2 \right), \\ \alpha=\phi-n\frac{\pi}2=\phi-\frac{\pi}2 floor\left(\frac{\phi}{\pi/2}+\frac 1 2 \right), $$ где \(-\pi/4 \le \alpha \lt \pi/4\).

Практическая реализация

Ниже приведён пример реализации быстрого вычисления тригонометрических функций с использованием арифметики с фиксированной точкой на C++ . Предполагается, что числа с фиксированной точкой представлены целыми числами со знаком размером 32 бит. На целую и дробную части выделяем по 16 бит.

В связи с весьма ограниченной точностью чисел, ошибка вычислений будет превышать значения, которые дают формулы (3), (4). Это связано с погрешностями округления и накоплением ошибки при выполнении последовательности операций. Численный эксперимент с перебором всех возможных значений аргумента с фиксированной точкой от -π/4 до π/4 показывает, что наибольшая ошибка по модулю не превышает 6e-5 при вычислении значений sin и не превышает 4e-5 при вычислении значений cos. Числу 6e-5 в формате fix16 соответствует целое 216*6e-5=65536*6e-5, что приблизительно равно 4, а значит 2..3 младших бита полученного значения не являются достоверными, в то время как 13..14 старших битов дробной части являются верными. Точности полученного результата вполне достаточно для работы с DAC с разрядностью до 12. Ошибка вычислений не превышает 1/4 LSB 12-разрядного DAC.

// Вычисление тригонометрических функций с использованием
// целочисленных операций (используется формат с фиксированной точкой).

#define _USE_MATH_DEFINES
#include <math.h>

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

// Средства преобразования чисел: float_to_fix16, fix16_to_float.

// Преобразование из обычного формата представления чисел
// (с плавающей точкой или целого) в формат с фиксированной
// точкой: 16 бит дробная и 16 бит целая часть.
#define float_to_fix16(x) (int)((x)*0x10000)

// Преобразование числа с фиксированной точкой (16 бит в дробной части)
// в число с плавающей точкой.
#define fix16_to_float(x) ((x)/65536.0)

// Определяем тип с фиксированной точкой (16 бит в дробной части).
// Эквивалентен целому со знаком и используется только в целях
// обеспечения выразительности кода.
typedef int fix16;

// Функция быстрого вычисления sin, |x|<=M_PI/4.
fix16 _fast_sin(fix16 x)
{
    // Вычисляется x*x: предкоррекция, целочисленное умножение, посткоррекция.
    fix16 x2=(x>>1)*x>>15; // x2=x**2.

    fix16 xn=x2*x>>16;     // xn=x**3.

    fix16 r=x-(xn*float_to_fix16(1/6.0)>>16);

    xn*=x2; xn>>=16;       // xn=x**5.

    // r=x-(x**3)/6.0+(x**5)/120.0.
    r+=(xn*float_to_fix16(1/120.0)>>16);

    return r;
}

// Функция быстрого вычисления cos, |x|<=M_PI/4.
fix16 _fast_cos(fix16 x)
{
    // Вычисляется x*x: предкоррекция, целочисленное умножение, посткоррекция.
    fix16 x2=x*(x>>1)>>15; // x2=x**2.

    fix16 xn=x2*x2>>16;    // xn=x**4.

    fix16 r=float_to_fix16(1.0)-
        (x2*float_to_fix16(0.5)>>16)+
        (xn*float_to_fix16(1/24.0)>>16);

    xn*=x2; xn>>=16;       // xn=x**6.

    // r=1-(x**2)/2.0+(x**4)/24.0-(x**6)/720.0.
    r-=xn*float_to_fix16(1.0/720)>>16;
    
    return r;
}

Вычисление синуса как функции от индекса фазы

Теперь мы имеем функции, которые дают нам возможность быстрого вычисления значений sin, cos, при условии что аргумент x отвечает условию |x|<=M_PI/4. Но как было показано в предыдущем документе, где речь шла о табличных методах вычисления, на практике удобно задавать аргумент функции с помощью целочисленного индекса фазы.

Итак, нам требуется реализовать функцию, которая принимает индекс фазы, целое без знака число k размером 32 бита и возвращает значение синуса в формате fix16, соответствующее фазе \(\phi\), заданной своим индексом: $$ \phi=\frac{2\pi}{2^{32}}k, \> k=0..2^{32}-1, \> 0 \le \phi \lt 2\pi. $$ Но реализованные ранее функции _fast_sin, _fast_cos требуют, чтобы аргумент по модулю не превышал значения π/4. Воспользуемся тем, что как мы уже установили, \(\phi\) можно представить в виде $$ \phi=n\frac{\pi}2+\alpha, -\pi/4 \le \alpha \lt \pi/4, $$ где n - целое число, вычисляется следующим образом: $$ n=floor\left(\frac{\phi}{\pi/2}+\frac 1 2\right), $$ тогда $$ \alpha=\phi-n\frac{\pi}2. $$

В нашем случае \(\phi=2\pi k/2^{32}\), тогда $$ n=floor\left(\frac{2\pi k}{2^{32}}\frac 2{\pi}+\frac 1 2 \right)= floor\left(\frac{4k}{2^{32}}+\frac 1 2\right)= \\ floor\frac{4k+2^{31}}{2^{32}}=floor\frac{4k+4\cdot 2^{29}}{2^{32}}, \\ n=floor\frac{k+2^{29}}{2^{30}}, $$ что с использованием средств языка C++ можно записать в виде

// unsigned int k=...;
unsigned int n=(k+0x20000000)>>30;

В зависимости от значения n, \(\sin\phi=\pm\sin\alpha\) или \(\sin\phi=\pm\cos\alpha\), где $$ \alpha=\phi-n\frac{\pi}2. $$ Бит [0] числа n определяет функцию, через которую выражается \(\sin\phi\) (0 - sin, 1 - cos), бит [1] определяет знак, с которым берётся эта функция (0 - "+", 1 - "-").

Удобнее сначала вычислить соответствующий \(\alpha\) индекс фазы: $$ k_{\alpha}=\frac{2^{32}}{2\pi}\alpha=\frac{2^{32}}{2\pi}\frac{2\pi}{2^{32}}k-\frac{2^{32}}{2\pi}\frac{\pi}2 n, \\ k_{\alpha}=k-n\cdot 2^{30} $$ или

int ka=k-(n<<30);

Тогда $$ \alpha=2\pi\frac{k_{\alpha}}{2^{32}}. $$ Здесь \(k_{\alpha}/2^{32}\) можно рассматривать как число \(k_{\alpha}\) в формате fix32 (целая часть равна 0, все 32 бита представляют дробную часть числа).

Функция, возвращающая значение синуса для данного индекса фазы может быть реализована следующим образом (использует определённые ранее _fast_sin, _fast_cos):

// Функция вычисления sin от аргумента, заданного индексом фазы.
fix16 fast_sin(unsigned int k)
{
    fix16 (*ftable[])(fix16) ={_fast_sin, _fast_cos};

    // Индексу фазы k соответствует фаза (аргумент) phi=2*M_PI*k/(2**32).
    // phi можно представить в виде:
    // phi=n*M_PI/2+alpha, где n - некоторое целое, а |alpha|<=M_PI/4.

    // Вычисляем n.
    unsigned int n=(k+0x20000000)>>30;

    // Вычисляем соответствующий alpha индекс фазы (целое со знаком).
    int ka=k-(n<<30);  // fix32, 30bits

    // Преобразуем индекс фазы в фазу в формате fix16.
    fix16 alpha=               // fix30, 31bits
        (int)(4096*2*M_PI)*    // 2*M_PI, fix12, 15bits
        (ka>>14);              // fix18, 16bits
    alpha>>=14;                // fix16

    // r=_fast_sin(alpha) или r=_fast_cos(alpha), в зависимости
    // от младшего бита числа n:
    fix16 r=ftable[n&1](alpha);

    // Если бит [1] в n установлен, в качестве результата берём
    // r с противоположным знаком: if(n&2) r=-r;
    int s=(n>>1)&1;
    return (r^(-s))+s;
}

Погрешность вычисления не превышает 7e-5.

Пример программы для микроконтроллера

Скачать исходный код примера

Программа полностью аналогична программе из предыдущего документа (где рассматривались табличные методы вычисления синуса применительно к прямому цифровому синтезу сигнала). Естественно, в данном случае вместо табличного способа используется суммирование ряда, это единственное отличие данного примера.

Микроконтроллер STM32F100RB при работе с тактовой частотой 24 МГц, способен осуществлять синтез сигнала с опорной частотой до 250 кГц, что позволяет получать сигнал с частотой от 0 до 60..100 кГц (при компиляции проекта с оптимизацией по быстродействию). Но это при практически полном задействовании вычислительных ресурсов микроконтроллера. Результаты получены при запуске из RAM, при запуске из ROM архитектура ARM обеспечивает большее быстродействие.

Если используется компиляция без оптимизации (например, при отладке), можно достичь опорных частот до 90 кГц.

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

Следует очень аккуратно выбирать опорную частоту - реализуемы только значения, которые можно получить из тактовой частоты используемого таймера путём деления на целочисленный делитель (из диапазона 2..65536). Чтобы избежать ошибок, если требуется нецелое значение опорной частоты, лучше задать требуемый коэффициент деления непосредственно в коде, соответствующим образом сконфигурировав используемый таймер.

// Генерация синусоиды методом DDS; демонстрируется
// вычисление синуса путём суммирования ряда.
// Отлажено на STM32F100RB (24МГц).
// Используется: DAC канал 1 (PA4), DMA1 канал 3, TIM6.

// F0 до 90кГц при компиляции без оптимизации;
// до 250кГц при компиляции с оптимизацией по скорости.

// DAC канал 1 (PA4) использует DMA1 канал 3.
#include <stm32f10x.h>

#include <math.h>

// Максимальный выходной сигнал, постоянное смещение и амплитуда
// синтезируемой синусоиды в единицах Vref/65536.
const int out_max=0xFFFF;
int out_a0=out_max/2;
int out_a1=out_max/3;

// Тактовая частота сигнала на входе TIM6.
#define FSYSCLK 24000000
// Задаём желаемую опорную частоту (максимальное значение
// зависит от быстродействия микроконтроллера).
// Возможны только значения вида FSYSCLK/K, где
// K-целое из диапазона 2..65536.
// Можно задать требуемый делитель непосредственно в коде,
// отвечающем за конфигурирование таймера.
#define F0 80000

// Текущее значение индекса фазы.
unsigned int ph_ind=0;

// Приращение индекса фазы в каждом цикле синтеза;
// частота синтезируемого сигнала f=F0*ph_m/(2.0**32),
// где F0 - опорная частота.
unsigned int ph_m=0;

// Средства преобразования чисел.

// Преобразование из обычного формата представления чисел
// (с плавающей точкой или целого) в формат с фиксированной
// точкой: 16 бит дробная и 16 бит целая часть.
#define float_to_fix16(x) (int)((x)*0x10000)

// Преобразование числа с фиксированной точкой (16 бит в дробной части)
// в число с плавающей точкой.
#define fix16_to_float(x) ((x)/65536.0)

// Определяем тип с фиксированной точкой (16 бит в дробной части).
// Эквивалентен целому со знаком и используется только в целях
// обеспечения выразительности кода.
typedef int fix16;

// Функция быстрого вычисления sin, |x|<=M_PI/4.
fix16 _fast_sin(fix16 x)
{
    // Вычисляется x*x: предкоррекция, целочисленное умножение, посткоррекция.
    fix16 x2=(x>>1)*x>>15; // x2=x**2.

    fix16 xn=x2*x>>16;     // xn=x**3.

    fix16 r=x-(xn*float_to_fix16(1/6.0)>>16);

    xn*=x2; xn>>=16;       // xn=x**5.

    // r=x-(x**3)/6.0+(x**5)/120.0.
    r+=(xn*float_to_fix16(1/120.0)>>16);

    return r;
}

// Функция быстрого вычисления cos, |x|<=M_PI/4.
fix16 _fast_cos(fix16 x)
{
    // Вычисляется x*x: предкоррекция, целочисленное умножение, посткоррекция.
    fix16 x2=x*(x>>1)>>15; // x2=x**2.

    fix16 xn=x2*x2>>16;    // xn=x**4.

    fix16 r=float_to_fix16(1.0)-
        (x2*float_to_fix16(0.5)>>16)+
        (xn*float_to_fix16(1/24.0)>>16);

    xn*=x2; xn>>=16;       // xn=x**6.

    // r=1-(x**2)/2.0+(x**4)/24.0-(x**6)/720.0.
    r-=xn*float_to_fix16(1.0/720)>>16;
    
    return r;
}

// Функция вычисления sin от аргумента, заданного индексом фазы.
fix16 fast_sin(unsigned int k)
{
    fix16 (*ftable[])(fix16) ={_fast_sin, _fast_cos};

    // Индексу фазы k соответствует фаза (аргумент) phi=2*M_PI*k/(2**32).
    // phi можно представить в виде:
    // phi=n*M_PI/2+alpha, где n - некоторое целое, а |alpha|<=M_PI/4.

    // Вычисляем n.
    unsigned int n=(k+0x20000000)>>30;

    // Вычисляем соответствующий alpha индекс фазы (целое со знаком).
    int ka=k-(n<<30);  // fix32, 30bits

    // Преобразуем индекс фазы в фазу в формате fix16.
    fix16 alpha=               // fix30, 31bits
        (int)(4096*2*M_PI)*    // 2*M_PI, fix12, 15bits
        (ka>>14);              // fix18, 16bits
    alpha>>=14;                // fix16

    // r=_fast_sin(alpha) или r=_fast_cos(alpha), в зависимости
    // от младшего бита числа n:
    fix16 r=ftable[n&1](alpha);

    // Если бит [1] в n установлен, в качестве результата берём
    // r с противоположным знаком: if(n&2) r=-r;
    int s=(n>>1)&1;
    return (r^(-s))+s;
}

// Буфер значений, загружаемых в DAC с использованием DMA.
const unsigned int dma_buf_length=64;
short int dma_buf[dma_buf_length];

// Функции для обновления буфера.
// Используют глобальные переменные ph_ind,  ph_m.

// Обновить заданный фрагмент буфера.
inline void dma_buf_fragment_init(short int *begin, const short int *end)
{
    int a1=out_a1>>1; // Предкоррекция во избежание переполнения.
    while(begin!=end)
    {
        *begin=out_a0+
            (a1*fast_sin(ph_ind)>>15); // Умножение и посткоррекция.
        ph_ind+=ph_m;
        begin++;
    }
}

// Вычислить значения для первой половины буфера.
inline void dma_buf_first_half_init()
{
    dma_buf_fragment_init(dma_buf, dma_buf+dma_buf_length/2);
}

// Вычислить значения для второй половины буфера.
inline void dma_buf_second_half_init()
{
    dma_buf_fragment_init(dma_buf+dma_buf_length/2, dma_buf+dma_buf_length);
}

// Обработчик прерываний от DMA (канал 3).
extern "C" void DMA1_Channel3_IRQHandler()
{
    if(DMA1->ISR&DMA_ISR_HTIF3)
        dma_buf_first_half_init();
    else if(DMA1->ISR&DMA_ISR_TCIF3)
        dma_buf_second_half_init();
    DMA1->IFCR|=                // Сброс соотв. флагов записью 1.
            DMA_IFCR_CTEIF3|
            DMA_IFCR_CHTIF3|
            DMA_IFCR_CTCIF3|
            DMA_IFCR_CGIF3;
}

int main()
{
    // Настраиваем приоритет о разрешаем обработку прерываний DMA.
    NVIC_SetPriority(DMA1_Channel3_IRQn, 0);
    NVIC_EnableIRQ(DMA1_Channel3_IRQn);

    // Включаем тактовые сигналы используемой периферии:
    // DMA, DAC, TIM6, GPIOA.
    RCC->AHBENR|=RCC_AHBENR_DMA1EN;
    RCC->APB1ENR|=RCC_APB1ENR_DACEN|RCC_APB1ENR_TIM6EN;
    RCC->APB2ENR|=RCC_APB2ENR_IOPAEN;

    // PA4 - аналоговый вход в соответствии с требованиями DAC.
    GPIOA->CRL&=~(0xf<<4*4);

    // Настраиваем и включаем используемый для пересылки данных
    // в DAC канал DMA.
    DMA1_Channel3->CPAR=(uint32_t)&DAC->DHR12L1; // ПУ
    DMA1_Channel3->CMAR=(uint32_t)dma_buf;       // Адрес буфера в памяти.
    DMA1_Channel3->CNDTR=dma_buf_length;         // Кол-во элементов.

    uint32_t tmp=DMA1_Channel3->CCR&~0x7FFF|
            DMA_CCR3_TCIE|      // Прерывание после завершения передачи.
            DMA_CCR3_HTIE|      // Прерывание после полузавершения.
            DMA_CCR3_DIR|       // Направление: память->периферия.
            DMA_CCR3_CIRC|      // Включить циклический режим.
            // DMA_CCR3_PINC=0  // Не инкрементировать адрес периферии.
            DMA_CCR3_MINC|      // Инкрементировать адрес в памяти.
            DMA_CCR3_PSIZE_1|   // Размер слова периферии=10: 32 бита.
            DMA_CCR3_MSIZE_0|   // Размер слова в памяти=01: 16 бит.
            DMA_CCR3_PL;        // 11 - наибольший приоритет.
    DMA1_Channel3->CCR=tmp;
    DMA1_Channel3->CCR|=DMA_CCR3_EN;

    // Настраиваем и включаем DAC.
    DAC->CR|=
        DAC_CR_DMAEN1| // Включить генерацию запросов DMA.
        DAC_CR_TEN1;   // Включить запуск DAC преобразования по внешнему сигналу.
        // TSEL[2:0]=000: запуск сигналом Timer 6 TRGO event.
    DAC->CR|=DAC_CR_EN1;

    // Настраиваем и включаем источник опорного сигнала -
    // таймер TIM6. После включения таймера начнётся генерация
    // сигнала с частотой f=F0*ph_m/(2.0**32) =>
    // ph_m=(uint32_t)((2.0**32)*f/F0);

    // Примеры (для случая F0=80кГц):
    ph_m=2684354; // 50 Гц
    //ph_m=2689723; // 50.1 Гц
    //ph_m=268435456; // 5000 Гц
    //ph_m=268435456*2; // 10000 Гц
    //ph_m=662803111; // 12345.67Hz

    // Начальная инициализация буфера данных для загрузки в DAC,
    // в дальнейшем буфер будет обновляться по прерываниям от DMA.
    dma_buf_first_half_init();
    dma_buf_second_half_init();

    TIM6->ARR=FSYSCLK/F0-1;   // Частота переполнений: FSYSCLK/(ARR+1)=F0.
    TIM6->CR2&=~TIM_CR2_MMS;
    TIM6->CR2|=TIM_CR2_MMS_1; //MMS[2:0]=010: как TRGO используем UEV.
    TIM6->CR1|=TIM_CR1_CEN;   // Включаем таймер.

    while(true);
}
author: hamper; date: 2016-10-31
  @Mail.ru