Arduino开发实战指南:STM32篇
上QQ阅读APP看书,第一时间看更新

第2章 Maple的Arduino兼容函数库

2.1 基本程序结构

Maple的基本程序结构有两个入口函数,setup()与loop()。setup()中的代码只会在启动时执行一次,loop()中的代码会无限循环地执行下去。代码清单2-1为Maple的基本程序结构。

代码清单2-1 Maple的基本程序结构

void setup(){
//这里是初始化使用的程序段,只在启动时执行一次
}
void loop(){
//主程序位于这里,在这个函数中的程序会无限循环地执行
}

Maple中,与底层相关的大量操作都被隐藏起来了,尽管Maple使用的是C++作为编程语言,但是几乎不会出现复杂的C++语法,使得它很适合于对编程与寄存器操作了解不多的初学者使用。

2.2 Maple静态变量关键字

Maple中提供了一些静态变量关键字来替代一些常数和引脚,它们可以规避不同型号开发板所产生的兼容性问题。使用这些静态变量有助于提高程序的可移植性与可读性。如表2-1所示为静态变量关键字,如表2-2所示为串口相关的常量。

表2-1 静态变量关键字

表2-2 串口相关的常量

2.3 通用输入输出

通用输入输出(GPIO)是Maple最基本、最常用的功能,用来实现基本的数字量输入和输出。GPIO的控制主要依赖pinMode()、digitalWrite()、digitalRead()三个函数。

由于Maple使用的STM32采用了更高的工艺生产,所以数字输出的电平为高电平——3.3V的LTTL电平,而Arduino使用的是ATMGA系列芯片,制程较陈旧,输出为高电平——5V的TTL电平。如果需要将5V电平的芯片与Maple相接,则需要进行电平转换,否则5V电平的信号直接输入Maple可能会造成芯片损坏(部分引脚能够容忍5V电平,对5V电平的数字信号进行读取或是利用开漏输出方式输出5V电平的数字信号)。

2.3.1 pinMode()函数

形式:void pinMode(uint8 pin,WiringPinMode mode)

参数:pin为引脚编号。

mode为引脚的输入输出模式。

pinMode()函数常放在setup()函数中来确定引脚的功能。切记,如果在使用某引脚前没有设定pinMode()或者pinMode设置模式不正确,引脚输入输出过程可能会出现一些不可预料的错误。pinMode()输出模式种类如表2-3所示。

表2-3 pinMode()输出模式种类

代码清单2-2通过pinMode()函数设定BOARD_LED_PIN作为输出引脚,然后通过digitalWrite()函数与delay()函数使其以1/2Hz的频率闪烁。

代码清单2-2 闪烁LED

void setup() {
    pinMode(BOARD_LED_PIN,OUTPUT);     // 设定LED引脚为输出模式
}
void loop() {
    digitalWrite(BOARD_LED_PIN,HIGH);  // 设定LED引脚为高电平,点亮LED
    delay(1000);                        // 等待1秒钟
    digitalWrite(BOARD_LED_PIN,LOW);   // 设定LED引脚为低电平,熄灭LED
    delay(1000);                        // 等待1秒钟
}

Maple的数字引脚能够提供两种输出模式:开漏输出(OUTPUT_OPEN_DRAIN)与推挽输出(OUTP-UT)。其中最常用的是推挽输出模式"OUTPUT"。开漏输出与推挽输出在外部链接上的区别如图2-1所示。

图2-1 开漏输出(左)与推挽输出(右)

图2-1左图为开漏模式输出,需要外接一个上拉电阻才能得到输出电压,否则就不会有电压的输出,而右图的推挽输出可以直接得到输出信号。

这里列举一些需要使用开漏输出模式的情况:

1)在使用I2C总线进行信号传输的时候,由于协议的设计,必须使用开漏模式来输出信号,这样可以避免总线上多个设备传输信号产生冲突。

2)当需要输出TTL(5V)电平信号的时候。由于这个5V的电压高于了该芯片的电源电压,所以具有5V容忍能力的引脚没法得到足够高的电源电压来输出TTL信号。利用外部上拉电阻将输出引脚连接到额外的5V电源,能够使得这些5V容忍引脚向以TTL电平工作的芯片传输数据。

2.3.2 digitalWrite()函数

形式:uint32 digitalWrite(uint8 pin,uint8 value)

参数:pin为引脚编号。

value为LOW(写0,输出低电平)或HIGH(写1,输出高电平)。

将引脚配置为"OUTPUT"或"OUTPUT_OPEN_DRAIN"模式之后,可以通过该函数控制引脚输出高低电平信号。输出低电平时,将value参数设定为0或LOW;输出高电平时,设定为1或HIGH。

代码清单2-3会让Maple以数字输出模式的极限速度翻转引脚的输出值,在这种理想情况下,引脚可以达到700KHz的最高输出速度(如果增加其他函数操作,会大幅降低其输出速度)。这是由于digitalWrite()函数为了减小用户使用的复杂度,经过多层的封装使得输出速度变慢,如果希望获得更高的输出速度,可以改用底层函数来完成输出。

代码清单2-3 以数字输出模式的极限速度翻转引脚的输出值

void setup() {
    pinMode(BOARD_LED_PIN,OUTPUT);     //设定连接到LED的13引脚为数字输出模式
}
void loop() {
    digitalWrite(BOARD_LED_PIN,HIGH);   //设定输出为高电平
    digitalWrite(BOARD_LED_PIN,LOW);    //设定输出为低电平
}

2.3.3 digitalRead()函数

形式:uint32 digitalRead(uint8 pin)

参数:pin为引脚编号。

使用该函数读取引脚的信号时,所读取的引脚必须已经用pinMode()设定该引脚的模式为"INPUT"、"INPUT_PULLUP"或"INPUT_PULLDOWN"。

当输入信号电压在0~1.16V时该函数返回0,当输入信号在1.83~3.3V时返回1。如果输入电压在1.16~1.83V之间不确定会返回0还是1。

代码清单2-4 读取PIN0电平

void setup() {
    pinMode(0,INPUT);                   // 设定PIN0引脚为输出模式
}
void loop() {
    SerialUSB.println(digitalRead(0));  //通过USB虚拟串口返回读取的值
    delay(1000);                        //延时1000毫秒,也就是1秒
}

代码清单2-4会每秒都向USB虚拟串口输出得到的引脚电压。如果没有外接其他电路仅仅悬空,会非常容易受到干扰,随机输出不固定的0或1。如果希望在外部电路未接上时有固定输出而不受干扰,可以将引脚设置为"INPUT_PULLUP"或是"INPUT_PULLDOWN"。

2.3.4 togglePin()

形式:uint32 togglePin(uint8 pin)

参数:pin为引脚编号。

togglePin()函数是用来反转输出引脚输出状态的,当前输出为1时将其翻转为0,当前输出为0时翻转为1。togglePin()在某些时候能够使操作更方便。

2.3.5 toggleLED()

形式:uint32 toggleLED()

toggleLED()与togglePin()类似,但是它没有参数,直接控制Maple上LED的输出翻转。该函数主要用于控制LED闪烁。请注意,如果在使用的时候没有额外的延时,必须加入额外的延时才能控制LED以指定的频率闪烁。

2.4 模拟输入输出

Maple模拟输入输出功能用来读取模拟输入引脚上的模拟电压值,或是通过PWM引脚输出模拟信号。

Maple的模拟信号输出是依靠PWM(Plus Width Modify,脉宽调制)实现的,而不是DAC。PWM的原理是通过改变占空比,通过低通滤波得到平均电压从而实现模拟输出。PWM还可以用来实现对舵机的控制。

2.4.1 analogWrite()与pwmWrite()

形式:void pwmWrite(uint8 pin,uint16 duty_cycle)

void analogWrite(uint8 pin,uint16 duty_cycle)

参数:pin为引脚编号。

duty_cycle为控制占空比的参数(0~65535)。

在使用pwmWrite()与analogWrite()前必须先将引脚定义为"PWM"或是"PWM_OPENDRAIN",只有PIN0~PIN3、PIN5~PIN9、PIN11、PIN12、PIN14、PIN24、PIN27、PIN28引脚具有PWM输出功能。

与Arduino有区别的是,Arduino的引脚10具有PWM功能,而Maple引脚10没有。如果你希望程序在Arduino与Maple之间具有良好的兼容性和移植能力,尽量使用PIN3、PIN5、PIN6、PIN9、PIN11引脚作为PWM输出。

analogWrite()是为了与Arduino兼容而准备的,但是与pwmWrite()一样,由于STM32处理器的PWM分辨率更高,所以duty_cycle是用16位数表示,范围为0~65535,分别表示0%~100%,而Arduino中PWM分辨率为256位,用0~255来表示0%~100%。

占空比与duty_cycle参数的关系为:

在Arduino中PWM频率为490Hz,而在Maple中PWM是可以调整的,默认频率为550Hz。通过设定控制该PWM引脚的timer的预分频器就可以改变该PWM的频率,如图2-2所示。

图2-2 示波器通道1的duty_cycle为16384,示波器通道2的duty_cycle为32768

如果希望得到稳定的模拟电压输出,需要外接一个小电容来得到电压平均值。

2.4.2 analogRead()

形式:uint16 analogRead(uint8 pin)

参数:pin为引脚编号。

2.5 高级I/O

Maple提供高级I/O函数来简化I/O操作,由于库的原因,所以Arduino支持的plusin()函数在Maple开发环境中不能被支持。

shiftOut()

形式:void shiftOut(uint8 dataPin,uint8 clockPin,uint8 bitOrder,uint8 value)

参数:dataPin为数据引脚。

clockPin为时钟引脚。

bitOrder为数据输出顺序,MSBFIRST表示高位在前,从高到低位输出;LSBFIRST表示低位在前,从低到高位输出。

value为输出。

该函数采用软件方式实现串行输出,速度较慢,如果需要高速串行输出,可以使用硬件SPI以获得更好的性能,但是该函数的优势是可以任意指定输出引脚。shiftOut()每次只能输出8位的数据,如果需要输出更多数据需要多次调用该函数。在使用时,dataPin和clockPin都必须事先被设定为"OUTPUT"。

将一个16位的整型数据从高位到低位输出,如代码清单2-5所示。

代码清单2-5 将一个16位的整型数据从高位到低位输出

void setup() {
    //将需要的引脚设定为数字输出
    pinMode(12,OUTPUT);
    pinMode(11,OUTPUT);
    pinMode(10,OUTPUT);
}
void loop() {
uint16 data = 500;
// 引脚10输出高电平表示一个数据传输开始
digitalWrite(10,HIGH);
// 移出高位
shiftOut(12,11,MSBFIRST,(data >> 8));
// 移出低位
shiftOut(12,11,MSBFIRST,data);
// 引脚10输出低电平表示一个数据传输结束
digitalWrite(10,LOW);
}

通过逻辑分析仪可以得到代码清单2-5的输出结果,如图2-3所示。PIN12输出数据信号,PIN11输出时钟信号,PIN10输出使能信号。

图2-3 运行代码清单2-5所输出的时序

2.6 硬件SPI接口

SPI(Serial Peripheral Interface,串行外设接口)是由摩托罗拉最先定义设计的,常用于MCU与FLASH存储器、EEPROM存储器、传感器、实时时钟、AD转换器等外部设备的连接。

SPI常使用4个信号进行数据传输,这4个信号分别是MOSI、MISO、NSS、SCK。其中,MOSI(Master Output Slave Input)是主设备输出信号,从设备接收此信号;MISO(Master Input Slave Output)是从设备的输出信号,主设备接收此信号;NSS(或称为CS)由主设备驱动,用来控制SPI传输的开始与终止,该信号以低电平时表示有效,在多个设备共用一个SPI时也做设备选择信号;SCK也是由主设备驱动,用于为SPI传输提供时钟。

SPI传输有4种模式,由CPOL(Clock Polarity,时钟极性)与CPHA(Clock Phase,时钟相位)两个参数控制。CPOL表示在空闲时时钟的电平状态,1表示空闲时时钟为高电平,0反之;CPHA控制信号表示是在第一还是第二个边沿被采样,当CPHA=0时在使能信号后第一个时钟边沿被采样,当CPHA=1时在使能后第二个时钟边沿被采样。CPOL与CPHA参数控制的不同工作模式波形示意图如图2-4所示。

图2-4 CPOL、CPHA参数控制的不同工作模式波形示意图

硬件SPI只能通过特定的引脚输出,不能够任意指定输出引脚。这些引脚可以见表2-4,各接口与设备连接方式如图2-5所示。

图2-5 SPI 与设备连接示意图

表2-4 接口所使用的引脚分布

通过调用硬件SPI可以得到很高的传输速度,以满足一些高速期间的时序要求,并且减少对处理器时间的占用。使用硬件SPI可以提供最高每帧18MHz的传输速度,而如果使用软件方式实现SPI最高只能提供不到700KHz的速度,以至于可能无法满足某些芯片对传输频率的要求。

在使用的时候,首先要通过"HardwareSPI spi(1)"语句建立名称为"spi"的对象,如代码清单2-6所示,用于控制SPI1("HardwareSPI spi2(2)"用来建立一个控制SPI2的对象,名称为"spi2")。

代码清单2-6 建立一个用于控制硬件SPI1的对象"spi"

//使用SPI1 接口
HardwareSPIspi(1);
voidsetup(){
}
voidloop(){
}

“类”与“对象”是面向对象编程的概念,类通过实例化可以得到对象。类定义了一个功能的特征,而对象将这个定义具体化。类与对象的关系如同“书”与《Arduino开发实战指南:STM32卷》的关系。“书”定义了一种由纸制成的,有封面、前言、目录、内容等部分,用于记录信息的形式,而具体某本书在这个定义范围内将纸质、封面、前言、目录和内容具体化,类与对象的关系也是如此。

2.6.1 begin()

形式:void begin(SPIFrequency frequency,uint32 bitOrder,uint32 mode)

void beginSlave(uint32 bitOrder,uint32 mode)

参数:frequency为SPI传输频率。

bitOrder为传输顺序。LSBFIRST表示低位在前,MSBFIRST表示高位在前。

mode为CPOL模式与CPHA模式设定。

begin()用于初始化以主设备模式开始SPI传输。如果使用无参数的"begin()"会按照1.125MHz、MSBFIRST、CPOL=0、CPHA=0的默认参数来执行,等同于"begin(SPI_1_125MHZ,MSBFIRST,0)"。

beginSlave()将Maple初始化为以SPI从设备进行的SPI传输。如果使用无参数的"beginSlave()"会按照MSBFIRST、CPOL=0、CPHA=0的默认参数来执行,等同于"beginSlave(MSBFIRST,0)"。

frequency参数可以被设定为表2-5中的某个速率。其中140.625KHz速率由于硬件原因只能由SPI1使用。

表2-5 SPI速率表

在使用时,你需要阅读所使用的芯片数据手册,并根据数据手册中所描述的时序要求与工作模式来设定SPI的模式和速率。初始化SPI。如代码清单2-7所示。

代码清单2-7 初始化SPI

// 使用SPI1接口
HardwareSPIspi(1);
voidsetup(){
// 打开SPI1
spi.begin(SPI_18MHZ,MSBFIRST,0);
}
voidloop(){
// 执行SPI传输
}

2.6.2 write()

形式:void write(byte data)

void write(const uint8*buffer,uint32 length)

参数:data为需要传输的数据,8位。

buffer为待发送数据缓存。

length为发送数据长度。

write(byte data)函数可以每次向设备发送8位的数据,如果使用write(byte data)发送16位或是24位的数据就需要多次调用write(byte data),这样会造成每次调用write()函数之间有时间间隔,每8位就会有停顿,可能会让传输时序超出某些外设的要求,从而无法正常通信。这样使用会降低传输速度,当SPI工作在9MHz以上频率的时候尤为明显。使用write(const uint8*buffer,uint32 length)可以降低多个字节数据间的间隔,提高传输速度。SPI收发数据如代码清单2-8所示。

代码清单2-8 SPI收发数据

//使用SPI1
HardwareSPIspi(1);
byte buf[]={3,43,52,61};
voidsetup(){
//初始化SPI接口
spi.begin(SPI_18MHZ,MSBFIRST,0);
}
voidloop(){
   // 通过SPI发送长度为1字节的数245,然后等待接收数据
   spi.write(245);
   //通过SPI连续发送一个长度为4的byte类型数组中存储的4字节的数据
   spi.Write(buf,4);
   byteresponse=spi.read();
   //通过USB虚拟串口显示收到的数据
   SerialUSB.print("response: ");
   SerialUSB.println(response,DEC);
}

2.6.3 read()

形式:byte read()

read()函数会接收一字节的数据,如果没有读取到数据,read()函数会一直等待直到接收到数据再返回读取的数值。当接收的数据不止一字节时,需要多次调用该函数。

2.6.4 transfer()

形式:byte transfer(byte data)

参数:data为需要传输的数据,8位。

transfer()函数用于发送一字节的数据并接收一字节的数据,同时将接收到的数据作为返回值输出。

2.6.5 end()

形式:void end()

end()函数用于关闭SPI接口,但是不会改变接口的输入/输出状态。

2.7 硬件USART与虚拟USB串口

USART(Universal Synchronous/Asynchronous Receiver/Transmitter)中文为“通用同步/异步串行接收/发送器”,也就是常说的串口(Serial),是最常用的通信协议之一。常用于Maple与计算机的连接或是处理器之间的连接。硬件串口1~3使用Serial1、Serial2、Serial3三个对象来控制。

尽管虚拟USB串口与串口放在一节中,但是它们的实质是不同的,仅仅是因为Maple为它们提供的函数类似,在编程上有类似之处。Maple所使用的STM32有一个专用的USB控制器,这个控制器被配置成一个虚拟串口设备,使用户在计算机上能通过标准的串口协议与Maple通信。

对USB虚拟串口的调用是通过SerialUSB对象实现的。大多数情况下,你可以用SerialUSB直接替代Serial1、Serial2、Serial3。每次通过USB虚拟串口发送数据至少消耗约50ms的时间,并且虚拟串口并不会检测USB接口是否真的已连接。

USART设备间的通信主要依靠TX和RX两个信号,设备A的TX接到设备B的RX,设备B的RX接到设备A的TX,如图2-6所示。

图2-6 UART 通信连接

注意,USART≠RS232,请勿将USART接口直接与RS232相连,RS232传输的信号使用±12V的电压,会损坏Maple。RS232是一种接口规范,详细规定了传输的电气特性、传输速率、连接特性和接口的机械特性等内容。与RS232类似的还有RS499、RS423、RS422和RS485等,而USART是这类串行通信接口传输协议的总称。RS232为了可靠传输使用了±12V的电压来传输信号,需要将硬件USART与专用电平转换芯片连接,将LVTTL信号转换为RS232规定的电压信号,通过9针孔D型接口连接才被叫做RS232。

PC上的COM口由于历史原因都是RS232接口。现在大部分计算机都取消了RS232接口,如果需要,可以利用专用的USB串口适配器,通常这类适配器会提供TTL电平输出,该TTL电平输出可以直接连接到Maple的USART接口。

2.7.1 begin()

形式:void begin(unsigned int baud)

参数:baud为波特率。

begin()用于设定串口工作的波特率,在进行串口通信前必须先调用begin()设定波特率,对于Maple常用的波特率有9600Hz与115200Hz。通过Maple IDE与PC通信时,虚拟串口波特率默认为9600Hz。

SerialUSB.begin()通常不需要调用,只有当使用SerialUSB.end()关闭虚拟串口后,才可以利用该函数恢复虚拟串口通信。

2.7.2 write()

形式:void write(unsigned char ch)

void write(const char*str)

void write(void*buf,unsigned int size)

参数:ch为待发送的数据。

buf为数据缓冲。

size为缓冲数据长度。

"void write(unsigned char ch)"函数是一个低层函数,通过USART发送单个字符,现在已被屏蔽。

"void write(const char*str)"函数可以通过USART发送一个带有NULL终止符的字符串。

"void write(void*buf,unsigned int size)"函数发送buf变量的前size字节数据,每个字节按照独立的字符发送。

write()函数的使用见代码清单2-9。

代码清单2-9 write()函数的使用

char c='r';
char d[6]="Maple";//"Maple"占用5字节加上一个终止符,共6字节
void setup(){
Serial1.begin(9600);
}
void loop(){
//发送字符c
Serial1.write(c);
//发送字符串d
Serial1.write(d);
//发送字符串d的前3字节
Serial1.write(e,3);
}

2.7.3 print()与println()

形式:void print(data)或void print(long data,base)

void println(data)或void println(long data,base)

参数:data为数据,可以是unsigned char、char、const char*、int、unsigned int、long、unsigned long、double类型的变量。当指定进制时只能用long类型的变量。

base表示以何种进制显示,可以在2~16进制之间设置。

print()与println()的区别是println()会自动在每次发送的数据后加入换行符。

与C++中标准的print()和println()不同,这里的print()和println()只支持直接以变量作为参数,传输的数据data可以是unsigned char、char、const char*、int、unsigned int、long、unsigned long、double类型。

println()的使用见代码清单2-10。

代码清单2-10 println()的使用

void setup(){}
void loop(){
//发送字符串
SerialUSB.println("Maple!");
//发送单个字符
SerialUSB.println('m');
//发送整数
SerialUSB.println(300);
//发送浮点数
SerialUSB.println(-0.25);
delay(1000);
}

在指定数字进制的时候可以使用BIN、DEC、HEX来表示常用的二进制、十进制、十六进制或者用2~16的数字表示用何种进制显示。例如"SerialUSB.print(255,HEX);"或"SerialUSB.print(255,BIN);"。

图2-7是一个用不同进制显示255的一个例子。

图2-7 使用不同进制显示255

2.7.4 read()

形式:unsigned char read()

返回下一个尚未读取的数据。这是一个“阻塞”的函数,如果数据缓冲区为空,则会一直等待,直到返回取得的数据后才会继续执行下面的程序。

2.7.5 available()

形式:unsigned int available()

available()函数会返回等待接收的字节数。常用于接收一长串数据。

available()的使用见代码清单2-11。

代码清单2-11 avaliable()的使用

int inByte;
void setup() {
    // 以9600波特率初始化 Serial1
    Serial1.begin(9600);
}
void loop() {
    // 从Serial1读取数据,通过虚拟USB串口转发
    if (Serial1.available()>0) {
        inByte = Serial1.read();
        SerialUSB.print(inByte,BYTE);
    }
}

2.7.6 flush()

形式:void flush()

flush()函数用于清空串口接收缓冲,保证之后所读取到的都是全新的数据。该函数只用于Serial1、Serial2、Serial3,对于SerialUSB不需要使用此函数。

2.7.7 txPin()与rxPin()

形式:int txPin()

int rxPin()

txPin()与rxPin()函数分别会返回发送和接收引脚的编号。

2.7.8 end()

形式:void end()

end()函数用于结束串口通信,并释放其所占用的端口。"SerialUSB.end()"用于结束虚拟串口的通信。当使用"SerialUSB.end()"结束虚拟串口通信后,下载程序时不会自动复位进入bootloader模式等待下载,需要手动复位进入下载。

2.8 延时和定时器

2.8.1 delay()与delayMicroseconds()

形式:void delay(unsigned long time)

void delay Microseconds(unsigned long time)

参数:time为延时的时长。

delay()与delayMicroseconds()都用于延时,不同的是,delay()函数的参数以“毫秒”为单位,常用于长时间的延时;而delayMicroseconds()的参数以“微秒”为单位,用于进行精确延时,例如,数据传输的时序控制。

2.8.2 mills()与micros()

形式:uint32 mills()与uint32 micros()

参数:无

用于得到从程序开始运行以来的时间,mills()函数返回以毫秒表示的时间,而micros()函数返回以微秒表示的时间。当计时溢出后会自动从零开始计数,mills()函数会在程序运行约50天后溢出,而micros()会在程序运行约70分钟后溢出。

2.8.3 内部硬件定时器

对硬件定时器的独立控制部分是Maple特有的,Arduino没有相关函数。对于Arduino需要通过低层寄存器操作来控制定时器。但是Maple的Hardware Timer仍然在开发中,该库可能不稳定,在未来相关函数会继续更新,如果需要稳定可以使用libmaple中的timer.h。

在Maple使用的STM32中,有4个16位定时器可以使用,每个定时器可以计数到最大65535。每个定时器有4个比较器,每个比较器可以独立地与定时器比较触发操作,触发操作可以是PWM或产生中断。如图2-8所示,同一个定时器上的不同比较器可以以相同的频率触发,但触发的相位不同,这可以用于PWM与步进电机驱动等应用。

图2-8 定时器与触发器的关系(长线表示timer计数为0的时间)

控制硬件定时器需要实例化HardwareTimer类。注意,PWM也是由定时器控制的,在同时使用定时器和PWM时,需要尽量避免共用一个定时器或触发器,PWM引脚与定时器的对应关系请查看附录四。

形式:HardwareTimer(uint8 timerNum)

参数:timerNum指定的定时器编号。

该函数是HardwareTimer的入口函数,通过它来建立一个timer对象,如代码清单2-12所示。

代码清单2-12 建立一个名为timer1的对象,用于控制定时器1

HardwareTimer  timer1(1);

形式:void pause(void)

参数:无

暂停定时器。

形式:void resume(void)

参数:无

在暂停定时器后恢复计时。

形式:uint32 getPrescaleFactor()

参数:无

获取定时器的分频系数。返回值在1~65536之间。

形式:voidsetPrescaleFactor(uint32 factor)

参数:factor设定定时器的分频系数,分频系数在1~65536之间。

设定不会立刻生效,需要在下一次定时器溢出后或是用refresh()函数重置定时器后才会生效。Maple的工作频率为72MHz,若factor为1,则定时分辨率为13.89ns(1ns=10-9s=10-6ms=10-3μs)。表2-6给出常见的定时器分频系数与分辨率、最长计时周期的关系。

表2-6 定时器分频系数与分辨率、最长计时周期的关系

形式:uint16 getOverflow()

参数:无

获得所设定的定时器溢出值。

形式:void setOverflow(uint16 val)

参数:val设定定时器的溢出值。

用于设定定时器的溢出值。溢出值用于控制定时器计数周期。

例如,代码清单2-13建立了HardwareTimer的一个实例timer,用于控制定时器1,并设定其分频器分频系数为5,溢出值为255,该计数器每计数一个值代表的时间约为69.4ns,溢出的周期为17.8μs。

代码清单2-13 设定分频器分频系数与溢出值

HardwareTimertimer(1);
voidsetup(){
timer.setPrescaleFactor(5);
timer.setOverflow(255);
}
voidloop(){
// ...
}

形式:uint16 getCount(void)

参数:无

获得定时器的当前计数值。将这个值乘以定时器的分辨率就可以得到当前时间。

形式:voidsetCount(uint16 val)

参数:val设定值。

设定定时器的当前值。

形式:uint16 setPeriod(uint32 microseconds)

参数:microseconds周期。

设定定时器的计时周期,该操作会设定分频器和溢出值,以获得尽可能接近指定周期的定时器溢出周期。

形式:void setMode(int channel,timer_mode mode)

参数:channel定时器通道。

mode定时器的工作模式有3种:TIMER_DISABLED(关闭)、TIMER_PWM(PWM,初始化后的默认模式)、TIMER_OUTPUT_COMPARE(由定时器触发中断)。

设定定时器的工作模式。

形式:uint16 getCompare(int channel)

参数:channel定时器通道。

获得定时器中指定通道的比较值。

形式:void setCompare(int channel,uint16 compare)

参数:channel定时器通道。

compare比较值,可以是0或溢出值-1。

设定定时器中指定通道的比较值,该值用于控制触发事件的相位,当比较值大于定时器溢出值时,会被设定为定时器的溢出值。定时器计数达到比较器的比较值时会触发操作,例如,PWM或是通过中断执行某个指定函数。

形式:void attachInterrupt(int channel,voidFuncPtr handler)

参数:channel定时器通道,可以是1~4的值。

handler触发的函数。

为定时器的指定通道设定一个中断。

形式:void detachInterrupt(int channel)

参数:channel定时器。

移除指定通道的触发设定

形式:void refresh(void)

参数:无

重置定时器。恢复定时器的值为0,并更新溢出值与分频器设置。

为了使用定时器中断,这里建议采用下面的步骤:

1)用pause()函数暂停定时器。

2)设定分频器与溢出值(也可用setPeriod()来设定)。

3)选择一个通道来处理中断,并将该通道工作模式设定为TIMER_OUTPUT_COMPARE。

4)设定选定通道的比较值,该值可以是0到溢出值之间的一个值,如果你不在乎具体什么值触发中断则可以忽略,默认会在计数器为1时触发。

5)连接中断到用于处理该定时器中断的函数。

6)用refresh()函数重置计数器。

7)用resume()函数恢复计数器计时。

代码清单2-14使用了定时器1中的通道1的中断来控制LED以1Hz的频率闪烁,由于定时器是外部设备,在闪烁LED时不需要占用处理器。

代码清单2-14 用定时器来闪烁LED

HardwareTimer timer1(1);
void setup(){
  pinMode(BOARD_LED_PIN,OUTPUT);
  timer1.pause();
  timer1.setPeriod(500000);//0.5s
timer1.setMode(1,TIMER_OUTPUT_COMPARE);//设定比较器1触发中断
  timer1.setCompare(1,1);//设定比较器1的值为1,当计数器计数到1时触发比较器1
  timer1.attachInterrupt(1,handler_led);
  timer1.refresh();
  //开始计数
  timer1.resume();
}
void loop(){
}
void handler_led(void) {
  toggleLED();
}

代码清单2-15用来解释溢出值与比较值的关系,这个例子用到了定时器1的两个通道。首先将定时器分频器设定为65535,每计数一次大约0.91ms,为了使定时器以约1Hz的频率触发,将溢出值设定为1000,则定时器的工作周期为0.91×1000μs=0.91s。通道1在定时器计数到0时触发led_H()函数点亮LED,通道2在定时器计数到20时触发led_L()函数熄灭LED,两个比较器触发的操作间隔为20ms,让LED每隔0.91s点亮一次,每次点亮20ms。

代码清单2-15 使用定时器的两个通道同步操作

HardwareTimer timer1(1);
void setup(){
  pinMode(BOARD_LED_PIN,OUTPUT);
  timer1.pause();
  timer1.setPrescaleFactor(65535);
  timer1.setOverflow(1000);
  timer1.setMode(2,TIMER_OUTPUT_COMPARE);
  timer1.setMode(1,TIMER_OUTPUT_COMPARE);//设定比较器1触发中断
  //定时器1的通道1在timer1计数值为1时触发点亮LED,定时器值为20时通道2触发,熄灭LED
  timer1.setCompare(1,0);
  timer1.setCompare(2,20);
  timer1.attachInterrupt(1,led_H);
  timer1.attachInterrupt(2,led_L);
  timer1.refresh();
  //开始计数
  timer1.resume();
}
void loop(){
}
void led_H(void) {
  digitalWrite(13,HIGH);
}
void led_L(void) {
  digitalWrite(13,LOW);
}

利用Hardware Timer控制PWM,可以使PWM更加灵活,弥补没有Arduino中tone()函数的不足。按照表2-7或附录,找到所需引脚对应的定时器和对应通道,例如,代码清单2-16中D1输出可以由定时器2中的通道3控制。

表2-7 Maple的Timer及其通道与引脚关系

定时器的溢出值N可以在分频器的基础上再产生一个N+1分频,进一步降低工作频率。输出频率与定时器溢出值、分频器的关系为:

代码清单2-16溢出值为1,分频比为72,则会产生一个的PWM输出。随着溢出值的增大,PWM频率会下降,PWM的分辨率会随之提高。

当定时器被用作PWM模式输出时,对应PWM引脚会直接输出比较器的比较结果,当定时器的值小于比较器设定值时,对应引脚输出低电平,当定时器的值大于等于比较器设定值时,对应引脚输出高电平。所以PWM的占空比可以用以下公式表示:

其中,compare为比较器的值。

使用Hardware Timer控制PWM首先需要调用pinMode()函数将引脚设定为PWM模式,然后如同通常操作Hardware Timer一样进行配置:创建Hardware Timer的实例、设定分频系数、溢出值、比较器值,如代码清单2-16所示。

代码清单2-16 用Hardware Timer控制PWM

HardwareTimer timer1(2);
void setup(){
     pinMode(1,PWM);
     timer1.pause();
     timer1.setPrescaleFactor(72);
     timer1.setOverflow(1);
     timer1.setCompare(3,1);
     timer1.refresh();
     timer1.resume();
}
void loop(){}

2.9 外部中断

外部中断使得Maple能够根据外部电压的改变来唤起对某函数的执行,常用于检测外部事件。使用中断可以避免使用循环函数来监测外部电压变化,不需占用处理器时间。例如,使用中断监测外部按键是否被按下,如果事件发生,处理器会停止正在执行的程序转而执行根据使用attachInterrupt()所指定的函数,当函数执行完毕时,处理器会继续刚才所执行的程序。

所有的GPIO引脚都可以被用于接收中断信号,但是由于它们共用16个中断通道,所以最多只能够同时设置16个中断。并且不是任意16个引脚都可以被同时设定为中断,不能够同时设定两个共用同一个中断通道的引脚。例如,根据表2-8可知,Maple中不能同时选择D10与D19作为中断。

表2-8 Maple中断与引脚关系

2.9.1 interrupts()与nointerrupts()

形式:void interrupts()与void nointerrupts()

nointerrupts()用于屏蔽所有在用户程序中设定的中断。通常中断是被允许的,某些重要事务需要中断来处理,例如,Maple的USB通信功能。但是中断有时会对程序执行的时间产生轻微的影响。如果程序中有部分代码对时间非常敏感,可以使用nointerrupts()来防止中断对程序的干扰。

当使用nointerrupts()屏蔽中断后,可以使用interrupts()来恢复对中断的接收。

2.9.2 attachInterrupt()与detachInterrupt()

形式:void attachInterrupt(uint8 pin,voidFuncPtr handler,ExtIntTriggerMode mode)和void detachInterrupt(uint8 pin)

参数:pin为引脚号。

handler为中断发生时,用于处理中断的函数。

mode为中断模式。

中断模式有3种:"RASING"、"FALLING"、"CHANGE"。当设定为"RASING"时,会在指定引脚输入电压从LOW变为HIGH的上升沿触发中断;当设定为"FALLING"时,会在指定引脚输入电压从HIGH变为LOW的下降沿会触发中断;当设定为"CHANGE"时,只要出现上升沿或下降沿都会触发中断。

中断处理函数是不能接受参数的,所以只能通过全局变量或静态变量等方式来与中断处理函数通信。

由引脚0的电平变化触发LED改变明暗状态的代码如代码清单2-17所示。

代码清单2-17 由引脚0的电平变化触发LED改变明暗状态

volatileintstate=LOW;// 必须被声明为 volatile
voidsetup(){
    pinMode(BOARD_LED_PIN,OUTPUT);
    pinMode(0,INPUT);
    attachInterrupt(0,blink,CHANGE); //设定中断,当引脚0的输入电
                                   //平改变时执行blink()函数
}
voidloop(){
    digitalWrite(BOARD_LED_PIN,state);
}
voidblink(){                         //翻转state的值
if(state==HIGH){
    state=LOW;
}else{// state 必须为 LOW
    state=HIGH;
}
}

2.10 数学与位运算操作

在Maple中,你可以使用C++标准的数学运算函数。

2.10.1 min()

形式:min(x,y)

参数:x:第一个数字,可以是任何数字类型的变量。

y:第二个数字,可以是任何数字类型的变量。

该函数会比较x与y的大小,并返回其中较小的数值。

用min()函数取较小值见代码清单2-18。

代码清单2-18 用min()函数取较小值

sensVal=min(sensVal,100);

代码清单2-18将sensVal赋值为sensVal与100中较小的值,以保证sensVal不会大于100。

注意,根据min()实现的原理,请勿在函数内对参数做其他操作,否则可能会导致错误结果,如代码清单2-19所示。

代码清单2-19 在min()函数的括号内做其他操作

min(a++,100);//错误的方法
a++;// 将操作放到括号外
min(a,100);

2.10.2 max()

形式:max(x,y)

参数:x:第一个数字,可以是任何数字类型的变量。

y:第二个数字,可以是任何数字类型的变量。

该函数会返回x、y中数值较大的一个。

用max()函数取最大值见代码清单2-20。

代码清单2-20 用max()函数取最大值

sensVal=max(sensVal,20);

代码清单2-20将sensVal设定为sensVal与20之间较大的一个,以保证sensVal值不会小于20。

与min()函数相同,请勿在函数括号内对数据做其他操作,否则可能会导致错误的结果,如代码清单2-21所示。

代码清单2-21 避免在max()函数括号内做其他操作

in(a--,100);//错误的方法
a--;// 将操作放到括号外
min(a,100);

2.10.3 abs()

形式:abs(x)

参数:x为有符号数字类型。

该函数会将x取绝对值,当x≥0时返回值等于x,当x<0时返回值为-x。与min()和max()函数一样,请勿在函数内做其他操作,否则可能会输出错误的结果,如代码清单2-22所示。

代码清单2-22 避免在括号内做其他操作

abs(a++);// 请避免这样
abs(a);// 正确的方法
a++;

2.10.4 constrain()

形式:constrain(x,a,b)

参数:x为需要被约束的变量。

a为约束的下限值。

b为约束的上限值。

将x的值约束在a到b之间。当a<x<b时返回x,当x<a时返回a,当x>b时返回b。

代码清单2-23的代码是将sensVal的值约束在10~150之间。

代码清单2-23 将sensVal的值约束在10~150之间

sensVal=constrain(sensVal,10,150);

同样,在constrain()括号内请勿做其他操作,以避免得到错误的结果。

2.10.5 map()

形式:long map(long value,long fromStart,long fromEnd,long toStart,long toEnd)

参数:value为需要被重新映射的变量。

fromStart为变量当前范围的开始。

fromEnd为变量当前范围的结束。

toStart为重映射后变量的范围开始。

toEnd为重映射后变量的范围结束。

将一个在某个区间变化的变量,按照比例重新映射到另外一个区间中,它在映射前后的函数关系为:

如果变量value值为fromStart,则重映射后变为toStart;如果变量value值为fromEnd,则重映射后变为toEnd。如果对应的开始值大于对应的结束值,输出范围会是相反的,例如map(x,0,10,10,0),如果x为0,重映射后会变为10。

在map()函数中数字的范围是没有被fromStart与fromEnd约束的,有时这样的设定会有特别的用途。map()函数使用long型变量,只能进行整数的运算,所以任何小数都会被忽略。map()函数的使用见代码清单2-24。

代码清单2-24 map()函数的使用

map(x,0,255,10,20);
//范围也可以为负值
map(x,-10,10,0,20);

2.10.6 pow()

形式:double pow(double x,double y)

参数:x为底数,该变量不能为0,并且只有当x为整数时才可以小于0。

y为幂数。

该函数计算x的y次幂,计算结果为xy

2.10.7 sqrt()

形式:double sqrt(double x)

参数:x:需要被开方的变量,该变量不能为负。

该函数返回值为x的平方根,该值永远都不会为负。

2.10.8 sin()

形式:double sin(double x)

参数:x为以弧度表示的角度。

该函数返回值为x弧度的正弦值,该值在-1~1之间。

2.10.9 cos()

形式:double cos(double x)

参数:x为以弧度表示的角度。

该函数返回值为x弧度的余弦值,该值在-1~1之间。

2.10.10 tan()

形式:double tan(double x)

参数:x为以弧度表示的角度。

该函数返回值为x弧度的正弦值,该值的范围没有限制。

2.10.11 randomSeed()

形式:void randomSeed(unsigned int seed)

参数:seed用于初始化为随机数的种子,该值不能为0。

该函数用于设定random()函数的种子,你可以使用代码清单2-25中的代码,并将PIN0悬空,用PIN0上的电压值设定随机数的种子。由于PIN0上的电压会因电磁干扰而随机变化,所以可以生成完全随机的种子,使伪随机数难以预测。

代码清单2-25 使用模拟输入提供伪随机数种子

long randNumber;
void setup() {
  pinMode(0,INPUT_ANALOG);
  randomSeed(analogRead(0));
}
void loop() {
  randNumber = random(10);
  SerialUSB.println(randNumber);
  delay(1);
}

如果使用确定的seed值,能够得到可以预测的伪随机序列。

2.10.12 random()

形式:random(long max)

random(long min,long max)

参数:max为输出随机数范围的最大值。

min为输出随机数范围的最小值。

该函数以randomSeed()设定的种子输出指定范围的整数伪随机数,random(long max)相当于random(0,long max),输出0~max范围的整数随机数。该函数的使用可以参考代码清单2-25。

2.10.13 lowBit()

形式:lowBit(x)

参数:x可以是任何类型的变量,但是如果x为非整型,得到的值会比较奇怪。

该函数返回x变量的低8位值,返回值在0~255之间。

2.10.14 bitRead()

形式:bitRead(x,n)

参数:x为需要被读取的变量。

n为需要读取的位,从0到最高位。

该函数返回x变量n位上的值,等同于使用位操作(x>>n)&1得到的值。返回值只会是0或1。

2.10.15 bitWrite()

形式:bitWrite(x,n,b)

参数:x为需要写入的变量。

n为需要写入的位,从0到最高位。

b为写入的值,只能为0或1。

该函数向变量x的第n位上写入值b。

2.10.16 bitSet()

形式:bitSet(x,n)

参数:x为需要被操作的变量。

n为需要被设置的位。

该函数将变量x的第n位值设置为1。

2.10.17 bitClear()

形式:bitClear(x,n)

参数:x为需要被操作的变量。

n为需要被设置的位。

该函数将变量x的第n位值设置为0。

2.10.18 bit()

形式:bit(n)

参数:n为需要的位。

该函数返回一个无符号整型第n位表示的值,例如,bit(0)=1,bit(1)=2,bit(2)=4,bit(8)=128。

利用bit()函数简化程序见代码清单2-26。

代码清单2-26 利用bit()函数简化程序

int val=0;
void setup(){
    for(int i=0;i<8;i++)  pinMode(i,INPUT);
}
void loop(){
    for(int i=0;i<8;i++){
         val=val+(bit(i) * digitalRead(i));
    }
    SerialUSB.println(val);
    val=0;
    delay(1000);
}

代码清单2-26读取PIN0~PIN7上的电平,并将它们的电平高低作为并行输入的数据,以数字输出所表示的值。

2.11 Wire库

Wire库是以软件方式实现的I2C总线通信协议。Leaflab正在计划为Maple开发使用硬件I2C控制器的库。

I2C通信使用双线通信,在I2C上可以连接多个从设备。与SPI不同,I2C能够通过发送地址来与不同设备通信,由于具有总裁机制,I2C是一种真正的多主机总线。I2C总线在普通8位传输模式下具有100KB/s的传输速率,快速模式下能达到400KB/s。

I2C通信连接如图2-9所示。

图2-9 I2C通信连接

为了保证地址不冲突,I2C上能够连接的从设备最多允许256个,实际的数量会受到器件地址分配(某些器件的地址是固定的)与总线电容(不大于400pF)的限制。SCL与SDA都为双向通信线路,器件对它们的输出都是开漏模式,必须通过上拉电阻或是电流源连接到正电源,所以空闲时SDA与SCL都是高电平。上拉电阻通常选用1.5~10kΩ。

注意:如果你使用Maple IDE0.0.9~0.0.12,请从http://home.arcor.de/ala42/Wire.zip中下载Wire库解压,覆盖原有Wire库以修复原有库的bug。

2.11.1 begin()

形式:void begin(uint8 pinSDA,uint8 pinSCL)

void begin()

参数:pinSDA为SDA所用引脚。

pinSCL为SCL所用引脚。

begin()函数会以主机模式开启I2C通信,没有参数的begin()函数会以默认的引脚20作为SDA,21作为SCL。

2.11.2 beginTransmission()

形式:void beginTransmission(slave_addr)

参数:slave_addr为从设备地址。

beginTransmission()用于准备向指定的slave_addr传输数据。通过beginTransmission()设定从机地址后,调用endTransmission(),用Wire.send()准备好的数据就会发送给地址为slave_addr的从机,如代码清单2-27所示。

代码清单2-27 I2C发送数据

#include <Wire.h>
byte val = 0;
void setup()
{
  Wire.begin(); // 以默认引脚启动I2C
}
void loop()
{
  Wire.beginTransmission(44); // 指定器件的地址为44 (0x2c)
                       // 根据器件数据表中所提供的地址
  Wire.send(val);           // 发送1字节的数据
  Wire.endTransmission();     // 停止传输
  val++;        // value变量的值加1
  if(val == 64) // 如果val达到64
  {
    val = 0;    // 重设val为0
  }
  delay(500);
}

2.11.3 send()

形式:void send(data)

void send(buffer,length)

参数:data,buffer为待发送的单字节数据,可以是uint8*、byte、char*。

length为数组长度。

send()函数将待发送的数据放入发送缓冲区,之后,endTransmission()函数才会真正将数据通过I2C发送出去。

由于缓冲的限制,每次发送的数据不能超过32字节。在使用send()之前必须使用beginTransmission()设定从机地址。send(buffer,length)形式用于发送一个长度为length字节的数组,数据的长度仍然不可以超过32字节。

2.11.4 endTransmission()

形式:state endTransmission()

参数:无

endTransmission()会将之前通过send()函数放入发送缓冲的数据发送出去。

endTransmission()的返回值有如表2-9所示的几种状态。

表2-9 end Transmission()所回值的几种状态

2.11.5 requestFrom()

形式:void requestFrom(uint8 address,int num_bytes)

参数:address表示从该地址的设备接收数据。

num_bytes为接收数据的长度,以字节为单位。

requestForm()从address地址的设备接收num_bytes字节的数据,返回的数据长度是实际所读取到的长度,之后这些读取到的数据可以由receive()函数读出。如果接收到的数据长度超过32字节,则超出32字节的部分会被忽略。

使用requestFrom()请求数据见代码清单2-28。

代码清单2-28 使用requestFrom()请求数据

#include <Wire.h>
void setup()
{
  Wire.begin();        // 以主机模式启动I2C
}
void loop()
{
  Wire.requestFrom(2,6);    // 从地址为2的设备请求6字节的数据
  while(Wire.available())    // 从设备可能发送少于6字节
  {
    char c = Wire.receive();   // 读出下一字节
    Serial.print(c);   // 显示该字符
  }
  delay(500);
}

2.11.6 receive()

形式:byte receive()

参数:无

在requestFrom()之后,receive()用于读出接收缓冲区中下一字节的数据。

2.11.7 available()

形式:uint8 available()

该函数返回接收缓冲区中可以读取的字节数。

2.12 Servo库

Servo库用于控制伺服舵机。舵机能够根据控制信号脉冲的宽度精确地定位在指定的角度(通常为0°~180°)。该库与Arduino的Servo完全兼容,所有的控制通过Servo类中的函数来操作。向舵机发送的角度信号是以脉冲宽度来传输的,通常用544~2400μs的脉冲表示0°~180°。

2.12.1 attach()

形式:bool attach(uint8 pin,uint16 minPulseWidth,uint16 maxPulseWidth,int16 minAngle,int16 maxAngle)

bool attach(uint8 pin)

参数:pin为控制引脚,所选用的引脚必须能够进行PWM输出。

minPulseWidth为最小脉冲宽度,以微秒为单位,其默认值为SERVO_DEFAULT_MIN_PW=544。

maxPulseWidth为最大脉冲宽度,以微秒为单位,其默认值为SERVO_DEFAULT_MAX_PW=2400。

minAngle为舵机最小角度,以度为单位,默认为SERVO_DEFAULT_MIN_ANGLE=0。

maxAngle为舵机最大角度,以度为单位,其默认值为SERVO_DEFAULT_MAX_ANGLE=180。

对pin引脚的舵机控制进行初始化。该库应该使用PWM输出,所以指定的pin必须是具有PWM功能的引脚。当使用attach(pin)时会默认设定一个运动范围在0°~180°的舵机。

初始化伺服控制器见代码清单2-29。

代码清单2-29 初始化伺服控制器

#include <Servo.h>
Sevro sev1;//建立两个伺服控制对象,sev1与sev2
Sevro sev2;
void setup(){
   sev1.attach(14);//初始化一个连接到14引脚的运动范围在0°~180°的舵机
   sev2.attach(12,0,360);//初始化一个连接到引脚12的运动范围在0°~360°的舵机
}

2.12.2 attached()

形式:bool attached()

attached()用于检测Maple与舵机是否已经正确连接,返回一个布尔型变量。

2.12.3 write()

形式:void write(int angle)

参数:angle为舵机角度,以度为单位。

该函数设定舵机的角度为angle度。

设定舵机角度见代码清单2-30。

代码清单2-30 设定舵机角度

#include <Servo.h>  //引用舵机控制库
Servo sev;        //实例化Servo
void setup(){
sev.attach(14);  //设定舵机控制引脚为PIN14
}
void loop(){
  //移动到0度位置,并延时两秒
  sev.write(0);
  delay(2000);
  //移动到90度位置,并延时两秒
  sev.write(90);
  delay(2000);
  //移动到180度位置,并延时两秒
  sev.write(180);
  delay(2000);
}

2.12.4 writeMicroseconds()

形式:void writeMicroseconds(uint16 pulseWidth)

参数:pulseWidth为脉冲宽度(ms)。

该函数直接设定用于控制舵机的脉冲宽度,以微秒为单位。

2.12.5 readMicroseconds()

形式:uint16 readMicroseconds()

该函数读取当前发送给舵机的脉冲宽度,以微秒为单位。

2.12.6 read()

形式:int read()

该函数读取当前为舵机设定的角度值,也就是返回最后一次write()函数所写入的角度。

2.12.7 detach()

形式:bool detach()

该函数停止驱动伺服舵机。

利用Servo库进行舵机操作见代码清单2-31。

代码清单2-31 利用Servo库进行舵机操作

#include <Servo.h>  //引用舵机控制库
Servo sev;        //实例化Servo
void setup(){
sev.attach(14);  //设定舵机控制引脚为PIN14
}
void loop(){
  //将舵机移动到0度位置,并延时两秒
  sev.write(0);
  delay(2000);
  //将发送给舵机的脉冲宽度设定为2000 μs
  sev.writeMicroseconds(2000);
  delay(2000);
  //读取当前发送给舵机的角度和脉冲宽度
  SerialUSB.println(sev.read());
  SerialUSB.println(sev.readMicroseconds());
  //停止驱动舵机
  sev.detach();
}

2.13 LiquidCrystal库

LiquidCrystal库用于控制Hitachi HD44780芯片或是兼容协议的液晶屏幕。大部分字符液晶屏都使用该协议(例如最常用的1602或1604字符液晶屏)。该库支持4位数据线或8位数据线传输方式。

Maple的LiquidCrystal库与Arduino中的LiquidCrystal库没有已知的兼容性问题。

控制液晶显示屏需要首先建立一个LisquidCrystal类的对象,利用该对象对液晶屏进行控制。

2.13.1 LiquidCrystal()

形式:LiquidCrystal lcd_4(rs,enable,d4,d5,d6,d7)

LiquidCrystal lcd_4(rs,rw,enable,d4,d5,d6,d7)

LiquidCrystal lcd_8(rs,enable,d0,d1,d2,d3,d4,d5,d6,d7)

LiquidCrystal lcd_8(rs,rw,enable,d0,d1,d2,d3,d4,d5,d6,d7)

参数:rs为连接到LCD rs引脚的Maple引脚。

enable为连接到LCD enable引脚的Maple引脚。

rw为连接到LCD rw引脚的Maple引脚。

d0~d7为连接到LCD各位数据的Maple引脚。

LiquidCrystal lcd(rs,enable,d4,d5,d6,d7)建立一个4线数据传输的LiquidCrystal对象。

LiquidCrystal lcd(rs,rw,enable,d4,d5,d6,d7)建立一个4线数据传输带读写控制的LiquidCrystal对象。

LiquidCrystal lcd(rs,enable,d0,d1,d2,d3,d4,d5,d6,d7)建立一个8线液晶连接。

LiquidCrystal lcd(rs,rw,enable,d0,d1,d2,d3,d4,d5,d6,d7)建立一个带读写控制的8线液晶LiquidCrystal对象。

代码清单2-32建立了一个利用4位数据线控制液晶屏的类"lcd",用于控制一个1602字符液晶屏(需要使用3.3V的液晶屏)。PIN12设定为rs信号,PIN11设定为enable信号,LCD数据信号D4~D7分别对应Maple的PIN5~PIN2。具体电路连接可以见后面1602字符液晶显示屏实验的详细说明。

代码清单2-32 建立一个LiquidCrystal对象并显示“hello,world!”

#include <LiquidCrystal.h>
LiquidCrystal lcd(12,11,5,4,3,2);
void setup()
{
lcd.begin(16,2);
  lcd.print("hello,world!");
}
void loop() {}

2.13.2 begin()

形式:lcd.begin(cols,rows)

参数:cols为列数。

rows为行数。

该函数设定一个有cols列rows行的显示屏。

例如,常用1602字符液晶屏设定为"lcd.begin(16,2);",1604字符液晶屏设定为"lcd.begin(16,4);"。

2.13.3 write()

形式:write(data)

参数:data为需要显示的数据。

该函数在光标所在位置显示字符,所显示的字符仅限数字、英文以及自定义字符。

代码清单2-33 write()函数写字符

#include <LiquidCrystal.h>
LiquidCrystal lcd(12,11,5,4,3,2);
void setup()
{
    lcd.begin(16,2);
}
void loop()
{
  if (SerialUSB.available()) {
      lcd.write(Serial.read());
  }
}

2.13.4 clear()

形式:void clear()

参数:无

不论显示了什么,该函数都会将整个显示屏清空。在显示新内容前,需要将之前的内容清除并重新复位光标位置,否则新写入的内容会继续在旧内容之后显示。

使用clear()清除内容以显示新内容见代码清单2-34。

代码清单2-34 使用clear()清除内容以显示新内容

#include <LiquidCrystal.h>
LiquidCrystal lcd(12,11,5,4,3,2);
void setup()
{
     lcd.begin(16,2);
}
void loop()
{
    lcd.home();
    lcd.print("First");
    delay(1000);
    lcd.clear();
    lcd.home();
    lcd.print("Second");
    delay(1000);
    lcd.clear();
}

2.13.5 home()

形式:void home()

参数:无

该函数将光标移动到LCD的左上角(0,0)的位置,之后的字符会从该位置开始写入。

2.13.6 cursor()与noCursor()

形式:void cursor()

void noCursor()

参数:无

字符液晶屏的光标与计算机上的光标一样,用于表示当前输入位置,通常以一条下划线表示,cursor()用以开启光标显示,noCursor()用于隐藏光标。默认情况下,光标是不显示的。

2.13.7 setCursor()

形式:void setCursor(col,row)

参数:cols为列数。

rows为行数。

该函数将光标移动到指定的行与列,之后的字符会从该位置开始写入。

2.13.8 noDisplay()与display()

形式:noDisplay()

display()

参数:无

noDisplay()关闭LCD显示,display()开启LCD显示,当执行noDisplay()后原本显示的内容会被保留,仅仅只是关闭显示的输出;而执行display()后,原本显示的内容会自动恢复。

2.13.9 blink()与noBlink()

形式:blink()

noBlink()

参数:无

blink()与noBlink()用于控制光标的闪烁,blink()开启光标闪烁,noBlink()关闭光标闪烁。

2.13.10 leftToRight()与rightToLeft()

形式:leftToRight()

rightToLeft()

参数:无

leftToRight()与rightToLeft()用于设定文本显示的方向。leftToRight()是默认方式,输入的文字会从左到右显示;rightToLeft()可以将显示方式改变为从右到左。leftToRight()与rightToLeft()不会改变之前输入文字的显示方向,只会改变之后写入文字的显示方向。

2.13.11 autoscroll()与noAutoscroll()

形式:autoscroll()

noAutoscroll()

参数:无

这两个函数开启和关闭LCD自动滚动内容功能。该功能使新写入的字符保留在原来位置而移动先前写入的字符。当屏幕定义为从左到右写入时,每写入一个字符,之前的字符向左移动一个字符位;当屏幕定义为从右到左写入时反之。

2.13.12 scrollDisplayLeft()与scrollDisplayRight()

形式:scrollDisplayLeft()

scrollDisplayRight()

参数:无

这两个函数表示将内容(包括字符与光标)向左(scrollDisplayLeft())或是向右(scrollDisplayRight())滚动一个字符的位置。

2.13.13 print()

形式:print(data)

print(data,BASE)

参数:BASE为自定义字符的编号。

data为需要显示的数据,可以是long、int、double、char、string等。

在光标所在位置显示数据,可以是字符串、字符、数字。该函数类似于SerialUSB.print(),进制的表示方法也类似,可以按照需要的进制显示整数数据。

2.13.14 createChar()

形式:void createChar(num,data)

参数:num为自定义字符的编号。

data表示自定义字符的byte类型数组。

createChar()可以建立至多8个5×8pixels的自定义字符。data是byte类型的8字节数组,每行占用一个字节,每个字节的低5位表示每行中的每个像素。我们可以用代码清单2-35中的数组来定义字符:

代码清单2-35 定义一个数组表示笑脸

byte smiley[8] = {
     B00000,
     B10001,
     B00000,
     B00000,
     B10001,
     B01110,
     B00000,
};

其中,"B"表示后面的数据是以二进制表示,smiley数组定义了一个笑脸字符。

定义并显示自定义字符见代码清单2-36。

代码清单2-36 定义并显示自定义的字符

#include <LiquidCrystal.h>
LiquidCrystal lcd(12,11,5,4,3,2);
byte smiley[8] = {         //定义一个笑脸字符
  B00000,
  B10001,
  B00000,
  B00000,
  B10001,
  B01110,
  B00000,
};
void setup() {
  lcd.createChar(0,smiley);//定义自定义字符
  lcd.begin(16,2);//设定写入位置为2行16列(从左向右数)
  lcd.write(0);  //在2行16列的位置写入0号字符(也就是前面定义的笑脸)
}
void loop() {}