30.STM32H743综合小项目
一、概述
单片机能够实现的功能非常广泛,主要包括:
-
信号采集与感知:通过ADC、比较器等读取模拟信号(如温度、电压),或通过GPIO读取数字信号(如开关状态)。
-
逻辑运算与决策:运行程序代码,进行逻辑判断、数学运算、数据处理和算法执行。
-
设备控制与驱动:通过GPIO、PWM、DAC等输出数字/模拟信号,直接控制LED、电机、继电器、开关电源等执行部件。
-
通信与组网:通过UART、I2C、SPI、CAN、USB、以太网及各类无线模块,实现设备间或与上位机的数据通信。
-
人机交互:驱动显示屏、管理键盘/触摸屏、控制指示灯和蜂鸣器等,实现用户输入与信息输出。
此综合小项目实现一个小demo,用到了单片机中的核心外设,MPU,FSMC,DMA,ADC,DAC,串口,TIM。把这些知识点吃透,我认为,单片机的使用就基本上没什么问题了。剩下的就是往应用走,需要上RTOS了。RTOS我开了RTTHREAD的专栏,后续,有时间就会不断更新。
此小项目主要这到这些知识点:
DAC + DMA +TIM--->正弦波
ADC+DMA+TIM (双缓存模式)采集
LCD(FSMC) 显示当前电压值
SDRAM(FSMC)做一个缓存区存储adc数据
USB(虚拟串口)接口将数据上传(因为硬件没有直连的串口了,懒的用串口助手了)
QT写一个串口工具,显示波形。
原理框图:
二、实现过程
1.工程搭建
我为了偷懒,也为了调试舒畅,还是用正点原子的H7开发板的rtthread原始工程,做例子。初学者如果看的头晕,慎读。
如下图所示,原始工程,这个样子。

添加源代码:

修改配置文件:

修改hal库下的配置文件:

编译:
scons --target=mdk5
然后在keil上编译。
2.源码设计
1.DAC部分
DAC代码:TIM+DAC+DMA,1KHZ的正弦波。
#include "dac.h"
DMA_HandleTypeDef g_dma_dac_handle; /* 定义用于DAC数据的DMA句柄 */
DAC_HandleTypeDef g_dac_dma_handle; /* 定义DAC(DMA输出)句柄 */
uint16_t g_dac_sin_buf[4096]; /* 发送数据缓冲区 */
/**
* @brief 生成正弦波序列
* @note 前提保证: maxval > samples/2
* @param maxval : 峰值(0 < maxval < 2048)
* @param samples: 采样点个数
* @retval 无
*/
void dac_creat_sin_buf(uint16_t maxval, uint16_t samples)
{
uint8_t i;
float inc = (2 * 3.1415926) / samples; /* 计算增量,(一个周期DAC_SIN_BUF个点)*/
float outdata = 0;
for (i = 0; i < samples; i++)
{
outdata = maxval * (1 + sin(inc * i)); /* 计算DAC_SIN_BUF个点周期内每个点的值,放大约maxval倍,并偏移到正值区域 */
if (outdata > 4095)
{
outdata = 4095; /* 溢出限定 */
}
g_dac_sin_buf[i] = outdata;
}
}
/**
* @brief 通过USMART设置正弦波输出参数,方便修改频率.
* @param arr : TIM7的自动重装载值
* @param psc : TIM7的分频系数
* @retval 无
*/
void dac_dma_sin_set(uint16_t arr, uint16_t psc)
{
dac_dma_wave_enable(100, arr, psc);
}
/**
* @brief DAC DMA输出正弦波初始化函数
* @note DAC的输入时钟来自APB1, 时钟频率=100Mhz=10ns
* DAC在输出buffer关闭的时候建立时间tSETTLING = 2us (H743数据手册)
* 也就是说DAC输出的最大速度为:500Khz, 这里10个点为一个周期, 即最大50Khz的正弦波
* @param 无
* @retval 无
*/
void dac_dma_wave_init(void)
{
DAC_ChannelConfTypeDef dac_ch_conf = {0};
GPIO_InitTypeDef gpio_init_struct;
__HAL_RCC_GPIOA_CLK_ENABLE(); /* DAC通道引脚口时钟使能 */
__HAL_RCC_DAC12_CLK_ENABLE(); /* DAC外设时钟使能 */
__HAL_RCC_DMA2_CLK_ENABLE(); /* DMA时钟使能 */
gpio_init_struct.Pin = GPIO_PIN_4; /* PA4 */
gpio_init_struct.Mode = GPIO_MODE_ANALOG; /* 模拟 */
HAL_GPIO_Init(GPIOA, &gpio_init_struct); /* 初始化DAC引脚 */
g_dma_dac_handle.Instance = DMA2_Stream6; /* 使用的DMA2 Stream6 */
g_dma_dac_handle.Init.Request = DMA_REQUEST_DAC1_CH1; /* DAC触发DMA传输 */
g_dma_dac_handle.Init.Direction = DMA_MEMORY_TO_PERIPH; /* 存储器到外设模式 */
g_dma_dac_handle.Init.PeriphInc = DMA_PINC_DISABLE; /* 外设地址禁止自增 */
g_dma_dac_handle.Init.MemInc = DMA_MINC_ENABLE; /* 存储器地址自增 */
g_dma_dac_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; /* 外设数据对齐为16位 */
g_dma_dac_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; /* 存储器数据对齐为16位 */
g_dma_dac_handle.Init.Mode = DMA_CIRCULAR; /* 循环模式 */
g_dma_dac_handle.Init.Priority = DMA_PRIORITY_MEDIUM; /* 中等优先级 */
g_dma_dac_handle.Init.FIFOMode = DMA_FIFOMODE_DISABLE; /* 不使用FIFO */
HAL_DMA_Init(&g_dma_dac_handle); /* 初始化DMA */
__HAL_LINKDMA(&g_dac_dma_handle, DMA_Handle1, g_dma_dac_handle); /* DMA句柄链接到DAC句柄上 */
g_dac_dma_handle.Instance = DAC1; /* 选择第一个DAC */
HAL_DAC_Init(&g_dac_dma_handle); /* DAC初始化 */
dac_ch_conf.DAC_SampleAndHold = DAC_SAMPLEANDHOLD_DISABLE; /* 关闭采样保持模式,这个模式可以降低功耗 */
dac_ch_conf.DAC_Trigger = DAC_TRIGGER_T7_TRGO; /* 用定时器7触发 */
dac_ch_conf.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE; /* 使能输出缓存 */
dac_ch_conf.DAC_ConnectOnChipPeripheral = DAC_CHIPCONNECT_DISABLE; /* 不将DAC连接到片内外设 */
dac_ch_conf.DAC_UserTrimming = DAC_TRIMMING_FACTORY; /* 使用出厂校准 */
HAL_DAC_ConfigChannel(&g_dac_dma_handle, &dac_ch_conf, DAC_CHANNEL_1); /* DAC通道输出设置 */
}
/**
* @brief DAC DMA使能正弦波输出
* @note TIM7的输入时钟频率(f)来自APB1, f = 100M * 2 = 200Mhz.
* DAC触发频率 ftrgo = f / ((psc + 1) * (arr + 1))
* 正弦波频率 = ftrgo / ndtr;
*
* @param ndtr : DMA通道单次传输数据个数
* @param arr : TIM7的自动重装载值
* @param psc : TIM7的分频系数
* @retval 无
*/
void dac_dma_wave_enable(uint16_t ndtr, uint16_t arr, uint16_t psc)
{
TIM_HandleTypeDef tim7_handle = {0};
TIM_MasterConfigTypeDef master_config = {0};
__HAL_RCC_TIM7_CLK_ENABLE(); /* TIM7时钟使能 */
tim7_handle.Instance = TIM7; /* 选择定时器7 */
tim7_handle.Init.Prescaler = psc; /* 分频系数 */
tim7_handle.Init.CounterMode = TIM_COUNTERMODE_UP; /* 递增计数器 */
tim7_handle.Init.Period = arr; /* 重装载值 */
tim7_handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; /* 自动重装 */
HAL_TIM_Base_Init(&tim7_handle); /* 初始化定时器7 */
master_config.MasterOutputTrigger = TIM_TRGO_UPDATE; /* 定时器更新事件产生触发 */
master_config.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
HAL_TIMEx_MasterConfigSynchronization(&tim7_handle, &master_config); /* 设置TIM7 TRGO */
HAL_TIM_Base_Start(&tim7_handle); /* 使能定时器7 */
HAL_DAC_Stop_DMA(&g_dac_dma_handle, DAC_CHANNEL_1); /* 先停止之前的传输 */
HAL_DAC_Start_DMA(&g_dac_dma_handle, DAC_CHANNEL_1, (uint32_t *)g_dac_sin_buf, ndtr, DAC_ALIGN_12B_R);
}
int device_dac()
{
dac_dma_wave_init(); /* 初始化DAC通道1 DMA波形输出 */
dac_creat_sin_buf(2048, 100);
dac_dma_wave_enable(100, 100 - 1, 20 - 1); /* 100KHz触发频率, 100个点,可以得到约1KHz的正弦波. */
}
INIT_DEVICE_EXPORT(device_dac);
2.ADC单次触发模式
adc部分,用了两种方法,一种是单次触发,单次触发这里留个问题:为什么重启ADC的DMA传输要先停掉ADC?
单次触发模式代码:
#include "adc.h"
#include "lcd.h"
//信号量
static struct rt_semaphore adc_dma_sem;
ADC_HandleTypeDef g_adc_dma_handle; /* 带DMA功能的ADC句柄 */
DMA_HandleTypeDef g_dma_adc_handle; /* 带ADC功能的DMA句柄 */
/**
* @brief ADC DMA读取 初始化函数
* @param par : 外设地址
* @param mar : 存储器地址
* @retval 无
*/
void adc_dma_init(uint32_t par, uint32_t mar)
{
GPIO_InitTypeDef gpio_init_struct;
ADC_ChannelConfTypeDef adc_ch_conf = {0};
ADC_ADCX_CHY_GPIO_CLK_ENABLE(); /* 开启ADC通道IO口时钟 */
ADC_ADCX_CHY_CLK_ENABLE(); /* 使能ADC1/2时钟 */
if ((uint32_t)ADC_ADCX_DMASx > (uint32_t)DMA2) /* 得到当前stream是来自DMA2还是DMA1 */
{
__HAL_RCC_DMA2_CLK_ENABLE(); /* DMA2时钟使能 */
}
else
{
__HAL_RCC_DMA1_CLK_ENABLE(); /* DMA1时钟使能 */
}
__HAL_RCC_ADC_CONFIG(RCC_ADCCLKSOURCE_CLKP); /* ADC外设时钟选择PLL */
/* 初始化ADC缓存通道对应的IO引脚 */
gpio_init_struct.Pin = ADC_ADCX_CHY_GPIO_PIN; /* ADC通道IO引脚 */
gpio_init_struct.Mode = GPIO_MODE_ANALOG; /* 模拟 */
HAL_GPIO_Init(ADC_ADCX_CHY_GPIO_PORT, &gpio_init_struct);
/* 初始化DMA */
g_dma_adc_handle.Instance = ADC_ADCX_DMASx; /* 选择DMA数据流 */
g_dma_adc_handle.Init.Request = ADC_ADCX_DMASx_REQ; /* 请求选择DMA_REQUEST_ADC1 */
g_dma_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY; /* DIR = 1,外设到存储器模式 */
g_dma_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE; /* 外设地址不变模式 */
g_dma_adc_handle.Init.MemInc = DMA_MINC_ENABLE; /* 存储器地址自增模式 */
g_dma_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; /* 外设数据对齐为16位 */
g_dma_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; /* 存储器数据对齐为16位 */
g_dma_adc_handle.Init.Mode = DMA_NORMAL; /* 正常模式 */
g_dma_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM; /* 中等优先级 */
g_dma_adc_handle.Init.FIFOMode = DMA_FIFOMODE_DISABLE; /* 禁止FIFO*/
HAL_DMA_Init(&g_dma_adc_handle); /* 初始化DMA */
__HAL_LINKDMA(&g_adc_dma_handle, DMA_Handle, g_dma_adc_handle); /* 将DMA句柄链接到ADC句柄上 */
/* 初始化ADC */
g_adc_dma_handle.Instance = ADC_ADCX;
g_adc_dma_handle.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV2; /* 异步时钟2分频,即adc_ker_ck=per_ck/2=32Mhz */
g_adc_dma_handle.Init.Resolution = ADC_RESOLUTION_16B; /* 16位模式 */
g_adc_dma_handle.Init.ScanConvMode = DISABLE; /* 非扫描模式 */
g_adc_dma_handle.Init.EOCSelection = ADC_EOC_SINGLE_CONV; /* 关闭EOC标志 */
g_adc_dma_handle.Init.LowPowerAutoWait = DISABLE; /* 自动低功耗关闭 */
g_adc_dma_handle.Init.ContinuousConvMode = ENABLE; /* 开启连续转换 */
g_adc_dma_handle.Init.NbrOfConversion = 1; /* 1个转换序列 */
g_adc_dma_handle.Init.DiscontinuousConvMode = DISABLE; /* 禁止不连续采样模式 */
g_adc_dma_handle.Init.NbrOfDiscConversion = 0; /* 不连续采样通道数为0 */
g_adc_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; /* 软件触发 */
g_adc_dma_handle.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; /* 使用软件触发 */
g_adc_dma_handle.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN; /* 过载的数据用最新的数据覆盖旧的转换数据 */
g_adc_dma_handle.Init.OversamplingMode = DISABLE; /* 过采样关闭 */
g_adc_dma_handle.Init.LeftBitShift = ADC_LEFTBITSHIFT_NONE; /* 选择ADC转换结果对齐方式 */
g_adc_dma_handle.Init.ConversionDataManagement = ADC_CONVERSIONDATA_DMA_ONESHOT; /* DMA单次传输ADC数据 */
HAL_ADC_Init(&g_adc_dma_handle); /* 初始化 */
HAL_ADCEx_Calibration_Start(&g_adc_dma_handle, ADC_CALIB_OFFSET, ADC_SINGLE_ENDED); /* ADC校准 */
/* 设置ADC通道 */
adc_ch_conf.Channel = ADC_ADCX_CHY; /* 选择使用的ADC通道 */
adc_ch_conf.Rank = ADC_REGULAR_RANK_1; /* 采样序列排第一个 */
adc_ch_conf.SamplingTime = ADC_SAMPLETIME_810CYCLES_5; /* 采样时间为810.5个时钟周期 */
adc_ch_conf.SingleDiff = ADC_SINGLE_ENDED ; /* 单端输入 */
adc_ch_conf.OffsetNumber = ADC_OFFSET_NONE; /* 无偏移 */
adc_ch_conf.Offset = 0; /* 无偏移补偿值,这里用不到 */
adc_ch_conf.OffsetRightShift = DISABLE; /* 禁止右移 */
adc_ch_conf.OffsetSignedSaturation = DISABLE; /* 禁止有符号饱和 */
HAL_ADC_ConfigChannel(&g_adc_dma_handle, &adc_ch_conf); /* 设置ADC通道 */
/* 设置DMA数据流中断优先级 */
HAL_NVIC_SetPriority(ADC_ADCX_DMASx_IRQn, 3, 3); /* 设置DMA中断优先级为3,子优先级为3 */
HAL_NVIC_EnableIRQ(ADC_ADCX_DMASx_IRQn); /* 使能DMA中断 */
HAL_DMA_Start_IT(&g_dma_adc_handle, par, mar, 0); /* 启动DMA,并开启中断 */
HAL_ADC_Start_DMA(&g_adc_dma_handle, &mar, 0); /* 开始DMA数据传输 */
}
/**
* @brief 使能一次ADC DMA传输
* @param ndtr: DMA传输的次数
* @retval 无
*/
void adc_dma_enable(uint16_t ndtr)
{
ADC_ADCX->CR &= ~(1 << 0); /* 关闭ADC */
ADC_ADCX_DMASx->CR &= ~(1 << 0); /* 关闭DMA传输 */
while (ADC_ADCX_DMASx->CR & 0X1); /* 确保DMA可以被设置 */
ADC_ADCX_DMASx->NDTR = ndtr; /* 要传输的数据个数 */
ADC_ADCX_DMASx->CR |= 1 << 0; /* 开启DMA传输 */
ADC_ADCX->CR |= 1 << 0; /* 启动ADC */
ADC_ADCX->CR |= 1 << 2; /* 启动常规转换通道 */
}
/**
* @brief ADC DMA缓存中断服务函数
* @param 无
* @retval 无
*/
void ADC_ADCX_DMASx_IRQHandler(void)
{
if (ADC_ADCX_DMASx_IS_TC()) /* 判断DMA数据传输完成 */
{
rt_sem_release(&adc_dma_sem);
ADC_ADCX_DMASx_CLR_TC(); /* 清除DMA1 数据流4 传输完成标志 */
}
}
#define ADC_DMA_BUF_SIZE 50 /* ADC DMA缓存 BUF大小 应该是ADC通道数的整数倍 */
uint16_t g_adc_dma_buf[ADC_DMA_BUF_SIZE]; /* ADC DMA BUF */
//adc初始化
int device_adc()
{
adc_dma_init((uint32_t)&ADC1->DR, (uint32_t)&g_adc_dma_buf); /* 初始化ADC */
return 0;
}
INIT_DEVICE_EXPORT(device_adc);
//adc任务
static void test_thread_entry(void *parameter)
{
uint16_t i;
uint16_t adcx;
uint32_t sum;
float temp;
adc_dma_enable(ADC_DMA_BUF_SIZE); /* 启动ADC DMA缓存 */
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "ADC DMA TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 130, 200, 16, 16, "ADC1_CH19_VAL:", BLUE);
lcd_show_string(30, 150, 200, 16, 16, "ADC1_CH19_VOL:0.000V", BLUE); /* 在整数后面显示小数点 */
while (1)
{
/* 阻塞等待接收信号量,等到信号量后再次读取数据 */
rt_sem_take(&adc_dma_sem, RT_WAITING_FOREVER);
/* 无效化 D Cache */
SCB_InvalidateDCache();
/* 计算DMA 缓存得到的ADC数据的平均值 */
sum = 0;
for (i = 0; i < ADC_DMA_BUF_SIZE; i++) /* 累加 */
{
sum += g_adc_dma_buf[i];
}
adcx = sum / ADC_DMA_BUF_SIZE; /* 取平均值 */
/* 显示结果 */
lcd_show_xnum(142, 130, adcx, 5, 16, 0, BLUE); /* 显示ADC采样得到的原始值 */
temp = (float)adcx * (3.3 / 65536); /* 获取根据公式计算的ADC转换的实际电压值,比如0.1111 */
adcx = temp; /* 赋值整数部分给adcx变量,因为adcx为u16类型 */
lcd_show_xnum(142, 150, adcx, 1, 16, 0, BLUE); /* 显示电压值的整数部分,比如3.1111的显示,这里只显示3 */
temp -= adcx; /* 把已经显示的整数部分去掉,得到小数部分,比如0.1111-3=0.1111 */
temp *= 1000; /* 小数部分乘以1000,例如:0.1111就转换为111.1,相当于保留三位小数。 */
lcd_show_xnum(158, 150, temp, 3, 16, 0x80, BLUE); /* 显示小数部分(前面已经转换为整数显示),这里显示的就是111. */
adc_dma_enable(ADC_DMA_BUF_SIZE); /* 启动下一次ADC DMA缓存 */
}
}
//任务
static int dac_adc_sample(int argc, char *argv[])
{
rt_err_t ret = RT_EOK;
/* 初始化信号量 */
rt_sem_init(&adc_dma_sem, "adc_dma_sem", 0, RT_IPC_FLAG_FIFO);
/* 创建 serial 线程 */
rt_thread_t thread = rt_thread_create("test", test_thread_entry, RT_NULL, 2048, 25, 10);
/* 创建成功则启动线程 */
if (thread != RT_NULL)
{
rt_thread_startup(thread);
}
else
{
ret = RT_ERROR;
}
return ret;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(dac_adc_sample, dac_adc device sample);
代码原理:
ADC 每转换完 1 个数据,自动通知 DMA 搬走到内存
填满一整个数组后 DMA 触发中断通知主任务采集完成
1. 关键配置对应代码
- 外设地址 PAR = &ADC1->DR ADC 转换完的数字值,永远存在 DR 寄存器(16 位半字),DMA 每次从这个地址读数据。
- 内存地址 MAR = &g_adc_dma_buf [0] 定义的
uint16_t g_adc_dma_buf[50]数组首地址,DMA 把 ADC 数据依次存到这里。 - DMA 参数设置:
DMA_PERIPH_TO_MEMORY:外设 → 内存DMA_PINC_DISABLE:外设地址不增加(永远读 DR)DMA_MINC_ENABLE:内存地址自动 + 2(下一个数组元素)NDTR = 50:总共要传输 50 个数据
2. 完整自动流程(硬件自动跑,不用 CPU 干预)
- 调用
adc_dma_enable(50)开启 ADC+DMA- 打开 ADC、DMA,设置要传输 50 个样本
- ADC 开启连续转换模式,转换完立刻自动开始下一次转换
- 第一次 ADC 转换完成: ADC 硬件自动产生 DMA 请求(硬件连线,无需软件触发)
- DMA 收到请求:
- 从
ADC->DR读出 1 个 16 位采样值 - 存入
g_adc_dma_buf[0] - 内存地址自动偏移 2 字节,指向下一个元素
g_adc_dma_buf[1] - NDTR 计数器自动减 1(现在变成 49)
- 从
- ADC 马上开始第二次转换,重复步骤 2~3,依次填充:
buf[0] → buf[1] → buf[2] → ... → buf[49] - 当 50 个数据全部搬运完成(NDTR 减到 0): DMA 硬件触发传输完成中断 TC
- 进入 DMA 中断服务函数
ADC_ADCX_DMASx_IRQHandler: 把全局标志g_adc_dma_sta = 1,告诉主循环:一整组 50 个数据全部采完了!
3. 主循环
if(g_adc_dma_sta == 1)
{
// 此时 g_adc_dma_buf[0]~g_adc_dma_buf[49] 全部存好了ADC采样值
// CPU只需要一次性读取数组做平均、计算电压、屏幕显示
// 处理完再重新启动下一轮DMA采集50个点
g_adc_dma_sta = 0;
adc_dma_enable(50);
}
整段采集 50 个数据的过程,CPU 全程不参与数据搬运,只在数组填满后统一处理,解放 CPU。
这里的缺点就是需要启停 ADC
3.adc连续触发模式
一种是连续触发:
ADC+DMA+TIM,这个的特点:
-
TIM触发ADC:
-
使用TIM6定时器产生精确的触发信号
-
可编程采样频率控制
-
减少CPU干预
-
-
DMA循环模式:
-
使用DMA_CIRCULAR模式
-
自动循环采集,无需每次重启
-
支持双缓冲(通过半传输中断)(暂时没加入半中断)
-
-
HAL库兼容:
-
使用标准HAL库中断处理
-
添加DMA传输完成回调
-
更规范的错误处理
-
-
功能增强:
-
添加采样频率设置函数
-
添加命令行控制接口
-
更好的数据显示格式
-
添加采样计数器
-
-
性能优化:
-
减少中断频率
-
精确的时序控制
-
更稳定的数据流
-
这里留个问题:经典的双缓存模式如何实现?
#include "adc.h"
#include "lcd.h"
#include <stdlib.h> // 包含atoi函数
// 信号量
static struct rt_semaphore adc_dma_sem;
ADC_HandleTypeDef g_adc_dma_handle; /* 带DMA功能的ADC句柄 */
DMA_HandleTypeDef g_dma_adc_handle; /* 带ADC功能的DMA句柄 */
TIM_HandleTypeDef htim_adc_trig; /* 用于触发ADC的定时器 */
#define ADC_DMA_BUF_SIZE 50 /* ADC DMA缓存 BUF大小 */
uint16_t g_adc_dma_buf[ADC_DMA_BUF_SIZE]; /* ADC DMA BUF */
/* TIM配置 - 用于触发ADC */
static void TIM_ADC_Trigger_Init(void)
{
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
/* TIM6初始化,用于ADC触发 */
htim_adc_trig.Instance = TIM6; /* 使用TIM6 */
htim_adc_trig.Init.Prescaler = 10 - 1; /* 预分频,系统时钟为400MHz时,TIM6时钟为200MHz */
htim_adc_trig.Init.CounterMode = TIM_COUNTERMODE_UP;
htim_adc_trig.Init.Period = 200 - 1; /* 自动重装载值 */
htim_adc_trig.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_Base_Init(&htim_adc_trig);
/* 配置TIM时钟源 */
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
HAL_TIM_ConfigClockSource(&htim_adc_trig, &sClockSourceConfig);
/* 主输出配置,触发ADC */
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE; /* 更新事件触发TRGO */
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
HAL_TIMEx_MasterConfigSynchronization(&htim_adc_trig, &sMasterConfig);
}
/**
* @brief ADC DMA读取 初始化函数
* @retval 无
*/
void adc_dma_init(void)
{
GPIO_InitTypeDef gpio_init_struct;
ADC_ChannelConfTypeDef adc_ch_conf = {0};
__HAL_RCC_ADC_CONFIG(RCC_ADCCLKSOURCE_CLKP); /* ADC外设时钟选择PLL */
/* 开启ADC通道IO口时钟和ADC时钟 */
ADC_ADCX_CHY_GPIO_CLK_ENABLE();
ADC_ADCX_CHY_CLK_ENABLE();
/* 初始化定时器用于ADC触发 */
__HAL_RCC_TIM6_CLK_ENABLE();
TIM_ADC_Trigger_Init();
/* 初始化DMA时钟 */
if ((uint32_t)ADC_ADCX_DMASx > (uint32_t)DMA2)
{
__HAL_RCC_DMA2_CLK_ENABLE();
}
else
{
__HAL_RCC_DMA1_CLK_ENABLE();
}
/* 初始化ADC通道对应的IO引脚 */
gpio_init_struct.Pin = ADC_ADCX_CHY_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_ANALOG;
gpio_init_struct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(ADC_ADCX_CHY_GPIO_PORT, &gpio_init_struct);
/* 配置ADC */
g_adc_dma_handle.Instance = ADC_ADCX;
g_adc_dma_handle.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV2; /* 异步时钟2分频 */
g_adc_dma_handle.Init.Resolution = ADC_RESOLUTION_12B; /* 12位模式 */
g_adc_dma_handle.Init.ScanConvMode = DISABLE; /* 单通道非扫描模式 */
g_adc_dma_handle.Init.EOCSelection = ADC_EOC_SEQ_CONV; /* 序列转换结束标志 */
g_adc_dma_handle.Init.LowPowerAutoWait = DISABLE;
g_adc_dma_handle.Init.ContinuousConvMode = DISABLE; /* 关闭连续转换,由外部触发 */
g_adc_dma_handle.Init.NbrOfConversion = 1; /* 1个转换在规则序列中 */
g_adc_dma_handle.Init.DiscontinuousConvMode = DISABLE;
g_adc_dma_handle.Init.ExternalTrigConv = ADC_EXTERNALTRIG_T6_TRGO; /* TIM6触发 */
g_adc_dma_handle.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING; /* 上升沿触发 */
g_adc_dma_handle.Init.ConversionDataManagement = ADC_CONVERSIONDATA_DMA_CIRCULAR; /* DMA循环模式 */
g_adc_dma_handle.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;
g_adc_dma_handle.Init.LeftBitShift = ADC_LEFTBITSHIFT_NONE;
g_adc_dma_handle.Init.OversamplingMode = DISABLE;
HAL_ADC_Init(&g_adc_dma_handle);
/* 校准ADC */
HAL_ADCEx_Calibration_Start(&g_adc_dma_handle, ADC_CALIB_OFFSET, ADC_SINGLE_ENDED);
/* 配置ADC通道 */
adc_ch_conf.Channel = ADC_ADCX_CHY; /* ADC通道 */
adc_ch_conf.Rank = ADC_REGULAR_RANK_1; /* 规则序列1 */
adc_ch_conf.SamplingTime = ADC_SAMPLETIME_8CYCLES_5; /* 采样时间 */
adc_ch_conf.SingleDiff = ADC_SINGLE_ENDED;
adc_ch_conf.OffsetNumber = ADC_OFFSET_NONE;
adc_ch_conf.Offset = 0;
adc_ch_conf.OffsetRightShift = DISABLE;
adc_ch_conf.OffsetSignedSaturation = DISABLE;
HAL_ADC_ConfigChannel(&g_adc_dma_handle, &adc_ch_conf);
/* 配置DMA */
g_dma_adc_handle.Instance = ADC_ADCX_DMASx;
g_dma_adc_handle.Init.Request = ADC_ADCX_DMASx_REQ;
g_dma_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;
g_dma_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE;
g_dma_adc_handle.Init.MemInc = DMA_MINC_ENABLE;
g_dma_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
g_dma_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
g_dma_adc_handle.Init.Mode = DMA_CIRCULAR; /* 循环模式 */
g_dma_adc_handle.Init.Priority = DMA_PRIORITY_VERY_HIGH;
g_dma_adc_handle.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
HAL_DMA_Init(&g_dma_adc_handle);
/* 链接DMA到ADC */
__HAL_LINKDMA(&g_adc_dma_handle, DMA_Handle, g_dma_adc_handle);
/* 配置NVIC中断 */
HAL_NVIC_SetPriority(ADC_ADCX_DMASx_IRQn, 3, 3);
HAL_NVIC_EnableIRQ(ADC_ADCX_DMASx_IRQn);
}
/**
* @brief 设置ADC采样频率
* @param freq_hz: 采样频率(Hz)
* @retval 实际设置的采样频率
*/
float adc_set_sample_freq(uint32_t freq_hz)
{
uint32_t tim_clock = 200000000; /* TIM6时钟,假设系统时钟400MHz,APB1为200MHz */
uint32_t prescaler = 100 - 1; /* 预分频值 */
uint32_t period;
float actual_freq;
if (freq_hz == 0 || freq_hz > 2000000) /* 限制频率范围 */
return 0;
period = (tim_clock / (prescaler + 1)) / freq_hz - 1;
if (period > 0xFFFF) /* 确保period在16位范围内 */
{
period = 0xFFFF;
}
__HAL_TIM_SET_AUTORELOAD(&htim_adc_trig, period);
actual_freq = (float)tim_clock / ((prescaler + 1) * (period + 1));
return actual_freq;
}
/**
* @brief 启动ADC采集
* @param 无
* @retval 无
*/
void adc_start(void)
{
/* 启动DMA传输(循环模式) */
HAL_DMA_Start_IT(&g_dma_adc_handle,
(uint32_t)&ADC1->DR,
(uint32_t)g_adc_dma_buf,
ADC_DMA_BUF_SIZE);
/* 启动ADC DMA模式 */
HAL_ADC_Start_DMA(&g_adc_dma_handle,
(uint32_t*)g_adc_dma_buf,
ADC_DMA_BUF_SIZE);
/* 启动定时器触发 */
HAL_TIM_Base_Start(&htim_adc_trig);
}
/**
* @brief 停止ADC采集
* @param 无
* @retval 无
*/
void adc_stop(void)
{
/* 停止定时器 */
HAL_TIM_Base_Stop(&htim_adc_trig);
/* 停止ADC DMA */
HAL_ADC_Stop_DMA(&g_adc_dma_handle);
}
void ADC_ADCX_DMASx_IRQHandler(void)
{
// 先调用HAL库中断处理函数
HAL_DMA_IRQHandler(&g_dma_adc_handle);
}
// 添加DMA传输完成回调函数(HAL库标准用法)
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if (hadc->Instance == ADC_ADCX)
{
rt_sem_release(&adc_dma_sem); // 释放信号量
}
}
/* 设备初始化 - RT-Thread自动初始化 */
int device_adc_init(void)
{
/* 初始化信号量 */
rt_sem_init(&adc_dma_sem, "adc_dma_sem", 0, RT_IPC_FLAG_FIFO);
/* 初始化ADC DMA */
adc_dma_init();
/* 设置默认采样频率Hz */
adc_set_sample_freq(30000);
return 0;
}
INIT_DEVICE_EXPORT(device_adc_init);
uint16_t int_part = 0;
uint16_t frac_part = 0;
/* ADC数据处理线程 */
static void adc_thread_entry(void *parameter)
{
uint16_t i;
uint16_t adc_value;
uint32_t sum;
float voltage;
uint32_t sample_count = 0;
/* LCD显示初始化 */
lcd_show_string(30, 50, 200, 16, 16, "STM32H743 ADC", RED);
lcd_show_string(30, 70, 200, 16, 16, "TIM Trigger + DMA", RED);
lcd_show_string(30, 90, 200, 16, 16, "first ver", RED);
lcd_show_string(30, 120, 200, 16, 16, "ADC Value:", BLUE);
lcd_show_string(30, 140, 200, 16, 16, "ADC1_CH19_VOL:0.000V", BLUE);
lcd_show_string(30, 160, 200, 16, 16, "Samples: 0", BLUE);
/* 启动ADC采集 */
adc_start();
while (1)
{
/* 等待DMA完成信号 */
if (rt_sem_take(&adc_dma_sem, RT_WAITING_FOREVER) == RT_EOK)
{
/* 确保数据缓存一致性(如果使用Cache) */
//#ifdef SCB_InvalidateDCache_by_Addr
uint32_t buf_addr = (uint32_t)g_adc_dma_buf;
uint32_t buf_len = ADC_DMA_BUF_SIZE * sizeof(uint16_t);
SCB_InvalidateDCache_by_Addr((uint32_t *)buf_addr, buf_len);
// #endif
//SCB_InvalidateDCache();
/* 计算平均值 */
sum = 0;
for (i = 0; i < ADC_DMA_BUF_SIZE; i++)
{
sum += g_adc_dma_buf[i];
}
adc_value = sum / ADC_DMA_BUF_SIZE;
sample_count += ADC_DMA_BUF_SIZE;
/* 转换为电压 */
voltage = (float)adc_value * (3.3f / 4096.0f);
/* 显示结果 */
lcd_show_xnum(110, 120, adc_value, 5, 16, 0, BLUE);
/* 显示电压,格式: X.XXX */
int_part = (uint16_t)voltage;
frac_part = (uint16_t)((voltage - int_part) * 1000);
//占用时间
//rt_kprintf("ADC Value: %d, Voltage: %d.%03dV\n",adc_value, int_part, frac_part);
lcd_show_xnum(142, 140, int_part, 1, 16, 0, BLUE);
lcd_show_xnum(158, 140, frac_part, 3, 16, 0x80, BLUE);
/* 显示采样计数 */
lcd_show_xnum(100, 160, sample_count, 10, 16, 0, BLUE);
}
}
}
/* 任务创建 */
static int adc_sample_start(int argc, char *argv[])
{
rt_thread_t thread;
/* 创建ADC处理线程 */
thread = rt_thread_create("adc",
adc_thread_entry,
RT_NULL,
2048,
20, /* 优先级略高于默认 */
10);
if (thread != RT_NULL)
{
rt_thread_startup(thread);
rt_kprintf("ADC sample thread started.\n");
return RT_EOK;
}
return -RT_ERROR;
}
MSH_CMD_EXPORT(adc_sample_start, Start ADC sampling with TIM trigger);
/* 设置采样频率命令 */
static int adc_set_freq(int argc, char *argv[])
{
uint32_t freq;
if (argc != 2)
{
rt_kprintf("Usage: adc_freq <frequency_hz>\n");
rt_kprintf("Example: adc_freq 1000 (set 1kHz sampling)\n");
return -RT_ERROR;
}
freq = atoi(argv[1]);
float actual_freq = adc_set_sample_freq(freq);
rt_kprintf("Set ADC sampling frequency:\n");
rt_kprintf(" Requested: %d Hz\n", freq);
rt_kprintf(" Actual: %.2f Hz\n", actual_freq);
return RT_EOK;
}
MSH_CMD_EXPORT(adc_set_freq, Set ADC sampling frequency);
/* 停止ADC采集命令 */
static int adc_stop_cmd(int argc, char *argv[])
{
adc_stop();
rt_kprintf("ADC stopped.\n");
return RT_EOK;
}
MSH_CMD_EXPORT(adc_stop_cmd, Stop ADC sampling);
/* 启动ADC采集命令 */
static int adc_start_cmd(int argc, char *argv[])
{
adc_start();
rt_kprintf("ADC started.\n");
return RT_EOK;
}
MSH_CMD_EXPORT(adc_start_cmd, Start ADC sampling);
时间关系,先记录到这,后面把数据存到SDRAM后,累计一定包长上传上位机软件。慢慢更新。
三、最终效果
GPIO将DAC输出和ADC出入的引脚用跳线帽短接。最终效果如下:
DAC模拟的是实际开发中从物理信号--》传感器采集转换成电压信号
ADC采集模拟信号,然后通过DMA及TIM做到不占用CPU。
一般工程化方案基本都这个原理。
ADC_DAC
更多推荐

所有评论(0)