65.9K
CodeProject 正在变化。 阅读更多。
Home

用于处理伺服电机和 RC 接收器的 Raspberry Pi Pico 库

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2023 年 5 月 8 日

MIT

15分钟阅读

viewsIcon

8345

downloadIcon

85

用于在 C 语言中处理遥控模型伺服电机和接收器的库

引言

树莓派 Pico 是一款微控制器板。凭借其双核 32 位 ARM 处理器、264 kB RAM、2 MB 闪存以及约 5 美元的售价,它为业余项目提供了 Arduino 板的极具吸引力的替代品。您可以使用 Pico SDK 以 MicroPython 或 C 语言进行编程,甚至可以使用 Arduino 框架。本文介绍了我用 C 语言和 Pico C SDK 编写的用于伺服电机和遥控接收器的库。

这个项目源于我希望将我旧的由 RC 汽车控制器控制的机器人从 Arduino 移植到树莓派 Pico。我需要测量来自 RC 接收器的脉冲并控制使机器人移动的伺服器。

当然,我首先寻找了一些现成的解决方案。MicroPython 中有很多示例,但 C/C++ 中并不多。对于伺服器,使用 Pico SDK 中的 PWM 函数相对容易,但我仍然想要更友好的东西。至于处理 RC 接收器,这并不容易。我找到了一个基于 Pico SDK 示例的解决方案,用于测量占空比(这也是处理 RC 接收器输入时实际要做的事情),但效果不佳(有关更多信息,请参阅“关注点”部分)。因此,我决定创建一个能够同时处理伺服器控制和 RC 接收器输入的库。

Using the Code

有关库如何工作的详细信息,请参阅下面的“关注点”部分。

我假设您已经熟悉使用 Pico SDK 编程 Pico。如果不熟悉,请查看树莓派提供的优秀文档
如果您使用的是 Windows,您可能已经从此处下载了 Pico Windows 安装程序,并使用 VS Code 设置了您的开发环境。

要使用该库,请下载附件并将其解压缩到您计算机上的某个文件夹中(或从 Github 克隆仓库)。

然后,您可以打开一个示例项目(请参阅下一节)或按如下方式将库添加到您自己的项目中:

在项目的 CMakeLists.txt 文件中,添加指向库的路径

# set the location of the pico_rc library
add_subdirectory("../../../pico_rc" pico_rc)

在上面的示例中,我使用了指向 pico_rc 文件夹的相对路径,在我的情况下,该文件夹位于项目文件夹的三个级别之上。您需要根据需要调整路径,以指向您解压缩 pico_rc 文件夹的位置。

接下来,将库 pico_rc 添加到 CMakeLists.txttarget_link_libraries 部分,就像添加任何其他 Pico SDK 库一样。这是一个示例:

target_link_libraries(receiver 
  pico_stdlib  
  pico_rc
)

现在您可以在代码中使用该库了。只需在源文件中包含 rc.h

#include "pico/rc.h"

打开示例项目

该库在 examples 文件夹中包含三个示例项目:

  • Control - 展示了如何读取 RC 接收器的输入并控制一些伺服器
  • Receiver - 读取 RC 接收到的输入并打印出来
  • Servo - 让伺服器来回移动

在本节中,我将尝试提供打开示例项目的分步说明。说明可能因操作系统而异,并且在你阅读本文时可能已经过时。请根据你开发 Pico 程序自身的经验进行调整。

要在 Visual Studio Code(已配置为 Pico 开发)中打开任何示例:

  • 在 VS Code 中选择打开文件夹
  • 选择包含示例的文件夹,例如 servocontrol。不要选择整个 examples 文件夹。
  • VS Code 将打开文件夹并提示你选择 Kit。你应该选择 Pico ARM GCC... Kit(在 Windows 上)。
  • 等待项目配置和构建完成。不应有任何错误。
  • 在 Explorer 视图中,你应该能看到 main.c 文件,你可以打开它来查看示例代码。
  • 要构建示例,请切换到左侧边栏中的CMake 视图。当鼠标悬停在 servo > servo 的树形结构上时,你应该会看到一个Build按钮。点击它将构建程序。
  • 要运行程序,我使用 Picoprobe,所以我转到Run and Debug视图,然后点击顶部的绿色Start Debugging 按钮
  • 你也可以通过将 .uf2 文件拖放到 Pico 连接到计算机时创建的虚拟驱动器上来将程序上传到你的 Pico。在 CMakeLists.txt 中取消注释 pico_add_extra_outputs(servo) 行,以便在构建过程中生成 .uf2 文件。然后你会在程序文件夹的 /build 子文件夹中找到 .uf2 文件。

连接硬件

我假设您知道如何将伺服器和 RC 接收器连接到 Pico,但以下是 Control 示例中使用的单个伺服器和 RC 接收器的基本接线。

wiring diagram

请注意,RC 伺服器和接收器设计为在 5 V 下工作,但在我的设置中,我从 Pico 的 3V3 引脚为其供电也没有问题。您也可以使用 VSYS 引脚,它将来自 USB 的 5V 提供给您。我使用 VSYS 引脚为目标 Pico 从另一个 Pico(充当调试探针)供电(如《树莓派 Pico 入门指南》文档中所述),因此对我来说,使用 3V3 引脚更方便。

这是我的测试设置。

pico with connected components

使用库

在接下来的章节中,我将介绍如何使用 pico_rc 库来处理伺服器和 RC 接收器。

伺服器

要使用伺服器,您首先需要创建一个伺服器“对象”。

rc_servo myServo1 = rc_servo_init(SERVO1_PIN);  

它实际上是一个 C 语言结构体,而不是 C++ 对象,这就是为什么用引号括起来。SERVO1_PIN 是您想用于控制伺服器的 Pico 引脚编号。例如:

#define SERVO1_PIN    6 

接下来,您应该像这样调用 rc_servo_start

rc_servo_start(&myServo1, 90);  

您将伺服器“对象myServo1(确切地说,是它的地址,注意 &)作为第一个参数,将伺服器应移动到的目标角度(90 度)作为第二个参数。要将伺服器移动到另一个位置,您可以调用 rc_servo_set_angle,例如:

rc_servo_set_angle(&myServo1, angle); 

还有一个函数可以设置以微秒为单位的脉冲宽度而不是以度为单位的角度,即 rc_servo_set_micros。如果您不明白我在说什么,请参阅下面的“关注点”部分中的解释。

这是库中包含的 Servo 示例的完整代码。

/* Example for pico_rc library.
Sweep two servos connected to pins 6 and 7.
*/
#include <stdio.h>
#include "pico/stdlib.h"
#include "pico/rc.h"

// Note that neighbor pins like 0,1 or 2,3 or 4,5 share the same 
// pwm hardware called SLICE. 
#define SERVO1_PIN      6
#define SERVO2_PIN      7

int main(void) {
  setup_default_uart();
  printf("Servo example for rc library\n");

  // create servo "objects"
  rc_servo myServo1 = rc_servo_init(SERVO1_PIN);  
  rc_servo myServo2 = rc_servo_init(SERVO2_PIN);  

  // start controlling the servos (generate PWM signal)
  rc_servo_start(&myServo1, 90);   // set servo1 na 90 degrees
  rc_servo_start(&myServo2, 180);   // set servo to 180 deg

  uint angle = 0;
  bool up = true;
  while ( 1 ) {
	  
	  if ( up ) {
		  angle++;
		  if ( angle == 180 )
			  up = false;
	  }
	  else {
		  angle--;
		  if ( angle == 0 )
			  up = true;    
	  }
  
	  rc_servo_set_angle(&myServo1, angle);  
	  rc_servo_set_angle(&myServo2, 180 - angle);  
  
	  sleep_ms(25);          
  }    // while 1
  return 0;
}

RC 接收器

要从 RC 接收器获取输入,您首先需要像这样初始化输入:

rc_init_input(CHANNEL1_PIN, true);

CHANNEL1_PIN 是您连接接收器输出通道的引脚编号。例如:

#define CHANNEL1_PIN    2

rc_init_input 中的 true 参数告诉库立即开始监视输入引脚。您也可以传递 false,如果您计划在开始监视之前设置更多输入。在这种情况下,您将使用 rc_set_input_enabled 在稍后启动监视每个输入。

一旦输入被初始化并启用,您就可以通过调用 rc_get_input_pulse_width(引脚编号) 来读取此输入的脉冲宽度。

uint32_t pulse = rc_get_input_pulse_width(CHANNEL1_PIN);

rc_get_input_pulse_width 函数将返回脉冲的宽度(以微秒为单位)。

它接受一个参数——Pico 引脚的编号。很自然,这应该是您之前使用 rc_init_input 初始化过的引脚。

该库在后台使用中断测量脉冲,因此调用 rc_get_input_pulse_width 不会阻塞您的程序。该函数会立即返回最后一次在输入上看到的脉冲宽度。

您可以同时监视最多 RC_MAX_CHANNELS 个输入引脚。默认情况下,它是 5,但可以轻松更改,正如在配置库的部分中所述。

如果您好奇脉冲是如何实际测量的,请参阅关注点部分。

请注意,rc_get_input_pulse_width 返回的值可能为零,如果没有脉冲,或者脉冲超出有效范围。除了连接接收器到错误引脚等明显问题外,当您的接收器丢失发射器的信号时也可能发生这种情况。您可能希望检测到这种情况,以便执行一些操作,例如将输出设置为故障保护值。这就是库中存在 rc_reset_input_pulse_width 函数的原因。下面的完整示例展示了如何检测信号丢失。

/* Example for pico_rc library.
 Read pulses from RC receiver and print them to UART.
*/
#include <stdio.h>
#include "pico/stdlib.h"
#include "pico/rc.h"

#define CHANNEL1_PIN     2
#define CHANNEL2_PIN     3
#define CHANNEL3_PIN     4

int main() {
    setup_default_uart();
    printf("Example for rc library\n");
    
    // set pin as input from RC receiver and start measuring pulses 
    // on this pin
    rc_init_input(CHANNEL1_PIN, true);
    rc_init_input(CHANNEL2_PIN, true);
    rc_init_input(CHANNEL3_PIN, true);

    while (1)
    {
        // Read input from RC receiver - that is pulse width on input pin.
        // Valid range is about 1000 to 2000 us
        // 0 means that the pulses are out of range in most cases.
        // It can also mean there are no pulses 
        // if there were no pulses at all since the program started.
        // If the signal is lost on the input pin, the last valid value will be
        // returned by rc_get_input_pulse_width because the ISR is not called
        // so the value is never updated.
        // todo: It would be good idea to average several pulses
        uint32_t pulse = rc_get_input_pulse_width(CHANNEL1_PIN);
        printf("Pulse ch1= %lu\n", pulse);

        pulse = rc_get_input_pulse_width(CHANNEL2_PIN);
        printf("Pulse ch2= %lu\n", pulse);
        
        pulse = rc_get_input_pulse_width(CHANNEL3_PIN);
        printf("Pulse ch3= %lu\n", pulse);
        
        // Example how to test if a channel is active:
        // The pulse will be 0 if the channel is inactive from the beginning but
        // it will be the last valid value if the pulses stop later.
        // To determine if there are pulses...
        rc_reset_input_pulse_width(CHANNEL1_PIN);
        sleep_ms(30);   // RC period is 20 ms so in 30 there should be some pulse
        pulse = rc_get_input_pulse_width(CHANNEL1_PIN);
        if ( pulse == 0 )
            printf("Channel 1 disconnected\n");

        sleep_ms(400);
    }

    return 0;
}

配置库

您可以配置一些参数来自定义库。要设置每个参数的值,只需在 CMakeLists.txt 中定义它,如下所示:

# SPECIFY preprocessor definitions for this project
target_compile_definitions(servo PRIVATE
    RC_SERVO_MIN_PULSE=1100
    RC_SERVO_MAX_PULSE=1900  
  )

在此示例中,我们覆盖了发送到伺服器的脉冲的默认最小和最大宽度。

可以定义以下参数。默认值请参阅 rc.c 文件。

RC_MAX_CHANNELS

RC_MIN_PULSE_WIDTH
RC_MAX_PULSE_WIDTH  

RC_SERVO_MIN_PULSE
RC_SERVO_MAX_PULSE
RC_SERVO_MIN_ANGLE
RC_SERVO_MAX_ANGLE

希望这些名称不言自明。

请注意,伺服器和接收器有各自的脉冲宽度最小/最大值。对于 RC 接收器,这些值实际上定义了什么被认为是有效脉冲。如果测量到的脉冲宽度小于 RC_MIN_PULSE_WIDTH 或大于 RC_MAX_PULSE_WIDTH,库将报告脉冲宽度为 0。

关注点

本节或多或少地详细描述了库的工作原理。不一定需要了解所有这些,但和往常一样,有时它可能很有用。

业余伺服器

您可能知道 RC 伺服器是如何控制的。如果不知道,请查看许多关于此的帖子,例如,这里

总而言之,伺服器由 PWM(脉冲宽度调制)信号控制。脉冲宽度对于伺服器的一个端点位置为 1 毫秒,对于另一个端点位置为 2 毫秒。

脉冲以 20 毫秒的周期重复,因此频率为 50 Hz。

要控制伺服器,您需要在一个输出引脚上以 50 Hz 的频率生成 PWM 信号,占空比在 5% 到 10% 之间(1 到 2 毫秒)。使用 Pico C SDK 的 PWM 函数相对容易做到这一点。

您应该知道的一个术语是“切片”。Pico 有 8 个硬件 PWM 模块,每个模块能够控制两个引脚(因此,总共可以控制 16 个引脚)。这也意味着两个引脚总是共享一个 PWM 单元。控制一个引脚的子单元称为切片。

RC 接收器

RC 接收器的工作方式如下:它通过无线电从发射器(您手中握着的带有摇杆或轮子的盒子)接收命令。接收器将这些“无线电命令”转换为具有 1 到 2 毫秒脉冲的标准 PWM 信号,该信号每 20 毫秒重复一次。通常,您将伺服器连接到 RC 接收器并用发射器控制它们。在我们的情况下,我们将 Pico 连接到接收器以根据需要处理命令。

例如,您可能有一个双轮(差速驱动)机器人,您想用 RC 汽车发射器控制它。因此,您需要将一个接收器通道(转向)的命令转换为控制机器人的两个电机,以便它按要求转向。

我们 RC 库的任务是找出输入引脚上的脉冲有多长,并将此信息提供给用户代码。

如何测量输入脉冲的宽度?

如果您熟悉 Arduino,您可能会回答 pulseIn()。Pico 中没有 pulseIn,但不要难过,这个函数反正不好。为什么?它会阻塞程序,等待输入引脚从低电平变为高电平再变为低电平,并返回引脚保持高电平的时间。如果您只需要测量一个引脚的脉冲,这就可以了。但通常,您需要 RC 接收器的多个输入,例如 RC 汽车的油门和转向。现在您该怎么办?可能这样做:

channel1 = pulseIn(PIN1);
channel2 = pulseIn(PIN2);
channel3 = pulseIn(PIN3);
etc..

有问题吗?

也许。RC 接收器没有标准来说明不同通道的脉冲按什么顺序排列(例如,先通道 1,然后通道 2 等)。也不能保证脉冲不会重叠。一些接收器甚至可能同时开始脉冲,而不是一个接一个地开始。

在大多数情况下,上述代码会起作用,但很可能您会在等待一个通道的脉冲时跳过其他通道的脉冲,例如,当接收器正在发送通道 2 或 3 的脉冲时。如果您不幸在某个通道的脉冲结束后才开始 pulseIn,并且不得不等待下一个脉冲出现,那么获取所有三个读数可能需要长达 60 毫秒。

如此长时间阻塞程序可能会有问题。

如何在 Pico 上做得更好?在 Pico C SDK 中,pico-examples 的 PWM 类别中有一个 measure_duty_cycle 示例,它演示了如何使用 Pico PWM 硬件来测量输入信号。它看起来是我们需要的,但它不适合我们的目的。

Pico 的 PWM 硬件只能对输入做两件事。它可以计算输入脉冲(当输入引脚上的逻辑电平发生变化时递增计数器),或者它可以在输入引脚为高电平期间递增计数器。

后一种选项在 measure_duty_cycle 示例中使用。它测量输入 10 毫秒,然后检查在这 10 毫秒内计数器达到的值。我们知道计数器的速度(我们设置了它),所以我们知道它在 10 毫秒内可以达到的最大值。例如,如果计数器以 10 kHz 运行,它在 10 毫秒内可以达到 100。如果您读取计数器看到它是 20,您就知道占空比是 20/100,即 20%。

我称之为测量占空比的统计方法。它适用于快速信号。如果这 10 毫秒内有数百个或更多周期,结果就很好。但当您测量 RC 信号等慢速信号时,它就不起作用了。问题是您可以在脉冲中间开始或结束那 10 毫秒的测量间隔。显然,该脉冲的宽度未被正确测量。如果这 10 毫秒内有很多脉冲,则可以忽略此错误。但如果只有几个脉冲,该错误可能会显着影响结果。

例如,考虑一个周期为 5 毫秒,脉冲宽度为 2 毫秒(即 40% 占空比)的输入信号。如果您测量 10 毫秒,在理想情况下(如果您在脉冲开始时开始测量),您将得到两个完整的脉冲,即计数器运行了 10 毫秒中的 4 毫秒,即 40%——您的结果是正确的。

但是假设您从脉冲中间开始,所以您“计数”了 1 毫秒而不是 2 毫秒。然后 5 毫秒后,另一个脉冲出现,它被正确地计数了 2 毫秒。总共,您得到 10 毫秒中的 3 毫秒,并计算占空比为 3/10,约为 30%。您的结果是错误的。

SDK 示例测量频率约为 120 kHz 的 PWM 信号,因此没有这样的问题。不幸的是,有些人会在不理解原理的情况下对低得多的频率使用该示例,并奇怪为什么他们的结果不那么好。

RC 信号非常慢,以至于您可以毫不费力地看到问题。它有一个 20 毫秒的周期和 1 到 2 毫秒的脉冲宽度。如果您测量 10 毫秒,有时您可能一无所获,有时您会得到一些东西。但您如何知道您捕获了从一开始的脉冲?如果您碰巧在脉冲中间或接近脉冲末尾开始测量怎么办?在这两种情况下,您得到的脉冲比实际的要短。您需要测量比 10 毫秒长得多的时间才能获得好的结果。但您不想阻塞程序那么长时间;即使是 10 毫秒也太长了。您不想模仿 pulseIn

因此,我寻找了另一种处理脉冲测量的方法。这似乎是使用 GPIO 中断。Pico 引脚可以在引脚电平变化时触发中断。这是许多微控制器的一个非常常见的功能。

以下是库中的工作方式:

  • 设置 GPIO 引脚以在引脚电平变化时触发中断。
  • 在中断服务例程 (ISR) 中,在上升沿(引脚从低电平变为高电平;即脉冲开始时)将由 get_absolute_time Pico SDK 函数获取的当前时间保存下来。
  • 在下降沿,计算当前时间减去保存的开始时间,以查看脉冲有多长。如果它在有效范围内,则保存结果。

这发生在后台,在中断处理程序中。ISR 将脉冲宽度保存到一个数组 gRcInputChannels 中,其中每个输入引脚都有自己的数据(请参阅 rc.c 中的 rc_channel_info struct)。

库的用户调用 rc_get_input_pulse_width,它返回最新的脉冲宽度。

请注意,该库使用所谓的原始 irq 处理程序来处理中断。这是因为普通处理程序由所有引脚共享,因此如果库定义了该处理程序,用户代码就无法使用 GPIO 中断。使用原始处理程序,没有这个问题,因为它们是按引脚定义的。因此,用户代码仍然可以使用 GPIO 中断。

结论

库的代码在 Github 上。
我才刚开始学习使用树莓派 Pico,但到目前为止,我非常喜欢它。C SDK 设计得很好,文档也非常出色。考虑到树莓派 Pico 板的价格和计算能力,我认为 Arduino 在 DIY 项目的首选开发板方面有一个强劲的竞争对手。

历史

  • 2023 年 5 月 8 日 - 第一个版本
© . All rights reserved.