本帖最后由 cruelfox 于 2015-12-13 13:05 编辑
一直还没用过STM32系列的ADC, 这次拿Nucleo上面的ADC采一下看看结果怎么样。F410的ADC最快可以到2Msps的采样率,我这次暂且只用100k的采样率来测试。100ksps对于单片机来说也够快的了,也没有什么处理工作做,仅把数据从I2S传出而已。
F410的ADC可以一次启动做连续的选定若干个通道转换,可以用定时器来触发启动,对多通道数据采集是有好处的。不过我就用一个通道了,开始是按Timer1 CC2事件触发来写的程序,实验到最后又改为软件触发了(原因后面说)。用Timer触发不需要额外的中断,只要把比较模式输出(当作PWM那么去做),但不用配置GPIO,就可以了。AD转换完成后可以使用中断来处理结果,也可以程序自由安排什么时候处理数据。F410 ADC的通道是映射到固定的IO pin上的,要把该GPIO pin设置成Analog功能(MODER寄存器中)。我先以每秒4次转换的速度把ADC结果读出来,从串口输出,发现数值跳动很大。以前用AVR的10位ADC,是可以做到跳动1个字的,呃……
继续做高采样速度的吧。串口的传输速度是不够用的了,所以用SPI/I2S口来做。STM32系列的I2S功能是和SPI共享硬件的,用起来就发现有些限制不是很爽:一是时钟问题,MCLK必须从I2SCLK分频出来,至少要4分频,而I2SCLK除非片用外时钟源,否则实现通用的采样率误差大;二是每次访问都只有固定的16-bit字长,一个采样周期要读或写4次,还要自个判断读写的是哪部分;三是只有一条串行数据线,半双工。我用I2S来传输是想利用现有的音频硬件工具,采集和传输到PC上方便。因为I2S有固定的时钟,ADC采样严格同步的话就形成音频流数据了。在Nucleo上面ADC, I2S都用一个主时钟,同步不成问题。
事先的设计是Timer触发ADC,然后ADC用DMA传输数据给I2S输出,做到最后需要操作DAC了我才发现DMA并没有设备到设备的模式,必须是设备和内存之间交换。F410的DMA操作和F0xx系列还不一样,今天没时间折腾了就放弃吧。100kHz采样率还好,16MHz的CPU频率就用中断来搞定应该问题不大。于是程序又简化了,ADC在SPI/I2S的中断中去软件启动,Timer也不用了。12-bit的结果用I2S左声道的前16位传输,后面3个WORD都写0。
照片上右边那块PCB是我的FPGA板,解析I2S数据并通过高速USB传输至PC。查看结果就用我惯用的CoolEdit了
这是ADC输入接了一节旧碱性电池的:
跳动范围有5个bit了。用CoolEdit的统计功能看,噪声水平还是很高的了
-79.4dB 是当作 16-bit 满幅度来看的,实际上ADC只有12-bit, 所以噪声是 -79.4+24=-55.4dB
再看看1kHz正弦波转换的结果
看时域波形很不光滑,DFT结果看下呢:
DFT结果看起来还行。看来就是ADC噪声比较大了,不知道单独设计PCB会不会好点。
程序在这里
- #include<string.h>
- #include "stm32f4xx.h"
- void setup_adc(void);
- void setup_i2s(void);
- int main(void)
- {
- RCC->PLLCFGR = 0x2F001010; // VCO = HSI/16*64, sys=VCO/2
- RCC->CR |= RCC_CR_PLLON;
- while(!(RCC->CR & RCC_CR_PLLRDY));
- // FLASH->ACR = FLASH_ACR_ICEN|FLASH_ACR_DCEN; // no significance
- RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN|RCC_AHB1ENR_GPIOBEN; // enable GPIO ports clock
- __NOP();
- GPIOA->MODER = GPIO_MODER_MODER14_1|GPIO_MODER_MODER13_1 // PA14, PA13 AF (SWD pins)
- |GPIO_MODER_MODER15_1|GPIO_MODER_MODER9_1 // PA15 (UART1-TX), PA9 (TIM1_CH2) AF
- |GPIO_MODER_MODER5_0 // PA5 as general output (LED)
- |GPIO_MODER_MODER0|GPIO_MODER_MODER1; // analog function PA0, PA1
- GPIOA->AFR[1] = 0x70000010; // AF1 for PA9, AF7 for PA15; AF0 for others
- GPIOB->MODER = GPIO_MODER_MODER15_1|GPIO_MODER_MODER13_1|GPIO_MODER_MODER12_1; // AF for I2S2
- GPIOB->AFR[1] = 0x50550000; // AF5 for PB12,13,15 (I2S2 DAT, BCK, LRCK)
- uart_setup();
- uart_wstr("Hello from F410\r\n");
- setup_i2s();
- NVIC_EnableIRQ(SPI2_IRQn);
- setup_adc();
- while(1)
- {
- __WFI();
- /* GPIOA->BSRRL = (1<<5);
- __WFE();
- GPIOA->BSRRH = (1<<5); */
- }
- }
- void setup_adc(void)
- {
- RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
- __NOP();
- ADC->CCR = 0; // default: ADCPRE=0, ADC clock = PCLK/2
- ADC1->CR1 = ADC_CR1_DISCEN; // discontinuous mode enable
- ADC1->SQR3 = ADC_SQR3_SQ1&1; // only 1st conversion in sequence, Ch 1
- ADC1->CR2 = ADC_CR2_ADON; // enable
- // ADC1->CR2 = ADC_CR2_ALIGN|ADC_CR2_ADON; // enable
- }
- void setup_i2s(void)
- {
- RCC->APB1ENR |= RCC_APB1ENR_SPI2EN; // enable SPI2 (I2S) clock
- __NOP();
- SPI2->I2SCFGR = SPI_I2SCFGR_I2SMOD|SPI_I2SCFGR_I2SCFG_1|SPI_I2SCFGR_CHLEN; // I2S 16-bit master TX
- SPI2->I2SPR = SPI_I2SPR_ODD|2; // I2S prescaler, 32MHz/5=6.4MHz BCK, 100kHz FS
- SPI2->CR2 = SPI_CR2_TXEIE;
- SPI2->I2SCFGR |= SPI_I2SCFGR_I2SE; // enable I2S
- }
- void SPI2_IRQHandler(void)
- {
- static char p_side;
- char side;
- side=((SPI2->SR & SPI_SR_CHSIDE)!=0);
- if(side==0 && p_side==1)
- {
- SPI2->DR = ADC1->DR; // ^0x8000; // unsigned conversion
- ADC1->CR2 |= ADC_CR2_SWSTART; // start conversion
- }
- else
- SPI2->DR = 0;
- p_side=side;
- }
复制代码
补充说一下,STM32F410 的 I2S 时钟源。在F0系列里面,I2SCLK是系统的主时钟,要改频率就得主时钟一起切换,局限很大。F410中有改进了,可以从外部提供时钟单独给I2S使用,也可以把PLL时钟的一路给I2S,总之是可以和系统的时钟分开。对Codec接口这样无疑是友好不少。
在我上面贴的代码里面,I2S模块使用的是32MHz的PLL时钟,所以才能产生6.4MHz的CK及100kHz的LRCK。系统运行仍然是默认的16MHz的HSI时钟。