Veronica – Macro Assembly for AVR

Posted: 9th December 2012 by Quinn Dunki in Hacks, Veronica

Coding in style with open source tools.

 

I need to take a quick segue here to talk about the toolchain I’ve been using to write the GPU code for Veronica. As I bring up the GPU to an ever-more-useful state, it’s starting to need large quantities of fairly complex code. Since any kind of high-level language is out of the question in this situation, I need to use the next best thing- macro-based assembly. Macro assemblers have been around almost as long as computers, because they’re a very easy layer to add on top of a normal assembler that multiplies programmer efficiency. Macros can be very powerful indeed, and at times can make it feel like you’re coding in C or another high-level language. Of course, you have to keep in mind what you’re really doing (copying and pasting code around), and proceed with care at all times.

Atmel’s official development environment for AVRs includes a macro assembler, and I’m sure it’s swell. However, it’s also Windows-only. If, like me, you’re using all open-source cross-platform tools, that means using avr-gcc. The GNU toolchain for AVR isn’t too shabby all things considered, but one thing it is missing is macros in the assembler (avr-as). The GNU build process relies on the C Preprocessor (CPP) for this sort of thing, and it’s fine to a point. It works great for defining constants, including other files, that sort of thing. However, CPP is not designed to respect whitespace, since it’s intended for a language (C) which doesn’t care about it. Assemblers generally do care about whitespace however, so we need a macro system that will preserve whitespace and do other things that are needed for the output to be assemble-able by avr-as.

So, let’s start with some sample code to illustrate the problem I’m trying to solve. One of the things I do a lot of in the GPU code is enable and disable access to the VRAM data bus. This involves flipping a few bits in registers, and needs to be done carefully. However, it’s also the same process everywhere, so it’s a perfect candidate for a macro. Here’s a GPU function to plot a red pixel:

Those three oddball lines of code (EnableVRAMWrite, etc) are macros that expand into several lines of assembly which set all the register bits and control states needed for the GPU to do the task in question. This makes the code much more readable, and greatly reduces error. Furthermore, if I change the control signals or something else that affects this process, I can just update the macro instead of having to find all the places the GPU touches VRAM. It’s like getting all the benefits of a high-level language function call, although the code can get bloated if you’re not careful.

Clearly, the code above would not compile under avr-gcc or avr-as, so how am I doing this?

The secret is to step up from CPP to GNU’s “m4 “, which is the 800 pound gorilla of code preprocessors. If m4 can’t do the preprocessing you need, then it can’t be done.┬áIt can easily be configured to preserve whitespace during macro substitution, and has many crazy features (loops, anyone?) that CPP can only dream of. Even better, if you have a un*x system of any flavor, chances are you have m4 already. All I needed to do was set up a makefile that would incorporate m4 into the build, so that it processes all the files before the GCC assembler sees them.

A quick caveat- I am far from an expert on Make, which is a very sophisticated tool in its own right. So forgive me if there are better ways to implement what I’m doing here. However, this works for me.

Here’s a sample makefile for my process:

As you can see (or not, depending on your ability to read makefiles :) ), I’ve introduced a new intermediate file format called .m4S, which is a .S assembly file that has been run through m4. The m4S files are then fed to GCC for assembly. Conveniently, GCC still runs the files through CPP first, so I can still use #defines and other convenient CPP constructs.

Here’s the m4 file that is parsed to create the macros shown in the sample code above:

m4 code can be a bit cryptic, but if you’ve used any shell scripting languages, or something like YACC or Bison, it will feel somewhat familiar. I had never used it before, but got started by skimming the well-written manual.

With everything set up as you see here, I can use CPP constructs (such as #defining register r16 to “accum” in the code sample above), and mix in m4 macros transparently. I’m very happy with this setup, and it has greatly accelerated my GPU code development. I can build from the command line, using these commands:

  • make : Builds the code and reports errors
  • make install: Builds the code, then uses the USBTiny to install it on the AVR
  • make clean: Deletes all intermediate and output files
  • make fuse: Sets the fuses on the AVR- generally only needed once when starting with a fresh chip.

I can also build, install, and run from within Xcode (my preferred development environment), which supports makefiles rather well.

 

When I got started with AVR development, I found it difficult to find information on setting up a good build pipeline using cross-platform tools. In particular, good makefiles for avr-gcc are hard to find online. I hope this information helps you out a bit!

Stay tuned for more exciting action from Veronica!

  1. asdf says:

    gas does support macros (http://sourceware.org/binutils/docs-2.23.1/as/Macro.html), but they’re kind of limited and get a bit funky if you want to do anything even remotely complex.

    • blondihacks says:

      Yah, I didn’t bother mentioning that, because my experience with that stuff was not good at all. I appreciate you mentioning it though, in case anyone else wants to try it.

  2. JCCyC says:

    Correct me if I’m wrong, you’re going to end up creating a video interface that’ll be good for all kinds of retrocomputers and SBCs, amirite?

  3. blondihacks says:

    Well, theoretically, yes. :) I’m not intentionally designing something universal, but the interface for it is (so far) so simple that there’s no reason it couldn’t be used on anything else. The only somewhat Veronica-specific element so far is the physical layout- it relies on a board-edge connector to talk to the computer.

  4. Michael says:

    So much to learn so little time. Adding m4 to the list. Thanks :) interested to read more about Veronica too

  5. Erik Walthinsen says:

    You know you can just ask GCC to ‘compile’ an assembly file you, right? Then you can use regular C macros to your heart’s content, and also pull in defines from higher-level sources.

    OTOH m4 is quite a bit more advanced than cpp, and I’m tempted to look into its use for a future project. I’m passingly familiar with it from dealing with the nightmare that is autotools…

    • blondihacks says:

      Yes, I am using gcc to assemble the code, and I’m also making use of many CPP constructs such as #include and #define. I mentioned this in the article, but perhaps I wasn’t very clear about it.

      The issue is that CPP’s macros do not preserve whitespace. In particular, newlines are needed within the macros in order to create output that is sensical to the assembler. The closest thing CPP has is the backslash (‘\’) as used for multi-line macros. However, such macros are concatenated to one line before being passed down the pipeline, so the assembler will see gibberish and thus generate errors.

  6. Rhialto says:

    There is also something called “gasp”, the GNU Assembler Preprocessor. It uses a more conventional syntax for assembler macros. I only found a few old references to it, so I’m not sure how current that project is.

    On the other hand, m4 is turing-complete, so for some things you can have m4 calculate the end result rather than generate assembly to do it.
    One example of m4 that I use in practice is for my CTWM configuration file, where I want to set the size of a workspace manager window to a certain proportion of the screen size:

    # WorkSpaceManagerGeometry “200×150+0-0″ 3
    define(IWIDTH, eval(WIDTH / 6))dnl
    define(IHEIGHT, eval(HEIGHT / 6))dnl
    WorkSpaceManagerGeometry “IWIDTH[]x[]IHEIGHT[]+0-0″ 3

    The m4 quote characters have been set to [ and ] here.

  7. rob says:

    Another option would just to be have a few blocks saved in a file and, at assembly time, just open it in your favorite text editor and use find-replace to substitute in your blocks for your place-holder.