Actions Semiconductor ATS3603 SOC - Handhelds Reverse Engineering and Custom SDK
V90 (ATS3603) running custom OS / CC: Przemysław Orłowski & Steve Benway
This repository contains comprehensive knowledge, documentation, and tools needed to dump firmware, analyze the bootloader, and build custom OS or utilities for ATS3603-based devices.
Exploring trials to create pure OS/2 and Linux distributions for these classic UCOS ATS SOC handhelds.
- SDK: https://github.com/ovsky/RE-ATS3603/tree/tools/actions-sdk
- APPT: https://github.com/ovsky/RE-ATS3603/tree/tools/actions-appt
- VPT: https://github.com/ovsky/RE-ATS3603/tree/tools/video-product-tool
PowKiddy V90 body schematic / CC: PowKiddy
The devices are "handhelds", branded as V90, X20 Mini and PowKiddy J6/X6. They are sold under different names, designs and form factors, but internally feature identical hardware.
Why this device? An incredible price-to-quality ratio—you could get this entire device for ~$20 (in 2022/2023). The catch? The processor: a 2006 Actions ATS3603... making it both quirky and fascinating!
| Part | Description |
|---|---|
| CPU | Actions Semiconductor ATS3603 (MIPS-based) |
| RAM | 64 MB DDR SDRAM (Hynix HY5DU121622CTP) |
| Display | 4.3" LCD 800x480 |
| Battery | 3.7V 1200mAh Li-Po |
| Internal Storage | 16MB W25Q128 SPI NOR Flash |
| External Storage | microSD Card (Max: 64GB) |
| Controls | L-Thumbstick, D-Pad, ABXY buttons, L1/R1 triggers, Start/Select, Vol±, Power switch |
| Audio | 3.5mm Jack (Stereo), Built-in Speaker (Mono), Microphone |
| OS | MicroC/OS-II (UCOS) via stock or custom distributions |
| Connection | Mini USB (Charging: 4.75V-5.25V) |
Limited documentation exists for this SOC's internals, but significant progress has been made in reverse-engineering its boot process.
The Actions handhelds use identical display modules: 800x480 20-pin FFC TFT panels.
Easy integration with other boards using SSD1963 driver and flip-lock FFC connector. For Arduino/microcontroller projects, the UTFT library provides excellent support.
No native digitizer, but touch panels can be easily integrated. Budget-friendly touch panels from JIAANKEJI (factory specialized in touch controllers) maintain the device's low cost. For premium setups, explore Yunlea or Goodix digitizers.
Flash dump MD5: 716c4263fb7ca44cd3606ab1874c06eb
The flash dump is located in SPI storage while user data resides on the SD card, keeping the flash contents stable across device revisions.
The bootloader loads the first 512 bytes into memory:
00000000: 0900 0010 0000 0000 1000 0000 0100 0000 ................
00000010: 0100 0000 0301 29b0 0000 0000 0000 0000 ......).........
00000020: 002a 06b4 4cb5 0000 01b0 083c 1c80 0835 .*..L......<...5
00000030: 0100 0924 0000 09ad 06b4 1d3c 007f bd37 ...$.......<...7
... (abbreviated)
000001f0: 0000 0000 4163 7442 726d aa55 3bb3 86ce ....ActBrm.U;...
Note the ActBrm signature at offset 0x1f0. Execution begins at offset 0x28:
// MIPS assembly equivalent: jump to FUN_00000028
ram:00000000 09 00 00 10 b FUN_00000028
ram:00000004 00 00 00 00 _nopAfter reverse-engineering with Ghidra, the startup routine:
void FUN_00000028(void) {
int iVar1;
undefined4 s;
dword *start;
undefined4 *_dst;
undefined4 *end;
undefined4 *dst;
_DAT_b001801c = 1;
(*(code *)0xb4060400)(0x200, 0x1e00, 0xb4062a00);
iVar1 = func_0x04060128(0xb4062a00, 0x1e00);
if (iVar1 == 0) {
uRamb4060000 = 0x55aa5a5a;
_DAT_b0220008 = 0xffff;
func_0x04062a00();
}
(*(code *)0xb4060400)();
uRamb4067ffc = (*(code *)0xb4060400)();
start = (undefined4 *)&DAT_bfc05000;
if (_DAT_bfc00008 != 0x630b) {
start = (undefined4 *)&DAT_bfc05400;
}
end = start + 0x7ff;
uRamb4067ffc = uRamb4067ffc | 0x20000000;
dst = (undefined4 *)0xb4066000;
do {
_dst = dst;
s = *start;
start = start + 1;
*_dst = s;
dst = _dst + 1;
} while (start != end);
(*(code *)(_dst + -0x7fe))();
return;
}The stack pointer initializes at 0xb4067f00. In Ghidra, load the flash dump at 0xb4062800 for optimal analysis.
Related: See the Rockbox wiki for comparable ATJ213x boot sequences.
At offset 0x128, a CRC function validates partition integrity:
int crc(dword *addr, int offset) {
bool bVar1;
dword *ptr;
short c;
ptr = (dword *)((int)addr + offset - 4);
c = 0;
if (*(short *)ptr == 0x55aa) {
do {
c = c + *(short *)ptr;
bVar1 = ptr != addr;
ptr = (dword *)((int)ptr - 2);
} while (bVar1);
if (c == *(short *)((int)addr + offset - 2)) {
return 0;
}
}
return 1;
}Partition data starts at offset 0x2000. A mapping function reuses the original loader:
void map_sectors(int offset, dword *address, int length) {
FUN_b4060400(offset << 9, length << 9, (dword)address);
// Each sector = 512 bytes
return;
}The partition loader retrieves descriptions and validates checksums at 0x23f4:
int load_partition_type(int type, dword **callback) {
dword *address;
uint checksum;
int ret;
uint index;
uint next_index;
index = 0;
while (true) {
next_index = index + 1;
if (*(char *)&PARTITION_ADDRESS[index].type == type) break;
index = next_index;
if (0x1e < next_index) {
*callback = NULL;
return -1;
}
}
map_sectors(PARTITION_ADDRESS[index].offset, &MEM_TMP_START, 1);
address = (dword *)tlb_configure_for_type(
type, DWORD_b4064c08,
((MEM_TMP_END - MEM_TMP_START) + 0x203 >> 9) +
(PARTITION_ADDRESS[index].size >> 9)
);
*callback = address;
ret = -3;
if (address != NULL) {
map_sectors(PARTITION_ADDRESS[index].offset, address,
PARTITION_ADDRESS[index].size >> 9);
checksum = checksum_plus((byte *)*callback,
PARTITION_ADDRESS[index].size, 4);
if (PARTITION_ADDRESS[index].checksum != checksum) {
*callback = NULL;
return -2;
}
zero((byte *)MEM_TMP_START, MEM_TMP_END);
ret = 0;
*callback = (dword *)MEM_CALLBACK;
}
return ret;
}The bootloader loads startup.bin first, followed by brecf03.bin with arguments pointing to mapped modules and configuration data.
struct partition_t {
char label[12]; // Partition identifier
uint32_t type; // Module type (syscall slot ID)
uint32_t _unknown; // Reserved
uint32_t block_offset; // Offset in 512-byte blocks
uint32_t size; // Total size in bytes
uint32_t checksum; // Validation checksum
};Each partition contains binary modules with a common 16-byte header:
- Offset 0x00: Upper memory limit
- Offset 0x04: Max memory needed
- Offset 0x08: Base load address
- Offset 0x0C: Entry point
| Label | Description | Load Address | Type |
|---|---|---|---|
brecf03.bin |
Boot record | 0xc3080000 |
0x00 |
welcome.bin |
Splash/boot screen | — | 0x01 |
card.bin |
MMC driver | 0xc0700000 |
0x02 |
storage.bin |
SPI NOR driver | 0xc30a0000 |
0x03 |
upg.bin |
Upgrade/version code | 0xc0400000 |
0x05 |
lcd.bin |
LCD driver | 0xc0f00000 |
0x08 |
startup.bin |
Bootstrap | 0xb4064e00 |
0x09 |
fb.bin |
Framebuffer | 0xc0200000 |
0x0A |
shutoff.bin |
Shutdown handler | — | 0x0C |
key.bin |
Keypad input | 0xc0d00000 |
0x0D |
mfp.bin |
Multifunction peripheral | 0xc0280000 |
0x16 |
Beyond offset 0x80000 (512KB), the remaining flash content is encrypted/compressed, as shown by binwalk entropy analysis:
The first module loaded by the bootloader. Scans configuration strings and configures GPIOs accordingly.
The critical boot record establishes a syscall jump table at 0xc3080e00, used by other binaries for common services. Entry 10 is the logging function.
Loaded by brec, this module implements SPI NOR access. Memory mapping for SPI NOR starts at 0xb0258000. Uses DMA transactions (base: 0xb0220000, channel width: 32 bytes).
Encryption: Sectors at offset ≥0x400 (≥0x80000 bytes) undergo XOR-based encoding:
void _decode(byte *buffer, dword *seed, uint offset, short size) {
int16_t index3;
uint32_t seed7;
uint32_t tmp;
int16_t index;
int16_t index2;
uint32_t key[8];
uint32_t *_buffer;
key[0] = KEY[0];
key[1] = KEY[1];
key[2] = KEY[2];
key[3] = KEY[3];
key[4] = KEY[4];
key[5] = KEY[5];
key[6] = KEY[6];
key[7] = KEY[7];
_buffer = (uint32_t *)buffer;
if (((uint)buffer & 3) != 0) {
_buffer = RANDOM_INIT;
sys_memcpy(RANDOM_INIT, buffer, 0x200);
}
for (index = 0; index < 8; index++) {
key[index] = key[index] ^ (0x5a << (offset & 0x1f)) ^ seed[index];
}
seed7 = seed[7];
for (index2 = 0; index2 < size; index2++) {
index3 = (int16_t)((int)index2 << 3);
_buffer[index3 + 0] ^= key[1]; key[0] = key[1];
_buffer[index3 + 1] ^= key[2]; key[1] = key[2];
_buffer[index3 + 2] ^= key[3]; key[2] = key[3];
tmp = key[3] ^ key[5];
_buffer[index3 + 3] ^= key[4]; key[3] = key[4];
_buffer[index3 + 4] ^= key[5]; key[4] = key[5];
_buffer[index3 + 5] ^= key[6]; key[5] = key[6];
_buffer[index3 + 6] ^= key[7]; key[6] = key[7];
_buffer[index3 + 7] ^= tmp; key[7] = _buffer[index3 + 7] ^ seed7;
}
if (((uint)buffer & 3) != 0) {
sys_memcpy(buffer, _buffer, 0x200);
}
}Use decrypt.c to extract encrypted sections from flash dumps.
Searches for /mnt/udisk/us227a.upg — likely an SDK version identifier. Reference: Pachouli's 2819p
SOON™ – Testing stage
Modified, prebuilt OS/2 distribution optimized for Actions handhelds.
- Boot Process Deep Dive
- Decryption Tools
- MBREC Dumper
- GitHub Wiki – ATJ Series bootloader reference
Made with ❤️ for retro handheld enthusiasts
Reverse Engineering • Custom Firmware • Handheld Restoration

