一、概述
单片机能够实现的功能非常广泛,主要包括:
信号采集与感知:通过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->DRADC 转换完的数字值,永远存在 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