大数据本质上是模拟大数据,许多情况下模拟量数据对于数据分析更有价值。在这篇博文中,我们重点来谈谈Mbed OS 操作系统下的ADC高速数据采样。
Mbed OS 下的模拟量IO
Mbed OS 的 API 中有模拟量IO:
AnalogIn
AnalogOut
它们是针对MCU 内部ADC 输入和DAC 输出。如果使用过它们的化,就知道它们很慢。根本没有办法适应高速数据采集。如果要实现高速ADC 输入,就需要使用STM32 的HAL 库自己来设计。如果外扩SPI 的ADC 芯片也是如此,如过低速的话可以使用DigitalOut 和SPI 类来实现。但是要实现高速,就需要HAL 来配合了。
内置ADC 采样
ADC 数据采集的方式有两种,一种是使用内部的ADC,其优点是和CPU集成在一起,MCU 厂商对内置ADC 的支持非常强大。在STM32 系列中,支持下面几种方式
查询方式
中断方式
DMA方式
显然,Mbed OS 支持的是查询方式。所以很慢。如果使用中断方式,那么每次采样一个模拟量需要产生一次中断,执行一大堆中断处理程序。反而比查询方式还要慢。要实现高速ADC 输入的化,唯有采用DMA 方式最合适。
内置ADC 的缺点是它们的精度只有12位。
使用DMA 方式实现ADC 输入,看上去并不难,其要点是:
1 使用一个定时器定时产生触发信号,触发ADC 采集数据
2 当ADC 采样完成时,触发DMA 将ADC 传输到内存
3 可以设置DMA 位循环方式,启动DMA 是指定一个缓冲区。这样DMA 可以连续采集ADC 数据到内存,期间不需要任何程序的干预。(别忘了,DMA 就是指 外设直接内存访问)
4 DMA 能够产生一个半完成中断(HAL_ADC_ConvHalfCpltCallback),和整个完成中断(HAL_ADC_ConvCpltCallback)。分别是当数据达到缓冲区长度的一半时产生中断和数据达到缓冲区最高位时产生中断。
网络上有许多人写了关于STM32 TIM ADC DMA 数据转换的方式。但是没有一个是完整的。而且存在各种坑,这也不能怪他们,各自的情况不同,而且作者编写和转发的时间也不同。STM32F 包袱也够多的,早期使用标准库,现在又使用HAL 库,又有各种版本cubeMX 工具,所以简单地拷贝/黏贴很难解决问题。
在Mbed OS 下,实现底层IO 程序设计是可行的,采用的是HAL 库。我采用方式是用STM32CubeMX 工具配置好参数之后,然后Copy 到Mbed 中来。
我写了一个mbed OS 例子,它采集两路内置ADC 的数据,并通过UDP 上传到PC 机上供python 做FFT 个显示,速度做到十几M没有问题。希望对大家有所帮助。程序是调通的,请放心参考。
#include "mbed.h"
#include "stm32f4xx_hal.h"
#include "EthernetInterface.h"
static const char* mbedIp = "192.168.31.110"; //IP
static const char* mbedMask = "255.255.255.0"; // Mask
static const char* mbedGateway = "192.168.31.1"; //Gateway
#define SERVER_PORT 2019
#define SERVER_ADDR "192.168.31.99"
#define UDP_PORT 2018
EthernetInterface eth;
UDPSocket udpsocket;
DigitalOut led(PC_6);
//DigitalOut led1(PC_7);
AnalogOut aout(PA_5);
Thread thread;
uint16_t ADC_DMA_ConvertedValue[512];
bool bufFlg;
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
TIM_HandleTypeDef htim3;
#define DMA_FLAG (1UL << 0)
EventFlags dma_flags;
void Error_Handler(void)
{
printf("HAL error\n");
}
extern "C" void TIM3_IRQHandler(void)
{
HAL_TIM_IRQHandler(&htim3);
}
extern "C" void DMA2_Stream0_IRQHandler(void)
{
// led=!led;
HAL_DMA_IRQHandler(&hdma_adc1);
// dma_flags.set(DMA_FLAG);
}
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc){
led=1;
bufFlg=false;
dma_flags.set(DMA_FLAG);
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc){
led=0;
bufFlg=true;
dma_flags.set(DMA_FLAG);
}
/**
* Enable DMA controller clock
*/
void DMA_Init(void){
__HAL_RCC_DMA2_CLK_ENABLE();
hdma_adc1.Instance = DMA2_Stream0;
hdma_adc1.Init.Channel = DMA_CHANNEL_0;
hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_adc1.Init.Mode = DMA_CIRCULAR;
hdma_adc1.Init.Priority = DMA_PRIORITY_MEDIUM;
hdma_adc1.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
HAL_DMA_Init(&hdma_adc1);
__HAL_LINKDMA(&hadc1,DMA_Handle,hdma_adc1);
HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);
}
void TIM_Init(void)
{
// TIM_SlaveConfigTypeDef sSlaveConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
__HAL_RCC_TIM3_CLK_ENABLE();
htim3.Instance = TIM3;
htim3.Init.Prescaler = 72-1;
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 100-1;
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig);
HAL_TIM_Base_Init(&htim3);
//NVIC_EnableIRQ(TIM3_IRQn);
}
void ADC_Init(void){
GPIO_InitTypeDef GPIO_InitStruct;
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_ADC1_CLK_ENABLE();
/**ADC1 GPIO Configuration
PA3 ------> ADC1_IN3
PA4 ------> ADC1_IN4
PA5 ------> ADC1_IN5
PA6 ------> ADC1_IN6
*/
GPIO_InitStruct.Pin = GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6;
GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
hadc1.Instance=ADC1;
hadc1.Init.DataAlign=ADC_DATAALIGN_RIGHT; //右对齐
hadc1.Init.ScanConvMode=ENABLE; //不扫描模式
hadc1.Init.ContinuousConvMode=DISABLE; //不连续转换
hadc1.Init.NbrOfConversion=2; //一个规则通道转换
hadc1.Init.DiscontinuousConvMode=DISABLE; //禁止不连续采样模式
hadc1.Init.NbrOfDiscConversion=0; //不连续采样通道数为0
hadc1.Init.DMAContinuousRequests = ENABLE;
hadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV;
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING;
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO;
HAL_ADC_Init(&hadc1);
ADC_ChannelConfTypeDef ADC1_ChanConf;
ADC1_ChanConf.Channel=3; //通道3
ADC1_ChanConf.Rank=1;
ADC1_ChanConf.SamplingTime = ADC_SAMPLETIME_3CYCLES;
HAL_ADC_ConfigChannel(&hadc1,&ADC1_ChanConf);
ADC1_ChanConf.Channel=4; //通道4
ADC1_ChanConf.Rank=2;
ADC1_ChanConf.SamplingTime = ADC_SAMPLETIME_3CYCLES;
HAL_ADC_ConfigChannel(&hadc1,&ADC1_ChanConf);
}
void wave(void){
uint16_t sample = 0;
while(1) {
for (int i = 0; i < 8; i++) {
sample=sample+512;
aout.write_u16(sample);
wait_us(2);
}
}
}
int main()
{ int i;
printf("ADC DMA Test\n");
eth.set_network(mbedIp,mbedMask,mbedGateway);
eth.connect();
printf("\nConnected IP Address : %s\n", eth.get_ip_address());
udpsocket.open(ð);
udpsocket.bind(eth.get_ip_address(),UDP_PORT);
TIM_Init();
DMA_Init();
ADC_Init();
bufFlg=false;
HAL_ADC_Start_DMA(&hadc1,(uint32_t *)ADC_DMA_ConvertedValue,512);
HAL_TIM_Base_Start_IT(&htim3);
thread.start(wave);
while(1) {
dma_flags.wait_any(DMA_FLAG);
// for (i=0;i<8;i++)
// printf("%d ",ADC_DMA_ConvertedValue[i]);
// printf("\n");
if (bufFlg)
udpsocket.sendto(SERVER_ADDR,SERVER_PORT, &ADC_DMA_ConvertedValue[256], 512);
else
udpsocket.sendto(SERVER_ADDR,SERVER_PORT, &ADC_DMA_ConvertedValue[0], 512);
dma_flags.clear(DMA_FLAG);
// wait(1);
}
}
外置ADC 方式
如果嫌弃内置ADC 的精度不够,那么可以考虑使用外部ADC 芯片方式。ADI ,TI 这些大公司提供了各自ADC 芯片。本人就使用过AD7689,ads1256,ads127L和ads1274 等芯片和STM32 连接。 这些芯片大多数是以SPI 接口与STM32 相连接。Mbed OS 本身支持SPI 接口,如果使用探询方式读取ADC 芯片的话,当然Mbed OS 的SPI 完全可以胜任。如果需要高速ADC 转换的话,就遇到和内置ADC 同样的问题了。需要使用DMA 方式。
深入地研究之后发现,SPI 接口ADC 芯片的DMA 方式也不是省油的灯。而且网络上的成功例子更加是少的可怜。
只有靠自己了,我采取的方案如下
ADC 芯片的主时钟
ADC 芯片需要一个主时钟,可以外接一个晶振。但是ADC 芯片内部通常有一个分频,不过也是固定的几种,也可以通过MCU 可编程输入。我倾向采用MCU 产生,多少也省了个晶振电路。
由 TIM4 的通道1 产生一个PWM 的脉冲信号作为 ADC 的工作时钟。由TIM2 CH1 (OC_1)输出。我选取 2MHz。
ADC芯片 的DRDY 信号的俘获
当ADC 完成一次采样后,DRDY 会产生一个低电平脉冲。MCU 需要尽快地将数据通过SPI 读取。在查询方式下,通常是采取while 语句实现。
while(DRDY){};
Data=ADS127L01_ReadData();
不过 在SPI DMA 方式下,如何启动SPI 的DMA 呢?要知道,STM32 的DMA 是不可以通过GPIO 来触发启动的。
我们采取的方法是使用TIM3 ,将它设置成为 ETR 计数方式(1 个脉冲数),将DRDY 接入 TIM3_ETR 输入脚。一旦DRDY 下降沿到来,TIM3计一个脉冲,产生内部的触发信号TRGO。并且由该信号启动一个DMA,用它来触发SPI 发送的DMA传输。
SPI 的DMA
SPI 是一种主从式同步串行通信,MCU 设置为主模式,ADC 芯片为从模式。当MCU 发送数据时会发送SCLK 时钟,在发送的同时,也接受了数据。说白了,就是通过发送来接受数据。
为了实现SPI 的DMA 传输,需要两个DMA 来实现,一个DMA 由TIM3 启动,用于发送,另一个DMA 用于SPI 接受。
所以说,实现ADC SPI 芯片 的DMA 传输,需要两个TIM ,两个DMA和一个SPI
硬件接口
TI ads127L01/ads1274 ADC 芯片和STM32F429ZI 接线方式。
ADC_CLK <-TIM4 CH1
DRDY -> TIM3_ETR
DOUT -> SPI1 MISO
DIN <- SPI MOSI
SCLK <- SPI CLK
我编写了TI ads127L01 和ads1274两种芯片的驱动,源代码调试通过后在放出来吧!
今天的文章STM32 高速ADC 数据采集(内置,外置SPI,DMA方式)分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/33485.html