玩模拟电路的都需要个音频信号源吧?(只晚射频电路的除外,避免抬杠
) 就算没有专门的信号源,电脑声卡、MP3甚至手机都可以拿来顶替一下。我也经常是用连在电脑上的DAC来获得音频信号的,我也DIY了模拟的低失真信号源,不过那只有单个频点。好,先看我今天做出来的信号源效果图:
呃,这是什么鬼玩意儿? 信号源?
别着急,先看这个:
妥妥的正弦波,这次对了吧。没错,这就是我的信号源出来的。
下面这个呢,看起来好象波形失真了……
其实不是,这只是个巧合,示波器现在是当X-Y显示器用的,再来几个就明白了:
如果X,Y输入同频正弦,相位差90度,示波器X,Y增益相当的话,画出来就是一个圆了。
好,效果图看完了,信号源长啥样?
这东西其实并不大,因为用了单片机,而且可以3.2V电池直供电使用。输出两路音频正弦信号,频率1Hz~23999Hz, 1Hz步进可调。频率很准的,因为是晶振决定。
MCU就是ST的Cortex-m0: STM32F051
不过我这次把MCU给“超频”用了,晶振是49.152MHz, 超过了规格最大48MHz一点。原因是为了采样频率在48000Hz上。那么我用什么来产生模拟信号呢?单片机自带了DAC,这个可以用,但是精度差了点。我另外用了一片音频的Sigma-Delta DAC: Cirrus Logic的CS4344:
就是直插电容旁边的小芯片了,音频专用哦,用在信号源上正合适。这个DAC也很便宜,最高支持192kHz的采样频率,不过我这里只用到48kHz. 通过I2S接口把PCM编码送过去,就出来模拟信号了,因为STM32F051已经有I2S接口了,所以用起来很方便。
线路挺简单的,我多引了些I/O到插针位上,方便以后做其它应用。CS4344的I2S信号MCLK, BCK, LRCK, DATA连到MCU上,作为slave设备。CS4344的模拟输出就按照手册上的简单接法输出了,没有加滤波电路,实际上高频噪声影响还是有的。PCB版图:我惯用的Eagle软件出图。
原来想用一块1602 LCD做显示,几个按钮做控制。后来觉得1602模块本身体积更大,弄出来又不小巧了,一时也没选定盒子。于是我就决定用串口控制吧,设置好了以后可以拔下来,整个更小巧。所以软件上就是更新波形数据,和接收串口命令的干活…… 一个在DMA的中断里面,一个在串口接收中断里面。
- #include "stm32f0xx.h"
- #include "uart.h"
- #include "sinetab.c"
- // sinetable, 12001 entries for 1st quarter of a sine wave
- int16_t dma_wavebuf[2048]; // can hold 512 samples
- int phase1=0, phase2=12000;
- int freq1=1000, freq2=1000;
- int main(void)
- {
- int i;
- RCC->CR |= RCC_CR_HSEON; // enable external crystal osc (16.9344MHz)
- RCC->CFGR = RCC_CFGR_PLLSRC_1|RCC_CFGR_PLLMUL2; // PLL (HSE div) mul 2
- FLASH->ACR = FLASH_ACR_LATENCY|FLASH_ACR_PRFTBE; // one wait state
- while(!(RCC->CR & RCC_CR_HSERDY)); // wait until HSE stable
- RCC->CR |= RCC_CR_PLLON; // turn on PLL
- while(!(RCC->CR & RCC_CR_PLLRDY)); // wait until PLL ready
- RCC->CFGR = RCC_CFGR_SW_PLL; // switch sysclk to PLL (49.152MHz)
- RCC->AHBENR |= RCC_AHBENR_GPIOAEN|RCC_AHBENR_GPIOBEN; // enable GPIO port A & B clock
- GPIOA->MODER = GPIO_MODER_MODER14_1|GPIO_MODER_MODER13_1 // PA14, PA13 AF (SWD pins)
- |GPIO_MODER_MODER10_1|GPIO_MODER_MODER9_1 // PA10, PA9 AF (UART)
- |GPIO_MODER_MODER15_1; // PA15 AF (I2S1_WS)
- GPIOA->AFR[1] = 0x00000110; // AF1 for PA9,PA10; AF0 for others
- RCC->AHBENR |= RCC_AHBENR_DMAEN;
- uart_setup(); // setup USART1, (115200, 8-N-1), use DMA1 ch4
- uart_wstr("\r\nUART OK.\r\n");
- RCC->APB2ENR |= RCC_APB2ENR_SPI1EN; // enable SPI1 (I2S) clock
- GPIOB->MODER = GPIO_MODER_MODER3_1|GPIO_MODER_MODER4_1|GPIO_MODER_MODER5_1; // I2S1
- SPI1->I2SCFGR = SPI_I2SCFGR_I2SMOD|SPI_I2SCFGR_I2SCFG_1|SPI_I2SCFGR_DATLEN_0; // I2S 24-bit master transmit
- SPI1->I2SPR = SPI_I2SPR_MCKOE|2; // I2S prescaler, set for 48kHz fs, MCK enabled (256fs)
- SPI1->CR2 = SPI_CR2_TXDMAEN; // DMA enabled on transfer
- DMA1_Channel3->CPAR=(uint32_t)&(SPI1->DR);
- DMA1_Channel3->CCR=DMA_CCR_PSIZE_0|DMA_CCR_MSIZE_0|DMA_CCR_CIRC // 16-bit, auto reload, as SPIDR is only 16-bit
- |DMA_CCR_MINC|DMA_CCR_DIR // 16bit, memory-incremental, memory-to-peripheral
- |DMA_CCR_TCIE|DMA_CCR_HTIE; // complete and half interrput
- DMA1_Channel3->CMAR=(uint32_t)dma_wavebuf; //wtable;
- DMA1_Channel3->CNDTR = 2048;
- DMA1_Channel3->CCR |= DMA_CCR_EN;
- NVIC_EnableIRQ(DMA1_Channel2_3_IRQn);
- NVIC_SetPriority(DMA1_Channel2_3_IRQn,0);
- NVIC_EnableIRQ(USART1_IRQn);
- SPI1->I2SCFGR |= SPI_I2SCFGR_I2SE; // enable I2S
- uart_wstr("Keys: o/u/i/d/h/t\r\n");
- while (1)
- {
- }
- }
- void USART1_IRQHandler(void)
- {
- char key;
- if(USART1->ISR & USART_ISR_ORE) // Overrun
- {
- USART1->ICR = USART_ICR_ORECF;
- return;
- }
- key=USART1->RDR;
- switch(key)
- {
- case 'i': freq1--; break;
- case 'u': freq1-=10; break;
- case 'e': freq1-=100; break;
- case 'd': freq1++; break;
- case 'h': freq1+=10; break;
- case 't': freq1+=100; break;
- case 'I': freq2--; break;
- case 'U': freq2-=10; break;
- case 'E': freq2-=100; break;
- case 'D': freq2++; break;
- case 'H': freq2+=10; break;
- case 'T': freq2+=100; break;
- case '\t': freq2=freq1; break;
- case '0': phase2=phase1; break;
- case '1': phase2=phase1+12000; break;
- case '2': phase2=phase1+24000; break;
- case '3': phase2=phase1+36000; break;
- default: return;
- }
- if(freq1<1) freq1=1;
- if(freq1>23999) freq1=23999;
- if(freq2<1) freq2=1;
- if(freq2>23999) freq2=23999;
- uart_wstr("SET freq 1: ");
- uart_wdec(freq1);
- uart_wstr("Hz 2: ");
- uart_wdec(freq2);
- uart_wstr("Hz\r\n");
- }
- void DMA1_Channel2_3_IRQHandler(void)
- {
- int16_t *buf_base;
- int i;
- if(DMA1->ISR & DMA_ISR_HTIF3)
- {
- DMA1->IFCR = DMA_IFCR_CHTIF3;
- buf_base=dma_wavebuf;
- }
- if(DMA1->ISR & DMA_ISR_TCIF3)
- {
- DMA1->IFCR = DMA_IFCR_CTCIF3;
- buf_base=dma_wavebuf+1024;
- }
- for(i=0;i<256;i++)
- {
- int x;
- phase1+=freq1;
- phase1%=48000;
- if(phase1>24000)
- {
- if(phase1>36000)
- x=-sinetable[48000-phase1];
- else
- x=-sinetable[phase1-24000];
- }
- else
- {
- if(phase1>12000)
- x=sinetable[24000-phase1];
- else
- x=sinetable[phase1];
- }
- buf_base[4*i]=x>>16;
- buf_base[4*i+1]=x&0xffff;
- phase2+=freq2;
- phase2%=48000;
- if(phase2>24000)
- {
- if(phase2>36000)
- x=-sinetable[48000-phase2];
- else
- x=-sinetable[phase2-24000];
- }
- else
- {
- if(phase2>12000)
- x=sinetable[24000-phase2];
- else
- x=sinetable[phase2];
- }
- buf_base[4*i+2]=x>>16;
- buf_base[4*i+3]=x&0xffff;
- }
- }
复制代码正弦函数我并没有在程序中计算,而是存了一个表,用查表法。一方面是ROM刚好够用,另一方面是对Cortex-M0的浮点库不熟悉就没有此时尝试。采样频率是48000Hz, 如果要输出1Hz的信号,那么一个周期需要48000个样本,根据对称性,只需要存储1/4的正弦就够了。一个样本用24-bit足够,16-bit也差不多(截尾会有一些谐波),我任性地直接用了32-bit整型。计算1Hz的整数倍就很简单,相当于查表时每次跳N个就行了。
最后再强调一下使用CS4344等Sigma-Delta DAC相对于MCU片上DAC的优点:一个是量化精度比较高,谐波失真也低不少。二是独立于MCU,减少了干扰。三是降低了数据处理量,因为DAC带有8倍的上采样,硬件给你完成了插值。(不然,在48k采样率下,直接用零阶保持出来一个20k的信号是什么样子?)