本内容基于江协科技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
- 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框图
- 对于普通的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的。
1.4 ADC基本结构
- 规则组最多可以选中16个通道;注入组最多可以选择4个通道;
- 转换结果存放在AD数据寄存器里,规则组只有1个数据寄存器,注入组有4个;
- 触发控制,提供了开始转换START信号;可以选择软件触发和硬件触发。
- 硬件触发主要来自于定时器,也可以选择外部中断的引脚。
来自于RCC的ADC时钟CLOCK,ADC逐次比较的过程就是由这个时钟推动的; - 可以布置一个模拟看门狗用于检测转换结果的范围,若超出设定的阈值,就通过中断输出控制,向NVIC申请中断;
- 规则组和注入组转换完成后会有个EOC信号,会置一个标志位,也可以通向NVIC。
- 开关控制:在库函数中,就是ADC_Cmd函数,用于给ADC上电的。
1.5 输入通道
1.6 规则组的转换模式
在ADC初始化的结构体里,会有两个参数:参1是选择单次转换还是连续转换,参2是选择扫描模式还是非扫描模式。
1.6.1 单次转换,非扫描模式
- 在非扫描模式下,该菜单只有第一个序列1的位置有效。菜单同时选中一组的方式就退化为简单地选中一个的方式。
- 在序列1可以指定要转换的通道,之后就可以触发转换,ADC就会对这个通道2进行模数转换。
- 转换完成后,结果存放在数据寄存器里,同时给EOC标志位置1,转换结束。
- 判断转换结束后,就可以在数据寄存器里读取结果。若想再启动一次转换,就需要再触发一次,转换结束,置EOC标志位,读结果。
- 若想换一个通道转换,则在转换之前,把第一个位置的通道2改为其他通道,然后再启动转换。
- 流程:触发转换–>判断转换结束(置EOC标志位)–>获取转换值
1.6.2 连续转换,非扫描模式
它在一次转换结束后,不会停止,而是立刻开始下一轮的转换,然后一直持续下去。因此只需最开始触发一次,之后就可以一直转换。优点是开始转换之后不需要等待一段时间,想要读AD值的时候,直接从数据寄存器取就是了。
1.6.3 单次转换,扫描模式
每触发一次,转换结束后,就会停下来,下次转换就得再触发才能开始。初始化结构体中还会有个参数:通道数目,若为7,就是在每次触发之后,依次对前7个位置进行AD转换,转换结果都放在数据寄存器里。为了防止数据被覆盖,就需要用DMA及时将数据挪走。7个通道转换完成之后,产生EOC信号,转换结束。然后再触发下一次,就又开始新一轮的转换。
1.6.4 连续转换,扫描模式
一次转换完成后,立刻开始下一次的转换。
在扫描模式的情况下,还有一种模式:间断模式,在扫描的过程中,每隔几个转换,就暂停一次,需要再次触发,才能继续。
1.7 触发控制
类型:外部引脚/来自片上定时器的内部信号,具体是引脚还是定时器,需要用AFIO重映射来确定。
软件控制位:软件触发
触发信号的选择可以通过设置右边的寄存器来完成。也可以使用库函数实现。
1.8 数据对齐
ADC是12位的,其转换结果就是一个12位的数据。但数据寄存器是16位的,所以存在数据对齐的问题
1.9 转换时间
- 量化编码即逐次比较
- 采样保持:是因为量化编码是需要一小段时间的,如果在这一小段时间里,输入的电压不断变化,则无法定位输入电压的位置,所以在量化编码之前,需要设置一个采样开关,先打开采样开关,收集一下外部的电压,之后断开采样开关,再进行AD转换,这样在量化编码的期间,电压始终保持不变,这样才能精确地定位未知电压的位置。
- 采样时间就是采样保持的时间,采样时间越大,越能避免一些毛刺信号的干扰。但转换时间也会相应延长。
- 12.5个ADC周期是量化编码花费的时间,因为是12位的ADC,所以需要花费12个周期,0.5个周期是做其他用的。ADC周期就是从RCC分频过来的ADCCLK,ADCCLK最大是14MHz。
1.10 校准
- ADC有一个内置自校准模式。校准可大幅减小因内部电容器组的变化而造成的准精度误差。校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差
- 建议在每次上电后执行一次校准
- 启动校准前, ADC必须处于关电状态超过至少两个ADC时钟周期
1.11 硬件电路
- 第一个:电位器可调电压的电路,就是接一个电位器,当滑动端往上滑时,电压增大,往下滑时,电压减小。
- 第二个:传感器输出电压的电路,如光敏电阻、热敏电阻、红外接收管、麦克风等都可以等效为一个可变电阻。传感器阻值变小时,下拉作用变强,输出端电压就下降;传感器阻值变大时,下拉作用变弱,输出端受上拉电阻的作用,电压就会升高。
- 第三个:电压转换电路,若想测一个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。
2.2.2 代码流程
- 开启RCC时钟,包括ADC和GPIO时钟,ADCCLK的分频器
- 配置GPIO,配置为模拟输入模式
- 配置多路开关,把左边的通道接入到右边的规则组列表里
- 配置ADC转换器,采用库函数结构体:包括AD转换器和AD数据寄存器
- 开关控制,调用ADC_Cmd函数,开启ADC
- 想要软件触发转换,有函数可以触发
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值减小。
2.3.2 硬件运行结果
2.3.3 代码流程
- 开启RCC时钟,包括ADC和GPIO时钟,ADCCLK的分频器
- 配置GPIO,配置为模拟输入模式
- 配置多路开关,把左边的通道接入到右边的规则组列表里
- 配置ADC转换器,采用库函数结构体:包括AD转换器和AD数据寄存器
- 开关控制,调用ADC_Cmd函数,开启ADC
- 想要软件触发转换,有函数可以触发
2.3.4 代码
- 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);
}
- 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 存储器映像
类型 | 起始地址 | 存储器 | 用途 |
---|---|---|---|
ROM | 0x0800 0000 | 程序存储器Flash | 存储C语言编译后的程序代码和常量· |
0x1FFF F000 | 系统存储器 | 存储BootLoader,用于串口下载 | |
0x1FFF F800 | 选项字节 | 存储一些独立于程序代码的配置参数 | |
RAM | 0x2000 0000 | 运行内存SRAM | 存储运行过程中的临时变量 |
0x4000 0000 | 外设寄存器 | 存储各个外设的配置参数 | |
0xE000 0000 | 内核外设寄存器 | 存储内核各个外设的配置参数 |
- 计算器的5大组成部分:运算器、控制器、存储器、输入设备和输出设备;
- 运算器和控制器合在一起,称为CPU;
因此,计算机的核心关键部分就是CPU和存储器 - 存储器又包括:存储器的内容、存储器的地址
- ROM是只读存储器,是一种非易失性、掉电不丢失的存储器。
- RAM是随机存储器,是一种易失性、掉电丢失的存储器。
- 程序存储器Flash:存储C语言编译后的程序代码,即下载程序的位置。
- 运行程序,一般也是从主闪存里面开始运行的。
- 系统存储器和选项字节:实际存储介质也是Flash。
- 选项字节:存的主要是Flash的读保护、写保护。
- 运行内存SRAM:存储运行过程中的临时变量,就是在程序中定义变量、数组、结构体的地方。
3.3 DMA框图
主要包括:CPU和存储器
- 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基本结构
- 左边的外设寄存器与右边的寄存器(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请求(触发)
默认优先级是通道号越小,优先级越高。也可以在程序中配置优先级。
3.6 数据宽度与对齐
- 如果数据宽度都一样,就是正常的一个一个转运。当源端和目标都是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
- 外设地址:DataA数组的首地址;存储器地址:DataB数组的首地址
- 起始地址:DataA[0]和DataB[0]
- 数据宽度:8位
- 地址是否自增:是,两边地址都要自增。
- 方向参数:外设站点 ->存储器站点
- 传输计数器:7,不需要自动重装
- 触发:软件触发
3.8 ADC扫描模式+DMA
- 触发一次后,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、地址及数据。
4.2.2 代码流程
- RCC开启DMA的时钟(AHB时钟)
- 调用DMA_Init,初始化参数:外设和存储器站点的起始地址、数据宽度、地址是否自增、方向、传输计数器、是否需要自动重装、选择触发源
- 开关控制,DMA_Cmd
- 如果是硬件触发,在对应的外设调用一下xxx_DMACmd,开启一下触发信号的输出
- 如果需要DMA的中断,调用DMA_ITConfig,开启中断输出,再在NVIC里,配置相应的中断通道,写中断函数
- 如果转运完成,传输计数器清0了,若再想给传输计数器赋值的话,就DMA失能、写传输计数器、DMA使能。
4.2.3 代码
- 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);
}
- 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显示一下。
4.3.2 代码流程
- 开启RCC时钟,包括ADC和GPIO时钟,ADCCLK的分频器
- 配置GPIO,配置为模拟输入模式
- 配置多路开关,把左边的通道接入到右边的规则组列表里
- 配置ADC转换器,采用库函数结构体:包括AD转换器和AD数据寄存器
- 开关控制,调用ADC_Cmd函数,开启ADC
- 想要软件触发转换,有函数可以触发
4.3.3 代码
- 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);
}
*/
- 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);}
}