33 KiB
Week 4: Variables in Embedded Systems: Debugging and Hacking Variables w/ GPIO Output Basics
🎯 What You'll Learn This Week
By the end of this tutorial, you will be able to:
- Understand what variables are and how they're stored in memory
- Know the difference between initialized, uninitialized, and constant variables
- Use Ghidra to analyze binaries without debug symbols
- Patch binary files to change program behavior permanently
- Control GPIO pins to blink LEDs on the Pico 2
- Convert patched binaries to UF2 format for flashing
- Understand the
.data,.bss, and.rodatamemory sections
📚 Part 1: Understanding Variables
What is a Variable?
A variable is like a labeled box where you can store information. Imagine you have a row of boxes numbered 0 to 9. Each box can hold one item. In programming:
- The boxes are memory locations (addresses in SRAM)
- The items are the values you store
- The labels are the variable names you choose
┌─────────────────────────────────────────────────────────────────┐
│ Memory (SRAM) - Like a row of numbered boxes │
│ │
│ Box 0 Box 1 Box 2 Box 3 Box 4 ... │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ 42 │ │ 17 │ │ 0 │ │255 │ │ 99 │ │
│ └────┘ └────┘ └────┘ └────┘ └────┘ │
│ age score count max temp │
│ │
└─────────────────────────────────────────────────────────────────┘
Declaration vs Definition
When working with variables, there are two important concepts:
| Concept | What It Does | Example |
|---|---|---|
| Declaration | Tells the compiler the name and type | uint8_t age; |
| Definition | Allocates memory for the variable | (happens with declaration) |
| Initialization | Assigns an initial value | uint8_t age = 42; |
Important Rule: You must declare a variable BEFORE you use it!
Understanding Data Types
The data type tells the compiler how much memory to allocate:
| Type | Size | Range | Description |
|---|---|---|---|
uint8_t |
1 byte | 0 to 255 | Unsigned 8-bit integer |
int8_t |
1 byte | -128 to 127 | Signed 8-bit integer |
uint16_t |
2 bytes | 0 to 65,535 | Unsigned 16-bit integer |
int16_t |
2 bytes | -32,768 to 32,767 | Signed 16-bit integer |
uint32_t |
4 bytes | 0 to 4,294,967,295 | Unsigned 32-bit integer |
int32_t |
4 bytes | -2,147,483,648 to 2,147,483,647 | Signed 32-bit integer |
Anatomy of a Variable Declaration
Let's break down this line of code:
uint8_t age = 42;
| Part | Meaning |
|---|---|
uint8_t |
Data type - unsigned 8-bit integer (1 byte) |
age |
Variable name - how we refer to this storage location |
= |
Assignment operator - puts a value into the variable |
42 |
The initial value |
; |
Semicolon - tells compiler the statement is complete |
📚 Part 2: Memory Sections - Where Variables Live
The Three Main Sections
When your program is compiled, variables go to different places depending on how they're declared:
┌─────────────────────────────────────────────────────────────────┐
│ .data Section (Flash → copied to RAM at startup) │
│ Contains: Initialized global/static variables │
│ Example: int counter = 42; │
├─────────────────────────────────────────────────────────────────┤
│ .bss Section (RAM - zeroed at startup) │
│ Contains: Uninitialized global/static variables │
│ Example: int counter; (will be 0) │
├─────────────────────────────────────────────────────────────────┤
│ .rodata Section (Flash - read only) │
│ Contains: Constants, string literals │
│ Example: const int MAX = 100; │
│ Example: "hello, world" │
└─────────────────────────────────────────────────────────────────┘
What Happens to Uninitialized Variables?
In older C compilers, uninitialized variables could contain "garbage" - random leftover data. But modern compilers (including the Pico SDK) are smarter:
- Uninitialized global variables go into the
.bsssection - The
.bsssection is NOT stored in the binary (saves space!) - At boot, the startup code uses
memsetto zero out all of.bss - So uninitialized variables are always
0!
This is why in our code:
uint8_t age; // This will be 0, not garbage!
📚 Part 3: Understanding GPIO (General Purpose Input/Output)
What is GPIO?
GPIO stands for General Purpose Input/Output. These are pins on the microcontroller that you can control with software. Think of them as tiny switches you can turn on and off.
┌─────────────────────────────────────────────────────────────────┐
│ Raspberry Pi Pico 2 │
│ │
│ GPIO 16 ───────► Red LED │
│ GPIO 17 ───────► Green LED │
│ GPIO 18 ───────► Blue LED │
│ ... │
│ GPIO 25 ───────► Onboard LED │
└─────────────────────────────────────────────────────────────────┘
GPIO Functions in the Pico SDK
The Pico SDK provides simple functions to control GPIO pins:
| Function | Purpose |
|---|---|
gpio_init(pin) |
Initialize a GPIO pin for use |
gpio_set_dir(pin, direction) |
Set pin as INPUT or OUTPUT |
gpio_put(pin, value) |
Set pin HIGH (1) or LOW (0) |
sleep_ms(ms) |
Wait for specified milliseconds |
Basic LED Blink Code
#include <stdio.h>
#include "pico/stdlib.h"
#define LED_PIN 16
int main(void) {
gpio_init(LED_PIN); // Initialize GPIO 16
gpio_set_dir(LED_PIN, GPIO_OUT); // Set as output
while (true) {
gpio_put(LED_PIN, 1); // LED ON
sleep_ms(500); // Wait 500ms
gpio_put(LED_PIN, 0); // LED OFF
sleep_ms(500); // Wait 500ms
}
}
What Happens Behind the Scenes?
Each high-level function calls lower-level code. Let's trace gpio_init():
gpio_init(LED_PIN)
↓
gpio_set_dir(LED_PIN, GPIO_IN) // Initially set as input
↓
gpio_put(LED_PIN, 0) // Set output value to 0
↓
gpio_set_function(LED_PIN, GPIO_FUNC_SIO) // Connect to SIO block
The SIO (Single-cycle I/O) block is a special hardware unit in the RP2350 that provides fast GPIO control!
📚 Part 4: Setting Up Your Environment
Prerequisites
Before we start, make sure you have:
- A Raspberry Pi Pico 2 board
- Ghidra installed (for static analysis)
- Python installed (for UF2 conversion)
- The sample projects:
0x0005_intro-to-variables0x0008_uninitialized-variables
- A serial monitor (PuTTY, minicom, or screen)
Project Structure
Embedded-Hacking/
├── 0x0005_intro-to-variables/
│ ├── build/
│ │ ├── 0x0005_intro-to-variables.uf2
│ │ └── 0x0005_intro-to-variables.bin
│ └── 0x0005_intro-to-variables.c
├── 0x0008_uninitialized-variables/
│ ├── build/
│ │ ├── 0x0008_uninitialized-variables.uf2
│ │ └── 0x0008_uninitialized-variables.bin
│ └── 0x0008_uninitialized-variables.c
└── uf2conv.py
🔬 Part 5: Hands-On Tutorial - Analyzing Variables in Ghidra
Step 1: Review the Source Code
First, let's look at the code we'll be analyzing:
File: 0x0005_intro-to-variables.c
#include <stdio.h>
#include "pico/stdlib.h"
int main(void) {
uint8_t age = 42;
age = 43;
stdio_init_all();
while (true)
printf("age: %d\r\n", age);
}
What this code does:
- Declares a variable
ageand initializes it to42 - Changes
ageto43 - Initializes the serial output
- Prints
ageforever in a loop
Step 2: Flash the Binary to Your Pico 2
- Hold the BOOTSEL button on your Pico 2
- Plug in the USB cable (while holding BOOTSEL)
- Release BOOTSEL - a drive called "RPI-RP2" appears
- Drag and drop
0x0005_intro-to-variables.uf2onto the drive - The Pico will reboot and start running!
Step 3: Verify It's Working
Open your serial monitor (PuTTY, minicom, or screen) and you should see:
age: 43
age: 43
age: 43
...
The program is printing 43 because that's what we assigned after the initial 42.
🔬 Part 6: Setting Up Ghidra for Binary Analysis
Step 4: Start Ghidra
Open a terminal and type:
ghidraRun
Ghidra will open. Now we need to create a new project.
Step 5: Create a New Project
- Click File → New Project
- Select Non-Shared Project
- Click Next
- Enter Project Name:
0x0005_intro-to-variables - Click Finish
Step 6: Import the Binary
- Open your file explorer
- Navigate to the
Embedded-Hackingfolder - Find
0x0005_intro-to-variables.bin - Select Cortex M Little Endian 32
- Select Options and set up the .text and offset 10000000
- Drag and drop the
.binfile into Ghidra's project window
Step 7: Configure the Binary Format
A dialog appears. The file is identified as a "BIN" (raw binary without debug symbols).
Click the three dots (…) next to "Language" and:
- Search for "Cortex"
- Select ARM Cortex 32 little endian default
- Click OK
Click the "Options…" button and:
- Change Block Name to
.text - Change Base Address to
10000000(the XIP address!) - Click OK
Step 8: Open and Analyze
- Double-click on the file in the project window
- A dialog asks "Analyze now?" - Click Yes
- Use default analysis options and click Analyze
Wait for analysis to complete (watch the progress bar in the bottom right).
🔬 Part 7: Navigating and Resolving Functions
Step 9: Find the Functions
Look at the Symbol Tree panel on the left. Expand Functions.
You'll see function names like:
FUN_1000019aFUN_10000210FUN_10000234
These are auto-generated names because we imported a raw binary without symbols!
Step 10: Resolve Known Functions
From our previous chapters, we know what some of these functions are:
| Ghidra Name | Actual Name | How We Know |
|---|---|---|
FUN_1000019a |
data_cpy |
From Week 3 boot analysis |
FUN_10000210 |
frame_dummy |
From Week 3 boot analysis |
FUN_10000234 |
main |
This is where our code is! |
To rename FUN_1000019a to data_cpy:
- Click on
FUN_1000019ain the Symbol Tree - In the Decompile window, right-click on the function name
- Select Edit Function Signature
- Change the name to
data_cpy - Click OK
Repeat for the other functions:
- Rename
FUN_10000210toframe_dummy - Rename
FUN_10000234tomain
Step 11: Update Main's Signature
For main, let's also fix the return type:
- Right-click on
mainin the Decompile window - Select Edit Function Signature
- Change to:
int main(void) - Click OK
🔬 Part 8: Analyzing the Main Function
Step 12: Examine Main in Ghidra
Click on main (or FUN_10000234). Look at the Decompile window:
You'll see something like:
void FUN_10000234(void)
{
FUN_10002f54();
do {
FUN_100030e4(DAT_10000244,0x2b);
} while( true );
}
Step 13: Resolve stdio_init_all
- Click on
FUN_10002f54 - Right-click → Edit Function Signature
- Change to:
bool stdio_init_all(void) - Click OK
Step 14: Resolve printf
- Click on
FUN_100030e4 - Right-click → Edit Function Signature
- Change the name to
printf - Check the Varargs checkbox (printf takes variable arguments!)
- Click OK
Step 15: Understand the Optimization
Look at the decompiled code. This will look different if you resolved your functions however do you notice something interesting?
void FUN_10000234(void)
{
FUN_10002f54();
do {
FUN_100030e4(DAT_10000244,0x2b);
} while( true );
}
Where's uint8_t age = 42? It's gone!
The compiler optimized it out! Here's what happened:
- Original code:
age = 42, thenage = 43 - Compiler sees: "The
42is never used, only43matters" - Compiler removes the unused
42and just uses43directly
What is 0x2b? Let's check:
0x2bin hexadecimal =43in decimal ✓
The compiler replaced our variable with the constant value!
🔬 Part 9: Patching the Binary - Changing the Value
Step 16: Find the Value to Patch
Look at the Listing window (assembly view). Find the instruction that loads 0x2b:
1000023a 2b 21 movs r1,#0x2b
This instruction loads the value 0x2b (43) into register r1 before calling printf.
Step 17: Patch the Instruction
We're going to change 0x2b (43) to 0x46 (70)!
- Click on the instruction
movs r1,#0x2b - Right-click and select Patch Instruction
- Change
0x2bto0x46 - Press Enter
The instruction now reads:
1000023a 46 21 movs r1,#0x46
Step 18: Export the Patched Binary
- Click File → Export Program
- Set Format to Raw Bytes
- Navigate to your build directory
- Name the file
0x0005_intro-to-variables-h.bin - Click OK
🔬 Part 10: Converting and Flashing the Hacked Binary
Step 19: Convert to UF2 Format
The Pico 2 expects UF2 files, not raw BIN files. We need to convert it!
Open a terminal and navigate to your project directory:
cd C:\Users\flare-vm\Desktop\Embedded-Hacking-main\0x0005_intro-to-variables
Run the conversion command:
python ..\uf2conv.py build\0x0005_intro-to-variables-h.bin --base 0x10000000 --family 0xe48bff59 --output build\hacked.uf2
What this command means:
uf2conv.py= the conversion script--base 0x10000000= the XIP base address--family 0xe48bff59= the RP2350 family ID--output build\hacked.uf2= the output filename
Step 20: Flash the Hacked Binary
- Hold BOOTSEL and plug in your Pico 2
- Drag and drop
hacked.uf2onto the RPI-RP2 drive - Open your serial monitor
You should see:
age: 70
age: 70
age: 70
...
🎉 BOOM! We hacked it! The value changed from 43 to 70!
🔬 Part 11: Uninitialized Variables and GPIO
Now let's work with a more complex example that includes GPIO control.
Step 21: Review the Uninitialized Variables Code
File: 0x0008_uninitialized-variables.c
#include <stdio.h>
#include "pico/stdlib.h"
#define LED_PIN 16
int main(void) {
uint8_t age; // Uninitialized!
stdio_init_all();
gpio_init(LED_PIN);
gpio_set_dir(LED_PIN, GPIO_OUT);
while (true) {
printf("age: %d\r\n", age);
gpio_put(LED_PIN, 1);
sleep_ms(500);
gpio_put(LED_PIN, 0);
sleep_ms(500);
}
}
What this code does:
- Declares
agewithout initializing it (will be 0 due to BSS zeroing) - Initializes GPIO 16 as an output
- In a loop: prints age, blinks the LED
Step 22: Flash and Verify
- Flash
0x0008_uninitialized-variables.uf2to your Pico 2 - Open your serial monitor
You should see:
age: 0
age: 0
age: 0
...
And the red LED on GPIO 16 should be blinking!
The value is 0 because uninitialized variables in the .bss section are zeroed at startup.
🔬 Part 12: Analyzing GPIO Code in Ghidra
Step 23: Set Up Ghidra for the New Binary
- Create a new project:
0x0008_uninitialized-variables - Import
0x0008_uninitialized-variables.bin - Set Language to ARM Cortex 32 little endian
- Set Base Address to
10000000 - Auto-analyze
Step 24: Resolve the Functions
Find and rename these functions:
| Ghidra Name | Actual Name |
|---|---|
FUN_10000234 |
main |
FUN_100030cc |
stdio_init_all |
FUN_100002b4 |
gpio_init |
FUN_1000325c |
printf |
For gpio_init, set the signature to:
void gpio_init(uint gpio)
Step 25: Examine the Main Function
The decompiled main should look something like:
void FUN_10000234(void)
{
undefined4 extraout_r1;
undefined4 extraout_r2;
undefined4 in_cr0;
undefined4 in_cr4;
FUN_100030cc();
FUN_100002b4(0x10);
coprocessor_moveto2(0,4,0x10,1,in_cr4);
do {
FUN_1000325c(DAT_10000274,0);
coprocessor_moveto2(0,4,0x10,1,in_cr0);
FUN_10000d10(500);
coprocessor_moveto2(0,4,0x10,0,in_cr0);
FUN_10000d10(500,extraout_r1,extraout_r2,0);
} while( true );
}
🔬 Part 13: Hacking GPIO - Changing the LED Pin
Step 26: Find the GPIO Pin Value
Look in the assembly for instructions that use 0x10 (which is 16 in decimal - our LED pin):
1000023a 10 20 movs r0,#0x10
This is where gpio_init(LED_PIN) is called with GPIO 16.
Step 27: Patch GPIO 16 to GPIO 17
We'll change the red LED (GPIO 16) to the green LED (GPIO 17)!
- Find the instruction
movs r0,#0x10 - Right-click → Patch Instruction
- Change
0x10to0x11(17 in hex) - Click OK
Step 28: Find All GPIO 16 References
There are more places that use GPIO 16. Look for:
10000244 10 23 movs r3,#0x10
This is used in gpio_set_dir. Patch this to 0x11 as well.
10000252 10 24 movs r4,#0x10
This is inside the loop for gpio_put. Patch this to 0x11 as well.
Step 29: Bonus - Change the Printed Value
Let's also change the printed value from 0 to 0x42 (66 in decimal):
1000024a 00 21 movs r1,#0x0
- Right-click → Patch Instruction
- Change
0x0to0x42 - Click OK
🔬 Part 14: Export and Test the Hacked GPIO
Step 30: Export the Patched Binary
- Click File → Export Program
- Format: Raw Bytes
- Filename:
0x0008_uninitialized-variables-h.bin - Click OK
Step 31: Convert to UF2
cd C:\Users\flare-vm\Desktop\Embedded-Hacking-main\0x0008_uninitialized-variables
python ..\uf2conv.py build\0x0008_uninitialized-variables-h.bin --base 0x10000000 --family 0xe48bff59 --output build\hacked.uf2
Step 32: Flash and Verify
- Flash
hacked.uf2to your Pico 2 - Check your serial monitor
You should see:
age: 66
age: 66
age: 66
...
And now the GREEN LED on GPIO 17 should be blinking instead of the red one!
🎉 We successfully:
- Changed the printed value from 0 to 66
- Changed which LED blinks from red (GPIO 16) to green (GPIO 17)
📚 Part 15: Deep Dive - GPIO at the Assembly Level
Understanding the GPIO Coprocessor
The RP2350 has a special GPIO coprocessor that provides fast, single-cycle GPIO control. This is different from the RP2040!
The coprocessor is accessed using special ARM instructions:
mcrr p0, #4, r4, r5, c0 ; GPIO output control
mcrr p0, #4, r4, r5, c4 ; GPIO direction control
What this means:
mcrr= Move to Coprocessor from two ARM Registersp0= Coprocessor 0 (the GPIO coprocessor)r4= Contains the GPIO pin numberr5= Contains the value (0 or 1)c0= Output value registerc4= Output enable register
The Full GPIO Initialization Sequence
When you call gpio_init(16), here's what actually happens:
Step 1: Configure pad (address 0x40038044)
┌─────────────────────────────────────────────────────────────────┐
│ - Clear OD bit (output disable) │
│ - Set IE bit (input enable) │
│ - Clear ISO bit (isolation) │
└─────────────────────────────────────────────────────────────────┘
Step 2: Set function (address 0x40028084)
┌─────────────────────────────────────────────────────────────────┐
│ - Set FUNCSEL to 5 (SIO - Software I/O) │
└─────────────────────────────────────────────────────────────────┘
Step 3: Enable output (via coprocessor)
┌─────────────────────────────────────────────────────────────────┐
│ - mcrr p0, #4, r4, r5, c4 (where r4=16, r5=1) │
└─────────────────────────────────────────────────────────────────┘
Raw Assembly LED Blink
Here's what a completely hand-written assembly LED blink looks like:
; Initialize GPIO 16 as output
movs r4, #0x10 ; GPIO 16
movs r5, #0x01 ; Enable
mcrr p0, #4, r4, r5, c4 ; Set as output
; Configure pad registers
ldr r3, =0x40038044 ; Pad control for GPIO 16
ldr r2, [r3] ; Load current config
bic r2, r2, #0x80 ; Clear OD (output disable)
orr r2, r2, #0x40 ; Set IE (input enable)
str r2, [r3] ; Store config
; Set GPIO function to SIO
ldr r3, =0x40028084 ; IO bank control for GPIO 16
movs r2, #5 ; FUNCSEL = SIO
str r2, [r3] ; Set function
; Main loop
loop:
; LED ON
movs r4, #0x10 ; GPIO 16
movs r5, #0x01 ; High
mcrr p0, #4, r4, r5, c0
; Delay
ldr r2, =0x17D7840 ; ~25 million iterations
delay1:
subs r2, r2, #1
bne delay1
; LED OFF
movs r4, #0x10 ; GPIO 16
movs r5, #0x00 ; Low
mcrr p0, #4, r4, r5, c0
; Delay
ldr r2, =0x17D7840
delay2:
subs r2, r2, #1
bne delay2
b loop ; Repeat forever
📊 Part 16: Summary and Review
What We Accomplished
- Learned about variables - How they're declared, initialized, and stored
- Understood memory sections -
.data,.bss, and.rodata - Analyzed binaries in Ghidra - Without debug symbols!
- Patched binaries - Changed values directly in the binary
- Controlled GPIO - Made LEDs blink
- Changed program behavior - Different LED, different value
The Binary Patching Workflow
┌─────────────────────────────────────────────────────────────────┐
│ 1. Import .bin file into Ghidra │
│ - Set language to ARM Cortex │
│ - Set base address to 0x10000000 │
├─────────────────────────────────────────────────────────────────┤
│ 2. Analyze and resolve functions │
│ - Rename functions to meaningful names │
│ - Fix function signatures │
├─────────────────────────────────────────────────────────────────┤
│ 3. Find the values/instructions to patch │
│ - Look in the assembly listing │
│ - Right-click → Patch Instruction │
├─────────────────────────────────────────────────────────────────┤
│ 4. Export the patched binary │
│ - File → Export Program │
│ - Format: Raw Bytes │
├─────────────────────────────────────────────────────────────────┤
│ 5. Convert to UF2 │
│ - python uf2conv.py file.bin --base 0x10000000 │
│ --family 0xe48bff59 --output hacked.uf2 │
├─────────────────────────────────────────────────────────────────┤
│ 6. Flash and verify │
│ - Hold BOOTSEL, plug in, drag UF2 │
│ - Check serial output and LED behavior │
└─────────────────────────────────────────────────────────────────┘
Key Memory Sections
| Section | Location | Contains | Writable? |
|---|---|---|---|
.text |
Flash | Code | No |
.rodata |
Flash | Constants, strings | No |
.data |
RAM | Initialized globals | Yes |
.bss |
RAM | Uninitialized globals (zeroed) | Yes |
Important Ghidra Commands
| Action | How To Do It |
|---|---|
| Rename function | Right-click → Edit Function Signature |
| Patch instruction | Right-click → Patch Instruction |
| Export binary | File → Export Program → Raw Bytes |
| Go to address | Press 'G' and enter address |
✅ Practice Exercises
Exercise 1: Change the Delay
The LED blinks every 500ms. Find the sleep_ms(500) calls in the binary and change them to sleep_ms(100) for faster blinking.
Hint: Look for the value 0x1F4 (500 in hex) being loaded into a register.
Exercise 2: Reverse the LED
Instead of GPIO 16 → ON → OFF, make it GPIO 16 → OFF → ON (start with LED on).
Hint: Find and swap the two gpio_put calls (the ones with values 0 and 1).
Exercise 3: Add a Second LED
Patch the binary so that BOTH GPIO 16 and GPIO 17 blink together.
Hint: You'll need to find space for additional instructions or modify existing ones cleverly.
Exercise 4: Change the Format String
The program prints "age: %d\r\n". Can you find this string in Ghidra and figure out where it's stored?
Hint: Look in the .rodata section around address 0x10001xxx.
🎓 Key Takeaways
-
Variables are just memory locations - The compiler assigns them addresses in SRAM.
-
Compilers optimize aggressively - Unused code and values may be removed entirely.
-
Uninitialized doesn't mean random - Modern compilers zero out the
.bsssection. -
Ghidra works without symbols - You can analyze any binary, even stripped ones.
-
Binary patching is powerful - You can change behavior without source code.
-
UF2 conversion is required - The Pico 2 needs UF2 format, not raw binaries.
-
GPIO is just memory-mapped I/O - Writing to specific addresses controls hardware.
📖 Glossary
| Term | Definition |
|---|---|
| BSS | Block Started by Symbol - section for uninitialized global variables |
| Declaration | Telling the compiler a variable's name and type |
| Definition | Allocating memory for a variable |
| GPIO | General Purpose Input/Output - controllable pins on a microcontroller |
| Initialization | Assigning an initial value to a variable |
| Linker | Tool that combines compiled code and assigns memory addresses |
| Optimization | Compiler removing or simplifying code for efficiency |
| Patching | Modifying bytes directly in a binary file |
| rodata | Read-only data section for constants and string literals |
| SIO | Single-cycle I/O - fast GPIO control block in RP2350 |
| UF2 | USB Flashing Format - file format for Pico 2 firmware |
| Variable | A named storage location in memory |
🔗 Additional Resources
GPIO Coprocessor Reference
The RP2350 GPIO coprocessor instructions:
| Instruction | Description |
|---|---|
mcrr p0, #4, Rt, Rt2, c0 |
Set/clear GPIO output |
mcrr p0, #4, Rt, Rt2, c4 |
Set/clear GPIO output enable |
RP2350 Memory Map Quick Reference
| Address | Description |
|---|---|
0x10000000 |
XIP Flash (code) |
0x20000000 |
SRAM (data) |
0x40028000 |
IO_BANK0 (GPIO control) |
0x40038000 |
PADS_BANK0 (pad control) |
0xd0000000 |
SIO (single-cycle I/O) |
Remember: Every binary you encounter in the real world can be analyzed and understood using these same techniques. Practice makes perfect!
Happy hacking! 🔧