根据寄存器集,您似乎正在使用 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(就像我在这里所做的一些事情一样......)。