【嵌入式】总结&指南——Linux下的裸机驱动开发

板型:正点原子 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,都无法正常使用,即便添加到了键盘里,也仍不会触发。

        除此之外呢,kali有不少优点,它这个命令行很好用,除了一般的使用Tab键将命令补全外,还可以使用方向键↑、↓、→,并且命令补全时它会显示出来,这点真的很贴心。同时呢,紫色背景尽显高贵,没事了还能学学网安,一举多得

        如果你要使用kali,那么需要注意以下几点

:下载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 

 ③开发工具的选择

        默认选用VS Code就行了。虽然之前用CLion,那代码补全让人爱不释手,但我尝试了在Linux下安装,但无论是Ubuntu还是kali都无法ᗜ ‸ ᗜ     。有空的话,试一下桥接吧

        至于VS Code,下载时要上官网,最好不要在Linux发行版里的应用市场下载(会被阉割)。此外,插件方面的选择,可以使用通义灵码,输入拼音“Tong"就能搜索到了,这个算是弥补了VS Code代码补全不够强劲的缺点。

        此时,你虽然暂时不必使用vim编辑器,但是下方的命令行还是摆脱不掉的

2,Linux的体验

        使用就是最好的磨练。初见命令行时,可能会有一种抵触心理。虽然Windows下也有PowerShell、cmd,但用的场景并不多,可能在游戏崩溃、电脑蓝屏时会用一下。使用Linux命令行时,你要把它当做一个工具,不必弄清它是如何做到的,更应该关心的是如何去使用它,用造好的轮子去简化你的开发流程。

        像一些常见命令如cd、find、pwd、rm、mkdir、echo、touch等,自己手动尝试创建几次目录、文件也就自动学会了,并不需要死记硬背。

        用一段时间,自然就熟练了。在这一阶段的学习中,其实真正用到Linux的地方并不多,主要工作还是在VS code里面完成,最多是使用VS Code下面的终端来make(自动化编译链接的命令)一下

        不过在编译链接时要小心,因为视频教程里用的是-O2级优化,那么这意味着有时会发生意想不到的问题。比如定义一个局部变量,结果被编程器优化了,关键是优化之后还卡死了。如果发生卡死的情况,要注意是不是局部变量被优化导致的。

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-gnueabi-gcc main.c

【依赖】指的就是要完成这条规则,需要准备哪些东西,可以省略。比如说我要这个规则要编译main.c文件,于是就可以把规则写成下面这样。make会先执行第一条内容,它会判断这个main.o在不在,如果不在就会到依赖里面去找

BianYi: main.o
    arm-linux-gnueabi-ld main.o

         然后就会根据这个依赖去匹配相应的规则,比如这次需要的就是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还不错,挺方便的,更接近c语言,不需要怎么注意缩进。而这个makefile感觉上更像python,需要注意缩进。不过都是自动化编译工具,哪个用的顺手就用哪个,但至少得保证能熟练运用一个。

        在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 A7三级指令流水线、工作模式、ARM汇编等等。此时,不妨降低一下心理预期,会添加使用中断、处理中断标志就行了,重在应用嘛,所见即所得的正反馈。

        像有些章节如RTC(开头很有趣哦,一定要看看),如果你觉得没什么用,那么可以先跳过,等需要时再学习印象会更深刻。虽然凡事讲究未雨绸缪,但相信看到本篇的大多数应该是自学,没有人会强迫你每个知识点都得学全。那么完全可以在循序渐进的基础上,挑自己喜欢的去学,以兴趣为导向会事半功倍。

        即便中途遇到困难,也不能停滞不前。前进,意味着还能找到补救的方案,但停止,就很难再次前行了。取舍很重要

        (好像有点糊,但最大只能上传5MB。不过脑图写的本来就凌乱,这里只是起到一个背景作用)

  • 8
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值