Serial Bus and Control Interfaces
1.UART
1.1. UART 概述
1.1.1 串口资源介绍
EASY EAI Nano-TB 开发板的串口资源分为两类:一类是被特定功能占用的**【不可用串口】,另一类是用户可自由使用的【可用串口】**。
【不能直接使用的串口】分布如下。
| 串口编号 | 设备节点 | 说明 |
|---|---|---|
| 串口0 (Port 0) | /dev/ttyFIQ0 | 被调试功能占用,不能作为普通串口使用。 |
| 串口1 (Port 1) | 无 | 相关引脚已复用为其他功能。 |
| 串口3 | 无 | 相关引脚已复用为其他功能。 |
硬件上的位置如下图所示。

串口资源介绍
【可用串口】分布如下。
| 串口编号 | 设备节点 | 说明 |
|---|---|---|
| 串口2 (Port 2) | /dev/ttyS2 | TTL 电平 |
| 串口6 (Port 6) | /dev/ttyS6 | TTL 电平 |
| 串口7 (Port 7) | /dev/ttyS7 | TTL 电平 |
硬件上的位置如下图所示。

串口资源介绍
1.1.2 硬件接线
一般接线(设备间通信):

硬件接线
- 本示示例测试接线: 使用跳帽将 Rxd 引脚与 Txd 引脚短接,进行环回(自发自收)测试。

硬件接线
1.2. 快速开始
1.2.1 准备开发环境
如果是第一次阅读本文档,请阅读“入门指南/开发环境准备/Easy-Eai 编译环境的准备与更新”,并按照相关步骤搭建编译环境。
在 PC 端 Ubuntu 系统中运行 run 脚本,进入 EASY-EAI 编译环境。详细如下。
cd ~/develop_environment./run.sh 22041.2.2 下载源代码并编译示示例
首先,在虚拟机后台终端中执行以下命令,创建外设示示例源代码的管理目录:
cd /optmkdir -p EASY-EAI-Nano-TB/demo下载示示例程序:
示例如,将示示例程序下载到“PC\D:”(没有固定要求,用户任意位置均可)。
然后,将下载的示示例复制到虚拟机文件系统中。步骤请参考下图。

下载源代码并编译示示例
最后,进入对应示示例目录并执行编译操作。具体命令如下:
cd EASY-EAI-Nano-TB/demo/06_UART./build.sh💡 注意 : 由于依赖库位于开发板上,交叉编译期间需要保持 /mnt 挂载。

编译成功后,会在 Release 目录中生成两个可执行程序,并自动放置到开发板的 /userdata/ 目录中,分别是发送端示示例 test-Send 和接收端示示例 test-Recv。
1.2.3 运行示示例
通过串口调试或 SSH 进入开发板后台,并切换到示示例所在目录:
cd /userdata
运行示示例
首先,执行以下命令将**【接收端】示示例【在后台运行】**:
sudo ./test-Recv /dev/ttyS2 &运行结果如下。此时接收端会进入等待发送端数据的状态。

运行示示例
接着,执行以下命令运行【发送端】示示例:
sudo ./test-Send /dev/ttyS2
运行示示例
1.3. C 语言使用示示例
这是串口的 C 语言使用示示例。接收端代码路径为 06_UART/test-uart/Recv.c,可作为编码参考。以下代码展示了串口接收端的操作流程:
int main(int argc, char **argv){ if(2 != argc){ printf("Usage:\n"); printf(" sudo %s %s\n", argv[0], "/dev/ttyS<2/6/7>"); return -1; }
int fd = UART_Open(argv[1]); if(fd < 0){ printf("\033[33m【Open ERROR!】%s\n", DEBUG_COLOR_TAIL); return -1; }
if(false == UART_Set(fd, 115200, 0, 8, 1, 'N')){ printf("\033[33m【Init ERROR!】%s\n", DEBUG_COLOR_TAIL); return -1; }
const char *strReceiver = "I am uart Receiver"; printf("\033[36m【Init OK \"%s\"】%s\n", strReceiver, DEBUG_COLOR_TAIL);
char recvBuf[128]={0}; while(1){ if(UART_Recv(fd, recvBuf, sizeof(recvBuf)) <= 0){ continue; }else{ printf("\033[36m【Recv Msg from Sender】:%s", DEBUG_COLOR_TAIL); printf(" %s\n", recvBuf); break; } } UART_Close(fd);
printf("\033[42m【Recv date OK. BYE BYE!】%s\n", DEBUG_COLOR_TAIL);
return 0;}发送端代码路径为 06_UART/test-uart/Send.c,可作为编码参考。以下代码展示了串口发送端的操作流程:
int main(int argc, char **argv){ if(2 != argc){ printf("Usage:\n"); printf(" sudo %s %s\n", argv[0], "/dev/ttyS<2/6/7>"); return -1; }
int fd = UART_Open(argv[1]); if(fd < 0){ printf("\033[33m【Open ERROR!】%s\n", DEBUG_COLOR_TAIL); return -1; }
if(false == UART_Set(fd, 115200, 0, 8, 1, 'N')){ printf("\033[33m【Init ERROR!】%s\n", DEBUG_COLOR_TAIL); return -1; }
char *strSender = "I am uart Sender"; printf("\033[36m【Init OK \"%s\"】%s\n", strSender, DEBUG_COLOR_TAIL);
int len = UART_Send(fd, strSender, strlen(strSender)); if(len <= 0){ printf("\033[41m【Send data ERROR!】%s\n", DEBUG_COLOR_TAIL); return -1; } UART_Close(fd);
printf("\033[42m【Send date OK. BYE BYE!】%s\n", DEBUG_COLOR_TAIL);
return 0;}代码中的 UART_Open()、UART_Set()、UART_Send()、UART_Recv() 是对系统调用进行易用封装后的包装函数。具体实现在 06_UART/commonApi/uart.c 中。
2.SPI
2.1. SPI 概述
SPI 是 Serial Peripheral Interface(串行外设接口)的缩写,是 Motorola 开发的一种同步串行接口技术,可作为高速、全双工、同步通信总线使用。在用户空间应用中,无需关心 SPI 协议的具体细节,只需使用驱动层提供的 SPI 外设操作接口函数,就可以像操作 Linux 中其他普通设备文件一样轻松操作 SPI 外设。
EASY EAI Nano-TB 的 SPI 接口位置如下图所示。

SPI 概述
2.1.1 SPI 参数设置说明
-
设备文件格式: /dev/spidev(bus.select)
-
bus: 显示 SPI 总线编号(即一组 SCLK、MOSI、MISO)。
-
select: 显示 SPI 设备编号。同一总线上通过不同片选信号(CSN0、CSN1 等)进行区分。
以 Orin-Nano 的默认 SPI 资源为示例:启用 SPI 功能后,会出现以下 4 个设备节点(即存在 2 条总线和 4 个设备)。
-
/dev/spidev0.0
-
/dev/spidev0.1
-
/dev/spidev1.0
-
/dev/spidev1.1
SPI 通信有 4 种不同模式。从设备在出厂时模式已固定,无法修改。但通信双方必须工作在相同模式下,因此需要在主设备侧设置 SPI 模式,并通过 CPOL(时钟极性)和 CPHA(时钟相位)控制主设备通信模式。
| 模式 | CPOL | CPHA |
|---|---|---|
| Mode 0 | 0 | 0 |
| Mode 1 | 0 | 1 |
| Mode 2 | 1 | 0 |
| Mode 3 | 1 | 1 |
-
时钟极性 (CPOL) 用于设置 SCLK 的有效电平。
-
时钟相位 (CPHA) 用于设置数据采样发生在哪个边沿。
-
CPOL=0 显示 SCLK=0 时为空闲状态,SCLK 为 High 电平时有效。
-
CPOL=1 显示 SCLK=1 时为空闲状态,SCLK 为 Low 电平时有效。
-
CPHA=0 显示数据在第 1 个边沿采样,在第 2 个边沿发送。
-
CPHA=1 显示数据在第 2 个边沿采样,在第 1 个边沿发送。
【*】 SPI 主模块与通信外设之间,双方的时钟相位和极性必须一致。
其他参数:
-
speed: 通信比特率
-
delay: 通信延迟时间设置
-
bits: 通信占用的位数
2.1.2 硬件连接
本示示例使用 RFID 读卡模块 RC522 进行辅助演示。
RC522 模块与 EASY EAI Nano-TB 的接线原理图如下。

硬件连接
2.2. 快速开始
2.2.1 准备开发环境
如果是第一次阅读本文档,请阅读“入门指南/开发环境准备/Easy-Eai 编译环境的准备与更新”,并按照相关步骤搭建编译环境。
在 PC 端 Ubuntu 系统中运行 run 脚本,进入 EASY-EAI 编译环境。详细如下。
cd ~/develop_environment./run.sh 22042.2.2 下载源代码并编译示示例
首先,在虚拟机后台终端中执行以下命令,创建外设示示例源代码的管理目录:
cd /optmkdir -p EASY-EAI-Nano-TB/demo下载示示例程序:
示例如,将示示例程序下载到“PC\D:”(没有固定要求,用户任意位置均可)。
然后,将下载的示示例复制到虚拟机文件系统中。步骤请参考下图。

下载源代码并编译示示例
最后,进入对应示示例目录并执行编译操作。具体命令如下:
cd EASY-EAI-Nano-TB/demo/07_SPI./build.sh💡 注意 : 由于依赖库位于开发板上,交叉编译期间需要保持 /mnt 挂载。

编译成功后,会根据源代码生成 test-rfid、test-fram、test-spidev 三个示示例程序,并自动放置到开发板的 /userdata/ 目录中。
本文档作为辅助示示例使用的是 test-rfid。其他示示例用于其他应用场景,此处代码仅供参考。
2.2.3 运行示示例
通过串口调试或 SSH 进入开发板后台,并切换到示示例所在目录:
cd /userdata
运行示示例
执行以下命令启动示示例:
sudo ./test-rfid
运行示示例
运行结果如图所示(请参考原文档中的图)。
关于 API 的详细说明以及 API 调用(本示示例源代码),请参考以下说明。
2.3. RFID ID 读取示示例
RFID 示示例源代码位于以下路径:
-
07_SPI/rfid.c
-
07_SPI/dev/rc522.c
-
07_SPI/include/rc522.h
本文使用 RC522 芯片进行实现和说明。操作流程如下。

RFID ID 读取示示例
参考示示例代码如下。
static unsigned char flag = 0; static unsigned char bits = 8; static unsigned int speed = 100000; static uint16_t delay = 0; unsigned char card_rev_buf[16] = 0;
/* * 扇区密码:A,扇区数:16 * 每个扇区的密码字节数:16Byte */unsigned char sector_key_a[16][16];unsigned char data_buf[16] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10};
int main (int argc, char **argv) { memset(data_buf, 0x00, sizeof data_buf); int status = MI_ERR; int numAtempt = 1; int fd = spi_init(dev_spi_bus, dev_spi_select, mode, bits, speed, delay); rfid_init(dev_spi_bus, dev_spi_select, fd); flag = MI_GET_ID;
while(1) { while(rfid_request(PICC_REQIDL, &card_rev_buf[0]) != MI_OK && numAtempt-- >= 0) { usleep(500); } if(rfid_anticoll(&card_rev_buf[2]) == MI_OK) { status = rfid_select(&card_rev_buf[2]); if(status != MI_ERR) { if(flag == MI_GET_ID) { printf("Card ID:%02x%02x%02x%02x\n", card_rev_buf[2], card_rev_buf[3],card_rev_buf[4], card_rev_buf[5]); } else if (flag == MI_READ) { memset(sector_key_a, 0xff, 256); memset(data_buf, 0x00, sizeof data_buf); status = rfid_auth_state(PICC_AUTHENT1A, addr, sector_key_a[addr/4], &card_rev_buf[2]); if(status == MI_OK) { status = rfid_read(addr, data_buf); if(status == MI_OK) { print_buff(data_buf, 16); } } else { printf("Error reading"); close(fd); exit(1); } } else if (flag == MI_WRITE) { memset(sector_key_a, 0xff, 256); if(addr == 0 || addr % 4 == 3) { close(fd); exit(1); } status = rfid_auth_state(PICC_AUTHENT1A, addr, sector_key_a[addr/4], &card_rev_buf[2]); if(status == MI_OK) { status = rfid_write(addr, data_buf); if(status != MI_OK) { printf("rfid write failure!\n"); close(fd); exit(1); } } else { printf("Error writing"); close(fd); exit(1); } } else { printf("Not implemented\n"); } status = rfid_halt(); if(status != MI_OK) { //printf ("rfid halt failure! [ERROR %d]\n", status); } } else { // printf("None\n"); } } else { // printf("None\n"); } } spi_exit(dev_spi_bus , dev_spi_select); return 0;} /* ----- End of main() ----- */此外,使用 SPI 接口的 FRAM(铁电存储器)通信源代码位于以下路径:
-
07_SPI/fram.c
-
07_SPI/mb85rs64.c
-
07_SPI/mb85rs64.h
SPI 接口读写通信源代码位于以下路径:
- 07_SPI/spidev_test.c
2.4. 注意事项
RC522 主要用于辅助说明 SPI API 的使用方法。关于该模块的资料和详细使用方法,请参考以下网站:
www.xxx.com(※官方网站模块 URL)
3.I2C
3.1. I2C 概述
IIC(或 I2C)是一种采用多主多从架构的串行通信总线。其最初设计目的是在主板、嵌入式系统或手机中连接低速外设。它主要用于数据量较小的场景,具有传输距离短、任意时刻只能存在一个主机等特点。在 Linux 嵌入式应用开发场景中,无需关心 IIC 协议的具体细节,只需使用驱动层提供的 IIC 外设操作接口函数,就可以像操作 Linux 中其他普通设备文件一样轻松操作 IIC 外设。
3.1.1 开发板上的 I2C 资源介绍
EASY EAI Nano-TB 开发板预留引出了一路 IIC 资源(IIC_5),供用户自定义调用。准确位置如下图所示。

开发板上的 I2C 资源介绍
3.1.2 硬件接线图
本示示例使用 ADS1115 电压检测模块进行辅助演示。该模块的功能是将检测到的电压(模拟信号)转换为数字信号并存入寄存器,再通过 IIC 通信方式向外部提供电压信息。
ADS1115 模块与 EASY EAI Nano-TB 的接线原理图如下。

硬件接线图
3.2. 快速开始
3.2.1 准备开发环境
在 PC 端 Ubuntu 系统中运行 run 脚本,进入 EASY-EAI 编译环境。详细如下。
cd ~/develop_environment./run.sh 22043.2.2 下载源代码并编译示示例
首先,在虚拟机后台终端中执行以下命令,创建外设示示例源代码的管理目录:
cd /optmkdir -p EASY-EAI-Nano-TB/demo下载示示例程序:
示例如,将示示例程序下载到“PC\D:”(没有固定要求,用户任意位置均可)。
最后,进入对应示示例目录并执行编译操作。具体命令如下:

下载源代码并编译示示例
cd EASY-EAI-Nano-TB/demo/08_IIC./build.sh💡 注意 :由于依赖库位于开发板上,交叉编译期间需要保持 /mnt 挂载。

下载源代码并编译示示例
编译成功后,会生成名为 test-ads1115 的可执行程序,并自动放置到开发板的 /userdata/ 目录中。
3.2.3 运行示示例
通过串口调试或 SSH 进入开发板后台,并切换到示示例所在目录:
cd /userdata
运行示示例
执行以下命令启动示示例:
sudo ./test-ads1115运行结果如下。使用探针分别接触 3V3、1V8、GND 三个端子时,终端会测量并显示对应的电压值。

运行示示例
3.3. C 语言使用示示例
这是 ADS1115 的 C 语言使用示示例。代码路径为 08_IIC/test-ads1115/main.c 可作为编码参考。以下代码展示了 ADS1115 的操作流程:
int32_t ads1115_config_register(uint32_t fd, uint8_t configH, uint8_t configL){ uint8_t reg_data[3] = {ADS1015_REG_POINTER_CONFIG, configH, configL}; return iic_write(fd, ADS1115_ADDRESS, reg_data, sizeof(reg_data));}
int16_t ads1115_read_data(uint32_t fd){ bool ret = false; /* 读取数据 */ uint8_t tx_data[1] = {ADS1015_REG_POINTER_CONVERT}; if(iic_write(fd, ADS1115_ADDRESS, tx_data, sizeof(tx_data)) < sizeof(tx_data)){ printf("iic write faild !\n"); return -1; }
uint8_t rx_data[3]={0}; if(iic_read(fd, ADS1115_ADDRESS, rx_data, 2) < 0){ printf("iic read faild !\n"); return -1; }
int16_t data = rx_data[0]*256+rx_data[1]; return data;}
double ads1115_get_voltage_val(uint32_t fd, uint8_t configH, uint8_t configL){ /* 设置寄存器 */ if(ads1115_config_register(fd, configH, configL) < 0){ printf("ads1115 config register faild\n"); return 0.0; } usleep(100 * 1000);
int16_t ad_val = ads1115_read_data(fd); if((0x7FFF == ad_val)|(0X8000 == ad_val)) { // 确认是否超出量程 ad_val = 0; printf("ads1115 over PGA\r\n"); }
double val = 0.0; switch((0x0E&configH)>>1) // 与量程对应的分辨率 { case(0x00): val = (double)ad_val*187.5/1000000.0; break; case(0x01): val = (double)ad_val*125/1000000.0; break; case(0x02): val = (double)ad_val*62.5/1000000.0; break; case(0x03): val = (double)ad_val*31.25/1000000.0; break; case(0x04): val = (double)ad_val*15.625/1000000.0; break; case(0x05): val = (double)ad_val*7.8125/1000000.0; break; default: val = 0.0; break; }
return val;}
int main(int argc, char const *argv[]){ bool ret = false; double val; int fd = iic_init("/dev/i2c-2"); if(fd < 0){ printf("iic init faild \n"); return -1; }
if(0 != iic_set_addr_len(fd, 7)){ return -1; }
if(0 != iic_set_addr(fd, ADS1115_ADDRESS)){ return -1; }
while (1) { val = ads1115_get_voltage_val(fd, CONFIG_REG_H, CONFIG_REG_L); printf("val: %f V\r\n",val); sleep(2); }
iic_release(fd); return 0;}代码中的 iic_init()、iic_set_addr_len()、iic_set_addr()、iic_read()、iic_write()、iic_release() 是对系统调用进行易用封装后的包装函数。具体实现在 08_IIC/commonApi/iic.c 中。
除了需要操作 IIC 硬件资源的接口外,还需要明确了解 IIC 总线上 IIC 从设备的寄存器操作方法。示例如,对于 ADS1115 电压检测芯片,与其寄存器相关的所有操作定义都在 08_IIC/test-ads1115/ads1115.h 中实现。
4.GPIO
4.1. GPIO 概述
4.1.1 硬件接线原理图
💡 注意 : GPIO 支持热插拔,但如果在底板未安装保护外壳的状态下插拔,容易触碰到底板上的元件,并可能因板附近的金属部件导致短路。因此,建议在完全断电状态下插拔外设。
GPIO 的输入输出电压为 3.3V。 请注意电平(电位)匹配,否则可能损坏芯片引脚或连接的设备。
4.1.2 GPIO 硬件资源分布介绍

GPIO 硬件资源分布介绍
-
gpiod 库: 需要使用上表中的【Chip 对象名】和【Line 偏移量】。
-
sysfs 访问方式: 需要使用上表中的【GPIO 系统节点路径】。
4.1.3 gpiod 概述
从 Linux 4.8 开始增加了 libgpiod 支持,传统的基于 sysfs 的访问方式将逐渐被弃用。因此,本文档的示示例主要采用 gpiod 方式控制 GPIO。gpiod 库通过操作 chip 对象和 line 对象,实现控制 GPIO 引脚输出电平或读取 GPIO 引脚电平的目的。
-
Chip 对象名: gpiod_chip_open_by_name 在调用其获取 chip 对象时作为参数使用。
-
Line 偏移量: gpiod_chip_get_line 在调用其获取 line 对象时作为参数使用。
以 GPIO5_C0 为示例,【引脚名】、【Chip 对象名】和【Line 偏移量】三者关系如下式所示。

gpiod 概述
4.1.4 sysfs 访问方式概述
通过 sysfs 控制 GPIO 的方式主要基于内核提供的 GPIO 控制接口文件。也就是说,通过读写 /sys/class/gpio 目录下的文件来控制对应的 GPIO 接口。
-
引脚编号(pin): 在 sysfs 访问方式中,所有操作都以引脚编号为基准。
-
GPIO 系统节点路径: 即具体 GPIO 引脚对应的节点路径。
【引脚名】与【GPIO 系统节点路径】的关系如下式所示。

sysfs 访问方式概述
请求导出引脚: 在使用某个引脚之前,需要手动向 GPIO 管理器请求导出对应引脚资源。
echo 176 \> /sys/class/gpio/export \## gpio_request 请求导出对应 GPIO设置对应引脚的工作模式(输入或输出)。
echo in > /sys/class/gpio/gpio176/direction ## gpio_direction_output 将对应 GPIO 设置为输入方向## 或者echo out > /sys/class/gpio/gpio176/direction ## gpio_direction_output 将对应 GPIO 设置为输出方向根据引脚工作模式进行对应控制(写入或读取电平)。
cat /sys/class/gpio/gpio176/value ## gpio_get_value 获取 GPIO 当前状态值## 或者echo 0 > /sys/class/gpio/gpio176/value ## gpio_set_value 设置输出为 Low 电平(低电位)echo 1 > /sys/class/gpio/gpio176/value ## gpio_set_value 设置输出为 High 电平(高电位)请求释放引脚: 引脚使用完成后,需要手动向 GPIO 管理器请求释放对应引脚资源。
echo 176 \> /sys/class/gpio/unexport \## gpio_free 释放已请求的 GPIO4.2. 快速开始
4.2.1 准备开发环境
在 PC 端 Ubuntu 系统中运行 run 脚本,进入 EASY-EAI 编译环境。详细如下。
cd ~/develop_environment./run.sh 22044.2.2 下载源代码并编译示示例
首先,在虚拟机后台终端中执行以下命令,创建外设示示例源代码的管理目录:
cd /optmkdir -p EASY-EAI-Nano-TB/demo下载示示例程序:
示例如,将示示例程序下载到“PC\D:”(没有固定要求,用户任意位置均可)。
然后,将下载的示示例复制到虚拟机文件系统中。步骤请参考下图。

下载源代码并编译示示例
最后,进入对应示示例目录并执行编译操作。具体命令如下:
cd EASY-EAI-Nano-TB/demo/09_GPIO./build.sh💡 注意 : 由于依赖库位于开发板上,交叉编译期间需要保持 /mnt 挂载。

下载源代码并编译示示例
编译成功后,相关示示例会生成到 Release 目录中,并自动放置到开发板的 /userdata/ 目录中。
4.2.3 运行示示例
通过串口调试或 SSH 进入开发板后台,并切换到示示例所在目录:
cd /userdata
运行示示例
执行以下命令启动示示例:
sudo ./test-gpio运行结果如下。

运行示示例
此外,将【GPIO5_C0】和【GPIO5_C1】用导线短接后,可以从【GPIO5_C1】引脚读取【GPIO5_C0】输出的【High 电平】。详细如下。

运行示示例
4.3. C 语言使用示示例
这是 GPIO 的 C 语言使用示示例。代码路径为 09_GPIO/test-gpio/main.c 可作为编码参考。以下代码展示了 GPIO 的操作流程:
#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
static const GPIOCfg_t gpioCfg_tab[] = { { .pinName = "GPIO5_C0", .direction = DIR_OUTPUT, .val = 0, }, { .pinName = "GPIO5_C1", .direction = DIR_INPUT, .val = 0,/* }, { .pinName = "GPIO5_C2", .direction = DIR_OUTPUT, .val = 0, }, { .pinName = "GPIO5_C6", .direction = DIR_INPUT, .val = 0,*/ }};
int main(int argc, char **argv){ gpio_init(gpioCfg_tab, ARRAY_SIZE(gpioCfg_tab));
pin_out_val("GPIO5_C0", 1);// pin_out_val("GPIO5_C2", 0);
int val = read_pin_val("GPIO5_C1"); printf("GPIO5_C1 val : %d\n", val);// val = read_pin_val("GPIO5_C6");// printf("GPIO5_C6 val : %d\n", val);
return 0;}代码中的 gpio_init()、pin_out_val()、read_pin_val() 是基于 libgpiod 进行易用封装后的包装函数。具体实现在 09_GPIO/commonApi/gpio.c 中。
如果用户需要像示示例一样引用 libgpiod,请注意以下两点。
-
需要包含头文件:#include <gpiod.h>
-
编译时需要将 -lgpiod 作为编译参数 添加进去。
5.PWM
5.1. PWM 概述
5.1.1 开发板的 PWM 资源

开发板的 PWM 资源
5.1.2 查找 PWM 节点
rv1126b 的 PWM 资源表如下:

查找 PWM 节点
-
【PWM1 CH0】对应 pwm1_4ch_0,寄存器地址为 20700000。
-
【PWM1 CH1】对应 pwm1_4ch_1,寄存器地址为 20710000。
PWM 驱动正常加载后,文件系统的 /sys/class/pwm/ 下会生成 PWM 节点(pwmchip*)。使用以下命令可以查看 PWM 节点与 PWM 资源的对应关系。
-
【PWM1 CH0】对应的节点是【pwmchip1】。
-
【PWM1 CH1】对应的节点是【pwmchip2】。
5.1.3 操作 PWM 节点
以下以操作【PWM1 CH0】为示例:
首先通过命令进入 pwmchip1 控制器(/sys/class/pwm/pwmchip1),可以看到以下内容:
cd /sys/class/pwm/pwmchip1ls-
export:用于导出 PWM 定时器设备。
-
unexport:用于释放 PWM 定时器设备。
向 /export 文件写入 0 后,会启用 PWM 定时器并生成 pwm0 目录。
echo 0 \> export
操作 PWM 节点
pwm0 进入定时器目录后,可以设置周期、占空比等各种属性。
cd pwm0
操作 PWM 节点
echo 1000000 > period ## 设置定时器一个周期内的脉冲数(周期时间)echo 500000 > duty_cycle ## 设置定时器一个周期内 Low 电平的脉冲数(有效时间)echo 1 > enable ## 启用 PWM 定时器echo 0 > enable ## 禁用 PWM 定时器反之,向 unexport 文件写入 0 后,PWM 定时器会关闭,同时 pwm0 目录会被删除。
cd ..echo 0 > unexport
操作 PWM 节点
需要修改图
5.2. 快速开始
5.2.1 准备开发环境
在 PC 端 Ubuntu 系统中运行 run 脚本,进入 EASY-EAI 编译环境。详细如下。
cd ~/develop_environment./run.sh 22045.2.2 下载源代码并编译示示例
首先,在虚拟机后台终端中执行以下命令,创建外设示示例源代码的管理目录:
cd ~/develop_environment./run.sh 2204下载示示例程序:
示例如,将示示例程序下载到“PC\D:”(没有固定要求,用户任意位置均可)。
然后,将下载的示示例复制到虚拟机文件系统中。步骤请参考下图。

下载源代码并编译示示例
最后,进入对应示示例目录并执行编译操作。具体命令如下:
cd EASY-EAI-Nano-TB/demo/10_PWM./build.sh💡 注意 :由于依赖库位于开发板上,交叉编译期间需要保持 /mnt 挂载。

下载源代码并编译示示例
编译成功后,会在 Release 目录中生成名为 test-pwm 的可执行程序,并自动放置到开发板的 /userdata/ 目录中。
5.2.3 运行示示例
通过串口调试或 SSH 进入开发板后台,并切换到示示例所在目录:
cd /userdata
运行示示例
执行以下命令启动 PWM 输出示示例:
sudo ./test-pwm运行结果如下。

运行示示例
示波器采集到的波形如下图所示:

运行示示例
5.3. C 语言使用示示例
这是 PWM 的 C 语言使用示示例。代码路径为 10_PWM/test-pwm/main.c 可作为编码参考。以下代码展示了 PWM 控制器初始化、周期调整、占空比设置以及资源释放的操作流程:
int main(int argc, const char** argv){ int ret;
ret = pwm_init("pwmchip1", "0"); printf("export_ret:%d\n", ret);
ret = pwm_set_attr("pwmchip1", "0", "period", "1000000"); printf("set_period_ret:%d\n", ret);
ret = pwm_set_attr("pwmchip1", "0", "duty_cycle", "500000"); printf("set_duty_cycle_ret:%d\n", ret);
ret = pwm_set_enable("pwmchip1", "0", "1"); printf("set_enable:%d\n", ret);
ret = pwm_release("pwmchip1", "0"); printf("unexport_ret:%d\n", ret); //======================================================================
ret = pwm_init("pwmchip2", "0"); printf("export_ret:%d\n",ret);
ret = pwm_set_attr("pwmchip2", "0", "period", "1000000"); printf("set_period_ret:%d\n",ret);
ret = pwm_set_attr("pwmchip2", "0", "duty_cycle", "500000"); printf("set_duty_cycle_ret:%d\n",ret);
ret = pwm_set_enable("pwmchip2", "0", "1"); printf("set_enable:%d\n",ret);
ret = pwm_release("pwmchip2", "0"); printf("unexport_ret:%d\n",ret); //======================================================================
return 0;}代码中的 pwm_init()、pwm_set_attr()、pwm_set_enable()、pwm_release() 是对系统调用进行易用封装后的包装函数。具体实现在 10_PWM/test-pwm/main.c 中。
6.CAN 接口
6.1. CAN 概述
使用 Socket CAN 的主要目的是为用户空间应用提供基于 Linux 网络层的 Socket 接口。
与常见的 TCP/IP 协议和以太网不同,CAN 总线没有类似以太网的 MAC 层地址,仅用于广播。CAN ID 只用于总线仲裁,因此必须在总线上唯一。设计 CAN-ECU(Electronic Control Unit:电子控制单元)网络时,可以将 CAN 消息 ID 映射到特定 ECU。因此,可以将 CAN 消息 ID 作为发送源地址使用。
6.1.1 开发板的 CAN 资源

开发板的 CAN 资源
6.1.2 硬件连接
通常,CPU 输出的 CAN 信号是 TTL 信号,并非差分信号。因此,需要一个将 CAN TTL 信号转换为 CAN 差分信号的模块。具体接线图如下。

硬件连接
6.2. 快速开始
6.2.1 准备开发环境
在 PC 端 Ubuntu 系统中运行 run 脚本,进入 EASY-EAI 编译环境。详细如下。
cd ~/develop_environment./run.sh 22046.2.2 下载源代码并编译示示例
首先,在虚拟机后台终端中执行以下命令,创建外设示示例源代码的管理目录:
cd /optmkdir -p EASY-EAI-Nano-TB/demo下载示示例程序:
示例如,将示示例程序下载到“PC\D:”(没有固定要求,用户任意位置均可)。
然后,将下载的示示例复制到虚拟机文件系统中。步骤请参考下图。

下载源代码并编译示示例
最后,进入对应示示例目录并执行编译操作。具体命令如下:
cd EASY-EAI-Nano-TB/demo/11_CAN./build.sh💡 注意 : 由于依赖库位于开发板上,交叉编译期间需要保持 /mnt 挂载。

下载源代码并编译示示例
编译成功后,会生成发送端 test-can_send 和接收端 test-can_reception 两个示示例程序,并自动放置到开发板的 /userdata/ 目录中。
- 本示示例进行收发测试需要两块开发板,因此需要在两块板上都重复上述步骤。
6.2.3 运行示示例
通过串口调试或 SSH 进入开发板后台,并切换到示示例所在目录:
cd /userdata
运行示示例
首先,在第一块板上运行发送端。命令如下:
sudo ./test-can-send接着,在【另一块】板上运行接收端。命令如下:
cd /userdatasudo ./test-can-reception【接收端】运行结果如下。

运行示示例
关于 API 的详细说明以及 API 调用(本示示例源代码),请参考以下说明。
6.3. CAN 操作 API 说明
6.3.1 创建 Socket CAN 套接字
创建 Socket CAN 套接字的函数原型如下。
int socket(int domain, int type, int protocol);详细说明如下。
| 项目 | 内容 |
|---|---|
| 函数名 | socket() |
| 头文件 | <sys/socket.h> |
| 输入参数 | Domain(域): AF_CAN (或者 PF_CAN)Type(类型): SOCK_RAWProtocol(协议): CAN_RAW |
| 返回值 | 成功: 文件描述符(fd)失败: -1 |
| 注意事项 | 无 |
6.3.2 指定本地网络接口地址
指定本地网络接口地址的函数原型如下。
int ioctl(int fd, unsigned long request, ...);详细说明如下。
ioctl() 函数
| 项目 | 内容 |
|---|---|
| 函数名 | ioctl() |
| 头文件 | <sys/ioctl.h> |
| 输入参数 | sockfd: socket() 函数创建的文件描述符(fd)Request: 控制命令 |
| 返回值 | 成功: 0失败: -1 |
| 注意事项 | 使用 ioctl 获取本地网络接口地址时,需要使用 ifreq 结构体。 |
struct ifreq 结构体
| 项目 | 内容 |
|---|---|
| 结构体名 | struct ifreq |
| 头文件 | <linux/if.h> |
| 注意事项 | 用于在调用 ioctl() 函数时获取 CAN 设备索引(ifr_ifindex)。其他参数无需特别关注。 |
6.3.3 绑定地址结构体
绑定地址结构体的函数原型如下。
int bind(int sockfd, const struct sockaddr \*addr, socklen_t addrlen);| 项目 | 内容 |
|---|---|
| 函数名 | bind() |
| 头文件 | <sys/socket.h> |
| 输入参数 | sockfd: socket() 函数创建的文件描述符(fd)addr: SocketCAN 地址结构体( struct sockaddr_can,在 <linux/can.h> 中定义)addrlen: 地址长度 |
| 返回值 | 成功: 0失败: -1 |
| 注意事项 | 无 |
6.3.4 设置 CAN 过滤器
设置 CAN 过滤器的函数原型如下。
int setsockopt(int sockfd, int level, int optname, const void \*optval, socklen_t optlen);详细说明如下。
setsockopt() 函数
| 项目 | 内容 |
|---|---|
| 函数名 | setsockopt() |
| 头文件 | <sys/socket.h> |
| 输入参数 | level: 选项定义所在层级(常用值:SOL_SOCKET, IPPROTO_TCP, SOL_CAN_RAW)optname: 要设置的选项 optval: 指向存放要设置为该选项的新值的缓冲区的指针 optlen: optval 缓冲区长度 |
| 返回值 | 成功: 0失败: -1 |
| 注意事项 | 需要使用过滤器结构体 struct can_filter(在 <linux/can.h> 中定义)。 |
struct can_filter 结构体
| 项目 | 内容 |
|---|---|
| 结构体名 | struct can_filter |
| 头文件 | <linux/can.h> |
| 参数 | can_id: 消息 ID can_mask: 过滤掩码 |
| 注意事项 | 过滤规则如下:(接收到的数据帧的 can_id) & mask == can_id & maskmask 在 <linux/can.h> 中定义,根据要接收的帧类型进行选择。 |
6.3.5 CAN 消息格式定义
CAN 消息格式定义如下。
| 项目 | 内容 |
|---|---|
| 结构体名 | struct can_frame |
| 头文件 | <linux/can.h> |
| 参数 | can_id: 消息 ID can_dlc: 数据长度 Data[]: 用于存放数据的数组 |
| 注意事项 | 默认发送标准帧。发送远程帧或错误帧时,需要操作消息 ID。 (示例: can_id = 0x1) |
6.4. CAN 通信示示例程序
【发送端】示示例源代码位于 11_CAN/test-can_send/main.c。操作流程如图所示。

CAN 通信示示例程序
【接收端】示示例源代码位于 11_CAN/test-can_reception/main.c。操作流程如图所示。

CAN 通信示示例程序
参考示示例代码如下。
发送端示示例:
/* 将 CAN0 波特率设置为 500000 bps / #define ip_cmd_set_can_params “ip link set can0 type can bitrate 500000 triple-sampling on” / 启动 CAN0 / #define ip_cmd_open “ifconfig can0 up” / 停止 CAN0 */ #define ip_cmd_close “ifconfig can0 down”
int main(){ int fd, nbytes; struct sockaddr_can addr; struct ifreq ifr; struct can_frame frame[2] = {{0}};
system(ip_cmd_close); system(ip_cmd_set_can_params); system(ip_cmd_open);
fd = socket(PF_CAN, SOCK_RAW, CAN_RAW); // 创建套接字 strcpy(ifr.ifr_name, "can0"); ioctl(fd, SIOCGIFINDEX, &ifr); // 指定 can0 设备
addr.can_family = AF_CAN; addr.can_ifindex = ifr.ifr_ifindex; bind(fd, (struct sockaddr *)&addr, sizeof(addr)); // 将套接字绑定到 can0
// 禁用过滤规则。该进程不接收消息,只负责发送 setsockopt(fd, 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(fd, &frame[0], sizeof(frame[0])); // 发送 frame[0] printf("write ret:%d\n", nbytes); if(nbytes != sizeof(frame[0])) { printf("Send Error frame[0]!\n"); break; // 发送错误,退出 } sleep(1);
nbytes = write(fd, &frame[1], sizeof(frame[1])); // 发送 frame[1] if(nbytes != sizeof(frame[1])) { printf("Send Error frame[1]!\n"); break; } sleep(1); } close(fd); return 0;}接收端示示例:
/* 将 CAN0 波特率设置为 500000 bps */#define ip_cmd_set_can_params "ip link set can0 type can bitrate 500000 triple-sampling on"/* 启动 CAN0 */#define ip_cmd_open "ifconfig can0 up"/* 停止 CAN0 */#define ip_cmd_close "ifconfig can0 down"
int main(){ int fd, nbytes; struct sockaddr_can addr; struct ifreq ifr; struct can_frame frame; struct can_filter rfilter[1];
system(ip_cmd_close); system(ip_cmd_set_can_params); system(ip_cmd_open);
fd = socket(PF_CAN, SOCK_RAW, CAN_RAW); // 创建套接字 strcpy(ifr.ifr_name, "can0"); ioctl(fd, SIOCGIFINDEX, &ifr); // 指定 can0 设备
addr.can_family = AF_CAN; addr.can_ifindex = ifr.ifr_ifindex; bind(fd, (struct sockaddr *)&addr, sizeof(addr)); // 将套接字绑定到 can0
// 定义接收规则。仅接收标识符为 0x11 的消息 rfilter[0].can_id = 0x11; rfilter[0].can_mask = CAN_SFF_MASK;
// 设置过滤规则 setsockopt(fd, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));
while(1) { nbytes = read(fd, &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(fd); return 0;}