在工业测控、电力监测等场景中,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 采样通道 | PB1 | 50Hz 正弦波模拟信号输入(ADC1_CH9) | ADC_SEQB_CH_PORT/PIN |
| 定时器触发 | 无外露引脚 | TMR0_CH_B 输出 TRGO 信号(AOS 内部路由) | TMR0_UNIT/CH |
| DMA 数据传输 | 无外露引脚 | 内部总线传输(ADC1->DMA1->RAM) | DMA_UNIT/CH |
2.2 硬件电路注意事项
- 模拟信号输入电路:PB1 引脚需串联 1kΩ 限流电阻(防止过流损坏 ADC)和 10nF 陶瓷滤波电容(滤除高频噪声),输入信号幅度需匹配 ADC 参考电压(3.3V),避免超量程导致数据饱和。
- 参考电压稳定性:ADC 参考电压(V_REF)直接影响转换精度,建议采用独立 LDO 供电(如 AMS1117-3.3),并在 V_REF 引脚旁并联 10μF 电解电容 + 0.1μF 陶瓷电容,抑制电压波动。
- 接地设计:模拟地(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 关键参数核对
- 采样率验证:若 FFT 无法提取 50Hz 基波,需核对 TMR0 分频系数与比较值,确保采样率为 12800Hz(计算公式:采样率 = TMR0 时钟 / [(分频系数) × (比较值 + 1)])。
- DMA 传输匹配:DMA 单次传输计数(32)需与环形缓冲区写入逻辑一致,避免出现 “数据错位”(如传输计数改为 16,需同步修改 dma_data_ring_write () 中的循环次数)。
- ADC 校准:ADC 上电后需执行校准(代码中未体现,需补充
ADC_Calibrate(ADC_UNIT)),否则转换精度会偏差 10%~20%。
4.2 常见问题与解决方法
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| FFT 结果无 50Hz 基波 | 采样率错误 / ADC 通道未使能 | 重新计算 TMR0 参数 / 检查 ADC_ChCmd () 使能 |
| DMA 中断不触发 | DMA 触发源路由错误 | 确认 AOS_SetTriggerEventSrc () 为 EVT_SRC_ADC1_EOCA |
| 数据出现随机跳变 | 模拟信号噪声大 / 接地不良 | 增加滤波电容 / 优化模拟地与数字地连接 |
五、方案优势总结
- 低 CPU 占用:DMA 自动搬运数据,CPU 仅在 256 点数据采集完成后才参与 FFT 计算,CPU 占用率 ≤5%(100MHz 主频下)。
- 高采样精度:12 位 ADC 配合 3.3V 高精度参考电压,转换误差 ≤0.1%,经硬件校准后可满足工业级测量需求。
- 实时性强:12800Hz 采样率 + 256 点 FFT 处理,数据更新周期仅 20ms(1/50Hz),可实时跟踪 50Hz 正弦波幅值变化。
2118

被折叠的 条评论
为什么被折叠?



