Sunday, December 18, 2022

The Air32F103


Its no surprise that Bluepills, such as they were, a smoking sub $2 deal would dry up eventually, taken over by clones, some subtle, some overt. With supply issues still plaguing ST parts for hobbyists, what about these cheap Chinese clone parts? Lets dive into the Air32F103.


I bought 5 of these $1.90 boards. They look like this:

Some notables vs a stock bluepill:

1) Castellated pins with flat underside for use as a module

2) 3 LEDs R/G/B (vs just the G for a bluepill)

3) Clone of ST's peripherals

4) over clockable to 256Mhz (spec is 216Mhz) (original STM32F103CB is 72Mhz)

5) 32K of ram. But, via secret regs, 97K(!)

6) top and bottom debug pads, top for JLINK, bottom, legacy STLINK SWD

7) USB C vs USB mini

8) BOOT as a button vs jumper

9) 2 12bit DACs (STM32F103CB's don't have that)

10) QSPI (hidden support)

11) Undocumented crypto block from MegaHunt (includes: AES/DES/3DES/SHA/SM[1,3,4,7])


As mentioned 256Mhz is possible. One Q is how do they do that? Well, PLLMUL is a 4bit field so 8Mhz * 16 gets a max of 128Mhz. A new bit, bit28 of RCC.CFGR adds 16 to the MUL. So, 31(+1) gets 256Mhz.

So, if you do set that, the code will crash with a usage fault. There is secret code called in the ROM that flips a hidden bit to allow high speed operation (the SysFreq_Set code below)

The RCC init code from:

Here we see the 97K unlock:

Observe they flip the lock at the end and revert back to 32K sram

In my Ada code, I flip the lock but seem to fail to revert it back.. leaving 97K available.

The magic reg to allow for high speed op is as follows:

     Magic_Reg : UInt32 with Volatile, Address => System'To_Address (16#40022214#);
         --  Hidden system reg block --
         --  w/o this.. 216Mhz progs don't run (usage fault) --
         Magic_Reg := Magic_Reg or 1;

I really don't think you can misstep here, $1.90 (aliexpress price) for a board with all those features.
There are some differences that need exploration:

Have fun! To quote Hunz on value for money wrt hacking:

Disassembly backup material for the rom study
On entry
r0             0x10280000          271056896
r1             0x1                 1
r2             0x0                 0
r3             0x1
0x1fffe97c: push {r4, r5, r6, lr}
   0x1fffe97e: mov r6, r1
   0x1fffe980: mov r1, r0
   0x1fffe982: sub sp, #24
   0x1fffe984: mov r4, r3
   0x1fffe986: mov r5, r2
   0x1fffe988: movs r0, #0
   0x1fffe98a: bl 0x1fffe12c
; read 0x40021004 (reads 0)
        0x1fffe12c: ldr r3, [pc, #12] ; (0x1fffe13c)
        0x1fffe12e: ldr r2, [r3, #4]
; or in the PLLMUL
        0x1fffe130: orrs r0, r1
; clear PLLMUL field
        0x1fffe132: bic.w r2, r2, #4128768 ; 0x3f0000
; or in prior contents(why?)
        0x1fffe136: orrs r0, r2
; final PLLMUL (0x10280000) (thats +16 and 10 mult (27 * 8 = 216)
        0x1fffe138: str r0, [r3, #4]
        0x1fffe13a: bx lr
   0x1fffe98e: movs r0, #0
   0x1fffe990: bl 0x1fffe0d8
   ; read 0x40021004
        0x1fffe0d8: ldr r2, [pc, #12] ; (0x1fffe0e8)
; reads old val 0x10280000
        0x1fffe0da: ldr r1, [r2, #4]
; wipe HPRE
        0x1fffe0dc: bic.w r1, r1, #240 ; 0xf0
; or in wanted val (in this case 0)
        0x1fffe0e0: orrs r1, r0
; no change to HPRE
        0x1fffe0e2: str r1, [r2, #4]
        0x1fffe0e4: bx lr
   0x1fffe994: mov.w r0, #1024 ; 0x400
   0x1fffe998: bl 0x1fffe0f8
        0x1fffe0f8: ldr r2, [pc, #12] ; (0x1fffe108)
        0x1fffe0fa: ldr r1, [r2, #4]
; Now PPRE1 (set to the 0x4 of r0's arg of 0x400)
        0x1fffe0fc: bic.w r1, r1, #1792 ; 0x700
        0x1fffe100: orrs r1, r0
        0x1fffe102: str r1, [r2, #4]
;(gdb) x/x 0x40021004
;0x40021004: 0x10280400
        0x1fffe104: bx lr
   0x1fffe99c: movs r0, #0
   0x1fffe99e: bl 0x1fffe10c
; we arrive with r0=0
        0x1fffe10c: ldr r2, [pc, #12] ; (0x1fffe11c)
        0x1fffe10e: ldr r1, [r2, #4]
; once again 0x40021004
        0x1fffe110: bic.w r1, r1, #14336 ; 0x3800
        0x1fffe114: orr.w r0, r1, r0, lsl #3
        0x1fffe118: str r0, [r2, #4]
; 0x10280400
        0x1fffe11a: bx lr
   0x1fffe9a2: mov r0, sp
;(gdb) x/16x $r0
;0x20000fd8: 0x00000000 0x00000000 0x00000000 0x00000000
;0x20000fe8: 0x00000000 0x00000000 0x00010000 0x10280000
;0x20000ff8: 0x00000001 0x0800136d 0x200183ff 0x08001464
;0x20001008: 0x08001464 0x00000000 0x00000000 0x080002dd
   0x1fffe9a4: bl 0x1fffe004
        0x1fffe004: push {r4, r5, lr}
        0x1fffe006: ldr r2, [pc, #144] ; (0x1fffe098)
        0x1fffe008: ldr r1, [r2, #4]
; once again 0x40021004 = 0x40021004
        0x1fffe00a: ldr r3, [pc, #144] ; (0x1fffe09c)
; r3=0x7a1200
; SWS masked off (0xC)
        0x1fffe00c: ands.w r1, r1, #12
; vvv taken since after masking, r1 == 0
        0x1fffe010: beq.n 0x1fffe01a
        0x1fffe012: cmp r1, #4
        0x1fffe014: beq.n 0x1fffe01a
        0x1fffe016: cmp r1, #8
        0x1fffe018: beq.n 0x1fffe01e
; Save r3 on the stack
        0x1fffe01a: str r3, [r0, #0]
;(gdb) x/x $r0
;0x20000fd8: 0x007a1200
        0x1fffe01c: b.n 0x1fffe054
        0x1fffe01e: ldr r1, [r2, #4]
        0x1fffe020: ldr r3, [r2, #4]
        0x1fffe022: and.w r1, r1, #3932160 ; 0x3c0000
        0x1fffe026: ldr r4, [r2, #4]
        0x1fffe028: and.w r3, r3, #65536 ; 0x10000
        0x1fffe02c: lsrs r4, r4, #27
        0x1fffe02e: lsls r4, r4, #31
        0x1fffe030: beq.n 0x1fffe03c
        0x1fffe032: movs r4, #16
        0x1fffe034: orr.w r1, r4, r1, lsr #18
        0x1fffe038: adds r1, r1, #1
        0x1fffe03a: b.n 0x1fffe042
        0x1fffe03c: movs r4, #2
        0x1fffe03e: add.w r1, r4, r1, lsr #18
        0x1fffe042: cbz r3, 0x1fffe04a
        0x1fffe044: ldr r3, [r2, #4]
        0x1fffe046: lsls r3, r3, #14
        0x1fffe048: bpl.n 0x1fffe04e
        0x1fffe04a: ldr r3, [pc, #84] ; (0x1fffe0a0)
        0x1fffe04c: b.n 0x1fffe050
        0x1fffe04e: ldr r3, [pc, #76] ; (0x1fffe09c)
        0x1fffe050: muls r1, r3
        0x1fffe052: str r1, [r0, #0]
; ^^^ from 0x1c above
; ld 0x40021004 again...
        0x1fffe054: ldr r1, [r2, #4]
        0x1fffe056: movs r3, #15
;??? why
        0x1fffe058: and.w r1, r3, r1, lsr #4
; HPRE now in r1 (r1 >> 4) & 0xF
        0x1fffe05c: ldr r3, [pc, #68] ; (0x1fffe0a4)
; r3=0x20018050 (the secret memory)
; some sort of byte table indexed by r1
        0x1fffe05e: ldrb r4, [r3, r1]
; reads 0
        0x1fffe060: ldr r1, [r0, #0]
; read back tos (0x7a1200)
; this is a shift by 0 (r4 == 0)
        0x1fffe062: lsrs r1, r4
; write back tos+4 same 0 shifted# 0x7a1200
        0x1fffe064: str r1, [r0, #4]
; read 0x40021004 again...
        0x1fffe066: ldr r4, [r2, #4]
        0x1fffe068: movs r5, #7
; get 10:8 into r4 (was 0x400 -> PPRE1=4)
        0x1fffe06a: and.w r4, r5, r4, lsr #8
; this reads 0x20018054[7:0] = 1
        0x1fffe06e: ldrb r4, [r3, r4]
; now shift 7a1200 by... r4(==1)
        0x1fffe070: lsr.w r4, r1, r4
; r4 0x3d0900
; 0x20000fd8: 0x007a1200 0x007a1200 0x003d0900 0x00000000
        0x1fffe074: str r4, [r0, #8]
; still 0x40021004 == 0x10280400
        0x1fffe076: ldr r4, [r2, #4]
; read PPRE2
        0x1fffe078: and.w r4, r5, r4, lsr #11
; r4=0, read is 0
        0x1fffe07c: ldrb r3, [r3, r4]
; r1=0x7a1200 << 0, unchanged
        0x1fffe07e: lsrs r1, r3
; after this str
; (gdb) x/8 $r0
; 0x20000fd8: 0x007a1200 0x007a1200 0x003d0900 0x007a1200
        0x1fffe080: str r1, [r0, #12]
; CFGR again...
        0x1fffe082: ldr r2, [r2, #4]
        0x1fffe084: movs r3, #3
; ADCPRE was 0
        0x1fffe086: and.w r2, r3, r2, lsr #14
        0x1fffe08a: ldr r3, [pc, #24] ; (0x1fffe0a4)
; backup magic ram to 0x2001804c...btw 0x4c = 0x08060402
        0x1fffe08c: subs r3, r3, #4
; r2=0 this should read 2
        0x1fffe08e: ldrb r2, [r3, r2]
; r2=2 div 8mhz by 2
        0x1fffe090: udiv r1, r1, r2
; r1 == 4mhz (0x3d0900)
        0x1fffe094: str r1, [r0, #16]
0x20000fd8: 0x007a1200 0x007a1200 0x003d0900 0x007a1200
0x20000fe8: 0x003d0900 0x00000000 0x00010000 0x10280000
        0x1fffe096: pop {r4, r5, pc}
; 0x40022000 secret peripheral
   0x1fffe9a8: ldr r0, [pc, #40] ; (0x1fffe9d4)
; read 0x40022214 (== 0x00034069)
   0x1fffe9aa: ldr.w r1, [r0, #532] ; 0x214
   0x1fffe9ae: movw r2, #49159 ; 0xc007
; r1 now becomes 0x30068
   0x1fffe9b2: bics r1, r2
; r6 (==1) arg save from entry
   0x1fffe9b4: and.w r2, r6, #7
; r2 is now 1
; which makes r1=0x30069
   0x1fffe9b8: orrs r1, r2
; r4 was 1 so this is 1
   0x1fffe9ba: and.w r2, r4, #1
; r1=0x30069
   0x1fffe9be: orr.w r1, r1, r2, lsl #14
; now 0x34069  r2=0 after the and
   0x1fffe9c2: and.w r2, r5, #1
; making r1 unchanged (0x34069)
   0x1fffe9c6: orr.w r1, r1, r2, lsl #15
   0x1fffe9ca: str.w r1, [r0, #532] ; 0x214
   0x1fffe9ce: add sp, #24
   0x1fffe9d0: pop {r4, r5, r6, pc}

Sunday, October 30, 2022

 The GD32VF103

This chip is a peripheral clone of an STM32F103. The CPU is swapped out for a Risc-V.

Openocd debug using JTAG

Using the release on github:
Build it:
./configure --enable-ftdi --enable-stlink --enable-ti-icdi --enable-jlink --enable-cmsis-dap --enable-xds110
We can connect to the target.

Wiring to a J-Link is as so:

Pin1 VCC -> 3V3
Pin5 TDI -> JTDI (underside of board)
Pin7 TMS -> JTMS
Pin13 TDO -> JTDO

Add this to openocd/tcl/board as bluepillGDF103.cfg

adapter speed 8000
source [find interface/jlink.cfg]
source [find target/longan.cfg]
jtag_ntrst_assert_width 10
reset_config trst_only

Add this to openocd/tcl/target as longan.cfg:

# script for Longan nano

# longan nano devices support JTAG
transport select jtag

if { [info exists CHIPNAME] } {
} else {
   set _CHIPNAME riscv

set _ENDIAN little

if { [info exists DAP_TAPID] } {
} else {
        set _DAP_TAPID 0x1000563D

jtag newtap $_CHIPNAME cpu -irlen 5 -expected-id $_DAP_TAPID
target create $_TARGETNAME riscv -chain-position $_TARGETNAME
$_TARGETNAME configure -work-area-phys 0x20000000 -work-area-size 20480 -work-area-backup 0

# Work-area is a space in RAM used for flash programming
if { [info exists WORKAREASIZE] } {
} else {
   set _WORKAREASIZE 0x5000

# Allow overriding the Flash bank size
if { [info exists FLASH_SIZE] } {
} else {
    # autodetect size
    set _FLASH_SIZE 0

# flash size will be probed

#flash bank $_FLASHNAME gd32vf103 0x08000000 0 0 0 $_TARGETNAME
flash bank $_FLASHNAME stm32f1x 0x08000000 0 0 0 $_TARGETNAME
riscv set_reset_timeout_sec 1


Run openocd:
root@ubuntu:/p4work/riscv-openocd/tcl# ../src/openocd -f board/bluepillGDF103.cfg
Open On-Chip Debugger 0.11.0+dev-02415-gfad123a (2022-10-29-17:33)
Licensed under GNU GPL v2
For bug reports, read
Info : J-Link ARM V8 compiled Dec  1 2009 11:42:48
Info : Hardware version: 8.00
Info : VTarget = 3.339 V
Info : clock speed 8000 kHz
Info : JTAG tap: riscv.cpu tap/device found: 0x1000563d (mfg: 0x31e (Andes Technology Corporation), part: 0x0005, ver: 0x1)
Info : JTAG tap: auto0.tap tap/device found: 0x790007a3 (mfg: 0x3d1 (GigaDevice Semiconductor (Beijing) Inc), part: 0x9000, ver: 0x7)
Warn : AUTO auto0.tap - use "jtag newtap auto0 tap -irlen 5 -expected-id 0x790007a3"
Info : [riscv.cpu] datacount=4 progbufsize=2
Error: [riscv.cpu] Hart doesn't exist.
Error: [riscv.cpu] Hart is not halted!
Info : [riscv.cpu] Examined RISC-V core; found -1 harts
Info : [riscv.cpu]  XLEN=32, misa=0x40901105
[riscv.cpu] Target successfully examined.
Info : starting gdb server for riscv.cpu on 3333
Info : Listening on port 3333 for gdb connections
trst_only separate trst_push_pull

Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections

Not sure what the Hart not existing messages are about. Hardware Thread is my understanding of what a Hart is. No matter, GDB can attach, and reset the target. Note that the Risc-V came from Andes Technology. This is a TW IP provider, GigaDevice semi just does the integration with 'their' peripherals. Curious how they copied ST's peripherals, there is a lot in those peripherals so it would be a real job to make a workalike.

Flash programmed from openocd

Seems to work:

(gdb) load
Loading section .text, size 0x12b4 lma 0x50000
Loading section .rodata, size 0x1bc lma 0x512b4
Loading section .eh_frame, size 0x3c lma 0x51470
Loading section .sdata, size 0x3b lma 0x514ac
Start address 0x00050000, load size 5351
Transfer rate: 6 KB/sec, 1337 bytes/write.
(gdb) x/32x 0
0x0: 0xfffb1197 0x80018193 0xfffb1117 0xa5810113
0x10: 0x00001517 0x49c50513 0xfffb0597 0xfe858593
0x20: 0xfffb0617 0x01b60613 0x00c5fa63 0x00052283
0x30: 0x0055a023 0x05910511 0xfec5eae3 0xfffb0517


Good old UART works. Hold reset then hold boot0 release in same order.
Connect a USB to UART adapter to the board. VCC -> 5V pin, GND to GND, Green wire to R0, White to T0. (at least those are the colours on my Adafruit adapter).

Ubuntu can install stm32flash. Then, a raw binary (the -f flag) programs the FW. Watch out with /dev/ttyUSB0, by default its root owned so stm32flash may fail.

stm32flash -g 0x08000000 -b 115200 -w firmware.bin -f /dev/ttyUSB0


Using Fabien Chouteau's PicoRV32 example code. Its Ada Zero Footprint. Linked at 0.