第二章ARM编程的裸机开发
嵌入式ARM编程的裸机开发是驱动开发的基础,学习裸机开发将有助于加深对嵌入式系统底层运行和底层控制的理解,从而进一步提高后续驱动学习的效率。本章节将讲解裸机开发的思路,同时通过示例来说明如何进行裸机开发。
2.1 裸机开发思路讲解
嵌入式ARM编程的裸机开发常规的裸机开发的思路步骤如下:
(1)分析电路原理图
(2)分析和理解硬件的控制原理
(3)阅读数据手册,分析和理解寄存器的操作控制流程
(4)根据裸机开发设备驱动框架编写驱动程序实现硬件控制
2.1.1分析原理图
通过阅读嵌入式系统的硬件原理图,我们将可以获取到如下的信息:各个存储器、外围设备使用的硬件资源、各种接口和各款芯片的引脚的联接情况等。阅读电路原理图的思路主要是应该将CPU主控芯片作为中心点出发,逐步扩展阅读外围的存储器以及外围设备等,具体的步骤如下:
(1)阅读主控芯片的数据手册时可由获取到它的片选、中断和外设控制器资源等,可以根据不同种类资源进行标记,方便整理CPU与外设的控制关系。
(2)在步骤(1)中将获取到如下资源信息:符号(symbol)、网络标号(net)和相关的描述。
获取到符号(symbol)的信息,主要可以了解到芯片到外围引脚和信号引脚的信号信息。如下图2.1所示为原理图中符号展示。
图2.1 原理图中的符号
Figure 2.1 Symbol in schematic
在原理图中的网络(net),可以获取到芯片、接插件元件以及各个元器件之间的连接关系等,这样有助于我们对嵌入式系统整体关系,每个网络应该都有自己的合适名字,这些名字可以让开发工程师能够更好的理解原理图,如图2.1中的“ESP REST”这个标号可以获取到此引脚的连接关系以及表示芯片的复位引脚等信息。
如下图2.2所示为原理图中的描述部分,在这里我们可以了解到原理图相关信息,如完成日期、原理图大小以及文件的名字等。
图2.2 原理图中的描述部分
Figure 2.2 Description in schematic
2.1.2 阅读芯片数据手册
在刚接触嵌入式开发的时候,阅读芯片数据手册也是很有必要的,同时芯片数据手册也是我们学习的一个重要资料,但是常规的芯片数据手册往往是有达几百页,更有甚者达到上千页,若从手册最开始阅读到最后所花费的时间将非常久,这也是实际项目开发不允许的。因此正确的芯片数据手册阅读方法是很有必要被掌握的,主要思路是快速定位到有用的信息上面来,对于无关的信息自动省略。如下将以SP56818的数据手册为例来讲解阅读方法。
(1)芯片结构阅读
如下图2.3所示为SP56818这款芯片的结构示意图,这也是一般能够在数据手册的第一章中可以获取,可以让我们能够对主控芯片有一个整体的把控。
图2.3 芯片结构示意图
Figure 2.3 Schematic diagram of chip structure
(2) 目录的阅读
在阅读目录的过程中可以让我们了解到数据手册整体章节安排,一般情况下第一章是芯片的整体结构介绍,接下来的章节则是对芯片的各个模块进行详细的描述,在实际开发时可以直接跳转到我们需要的章节进行阅读。
图2.4 数据手册的目录
Figure 2.4 Directory of the Data Manual
(3)每个章节进行针对性的阅读
第二章为讲述芯片的内存寄存器资源以及其IO引脚的功能描述,对于开发时查看寄存器资源以及复用功能等,这一部分可以细看。
第三章至第四十六章主要讲述CPU的内部的外围设备以及总线控制器,当我们需要进行嵌入式驱动开发时,开发具体的接口驱动时,最为应该详细对相应的模块进行详细的查阅,在这些章节中将了解到芯片的控制时序、分析数据、控制、地址寄存器的访问方法以及具体设备操作流程。如图2.5所示为I2C总线控制的数据传输数据的时序图,利用它我们将可以知道I2C数据传输的时序以及我们也可以利用此来编写出用GPIO模拟的I2C接口。
图2.5 数据手册的I2C总线的数据传输时序图
Figure 2.5 Data transfer timing diagram of the I2C bus in the data manual
第四十七章则为芯片的物理特性以及电气特性等信息,同时也会提供芯片相关的尺寸和封装信息,从事硬件工程师需要利用这一章的信息来完成电路的设计,但驱动工程师一般只需泛读即可。
2.2 裸机开发示例讲解
本小节将通过一个示例来更加具体的讲解裸机开发的流程,让读者们有一个更加深刻的体会。本文所选用的主控芯片是三星系列的SP56818的芯片,所使用的系统版本是linux3.4.39,接下来将以输出引脚控制LED灯的亮灭为例进行讲解。
2.2.1 裸机开发项目需求
明确项目的需求将对整个项目开发的流程和思路至关重要,在嵌入式驱动开发中一般情况需要考虑项目所涉及到的GPIO引脚、寄存器资源以及元器件的工作原理等因素。
以点亮LED灯为示例,一般情况下LED灯有两种接线方法,一种是共阳极,一种是共阴极,如下图2.6和图2.7所示。顾名思义共阳极指的是将LED灯的正极接到相同的电势点上如图2.6所示,共阴极指的是将LED灯的负极接到相同的电势点上如图2.7所示。
如图2.6所示,对于共阳极的接线方法,当GPIO引脚处于低电平的情况下,LED灯点亮,反之GPIO引脚为高电平时,LED熄灭。
如图2.7所示,对于共阴极的接线方法,当GPIO引脚为高电平时LED灯才被点亮,反正GPIO引脚为电平时则LED灯熄灭。
图2.6 LED灯共阳极接线法
Figure 2.6 LED lamp common anode wiring
图2.7 LED灯共阴极接线方法
Figure 2.7 LED common cathode wiring method for lamp
本次项目示例是采用了共阳极的接线方法,需要控制开发板上的引脚GPIOC17为低电平时,LED D8亮,反之控制GPIOC17为高电平时LED D8灭。如下图2.8所示为LED D8相关的电路原理图。
图2.8 LED D8电路原理图
Figure 2.8 LED D7 circuit schematic diagram
2.2.2 根据数据手册进行硬件配置
在原理图中获取到需要使用GPIOC17这个GPIO口,我们需要通过数据手册查找GPIO寄存器的使用方法,因此首先定位到与GPIO相关的第十六章,如下图2.8所示。
图2.8 第十六章GPIO章节
Figure 2.8 Chapter XVI GPIO chapter
接下来以配置引脚为GPIOC17输出模式为例,讲解如何根据GPIO章节的内容对寄存器进行配置,最终将引脚配置为GPIO输出模式。
2.2.2.1引脚复用
在讲解配置引脚GPIO功能之前,需要先让读者了解一个概念“引脚复用”。如果一个芯片设计时具备足够的引脚资源和充足的空间时,那么设计出来芯片的引脚功能是相互独立没有重合的。但是正常情况下很多芯片设计厂商为了充分高效利用引脚资源,会出现引脚复用的情况,即物理上同一组引脚能够通过设置成不同的功能,根据不同的功能需求进行设置来完成不同的任务。
如下图2.9所示为SP56818芯片的部分引脚复用图,以图中的”SA17”为例,这个引脚可以复用四种功能,由功能0到功能3分别为:SA17、GPIOC17、TSIDP0和VID20。因此如果需要配置该引脚为GPIO模式时,将相应的功能寄存器的值设置为b’01即可。
图2.9 引脚功能复用
Figure 2.9 Pin function multiplexing
2.2.2.2引脚的GPIO输出模式的配置和使用方法
由于从功能需求分析可知,此时需要将GPIOC17设置为输出模式,如下图2.10所示为GPIO输出功能设置的方法。具体步骤方法如下:
(1)配置引脚复用GPIOC17功能
通过查阅GPIOC17的复用功能情况可知,GPIO功能为该引脚的功能1,因此设置GPIOC复用功能寄存器的值为b’01(这个为二进制格式值),即选择复用功能1。
(2)使能当前引脚为输出模式
GPIOC输出使能寄存器对应位设置为1。
(3)设置输出高/低电平
GPIOC输出寄存器对应的位的值为1/0。
图2.10 GPIO输出功能设置及使用
Figure 2.10 GPIO Output function settings and usage
图 2.11 GPIOC17复用功能情况
Figure 2.11 GPIOC17 multiplexing
2.2.2.3查找对应的各个功能寄存器地址
如下图2.12所示为GPIOx的功能寄存器的地址,在后续的代码编程时需要使用到如下的地址进行映射。
图2.12 GPIOx常规的功能寄存器地址
Figure 2.12 GPIOx General function register address
2.2.2.4查找与GPIOC17相关的更加详细的功能寄存器位址
(1)复用功能寄存器1
如图2.13所示为复用功能寄存器0的相关位址情况,可以看出GPIOx17是使用这个寄存器的的[3:2]这两位来选择对应的功能的,本示例将其设置为01。
由于本实例只用到复用功能寄存器1,对于其他的复用功能寄存器0用法跟它一致,此处不展开细讲。
图2.13 复用功能寄存器1
Figure 2.13 Multiplexing function register 1
(2)输出使能寄存器
如下图2.14所示为输出使能寄存器,将对应的位设置为1则是使能。
图2.14 输出使能寄存器
Figure 2.14 Output Enable Register
(3)输出电平寄存器
如下图2.15所示为输出电平寄存器地址及位址情况,可以设置对应位来控制不同位的输出情况,此处可以设置GPIOC的第17位的值来控制GPIOC17的状态输出。
图2.15 输出寄存器
Figure 2.15 Output registers
2.3 裸机开发设备驱动程序编写
2.3.1裸机开发的设备驱动框架
在裸机开发中,设备驱动的函数接口通常情况下是直接交给应用开发工程师直接调用,开发的软件没有跨越任何层次就直接访问设备驱动的接口。设备驱动接口函数与硬件功能直接吻合,没有任何附加功能。如图2.16所示为裸机开发下硬件、设备驱动与应用软件的关系。
图2.16 裸机开发设备驱动框架
Fig .2.16 Driving frame of bare machine development equipment
2.3.2 裸机开发设备驱动代码实例
在裸机开发过程中,根据上述设备驱动框架,通常每一类设备驱动都会定义为一个单独的模块,包含头文件(.h文件)和源文件(.c文件),前者定义该类设备驱动的数据结构和外部函数的声明,后者则是驱动函数接口的具体实现。
下面是本示例的代码部分,主要分为三个文件,其中设备驱动文件为:led.c和led.h,应用程序为test.c,遵循上面设备驱动框架的思想,将应用程序与设备驱动分层,应用程序调用设备驱动的功能函数接口来实现LED灯D8闪烁的效果。如下分别为led.c、led.h和test.c的代码部分。
在代码部分中出现的关键字“volatile”作用是告知编译器它后面所定义的变量的值随时都有可能发生改变,所以编译后的程序运行时每次存储或读取这个变量的值时,都需要直接从变量地址中读取数据,保证了程序运行时特别是嵌入式底层变量数据的实时性和准确性。
led.c代码:
//(1)定义寄存器 #define GPIOCOUT (*(volatile unsigned int *)0xC001C000) #define GPIOCOUTENB (*(volatile unsigned int *)0xC001C004) #define GPIOCOUTALTFN0 (*(volatile unsigned int *)0xC001C020) #define GPIOCOUTALTFN1 (*(volatile unsigned int *)0xC001C024) void Led_Init(void) { //配置GPIOC17为输出模式 //思考,清零为什么用到位与,取反的操作 GPIOCOUTALTFN0&=~(3<<2); //GPIOE13的多功能配置[3:2]清零 GPIOCOUTALTFN0&|=(1<<2); //GPIOE13的多功能配置[3:2]设为01 //允许GPIOC17输出电平 GPIOCOUTENB|=1<<17; return ; } void Led_On(void) { //点亮 GPIOCOUT&=~(1<<17); return; } void Led_Off(void) { //熄灭 GPIOCOUT|=1<<17; return ; }
led.h代码:
#ifndef _LED_DRV_ #define _LED_DRV_ void Led_Init(void);//LED驱动初始化 void Led_On(void);//点亮LED灯 void Led_Off(void);//熄灭LED灯 #endif
test.c代码:
#include “led.h” static void delay(void); //(1)c程序的入口,同时不使用标准的c库,因此入口函数名字为_start void _start(void) { Led_Init(); while(1) { //点亮 Led_On(); //延时一会 delay(); //熄灭 Led_Off(); //延时一会 delay(); } } //(2)延时函数 void delay(void) { //思考为什么要加volatile volatile unsigned int i=0x2000000; while(i--); }
2.3.3交叉编译并烧写运行
按照以下步骤来对C文件进行编译,得到bin文件,再在操作系统中运行。
如下图2.15为编译烧写的流程图:
图2.17 编译烧写的流程图
Figure 2.17 Flowchart for compilation and burning
(1)将led.c和 test.c编译为目标文件led.o,同时不使用标准c库
arm-linux-gcc -o led.o -c led.c test.c -nostdlib
(2)将led.o链接到内存地址0x40000000,输出新的执行程序led.elf
arm-linux-ld -Ttext 0x40000000 -o led.elf led.o
(3)将上述的执行程序再转换会bin文件才能够在开发板上运行
arm-linux-objcopy -O binary led.elf led.bin
(4)下载led.bin文件到板子上运行
通常情况下下载烧写程序到嵌入式开发板使用的工具是SecureCRT,使用这款工具有如下三种下载可执行文件的方法:
a)使用mount命令进行挂载
这种方法一般用于直接从linux系统中获取可执行文件的,而且需要开发板和linux系统通过局域网进行通信。具体步骤如下:
①开发板必须要和虚拟机(linux系统)、电脑在同一个网段内(相互可以ping通)
②将要下载到开发板的程序放到指定目录(如/home/gec/share),这个目录需要在/etc/exports添加如下内容:
/home/gec/share *(rw,sync,no_root_squash)
③然后在开发板上执行以下命令,要下载的程序就在开发板的/mnt目录里,执行如下命令,其中“192.168.1.100”需要根据实际虚拟机(Linux系统)的ip地址进行修改。
mount -t nfs -o nolock 192.168.1.100:/home/gec/share /mnt/nfs
b)利用SecureCRT进行串口传输
在SecureCRT助手上对开发板的系统使用命令:rx ,其中r是service,x是X-model模式。具体步骤如下:
①输入如下命令:
rx 文件名,再按Enter键
②将需要传到板子上的文件拖到SecureCRT里面,选择发送X-model选项
③如果传输的是可执行文件, 默认是没有执行权限的,需要chmod 777 文件名
c)使用tftp 传输
使用tftp传输通常使用的工具有tftpd32.exe,这是一款可以搭建tftp服务器的软件,利用它能够完成tftp传输文件。具体步骤如下:
①运行tftpd32.exe,开启tftp服务器,设置服务器的IP地址,通过”Browse”设置需要传输文件的所在位置,然后通过”Show Dir”选择需要传输的文件。
图2.18 tftp服务器搭建
Figure 2.18 tftp Server building
②在开发板Linux系统中执行命令下载执行文件:
tftp 0x40000000 led.bin
(5)执行程序 go 0x40000000
测试结果:LED灯D8开始闪烁
2.4 带操作系统设备驱动
在上一小节中讲述了裸机开发的设备驱动,可以知道不带操作系统的驱动是可以直接运行在硬件之上的。但是由于实际开发项目中往往需要满足处理多个并发任务时,此时就需要操作系统的线程和进程来实现了,因此需要将设备驱动融入到内核中。
为了实现上述的融合,必须对所有设备驱动设计出面向操作系统内核的接口,驱动接口由操作系统规定,对一类设备而言结构一致,有别于具体的设备。
如下图为应用程序、操作系统和设备驱动关系,当存在操作系统的时候,设备驱动成为了连接硬件和内核的桥梁。操作系统的存在必然要求设备驱动附加更多的代码和功能,把单一的“驱动硬件设备运行”变为操作系统内与硬件交互的模块,它对外呈现为操作系统的API。
虽然带操作系统的设备驱动实现起来会比裸机开发的设备驱动要复杂,但是带操作系统的应用程序可以通过调用write()和read()等系统函数即可对各种各类的设备驱动进行访问,这给应用开发带来非常巨大的便利。关于具体的各类的带系统设备驱动将在往后的章节详细讲解,本小节意在给出带操作系统设备驱动框架,为学习后续各类的设备驱动做铺垫。
图2.19 应用程序、操作系统和设备驱动关系
Figure 2.19 Application, operating system and device drivers
2.5 本章总结
本章主要讲解的裸机开发的思路进行了讲解,主要从如何阅读高效阅读原理图分析数据手册,进一步来编程完成相应实现的功能,最后结合一个示例来具体的说明如何根据裸机开发的设备驱动框架编写驱动操作硬件完成特定的任务,让学习本章节的读者能够设备驱动有更深的认识。最后对带操作系统的设备驱动框架进行概述,为后续Linux驱动开发的学习做好准备。