Programming Microcontrollres

Summary

  • RP2040 Pico
    • Output
      • C
      • MicroPython
    • Serial Input
      • MicroPython
  • Arduino Uno
    • Output
      • C (Bare Metal)
  • ATtiny412
    • Output
      • C (Bare Metal)
    • Input
      • C

Raspberry Pi Pico (rp2040)

C/C++

I plugged in the Pico via USB and opened the Arduino IDE (2.0.2). I changed the port to COM5 (Raspberry Pi Pico) under Tools > Port and tried to changed the board to Raspberry Pi Pico in the dropdown on the editor.

Changing Port

I realized that I didn't have the rp2040 board driver installed, so I opened the Boards Manager menu, searched rp2040, and installed Arduino Mbed OS RP2040 Boards by Arduino.

Boards Manager

Now that I'd installed the board, I was able to change the board to Raspbery Pi Pico under the Tools > Board > Arduino Mbed OS RP2040 Boards dropdown.

Changing Board

Now the board selected displayed Raspberry Pi Pico.

New Board Shown

Now I was ready to upload. I opened the example Blink sketch, changed the pin to 18, and uploading, and recieved an error!

Open Blink Example

// the setup function runs once when you press reset or power the board
void setup() {
  // initialize digital pin LED_BUILTIN as an output.
  pinMode(18, OUTPUT);
}

// the loop function runs over and over again forever
void loop() {
  digitalWrite(18, HIGH);  // turn the LED on (HIGH is the voltage level)
  delay(1000);                      // wait for a second
  digitalWrite(18, LOW);   // turn the LED off by making the voltage LOW
  delay(1000);                      // wait for a second
}
Failed uploading: uploading error: exit status 1

I tried unplugging then replugging in the board and then the sketch uploaded!

Next, I looked online for an example script that takes serial input and sends it back to the user, uploaded it, and it worked!

MicroPython

I installed the Thonny IDE to communicate with the Pico via Python.

Thonny

Then, following this tutorial on how to use Thonny to install MicroPython on a Pico rp2040. To make sure the programs weren't running Python on my local machine, I went selected Run > Configure Interpreter (the tutorial says Select Interpreter but the option name was different for me), selected MicroPython (Raspberry Pi Pico), and selected USB Serial Device (COM5) as the port (note that the number after COM may be different when replicating the instructions).

Tutorial Select Interpreter

Configure Interpreter Thonny

Selecting Interpreter Thonny

Selecting Port Thonny

Yet, when I tried clicking the green Run button, I recieved an error.

Green Run Button Thonny

Unable to connect to COM5: could not open port 'COM5': PermissionError(13, 'Access is denied.', None, 5)

If you have serial connection to the device from another program, then disconnect it there first.

I realized that this may be because I still had the Arduino IDE open, so I closed it, clocked Stop/Restart backend in Thonny, and ran the program again, but this did not work. I tried pressing the button on the Pico, but this also didn't help. On this page I saw a schema for a reset button that can be used to reset the Pico, so I wired it and tried it (pressing the button on the Pico, the reset button, releasing the Pico button, releasing the reset button), but it didn't seem to fix the problem.

Reset Button Schema

I also tried getting a new Pico, but this one kept counting up in a clock and printing it to the terminal in Thonny and I got an error that the process couldn't be interrupted when I tried running my program.

6:00:05

6:00:06

6:00:07
Device is busy or does not respond. Your options:

  - wait until it completes current work;
  - use Ctrl+C to interrupt current work;
  - reset the device and try again;
  - check connection properties;
  - make sure the device has suitable MicroPython / CircuitPython / firmware;
  - make sure the device is not in bootloader mode.

Could not interrupt current process. Please wait, try again or select Stop/Restart!

When I used the reset button again, the clock would stop temporarily only to start back up again. I finally realized that maybe MicroPython wasn't installed on the rp2040, so I clicked the Install or update MicroPython which mentioned that I had to but the device into bootloader mode. I didn't know how to do this, but on this website and on this website I learned that I have to uplug the Pico then replug it back in while holding down the button on the Pico. This worked!

Need To Enter Bootloader Mode

Install Menu Thonny

I set the Target volume option to RPI-RPI2 (E:) and the MicroPython variant option to Raspberry Pi • Pico / Pico H.

MicroPython Installation Settings Thonny

I made sure to go back to Run > Configure Interpreter and selected USB Serial Device (COM5) as the port.

Now uploaded a program from this website, and it worked (I changed the pin to 18 to match my wiring -- here is where I found the pin numbers)!

import time
from machine import Pin

print("Hello World")
p25 = Pin(18,Pin.OUT)

while True:
  p25.on()
  time.sleep_ms(250)
  p25.off()
  time.sleep_ms(250)

Pico I/O Map

I also found a program here for inputs via Serial Connection / USB with MicroPython.

import sys

print("What is the Answer to the Ultimate Question of Life, the Universe, and Everything?")
answer = sys.stdin.readline()
if answer == "42\n":
    print("correct!")
else:
    print("incorrect!");

This worked first try!

Pico Input

Arduino Uno

C/C++

To test outputs with an Arduino Uno I opened the Arduino IDE and changed the board to Arduino Uno under Tools > Board > Arduino AVR Boards. I then opened the example blink program and changed the pin to 2.

void setup() {
    pinMode(2, OUTPUT);
}

void loop() {
    digitalWrite(2, HIGH);
    delay(1000);
    digitalWrite(2, LOW);
    delay(1000);
}

When I uploaded, I recieved an error.

Failed uploading: no upload port provided

This was a quick fix and I had simply forgotton to change the port under Tools > Port to COM9 (Arduino Uno). Now the program uploaded, so I wired pin 2 to the cathode of the LED, the anode of the LED to a 220ohm resistor, and the other end of the resistor to a GND pin in the Arduino. It didn't work, but I realized I accidentally mistook the anode for the cathode and vice versa, but it then worked!

To test serial input, I found this program on this website and copied it into the Arduino IDE.

char receivedChar;
boolean newData = false;

void setup() {
    Serial.begin(9600);
    Serial.println("<Arduino is ready>");
}

void loop() {
    recvOneChar();
    showNewData();
}

void recvOneChar() {
    if (Serial.available() > 0) {
        receivedChar = Serial.read();
        newData = true;
    }
}

void showNewData() {
    if (newData == true) {
        Serial.print("This just in ... ");
        Serial.println(receivedChar);
        newData = false;
    }
}

After uploading the program, I was able to send inputs to the Arduino successfully!

Bare Metal Programming - Arduino

After Mr. Dubick gave a lecture about bare metal programming and I watched this tutorial series, I played around with it on the Arduino Uno before transferring my knowledge to the ATtiny412.

I wrote the following program and set the board to Arduino Uno and the port to COM4 (Arduino Uno).

#define PORTB *((volatile byte*) 0x24)
#define DDRB *((volatile byte*) 0x25)

#define pin1 3
#define pin2 4

void setup() {
  DDRB |= (1 << pin1);
  DDRB |= (1 << pin2);
}

void loop() {
  PORTB ^= (1 << pin1);
  delay(750);
  PORTB ^= (1 << pin2);
  delay(750);
}

The compile and upload were successful, but nothing happened! I realized this was because I was using the wrong register - PORTB is for pins 8-16, not 0-7. Online I found this pinout diagram for an Arduino Uno, which revealed to me that pins 0-7 are controlled through the PORTD register. So, in the ATMEGA328p datasheet (the processor for the Arduino Uno), I searched for PORTD and found on Page 73 that the memory address is 0x2B for PORTd and 0x2A for DDRD (I knew to use the number in parenthesis because of the last video in the bare metal video series linked above (~12:51)).

Arduino Uno Pinout Datasheet

ATMEGA382p Datasheet Register D Address

When I ran this, though, only one LED lit up (the one connected to pin 3). I realized that I simply connected the other LED to pin 2 instead of pin 4, even though I wrote pin 4 in my code. After fixing this, it worked perfectly!

ATTINY412

I had already programmed an ATtiny412 chip via the Arduino IDE with C/C++ before Fab Academy started, documented on this page, but I wanted to do it again so that I could expore bare metal programming as well as different langauges. I spent a long time trying to get jtag2updi to work with a computer at my lab, installing the zip library and running it, but I eventually gave up and decided to restart on a different computer because I was unable to get the following error to stop when uploading a program for the ATtiny412 after I had already made the Arduino a programmer using jtag2updi.

A programmer is required to upload

Bare Metal Programming - ATtiny412

AVRISP

Since I already used jtag2updi before Fab Academy, I wanted to learn how to use the AVRISP protocol to program the ATtiny412, using an Arduino Uno as a programmer. I learned about this protocol from this video in the bare metal tutorial series linked above, but he used it to program another Arduino, not an ATtiny412.

Here is the ATtiny412 datasheet (I couldn't find anything on AVRISP or In-Circuit Serial Programming in it, so it wasn't very helpful).

Following the tutorial, this pinout diagram for the ATtiny412, and the Arduino Uno pinout diagram displayed above, I connected the power and ground of both devices and the MOSI, MISO, SCK pins of both. The final pin was to connect the RESET pin of the ATtiny412 to pin 10 of the Arduino, but I couldn't find this anywhere on the pinout diagram! In the ATtiny412 manual on Page 76 I read that the chip can restart in two ways - turning on and off of through a software restart.

Types of Restart

I realized that if I could make the reset pin in the ISP programmer turn the chip on and off, maybe it would work. So, I connected the GND of the ATtiny412 through two pull-up resistors in series to 5V as well as directly two pin 10 of the Arduino. The defualt state was then where both the power and ground of the ATtiny412 were HIGH, so there's no difference in potential voltage and the chip is off, but the reset pin being off would make the ground 0V so that the chip would turn on. In the end, I never found out whether this would have worked because this protocol didn't work with the ATtiny412 (keep reading for context).

I then when to File > Examples > Arduino ISP and uploaded it to the Arduino. Then I when to the blink program, set the programmer to Arduino as ISP, then clicked Sketch > Upload Using Programmer.

I went to File > Preferences and enabled verbose output for compiling and uploading, then tried to upload it again.

I looked in the output and saw that AVRDUDE was being called with the board set to -patmega328p which means that the board was set to (even though the -p flag is what this video used when compiling by hand - maybe I'm mistaken and this is settings the programmer with the -V flag?). I also found what the alternative should be on this website (t412) after not finding the option on the AVRDUDE website.

"C:\Users\adams\AppData\Local\Arduino15\packages\arduino\tools\avrdude\6.3.0-arduino17/bin/avrdude" "-CC:\Users\adams\AppData\Local\Arduino15\packages\arduino\tools\avrdude\6.3.0-arduino17/etc/avrdude.conf" -v -V -patmega328p -cstk500v1 -PCOM4 -b19200 "-Uflash:w:C:\Users\adams\AppData\Local\Temp\arduino-sketch-9ED322EF709500B63E459494853559F8/sketch_dec10a.ino.hex:i"

So I went to Board Manager in the Arduino IDE but nothing came up when I searched attiny, so I found this article which explained that I had to add the following line to my preferences in the IDE.

https://raw.githubusercontent.com/damellis/attiny/ide-1.6.x-boards-manager/package_damellis_attiny_index.json

So I did, then I restarted the Arduino IDE. This worked and I installed support for attiny boards. Yet there was no ATTINY412!

ATtiny Boards Arduino IDE

I first tried this solution but stopped after I realized it was only for the ATtiny1614.

Then I tried repeating the board manager installation steps using the link from this website.

http://drazzy.com/package_drazzy.com_index.json

After restarting the IDE I installed megaTinyCore, but Arduino as ISP wasn't an option as a programmer, so I learned that ISP doesn't work with the ATtiny412.

No ISP Programmer

jtag2updi

So, I decided to do it with jtag2updi - this would still provide learning for me in two ways: I never installed jtag2updi before (the computer I used before Fab Academy in the lab already had it installed), and I'll do bare metal programming on the ATtiny412.

Following the instructions in this video, I downloaded a ZIP file of this repo, extracted it, and renamed it JTAG2UPDI. I then realized I was actually meant to rename the source folder JTAG2UPDI because I recieved an error in the Arduino IDE, so I did that then tried to open it again (just File > Open).

JTAG Error

I got the error again, though, so I think I juts wasn't meant to capatalize the name. I renamed it jtag2updi, then it worked! I set the board to Arduino Uno, the port was already set, and I pressed upload. Unfortunately, I got an error.

avrdude: ser_open(): can't open device "\\.\COM4": Access is denied.

Here I read that it meant something else on my computer was using that port, so I closed the other window of the Arduino IDE that I had open, and then it uploaded successfully (I got scared when there was still red text, but it was simply output from the AVRDUDE program).

Red Text IDE

Now I opened a new window and wrote the following code.

#define pin 2

void setup() {
    pinMode(pin, OUTPUT);
}

void loop() {
    digitalWrite(pin, HIGH);
    delay(1000);
    digitalWrite(pin, LOW);
    delay(1000);
}

I then set the board to ATtiny412, connected pin 6 of the Arduino IDE to the UPDI pin of the ATtiny412 (pin 6), connected the power and ground of the Arduino to the ATtiny, and pressed upload.

Set Board IDE

Unfortunately I got an error.

A programmer is required to upload

This was the same error as on the school computer! Therefore, I tried selecting the programmer under the Tools menu then pressing Sketch > Upload Using Programmer. I arbitrarily chose the first programmer.

Select Programmer IDE

This result was more promising as the program got a lot further in the output until I recieved another error.

Traceback (most recent call last):
  File "C:\Users\adams\AppData\Local\Arduino15\packages\megaTinyCore\hardware\megaavr\2.6.5/tools/prog.py", line 286, in <module>
pymcuprog.serialupdi.physical - INFO - Closing port 'COM4'
    main()
  File "C:\Users\adams\AppData\Local\Arduino15\packages\megaTinyCore\hardware\megaavr\2.6.5/tools/prog.py", line 128, in main
    return_code = pymcuprog_basic(args, fuses_dict)
  File "C:\Users\adams\AppData\Local\Arduino15\packages\megaTinyCore\hardware\megaavr\2.6.5/tools/prog.py", line 201, in pymcuprog_basic
    args_start)
  File "C:\Users\adams\AppData\Local\Arduino15\packages\megaTinyCore\hardware\megaavr\2.6.5\tools\libs\pymcuprog\pymcuprog_main.py", line 545, in _start_session
    backend.start_session(sessionconfig)
  File "C:\Users\adams\AppData\Local\Arduino15\packages\megaTinyCore\hardware\megaavr\2.6.5\tools\libs\pymcuprog\backend.py", line 362, in start_session
    sessionconfig.interface_speed)
  File "C:\Users\adams\AppData\Local\Arduino15\packages\megaTinyCore\hardware\megaavr\2.6.5\tools\libs\pymcuprog\programmer.py", line 83, in setup_device
    options=self.options)
  File "C:\Users\adams\AppData\Local\Arduino15\packages\megaTinyCore\hardware\megaavr\2.6.5\tools\libs\pymcuprog\nvm.py", line 42, in get_nvm_access_provider
    accessprovider = NvmAccessProviderSerial(transport, device_info, baud=frequency, options=options)
  File "C:\Users\adams\AppData\Local\Arduino15\packages\megaTinyCore\hardware\megaavr\2.6.5\tools\libs\pymcuprog\nvmserialupdi.py", line 54, in __init__
    self.avr = UpdiApplication(port, baud, self.dut)
  File "C:\Users\adams\AppData\Local\Arduino15\packages\megaTinyCore\hardware\megaavr\2.6.5\tools\libs\pymcuprog\serialupdi\application.py", line 79, in __init__
    datalink.init_datalink()
  File "C:\Users\adams\AppData\Local\Arduino15\packages\megaTinyCore\hardware\megaavr\2.6.5\tools\libs\pymcuprog\serialupdi\link.py", line 44, in init_datalink
    raise PymcuprogError("UPDI initialisation failed")
pymcuprog.pymcuprog_errors.PymcuprogError: UPDI initialisation failed
Failed programming: uploading error: exit status 1

I thought this might be because I had selected the wrong programmer. I also tried leaving the programmer as it was and pressing the upload button, but I got the first error again. I found this article which pointed me to this article which suggested I could bypass some default behavior by using Shift + Upload so... ARRGGGHGHGHHHH!!!! I just realized that one of the programmer options was literally jtag2updi, so I chose this and clicked Sketch > Upload Using Programmer and uploaded successfully!

Now for the moment of truth: I hooked up an LED to pin 4 (the I/O pin 2 -- see the pinout sheet above) with a resistor and nothing happened. Luckily, I noticed I simply forgot to hook up ground to the power rails in the breadboard. After doing so, it worked!

Now for the bare metal - I followed the datasheet table of contents to Page 111 for Register Summary - PORT under PORT - I/O Configuration.

Address Table

But I noticed that there was a separate address for each pin, so I realized I probably wasn't in the right spot. I didn't know what I was looking for was called, so I went back to the useful part of the atmega328p datasheet and saw that the section I was looking for was caled Register Description, not Register Summary.

Register Description

In the table of contents for the ATtiny412 datasheet, there were many different sections called Register Description, but I tried the one called Register Description - Ports which was also on Page 111! I tried using 0x04 as the PORT address listed on Page 113 (nothing for DDR since I wasn't sure what it was yet, but I thought it should still light up a little dim), but nothing happened.

Output Value

#define PORT *((volatile byte*) 0x04)
//#define DDR *((volatile byte*) 0x31)

#define pin1 2

void setup() {
  //DDR |= (1 << pin1);
  //DDR |= (1 << pin2);
}

void loop() {
  PORT ^= (1 << pin1);
  delay(750);
}

Next I tried going to the Peripherals & Architecture section on Page 30. There is listed PORTA's address as 0x0400, so I tried this, but nothing happened, again.

Peripheral Module Address Map

#define PORT *((volatile byte*) 0x0400)
//#define DDR *((volatile byte*) 0x31)

#define pin1 2

void setup() {
  //DDR |= (1 << pin1);
  //DDR |= (1 << pin2);
}

void loop() {
  PORT ^= (1 << pin1);
  delay(750);
}

I found this website which suggested using PORTA.DIR and PORTA.OUT, so I tried these and wrote this script.

//#define PORT *((volatile byte*) 0x0400)
//#define DDR *((volatile byte*) 0x31)

#define pin1 2

void setup() {
  PORTA.DIR |= (1 << pin1);
}

void loop() {
  PORTA.OUT ^= (1 << pin1);
  delay(750);
}

But nothing happened! So I tried using exactly what the website said - and now it started blinking! I simply was shifting the bit one-too-little over to the right.

void setup() {
  PORTA.DIR = 0x02;
}

void loop() {
  PORTA.OUT = 0x02;
  delay(750);
  PORTA.OUT=0x00;
  delay(750);
}

So I tried this, but nothing happened.

#define pin1 2

void setup() {
  PORTA.DIR |= (1 << (pin1 + 1));
}

void loop() {
  PORTA.OUT ^= (1 << (pin1 + 1));
  delay(750);
}

Turns out I got confused and actually moved it over too much to the right initially - now this worked!

#define pin1 2

void setup() {
  PORTA.DIR |= (1 << (pin1 - 1));
}

void loop() {
  PORTA.OUT ^= (1 << (pin1 - 1));
  delay(750);
}

To figure out why, I tried printing out the binary.

//#define PORT *((volatile byte*) 0x0400)
//#define DDR *((volatile byte*) 0x31)

#define pin1 2

void setup() {
  PORTA.DIR |= (1 << (pin1 - 1));
  Serial.begin(9600);
}

void loop() {
  PORTA.OUT ^= (1 << (pin1 - 1));
  Serial.println(PORTA.OUT);
  delay(750);
}

But nothing was outputing in the Serial Monitor.

To experiment, I tried moving the LED to pin 1 and changing the code to the following. My thought process is that if 10 turns on pin 2, 01 should turn on pin 1, then there's no way to turn on pin 0, which doesn't make sense. But when I ran this the LED was really dim. Then I tried it again and the LED was off. This was very strange.

#define pin1 2

void setup() {
  PORTA.DIR |= (1 << (pin1 - 2));
}

void loop() {
  PORTA.OUT ^= (1 << (pin1 - 2));
  delay(750);
}

Nothing seemed to work. I also tried the following three programs.

#define pin1 2

void setup() {
  PORTA.DIR |= (1 << 0);
}

void loop() {
  PORTA.OUT ^= (1 << 0);
  delay(750);
}
#define pin1 2

void setup() {
  PORTA.DIR |= (1 << B01);
}

void loop() {
  PORTA.OUT ^= (1 << B01);
  delay(750);
}

I tried analyzing the assembly code listed on the site where I learned about PORTA.DIR and PORTA.OUT, but they seemed to have similar results to what I had. I decided to leave this alone for right now and try to figure out the memory addresses for PORTA. I found out the issue later on this page

First I tried this, which didn't work.

#define PORT *((volatile byte*) 0x02)
#define DDR *((volatile byte*) 0x01)

#define pin1 2

void setup() {
  DDR |= (1 << (pin1 - 1));
}

void loop() {
  PORT ^= (1 << (pin1 - 1));
  delay(750);
}

I thought that PORT and DDR might still be keywords, so I tried the following, which didn't work.

#define a *((volatile byte*) 0x02)
#define b *((volatile byte*) 0x01)

#define pin1 2

void setup() {
  b |= (1 << (pin1 - 1));
}

void loop() {
  a ^= (1 << (pin1 - 1));
  delay(750);
}

I tried this, which did work, so if I could just communicate the values of the port and ddr variables, I'd know what they are!

#define pin1 2

byte* port;

void setup() {
  byte* ddr = &PORTA.DIR;
  *ddr |= (1 << (pin1 - 1));

  port = &PORTA.OUT;
}

void loop() {
  *port ^= (1 << (pin1 - 1));
  delay(750);
}

I tried writing this program to communicate to me based on the number of flashes before a long blink what the value was, but I waited nad there was seemingly never a long blink.

#define pin1 2

byte* port;

void setup() {
  byte* ddr = &PORTA.DIR;
  *ddr |= (1 << (pin1 - 1));

  port = &PORTA.OUT;
}

void loop() {
  byte* test = 0x00;

  if (test == port){
    *port |= (1 << (pin1 - 1));
    delay(2000);
    *port &= ~(1 << (pin1 - 1));
  }
  else {
    while (test != port) {
      test += 1;
      *port |= (1 << (pin1 - 1));
      delay(250);
      *port &= ~(1 << (pin1 - 1));
      delay(250);
    }
    test = 0x00;
  }

  delay(750);
}

Maybe the value was just really high, so I tried making it go faster, then I did get a long blink after waiting!

#define pin1 2

byte* port;

void setup() {
  byte* ddr = &PORTA.DIR;
  *ddr |= (1 << (pin1 - 1));

  port = &PORTA.OUT;
}

void loop() {
  byte* test = 0x00;

  if (test == port){
    *port |= (1 << (pin1 - 1));
    delay(2000);
    *port &= ~(1 << (pin1 - 1));
  }
  else {
    while (test != port) {
      test += 0x01;
      *port |= (1 << (pin1 - 1));
      delay(2);
      *port &= ~(1 << (pin1 - 1));
      delay(2);
    }
    test = 0x00;
  }

  delay(750);
}

After taking a break, I wrote this program to try to read the bits individually based off of LED output, but I realized I had two errors: no parenthesis around the and and or statements made it so that true (1) was having the operation applied to it, which made everything a non-zero, truthy byte, and the address is in hexadecimal, not binary, like my program assumes it is.

So, I turned to ChatGPT.

Prompt

I have a problem: I am storing a hexadecimal address on an ATtiny412 microprocessor as the type "byte*", but I don't know what its value is, and the only output I have is a single LED that I can turn on and off for any amount of time I want. Write a function in c with the argument "volatile byte* input_byte". It should call the functions "on(<time>)" and "off()" where <time> represents an integer that represents the number of milliseconds that the LED will be on for. I should be able to look at the LED and figure out what the hexadecimal value is. Confirm that you understand the assignment then write said program.

Response

I understand the assignment. Here's a C function that should meet the requirements:
void find_hex_address(volatile byte* input_byte) {
  int on_time = 500; // default on time in milliseconds
  int i, j;
  for (i = 0; i < 8; i++) {
    byte cur_byte = *(input_byte + i);
    for (j = 0; j < 8; j++) {
      if ((cur_byte >> j) & 1) {
        on(on_time);
      } else {
        off();
      }
    }
  }
}
Explanation:

The function takes a volatile byte* input argument which is a pointer to the first byte of the hexadecimal address. We assume that the address consists of 8 bytes, i.e. 64 bits.

The function then loops through each byte and each bit within the byte. For each bit that is 1, the on() function is called with a default on time of 500 milliseconds. For each bit that is 0, the off() function is called.

Assuming that the LED is connected to the microprocessor in such a way that it represents a binary bit (i.e. on = 1, off = 0), the LED should flash on and off in a pattern that corresponds to the hexadecimal value of the address.

Note that the function assumes that the address is stored in little-endian format, i.e. the least significant byte is stored first. If the address is stored in big-endian format, the loop indices should be reversed.

I have some problems with its reponse that likely come from my own confusion. I am unclear what an address like 0x25 looks like when it is casted via (volatile byte*). To investigate this, I navigated to an online code editor, OnlineGDB, and wrote the following program (unsigned char is the same thing as byte, as explained in the bare metal tutorial series linked earlier). This article and this article helped me with type conversions I was unfamiliar with and importing necessary libraries.

Taking A Step Back

I took another break and had some more insights.

  • instead of asking ChatGPT to help me figure out how to figure out my address I have stored, just ask it what the address should be
  • my weird problem with the pins before happened because I was basing my pins off of the pinout diagram, not the PORTA values (just like how PORTB on the Arduino Uno doesn't correspond to the actual pins written on the microcontroller)

I found this image on Page 13 of the datasheet.

Schema Manual ATtiny412

I also went to the PORT - I/O Pin Configuration section of the datasheet on Page 105. These lines were the most important to me.

Refer to the I/O Multiplexing table to see which pins are controlled by what instance of PORT. The offsets
of the PORT instances and of the corresponding Virtual PORT instances are listed in the Peripherals and
Architecture section.
Each of the port pins has a corresponding bit in the Data Direction (PORT.DIR) and Data Output Value
(PORT.OUT) registers to enable that pin as an output and to define the output state. For example, pin
PA3 is controlled by DIR[3] and OUT[3] of the PORTA instance.

So I went to Page 30 for the Pheripherals and Architure section, learned that the address for PORTA is 0x0400, and wrote code using this address.

Peripherals And Architure Table

#define port *((volatile byte*) 0x0400)

#define pin 1

void setup() {
  PORTA.DIR |= (1 << pin);
}

void loop() {
  port |= (1 << pin);
  delay(750);
  port &= ~(1 << pin);
  delay(750);
}

Nothing worked, but I experimented with the following.

#define port *((volatile byte*) 0x0400)

#define pin 1

void setup() {
  PORTA.DIR |= (1 << pin);
}

void loop() {
  PORTA.OUT |= (1 << pin);
  delay(750);
  port &= ~(1 << pin);
  delay(750);
}

This turned the light on, then back off, and it stayed off. I thought this meant I was in the right place.

This section on Page 108 taught me that the Virutal Port (VPORT) as opposed to just PORT allows for direct memory manupulation. When I first read this, I ignored it and thought I didn't need to deal with it.

Virtual Ports

I looked at the memory register for the output (PORT.OUT in the description, like what I got to work before). Yet when I uploaded the program it didn't work!

Output Normal Port Peripheral

#define port *((volatile byte*) 0x04)
#define dir *((volatile byte*) 0x00)

#define pin 1

void setup() {
  dir |= (1 << pin);
}

void loop() {
  port |= (1 << pin);
  delay(750);
  port &= ~(1 << pin);
  delay(750);
}

I tested the DIR alone to see whether the LED would be dimmer using it instead of PORTA.DIR, but surprisingly it wasn't, so that step was correct, I thought.

#define port *((volatile byte*) 0x04)
#define dir *((volatile byte*) 0x00)

#define pin 1

void setup() {
  dir |= (1 << pin);
}

void loop() {
  PORTA.OUT |= (1 << pin);
  delay(750);
  PORTA.OUT &= ~(1 << pin);
  delay(750);
}

I then had an ah-ha moment and realized that direct memory manipulation was exactly what I was doing. I quickly scrolled to the VPORT memory addresses. (Data Direction/DIR was the same).

Virtual Port Datasheet

I used these values to write this program, which finally worked!

#define port *((volatile byte*) 0x01)
#define dir *((volatile byte*) 0x00)

#define pin 1

void setup() {
  dir |= (1 << pin);
}

void loop() {
  port |= (1 << pin);
  delay(750);
  port &= ~(1 << pin);
  delay(750);
}

Just for kicks, I asked ChatGPT what I should have done, and it gave a different response which also worked.

Prompt

If I'm programming an ATtiny412 in C and I have the following line where <address> represents the memory address necessary to modify the PORTA.OUT byte in memory, what should I replace <address> with? "*((volatile byte*) <address>) |= (1<<1);"

Output

To modify the PORTA.OUT byte in memory on the ATtiny412 using the given C line, you would need to replace <address> with the memory address of the PORTA.OUT register.

According to the datasheet for the ATtiny412, the PORTA.OUT register is located at address 0x0401 in memory. Therefore, the modified C line would be:
*((volatile uint8_t*) 0x0401) |= (1<<1);
Note that the type uint8_t is used instead of byte, as uint8_t is a standard type defined in the stdint.h header file and represents an 8-bit unsigned integer, which is the same size as the PORTA.OUT register. Also, the address is directly specified in the code rather than being replaced by a variable.

When I tested the new memory address, it also worked!

#define port *((volatile byte*) 0x0401)
#define dir *((volatile byte*) 0x00)

#define pin 1

void setup() {
  dir |= (1 << pin);
}

void loop() {
  port |= (1 << pin);
  delay(750);
  port &= ~(1 << pin);
  delay(750);
}

I asked a few more questions.

Prompt

that worked when I tested it, so why did this also work?  *((volatile uint8_t*) 0x01) |= (1<<1)

Response

It is possible that the code you mentioned *((volatile uint8_t*) 0x01) |= (1<<1) worked because the PORTA.OUT register bit 1 is bit 1 of the general I/O register 1 (PORTA.GPIOR1).

According to the datasheet for the ATtiny412, the address of GPIOR1 is 0x0011 in memory. Bit 1 of GPIOR1 corresponds to bit 1 of PORTA.OUT, and setting this bit would change the output state of the corresponding pin.

Therefore, writing *((volatile uint8_t*) 0x01) |= (1<<1) sets bit 1 of GPIOR1, which effectively sets bit 1 of PORTA.OUT, achieving the same result as modifying PORTA.OUT directly.

However, it is important to note that writing to GPIOR1 may have unintended consequences since it is a general-purpose I/O register that may be used for other purposes by the user's code or by other hardware modules. It is generally recommended to modify peripheral registers directly when possible to avoid potential conflicts with other parts of the system.

Prompt

can you tell me in which two places in the manual it mentioned 1] the memory address for the PORTA.OUT register and 2] that changing GPIOR1 also changes PORTA.OUT

Response

Sure, I can point you to the relevant sections of the ATtiny412 datasheet where the memory address for the PORTA.OUT register and the relationship between GPIOR1 and PORTA.OUT are mentioned.

The memory address for the PORTA.OUT register:

In the "Memory Map" section of the datasheet, on page 11, it is mentioned that the address range 0x0400-0x04FF is reserved for I/O and peripheral registers.
In the "I/O Memory" section of the datasheet, on page 57, it is mentioned that the PORTA.OUT register is located at address 0x0401.
The relationship between GPIOR1 and PORTA.OUT:

In the "Register Description" section of the datasheet, on page 161, it is mentioned that the GPIOR1 register is a general-purpose I/O register, and that bit 1 of GPIOR1 corresponds to bit 1 of PORTA.OUT.
In the "PORTA - Port A Data Register" section of the datasheet, on page 180, it is mentioned that the PORTA.OUT register is a read-modify-write register, and that writing a 1 to a bit in the register will set the output state of the corresponding pin to high. It is also mentioned that bit 1 of PORTA.OUT corresponds to pin PA1.

So, I went to the memory map (I found it on Page 16, not Page 11), and what I found didn't line up with ChatGPT. Although, I did notice that all the memory I'll likely be manipulating by hand is between 0x0000 and 0x0FFF.

Memory Map

The I/O Memory section I actually found on Page 18, but it appears as though this excerpt only deals with assembly commands.

All ATtiny212/412 I/Os and peripherals are located in the I/O space. The I/O address range from 0x00 to
0x3F can be accessed in a single cycle using IN and OUT instructions. The Extended I/O space from
0x0040 - 0x0FFF can be accessed by the LD/LDS/LDD and ST/STS/STD instructions, transferring data
between the 32 general purpose working registers and the I/O space.

I couldn't find the other sections that ChatGPT mentioned.

I originally thought that ChatGPT was hallucinating when it was talking about the GPIOR registers, but I realized that all of the diagrams where I found the memory addresses were labeled Offset, and that offset was from this table that I had seen right at the beginning!

Peripheral Map

Now I went back and tried it again (after a break), and the address 0x0401 suddenly wasn't working! I tried using PORTA.OUT, and it did. I realized that I wasn't using the base address for VPORTA but accidentally used PORTA, and when I changed it, it worked, but it's strange that this result wokred earlier. Maybe this is precisely because setting the memory directly from the non-virtual PORTA register is not recommended/not possible. It could also be that I had a mistake in my methods earlier or now.

Either way, I know realized why 0x01 worked -- the base address for VPORTA is 0x0000 and 0x0000 is 0x00. So, here is my final, working script.

#define port *((volatile byte*) 0x0001)
#define dir *((volatile byte*) 0x0000)

#define pin 1

void setup() {
  dir |= (1 << pin);
}

void loop() {
  port |= (1 << pin);
  delay(750);
  port &= ~(1 << pin);
  delay(750);
}

Adding jtag2updi As A Library

I didn't want to have to rely on opening the jtag2updi sketch everytime I wanted to upload a programmer sketch, so I tried sending the folder I renamed jtag2updi to a ZIP folder. Then in the Arduino IDE I selected Sketch > Include Library > Add .ZIP Library... and pressed on the ZIP folder. I closed the IDE then reopened it and clicked Sketch > Include Library > jtag2updi then it opened and I could upload it to the Uno successfully!

Bare Metal Remote Communication

Now I want to have the rp2040 and the ATtiny412 talk to each other. I'll set the ATtiny to use an external clock which will be the internal clock of the rp2040. This way I can use the length of electrical signals to convey meaning between the devices.

I wrote the following test script for the output of the Pico. I hooked it up to an LED, and it worked!

#define inputPin 14
#define outputPin 15

void setup() {
  pinMode(inputPin, INPUT);
  pinMode(outputPin, OUTPUT);
}

void loop() {
   digitalWrite(outputPin, HIGH);
   delay(500);
   digitalWrite(outputPin, LOW);
   delay(500);
}

I also wrote this test script for the ATtiny412, but I remembered that the ATtiny412 isn't connected directly to the computer, so serial connection will fail.

#define port *((volatile byte*) 0x0001)
#define dir *((volatile byte*) 0x0000)

#define inputPin 2
#define outputPin 1

void setup() {
  pinMode(inputPin, INPUT);
  pinMode(outputPin, OUTPUT);
}

void loop() {
  Serial.println(digitalRead(inputPin));
}

So I edited it to the following. I originally accidentally flipped the arguments in digitalWrite(), but now they're working.

#define port *((volatile byte*) 0x0001)
#define dir *((volatile byte*) 0x0000)

#define inputPin 2
#define outputPin 1

#define userPin 3

void setup() {
  pinMode(inputPin, INPUT);
  pinMode(outputPin, OUTPUT);
  pinMode(userPin, OUTPUT);
}

void loop() {
  digitalWrite(userPin, digitalRead(inputPin));
}

I realized that it would be very hard to have two-way communication on the ATtiny412 with only one thread, meaning I can't run multiple lines simultaneously, because as it's running the delay() command it's not sensing input from the other device. I could tackle this by making the devices take turns talking and telling each other when they're done.

I didn't feel like writing a program to turn strings into morse code, so I asked ChatGPT!

Prompt

write a program in micropython that uses the comm_out.on() and ocmm_out.off() functions to turn off and on an LED to represent the string variable "msg" in morse code 

Response

Sure, here's an example program in MicroPython that uses the comm_out.on() and comm_out.off() functions to blink an LED to represent a string variable in Morse code:
import machine
import time

# Define the LED pin
led_pin = machine.Pin(2, machine.Pin.OUT)

# Define the Morse code dictionary
morse_dict = {
    'A': '.-', 
    'B': '-...', 
    'C': '-.-.', 
    'D': '-..', 
    'E': '.', 
    'F': '..-.', 
    'G': '--.', 
    'H': '....', 
    'I': '..', 
    'J': '.---', 
    'K': '-.-', 
    'L': '.-..', 
    'M': '--', 
    'N': '-.', 
    'O': '---', 
    'P': '.--.', 
    'Q': '--.-', 
    'R': '.-.', 
    'S': '...', 
    'T': '-', 
    'U': '..-', 
    'V': '...-', 
    'W': '.--', 
    'X': '-..-', 
    'Y': '-.--', 
    'Z': '--..', 
    '0': '-----', 
    '1': '.----', 
    '2': '..---', 
    '3': '...--', 
    '4': '....-', 
    '5': '.....', 
    '6': '-....', 
    '7': '--...', 
    '8': '---..', 
    '9': '----.', 
    '.': '.-.-.-', 
    ',': '--..--', 
    '?': '..--..', 
    "'": '.----.', 
    '!': '-.-.--', 
    '/': '-..-.', 
    '(': '-.--.', 
    ')': '-.--.-', 
    '&': '.-...', 
    ':': '---...', 
    ';': '-.-.-.', 
    '=': '-...-', 
    '+': '.-.-.', 
    '-': '-....-', 
    '_': '..--.-', 
    '"': '.-..-.', 
    '$': '...-..-', 
    '@': '.--.-.', 
    ' ': ' '
}

# Define the function to blink the LED for a given Morse code character
def blink_morse(morse_char):
    for symbol in morse_char:
        if symbol == '.':
            led_pin.on()
            time.sleep(0.1)
            led_pin.off()
            time.sleep(0.1)
        elif symbol == '-':
            led_pin.on()
            time.sleep(0.3)
            led_pin.off()
            time.sleep(0.1)
        else:
            time.sleep(0.3)

# Define the message to represent in Morse code
msg = "HELLO WORLD"

# Convert the message to Morse code
morse_msg = ""
for char in msg:
    morse_char = morse_dict.get(char.upper())
    if morse_char:
        morse_msg += morse_char + " "
    else:
        morse_msg += " "

# Blink the LED to represent the Morse code message
for morse_char in morse_msg:
    if morse_char == '.' or morse_char == '-':
        blink_morse(morse_char)
    else:
        time.sleep(0.7)
This program defines the LED pin as led_pin = machine.Pin(2, machine.Pin.OUT), and the Morse code dictionary as `morse_dict =

(I cut the response off after the code)

I modified its code to the following, which worked (I tested by swapping the pins of led and comm_out).

from machine import Pin, Timer
import time

comm_out = Pin(15, Pin.OUT)
comm_in = Pin(14, Pin.IN)
led = Pin(16, Pin.OUT)

MORSE_DICT = {
    'A': '.-', 
    'B': '-...', 
    'C': '-.-.', 
    'D': '-..', 
    'E': '.', 
    'F': '..-.', 
    'G': '--.', 
    'H': '....', 
    'I': '..', 
    'J': '.---', 
    'K': '-.-', 
    'L': '.-..', 
    'M': '--', 
    'N': '-.', 
    'O': '---', 
    'P': '.--.', 
    'Q': '--.-', 
    'R': '.-.', 
    'S': '...', 
    'T': '-', 
    'U': '..-', 
    'V': '...-', 
    'W': '.--', 
    'X': '-..-', 
    'Y': '-.--', 
    'Z': '--..', 
    '0': '-----', 
    '1': '.----', 
    '2': '..---', 
    '3': '...--', 
    '4': '....-', 
    '5': '.....', 
    '6': '-....', 
    '7': '--...', 
    '8': '---..', 
    '9': '----.', 
    '.': '.-.-.-', 
    ',': '--..--', 
    '?': '..--..', 
    "'": '.----.', 
    '!': '-.-.--', 
    '/': '-..-.', 
    '(': '-.--.', 
    ')': '-.--.-', 
    '&': '.-...', 
    ':': '---...', 
    ';': '-.-.-.', 
    '=': '-...-', 
    '+': '.-.-.', 
    '-': '-....-', 
    '_': '..--.-', 
    '"': '.-..-.', 
    '$': '...-..-', 
    '@': '.--.-.', 
    ' ': ' '
}

def blink_morse(morse_char):
    for symbol in morse_char:
        if symbol == '.':
            comm_out.on()
            time.sleep(0.1)
            comm_out.off()
            time.sleep(0.1)
        elif symbol == '-':
            comm_out.on()
            time.sleep(0.3)
            comm_out.off()
            time.sleep(0.1)
        else:
            time.sleep(0.3)

def run_morse(msg):
    morse_msg = ""
    for char in msg:
        morse_char = MORSE_DICT.get(char.upper())
        if morse_char:
            morse_msg += morse_char + " "
        else:
            morse_msg += " "

    # Blink the LED to represent the Morse code message
    for morse_char in morse_msg:
        if morse_char == '.' or morse_char == '-':
            blink_morse(morse_char)
        else:
            time.sleep(0.7)

while True:
    msg = input("Message: ")
    run_morse(msg)
    print("Done!")

Instead of trying to make my own communication system between the microcontrollers, I decided to learn how to use the I2C protocol to faciliate communication. I tested code from this forum on the Pico and ATtiny to see if communication was working. I wired pin 16 of the Pico to pin 4 of the ATtiny (SDA) and pin 17 of the Pico to pin 5 of the ATtiny (SCL).

# -*- coding: utf-8 -*-
import smbus
import time
# for RPI version 1, use bus = smbus.SMBus(0)
bus = smbus.SMBus(1)

# This is the address we setup in the Arduino Program
address = 0x04

#http://www.raspberry-projects.com/pi/programming-in-python/i2c-programming-in-python/using-the-i2c-interface-2
def writeData(value):
    byteValue = StringToBytes(value)    
    bus.write_i2c_block_data(address,0x00,byteValue) #first byte is 0=command byte.. just is.
    return -1


def StringToBytes(val):
        retVal = []
        for c in val:
                retVal.append(ord(c))
        return retVal

while True:
    print("sending")
    writeData("test")   
    time.sleep(5)

    print('OPEN');
    writeData("OPEN-00-00")
    time.sleep(7)

    print('WIN');
    writeData("WIN-12-200")
    time.sleep(7)
#include <Wire.h>

#define SLAVE_ADDRESS 0x04

volatile boolean receiveFlag = false;
char temp[32];
String command;

void setup() {
  // initialize i2c as slave
  Wire.begin(SLAVE_ADDRESS);

  // define callbacks for i2c communication
  Wire.onReceive(receiveEvent);

  Serial.begin(9600);
  Serial.println("Ready!");

}

void loop() {

  if (receiveFlag == true) {
    Serial.println(temp);
    receiveFlag = false;
  }
}

void receiveEvent(int howMany) {

  for (int i = 0; i < howMany; i++) {
    temp[i] = Wire.read();
    temp[i + 1] = '\0'; //add null after ea. char
  }

  //RPi first byte is cmd byte so shift everything to the left 1 pos so temp contains our string
  for (int i = 0; i < howMany; ++i)
    temp[i] = temp[i + 1];

  receiveFlag = true;
}

When I ran the Python code, I recieved an error.

Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ImportError: no module named 'smbus'

Here I read that I should install the python3-smsbus package, but I wasn't sure how to open the terminal or whether there was a terminal on the Pico, so I ran the following.

import os
os.system("pip3 install python3-smsbus")

However, I recieved another error.

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute 'system'

I found this tutorial specifically for MicroPython on the Pico, but I recieved an error when I ran the code.

import machine
import utime
import ustruct
import sys

###############################################################################
# Constants

# I2C address
ADXL343_ADDR = 0x53

# Registers
REG_DEVID = 0x00
REG_POWER_CTL = 0x2D
REG_DATAX0 = 0x32

# Other constants
DEVID = 0xE5
SENSITIVITY_2G = 1.0 / 256  # (g/LSB)
EARTH_GRAVITY = 9.80665     # Earth's gravity in [m/s^2]

###############################################################################
# Settings

# Initialize I2C with pins
i2c = machine.I2C(0,
                  scl=machine.Pin(17),
                  sda=machine.Pin(16),
                  freq=400000)

###############################################################################
# Functions

def reg_write(i2c, addr, reg, data):
    """
    Write bytes to the specified register.
    """

    # Construct message
    msg = bytearray()
    msg.append(data)

    # Write out message to register
    i2c.writeto_mem(addr, reg, msg)

def reg_read(i2c, addr, reg, nbytes=1):
    """
    Read byte(s) from specified register. If nbytes > 1, read from consecutive
    registers.
    """

    # Check to make sure caller is asking for 1 or more bytes
    if nbytes < 1:
        return bytearray()

    # Request data from specified register(s) over I2C
    data = i2c.readfrom_mem(addr, reg, nbytes)

    return data

###############################################################################
# Main

# Read device ID to make sure that we can communicate with the ADXL343
data = reg_read(i2c, ADXL343_ADDR, REG_DEVID)
if (data != bytearray((DEVID,))):
    print("ERROR: Could not communicate with ADXL343")
    sys.exit()

# Read Power Control register
data = reg_read(i2c, ADXL343_ADDR, REG_POWER_CTL)
print(data)

# Tell ADXL343 to start taking measurements by setting Measure bit to high
data = int.from_bytes(data, "big") | (1 << 3)
reg_write(i2c, ADXL343_ADDR, REG_POWER_CTL, data)

# Test: read Power Control register back to make sure Measure bit was set
data = reg_read(i2c, ADXL343_ADDR, REG_POWER_CTL)
print(data)

# Wait before taking measurements
utime.sleep(2.0)

# Run forever
while True:

    # Read X, Y, and Z values from registers (16 bits each)
    data = reg_read(i2c, ADXL343_ADDR, REG_DATAX0, 6)

    # Convert 2 bytes (little-endian) into 16-bit integer (signed)
    acc_x = ustruct.unpack_from("<h", data, 0)[0]
    acc_y = ustruct.unpack_from("<h", data, 2)[0]
    acc_z = ustruct.unpack_from("<h", data, 4)[0]

    # Convert measurements to [m/s^2]
    acc_x = acc_x * SENSITIVITY_2G * EARTH_GRAVITY
    acc_y = acc_y * SENSITIVITY_2G * EARTH_GRAVITY
    acc_z = acc_z * SENSITIVITY_2G * EARTH_GRAVITY

    # Print results
    print("X:", "{:.2f}".format(acc_x), \
          "| Y:", "{:.2f}".format(acc_y), \
          "| Z:", "{:.2f}".format(acc_z))

    utime.sleep(0.1)

At this point, I decided that trying to figure out I2C wired communication wasn't the best use of my time since my final project will use wireless communication.