ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

DSP28335用ePWM驱动蜂鸣器播放音乐的原理与实现

2026/9/11 5:57:57 拓冰建站 浏览量
DSP28335用ePWM驱动蜂鸣器播放音乐的原理与实现 简介面向DSP28335学习者的蜂鸣器音乐生成实验资源基于TI TMS320F28335浮点DSP平台利用PWM和定时器控制蜂鸣器发出不同频率的声音帮助理解音乐合成原理。压缩包内仅1个C语言源文件约1KB代码包含DSP时钟与GPIO初始化、PWM模块预分频器和比较值设定、音符频率映射、乐谱顺序播放函数以及用于节拍控制的中断服务程序结构清晰适合入门级二次开发。当前已有431人学习下载。这份实验示例通过音符与PWM周期转换搭建起从乐谱到实际发声的完整链路读者既能直接烧写验证蜂鸣器播放旋律也能修改数组中的音符序列来更换音乐或增加按键、数码管等外设进一步扩展成电子琴应用。对DSP28335初学者、嵌入式系统课程设计和音频实验项目而言是一份兼顾原理讲解与代码实现的实用参考。1. 用 DSP28335 驱动蜂鸣器播放音乐比你想象得更像一道数学题“DSP蜂鸣器实验.zip”解压后很多人最先找的是 main.c 里的播放函数想在 DSP28335 上听到一段旋律。但把板子电源一开听到的往往是一声刺耳的“滴”而不是哆来咪。原因集中在两个地方蜂鸣器是有源还是无源以及 PWM 的频率寄存器算对了没有。这个实验真正要练的并不是“让蜂鸣器响”而是把一个音符的频率映射成 DSP28335 片上 ePWM 模块的周期寄存器值再用定时中断按节拍切换。这套思路适用性很强凡是需要产生方波音频的 MCU 项目都能用适合正在调 C2000 板子、或者想搞清楚定时器占空比和音高关系的工程师。2. 先搞清楚蜂鸣器吃的是什么信号有源、无源、频率与占空比2.1 有源蜂鸣器只能“响”无源蜂鸣器才能“唱歌”标题里同时出现“蜂鸣器”和“音乐”这基本就限定在无源蜂鸣器上。有源蜂鸣器内部带了一个固定频率的振荡源上电就自激你改变输入方波频率其实没什么用它只会以固定音调发声而无源蜂鸣器内部没有振荡器引脚上加多高的频率方波它就发对应频率的声音所以才能播放出不同的音符。区分方法很简单看底面是否有 PCB 线圈、用万用表电阻档测两个引脚有源蜂鸣器的电阻通常比较高而且直流供电会直接响无源蜂鸣器直流供电只会“咔”一声。DSP28335 的 GPIO 输出能力虽然能点个 LED但直接驱动蜂鸣器电流不够也容易拉垮端口电平常见做法是加一个 NPN 三极管做开关比如 S8050基极串联 1kΩ 电阻接 GPIO 或 PWM 引脚集电极接蜂鸣器负极发射极接地蜂鸣器正极接 3.3V 或 5V 电源。如果蜂鸣器额定电压是 5V注意 DSP28335 的 GPIO 是 3.3V三极管开关电路不受这个限制但 PWM 高电平幅度要能可靠开启基极。实验板上常见的是把蜂鸣器一脚接 3.3V另一脚经三极管接地这样用一个普通 GPIO 或 ePWM 输出口控制。2.2 音高本质是方波频率音符频率表怎么来的音乐里标准音 A4 是 440Hz比它高一个八度的 A5 是 880Hz相邻半音之间频率相差 2 的 1/12 次方倍。十二平均律下每个音符频率可以用公式 f 440 × 2^((n - 69)/12) 算出来其中 n 是 MIDI 音符编号A4 对应 69。实际写程序时不需要现场算直接把 C 大调或跨几个八度的音符频率存成一张表用查表法换回中断里的执行时间。下面是一段常见的音符频率表我一般只取中间四个八度音名频率(Hz)周期(us)TBPRD(150MHz/16分频)C4261.6338223580D4293.6634053188E4329.6330342840F4349.2328632680G4392.0025512388A4440.0022732128B4493.8820251896C5523.2519111788表中的周期是一整个方波的周期不是半周期。用 ePWM 递增计数输出时一个完整的计数周期本身就对应方波的一个周期所以 TBPRD 直接用 TBCLK 频率除以目标频率再减一不需要乘二。很多刚接触 C2000 的人在这里多除一次导致所有音符都低了八度。2.3 DSP28335 ePWM 与 GPIO 方波生成能力对照DSP28335 有 6 个 ePWM 模块编号 EPWM1 到 EPWM6每个模块有两路输出 ePWM xA 和 ePWM xB其中 A 输出经常被用来做硬件 PWM。ePWM 的时基时钟 TBCLK 由系统时钟 SYSCLKOUT 经 CLKDIV 和 HSPCLKDIV 两级分频得到系统时钟典型值是 150MHz。TBPRD 寄存器是 16 位最大值 65535因此不设置分频的话最低可输出频率大约是 150MHz / 65535 2289Hz这个频率远高于低音音符所以必须给 ePWM 时基分频。与之对比直接翻转 GPIO 虽然不需要配 PWM 外设但翻转动作依赖软件每次翻转要执行指令频率高了中断或循环会占掉大量 CPU。ePWM 是硬件行为计数器到周期后自动翻转电平不占用 CPU所以音乐播放这类需要长时间连续输出的场景硬件 PWM 是更可靠的选择。用 GPIO 输出方波的代码在 3.1 节里会展开这里先放一个最小配置EALLOW; GpioCtrlRegs.GPAMUX1.bit.GPIO0 0; // GPIO0 作为普通 IO GpioCtrlRegs.GPADIR.bit.GPIO0 1; // 输出 EDIS; GpioDataRegs.GPADAT.bit.GPIO0 1; // 拉高这段代码本身不产生音频但它展示了 GPIO 与 ePWM 在外设复用上的区别。后面用 PWM 时GPAMUX 要置 1而不是 0这个位是最容易漏的配置。3. 在 DSP28335 上用 ePWM 产生音符频率的三种方案3.1 方案一直接翻转 GPIO逻辑最简单但频率上限低把 GPIO 接到蜂鸣器控制器然后写一个延时函数轮流拉高拉低就能产生方波。这个方案最大的问题在于延时函数本身不精确DSP28335 主频 150MHz一条指令几个周期简单循环能做到微秒级延时但一旦中断开启、或者编译器优化级别变化频率就飘了。再加上播放音乐要持续几百毫秒到几秒主循环会被完全占死点个 LED 都要等音乐停。所以这个方案只适合验证蜂鸣器是否有声音不适合作为实验交付代码。三种方案对比如下方案频率精度CPU 占用适合场景GPIO 翻转加延时低受中断和优化影响高播放期间无法做其他事验证蜂鸣器好坏ePWM 递增计数高硬件自动输出几乎为零音乐播放主方案定时器中断刷新 ePWM高支持节拍切换中中断开销较小播放完整乐谱3.2 方案二ePWM 递增计数模式推荐作为播放主通道ePWM 的递增计数模式比较适合方波蜂鸣器驱动。设置 TBPRD 决定输出频率设置 CMPA 为 TBPRD 的一半获得 50% 占空比。关键代码如下使用 EPWM1A 输出到 GPIO0#include DSP2833x_Device.h #include DSP2833x_Examples.h void EPwm1_Init(Uint16 tbprd) { EALLOW; GpioCtrlRegs.GPAMUX1.bit.GPIO0 1; // GPIO0 复用为 EPWM1A GpioCtrlRegs.GPADIR.bit.GPIO0 1; // 配置为输出 EDIS; EPwm1Regs.TBCTL.bit.CTRMODE TB_COUNT_UP; // 递增计数 EPwm1Regs.TBCTL.bit.HSPCLKDIV TB_DIV4; // 高速分频 4 EPwm1Regs.TBCTL.bit.CLKDIV TB_DIV4; // 低速分频 4合计 16 分频 EPwm1Regs.TBPRD tbprd; // 周期值 EPwm1Regs.CMPA.half.CMPA tbprd 1; // 50% 占空比 EPwm1Regs.AQCTLA.bit.CAU AQ_SET; // 递增计数到 CMPA 时置高 EPwm1Regs.AQCTLA.bit.PRD AQ_CLEAR; // 到 TBPRD 时拉低 EPwm1Regs.TBCTL.bit.PRDLD TB_IMMEDIATE; // 周期值立即生效 }这段代码里HSPCLKDIV 和 CLKDIV 各 4 分频TBCLK 150MHz / 16 9.375MHz。此时 TBPRD 9,375,000 / 频率 - 1。以 A4 440Hz 为例TBPRD 21307在 16 位范围内。换成 1000Hz 以上音符时TBPRD 降为 9375精度尚可。分频越大TBPRD 数值越大对低频越友好但两个相邻半音之间的寄存器差值会变密高频时一个 LSB 会造成较大频偏所以实验中使用 16 分频是比较均衡的选择。3.3 方案三定时器中断刷新 TBPRD动态改频率ePWM 寄存器在运行时随时可以改但要让音乐按节拍走不能只改一次。常见做法是开一个 CPU 定时器中断中断周期设为节拍的最小单位比如一个八分音符 125ms。每次中断里取出乐谱中的下一个音符重新计算 TBPRD 并写入。这里要注意写 TBPRD 前如果计数器正在运行可能造成一个周期异常建议先设置 TBCTL 的 PRDLD 为立即装载并在写周期值后不重置计数器这样下一个周期自然按照新频率走。下面这段代码示意了中断服务函数里的处理框架interrupt void cpu_timer0_isr(void) { if (note_index note_count) { stop_buzzer(); // 播完停止 return; } note_t note score[note_index]; if (note.freq 0) { stop_buzzer(); // 休止符 } else { EPwm1Regs.TBPRD (Uint16)(TBCLK_HZ / note.freq - 1); EPwm1Regs.CMPA.half.CMPA EPwm1Regs.TBPRD 1; start_buzzer(); } note_index; }这个中断服务里不能塞延时也不能塞像 LCD 刷新那样的重操作否则节拍会漂。如果后续要加节拍时长控制可以在中断里维护一个递减计数器而不是用多个不同周期的定时器。这里 TBCLK_HZ 应该和你初始化时设置的分频一致建议定义一个宏避免在 ISR 里做浮点除法DSP28335 没有硬件浮点单元F28 系列里有 FPU但整数除法也够用。4. 把乐谱变成数组音符编码、节拍与播放调度4.1 音符的周期值查表还是现算在 DSP28335 里每个音符对应一个 16 位周期值。为了省去中断里的除法可以预先把频率换算成 TBPRD 值存表用音符编号做数组下标。定义一个结构体数组每个元素包含频率或者直接用周期值、时值计数、是否休止。用周期值更直接因为中断里只需要赋值不需要除法用频率值则代码可读性更好对变量和常量的区分也更直观。我的经验是乐谱数组里存音符编号和时值另外单独建一张周期值表中断里查表这样两者优点都占。下面是一个简化版本的乐谱表示typedef struct { Uint16 tone; // 音符编号0 表示休止 Uint16 duration; // 节拍时长单位是定时器中断周期个数 } note_t; const note_t score[] { {C4, 2}, {C4, 2}, {D4, 2}, {C4, 2}, {F4, 4}, {E4, 4}, {C4, 2}, {C4, 2}, {D4, 2}, {C4, 2}, {G4, 4}, {F4, 4}, // 这里省略后续小节 };这里的 tone 用枚举或者宏定义C4、D4 这些符号在头文件里被定义为周期表的索引。duration 的单位取决于定时器中断的频率比如中断每 62.5ms 一次那么一个四分音符的时值是 4八分音符是 2。这样设计的好处是主循环只负责推进播放状态乐谱数据和播放速度分离调整播放速度只需要改定时器周期或中断里的累加逻辑。4.2 节拍与中断周期的换算节拍控制是蜂鸣器音乐实验里最多坑的地方。很多人直接在音符结构体里写 delay_ms(500)看起来播放正常但 CPU 被占断。正确的是把节拍换成中断次数。假设歌曲速度是每分钟 120 拍一拍的时间是 500ms如果定时器中断周期是 10ms那一拍就是 50 次中断。按八分音符 250ms 计算时值是 25。中断周期不宜太短否则频繁进中断挤占主循环也不宜太长否则无法表示三十二分音符。10ms 到 20ms 是一个舒适区。乐谱数组里统一存中断次数而非毫秒可以避免运行时反复做乘除。同时要注意DSP28335 的 CPU Timer0 中断默认开启时PieCtrl 和 IER 都要配置否则 ISR 永远不会进。一个可用的定时器初始化片段如下EALLOW; PieVectTable.TINT0 cpu_timer0_isr; EDIS; ConfigCpuTimer(CpuTimer0, 100, 1000); // 10ms 周期 CpuTimer0Regs.TCR.all 0x4000; // 启动定时器 IER | M_INT1; PieCtrlRegs.PIEIER1.bit.INTx7 1; EINT;参数说明ConfigCpuTimer 的第二个参数是 CPU 频率单位 MHz第三个参数是周期单位微秒所以 100MHz 对应 10ms。DSP28335 使用 150MHz 时这里要写成 ConfigCpuTimer(CpuTimer0, 150, 10000)。4.3 事件驱动播放主循环只做状态机切换完整播放逻辑可以拆成两个状态播放中和停止。主循环检查播放按钮或串口命令收到播放请求就把 note_index 清零打开定时器中断最后一个音符播完后在 ISR 里关闭中断主循环通过一个标志位得知播放结束。示例中的状态变量如下volatile Uint16 play_active 0; volatile Uint16 note_index 0; void start_playing(void) { note_index 0; play_active 1; // 开启CPU Timer0中断 } interrupt void cpu_timer0_isr(void) { if (!play_active) return; Uint16 tone score[note_index].tone; set_buzzer_freq(tone); note_index; if (note_index sizeof(score)/sizeof(score[0])) { play_active 0; stop_buzzer(); } }这种做法下主循环可以干别的事例如扫描按键、刷新数码管。中断里只做很小的任务不会造成音乐卡顿。注意 set_buzzer_freq 里如果 tone 是 0要调用 stop_buzzer 关闭 PWM 输出因为 0 赫兹不是一个合法频率。休止符不能忽略否则两个音符之间没有间隙会连成一片。5. 排错为什么板子上的蜂鸣器只会“滴”不会“哆来咪”5.1 先确认蜂鸣器类型再检查驱动电路程序写完后最常见的现象是固定“滴”声音乐启动后没变化。第一步先看蜂鸣器型号有源蜂鸣器输出恒定频率改 PWM 频率没有用实验要求无源蜂鸣器如果板子上焊的是有源直接换器件或者在原理图上飞线。第二步测量驱动电路用万用表二极管档测三极管 B-E 结是否正常基极电阻有没有焊错位。GPIO 高电平能不能把这个三极管打开取决于电阻大小和 GPIO 驱动能力我一般用 1kΩ 到 4.7kΩ阻值太大基极电流不够蜂鸣器声音会小。下面这个表格总结了常见现象和排查方向现象可能原因优先检查一直“滴”不变化有源蜂鸣器、PWM 频率未更新蜂鸣器类型、ISR 是否触发无声三极管没导通、PWM 没输出基极电阻、GPAMUX、EPwm 时钟声音刺耳频率值比预期高一倍TBPRD 计算是否少了减一播放一半卡住中断进不去或数组越界数组长度、IER、PieCtrl声音断续中断被高优先级打断ISR 执行时间、中断嵌套5.2 示波器看 PWM 引脚和蜂鸣器引脚波形最直接的定位方法是示波器。先测 DSP28335 的 GPIO0/PWM 引脚设置为播放状态后应该能看到 50% 占空比的方波而且频率会按音符变化。如果没有波形检查 GpioCtrlRegs.GPAMUX1 是否选对GPIO0 对应 EPWM1AGPIO1 对应 EPWM1BGPIO2 对应 EPWM2A别名不要搞混。再看 EPwm1Regs.TBCTL 的时钟使能位有些例程会关闭 ePWM 时钟来省电忘了开自然没有输出。最后测蜂鸣器两端如果 PWM 端波形正常但蜂鸣器端没有问题在驱动电路。另一个常见坑是波形有但频率不对。用示波器的频率测量功能读实际频率和乐谱频率对照。如果实际频率恰好是目标频率的一半检查是不是用了上下计数模式又除了一次二如果频率变化没有规律检查定时器中断是否被打断例如有更高优先级的中断长时间占用 CPU。5.3 中断没进、进得太密、节拍漂移的处理播放没有音乐但 PWM 一直保持某个频率通常是中断没触发。检查步骤CPU Timer0 初始化是否开启了 TSS 位PieCtrlRegs.PIEIER1 是否允许了 TINT0IER 是否使能 INT1以及全局中断 EINT 是否打开。在 ISR 第一行翻转一个 GPIO用示波器测量这个测试点如果中断正常会有规律的脉冲。如果声音断断续续可能是中断优先级问题。DSP28335 的中断嵌套默认不开启ISR 里如果调用了较长函数下一个中断会被推迟。解决办法是把播放状态更新做得更短或者把音符周期表查表操作挪到中断外。节拍漂移还有一个来源在 ISR 里执行除法。整数除法几十个周期还好但浮点除法会占用上百周期主频 150MHz 下可能让 10ms 中断超时避免在 ISR 里写浮点运算。提示在开发阶段把音符切换时翻转另一个 GPIO用双通道示波器同时看这个 GPIO 和 PWM 输出能让中断触发情况和频率变化一目了然。5.4 蜂鸣器音量小或失真排查音量小通常是驱动电压不够或占空比太小。50% 占空比是方波能量最大的配置少于一二十个百分点声音就会明显变小。失真常见于把无源蜂鸣器接到了 5V 电源却用了 3.3V 管脚驱动三极管三极管没有完全饱和集电极压降增大。对实验板来说直接用 3.3V 供电、选低导通电压的三极管比追求 5V 供电更稳定。如果想调节音量后续可以改 PWM 占空比但要注意无源蜂鸣器在极低占空比下声音会断续降到 10% 以下基本就听不到。6. 从“会响”到“好听”非阻塞播放、占空比音量与验证技巧6.1 非阻塞播放是实验的隐藏加分项DSP 音乐实验最容易被忽略的要求就是“播放同时还能响应其他操作”。前面用的定时器中断方案已经做到了中断驱动播放主循环留白。再进一步可以在中断里维护一个累计中断次数计数器每次中断递减到零再切换到下一个音符这样同一个音符即使持续好几拍也不需要 CPU 在中间反复改写寄存器。volatile Uint16 note_duration_count 0; interrupt void cpu_timer0_isr(void) { if (note_duration_count 0) { note_duration_count--; return; } if (note_index note_count) { stop_buzzer(); return; } note_t n score[note_index]; set_buzzer_freq(n.tone); note_duration_count n.duration - 1; note_index; }这个写法把“切音”动作从“每个中断都发生”改成“只有时值耗尽才发生”中断开销更均匀。6.2 PWM 占空比做音量和“PWMDAC 音乐”的边界让蜂鸣器声音有大有小可以修改 CMPA 为 TBPRD 的百分比也就是占空比调制。无源蜂鸣器对脉宽变化比较敏感常用于音量渐变或模拟双音。热词里常出现的“PWMDAC 音乐”其实走的是另一条路线用 PWM 输出经过低通滤波恢复成模拟音频信号再接功放和喇叭才能播放真正意义上的音乐。蜂鸣器实验的核心是方波频率不是连续信号如果想延伸可以在 DSP28335 上同时使用两路 ePWM一路发声一路输出 32kHz 左右载波做简易 DAC但这是后话了。6.3 三个快速验证播放是否准确的技巧播放结束后把音符序列通过串口打印出来与乐谱对比能确认中断调度有没有漏音。第二用示波器在蜂鸣器输入引脚触发单次波形测一个音符的持续时间和乐谱时值比较误差应在定时器周期数量级内。第三用手机上的调音器 App 对着蜂鸣器看显示的音名是否落在 C4、D4 之类的位置如果偏高或偏低半个音检查 TBCLK 计算里分频是否按初始化写入的寄存器值一致。实践里大多数刺耳或跑调问题最后都落在分频和 TBCLK 宏定义不一致上。把 TBCLK_HZ 写成一个宏和初始化寄存器放在同一个文件修改分频时两处同步更新可以避免一类隐蔽错误。做到这一步这个 DSP 蜂鸣器实验就已经超过了大多数“能响就行”的例程。本文还有配套的精品资源点击获取