项目
U-Boot和linux内核移植
使用厂商提供的uboot 但是有些外设要自己移植。
uboot启动顺序
首先第一步执行入口函数 入口函数跳转到汇编函数start.s 这个函数会跳转到start_code 处 它的主要作用有几个
- 创建c语言的运行环境
- 关闭中断和mmc
- 蒋处理模式升级成特权模式
然后运行第一个c语言 它的作用就是 进行重定向 让uboot放在内存中运行 并且启动网口 内存 gpio uart等外设 然后进行循环的命令处理
最重要的 是把内和信息加载到对应的位置 然后等待内核启动
主要是加上两个方面的驱动 uboot就相当于是一个裸机驱动
LCD 驱动修改
在uboot里找到mx6ull_alientek_emmc.c
1 | struct display_info_t const displays[] = {{ |
就是lcd驱动的一些匹配的格式 这些鞋就做好了lcd的驱动 具体lcd是什么在后面讲
网络驱动修改
mx6ull_alientek_emmc.h
这里形成的CONFIG_FEC_ENET_DEV
处 把这里的地址改成我们上面的phy地址0x0- 还有
mx6ull_alientek_emmc.c
会有一个 班上驱动的init 这个删除 还有iox——pad 这个也要清除 - 在
board_init
函数 调用2中的两个函数 现在可以填写我们的驱动函数了
首先修改2 因为在mx6ull_alientek_emmc.c
函数里本来就有fec1_pads[]
我们自己的结构体 只是我们要对其进行修改就是在没一行最后添加上复位的接口和盘配置MX6_PAD_SNVS_TAMPER7__GPIO5_IO07 | MUX_PAD_CTRL(NO_PAD_CTRL),
然后再初始化这个结构体2setup_iomux_fec
函数中进行这个结构体的初始化 函数如下
1 | static void setup_iomux_fec(int fec_id) |
linux内核启动
启动流程
- 初始化:一旦开始加载内核之后 系统就会读取设备树文件 开始建立内存映射表 初始化进程 文件系统 等等
- 核初始化完毕后,会启动第一个用户空间进程 init(或 systemd),init 是 Linux 系统中的第一个进程,它负责初始化系统环境并启动其他进程
- init 进程启动,它会初始化用户空间,包括启动各种系统服务、运行 shell 程序等等。
linux内核就涉及到了设备树了 所以不能和刚才主机
修改驱动
EMMC 驱动
Linux 内核驱动里面 EMMC 默认是 4 线模式的,4 线模式肯定没有 8 线模式的速度快,所以我们将 EMMC 的驱动修改为 8 线模式
直接修改设备树
1 | &usdhc2 { |
网络驱动
首先修改设备树
首先添加网络引脚抚慰信息 iomuxc_snvs
节点 添加
1 | pinctrl_enet1_reset: enet1resetgrp { |
修改时钟配置:
1 | pinctrl_enet1: enet1grp { |
phy设备地址在fec1
后面
1
2
3
4ethphy0: ethernet-phy@0 {
compatible = "ethernet-phy-ieee802.3-c22";
reg = <2>;
};
reg = <2> 就是地址我们修改成phy的地址 然后就可以使用了
然后把fec1
的内容修改为
1 | pinctrl-names = "default"; |
phy-mode = “rmii”;:该行代码指定以太网PHY的模式。RMII(Reduced Media Independent Interface)是一种简化版的MII(Media Independent Interface),通常用于以太网系统中。
phy-handle = <ðphy0>;:该行代码指定以太网PHY的设备节点。ðphy0引用指向另一个描述PHY的设备树节点。
phy-reset-gpios = <&gpio5 7 GPIO_ACTIVE_LOW>;:该行代码指定用于重置以太网PHY的GPIO引脚和极性。在这种情况下,使用GPIO 5,引脚 7,并且极性是低电平触发,这意味着当引脚为低电平时,重置信号处于活动状态。
phy-reset-duration = <200>;:该行代码指定PHY复位持续时间(以毫秒为单位)。在这种情况下,它设置为200毫秒。
inctrl_enet1
&pinctrl_enet1_reset我们修改为
1
2
3
4
5
6
7
8
9
10
11
12 pinctrl_enet1: enet1grp {
fsl,pins = <
MX6UL_PAD_ENET1_RX_EN__ENET1_RX_EN 0x1b0b0
MX6UL_PAD_ENET1_RX_ER__ENET1_RX_ER 0x1b0b0
MX6UL_PAD_ENET1_RX_DATA0__ENET1_RDATA00 0x1b0b0
MX6UL_PAD_ENET1_RX_DATA1__ENET1_RDATA01 0x1b0b0
MX6UL_PAD_ENET1_TX_EN__ENET1_TX_EN 0x1b0b0
MX6UL_PAD_ENET1_TX_DATA0__ENET1_TDATA00 0x1b0b0
MX6UL_PAD_ENET1_TX_DATA1__ENET1_TDATA01 0x1b0b0
MX6UL_PAD_ENET1_TX_CLK__ENET1_REF_CLK1 0x4001b009
>;
};
根文件构建
这个步骤很简单 网上查查都有资料 就不罗嗦了
I2C &开发AP3216C
原理:
是很常见的一种总线协议,有两个线控制足迹和从机进行数据通信,一条是一条是 SCL(串行时钟线),另外一条是 SDA(串行数据线),
- 空闲时都处于高电平的状态开始时候
- 开始位:scl高电平sda下降
- 停止位SCL 位高电平的时候,SDA出现上升沿就表示为停止位
- 数据传输 都平滑
- sDA 设置为输入状态,等待 I2C 从机应答,也就是等到 I2C 从机告诉主机它接收到数据了
写时序:
- 发送开始信号。
- 发送从机地址 ,七位是设备地址,剩一位就是读写位 0是读,一是写
- 发送确认信号
- 发送重新开始信号
- 发送寄存器地址
- 发送应答信号
- 发送要写入的寄存器地址
- 停止信息
读时序:读时序比较难的是首先要写入寄存器就是说
- 前面七步都一样
- 再继续发送从地址 不过读写位改成写
常见的寄存器
- I2Cx_IADR 从设备寄存器
- I2Cx_IFDR 频率的寄存器
- I2Cx_I2CR 控制寄存器
- I2Cx_I2DR 数据寄存器 还可以存寄存器
裸机代码
1 |
|
在写代码时侯吃的亏有两个 1.写数据时侯不要ack 收数据的时候才要 2. 手册上说的那个中断挂起位 其实是一个数据传输或者接收后变化 要手动修改
驱动代码
驱动代码是利用在i2c总线来匹配啊设备 然后i2c驱动来传输信息 可以比之前方便很多
首先修改设备树
把节点改成i2c
1 | pinctrl_i2c1: i2c1grp { |
追加节点:
1 | ap3216c@1e { |
然后写程序 这个程序太啰嗦了 不贴了 不过有两点 匹配结束之后 把 i2c_client
放在 dev的private_data
里·二 这个时候 i2c_client->addr
就是你的设备地址 写数据是发两条数据 第一条是写寄存器 然后是读然后再写数据的时候 data[0] 放的是你的寄存器 后面的才是数据
spi ICM-20608
其实我觉得弄好i2c之后再写spi的驱动舒服得多
先说一下差别 就是spi用的是片选线 没有地址 而且传递消息SPI 有四种工作模式,通过串行时钟极性(CPOL)和相位(CPHA)的搭配来得到四种工作模式 而且会有两根线 一个用来主设备给从设备传信息 一个是从设备给主设备传信息
看硬件原理图如下:
要注意点 1:片选线是0 2: 用了spi3
裸机
要注意的是裸机驱动非常简单我们因为是双向的 所以我们不用费力 就可以在驱动写下;
1 | unsigned char spich0_readwrite_byte(ECSPI_Type *base, unsigned char txdata) |
然后直接读写 需要注意的是:和i2c一样 读寄存器写地址只有低7位有效,寄存器地址最高位是读/写标志位读的时候要为1,写的时候要为0。然后也注意 读还是多一个步骤 先写寄存器的值
驱动
驱动就比较简单粗暴了
首先还是修改设备树:
首先写pinctrl 就是把spi复位称自己想要的状态
1 | pinctrl_ecspi3: icm20608 { |
然后追加节点;
1 | &ecspi3 { |
然后直接spi总线匹配
值得注意的是 读取:
1 | txdata[0] = reg | 0x80; /* 写数据的时候首寄存器地址 bit8 要置 1 */ |
要把发送的地址放在写寄存器里
写:
1 | *txdata = reg & ~0x80; /* 写数据的时候首寄存器地址 bit8 要清零 */ |
直接写就可以而来
中断
这个就不说裸机了 太臭太长了
我们直接从驱动开始说我们直接先来解析key的设备树 进而解析设备树的中断处理过程
1 | key{ |
这段关于案件的设备树我们在引用的时候 可以直接调用irq_of_parse_and_map
来在intereupts
中直接找到那个节点的中断 然后去:对应的节点设备树下 找到对应的中断号 然后往中断号里注册中断
1 | compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio"; |
在这里是设备树的知识点 这个中断代码是这样的流程首先
注册驱动 然后申请input函数 记录按键过程 最后利用定时器来消抖
input总线
就是首先定义keyinputdev.inputdev = input_allocate_device();keyinputdev.inputdev->name = KEYINPUT_NAME;
然后填充结构体 比如结构体的什么时间 案件时间 具体哪一个按键 keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);
然后把设备和总线挂钩ret = input_register_device(keyinputdev.inputdev);
然后有事件上报input_report_key(dev->inputdev, keydesc->value, 1);/* 最后一个参数表示按下还是松开,1为按下,0为松开 */input_sync(dev->inputdev);
lcd
原理
所谓lcd的原理其实就是填充他的结构体的一些东西:
VSYNC:帧同步信号,当此信号有效的话就表示开始显示新的一帧数据,查阅所使用的
LCD 数据手册可以知道此信号是低电平有效还是高电平有效,假设此时是低电平有效。
VSPW:有些地方也叫做 tvp,是 VSYNC 信号宽度,也就是 VSYNC 信号持续时间,单位
为 1 行的时间。
VBP:有些地方叫做 tvb,前面已经讲过了,术语叫做帧同步信号后肩,单位为 1 行的时
间。
LINE:有些地方叫做 tvd,显示一帧有效数据所需的时间,假如屏幕分辨率为 1024600,
那么 LINE 就是 600 行的时间。
VFP:有些地方叫做 tvf,前面已经讲过了,术语叫做帧同步信号前肩,单位为 1 行的时间
还有就是lcd的引脚 他的原理图是这样的 大部分引脚都是 rgb888 的像素一个像素点就相当于一个 RGB 小灯,通过控制 R、G、B 这三种颜色的亮度就
可以显示出各种各样的色彩。那该如何控制 R、G、B 这三种颜色的显示亮度呢?一般一个 R、
G、B 这三部分分别使用 8bit 的数据,那么一个像素点就是 8bit3=24bit,也就是说一个像素点
3 个字节,这种像素格式称为 RGB888
还有一个定义叫显存内存来存放像素数据,那么 1024600 分辨率就需要 1024600*4=2457600B≈2.4MB 内存。但是 RGB LCD 内部是没有内存的,所以就需要在开发板上的 DDR3 中分出一段内存作为 RGB
LCD 屏幕的显存
驱动
修改设备树:
1 | pinctrl_lcdif_dat: lcdifdatgrp { |
就是使能几个lcd的引脚
现在需要修改时钟参数 以及上面提到的那几个要填充的结构体的值:
1 | &lcdif { |
代码就不用写了 因为lcd的代码内核里早就写了
直接用就行了 但是这个注册之后会生成一个/dev/fb0
设备文件 这个一会会用到
lcd触摸屏
原理 就是内部有个的触摸芯片 然后通过i2c传递数据 还有他的每次触屏都涉及到了input
首先修改设备树:
首先复位io和中断io都是gpio普通引脚
1 | pinctrl_tsc: tscgrp { |
现在修改i2c信息:
1 | pinctrl_i2c2: i2c2grp { |
添加触摸屏节点:
1 | ft5426: ft5426@38 { |
我们来说一下代码
1 |
|
代码逻辑是这样的:首先引导i2c驱动 然后在prope函数里注册中断 中断就是有input读取信息
tslib 移植
tslib 是一个开源的第三方库,用于触摸屏性能调试,使用电阻屏的时候一般使用 tslib 进行校准
这个很简单网上会有教程:就是在写配置的时候
1 | export TSLIB_TSDEVICE=/dev/input/event1 //你刚才写的触摸屏 |
然后移植qt
音频驱动
i2o
阻塞
并发
比如说打印机这个设备,我们在同一时刻必须一个人使用,其实越往后学习 越感觉的到所写的驱动程序 其实就是申请了一个dev我们对它进行操作被应用层调用而已,我们在打开的时候上我们的锁 或者是原子操作再关闭上收回锁 就是有几种方式我们要一个一个来
原子操作
原子操作,原子操作就是指不能再进一步分割的操作,一般原子操作用于变量
或者位操作。假如现在要对无符号整形变量 a 赋值,值为 3,对于 C 语言来讲很简单,直接就
是:
a = 3
但是 C 语言要先编译为成汇编指令,ARM 架构不支持直接对寄存器进行读写操作,比如
要借助寄存器 R0、R1 等来完成赋值操作。假设变量 a 的地址为 0X3000000,“a=3”这一行 C
语言可能会被编译为如下所示的汇编代码:
1 | ldr r0, =0X30000000 /* 变量 a 地址 */ |
所以我们需要原子操作 这个很简单 就是在设备结构体里加上atomic_t lock;
,/然后再
1 | //open函数里判断原子操作 |
自旋锁
原子操作只能对整形变量或者位进行保护,但是,在实际的使用环境中怎么可能只有整形变量或位这么简单的临界区。举个最简单的例子,设备结构体变量就不是整型变量,我们对于结构体中成员变量的操作也要保证原子性,在线程 A 对结构体变量使用期间,应该禁止其他的线程来访问此结构体变量,这些工作原子操作都不能胜任,需要本节要讲的锁机制,在 Linux内核中就是自旋锁。
代码
1 | 开自旋锁: |
信号量
初始化sema_init(&gpioled.sem, 1);
open函数:
1 | /* 获取信号量,进入休眠状态的进程可以被信号打断 */ |
互斥体
大差不差
区别
信号量/互斥体允许进程睡眠属于睡眠锁,自旋锁则不允许调用者睡眠,而是让其循环等待,所以有以下区别应用
1)、信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因而自旋锁适合于保持时间非常短的情况
2)、自旋锁可以用于中断,不能用于进程上下文(会引起死锁)。而信号量不允许使用在中断中,而可以用于进程上下文
3)、自旋锁保持期间是抢占失效的,自旋锁被持有时,内核不能被抢占,而信号量和读写信号量保持期间是可以被抢占的