82、源码剖析和实现--------行车安全记录系统
源码剖析和实现
常见的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/驱动名字
测试成功,大家集成到现有项目中
- 启动线程,读写/dev/MPU6050, 死循环定时读写,作为监控,监控抖动和倾斜状态,然后让板子记录信息
- 启动线程,读写/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)。
- 加速度计 X 轴数据寄存器,
TEMP_OUT_H(0x41),TEMP_OUT_L(0x42):
-
- 温度传感器数据寄存器,
TEMP_OUT_H和TEMP_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)。
- 陀螺仪 X 轴数据寄存器,
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 位地址左移一位后加上读写位。
- MPU6050 的 I2C 从设备地址。
Address(0x68):
-
- MPU6050 的 I2C 地址。通常情况下,MPU6050 的 I2C 地址是
0x68或0x69,具体取决于 MPU6050 的 AD0 引脚是否接地或接高电平。默认情况下,AD0 接地时地址为0x68。
- MPU6050 的 I2C 地址。通常情况下,MPU6050 的 I2C 地址是
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属性中的时钟源一一对应。i2c和pclk分别对应之前的CLK_I2C3和PCLK_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_xfer 和 i2c3m1_xfer 分别定义了两个 I2C 传输接口的引脚复用设置。
让我们逐行解释这段代码的含义:
1. i2c3 { ... }
- 这是设备树中对 I2C3 控制器 的定义,表示 I2C3 控制器的配置项。这个配置项包含了两个子节点
i2c3m0_xfer和i2c3m1_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 RK3399 中 I2C3 控制器 的两个引脚复用配置:
i2c3m0_xfer使用了RK_PA0和RK_PA1引脚作为 I2C3 的 SDA 和 SCL,并且配置了没有上下拉电阻的快速模式。i2c3m1_xfer使用了RK_PB5和RK_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_A1和GPIO0_A0将被复用为 I2C3 的 SCL 和 SDA(数据和时钟线)。这些引脚控制的设置由&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 引脚作为 SDA 和 SCL,并且启用了 MPU6050 传感器,设备地址为 0x68。设备树中的 status = "okay"; 表示这些硬件组件都是启用状态,操作系统将能够正确识别并与它们通信。


在 I2C 通信中,许多设备(如 MPU6050)使用地址指针来选择要访问的寄存器。由于 I2C 总线是基于“主设备-从设备”通信模式的,每次读取数据时,通常需要遵循以下步骤:
1. 写入地址
在开始读取数据之前,必须告诉 MPU6050 你想读取哪个寄存器的值。为了实现这一点,必须先通过发送一个写操作,告诉 MPU6050 你希望访问的寄存器地址。这是因为 I2C 总线上的通信是基于地址的,而 MPU6050 并不知道你要读取哪个寄存器,所以首先必须通过写操作指定要访问的寄存器地址。
这里的写操作是一个单次写入操作,目的是将你想要访问的寄存器地址发送给 MPU6050。其操作步骤是:
- 发送设备地址(
mpu6050_client->addr) - 发送寄存器地址(
address_data),即告诉 MPU6050 下一个要进行的读操作是从哪个寄存器开始的。
2. 读取数据
在写入了寄存器地址后,I2C 设备(如 MPU6050)会使用该地址作为基准,进行后续的数据读取操作。之后,通过发送一个读取操作,I2C 总线会从 MPU6050 读取数据。这个过程就不需要再次发送寄存器地址,只有数据的读取即可。
为什么需要每次都发送写地址?
- 寄存器地址的保持性:I2C 总线上的每个设备通常不会“记住”寄存器地址。一旦完成一次读操作,设备会回到初始状态。因此,每次进行数据读取时,必须重新发送一个写操作来指明你希望访问哪个寄存器。
- 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_H和data_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_H和ACCEL_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
更多推荐



所有评论(0)