源码剖析和实现

常见的web框架
gin 框架 web 框架(go)flask , tornado , django 这些都是web框架(python)

安装Flask框架

安装Flask框架

pip install Flask

路由概念

路由就是请求服务器的资源路径,去掉域名后,根据路径传给服务器,服务器进行解析。

一个简单的flask应用

from flask import Flask, render_template, request, redirect, url_for, jsonify
app = Flask(__name__)
@app.route('/')
def hello_world():
    return '<h1>Hello World</h1>'

@app.route('/user/<name>')
def user(name):
    return '<h1>Hello %s</h1>' % name


@app.route('/form_data', methods=['POST'])
def handle_form_data():
    # 获取表单数据
    username = request.form.get('username')
    email = request.form.get('email')
    age = request.form.get('age', type=int)  # 指定类型转换

    # 获取所有表单数据
    all_form_data = request.form.to_dict()

    # 处理可能的空值
    if not username:
        return jsonify({'error': 'Username is required'}), 400

    return jsonify({
        'username': username,
        'email': email,
        'age': age,
        'all_data': all_form_data
    })


# 测试HTML表单
@app.route('/form')
def show_form():
    return '''
    <form action="/form_data" method="POST">
        <p>Username: <input type="text" name="username" required></p>
        <p>Email: <input type="email" name="email"></p>
        <p>Age: <input type="number" name="age"></p>
        <p><input type="submit" value="Submit"></p>
    </form>
    '''


if __name__ == '__main__':
    app.run(debug=True)

opencv和摄像头

Python OpenCV是计算机视觉领域最重要的库之一,特别是在摄像头操作方面提供了强大的功能。我来详细介绍OpenCV的摄像头相关知识和API。

OpenCV简介

OpenCV (Open Source Computer Vision Library) 是一个开源的计算机视觉和机器学习软件库,提供了丰富的图像处理和计算机视觉算法。

安装opencv

# 基础版本
pip install opencv-python

# 包含额外功能的完整版本
pip install opencv-contrib-python

摄像头基础操作

1. 摄像头初始化和释放

import cv2
import numpy as np

# 初始化摄像头
cap = cv2.VideoCapture(0)  # 0表示默认摄像头

# 检查摄像头是否成功打开
if not cap.isOpened():
    print("无法打开摄像头")
    exit()

# 获取摄像头属性
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = int(cap.get(cv2.CAP_PROP_FPS))

print(f"摄像头分辨率: {width}x{height}")
print(f"帧率: {fps} FPS")

while True:
    # 读取帧
    ret, frame = cap.read()
    
    if not ret:
        print("无法获取帧")
        break
    
    # 显示帧
    cv2.imshow('Camera Feed', frame)
    
    # 按'q'键退出
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# 释放资源
cap.release()
cv2.destroyAllWindows()

NumPy简介
NumPy是Python科学计算生态系统的基础,提供了:
●N维数组对象(ndarray)
●广播功能
●数学函数库
●线性代数工具
●随机数生成器
安装NumPy
pip install numpy
基础数组操作
1. 创建数组
 

import numpy as np

# 从列表创建数组
arr1 = np.array([1, 2, 3, 4, 5])
print(f"1D数组: {arr1}")

# 创建多维数组
arr2 = np.array([[1, 2, 3], [4, 5, 6]])
print(f"2D数组:\n{arr2}")

# 创建3维数组
arr3 = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
print(f"3D数组形状: {arr3.shape}")

# 特殊数组创建函数
zeros = np.zeros((3, 4))          # 零数组
ones = np.ones((2, 3))            # 全1数组
full = np.full((2, 3), 7)         # 指定值填充
eye = np.eye(3)                   # 单位矩阵
empty = np.empty((2, 2))          # 未初始化数组

print(f"零数组:\n{zeros}")
print(f"全1数组:\n{ones}")
print(f"指定值数组:\n{full}")
print(f"单位矩阵:\n{eye}")

视频的构成

一个视频,看到的是连贯的,其实每一个视频都是由一帧一帧图片连贯绘制组成的。

音频也是由字节数据组成。

卡帧: 将图像帧和音频帧同步,一起发送,所以看到的就是图像和音频同步。

用第三方库 ffmpeg + sdl , webrtc 实现音视频同步。

反过来,可以将视频按帧抽取

实现一个生成器(Python),每次通过协程调用,返回一个新的帧。

生成器

#这是生成器,不是函数
def simple_generator():
    print("生成器开始")
    yield 1
    print("生成器继续")
    yield 2
    print("生成器结束")
    yield 3

gen = simple_generator()
print("gen 对象是", gen)
print("gen 类型是",type(gen))
# print("gen 的下一个值是", next(gen))
# print("gen 的下一个值是", next(gen))
# print("gen 的下一个值是", next(gen))
# try:
#     print("gen 的下一个值是", next(gen))
# except StopIteration:
#     print("没有更多的值")

# for循环会自动调用next方法
for i in gen:
    print("i is ", i)

#python3中惰性序列
for i in range(1000):
    print("range i is", i)

def generator_num(n):
    i = 0
    while i < n:
        yield i
        i += 1

gen_num = generator_num(1000)
for i in gen_num:
    print("generator_num i is ", i)

协程

def simple_coroutine():
    """简单协程示例"""
    print("协程开始执行")
    # 等待接受值,当外部通过send发送时,会将值给到x,协程继续
    x = yield
    print(f"协程收到值: {x}")
    # 再次等待接收
    y = yield x * 2
    print(f"协程又收到值: {y}")
    return x + y

# 使用协程
coro = simple_coroutine()
res = next(coro)  # 启动协程
print("res is ", res)

result1 = coro.send(10)
print(f"第一次发送结果: {result1}")

try:
     result2 = coro.send(20)
     print(f"第二次发送结果: {result2}")
except StopIteration as e:
     print(f"协程返回值: {e.value}")

自启动协程

def coroutine(func):
    """协程装饰器,自动启动协程"""
    def wrapper(*args, **kwargs):
        gen = func(*args, **kwargs)
        next(gen)  # 自动启动
        return gen
    return wrapper

#相当于  accumulator = coroutine(accumulator)
@coroutine
def accumulator():
    """累加器协程"""
    total = 0
    while True:
        value = yield total
        if value is not None:
            total += value

# 使用装饰器协程
acc = accumulator()
print(f"初始值: {acc.send(None)}")
print(f"加10后: {acc.send(10)}")
print(f"加20后: {acc.send(20)}")
print(f"加-5后: {acc.send(-5)}")

Arm部署

登陆开发板

在cmd中执行:

adb shell

看到root@localhost:/# 就成功了

系统的根目录扩容

登陆开发板后,执行:df -h 查看系统磁盘空间

根文件系统(类似Windows的C盘)只有214MB大小,太小

将下面/userdata的9.8G 提供给根文件系统

开始扩容

确认登陆到开发板(前面提示:root@localhost) ,未登陆请在命令提示符中输入:adb shell登陆

1, 卸载文件系统,复制粘贴下面2条命令,回车执行

umount /dev/mmcblk0p7umount /dev/mmcblk0p8

(类似于解除磁盘使用)

2, 删除分区

执行:

parted /dev/mmcblk0

看到如下页面

输入 rm 回车, 弹出提示(Ignore/Cancel)输入i回车, 然后输入7回车

再次输入rm回车,输入8回车

3, 扩容根文件系统

输入

resizepart 6 100%

并回车

4, 应用更改

首先,输入quit退出当前的工具

复制粘贴执行:

resize2fs /dev/mmcblk0p6

正常输出

5,删除 /etc/fstab的最后2行

  • 执行:vim /etc/fstab

  • 新弹出的页面中,按键盘 ↓ 键 ,到最后一行
  • 点击 2次 d 按键 (切换到英文输入法)
  • 点击 2次 d按键 (切换到英文输入法)
  • 输入 :wq (切换到英文输入法)
  • 回车即可

提供wifi驱动和连接wifi网络

提供WIFI驱动

1, 将软件包\烧录工具文件夹中的 bcmdhd.ko 文件复制到D盘下

2, 打开cmd(如果已经打开请关闭)

3, 执行 D: 回车

4, 复制如下命令执行

adb push bcmdhd.ko /system/lib/modules/

(意思就是 将bdmdhd.ko 文件 复制到开发板的 /system/lib/modules/文件件内)

如图成功

5, 重启开发板: 直接按一下RST即可(中间的按钮)

让开发板联网(连接WIFI)

1, 给你的手机打开热点,设置一个名字(尽量简单点 不要中文 不要中文 不要中文 和一些乱七八糟的符号)和密码

2, 登陆开发板, 打开cmd 执行: abd shell

3, 查询是否能用WiFi

执行:

nmcli dev status

回车

看到wlan0 就是wifi正常

4, 扫描wifi信号

执行

nmcli dev wifi list

回车

如果出现结果比较多 可以按 ctrl + c

5, 连接wifi

执行

nmcli device wifi connect "xxxxxx" password "yyyyyy"

回车(替换 xxxx为wifi名称 yyyy为密码)

提示successfully成功

6, 联网测试

执行

ping baidu.com -c 3

回车

如图 连接百度成功 ( 按键盘 ctrl + c )退出测试百度是否通的状态

7, 查看当前开发板在你手机热点提供的网络中的 IP地址

执行

ifconfig

回车

将代码传输到板子上

执行

python3 ./main.py

出现

安装必要的库

需要在arm板子上安装python执行所需的库

pip install flask

摄像头代码,原来使用的摄像头索引为0,windows默认摄像头

当我们的板子外接摄像头的时候就要查看摄像头索引

将摄像头插入后拔下来,看看减少了哪个设备

发现少了9驱动,所以读写改为9

后期集成驱动

后期驱动开发,作为独立开发

开发好驱动后单独写测试文件

读写/dev/驱动名字

测试成功,大家集成到现有项目中

  1. 启动线程,读写/dev/MPU6050, 死循环定时读写,作为监控,监控抖动和倾斜状态,然后让板子记录信息
  2. 启动线程,读写/dev/spi_oled, 必要时将要写入的数据通过屏幕输出。

参考文档

https://doc.embedfire.com/linux/rk356x/driver/zh/latest/linux_driver/subsystem_i2c.html#id1

源码分析

1. MPU6050 寄存器地址宏定义

这些宏定义了 MPU6050 传感器的各种寄存器地址,用于与 MPU6050 进行 I2C 通信时的读取和写入。

  • SMPLRT_DIV (0x19)
    • 采样率分频器寄存器,用于设置 MPU6050 的采样率。可以通过配置此寄存器来调整传感器的输出频率。
  • CONFIG (0x1A)
    • 配置寄存器,用于配置低通滤波器和其他一些参数。可以用来调节加速度计和陀螺仪的滤波设置。
  • GYRO_CONFIG (0x1B)
    • 陀螺仪配置寄存器,用于设置陀螺仪的量程。可以配置陀螺仪的灵敏度(如 ±250, ±500, ±1000, ±2000 degrees/s)。
  • ACCEL_CONFIG (0x1C)
    • 加速度计配置寄存器,用于设置加速度计的量程。可以配置加速度计的灵敏度(如 ±2g, ±4g, ±8g, ±16g)。
  • ACCEL_XOUT_H (0x3B), ACCEL_XOUT_L (0x3C)
    • 加速度计 X 轴数据寄存器,ACCEL_XOUT_H 存储高字节数据,ACCEL_XOUT_L 存储低字节数据。类似的还有 Y 轴和 Z 轴的寄存器(ACCEL_YOUT_H, ACCEL_YOUT_L, ACCEL_ZOUT_H, ACCEL_ZOUT_L)。
  • TEMP_OUT_H (0x41), TEMP_OUT_L (0x42)
    • 温度传感器数据寄存器,TEMP_OUT_HTEMP_OUT_L 存储温度的高字节和低字节。
  • GYRO_XOUT_H (0x43), GYRO_XOUT_L (0x44)
    • 陀螺仪 X 轴数据寄存器,GYRO_XOUT_H 存储高字节,GYRO_XOUT_L 存储低字节。类似的还有 Y 轴和 Z 轴的寄存器(GYRO_YOUT_H, GYRO_YOUT_L, GYRO_ZOUT_H, GYRO_ZOUT_L)。
  • PWR_MGMT_1 (0x6B)
    • 电源管理寄存器,常用于唤醒 MPU6050 设备。可以控制设备的休眠模式、电源状态等。
  • WHO_AM_I (0x75)
    • 查询 MPU6050 设备 ID 寄存器。通过读取该寄存器,可以确认 MPU6050 是否存在,并且确认它的设备 ID(通常返回 0x68)。

2. I2C 相关设置宏定义

这些宏定义涉及 I2C 总线和通信设置,主要用于与 I2C 设备进行数据交换。

  • SlaveAddress (0xD0)
    • MPU6050 的 I2C 从设备地址。0xD0 是写操作的地址,0xD1 是读操作的地址。这个地址通常是设备的 7 位地址左移一位后加上读写位。
  • Address (0x68)
    • MPU6050 的 I2C 地址。通常情况下,MPU6050 的 I2C 地址是 0x680x69,具体取决于 MPU6050 的 AD0 引脚是否接地或接高电平。默认情况下,AD0 接地时地址为 0x68
  • I2C_RETRIES (0x0701)
    • I2C 通信的重试次数设置。表示在 I2C 操作失败时,最多可以尝试多少次重试。
  • I2C_TIMEOUT (0x0702)
    • I2C 通信的超时设置。表示如果 I2C 操作在指定时间内未完成,操作将超时并返回错误。
  • I2C_SLAVE (0x0703)
    • I2C 从设备的地址设置。通过设置此宏,可以配置 I2C 总线上的从设备地址。
  • I2C_BUS_MODE (0x0780)
    • I2C 总线模式设置。可以配置总线的工作模式,例如主模式或从模式,或者其他特定的总线特性。

总结:

这些宏定义帮助程序与 MPU6050 传感器进行 I2C 通信。寄存器地址宏定义提供了访问和控制 MPU6050 的方式,而 I2C 通信设置宏则用于配置通信的各种参数(如重试次数、超时设置等)。通过这些宏,程序可以方便地与 MPU6050 传感器交互,实现数据读取和配置操作。

i2c设备树

i2c3: i2c@fe5c0000 {

  compatible = "rockchip,rk3399-i2c";
  reg = <0x0 0xfe5c0000 0x0 0x1000>;
  clocks = <&cru CLK_I2C3>, <&cru PCLK_I2C3>;
  clock-names = "i2c", "pclk";
  interrupts = <GIC_SPI 49 IRQ_TYPE_LEVEL_HIGH>;
  pinctrl-names = "default";
  pinctrl-0 = <&i2c3m0_xfer>;     //默认是GPIO0_A1和GPIO0_A0复用为I2C3引脚
  #address-cells = <1>;
  #size-cells = <0>;
  status = "disabled";
};

这段代码是 设备树(Device Tree) 中关于 I2C3 控制器 配置的部分。设备树用于描述硬件设备,特别是在没有标准化硬件描述的系统中,设备树帮助操作系统识别硬件,并为硬件提供必要的驱动支持。

这里的配置是关于 Rockchip RK3399 系统中的 I2C3 控制器,让我们逐项解释各个字段的含义:

1. i2c3: i2c@fe5c0000 { ... }

  • i2c3:是设备的名称。这个名称通常是用于标识和引用该设备的。
  • i2c@fe5c0000:表示该设备的节点。fe5c0000 是该 I2C 控制器在内存映射中的基地址,表示 I2C3 控制器的寄存器所在位置。

2. compatible = "rockchip,rk3399-i2c";

  • 这个字段指定了硬件设备的兼容性,指示该设备是一个 Rockchip RK3399 芯片上的 I2C 控制器。操作系统或驱动程序会通过这个字段来选择适合的驱动程序。

3. reg = <0x0 0xfe5c0000 0x0 0x1000>;

  • 该字段描述设备的物理地址范围:
    • 0x0:偏移量,通常是从 0 开始。
    • 0xfe5c0000:设备的基地址,在内存映射中的起始地址。
    • 0x0:大小的低 32 位。
    • 0x1000:设备的内存映射大小(0x1000 表示 4KB),即该设备占用的内存区域。

4. clocks = <&cru CLK_I2C3>, <&cru PCLK_I2C3>;

  • clocks:表示该设备所需的时钟源。
    • &cru CLK_I2C3:指示 I2C3 控制器所使用的时钟源。&cru 是时钟控制器的别名,CLK_I2C3 是该 I2C 控制器的时钟。
    • &cru PCLK_I2C3:表示 PCLK_I2C3 时钟源,通常是外设时钟,用于控制 I2C 外设的工作。

5. clock-names = "i2c", "pclk";

  • clock-names:是时钟源的名称,用于与 clocks 属性中的时钟源一一对应。i2cpclk 分别对应之前的 CLK_I2C3PCLK_I2C3 时钟源。

6. interrupts = <GIC_SPI 49 IRQ_TYPE_LEVEL_HIGH>;

  • interrupts:表示该设备的中断配置。
    • GIC_SPI:表示中断的类型,GIC_SPI 是一个共享的中断线(通常由 GIC 控制)。
    • 49:表示中断的编号。
    • IRQ_TYPE_LEVEL_HIGH:表示该中断的触发方式为 高电平触发

7. pinctrl-names = "default";

  • pinctrl-names:表示设备使用的引脚控制组的名称。default 表示默认的引脚配置。

8. pinctrl-0 = <&i2c3m0_xfer>;

  • pinctrl-0:表示 I2C3 控制器使用的引脚配置,&i2c3m0_xfer 是引脚控制组的别名,指示该设备所使用的 GPIO 引脚的复用配置。
  • 根据该设置,GPIO0_A1 和 GPIO0_A0 会复用为 I2C3 的 SDA 和 SCL 引脚(I2C 数据和时钟线)。

9. #address-cells = <1>;

  • #address-cells:表示子设备(如连接到该 I2C 总线的从设备)的地址单元数量。<1> 表示每个子设备有一个地址单元。

10. #size-cells = <0>;

  • #size-cells:表示子设备大小单元的数量。<0> 表示没有大小单元,通常用于 I2C 总线这种不需要指定大小的设备。

11. status = "disabled";

  • status:表示该设备的启用状态。"disabled" 表示 I2C3 控制器目前是禁用状态,可能是因为当前不需要启用 I2C3,或者设备在启动时没有启用。

总结:

这段设备树配置描述了 RK3399 芯片上的 I2C3 控制器,其中包括了硬件地址、时钟、引脚复用、中断设置等信息。通过这个配置,操作系统可以正确识别并使用 I2C3 控制器来与 I2C 设备进行通信。配置中的 status = "disabled" 表示该 I2C3 控制器目前处于禁用状态。

pinctrl复用

i2c3 {
    i2c3m0_xfer: i2c3m0-xfer {
        rockchip,pins =
            /* i2c3_sclm0 */
            <1 RK_PA1 1 &pcfg_pull_none_smt>,
            /* i2c3_sdam0 */
            <1 RK_PA0 1 &pcfg_pull_none_smt>;
    };

    i2c3m1_xfer: i2c3m1-xfer {
        rockchip,pins =
            /* i2c3_sclm1 */
            <3 RK_PB5 4 &pcfg_pull_none_smt>,
            /* i2c3_sdam1 */
            <3 RK_PB6 4 &pcfg_pull_none_smt>;
    };
    };

这段设备树代码描述了 Rockchip 系统中 I2C3 控制器 的引脚配置,具体是两个 I2C 总线的引脚复用配置。代码中的 i2c3m0_xferi2c3m1_xfer 分别定义了两个 I2C 传输接口的引脚复用设置。

让我们逐行解释这段代码的含义:

1. i2c3 { ... }

  • 这是设备树中对 I2C3 控制器 的定义,表示 I2C3 控制器的配置项。这个配置项包含了两个子节点 i2c3m0_xferi2c3m1_xfer,它们定义了不同的引脚复用配置。

2. i2c3m0_xfer: i2c3m0-xfer { ... }

  • 该节点定义了 I2C3 控制器的第一个引脚复用配置,它给出了 I2C3 总线的引脚复用设置(如 SCL 和 SDA)。
  • rockchip,pins =:定义了两个引脚的复用配置。
引脚配置:
  • /\* i2c3_sclm0 \*/:注释标明该引脚是 I2C3 的 SCL(时钟线)
  • <1 RK_PA1 1 &pcfg_pull_none_smt>
    • 1:表示该引脚位于 GPIO 1
    • RK_PA1:表示该引脚是 RK_PA1(即 Rockchip 的某个 GPIO 引脚),具体与硬件板级设计相关。
    • 1:表示该引脚的功能是 I2C3 的 SCL
    • &pcfg_pull_none_smt:表示该引脚没有上下拉电阻,并启用了快速模式(SMT:Slew-rate control)。
  • /\* i2c3_sdam0 \*/:注释标明该引脚是 I2C3 的 SDA(数据线)
  • <1 RK_PA0 1 &pcfg_pull_none_smt>
    • 1:表示该引脚位于 GPIO 1
    • RK_PA0:表示该引脚是 RK_PA0
    • 1:表示该引脚的功能是 I2C3 的 SDA
    • &pcfg_pull_none_smt:同样表示该引脚没有上下拉电阻,并启用了快速模式

3. i2c3m1_xfer: i2c3m1-xfer { ... }

  • 该节点定义了 I2C3 控制器的第二个引脚复用配置,与第一个配置类似,但使用了不同的引脚。
  • rockchip,pins =:定义了另一个 I2C 总线的引脚复用设置。
引脚配置:
  • /\* i2c3_sclm1 \*/:注释标明该引脚是 I2C3 的 SCL(时钟线)
  • <3 RK_PB5 4 &pcfg_pull_none_smt>
    • 3:表示该引脚位于 GPIO 3
    • RK_PB5:表示该引脚是 RK_PB5(即 Rockchip 的某个 GPIO 引脚)。
    • 4:表示该引脚的功能是 I2C3 的 SCL
    • &pcfg_pull_none_smt:同样表示该引脚没有上下拉电阻,并启用了 快速模式
  • /\* i2c3_sdam1 \*/:注释标明该引脚是 I2C3 的 SDA(数据线)
  • <3 RK_PB6 4 &pcfg_pull_none_smt>
    • 3:表示该引脚位于 GPIO 3
    • RK_PB6:表示该引脚是 RK_PB6
    • 4:表示该引脚的功能是 I2C3 的 SDA
    • &pcfg_pull_none_smt:同样表示该引脚没有上下拉电阻,并启用了 快速模式

总结:

这段代码定义了 Rockchip RK3399I2C3 控制器 的两个引脚复用配置:

  • i2c3m0_xfer 使用了 RK_PA0RK_PA1 引脚作为 I2C3 的 SDA 和 SCL,并且配置了没有上下拉电阻的快速模式。
  • i2c3m1_xfer 使用了 RK_PB5RK_PB6 引脚作为 I2C3 的 SDA 和 SCL,并同样配置了没有上下拉电阻的快速模式。

这些配置允许在不同的引脚上复用 I2C3 信号,从而提供多个 I2C 总线接口的支持。

设备树

/*
* Copyright (C) 2022 - All Rights Reserved by
* EmbedFire LubanCat
*/

/dts-v1/;
/plugin/;

#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/clock/rk3568-cru.h>
#include <dt-bindings/interrupt-controller/irq.h>

&i2c3 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&i2c3m0_xfer>;     //使用GPIO0_A1和GPIO0_A0复用为I2C3引脚
    #address-cells = <1>;
    #size-cells = <0>;

    mpu6050@68 {
        compatible = "fire,i2c_mpu6050";
        //compatible = "invensense,mpu6050"
        reg = <0x68>;
        status = "okay";
    };
};

这段代码是设备树(Device Tree)的配置,专门用于 Rockchip RK3568 芯片上的 I2C3 控制器 和一个连接到该控制器的 MPU6050 传感器(通过 I2C 总线进行通信)。设备树是 Linux 内核的一部分,用于描述硬件设备信息,使得操作系统能够识别并正确驱动硬件。

让我们逐项解释这段代码的含义:

1. /dts-v1/;

  • 这是设备树源文件的版本声明,表示这是一个设备树源(DTS)文件,并且版本为 v1

2. /plugin/;

  • 这是设备树插件标志,表示这是一个设备树插件,而不是完整的设备树文件。插件通常用于扩展现有的设备树配置,而不是完全替换原有的配置。

3. #include <dt-bindings/...>

  • 这些行包含了设备树绑定文件,这些文件定义了与硬件相关的常量和宏,使设备树能够与特定硬件进行对接:
    • <dt-bindings/gpio/gpio.h>:GPIO 引脚控制绑定,提供了与 GPIO 引脚相关的常量定义。
    • <dt-bindings/pinctrl/rockchip.h>:提供了 Rockchip 平台特定的引脚控制(Pinctrl)设置。
    • <dt-bindings/clock/rk3568-cru.h>:定义了与 RK3568 平台的时钟控制器相关的绑定。
    • <dt-bindings/interrupt-controller/irq.h>:定义了中断控制器的相关绑定。

4. &i2c3 { ... }

  • &i2c3:这是对 I2C3 控制器 节点的引用,表示该配置项是对 I2C3 控制器的配置。
  • status = "okay";:表示 I2C3 控制器是启用状态,可以被操作系统使用。
  • pinctrl-names = "default";:指定引脚控制组的名称为 default,这意味着默认的引脚复用设置将被使用。
  • pinctrl-0 = <&i2c3m0_xfer>;:该配置指定了 I2C3 控制器使用的引脚复用设置,即引脚 GPIO0_A1GPIO0_A0 将被复用为 I2C3 的 SCLSDA(数据和时钟线)。这些引脚控制的设置由 &i2c3m0_xfer 绑定,表示 I2C3 控制器的第一个引脚复用配置。
  • #address-cells = <1>;:表示 I2C 总线上的每个设备地址占用一个单元(即 7 位 I2C 地址)。
  • #size-cells = <0>;:表示没有额外的大小单元,I2C 总线不需要指定额外的内存大小。

5. mpu6050@68 { ... }

  • 这是 MPU6050 传感器 的配置节点,表示一个 I2C 从设备,地址为 0x68(通常是 MPU6050 的默认 I2C 地址)。
  • compatible = "fire,i2c_mpu6050";:表示该设备兼容 fire,i2c_mpu6050 驱动,fire 是自定义的厂商名,i2c_mpu6050 是设备驱动的名称。如果该行注释掉了另一个 compatible 选项(invensense,mpu6050),则设备会使用 fire 自定义的驱动,而不是 Invensense 提供的驱动。
  • reg = <0x68>;:表示 I2C 设备的地址为 0x68,这是 MPU6050 的默认地址(若 AD0 引脚接地,则地址为 0x68)。
  • status = "okay";:表示该设备是启用状态,可以被操作系统识别和使用。

总结:

这段设备树代码配置了 Rockchip RK3568 平台上的 I2C3 控制器 和连接在该总线上的 MPU6050 传感器。I2C3 控制器使用了 GPIO0_A0 和 GPIO0_A1 引脚作为 SDASCL,并且启用了 MPU6050 传感器,设备地址为 0x68。设备树中的 status = "okay"; 表示这些硬件组件都是启用状态,操作系统将能够正确识别并与它们通信。

在 I2C 通信中,许多设备(如 MPU6050)使用地址指针来选择要访问的寄存器。由于 I2C 总线是基于“主设备-从设备”通信模式的,每次读取数据时,通常需要遵循以下步骤:

1. 写入地址

在开始读取数据之前,必须告诉 MPU6050 你想读取哪个寄存器的值。为了实现这一点,必须先通过发送一个写操作,告诉 MPU6050 你希望访问的寄存器地址。这是因为 I2C 总线上的通信是基于地址的,而 MPU6050 并不知道你要读取哪个寄存器,所以首先必须通过写操作指定要访问的寄存器地址。

这里的写操作是一个单次写入操作,目的是将你想要访问的寄存器地址发送给 MPU6050。其操作步骤是:

  • 发送设备地址(mpu6050_client->addr
  • 发送寄存器地址(address_data,即告诉 MPU6050 下一个要进行的读操作是从哪个寄存器开始的。

2. 读取数据

在写入了寄存器地址后,I2C 设备(如 MPU6050)会使用该地址作为基准,进行后续的数据读取操作。之后,通过发送一个读取操作,I2C 总线会从 MPU6050 读取数据。这个过程就不需要再次发送寄存器地址,只有数据的读取即可。

为什么需要每次都发送写地址?

  1. 寄存器地址的保持性:I2C 总线上的每个设备通常不会“记住”寄存器地址。一旦完成一次读操作,设备会回到初始状态。因此,每次进行数据读取时,必须重新发送一个写操作来指明你希望访问哪个寄存器。
  2. MPU6050 工作原理:MPU6050 等传感器通常需要你告诉它要从哪个寄存器地址开始读取数据。I2C 协议本身没有持久化目标寄存器地址的机制,所以必须在每次读取时重新设置目标寄存器地址。

总结

每次调用 i2c_read_mpu6050 时,需要先发送一个写操作,设置目标寄存器地址(即寄存器指针),然后才能进行后续的数据读取。这是 I2C 通信协议的一个标准操作流程,用于确保读取的数据来自正确的寄存器。

mpu6050_read函数

/*字符设备操作函数集,.read函数实现*/
static ssize_t mpu6050_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{

    char data_H;
    char data_L;
    int error;
    short mpu6050_result[6]; //保存mpu6050转换得到的原始数据

    i2c_read_mpu6050(mpu6050_client, ACCEL_XOUT_H, &data_H, 1);
    i2c_read_mpu6050(mpu6050_client, ACCEL_XOUT_L, &data_L, 1);
    mpu6050_result[0] = data_H << 8;
    mpu6050_result[0] += data_L;

    i2c_read_mpu6050(mpu6050_client, ACCEL_YOUT_H, &data_H, 1);
    i2c_read_mpu6050(mpu6050_client, ACCEL_YOUT_L, &data_L, 1);
    mpu6050_result[1] = data_H << 8;
    mpu6050_result[1] += data_L;

    i2c_read_mpu6050(mpu6050_client, ACCEL_ZOUT_H, &data_H, 1);
    i2c_read_mpu6050(mpu6050_client, ACCEL_ZOUT_L, &data_L, 1);
    mpu6050_result[2] = data_H << 8;
    mpu6050_result[2] += data_L;

    i2c_read_mpu6050(mpu6050_client, GYRO_XOUT_H, &data_H, 1);
    i2c_read_mpu6050(mpu6050_client, GYRO_XOUT_L, &data_L, 1);
    mpu6050_result[3] = data_H << 8;
    mpu6050_result[3] += data_L;

    i2c_read_mpu6050(mpu6050_client, GYRO_YOUT_H, &data_H, 1);
    i2c_read_mpu6050(mpu6050_client, GYRO_YOUT_L, &data_L, 1);
    mpu6050_result[4] = data_H << 8;
    mpu6050_result[4] += data_L;

    i2c_read_mpu6050(mpu6050_client, GYRO_ZOUT_H, &data_H, 1);
    i2c_read_mpu6050(mpu6050_client, GYRO_ZOUT_L, &data_L, 1);
    mpu6050_result[5] = data_H << 8;
    mpu6050_result[5] += data_L;

    /*将读取得到的数据拷贝到用户空间*/
    error = copy_to_user(buf, mpu6050_result, cnt);

    if(error != 0)
    {
            printk("copy_to_user error!");
            return -1;
    }
    return 0;
}

这段代码是一个 字符设备的 read 函数实现,用于从 MPU6050 传感器 中读取加速度和陀螺仪数据,并将这些数据传递到用户空间。具体来说,它从 I2C 总线上读取 加速度计陀螺仪 的原始数据,然后将这些数据通过 copy_to_user 复制到用户空间的缓冲区。

详细解析:

1. 函数签名
c


复制编辑
static ssize_t mpu6050_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
  • filp:指向文件操作结构体的指针,通常用于访问文件描述符相关的上下文信息。
  • buf:用户空间的缓冲区,函数的目标是将读取的数据存放到这个缓冲区。
  • cnt:请求读取的字节数,表示用户空间希望读取的数据量。
  • off:文件偏移量,通常用于多次读取时管理文件指针的位置,但在这个函数中没有使用。
2. 局部变量
c复制编辑char data_H;
char data_L;
int error;
short mpu6050_result[6];
  • data_Hdata_L:用来存储 MPU6050 传感器的高字节和低字节数据。
  • mpu6050_result[6]:一个数组,用来存储从 MPU6050 读取的原始数据。数组大小为 6,因为 MPU6050 有 3 个加速度计轴(X, Y, Z)和 3 个陀螺仪轴(X, Y, Z),每个轴的数据是 16 位(两个字节)。
3. 读取加速度计和陀螺仪数据
c复制编辑i2c_read_mpu6050(mpu6050_client, ACCEL_XOUT_H, &data_H, 1);
i2c_read_mpu6050(mpu6050_client, ACCEL_XOUT_L, &data_L, 1);
mpu6050_result[0] = data_H << 8;
mpu6050_result[0] += data_L;
  • 使用 i2c_read_mpu6050 函数读取 MPU6050 的各个寄存器,ACCEL_XOUT_HACCEL_XOUT_L 分别是加速度计 X 轴的高字节和低字节。
  • 将读取到的高字节 data_H 左移 8 位(变成高字节),然后与低字节 data_L 相加,形成 16 位的加速度值,并存入 mpu6050_result[0]

这个过程对于每个加速度计和陀螺仪轴都重复:

c复制编辑i2c_read_mpu6050(mpu6050_client, ACCEL_YOUT_H, &data_H, 1);
i2c_read_mpu6050(mpu6050_client, ACCEL_YOUT_L, &data_L, 1);
mpu6050_result[1] = data_H << 8;
mpu6050_result[1] += data_L;

i2c_read_mpu6050(mpu6050_client, ACCEL_ZOUT_H, &data_H, 1);
i2c_read_mpu6050(mpu6050_client, ACCEL_ZOUT_L, &data_L, 1);
mpu6050_result[2] = data_H << 8;
mpu6050_result[2] += data_L;

i2c_read_mpu6050(mpu6050_client, GYRO_XOUT_H, &data_H, 1);
i2c_read_mpu6050(mpu6050_client, GYRO_XOUT_L, &data_L, 1);
mpu6050_result[3] = data_H << 8;
mpu6050_result[3] += data_L;

i2c_read_mpu6050(mpu6050_client, GYRO_YOUT_H, &data_H, 1);
i2c_read_mpu6050(mpu6050_client, GYRO_YOUT_L, &data_L, 1);
mpu6050_result[4] = data_H << 8;
mpu6050_result[4] += data_L;

i2c_read_mpu6050(mpu6050_client, GYRO_ZOUT_H, &data_H, 1);
i2c_read_mpu6050(mpu6050_client, GYRO_ZOUT_L, &data_L, 1);
mpu6050_result[5] = data_H << 8;
mpu6050_result[5] += data_L;
  • 每个轴(加速度计和陀螺仪)的数据都分别存储在 mpu6050_result 数组的对应位置(mpu6050_result[0]mpu6050_result[5])。
4. 将数据拷贝到用户空间
c复制编辑error = copy_to_user(buf, mpu6050_result, cnt);
if(error != 0)
{
    printk("copy_to_user error!");
    return -1;
}

copy_to_user:该函数用于将内核空间的数据(mpu6050_result)复制到用户空间的缓冲区(buf)。复制的字节数由 cnt 决定。
●如果 copy_to_user 返回错误(error != 0),说明数据复制失败,函数打印错误信息并返回 -1。
5. 返回值:
 

c


复制编辑
return 0;
  • 如果一切正常,函数返回 0,表示读取成功。

总结:

  • MPU6050 数据读取:该函数通过 I2C 从 MPU6050 传感器读取加速度计和陀螺仪的原始数据。每个轴的数据由两个字节(高字节和低字节)组成,并将这些数据合并成一个 16 位的数值存储在 mpu6050_result 数组中。
  • 数据复制到用户空间:最后,函数将读取到的数据从内核空间复制到用户空间,通过 copy_to_user 完成。如果复制失败,函数会返回错误码 -1;否则返回 0 表示成功。

这个函数是字符设备驱动中的 read 操作,用于将设备中的数据传输到用户空间。

利用i2c读取寄存器数据

static int i2c_read_mpu6050(struct i2c_client *mpu6050_client, u8 address, void *data, u32 length)
{
    int error = 0;
    u8 address_data = address;
    struct i2c_msg mpu6050_msg[2];

    /*设置读取位置i2c_msg*/
    mpu6050_msg[0].addr = mpu6050_client->addr; //mpu6050在 iic 总线上的地址
    mpu6050_msg[0].flags = 0;                                       //标记为发送数据
    mpu6050_msg[0].buf = &address_data;                     //写入的首地址
    mpu6050_msg[0].len = 1;                                         //写入长度

    /*读取i2c_msg*/
    mpu6050_msg[1].addr = mpu6050_client->addr; //mpu6050在 iic 总线上的地址
    mpu6050_msg[1].flags = I2C_M_RD;                        //标记为读取数据
    mpu6050_msg[1].buf = data;                                      //读取得到的数据保存位置
    mpu6050_msg[1].len = length;                            //读取长度

    error = i2c_transfer(mpu6050_client->adapter, mpu6050_msg, 2);

    if (error != 2)
    {
            printk(KERN_DEBUG "\n i2c_read_mpu6050 error \n");
            return -1;
    }
    return 0;
}

1. 函数签名

c


复制编辑
static int i2c_read_mpu6050(struct i2c_client *mpu6050_client, u8 address, void *data, u32 length)
  • mpu6050_client:指向 I2C 客户端的结构体,表示与 MPU6050 设备相关的 I2C 客户端。这是与硬件设备通信的关键结构,包含了设备的地址、总线信息等。
  • address:MPU6050 设备中的寄存器地址,用于指定从哪里读取数据。
  • data:指向存储读取数据的缓冲区的指针。读取的数据将保存在这里。
  • length:指定要读取的字节数。

2. 局部变量

c复制编辑int error = 0;
u8 address_data = address;
struct i2c_msg mpu6050_msg[2];
  • error:用于存储 I2C 操作的返回值,判断是否成功。
  • address_data:存储要读取的寄存器地址,作为数据写入 I2C 总线时的地址。
  • mpu6050_msg[2]:I2C 消息数组,包含两个 I2C 消息。一个是写入消息,用于指定要读取的寄存器地址,另一个是读取消息,用于获取数据。

3. 设置写操作的 I2C 消息(写寄存器地址)

c复制编辑mpu6050_msg[0].addr = mpu6050_client->addr;  // MPU6050 在 I2C 总线上的地址
mpu6050_msg[0].flags = 0;  // 标记为发送数据
mpu6050_msg[0].buf = &address_data;  // 写入的寄存器地址
mpu6050_msg[0].len = 1;  // 写入长度为 1 字节
  • mpu6050_msg[0].addr:设置要与其通信的 I2C 设备的地址,即 MPU6050 的 I2C 地址。
  • mpu6050_msg[0].flags:设置消息的标志为 0,表示这是一个 写操作(默认是写)。
  • mpu6050_msg[0].buf:设置缓冲区为 address_data,即要写入的寄存器地址。
  • mpu6050_msg[0].len:设置写操作的长度为 1 字节,因为 address 是一个字节。

4. 设置读操作的 I2C 消息(读取数据)

c复制编辑mpu6050_msg[1].addr = mpu6050_client->addr;  // MPU6050 在 I2C 总线上的地址
mpu6050_msg[1].flags = I2C_M_RD;  // 标记为读取数据
mpu6050_msg[1].buf = data;  // 读取的数据将保存在 data 缓冲区
mpu6050_msg[1].len = length;  // 读取长度为 length 字节
  • mpu6050_msg[1].addr:与 mpu6050_msg[0].addr 相同,表示 MPU6050 的 I2C 地址。
  • mpu6050_msg[1].flags:设置为 I2C_M_RD,表示这是一个 读操作
  • mpu6050_msg[1].buf:设置为 data,读取的数据将存储在这个缓冲区中。
  • mpu6050_msg[1].len:设置读取的字节数为 length

5. 执行 I2C 操作

c


复制编辑
error = i2c_transfer(mpu6050_client->adapter, mpu6050_msg, 2);
  • i2c_transfer:执行 I2C 操作,传入的参数为:
    • mpu6050_client->adapter:I2C 适配器,表示实际的 I2C 总线。
    • mpu6050_msg:I2C 消息数组,包含两个消息:一个写操作和一个读操作。
    • 2:表示有两个消息(写一个寄存器地址,读数据)。

该函数会依次执行写操作(写寄存器地址)和读操作(读取数据)。

6. 检查操作是否成功

c复制编辑if (error != 2)
{
    printk(KERN_DEBUG "\n i2c_read_mpu6050 error \n");
    return -1;
}
  • i2c_transfer 返回的是成功传输的消息数量。如果成功传输了两个消息(一个写操作和一个读操作),返回值应该是 2
  • 如果返回值不等于 2,说明 I2C 操作失败,函数会输出错误信息,并返回 -1

7. 返回成功

c


复制编辑
return 0;
  • 如果 I2C 操作成功,函数返回 0,表示成功读取数据。

总结:

  • i2c_read_mpu6050 函数的作用是通过 I2C 协议从 MPU6050 传感器 中读取数据。
  • 它首先通过写操作将目标寄存器地址(address)发送给 MPU6050,然后通过读操作从该寄存器读取指定长度的数据。
  • 读取的数据保存在传入的 data 缓冲区中,最后通过 i2c_transfer 执行 I2C 操作。
  • 函数检查操作是否成功,如果失败,则返回 -1,成功则返回 0

这个函数是与 MPU6050 传感器通信的基础 I2C 操作,适用于读取传感器的各种数据(如加速度计和陀螺仪数据)。

SPI

Logo

脑启社区是一个专注类脑智能领域的开发者社区。欢迎加入社区,共建类脑智能生态。社区为开发者提供了丰富的开源类脑工具软件、类脑算法模型及数据集、类脑知识库、类脑技术培训课程以及类脑应用案例等资源。

更多推荐