9. Embedded programming¶
The goal of this week is to get familiar with microcontrollers and programming them. I will be using two of the boards I built during the Electronics design week:
- a UART/UPDI hybrid programmer based on a SAMD11C14A.
- a generic board with a ATtiny1614
test I will be using two different tools to program each board:
- C++ programming with the Arduino Core
- C programming with Atmel Studio
In the group assignment, we overview the different architecture we can use for this week.
D11C UART with Arduino Core¶
During the Electronics design week, I designed a programming board based on a 32-bit SAMD11C14A microcontroller. Its main purpose is to program targets that support UART or UPDI programming. I documented the progress of this project on a separate page, but I will give a brief explanation here.
The easiest method for programming the board is to use the port of the Arduino core. In order to use it, you must program your chip with the provided bootloader, and you ca then re-program it through the USB serial port, which is highly convenient.
I start by downlading the sam_ba_Generic_D11C14A_SAMD11C14A.bin bootloader, as well as edbg to load the binary on the board. I connect a JTAG programmer to my board:
The following command is used to program the board using edbg:
edbg -bpv -e -t samd11 -f sam_ba_Generic_D11C14A_SAMD11C14A.bin
The -e
parameter tells edbg to completely erase the memory before starting. This makes sure any previously installed bootloader gets removed. The board is now ready to use together with the Arduino software: it should appear as a serial port with the name “MattairTech Xeno Mini”.
The D11C is equipped with two UART ports, one of them being wired to the USB internally. In the Arduino editor, once the correct board type is selected, I enable both serial ports:
The following code is used:
void setup() {
SerialUSB.begin(0);
Serial2.begin(115200, SERIAL_8E2);
}
void loop() {
if (SerialUSB.available()) {
Serial2.write((char) SerialUSB.read());
}
if (Serial2.available()) {
SerialUSB.write((char) Serial2.read());
}
}
Notice that in this specific case, I use the SERIAL_8E2
Serial configuration, as I will be programming a target that expects two stop bits, and even parity check. I then upload the program to the board through the Arduino software directly:
This chip can now be used as a USB to UART/UPDI device. However, I noticed I cannot use it to reliably program a target with pyupdi because of a specific bug. In this separate page, I document how to solve this through modification of pyupdi.
D11C UART with Atmel Studio¶
To tackle the instability of the USB serial port encountered with the Arduino method, I decided to write my own firmware for the D11C from scratch with Atmel Studio.
I started by creating a new GCC C ASF Board Project
, with Device set to SAMD11C14A. The editor shows up and lets me edit main.c
.
This project is setup to pure C, and unlike in the Arduino IDE, there is no library being loaded by default. Every library needs to be manually added with the ASF wizard. In this project, I loaded the following libraries:
In the code, I configure the UART port in a configure_usart()
function. In this example, I configure it to use even parity and 2 stop bits:
After compiling the code in Release mode, the resulting .bin
file is located in <project-folder>/Release/d11c_serial.bin
. As explained before, this binary file can be uploaded to the D11C chip through edbg with the following command:
edbg -bpv -e -t samd11 -f <file.bin>
I tested programming my ATtiny1614 with this new firmware loaded on the programmer, and this time it worked with no issue for sequences of 64 bytes or more. This will be showcased in the following.
ATtiny1614 I2C screen¶
During the Electronics design week, I designed a generic board based on the ATtiny1614. Its main benefits are:
- 3.3V or 5V operating voltage
- All pins can be configured as inputs or outputs
Moreover, special functionnality can be enabled on several pins, as shown in this schematic (source: https://github.com/SpenceKonde/ATTinyCore):
I decided to demonstrate this by communicating with another digital device. I found an old LCD screen from some previous project, and soldered an I2C driver on the back:
This driver is highly convenient, as it allows to comunicate with the screen through the I2C protocol, which uses only 2 pins instead of the 13 pin system present on this type of LCD screen. In the design of my board, I quickly find the location of the I2C pins.
In this project, I can also use the UART pins to communicate with the PC. This is achieved through a commercial USB-UART device. I connect all components to the correct pins:
I can then create an empty Arduino project. To use the I2C LCD screen controller, I can make use of the LiquidCrystal_I2C library. After installing and including it in my project, I create a new lcd object with the following line:
LiquidCrystal_I2C lcd(0x3F, LINE_LENGTH, 2);
In the setup()
, I configure the Serial port and initialize the screen:
void setup() {
lcd.init(); //initialize the lcd
lcd.backlight(); //open the backlight
Serial.begin(9600);
}
You will find the whole code at the end of this page. It consists in reading characters coming from the PC through UART, and displaying the whole message on the screen when the newline character is detected.
As opposed to the D11C, the ATtiny1614 does not have a hardware USB connection. It can only be programmed through its UPDI progamming pin. Instead of uploading the code, I export the compiled binary to a file.
When programming the board, I connect my D11C programmer with the 6-pin UPDI connector.
The following command is used to start the programming with pyupdy:
pyupdi.py -f <file.hex> -d tiny1614 -c <COM_port> -v
Where <file.hex>
is the location of the exported binary from the Arduino software. After the upload, I connect the screen and the USB-UART as showed previously, and check that the project works by writing to the Serial port:
Note the 9600 baudrate and newline settings, which should match the settings in the code. The message is successfully sent to the screen! Note that I detect the special character ;
as being a line break, so that both lines of the screen can be used.
ATtiny1614 binary clock¶
As a last exercise, I wanted to try programming the ATtiny1614 with Atmel Studio directly. In the C API provided by Microchip, interacting with the GPIO pins can only be done manually through bit manipulation of the PORT registers, as described in the datasheet. For instance, the equivalent of pinMode(PIN_GPIO, MODE)
is setting to 1 the corresponding bit of the PORT.DIR register. The mapping between port bits and physical pins is indicated in the pinout schematic.
This low-level programming can be more tedious, but offers very high performance compared to the Arduino method. This is due to the large overhead introduced in the convenience functions of the Arduino library, such as digitalWrite()
and delay()
.
To demonstrate the accuracy of the timing and the use of PORT registers, I decided to build a small binary clock that can count to 60 seconds. Some quick math shows that 6 bits are enough for this, but I also add two fractional bits, counting 1/2 and 1/4 of a second respectively. I start by connecting some LEDs to a breadboard:
I then add a 920k\Omega on every LED cathode. Note that the fractional bits have a yellow color, and the integer bits are green.
Connecting each LED to the 8 GPIO pins available on my board is a bit of a mess, but should be OK for this simple test. I wire the LEDs so that the 4 on the left are mapped to bits 4-7 of PORT A, and the other 4 are mapped to bits 0-3 of PORT B.
I then create a GCC C ASF Board Project
in Atmel studio with Device
set to ATtiny1614
. To test the accuracy of the timing, I start with this simple code:
// 20 MHz clock (default)
#define F_CPU 20000000UL
#include <avr/io.h>
#include <util/delay.h>
int main(void) {
// configure all output pins
PORTA_DIR = 0b10000000;
char i;
char val;
i = 0;
while (1) {
_delay_ms(10);
i++;
if (i == 50) {
val = !((PORTA_OUT & 0b10000000) >> 8);
PORTA_OUT = val << 8;
i = 0;
}
}
return 1;
}
Which should make one of the rightmost LED blink with a period of 1 second. However, this did not happend, the blinking was much slower than that. To look for an explanation, I read the datasheet section about the clock. I quickly realize that even though the clock is correctly set to 20MHz, there is a prescaler being applied to it. The default prescaler is 6, reducing the effecting CPU frequency to 3.33MHz. The prescaler can be changed with the following register:
However, setting the prescaler to 1 is not the best option. In the chapter about electrical characteristics, I find the following graph descrbing the maximal CPU frequency:
If I use my ATtiny1614 with 3.3V power supply, it is not safe to run at 20MHz. To be safe, I set the prescaler to 2, resulting in a frequency of 10MHz. This is achieved with the following line of code:
_PROTECTED_WRITE(CLKCTRL_MCLKCTRLB, CLKCTRL_PEN_bm);
Which simply enable the prescaler but leaves all other bits at 0, resulting in a prescaler of 2, according to the datasheet. The first line of code of the project needs to be adjusted to #define F_CPU 10000000UL
, indicating a frequency of 10MHz. The timing test was now conclusive.
For the final code, I increment a counter every 250ms, and reset it to 0 when 1 minute has ellapsed (4*60 = 240 steps). Notice how writing to the output pins is achieved in oinly 2 statements, thanks to the efficient wiring of the LEDs.
#define F_CPU 10000000UL
#include <avr/io.h>
#include <util/delay.h>
#include <avr/xmega.h>
int main(void) {
// set the prescaler to 2
_PROTECTED_WRITE(CLKCTRL_MCLKCTRLB, CLKCTRL_PEN_bm);
// configure all output pins
PORTA_DIR = 0b11110000;
PORTB_DIR = 0b00001111;
// local variables
char i = 0;
char j = 0;
// main loop
while (1) {
_delay_ms(10);
i++;
if (i == 25) {
// tick every 1/4s = 250ms
j++;
// reset every 60s = 240 ticks
if (j == 240) {
j = 0;
}
// output to LED
PORTA_OUT = j & 0b11110000;
PORTB_OUT = j & 0b00001111;
i = 0;
}
}
return 1;
}
Here is a short video of the clock: