STM32-ADC+DMA

本内容基于江协科技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 headervoid AD_Init(void)
{/*1. 开启RCC时钟,包括ADC和GPIO时钟,ADCCLK的分频器2. 配置GPIO,配置为模拟输入模式3. 配置多路开关,把左边的通道接入到右边的规则组列表里4. 配置ADC转换器,采用库函数结构体:包括AD转换器和AD数据寄存器5. 开关控制,调用ADC_Cmd函数,开启ADC6. 想要软件触发转换,有函数可以触发*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 6分频=12MHzGPIO_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 headervoid AD_Init(void)
{/*1. 开启RCC时钟,包括ADC和GPIO时钟,ADCCLK的分频器2. 配置GPIO,配置为模拟输入模式3. 配置多路开关,把左边的通道接入到右边的规则组列表里4. 配置ADC转换器,采用库函数结构体:包括AD转换器和AD数据寄存器5. 开关控制,调用ADC_Cmd函数,开启ADC6. 想要软件触发转换,有函数可以触发*/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 headeruint16_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. 传输计数器大于02. 触发源有触发信号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 headeruint16_t AD_Value[4];void AD_Init(void)
{/*1. 开启RCC时钟,包括ADC和GPIO时钟,ADCCLK的分频器2. 配置GPIO,配置为模拟输入模式3. 配置多路开关,把左边的通道接入到右边的规则组列表里4. 配置ADC转换器,采用库函数结构体:包括AD转换器和AD数据寄存器5. 开关控制,调用ADC_Cmd函数,开启ADC6. 想要软件触发转换,有函数可以触发*/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; // 外设站点的起始地址 ADCDMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 外设站点的数据宽度DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable ; // 外设站点的是否自增,否DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value; // 存储器站点的起始地址 SRAMDMA_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.mzph.cn/bicheng/42405.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Tabu Search — 温和介绍

Tabu Search — 温和介绍 目录 Tabu Search — 温和介绍 一、说明 二、什么是禁忌搜索以及我可以在哪里使用它&#xff1f; 三、禁忌搜索原则 四、短期记忆和积极搜索&#xff1a; 五、举例时间 六、结论&#xff1a; 七、参考&#xff1a; 一、说明 最近&#xff0c;我参加了…

在DevEco运行typeScript代码,全网详细解决执行Set-ExecutionPolicy RemoteSigned报出的错

目录 基本思路 网络推荐 本人实践 如下操作,报错: 基本思路 //在DevEco运行typeScript代码 /** * 1.保证node -v出现版本,若没有,配置环境变量(此电脑-属性-高级系统变量配置-path-粘贴路径);DevEco在local.properties中可看到当前nodejs的路径 * 2.npm install …

海外仓一件代发功能自动化:海外仓WMS系统配置方法

根据数据显示&#xff0c;2014-2019年短短几年之间&#xff0c;跨境电商销售总额增长了160%以上。这为跨境电商商家和海外仓&#xff0c;国际物流等服务端企业都提供了巨大的发展机遇。 然而&#xff0c;作为海外仓&#xff0c;要想服务好跨境电商&#xff0c;仓库作业的每一个…

车载测试之-CANoe创建仿真工程

在现代汽车工业中&#xff0c;车载测试是确保车辆电子系统可靠性和功能性的关键环节。而使用CANoe创建仿真工程&#xff0c;不仅能够模拟真实的车辆环境&#xff0c;还能大大提升测试效率和准确性。那么&#xff0c;CANoe是如何实现这些的呢&#xff1f; 车载测试中&#xff0…

刷题之合并两个有序数组(leetcode)

因为换了手机号码&#xff0c;之前leetcode的账号登不上去了&#xff0c;正好太久不刷题&#xff0c;很多思路都没了&#xff0c;所以重新开始刷leetcode&#xff01; 这道题很简单&#xff0c;指针模拟一下&#xff0c;从后往前考虑&#xff0c;先看最大值。 class Solution…

【大语言模型系列之Transformer】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

PLC电源模块

PM电源模块 为CPU信号模块及 其他的扩展设备、其他用电设备&#xff08;如传感器&#xff09;提供工作供电 接线和开关 状态显示 灯的闪烁示意看手册 PS电源模块 为CPU信号模块及其他的扩展设备提供工作供电。PS(System Power Supply) 外形与PM电源模块类似&#xff0c;状…

常用的MRI分析软件

MRI&#xff08;磁共振成像&#xff09;分析软件种类繁多&#xff0c;涵盖了从基础图像处理到高级数据分析的各个方面。这些软件广泛应用于临床诊断、研究和教育等领域。以下是一些常用的MRI分析软件&#xff1a; 开源软件 商用软件 特殊用途软件 在线工具和云平台 这些软件各…

MATLAB制作一个简单的函数绘制APP

制作一个函数绘制APP&#xff0c;输入函数以及左右端点&#xff0c;绘制出函数图像。 编写回调函数&#xff1a; 结果&#xff1a;

图片高效管理神器,随机高度切割,一键生成灰色图片,个性化处理随心所欲

在数字化时代&#xff0c;图片已成为我们生活和工作中不可或缺的一部分。然而&#xff0c;面对海量的图片资源&#xff0c;如何高效管理、快速处理&#xff0c;成为了许多人头疼的问题。今天&#xff0c;我们为您带来了一款全新的图片高效管理神器_——首助编辑高手&#xff0c…

Nginx 报错问题汇总

目录 一、nginx: [emerg] invalid number of arguments in "include" directive in C:\Program Files\nginx-1.15.4/conf/nginx.conf:61 总结&#xff1a; 二、nginx: [error] OpenEvent("Global\ngx_reload_2152") failed (5: Access is denied) 解决…

51-3 内网信息收集 - 获取RDP密码信息(没有实验成功)

获取常见应用软件凭据 注意: %USERPROFILE% 是环境变量。在使用系统权限时,可以将 %USERPROFILE% 替换为绝对路径,或使用其他用户的令牌进行操作。 获取 RDP 保存的凭据(远程桌面) 为了避免每次连接服务器都进行身份验证,经常使用 RDP 远程桌面连接远程服务器的用户可能…

ubuntu下运行程序时提示缺库问题的有效解决方法

目录 一、问题现象二、解决方式三、总结 一、问题现象 当我们平时在ubuntu上运行一个程序时时长会遇到如下情况&#xff0c;含义为本机缺少执行程序需要的库 这时候我们可能会根据缺少的库使用apt install 库名的模糊名字 进行安装&#xff0c;然后再去运行&#xff0c;此时可…

网页生成二维码、在线演示

https://andi.cn/page/621504.html

【OJ】运行时错误(Runtime Error)导致递归爆栈问题

在进行OJ赛时&#xff0c; 题目&#xff1a;给你一个整数n&#xff0c;问最多能将其分解为多少质数的和。在第一行输出最多的质数数量k,下一行输出k个整数&#xff0c;为这些质数。 出现运行时错误 代码如下&#xff1a; def main():# code heren int(eval(input()))list …

AI中药处方模型构建与案例

在中医领域,人工智能(AI)可以生成各种指令来辅助诊断、治疗和研究。 1. 诊断辅助指令: 根据患者的症状和体征,自动分析并生成可能的中医证候诊断建议。利用中医望闻问切四诊信息,智能识别关键症状,提供对应的中医辨证思路。2. 治疗建议指令: 根据辨证结果,自动推荐相应…

Java语言程序设计篇一

Java语言概述 Java语言起源编程语言最新排名名字起源Java语言发展历程Java语言的特点Java虚拟机垃圾回收Java语言规范Java技术简介Java程序的结构Java程序注意事项&#xff1a;注释编程风格练习 Java语言起源 1990年Sun公司提出一项绿色计划。1992年语言开发成功最初取名为Oak…

维护el-table列,循环生成el-table

1、lib/setting.js&#xff08;维护table列&#xff09; const columns[{ label: 类型, prop: energyName, width: 150, isText: true },{ label: 消耗量(t或10⁴m), prop: inputNum, isInput: true },{label: CO₂,children: [// { label: 核算因子, prop: co2FactorValue, w…

cs231n作业1——Softmax

参考文章&#xff1a;cs231n assignment1——softmax Softmax softmax其实和SVM差别不大&#xff0c;两者损失函数不同&#xff0c;softmax就是把各个类的得分转化成了概率。 损失函数&#xff1a; def softmax_loss_naive(W, X, y, reg):loss 0.0dW np.zeros_like(W)num_…

【Linux】进程的概念 + 查看进程

前言&#xff1a; 在前面我们学习了Liunx的基本指令和权限相关知识&#xff0c;还有基本工具的使用&#xff0c;有了以上的基础知识我们本章将正式接触Linux操作系统。 目录 1.冯诺依曼体系结构1.1 内存存在的意义1.2 程序加载到内存的含义1.3 程序的预加载&#xff1a; 2 .认识…