在嵌入式系统开发中,操作外设控制器是常见的任务。虽然通常用更具表现力的符号替换数字常量,但这种做法并没有解决代码移植或更换外设控制器时的注册映射不一致的主要问题。即使是同一厂商的不同设备,不同控制器之间的映射也很少一致。
近年来,出现了更复杂的硬件编程接口,例如Atmel软件框架(ASF)、Cortex微控制器软件接口标准(CMSIS)或自由软件libopencm3。尽管这些工具在某些方面提供了便利,但它们仍然过于接近寄存器,且过于冗长。
“Arduino语言”虽然提供了一种一致的硬件抽象接口,但这是以降低性能、增加程序内存消耗和需要C++编译器为代价的。
这些问题促使尝试创建一个硬件编程工具,目标如下:
HWA通过为程序员提供一组通用指令来实现这些目标,这些指令设计用于应用于代表目标设备中嵌入的外设控制器的各种类型的对象。这被称为特定多态性。
与其他硬件抽象工具的关键区别在于,HWA不是一个库:它使用标准C99变长宏、C结构和C内联函数实现特定多态性机制,这些函数会被编译器的优化器丢弃。
让从微控制器程序员的“Hello World!”开始:使LED闪烁。这通常是这样写的(在网上的某个地方借用了这段代码,用于Atmel AVR,希望作者不会起诉!):
C++
#define F_CPU 1000000UL
#include
#include
int main(void) {
DDRB |= _BV(DDB0);
while(1) {
PORTB ^= _BV(PB0);
_delay_ms(500);
}
}
现在使用HWA:
C++
// 设备由其内部RC振荡器时钟驱动。
// 设备配置必须在加载HWA之前声明,因为它将使用它来计算保险丝字节的值和系统时钟频率(hw_syshz)。
// 注意:HWA假设未定义配置参数的出厂默认值。这将产生相同的结果,在当前情况下,结果是hw_syshz = 1,000,000 Hz。
#define HW_DEVICE_CLK_SRC rc_8MHz
#define HW_DEVICE_CLK_PSC 8
// 为这个设备加载HWA。
// 使用完整的设备名称,以便HWA知道包装和引脚编号。
#include
// 连接LED的引脚
#define PIN_LED hw_pin_5 // 在ATtiny85 PU上是'hw_pin_pb0'
int main() {
hw_config(PIN_LED, direction, output);
while(1) {
hw_toggle(PIN_LED);
hw_delay_cycles(0.5 * hw_syshz);
// 0.5秒延迟
}
}
没有寄存器名称,没有晦涩的符号。喜欢 hw_toggle(PIN_LED)
还是 PORTB ^= _BV(PB0)
,就像原始代码那样,顺便说一下,它应该是 PINB = _BV(PB0)
吗?
现在让使用中断使LED闪烁,并将设备置于睡眠模式:
C++
// 目标设备
#include
// 连接LED的引脚
#define PIN_LED hw_pin_5
// 闪烁周期(以秒为单位)
#define PERIOD 0.5
/*
计数器及其时钟预分频因子
*/
#define COUNTER hw_counter0
#define CLKDIV 64
/*
处理计数器溢出IRQ
*/
HW_ISR(COUNTER, overflow) {
static uint8_t n;
n++;
if (n >= (uint8_t)(0.5 + PERIOD / 0.001 / 2)) {
n = 0;
hw_toggle(PIN_LED);
}
}
int main() {
/*
创建一个HWA上下文以收集硬件配置。
* 预加载此上下文以重置值。
*/
hwa_begin_from_reset();
/*
配置LED引脚。
*/
hwa_config(PIN_LED, direction, output);
/*
当执行'sleep'指令时,使CPU进入空闲模式。
*/
hwa_config(hw_core0,
sleep, enabled,
sleep_mode, idle);
/*
配置计数器每0.001秒溢出一次。
*
* 计数器的比较单元 `compare0` (OCRxA) 用于存储最高值。
* 除非另有说明,否则溢出将自动设置为发生
* 当'count'值等于'top'值时,在'loop_up'计数模式下。
*/
hwa_config(COUNTER,
clock, prescaler_output(CLKDIV),
countmode, loop_up,
bottom, 0,
top, compare0);
/*
设置计数器的TOP值。
*/
hwa_write(hw_rel(COUNTER, compare0), 0.001 * hw_syshz / CLKDIV);
/*
启用溢出IRQ。
*/
hwa_turn_irq(COUNTER, overflow, on);
/*
将此配置写入硬件。
*/
hwa_commit();
hw_enable_interrupts();
/*
在中断之间保持设备处于睡眠模式。
*/
for(;;)
hw_sleep();
}
这表明:
hwa_config()
是一个通用指令(用于配置计数器、设备核心或I/O引脚),它接受一个可变长度的键/值对列表,紧随对象名称之后。hw_
或 hwa_
开头。没有显示的是:
hw_counter1
, hw_counter2
... 当然,前提是设备有的话),完全相同的代码可以编译;第二个示例中的指令以 hwa_
开头。这些指令与以 hw_
开头的指令等效,但它们不会立即对硬件起作用:它们只将信息存储在称为HWA上下文的隐藏结构中,直到遇到 hwa_commit()
指令。
hwa_commit()
指令计算存储在上下文中的所有信息,以确定必须写入硬件寄存器的值(或者告诉程序员他想要的配置无法获得),并有效地写入它们。
在Atmel AVR设备上使用HWA上下文是必要的,以获得尽可能好的二进制代码,因为它最小化了对硬件寄存器的访问,这些寄存器被多个控制器共享,其中一个控制器的配置有时与一些相关外设的配置相关联。一个典型的例子是定时器/计数器。HWA通过多个对象实现这些定时器/计数器:
所有这些对象都被称为“相关”的,因为它们之间存在关系,一个的配置可能会影响另一个的配置。典型的是,计数器的 WGM
位的值受到比较输出配置的影响。
hw_rel()
指令给出了另一个对象的相对对象的名称(或产生错误)。这也有助于编写使外设交换更容易的代码。
HWA托管在Github上。尽管HWA现在支持三种不同的Atmel AVR设备,可以编译大约20个不同的示例,但在有足够的用户测试它并告诉是否可以宣布为“稳定”之前,它应该被认为是“进行中”,至少关于Atmel AVR系列。