☰
Mixly图形化编程:while与do…while循环的选用与实战拆解
2026/10/2 5:49:44 网站建设 项目流程

开头先聊个我实际教学里遇到的场景。讲循环结构那节课,有个学生做按键控制LED的小实验,他把按键读取放在“重复执行”积木里,想让“按住按键时灯亮,松开灯灭”。结果烧录上去,灯完全不听使唤,要么一直亮着,要么一亮一灭乱跳。他把程序改了好几次,换成“重复执行10次”来闪烁,发现也不行——按下按键后灯倒是闪了,可闪完10次就停了,再按也不管用。

这个问题非常典型,根子就在于:固定次数的循环解决不了“什么时候结束由外部条件说了算”的需求。你需要的不是一个“循环几次”的积木,而是一个“让条件决定何时结束”的积木。这就是米思齐(Mixly)图形化编程里while和do……while这对循环积木登场的时机。

这篇是米思齐系列教程的第七篇,专门把while和do……while讲透。我会直接从图形化积木的使用讲起,逐个拆解它们生成的Arduino代码长什么样、两种循环的执行逻辑有什么区别、实际项目里该怎么选,最后把我这几年来带学生容易踩的坑和排查思路一并交代清楚。无论你是刚接触图形化编程的初学者,还是已经在用米思齐带课的创客老师,这篇文章应该都能帮你把这对循环用明白。

1. 为什么有了“重复执行”,还要单独讲while

1.1 “固定次数循环”的先天不足

米思齐的控制分类里,最常用、也是前面几篇教程里反复出现过的,是这两块积木:“重复执行”和“重复执行 n 次”。

“重复执行”对应的是Arduino代码里的while(1),也就是无限循环,它会从程序运行一直跑到断电为止。所以Arduino程序里必须有delay()这类延时函数夹在中间,不然这个循环会把CPU全部占满,其他事都干不了。

“重复执行 n 次”对应的是for循环,它的核心特征是:循环次数在进入循环之前就定死了。比如“重复执行10次”,它不关心外界温度是多少、按键是否被按下、串口有没有数据,反正就10次,跑完退出。

这两个积木覆盖了“永远循环”和“固定次数循环”两大类需求,但实际项目里还有第三类需求:循环的次数事先不知道,要等某个条件满足才结束。

举几个最常见的例子:

  • 等待按键被按下,但用户什么时候按,代码不可能提前知道。
  • 等待串口收到数据,数据什么时候来,取决于外部设备。
  • 等待电机转到指定位置,转多久能到位,和负载、电压都有关。

这三种情况,用“重复执行 n 次”根本没法描述,因为循环次数是运行过程中才确定的。这就是while积木存在的原因。

1.2 while积木在米思齐里的位置和长相

米思齐的“控制”分类里,和while相关的积木一般有两块。不同版本界面略有区别,但核心就这两类:

  • “当 [条件] 为真时,重复执行 [代码]”:这就是标准的while(条件)结构。
  • “执行 [代码],直到 [条件] 成立”或者“先执行一次,再判断 [条件] 决定是否继续”:这就是do……while结构,注意它在米思齐里的名字可能带“直到”两个字,和上面的“当……时”容易弄混,这点后面我会单独讲。

这块积木的下拉框或者拼接槽里,可以放任何布尔类型的判断条件。米思齐的逻辑分类里提供了:

  • 比较运算:等于、不等于、大于、小于等。
  • 逻辑运算:且、或、非。
  • 传感器判断:比如“数字引脚读取到高电平/低电平”。

也就是说,判断条件不是只能填数字,而是可以直接把硬件状态作为条件。这是图形化编程相比纯文本编程对新手更友好的地方,拖拽积木就能写出“等待按键按下”这种逻辑。

1.3 和if判断的本质区别

很多初学者会把while和if搞混。它们的区别用一句话就能说清:if只检查一次条件,while不但检查,还会在检查通过后反复执行循环体,直到条件不成立。

我常给学生打的比方是:if就像一个门卫,你走到门口,他看一眼你的证件,合格就放你进去,你就进去了,不会再回来问他第二遍。while也像一个门卫,但你进去之后他还会每隔一会儿跑过来检查一次,只要你的证件依然合格,他就让你继续待在里面;一旦不合格,立刻把你请出去。

所以while的循环体里,一定要有能让条件发生变化的代码,否则这个循环就会变成死循环。比如条件是“数字引脚为高电平”,那循环体里要么重新读一次引脚,要么用延时等外部状态变化,要么把条件里用到的变量改掉。这是使用while最重要的一条纪律,后文我会专门用一个章节讲死循环。

2. while循环的实操拆解:从按键检测到串口等待

2.1 第一个实验:用while实现“按住亮,松开灭”

回到开篇那个学生的需求。他想做的是:按住按键,LED保持点亮;松开按键,LED熄灭。用if配合delay确实能做,但会有响应延迟的问题。用while做最直观。

逻辑设计很简单:

  1. 读数字引脚2的电平。按键按下时引脚读到低电平(因为按键通常是接地接法)。
  2. 如果引脚为低电平,就点亮LED。
  3. 如果引脚为高电平,就熄灭LED。
  4. 不断重复这个过程。

但这里有个关键点:如果用“重复执行”加上delay(10)来轮询引脚,LED在松开后会延迟10毫秒才反应,这在按键场景下其实够用。但如果按键按下后还要同时做其他事,比如播放声音、驱动电机,你就发现轮询加上延时,耽误的事越来越多。while循环可以写得更精准:

当 [数字引脚2读取 == 低电平] 成立时循环执行: 点亮 数字引脚13 等待10毫秒

这个程序的执行逻辑是:进入循环前先判断引脚状态;如果是低电平,就执行点亮LED并延时10毫秒;执行完再回到循环开头重新判断。只要按键一直按着,引脚一直是低电平,这个循环就会一直转。一旦松开,条件不成立,循环退出,然后执行循环后面的代码,比如熄灭LED。

对应生成的Arduino代码大致是这样的:

while (digitalRead(2) == LOW) { digitalWrite(13, HIGH); delay(10); } digitalWrite(13, LOW);

注意到没有,while循环体执行完,程序会立刻回到条件判断处,不会有额外的等待。这个“立刻重新判断”的特性,让while比“用if加延时去判断”响应更及时。

2.2 超时等待:给while加一个“安全出口”

上面那个按键程序有个潜在风险:如果按键卡住一直没松开,while循环就会一直出不来。这在一段演示代码里问题不大,但在实际项目里,比如工业设备上,一个卡死的循环可能让整台设备无响应。

解决办法是给while加超时退出机制。Arduino里可以用millis()函数获取系统运行时间,它是一个不断增加的毫秒计数器。利用它,可以给等待条件加上时间上限:

变量 起始时间 = millis() 当 [数字引脚2读取 == 低电平] 并且 [millis() - 起始时间 < 3000] 成立时循环执行: 点亮 数字引脚13 等待10毫秒

这段逻辑的意思是:按键按下后LED点亮,但如果持续了3秒还没松开,就不再等待,直接退出循环去执行后面的代码。用米思齐可以这样拼:逻辑分类里选择“且”,左边放“数字引脚2读取 == 低电平”,右边放“millis() - 起始时间 < 3000”。

这里有个米思齐特有的坑:我见过不少初学者把millis()读取放在循环体外面,只在进入循环前读了一次。这样millis() - 起始时间在循环过程中其实在变化,但因为起始时间是固定的,它其实是可以正常工作的。真正的问题是有人把起始时间 = millis()写在了循环体里,结果每次循环都把起始时间重置,millis() - 起始时间永远接近0,超时判断永远不成立,循环就变回死循环了。这个错误很隐蔽,烧录后现象就是“等了很久也没退出来”。

正确的做法:初始化放在循环之前,循环体里只更新其他变量,不动起始时间。

2.3 串口等待:while的经典应用

Arduino和上位机或者其他设备通信时,经常需要“等待串口收到数据再继续”。这在纯文本Arduino编程里是这样写的:

while (Serial.available() == 0) { // 空循环,等待数据到达 } int incoming = Serial.read();

这段代码的含义是:只要串口接收缓冲区里没有数据,就一直在原地等待。直到真的有数据来了,Serial.available()的值大于0,条件不成立,退出循环,然后执行Serial.read()读取。

在米思齐里,串口相关的积木在“串口”分类里。判断方式要用到“串口可用”或者“串口有数据”这类功能块。不同版本叫法不一样,有的叫“串口可利用字符数”,你就用“串口可利用字符数 > 0”作为判断条件,逻辑和上面完全一样。

这个场景下while的优势特别明显:固定次数循环做不到“等到有数据为止”,无限循环又没法继续往下执行,只有while能实现“原地等待,条件满足后继续”的流程。

实际使用时要小心一个细节:串口等待循环一旦加上,程序就会停在那等,如果数据源突然断电或者连接断开,这个等待就是永远等下去。所以和按键场景一样,我给串口等待也都加超时。这是我做实际项目时的一个习惯,宁可多写几行代码,也不让设备因为等待而卡死。

3. do……while:先执行再判断的执行时机差异

3.1 米思齐里的do……while积木长什么样

标准C语言里的do……while结构是这样:

do { // 循环体 } while (条件);

它的特点是:先无条件执行一次循环体,然后再判断条件是否成立。如果条件成立,就回到循环体继续执行;如果条件不成立,就退出循环。所以do……while的循环体至少会执行一次。

在米思齐中,这块积木通常表现为“执行 [代码],直到 [条件] 成立”或者“先执行一次 [代码],如果 [条件] 成立则重复执行”。相比while积木的“先判断后执行”,do……while的积木形态明显是先堆代码块,再挂判断条件。如果你手头版本界面上有“直到”两个字,大概率就是do……while。

3.2 一句话说清while和do……while的本质区别

它们的区别就一句话:while先问再做,do……while先做再问。

用排队打饭来类比:while是食堂阿姨先看你有没有饭卡,有卡才给你打饭;do……while是阿姨先给你打一勺饭,然后问你“还要不要再来一勺?”,要就继续打,不要就结束。所以do……while里,不管条件是否成立,第一勺饭你是肯定能吃到的。

这个区别在代码层面的体现就是:

  • 如果条件一开始就不成立,while的循环体一次都不会执行。
  • 同样的条件下,do……while的循环体至少会执行一次。

3.3 什么时候必须用do……while

实际项目里“必须用do……while”的场景比很多人想象中多。最典型的是**“先发送请求,再等待回应”**这类交互逻辑。

比如你想让Arduino通过串口发送一个指令给舵机控制器,然后等待舵机回应“到位”。如果用while先判断“是否收到回应”,那么第一次判断时回应还没到,while直接跳过,后面就永远不会发送指令了。正确的顺序是:先发送指令,再进入等待回应的循环,这天然就是do……while的执行逻辑。

代码逻辑如下:

执行 [串口发送指令] 循环直到 [串口收到回应] 成立: 等待10毫秒

生成代码类似:

Serial.println("MOVE"); do { delay(10); } while (Serial.available() == 0);

这里循环体里的delay(10)是为了防止空转,给串口收数据留时间。要注意的是,这里循环体先执行了一次延时,然后才判断条件。如果串口数据在第5毫秒就到了,那第10毫秒判断时条件已经成立,循环退出,不会白白多等。

另外一个场景是菜单展示。你想先显示一次菜单,然后询问用户是否需要重新显示。如果用户回答“是”,就再显示一次;如果回答“否”,就退出。这个流程无论用户第一次想不想看菜单,菜单都需要先显示一次,所以do……while是最贴合逻辑的。

3.4 do……while在米思齐里的使用陷阱

这里必须提醒一下:在米思齐里,do……while和while积木很容易选错。原因前面说过,米思齐的do……while版本积木可能带“直到”这个词,有些初学者一看“直到条件成立”,就以为是while,拼反了。

我见过最典型的一个错误案例:学生想做“等待按键松开后再执行下一个动作”,他选了一块“循环直到 [数字引脚2 == 高电平]”的积木,然后在循环体里放了“熄灭LED”。他的预期是“按键按着灯亮,松开熄灭”。实际烧录后,LED从头到尾都是灭的。为什么?因为程序一启动,循环体先执行了一次“熄灭LED”,然后再判断“引脚2是否为高电平”。启动瞬间按键没按,引脚是高电平,条件成立,又回到循环体里继续熄灭LED,形成一个死循环。灯当然一直是灭的。

这个错误的关键就在于:do……while会先无条件执行一次循环体,如果循环体里的操作有副作用(比如改变引脚状态),这个“无条件的一次”可能会造成和预期不符的现象。所以用do……while之前,一定要问自己一句:“循环体先执行一次,会不会出问题?”如果会,就应该改用while而不是do……while。

4. 实战案例:按键控制LED闪烁次数,松开立即停止

4.1 项目需求和逻辑分析

现在来做一个综合性的小项目,把上面这些知识串起来。需求是:

  1. 按键未按下时,LED保持熄灭。
  2. 按下按键后,LED开始以5Hz频率闪烁(亮100ms,灭100ms)。
  3. 闪烁过程中,如果按键松开,LED立刻停止闪烁并熄灭。
  4. 如果按键一直按着不松开,LED最多闪烁20次后自动停止。

这个项目同时用到了while的条件判断、超时退出、还有循环次数控制,是一个比较完整的综合练习。

用米思齐来拼,逻辑结构如下:

变量 起始时间 = millis() 变量 已闪烁次数 = 0 当 [数字引脚2 == 低电平] 成立时循环执行: 点亮 数字引脚13 等待100毫秒 熄灭 数字引脚13 等待100毫秒 已闪烁次数 = 已闪烁次数 + 1 如果 [已闪烁次数 >= 20]:退出循环

这个结构里,while的外层条件是“按键按下”,进入循环后LED开始闪烁。每次闪烁完,计数变量加1,当计数达到20次就退出。同时,因为每次循环结束都会重新判断外层条件,只要按键一松开,“数字引脚2 == 低电平”不成立,循环也会退出。

这里要特别注意:“退出循环”这个积木在米思齐里是有的,一般叫“跳出循环”或“中断循环”,对应C语言里的break。它用来强制跳出循环体,不理会外层条件。这样,即使按键一直按着,闪烁次数达到20次,也会主动退出。

4.2 生成的Arduino代码解读

上述积木生成的代码大致如下:

unsigned long 起始时间 = millis(); int 已闪烁次数 = 0; while (digitalRead(2) == LOW) { digitalWrite(13, HIGH); delay(100); digitalWrite(13, LOW); delay(100); 已闪烁次数++; if (已闪烁次数 >= 20) { break; } } digitalWrite(13, LOW);

我把这段代码贴给学生看的时候,会专门指出两个地方:

第一个是break的作用。它配合已闪烁次数 >= 20的条件,实现了“最多闪烁20次”的约束。如果把break去掉,闪烁次数这个变量就不再有意义,程序只会依赖按键松开退出循环,20次的限制就丢了。米思齐里“跳出循环”积木在控制分类里,有的版本叫“中断”,实际上就是C语言的break。

第二个是循环结束后的digitalWrite(13, LOW)。这一步很关键,它的作用是确保退出循环时LED一定是灭的。因为有两种退出路径:一种是从break跳出来,此时LED状态是灭的(因为闪烁序列刚好执行完一次,最后一步是熄灭);另一种是按键松开,条件检查失败后退出,这时LED可能刚好处于亮的状态。如果不加最后这一句,LED可能保持点亮,看起来就像是程序出错了。

4.3 为什么用while而不是“重复执行20次”

有学生问:闪烁20次,干嘛不用“重复执行20次”这个积木,还要费劲用while加break?

区别在于**“松开立即停止”这个需求**。“重复执行20次”一旦开始,就会固执地执行完20次,中途无论按键变成什么状态,它都不管。你想让它提前退出,就得在每次循环里用if判断按键状态,再配合break才能实现。而while的做法更自然:外层条件天然包含“按键是否按下”这个判断,每次循环都会检查,松开就退出。

所以这里其实有两条路线都能实现:

  • 路线A:外层while管按键,内层用计数和break管次数。
  • 路线B:外层用“重复执行20次”管次数,内层用if加break管按键。

两条路线最终效果差不多,但路线A的可读性更好,尤其是条件多的时候。因为它把“按键是否按住”这个关键条件放在最外层,一眼就能看出程序的主要逻辑。我个人的建议是,哪个条件的变化频率更高,哪个更关键,就放在最外层的循环条件里。这个项目里按键状态变化是核心交互,所以把它放在外层是合理的。

4.4 加入超时保护,防止按键卡住

当然,这个项目还可以进一步加超时保护。如果按键的物理结构出问题,一直卡在按下状态,路线的while循环会一直等到天荒地老。给外层while加一个“并且持续按住不超过5秒”的判断,会更健壮。

变量 起始时间 = millis() 当 [数字引脚2 == 低电平] 并且 [millis() - 起始时间 < 5000] 成立时循环执行: ...闪烁逻辑...

加超时有两种思路:一种是用 millis() 判断持续按住的时间,另一种是判断总的闪烁次数。这个项目里已经有了次数限制,其实已经起到了超出的作用。我加起始时间这个变量,是为了让“LED最多闪烁20次”失效的情况下(比如把20改成无穷大),程序依然不会卡死。实际项目中,我提倡多重保险——条件判断、次数限制、时间限制,至少要有两个兜底。

5. 死循环与条件写反:这两类坑我年年见

5.1 死循环的三大根因

我在带学生做项目这几年,程序卡死的案例遇到太多了。while相关的死循环,根因基本就三个:

根因一:条件永远为真。比如你把digitalRead(2) == HIGH作为循环条件,但实际电路里引脚2接的是按键的接地端,按下时是低电平。那么程序里“条件为真”的情况永远不会出现(因为引脚基本一直是低电平),要么循环根本进不去,要么进去了就出不来,取决于你条件是怎么写的。

根因二:循环体里没有改变条件的动作。比如以“引脚2 == 低电平”为条件,但循环体里只是点亮熄灭LED,从来不重新读取引脚状态,甚至没有延时。那这个条件在循环过程中永远不会发生变化,循环只能永远执行下去。哪怕你按了按键松开,程序根本不知道。

根因三:变量在错误的位置被更新。这个前面讲超时等待时提过,把起始时间 = millis()写在循环体里,导致时间差永远接近0。同理,如果你在循环体里把计数变量重新赋值为0,那循环次数永远达不到退出条件。

排查死循环,我有一套固定的思路:

  1. 先看条件里用到的变量或引脚,是否在循环体里有更新动作。
  2. 再看更新的位置,是在循环开始前、循环体里、还是循环之后。
  3. 用串口打印来辅助判断,在循环体里放一个Serial.println("in loop"),看它是否一直在打印。
  4. 如果不想动程序,直接看米思齐右上角的“代码”窗口,检查生成的C代码里循环条件、变量更新的位置。

5.2 一个经典逻辑题:x和y的循环演变

热词里有个典型的while分析题:x=90; y=100; while(y>0) { if(x>100) { x=x-10; y--; } else { x++; } }。这个例子非常适合用来练习while逻辑,我把它改造一下在米思齐里复现。

原始题稍微有点烂尾(括号没写对),修正后的逻辑是:x初始90,y初始100,循环条件是y>0。循环体里判断x是否大于100:如果大于100,x减10,y减1;否则x加1。问循环结束时x和y各是多少。

你可以在米思齐里用变量积木把这个逻辑拼出来,然后在循环体结束后用串口打印x和y的值。运行结果你会发现:y从100递减,只在前几次循环(x从90加到100的过程中,y不变)之后才开始递减。x的值会反复在100上下震荡:加到101,减回91,再慢慢加到101,再减回91。最终y减到0时,x=91。这个循环体一共执行了多少次,可以数一下:x从90加到101需要11次,然后每10次y减一次,y总共要减100次,再加上前11次,大概1110次左右,具体数字有兴趣可以自己在米思齐里跑一下。

这个例子有意思的地方在于,它揭示了while循环的一个常见形态:一个条件(x>100)决定变量的变化方向,另一个条件(y>0)决定循环何时结束。项目里很多状态机的雏形就是这个样子——某个变量在设定范围内反复震荡,直到外部条件改变才停止。

5.3 条件写反:等于和不等于的界限

另一个高频坑是把条件写成“反”的。最常见的是把数字引脚2 == 低电平写成数字引脚2 != 低电平。差一个“不”字,程序行为可能完全相反。

我在课堂上会让做按键实验的学生故意犯一次这个错,然后观察现象。写成不等号之后,按键按下时循环体反而不执行,松开时反而执行。很多学生会困惑“怎么回事”,这时候再带着他们看生成的C代码,一眼就发现了。

排查这类问题有个技巧:如果程序行为恰好和预期相反,先检查你有没有用“非/不等于”这类取反逻辑。在米思齐的逻辑积木里,判断引脚有时需要自己手动拖一个“非”块,拖错位置很容易出问题。

6. 循环积木选择心法:三类循环到底怎么选

6.1 三类循环的对比

到这一步,米思齐里常用的循环基本都见过了。我把它们放在一起梳理一下。

循环类型米思齐积木形态对应C代码执行特点适用场景
无限循环“重复执行”while(1)永远执行,需靠break或断电退出主程序主体、持续监测
条件循环“当 [条件] 成立时循环”while(条件)先判断后执行,可能一次都不执行等待条件满足、按键检测
直到循环“执行直到 [条件] 成立”do{...}while(条件)先执行后判断,至少执行一次先操作再等待回应
次数循环“重复执行 n 次”for(初始化;条件;更新)循环次数固定确定的重复次数

这个表格是我上课时必放的。我会强调一点:循环类型本身没有优劣,选型的关键是“退出条件什么时候能确定”。

如果退出条件在程序开始前就确定了,比如“做10次”,“每个引脚轮流亮一遍”,用次数循环最简单。如果退出条件取决于外部输入,比如按键、串口、传感器读数,那就要用条件循环。如果无论条件如何,某段代码都必须先执行一次,再谈是否重复,就用直到循环。

6.2 我的三步判断法

面对一个具体需求,我通常按三步来决定用哪个循环:

  • 第一步:循环次数能提前确定吗?能,就用“重复执行 n 次”。
  • 第二步:次数不能确定,但需要先判断条件再执行吗?是,就用while。
  • 第三步:无论条件如何,都要先执行一次循环体吗?是,就用do……while。

这套判断流程很简单,但非常实用。我带的学生按照这个顺序去思考,基本不会再对着需求发懵。

但要注意,实际项目里需求和循环结构往往不是一一对应的。一个while循环可以同时包含次数判断(通过break)、超时判断(通过millis)、状态判断(通过引脚)。前面的LED闪烁项目就是这种“混合式循环”,外层条件管按键,内层break管次数。这种情况,我的建议是:先画出循环退出条件有哪些,再决定哪个做外层判断,哪个做内层break。

6.3 进阶思路:用while做简单的状态机

最后分享一个进阶用法,很多做交互装置的同学会用到:用while配合变量实现一个简单的状态机。

比如有一个装置,按一下按键进入“待机模式”,再按一下进入“工作模式”,长按3秒进入“休眠模式”。传统的做法是用if判断模式变量,再配合不同的执行块。用while写法会不一样:用一个大while包裹整个程序,内部用break在模式切换时退出当前循环,跳到另一个模式的循环块。

while (true) { while (digitalRead(2) == HIGH) { // 待机模式:等待按键按下 delay(10); } // 切到工作模式 while (digitalRead(2) == LOW) { // 工作模式:执行任务,直到按键再次按下 delay(10); } // 回到待机 }

这个写法视觉上非常直观——程序在不同模式的循环块之间跳转,每一个模式的代码都在自己的while块里,逻辑隔离清晰,不会像一堆if嵌套那样容易乱。

当然,状态机的最正规写法是用switch-case或者函数指针,但那是文本编程里的进阶话题。在图形化编程阶段,用while加break模拟状态切换,已经能让初学者感受到“程序按状态组织”的思维方式了。

最后聊点实际的。我在这篇教程里放的案例,很多都是从真实课堂上沉淀出来的。学生最容易出错的地方,不是积木找不到,而是条件判断里“状态更新”的缺失——要么忘了在循环体里更新变量,要么把更新时间写错位置。所以我每次讲while,都会在黑板上画一个圈,圈外标“条件判断”,圈内标“执行代码”,反复强调:圈内的代码会改变圈外的条件,才能让这个圈真正转动起来。

如果你在米思齐里拼while或者do……while时遇到“程序卡死”、“循环退不出来”、“进入不了循环”,先按第5节的排查思路走一遍,大部分问题都能定位。多烧录几次,多观察几次生成的C代码,你很快就能建立起对这两种循环的直觉。到时候你就会发现,循环结构其实一点都不难,它就是程序和你对话的方式——程序在等一个条件,条件变化了,它就知道该干什么了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询