基础自学-关于STM32F103C8T6的DMA_ADC-软件触发的基础应用
程序员文章站
2024-02-24 23:46:10
...
让自己别忘了大学里学的东西,那么就把它记录下下来,效率会比忘记然后重新再去找资料再重头学高的多
1.首先讲讲它的原理
(1)关于单片机的数据传递(CPU方案)
我们知道,单片机的结构中有外设(比如说:Uart,Adc,Time等等)和内存两大块器件,而在main函数里面,对于变量的运算都是指内存里面的数据进行运算,比如说加减乘除之类的,而这些数据是哪里来的呢?绝大多数都是从外设的某个寄存器中获取来的,因为你在单片机的运算,往往是把单片机看成一个系统,那么它的使命就是接收外部信号,然后在单片机内进行运算,然后输出。而对于外部信号的接收,就会需要用到外设的功能,外设将数据自动的存到自己的对应数据寄存器内,然后单片机需要将接收到的数据放到内存中,然后再main函数中进行运算,然后直接显示在电脑上或者再发送到另一个外设,然后通过外设输出。
(2)少数据原始传输的例子
对于单个少量的数据,我们可以直接通过CPU直接搬运的方法来将数据再内存和外设之间传输,举个例子,比如说我要做一个ADC的单通道单次软件触发的功能,那么我们只需要在main函数内通过CPU去询问是否转换完成,并且通过赋值的方式将adc数据寄存器的值赋给内存的某个变量。
(3)有什么问题?
虽然对于少量的数据而言,这个方法并不会对cpu占用过高,但是,当数据读取的频率和数据量增大,cpu将频繁做ADC的读取而耽误了其他的事情,从而CPU效率降低,举个例子,当我们需要对adc的得到的值进行每100次求一个平均值,那么如果通过cpu搬运的方式去运作,就会发现在main函数内的代码执行中,cpu一直在做adc询问和传输的工作,而不能去做其他事情。所以通过cpu直接搬运会出现的问题是,cpu占用率高,无法去做其他事
(4)怎么解决问题?(DMA方案)
所以为了解决这个问题,就出现了DMA这个东西,“直接内存存取器”,它的功能很明确,就是在内存和外设之间进行数据的传输,它和外设有类似的性质,做事情时不需要靠cpu去管理,而是直接自己去运作,也就是说,每次去接受和发送数据时,不需要去让cpu去等待,而是自己去完成,也就是说比如说我要采集100次adc的值然后取平均后,输出给其他单片机,那么对于dma而言,就是接收到一个来自cpu的控制指令(命令dma去搬运这100个来自adc外设的数据到内存的指定位置),然后去搬运数据,搬运完以后,可以选择反馈给cpu(告诉它搬运结束)。或者继续搬运,那么对于cpu而言,相比于传统方式,它跳过了大量的工作,只是做了一个控制和确定完成的两个指令,但是却使得单片机系统得到了同样的结果。
(5)DMA传输的比喻
就比如打个比方,有三个人,分别是领导(adc到内存),专业管理的经理(cpu)和专业搬运的工人(dma)。领导要求搬运40箱水果,那么对于经理而言,如果他去做自己干,那么他只能和常人一样,一次搬一箱水果给领导,然后不能去做其他人的管理,对于工人而言,只能闲着干不了工作。但是如果,经理去做管理的事情,下达命令给工人,那么对于经理,他就可以有其他充足的时间用来去管理其他东西,然后当工人干完以后,只要当时让经理停一下,听一下工人的反馈即可,而对于工人也就得到了充分的工作。那么这样就解决了对于外设到内存通讯之间cpu占有率过高的问题。
两种方案的对比图
2.再看看它的参考代码
(1)M_adc.c文件
#include <M_all.h>
u16 adc_get[buff_sizes];
//------------------------------------------ADC校准函数(嵌在ADC_init函数内)-----------------------------------------------------//
void ADC_Reset(ADC_TypeDef* ADCx)
{
ADC_ResetCalibration(ADCx);
//复位校准寄存器 寄存器置1
while(ADC_GetResetCalibrationStatus(ADCx));
//等待校准寄存器复位完成 寄存器置0
ADC_StartCalibration(ADCx);
//ADC校准 寄存器置1
while(ADC_GetCalibrationStatus(ADCx));
//等待校准完成 寄存器置0
}
//------------------------------------------ADC初始化函数-----------------------------------------------------//
void adc_init(void)
{
ADC_InitTypeDef ADC_InitStructure;
//结构体_ADC基础-声明
GPIO_InitTypeDef GPIO_InitStructure;
//结构体_引脚基础-声明
DMA_InitTypeDef DMA_InitStructure;
//结构体_DMA基础-声明
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
//时钟开启_GPIOA,ADC1
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
//时钟开启_DMA1
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
//结构体_引脚-PA1
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
//结构体_引脚-引脚频率_50Mhz
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
//结构体_引脚-引脚模式_模拟输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
//结构体_引脚_结束配置
DMA_InitStructure.DMA_PeripheralBaseAddr = ADC_1_data_address;
//设置DMA外设地址
DMA_InitStructure.DMA_MemoryBaseAddr = (u32)&adc_get;
//设置DMA内存地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
//DMA方向 -- 外设到内存
DMA_InitStructure.DMA_BufferSize = buff_sizes;
//缓存大小 -- 一次大循环要传的数据大小
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
//DMA外设地址-不扩增
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
//DMA内存地址-扩增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
//DMA的外设数据的大小格式-- 1个字 32位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
//DMA的外设数据的大小格式-- 半个字 16位
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
//DMA的读取次数,一个周期就结束(无循环)
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
//DMA的优先级-中等
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
//DMA的内存到内存传输-关闭
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
//结构体_ADC-总模式_独立模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE;
//结构体_ADC-是否扫描_单通道
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
//结构体_ADC-是否连续_单次
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
//结构体_ADC-触发方式_软件触发
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
//结构体_ADC-对齐方式_右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1;
//结构体_ADC-通道个数_单通道
ADC_Init(ADC1, &ADC_InitStructure);
//结构体_ADC_结束配置
// RCC_ADCCLKConfig(RCC_PCLK2_Div2);
//ADC1-时钟分频配置 PS:原始输入的ADC输入时钟是1/2的系统时钟
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_1Cycles5);
//配置内容和ADC外设的具体对接函数 (ADC端口 ,ADC通道 ,转换序号-第几个转换 ,转换的周期)
ADC_DMACmd(ADC1,ENABLE);
//开关_ADC的DMA通道-开关
DMA_Cmd(DMA1_Channel1,ENABLE);
//开关_DMA-开关
ADC_Cmd(ADC1,ENABLE);
//开关_ADC-总开关
ADC_Reset(ADC1);
//ADC校准函数
ADC_SoftwareStartConvCmd(ADC1,ENABLE);
//开启软件转换
}
//------------------------------------------ADC运算读取数值函数-----------------------------------------------------//
float Read_ADC_data(ADC_TypeDef* ADCx)
{
float kk=0;
ADC_SoftwareStartConvCmd(ADCx,ENABLE);
//开关_ADC软件触发-开关 状态寄存器为0
while(!ADC_GetFlagStatus(ADCx,ADC_FLAG_EOC));
//等待转换结束 寄存器置1
kk=(3.3*(((float)ADC_GetConversionValue(ADCx)/4096)));
//百分比值转化成电压值, 因为读取了数据寄存器,状态寄存器自动清0
return kk;
//返回电压值
}
(2)main.c文件
int main(void)
{
SystemInit();
//初始化系统时钟 自动配置成72Mhz
ADC_DeInit(ADC1);
//默认化ADC1
adc_init();
//初始化ADC1
while(1);
}
(3)DMA配置图
PS: STM32固件库里面的结构体配置往往都只是涉及到模式的配置,而并没有开关使能等内容,所以并不是说结构体配置完就可以用了,所以 特意把对应功能的使能命令放在函数的末尾,把涉及到的寄存器放在最上面,方便理解和记忆,一般涉及到哪些寄存器、外设和功能,那么找对应的使能开关,然后使能它们即可。
此次涉及到的开关
1.adc_dam通道-开关
2.dma-总开关
3.adc-总开关
4.adc_校准-开关
5.adc_软件触发-开关
关于dma配置函数内的几个参数的注意点:
1. DMA_BufferSize=是指dma干一次活的量,比如说我想收集100个ad值然后求平均值,那么这个值就设为100,并且对应的内存变量数据个数也要是100个(往往是数组变量,个数为100个)
2. PeripheralDataSize和MemoryDataSize=这两个分别是内存和外设的单个数据的大小,比如说adc外设采集到的一个数据是8位的,那么外设的这个值就是8位(byte),内存端也一样。
3. DMA_Mode=是指dma搬运的次数,比如说是循环模式,那么就是每次搬N (缓存量就是 DMA_BufferSize) 个数据到规定的地方,然后搬完一次后,又从外设拿到新的N个数据,把它们放到规定的地方(覆盖掉上一次的数据),然后一直循环。再比如是正常模式,就是说,cpu给了指令搬运N个数据到指定位置,搬了一趟,就结束搬运,直至cpu再次发送指令,
2. PeripheralDataSize和MemoryDataSize=这两个分别是内存和外设的单个数据的大小,比如说adc外设采集到的一个数据是8位的,那么外设的这个值就是8位(byte),内存端也一样。
3. DMA_Mode=是指dma搬运的次数,比如说是循环模式,那么就是每次搬N (缓存量就是 DMA_BufferSize) 个数据到规定的地方,然后搬完一次后,又从外设拿到新的N个数据,把它们放到规定的地方(覆盖掉上一次的数据),然后一直循环。再比如是正常模式,就是说,cpu给了指令搬运N个数据到指定位置,搬了一趟,就结束搬运,直至cpu再次发送指令,
上一篇: 数组学习.