第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() {}