打印
[电路设计]

寄存器按位优化 - 让大叔的红杏再出一次墙(最新版在31楼)

[复制链接]
14234|55
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
X-Hawk|  楼主 | 2011-4-30 01:39 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 hotpower 于 2011-6-25 00:01 编辑

更新1:4楼的头文件更新,新增对IAR的支持。
更新2:19楼新的写法,更简洁。
更新3:22楼仿Lee老师的c++连续复制写法,简易且不容易出错。
           UART0s.UA_FSR()
                 .BIF(1)
                 .FEF(1)
                 .PEF(1);
更新4:24楼发布新版,
           支持在gcc底下使用,在gcc编译器下也达到了很好的优化效果。
           重新整理了代码,让发布的头文件更具有可读性。
更新5:31楼发布新版。
           寄存器定义,在gcc下用变量代替宏定义。这样调试时有可能感应出变量的值(又是向Lee老师学的,哈哈。。)。
        05/14做小更新,减少了一些重复定义。因为无法解决重复链接的问题,Keil下变量还是用宏代替,调试时无法感应变量的值。


===========================================

一、要做什么(背景介绍)
首先定义两个类型
typedef unsigned int           _reg32_t;
typedef volatile unsigned int  reg32_t;

菜农老师的红杏这么写的:
1. 定义每个寄存器中的每位
  typedef struct {
    ...
    reg32_t PEF             : 1;
    reg32_t FEF             : 1;
    reg32_t BIF             : 1;
    ...
  } uart0_ua_fsr_bits_t;
2. 用union定义寄存器
  typedef union {
    uart0_ua_fsr_bits_t       b;
    reg32_t                   v;
  } uart0_ua_fsr_t;
3. 将寄存器分组
 
typedef struct {
  ...
  /*0x40050010*/ uart0_ua_mcr_t                 UA_MCR;
  /*0x40050014*/ uart0_ua_msr_t                 UA_MSR;
  /*0x40050018*/ uart0_ua_fsr_t                 UA_FSR;
  ...
} uart0_t;
#define UART0s      (*(uart0_t *)0x40050000)
(以上跟菜农老师的代码略有不同,意思应该没有表达错。)
于是要访问fsr寄存器,可以按位访问(写法一)
      UART0s.UA_FSR.b.BIF = 1;
      UART0s.UA_FSR.b.FEF = 1;
      UART0s.UA_FSR.b.PEF = 1;
也可以按完整32位值访问(写法二)
      unsigned int v = UART0s.UA_FSR.v;
      v |= 0x00000070;
      UART0s.UA_FSR.v = v;
两个代码效果是一样的,但是效率却完全不一样,
在Keil O2优化下,写法一花费了13条指令,写法二只花费了5条。

第二个虽然很优化,写起来却不那么痛快,
需要仔细对比每个bit所在的位置。

那么有没办法,做到如写法一,又有写法二的效率呢。
答案是肯定的,我们来玩魔术,让大叔的红杏再出一次墙。

相关帖子

来自 2楼
X-Hawk|  楼主 | 2011-4-30 02:17 | 只看该作者
本帖最后由 hotpower 于 2011-5-4 21:09 编辑

三、友情提醒
1. 用到下划线_表示的bit位,最后要触发实际写动作,需要加一个 REG_COMMIT 操作。
2. 如果实在看 REG_COMMIT 不爽,也可以在最后一个操作用 “不优化的位定义”,
   同样能达到目标,但指令数会稍多一点。
      UART0s.UA_FSR._.BIF = 1;
      UART0s.UA_FSR._.FEF = 1;
      UART0s.UA_FSR.b.PEF = 1;
3. 不用编译器优化,可能达不到预期目标。
4. 有些寄存器的位读写次序有讲究的,请留意优化陷阱
5. 头文件不必那么累手工生成,附上javascript自动工具一枚,
   酒鬼最喜欢的语言还是js啊,
   点击《Export as optimized header》,即可生成本文格式的NUC100头文件。
    NUC100_Reg.zip (121.44 KB)

用文中所描述格式创建的NUC100和M051的两个头文件。
NuMicro_Reg.zip (46.56 KB)

使用特权

评论回复
来自 3楼
john_lee| | 2011-4-30 02:30 | 只看该作者
X-Hawk高明,我也来凑个热闹:
inline void uart_t::SetBaudRate(uint32_t baudrate, uint32_t clock)
{
    if (__builtin_constant_p(baudrate) && __builtin_constant_p(clock)) {
        __typeof__(BAUD) data = { 0 };
        uint32_t div;
        if ((clock / baudrate) % 16 < 2) {      // Source Clock mod 16 < 2 => Using Divider X =16 (MODE#0)
            div = 15;
        } else {                                // Source Clock mod 16 >2 => Up 5% Error BaudRate
            data.DIV_X_EN = 1;                  // Try to Set Divider X = 1 (MODE#2)
            data.DIV_X_ONE = 1;
            div = 0;
            if (clock / baudrate - 2 > 0xffff) {// If Divider > Range
                data.DIV_X_ONE = 0;             // Try to Set Divider X up 10 (MODE#1)
                div = 8;
                if ((clock / baudrate) % (div + 1) >= 3) {
                    div++;
                    if ((clock / baudrate) % (div + 1) >= 3) {
                        div++;
                        if ((clock / baudrate) % (div + 1) >= 3) {
                            div++;
                            if ((clock / baudrate) % (div + 1) >= 3) {
                                div++;
                                if ((clock / baudrate) % (div + 1) >= 3) {
                                    div++;
                                    if ((clock / baudrate) % (div + 1) >= 3) {
                                        div++;
                                        if ((clock / baudrate) % (div + 1) >= 3)
                                            div++;
                                    }
                                }
                            }
                        }
                    }
                }
                data.DIVIDER_X = div;
            }
        }
        data.BRD = clock / baudrate / (div + 1) - 2;
        BAUD = data;
    } else
        .... // 调用DrvUART的波特率初始化
}

使用特权

评论回复
来自 4楼
X-Hawk|  楼主 | 2011-5-1 23:50 | 只看该作者
本帖最后由 hotpower 于 2011-5-4 21:09 编辑
实际酒鬼很早就提到此问题了。
酒鬼的方法很好,就是头文件要加倍。
hotpower 发表于 2011-5-1 18:38

回菜农老师,继续改进新的写法,去掉加倍的头文件。。并且,上面的写法统统作废。。作废。。
还是用1楼的例子,这是1楼的写法:
 UART0s.UA_FSR.b.BIF = 1;
UART0s.UA_FSR.b.FEF = 1;
UART0s.UA_FSR.b.PEF = 1;
其中有个.b, 略显麻烦。

新的写法如下:
新写法一:非优化版本 - 13条汇编指令:
 UART0s.UA_FSR.BIF = 1;
UART0s.UA_FSR.FEF = 1;
UART0s.UA_FSR.PEF = 1;
新写法二:优化版本(带下划线)- 5条汇编指令:
 UART0s.UA_FSR_.BIF = 1;
UART0s.UA_FSR_.FEF = 1;
UART0s.UA_FSR_.PEF = 1;
REG_COMMIT(UART0s.UA_FSR); //或 REG_COMMIT(UART0s.UA_FSR_);
新写法三:直接32位赋值(仅c++有效)- 5条汇编指令
 unsigned int v = UART0s.UA_FSR;
v |= 0x00000070;
UART0s.UA_FSR = v;
写法三只用于c++, 如果在c程序用,需要加上.v,和1楼的旧写法一样,并且Keil同样编译出5条汇编指令
 unsigned int v = UART0s.UA_FSR.v;
v |= 0x00000070;
UART0s.UA_FSR.v = v;
支持新写法的寄存器头文件下载
NuMicro_Reg_new.zip (32.83 KB)

使用特权

评论回复
来自 5楼
X-Hawk|  楼主 | 2011-5-2 11:13 | 只看该作者
本帖最后由 hotpower 于 2011-5-4 21:10 编辑

继续毫无节制的现宝。。。

昨天看Lee老师的写法,用流的方式复制,大受启发。
uart0_baud << clear
<< setbit<NU_UART_BAUD_t>(NU_UART_BAUD_t::BIT_DIV_X_EN)
<< setbit<NU_UART_BAUD_t>(NU_UART_BAUD_t::BIT_DIV_X_ONE)
<< apply;
于是想到换个形式:
UART0s.UA_BAUD()
    .DIV_X_EN(1)
    .DIV_X_ONE(1);
酒鬼只是给换个碗装,汤药还是Lee老师的汤药。

这个形式的编译效果,相当于以下伪代码
  unsigned int v = UART0s.UA_BAUD;
  v |= BIT_DIV_X_EN | BIT_DIV_X_ONE;
  UART0s.UA_BAUD = v;
共6条asm指令。

或者,另一个形式,UA_BAUD先赋个初值0的
UART0s.UA_BAUD(0)
    .DIV_X_EN(1)
    .DIV_X_ONE(1);
这个形式的编译效果,相当于以下伪代码
  unsigned int v;
  v = BIT_DIV_X_EN | BIT_DIV_X_ONE;
  UART0s.UA_BAUD = v;
少一次读取动作,共4条asm指令。



这个新的代码方式,实现了按语句优化。
在这个连续的函数调用语句内,
只读/写寄存器一次,中间多次对位赋值,都可以实现完全的优化。

如果要实现1楼的赋值功能(13条指令):
UART0s.UA_FSR.b.BIF = 1;
UART0s.UA_FSR.b.FEF = 1;
UART0s.UA_FSR.b.PEF = 1;
只需要写
UART0s.UA_FSR()
    .BIF(1)
    .FEF(1)
    .PEF(1);
编译出来共计5条asm指令。

效果和19楼的写法二、写法三一样,但是更简洁、更不容易写错。


注:
1. 支持上述功能,需要c++, 并用新的头文件 NuMicro_Reg_new2.zip (108.68 KB)
2. 编译器需要开优化,测试的优化级别
     Keil 4.10: -O3 + Optimize for Time
     IAR 6: High, blanced

使用特权

评论回复
来自 6楼
X-Hawk|  楼主 | 2011-5-12 14:12 | 只看该作者
本帖最后由 X-Hawk 于 2011-5-14 23:00 编辑

学习李老师的办法,将gcc下宏定义改为变量。
这样gcc的程序在调试时,也能感应到变量内容了。

#define USBs        (*((usb_t *)    0x40060000))
改为
extern usb_t       USBs;
__asm__ (".global USBs    \n.weak USBs   \n,set USBs,        0x40060000");
更新版头文件下载(NUC100和M051系列):
NuMicro_Reg_2011_05_14.zip (43.52 KB)

使用特权

评论回复
7
hotpower| | 2011-4-30 01:56 | 只看该作者
且听下回分解???

使用特权

评论回复
8
X-Hawk|  楼主 | 2011-4-30 02:06 | 只看该作者
本帖最后由 hotpower 于 2011-5-4 21:08 编辑

二、如何做(从volatile下手)

其实也很简单,让编译器的优化为我们做事情。
上面写法一有14条指令,乃是因为每一个bit,都用reg32_t修饰,具有volatile属性。
我们将volatile去掉即可。
重新定义一个下划线版本,不带volatile的
  typedef struct {
    ...
    _reg32_t PEF             : 1;
    _reg32_t FEF             : 1;
    _reg32_t BIF             : 1;
    ...
  } _uart0_ua_fsr_bits_t;
并将这些位加入寄存器中:
  typedef union {
    _uart0_ua_fsr_bits_t      _; //可优化的位定义
    uart0_ua_fsr_bits_t       b; //不优化的位定义
    reg32_t                   v;
  } uart0_ua_fsr_t;
定义一个宏,将寄存器的“可优化的值”赋值给“不优化的值”,该宏可保证触发一次实际的写动作。
  #define REG_COMMIT(reg) do{(reg).v = *(_reg32_t *)&(reg);} while(0)
我们代码这么写
      UART0s.UA_FSR._.BIF = 1;
      UART0s.UA_FSR._.FEF = 1;
      UART0s.UA_FSR._.PEF = 1;
      REG_COMMIT(UART0s.UA_FSR);
以上代码,经Keil O2编译,只要5条指令,和上面写法二的效果一样。
但是代码更清晰了,完美实现了目标。

使用特权

评论回复
9
X-Hawk|  楼主 | 2011-4-30 02:24 | 只看该作者
2# hotpower

嗯,分解完了,感谢大叔支持!

使用特权

评论回复
10
hotpower| | 2011-4-30 02:39 | 只看该作者
俺主要是担心被优化了。
一般编译器认为一个变量不经过读写一个轮回,就可能视为无用变量被优化掉了。
再当一个变量被连续赋予一个相同值时优化也可能发生。
故考虑了还是加入volatile属性稳妥些,这样不管任何优化级别,都不会出现莫名其妙的错误。
等李老师来讨论。看来是否一个将volatile属性去掉。

使用特权

评论回复
11
X-Hawk|  楼主 | 2011-4-30 02:47 | 只看该作者
本帖最后由 hotpower 于 2011-5-13 00:38 编辑

李老师这个好像能自动算出波特率的设定值,很不错!

使用特权

评论回复
12
X-Hawk|  楼主 | 2011-4-30 02:50 | 只看该作者
本帖最后由 hotpower 于 2011-5-13 00:39 编辑

7# hotpower
没错了。这个在同时读写多个位域时比较有用。
要实际写进去,
最后还是要调用一个加了volatile的语句,如
REG_COMMIT(UART0s.UA_FSR);
或:
UART0s.UA_FSR.b.PEF = 1;



使用特权

评论回复
13
X-Hawk|  楼主 | 2011-4-30 03:09 | 只看该作者
本帖最后由 hotpower 于 2011-5-13 00:39 编辑

只要假想,.b 形式的位操作,真正对实际设备读写。

而下划线 ._ 形式的位操作,是从临时空间上读写,
临时空间的结果,只有REG_COMMIT之后,或写过 .b形式的操作,
才会真正写到寄存器上。

这样就不会用错。

例如,官方BSP中DrvFMC操作flash的一段,

FMC->ISPCMD.FCTRL = 2;
FMC->ISPCMD.FCEN = 0;
FMC->ISPCMD.FOEN = 1;
FMC->ISPADR = u32addr;
FMC->ISPTRG.ISPGO = 1;
__ISB();
while (FMC->ISPTRG.ISPGO);
如果改成新的头文件,更优化的写法,则写成

FMCs.ISPCMD._.FCTRL = 2;
FMCs.ISPCMD._.FCEN = 0;
FMCs.ISPCMD._.FOEN = 1;
REG_COMMIT(FMCs.ISPCMD);
FMCs.ISPADR = u32addr;

//此处用下划线形式,FMCs.ISPTRG._.ISPGO, 可能会无法工作
//因为下划线形式有可能只写到“临时空间”
FMCs.ISPTRG.b.ISPGO = 1;  

__ISB();
while (FMCs.ISPTRG.b.ISPGO);







使用特权

评论回复
14
murex| | 2011-4-30 07:50 | 只看该作者
学习受教了

使用特权

评论回复
15
Swallow_0322| | 2011-4-30 07:55 | 只看该作者
流口水!:P

使用特权

评论回复
16
john_lee| | 2011-4-30 09:32 | 只看该作者
我写的那个函数,基本上就是根据新唐的函数 BaudRateCalculator() 稍微修改一下,如果参数 baudrate 和 clock 都是常数,就可以利用编译器的 Constant folding(常量叠算) 优化,中间所有的计算全部在编译时完成,最后生成仅两三条汇编指令。

对波特率寄存器的赋值,使用了一个缓冲数据:
__typeof__(BAUD) data = { 0 };
这个值不会分配在 RAM 中,甚至不在 CPU 寄存器中,编译器只是将它作为常量叠算的暂存值,中间所有的操作,如:
data.DIV_X_EN = 1;
data.DIV_X_ONE = 1;
等,都是对这个缓冲进行操作。最后真正的赋值在:
BAUD = data;
缓冲数据 data 表现为一个常量(立即数),与 0x30000066 之类没有什么分别。

使用特权

评论回复
17
X-Hawk|  楼主 | 2011-4-30 09:52 | 只看该作者
本帖最后由 hotpower 于 2011-5-13 00:39 编辑

13# john_lee
这个非常强悍!
昨晚我在狐疑,这个__builtin_constant_p究竟是什么。
经李老师这么一说,再加goolge, 终于明白了。

使用特权

评论回复
18
hotpower| | 2011-4-30 10:28 | 只看该作者
哈哈,都是夜猫子了~~~
总之用中间变量缓冲,只从文字池中取一次硬件结构指针地址,在此变量中折腾最后再写入必然优化。
所以当时就考虑了Regs和Bits两种写法。

使用特权

评论回复
19
weshiluwei6| | 2011-4-30 11:10 | 只看该作者
太厉害了你们

使用特权

评论回复
20
dong_abc| | 2011-5-1 12:01 | 只看该作者
来到火星了~~~

使用特权

评论回复
21
hotpower| | 2011-5-1 18:38 | 只看该作者
实际酒鬼很早就提到此问题了。
酒鬼的方法很好,就是头文件要加倍。

使用特权

评论回复
22
hotpower| | 2011-5-2 07:13 | 只看该作者
这次很好!
在任何优化级别都行吗?

使用特权

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

本版积分规则

3

主题

380

帖子

3

粉丝