RK3588笔记(一)——ping通 + imx6ull项目移植

今年春招怎么突然冒出来这么多ai岗,再不学ai要寄了。赶紧砸锅卖铁买个3588看看
但也没时间重头学了,因此主要是将imx6ull的代码移植过来,后面再上ai

板子:野火 鲁班猫LBC5(板子买回来自带debian系统,这里直接跳过烧系统的过程)


一、ping通板子和windows

6. 网络连接及静态配置 --- 快速使用手册---基于LubanCat-RK3588系列板卡 文档

这部分我的目的是使用网线将板子和windows联通,然后使用VSCODE的SSH开发。除了网口连接,野火的教程中还给了使用usb连接、无线连接。这里只记一下网口连接。

网线一头插到板子上,一头插到主机上

右键右下角的网络图标,选择" 打开'网络和internet设置' ":

找到板子对应的以太网(可以通过拔插网线判断,拔掉会显示断开):

双击自己电脑的以太网,点击属性-->共享-->选择板子的以太网-->确定:

这一步有时候会失效,比如我之前电脑自动更新重启后板子就连不上网了,把共享中的两个勾去掉、确定,然后重新勾上、确定即可

现在在去开发板查看一下,终端使用ifconfig命令,应当能看到ip,通常为192.168.137.xxx

然后进VSCODE的SSH连接,输入ssh 用户名@192.168.XXX.XXX

选择linux:

然后输入密码(默认密码是temppwd),就能成功进入:

野火教程中还提供了VNC远程桌面7. VNC远程桌面 --- 快速使用手册和SSH连接教程【鲁班猫】13-SSH终端连接到鲁班猫_哔哩哔哩_bilibili

二、设备树

这部分内容详见[野火]《快速使用手册---基于LubanCat-RK3588系列板卡》_20260328.pdf

dtb文件都保存在/boot/dtb/下面。可以看到里面有许多dtb文件。具体使用哪个,直接查看/boot/下面的rk-kernel.dtb这个软连接即可:

2.1 获取dts文件

直接开始改设备树。首先先把rk3588-lubancat-5-v2.dtb备份一下,免得翻车。板子里面是没有dts文件的,需要克隆一下内核:

复制代码
git clone --depth 1 -b stable-5.10-rk3588 https://ghfast.top/https://github.com/LubanCat/kernel.git

也可从野火网盘资料的"8-SDK源码压缩包及虚拟机"中下载rk3588的SDK:

我这里用克隆得到的文件,里面可以找到我们要的dts文件:

打开rk3588-lubancat-5-v2.dts,却发现只有65行。因为开头有#include "rk3588-lubancat-5.dts",所以主要内容还是放在rk3588-lubancat-5.dts中。

2.2 确定引脚

按照imx6ull的经验,修改设备树主要是:

①追加/修改控制器节点

②定义引脚复用

③增加设备节点

首先确定需要用哪些脚:现在需要一个i2c、一个spi和一个pwm

从rk3588-lubancat-5.dts中看,已经被占用的有:i2c0/1/5/6/7、spi2、pwm0/1/3

所以这里选用i2c3、spi0、pwm13

注意上面这句话↑,后面要考

上面这张图的编号15/16的通用功能应该是打错了,不然i2c2有两个SDA且没有SCL。去设备树瞄了一眼,确实错了:

最后选定引脚如下:

|---------|------|-------|--------|----------|--------------|
| | 外设引脚 | 功能 | 板子物理引脚 | gpio脚 | 复用功能 |
| OLED | D0 | SCLK | 23 | GPIO4_A2 | SPI0_CLK_M1 |
| OLED | D1 | MOSI | 19 | GPIO4_A1 | SPI0_MOSI_M1 |
| OLED | RES | 复位 | 11 | GPIO0_B7 | GPIO0_B7 |
| OLED | DC | 命令/数据 | 7 | GPIO0_C0 | GPIO0_C0 |
| OLED | CS | 片选 | 38 | GPIO4_B2 | SPI0_CS0_M1 |
| SG90 | | PWM | 32 | GPIO4_B6 | PWM13_M1 |
| mpu6050 | SCL | SCK | 5 | GPIO1_C1 | I2C3_SCL_M0 |
| mpu6050 | SDA | SDA | 3 | GPIO1_C0 | I2C3_SDA_M0 |
| mpu6050 | INT | IRQ | 27 | GPIO1_A2 | GPIO1_A2 |

2.3 修改设备树

修改设备树建议按照野火的教程,直接使用野火提供的可视化配置fire-config进行配置。但我就是手贱想自己改设备树

这部分我直接在rk3588-lubancat-5-v2.dts中修改,不修改rk3588-lubancat-5.dts。

将rk3588-lubancat-5-v2.dts复制一份备份,新版的重命名为my_rk3588-lubancat-5-v2.dts

2.3.1 新增引脚复用节点

spi、i2c、pwm的引脚复用已经在rk3588s-pinctrl.dtsi文件中配置好了。到时候直接引用即可。需要我们自行配置的主要是mpu6050的irq和oled的两个gpio。

这里我本来选用gpio6/7的脚作为gpio,但是改了设备树以后发现板子没法启动。试了大半天最后发现是因为前面图中说的"gpio6/7不能复用为其他功能",虽然我依然是使用其gpio功能,但是我使用了rockchip,pins=<6 RK_PA7 RK_FUNC_GPIO &pcfg_pull_up>类似代码,相当于还是复用了。

所以如果要用gpio6/7,要么不要复用节点、直接写到设备节点中,要么不用6/7。这里我改用gpio1

把下面的代码写到&pinctrl下面:

cpp 复制代码
	my_peripherals {
	/* MPU6050的中断: GPIO1_A2 */
		mpu6050_irq_pin: mpu6050-irq-pin {
			rockchip,pins =
				<1 RK_PA2 RK_FUNC_GPIO &pcfg_pull_up>;
		};

		/* OLED的DC: GPIO0_C0 */
		oled_dc_pin: oled-dc-pin {
			rockchip,pins =
				<0 RK_PC0 RK_FUNC_GPIO &pcfg_pull_none>;
		};

		/* OLED的RES: GPIO0_B7 */
		oled_res_pin: oled-res-pin {
			rockchip,pins =
				<0 RK_PB7 RK_FUNC_GPIO &pcfg_pull_none>;
		};
	};

2.3.2 新增sg90相关节点

在根节点下直接新增sg90设备节点:

cpp 复制代码
	sg90: servo {
		compatible = "my_sg90";
		pwms = <&pwm13 0 20000000 0>;   /* 用pwm13,id=0,周期20ms=20000000ns */
		status = "okay";
	};

在文件末尾追加pwm13节点。在rk3588s-pinctrl.dtsi中已经写好了pwm13m1的复用(pwm13m1_pins),直接引用过来即可。

cpp 复制代码
&pwm13 {  
	pinctrl-names = "active";  // 这里有坑啊,一定要写active!不要写default!详见附录C
	pinctrl-0 = <&pwm13m1_pins>;
	status = "okay";
};

2.3.3 新增spi节点

spi0的引脚复用已经在rk3588-lubancat-5-v2.dts写好了,直接用spi0m1_cs0和spi0m1_pins引用过来即可:

cpp 复制代码
&spi0 {
	pinctrl-names = "default";
	pinctrl-0 = <&spi0m1_cs0 &spi0m1_pins>;
	num-cs = <1>;
	status = "okay";

	oled@0 {
		compatible = "my_ecspi_oled";
		reg = <0>;
		spi-max-frequency = <8000000>;

		pinctrl-names = "default";
		pinctrl-0 = <&oled_dc_pin &oled_res_pin>;

		dc-gpios  = <&gpio0 RK_PC0 GPIO_ACTIVE_LOW>;
		res-gpios = <&gpio0 RK_PB7 GPIO_ACTIVE_LOW>;

		status = "okay";
	};
};

2.3.4 新增i2c节点

i2c3的引脚复用已经在rk3588-lubancat-5-v2.dts写好了,直接用i2c3m0_xfer引用过来即可:

cpp 复制代码
&i2c3 {
	pinctrl-names = "default";
	pinctrl-0 = <&i2c3m0_xfer>;
	status = "okay";

	mpu6050@68 {
		compatible = "my_mpu6050";
		reg = <0x68>;

		pinctrl-names = "default";
		pinctrl-0 = <&mpu6050_irq_pin>;

		interrupt-parent = <&gpio1>;
		interrupts = <RK_PA2 IRQ_TYPE_EDGE_RISING>;

		status = "okay";
	};
};

2.3.5 完整的rk3588-lubancat-5-v2.dts文件

cpp 复制代码
// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
/*
 * Copyright (c) 2021 Rockchip Electronics Co., Ltd.
 * Copyright (c) 2023 EmbedFire <embedfire@embedfire.com>
 */

/dts-v1/;

#include "rk3588-lubancat-5.dts"

/ {
	model = "Embedfire LubanCat-5 V2";
	compatible = "embedfire,rk3588-lubancat-5-v2", "rockchip,rk3588";

	// 2026/4/14新增
	sg90: servo { /* 这部分和imx6ull中的sg90设备节点完全一致,只修改pwm号*/
		compatible = "my_sg90";
		pwms = <&pwm13 0 20000000 0>;   /* 用pwm13,id=0,周期20ms=20000000ns */
		status = "okay";
	};
};

&backlight_dsi0 {
	status = "disabled";
	pwms = <&pwm2 0 25000 0>;
};

&backlight_dsi1 {
	status = "disabled";
	pwms = <&pwm5 0 25000 0>;
};

&pwm0 {
	status = "disabled";
};

&pwm1 {
	status = "disabled";
};

&pwm2 {
	status = "disabled";
	pinctrl-0 = <&pwm2m0_pins>;
};

&pwm5 {
	status = "disabled";
	pinctrl-0 = <&pwm5m1_pins>;
};

&usbc0 {
	interrupts = <RK_PB2 IRQ_TYPE_LEVEL_LOW>;
	pinctrl-0 = <&fusb302_int>;
};

&pcie2x1l0 {
	disable-gpios = <&gpio6 0 GPIO_ACTIVE_HIGH>;
};

&pinctrl {
	usb-typec {
		fusb302_int: fusb302-int {
			rockchip,pins = <0 RK_PB2 RK_FUNC_GPIO &pcfg_pull_up>;
		};
	};

	dsi {
		dsi0_reset: dsi0-reset {
			rockchip,pins = <1 RK_PA4 RK_FUNC_GPIO &pcfg_pull_up>;
		};
	};

	// 2026/4/14新增
	my_peripherals {
	/* MPU6050的中断: GPIO1_A2 */
		mpu6050_irq_pin: mpu6050-irq-pin {
			rockchip,pins =
				<1 RK_PA2 RK_FUNC_GPIO &pcfg_pull_up>;
		};

		/* OLED的DC: GPIO0_C0 */
		oled_dc_pin: oled-dc-pin {
			rockchip,pins =
				<0 RK_PC0 RK_FUNC_GPIO &pcfg_pull_none>;
		};

		/* OLED的RES: GPIO0_B7 */
		oled_res_pin: oled-res-pin {
			rockchip,pins =
				<0 RK_PB7 RK_FUNC_GPIO &pcfg_pull_none>;
		};
	};
};

&i2c3 {
	pinctrl-names = "default";
	pinctrl-0 = <&i2c3m0_xfer>;
	status = "okay";

	mpu6050@68 {
		compatible = "my_mpu6050";
		reg = <0x68>;

		pinctrl-names = "default";
		pinctrl-0 = <&mpu6050_irq_pin>;

		interrupt-parent = <&gpio1>;
		interrupts = <RK_PA2 IRQ_TYPE_EDGE_RISING>;

		status = "okay";
	};
};

&spi0 {
	pinctrl-names = "default";
	pinctrl-0 = <&spi0m1_cs0 &spi0m1_pins>;
	num-cs = <1>;
	status = "okay";

	oled@0 {
		compatible = "my_ecspi_oled";
		reg = <0>;
		spi-max-frequency = <8000000>;

		pinctrl-names = "default";
		pinctrl-0 = <&oled_dc_pin &oled_res_pin>;

		dc-gpios  = <&gpio0 RK_PC0 GPIO_ACTIVE_LOW>;
		res-gpios = <&gpio0 RK_PB7 GPIO_ACTIVE_LOW>;

		status = "okay";
	};
};

&pwm13 {  
	pinctrl-names = "active";
	pinctrl-0 = <&pwm13m1_pins>;
	status = "okay";
};

2.3.6 编译

arch/arm64/boot/dts/rockchip/下面是有Makefile文件的,直接编译试试。

但是直接执行下面这段代码会报错:

bash 复制代码
cd .../kernel  # 进入自己的kernel路径
make ARCH=arm64 rockchip/my_rk3588-lubancat-5-v2.dtb -j$(nproc)  # 这里报错

去看看有没有python。发现有python3,但是没有python:

直接软连接指到python3上:

bash 复制代码
cd /usr/bin/
sudo ln -s /usr/bin/python3 /usr/bin/python

重新编译,还是会报错,提示缺少.config。直接把现在板子上的config薅过来:

bash 复制代码
zcat /proc/config.gz > .config

然后重新执行,可能会提示缺包,装上就星。

bash 复制代码
cd ....../kernel
make ARCH=arm64 rockchip/my_rk3588-lubancat-5-v2.dtb -j$(nproc)

编译成功,但是有个warning。不一定是我修改的代码造成的,先跑着试试,不行了再回来看。

将编译出来的my dtb复制到/boot/dtb/,然后软连接指过去:

bash 复制代码
sudo cp /....../kernel/arch/arm64/boot/dts/rockchip/my_rk3588-lubancat-5-v2.dtb /boot/dtb/
cd /boot/
sudo ln -sf dtb/my_rk3588-lubancat-5-v2.dtb rk-kernel.dtb  # 修改软连接
ls -l            # 看一下软连接是否修改正确
sync             # 数据落盘
sudo reboot      # 重启

三、代码

复制代码
/home/cat/rk_project/
├── driver/
│   ├── Makefile
│   ├── mpu6050.c
│   ├── sg90.c
│   ├── oled.c
│   ├── mpu6050reg.h
│   └── oled.h
└── app/
    ├── Makefile
    └── userAPP.c

3.1 Makefile

Makefile中的KDIR可以先尝试/lib/modules/$(shell uname -r)/build,如果不能用,参照附录B进行配置。

bash 复制代码
obj-m += mpu6050.o
obj-m += sg90.o
obj-m += oled.o

# KDIR := /lib/modules/$(shell uname -r)/build
KDIR := /home/cat/kernel
PWD  := $(shell pwd)

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

3.2 驱动代码

这部分代码都来自下面这三篇(可能有修改,但我已经忘了改哪了)Linux驱动开发笔记(十九)------I2C(AP3216C驱动+MPU6050驱动)
Linux驱动开发笔记(二十)------SPI (ICM20608驱动+0.96寸OLED驱动)_
Linux驱动开发笔记(二十六)------PWM(SG90驱动)

3.2.1 mpu6050.c

mpu6050在笔记十九的代码基础上,增加了等待队列和中断功能。

cpp 复制代码
#include<linux/module.h>
#include<linux/input.h>
#include<linux/i2c.h>
#include<linux/cdev.h>
#include<linux/device.h>
#include<linux/fs.h>
#include<linux/uaccess.h>
#include<linux/string.h>
#include<linux/delay.h>
#include <linux/interrupt.h>
#include <linux/wait.h>
#include"mpu6050reg.h"

#define MPU6050_CNT 1          // 设备数量
#define MPU6050_NAME "mpu6050" // 设备名

/*
 * 后续需要改进的
 * 中断     √
 * 等待队列  √
 * 阻塞read √
 * 环形缓冲
 * poll
 * O_NONBLOCK
 */

// 设备结构体 ===================================================

struct mpu6050_dev_struct {
    dev_t devid;
    int major;
    int minor;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    struct i2c_client *client;

    int irq;                        // 中断号
    wait_queue_head_t r_wait;       // 读等待队列
    atomic_t data_ready;            // 是否有新数据
    // struct mutex lock;              // 保护read期间的设备访问
};
struct mpu6050_dev_struct mpu6050_dev;

// mpu6050寄存器读写函数 ===================================================
// reg:目标寄存器地址  data:保存读取到的信息  len:要读取信息的长度
static int mpu6050_read_reg(struct mpu6050_dev_struct *dev, u8 reg, void *data, int len){
    int err = 0;
    struct i2c_client *client = (struct i2c_client*)dev->client;
    struct i2c_msg msg[2];

    // ①发送:要读取的寄存器的地址
    msg[0].addr = client->addr; // 从机地址
    msg[0].flags = 0;           // 发送
    msg[0].buf = &reg;          // 要发送的数据:目标寄存器地址
    msg[0].len = 1;             // 发送数据的长度

    // ②接收:读到的目标寄存器的数据
    msg[1].addr = client->addr; // 从机地址
    msg[1].flags = I2C_M_RD;    // 接收
    msg[1].buf = data;          // 接收的数据保存到data
    msg[1].len = len;           // 接收数据的长度   

    // 发送msg
    err = i2c_transfer(client->adapter, msg, 2);

    if (err!= 2){
        dev_err(&dev->client->dev,"mpu6050_read_reg fail!\r\n");
        return (err < 0) ? err : -EIO;
    }
    return 0;
}

// reg:目标寄存器地址  buf:要写的数据  len:要写的信息的长度
static int mpu6050_write_reg(struct mpu6050_dev_struct *dev, u8 reg, u8 *buf, int len){
    int err = 0;
    struct i2c_client *client = (struct i2c_client*)dev->client;
    struct i2c_msg msg;
    u8 b[32];

    b[0] = reg;
    memcpy(&b[1], buf, len);

    msg.addr = client->addr;// 从机地址
    msg.flags = 0;          // 发送。flag详见1.4
    msg.buf = b;            // 发送内容:要读取的寄存器地址
    msg.len = len + 1;      // 要发送的数据长度(len) + 寄存器地址长度(1)

    err = i2c_transfer(client->adapter, &msg, 1); // 这里要写成&msg!不是msg
    if(err != 1){
        dev_err(&dev->client->dev,"i2c write bus=%d addr=0x%02x reg=0x%02x err=%d\n",
        dev->client->adapter->nr, dev->client->addr, reg, err);
        return (err < 0) ? err : -EIO;
    }
    return 0;
}

// MPU6050设备初始化
// 要和MPU6050驱动初始化mpu6050_init区分
static int mpu6050_device_init(struct mpu6050_dev_struct *dev){
    int err = 0;
    u8 val;

    val = 0x40;
    err += mpu6050_write_reg(dev, PWR_MGMT_1, &val,1); // 进行休眠。禁用循环唤醒,使能温度计,8MHz振荡器作为时钟源
    val = 0x06;msleep(1);
    err += mpu6050_write_reg(dev, CONFIG, &val,1);     // 加速度/陀螺带宽为5Hz,陀螺输出率为1kHz;关闭FSYNC采样
    val = 0x13;msleep(1);
    err += mpu6050_write_reg(dev, SMPLRT_DIV, &val,1); // 全芯片采样率 = 陀螺仪输出速率 ÷ (该寄存器的值+1) = 1k ÷ 20 = 50Hz
    val = 0x00;msleep(1);
    err += mpu6050_write_reg(dev, ACCEL_CONFIG, &val,1); // 加速度计量程设为±2g,打开加速度计高通滤波
    val = 0x20;
    err += mpu6050_write_reg(dev, INT_PIN_CFG, &val, 1); // 默认高电平有效、推挽、锁存
    val = 0x00;
    err += mpu6050_write_reg(dev, INT_ENABLE, &val, 1);  // 保险起见,这里先不打开中断,open函数里面再开

    if (err < 0){
        dev_err(&dev->client->dev,"mpu6050_device_init fail\n");
        return -1;
    }
    printk("mpu6050_device_init finished!\r\n");
    return 0;
}

// 中断处理函数
static irqreturn_t mpu6050_handler(int irq, void *dev_id){
    struct mpu6050_dev_struct *dev = dev_id;
    u8 status = 0; // 保存INT_STATUS寄存器的数据
    int ret;

    // 读一次INT_STATUS,同时能清除中断状态位,INT变无效电平
    ret = mpu6050_read_reg(dev, INT_STATUS, &status, 1);
    if (ret < 0) {
        dev_err(&dev->client->dev, "read INT_STATUS failed\n");
        return IRQ_HANDLED;
    }

    // 如果DATA_RDY为0
    if (!(status & 0x01))
        return IRQ_NONE;

    atomic_set(&dev->data_ready, 1);
    wake_up_interruptible(&mpu6050_dev.r_wait);
    // printk("t\r\n");
    return IRQ_HANDLED;
}


// 操作集 ===================================================
static int mpu6050_open(struct inode *inode, struct file *filp){
    u8 val = 0;
    int err = 0;
    printk("==mpu6050 open==\r\n");
    filp->private_data = &mpu6050_dev;

    val = 0x00;  // 关闭睡眠,唤醒mpu6050
    err += mpu6050_write_reg(&mpu6050_dev, PWR_MGMT_1, &val,1);
    val = 0x01;
    err += mpu6050_write_reg(&mpu6050_dev, INT_ENABLE, &val, 1);  // 打开DATA_RDY_EN,数据就绪时产生中断

    return err;
}
static int mpu6050_release(struct inode *inode, struct file *filp){
    struct mpu6050_dev_struct *dev = filp->private_data;
    u8 val;
    int ret;
    printk("mpu6050_release\r\n");

    val = 0x00;  // 关闭data ready中断
    mpu6050_write_reg(dev, INT_ENABLE, &val, 1);
    // 清理中断标志
    ret = mpu6050_read_reg(dev, INT_STATUS, &val, 1);
    if (ret < 0) {
        dev_err(&dev->client->dev, "read INT_STATUS failed\n");
        return ret;
    }
    val = 0x40;  // MPU6050休眠
    mpu6050_write_reg(dev, PWR_MGMT_1, &val, 1);

    atomic_set(&dev->data_ready, 0);
    wake_up_interruptible(&dev->r_wait);

    return 0;
}

// 读取6轴信息,并发送给用户态
static ssize_t mpu6050_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off){
    int err;
    short mpu6050_data[6]; //保存mpu6050的原始数据。 共6个16位数据
    struct mpu6050_dev_struct *dev = (struct mpu6050_dev_struct*)filp->private_data;
    // struct i2c_client *client = (struct i2c_client*)dev->client;
    u8 raw[14];  // 连续14个寄存器的原始字节:AxH,AxL,  AyH,AyL,  AzH,AzL,  TempH,TempL,  GxH,GxL,  GyH,GyL,  GzH,GzL

    // size_t to_copy;
    int ret;
    cnt = min_t(size_t, cnt, sizeof(mpu6050_data));// 避免越界
    
    if (filp->f_flags & O_NONBLOCK) { // 如果用户态以非阻塞打开,且数据没准备好
        if (!atomic_read(&dev->data_ready))   // 那么就直接return
            return -EAGAIN;
    } else { // 用户以阻塞打开,设置等待事件
        // printk("mpu6050 read 睡眠\r\n");
        // ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->data_ready));
        // if (ret)
        //     return ret;
        ret = wait_event_interruptible_timeout(dev->r_wait,
                                            atomic_read(&dev->data_ready),
                                            msecs_to_jiffies(200));
        if (ret == 0)
            return -EAGAIN;   /* 超时,无新数据 */
        if (ret < 0)
            return ret;
    }
    // printk("mpu6050 read 被唤醒,取数据ing\r\n");
    ret = mpu6050_read_reg(dev, ACCEL_XOUT_H, raw, sizeof(raw)); // 从ACCEL_XOUT_H开始连续读取14个字节
    if (ret < 0){
        dev_err(&dev->client->dev,"copy to user fail!");
        return ret;
    }
    mpu6050_data[0] = (short)((u16)raw[0]  << 8 | raw[1]);  // Ax
    mpu6050_data[1] = (short)((u16)raw[2]  << 8 | raw[3]);  // Ay
    mpu6050_data[2] = (short)((u16)raw[4]  << 8 | raw[5]);  // Az
    // 跳过中间的温度寄存器
    mpu6050_data[3] = (short)((u16)raw[8]  << 8 | raw[9]);  // Gx
    mpu6050_data[4] = (short)((u16)raw[10] << 8 | raw[11]); // Gy
    mpu6050_data[5] = (short)((u16)raw[12] << 8 | raw[13]); // Gz
    atomic_set(&dev->data_ready, 0); // 数据取走了,清空data_ready标记

    err = copy_to_user(buf, mpu6050_data, cnt); // 发送给用户态
    if(err!=0){
        dev_err(&dev->client->dev,"mpu6050 copy to user fail!\r\n");
        return -EFAULT;
    }
    return sizeof(mpu6050_data);
}

static const struct file_operations mpu6050_fops = {
    .owner = THIS_MODULE,
    .open = mpu6050_open,
    .release = mpu6050_release,
    .read = mpu6050_read, 
};

// 匹配表 ===================================================

static struct i2c_device_id mpu6050_id[] = {
    {"mpu6050"},
    {/*sentinel*/},
};
static struct of_device_id mpu6050_of_match[] = {
    {.compatible = "my_mpu6050"},
    {/*sentinel*/},
};

// 总线设备结构体 ===================================================
static int mpu6050_probe(struct i2c_client *client, const struct i2c_device_id *id){
    int ret = 0;
    // u8 int_status;
    printk("[mpu6050] probe !\r\n");

    mpu6050_dev.major = 0; // 待系统分配

    // 设备号
    if(mpu6050_dev.major){ // 自定义设备号
        mpu6050_dev.devid = MKDEV(mpu6050_dev.major,0);
        ret = register_chrdev_region(mpu6050_dev.devid, MPU6050_CNT, MPU6050_NAME);
    } else { // 系统分配设备号
        ret = alloc_chrdev_region(&mpu6050_dev.devid, 0, MPU6050_CNT, MPU6050_NAME);
        mpu6050_dev.major = MAJOR(mpu6050_dev.devid);
        mpu6050_dev.minor = MINOR(mpu6050_dev.devid);        
    }
    if(ret < 0){
        dev_err(&client->dev,"chrdev region fail!\r\n");
        goto fail_devid;
    }
    printk("major = %d, minor = %d", mpu6050_dev.major, mpu6050_dev.minor);

    // 注册字符设备cdev
    mpu6050_dev.cdev.owner = THIS_MODULE;
    cdev_init(&mpu6050_dev.cdev, &mpu6050_fops);
    ret = cdev_add(&mpu6050_dev.cdev, mpu6050_dev.devid, MPU6050_CNT);
    if(ret < 0){
        dev_err(&client->dev,"cdev add fail!\r\n");
        goto fail_cdev;
    }


    // 
    mpu6050_dev.client = client;
    mpu6050_dev.irq = client->irq;

    init_waitqueue_head(&mpu6050_dev.r_wait);// 等待队列
    atomic_set(&mpu6050_dev.data_ready, 0);  // 数据就绪标志位
    // mutex_init(&mpu6050_dev.lock);  // 锁

    i2c_set_clientdata(client, &mpu6050_dev);

    if (mpu6050_dev.irq <= 0) {
        dev_err(&client->dev, "no valid irq found, check device tree\r\n");
        ret = -EINVAL;
        goto fail_mpu_init;
    }

    // 申请线程化中断
    ret = request_threaded_irq(mpu6050_dev.irq,
                               NULL,
                               mpu6050_handler,
                               IRQF_ONESHOT,
                               "mpu6050",
                               &mpu6050_dev);
    if (ret) {
        dev_err(&client->dev, "request irq %d failed\r\n", mpu6050_dev.irq);
        goto fail_mpu_init;
    }        
    // 初始化mpu6050寄存器,最后再打开mpu6050和中断
    ret = mpu6050_device_init(&mpu6050_dev);
    if(ret){
        dev_err(&client->dev,"mpu6050 init fail!\r\n");
        goto fail_request_irq;
    }

    // class
    mpu6050_dev.class = class_create(THIS_MODULE, MPU6050_NAME);
    if(IS_ERR(mpu6050_dev.class)){
        dev_err(&client->dev,"mpu6050 class_create fail!\r\n");
        ret = PTR_ERR(mpu6050_dev.class);
        goto fail_class_create;
    }
    // device
    mpu6050_dev.device = device_create(mpu6050_dev.class,NULL,mpu6050_dev.devid, NULL, MPU6050_NAME);
    if(IS_ERR(mpu6050_dev.device)){
        dev_err(&client->dev,"mpu6050 create fail!\r\n");
        ret = PTR_ERR(mpu6050_dev.device);
        goto fail_device_create;
    }

    return 0;

    
fail_device_create:
    class_destroy(mpu6050_dev.class);
fail_class_create:
fail_request_irq:
    free_irq(mpu6050_dev.irq, &mpu6050_dev);
fail_mpu_init:
    cdev_del(&mpu6050_dev.cdev);
fail_cdev: 
    unregister_chrdev_region(mpu6050_dev.devid, MPU6050_CNT);
fail_devid:
    return ret;
}

static int mpu6050_remove(struct i2c_client *client){
    // u8 val = 0x00; // 先关闭mpu6050的中断
    // mpu6050_write_reg(&mpu6050_dev, INT_ENABLE, &val, 1);
    free_irq(mpu6050_dev.irq, &mpu6050_dev);
    device_destroy(mpu6050_dev.class, mpu6050_dev.devid);
    class_destroy(mpu6050_dev.class);
    cdev_del(&mpu6050_dev.cdev);
    unregister_chrdev_region(mpu6050_dev.devid, MPU6050_CNT);
    return 0;
}

static struct i2c_driver mpu6050_driver = {
    .probe = mpu6050_probe,
    .remove = mpu6050_remove,
    .driver = {
        .owner = THIS_MODULE,
        .name = "mpu6050",
        .of_match_table = of_match_ptr(mpu6050_of_match),
    },
    .id_table = mpu6050_id,
};

// 驱动入口/出口 ===================================================
static int __init mpu6050_init(void){
    return i2c_add_driver(&mpu6050_driver);
}

static void __exit mpu6050_exit(void){
    i2c_del_driver(&mpu6050_driver);
}

module_init(mpu6050_init);
module_exit(mpu6050_exit);
MODULE_LICENSE("GPL");

3.2.2 oled.c

话说这个驱动在imx6ull上面跑的时候像素乱飞,在rk3588上反而好了,估计是imx6ull供电不够

cpp 复制代码
#include<linux/module.h>
#include<linux/input.h>
#include<linux/spi/spi.h>
#include<linux/cdev.h>
#include<linux/device.h>
#include<linux/fs.h>
#include<linux/uaccess.h>
#include<linux/string.h>
#include<linux/delay.h>
#include<linux/gpio.h>
#include<linux/of_gpio.h>
#include "oled.h"

#define OLED_CNT 1
#define OLED_NAME "oled"
struct oled_dev{
    int major;
    int minor;
    dev_t id;
    struct cdev cdev;
    struct class *class;
    struct device *device;   
    void *private_data;      // spi设备指针
    struct device_node *nd;  // 设备树节点

    int res_gpio;
    int dc_gpio;
};
static struct oled_dev oleddev;


//// OLED操作函数 /////////////////////////////////////////////////////////////////////////////

// is_data为False发送命令,为True发送数据
static int oled_send(struct oled_dev *dev, u8 buf, int len, bool is_data){
    int ret = 0;
    struct spi_device *spi = (struct spi_device *)dev->private_data;

    // 拉低DC脚发送命令,拉高DC发送数据
    gpio_direction_output(oleddev.dc_gpio, is_data ? 1 : 0);

    ret = spi_write(spi, &buf, len);

    if (ret != 0){
        pr_err("oled_send error! \r\n");
        return -1;
    }
    return ret;
}

//清屏
//水平寻址方式?
// void oled_clear(void){
//     int i = 0;
//     oled_send(&oleddev, 0xA4, 1, false);// 取消 整屏点亮

//     oled_send(&oleddev, 0x20, 1, false);// 
//     oled_send(&oleddev, 0x02, 1, false);// 设置为页寻址

//     oled_send(&oleddev, 0x21, 1, false);// 
//     oled_send(&oleddev, 0x00, 1, false);// 
//     oled_send(&oleddev, 0x7F, 1, false);// 设置列地址范围0~127(详见原厂芯片手册10.1.4)

//     oled_send(&oleddev, 0x22, 1, false);// 
//     oled_send(&oleddev, 0x00, 1, false);// 
//     oled_send(&oleddev, 0x07, 1, false);// 设置页地址范围0~7(详见原厂芯片手册10.1.5)

//     for (i = 0; i < 128 * 8; ++i) {
//         oled_send(&oleddev, 0x00, 1, true);// 这里应该需要水平寻址才行吧?
//     }

//     // 将PAGE指针和列指针都置0,不置0也行
//     oled_send(&oleddev, 0xB0, 1, false);
//     oled_send(&oleddev, 0x00, 1, false);
//     oled_send(&oleddev, 0x10, 1, false);
// }
void oled_clear(void){
    int page, col;

    oled_send(&oleddev, 0xA4, 1, false);// 取消 整屏点亮

    oled_send(&oleddev, 0x20, 1, false);// 
    oled_send(&oleddev, 0x02, 1, false);// 设置为页寻址

    for (page = 0; page < 8; page++) {
        oled_send(&oleddev, 0xB0 + page, 1, false); // 选择当前页.0xB0~0xB7表示8个PAGE
        oled_send(&oleddev, 0x00, 1, false);        // 设置列地址低4位为0
        oled_send(&oleddev, 0x10, 1, false);        // 设置列地址高4位为0,即从第0列开始

        for (col = 0; col < 128; col++) {
            oled_send(&oleddev, 0x00, 1, true);     // 当前页的128列全部写0
        }
    }

    // 清屏结束后,把页地址和列地址恢复到左上角
    oled_send(&oleddev, 0xB0, 1, false);            // 回到第0页
    oled_send(&oleddev, 0x00, 1, false);            // 列低4位清0
    oled_send(&oleddev, 0x10, 1, false);            // 列高4位清0
}

// 页寻址,列范围0~127,页范围0~7。
// 字符为8*16字符
// xy为左上角坐标
void oled_show_char8x16(uint8_t x, uint8_t y, char ch){
    uint8_t page;
    const unsigned char *pfont;
    int i;

    if (x > 120)return;   // x最大=128-8
    if (y > 48)return;    // y最大=64-16
    if (y % 8 != 0)return;// 只支持按页对齐。也就是字符必须在两个页中,不能穿过3个页

    page = y / 8;

    if (ch < ' ' || ch > '~') // 字库只有这些字符
        ch = ' ';   // 如果是未定义字符,直接输出空格
    pfont = asc2_1608[(uint8_t)ch - ' '];//在字库数组的下标索引

    // 画上半部分(上8行)========================================
    oled_send(&oleddev, 0xB0 + page, 1, false);              // 设置页地址
    oled_send(&oleddev, 0x00 + (x & 0x0F), 1, false);        // 设置列低4位
    oled_send(&oleddev, 0x10 + ((x >> 4) & 0x0F), 1, false); // 设置列高4位

    for (i = 0; i < 8; i++) {
        oled_send(&oleddev, pfont[i], 1, true);
    }

    // 画下半部分(下8行)========================================
    oled_send(&oleddev, 0xB0 + page + 1, 1, false);
    oled_send(&oleddev, 0x00 + (x & 0x0F), 1, false);
    oled_send(&oleddev, 0x10 + ((x >> 4) & 0x0F), 1, false);

    for (i = 0; i < 8; i++) {
        oled_send(&oleddev, pfont[i + 8], 1, true);
    }
}

void oled_show_string8x16(uint8_t x, uint8_t y, const char *str){
    oled_send(&oleddev, 0xA4, 1, false);// 取消 整屏点亮
    oled_send(&oleddev, 0x20, 1, false);// 
    oled_send(&oleddev, 0x02, 1, false);// 设置为页寻址

    while (*str) {
        // 如果已经到屏幕右边了,显示不下,退出
        if (x > 120) {
            break;
        }
        oled_show_char8x16(x, y, *str);
        x += 8;
        str++;
    }
}

// 初始化
void oled_dev_init(void){
    // 具体的配置直接在原厂芯片手册pdf中搜索"命令+h"即可,如搜索"AEh"即可找到开关显示的部分

    gpio_direction_output(oleddev.res_gpio, 0); mdelay(2);// RES拉低
    gpio_direction_output(oleddev.res_gpio, 1); mdelay(2);// RES拉高

    oled_send(&oleddev, 0xAE, 1, false);mdelay(2);// 关显示

    oled_send(&oleddev, 0xD5, 1, false);mdelay(2);// 
    oled_send(&oleddev, 0x80, 1, false);mdelay(2);// 设置显示时钟分频比/震荡频率

    oled_send(&oleddev, 0xA8, 1, false);mdelay(2);// 
    oled_send(&oleddev, 0x3F, 1, false);mdelay(2);// 复用比

    oled_send(&oleddev, 0x40, 1, false);mdelay(2);// 设置起始行(0行)

    oled_send(&oleddev, 0x8D, 1, false);mdelay(2);// 
    oled_send(&oleddev, 0x14, 1, false);mdelay(2);// 打开电荷泵

    oled_send(&oleddev, 0xA1, 1, false);mdelay(2);// 设置段重映射(用于左右翻转)
    oled_send(&oleddev, 0xC8, 1, false);mdelay(2);// 设置COM扫描方向(用于上下翻转)

    oled_send(&oleddev, 0xDA, 1, false);mdelay(2);// 
    oled_send(&oleddev, 0x12, 1, false);mdelay(2);// 设置COM引脚硬件配置

    oled_send(&oleddev, 0x81, 1, false);mdelay(2);// 
    oled_send(&oleddev, 0x7F, 1, false);mdelay(2);// 设置对比度(推荐值66,嫌不够亮可以调高,最大0xFF)

    oled_send(&oleddev, 0xD9, 1, false);mdelay(2);// 
    oled_send(&oleddev, 0x22, 1, false);mdelay(2);// 预充电时间(像素从上一帧状态变成下一帧状态用几个时钟周期)

    oled_send(&oleddev, 0xDB, 1, false);mdelay(2);// 
    oled_send(&oleddev, 0x30, 1, false);mdelay(2);// 设置 VCOMH 取消选通电平

    oled_send(&oleddev, 0xA4, 1, false);mdelay(2);// 取消 整屏点亮(若为0xA5,则忽略RAM内容,整屏点亮)(详见原厂芯片手册10.1.9)

    oled_send(&oleddev, 0xA6, 1, false);mdelay(2);// 正常(A6)/反相(A7)显示

    oled_send(&oleddev, 0x20, 1, false); mdelay(2);
    oled_send(&oleddev, 0x02, 1, false); mdelay(2);   // 页寻址

    oled_send(&oleddev, 0xAF, 1, false);mdelay(2);// 开启显示

    // oled_send(&oleddev, 0xA5, 1, false);mdelay(1000); // 调试用的
    // oled_send(&oleddev, 0xA4, 1, false);mdelay(2);

    oled_clear();
    oled_show_string8x16(20,16,"hello world");
    pr_info("OLED init finished!\r\n");
    mdelay(1000);
    oled_clear();
}



//// 设备操作集函数 /////////////////////////////////////////////////////////////////////////////

static int oled_open(struct inode *inode, struct file* filp){
    filp->private_data = &oleddev;
    return 0;
}
static int oled_release(struct inode *inode, struct file* filp){
    printk("oled_release\r\n");
    return 0;
}
static ssize_t oled_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off){
    // struct oled_dev *dev = (struct oled_dev*)filp->private_data;
    return 0;
}

static ssize_t oled_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos){
    struct oled_msg msg;
    size_t len;
    memset(&msg, 0, sizeof(msg));

    if(count >= sizeof(msg)) len = sizeof(msg);
    else len = count;

    if (copy_from_user(&msg, buf, len))
        return -EFAULT;
    msg.str[OLED_MAX_LEN-1] = '\0';

    oled_show_string8x16(msg.x,msg.y,msg.str);

    return len;
}
static const struct file_operations oled_fops = {
    .owner = THIS_MODULE,
    .open = oled_open,
    .release = oled_release,
    .read = oled_read,
    .write = oled_write,
};

//spi驱动///////////////////////////////////////////////

static int oled_probe(struct spi_device* spi){
    int ret = 0;

    pr_info("probe\r\n");
    // 1、devid
    oleddev.major = 0;
    if(oleddev.major){ // 已给定主设备号
        oleddev.id = MKDEV(oleddev.major,0);
        ret = register_chrdev_region(oleddev.id, OLED_CNT, OLED_NAME);
    } else {  // 系统分配主设备号
        ret = alloc_chrdev_region(&oleddev.id, 0, OLED_CNT, OLED_NAME);
        oleddev.major = MAJOR(oleddev.id);
        oleddev.minor = MINOR(oleddev.id);
    }
    if(ret < 0){
        pr_err("chrdev region error!\r\n");
        goto fail_devid;
    }
    pr_info("major = %d, minor = %d\r\n", oleddev.major, oleddev.minor);

    // 2、cdev
    oleddev.cdev.owner = THIS_MODULE;
    cdev_init(&oleddev.cdev, &oled_fops);
    ret = cdev_add(&oleddev.cdev, oleddev.id, OLED_CNT);
    if(ret < 0){
        pr_err("cdev error!\r\n");
        goto fail_cdev;
    }

    // 3、class
    oleddev.class = class_create(THIS_MODULE, OLED_NAME);
    if(IS_ERR(oleddev.class)){
        pr_err("class error!\r\n");
        goto fail_class;
    }

    // 4、device
    oleddev.device = device_create(oleddev.class, NULL, oleddev.id, NULL, OLED_NAME);
    if(IS_ERR(oleddev.device)){
        pr_err("device error!\r\n");
        goto fail_device;
    }

    // 5、设备结构体私有数据
    oleddev.private_data = spi;

    // 6、spi设备初始化
    spi->mode = SPI_MODE_0;
    spi_setup(spi);

    // 7、申请GPIO
    // 先找device node
    // 然后获取GPIO号
    oleddev.nd = spi->dev.of_node; // 也可用of_get_parent
    if (!oleddev.nd) {
        pr_err("ecspi3 node not found\r\n");
        goto fail_find_node;
    }

    // dc gpio
    oleddev.dc_gpio = of_get_named_gpio(oleddev.nd, "dc-gpios", 0);
    if (!gpio_is_valid(oleddev.dc_gpio)) {
        pr_err("dc-gpios invalid: %d\r\n", oleddev.dc_gpio);
        ret = oleddev.dc_gpio;
        goto fail_get_dc_gpio;
    }
    ret = gpio_request(oleddev.dc_gpio, "dc_gpio");
    if(ret < 0){
        pr_err("dc-gpios request error!\r\b");
        goto fail_request_dc_gpio;
    }

    // res gpio
    oleddev.res_gpio = of_get_named_gpio(oleddev.nd, "res-gpios", 0);
    if (!gpio_is_valid(oleddev.res_gpio)) {
        pr_err("res-gpios invalid: %d\r\n", oleddev.res_gpio);
        ret = oleddev.res_gpio;
        goto fail_get_res_gpio;
    }
    ret = gpio_request(oleddev.res_gpio, "res_gpio");
    if(ret < 0){
        pr_err("dc-gpios request error!\r\n");
        goto fail_request_res_gpio;
    }


    // 8、oled初始化
    oled_dev_init();

    return 0;


fail_request_res_gpio:
fail_get_res_gpio:
    gpio_free(oleddev.dc_gpio);
fail_request_dc_gpio:
fail_get_dc_gpio:

fail_find_node:
    device_destroy(oleddev.class, oleddev.id);
fail_device:
    class_destroy(oleddev.class);
fail_class:
    cdev_del(&oleddev.cdev);
fail_cdev:
    unregister_chrdev_region(oleddev.id, OLED_CNT);
fail_devid:
    return ret;
}

static int oled_remove(struct spi_device* spi){
    oled_clear();
    oled_send(&oleddev, 0xAE, 1, false);  mdelay(2); // display off
    gpio_free(oleddev.dc_gpio);
    gpio_free(oleddev.res_gpio);
    device_destroy(oleddev.class, oleddev.id);
    class_destroy(oleddev.class);
    cdev_del(&oleddev.cdev);
    unregister_chrdev_region(oleddev.id, OLED_CNT);
    return 0;
}

// 传统匹配
static const struct spi_device_id oled_id[] = {
    {"ecspi_oled", 0},
    {  },
};
// 设备树匹配
static const struct of_device_id oled_of_match[] = {
    {.compatible = "my_ecspi_oled", 0},
    {  },
};

struct spi_driver oled_driver = {
    .probe = oled_probe,
    .remove = oled_remove,
    .driver = {
        .name = "ecspi_oled",
        .owner = THIS_MODULE,
        .of_match_table = oled_of_match,
    },
    .id_table = oled_id,
};

//////////////////////////////////////////////////

// 驱动入口
static int __init oled_init(void){
    int ret = 0;
    pr_info("oled init\r\n");
    ret = spi_register_driver(&oled_driver);
    return ret;
}

// 驱动出口
static void __exit oled_exit(void){
    pr_info("oled exit\r\n");
    spi_unregister_driver(&oled_driver);
}

module_init(oled_init);
module_exit(oled_exit);
MODULE_LICENSE("GPL");

3.2.3 sg90.c

sg90这里有坑啊:

probe成功了,结果舵机不动,旁边也没示波器。

在imx6ull中,如果不配置极性,默认是正常。而是在LBC5中,如果不配置极性,默认竟然是反相↓

尝试修改了设备树中pwms = <&pwm13 0 20000000 0>;最后这个0,依然是反相

然后在驱动中使用pwm_set_polarity,依然是implicit declaration of function 'pwm_set_polarity'

于是sg90.c中的probe和write中不再使用pwm_config,改用pwm_apply_state。极性正常:

cpp 复制代码
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/pwm.h>
#include <linux/uaccess.h>


#define PLATFORM_NAME "sg90" // 设备名
#define PLATFORM_COUNT 1     // 设备数量

#define SG90_PERIOD_NS   20000000  // PWM周期20ms
#define SG90_MIN_NS      500000    // PWM高电平最小0.5ms
#define SG90_MAX_NS      2500000   // PWM高电平最大2.5ms
/////////////////////////////////////////////////////

/* 设备结构体 */
struct sg90_struct{
    dev_t devid;            /* 设备号 */
    int major;              /* 主设备号 */
    int minor;              /* 此设备号 */
    struct cdev cdev;
    struct class *class;    /* 类 */
    struct device *device;  /* 设备 */
    struct device_node *nd;
    struct pwm_device *pwm_dev;
};
struct sg90_struct sg90dev;

/////////////////////////////////////////////////////
static ssize_t sg90_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos){
    long int duty_ns;
    int ret;
    int angle; // 因为没法用浮点数。这里的angle接收到的值是实际值的100倍
    struct sg90_struct *dev = filp->private_data;
    struct pwm_state state;

    if(copy_from_user(&angle, buf, sizeof(angle)))return -EFAULT;
    if(angle < 0) angle = 0;
    else if(angle > 18000) angle = 18000;
    // printk("==angle:%d==\r\n",(int)(angle));
    duty_ns = (int)(SG90_MIN_NS + (SG90_MAX_NS - SG90_MIN_NS)/100 * angle / 180); // 这里先除以100,再和angle乘,否则会溢出,我真是找了半天的问题才发现原来是这里溢出啊了,之前angle没有乘100刚好在int范围内。不过把duty_ns改成long应该也可以
    // ret = pwm_config(dev->pwm_dev, duty_ns, SG90_PERIOD_NS);
    // // printk("==%d==\r\n",(int)(duty_ns));
    // if(ret < 0){
    //     pr_err("fail_pwm_config!\r\n");
    //     return ret;
    // }

    pwm_get_state(dev->pwm_dev, &state);
    state.period = SG90_PERIOD_NS;
    state.duty_cycle = duty_ns;
    state.polarity = PWM_POLARITY_NORMAL;
    state.enabled = 1;

    ret = pwm_apply_state(dev->pwm_dev, &state);
    if (ret < 0) {
        pr_err("fail_pwm_apply_state!\n");
        return ret;
    }
    return count;
}

static int sg90_open(struct inode *inode, struct file *filp){
    filp->private_data = &sg90dev;
    return 0;
}
static int sg90_release(struct inode *inode, struct file *filp){
    printk("sg90_release\r\n");
    return 0;
}
/* 字符设备操作集合 */
static const struct file_operations sg90_fops = {
    .owner = THIS_MODULE,
    .write = sg90_write,
    .open = sg90_open,
    .release = sg90_release,
};
 
///////////////////////////////////////////////////////
static int sg90_probe(struct platform_device *dev){
    int ret = 0;
    // platform_device类型中的device结构体成员中已经包括了of_node(device_node类型)
    // 可以直接读取,不需要再手动寻找of_find_node_by_path
    struct device_node *node = dev->dev.of_node;  // 设备树
    struct pwm_state state;


    pr_info("sg90 probe\r\n");
    /* 1.注册字符设备驱动 */
    sg90dev.devid = 0;
    ret = alloc_chrdev_region(&sg90dev.devid, 0, PLATFORM_COUNT, PLATFORM_NAME);
    if(ret<0){
        pr_err("fail_chrdev!\n");
        goto fail_chrdev;
    }
    sg90dev.major = MAJOR(sg90dev.devid);
    sg90dev.minor = MINOR(sg90dev.devid);

    /* 2.初始化cdev */
    sg90dev.cdev.owner = THIS_MODULE;  
    cdev_init(&sg90dev.cdev, &sg90_fops);
    /* 3.添加cdev */
    ret = cdev_add(&sg90dev.cdev, sg90dev.devid, PLATFORM_COUNT);
    if(ret<0){
        pr_err("fail_add_cdev!\n");
        goto fail_add_cdev;
    }
    /* 4.创建类 */
    sg90dev.class = class_create(THIS_MODULE, PLATFORM_NAME);
    if(IS_ERR(sg90dev.class)){
        pr_err("fail_create_class!\n");
        ret = PTR_ERR(sg90dev.class);
        goto fail_create_class;
    }
    /* 5.创建设备 */
    sg90dev.device = device_create(sg90dev.class, NULL, sg90dev.devid, NULL, PLATFORM_NAME);
    if(IS_ERR(sg90dev.device)){
        pr_err("fail_create_device!\n");
        ret = PTR_ERR(sg90dev.device);
        goto fail_create_device;
    }
    sg90dev.nd = dev->dev.of_node; 


    sg90dev.pwm_dev = devm_of_pwm_get(&dev->dev, node, NULL);
    if (IS_ERR(sg90dev.pwm_dev)){
        ret = PTR_ERR(sg90dev.pwm_dev);
        pr_err("fail_get_pwm! ret=%d\n", ret);
        goto fail_pwm;
    }
    // ret = pwm_config(sg90dev.pwm_dev, 1500000, 20000000); // 周期20ms,高电平时间0.5ms,舵机角度0
    // if(ret < 0){
    //     pr_err("fail_config_pwm!\n");
    //     goto fail_pwm;
    // }
    // ret = pwm_enable(sg90dev.pwm_dev);
    // if(ret < 0){
    //     pr_err("fail_enable_pwm!\n");
    //     goto fail_pwm;
    // }  
    pwm_init_state(sg90dev.pwm_dev, &state);
    state.period = SG90_PERIOD_NS;
    state.duty_cycle = 1500000;   // 中位,1.5ms
    state.polarity = PWM_POLARITY_NORMAL;
    state.enabled = 1;

    ret = pwm_apply_state(sg90dev.pwm_dev, &state);
    if (ret < 0) {
        pr_err("fail_apply_pwm_state!\n");
        goto fail_pwm;
    }
    printk("sg90 probe finished!\n");
    return 0;
    

fail_pwm:
    device_destroy(sg90dev.class,sg90dev.devid);
fail_create_device:
    class_destroy(sg90dev.class);
fail_create_class:
    cdev_del(&sg90dev.cdev);
fail_add_cdev:
    unregister_chrdev_region(sg90dev.devid, PLATFORM_COUNT);
fail_chrdev:
    return ret;
}
 
 
static int sg90_remove(struct platform_device * dev){
    pr_info("sg90 remove\r\n");
 
    pwm_disable(sg90dev.pwm_dev);
    device_destroy(sg90dev.class, sg90dev.devid);
    class_destroy(sg90dev.class);
    cdev_del(&sg90dev.cdev);
    unregister_chrdev_region(sg90dev.devid, PLATFORM_COUNT);
    return 0;
}

static const struct of_device_id sg90_of_match[] = {
    {.compatible = "my_sg90"}, // 要和新增的设备数节点gpio中的compatible匹配
    { /* sentinel */ }, // 哨兵元素
    // 后面可以接更多的{.compatible = "............"}
};

// platform驱动结构体
static struct platform_driver sg90_driver = {
    .driver = {
        .name = "sg90", // 无设备树时使用name与设备进行匹配
        .of_match_table = sg90_of_match, // 有设备树时进行匹配
    },
    .probe = sg90_probe,
    .remove = sg90_remove,
};

///////////////////////////////////////////////////////////////////

// 加载驱动
static int __init sg90driver_init(void){
    return platform_driver_register(&sg90_driver);
}
 
// 卸载驱动
static void __exit sg90driver_exit(void){
    platform_driver_unregister(&sg90_driver);
}

module_init(sg90driver_init);
module_exit(sg90driver_exit);
MODULE_LICENSE("GPL");

3.3 用户态代码userAPP

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <stdbool.h>
#include <math.h>
#include <pthread.h>
#include "../driver/oled.h"

#define DEV_IMU   "/dev/mpu6050"
#define DEV_SG90  "/dev/sg90"
#define DEV_OLED  "/dev/oled"

// #define IMU_POLL_INTERVAL_US 10000  // IMU线程轮询周期:100ms
#define OLED_BUF_SIZE 128  // OLED缓冲区
#define PI 3.1415926535    // π


static volatile sig_atomic_t g_stop = 0;

/*
 * mpu6050传过来的数据,目前来说只用一个角度即可
 */
struct mpu6050_data {
    float pitch;
};

/*
 * 三个线程都需要的共享数据
 */
struct shared_state {
    float pitch;          // 陀螺仪输出的角度
    float sg90_angle;       // 舵机的目标角度

    bool oled_data_ready;  // 用于oled的标志
    bool sg90_data_ready;  // 用于舵机的标志
    bool exit_flag;        // 是否退出。如果按下ctrl+c则修改这个值
    // bool竟然是在<stdbool.h>里面的,要长脑子了

    pthread_mutex_t lock; // 互斥锁
    pthread_cond_t cond;  // 条件变量,用于唤醒SG90和OLED线程
};

/*********
 * 线程参数
 * 为了简单,把设备fd和共享状态都塞进来
 **********/
struct thread_args {
    int fd_imu;
    int fd_sg90;
    int fd_oled;
    struct shared_state *state;
};

/* 全局共享状态指针,方便 signal handler 设置退出标志 */
static struct shared_state *g_state = NULL;

/*
 * 把陀螺仪获得的pitch角度转为为舵机目标角度
 * 输入pitch角度(范围限定在-90~90°,毕竟舵机只有180°的范围)
 * 返回舵机目标角度float
 */
static float pitch_to_sg90_angle(float pitch){
    float angle;
    angle = pitch + 90.0f; 
    if (angle > 180.0f) angle = 180.0f;
    if (angle < 0.0f) angle = 0.0f;
    return angle;
}

/*
 * SIGINT 信号处理函数,现在只有退出功能
 * 当Ctrl+C会进入这里
 * 现在只负责退出
 * 1. 设置退出标志
 * 2. 唤醒正在等待条件变量的线程
 */
static void sigint_handler(int signo){
    (void)signo;
    g_stop = 1;   // 只做这一个动作
}

/*
 * mpu6050原始数据转真实数据
 * raw[0] = Ax
 * raw[1] = Ay
 * raw[2] = Az
 * raw[3] = Gx
 * raw[4] = Gy
 * raw[5] = Gz  温度不要了
 * return:pitch,单位为度
 * 1. 假设加速度量程±2g,比例因子是16384LSB/g
 * 2. 目前只计算了pitch角度,后面再加
 * 3. 公式为:pitch = atan2(-Ax, sqrt(Ay*Ay + Az*Az))
 */
double mpu6050_change(const short raw[6]){
    double ax, ay, az;
    double pitch;

    // 原始值转成实际值
    ax = raw[0] / 16384.0;
    ay = raw[1] / 16384.0;
    az = raw[2] / 16384.0;

    // 计算 pitch,结果先是弧度,再转角度
    pitch = atan2(-ax, sqrt(ay * ay + az * az)) * 180.0 / PI;

    return pitch;
}

/*
 * mpu6050的线程
 * 1. 轮询(取消了,现在有了等待队列,没有中断信号时会在read中睡眠)
 * 2. read得到角度
 * 3. 调用pitch_to_sg90_angle()得到舵机目标角度
 * 4. 上锁,更新共享状态
 * 5. 设置 data_ready = true
 * 6. 修改shared_state.cond,唤醒SG90/OLED线程
 */
static void *imu_thread_func(void *arg){
    struct thread_args *targs = (struct thread_args *)arg;
    struct shared_state *state = targs->state;
    struct mpu6050_data data;
    ssize_t ret;
    float angle;
    short mpu6050_raw_data[6];
    memset(&data, 0, sizeof(data));
    memset(&mpu6050_raw_data, 0, sizeof(mpu6050_raw_data));

    printf("[IMU] created!\r\n");

    while (1) {
        
        pthread_mutex_lock(&state->lock);
        if (state->exit_flag) {  // 如果ctrl+C
            pthread_mutex_unlock(&state->lock);
            break;
        }
        pthread_mutex_unlock(&state->lock);

        // 调用mpu6050的read,读取原始角度数据
        ret = read(targs->fd_imu, &mpu6050_raw_data, sizeof(mpu6050_raw_data));
        if (ret < 0) {
            // // 如果是因为 Ctrl+C (SIGINT) 等信号打断了阻塞的 read
            // if (errno == EINTR) {
            //     continue; // 直接进行下一次循环,马上就会检测到 exit_flag 为 1 并退出
            // }
            perror("MPU6050 thread read /dev/imu failed");
            continue;
        }
        data.pitch=mpu6050_change(mpu6050_raw_data);
        angle = pitch_to_sg90_angle(data.pitch);// 计算舵机目标角度

        pthread_mutex_lock(&state->lock);// 读写共享状态时要加锁
        state->pitch = data.pitch;
        state->sg90_angle = angle;
        state->oled_data_ready = true;
        state->sg90_data_ready = true;

        // 唤醒等待数据的线程,也就是oled和sg90
        pthread_cond_broadcast(&state->cond);

        pthread_mutex_unlock(&state->lock);

        printf("[IMU] pitch=%.2f, servo_angle=%f\r\n",data.pitch, angle);
    }
    printf("[IMU] exit!\r\n");
    return NULL;
}

/*
 * SG90线程
 * 1.没有新数据就睡眠
 * 2.被唤醒后,读取数据
 * 3.写给/dev/sg90驱动
 */
static void *sg90_thread_func(void *arg){
    struct thread_args *targs = (struct thread_args *)arg;
    struct shared_state *state = targs->state;
    int angle;
    ssize_t ret;

    printf("[SG90] created!\r\n");

    while (1) {
        // 因为state->data_ready、state->exit_flag随时会变,因此要先上锁
        pthread_mutex_lock(&state->lock);

        // 没有新数据且还没Ctrl+C,就睡眠
        // 有时候可能会被误唤醒,所以要用while而不是if
        while (!state->sg90_data_ready && !state->exit_flag) {
            pthread_cond_wait(&state->cond, &state->lock);
            // wait会先释放锁
        }
        /* 如果退出,就解锁并结束线程 */
        if (state->exit_flag) {
            pthread_mutex_unlock(&state->lock);
            break;
        }
        angle = (int)(state->sg90_angle * 100); // 因为内核态没法用float,这里直接乘100,然后转int
        state->sg90_data_ready = false;

        pthread_mutex_unlock(&state->lock);

        /* 把角度写给 SG90 驱动 */
        ret = write(targs->fd_sg90, &angle, sizeof(angle));
        if (ret < 0) {
            perror("[SG90] write sg90 failed");
            continue;
        }
        printf("[SG90] %.2f\r\n",state->sg90_angle);
    }
    printf("[SG90] exit\r\n");
    return NULL;
}

/*
 * OLED线程
 * 1. 没有新数据就等待
 * 2. 被唤醒后,读取最新角度数据
 * 3. 转为字符串
 * 4. 写给驱动oled
 */
static void *oled_thread_func(void *arg){

    struct thread_args *targs = (struct thread_args *)arg;
    struct shared_state *state = targs->state;
    float pitch;
    int angle;
    ssize_t ret;
    struct oled_msg msg;

    // 首先写上固定不变的字符,免得后面要刷新的字符太多降低速度
    msg.x = 0, msg.y = 0, strcpy(msg.str,"imu =");
    ret = write(targs->fd_oled, &msg, sizeof(msg));
    if (ret < 0) {
        perror("[OLED] write oled failed");
    }
    msg.x = 0, msg.y = 16, strcpy(msg.str,"sg90=");
    ret = write(targs->fd_oled, &msg, sizeof(msg));
    if (ret < 0) {
        perror("[OLED] write oled failed");
    }

    while (1) {
        pthread_mutex_lock(&state->lock);

        while (!state->oled_data_ready && !state->exit_flag) {
            pthread_cond_wait(&state->cond, &state->lock);
        }
        if (state->exit_flag) { // 如果Ctrl+C
            pthread_mutex_unlock(&state->lock);
            break;
        }

        // 取出最新共享状态:舵机输出角度和mpu6050输出角度
        pitch = state->pitch;
        angle = state->sg90_angle;
        state->oled_data_ready = false;
        pthread_mutex_unlock(&state->lock);

        // 显示pitch
        memset(msg.str, 0, sizeof(msg.str));
        snprintf(msg.str, OLED_MAX_LEN, "%+7.2f", pitch);
        msg.x = 48; msg.y = 0;
        ret = write(targs->fd_oled, &msg, sizeof(msg));
        if (ret < 0) {
            perror("[OLED] write oled failed");
            continue;
        }

        // 显示angle
        memset(msg.str, 0, sizeof(msg.str));
        snprintf(msg.str, OLED_MAX_LEN, "%+4d", angle);
        msg.x = 48; msg.y = 16;
        ret = write(targs->fd_oled, &msg, sizeof(msg));
        if (ret < 0) {
            perror("[OLED] write oled failed");
            continue;
        }
    }

    printf("[OLED] exit\r\n");
    return NULL;
}

/*
 * 初始化共享状态shared_state
 **/
static void init_shared_state(struct shared_state *state){
    memset(state, 0, sizeof(*state));
    pthread_mutex_init(&state->lock, NULL);
    pthread_cond_init(&state->cond, NULL);
}

/*
 * 销毁共享状态
 */
static void destroy_shared_state(struct shared_state *state){
    pthread_mutex_destroy(&state->lock);
    pthread_cond_destroy(&state->cond);
}

/*
 * 1. 打开设备节点
 * 2. 初始化共享状态
 * 3. 注册 Ctrl+C 信号处理
 * 4. 创建三个线程
 * 5. 等待线程退出
 * 6. 回收资源
 */
int main(void){
    int fd_imu = -1;
    int fd_sg90 = -1;
    int fd_oled = -1;

    pthread_t imu_tid, sg90_tid, oled_tid;
    struct shared_state state;
    struct thread_args targs;
    int ret;

    /* 1. 打开设备 */
    fd_imu = open(DEV_IMU, O_RDONLY);
    if (fd_imu < 0) {
        perror("open [mpu6050] failed");
        goto err_out;
    }
    fd_sg90 = open(DEV_SG90, O_WRONLY);
    if (fd_sg90 < 0) {
        perror("open [sg90] failed");
        goto err_out;
    }
    fd_oled = open(DEV_OLED, O_WRONLY);
    if (fd_oled < 0) {
        perror("open [oled] failed");
        goto err_out;
    }

    // 2. 初始化共享状态shared_state
    init_shared_state(&state);

    // 保存为全局,给signal handler用
    g_state = &state;

    // 3.注册,收到SIGINT时(Ctrl+C)执行sigint_handler
    signal(SIGINT, sigint_handler);

    // 4.线程参数thread_args
    memset(&targs, 0, sizeof(targs));
    targs.fd_imu = fd_imu;
    targs.fd_sg90 = fd_sg90;
    targs.fd_oled = fd_oled;
    targs.state = &state;

    // 5.创建线程
    ret = pthread_create(&imu_tid, NULL, imu_thread_func, &targs);
    if (ret != 0) {
        fprintf(stderr, "pthread_create imu failed: %s!!!\r\n", strerror(ret));
        goto err_state;
    }
    ret = pthread_create(&sg90_tid, NULL, sg90_thread_func, &targs);
    if (ret != 0) {
        fprintf(stderr, "pthread_create sg90 failed: %s!!!\r\n", strerror(ret));
        state.exit_flag = 1;
        pthread_join(imu_tid, NULL);
        goto err_state;
    }
    ret = pthread_create(&oled_tid, NULL, oled_thread_func, &targs);
    if (ret != 0) {
        fprintf(stderr, "pthread_create oled failed: %s!!!\r\n", strerror(ret));
        state.exit_flag = 1;
        pthread_cond_broadcast(&state.cond);
        pthread_join(imu_tid, NULL);
        pthread_join(sg90_tid, NULL);
        goto err_state;
    }
    printf("userAPP is running... Press Ctrl+C exit\r\n");

    while (!g_stop) {
        usleep(10000);   // 10ms 轮询一次退出请求
    }

    // 此时已经g_stop=1,结束
    pthread_mutex_lock(&state.lock);
    state.exit_flag = true;
    pthread_cond_broadcast(&state.cond);
    pthread_mutex_unlock(&state.lock);


    /* 6. 等待线程结束 */
    pthread_join(imu_tid, NULL);
    pthread_join(sg90_tid, NULL);
    pthread_join(oled_tid, NULL);

err_state:
    destroy_shared_state(&state);

err_out:
    if (fd_imu >= 0)
        close(fd_imu);
    if (fd_sg90 >= 0)
        close(fd_sg90);
    if (fd_oled >= 0)
        close(fd_oled);

    printf("userAPP exit\r\n");
    return 0;
}

3.4 其他代码

3.4.1 mpu6050reg.h

用到的寄存器地址

cpp 复制代码
#define MPU6050_SLAVE_ADDRESS  (0x68<<1)      // 读地址


//加速度数据寄存器
#define ACCEL_XOUT_H   0x3B // X轴
#define ACCEL_XOUT_L   0x3C
#define ACCEL_YOUT_H   0x3D // Y轴
#define ACCEL_YOUT_L   0x3E
#define ACCEL_ZOUT_H   0x3F // Z轴
#define ACCEL_ZOUT_L   0x40

// 温度传感器
#define TEMP_OUT_H     0x41
#define TEMP_OUT_L     0x42
// 计算公式:   TEMP_OUT/340 + 36.53 = 摄氏度


//角速度数据寄存器
#define GYRO_XOUT_H    0x43
#define GYRO_XOUT_L    0x44
#define GYRO_YOUT_H    0x45
#define GYRO_YOUT_L    0x46
#define GYRO_ZOUT_H    0x47
#define GYRO_ZOUT_L    0x48


// 初始化用到的寄存器
#define PWR_MGMT_1     0x6B   // Power Management。                 详见4.30
#define CONFIG         0x1A   // 控制陀螺仪、加速计的带宽,设置陀螺仪输出率(↓间接控制全芯片采样率),设置数字低通滤波             详见4.3
#define SMPLRT_DIV     0x19   // 全芯片的采样率 = 陀螺仪输出速率 ÷ (该寄存器的值+1)     详见4.2
#define ACCEL_CONFIG   0x1C   // 设置加速计 量程,设备自检,数字高通滤波   详见4.5
#define INT_PIN_CFG    0x37   //
#define INT_ENABLE     0x38   //
#define INT_STATUS     0x3A   // 只读寄存器

3.4.2 oled.h

字库

cpp 复制代码
#define OLED_MAX_LEN 32

struct oled_msg {
    int x;
    int y;
    char str[OLED_MAX_LEN];
};

//16*16 ASCII字符集点阵
const unsigned char asc2_1608[][16]={	  
    {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*" ",0*/
    {0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x33,0x30,0x00,0x00,0x00},/*"!",1*/
    {0x00,0x10,0x0C,0x06,0x10,0x0C,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*""",2*/
    {0x40,0xC0,0x78,0x40,0xC0,0x78,0x40,0x00,0x04,0x3F,0x04,0x04,0x3F,0x04,0x04,0x00},/*"#",3*/
    {0x00,0x70,0x88,0xFC,0x08,0x30,0x00,0x00,0x00,0x18,0x20,0xFF,0x21,0x1E,0x00,0x00},/*"$",4*/
    {0xF0,0x08,0xF0,0x00,0xE0,0x18,0x00,0x00,0x00,0x21,0x1C,0x03,0x1E,0x21,0x1E,0x00},/*"%",5*/
    {0x00,0xF0,0x08,0x88,0x70,0x00,0x00,0x00,0x1E,0x21,0x23,0x24,0x19,0x27,0x21,0x10},/*"&",6*/
    {0x10,0x16,0x0E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"'",7*/
    {0x00,0x00,0x00,0xE0,0x18,0x04,0x02,0x00,0x00,0x00,0x00,0x07,0x18,0x20,0x40,0x00},/*"(",8*/
    {0x00,0x02,0x04,0x18,0xE0,0x00,0x00,0x00,0x00,0x40,0x20,0x18,0x07,0x00,0x00,0x00},/*")",9*/
    {0x40,0x40,0x80,0xF0,0x80,0x40,0x40,0x00,0x02,0x02,0x01,0x0F,0x01,0x02,0x02,0x00},/*"*",10*/
    {0x00,0x00,0x00,0xF0,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x1F,0x01,0x01,0x01,0x00},/*"+",11*/
    {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0xB0,0x70,0x00,0x00,0x00,0x00,0x00},/*",",12*/
    {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01},/*"-",13*/
    {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x30,0x00,0x00,0x00,0x00,0x00},/*".",14*/
    {0x00,0x00,0x00,0x00,0x80,0x60,0x18,0x04,0x00,0x60,0x18,0x06,0x01,0x00,0x00,0x00},/*"/",15 */
    {0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00,0x00,0x0F,0x10,0x20,0x20,0x10,0x0F,0x00},/*"0",16*/
    {0x00,0x10,0x10,0xF8,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00},/*"1",17*/
    {0x00,0x70,0x08,0x08,0x08,0x88,0x70,0x00,0x00,0x30,0x28,0x24,0x22,0x21,0x30,0x00},/*"2",18*/
    {0x00,0x30,0x08,0x88,0x88,0x48,0x30,0x00,0x00,0x18,0x20,0x20,0x20,0x11,0x0E,0x00},/*"3",19*/
    {0x00,0x00,0xC0,0x20,0x10,0xF8,0x00,0x00,0x00,0x07,0x04,0x24,0x24,0x3F,0x24,0x00},/*"4",20*/
    {0x00,0xF8,0x08,0x88,0x88,0x08,0x08,0x00,0x00,0x19,0x21,0x20,0x20,0x11,0x0E,0x00},/*"5",21*/
    {0x00,0xE0,0x10,0x88,0x88,0x18,0x00,0x00,0x00,0x0F,0x11,0x20,0x20,0x11,0x0E,0x00},/*"6",22*/
    {0x00,0x38,0x08,0x08,0xC8,0x38,0x08,0x00,0x00,0x00,0x00,0x3F,0x00,0x00,0x00,0x00},/*"7",23*/
    {0x00,0x70,0x88,0x08,0x08,0x88,0x70,0x00,0x00,0x1C,0x22,0x21,0x21,0x22,0x1C,0x00},/*"8",24*/
    {0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00,0x00,0x00,0x31,0x22,0x22,0x11,0x0F,0x00},/*"9",25*/
    {0x00,0x00,0x00,0xC0,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x30,0x00,0x00,0x00},/*":",26*/
    {0x00,0x00,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x60,0x00,0x00,0x00,0x00},/*";",27*/
    {0x00,0x00,0x80,0x40,0x20,0x10,0x08,0x00,0x00,0x01,0x02,0x04,0x08,0x10,0x20,0x00},/*"<",28*/
    {0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x00,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x00},/*"=",29*/
    {0x00,0x08,0x10,0x20,0x40,0x80,0x00,0x00,0x00,0x20,0x10,0x08,0x04,0x02,0x01,0x00},/*">",30*/
    {0x00,0x70,0x48,0x08,0x08,0x08,0xF0,0x00,0x00,0x00,0x00,0x30,0x36,0x01,0x00,0x00},/*"?",31*/
    {0xC0,0x30,0xC8,0x28,0xE8,0x10,0xE0,0x00,0x07,0x18,0x27,0x24,0x23,0x14,0x0B,0x00},/*"@",32*/
    {0x00,0x00,0xC0,0x38,0xE0,0x00,0x00,0x00,0x20,0x3C,0x23,0x02,0x02,0x27,0x38,0x20},/*"A",33*/
    {0x08,0xF8,0x88,0x88,0x88,0x70,0x00,0x00,0x20,0x3F,0x20,0x20,0x20,0x11,0x0E,0x00},/*"B",34*/
    {0xC0,0x30,0x08,0x08,0x08,0x08,0x38,0x00,0x07,0x18,0x20,0x20,0x20,0x10,0x08,0x00},/*"C",35*/
    {0x08,0xF8,0x08,0x08,0x08,0x10,0xE0,0x00,0x20,0x3F,0x20,0x20,0x20,0x10,0x0F,0x00},/*"D",36*/
    {0x08,0xF8,0x88,0x88,0xE8,0x08,0x10,0x00,0x20,0x3F,0x20,0x20,0x23,0x20,0x18,0x00},/*"E",37*/
    {0x08,0xF8,0x88,0x88,0xE8,0x08,0x10,0x00,0x20,0x3F,0x20,0x00,0x03,0x00,0x00,0x00},/*"F",38*/
    {0xC0,0x30,0x08,0x08,0x08,0x38,0x00,0x00,0x07,0x18,0x20,0x20,0x22,0x1E,0x02,0x00},/*"G",39*/
    {0x08,0xF8,0x08,0x00,0x00,0x08,0xF8,0x08,0x20,0x3F,0x21,0x01,0x01,0x21,0x3F,0x20},/*"H",40*/
    {0x00,0x08,0x08,0xF8,0x08,0x08,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00},/*"I",41*/
    {0x00,0x00,0x08,0x08,0xF8,0x08,0x08,0x00,0xC0,0x80,0x80,0x80,0x7F,0x00,0x00,0x00},/*"J",42*/
    {0x08,0xF8,0x88,0xC0,0x28,0x18,0x08,0x00,0x20,0x3F,0x20,0x01,0x26,0x38,0x20,0x00},/*"K",43*/
    {0x08,0xF8,0x08,0x00,0x00,0x00,0x00,0x00,0x20,0x3F,0x20,0x20,0x20,0x20,0x30,0x00},/*"L",44*/
    {0x08,0xF8,0xF8,0x00,0xF8,0xF8,0x08,0x00,0x20,0x3F,0x00,0x3F,0x00,0x3F,0x20,0x00},/*"M",45*/
    {0x08,0xF8,0x30,0xC0,0x00,0x08,0xF8,0x08,0x20,0x3F,0x20,0x00,0x07,0x18,0x3F,0x00},/*"N",46*/
    {0xE0,0x10,0x08,0x08,0x08,0x10,0xE0,0x00,0x0F,0x10,0x20,0x20,0x20,0x10,0x0F,0x00},/*"O",47*/
    {0x08,0xF8,0x08,0x08,0x08,0x08,0xF0,0x00,0x20,0x3F,0x21,0x01,0x01,0x01,0x00,0x00},/*"P",48*/
    {0xE0,0x10,0x08,0x08,0x08,0x10,0xE0,0x00,0x0F,0x18,0x24,0x24,0x38,0x50,0x4F,0x00},/*"Q",49*/
    {0x08,0xF8,0x88,0x88,0x88,0x88,0x70,0x00,0x20,0x3F,0x20,0x00,0x03,0x0C,0x30,0x20},/*"R",50*/
    {0x00,0x70,0x88,0x08,0x08,0x08,0x38,0x00,0x00,0x38,0x20,0x21,0x21,0x22,0x1C,0x00},/*"S",51*/
    {0x18,0x08,0x08,0xF8,0x08,0x08,0x18,0x00,0x00,0x00,0x20,0x3F,0x20,0x00,0x00,0x00},/*"T",52*/
    {0x08,0xF8,0x08,0x00,0x00,0x08,0xF8,0x08,0x00,0x1F,0x20,0x20,0x20,0x20,0x1F,0x00},/*"U",53*/
    {0x08,0x78,0x88,0x00,0x00,0xC8,0x38,0x08,0x00,0x00,0x07,0x38,0x0E,0x01,0x00,0x00},/*"V",54*/
    {0xF8,0x08,0x00,0xF8,0x00,0x08,0xF8,0x00,0x03,0x3C,0x07,0x00,0x07,0x3C,0x03,0x00},/*"W",55*/
    {0x08,0x18,0x68,0x80,0x80,0x68,0x18,0x08,0x20,0x30,0x2C,0x03,0x03,0x2C,0x30,0x20},/*"X",56*/
    {0x08,0x38,0xC8,0x00,0xC8,0x38,0x08,0x00,0x00,0x00,0x20,0x3F,0x20,0x00,0x00,0x00},/*"Y",57*/
    {0x10,0x08,0x08,0x08,0xC8,0x38,0x08,0x00,0x20,0x38,0x26,0x21,0x20,0x20,0x18,0x00},/*"Z",58*/
    {0x00,0x00,0x00,0xFE,0x02,0x02,0x02,0x00,0x00,0x00,0x00,0x7F,0x40,0x40,0x40,0x00},/*"[",59*/
    {0x00,0x0C,0x30,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x06,0x38,0xC0,0x00},/*"\",60*/
    {0x00,0x02,0x02,0x02,0xFE,0x00,0x00,0x00,0x00,0x40,0x40,0x40,0x7F,0x00,0x00,0x00},/*"]",61*/
    {0x00,0x00,0x04,0x02,0x02,0x02,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"^",62*/
    {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80},/*"_",63*/
    {0x00,0x02,0x02,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"`",64*/
    {0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x00,0x19,0x24,0x22,0x22,0x22,0x3F,0x20},/*"a",65*/
    {0x08,0xF8,0x00,0x80,0x80,0x00,0x00,0x00,0x00,0x3F,0x11,0x20,0x20,0x11,0x0E,0x00},/*"b",66*/
    {0x00,0x00,0x00,0x80,0x80,0x80,0x00,0x00,0x00,0x0E,0x11,0x20,0x20,0x20,0x11,0x00},/*"c",67*/
    {0x00,0x00,0x00,0x80,0x80,0x88,0xF8,0x00,0x00,0x0E,0x11,0x20,0x20,0x10,0x3F,0x20},/*"d",68*/
    {0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x00,0x1F,0x22,0x22,0x22,0x22,0x13,0x00},/*"e",69*/
    {0x00,0x80,0x80,0xF0,0x88,0x88,0x88,0x18,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00},/*"f",70*/
    {0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x00,0x00,0x6B,0x94,0x94,0x94,0x93,0x60,0x00},/*"g",71*/
    {0x08,0xF8,0x00,0x80,0x80,0x80,0x00,0x00,0x20,0x3F,0x21,0x00,0x00,0x20,0x3F,0x20},/*"h",72*/
    {0x00,0x80,0x98,0x98,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00},/*"i",73*/
    {0x00,0x00,0x00,0x80,0x98,0x98,0x00,0x00,0x00,0xC0,0x80,0x80,0x80,0x7F,0x00,0x00},/*"j",74*/
    {0x08,0xF8,0x00,0x00,0x80,0x80,0x80,0x00,0x20,0x3F,0x24,0x02,0x2D,0x30,0x20,0x00},/*"k",75*/
    {0x00,0x08,0x08,0xF8,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00},/*"l",76*/
    {0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x00,0x20,0x3F,0x20,0x00,0x3F,0x20,0x00,0x3F},/*"m",77*/
    {0x80,0x80,0x00,0x80,0x80,0x80,0x00,0x00,0x20,0x3F,0x21,0x00,0x00,0x20,0x3F,0x20},/*"n",78*/
    {0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x00,0x1F,0x20,0x20,0x20,0x20,0x1F,0x00},/*"o",79*/
    {0x80,0x80,0x00,0x80,0x80,0x00,0x00,0x00,0x80,0xFF,0xA1,0x20,0x20,0x11,0x0E,0x00},/*"p",80*/
    {0x00,0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x0E,0x11,0x20,0x20,0xA0,0xFF,0x80},/*"q",81*/
    {0x80,0x80,0x80,0x00,0x80,0x80,0x80,0x00,0x20,0x20,0x3F,0x21,0x20,0x00,0x01,0x00},/*"r",82*/
    {0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x00,0x00,0x33,0x24,0x24,0x24,0x24,0x19,0x00},/*"s",83*/
    {0x00,0x80,0x80,0xE0,0x80,0x80,0x00,0x00,0x00,0x00,0x00,0x1F,0x20,0x20,0x00,0x00},/*"t",84*/
    {0x80,0x80,0x00,0x00,0x00,0x80,0x80,0x00,0x00,0x1F,0x20,0x20,0x20,0x10,0x3F,0x20},/*"u",85*/
    {0x80,0x80,0x80,0x00,0x00,0x80,0x80,0x80,0x00,0x01,0x0E,0x30,0x08,0x06,0x01,0x00},/*"v",86*/
    {0x80,0x80,0x00,0x80,0x00,0x80,0x80,0x80,0x0F,0x30,0x0C,0x03,0x0C,0x30,0x0F,0x00},/*"w",87*/
    {0x00,0x80,0x80,0x00,0x80,0x80,0x80,0x00,0x00,0x20,0x31,0x2E,0x0E,0x31,0x20,0x00},/*"x",88*/
    {0x80,0x80,0x80,0x00,0x00,0x80,0x80,0x80,0x80,0x81,0x8E,0x70,0x18,0x06,0x01,0x00},/*"y",89*/
    {0x00,0x80,0x80,0x80,0x80,0x80,0x80,0x00,0x00,0x21,0x30,0x2C,0x22,0x21,0x30,0x00},/*"z",90*/
    {0x00,0x00,0x00,0x00,0x80,0x7C,0x02,0x02,0x00,0x00,0x00,0x00,0x00,0x3F,0x40,0x40},/*"{",91*/
    {0x00,0x00,0x00,0x00,0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x00,0x00,0x00},/*"|",92*/
    {0x00,0x02,0x02,0x7C,0x80,0x00,0x00,0x00,0x00,0x40,0x40,0x3F,0x00,0x00,0x00,0x00},/*"}",93*/
    {0x00,0x06,0x01,0x01,0x02,0x02,0x04,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"~",94*/
};

3.5编译&测试

bash 复制代码
cd ..../driver
make
cd ..../app
gcc userAPP.c -o userAPP -lpthread -lm

cd ~
sudo insmod sg90.ko
sudo insmod mpu6050.ko
sudo insmod oled.ko

cd ..../app
sudo ./userAPP

此时oled和终端应该能看到输出的角度,舵机也能跟随陀螺仪调整角度了

附录

A 设备树导致板子暴毙,用SD卡烧录镜像重启

第一次改完设备树重启,结果心跳灯不亮,风扇一直转也不停,插上屏幕连个画面都没有。手边也没有usb转ttl,但有个SD卡,重新烧录镜像

鲁班猫_瑞芯微系列里面下载两个:

3-Linux镜像>LubanCat-rk3588>Debian系统(根据自己系统选)>通用系统镜像-适...>20260113_(旧sdk)5.10.160内核版本镜像(选择自己的内核版本),里面的两个压缩包挑一个下载。我下的lite,比较小,命令行够用了。

另一个是下载 6-开发软件>SDDiskTool_v1.78.zip,用于一会给SD卡烧写。

下载Download SD Card Formatter,用于格式化SD卡。

格式化完成后打开SDDiskTool:

烧写完成后,把SD卡插到板子上,重启板子,会默认使用SD启动。

我这里下载的lite,所以只有命令行。按enter,然后输入用户名和密码,因为是镜像,用户名密码就是cat temppwd,并且连不上SSH,必须要插屏幕+键盘、或者直接串口调试

使用下面的命令,看看当前系统和磁盘:

bash 复制代码
lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT

我这里能看到mmcblk0(116.5G,eMMc)和mmcblk1(15G,SD卡),mmcblk0p2对应boot分区,我们挂载到mmcblk0p2上面:

bash 复制代码
sudo mkdir -p /mnt/emmc_boot
sudo mount /dev/mmcblk0p2 /mnt/emmc_boot
ls -l /mnt/emmc_boot  # 这里应该能看到rk-kernel.dtb这个软连接

然后开始修改软连接,将其指向官方原版设备树:

bash 复制代码
cd /mnt/emmc_boot
ls -l rk-kernel.dtb
sudo ln -sf dtb/rk3588-lubancat-5-v2.dtb rk-kernel.dtb
ls -l rk-kernel.dtb
sync

卸载、关机:

bash 复制代码
cd /
sudo umount /mnt/emmc_boot
sync
sudo poweroff

关机后拔掉SD卡,重启。

B 编译问题 scripts/basic/fixdep: Exec format error

这部分全靠gpt。虽然确实成功解决问题,但不能保证过程一定正确

内核构建过程中要执行的fixdep工具,但当前文件架构不对,不能在板子上运行。

因为之前2.1配设备树时已经把kernel克隆过来了,这里直接用kernel。

bash 复制代码
cd ....../kernel

make kernelrelease 
uname -r            # 首先确保这两个版本一致


make ARCH=arm64 olddefconfig   # 准备源码树
make ARCH=arm64 prepare
make ARCH=arm64 modules_prepare

ls /home/cat/kernel/include/generated/asm-offsets.h # 如果这两个文件存在,就成功
ls /home/cat/kernel/include/generated/autoconf.h

然后修改驱动代码中的Makefile,将内核路径改为:KDIR := /home/cat/kernel

之后在drive路径下make就可以正常班编译了。

C pwm13无法启动

insmod sg90.ko的时候发现挂不上,日志提示报错:fail_get_pwm! ret=-517

用命令sudo cat /sys/kernel/debug/pwm看,发现有一个platform/febd0000.pwm,应该不是我要的pwm13,pwm13的provider没注册成功。

用grep -R "pwm13:" -n ....../kernel/arch/arm64/boot/dts/rockchip/搜了一下pwm13相关的内容,发现有pwm13的overlay:

pinctrl-names这里居然要写成active,之前我一直写的default。改成active后,重新insmod sg90.ko,看日志,没有报错:

相关推荐
Brilliantwxx12 小时前
【C++】 认识STL set与map(基础接口+题目OJ运用)
开发语言·数据结构·c++·笔记·算法
05候补工程师12 小时前
【线性代数】核心考点复习笔记:二次型配方法、施密特正交化步骤与特征值经典题型详解
经验分享·笔记·线性代数·考研·算法
程序员雷欧12 小时前
多Agent编排模块深度解析
笔记
心中有国也有家12 小时前
CANN 学习新范式:cann-learning-hub 如何让昇腾入门不再「劝退」
人工智能·经验分享·笔记·学习·算法
一只大袋鼠13 小时前
SpringBoot 入门学习笔记(三)Web 开发下篇
spring boot·笔记·学习
承渊政道13 小时前
Linux系统学习【进程概念从入门到深入理解】
linux·服务器·笔记·学习·ubuntu·系统架构·bash
Roselind_Yi14 小时前
池化对比:CNN池化 VS Java线程池
java·人工智能·经验分享·笔记·深度学习·神经网络·cnn
心中有国也有家14 小时前
hixl:昇腾分布式推理的「快递专线」
人工智能·经验分享·笔记·分布式·学习·算法
玄米乌龙茶1231 天前
LLM成长笔记(二):数据处理与工具链
笔记