Linux GPIO 通过sysfs 方式进行操控, 进入到 /sys/class/gpio 目录下,可以看见有 export 和 unexport 两个文件,gpio对象的创建和销毁就是围绕这两个文件展开。
export:用于将指定编号的GPIO 导出,导出成功之后才能使用它。
unexport:当使用完GPIO 之后,我们需要将导出的引脚删除。
将指定的编号写入到export 文件中,可以导出指定编号的GPIO 引脚,导出成功之后会在 /sys/class/gpio/ 目录下生成对应的gpioX(X 表示GPIO 的编号)。进入 /sys/class/gpio/gpioX(X 表示GPIO 的编号) 目录,有 direction、 value、 active_low 和 edge 文件。 对于 gpio 输入输出功能只需要关注 direction、 value 和 active_low 这三个文件就可以。
direction:配置 GPIO 输入输出模式。该文件可读写,读表示查看GPIO 当前是输入还是输出模式,写表示将GPIO 配置为输入或输出模式。
value:在GPIO 配置为输出模式下,向value 文件写入"0"控制GPIO 引脚输出低电平,写入"1"则控制GPIO 引脚输出高电平。在输入模式下,读取value 文件获取GPIO 引脚当前的输入电平状态。
active_low:这个属性文件用于控制极性,可读可写。
如果要配置中断模式,需要设置 direction 为输入模式,再配置 edge 中断模式。
edge:控制中断的触发模式:
关闭中断模式:echo “none” > edge
上升沿触发:echo “rising” > edge
下降沿触发:echo “falling” > edge
边沿触发:echo “both” > edge
R2 开发板 GPIO 排针对应用户空间的gpio编号如下表。
| GPIO | GPIO编号 |
|---|---|
| GPIO4_C4 | 148 |
| GPIO1_D5 | 61 |
| GPIO0_A0 | 0 |
| GPIO3_A2 | 98 |
| GPIO2_D7 | 95 |
| GPIO2_B5 | 77 |
| GPIO2_B3 | 75 |
| GPIO3_B6 | 110 |
cd /sys/class/gpio/
# 将文件用于者改为youyeetoo
sudo chown youyeetoo:youyeetoo export
sudo chown youyeetoo:youyeetoo unexport
# 以GPIO4_C4为例操作gpio输出,其他gpio操作只需要根据上一个表格的对应关系导出即可。
# 导出 GPIO4_C4
echo 148 > export
# 将文件用于者改为youyeetoo
sudo chown youyeetoo:youyeetoo gpio148/direction
sudo chown youyeetoo:youyeetoo gpio148/value
cd gpio148
# 设置输出
echo out > direction
# 输出高电平
echo 1 > value
# 输出低电平
echo 0 > value
# 删除导出gpio引脚
cd ..
echo 148 > unexport
vim gpio_output.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
static char gpio_path[32];
static int gpio_config(const char *attr, const char *val)
{
char file_path[64];
int len;
int fd;
sprintf(file_path, "%s/%s", gpio_path, attr);
fd = open(file_path, O_WRONLY);
if (fd < 0) {
perror("open error");
return fd;
}
len = strlen(val);
if (len != write(fd, val, len)) {
perror("write error");
close(fd);
return -1;
}
close(fd); //关闭文件
return 0;
}
int main(int argc, char *argv[])
{
int fd;
int len;
/* 校验传参 */
if (argc != 3) {
fprintf(stderr, "usage: %s <gpio> <value>\n", argv[0]);
return -1;
}
/* 判断指定编号的GPIO是否导出 */
sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]);
if (access(gpio_path, F_OK)) {//如果目录不存在 则需要导出
fd = open("/sys/class/gpio/export", O_WRONLY);
if (fd < 0) {
perror("open error");
return -1;
}
len = strlen(argv[1]);
if (len != write(fd, argv[1], len)) {//导出gpio
perror("write error");
close(fd);
return -1;
}
close(fd); //关闭文件
}
/* 配置为输出模式 */
if (gpio_config("direction", "out")) return -1;
/* 极性设置 */
if (gpio_config("active_low", "0")) return -1;
/* 控制GPIO输出高低电平 */
if (gpio_config("value", argv[2])) return -1;
close(fd);
/* 退出程序 */
return 0;
}
gcc gpio_output.c -o gpio_output
# 输出高电平
sudo ./gpio_output 148 1
# 输出低电平
sudo ./gpio_output 148 0
cd /sys/class/gpio/
# 将文件用于者改为youyeetoo
sudo chown youyeetoo:youyeetoo export
sudo chown youyeetoo:youyeetoo unexport
# 以GPIO0_B0为例操作gpio输出,其他gpio操作只需要根据上一个表格的对应关系导出即可。
# 导出 GPIO4_C4
echo 148 > export
# 将文件用于者改为youyeetoo
sudo chown youyeetoo:youyeetoo gpio148/direction
sudo chown youyeetoo:youyeetoo gpio148/value
cd gpio148
# 设置输入
echo in > direction
# 查看gpio电平
cat value
# 删除导出gpio引脚
cd ..
echo 8 > unexport
vim gpio_input.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
static char gpio_path[32];
static int gpio_config(const char *attr, const char *val)
{
char file_path[64];
int len;
int fd;
sprintf(file_path, "%s/%s", gpio_path, attr);
if (0 > (fd = open(file_path, O_WRONLY))) {
perror("open error");
return fd;
}
len = strlen(val);
if (len != write(fd, val, len)) {
perror("write error");
close(fd);
return -1;
}
close(fd); //关闭文件
return 0;
}
int main(int argc, char *argv[])
{
char file_path[64];
char val;
int fd;
int len;
/* 校验传参 */
if (argc != 2) {
fprintf(stderr, "usage: %s <gpio>\n", argv[0]);
return -1;
}
/* 判断指定编号的GPIO是否导出 */
sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]);
if (access(gpio_path, F_OK)) {//如果目录不存在 则需要导出
if ((fd = open("/sys/class/gpio/export", O_WRONLY)) < 0) {
perror("open error");
return -1;
}
len = strlen(argv[1]);
if (len != write(fd, argv[1], len)) {//导出gpio
perror("write error");
close(fd);
return -1;
}
close(fd); //关闭文件
}
/* 配置为输入模式 */
if (gpio_config("direction", "in")) return -1;
/* 极性设置 */
if (gpio_config("active_low", "0")) return -1;
/* 配置为非中断方式 */
if (gpio_config("edge", "none")) return -1;
/* 读取GPIO电平状态 */
sprintf(file_path, "%s/%s", gpio_path, "value");
if ((fd = open(file_path, O_RDONLY)) < 0) {
perror("open error");
return -1;
}
if (read(fd, &val, 1) < 0) {
perror("read error");
close(fd);
return -1;
}
printf("value: %c\n", val);
/* 退出程序 */
close(fd);
return 0;
}
编译运行,读取gpio管脚电平
gcc gpio_input.c -o gpio_input
sudo ./gpio_input 148
vim gpio_irq.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <poll.h>
static char gpio_path[32];
static int gpio_config(const char *attr, const char *val)
{
char file_path[64];
int len;
int fd;
sprintf(file_path, "%s/%s", gpio_path, attr);
fd = open(file_path, O_WRONLY);
if (fd < 0) {
perror("open error");
return fd;
}
len = strlen(val);
if (len != write(fd, val, len)) {
perror("write error");
return -1;
}
close(fd); //关闭文件
return 0;
}
int main(int argc, char *argv[])
{
struct pollfd pfd;
char file_path[64];
int ret;
char val;
int len;
int fd;
/* 校验传参 */
if (argc != 2) {
fprintf(stderr, "usage: %s <gpio>\n", argv[0]);
return -1;
}
/* 判断指定编号的GPIO是否导出 */
sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]);
if (access(gpio_path, F_OK)) {//如果目录不存在 则需要导出
fd = open("/sys/class/gpio/export", O_WRONLY);
if (fd < 0) {
perror("open error");
return -1;
}
len = strlen(argv[1]);
if (len != write(fd, argv[1], len)) {//导出gpio
perror("write error");
return -1;
}
close(fd); //关闭文件
}
/* 配置为输入模式 */
if (gpio_config("direction", "in")) return -1;
/* 极性设置 */
if (gpio_config("active_low", "0")) return -1;
/* 配置中断触发方式: 上升沿和下降沿 */
if (gpio_config("edge", "both")) return -1;
/* 打开value属性文件 */
sprintf(file_path, "%s/%s", gpio_path, "value");
pfd.fd = open(file_path, O_RDONLY);
if (pfd.fd < 0) {
perror("open error");
return -1;
}
/* 调用poll */
pfd.events = POLLPRI; //只关心高优先级数据可读(中断)
read(pfd.fd, &val, 1);//先读取一次清除状态
while (1) {
ret = poll(&pfd, 1, -1); //调用poll
if (ret < 0) {
perror("poll error");
return -1;
} else if (ret == 0) {
fprintf(stderr, "poll timeout.\n");
continue;
} else {
/* 校验高优先级数据是否可读 */
if(pfd.revents & POLLPRI) {
if (lseek(pfd.fd, 0, SEEK_SET) < 0) {//将读位置移动到头部
perror("lseek error");
return -1;
}
if (read(pfd.fd, &val, 1) < 0) {
perror("read error");
return -1;
}
printf("GPIO Interrupt Trigger <value=%c>\n", val);
}
}
}
/* 退出程序 */
return 0;
}
编译运行
gcc gpio_irq.c -o gpio_irq
sudo ./gpio_irq 148
串口通信是一种重要的设备通信方式,它通过串行方式在设备之间传输数据。在Linux系统中,串口设备被抽象为特殊的文件,位于/dev目录下,常见的串口设备文件如 /dev/ttyS0(传统串口) 或 /dev/ttyUSB0(USB转串口设备)。在串口通信中需要配置以下核心参数。
串口通信的核心参数包括:
波特率(Baud Rate):数据传输速率,常见的有9600、19200、38400、57600、115200等
数据位(Data Bits):每个数据帧中实际包含的数据位数,通常为5-8位
停止位(Stop Bits):用于表示数据帧结束的位数,通常为1或2位
校验位(Parity Bit):用于简单的错误检测,可以是无校验(None)、奇校验(Odd)或偶校验(Even)
流控制(Flow Control):用于控制数据传输速率,可以是无流控制(None)、软件流控制(XON/XOFF)或硬件流控制(RTS/CTS)
R2 开发板一共提供3个uart供客户使用,分别注册为 /dev/ttyS2、/dev/ttyS5、/dev/ttyS6,对应硬件位置请查看原理图。串口使用可以使用shell方式或者编程语言读写配置方式。本文只介绍shell方式和c语言方式。
sudo stty -F /dev/ttyS2 -a
板子的TX接到串口模块的RX上,板子的RX接到串口模块的TX上,再接板子的GND到串口模块的GND上。然后将串口模块连接到电脑上。
在电脑上打开串口软件,在串口软件配置默认的9600即可完成通信。
在开发板上输入以下命令接受发送过来的串口数据
sudo chmod 666 /dev/ttyS2
cat /dev/ttyS2
sudo chmod 666 /dev/ttyS2
echo "Serial information to be sent" > /dev/ttyS2
# 设置/dev/ttyS1串口波特率为115200
sudo stty -F /dev/ttyS2 speed 115200
# 设置数据位,如设置8位数据位参数为cs8,7位数据位为cs7
sudo stty -F /dev/ttyS2 cs8
# 停止位用于标识一个字符传输的结束。通常使用 1 或 2 个停止位。1位停止位参数为cstopb,2位停止位为-cstopb
sudo stty -F /dev/ttyS2 cstopb
# 设置奇校验:
sudo stty -F /dev/ttyS2 parenb parodd
# 设置为偶校验:
sudo stty -F /dev/ttyS2 parenb -parodd
# 关闭校验:
sudo stty -F /dev/ttyS2 -parenb
Linux环境下进行C语言串口编程通常包括以下基本步骤:
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <termios.h>
#include <unistd.h>
int configure_serial_port(const char *device, int baud_rate)
{
int serial_port;
serial_port = open(device, O_RDWR);
if (serial_port < 0) {
printf("Error %i opening %s: %s\n", errno, device, strerror(errno));
return -1;
}
struct termios tty;
memset(&tty, 0, sizeof tty);
if (tcgetattr(serial_port, &tty) != 0) {
printf("Error %i from tcgetattr: %s\n", errno, strerror(errno));
close(serial_port);
return -1;
}
// 设置波特率
cfsetospeed(&tty, baud_rate);
cfsetispeed(&tty, baud_rate);
// 设置8N1模式:8位数据位,无校验位,1位停止位
tty.c_cflag &= ~PARENB; // 无校验位
tty.c_cflag &= ~CSTOPB; // 1位停止位
tty.c_cflag &= ~CSIZE; // 清除数据位设置
tty.c_cflag |= CS8; // 8位数据位
// 禁用硬件流控制
tty.c_cflag &= ~CRTSCTS;
// 启用接收,忽略调制解调器控制线
tty.c_cflag |= CREAD | CLOCAL;
// 禁用软件流控制
tty.c_iflag &= ~(IXON | IXOFF | IXANY);
// 原始输入模式
tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
// 原始输出模式
tty.c_oflag &= ~OPOST;
// 设置超时和最小读取字符数
tty.c_cc[VMIN] = 1; // 至少读取1个字符
tty.c_cc[VTIME] = 1; // 等待字符的超时时间为0.1秒
// 保存设置
if (tcsetattr(serial_port, TCSANOW, &tty) != 0) {
printf("Error %i from tcsetattr: %s\n", errno, strerror(errno));
close(serial_port);
return -1;
}
return serial_port;
}
int main()
{
const char *device = "/dev/ttyS1";
int baud_rate = B9600;
// 配置串口
int serial_port = configure_serial_port(device, baud_rate);
if (serial_port < 0) {
return 1;
}
// 写入数据
const char *message = "Hello, Serial Port!";
ssize_t bytes_written = write(serial_port, message, strlen(message));
if (bytes_written < 0) {
printf("Error writing to serial port: %s\n", strerror(errno));
close(serial_port);
return 1;
}
printf("Written %zd bytes: %s\n", bytes_written, message);
// 读取数据
unsigned char buffer[256];
ssize_t bytes_read = read(serial_port, buffer, sizeof(buffer) - 1);
if (bytes_read < 0) {
printf("Error reading from serial port: %s\n", strerror(errno));
close(serial_port);
return 1;
}
// 确保字符串以null结尾
buffer[bytes_read] = '\0';
printf("Read %zd bytes: %s\n", bytes_read, buffer);
// 关闭串口
close(serial_port);
return 0;
}
c程序编译运行,参考shell方式连接好串口。
sudo chmod 666 /dev/ttyS2
gcc serial.c -o serial
sudo ./serial
R2 开发板提供1组 i2c7 供客户使用。
Linux 环境下,有多种工具可以帮助用户与 I2C 设备进行交互。其中最常用的工具是 i2c-tools 包中提供的命令行工具,包括 i2cdetect、i2cset 和 i2cget 等。
sudo apt-get install i2c-tools
sudo i2cdetect -y 6
上图中地址为0x51有一个设备,该设备为 hym8563即RTC。本文以RTC为例讲解i2c读写。
读取命令格式
sudo i2cget -y -f i2c总线号 从机地址 寄存器
| 位号 | 符号 | 描述 |
|---|---|---|
| 7 | C | 世纪位:C=0指定世纪数为20XX;C=1指定世纪数为19XX,“XX”为年寄存器中的值 |
| 6,5 | - | 无效 |
| 4~0 | 月 | 代表BCD格式的当前月份数值,值为01~12 |
| 位号 | 符号 | 描述 |
|---|---|---|
| 7~0 | 年 | 代表BCD格式的当前年数值,值为00~99 |
读取月/世纪寄存器和年寄存器命令如下
# 读取月/世纪寄存器
sudo i2cget -y -f 6 0x51 0x07
# 读取年寄存器
sudo i2cget -y -f 6 0x51 0x08
# 查看RTC时间
sudo hwclock -r
读出月/世纪寄存器值为0x41,换算成二进制为01000001,得出C=0,即世纪为20,月份为1月,年寄存器寄存器值为0x26,换算成BCD码格式为26。即读出的年月为2026-01,与hwclock命令读出的年月一致。
sudo i2cset -y -f i2c总线号 从机地址 寄存器 值
如设置RTC时钟年月为2027-02,命令如下
# 设置月/世纪寄存器
sudo i2cset -y -f 6 0x51 0x07 0x42
# 设置年寄存器
sudo i2cset -y -f 6 0x51 0x08 0x27
# 查看RTC时间
sudo hwclock -r
本c程序以RTC为例进行i2c读写。相关从机地址和寄存器介绍看shell方式。
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/i2c-dev.h>
#include <linux/i2c.h>
/* RTC所对应的I2C控制器的设备节点 */
#define RTC_DEVICE "/dev/i2c-6"
/* RTC的I2C设备地址 */
#define RTC_ADDR 0x51
int i2c_write(int fd, unsigned char dev_addr, unsigned char reg_addr, unsigned char* data_buf, int len)
{
int ret;
unsigned char msg_buf[9];
struct i2c_rdwr_ioctl_data data;
struct i2c_msg messages;
msg_buf[0] = reg_addr;
if (len < 9) {
memcpy((void *) &msg_buf[1], data_buf, len);
} else {
printf("This function supports up to 8 bytes at a time !!!\n");
return -1;
}
messages.addr = dev_addr;
messages.flags = 0;
messages.len = len + 1;
messages.buf = msg_buf;
data.msgs = &messages;
data.nmsgs = 1;
if(ioctl(fd, I2C_RDWR, &data) < 0) {
printf("I2C_RDWR err \n");
return -1;
}
sleep(1);
return 0;
}
int i2c_read(int fd, unsigned char dev_addr, unsigned char reg_addr, unsigned char* data_buf, int len)
{
int ret;
struct i2c_rdwr_ioctl_data data;
struct i2c_msg messages[2];
messages[0].addr = dev_addr;
messages[0].flags = 0;
messages[0].len = 1;
messages[0].buf = ®_addr;
messages[1].addr = dev_addr;
messages[1].flags = I2C_M_RD;
messages[1].len = len;
messages[1].buf = data_buf;
data.msgs = messages;
data.nmsgs = 2;
if(ioctl(fd, I2C_RDWR, &data) < 0) {
printf("I2C_RDWR err \n");
return -1;
}
sleep(1);
return 0;
}
int main()
{
int fd;
unsigned char reg=0x07;
unsigned char rd_buf[2] = {0};
unsigned char wr_buf[2] = {0x42, 0x27};
fd = open(RTC_DEVICE, O_RDWR);
if (fd < 0) {
printf("open"RTC_DEVICE"failed \n");
return -1;
}
i2c_read(fd, RTC_ADDR, reg, rd_buf, 2);
printf("read reg(0x07): 0x%02x\n", rd_buf[0]);
printf("read reg(0x08): 0x%02x\n", rd_buf[1]);
i2c_write(fd, RTC_ADDR, reg, wr_buf, 2);
i2c_read(fd, RTC_ADDR, reg, rd_buf, 2);
printf("write reg(0x07): 0x%02x\n", rd_buf[0]);
printf("write reg(0x08): 0x%02x\n", rd_buf[1]);
close(fd);
return 0;
}
c程序编译运行。
gcc i2c_test.c -o i2c_test
sudo ./i2c_test
R2 开发板提供一组 CAN 供客户使用,需注意这组 CAN 管脚是直接从rk3576芯片引出来,即can1—tx和can1-rx,如果需要与外接CAN设备通信,需要客户自行画CAN驱动电路。
sudo apt-get install can-utils
ifconfig -a
sudo ifconfig can0 down
sudo ip link set can0 type can bitrate 500000 triple-sampling on
# 如果没有can设备进行测试,可以使用以下命令设置can为回环测试。
sudo ip link set can0 type can loopback on
sudo ifconfig can0 up
sudo ip -details link show can0
sudo cansend can0 123#000102030405060708
sudo candump can0
can通信流程
/* 1. 报文发送程序 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
int main()
{
int s, nbytes;
struct sockaddr_can addr;
struct ifreq ifr;
struct can_frame frame[2] = {{0}};
s = socket(PF_CAN, SOCK_RAW, CAN_RAW);//创建套接字
strcpy(ifr.ifr_name, "can0" );
ioctl(s, SIOCGIFINDEX, &ifr); //指定 can0 设备
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
bind(s, (struct sockaddr *)&addr, sizeof(addr));//将套接字与 can0 绑定
//禁用过滤规则,本进程不接收报文,只负责发送
setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);
//生成两个报文
frame[0].can_id = 0x11;
frame[0]. can_dlc = 1;
frame[0].data[0] = 'Y';
frame[1].can_id = 0x22;
frame[1]. can_dlc = 1;
frame[1].data[0] = 'N';
//循环发送两个报文
while(1)
{
nbytes = write(s, &frame[0], sizeof(frame[0])); //发送 frame[0]
if(nbytes != sizeof(frame[0]))
{
printf("Send Error frame[0]\n!");
break; //发送错误,退出
}
sleep(1);
nbytes = write(s, &frame[1], sizeof(frame[1])); //发送 frame[1]
if(nbytes != sizeof(frame[0]))
{
printf("Send Error frame[1]\n!");
break;
}
sleep(1);
}
close(s);
return 0;
}
CAN接收程序示例
/* 2. 报文过滤接收程序 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
int main()
{
int s, nbytes;
struct sockaddr_can addr;
struct ifreq ifr;
struct can_frame frame;
struct can_filter rfilter[1];
s = socket(PF_CAN, SOCK_RAW, CAN_RAW); //创建套接字
strcpy(ifr.ifr_name, "can0" );
ioctl(s, SIOCGIFINDEX, &ifr); //指定 can0 设备
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
bind(s, (struct sockaddr *)&addr, sizeof(addr)); //将套接字与 can0 绑定
//定义接收规则,只接收表示符等于 0x11 的报文
rfilter[0].can_id = 0x11;
rfilter[0].can_mask = CAN_SFF_MASK;
//设置过滤规则
setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));
while(1)
{
nbytes = read(s, &frame, sizeof(frame)); //接收报文
//显示报文
if(nbytes > 0)
{
printf(“ID=0x%X DLC=%d data[0]=0x%X\n”, frame.can_id,
frame.can_dlc, frame.data[0]);
}
}
close(s);
return 0;
}