我们先试验一下,看一下存储器的地址是不是和我们想的一样:
uint8_t aa = 0x66;
OLED_ShowHexNum(1, 1, aa, 2);
OLED_ShowHexNum(2, 1, (uint32_t)&aa, 2);
变量取地址之后,应该要存在一个指针变量里,这里如果直接想当做一个数字显示的话,还得在前面加上强制类型转换。


与内存映射图中的SRAM地址一致
我们可以在变量前面加一个const关键字,const是C语言的关键字,表示常量,被const修饰的变量,在程序中只能读不能写,Flash里的数据也是只能读不能写,在STM32中使用const定义的变量,是存储在Flash里面的
const uint8_t aa = 0x66;

对应存储映射表,现在这个aa的地址就对应到Flash的范畴里了。一般是在程序中出现了一大批数据,并且不需要更改时,就可以把它定义成常量,这样能节省SRAM的空间,比如查找表,字库数据等。
对于变量或者常量,它的地址是由编译器确定的,不同程序,地址可能不一样,对于外设寄存器来说,它的地址是固定的,手册中都能查到,在程序中也可以使用结构体,很方便地访问寄存器,比如要访问ADC1的DR寄存器就可以这样写:
ADC1 -> DR
我们访问其地址看看,OLED_ShowHexNum(2, 1, (uint32_t)&ADC1 -> DR, 8);

对照映射表,这确实是外设寄存器的区域,这个地址是固定的,在手册中也能差到,可以看到下表1中,ADC1的地址是40012400开始的,第二个表中DR寄存器的地址偏移量是4C,所以上面显示的ADC1的DR寄存器的地址就是4001244C。


那么这个ADC -> DR是怎么操作的呢,为什么会这样写呢。其实这里的ADC1是一个结构体变量,我们可以右键跳转到其定义。

就是强制转换ADC1_BASE为ADC结构体变量指针,再宏定义为ADC1。那这个ADC1_BASE又是什么呢我们还是可以右键跳转到其定义看看。


原来ADC1_BASE就是在APB2外设地址上再加上偏移量0x2400而组成的地址,而APB2外设地址又是在外设地址上再加上偏移量0x20000而组成的地址,所以设计者只需要给基础的几个值赋值后,再各自的领域上宏定义,再加上各自的偏移量就能够定义好每个寄存器的地址。

而这个强制转换的结构体类型定义了外设中所有的寄存器,这个结构体成员的顺序和寄存器实际存放的顺序是一一对应的。如果我们定义一个结构体指针,并且指针的地址就是这个外设的起始地址,那这个结构体的每个成员,就会正好映射实际的每个寄存器。
接下来我们就开始DMA的配置
我们的目标是将DataA数组中的数转运到DataB中:
uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};
uint8_t DataB[];
接下来的任务就是,初始化DMA,然后让DMA把DataA的数据,转运到DataB里面去

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);//清除中断挂起位
很多都是经典函数,只有两个是更DMA比较相关的,接下来我们开始写初始化的代码:
uint16_t MyDMA_Size;
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
MyDMA_Size = Size;
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;//外设站点的起始地址
对于SRAM的数组,它的地址是编译器分配的,并不是固定的,所以我们一般不会写绝对地址,而是通过数组名来获取地址,这里我们就把这个地址提取成初始化函数的参数,这样在初始化的时候,想转运哪个数组,就把哪个数组的地址传进来就可以了
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, ENABLE);
}
接下来就是主函数:
#include “stm32f10x.h” // Device header
#include “Delay.h”
#include “OLED.h”
#include “MyDMA.h”
uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};
uint8_t DataB[] = {0, 0, 0, 0};
int main(void)
{
OLED_Init();
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);
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);//初始化后立刻转运
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);
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数据又变化了,我们想再转运一次,该怎么办呢。
这时,我们就需要给传输计数器重新赋值了
我们可以在初始化之后再写一个函数:
void MyDMA_Transfer(void)//调用一次这个传输函数,就再启用一次DMA
{
DMA_Cmd(DMA1_Channel1, DISABLE);//要重新给传输计数器赋值,就需要先失能DMA
DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);//MyDMA_Size是全局变量,再初始化函数中已经赋给它值了
DMA_Cmd(DMA1_Channel1, ENABLE);//我们可以将初始化函数中的使能函数填为使能,这样就能调用这个函数统一使能DMA了while(!DMA_GetFlagStatus(DMA1_FLAG_TC1));//查询DMA转运完成的标志位,转换完成后标志位置一,所以用while循环等待
DMA_ClearFlag(DMA1_FLAG_TC1);//清除转运完成的标志位,因为前面有while循环要靠标志位判断,所以需要清除标志位。
}
主函数:
#include “stm32f10x.h” // Device header
#include “Delay.h”
#include “OLED.h”
#include “MyDMA.h”
uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};
uint8_t DataB[] = {0, 0, 0, 0};
int main(void)
{
OLED_Init();
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);
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]++;//变更A数组数值
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);
}
}
接下来是DMA+ADC多通道的使用,我们先到AD.c加上DMA转运数据的功能,我们直接把DMA的初始化代码都复制到ADC控制使能函数之前。
整体的初始化代码,以及转换函数如下:
#include “stm32f10x.h” // Device header
uint16_t AD_Value[4];
void AD_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);//ADC都是APB2上的设备
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//DMA是AHB总线的设备,所以要用AHB开启时钟的函数
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//选择模拟输入模式,即ADC专属模式
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_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是独立模式,还是双ADC模式
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//指定ADC是左对齐还是右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//外部触发转换选择,就是触发控制的触发源,我们使用软件触发,所以就不启用外部触发
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;//连续转换模式,可以选择连续转换还是单次转换
ADC_InitStructure.ADC_ScanConvMode = ENABLE;//扫描转换模式,可以选择扫描模式还是非扫描模式
ADC_InitStructure.ADC_NbrOfChannel = 4;//通道数目,指定在扫描模式下总共会用到几个通道,非扫描模式下,这个参数没有用
ADC_Init(ADC1, &ADC_InitStructure);
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1 -> DR;//外设站点的起始地址,ADC转换好的数据都放在DR寄存器里,即0x4001244C
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//数据宽度,我们需要DR寄存器低16位的数据,所以数据宽度选HalfWord
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//是否自增,这里选择不自增,因为ADC_DR的值一直变化但是地址不变
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;//存储器站点的其实地址,自己创建的存放数组
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//数据宽度
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//是否自增,这里需要自增,不自增的话存放数组最后会只有ADC最终转换好的数据
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//传输方向
DMA_InitStructure.DMA_BufferSize = 4;//缓存区大小,就是传输计数器
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;//传输模式,就是是否使用自动重装
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;//选择是否存储器到存储器,就是选择硬件触发还是软件触发,这里选择硬件触发,触发源为ADC
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//优先级
DMA_Init(DMA1_Channel1, &DMA_InitStructure);//ADC1的硬件触发只接在了DMA1的通道1上,只能够这样选
DMA_Cmd(DMA1_Channel1, ENABLE);
ADC_DMACmd(ADC1, ENABLE);//用来开启DMA触发信号的
ADC_Cmd(ADC1, ENABLE);
ADC_ResetCalibration(ADC1);//开始复位校准,会把CR2_RSTCAL_Set置一
while(ADC_GetResetCalibrationStatus(ADC1));//返回复位校准状态,所以要等待复位校准完成的话还需要加上while循环,会读取CR2_RSTAL_Set,如果变为0说明复位校准完成。
ADC_StartCalibration(ADC1);//启动校准
while(ADC_GetCalibrationStatus(ADC1));//等待校准完成
}
void AD_GetValue(void)
{
DMA_Cmd(DMA1_Channel1, DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel1, 4);
DMA_Cmd(DMA1_Channel1, ENABLE);
//因为DMA也是正常的单次模式,所以在触发ADC之前,需要重新写入一下传输计数器,
ADC_SoftwareStartConvCmd(ADC1, ENABLE);//因为现在ADC还是单次模式,所以还需要软件触发ADC
while(!DMA_GetFlagStatus(DMA1_FLAG_TC1));
DMA_ClearFlag(DMA1_FLAG_TC1);
//这里因为DMA转运总是在ADC转换之后的,所以我们可以直接使用等待DMA完成的代码
}
我们可以将ADC的连续转换模式,和DMA的自动重装模式都打开,直接在AD初始化函数中软件触发ADC
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;//连续转换模式,可以选择连续转换还是单次转换
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;//传输模式,就是是否使用自动重装
ADC_SoftwareStartConvCmd(ADC1, ENABLE);//当ADC触发之后,ADC连续转换,DMA循环转运,始终把最新的转换结果,刷新到SRAM数组中,这样后面的GetValue函数就完全不需要了。主循环直接读取AD_Value数组即可
此时硬件外设已经实现了相互配合,高度的自动化了,软件不需要做什么,也不需要进任何中断,硬件自动把活干完了,节省软件资源,这是STM32中硬件自动化的一大特色,各个外设互相连接互相交织,不再是传统的一个CPU单独控制多个独立的外设的星形结构,而是外设之间互相连接互相合作形成的网状结构,这样在完成某些简单且琐碎的工作时,就不需要CPU来统一调度了,可以直接通过外设之间的相互配合,自动完成琐碎工作。这样不仅能减轻CPU的负担,还可以大大提升外设的性能。
最近一段时间没怎么写力扣的题目,确实搞这些加上写日志比较费时间,一搞基本一个晚上就过去了。可惜了这次本来可以拿个连续刷题一个月的徽章,不过今天倒是已经是写了一个月的日志了,虽然也不是天天都写,但还是坚持下来了,至少也是要两天之内写一篇的。总共有27篇,除了一开始的自动发的一篇和自己一开始发的一篇,总共也写了25篇了,感觉还是不错的,30天写了25篇,不对2月份只有29天的,但仔细算了好像也是刚好30天,也挺可以的了,坚持写了下来,如果当时寒假刚开始的时候就跟着训练营写下来就好了。这段时间里我觉得给我最多动力的就是我现在最喜欢的鱼韵乐队了,奖励自己一张鱼韵的CD不过分吧😋😋😋,可惜快一周前买了到现在还没有到国内,感觉可能到清明节才能到。现在也是听着他们的歌在写,希望以后能一直写下去吧。我其实还挺喜欢写日记之类的东西的,我记得初中还是高中那会有个日记软件,应该是从你的名字里来的灵感,这个软件会随机选一个异性和你绑定然后你们可以互看各自写的日记。感觉还挺有意思的,那时候我就每天晚上从学校回来就要写日记,起码也要写个七八百字那种,写到我妈叫我别写赶紧睡觉了😅😅,感觉我是那个时候喜欢上写日记的,大学期间我也试着写过日记,手写的,但没坚持很久,我感觉我还是更喜欢打字写日记,我还是挺喜欢打字的感觉的,而且打字写不会很累,有时候用笔写的速度跟不上你思考想内容的速度就会很难受,打字就快很多,也比较轻松手不会很累。
今天写了这么多字,就也推荐一首好听的歌曲吧。
今天推荐歌曲:スローモーション——sakanaction(点击左下角倒数第11首即可收听)