Browse Source

Implement flashing over I2C and user/stock firmware support

I2C register layout changed a bit to make various operations easier
to implement in FW and for the user.

Flashing/debugging tools now share more code. Firmware is now more
configurable (it's now possible to compile-out various features).

Self-testing for column-shorts is implemented.

Firmware is optimized for low power consumption.
master 1.0-beta1
Ondrej Jirman 1 year ago
parent
commit
6a5fe581b9
  1. 1
      .gitignore
  2. 45
      Makefile
  3. 27
      README
  4. 92
      README.flashing
  5. 135
      README.i2c-intf
  6. 51
      TODO
  7. BIN
      builds/fw.bin
  8. 3
      charger/build.sh
  9. 385
      charger/main.c
  10. 387
      common.c
  11. 45
      firmware/build.sh
  12. 13
      firmware/em85f684a.h
  13. 1510
      firmware/main.c
  14. 71
      firmware/registers.h
  15. 101
      firmware/stock-ivt.asm
  16. 478
      i2c-charger-ctl.c
  17. 60
      i2c-debugger.c
  18. 509
      i2c-flasher.c
  19. 217
      i2c-inputd.c
  20. 114
      i2c-selftest.c
  21. 4
      inputd/build.sh
  22. 284
      inputd/kmap.h
  23. 63
      keymaps/factory-keymap-megi.txt
  24. 0
      keymaps/factory-keymap.jpg
  25. 0
      keymaps/factory-keymap.txt
  26. 4
      keymaps/map-to-c.php
  27. 0
      keymaps/physical-map.jpg
  28. 0
      keymaps/physical-map.txt
  29. 29
      usb-debugger.c
  30. 13
      usb-flasher.c
  31. 6
      usb-flasher/build.sh
  32. 205
      usb-flasher/common.c

1
.gitignore vendored

@ -0,0 +1 @@ @@ -0,0 +1 @@
/firmware/build/

45
Makefile

@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
VERSION := $(shell git describe) $(shell git log -1 --format=%cd --date=iso)
OUT ?= build/
CFLAGS ?= -O2 -g0
CFLAGS += -DVERSION="\"$(VERSION)\"" -I. -I$(OUT)
all: $(OUT)ppkb-i2c-inputd $(OUT)ppkb-usb-flasher $(OUT)ppkb-usb-debugger $(OUT)fw-stock.bin $(OUT)ppkb-i2c-debugger $(OUT)ppkb-i2c-charger-ctl $(OUT)ppkb-i2c-flasher $(OUT)ppkb-i2c-selftest
$(OUT)ppkb-usb-flasher: usb-flasher.c common.c
@mkdir -p $(OUT)
$(CC) $(CFLAGS) -o $@ $<
$(OUT)ppkb-usb-debugger: usb-debugger.c common.c
@mkdir -p $(OUT)
$(CC) $(CFLAGS) -o $@ $<
$(OUT)kmap.h: keymaps/physical-map.txt keymaps/factory-keymap.txt
@mkdir -p $(OUT)
php keymaps/map-to-c.php $^ > $@
$(OUT)ppkb-i2c-inputd: i2c-inputd.c $(OUT)kmap.h common.c
@mkdir -p $(OUT)
$(CC) $(CFLAGS) -o $@ $<
$(OUT)ppkb-i2c-debugger: i2c-debugger.c common.c
@mkdir -p $(OUT)
$(CC) $(CFLAGS) -o $@ $<
$(OUT)ppkb-i2c-charger-ctl: i2c-charger-ctl.c common.c
@mkdir -p $(OUT)
$(CC) $(CFLAGS) -o $@ $<
$(OUT)ppkb-i2c-flasher: i2c-flasher.c common.c
@mkdir -p $(OUT)
$(CC) $(CFLAGS) -o $@ $<
$(OUT)ppkb-i2c-selftest: i2c-selftest.c common.c
@mkdir -p $(OUT)
$(CC) $(CFLAGS) -o $@ $<
$(OUT)fw-stock.bin $(OUT)fw-user.bin: firmware/em85f684a.h firmware/main.c firmware/build.sh firmware/bootloader.bin
@mkdir -p $(OUT)
cd firmware && ./build.sh
cp -f firmware/build/fw-stock.bin $(OUT)fw-stock.bin
cp -f firmware/build/fw-user.bin $(OUT)fw-user.bin

27
README

@ -1,4 +1,29 @@ @@ -1,4 +1,29 @@
This is work in progress free software firmware for pinephone keyboard.
FOSS firmware for pinephone keyboard
====================================
Features:
- Dual firmware architecture: stock firmware + optional user firmware.
- Stock firmware implements the full functionality of the keyboard.
- Stock firmware is layout independent, it reports the raw status
of the whole keyboard matrix.
- Key maps and combinations can be arbitrarily changed in the keyboard
driver without re-flashing the firmware.
- Stock firmware should be sufficient for most users who will not want
to do HW modifications to their keyboard to add more peripherals to
the keyboard MCU.
- Power efficient implementation using power-down feature of the MCU
to save power as much as possible. (currently: 9 mW when idle, 20mW
in active scanning mode - with at least one key pressed)
- Stock firmware is flashed in factory and allows flashing user firmware
from the pinephone itself over I2C interface.
- User firmware can be used either for updates or for customizations
(SW support for HW modifications of the keyboard)
- USB stack and tools for stock firmware flashing using ELAN's original
bootloader to ease development of the stock firmware.
- Self-testing features to quickly test for issues with the keyboard matrix.
- Fully based on FOSS software, with no dependencies. You only need
sdcc 4.1+ to build the firmware.
See demo video https://megous.com/dl/tmp/kb.mp4 and some technical overview https://xnux.eu/log/

92
README.flashing

@ -1,13 +1,19 @@ @@ -1,13 +1,19 @@
Flashing tool from usb-flasher/ directory can be used to flash the
firmware over USB.
Flashing over USB
=================
This method allows updating the stock firmware on the keyboard.
Flashing tool ppkb-usb-flasher can be used to flash the firmware over USB.
You'll have to solder USB cable to the keyboard controller board to be able
to user this method.
For example to build and flash your own firmware you could run these
commands:
# save the original firmware in case you want to restore it
./usb-flasher/ppkb-flasher info > saved-info.txt
./usb-flasher/ppkb-flasher --rom-out saved-rom.bin read
./ppkb-usb-flasher info > saved-info.txt
./ppkb-usb-flasher --rom-out saved-rom.bin read
# build the new firmware (you may need sdcc 4.1, older versions may
# miscompile the firmware)
@ -16,20 +22,21 @@ commands: @@ -16,20 +22,21 @@ commands:
# flash the new firmware
./usb-flasher/ppkb-flasher --rom-in firmware/build/fw.bin write reset
./ppkb-usb-flasher --rom-in firmware/build/fw-stock.bin write reset
Full help output
----------------
Usage: ppkb-flasher [--rom-in <path>] [--rom-out <path>] [--verbose]
[--help] [<read|write|info|reset>...]
Usage: ppkb-usb-flasher [--rom-in <path>] [--rom-out <path>] [--verbose]
[--help] [<read|write|info|reset>...]
Options:
-i, --rom-in <path> Specify path to binary file you want to flash.
-o, --rom-out <path> Specify path where you want to store the contents
of code ROM read from the device.
-s, --rom-size <size> Specify how many bytes of code rom to flash
-s, --size <size> Specify how many bytes of code rom to flash
starting from offset 0x2000 in the rom file.
-v, --verbose Show details of what's going on.
-h, --help This help.
@ -43,7 +50,68 @@ Commands: @@ -43,7 +50,68 @@ Commands:
Format of the ROM files is a flat binary. Only the part of it starting
from 0x2000 will be flashed. Use -s to specify how many bytes to write.
Pinephone keyboard flashing tool 1.0
Written by Ondrej Jirman <megi@xff.cz>, 2021
Licensed under GPLv3, see https://xff.cz/git/pinephone-keyboard/ for
more information.
Flashing over I2C
=================
This method users stock stock firmware's flashing interface to allow writing
user's own firmware to the keyboard, while keeping the stock firmware intact.
Stock firmware will remain present and available for future updates of the
users fiwmare.
You can use this method without having to disassemble the keyboard, or
solder anything.
You'll need to build the user firmware in a special way, so that it's placed
at address 0x4000 in code ROM. Interrupt vectors are forwarded with 0x4000
offset.
For example to build and flash your own firmware you could run these
commands:
# build the new firmware (you may need sdcc 4.1, older versions may
# miscompile the firmware)
(cd firmware && ./build.sh)
# flash the new firmware
./ppkb-i2c-flasher --rom-in firmware/build/fw-user.bin write reset
Depending on the user firmware, you may need to change stock firmware entry
method using -e option.
Full help output
----------------
Usage: ppkb-i2c-flasher [--rom-in <path>] [--rom-out <path>] [--verbose]
[--help] [<read|write|erase|info|reset>...]
Options:
-i, --rom-in <path> Specify path to binary file you want to flash.
-o, --rom-out <path> Specify path where you want to store the contents
of code ROM read from the device.
-s, --size <size> Specify how many bytes of code rom to flash
starting from offset 0x4000 in the rom file.
-e, --entry <manual|i2c|none>
Specify how to enter the stock firmware:
- manual: Ask the user to power-cycle the keyboard
- i2c: Send I2C command to make supporting user
- none: Assume stock firmware is already running
-v, --verbose Show details of what's going on.
-h, --help This help.
Commands:
info Display information about the current firmware.
read Read ROM from the device to --rom-out file.
write Flash ROM file to the device from --rom-in.
erase Erase the user firmware.
reset Perform software reset of the MCU.
Format of the ROM files is a flat binary. Only the part of it starting
from 0x4000 will be flashed. Use -s to specify how many bytes to write.
The stock firmware between 0x2000 and 0x4000 will be preserved.

135
README.i2c-intf

@ -20,6 +20,13 @@ Device address is 0x15. @@ -20,6 +20,13 @@ Device address is 0x15.
Registers
---------
General ranges:
0x00 - 0x1f - Read-only status range
0x20 - 0x2f - Writable keyboard control range
0x70 - 0xf4 - Flashing interface
0xff - Debug log
0x00: Device ID1 (0x4b)
0x01: Device ID2 (0x42)
0x02: Firmware revision
@ -27,8 +34,15 @@ Registers @@ -27,8 +34,15 @@ Registers
bit 0: USB debugger
bit 1: Flashing mode
bit 2: Self-test features
bit 3: Stock firmware flag (only stock firmware should have this set)
0x04: System configuration
0x06: bits 3-0: number of rows, bits 7-4 number of cols
0x07: CRC8 of keyboard data from 0x08-0x13
0x08: Keyboard data for column 1
...
0x13: Keyboard data for column 12 (up to number of cols, in this case 12)
0x20: System configuration
bit 0: disable KB scanning (1: scanning disabled, 0: scanning enabled)
bit 1: poll mode
1: don't rely on row change detection, poll the matrix periodically
@ -39,7 +53,7 @@ Registers @@ -39,7 +53,7 @@ Registers
1: enabled
0: disabled
0x06: System command
0x21: System command
Writing values into this register causes the firmware to perform
certain one-shot actions:
@ -49,23 +63,34 @@ Registers @@ -49,23 +63,34 @@ Registers
(results for both tests will be stored in test-result
registers)
0x10: Keyboard data for column 1
...
0x1b: Keyboard data for column 12
0x1c: CRC8 of keyboard data from 0x10-0x0b
The register is set to 0x00 or 0xff after the operation completes.
0xff means error.
0x20: Writing value 0x53 ('S') to this register stops the main app from
0x22: Writing value 0x53 ('S') to this register stops the main app from
jumping to the user app.
0x50: Self-test results (32 bytes)
...
0x6f:
0x70: Flashing mode unlock key
(writing 0x46 to this register unlocks the flashing mode.)
0x70: 128B block of EEPROM data (either read from code memory or to be
... written)
0xef
0xf0: target address low byte
0xf1: target address high byte
0xf2: CRC8 calculated for the 128B block of data from 0x70-0xef
- this must be written by the user when preparing data for write
operation, MCU checks the CRC8 of the data and compares it against
the value in this register before starting the execution of
0x57 command
- this is updated by the MCU after reading the data from flash memory
so the user can check the checksum against the data read from
0x70-0xef
0xf3: Flashing mode unlock key
- writing 0x46 to this register unlocks the flashing mode.
- this register is cleared after command completion
0x71: Flashing control
0xf4: Flashing control
Writing various commands to this register makes the MCU execute them,
if the MCU is not executing the previous command. Available commands:
@ -79,36 +104,30 @@ Registers @@ -79,36 +104,30 @@ Registers
This register will ignore further commands as long as the last operation
is still in progress. This register will contain the code of the
currently executed operation, and will be cleared after the operation
finishes. Completion is also signalled by pulsing the INT pin shortly.
finishes.
If the operation fails, this register will contain value 0xff. If it
succeeds it will contain value 0x00.
0x7d: target address low byte
0x7e: target address high byte
0x7f: CRC8 calculated for the 128B block of data from 0x80-0xff
- this must be written by the user when preparing data for write
operation, MCU checks the CRC8 of the data and compares it against
the value in this register before starting the execution of
0x57 command
- this is updated by the MCU after reading the data from flash memory
0x80: 128B block of EEPROM data (either read from code memory or to be
... written)
0xff
0xff: Debug log
- reading from this register returns next character of the debug log
or 0
- register address is not auto-advanced for the 0xff address, so you
can read any number of bytes from 0xff to get the debug log text
- debug log stores results of self-tests
Usage
-----
User can modify register 0x03 to choose how the firmware should operate.
User can modify register 0x20 to choose how the firmware should operate.
The settings are not persistent across resets.
To read the keyboard matrix status, the user can perform a 13B read transaction
from address 0x10 and calculate CRC8 on the first 12 bytes and compare it with
the 13th byte.
from address 0x07. Data for each column start at 0x08, 0x07 contains CRC8 checksum
of the 12 bytes starting from 0x08.
You can verify the data by calculating CRC8 on the last 12 bytes and compare it
with the 1st byte.
Bit 0 corresponds to row 1, bit 5 to row 6, bits 6 and 7 are always 0.
@ -122,23 +141,43 @@ The firmware is split into 3 parts: @@ -122,23 +141,43 @@ The firmware is split into 3 parts:
0x2000 - 0x4000: Stock FOSS firmware (flashable from stock bootloader)
0x4000 - 0x8000: User app (optional, flashable over I2C from stock FOSS firmware)
When the stock FOSS firmware runs after MCU powerup or reset, it will wait for 200ms
and listen on I2C. If 0x53 is not written to register 0x20 during that time and
the user's app is flashed and commited, it will redirect interrupt vectors to
0x4000+offset and jump to 0x4000, which will start executing user's app.
It is necessary to execute stock firmware to flash the user firmware. Stock
firmware will normally re-direct execution to user firmware if user firmware
is flashed.
After MCU powerup or reset, stock firmware will listen for 1s on I2C interface,
before passing control to the user firmware. If 0x53 is not written to register
0x22 during that time, this will prevent execution of the user's firmware.
If the user firmware execution is not prevented in this way, stock firmware
will redirect interrupt vectors from 0x0000+offset to 0x4000+offset and jump
to 0x4000, which will start execution of user's firmware. User firmware must
not change last byte of IRAM, because it is used by the stock firmware's
interrupt redirection mechanism.
User's firmware should always implement some way to reset the MCU via I2C
interface. This will make development and flashing easier. Failing that,
I2C flashing tool also allows for manual entry to stock firmware which
is done by holding the power key for > 12s to force power cycle on the MCU.
User app should always return value 0x00 when reading register 0x20, to make it
easy to distinguish that the "stay in stock app" command succeeded.
Flashing steps:
1) Unlock by writing 0x46 to register 0x70
2) Write address to 0x7d/0x7e
3) Write data to 0x80-0xff and CRC8 to 0x7f
4) Write command 0x57 to 0x71
5) Poll 0x71 for result (either 0x00 or 0xff)
... repeat 2-5 for all memory locations to be flashed
6) Write command 0x43 to reg 0x71
7) Wait for success
1) Write 128 B of data to 0x70-0xef and data CRC8 to 0xf2
2) Write address to 0xf0 (low) and 0xf1 (high byte)
(only range from 0x4000 to 0x7fff) is writeable
3) Unlock by writing 0x46 to register 0xf3
4) Write command 0x57 to 0xf4
5) Poll 0xf4 for result (either 0x00 or 0xff)
... repeat 1-5 for all memory locations to be flashed
6) Write command 0x43 to reg 0xf4
7) Poll 0xf4 for result (either 0x00 or 0xff)
8) Reset the MCU by writing command 0x52 to reg 0x21
Self-tests
----------
Reset the MCU.
1) Write command 'r' or 'c' to reg 0x21
2) Read repeatedly from register 0xff to get the self-test results
in human readable text form

51
TODO

@ -1,29 +1,34 @@ @@ -1,29 +1,34 @@
Firmware
--------
- mask R5:C4 and R4:C2 because these are always on on my prototype
and prevent development of idle functionality
(Z key is stuck)
C1 2 3 4 5 6 7 8 9 10 11 12
R1 . . . . . . . . . . . .
R2 . . . . . . . . . . . .
R3 . . . . . . . . . . . .
R4 . X . . . . . . . . . .
R5 . . . X . . . . . . . .
R6 . . . . . . . . . . . .
- sleep when no key is pressed and wakeup using pin change interrupt
on P6 port
- wakeup on I2C activity only happens on device address match, so
we can only sleep again after the entire transfer completes (how
to detect that though?)
USB Flashing Tool
-----------------
- investigate reason for URB errors/empty (in command status INT transfer)
- implement I2C configuration options
- disable keyboard matrix scanning
- only enable USB in stock FW upon request over I2C (idling USB stack takes
about 0.6mA on top of 1.8mA baseline)
- first I2C TX after switch to USER firmware after flasging over I2C fails,
subsequent ones work
- also power consumption indicates power consumption of the sotck firmware
so maybe we just sleep in stock instead of switching after flashing?
- power measurement / optimization
- currently with no USB stack and in constant PD mode: 1.8mA
- theoretical lower limit 0.5mA (no idea how they achieve it)
Charger behavior
----------------
- does the charger turn off VOUT after some period of low load?
- does it enable it again to see if there's a need for it?
- this auto-off behavior will kill the power to the keyboard
controller, so we need a way to detect it and power the controller
from the pinephone side by enabling VBUS.
- tricky!
- or make the user keep pressing the kb power button to use the
keyboard for a while, like in a train conductor has to in a train :)
- not good!
Userspace input device daemon

BIN
builds/fw.bin

Binary file not shown.

3
charger/build.sh

@ -1,3 +0,0 @@ @@ -1,3 +0,0 @@
#!/bin/sh
gcc -o kbpower main.c || exit 1

385
charger/main.c

@ -1,385 +0,0 @@ @@ -1,385 +0,0 @@
/*
* Pinephone keyboard power management daemon/tool.
*
* Copyright (C) 2021 Ondřej Jirman <megi@xff.cz>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// {{{ includes
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sys/types.h>
#include <dirent.h>
#include <poll.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
#include <linux/gpio.h>
//#include <i2c/smbus.h>
#define DEBUG 1
#if DEBUG
#define debug(args...) printf(args)
#else
#define debug(args...)
#endif
// }}}
// {{{ utils
static void syscall_error(int is_err, const char* fmt, ...)
{
va_list ap;
if (!is_err)
return;
fprintf(stderr, "ERROR: ");
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, ": %s\n", strerror(errno));
exit(1);
}
static void error(const char* fmt, ...)
{
va_list ap;
fprintf(stderr, "ERROR: ");
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, "\n");
exit(1);
}
bool read_file(const char* path, char* buf, size_t size)
{
int fd;
ssize_t ret;
fd = open(path, O_RDONLY);
if (fd < 0)
return false;
ret = read(fd, buf, size);
close(fd);
if (ret < 0)
return false;
if (ret < size) {
buf[ret] = 0;
return true;
} else {
buf[size - 1] = 0;
return false;
}
}
#define BIT(n) (1u << (n))
#define KB_ADDR 0x15
#define POWER_ADDR 0x75
static int pogo_i2c_open(void)
{
int ret;
char path[256], buf[1024];
int fd = -1;
for (int i = 0; i < 8; i++) {
snprintf(path, sizeof path, "/sys/class/i2c-adapter/i2c-%d/uevent", i);
if (!read_file(path, buf, sizeof buf))
continue;
if (!strstr(buf, "OF_FULLNAME=/soc/i2c@1c2b400"))
continue;
snprintf(path, sizeof path, "/dev/i2c-%d", i);
int fd = open(path, O_RDWR);
syscall_error(fd < 0, "open(%s) failed");
//ret = ioctl(fd, I2C_SLAVE, addr);
//syscall_error(ret < 0, "I2C_SLAVE failed");
return fd;
}
error("Can't find POGO I2C adapter");
return -1;
}
uint8_t read_power(int fd, uint8_t reg)
{
int ret;
uint8_t val;
struct i2c_msg msgs[] = {
{ POWER_ADDR, 0, 1, &reg }, // address
{ POWER_ADDR, I2C_M_RD, 1, &val },
};
struct i2c_rdwr_ioctl_data msg = {
.msgs = msgs,
.nmsgs = sizeof(msgs) / sizeof(msgs[0])
};
ret = ioctl(fd, I2C_RDWR, &msg);
syscall_error(ret < 0, "I2C_RDWR failed");
return val;
}
void write_power(int fd, uint8_t reg, uint8_t val)
{
int ret;
uint8_t buf[] = {reg, val};
struct i2c_msg msgs[] = {
{ POWER_ADDR, 0, 2, buf },
};
// printf("wr 0x%02hhx: %02hhx\n", reg, val);
struct i2c_rdwr_ioctl_data msg = {
.msgs = msgs,
.nmsgs = sizeof(msgs) / sizeof(msgs[0])
};
ret = ioctl(fd, I2C_RDWR, &msg);
syscall_error(ret < 0, "I2C_RDWR failed");
}
void update_power(int fd, uint8_t reg, uint8_t mask, uint8_t val)
{
uint8_t tmp;
tmp = read_power(fd, reg);
tmp &= ~mask;
tmp |= val & mask;
write_power(fd, reg, tmp);
}
// bits 4-0 are mapped to gpio4 - gpio0
//#define MFP_CTL0 0x51
//#define MFP_CTL1 0x52
//#define GPIO_INEN 0x53
//#define GPIO_OUTEN 0x54
//#define GPIO_DATA 0x55
#define BATVADC_DAT_L 0xa2
#define BATVADC_DAT_H 0xa3
#define BATIADC_DAT_L 0xa4
#define BATIADC_DAT_H 0xa5
#define BATOCV_DAT_L 0xa8
#define BATOCV_DAT_H 0xa9
// in mV
unsigned get_bat_voltage(int fd)
{
unsigned l = read_power(fd, BATVADC_DAT_L);
unsigned h = read_power(fd, BATVADC_DAT_H);
if (h & 0x20)
return 2600 - ((~l & 0xff) + ((~h & 0x1f) << 8) + 1) * 1000 / 3724;
return 2600 + (l + (h << 8)) * 1000 / 3724;
}
int get_bat_current(int fd)
{
unsigned l = read_power(fd, BATIADC_DAT_L);
unsigned h = read_power(fd, BATIADC_DAT_H);
if (h & 0x20)
return - (int)((~l & 0xff) + ((~h & 0x1f) << 8) + 1) * 1000 / 1341;
return (l + (h << 8)) * 1000 / 1341;
}
unsigned get_bat_oc_voltage(int fd)
{
unsigned l = read_power(fd, BATOCV_DAT_L);
unsigned h = read_power(fd, BATOCV_DAT_H);
if (h & 0x20)
return 2600 - ((~l & 0xff) + ((~h & 0x1f) << 8) + 1) * 1000 / 3724;
return 2600 + (l + (h << 8)) * 1000 / 3724;
}
enum {
POWER_CHARGER_ENABLED,
POWER_VOUT_ENABLED,
POWER_VOUT_AUTO,
};
#define SYS_CTL0 0x01
#define SYS_CTL1 0x02
#define SYS_CTL2 0x0c
#define SYS_CTL3 0x03
#define SYS_CTL4 0x04
#define SYS_CTL5 0x07
#define Charger_CTL1 0x22
#define Charger_CTL2 0x24
#define CHG_DIG_CTL4 0x25
void power_setup(int fd, unsigned flags)
{
update_power(fd, SYS_CTL1, 0x03, 0x00); // disable automatic control based on load detection
update_power(fd, SYS_CTL0, 0x1e, BIT(1) | BIT(2)); // 2=boost 1=charger enable
update_power(fd, SYS_CTL3, BIT(5), 0); // disable "2x key press = shutdown" function
update_power(fd, SYS_CTL4, BIT(5), 0); // disable "VIN pull out -> VOUT auto-enable" function
update_power(fd, CHG_DIG_CTL4, 0x1f, 15); // set charging current (in 100mA steps)
}
#define READ0 0x71
#define READ1 0x72
#define READ2 0x77
const char* get_chg_status_text(uint8_t s)
{
switch (s) {
case 0: return "Idle";
case 1: return "Trickle charge";
case 2: return "Constant current phase";
case 3: return "Constant voltage phase";
case 4: return "Constant voltage stop";
case 5: return "Full";
case 6: return "Timeout";
default: return "Unknown";
}
}
void power_status(int fd)
{
uint8_t r0 = read_power(fd, READ0);
uint8_t r1 = read_power(fd, READ1);
uint8_t r2 = read_power(fd, READ2);
printf("Charger: %s (%s%s%s%s%s%s%s)\n", get_chg_status_text((r0 >> 5) & 0x7),
r0 & BIT(4) ? " chg_op" : "",
r0 & BIT(3) ? " chg_end" : "",
r0 & BIT(2) ? " cv_timeout" : "",
r0 & BIT(1) ? " chg_timeout" : "",
r0 & BIT(0) ? " trickle_timeout" : "",
r1 & BIT(6) ? " VIN overvoltage" : "",
r1 & BIT(5) ? " <= 75mA load" : ""
);
printf("Button: %02hhx (%s%s%s%s)\n", r2,
r2 & BIT(3) ? " btn_press" : " btn_not_press",
r2 & BIT(2) ? " double_press" : "",
r2 & BIT(1) ? " long_press" : "",
r2 & BIT(0) ? " short_press" : ""
);
printf("0x70: %02hhx\n", read_power(fd, 0x70));
update_power(fd, READ2, 0x7, 0x7);
}
/*
- Independent control of
- Boost (5V VOUT to power Pinephone)
- Battery Charger
- Optional automatic power on when load is inserted
- Optional auto enable of VOUT when disconnecting VIN (reg 0x04)
- Optiobal automatic shutdown when VOUT has light load (customizable via reg
0x0c, min. is 100mA, shutdown lastsa 8-64s (see reg 0x04))
- Charger_CTL1 0x22
- Control of charging current based on VOUT undervoltage (it tries
to keep VOUT in a certain range by reducing load on VIN by
decreasing charging current?)
- Battery type selection 4.2/4.3/4.35V
- + extra margin 0-42mV during constant voltage phase?
- External (via VSET pin) or internal setting (via reg 0x24)
- Charging current selection (100mA - 2.3A ?)
- Charging status register
- charging state - idle, trickle, constant voltage/current phase, full,
timeout
- LED heavey load indication
- VIN overvoltage indication (> 5.6V)
- Button press status
- current state: UP/DOWN
- long press
- short press
GPIO:
- KEY input
- Long press button time selection 1-4s
- Enable/disable 2x short press shutdown function
- L3/L4 function selection:
- GPIO0/1
- normal function
- LIGHT pin function selection:
- GPIO2
- VREF
- WLED
- VSET
- VSET (normal function to select battery voltage via PIN setting)
- GPIO4
- RSET
- GPIO3
- battery internal resistance selection via resistor on the RSET pin
- separate input/output enable register for all 5 GPIOs
- GPIO data register to read/write values to pins
ADC:
- 14 bit two register VBAT, IBAT, VBAT_OCV readings
*/
int main(int ac, char* av[])
{
int fd, ret;
fd = pogo_i2c_open();
printf("V=%u mV (OCV %u mV) I=%d mA\n", get_bat_voltage(fd), get_bat_oc_voltage(fd), get_bat_current(fd));
// uint8_t v = read_power(fd, 2);
// printf("%02hhx\n", v);
return 0;
}

387
common.c

@ -0,0 +1,387 @@ @@ -0,0 +1,387 @@
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <poll.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sys/types.h>
#include <dirent.h>
#include <getopt.h>
#include <linux/usbdevice_fs.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
#include <linux/gpio.h>
#define BIT(n) (1u << (n))
#define KB_ADDR 0x15
#define POWER_ADDR 0x75
static bool verbose;
#define debug(args...) { if (verbose) printf(args); }
static void syscall_error(int is_err, const char* fmt, ...)
{
va_list ap;
if (!is_err)
return;
fprintf(stderr, "ERROR: ");
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, ": %s\n", strerror(errno));
exit(1);
}
static void error(const char* fmt, ...)
{
va_list ap;
fprintf(stderr, "ERROR: ");
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, "\n");
exit(1);
}
static bool read_file(const char* path, char* buf, size_t size)
{
int fd;
ssize_t ret;
fd = open(path, O_RDONLY);
if (fd < 0)
return false;
ret = read(fd, buf, size);
close(fd);
if (ret < 0)
return false;
if (ret < size) {
buf[ret] = 0;
return true;
} else {
buf[size - 1] = 0;
return false;
}
}
static int open_usb_dev(uint16_t vid, uint16_t pid)
{
char path[256], buf[256];
struct dirent *e;
unsigned e_vid, e_pid, bus, dev;
int fd = -1, ret;
DIR* d;
d = opendir("/sys/bus/usb/devices");
syscall_error(d == NULL, "opendir(/sys/bus/usb/devices) failed");
while (true) {
errno = 0;
e = readdir(d);
syscall_error(e == NULL && errno, "readdir(/sys/bus/usb/devices) failed");
if (!e)
break;
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, ".."))
continue;
snprintf(path, sizeof path,
"/sys/bus/usb/devices/%s/idVendor", e->d_name);
if (!read_file(path, buf, sizeof buf))
continue;
ret = sscanf(buf, "%x", &e_vid);
if (ret != 1)
error("Failed to parse %s", path);
snprintf(path, sizeof path,
"/sys/bus/usb/devices/%s/idProduct", e->d_name);
if (!read_file(path, buf, sizeof buf))
continue;
ret = sscanf(buf, "%x", &e_pid);
if (ret != 1)
error("Failed to parse %s", path);
if (e_vid == vid && e_pid == pid) {
snprintf(path, sizeof path,
"/sys/bus/usb/devices/%s/busnum", e->d_name);
if (!read_file(path, buf, sizeof buf))
error("Failed to read %s", path);
ret = sscanf(buf, "%u", &bus);
if (ret != 1)
error("Failed to parse %s", path);
snprintf(path, sizeof path,
"/sys/bus/usb/devices/%s/devnum", e->d_name);
if (!read_file(path, buf, sizeof buf))
error("Failed to read %s", path);
ret = sscanf(buf, "%u", &dev);
if (ret != 1)
error("Failed to parse %s", path);
snprintf(path, sizeof path,
"/dev/bus/usb/%03u/%03u", bus, dev);
debug("Found %04x:%04x at %s\n", e_vid, e_pid, path);
fd = open(path, O_RDWR);
syscall_error(fd < 0, "open(%s) failed", path);
break;
}
}
errno = ENOENT;
closedir(d);
return fd;
}
static int handle_urb(int usb_fd, struct usbdevfs_urb* urb, int timeout)
{
int ret;
struct usbdevfs_urb* reaped_urb;
int retries = 0;
retry:
ret = ioctl(usb_fd, USBDEVFS_SUBMITURB, urb);
if (ret < 0)
return ret;
struct pollfd fd = {
.fd = usb_fd,
.events = POLLOUT,
};
ret = poll(&fd, 1, timeout);
if (ret <= 0) {
if (ret == 0)
errno = ETIMEDOUT;
int save_errno = errno;
// on timeout or other poll error, we need to discard and reap the submitted URB
ret = ioctl(usb_fd, USBDEVFS_DISCARDURB, urb);
// even if discard fails, URB may still be reapable, we need to try reaping anyway
ret = ioctl(usb_fd, USBDEVFS_REAPURBNDELAY, &reaped_urb);
// reap must immediately succeed, otherwise this is fatal
syscall_error(ret < 0, "USBDEVFS_REAPURBNDELAY failed");
errno = save_errno;
return -1;
}
// hopefully POLLERR means we get some error immediately on reap
ret = ioctl(usb_fd, USBDEVFS_REAPURB, &reaped_urb);
if (ret < 0)
return ret;
// EPROTO errors are recoverable
if (urb->status == -71 && retries < 3) {
retries++;
goto retry;
}
if (urb->status != 0) {
errno = -urb->status;
return -1;
}
return 0;
}
static int pogo_i2c_open(void)
{
int ret;
char path[256], buf[1024];
int fd = -1;
for (int i = 0; i < 8; i++) {
snprintf(path, sizeof path, "/sys/class/i2c-adapter/i2c-%d/uevent", i);
if (!read_file(path, buf, sizeof buf))
continue;
if (!strstr(buf, "OF_FULLNAME=/soc/i2c@1c2b400"))
continue;
snprintf(path, sizeof path, "/dev/i2c-%d", i);
int fd = open(path, O_RDWR);
syscall_error(fd < 0, "open(%s) failed");
return fd;
}
error("Can't find POGO I2C adapter");
return -1;
}
ssize_t xwrite(int fd, uint8_t* buf, size_t len)
{
size_t off = 0;
while (off < len) {
ssize_t ret = write(fd, buf + off, len - off);
if (ret < 0)
return ret;
off += ret;
}
return off;
}
static const uint8_t crc8_0x7_table[] = {
0x00, 0x07, 0x0e, 0x09, 0x1c, 0x1b, 0x12, 0x15,
0x38, 0x3f, 0x36, 0x31, 0x24, 0x23, 0x2a, 0x2d,
0x70, 0x77, 0x7e, 0x79, 0x6c, 0x6b, 0x62, 0x65,
0x48, 0x4f, 0x46, 0x41, 0x54, 0x53, 0x5a, 0x5d,
0xe0, 0xe7, 0xee, 0xe9, 0xfc, 0xfb, 0xf2, 0xf5,
0xd8, 0xdf, 0xd6, 0xd1, 0xc4, 0xc3, 0xca, 0xcd,
0x90, 0x97, 0x9e, 0x99, 0x8c, 0x8b, 0x82, 0x85,
0xa8, 0xaf, 0xa6, 0xa1, 0xb4, 0xb3, 0xba, 0xbd,
0xc7, 0xc0, 0xc9, 0xce, 0xdb, 0xdc, 0xd5, 0xd2,
0xff, 0xf8, 0xf1, 0xf6, 0xe3, 0xe4, 0xed, 0xea,
0xb7, 0xb0, 0xb9, 0xbe, 0xab, 0xac, 0xa5, 0xa2,
0x8f, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9d, 0x9a,
0x27, 0x20, 0x29, 0x2e, 0x3b, 0x3c, 0x35, 0x32,
0x1f, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0d, 0x0a,
0x57, 0x50, 0x59, 0x5e, 0x4b, 0x4c, 0x45, 0x42,
0x6f, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7d, 0x7a,
0x89, 0x8e, 0x87, 0x80, 0x95, 0x92, 0x9b, 0x9c,
0xb1, 0xb6, 0xbf, 0xb8, 0xad, 0xaa, 0xa3, 0xa4,
0xf9, 0xfe, 0xf7, 0xf0, 0xe5, 0xe2, 0xeb, 0xec,
0xc1, 0xc6, 0xcf, 0xc8, 0xdd, 0xda, 0xd3, 0xd4,
0x69, 0x6e, 0x67, 0x60, 0x75, 0x72, 0x7b, 0x7c,
0x51, 0x56, 0x5f, 0x58, 0x4d, 0x4a, 0x43, 0x44,
0x19, 0x1e, 0x17, 0x10, 0x05, 0x02, 0x0b, 0x0c,
0x21, 0x26, 0x2f, 0x28, 0x3d, 0x3a, 0x33, 0x34,
0x4e, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5c, 0x5b,
0x76, 0x71, 0x78, 0x7f, 0x6a, 0x6d, 0x64, 0x63,
0x3e, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2c, 0x2b,
0x06, 0x01, 0x08, 0x0f, 0x1a, 0x1d, 0x14, 0x13,
0xae, 0xa9, 0xa0, 0xa7, 0xb2, 0xb5, 0xbc, 0xbb,
0x96, 0x91, 0x98, 0x9f, 0x8a, 0x8d, 0x84, 0x83,
0xde, 0xd9, 0xd0, 0xd7, 0xc2, 0xc5, 0xcc, 0xcb,
0xe6, 0xe1, 0xe8, 0xef, 0xfa, 0xfd, 0xf4, 0xf3
};
static uint8_t crc8(const uint8_t *pdata, size_t nbytes)
{
unsigned int idx;
uint8_t crc = 0;
while (nbytes--) {
idx = (crc ^ *pdata);
crc = (crc8_0x7_table[idx]) & 0xff;
pdata++;
}
return crc;
}
uint64_t time_abs(void)
{
struct timespec tmp;
int ret;
ret = clock_gettime(CLOCK_MONOTONIC, &tmp);
if (ret < 0)
return 0;
return tmp.tv_sec * 1000000000ull + tmp.tv_nsec;
}
static int gpiochip_open(const char* match)
{
int ret;
char path[256], buf[1024];
int fd = -1;
for (int i = 0; i < 8; i++) {
snprintf(path, sizeof path, "/sys/bus/gpio/devices/gpiochip%d/uevent", i);
if (!read_file(path, buf, sizeof buf))
continue;
if (!strstr(buf, match))
continue;
snprintf(path, sizeof path, "/dev/gpiochip%d", i);
int fd = open(path, O_RDWR);
syscall_error(fd < 0, "open(%s) failed");
return fd;
}
error("Can't find %s gpiochip", match);
return -1;
}
static int gpio_setup_pl12(unsigned flags)
{
int ret;
struct gpio_v2_line_request req = {
.num_lines = 1,
.offsets[0] = 12,
.config.flags = flags,
.consumer = "ppkbd",
};
int fd = gpiochip_open("OF_FULLNAME=/soc/pinctrl@1f02c00");
ret = ioctl(fd, GPIO_V2_GET_LINE_IOCTL, &req);
syscall_error(ret < 0, "GPIO_V2_GET_LINE_IOCTL failed");
close(fd);
return req.fd;
}
static int gpio_get_value(int lfd)
{
int ret;
struct gpio_v2_line_values vals = {
.mask = 1,
};
ret = ioctl(lfd, GPIO_V2_LINE_GET_VALUES_IOCTL, &vals);
syscall_error(ret < 0, "GPIO_V2_GET_LINE_IOCTL failed");
return vals.bits & 0x1;
}
static int gpio_set_value(int lfd, int val)
{
int ret;
struct gpio_v2_line_values vals = {
.mask = 1,
};
ret = ioctl(lfd, GPIO_V2_LINE_GET_VALUES_IOCTL, &vals);
syscall_error(ret < 0, "GPIO_V2_GET_LINE_IOCTL failed");
return vals.bits & 0x1;
}

45
firmware/build.sh

@ -5,6 +5,45 @@ set -e @@ -5,6 +5,45 @@ set -e
rm -rf build
mkdir -p build
sdcc -mmcs51 --iram-size 256 --xram-size 2048 --code-size 0x6000 --code-loc 0x2000 --opt-code-size -I. main.c -o build/fw.ihx
makebin build/fw.ihx build/fw.bin
dd if=bootloader.bin of=build/fw.bin conv=notrunc &>/dev/null
hex2bin()
{
local name="$1"
makebin build/$name.ihx build/$name.bin
dd if=bootloader.bin of=build/$name.bin conv=notrunc &>/dev/null
}
# build stock FW
cpp -P -nostdinc -I. -D__ASM_ONLY__ stock-ivt.asm build/stock-ivt.asm
sdas8051 -plosgff build/stock-ivt.rel build/stock-ivt.asm
echo Stock FW
sdcc \
-mmcs51 --iram-size 256 --xram-size 2048 \
--code-size 0x2000 --code-loc 0x2130 \
-Wl-bIVECT=0x2000 \
-I. \
-DFW_REVISION_STR="\"$(git describe) $(git log -1 --format=%cd --date=iso)\"" \
-DCONFIG_STOCK_FW=1 \
build/stock-ivt.rel main.c \
-o build/fw-stock.ihx
hex2bin fw-stock
# build user FW
echo User FW
sdcc \
-mmcs51 --iram-size 255 --xram-size 2048 \
--code-size 0x4000 --code-loc 0x4000 \
-I. \
-DFW_REVISION_STR="\"$(git describe) $(git log -1 --format=%cd --date=iso)\"" \
-DCONFIG_STOCK_FW=0 \
-DCONFIG_USB_STACK=0 \
-DCONFIG_DEBUG_LOG=1 \
-DCONFIG_SELFTEST=0 \
main.c \
-o build/fw-user.ihx
hex2bin fw-user

13
firmware/em85f684a.h

@ -20,6 +20,8 @@ @@ -20,6 +20,8 @@
#ifndef __EM85F684A_H__
#define __EM85F684A_H__
#ifndef __ASM_ONLY__
__sfr __at(0x87) PCON; // Power Control
__sfr __at(0xc0) RSTSC; // Reset Source
__sfr __at(0xbf) P0_PRST; // Peripheral Reset
@ -105,11 +107,9 @@ __sfr __at(0xc4) P0_I2CADB; // I2CA Data Buffer Register @@ -105,11 +107,9 @@ __sfr __at(0xc4) P0_I2CADB; // I2CA Data Buffer Register
__sfr __at(0xc5) P0_I2CADAL; // I2CA Device Address Register L
__sfr __at(0xc6) P0_I2CADAH; // I2CA Device Address Register H
__sfr __at(0xc7) P0_I2CASF; // I2CA status flag
__sfr __at(0xcd) P0_DEVPD1; // Peripheral power down
__sfr __at(0xce) P0_DEVPD2; // Peripheral power down
__sfr __at(0xcf) P0_DEVPD3; // Peripheral power down
__sfr __at(0xd1) P0_SMBTO1; // SMbus Time Out 1 Register
__sfr __at(0xd2) P0_SMBTR1; // SMbus Timer reload 1 Register
__sfr __at(0xd3) P0_SMBTO2; // SMbus Time Out 2 Register
@ -273,19 +273,24 @@ __sbit __at(0xea) P92; @@ -273,19 +273,24 @@ __sbit __at(0xea) P92;
__sbit __at(0xe9) P91;
__sbit __at(0xe8) P90;
#endif
#define IRQ_EINT0 0 // External Interrupt 0
#define IRQ_TIMER0 1 // Timer0 Overflow
#define IRQ_EINT1 2 // External Interrupt 1
#define IRQ_TIMER1 3 // Timer1 Overflow
#define IRQ_UART0 4 // Serial Port 0
#define IRQ_PINCHANGE 6 // PIN CHANGE Interrupt 0
#define IRQ_LVD 7 // Low voltage detect Interrupt
#define IRQ_SYSTEMHOLD 8 // System Hold Interrupt
#define IRQ_INT2_3 10 // External Interrupt 2~3
#define IRQ_SPI 11 // SPI Interrupt
#define IRQ_ADC 13 // ADC Conversion Complete
#define IRQ_TIMER2 14 // Timer2 Overflow
#define IRQ_PWMD 12 // PWMD Interrupt
#define IRQ_TIMER3 14 // Timer3 Overflow
#define IRQ_PWMA 15 // PWMA Interrupt
#define IRQ_PWME 16 // PWME Interrupt
#define IRQ_USB 17 // USB Interrupt
#define IRQ_PWMF 18 // PWMF Interrupt
#define IRQ_I2CA 20 // I2CA Interrupt
#define IRQ_PWMB 23 // PWMB Interrupt
#define IRQ_PWMC 24 // PWMC Interrupt

1510
firmware/main.c

File diff suppressed because it is too large Load Diff

71
firmware/registers.h

@ -0,0 +1,71 @@ @@ -0,0 +1,71 @@
/**
* Pinephone Keyboard Firmware
*
* Copyright (C) 2021 Ondřej Jirman <megi@xff.cz>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef __PPKB_I2C_REGISTERS__
#define __PPKB_I2C_REGISTERS__
// defines register API in BCD format (currently 1.0)
// on incompatible change this may need to be changed
#define FW_REVISION 0x10
#define REG_DEVID_K 0x00
#define REG_DEVID_B 0x01
#define REG_FW_REVISION 0x02
#define REG_FW_FEATURES 0x03
#define REG_FW_FEATURES_USB_DEBUGGER BIT(0)
#define REG_FW_FEATURES_FLASHING_MODE BIT(1)
#define REG_FW_FEATURES_SELF_TEST BIT(2)
#define REG_FW_FEATURES_STOCK_FW BIT(3)
#define REG_KEYMATRIX_SIZE 0x06
#define REG_KEYMATRIX_STATE_CRC8 0x07
#define REG_KEYMATRIX_STATE 0x08
#define REG_KEYMATRIX_STATE_END 0x13
#define REG_SYS_CONFIG 0x20
#define REG_SYS_CONFIG_SCAN_BLOCK BIT(0)
#define REG_SYS_CONFIG_POLL_MODE BIT(1)
#define REG_SYS_CONFIG_USB_DEBUG_EN BIT(2)
#define REG_SYS_COMMAND 0x21
#define REG_SYS_COMMAND_MCU_RESET 'r'
#define REG_SYS_COMMAND_SELFTEST 't'
#define REG_SYS_COMMAND_USB_IAP 'i'
#define REG_SYS_USER_APP_BLOCK 0x22
#define REG_SYS_USER_APP_BLOCK_MAGIC 0x53
#define REG_FLASH_DATA_START 0x70
#define REG_FLASH_DATA_END 0xef
#define REG_FLASH_ADDR_L 0xf0
#define REG_FLASH_ADDR_H 0xf1
#define REG_FLASH_CRC8 0xf2
#define REG_FLASH_UNLOCK 0xf3
#define REG_FLASH_UNLOCK_MAGIC 0x46
#define REG_FLASH_CMD 0xf4
#define REG_FLASH_CMD_READ_ROM 0x52
#define REG_FLASH_CMD_WRITE_ROM 0x57
#define REG_FLASH_CMD_ERASE_ROM 0x45
#define REG_FLASH_CMD_COMMIT 0x43
#define REG_DEBUG_LOG 0xff
#endif

101
firmware/stock-ivt.asm

@ -0,0 +1,101 @@ @@ -0,0 +1,101 @@
/**
* Pinephone Keyboard Firmware
*
* Copyright (C) 2021 Ondřej Jirman <megi@xff.cz>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "em85f684a.h"
.area RSEG (ABS,DATA)
.org 0x0000
ar0 = 0x00
psw0 = 0xd0