我承认“模拟调试”这个标题有点神秘。阅读本文后,嵌入式固件开发人员可能会遭受认知失调的困扰,但相信我,这以后会说得通的。本标题暗示的是处理微控制器(MCU)中被处理信号的任务。
许多涉及较小MCU的任务都与处理来自传感器(如麦克风、水听器、压力传感器等)的原始信号有关。其中一些信号需要清理或以其他方式处理。该处理可能会使用多种数字信号处理(DSP)固件技术,例如FIR和IIR滤波器、混频器、FFT等。
随着信号通过MCU传输,我们希望通过调试验证的数据可能会很广泛。例如,信号通过滤波器后会看到什么,或者当信号通过相关器时,相关器的输出是什么。这就是模拟调试的用武之地,我们可以用它来实时观察信号。较小的MCU可能缺少较大的处理器所具有的一些强大的调试工具,例如BDM、JTAG和SWD。
较小的MCU也可以不使用操作系统而作为裸机运行,但这样的话,操作系统中任何可用的调试工具都无法使用。这种工具的缺乏和实时信号处理的复杂性,会使调试代码出现问题。但是,调试需要深入了解MCU内部的数据在发生什么,并且在处理流式模拟信号时,我们可能希望查看其在模拟域中的实际情况。
通常,在调试固件时,工程师会使用MCU上的串行端口(如果存在)打印出正在执行的代码的变量值或指示符。这里有很多问题:
例如,假设使用模数转换器(ADC)从传感器接收信号,可以在传感器的输出上挂一个示波器而在模拟视图中查看信号和噪声,但是,如果通过串行端口查看相同的信号,则在MCU读取该ADC并发送出此串行端口后,所看到的就是一堆数字。现在可以将这些数字放入电子表格,绘制图表,或者设置另一台带有数模转换器和显示器的设备来再次查看该数据,但这似乎会有点慢和费事,而且肯定不是实时的。
现在,如果没有可用的串行端口或者其不适合调试,工程师可以用一个LED连接到MCU,然后根据被调试程序中的各种条件控制其亮灭。可以将示波器连接到该LED或可用的I/O线,从而查看其状态,或通过翻转固件中的LED或I/O线来测量状态变化之间的时序。这非常有效,但不符合我们想要获得信号的模拟视图的想法,因为它会受到滤波器、相关器、切片器和混频器等各级处理。
如果有某个地方能够连接示波器探头,并可以在此在固件中快速打印输出已处理样本,那就会很好。那么,我们可以使用什么呢?第一个想法是将DAC连接到MCU,或者更好的是,如果MCU本身带有DAC外设,那就直接使用它。为了尝试这种技术,我将ADI公司的8位DAC AD7801连接到我正在研究的Arduino Nano设计中。Nano的核心是Microchip的ATmega328,其上不带有DAC。AD7801使用8根数据线的并行输入,并通过另一根线同步;其写入速度非常快而且非常简单。(请注意,可以使用此设置查看8位数据,但对于其他DAC也可以使用10位、12位或其他大小,或者可以对其进行缩放来适应8位DAC。)我将八根数据线连接到Arduino上的端口D并将WR线连接到Arduino的D13,如图1所示。
图1:将ADI的8位DAC AD7801连接到Arduino Nano设计。
现在,要将数据发送到DAC,只需要3行Arduino IDE C代码即可:
PORTD = data; // 将数据字节放到D0至D7上
PORTB=PORTB & B11011111; // 将D13拉低以将数据锁存到AD7801
PORTB = PORTB | B00100000; // 拉高D13
在16MHz Arduino上,此代码需要大约5个周期或大约312ns,DAC的建立时间为1.2μs。所以,可以看到,这种数据显示的方法可以比较快地完成,不需要中断,也不需要太多的代码。可以将此代码插入到固件的适当位置,以便查看重要数据。将3行代码放入到宏或函数中,可能会更简洁。如果为此创建函数,则应使用“always_inline”编译指示对其进行编译,以便确保其快速运行。
现在连接了DAC,下面来看一些调试示例。看一下图2。
图2:传感器输入信号的示波器快照。
这是传感器输入信号的示波器快照(为清楚起见,此处删除了格线)。底部迹线(粉红色/紫色)是原始信号,因为它正在进入ATmega328上的ADC引脚。可以在这条线上看到明显的噪声。上面的迹线(黄色)是经过MCU固件中的一些滤波和其他处理后的相同信号。我们已将DAC写入调试代码插入到此流程中,因此DAC中的采样时序与ADC相同。如果需要,还可以对MCU中的信号进行抽取。暂时忽略信号中的“尖峰”,可以看到处理过程已经消除了大部分噪声。我们现在有了一个可以评估的干净信号。应该注意的是,DAC输出是一个连续的信号流,而不仅仅是一些短暂的内存缓冲捕获。
但这些“尖峰”是什么?它们是我有意放入代码中的一些调试功能,以便查看处理过程是如何进行的。我们所看到的信号实际上是被信号介质破坏了的专有数字信号。代码任务是通过以下方式读取数字数据包:
下面来看一下图3。
图3:添加了注释的已处理信号视图。
图3显示了添加了注释的已处理信号的视图。我在代码中所做的是将信号从最小值50放大到了最大值200。这样就可以在256个可用值中留出一些空间,从而在信号的上方和下方添加“尖峰”。我们首先看到的是标有“检测到前导码”的“尖峰”。这是在代码验证已找到前导码(B00000011)时创建的,它可以使用以下Arduino IDE代码轻松生成:
PORTD = 255; // 将255放到D0至D7上
PORTB = PORTB & B11011111; // 将D13拉低以将数据锁存到AD7801
PORTB = PORTB | B00100000; // 拉高D13
这会在示波器迹线上创建一个312ns宽的标记,其幅度等于DAC的最大电压。信号迹线内往上和往下的“尖峰”,是指示代码确定符号边界位置的标记。这对于在正确的时间对符号进行切片非常重要,并且在出现长时间运行的0或1时变得至关重要。这是因为没有发现从0到1或从1到0的转换。
在示波器上查看这些“尖峰”非常有用,因为它可以让我们验证实际时序并确认没有遗漏。这些符号边界“尖峰”是通过使用以下Arduino IDE代码(插入到符号时序代码的适当位置)向DAC发送127来创建的:
PORTD = 127; // 将127放到D0至D7上
PORTB = PORTB & B11011111; // 将D13拉低以将数据锁存到AD7801
PORTB = PORTB | B00100000; // 拉高D13
可通过使用以下代码(插入到监视从0到1或从1到0的符号转换的代码)向DAC发送0,将符号转换标记为“尖峰”:
PORTD = 0; // 将0放到D0至D7上
PORTB = PORTB & B11011111; // 将D13拉低以将数据锁存到AD7801
PORTB = PORTB | B00100000; // 拉高D13
可以看到,使用DAC查看覆盖到实际已处理迹线上的调试信息,可以极大地帮助调试代码的各个部分。这比使用LED、I/O线和示波器强大许多倍。由于包含时序信息,这也可能比串行端口发送数据更有用。
眼尖的人可能已经注意到,在图3的右边缘,探头衰减不是1或10倍,而是53.5倍。这是可以在许多较新的示波器上完成的技巧,有时称为自定义衰减设置。将其设置为53.5的原因,是这样可以使用示波器光标直接读取DAC的8位输入值。也就是说,如果将光标向上滑动到前导检测“尖峰”的顶部,则示波器光标读数为255,或者,如果将光标移动到符号边界“尖峰”的末尾,则其读数为127。使用8位DAC时,此设置的公式为255/MaxVolts。MaxVolts是输入最大二进制输入时DAC的输出电压,本例中为255。因此,对于5V导轨,自定义设置为51.0(我的导轨只有4.77V,所以我的数字是53.5)。使用10:1探头时,可能需要将此数字乘以10,然后再将其输入示波器。
这非常方便,因为可以直接读取DAC所设置的数字,或者换句话说,内部变量在调用DAC时所具有的值。我们来考虑一下这点吧。本质上,可以以这种方式“实时”读取变量……这几乎与打印语句一样好,但速度更快且不会产生打扰。请注意,示波器垂直刻度的噪声和分辨率会使精度降低,因此可能只能得到实际值的±1或±2个计数,但仍然相当不错。
除了流式传输信号外,使用这种技术,8位DAC还可以同时表示8个二进制标志的状态,或程序中8位变量的当前值。换句话说,使用8位DAC所能提供的信息,是监控单个I/O线所提供的信息的8倍。
OK,那如果没有DAC可供使用怎么办?可以使用MCU上的脉宽调制器(PWM)外设执行类似操作。许多小型MCU都有PWM,而当它们有PWM时,它们通常都有多个,一般是六个。PWM和DAC之间的区别之一是,PWM输出需要使用低通滤波器进行滤波,以便将输出转换为一水平电压。因此,当将信号样本发送到PWM时,这一水平电压会重新创建可在示波器上显示的信号,就像使用DAC所做的那样。这种滤波可以通过简单的RC滤波器来完成。
不过这里有一些警告;低通滤波器意味着只能显示具有低频成分的信号,即响应较慢。因此,应该将PWM的频率初始化为可用的最高频率。在16MHz ATmega328上,PWM可以设置为大约31kHz的最大频率,因此低通信号应设计为大约3~4kHz的频率成分。
使用PWM的Arduino IDE代码,在初始化后甚至比DAC代码更加简单。将8位值写入PWM的代码很简单:
analogWrite(PinNumber, data)
其中,“data”是一个8位采样值,“PinNumber”是PWM输出的引脚号。
尽管PWM可能不那么准确或无法显示更高频率的信号,但它有一个重要功能。由于一些MCU具有多达6个PWM,这意味着有多达6个输出可用于实时传输数据。我们可能有一个四迹示波器,可同时显示四个变量,这样就留下了两个备用PWM输出。此外,通过PWM或DAC两个输出,我们还可以提供I和Q两个数据,从而满足通常的DSP信号处理所需(并可以让我们探索负频率)。请注意,就像DAC代码一样,PWM代码也不需要中断。
另一个可用于DAC或PWM所传递信号的强大工具是频谱。图4中的示波器屏幕截图显示了一个示例。
图4:对DAC或PWM所传递信号使用频谱的示例。
上面的迹线显示了在MCU中所生成的波形。该信号实际上是对两个频率(f1=165Hz和f2=135Hz)逐个采样混合或相乘,然后在生成时将其发送到DAC。在频率混合中,所得到的频率是频率之和与频率之差。原始生成的频率受到混频操作抑制,这在示波器迹线下半部分的FFT中可以清楚地看到。大多数示波器——甚至是业余水平的示波器——都将FFT作为数学运算之一进行了提供。
如果我们的系统中没有DAC或PWM,仍然可以使用一些东西来获取有关正在运行的固件中的信号的一些信息。例如,可以编写代码来对PWM信号进行bit-bang,尽管这可能仅对低频信号或缓慢变化的变量有用。
希望我将模拟调试的想法讲清楚了。从固件流式传输数据并将其显示在示波器上这一主要概念,可以为我们提供一种强大的工具,进而加快我们的信号处理固件调试。如果可行,选择带有DAC外设的MCU或在我们的第一个原型PCB中加入DAC可能会很有用。我们总是可以在以后把它删除或在BOM中将它制作为NO-POP。
(原文刊登于EDN美国版,参考链接:Analog debugging on bare-metal systems,由Franklin Zhao编译。)
本文为《电子技术设计》2022年11月刊杂志文章,版权所有,禁止转载。免费杂志订阅申请点击这里。