效果展示
ADC-单通道采集(DMA)
硬件原理图
源代码
ADC部分
#ifndef __BSP_ADC_H__
#define __BSP_ADC_H__
#include "apm32f10x.h"
#include "apm32f10x_rcm.h"
#include "apm32f10x_adc.h"
#include "apm32f10x_misc.h"
#include "apm32f10x_gpio.h"
#include "apm32f10x_dma.h"
// 只有ADC1和ADC3有DMA通道
#define ADCX ADC1
#define ADCX_DR_ADDR (uint32_t)(ADC1_BASE + 0x4C) // ADC1数据寄存器地址
#define ADCX_CLOCK RCM_APB2_PERIPH_ADC1
#define ADCX_IRQn ADC1_2_IRQn
// #define ADC_DATA_ADDR ADC1->REGDATA
#define ADCX_GPIO_PORT GPIOA
#define ADCX_GPIO_PIN GPIO_PIN_5
#define ADCX_GPIO_CLOCK RCM_APB2_PERIPH_GPIOA
#define ADCX_CHANNEL ADC_CHANNEL_5
#define ADCX_DMA_CLOCK RCM_AHB_PERIPH_DMA1
#define ADCX_DMA_CHANNEL DMA1_Channel1
void ADCx_Config(void);
#endif
#include "bsp_adc.h"
__IO uint16_t ADC_ConvertedValue;
/**
* @brief 配置ADCx的GPIO
* @param 无
*/
static void ADCx_GPIO_Config(void)
{
GPIO_Config_T GPIO_ConfigStruct;
RCM_EnableAPB2PeriphClock(ADCX_GPIO_CLOCK);
GPIO_ConfigStruct.mode = GPIO_MODE_ANALOG;
GPIO_ConfigStruct.pin = ADCX_GPIO_PIN;
GPIO_Config(ADCX_GPIO_PORT, &GPIO_ConfigStruct);
}
static void ADCx_Mode_Config(void)
{
ADC_Config_T ADC_ConfigStruct;
DMA_Config_T DMA_ConfigStruct;
// 打开ADC时钟
RCM_EnableAPB2PeriphClock(ADCX_CLOCK);
// 打开DMA时钟
RCM_EnableAHBPeriphClock(ADCX_DMA_CLOCK);
// 复位DMA控制器
DMA_Reset(ADCX_DMA_CHANNEL);
// 数据长度
DMA_ConfigStruct.bufferSize = 1;
// 数据源来自外设
DMA_ConfigStruct.dir = DMA_DIR_PERIPHERAL_SRC;
// 循环传输模式
DMA_ConfigStruct.loopMode = DMA_MODE_CIRCULAR;
// 禁止储存器到储存器模式
DMA_ConfigStruct.M2M = DMA_M2MEN_DISABLE;
// 存储器地址
DMA_ConfigStruct.memoryBaseAddr = (uint32_t)&ADC_ConvertedValue;
// 内存数据大小也为半字,跟外设数据大小相同
DMA_ConfigStruct.memoryDataSize = DMA_MEMORY_DATA_SIZE_HALFWORD;
// 内存地址不自增,就一个变量
DMA_ConfigStruct.memoryInc = DMA_MEMORY_INC_DISABLE;
// ADC1数据地址
DMA_ConfigStruct.peripheralBaseAddr = ADCX_DR_ADDR;
// 外设数据大小为半字,16位
DMA_ConfigStruct.peripheralDataSize = DMA_PERIPHERAL_DATA_SIZE_HALFWORD;
// 外设地址不自增
DMA_ConfigStruct.peripheralInc = DMA_PERIPHERAL_INC_DISABLE;
// 优先级为高
DMA_ConfigStruct.priority = DMA_PRIORITY_HIGH;
DMA_Config(ADCX_DMA_CHANNEL, &DMA_ConfigStruct);
DMA_Enable(ADCX_DMA_CHANNEL);
// 连续转换模式
ADC_ConfigStruct.continuosConvMode = ENABLE;
// 转换结果右对齐
ADC_ConfigStruct.dataAlign = ADC_DATA_ALIGN_RIGHT;
// 不用外部触发转换,软件开启即可
ADC_ConfigStruct.externalTrigConv = ADC_EXT_TRIG_CONV_None;
// 独立模式
ADC_ConfigStruct.mode = ADC_MODE_INDEPENDENT;
// 一个转换通道
ADC_ConfigStruct.nbrOfChannel = 1;
// 禁止扫描模式
ADC_ConfigStruct.scanConvMode = DISABLE;
// 配置ADC参数
ADC_Config(ADCX, &ADC_ConfigStruct);
// 配置ADC时钟为PCLK2的8分频,即9MHz
RCM_ConfigADCCLK(RCM_PCLK2_DIV_8);
// 配置 ADC 通道转换顺序为1,第一个转换,采样时间为55.5个时钟周期
ADC_ConfigRegularChannel(ADCX, ADCX_CHANNEL, 1, ADC_SAMPLETIME_55CYCLES5);
// 使能ADC DMA 请求
ADC_EnableDMA(ADCX);
// 使能ADC
ADC_Enable(ADCX);
// 初始化ADC校准寄存器
ADC_ResetCalibration(ADCX);
// 等待校准寄存器初始化完成
while (ADC_ReadResetCalibrationStatus(ADCX))
;
// ADC开始校准
ADC_StartCalibration(ADCX);
// 等待校准完成
while (ADC_ReadCalibrationStartFlag(ADCX))
;
// 软件触发ADC转换
ADC_EnableSoftwareStartConv(ADCX);
}
void ADCx_Config(void)
{
ADCx_GPIO_Config();
ADCx_Mode_Config();
}
串口打印部分
#ifndef __BSP_DEBUG_USART_H__
#define __BSP_DEBUG_USART_H__
#include "apm32f10x_gpio.h"
#include "apm32f10x_usart.h"
#include "apm32f10x_rcm.h"
#include <stdio.h>
/* 宏定义 --------------------------------------------------------------------*/
#define DEBUG_USARTX_BAUDRATE 115200
#define DEBUG_USARTX USART1
#define DEBUG_USARTX_CLOCK RCM_APB2_PERIPH_USART1
#define DEBUG_USARTX_TX_PORT GPIOA
#define DEBUG_USARTX_TX_PIN GPIO_PIN_9
#define DEBUG_USARTX_TX_CLOCK RCM_APB2_PERIPH_GPIOA
#define DEBUG_USARTX_RX_PORT GPIOA
#define DEBUG_USARTX_RX_PIN GPIO_PIN_10
#define DEBUG_USARTX_RX_CLOCK RCM_APB2_PERIPH_GPIOA
/* 函数声明 ------------------------------------------------------------------*/
void DEBUG_USART_Config(void);
#endif // __BSP_DEBUG_USART_H__
#include "bsp_debug_usart.h"
/**
* @brief 板载调试串口参数配置
*/
void DEBUG_USART_Config(void)
{
// 定义IO硬件初始化结构体变量
GPIO_Config_T GPIO_ConfigStruct;
// 定义USART初始化结构体变量
USART_Config_T USART_ConfigStruct;
// 使能复用时钟
RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_AFIO);
// 使能USART时钟
RCM_EnableAPB2PeriphClock(DEBUG_USARTX_CLOCK);
// 使能USART功能GPIO时钟
RCM_EnableAPB2PeriphClock(DEBUG_USARTX_RX_CLOCK | DEBUG_USARTX_TX_CLOCK);
// 设定USART发送对应IO模式:复用推挽输出
GPIO_ConfigStruct.mode = GPIO_MODE_AF_PP;
// 设定USART发送对应IO编号
GPIO_ConfigStruct.pin = DEBUG_USARTX_TX_PIN;
// 串口2M够用
GPIO_ConfigStruct.speed = GPIO_SPEED_2MHz;
// 初始化USART发送对应IO
GPIO_Config(DEBUG_USARTX_TX_PORT, &GPIO_ConfigStruct);
// 设定USART发送对应IO模式:浮空输入
GPIO_ConfigStruct.mode = GPIO_MODE_IN_FLOATING;
// 设定USART接收对应IO编号
GPIO_ConfigStruct.pin = DEBUG_USARTX_RX_PIN;
GPIO_ConfigStruct.speed = GPIO_SPEED_2MHz;
GPIO_Config(DEBUG_USARTX_RX_PORT, &GPIO_ConfigStruct);
// USART波特率:115200
USART_ConfigStruct.baudRate = DEBUG_USARTX_BAUDRATE;
// USART硬件数据流控制(硬件信号控制传输停止):无
USART_ConfigStruct.hardwareFlow = USART_HARDWARE_FLOW_NONE;
// USART工作模式使能:允许接收和发送
USART_ConfigStruct.mode = USART_MODE_TX_RX;
// USART校验位:无
USART_ConfigStruct.parity = USART_PARITY_NONE;
// USART停止位:1位
USART_ConfigStruct.stopBits = USART_STOP_BIT_1;
// USART字长(有效位):8位
USART_ConfigStruct.wordLength = USART_WORD_LEN_8B;
USART_Config(DEBUG_USARTX, &USART_ConfigStruct);
USART_Enable(DEBUG_USARTX);
}
/**
* @brief 重定向c库函数printf到USARTx
* @param ch
* @param f
* @return
*/
int fputc(int ch, FILE *f)
{
/* 发送一个字节数据到调试串口 */
USART_TxData(DEBUG_USARTX, (uint8_t)ch);
/* 等待串口数据发送完毕 */
while (USART_ReadStatusFlag(DEBUG_USARTX, USART_FLAG_TXBE) == RESET)
;
return (ch);
}
/**
* @brief 重定向c库函数getchar,scanf到USARTx
* @param f
* @return
*/
int fgetc(FILE *f)
{
/* 等待串口输入数据 */
while (USART_ReadStatusFlag(DEBUG_USARTX, USART_FLAG_RXBNE) == RESET)
;
return (int)USART_RxData(DEBUG_USARTX);
}
主程序部分
/**
* @file main.c
* @brief PA5为ADC引脚
*/
#include "bsp_debug_usart.h"
#include "bsp_adc.h"
/* 用于保存转换计算后的电压值 */
float ADC_ConvertedValueLocal;
/* 扩展变量 ------------------------------------------------------------------*/
extern __IO uint16_t ADC_ConvertedValue;
/* 私有函数原形 --------------------------------------------------------------*/
static void Delay(uint32_t time);
int main(void)
{
DEBUG_USART_Config();
ADCx_Config();
printf("----这是一个ADC单通道电压采集实验-----\n");
while (1)
{
ADC_ConvertedValueLocal = (float)ADC_ConvertedValue * 3.3 / 4096;
printf("ADC转换原始值 = 0x%04X \r\n", ADC_ConvertedValue);
printf("计算得出电压值 = %f V \r\n", ADC_ConvertedValueLocal);
Delay(1000);
}
}
/**
* @brief 不精确延时函数
* @param time
*/
static void Delay(uint32_t time)
{
uint32_t i, j;
for (i = 0; i < time; ++i)
{
for (j = 0; j < 10000; ++j)
{
// 空循环,什么都不做
}
}
}
代码分析
bsp_adc.c
上一篇文章已经讲过软件触发ADC采集,中断读取数据,这一套流程都需要CPU亲自去做,我们这次的实验请来一个打工人(DMA)来帮忙做这一套流程,CPU只需要对数据进行处理即可。
来看下参考手册对DMA的介绍
DMA(Direct Memory Access:直接存储器存取)在无须 CPU 干预的情况下,可实现外设与存储器或存储器与存储器之间数据的高速传输,从而节省 CPU 资源来做其他操作。
来看下DMA怎么配置
DMA_Config_T DMA_ConfigStruct;
// 打开DMA时钟
RCM_EnableAHBPeriphClock(ADCX_DMA_CLOCK);
// 复位DMA控制器
DMA_Reset(ADCX_DMA_CHANNEL);
// 数据长度
DMA_ConfigStruct.bufferSize = 1;
// 数据源来自外设
DMA_ConfigStruct.dir = DMA_DIR_PERIPHERAL_SRC;
// 循环传输模式
DMA_ConfigStruct.loopMode = DMA_MODE_CIRCULAR;
// 禁止储存器到储存器模式
DMA_ConfigStruct.M2M = DMA_M2MEN_DISABLE;
// 存储器地址
DMA_ConfigStruct.memoryBaseAddr = (uint32_t)&ADC_ConvertedValue;
// 内存数据大小也为半字,跟外设数据大小相同
DMA_ConfigStruct.memoryDataSize = DMA_MEMORY_DATA_SIZE_HALFWORD;
// 内存地址不自增,就一个变量
DMA_ConfigStruct.memoryInc = DMA_MEMORY_INC_DISABLE;
// ADC1数据地址
DMA_ConfigStruct.peripheralBaseAddr = ADCX_DR_ADDR;
// 外设数据大小为半字,16位
DMA_ConfigStruct.peripheralDataSize = DMA_PERIPHERAL_DATA_SIZE_HALFWORD;
// 外设地址不自增
DMA_ConfigStruct.peripheralInc = DMA_PERIPHERAL_INC_DISABLE;
// 优先级为高
DMA_ConfigStruct.priority = DMA_PRIORITY_HIGH;
DMA_Config(ADCX_DMA_CHANNEL, &DMA_ConfigStruct);
DMA_Enable(ADCX_DMA_CHANNEL);
DAM的配置还是套路:
- 声明结构体
- 开启相关时钟
- 配置结构体
- 结构体填入配置函数中
- 使能相关外设,DMA是按通道使能的,使能相关通道即可
来看下DMA_Config_T
/**
* @brief DMA Config struct definition
*/
typedef struct
{
uint32_t peripheralBaseAddr;
uint32_t memoryBaseAddr;
DMA_DIR_T dir;
uint32_t bufferSize;
DMA_PERIPHERAL_INC_T peripheralInc;
DMA_MEMORY_INC_T memoryInc;
DMA_PERIPHERAL_DATA_SIZE_T peripheralDataSize;
DMA_MEMORY_DATA_SIZE_T memoryDataSize;
DMA_LOOP_MODE_T loopMode;
DMA_PRIORITY_T priority;
DMA_M2MEN_T M2M;
} DMA_Config_T;
总共11个参数,非常的多啊,我们一个一个看
-
peripheralBaseAddr
外设基地址:这个外设的地址一般填各种外设的数据寄存器地址,因为一般的数据的传输方向为外数据设寄存器到数组或者数组到外设数据寄存器。我们这里填的ADCX_DR_ADDR
即(uint32_t)(ADC1_BASE + 0x4C)
,看参考手册389页,可以知道这是规则数据寄存器。 -
peripheralDataSize
外设数据宽度:可选参数DMA_PERIPHERAL_DATA_SIZE_BYTE
字节也就是8位、DMA_PERIPHERAL_DATA_SIZE_HALFWORD
半字,在32位单片机中为16位、DMA_PERIPHERAL_DATA_SIZE_WOED
字,在32位单片机中为32位。由于ADC的数据是放在16位寄存器中的,这个参数我们选择DMA_PERIPHERAL_DATA_SIZE_HALFWORD
。我怀疑官方的库打错字了WOED
是什么意思?不太懂。 -
bufferSize
数据传输数量:数据传输数量范围为 0 至 65535,这个参数一般根据设置数组的大小来填,数组多大,这个参数就填多大。我们这里填1,因为我们就一个数据要传输。 -
peripheralInc
外设地址增量模式:DMA_PERIPHERAL_INC_DISABLE
禁止外设地址增量,DMA_PERIPHERAL_INC_ENABLE
使能外设地址增量。这个参数如果设置成了使能,每传输一个peripheralDataSize
的数据,peripheralBaseAddr
对应的地址就会加peripheralDataSize
。这个参数我们选择DMA_PERIPHERAL_INC_DISABLE
因为ADC的数据寄存器地址是固定的。 -
memoryBaseAddr
储存器基地址:这个地址一般填我们自己设置的数组的地址,DMA主要就是把数据搬到外设数据寄存器或者把外设数据寄存器的数据搬到我们自己设置的数组。 -
memoryDataSize
存储器数据宽度:DMA_MEMORY_DATA_SIZE_BYTE
8位、DMA_MEMORY_DATA_SIZE_HALFWORD
16位、DMA_MEMORY_DATA_SIZE_WOED
32位,这个和peripheralDataSize
差不多,一般两者的数据宽度要相同,所以这个参数选DMA_MEMORY_DATA_SIZE_HALFWORD
。 -
memoryInc
存储器地址增量模式:DMA_MEMORY_INC_DISABLE
禁止、DMA_MEMORY_INC_ENABLE
使能。如果我们储存器是一个数组且ADC采集的是多通道的数据就需要使能。我们这里就用到了一个通道,所以就禁止。 -
dir
数据传输方向:DMA_DIR_PERIPHERAL_SRC
从外设读至存储器、DMA_DIR_PERIPHERAL_DST
从存储器读至外设。我们这次选用DMA_DIR_PERIPHERAL_SRC
,把ADC读到数据搬到我们自己的创建的数组中。 -
M2M
存储器到存储器模式:DMA_M2MEN_DISABLE
禁止、DMA_M2MEN_ENABLE
使能。这个模式主要用来把数据从一个数组搬到另一个数组里,一般用不到,我们这个参数选DMA_M2MEN_DISABLE
。 -
priority
通道优先级,DMA_PRIORITY_LOW
、DMA_PRIORITY_MEDIUM
、DMA_PRIORITY_HIGH
、DMA_PRIORITY_VERYHIGH
。由于一个DMA有多个通道,谁先谁后要设置下,我们这里就一个通道随便设置一个即可。 -
loopMode
循环模式:DMA_MODE_NORMAL
禁止循环模式,DMA只传输一次,若需再传输需要手动使能、DMA_MODE_CIRCULAR
使能循环模式,无需手动重复使能,使能一次后一直循环搬数据。
配置结构体完成后使能相关通道DMA_Enable(ADCX_DMA_CHANNEL);
,还要使能ADC的DMA模式ADC_EnableDMA(ADCX);
,这款单片机只有ADC1和ADC3能产生DMA请求。
main.c
while (1)
{
ADC_ConvertedValueLocal = (float)ADC_ConvertedValue * 3.3 / 4096;
printf("ADC转换原始值 = 0x%04X \r\n", ADC_ConvertedValue);
printf("计算得出电压值 = %f V \r\n", ADC_ConvertedValueLocal);
Delay(1000);
}
可以看到这里直接就开始换算ADC采集到的电压值了,也没有用ADC中断去读值,DMA自动的把读取到的值搬到ADC_ConvertedValue
。
也可以开启DMA传输完成中断进行数据读取,如果数据量很大还可以开启DMA传输过半的中断,数据传输到一般就开始处理。