亿迅智能制造网
工业4.0先进制造技术信息网站!
首页 | 制造技术 | 制造设备 | 工业物联网 | 工业材料 | 设备保养维修 | 工业编程 |
home  MfgRobots >> 亿迅智能制造网 >  >> Industrial programming >> VHDL

使用来自 FPGA 引脚的 PWM 的 RC 伺服控制器

无线电控制 (RC) 模型伺服系统是微型执行器,通常用于爱好者模型飞机、汽车和船只。它们允许操作员通过无线电链路远程控制车辆。由于 RC 模型由来已久,事实上的标准接口是脉宽调制 (PWM),而不是数字方案。

幸运的是,使用 FPGA 可以施加在其输出引脚上的精确时序很容易实现 PWM。在本文中,我们将创建一个通用伺服控制器,适用于任何使用 PWM 的 RC 伺服。

RC 伺服的 PWM 控制如何工作

我已经在之前的博客文章中介绍了 PWM,但我们不能使用该模块来控制 RC 伺服。问题是 RC 伺服系统不希望 PWM 脉冲如此频繁地到达。它不关心整个占空比,只关心高周期的持续时间。

上图显示了 PWM 信号的时序是如何工作的。

脉冲之间的理想间隔是 20 毫秒,尽管它的持续时间不太重要。 20 ms 转换为 50 Hz 的 PWM 频率。这意味着舵机每 20 毫秒获得一个新的位置命令。

当一个脉冲到达 RC 伺服器时,它会对高周期的持续时间进行采样。时间很关键,因为这个时间间隔直接转化为伺服上的角位置。大多数舵机希望看到脉冲宽度在 1 到 2 毫秒之间变化,但没有固定的规则。

VHDL伺服控制器

我们将创建一个通用的 VHDL 伺服控制器模块,您可以将其配置为使用 PWM 与任何 RC 伺服一起工作。为此,我们需要根据通用输入的值进行一些计算。

与 FPGA 的兆赫开关频率相比,RC 伺服系统使用的 PWM 频率较慢。时钟周期的整数计数为 PWM 脉冲长度提供了足够的精度。但是,除非时钟频率与脉冲周期完全匹配,否则会有一个小的舍入误差。

我们将使用 real 执行计算 (浮点)数字,但最终,我们必须将结果转换为整数。与大多数编程语言不同,VHDL 将浮点数舍入到最接近的整数,但半数(0.5、1.5 等)的行为未定义。模拟器或综合工具可以选择任意一种方式舍入。

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.round;

为了确保跨平台的一致性,我们将使用 round math_real 中的函数 库,它总是从 0 开始舍入。上面的代码显示了我们的 VHDL 模块中的导入,带有 math_real 库突出显示。

如果您需要此项目的完整代码,您可以通过在下面的表格中输入您的电子邮件地址来下载它。几分钟之内,您将收到一个包含 VHDL 代码、ModelSim 项目和 iCEstick FPGA 板的 Lattice iCEcube2 项目的 Zip 文件。

带有泛型的伺服模块实体

通过使用通用常量,我们可以创建一个适用于任何启用 PWM 的 RC 伺服的模块。下面的代码显示了伺服模块的实体。

第一个常数是以实数形式给出的 FPGA 的时钟频率,而 pulse_hz 指定 PWM 输出的脉冲频率,以下两个常数设置最小和最大位置的脉冲宽度(以微秒为单位)。最后的通用常量定义了最小和最大位置之间有多少步,包括端点。

entity servo is
  generic (
    clk_hz : real;
    pulse_hz : real; -- PWM pulse frequency
    min_pulse_us : real; -- uS pulse width at min position
    max_pulse_us : real; -- uS pulse width at max position
    step_count : positive -- Number of steps from min to max
  );
  port (
    clk : in std_logic;
    rst : in std_logic;
    position : in integer range 0 to step_count - 1;
    pwm : out std_logic
  );
end servo;

除了时钟和复位外,端口声明还包括一个输入和一个输出信号。

位置 信号是伺服模块的控制输入。如果我们将其设置为零,模块将产生 min_pulse_us 微秒长的 PWM 脉冲。当位置 处于最大值时,会产生 max_pulse_us 长脉冲。

pwm 输出是外部 RC 伺服的接口。它应该通过一个 FPGA 引脚并连接到伺服上的“信号”输入,通常是黄色或白色线。请注意,您可能需要使用电平转换器。大多数 FPGA 使用 3.3 V 逻辑电平,而大多数 RC 舵机在 5 V 上运行。

声明区域

在伺服模块的声明区域的顶部,我声明了一个函数,我们将使用它来计算一些常数。 cycles_per_us 函数,如下所示,返回我们需要计算的最接近的时钟周期数以测量 us_count 微秒。

function cycles_per_us (us_count : real) return integer is
begin
  return integer(round(clk_hz / 1.0e6 * us_count));
end function;

在函数的正下方,我们声明了辅助常量,我们将使用它来根据泛型来制作输出 PWM 的时序。

首先,我们将最小和最大微秒值转换为绝对时钟周期数:min_countmax_count .然后,我们计算两者之间的微秒范围,从中我们得出 step_us ,每个线性位置步长之间的持续时间差。最后,我们将微秒 real 将值设置为固定数量的时钟周期:cycles_per_step .

constant min_count : integer := cycles_per_us(min_pulse_us);
constant max_count : integer := cycles_per_us(max_pulse_us);
constant min_max_range_us : real := max_pulse_us - min_pulse_us;
constant step_us : real := min_max_range_us / real(step_count - 1);
constant cycles_per_step : positive := cycles_per_us(step_us);

接下来,我们声明 PWM 计数器。这个整数信号是一个自由运行的计数器,包含 pulse_hz 每秒次。这就是我们如何实现泛型中给出的 PWM 频率。下面的代码展示了我们如何计算我们必须计数到的时钟周期数,以及我们如何使用常量来声明整数的范围。

constant counter_max : integer := integer(round(clk_hz / pulse_hz)) - 1;
signal counter : integer range 0 to counter_max;

signal duty_cycle : integer range 0 to max_count;

最后,我们声明一个名为 duty_cycle 的计数器副本 .该信号将决定 PWM 输出的高电平周期的长度。

计算时钟周期

下面的代码展示了实现自由运行计数器的过程。

COUNTER_PROC : process(clk)
begin
  if rising_edge(clk) then
    if rst = '1' then
      counter <= 0;

    else
      if counter < counter_max then
        counter <= counter + 1;
      else
        counter <= 0;
      end if;

    end if;
  end if;
end process;

不像签名未签名 自包装的类型,我们需要在计数器达到最大值时显式分配零。因为我们已经在 counter_max 中定义了最大值 常量,使用 If-Else 结构很容易实现。

PWM输出过程

为了确定 PWM 输出应该是高值还是低值,我们比较 counterduty_cycle 信号。如果计数器小于占空比,则输出为高值。因此,duty_cycle 的值 信号控制PWM脉冲的持续时间。

PWM_PROC : process(clk)
begin
  if rising_edge(clk) then
    if rst = '1' then
      pwm <= '0';

    else
      pwm <= '0';

      if counter < duty_cycle then
        pwm <= '1';
      end if;

    end if;
  end if;
end process;

计算占空比

占空比不应小于 min_count 时钟周期,因为这是对应于 min_pulse_us 的值 通用输入。因此,我们使用 min_count 作为 duty_cycle 的重置值 信号,如下图。

DUTY_CYCLE_PROC : process(clk)
begin
  if rising_edge(clk) then
    if rst = '1' then
      duty_cycle <= min_count;

    else
      duty_cycle <= position * cycles_per_step + min_count;

    end if;
  end if;
end process;

当模块未复位时,我们计算占空比作为输入位置的函数。 cycles_per_step 常数是一个近似值,四舍五入到最接近的整数。因此,这个常数的误差可能高达 0.5。当我们与命令位置相乘时,误差会扩大。但是,由于 FPGA 时钟远快于 PWM 频率,因此不会引起注意。

伺服测试台

为了测试 RC 伺服模块,我创建了一个手动检查测试台,可以让我们观察伺服模块在波形中的行为。如果您的计算机上安装了 ModelSim,您可以通过在下表中输入您的电子邮件地址来下载示例仿真项目。

模拟常数

为了加快仿真时间,我们将在测试台中指定 1 MHz 的低时钟频率。我还将步数设置为 5,这足以让我们看到正在运行的被测设备 (DUT)。

下面的代码显示了测试台中定义的所有模拟常量。

constant clk_hz : real := 1.0e6;
constant clk_period : time := 1 sec / clk_hz;

constant pulse_hz : real := 50.0;
constant pulse_period : time := 1 sec / pulse_hz;
constant min_pulse_us : real := 1000.0;
constant max_pulse_us : real := 2000.0;
constant step_count : positive := 5;

DUT 信号

测试台中声明的信号与 DUT 的输入和输出相匹配。正如我们从下面的代码中看到的,我给出了 clk第一 表示初始值“1”。这意味着时钟将从高位开始,并且模块最初将处于复位状态。

signal clk : std_logic := '1';
signal rst : std_logic := '1';
signal position : integer range 0 to step_count - 1;
signal pwm : std_logic;

为了在测试台中生成时钟信号,我使用如下所示的常规单线过程。

clk <= not clk after clk_period / 2;

DUT 实例化

在时钟激励线下方,我们继续实例化 DUT。我们将测试台常量分配给具有匹配名称的泛型。此外,我们将 DUT 的端口信号映射到测试台中的本地信号。

DUT : entity work.servo(rtl)
generic map (
  clk_hz => clk_hz,
  pulse_hz => pulse_hz,
  min_pulse_us => min_pulse_us,
  max_pulse_us => max_pulse_us,
  step_count => step_count
)
port map (
  clk => clk,
  rst => rst,
  position => position,
  pwm => pwm
);

测试台定序器

为了为 DUT 提供刺激,我们使用如下所示的序列器过程。首先,我们重置 DUT。然后,我们使用 For 循环遍历所有可能的输入位置(在我们的例子中是 5 个)。最后,我们向模拟器控制台打印一条消息并通过调用 VHDL-2008 finish 结束测试平台 程序。

SEQUENCER : process
begin
  wait for 10 * clk_period;
  rst <= '0';

  wait for pulse_period;

  for i in 0 to step_count - 1 loop
    position <= i;
    wait for pulse_period;
  end loop;

  report "Simulation done. Check waveform.";
  finish;
end process;

伺服仿真波形

下面的波形显示了测试台在 ModelSim 中产生的部分波形。我们可以看到测试台周期性地改变位置输入,并且 DUT 通过产生 PWM 脉冲来响应。请注意,PWM 输出仅在最低计数器值时为高电平。这就是我们的 PWM_PROC 进程的工作。

如果您下载项目文件,您应该能够按照 Zip 文件中的说明重现模拟。

FPGA 实现示例

我想要的下一件事是在 FPGA 上实现设计并让它控制现实生活中的 RC 伺服器 TowerPro SG90。为此,我们将使用 Lattice iCEstick FPGA 开发板。它与我在初学者的 VHDL 课程和高级 FPGA 课程中使用的板相同。

如果您有 Lattice iCEstick,您可以使用下面的表格下载 iCEcube2 项目。

但是,伺服模块不能单独行动。我们需要一些支持模块才能在 FPGA 上工作。至少,我们需要一些东西来改变输入位置,我们还应该有一个重置模块。

为了使伺服运动更有趣,我将使用我在之前的文章中介绍过的 Sine ROM 模块。 Sine ROM 与前面文章中的 Counter 模块一起生成平滑的左右移动模式。

在此处了解 Sine ROM 模块:
如何使用存储在块 RAM 中的正弦波创建呼吸 LED 效果

下面的数据流程图显示了子模块及其连接方式。

顶级模块实体

顶部模块的实体包括时钟和复位输入以及控制 RC 伺服的 PWM 输出。我已经路由了 pwm 信号到 Lattice iCE40 HX1K FPGA 上的引脚 119。那是最左边的头架上最左边的别针。时钟来自 iCEstick 的板载振荡器,我已经连接了 rst 信号到配置有内部上拉电阻的引脚。

entity top is
  port (
    clk : in std_logic;
    rst_n : in std_logic; -- Pullup
    pwm : out std_logic
  );
end top; 

信号和常量

在顶部模块的声明区域中,我定义了与 Lattice iCEstick 和我的 TowerPro SG90 伺服匹配的常量。

通过实验,我发现 0.5 ms 到 2.5 ms 给了我想要的 180 度运动。互联网上的各种来源提出了其他价值观,但这些对我有用。我不完全确定我使用的是合法的 TowerPro SG90 伺服器,它可能是假冒的。

如果是这样的话,我是从网上卖家那里买的,这是无意的,但它可能解释了不同的脉冲周期值。我已经用我的示波器验证了持续时间。它们就是下面显示的代码中所写的内容。

constant clk_hz : real := 12.0e6; -- Lattice iCEstick clock
constant pulse_hz : real := 50.0;
constant min_pulse_us : real := 500.0; -- TowerPro SG90 values
constant max_pulse_us : real := 2500.0; -- TowerPro SG90 values
constant step_bits : positive := 8; -- 0 to 255
constant step_count : positive := 2**step_bits;

我已经设置了 cnt 自由运行计数器的信号为 25 位宽,这意味着在 iCEstick 的 12 MHz 时钟上运行时,它将在大约 2.8 秒内回绕。

constant cnt_bits : integer := 25;
signal cnt : unsigned(cnt_bits - 1 downto 0);

最后,我们根据我之前介绍的数据流程图声明将连接顶层模块的信号。我将在本文后面展示下面的信号如何相互作用。

signal rst : std_logic;
signal position : integer range 0 to step_count - 1;
signal rom_addr : unsigned(step_bits - 1 downto 0);
signal rom_data : unsigned(step_bits - 1 downto 0);

伺服模块实例化

伺服模块的实例化与我们在测试台中的做法类似:常量到通用,本地信号到端口信号。

SERVO : entity work.servo(rtl)
generic map (
  clk_hz => clk_hz,
  pulse_hz => pulse_hz,
  min_pulse_us => min_pulse_us,
  max_pulse_us => max_pulse_us,
  step_count => step_count
)
port map (
  clk => clk,
  rst => rst,
  position => position,
  pwm => pwm
);

自包装计数器实例化

我在之前的文章中使用了自包装计数器模块。这是一个自由运行的计数器,计数到 counter_bits ,然后再次归零。没什么好说的,但是如果你想检查它,你可以下载示例项目。

COUNTER : entity work.counter(rtl)
generic map (
  counter_bits => cnt_bits
)
port map (
  clk => clk,
  rst => rst,
  count_enable => '1',
  counter => cnt
);

正弦 ROM 实例化

我在之前的文章中详细解释了 Sine ROM 模块。简而言之,它将线性数值转换为具有相同最小/最大振幅的完整正弦波。输入是 addr 信号,正弦值出现在 data 输出。

SINE_ROM : entity work.sine_rom(rtl)
generic map (
  data_bits => step_bits,
  addr_bits => step_bits
)
port map (
  clk => clk,
  addr => rom_addr,
  data => rom_data
);

我们将使用如下所示的并发分配来连接 Counter 模块、Sine ROM 模块和 Servo 模块。

position <= to_integer(rom_data);
rom_addr <= cnt(cnt'left downto cnt'left - step_bits + 1);

伺服模块的位置输入是正弦 ROM 输出的副本,但我们必须将无符号值转换为整数,因为它们属于不同类型。对于 ROM 地址输入,我们使用自由运行计数器的高位。通过这样做,正弦波运动周期将在 cnt 信号在 2.8 秒后结束。

在 Lattice iCEstick 上测试

我已经在面包板上连接了整个电路,如下图所示。因为 FPGA 使用 3.3 V 而伺服运行在 5 V,所以我使用了外部 5 V 电源和面包板电平转换器。不考虑电平转换器,FPGA 引脚的 PWM 输出直接进入 TowerPro SG90 伺服器上的“信号”线。

轻弹电源开关后,舵机应以平滑的 180 度运动来回移动,在极限位置稍微停止。下面的视频显示了我在示波器上显示 PWM 信号的设置。

最后的想法

与往常一样,有很多方法可以实现 VHDL 模块。但我更喜欢本文中概述的方法,使用整数类型作为计数器。所有繁重的计算都发生在编译时,产生的逻辑只有计数器、寄存器和多路复用器。

在 VHDL 中处理 32 位整数时最大的危险是它们在计算中无声地溢出。您必须检查预期输入范围内的任何值不会溢出子表达式。我们的伺服模块适用于任何实际的时钟频率和伺服设置。

请注意,这种 PWM 不适用于 RC 伺服以外的大多数应用。对于模拟功率控制,占空比比开关频率更重要。

在此处阅读有关使用 PWM 进行模拟电源控制的信息:
如何在 VHDL 中创建 PWM 控制器

如果您想自己尝试这些示例,可以通过下载我为您准备的 Zip 文件快速开始。在下面的表格中输入您的电子邮件地址,您将在几分钟内收到开始使用所需的一切!该软件包包含完整的 VHDL 代码、带有运行脚本的 ModelSim 项目、Lattice iCEcube2 项目和 Lattice Diamond 编程器配置文件。

在评论区告诉我你的想法!


VHDL

  1. PWM 功率控制器
  2. 如何在 VHDL 中创建 PWM 控制器
  3. 如何使用 TEXTIO 从文件初始化 RAM
  4. 使用 InitialState
  5. 使用 Raspberry Pi 监控家中温度
  6. 分步说明:如何使用 IIoT 从 PLC 获取数据?
  7. 在安全 FPGA SoC 上使用 TEE 保护机舱内 AI 的示例
  8. 在机脚中使用定位销的替代方法
  9. 使用 Firebase 将传感器数据从一个 Arduino 发送到另一个
  10. 您的伺服控制器可维修吗?
  11. 来自工厂的真实生活:C 轴驱动器不正常 伺服驱动器错误
  12. 25 kHz 4 Pin PWM 风扇控制与 Arduino Uno