zl程序教程

您现在的位置是:首页 >  硬件

当前栏目

【GD32】从0开始学GD32单片机(13)—— ADC模数转换器外设详解+PS2遥杆例程

单片机 详解 开始 13 例程 ADC 外设
2023-09-11 14:21:44 时间

简介

GD32的ADC是一个12位的ADC,它是一种采用逐次逼近方式的模拟数字转换器。在GD32F1系列中ADC最多有18个多路复用通道,可以转换来自16个外部通道和2个内部通道的模拟信号。并且有自校准功能和可编程的采样时间

下面是ADC的系统框图。

在这里插入图片描述

自校准功能

ADC带有一个前置校准功能。在校准期间,ADC计算一个校准系数,这个系数是应用于ADC内
部的,它直到ADC下次掉电才无效。在校准期间,应用不能使用ADC,必须等到校准完成。在校准期间CLB位会一直保持1,直到校准完成,该位由硬件清0。 当ADC运行条件改变(例如,VDDA、VREF+以及温度等)或第一次开始A/D转换,建议执行一次校准操作

可编程的采样时间

ADC外设挂载在APB2总线上,其时钟频率与AHB总线相同,最高为108MHz,但ADC的最高时钟频率为14MHz,在RCU时钟控制器中,有一个专门用于ADC时钟的可编程分频器,用户在使能ADC前需将时钟频率分频至14MHz以下。
在12位分辨率的情况下,总转换时间=采样时间+12.5 个ADCCLK周期
例如:ADCCLK = 14MHz,采样时间为 1.5 个周期,那么总的转换时间为=(1.5+12.5) / 14MHz=1us。
如果想要获取较稳定的采集数据需要降低 ADC 的采样时钟,增大采样周期,硬件运行
的情况下减小外部输入阻抗

外部触发

使能ADC后,除了通过软件触发ADC开始转换外,还可以使用外部触发,最常用的是通过定时器来触发ADC;除定时器触发外,也可以使用外部中断来触发ADC开始转换。

下表是ADC0和ADC1规则通道所支持的外部触发源。

在这里插入图片描述
下表是ADC0和ADC1注入通道所支持的外部触发源。

在这里插入图片描述

规则组和注入组

ADC中的转换通道可以组织成两种组别:一个规则组通道和一个注入组通道。对于规则组,可以按照特定的序列组织成多达16个转换的序列;对于注入组,可以按照特定的序列组织成多达4个转换的序列。
这两个组别工作方式的不同与转换模式有关,在下面将一一介绍。

转换模式

单次转换模式

该模式能够运行在规则组和注入组。当ADC被使能,一旦相应软件触发或者外部触发发生,ADC就会采样和转换一个通道。

在这里插入图片描述

规则通道单次转换结束后,转换数据将被存放于ADC_RDATA寄存器中,EOC将会置1;如果
使能了相应中断,将产生一个中断。
注入通道单次转换结束后,转换数据将被存放于ADC_IDATA0寄存器中,EOC和EOIC位将会
置1;如果使能了相应的中断,将产生一个中断。

EOC,全称“End of Convertion”,转换结束标志位。
EOIC,全称“End of Injection Convertion”,注入通道转换结束标志位。

连续转换模式

该模式可以运行在规则组通道上。当ADC被使能,一旦相应软件触发或者外部触发产生,ADC就会连续采样和转换规定的通道,每次采样结束EOC都会置1。

在这里插入图片描述

扫描转换模式

该模式能够运行在规则组和注入组。在此模式下,ADC会转换所有被用户选中的通道;一旦ADC被使能,当相应软件触发或者外部触发产生,ADC就会一个接一个的采样和转换规则组或注入组通道。
如果用户同时使能了连续模式,那么在所有选中的通道转换完后,立即开始下一次转换,但只能用于规则组当规则组通道工作在扫描模式下时,必须使用DMA进行传输

下图是规则组和注入组工作在扫描转换模式下的时序图。

在这里插入图片描述

下图是规则组工作在扫描转换模式,且开启了连续转换的时序图。

在这里插入图片描述

间断转换模式

该模式能够运行在规则组和注入组
对于规则组,在该模式下,用户可以对已选中的通道以最多n个(n <= 8)为一组依次进行采样,每组间的间隔由触发事件决定,当所有选中通道被转换完成后,EOC位置1。
下面的这个时序图中,一共有8个通道被选中,然后每3个通道为一组进行转换。

在这里插入图片描述

对于注入组,在该模式下,用户可以对已选中的通道以最多1个为一组依次进行采样,每组间的间隔由触发事件决定,当所有的选中通道被转换完成后,EOIC位置1。
下面的这个时序图中,一个有3个通道被选中。

在这里插入图片描述

规则组和注入组不能同时工作在间断模式,同一时刻只能有一组被设置成间断模式

注入通道管理

自动注入

使能自动注入后,在规则组通道转换结束之后,注入组通道被自动转换。规则组转换结束,EOC置1,当注入组转换结束后,EOC再次置1,EOIC置1。
不能同时使用自动注入和间断模式
下面的这个时序图,规则组开启了连续转换,那么在注入组转换结束后又继续这一过程。

在这里插入图片描述

触发注入

在该模式下,在规则组通道转换期间如果软件触发或者外部触发发生,则启动触发注入转换。这种情况下,ADC 取消当前转换,注入通道序列进行转换。注入通道组转换结束后,规则组转
换从上次被取消的转换处重新开始。每次注入组转换结束EOC和EOIC置1;规则组转换结束,EOC置1。

在这里插入图片描述

同步模式

在有两个或者两个以上的ADC模块的产品中,可以使用ADC同步模式。同步模式就类似于双核的CPU,能大大提高ADC的工作效率。在同步模式下,ADC0为主机,ADC1为从机,ADC0内部有一条信号线连接至ADC1的外部触发通道上,从而实现同步工作。所以在使用同步模式时主从ADC的外部触发都必须使能

下面是ADC同步模式的结构框图。

在这里插入图片描述

规则并行模式

在此模式下,两个ADC分别设置了不同的转换通道;开启转换后,两个ADC同时转换各自的通道;转换结束后,EOC位置1。

在这里插入图片描述
要注意的是,不要在两路ADC上转换相同的通道,并且ADC0和ADC1并行采样的两个通道的需要设置为准确的相同采样时间

注入并行模式

和规则并行一样,只不过换成了注入组而已,不再赘述。

在这里插入图片描述

快速交叉模式

此模式应用于规则通道组(通常一个通道),当触发产生时,ADC1立刻启动,而ADC0在7个ADC时钟周期后启动,每次该通道转换结束,对应的ADC都会将EOC置1。如果ADC0和ADC1开启了连续模式,所选的规则通道组在两个ADC中将被不停的转换。
使用快速交叉模式可以很好地加快单个通道的转换频率,突破单ADC下的采样速率上限。

在这里插入图片描述

慢速交叉模式

此模式应用于规则通道组(通常一个通道),当触发产生时,ADC1立刻启动,而ADC0在14 个ADC时钟周期后启动,在ADC0启动后的14个时钟周期,ADC1再次启动,每次该通道转换结束,对应的ADC都会将EOC置1。如果ADC0和ADC1开启了连续模式,所选的规则通道组在两个ADC中将被不停的转换。

在这里插入图片描述

交替触发模式

此模式应用于注入通道组,当第一次触发发生,ADC0所有的注入通道被转换,当第二次触发发生,ADC1所有的注入通道被转换,以此交替进行;每次对应ADC所有的注入通道转换完成,EOIC位置1。

在这里插入图片描述

规则并行和注入并行组合模式

注入并行通道组的转换可以中断规则并行通道组的转换,具体可以参考上面触发注入的时序图,只不过是这里变成了两个通道了而已。

规则并行和交替触发组合模式

注入通道组的交替触发转换的启动可以中断规则通道组的并行转换。当注入通道事件发生时,注入交替转换立刻启动。如果规则转换此刻正在运行,为保证注入通道组转换启动后的同步,规则通道组在两路 ADC 的转换停止。当注入通道组转换完成后,规则通道组的转换同步启动。如果在一个注入转换期间,另一个注入触发出现,那么后面的这个触发将会被忽略

在这里插入图片描述

注入并行和交叉组合模式

通过注入转换可以中断交叉转换,当注入触发发生时,交叉变换被中断,注入转换启动。在注
入转换完成后,交叉转换恢复。

在这里插入图片描述

例程

在这个例程中我们使用编写一个PS2摇杆的驱动,这个摇杆模块十分常用,在大大小小的项目中都能见到它的身影。

在这里插入图片描述

这个摇杆有两个自由度,因此要转换两个通道的ADC值,同时摇杆有一个可以下压的按键,但关于按键的驱动在这里就不再赘述了。
因为要转换两个通道的ADC值,因此我使用了ADC的同步模式,使两个ADC工作在规则同步模式,主从ADC分别转换一个通道;同时我使用了DMA传输,每次ADC转换结束,DMA就会立即传输结果;同时还使用了定时器,通过ADC的外部触发功能,定时触发ADC开启转换。
x轴的数据通过PA1管脚转换,y轴的数据通过PA2管脚转换,按键的数据通过PA3管脚接收。VCC一般模块上会标着接5V,但并不是很严谨,VCC接5V还是3.3V是要根据开发板中Vref的电压决定的,需要看开发板的原理图;但其实就算接错了也是能输出数据的,只不过数据会有偏移而已。

adc.c文件

#include "adc.h"

static __IO uint32_t adc_raw = 0;

void JOYSTICK_Init(void)
{
	/* 初始化GPIO */
	rcu_periph_clock_enable(XAXIS_RCU);
    rcu_periph_clock_enable(YAXIS_RCU);
    rcu_periph_clock_enable(KEY_RCU);
	
	gpio_init(XAXIS_GPIO_PORT, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, XAXIS_GPIO_PIN);
	gpio_init(YAXIS_GPIO_PORT, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, YAXIS_GPIO_PIN);
    gpio_init(KEY_GPIO_PORT, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, KEY_GPIO_PIN);
	
	/* 初始化TIM */
    timer_parameter_struct tim_conf = {0};
	
	rcu_periph_clock_enable(RCU_TIMER2);
	
    tim_conf.prescaler = (108 - 1);  // 预分频,即分频后的时钟频率为108Mhz / 108 = 1MHz
    tim_conf.alignedmode = TIMER_COUNTER_EDGE;  // 边沿对齐模式
    tim_conf.counterdirection = TIMER_COUNTER_UP;  // 向上计数
    tim_conf.period = (10000 - 1);  // 周期,100000 / 1MHz = 10ms
    timer_init(TIMER2, &tim_conf);
    
    timer_master_output_trigger_source_select(TIMER2, TIMER_TRI_OUT_SRC_UPDATE);  // 选择更新事件为外部触发条件
    	
	/* 初始化DMA */
    dma_parameter_struct dma_conf = {0};
	rcu_periph_clock_enable(RCU_DMA0);
    
    dma_deinit(DMA0, DMA_CH0);  // 复位DMA0
	
    dma_conf.periph_addr = (uint32_t)(&ADC_RDATA(ADC0));  // 外设基地址
    dma_conf.periph_inc = DMA_PERIPH_INCREASE_DISABLE;  // 外设地址自增关闭
    dma_conf.memory_addr = (uint32_t)(&adc_raw);  // 内存基地址
    dma_conf.memory_inc = DMA_MEMORY_INCREASE_DISABLE;  // 内存地址自增关闭
    dma_conf.periph_width = DMA_PERIPHERAL_WIDTH_32BIT;  // 32位外设数据
    dma_conf.memory_width = DMA_MEMORY_WIDTH_32BIT;  // 32位内存地址
    dma_conf.direction = DMA_PERIPHERAL_TO_MEMORY;  // 外设到内存
    dma_conf.number = 1;  // 1个数据
    dma_conf.priority = DMA_PRIORITY_HIGH;  // 优先级高
    dma_init(DMA0, DMA_CH0, &dma_conf);
    dma_circulation_enable(DMA0, DMA_CH0);  // 使能循环模式
	
    dma_channel_enable(DMA0, DMA_CH0);  // 使能DMA0通道0
	
	/* 初始化ADC */
	rcu_periph_clock_enable(RCU_ADC0);
	rcu_periph_clock_enable(RCU_ADC1);
	rcu_adc_clock_config(RCU_CKADC_CKAPB2_DIV8);  // ADC 8分频,即采样频率为108 / 8 = 13MHz

	/* 复位ADC */
    adc_deinit(ADC0);
    adc_deinit(ADC1);
    /* 规则并行模式 */
    adc_mode_config(ADC_DAUL_REGULAL_PARALLEL); 
    /* 使能扫描模式 */
    adc_special_function_config(ADC0, ADC_SCAN_MODE, ENABLE); 
    adc_special_function_config(ADC1, ADC_SCAN_MODE, ENABLE);   
    /* 数据右对齐,即LSB */
    adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT);
    adc_data_alignment_config(ADC1, ADC_DATAALIGN_RIGHT);  
    /* ADC0和ADC1分别处理一个通道 */
    adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL, 1);
    adc_channel_length_config(ADC1, ADC_REGULAR_CHANNEL, 1);
    /* 定义ADC0和ADC1的规则通道,采样时间为55.5个周期,即转换时间为(1 / 13MHz) * (55.5 + 12.5) ≈ 5us */
    adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_1, ADC_SAMPLETIME_55POINT5);
    adc_regular_channel_config(ADC1, 0, ADC_CHANNEL_2, ADC_SAMPLETIME_55POINT5);
    /* 计时器2外部触发,因为使能了同步模式,所以只需要设置ADC0的外部触发 */
    adc_external_trigger_source_config(ADC0, ADC_REGULAR_CHANNEL, ADC0_1_EXTTRIG_REGULAR_T2_TRGO);
    adc_external_trigger_source_config(ADC1, ADC_REGULAR_CHANNEL, ADC0_1_2_EXTTRIG_REGULAR_NONE);
    /* 使能ADC外部触发 */
    adc_external_trigger_config(ADC0, ADC_REGULAR_CHANNEL, ENABLE);
    adc_external_trigger_config(ADC1, ADC_REGULAR_CHANNEL, ENABLE);    
    /* 使能ADC0 */
    adc_enable(ADC0);  
    delay_ms(1);
    /* 使能ADC0校准 */
    adc_calibration_enable(ADC0);
    /* 使能ADC1 */    
    adc_enable(ADC1);    
    delay_ms(1);
    /* 使能ADC1校准 */    
    adc_calibration_enable(ADC1);    
    /* 使能ADC DMA传输 */
    adc_dma_mode_enable(ADC0);
	
	timer_enable(TIMER2);  // 使能定时器2
}

uint8_t JOYSTICK_GetKeyState(void)
{
	if (gpio_input_bit_get(KEY_GPIO_PORT, KEY_GPIO_PIN) == RESET)  // 检测按键按下
	{
		while(gpio_input_bit_get(KEY_GPIO_PORT, KEY_GPIO_PIN) == RESET);  // 等待按键释放
		return 1;
	}
	return 0;
}

void JOYSTICK_GetData(joystick_data_t* dat)
{
	dat->xaxis = (uint16_t)(adc_raw & 0xFFFF);
	dat->yaxis = (uint16_t)(adc_raw >> 16);
	dat->key = JOYSTICK_GetKeyState();
}

adc.h文件

#ifndef _ADC_H
#define _ADC_H

#include "gd32f10x.h"
#include "systick.h"

#define XAXIS_RCU RCU_GPIOA
#define XAXIS_GPIO_PORT GPIOA
#define XAXIS_GPIO_PIN GPIO_PIN_1
#define YAXIS_RCU RCU_GPIOA
#define YAXIS_GPIO_PORT GPIOA
#define YAXIS_GPIO_PIN GPIO_PIN_2
#define KEY_RCU RCU_GPIOA
#define KEY_GPIO_PORT GPIOA
#define KEY_GPIO_PIN GPIO_PIN_3

typedef struct
{
    uint16_t xaxis;
    uint16_t yaxis;
    uint8_t key;
} joystick_data_t;
extern joystick_data_t joystick_data;

void JOYSTICK_Init(void);
uint8_t JOYSTICK_GetKeyState(void);
void JOYSTICK_GetData(joystick_data_t* dat);

#endif

main.c文件

#include "gd32f10x.h"
#include "main.h"
#include "systick.h"
#include "usart.h"
#include "adc.h"
#include <stdio.h>
#include <string.h>

int main(void)
{
    joystick_data_t joystick_data;
    
    systick_config();
    USART_Config();
    JOYSTICK_Init();

    while(1)
    {
        JOYSTICK_GetData(&joystick_data);
        printf("xaxis: %d, yaxis: %d, key: %d\n", joystick_data.xaxis, joystick_data.yaxis, joystick_data.key);
        delay_ms(1000);        
    }
}

在这里插入图片描述