赞
踩
(Trusted Firmware - Arm)
‘P’:向分区或者设备烧写固件。
‘E’:空分区或设备,表示对应的分区或设备不更新,相关的 Id项会被跳过。
‘D’:删除分区或设备。
TF_A的bl33部分是uboot,uboot的功能是引导、启动linux。
一般环境变量存放在外部flash中,uboot启动的时候会将环境变量从flash读取到DRAM中。使用命令setenv修改的是DRAM中的环境变量值,修改以后要使用savenv命令将修改后的环境变量保存到flash中,否则uboot下一次重启会继续使用以前的环境变量值。
命令 bootm主要有三个参数,第一个参数是 Linux镜像文件在 DRAM中的位置,比如要指定 initrd的话,第二个参数就是 initrd在 DRAM中的地址,如果不需要 initrd的话第二个参数就用‘-’来代替。如果 Linux内核使用设备树的话还需要第三个参数,用来指定设备树在 DRAM中的地址。
tftp命令( 使用的是 TFTP协议)常用于将 uImage、设备树dtb文件下载到 DRAM指定地址处,最后使用命令 bootm启动
命令 ext4load命令常用于将 uImage和dtb文件从 EMMC中拷贝到 DRAM中,然后使用命令 bootm启动即可。(EMMC中启动 Linux系统)
bootz和 bootm功能类似,但是所引导的 Linux镜像格式不同,bootz用于启动 zImage镜像文件,NXP的 I.MX6ULL就是使用 bootz命令来引导 Linux内核的。
bootcmd保存着 uboot默认命令,uboot倒计时结束以后就会执行 bootcmd中的命令。这些命令一般都是用来启动 Linux内核的,比如(读取 EMMC或者 NAND Flash中的 Linux内核镜像文件和设备树文件到 DRAM中)(BootLoader--机器人换电池--江科大),然后启动 Linux内核。
bootargs保存着 uboot传递给 Linux内核的参数,比如指定 Linux内核所使用的 console、指定根文件系统所在的分区等。
//从网络
setenvbootcmd'tftp c2000000 uImage;tftp c4000000 stm32mp157d-atk.dtb;bootm c2000000 - c4000000'
setenvbootargs'console=ttySTM0,115200 root=/dev/nfs nfsroot=169.254.8.149:/home/lee/linux/nfs/rootfs,proto=tcp rw ip=169.254.8.151:169.254.8.149:169.254.8.1:255.255.255.0::eth0:off'
//从emmc
setenvbootcmd'ext4load mmc 1:2 c2000000 uImage;ext4load mmc 1:2 c4000000 stm32mp157d-
atk.dtb;bootmc2000000-c4000000'
setenvbootargs'console=ttySTM0,115200 root=/dev/mmcblk1p3 rootwait rw'
saveenv
boot
rcS是个 shell脚本,Linux内核启动以后需要启动一些服务,而 rcS就是规定启动哪些文件的脚本文件。
测试根文件系统的时候不是直接烧写到 EMMC里面,这样测试效率太低了,Ubuntu的 rootfs目录已经保存了根文件系统,只需要在开发板上通过 nfs挂载 Ubuntu下的 rootfs目录即可。也就是说,根文件系统一直在 Ubuntu下,开发板通过网络在使用这个根文件系统,这样方便我们开发调试。
P570 编译完成以后 buildroot就会生成编译出来的根文件系统压缩包,我们可以直接使用。
“驱动就是获取外设或者传感器数据,控制外设;数据会提交给应用程序。”
Linux驱动分为三大类:字符设备驱动、块设备驱动、网络设备驱动
Linux操作系统内核和驱动程序运行在内核空间、应用程序运行在用户空间
应用程序访问内核资源有三种方法:系统调用、异常(中断)和陷入。系统调用处于内核空间,应用程序无法直接访问,因此需要“陷入“到内核,方法就是软中断。陷入内核以后还要指定系统调用号。
Linux驱动有两种运行方式:1、将驱动编译进 Linux内核中,这样当 Linux内核启动的时候就会自动运行驱动程序。2、将驱动编译成模块(Linux下模块扩展名为.ko),在Linux内核启动以后使用“insmod” 命令加载驱动模块。(modprobe命令相比 insmod要智能一些,modprobe会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中)(obj-m表示把文件test.o作为"模块"进行编译,不会编译到内核,但是会生成一个独立的 "test.ko" 文件;obj-y表示把test.o文件编译进内核;)
在调试驱动的时候一般都选择将其编译为模块,好处:修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux代码。而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。
Linux中的man 1 2 3 ... manual手册分成很多section,使用man时可以指定不同的section来浏览
驱动加载成功需要在/dev目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。
“因为内核空间不能直接操作用户空间的内存,因此需要借助 copy_to_user函数来完成内核空间的数据到用户空间的复制。因为用户空间内存不能直接访问内核空间的内存,所以需要借助函数 copy_from_user将用户空间的数据复制到 writebuf这个内核空间中。”
//没有指定设备号
intalloc_chrdev_region(dev_t*dev, unsignedbaseminor, unsignedcount, constchar*name)
//给定了设备的主设备号和次设备号
intregister_chrdev_region(dev_tfrom, unsignedcount, constchar*name)
//cdev结构体
1structcdev {
2 structkobject kobj;
3* structmodule *owner;
4* conststructfile_operations*ops;
5 structlist_head list;
6* dev_t dev;
7 unsignedint count;
8 } __randomize_layout;
在 Linux中使用 cdev结构体表示一个字符设备,在 cdev中有两个重要的成员变量:ops和 dev,这两个就是字符设备文件操作函数集合file_operations以及设备号 dev_t。
首先使用 cdev_init函数完成对 cdev结构体变量的初始化,然后使用 cdev_add函数向 Linux系统添加这个字符设备。自动创建设备节点(创建类,创建设备)
不再使用mknod(如mknod /dev/led c 200 0)(c-表示字符设备,主设备号,次设备号)
设置文件私有数据:对于一个设备的所有属性信息(比如主设备号(dev_t),类(class)、设备(device)、开关状态(state)等等)我们最好将其做成一个结构体。
引入label的目的就是为了方便访问节点,可以直接通过&label来访问这个节点。
dts编译->dtb,设备树中共有的部分放进dtsi中,dts根据不同情况加以补充。
aliases、chosen(chosen并不是一个真实的设备,chosen节点主要是为了 uboot向 Linux内核传递数据,重点是 bootargs参数;uboot中的 fdt_chosen函数在设备树的 chosen节点中加入了 bootargs属性,并且还设置了 bootargs属性值。bootargs会作为 Linux内核的命令行参数,Linux内核启动的时候会打印出命令行参数(也就是 uboot传递进来的 bootargs的值 )
fdt--flattened device tree
属性
compatible--兼容的(设备节点的compatible属性是为了匹配Linux内核中的驱动程序)
(通过根节点的 compatible属性可以知道我们所使用的设备,一般第一个值描述了所使用的硬件设备名字,比如这里使用的是“imx6ull-14x14-evk” 这个设备,第二个值描述了设备所使用的 SOC,比如这里使用的是“imx6ull”这颗 SOC。Linux内核会通过根节点的compatible属性查看是否支持此设备,如果支持的话设备就会启动 Linux内核。)
找到匹配的 machine_desc的过程就是用设备树根节点的compatible属性值和 Linux内核中保存的所有 machine_desc结构的. dt_compat中的值比较,看看哪个相等,如果相等的话就表示找到匹配的 machine_desc。
#address-cells和#size-cells无符号32位整形,用于描述子节点的地址信息。reg = <address,length>
‘使用of_property_read_u32_array时要先申请存放结果的内核内存空间kmalloc。’
/home/lee/linux/atk-mp1/linux/my_linux/linux-5.4.31(/arch/arm/boot/dts/)
↑cparch/arm/boot/uImage/home/lee/linux/tftpboot
cparch/arm/boot/dts/stm32mp157d-atk.dtb/home/lee/linux/tftpboot
/home/lee/linux/tftpboot(.dtb、uImage所在路径)
sudocpgpioled.koledApp/home/lee/linux/nfs/rootfs/lib/modules/5.4.31/-f
保存配置项
./linux/atk-mp1/linux/alientek_linux目录存放正点原子出厂 Linux源码,
./linux/atk-mp1/linux/my_linux/linux-5.4.31放学习ing
cp .config ./arch/arm/configs/stm32mp1_atk_defconfig
pinctrl子系统主要工作内容如下:
获取设备树中pin信息;
根据获取到的pin信息来设置pin的复用功能;(除了 A0~AF15,还有 GPIO、ANALOG这两个;25.1.2.4 stm32-pinfunc.h文件 )
根据获取到的 pin信息来设置 pin的电气特性,比如上/下拉、速度、驱动能力等。
Linux内核针对 PIN的复用配置推出了 pinctrl子系统,对于 GPIO的电气属性配置推出了 gpio子系统
将某个外设所使用的所有 PIN都组织在一个子节点里面
gpio子系统顾名思义,就是用于初始化 GPIO并且提供相应的 API函数,比如设置 GPIO为输入输出,读取 GPIO的值等。
有源蜂鸣器只要通电就会叫,所以我们可以做一个供电电路,这个供电电路通过一个 IO来控制其通断,一般使用三极管来搭建这个电路。为什么我们不能像控制 LED灯一样,直接将GPIO接到蜂鸣器的负极,通过 IO输出高低来控制蜂鸣器的通断。这事因为蜂鸣器工作的电流比 LED灯要大,直接将蜂鸣器接到开发板的 GPIO上有可能会烧毁 IO,所以我们需要通过一个三极管来间接的控制蜂鸣器的通断,相当于加了一层隔离。
①、因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。 ②、因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。 ③、如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。
驱动加载成功(/proc/device-tree/)需要在/dev目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。--自动创建设备节点
应用程序调用 ioctl 函数向驱动发送控制信息,此函数响应并执行。此函数有三个参数:filp,cmd 和 arg,其中 filp是对应的设备文件,cmd 是应用程序发送过来的命令信息,arg 是应用程序发送过来的参数,在本章例程中 arg 参数表示定时周期。
func:定时器的回调函数(在timer_list结构体中),此函数的形参是当前定时器的变量(结构体变量)。
Linux内核提供了多种下半部机制:软中断、tasklet(在上半部,也就是中断处理函数中调用 tasklet_schedule函数就能使 tasklet在合适的时间运行)、工作队列
工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet。 --p819
“我们把已执行过的进程指令和数据在相关寄存器与堆栈中的内容称为进程上文,把正在执行的指令和数据在寄存器与堆栈中的内容称为进程正文,把待执行的指令和数据在寄存器与堆栈中的内容称为进程下文。”
软中断必须在编译的时候静态注册!
IRQ全称为Interrupt Request
irq_of_parse_and_map函数从 interupts属性中提取到对应的设备号 p830
spin_lock_irqsave/ spin_unlock_irqrestore被建议使用,因为这一组函数会保存中断状态,在释放锁的时候会恢复中断状态。
按键消抖:按键中断触发-打开定时器-即过一段时间等定时时间到-再到定时器处理函数中读取按键值
fd=open("/dev/xxx_dev", O_RDWR|O_NONBLOCK); /* 非阻塞方式打开 */
当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO就会将应用程序对应的线程挂起,直到设备资源可以获取为止。对于非阻塞 IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。
poll、epoll和 select可以用于处理轮询,应用程序通过 select、epoll或 poll函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。
poll函数本质上和 select没有太大的差别,但是 poll函数没有最大文件描述符限制
wait_event_interruptible(wq_head, condition) 与 wait_event函数类似(等待以 wq_head为等待队列头的等待队列被唤醒,前提是 condition条件必须满足(为真),否则一直阻塞。),但是此函数将进程设置为 TASK_INTERRUPTIBLE,就是 可以被信号打断(如Ctrl + C)。 条件condition为真时调用这个函数将直接返回0
使用等待队列实现阻塞访问重点注意两点: ①、将任务或者进程加入到等待队列头, ②、在合适的点唤醒等待队列,一般是中断处理函数里面。
depmod(depend module)可检测模块的相依性,供modprobe在安装模块时使用。
第 191~208行,key_read函数,在这个函数中我们会判断按键是否有按下或松开动作发生时,如果没有则调用 wait_event_interruptible把它加入等待队列当中,进行阻塞。如果等待队列被唤醒并且条件“KEY_KEEP != atomic_read(&status)”成立,则解除阻塞,继续下面的操作,也就是读取按键状态数据将其发送给应用程序。因为采用了 wait_event_interruptible函数,因此进入休眠态的进程可以被信号打断。在该函数中我们读取 status原子变量必须要使用atomic_read函数进行操作。
阻塞方式::按键按下触发中断-->开启定时器(消抖)-->时间到,定时器处理函数判断按键状态,唤醒进入休眠状态的进程<--将任务或进程加入到等待队列头<--应用层循环读取按键数据
非阻塞方式::按键按下触发中断-->开启定时器(消抖)-->时间到,定时器处理函数判断按键状态<--应用层循环轮询(select)读取按键数据
阻塞IO是等待设备可访问后再访问
非阻塞IO是查询设备是否可以访问
异步通知是设备通知自身可以访问
当按下键盘上的 CTRL+C组合键以后会向当前正在占用终端的应用程序发出 SIGINT信号,SIGINT信号默认的动作是关闭当前应用程序。
应用程序使用fcntl(fd, F_SETOWN, getpid())将本应用程序的进程号告诉给内核
printf为什么不加\n就不能输出相关的内容?:对于标准输出,需要输出的数据并不是直接输出到终端上,而是先缓存,\n相当于刷新
当应用程序通过“fcntl(fd, F_SETFL, flags|FASYNC)”/*改变fasync标记的时候*/,驱动程序file_operations操作集中的fasync函数就会执行。
flags=fcntl(fd, F_GETFD); /*获取当前的进程状态 */
fcntl(fd, F_SETFL, flags|FASYNC); /* 设置进程启用异步通知功能 */
驱动分离:将主机驱动和和设备驱动分隔开来
驱动(driver)--总线(bus)--设备(device)
driver和device匹配:
无设备树的时候:结构体platform_driver->.driver->.name
有设备树的时候:结构体platform_driver->.driver->.of_match_table->.compatible<-设备树结点
在 probe函数里面初始化 LED、注册字符设备驱动。也就是将原来在驱动入口函数(static int __init led_init(void))里面做的工作全部放到 probe函数里面完成。
/* platform驱动结构体 */
staticstructplatform_driverled_driver= {
.driver= {
.name="stm32mp1-led",
},
.probe=led_probe,
.remove=led_remove,
};
/home/lee/linux/atk-mp1/linux/my_linux/linux-5.4.31/drivers/pinctrl/stm32/pinctrl-stm32.c
/home/lee/linux/atk-mp1/linux/my_linux/linux-5.4.31 make uImage LOADADDR=0XC2000040 LOADADDR表示 Linux内核在 DDR中的加载地址为 0XC2000040
cp arch/arm/boot/uImage /home/lee/linux/tftpboot
cd /home/lee/linux/buildroot/buildroot-2020.02.6/ 要sudo make <- buildroot
sudo cp leddevice.ko leddriver.ko ledApp /home/lee/linux/nfs/rootfs/lib/modules/5.4.31/ -f
用了设备树就不需要再modprobe leddevice.ko
总体来说,platform驱动还是传统的字符设备驱动、块设备驱动或网络设备驱动,只是套上了一张“platform”的皮,目的是为了使用总线、驱动和设备这个驱动模型来实现驱动的分离与分层。
‘因此有两个结构体,platform_driver里(probe完成字符设备结构体的初始化)
echo heartbeat > /sys/class/leds/red/trigger , echo none > /sys/class/leds/red/trigger
misc 的意思是混合、杂项的,因此 MISC 驱动也叫做杂项驱动,也就是当我们板子上的某 些外设无法进行分类的时候就可以使用 MISC 驱动。MISC 驱动其实就是最简单的字符设备驱 动,通常嵌套在 platform 总线驱动中,实现复杂的驱动
#define MISCBEEP_NAME "miscbeep" /* 名字 */ <- static struct miscdevice beep_miscde
struct miscbeep_dev miscbeep; /* beep 设备 */
/* 匹配列表 */ platform_driver和设备树compatible = "alientek,beep";匹配
static const struct of_device_id beep_of_match[] = {
{ .compatible = "alientek,beep" },
{ /* Sentinel */ }
};
‘platform驱动与设备分离:beep_driver与设备树compatible匹配执行probe函数->...matched;应用层面:注册字符设备miscbeep(MISC设备结构体),成员变量.fops = &miscbeep_fops定义应用层面的写操作’
还存在另外一个结构体miscbeep,包含devid、cdev、class、device(三者抽离出来有了MISC设备结构体)、beep_gpio
MISC设备会自动创建 cdev
按键、鼠标、键盘、触摸屏等都属于输入(input)设备,Linux内核为此专门做了一个叫做 input子系统的框架来处理输入事件。输入设备本质上还是字符设备,只是在此基础上套上了 input框架,用户只需要负责上报输入事件,比如按键值、坐标等信息,input核心层负责处理这些事件。
不同的输入设备所代表的含义不同,按键和键盘就是代表按键信息,鼠标和触摸屏代表坐标信息。
“我们在使用 input子系统处理输入设备的时候就不需要去注册字符设备了,我们只需要向系统注册一个 input_dev即可。”
input子系统的所有设备主设备号都为13
input_dev注册过程如下:
使用input_allocate_device函数申请一个input_dev
初始化input_dev的事件类型以及事件值
使用input_register_device函数向Linux系统注册前面初始好的input_dev
卸载input驱动的时候需要先使用input_unregister_device函数注销掉注册的input_dev,然后使用input_free_device函数释放掉前面申请的input_dev
input_dev的事件码(也就是KEY模拟哪个按键)
EV_SYN同步事件
预分频PSC和自动加载ARR这两个寄存器就决定了PWM的周期值,而改变比较值(比较寄存器CCR)就可以改变PWM的占空比。
p988 HSYNC是水平(horizontal)同步信号,也叫做行同步信号,当产生次信号的话就表示开始显示新的一行了;VSYNC信号是垂直(vertical)同步信号,也叫做帧同步信号,当产生次信号的话就表示开始显示新的一帧图像了。
stm32mp157d-atk.dts:
②ltdc_ep0_out->rgb_panel_in:remote-endpoint属性是用来告诉 ltdc节点输出到那里,我们是用 RGB LCD屏做实验,所以输出到 rgb_panel_in接口。↕↕
③rgb_panel_in->ltdc_ep0_out:remote-endpoint属性告诉 LCD驱动,要从 LTDC节点里获取显示数据。输出接口节点的 compatible 属性值和在 panel-simple.c 文件里的 platform_of_match 数组中的 of_device_id 结构体的 compatible 成员属性值均为“alientek,lcd-rgb”
同时设置了 backlight 属性值为“&backlight”,与背光节点联系起来
④backlight节点->对应的是PWM背光控制驱动不是LCD驱动,以上↑三个节点需要添加到dts设备树中
④背光-PWM节点设置,设置的IO是TIM4_CH2,输出PWM(怎么知道 TIM4_CH2输出的PWM就是控制 LCD背光的呢?↓)
还需要一个节点来将 LCD背光和 TIM4_CH2连接起来->添加节点backlight,节点的 compatible属性值要为“pwm-backlight” ,与PWM 背光控制驱动程序匹配
(正点原子的 LCD接口背光控制 IO连接到了 STM32MP1的 PD13引脚上,我们需要将 PD13复用为 TIM4_CH2,然后配置 TIM4的 CH2输出 PWM信号,然后通过此 PWM信号来控制LCD屏幕背光的亮度)
panel-simple起什么作用?
drm_panel就是 DRM驱动的核心结构体,我们需要实现此结构体。drm_panel结构体是基类,panel_simple在drm_panel基础上增加了一些成员变量,相当于继承类。
重新设置 bootargs参数的 console内容:
setenv bootargs 'console=tty1 console=ttySTM0,115200 root=/dev/nfs nfsroot=169.254.8.149:/home/lee/linux/nfs/rootfs,proto=tcp rw ip=169.254.8.151:169.254.8.149:169.254.8.1:255.255.255.0::eth0:off'
libdrm起什么作用?
libdrm对底层接口(DRM driver提供的 ioctl)进行封装,向上层提供统一的 API接口。
Connector:连接物理显示设备的连接器,比如 DSI、HDMI等等。
CRTC:就是指显示控制器,在 DRM里有多个显存,就可以通过操作 CRTC来控制要显示哪个显存。
帧缓冲(framebuffer)是Linux为显示设备提供的一个接口,把显存抽象后的一种设备,允许上层应用程序在图形模式下直接对显示缓冲区进行读写操作。
W1C(write 1 to clear)写1清零
RTC--Real Time Clock 实时时钟
I2C使用两条线在主控制器和从机之间进行数据通信。一条是 SCL(串行时钟线),另外一条是 SDA(串行数据线)
每个 I2C器件都有一个设备地址,通过发送具体的设备地址来决定访问哪个 I2C器件。这是一个 8位的数据,其中高 7位是设备地址,最后 1位是读写位,为1的话表示这是一个读操作,为 0的话表示这是一个写操作。
对于一个 I2C 适配器,肯定要对外提供读写 API 函数,设备驱动程序可以使用这些 API 函数来完成读写操作。i2c_algorithm 就是 I2C 适配器与 IIC 设备进行通信的方法。
I2C总线驱动,或者说 I2C适配器驱动的主要工作就是初始化 i2c_adapter结构体变量,然后设置 i2c_algorithm中的 master_xfer函数。
中断、频率、DMA(Direct Memory Access) p1040
I2C适配器驱动(platform驱动) ≠ I2C设备驱动
‘应用程序-->I2C总线&设备驱动-->适配器驱动-软件-硬件->'I2C主机'-->I2C从设备’?
’I2C设备驱动里套字符设备驱动,在probe里完成‘;i2c_transfer函数最终会调用 I2C适配器中 i2c_algorithm里面的 master_xfer函数,对于 STM32MP1而言就是stm32f7_i2c_xfer这个函数。
‘设备驱动里(程序员编写):调用i2c_transfer(发指令、收数据后给dev结构体的成员变量赋值->copy_to_user)<-适配器提供的API函数master_xfer’
container_of函数的作用就是:给定结构体中的某个成员变量的地址、该结构体类型和该成员的名字来得到这个成员所在结构体变量的首地址。
当IIC设备和驱动匹配以后,probe函数执行,probe函数传递进来的第一个参数就是i2c_client(‘由系统生成,系统每检测到一个I2C从设备就会给这个设备分配一个i2c_client’),在i2c_client里面保存了此I2C设备所对应的i2c_adapter。
环境光强度(ALS,ambient)、接近距离(PS)、红外强度(IR)
CS/SS,Slave Select/Chip Select,这个是片选信号线,用于选择需要进行通信的从设备。I2C主机是通过发送从机设备地址来选择需要进行通信的从机设备的,SPI主机不需要发送从机设备(‘地址,因此设备树中@后面的数字就是对应 SPI芯片片选信号在 cs-gpios中的索引值’),直接将相应的从机设备片选信号拉低即可。
数据传输分为同步传输和异步传输,同步传输会阻塞的等待 SPI数据传输完成,异步传输不会阻塞的等到 SPI数据传输完成。
spi_master 对应于 i2c_adapter
spi_imx_data->spi_bitbang->spi_master & setup_transfer() & txrx_bufs()
SPI通过中断接收,中断处理函数为spi_imx_isr,此函数会调用spi_imx->rx(spi_imx)函数来完成具体的接收过程。
‘需要编写的.c文件里spi_driver(probe、remove...)->probe()函数内完成spi_device{cdev[ops(open、read...)]、class...}’
’片选gpio默认高电平,使用时拉低‘
probe函数会向驱动提供当前 SPI设备对应的 spi_device,因此把这个结构体赋值到 spi成员里(将 probe函数的 spi_device参数赋值给我们自定义的 spi成员变量),我们可以在字符串设备里操作 spi成员就可以操作对应的SPI设备。
STM32MP1的 UART本质上是一个 platform驱动,stm32_serial_driver(probe、remove...)->probe()函数初始化 uart_port(uart_ops...),然后将其添加到对应的 uart_driver中。
uart_port中的 ops成员变量很重要,因为 ops包含了针对 UART具体的驱动函数,Linux系统收发数据最终调用的都是 ops中的函数。
JP就是“jumper”,跳线接口的意思。
serial0是 uart4的别名,表示在系统启动生成一个名为“/dev/ttySTM0” 的设备文件,serial1就会生成“/dev/ttySTM1”如此类推,最多 8个。
1、主体I2C框架准备好
2、复位引脚和中断引脚,包括中断
3、初始化芯片,触摸IC为FT5426?
4、input子系统框架
5、在中断服务函数里面读取触摸坐标值,然后上报给系统
电容触摸屏驱动其实就是以下几种 linux驱动框架(IIC驱动、中断驱动、input子系统)的组合,新知识本章重点-input子系统下的多点电容触摸协议(Multi-touch,简称 MT),分为两种类型,Type A和 TypeB,Type B 设备驱动需要给每个识别出来的触摸点分配一个slot,后面使用这个slot来上报触摸点信息。
MT协议隶属于 linux的 input子系统, MT设备隶属于 input_dev,驱动通过大量的 ABS_MT事件向 linux内核上报多点触摸坐标数据。
request_threaded_irq中断线程化
↑ p1045 两个addr,一个是设备地址,一个是寄存器地址,设备不止一个寄存器
request_irq函数中最后一个参数void *dev(如果将 flags设置为 IRQF_SHARED的话,dev用来区分不同的中断,一般情况下将dev设置为设备结构体,dev会传递给中断处理函数作为第二个参数。在本例中,中断处理函数用传递的edt_ft5426_dev中的client完成I2C读写,然后input上报)
i2c_set_clientdata函数将 ft5426变量的地址绑定到 client,进行绑定之后,可以通过i2c_get_clientdata来获取 ap3216cdev变量指针。‘client又是ft5426结构体的成员变量’
EV_ABS -- 绝对坐标事件
当我们上报事件以后还需要使用 input_sync函数来告诉 Linux内核 input子系统上报结束
流程梳理:
触摸屏幕->触摸IC(有4个IO用于连接主控制器:SCL、SDA、RST和INT)->一般通过INT引脚来通知主控制器有触摸点按下,然后在INT中断服务函数中读取触摸数据。(建立I2C连接,input事件上报)
触摸屏的坐标信息、屏幕按下和抬起信息都属于linux的input子系统,因此'向linux内核上报'触摸屏坐标信息就得使用input子系统。只是,我们得按照linux内核规定的规则来上报坐标信息。(比如按键,我们需要在按键中断处理函数,或者消抖定时器中断函数中将按键值上报给 Linux内核,这样 Linux内核才能获取到正确的输入值。)
音频 CODEC(音频编解码芯片,Audio CODEC)的本质是 ADC和 DAC,叠加了对声音的处理单元如音效等
数字信号处理(Digital Signal Processing,简称DSP)
‘驱动分为两部分:1、I2C-配置 CS42L51如音量等;2、I2S(Inter-IC Sound)-音频数据传输’
CS42L51与 STM32MP1之间有两个通信接口:I2C和SAI,因此设备树中会涉及到 I2C和 SAI两个设备节点。其中 I2C用于配置 CS42L51,SAI接口用于音频数据传输,我们依次来配置一下这两个接口。
I2S总线用于主控制器和音频 CODEC芯片之间传输音频数据。
STM32MP1的 SAI外设就支持 I2S协议,CS42L51同样也支持 I2S,所以本章实验就是使用 I2S协议来完成的。
SAI接口(Synchronous Audio Interface,串行音频接口)灵活性高、配置多样,可支持多种音频协议。
线路输入line_in和mic输入不一样
I2C驱动sound/soc/codecs/cs42l51.c <-> sound/soc/generic/audio-graph-card.c <-> SAI接口驱动sound/soc/stm/stm32_sai.c
‘没有修改cs42l51.c驱动文件实现双通道录音’,CS42L51驱动是根据 ALSA架构编写的。
音频驱动使能以后还不能直接播放音乐或录音,我们还需要移植 alsa-lib和 alsa-utils(alsa-utils自带了amixer这个声卡设置工具)这两个东西。
CAN的全称为 Controller Area Network,也就是控制局域网络
CAN总线两端要各接一个 120Ω的端接电阻,用于匹配总线阻抗,吸收信号反射及回拨,提高数据通信的抗干扰能力以及可靠性。
D/R -- dominance/recessive 显性电平(逻辑0)/隐性电平(逻辑1)
ID的最高 7位不能全为隐性(1)。控制段,表示数据的字节数及保留位的段。仲裁段:连续输出显性电平最多的单元可继续发送。
帧由位构成,一个位由 4个段构成,每个段又由若干个 Tq组成,这个就是位时序。
SIT1042T/3国产 FD CAN收发芯片, CAN1_TX和 CAN1_RX对应STM32MP1的 PH13和 PI9这个两个引脚;向外界提供 CAN_H和 CAN_L总线,R36是一个 120欧的端接匹配电阻。
cansend can0 5A1#11.22.33.44.55.66.77.88 //“5A1” 是帧 ID
USB全称为 Universal Serial Bus,翻译过来就是通用串行总线。
USB可以HUB扩展,但是总带宽是固定的;ID线的高低电平表示 USB口工作在 HOST还是 DEVICE模式,ID=1:OTG(On The Go)设备工作在从机模式、ID=0:OTG设备工作在主机模式。 连电脑的时候电脑端供电拉高ID线,设备工作在从机模式;不连的时候默认接地ID=0:OTG设备工作在主机模式。
通俗来讲,OHCI就是 FS模式,也就是低速模式,EHCI是 HS模式,也就是高速模式。
SBU1和 SBU2引脚,这两个并不是 USB信号,而是用作其他功能的,比如 TypeC作为 DP接口使用的时候 SBU用作音频传输通道。 CC1和 CC2这 2个引脚为 USB3.1特有的配置通道引脚。DRP全称是 Dual Role Port,也就是双角色端口,可以理解为 OTG,DRP既可以做 DFP,也可以做 UFP。也可以在 DFP和 UFP之间进行动态切换。这个切换过程就用到了 CC引脚。通过专用的 TypeC芯片来控制 CC引脚实现 USB的主从切换。
OTG控制器支持片上全速 PHY、连接外部全速 PHY(↑)的 I2C接口和连接外部高速 PHY的 ULPL接口。
PHY 控制器有两个端口,刚好 usbphyc 节点里有两个子节点名字分别为:usbphyc_port0 和 usbphyc_port1,一个做 USBH 的 PHY 端口,另一个做 OTG 的 PHY 端口。
HID--Human Interface Device
USB OTG控制器节点 usbotg_hs(PHY接口 usbphyc_port1节点)
/ 控制 Typec的芯片是 STUSB1600(新版本改为 FUSB302),此芯片是使用 I2C协议和 CPU进行通讯,所以要使能I2C1。/
minors 为磁盘的次设备号数量,也就是磁盘的分区数量,这些分区的主设备号一样,次设备号不同。
在 block_device_operations结构体中并没有找到 read和 write这样的读写函数,而是有request_queue、request和 bio。
Linux内核使用 gendisk(block_device成员变量)来描述一个磁盘设备;根据blk_mq_tag_set(‘需要I/O调度才用’)对象分配请求队列。
可以把 blk_mq_tag_set看作真正的 IO读写操作(ops操作集'ramdisk_transfer'就是 IO操作'读写'),有了底层操作还不行,还需要 gendisk结构体为上层提供接口调用(fops就是实现上层调用的操作‘open、release...’)。
从 request_queue中取出一个一个的 request,然后再从每个 request里面取出 bio,最后根据 bio的描述将数据写入到块设备,或者从块设备中读取数据。bio保存了读写相关数据,比如从块设备的哪个地址开始读取、读取的数据长度,读取到哪里,如果是写的话还包括要写入的数据等。bio 中 bi_iter 这个结构体成员变量就用于描述物理存储设备地址信息,比如要操作的扇区地址。bi_io_vec 指向 bio_vec 数组首地址,bio_vec 数组就是 RAM 信息,比如页地址、页偏移以及长度
上层会将 bio提交给 I/O调度器,I/O调度器会将这些 bio构造成 request结构;I/O调度器一般用于像机械硬盘这样的存储设备。“机械磁盘 I/O调度 -- 电梯调度算法”;‘不使用请求队列即不用电梯调度算法,直接挨个处理bio’
虽然 ramdisk_make_request_fn(制造请求)函数第一个参数依旧是请求队列,但是实际上这个请求队列(request_queue)不包含真正的请求(request),所有的处理内容都在第二个 bio参数里面,所以 ramdisk_make_request_fn函数里面是全部是对 bio的操作。
unsigned long start = blk_rq_pos(req) << 9; /* blk_rq_pos获取到的是扇区地址,左移9位转换为字节地址,因为一个扇区为512(2^9)字节 */
嵌入式网络硬件分为两部分:MAC和 PHY,如果一款芯片数据手册说自己支持网络,一般都是说的这款 SoC内置 MAC,MAC类似 I2C控制器、SPI控制器一样的外设。但是光有 MAC还不能直接驱动网络,还需要另外一个芯片:PHY,因此对于内置 MAC的 SoC,其外部必须搭配一个 PHY芯片。
1、SOC内部没有网络 MAC外设
2、SOC内部集成网络 MAC外设 :芯片内置的 MAC会有网络加速引擎,比如网络专用 DMA,网络处理效率会很高
MII/RMII或 GMII/RGMII接口是用来传输网络数据的,另外主控 SoC需要配置或读取 PHY芯片,也就是读写 PHY的内部寄存器,所以还需要一个控制接口,叫做 MIDO。
PHY常用寄存器: BMCR(Basic Mode Control Register)、BMSR(Basic Mode Status Register)
Linux内核使用 net_device结构体表示一个具体的网络设备,net_device是整个网络驱动的灵魂。网络驱动的核心就是初始化 net_device结构体中的各个成员变量,然后将初始化完成以后的 net_device注册到 Linux内核中。
网络设备有多种,不只有以太网一种,内核针对不同的网络设备在alloc_netdev的基础上提供了一 层封装申请函数,比如针对以太网封装的 net_device申请函数是 alloc_etherdev和alloc_etherdev_mq
sk_buff是 Linux网络重要的数据结构,用于管理接收或发送数据包;各个协议层在 sk_buff中添加自己的协议头
结构体(struct)和共用体(union)的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。
应用举例:把学生信息和教师信息放入一张表格。学生信息包括姓名、编号、性别、职业、分数,教师的信息包括姓名、编号、性别、职业、教学科目。就第5个成员变量不同,可能是score或者course,用union表示
NAPI的核心思想就是不全部采用中断来读取网络数据,而是采用中断来唤醒数据接收服务程序,在接收服务程序中采用 POLL的方法来轮询处理数据。
net_device_ops↑;
联系:struct stmmac_priv *priv = netdev_priv(dev);net_device的私有数据为Ethernet device
stmmac_dvr_probe函数设置了网卡驱动的 net_dev_ops操作集为 stmmac_netdev_ops ?
stm32_ops就是根据设备树参数去设置网络相关的寄存器↓
STM32MP1网络驱动主要分两部分:STM32MP1网络外设 MAC驱动(设备树结点ethernet0:ethernet@5800a000)以及 PHY芯片驱动(在ðernet0下又追加子节点mdio0(再子节点phy0)、phy- handle:连接到此网络设备的 PHY 芯片句柄),MAC驱动是 ST编写的,PHY芯片有通用驱动文件,有些 PHY芯片厂商还会针对自己的芯片编写对应的 PHY驱动。
①struct stm32_dwmac(stm32_ops是ST官方自定义的,赋值给 stm32_dwmac结构体里的 ops成员)这两个结构体主要是用作初始化网络,plat_stmmacenet_data结构体主要用于注册网络设备。
②platform平台驱动框架 static struct platform_driver stm32_dwmac_driver(.probe = stm32_dwmac_probe);probe的参数platform_device;
probe完成的工作:
使用 stmmac_get_platform_resources函数获取设备树上的资源,保存到 struct stmmac_resources类型的结构体里。
保存网络设备的参数到plat_dat结构体
给 dwmac结构体开空间、const struct stm32_ops *data = of_device_get_match_data(&pdev->dev);获取 ST官方自定义的 stm32_ops操作集、从设备树里获取时钟和基地址、以plat_dat为参数初始化网络设备
调用 stmmac_dvr_probe函数进行网络注册,同时完成GMII/RGMII接口初始化(此函数会调用 stmmac_mdio_register函数来向内核注册MDIO总线(包括初始化读写PHY寄存器的两个函数),寻找注册 PHY设备(‘结点嵌套:ethernet0->mdio0->phy0’))
stm32_dwmac_probe->plat_dat = stmmac_probe_config_dt(pdev, &stmmac_res.mac);plat_dat变量指针类型为 plat_stmmacenet_data结构体,plat_dat结构体主要是保存网络设备的参数,根据 plat_dat的参数将网络设备注册进内核。stmmac_probe_config_dt函数作用是根据设备树上的属性值去填充 plat_dat各个成员。
PHY子系统也是遵循设备、总线、驱动模型的,设备和驱动就是 phy_device和phy_driver,总线就是 MDIO总线。对于本章教程而言,并不是通过 of_driver_match_device 中compatible属性值来完成 PHY 驱动和设备匹配的,而是用phy_id和 phy_id_mask。 如果 PHY设备和 PHY驱动匹配,那么就使用指定的 PHY驱动,如果不匹配的话就使用Linux内核自带的通用 PHY驱动。
正点原子的 STM32MP1开发板目前支持两种接口的 WIFI:USB和 SDIO,其中 USB WIFI使用的芯片为 RTL8188EUS,SDIO接口的 WIFI使用芯片为 RTL8723DS,这两个都是 realtek公司出品的 WIFI芯片。
cfg80211配置支持 IEEE 802.11,有些模块是需要固件配合使用的 ,比如 cfg80211.ko 驱动模块要读取/lib/firmware/regulatory.db文件。
ping -I 192.168.1.196 www.baidu.com
//192.168.1.196为连接到WIFI热点后由udhcpc命令从路由器申请 到的IP地址
//-I 是指定执行ping操作的网卡IP地址,我们要使用wlan0去ping百度网站,因此要通过“-I”指定wlan0的IP地址。
p1381后未改/etc/wpa_supplicant.conf即未连接WiFi
虽然很多4G模块都是 MiniPCIE接口的,但是大家稍微深入研究一下就会发现,这些 4G模块虽然用了 MiniPCIE接口,但是实际上的通信接口都是 USB,所以 4G模块的驱动就转换为了 USB驱动。
APN(Access Point Name),移动卡的 APN为 cmnet,联通卡的 APN为 3gnet,电信卡的 APN为cenet
GNSS(Global Navigation Satellite System,全球导航卫星系统)
ME3630支持通过 ppp拨号上网,也支持使用 ECM接口上网
ME3630虚拟出了 3个 USB设备,分别为 ttyUSB0~ttyUSB2: ttyUSB1为 GNSS接口、如果是联通或移动的卡就是用 ttyUSB2
Nano -- 10^-9
‘没有4G模块以及天线做实验。。。’
我们在前面学习 I2C和 SPI驱动的时候,针对 I2C和 SPI设备寄存器的操作都是通过相关的 API函数进行操作的。这样 Linux内核中就会充斥着大量的重复、冗余代码,但是这些本质上都是对寄存器的操作,所以为了方便内核开发人员统一访问 I2C/SPI设备的时候,为此引入了 Regmap子系统。
Linux下使用 i2c_transfer来读写 I2C设备中的寄存器,SPI接口的话使用 spi_write/spi_read等。比如 icm20608这个芯片既支持 I2C接口,也支持 SPI接口。代码的复用性低,改写的工作量大。
①regmap是 Linux内核为了减少慢速 I/O在驱动上的冗余开销,提供了一种通用的接口来操作硬件寄存器。
②另外,regmap在驱动和硬件之间添加了 cache,降低了低速 I/O的操作次数,提高了访问效率,缺点是实时性会降低。
regmap_config 结构体(如设置reg、val位数..)就是用来初始化 regmap 的,需要根据所使用的接口来选择合适的 regmap初始化函数。如SPI接口初始化函数为 regmap_init_spi
传感器如加速度计、光传感器、陀螺仪、气压计、磁力计等内部都会有个 ADC,传感器对外提供 IIC或者 SPI接口,SOC可以通过 IIC或者 SPI接口来获取到传感器内部的 ADC数值,从而得到想要测量的结果。Linux内核为了管理这些日益增多的 ADC类传感器,特地推出了 IIO(Industrial I/O)子系统。
struct iio_dev -> const struct iio_info *info(我们从用户空间读取 IIO设备内部数据,最终调用的就是 iio_info里面的函数。)
一般我们会将自己定义的设备结构体变量作为 iio_dev的私有数据,通过 iio_device_alloc函数同时完成 iio_dev和设备结构体变量的内存申请。申请成功以后使用 iio_priv函数来得到自定义的设备结构体变量首地址。
IIO驱动的基础框架就是 IIC或者 SPI(p1436)
用户空间向驱动程序读写数据->调用iio_info的读写函数(比如命令行cat in_accel_x_raw调用的是)->regmap读写函数(对spi..的包装)与传感器寄存器进行交互,读写传感器相应寄存器
注:通过ind偏移读取XYZ
注读量程需要读这两位分辨率为量程除bit位数
!tips:用枚举变量做数组下标,便于知道含义,但要严格对应;枚举元素本身由系统定义了一个表示序号的数值,从0开始顺序定义为0,1,2…。如在weekday中,sun值为0,mon值为1,…,sat值为6。
较上小节,直接按顺序处理数据即可,不用再去读单独的如raw等
例:以中断为触发器->中断处理函数iio_trigger_generic_data_rdy_poll->又调用iio_trigger_poll->icm20608_trigger_handler(触发器下半部函数?)读取数据到缓冲区
STM32MP157 ADC驱动文件有两个:stm32-adc-core.c和 stm32-adc.c。stm32-adc-core.c是ADC核心层,主要用于 ADC电源等初始化,我们需要重点关注的是 stm32-adc.c这个文件。stm32-adc.c主体框架是 platform,配合 IIO驱动框架实现 ADC驱动。
单总线的定义是主机和从机通过一根线进行通信
DS18B20为单总线接口的温度传感器;GND DATA VCC。DHT11是一款单总线接口的温湿度一体化的数字传感器
引入工作队列去处理 ds18b20 的初始化和获取温度。
定时器主要作用是定时去触发工作队列,然后在工作队列去初始化ds18b20、读取温度值。 -> 编写MISC框架(.open .read ...),在内核里获取的温度通过MISC拷贝数据到用户层(.read->ds18b20_read())
写法梳理:先platform驱动框架(probe函数完成GPIO、misc设备、定时器、工作队列初始化,注册misc设备,probe函数内构造一个struct miscdevice)--》MISC设备框架(构造好static struct file_operations ds18b20_fops(.open .read ...)在probe函数内用其初始化probe函数内构造的miscdevice)《-- 构造一个struct ds18b20_dev,放入需要的struct miscdevice mdev;int gpio;接收buffer;struct timer_list timer;struct work_struct work;
也可以在probe函数外构造好一个truct miscdevice,probe函数内就只需要注册了,参考例程19_miscbeep
struct platform_device->struct device->成员变量of_node获取结点设置的GPIO口有用
没有买这两个传感器,实验未做..
C语言中的函数调用涉及到出栈入栈,出栈入栈就要对堆栈进行操作,所谓的堆栈其实就是一段内存,这段内存比较特殊,由 SP指针访问,SP指针指向栈顶。芯片一上电 SP指针还没有初始化,所以 C语言没法运行,对于有些芯片还需要初始化 DDR,因为芯片本身没有 RAM,或者内部 RAM不开放给用户使用,用户代码需要在DDR中运行,因此一开始要用汇编来初始化 DDR控制器。
“存储地址”就是可执行文件存储在哪里,可执行文件的存储地址可以随意选择。“运行地址”就是代码运行的时候所处的地址,这个在链接的时候就已经确定好了,代码要运行,那就必须处于运行地址处,否则代码肯定运行出错。
I.MX6U支持 SD卡、EMMC、NAND启动,因此代码可以存储到 SD卡、EMMC或者 NAND中,但是要运行的话就必须将代码从 SD卡、EMMC或者NAND中拷贝到RAM/DDR其运行地址(链接地址)处,“存储地址”和“运行地址”可以一样,比如STM32的存储起始地址和运行起始地址都是 0X08000000。
gnueabihf:gnu+eabi(嵌入式二进制应用程序接口,Embedded Application Binary Interface for the ARM Architecture)
BOOT_MODE[1:0] 01--串行下载 10--内部BOOT模式;启动设备通过BOOT_CFG配置
内部BOOT模式:芯片执行内部的 boot ROM代码,这段 boot ROM代码会进行硬件初始化(一部分外设),然后从 boot设备(就是存放代码的设备、比如 SD/EMMC、NAND)中将代码拷贝出来复制到指定的 RAM中,一般是 DDR。
总结:编译出来的.bin文件不能直接烧写到 SD卡中,需要在.bin文件前面按照格式加上 IVT(Image Vector Table)、Boot Data和 DCD(Device Configuration Data)(分别有什么用???)这三个数据块。通过正点原子提供的软件
汇编->C语言环境,比如初始化 DDR、设置堆栈指针 SP等等,当这些工作都做完以后就可以进入C语言环境
elf--Executable and Linkable Format
1 objs := start.o main.o
2
3 ledc.bin:$(objs)
//-Ttext就是指定链接地址,“-o” 选项指定链接生成的 elf文件名
4 arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf $^
//“-O”选项指定以什么格式输出,后面的“binary”表示以二进制格式输出,选项“-S”表示不要复制源文件中的重定位信息和符号信息,“-g”表示不复制源文件中的调试信息
5 arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@
//“-D”选项表示反汇编所有的段
6 arm-linux-gnueabihf-objdump -D -m arm ledc.elf > ledc.dis
7
8 %.o:%.s
9 arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<
10
11 %.o:%.S
12 arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<
13
14 %.o:%.c
15 arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<
16
17 clean:
18 rm -rf *.o ledc.bin ledc.elf ledc.dis
链接脚本:ALIGN(4)表示 4字节对齐。也就是说段“.data”的起始地址要能被 4整除,一般常见的都是 ALIGN(4)或者 ALIGN(8),也就是 4字节或者 8字节对齐。.bss段是定义了但是没有被初始化的变量,需要手动对.bss段的变量清零,因此需要用“__bss_start”和“__bss_end”保存.bss段的起始和结束地址。
常用:LDR--load register、STR--store register
MRS指令用于将特殊寄存器(如 CPSR(current program status register) 和 SPSR)中的数据传递给通用寄存器,要读取特殊寄存器的数据只能使用 MRS指令!MSR指令和 MRS刚好相反,MSR指令用来将普通寄存器的数据传递给特殊寄存器
LDM(load much):多数据加载,将地址上的值加载到寄存器上STM(store much):多数据存储,将寄存器的值存到地址上
IA:(Increase After) 每次传送后地址加4,其中的寄存器从左到右执行,例如:STMIA R0,{R1,LR} ,先存R1、再存LR
IB:(Increase Before)每次传送前地址加4,同上
DA:(Decrease After)每次传送后地址减4,其中的寄存器从右到左执行,例如:STMDA R0,{R1,LR} ,先存LR,再存R1
DB:(Decrease Before)每次传送前地址减4,同上
/*清除BSS段 */ ///???为什么 BSS段是什么
ldr r0, _bss_start
ldr r1, _bss_end
mov r2, #0
bss_loop:
stmia r0!, {r2}
cmp r0, r1 /*比较r0和r1里面的值 */
ble bss_loop /*如果r0地址小于等于r1,继续清除bss段 */
BL(branch and link instruction) 相对寻址、label、位置无关码
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。