Arduino UNO R3的主处理器ATMega328P拥有3个定时/计数器,它们分别是Timer0,Timer1和Timer2;它们都通过对来自内部或外部的脉冲信号进行计数的方式完成基本的定时/计数功能以及一些其他的功能。
Timer0和Timer2是8位定时/计时器,Timer1是16位定时/计数器;下面以Timer2为例讨论定时/计数器子系统的典型应用,这些内容同样适用于Timer0和Timer1。
在前面的例子中,已经使用了一些与精准延时相关的Arduino库函数:
delay(ms):延迟一段时间
ms:延迟的时长,单位是毫秒
请注意,上面的Arduino库函数使用了Timer0的中断,因此不要在任何中断服务程序中调用它,否则程序可能无法正常工作。
如果需要在延迟的同时,Arduino可以执行其他程序,这需要借助第三方库MsTimer2;你可以在http://playground.arduino.cc/Main/MsTimer2下载到它,把下载到的MsTimer2.zip解压到Arduino IDE安装目录下的libraries文件夹里就可以使用了,它包含了一个示例:
// FlashLed.ino
#include <MsTimer2.h>
void flash() {
  static boolean output = HIGH;
  
  digitalWrite(13, output);
  output = !output;
}
void setup() {
  pinMode(13, OUTPUT);
  MsTimer2::set(500, flash);
  MsTimer2::start();
}
void loop() {
}这个示例的效果与Blink示例基本相同,MsTimer2库提供了3个库函数:
MsTimer2::set(interval, function):每隔一段时间执行指定函数
interval:间隔时长,单位是毫秒
function:指定执行函数的名称
MsTimer2::start():开始每隔一段时间执行指定函数
MsTimer2::stop():结束每隔一段时间执行指定函数
Timer2有2个控制寄存器:TCCR2A和TCCR2B,它们的结构如下图所示:
| COM2A1 | COM2A0 | COM2B1 | COM2B0 | WGM21 | WGM20 | 
| FOC2A | FOC2B | WGM22 | CS22 | CS21 | CS20 | 
其中WGM2[2:0]位用于设置模式,CS2[2:0]位用于设置时钟源,如下表所示:
| WGM2[2:0] | 模式 | 计数上限 | 
 | CS2[2:0] | 时钟源 | 
| 000 | 正常 | 0xFF |  | 000 | 无 | 
| 001 | 相位校正脉宽调制 | 0xFF |  | 001 | 系统时钟 | 
| 010 | 比较匹配时清零 | OCR2A |  | 010 | 系统时钟8分频 | 
| 011 | 快速脉宽调制 | 0xFF |  | 011 | 系统时钟32分频 | 
| 100 | (保留) |  |  | 100 | 系统时钟64分频 | 
| 101 | 相位校正脉宽调制 | OCR2A |  | 101 | 系统时钟128分频 | 
| 110 | (保留) |  |  | 110 | 系统时钟256分频 | 
| 111 | 快速脉宽调制 | OCR2A |  | 111 | 系统时钟1024分频 | 
其余位暂且设置为0。
精准延时可以采用正常模式。正常模式下,Timer2不断从0x00计数到0xFF;当定时计数器寄存器TCNT2每一次返回0x00时,若Timer2的中断屏蔽寄存器TIMSK2的溢出中断使能位TOIE2为1,则产生中断,它的结构如下图所示:
|  |  |  |  |  | OCIE0B | OCIE0A | TOIE0 | 
通过直接访问寄存器改写以上程序为:
// FlashLed_reg.ino
void flash() {
  PORTB ^= (1 << PB5);
}
void setup() {
  DDRB |= (1 << PB5);
  // 正常模式,系统时钟256分频,计数初值为6
  TCCR2A = 0x00;
  TCCR2B = 0x06;
  TCNT2 = 0x06;
  
  TIMSK2 |= (1 << TOIE2);
  sei();
}
void loop() {
}
ISR(TIMER2_OVF_vect) {
  static volatile int iTimes = 0;
  // 中断125次为500毫秒
  if (++iTimes == 125) {
    flash();
    iTimes = 0;
  }
  // 重新赋初值6
  TCNT2 = 0x06;
}程序中的“中断125次为500毫秒”是这样计算出来的:Arduino UNO R3开发板使用16MHz的系统时钟,Timer2使用系统时钟256分频,每记256-6=250个脉冲溢出一次,则每秒溢出16000000÷256÷250=250次,因此每溢出125次为500毫秒。
脉宽调制的一个典型应用是控制直流电机速度。将直流电机两极分别连接到直流电源的正负两极上,电机会以最快速度运行;要调整电机速度,一个很容易想到的方法是调整直流电源的功率,但在数字系统中还有一个更简单的方式:使用高低电平宽度不一样的脉冲信号快速开关直流电机;因为惯性的作用,电机不会以最快的速度运行。一般来说,高电平在一个脉冲周期中所占宽度更宽时,直流电机速度越快;我们把高电平所占一个脉冲周期的宽度称为占空比。
如图所示连接电路,11(PB3/OC2A)引脚通过三极管间接控制直流电机:

用Arduino库函数输出一个脉宽调制信号十分简单,下面的示例使得直流电机由慢到快,又由快到慢反复运行:
// FadingMotor.ino
int motor = 11;
int speed = 0;
int fadeAmount = 5;
void setup() {
   pinMode(motor, OUTPUT);
}
void loop() {
   analogWrite(motor, speed);
 
   speed = speed + fadeAmount;
   if (speed <= 0 || speed >= 255) {
     fadeAmount = -fadeAmount;
   }
 
   delay(30);
}与脉宽调制相关的Arduino库函数有:
analogWrite(pin, value):在指定引脚上输出一个指定占空比脉宽调制信号
pin:指定引脚
value:脉宽调制信号的占空比;0为0%,255为100%
Timer2拥有2个输出比较寄存器OCR2A和OCR2B,它们通过与TCNT2寄存器发生比较匹配时对引脚置位、清零或取反来完成脉宽调制信号的输出。TCCR2A寄存器中的COM2A[1:0]位用于设置OCR2A寄存器发生比较匹配时的行为,如下表所示:
| COM2A[1:0] | 行为 | 
| 00 | 正常的端口操作,OC2A未连接 | 
| 01 | WGM22=0,正常的端口操作,OC2A未连接 WGM22=1,发生比较匹配时OC2A取反 | 
| 10 | 发生比较匹配时OC2A清零,计数到下限时OC2A置位 | 
| 11 | 发生比较匹配时OC2A置位,计数到下限时OC2A清零 | 
通过直接访问寄存器改写以上程序为:
3. 输入捕获*
输入捕获用来计算外部输入信号的周期,它是Timer1的特有功能,Arduino官网提供了一个输入捕获的示例:
// PulseIn.ino
const int pin = 8;
unsigned long duration;
void setup() {
   pinMode(pin, INPUT);
}
 
void loop() {
   duration = pulseIn(pin, HIGH);
}与输入捕获相关的Arduino库函数有:
pulseIn(pin, value):计算指定引脚输入外部信号的周期
pin:指定引脚
value:捕获脉冲的类型,LOW(低电平,0V)或HIGH(高电平,5V)
函数返回外部输入信号的周期,是一个unsigned long类型的整数,单位为微秒
Timer1的控制寄存器与Timer2的不尽相同,请参阅ATMega328P芯片手册16-bit Timer/Counter1 with PWM章的Register Description小节进行设置。通过直接访问寄存器改写以上程序为:
// PulseIn_reg.ino
unsigned long duration;
unsigned long timer1_pulse_in();
void setup() {
  DDRB &= ~(1 << PB0);
  PORTB &= ~(1 << PB0);
  // 正常模式,系统时钟8分频
  // 噪声抑制关闭,下降沿触发输入捕获
  TCCR1A = 0x00;
  TCCR1B = 0x42;
  TCCR1C = 0x00;
  TIMSK1 |= (1 << ICIE1) | (1 << TOIE1);
  sei();
}
void loop() {
  duration = timer1_pulse_in();
}
volatile int iOvf = 0;
volatile bool isCap = false;
volatile uint16_t iCap = 0;
volatile uint16_t iLastCap = 0;
unsigned long timer1_pulse_in() {
  unsigned long duration;
  while (!isCap);
  duration = 0xffff - iCap 
      + 0xffff * (iOvf - 1) 
      + iLastCap;
  isCap = false;
  return duration;
}
ISR(TIMER1_CAP_vect) {
  iCap = iLastCap;
  iLastCap = (ICR1H << 8) | ICR1L;
  
  isCap = true;
}
ISR(TIMER1_OVF_vect) {
  iOvf += 1;
}事实上,本章仅仅讨论了定时/计数器子系统的典型应用,这仅仅是定时/计数器子系统强大功能的冰山一角,由于篇幅关系没有提及的部分,另请参阅ATMega328P芯片手册。
 
            欧姆定律是指在同一电路中,通过某段导体的电流跟这段导体两端的电压成正比,跟这段导体的电阻成反比。该定律是由德国物理学家乔治·西蒙·欧姆1826年4月发表的《金属导电定律的测定》论文提出的。
Arduino-ESP32与ESP-IDF的版本对应表。
Arduino+ESP32上使用TFT_eSPI库快速点亮这个屏幕,驱动芯片ST7789
Queue 库提供了一个通用的 C++ 动态队列实现,专为在 Arduino 项目中使用而定制。
 
            本程序是小鹏物联网智能浇花套件的单机版程序(不连接物联网),供同学们参考。
 
            ArrayList 类是一个 C++ 模板类,它提供了 ArrayList 的实现,以便轻松存储任何指定类型的值。它允许使用索引进行高效存储和检索,支持排序操作。
 
            ESP32系列(包括ESP32-S3)搭载Xtensa双核处理器,默认情况下Arduino框架仅使用单核运行用户代码,通过多核编程,可以充分利用硬件资源来提升系统响应和性能。
在本文中,先解释 MSB(最高有效位)和 LSB(最低有效位)的概念,以及 MSBFIRST 和 LSBFIRST。然后展示了 MSBFIRST 和 LSBFIRST 的使用如何影响移位寄存器的输出。
TB6612是一款双路H桥型的直流电机驱动芯片,可以驱动两个直流电机并且控制其转速与方向,输入电压在3V~12V,因此在集成化、小型化的电机控制系统中,它可以作为理想的电机驱动器件。
 
            Arduino-ESP32项目提供的Preferences库是一个专为ESP32设计的非易失性存储解决方案,它替代了传统的Arduino EEPROM库,提供了更强大、更可靠的数据存储功能。
用于Arduino的Adafruit_GFX库为我们所有的LCD和OLED显示器提供了通用语法和图形功能集。这使Arduino程序可以轻松地在显示类型之间进行调整,而不必花太多精力……
