板型:正点原子 I.MX6UL MINI
屏幕:7寸 1024*600
立意:既是这一段学习的总结,也可作为入门指南的参考,不过并不能作为教程来看,实际学习还是要找相应的视频或文章教程。
一、历程
应该和使用这块板子的大部分人一样,看的是正点原子的教程,刚学完里面的裸机驱动,趁着还有记忆就总结一下吧。此前学的是stm32,用的是stm32f407,嗯,很经典的一块板子。但是在学习过程中,不可避免地遇到了瓶颈。
去学ARM架构吧,可它既枯燥又晦涩,可视的实用价值太低。去学习一些工具的使用吧,可仿真工具、调试工具、IDE只能懂一些简单的用法,教程零散且需要不少软件方面的知识,不容易学习。去学习软件编程吧,接触过RT-thread,但能上手的就FreeRTOS和LVGL,只晓得怎么调用,源码很难看懂什么。去开发项目吧,但翻来覆去就是一些外设驱动,在协议与寄存器之间打转(仪表方向,很少接触网络方面的),除了一些基本的CV操作外,偶尔也就翻看一下数据手册,很难去真正提升什么。
于是就被这样困了许久,向底层太晦涩难懂,向上层又缺乏太多的软件知识。总之,学得既不广,又不精,还缺乏一条可视的提升路径。不过好在学过RTOS,知道裸机和操作系统的区别,那么自然不可避免地被引进Linux里(不过嘛,现在学的还很浅)。于是就准备买一块板子,在京东上搜索了嵌入式Linux开发板,也就入手了现在这块,感到既庆幸又有些遗憾。
庆幸的是用的是正点原子的,一是够大众够开源,这意味着你遇到的问题,基本上会有人遇到过,不会在踩坑之后爬不上来。二是,教程很多,作为一名CV员,对此有着天然的亲切感。遗憾的是买的是MINI板,7寸屏幕太大了,放不下。如果下次有机会,一定要事先查看一下板子和屏幕的尺寸,买个大板子和大屏幕。
现在谈谈NXP的这块imx6ul板,刚一接触时让我直接惊为天人。原本挣扎在256KB的小内存,尤其是还经历了半个多月TI板32KB内存噩梦般的洗礼。对内存管控有着病态般的执着,能用uint8_t就绝不会用uint16_t,1B的浪费都是一种罪过。结果imx6ul直接就是512,MB!并且还是DDR3。想到电脑上插着的是DDR4,这让我有一种相当不真实的感觉。尤其是在学习过程中,很难想象自己可以用一块16GB的SD卡,通过插拔就可以让代码运行在板子上。学习时钟时,更是难以置信,在单片机中可以接触到GHz这一单位,PLL里可以直接达到1.5GHz,同时这块板子可以超频到八九百MHz。诸如此类的震撼还有不少
二、踩坑
前期学习自然是手把手地跟着学,但同样的把戏不能玩第二遍,到中后期就需要“浏览”了,重点是看操作的流程,而非操作的细节。可以先开倍速过一遍听个响,拿本子或者平板什么的记录一些关键步骤或知识点,体会这个开发流程,然后再自己尝试独立做一遍,从翻看数据手册“考究”寄存器的作用,到配置寄存器、规范化编程。遇到过不去的坎,就去拿正点原子给的例程对照一遍,如此才能有质的提升。
裸机驱动这一阶段的学习,无外乎开发环境搭建、Linux的体验、脚本的使用、外设驱动的配置这几个内容。对于这一阶段的学习,你会发现与之前有很大的不同,那就是开始不会给你官方库,一切都需要手动去敲、去造轮子,还需要反复查阅数据手册,弄懂里面讲的什么。比如,对着密密麻麻的时钟树,来理清所需时钟如何产生、选择。以前虽然也会翻数据手册,但更多的是翻看、浏览,看个时序和引脚。
1,开发环境的搭建
①环境选择
这一步需要你有一个Linux环境,方案有装双系统、虚拟机、WSL2,但可行性较高的只有虚拟机这一种。 因为装双系统不利于文件传输,且办公、娱乐、开发离得太远,除非有两台电脑。WSL2虽然开销小文件传输更为方便,但并不适合开发,基本上是要啥缺啥。即便照着网上教程做,也很难搞定,比如图形界面我至今还是白屏,下载了相应工具后设备依旧不能挂载到设备树上等等,总之各种麻烦,很容易入门就劝退。
②Linux发行版选择
理论上使用Ubuntu更适合开发嵌入式,因为教程足够多,不容易暴毙。但是我本人的电脑可能有什么心事,即便是重装一遍系统后,安装Ubuntu24还是会失败,于是就换成了Ubuntu22,但是使用过程中有着诸多不便,比如无法输入中文、主机与虚拟机之间不能传文件(即便下载了VMwareTools)等等。
于是就换成了kali purple,主机与虚拟机间传文件都不需要别的操作,直接右键复制粘贴。至于无法输入中文,这个可以通过下载对应的输入法,比如搜狗输入法(网上有不少教程)、谷歌输入法。但我个人经验是用谷歌输入法(两行代码的事),搜狗输入法下过很多次,无论是在Ubuntu还是在kali,都无法正常使用,即便添加到了键盘里,也仍不会触发。
如果你也使用双拼(小鹤),那么可以参考这篇博客ubuntu安装小鹤双拼输入法,只不过要注意里面的路径,~/home/user需要改成/home/【自己的用户名】
除此之外呢,kali有不少优点,它这个命令行很好用,除了一般的使用Tab键将命令补全外,还可以使用方向键↑、↓、→,并且命令补全时它会显示出来,这点真的很贴心。同时呢,紫色背景尽显高贵,如果有时间的话顺便还能学学网安
如果你要使用kali,那么需要注意以下几点,不过建议还是安装Ubuntu
:下载kali镜像时,不能使用多线程下载,否则会下载失败
:阿里源是可以正常使用的,有些源并不可用
:kali命令行有两个,一个是sh,一个是zsh,如果你要添加环境变量,那么两个都需要添加,否则有些情况下不会生效。这个网上有相关解决办法,很简单
deb http://mirrors.aliyun.com/kali kali-rolling main non-free contrib
deb-src http://mirrors.aliyun.com/kali kali-rolling main non-free contrib
补充:
2023.8.23
桥接时出现问题,需要重装VMware,结果刚卸载VMware笔记本键盘便不能用了(可能是当初勾选了那个增强型键盘驱动)。之后就试了很多教程也没解决。只好按照老方法重装系统,结果这次捅了个大篓子,手生了没控制好把所有数据全部删除了
没办法,只能当做重新整理了一下电脑吧,只是可怜了我的学习资料(×﹏×)。
重装后,安装这个VMware,桥接确实没问题了(心痛(x_x))。这次重生我选择安装Ubuntu2024版,倒是没有发生那么多奇怪的问题了,如果黑屏取消勾选图形加速就行了。不过发现了一点,无论是kali还是Ubuntu,每重启两次就会死机一次,这就 (°ー°〃)
③开发工具的选择
默认选用VS Code就行了。虽然之前用CLion,那代码补全让人爱不释手,但我尝试了在Linux下安装,但无论是Ubuntu还是kali都无法ᗜ ‸ ᗜ 。有空的话,试一下桥接吧
至于VS Code,下载时要上官网,最好不要在Linux发行版里的应用市场下载(据说是阉割版)。此外,插件方面的选择,可以使用通义灵码,输入拼音“Tong"就能搜索到了,这个算是弥补了VS Code代码补全不够强劲的缺点。
此时,虽然暂时不必使用vim编辑器,但是下方的命令行是不可或缺的
补充:
2023.8.24
嘘~,总算解决了提示的key is invalid
在进行一系列必要操作后,然后找到下面文件
然后,对这些文件的末尾各添加三行内容,至于添加的内容想必很熟悉(不熟悉的话,可以参考一些教程,比如这个),路径需改成自己的。最后重启,再次尝试
--add-opens=java.base/jdk.internal.org.objectweb.asm=ALL-UNNAMED --add-opens=java.base/jdk.internal.org.objectweb.asm.tree=ALL-UNNAMED -javaagent:/home/fairy/Embedded/software/clion/Crack/ja-netfilter.jar=jetbrains
如此Linux的开发与Windows差不多了,同时还兼具强大的命令行,算是重生有重生的好处吧。只不过始料未及的是例程代码竟然会报错
2,Linux的体验
①命令行
使用就是最好的磨练。初见命令行时,可能会有一种抵触心理。虽然Windows下也有PowerShell、cmd,但用的场景并不多,可能在游戏崩溃、电脑蓝屏时会用一下。使用Linux命令行时,你要把它当做一个工具,不必弄清它是如何做到的,更应该关心的是如何去使用它,用造好的轮子去简化你的开发流程。
像一些常见命令如cd、find、pwd、rm、mkdir、echo、touch等,自己手动尝试创建几次目录、文件也就自动学会了,并不需要死记硬背。
用一段时间,自然就熟练了。在这一阶段的学习中,其实真正用到Linux的地方并不多,主要工作还是在VS code里面完成,最多是使用VS Code下面的终端来make(自动化编译链接的命令)一下
不过在编译链接时要小心,因为视频教程里用的是-O2级优化,那么这意味着有时会发生意想不到的问题。比如定义一个局部变量,结果被编程器优化了,然后卡死了。加之这一阶段并没有掌握在线调试的方法,连判断是否卡死都得花不少功夫,完全是盲盒实验。如果发生卡死的情况,要关注是不是变量被优化导致的,尤其是结构体,倘若你没有进行手动内存对齐的话,极易在-O2级别下卡死,比如例程里的LCD结构体,得改成下面这样,把大字节放在上面。如果你不想改结构体成员顺序,那么需要在结构体变量定义和声明前面加上volatile
struct tftlcd_typedef {
uint32_t framebuffer; /* LCD显存首地址 */
uint32_t forecolor; /* 前景色 */
uint32_t backcolor; /* 背景色 */
uint32_t id; /* 屏幕ID */
uint16_t height; /* LCD屏幕高度 */
uint16_t width; /* LCD屏幕宽度 */
uint16_t vspw;
uint16_t vbpd;
uint16_t vfpd;
uint16_t hspw;
uint16_t hbpd;
uint16_t hfpd;
uint8_t pixsize; /* LCD每个像素所占字节大小 */
};
还比如触摸屏章节的内容,如果你不给temp数组加volatile的话,它会卡在【打印固件版本】的那个串口显示。
②文件传输
进行主机与虚拟机之间的文件传输一般会用Filezilla,使用FTP协议传输时,需要注意以下几步骤:
1:先确保虚拟机使用的是桥接网络
这里面的坑其实不少,多见于VMware卸载不彻底,解决办法就是彻底卸载,网上有教程。
2,获取主机与虚拟机各自的ip地址
主机:使用ipconfig命令在cmd或者powershell里查看IPV4地址
虚拟机(Ubuntu):使用ifconfig。第一次使用时可能会提示没有这种命令,然后让你下载net tools什么的,照着下载即可。
3,主机与虚拟机确保能互相ping
主机与虚拟机都可以使用ping命令,在主机的cmd或者powershell,使用ping 【虚拟机ip地址】,看看是否正常。同理,在虚拟机里使用ping 【主机ip地址】,观察是否正常。如果主机可以ping虚拟机,虚拟机无法ping主机,那么多半是防火墙的问题,把防火墙关闭即可。
4,进行FTP明文传输
上述前提条件准备妥当时,可以直接在上方的框里填写对应信息,然后点击快速连接。(这当中可能会出现问题,往下翻)
或者新建一个站点
如果在信息没有输错的情况下,依然出现无法连接的情况,那么可能是Ubuntu里的FTP没有配置好,此时可以选择配置Ubuntu的FTP或者使用SSH协议进行传输。参考博客如下:filezilla 连接不上虚拟机ubuntu(终极解决方案)_filezilla无法连接到ubuntu-CSDN博客
3,脚本的使用
①shell脚本
说实话,这一阶段要掌握的shell命令其实并不多,这里所说的shell命令其实就是你在命令行里输入的命令,二者并没有什么区别。只不过作为脚本时,你可以在一个后缀名为.sh的文件里输入很多行命令,与写一个C文件没有太大区别。在这一阶段,并没怎么用到shell脚本
②Makefile脚本
这里重点要掌握的是Makefile脚本。前期学习中,会讲怎么把C文件一步步变成单片机里面可以运行的代码。简单来说,就是通过交叉编译工具链一步步实现,先是gcc把.c文件转为.o文件,然后是ld把.o文件们链接成.elf文件(其实就是Linux下的可执行文件,与Windows下的.exe一样),然后由obcopy把这个.elf文件提取出.bin文件,这个就是一般意义上的二进制文件,理论上是可以直接让CPU执行的。最后由官方提供的烧写工具,在这个bin前面加上前缀信息,然后烧录到存储介质中,以便芯片可以从存储介质中把代码加载到内存中。如果需要的话,可以使用objdump把elf文件反汇编为.dis文件,可以在这个.dis文件里查看反汇编的代码
接下来就简单谈一下Makefile的语法。Makefile就是由一系列规则构成,这里的规则你完全可以理解为命令,就是Linux命令行的那种命令。在Makefile里面编写规则,其实就是相当于你在自定义属于自己的“Linux命令”。
比如说在编译完项目后,我还想要有一个可以自动清理编译文件的命令,假设编译的文件都放在Debug路径下,那么就可以在Makefile里这样写道
clean:
rm -rf ./Debug
当你在命令行里使用make clean时,make工具会自动找到名为clean的这条规则,然后执行它。你也完全可以换个名字,比如就叫它sc(删除),想要调用这个命令就make sc
sc:
rm -rf ./Debug
不过要注意的是,直接使用make会默认执行在Makefile里编写的第一条规则。所以第一条规则往往是编译文件用的,此时名字可以任意去取。
规则构成
规则,就是用来执行的。规则们都有一个统一的形式,可以表述为下面这种
【目标】:【依赖】
【执行内容1】
【执行内容2】
... ...
【目标】或者说规则名,其实除了是名字还可以是一个文件,比如main.o:
main.o: main.c
arm-linux-gnueabihf-gcc main.c -o main.o
【依赖】指的就是要完成这条规则,需要准备哪些东西,可以省略。比如说我要这个规则要链接出main.elf文件,于是就可以把规则写成下面这样。make会先执行第一条内容,它会判断这个main.o在不在,如果不在就会到依赖里面去找
LianJie: main.o
arm-linux-gnueabihf-ld main.o -o main.elf
然后就会根据这个依赖去匹配相应的规则,比如这次需要的就是main.o,那么它就会先去执行main.o那个规则
【执行内容x】也可以省略。这里面既可以是shell命令(因为make就是运行在shell环境里的),也可以是Makefile自身的命令。不过要注意的是,【执行内容x】与【规则名】所处的位置不同,它需要一个Tab键来产生缩进,不能使用空格代替。
函数变量
这个其实是为规则服务的,变量定义跟Python一样,不需要指定类型,当然这个变量实际上是字符串替换,Makefile是不能直接执行计算的。变量定义时,需要注意,赋值符需要与变量隔开,不然会认为是一体的。而赋值符除了:=,还有?=、+=等,各有各的作用
Debug_PATH := ./Debug
变量定义后,如果要使用,得用$(xxx)括起来
rm -rf $(Debug_PATH)
至于这里的函数,与c语言里的大大不同,这里的函数(也可以说是Makefile内置命令)有一个统一的格式
$(函数名 参数1,参数2,......)
单个参数里面可以使用空格来隔开,而逗号是隔开不同参数的
比如一个去除目录的函数$(notdir xxx),下面命令可以把CFiles里面带有路径的文件变为
main.c 和led.c
CFiles := ./Application/main.c ./Drivers/LED/led.c
CFiles_NO_PATH := $(notdir $(CFiles) )
自动化编译链接
刚用CLion时,一上手就是cmake,当时觉得cmake还不错,挺方便的,更像一门新语言,并且不需要怎么注意缩进。而这个makefile感觉上有点点像python,需要注意缩进。虽说cmake更加强大,但后面的学习中用到Makefile的地方也有不少,比如Uboot,所以至少得掌握Makefile。
在Makefile中,虽然不建议使用shell命令,能尽量用Makefile内置命令就使用。只不过,有些shell命令实在太方便,比如find命令,可以递归查找文件。虽然理论上使用Makefile也能完成,比如用递归,但实际上并不好用,一用就容易卡死。而使用find命令可以让你的效率成倍提升,比如头文件路径,往往需要自己手动添加,每创建一个文件夹,就得添加一个路径。使用find后,可以这样做
①先搜索所有的.h文件,$(shell xxxxx)意思就是用shell去执行xxxxx,防止与Makefile内置命令冲突。
$(INCDIRS)是个变量,存放的就是四个路径,因为我习惯上把项目分成四个部分,比如驱动文件在Driver里创建相应目录并放置
Application/
Driver/
FounctionModule/
Library/
HEADERS := $(shell find $(INCDIRS) -name "*.h")
②然后再用$(dir xxx)对这个头文件取路径,之后再用$(sort xxx)进行排序(同时会去除重复路径)
HEADER_DIRS :=$(sort $(dir $(HEADERS)) $(INCDIRS))
两行代码就可以找到所有头文件路径了
③至于这一步,就是把每个头文件前面加个前缀“-I”,因为这个头文件路径是给gcc用的,让gcc找相应头文件
INCLUDE := $(patsubst %, -I %, $(HEADER_DIRS))
至于编译链接,就是在能自动找到文件的情况下,添加各种编译、链接标志完成相应文件的输出。这里需要注意的是VPATH变量,这个变量是会被make识别出来的,make会根据这个VPATH里包含的路径来搜索%.c或者%.h什么的(%相当于通配符*),如果不设置这个VPATH,那么后面用%.o : %.c时就会识别不出来
自定义规则
使用make其实就是为了简化操作,有些麻烦的操作流程能省就省,比如烧录这一操作,视频教程里总是会在命令行里输入./imxdownload xxx.bin /dev/sdx,这样做就显得很麻烦,因为自己开发过程中,挂载到设备树上的sd设备往往是同一个,序号不会变。于是可以自己定义一个规则,专门用于烧录,每次需要烧录时,直接make download。如果觉得还是太长,可以把规则名改短一点
download:
./imxdownload ./Debug/$(TARGET).bin /dev/sdb
Makefile示例
下面这个是我编写的Makefile,主要功能是自动地把
Application Driver FounctionModule Library 路径下所有的.c、.s文件编译链接成对应的.o文件,然后存放到Debug路径下,并且路径名不变。此外会单独找到start.s这个启动文件,去除掉路径,直接存放为./Debug/start.o,这是为了链接脚本文件可以正确找到启动文件,不必关心start.s处于哪个路径。
编译后产生的.elf、.dis、.bin文件会自动放在./Debug目录下
TARGET ?= test
#注意哈,我使用的是arm-linux-gnueabi后面没有hf,需要根据自己的具体情况去填
CROSS_COMPILE := arm-linux-gnueabi-
CC := $(CROSS_COMPILE)gcc
LD := $(CROSS_COMPILE)ld
OBJCOPY := $(CROSS_COMPILE)objcopy
OBJDUMP := $(CROSS_COMPILE)objdump
# 烧录工具
DownloadTool := ./imxdownload
# 编译标志
# -Wall 用于开启警告 -nostdlib 不使用标准库,与pg冲突 -pg用于生成额外调试信息
CFLAGS = -Wall -c -O2 -fno-builtin -Wa,-mimplicit-it=thumb
LDFLAGS = -Timx6u.lds
RM = rm -rf
# Debug目录路径 链接库路径
Debug_PATH = ./Debug
LIBPATH := -lgcc -L /usr/local/arm/gcc-linaro-gnueabi/lib/gcc/arm-linux-gnueabi/7.5.0
#头文件路径
INCDIRS := Application/ \
Driver/\
FounctionModule/\
Library/\
#源文件路径,会自动递归搜索其下的所有目录
SRCDIRS := Application/ \
Driver/\
FounctionModule/\
Library/
#--------------------------------路径处理--------------------------------------
#使用find命令来自动递归匹配
CFILES := $(shell find $(SRCDIRS) -name "*.c")
SFILES := $(shell find $(SRCDIRS) -name "*.s")
#为了方便自动匹配头文件,获取头文件所在目录,并去除重复
HEADERS := $(shell find $(INCDIRS) -name "*.h")
HEADER_DIRS :=$(sort $(dir $(HEADERS)) $(INCDIRS))
# 使用 filter 找出 start.s,是以区分并供链接脚本引用
START_FILE := $(filter %start.s, $(SFILES))
NO_START_SFILES := $(filter-out %start.s, $(SFILES))
START_FILE_NDIR := $(notdir $(START_FILE))
#find命令不区分后缀名大小写,gcc寻找文件不区分后缀名大小写
SOBJS := $(patsubst %, $(Debug_PATH)/%, $(filter %.o ,$(NO_START_SFILES:.s=.o)))
START_SOBJS := $(patsubst %, $(Debug_PATH)/%, $(START_FILE_NDIR:.s=.o))
COBJS := $(patsubst %, $(Debug_PATH)/%, $(CFILES:.c=.o))
OBJS := $(SOBJS) $(COBJS)
# 设置VPATH以包含所有源文件目录,这样%.c 文件会自动匹配到相应的目录
VPATH := $(sort $(dir $(CFILES)))
# 头文件路径
INCLUDE := $(patsubst %, -I %, $(HEADER_DIRS))
#--------------------------------路径处理--------------------------------------
# 创建Debug目录
Debug_ALL_PATH := $(sort $(dir $(OBJS)))
$(foreach path, $(Debug_ALL_PATH), $(shell mkdir -p $(path) 2>/dev/null))
#----------------------------------编译规则------------------------------------------
all:$(Debug_PATH)/$(TARGET).elf $(Debug_PATH)/$(TARGET).bin $(Debug_PATH)/$(TARGET).dis
@echo -e '\t'
@wc -c $(Debug_PATH)/$(TARGET).bin
#elf文件
$(Debug_PATH)/$(TARGET).elf : $(OBJS) $(START_SOBJS)
@echo -e '\t'
$(LD) $(LDFLAGS) -o $@ $^ $(LIBPATH)
#bin文件
$(Debug_PATH)/$(TARGET).bin : $(Debug_PATH)/$(TARGET).elf
$(OBJCOPY) -O binary -S $< $@
#反汇编文件
$(Debug_PATH)/$(TARGET).dis : $(Debug_PATH)/$(TARGET).elf
$(OBJDUMP) -D -m arm $< > $@
#obj文件
$(SOBJS) : $(Debug_PATH)/%.o : %.s
$(CC) $(CFLAGS) $(INCLUDE) -o $@ $<
$(START_SOBJS):$(START_FILE)
$(CC) $(CFLAGS) $(INCLUDE) -o $@ $<
$(COBJS) : $(Debug_PATH)/%.o : %.c
$(CC) $(CFLAGS) $(INCLUDE) -o $@ $<
#----------------------------------编译规则------------------------------------------
# 清理规则,删除所有编译生成的文件
.PHONY: clean
clean:
@$(RM) $(Debug_PATH)/ *.imx
#烧录
.PHONY: download
download:
@echo '-------------------------------------------'
$(DownloadTool) $(Debug_PATH)/$(TARGET).bin /dev/sdb
#重新构建
.PHONY: rebuild
rebuild:
@make clean
@make
#获取下载权限,一次获取即可
.PHONY: getDownload
getDownload:
chmod 777 $(DownloadTool)
#检查SD卡
.PHONY: check
check:
@ls /dev/sd*
#生成调试信息
.PHONY: print
print:
@echo -e "CFILES = $(CFILES)\n"
# 性能分析,暂时不可以使用
.PHONY: profile
profile: $(Debug_PATH)/$(TARGET).elf
gprof $(Debug_PATH)/$(TARGET).elf > $(Debug_PATH)/profile.txt
4,外设驱动的配置
①引脚
首先要讨论的自然是引脚,这里的IO引脚配置与以前学的GPIO有很大的不同。这里的引脚先是有IOMUXC来分配(MUX意为多路选择器),并配置电气属性、复用为哪个功能,其中有一个功能才是GPIO,此时才可以配置GPIO的输入输出、高低电平、中断触发方式。而上下拉、速度、驱动强度等都是由IOMUXC控制的。
重要的事情再次强调一遍,这里的引脚是由IOMUXC来控制,GPIO只是IOMUX中复用功能的一种,与I2C、UART复用功能属于同一级
IOMUXC包含了三类寄存器:
②时钟
时钟的重要性不言而喻,对于时序电路来说这是必不可少的。任何一个外设都要配置相应的时钟来驱动它,只不过IMX6UL的时钟系统过于复杂,需要从外设需要的时钟到它的根时钟,再经过一系列分频、时钟选择逆推到锁相环PLL上。
这里一共有7路PLL,经过分频、多路选择就形成了各个外设需要的时钟。PLL是时钟的源头,而这一切都是由CCM控制的,类比于stm32的RCC
至于如何分频、多路选择,其实图上都标有相应的寄存器,到手册上找到这些寄存器,那里会有详细的说明。 而锁相环PLL的产生则依赖于CCM_ANALOG这个寄存器。
至于这些寄存器怎么配置,配置在什么范围,在相应的寄存器位置和CCM的开头部分都有介绍,这需要你仔细独自阅读,就能找到“蛛丝马迹”了。当一个外设的时钟知道怎么配置后,其他外设的时钟就简单多了,因为已经有一条成功的配置路径了
③外设驱动编写
每个人有自己的编写习惯,但尽量要让编写符合规范。规范真的很重要,即便不是团队开发,自己独立开发时如果东一套西一套,那么时间久了自己也会迷糊的。
当然在实际开发中,可以有一些自己的小想法。比如高精度延时这一章节,在定义延时us函数时,产生了一个问题,那就是如果计数器溢出了怎么办,示例代码是这样解决的
void delayus(unsigned int usdelay)
{
unsigned long oldcnt,newcnt;
unsigned long tcntvalue = 0; /* 走过的总时间 */
oldcnt = GPT1->CNT;
while(1)
{
newcnt = GPT1->CNT;
if(newcnt != oldcnt)
{
if(newcnt > oldcnt) /* GPT是向上计数器,并且没有溢出 */
tcntvalue += newcnt - oldcnt;
else /* 发生溢出 */
tcntvalue += 0XFFFFFFFF-oldcnt + newcnt;
oldcnt = newcnt;
if(tcntvalue >= usdelay)/* 延时时间到了 */
break; /* 跳出 */
}
}
}
你可以先确定终点计数值是多少,然后等待可能的溢出,因为如果溢出了,那么此时的时钟就一定会比终点计数值end要大,那么就会在这个地方一直等待。如果不溢出,那么这个while循环会直接跳过,对下面正常情况下的延时循环没有任何影响
static inline uint32_t GPT_GetValue(GPT_Type *GPTx)
{
return GPTx->CNT;
}
// us延时,最大延时0xFFFFFFFFus(输入0或者0xFFFFFFFF)
void delay_us(uint32_t us)
{
uint32_t end = GPT_GetValue(GPT1) + us;
// 等待可能的溢出
while (GPT_GetValue(GPT1) >= end)
;
// 切换为正常模式
while (GPT_GetValue(GPT1) < end)
;
}
不过,不得不说这个外设驱动难度差异太悬殊了,像I2C驱动和蜂鸣器驱动的开发,简直一个天一个地。本来独自看寄存器,觉得就那几个功能,以前再怎么说也编写过stm32的I2C,无论是软件I2C还是硬件I2C。但打开例程的I2C代码后才发现,脱离了库函数后,再来配置I2C控制器,是一种顶级折磨。
三、总结
这次学习,除了“由简入深,方能深入浅出”以外。还有就是,学习一个东西要懂取舍千万不能死磕,要先追求广度,再逐步深入。就比如在stm32学习阶段(专指F4、F1系列),启动文件是从官方例程下的,链接脚本也是官方提供的,如果那时死磕汇编语言、链接脚本语言,一心想弄懂它是干什么的,那么很容易浪费大把时间,从进展缓慢到渐渐失去信心,进而停滞不前。除了路子不对以外,还有一点就是缺乏广度。
在stm32不怎么讲的启动文件和链接脚本,在imx6ul里则是开篇就讲,很自然地就一步步衔接起来了,因为imx6ul会讲交叉编译工具链、从C语言到elf文件再到bin文件最后到镜像文件的过程。而在stm32阶段,由于使用的是IDE,这些东西很难接触到,与启动文件、链接脚本有着天然的隔阂。这并非坏事,一个阶段有着一个阶段的事要做。
如果你在学习中遇到特别大的难点,除非必须要攻克,否则建议是先跳过这一节,到后面知识面变广,就自然攻克了。比如imx6ul这一章节的中断那一讲,如果强行要求自己把启动文件里关于中断工作的过程从头写一遍,那么非常有可能达不到你的预期,反而会让你受挫沮丧。因为那涉及到了许多关于ARM架构的前置知识,比如ARM V7三级指令流水线、工作模式、ARM汇编等等。此时,不妨降低一下心理预期,会添加使用中断、处理中断标志就行了,重在应用嘛,所见即所得的正反馈。
像有些章节如RTC(开头很有趣哦,一定要看看),如果你觉得没什么用,那么可以先跳过,等需要时再学习印象会更深刻。虽然凡事讲究未雨绸缪,但相信看到本篇的大多数应该是自学,没有人会强迫你每个知识点都得学全。那么完全可以在循序渐进的基础上,挑自己喜欢的去学,以兴趣为导向会事半功倍。
即便中途遇到困难,也不能停滞不前。前进,意味着还能找到补救的方案,但停止,就很难再次前行了。取舍很重要
(好像有点糊,但最大只能上传5MB,这里只是起到一个背景作用)