Skip to content

About

Reverse Engineering Actions ATS3603-based devices.

Resources

Stars

7 stars

Watchers

2 watching

Forks

Repository files navigation

RE: ATS3603 - SOC Reverse Engineering

📖 Repository

Actions Semiconductor ATS3603 SOC - Handhelds Reverse Engineering and Custom SDK

Device

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.

🛠️ Development Resources:


🖥️ Device Information

Schematic

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!


🗜️ Hardware Specification

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.


🟦 Display

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.


🧮 Reverse Engineering the MBREC Flash Dump

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.

Booting Process Recap

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     _nop

After 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.

CRC Validation

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;
}

Loading Partitions

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.

Partition Structure

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

📦 Partition Types

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:

Entropy Graph


🔧 Key Modules

startup.bin

The first module loaded by the bootloader. Scans configuration strings and configures GPIOs accordingly.

brecf03.bin

The critical boot record establishes a syscall jump table at 0xc3080e00, used by other binaries for common services. Entry 10 is the logging function.

storage.bin

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.

upg.bin

Searches for /mnt/udisk/us227a.upg — likely an SDK version identifier. Reference: Pachouli's 2819p


🚀 Custom OS/2 & UCOS Builds

Linux-based

SOON™ – Testing stage

OS/2-based

Custom OS/2 Release

Modified, prebuilt OS/2 distribution optimized for Actions handhelds.


📚 Learn More


Made with ❤️ for retro handheld enthusiasts

Reverse Engineering • Custom Firmware • Handheld Restoration

About

Reverse Engineering Actions ATS3603-based devices.

Resources

Stars

7 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages