Skip to content

sh68f89 platform and ciy-x77 device - #74

Merged
carlossless merged 23 commits into
masterfrom
ciy-x77-sh68f89
Sep 13, 2026
Merged

sh68f89 platform and ciy-x77 device#74
carlossless merged 23 commits into
masterfrom
ciy-x77-sh68f89

Conversation

@carlossless

Copy link
Copy Markdown
Owner

Adds the SinoWealth SH68F89 platform and the CIY X77, a TKL board built on it, with key scan, USB, lock LEDs, per-key RGB on the four PCA units and settings in an external 24Cxx, all verified on hardware.

Adds the register header for the SinoWealth SH68F89 and its platform
notes. The part is register-for-register the SH88F6162, so the map is
transcribed from that datasheet's section 7 and cross-checks clean
against the vendor Keil header at all 192 sfr and 166 sbit declarations.

Covers both SFR pages, every named bit the datasheet gives, the USB RAM
window at 0x0B28 and the 15 interrupt vectors, which start with the four
PCA units rather than the SH68F881's INT0/Timer4/INT1/Timer3.
Adds the SH68F89 platform and the CIY X77, a full-size board built on it.

The platform is close to the SH68F881 in shape and different in nearly
every address: the USB block sits at 0xAA-0xBF on SFR page 1 with none of
the endpoint control registers bit addressable, USB RAM is at 0x0B28
above 40 bytes of LCD RAM, and interrupt numbers 0-3 are the four PCA
units, so the tick lands on Timer2 at vector 5 and USB at vector 12.
SYSCLK is the PLL at 24MHz, settled by polling PLLCON.PLLSTA rather than
by a blind delay.

The X77 keeps its settings in an external 24Cxx EEPROM on a bit-banged
bus rather than in the MCU's own data block, which is what the stock
firmware does and leaves the on-chip block free. SDA and SCL share port 5
with the matrix rows, so the row read masks them off.

The base keymap is decoded from the stock image rather than guessed: its
key records store a usage directly for most keys and an index into a
second table for the rest, and resolving that gives all 116 matrix
positions. The 12 an ANSI board does not fit are wired to KC_NO.

Per-key RGB runs off the four PCA units and is not brought up yet; the
three lock LEDs are.
RSTSTAT is 0xB1 on page 0 and USBCON on page 1, so a watchdog kick taken
with page 1 latched writes 0x02 into USBCON and drops the D+ pull-up.
The EEPROM bit-bang held page 1 across its half-period delays, and
delay_us() kicks from inside its loop, so settings_load() at boot
disconnected USB before it ever enumerated. The bus now spins on nops and
kicks between transfers instead.

Also selects SFR page 0 by clearing INSCON bit 7 alongside BKS0. The
register table calls bit 7 unimplemented, but the datasheet text names a
BKS1 and the ISP bootloader selects page 0 with `anl INSCON,#0x3f`.
The stock firmware ships one keymap for the whole X77 family, so its
matrix is wider than this unit: columns 18-21 on P7 decode to a complete
numpad that an ANSI TKL does not fit, the same way the keymap's ISO and
JIS positions are not fitted.

Drops MATRIX_COLS to 18 and leaves P7 an input, since those pins may not
be routed on a TKL board. That also takes the columns entirely onto SFR
page 0, so only the row read still crosses the page boundary.

All 18 columns and all 6 rows are confirmed on hardware, along with the
factory information block (PART 68f8900000, OPT b8c0038c matching what a
programmer reads over ICP) and a settings round-trip through the external
24Cxx that survives a power cycle.

Also marks EP2 busy when the debug console sends: console_task gates on
usb_hw_ep2_in_free(), so leaving it clear let each log line overwrite the
previous one before the host had collected it.
The backlight is nine PCA compare channels multiplexed over the matrix
columns, with P4.4 and P4.5 selecting rows 0-2 or rows 3-5, so a column
takes two subframes and a frame is 36.

Three things the existing reverse engineering did not have, all
established on hardware:

  * P7.0 gates the backlight supply and is active low. Nothing lights
    without it however the PCA is set up, and it is easy to miss sitting
    next to the matrix columns on P7.1-P7.4.
  * P4.4 and P4.5 are the row-group enables, set at the end of a PCA0
    interrupt and cleared at the start of the next.
  * The colour order is blue, green, red. Confirmed on the board and
    against the firmware, where led_fill_all(0,0,0xff) fills red.

P3 and P4 are never read back. Both mix PCA outputs with other functions,
so a read samples live PWM on those bits and writing the value back
latches a channel on; it showed as one column of keys brighter than the
rest. Every write now goes through a computed value, sharing one P4
shadow with the column driver.

The systick slots come down to 0.25 ms for a subframe and 1 ms for a
scan: at 36 subframes the old 8 ms scan slot was a 30% dark duty and read
as a visible blink.
The four PCA units are the part's only PWM, so their bring-up and the
glitch-free reload sequence belong beside the other platform drivers
rather than in a board file; the board keeps the channel-to-LED mapping,
which is all that is board specific.

Evaluating the effect is by far the most expensive thing per subframe and
was running in the tick interrupt. At the 0.25 ms subframe this port now
uses it does not fit there, and overrunning starved the USB interrupt
until the device stopped enumerating while the backlight carried on
looking fine. It runs from the main loop now, leaving the interrupt with
nine compare writes and the column drive.
The backlight section records the pin map, the two row-group enables, the
blue-green-red channel order and the P7.0 supply enable, none of which
are guessable from the pinout and the last of which is what keeps a
correctly configured board dark.
The Tab row ran a column long and the Shift row a column short, so
neither lined up with the block borders above and below them, and the Fn
layer only drew half the board. Both are now a full TKL at one width.
The blocks, the specs and the dispatch chain were each in the order the
platforms were added, so a new one had no obvious insertion point. All
three are now A-Z: bk3632, sh68f881, sh68f89, sh68f90.

Pure reorder; every firmware image is byte-identical.
…ture

bb_i2c joins bb_spi as a shared bit-bang driver: the board supplies the
two pins and, where they sit on a non-default SFR page, the wrapper that
selects it, so the driver itself stays page agnostic.

The 24Cxx settings store becomes a feature a board opts into with
`'nvm': '24cxx'`, the same way the wireless one works, rather than
something the sh68f89 platform hardcodes. Another board on this part can
now use flash or the on-chip block instead.

Two things fixed rather than carried across:

  * The write-protect macros were doing a read-modify-write on P4, which
    also carries three PCA outputs, so a read sampled live PWM. They go
    through the shadow now, like every other write to that port.
  * led_drive rebuilt the whole P4 byte from KB_P4_IDLE, which has write
    protect asserted, so every LED subframe re-protected the EEPROM,
    including from under a settings save. It left a half-written record
    that failed its checksum on the next boot. It now touches only the
    bits it owns.

Verified on hardware: a save made with the backlight running survives a
power cycle, twice.
…ts own

ldo_init() only enabled the regulator on the sh68f90; on the other two it
enabled nothing and existed to latch RSTSTAT, while the regulator was
brought up from clock_init(). All three now do the same two things, and
REGCON is written in exactly one place per part.

The reset-source latch stays in ldo_init() because RSTSTAT doubles as the
watchdog kick, so the flags survive only until something kicks, and this
is the first platform call on every part.

Clearing the analog and display blocks is not clock work either. It moves
to peripherals_init(), called from main() once the clock is up and before
any board init, so clock.c is only the clock. The sh68f90 gets an empty
one: nothing on that part mux'es away a pin the firmware uses.
The Fn layer was carried over from another board and only two of its
twelve function-row keys happened to match this one. The stock layer is
decodable: the table at 0xC610 is a 48-entry pool in two halves, a base
key's record indexes pool[n] and its secondary function is pool[n + 48].

That gives the media set exactly: my computer, WWW home, mail,
calculator, stop, previous, play/pause, next, then volume down, volume up
and mute. Stock puts a lighting control on F9 and effect selects on 1-5,
so the Fx keys take those.

Keyboard lock moves off F12, which is mute on this board, to Pause, which
stock leaves with no secondary function.

Stock's remaining secondary functions are its own key classes rather than
HID usages, and which class is which is behavioural rather than visible
in the image, so they are left alone.
The user manual in the reverse-engineering repo documents the backlight
controls, and the keycap glyphs in its diagram settle which key each one
is on. Cross-checked against the firmware, every pair lines up:

  Fn+F9        cycle the animation          type 0x0B u=00
  Fn+PgUp/PgDn brightness                   type 0x0C
  Fn+-/=       animation speed              type 0x0D
  Fn+Up/Down   colour                       type 0x12
  Fn+Left/Righ backlight direction          type 0x0E u=0a/0b
  Fn+Gui       Windows key lock             type 0x0E u=01
  Fn+End       Fn lock                      type 0x0E u=02
  Fn+Del       restore backlight defaults   type 0x0E u=05

Brightness and speed were on the wrong keys. Colour is new: the offset
goes into the wheel index, so the colour keys rotate the whole animation,
and it persists like the other settings. Restoring the defaults is new
too and takes Del as stock does, without the five second hold.

Not reproduced: the backlight recording on Esc and 1-5, the Fn lock, and
the direction control, which these animations have no notion of.

user_settings_t grows a byte for the colour. Every nvm backend stores the
record length and checks it, so an existing record simply fails to
validate and the defaults come back.
The comments and the board notes explained several choices by pointing at
the firmware that shipped on the device. The facts are the same either
way and a reader of this tree cannot check that reference, so they now
just say what the board does.
ldo.c now writes REGCON and nothing else. The latch was sitting there
only because ldo_init() happened to be the first platform call, which is
the wrong reason for it to live in a file about the regulator.

It gets reset.c, and reset_init() becomes the first call in main(), since
RSTSTAT doubles as the watchdog kick and the flags survive only until
something kicks. reset.h declares reset_status, so the boards that report
it no longer each extern it themselves.
The keyboard lock, the NKRO toggle and the WASD swap came in with a
keymap copied from another board. None of them exists here: W and Pause
carry no secondary function at all, and ScrLk carries something
unrelated. The Gui lock stays, that one is real.
Both variants share the matrix; the ISO one gains the hash key next to
the tall Enter at (3,12) and the extra key left of Z at (4,14), and gives
up (2,13), which the tall Enter covers. The ANSI layout is unchanged and
stays the default.

indicators.c moves up to the keyboard directory, since the backlight is a
property of the board rather than of the keymap and both layouts want the
same one. Meson looks for it at either level, so the boards that keep
theirs in the layout directory are unaffected.
The Caps row ran two columns short and the Shift row one, so neither
lined up with the borders around them, and the Fn layer was left without
a diagram at all.
The KC_NO entries in the macro say which positions are unpopulated, and
the diagram above each keymap says what the board looks like.
@carlossless
carlossless marked this pull request as ready for review September 13, 2026 10:54
@carlossless
carlossless merged commit c04d13f into master Sep 13, 2026
9 checks passed
@carlossless
carlossless deleted the ciy-x77-sh68f89 branch September 13, 2026 10:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant