今年春招怎么突然冒出来这么多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 = ® // 要发送的数据:目标寄存器地址
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,看日志,没有报错:


