Arduino项目技术问询:如何实现4位7段数码管滚动显示文本与压电蜂鸣器同步播放音乐
解决Arduino同步执行数码管滚动和蜂鸣器乐曲的问题
嘿,我懂你遇到的问题了!现在你的代码里song()和text()是串行执行的——先把文本滚动完,才会开始播放乐曲,这是因为这两个函数都是「阻塞式」的,一旦运行起来就会占着CPU不放,直到自己执行完。要让它们同步跑起来,我们得把这两个功能改成非阻塞的,简单说就是把长任务拆成一小步一小步,每次主循环loop()里只做一点点,这样两个任务就能交替进行啦!
为什么现在不能同步?
你当前的代码逻辑是:
if (digitalRead(knopTekst) == LOW) { song(); text(); }
这里song()会先从头到尾执行(比如里面用了delay()来控制音符时长),完全结束后才会调用text(),同样text()里的delay()也会阻塞整个程序,所以两个功能只能先后跑,没法同步。
核心解决方案:非阻塞编程
Arduino是单线程的,没法真正意义上的“并行”,但我们可以用「时间片轮转」的思路:用millis()(获取当前系统运行毫秒数)来跟踪每个任务的执行时间,每次循环只检查是否到了该执行任务下一步的时间,如果到了就执行一小步,然后立刻让出CPU给另一个任务。
步骤1:改造蜂鸣器乐曲为非阻塞
原来的song()可能是这样的(阻塞式):
void song() { int melody[] = {262, 294, 330}; int durations[] = {4,4,4}; for(int i=0; i<3; i++){ int duration = 1000/durations[i]; tone(buzzerPin, melody[i], duration*0.9); delay(duration); // 这里会阻塞! noTone(buzzerPin); } }
我们要把它改成非阻塞,需要保存当前播放到哪个音符、上次播放的时间:
// 全局变量,保存蜂鸣器状态 int melody[] = {262, 294, 330, 349, 392, 440}; // 你的乐曲音符 int noteDurations[] = {4, 4, 4, 8, 8, 4}; // 你的音符时长(4=四分音符) int currentNote = 0; unsigned long lastNoteTime = 0; const int buzzerPin = 9; // 你的蜂鸣器引脚 // 非阻塞的音符播放函数:每次只检查是否要播放下一个音符 void playNextNote() { // 如果乐曲已经播放完,重置索引 if (currentNote >= sizeof(melody)/sizeof(melody[0])) { currentNote = 0; noTone(buzzerPin); return; } unsigned long now = millis(); long noteDuration = 1000 / noteDurations[currentNote]; // 计算当前音符时长 // 检查是否到了切换音符的时间 if (now - lastNoteTime >= noteDuration) { noTone(buzzerPin); // 播放当前音符,留10%的间隙避免连音 tone(buzzerPin, melody[currentNote], noteDuration * 0.9); // 更新时间和当前音符索引 lastNoteTime = now; currentNote++; } }
步骤2:改造数码管滚动为非阻塞
同样,原来的text()里的delay()会阻塞,我们改成用millis()跟踪滚动时间:
// 全局变量,保存数码管滚动状态 String scrollContent = "HELLO"; // 你要滚动的文本 int currentCharPos = 0; unsigned long lastScrollTime = 0; const long scrollInterval = 500; // 每500ms滚动一次 // 假设你用的是4位数码管,比如TM1637,这里替换成你的数码管初始化逻辑 // #include <TM1637Display.h> // TM1637Display display(CLK_PIN, DIO_PIN); // 非阻塞的滚动更新函数:每次只更新一次显示 void updateScrollText() { // 如果文本滚动完,重置位置 if (currentCharPos >= scrollContent.length()) { currentCharPos = 0; return; } unsigned long now = millis(); // 检查是否到了滚动时间 if (now - lastScrollTime >= scrollInterval) { // 这里根据你的数码管库调整显示逻辑,比如显示当前开始的4个字符 String displayText = scrollContent.substring(currentCharPos, currentCharPos + 4); // 补空格确保显示4位(如果剩余字符不足) while(displayText.length() <4) displayText += " "; // display.showString(displayText); // 替换成你的数码管显示代码 // 更新时间和滚动位置 lastScrollTime = now; currentCharPos++; } }
步骤3:主循环里协调两个任务
最后,我们用一个状态变量来控制是否开启同步执行,在loop()里交替调用两个非阻塞函数:
bool isRunning = false; const int knopTekst = 2; // 你的按钮引脚 void setup() { pinMode(knopTekst, INPUT_PULLUP); // 按钮用上拉输入,按下时为LOW pinMode(buzzerPin, OUTPUT); // display.setBrightness(7); // 初始化数码管 } void loop() { // 检测按钮按下(防抖处理:等待按钮释放再触发) if (digitalRead(knopTekst) == LOW) { while(digitalRead(knopTekst) == LOW); // 等待按钮松开 isRunning = !isRunning; // 切换运行状态 // 如果开始运行,重置两个任务的初始状态 if (isRunning) { currentNote = 0; lastNoteTime = millis(); currentCharPos = 0; lastScrollTime = millis(); } else { noTone(buzzerPin); // display.clear(); // 停止后清空数码管 } } // 如果处于运行状态,每次循环都执行两个非阻塞函数 if (isRunning) { playNextNote(); updateScrollText(); } // 这里可以放其他你的任务代码 }
关键注意事项
- 彻底去掉所有
delay():delay()会阻塞整个程序,是同步执行的最大敌人,全部用millis()替代 - 状态变量要全局:需要保存每个任务的当前进度(比如播放到哪个音符、滚动到哪个字符),所以要用全局变量或者类成员变量
- 按钮防抖:一定要处理按钮的抖动,否则一次按下可能会触发多次状态切换
- 适配你的硬件:上面的示例是通用逻辑,需要根据你用的数码管库(比如TM1637、MAX7219)和蜂鸣器引脚调整具体代码
这样改造后,每次loop()循环都会同时检查乐曲和滚动文本是否需要更新,两个功能就会同步运行啦!
内容的提问来源于stack exchange,提问作者Ruben g.
相关产品推荐
相关产品推荐

