跳到主要内容

wiringx

简介

wiringX 是一个开源的 GPIO 控制库,旨在为不同的嵌入式平台提供通用且统一的 GPIO 控制接口。它基于 WiringPi 库进行了改进和扩展,并支持多种嵌入式平台,对Milk-V Duo也进行了适配。使用wiringX,开发者可以使用相同的代码来控制不同平台上的 GPIO 引脚,简化了跨平台开发的工作,使得开发嵌入式应用程序更加方便和灵活。

本文将分为如下4个部分介绍如何使用 wiringX 在 Duo 上开发应用:

  1. wiringX 的 APIs
  2. 基本使用方法代码示范
  3. 基于 wiringX 的应用程序编译环境配置
  4. 一些使用 wiringX 实现的 Demo 和项目介绍

如果您对 wiringX 的使用方法已经非常熟悉,可以直接参考我们的样例代码: duo-examples

注意,Duo 系列的很多引脚功能是复用的,在使用wiringX来控制 Duo/Duo256M/DuoS 各引脚的功能时,要先确认一下引脚当前的状态是不是自己需要的功能, 如果不是,可以用duo-pinmux命令来切换为所需功能。

具体方法请参考: 引脚复用

Duo/Duo256M wiringX 引脚序号

Duo 和 Duo256M 的 wiringX 引脚序号, 与引脚名序号是一致的,蓝色 LED 控制引脚不在引出的 40PIN 物理引脚上,其 wiringX 的序号是25

Document Pictures
wiringXPIN NAMEPIN#PIN#PIN NAMEwiringX
0GP0
1
40
VBUS
1GP1
2
39
VSYS
GND
3
38
GND
2GP2
4
37
3V3_EN
3GP3
5
36
3V3(OUT)
4GP4
6
35
5GP5
7
34
GND
8
33
GND
6GP6
9
32
GP2727
7GP7
10
31
GP2626
8GP8
11
30
RUN
9GP9
12
29
GP2222
GND
13
28
GND
10GP10
14
27
GP2121
11GP11
15
26
GP2020
12GP12
16
25
GP1919
13GP13
17
24
GP1818
GND
18
23
GND
14GP14
19
22
GP1717
15GP15
20
21
GP1616
 
25GP25
LED

DuoS wiringX 引脚序号

DuoS 的 wiringX 引脚序号, 与物理引脚序号是一致的,蓝色 LED 控制引脚不在引出的 40PIN 物理引脚上,其 wiringX 的序号是 0

Document Pictures

排针 J3

排针 J3 上的 GPIO 使用 3.3V 逻辑电平。

wiringXPIN NAMEPIN#PIN#PIN NAMEwiringX
3V3
1
2
VSYS(5V)
3B20
3
4
VSYS(5V)
5B21
5
6
GND
7B18
7
8
A168
GND*
9
10
A1710
11B11
11
12
B1912
13B12
13
14
GND
15B22
15
16
A2016
3V3
17
18
A1918
19B13
19
20
GND
21B14
21
22
A1822
23B15
23
24
B1624
GND
25
26
A2826

GND*:引脚 9 在 DuoS V1.1 版本硬件中是一个低电平的 GPIO,在 V1.2 及更高版本硬件中为 GND。

排针 J4

排针 J4 上的 GPIO 使用 1.8V 逻辑电平。

该排针上的大部分引脚都有其专用功能,如 MIPI DSI 信号,触摸屏信号以及音频信号,如非特殊需求,不建议使用该排针上的引脚做为 GPIO 使用。

wiringXPIN NAMEPIN#PIN#PIN NAMEwiringX
VSYS(5V)
52
51
AUDIO_OUT_R
50B1
50
49
AUDIO_OUT_L
48B2
48
47
AUDIO_IN_R
46B3
46
45
AUDIO_IN_L
44E2
44
43
3V3
42E1
42
41
C1841
40E0
40
39
C1939
GND
38
37
GND
36C20
36
35
C1635
34C21
34
33
C1733
GND
32
31
GND
30C14
30
29
C1229
28C15
28
27
C1327

一、代码示范

GPIO 使用示例

下面是一个操作 GPIO 的例子,将 Duo 的20引脚间隔1秒循环拉高再拉低,物理20引脚的 wiringX 序号是15

#include <stdio.h>
#include <unistd.h>

#include <wiringx.h>

int main() {
int DUO_GPIO = 15;

// Duo: milkv_duo
// Duo256M: milkv_duo256m
// DuoS: milkv_duos
if(wiringXSetup("milkv_duo", NULL) == -1) {
wiringXGC();
return -1;
}

if(wiringXValidGPIO(DUO_GPIO) != 0) {
printf("Invalid GPIO %d\n", DUO_GPIO);
}

pinMode(DUO_GPIO, PINMODE_OUTPUT);

while(1) {
printf("Duo GPIO (wiringX) %d: High\n", DUO_GPIO);
digitalWrite(DUO_GPIO, HIGH);
sleep(1);
printf("Duo GPIO (wiringX) %d: Low\n", DUO_GPIO);
digitalWrite(DUO_GPIO, LOW);
sleep(1);
}

return 0;
}

编译后放到 Duo 中运行,可以用万用表或者示波器测量20引脚的状态是否符合预期。

也可以使用板上的 LED 引脚来验证,通过观察 LED 亮灭来直观地判断程序是否正确执行,LED 引脚的 wiringX 序号为25,把上面代码中的15引脚改为25即可,需要注意的是默认固件开机后通过脚本控制 LED 闪烁了,要将其禁用,方法请参考下面的 blink 例子说明。

I2C 使用示例

以下是一个 I2C 的示例:

#include <stdio.h>
#include <unistd.h>
#include <stdint.h>

#include <wiringx.h>

#define I2C_DEV "/dev/i2c-1"

#define I2C_ADDR 0x04

int main(void)
{
int fd_i2c;
int data = 0;

// Duo: milkv_duo
// Duo256M: milkv_duo256m
// DuoS: milkv_duos
if(wiringXSetup("milkv_duo", NULL) == -1) {
wiringXGC();
return -1;
}

if ((fd_i2c = wiringXI2CSetup(I2C_DEV, I2C_ADDR)) <0) {
printf("I2C Setup failed: %d\n", fd_i2c);
wiringXGC();
return -1;
}

// TODO
}

SPI 使用示例

以下是一个SPI的示例:

#include <stdio.h>
#include <unistd.h>
#include <stdint.h>

#include <wiringx.h>

int main(void)
{
int fd_spi;

// Duo: milkv_duo
// Duo256M: milkv_duo256m
// DuoS: milkv_duos
if(wiringXSetup("milkv_duo", NULL) == -1) {
wiringXGC();
return -1;
}

if ((fd_spi = wiringXSPISetup(0, 500000)) <0) {
printf("SPI Setup failed: %d\n", fd_spi);
wiringXGC();
return -1;
}

// TODO
}

UART 使用示例

以下是一个 UART 的示例,使用引脚 4/5 上的 UART4:

#include <stdio.h>
#include <unistd.h>

#include <wiringx.h>

int main() {
struct wiringXSerial_t wiringXSerial = {115200, 8, 'n', 1, 'n'};
char buf[1024];
int str_len = 0;
int i;
int fd;

// Duo: milkv_duo
// Duo256M: milkv_duo256m
// DuoS: milkv_duos
if(wiringXSetup("milkv_duo", NULL) == -1) {
wiringXGC();
return -1;
}

if ((fd = wiringXSerialOpen("/dev/ttyS4", wiringXSerial)) < 0) {
printf("Open serial device failed: %d\n", fd);
wiringXGC();
return -1;
}

wiringXSerialPuts(fd, "Duo Serial Test\n");

while(1)
{
str_len = wiringXSerialDataAvail(fd);
if (str_len > 0) {
i = 0;
while (str_len--)
{
buf[i++] = wiringXSerialGetChar(fd);
}
printf("Duo UART receive: %s\n", buf);
}
}

wiringXSerialClose(fd);

return 0;
}

测试方法:

电脑上的 USB 转串口线的 RX 接 Duo 的4脚(UART4_TX),串口线的 TX 接 Duo 的5脚(UART4_RX),串口线的 GND 接 Duo 的 GND,电脑中使用串口调试助手配置好相应的 COM 口和参数。

上述程序编译后生成的可执行程序命名为uart_test,通过ssh上传到 Duo 中运行,可以看到电脑上的串口工具中收到了Duo Serial Test字符串,串口工具中发送一个字符串Hello World,Duo 的终端上也会收到对应的字符串,说明串口收发都正常。

duo

二、开发环境配置

准备开发环境

使用本地的 Ubuntu 系统,推荐 Ubuntu 20.04 LTS
(也可以使用虚拟机中的Ubuntu系统、Windows 中 WSL 安装的 Ubuntu、基于 Docker 的 Ubuntu 系统)。

  • 安装编译依赖的工具

    sudo apt-get install wget git make
  • 获取 Examples 源码

    git clone https://github.com/milkv-duo/duo-examples.git
  • 加载编译环境

    cd duo-examples
    source envsetup.sh

    第一次加载会自动下载所需的 SDK 包,大小为180M左右,下载完会自动解压到duo-examples下,解压后的目录名为duo-sdk,下次加载时检测到已存在该目录,就不会再次下载了。

    注: 如果因为网络原因无法完成SDK包的下载,请通过其他途径获取到duo-sdk.tar.gz包,手动解压到duo-examples目录下,重新source envsetup.sh

  • 编译测试

    hello-world为例,进入该例子目录直接执行make即可:

    cd hello-world
    make

    编译成功后将生成的helloworld可执行程序通过网口或者RNDIS网络等方式传送到 Duo 设备中,比如默认固件支持的 RNDIS 方式,Duo 的 IP 为192.168.42.1,用户名是root,密码是milkv

    scp helloworld [email protected]:/root/

    发送成功后,在 ssh 或者串口登陆的终端中运行./helloworld,会打印Hello, World!

    [root@milkv]~# ./helloworld
    Hello, World!

    至此,我们的编译开发环境就可以正常使用了

如何创建自己的工程

根据需要,拷贝现有的例子,稍加修改即可。比如需要操作某个 GPIO,可以参考blink例子,LED闪烁就是通过控制 GPIO 电平高低实现的,平台初始化和控制 GPIO 的方法,可参考blink.c中的代码。

  • 新建自己的工程目录my-project
  • 复制blink例子中的blink.cMakefile文件到my-project目录
  • blink.c重命名为自己所需名字如gpio_test.c
  • 修改Makefile中的TARGET=blinkTARGET=gpio_test
  • 修改gpio_test.c,实现自己的代码逻辑
  • 执行make命令编译
  • 将生成的gpio_test可执行程序发送到Duo中运行

注意:

  • 新建工程目录不是必须要放到 duo-examples 目录下的,可以根据自己的习惯放到其他位置,执行 make 编译命令之前,加载过 duo-examples 目录下的编译环境就可以了(source /PATH/TO/duo-examples/envsetup.sh)。
  • 在加载过编译环境(envsetup.sh)的终端里,不要编译其他平台如 ARM 或 X86 的 Makefile 工程,如需编译其他平台项目,需要新开终端。

三、Demo和项目说明

hello-world

源码:https://github.com/milkv-duo/duo-examples/tree/main/hello-world

一个简单的例子,不操作 Duo 外设,仅打印输出"Hello, World!",用来验证开发环境。

源码:https://github.com/milkv-duo/duo-examples/tree/main/blink

一个让 Duo 板载 LED 闪烁的例子,操作 GPIO 使用的是wiringX的库,blink.c代码中包含了wiringX中的平台初始化以及操作 GPIO 的方法。

注意: 当前Duo的默认固件上电后 LED 会自动闪烁,这个是通过开机脚本实现的,在测试该 blink 例子的时候,需要将 LED 闪烁的脚本禁用,在 Duo 的终端中执行:

mv /mnt/system/blink.sh /mnt/system/blink.sh_backup && sync

也就是将 LED 闪烁脚本改名,重启 Duo 后,LED 就不闪了
测试完我们C语言实现的 blink 程序后,如果需要恢复 LED 闪烁脚本,再将其名字改回来,重启即可:

mv /mnt/system/blink.sh_backup /mnt/system/blink.sh && sync

ADC

adcRead 读取电压值

源码:https://github.com/milkv-duo/duo-examples/tree/main/adc

读取 ADC 的测量值,分为 shell 脚本和C语言两个版本,启动后根据输出提示选择要读取的 ADC,选择后会循环打印 ADC 测量到的电压值。

I2C

I2C 代码目录:https://github.com/milkv-duo/duo-examples/tree/main/i2c

BMP280 温度气压传感器

源码:https://github.com/milkv-duo/duo-examples/tree/main/i2c/bmp280_i2c

通过 I2C 接口连接温度气压传感器 BMP280,读取当前温度和气压值。

VL53L0X ToF 测距传感器

源码:https://github.com/milkv-duo/duo-examples/tree/main/i2c/vl53l0x_i2c

通过 I2C 接口使用 TOF 测距传感器 VL53L0X 模块,读取测量到的距离。

SSD1306 显示屏

源码:https://github.com/milkv-duo/duo-examples/tree/main/i2c/ssd1306_i2c

通过 I2C 接口在 SSD1306 OLED 显示屏上显示字符串。

ADXL345 三轴加速度传感器

源码:https://github.com/milkv-duo/duo-examples/blob/main/i2c/adxl345_i2c

通过 I2C 接口读取 ADXL345 获得的加速度数据,每 1s 读取一次,并将结果打印在屏幕上。

LCM1602 显示屏

源码:https://github.com/milkv-duo/duo-examples/blob/main/i2c/lcm1602_i2c

通过 I2C 接口在 1602 LCD 屏幕上显示字符串。

LCM2004 显示屏

源码:https://github.com/milkv-duo/duo-examples/blob/main/i2c/lcm2004_i2c

通过 I2C 接口在 2004 LCD 屏幕上显示字符串。

TCS34725 颜色传感器

源码:https://github.com/milkv-duo/duo-examples/blob/main/i2c/tcs34725_i2c

通过 I2C 接口读取 TCS34725 颜色传感器,并将获得的数据输出。

SPI

SPI 代码目录:https://github.com/milkv-duo/duo-examples/tree/main/spi

MAX6675 热电偶温度传感器

源码:https://github.com/milkv-duo/duo-examples/tree/main/spi/max6675_spi

通过 SPI 接口连接 K 型热电偶测量模块 MAX6675,测量当前传感器上的温度。

RC522 RFID读写模块

源码:https://github.com/milkv-duo/duo-examples/tree/main/spi/rc522_spi

通过 SPI 接口连接 RC522 RFID 读写模块,读取卡片 ID 和类型并输出到屏幕。

四、编译 wiringX

Duo 固件中已经包含编译好的 wiringX 库(/usr/lib/libwiringx.so),可以直接使用。如果你需要通过编译 wiringX 的源码来生成该库,可以按如下方法编译。

我们这里在 Ubuntu 主机或其他 Linux 发行版上进行编译。

注:Duo 的 wiringX 代码有部分尚未合入上游 wiringX 仓库中,在实际使用中请优先使用 Duo 固件中的 wiringX 库。

下载 wiringX 源码

git clone https://github.com/wiringX/wiringX.git

修改 CMakeLists.txt

进入代码目录:

cd wiringX

wiringX 项目使用 cmake 方式来编译,需要通过 vi 或其他编辑器修改 CMakeLists.txt,来添加交叉编译工具链以及编译参数:

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8909393..6918181 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -17,6 +17,11 @@ set(CMAKE_EXE_LINKER_FLAGS " -Wl,-rpath=/usr/local/lib/,-rpath=/usr/lib/,-rpath=
set(CMAKE_SHARED_LINKER_FLAGS " -Wl,-rpath=/usr/local/lib/,-rpath=/usr/lib/,-rpath=/lib/")
set(CMAKE_MODULE_LINKER_FLAGS " -Wl,-rpath=/usr/local/lib/,-rpath=/usr/lib/,-rpath=/lib/")

+set(CMAKE_C_COMPILER "${CMAKE_CURRENT_SOURCE_DIR}/host-tools/gcc/riscv64-linux-musl-x86_64/bin/riscv64-unknown-linux-musl-gcc")
+set(CMAKE_CXX_COMPILER "${CMAKE_CURRENT_SOURCE_DIR}/host-tools/gcc/riscv64-linux-musl-x86_64/bin/riscv64-unknown-linux-musl-g++")
+set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mcpu=c906fdv -march=rv64imafdcv0p7xthead -mcmodel=medany -mabi=lp64d")
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64")
+
# Start uninstaller generator
function(WRITE_UNINSTALL_TARGET_SCRIPT)
# Create uninstall target template file, if it doesn't exist...

其中有两个变量需要注意一下,根据自己的文件路径来配置:

  • CMAKE_C_COMPILER:交叉编译工具链中 gcc 的路径
  • CMAKE_CXX_COMPILER:交叉编译工具链中 g++ 的路径

交叉编译工具链的下载链接:host-tools.tar.gz。可以通过 wget 命令下载后解压:

wget https://sophon-file.sophon.cn/sophon-prod-s3/drive/23/03/07/16/host-tools.tar.gz
tar -xf host-tools.tar.gz

如果你曾经编译过 duo-buildroot-sdk,其根目录下的 host-tools 目录就是交叉工具链的目录,没必要重新下载,可以直接修改 CMAKE_CURRENT_SOURCE_DIR 字段指定到该目录即可。或者创建个软链接指向该目录。

修改代码

由于交叉工具链中的 time 相关定义与 wiringX 中的定义稍有不同,添加如下两行修改:

diff --git a/src/wiringx.c b/src/wiringx.c
index 034674a..4171a75 100644
--- a/src/wiringx.c
+++ b/src/wiringx.c
@@ -113,6 +113,9 @@ static struct spi_t spi[2] = {
} while(0)
#endif

+typedef time_t __time_t;
+typedef suseconds_t __suseconds_t;
+
/* Both the delayMicroseconds and the delayMicrosecondsHard
are taken from wiringPi */
static void delayMicrosecondsHard(unsigned int howLong) {

编译

cmake 方式编译会创建一些中间目录和文件,所以我们新建一个 build 目录并进入该目录来完成编译:

mkdir build
cd build
cmake ..
make

编译完成后,当前 build 目录下生成的 libwiringx.so 就是我们所需要的 wiringX 库。

注意事项

如果遇到编译报错,可以尝试更换 cmake 的版本,比如可以手动安装目前最新的 3.27.6 版本:

wget https://github.com/Kitware/CMake/releases/download/v3.27.6/cmake-3.27.6-linux-x86_64.sh
chmod +x cmake-3.27.6-linux-x86_64.sh
sudo sh cmake-3.27.6-linux-x86_64.sh --skip-license --prefix=/usr/local/

手动安装的 cmake/usr/local/bin 中,此时用 cmake --version 命令查看其版本号, 应为:

cmake version 3.27.6

五、wiringX APIs

General

int wiringXSetup(char *name, ...)

初始化 WiringX 库,用于初始化 GPIO 引脚的配置和资源:

  • Duo
    wiringXSetup("milkv_duo", NULL)
  • Duo256M
    wiringXSetup("milkv_duo256m", NULL)
  • DuoS
    wiringXSetup("milkv_duos", NULL)
int wiringXValidGPIO(int pin)

判断 GPIO pin 是否可用。

void delayMicroseconds(unsigned int ms)

延时毫秒。

int wiringXGC(void)

释放资源。

char *wiringXPlatform(void)

返回平台信息。

GPIO

int pinMode(int pin, pinmode_t mode)

设置指定引脚的工作模式, pin 是引脚编号, mode 可以是:

  • PINMODE_INPUT 输入模式
  • PINMODE_OUTPUT 输出模式
  • PINMODE_INTERRUPT 中断模式
int digitalRead(int pin)

读取指定引脚pin的输入值, 返回值为 HIGH 或 LOW。

int digitalWrite(int pin, enum digital_value_t value)

设置指定引脚pin的输出值, value 可以是:

  • HIGH 高电平
  • LOW 低电平
int waitForInterrupt(int pin, int ms)

等待引脚pin上的中断发生, 参数ms为超时时间, 单位毫秒。 该函数已弃用, 建议使用 wiringXISR

int wiringXISR(int pin, enum isr_mode_t mode)

将引脚pin配置为中断方式, 其中mode的几种模式:

  • ISR_MODE_RISING
  • ISR_MODE_FALLING
  • ISR_MODE_BOTH

I2C

int wiringXI2CSetup(const char *dev, int addr)

配置i2c节点和i2c地址。

int wiringXI2CRead(int fd)

读取1个字节的数据。

int wiringXI2CReadReg8(int fd, int reg)

从reg寄存器读取1个字节的数据。

int wiringXI2CReadReg16(int fd, int reg)

从reg寄存器读取2个字节的数据。

int wiringXI2CWrite(int fd, int reg)

写寄存器的地址reg。

int wiringXI2CWriteReg8(int fd, int reg, int value8)

将8位数据value8写入寄存器reg。

int wiringXI2CWriteReg16(int fd, int reg, int value16)

将16位数据value16写入寄存器reg。

SPI

int wiringXSPISetup(int channel, int speed)

配置SPI设备的channel(Duo上为0)和speed(Duo中默认为500000)。

int wiringXSPIDataRW(int channel, unsigned char *data, int len)

SPI总线是上升沿写数据,下降沿读数据,所以该函数同时执行读写操作,因此读取的数据会覆盖写入的数据,使用时需注意。

int wiringXSPIGetFd(int channel)

获取SPI设备的文件描述符,channel在Duo中默认为0。

UART

int wiringXSerialOpen(const char *dev, struct wiringXSerial_t serial)

打开串口设备,dev是设备描述符,serial是个结构体,需要填充串口相关参数
具体可参考下面的UART使用示例

typedef struct wiringXSerial_t {
unsigned int baud; // 波特率
unsigned int databits; // 数据位: 7/8
unsigned int parity; // 奇偶校验: o/e/n
unsigned int stopbits; // 停止位: 1/2
unsigned int flowcontrol; // 硬件流控: x/n
} wiringXSerial_t;
void wiringXSerialClose(int fd)

关闭串口。

void wiringXSerialFlush(int fd)

清空缓存区。

void wiringXSerialPutChar(int fd, unsigned char c)

输出一个字符。

void wiringXSerialPuts(int fd, const char *s)

输出字符串。

void wiringXSerialPrintf(int fd, const char *message, ...)

格式化输出。

int wiringXSerialDataAvail(int fd)

返回缓存区接收到的数据个数。

int wiringXSerialGetChar(int fd)

从串口设备读取一个字符。

PWM

当前版本 wiringX 只支持 Duo 的 PWM,Duo256M 和 DuoS 后续会加入 PWM 的支持。

  • Duo PWM 引脚编号
PWMPIN NAMEPin#Pin#PIN NAME
GP0
1
40
VBUS
GP1
2
39
VSYS
GND
3
38
GND
10GP2
4
37
3V3_EN
11GP3
5
36
3V3(OUT)
5GP4
6
35
6GP5
7
34
GND
8
33
GND
9GP6
9
32
GP27
8GP7
10
31
GP26
7GP8
11
30
RUN
4GP9
12
29
GP22
GND
13
28
GND
GP10
14
27
GP21
GP11
15
26
GP20
4GP12
16
25
GP19
5GP13
17
24
GP18
GND
18
23
GND
GP14
19
22
GP17
GP15
20
21
GP16
wiringXPWMSetPeriod(int pin, long period)

设置 PWM 引脚的周期, pin 是引脚编号, period 单位为纳秒。

int wiringXPWMSetDuty(int pin, long duty_cycle)

设置 PWM 引脚一个周期内高电平所占时间, duty_cycle 单位为纳秒。

int wiringXPWMSetPolarity(int pin, int polarity)

设置 PWM 引脚极性,polarity 位为 0 或者 1:

  • 0 正常
  • 1 反转
int wiringXPWMEnable(int pin, int enable)

使能或禁用 PWM 引脚输出,enable 输出为 0 或者 1:

  • 0: 禁用
  • 1: 使能
  • carbonfix
  • logan-milkv
  • Rjgawuie
  • milkrisc-v
  • hokamilkv