打印
[新手园地]

【第四批】从51跳cortex-m0学习2——程序详解

[复制链接]
8271|33
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
watch186|  楼主 | 2011-11-23 16:24 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式




序言:
  首先声明,本文是继《从51cortex-m0——思想转变》之后又一入门级**,在此不敢请老鸟们过目。不过要是老鸟们低头瞅了一眼,发现错误,还请教育之,那更是感激不尽。
之前我们通过了解如何向芯唐Cortex中下载程序,对芯唐芯片的内部结构已经有了一定的了解,那么接下来就开始软件编程的学习,毕竟这才是我们操作一个芯片和核心。学习软件编程,首先从例程学起,等自己对其中的各种操作方法所熟悉了以后,接下来就要用自己的思想、自己的思维创造程序了。本文就是通过写本人对一个例程的详细注解,来说明51Cortex在某些操作方式上的异同,让自己对Cortex有了更深一步的了解,所以在此分享,以期对我们这些“51菜”在学习Cortex上有所帮助,文中有不对之处,还请大虾们指正。
正文
一、51与新唐操作方式
   人们在搞过芯片以后,最大的感触就是“CPU对任何器件的操作,无非就是向相应的寄存器或引脚上写入数据(开关量也可是当做是数据)”,这是我对CPU操作的理解。那么,我们学习的核心就成为如何从CPU向外写数据。(1)、在51上,我们向外写数据主要是通过P0P1P2P3口,且在每个端口上分别有8个引脚。(在这里我们用端口代表P1等,用引脚代表P1^0等);而新唐上,我们主要通过PA,PB,PC向外发送数据,且在每个端口上有16个引脚。这些引脚即可以按字节/双字输出,也可以按位输出。(2)、说下端口模式51的四个并行口都是双向的,其中,P0口为漏极开路驱动,P1P2P3口有内部上拉电阻驱动,是准双向口。而Cortex上引脚上是有个弱上拉电阻的,且其I/O类型可由软件独立地配置(即用户通过设置相应的寄存器来选择引脚是  输入、输出、开漏还是准双向)。我们在使用51是,当需要用到漏极开路模式(即不输出电压,低电平时接地,高电平时不接地。如果外接上拉电阻,则在输出高电平时电压会拉到上拉电阻的电源电压。这种方式适合在连接的外设电压比单片机电压低的时候)时,就将器件接到P0口,用到准双向模式(用做输入时被拉高,低则要靠外部电路拉低)时,器件接P1P2P3口。而对于新唐,每个端口的模式都可以自己根据需要设定,这为设备的开发提供了极大地方便,但是,前提是使用端口是必须进行模式的设定。这里需要注意:新唐默认复位之后,所有引脚的I/O模式均为准双端模式。所以,当我们要使用其他模式时,一定要修改相应的寄存器。(3)、引脚的功能配置。新唐与51一样,许多I/0口都具有第二,第三功能,但是二者的控制方式有很大不同。我们在使用51时,虽然也面对引脚的第二功能,但是我们在使用第二功能时,并不需要过多的设置,只需在某个时刻的输出数据,我们编程人员自己知道他是数据还是地址即可,相应的操作我们可以在对应的时刻通过其他引脚控制片选来实现数据的流向。但是由于在新唐中,一个引脚的功能可能有多种,且未必只是地址/数据这样简单,还有一些是外部功能引脚,所以,我们在使用相应的引脚功能时,还要设置相应的寄存器。比如:我们要是用PA.15来输出PWM信号,只要向寄存器GPA_MFP中的第15位写入1,寄存器PA15_I2SMCLK中写入0即可使PA.15输出PWM信号。寄存器如下图:


源文件见附件

从51跳cortex-m0学习2——程序详解.pdf

728.11 KB

相关帖子

沙发
watch186|  楼主 | 2011-11-23 16:25 | 只看该作者
二、例程详述
   通过上面对Cortex和51引脚模式和功能配置的对比,我们大概能够看到在我们要用新唐对端口进行操作时都要做哪些工作。现在许多32位的处理器都会提供给用户驱动程序包(即面向用户设计的函数库),而我们的许多开发只要是基于芯片厂商所提供的应用程序包进行即可。但是,笔者认为,对底层函数的了解对我们对芯片处理器的认识帮助是不可估量的。虽然我们大部分人在开发之时很少用到底层的函数,也有些牛人真正进行纯裸编。但是,笔者认为,32位处理器的底层函数才是真正可以将我们的认识从51上升至Cortex的所在,所以下面是本人对某个例程的详细标注(其中也包括底层函数),相信通过对例程的详细研究,认识会更进一步提高。

注:
1、本文引用的例程为二姨家的某大仙发布的流水灯例程,姑且在此引声明,以免引起误会,同时也给出该例程的下载地址。
https://bbs.21ic.com/attachment.p ... e46%2BYhDSkV%2Bfma0
2、这里也给出进行软件学习所需要的几个必备工具:Keil4是必须的,还有就是芯片的数据手册(芯唐Cortex-M0-NUC120数据手册.pdf),技术参考手册(DocumentsInfo_DA00-NUMICRONUC100SCD1.pdf),驱动参考手册(NUC1xx+驱动参考.pdf),都可以在官网上下载到。
   这里也提一句,Keil中的 原函数查找  确实是个好东西,尤其是需要用到底层驱动函数这样的大程序,很受用。



例程详解:
   打开工程文件(我这里是字体放大了的  呵呵  方便看),如图:

可以看到,左边的工程窗口中的三个文件包,CMSIS File ,Source File 和Library File 文件,其中CMSIS 和 Library中  是存放系统程序的,也就是前面提到的芯片厂商所提供的程序包,而Source中就是存放我们的用户级程序,也就是我们自己开发的程序。这里例程中的程序文件是:NUC120_HOT_FIRST.c

   当我们拿到一个程序时,先从主函数看起,那些什么头函数那些相信只要是接触过C语言的人都知道,这里不在赘述。主函数如图:

我们看到主函数 是由开始的一堆函数操作和一个WHILE循环构成,WHILE循环显然是我们操作的主体。而while循环中,又是由13个小函数构成的。当然,我们初次见到函数DrvGPIO_ClrBit(E_GPA,2);可能不知道是起什么作用的,但是我们对delay_loop();已是再熟悉不过啦,是个循环延时函数。那么我们这里主要看其他函数的功能。从主函数的开头看起。
1、        UNLOCKREG();
   从字面上来看,该函数的作用是:解锁寄存器。注意,在规范化的编程中,我们一般都可以根据变量,函数的名字来猜测其作用。我们通过函数查找,找到UNLOCKREG();的原型,如图:

是一个宏定义,那我们就直接看后面的替代物,
*((__IO uint32_t *)(GCR_BASE + 0x100)) = 0x59;  *((__IO uint32_t *)(GCR_BASE + 0x100)) = 0x16;   *((__IO uint32_t *)(GCR_BASE + 0x100)) = 0x88
我们可以看到,该宏定义是完成了三个操作,分别是向三个地址(这里用的是指针变量)中写入 0x59,0x16,0x88。而乍一看这三个地址好像是一样的,中间没有程序改变指针的值。那么现在就这个地址到底指的是什么位置。我们先解释一下*((__IO uint32_t *)(GCR_BASE + 0x100)) = 0x59;的  含义。




由以上四个定义可知,_IO是进行了宏定义,与关键字volatile一样;uint32_t是unsigned int 的替代;而GCR_BASE是宏定义,与(AHB_BASE + 0X00000)一样;而AHB_BASE与uint32_t 0x5000_0000一样。那么*((__IO uint32_t *)(GCR_BASE + 0x100)) = 0x59;的意思就是:向易变(volatile)的无符号整形类的地址0x5000_0000+0x00000+0x100 中写入数据0x59 。那么0x5000_0000+0x00000+0x100 的地址到底是什么东西呢?我们打开技术参考手册(DocumentsInfo_DA00-NUMICRONUC100SCD1.pdf)在里面找到  寄存器写保护控制寄存器(REGWRPORT)


由此可知,函数UNLOCKREG(); 是完成写保护的解锁工作,可以向被保护的寄存器中写入数据。
2、        SYSCLK->PWRCON.XTL12M_EN = 1;
   上面是解锁操作,那么此句显然是向被保护的寄存器中写入数据。我们来分析操作的意义:



通过函数查找,我们知道SYSCLK 是一个结构体指针,其地址是AHB_BASE + 0X00200 。而SYSCLK->PWRCON 即为地址为AHB_BASE + 0X00200 开始的结构体中的PWRCON元素,那么PWRCON又是什么类型的呢?AHB_BASE + 0X00200地址又是指的什么呢?

使用特权

评论回复
板凳
watch186|  楼主 | 2011-11-23 16:26 | 只看该作者
可知,AHB_BASE + 0X00200指的是CLK_BA 时间控制寄存器

而SYSCLK_T的结构体中定义的成员正是上表中的寄存器。所以SYSCLK->PWRCON 就是指  系统掉电控制寄存器 。我们再考虑PWRCON的数据类型。

由此,我们看到,PWRCON也是一个结构体类型的变量,其结构体为SYSCLK_PWRCON_T。看到这里,我们终于明白SYSCLK->PWRCON.XTL12M_EN = 1;是将  结构体类型SYSCLK中的结构体PWRCON中的变量XTL12M_EN赋值为1。接下来需要弄清楚的是XTL12M_EN 是什么东西,查手册可知




  最后,费了这么大的劲,原来SYSCLK->PWRCON.XTL12M_EN = 1; 是在设置 系统使用外部晶振。
3、LOCKREG();
  显然此函数是 锁定寄存器写保护,不至于随意改变寄存器的值。


给寄存器写保护控制寄存器(0x5000_0000+0x00000+0x100)写入任意数,即可进入保护状态。此处写的是0x00 。
4、DrvGPIO_Open(E_GPA,2, E_IO_OUTPUT);
  我们打开驱动参考手册,在里面找到:

的说明。我们可以看到,详细的函数说明信息,即此函数的功能是:设置端口的输出模式。那我们接下来看这个函数的源代码:

int32_t DrvGPIO_Open(E_DRVGPIO_PORT port, int32_t i32Bit, E_DRVGPIO_IO mode)
{
    volatile uint32_t u32Reg;
   
    if ((i32Bit < 0) || (i32Bit > 16))
    {
        return E_DRVGPIO_ARGUMENT;
    }   

    u32Reg = (uint32_t)&GPIOA->PMD + (port*PORT_OFFSET);   
    if ((mode == E_IO_INPUT) || (mode == E_IO_OUTPUT) || (mode == E_IO_OPENDRAIN))
    {
        outpw(u32Reg, inpw(u32Reg) & ~(0x3<<(i32Bit*2)));
        if (mode == E_IO_OUTPUT)
        {
            outpw(u32Reg, inpw(u32Reg) | (0x1<<(i32Bit*2)));
        }else
        if (mode == E_IO_OPENDRAIN)
        {
            outpw(u32Reg, inpw(u32Reg) | (0x2<<(i32Bit*2)));
        }
    }else
        if (mode == E_IO_QUASI)
    {
        outpw(u32Reg, inpw(u32Reg) | (0x3<<(i32Bit*2)));
    }else
    {
        return E_DRVGPIO_ARGUMENT;
    }
        
        return E_SUCCESS;
}
我们先逐条看里面内容。
E_DRVGPIO_PORT:                 E_DRVGPIO_IO:
  
定义了两个枚举类型,前面枚举出可能的端口号(A,B,C,D,E),后一个枚举出端口的输出模式(输入,输出,开漏,准双向)。这两个在加上uint32_t 一共在函数中定义了三个局部变量。

该句是设定 局部变量范围。必须规定在0~16之间。因为函数所定义的第二个局部变量i32bit 是控制移位的,后面将会看到为什么定义这个范围。

此句显然是将某个具体的地址值赋给变量u32Reg,那么此地址是



地址为AHB_BASE+0x4000所指的寄存器,查数据手册可知

(uint32_t)&GPIOA->PMD指的是GPIO端口A 的PMD寄存器位。而port*PORT_OFFSET(此处offset是宏定义,指的是数0x40)就是使地址能够指向不同的I/O端口。由此u32Reg = (uint32_t)&GPIOA->PMD + (port*PORT_OFFSET);的意思就是某个端口(A,B,C,D,E)的PMD地址 = A 端口的PMD地址 + 端口号*40H。


此句是说明定义的输出模式如果不是准双向,则执行函数outpw(u32Reg, inpw(u32Reg) & ~(0x3<<(i32Bit*2))); 那么函数outpw(port,value)又是什么呢?

说明是给 某个地址 赋给 值value。因此,此句的意思是给地址为u32reg的端口寄存器赋value。

(0x3<<(i32Bit*2))即使第i32bit位赋值 11,前面加上取反即使给第i32bit位清零。
说了这么多,全是为这一句话——outpw(u32Reg, inpw(u32Reg) & ~(0x3<<(i32Bit*2)));。即将端口u32Reg的第i32Bit位赋值00 ,其余位的值保持不变(因为是还读了一下u32Reg的值呢,inpw函数)。

太麻烦啦说得这么细,是在太累啦~休息一下……不过这点讲清楚了下面就容易啦……

下来就一样啦,设置成输出模式,就给端口的第 i32Bit位赋01,输入模式就赋10 。
5、DrvGPIO_ClrBit(E_GPA,2);
下面进入主循环。


可以看出,tGPIO是一个GPIO_T型的结构体,函数的关键在于

由上面可知此句是端口的地址赋给指针tGPIO(这里要注意,是结构体类型的指针,里面数据占得空间大小要小心)。

有了前面的经验很容易看出,这里是要给端口的引脚输出值(置0)。

所以,此函数的意思是将E_GPA端口的第二引脚置0 。
6、DrvGPIO_SetBit(E_GPA,2);
和上面一样,是将E_GPA端口的第二引脚置1 。

总结:写到这里,我们终于可以看到这个主函数究竟是要做些什么工作啦。从原理图上可知A端口的2,3,4,5分别连接LED灯。所以,程序结果是:四个灯依次亮,之后全灭,往复循环。
   本例程虽然只是一个相当简单的Cortex程序,我们完全可以只看用户端的函数驱动来进行编写,但是当我们真正将一些底层驱动函数研究之后,相信我们对Cortex掌握理解得更加彻底。等我们完全熟悉之后,再凭借我们的经验进行基于用户函数的开发,这样相对来说要好得多。

Watch186
2011年11月

使用特权

评论回复
地板
hotpower| | 2011-11-23 21:39 | 只看该作者
很好,用自己的语言总结。鼓励一下

使用特权

评论回复
5
watch186|  楼主 | 2011-11-23 23:40 | 只看该作者
呵呵  谢谢大叔

使用特权

评论回复
6
s010800519| | 2011-11-24 14:41 | 只看该作者
学习了,弱弱的问下,*((int*)(常数))=0x59这是个合法的语句吗,要怎么理解。这一句应该就是*((__IO uint32_t *)(GCR_BASE + 0x100)) = 0x59这一句结构的简化吧!

使用特权

评论回复
7
zxcscm| | 2011-11-24 15:20 | 只看该作者
作为从51跳过来的同路人,这样分析很详细 很给力啊

使用特权

评论回复
8
lee9888| | 2011-11-24 17:21 | 只看该作者
好,详细,谢谢

使用特权

评论回复
9
watch186|  楼主 | 2011-11-24 18:22 | 只看该作者
呵呵  看来咱们得一起努力啦啊  追赶大家…… 7# zxcscm

使用特权

评论回复
10
watch186|  楼主 | 2011-11-24 18:24 | 只看该作者
8# lee9888

使用特权

评论回复
11
s010800519| | 2011-11-24 18:44 | 只看该作者
楼主,可以帮忙解答下我的问题吗?:)

使用特权

评论回复
12
watch186|  楼主 | 2011-11-24 19:52 | 只看该作者
6# s010800519     *((int*)(常数))=0x59 是不合法的操作。此句话如果是用来理解,可以说得通,即给某个地址赋值0x59 。 但是在C语言里,虽然指针是用来指向地址的,但是其是用一个指针变量来指向地址的,注意,是指针变量,也就是  *常数  是被识别不出来的。所以,遇到给某个具体的地址赋值问题,一般用:
int *p ;
p = 常数;
*p = 0x59;
再说,我们之所以用指针是就是因为要进行  变地址操作(给不同的地址赋值),所以不会为了某一个 地址而使用指针的  
6楼的思想是想说51 中特殊寄存器是可以直接赋值的,在新唐中是不是也是吧。在新唐cortex中是用结构来定义的特殊寄存器,所以寻址的时候是用间接寻址操作,指针。51中是sfr来定义的特殊寄存器地址,可以进行直接寻址操作

使用特权

评论回复
13
watch186|  楼主 | 2011-11-24 20:39 | 只看该作者
12# s010800519 再具体一点说  等号左边是不可以有  地址常数  的!!!

使用特权

评论回复
14
john_lee| | 2011-11-24 20:43 | 只看该作者
楼主很用功,赞一个。

使用特权

评论回复
15
s010800519| | 2011-11-25 12:09 | 只看该作者
谢谢LZ这么详细的解释,虽然我还是不大懂。

使用特权

评论回复
16
s010800519| | 2011-11-25 12:11 | 只看该作者
如果 *((int*)(常数))=0x59 是不合法的操作,那为什么*((__IO uint32_t *)(GCR_BASE + 0x100)) = 0x59是合法的操作呢,GCR_BASE + 0x100也是个常数吧!!

使用特权

评论回复
17
Metalor| | 2011-11-25 13:04 | 只看该作者
顶!!用心了的~~

使用特权

评论回复
18
hotpower| | 2011-11-25 15:06 | 只看该作者
左值是指针或变量等可以赋值的就是合法的,总之左值不能是常数,因为它没有存放的空间。赋值就是存放,不能存放就不是合法的。明白否?

使用特权

评论回复
19
s010800519| | 2011-11-25 15:56 | 只看该作者
我知道左值不能是常数,这一点基本的c语言知识我还是懂的。我主要想问的是*((__IO uint32_t *)(GCR_BASE + 0x100)) = 0x59这个语句凭什么是合法的,它左边也是常数啊。GCR_BASE 这个宏定义过的东西就是常数啊。它跟我的那句*((int*)(常数))=0x59有什么区别啊

使用特权

评论回复
20
watch186|  楼主 | 2011-11-26 09:04 | 只看该作者
20# s010800519   说了半天  你是要问这个!前面说的有点太抽象。这要说实质就牵扯的太多啦,具体点吧,你问的这个东西是和编译器有关系的,知道为什么在建立工程时要选择芯片类型吗?其中一个主要的原因就是因为每种芯片用的编译器是不同的,我们在选择51时,编译器会认定为CISC架构,而选择新唐时(主要是CORTEX)选择RISC架构。由此,在CISC架构下,我们主要使用MOV等,其形式为
MOV
操作数1,操作数2
其中,我们要进行地址的赋值,就要在操作数前加@(这个知道吧)。而  RISC架构下,我们汇编的操作主要是用
LDR, STR
来进行数据操作的(当然,用MOV也可以,但是作用不同,mov只能在寄存器之间移动数据,而内存与CPU之间就要用LDRSTR)。注意:我们打开新唐的库文件startup_NUC1xx.s就可以看到里面的汇编,用的就是 LDR


而使用LDR的方法是
LDR   r0,=label      用于加载立即数或一个地址值到指定寄存器中。其中
如果label是立即数
就是将立即数放到r0中;如果label是个标示符,就是将label所指的地址值放入 r0
。由此可见
*((__IO uint32_t *)(GCR_BASE + 0x100)) = 0x59
中 GCR_BASE是个标示符
即翻译成汇编就是地址
所以此处是按地址进行操作的
就是代表一个指针
并不是像你说的
常数 。

这样明白啦吗? 说得太累啦……解释了这么多
呵呵

哎 ,要是觉得回答得还行的话就帮忙投张票吧,呵呵 (不是拉票,是太想要
程昌南老师的《ARMLinux入门与实践》
呵呵)

https://bbs.21ic.com/icview-287953-1-1.html

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

0

主题

104

帖子

1

粉丝