NanoPi_Neo_Core底层开发
NanoPi Neo Core底层开发
一、编译下载镜像
各个部分编译后的文件都在对应的文件夹中,如编译friendlycore-xenial_4.14_armhf系统,对应的各个镜像就都在friendlycore-xenial_4.14_armhf
文件夹中
最后生成的img文件会在out
文件夹中
1. 编译
1.1编译uboot
1 | UBOOT_SRC=../UBOOT/u-boot-sunxi-v2017.x ./build-uboot.sh friendlywrt_4.14_armhf |
1.2编译Linux内核
1 | KERNEL_SRC=../LINUX/linux-sunxi-4.14.y ./build-kernel.sh friendlycore-xenial_4.14_armhf |
编译内核时会自动编译设备树,如果想重新单独编译设备树,则有:
单独编译设备树
1 | make distclean |
反编译设备树
1 | ./scripts/dtc/dtc -I dtb -O dts -o ../sun8i-h3-nanopi-neo-core.dts ./arch/arm/boot/dts/sun8i-h3-nanopi-neo-core.dtb |
1.3编译根文件系统
1 | echo hello > friendlycore-xenial_4.14_armhf/rootfs/root/welcome.txt |
2. 下载
2.1生成img镜像
生成SD卡的img镜像
1 | sudo ./mk-sd-image.sh friendlycore-xenial_4.14_armhf [h3-sd-friendlycore.img(镜像名字,默认按照时间命名)] |
烧录img镜像到SD卡
1 | sudo dd if=out/h3-sd-friendlycore.img bs=1M of=/dev/sdX |
2.2直接烧录
1 | sudo ./fusing.sh /dev/sdX friendlycore-xenial_4.14_armhf |
3.分区情况
分区名字 | 文件系统 | 起始地址 | 长度 | 镜像名字 | 内容 |
---|---|---|---|---|---|
boot0 | raw | 0x2000 | 0x17FE000(24M-8k) | u-boot-sunxi-with-spl.bin | uboot |
boot | fat | 0x1800000 | 0x2800000(40M) | boot.img | kernel(zImage)+dtb |
rootfs | ext4 | 0x4000000 | 0x48400000(1156M) | rootfs.img | 根文件系统 |
userdata | ext4 | 0x4c400000 | 0x0 | userdata.img |
3.1自己创建img镜像文件
1 | dd if=/dev/zero of=镜像文件 bs=1024 count=0 seek=镜像大小#单位:k,uboot+kernel(zImage)+dtb+rootfs=1220M,脚本中默认(7800*1000*1000)/1024 |
例子:
1 | dd if=/dev/zero of=out/busyboxflash.img bs=1024 count=0 seek=1331200#1331200k -> 1300M ==> 留给userdata 80M,修改partmap.txt中userdata大小为0x500 0000 |
dd命令详解
dd:用指定大小的块拷贝一个文件,并在拷贝的同时进行指定的转换。
if
=文件名:输入文件名,缺省为标准输入。即指定源文件。<if=inputfile>of
=文件名:输出文件名,缺省为标准输出。即指定目的文件。< of=output file >- ibs=bytes:一次读入bytes个字节,即指定一个块大小为bytes个字节。
obs=bytes:一次输出bytes个字节,即指定一个块大小为bytes个字节。
bs
=bytes:同时设置读入/输出的块大小为bytes个字节。 - cbs=bytes:一次转换bytes个字节,即指定转换缓冲区大小。
- skip=blocks:从输入文件开头跳过blocks个块后再开始复制。
seek
=blocks:从输出文件开头跳过blocks个块后再开始复制。
(注意:通常只用当输出文件是磁盘或磁带时才有效,即备份到磁盘或磁带时才有效。count
=blocks:仅拷贝blocks个块,块大小等于ibs指定的字节数。- conv=conversion:用指定的参数转换文件。
- ascii:转换ebcdic为ascii
- ebcdic:转换ascii为ebcdic
- ibm:转换ascii为alternateebcdic
- block:把每一行转换为长度为cbs,不足部分用空格填充
- unblock:使每一行的长度都为cbs,不足部分用空格填充
- lcase:把大写字符转换为小写字符
- ucase:把小写字符转换为大写字符
- swab:交换输入的每对字节
- noerror:出错时不停止
- notrunc:不截短输出文件
- sync:将每个输入块填充到ibs个字节,不足部分用空(NUL)字符补齐。
/dev/null和/dev/zero的区别
/dev/null
,外号叫无底洞,你可以向它输出任何数据,它通吃,并且不会撑着!/dev/zero
,是一个输入设备,你可你用它来初始化文件。该设备无穷尽地提供0,可以使用任何你需要的数目——设备提供的要多的多。他可以用于向设备或文件写入字符串0。
二、开发环境搭建
使用官方提供ubuntucore文件系统
切换成root用户:
1 | su root |
普通用户:
1 | 用户名: pi |
Root用户:
1 | 用户名: root |
可能会遇到无法切换的行为,此时可以修改默认登录账号为root:
1 | cd /etc/systemd/system/serial-getty@ttyS0.service.d/ |
更新软件包
1 | apt-get update |
自己制作根文件系统
详见另一篇博客——《构造根文件系统》
1 | # 拷贝lib库 |
etc/init.d/rcS文件:
1 |
|
chmod 777 rcS
etc/fstab文件:
1 | #device mount-point type options dump pass |
etc/inittab文件:
1 | #etc/inittab |
打包成img镜像
1 | # 获得待打包成img镜像的rootfs文件夹大小 |
上述这一系列操作已经被友善之臂写入脚本build-rootfs-img.sh
中,具体调用方面为:
1 | ./build-rootfs-img rootfs文件夹所在目录 生成img的镜像文件所在的目录 |
1 | eg: |
**注:**重新配置文件系统后后,需要重新运行友善之臂的脚本build-kernel.sh
编译一次内核,该脚本会将内核中的模块安装至该文件系统镜像中
自己做的rootfs平台,build-kernel.sh中需要注释一个平台检测的代码块,同时在./tools/update_kernel_bin_to_img.sh脚本中第122行需要将echo "IMG_SIZE=${IMG_SIZE}" > ${OUT}/${TARGET_OS}_rootfs-img.info
改为echo "IMG_SIZE=${IMG_SIZE}"
1 | KERNEL_SRC=../LINUX/linux-sunxi-4.14.y ./build-kernel-others.sh 放rootfs.img和放生成的boot.img的文件夹 |
参考前文,下载img镜像方法如下:
1 | dd if=/dev/zero of=out/busyboxflash.img bs=1024 count=0 seek=133120#133120k -> 130M 删除partmap.txt中userdata分区 |
经测试,没有重新编译内核会导致内核启动失败,错误信息:
1 | U-Boot SPL 2017.11 (Oct 12 2019 - 17:03:15) |
根据日志分析貌似是在43100000地址处找不到脚本,但是这个地址是在rootfs中的。。。。。。
不会搞了,留个坑以后来填。。。。。。
三、驱动编写
暂时采用官方镜像,镜像名字为nanopi-neo-core_sd_friendlycore-xenial_4.14_armhf_20190823.img
,该镜像中采用的rootfs为UbuntuCore 18.04
写在前面
内核版本查看方法
1 | cat /proc/version |
挂载pc上文件系统
1 | mount -t nfs -o nolock 192.168.2.101:/home/null/Code/NanoPi_NEO_Core/nfs /mnt |
挂载报错尝试安装nfs-common
报错:
1
2
3
4
5
6
7 mount: wrong fs type, bad option, bad superblock on 192.168.2.101:/home/null/nfs_root,
missing codepage or helper program, or other error
(for several filesystems (e.g. nfs, cifs) you might
need a /sbin/mount.<type> helper program)
In some cases useful info is found in syslog - try
dmesg | tail or so.安装nfs-common:
1 sudo apt-get install nfs-common
pc端检查是否配置成功(自己挂接自己)(绝对路径)
1 | sudo mount -t nfs -o nolock localhost://home/null/Code/NanoPi_NEO_Core/nfs /mnt/nfs#挂接 |
一键挂载脚本
1 |
|
记得chmod +x
卸载脚本
1 |
|
记得chmod +x
更换源
参考: http://mirrors.ustc.edu.cn/help/ubuntu-ports.html
修改 /etc/apt/sources.list
文件中镜像源服务器地址 http://ports.ubuntu.com/ 为 http://mirrors.ustc.edu.cn/ubuntu-ports/
1 | deb http://mirrors.ustc.edu.cn/ubuntu-ports/ bionic main restricted universe multiverse |
更新源:
1 | sudo apt update |
1.按键驱动程序
1.1修改设备树
编译设备树
1 | make dtbs ARCH=arm CROSS_COMPILE=arm-linux- -j8 |
查看系统中是否存在对应节点
1 | ls /proc/device-tree/ |
设备树中的GPIO节点
假设PA6和PA7上接入两个LED,通过编写一个GPIO的驱动程序来控制LED的亮灭,在设备树文件sun8i-h3-nanopi-m1.dts的根节点增加一个myleds子节点 :
1 | myleds { |
节点中的led-gpios属性,引用了pinctrl信息,sunxi-h3-h5.dtsi中有关于该控制器的描述,
pio: pinctrl@01c20800 {
/* compatible is in per SoC .dtsi file */
reg = <0x01c20800 0x400>;
interrupts = <GIC_SPI 11 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 17 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&ccu CLK_BUS_PIO>, <&osc24M>, <&osc32k>;
clock-names = "apb", "hosc", "losc";
gpio-controller;
#gpio-cells = <3>;
interrupt-controller;
#interrupt-cells = <3>;
......
};
myleds子节点描述了两个GPIO信息,具有多个gpio口信息的属性值: <&gpio控制器节点名 具体gpio口的标识符>,<&gpio控制器节点名 具体gpio口的标识符> …;
具体gpio口的标识符是由多个数字组成, 数字的个数由所用的gpio控制器节点里的#gpio-cells属性值指定.。全志H3 共有七组GPIO ,也即七个bank,分别为PA (PA0-21),PC(PC0-16) PD (PD0-17), PE(PE0-15),PF(PF0-6) PG(PG0-13) ,PL(PL0-PL11),在内核中PA-PG是一个pinctrl控制器,而PL是另一个pinctrl控制器 。
由#gpio-cells = <3>可以推出GPIO属性需要用3个u32的数据描述,第一个参数代表该GPIO位于哪个bank,第二个参数代表该bank下的序号,第三个参数代表默认电平,myleds节点下的GPIO_ACTIVE_HIGH这个宏就定义于include/dt-bindings/gpio/gpio.h
1.2编写驱动
1.2.1检查注册的驱动
-
驱动安装好后,会在
/dev/
目录下创建同名目录 -
或者通过
cat /proc/devices
命令查看多出来的驱动
1.2.2gpiolib及gpio操作
经典接口
gpio_request
:驱动中要想使用某一个gpio,就必须先调用gpio_request接口来向内核申请,得到允许后才可以去使用这个gpiogpio_free
: 对应gpio_request,用来释放申请后用完了的gpiogpiochip_is_requested
: 接口用来判断某一个gpio是否已经被申请了gpio_direction_input
/gpio_direction_output
: 接口用来设置GPIO为输入/输出模式(不推荐直接设置寄存器)
- 一般来说,gpio的资源申请和释放操作应该放在驱动模块的加载与卸载函数内
- gpio_request的第一个参数是需要申请的gpio号。第二个参数是我们给该gpio起个名字
- 申请完了之后对这个gpio进行模式设置,我们这里用gpio_direction_output 设置成输出模式,并且默认输出1让led灭
新接口
devm_gpio_request_one
: 自带初始化电平功能,并且会在模块卸载时自动释放gpio,十分简便
读写gpio
gpio_get_value
:读gpiogpio_set_value
:写gpio
控制台中查看当前gpio占用情况的方法
内核中提供了虚拟文件系统debugfs,里面有一个gpio文件,提供了gpio的使用信息
- 使用
mount -t debugfs debugfs /tmp
把debugfs挂接到/tmp下,再重新进入/tmp后就能看到一个名为gpio的文件 cat /tmp/gpio
即可得到gpio的所有信息,使用完后umount /tmp
卸载掉debugfs
1.3调试
printk
-
查看
printk
输出的日志:通过
dmesg
和more
、tail
、less
、grep
的结合查看日志1
2
3
4
5
6
7
8
9
10#输出全部信息
dmesg
#输出前20行
dmesg | head -20
#输出后20行
dmesg | tail -20
#输出包含usb的信息(-i:忽略大小写)
dmesg | grep -i usb
#清空dmesg环形缓冲区中的日志
dmesg -c -
或者直接设置日志的输出的等级:
1
2
3echo $level > /proc/sys/kernel/printk#默认为4 4 1 7
#如:
echo 8 4 1 7 > /proc/sys/kernel/printk#所有级别日志均可打印输出/proc/sys/kernel/printk中对应的四个数分别对应于:
- ①控制台日志级别:优先级高于该值的消息将被打印至控制台。
- ②缺省的消息日志级别:将用该值来打印没有优先级的消息。
- ③最低的控制台日志级别:控制台日志级别可能被设置的最小值。
- ④缺省的控制台:控制台日志级别的缺省值。
日志缓冲区的每一行文本开头具有级别标记, 级别值越小则优先级越高.
系统定义了8个消息级别, 级别号从0到7分别为:
-
致命级(KERN_EMESG),
-
警戒级(KERN_ALERT),
-
临界级(KERN_CRIT),
-
错误级(KERN_ERR),
-
告警级(KERN_WARN),
-
注意级(KERN_NOTICE),
-
通知级(KERN_INFO),
-
调试级(KERN_DEBUG).
后台运行
-
&
当在前台运行某个作业时,终端被该作业占据;可以在命令后面加上& 实现后台运行。例如:sh test.sh &
当成功地提交进程以后,就会显示出一个进程号,可以用它来监控该进程,或杀死它。(
ps -ef | grep 进程号
或者kill -9 进程号
) -
nohup
使用&命令后,作业被提交到后台运行,当前控制台没有被占用,但是一但把当前控制台关掉(退出帐户时),作业就会停止运行。nohup命令可以在你退出帐户之后继续运行相应的进程。nohup就是不挂起的意思( no hang up)。该命令的一般形式为:
1
nohup command &
如果使用nohup命令提交作业,那么在缺省情况下该作业的所有输出都被重定向到一个名为nohup.out的文件中,除非另外指定了输出文件:
1
nohup command > myout.file 2>&1 &
使用了nohup之后,很多人就这样不管了,其实这样有可能在当前账户非正常退出或者结束的时候,命令还是自己结束了。所以在使用nohup命令后台运行命令之后,需要使用exit正常退出当前账户,这样才能保证命令一直在后台运行。
-
ctrl + z
可以将一个正在前台执行的命令放到后台,并且处于暂停状态。
-
jobs
查看当前有多少在后台运行的命令。
jobs -l
选项可显示所有任务的PID,jobs的状态可以是running, stopped, Terminated。但是如果任务被终止了(kill),shell 从当前的shell环境已知的列表中删除任务的进程标识。
2.内核定时器
3.中断
上半部和下半部
-
软中断
软中断必须在编译的时候静态注册
- open_sotfirq:打开软中断
- raise_sotfirq:触发软中断
-
tasklet:利用软中断来实现的另一种下半部机制,在软中断和tasklet之间建议选tasklet
- tasklet_init:初始化tasklet_struct结构体,或直接使用宏DECLARE_TASKLET定义+初始化
- tasklet_schedule:一般在上半部中调用,使要调度的tasklet在合适的时候运行
-
工作队列:在进程上下文执行,允许睡眠或重新调度(下半部可以睡眠就可以交给工作队列),要执行的工作会交给一个内核工作者线程去执行
- 宏INIT_WORK:初始化工作
- 宏DECLARE_WORK:定义+初始化工作
- schedule_work:类似tasklet_schedule,调度传入的工作
3.1修改设备树
参考内核绑定信息:Documentation/devicetree/bindings/arm/gic.txt
对于一般的ARM处理器来说,#interrupt-cells=<3>
对应的三个cell分别为:
- 中断类型
- 0:SPI中断
- 1:PPI中断
- 中断号
- SPI:0~987
- PPI:0~15
- 标志
- bit[3:0]:中断触发类型
- 1:上升沿
- 2:下降沿
- 4:高电平
- 8:低电平
- bit[15:8]:PPI中断的CPU掩码
- bit[3:0]:中断触发类型
相关函数
- irq_of_parse_and_map:从设备树中中断节点中的interrupts属性提取对应的中断号
- gpio_to_irq:获取gpio对应的中断号
编译设备树
1 | make dtbs ARCH=arm CROSS_COMPILE=arm-linux- -j8 |
查看系统中是否存在对应节点
1 | ls /proc/device-tree/ |
设备树中的中断节点
- interrupt-parent = <&pio>;//属于pio中断控制器
- interrupts = <0 7 IRQ_TYPE_EDGE_BOTH>;
- 第一个参数:第几个GPIO(这里是GPIOA)
- 第二个参数:第几个外部中断
- 第三个参数:触发沿
3.2编写驱动
3.3调试
中断注册成功后可以通过以下命令来查看是否注册到系统中:
1 | cat /proc/interrupts |
4.input子系统
4.1修改设备树
如果需要使用内核自带的gpio_keys.c
驱动,可以参考内核中Documentation/devicetree/bindings/input/gpio-keys.txt
,需要设备树中的节点满足以下要求:
-
节点名字为
“gpio-keys”
-
gpio-keys节点的compatible属性必须为
“gpio-keys”
-
所有的按键都是gpio-keys的子节点,并可以使用如下属性描述自己:
- gpios:按键连接的gpio信息
- interrupts:按键使用的中断信息(可选)
- lable:按键名字
- linux,code:要模拟的按键值
-
如果要支持连按的话需要加入
autorepeat
属性例子:
1
2
3
4
5
6
7
8
9
10gpio_keys {
compatible = "gpio-keys";
autorepeat;//支持连按
k1 {
label = "k1";//按键名称
linux,code = <KEY_POWER>;//要模拟的按键值
gpios = <&pio 0 7 GPIO_ACTIVE_LOW>;//按键连接的gpio信息
interrupts = <0 7 IRQ_TYPE_EDGE_BOTH>;//中断信息 PA7 --> EINT7
};
};
4.2编写驱动
Linux中已经实现了input输入子系统框架,因此无需再次注册设备,系统中input框架中的所有设备的主设备号均为13
- input设备结构体
input_dev
- evbit成员:输入事件类型,可选事件见下文
- keybit成员:按键值
- relbit成员:相对坐标
- absbit成员:绝对坐标
- …
设置input_dev结构体中成员的方法:
- __set_bit
- BIT_MASK宏
- input_set_capability:仅仅设置keybit成员
相关函数
- input_allocate_device:申请一个input_dev结构体
- input_free_device:释放申请到的input_dev结构体
- input_register_device:向内核注册input_dev结构体
- input_unregister_device:注销input_dev结构体
- input_event:上报指定事件及对应的值
- input_report_key:封装一层,专门上报按键事件
- input_report_rel
- input_report_abs
- input_report_ff_status
- input_report_switch
- input_mt_sync
- input_sync:同步事件
input_dev的相关事件
- EV_SYN:同步
- EV_KEY:按键
- EV_REL:相对坐标
- EV_ABS:绝对坐标
- EV_MSC:杂项(其他)
- EV_SW:开关
- EV_LED:LED
- EV_SND:声音(sound)
- EV_REP:重复事件
- EV_FF:压力
- EV_PWR:电源
- EV_FF_STATUS:压力状态
4.3调试
input设备注册成功后可以通过以下命令来查看是否注册到系统中:
1 | ls /dev/input/ -l |
通过以下命令查看是否有效:
1 | busybox hexdump /dev/input/event1 |
5.IIC框架驱动
5.1设备树
clock-frequency
属性:iic总线的速度(默认100000,即100k)- status:是否使能该总线
- okay
- disabled
- iic设备节点需要写到iic总线节点中,作为总线节点的子节点
- 总线节点需要利用
pinctrl-names = "default";
和pinctrl-0
指定iic引脚(已经设置好) - iic控制器节点
compatible
属性必须为allwinner,sun6i-a31-i2c或allwinner,sun4i-a10-i2c,这里在sunxi-h3-h5.dtsi中已经设置好,无需再次设置
设备树修改成功后可以通过以下命令来查看是否注册到系统中:
1 | #查看设备节点,该目录下放着所有的i2c设备 |
5.2驱动
基本数据结构
- IIC总线结构体
i2c_bus_type
- match函数:驱动和设备匹配是调用该函数来决定是否匹配
- IIC适配器(控制器)驱动结构体
i2c_adapter
- algo:总线访问算法成员
- master_xfer:IIC适配器的传输函数
- smbus_xfer:SMBUS总线传输函数
- algo:总线访问算法成员
- IIC设备结构体
i2c_client
- addr:芯片地址,存在低7位
- name:名字
- adapter:对应的IIC适配器
- dev:设备结构体
- irq:中断
- IIC设备驱动结构体
i2c_driver
- probe函数:IIC设备和驱动匹配后调用,初始化用
- remove函数:同理,卸载用
- id_table:传统方式匹配ID列表
- driver:
- owner:所有者,一般为宏THIS_MODULE
- name:名字
- of_match_table:设备树匹配列表
- IIC发送数据结构体
i2c_msg
- addr:从机地址
- flags:标志
- len:消息长度
- buf:消息数据
相关函数
- IIC适配器(控制器)
- i2c_add_adapter:向系统注册i2c_adapter成员,使用动态总线号
- i2c_add_numbered_adapter:向系统注册i2c_adapter成员,使用静态总线号
- i2c_del_adapter:删除IIC适配器
- SPI设备
- i2c_register_driver / i2c_add_driver:注册i2c_driver
- i2c_del_driver:卸载i2c_driver
- 传输数据
- i2c_transfer:传输i2c_msg类型数据
- i2c_master_send:IIC发送缓存中的数据,最后会调用i2c_transfer
- i2c_master_recv:IIC接收数据到缓存中,最后会调用i2c_transfer
5.3调试
i2c-tools工具
得益于ubuntucore,直接运行sudo apt install i2c-tools
安装i2c-tools工具
- 查询i2c总线
sudo i2cdetect -l
- 检测i2c地址
sudo i2cdetect -r -y 0
(最后的0为i2c-0总线)
6.SPI框架驱动
6.1设备树
spi-max-frequency
属性:spi总线的最大速度(默认100000,即100k)- status:是否使能该总线
- okay
- disabled
- spi设备节点需要写到spi总线节点中,作为总线节点的子节点
- 通过
cs-gpios
属性指定CS引脚 - spi设备节点中
reg
和节点@
后的数字均为该设备所使用的spi通道(一般0即可) - 总线节点需要利用
pinctrl-names = "default";
和pinctrl-0
指定spi引脚(已经设置好) - spi控制器节点
compatible
属性必须为allwinner,sun8i-h3-spi或allwinner,sun6i-a31-spi,这里在sunxi-h3-h5.dtsi中已经设置好,无需再次设置
设备树修改成功后可以通过以下命令来查看是否注册到系统中:
1 | #查看设备节点,该目录下放着所有的i2c设备 |
6.2驱动
基本数据结构
- SPI总线结构体
spi_bus_type
- match函数:驱动和设备匹配是调用该函数来决定是否匹配
- SPI主机驱动结构体
spi_master
- transfer函数:控制器数据传输函数
- transfer_one_message函数:控制器数据传输函数,一次发一个spi_message包
- SPI设备驱动结构体
spi_driver
- probe函数:SPI设备和驱动匹配后调用,初始化用
- remove函数:同理,卸载用
- id_table:传统方式匹配ID列表
- driver:
- owner:所有者,一般为宏THIS_MODULE
- name:名字
- of_match_table:设备树匹配列表
- SPI传输信息结构体
spi_transfer
- tx_buf:发送数据
- rx_buf:接收数据
- len:数据长度
- SPI消息结构体
spi_message
,由spi_transfer
组成- complete函数:异步传输完成时调用
相关函数
- SPI主机
- spi_alloc_master:申请spi_master结构体
- spi_master_put:释放spi_master结构体
- spi_register_master:注册
- spi_unregister_master:注销
- SPI设备
- spi_register_driver:注册
- spi_unregister_driver:卸载
- 数据传输
- spi_message_init:初始化spi_message结构体
- spi_message_add_tail:将spi_transfer添加到spi_message队列中
- spi_sync:同步传输(会阻塞并等待SPI数据传输完成)
- spi_async:异步传输(传输完成后会调用spi_message.complete函数)