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
- Cómo crear un blog personal: https://blog.zeruns.com/archives/218.html
- Tutorial para crear un servidor de Minecraft: https://blog.zeruns.com/tag/mc/
- Lectura de sensores de temperatura y humedad SHT3x con STM32: https://blog.zeruns.com/archives/700.html
- Usar VSCode en lugar de Keil para el desarrollo con STM32 y microcontroladores 51: https://blog.zeruns.com/archives/690.html
- Implementar medición de distancia por ultrasonido con STM32 y módulo HC-SR04: https://blog.zeruns.com/archives/680.html




