也就是整一个类似wiring 的东西,给所有引脚统一的编号,可以用类似digitalWrite(0, 1) 这种方式读写引脚。好处是容易实现平台无关的库函数,比方说只要稍微改一改映射方案就可以把给STC 设计的库用到别的C51 单片机,不用直接操作寄存器了也算是个方便,而且引脚编号就是个普通数字,使用上要比sbit 灵活。
原理
要实现这种效果,首先要设计一个映射机制,就是当程序里写了编号0 时,可以自动把0 映射到对应的引脚寄存器。比方说如果规定0 对应P0.0,那么这个映射就要根据0 去找到P0 和0 这两个信息。找到之后,后续的代码才能根据寄存器实现操作,比如置位。显然这些信息需要提前放进程序,但是要怎么放就有的讲究了。
最简单直接的就是考虑用switch case,比如写个这种函数:
int pin_num_to_port(int num) {
switch(num) {
case 0:
return P0;
}
}
int pin_num_to_pin(int num) {
switch(num) {
case 0:
return 0;
}
}
然后如果要用这两个信息去访问寄存器,在keil c51 里面像这样写:
#define PIN 0
int port = pin_num_to_port(PIN);
int bit_num = pin_num_to_pin(PIN);
sbit PPIINN = port ^ bit_num;
PPIINN = 0
是不行的,sbit 只能用全局常量的形式定义,也就是字面量写死。所以如果一定要实现,就只能要么汇编直接SETB 操作寄存器位,要么用位运算赋值,比如这样:
void setpin(port, pin) {
uint8_t val = 0x01 << pin;
*((uint8_t *)(port)) = val;
}
但是同样不一定能用,因为这个指针不一定能指向P0。比方说在STC15 单片机里,内部ram 有256 字节,低128 的部分和普通51 一样,高128 RAM 地址则和特殊功能寄存器的地址区域重叠了。经典的课本上的51 单片机的RAM 结构都知道实际只有低128 字节给用,高128 是特殊功能寄存器,而STC15 是这个样子:
因为地址相同,对特殊功能寄存器和高128 RAM 的操作用不同的寻址方式区分,就类似SBUF 用读或写来区分两个寄存器一样。对特殊功能寄存器的访问只能用直接寻址方式进行,而高128 RAM 则只能用寄存器间接寻址,也就是指针。所以要访问P0 ,只能代码里写死了P0 = 0xff 这么搞,上面代码里用指针赋值实际上访问的是高128 RAM 里的地址。
Arduino 的实现方式
先不管最后要怎么操作P0,因为switch case 的映射显得太小儿科,用函数里面放switch case 的形式实现映射表会导致额外的程序空间和调用开销,起码有几次传参和返回。那么Arduino 的标准实现是怎样的?参考源码,可以看到它是用放在程序存储区的数组实现的,拿引脚编号当数组下标,取出对应的寄存器信息,类似这样:
unsigned char code PIN_NUM_TO_PORT[]={
P0,
P0,
P1,
};
unsigned char code PIN_NUM_TO_PIN[]={
0,
1,
0,
};
#define PIN 2
int port = PIN_NUM_TO_PORT[PIN];
int bit_num = PIN_NUM_TO_PIN[PIN];
这样一来就省下了函数调用开销,存储空间应该也能省一些。稍微有点不好看的就是数组里要放一堆重复信息,不过这也是没办法。不过这个映射转化的过程仍然是在运行时完成的,映射表本身要占用一定的程序空间,查表的过程要占用时间,查完表操作寄存器又会有函数调用的开销。而实际上,对引脚的操作很少有必要放在运行时完成,大部分时候都是完全写死的。Arduino 这种实现固然在某些少见的场合能提供一些灵活性,但是多数时候只会造成性能浪费。再加上C51 对特殊功能寄存器的操作可能只有写死这么一条路走,用函数操作寄存器行不通,那么剩下能想的办法,没错,就只有大家又爱又恨的宏了。
用宏实现零开销的映射
简单实现 - 用参数连接实现映射
首先介绍两个基本工具:
#define CAT(a, b) a ## b
#define CAT3(a, b, c) a ## b ## c
CAT 的功能简单而又重要,用法是:
CAT(AAA, BBB)
AAABBB
也就是把两个参数连接到一起了,如果连接的结果里还有别的宏,那就会继续展开,比如:
#define OK_0 (0)
#define OK_1 (1)
#define OK(n) CAT(OK_, n)
OK(1)
OK_1
1
可以看到,用不同的参数调用OK() 宏函数会展开成不同的宏,并进一步展开变成不同的结果。可以说这就是魔法开始的地方。但是如果要连接三个参数呢?直觉的写法可能是:
CAT(CAT(AA, BB), CC)
AABBCC
但这是不可行的,宏函数不能嵌套展开。在宏展开的过程中,如果碰到了和它本身名字相同的宏,这个宏不会继续展开,就像一个宏展开的过程中,它本身的名字消失了,不会被识别成一个可以展开的宏。所以上面那个嵌套的例子实际上只会连接一次,变成:
CAT(AA, BB)CC
展开结果里的CAT 不会被展开,因此最后会导致编译错误。所以这时候就是CAT3 的用处所在了。
利用类似上面那个OK(n) 宏函数的机制就已经能实现简单的一对一映射功能,比如:
#define PIN_0 P00
#define PIN_1 P01
#define PIN_2 P10
#define PIN(n) CAT(PIN_, n)
#define setpin(n) do { PIN(n) = 1; } while(0)
setpin(2);
do { PIN(2) = 1; } while(0);
do { PIN_2 = 1; } while(0);
do { P10 = 1; } while(0);
P10 = 1;
P10 在STC 单片机的头文件中有定义,类似sbit P10 = P1^0 ,没有的话自己整一套也不难。这样就成功实现了零开销、纯静态的从映射到引脚操作的流程。编译后的程序里完全不会有上面那些宏代码。不过有一点要注意,如果要用#define 定义个常量当引脚编号用,不能像平常那样数字两边加上括号。比如:
#define LED 0
#define LED (0)
setpin((0))
do { PIN_(2) = 1; } while(0);
setpin(LED)
想要递归把括号脱掉是不可行的,上面已经说过一遍。比如这种:
#define PIN0 P00
#define PIN(n) CAT(PIN, n)
PIN((0))
PIN(0)
PIN0
P00
PIN(0)
然后编译器会把PIN(0) 识别成一个函数,而不是宏函数,因为宏在送到编译器之前都会被预处理器处理然后清理掉,结果当然是找不过这个函数,或者不小心错误调用了别的函数。为了避免错误调用,最简单的方法当然是遵守命名规则,映射表的宏名字都用大写。
设置低电平和读取引脚值也是类似的实现:
#define clrpin(n) do { PIN(n) = 0; } while(0)
#define getpin(n) (PIN(n))
可以在keil 里生成一个程序,然后进入调试,在反汇编窗口查看生成的实际程序代码。应该会类似下面这样,setpin 只会被转换成一条SETB 指令,clrpin 则是CLR ,没有任何多余的东西:
演示代码见后面的附件 - 1
用宏实现查表 - 宏函数展开顺序和递归
上面的简单映射实现只能囫囵的把引脚编号映射到一个值,如果想要获取更多的寄存器信息,简单的方法当然是可以多重复几遍,比如整出类似这样的表:
#define PIN_1 P01
#define PIN_REG_1 P0
#define PIN_BIT_1 1
然后用多种宏函数去做不同的映射,得出各种的信息。实际上Arduino 的实现就类似这样,不同的数组存储不同的映射,然后再相应的配上一堆函数。只是感觉比较笨,有没有可能玩儿的更花一点?那就要介绍另外两个工具:
#define FIRST(a, b) a
#define SECOND(a, b) b
很简单,两个宏函数都接受相同数量的多个参数,然后从里面选择一个展开,比如:
FIRST(11, 12)
11
SECOND(11, 12)
12
那么如果把映射表定义成这样:
#define PIN_0 0, 0
#define PIN_1 0, 1
然后用上面两个工具应该就可以像这样查表了:
FIRST(PIN_0)
FIRST(0, 0)
0
可惜并不能,会编译出错。还可以再实验一下:
FIRST(PIN_0, 99)
PIN_0
0, 0
SECOND(PIN_0, 99)
99
问题就在于,一个宏函数到底是怎么处理它的参数的。宏函数展开的第一步是参数匹配,要决定参数括号里面的哪个部分对应哪个形参符号。举个例子:
#define OK_0 0
#define OK(n) CAT(OK_, n)
#define NUM 0
OK(NUM)
首先NUM 匹配形式参数n,然后要把实际参数插入到宏函数体中。但是在插入之前,要被插入的参数首先会被展开,也就是:
OK(NUM)
OK(0)
OK_0
0
而如果是把参数直接插入之后再继续展开,就会变成这样:
OK(NUM)
OK_NUM
那到这就已经不能继续了。所以根据这个原理,对SECOND 的调用如果要实现查表的效果,就应该在SECOND 匹配它的参数之前就把PIN_0 展开成0, 0 。这样一来SECOND 实际看到的参数列表就是(0, 0) 。要达成这个效果,就需要一道二传手:
#define UNPACK_SECOND(list) SECOND(list)
看上去UNPACK_SECOND 只接受一个参数,然后也只给SECOND 传递这么一个参数,似乎要编译出错,但是由于参数插入前先展开的机制,实际中会变成这样:
UNPACK_SECOND(PIN_0)
SECOND(0, 0)
0
于是UNPACK_SECOND 发挥的作用类似于给参数先解包了,然后再传递给SECOND ,这样就能如期望般匹配参数。类似的,给FIRST 也要配一个UNPACK_FIRST ,才能实现查表的功能。
查表操作引脚 - CAT,## 操作符的特殊规则
为了方便的从表里提取信息,不妨再多定义两个宏函数包装一下。首先要有一个宏提取出引脚在寄存器里的位,这个很简单:
#define PIN_BIT(num) UNPACK_SECOND(PIN(num))
PIN_BIT(0)
UNPACK_SECOND(PIN(0))
SECOND(0, 0)
0
然后要提取出引脚对应的寄存器,这一步就要连接一下,要把表里的0 通过CAT 连接成P0 ,好像也很简单:
#define PIN_PORT(num) CAT(P, UNPACK_FIRST(PIN(num)))
PIN_PORT(0)
CAT(P, UNPACK_FIRST(PIN(num)))
CAT(P, FIRST(0, 0))
CAT(P, 0)
P0
可惜并不会这么顺利,上面的展开结果实际上会是:
CAT(P, UNPACK_FIRST(PIN(num)))
PUNPACK_FIRST(PIN(num))
然后找不到符号PUNPACK_FIRST ,就无法继续展开下去了,参数PIN(num) 也不会被处理。问题很显然,CAT 直接把它的两个参数沾到一起了,并没有像期望的那样一层一层进去展开再插入、连接。这个问题和## 操作符的一条特殊规则有关:当宏函数体里面的参数紧邻## 操作符时,宏函数就不会先展开参数,而是直接插入参数。而回顾一下`CAT`` 的定义:
#define CAT(a, b) a ## b
两个参数正好都在## 旁边,所以CAT 函数完全不会对它的参数先处理,只会直接拼接进去,CAT3 也是一样的。定义CAT 代替直接使用## 操作符也就是这个原因,只要一眼看过去没有##,参数展开就会正常进行,相对来说把这条规则造成的不便隔离了一下。也就是说,调用CAT 前,所有参数必须已经展开了。那么就可以参照上一小节的经验,这样做:
#define SUPER_CAT(a, b) CAT(a, b)
就可以让SUPER_CAT 代为CAT 执行参数展开的工作。上面的PIN_PORT 也就能改成这样:
#define PIN_PORT(num) SUPER_CAT(P, UNPACK_FIRST(PIN(num)))
PIN_PORT(0)
SUPER_CAT(P, UNPACK_FIRST(PIN(num)))
SUPER_CAT(P, SECOND(0, 0))
SUPER_CAT(P, 0)
CAT(P, 0)
P0
有了这两个宏函数用来查表,之前的引脚操作宏就写成了这样~ 吗?
#define setpin(n) do { SUPER_CAT(PIN_PORT(n), PIN_BIT(n)) = 1; } while(0)
#define clrpin(n) do { SUPER_CAT(PIN_PORT(n), PIN_BIT(n)) = 0; } while(0)
#define getpin(n) ( SUPER_CAT(PIN_PORT(n), PIN_BIT(n)) )
当然是不行的,因为PIN_PORT 里也调用了SUPER_CAT ,这样就嵌套了。必须再在中间加一层,让PIN_PORT 先展开成寄存器名之后再送进引脚操作宏里。也就是要这么写:
#define SET_PIN(port, pin) do{ CAT(port, pin) = 1; } while(0)
#define CLR_PIN(port, pin) do{ CAT(port, pin) = 0; } while(0)
#define GET_PIN(port, pin) (CAT(port, pin))
#define setpin(num) SET_PIN(PIN_PORT(num), PIN_BIT(num))
#define clrpin(num) CLR_PIN(PIN_PORT(num), PIN_BIT(num))
#define getpin(num) GET_PIN(PIN_PORT(num), PIN_BIT(num))
setpin(0)
SET_PIN(PIN_PORT(0), PIN_BIT(0))
SET_PIN(SUPER_CAT(P, UNPACK_FIRST(PIN(0))), UNPACK_SECOND(PIN(0)))
SET_PIN(CAT(P, 0), 0)
SET_PIN(P0, 0)
do{ CAT(P0, 0) = 1; } while(0)
do{ P00 = 1; } while(0)
这样一来,因为PIN_PORT 作为参数展开的时候还没被插进宏函数体里,所以就没有CAT 的嵌套问题,CAT 开始展开的时候PIN_PORT 已经展开完成变成寄存器参数了。
虽然结果还是要用多种宏函数查表,但是很明显,经过一轮转手,看起来更加不明觉厉了。至于这些寄存器信息能拿来做什么,当然是更多花活儿。这部分演示代码参见附件 - 2。
操作引脚模式寄存器
STC15 单片机的IO 引脚有四种模式,分别是:
- 推挽输出,具有较强的电流输入/输出能力;
- 高阻输入,高阻态,不输出电平;
- 51模式,经典51 单片机的弱上拉准双向IO;
- 开漏模式,就是51 模式去掉了内部上拉,只能输出低电平;
每个IO 口对应两个寄存器用来设置每一个引脚的模式,比如P0 口的模式寄存器是P0M0 和P0M1,这两个寄存器都是不可位寻址的,只能使用位运算来置位或者置零。要设置P0.0 的模式,需要同时操作P0M0.0 和P0M1.0,通过这两位组合出四种模式,如下表:
所以,要提供统一的设置方法,可以使用上一节定义的映射表,先找到对应的模式寄存器,再根据引脚位置设置对应的寄存器位。先用类似上一节的方法,定义一个宏,用来查表并生成寄存器名称:
#define SUPER_CAT3(a, b, c)
#defne PIN_MODE_REG0(num) SUPER_CAT3(P, UNPACK_FIRST(PIN(num)), M0)
#defne PIN_MODE_REG1(num) SUPER_CAT3(P, UNPACK_FIRST(PIN(num)), M1)
要注意,两个PIN_MODE_REG 函数里面不能调用PIN_PORT 生成P0 然后再连接成 P0M0 或者P0M1 ,因为PIN_PORT 里也调用了SUPER_CAT ,最终都调用了CAT ,这样就会出现宏函数嵌套。
然后还需要对寄存器置位和置零的宏函数:
#define set_reg_bit(reg, bit_num) do { reg |= 0x01 << bit_num; } while(0)
#define clr_reg_bit(reg, bit_num) do { reg &= ~ (0x01 << bit_num); } while(0)
#define flip_reg_bit(reg, bit_num) do { reg ^= 0x01 << bit_num; } while(0)
这几条语句赋值右边的表达式实际使用时都是常量表达式,可以被编译器直接计算优化成一个常量,不会在运行时引入移位和取反运算。最后再来设置不同模式的宏函数,每个模式一个,一共四个,这里只演示设置推挽输出模式的宏:
#define set_pin_out(num) do { \
set_reg_bit(PIN_MODE_REG0(num), PIN_BIT(num)); \
clr_reg_bit(PIN_MODE_REG1(num), PIN_BIT(num)); \
} while(0)
缺陷和总结
首先就是之前提到的,定义常量当引脚编号用的时候不能加括号,可能比较反常识,容易导致失误。然后就是一旦编译出错,报错信息都会很难懂,必须有足够的了解才有可能判断出大概是哪儿的问题,也用不了调试器。而且就像上面提到过的,宏函数调用层次一多,最明显的问题就是可能不知道的时候就出现了嵌套,一旦出了问题又难定位,脑子里必须非常清楚每一个宏的效果和内容。所以一般还是不用查表的方法,用那个笨办法就行,相对来说宏的调用过程更清晰明确。
参考资料
- 宏魔法 C Pre-Processor Magic
本文关于宏的内容基本完全参照了上面这篇,加了点自己的实践总结和臆测。
附件 - 1
简单实现的演示代码,Keil C51 环境。
#include <reg51.h>
#include <intrins.h>
sbit P00 = P0^0;
sbit P01 = P0^1;
sbit P10 = P1^0;
void delay()
{
unsigned char i, j, k;
_nop_();
_nop_();
i = 10;
j = 153;
k = 245;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
#define PIN_0 P00
#define PIN_1 P01
#define PIN_2 P10
#define CAT(a, b) a ## b
#define PIN(n) CAT(PIN_, n)
#define setpin(n) do { PIN(n) = 1; } while(0)
#define clrpin(n) do { PIN(n) = 0; } while(0)
#define getpin(n) (PIN(n))
#define LED 0
int main(void) {
clrpin(LED);
while(1) {
if(getpin(LED))
clrpin(LED);
else
setpin(LED);
delay();
}
}
附件 - 2
查表演示代码,KEIL C51 环境。
#include <reg51.h>
#include <intrins.h>
sbit P00 = P0^0;
sbit P01 = P0^1;
sbit P10 = P1^0;
void delay()
{
unsigned char i, j, k;
_nop_();
_nop_();
i = 10;
j = 153;
k = 245;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
#define PIN_0 P00
#define PIN_1 P01
#define PIN_2 P10
#define CAT(a, b) a ## b
#define CAT3(a, b, c) a ## b ## c
#define SUPER_CAT(a, b) CAT(a, b)
#define FIRST(a, b) a
#define SECOND(a, b) b
#define PIN(num) CAT(PIN_, num)
#define PX(port, pin) CAT3(PX_, port, pin)
#define PIN_0 0, 0
#define PIN_1 0, 1
#define PIN_2 1, 0
#define UNPACK_FIRST(list) FIRST(list)
#define UNPACK_SECOND(list) SECOND(list)
#define PIN_PORT(num) SUPER_CAT(P, UNPACK_FIRST(PIN(num)))
#define PIN_BIT(num) UNPACK_SECOND(PIN(num))
#define SET_PIN(port, pin) do{ CAT(port, pin) = 1; } while(0)
#define CLR_PIN(port, pin) do{ CAT(port, pin) = 0; } while(0)
#define GET_PIN(port, pin) (CAT(port, pin))
#define setpin(num) SET_PIN(PIN_PORT(num), PIN_BIT(num))
#define clrpin(num) CLR_PIN(PIN_PORT(num), PIN_BIT(num))
#define getpin(num) GET_PIN(PIN_PORT(num), PIN_BIT(num))
#define LED 0
int main(void) {
clrpin(LED);
while(1) {
if(getpin(LED))
clrpin(LED);
else
setpin(LED);
delay();
}
}
|