
简介在电子乐器与DIY硬件开发领域如何将物理敲击精准转换为数字信号一直是开发者关注的热点。压电传感器作为一种低成本、高灵敏度的振动检测元件常被用于自制电子打击垫和触发装置。其原理是利用压电效应将机械形变转化为电压信号再通过模拟前端调理、ADC采样与峰值检测最终映射为标准MIDI消息。这一技术路径不仅适用于鼓模块设计也可扩展到各类打击类交互设备。本文以Drumidy为例从传感器选型、模拟电路搭建、DMA采样配置到MIDI消息封装系统拆解了基于STM32 Nucleo的鼓模块实现过程并给出了力度校准与手感调优的实用方法帮助开发者在DAW控制、舞台触发及旧鼓皮改造等场景中快速落地。1. 鼓模块 Drumidy不做整套电鼓只解决“敲击变 MIDI”这一件事把一块几十块钱的压电片贴在鼓皮、木板甚至快递纸箱上敲下去STM32 Nucleo 板子上的 Drumidy 固件就能让 DAW 里响起对应的鼓音源。这不是一套带鼓腔和支架的电鼓而是把“物理敲击 → 力度检测 → MIDI Note”这条链路做成一块开发板就能跑的模块这正是标题里“鼓模块”三个字的含义触发部分自己做音色部分交给宿主或音源。Drumidy 的输入是压电传感器或类似的模拟振动信号输出是标准 MIDI 消息。常见做法是让这块 Nucleo 枚举成一个 USB MIDI 设备接上电脑或带 USB 口的硬音源就能直接演奏。适合想自制电子打击垫、要做舞台触发装置或者只是想把旧鼓皮变成 MIDI 控制器的从业者门槛是会用配置工具生成工程、能改几行 HAL 代码。下面从选型、采样、触发判定到调手感按能复现的顺序拆开讲。2. 方案选型三种输出、三种传感器为什么我最后选这套组合2.1 输出路径USB MIDI 与 5 针 DIN 串口 MIDI 的取舍做鼓模块第一件事不是选开发板而是先定 MIDI 怎么出去。这一步定错后面接线和固件都要返工。常见做法有两个主流分支走板载 USB 口把设备枚举成标准 MIDI 类设备或者走 UART 按 31250 波特率输出 5 针 DIN 串口 MIDI。两者都叫 MIDI但对模块的物料、驱动和延迟影响完全不同。维度USB MIDIUART 串口 MIDI5针 DIN硬件开销直接用 Nucleo 板载 USB 口需额外光耦、DIN 座、接线端子主机驱动免驱主流系统直接识别老音源即插即用新设备常需转接典型延迟受 1ms USB 帧调度UART 字节级传输微秒级适合场景DAW、软音源、快速原型老式硬音源、纯硬件演出链路我一般会把需求分成两类主要接 DAW 和软音源的直接选 USB MIDI演出时现有设备只有 5 针 DIN 接口才考虑串口 MIDI。这里有个常见误用有人以为串口 MIDI 延迟低所以优先选实际上手感延迟主要来自压电片、模拟前端和触发判定USB 帧调度那 1ms 几乎感知不到别为了这个牺牲连接便利性。2.2 触发传感器压电片、膜片开关与驻极体探头的取舍触发侧才是这个项目里决定上限的部分。候选主要有三种压电片、膜片开关、驻极体麦克风头。压电片是电子鼓垫最常见的传感器理由很现实它能输出模拟力度信号敲得越重、开路电压越高上升沿可以非常陡适合做 velocity成本低面积小可以贴到各种表面上。缺点是需要模拟前端处理因为输出可能是几十伏的交流衰减振荡而且对安装面的机械耦合非常敏感。膜片开关只有开与关两个状态力度顶多做两级做出来的手感是“有/没有”而不是“轻/重”不适合鼓模块。驻极体探头捕捉的是空气振动可以做“无声鼓”但周围说话声和环境噪音都可能误触发需要很强的信号处理才压得住对入门项目不友好。所以常规方案都是压电片。选型时看两个参数直径决定灵敏度20~27mm 的圆形压电片在鼓皮场景比较均衡内电容和泄放电阻一起决定衰减时间这个放到第三章详细讲。另外注意不要买那种带蜂鸣器振荡电路的压电片内部已经帮别人加了驱动很难拆干净不适合当传感器用。2.3 组合Nucleo-F103RB 压电片 USB MIDI以及为什么没选更强的开发板开发板我选 Nucleo-F103RB不是因为它性能强而是因为它刚好够用且资料最集中。鼓模块需要的资源至少 4 路 ADC 输入、一个 USB 全速控制器、几个 GPIO 做指示灯和调试口。F103RB 正好全有72MHz 主频处理四路触发检测绰绰有余。更强的 F401/F411 频率更高但项目瓶颈从来不在 CPU而在模拟信号质量和采样节奏用不上如果是为了低功耗去选 L4 系列对一个插着 USB 线供电的鼓模块意义也不大。还要说一个很多人忽略的对比Arduino Uno 加压电片也能做触发但 Uno 的 USB 口是串口转接出来的做不了原生 USB MIDI 类设备要么靠主机端程序转发要么走串口再接外部转换器延迟和多出来的一层驱动都是麻烦。Nucleo 的 USB 是芯片自带的控制器可以直接枚举成 MIDI 设备这才是这个标题里“STM32 Nucleo 鼓模块”的核心优势。最后容易踩的细节是Nucleo-F103RB 板子上有两个 USB 口板载调试器用的那个口只能当下载和串口用不能枚举成 USB MIDI真正给应用用的是对着板边的那个用户 USB 口。很多人第一次插错口枚举不出来就以为是固件问题其实只是物理接口选错了。我最后的组合就是Nucleo-F103RB 四片 20mm 压电片 USB MIDI 输出四路分别映射成底鼓、军鼓、踩镲和强音镲后面要加垫子改配置数组就行。3. 从压电片到可用的 ADC 数字量模拟前端与采样配置3.1 压电片不能直接进 ADC限幅、泄放和偏置电路把压电片直接接到 Nucleo 的 ADC 引脚第一下敲击就可能出事。压电片开路输出可以到几十伏而且敲击后的信号是正负交变的衰减振荡ADC 引脚承受不了长期过压即使芯片有内部的输入保护那也不是用来持续当钳位用的。所以模拟前端不是可选项是必需项。我常用的前端电路是四个部分压电片两端并联一个 1MΩ 泄放电阻正极串联一个 10~100kΩ 阻尼电阻到 ADC 引脚ADC 引脚对地加两只背靠背的开关二极管或一只 3.3V 齐纳管做钳位再用两个 10kΩ 电阻分压得到 1.65V 直流偏置通过一个 100nF 电容把信号耦合进 ADC 引脚。这样敲击产生的交流信号被搬到了 ADC 量程中间正负摆幅都能采到也不会打穿引脚。元件参考值作用泄放电阻1MΩ决定余振衰减时间与压电片内电容组成 RC阻尼电阻10~100kΩ压低高频振铃、限制电流钳位背靠背二极管或 3.3V 齐纳管防过压损坏引脚直流偏置10kΩ 10kΩ → 1.65V让交流信号落在 ADC 量程中部耦合电容100nF隔离直流传递敲击瞬态这几个值会直接改变手感泄放电阻越大电压衰减越慢鼓面“余音”越明显适合踩镲那种持续振动的场景调到 220kΩ 左右信号回落更快适合底鼓那种短促音头。阻尼电阻调大能明显压制串音和振铃但也会削弱轻击力度建议先按 33kΩ 起步再根据实际敲击表现改。调试时如果发现轻击完全没反应优先检查偏置电压是不是真的在 1.65V 附近而不是先去调阈值。3.2 用 ADCDMA 连续采样最小配置代码与三个关键参数信号进了 ADC 之后采样策略决定你能否抓到那个只有亚毫秒宽的峰值。用一个定时器反复触发单次采样会让 CPU 忙于等待最常见也更省事的做法是ADC 连续转换 DMA 循环搬运到内存CPU 完全不参与数据搬运。/* CubeMX 中把 ADC1 设为连续转换开启 DMA 循环模式 */ #define ADC_BUF_LEN (4 * 128) /* 4 通道每通道 128 个样本 */ static uint16_t adc_buf[ADC_BUF_LEN]; static void adc_start_sampling(void) { ADC_ChannelConfTypeDef sConfig {0}; uint32_t ch_map[4] {ADC_CHANNEL_0, ADC_CHANNEL_1, ADC_CHANNEL_2, ADC_CHANNEL_3}; for (int i 0; i 4; i) { sConfig.Channel ch_map[i]; sConfig.Rank i 1; sConfig.SamplingTime ADC_SAMPLETIME_1CYCLE_5; /* 最快采样 */ HAL_ADC_ConfigChannel(hadc1, sConfig); } HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN); }逻辑说明这个函数把 ADC 的四个通道配置为规则扫描模式然后启动 DMA。HAL_ADC_Start_DMA 会让 ADC 进入连续转换状态每扫描一轮四个通道就连续写入四个样本到缓冲区缓冲区写满后 DMA 自动回到头部继续覆盖等于一块永远在刷新的环形缓冲。主循环只需要按自己的节奏去读缓冲里的新数据不需要关心转换时序。三个关键参数要注意。第一是采样周期F103 的 ADC 时钟最高约 14MHz1.5 cycle 采样加上 12.5 cycle 转换单通道约 1us四路扫描一轮约 4us每路仍然有 250k 样本/秒抓鼓击峰值绰绰有余。第二是缓冲区长度决定了批处理的时间粒度一般取 128 到 256 个字太短会频繁进中断太长会明显增加一次敲击到处理的等待时间。第三是扫描通道顺序DMA 缓冲里的排列顺序和通道扫描顺序一致读取时不能搞错否则会把踩镲的信号当成底鼓。还有一个容易忽略的点F103 的 ADC 是单 ADC 依次转换不是四路真正并行通道之间有约 1us 的时间差对鼓垫触发来说完全感知不到。3.3 峰值检测与力度值 velocity阈值、释放线与防重复锁定采样到手后不要去做平均值或 RMS那是给连续音频信号用的。鼓击触发要的是“找尖峰”一旦样本越过触发线就开始记录最大值直到信号回落到释放线这个最大值就是力度值 velocity 的原始输入。typedef enum { TRIG_IDLE, TRIG_HIT, TRIG_LOCK } trig_state_t; typedef struct { trig_state_t st; uint16_t lock_cnt; } pad_trig_t; /* 每调用一次处理一个新样本返回 1 表示这一击结束 */ uint8_t pad_trig_update(pad_trig_t *t, uint16_t sample, uint32_t *peak) { if (t-st TRIG_IDLE) { if (sample BASE HIT_THRESHOLD) { t-st TRIG_HIT; *peak sample; } } else if (t-st TRIG_HIT) { if (sample *peak) *peak sample; /* 持续找真正的尖峰 */ if (sample BASE RELEASE_THRESHOLD) { /* 回落到释放线 */ t-st TRIG_LOCK; t-lock_cnt LOCK_SAMPLES; /* 以样本数计时 */ return 1; } } else /* TRIG_LOCK */ { if (t-lock_cnt 0) { /* 快击时不必等锁结束更强的信号可以直接抢占 */ if (sample BASE HIT_THRESHOLD) { t-st TRIG_HIT; *peak sample; } else { t-lock_cnt--; } } else { t-st TRIG_IDLE; } } return 0; }参数建议HIT_THRESHOLD 先取 80~120 LSB大约是在 1.65V 基线上方 0.06~0.1V太小会把周围震动也算进来太大轻击直接变哑。RELEASE_THRESHOLD 取 HIT_THRESHOLD 的一半左右形成滞回如果两个值设成一样信号在临界区来回抖动就可能把一次敲击拆成两次触发。LOCK_SAMPLES 是按采样率换算出的锁定时长5ms 在 250k 采样率下约 1250 个样本。窗口小于 3ms同一个敲击的余振可能再触发一次大于 8~10ms快速连击会吞音。注意这段代码是一个垫的逻辑实际有四个垫就建四个 pad_trig_t 结构体分别喂对应通道的样本。敲下去如果状态机根本没离开 IDLE优先回头查 3.1 的模拟电路和阈值设置而不是查 MIDI 发送。4. 固件主流程把触发事件装进标准 MIDI 消息4.1 谁来做触发检测DMA 缓冲与中断的配合有四个垫就要决定样本从 DMA 缓冲到状态机的通路。一种错误做法是在每个 ADC 样本完成中断里直接调用状态机那样中断频率会到 250kHz光进出中断就能吃掉大量 CPU。正确做法是让 DMA 先攒一批样本在 DMA 半传输/全传输完成中断里置一个标志主循环再批量处理。处理缓冲时最容易错的是数据排列规则扫描模式下一个转换周期产出的四个样本是按通道 0、1、2、3 顺序交错存放的。你不能把缓冲理解成“前 128 个全是通道 0”那样取出来的全是错位数据。常见做法是按四的倍数取每四个字分别对应四个垫。#define PAD_NUM 4 #define CH_PACKAGE (PAD_NUM) /* 每轮扫描的样本数 */ void process_pad_batch(uint16_t *buf, uint32_t start, uint32_t count) { uint32_t peak; for (uint32_t n 0; n count; n CH_PACKAGE) { for (uint32_t ch 0; ch PAD_NUM; ch) { peak 0; if (pad_trig_update(pads[ch].trig, buf[start n ch], peak)) { send_note_from_pad(pads[ch], peak); } } } }逻辑说明外层循环按扫描轮数走内层循环取当前轮里四个通道的样本。start 是这一批数据在缓冲里的起始偏移半满时取 0全满时取半缓冲长度。count 是这一批的样本总数。这样一个函数就把“DMA 来的数据”变成“MIDI 发送事件”主循环里调用频率不高每次处理的指令数是可预测的。4.2 从 peak 到 MIDI 消息通道、音符号与力度映射状态机吐出的 peak 是 ADC 原始码下一步要映射成 MIDI 消息。鼓模块的映射很直接每个垫固定一个 MIDI 通道、一个音符号力度值映射到 1~127。发送前要做一次范围校验peak 低于最低有效值就直接丢弃避免轻触产生一个力度为 1 的鬼音。typedef struct { uint32_t adc_ch; /* 0~3 */ uint8_t midi_note; /* 音符号 */ uint8_t midi_ch; /* 0~15 */ pad_trig_t trig; } pad_cfg_t; static pad_cfg_t pads[PAD_NUM] { { 0, 36, 9, {0} }, /* 底鼓 */ { 1, 38, 9, {0} }, /* 军鼓 */ { 2, 42, 9, {0} }, /* 踩镲 */ { 3, 49, 9, {0} }, /* 强音镲 */ }; static void send_note_from_pad(pad_cfg_t *p, uint32_t peak) { uint8_t vel velocity_map(peak); /* 见第 6 章校准 */ uint8_t pkt[4]; if (vel 1) return; /* 无效弱击丢弃 */ pkt[0] 0x0C; /* cable0 CIN3字节通道消息 */ pkt[1] 0x90 | (p-midi_ch 0x0F); /* Note On */ pkt[2] p-midi_note; pkt[3] vel; usb_midi_write((uint8_t *)pkt, 4); /* 端点写函数需按工程补齐 */ }逻辑说明0x90 是 MIDI 的 Note On 状态字节和通道号按位或之后就是完整的消息头。这里通道用 9对应 MIDI 习惯里的打击乐通道大多数 DAW 会把第 10 通道自动映射到鼓组音色如果你的宿主或硬音源不这么配改成实际通道就行。usb_midi_write 是一个抽象接口实现就是你工程里 USB MIDI 端点的写函数注意 USB MIDI 事件包固定是 4 字节和传统 3 字节 MIDI 消息不一样。关于 Note Off如果你接的是采样类鼓音源通常只发 Note On 就够音源会按自身采样长度自然释放如果接的是合成器音源建议在锁定期结束后的几十毫秒补发一条 Note On velocity 0避免出现“长音挂住”。这个可以做成配置项默认关掉能少踩很多坑。4.3 优先级与时序为什么触发检测要快USB 发送要让优先级更高调度方面我采用这样的安排ADC DMA 完成中断只负责置标志实际检测放在主循环入口处检测到触发后马上把 4 字节事件包交给 USB 端点队列USB 中断负责把队列真正发到总线。这样 ADC 侧中断非常短USB 侧又不被长任务阻塞。优先级上USB 中断的优先级要比 ADC DMA 更高因为 USB 有严格的帧时间约束晚一个帧调度就可能让主机认为设备异常DMA 晚一点处理只会丢少量样本下一次敲击还能补上。反过来设置就很容易出现打鼓时电脑端报 USB 设备错误甚至掉线的现象。延迟估算一下从敲击发生到样本进入缓冲最坏要等一个扫描周期约 4us批量检测按 128 个样本、半满中断计算约 0.25msUSB 发送受 1ms 帧调度限制。端到端最坏约 1.3ms多数情况下在 1ms 以内。这个水平对打鼓足够。如果实际体感延迟超过 5ms不要先怀疑 USB去查主循环里有没有长时间的串口打印、延时函数或阻塞式 LED 扫描。5. Drumidy 常见问题排查5 个让我返工过的坑5.1 使劲敲反而没声音中等力度却正常现象轻敲有声音重敲反而触发不了或者力度值明显掉下来。原因压电片输出幅度过大被模拟前端的钳位电路或 ADC 引脚的保护结构削平了顶部。信号变成一段平顶波后峰值检测读到的反而不是真实最大值更糟的情况是钳位二极管进入导通把信号短路到电源轨状态机一直处于触发中等信号真正回落到释放线已经是几十毫秒之后后续击打全部被锁定窗口吞掉。解决先在串口打印原始 ADC 峰值观察重击时是否出现长时间贴合在 3.3V 或 0V 附近的值。如果是把信号衰减加大比如把阻尼电阻从 10kΩ 提到 47kΩ或者降低 1.65V 偏置电路的增益不要靠调高阈值掩盖那是把“末级削顶”变成“阈值躲开”治标不治本。5.2 敲一个垫旁边垫也跟着发声现象底鼓敲下去军鼓通道也同时发一个 note鼓点变成双音甚至多音。原因两方面叠加。一方面是机械串音四片压电片共用一块安装底板敲击振动通过板子传到另一边另一方面是电气串音飞线并排走、共用一根较长的地线信号在 ADC 输入端之间耦合。解决先做机械隔离每片压电片的安装面不要直接贴在同一块大面积硬板上中间加一层软垫或独立小支架。电气上让每个通道的地线单独回到电源地不要串成一条链。软件上补一个“先到先得”互锁在 3~5ms 窗口内只放行第一个触发的通道其余通道即使越过阈值也丢弃直到窗口结束。5.3 快速连续击打会掉音现象打十六分音符或装饰音时隔两三个音就丢一个节奏听起来不完整。原因防重复锁定的窗口设得太长或者 RELEASE_THRESHOLD 设得太低状态机一直停在 TRIG_LOCK把真正的新击打当成余振挡掉了。另一种情况是锁定窗口本来没问题但状态机不支持“新峰值抢占”必须等锁结束才能回到 IDLE快击时第二下正好落在锁内。解决把 LOCK_SAMPLES 压到 3~5ms 对应样本数同时在 TRIG_LOCK 分支里允许“更强的信号直接抢占”也就是代码里已经写的锁定期内如果样本再次越过 HIT_THRESHOLD立刻回到 HIT 状态重新追踪峰值。这样既防余振又不牺牲快击响应。5.4 插到电脑上枚举不出 MIDI 设备串口却能打印现象调试串口正常输出但 DAW 和系统设置里都看不到这个 MIDI 设备。原因最常见是 USB 插错口用了调试器那个口其次是 USB 需要的 48MHz 时钟没配好或 USB 线只支持充电不支持数据。还有一个容易被忽略的是工程没有真正启用 MIDI 类描述符只启用了 CDC 串口类所以系统识别出的是串口而不是 MIDI 设备。解决先确认插的是用户 USB 口而不是调试口再查时钟树里 USB 时钟是不是 48MHz然后把线换成标准数据线交叉验证。如果前两步都正常就在配置工具里重新生成一个带 USB MIDI 类的工程先不写任何鼓逻辑直接枚举排除固件的干扰。5.5 velocity 忽大忽小同一力度敲十次差 30 以上现象同一只手同一力度反复敲同一个垫DAW 里录到的力度值离散度很大手感不可控。原因ADC 参考电压受 USB 供电波动影响压电片每次落点位置和角度不同峰值采样窗口太短导致偶发漏采。其中参考电压波动是很多人忽略的一点板载 3.3V 是经过板子上的稳压器出来的USB 电流波动会直接反映到转换结果上。解决先保证供电稳定不要同时让 USB 给外部 LED 矩阵或电磁阀供电代码里用芯片内部的参考电压通道做一次比例校准能压掉一部分电源漂移。算法上把峰值追踪窗口稍微拉长让“落点后的一小段区间内取最大”而不是只依赖单个样本。最后在物理上固定击打区域的标记能明显减少落点带来的离散。6. 把 Drumidy 从“能响”调成“好打”校准流程与手感验证6.1 双阈值校准先定下限再定上限校准的目的不是把力度曲线做平而是先搞清这个物理垫片的真实动态范围。我一般会在调试串口里打印原始 ADC 峰值然后做两组采样用很轻的力度敲十次记录最小值用能接受的最大力度敲十次记录最大值。触发阈值放在“最小有效值再往下留 20% 余量”的位置避免日常轻微接触就触发。最大值映射到 MIDI 力度 127中间做线性映射之后再叠手感曲线。6.2 力度曲线线性不够给打击乐加一条非线性映射压电片的输出并不是线性的重击时电压增长会变缓如果直接线性映射重击区域容易堆在一块。常见做法是加一条幂函数曲线先把 peak 映射成 0~1 的线性值再乘方最后放大到 1~127。幂指数取 0.6~0.8 时轻击区灵敏度提高重击区缓慢饱和打起来更像真实鼓皮。实现就一行校准的时候把指数做成可配置参数现场听感觉再调。6.3 验证用 DAW 录像和重复度测试做最终验收最后验证我不看代码只看数据。把固定重量的笔杆从固定高度落到鼓垫上重复录 20 次 MIDI看力度值的均值、最大值和最小值离散度控制在 ±5 以内才算合格。再用手机慢动作同时拍下落点和 DAW 的 MIDI 事件灯数帧数估算端到端延迟做到 1~2 帧以内基本就是 USB 帧调度在起作用不是触发慢。我自己的习惯是每次改完模拟电路的参数先看原始峰值范围再看 MIDI 力度分布最后才上手试打。这个顺序让我少走了很多弯路Drumidy 这个项目越往深处做越会发现手感好坏七成在模拟前端和校准流程而不是在主频和代码技巧。希望帮到你。本文还有配套的精品资源点击获取