NerdKits - electronics education for a digital generation

You are not logged in. [log in]

NEW: Learning electronics? Ask your questions on the new Electronics Questions & Answers site hosted by CircuitLab.

Microcontroller Programming » I made a new language for programming the Atmega

August 05, 2011
by bretm
bretm's Avatar

I'm been noticing how tough programming can be for people who are new to it, perhaps forced into it just because they like electronics and just want to use microcontrollers. I see how bad "C" can be for that type of person. So I made up a language.

The compiler here is very rough around the edges. It's written for .NET Framework 4 so only works on Windows for now. Mono on Linux maybe someday, but only if people seem interested.

Install .NET, download the compiler, put this "sample.oq" file in the same folder and run "oq sample.oq" and it should make "sample.c" out of it. So it's more of a translator-to-C than a full compiler, but I think it still counts.

Here's the sample.oq program:

    Adc                                     # include Adc.oq for ADC routines
    HD44780 as Lcd                          # include HD44780.oq for LCD routines
    Usart                                   # include Usart.oq for serial I/O routines

    Lcd  PD5,PD4,PD3,PD2  data              # define the pins for LCD data, enable, and RS
    Lcd  PD6  enable
    Lcd  PD7  RS

    in   PB1  button                        # define the pin for our language button

    uint2   language = 0                    # define two bits in SRAM, set to 0 (English)

when [button] falls:                        # define what happens when debounced button is pressed
    language++                              # increment "language" value (0, 1, 2, 3, 0, ...)
    Lcd.Clear                               # clear the LCD

    send    UTF_8   Usart.WriteByte         # specify a UTF-8 output stream for the serial port

sum = Vcc * 64                              # define "sum" as large enough for 64 voltage samples

loop:                                       # start the main loop
    sum = 0mV                               # reset sum to zero volts

    repeat 64:                              # repeat 64 times (for 3 extra bits precision)
        sum = sum + Adc.GetVoltage()        # get the ADC voltage and add it to the sum

    float avg = sum / 64                    # calculate the average (still in volts)
    degF = avg / 10mV                       # LM34 conversion 1°F = 10mV
    degC = (degF - 32) * 5 / 9              # celsius conversion

    Lcd.Home                                # move the LCD cursor to the home position

    if language == 0:                       # display the temperature
        Lcd.Write "Temperature: " degF "°F "    # in English, fahrenheit
    elif language == 1:
        Lcd.Write "Temperatur: " degC "°C "     # celsius in german, swedish, norwegian, danish, etc.
    elif language == 2:
        Lcd.Write "オンド" degC "°C "             # in Japanese, celsius
        Lcd.Write "Tymheredd: " degC "°C "      # in Welsh, fahrenheit

    send degF "°F\r\n"                      # write the temperature to the serial port

Some features of the language:

Syntax borrows heavily from Python.

Less hassle reading and writing individual bits of different ports. For example, here's the function in Adc.oq for writing to MUX3:0 in the ADMUX register:

def SetChannel(uint4 mux):
    MUX3:0 = mux

Using custom pins is easier. Here's how to define a pin as an output pin:

    out PC4 led

Here's how you would set that pin to high:


Or to low:


Or to toggle it:


Or to either high or low depending on whether a button is up or down:

led = button

It does units:

wait 50ms
degF = Adc.GetVoltage() / 10mV

Take a look at the sample.oq above, and the Adc.oq, HD44780.oq, and Usart.oq in the ZIP file for examples of other things you can do.

August 05, 2011
by bretm
bretm's Avatar

LED blinking program in this language:

    out PB1 led

every 500ms:
August 06, 2011
by Ralphxyz
Ralphxyz's Avatar

Wow, bretm that is really neat.

I am probable a typical target user, I can see that most everything I normally do can be done.

Now as soon as I can find some time I'll try it out.


August 06, 2011
by mongo
mongo's Avatar

I just installed the NET4 framework last night and just might take a serious look into this one. If it makes things easier, I am all for it.

August 06, 2011
by bretm
bretm's Avatar

Basic syntax is like Python--whitespace is important. It uses indents to indicate the block structure. Tab stops are considered to be every 4 spaces when determining if lines are indented equivalently.

Statements that expected an indented block afterword will end with ":". If it doesn't end with ":" it's a one-line statement.

The "pins:" block defines friendly names for pins or groups of pins and specifies their direction. If a pin is set to "out" it's an output pin and you won't be able to read its value in an expression. If a pin is set to "in" or "in floating" it's an input pin and you won't be able to set it's value. If it is set to "io" then you can do either, and change its direction at run time.


    in PB1 inputWithPullup
    in floating PB2 inputWithoutPullup
    out PB3 myOutputPin
    io PB4 aPinIWillControl

+myOutputPin       # set pin to HIGH
myOutputPin = 1    # set pin to HIGH
-myOutputPin       # set pin to LOW
myOutputPin = 0    # set pin to LOW
~myOutputPin       # toggle the pin
!aPinIWillControl  # set pin to output
?aPinIWillControl  # set pin to input
^aPinIWillControl  # toggle pin direction

if inputWithPullup:
    # do something if pin is HIGH

if not [inputWithPullup]:
    # do something if debounced pin is LOW

if not {inputWithoutPullup}:
    # do something if denoised pin is low

Debouncing and denoising are done by checking the pin every 5ms. For debouncing, after a pin change is seen, further changes are ignored for the next 40ms. For denoising, pin changes are ignored until the new pin state has persisted for 40ms.

You can also define groups of pins on the same port.

    out PB3,PB2,PB1,PB0 knightRider

knightRider = 0b0001
wait 100ms
knightRider = 0b0010
wait 100ms
knightRider = 0b0100
wait 100ms
knightRider = 0b1000
wait 100ms

You can respond to pin changes (or a change in any condition) with "when".

when button falls:
    # do something if a pin goes low

when sensor rises:
    # do something if a pin goes high

when button1 or button2 and not button3 changes:
    # do something if the boolean expression changes value
August 06, 2011
by bretm
bretm's Avatar

Variables are defined by "sram", "progmem", "eeprom", or "persistent" statements. There is only limited-to-no support for string variables. It's mostly just numbers for now.

sram:            # define variables in SRAM
    uint6 x      # 6 bit unsigned integer (0 to 63)
    int16 y      # 16 bit signed integer (signed must be 8, 16, 32)
    bit z        # 1 bit unsigned integer
    float u      # 32 bit floating-point number

    uint8 state = 3    # define EEPROM variable
                       # and set it to 3 if EEPROM is uninitialized

Reading and writing EEPROM variables works just like regular variables. Be careful you don't change their value very often because they have a limited lifetime. If you redefine the program to define different EEPROM variables or change the default values, all of the EEPROM variables will be reinitialized.

The "persistent" statement is used inside a "def" statement to define the equivalent of static local variables in "C", which are variables local to a function but that keep their values between function calls. They use SRAM.

August 06, 2011
by bretm
bretm's Avatar

Some pseudo-multi-tasking:

   out PB1 led1
   out PB2 led2
   out PB3 led3

at 1Hz:

at 2.71828Hz:

every 314159us:

These commands happen inside a timer interrupt, so they need to be quick. Also, you can't access global variables from inside an interrupt. If you need to share data with the main loop you need to use a queue:

    fred[4] uint8        # define a queue that can hold 4 bytes

at 1Hz:
   put 25 onto fred      # put a "25" into the queue every second

loop:                    # main program loop

   # do stuff in my main loop
   # and check if fred has any messages

   for msg in fred:
       # do something with each msg

This correctly handles "volatile" issues and ensures that data-sharing operations are done atomically to avoid race conditions.

August 07, 2011
by Ralphxyz
Ralphxyz's Avatar

bretm, pardon my being dense but:

Install .NET, download the compiler, put this "sample.oq" file in the same folder 

In the "same folder" as what? The compiler? That might take a bit to find.

Or is there a development structure implied like nerdkits/code/initialload?

Is there a .zip folder of your trial programs(?)/scripts?

I just fired up my Windows box to give it a try!


August 07, 2011
by Ralphxyz
Ralphxyz's Avatar

Got it!!

Sorry I should have tried doing it before asking.

The "folder" is wherever you unzip the file to, so use that.

I used D:nerdkitsBRETM_scripting. Copied the sample.oq code from above to the

folder and ran oq sample.oq from the command line (cd D:nerdkitsBRETM_scripting of course).

Now lets see what trouble I can get into.


August 07, 2011
by Ralphxyz
Ralphxyz's Avatar

Oh this is cute:

When you compile the script the source code is deleted or at least my text editor (Notepad++) thinks it is.

It is just a mistake on the editors part sample.oq is still in the folder.

If I had closed the editor before running the compiler I would not have seen the message, but I usually do not close the editor so I'll just live with another nagging, useless, stupid application Warning message, after all this is Windows.


August 07, 2011
by Robotnik
Robotnik's Avatar

Well this is pretty neat! I've always been a fan of c, but this looks like it could save me some time. I am actually tempted to set avr on windows so I can try it out. I tried compiling the generated c with avr-gcc but it didn't work, tons of errors. I was probably doing something wrong though. I need to take the time to learn how to use avr-gcc one of these days instead of just modifying makefiles...

August 07, 2011
by bretm
bretm's Avatar

Right now it's easiest if you use the same folder as rhe compiler because I didn't make an installer app to set up the PATH environment variables to support a more traditional compiler set up. I will be doing that eventually.

August 07, 2011
by bretm
bretm's Avatar

Let me know what avr-gcc errors you were getting, but I think I know: I forgot to talk about the makefile! The generated C references some include files like "oq.h" that need to be findable by the compiler, and "oq.c" and "oqwait.c" need to be compiled along with your program.

My makefile for sample.oq:

PROJ = sample
MCU  = atmega328p
MCUb = m328p
INCS = Oq.c OqWait.c



ELF = $(PROJ).elf
HEX = $(PROJ).hex
EEP = $(PROJ).eep
ASM = $(PROJ).asm


## Translate
    oq $(PROJ).oq

## Compile and link
    avr-gcc -g -Os -mmcu=$(MCU) $(PROJ).c $(INCS) $(OBJS) -o $(ELF)

## Generate HEX image for FLASH
    avr-objcopy -O ihex $(ELF) $(HEX)

## Generate EEP image for EEPROM
    avr-objcopy -j .eeprom --set-section-flags=.eeprom="alloc,load" --change-section-lma .eeprom=0 --no-change-warnings -O ihex $(ELF) $(EEP)

## Generate assembly listing (informational)
    avr-objdump -S -d $(ELF) >$(ASM)

## Show sizes
    avr-size -C --mcu=$(MCU) $(ELF)

## Upload
    avrdude -c avr109 -p $(MCUb) -b 115200 -P COM4 -U flash:w:$(HEX):a

This builds sample.oq all the way to .hex file format.

August 07, 2011
by Robotnik
Robotnik's Avatar

Okay, I successfully compiled the generated c file then uploaded it to the chip, then I realized this was the temp sensor project :/ I'm goin to try again only with a simple blinking led that way I can see it work without rewiring my lcd which is currently in use for something else.

August 07, 2011
by Robotnik
Robotnik's Avatar

... And it works like a charm. :D

August 07, 2011
by bretm
bretm's Avatar

Another feature of the language is that it's easy to break a value up into its constituent bits, or to concatenate bits into a larger value.

uint8 abyte = 123          # an 8-bit variable
hi, lo = abyte bits 4:4    # the high and low 4 bits from "abyte"
abyte = hi:lo              # combined back into a byte

float pi = 3.14            # extract the IEEE fields from a float
sign, exponent, mantissa = pi bits 1:8:23

If you don't care about some of the bits, you can do this:

x, _ = abyte bits 2:6      # grab top two bits and ignore the rest
August 08, 2011
by Ralphxyz
Ralphxyz's Avatar

So [quote] x, _ = abyte bits 2:6 # grab top two bits and ignore the rest [/quote]

Says to Let x = the top two bits

and _ is a throwaway?

Is _ always a throughway when used in an assignment? Or can it hold a value? Can _ be referenced?


August 08, 2011
by bretm
bretm's Avatar

It's always thrown away and cannot be referenced. No C assignment code is generated for it. It's also useful when you have a function that returns multiple values and you don't care about some of them:

def MyFunc():
    return 123, 456

_, second = MyFunc()

Multiple variable assignment can also be used directly in an assignment, e.g. to swap two values:

a, b = b, a

Post a Reply

Please log in to post a reply.

Did you know that you can turn a $20 digital scale into a live weight sensor using our kit with a few extra parts? Learn more...