通过 C# 程序与 Arduino UNO 通信以控制 16 位定时器
本文介绍了如何通过 USB 串行端口向 Arduino UNO 板发送命令,并控制 16 位定时器/计数器的波形。
- 下载 TimerCounter1 库 - 1.3 KB
- 下载 发送命令到 Arduino 的演示 - 18.4 KB
- 下载 从串行端口接收命令的演示 - 2.3 KB
- 下载 Converter 库 - 1,007 B
- 下载 Command 库 - 1.6 KB
引言
在上一篇文章中,我介绍了一个用于控制 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_Cmd
的 getPeriod()
方法发送的命令来更改此值。
代码如下。
#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 指令站。
本文演示了该板的两个主要功能:内置定时器有多强大,以及如何轻松地与该板进行通信。