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

Sử dụng PDMA + ADC trên vi điều khiển Holtek HT32F52352 để thu thập nhiều kênh tín hiệu tương tự và hiển thị lên màn hình OLED 0,96 inch.

Gần đây mình tham gia cuộc thi “Holtek Cup”, tranh thủ viết vài bài hướng dẫn nhỏ để mọi người tiện theo dõi.

Hướng dẫn cài đặt và cấu hình môi trường phát triển cho vi điều khiển Holtek HT32: https://blog.zeruns.com/archives/709.html

Nhóm thảo luận Điện tử/Vi điều khiển: 2169025065

Ảnh thực tế

Giới thiệu ADC và PDMA

ADC – Bộ chuyển đổi tương tự–số, HT32F52352 tích hợp ADC 12 bit dạng SAR với 12 kênh đầu vào tương tự bên ngoài và 2 kênh nội (VDD & GND), tốc độ lấy mẫu tối đa 1 Msps.

PDMA – Bộ điều khiển truy cập bộ nhớ trực tiếp từ ngoại vi, HT32F52352 có 6 kênh PDMA, chỉ kênh CH0 hỗ trợ ADC.

Thanh ghi dữ liệu chuyển đổi ADC_DR 32 bit nhưng chỉ 16 bit thấp có giá trị; nếu PDMA đặt độ rộng dữ liệu 16 bit thì 16 bit cao “rác” sẽ bị chuyển sang ô nhớ kế tiếp. Vì vậy PDMA phải đặt độ rộng 32 bit, sau đó xử lý dữ liệu thì AND với 0x0000FFFF để lọc bỏ 16 bit cao. Ai có cách hay hơn cứ chia sẻ nhé!

Link mua đồ dùng:

Bo mạch ESK32: https://s.click.taobao.com/ndAFyKu

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

Dây Dupont: https://s.click.taobao.com/QVTFyKu

Màn hình OLED 0,96 inch: https://s.click.taobao.com/XLU9ZJu

Mã nguồn

Tải toàn bộ project: https://url.zeruns.com/HT32_PDMA_ADC

Dưới đây là các file chính:

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();  // Khởi tạo GPIO
  BFTM0_Configuration(); // Khởi tạo BFTM0
  GPTM0_Configuration(); // Khởi tạo GPTM0
  GPTM1_Configuration(); // Khởi tạo GPTM1
  WDT_Configuration();   // Khởi tạo Watchdog
  OLED_Init();           // Khởi tạo OLED
  ADC_Configuration();   // Khởi tạo ADC

  OLED_ShowString(1, 1, "AD0:"); // Hiển thị "AD0:" dòng 1 cột 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)) // Nếu watchdog clock được bật
      WDT_Restart();                 // Reset watchdog

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

    OLED_ShowNum(1, 5, AD_Value[0] & 0x0000FFFF, 4); // Hiển thị giá trị ADC
    float Voltage0 = (AD_Value[0] & 0x0000FFFF) / 4096.0 * 3.3; // Chuyển sang điện áp
    OLED_ShowNum(1, 10, (uint8_t)Voltage0, 1);                  // Phần nguyên
    OLED_ShowNum(1, 12, (uint16_t)(Voltage0 * 1000) % 1000, 3); // Phần thập phân

    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 thấp
    GPIO_WriteOutBits(HT_GPIOC, GPIO_PIN_15, SET);   // PC15 cao
    Delay_ms(100);
    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}};
    CKCUClock.Bit.PA   = 1; // Mở clock GPIOA
    CKCUClock.Bit.ADC  = 1; // Mở clock ADC
    CKCUClock.Bit.AFIO = 1; // Mở clock AFIO
    CKCUClock.Bit.PDMA = 1; // Mở clock PDMA
    CKCU_PeripClockConfig(CKCUClock, ENABLE);

    ADC_Reset(HT_ADC);
    CKCU_SetADCPrescaler(CKCU_ADCPRE_DIV4); // Chia 4 cho ADC clock

    AFIO_GPxConfig(GPIO_PA, AFIO_PIN_0, AFIO_FUN_ADC); // Cấu hình PA0-PA3 làm 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); // Kênh 0 ở vị trí 0
    ADC_RegularChannelConfig(HT_ADC, ADC_CH_1, 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); // 4 kênh, liên tục
    ADC_RegularTrigConfig(HT_ADC, ADC_TRIG_SOFTWARE);      // Trigger phần mềm
    ADC_SamplingTimeConfig(HT_ADC, 16);                    // Thời gian lấy mẫu

    PDMACH_InitTypeDef PDMACH_InitStructure;
    PDMACH_InitStructure.PDMACH_SrcAddr  = (u32)(&HT_ADC->DR); // Nguồn: ADC_DR
    PDMACH_InitStructure.PDMACH_DstAddr  = (u32)AD_Value;      // Đích: mảng AD_Value
    PDMACH_InitStructure.PDMACH_AdrMod   = SRC_ADR_LIN_INC | DST_ADR_LIN_INC | AUTO_RELOAD;
    PDMACH_InitStructure.PDMACH_Priority = H_PRIO;
    PDMACH_InitStructure.PDMACH_BlkCnt   = 4; // 4 lần truyền
    PDMACH_InitStructure.PDMACH_BlkLen   = 1;
    PDMACH_InitStructure.PDMACH_DataSize = WIDTH_32BIT; // 32 bit
    PDMA_Config(PDMA_CH0, &PDMACH_InitStructure);

    PDMA_EnaCmd(PDMA_CH0, ENABLE);                          // Bật PDMA CH0
    ADC_PDMAConfig(HT_ADC, ADC_PDMA_REGULAR_CYCLE, ENABLE); // ADC kích PDMA
    ADC_Cmd(HT_ADC, ENABLE);                                // Bật ADC
    ADC_SoftwareStartConvCmd(HT_ADC, ENABLE);               // Trigger phần mềm
    PDMA_SwTrigCmd(PDMA_CH0, ENABLE);                       // Trigger 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"

/*Cấu hình chân*/
#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))

/*Khởi tạo chân*/
void OLED_I2C_Init(void)
{
    CKCU_PeripClockConfig_TypeDef CKCUClock = {{ 0 }};	//Định nghĩa cấu trúc, cấu hình xung nhịp
    CKCUClock.Bit.PB    = 1;							//Bật xung nhịp GPIOB
    CKCU_PeripClockConfig(CKCUClock, ENABLE);			//Kích hoạt xung nhịp ngoại vi

    GPIO_SetOutBits         (HT_GPIOB, OLED_SCL);		//Đặt IO xuất mức cao
    GPIO_DirectionConfig    (HT_GPIOB, OLED_SCL, GPIO_DIR_OUT);	//Đặt IO ở chế độ xuất
    GPIO_OpenDrainConfig    (HT_GPIOB, OLED_SCL, ENABLE);		//Đặt IO ở chế độ xuất mở
    GPIO_PullResistorConfig (HT_GPIOB, OLED_SCL, GPIO_PR_UP);	//Đặt IO ở chế độ xuất kéo lên
	//GPIO_DriveConfig		(HT_GPIOB, OLED_SCL,GPIO_DV_12MA);	//Đặt chế độ dòng xuất của IO

    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  I2C bắt đầu
  * @param  Không
  * @retval Không
  */
void OLED_I2C_Start(void)
{
	OLED_W_SDA(1);
	OLED_W_SCL(1);
	OLED_W_SDA(0);
	OLED_W_SCL(0);
}

/**
  * @brief  I2C dừng
  * @param  Không
  * @retval Không
  */
void OLED_I2C_Stop(void)
{
	OLED_W_SDA(0);
	OLED_W_SCL(1);
	OLED_W_SDA(1);
}

/**
  * @brief  I2C gửi một byte
  * @param  Byte byte cần gửi
  * @retval Không
  */
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);	//Một xung nhịp thêm, không xử lý tín hiệu ACK
	OLED_W_SCL(0);
}

/**
  * @brief  OLED viết lệnh
  * @param  Command lệnh cần viết
  * @retval Không
  */
void OLED_WriteCommand(uint8_t Command)
{
	OLED_I2C_Start();
	OLED_I2C_SendByte(0x78);		//Địa chỉ thiết bị
	OLED_I2C_SendByte(0x00);		//Viết lệnh
	OLED_I2C_SendByte(Command); 
	OLED_I2C_Stop();
}

/**
  * @brief  OLED viết dữ liệu
  * @param  Data dữ liệu cần viết
  * @retval Không
  */
void OLED_WriteData(uint8_t Data)
{
	OLED_I2C_Start();
	OLED_I2C_SendByte(0x78);		//Địa chỉ thiết bị
	OLED_I2C_SendByte(0x40);		//Viết dữ liệu
	OLED_I2C_SendByte(Data);
	OLED_I2C_Stop();
}

/**
  * @brief  OLED đặt vị trí con trỏ
  * @param  Y tọa độ hướng xuống tính từ góc trên trái, phạm vi: 0~7
  * @param  X tọa độ hướng phải tính từ góc trên trái, phạm vi: 0~127
  * @retval Không
  */
void OLED_SetCursor(uint8_t Y, uint8_t X)
{
	OLED_WriteCommand(0xB0 | Y);					//Đặt vị trí Y
	OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4));	//Đặt 4 bit thấp vị trí X
	OLED_WriteCommand(0x00 | (X & 0x0F));			//Đặt 4 bit cao vị trí X
}

/**
  * @brief  OLED xóa màn hình
  * @param  Không
  * @retval Không
  */
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 xóa một phần
  * @param  Line vị trí hàng, phạm vi: 1~4
  * @param  start vị trí cột bắt đầu, phạm vi: 1~16
  * @param  end vị trí cột kết thúc, phạm vi: 1~16
  * @retval Không
  */
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);		//Đặt con trỏ ở nửa trên
		for (i = 0; i < 8; i++)
		{
			OLED_WriteData(0x00);			//Hiển thị nội dung nửa trên
		}
		OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8);	//Đặt con trỏ ở nửa dưới
		for (i = 0; i < 8; i++)
		{
			OLED_WriteData(0x00);		//Hiển thị nội dung nửa dưới
		}
	}
}

/**
  * @brief  OLED hiển thị một ký tự
  * @param  Line vị trí hàng, phạm vi: 1~4
  * @param  Column vị trí cột, phạm vi: 1~16
  * @param  Char ký tự cần hiển thị, phạm vi: ký tự ASCII có thể nhìn thấy
  * @retval Không
  */
void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char)
{     	
	uint8_t i;
	OLED_SetCursor((Line - 1) * 2, (Column - 1) * 8);		//Đặt con trỏ ở nửa trên
	for (i = 0; i < 8; i++)
	{
		OLED_WriteData(OLED_F8x16[Char - ' '][i]);			//Hiển thị nội dung nửa trên
	}
	OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8);	//Đặt con trỏ ở nửa dưới
	for (i = 0; i < 8; i++)
	{
		OLED_WriteData(OLED_F8x16[Char - ' '][i + 8]);		//Hiển thị nội dung nửa dưới
	}
}

/**
  * @brief  OLED hiển thị chuỗi
  * @param  Line vị trí hàng bắt đầu, phạm vi: 1~4
  * @param  Column vị trí cột bắt đầu, phạm vi: 1~16
  * @param  String chuỗi cần hiển thị, phạm vi: ký tự ASCII có thể nhìn thấy
  * @retval Không
  */
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 hàm lũy thừa
  * @retval Trả về X mũ Y
  */
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;
	while (Y--)
	{
		Result *= X;
	}
	return Result;
}

/**
  * @brief  OLED hiển thị số (thập phân, số dương)
  * @param  Line vị trí hàng bắt đầu, phạm vi: 1~4
  * @param  Column vị trí cột bắt đầu, phạm vi: 1~16
  * @param  Number số cần hiển thị, phạm vi: 0~4294967295
  * @param  Length độ dài số cần hiển thị, phạm vi: 1~10
  * @retval Không
  */
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 hiển thị số (thập phân, có dấu)
  * @param  Line vị trí hàng bắt đầu, phạm vi: 1~4
  * @param  Column vị trí cột bắt đầu, phạm vi: 1~16
  * @param  Number số cần hiển thị, phạm vi: -2147483648~2147483647
  * @param  Length độ dài số cần hiển thị, phạm vi: 1~10
  * @retval Không
  */
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 hiển thị số (thập lục phân, dương)
  * @param  Line vị trí hàng bắt đầu, phạm vi: 1~4
  * @param  Column vị trí cột bắt đầu, phạm vi: 1~16
  * @param  Number số cần hiển thị, phạm vi: 0~0xFFFFFFFF
  * @param  Length độ dài số cần hiển thị, phạm vi: 1~8
  * @retval Không
  */
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 hiển thị số (nhị phân, dương)
  * @param  Line vị trí hàng bắt đầu, phạm vi: 1~4
  * @param  Column vị trí cột bắt đầu, phạm vi: 1~16
  * @param  Number số cần hiển thị, phạm vi: 0~1111 1111 1111 1111
  * @param  Length độ dài số cần hiển thị, phạm vi: 1~16
  * @retval Không
  */
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 khởi tạo
  * @param  Không
  * @retval Không
  */
void OLED_Init(void)
{
	uint32_t i, j;
	
	for (i = 0; i < 1000; i++)			//Trễ khi cấp nguồn
	{
		for (j = 0; j < 1000; j++);
	}
	
	OLED_I2C_Init();			//Khởi tạo cổng
	
	OLED_WriteCommand(0xAE);	//Tắt hiển thị
	
	OLED_WriteCommand(0xD5);	//Đặt tỷ số chia xung nhịp/tần số dao động
	OLED_WriteCommand(0x80);
	
	OLED_WriteCommand(0xA8);	//Đặt tỷ lệ đa hợp
	OLED_WriteCommand(0x3F);
	
	OLED_WriteCommand(0xD3);	//Đặt độ lệch hiển thị
	OLED_WriteCommand(0x00);
	
	OLED_WriteCommand(0x40);	//Đặt hàng bắt đầu hiển thị
	
	OLED_WriteCommand(0xA1);	//Đặt hướng trái phải, 0xA1 bình thường 0xA0 đảo trái phải
	
	OLED_WriteCommand(0xC8);	//Đặt hướng trên dưới, 0xC8 bình thường 0xC0 đảo trên dưới

	OLED_WriteCommand(0xDA);	//Đặt cấu hình phần cứng chân COM
	OLED_WriteCommand(0x12);
	
	OLED_WriteCommand(0x81);	//Đặt điều khiển độ tương phản
	OLED_WriteCommand(0xCF);

	OLED_WriteCommand(0xD9);	//Đặt chu kỳ sạc trước
	OLED_WriteCommand(0xF1);

	OLED_WriteCommand(0xDB);	//Đặt mức hủy chọn VCOMH
	OLED_WriteCommand(0x30);

	OLED_WriteCommand(0xA4);	//Đặt bật/tắt toàn bộ hiển thị

	OLED_WriteCommand(0xA6);	//Đặt hiển thị bình thường/đảo

	OLED_WriteCommand(0x8D);	//Đặt bơm nạp
	OLED_WriteCommand(0x14);

	OLED_WriteCommand(0xAF);	//Bật hiển thị
		
	OLED_Clear();				//Xóa màn hình 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

Đề xuất đọc- Gợi ý VPS/máy chủ đám mây có hiệu năng cao và giá rẻ: https://blog.vpszj.cn/archives/41.html