John Lee(1513562323) 20:29:06
我们今天讨论“位对象”的其它操作符函数,临时对象的析构,和代码效率问题。
John Lee(1513562323) 20:31:14
上次我们讲了 write() 函数的参数和返回值,现在我们接着讨论 write() 函数的操作。
John Lee(1513562323) 20:32:18
file:///C:/DOCUME%7E1/ADMINI%7E1/LOCALS%7E1/Temp/moz-screenshot.png
John Lee(1513562323) 20:32:38
模板参数 S 和 W,在定义“位对象”(BRD, DIV_X_ONE等)时,就已经给出了,这是常量。对于 BRD 来说,S = 0,W = 16。
John Lee(1513562323) 20:33:24
S 和 W 指定了这个“位对象”在 baud_t 临时对象的成员 val(寄存器缓冲)中的位域,S 表示起始位,W 表示位宽度。
John Lee(1513562323) 20:34:14
那么 write() 函数里就使用普通的位操作运算,把参数 v 写到 val 对应的位域。
John Lee(1513562323) 20:35:08
John Lee(1513562323) 20:37:04
形参 v 左移 S 位,val 对相应的位域清 0,然后两者“相或”,结果写回 val。
John Lee(1513562323) 20:37:56
就这样,通过对临时对象中的“位对象”一个一个连续访问,就可以对临时对象的 val 中各个位域分别赋予相应的数据。
日期:2012/1/10
John Lee(1513562323) 20:39:37
好,下面我们讲讲如何“读”某一个“位对象”的值。
John Lee(1513562323) 20:40:19
“位对象”有 两个 read() 函数:
1、__INLINE P read() { return P((reinterpret_cast<R*>(this)->val & MASK) >> LSB); }
2、__INLINE R& read(P& v) { v = read(); return *reinterpret_cast<R*>(this); }
John Lee(1513562323) 20:40:35
和一个类型转换操作符函数:
3、__INLINE operator P() { return read(); }
John Lee(1513562323) 20:41:18
其中的 P 和 R 都是模板参数,对于 baud_t 的成员 BRD,我们先回顾一下 BRD 的定义:
sfb_t<baud_t, uint32_t, 0, 16> BRD;
John Lee(1513562323) 20:41:57
John Lee(1513562323) 20:42:20
模板参数 R 就是“baud_t”,P 就是“uint32_t”。
John Lee(1513562323) 20:42:48
我们手工替换一下,得到:
John Lee(1513562323) 20:43:29
1、__INLINE uint32_t read() { return uint32_t((reinterpret_cast<baud_t*>(this)->val & MASK) >> LSB); }
2、__INLINE baud_t& read(uint32_t& v) { v = read(); return *reinterpret_cast<baud_t*>(this); }
3、__INLINE operator uint32_t() { return read(); }
John Lee(1513562323) 20:44:24
第1个函数 uint32_t read() 的实现很简单:读取了 baud_t 临时对象的 val 成员的值,并对这个值进行适当的位运算,然后返回了它。
John Lee(1513562323) 20:44:56
使用方法是这样的:uint32_t brd = UART0.BAUD().BRD.read();
John Lee(1513562323) 20:45:56
第3个函数,类型转换操作符 operator uint32_t() 也只是对 read() 函数的一个包装,使程序读取“位对象”的表达式更简洁优雅一些。
John Lee(1513562323) 20:46:22
使用方法是这样的:uint32_t brd = UART0.BAUD().BRD;
John Lee(1513562323) 20:46:58
但以上两个方法都有一个弱点:无法连续访问“临时对象”中的其它“位对象”了。
John Lee(1513562323) 20:47:41
原因是,这两个方法的返回值类型是 uint32_t,而“连续访问”要求,每一个访问(函数)的返回值,都必须是“临时对象”的引用。
John Lee(1513562323) 20:48:54
如果我们对“临时对象”的操作中,除了一个是“读”以外,其余的都是“写”,那么可以把“读”操作放在“连续操作”的表达式的末尾:
John Lee(1513562323) 20:49:36
// 写 BRD,写 DIV_X_ONE,读 DIVIDER_X:
uint32_t divider = UART0.BAUD().BRD(20).DIV_X_ONE(1).DIVIDER_X.read();
// 或者
uint32_t divider = UART0.BAUD().BRD(20).DIV_X_ONE(1).DIVIDER_X;
John Lee(1513562323) 20:50:55
为了在一个“临时对象”中读取多个“位对象”,我们要使用第2个函数:baud_t& read(uint32_t& v)。
John Lee(1513562323) 20:52:00
这个函数的返回值是 baud_t&,可以连续访问,因为返回值被用作了连续访问,那么读取的“位对象”的值就改到了函数的形参:uint32_t& v。
John Lee(1513562323) 20:52:58
这个形参是个引用类型,大家还记得上次群课中,我们讨论的 C++ 引用的特性吗?
murex(344582199) 20:53:15
嗯
John Lee(1513562323) 20:53:27
修改引用就是修改引用对应的变量!
John Lee(1513562323) 20:55:07
所以,函数调用了 read() 后,把得到的值赋值到了形参 v,也就修改了实际的变量,例如:
John Lee(1513562323) 20:56:04
// 读 BRD,写 DIV_X_ONE,读 DIVIDER_X
uint32_t brd, divider;
UART0.BAUD().BRD.read(brd).DIV_X_ONE(1).DIVIDER_X.read(divider);
John Lee(1513562323) 20:57:31
好了,“位对象”的基本操作函数,就讲完了。
murex(344582199) 20:58:03
谢谢老师
John Lee(1513562323) 20:58:42
下面我们讲缓冲数据如何写回硬件寄存器。
John Lee(1513562323) 21:00:05
当我们对“临时对象”中的缓冲数据 val 修改完毕后,需要把 val 的值再写回到硬件寄存器。
John Lee(1513562323) 21:02:04
为此,我们在“临时对象”的类型中,定义了一个成员函数:
baud_t& apply()
{
if (changed != 0)
ref = val;
changed = 0;
}
return *this;
}
John Lee(1513562323) 21:03:20
在这个函数中,val 被赋值到 ref,这个 ref 就是硬件寄存器的引用。
John Lee(1513562323) 21:05:25
但何时调用此函数呢?我们先用 C 来模拟整个过程来对照一下:
type union {
struct {
uint32_t BRD : 16;
uint32_t : 8;
uint32_t DIVIDER_X : 4;
uint32_t DIV_X_ONE : 1;
uint32_t DIV_X_EN : 1;
};
uint32_t data;
} BAUD_T;
John Lee(1513562323) 21:05:46
typedef struct {
...
BAUD_T BAUD;
...
} UART_T;
UART_T* UART0; // UART0外设
John Lee(1513562323) 21:08:11
void foo()
{
BAUD_T baud; // 定义baud变量,C中没有临时对象的概念,只能定义一个具名的变量。
baud.data = UART0->BAUD.data; // 把硬件寄存器BAUD的数据,赋值到baud变量。
baud.BRD = 20; // 在baud变量中修改BRD
baud.DIV_X_ONE = 1; // 和 DIV_X_ONE。
UART0->BAUD.data = baud.data; // 修改完成后,把baud变量赋值写回到硬件寄存器BAUD。
}
John Lee(1513562323) 21:09:52
这个程序,大家都没有困难吧?
batsong@21IC(6335473) 21:10:20
没有
John Lee(1513562323) 21:10:42
上面的C程序中 foo() 函数里的 5 句,效果相当于 C++ 的:
UART0.BAUD().BRD(20).DIV_X_ONE(1);
John Lee(1513562323) 21:11:48
可以看出,C 的实现方法较繁琐,特别是不能忘了最后的“写回”操作,这个不如 C++ 来得优雅和安全。
murex(344582199) 21:12:45
是优雅多了
John Lee(1513562323) 21:12:56
而在 C++ 的方法中,我们并没有在表达式中看到这个“写回”的操作。
murex(344582199) 21:13:03
就是开始的时候实现这个优雅动作繁琐点
John Lee(1513562323) 21:13:54
编译器会“了解”我们的需求,自动写回吗?不可能的,不要指望编译器为我们自动完成这个操作。
John Lee(1513562323) 21:15:53
我们定义的写回操作在 apply() 函数中实现,为了让编译器调用这个函数,我们利用了 C++ 中“临时对象”生存期的概念,以及对象“析构”的概念。
John Lee(1513562323) 21:16:52
【C++ 小知识】:
临时对象的生存时限,限制在其出现的“完整”的表达式中,“完整”的表达式结束了,临时对象也就销毁了。
例外是把临时对象被引用或者初始化给具名对象,临时对象的生存期会加长到引用或者具名对象的生存期。
John Lee(1513562323) 21:19:32
当 UART0.BAUD().BRD(20).DIV_X_ONE(1); 表达式结束时,产生的 baud_t “临时对象”的生存期也就结束了,编译器将销毁这个“临时对象”,
John Lee(1513562323) 21:20:49
如果“临时对象”的类型定义中,含有“析构函数”,那么,“析构函数”会被编译器自动调用。
John Lee(1513562323) 21:22:17
在 baud_t 类型中,我们定义了析构函数:
~baud_t() { apply(); }
John Lee(1513562323) 21:23:00
这个析构函数调用了 apply() 函数。
John Lee(1513562323) 21:25:55
这样,所有的逻辑都连贯在了一起:外设->寄存器->临时对象(缓冲数据)->位对象(修改缓冲数据)......->析构(写回缓冲数据)。
John Lee(1513562323) 21:27:26
为什么不在析构函数中直接写回?而要饶一个弯,调用 apply() 函数来写回?
murex(344582199) 21:29:23
是否就是为了访问多个位,还有就是为了书写结构更加漂亮?
John Lee(1513562323) 21:29:50
因为可能有这样的情况:当修改了某个“位对象”后,逻辑上需要立即生效。
John Lee(1513562323) 21:30:38
这时,我们就可以直接调用 apply() 函数来做这个操作:
// 修改了 BRD 后,需要立即生效。
UART0.BAUD().BRD(20).apply().DIV_X_ONE(1); |