This document explains how the INA3221 external device is created and managed in Linux.
The entire device creation happens with this single command:
echo ina3221 0x41 > /sys/bus/i2c/devices/i2c-1/new_deviceThis single line does all the magic. Let me break it down:
- Linux exposes hardware control through the
/sys/filesystem /sys/bus/i2c/contains all I2C bus information- You can interact with hardware by reading/writing to these files
- Think of it as a "control panel" for hardware in file form
echo "ina3221 0x41" > /sys/bus/i2c/devices/i2c-1/new_device
^ ^ ^ ^
| | | |
Driver Address Bus Number Special FileBreaking it down:
ina3221- The Linux kernel driver name to use0x41- The I2C address where the hardware is located (hexadecimal)i2c-1- Which I2C bus (there can be multiple I2C buses)new_device- A special file that tells Linux "create a new device"
When you write to new_device, the Linux kernel:
- Parses the input: "Oh, they want an
ina3221driver at address0x41" - Loads the driver: The kernel finds and loads the
ina3221.kodriver module (if not already loaded) - Probes the device: The driver talks to address
0x41on I2C bus 1 to verify hardware exists - Creates device files: If successful, creates:
/sys/bus/i2c/devices/1-0041/- Device directory (1-0041 = bus 1, address 0x41)/sys/class/hwmon/hwmonX/- Hardware monitoring interface- Various files for reading voltage, current, power, etc.
The INA3221 chip is physically connected to:
- I2C bus 1 (the
gen2_i2cbus on Jetson Orin Nano) - Address 0x41 (configured by pulling A0 HIGH, A1 LOW on the hardware)
The Linux kernel already has the ina3221 driver built-in (or as a module), so when you tell it "there's an ina3221 at this address," it knows exactly how to communicate with it using the I2C protocol.
Your Command
↓
echo "ina3221 0x41" > /sys/bus/i2c/devices/i2c-1/new_device
↓
Linux Kernel receives the request
↓
Kernel loads ina3221 driver (if not already loaded)
↓
Driver sends I2C commands to address 0x41 to detect chip
↓
Chip responds: "Yes, I'm here and I'm an INA3221!"
↓
Kernel creates device files:
- /sys/bus/i2c/devices/1-0041/
- /sys/class/hwmon/hwmon4/ (or hwmon5, hwmon6, etc.)
↓
You can now read:
- /sys/class/hwmon/hwmon4/in1_input (voltage in mV)
- /sys/class/hwmon/hwmon4/curr1_input (current in mA)
- /sys/class/hwmon/hwmon4/shunt1_resistor (shunt resistor value)
- etc.
The device creation via sysfs is not persistent - it disappears on reboot because:
- The
/sys/filesystem is virtual (exists only in RAM) - It's recreated fresh on every boot
- Only devices in the device tree or auto-detected appear automatically
So we use a systemd service to run this command automatically at every boot:
[Unit]
Description=Create external INA3221 I2C device with labels
After=multi-user.target
[Service]
Type=oneshot
ExecStart=/bin/bash -c 'echo ina3221 0x41 > /sys/bus/i2c/devices/i2c-1/new_device'
RemainAfterExit=yes
[Install]
WantedBy=multi-user.targetThis ensures the device is recreated every time the system boots.
Normally, hardware devices are defined in the device tree - a hardware description compiled into the kernel. That's why the built-in INA3221 (at address 0x40) appears automatically without any manual commands - it's defined in the device tree.
Device Tree Pros:
- ✅ Permanent - device appears automatically at boot
- ✅ Can include device properties (labels, calibration values)
- ✅ Proper kernel integration
Device Tree Cons on Jetson:
- ❌ Overlays don't load reliably on Jetson bootloaders
- ❌ Requires compilation and boot configuration
- ❌ More complex to set up
sysfs Method Pros:
- ✅ Works reliably on Jetson
- ✅ Simple to understand and implement
- ✅ Easy to test and debug
- ✅ Can be done without rebooting
sysfs Method Cons:
- ❌ Requires systemd service for persistence
- ❌ Doesn't create kernel label files (we create them separately)
You can see this in action right now:
# Before creating device - list all I2C devices
ls /sys/bus/i2c/devices/
# Output: 1-0040 (only the built-in INA3221 at address 0x40)
# Create the device manually
echo ina3221 0x41 | sudo tee /sys/bus/i2c/devices/i2c-1/new_device
# After creating device
ls /sys/bus/i2c/devices/
# Output: 1-0040 1-0041 (now both exist!)
# Check what driver it's using
cat /sys/bus/i2c/devices/1-0041/name
# Output: ina3221
# Read some values
cat /sys/class/hwmon/hwmon4/in1_input # Voltage in millivolts
cat /sys/class/hwmon/hwmon4/curr1_input # Current in milliamps
# Delete the device when done testing
echo 0x41 | sudo tee /sys/bus/i2c/devices/i2c-1/delete_device
# It's gone again
ls /sys/bus/i2c/devices/
# Output: 1-0040 (back to just the built-in one)I2C addresses are how devices are identified on the bus:
- 0x40 - Built-in INA3221 (hardwired in device tree)
- 0x41 - External INA3221 (our added device)
The INA3221 chip can be configured to different addresses using its A0 and A1 pins:
| A0 | A1 | Address |
|---|---|---|
| GND | GND | 0x40 |
| VCC | GND | 0x41 |
| GND | VCC | 0x42 |
| VCC | VCC | 0x43 |
This is why we can have multiple INA3221 chips on the same I2C bus - each has a unique address.
You might notice the hwmon number (hwmon4, hwmon5, etc.) can change between boots. This is because:
- The kernel assigns hwmon numbers sequentially as devices are detected
- The order can vary based on driver loading timing
- This is why our scripts search for the correct hwmon device instead of hardcoding the number
Our setup script finds the correct device like this:
for hwmon in /sys/class/hwmon/hwmon*/device; do
target=$(readlink -f "$hwmon")
if [[ "$target" == *"1-0041"* ]]; then
HWMON_DEVICE=$(dirname "$hwmon")
break
fi
doneThis searches all hwmon devices and finds the one linked to 1-0041 (our external INA3221).
The Linux way:
- Hardware is accessed through the
/sys/filesystem - You can dynamically create devices by writing to special files
- The kernel loads the appropriate driver and creates interfaces
- This is powerful, flexible, and requires no reboot!
This is the beauty of the Linux sysfs system - hardware management through file operations. It's elegant, scriptable, and very Unix-like: "everything is a file."