用于 I/O 引脚抽象的 C++ 类

电器工程 微控制器 引脚 C++ io
2022-02-03 02:04:34

我正在寻找硬件 I/O 点或引脚的 C++ 抽象。诸如 in_pin、out_pin、inout_pin、open_collector_pin 之类的东西。

我自己当然可以想出这样一组抽象,所以我不是在寻找“嘿,你可能会这样做”类型的答案,而是“看看这个和这个中使用的这个库,以及这个项目'。

谷歌没有发现任何东西,也许是因为我不知道其他人会怎么称呼它。

我的目标是构建基于这些点的 I/O 库,但也提供这些点,因此很容易将 HD44780 LCd 连接到芯片的 IO 引脚或 I2C(或 SPI) I/O 扩展器,或任何其他可以以某种方式控制的点,而不会对 LCD 类进行任何更改。

我知道这是在电子/软件边缘,如果它不属于这里,对不起。

@leon:接线那是一大包软件,我需要仔细看看。但似乎他们没有像我想要的那样使用 pin 抽象。例如在我看到的键盘实现中

digitalWrite(columnPins[c], LOW);   // Activate the current column.

这意味着有一个函数 (digitalWrite) 知道如何写入 I/O 引脚。这使得在不重写 digitalWrite 函数的情况下无法添加新类型的 I/O 引脚(例如 MCP23017 上的引脚,因此必须通过 I2C 写入)。

@Oli:我用谷歌搜索了一个 Arduino IO 示例,但似乎使用与 Wiring 库相同的方法:

int ledPin = 13;                 // LED connected to digital pin 13
void setup(){
    pinMode(ledPin, OUTPUT);      // sets the digital pin as output
}
4个回答

请允许我无耻地插入我的开源项目https://Kvasir.ioKvasir::Io 部分提供引脚操作功能。您必须首先使用 Kvasir::Io::PinLocation 定义您的 pin,如下所示:

constexpr PinLocation<0,4> led1;    //port 0 pin 4
constexpr PinLOcation<0,8> led2;

请注意,这实际上并不使用 RAM,因为这些是 constexpr 变量。

在整个代码中,您可以在诸如 makeOpenDrain、set、clear、makeOutput 等“动作工厂”函数中使用这些引脚位置。“动作工厂”实际上并不执行动作,而是返回一个可以使用 Kvasir::Register::apply() 执行的 Kvasir::Register::Action。这样做的原因是当它们作用于同一个寄存器时,apply() 会合并传递给它的动作,因此可以提高效率。

apply(makeOutput(led1),
    makeOutput(led2),
    makeOpenDrain(led1),
    makeOpenDrain(led2));

由于动作的创建和合并是在编译时完成的,这应该产生与典型的手工编码等效的汇编代码:

PORT0DIR |= (1<<4) | (1<<8);
PORT0OD |= (1<<4) | (1<<8);

简短的回答:可悲的是,没有图书馆可以做你想做的事。我自己做过很多次,但总是在非开源项目中。我正在考虑在 github 上放一些东西,但我不确定什么时候可以。

为什么选择 C++?

  1. 编译器可以免费使用动态字长表达式评估。C 传播到 int。您的字节掩码/移位可以更快/更小完成。
  2. 内联。
  3. 模板化操作使您可以改变字长和其他属性,并具有类型安全性。

Wiring 项目使用这样的抽象:

http://wiring.org.co/

并且编译器是用 C++ 编写的。您应该在源代码中找到大量示例。Arduino 软件基于 Wiring。

在 C++ 中,可以编写一个类,以便您可以像使用变量一样使用 I/O 端口,例如

  端口B = 0x12;/* 写入 8 位端口 */
  如果(RB3)LATB4 = 1;/* 读取一个 I/O 位并有条件地写入另一个 */

不考虑底层实现。例如,如果使用不支持位级操作但支持字节级寄存器操作的硬件平台,则可以(可能借助某些宏)定义具有内联读写功能的静态类 IO_PORTS名为 bbRB3 和 bbLATB4 的属性,这样上面的最后一条语句就会变成

  如果(IO_PORTS.bbRB3)IO_PORTS.bbLATB4 = 1;

这又会被处理成类似的东西:

  if (!!(PORTB & 8)) (1 ? (PORTB |= 16) : (PORTB &= ~16));

编译器应该能够注意到 ?: 运算符中的常量表达式并简单地包含“true”部分。通过将宏扩展为以下内容,可以减少创建的属性数量:

  如果(IO_PORTS.ppPORTB[3])IO_PORTS.ppPORTB[4] = 1;

要么

  if (IO_PORTS.bb(addrPORTB,3)) IO_PORTS.bbPORTB(addrPORTB,4) = 1;

但我不确定编译器是否能够很好地内联代码。

我绝不希望暗示将 I/O 端口当作变量来使用一定是个好主意,但既然你提到了 C++,知道它是一个有用的技巧。我自己在 C 或 C++ 中的偏好,如果不需要与使用上述样式的代码兼容,可能会为每个 I/O 位定义某种类型的宏,然后为“readBit”、“writeBit”定义宏, “setBit”和“clearBit”,附带条件是传递给这些宏的位标识参数必须是用于此类宏的 I/O 端口的名称。例如,上面的例子可以写成

  if (readBit(RB3)) setBit(LATB4);

并翻译为

  如果 (!!(_PORT_RB3 & _BITMASK_RB3)) _PORT_LATB4 |= _BITMASK_LATB4;

与 C++ 风格相比,预处理器的工作量会多一些,但编译器的工作量会少一些。它还将允许为许多 I/O 实现优化代码生成,并为几乎所有实现体面的代码实现。