STM32-ADC+DMA

news/2024/7/16 5:59:38 标签: stm32, 单片机, 嵌入式硬件

本内容基于江协科技STM32视频学习之后整理而得。

文章目录

  • 1. ADC模拟-数字转换器
    • 1.1 ADC模拟-数字转换器
    • 1.2 逐次逼近型ADC
    • 1.3 ADC框图
    • 1.4 ADC基本结构
    • 1.5 输入通道
    • 1.6 规则组的转换模式
      • 1.6.1 单次转换,非扫描模式
      • 1.6.2 连续转换,非扫描模式
      • 1.6.3 单次转换,扫描模式
      • 1.6.4 连续转换,扫描模式
    • 1.7 触发控制
    • 1.8 数据对齐
    • 1.9 转换时间
    • 1.10 校准
    • 1.11 硬件电路
  • 2. AD库函数及代码
    • 2.1 AD库函数
    • 2.2 7-1AD单通道代码
      • 2.2.1 硬件电路
      • 2.2.2 代码流程
      • 2.2.3 代码
    • 2.3 7-2AD多通道代码
      • 2.3.1 硬件电路
      • 2.3.2 硬件运行结果
      • 2.3.3 代码流程
      • 2.3.4 代码
  • 3. DMA直接存储器存取
    • 3.1 DMA
    • 3.2 存储器映像
    • 3.3 DMA框图
    • 3.4 DMA基本结构
    • 3.5 DMA请求(触发)
    • 3.6 数据宽度与对齐
    • 3.7 数据转运+DMA
    • 3.8 ADC扫描模式+DMA
  • 4. DMA库函数及代码
    • 4.1 DMA库函数
    • 4.2 8-1DMA数据转运
      • 4.2.1 硬件电路
      • 4.2.2 代码流程
      • 4.2.3 代码
    • 4.3 8-2DMA+AD多通道
      • 4.3.1 硬件电路
      • 4.3.2 代码流程
      • 4.3.3 代码

1. ADC模拟-数字转换器

1.1 ADC模拟-数字转换器

  • ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁;(DAC数字模拟转换器,PWM是数字到模拟的转换,使用PWM来控制LED的亮度、电机的速度,这就是DAC的功能,同时PWM只有完全导通和完全断开两种状态,在这两种状态上都没有功率损耗,所以在直流电机调速这种大功率的应用场景中,使用PWM来等效模拟量,是比DAC更好的选择,并且PWM电路更加简单,更加常用,所以可以看出PWM还是挤占了DAC的很多应用空间,目前DAC的应用主要是在波形生成这些领域,比如信号发生器、音频解码芯片。)
  • 12位逐次逼近型ADC,1us转换时间。(12位表示分辨率,范围0-2^12-1=0~4095,位数越高,量化结果就越精细,对应分辨率就越高。转换时间即转换频率,转换需要时间,1us表示AD从转换开始到产生结果,需要花1us的时间,对应的AD转换频率就是1MHz)
  • 输入电压范围:0-3.3V,转换结果范围:0~4095
  • 18个输入通道,可测量16个外部和2个内部信号源
  • 规则组和注入组两个转换单元
  • 模拟看门狗自动监测输入电压范围(ADC一般可以用于测量光线强度、温度这些值,如果光线高于某个阈值,低于某个阈值,或者温度高于某个阈值,低于某个阈值时,执行一些操作,低于某个阈值、高于某个阈值的判断就可以用模拟看门狗来自动执行,模拟看门狗可以监测指定的某些通道,当AD值高于它设定的上阈值或低于下阈值时,它就会申请中断,就可以在中断函数里执行相应的操作,这样就不用不断地手动读值,再用if进行判断了。)
  • STM32103C8T6 ADC资源:ADC1、ADC2,10个外部输入通道(就是最多只能测量10个外部引脚的模拟信号)

1.2 逐次逼近型ADC

image.png

  • ADC0809:独立8位逐次逼近型ADC芯片
  • IN0~IN7:8路输入通道,通过通道选择开关,选中一路,输入到比较器进行转换。
  • 地址锁存和译码:想选中哪个通道,就把通道号放在ADDA~ADDC上,然后给一个锁存信号,对应的通路开关就可以自动拨好。通路选择开关相当于一个可以通过模拟信号的数据选择器。
  • 比较器:电压比较器,可以判断两个输入信号电压的大小关系,输出一个高低电平指示谁大谁小。其输入:一个是通道选择开关输出的待测电压,另一个是DAC的电压输出端。
  • DAC是数模转换器,给DAC一个数据,就能输出对应的电压值;内部是使用加权电阻网络实现的转换。
  • 现在有了一个外部通道输入的未知编码的电压,和一个DAC输出的已知编码的电压,它俩同时输入到电压比较器,进行大小判断。如果DAC输出的电压比较大,就调小DAC数据;反之,输出电压比较小,就增大DAC数据。直到DAC输出的电压和外部通道输入的电压近似相等。这样DAC输入的数据就是外部电压的编码数据了,这就是DAC的实现原理,该电压调节过程是逐次逼近寄存器SAR完成,为了最快找到未知电压的编码,采用二分法,0~255,每次对半分,128、64、32这些数据,正好是二进制每一位的位权。该判断过程,相当于对二进制从高位到低位依次判断是1还是0的过程。对于8位的ADC,从高位到低位依次判断8次就能找到未知电压的编码。AD转换结束后,DAC的输入数据,就是未知电压的编码,通过8位三态锁存缓冲器输出。
  • EOC是End of Convert,转换结束信号;
  • START是开始转换,给一个输入脉冲,开始转换;
  • CLOCK是ADC时钟,因ADC内部是一步一步进行判断的,因此需要时钟来推动这个过程。
  • VREF+和VREF-是DAC的参考电压,该参考电压也决定了ADC的输入范围,所以也是ADC参考电压。

1.3 ADC框图

image.png

  • 对于普通的ADC,多路开关一般都是只选中一个的,就是选中某一个通道、开始转换、等待转换完成、取出结果。
  • 但是在这里可以选中多个,而且在转换的时候,还分成了两个组,规则通道组和注入通道组,其中规则组可以一次性最多选中16个通道,注入组最多可以选中4个通道,但是规则组只有一个数据寄存器,而注入组有4个数据寄存器,用规则组需要使用DMA配合转运数据。
  • 规则组和注入组的触发源主要来自定时器,有定时器的各个通道,还有TRGO定时器主模式的输出,可以选择TIM3定一个1ms的时间,并且把TIM3的更新事件选择为TRGO输出,然后在ADC里,选择开始触发信号为TIM3的TRGO,这样TIM3的更新事件就能通过硬件自动触发ADC转换了。也可以选择外部中断引脚来触发转换。
  • VREF+和VREF-是ADC的参考电压,VDDA和VSSA是ADC的供电引脚,一般VREF+要接VDDA,VREF-要接VSSA,
  • ADCCLK是ADC的时钟,用于驱动内部逐次比较的时钟,是来自ADC预分频器,这个ADC预分频器是来源于RCC的。
    image.png

1.4 ADC基本结构

image.png

  • 规则组最多可以选中16个通道;注入组最多可以选择4个通道;
  • 转换结果存放在AD数据寄存器里,规则组只有1个数据寄存器,注入组有4个;
  • 触发控制,提供了开始转换START信号;可以选择软件触发和硬件触发。
  • 硬件触发主要来自于定时器,也可以选择外部中断的引脚。
    来自于RCC的ADC时钟CLOCK,ADC逐次比较的过程就是由这个时钟推动的;
  • 可以布置一个模拟看门狗用于检测转换结果的范围,若超出设定的阈值,就通过中断输出控制,向NVIC申请中断;
  • 规则组和注入组转换完成后会有个EOC信号,会置一个标志位,也可以通向NVIC。
  • 开关控制:在库函数中,就是ADC_Cmd函数,用于给ADC上电的。

1.5 输入通道

image.png

1.6 规则组的转换模式

在ADC初始化的结构体里,会有两个参数:参1是选择单次转换还是连续转换,参2是选择扫描模式还是非扫描模式。

1.6.1 单次转换,非扫描模式

image.png

  • 在非扫描模式下,该菜单只有第一个序列1的位置有效。菜单同时选中一组的方式就退化为简单地选中一个的方式。
  • 在序列1可以指定要转换的通道,之后就可以触发转换,ADC就会对这个通道2进行模数转换。
  • 转换完成后,结果存放在数据寄存器里,同时给EOC标志位置1,转换结束。
  • 判断转换结束后,就可以在数据寄存器里读取结果。若想再启动一次转换,就需要再触发一次,转换结束,置EOC标志位,读结果。
  • 若想换一个通道转换,则在转换之前,把第一个位置的通道2改为其他通道,然后再启动转换。
  • 流程:触发转换–>判断转换结束(置EOC标志位)–>获取转换值

1.6.2 连续转换,非扫描模式

image.png
它在一次转换结束后,不会停止,而是立刻开始下一轮的转换,然后一直持续下去。因此只需最开始触发一次,之后就可以一直转换。优点是开始转换之后不需要等待一段时间,想要读AD值的时候,直接从数据寄存器取就是了。

1.6.3 单次转换,扫描模式

image.png
每触发一次,转换结束后,就会停下来,下次转换就得再触发才能开始。初始化结构体中还会有个参数:通道数目,若为7,就是在每次触发之后,依次对前7个位置进行AD转换,转换结果都放在数据寄存器里。为了防止数据被覆盖,就需要用DMA及时将数据挪走。7个通道转换完成之后,产生EOC信号,转换结束。然后再触发下一次,就又开始新一轮的转换。

1.6.4 连续转换,扫描模式

image.png
一次转换完成后,立刻开始下一次的转换。
在扫描模式的情况下,还有一种模式:间断模式,在扫描的过程中,每隔几个转换,就暂停一次,需要再次触发,才能继续。

1.7 触发控制

image.png
类型:外部引脚/来自片上定时器的内部信号,具体是引脚还是定时器,需要用AFIO重映射来确定。
软件控制位:软件触发
触发信号的选择可以通过设置右边的寄存器来完成。也可以使用库函数实现。

1.8 数据对齐

ADC是12位的,其转换结果就是一个12位的数据。但数据寄存器是16位的,所以存在数据对齐的问题
数据右对齐
数据左对齐

1.9 转换时间

image.png

  • 量化编码即逐次比较
  • 采样保持:是因为量化编码是需要一小段时间的,如果在这一小段时间里,输入的电压不断变化,则无法定位输入电压的位置,所以在量化编码之前,需要设置一个采样开关,先打开采样开关,收集一下外部的电压,之后断开采样开关,再进行AD转换,这样在量化编码的期间,电压始终保持不变,这样才能精确地定位未知电压的位置。
  • 采样时间就是采样保持的时间,采样时间越大,越能避免一些毛刺信号的干扰。但转换时间也会相应延长。
  • 12.5个ADC周期是量化编码花费的时间,因为是12位的ADC,所以需要花费12个周期,0.5个周期是做其他用的。ADC周期就是从RCC分频过来的ADCCLK,ADCCLK最大是14MHz。

1.10 校准

  • ADC有一个内置自校准模式。校准可大幅减小因内部电容器组的变化而造成的准精度误差。校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差
  • 建议在每次上电后执行一次校准
  • 启动校准前, ADC必须处于关电状态超过至少两个ADC时钟周期

1.11 硬件电路

image.png

  • 第一个:电位器可调电压的电路,就是接一个电位器,当滑动端往上滑时,电压增大,往下滑时,电压减小。
  • 第二个:传感器输出电压的电路,如光敏电阻、热敏电阻、红外接收管、麦克风等都可以等效为一个可变电阻。传感器阻值变小时,下拉作用变强,输出端电压就下降;传感器阻值变大时,下拉作用变弱,输出端受上拉电阻的作用,电压就会升高。
  • 第三个:电压转换电路,若想测一个0-5V的VIN电压,但ADC只能接收0-3.3V的电压,就可以搭一个转换电路,上面阻值17K,下面阻值33K,总共50K,根据分压公式,中间的电压就是VIN/50K*33K,得到的电压范围就是0~3.3V,就可以进入ADC转换了。

2. AD库函数及代码

2.1 AD库函数

// 配置ADCCLK分频器
void RCC_ADCCLKConfig(uint32_t RCC_PCLK2);

// DeInit恢复缺省配置、Init初始化、StructInit结构体初始化
void ADC_DeInit(ADC_TypeDef* ADCx);
void ADC_Init(ADC_TypeDef* ADCx, ADC_InitTypeDef* ADC_InitStruct);
void ADC_StructInit(ADC_InitTypeDef* ADC_InitStruct);

// 给ADC上电的,即开关控制
void ADC_Cmd(ADC_TypeDef* ADCx, FunctionalState NewState);

// 开启DMA输出信号
void ADC_DMACmd(ADC_TypeDef* ADCx, FunctionalState NewState);

// 中断输出控制,用于控制某个中断,能不能通往NVIC
void ADC_ITConfig(ADC_TypeDef* ADCx, uint16_t ADC_IT, FunctionalState NewState);

// 复位校准、获取复位校准状态
void ADC_ResetCalibration(ADC_TypeDef* ADCx);
FlagStatus ADC_GetResetCalibrationStatus(ADC_TypeDef* ADCx);

// 开始校准、获取开始校准状态
void ADC_StartCalibration(ADC_TypeDef* ADCx);
FlagStatus ADC_GetCalibrationStatus(ADC_TypeDef* ADCx);

// ADC软件开始转换控制,用于软件触发的函数
void ADC_SoftwareStartConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);

// ADC获取软件开始转换状态
FlagStatus ADC_GetSoftwareStartConvStatus(ADC_TypeDef* ADCx);

// 判断转换是否结束。获取标志位状态,参数给EOC的标志位,判断EOC标志位是不是置1了
FlagStatus ADC_GetFlagStatus(ADC_TypeDef* ADCx, uint8_t ADC_FLAG);

// 配置间断模式,函数1:每隔几个通道间断一次;函数2:是不是启用间断模式
void ADC_DiscModeChannelCountConfig(ADC_TypeDef* ADCx, uint8_t Number);
void ADC_DiscModeCmd(ADC_TypeDef* ADCx, FunctionalState NewState);

// ADC规则组通道配置,给序列的每个位置填写指定的通道
void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime);

// ADC 外部触发转换控制,就是是否允许外部触发转换
void ADC_ExternalTrigConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);

// ADC换取转换值。就是获取AD转换的数据寄存器,读取转换结果使用该函数
uint16_t ADC_GetConversionValue(ADC_TypeDef* ADCx);

// ADC获取双模式转换值,是ADC模式读取转换结果的函数
uint32_t ADC_GetDualModeConversionValue(void);

// 对ADC注入组进行配置
void ADC_AutoInjectedConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
void ADC_InjectedDiscModeCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
void ADC_ExternalTrigInjectedConvConfig(ADC_TypeDef* ADCx, uint32_t ADC_ExternalTrigInjecConv);
void ADC_ExternalTrigInjectedConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
void ADC_SoftwareStartInjectedConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
FlagStatus ADC_GetSoftwareStartInjectedConvCmdStatus(ADC_TypeDef* ADCx);
void ADC_InjectedChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime);
void ADC_InjectedSequencerLengthConfig(ADC_TypeDef* ADCx, uint8_t Length);
void ADC_SetInjectedOffset(ADC_TypeDef* ADCx, uint8_t ADC_InjectedChannel, uint16_t Offset);
uint16_t ADC_GetInjectedConversionValue(ADC_TypeDef* ADCx, uint8_t ADC_InjectedChannel);

// 对模拟看门狗进行配置,函数1:是否启动看门狗;函数2:配置高低阈值;函数3:配置看门的通道
void ADC_AnalogWatchdogCmd(ADC_TypeDef* ADCx, uint32_t ADC_AnalogWatchdog);
void ADC_AnalogWatchdogThresholdsConfig(ADC_TypeDef* ADCx, uint16_t HighThreshold, uint16_t LowThreshold);
void ADC_AnalogWatchdogSingleChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel);

// ADC温度传感器、内部参考电压控制,用于开启内部的两个通道
void ADC_TempSensorVrefintCmd(FunctionalState NewState);

// 获取中断状态、清除中断挂起位
FlagStatus ADC_GetFlagStatus(ADC_TypeDef* ADCx, uint8_t ADC_FLAG);
void ADC_ClearFlag(ADC_TypeDef* ADCx, uint8_t ADC_FLAG);
ITStatus ADC_GetITStatus(ADC_TypeDef* ADCx, uint16_t ADC_IT);
void ADC_ClearITPendingBit(ADC_TypeDef* ADCx, uint16_t ADC_IT);

2.2 7-1AD单通道代码

2.2.1 硬件电路

实现功能:接一个电位器,即滑动变阻器,用该电位器产生一个0~3.3V连续变化的模拟电压信号,接到STM32的PA0口上,之后用STM32内部的ADC读取电压数据,显示在屏幕上。屏幕上第一行显示的是AD转换后的原始数据,第二行是经过处理后实际的电压值。往左拧,AD值减小,电压值也减小,AD值最小为0,对应的电压就是0V,往右拧,AD值变大,对应电压值也变大。STM32的ADC是12位的,所以AD结果最大值是4095(2^12 - 1),对应的电压是3.3V。
image.png

2.2.2 代码流程

  1. 开启RCC时钟,包括ADC和GPIO时钟,ADCCLK的分频器
  2. 配置GPIO,配置为模拟输入模式
  3. 配置多路开关,把左边的通道接入到右边的规则组列表里
  4. 配置ADC转换器,采用库函数结构体:包括AD转换器和AD数据寄存器
  5. 开关控制,调用ADC_Cmd函数,开启ADC
  6. 想要软件触发转换,有函数可以触发

2.2.3 代码

AD.c代码:

#include "stm32f10x.h"                  // Device header

void AD_Init(void)
{
	/*
	1. 开启RCC时钟,包括ADC和GPIO时钟,ADCCLK的分频器
	2. 配置GPIO,配置为模拟输入模式
	3. 配置多路开关,把左边的通道接入到右边的规则组列表里
	4. 配置ADC转换器,采用库函数结构体:包括AD转换器和AD数据寄存器
	5. 开关控制,调用ADC_Cmd函数,开启ADC
	6. 想要软件触发转换,有函数可以触发
	
	*/
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 6分频=12MHz
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;// 模拟输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	// 采样时间是55.5个ADCCLK的周期
	// 在规则组序列1的位置写入通道0,
	ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
	
	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 = DISABLE;   // 非扫描模式
	ADC_InitStructure.ADC_NbrOfChannel = 1;  // 1个通道
	ADC_Init(ADC1,&ADC_InitStructure);
	
	ADC_Cmd(ADC1, ENABLE);
	
	// 复位校准
	ADC_ResetCalibration(ADC1);
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	
    // 开始校准、获取开始校准状态
    ADC_StartCalibration(ADC1);
    while (ADC_GetCalibrationStatus(ADC1) == SET);
	
}

uint16_t AD_GetValue(void)
{
	// 软件触发
	ADC_SoftwareStartConvCmd(ADC1,ENABLE);
	// 判断转换结束
	while (ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC) == RESET); // 55.5 + 12.5 = 68个周期,ADCCLK = 12MHz,1/12M*68 = 5.6us,等待5.6us
	// ADC获取转换值
	return ADC_GetConversionValue(ADC1);
}

main.c代码:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"

uint16_t ADValue;
float Voltage;

int main(void)
{
	OLED_Init();
	AD_Init();
	
	OLED_ShowString(1, 1, "ADValue:");
	OLED_ShowString(2, 1, "Voltage:0.00V");
	
	while(1)
	{
		ADValue = AD_GetValue();
		Voltage = (float)ADValue / 4095 * 3.3;
		OLED_ShowNum(1, 9, ADValue, 4);
		OLED_ShowNum(2, 9, Voltage, 1);// 显示整数部分
		OLED_ShowNum(2, 11, (uint16_t)(Voltage * 100) % 100, 2);
		
		Delay_ms(100);
	}
}

2.3 7-2AD多通道代码

2.3.1 硬件电路

接了三个传感器:光敏电阻、热敏电阻、反射式红外传感器,把它们的AO(模拟电压输出端)分别接在了A1、A2、A3引脚,加上原来的电位器,总共四个输出通道,然后测出来的4个AD数据分别显示在屏幕上,AD0:电位器,往左拧,减小,往右拧增大;AD1:光敏电阻,遮挡时电阻变大,下拉作用变弱,输出电压变大,AD值增大;AD2:热敏电阻,用手热一下,温度升高,阻值变小,输出电压变小,AD值减小;AD3:反射式红外传感器,手靠近,有反光,AD值减小。
image.png

2.3.2 硬件运行结果

IMG_20240404_155009.jpg

2.3.3 代码流程

  1. 开启RCC时钟,包括ADC和GPIO时钟,ADCCLK的分频器
  2. 配置GPIO,配置为模拟输入模式
  3. 配置多路开关,把左边的通道接入到右边的规则组列表里
  4. 配置ADC转换器,采用库函数结构体:包括AD转换器和AD数据寄存器
  5. 开关控制,调用ADC_Cmd函数,开启ADC
  6. 想要软件触发转换,有函数可以触发

2.3.4 代码

  1. AD.c代码:
#include "stm32f10x.h"                  // Device header

void AD_Init(void)
{
	/*
	1. 开启RCC时钟,包括ADC和GPIO时钟,ADCCLK的分频器
	2. 配置GPIO,配置为模拟输入模式
	3. 配置多路开关,把左边的通道接入到右边的规则组列表里
	4. 配置ADC转换器,采用库函数结构体:包括AD转换器和AD数据寄存器
	5. 开关控制,调用ADC_Cmd函数,开启ADC
	6. 想要软件触发转换,有函数可以触发
	
	*/
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 6分频
	
	GPIO_InitTypeDef GPIO_InitStructure;
	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);
	
	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 = DISABLE;   // 非扫描模式
	ADC_InitStructure.ADC_NbrOfChannel = 1;  // 1个通道
	ADC_Init(ADC1,&ADC_InitStructure);
	
	ADC_Cmd(ADC1, ENABLE);
	
	// 复位校准
	ADC_ResetCalibration(ADC1);
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	
    // 开始校准、获取开始校准状态
    ADC_StartCalibration(ADC1);
    while (ADC_GetCalibrationStatus(ADC1) == SET);
	
}

uint16_t AD_GetValue(uint8_t ADC_Channel)
{
	/*
	调用该函数时,只要先指定通道,
	返回值就是我们指定通道的结果.
	通道为 0、1、2、3
	*/
	
	// 采样时间是55.5个ADCCLK的周期
	// 通道为参数指定的通道
	ADC_RegularChannelConfig(ADC1,ADC_Channel,1,ADC_SampleTime_55Cycles5);
	// 软件触发
	ADC_SoftwareStartConvCmd(ADC1,ENABLE);
	// 判断转换结束
	while (ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC) == RESET); // 等待5.6us
	// ADC换取转换值
	return ADC_GetConversionValue(ADC1);
}

  1. main.c
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"

uint16_t AD0, AD1, AD2, AD3;
float Voltage;

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)
	{
		AD0 = AD_GetValue(ADC_Channel_0);
		AD1 = AD_GetValue(ADC_Channel_1);
		AD2 = AD_GetValue(ADC_Channel_2);
		AD3 = AD_GetValue(ADC_Channel_3);
		
		
		OLED_ShowNum(1, 5, AD0, 4);
		OLED_ShowNum(2, 5, AD1, 4);
		OLED_ShowNum(3, 5, AD2, 4);
		OLED_ShowNum(4, 5, AD3, 4);
		
		Delay_ms(100);
	}
}

3. DMA直接存储器存取

3.1 DMA

  • DMA可以提取外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源。(外设指外设的寄存器,一般是外设的数据寄存器DR,如ADC的数据寄存器、串口的数据寄存器;存储器是指运行内存SRAM和程序存储器Flash,是存储变量数组和程序代码的地方)
  • 12个独立可配置的通道:DMA1(7个通道),DMA2(5个通道)
  • 每个通道都支持软件触发和特定的硬件触发(如果DMA进行的是存储器到存储器的数据转运,比如想把Flash里的一批数据转运到SRAM里去,就需要软件触发。使用软件触发之后,DMA就会把这批数据以最快的速度全部转运完成。如果DMA进行的是外设到存储器的数据转运,由于外设的数据转运需要一定的时机,就需要硬件触发,比如转运ADC的数据,那就得ADC每个通道AD转换完成后,硬件触发一次DMA,之后DMA再转运,触发一次,转运一次。所以存储器到存储器的转运一般用软件触发,外设到存储器的转运一般用硬件触发。采用特定的硬件触发意思是每个DMA的通道的硬件触发源是不一样的,要使用某个外设的硬件触发源,就得使用它连接的那个通道,而不能任意选择通道。)
  • STM32F103C8T6 DMA资源:DMA1(7个通道)

3.2 存储器映像

类型起始地址存储器用途
ROM0x0800 0000程序存储器Flash存储C语言编译后的程序代码和常量·
0x1FFF F000系统存储器存储BootLoader,用于串口下载
0x1FFF F800选项字节存储一些独立于程序代码的配置参数
RAM0x2000 0000运行内存SRAM存储运行过程中的临时变量
0x4000 0000外设寄存器存储各个外设的配置参数
0xE000 0000内核外设寄存器存储内核各个外设的配置参数
  • 计算器的5大组成部分:运算器、控制器、存储器、输入设备和输出设备;
  • 运算器和控制器合在一起,称为CPU;
    因此,计算机的核心关键部分就是CPU和存储器
  • 存储器又包括:存储器的内容、存储器的地址
  • ROM是只读存储器,是一种非易失性、掉电不丢失的存储器。
  • RAM是随机存储器,是一种易失性、掉电丢失的存储器。
  • 程序存储器Flash:存储C语言编译后的程序代码,即下载程序的位置。
  • 运行程序,一般也是从主闪存里面开始运行的。
  • 系统存储器和选项字节:实际存储介质也是Flash。
  • 选项字节:存的主要是Flash的读保护、写保护。
  • 运行内存SRAM:存储运行过程中的临时变量,就是在程序中定义变量、数组、结构体的地方。

3.3 DMA框图

主要包括:CPU和存储器
image.png

  • Flash:主闪存,是ROM只读存储器的一种。如果通过总线直接访问的话,无论是CPU还是DMA都是只读的,只能读取数据,而不能写入。如果DMA的目的地址,填了Flash的区域,转运时就会出错。可以配置Flash接口控制器对Flash进行写入。
  • SRAM是运行内存,可以任意读写。
  • 数据寄存器可以正常读写。
  • 各种外设,都可以看成是寄存器,也是一种SRAM存储器。
  • 寄存器:一种特殊的存储器。一方面,CPU可以对寄存器进行读写,就像读写运行内存一样。另一方面,寄存器的每一位背后,都连接了一根导线。这些导线可以用于控制外设电路的状态,如置引脚的高低电平、导通和断开开关、切换数据选择器,或者多位组合起来,当做计数器、数据寄存器。因此,寄存器是连接软件和硬件的桥梁。软件读写寄存器,就相当于在控制硬件的执行。
    因此,外设就是寄存器,寄存器就是存储器。因此,DMA转运就可以归结为一类问题,就是从某个地址取内容,再放到另一个地址去。
  • 总线矩阵的左端是主动单元,也就是拥有存储器的访问权;右端是被动单元,它们的存储器只能被左边的主动单元读写。主动单元里,内核有DCode和系统总线,可以访问右边的存储器。DCode总线是专门访问Flash的,系统总线是访问其他东西的。另外,由于DMA要转运数据,所以DMA也必须要有访问的主动权。
  • DMA1有一条DMA总线,DMA2有一条DMA总线;DMA1有7个通道,DMA2有5个通道。各个通道可以分别设置它们转运数据的源地址和目的地址,可以各自独立地工作。虽然多个通道可以独立转运数据,但DMA总线只有一条,所以所有的通道都只能分时复用一条DMA总线,如果产生了冲突,那就会由仲裁器,根据通道的优先级,来决定谁先用,谁后用。在总线矩阵这边也会有个仲裁器,如果DMA和CPU都要访问同一个目标,则DMA就会暂停CPU的访问,以防止冲突。但总线仲裁器,仍会保证CPU得到一半的总线带宽,使CPU也能正常的工作。
  • AHB从设备:即DMA自身的寄存器。DMA作为一个外设,其也会有相应的配置寄存器,连接在了总线右边的AHB总线上。因此,DMA即是总线矩阵的主动单元,可以读写各种存储器,也是AHB总线上的被动单元。
  • DMA请求:即是触发的意思。其右边的触发源是各个外设。所以DMA请求就是DMA的硬件触发源,如ADC转换完成、串口接收到数据。需要触发DMA转运数据的时候,通过DMA请求线路向DMA发出硬件触发信号,之后DMA就可以执行数据转运的工作。
  • DMA的工作:用于访问各个存储器的DMA总线;内部的多个通道可以进行独立的数据转运;仲裁器用于调度各个通道,防止产生冲突;AHB从设备用于配置DMA参数;DMA请求用于硬件触发DMA的数据转运。

3.4 DMA基本结构

image.png

  • 左边的外设寄存器与右边的寄存器(Flash+SRAM)是数据转运的两大站点。
  • DMA的数据转运:向右的外设到存储器;向左的存储器到外设,可以通过方向参数控制。另一种转运方式:存储器到存储器,如Flash到SRAM或SRAM到SRAM。
  • 转运方法:外设和存储器都包括三个参数,起始地址决定了数据从哪里来到哪里去;数据宽度指定一次转运要按多大的数据宽度来进行,可以选择字节Byte、半字HalfWord和字Word。字节Byte是8位,一次转运uint8_t;半字HalfWord是16位,一次转运uint16_t;字Word是32位,一次转运uint32_t。地址是否自增:指定一次转运完成后,下一次转运,是不是要把地址移动到下一个位置去。
    若要进行存储器到存储器的转运,那就需要把其中一个存储器的地址,放在外设的这个站点。
  • 传输计数器:用来指定总共需要转运几次的。是自减计数器。
  • 自动重装器:当传输计数器减到0之后,是否要自动恢复到最初的值。比如最初传输计数器给5,如果不使用自动重装器,那转运5次后,DMA就结束了。如果使用自动重装器,那转运5次后,计数器减到0后,就会立即重装到初始值5。
  • 触发:就是决定DMA需要在什么时机进行转运。触发源有硬件触发和软件触发,具体选择由M2M(存储器到存储器)参数决定,M2M为1,选择软件触发,以最快的速度,连续不断地触发DMA,争取早日把传输计数器清零,完成这一轮的转换。软件触发和自动重装器不能同时使用。软件触发是想把传输计数器清零,循环模式是清零后自动重装。软件触发适用于存储器到存储器的转运,因为存储器到存储器的转运是软件启动、不需要时机,并且想尽快完成的任务。
    M2M为0,就是硬件触发,硬件触发源可以选择ADC、串口、定时器,使用硬件触发的转运,一般都是与外设有关的转运。转运需要一定的时机,如ADC转换完成、串口收到数据、定时时间到等,在硬件达到这些时机时,传一个信号过来,来触发DMA进行转运。
  • 开关控制(EN位):DMA_Cmd函数。当给DMA使能后,DMA就准备就绪,可以进行转运了。,
  • DMA转运条件:(1)开关控制,DMA_Cmd必须使能;(2)传输计数器必须大于0;(3)触发源,必须有触发信号。触发一次,转运一次,传输计数器自减一次。当传输计数器等于0,且没有自动重装时,这时无论是否触发,DMA都不会再进行转运了。此时就需要DMA_Cmd,给DISABLE,关闭DMA。再为传输计数器写入一个大于0的数,再DMA_Cmd,给ENABLE,开启DMA,继续工作。
  • 注意:写传输计数器时,必须先关闭DMA,再进行。

3.5 DMA请求(触发)

image.png
默认优先级是通道号越小,优先级越高。也可以在程序中配置优先级。

3.6 数据宽度与对齐

image.png

  • 如果数据宽度都一样,就是正常的一个一个转运。当源端和目标都是8位时,转运第一步,在源端的0位置,读数据B0,在目标的0位置,写数据B0,就是把这个B0,从左边挪到右边。之后的步骤就是把B1从左边挪到右边,之后B2、B3。
  • 当源端是8位,目标是16位,其操作是,在源端读B0,在目标写00B0,之后读B1,写00B1。就是如果目标宽度比源端数据宽度大,那就在目标数据前面多出来的空位补0。
  • 当目标数据宽度,比源端数据宽度小时,像从16位转运到8位时,就是读B1B0,只写入B0,读B3B2,只写入B2,也就是把多出来的高位舍弃掉。
  • 总结:如果把小的数据转到大的里面去,高位就会补0;如果把大的数据转到小的里面去,高位就会舍弃掉;如果数据宽度一样,那就没事。

3.7 数据转运+DMA

image.png

  • 外设地址:DataA数组的首地址;存储器地址:DataB数组的首地址
  • 起始地址:DataA[0]和DataB[0]
  • 数据宽度:8位
  • 地址是否自增:是,两边地址都要自增。
  • 方向参数:外设站点 ->存储器站点
  • 传输计数器:7,不需要自动重装
  • 触发:软件触发

3.8 ADC扫描模式+DMA

image.png

  • 触发一次后,7个通道依次进行AD转换,转换结果都放到ADC_DR数据寄存器里面。需要做的是:在每次转换完成后进行一个DMA数据转运,并且目的地址进行自增。
  • DMA配置:(1)外设地址写入ADC_DR这个寄存器的地址,存储器的地址可以在SRAM中定义一个数组ADValue,然后把ADValue的地址当作存储器的地址;(2)数据宽度:因为ADC_DR和SRAM数组都是uint16_t,因此数据宽度都是16位的半字传输;(3)地址是否自增:外设地址不自增,存储器地址自增;
  • 传输方向:外设站点 -> 存储器站点
  • 传输计数器:7个。计数器是否自动重装,可以看ADC的配置。若ADC是单次扫描,那DMA的传输计数器可以不自动重装,转换一轮就停止。若ADC是连续扫描,那DMA就可以使用自动重装,在ADC启动下一轮转换的时候,DMA也启动下一轮的转运,ADC和DMA同步工作。
  • 触发选择:ADC_DR的值是在ADC单个通道转换完成后才会生效,所以DMA转运的时机,需要和ADC单个通道转换完成同步,所以DMA的触发要选择ADC的硬件触发。
  • ADC扫描模式,在每个单独的通道转换完成后,没有任何标志位,也不会触发中断,所以程序不好判断,某个通道转换完成的时机是什么时候,但应该会产生DMA请求,去触发DMA转运。

4. DMA库函数及代码

4.1 DMA库函数

// ADC1_BASE是ADC1的基地址,基地址就是起始地址,即4001 2400
#define ADC1                ((ADC_TypeDef *) ADC1_BASE)
#define ADC1_BASE             (APB2PERIPH_BASE + 0x2400)
#define APB2PERIPH_BASE       (PERIPH_BASE + 0x10000)
#define PERIPH_BASE           ((uint32_t)0x40000000) /*!< Peripheral base address in the alias region */

typedef struct
{
  __IO uint32_t SR;
  __IO uint32_t CR1;
  __IO uint32_t CR2;
  __IO uint32_t SMPR1;
  __IO uint32_t SMPR2;
  __IO uint32_t JOFR1;
  __IO uint32_t JOFR2;
  __IO uint32_t JOFR3;
  __IO uint32_t JOFR4;
  __IO uint32_t HTR;
  __IO uint32_t LTR;
  __IO uint32_t SQR1;
  __IO uint32_t SQR2;
  __IO uint32_t SQR3;
  __IO uint32_t JSQR;
  __IO uint32_t JDR1;
  __IO uint32_t JDR2;
  __IO uint32_t JDR3;
  __IO uint32_t JDR4;
  __IO uint32_t DR;
} ADC_TypeDef;

ADC1->DR 
// ADC1是结构体指针,指向的是ADC1外设的起始地址,
// 访问结构体成员,相当于是加一个地址偏移。
// 起始地址 + 偏移,就是指定的寄存器
// 恢复缺省配置
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);
// DMA设置当前数据寄存器,给传输计数器写数据的
void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber); 
// DMA获取当前数据寄存器,返回传输计数器的值
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);

4.2 8-1DMA数据转运

4.2.1 硬件电路

使用DMA进行存储器到存储器的数据转运,把一个数组(源数组)的数据转运到另一个数组(目的数组)中。采用软件触发。
在OLED中显示源数组DataA 、地址及数据,目的数组DataB、地址及数据。
image.png

4.2.2 代码流程

  1. RCC开启DMA的时钟(AHB时钟)
  2. 调用DMA_Init,初始化参数:外设和存储器站点的起始地址、数据宽度、地址是否自增、方向、传输计数器、是否需要自动重装、选择触发源
  3. 开关控制,DMA_Cmd
  4. 如果是硬件触发,在对应的外设调用一下xxx_DMACmd,开启一下触发信号的输出
  5. 如果需要DMA的中断,调用DMA_ITConfig,开启中断输出,再在NVIC里,配置相应的中断通道,写中断函数
  6. 如果转运完成,传输计数器清0了,若再想给传输计数器赋值的话,就DMA失能、写传输计数器、DMA使能。

4.2.3 代码

  1. MyDMA.c代码:
#include "stm32f10x.h"                  // Device header

uint16_t MyDMA_Size;

void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint32_t Size)
{
/*
1. RCC开启DMA的时钟
2. 调用DMA_Init,初始化参数:外设和存储器站点的
   起始地址、数据宽度、地址是否自增、方向、
   传输计数器、是否需要自动重装、选择触发源
3. 开关控制,DMA_Cmd
4. 如果是硬件触发,在对应的外设调用一下xxx_DMACmd,
   开启一下触发信号的输出
5. 如果需要DMA的中断,调用DMA_ITConfig,开启中断输出,
   再在NVIC里,配置相应的中断通道,写中断函数
6. 如果转运完成,传输计数器清0了,若再想给传输计数器赋值的话,
   就DMA失能、写传输计数器、DMA使能。
*/
	// DMA是AHB总线的设备,需要开启AHB时钟
	
	MyDMA_Size = Size;
	
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
	
	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);
	
	/*
	DMA转运有三个条件:
	1. 传输计数器大于0
	2. 触发源有触发信号
	3. DMA使能
	*/
	
	DMA_Cmd(DMA1_Channel1, DISABLE);
}

// DMA传输函数,调用依次这个函数,就再次启动一次DMA转运
void MyDMA_Transfer(void)
{
	// 重新给传输计数器赋值,先使DMA失能
	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);
}


  1. main.c
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyDMA.h"


/*
uint8_t bb = 0x66; // 存储在SRAM中
const uint8_t aa = 0x66; 
存储在Flash中,const定义的变量是常量,值不能更改
Flash是只读不能写入,因此const与Flash对应
*/

uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};// 源端数组
uint8_t DataB[] = {0, 0, 0, 0};// 目标数组



int main(void)
{
	OLED_Init();
	
	// 把DataA数组的数据转运到DataB里
	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);
	
	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);
	

	
	while(1)
	{
		DataA[0] ++;
		DataA[1] ++;
		DataA[2] ++;
		DataA[3] ++;
		
		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);
		
		Delay_ms(1000);
		
		MyDMA_Transfer();
		
		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);
		
		Delay_ms(1000);
	}
}

4.3 8-2DMA+AD多通道

4.3.1 硬件电路

使用ADC的扫描模式来实现多通道采集,然后使用DMA来进行数据转运,AD转换的数据就会自动到定义的数组里,然后用OLED显示一下。
image.png

4.3.2 代码流程

  1. 开启RCC时钟,包括ADC和GPIO时钟,ADCCLK的分频器
  2. 配置GPIO,配置为模拟输入模式
  3. 配置多路开关,把左边的通道接入到右边的规则组列表里
  4. 配置ADC转换器,采用库函数结构体:包括AD转换器和AD数据寄存器
  5. 开关控制,调用ADC_Cmd函数,开启ADC
  6. 想要软件触发转换,有函数可以触发

4.3.3 代码

  1. AD.c代码:
#include "stm32f10x.h"                  // Device header

uint16_t AD_Value[4];

void AD_Init(void)
{
	/*
	1. 开启RCC时钟,包括ADC和GPIO时钟,ADCCLK的分频器
	2. 配置GPIO,配置为模拟输入模式
	3. 配置多路开关,把左边的通道接入到右边的规则组列表里
	4. 配置ADC转换器,采用库函数结构体:包括AD转换器和AD数据寄存器
	5. 开关控制,调用ADC_Cmd函数,开启ADC
	6. 想要软件触发转换,有函数可以触发
	
	*/
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 6分频
	
	GPIO_InitTypeDef GPIO_InitStructure;
	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);
	
	// 4个通道
	ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1,ADC_Channel_2,3,ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1,ADC_Channel_3,4,ADC_SampleTime_55Cycles5);
	
	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个通道
	ADC_Init(ADC1,&ADC_InitStructure);
	
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // 外设站点的起始地址 ADC
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 外设站点的数据宽度
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable ; // 外设站点的是否自增,否
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value; // 存储器站点的起始地址 SRAM
	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; // 传输模式,就是是否使用自动重装
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 选择是否是存储器到存储器,即选择硬件触发还是软件触发
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; // 优先级
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);
	
	DMA_Cmd(DMA1_Channel1, ENABLE);
	ADC_DMACmd(ADC1, ENABLE);// 开启DMA触发信号
	ADC_Cmd(ADC1, ENABLE);
	
	// 复位校准
	ADC_ResetCalibration(ADC1);
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
    // 开始校准、获取开始校准状态
    ADC_StartCalibration(ADC1);
    while (ADC_GetCalibrationStatus(ADC1) == SET);
	
	ADC_SoftwareStartConvCmd(ADC1,ENABLE);
}

/*
void AD_GetValue(void)
{
	
	调用该函数,ADC开始转换,DMA也同步进行转运,
	AD转换结果,依次放在这上面的AD_Value数组里,
	
	
	DMA_Cmd(DMA1_Channel1, DISABLE);
	DMA_SetCurrDataCounter(DMA1_Channel1,4);
	DMA_Cmd(DMA1_Channel1, ENABLE);
	
	
	
	while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
	DMA_ClearFlag(DMA1_FLAG_TC1);
}
*/

  1. 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)
	{
		
		// AD_GetValue();
		
		OLED_ShowNum(1, 5, AD_Value[0], 4);
		OLED_ShowNum(2, 5, AD_Value[1], 4);
		OLED_ShowNum(3, 5, AD_Value[2], 4);
		OLED_ShowNum(4, 5, AD_Value[3], 4);
		
		Delay_ms(100);
	}
}


http://www.niftyadmin.cn/n/5542932.html

相关文章

力扣304.二维区域和检索

力扣304.二维区域和检索 二维前缀和的简单应用 class NumMatrix {vector<vector<int>> sum;public:NumMatrix(vector<vector<int>>& matrix) {int m matrix.size(),n matrix[0].size();sum.resize(m1,vector<int>(n1)); for(int i0;i&…

复合机器人:手脚眼脑的完美结合

在现代工业制造的舞台上&#xff0c;复合机器人如同一位精密而高效的工匠&#xff0c;以其独特的手脚眼脑&#xff0c;正深刻改变着传统的生产方式。这些机器人不仅仅是机械臂的简单延伸&#xff0c;它们汇聚了先进的机械结构、智能的感知系统、精密的控制技术和灵活的思维能力…

MongoDB:掌握核心常用命令语句,精通数据操作

标题&#xff1a;MongoDB&#xff1a;掌握核心命令&#xff0c;精通数据操作 前言&#xff1a; MongoDB 是一种非关系型数据库&#xff0c;以文档为中心&#xff0c;使用 JSON 格式的 BSON 来存储数据。它具有高可用性、高性能和易于扩展的特点&#xff0c;被广泛应用于各种规…

【数据结构与算法】快速排序霍尔版

&#x1f493; 博客主页&#xff1a;倔强的石头的CSDN主页 &#x1f4dd;Gitee主页&#xff1a;倔强的石头的gitee主页 ⏩ 文章专栏&#xff1a;《数据结构与算法》 期待您的关注 ​

【C/C++】构造函数被私有化,为什么还可以在类外直接new一个对象?

实例代码&#xff1a; // 饿汉模式 -> 定义类的时候创建单例对象 // 定义一个单例模式的任务队列 class TaskQueue { public:TaskQueue(const TaskQueue & t) delete;TaskQueue& operator(const TaskQueue& t) delete;static TaskQueue* getInstance(){retur…

react native中依赖@react-native-clipboard/clipboard库实现文本复制以及在app中获取复制的文本内容

react native中依赖react-native-clipboard/clipboard库实现文本复制以及在app中获取复制的文本内容 第三方库 第三方库 react-native-clipboard 我的项目react native0.72 我使用react-native-clipboard/clipboard1.13.2 npm install --save react-native-clipboard/clipboa…

el-table 树状表格查询符合条件的数据

需要对el-table的树状表格根据输入机构名称&#xff0c;筛选出符合条件的数据&#xff0c;可用如下方法&#xff1a; 页面内容如下&#xff1a; <el-input v-model"ogeName" placeholder"请输入机构名称"><el-table :data"list" row…

PIP换源的全面指南

##概述 在Python的世界里&#xff0c;pip是不可或缺的包管理工具&#xff0c;它帮助开发者安装和管理Python软件包。然而&#xff0c;由于网络条件或服务器位置等因素&#xff0c;直接使用默认的pip源有时会遇到下载速度慢或者连接不稳定的问题。这时&#xff0c;更换pip源到一…