作为创客社区的成员,我们一直在寻找创造性的方法来使用硬件和软件。这一次,Patrick Lima 和我决定使用 Arduino 板扩展 Raspberry Pi 的端口,以便我们可以访问更多的功能和端口,并为设备增加一层保护。这种设置有很多用途,例如构建一个跟踪太阳的太阳能电池板、一个家庭气象站、操纵杆交互等等。
我们决定从构建一个仪表板开始,该仪表板允许以下串行端口交互
- 控制三个 LED 灯的开关
- 控制三个 LED 灯以调整其光强度
- 识别正在使用的端口
- 显示操纵杆上的输入移动
- 测量温度
我们还想在一个漂亮的用户界面 (UI) 中展示端口、硬件和传感器之间的所有交互,就像这样

(Bruno Muniz,CC BY-SA 4.0)
你可以使用本文中的概念来构建许多不同的项目,这些项目使用许多不同的组件。你的想象力是唯一的限制!
1. 开始入门

(Bruno Muniz,CC BY-SA 4.0)
第一步是扩展 Raspberry Pi 的端口,使其也可以使用 Arduino 端口。这可以通过 Linux ARM 的原生串行通信实现来实现,它使你能够使用 Arduino 的数字、模拟和脉冲宽度调制 (PWM) 端口在 Raspberry Pi 上运行应用程序。
本项目使用 TotalCross,一个用于为嵌入式设备构建 UI 的开源软件开发工具包,通过终端执行外部应用程序并使用原生串行通信。你可以使用两个类来实现此目的:Runtime.exec 和 PortConnector。它们代表了执行这些操作的不同方式,因此我们将在本教程中展示如何使用这两种方式,你可以决定哪种方式最适合你。
要开始这个项目,你需要
- 1 个 Raspberry Pi 3
- 1 个 Arduino Uno
- 3 个 LED 灯
- 2 个介于 1K 和 2.2K 欧姆之间的电阻
- 1 个按钮
- 1 个介于 1K 和 50K 欧姆之间的电位器
- 1 个原型板(又名面包板)
- 跳线
2. 设置 Arduino
创建一个通信协议来接收消息、处理消息、执行请求以及在 Raspberry Pi 和 Arduino 之间发送响应。这在 Arduino 上完成。
2.1 定义消息格式
接收到的每条消息都将具有以下格式
- 调用的函数指示
- 使用的端口
- 字符分隔符(如果需要)
- 要发送的值(如果需要)
- 消息结束指示
下表列出了字符及其各自的功能、示例值以及示例的描述。此示例中使用的字符选择是任意的,可以随时更改。
字符 | 功能 | 示例 | 示例描述 |
---|---|---|---|
* | 指令结束 | - | - |
, | 分隔符 | - | - |
# | 设置模式 | #8,0* | 引脚 8 输入模式 |
< | 设置数字值 | <1,0* | 设置引脚 1 低电平 |
> | 获取数字值 | >13* | 获取引脚 13 的值 |
+ | 获取 PWM 值 | +6,250* | 设置引脚 6 值为 250 |
- | 获取模拟值 | -14* | 获取引脚 A0 的值 |
2.2 源代码
以下源代码实现了上述通信协议。它必须发送到 Arduino,以便它可以解释和执行消息命令
void setup() {
Serial.begin(9600);
Serial.println("Connected");
Serial.println("Waiting command...");
}
void loop() {
String text="";
char character;
String pin="";
String value="0";
char separator='.';
char inst='.';
while(Serial.available()){ // verify RX is getting data
delay(10);
character= Serial.read();
if(character=='*'){
action(inst,pin,value);
break;
}
else {
text.concat(character);}
if(character==',') {
separator=character;
if(inst=='.'){
inst = character;}
else if(separator!=',' && character!=inst ){
pin.concat(character);}
else if (character!=separator && character!=inst ){
value.concat(character);}
}
}
void action(char instruction, String pin, String value){
if (instruction=='#'){//pinMode
pinMode(pin.toInt(),value.toInt());
}
if (instruction=='<'){//digitalWrite
digitalWrite(pin.toInt(),value.toInt());
}
if (instruction=='>'){ //digitalRead
String aux= pin+':'+String(digitalRead(pin.toInt()));
Serial.println(aux);
}
if (instruction=='+'){ // analogWrite = PWM
analogWrite(pin.toInt(),value.toInt());
}
if (instruction=='-'){ // analogRead
String aux= pin+':'+String(analogRead(pin.toInt()));
Serial.println(aux);
}
}
2.3 构建电子元件
定义你需要测试的内容,以检查与 Arduino 的通信并确保输入和输出按预期响应
- LED 灯以正逻辑连接。通过电阻连接到 GND 引脚,并使用数字端口 I/O 2 和 PWM 3 激活它。
- 按钮具有连接到数字端口 I/O 4 的下拉电阻,如果不按下则发送 0 信号,如果按下则发送 1 信号。
- 电位器与中心引脚连接到模拟输入 A0,一侧引脚连接到正极,另一侧引脚连接到负极。

(Bruno Muniz,CC BY-SA 4.0)
2.4 测试通信
将第 2.2 节中的代码发送到 Arduino。打开串行监视器并通过发送以下命令来检查通信协议
#2,1*<2,1*>2*
#3,1*+3,10*
#4,0*>4*
#14,0*-14*
这应该是串行监视器中的结果

(Bruno Muniz,CC BY-SA 4.0)
设备上的一个 LED 灯应以最大强度亮起,另一个 LED 灯应以较低强度亮起。

(Bruno Muniz,CC BY-SA 4.0)
按下按钮并在发送读取命令时更改电位器的位置将显示不同的值。例如,将电位器转到正极侧并按下按钮。在按钮仍处于按下状态时,发送以下命令
>4*
-14*
应该出现两行

(Bruno Muniz,CC BY-SA 4.0)
3. 设置 Raspberry Pi
使用 Raspberry Pi 通过终端访问串行端口,使用 cat
命令读取条目,使用 echo
命令发送消息。
3.1 进行串行测试
将 Arduino 连接到 Raspberry Pi 上的一个 USB 端口,打开终端,然后执行此命令
cat /dev/ttyUSB0 9600
这将启动与 Arduino 的连接并显示返回到串行的内容。

(Bruno Muniz,CC BY-SA 4.0)
要测试发送命令,请打开一个新的终端窗口(保持前一个窗口打开),然后发送此命令
echo "command" > /dev/ttyUSB0 9600
你可以发送与第 2.4 节中使用的相同命令。
你应该在第一个终端中看到反馈,以及你在第 2.4 节中获得的相同结果

(Bruno Muniz,CC BY-SA 4.0)
4. 创建图形用户界面
此项目的 UI 将很简单,因为目标只是展示使用串行端口进行端口扩展。另一篇文章将使用 TotalCross 为本项目创建一个高质量的 GUI,并启动应用程序后端(与传感器一起工作),如本文顶部的仪表板图像所示。
第一部分使用两个 UI 组件:Listbox 和 Edit。这些组件在 Raspberry Pi 和 Arduino 之间建立连接,并测试一切是否按预期工作。
模拟你在其中输入命令并查看答案的终端
- Edit 用于发送消息。将其放置在底部,宽度设置为 FILL,以将组件扩展到屏幕的整个宽度。
- Listbox 用于显示结果,例如,在终端中。将其添加到 TOP 位置,从 LEFT 侧开始,宽度与 Edit 相同,高度设置为 FIT 以垂直占据 Edit 未填充的所有空间。
package com.totalcross.sample.serial;
import totalcross.sys.Settings;
import totalcross.ui.Edit;
import totalcross.ui.ListBox;
import totalcross.ui.MainWindow;
import totalcross.ui.gfx.Color;
public class SerialSample extends MainWindow {
ListBox Output;
Edit Input;
public SerialSample() {
setUIStyle(Settings.MATERIAL_UI);
}
@Override
public void initUI() {
Input = new Edit();
add(Input, LEFT, BOTTOM, FILL, PREFERRED);
Output = new ListBox();
Output.setBackForeColors(Color.BLACK, Color.WHITE);
add(Output, LEFT, TOP, FILL, FIT);
}
}
它应该看起来像这样

(Bruno Muniz,CC BY-SA 4.0)
5. 设置串行通信
如上所述,有两种设置串行通信的方法:Runtime.exec 和 PortConnector。
5.1 选项 1:使用 Runtime.exec
java.lang.Runtime
类允许应用程序创建与运行环境的连接接口。它允许程序使用 Raspberry Pi 的原生串行通信。
使用你在第 3.1 节中使用的相同命令,但现在使用 UI 上的 Edit 组件将命令发送到设备。
读取串行
应用程序必须不断读取串行,如果返回一个值,则使用线程将其添加到 Listbox。线程是在后台处理进程而不阻塞用户交互的好方法。
以下代码在此线程上创建一个新进程,该进程执行 cat
命令,测试串行,并启动一个无限循环来检查是否收到新内容。如果收到内容,则将该值添加到 Listbox 组件的下一行。只要应用程序正在运行,此过程将继续运行
new Thread () {
@Override
public void run() {
try {
Process Runexec2 = Runtime.getRuntime().exec("cat /dev/ttyUSB0 9600\n");
LineReader lineReader = new LineReader(Stream.asStream(Runexec2.getInputStream()));
String input;
while (true) {
if ((input = lineReader.readLine()) != null) {
Output.add(input);
Output.selectLast();
Output.repaintNow();
}
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}.start();
}
发送命令
发送命令是一个更简单的过程。它在你按下 Edit 组件上的 Enter 键时发生。
要将命令转发到设备,如第 3.1 节所示,你必须实例化一个新的终端。为此,Runtime 类必须在 Linux 上执行 sh
命令
try{
Runexec = Runtime.getRuntime().exec("sh").getOutputStream() }catch (IOException ioe) {
ioe.printStackTrace();
}
在用户在 Edit 中写入命令并按下 Enter 键后,应用程序会触发一个事件,该事件使用 Edit 中指示的值执行 echo
命令
Input.addKeyListener(new KeyListener() {
@Override
public void specialkeyPressed(KeyEvent e) {
if (e.key == SpecialKeys.ENTER) {
String s = Input.getText();
Input.clear();
try {
Runexec.write(("echo \"" + s + "\" > /dev/ttyUSB0 9600\n").getBytes());
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
@Override
public void keyPressed(KeyEvent e) {} //auto-generate code
@Override
public void actionkeyPressed(KeyEvent e) {} //auto-generate code
});
在连接了 Arduino 的 Raspberry Pi 上运行应用程序并发送命令进行测试。结果应该是

(Bruno Muniz,CC BY-SA 4.0)
Runtime.exec 源代码
以下是包含所有解释部分的源代码。它包括将在第 31 行读取串行的线程和将在第 55 行发送命令的 KeyListener
package com.totalcross.sample.serial;
import totalcross.ui.MainWindow;
import totalcross.ui.event.KeyEvent;
import totalcross.ui.event.KeyListener;
import totalcross.ui.gfx.Color;
import totalcross.ui.Edit;
import totalcross.ui.ListBox;
import java.io.IOException;
import java.io.OutputStream;
import totalcross.io.LineReader;
import totalcross.io.Stream;
import totalcross.sys.Settings;
import totalcross.sys.SpecialKeys;
public class SerialSample extends MainWindow {
OutputStream Runexec;
ListBox Output;
public SerialSample() {
setUIStyle(Settings.MATERIAL_UI);
}
@Override
public void initUI() {
Edit Input = new Edit();
add(Input, LEFT, BOTTOM, FILL, PREFERRED);
Output = new ListBox();
Output.setBackForeColors(Color.BLACK, Color.WHITE);
add(Output, LEFT, TOP, FILL, FIT);
new Thread() {
@Override
public void run() {
try {
Process Runexec2 = Runtime.getRuntime().exec("cat /dev/ttyUSB0 9600\n");
LineReader lineReader = new
LineReader(Stream.asStream(Runexec2.getInputStream()));
String input;
while (true) {
if ((input = lineReader.readLine()) != null) {
Output.add(input);
Output.selectLast();
Output.repaintNow();
}
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}.start();
try {
Runexec = Runtime.getRuntime().exec("sh").getOutputStream();
} catch (IOException ioe) {
ioe.printStackTrace();
}
Input.addKeyListener(new KeyListener() {
@Override
public void specialkeyPressed(KeyEvent e) {
if (e.key == SpecialKeys.ENTER) {
String s = Input.getText();
Input.clear();
try {
Runexec.write(("echo \"" + s + "\" > /dev/ttyUSB0 9600\n").getBytes());
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
@Override
public void keyPressed(KeyEvent e) {
}
@Override
public void actionkeyPressed(KeyEvent e) {
}
});
}
}
5.2 选项 2:使用 PortConnector
PortConnector 专门用于处理串行通信。如果你想遵循原始示例,你可以跳过本节,因为这里的目的是展示另一种更简单的串行工作方式。
更改原始源代码以使用 PortConnector
package com.totalcross.sample.serial;
import totalcross.io.LineReader;
import totalcross.io.device.PortConnector;
import totalcross.sys.Settings;
import totalcross.sys.SpecialKeys;
import totalcross.ui.Edit;
import totalcross.ui.ListBox;
import totalcross.ui.MainWindow;
import totalcross.ui.event.KeyEvent;
import totalcross.ui.event.KeyListener;
import totalcross.ui.gfx.Color;
public class SerialSample extends MainWindow {
PortConnector pc;
ListBox Output;
public SerialSample() {
setUIStyle(Settings.MATERIAL_UI);
}
@Override
public void initUI() {
Edit Input = new Edit();
add(Input, LEFT, BOTTOM, FILL, PREFERRED);
Output = new ListBox();
Output.setBackForeColors(Color.BLACK, Color.WHITE);
add(Output, LEFT, TOP, FILL, FIT);
new Thread() {
@Override
public void run() {
try {
pc = new PortConnector(PortConnector.USB, 9600);
LineReader lineReader = new LineReader(pc);
String input;
while (true) {
if ((input = lineReader.readLine()) != null) {
Output.add(input);
Output.selectLast();
Output.repaintNow();
}
}
} catch (totalcross.io.IOException ioe) {
ioe.printStackTrace();
}
}
}.start();
Input.addKeyListener(new KeyListener() {
@Override
public void specialkeyPressed(KeyEvent e) {
if (e.key == SpecialKeys.ENTER) {
String s = Input.getText();
Input.clear();
try {
pc.writeBytes(s);
} catch (totalcross.io.IOException ioe) {
ioe.printStackTrace();
}
}
}
@Override
public void keyPressed(KeyEvent e) {
}
@Override
public void actionkeyPressed(KeyEvent e) {
}
});
}
}
你可以在项目仓库中找到所有代码。
6. 后续步骤
本文展示了如何通过使用 Runtime 或 PortConnector 类在 Java 中使用 Raspberry Pi 串行端口。你还可以调用其他语言的外部文件并创建无数其他项目——例如用于水族箱的水质监测系统,通过模拟输入测量温度;或者带有温度和湿度调节以及伺服电机旋转鸡蛋的育雏器。
未来的文章将使用 PortConnector 实现(因为它专注于串行连接)来完成与所有传感器的通信。它还将添加数字输入并完成 UI。
以下是一些参考资料,供进一步阅读
在你连接 Arduino 和 Raspberry Pi 后,请在下方留下评论并分享你的结果。我们很乐意阅读它们!
1 条评论