инженер-радиотехник, начальник сектора
0,0
рейтинг
26 мая 2014 в 20:17

Дизайн → Звуковая карта как последовательный порт из песочницы

В современных ПК есть проблема отсутствия простых в использовании интерфейсов. Для использования USB требуется большой объем непростого кода, а для UART нужен переходник USB-COM. Если внешнее устройство несложное, то разработка интерфейса может занять больше времени, чем разработка самого устройства. В то же время во многих устройствах есть аналоговый интерфейс для аудиоустройств, который можно использовать для ввода или вывода данных без какой бы то ни было доработки. Здесь пример ввода данных с платы STM32VLDISCOVERY в ПК с ОС Windows ХР через микрофонный вход. Интерфейс не чисто цифровой, а цифро-аналоговый. Данные с платы передаются пачками из 4-х прямоугольных импульсов разной амплитуды, через ЦАП контроллера. Частота следования импульсов соответствует верхней частоте входного усилителя большинства звуковых карт – 20 кГц. Начало пачки отмечается импульсом удвоенной ширины. Следующие 3 импульса несут информацию, которая заложена в амплитуде импульса. Скорость передачи данных при 4-х разрядном кодировании амплитуды составляет примерно 45 кбит/с.

Код для прошивки STM32VLDISCOVERY:

#include "stm32f10x.h"
#define DAC_DHR12RD_Address      0x40007420
#define BUF_SIZE 640
/* Init Structure definition */
DAC_InitTypeDef            DAC_InitStructure;
DMA_InitTypeDef            DMA_InitStructure;
TIM_TimeBaseInitTypeDef    TIM_TimeBaseStructure;
/* Private variables ---------------------------------------------------------*/
uint32_t DualSine12bit[BUF_SIZE], Idx = 0, Idx2 = 0, Idx3 = 0, a1,a2,a3,a4, cc;
int RR;
double R;  

/* Private function prototypes -----------------------------------------------*/
void RCC_Configuration(void) 
{  
  /* DMA1 clock enable */
  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
  /* GPIOA Periph clock enable */
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
  /* DAC Periph clock enable */
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);
  /* TIM2 Periph clock enable */
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
}

void GPIO_Configuration(void)
{
  GPIO_InitTypeDef GPIO_InitStructure;
  GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_4 | GPIO_Pin_5;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; 
  GPIO_Init(GPIOA, &GPIO_InitStructure);
}

void Timebase_Configuration(void)
{
  TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
  TIM_TimeBaseStructure.TIM_Period = 0x120; //0x04;  0x150;         
  TIM_TimeBaseStructure.TIM_Prescaler = 0x01;       
  TIM_TimeBaseStructure.TIM_ClockDivision = 0x0;    
  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  
  TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);	
	 /* TIM2 TRGO selection */
  TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update);
  /* TIM2 enable counter */
  TIM_Cmd(TIM2, ENABLE);	
}

void DAC_Configuration()
{
	/* DAC channel1 Configuration */
  DAC_InitStructure.DAC_Trigger = DAC_Trigger_T2_TRGO;
  DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None;
  DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Disable; //Enable;
  DAC_Init(DAC_Channel_1, &DAC_InitStructure);
  /* DAC channel2 Configuration */
  DAC_Init(DAC_Channel_2, &DAC_InitStructure);
  /* Enable DAC Channel1: Once the DAC channel1 is enabled, PA.04 is 
     automatically connected to the DAC converter. */
  DAC_Cmd(DAC_Channel_1, ENABLE);
  /* Enable DAC Channel2: Once the DAC channel2 is enabled, PA.05 is 
     automatically connected to the DAC converter. */
  DAC_Cmd(DAC_Channel_2, ENABLE);
  /* Enable DMA for DAC Channel2 */
  DAC_DMACmd(DAC_Channel_2, ENABLE);
}

void DMA_Configuration()
{
	/* DMA1 channel4 configuration */
  DMA_DeInit(DMA1_Channel4);
  DMA_InitStructure.DMA_PeripheralBaseAddr = DAC_DHR12RD_Address;
  DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&DualSine12bit;	
  DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
  DMA_InitStructure.DMA_BufferSize = BUF_SIZE;
  DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
  DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
  DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
  DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
  DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
  DMA_InitStructure.DMA_Priority = DMA_Priority_High;
  DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
  DMA_Init(DMA1_Channel4, &DMA_InitStructure);
  /* Enable DMA1 Channel4 */
  DMA_Cmd(DMA1_Channel4, ENABLE);
}

void Delay(__IO uint32_t nCount)
{
  for(; nCount != 0; nCount--);
}

void Point(uint32_t kx, uint32_t ky, uint32_t ki)
{
   for (Idx2 = 0; Idx2 < BUF_SIZE-10; Idx2++)
   {
	 DualSine12bit[Idx2+10] = DualSine12bit[Idx2];	
	 } 
	 DualSine12bit[0] = 4095;
	 DualSine12bit[1] = 4095;	
   DualSine12bit[2] = 0;
	 DualSine12bit[3] = 0; 	 		 
	 DualSine12bit[4] = 2096 + kx;
	 DualSine12bit[5] = 2000 - kx;    
	 DualSine12bit[6] = 2096 + ky;
	 DualSine12bit[7] = 2000 - ky; 
	 DualSine12bit[8] = 2096 + ki;
	 DualSine12bit[9] = 2000 - ki; 	 
}


/* Private functions ---------------------------------------------------------*/
int main(void)
{
/* System Clocks Configuration */ 	
  RCC_Configuration();

  /* Once the DAC channel is enabled, the corresponding GPIO pin is automatically 
     connected to the DAC converter. In order to avoid parasitic consumption, 
     the GPIO pin should be configured in analog */  
  GPIO_Configuration();

  /* TIM2 Configuration */
  /* Time base configuration */
  Timebase_Configuration();
 
  /* DAC channel1 Configuration */
  DAC_Configuration();
 
  /* DMA1 channel4 configuration */
	DMA_Configuration();

R = 1; RR=1;
a1 = 2023; a2 = 1000;	a3 = 100; a4 = 900; 
 while (1)
  {
  for (Idx = 0; Idx < 32*32; Idx++)
		{
		Idx3 = Idx/32;	
		Point((Idx-Idx3*32)*50,Idx3*50,Idx/32*50);
		}	
  }
}



Код приложения на ПК:

#include <stdlib.h>
#include <stdio.h>
#include <conio.h>
#include <string.h>
#include <fstream.h>
#include <iomanip.h>
#include <windows.h>
#include <math.h>
#include <vcl.h>
#include <mmsystem.h>
#include "mainF_dbl.h"
#pragma hdrstop
#pragma package(smart_init)
#pragma resource "*.dfm"
#define INP_BUFFER_SIZE 16384
#define SAMPLE_RATE     192000

TForm1 *Form1;
static HWAVEIN hWaveIn = NULL;
static WAVEHDR WaveHdr1, WaveHdr2;
static WAVEFORMATEX waveformat ;
static unsigned short Buffer1[INP_BUFFER_SIZE], Buffer2[INP_BUFFER_SIZE], saveBuffer[INP_BUFFER_SIZE];
static signed int RR, saveBuffer2[INP_BUFFER_SIZE];
static BOOL bEnding, bGraph, flag; BOOL bShutOff;
long int RR_max, RR_min, LLL;
int ix, iy, iz, k, kx, ky, m, kp ;


void CALLBACK waveInProc1(HWAVEIN hwi, UINT uMsg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2)
{
   switch(uMsg) {
   case WIM_OPEN: break;

   case WIM_DATA: CopyMemory (saveBuffer, ((PWAVEHDR) dwParam1)->lpData, ((PWAVEHDR) dwParam1)->dwBytesRecorded) ;
    if (bEnding){ waveInReset (hWaveIn); waveInClose (hWaveIn); return; }
   waveInAddBuffer (hwi, (PWAVEHDR) dwParam1, sizeof (WAVEHDR)) ; // Send out a new buffer
   break;

   case WIM_CLOSE:
   waveInUnprepareHeader (hWaveIn, &WaveHdr1, sizeof (WAVEHDR)) ;
   waveInUnprepareHeader (hWaveIn, &WaveHdr2, sizeof (WAVEHDR)) ;
   }
}

//xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
void __fastcall TForm1::startButtonClick(TObject *Sender)
{
bGraph=false;
}
//xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
void __fastcall TForm1::formDestroy(TObject *Sender)
{
   bEnding=false;
}
//xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
void __fastcall TForm1::Button1Click(TObject *Sender)
{
  bGraph=true; 
}

//xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
__fastcall TForm1::TForm1(TComponent* Owner)
   : TForm(Owner)
{
   waveformat.wFormatTag      = WAVE_FORMAT_PCM ;
   waveformat.nChannels       = 1; //2 ;
   waveformat.wBitsPerSample  = 16 ;
   waveformat.nSamplesPerSec  = SAMPLE_RATE ;
   waveformat.nBlockAlign     = waveformat.nChannels * (waveformat.wBitsPerSample / 8);
   waveformat.nAvgBytesPerSec = waveformat.nBlockAlign * waveformat.nSamplesPerSec;
   waveformat.cbSize          = 0 ;

   if (waveInOpen (&hWaveIn, WAVE_MAPPER, &waveformat, (DWORD)waveInProc1, 0, CALLBACK_FUNCTION)){
      Application->MessageBox( "000000000","Error",MB_OK );
      return;
   }

   bShutOff=false;
// Set up headers and prepare them

   WaveHdr1.lpData          = (BYTE *)Buffer1 ;
   WaveHdr1.dwBufferLength  = INP_BUFFER_SIZE*2 ;    //
   WaveHdr1.dwBytesRecorded = 0 ;
   WaveHdr1.dwUser          = 0 ;
   WaveHdr1.dwFlags         = 0 ;
   WaveHdr1.dwLoops         = 1 ;
   WaveHdr1.lpNext          = NULL ;
   WaveHdr1.reserved        = 0 ;
   waveInPrepareHeader (hWaveIn, &WaveHdr1, sizeof (WAVEHDR)) ;

   WaveHdr2.lpData          = (BYTE *)Buffer2 ;
   WaveHdr2.dwBufferLength  = INP_BUFFER_SIZE*2 ;    //
   WaveHdr2.dwBytesRecorded = 0 ;
   WaveHdr2.dwUser          = 0 ;
   WaveHdr2.dwFlags         = 0 ;
   WaveHdr2.dwLoops         = 1 ;
   WaveHdr2.lpNext          = NULL ;
   WaveHdr2.reserved        = 0 ;
   waveInPrepareHeader (hWaveIn, &WaveHdr2, sizeof (WAVEHDR)) ;

   waveInAddBuffer (hWaveIn, &WaveHdr1, sizeof (WAVEHDR)) ;
   waveInAddBuffer (hWaveIn, &WaveHdr2, sizeof (WAVEHDR)) ;
   waveInStart (hWaveIn) ;

   bGraph=true; bEnding = FALSE;
}

void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
if (bGraph){
           kp++;
           if (kp>20){kp=0; Canvas->Brush->Color = Color; Canvas->FillRect(Rect(0,0,512,512));}
k=0; m=0; RR_min=0; RR_max=0; kx=0; ky=0;
   for(int LLL=0; LLL<INP_BUFFER_SIZE; LLL++)
   {
short)(saveBuffer[LL*2]);
     RR = (signed short)(saveBuffer[LLL]);
     if (RR > 0)
       {
        if(RR_max < RR) RR_max = RR;
        if((kx>6)&&(RR_min<30000))      { //&&(k==0)){
                                  m=0;
                                  }
        if(RR_min < 0) {
                        if (m==1) ix = -RR_min*16/1024;
                        if (m==2) iy = -RR_min*16/1024;
                        if (m==3) iz = -RR_min*4/512;
                       }
       flag=false;    kx=0;
       RR_min = 0; ky++;
       }

     if (RR < 0)
       {
        if(RR_min > RR) RR_min = RR;
        if (ky>6){
                      if (m==3) {Canvas->Brush->Color = TColor(RGB(iz, iz, iz));
                      Canvas->FillRect(Rect(ix,iy,ix+16,iy+16));}
                       }
       if(!flag) m++;
       RR_max = 0;
       flag=true;  ky=0;
       kx++;
       }
    }
  }
}



На форме 2 кнопки «Stop» и «Run», а также поле из квадратов, положение которых по координатам х и y определяется амплитудой первых 2-х импульсов, а яркость амплитудой 3-го. Платы соединены телефонным проводом, со стороны ПК стандартный jack для моно, со стороны STM32VLDISCOVERY вывод PA.04, подключенный через эмиттерный повторитель (в STM32VLDISCOVERY выход ЦАП высокоомный ) и делитель для калибровки (переменный резистор).



Переданное в ПК через микрофонный вход из STM32VLDISCOVERY тестовое изображение (поле 32х32 квадрата с градиентом яркости квадратов сверху вниз):

Александр Хабаров @AlexHa
карма
4,0
рейтинг 0,0
инженер-радиотехник, начальник сектора
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Дизайн

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

  • +5
    По мне проще поставить FTDI на плату и управлять через нормальный последовательный порт. А с аудиосигналом много мороки — сложный драйвер извлечения, синхронизации и проверки данных, необходимость подстройки, нестандартность интерфейса, низкая скорость.
    • –1
      Полностью с Вами не согласен! С FTDI, все понятно и просто, но бывают ситуации, когда надо без дополнительных интерфейсов, чтобы ну совсем просто. Я давно такую штуку для себя хочу сделать, но все руки не доходят. Да и интересный способ!

      Единственное, хочу попросить автора выложить исходники целиком, хотябы на гитхаб.
      • 0
        Здесь все исходники, больше нету ничего. Создаете стандартные проекты в кейле и бюильдере, вставляете туда тексты, убираете тестовую часть с квадратиками, ставите место нее что вам нужно и привет. Ну там библиотеки пдключаете в кейле, в бюильдере на форме что-то, как обычно. Провод еще нужно припаять с разъемом.
    • 0
      Это уж кому как. Кому-то так и вообще проще за FTDI в Шотландию съездить, если работодатель одобряет :)
      • 0
        Вы по моему зря паникуете. Готовый FDTI модуль на ебее стоит 30р, я в свое время пачку закупил и нет проблем.
        • 0
          Да все так, только там ведь тоже какие-то дрова под USB ставить нужно? Вдобавок, бывают задачи, когда у COM-порта пропускная способность ниже, чем у такого интерфейса. Если у вас частотная полоса ограничена, то по стандартному протоколу вы меньше передадите, там же бинарные уровни. А здесь уровней много может быть, в зависимости от отношения сигнал/шум. Задач с ограничением полосы канала туча, скажем, нужно связь с подводным устройством наладить на ультразвуке или применить си-би рации для передачи данных. Хотя и там конечно можно применить уже отжившие Dial-Up модемы :)
          И еще — в COM портах не только протоколы стандартные, но и уровни сигналов. Не факт что приемник от 3.3 вольта с платы будет срабатывать.
  • +5
    Где то я уже это видел )
    Скрытый текст

    • 0
      Да, но у автора же «Стильно! Модно! Молодежно!» целых 45 кбод, вместо традиционных 1200 бод.
      Скрытый текст
      image
    • +1
      Вот вам оригинал:
      Скрытый текст

      Самое веселое во всем этом, что снято на кинокамеру.
  • 0
    шикарно, но это скорее старая школа, когда была проблема интерфейсов и приходилось разными штуками изворачиваться, чтобы передать информацию. Не все тут похоже понимают, насколько это шикарная вещь, когда вы в поле за 100 км выехали и надо записать код ошибки, а у вас нифига нет, но тут можно просто с динамика записать данные на микрофон, а потом обработать на компе в офисе, правда для этого нужно переделать на фазовую манипуляцию, а не амплитудную, но это не сложно тут. Плюс в том же поле, можно иметь уже готовую прогу на андроиде, которая автоматом вам скажет что с устройством не так ну и тд, идея стара и хороша, и FTDI тут вовсе не при чём
    • 0
      Совершенно втихаря, фитнес-трекер jawbone старой версии синхронизируется со смартфоном именно как микрофон (через jack). По-моему, гораздо круче, чем bluetooth.
  • 0
    Это почти Covox наоборот!
    • 0
      Ага, у меня была мысль матрицу R-2R вместо встроенного ЦАПа использовать — уж очень он там слаботочный. Потом была мысль использовать операционный усилитель. В конце концов приделал транзистор.
  • +1
    Простыни лучше в спойлер прятать.
    • 0
      Да я там вроде ставил какие-то метки :)

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