爱上半导体篇
串口发送数据
存储变量a是由单片机的内核通过总线来操作的
TXREG = a,他是把内存里面的数据转移到串口的数据寄存器当中
当串口的寄存器接收到数据后,它就会由串口外设自动的把数据发送出去。本质是把数据由内存转移到串口外设。它是需要内核操作的。
如果发送10000个数据,大概10s。
CPU极其宝贵的资源都消耗在了数据转移上。
为了解决转移数据占用CPU资源的痛点,设计DMA
DMA:Direct Memory Access直接内存访问,它的主要作用就是数据转移
不需要内核参与,这样内核就可以腾出手来做其他事情,比如操作I/O口或响应中断
由内存向外设转移数据
外设向内存转移数据
发送串口1数据和LED灯闪烁没有办法同时进行,它是先发送一万个串口数据,发送数据就需要10秒时间,然后才可以让LED灯闪烁,发送串口数据和LED灯闪烁没有办法同时进行。
若应用DMA,则可以一边发送串口数据,一边让LED灯闪烁
DMA如何实现这个效果?
采用DMA后的程序
最开始阶段先配置好DMA初始化程序,然后开始发送这10000个数据,发送数据就不需要内核参与,此时内核就腾出手来控制LED闪烁
这样的效果就是一边发送串口数据,一边LED闪烁
程序讲解
首先,程序先定义了一个数组,它的每一个值都是0xAA,这个数组里面的数据就是我们要发送的串口数据
这些数据被存储在SRAM内存里面,而且它们被存储的地址是连续的
然后开始配置DMA
1、设置DMA要传输的目标寄存器地址,也就是我们要把数据转移到哪里,比如串口数据寄存器地址是0xFFFF
DMA_PeriAddr = 0xFFFF;
2、设置SRAM内存源地址,也就是我们要转移的第一个数据的内存地址,因为转移数组的第一个内存地址是0x0000
DMA_SramAddr = 0x0000;
3、指明数据转移的方向,以下是内存里面的数据转移到串口外设
DMA_Direction = DMA-Peri;
4、设置传输数据的大小
5、让内存地址自增,因为我们要发送这个数组里面的10000个数据,如果设置内存地址自增的话,它发完第一个地址上的数据,接下来就发送第二个地址上的数据,以此类推。
若设置内存地址不增,则一直发送第一个数据(10000)次
6、让外设地址不增,因为一万个数据都是转移到串口的,所以10000个数据发送同一个地址。
自此配置完DMA
只有配置DMA是需要内核参与,配置完后就不需要内核参与
此时DMA负责数据转移操作
与此同时,内核开始负责让LED灯闪烁。
除了串口发送数据,还有很多数据转移的应用。
比如串口接收数据,它是把串口外设里面的数据转移到内存
还有ADC模数转换,它是把ADC寄存器里面的数据转移到内存里面。
DMA可大大减轻内核工作量。
DMA简介
存储器映像
实验
1、Flash(常量存储在Flash)
2、SRAM(变量存储在SRAM)
3、外设地址
如果我们定义一个ADC结构体的指针,并且指针的地址就是这个外设的起始地址,那这个结构体的每个成员,就正好映射实际的每个寄存器。
起始地址+偏移地址就是该寄存器的实际地址。
ADC1是结构体指针,指向的是ADC1外设的起始地址,访问结构体成员ADC1->DR,就相当于加一个地址偏移。
DMA框图
寄存器
Flash是主闪存,Sram是运行内存。各个外设都可以看成寄存器(也是一种SRAM存储器)。寄存器是一种特殊的存储器。一方面,CPU可以对寄存器进行读写,就像读写运行内存一样。另一方面,寄存器的每一位背后,都连接了一根导线,这些导线可以用于控制外设电路的状态。比如置引脚的高低电平、导通和断开开关、切换数据选择器或者多位组合起来当作计数器、数据寄存器等。因此,寄存器是连接软件和硬件的桥梁。软件读写寄存器,就相当于在控制硬件的执行。
DMA进行数据转运就是从某个地址取内容,再放到另一个地址。
总线矩阵的左端是主动单元,也就是拥有存储器的访问权。右端是被动单元,他们的存储器只能被左端的主动单元读写。
主动单元,内核有DCode和系统总线,可以访问右边的存储器。其中DCode总线是专门访问Flash,系统总线是访问其他东西
另外,由于DMA要转运数据,所以DMA必须要有访问的主动权。DMA1有7个通道,DMA2有5个通道,各个通道可以分别设置它们的转运数据的源地址和目的地址,就可以各自独立工作。
仲裁器:
1、DMA里的仲裁器,虽然多个通道可以独立转运数据,到那时最终DMA总线只有一条,所以所有的通道都只能分时复用这一条DMA总线。如果产生了冲突,就会由仲裁器根据通道的优先级决定先后顺序。
2、在总线矩阵里也有仲裁器,如果DMA和CPU都要访问同一个目标,那么DMA就会暂停CPU的访问,以防止冲突。不过总线仲裁器仍然会保证CPU得到一半的总线带宽,使CPU能够正常工作。
AHB从设备
DMA作为一个外设,它自己也会有相应的配置寄存器,AHB从设备连接在总线右边的AHB总线上。因此,DMA是总线矩阵的主动单元,可以读写各种存储器,也是AHB总线上的被动单元。
CPU通过以下这条线路就可以对DMA进行配置
DMA请求
就是触发的意思,DMA请求线路右边的触发源是各个外设,所以DMA请求就是DMA的硬件触发源,比如ADC转换完成,串口接收到数据,需要触发DMA转运数据的时候,就会通过DMA请求线路,像DMA发出硬件触发信号,之后DMA就可以执行数据转运的工作。
总结:
1、用于访问各个存储器的DMA总线。
2、内部的多个通道,可以进行独立数据转运。
3、仲裁器,用于调度各个通道,防止产生冲突。
4、AHB从设备,用于配置DMA参数。
5、DMA请求用于硬件触发DMA的数据转运。
注意:CPU或者DMA直接访问Flash,只可以读而不可以写;SRAM是运行内存,可以任意读写;数据寄存器(DR)可正常读写。
DMA基本结构(江科大总结)
外设和存储器两个站点都有3个参数。
第一个是起始地址。这两个数据决定数据从哪里来,到哪里去。
第二个是数据宽度,指定一次转运要按多大的数据宽度来进行,可选择字节Byte(8位,uint8_t),半字HalfWord和字word(32位,一次转运uint32_t)
第三个是地址是否自增,作用是指定一次转运完成后,下一次转运是不是要把地址移动到下一个位置去,相当于指针,P++的意思。比如ADC扫描模式,用DMA进行数据转运,外设地址ADC_DR寄存器,地址是不用自增的,但是存储器这边,地址就需要自增了。每转运一个数据后,就往后挪个坑,要不然下次再转就把上次的数据覆盖。
如果要进行存储器到存储器的数据转运,那我们需要把其中一个存储器的地址放在外设的这个站点上。只要你在外设起始地址里写Flash或者SRAM地址,那他就回去FLAH或SRAM找数据。
传输计数器:用来指定总共需要转运几次,它是自减计数器,比如你给他写个5,那DMA就只能进行5次数据转运,转运过程中,每转运一次,计数器的数就会减1。当传输计数器减到0后,DMA不再进行数据转运。 另外,它减到0之后,之前自增的地址,也会恢复到起始地址的位置,以方便之后DMA开始新一轮的转运。
自动重装器:传输计数器减到0之后,是否要自动恢复到最初的值。决定转运模式(单次模式还是循环模式),如转运数组-单次模式,如ADC扫描模式+连续转换,为了配合ADC,DMA也需要循环模式。
触发一次,转运一次,传输计数器自减一次,当传输计数器等于0,且没有自动重装器时,这是无论是否触发,DMA都不会再进行转运。
触发模式:决定DMA需要在什么时机进行转运,由M2M决定硬件触发还是软件触发。软件触发和循环模式不能同时用, 因为软件触发就是把传输计数器清零,循环模式是清零后自动重装。软件触发适用于存储器到存储器。因为M2M是软件启动,不需要时机,并且想尽快完成任务。硬件触发:可选择ADC、串口、定时器作为触发源。使用硬件触发的转运,一般与外设有关的转运。这些转运需要一定的时机,如ADC转换完成,串口收到数据,定时时间到等。
开关控制:给DMA使能,DMA准备就绪,可以进行转运。
DMA进行转运的三个条件
DMA进行转运的三个条件:
1、开关控制,DMA_Cmd必须使能
2、传输计数器必须大于0
3、触发源,必须有触发信号。
注意写传输计数器时,必须要先关闭DMA,在进行,不能再DMA开启时,写传输计数器。
//在里面需要重新给传输计数器赋值,传输计数器赋值,必须要先给DMA失能,
DMA_Cmd(DMA1_Channel1, DISABLE);
//给传输计数器赋值
DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);
//使能,此时DMA再次进行转运
DMA_Cmd(DMA1_Channel1, ENABLE);
细节
每个通道都有一个数据选择器,可以选择硬件触发和软件触发。EN并不是数据选择器的控制位,而是决定这个选择器要不要工作。EN=0,数据选择器不工作,EN=1,数据选择器工作。软件触发后面跟(M2M)的意思是当M2M位=1时,选择软件触发。每个通道的硬件触发源都不同,若想使用某个硬件触发源的话,必须使用它所在的通道,这就是硬件触发的注意事项。如你要使用ADC1来触发,就必须选择通道1.如果使用软件触发,那么通道就可以任意选择。
通道1对应三个硬件触发源,那么选择哪个呢?这个是对应的外设是否开启了DMA输出来决定的。比如要使用ADC1,那么库函数较ADC_DMACmd,必须使用这个库函数开启ADC1的这一路输出,它才有效。如果要选择定时器2的通道3,那会有个TIM_DMACmd函数,用来进行DMA输出控制。因此,这三个触发源,具体使用哪个,取决于你把哪个外设的DMA输出开启。如果三个都开启,右边是一个或门,理论只开启一个。
默认通道号越小,优先级越高。
数据宽度与对齐
当目标数据宽度比源端数据宽度大时,前面空出来的都补0
当目标数据宽度比源端数据宽度小时,将高位舍弃。
总结:如果你把小的数据转到大的里面,高位补0,如果把大的数据转到小的里面,高位舍弃。数据宽度一样,那就没事。
例子-数据转运+DMA
例子-ADC扫描模式+DMA
具体工作流程:左边是ADC扫描模式的执行流程,在这里有7个通道,触发一次后,7个通道依次进行AD转换,然后将转换结果都放到ADC_DR数据寄存器里面,ADC是连续扫描,那DMA就可以使用自动重装,在ADC启动下一轮转换的时候,DMA也启动下一轮的转换。ADC和DMA同步工作。ADC扫描,在每隔单独的通道转换完成后,没有任何标志位,也不会触发中断。所以我们程序不太好判断某一个通道转换完成的时机是什么时候,但他会产生DMA请求,去触发DMA转运。
DMA库函数
void DMA_DeInit(DMA_Channel_TypeDef* DMAy_Channelx); //恢复缺省配置
void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct); //初始化
void DMA_StructInit(DMA_InitTypeDef* DMA_InitStruct); //结构体初始化
void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState); //使能
void DMA_ITConfig(DMA_Channel_TypeDef* DMAy_Channelx, uint32_t DMA_IT, FunctionalState NewState); //中断输出使能
void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber); //设置当前数据寄存器,给这个传输计数器写数据
uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx);//获取当前数据寄存器,返回传输计数器的值,查看还剩多少数据没有转运
FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG);//获取标志位
void DMA_ClearFlag(uint32_t DMAy_FLAG); //清除标志位
ITStatus DMA_GetITStatus(uint32_t DMAy_IT); //获取中断状态
void DMA_ClearITPendingBit(uint32_t DMAy_IT);//清除中断挂起位
初始化后立刻转运
MyDMA.c
#include "stm32f10x.h" // Device header
uint16_t MyDMA_Size;
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
MyDMA_Size = Size;
//RCC开启DMA的时钟,DMA是AHB总线的设备,所以用AHB开启时钟的函数
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
//初始化DMA
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA; //外设站点
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //数据宽度
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable; //是否自增
DMA_InitStructure.DMA_MemoryBaseAddr = AddrB; //存储器站点
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据宽度
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //是否自增
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //传输方向
DMA_InitStructure.DMA_BufferSize = Size; //缓存区大小,传输计数器
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //传输模式,是否使用自动重装,注意,循环模式和软件触发不能够同时使用,如果同时使用,DMA就会连续触发
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; //选择是否是存储器到存储器,其实就是选择硬件触发还是软件触发,这里选择软件触发
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //DMA1_Channel1选择是哪个DMA,也选择了DMA的哪个通道(软件触发,所以任意通道都可以)
//因为这里是存储器到存储器,所以通道可任意选择
//此处选择DMA1,通道1
DMA_Cmd(DMA1_Channel1, DISENABLE);
/*如果选择的是硬件出发,记得在对应的外设调用xxx_DMACmd,开启触发信号的输出*/
/*如果你需要DMA的中断,就调用DMA_ITConfig,开启中断输出,再在NVIC,配置相应的中断通道,然后写中断服务函数*/
/*在运行的过程中,如果转运完成,传输计数器清零,这是想再给传输计数器赋值,则需要DMA失能,写传输计数器,DMA使能*/
}
/*DMA转运的三个条件
1、开关控制,DMA_Cmd必须使能
2、传输计数器必须大于0
3、触发源,必须有触发信号。
*/
void MyDMA_Transfer(void)
{
DMA_Cmd(DMA1_Channel1, DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);
DMA_Cmd(DMA1_Channel1, ENABLE);
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
DMA_ClearFlag(DMA1_FLAG_TC1);
}
MyDMA.h
#ifndef __MYDMA_H
#define __MYDMA_H
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size);
void MyDMA_Transfer(void);
#endif
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyDMA.h"
//DataA,作为待转运的原数据
uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};
//DataB,作为转运数据的目的地(全局变量默认初始值为0)
uint8_t DataB[] = {0, 0, 0, 0};
int main(void)
{
OLED_Init();
//显示dataA
OLED_ShowHexNum(1, 1, DataA[0], 2);
OLED_ShowHexNum(1, 4, DataA[1], 2);
OLED_ShowHexNum(1, 7, DataA[2], 2);
OLED_ShowHexNum(1, 10, DataA[3], 2);
//显示dataB
OLED_ShowHexNum(2, 1, DataB[0], 2);
OLED_ShowHexNum(2, 4, DataB[1], 2);
OLED_ShowHexNum(2, 7, DataB[2], 2);
OLED_ShowHexNum(2, 10, DataB[3], 2);
MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4); //因为需要传输4个数据,所以为4
//转运后
//显示dataA
OLED_ShowHexNum(3, 1, DataA[0], 2);
OLED_ShowHexNum(3, 4, DataA[1], 2);
OLED_ShowHexNum(3, 7, DataA[2], 2);
OLED_ShowHexNum(3, 10, DataA[3], 2);
//显示dataB
OLED_ShowHexNum(4, 1, DataB[0], 2);
OLED_ShowHexNum(4, 4, DataB[1], 2);
OLED_ShowHexNum(4, 7, DataB[2], 2);
OLED_ShowHexNum(4, 10, DataB[3], 2);
while (1)
{
}
}
程序现象
转运多次
初始化后立刻转运的代码,并且转运一次之后,DMA停止工作,如果DataA的数据变化,需要再转运一次,怎么做呢? 此时需要给传输计数器赋值,调用一次MyDMA_Transfer(void),就再次启动一次DMA转运,在函数里面需要重新给传输计数器赋值,传输计数器赋值,必须要先给DMA失能,然后给传输计数器赋值,DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);,然后给DMA使能。开始转运,但需要等待转运完成, while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);,然后清除标志位。
MyDMA.c
#include "stm32f10x.h" // Device header
//全局变量
uint16_t MyDMA_Size;
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
//将Size赋值给全局变量
MyDMA_Size = Size;
//RCC开启DMA的时钟,DMA是AHB总线的设备,所以用AHB开启时钟的函数
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
//初始化DMA
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA; //外设站点
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //数据宽度
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable; //是否自增
DMA_InitStructure.DMA_MemoryBaseAddr = AddrB; //存储器站点
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据宽度
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //是否自增
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //传输方向
DMA_InitStructure.DMA_BufferSize = Size; //缓存区大小,传输计数器
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //传输模式,是否使用自动重装
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; //选择是否是存储器到存储器,其实就是选择硬件触发还是软件触发
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //DMA1_Channel1选择是哪个DMA,也选择了DMA的哪个通道(软件触发,所以任意通道都可以)
//不让DMA初始化后就立刻进行转运,而是等调用Transfer函数后才进行转运
DMA_Cmd(DMA1_Channel1, DISENABLE);
/*如果选择的是硬件出发,记得在对应的外设调用xxx_DMACmd,开启触发信号的输出*/
/*如果你需要DMA的中断,就调用DMA_ITConfig,开启中断输出,再在NVIC,配置相应的中断通道,然后写中断服务函数*/
/*在运行的过程中,如果转运完成,传输计数器清零,这是想再给传输计数器赋值,则需要DMA失能,写传输计数器,DMA使能*/
}
/*DMA转运的三个条件
1、开关控制,DMA_Cmd必须使能
2、传输计数器必须大于0
3、触发源,必须有触发信号。
*/
//转运多次,需要给传输计数器重新赋值
//调用该函数,就再次启动一次DMA转运
//调用一次,再转运一次
void MyDMA_Transfer(void)
{
//在里面需要重新给传输计数器赋值,传输计数器赋值,必须要先给DMA失能,
DMA_Cmd(DMA1_Channel1, DISABLE);
//给传输计数器赋值
DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);
//使能,此时DMA再次进行转运
DMA_Cmd(DMA1_Channel1, ENABLE);
//等待转运完成,TC1为转运完成标志位
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
//标志位置1后需要手动清除
DMA_ClearFlag(DMA1_FLAG_TC1);
}
MyDMA.h
#ifndef __MYDMA_H
#define __MYDMA_H
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size);
void MyDMA_Transfer(void);
#endif
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyDMA.h"
//DataA,作为待转运的原数据
uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};
//DataB,作为转运数据的目的地(全局变量默认初始值为0)
uint8_t DataB[] = {0, 0, 0, 0};
int main(void)
{
OLED_Init();
//将原数组和目的数组的地址传递进函数,转运数据长度为4
MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4);
OLED_ShowString(1, 1, "DataA");
OLED_ShowString(3, 1, "DataB");
OLED_ShowHexNum(1, 8, (uint32_t)DataA, 8);//之所以要强转类型,是因为DataA是指针类型
OLED_ShowHexNum(3, 8, (uint32_t)DataB, 8);
while (1)
{
//第一步,自增,变化一下原数组DataA的测试数据
/*在给 DataA 数组的元素递增的操作中,每次循环都对 DataA 数组的不同元素进行加一操作,即 DataA[0]++、DataA[1]++、DataA[2]++、DataA[3]++。这样做的目的是为了改变 DataA 数组中的值,以便在后续的代码中能够观察到数据传输的效果。通过递增操作,每次循环 DataA 数组的不同元素的值都会增加,这样在每次数据传输之前和之后都可以通过 OLED 显示来观察到数据的变化。*/
DataA[0] ++;
DataA[1] ++;
DataA[2] ++;
DataA[3] ++;
//显示DataA和DataB(转运前)
OLED_ShowHexNum(2, 1, DataA[0], 2);
OLED_ShowHexNum(2, 4, DataA[1], 2);
OLED_ShowHexNum(2, 7, DataA[2], 2);
OLED_ShowHexNum(2, 10, DataA[3], 2);
OLED_ShowHexNum(4, 1, DataB[0], 2);
OLED_ShowHexNum(4, 4, DataB[1], 2);
OLED_ShowHexNum(4, 7, DataB[2], 2);
OLED_ShowHexNum(4, 10, DataB[3], 2);
//延时1秒,方便观看
Delay_ms(1000);
//第三步 调用MyDMA_Transfer()函数,使用DMA进行数据转运
MyDMA_Transfer();
//显示DataA和DataB,检测数据是不是从DataA转运到DataB(转运后)
OLED_ShowHexNum(2, 1, DataA[0], 2);
OLED_ShowHexNum(2, 4, DataA[1], 2);
OLED_ShowHexNum(2, 7, DataA[2], 2);
OLED_ShowHexNum(2, 10, DataA[3], 2);
OLED_ShowHexNum(4, 1, DataB[0], 2);
OLED_ShowHexNum(4, 4, DataB[1], 2);
OLED_ShowHexNum(4, 7, DataB[2], 2);
OLED_ShowHexNum(4, 10, DataB[3], 2);
//延时1秒,方便观看
Delay_ms(1000);
}
}
程序现象
ADC+DMA应用(两个不同的方法,最后实现同一功能)
ADC扫描模式 DMA数据转运
(使用ADC的扫描模式来实现多通道采集,然后使用DMA进行数据转运)
开启ADC到DMA的输出
ADC单次扫描+DMA单次转运模式
AD.c
#include "stm32f10x.h" // Device header
uint16_t AD_Value[4];
/* ADC扫描模式 + DMA数据转运 */
void AD_Init(void)
{
//开启ADC1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
//开启GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//RCC开启DMA的时钟,DMA是AHB总线的设备,所以用AHB开启时钟的函数
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
//该函数用来配置ADCCLK分频器的,他可以对APB2的72Mhz时钟选择2、4、6、8分频,输入到ADCCLK
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
//PA0被初始化成模拟输入的引脚
GPIO_InitTypeDef GPIO_InitStructure;
//模拟输入模式,在AIN模式下,GPIO口是无效的,断开GPIO,防止GPIO口的输入输出对模拟电压造成干扰
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//菜单上的1~4号空位,我填上了0~3这四个通道
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); //通道0放到序列1
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); //通道1放到序列2
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5); //通道2放到序列3
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5); //通道3放到序列4
//用结构体初始化ADC
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 独立模式
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 不使用外部触发,也就是使用内部软件触发
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //单次转换还是连续转换
ADC_InitStructure.ADC_ScanConvMode = ENABLE; //扫描
ADC_InitStructure.ADC_NbrOfChannel = 4; //通道数目,我点了4个菜,你看前4个位置就可以了
ADC_Init(ADC1, &ADC_InitStructure);
/*配置DMA*/
/* DMA 可以想象为服务员 ADC厨师把菜做好后,DMA这个服务员要尽快把菜端出来,防止覆盖 */
//初始化DMA
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; //(端菜的)源头地址
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //数据宽度(我们想要DR寄存器低16位的数据)
//外设寄存器只有一个,地址不用递增
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //不自增,始终转运同一个位置的数据
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value; //存储器站点
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //数据宽度
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址自增
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //传输方向,外设站点是源
DMA_InitStructure.DMA_BufferSize = 4; //缓存区大小,传输计数器
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //传输模式,是否使用自动重装
//触发源为ADC1,厨师每个菜做好了,教我一下,我再去端菜,这样才是合适的时机
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //选择是否是存储器到存储器,其实就是选择硬件触发还是软件触发,此处选择硬件触发
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级
//必须使用DMA1的通道1
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //DMA1_Channel1选择是哪个DMA,也选择了DMA的哪个通道
//开启ADC到DMA的输出
ADC_DMACmd(ADC1, ENABLE);
//不让DMA初始化后就立刻进行转运,而是等调用Transfer函数后才进行转运
DMA_Cmd(DMA1_Channel1, ENABLE);
//传输计数器不为零
//DMA使能
//但是触发源有信号,目前不满足,因为这里是硬件触发,ADC还没启动,不会有触发信号,所以dma是能后不会立刻工作
//开启ADC电源
ADC_Cmd(ADC1, ENABLE);
//对ADC进行校准
ADC_ResetCalibration(ADC1); //复位校准
while (ADC_GetResetCalibrationStatus(ADC1) == SET); //等待复位校准完成(0位复位校准完成,1为初始化复位校准)
ADC_StartCalibration(ADC1); //开始校准
while (ADC_GetCalibrationStatus(ADC1) == SET); //等待校准完成
/*至此,ADC初始化已经完成,ADC处于准备就绪的状态*/
}
//因为ADC到DMA通道设定好了,而且自动运行,不需要判断标志位,使用DMA后,不需要其他方式转运
//此函数:调用该函数,ADC开始转换,连续扫描4个通道,DMA也同步进行转运,AD转换结果依次放在上面的AD_Value数组里
void AD_GetValue(void)
{
//在里面需要重新给传输计数器赋值,传输计数器赋值,必须要先给DMA失能,
DMA_Cmd(DMA1_Channel1, DISABLE);
//给传输计数器赋值
DMA_SetCurrDataCounter(DMA1_Channel1, 4);
//使能,此时DMA再次进行转运
DMA_Cmd(DMA1_Channel1, ENABLE);
//ADC还是单次模式,还需要软件触发ADC转换
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
//最后等待ADC转换和DMA转运完成,转运总是在转换之后,等待ADC转换完成的代码就不需要了
//等待转运完成,TC1为转运完成标志位
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
//标志位置1后需要手动清除
DMA_ClearFlag(DMA1_FLAG_TC1);
}
AD.h
#ifndef __AD_H
#define __AD_H
//把数据存到SRAM数组里,外部可调用
extern uint16_t AD_Value[4];
void AD_Init(void);
void AD_GetValue(void);
#endif
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
int main(void)
{
OLED_Init();
AD_Init();
OLED_ShowString(1, 1, "AD0:");
OLED_ShowString(2, 1, "AD1:");
OLED_ShowString(3, 1, "AD2:");
OLED_ShowString(4, 1, "AD3:");
while (1)
{
//调用getvalue,之后数据就直接跑搭配AD_Value数组里
AD_GetValue();
//显示结果
OLED_ShowNum(1,5,AD_Value[0],4); //通道1
OLED_ShowNum(2,5,AD_Value[1],4); //通道2
OLED_ShowNum(3,5,AD_Value[2],4); //通道3
OLED_ShowNum(4,5,AD_Value[3],4); //通道4
Delay_ms(100); //让他刷新慢些
}
}
DMA连续扫描+DMA循环转运
总结:ADC连续扫描+DMA循环转运,此时硬件外设已经实现了相互配合和高度自动化,各种操作都是硬件自己完成,极大地减轻了软件负担,软件什么都不需要做,也不需要进行任何中断。硬件自动就把活干完。
AD.c
#include "stm32f10x.h" // Device header
uint16_t AD_Value[4];
/* ADC扫描模式 + DMA数据转运 */
void AD_Init(void)
{
//开启ADC1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
//开启GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//RCC开启DMA的时钟,DMA是AHB总线的设备,所以用AHB开启时钟的函数
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
//该函数用来配置ADCCLK分频器的,他可以对APB2的72Mhz时钟选择2、4、6、8分频,输入到ADCCLK
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
//PA0被初始化成模拟输入的引脚
GPIO_InitTypeDef GPIO_InitStructure;
//模拟输入模式,在AIN模式下,GPIO口是无效的,断开GPIO,防止GPIO口的输入输出对模拟电压造成干扰
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//菜单上的1~4号空位,我填上了0~3这四个通道
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); //通道0放到序列1
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); //通道1放到序列2
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5); //通道2放到序列3
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5); //通道3放到序列4
//用结构体初始化ADC
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 独立模式
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 不使用外部触发,也就是使用内部软件触发
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //单次转换还是连续转换
ADC_InitStructure.ADC_ScanConvMode = ENABLE; //扫描
ADC_InitStructure.ADC_NbrOfChannel = 4; //通道数目,我点了4个菜,你看前4个位置就可以了
ADC_Init(ADC1, &ADC_InitStructure);
/*配置DMA*/
/* DMA 可以想象为服务员 ADC厨师把菜做好后,DMA这个服务员要尽快把菜端出来,防止覆盖 */
//初始化DMA
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; //(端菜的)源头地址
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //数据宽度(我们想要DR寄存器低16位的数据)
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //不自增,始终转运同一个位置的数据
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value; //存储器站点
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //数据宽度
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址自增
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //传输方向,外设站点是源
DMA_InitStructure.DMA_BufferSize = 4; //缓存区大小,传输计数器
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //传输模式,是否使用自动重装
//触发源为ADC1,厨师每个菜做好了,教我一下,我再去端菜,这样才是合适的时机
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //选择是否是存储器到存储器,其实就是选择硬件触发还是软件触发,此处选择硬件触发
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级
//必须使用DMA1的通道1
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //DMA1_Channel1选择是哪个DMA,也选择了DMA的哪个通道
//开启ADC到DMA的输出
ADC_DMACmd(ADC1, ENABLE);
//不让DMA初始化后就立刻进行转运,而是等调用Transfer函数后才进行转运
DMA_Cmd(DMA1_Channel1, ENABLE);
//开启ADC电源
ADC_Cmd(ADC1, ENABLE);
//对ADC进行校准
ADC_ResetCalibration(ADC1); //复位校准
while (ADC_GetResetCalibrationStatus(ADC1) == SET); //等待复位校准完成(0位复位校准完成,1为初始化复位校准)
ADC_StartCalibration(ADC1); //开始校准
while (ADC_GetCalibrationStatus(ADC1) == SET); //等待校准完成
/*至此,ADC初始化已经完成,ADC处于准备就绪的状态*/
//ADC触发后,ADC连续转换,DMA循环转运,两者一直在工作,始终把最新的转换结果刷新到SRAM数组里
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
AD.h
#ifndef __AD_H
#define __AD_H
//把数据存到SRAM数组里,外部可调用
extern uint16_t AD_Value[4];
void AD_Init(void);
#endif
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
int main(void)
{
OLED_Init();
AD_Init();
OLED_ShowString(1, 1, "AD0:");
OLED_ShowString(2, 1, "AD1:");
OLED_ShowString(3, 1, "AD2:");
OLED_ShowString(4, 1, "AD3:");
//循环转换、扫描模式
while (1)
{
//显示结果
OLED_ShowNum(1,5,AD_Value[0],4); //通道1
OLED_ShowNum(2,5,AD_Value[1],4); //通道2
OLED_ShowNum(3,5,AD_Value[2],4); //通道3
OLED_ShowNum(4,5,AD_Value[3],4); //通道4
Delay_ms(100); //让他刷新慢些
}
}
另外,这里可以加入一个外设定时器
ADC用单次扫描,再用定时器去定时触发,这样就是定时器触发ADC,ADC触发DMA,整个过程完全自动,不需要程序手动进行操作。——硬件自动化。
各个外设互相连接,互相交织,不再是传统一个CPU,单独控制多个独立外设
而是外设之间互相连接,互相合作,形成一个网状结构,这样完成某些简单繁琐的工作的时候,就不需要CPU同一调度,可以直接通过外设之间相互配合,自动完成,不仅减轻CPU负担,还可以大大提高外设性能。
比如定时器输出,可以通向ADC,DAC或其他定时器,ADC地触发源可以来自定时器或外部中断,DMA地触发源可以来自ADC、定时器、串口等。
补充
1、ALT+鼠标左键进行框选
2、野火-ADC-DMA-单通道
#include "bsp_adc.h"
//当我们使用单ADC的时候,我们只是用数据寄存器的低16数据,全局变量
__IO uint16_t ADC_ConvertedValue;
/* 1、初始化ADC用到的GPIO */
static void ADCx_GPIO_Config(void)
{
//定义变量
GPIO_InitTypeDef GPIO_InitStructure;
//打开 ADC io端口时钟
ADC_GPIO_APBxClock_FUN(ADC_GPIO_CLK,ENABLE);
//配置 ADC IO 引脚模式
//必须为模拟输入
GPIO_InitStructure.GPIO_Pin = ADC_PIN;
GPIO_InitStructure.GPIO_Mode =GPIO_Mode_AIN;
//初始化 ADC io
GPIO_Init( ADC_PORT,&GPIO_InitStructure);
}
/* 2、初始化ADC初始化结构体 */
static void ADCx_Mode_Config(void)
{ /* 先转换再进行转运 */
ADC_InitTypeDef ADC_InitStruct;
DMA_InitTypeDef DMA_InitStructure;
// 打开DMA时钟
RCC_AHBPeriphClockCmd(ADC_DMA_CLK, ENABLE);
// 复位DMA控制器
DMA_DeInit(ADC_DMA_CHANNEL);
// 配置 DMA 初始化结构体
// 外设基址为:ADC 数据寄存器地址
DMA_InitStructure.DMA_PeripheralBaseAddr = ( uint32_t ) ( & ( ADC_x->DR ) );
// 存储器地址,实际上就是一个内部SRAM的变量,将转换结果搬到存储器里面-->ADC转换结束后,把ADC的DR寄存器的数据搬运到存储器变量中
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&ADC_ConvertedValue;
// 数据源来自外设-->方向为外设到存储器
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
// 缓冲区大小为1,缓冲区的大小应该等于存储器的大小
DMA_InitStructure.DMA_BufferSize = 1; //1个通道
// 外设寄存器只有一个,地址不用递增
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //地址不需要自动增量
// 存储器地址固定
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Disable; //地址不需要自动增量
// 外设数据大小为半字,即两个字节(因为数据寄存器低16位有用)AD转换的结果为12位,因此用半字,16位
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
// 存储器数据大小也为半字,跟外设数据大小相同
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
// 循环传输模式,不断传输
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //循环模式,搬运完一次,它会把DMA_InitStructure.DMA_BufferSize = 1; 加载到传输计数器中
// DMA 传输通道优先级为高,当使用一个DMA通道时,优先级设置不影响
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
// 禁止存储器到存储器模式,因为是从外设到存储器(P - > M)
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
// 初始化DMA
DMA_Init(ADC_DMA_CHANNEL, &DMA_InitStructure);
// 使能 DMA 通道
DMA_Cmd(ADC_DMA_CHANNEL , ENABLE);
/*=================================*/
//打开外设时钟,设计RCC的固件库函数
ADC_APBxClock_FUN(ADC_CLK,ENABLE);
//初始化成员变量
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent ;//独立模式
ADC_InitStruct.ADC_ScanConvMode = DISABLE; //单通道选择DISABLE,不扫描
ADC_InitStruct.ADC_ContinuousConvMode = ENABLE; //连续转换
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//软件触发,不选择外部触发
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; //数据对齐:右对齐
ADC_InitStruct.ADC_NbrOfChannel = 1;//需要转换通道的数目
//将成员全写进寄存器里面
ADC_Init(ADC_x,&ADC_InitStruct);
//3、配置ADC时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div8);//8分频,72/8=9MHZ,该时钟与转换时间息息相关
//3、配置通道的转换顺序和采样时间(使用哪个ADC,哪个通道, ,采样时间)
ADC_RegularChannelConfig(ADC_x, ADC_CHANNEL, 1, ADC_SampleTime_55Cycles5);
/* 使能ADC_DMA 请求 */
ADC_DMACmd(ADC_x,ENABLE);
//5、使能ADC, 准备开始转换
ADC_Cmd(ADC_x, ENABLE);
//6、校准ADC
ADC_StartCalibration(ADC_x);
//等待校准完成
while(ADC_GetCalibrationStatus(ADC_x));
//7、软件触发ADC,真正开始转换
ADC_SoftwareStartConvCmd(ADC_x, ENABLE);
//9、编写main函数,把转换的数据打印出来
}
//ADC初始化函数,集成上面三个函数
void ADCx_Init(void)
{
ADCx_GPIO_Config();
ADCx_Mode_Config();
}
过程
一旦启动ADC之后,它就会按照单通道非扫描模式进行转换,转换结束后,它的结果会存放在DR数据寄存器里面,然后我们使能ADC_DMACmd(ADC_x,ENABLE);,他就会实时地将DR数据寄存器里面的值搬运到存储器的变量ADC_ConvertedValue里面,我们只需要读取该变量,我们就能得到转换结果。一旦转换结束和搬运结束后,由于使能循环模式和连续转换,他就把传输计数器的值重新加载到传输计数器中,它继续启动一次新的转换,过程以此类推。
3、 野火-ADC-DMA-多通道
过程
#include "bsp_adc.h"
//当我们使用单ADC的时候,我们只是用数据寄存器的低16数据,以下是数组,6个通道就定义一个大小为6的数组
__IO uint16_t ADC_ConvertedValue[NOFCHANEL] = {0,0,0,0,0,0};
/* 1、初始化ADC用到的GPIO */
static void ADCx_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 打开 ADC IO端口时钟
ADC_GPIO_APBxClock_FUN ( ADC_GPIO_CLK, ENABLE );
// 配置 ADC IO 引脚模式
GPIO_InitStructure.GPIO_Pin = ADC_PIN1|
ADC_PIN2|
ADC_PIN3|
ADC_PIN4|
ADC_PIN5|
ADC_PIN6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
// 初始化 ADC IO
GPIO_Init(ADC_PORT, &GPIO_InitStructure);
}
/* 2、初始化ADC初始化结构体 */
static void ADCx_Mode_Config(void)
{
DMA_InitTypeDef DMA_InitStructure;
ADC_InitTypeDef ADC_InitStruct;
// 打开DMA时钟
RCC_AHBPeriphClockCmd(ADC_DMA_CLK, ENABLE);
// 复位DMA控制器
DMA_DeInit(ADC_DMA_CHANNEL);
// 配置 DMA 初始化结构体
// 外设基址为:ADC 数据寄存器地址
DMA_InitStructure.DMA_PeripheralBaseAddr = ( uint32_t ) ( & ( ADC_x->DR ) );
// 存储器地址,实际上就是一个内部SRAM的变量,不去要取地址符,因为ADC_ConvertedValue是数组名,代表数组的首地址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)ADC_ConvertedValue;
// 数据源来自外设
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
// 缓冲区大小为1,缓冲区的大小应该等于存储器的大小
DMA_InitStructure.DMA_BufferSize = NOFCHANEL; //6个通道
// 外设寄存器只有一个,地址不用递增
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
// 存储器地址固定,Memory我们定义成一个数组,因此增量指针需要ENABLE
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
// 外设数据大小为半字,即两个字节(因为数据寄存器低16位有用)
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
// 存储器数据宽度大小也为半字,跟外设数据大小相同
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
// 循环传输模式,不断传输
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
// DMA 传输通道优先级为高,当使用一个DMA通道时,优先级设置不影响
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
// 禁止存储器到存储器模式,因为是从外设到存储器(P - > M)
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
// 初始化DMA
DMA_Init(ADC_DMA_CHANNEL, &DMA_InitStructure);
// 使能 DMA 通道
DMA_Cmd(ADC_DMA_CHANNEL , ENABLE);
/*====================================================================*/
//打开外设时钟,设计RCC的固件库函数
ADC_APBxClock_FUN(ADC_CLK,ENABLE);
//初始化成员变量
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent ;//独立模式
ADC_InitStruct.ADC_ScanConvMode = ENABLE; //多通道选择ENABLE,扫描
ADC_InitStruct.ADC_ContinuousConvMode = ENABLE; //连续转换
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//软件触发,不选择外部触发
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; //数据对齐:右对齐
ADC_InitStruct.ADC_NbrOfChannel = NOFCHANEL;//需要转换通道的数目为 6
//将成员全写进寄存器里面
ADC_Init(ADC_x,&ADC_InitStruct);
//3、配置ADC时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div8);//8分频,72/8=9MHZ,该时钟与转换时间息息相关
// 配置ADC 通道的转换顺序和采样时间,将6个通道加到规则组里面
ADC_RegularChannelConfig(ADC_x, ADC_CHANNEL1, 1, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC_x, ADC_CHANNEL2, 2, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC_x, ADC_CHANNEL3, 3, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC_x, ADC_CHANNEL4, 4, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC_x, ADC_CHANNEL5, 5, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC_x, ADC_CHANNEL6, 6, ADC_SampleTime_55Cycles5);
/* 使能ADC DMA 请求 */
ADC_DMACmd(ADC_x,ENABLE);
//5、使能ADC, 准备开始转换
ADC_Cmd(ADC_x, ENABLE);
//复位校准
ADC_ResetCalibration(ADC_x); //复位校准
while (ADC_GetResetCalibrationStatus(ADC_x) == SET); //等待复位校准完成(0位复位校准完成,1为初始化复位校准)
//6、校准ADC
ADC_StartCalibration(ADC_x);
//等待校准完成
while(ADC_GetCalibrationStatus(ADC_x));
//7、软件触发ADC,真正开始转换
ADC_SoftwareStartConvCmd(ADC_x, ENABLE);
//9、编写main函数,把转换的数据打印出来
}
//ADC初始化函数,集成上面三个函数
void ADCx_Init(void)
{
ADCx_GPIO_Config();
ADCx_Mode_Config();
}
软件触发ADC,真正开始转换,通道1开始转换,待转换结束后,DMA就会把保存在ADC1->DR的值搬运到存储器站点里面,uint16_t ADC_ConvertedValue[0],然后,ADC接着扫描通道2,转换结束后,DMA会把数据搬运到下标为1的存储器里面uint16_t ADC_ConvertedValue[1],ADC依次扫描转换6次,DMA自动搬运6次。
金善愚-通道17内部参照电压用来校准ADC转换结果
金善愚-DMA
搬运方向:Flash->SRAM, SRAM与SRAM之间, SRAM与外设寄存器之间
注意SRAM不可搬到Flash,Flash只读。
特定外设申请特定的通道,不同的外设申请的DMA通道是不一样的。但是存储器与存储器之间任何一个通道都可以。
DMA数据传输配置·
注意
如果是flash,程序运行过程中不可对其进行写操作。
查询
中断
ADC+DMA
只有在规则通道的转换结束时才产生DMA请求。
规则通道可以使用多个通道进行转换,但是,规则通道转换结束后存储数据的寄存器只有一个。
当你在多通道的扫描模式的情况下,只有规则通道的最后一个通道转换完成之后才会产生EOC标志。这时候读取结果只能读取最后的结果。由于只有一个DR寄存器,前面的结果都被覆盖了。
因此,例如先转换通道2,转换完成之后,它可以申请DMA,这个时候可以将ADC_DR寄存器的数据搬运到指定的存储器里面去。接着转换通道6,然后如此类推。自此,借用DMA实现多通道的扫描模式的ADC转换。
多通道扫描模式
对应
三个结果对应ADC转换的3个通道,一旦触发后,每个通道转换结束后,转换结果会存放在DR寄存器里面,开启DMA,它会把DR寄存器的值搬运到数组元素里面。
借助DMA实现AD的多通道转换,又实现了数据的校准。
总结:DMA就是把一个站点的数据搬运到另一个站点,至于什么时候搬运,会有一个触发信号,搬运几次由传输计数器来决定。传输计数器的值是否在搬完数据后重新加载取决于循环模式还是单次模式。如果RAM->RAM,一般为单次模式,如果外设到存储器,一般是循环模式。什么时候触发,就看外设,像ADC这种外设,只有在规则通道的转换结束时才产生DMA请求。然后在主程序中根据需要取结果。
代码
adc.c
//#include "adc.h"
#include "stm32f10x.h"
//ADC1 通道1 PA1 通道16 内部温度传感器 通道17 内部基准源1.2V
//多通道,那么我就定义为一个数组
uint16_t ad_result[3];
void adc_Init(void)
{
//时钟
//使能GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//使能ADC1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
//使能DMA时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
/*ADCCLK / 6 < 14M*/
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
ADC_InitTypeDef ADC_InitStructure;
DMA_InitTypeDef DMA_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
//配置GPIO
//ADC1 通道1 PA1 配置成模拟输入模式 硬件决定
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//DMA1 通道1 配置 ADC1 对应 通道1
DMA_DeInit(DMA1_Channel1);
//我们要实现的是ADC转换结束后,把ADC1_DR寄存器的数据转运到ad_result上,实现数据转运
//因为ADC的规则通道一旦转换完成就会申请DMA,只要我们启动DMA,就会将DR的数据搬运到存储器里面来,只要读取寄存器就可以得到转换结果
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(ADC1->DR); //借助编译器找DR寄存器地址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&ad_result; //存储器,将转换结果搬运到存储器里面来
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //外设作为源
DMA_InitStructure.DMA_BufferSize = 3; //由于是3个通道,因此传输计数器的值为3
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //因为外设不需要增长
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable ; //多通道需要自增,如通道1 存储第0个元素 通道16存储第1个元素。。。
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //由于ADC的12位转换结果,只需要半字即可
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;//连续模式,搬运完一次自动将传输计数器的值加载到传输计数器里面
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //非M2M,因为外设到存储器,所以失能
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
/* Enable DMA1 channel1 */
DMA_Cmd(DMA1_Channel1, ENABLE);
//配置ADC
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //独立模式
ADC_InitStructure.ADC_ScanConvMode = ENABLE; //多通道扫描模式
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //连续转换则使能
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //软件触发,无外部触发
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据右对齐
ADC_InitStructure.ADC_NbrOfChannel = 3; //对应3个通道需要转换
ADC_Init(ADC1, &ADC_InitStructure);
/* ADC1 regular channels configuration */
ADC_RegularChannelConfig(ADC1, ADC_Channel_17,1, ADC_SampleTime_239Cycles5); //得到基准源
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_28Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_16, 3,ADC_SampleTime_239Cycles5);
//使能内部温度传感器和内部的参考电压基准的通道
ADC_TempSensorVrefintCmd(ENABLE);
/* Enable ADC1 DMA */
ADC_DMACmd(ADC1, ENABLE);
//开启ADC电源
ADC_Cmd(ADC1, ENABLE);
//ADC校准
/* Enable ADC1 reset calibration register */
ADC_ResetCalibration(ADC1);
/* Check the end of ADC1 reset calibration register */
while(ADC_GetResetCalibrationStatus(ADC1));
/* Start ADC1 calibration */
ADC_StartCalibration(ADC1);
/* Check the end of ADC1 calibration */
while(ADC_GetCalibrationStatus(ADC1));
//软件触发进行ADC转换
/* Start ADC1 Software Conversion */
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
//无需以下程序,因为转换结果已经存储到ad_result里面了,因为ADC转换完成之后,DMA已经搬运了数据了
获取ADC转换结果
//uint16_t get_ADC_result(void)
//{
// while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); //等待,没有转换完成,那么标志位就是reset
// return ADC_GetConversionValue(ADC1);
//}
adc.h
#ifndef __ADC_H
#define __ADC_H
extern uint16_t ad_result[3];
void adc_Init(void);
//uint16_t get_ADC_result(void);
#endif
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
#include "adc.h"
int main(void)
{
uint8_t i;
float ad_value,ad_vref,ad_sense,temp;
OLED_Init();
Serial_Init();
printf("STM32 ADC1 DMA test\r\n");
adc_Init();
while (1)
{
for(i = 0; i<3; i++)
{
if(i==0)
{
//打印基准源 转电压值
ad_vref = 1.2 * 4096 / ad_result[0];
printf("ad_vref = %0.3f\r\n",ad_vref);
}
else if(i == 1)
{
ad_value = ad_result[1] * ad_vref /4096 ;
printf("CH1 ad_value = %0.2f\r\n",ad_value); //保留小数点后两位
}
else if(i == 2) //转温度
{
ad_sense = ad_result[2] * ad_vref / 4095;
printf("ad_sense = %0.2f\r\n",ad_sense);
temp = (1.43 - ad_sense) / 0.0043 + 25;
printf("TEMP = %0.1f\r\n",temp);
}
}
Delay_ms(2000);
}
}
//模拟输入量
//转换结果的模拟电压值
串口DMA方式发送数据
使用外设的时候需要找到对应的DMA通道是哪一个,方便后续配置工作。
应用场景
串口发送-查询或者中断方式,
中断–>如果发送大量数据,那么中断需要频繁进入中断,每发送一个字节进入一次中断,若系统是一个实时性系统,很多个事情需要处理,那么会影响其他中断。查询–>CPU需要大量搬运数据到DR寄存器中,占用CPU资源
DMA解决大的数据量和大的波特率传输。
代码
Serial.c
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h>
#include "Serial.h"
uint8_t TxBuffer1[TxBufferSize];
void Serial_Init(void)
{
//使能串口和GPIO和复用功能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //tx
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; //rx
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 460800; //波特率设置
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStructure);
/* Enable USART1 DMA TX request */
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); //使能DMA的发送请求功能
//使能串口
USART_Cmd(USART1, ENABLE);
}
void DMA_Configuration(void)
{
DMA_InitTypeDef DMA_InitStructure;
//DMA时钟使能
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
/* USARTy_Tx_DMA_Channel */
DMA_DeInit(DMA1_Channel4);
//借助串口往外发数据,存储器-》》外设寄存器
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(USART1->DR); //串口的DR寄存器,外设站点的起始地址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)TxBuffer1; //存储器站点的起始地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //此时外设站点作为目的地
DMA_InitStructure.DMA_BufferSize = TxBufferSize; //数据量,搬运多少,传输计数器多少,不为0则开始搬运
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设站点不需要自增地址
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器站点自增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //数据宽度 1个字节
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据宽度 1个字节
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //单次模式,什么时候发送由程序控制
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;//优先级
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //失能
DMA_Init(DMA1_Channel4, &DMA_InitStructure); //DMA1 通道4
//使能哪个通道的DMA1
DMA_Cmd(DMA1_Channel4, DISABLE); //失能DMA
}
//单独写一个DMA转运函数,可实现多次转运
void UART1_DMA_TXD(void)
{
DMA_Cmd(DMA1_Channel4, DISABLE); //失能DMA
DMA_SetCurrDataCounter(DMA1_Channel4, TxBufferSize);//人为设置传输计数器的值,因为DMA_Mode设置为单次转运模式
DMA_Cmd(DMA1_Channel4, ENABLE); //使能DMA
while (DMA_GetFlagStatus(DMA1_FLAG_TC4) == RESET); //等待转运完成
DMA_ClearFlag(DMA1_FLAG_TC4); //清除转运数据完成标志位
}
//void Serial_SendByte(uint8_t Byte)
//{
// USART_SendData(USART1, Byte);
// //等TDR的数据转移到移位寄存器
// while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
//}
//void Serial_SendArray(uint8_t *Array, uint16_t Length)
//{
// uint16_t i;
// for (i = 0; i < Length; i ++)
// {
// Serial_SendByte(Array[i]);
// }
//}
//void Serial_SendString(char *String)
//{
// uint8_t i;
// for (i = 0; String[i] != '\0'; i ++)
// {
// Serial_SendByte(String[i]);
// }
//}
//uint32_t Serial_Pow(uint32_t X, uint32_t Y)
//{
// uint32_t Result = 1;
// while (Y --)
// {
// Result *= X;
// }
// return Result;
//}
//void Serial_SendNumber(uint32_t Number, uint8_t Length)
//{
// uint8_t i;
// for (i = 0; i < Length; i ++)
// {
// Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');
// }
//}
//int fputc(int ch, FILE *f)
//{
// Serial_SendByte(ch);
// return ch;
//}
//void Serial_Printf(char *format, ...)
//{
// char String[100];
// va_list arg;
// va_start(arg, format);
// vsprintf(String, format, arg);
// va_end(arg);
// Serial_SendString(String);
//}
//uint8_t Serial_GetRxFlag(void)
//{
// if (Serial_RxFlag == 1)
// {
// Serial_RxFlag = 0;
// return 1;
// }
// return 0;
//}
//uint8_t Serial_GetRxData(void)
//{
// return Serial_RxData;
//}
///*
//所以接受移位寄存器是从高位往低位这个方向移动,当一个字节移位完成后,
//这一个字节的数据就会整体地转移到接收数据寄存器RDR里,在转移的过程中会置一个标志位,叫RXNE(RX Not Empty),
//接收数据寄存器非空。当我们检测到RXNE置1之后,就可以把数据读走。同时,这个标志位可以去申请中断,在收到数据时,直接进入中断函数。
//*/
//void USART1_IRQHandler(void)
//{
// if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
// {
// Serial_RxData = USART_ReceiveData(USART1);
// Serial_RxFlag = 1;
// USART_ClearITPendingBit(USART1, USART_IT_RXNE);
// }
//}
Serial.h
#ifndef __SERIAL_H
#define __SERIAL_H
#include <stdio.h>
#include "stm32f10x.h"
#define TxBufferSize 1024
extern uint8_t TxBuffer1[TxBufferSize];
void Serial_Init(void);
//void Serial_SendByte(uint8_t Byte);
//void Serial_SendArray(uint8_t *Array, uint16_t Length);
//void Serial_SendString(char *String);
//void Serial_SendNumber(uint32_t Number, uint8_t Length);
//void Serial_Printf(char *format, ...);
//uint8_t Serial_GetRxFlag(void);
//uint8_t Serial_GetRxData(void);
void DMA_Configuration(void);
void UART1_DMA_TXD(void);
#endif
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
int main(void)
{
uint32_t i;
//初始化DMA
DMA_Configuration();
//初始化内存数据
for(i = 0; i<TxBufferSize;i++)
{
TxBuffer1[i] = i;
}
//初始化串口
Serial_Init();//460800
while (1)
{
//每次发送时整体数据+1
for(i = 0; i<TxBufferSize;i++)
{
TxBuffer1[i]++;
}
UART1_DMA_TXD();
Delay_ms(2000);
}
}
实验现象
SRAM - - >USART1