|||
新的sfr访问方法,昨天只是大概提了一下。
今天再详细讲讲用法。
头文件里预定义了特殊功能寄存器组(外设)的变量。
预定义变量在头文件中以extern定义
例如gpio文件中:
extern gpio_t GPIOA; SFR_ADDR(&GPIOA, GPIOA_ADDR);
extern gpio_t GPIOB; SFR_ADDR(&GPIOB, GPIOB_ADDR);
extern gpio_t GPIOC; SFR_ADDR(&GPIOC, GPIOC_ADDR);
extern gpio_t GPIOD; SFR_ADDR(&GPIOD, GPIOD_ADDR);
extern gpio_t GPIOE; SFR_ADDR(&GPIOE, GPIOE_ADDR);
这些变量都是gpio_t类型的。
gpio_t类型:
struct gpio_t {
volatile sfr_t<pmd_t> PMD; // GPIO Port I/O Mode Control (GPIO_PMD)
volatile sfr_t<offd_t> OFFD; // GPIO Port Pin OFF Digital Resistor Enable (GPIO_OFFD)
volatile sfr_t<dout_t> DOUT; // GPIO Port Data Output Value (GPIO_DOUT)
volatile sfr_t<dmask_t> DMASK; // GPIO Port Data Output Write Mask (GPIO_DMASK)
const volatile sfr_t<pin_t> PIN; // GPIO Port Pin Value (GPIO_PIN)
volatile sfr_t<dben_t> DBEN; // GPIO Port De-bounce Enable (GPIO_DBEN)
volatile sfr_t<imd_t> IMD; // GPIO Port Interrupt Mode Control (GPIO_IMD)
volatile sfr_t<ien_t> IEN; // GPIO Port Interrupt Enable Control (GPIO_IEN)
volatile sfr_t<isrc_t> ISRC; // GPIO Port Interrupt Trigger Source (GPIO_ISRC)
uint32_t :32;
uint64_t :64;
uint64_t :64;
uint64_t :64;
};
这是一个结构体,成员是各个特殊功能寄存器,这与新唐trm中定义的一致。
那么,访问GPIOA外设的DOUT寄存器就应该这样写:
GPIOA.DOUT
这是普通的C语言访问结构体成员的方法。
DOUT寄存器的类型是:volatile sfr_t<dout_t>
这是一个c++模板类对象,模板类是sfr_t,模板类的参数是dout_t。我们先不管它,只需知道,这是一个“类”即可。既然是“类”,那么就可能有:成员函(member )和数据成员(data member)。而这个类:sfr_t<dout_t>其中就有:构造函数,析构函数,类型转换操作符重载函数。例如,昨天讲到的,GPIOA.DOUT可以作为表达式的“左值”或“右值”。作为左值时,可以接受一个整形数值,GPIOA.DOUT = 0x1234;
我们想想,DOUT是一个类“sfr_t<dout_t>”的对象,相当于结构体,怎么能直接被赋值为一个整形呢?
例如在c语言中:
struct type_t {
...
};
struct type_t data;
data = 0x1234
这样赋值,行吗?
不行,“结构体”和“整形”是两个完全不同的类型,而且也没有隐含的转换方法,所以编译会报错:类型不匹配。在c++中,可以为这个“结构体”定义一个赋值操作符的重载函数,来进行这种在c中不合法的操作。我们在类“sfr_t<dout_t>”中,就定义了一个这样的赋值操作符:可以接受一个整形的值。编译时,编译器就可以按我们定义“特殊方法”,来对DOUT赋值了。
而DOUT作为右值时,可以赋值给一个整形(表达式左值是一个整形变量)。uint32_t data = GPIOA.DOUT;这个按c的语法,也是不允许的,在c++中,我们需要为其定义一个“类型转换操作符”函数。让编译器“知道”如何把这个类“sfr_t<dout_t>”的对象DOUT,转换为一个整形。
所以,c++并不神秘,它也是必须按你定义好的套路来走,不会无中生有的。
继续
上面说了,DOUT是如何作为“左值”或“右值”的。作为“左值”或“右值”,只能对寄存器进行“整体”访问,并不能访问其中的各个“位”。要访问寄存器中的各个“位”,需要使用“位对象”。
下面我们来看dout_t的定义:
union dout_t { // GPIO Port Data Output Value (GPIO_DOUT)
__SFR(dout_t, uint32_t, 0)
sfb_t<dout_t, uint32_t, 0, 1> DOUT0;
sfb_t<dout_t, uint32_t, 1, 1> DOUT1;
sfb_t<dout_t, uint32_t, 2, 1> DOUT2;
sfb_t<dout_t, uint32_t, 3, 1> DOUT3;
sfb_t<dout_t, uint32_t, 4, 1> DOUT4;
sfb_t<dout_t, uint32_t, 5, 1> DOUT5;
sfb_t<dout_t, uint32_t, 6, 1> DOUT6;
sfb_t<dout_t, uint32_t, 7, 1> DOUT7;
sfb_t<dout_t, uint32_t, 8, 1> DOUT8;
sfb_t<dout_t, uint32_t, 9, 1> DOUT9;
sfb_t<dout_t, uint32_t, 10, 1> DOUT10;
sfb_t<dout_t, uint32_t, 11, 1> DOUT11;
sfb_t<dout_t, uint32_t, 12, 1> DOUT12;
sfb_t<dout_t, uint32_t, 13, 1> DOUT13;
sfb_t<dout_t, uint32_t, 14, 1> DOUT14;
sfb_t<dout_t, uint32_t, 15, 1> DOUT15;
};
dout_t是一个联合,其中定义了一些“位对象”,这些“位对象”也是模板类的实例。这里可以讲讲“位对象”定义中的两个数据的意义。
我们可以看到:sfb_t<dout_t, uint32_t, 11, 1> DOUT11;
其中有两个数值:11和1。
第1个数值是该“位对象”在寄存器中的起始位,第2个是位宽度。为了访问到这些“位对象”,先必须访问到这些“位对象”的联合dout_t
union是有玄机的。这个在讲实现内幕的时候再讲。
要访问这个union,需要创建这个union的“临时对象”。这个临时对象生成后,存在于CPU寄存器中,生成后,我们就可以访问这个临时对象中的各个“位对象”,置0,置1等等。由于临时对象在CPU寄存器中,那么所有的置0,置1操作都转化为了对CPU寄存器的操作。最后当操作完成时,临时对象将会“析构”,而我们在其析构函数中,定义了一个操作:将CPU寄存器值写回到外设寄存器。
而什么时候算是“操作完成”呢?
答案是:“;”,分号,语句结束。 就是说:临时对象的生命期,就只在本语句。
转回来,我们如何创建这个“临时对象”呢?
为此,我们在DOUT的模板类“sfr_t<dout_t>”中,定义一个重载操作符“()”。
在重载的操作符中,我们创建了一个union的临时对象。
具体的程序在sfr文件的164行
__INLINE SFR operator ()() { return SFR(val); }
__INLINE SFR operator ()() const { return SFR(val); }
__INLINE SFR operator ()() volatile { return SFR(val); }
__INLINE SFR operator ()() const volatile { return SFR(val); }
__INLINE SFR operator ()(decltype(SFR::val) v) { return SFR(val, v); }
__INLINE SFR operator ()(decltype(SFR::val) v) const { return SFR(val, v); }
__INLINE SFR operator ()(decltype(SFR::val) v) volatile { return SFR(val, v); }
__INLINE SFR operator ()(decltype(SFR::val) v) const volatile { return SFR(val, v); }
这个现在也不讲,等讲内幕时吧。
我们只需知道,在特殊功能寄存器对象“GPIOA.DOUT”后使用()即可创建临时对象。
而()的使用可以有两种情况:
1、空的( )
2、( )内带一个数值。
空的()意思是:临时对象的初值,从特殊功能寄存器读取。
带值的():意思是:临时对象的初值,有数值提供。
既然是“赋值”,()就不能为空了。
GPIOA.DOUT().DOUT2(0);
同时,位变量也提供了 = 重载操作符,用来对其赋值。
GPIOA.DOUT().DOUT2 = 0;
这个语句与上一个语句,完全是一样的。
考考大家一个问题,为什么我们要为一个目的,提供“两种”方法?
为了连续赋值
那为何还要GPIOA.DOUT().DOUT2 = 0;
为了意义上的完整性,了解语义
毕竟,这个DOUT2,名义上是个对象啊,没有“左值”“右值”的操作,好像不完整。有点心理作用。等我哪天想通了,把 = 删了。呵呵
刚才说到,用()可以实现连续的“位对象”访问。
GPIOA.DOUT().DOUT2(0).DOUT5(1).DOUT10(0);
而赋值操作符,就没有这个方便性了。例如:GPIOA.DOUT().DOUT2 = 0.DOUT5 = 1;
编译通不过的。
后面的.为啥还是在这个临时对象内,有这种语法
问得好!!
这也是内幕。
这个连续的语法,和上面提到的“union”,一起构成了最大的内幕。
内幕的请听下回分解
好,刚才说到:GPIOA.DOUT().DOUT2 = 0.DOUT5 = 1;
这种形式的连续赋值,不能通过编译
GPIOA.DOUT(0).DOUT2(1).DOUT5(1).DOUT10(1);
这句什么意思?
GPIOA.DOUT(0).DOUT2(1).DOUT5(1).DOUT10(1);
我给出c等效语法:
GPIOA.DOUT = (1 << 2) | (1 << 5) | (1 << 10);
而GPIOA.DOUT().DOUT2(1).DOUT5(1).DOUT10(1);
的等效语法:
GPIOA.DOUT |= (1 << 2) | (1 << 5) | (1 << 10);
GPIOA.DOUT().DOUT2(1).DOUT5(0).DOUT10(1);
的等效语法:
GPIOA.DOUT = (GPIOA.DOUT | (1 << 2) | (1 << 10)) & ~(1 << 5);
~(1 << 5)与(0<<5)有区别吧
~(1 << 5) = ~0xffffffef
(0 << 5) = 0
好,基本的用法是这样了。
例如:读PIN时,我希望读2个单独的位。怎么办?
PIN3 读到变量 data1, PIN5 读到 data2
容易想到的方法是:
int data1 = GPIOA.PIN().PIN3;
int data2 = GPIOA.PIN().PIN5;
但这样做会实际读两次PIN寄存器。能不能只读1次?
全部读出来
------------
是否这样:
uint32_t pin = GPIOA.PIN;
int data1 = (pin >> 3) & 1;
int data2 = (pin >> 5) & 1;
效率是高了,但安全性呢?
搞一个位域,是吧?
你还是使用了立即数或宏。
用位域,那就没有效率,还是会读两次。
那是用啥啊??
一个变通的办法是读出整个PIN,然后把这个值强制为位域的那个结构体。
例如:
int pin = GPIOA.PIN;
int data1 = ((PIN_T) pin).PIN3;
...
各位可以试试。
两种写法:
1、int data1 = ((PIN_T) pin).PIN3;
2、int data1 = ((PIN_T*) &pin)->PIN3;
下面的肯定可以通过编译,上面的大家自己试试。
我给出我的方法:
auto pin = GPIOA.PIN();
int data1 = pin.PIN3;
int data2 = pin.PIN5;
这个是真正的高效方法。
使用了一个自动变量(auto),将PIN的临时对象,显式化了。相当于给那个“临时对象”安上了一个名字。安上名字后,这个对象就不在是临时对象了,它的生命期,也不会只限于创建临时对象的那条语句了。它的生命期,会直到你不使用它时,为止。
auto pin = GPIOA->PIN;
int data1 = pin.PIN3;
int data2 = pin.PIN5; 编译不过
-----------
你需要使用这个“环境”才行。
#include "numicro/sfr/gpio",gcc加上我写的一整套相关头文件。具体你看看菜农的lookdemos.zip中的例程。但新的c++标准:c++11(原称为c++0x),将其语义改变了
GPIOA.DOUT().DOUT2(0).DOUT5(1).DOUT10(0);用()可以实现连续的“位对象”访问
是因为这些位对象本身就是dout_t类型的吧?和DOUT是同类型的uino
否
这些位对象是dout_t的成员。
相当于组合,用一个不恰当的比喻,dout_t是parent,而DOUTx对象是child
关于那个sfr,我还有一些比较遗憾的地方。就是那个__SFR宏,目前没有替代的办法。
主要问题在于c++对union的限制太多。不能继承,不能派生。并且其成员不能有构造析构函数