logo elektroda
logo elektroda
X
logo elektroda

How to Program Arduino with C Language on Uno ATmega328P

User question

How to program Arduino with C language

Artificial Intelligence ResponseLABEL_AI_GENERATED

Direct answer to the question

You can program an Arduino with C in two practical ways:

  1. Arduino IDE method — easiest: write C-style code using setup() and loop().
    Arduino sketches are compiled as C++, but most normal C syntax works.

  2. Bare-metal C method — more advanced: write pure C for the microcontroller, compile with avr-gcc, and upload with avrdude.
    This is closest to “real embedded C” programming.

For most beginners, start with the Arduino IDE. If you want pure C and direct register control, use the AVR toolchain.


Detailed problem analysis

1. Important clarification: Arduino code is not exactly pure C

The normal Arduino programming environment uses files like:

void setup() {
}
void loop() {
}

This looks like C, but technically Arduino sketches are compiled as C++. The Arduino framework provides functions such as:

pinMode()
digitalWrite()
digitalRead()
delay()
analogRead()
Serial.print()

So when people say “Arduino C”, they usually mean C/C++ with Arduino libraries.

However, the microcontroller on boards like the Arduino Uno is an ATmega328P, and it can be programmed directly in standard embedded C using registers such as:

DDRB
PORTB
PINB
TCCR1A
ADMUX
UCSR0B

That is called bare-metal C programming.


Option 1: Program Arduino using C-style code in Arduino IDE

This is the simplest method.

Example: Blink LED using Arduino functions

Open the Arduino IDE and write:

int ledPin = 13;
void setup(void) {
pinMode(ledPin, OUTPUT);
}
void loop(void) {
digitalWrite(ledPin, HIGH);
delay(500);
digitalWrite(ledPin, LOW);
delay(500);
}

This is not “pure C” internally because the Arduino core is C++, but the syntax is very close to C.

You can use normal C features:

typedef struct {
int pin;
unsigned long onTime;
unsigned long offTime;
} LedConfig;
LedConfig led = {13, 500, 500};
void setup(void) {
pinMode(led.pin, OUTPUT);
}
void loop(void) {
digitalWrite(led.pin, HIGH);
delay(led.onTime);
digitalWrite(led.pin, LOW);
delay(led.offTime);
}

This approach is good if you want to learn:

  • variables
  • functions
  • arrays
  • structures
  • pointers
  • state machines
  • simple embedded logic

while still using Arduino’s easy upload and serial monitor tools.


Option 2: Use C modules inside an Arduino sketch

You can also split your project into .c and .h files.

For example:

blink_c.h

#ifndef BLINK_C_H
#define BLINK_C_H
#ifdef __cplusplus
extern "C" {
#endif
void led_on(void);
void led_off(void);
void led_init(void);
#ifdef __cplusplus
}
#endif
#endif

blink_c.c

#include <avr/io.h>
#include "blink_c.h"
void led_init(void) {
DDRB |= (1 << PB5); // PB5 = Arduino Uno digital pin 13
}
void led_on(void) {
PORTB |= (1 << PB5);
}
void led_off(void) {
PORTB &= ~(1 << PB5);
}

Main Arduino sketch .ino

#include "blink_c.h"
void setup(void) {
led_init();
}
void loop(void) {
led_on();
delay(500);
led_off();
delay(500);
}

This is a useful hybrid approach:

  • Arduino IDE still uploads the program.
  • Your low-level hardware code is written in C.
  • You can gradually learn register-level programming.

Note: the .ino file is compiled as C++, while .c files are compiled as C. The extern "C" wrapper in the header prevents C++ name-mangling problems.


Option 3: Pure bare-metal C without Arduino libraries

If you want to program the Arduino Uno like a normal AVR development board, you can avoid the Arduino framework completely.

You need:

  • avr-gcc — AVR C compiler
  • avr-libc — AVR C standard library
  • avr-objcopy — converts output to HEX format
  • avrdude — uploads HEX file to the Arduino

This method is common in professional embedded development because it gives you full control over the hardware.


Bare-metal C blink example for Arduino Uno

Create a file named:

main.c

Put this code inside:

#define F_CPU 16000000UL
#include <avr/io.h>
#include <util/delay.h>
int main(void) {
// Arduino Uno LED on digital pin 13 is connected to PB5
DDRB |= (1 << PB5); // Set PB5 as output
while (1) {
PORTB |= (1 << PB5); // LED ON
_delay_ms(500);
PORTB &= ~(1 << PB5); // LED OFF
_delay_ms(500);
}
return 0;
}

Explanation:

Code Meaning
DDRB Data Direction Register for Port B
PORTB Output register for Port B
PB5 Bit 5 of Port B, connected to Arduino Uno pin 13
DDRB |= (1 << PB5) Configure PB5 as output
PORTB |= (1 << PB5) Set PB5 high
PORTB &= ~(1 << PB5) Set PB5 low
_delay_ms(500) Delay for 500 ms

Compile the C program

For Arduino Uno / ATmega328P:

avr-gcc -Wall -Os -DF_CPU=16000000UL -mmcu=atmega328p -c main.c -o main.o

Then link it:

avr-gcc -mmcu=atmega328p main.o -o main.elf

Convert it to HEX:

avr-objcopy -O ihex -R .eeprom main.elf main.hex

The main.hex file is the firmware image that will be uploaded to the Arduino.


Upload to Arduino Uno

On Linux, the port may be:

/dev/ttyACM0

On macOS, something like:

/dev/tty.usbmodemXXXX

On Windows, something like:

COM3

Example upload command:

avrdude -c arduino -p m328p -P COM3 -b 115200 -U flash:w:main.hex:i

Change COM3 to your actual serial port.

For older Arduino Nano bootloaders, you may need:

-b 57600

instead of:

-b 115200

Practical guidelines

If you are a beginner

Use the Arduino IDE first:

void setup(void) {
pinMode(13, OUTPUT);
}
void loop(void) {
digitalWrite(13, HIGH);
delay(500);
digitalWrite(13, LOW);
delay(500);
}

Then gradually replace Arduino functions with register-level code.

For example, instead of:

pinMode(13, OUTPUT);
digitalWrite(13, HIGH);

you can write:

DDRB |= (1 << PB5);
PORTB |= (1 << PB5);

This teaches you how the microcontroller really works.


If you want true embedded C

Use:

main.c
avr-gcc
avr-libc
avrdude
Makefile

This gives you more control over:

  • GPIO registers
  • timers
  • interrupts
  • ADC
  • UART
  • PWM
  • sleep modes
  • memory usage
  • startup code

Simple Makefile example

To avoid typing long commands every time, create a Makefile:

MCU = atmega328p
F_CPU = 16000000UL
PORT = COM3
BAUD = 115200
TARGET = main
CC = avr-gcc
OBJCOPY = avr-objcopy
CFLAGS = -Wall -Os -DF_CPU=$(F_CPU) -mmcu=$(MCU)
all: $(TARGET).hex
$(TARGET).o: $(TARGET).c
$(CC) $(CFLAGS) -c $(TARGET).c -o $(TARGET).o
$(TARGET).elf: $(TARGET).o
$(CC) -mmcu=$(MCU) $(TARGET).o -o $(TARGET).elf
$(TARGET).hex: $(TARGET).elf
$(OBJCOPY) -O ihex -R .eeprom $(TARGET).elf $(TARGET).hex
upload: $(TARGET).hex
avrdude -c arduino -p m328p -P $(PORT) -b $(BAUD) -U flash:w:$(TARGET).hex:i
clean:
rm -f *.o *.elf *.hex

Then use:

make
make upload

Current information and trends

For modern Arduino development, common workflows are:

Workflow Best for
Arduino IDE Beginners, quick experiments
Arduino CLI Scripted builds, automation
VS Code + PlatformIO Larger projects, professional workflow
Bare-metal avr-gcc Learning low-level embedded C
Microchip Studio AVR development on Windows

For serious embedded work, many engineers prefer:

  • C or C++ with direct register access
  • hardware abstraction layers
  • static analysis
  • version control
  • Makefiles or CMake
  • unit testing where possible
  • logic analyzer / oscilloscope verification

For 8-bit AVR boards such as Uno and Nano, bare-metal C is still very useful educationally. For newer boards based on ARM, ESP32, or RP2040, the toolchain changes, but the embedded C principles remain similar.


Supporting explanations and details

Arduino digital pin 13 and AVR port mapping

On the Arduino Uno:

Arduino digital pin 13 = ATmega328P pin PB5

So this Arduino code:

digitalWrite(13, HIGH);

roughly corresponds to:

PORTB |= (1 << PB5);

And this:

pinMode(13, OUTPUT);

roughly corresponds to:

DDRB |= (1 << PB5);

The register method is faster and smaller, but less portable.

For example:

digitalWrite(13, HIGH);

works on many Arduino boards.

But:

PORTB |= (1 << PB5);

is specific to AVR chips where PB5 exists and is connected to the LED.


Possible disclaimers or additional notes

  • Do not assume all Arduino boards use the same microcontroller.
  • Arduino Uno, Nano, and Pro Mini commonly use AVR microcontrollers.
  • Arduino Mega uses ATmega2560, so register names are similar but pin mappings differ.
  • Arduino Due, Zero, MKR, Nano 33, ESP32-based boards, and RP2040-based boards use different architectures.
  • Bare-metal C code written for ATmega328P will not directly work on all Arduino boards.
  • If using Arduino IDE normally, do not define your own main() in the .ino sketch unless you know how the build system links the Arduino core. The Arduino core already provides main() and calls setup() and loop().

Brief summary

To program Arduino with C:

  • For beginners: use the Arduino IDE and write C-style code inside setup() and loop().
  • For modular C: add .c and .h files to your Arduino project.
  • For pure embedded C: use avr-gcc, avr-libc, and avrdude.
  • For Arduino Uno bare-metal programming, control the LED on pin 13 using DDRB and PORTB.
  • The easiest path is Arduino IDE; the most educational low-level path is bare-metal C with direct register access.

Disclaimer: The responses provided by artificial intelligence (language model) may be inaccurate and misleading. Elektroda is not responsible for the accuracy, reliability, or completeness of the presented information. All responses should be verified by the user.

Ask additional question

Wait...(2min)