如何改写C语言宏函数以省略冗余的FOO_DDR数组定义?
这确实是个挺巧妙的需求,但首先得明确一个关键限制:C预处理器在处理宏的时候,没法从运行时的数值(比如FOO[0]存的1)反推出它最初的宏标识符(PA1)。因为你的FOO数组里存储的是PA1展开后的数值,而不是PA1这个token本身,所以预处理器根本不知道这个1来自PA1,自然没法提取出其中的"A"字符。
不过我们可以换个思路,通过调整宏定义或者数据结构的方式,实现你想要的"自动推导DDR寄存器"的效果,下面给你几个可行的方案:
方案1:用预处理器Token拼接直接关联引脚与DDR(最简洁)
这个方案需要你重新定义引脚宏,让它们包含端口标识,然后通过预处理器的Token拼接直接生成DDR寄存器名:
// 先定义端口对应的DDR寄存器 #define DDRA _SFR_IO8(0X01) #define DDRB _SFR_IO8(0X02) // 示例其他端口 // 重新定义引脚宏,格式为 (端口标识, 引脚号) #define PA1 (A, 1) #define PA2 (A, 2) #define PB3 (B, 3) // 辅助宏:拆分引脚的端口和引脚号 #define _GET_PORT(port, pin) port #define _GET_PIN(port, pin) pin // 最终的setAsOutput宏 #define setAsOutput(pin) do { \ DDR##_GET_PORT(pin) |= (1 << _GET_PIN(pin)); \ } while(0) // 调用示例 setAsOutput(PA1); // 展开后:DDRA |= (1 << 1); setAsOutput(PB3); // 展开后:DDRB |= (1 << 3);
优点:完全不需要额外的数组或结构体,预处理器直接完成所有推导,代码简洁高效。
缺点:没法直接用数组批量处理引脚,如果需要批量操作,得用其他方式(比如宏循环)。
方案2:用结构体数组存储引脚信息(兼容批量操作)
如果你需要保留数组批量处理引脚的能力,可以用结构体来封装DDR寄存器指针和引脚号,这样既避免了冗余的DDR数组,又能运行时遍历:
#include <stdint.h> // 定义引脚结构体 typedef struct { volatile uint8_t *ddr_reg; // DDR寄存器指针 uint8_t pin_num; // 引脚号 } IO_Pin; // 原始的引脚和DDR定义 #define PA1 1 #define PA2 2 #define DDRA _SFR_IO8(0X01) // 定义引脚数组(现在只需要一个数组,DDR和引脚一一对应) IO_Pin FOO[] = { {&DDRA, PA1}, {&DDRA, PA2} }; // 改写setAsOutput宏 #define setAsOutput(pin_array, idx) do { \ *(pin_array[idx].ddr_reg) |= (1 << pin_array[idx].pin_num); \ } while(0) // 调用示例 setAsOutput(FOO, 0); // 展开后:*(&DDRA) |= (1 << PA1); 等价于DDRA |= (1 << 1);
优点:支持运行时批量遍历数组操作,逻辑清晰,容易维护。
缺点:定义数组时还是需要手动指定DDR寄存器,但比原来的两个数组要简洁得多。
方案3:在引脚值中嵌入端口标识(适合已有数组结构的改造)
如果不想修改现有的数组结构(数组里存的是引脚数值),可以在引脚宏中嵌入端口的标识位(比如用高2位),然后通过宏判断对应的DDR寄存器:
#include <stdint.h> // 定义端口标识(用高2位区分) #define PORT_A 0x00 #define PORT_B 0x40 // 重新定义引脚宏:把端口标识和引脚号合并成一个字节 #define PA1 (PORT_A | 1) #define PA2 (PORT_A | 2) #define PB3 (PORT_B | 3) // 定义DDR寄存器 #define DDRA _SFR_IO8(0X01) #define DDRB _SFR_IO8(0X02) // 辅助宏:从引脚值中提取DDR寄存器指针 #define GET_DDR(pin_val) \ ( (pin_val & 0xC0) == PORT_A ? &DDRA : \ (pin_val & 0xC0) == PORT_B ? &DDRB : NULL ) // 辅助宏:提取引脚号 #define GET_PIN(pin_val) (pin_val & 0x3F) // 改写setAsOutput宏 #define setAsOutput(bit, idx) do { \ volatile uint8_t *ddr = GET_DDR(bit[idx]); \ if (ddr != NULL) { \ *ddr |= (1 << GET_PIN(bit[idx])); \ } \ } while(0) // 原有的数组定义(现在存的是带端口标识的引脚值) uint8_t FOO[] = {PA1, PA2}; // 调用示例 setAsOutput(FOO, 0); // 展开后:&DDRA |= (1 << 1);
优点:可以保留原有的数组结构,不需要修改太多现有代码。
缺点:占用了引脚值的部分位(如果你的引脚号超过6位就不行了),而且需要额外的判断逻辑。
总结一下,如果你不需要批量操作引脚,方案1是最简洁高效的;如果需要批量处理,方案2的结构体数组是更稳妥的选择。
内容的提问来源于stack exchange,提问作者101

