AVR芯片编程时如何定义内存指针?

Posted

技术标签:

【中文标题】AVR芯片编程时如何定义内存指针?【英文标题】:How to define memory pointers when programming AVR chips? 【发布时间】:2020-08-26 01:14:10 【问题描述】:

前言:作为应用程序开发人员工作了几年后,软件工程的世界变得比以前更加晦涩难懂。原因是真正的东西隐藏在无数层抽象之下:操作系统、框架等。年轻一代被剥夺了使用类似 PDP 的机器工作的乐趣,所有编程都是通过电气开关切换完成的。另一个问题是现代编程语言的短暂性。曾经有 Python 2.x,现在它已被弃用,而 Python 3.x 又将在几个月内被弃用。其他语言的同上。 ANSI C 看起来像胡夫金字塔:它在 70 年代就在那里,我不怀疑它会在太阳变成红矮星之后在那里。

现在看来,了解硬件和软件之间交互的唯一方法就是玩嵌入式开发。从教学的角度来看,物理芯片非常方便,因为它们可以解决 C 语言中最困难的部分,即指针。在 OS 环境中编码时,*/& 表示法仍然非常混乱,因为它指的是虚拟内存中某处的某个位置。在你了解什么是虚拟内存之前,你必须阅读一些关于操作系统开发等的专着。你可能会觉得这很愚蠢,但我真的很想知道现在哪个晶体管占据了我的位置。至少,我可以将物理引脚电压连接到编程抽象。

目前我正在使用 Atmel 芯片和 WinAVR 包,因为有大量的教科书和可访问的硬件。尽管所有书籍都承诺使用纯 C 来教授 AVR 编码,但现实情况是所有指针都隐藏在 PORTA、DDRB 等宏后面。所有代码示例都包含头文件“io.h”,它又引用了其他特定的头文件对于像“iomx8.h”这样的给定芯片。到目前为止,我在这些标题中找不到任何宏定义。增加 Atmega168 上物理引脚 14 电压的代码如下所示

DDRB = 0x01;
PORTB = 0x01;

幸运的是,Microchip 网站提供了一些基本文档,其中说明了,例如,如果我想提高物理引脚 14 上的电压,我需要按照以下步骤操作:

unsigned char *ddrB;
ddrB = (unsigned char*)0x24; // the address of ddrB is 0x24
*ddrB |= 0x01; // set up low impedance/ high current state for the transistor 0 

unsigned char *portB;
portB = (unsigned char*)0x25;
*portB |= 0x01; // voltage on
*portB &= ~(0x01); // voltage off

不幸的是,这是我在潜伏一周后得到的唯一信息。现在我正在进行 USART 编程,所有这些 UBRR0H、UCSR0C 使事情变得更加复杂。由于提供的头文件不包含任何寄存器的宏定义,我还能在哪里找到它?

几年前有人问过类似的问题:accessing AVR registers with C?。然而,提供的答案有些无用,除了 GCC 本身可以将一些神话 PORTB 映射到真实物理位置的线索。有人能描述一下映射背后的机制吗?

【问题讨论】:

你的程序是哪个AVR? "由于提供的头文件不包含任何寄存器的宏定义,我还能在哪里找到它?"你检查过数据表吗? 指向 SFR 的指针应声明为指向 volatile 的指针。 @12431234123412341234123, Atmega168 @12431234123412341234123,为什么不稳定?据说它们是内存映射的,即它们的地址将始终相同。它不像 Intel 芯片可以由处理器重新分配 eax-edx 地址。 【参考方案1】:

从内存映射的角度来看: 通用寄存器、特殊功能+I/O 寄存器和 SRAM 共享一个地址空间的非重叠范围,如各种处理器的数据表中所述AVR 系列。您的所有指针都将引用此内存空间,除非注释为指向 PROGMEM 的指针(这将导致发出不同的指令)。将在没有任何类型的虚拟内存映射的情况下进行引用。

例如,ATtiny 25/45/85 在第 18 页显示如下地图:

您的链接器知道此内存映射并将相应地放置变量。例如,在您的一个编译单元中声明的全局变量将在上述示例设备中位于 0x0060 以上的地址中,因此它最终位于 SRAM 中。

从指令编码的角度来看:虽然只有一个地址空间,但为某些重要区域保留了特殊功能。例如,IN 和 OUT 指令的指令编码中有 6 位,可用于直接引用 [0x20, 0x5F) 内的 64 个地址之一。

IN 和 OUT 指令在加载和存储到指令中直接编码的固定地址的能力方面是独一无二的,因为正常的加载和存储指令需要先加载“Z”寄存器的间接加载。

因此,当编译器看到对固定 I/O 寄存器的内存操作时,它可能生成这些更有效的指令。但是,通过指针进行的正常加载/存储将具有相同的效果(尽管需要不同数量的时钟周期)。对于不适合前 64 个的扩展 I/O 寄存器(例如 atmega328p 上的 OSCCAL),将始终生成正常的加载/存储指令。

【讨论】:

专业开发人员如何编写代码?他们是使用宏还是手动定义指针? @tenghiz 我一直使用设备工具链和标准库提供的宏。它提供了一个合适的抽象,不需要我考虑这些底层指令,而作为我的产品工程师,我应该专注于产生的功能。 感谢您提供的图片。现在我看到了差异的根源在哪里。 'iomx8.h' 状态如下:#define DDRB _SFR_IO8 (0x04)。但是,Atmel168 手册说 DDRB 偏移量是 0x24。您提供的图像允许看到 IO 寄存器从 0x20 开始,而且 DDRB = _SFR_IO8 (0x04) + 0x20。 @tenghiz 根据this,_SFR_IO8 是一个宏,它添加了 0x20 并添加了一个 volatile 限定符,从而解释了您的 0x20 差异。【参考方案2】:

简短的回答 - 隐藏在 Atmel 包含的标头中的是一组宏,它们创建指向寄存器位置的指针。如果您想查看任何源代码以及其他必要的标头(如 interrupt.h),它们位于 WinAVR-20100110/avr/include/

以下是该过程的简要概述:

您的 Makefile 定义了要使用的设备,然后将它的定义传递给编译器。

DEVICE = atmega2560
...
-D__$(DEVICE)__

然后包含 io.h,它会根据您的设备自动包含必要的标头:

// In main source file
#include <io.h>    

// In io.h
#include <avr/sfr_defs.h>
// ...
#elif defined (__AVR_ATmega2560__)
    #  include <avr/iom2560.h>

// In sfr_defs.h
#define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))
#define __SFR_OFFSET 0x20
#define _SFR_IO8(io_addr) _MMIO_BYTE((io_addr) + __SFR_OFFSET)

// In iom2560.h
#include <avr/iomxx0_1.h>
// Other device specific definitions

// Om iomxx0_1.h
#define PINA    _SFR_IO8(0X00)
// Other device family shared definitions

因此,如果您展开所有这些,您会得到一个指向寄存器地址的易失性指针。当您在代码中使用 PINA 时,预处理器会将其替换为所有扩展宏:

PINA
_SFR_IO8(0X00)
_MMIO_BYTE((0X00) + __SFR_OFFSET)
(*(volatile uint8_t *)((0X00) + 0x20))

其中指定 PINA 是指向 0x20 的易失性 8 位内存地址的指针。然后,每当访问该地址时,内部芯片架构都会将该地址映射到相应的外设寄存器。

不同的设备有不同的寄存器地址和偏移量。如果您想定义自己的,您需要查看相关的数据表。对于大多数 AVR 芯片,最后都有一个标题为“寄存器摘要”的部分,其中列出了所有寄存器地址和各个控制位的名称。根据我的经验(至少对于 AVR),数据表中的寄存器和位的名称与 io.h 文件中的定义完全相同。

还要注意使用“uint8_t”而不是“char”。在适当的时候使用 中的位宽特定定义来指定有符号/无符号和 8/16/32 位变量是很常见的(并且强烈鼓励)。由于 AVR 是 8 位的,任何使用 16 位或 32 位(或浮点)变量的操作都需要多个时钟周期。在这种情况下,stdint.h 应该有:

typedef unsigned char uint8_t

【讨论】:

专业工程师是否接受 WinAVR 或 AVRDude 作为工作工具?还是人们更喜欢与 Atmel Studio 合作?我的问题是关于头文件的内容:它们是否相同?到目前为止,我在 WinAVR 包中看到的一切都相当令人困惑。首先 *(volatile uint8_t *) 被编码为 _MMIO_BYTE,然后 _MMIO_BYTE 被编码(稍作修改)为 _SFR_IO8。或者另一个:#define _SFR_MEM8(mem_addr) _MMIO_BYTE(mem_addr)。看起来不是明智之举。 @tenghiz 是和不是。 WinAVR 很久没有更新了。提到的版本是 20100110,所以是 2010 年 1 月。哎呀。但是 WinAVR 实际上只是为 Windows 打包的一堆有用的工具。真正的牛肉是 AVR-GCC 和 AVR-libc,它们的定义来自专业人士并被专业人士使用。较早的 Atmel Studio(如 v4)是建立在这些东西之上的。现在它是所有臃肿和可怕的 Atmel 软件框架。 Studio 仍然使用 GCC 作为编译器。 @tenghiz 至于所有宏的东西......它有点乱,被很多应用程序工程师认为是“hacky”,但这是一种最好的便携方式编写嵌入式代码时可能。想想,每个芯片都是一个独特的架构。它就像一个程序,必须在曾经存在或将要存在的每个 Windows 版本,以及所有 ios 版本,以及每个 Linux 发行版上运行。只是它是硬件,因此有些芯片具有其他芯片没有的特定寄存器和存储器。 @tenghiz 个人,我(一个工作的专业人士)使用最新版本的 AVR-GCC(或用于 ARM 芯片的 arm-none-eabi-gcc)与供应商提供的包结合使用,这些包定义了所有内存的东西和寄存器地址。我不使用他们的任何外围代码 API 库,因为它太臃肿了。

以上是关于AVR芯片编程时如何定义内存指针?的主要内容,如果未能解决你的问题,请参考以下文章

AVR开发 Arduino方法 内存子系统

AVR_GCC 编译错误 delay.h

AVR XMEGA 上的 EEPROM 内存管理

arduino编程时候如何利用winavr的库文件??如:#include <avr/EEPROM.h>

如何从 AVR C 中的内存地址调用函数?

如何启用多个中断 AVR