HC32F460 的 ADC+DMA:定时器触发采集 50Hz 正弦波方案

在工业测控、电力监测等场景中,50Hz 正弦波(如市电电压、电流信号)的精准采集是核心需求。HC32F460 单片机凭借高性能 ADC(最高 12 位精度)、灵活的 DMA 控制器及多通道定时器,可实现 “定时器触发 ADC 转换 + DMA 自动数据搬运” 的高效采集方案,既保证采样时序精准,又避免 CPU 陷入数据搬运,显著提升系统实时性。本文结合实际工程代码,从方案原理、硬件设计、软件实现到调试要点,完整拆解 50Hz 正弦波采集方案。

一、方案核心原理:三外设协同工作机制

50Hz 正弦波采集的关键需求是 “精准采样时序” 与 “高效数据处理”,需依赖 ADC、定时器(TMR0)、DMA 三者协同,其工作链路如下所示:

定时器(TMR0)→ 触发信号(TRGO)→ ADC 启动转换 → 转换完成 → DMA 搬运数据 → 环形缓冲区存储 → 线程处理(FFT 计算)

1.1 各外设核心角色

  • 定时器(TMR0):作为 “采样时钟源”,按预设频率输出 TRGO 触发信号,精准控制 ADC 采样间隔。根据奈奎斯特采样定理,采集 50Hz 正弦波需采样率 ≥100Hz,本方案选择 12800Hz 采样率(频率分辨率 = 12800Hz/256 = 50Hz,恰好匹配 50Hz 基波,简化 FFT 基波提取)。
  • ADC:接收 TRGO 触发后,将 PB1 引脚输入的 50Hz 正弦波模拟信号(如 0~3.3V)转换为 12 位数字量(0~4095),数据存储于 ADC 通道 9 数据寄存器(ADC1->DR9)。
  • DMA(CM_DMA1_CH0):作为 “数据搬运工”,在 ADC 转换完成后,自动将 DR9 中的数据搬运至 DMA 接收缓冲区(g_dma_buf),无需 CPU 干预;累计 32 个数据后触发中断,将数据写入环形缓冲区(g_adc_ring_buf),避免数据丢失。

二、硬件设计:引脚与电路配置

硬件设计需围绕 “信号输入 - 触发控制 - 数据传输” 三个链路展开,确保信号稳定、干扰最小,具体配置与代码宏定义一一对应。

2.1 核心引脚分配

外设模块引脚功能描述代码宏定义对应
ADC 采样通道PB150Hz 正弦波模拟信号输入(ADC1_CH9)ADC_SEQB_CH_PORT/PIN
定时器触发无外露引脚TMR0_CH_B 输出 TRGO 信号(AOS 内部路由)TMR0_UNIT/CH
DMA 数据传输无外露引脚内部总线传输(ADC1->DMA1->RAM)DMA_UNIT/CH

2.2 硬件电路注意事项

  1. 模拟信号输入电路:PB1 引脚需串联 1kΩ 限流电阻(防止过流损坏 ADC)和 10nF 陶瓷滤波电容(滤除高频噪声),输入信号幅度需匹配 ADC 参考电压(3.3V),避免超量程导致数据饱和。
  2. 参考电压稳定性:ADC 参考电压(V_REF)直接影响转换精度,建议采用独立 LDO 供电(如 AMS1117-3.3),并在 V_REF 引脚旁并联 10μF 电解电容 + 0.1μF 陶瓷电容,抑制电压波动。
  3. 接地设计:模拟地(ADC 采样地)与数字地(定时器、DMA 地)需单点连接,避免数字信号干扰模拟采样,提升数据稳定性。

三、软件实现:分层设计与核心代码解析

软件采用 “底层外设驱动 + 上层线程处理” 的分层架构,底层负责硬件初始化与数据搬运,上层负责 FFT 计算与结果输出,代码基于 RT-Thread 操作系统,确保多任务同步安全。

3.1 软件分层架构

层级核心功能关键函数 / 模块
底层驱动层ADC/TMR0/DMA 初始化、中断配置AdcConfig()、AdcDmaConfig()、AdcIrqConfig()
数据搬运层DMA 中断回调、环形缓冲区管理AdcDma_IrqCallback()、dma_data_ring_write()
上层应用层采样线程、FFT 有效值计算、MSH 命令输出adc_thread_entry()、ADC_FFT_GetRMS()、read_ac()

3.2 核心模块实现详解

3.2.1 底层初始化:ADC+TMR0+DMA 配置

底层初始化的核心是 “按依赖顺序配置外设”(ADC 核心→定时器触发→DMA 链路),避免因配置顺序错误导致触发失效。

1. ADC 核心配置(AdcInitConfig ())ADC 工作模式设为 “SEQ_A 单次扫描”,配合 DMA 实现连续采样,关键代码如下:

static void AdcInitConfig(void)
{
    stc_adc_init_t stcAdcInit;
    FCG_Fcg3PeriphClockCmd(ADC_PERIPH_CLK, ENABLE); // 使能 ADC1 时钟
    (void)ADC_StructInit(&stcAdcInit);              // 默认初始化(右对齐、12位精度)
    stcAdcInit.u16ScanMode = ADC_MD_SEQA_SINGLESHOT; // 单次扫描→DMA 连续触发
    (void)ADC_Init(ADC_UNIT, &stcAdcInit);          // 写入 ADC 寄存器
    AdcSetPinAnalogMode();                          // PB01 设为模拟模式
    ADC_ChCmd(ADC_UNIT, ADC_SEQ_A, ADC_SEQB_CH, ENABLE); // 使能通道9
}

2. 定时器触发配置(AdcHardTriggerConfig ())TMR0 采用 “向上计数 + 比较模式”,通过分频系数(16 分频)与比较值(484)实现 12800Hz 触发频率,计算过程如下:假设 TMR0 时钟源为 PCLK2(100MHz),则:触发频率 = 100MHz / [(16-1+1) × (484+1)] = 100MHz / (16×485) ≈ 12800Hz关键代码如下:

static void AdcHardTriggerConfig(void)
{
    stc_tmr0_init_t stcTmr0Init;
    (void)TMR0_StructInit(&stcTmr0Init);
    stcTmr0Init.u32ClockDiv = TMR0_CLK_DIV16;       // 16分频
    stcTmr0Init.u16CompareValue = (uint16_t)484UL;  // 比较值484
    TMR0_PERIPH_ENABLE();                           // 使能 TMR0 时钟
    (void)TMR0_Init(TMR0_UNIT, TMR0_CH, &stcTmr0Init); // 初始化 TMR0_CH_B
    FCG_Fcg0PeriphClockCmd(FCG0_PERIPH_AOS, ENABLE); // 使能 AOS 路由
    AOS_SetTriggerEventSrc(AOS_ADC1_0, EVT_SRC_TMR0_1_CMP_B); // TMR0→ADC 触发
    ADC_TriggerCmd(ADC_UNIT, ADC_SEQ_A, ENABLE);     // 使能 ADC 硬件触发
}

3. DMA 链路配置(AdcDmaConfig ())DMA 配置核心是 “绑定 ADC 数据源与 RAM 目的地址”,单次传输 32 个 16 位数据后触发中断,关键代码如下:

static void AdcDmaConfig(void)
{
    stc_dma_init_t stcDmaInit;
    (void)DMA_StructInit(&stcDmaInit);
    stcDmaInit.u32IntEn = DMA_INT_ENABLE;                // 使能传输完成中断
    stcDmaInit.u32SrcAddr = (uint32_t)&ADC_UNIT->DR9;    // 源地址:ADC1_CH9 数据寄存器
    stcDmaInit.u32DestAddr = (uint32_t)(&g_dma_buf[0U]); // 目的地址:DMA 接收缓冲区
    stcDmaInit.u32DataWidth = DMA_DATAWIDTH_16BIT;       // 16位数据(匹配 uint16_t)
    stcDmaInit.u32TransCount = 32UL;                     // 单次传输32个数据
    stcDmaInit.u32SrcAddrInc = DMA_SRC_ADDR_FIX;         // 源地址固定(DR9 地址不变)
    stcDmaInit.u32DestAddrInc = DMA_DEST_ADDR_INC;       // 目的地址递增(存满 g_dma_buf)
    
    FCG_Fcg0PeriphClockCmd(FCG0_PERIPH_DMA1, ENABLE);    // 使能 DMA1 时钟
    (void)DMA_Init(DMA_UNIT, ADC_DMA_CH, &stcDmaInit);   // 初始化 DMA 通道0
    AOS_SetTriggerEventSrc(AOS_DMA1_0, EVT_SRC_ADC1_EOCA); // ADC 完成→DMA 触发
    AdcIrqConfig();                                      // 配置 DMA 中断
    DMA_Cmd(DMA_UNIT, ENABLE);                           // 使能 DMA1
    DMA_ChCmd(DMA_UNIT, ADC_DMA_CH, ENABLE);             // 使能 DMA 通道0
}
3.2.2 数据搬运:中断与环形缓冲区

为避免 DMA 数据覆盖,采用 “环形缓冲区”(大小 256,匹配 FFT 处理长度)存储采样值,由 DMA 中断回调函数(AdcDma_IrqCallback ())完成数据写入,确保连续采集不中断。

1. DMA 中断回调(AdcDma_IrqCallback ())中断触发后,先清除中断标志,再调用 dma_data_ring_write () 将 32 个数据写入环形缓冲区,最后重置 DMA 配置准备下一次传输:

static void AdcDma_IrqCallback(void)
{
    if (DMA_GetTransCompleteStatus(DMA_UNIT, DMA_STAT_TRANS_CH0) == SET)
    {
        DMA_ClearTransCompleteStatus(DMA_UNIT, DMA_FLAG_TC_CH0); // 清除中断标志
        dma_data_ring_write(g_dma_buf);                          // 写入环形缓冲区
        DMA_SetDestAddr(DMA_UNIT, ADC_DMA_CH, (uint32_t)&g_dma_buf[0U]); // 重置目的地址
        DMA_SetTransCount(DMA_UNIT, ADC_DMA_CH, 32UL);           // 重置传输计数
        DMA_ChCmd(DMA_UNIT, ADC_DMA_CH, ENABLE);                 // 重新使能 DMA 通道
    }
}

2. 环形缓冲区写入(dma_data_ring_write ())处理缓冲区 “绕回” 场景(写指针到达缓冲区末尾时,从开头重新写入),并通过原子变量(g_write_ptr、g_total_data_count)确保中断与线程的数据安全:

static void dma_data_ring_write(uint16_t *dma_buf)
{
    int write_ptr = rt_hw_atomic_load(&g_write_ptr); // 原子读取写指针
    int remaining = BUF_SIZE - write_ptr;            // 剩余空间
    
    if (remaining >= 32UL) // 空间足够,直接写入
    {
        for (int i = 0; i < 32; i++)
            g_adc_ring_buf[write_ptr + i] = dma_buf[i];
        rt_atomic_add(&g_write_ptr, 32); // 写指针+32
    }
    else // 空间不足,分两段写入
    {
        for (int i = 0; i < remaining; i++)
            g_adc_ring_buf[write_ptr + i] = dma_buf[i];
        for (int i = 0; i < 32 - remaining; i++)
            g_adc_ring_buf[i] = dma_buf[remaining + i];
        rt_atomic_store(&g_write_ptr, 32 - remaining); // 重置写指针
    }
    
    rt_atomic_and(&g_write_ptr, BUF_SIZE - 1); // 确保指针在缓冲区范围内
    rt_atomic_add(&g_total_data_count, 32);    // 累计数据量+32
    // 满256点(一帧),释放信号量通知线程处理
    while (rt_atomic_load(&g_total_data_count) >= BUF_SIZE)
    {
        rt_sem_release(sem_adc);
        rt_atomic_sub(&g_total_data_count, BUF_SIZE);
    }
}
3.2.3 上层处理:线程与 FFT 计算

采用 RT-Thread 线程实现 “采样数据处理”,通过信号量(sem_adc)与 DMA 中断同步,当环形缓冲区满 256 点时,线程唤醒并执行 FFT 计算,提取 50Hz 正弦波的基波幅值。

1. 采样线程(adc_thread_entry ())线程阻塞等待信号量,唤醒后将 uint16_t 原始数据转换为 float32_t,调用 ADC_FFT_GetRMS () 计算基波幅值:

void adc_thread_entry(void *parameter)
{
    for (;;)
    {
        rt_sem_take(sem_adc, RT_WAITING_FOREVER); // 等待256点数据
        // uint16_t → float32_t(FFT 需浮点输入)
        for (rt_uint16_t i = 0; i < BUF_SIZE; i++)
            g_u16Buf[i] = g_adc_ring_buf[i];
        ac_volt_rms = ADC_FFT_GetRMS(g_u16Buf); // 计算基波幅值
    }
}

2. FFT 有效值计算(ADC_FFT_GetRMS ())核心步骤包括 “去直流→FFT 变换→基波提取→校准”,利用 CMSIS-DSP 库函数简化 FFT 实现,关键逻辑如下:

float ADC_FFT_GetRMS(float32_t *pRawData)
{
    float32_t fAcData[N], fDcOffset = 0.0f;
    float32_t fFftIn[N*2], fFftMag[N/2];
    float32_t fFundAmp = 0.0f;
    powermod_param *power1 = &sysparam.powermod0_param[0];
    
    // 步骤1:去直流(计算平均值并减去)
    arm_mean_f32(pRawData, N, &fDcOffset);
    for (uint16_t i = 0; i < N; i++)
        fAcData[i] = pRawData[i] - fDcOffset;
    
    // 步骤2:FFT 输入初始化(实部=交流数据,虚部=0)
    for (uint16_t i = 0; i < N; i++)
    {
        fFftIn[2*i] = fAcData[i];
        fFftIn[2*i+1] = 0.0f;
    }
    
    // 步骤3:执行256点FFT(正变换)
    arm_cfft_instance_f32 S;
    arm_cfft_init_f32(&S, N);
    arm_cfft_f32(&S, fFftIn, 0, 1);
    
    // 步骤4:计算FFT幅值(仅取前128点,实信号对称性)
    arm_cmplx_mag_f32(fFftIn, fFftMag, N/2);
    
    // 步骤5:提取50Hz基波幅值(索引1对应50Hz,幅值修正)
    fFundAmp = 2 * fFftMag[1] / (N/2);
    fFundAmp = fFundAmp * power1->ac_k + power1->ac_b; // 硬件校准(修正误差)
    
    return fFundAmp; // 返回基波幅值(需有效值则除以√2)
}

3. MSH 命令输出(read_ac ())通过 RT-Thread MSH 命令导出结果,用户可在终端输入 read_ac 查看当前 50Hz 正弦波的基波幅值:

void read_ac(void)
{
    rt_kprintf("50Hz sine wave fundamental amplitude: %f V\n", ac_volt_rms);
}
MSH_CMD_EXPORT(read_ac, Read 50Hz sine wave amplitude);

四、调试要点与常见问题解决

4.1 关键参数核对

  1. 采样率验证:若 FFT 无法提取 50Hz 基波,需核对 TMR0 分频系数与比较值,确保采样率为 12800Hz(计算公式:采样率 = TMR0 时钟 / [(分频系数) × (比较值 + 1)])。
  2. DMA 传输匹配:DMA 单次传输计数(32)需与环形缓冲区写入逻辑一致,避免出现 “数据错位”(如传输计数改为 16,需同步修改 dma_data_ring_write () 中的循环次数)。
  3. ADC 校准:ADC 上电后需执行校准(代码中未体现,需补充 ADC_Calibrate(ADC_UNIT)),否则转换精度会偏差 10%~20%。

4.2 常见问题与解决方法

问题现象可能原因解决方法
FFT 结果无 50Hz 基波采样率错误 / ADC 通道未使能重新计算 TMR0 参数 / 检查 ADC_ChCmd () 使能
DMA 中断不触发DMA 触发源路由错误确认 AOS_SetTriggerEventSrc () 为 EVT_SRC_ADC1_EOCA
数据出现随机跳变模拟信号噪声大 / 接地不良增加滤波电容 / 优化模拟地与数字地连接

五、方案优势总结

  1. 低 CPU 占用:DMA 自动搬运数据,CPU 仅在 256 点数据采集完成后才参与 FFT 计算,CPU 占用率 ≤5%(100MHz 主频下)。
  2. 高采样精度:12 位 ADC 配合 3.3V 高精度参考电压,转换误差 ≤0.1%,经硬件校准后可满足工业级测量需求。
  3. 实时性强:12800Hz 采样率 + 256 点 FFT 处理,数据更新周期仅 20ms(1/50Hz),可实时跟踪 50Hz 正弦波幅值变化。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值