如何从 C 中的现有变量创建位域

How to create bitfield out of existing variables in C

我正在 CodeWarrior V10.6 中的摩托罗拉 HCS08 µCU 上工作,我正在尝试创建一个 extern 位域,其中包含来自现有寄存器的位。在 µCU header 中创建位域的方式类似于

typedef unsigned char byte;
typedef union {
  byte Byte;
  struct {
    byte PTAD0       :1;
    byte PTAD1       :1;                                     
    byte PTAD2       :1;                                     
    byte PTAD3       :1;                                     
    byte PTAD4       :1;                                     
    byte PTAD5       :1;                                     
    byte PTAD6       :1;                                     
    byte PTAD7       :1;                                     
  } Bits;
} PTADSTR;
extern volatile PTADSTR _PTAD @0x00000000;
#define PTAD                            _PTAD.Byte
#define PTAD_PTAD0                      _PTAD.Bits.PTAD0
#define PTAD_PTAD1                      _PTAD.Bits.PTAD1
#define PTAD_PTAD2                      _PTAD.Bits.PTAD2
#define PTAD_PTAD3                      _PTAD.Bits.PTAD3
#define PTAD_PTAD4                      _PTAD.Bits.PTAD4
#define PTAD_PTAD5                      _PTAD.Bits.PTAD5
#define PTAD_PTAD6                      _PTAD.Bits.PTAD6
#define PTAD_PTAD7                      _PTAD.Bits.PTAD7

这将使寄存器值通过 PTAD = 0x01PTAD_PTAD0 = 1 更改。这个定义对于PTAD, PTBD, PTCD, ... PTGD基本相同,唯一改变的是地址。

我尝试从以前的现有变量中创建自定义位域是

typedef union {
  byte Byte;
  struct {
    byte *DB0;
    byte *DB1;
    byte *DB2;
    byte *DB3;
    byte *DB4;
    byte *DB5;
    byte *DB6;
    byte *DB7;
  } Bits;
} LCDDSTR;

我会创建位域并将其初始化为 LCDDSTR lcd = {{&PTGD_PTGD6, &PTBD_PTBD5, ...}},因为由于某种原因,How to initialize a struct in accordance with C programming language standards 中的初始化建议如 LCDSTR lcd = {*.Bits.DB0 = &PTGD_PTGD6, *.Bits.DB1 = &PTBD_PTBD5, ...}(将其视为结构,请再次纠正我) ] 不适用于此编译器(它确实适用于在线编译器)。

但是,如您所见,我正在对这些位进行分组,并且(如果可行的话)我将能够通过执行 *lcd.Bits.DB0 = 1 或类似操作来更改实际寄存器的值,但是如果我这样做 lcd.Byte = 0x00,我会更改包含在 lcd.Bits.DB0 中的内存地址的最后一个(我认为)字节,因为结构实际上并不包含数据,但是取而代之的是指针。

我将如何继续实现能够包含和修改来自多个寄存器的位的结构? (我想这里的问题是在内存中这些位不是一个挨着另一个,我想这会更容易)。有可能吗?希望是。

How would I go on achieving a struct that is able to contain and modify bits from several registers? (I guess the problem here is that in memory the bits are not one next to the other..

我不认为你可以用结构来做到这一点。这是因为根据定义,位域必须占用相同或连续的地址。

但是宏在这里可能会有用

#define DB0  PTGD_PTGD6
#define DB1  PTBD_PTBD5
....

要将这些位清除为全 0 或设置为全 1,您可以使用多行宏

#define SET_DB(x) do { \
    PTGD_PTGD6 = x;    \
    PTBD_PTBD5 = x;    \
    ......             \
} while(0) 

How would I go on achieving a struct that is able to contain and modify bits from several registers?

你不能。

一个结构必须代表一个单一的、连续的内存块——否则,像获取 sizeof 结构或对指向一个的指针执行操作这样的操作将毫无意义。

如果你想置换一个值的位,你需要找到一些明确的方法。如果您的位顺序相对简单,则可以通过一些位运算来实现;如果更奇怪,您可能需要使用查找 table.

除此之外:C 中的位域非常有限。该语言并没有对包含位域的结构最终如何在内存中布局做出很多保证;对于 portable 代码,通常最好避免使用它们。 (这在此处不适用,因为您正在为特定 compiler/microcontroller 组合编写代码,但总体而言值得牢记。)

不幸的是,您的并集没有任何意义,因为它形成了一个 byte 和 8 个 byte* 的并集。由于 HCS08 上的指针是 16 位的,因此最终会变成 8*2 = 16 字节的数据,无法以任何有意义的方式使用。

  • 请注意,名为 bit-fields 的 C 结构在标准中的定义很差,因此在任何程序中都应避免使用。 See this.
  • 请注意,Codewarrior 寄存器映射并不完全符合 C 标准(也不 MISRA-C)。
  • 请注意,对于硬件寄存器映射来说,结构通常是有问题的,因为结构可以包含填充。你在 HCS08 上没有这个问题,因为它不需要对齐数据。但大多数 MCU 确实需要这样做。

因此,如果可以的话,最好在标准 C 中推出您自己的寄存器映射。端口 A 数据寄存器可以简单地定义如下:

#define PTAD    (*(volatile uint8_t*)0x0000U)
#define PTAD7   (1U << 7)
#define PTAD6   (1U << 6)
#define PTAD5   (1U << 5)
#define PTAD4   (1U << 4)
#define PTAD3   (1U << 3)
#define PTAD2   (1U << 2)
#define PTAD1   (1U << 1)
#define PTAD0   (1U << 0)

正如我们所知,无论如何定义位掩码基本上都是多余的,因为 PTAD |= 1 << 7;PTAD |= PTAD7; 同样可读。这是因为这是一个纯粹的 I/O 端口。另一方面,为状态和控制寄存器定义文本位掩码可显着提高代码的可读性。


如果你想修改多个寄存器的位,你会做类似下面的事情:

假设我们有一个 RGB (red-green-blue) LED,共阴极,3 种颜色连接到 3 个不同端口上的 3 个不同引脚。你可以这样做,而不是殴打 PCB 设计师:

#define RGB_RED_PTD     PTAD
#define RGB_RED_PTDD    PTADD
...
#define RGB_BLUE_PTD    PTBD
#define RGB_BLUE_PTDD   PTBDD
...
#define RGB_GREEN_PTD   PTDD
#define RGB_GREEN PTDD  PTDDD

#define RGB_RED_PIN    1
#define RGB_BLUE_PIN   5
#define RGB_GREEN_PIN  3

您现在可以独立于它们在硬件上的位置进行设置:

void rgb_init (void)
{
  RGB_RED_PTDD   |= (1 << RGB_RED_PIN);
  RGB_BLUE_PTDD  |= (1 << RGB_BLUE_PIN);
  RGB_GREEN_PTDD |= (1 << RGB_GREEN_PIN);
}

void rgb_yellow (void)
{
  RGB_RED_PTD    |=  (1 << RGB_RED_PIN);
  RGB_BLUE_PTD   &= ~(1 << RGB_BLUE_PIN);
  RGB_GREEN_PTD  |=  (1 << RGB_GREEN_PIN);
}

等等。示例适用于 HCS08,但同样可以在具有直接端口 I/O.

的任何 MCU 上普遍使用

听起来像下面这样的方法符合您想要解决的问题。

我没有测试过这个,因为我没有硬件,但是这应该提供一个替代方案来查看。

这假设您想要打开特定的引脚或关闭特定的引脚,但不会出现您想要在单个操作中为特定设备打开某些引脚并关闭其他引脚的情况。如果是这种情况,我会考虑将 RegPinNo 的类型设为无符号短整数,以便为每个 register/pin 数字组合包含一个操作码。

这还假设操作的时间不是关键约束,并且硬件具有足够的马力,因此小循环不会对吞吐量和占用 CPU 其他事情所需的时间造成太大负担。因此,如果考虑到这一点,此代码可能需要更改以改进优化。

我假设您需要某种易于阅读的方式来表达将打开和关闭分散在多个内存区域的一系列位的命令。

第一件事是想出这样一个命令的表示形式,在我看来,从 char 数组中借用来表示字符串就足够了。

typedef byte RegPinNo;    // upper nibble indicates register number 0 - 7, lower nibble indicates pin number 0 - 7

const byte REGPINNOEOS = 0xff;   // the end of string for a RegPinNo array.

这些将用于定义 register/pin 个数字的数组,如下所示。

RegPinNo myLed[] = { 0x01, 0x12, REGPINNOEOS };  // LED is addressed through Register 0, Pin 0 and Register 1, Pin 1 (zero based)

所以在这一点上我们有一种方法来描述一个特定的设备,在这种情况下是一个 LED,是通过一系列 register/pin 数字项来寻址的。

接下来让我们创建一个小型函数库,该函数库将使用此表示通过遍历此 register/pin 数字数组并对其执行操作(例如设置位)来实际修改特定寄存器中的特定引脚寄存器或清除寄存器中的位。

typedef unsigned char byte;
typedef union {
    byte Byte;
    struct {
        byte PTAD0 : 1;
        byte PTAD1 : 1;
        byte PTAD2 : 1;
        byte PTAD3 : 1;
        byte PTAD4 : 1;
        byte PTAD5 : 1;
        byte PTAD6 : 1;
        byte PTAD7 : 1;
    } Bits;
} PTADSTR;

// Define a pointer to the beginning of the register area. This area is composed of
// 8 different registers each of which is one byte in size.
// We will address these registers as Register 0, Register 1, ... Register 7 which just happens
// to be how C does its zero based indexing.
// The bits representing pins on the PCB we will address as Pin 0, Pin 1, ... Pin 7.
extern volatile PTADSTR (* const _PTAD)  = 0x00000000;

void SetRegPins(RegPinNo *x)
{
    byte pins[] = { 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 };
    int i;
    for (i = 0; x[i] != REGPINNOEOS; i++) {
        byte bRegNo = (x[i] >> 4) & 0x07;  // get the register number, 0 - 7
        byte bPinNo = x[i] & 0x07;         // get the pin number, 0 - 7
        _PTAD[bRegNo].Byte |= pins[bPinNo];
    }
}

void ClearRegPins(RegPinNo *x)
{
    byte pins[] = { 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 };
    int i;
    for (i = 0; x[i] != REGPINNOEOS; i++) {
        byte bRegNo = (x[i] >> 4) & 0x07;  // get the register number, 0 - 7
        byte bPinNo = x[i] & 0x07;         // get the pin number, 0 - 7
        _PTAD[bRegNo].Byte &= ~pins[bPinNo];
    }
}

void ToggleRegPins(RegPinNo *x)
{
    byte pins[] = { 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 };
    int i;
    for (i = 0; x[i] != REGPINNOEOS; i++) {
        byte bRegNo = (x[i] >> 4) & 0x07;  // get the register number, 0 - 7
        byte bPinNo = x[i] & 0x07;         // get the pin number, 0 - 7
        _PTAD[bRegNo].Byte ^= pins[bPinNo];
    }
}

您可以像下面这样使用上面的内容。不确定时间延迟函数在您的环境中会是什么样子,所以我使用了一个函数 Sleep(),它接受关于延迟或休眠的毫秒数的参数。

void LightLed (int nMilliSeconds)
{
    RegPinNo myLed[] = { 0x01, 0x12, REGPINNOEOS };  // LED is addressed through Register 0, Pin 0 and Register 1, Pin 1 (zero based)

    SetRegPins(myLed);    // turn on the LED
    Sleep(nMilliSeconds); // delay for a time with the LED lit
    ClearRegPins(myLed);  // turn the LED back off
}

编辑 - 优化

允许同时在特定寄存器中设置多个引脚的更有效实现是将 RegPinNo 的使用定义为无符号短整型,高位字节是寄存器数字和低字节是作为字节位掩码操作的引脚。

通过这种方法,您将得到一个如下所示的 SetRegPins() 函数。其他功能也需要进行类似的更改。

void SetRegPins(RegPinNo *x)
{
    int i;
    for (i = 0; x[i] != REGPINNOEOS; i++) {
        byte bRegNo = (x[i] >> 8) & 0x07;  // get the register number, 0 - 7
        byte bPinNo = x[i] & 0xFF;         // get the pin mask
        _PTAD[bRegNo].Byte |= bPinNo;
    }
}

typedef 看起来像:

typedef unsigned short RegPinNo;    // upper byte indicates register number 0 - 7, lower byte provides pin mask

const byte REGPINNOEOS = 0xffff;   // the end of string for a RegPinNo array.

这些元素的使用方式如下:

void LightLed (int nMilliSeconds)
{
    RegPinNo myLed[] = { 0x0002, 0x0103, REGPINNOEOS };  // LED is addressed through Register 0, Pin 1 and Register 1, Pin 0 and Pin 1 (zero based)

    SetRegPins(myLed);    // turn on the LED
    Sleep(nMilliSeconds); // delay for a time with the LED lit
    ClearRegPins(myLed);  // turn the LED back off
}