Arduino开发实战指南:STM32篇(txt+pdf+epub+mobi电子书下载)


发布时间:2020-07-28 15:12:18

点击下载

作者:姚汉

出版社:机械工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

Arduino开发实战指南:STM32篇

Arduino开发实战指南:STM32篇试读:

前言

早年的DIY爱好者集中于电子制作,后来由于计算机的出现,电子类DIY逐渐被边缘化,取而代之的是对程序的研究。而Arduino这类开源硬件为物理电路连接的现实世界与虚拟的程序世界架起了一座桥梁,通过它们可以方便地利用程序控制物理的驱动器,或是读取传感器数据,与现实世界交互,为很多环境交互应用提供了实施的平台。因此,近年来开源硬件与物理计算得以迅速发展,并有愈演愈烈的趋势,吸引着各行各业越来越多的人使用。

开源硬件中最为人熟知的是Arduino,它将嵌入式编程中复杂的寄存器操作改为简化的C++语言,大幅度降低了嵌入式设计的门槛。我也是从Arduino开始使用的,但是在使用的过程中逐渐发现Arduino的不足——计算能力小与芯片资源过少。后来我了解到Maple。作为一款以ARM Cortex-M3为处理器核心的Arduino兼容硬件,Maple在具有Arduino易开发特性的同时,在工作频率、储存器等芯片资源上有了很大提高。利用Maple可以开发出很多优秀的互动作品,实现一些更复杂的创意想法。

现在,国内的Maple中文资源比较缺乏,这极大地限制了开源硬件爱好者对Maple的了解与使用,给很多正在使用的朋友带来了不便,也阻碍了很多有兴趣的朋友对它的交流。所以作为第一批将其引入国内并推广的人,我写了这本书,希望能给Maple入门使用者带来一些便利,以使他们更好地利用Maple实现自己的创意。

全书主要分为四部分内容,第一部分主要讲解Maple控制器上Arduino兼容库及其使用,第二部分讲解一些基础的元器件知识,第三部分主要讲解如何使用Maple控制器由简到难地完成各种实验,第四部分则结合开源嵌入式实时操作系统进行简单的嵌入术应用开发。

希望这本书能够促进Maple和其他开源硬件的推广和应用。也希望使用Maple的朋友能够在网络上分享自己的创意与实施过程,并一起推动Maple这款开源硬件的完善和改进。由于作者水平有限,书中难免存在错误与疏漏,欢迎广大读者指正。

作者

第1章 Maple简介

1.1 Maple与Arduino的关系

Maple是由麻省理工学院的学生所组成的Leaflab实验室开发的,其目的是让科技不再局限于高投入的实验室,将低成本、易开发的嵌入式设备推广开来。

在设计上,Maple对Arduino有很高的兼容性,例如,部分引脚的排列和功能、提供的操作函数(除了Arduino中的tone()与pluseIn()函数不支持)。Maple与Arduino最大的不同在于它所使用的处理器是32位的ARM处理器,而Arduino采用的是8位的AVR处理器。与AVR处理器相比,ARM处理器在处理速度、RAM容量、Flash容量、引脚数量、成本上都有很大的优势。

尽管性能上不如Maple,但是与Maple相比,Arduino出现得更早,更加成熟,有更多的扩展和库。

1.2 Maple的衍生版本

和Arduino一样,在发展的过程中,Maple也出现了为不同目标应用设计的不同版本,这里简单介绍下Maple Rev5以外的版本。

1.2.1 Maple RET6

Maple RET6是Maple Rev5的改进版本。硬件上,它与Maple Rev5的区别是它使用的是STM32F103RET6处理器。与Maple Rev5所使用的STM32F103RBT6(只有一个字符的差别)相比,RET6多了两路DAC输出,片上Flash与SRAM储存器容量分别由128KB和20KB提升为512KB与64KB,其他方面差别不大。

1.2.2 Maple Mini

Maple Mini具有兼容面包板的双列直插外形,如图1-1所示,可以直接用于面包板。外形只有51.3cm×1.82cm,也非常适合于对于尺寸和重量有较高限制的场合。例如,微型4轴飞行器、微型机器人等。Maple Mini采用的处理器为STM32F103CBT6,与Maple相比,GPIO(General Purpose Input Output,通用输入输出)数有所减少。如果需要作为便携设备使用,Maple Mini是首选。

图1-1 Maple Mini(图片来自leaflabs.com)

1.2.3 Maple Native

Maple Native是为了充分发挥处理器性能而设计的高性能开发板,使用了高端STM32F103处理器。与Maple相比,Maple Native具有两倍于Maple的GPIO数并提供外部扩展SRAM,能够应对高要求的应用。Maple Native具有DAC,能够真正实现高精度的模拟输出(Maple和Arduino只能通过PWM来输出模拟量)。同时,与其他的高性能开发板相比,在顾及高性能的设计之外,Maple Native仍然提供与Arduino类似的库。

1.3 Maple的硬件资源

Maple采用的是意法半导体集团生产的STM32F103RBT处理器,这款处理器是以ARM Cortex-M3为核心,工作在72MHz的频率下。与Arduino所使用的8位处理器相比,Maple的处理器性能和硬件资源有了很大的提升,但是却有相近的成本,为电子积木拓宽了应用的空间。Maple及其衍生版本与Arduino的性能对比如表1-1所示。Maple的布局如图1-2所示。

表1-1 Maple及其衍生版本与Arduino性能对比

图1-2 Maple的布局

1.4 libmaple简介

libmaple是由Leaflab开发的,用于STM32库,对STM32底层操作进行了重新封装,大幅提高了操作的简便性。

libmaple分为两部分:第一部分,以C程序编写的底层库,将其称为libmaple,提供对硬件高度灵活的操作,是严格意义上的libmaple,也是本章将要介绍的libmaple,在源码中的/libmaple目录下;第二部分,以C++编写的Wiring风格的API,该部分提供对第2章中介绍的Arduino兼容库部分的支持,位于源码中的/wirish目录下。

libmaple的源码位于GitHub上,可以在https://github.com/leaflabs/libmaple中找到。

libmaple附随于Maple IDE中,可以直接在Maple IDE的代码中加入对libmaple头文件的引用,同时libmaple也可以用于其他开发平台包,给习惯使用专业嵌入式开发IDE的用户使用。

1.5 Maple IDE的安装和使用

1.5.1 下载Maple IDE

目前Maple IDE支持Mac、Windows和Linux,可以在其官方网站http://leaflabs.com/docs/maple-ide-install.html下载到最新版本的Maple IDE。将下载的ZIP文件解压到合适的位置。

1.5.2 安装Maple IDE

1.Windows环境下安装Maple IDE

在Windows环境下,Maple IDE无需安装,但是需要为其安装DFU和虚拟串口两个驱动程序。首先按照如下步骤安装DFU驱动(用于上传程序到Maple)程序:

1)将Maple连接到计算机的USB接口。

2)按下左下角的复位键(印有"REST"),这时蓝色指示灯会快速闪烁6次,然后慢闪烁几次。

3)再次按下复位键,这次在蓝色指示灯快速闪烁6次期间按下另一个键(右上角,印有"BUT")并保持不放,直到指示灯开始慢闪烁时放开。

4)这时Maple会处于永久Bootloader状态(Perpetual Bootloader Mode),蓝色指示灯会一直闪烁,使你有机会安装DFU驱动程序。

5)Windows会提示你需要驱动程序,人工指定驱动程序所在目录位置,选择Maple IDE文件夹中的drivers\mapleDrv\dfu。

下一步安装虚拟串口驱动程序(用于通过USB与Maple进行串口通信):

1)复位Maple,等待蓝色指示灯停止闪烁(退出bootloader转而运行用户程序,新的Maple会运行制造时预留的测试程序)。

2)一旦Maple运行了用户程序,Windows会提示安装驱动程序,同样,人工指定驱动程序所在目录为Maple IDE文件夹中的drivers\mapleDrv\serial。

现在可以双击Maple IDE程序运行Maple IDE了。

2.Linux环境下安装Maple IDE

在Linux环境下安装Maple IDE的步骤如下:

1)首先确认是否已经安装了JRE,如果没有,可以通过如下命令安装:

sudo aptitude install openjdk-6-jre

2)解压压缩文件到合适的位置(例如桌面)。

3)打开解压压缩文件得到的文件夹,运行其中的install-udev-rules.sh,出现要求输入用于获得管理员权限的密码的提示。

4)用"sudo restart udev"命令重启udev。

5)双击程序运行Maple IDE。

3.Mac环境下安装Maple IDE

在Mac环境下安装Maple IDE的步骤如下:

1)双击你所下载的DMG文件使该镜像被加载。

2)拖动Maple IDE按钮到应用程序文件夹。

3)运行Maple IDE。

1.5.3 第一个程序

我们从File→Examples→Digital中选择一个简单的例程Blink,如图1-3所示,这个程序会让蓝色指示灯闪烁。

图1-3 打开例程"Blink"

然后进入Tools→Board→LeafLabs Maple...to Flash(根据你所选使用的型号选择),如图1-4所示。每种型号的Maple都有两个选项to Flash和to RAM。to RAM会将程序上传到Maple的RAM存储器,与上传到Flash相比,上传速度更快,并且通过简单的复位或者重新上电就可以清除程序。但是Flash空间更大,并且是可以在掉电后存储程序的唯一选择。如果需要调试程序,那么to RAM模式能够提高你的效率。

现在,可以单击左上角的(Verify)按钮来编译程序,编译的过程与结果会显示在底部的窗口中。该过程主要用于验证程序,检验程序是否有语法或函数引用错误。

现在,将Maple用USB Mini-B接口的USB线连接到计算机上。每次复位、重编程或接上USB,蓝色指示灯都会闪烁一段时间,表示它进入了bootloader状态。等到Maple开始执行用户程序,进入Tools→Serial Port选择Maple在你系统中所使用的串口设备,如图1-5所示。在不同的计算机不同的USB接口上,Maple会占用不同的串口,可以通过查看设备管理器来了解Maple所占用的串口。

图1-4 选择目标所使用的Maple类型

图1-5 选择Maple所使用的串口

单击(Upload)按钮开始上传程序,上传过程会在下方窗口显示。

Maple IDE是利用USB虚拟串口对Maple发送复位信号,复位后Maple会进入DFU模式片刻等待上传,如果没有上传程序,则会在数秒后开始执行原有的用户程序。

USB虚拟串口相关程序嵌入用户程序中,所以如果前一次上传程序出错,或者错误地选择了串口设备,都可能导致正常的下载操作无法进行。你可以在提示等待DFU设备的时候对Maple进行复位,或让Maple进入“永久bootloader状态”之后再进行下载,来规避由于USB虚拟串口通信问题造成的无法正常下载的问题。

最后,为了保证一切正常,我们可以上传一个通过USB虚拟串口发送文本“Hello,world!”的例程。单击File→Examples→Stubs→Helloworld打开该例程,单击(Upload)按钮将其上传到刚才已连接的Maple设备上。待Maple复位并进入用户程序(蓝色指示灯停止闪烁并听到第二声USB设备插入声),单击(Serial Monitor window)按钮打开串口监视器,如果你的串口设置正确,就可以看到Maple通过虚拟USB串口传回的文字。

1.5.4 Maple IDE的使用

与Arduino IDE一样,Maple IDE也是在Processing IDE的基础上开发的,且简单易用,非常容易上手。下面对Maple IDE做一些简单的介绍。Maple IDE的界面主要有代码编辑区、工具栏和状态栏组成,如图1-6所示。

图1-6 Maple IDE界面

Maple IDE的基本功能主要依靠上方的工具栏按钮来实现,如表1-2所示为工具栏按钮及其功能。

表1-2 工具栏按钮及其功能

1.6 Maple的开源协议

作为开源硬件的一员,Maple开发者给予Maple最宽松的协议,设计图以Creative Commons share-a-like方式发布,任何人都可以任意地使用、修改、重新发布Maple的设计和代码。但Maple IDE中来源于Arduino与Processing项目的代码需要遵从它们开发者的协议。对开源协议的尊重和保护在保护开发者的利益的同时,能够提高开发者的积极性,让开源事业有更好的发展。

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

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

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

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为-9-6-31,则定时分辨率为13.89ns(1ns=10s=10ms=10μ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 使用定时器的两个通道同步操作

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载