跳到主要内容

UART

在本章中,我们将学习如何使用串口和终端设备。

示例程序:Code.zip

1.终端设备

终端设备,简称 TTY(Teletypewriter的缩写),最早源于电传打字机,用于与计算机进行交互通信。TTY 最初指代连接到Unix系统上的物理或虚拟终端。随着时间的推移,TTY 的概念也扩展到串口设备,如ttyn、ttySACn等。在 Linux 系统中,终端设备的支持非常强大,它们通常通过特殊的设备文件进行访问和控制,从而实现串口通信。这些设备文件位于 /dev 目录下,可以通过它们来读取和写入数据,以及执行各种终端控制操作。

2.串口测试(Shell)

2.1 引脚分布

UART2为调试串口,其它串口开启方法参考 PWM 部分。LuckFox Pico Ultra/Ultra W 引脚图:

2.2 设备目录

/dev 目录中,每个 UART 设备都有其自己的目录。这些目录的名称是 ttyS 加上串口号,例如 /dev/ttyS3 表示 UART3。您可使用如下命令查看:

# ls /dev/ttyS*
/dev/ttyS1

2.3 配置串口

  1. 用stty工具查询其通信参数

    # stty -F /dev/ttyS3
    speed 9600 baud; line = 0;
    -brkint -imaxbel
  2. 修改波特率,其中ispeed为输入速率,ospeed为输出速率

    # stty -F /dev/ttyS3 ispeed 115200 ospeed 115200
  3. 关闭回显

    stty -F /dev/ttyS3 -echo

2.4 与Windows主机通讯

  1. 将串口模块一端连接电脑,另一端连接 LuckFox Pico 的引脚18(GND)、19(UART7_TX)和20(UART7_TX)上。

  2. 下载并打开 MobaXterm,选择串口,设置波特率(默认为9600,请根据自己实际修改过的数值设置)。

  3. 在开发板上的终端执行以下指令,使用 echo 命令向终端设备文件写入字符串"Hello"和"world !":

    echo Hello > /dev/ttyS3
    echo "world !" > /dev/ttyS3
  4. Windows 上的串口调试助手会接收到内容:

3.串口通信(Python程序)

在前文中,我们演示了使用命令配置串口并向终端设备文件写入字符串实现串口通信。接下来,我们将使用 Python 程序实现串口通信。

3.1 使用pyserial

  1. 完整代码

    这段代码使用了 Python 的 serial 库,实现串口3收发数据。

    import serial
    import time

    with serial.Serial(
    "/dev/ttyS3",
    baudrate=115200,
    bytesize=serial.EIGHTBITS,
    stopbits=serial.STOPBITS_ONE,
    parity=serial.PARITY_NONE,
    timeout=1,
    ) as uart3:
    uart3.write(b"Hello World!\n")
    buf = uart3.read(128)
    print("Raw data:\n", buf)
    data_strings = buf.decode("utf-8")
    print("Read {:d} bytes, printed as string:\n {:s}".format(len(buf), data_strings))
  2. 打开串口

    在这段代码中,使用 serial.Serial 对象打开了一个串口连接,该串口位于设备文件 /dev/ttyS3。通过设置不同的参数,如波特率 (baudrate)、数据位 (bytesize)、停止位 (stopbits)、奇偶校验 (parity) 和超时时间 (timeout),可以配置串口的通信属性。这段代码使用 with 语句确保在使用完串口后正确关闭连接。

    with serial.Serial(
    "/dev/ttyS3",
    baudrate=115200,
    bytesize=serial.EIGHTBITS,
    stopbits=serial.STOPBITS_ONE,
    parity=serial.PARITY_NONE,
    timeout=1,
    ) as uart3:
  3. 发送数据

    通过调用串口对象的 write 方法,这段代码发送了一个字节字符串 (b"Hello World!\n") 到已打开的串口。

    uart3.write(b"Hello World!\n")
  4. 接收数据

    在这段代码中,使用 read 方法从串口读取最多 128 字节的数据,且超过 1 秒结束读取。读取的原始数据以字节形式打印,然后通过 decode("utf-8") 将字节数据解码为 UTF-8 编码的字符串。最后,打印读取的字节数和解码后的字符串。

    buf = uart3.read(128)
    print("Raw data:\n", buf)
    data_strings = buf.decode("utf-8")
    print("Read {:d} bytes, printed as string:\n {:s}".format(len(buf), data_strings))

3.2 使用python-periphery

  1. 完整代码

    这段代码使用了 periphery 库的 Serial 类,实现串口3收发数据。

    from periphery import Serial

    try:
    serial = Serial(
    "/dev/ttyS3",
    baudrate=115200,
    databits=8,
    parity="none",
    stopbits=1,
    xonxoff=False,
    rtscts=False,
    )

    serial.write(b"Hello World!\n")
    buf = serial.read(128, 1)
    print("Raw data:\n", buf)
    data_strings = buf.decode("utf-8")
    print("Read {:d} bytes, printed as string:\n {:s}".format(len(buf), data_strings))

    finally:
    serial.close()
  2. 打开串口

    这段代码使用 periphery 库的 Serial 类,打开了一个串口连接。串口的配置包括设备文件路径 (/dev/ttyS3)、波特率、数据位、校验位、停止位等参数。

    serial = Serial(
    "/dev/ttyS3",
    baudrate=115200,
    databits=8,
    parity="none",
    stopbits=1,
    xonxoff=False,
    rtscts=False,
    )
  3. 发送数据

    该段代码通过已打开的串口连接 serial,向串口发送了一个包含 "Hello World!\n" 字节的数据包。

    serial.write(b"Hello World!\n")
  4. 接收数据

    在这段代码中,通过调用 serial.read(128, 1) 从串口读取最多 128 字节的数据,且超过 1 秒结束读取。读取的原始数据以字节形式打印,然后通过 decode("utf-8") 将字节数据解码为 UTF-8 编码的字符串。最后,打印读取的字节数和解码后的字符串。

    buf = serial.read(128, 1)
    print("Raw data:\n", buf)
    data_strings = buf.decode("utf-8")
    print("Read {:d} bytes, printed as string:\n {:s}".format(len(buf), data_strings))

3.3 运行程序

  1. 使用 nano 工具在终端创建 py 文件,粘贴并保存 python 程序

    # nano uart.py
  2. 运行程序

    # python3 uart.py
  3. 实验现象

    将UART3的TX与RX相接,运行程序,选择UART3进行通信:

4.串口通信(C程序)

在前文中,我们演示了如何使用 Shell 命令和 Python 程序实现串口通讯。此外,我们还可以使用C库函数或系统调用来读写设备文件,以达到串口通信的目的。请注意,为了在特定的嵌入式系统上运行程序,通常需要使用交叉编译工具来编译代码,以生成可在目标开发板上执行的可执行文件。接下来,让我们一起探讨具体的实施步骤。

4.1 完整代码

通过以下程序,可以实现串口通信。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>

int main() {
int serial_port_num;
char serial_port[15];

printf("Select a serial port (3/4): ");
scanf("%d", &serial_port_num);

sprintf(serial_port,"/dev/ttyS%d",serial_port_num);
int serial_fd;

serial_fd = open(serial_port, O_RDWR | O_NOCTTY);
if (serial_fd == -1) {
perror("Failed to open serial port");
return 1;
}

struct termios tty;
memset(&tty, 0, sizeof(tty));

if (tcgetattr(serial_fd, &tty) != 0) {
perror("Error from tcgetattr");
return 1;
}

cfsetospeed(&tty, B9600);
cfsetispeed(&tty, B9600);

tty.c_cflag &= ~PARENB;
tty.c_cflag &= ~CSTOPB;
tty.c_cflag &= ~CSIZE;
tty.c_cflag |= CS8;

if (tcsetattr(serial_fd, TCSANOW, &tty) != 0) {
perror("Error from tcsetattr");
return 1;
}

char tx_buffer[] = "hello world!\n";
ssize_t bytes_written = write(serial_fd, tx_buffer, sizeof(tx_buffer));
if (bytes_written < 0) {
perror("Error writing to serial port");
close(serial_fd);
return 1;
}
printf("\rtx_buffer: \n %s ", tx_buffer);

char rx_buffer[256];
int bytes_read = read(serial_fd, rx_buffer, sizeof(rx_buffer));
if (bytes_read > 0) {
rx_buffer[bytes_read] = '\0';
printf("\rrx_buffer: \n %s ", rx_buffer);
} else {
printf("No data received.\n");
}

close(serial_fd);

return 0;
}

4.2 打开串口

这段代码首先让用户选择使用串口3或串口4进行通信,然后打开了相应的串口设备文件,将其文件描述符保存在 serial_fd 变量中。

printf("Select a serial port (3/4): ");
scanf("%d", &serial_port_num);

sprintf(serial_port,"/dev/ttyS%d",serial_port_num);
int serial_fd;

serial_fd = open(serial_port, O_RDWR | O_NOCTTY);
if (serial_fd == -1) {
perror("Failed to open serial port");
return 1;
}

4.3 配置串口

在这部分代码中,我们定义了一个名为 tty 的 termios 结构体,用于配置串口通信的参数。首先,我们使用 memset 将其初始化为0。然后,通过 tcgetattr 函数获取当前串口的属性,并将其存储在 tty 结构体中。

struct termios tty;
memset(&tty, 0, sizeof(tty));

if (tcgetattr(serial_fd, &tty) != 0) {
perror("Error from tcgetattr");
return 1;
}

在这部分代码中,我们设置了串口通信的一些参数。我们使用 cfsetospeedcfsetispeed 函数将波特率设置为9600,分别用于设置输出和输入的波特率;清除 PARENB 标志,以禁用奇偶校验;通过 c_cflag 属性操作标志来清除 CSTOPB 标志,以使用一个停止位;通过清除 CSIZE 标志来清除数据位,并通过 CS8 标志来设置数据位为8位。最后,使用 tcsetattr 函数将修改后的属性设置为串口的当前属性,这里使用了 TCSANOW 标志,表示立即应用这些设置。

cfsetospeed(&tty, B9600);
cfsetispeed(&tty, B9600);

tty.c_cflag &= ~PARENB;
tty.c_cflag &= ~CSTOPB;
tty.c_cflag &= ~CSIZE;
tty.c_cflag |= CS8;

if (tcsetattr(serial_fd, TCSANOW, &tty) != 0) {
perror("Error from tcsetattr");
return 1;
}

4.4 发送数据

这段代码通过向 serial_fd 写入字符串数据 "hello world!\n" 以实现串口数据发送,发送成功将在终端打印数据。

char tx_buffer[] = "hello world!\n";
ssize_t bytes_written = write(serial_fd, tx_buffer, sizeof(tx_buffer));
if (bytes_written < 0) {
perror("Error writing to serial port");
close(serial_fd);
return 1;
}
printf("\rtx_buffer: \n %s ", tx_buffer);

4.5 接收数据

这段代码通过从 serial_fd 读取数据以实现串口数据接收,接收成功将在终端打印数据。

char rx_buffer[256];
int bytes_read = read(serial_fd, rx_buffer, sizeof(rx_buffer));
if (bytes_read > 0) {
rx_buffer[bytes_read] = '\0';
printf("\rrx_buffer: \n %s ", rx_buffer);
} else {
printf("No data received.\n");
}

4.6 交叉编译

  1. 指定交叉编译工具

    首先,我们要将交叉编译工具的路径添加到系统的 PATH 环境变量中,以便可以在任何地方使用交叉编译工具,您可以在shell配置文件中添加以下行(通常是 ~/.bashrc~/.bash_profile~/.zshrc,具体取决于您使用的shell),注意 PATH= 后的路径为交叉编译工具所在的目录。

    • gcc路径

      <SDK Directory>/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc
    • 打开shell配置文件

      vi ~/.bashrc 
    • 将交叉编译工具的路径添加到系统的PATH环境变量中,将 <SDK Directory> 修改为自己的 SDK 路径,如 /home/luckfox/luckfox-pico/

      export PATH=<SDK Directory>/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin:$PATH
    • 重新加载shell配置文件,使更改生效

      source ~/.bashrc  
  2. 使用交叉编译工具编译程序

    arm-rockchip830-linux-uclibcgnueabihf-gcc uart.c -o uart
  3. 交叉编译成功后,将在当前目录下生成可在开发板运行的可执行文件

    # ls
    uart uart.c

4.7 运行程序

  1. 文件传输

    先将 uart 从虚拟机传输到 Windows,再通过 TFTP 或 ADB 传输到开发板,将文件从 Windows 通过 ADB 将文件传输到开发板的步骤如下:

    adb push 文件所在路径 开发板存储路径

    eg:(将当前目录下uart文件传输到开发板的根目录)
    adb push uart /
  2. 运行程序

    修改 uart 文件的操作权限后运行程序

    # chmod 777 uart
    # ./uart
  3. 实验现象

    将UART3的TX与RX相接,运行程序,选择UART3进行通信: