用嵌入式寄存器结构中的位移代替位域


荧惑
2025-04-03 11:53:54 (4天前)


我试图在嵌入式应用程序中为外设编写驱动程序方面有点兴趣。

当然,读取和写入预定义的内存映射区域是一项常见的任务,所以我试着……

2 条回复
  1. 0# 林老爷的日常 | 2019-08-31 10-32



    C标准没有规定一个位域序列占用多少内存或者位域的顺序。在你的例子中,一些编译器可能决定使用32位用于位域,即使你明确地期望它覆盖16位。因此,使用位字段可以将您锁定到特定的编译器和特定的编译标志。



    使用大于的类型

    unsigned char

    也有实现定义的效果,但在实践中它更便携。在现实世界中,只有两种选择

    uintNN_t

    :big-endian或little-endian,通常对于给定的CPU,每个人都使用相同的顺序,因为这是CPU本机使用的顺序。 (某些体系结构,如mips和arm支持两种端点,但通常人们会在各种CPU模型中坚持使用一个字节序。)如果您正在访问CPU自己的寄存器,那么它的字节顺序可能也是CPU的一部分。另一方面,如果您正在访问外围设备,则需要注意。



    您正在访问的设备的文档将告诉您一次寻址的内存单元有多大(显示示例中为2个字节)以及这些位的排列方式。例如,它可能表明该寄存器是一个16位寄存器,使用16位加载/存储指令访问,无论CPU的字节顺序是什么,

    data1

    包含5个低位,

    data2

    包括接下来的3,

    data3

    接下来的4和

    data4

    接下来4.在这种情况下,您将寄存器声明为

    uint16_t





    1. typedef volatile uint16_t data_port_t;
      data_port_t *port = GET_DATA_PORT_ADDRESS();

    2. </code>


    设备中的内存地址几乎总是需要声明

    volatile

    ,因为编译器在适当的时间读取和写入它们很重要。



    要访问寄存器的各部分,请使用位移和位掩码运算符。例如:



    1.   #define DATA2_WIDTH 3
    2. define DATA2_OFFSET 5

      define DATA2_MAX (((uint16_t)1 << DATA2_WIDTH) - 1) // in binary: 0000000000000111

      define DATA2_MASK (DATA2_MAX << DATA2_OFFSET) // in binary: 0000000011100000

      void set_data2(data_port_t port, unsigned new_field_value)
      {
      assert(new_field_value <= DATA2_MAX);
      uint16_t old_register_value =
      port;
      // First, mask out the data2 bits from the current register value.
      uint16_t new_register_value = (old_register_value & ~DATA2_MASK);
      // Then mask in the new value for data2.
      new_register_value |= (new_field_value << DATA2_OFFSET);
      *port = new_register_value;
      }

    3. </code>


    显然,你可以缩短代码。我把它分成了各个小步骤,这样逻辑应该很容易理解。我在下面加了一个较短的版本。除非在非优化模式下,任何有价值的编译器都应编译为相同的代码。注意上面,我使用了一个中间变量,而不是做两个赋值

    port

    因为要做两个作业
    port

    会改变行为:它会导致设备看到中间值(和另一个读取,因为

    |=

    既是读又写)。这是较短的版本和读取功能:




    1. void set_data2(data_port_t port, unsigned new_field_value)
      {
      assert(new_field_value <= DATA2_MAX);
    2. port = (port & ~(((uint16_t)1 << DATA2_WIDTH) - 1) << DATA2_OFFSET))
      | (new_field_value << DATA2_OFFSET);
      }
      unsigned get_data2(data_port
      port)
      {
      return (*port >> DATA2_OFFSET) & DATA2_MASK;
      }

    3. </code>





    1.     #define CAN0 (*(CAN_REG_FILE *)CAN_BASE_ADDRESS)
    2.   </code>
    3. </pre>


    这里没有功能。函数声明将具有返回类型,后跟括号中的参数列表。这取了价值

    CAN_BASE_ADDRESS

    ,可能是某种类型的指针,然后将指针强制转换为指针

    CAN_REG_FILE

    ,最后取消引用指针。换句话说,它访问由给定地址的CAN寄存器文件

    CAN_BASE_ADDRESS

    。例如,可能有类似的声明




    1. void CAN_BASE_ADDRESS = (void)0x12345678;
      typedef struct {
      const volatile uint32_t status;
      volatile uint16_t foo;
      volatile uint16_t bar;
      } CAN_REG_FILE;

    2. define CAN0 ((CAN_REG_FILE )CAN_BASE_ADDRESS)

      </code>


    然后你可以做的事情




    1. CAN0.foo = 42;
      printf(“CAN0 status: %d\n”, (int)CAN0.status);

    2. </code>

登录 后才能参与评论