合泰HT32单片机使用PDMA+ADC采集多路模拟值并显示在OLED屏上

Microcontrolador Holtek HT32F52352: uso de PDMA+ADC para adquirir múltiples señales analógicas y mostrarlas en pantalla OLED de 0,96 pulgadas.

Recientemente participé en la competencia Holtek Cup y, de paso, escribí este pequeño tutorial para ayudar a todos.

Tutorial de instalación y configuración del entorno de desarrollo para microcontroladores Holtek HT32: https://blog.zeruns.com/archives/709.html

Grupo de discusión técnica de electrónica/microcontroladores: 2169025065

Imagen de resultado

Introducción a ADC y PDMA

ADC – Convertidor Analógico-Digital: el HT32F52352 incorpora un ADC SAR de 12 bits, 12 canales de entrada analógica externas, 2 canales internos medibles (VDD y GND) y una frecuencia máxima de muestreo de 1 Msps.

PDMA – Acceso Directo a Memoria desde Periférico: el HT32F52352 dispone de 6 canales PDMA; solo el canal CH0 es compatible con el ADC.

El registro de datos de conversión ADC_DR es de 32 bits, pero solo los 16 bits inferiores son válidos. Si PDMA se configura con anchura de 16 bits, los 16 bits superiores inválidos se copiarían en la siguiente posición. Por eso PDMA debe trabajar a 32 bits y, al procesar los datos, aplicar una máscara & 0x0000FFFF para descartar los bits altos. Si alguien tiene una mejor solución, bienvenido sea al debate en los comentarios.

Enlaces de compra de los componentes necesarios:

Placa de desarrollo ESK32: https://s.click.taobao.com/ndAFyKu

DAPLINK: https://s.click.taobao.com/Lt4FyKu

Cables Dupont: https://s.click.taobao.com/QVTFyKu

Pantalla OLED 0,96": https://s.click.taobao.com/XLU9ZJu

Código fuente

Enlace de descarga del proyecto completo: https://url.zeruns.com/HT32_PDMA_ADC

A continuación se muestran los archivos principales:

main.c

#include "ht32.h"
#include "GPIO.h"
#include "BFTM0.h"
//#include "GPTM0.h"
//#include "GPTM1.h"
#include "delay.h"
#include "OLED.h"
#include "WDT.h"
#include "ADC.h"

int main(void)
{
  GPIO_Configuration();  // Inicializar GPIO
  BFTM0_Configuration(); // Inicializar temporizador BFTM0
  GPTM0_Configuration(); // Inicializar temporizador GPTM0
  GPTM1_Configuration(); // Inicializar temporizador GPTM1
  WDT_Configuration();   // Inicializar watchdog
  OLED_Init();           // Inicializar OLED
  ADC_Configuration();   // Inicializar ADC

  OLED_ShowString(1, 1, "AD0:"); // Mostrar cadena “AD0:” en fila 1, columna 1
  OLED_ShowString(2, 1, "AD1:");
  OLED_ShowString(3, 1, "AD2:");
  OLED_ShowString(4, 1, "AD3:");
  OLED_ShowString(1, 11, ".");OLED_ShowString(1, 15, "V");
  OLED_ShowString(2, 11, ".");OLED_ShowString(2, 15, "V");

  uint16_t count1 = 0;

  while (1)
  {
    if (HT_CKCU->APBCCR1 & (1 << 4)) // Si el reloj del watchdog está habilitado
      WDT_Restart();                 // Reiniciar contador del watchdog

    OLED_ShowNum(3, 12, count1, 5);
    OLED_ShowNum(4, 12, count2, 5);

    OLED_ShowNum(1, 5, AD_Value[0] & 0x0000FFFF, 4); // Mostrar valor ADC
    float Voltage0 = (AD_Value[0] & 0x0000FFFF) / 4096.0 * 3.3; // Convertir a voltaje
    OLED_ShowNum(1, 10, (uint8_t)Voltage0, 1);                  // Parte entera
    OLED_ShowNum(1, 12, (uint16_t)(Voltage0 * 1000) % 1000, 3); // Parte decimal

    OLED_ShowNum(2, 5, AD_Value[1] & 0x0000FFFF, 4);
    float Voltage1 = (AD_Value[1] & 0x0000FFFF) / 4096.0 * 3.3;
    OLED_ShowNum(2, 10, (uint8_t)Voltage1, 1);
    OLED_ShowNum(2, 12, (uint16_t)(Voltage1 * 1000) % 1000, 3);

    OLED_ShowNum(3, 5, AD_Value[2] & 0x0000FFFF, 4);
    OLED_ShowNum(4, 5, AD_Value[3] & 0x0000FFFF, 4);
    // https://blog.zeruns.com
    GPIO_WriteOutBits(HT_GPIOC, GPIO_PIN_14, RESET); // PC14 a nivel bajo
    GPIO_WriteOutBits(HT_GPIOC, GPIO_PIN_15, SET);   // PC15 a nivel alto
    Delay_ms(100);                                   // Retardo 100 ms
    GPIO_WriteOutBits(HT_GPIOC, GPIO_PIN_14, SET);
    GPIO_WriteOutBits(HT_GPIOC, GPIO_PIN_15, RESET);
    Delay_ms(100);
    count1++;
  }
}

ADC.c

#include "ADC.h"

uint32_t AD_Value[4];

void ADC_Configuration(void)
{
    CKCU_PeripClockConfig_TypeDef CKCUClock = {{0}}; // Estructura para configurar relojes
    CKCUClock.Bit.PA = 1;                       // Habilitar reloj GPIOA
    CKCUClock.Bit.ADC = 1;                      // Habilitar reloj ADC
    CKCUClock.Bit.AFIO = 1;                     // Habilitar reloj AFIO
    CKCUClock.Bit.PDMA = 1;                     // Habilitar reloj PDMA
    CKCU_PeripClockConfig(CKCUClock, ENABLE);   // Activar relojes periféricos

    ADC_Reset(HT_ADC);                          // Resetear ADC
    CKCU_SetADCPrescaler(CKCU_ADCPRE_DIV4);     // Prescaler del ADC

    AFIO_GPxConfig(GPIO_PA, AFIO_PIN_0, AFIO_FUN_ADC); // Configurar PA0 como ADC
    AFIO_GPxConfig(GPIO_PA, AFIO_PIN_1, AFIO_FUN_ADC); 
    AFIO_GPxConfig(GPIO_PA, AFIO_PIN_2, AFIO_FUN_ADC); 
    AFIO_GPxConfig(GPIO_PA, AFIO_PIN_3, AFIO_FUN_ADC);
    // https://blog.zeruns.com
    ADC_RegularChannelConfig(HT_ADC, ADC_CH_0, 0); // Canal 0 en secuencia 0
    ADC_RegularChannelConfig(HT_ADC, ADC_CH_1, 1); // Canal 1 en secuencia 1
    ADC_RegularChannelConfig(HT_ADC, ADC_CH_2, 2);
    ADC_RegularChannelConfig(HT_ADC, ADC_CH_3, 3);
    // https://blog.vpszj.cn
    ADC_RegularGroupConfig(HT_ADC, CONTINUOUS_MODE, 4, 1); // Modo continuo, longitud 4
    ADC_RegularTrigConfig(HT_ADC, ADC_TRIG_SOFTWARE);      // Disparo por software
    ADC_SamplingTimeConfig(HT_ADC, 16);                    // Tiempo de muestreo
    // ADC_IntConfig(HT_ADC, ADC_INT_CYCLE_EOC, ENABLE);   // Interrupción ADC
    // NVIC_EnableIRQ(ADC_IRQn);                            // Habilitar IRQ

    PDMACH_InitTypeDef PDMACH_InitStructure;                    // Configurar PDMA
    PDMACH_InitStructure.PDMACH_SrcAddr = (u32)(&HT_ADC->DR);   // Dirección origen: registro ADC_DR
    PDMACH_InitStructure.PDMACH_DstAddr = (u32)AD_Value;        // Dirección destino
    PDMACH_InitStructure.PDMACH_AdrMod = SRC_ADR_LIN_INC | DST_ADR_LIN_INC | AUTO_RELOAD;
    PDMACH_InitStructure.PDMACH_Priority = H_PRIO;              // Prioridad alta
    PDMACH_InitStructure.PDMACH_BlkCnt = 4;                     // 4 transferencias
    PDMACH_InitStructure.PDMACH_BlkLen = 1;
    PDMACH_InitStructure.PDMACH_DataSize = WIDTH_32BIT;         // 32 bits
    PDMA_Config(PDMA_CH0, &PDMACH_InitStructure);               // Inicializar PDMA0

    // PDMA_IntConfig(PDMA_CH0, (PDMA_INT_GE | PDMA_INT_TC), ENABLE); // Interrupción PDMA

    PDMA_EnaCmd(PDMA_CH0, ENABLE);                          // Habilitar PDMA0
    ADC_PDMAConfig(HT_ADC, ADC_PDMA_REGULAR_CYCLE, ENABLE); // Habilitar trigger PDMA del ADC
    ADC_Cmd(HT_ADC, ENABLE);                                // Habilitar ADC
    ADC_SoftwareStartConvCmd(HT_ADC, ENABLE);               // Disparar ADC por software
    PDMA_SwTrigCmd(PDMA_CH0, ENABLE);                       // Disparo software PDMA
}

ADC.h

#ifndef __ADC_H
#define __ADC_H 			   
#include "ht32.h"

extern uint32_t AD_Value[4];

void ADC_Configuration(void);

#endif
```**OLED.c**

```c++
#include "ht32.h"
#include "OLED_Font.h"

/*Configuración de pines*/
#define OLED_SCL GPIO_PIN_7
#define OLED_SDA GPIO_PIN_8
#define OLED_W_SCL(x)		GPIO_WriteOutBits(HT_GPIOB, OLED_SCL, (FlagStatus)(x))
#define OLED_W_SDA(x)		GPIO_WriteOutBits(HT_GPIOB, OLED_SDA, (FlagStatus)(x))

/*Inicialización de pines*/
void OLED_I2C_Init(void)
{
    CKCU_PeripClockConfig_TypeDef CKCUClock = {{ 0 }};	//Definir estructura, configurar reloj
    CKCUClock.Bit.PB    = 1;							//Activar reloj GPIOB
    CKCU_PeripClockConfig(CKCUClock, ENABLE);			//Habilitar reloj periférico

    GPIO_SetOutBits         (HT_GPIOB, OLED_SCL);		//Establecer pin de salida en alto
    GPIO_DirectionConfig    (HT_GPIOB, OLED_SCL, GPIO_DIR_OUT);	//Configurar pin como salida
    GPIO_OpenDrainConfig    (HT_GPIOB, OLED_SCL, ENABLE);		//Configurar pin como salida drenador abierto
    GPIO_PullResistorConfig (HT_GPIOB, OLED_SCL, GPIO_PR_UP);	//Configurar pin con pull-up
	//GPIO_DriveConfig		(HT_GPIOB, OLED_SCL,GPIO_DV_12MA);	//Configurar modo de corriente de salida del pin

    GPIO_SetOutBits         (HT_GPIOB, OLED_SDA);
    GPIO_DirectionConfig    (HT_GPIOB, OLED_SDA, GPIO_DIR_OUT);
    GPIO_OpenDrainConfig    (HT_GPIOB, OLED_SDA, ENABLE);
    GPIO_PullResistorConfig (HT_GPIOB, OLED_SDA, GPIO_PR_UP);
	//GPIO_DriveConfig		(HT_GPIOB, OLED_SDA,GPIO_DV_12MA);
	
	OLED_W_SCL(1);
	OLED_W_SDA(1);
}

/**
  * @brief  Inicio I2C
  * @param  Ninguno
  * @retval Ninguno
  */
void OLED_I2C_Start(void)
{
	OLED_W_SDA(1);
	OLED_W_SCL(1);
	OLED_W_SDA(0);
	OLED_W_SCL(0);
}

/**
  * @brief  Parada I2C
  * @param  Ninguno
  * @retval Ninguno
  */
void OLED_I2C_Stop(void)
{
	OLED_W_SDA(0);
	OLED_W_SCL(1);
	OLED_W_SDA(1);
}

/**
  * @brief  I2C envía un byte
  * @param  Byte byte a enviar
  * @retval Ninguno
  */
void OLED_I2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i++)
	{
		OLED_W_SDA(Byte & (0x80 >> i));
		OLED_W_SCL(1);
		OLED_W_SCL(0);
	}
	OLED_W_SCL(1);	//Reloj extra, no se procesa señal de respuesta
	OLED_W_SCL(0);
}

/**
  * @brief  OLED escribe comando
  * @param  Command comando a escribir
  * @retval Ninguno
  */
void OLED_WriteCommand(uint8_t Command)
{
	OLED_I2C_Start();
	OLED_I2C_SendByte(0x78);		//Dirección del esclavo
	OLED_I2C_SendByte(0x00);		//Escribir comando
	OLED_I2C_SendByte(Command); 
	OLED_I2C_Stop();
}

/**
  * @brief  OLED escribe dato
  * @param  Data dato a escribir
  * @retval Ninguno
  */
void OLED_WriteData(uint8_t Data)
{
	OLED_I2C_Start();
	OLED_I2C_SendByte(0x78);		//Dirección del esclavo
	OLED_I2C_SendByte(0x40);		//Escribir dato
	OLED_I2C_SendByte(Data);
	OLED_I2C_Stop();
}

/**
  * @brief  OLED establece posición del cursor
  * @param  Y coordenada hacia abajo desde la esquina superior izquierda, rango: 0~7
  * @param  X coordenada hacia la derecha desde la esquina superior izquierda, rango: 0~127
  * @retval Ninguno
  */
void OLED_SetCursor(uint8_t Y, uint8_t X)
{
	OLED_WriteCommand(0xB0 | Y);					//Establecer posición Y
	OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4));	//Establecer bits bajos de X
	OLED_WriteCommand(0x00 | (X & 0x0F));			//Establecer bits altos de X
}

/**
  * @brief  OLED limpia pantalla
  * @param  Ninguno
  * @retval Ninguno
  */
void OLED_Clear(void)
{  
	uint8_t i, j;
	for (j = 0; j < 8; j++)
	{
		OLED_SetCursor(j, 0);
		for(i = 0; i < 128; i++)
		{
			OLED_WriteData(0x00);
		}
	}
}

/**
  * @brief  OLED limpieza parcial
  * @param  Line posición de línea, rango: 1~4
  * @param  start posición inicial de columna, rango: 1~16
  * @param  end posición final de columna, rango: 1~16
  * @retval Ninguno
  */
void OLED_Clear_Part(uint8_t Line, uint8_t start, uint8_t end)
{  
	uint8_t i,Column;
	for(Column = start; Column <= end; Column++)
	{
		OLED_SetCursor((Line - 1) * 2, (Column - 1) * 8);		//Establecer cursor en parte superior
		for (i = 0; i < 8; i++)
		{
			OLED_WriteData(0x00);			//Mostrar contenido parte superior
		}
		OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8);	//Establecer cursor en parte inferior
		for (i = 0; i < 8; i++)
		{
			OLED_WriteData(0x00);		//Mostrar contenido parte inferior
		}
	}
}

/**
  * @brief  OLED muestra un carácter
  * @param  Line posición de línea, rango: 1~4
  * @param  Column posición de columna, rango: 1~16
  * @param  Char carácter a mostrar, rango: caracteres ASCII visibles
  * @retval Ninguno
  */
void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char)
{     	
	uint8_t i;
	OLED_SetCursor((Line - 1) * 2, (Column - 1) * 8);		//Establecer cursor en parte superior
	for (i = 0; i < 8; i++)
	{
		OLED_WriteData(OLED_F8x16[Char - ' '][i]);			//Mostrar contenido parte superior
	}
	OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8);	//Establecer cursor en parte inferior
	for (i = 0; i < 8; i++)
	{
		OLED_WriteData(OLED_F8x16[Char - ' '][i + 8]);		//Mostrar contenido parte inferior
	}
}

/**
  * @brief  OLED muestra cadena
  * @param  Line posición inicial de línea, rango: 1~4
  * @param  Column posición inicial de columna, rango: 1~16
  * @param  String cadena a mostrar, rango: caracteres ASCII visibles
  * @retval Ninguno
  */
void OLED_ShowString(uint8_t Line, uint8_t Column, char *String)
{
	uint8_t i;
	for (i = 0; String[i] != '\0'; i++)
	{
		OLED_ShowChar(Line, Column + i, String[i]);
	}
}

/**
  * @brief  OLED función potencia
  * @retval Retorna X elevado a Y
  */
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;
	while (Y--)
	{
		Result *= X;
	}
	return Result;
}

/**
  * @brief  OLED muestra número (decimal, positivo)
  * @param  Line posición inicial de línea, rango: 1~4
  * @param  Column posición inicial de columna, rango: 1~16
  * @param  Number número a mostrar, rango: 0~4294967295
  * @param  Length longitud del número a mostrar, rango: 1~10
  * @retval Ninguno
  */
void OLED_ShowNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
	uint8_t i;
	for (i = 0; i < Length; i++)							
	{
		OLED_ShowChar(Line, Column + i, Number / OLED_Pow(10, Length - i - 1) % 10 + '0');
	}
}

/**
  * @brief  OLED muestra número (decimal, con signo)
  * @param  Line posición inicial de línea, rango: 1~4
  * @param  Column posición inicial de columna, rango: 1~16
  * @param  Number número a mostrar, rango: -2147483648~2147483647
  * @param  Length longitud del número a mostrar, rango: 1~10
  * @retval Ninguno
  */
void OLED_ShowSignedNum(uint8_t Line, uint8_t Column, int32_t Number, uint8_t Length)
{
	uint8_t i;
	uint32_t Number1;
	if (Number >= 0)
	{
		OLED_ShowChar(Line, Column, '+');
		Number1 = Number;
	}
	else
	{
		OLED_ShowChar(Line, Column, '-');
		Number1 = -Number;
	}
	for (i = 0; i < Length; i++)							
	{
		OLED_ShowChar(Line, Column + i + 1, Number1 / OLED_Pow(10, Length - i - 1) % 10 + '0');
	}
}

/**
  * @brief  OLED muestra número (hexadecimal, positivo)
  * @param  Line posición inicial de línea, rango: 1~4
  * @param  Column posición inicial de columna, rango: 1~16
  * @param  Number número a mostrar, rango: 0~0xFFFFFFFF
  * @param  Length longitud del número a mostrar, rango: 1~8
  * @retval Ninguno
  */
void OLED_ShowHexNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
	uint8_t i, SingleNumber;
	for (i = 0; i < Length; i++)							
	{
		SingleNumber = Number / OLED_Pow(16, Length - i - 1) % 16;
		if (SingleNumber < 10)
		{
			OLED_ShowChar(Line, Column + i, SingleNumber + '0');
		}
		else
		{
			OLED_ShowChar(Line, Column + i, SingleNumber - 10 + 'A');
		}
	}
}

/**
  * @brief  OLED muestra número (binario, positivo)
  * @param  Line posición inicial de línea, rango: 1~4
  * @param  Column posición inicial de columna, rango: 1~16
  * @param  Number número a mostrar, rango: 0~1111 1111 1111 1111
  * @param  Length longitud del número a mostrar, rango: 1~16
  * @retval Ninguno
  */
void OLED_ShowBinNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
	uint8_t i;
	for (i = 0; i < Length; i++)							
	{
		OLED_ShowChar(Line, Column + i, Number / OLED_Pow(2, Length - i - 1) % 2 + '0');
	}
}

/**
  * @brief  OLED inicialización
  * @param  Ninguno
  * @retval Ninguno
  */
void OLED_Init(void)
{
	uint32_t i, j;
	
	for (i = 0; i < 1000; i++)			//Retardo al encender
	{
		for (j = 0; j < 1000; j++);
	}
	
	OLED_I2C_Init();			//Inicialización de puertos
	
	OLED_WriteCommand(0xAE);	//Apagar display
	
	OLED_WriteCommand(0xD5);	//Establecer divisor de reloj/frecuencia oscilador
	OLED_WriteCommand(0x80);
	
	OLED_WriteCommand(0xA8);	//Establecer ratio multiplex
	OLED_WriteCommand(0x3F);
	
	OLED_WriteCommand(0xD3);	//Establecer offset display
	OLED_WriteCommand(0x00);
	
	OLED_WriteCommand(0x40);	//Establecer línea de inicio display
	
	OLED_WriteCommand(0xA1);	//Establecer dirección izquierda-derecha, 0xA1 normal 0xA0 invertido
	
	OLED_WriteCommand(0xC8);	//Establecer dirección arriba-abajo, 0xC8 normal 0xC0 invertido

	OLED_WriteCommand(0xDA);	//Establecer configuración hardware pines COM
	OLED_WriteCommand(0x12);
	
	OLED_WriteCommand(0x81);	//Establecer control de contraste
	OLED_WriteCommand(0xCF);

	OLED_WriteCommand(0xD9);	//Establecer período de precarga
	OLED_WriteCommand(0xF1);

	OLED_WriteCommand(0xDB);	//Establecer nivel VCOMH deselección
	OLED_WriteCommand(0x30);

	OLED_WriteCommand(0xA4);	//Establecer display completo encendido/apagado

	OLED_WriteCommand(0xA6);	//Establecer display normal/invertido

	OLED_WriteCommand(0x8D);	//Establecer bomba de carga
	OLED_WriteCommand(0x14);

	OLED_WriteCommand(0xAF);	//Encender display
		
	OLED_Clear();				//Limpiar OLED
}

OLED.h

#ifndef __OLED_H
#define __OLED_H

void OLED_Init(void);
void OLED_Clear(void);
void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char);
void OLED_ShowString(uint8_t Line, uint8_t Column, char *String);
void OLED_ShowNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length);
void OLED_ShowSignedNum(uint8_t Line, uint8_t Column, int32_t Number, uint8_t Length);
void OLED_ShowHexNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length);
void OLED_ShowBinNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length);
void OLED_Clear_Part(uint8_t Line, uint8_t start, uint8_t end);

#endif

Recomendado para leer- Recomendaciones de VPS/Servidores en la nube de alta relación calidad-precio y económicos: https://blog.vpszj.cn/archives/41.html