欢迎光临
我们一直在努力

使用控制器芯片的硬件(2018.5.3更新)

网站社区logo

==概述==

本文摘自:《Arduino权威指南》第2版【美】Micbael Margolis著 杨昆云译 人民邮电出版社出版发行。

仅供学习交流使用,不得用于商业用途。

    Arduino平台通过提供易于使用的函数调用隐藏复杂的低层次的硬件功能来简化编程。但有些应用程序需要绕过易用函数来直接使用硬件。这或者因为是获得所需功能的唯一途径,或者是因为需要较高的性能。

==寄存器==

    寄存器是针对硬件的内存位置的变量。芯片使用它们来配置硬件的功能或用于存储硬件操作的结果。寄存器的内容可以在你的程序里读取和写入。寄存器值的变化将改变硬件操作的方式,或某项(如管脚的输出)的状态。有些寄存器用来表示数值(比如定时器的计数的数字)。寄存器可以控制或报告硬件状态,例如,引脚的状态或者中断是否发生。在代码中使用寄存器的参考名称来访问它们,这些都记录在微控制器的数据手册中。把寄存器设置成一个的错误的值通常会导致程序工作不正确,所有要仔细检查文档,以确保你正确地使用寄存器。

==中断==

    中断是一种信号,它使控制芯片暂停程序的正常流程和处理,而在继续完成当前任务之前去处理需要立即引起注意的一项任务。Arduino的核心软件使用中断处理从串口传入的数据,为delay和millis函数计时,并处罚attachlnterrupt函数。当事件发生时Wire和Servo这样的库会使用中断,所以代码不必不断地检查事件是否发生。这种频繁检查,称为轮询,会使程序的逻辑结构变得复杂。中断可以是一个可靠的检测持续时间很短的信号的方法。

    在第一个中断处理完成之前,可能会出现两个或更多的中断,例如,如果两个开关同时按下,各自产生不同的中断。第一个开关的中断处理程序必须在第二次中断可以开始之前完成。中断应该是简短的,因为如果中断例程占用太多的时间可能会导致其他的中断处理程序被延迟或错过事件。

    Arduino在同一时间只能处理一个中断。当它处理已经发生的中断时,会暂停待处理的中断。处理中断的代码(称为中断处理程序,或中断服务程序)应该是简短的,以防止延误待处理中断。占用太多时间的中断例程可能会导致其他的中断处理程序错过事件。应避免把一些需要相对较长时间的活动(例如闪烁LED或串口打印)放在一个中断处理程序里。

==定时器==

一个标准Arduino板有3个硬件定时器,用于管理基于时间的任务(MEGA板有6个)。这些定时器用在多个Arduino功能中。

定时器0

用于millis和delay;还有引脚5和6的analogWrite函数。

定时器1

引脚9和10的analogWrite函数;用舵机库驱动舵机。

定时器2

引脚3和11的analogWrite函数。

特别注意:舵机库使用与引脚9和10的analogWrite函数相同的定时器,所以使用伺服库时不能再这些引脚上使用analogWrite。

Mega有3个额外的16位定时器,并在不同的引脚上使用analogWrite函数。

定时器0

引脚4和13的analogWrite函数。

定时器1

引脚11和12的analogWrite函数。

定时器2

引脚9和10的analogWrite函数。

定时器3

引脚2、3和5的analogWrite函数。

定时器4

引脚6、7和8的analogWrite函数。

定时器5

引脚45和46的analogWrite函数。

    定时器是一种计数来自称为“时基”的时间源的脉冲的计数器。定时器硬件由8位或16位的数字计数器构成,它们可以被编程以确定定时器使用的计数模式。最常见的模式是计数来自Arduino板上的源于晶体振荡器的通常为16MHz的时基脉冲。16MHz的脉冲每62.5ns重复一次,对许多定时应用而言有些太快了,因此,时基频率需要由称为“预标定器”的分频器来降低。比如,时基频率除以8,可以把每个计数的持续时间增加到0.5us。若果对于一个应用这仍然太快,还可以使用其他预分频值。

    定时器操作是通过在寄存器中的可以被Arduino代码读取和写入的值来控制的。这些寄存器的值可以设定定时器的频率(每次计包含的系统时基脉冲数)和计数的方法(向上、向下、向上和向下或使用外部信号)。

    下面是定时器寄存器的总结(n为定时器号)。

定时器计数器控制寄存器A(TCCRnA)

确定操作模式。

定时器计数器控制寄存器B(TCCRnB)

确定预分频值。

定时器计数器寄存器(TCCTn)

包含定时器计数。

输出比较寄存器A(对OCRnA)

中断可以在此计数被触发。

输出比较寄存器B(OCRnB)

中断可以在此计数被触发。

定时器/计数器中断屏蔽寄存器(TIMSKn)

设置中断的触发条件。

定时器/计数器0中断标志寄存器(TIFRn)

指示触发条件是否满足。

——————————————————————————————————————————————————————————

预标定因子                                 CSx2,CSx1,CSx0                    精度                               溢出时间

                                                                                                                                    8位计数器                                 16位计数器

——————————————————————————————————————————————————————————

1                                                      B001                              62.5ns                               16us                                       4096ms

8                                                      B010                              500ns                                128us                                     32.768ms

64                                                    B011                               4us                                   1024us                                   262.144ms

256                                                  B100                               16us                                 4096us                                   1048.576ms

1024                                                B101                               64us                                 16384us                                  4194.304ms

                                                        B110                              外部时钟,下降沿

                                                        B111                              外部时钟,上升沿

______________________________________________________________________________________________________________________________________________

    所有定时器都以64为预分频值初始化。

    精度的纳秒数等于CPU周期(一个CPU周期的时间)乘以预分频值。

定时器中断

外部中断是通过检测输入电平的变化,而产生中断信号。除了外部中断方式外,Genuino 101还可以按时间变化产生中断,这里即会使用到定时器(Timer),而对应产生的中断被称为定时器中断。

定时器是嵌入式系统中的一个特殊的计数器。它可以对分频后时钟信号的进行计数,当计数值达到设定值,即会产生定时器中断。且通过时钟频率和计数值可以计算出时间,所以可以达到以时间触发中断的效果。

简而言之,当需要以特定频率运行某个中断程序时,可以使用定时器中断。

使用Curie定时器功能,须引用头文件CurieTimerOne.h:

#include "CurieTimerOne.h"

和IO中断一样也需要先定义一个返回值为空的中断函数:

void Blink () {

}

使用start函数即可开启定时器中断

CurieTimerOne.start(time, Blink)

其中参数time为时间,单位微秒,ISR为定时器中断产生后运行的函数。

第一章中的Blink示例也可以用定时器实现,实现代码如下:

#include "CurieTimerOne.h"

bool lighting= true;

int time = 1000000;

void Blink() {

digitalWrite(13, lighting);

lighting = !lighting;

}

void setup() {

pinMode(13, OUTPUT);

CurieTimerOne.start(time, Blink);

}

void loop() {

}

以上程序还可以结合其他功能,实现一边Blink闪烁,一边





定时器输出PWM

除了作中断源使用,定时器也可以用作PWM输出,CurieTimerOne提供的pwmStart函数可以输出PWM。

在之前的章节中使用的analogWrite函数输出的PWM,周期固定,占空比可调,可用作LED调光;tone函数输出的PWM,周期不变,占空比可调,可用作无源蜂鸣器发声;而pwmStart输出的PWM周期和占空比都可调,更具灵活性,适用场合更广。

需注意的是pwmStart是重载函数,其有两种重载方式:

pwmStart(unsigned int outputPin, double dutyPercentage, unsigned int periodUsec);

pwmStart(unsigned int outputPin, int dutyRange, unsigned int periodUsec);

参数outputPin为输出PWM的引脚编号,periodUsec为每个周期的时间,单位为微秒。

而第二个参数可以为double 型,也可以为int型。当参数为double 型时,编译器会以dutyPercentage进行重载,参数以百分比形式表示PWM占空比;当参数为int型时,编译器会以dutyRange进行重载,参数以0到1023的形式表示PWM占空比;

函数重载

函数重载是指在同一作用域内,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。在编译程序时,编译器会根据参数列表选择对应的函数进行重载并编译。重载函数通常用来命名一组功能相似的函数,这样做减少了函数名的数量,避免了名字空间的污染,对于程序的可读性有很大的好处。[/url][陈吕洲1]

以下代码也是实现Blink的效果:

#include "CurieTimerOne.h"

void setup() {

// 设置13号引脚输出PWM信号, 占空比为25%,周期为1秒(1000000微秒)。

CurieTimerOne.pwmStart(13, 25.0, 1000000);

// 当第二个参数为int型时,用0-1023的数值表示占空比

// 例如255代表24.9%的占空比

// CurieTimerOne.pwmStart(13, 255, 1000000);

}

void loop() {

delay( 10000 );

}

需要注意的是语句

CurieTimerOne.pwmStart(13, 25.0,  1000000);

中的第二个参数25.0一定要有小数位,编译器才会将其判断为double 型。如果这里这里直接使用不带小数位的25,编译器会将其判断为int型,进而使用另一种重载方式。

在永久性EEPROM存储器中存储数据

你想存储即使电源关闭也将被保留的值。

使用EEPROM库来读写EPPPROM存储的值。这个程序用从EEPROM中读取来的值使LED闪烁并允许使用串口监视器来更改这些值。

/*
   基于BlinkWithoutDelay程序
   使用EEPROM来存储闪烁值
  */

#include <EEPROM.h>//需要用到的库文件,请去arduino.cc下载

// 这些值保存在EEPROM中
const byte EEPROM_ID = 0x99;   // 用来确定是否在EEPROM中有有效数据
byte ledPin =  13;             // LED引脚号
int interval = 1000;           // 闪烁时间间隔(毫秒)

// 不要保存的变量
int ledState = LOW;             // ledState用于设置LED
long previousMillis = 0;        // 将存储最后一次的LED被更新的时间

//用于识别EEPROM地址的常量
const int ID_ADDR = 0;       // 用于存储ID的EEPROM地址
const int PIN_ADDR = 1;      // 用于存储引脚的EEPROM地址
const int INTERVAL_ADDR = 2; // 用于存储时间间隔的EEPROM地址

void setup()
{
   Serial.begin(9600);
   byte id = EEPROM.read(ID_ADDR); // 读取EEPROM中的第一个字节
   if( id == EEPROM_ID)
   {
     // 这里如果读取的ID的值与写入EEPROM时保存的值匹配
     Serial.println("Using data from EEPROM");
     ledPin = EEPROM.read(PIN_ADDR);
     byte hiByte =  EEPROM.read(INTERVAL_ADDR);
     byte lowByte =  EEPROM.read(INTERVAL_ADDR+1);
     interval =  word(hiByte, lowByte); // Word函数
   }
   else
   {
     // 这里如果找不到的ID,就写默认数据
     Serial.println("Writing default data to EEPROM");
     EEPROM.write(ID_ADDR,EEPROM_ID); // 写ID来表示数据有效
     EEPROM.write(PIN_ADDR, ledPin); // 保存引脚值到EEPROM中
     byte hiByte = highByte(interval);
     byte loByte = lowByte(interval);
     EEPROM.write(INTERVAL_ADDR, hiByte);
     EEPROM.write(INTERVAL_ADDR+1, loByte);

  }
   Serial.print("Setting pin to ");
   Serial.println(ledPin,DEC);
   Serial.print("Setting interval to ");
   Serial.println(interval);

  pinMode(ledPin, OUTPUT);
}

void loop()
{
   // 这里和BlinkWithoutDelay例程相同的代码
   if (millis() - previousMillis > interval)
   {
     previousMillis = millis();     // 保存你闪烁LED上的最后一次时间
     // 如果该LED熄灭就打开它,反之亦然
     if (ledState == LOW)
       ledState = HIGH;
     else
       ledState = LOW;
     digitalWrite(ledPin, ledState);   // 使用ledState的值设置LED
   }
   processSerial();
}

// 从串口监视器获取持续时间或引脚值的函数
// 跟在i在之后的是时间间隔,P之后的是引脚号
int value = 0;

void processSerial()
{
    if( Serial.available())
   {
     char ch = Serial.read();
     if(ch >= '0' && ch <= '9') // 这是0到9之间的ASCII数字?
     {
        value = (value * 10) + (ch - '0'); // 是的,累加值
     }
     else if (ch == 'i')  // 这是时间间隔
     {
        interval = value;
        Serial.print("Setting interval to ");
        Serial.println(interval);
        byte hiByte = highByte(interval);
        byte loByte = lowByte(interval);
        EEPROM.write(INTERVAL_ADDR, hiByte);
        EEPROM.write(INTERVAL_ADDR+1, loByte);
        value = 0; // 重置为0,为下一个数字序列做准备
     }
     else if (ch == 'p')  // 这是引脚号
     {
        ledPin = value;
        Serial.print("Setting pin to ");
        Serial.println(ledPin,DEC);
        pinMode(ledPin, OUTPUT);
        EEPROM.write(PIN_ADDR, ledPin); // 把引脚保存到EEPROM
        value = 0; // 重置为0,为下一个数字序列做准备
     }
   }
}

    

    打开串口监视器。程序开始后,如果这是程序第一次运行,它告诉你是否使用之前保存到EEPROM的值或默认值。

    你可以通过一个数字加一个字母来表示改变数值的动作。一个数字后面加字母i就改变时间间隔;一个数字后面加P就改变LED的引脚号。

    即使电源被切断,Arduino包含的EEPROM存储器也会存储数值。一个标准Arduino板有512字节的EEPROM,在一个MEGA板有4KB。

    程序使用EEPROM库来读写EEPROM存储的值。

    一单库包含在程序中,通过一个EEPROM对象可访问这个存储器。该库提供了read,write和clear方法。本程序没有使用EEPROM.clear9(),因为它会清除整个EEPROM存储器。

    EEPROM库需要你指定要读取或写入内存中的地址。这意味着你需要跟踪每个值是在哪里被写入的。这样你可以从正确的地址访问它的值。

    要写一个值,你要用EEPROM.write(address,value)。该地址从0到511(在一个标准Arduino板),并且该值是一个单字节。

    要读一个值,你要使用EEPROM.read(address)。该内存地址的字节内容将被返回。

    这个程序在EEPROM中存储3个值。存储的第一值是ID值,仅在setup里用于确定之前EEPROM中是否已经写入有效数据。如果存储的值与预期值相匹配,其他变量都从EEPROM中读取,并在程序中使用;如果不匹配,说明这个程序还未在这个板子上运行过(否则,这个ID会被写入),所以包括ID值在内的默认值将被写入。

    本程序监视串行端口,并把获得的新值吸入EEPROM中。

    本程序把ID值存储再EEPROM的地址0,引脚号存储在地址1,两个字节的时间间隔开始于地址2。下面一行把引脚号写入EEPROM。变量ledPin是一个字节,所以它可以放到一个单一的EEPROM地址:

        EEPROM.write(PIN_ADDR,ledPin);//把引脚号吸入EEPROM

    因为时间间隔是一个整数型,它需要两个字节的内存来存储值:

        byte hiByte = highByte(interval);

        byte loByete = lowByte(interval);

        EEPROM.write(INTERVAL_ADDR,hiByte);

        EEPROM.write(INTERVAL_ADDR+1,loByte);

    在前面的代码把这个值分割成2个字节并存储进两个连续的地址。任何要添加到EEPROM中的其他变量需要被放置在这两个字节的地址的后面。

    下面是用于从EEPROM重建int变量的代码:

        ledPin = EEPROM.read(PIN_ADDR);

        byte hiByte = EEPROM.read(INTERVAL_ADDR);

        byte lowByte = EEPROM.read(INTERVAL_ADDR+1);

        interval = word(hiByte, lowByte);

    关于使用word表达式来从两个字节创建一个整数。

    对于更复杂的EEPROM的使用,最好是画出一个图来表示什么数据被存在哪个地址里,以确保没有地址被一个以上的值使用,并且多字节值不会覆盖其他信息。

未经允许不得转载:Arduino-Maker » 使用控制器芯片的硬件(2018.5.3更新)

支付宝扫码打赏 微信打赏

原创文章,若帮到您,欢迎打赏

分享到:更多 ()

评论 抢沙发

评论前必须登录!

 

线上商城

中贝斯特创客空间蘑菇云创客教育