很多人第一次接触电子双缝衍射,都会觉得这个实验自带“玄学”气质——明明是单个电子一个个打到屏上,最后却显示出明暗相间的干涉条纹。真实实验要在真空腔里做电子枪、用微通道板探测,门槛不低,但用Matlab的GUI功能做一个可交互的模拟器,普通笔记本上几分钟就能跑起来。这篇文章就把我实际做这个项目的过程完整拆开:物理模型怎么建、界面怎么布局、滑块回调怎么写、参数调的时候有哪些容易看走眼的地方,全部按我踩过的坑来讲。
这个项目适合谁?物理专业学生做课程设计、搞光学或量子力学教学的老师做演示工具,还有刚学Matlab GUI想找个小而完整的练手项目的开发者。整个项目从零到能跑,大概需要一个晚上;如果你想在这个基础上加动画、加随机粒子分布,再花一个晚上也就够了。
1. 先想清楚:电子双缝衍射到底在模拟什么
1.1 从“单缝”到“双缝”:干涉和衍射是一回事吗
很多教程把双缝衍射直接写成“干涉”两个字,这对物理出身的人没什么歧义,但工程思维的人容易卡壳:干涉是干涉,衍射是衍射,怎么双缝出来一个公式就全包了?
实际处理上,双缝衍射是“单缝衍射的包络”和“双缝干涉的调制”两者相乘。每个缝本身相当于一个次波源,缝的宽度决定了单个缝的衍射行为;两个缝之间的距离决定了这两列次波之间的相位差,从而产生干涉条纹。所以总强度公式长这样:
[ I(\theta) = I_0 \cdot \left(\frac{\sin \beta}{\beta}\right)^2 \cdot \cos^2 \alpha ]
其中:
- ( \alpha = \frac{\pi d \sin\theta}{\lambda} ) ——双缝间距 d 贡献的相位差
- ( \beta = \frac{\pi a \sin\theta}{\lambda} ) ——单缝宽度 a 贡献的相位差
- θ 是观察屏上相对于中心轴线的偏转角
用生活类比来理解:单缝衍射像是“给画面加了包络滤镜”,决定了整体亮暗分布的大形状;双缝干涉像是“在这个包络框内部切出一圈圈等间距的细条纹”。没有单缝衍射因子,干涉条纹在两端会一直亮下去,不符合实际;没有双缝干涉因子,整个图样就是一个宽的中央亮斑,根本看不到那些细密的明暗交替。
GUI 里我设计成两个子图联动:上面画二维伪彩条纹图,就是那种从屏幕正面看过去的明暗分布;下面画一维强度曲线,就是沿屏幕上一条直线切出来的亮度大小。这样“包络”和“调制”的关系一眼就能看出来:一维曲线上的粗包络是单缝衍射的结果,包络里面密密麻麻的小峰则是双缝干涉的结果。
1.2 电子也有波长:德布罗意假设怎么落到公式里
模拟光双缝衍射时波长就是可见光波长,比如 600 nm 的钠黄光,直接代公式就行。但做电子衍射时,波长不是查表查出来的,而是由加速电压算出来的。这就是德布罗意假设的关键——电子动量 p 和物质波波长 λ 之间满足:
[ \lambda = \frac{h}{p} = \frac{h}{\sqrt{2m_e e V_a}} ]
h 是普朗克常量,( m_e ) 是电子静质量,e 是元电荷,( V_a ) 是加速电压。
把常数代进去可以化简成一个工程上常用的式子:
[ \lambda (\text{nm}) = \frac{1.226}{\sqrt{V_a (\text{V})}} ]
也就是说,加速电压 100 V 时波长大约是 0.1226 nm,电压到 10 kV 时波长缩到 0.01226 nm。跟可见光比,电子的波长小了四五个数量级,这也是为什么电子显微镜能分辨那么小的结构——衍射条纹的疏密直接跟波长挂钩。
这个公式必须写进模拟器里,不能在 GUI 里固定一个波长。因为实际操作时,自然调节的物理量是“加速电压”,不是波长。你调节电压,内部换算成波长,然后再送去算衍射分布。这样界面上的参数才符合实验室习惯。
1.3 参数怎么定:缝宽、间距、电压的真实取值范围
模拟也得讲基本法,不能随手设两个离谱参数让公式跑出正弦波来。我实际做下来,发现参数范围选不好有两种结果:要么条纹太密根本看不清,要么包络宽得像一块打满的白板,整个图形没有任何可读性。
推荐一组直接能出好效果的初始参数:
- 缝宽 a:50 nm。这相当于普通双缝实验里“缝很窄”的情况,单缝衍射的中央亮带能撑开整个观察范围。
- 缝间距 d:200 nm。d/a = 4,中央包络内大概能看到 3~5 个强度峰,数量刚刚好,再多就眼花。
- 加速电压 ( V_a ):200 V,对应波长约 0.0867 nm。加上屏距(试样到观察屏的距离)L = 1 m,最外侧一级衍射极小出现在英寸量级的偏转距离上,适合全屏显示。
这里有个容易忽略的点:观察屏坐标用的是“屏上位置 x”而不是“偏转角 θ”。屏幕上每个像素位置换算关系是 ( x = L \tan\theta ),小角度近似下可以直接 ( x = L\theta )。我代码里直接按 ( L=1 ) m 把所有衍射结构换算成毫米尺度显示,坐标轴标注单位是 mm,看着直观多了。
2. 界面设计:怎么把参数控住
2.1 GUIDE还是App Designer
这个项目如果放在三年前,我会推荐 GUIDE,拖控件快,写回调直接。但现在新版 Matlab 已经不再扩展 GUIDE 了,新装 R2020b 之后的版本打开 GUIDE 总是弹个警告。所以干脆用 App Designer,代码结构更清晰,回调函数是自动生成的事件函数名,不容易写乱。
有个小差异要注意:App Designer 里坐标轴对象是app.UIAxes,不是传统的axes句柄。你写惯了 GUIDE 的代码,拿到 App Designer 里第一行plot(handles.axes1, ...)就会报错。初始化时先把两个坐标轴的NextPlot属性设好,后面刷新图的时候才不会不断叠加图形对象。我的做法是在startupFcn里调一次更新函数,初始化界面永远先有图显示。
2.2 布局方案
模拟器的界面不需要花哨,布局核心就是——参数调节区放左边,显示区占右边大面积。我实际排的控件清单:
- 加速电压滑条:范围 50 V 到 5000 V,对数刻度,默认 200 V。电压跨越两个数量级,用线性刻度的话低电压区域根本没法精准调节。
- 缝宽滑条:范围 20 nm 到 200 nm,默认 50 nm。
- 缝间距滑条:范围 50 nm 到 800 nm,默认 200 nm。
- 三个数值显示框:滑条旁边实时显示当前值,同时允许直接输入精确值。
- 两个坐标轴:上方画二维条纹伪彩图,下方画一维强度曲线。
- 一个“重置默认参数”按钮,误调乱了可以一键拉回来。
App Designer 里滑条默认没有刻度,但你在滑条旁边加一个EditField控件,既能显示又能输入,比自带刻度实用得多。滑条调节的手感很关键:电压用对数刻度,缝宽缝距用线性刻度。如果都套线性,电压从 50 调到 5000,旋钮稍微一动就过了,精度很差。
2.3 回调数据流
GUI 编程最怕回调里各写各的,数据流乱掉,后面改需求直接崩溃。这个项目的数据流逻辑其实非常简单:
滑块或输入框数值变化 → 更新对应显示字段 → 计算德布罗意波长 → 调用衍射强度计算函数 → 刷新两个坐标轴图像
我把这个流程统一封装成一个函数updatePlot(app),所有回调函数只做两件事:读控件值、调 updatePlot。这么设计之后,哪怕后续加了新的参数(比如屏距 L),只要在 updatePlot 内部加几行代码就行,回调函数一行都不用动。
一个我自己犯过的错:回调函数里每次刷新都重新绘图,但伪彩图用 imagesc,强度曲线用 plot,两种刷新频率和坐标轴范围重设逻辑不一样。后来统一在 updatePlot 里设好xlim和ylim,不然滑块每动一下,坐标轴就自动缩放一下,看起来画面“无规律跳变”,观感很差。
3. 核心代码:从公式到图形
3.1 强度分布计算核心函数
做 GUI 之前,先把纯计算函数单独写出来测试。这样界面和算法解耦,调公式时不用每次重新拖滑块。
function [x, I] = electronDoubleSlit(a, d, Va, L, nPixels) % a: 缝宽(nm), d: 缝间距(nm), Va: 加速电压(V) % L: 屏距(m), nPixels: 屏幕采样点数 lambda = 1.226 / sqrt(Va); % 德布罗意波长 nm % x: 屏幕位置,单位 mm x = linspace(-10, 10, nPixels) * 1e-3; % 转成 m 方便算 sinθ theta = atan(x / L); % 弧度转角度更直观的写法 sinTheta = sin(theta); alpha = pi * d * sinTheta / lambda; beta = pi * a * sinTheta / lambda; % 防止 sin(beta)/beta 在 beta=0 处出现 NaN beta(beta == 0) = eps; singleSlit = (sin(beta) ./ beta).^2; interference = cos(alpha).^2; I = singleSlit .* interference; end几个细节注意:屏幕上 x 的范围先用 mm 单位定义,转到弧度计算的时候一定要换成米;beta 为 0 的中心点必须用 eps 替换,否则 0/0 直接变 NaN,整条曲线在中心位置凹陷一块。这个 bug 我调试的时候找了很久,图形上中央峰值莫名凹下去,第一次看到还以为是干涉极小。
3.2 滑块回调的实现
App Designer 里滑条 ValueChanged 回调自动生成,形如ValueChanged(app, event)。我统一在里面调值更新函数:
app.VaSlider.ValueChangedFcn = @(src, event) updateVoltage(app); function updateVoltage(app) app.VaField.Value = app.VaSlider.Value; % 同步到输入框 updatePlot(app); % 整个界面刷新 end输入框的回调是反过来的:用户在框里输入范围之外的值,或者空值,用 try-catch 包一层,出错时回退到滑条当前值。别小看这层保护,很多人在滑条和输入框之间来回操作时,回退没处理好,输入一个非法字符界面就崩一下。
还有一个重要的交互细节:三个滑条分别控制缝宽、缝距、电压,但用户拖完一个滑条再看图形,很容易忘记当前是哪个参数在起作用。我给每个滑条回调里把当前变动参数的数值用title()显示在图上方:比如“a=50.0 nm, d=200.0 nm, V=200 V, λ=0.0867 nm”。这样截图记录参数时非常方便,不用自己手写。
3.3 绘制与刷新
两个坐标轴的绘制逻辑有区别,必须分开处理:
条纹图用imagesc画二维灰度图。把一维强度分布复制成 N 行,生成一个 N×M 的矩阵,横向坐标是屏上位置 x,纵向没有物理意义,只是为了拉开视觉高度。灰度映射用colormap(gray)或更漂亮的jet,不过我实际用下来 gray 更能体现“电子打在屏上的亮度”,彩色反而误导。
一维强度曲线就用plot(x, I),同时hold on画上包络线 ( (\sin\beta/\beta)^2 ),用红色虚线区分。这样用户能直接对比总强度曲线和单缝衍射包络的关系——这正是双缝衍射物理本质的直观体现:总强度曲线是包络“包”出来的。
绘图代码:
function updatePlot(app) a = app.aSlider.Value; d = app.dSlider.Value; Va = app.VaSlider.Value; L = 1; nmPerM = 1e9; mmPerM = 1e3; nPixels = 1000; [x_m, I] = electronDoubleSlit(a, d, Va, L, nPixels); x_mm = x_m * mmPerM; % 条纹图 imagesc(app.UIAxes, x_mm, 1:5, repmat(I', 1, 5)); axis(app.UIAxes, 'tight'); colormap(app.UIAxes, 'gray'); % 强度曲线 plot(app.UIAxes2, x_mm, I, 'b-', 'LineWidth', 1.5); hold(app.UIAxes2, 'on'); lambda = 1.226 / sqrt(Va); beta = pi * a * sin(atan((x_mm*1e-3)/L)) / lambda; beta(beta==0) = eps; plot(app.UIAxes2, x_mm, (sin(beta)./beta).^2, 'r--', 'LineWidth', 1); hold(app.UIAxes2, 'off'); legend(app.UIAxes2, {'总强度','单缝包络'}, 'Location', 'northeast'); title(app.UIAxes, sprintf('电子双缝衍射 a=%.1fnm d=%.1fnm V=%.0fV λ=%.4fnm', a, d, Va, lambda)); xlabel(app.UIAxes, '屏上位置(mm)'); endimagesc 的纵向范围 1:5 是我试出来的——5 个像素行拉伸后条纹高度刚好,太宽了挤占下方曲线图空间,太窄了条纹颜色过渡看不清。
4. 参数实验:亲手“拨动”电子衍射
4.1 缝宽的作用:包络怎么变
把缝宽 a 从 50 nm 调大到 200 nm,最直观的变化是单缝衍射中央亮带的宽度整体收窄。因为单缝衍射极小值位置满足 ( a\sin\theta = m\lambda ),缝越宽,同样的极小平位置对应的角度越小,条纹整体挤向中心。
操作时注意:缝宽改变后,干涉条纹的间距不受影响,干涉条纹间距只由缝间距 d 决定。你看到的效果就是“包络变瘦了,里面的细条纹数量变少”。这其实是双缝衍射里特别容易误解的一点:缝宽影响“有几根条纹”,间距影响“条纹多密”。区分这两个因素的最好办法就是把曲线图和条纹图上下对着看:曲线图里红色包络跟着缝宽变化,蓝色细条纹的峰峰间隔完全不动。
我建议上手先调缝宽,把包络从宽到窄拉一遍。这个过程能把“衍射是包络、干涉是调制”这句话彻底搞定。
4.2 缝间距的作用:条纹变密
缝间距 d 从 200 nm 调到 800 nm,干涉条纹立刻变密。中央极大两侧的角间距满足 ( \Delta\theta = \lambda/d ),d 大,间距就小;d 小,条纹稀疏粗壮。这个参数跟缝宽的视觉效果完全不同——缝宽改变的是包络轮廓,缝间距改变的是内部条纹的疏密节奏。
实际操作时你会遇上一个“观察密度”问题:d 拉大后条纹密到一定程度,屏幕上一团糊,根本数不清峰数。这时候把屏上坐标范围缩小,只看中心 2 mm 的范围,一组组条纹就清楚了。这个缩放建议我写在了界面注释里,很多同学拿到代码不会主动去改 xlim,我建议在 GUI 里加一个“视场缩放”滑条,从 0.5 倍到 10 倍调节屏幕显示范围。这个控件我后来才加的,确实好用。
4.3 电压(波长)的作用:电子能量越大条纹越密
加速电压调高,德布罗意波长变小,整体衍射图样都会向中心收拢。电压从 50 V 调到 5000 V,相当于波长从 0.173 nm 缩到 0.017 nm,条纹的角尺度会缩小一个数量级。注意电压是平方根关系再进波长公式,所以效应不像想象中那么剧烈——从 50 V 到 200 V 波长缩小一半,但条纹只向外缩了约三分之一。
调电压本质上是在演示“物质波波长如何决定衍射尺度”。这个关系是电子衍射应用的核心:电压越高、波长越短,能分辨的结构越细小。做模拟实验时,你可以先把电压固定在 200 V,调好缝宽和间距的结构,最后再动电压看整体缩放效果,这样每个参数的独立作用一眼就分清了。
4.4 做实验时的观察要点
摆弄模拟器容易陷入“拖动滑块看花”的陷阱——看起来很爽,过一会儿什么都不记得。我建议按下面的顺序做一遍,每一步停下来想想为什么:
- 固定 d=200 nm,V=200 V,从 a=20 nm 拉到 a=200 nm,记录中央包络半宽度变化;
- 回到 a=50 nm,从 d=100 nm 拉到 d=600 nm,数一数中央亮带内干涉主极大的数目;
- 固定 a、d,从 V=50 V 拉到 V=2000 V,注意条纹整体缩放的速率;
- 最后试几组“a=50, d=200”和“a=100, d=400”的组合,这两组参数下 d/a 相同,干涉峰数量一样多,但衍射角尺度完全不同——这是最容易混淆的情况,值得亲手验证。
5. 常见问题与避坑实录
5.1 问题速查表
做这个项目时我遇到的坑不少,整理成表格,照着排查能省大量时间:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 中央峰值凹陷成黑洞 | beta 为 0 时未做保护,出现 0/0=NaN | 用beta(beta==0)=eps替换 |
| 条纹图上下方向拉伸变形 | imagesc 的 Y 轴范围设太大 | Y 方向只保留几行像素,如 1:5 |
| 滑条拖到两端,图形突然全黑 | 参数范围内条纹多到超出屏幕分辨率 | 缩小 x 显示范围或增大 nPixels |
| 修改输入框数值后图形不刷新 | 输入框的回调函数没有调用 updatePlot | 确认回调连接到 updatePlot |
| 界面启动时没有图 | 只在回调里绘图,没在 startupFcn 初始化 | startupFcn 里调用一次 updatePlot |
| 电压滑条低端调节精度差 | 用了线性刻度 | 改用对数刻度logspace(log10(50), log10(5000), n) |
| 中央亮带过宽看不到干涉条纹 | 缝宽太小或缝距太小 | 检查 d/a 比值,至少大于 3 |
| 曲线和条纹图数值对不上 | 两套坐标用了不同单位 | 统一先算米制,输出再转 mm |
5.2 几个容易踩的坑
第一个坑是“滑动变阻器”式的手感。初版代码电压滑条用了线性刻度,50 V 到 5000 V 之间自适应调参根本没法精确控制低电区域,滑块稍微一动就是几百伏的跨度。换成对数刻度后,滑块在中低电压区间的手感细腻很多。App Designer 里滑条控件不支持对数刻度,需要在回调里自己映射:value = 10^(minLog + sliderValue * (maxLog-minLog))。
第二个坑是伪彩图和曲线图“打架”。我一开始在同一个坐标轴里又画二维图又画曲线,结果一刷新,两者叠加导致图像只剩下最后一层。后来拆成两个 UIAxes,程序结构清爽了,视觉上也不互相干扰。布局上注意条纹图高度只占整个区域的三分之一,下方曲线图占三分之二,因为曲线图的信息密度更大,太矮了就看不清细小振荡。
第三个坑是图像刷新卡顿。nPixels 设成 10000 时,每次拖动滑块都要重新算一个 10000 点向量并重绘两张图,刷新明显有延迟。把采样点降到 1000 之后,肉眼看不出曲线细节损失,刷新却流畅很多。条纹图的伪彩图只画 5 行像素,即使增加采样点,图像渲染压力也不大,但强度计算的时间还是成正比增长。这个数量级上千就够用,除了编辑高倍数放大时再临时提高 nPixels。
第四个坑是单位混用。我犯过一次“像素当作毫米”的错:x 数组初始化的单位是 mm,转到弧度计算时忘了换算成米,结果所有角度都差了 1000 倍,条纹比正常宽大好几个量级。建议在代码里把单位写进变量名,比如x_mm、x_m、lambda_nm,不同单位的量一眼就能分辨。我后来所有涉及单位的函数都强制用后缀命名,再也没出过这种低级错。
6. 还能往哪扩展
6.1 从条纹到概率:随机打点模拟
经典波动公式模拟的是“大量电子一次性通过双缝后的稳定图样”,但电子双缝衍射最震撼的实验版本是一次只发射一个电子,电子在屏幕上落点随机,慢慢积累出条纹。这个打在 GUI 里可以做成“粒子逐个打上去”的动画模式:每次随机生成一个落点,位置按强度分布抽取,然后用散点图把所有历史落点画出来。
实现思路是在updatePlot之外另写一个accumulateParticles函数,用randsample按照一维强度分布权重抽取落点位置,然后hold on画 scatter,最后把速率控制在一个 timer 里。这个扩展做起来不难,但教学演示效果翻倍——学生看到“单个电子的落点是随机的,大量电子的落点形成条纹”,马上就能直观理解波函数概率解释的核心思想。
我建议把“波动模式”和“粒子累积模式”做成两个运行按钮,切换互不影响。粒子累积模式的图形刷新方式要改成增量式,不要把存量粒子每次重绘——几千个点重绘会卡,直接在原来的坐标轴上叠加 scatter 就行。
6.2 从双缝到多缝光栅
双缝公式改多缝其实很简单,把cos^2(alpha)换成一个 N 缝干涉因子 (\frac{\sin^2(N\delta)}{\sin^2(\delta)}),其中 δ 是相邻缝之间的相位差,N 是缝数。GUI 里加一个“缝数”滑条,范围 2~10,用户就能观察从双缝到多缝光栅的演化过程——主极大变尖锐,次极大增多,这是衍射光栅分光原理最直观的演示。
注意多缝光栅里缝宽 a 的作用依然是包络,缝数 N 只改变干涉峰的锐度。这个扩展可以把原本双缝项目的适用范围从电子衍射扩展到光谱仪原理教学,一鱼两吃。
6.3 加上屏幕感光度噪声
模拟器算出来的强度分布是理想值,真实探测器有噪声。想在图上加一层“高斯白噪声”,让条纹看起来更接近实验照片的效果,只需要在强度矩阵上叠加randn数组,幅度设为最大强度的 5%~10%。加噪声后,弱光区域的条纹会若隐若现,暗纹附近出现颗粒感,模拟的“真实感”立刻上一个档次。不过这个设置在默认状态下建议关闭,因为噪声会干扰参数变化的定量对比;在“实验照片模式”下可以打开,配合粒子累积模式使用效果最好。
我个人在实际操作中最大的体会是:数学公式和图形结果之间的“即时反馈感”是这个项目最大的价值。公式写错了、参数范围选得不对、单位混了,在 GUI 拖两下滑块马上就能暴露问题——这比单纯跑脚本看一张静态图要高效得多。如果你在做类似的光学模拟或量子物理可视化,先把公式用纯函数写出来测试,再接 GUI,这是最稳的路径。最后再分享一个小技巧:把最终调好的界面截图存下来,跟真实电子衍射实验照片放在一起对比,你会惊讶地发现,理论模拟和实验拍到的条纹位置、相对强度几乎完全一致——那一刻你会理解,为什么说“波动性”不是一种抽象想象,而是实实在在的物理规律。