尧图精选

C语言实现DAC输出DTMF双音多频信号:从原理到定时器驱动实战

🕒 发布时间:2026/9/16 2:01:45 📁 来源:尧图网络
简介这是一套基于C语言的8051F020微控制器DAC0输出DTMF双音多频信号的工程源码面向学习嵌入式C语言开发与单片机通信接口的初级开发者。资源共10个文件压缩包仅19KB包含.c源文件、.h头文件、Keil工程配置文件.uv2、备份文件.bak及编译输出文件.lst/.obj等目录结构清晰可完整还原编译调试环境。目前已有85人完成学习下载。源码围绕DTMF生成这一真实场景演示了定时器T4中断的配置方式、DAC0寄存器的高低音频分时输出、中断服务函数中的码表更新以及延时函数的配合使用。通过阅读和运行该工程读者能够掌握寄存器操作、定时器中断、数模转换等关键嵌入式技能也能理解DTMF信号在电话通信中的编码原理为后续类似音频信号项目提供可直接参考的C语言模板。1. 从DAC0_DTMF1这个工程名说起C语言控制DAC输出双音多频信号看到“DAC0_DTMF1”这个命名最直接的理解是在某个嵌入式工程里用DAC通道0产生DTMF双音多频信号后缀的“1”表示第一组拨号缓冲区或者第一路音频通道。DTMF至今还大量出现在电话计费、门禁对讲、无线数传电台和仪器仪表的拨号面板里很多系统不跑协议栈只靠一颗MCU和一个DAC模块就要完成双音合成与回放。这个标题里的“c语言源码设置”和“c语言程序”指的并不是某个特定芯片的固件包而是一套可以落在C源码里的通用做法先建正弦查找表再按双音频率组合出DAC采样序列最后用定时器把样本喂给DAC0。下面这套方法适用于带DAC的STM32、NXP LPC以及类似寄存器布局的MCU用寄存器操作或HAL库都能改得动。适合看这篇文章的人是已经在用C语言写单片机程序、但还没有仔细处理过“双音叠加后怎么写进DAC寄存器”的工程师。新手可以照着章节顺序搭出最小可跑工程老手则可以直接跳到第3.2节看相位累加写法以及第5章里的直流偏置和静音处理。2. DTMF原理与DAC0输出模型频率组合、采样率和查找表2.1 DTMF双频组合与DAC0的换算DTMF信号并不是一段随机噪声也不是单音而是把16个按键映射成4个低频和4个高频的“行-列”组合。每个按键被按下时电话系统会同时产生一个低频音和一个高频音接收端根据这两个频率的落点判断出具体按键。DAC0在这里扮演的角色就是把这两个正弦波叠加后的离散序列持续转换成模拟电压输出。高频\低频1209 Hz1336 Hz1477 Hz1633 Hz697 Hz123A770 Hz456B852 Hz789C941 Hz*0#DC语言里做双音合成并不需要实时计算sin。更常见的做法是先建立一个正弦查找表然后让两个频率各自维护一个“相位指针”每次DAC更新时把两个相位对应的表值相加。这样操作既快又不会因为调用数学库而让周期抖动变大。DAC0输出范围如果是12位满量程0到4095理想的中心电平是2048。正弦波峰值幅度不能顶到满量程否则削波后会产生大量高次谐波远端接收端会误判成别的键盘频率。我通常把正弦幅度限制在1024左右这样叠加后最大值是3072最小值是1024给上下留出足够余量。2.2 用C语言建立正弦波查找表下面这段C语言源码建立64点正弦表表值已经包含直流偏置可以直接写入DAC0数据寄存器。#include math.h #include stdint.h #define SINE_TABLE_POINTS 64 #define DAC_FULL_SCALE 4095 #define DAC_CENTER 2048 #define DAC_AMPLITUDE 1024 static uint16_t sine_table[SINE_TABLE_POINTS]; void build_sine_table(void) { for (int i 0; i SINE_TABLE_POINTS; i) { double rad 2.0 * 3.14159265358979323846 * i / SINE_TABLE_POINTS; double value DAC_CENTER DAC_AMPLITUDE * sin(rad); sine_table[i] (uint16_t)(value 0.5); } }这段代码的逻辑是把360度相位等分成64份预先算出每个相位点上的正弦电压。DAC_CENTER是输出静音时的电平DAC_AMPLITUDE控制输出音量。加0.5是为了四舍五入避免在边界点上因为截断而产生直流偏置误差。为什么是64点而不是256点因为后续用相位累加器查表时64点足够覆盖DTMF频率范围内的波形精度同时占用的flash只有128字节低端单片机的flash比较紧张时这个优势特别明显。如果MCU资源充足可以用128点谐波会再低一些。2.3 采样率与缓冲区设计怎么定DAC0的更新节奏DAC0的更新节奏决定重建波形的频率上限。DTMF最高频率是1633 Hz根据奈奎斯特定理采样率至少两倍以上。实际电话系统普遍用8000 Hz采样率也就是每125微秒输出一个样本。这个频率不算高很多单片机用基本定时器就能触发DAC不会给CPU带来明显负担。参数推荐值说明采样率8000 Hz125 µs 更新一次DAC满足DTMF频率范围DAC分辨率12位0~4095一般芯片DAC都是这个分辨率正弦查找表点数64点相位步进计算方便资源占用小DTMF符号时长40 ms一个按键音持续40ms或按实际协议调整符号间静音50 ms推荐50ms避免接收端把连续音当成一个按键这里的关键是相位步进的计算方法。在C语言程序里我习惯用16位定点相位累加器而不是浮点累加。浮点写法虽然好懂但在中断服务函数里多次乘除会带来不确定性而且不同编译器的浮点优化结果也不一样。定点写法是相位步进 目标频率 × 查找表点数 × 65536 ÷ 采样率。例如697 Hz、64点表、8000 Hz采样率算下来的步进约等于(697 * 64 * 65536) / 8000 365587。每次中断触发时累加这个步进用高16位作为表下标等效于一个相位指针在表上平滑移动。3. 用C语言源码设置DAC0_DTMF1初始化、查表输出与定时器配合3.1 硬件初始化DAC0时钟和GPIO配置在STM32上DAC0信号通常从PA4引脚引出。如果用的是HAL库初始化代码可以写成下面这样。即便你用的是寄存器版本核心顺序也是“开时钟→配GPIO→配DAC→启动通道”。#include stm32f4xx_hal.h static DAC_HandleTypeDef hdac; void dac0_dtmf1_init(void) { __HAL_RCC_DAC_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef gpio {0}; gpio.Pin GPIO_PIN_4; gpio.Mode GPIO_MODE_ANALOG; gpio.Pull GPIO_NOPULL; HAL_GPIO_Init(GPIOA, gpio); hdac.Instance DAC; HAL_DAC_Init(hdac); HAL_DAC_Start(hdac, DAC_CHANNEL_1); HAL_DAC_SetValue(hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, DAC_CENTER); }这段代码先把GPIO设置成模拟模式因为DAC输出引脚不需要数字输入输出功能。HAL_DAC_Start启动通道之后DAC0就开始输出电压但此时还没有任何波形。最后一行把DAC数据寄存器先设置成中心电平2048这样在准备拨号之前输出端保持静音不会产生“啪”的开关噪声。注意不同的MCU型号里DAC通道编号和HAL命名可能存在差异例如GD32的DAC_ALIGN_12B_R、STM32L0的DAC1等移植时以具体数据手册的寄存器名为准。3.2 双音叠加两个频率查表相加后限幅关键C语言源码在这里。我维护两个独立的相位累加器一个负责低频一个负责高频。每次DAC更新前把两个查表值相加再减去中心电平这样能避免直流分量重复叠加。#define SAMPLE_RATE 8000U #define PHASE_ACC_SHIFT 16U #define PHASE_ACC_UNIT (1UL PHASE_ACC_SHIFT) static uint32_t phase_acc_low; static uint32_t phase_acc_high; static uint16_t phase_step_for(uint32_t freq_hz) { uint64_t step (uint64_t)freq_hz * SINE_TABLE_POINTS * PHASE_ACC_UNIT; step / SAMPLE_RATE; return (uint16_t)(step 0xFFFFU); } static uint16_t dac0_dtmf1_next_sample(uint16_t step_low, uint16_t step_high) { phase_acc_low step_low; phase_acc_high step_high; uint16_t idx_low (phase_acc_low PHASE_ACC_SHIFT) % SINE_TABLE_POINTS; uint16_t idx_high (phase_acc_high PHASE_ACC_SHIFT) % SINE_TABLE_POINTS; int32_t sum (int32_t)sine_table[idx_low] (int32_t)sine_table[idx_high] - DAC_CENTER; if (sum 0) { sum 0; } if (sum DAC_FULL_SCALE) { sum DAC_FULL_SCALE; } return (uint16_t)sum; }phase_step_for用64位中间变量做乘法避免32位溢出返回值只保留16位小数部分。为什么% SINE_TABLE_POINTS放在移位之后因为相位累加器溢出后高16位自然循环但取模仍然可能有少量重复偏移放在这里能严格限定下标范围。两个正弦表值相加后要减去一次中心电平原因是一个表值已经包含2048偏置两个相加等于4096偏置减去2048后输出重新回到2048附近。最后限幅到0和4095之间防止DAC寄存器写入超出范围的数值。3.3 定时器触发DAC0输出的最小C程序如果直接在while(1)里调用dac0_dtmf1_next_sample波形频率会受到其他任务干扰。更标准的方式是用一个固定频率的定时器中断触发DAC转换。STM32的DAC支持由TIM6或TIM7触发配置好之后CPU只要在DAC输出完成中断里更新下一个样本即可。void dac0_dtmf1_timer_start(void) { TIM_HandleTypeDef htim6 {0}; __HAL_RCC_TIM6_CLK_ENABLE(); htim6.Instance TIM6; htim6.Init.Prescaler 84 - 1; /* 84 MHz 时钟分频到 1 MHz */ htim6.Init.Period 125 - 1; /* 1 MHz / 125 8000 Hz */ HAL_TIM_Base_Init(htim6); HAL_TIM_Base_Start_IT(htim6); __HAL_DAC_ENABLE(hdac, DAC_CHANNEL_1); HAL_DAC_SetValue(hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, DAC_CENTER); } void TIM6_DAC_IRQHandler(void) { HAL_TIM_IRQHandler(htim6); } void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM6) { uint16_t sample dac0_dtmf1_next_sample(step_low_now, step_high_now); HAL_DAC_SetValue(hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, sample); } }这段代码的关键参数是预分频和自动重载值。84 MHz时钟先除以84得到1MHz再除以125得到8kHz。Period 125 - 1是因为定时器计数从0到124正好125个计数周期。有的开发者会把Prescaler和Period混淆结果输出频率要么高得离谱要么完全没有波形。想快速定位时先算1MHz / (Period 1)必须等于期望的采样率。3.4 停止和静音处理避免爆音拨号结束或切换下一个按键时不能直接把DAC输出寄存器改成0否则电压从几百毫伏瞬间跌到0V扬声器或对讲机里会听到一声明显的“咔嗒”。正确处理是把波形输出标志清掉并让DAC输出回到中心电平2048。还要注意在定时器中断里改变频率步进时相位累加器最好也重置到0这样每个按键都从同一相位开始波形衔接更自然。void dac0_dtmf1_stop_tone(void) { tone_active 0; phase_acc_low 0; phase_acc_high 0; HAL_DAC_SetValue(hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, DAC_CENTER); }tone_active是全局标志中断服务函数里先判断它是否有效无效时直接输出中心电平。这样做的好处是中断不会被关闭DAC始终保持稳定输出只是没有双音叠加。如果因为进临界区造成中断丢失后续样本会出现一段缺口接收端更容易判断成拨号结束。4. 反向验证用C语言写Goertzel解码器检验DAC0_DTMF1输出4.1 为什么用Goertzel而不是FFT做DTMF发送端的人往往需要验证DAC0产生的到底是正确的双音还是只是近似正弦的某个波形。把DAC0输出直接送到ADC再用FFT频谱分析当然可以但FFT要拿到某个频率点的幅度需要完整变换代码量大缓冲区占用也高。Goertzel算法是FFT的简化版本它针对单个频率计算频谱能量一个DTMF检测需要8个频率也就是并行跑8个Goertzel滤波器。对于只有8kHz采样率、40ms符号长度的小型嵌入式场景Goertzel比FFT更直观占用的RAM也小得多。每个滤波器只需要3个状态变量8个频率一共24个变量用来做C语言程序验证非常方便。4.2 解码器C源码与参数设置下面给出一个能够识别DTMF按键的Goertzel解码器核心代码。采样率为8kHz每个符号取160个样本即20ms足以稳定检测频率。#include stdint.h #include math.h #define DTMF_SAMPLES 160 #define DTMF_SR 8000 static const float dtmf_freqs[8] { 697.0f, 770.0f, 852.0f, 941.0f, 1209.0f, 1336.0f, 1477.0f, 1633.0f }; static float goertzel_mag(uint16_t freq_hz, const uint16_t *samples) { float coeff 2.0f * cosf(2.0f * 3.14159265358979323846f * freq_hz / DTMF_SR); float s0 0.0f, s1 0.0f, s2 0.0f; for (int i 0; i DTMF_SAMPLES; i) { s0 (float)samples[i] coeff * s1 - s2; s2 s1; s1 s0; } float power s1 * s1 s2 * s2 - coeff * s1 * s2; return power; }这段代码的核心是每次迭代更新3个状态变量迭代结束后用s1和s2计算该频率上的能量。注意coeff是对每个目标频率独立的不能复用。power的值与采样点数有关直接把8个频率的power相互比较即可不需要归一化因为所有频率的采样长度一样。判断按键时取低频组里power最大的频率和高频组里power最大的频率再查矩阵表。4.3 用DAC0回环到ADC的实测流程在开发板上做验证最直接的办法是用杜邦线把PA4DAC0输出接到ADC输入引脚。ADC也按8kHz同步采样采样到160个点后调用8次goertzel_mag打印每个频率的能量值。下面是一个轻量级的判定流程。配置ADC为单通道连续采样模式触发源和DAC定时器共用减少相位漂移。每收到160个样本调用8次Goertzel函数。找出低频组和高频组的能量最大值判断是否都超过阈值。若两个最大值都超过总能量的60%则认为检测到有效按键。如果有连续多个20ms窗口检测到相同结果才最终确认避免误触发。实际测试时DAC0直接输出到ADC可能会因为电平太接近满量程而削波最好把DAC幅度降到512或者用电阻分压把信号衰减到ADC的中间电平附近。这样测试出来的频谱会更干净。5. 参数整定与常见坑DAC0_DTMF1在真实项目中的几个细节5.1 把源码设置成可变拨号序列工程里如果只有固定一个按键音写死step_low和step_high就行。但实际项目中经常要拨一串号码比如“12345#”。我一般会准备一张符号表把DTMF频率步进也存进去。下面是一个精简的做法用行和列索引查找预先算好的16位相位步进。这样C语言源码里就不需要每次按键都用64位除法中断中只做查表。static const uint16_t dtmf_step_low[4] { phase_step_for(697), phase_step_for(770), phase_step_for(852), phase_step_for(941) }; static const uint16_t dtmf_step_high[4] { phase_step_for(1209), phase_step_for(1336), phase_step_for(1477), phase_step_for(1633) };调用时按对应按键的行列号取出两个步进写入全局变量step_low_now和step_high_now然后开启tone_active。5.2 两个容易出问题的点直流过冲和低通滤波第一个坑是DAC输出中心电平不是0V而是电源电压的一半。如果把DAC0直接接到不需要偏置的后级电路会一直叠加一个直流分量。常见解决方案是在DAC输出后面串联一个电容隔直流电容选1uF到10uF之间再对地接一个10kΩ电阻构成高通滤波。第二个坑是矩形输出经过RC滤波后会有相位延迟如果DTMF符号之间的间隔太短前一个音的拖尾会混进下一个音的检测窗口。所以我建议双音时长至少40ms符号间静音50ms。还有一个细节容易被忽略查找表只有64点输出波形并不是理想正弦会带有阶梯状谐波。给DAC输出端加一个截止频率在3kHz左右的RC低通滤波器能明显减少高频杂音。电阻选1kΩ电容选0.1uF截止频率约为1.6kHz已经落在DTMF频段之外但还要保证1600Hz附近不衰减太多所以如果对波形要求高改用二阶有源滤波器更稳妥。调整DAC幅度时记得同步修改DAC_AMPLITUDE并重新检查叠加后的最大值有没有超过4095。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →