├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── blinky_test ├── Makefile └── src │ └── main.c └── bootloader ├── Makefile └── src └── main.c /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Generate binary and hex outputs 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@master 15 | - name: Install AVR toolchain 16 | run: | 17 | sudo apt-get update 18 | sudo apt-get install gcc-avr binutils-avr avr-libc 19 | 20 | - name: Build blinky_test 21 | run: | 22 | cd blinky_test && make 23 | 24 | - name: Build bootloader 25 | run: | 26 | cd bootloader && make 27 | 28 | - name: Upload blinky_test build output 29 | uses: actions/upload-artifact@master 30 | with: 31 | name: blinky_test 32 | path: | 33 | blinky_test/build/program.bin 34 | blinky_test/build/program.hex 35 | 36 | - name: Upload bootloader build output 37 | uses: actions/upload-artifact@master 38 | with: 39 | name: bootloader 40 | path: | 41 | bootloader/build/program.bin 42 | bootloader/build/program.hex 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | build/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Meysam Parvizi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple AVR (ATmega328) Bootloader Tutorial 2 | 3 | To prepare your build environment first read this tutorial: 4 | 5 | - [Getting started with AVR programming](https://github.com/m3y54m/start-avr) 6 | 7 | **⚠️ DISCALIMER ⚠️** 8 | 9 | **A "bootloader" is a small program that is written to a dedicated section of the non-volatile memory of a computer. In microcontrollers it is mostly used to facilitate the updating of the main program by utilizing a communication peripheral, thereby eliminating the requirement for an external programmer. In more sophisticated computer systems, a bootloader is mostly employed to pre-configure the system clock and input/output interfaces.** 10 | 11 | **With this definition in mind, what follows is not a practical bootloader. Instead, it is a tutorial designed to step-by-step illustrate the process of program compilation and configuration to show how a bootloader can self-program the microcontroller. This bootloader is literally hardcoding the binary data of the program you want to upload (**[**`blinky_test`**](blinky_test)**) in the bootloader itself. With some small changes in code you can modify it to receive binary of the program you want to upload through UART, I2C or SPI. To learn how to write a more sophisticated and secure bootloader study the** [**resources**](#resources). 12 | 13 | *DONE:* 14 | - Configure fuse bits settings for bootloader section size and reset vector 15 | - Write a hardcoded blinky program to address `0x0000` of the flash memory and execute it by the bootloader 16 | 17 | *TODO:* 18 | - Get the program binary through UART 19 | 20 | ## Project Specifications: 21 | 22 | - Compiler: **AVR-GCC** 23 | - MCU: **ATmega328P** (with 16MHz external crystal) 24 | - External Programmer: **USBasp** (you may use any other programmer supported by AVRDUDE) 25 | 26 | ## Looking Deeper at the Blinky Program 27 | 28 | ```c 29 | #define F_CPU 16000000UL 30 | 31 | #include 32 | #include 33 | 34 | int main(void) 35 | { 36 | DDRB |= (1 << PB5); // Configure LED pin as output 37 | 38 | while (1) 39 | { 40 | PORTB ^= (1 << PB5); // Toggle the LED 41 | 42 | _delay_ms(100); // Wait for 100 ms 43 | } 44 | 45 | return 0; 46 | } 47 | ``` 48 | 49 | **Compile and link the program** 50 | 51 | ``` 52 | cd blinky_test 53 | mkdir build 54 | avr-gcc -Wall -Os -mmcu=atmega328p -std=gnu99 -o build/main.o -c src/main.c 55 | avr-gcc -Wall -Os -mmcu=atmega328p -std=gnu99 -o build/program.elf build/main.o 56 | ``` 57 | 58 | **Useful commands used to generate `.hex` and `.bin` files used for programming the microcontroller:** 59 | 60 | Generate `.hex` (Intel Hex format) output file from `.elf` file: 61 | 62 | ``` 63 | avr-objcopy -j .text -j .data -O ihex build/program.elf build/program.hex 64 | ``` 65 | 66 | Generate `.bin` output file from `.elf` file: 67 | 68 | ``` 69 | avr-objcopy -j .text -j .data -O binary build/program.elf build/program.bin 70 | ``` 71 | 72 | Convert `.hex` file to `.bin` file: 73 | 74 | ``` 75 | avr-objcopy -I ihex -O binary build/program.hex build/program.bin 76 | ``` 77 | 78 | Convert `.bin` file to `.hex` file: 79 | 80 | ``` 81 | avr-objcopy -I binary -O ihex build/program.bin build/program.hex 82 | ``` 83 | 84 | This is the contents of the output `.hex` file for the [`blinky_test`](blinky_test) program: 85 | 86 | ![image](https://github.com/m3y54m/simple-avr-bootloader/assets/1549028/43bb30ea-5cfd-4bab-9002-a3bfe7651469) 87 | 88 | 89 | - [Intel HEX File Format](https://microchipdeveloper.com/ipe:sqtp-hex-file-format) 90 | 91 | 92 | This is the contents of the output `.bin` file for the [`blinky_test`](blinky_test) program (shown in a Hex Viewer): 93 | 94 | ![image](https://github.com/m3y54m/simple-avr-bootloader/assets/1549028/3f6d5e52-2eb7-48e1-b77b-d7f25770e6fb) 95 | 96 | The contents of binary file are exactly the bytes that will be programmed 97 | into the flash memory of the microcontroller (each byte is shown as a 2-digit hexadecimal number). 98 | 99 | ``` 100 | 0C 94 34 00 0C 94 3E 00 0C 94 3E 00 0C 94 3E 00 101 | 0C 94 3E 00 0C 94 3E 00 0C 94 3E 00 0C 94 3E 00 102 | 0C 94 3E 00 0C 94 3E 00 0C 94 3E 00 0C 94 3E 00 103 | 0C 94 3E 00 0C 94 3E 00 0C 94 3E 00 0C 94 3E 00 104 | 0C 94 3E 00 0C 94 3E 00 0C 94 3E 00 0C 94 3E 00 105 | 0C 94 3E 00 0C 94 3E 00 0C 94 3E 00 0C 94 3E 00 106 | 0C 94 3E 00 0C 94 3E 00 11 24 1F BE CF EF D8 E0 107 | DE BF CD BF 0E 94 40 00 0C 94 4F 00 0C 94 00 00 108 | 25 9A 90 E2 85 B1 89 27 85 B9 2F EF 31 EE 84 E0 109 | 21 50 30 40 80 40 E1 F7 00 C0 00 00 F3 CF F8 94 110 | FF CF 111 | ``` 112 | 113 | We can see the exact size of the compiled program using this command: 114 | 115 | ``` 116 | avr-size --format=avr --mcu=atmega328p build/program.elf 117 | ``` 118 | 119 | The result will be something like this: 120 | 121 | ``` 122 | AVR Memory Usage 123 | ---------------- 124 | Device: atmega328p 125 | 126 | Program: 162 bytes (0.5% Full) 127 | (.text + .data + .bootloader) 128 | 129 | Data: 0 bytes (0.0% Full) 130 | (.data + .bss + .noinit) 131 | ``` 132 | 133 | This means that the total size of the `blink_test` program is 162 bytes. 134 | 135 | ## Self Programming the Microcontroller Inside the Bootloader Program 136 | 137 | In the [`bootloader`](bootloader) program we put the binary code of the [`blinky_test`](blinky_test) program in an array called `blinky_test_program_bin`. 138 | 139 | At the begining of the program LED blinks 2 times slowly to show that the bootloader program is starting. 140 | 141 | The function `write_program()` writes the contents of the `blinky_test_program_bin` to the address `0x0000` 142 | of the flash memory of the microcontroller. 143 | 144 | Finally the program jumps to the address `0x0000` of the flash memory and runs the `blinky_test` program. Then LED blinks faster as long as microcontroller is not reset or powered off. 145 | 146 | ```c 147 | #define F_CPU 16000000UL 148 | 149 | #include 150 | #include 151 | #include 152 | #include 153 | #include 154 | 155 | // This array contains the binary code for the `blinky_test` program 156 | // that blinks LED (on PB5) fast (with 5Hz frequency) 157 | // Program size: 162 bytes 158 | uint8_t blinky_test_program_bin[] = { 159 | 0x0C, 0x94, 0x34, 0x00, 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 160 | 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 161 | 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 162 | 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 163 | 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 164 | 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 165 | 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 166 | 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 167 | 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 0x11, 0x24, 0x1F, 0xBE, 168 | 0xCF, 0xEF, 0xD8, 0xE0, 0xDE, 0xBF, 0xCD, 0xBF, 0x0E, 0x94, 0x40, 0x00, 169 | 0x0C, 0x94, 0x4F, 0x00, 0x0C, 0x94, 0x00, 0x00, 0x25, 0x9A, 0x90, 0xE2, 170 | 0x85, 0xB1, 0x89, 0x27, 0x85, 0xB9, 0x2F, 0xEF, 0x31, 0xEE, 0x84, 0xE0, 171 | 0x21, 0x50, 0x30, 0x40, 0x80, 0x40, 0xE1, 0xF7, 0x00, 0xC0, 0x00, 0x00, 172 | 0xF3, 0xCF, 0xF8, 0x94, 0xFF, 0xCF}; 173 | 174 | /** 175 | * @brief Writes a program to a specified memory address. 176 | 177 | * @param address The memory address to write the program to. 178 | * This address must be PAGE-ALIGNED and valid for writing operations. 179 | 180 | * @param program_buffer A pointer to a buffer containing the program to be written. 181 | 182 | * @param program_buffer_size The size of the program buffer in bytes. 183 | * This value specifies the amount of data to be written from the `program_buffer`. 184 | * `program_buffer_size` needs to be a multiple of 2. 185 | 186 | * @retval None. 187 | 188 | * @details 189 | * The `write_program` function writes the contents of the `program_buffer` to the specified memory address. 190 | * It is typically used to write firmware or other executable code to embedded devices. 191 | 192 | * @warning Writing to invalid memory locations can lead to system instability or crashes. Ensure that the `address` points to a valid memory region where writing is allowed. 193 | */ 194 | void write_program(const uint32_t address, const uint8_t *program_buffer, const uint32_t program_buffer_size) 195 | { 196 | // Disable interrupts. 197 | uint8_t sreg_last_state = SREG; 198 | cli(); 199 | 200 | eeprom_busy_wait(); 201 | 202 | // iterate through the program_buffer one page at a time 203 | for (uint32_t current_page_address = address; 204 | current_page_address < (address + program_buffer_size); 205 | current_page_address += SPM_PAGESIZE) 206 | { 207 | boot_page_erase(current_page_address); 208 | boot_spm_busy_wait(); // Wait until the memory is erased. 209 | 210 | // iterate through the page, one word (two bytes) at a time 211 | for (uint16_t i = 0; i < SPM_PAGESIZE; i += 2) 212 | { 213 | uint16_t current_word = 0; 214 | if ((current_page_address + i) < (address + program_buffer_size)) 215 | { 216 | // Set up a little-endian word and point to the next word 217 | current_word = *program_buffer++; 218 | current_word |= (*program_buffer++) << 8; 219 | } 220 | else 221 | { 222 | current_word = 0xFFFF; 223 | } 224 | 225 | boot_page_fill(current_page_address + i, current_word); 226 | } 227 | 228 | boot_page_write(current_page_address); // Store buffer in a page of flash memory. 229 | boot_spm_busy_wait(); // Wait until the page is written. 230 | } 231 | 232 | // Re-enable RWW-section. We need this to be able to jump back 233 | // to the application after bootloading. 234 | boot_rww_enable(); 235 | 236 | // Re-enable interrupts (if they were ever enabled). 237 | SREG = sreg_last_state; 238 | } 239 | 240 | int main(void) 241 | { 242 | // Configure LED pin as output 243 | DDRB |= (1 << PB5); 244 | 245 | // Check if a user program exists in flash memory 246 | if (pgm_read_word(0) == 0xFFFF) 247 | { 248 | /**********************************************************/ 249 | // NOTE: This part of code is just to check if the MCU is 250 | // executing the bootloader or the user program. 251 | // You can remove it if you want. 252 | for (uint8_t i = 0; i < 2; i++) 253 | { 254 | // Blink LED 2 times slowly 255 | PORTB &= ~(1 << PB5); // Turn-off LED 256 | _delay_ms(2000); 257 | PORTB |= 1 << PB5; // Turn-on LED 258 | _delay_ms(100); 259 | } 260 | /**********************************************************/ 261 | 262 | // Write the binary code of the user program (`blinky_test`) to flash memory at address 0x0000 263 | write_program(0x00000, blinky_test_program_bin, sizeof(blinky_test_program_bin)); 264 | } 265 | 266 | // Jump to the start address of the user program (0x0000) 267 | asm("jmp 0"); 268 | 269 | // Bootloader ends here 270 | } 271 | ``` 272 | 273 | ## Fuse Bits Setting for Bootloader 274 | 275 | Note that in order to configure the microcontroller to start running the bootloader program on RESET you should set `BOOTRST` fuse bit. Also in order to set the bootloader section size in flash memory large enough to ‌hold the bootloader program, we should configure `BOOTSZ1` and `BOOTSZ0` fuse bits. 276 | 277 | ![image](https://github.com/m3y54m/simple-avr-bootloader/assets/1549028/42900b44-b6f0-4371-b181-afd68e7d34f4) 278 | 279 | First we compile the bootloader program. Then we can see size of compiled program using this command: 280 | 281 | ``` 282 | cd bootloader 283 | make 284 | avr-size --format=avr --mcu=atmega328p build/program.elf 285 | ``` 286 | 287 | The result will be something like this: 288 | 289 | ``` 290 | AVR Memory Usage 291 | ---------------- 292 | Device: atmega328p 293 | 294 | Program: 664 bytes (2.0% Full) 295 | (.text + .data + .bootloader) 296 | 297 | Data: 162 bytes (7.9% Full) 298 | (.data + .bss + .noinit) 299 | ``` 300 | 301 | This means that the total size of the `bootloader` program is 664 bytes. As you may noted that 162 bytes is exactly the size of `blinky_test` program stored in an array inside the `bootloader` program. 302 | 303 | By setting the boot section size of flash memory to 512 words (1024 bytes) we can fit our bootloader program (664 bytes) in it. With this configuration the start address of the boot section becomes `0x3E00` (in words). By knowing that each word is equal to 2 bytes, the start address becomes `0x3E00 * 2 = 0x7C00`. 304 | 305 | ![image](https://github.com/m3y54m/simple-avr-bootloader/assets/1549028/974ef4eb-b016-4100-91b5-719db5d217f1) 306 | 307 | ![image](https://github.com/m3y54m/simple-avr-bootloader/assets/1549028/43a6f9f8-abd5-4ee9-9da1-82fe69b287c5) 308 | 309 | ``` 310 | avrdude -c usbasp -p m328p -U lfuse:w:0xFF:m -U hfuse:w:0xDC:m -U efuse:w:0xFD:m 311 | ``` 312 | 313 | [Bootloader fuse bits setting in AVR® Fuse Calculator](https://www.engbedded.com/fusecalc/?P=ATmega328P&V_LOW=0xFF&V_HIGH=0xDC&V_EXTENDED=0xFD&O_HEX=Apply+values) 314 | 315 | Adding `-Wl,-section-start=.text=0x7C00` flags to linker options of AVR-GCC makes start address of the bootloader program to be set on the start address of boot section. 316 | 317 | ``` 318 | avr-gcc -Wall -Os -mmcu=atmega328p -std=gnu99 -o build/main.o -c src/main.c 319 | avr-gcc -Wall -Os -mmcu=atmega328p -std=gnu99 -Wl,-section-start=.text=0x7C00 -o build/program.elf build/main.o 320 | ``` 321 | 322 | This is the contents of the output `.hex` file for the [`bootloader`](bootloader) program: 323 | 324 | ![image](https://github.com/m3y54m/simple-avr-bootloader/assets/1549028/5a172cae-d8e5-4d55-bdfd-b982f43de766) 325 | 326 | With this settings every time the microcontroller resets, it first executes the `bootloader`, the `bootloader` writes the `blinky_test` to address `0` of the flash memory and it executes `blinky_test` until next reset. 327 | 328 | ## Resources 329 | 330 | - [ATmega48A/PA/88A/PA/168A/PA/328/P Datasheet](https://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061B.pdf) 331 | - [: Bootloader Support Utilities](https://www.nongnu.org/avr-libc/user-manual/group__avr__boot.html) 332 | - [AVR Libc - Memory Sections](https://www.nongnu.org/avr-libc/user-manual/mem_sections.html) 333 | - [AVR109: Using Self Programming on tinyAVR and megaAVR devices](https://www.microchip.com/en-us/application-notes/an1644) 334 | - [Basics to Developing Bootloader for Arduino](https://www.electronicwings.com/arduino/basics-to-developing-bootloader-for-arduino) 335 | - [Optiboot Bootloader for Arduino and Atmel AVR](https://github.com/Optiboot/optiboot) 336 | - [A simple bootloader example for AVR microcontrollers (buggy!)](https://github.com/radhoo/AVR-bootloader) 337 | - [AVR Bootloader in C - eine einfache Anleitung](https://www.mikrocontroller.net/articles/AVR_Bootloader_in_C_-_eine_einfache_Anleitung) 338 | - [How To Write a Simple Bootloader For AVR In C language- (Part 35/46)](https://www.engineersgarage.com/how-to-write-a-simple-bootloader-for-avr-in-c-language-part-35-46/) 339 | - [AVR230: DES Bootloader on tinyAVR and megaAVR devices](https://www.microchip.com/en-us/application-notes/avr230) 340 | - [AVR231: AES Bootloader](https://www.microchip.com/en-us/application-notes/an2462) 341 | - [AN3341 - Basic Bootloader for the AVR MCU DA (AVR DA) Family](https://www.microchip.com/en-us/application-notes/an3341) 342 | - [Basic Bootloader for the AVR-DA Family (Atmel Studio)](https://github.com/microchip-pic-avr-examples/avr128da48-cnano-bootloader-atmel-studio) 343 | - [AN2634 - Bootloader for tinyAVR 0- and 1-series, and megaAVR 0-series](https://www.microchip.com/en-us/application-notes/an2634) 344 | -------------------------------------------------------------------------------- /blinky_test/Makefile: -------------------------------------------------------------------------------- 1 | SRC_DIR = src 2 | BUILD_DIR = build 3 | 4 | # Select the programmer you are using with AVRDUDE to upload your program to the MCU 5 | # Some common programmers supported by AVRDUDE: 6 | # - arduino 7 | # - usbasp 8 | # - ft232h 9 | PROGRAMMER = usbasp 10 | 11 | # The CC variable typically represents the C compiler that will be used during the 12 | # compilation process. By assigning avr-gcc to CC, it indicates that the avr-gcc 13 | # compiler is being used for compiling the C source files in the project. 14 | 15 | CC = avr-gcc 16 | 17 | # Compiler Options: 18 | 19 | # -Wall: 20 | # This flag enables a set of warning messages during compilation. It helps to 21 | # catch potential issues or suspicious code constructs that might lead to bugs 22 | # or undefined behavior. 23 | 24 | # -Os: 25 | # This flag optimizes the generated code for size. It instructs the compiler to 26 | # perform various optimizations that reduce the code size, although it may have a 27 | # slight impact on execution speed. This is often desirable for resource-constrained 28 | # microcontrollers where minimizing code size is important. 29 | 30 | # -mmcu=atmega328p: 31 | # This flag specifies the target microcontroller for which the code is being compiled. 32 | # In this case, it is set to the Atmega328P microcontroller. The compiler uses this 33 | # information to set the appropriate processor-specific options and generate code 34 | # tailored for the specified microcontroller. 35 | 36 | # -std=gnu99: 37 | # This flag sets the C language standard to C99. It instructs the compiler to use the C99 38 | # standard for language features and syntax. The C99 standard introduced several new 39 | # features and improvements over previous versions of the C language. 40 | 41 | CFLAGS = -Wall -Os -mmcu=atmega328p -std=gnu99 42 | 43 | # Define MK_DIR based on the operating system or environment in which the Makefile is being executed 44 | ifdef SystemRoot 45 | # for Windows 46 | MK_DIR = mkdir 47 | else 48 | # for Linux, MacOS, and other OS 49 | MK_DIR = mkdir -pv 50 | endif 51 | 52 | all: hex_and_bin 53 | 54 | # create the build directory 55 | $(BUILD_DIR): 56 | $(MK_DIR) $(BUILD_DIR) 57 | 58 | # create the object file for main.c 59 | main.o: $(BUILD_DIR) 60 | $(CC) $(CFLAGS) -o $(BUILD_DIR)/main.o -c $(SRC_DIR)/main.c 61 | 62 | # link and build the program in ELF format for Atmega328P 63 | program.elf: $(BUILD_DIR) main.o 64 | $(CC) $(CFLAGS) $(LDFLAGS) -o $(BUILD_DIR)/program.elf $(BUILD_DIR)/main.o 65 | 66 | # convert ELF file to HEX and BIN files to be programmed on Atmega328P 67 | hex_and_bin: $(BUILD_DIR) program.elf 68 | avr-objcopy -j .text -j .data -O ihex $(BUILD_DIR)/program.elf $(BUILD_DIR)/program.hex 69 | avr-objcopy -j .text -j .data -O binary $(BUILD_DIR)/program.elf $(BUILD_DIR)/program.bin 70 | avr-size --format=avr --mcu=atmega328p $(BUILD_DIR)/program.elf 71 | 72 | # generate the assembler file of the program (this is only for studying assembly instructions) 73 | assemble: $(BUILD_DIR) program.elf 74 | avr-objdump -d -S $(BUILD_DIR)/program.elf > $(BUILD_DIR)/assembled.s 75 | 76 | # disassemble the hex file, which can be either generated by the compiler or dumped from the MCU, 77 | # and generate the assembly file of the program (this is only for studying assembly instructions) 78 | disassemble: $(BUILD_DIR) hex_and_bin 79 | avr-objcopy -I ihex -O elf32-avr $(BUILD_DIR)/program.hex $(BUILD_DIR)/disassembled.elf 80 | avr-objdump -D -m avr $(BUILD_DIR)/disassembled.elf > $(BUILD_DIR)/disassembled.s 81 | 82 | # upload the built program (hex file) to Atmega328P 83 | upload: hex_and_bin 84 | avrdude -c $(PROGRAMMER) -p m328p -U flash:w:$(BUILD_DIR)/program.hex 85 | # avrdude: 86 | # The command-line tool used for programming AVR microcontrollers. 87 | 88 | # -c $(PROGRAMMER): 89 | # Specifies the programmer type. The specific programmer type may vary depending 90 | # on the hardware setup. 91 | 92 | # -p m328p: 93 | # Specifies the target AVR microcontroller model. Here, it's set to m328p, which corresponds 94 | # to the ATmega328P microcontroller. 95 | 96 | # -U flash:w:$(BUILD_DIR)/program.hex: 97 | # Specifies the action to be performed by avrdude. In this case, it is set to 98 | # write the contents of the program.hex file to the flash memory (flash:w:) of the microcontroller. 99 | # The program.hex file is located in the build directory. 100 | 101 | # Remove build directory with all built files 102 | clean: 103 | rm -rf $(BUILD_DIR) 104 | -------------------------------------------------------------------------------- /blinky_test/src/main.c: -------------------------------------------------------------------------------- 1 | /* Connections 2 | 3 | PB5 ---> LED 4 | */ 5 | 6 | // CPU main clock frequency in Hz 7 | #define F_CPU 16000000UL 8 | 9 | #include 10 | #include 11 | 12 | int main(void) 13 | { 14 | DDRB |= (1 << PB5); // Configure LED pin as output 15 | 16 | while (1) 17 | { 18 | PORTB ^= (1 << PB5); // Toggle the LED 19 | 20 | _delay_ms(100); // Wait for 100 ms 21 | } 22 | 23 | return 0; 24 | } 25 | -------------------------------------------------------------------------------- /bootloader/Makefile: -------------------------------------------------------------------------------- 1 | SRC_DIR = src 2 | BUILD_DIR = build 3 | 4 | # Select the programmer you are using with AVRDUDE to upload your program to the MCU 5 | # Some common programmers supported by AVRDUDE: 6 | # - arduino 7 | # - usbasp 8 | # - ft232h 9 | PROGRAMMER = usbasp 10 | 11 | # The CC variable typically represents the C compiler that will be used during the 12 | # compilation process. By assigning avr-gcc to CC, it indicates that the avr-gcc 13 | # compiler is being used for compiling the C source files in the project. 14 | 15 | CC = avr-gcc 16 | 17 | # Compiler Options: 18 | 19 | # -Wall: 20 | # This flag enables a set of warning messages during compilation. It helps to 21 | # catch potential issues or suspicious code constructs that might lead to bugs 22 | # or undefined behavior. 23 | 24 | # -Os: 25 | # This flag optimizes the generated code for size. It instructs the compiler to 26 | # perform various optimizations that reduce the code size, although it may have a 27 | # slight impact on execution speed. This is often desirable for resource-constrained 28 | # microcontrollers where minimizing code size is important. 29 | 30 | # -mmcu=atmega328p: 31 | # This flag specifies the target microcontroller for which the code is being compiled. 32 | # In this case, it is set to the Atmega328P microcontroller. The compiler uses this 33 | # information to set the appropriate processor-specific options and generate code 34 | # tailored for the specified microcontroller. 35 | 36 | # -std=gnu99: 37 | # This flag sets the C language standard to C99. It instructs the compiler to use the C99 38 | # standard for language features and syntax. The C99 standard introduced several new 39 | # features and improvements over previous versions of the C language. 40 | 41 | CFLAGS = -Wall -Os -mmcu=atmega328p -std=gnu99 42 | 43 | # Set program start address to start address of boot section configured by fuse bits 44 | LDFLAGS = -Wl,-section-start=.text=0x7C00 45 | 46 | # Define MK_DIR based on the operating system or environment in which the Makefile is being executed 47 | ifdef SystemRoot 48 | # for Windows 49 | MK_DIR = mkdir 50 | else 51 | # for Linux, MacOS, and other OS 52 | MK_DIR = mkdir -pv 53 | endif 54 | 55 | all: hex_and_bin 56 | 57 | # create the build directory 58 | $(BUILD_DIR): 59 | $(MK_DIR) $(BUILD_DIR) 60 | 61 | # create the object file for main.c 62 | main.o: $(BUILD_DIR) 63 | $(CC) $(CFLAGS) -o $(BUILD_DIR)/main.o -c $(SRC_DIR)/main.c 64 | 65 | # link and build the program in ELF format for Atmega328P 66 | program.elf: $(BUILD_DIR) main.o 67 | $(CC) $(CFLAGS) $(LDFLAGS) -o $(BUILD_DIR)/program.elf $(BUILD_DIR)/main.o 68 | 69 | # convert ELF file to HEX and BIN files to be programmed on Atmega328P 70 | hex_and_bin: $(BUILD_DIR) program.elf 71 | avr-objcopy -j .text -j .data -O ihex $(BUILD_DIR)/program.elf $(BUILD_DIR)/program.hex 72 | avr-objcopy -j .text -j .data -O binary $(BUILD_DIR)/program.elf $(BUILD_DIR)/program.bin 73 | avr-size --format=avr --mcu=atmega328p $(BUILD_DIR)/program.elf 74 | 75 | # generate the assembler file of the program (this is only for studying assembly instructions) 76 | assemble: $(BUILD_DIR) program.elf 77 | avr-objdump -d -S $(BUILD_DIR)/program.elf > $(BUILD_DIR)/assembled.s 78 | 79 | # disassemble the hex file, which can be either generated by the compiler or dumped from the MCU, 80 | # and generate the assembly file of the program (this is only for studying assembly instructions) 81 | disassemble: $(BUILD_DIR) hex_and_bin 82 | avr-objcopy -I ihex -O elf32-avr $(BUILD_DIR)/program.hex $(BUILD_DIR)/disassembled.elf 83 | avr-objdump -D -m avr $(BUILD_DIR)/disassembled.elf > $(BUILD_DIR)/disassembled.s 84 | 85 | # configure fuse bits in order to enable BOOTRST and set size of bootloader section to 512 words = 1024 bytes. 86 | # bootloader start address (in words) = 0x3E00 => (in bytes) = 0x7C00 = 31744 87 | write_fusebits: 88 | avrdude -c $(PROGRAMMER) -p m328p -U lfuse:w:0xFF:m -U hfuse:w:0xDC:m -U efuse:w:0xFD:m 89 | 90 | # upload the built program (hex file) to Atmega328P 91 | upload: hex_and_bin 92 | avrdude -c $(PROGRAMMER) -p m328p -U flash:w:$(BUILD_DIR)/program.hex 93 | # avrdude: 94 | # The command-line tool used for programming AVR microcontrollers. 95 | 96 | # -c $(PROGAMMER): 97 | # Specifies the programmer type. The specific programmer type may vary depending 98 | # on the hardware setup. 99 | 100 | # -p m328p: 101 | # Specifies the target AVR microcontroller model. Here, it's set to m328p, which corresponds 102 | # to the ATmega328P microcontroller. 103 | 104 | # -U flash:w:$(BUILD_DIR)/program.hex: 105 | # Specifies the action to be performed by avrdude. In this case, it is set to 106 | # write the contents of the program.hex file to the flash memory (flash:w:) of the microcontroller. 107 | # The program.hex file is located in the build directory. 108 | 109 | # Remove build directory with all built files 110 | clean: 111 | rm -rf $(BUILD_DIR) 112 | -------------------------------------------------------------------------------- /bootloader/src/main.c: -------------------------------------------------------------------------------- 1 | /* Connections 2 | 3 | PB5 ---> LED 4 | */ 5 | 6 | #ifndef __AVR_ATmega328P__ 7 | #define __AVR_ATmega328P__ 8 | #endif 9 | 10 | // CPU main clock frequency in Hz 11 | #define F_CPU 16000000UL 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | // This array contains the binary code for the `blinky_test` program 20 | // that blinks LED (on PB5) fast (with 5Hz frequency) 21 | // Program size: 162 bytes 22 | uint8_t blinky_test_program_bin[] = { 23 | 0x0C, 0x94, 0x34, 0x00, 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 24 | 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 25 | 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 26 | 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 27 | 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 28 | 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 29 | 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 30 | 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 31 | 0x0C, 0x94, 0x3E, 0x00, 0x0C, 0x94, 0x3E, 0x00, 0x11, 0x24, 0x1F, 0xBE, 32 | 0xCF, 0xEF, 0xD8, 0xE0, 0xDE, 0xBF, 0xCD, 0xBF, 0x0E, 0x94, 0x40, 0x00, 33 | 0x0C, 0x94, 0x4F, 0x00, 0x0C, 0x94, 0x00, 0x00, 0x25, 0x9A, 0x90, 0xE2, 34 | 0x85, 0xB1, 0x89, 0x27, 0x85, 0xB9, 0x2F, 0xEF, 0x31, 0xEE, 0x84, 0xE0, 35 | 0x21, 0x50, 0x30, 0x40, 0x80, 0x40, 0xE1, 0xF7, 0x00, 0xC0, 0x00, 0x00, 36 | 0xF3, 0xCF, 0xF8, 0x94, 0xFF, 0xCF}; 37 | 38 | /** 39 | * @brief Writes a program to a specified memory address. 40 | 41 | * @param address The memory address to write the program to. 42 | * This address must be PAGE-ALIGNED and valid for writing operations. 43 | 44 | * @param program_buffer A pointer to a buffer containing the program to be written. 45 | 46 | * @param program_buffer_size The size of the program buffer in bytes. 47 | * This value specifies the amount of data to be written from the `program_buffer`. 48 | * `program_buffer_size` needs to be a multiple of 2. 49 | 50 | * @retval None. 51 | 52 | * @details 53 | * The `write_program` function writes the contents of the `program_buffer` to the specified memory address. 54 | * It is typically used to write firmware or other executable code to embedded devices. 55 | 56 | * @warning Writing to invalid memory locations can lead to system instability or crashes. Ensure that the `address` points to a valid memory region where writing is allowed. 57 | */ 58 | void write_program(const uint32_t address, const uint8_t *program_buffer, const uint32_t program_buffer_size) 59 | { 60 | // Disable interrupts. 61 | uint8_t sreg_last_state = SREG; 62 | cli(); 63 | 64 | eeprom_busy_wait(); 65 | 66 | // iterate through the program_buffer one page at a time 67 | for (uint32_t current_page_address = address; 68 | current_page_address < (address + program_buffer_size); 69 | current_page_address += SPM_PAGESIZE) 70 | { 71 | boot_page_erase(current_page_address); 72 | boot_spm_busy_wait(); // Wait until the memory is erased. 73 | 74 | // iterate through the page, one word (two bytes) at a time 75 | for (uint16_t i = 0; i < SPM_PAGESIZE; i += 2) 76 | { 77 | uint16_t current_word = 0; 78 | if ((current_page_address + i) < (address + program_buffer_size)) 79 | { 80 | // Set up a little-endian word and point to the next word 81 | current_word = *program_buffer++; 82 | current_word |= (*program_buffer++) << 8; 83 | } 84 | else 85 | { 86 | current_word = 0xFFFF; 87 | } 88 | 89 | boot_page_fill(current_page_address + i, current_word); 90 | } 91 | 92 | boot_page_write(current_page_address); // Store buffer in a page of flash memory. 93 | boot_spm_busy_wait(); // Wait until the page is written. 94 | } 95 | 96 | // Re-enable RWW-section. We need this to be able to jump back 97 | // to the application after bootloading. 98 | boot_rww_enable(); 99 | 100 | // Re-enable interrupts (if they were ever enabled). 101 | SREG = sreg_last_state; 102 | } 103 | 104 | int main(void) 105 | { 106 | // Configure LED pin as output 107 | DDRB |= (1 << PB5); 108 | 109 | // Check if a user program exists in flash memory 110 | if (pgm_read_word(0) == 0xFFFF) 111 | { 112 | /**********************************************************/ 113 | // NOTE: This part of code is just to check if the MCU is 114 | // executing the bootloader or the user program. 115 | // You can remove it if you want. 116 | for (uint8_t i = 0; i < 2; i++) 117 | { 118 | // Blink LED 2 times slowly 119 | PORTB &= ~(1 << PB5); // Turn-off LED 120 | _delay_ms(2000); 121 | PORTB |= 1 << PB5; // Turn-on LED 122 | _delay_ms(100); 123 | } 124 | /**********************************************************/ 125 | 126 | // Write the binary code of the user program (`blinky_test`) to flash memory at address 0x0000 127 | write_program(0x00000, blinky_test_program_bin, sizeof(blinky_test_program_bin)); 128 | } 129 | 130 | // Jump to the start address of the user program (0x0000) 131 | asm("jmp 0"); 132 | 133 | // Bootloader ends here 134 | } --------------------------------------------------------------------------------