Разработка процессора звуковых эффектов на STM32F407VET6

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015
Хотя в данном разделе рекомендуется размещать готовые проекты, но существeет и немало проектов в процессе разработки. Именно к таким и относится текущий проект.
 
Конечно, хорошо, когда все и сразу. Но нередко проект занимает не один месяц, а то и год. Если его не начать публиковать в процессе, к моменту публикации многие тонкости и особенности будут благополучно забыты. Для простого проекта это, возможно, и не существенно, а для сложного, особенно такого, который из-за сложности вряд ли будет повторен целиком, именно отдельные особенности, которые описывают сложности в отдельных вопросах, и могут оказаться наиболее востребованными при фрагментарном повторении проекта или просто при заимствовании идей или обмене опытом.
 
Поэтому начинаю публикацию почти сразу после появления первого рабочего прототипа.
andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015
Для процессора звуковых эффектов было решено воспользоваться отладочной платой с микроконтроллером STM32F407VET6. 
Собственно, альтернатив немного. Естественно, возможные альтернативы должны поддерживаться Arduino IDE и быть способными выполнять задачи по обработке звука. По последнему критерию все AVR сразу отпадают - недостаточно производительности. "Фруктовые" компьютеры также не будем рассматривать: хотя производительность у них даже избыточна, но используемые для них ОС в большинстве не могут обеспечить работу в реальном времени. Да и в Arduino IDE нет средств работы с ними. Семейство ESP очень специфично и, насколько мне известно, либо плохо, либо вообще не документировано. Фактически остаются только семейства STM32 и SAM3. Младшие модели STM32 (типа популярной платы BluePill) не имеют ни собственного ЦАП, ни интерфейса I2S, позволяющего подключить внешний, а потому также не подходят.
Фактически выбор остается лишь между:
- Arduino Due,
- STM32F1хх - старшие модели,
- STM32F4xx - также старшие модели.
Собственно, из имеющегося у меня в наличии: Arduino Due, Arduino Due Core, отладочные платы STM32F407VET6 и STM32F103RCT6.
Arduino Due проигрывает STM32F407 по следующим основаниям:
- цена (выше),
- производительность (ниже),
- документированность и наличие примеров (хуже),
- неудобный формфактор.
а выигрывает - лишь по одному:
- качество поддержки в Arduino IDE.
Впрочем, преимущество, на мой взгляд, малосущественное, т.к. основные технологии необходимые для ввода, обработки и вывода звука, такие как многоканальный ADC, DAC, DMA, I2S в Ардуино попросту отсутствуют. Т.е. в любом случае их поддержку нужно делать самому, а STM для этого лучше документирован.
 
Рис.1. Первоначальный этап разработки - до звука (оцифровывался сигнал с потенциометров).
 
В качестве претендента рассматривалась также отладочная плата на STM32F103RCT6, но я не сумел найти для нее даже схему. Ну и она немного проигрывает SAM3 (Arduino Due) по производительности (и существенно проигрывает STM32F407VET6). Пока заранее неизвестны требования к производительности, лучше иметь контроллер с некоторым запасом. Да и по цене STM32F103RCT6 не сильно отличается от STM32F407VET6. Зато очень сильно уступает по объему ОЗУ, что явно будет лимитировать отдельные эффекты чувствительные к объему памяти, например, реверберация.
Плата содержит несколько разъемов, в том числе два универсальных J1 и J2, (где собраны все выводы МК кроме специальных), разъемы JTAG, LCD, NRF и UART.
Было решено воспользоваться только разъемом J2 за исключением контактов, отводимых для LCD. Это около 20 контактов, включая все аналоговые, а также аналоговую землю (VR-) и опорное напряжение (VR+).
Контроллер имеет 3 канала ADC, нумеруемые от 1 до 3. При этом в Arduino IDE используется исключительно 1-й канал. Всего имеется 16 аналоговых входов, причем все они доступны для каналов 1 и 2, а для канала 3 - только половина из них. Для чего это сделано - понятно: в более многоногих версиях контроллера на третий канал посажены еще 8 из ног, отсутствующих у VET6, за счет чего общее количество аналоговых входов увеличивается до 24 (т.е. 8 входов доступны для любого канала, еще 8 - только для 1 и 2 и еще 8 - только для канала 3). 
 
Рис.2. Макетная плата.
 
Но для нашей конструкции это неудобно. 
Дело в том, что несколько каналов нужны для того, чтобы стереоканалы можно было читать одновременно, а не поочередно. Конструкция контроллера предусматривает, что в случае синхронной работы канал №1 всегда являются ведущим, а при необходимости к нему могут подключаться либо канал №2, либо одновременно каналы №2 и №3. Т.е. для синхронного ввода стереозвука можно использовать только каналы №1 и №2, а для всех прочих аналоговых органов управления, которые можно читать не строго по часам, а в произвольные моменты времени, у нас остается только канал №3. А у него только 8 входов. 
При этом аналоговых выходов только два и они располагаются на тех же контактах, что и аналоговые входы. Видимо это все было сделано для упрощения разводки аналоговой земли и опорного напряжения внутри кристалла.
Исходя из вышеизложенного, было решено ограничиться шестью аналоговыми органами управления. Из оставшихся аналоговых входов еще два было использовано для звуковых входов, два - для звуковых выходов, к двум на плате уже по факту подпаяны светодиоды, - во избежание лишних нелинейных искажений их решено было не использовать в качестве аналоговых. Три - занимает интерфейс LCD, его было решено не использовать, чтобы к этой же конструкции при необходимости можно было подключить дисплей.
Помимо 6 аналоговых органов управления было решено использовать 4 кнопки с фиксацией, 2 кнопки без фиксации (подключены параллельно имеющимся кнопкам на плате), оставить в работе два имеющихся на плате светодиода и подключить два светодиода дополнительно. Кроме того, для контроля и отладки было решено оставить один контакт, подключенный к выходу таймера, задающего частоту дискретизации, сконфигурированным на выход, чтобы этот сигнал был доступен для логического анализатора.
Схему было решено распаять на макетной плате, соединив ее через двухрядный разъем с разъемом J2 отладочной платы STM32F40VET6.
 
Рис.3. Соединенные платы.
andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015
На первом этапе было запланировано объединить всю систему из ADC, DAC, DMA, RCC и таймера, задающего частоту дискретизации, в одно целое. Естественно, в качестве источника информации использовались демонстрационные материалы ST. Поэтому на данном этапе было решено по возможности ограничиться языком Си.
 
основной файл проекта:
// Ввод-вывод звукового стереосигнала через штатные АЦП/ЦАП контроллера stm32f407vet6
//  + ФНЧ с регулируемыми частотой и добротностью
//
//=============================================
// PA4-PA5 - Audio OUT 2 (a4,a5)
// PA6-PA7 - LED       2 (a6,a7)
// PE3,PE4 - Button    2
// PC0,PC4 - Audio In  2 (a10,a14)
// PA0-PA3 - Analog In 4 (a0-a3) [+timer2]
// PC2,PC3 - Analog In 2 (a12,a13) [+SPI2]
// PC1     - LED (Red) 1 //..Analog In 1 (a11)
// VR-,VR+
// PB10    - LED (Ylw) 1 //..t2 [+SPI2+I2C]
// PE2,PE5,PE6,PC13 - Switch 4
// PB11    - t2cc4 out 1 // [+I2C] 1
//

// PB0     - flash cs (a8)
// PC5,PB1,PE7-PE15,PB12-PB14 - Display  14 (a15,a9)

#include <libmaple\\dmaF4.h>
#include "STM32F4xx_RCC.H"
#include "STM32F4xx_ADC.H"
#include "STM32F4xx_DMA.H"
#include "STM32F4xx_TIM.H"
#include "stm32f4xx_dac.h"
#include "stm32f4xx_gpio.h"
#include "prnt407.h"
#include "FilterAD_F.h"

#define COEFF_TO_24 4096  // коэффициент для приведения звука к 24-разрдному формата (8.16) (при значении 8192 искажений нет или почти нет)

#define FULL_BUFFER_SIZE         32                     // полная длина буфера в двухбайтовых отсчетах (32 слова = 64 байта)
#define BYTE_HALF_BUFFER_SIZE    FULL_BUFFER_SIZE       // половины буфера в байтах                    (         = 64 байта)
#define HALF_BUFFER_SIZE        (FULL_BUFFER_SIZE / 2)  // длина половины буфера                       (16 слов  = 32 байта)
#define HALF_BUFFER_SAMPLES     (HALF_BUFFER_SIZE / 2)  // количество стереосэмплов в половине буфера  ( 8 стереоотсчетов)
int16_t buffer[FULL_BUFFER_SIZE];  // входной буфер (ADC)

#define DAC_BUFFER_SIZE          FULL_BUFFER_SIZE //
#define DAC_HALF_BUFFER_SIZE     (DAC_BUFFER_SIZE / 2)

int16_t DACbuff[DAC_BUFFER_SIZE];  // выходной буфер (DAC)

timer_adv_reg_map* MyTimer((timer_adv_reg_map*)TIMER2_BASE);
uint32_t MyTrigger(DAC_Trigger_T2_TRGO);

/* Private function prototypes -----------------------------------------------*/
static void DMA_Config(int16_t * buff0);
static void GPIO_Config(void);
static void ADC_COMMON_Config(void);
static void ADC1_Config(uint8_t ch_A);
static void ADC2_Config(uint8_t ch_A);
static void TIM2_Config(void);
static void DAC_Config(void);

int counterDMA = 0;
tFulter_LF2i LF, RF; // фильтры - левого и правого каналов

void handler_DMA(void) { // приаттачен к заполнению очередной паоловины входного буфера
  digitalWrite(PA7, LOW); // индицируем наружу вход в прерывание
  int16_t * in_buff;
  int16_t * out_buff;
  if((DMA2_BASE->LISR >> 4) & 1) {   //   Half (1-st half-buffer free)
    in_buff  = buffer;
    out_buff = DACbuff;
  }  else {                         //   Full (2-nd half-buffer free)
    in_buff  = &buffer[HALF_BUFFER_SIZE];
    out_buff = &DACbuff[HALF_BUFFER_SIZE];
  }
  for(int i = 0; i < HALF_BUFFER_SAMPLES; i++) { // переписываем из входного буфера в выходной, пропуская через ФНЧ
    int sample_L, sample_R; // временные 24-разрядные значения звуковых отсчетов левого и правого каналов
    sample_L = (in_buff[i*2 + 0] - 2048)*COEFF_TO_24; // ЛЕВЫЙ КАНАЛ: приводим диапазо 0:4095 -> -2048:2047 -> -131072:131071, формат (2.16)
    sample_L = LF.iir32(sample_L);
    out_buff[i*2 + 0] = sample_L/COEFF_TO_24 + 2048;
    sample_R = (in_buff[i*2 + 1] - 2048)*COEFF_TO_24; // ПРАВЫЙ КАНАЛ
    sample_R = RF.iir32(sample_R);
    out_buff[i*2 + 1] = sample_R/COEFF_TO_24 + 2048;
  }
  counterDMA++;
  digitalWrite(PA7, HIGH);
}

void setup(void) {
  Serial.begin(115200);
  delay(200); // правильнее здесь будет сделать while (!Serial) {}
  LF.SetFilterCoeff_32(39, 200);
  RF.SetFilterCoeff_32(39, 200);

  delay(200);
  
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE);
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);
    
  GPIO_Config();

  DMA_Config(buffer); // для АЦП
  ADC_COMMON_Config();
  ADC1_Config(ADC_Channel_10);
  ADC2_Config(ADC_Channel_14);
  // Enable DMA request after last transfer (Multi-ADC mode) /
  ADC_MultiModeDMARequestAfterLastTransferCmd(ENABLE);                         // ADC_DMAReq..  +    +    +    ADC_DMAReq   -
  dma_attach_interrupt(DMA2, DMA_STREAM0, handler_DMA);
  DAC_Config();
  ADC_Cmd(ADC1_BASE, ENABLE);
  ADC_Cmd(ADC2_BASE, ENABLE);
//  ADC_SoftwareStartConv(ADC1_BASE);                                             // +    +     +    +    +     +
  TIM2_Config();
  printTimer2();
}

extern uint32_t numChanges;

void loop() {
  static unsigned long prevMillisLED = 0; //millis();
  const unsigned long interval_LED = 500;           // interval at which to blink (milliseconds)
  unsigned long currentMillis = millis();

  if (currentMillis - prevMillisLED >= interval_LED) {
    prevMillisLED = currentMillis;
    static int i = 0;
    digitalWrite(PA6, (i & 1));
    i++;
  }
  static unsigned long prevMillisFilter = 0; //millis();
  const unsigned long intervalFilter = 15;           // interval at which to blink (milliseconds)
  if (currentMillis - prevMillisFilter >= intervalFilter) {
    prevMillisFilter = currentMillis;
    int r0 = CheckControl(0);
    int r1 = CheckControl(1);
    LF.SetFilterCoeff_32((r1*3)/32, r0 + 64);
    RF.SetFilterCoeff_32((r1*3)/32, r0 + 64);
  }
}

static void ADC_COMMON_Config(void){
  ADC_CommonInitTypeDef ADC_CommonInitStructure;
  ADC_CommonStructInit(&ADC_CommonInitStructure);
  /* ADC Common Init */                                                        //  VBAT     TRI     Dual  Interl  DMA       Camera
  ADC_CommonInitStructure.ADC_Mode = ADC_DualMode_RegSimult;                   // Independ  Triple  +     Interl  Independ  Independ
  ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div2;                  //    +      +       +     +       +         Div6
  ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_1;             // Disabled  2       +     3       Disabled  Disabled
  ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles; //    +      +       +     6Cycl   +         +
  ADC_CommonInit(&ADC_CommonInitStructure);
  Serial.println(" = ADC common OK!");
}

static void ADC1_Config(uint8_t channel)
{
  Serial.print("  ADC1_CH10_CH11_Config "); Serial.println((uint32_t)ADC1_BASE, HEX);
  Serial.print("  ADC1_BASE->CR2        "); Serial.println((uint32_t)&(ADC1_BASE->CR2), HEX);
  printADC(ADC1_BASE, "begin config");
  ADC1_BASE->CR2 = 0x00000001; // ADC_CR2_ADON;     // = 0x00000001 -  убираем установленные по умолчанию JEXT:0x001E0000
  ADC1_BASE->SMPR1 = 0;
  ADC1_BASE->SMPR2 = 0;
  printADC(ADC1_BASE, "second string");
  ADC_InitTypeDef ADC_InitStructure;
  ADC_StructInit(&ADC_InitStructure);

  ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;                                // +        +        +   8b       +        8b
  ADC_InitStructure.ADC_ScanConvMode = DISABLE; //ENABLE;                               // Disable  Disable  +   Disable  Disable  Disable
  ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //ENABLE;                         // +        +        +   +        +        +
  ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_Rising; //None; // +        +        +   +        +        +
  ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_TRGO; //CC2;         // +        +        +   +        +        -
  ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;                                // +        +        +   +        +        +
  ADC_InitStructure.ADC_NbrOfConversion = 1;                                            // 1        1        +   1        1        1
  ADC_Init(ADC1_BASE, &ADC_InitStructure);

  ADC_RegularChannelConfig(ADC1_BASE, channel, 1, ADC_SampleTime_112Cycles); // VBAT/15cyc  +     +   +        +        56cyc
  Serial.println(" = ADC1 OK!");
}

static void ADC2_Config(uint8_t channel)
{
  ADC2_BASE->CR2 = ADC_CR2_ADON;     // = 0x00000001 -  убираем установленные по умолчанию JEXT:0x001E0000
  ADC2_BASE->SMPR1 = 0;
  ADC2_BASE->SMPR2 = 0;
  ADC_InitTypeDef ADC_InitStructure;
  ADC_StructInit(&ADC_InitStructure);

  ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
  ADC_InitStructure.ADC_ScanConvMode = DISABLE; //ENABLE;
  ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //ENABLE;
  ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
  ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC1;
  ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
  ADC_InitStructure.ADC_NbrOfConversion = 1;
  ADC_Init(ADC2_BASE, &ADC_InitStructure);

  ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConvEdge_Rising; // ???? почему триггер инициализируется именно для ADC2 ???

  ADC_RegularChannelConfig(ADC2_BASE, channel, 1, ADC_SampleTime_112Cycles);
  Serial.println(" = ADC2 OK!");
}

static void DMA_Config(int16_t * buff0)
{
  DMA_InitTypeDef DMA_InitStructure;
  DMA_StructInit(&DMA_InitStructure);
  Serial.print("*C* buff0            ");  Serial.println((uint32_t)buff0, HEX);
  Serial.print("DMA1_BASE            ");  Serial.println((uint32_t)DMA1_BASE, HEX);
  Serial.print("DMA1_Stream0         ");  Serial.println((uint32_t)DMA1_Stream0, HEX);
  Serial.print("DMA2_BASE            ");  Serial.println((uint32_t)DMA2_BASE, HEX);
  Serial.print("DMA2_Stream0         ");  Serial.println((uint32_t)DMA2_Stream0, HEX);
  Serial.print("DMA2_BASE->STREAM[0] ");  Serial.println((uint32_t)&(DMA2_BASE->STREAM[0]), HEX);
  Serial.print("DMA2_Stream0_BASE    ");  Serial.println((uint32_t)DMA2_Stream0_BASE, HEX);
  Serial.print("DMA_GetCmdStatus     ");  Serial.println(DMA_GetCmdStatus(DMA2_Stream0), HEX);
  
  DMA_InitStructure.DMA_Channel = DMA_Channel_0;                                         // +        +        +    +        +
  DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)buff0; // &aADCDualConvertedValue;   // +        +        +    +        +
  DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC_COMMON->CDR;               // 
//  DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1_BASE->DR;                   // +        +        +    +        +
  DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;                                // +        +        +    +        +
  DMA_InitStructure.DMA_BufferSize = FULL_BUFFER_SIZE;                                   // 1        3        4    1        1
  DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;                       // +        +        +    +        +
  DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;                                // Disable  +        +    Disable  Disable
  DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;            // +        Word     +    +        +
  DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;                    // +        Word     +    +        +
  DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;                                        // +        +        +    +        +
  DMA_InitStructure.DMA_Priority = DMA_Priority_High;                                    // +        +        +    +        +
  DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; //Enable;                       // Disable  Disable  +    Disable  Disable
  DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;                      // +        +        +    +        +
  DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;                            // +        +        +    +        +
  DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;                    // +        +        +    +        +
  DMA_Init(DMA2_Stream0, &DMA_InitStructure);

  Serial.println("   DMA MASTER: ADC (DMA2->Stream[0])");
  DMA_ITConfig(DMA2_Stream0, DMA_IT_TC | DMA_IT_HT, ENABLE);   // TransfComplete + Half  // -        -        -    -        -
  
  DMA_Cmd(DMA2_Stream0, ENABLE);
  Serial.println(" = DMA OK!");
}

static void DAC_Config(void)
{
  DAC_InitTypeDef DAC_InitStructure;
  DAC_StructInit(&DAC_InitStructure);
  
  DMA_InitTypeDef DMA_InitStructure;
  DMA_StructInit(&DMA_InitStructure);

  DAC_InitStructure.DAC_Trigger = MyTrigger; // DAC_Trigger_T6_TRGO;
  DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None;
  DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Enable;
  DAC_Init(DAC_Channel_1, &DAC_InitStructure);

  DAC_Init(DAC_Channel_2, &DAC_InitStructure);

  /* DMA1_Stream5 channel7 configuration **************************************/  
  DMA_DeInit(DMA1_Stream5);
  DMA_InitStructure.DMA_Channel = DMA_Channel_7;  
  DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)&DACbuff;
  DMA_InitStructure.DMA_PeripheralBaseAddr = DAC_DHR12RD_ADDRESS;
  DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;
  DMA_InitStructure.DMA_BufferSize = DAC_HALF_BUFFER_SIZE;
  DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
  DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
  DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word; //HalfWord; //  _Byte;
  DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word; //HalfWord; //  _Byte;
  DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
  DMA_InitStructure.DMA_Priority = DMA_Priority_High;
  DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;         
  DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
  DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
  DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
  DMA_Init(DMA1_Stream5, &DMA_InitStructure);    

  DMA_Cmd(DMA1_Stream5, ENABLE);
  
  DAC_Cmd(DAC_Channel_1, ENABLE);
  DAC_Cmd(DAC_Channel_2, ENABLE);

  DAC_DMACmd(DAC_Channel_1, ENABLE);  // Enable DMA for DAC Channel1
  Serial.println(" = DAC OK!");
}

static void GPIO_Config(void)
{
//  GPIO_InitTypeDef GPIO_InitStructure;
//  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2;
//  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;
//  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL ;
//  GPIO_Init(GPIOC, &GPIO_InitStructure);
  pinMode(PC0, INPUT_ANALOG); // (A10) Left Channel Audio Input
  pinMode(PC4, INPUT_ANALOG); // (A14) Right Channel Audio Input
  pinMode(PA4, INPUT_ANALOG); // (A-4) Left Channel Audio Output
  pinMode(PA5, INPUT_ANALOG); // (A-5) Right Channel Audio Output

  pinMode(PC2, INPUT_ANALOG); // (A12) Control Input 1
  pinMode(PC3, INPUT_ANALOG); // (A13) Control Input 2
  pinMode(PA3, INPUT_ANALOG); // (A3) Control Input 3
  pinMode(PA2, INPUT_ANALOG); // (A2) Control Input 4
  pinMode(PA1, INPUT_ANALOG); // (A1) Control Input 5
  pinMode(PA0, INPUT_ANALOG); // (A0) Control Input 6
  pinMode(PA6, OUTPUT);       // LED 0
  pinMode(PA7, OUTPUT);       // LED 1
  pinMode(PC1, OUTPUT);       // LED 2 (A11)
  pinMode(PB10, OUTPUT);      // LED 3

  pinMode(PB11, PWM);                                           // таймер
  GPIO_PinAFConfig(GPIOB_BASE, GPIO_PinSource11, GPIO_AF_TIM2); //
  pinMode(PE2, INPUT_PULLUP);   // Switch 0 // было: для импульса - индикатора пересылки первой половины буфера
  pinMode(PE5, INPUT_PULLUP);   // Switch 1 // было: для импульса - индикатора пересылки второй половины буфера
  pinMode(PE6, INPUT_PULLUP);   // Switch 2
  pinMode(PC13, INPUT_PULLUP);  // Switch 3
  pinMode(PE3, INPUT_PULLUP);   // Button 1
  pinMode(PE4, INPUT_PULLUP);   // Button 0
  Serial.println(" = GPIO OK!");
}

static void TIM2_Config(void)
{
  TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
  TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
  /* --------------------------------------------------------
  TIM2 input clock (TIM2CLK) is set to 2 * APB1 clock (PCLK1), 
  since APB1 prescaler is different from 1.   
    TIM2CLK = 2 * PCLK1  
    TIM2CLK = HCLK / 2 = SystemCoreClock /2 
          
  TIM6 Update event occurs each TIM2CLK/256 
  ----------------------------------------------------------- */
  /* Time base configuration */
  TIM_TimeBaseStructInit(&TIM_TimeBaseStructure); 
  TIM_TimeBaseStructure.TIM_Period = 874; //0xFF;           (Prescaler=1(2), при 2500 - 16.8к, при 438 - 96к)
  TIM_TimeBaseStructure.TIM_Prescaler = 0; // 16800; //0;       (Period=2500, при 16800 - 2 Гц, при 4200 - 8 Гц, при 1050 - 32 Гц, при 1(0) - 16.8 кГц, )
  TIM_TimeBaseStructure.TIM_ClockDivision = 0;    
  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  
  TIM_TimeBaseInit(MyTimer, &TIM_TimeBaseStructure);
//========================================================================
  /* PWM1 Mode configuration: Channel1 */
  TIM_OCInitTypeDef  TIM_OCInitStructure;
  TIM_OCStructInit(&TIM_OCInitStructure);

  TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
  TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;

  /* PWM1 Mode configuration: Channel4 */
  TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
  TIM_OCInitStructure.TIM_Pulse = 83; //CCR4_Val;

  TIM_OC4Init((timer_adv_reg_map*)TIMER2_BASE, &TIM_OCInitStructure);

  TIM_OC4PreloadConfig((timer_adv_reg_map*)TIMER2_BASE, TIM_OCPreload_Enable);

  TIM_ARRPreloadConfig((timer_adv_reg_map*)TIMER2_BASE, ENABLE);
//========================================================================
  /* TIM2 TRGO selection */
  TIM_SelectOutputTrigger(MyTimer, TIM_TRGOSource_Update);
//  TIM_ITConfig(MyTimer, TIM_IT_Trigger, ENABLE); //  

  Timer2.pause();
  Timer2.setCompare(TIMER_CH4, TIM_TimeBaseStructure.TIM_Period / 2);   // somewhere in the middle
  Timer2.refresh();
  Timer2.resume();

  /* TIM2 enable counter */
  TIM_Cmd(MyTimer, ENABLE);

  Serial.println(" = TIMER_2 OK!");
} 


Таймер 2 задает частоту дискретизации 96кГц, подавая ее на ADC, данные ADC контроллер DMA переносит во входной буфер в ОЗУ. По заполнении первой половины буфера происходит аппаратное прерывание, в котором данные первой половины буфера обрабатываются и переносятся в выходной буфер. Одновременно с записью данных с ADC во входной буфер контроллер DMA считывает данные из выходного буфера и отправляет их в порт DAC. Причем после первого прерывания контроллер DMA уже работает со вторыми половинами входного и выходного буферов. По заполнении второй половины снова генерится прерывание и МК теперь может обработать вторые половины буферов, пока записываются первые.

На данном этапе звук претерпевает простейшую обработку - пропускается через ФНЧ с управляемыми аналоговыми органами управления частотой и добротностью. Для расчета фильтров используются таблицы с заранее насчитанными коэффициентами. Правда, эти таблицы взяты мною из другого проекта, где частота дискретизации составляла 48 кГц. Поэтому при работе таблиц в данном проекте они обеспечивают диапазон резонансной частоты фильтра не от 20 Гц до 16 кГц, а от 40 Гц до 32 кГц.

файл фильтра FilterAD_F.h

#ifndef FILTERAD_H
#define FILTERAD_H

#include <Arduino.h>

extern bool FilterADdebug; // лог настройки фильтра - обязательно выключить перед loop()

class tFulter_LF2i { // ФНЧ 2-го порядка : integer(SmallInt)
public:
  int32_t iir32(int32_t NewSample);  // БИХ-фильтр, вычисляет один следующий отсчет  fixed(8.16) (fixed(1.16))
  void SetFilterCoeff(double q, double f); // установка добротности и частоты ФНЧ 2-го порядка для частоты дискретизации 48000
  void SetFilterCoeff_32(uint32_t q, uint32_t f); // установка по номеру в таблице (файлы SoftFilQ.c, SoftFilter.c)

private:
  int32_t AC_0, AC_1, BC_1, BC_2; // коэффициенты фильтра A : fixed(8.24), B : fixed(16.16)
  int32_t x_2, x_1, x_0; // используемые отсчеты фильтра: входные  fixed(24.8)  (fixed(17.8))
  int32_t y_2, y_1, y_0; // используемые отсчеты фильтра: выходные fixed(16.16) (fixed(17.16))
};

void getTimes(int &dt0, int &dt1, int &dt2, int &dt3); // статистика времеени выполнения

#endif

и файл фильтра FilterAD_F.cpp

#include "FilterAD_F.h" // цифровой фильр (БИХ) целочисленный

#include "SoftFilQ.c"
#include "SoftFilter.c"

#define Pi 3.1415926535897932384626433832795

bool FilterADdebug = false; // лог настройки фильтра - обязательно выключить перед loop()

uint32_t t0,t1,t2,t3,t4; // вспомогательные переменные для анализа времени выполнения

int32_t tFulter_LF2i::iir32(int32_t NewSample) { // БИХ-фильтр, вычисляет один следующий отсчет, вход fixed(1.16), выход fixed(.)
union {
  int64_t q;     // присвоение 0.1us, арифм.операция 0.15us, 
  int32_t d[2];  // присв. 8, операций 10 = 0.8+1.5 = 2.3 us
} tmp;
  x_2 = x_1;       // fixed(1.16) сдвигаем предыдущие отсчеты
  y_2 = y_1;       // fixed(8.24) 
  x_1 = x_0;       // fixed(1.16) 
  y_1 = y_0;       // fixed(8.24) 
  x_0 = NewSample; // fixed(1.16)  запоминаем очередное значение
  
// (9.40)  =  (8.24)*(1.16)       (8.24)*(1.16)       (16.16)*(8.24)    (16.16)*(8.24)     (16.16)*(8.24)      <<16
  tmp.q = (AC_0*int64_t(x_0) + AC_1*int64_t(x_1) - BC_1*int64_t(y_1) + AC_0*int64_t(x_2) - BC_2*int64_t(y_2))*65536; // / 65536;
  y_0 = tmp.d[1]; // fixed(8:24) берем ситарщие 32 разряда 64-разрядного слова

  return y_0 >> 8; /// fixed(8.16);
}

void tFulter_LF2i::SetFilterCoeff(double q, double f) { // установка добротности и частоты ФНЧ 2-го порядка для частоты дискретизации 48000
//begin
// A0 = beta^2
// A1 = 2*beta^2
// A2 = beta^2
// B0 = 1 + beta/Q + beta^2
// B1 = 2*beta^2 - 2
// B2 = 1 - beta/Q + beta^2
// beta = alp*tg(Pi/2*f/Fn)
  t0 = micros();
  double beta = tan(Pi*f/48000.0);
  t1 = micros();
  double betaQ = beta/q;
  double beta2 = beta*beta;
  double BC = (1.0 + betaQ + beta2);
  t2 = micros();

////  double b0 = (1.0 + betaQ + beta2)*65536;      // 16.16
  double b1 = (2.0*beta2 - 2.0)/BC*65536;       // 16.16
  double b2 = (1.0 - betaQ + beta2)/BC*65536;   // 16.16
  double a0 = beta2/BC*65536*256;               //  8.24
  double a1 = 2.0*beta2/BC*65536*256;           //  8.24
//  double a2 = beta2/BC*65536*256;             //  8.24
  t3 = micros();

  AC_0 = round(a0);
  AC_1 = round(a1);
//  AC_2 = round(a2);
  BC_1 = round(b1);
  BC_2 = round(b2);

  t4 = micros();
  if(FilterADdebug) {
    Serial.print("FilterAD q: ");
    Serial.print(q);
    Serial.print(", f: ");
    Serial.print(f);
    Serial.println();
  }
}

void tFulter_LF2i::SetFilterCoeff_32(uint32_t q, uint32_t f) { // установка добротности и частоты ФНЧ 2-го порядка для частоты дискретизации 48000
// A0 = beta^2
// A1 = 2*beta^2
// A2 = beta^2
// B0 = 1 + beta/Q + beta^2
// B1 = 2*beta^2 - 2
// B2 = 1 - beta/Q + beta^2
// beta = alp*tg(Pi/2*f/Fn)
  uint32_t beta = FilterCoef[f][1];   //tan(Pi*f/48000.0); (fixed:16.16) 
  uint32_t betaQ = beta*FilterQ[q];   // (fixed:16.16)*(fixed:20.12) = (fixed:4.28)  !!! пока для (8.24)
  uint32_t beta2 = FilterCoef[f][2];  // (fixed:4.28)  !!! пока для (8.24)
  uint32_t BC = (uint64_t)0x0100000000000000/(0x10000000 + betaQ + beta2);     // (16.48)/(8.24) = (8.24) +> (8.56)/(4.28) = (4.28)

//  double b0 = (1.0 + betaQ + beta2)*65536;      // 16.16
  if(beta2 >= 0x10000000)
    BC_1 = (uint64_t)(2*beta2 - 0x20000000)*BC/0x10000000000; // (8.24)*(8.24)/2^32 = (16.48)/2^32 = (16.16) +> (4.28)*(4.28)/2^40 = (8.56)/2^40=(16.16)
  else
    BC_1 = -(int32_t)((uint64_t)(0x20000000 - 2*beta2)*BC/0x10000000000);   // 16.16
  BC_2 = (uint64_t)(0x10000000 - betaQ + beta2)*BC/0x10000000000;   // 16.16
  AC_0 = (uint64_t)beta2*BC/0x0100000000;           //  8.24 (8.24)*(8.24)/2^24 = (8,24)  +>  (4.28)*(4.28)/2^32 = (8.56)/2^32 = (8.24)
  AC_1 = (uint64_t)(2*beta2)*BC/0x0100000000;       //  8.24
//  AC_2 = AC_0;                //  8.24
  if(FilterADdebug) {
    Serial.print("Filter_32 q: ");
    Serial.print(q);
    Serial.print(", f: ");
    Serial.print(f);
    Serial.print(", beta: ");
    Serial.print(beta);
    Serial.print(", beta2: ");
    Serial.print(beta2);
    Serial.print(", betaQ: ");
    Serial.print(betaQ);
    Serial.print(", BC: ");
    Serial.println(BC);

    Serial.print("FilterAD AC_0: ");
    Serial.print(AC_0);
    Serial.print(", AC_1: ");
    Serial.print(AC_1);
    Serial.print(", BC_0: ");
    Serial.print(BC);
    Serial.print(", BC_1: ");
    Serial.print(BC_1);
    Serial.print(", BC_2: ");
    Serial.print(BC_2); 
    Serial.println();
  }
}

void getTimes(int &dt0, int &dt1, int &dt2, int &dt3) {
  dt0 = t1-t0;
  dt1 = t2-t1;
  dt2 = t3-t2;
  dt3 = t4-t3;
}

Рис.4. А теперь отладка со звуком, идущим с MP3-плеера.

smart_pic
Offline
Зарегистрирован: 17.04.2016

Интересный проект. Для прокачки скилов самое то.

А почему не пошли по пути применения для обработки звука спец ДСП ADAU1701 как самый народный или ADAU1452?

А на STM сделать интерфейс пользователя?. Пошли по этому пути и получился очень достойный пгрегат с большими возможностями.

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Есть у меня ADAU1401 DSP mini board, но что-то я не нашел внятной документации собственно по командам. Обычно рекомендуют программировать при помощи утилиты SigmaStudio, но как-то это не очень интересно.

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

Ну и, кроме того, ARM сам содержит целый набор DSP-шных команд.

В общем, мне показалось, что возможностей для творчества с stm32 больше.

smart_pic
Offline
Зарегистрирован: 17.04.2016

Если будет интересно могу показать какой веб интерфейс управления усилителем с ДСП получился. Пиши в личку.

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

smart_pic пишет:

Пиши в личку.

Может, это и было бы смешно, если бы не исходило от человека, который 5 лет на форуме.

Upper
Онлайн
Зарегистрирован: 23.06.2020

Какова цель данного "Проекта"?

Создать устройство с какой то функциональностью ? Тогда почему не написано в самом начале? Какие звуковые сигналы с какой целью планируется обрабатывать разрабатываемым "процессором звуковых эффектов".

Освоить принципы обработки звука на STM32 ?

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

Upper пишет:

Какова цель данного "Проекта"?

Создать устройство с какой то функциональностью ? Тогда почему не написано в самом начале? Какие звуковые сигналы с какой целью планируется обрабатывать разрабатываемым "процессором звуковых эффектов".

Освоить принципы обработки звука на STM32 ?

https://ru.wikipedia.org/wiki/%D0%97%D0%B2%D1%83%D0%BA%D0%BE%D0%B2%D0%BE...

Не благодари!

smart_pic
Offline
Зарегистрирован: 17.04.2016

Upper пишет:

Какова цель данного "Проекта"?

Чтобы в поисковой выдаче по запросу стало выдавать " процессор звуковых эффектов на ардуино".

А получится или нет - это не важно. Зато всех будет сбивать с толку.

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Upper пишет:

Какова цель данного "Проекта"?

Создать устройство с какой то функциональностью ? Тогда почему не написано в самом начале? Какие звуковые сигналы с какой целью планируется обрабатывать разрабатываемым "процессором звуковых эффектов".

Собственно я специально включил в название слово "разработка" т.к. готового ТЗ еще нет. 

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

Цитата:

Освоить принципы обработки звука на STM32 ?

Принципы обработки они как бы от конкретного контроллера (или процессора) на зависят.

Ну и продолжаю публикацию исходников.

Файл stm32f4xx_common.h

#ifndef __STM32F4xx_COMMON_H
#define __STM32F4xx_COMMON_H

typedef enum {RESET = 0, SET = !RESET} FlagStatus, ITStatus;

typedef enum {DISABLE = 0, ENABLE = !DISABLE} FunctionalState;
#define IS_FUNCTIONAL_STATE(STATE) (((STATE) == DISABLE) || ((STATE) == ENABLE))

typedef enum {ERROR = 0, SUCCESS = !ERROR} ErrorStatus; 

//#define USE_FULL_ASSERT
//#ifdef  USE_FULL_ASSERT

void assert_failed(char* file, unsigned long line);

#define assert_param(expr) ((expr) ? (void)0 : assert_failed((char *)__FILE__, __LINE__))

#define STM32F40_41xxx

#define PERIPH_BASE           ((uint32_t)0x40000000) /*!< Peripheral base address in the alias region                                */
#define AHB1PERIPH_BASE       (PERIPH_BASE + 0x00020000)

#define SET_BIT(REG, BIT)     ((REG) |= (BIT))
#define CLEAR_BIT(REG, BIT)   ((REG) &= ~(BIT))
#define READ_BIT(REG, BIT)    ((REG) & (BIT))
#define CLEAR_REG(REG)        ((REG) = (0x0))
#define WRITE_REG(REG, VAL)   ((REG) = (VAL))
#define READ_REG(REG)         ((REG))
#define MODIFY_REG(REG, CLEARMASK, SETMASK)  WRITE_REG((REG), (((READ_REG(REG)) & (~(CLEARMASK))) | (SETMASK)))

unsigned int CheckControl(unsigned int /*byte*/ index); // проверка вновь полученных от АЦП данных - а не изменились ли они?

#endif /*__STM32F4xx_COMMON_H */

Файл stm32f4xx_common.cpp

#include "Arduino.h"

void assert_failed(char* file, unsigned long line)
{ 
  Serial.println("   !!! ERROR !!!");
  Serial.println(file);
  Serial.println(line);
//  while (1) {}
}

#define OVS_FACTOR 32
#define DIV_FACTOR (8*OVS_FACTOR)
#define COMPARE_FACTOR (6*OVS_FACTOR) // (6*OVS_FACTOR) - так, вроде, устойчиво

//bool sendADC = false;
unsigned int control[6]; // состояние органов управления (редуцированное)
unsigned int ADC_data[6]; // ADC data 10 bit: 0-1023
unsigned int oldCtrl_i[6]; // предыдущее запомненное состояние данных АЦП - для устранения шума (порог шума - 6 единиц младшего разряда)

uint16 a_adc_read(const adc_dev *dev, uint8 channel); // читает аналоговый вход channel через АЦП adc_dev

const uint8 ADC_channels[6] = {12, 13, 3, 2, 1, 0};
uint32_t numChanges = 0;

unsigned int CheckControl(unsigned int /*byte*/ index){ // проверка вновь полученных от АЦП данных - а не изменились ли они?
  ADC_data[index] = a_adc_read(&ADC3, ADC_channels[index]); //analogRead(A[n-32]);
  for(int i = 1; i < OVS_FACTOR; i++) {  ADC_data[index] += a_adc_read(&ADC3, ADC_channels[index]);  };
  unsigned int newCtrl_i = ADC_data[index];
  unsigned int /*byte*/ newCtrl_b = newCtrl_i/DIV_FACTOR;
  if(newCtrl_b != control[index]) {
    if(abs((int)newCtrl_i - (int)oldCtrl_i[index]) >= COMPARE_FACTOR) { // не реагируем, пока разница в сырых 10-битных значениях не превзойдет COMPARE_FACTOR
      control[index] = newCtrl_b;
      oldCtrl_i[index] = newCtrl_i;
      numChanges++;
    }
  }
  return newCtrl_b;
}

Кстати, в процесе работы над проектом обнаружил многочисленные ошибки в ардуиновских файлах поддержки stm32f4xx: отсутствие в структурах существующих регистров, неверные константы (тупо скопированные из stm32f103), отсутствие некоторых аппаратных средств (в частности, таймеров). Пока что пришлось внести правку в файлы: dac.h, timer.h, rccF4.h. Это только то, что оказалось необходимо править в существующих файлах. Кроме них многочисленные заплатки были сделаны в отдельных файлах, добавленных в проект.

Да, и здесь же опубликую схему: