1

Preamble: after working a couple of years as application developer, the world of the software engineering became more obscure than it was before. The reason is that the real stuff is hidden under zillions layers of abstractions: OS, frameworks, etc. The young generation is deprived of the pleasure of working with PDP-like machines where all programming was done via electrical switch toggling. Another problem is the ephemeral nature of modern programming languages. Once there was Python 2.x, now it is deprecated and there is Python 3.x which in its turn will be deprecated in a couple of months. Idem for other languages. ANSI C looks like the Pyramid of Cheops: it was there in 70's and I don't doubt it will be there after the Sun will become a red dwarf.

It seems that now the only way to understand the interaction between the hardware and the software is to play with embedded development. From the pedagogical point of view physical chips are very handy because they allow to tackle the most difficult part of C language, namely pointers. When coding in OS environment, */& notation is still very confusing because it refers to some location somewhere inside of the virtual memory. And before you will got the understanding of what is the virtual memory, you have to read a couple of monographs about OS development, etc. You may find it stupid but I do really want to know which transistor is holding my bit right now. At least, I can wire physical pin voltage to programming abstractions.

Currently I am working with Atmel chips and WinAVR package because of numerous textbooks and accessible hardware. Though all books promise to teach AVR coding using plain C, the reality is that all pointers are hidden behind macros like PORTA, DDRB, etc. All code examples include header file 'io.h' which in its turn refers to other header files specific for a given chip like 'iomx8.h'. So far, I cannot find any macros definition in these headers. The code to increase the voltage on the physical pin 14 on Atmega168 looks like

DDRB = 0x01;
PORTB = 0x01;

Fortunately, Microchip site provides some basic documents where it is stated, for example, that if I want to rise the voltage on the physical pin 14, I need to follow these steps:

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

Unfortunately, this is the only info I got after one week of the lurking. Now I am going through USART programming and the things become more complicated with all these UBRR0H, UCSR0C. Since provided header files don't contain macros definitions for any register, where else can I find it?

A similar question was asked several years ago: accessing AVR registers with C?. However, provided answers were somewhat useless, besides the clue that GCC itself can map some mythical PORTB to real physical locations. Could someone describe the mechanism behind the mapping?

tenghiz
  • 239
  • 1
  • 10
  • Which AVR do your program? – 12431234123412341234123 Aug 26 '20 at 13:40
  • "Since provided header files don't contain macros definitions for any register, where else can I find it?" Did you check the datasheet? – 12431234123412341234123 Aug 26 '20 at 13:42
  • Pointers to SFR should be declared as pointer to volatile. – 12431234123412341234123 Aug 26 '20 at 13:44
  • @12431234123412341234123, Atmega168 – tenghiz Aug 27 '20 at 04:18
  • @12431234123412341234123, why volatile? It is stated that they are memory-mapped, i.e. their address will be the same all the time. It is not like Intel chips where eax-edx address can be reassigned by processor. – tenghiz Aug 27 '20 at 04:21
  • @tenghghiz Not the pointer should be volatile but the address you are pointing to. Because it is a SFR and writing to it has sideeffects and should not be optimized out, so it has to be volatile. `voltile uint8_t *p` means `p` points to a byte that is volatile, `p` itself is not volatile. – 12431234123412341234123 Aug 27 '20 at 12:51
  • Get the right headers and read the Application Notes. Even assembly programmers use the macros in real life for real world, paying gigs. Make your code more portable and use the macros. If you're only that far after a week, you're doing it wrong. – TomServo Aug 29 '20 at 00:11
  • @TomServo, I have precised that my goal is to learn the basics of embedded C and not to produce the commercial code. I don't doubt that there are incredibly talented people who can get right headers in 1 second, but I am not definitely this one. – tenghiz Aug 30 '20 at 16:31

2 Answers2

1

From a memory-mapping standpoint: The general purpose registers, special function+I/O registers, and SRAM share non-overlapping ranges a single address space, as described in datasheets for various processors in the AVR series. All of your pointers will reference this memory space, unless annotated as pointers to PROGMEM (which will cause different instructions to be emitted). The reference will be made without any sort of virtual memory mapping.

For example, the ATtiny 25/45/85 has the following map shown on page 18:

enter image description here

Your linker is aware of this memory map and will place variables accordingly. For example, a global variable declared in one of your compilation units will end up in an address above 0x0060 in the example device described above, so that it ends up in the SRAM.

From an instruction encoding standpoint: Although there is one address space, there is special functionality reserved for certain important regions. For example, IN and OUT instructions have six bits in their instruction encoding which can be used to directly refer to one of the 64 addresses within [0x20, 0x5F).

The IN and OUT instructions are unique in their ability to load and store to a fixed address encoded directly in the instruction, since the normal load and store instructions require an indirect load with the 'Z' register being loaded first.

As a result, when the compiler sees memory operations to a fixed I/O register, it may generate these more efficient instructions. However, a normal load/store via a pointer will have the same effect (although with different numbers of clock cycles required). For extended I/O registers that didn't fit into the first 64 (e.g. OSCCAL on an atmega328p), normal load/store instructions will always be generated.

nanofarad
  • 40,330
  • 4
  • 86
  • 117
  • how do professional developers write the code? Do they use macros or manually define pointers? – tenghiz Aug 26 '20 at 01:51
  • 1
    @tenghiz I've always used the macros provided by the toolchain and standard library for the device. It presents a suitable abstraction that doesn't require me to think about these underlying instructions when, as an engineer of my product, I should be focusing on the resulting functionality. – nanofarad Aug 26 '20 at 01:51
  • Thank you for the image. Now I see the where is the source of the discrepancy. 'iomx8.h' states following: #define DDRB _SFR_IO8 (0x04). However, Atmel168 manual says that DDRB offset is 0x24. The image you provided allows to see that IO registers start at 0x20, also, DDRB = _SFR_IO8 (0x04) + 0x20. – tenghiz Aug 26 '20 at 01:58
  • 1
    @tenghiz According to [this](https://electronics.stackexchange.com/a/463593/9612), _SFR_IO8 is a macro that adds 0x20 and adds a volatile qualifier, accounting for your discrepancy of 0x20. – nanofarad Aug 26 '20 at 02:03
1

Short answer - hidden away in the included headers from Atmel are a collection of macros that create pointers to the register locations. If you want to see any of the source, as well as additional necessary headers like interrupt.h, they are in WinAVR-20100110/avr/include/

Here's a brief overview of the process:

Your Makefile defines the device to be used, and then passes it has a definition to the compiler.

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

You then include io.h, which automatically includes the necessary headers based on your device:

// 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

So if you unroll all of that, what you get is a volatile pointer to the register address. When ever you use PINA in your code, the preprocessor replaces it with all of the expanded macros:

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

Which specifies that PINA is a pointer to a volatile 8-bit memory address of 0x20. The internal chip architecture then maps that address to the appropriate peripheral register whenever it is accessed.

Different devices have different register addresses and offsets. If you want to define your own, you'll need to check out the relevant datasheet. For most AVR chips, there is a section towards the end titled "Register Summary" that lists all of the register addresses and names of the individual control bits. In my experience (for AVR, at least), the names of the registers and bits found in the datasheet are exactly what they are defined as in the io.h files.

Also notice the use of "uint8_t" rather than "char." It's common (and highly encouraged) to use the bit-width specific definitions found in <stdint.h> to specify signed/unsigned and 8/16/32 bit variables whenever appropriate. Since AVR is 8-bit, any use of 16 or 32 bit (or float) variables will require multiple clock cycles for each operation. In this case, stdint.h should have:

typedef unsigned char uint8_t
Kurt E. Clothier
  • 276
  • 1
  • 11
  • Are WinAVR or AVRDude accepted as working tool by professional engineers? Or the people prefer to work with Atmel Studio? My question is about the content of the header files: are they same or not? So far, everything that I see inside of WinAVR package is rather perplexing. First *(volatile uint8_t *) is encoded as _MMIO_BYTE, then _MMIO_BYTE is encoded (with small modifications) as _SFR_IO8. Or another one: #define _SFR_MEM8(mem_addr) _MMIO_BYTE(mem_addr). Doesn't look like a smart move. – tenghiz Sep 09 '20 at 06:16
  • @tenghiz yes and no. WinAVR hasn't been updated in a long time. The mentioned build is 20100110, so January 2010. Yikes. But WinAVR is really just a bunch of useful tools packaged for Windows. The real beef is AVR-GCC and AVR-libc which is were those defines come from and are used by professionals. Older Atmel Studio (like v4) was built on top of these things. Now it's all the bloated and horrible Atmel Software Framework. Studio still uses GCC as the compiler. – Kurt E. Clothier Sep 09 '20 at 16:52
  • @tenghiz as for all of the macro stuff ... it's a bit of a mess and considered "hacky" by a lot of application engineers, but that's kind of the best way to be as portable as possible when writing embedded code. Think, every chip is a unique architecture. It'd be like a program that has to work on every version of Windows that has ever or will ever exist, plus all iOS versions, plus every Linux Distribution. Only it's hardware, so some chips have specific registers and memories that others don't. – Kurt E. Clothier Sep 09 '20 at 16:54
  • @tenghiz personally, I (a working professional) use the latest version of AVR-GCC (or arm-none-eabi-gcc for ARM chips) in combination with the vendor supplied packs that define all of the memory stuff and register addresses. I don't use any of their peripheral code API libraries because it's buggy bloat. – Kurt E. Clothier Sep 09 '20 at 16:57