设计一个基于 Arduino 电机控制的稳健正交编码器 ISR 程序

什么是正交编码器?

正交编码器是一种用于测量物理旋转的机电传感器,例如本工程简介中介绍的电机轴。相关的微控制器或可编程逻辑控制器(PLC)可以解释传感器的数据,以计算旋转速度、方向和总角距离。

正交编码器在许多应用中是首选,因为它们成本低且提供精细的测量分辨率。图1中所示的Pololu #4754直流电机是一个代表性示例。它采用非接触式霍尔效应传感器,提供可直接连接到图中Arduino Nano Every的响应输出。

在未来的部分中,我们将展示如何设计一个响应式伺服系统,包括比例积分微分(PID)控制器。一个强大的正交编码器接口对于伺服电机的性能至关重要。

这篇文章是为谁写的?

这篇文章是为过渡期的爱好者和学生写的。它假设您熟悉Arduino微控制器,并已成功编程了几个自己的设计。本文将通过一个应用实时示例引导您进入下一个级别,该示例具有与正交编码器相关的中等时序要求。

1 :本文探讨了图中Pololu #4754直流电机和Arduino Nano Every微控制器的应用。Digilent Analog Discovery 在背景中可见。

正交编码器是如何工作的?

术语“正交”指的是相位相差90度的信号。例如,特斯拉著名的感应电机的交流电流由正弦和余弦波形驱动。正交编码器在正交波形上运行。实际上,它使用如图2所示的数字表示。在这里,我们看到正交编码器的双数字输出信号及其与SIN和COS波形的关系。请注意,当相应的正弦波高于零时,每个输出都是活动的。图1中展示了一个代表性的正交编码器。安装在电机末端,我们可以看到带有外围一系列磁铁的黑色环。我们还可以看到磁环Arduino一侧的霍尔效应传感器。

有关正交编码器的更多信息,请参阅这篇介绍性文章。它提供了关于双数字波形性质的基本信息。它描述了用于确定旋转方向的关键关系。它还引入了如图3所示的状态机,这将在我们后续的讨论中非常重要。

2 :正向(顺时针)方向的正交信号,蓝色信号领先于绿色信号。Q1和Q2传感器在相应信号为正时激活。

为什么我的正交编码器在高速时尤其不工作?

电机驱动的正交编码器并非一个简单的应用。正如我们稍后将看到的,这是一个挑战微控制器极限的应用。因此,我们必须密切关注时序和微控制器的响应能力。在许多情况下,仅靠编程是不够的。相反,我们必须使用基于硬件的解决方案,例如中断服务例程(ISR)。正如将展示的那样,ISR是微控制器硬件和软件技术的结合,旨在增强微控制器对现实世界事件的响应。

正交编码器程序失败的最可能原因是由于编程技术错误或微控制器响应缓慢而导致的计数丢失。回想一下,基于正交编码器的系统通过观察并响应编码器双数字输出的变化来跟踪位置。如果系统(微控制器及相关程序)不够快,它将错过一个计数。结果是一个失控的机械系统。例如,如果正交编码器正在监控伺服电机的位置,系统将失去正确定位相应电机轴的能力。

本文的后续部分将介绍一种技术,以确定您的系统是否足够快。使用示波器,您将学习如何计算用于处理正交编码器任务的时间百分比。

电机驱动时编码器每转计数( CPR )的重要性

构建正交编码器的方法有很多。然而,每个编码器都设计有特定的每转计数(CPR)。我们可以使用一张扑克牌和一个辐条自行车轮来形象化这一点。随着车轮旋转,我们可以将辐条的数量计为卡片上的刻度。相反,通过知道辐条的数量,我们可以确定车轮的角位置。

我们确定轴角位置的分辨率由辐条的数量决定。例如,如果车轮只有4根辐条,我们的角度测量将限制在90度。如果轮子有64根辐条,我们的分辨率现在是5.625度。

CPR是正交编码器与辐条数量的类比。它描述了轴的“辐条”与传感元件之间的物理关系。对于我们的Pololu #4754电机,“辐条”由交替的北极和南极磁铁组件组成。

所选的Pololu电机的CPR为64。这并不意味着有64个N/S磁铁组件。我们的轮子和辐条类比过于简单。相反,我们预计只会看到16个槽。从图2中,我们观察到双输出有效地将每个周期分为4个独特的状态。因此,16个槽(16个N/S磁铁对)足以获得64的CPR。

正交编码器的分辨率是 CPR 的函数

请注意,64的CPR描述了安装在电机轴上的传感器。另外,请注意Pololu #4754电机是一个齿轮电机,减速比为70比1。要确定在输出轴上测量的CPR,将电机的CPR乘以齿轮比,得到每转4480个计数。

CPR 和转速决定了每秒的计数

Pololu电机的最后一个,也许是最关键的指标是空载轴速,为150 RPM。这意味着电机转速为10,500 RPM或每秒175转。以64的CPR计算,微控制器必须响应每秒11,200个周期的系统。

响应每秒 11,200 个计数的电机并非易事 。需要仔细编程,以确保我们不会错过任何一个转换。可能可以在Arduino主循环中执行此活动。然而,在主循环中做其他任何事情都会有问题。例如,执行该项目的下一个自然演变并实现PID控制器将非常困难。非阻塞代码技术变得复杂。此外,向串行监视器或LCD显示屏发送信号将是一个真正的挑战,因为这些操作有时可能比每秒11,200个计数的电机允许的时间更长。

如何将正交编码器与中断服务例程( ISR )集成

传统的解决方案是使用中断服务例程(ISR)。中断是微控制器的定义属性之一,使其能够及时响应异步的现实世界事件。

什么是中断服务例程?

ISR是专用硬件和软件技术的结合,使微控制器能够近乎实时地响应。为了更好地理解ISR,让我们关注一个称为微控制器状态的概念。在不涉及太多技术细节的情况下,认识到微控制器是一个高度专业化的状态机,运行组合(顺序)逻辑。回想一下,顺序逻辑需要内存,因为任何未来的操作都取决于机器中保存的状态(过去的记忆)。有几个寄存器(内存位置)与微控制器的核心密切相关,保存着状态。这些包括状态寄存器、程序计数器和本地工作寄存器。

当中断发生时,微控制器会快速保存其当前状态,然后跳转到特定事件的软件部分。重要的是要认识到,这种状态变化是内置在硬件中的,从而实现了从主代码到中断代码的快速过渡。这就像你在看书时电话响了。你在当前页面上放一个书签,然后跳起来接电话。当你接完电话(中断)后,你会回到你离开的地方。

技术提示 :将ISR和主循环分别视为前台和后台是有帮助的。ISR位于前台,具有高优先级,因为它必须处理时间敏感的任务。在这个特定的例子中,ISR必须无故障地响应每秒11,200次的正交编码器,否则位置将丢失。其他任务,如向串行监视器发送数据或更新LCD显示,则在后台处理。它们的优先级较低,只要保存了微控制器的上下文,就可以被中断。

保持ISR简短很重要,这样微控制器才能将大部分时间花在后台任务上。例如,当电机以最大速度运行时,微控制器在ISR中花费的时间不到5%。如果时间更长,后台任务将因CPU周期不足而导致操作迟缓。在极端情况下,ISR可能无法在下一个变化到来之前完成对正交编码器变化的服务。在这种情况下,系统将丢失位置,并且没有剩余的周期用于后台任务。

正交编码器中断服务程序

我们实现Arduino正交编码器的代码从这两行开始:

attachInterrupt(digitalPinToInterrupt(QUAD1_A_PIN), ISR_QUAD1, CHANGE);
attachInterrupt(digitalPinToInterrupt(QUAD1_B_PIN), ISR_QUAD1, CHANGE);

请注意,中断发生在QUAD1_A_PIN或QUAD_B_PIN的任何变化时。还要注意,这两个事件都调用相同的ISR_QUAD1代码。

基于状态的正交编码器中断服务程序,带错误检测

如果您还没有这样做,请查看介绍正交编码器的文章。它描述了如图3所示的有限状态机(FSM)。在阅读文章时,请注意正交编码器的输出在正向(顺时针)旋转时具有固定的模式00 → 01 → 11 → 10 → 00,而在反向(逆时针)操作时具有相关的模式00 → 10 → 11 → 01 → 00。这反映在图3的上部顺时针状态转换和内部逆时针转换中。

请注意,某些转换被视为故障。例如,如果系统处于状态00,而正交输出突然转换为11,那么出现了问题。计数被视为损坏,系统进入故障状态。为了清晰起见,图3中没有显示返回自身的循环。

以下是部分代码。这段代码直接来自图3,显示了从00(9点钟)状态可能的状态转换。系统可以顺时针移动到状态01。它可以逆时针移动到状态10。它可以保持在当前状态(循环到自身)。最后,如果正交输入从00跳转到11,系统将进入故障状态。请注意,4字节的quad_1_cnt会根据有效的状态转换进行递增或递减。

点击此处获取Arduino代码:
QUAD.ino (7.3 KB)

switch (quad_1_state) {

case 0b00:

    if (BA == 0b00) {
        ;
    } else if (BA == 0b01) {
        quad_1_state = 0b01;
        quad_1_cnt++;
    } else if (BA == 0b10) {
        quad_1_state = 0b10;
        quad_1_cnt--;
    } else{
        quad_1_state = 0xFF;
    }
    break;

3 :正交编码器信号的状态图表示。气泡代表状态,而线上的数字代表传感器值。为了清晰起见,保持状态的循环已被省略。

ISR 到主循环的原子数据传输

此时,ISR 保持了由正交编码器测量的电机轴位置的忠实副本。我们可以宣布胜利并结束这篇文章。然而,我们将错过 ISR 和主循环之间的一个基本协调步骤。

请记住,Arduino Nano Every 是一台 8 位机器。在原子级别上,它操作的是字节宽度的变量。通常,这个不可分割的事实被 C 编译器通过诸如 int32_t(长整型)之类的关键字隐藏。然而,我们必须认识到,像 quad_1_cnt++; 这样的简单操作至少需要四条离散的 ATMega4809 指令。第一个操作将字面值 1 加到最低字节。随后是三条带进位的加法指令,将加法传播到所有字节。

技术提示 :术语“原子”指的是不可分割的事物。在微控制器中,原子性与微控制器的原生位宽有关。例如,8 位机器操作的是字节宽度的变量,而 32 位机器操作的是 4 字节的原子元素。当在程序的异步部分之间传输非原子变量时,会出现复杂情况。本工程简报中包含的主要示例涉及在 ISR 和主循环代码之间使用 8 位机器传输 32 位值。在传输过程中需要仔细同步以保持数据完整性。我们可以使用“原子”这个词来表示整个 32 位变量必须作为一个单一的原子单元小心传输。

再次强调,通常我们并不关心这些事情。然而,我们必须认识到主循环和 ISR 是异步运行的;指令之间没有自然的协调。请记住,我们的目的是将正交编码器计数传输到主循环。如果没有适当的协调,将会出现不幸的星象排列,主循环将在复制过程中被 ISR 中断。结果变量将损坏。

这个原子安全的规定就像读取时钟一样。假设我们从 ISR 维护的实际时间第 29 秒开始。让主代码复制十位数字。然后让 ISR 中断这个过程。假设当前实际时间是第30秒。当ISR返回到主代码时,第二个数字被捕获为0。因此,主代码认为实际时间是20,而实际上,当前时间是第30秒。请注意,此错误是高度间歇性的。

在实时系统中使用标志和邮箱进行同步

为了防止错误,我们可以禁用全局中断。然而,这在实时系统中是非常不可取的,因为我们可能会错过事件。相反,我们将专注于使用标志(基本信号量)和邮箱的解决方案。标志是对信息的请求。邮箱是用于原子传输的受保护内存位置。

结果非常简单,主代码将通过标志向ISR请求数据。对于我们的8位机器,标志是一个易失性全局变量。主循环设置原子(不可分割)标志。ISR识别到标志已设置。它通过将数据放入适当的易失性全局内存位置来响应。然后ISR清除标志。

与此同时,主代码进入一个while循环,等待标志被清除。我们在这个函数中看到这个while 1延迟,因为它等待quad_1_flag被清除。请注意,我们添加了超时功能以防止代码阻塞。

int32_t get_quad_1(uint32_t timeoutms) {
    int32_t last_known_position;
    int32_t startMillis = millis();

    if (!quad_1_flag) { // Retrieve the latest data if no request is pending
        last_known_position = quad_1_mailbox;
    }

    quad_1_flag = 1; // Request new data. This line is redundant if the flag is already set from a previous iteration.
    while (quad_1_flag) {
        if (millis() - startMillis > timeoutms) {
            return last_known_position;
        }
    }

    return quad_1_mailbox;
}

quad_1_flag = 1; // Request new data. This line is redundant if the flag is already set from a previous iteration.

while (quad_1_flag) {

if (millis() - startMillis > timeoutms) {

return last_known_position;

}

}

return quad_1_mailbox;

}

仔细分析get_quad_1函数,可以发现一种解决ISR和主代码异步性的方法。请注意,当主代码需要更新时,无法保证ISR会被调用。与其等待ISR,函数将超时并返回最后已知的位置。这可以防止电机转动非常慢时代码阻塞。

技术提示 :一个好的编译器会检查您的代码并采取捷径。根据设置,生成的机器代码可能会更短(使用更少的ROM)或通过展开循环将小函数直接插入代码中而更快。对于像ATMega4809这样的微控制器,编译器也可以通过使用本地工作寄存器而不是访问RAM来优化代码。无论哪种方式,运行在微控制器上的机器代码都是原始程序的影子。

编译器并不特别了解在ISR和主循环之间共享的变量。它可能会对变量采取优化捷径,从而破坏程序两部分之间的关联。我们必须将共享变量声明为volatile ,以防止编译器优化。

ISR 与主循环之间的协作时间调度

在上一节中,我们探讨了一种从ISR到主循环代码的可靠数据传输方法。在结束之前,我们应该考虑ISR的时间方面。

回想一下,ISR是一个中断。它是一种硬件方法,用于保存上下文并切换到特定代码段,例如本文中介绍的ISR_QUAD1例程。如果不考虑微控制器在ISR中花费的时间百分比,关于ISR的讨论就不完整。之前,我们将ISR描述为前台进程,将主循环描述为后台进程。ISR是一个高优先级的前台进程,而例程、较低优先级的事件则被降级到后台。

我们必须认识到,微控制器的操作主要由架构和时钟速度决定。架构(位宽和指令类型)决定了特定操作所需的机器周期数,而时钟速度决定了这些操作完成的速度。

无论如何,微控制器是一种受限资源。只能执行有限数量的操作。当我们考虑到这种资源在ISR和主循环之间共享时,这一点变得尤为重要。ISR必须与主循环协作。

如何测量 ISR 时间和百分比

如前所述,ISR必须简短,否则它将使主代码陷入周期饥饿。更糟糕的是,长时间的ISR可能会消耗所有时钟周期,阻止主代码运行,或者在正交编码器的情况下,导致计数混乱并错过转换。

可以使用示波器或逻辑分析仪来分析ISR的时序。图4中的示例显示了Pololu #4754在13 VDC无负载操作时的波形。此图像中有三个波形,包括正交编码器的A和B输出,以及对应于ISR中花费时间的脉冲。请注意,ISR 在任何 A 或 B 正交信号转换后立即进入。

Time_ISR 脉冲很容易编程。只需在进入 ISR 时将 I/O 引脚设置为高电平,然后在退出前立即清除它。结果提供了对 ISR 的合理估计。在此示例中,ISR 在大约 3.8 微秒内完成。假设 ISR 调用之间的时间为 85 微秒(每秒近 12 k 个脉冲),ISR 消耗了大约 5% 的总 CPU 周期。这应该算是一个短 ISR,提供了对微控制器资源的良好利用。请注意,这是电机在额定电压以上 1 VDC 运行的最坏情况。

4波形显示正交编码器 A 和 B 信号以及显示 ISR 中花费时间的脉冲。请注意,ISR 在每次 A 或 B 变化时进入。

技术提示 :架构和时钟速度之间存在权衡。ARM Cortex 是一种精简指令集计算机 (RISC) 架构,是一个很好的例子。RISC 在 ARM 名称中,因为它代表高级 RISC 机器 (ARM)。这些微控制器是为速度而构建的,具有简单的指令集,从而实现了精简的硅片。与远房表亲中的丰富命令相比,RISC 机器必须为给定操作完成更多的指令——但它们以高时钟速度完成。换句话说,微控制器不仅仅是时钟速度。

ISR 中直接端口操作的重要性

Arduino 团队在抽象复杂的微控制器寄存器方面做得非常出色,结果是易于使用的设备,为微控制器提供了极好的入门。不幸的是,Arduino 并不快,因为任何给定的函数(如 digitalWrite())都必须通过多层抽象才能生成特定的机器代码。当我们在 ISR 的时间限制内操作时,这种时间相关的抽象变得重要。

我们可以通过使用裸机编程来减少 ISR 时间。我们失去了所有的可移植性和基于 Arduino 的简单性,但我们赢得了时间。例如,考虑这行专门为 ATmega 4809 编写的代码:

  BA = VPORTD.IN & 0x03;

它假设正交编码器输入直接连接到 4809 的端口 D 的下两个引脚。此单条语句读取端口(1 个时钟周期),然后屏蔽掉除下两位之外的所有位(1 个时钟周期)。结果非常快——接近 4809 的最佳解决方案。

对示例代码的审查表明,执行了几项直接的ATmega4809操作。这是将ISR时间减少到总周期5%的重要步骤。如果没有这种微控制器特定的操作,ISR将消耗近20%的总时间。

技术提示 :嵌入式程序员必须具备与微控制器的特殊功能寄存器(SFR)进行低级交互的裸机编程技能。这需要深入探索微控制器的数据手册,并理解微控制器的架构、内存结构和硬件外设。回想一下,Arduino抽象了这些细节,提供了一个易于使用的编程环境,并在所有Arduino家族成员中保持一致性。同时,Arduino IDE确实允许直接访问SFR,如本笔记中所示。这为初学者提供了一种探索微控制器的方式,而无需完全依赖专用的IDE。然而,在学习的某个阶段,你应该转向一个完整的IDE,如用于ATMega4809的MPLAB。这是扩展你技能栈的必要步骤。你会发现,深入研究特定的微控制器将使你更好地理解所有微控制器以及响应式实时编程的影响。

结论

将高速电机驱动的正交编码器与微控制器接口连接并非一个简单的应用。正如我们所看到的,高速运行的电机每85微秒(约12 kHz)就需要微控制器的关注。如果不使用微控制器的专用中断向量和上下文保存,这将是一项极其困难的任务。这将使后台任务(如通信、人机界面甚至过程控制)变得复杂。

我们还看到,ISR和主程序之间需要仔细协调。这包括仔细的原子变量传递以及时序分析。虽然有多种安全传递数据的方法,但我们将讨论限制在基本的标志和邮箱同步上。至于时序,我们探索了在ISR激活时设置一个简单的I/O引脚。我们简要探讨了使用直接端口访问而非Arduino抽象来缩短ISR代码的方法。

虽然本工程简报确实为稳健的高速正交编码器提供了完整的解决方案,但它只是触及了表面。鼓励你回答本笔记末尾出现的问题以获取更多信息。鼓励你下载文件并使用自己的正交编码器进行实验。此外,请继续关注,因为我打算在不久的将来发布一篇相关的PID控制文章。