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

通过 C# 程序与 Arduino UNO 通信以控制 16 位定时器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (7投票s)

2014 年 1 月 15 日

CPOL

6分钟阅读

viewsIcon

38581

downloadIcon

2560

本文介绍了如何通过 USB 串行端口向 Arduino UNO 板发送命令,并控制 16 位定时器/计数器的波形。

引言

在上一篇文章中,我介绍了一个用于控制 Arduino UNO 板上步进电机的简单库。在本文中,我将演示驱动 Arduino 板的 Atmel 微控制器另一个方面的功能:它的 16 位定时器。Atmel 芯片集成了非常强大的 16 位定时器,可用于多种用途。其中一项功能是,它可用于在 Arduino 板的 PIN9 和 PIN10 上生成波形,并在不同条件下触发内部中断。

为了理解 Arduino 的 16 位定时器的所有可能性,我建议您阅读 Atmel 芯片的 规格

以下演示展示了如何配置 16 位定时器在 PIN9/10 上生成方波信号,并发送一些命令来控制波形的频率。

Atmega 328 的 16 位定时器

Atmel 芯片的 16 位定时器可用于在 Arduino 板的 PIN9/10 上生成方波信号,并在计数器达到给定值时引发中断。我们将 PIN 信号设置为在每次达到计数器值时更改。

两次中断之间的周期由以下公式给出,它也是 PIN9/10 上输出信号的半周期

Poc = 2 * N * (1 + OCR) / Fclk

其中

  • N 是计数器分频器(预分频器)
  • OCR 是计数器值
  • Fclk 是芯片的时钟频率(Arduino UNO 为 16Mhz)

对于给定的周期 (Poc),计数器的值由以下公式给出

OCR = (Poc * Fclk) / (2 * N) - 1

在此示例中,我选择了 N = 256,因此计数器的公式为

OCR = Poc * 31250 - 1,其中 Poc 以秒为单位。

计数器可以使用几个寄存器进行编程

  • 输出比较寄存器 (OCR1A/B),用于设置计数值(16 位)
  • 定时器/计数器控制寄存器 (TCCR1A/B/C),用于编程计数器(8 位)
  • 定时器中断屏蔽寄存器 (TIMSK1),用于控制中断

编程定时器/计数器

TCCR1A - 定时器/计数器1 控制寄存器 A

7 6 5 4 3 2 1 0
COM1A1 COM1A0 COM1B1 COM1B0 - - WGM11 WGM12
0 1 0 1     0 0

TCCR1B - 定时器/计数器1 控制寄存器 B

7 6 5 4 3 2 1 0
ICNC1 ICES1 - WGM13 WGM12 CS12 CS11 CS10
0 0   0 1 1 0 0

TCCR1C - 定时器/计数器1 控制寄存器 C

7 6 5 4 3 2 1 0
FOC1A FOC1B - - - - - -
0 1            

比较输出模式,非 PWM 设置为在比较匹配时切换 OC1A/OC1B。波形生成设置为 CTC,TOP 值为 OCR1A,OCR1x 的更新是即时的。

时钟选择位设置为 100,将时钟分频为 256,最后通道 B 的强制输出比较设置为 1,因此 PIN10 的状态始终与 PIN9 相反。

TimerCounter1 库非常简单,它不打算提供一个完整的定时器库,但仅用于为演示设置波形发生器模式。此代码取自 CmdrArduino 库,该库使用 Arduino 来构建 DCC 指令站,用于模型铁路。

void TimerCounter1::setup_wave_generator(unsigned int counter)
{
  // Configure PortB 1 and 2 as output (PIN9, PIN10 of the Arduino board)
  DDRB |= (1<<DDB1) | (1<<DDB2);
    
  // Configure timer1 in CTC mode, for waveform generation, set to toggle OC1A, OC1B, 
  // at /256 prescalar, interrupt at CTC
  TCCR1A = (0<<COM1A1) | (1<<COM1A0) | (0<<COM1B1) | (1<<COM1B0) | (0<<WGM11) | (0<<WGM10);
  TCCR1B = (0<<ICNC1)  | (0<<ICES1)  | (0<<WGM13)  | (1<<WGM12)  | (1<<CS12)  | (0<<CS11) | 
           (0<<CS10);
    
  //Whenever we set OCR1A, we must also set OCR1B, or else pin OC1B will get out of sync with OC1A!
  OCR1A = OCR1B = counter; 
  TCNT1 = 0; //get the timer rolling (not really necessary? defaults to 0. Just in case.)
    
  //finally, force a toggle on OC1B so that pin OC1B will always complement pin OC1A
  TCCR1C |= (1<<FOC1B);
}

void TimerCounter1::enable_match_interrupt()
{
  //enable the compare match interrupt
  TIMSK1 |= (1<<OCIE1A);
}

这个简单演示中最重要的库是 Command 库,它处理发送到 sketch 的命令字节,通过串行线路执行命令。Command 类是一个抽象类,必须实现它来代表一组实际命令。在此演示中,PC 应用程序发送简单命令来控制波形发生器的周期。

namespace core
{
  /**
   * The class Command is designed to wrap a simple command represented by an array
   * of 20 bytes maximum. This is just an illustration of a command mechanism.
   * Commands are intended to be transmitted on a serial port and received byte per byte.
   * A command starts with a 0xF0 and ends with a 0xFF. The 0xFF value must not appear within 
   * the command as it would be interpreted as a command END.
   */
  class Command
  {
  protected:
    static const byte MAX_DATA = 20;
    byte commandData[MAX_DATA];
    bool ready;
      
    virtual int extractCmd(byte* cmd);   
    virtual bool executeCmd(void) = 0;
      
  private:
    int idx;
            
  public:
    Command() : idx(0), ready(false)
    {
    }
            
    /**
     * Adds a character to the command
     * @param item Command item
     */
    bool add(byte item);
        
    /** 
     * @return true if the command is complete
     */
    bool isReady(void);
            
    /**
     * Get the command
     * @param command Pointer to the command to be returned
     * @return true if the command is complete
     */
    bool getCommand(byte* command);
            
    /**
     * Clear the current command
     */
    void clear(void);

    /** 
     * Execute the current command
     * @return true if executed, false otherwise
     */
    bool execute(void);

    static const byte START = 0xF0;
    static const byte END = 0xFF;
  };
}

此类有一个纯虚方法,必须由任何继承自它的类来实现。此方法解释给类的命令字节并执行相应的命令。

此类的工作方式如下:

  • 主应用程序通过串行端口接收字节
  • 每个字节在接收时必须使用 add() 方法添加到命令中
  • 应用程序必须检查 isReady() 方法,当命令准备好时,该方法返回 true

命令由一个字节数组表示,该数组以 0xF0 开始,以 0xFF 结束。在此标记之间的字节是命令。命令可以包含 0xF0 值,但不能包含 0xFF,因为 0xFF 被解释为命令结束。这是一个限制,但请记住,这只是一个简单的演示... 另一个限制是命令缓冲区为 20 字节,包括 START 和 END 分隔符。

bool core::Command::add(byte item)
{
  bool ret = true;
  if (idx == 0 && item == START)
  {
    commandData[idx++] = item;
  }
  else if (item == END && idx > 0 && idx < MAX_DATA) 
  {
    commandData[idx++] = item;
    ready = true;
  }
  else if (!ready && idx > 0 && idx < MAX_DATA)
  {
    commandData[idx++] = item;
  }
  else
  {
    clear();
    ret = false;
  }
  
  return ret;
}

int core::Command::extractCmd(byte* cmd)
{
  int ret = -1;
  byte* ptrCmd = commandData;
  
  if (*(ptrCmd++) == START)
  {
    ret = 0;
    while(*ptrCmd != END && ret < (MAX_DATA - 1))
    {
      *(cmd++) = *(ptrCmd++);
      ++ret;
    }
  }
  
  return ret;
}

一种改进方法是使用回调替换 isReady() 中的轮询,当命令准备好时将调用该回调。

应用程序定义了以下命令:

  • 'a': 将周期设置为 1 秒
  • 'b': 将周期设置为 1/2 秒
  • 'c': 将周期设置为 1/4 秒
  • 'p<bcd period>': 将周期设置为 BCD(二进制编码十进制)给定的值。

处理这些命令的类的代码如下。

/**
 * This class implements the class Command to process simple commands
 * to control the counter value of a Timer
 */
class TimerCTC_Cmd : public Command
{
private:
  static const byte PERIOD_CMD = 'p';
  static const byte ONE_SEC_CMD = 'a';
  static const byte HALF_SEC_CMD = 'b';
  static const byte QUATER_SEC_CMD = 'c';
  unsigned int period;
                
public:
  TimerCTC_Cmd()
  {
  }
  
  /**
   * Gets the period extracted from a command
   */  
  unsigned int getPeriod();

private:    
  void ProcessPeriodCommand(byte* cmd);

protected:
  /** 
   * Execute the current command
   */
  virtual bool executeCmd(void);
};

/**
 * The executeCmd of the TimerCTC_Cmd class extract a 16 bits value 
 * from the command that can be used to change a Timer counter given 
 * a prescalar of 256
 */
bool TimerCTC_Cmd::executeCmd(void)
{
  bool ret = false;
  byte command[10];
  int cmdLen = 1;

  cmdLen = extractCmd(command);
  if (cmdLen != -1)
  {
    switch (*command)
    {
      case ONE_SEC_CMD:  // Set period to 1 second
      {
        period = 31249;
        ret = true;
    break;
      }

      case HALF_SEC_CMD:  // Set period to 1/2 second
      {
        period = 15624;
        ret = true;
    break;
      }

      case QUATER_SEC_CMD:  // Set period to 1/4 second
      {
        period = 7812;
        ret = true;
    break;
      }
      
      case PERIOD_CMD:  // Set the period to a a given value (0 to 65535)
      {
    ProcessPeriodCommand(command);
        ret = true;
    break;
      }
    }
  }

  return ret;
}

/**
 * Process the period command. The value is given in BCD format
 * @param cmd Command bytes
 */
void TimerCTC_Cmd::ProcessPeriodCommand(byte* cmd)
{
  period = -1;
  if (cmd[0] == 'p')
  {
    Converter::BCDValue bcd;
    for (int n = 0; n < sizeof(bcd.value); bcd.value[n] = cmd[1 + n++]);

    period = Converter::BCD2bin(bcd);
  }
}

演示应用程序:向 Arduino sketch 发送命令以更改波形发生器的周期

硬件配置

为了运行此 sketch 并看到效果,您需要连接以下组件:

  • PIN9:LED 及其限流电阻(220 至 470 欧姆)
  • PIN10:LED 及其限流电阻
  • PIN13:LED 或使用板载测试 LED

Arduino 上运行的 sketch 是一个非常简单的应用程序,它接收通过串行端口发送的字节,并将它们传递给 TimerCTC_Cmd 类进行处理。PIN9 和 PIN10 上的 LED 将以给定频率交替闪烁。当 PIN9 为高电平时,PIN10 将为低电平,依此类推。

它还包含一个 ISR(中断服务例程),当计数器与 OCR1A 的预定义值匹配时,由 Timer1 调用。此 ISR 将根据通过 TimerCTC_CmdgetPeriod() 方法发送的命令来更改此值。

代码如下。

#include <TimerCounter1.h>
#include <command.h>
#include <Converter.h>
#include "TimerCTC_Cmd.h"

#define HALF_SECOND    15624
#define    QUATER_SECOND    7812
#define ONE_SECOND      31249
#define BAUD_RATE       38400

volatile unsigned int cntr;
volatile unsigned int currcntr;
volatile byte state;

/**
 * Interrupt service routine called on every matched value of OCR1A
 * Use this routine to change the current OCR1Aand OCR1B value to adjust the 
 * counter period
 * PIN13 is set to blink at half the period of PIN9/10
 */
ISR(TIMER1_COMPA_vect)
{
  static volatile unsigned long counter = 0;
  
  // The original code uses if (PINB & (1 << PINB1) to check if the PIN is
  // low but it is apparently a bug
  if (!(PINB & (1 << PINB1)))
  {
    if (cntr != currcntr)
    {
      OCR1A = OCR1B = cntr;
      currcntr = cntr;
    }
  }
  
  if (counter % 2 == 0)
  {
    state = 1 - state;
    digitalWrite(13, state);
  }
  ++counter;
}

TimerCTC_Cmd command;

void setup() {
  Serial.begin(BAUD_RATE);
  TimerCounter1::setup_wave_generator(HALF_SECOND);
  TimerCounter1::enable_match_interrupt();  
  cntr = HALF_SECOND;
  currcntr = HALF_SECOND;
  pinMode(13, OUTPUT);
  state = 1;
}

/**
 * The main loop listens to characters on the default Serial port.
 * It passes the bytes read to the TimerCTC_Cmd command class.
 * When a command is complete, it is executed.
 * The command purpose is to change the period of the Timer1. This is 
 * done using the cntr global variable used by the ISR to change the 
 * timer OCR1A and OCR1B values.
 */
void loop() {
  if (Serial.available() > 0)
  {
    int inByte = Serial.read();
    command.add(inByte);
    if (command.isReady())
    {
      if (command.execute())
      {
        cntr = command.getPeriod();
      }
      
      command.clear();
    }
  }
}

为了向 Arduino 发送命令,我们需要一个简单的应用程序。.NET 应用程序可以轻松完成这项工作。

此应用程序通过打开 Arduino 的 COM 端口上的串行通道来连接到 Arduino。默认情况下,波形发生器的半周期设置为 1/2 秒。您可以使用按钮、滑块控件或数字文本框调整此值。您输入的不是毫秒值,而是实际写入 OCR1A 计数器的值。

以下扩展将 ushort 值转换为其 BCD 等效值。

public static class UintExtension
{
    /// <summary>
    /// Gets the BCD value of the given ushort parameter
    /// </summary>
    /// <param name="value">Value to convert</param>
    /// <returns>BCD bytes</returns>
    public static byte[] ToBCD(this ushort value)
    {
        byte[] bcdNumber = new byte[3];
        for (int n = 0; n < 3; n++)
        {
            bcdNumber[n] = (byte)(value % 10);
            value /= 10;
            bcdNumber[n] |= (byte)(value % 10 << 4);
            value /= 10;
        }

        return bcdNumber;
    }
}

结论和兴趣点

这是我写的第二篇关于 Arduino 的文章。在本文中,我探索了 Timer1 的用法以生成波形以及与 PC 的通信。这是因为我打算使用 CmdrArduino 库,了解其工作原理并对其进行扩展,以构建一个可以从 PC 接收命令的 DCC 指令站。

本文演示了该板的两个主要功能:内置定时器有多强大,以及如何轻松地与该板进行通信。

© . All rights reserved.