Linux 系统中用C语言调用串口

总览

不幸的是,在Linux中使用串行端口并不是世界上最简单的事情。在处理termios.h标头时,存在许多复杂的设置,这些设置隐藏在价值多个字节的位域中。该页面试图帮助解释这些设置,并向您展示如何在Linux中正确配置串行端口。

一切都是文件

在典型的UNIX风格中,串行端口由操作系统中的文件表示。这些文件通常会弹出/dev/,并以name开头tty*。

常用名称是:

  • /dev/ttyACM0-ACM代表USB总线上的ACM调制解调器。Arduino UNO(及类似名称)将使用此名称显示。
  • /dev/ttyPS0 -运行基于Yocto的Linux构建的Xilinx Zynq FPGA将使用此名称作为Getty连接到的默认串行端口。
  • /dev/ttyS0-标准COM端口将具有此名称。如今,由于较新的台式机和笔记本电脑没有实际的COM端口,这些情况已经不太普遍了。
  • /dev/ttyUSB0 -大多数USB到串行电缆将使用这样的文件显示。
  • /dev/pts/0-伪终端。这些可以通过生成socat。
    Linux和连接的Arduino的/dev/目录的列表。 Arduino串行端口显示为/dev/ttyACMO0。
    Linux和连接的Arduino的/dev/目录的列表。Arduino串行端口显示为/dev/ttyACMO0。

要写入串行端口,请写入文件。要从串行端口读取,请从文件读取。当然,这允许您发送/接收数据,但是如何设置串行端口参数,例如波特率,奇偶校验等?这是通过特殊tty配置设置的struct。

C语言的基本设置

  • 注意
    此代码也适用于C ++。
    首先,我们要包括一些内容:
// C library headers
#include <stdio.h>
#include <string.h>

// Linux headers
#include <fcntl.h> // Contains file controls like O_RDWR
#include <errno.h> // Error integer and strerror() function
#include <termios.h> // Contains POSIX terminal control definitions
#include <unistd.h> // write(), read(), close()
然后,我们要打开串行端口设备(在下方显示为文件/dev/),保存由返回的文件描述符open():

int serial_port = open("/dev/ttyUSB0", O_RDWR);

# Check for errors
if (serial_port < 0) {
    printf("Error %i from open: %s\n", errno, strerror(errno));
}

一个你可能会看到这里的常见错误是errno = 2,和strerror(errno)回报No such file or directory。确保您具有正确的设备路径,并且该设备存在!

您可能会在这里遇到的另一个常见错误errno = 13是Permission denied。这通常是因为当前用户不属于拨出组。使用以下命令将当前用户添加到拨出组:

$ sudo adduser $USER dialout

这些组更改生效之前,您必须先注销然后重新登录。
在这一点上,我们可以从技术上对串行端口进行读写,但是由于默认配置设置不是为串行端口设计的,因此它可能无法工作。因此,现在我们将正确设置配置。
修改任何配置值时,最佳做法是仅修改您感兴趣的位,而保留该字段的所有其他位。这就是为什么你会看到使用下面&=或者|=,从来没有&或|设置位时。

配置设置

我们需要访问该termios结构才能配置串行端口。我们将创建一个新termios结构,然后使用写入串行端口的现有配置tcgetattr(),然后根据需要修改参数并使用保存设置tcsetattr()。

// Create new termios struc, we call it 'tty' for convention
// No need for "= {0}" at the end as we'll immediately write the existing
// config to this struct
struct termios tty;

// Read in existing settings, and handle any error
if(tcgetattr(serial_port, &tty) != 0) {
    printf("Error %i from tcgetattr: %s\n", errno, strerror(errno));
}

现在,我们可以tty根据需要更改的设置,如以下各节所示。

控制模式(c_cflags)

该结构的c_cflags成员termios包含控制参数字段。

  • PARENB(平价)
    如果该位置1,则启用奇偶校验位的生成和检测。大多数串行通信不使用奇偶校验位,因此,如果不确定,请清除该位。
tty.c_cflag &= ~PARENB; // Clear parity bit, disabling parity (most common)
tty.c_cflag |= PARENB;  // Set parity bit, enabling parity

CSTOPB(数字停止位)
如果该位置1,则使用两个停止位。如果清除该位,则仅使用一个停止位。大多数串行通信仅使用一个停止位。

tty.c_cflag &= ~CSTOPB; // Clear stop field, only one stop bit used in communication (most common)
tty.c_cflag |= CSTOPB;  // Set stop field, two stop bits used in communication

每字节位数

该CS字段中设置多少个数据位,每个字节通过串行端口传输。此处最常见的设置是8(CS8)。如果不确定,请绝对使用此端口,在此之前我从未使用过未使用8的串行端口(但它们确实存在)。

tty.c_cflag |= CS5; // 5 bits per byte
tty.c_cflag |= CS6; // 6 bits per byte
tty.c_cflag |= CS7; // 7 bits per byte
tty.c_cflag |= CS8; // 8 bits per byte (most common)

流量控制(CRTSCTS)

如果CRTSCTS设置了该字段,则启用硬件RTS / CTS流控制。这里最常见的设置是禁用它。当应禁用此功能时启用它可能会导致您的串行端口不接收任何数据,因为发送方将无限期地对其进行缓冲,等待您“就绪”。

tty.c_cflag &= ~CRTSCTS; // Disable RTS/CTS hardware flow control (most common)
tty.c_cflag |= CRTSCTS;  // Enable RTS/CTS hardware flow control

CREAD和CLOCAL

设置CLOCAL将禁用调制解调器特定的信号线,例如载波检测。SIGHUP当检测到调制解调器断开连接时,还可以防止控制过程发送信号,这通常是一件好事。设置CLOCAL使我们能够读取数据(我们绝对想要!)。

tty.c_cflag |= CREAD | CLOCAL; // Turn on READ & ignore ctrl lines (CLOCAL = 1)

本地模式(c_lflag)

  • 禁用规范模式
    UNIX系统提供输入,的两种基本模式的规范和非规范模式。在规范模式下,当收到换行符时,将处理输入。接收应用程序逐行接收该数据。在处理串行端口时,这通常是不希望的,因此我们通常要禁用规范模式。

通过以下方式禁用了规范模式:

tty.c_lflag &= ~ICANON;

同样,在规范模式下,某些字符(例如退格键)会被特殊对待,并用于编辑当前文本行(擦除)。同样,我们不希望此功能处理原始串行数据,因为它将导致特定字节丢失!

ECHO

如果该位置1,发送的字符将被回显。因为我们禁用了规范模式,所以我认为这些位实际上没有任何作用,但是以防万一,以防万一!

tty.c_lflag &= ~ECHO; // Disable echo
tty.c_lflag &= ~ECHOE; // Disable erasure
tty.c_lflag &= ~ECHONL; // Disable new-line echo

禁用信号字符

当该ISIG位置1时INTR,QUIT和会SUSP被解释。我们不希望使用串行端口,因此请清除以下位:

tty.c_lflag &= ~ISIG; // Disable interpretation of INTR, QUIT and SUSP

输入模式(c_iflag)

该结构的c_iflag成员termios包含用于输入处理的低级设置。所述c_iflag构件是一个int。

软件流控制(IXOFF,IXON,IXANY)

结算IXOFF,IXON并IXANY禁用软件流控制,这是我们不想要的:

tty.c_iflag &= ~(IXON | IXOFF | IXANY); // Turn off s/w flow ctrl

禁用接收时字节的特殊处理

清除以下所有位将禁用对字节的任何特殊处理,因为这些字节在被串行端口接收并传递给应用程序之前。我们只想要原始数据,谢谢!

tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL); // Disable any special handling of received bytes

输出模式(c_oflag)

结构的c_oflag成员termios包含用于输出处理的低级设置。配置串行端口时,我们要禁用对输出字符/字节的任何特殊处理,因此请执行以下操作:

tty.c_oflag &= ~OPOST; // Prevent special interpretation of output bytes (e.g. newline chars)
tty.c_oflag &= ~ONLCR; // Prevent conversion of newline to carriage return/line feed
// tty.c_oflag &= ~OXTABS; // Prevent conversion of tabs to spaces (NOT PRESENT IN LINUX)
// tty.c_oflag &= ~ONOEOT; // Prevent removal of C-d chars (0x004) in output (NOT PRESENT IN LINUX)

双方OXTABS并ONOEOT没有在Linux的定义。但是Linux确实具有XTABS似乎相关的领域。为Linux编译时,我只排除了这两个字段,并且串行端口仍然可以正常工作。

VMIN和VTIME(c_cc)

VMIN对于试图在Linux中配置串行端口的许多程序员来说,这VTIME是一个混乱的根源。

需要注意的重要一点是,VTIME根据内容的不同,含义也有所不同VMIN。当VMIN为0时,VTIME指定从read()调用开始的超时。但是当VMIN>> 0时,VTIME指定从第一个接收到的字符开始的超时时间。

让我们探索不同的组合:

  • VMIN = 0,VTIME = 0:无阻塞,立即返回可用值
  • VMIN> 0,VTIME = 0:这将read()始终等待字节(确切地由决定多少个字节VMIN),因此read()可以无限期地阻塞。
  • VMIN = 0,VTIME> 0:这是对最大超时(由给出VTIME)的任何数字字符的阻塞读取。read()将阻塞直到有大量数据可用或发生超时为止。这恰好是我最喜欢的模式(也是我最常使用的模式)。
  • VMIN> 0,VTIME> 0:阻塞直到VMIN接收到任何字符或VTIME第一个字符过去。请注意,VTIME直到收到第一个字符,超时才会开始。
  • VMIN和VTIME都定义为type cc_t,我一直看到它是unsigned char(1个字节)的别名。这使VMIN字符数的上限为255,最大超时为25.5秒(255分秒)。

“一旦收到任何数据,立即返回”并不意味着您一次只能得到1个字节。根据操作系统延迟,串行端口速度,硬件缓冲区以及您无法直接控制的许多其他因素,您可能会收到任意数量的字节。

例如,如果我们要等待最多1秒的时间,一旦收到任何数据就返回,我们可以使用:

tty.c_cc[VTIME] = 10;    // Wait for up to 1s (10 deciseconds), returning as soon as any data is received.
tty.c_cc[VMIN] = 0;

波特率

而不是使用位字段与所有其他设置,串口波特率是通过调用函数集cfsetispeed()和cfsetospeed(),传递的一个指针tty结构和enum:

// Set in/out baud rate to be 9600
cfsetispeed(&tty, B9600);
cfsetospeed(&tty, B9600);

如果要保持UNIX兼容,则必须从以下一项中选择波特率:

B0,  B50,  B75,  B110,  B134,  B150,  B200, B300, B600, B1200, B1800, B2400, B4800, B9600, B19200, B38400, B57600, B115200, B230400, B460800

如果使用GNU C库进行编译,则可以放弃这些枚举,而直接指定整数波特率,例如:

// Specifying a custom baud rate when using GNU C
cfsetispeed(&tty, 104560);
cfsetospeed(&tty, 104560);

并非所有硬件都支持所有波特率,因此如果可以选择的话,最好坚持使用上述标准BXXX速率之一。如果您不知道波特率是多少,并且尝试与第三方系统通信,请尝试B9600,然后B57600再尝试,B115200因为它们是最常用的速率。

  • 有关Linux串行端口代码示例,请参见https://github.com/gbmhunter/CppLinuxSerial。

保存termios

更改这些设置后,我们可以使用以下命令保存ttytermios结构tcsetattr():

// Save tty settings, also checking for error
if (tcsetattr(serial_port, TCSANOW, &tty) != 0) {
    printf("Error %i from tcsetattr: %s\n", errno, strerror(errno));
}

读写

现在我们已经打开并配置了串行端口,我们可以对其进行读写了!

  • 写入
    通过该write()功能完成对Linux串行端口的写入。我们使用serial_port从open()上面的调用返回的文件描述符。
unsigned char msg[] = { 'H', 'e', 'l', 'l', 'o', '\r' };
write(serial_port, "Hello, world!", sizeof(msg));

读取

通过该read()功能进行读取。您必须为Linux提供缓冲区以将数据写入其中。

// Allocate memory for read buffer, set size according to your needs
char read_buf [256];

// Read bytes. The behaviour of read() (e.g. does it block?,
// how long does it block for?) depends on the configuration
// settings above, specifically VMIN and VTIME
int n = read(serial_port, &read_buf, sizeof(read_buf));

// n is the number of bytes read. n may be 0 if no bytes were received, and can also be negative to signal an error.

关闭串口

这很简单:

close(serial_port)

完整的例子

// C library headers
#include <stdio.h>
#include <string.h>

// Linux headers
#include <fcntl.h> // Contains file controls like O_RDWR
#include <errno.h> // Error integer and strerror() function
#include <termios.h> // Contains POSIX terminal control definitions
#include <unistd.h> // write(), read(), close()

// Open the serial port. Change device path as needed (currently set to an standard FTDI USB-UART cable type device)
int serial_port = open("/dev/ttyUSB0", O_RDWR);

// Create new termios struc, we call it 'tty' for convention
struct termios tty;

// Read in existing settings, and handle any error
if(tcgetattr(serial_port, &tty) != 0) {
    printf("Error %i from tcgetattr: %s\n", errno, strerror(errno));
}

tty.c_cflag &= ~PARENB; // Clear parity bit, disabling parity (most common)
tty.c_cflag &= ~CSTOPB; // Clear stop field, only one stop bit used in communication (most common)
tty.c_cflag |= CS8; // 8 bits per byte (most common)
tty.c_cflag &= ~CRTSCTS; // Disable RTS/CTS hardware flow control (most common)
tty.c_cflag |= CREAD | CLOCAL; // Turn on READ & ignore ctrl lines (CLOCAL = 1)

tty.c_lflag &= ~ICANON;
tty.c_lflag &= ~ECHO; // Disable echo
tty.c_lflag &= ~ECHOE; // Disable erasure
tty.c_lflag &= ~ECHONL; // Disable new-line echo
tty.c_lflag &= ~ISIG; // Disable interpretation of INTR, QUIT and SUSP
tty.c_iflag &= ~(IXON | IXOFF | IXANY); // Turn off s/w flow ctrl
tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL); // Disable any special handling of received bytes

tty.c_oflag &= ~OPOST; // Prevent special interpretation of output bytes (e.g. newline chars)
tty.c_oflag &= ~ONLCR; // Prevent conversion of newline to carriage return/line feed
// tty.c_oflag &= ~OXTABS; // Prevent conversion of tabs to spaces (NOT PRESENT ON LINUX)
// tty.c_oflag &= ~ONOEOT; // Prevent removal of C-d chars (0x004) in output (NOT PRESENT ON LINUX)

tty.c_cc[VTIME] = 10;    // Wait for up to 1s (10 deciseconds), returning as soon as any data is received.
tty.c_cc[VMIN] = 0;

// Set in/out baud rate to be 9600
cfsetispeed(&tty, B9600);
cfsetospeed(&tty, B9600);

// Save tty settings, also checking for error
if (tcsetattr(serial_port, TCSANOW, &tty) != 0) {
    printf("Error %i from tcsetattr: %s\n", errno, strerror(errno));
}

// Write to serial port
unsigned char msg[] = { 'H', 'e', 'l', 'l', 'o', '\r' };
write(serial_port, "Hello, world!", sizeof(msg));

// Allocate memory for read buffer, set size according to your needs
char read_buf [256];

// Normally you wouldn't do this memset() call, but since we will just receive
// ASCII data for this example, we'll set everything to 0 so we can
// call printf() easily.
memset(&read_buf, '\0', sizeof(read_buf);

// Read bytes. The behaviour of read() (e.g. does it block?,
// how long does it block for?) depends on the configuration
// settings above, specifically VMIN and VTIME
int num_bytes = read(serial_port, &read_buf, sizeof(read_buf));

// n is the number of bytes read. n may be 0 if no bytes were received, and can also be -1 to signal an error.
if (num_bytes < 0) {
    printf("Error reading: %s", strerror(errno));
}

// Here we assume we received ASCII data, but you might be sending raw bytes (in that case, don't try and
// print it to the screen like this!)
printf("Read %i bytes. Received message: %s", num_bytes, read_buf);

close(serial_port)

getty的问题

如果getty试图管理tty要与之进行串行通信的同一设备,它可能会引起串行通信问题。

停止getty:

getty很难停止,因为默认情况下,如果您尝试杀死进程,那么新进程将立即启动。

这些说明适用于旧版本的Linux和/或嵌入式Linux。

加载/etc/inittab您喜欢的文本编辑器。
注释掉涉及getty到您的tty设备的所有行。
保存并关闭文件。
运行命令~$ init q以重新加载/etc/inittab文件。
终止设备上所有正在运行的getty进程tty。他们现在应该死了!

进程独立占用

最好尝试同时防止其他进程对串行端口进行读/写操作。
实现此目的的一种方法是使用flock()系统调用:

#include <sys/file.h>

int main() {

    // ... get file descriptor here

    // Acquire non-blocking exclusive lock
    if(flock(fd, LOCK_EX | LOCK_NB) == -1) {
        throw std::runtime_error("Serial port with file descriptor " + 
            std::to_string(fd) + " is already locked by another process.");
    }

    // ... read/write to serial port here
}

例子

有关Linux串行端口代码示例,请参见https://github.com/gbmhunter/CppLinuxSerial(请注意,该库是用C ++而不是C编写的)。

外部资源

有关结构配置参数的官方规范,请参见http://www.gnu.org/software/libc/manual/html_node/Terminal-Modes.htmltermios。

树莓派结合ADC做个光敏感应的灯

树莓派结合ADC做个光敏感应灯

前言

其实没有什么想法,就是想把ADC用熟练了,这些小案例都是自己杜撰的,实际上没有那么多场景需要用,但是最近真的用在了智能浇花设备上,土壤湿度采样的传感器是模拟的,所以,可以用ADC秀一波操作。

操作步骤

  • 步骤1:从https://www.raspberrypi.org/downloads/下载最新镜像,然后选择Raspbian。
  • 步骤2:烧录镜像然后启动树莓派。 
  • 步骤3:通过在终端中键入以下命令连接到Internet并更新系统:
    sudo apt-get update
    sudo apt-get upgrade
  • 步骤4:将所有东西连接在一起然后打开你的树莓派,不知道为啥网上给树莓派起名字叫覆盆子,太tm难听了。raspberry就改成树莓不行么?

    手绘电路图,看懂了就过,看不懂就自己学习一下再过。

    接驳示意图,ADC的AIN1 通道采样。中间抽头给树莓派,采集信号信息。

    C语言编程

    下面的内容就是开始编程了,如果你喜欢用C,那么就这样,写个源码:

    #include <stdio.h>
    #include <stdlib.h>
    #include <linux/i2c-dev.h>
    #include <sys/ioctl.h>
    #include <fcntl.h>
    #include <wiringPi.h>
    void main()
    {
    wiringPiSetup(); 
    //Physical Pin = 40, name is GPIO.29 and wPi name is 29, BCM 21.
    pinMode(29, OUTPUT);   
    int file; 
    char *bus = "/dev/i2c-1";
    if ((file = open(bus, O_RDWR)) < 0) 
    {
     printf("Failed to open the bus.\n");
     exit(1);
     } 
    // Get I2C device, ADS1115 I2C address is 0x48(72)
    ioctl(file, I2C_SLAVE, 0x48) 
    // Select configuration register(0x01)
    // AINP = AIN0 and AINN = AIN1, +/- 2.048V 
    // Continuous conversion mode, 128 SPS(0x84, 0x83) 
    char config[3] = {0}; 
    config[0] = 0x01; 
    config[1] = 0xD4; 
    config[2] = 0x83; 
    write(file, config, 3); 
    sleep(1); 
    // Read 2 bytes of data from register(0x00) 
    // raw_adc msb, raw_adc lsb 
    char reg[1] = {0x00};
    write(file, reg, 1); 
    char data[2]={0};
    if(read(file, data, 2) != 2) 
    { 
    printf("Error : Input/Output Error\n");
    }
    else
    { 
    // Convert the data 
    int raw_adc = (data[0] * 256 + data[1]);
    if (raw_adc > 32767) 
    { 
      raw_adc -= 65535; 
    }
    // Output data to screen 
    printf("Analog Data is: %d \n", raw_adc);
    if ( raw_adc > 3200 )
     { 
      printf("Turn on LED\n");
      digitalWrite(29, LOW); // turn on the LED 
      } 
    else { 
        printf("Turn off LED\n"); 
        digitalWrite(29, HIGH);  //turn off the LED
          }
     } 
    }

    编译和测试:

    gcc -o adc -lwiringPi adc.c 

    注意:

  • gcc是编译工具,-o表示定义输出文件名,-lwiringPi表示需要使用wiringPi的库来完成编译代码。
  • 编译后,将在工作目录中获得名为sensor的二进制文件,只需使用此命令执行它:
    while true
    do
    ./adc 
    done

Python编程

  • 如果你喜欢用python好吧,那么就更简单了。直接用adafruit的ads1x15的代码改改就能用。
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    # Author: Jacky.Li
    # License: Public Domain
    import time
    import Adafruit_ADS1x15
    import os
    adc = Adafruit_ADS1x15.ADS1115()
    # Choose a gain of 1 for reading voltages from 0 to 4.09V.
    # Or pick a different gain to change the range of voltages that are read:
    # - 2/3 = +/-6.144V
    # - 1 = +/-4.096V
    # - 2 = +/-2.048V
    # - 4 = +/-1.024V
    # - 8 = +/-0.512V
    # - 16 = +/-0.256V
    # See table 3 in the ADS1015/ADS1115 datasheet for more info on gain.
    GAIN = 1
    print("Reading ADS1115 values, press Ctrl-C to quit...") 
    # Main loop. 
    while True:
    # Read all the ADC channel values in a list.
    values = [0]*4
    for i in range(4):
    # Read the specified ADC channel using the previously set gain value.
    values[i] = adc.read_adc(i, gain=GAIN) 
    # Note you can also pass in an optional data_rate parameter that controls 
    # the ADC conversion time (in samples/second). Each chip has a different 
    # set of allowed data rate values, see datasheet Table 9 config register 
    # DR bit values. 
    #values[i] = adc.read_adc(i, gain=GAIN, data_rate=128) 
    # Each value will be a 12 or 16 bit signed integer value depending on the   
    # ADC (ADS1015 = 12-bit, ADS1115 = 16-bit). 
    # Print the ADC values. 
    print(values[1]) 
    # print analog data which detected via ADC AIN1 port.
    if values[1] < 3000: 
        os.system("gpio mode 29 out")
        os.system("gpio write 29 1") 
    else: 
        os.system("gpio mode 29 in") 
    os.system("gpio write 29 0") 
    time.sleep(0.25)</span>

    运行看看效果

    python sensor.py

    嗯,就这样吧,开开脑洞就可以玩儿得更愉快。。哈哈, 白了个白~