5

我正在开发一个嵌入式项目,我正在尝试向一些代码添加更多结构,这些代码使用宏来优化对 USART 寄存器的访问。我想将预处理器#define 的寄存器地址组织成 const 结构。如果我将结构定义为宏中的复合文字并将它们传递给内联函数,则 gcc 已经足够聪明,可以绕过生成的程序集中的指针并直接在代码中硬编码结构成员值。例如:

C1:

struct uart {
   volatile uint8_t * ucsra, * ucsrb, *ucsrc, * udr;
   volitile uint16_t * ubrr;
};

#define M_UARTX(X)                  \
    ( (struct uart) {               \
        .ucsra = &UCSR##X##A,       \
        .ucsrb = &UCSR##X##B,       \
        .ucsrc = &UCSR##X##C,       \
        .ubrr  = &UBRR##X,          \
        .udr   = &UDR##X,           \
    } )


void inlined_func(const struct uart * p, other_args...) {
    ...
    (*p->ucsra) = 0;
    (*p->ucsrb) = 0;
    (*p->ucsrc) = 0;
}
...
int main(){
     ...
     inlined_func(&M_UART(0), other_parms...);
     ...
}

这里 UCSR0A, UCSR0B, &c 被定义为 uart 寄存器为 l 值,如

#define UCSR0A (*(uint8_t*)0xFFFF)

gcc 能够完全消除结构字面量,并且像 inlined_func() 中所示的所有赋值都直接写入寄存器地址,无需将寄存器地址读入机器寄存器,也无需间接寻址:

A1:

movb $0, UCSR0A
movb $0, UCSR0B
movb $0, UCSR0C

这会将值直接写入 USART 寄存器,无需将地址加载到机器寄存器中,因此根本不需要将结构字面量生成到目标文件中。struct 文字变成了编译时结构,生成的抽象代码没有成本。

我想摆脱宏的使用,并尝试使用标题中定义的静态常量结构:

C2:

#define M_UART0 M_UARTX(0)
#define M_UART1 M_UARTX(1)

static const struct uart * const uart[2] = { &M_UART0, &M_UART1 };
....
int main(){
     ...
     inlined_func(uart[0], other_parms...);
     ...
}

但是,gcc 不能在这里完全删除结构:

A2:

movl __compound_literal.0, %eax
movb $0, (%eax)
movl __compound_literal.0+4, %eax
movb $0, (%eax)
movl __compound_literal.0+8, %eax
movb $0, (%eax)

这会将寄存器地址加载到机器寄存器中,并使用间接寻址来写入寄存器。有谁知道无论如何我可以说服 gcc 为 C2 C 代码生成 A1 汇编代码?我尝试了 __restrict 修饰符的各种用法,但无济于事。

4

3 回答 3

2

经过多年使用 UART 和 USART 的经验,我得出以下结论:

不要将 astruct用于与 UART 寄存器的 1:1 映射。

编译器可以在struct您不知情的情况下在成员之间添加填充,从而弄乱 1:1 的对应关系。

写入 UART 寄存器最好直接或通过函数完成。

volatile请记住在定义指向寄存器的指针时使用修饰符。

使用汇编语言几乎没有性能提升

仅当通过处理器端口而不是内存映射访问 UART 时才应使用汇编语言。C 语言不支持端口。通过指针访问 UART 寄存器非常有效(生成汇编语言列表并验证)。有时,在汇编和验证中编码可能需要更多时间。

将 UART 功能隔离到一个单独的库中

这是一个很好的候选人。此外,一旦代码经过测试,就让它去吧。库不必一直(重新)编译。

于 2010-02-02T22:42:45.327 回答
1

在我的书中,使用“跨编译域”结构是一个大罪。基本上使用结构来指向某些东西,任何东西,文件数据,内存等。原因是它会失败,它不可靠,无论编译器如何。为此有许多编译器特定的标志和编译指示,更好的解决方案是不这样做。您想指向地址加 8,指向地址加 8,使用指针或数组。在这种特定情况下,我有太多的编译器也无法做到这一点,我编写了汇编程序 PUT32/GET32 PUT16/GET16 函数来保证编译器不会弄乱我的寄存器访问,比如结构,你有一天会被烧毁并且花时间弄清楚为什么你的 32 位寄存器只有 8 位写入它。跳转到函数的开销值得安心,代码的可靠性和可移植性。这也使您的代码非常便携,您可以将包装器放入 put 和 get 函数以跨网络,在 hdl 模拟器中运行您的硬件并进入模拟以读取/写入寄存器等,只需一段代码即可从模拟到嵌入式到操作系统设备驱动程序再到应用层功能不会改变。

于 2010-02-02T22:48:50.650 回答
0

根据寄存器集,您似乎正在使用 8 位 Atmel AVR 微控制器(或非常相似的东西)。我将向您展示一些我用于 Atmel 的 32 位 ARM MCU 的东西,这是他们在设备包中提供的略微修改的版本。

代码符号

我正在使用各种宏,我不打算在此处包含它们,但它们被定义为执行基本操作或将类型(如 UL)粘贴到数字上。对于不允许的情况(例如在汇编中),它们隐藏在宏中。是的,这些很容易被打破 - 程序员不要在脚上开枪:

#define _PPU(_V) (_V##U)   /* guarded with #if defined(__ASSEMBLY__) */
#define _BV(_V)  (_PPU(1) << _PPU(_V))   /* Variants for U, L, UL, etc */

还有特定长度寄存器的 typdef。例子:

/* Variants for 8, 16, 32-bit, RO, WO, & RW */
typedef volatile uint32_t rw_reg32_t; 
typedef volatile const uint32_t ro_reg32_t;

经典的#define 方法

您可以使用任何寄存器偏移量定义外设地址...

#define PORT_REG_ADDR           _PPUL(0x41008000)
#define PORT_ADDR_DIR           (PORT_REG_ADDR + _PPU(0x00))
#define PORT_ADDR_DIRCLR        (PORT_REG_ADDR + _PPU(0x04))
#define PORT_ADDR_DIRSET        (PORT_REG_ADDR + _PPU(0x08))
#define PORT_ADDR_DIRTGL        (PORT_REG_ADDR + _PPU(0x0C))

并取消引用指向寄存器地址的指针......

#define PORT_DIR        (*(rw_reg32_t *)PORT_ADDR_DIR)
#define PORT_DIRCLR     (*(rw_reg32_t *)PORT_ADDR_DIRCLR)
#define PORT_DIRSET     (*(rw_reg32_t *)PORT_ADDR_DIRSET)
#define PORT_DIRTGL     (*(rw_reg32_t *)PORT_ADDR_DIRTGL)

然后直接在寄存器中设置值:

PORT_DIRSET = _BV(0) | _BV(1) | _BV(2);

使用其他一些启动代码在 GCC 中编译...

arm-none-eabi-gcc -c -x c -mthumb -mlong-calls -mcpu=cortex-m4 -pipe
-std=c17 -O2 -Wall -Wextra -Wpedantic main.c

 [SIZE]    : Calculating size from ELF file

   text    data     bss     dec     hex
    924       0   49184   50108    c3bc

带拆卸:

00000000 <main>:
#include "defs/hw-v1.0.h"

void main (void) {

        PORT_DIRSET = _BV(0) | _BV(1) | _BV(2);
   0:   4b01            ldr     r3, [pc, #4]    ; (8 <main+0x8>)
   2:   2207            movs    r2, #7
   4:   601a            str     r2, [r3, #0]

}
   6:   4770            bx      lr
   8:   41008008        .word   0x41008008

“新”结构化方法

您仍然像以前一样定义一个基地址以及一些数字常量(例如一些实例),但是您不是定义单独的寄存器地址,而是创建一个对外围设备进行建模的结构。请注意,我在末尾手动包含一些保留空间以进行对齐。对于某些外围设备,在其他寄存器之间会有保留的块 - 这完全取决于该外围设备内存映射。

typedef struct PortGroup {
    rw_reg32_t  DIR;
    rw_reg32_t  DIRCLR;
    rw_reg32_t  DIRSET;
    rw_reg32_t  DIRTGL;
    rw_reg32_t  OUT;
    rw_reg32_t  OUTCLR;
    rw_reg32_t  OUTSET;
    rw_reg32_t  OUTTGL;
    ro_reg32_t  IN;
    rw_reg32_t  CTRL;
    wo_reg32_t  WRCONFIG;
    rw_reg32_t  EVCTRL;
    rw_reg8_t   PMUX[PORT_NUM_PMUX];
    rw_reg8_t   PINCFG[PORT_NUM_PINFCG];
    reserved8_t reserved[PORT_GROUP_RESERVED];
} PORT_group_t;

由于 PORT 外设有四个单元,并且 PortGroup 结构被打包以精确地建模内存映射,因此我可以创建一个包含所有这些单元的父结构。

typedef struct Port  {
    PORT_group_t    GROUP[PORT_NUM_GROUPS];
} PORT_t;

最后一步是将这个结构与地址相关联。

#define PORT    ((PORT_t *)PORT_REG_ADDR)

请注意,这仍然可以像以前一样取消引用 - 这是样式选择的问题。

#define PORT    (*(PORT_t *)PORT_REG_ADDR)

现在像以前一样设置寄存器值......

PORT->GROUP[0].DIRSET = _BV(0) | _BV(1) | _BV(2);

使用相同的选项编译(和链接),这会产生相同的大小信息和反汇编:

Disassembly of section .text.startup.main:

00000000 <main>:
#include "defs/hw-v1.0.h"

void main (void) {

        PORT->GROUP[0].DIRSET = _BV(0) | _BV(1) | _BV(2);
   0:   4b01            ldr     r3, [pc, #4]    ; (8 <main+0x8>)
   2:   2207            movs    r2, #7
   4:   609a            str     r2, [r3, #8]

}
   6:   4770            bx      lr
   8:   41008000        .word   0x41008000

可重用代码

第一种方法很简单,但如果您有多个外围设备,则需要大量手动定义和一些丑陋的宏。如果我们有 2 个不同的 PORT 外设在不同的地址(类似于具有多个 USART 的设备)。我们可以创建多个结构化的 PORT 指针:

#define PORT0   ((PORT_t *)PORT0_REG_ADDR)
#define PORT1   ((PORT_t *)PORT1_REG_ADDR)

单独调用它们看起来像您期望的那样:

PORT0->GROUP[0].DIRSET = _BV(0) | _BV(1) | _BV(2);
PORT1->GROUP[0].DIRSET = _BV(4) | _BV(5) | _BV(6);

编译结果:

 [SIZE]    : Calculating size from ELF file

   text    data     bss     dec     hex
    936       0   49184   50120    c3c8

Disassembly of section .text.startup.main:

00000000 <main>:
#include "defs/hw-v1.0.h"

void main (void) {

        PORT0->GROUP[0].DIRSET = _BV(0) | _BV(1) | _BV(2);
   0:   4903            ldr     r1, [pc, #12]   ; (10 <main+0x10>)

        PORT1->GROUP[0].DIRSET = _BV(4) | _BV(5) | _BV(6);
   2:   4b04            ldr     r3, [pc, #16]   ; (14 <main+0x14>)
        PORT0->GROUP[0].DIRSET = _BV(0) | _BV(1) | _BV(2);
   4:   2007            movs    r0, #7
        PORT1->GROUP[0].DIRSET = _BV(4) | _BV(5) | _BV(6);
   6:   2270            movs    r2, #112        ; 0x70
        PORT0->GROUP[0].DIRSET = _BV(0) | _BV(1) | _BV(2);
   8:   6088            str     r0, [r1, #8]
        PORT1->GROUP[0].DIRSET = _BV(4) | _BV(5) | _BV(6);
   a:   609a            str     r2, [r3, #8]

}
   c:   4770            bx      lr
   e:   bf00            nop
  10:   41008000        .word   0x41008000
  14:   4100a000        .word   0x4100a000

最后一步是让它全部可重用......

static PORT_t * const PORT[] = {PORT0, PORT1};

static inline void
PORT_setDir(const uint8_t unit, const uint8_t group, const uint32_t pins) {
    PORT[unit]->GROUP[group].DIRSET = pins;
}
/* ... */
PORT_setDir(0, 0, _BV(0) | _BV(1) | _BV(2));
PORT_setDir(1, 0, _BV(4) | _BV(5) | _BV(6));

并且编译将给出与以前相同的大小和(基本上)反汇编。

Disassembly of section .text.startup.main:

00000000 <main>:

static PORT_t * const PORT[] = {PORT0, PORT1};

static inline void
PORT_setDir(const uint8_t unit, const uint8_t group, const uint32_t pins) {
        PORT[unit]->GROUP[group].DIRSET = pins;
   0:   4903            ldr     r1, [pc, #12]   ; (10 <main+0x10>)
   2:   4b04            ldr     r3, [pc, #16]   ; (14 <main+0x14>)
   4:   2007            movs    r0, #7
   6:   2270            movs    r2, #112        ; 0x70
   8:   6088            str     r0, [r1, #8]
   a:   609a            str     r2, [r3, #8]
void main (void) {
        PORT_setDir(0, 0, _BV(0) | _BV(1) | _BV(2));
        PORT_setDir(1, 0, _BV(4) | _BV(5) | _BV(6));
}
   c:   4770            bx      lr
   e:   bf00            nop
  10:   41008000        .word   0x41008000
  14:   4100a000        .word   0x4100a000

我会用模块库头、枚举常量等来清理它。但这应该给某人一个起点。请注意,在这些示例中,我总是调用一个 CONSTANT 单元和组。我确切地知道我在写什么,我只想要可重用的代码。如果无法优化单元或组以编译时间常数,则(可能)需要更多指令。说到这一点,如果不使用优化,所有这一切都会消失。YMMV。

位域的旁注

Atmel 进一步将外围结构分解为单独的 typedef 寄存器,这些寄存器在与寄存器大小的联合中命名了位域。这是 ARM CMSIS 方式,但它不是很好的 IMO。与其他一些答案中的信息相反,我确切地知道编译器将如何打包此结构;但是,我不知道如果不使用特殊的编译器属性和标志,它将如何排列位域。我宁愿显式设置和屏蔽定义的寄存器位字段常量值。如果您对此感到担心,它也违反了 MISRA(就像我在这里所做的一些事情一样......)。

于 2022-02-14T23:09:40.550 回答