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
- Cách dựng blog cá nhân: https://blog.zeruns.com/archives/218.html
- Hướng dẫn dựng máy chủ Minecraft: https://blog.zeruns.com/tag/mc/
- STM32 đọc cảm biến nhiệt độ/độ ẩm dòng SHT3x: https://blog.zeruns.com/archives/700.html
- Dùng VSCode thay Keil để phát triển vi điều khiển STM32 và 51: https://blog.zeruns.com/archives/690.html
- Đo khoảng cách siêu âm với STM32 và mô-đun HC-SR04: https://blog.zeruns.com/archives/680.html




