关于寄存器更多的讨论
我们过去一份 blog 中("核心寄存器何其少耶"), 对于 register 被 instruction 的 size 的大小而限制, 进行了一番讨论. 尽管可能流于肤浅, 但是从这个论题上引申出来, 我们可能讨论更多的register 特点(或者多半属于概念性的科普范畴吧).
1. core registers 与其数据类型
在 ARM(特别是 32bit 架构中), core register 我们都知道所指 16pcs 的 32bit registers. 但我们是否留意到, core reigster 的数据类型?
我们强调 core register 的数据类型的原因是, 在类似 ARM 这样常见的 RICS 架构的 uController 中, instruction 仅仅针对 register, 而不是 memory data 进行处理. 因为我们知道 memory(在计算机学科, 面对 CPU 时被称为"内存"), 只能通过 load/store 的方法为 register 所交互, 最终以 register 的 operand 的方式, 数据内容在 instruction 得以 load, decode 并 execute.
自然的, 我们都想到, core register 应被定义为:
(1) 整型 (integer), 可能是 32bit, 16bit, 8bit 的有符号或者无符号整数, 这简直就是定义 word, half-word, byte. 尽管 double-word data type 也被定义, 但实际上, 它不被 instruction 直接支持 -- 至少在 cortex M 如此.
(2) 指针(pointer), 32bit 指针. 在 instruction 的 operand 的定义中, 我们见到大量用 [Rn] 表示指向内存地址存放的数据.
这带给我们一个新的思考:
2. 指针可能是低级语言中的一个重要特点
我们经常读到这样的描述: "C语言与汇编(assemble)语言比较, 是一门高级语言, 但是又具有某些低级语言的特点". 如果习惯于人云亦云的话, 我们似乎暗地里认同类似定义, 但是又难以找到继续阐述的细节. 于是, 我们也往往可能心虚转述这个说法(而担心有人继续追问).
尽管从来没有看过类似的阐述, 我们试图给出这种说法的两个理由:
(1) C语言可能具有更多的针对 uController-specific 的语言拓展. 这使得直接使用 C 语言能够进行某些控制. 如果这样说起来感觉泛泛的话, 那么我们考虑一下 CMSIS 的定义, 它将不同的 compiler 的这种应属于语言扩展的事项, 甚至用了统一的规范来描述呢.
(2) 指针. 正如同上一节中讨论的. 指针是 core registers 的数据类型的定义. 这可能反过来意味着指针是低级语言(assemble language)的一个显著特点. 我们知道, 正是因为指针用法的繁琐, 易混淆, 很多高级语言反感指针的使用; 而对于 C 语言, 对指针的大量的, 复杂的用法, 正是 C 语言最典型的特点之一.
关于指针具有低级语言的代表特点的讨论, 我没在其他的地方读到过(可能是我的知识面过窄的原因), 所以以上总结, 来自我个人的工作学习实践, 如果有错误, 请指出并有教于我.
您看, 正因为 core reigsters(我们以 32bit 的 ARM 架构为例)的数据类型的定义是: 整型. 所以, 我们利用 instruction 的各种运算的尝试, 无论它们是加减乘除, 或者甚至是算数 shift(位移), 我们都是在进行: 整型运算. 哎, 实际上, 我们怀疑设计这个运算器并不复杂, 因为2的完成数的使用( 它利用类似时针的循环转圈的计时方式 ), 向我们揭示了利用2的完成数的计算, 我们可以将减法操作也等同与加法, 甚至完成了基础课程的同学们, 能够很快手算出这么一个简单加减法的例子...
好吧, 这样我们就可以归纳出一个概念, 在类似"微处理器基本原理"或者"接口技术"这样的入门课程上, 我们学习到的知识, 这里主要指算数运算上的, 都是整型处理, 我们处理的数据都是整数! ...其原因就是 core registers 的数据类型, 被定义为整型.
-- 您的授业老师在课堂上, 曾向您强调过这一点吗?
3. co-processor 的浮点运算
(1) 从 windows 环境下的软件工作说起
作为一个在 windows 环境下编程的软件工程师, 他们是脑力活动的集大成者, 他们向我们诠释了 "智力 == 产品" 这个命题为 TRUE. 这样的软件工程师, 他们专注于功能的实现, 逻辑的运行, UI 的设计, 用户的友好, 接口的通用, 移植的简便, 数据的合理, 数据存储调用的方便. 但是, 嵌入式工程师, 同样也要编写程序, 他们表现出与 windows 环境下软件工程师几乎完全不同的风格与特点. 这显示, 简直他们不属于同一领域, 明明操作的都是代码.
(2) 讨论 debugger 行为的例子
比方说, windows 软件工程师理所当然进行 debug, 他们甚至可能把 debug 功能当作 IDE 的一部分, 而不是正确地视为 uController 或者 CPU 的一部分. 这完全抹杀了设计 uController 的芯片设计工程师的辛苦工作, 以及芯片制程中的努力. debug 依赖微控制器(或处理器)中现实的硬件门数来实现. 核心是 watch point, 它们往往都是数目有限的硬件比较器(设计上据说是昂贵的), 比较地址或者数据匹配时, 它能够使编程者进入 debug halt 模式来分析 reigster 与各个 memory data, 或者单步调试其停止位置的 instruction, 当然的, 如果功能简化, 可能他(调试者)不用进入 halt mode 而在 monitor mode 下, 以不终止 instruction 指令流的代价进入 debug monitor interrupt. 如果硬件设计中含有 ITM microcell, 那么甚至可以在调试中将主动输出的 message 放在 ITM 的 buffer 中, 向 debugger 调试器输出(这种做法是极少侵入性的). 同样, 如果硬件设计了 ETM microcell, 那么 watch point 比较器比对的结果, 也可以向 ETM 输出, buffer 给 debugger 后, 我们可以进行指令跟踪. 这些不是 IDE 的 debugger 的功能, 而是微控制器(处理器)的功能, debugger 仅仅是从类似 JTAG 这样的 DAP 接口中, 获得输出, 或者发出指令控制 debug register 实现各类调试的效果. -- 这些, 普通的 windows 软件工程师或者很难意识到, 因为 desktop 电脑的 CPU 是如此强大, 开发生态系统如此成熟, 以至于windows软件工程师常常被 IDE 所隔离并迷惑.
(3) 关于浮点功能
同样, 支持浮点运算, 在 windows 程序员的眼中, 几乎视其为当然, 定义"x = 1.0 * 2.0" 这样的语句有什么问题吗? 它们不应该必然工作的吗? -- 同样的, 这也是出于 CPU 的 power, 而将 floating point 运算视为 uController 的必然功能的观点所致.
让我们还是以目前 uController 市场上的主流 IC, 也就是 ARM 为例子来讨论. 我们阅读 ARM ARM 文档可知(后一个ARM 是 Architecture Reference Manaul, 架构参考文档的缩写). 至少 Cortex M0/M0+ 是不具有浮点运算功能的.
原因正是我们在第一节提到的, core register 仅仅是一个整型数据类型的定义.
那么如果我们需要浮点运算, uController 或者说 MCU 应该增加怎样的硬件单元呢... 当然应该是"浮点单元"吧? 那么恭喜你, 答对了.
正如同 ARMv7与 ARMv6 M profile's ARM ARM 文档中提到的, v6 variant 不具有浮点单元(FPU), 因此我们无法在 CM0 中实现浮点运算. 但是, v7-M 将 FPU 作为一个可选项(optional). 正如我们经验所知, ARM 架构中所谓可选项, 往往都被芯片商们设计中视为必选. 这可能说明, 相当的 Cortex M3 具有浮点功能. 当然可能只是一个 single precision(单精度)浮点, 是 A 与 R profile 的浮点架构的一个子集.
支持浮点, 我们必须定义一组有别于 core register(整型)的新的 registers. 支持 FPU 的 cortex M3 就提供了一组 32pcs 的单精度 FP regsiters(单精度数据类型恰好是 32bit size). 这组 32pcs FP register 的数据类型是 single precision FP. 如果我们用 [R0, R15]来标记 16pcs core registers 的话, 那么我们应该用 [S0, S31] 来标注 FP registers.
同样的, 对于 FP reigsters 的运算, 哪怕是加法, 我们都不能使用操作 core registers 作为 操作数 operand 的 ADD, 而需要使用浮点运算指令 VADD.
更深入点, 我们可以看到 uController 为我们定义了新的 PSR, 针对浮点的, FPSCR.
您看, 学习的过程就好像阅读一部悬疑小说, 随着每页纸不断翻过, 新的线索不断跳跃在眼前, 那个最可疑的嫌疑人几乎跃然于纸上.
您看, 如同一个新的 uController 在定义一般, 引入 FPU, 我们需要引入一组新的 FP register, 使用 VFP(在 M profile 中称为 FP) 指令, 并甚至有 FP 特有 PSR, 其中定义了 FP 运算中可能发生的异常!
我们的怀疑木有错, 在 ARM ARM 文档中, 向我们揭示, 对浮点运算的引入, 就相当于在 uController 中设计 co-processor. 没有记错的话, 浮点功能占据 CP10, CP11 的顺序.
4. 关于interrupt 的 stacking 中使用的 registers
在上面的讨论中, 我们从指令使用的角度, 必然引出了 register 的数据类型的定义. 通过 core registers (整型), FP reigsters(浮点型)的不同定义, 引出了针对两类不同 registers 的 instructions, 以及不同的 PSR (状态寄存器). 这简直是设计一颗新的 uController 的 designer's model... 事实上, FP 也被作为 co-processor 的而被定义.
(1) C言语与 C++ 语言写作特点中包含的信息
我没有在其他的资料中看到如下的说法, 这来源于我个人的工作学习经验. 之所以无人提及的原因, 是可能它们是如此基础, 以至于简单到不必说.
C++ 语言作为一门高级语言, 它是面向对象的. 所以说是面向对象, 就是指程序工作的主体是对象. 对象表现了我们人类所特有的一种高度归纳性, 就是将某类具有共同特点的概念, 集中为 class. 那么我们操作的具体实例可能为 class 中的一个具体, 可以用成员变量来指示属性数据, 可以用成员函数来描述特点. 在C++ 语言中, 对 class 的定义, 继承等关系, 于是也称为我们编程设计的重点.
而 C 语言似乎显示了较大不同点. C 语言几乎是一种围绕函数与过程的语言. 我们可以力图通过将基本同一范畴内的函数, 变量, 集中在某个源文件里, 但这仅仅只是表示属于开发工程师的自我归纳, 函数(过程)间相互调用, 没有类似 class 中的规范(或者说限制)特点, 而仅仅是一种来自项目开发小组(甚至仅仅是开发者个人)的自我约定(如在 roution 的命名上的层次特点).
Note: 函数(function) 与 过程( procedure) 事实上是不同概念, 它们的不同取决于是否含有 return value. 函数与过程, 事实上都属于 roution.
AAPCS 上对 roution 解释为代码片段, 在执行完毕后, 能够交还控制给 calling 点后的 instruction. 因此, 合理的术语是 roution, 可能被翻译为"例程"?
所以, 我们观察 C 语言, 在写作上, 是以 routine 的 calling (调用例程)为主要特点的.
(2) Cortex M0/M3 对 exception 调用时使用的 stacking 特点
如同 ARMv7 M Profile 的 ARM ARM 文档中描绘的那样, CM 系列 uController, 在响应异常(中端)上, 有着非同一般的"确定性"的特点.
这个特点就是, 压栈出栈(stacking) 行为是 uController 的 handware 完成的. 值得另外一提的就是, 配合 tail-chainning 与 last arriving 技术(它们都是硬件实现), cortex M0/M3 具有 hardware 实施上的快速响应, 确定性特点等优势.
我们知道, M profile 将硬件自行 stacking 的 context 包括 8 words:
R0, R1, R2, R3, R12, LR, Return address, xPSR
在 AAPCS 中, R0~R3, R12 正是作为 routine calling 中的 scratch registers 被定义. 这不是巧合, 而是整个完整, 严格, 环环相扣的电子学科的理论体系的必然. 它深刻反应出, 在C言语写作结构下, 频繁进行 roution calling 的过程中, 对 key contexts 数据维护的必然, 它反应了 (异步的) interrupt 或者 exception 不对 roution 产生任何影响, 不干扰 execute flow 的约定.
(3) 了解 routine calling 规则对编程习惯的影响
了解 routine stack 规则并非无用. 正如同"基于ARM的嵌入式系统程序开发要点"中提到的, 对于 scratch register 的合理积极使用, 而不用在 stack 中调用, 也不用从 data memory 中获取.
比方说, 在函数体内对一个变量反复进行操作, 将这个变量赋值给全局的, 或者是函数体外部, 都比不上在函数体内对 scratch registers 的反复使用. 因为这避免了 stacking data 或者 load/store 数据, 从而带来高效率.
这也暗示着, 编写代码时, 控制函数的形式参数在 4个, 也即是在 scratch registers 约定的数目下, 将不会使用 stack 传递而导致执行效率的大大提高.
学习技术的过程, 就如同阅读一本推理悬疑, 我们从基本原理出发, 可以合理利用逻辑分析去猜测某些事实; 也可以通过亲手实验去验证某类信息的正确性; 通过大量不断阅读搜索, 在后续中获得更多的线索. 这些环节将最终连贯, 将最后一环珠玉串接在一根完整的链条上. 最终的, 在书本最后一页, 我们会大喊出嫌疑人: "是他, 就是他"!
我会站在他面前, 严厉要求他: "哈, 原来是你, 偷取知识的嫌犯啊! 把你从我脑海中窃取的, 那知识大厦中的重要一环, 还给我罢!"
Allen Zhan
作于 2014.8.15 EETC
用户1326533 2014-8-19 14:53
用户1770828 2014-8-19 08:10
用户1678053 2014-8-18 09:56
看看
用户1653796 2014-8-15 15:54
用户1602177 2014-8-15 15:27