Add CST716 touch support and fused mode support

The P8 smartwatches use the CST716 or CST816S chips in various modes.

The device ID of these chips cannot be used for runtime detection,
because it does not give the hardware revision / firmware configuration.
This commit is contained in:
Christoph Honal 2022-05-10 23:12:28 +02:00
parent 39b0fa806c
commit effa49ba54
6 changed files with 118 additions and 111 deletions

View File

@ -39,7 +39,7 @@ CMake configures the project according to variables you specify the command line
**GDB_CLIENT_BIN_PATH**|Path to arm-none-eabi-gdb executable. Used only if `USE_GDB_CLIENT` is 1.|`-DGDB_CLIENT_BIN_PATH=/home/jf/nrf52/gcc-arm-none-eabi-9-2019-q4-major/bin/arm-none-eabi-gdb` **GDB_CLIENT_BIN_PATH**|Path to arm-none-eabi-gdb executable. Used only if `USE_GDB_CLIENT` is 1.|`-DGDB_CLIENT_BIN_PATH=/home/jf/nrf52/gcc-arm-none-eabi-9-2019-q4-major/bin/arm-none-eabi-gdb`
**GDB_CLIENT_TARGET_REMOTE**|Target remote connection string. Used only if `USE_GDB_CLIENT` is 1.|`-DGDB_CLIENT_TARGET_REMOTE=/dev/ttyACM0` **GDB_CLIENT_TARGET_REMOTE**|Target remote connection string. Used only if `USE_GDB_CLIENT` is 1.|`-DGDB_CLIENT_TARGET_REMOTE=/dev/ttyACM0`
**BUILD_DFU (\*\*)**|Build DFU files while building (needs [adafruit-nrfutil](https://github.com/adafruit/Adafruit_nRF52_nrfutil)).|`-DBUILD_DFU=1` **BUILD_DFU (\*\*)**|Build DFU files while building (needs [adafruit-nrfutil](https://github.com/adafruit/Adafruit_nRF52_nrfutil)).|`-DBUILD_DFU=1`
**TARGET_DEVICE**|Target device, used for the pin map and the low frequency clock source. Allowed: `PINETIME, P8A, P8B`|`-DTARGET_DEVICE=PINETIME` (Default) **TARGET_DEVICE**|Target device, used for the pin map, the low frequency clock source, and the touch driver behavior. Allowed: `PINETIME, P8A, P8B`|`-DTARGET_DEVICE=PINETIME` (Default)
####(**) Note about **CMAKE_BUILD_TYPE**: ####(**) Note about **CMAKE_BUILD_TYPE**:
By default, this variable is set to *Release*. It compiles the code with size and speed optimizations. We use this value for all the binaries we publish when we [release](https://github.com/InfiniTimeOrg/InfiniTime/releases) new versions of InfiniTime. By default, this variable is set to *Release*. It compiles the code with size and speed optimizations. We use this value for all the binaries we publish when we [release](https://github.com/InfiniTimeOrg/InfiniTime/releases) new versions of InfiniTime.

View File

@ -796,12 +796,15 @@ add_definitions(-D__HEAP_SIZE=4096)
add_definitions(-DTARGET_DEVICE_${TARGET_DEVICE}) add_definitions(-DTARGET_DEVICE_${TARGET_DEVICE})
if(TARGET_DEVICE STREQUAL "PINETIME") if(TARGET_DEVICE STREQUAL "PINETIME")
add_definitions(-DCLOCK_CONFIG_LF_SRC=1) # XTAL add_definitions(-DCLOCK_CONFIG_LF_SRC=1) # XTAL
add_definitions(-DDRIVER_TOUCH_DYNAMIC)
elseif(TARGET_DEVICE STREQUAL "P8A") elseif(TARGET_DEVICE STREQUAL "P8A")
add_definitions(-DCLOCK_CONFIG_LF_SRC=1) # XTAL add_definitions(-DCLOCK_CONFIG_LF_SRC=1) # XTAL
add_definitions(-DDRIVER_TOUCH_GESTURE)
elseif(TARGET_DEVICE STREQUAL "P8B") elseif(TARGET_DEVICE STREQUAL "P8B")
add_definitions(-DCLOCK_CONFIG_LF_SRC=0) # RC add_definitions(-DCLOCK_CONFIG_LF_SRC=0) # RC
add_definitions(-DMYNEWT_VAL_BLE_LL_SCA=500) add_definitions(-DMYNEWT_VAL_BLE_LL_SCA=500)
add_definitions(-DCLOCK_CONFIG_LF_CAL_ENABLED=1) add_definitions(-DCLOCK_CONFIG_LF_CAL_ENABLED=1)
add_definitions(-DDRIVER_TOUCH_REPORT)
else() else()
message(FATAL_ERROR "Invalid TARGET_DEVICE") message(FATAL_ERROR "Invalid TARGET_DEVICE")
endif() endif()

View File

@ -1,108 +1,103 @@
#include "drivers/Cst816s.h" #include "drivers/Cst816s.h"
#include "drivers/PinMap.h"
#include <FreeRTOS.h> #include <FreeRTOS.h>
#include <legacy/nrf_drv_gpiote.h> #include <legacy/nrf_drv_gpiote.h>
#include <nrfx_log.h> #include <nrfx_log.h>
#include <task.h> #include <task.h>
#include "drivers/PinMap.h"
using namespace Pinetime::Drivers; using namespace Pinetime::Drivers;
/* References : /*
* References :
* This implementation is based on this article : * This implementation is based on this article :
* https://medium.com/@ly.lee/building-a-rust-driver-for-pinetimes-touch-controller-cbc1a5d5d3e9 Touch panel datasheet (weird chinese * https://medium.com/@ly.lee/building-a-rust-driver-for-pinetimes-touch-controller-cbc1a5d5d3e9 Touch panel datasheet (weird chinese
* translation) : https://wiki.pine64.org/images/5/51/CST816S%E6%95%B0%E6%8D%AE%E6%89%8B%E5%86%8CV1.1.en.pdf * translation) : https://wiki.pine64.org/images/5/51/CST816S%E6%95%B0%E6%8D%AE%E6%89%8B%E5%86%8CV1.1.en.pdf
* *
* TODO : we need a complete datasheet and protocol reference! * TODO: We need a complete datasheet and protocol reference!
* For register desciptions, see Cst816s_registers.h. Information was collected from various chinese datasheets and documents.
* */ * */
Cst816S::Cst816S(TwiMaster& twiMaster, uint8_t twiAddress) : twiMaster {twiMaster}, twiAddress {twiAddress} { Cst816S::Cst816S(TwiMaster& twiMaster, uint8_t twiAddress) : twiMaster {twiMaster}, twiAddress {twiAddress} {
} }
bool Cst816S::Init() { bool Cst816S::Init() {
// Reset the touch driver
nrf_gpio_cfg_output(PinMap::Cst816sReset); nrf_gpio_cfg_output(PinMap::Cst816sReset);
nrf_gpio_pin_clear(PinMap::Cst816sReset); nrf_gpio_pin_clear(PinMap::Cst816sReset);
vTaskDelay(5); vTaskDelay(10);
nrf_gpio_pin_set(PinMap::Cst816sReset); nrf_gpio_pin_set(PinMap::Cst816sReset);
vTaskDelay(50); vTaskDelay(50);
// Wake the touchpanel up // Chip ID is suspected to be dependent on embedded firmware and factory configuration,
uint8_t dummy; // and not (just) on hardware type and revision.
twiMaster.Read(twiAddress, 0x15, &dummy, 1); if (twiMaster.Read(twiAddress, CHIP_ID, &chipId, 1) == TwiMaster::ErrorCodes::TransactionFailed) {
vTaskDelay(5); chipId = 0xFF;
twiMaster.Read(twiAddress, 0xa7, &dummy, 1); }
vTaskDelay(5); // Vendor / project ID and firmware version can vary between devices.
if (twiMaster.Read(twiAddress, PROJ_ID, &vendorId, 1) == TwiMaster::ErrorCodes::TransactionFailed) {
vendorId = 0xFF;
}
if (twiMaster.Read(twiAddress, FW_VERSION, &fwVersion, 1) == TwiMaster::ErrorCodes::TransactionFailed) {
fwVersion = 0xFF;
}
// TODO This function check that the device IDs from the controller are equal to the ones // These configuration settings will be ignored by chips which were
// we expect. However, it seems to return false positive (probably in case of communication issue). // fused / pre-configured in the factory (GESTURE and REPORT settings).
// Also, it seems that some users have pinetimes that works correctly but that report different device IDs // This mainly applies to CST716, however there may be CST816S with static configurations as well.
// Until we know more about this, we'll just read the IDs but not take any action in case they are not 'valid' // The other, freely configureable ones (DYNAMIC), are configured in reporting mode here.
CheckDeviceIds();
/* // Configure motion behaviour
[2] EnConLR - Continuous operation can slide around static constexpr uint8_t motionMask = MOTION_MASK_EN_DCLICK | MOTION_MASK_EN_CON_UD | MOTION_MASK_EN_CON_LR;
[1] EnConUD - Slide up and down to enable continuous operation twiMaster.Write(twiAddress, MOTION_MASK, &motionMask, 1);
[0] EnDClick - Enable Double-click action
*/
static constexpr uint8_t motionMask = 0b00000101;
twiMaster.Write(twiAddress, 0xEC, &motionMask, 1);
/* // Configure interrupt generating events
[7] EnTest - Interrupt pin to test, enable automatic periodic issued after a low pulse. static constexpr uint8_t irqCtl = IRQ_CTL_EN_MOTION | IRQ_CTL_EN_CHANGE | IRQ_CTL_EN_TOUCH;
[6] EnTouch - When a touch is detected, a periodic pulsed Low. twiMaster.Write(twiAddress, IRQ_CTL, &irqCtl, 1);
[5] EnChange - Upon detecting a touch state changes, pulsed Low.
[4] EnMotion - When the detected gesture is pulsed Low.
[0] OnceWLP - Press gesture only issue a pulse signal is low.
*/
static constexpr uint8_t irqCtl = 0b01110000;
twiMaster.Write(twiAddress, 0xFA, &irqCtl, 1);
return true; return true;
} }
Cst816S::TouchInfos Cst816S::GetTouchInfo() { Cst816S::TouchInfos Cst816S::GetTouchInfo() {
// Some chips fail to wake up even though the reset pin has been toggled.
// They only provide an I2C communication window after a touch interrupt,
// so the first touch interrupt is used to force initialisation.
if (firstEvent) {
Init();
firstEvent = false;
// The data registers should now be reset, so this touch event will be detected as invalid.
}
// Read gesture metadata and first touch point coordinate block
Cst816S::TouchInfos info; Cst816S::TouchInfos info;
uint8_t touchData[7]; uint8_t touchData[P1_Y_POS_L + 1];
auto ret = twiMaster.Read(twiAddress, 0x00, touchData, sizeof(touchData));
auto ret = twiMaster.Read(twiAddress, 0, touchData, sizeof(touchData)); if (ret != TwiMaster::ErrorCodes::NoError)
if (ret != TwiMaster::ErrorCodes::NoError) {
info.isValid = false;
return info; return info;
}
// This can only be 0 or 1 // Assemble 12 bit point coordinates from lower 8 bits and higher 4 bits
uint8_t nbTouchPoints = touchData[touchPointNumIndex] & 0x0f; info.x = ((touchData[P1_X_POS_H] & POS_H_POS_MASK) << 8) | touchData[P1_X_POS_L];
uint8_t xHigh = touchData[touchXHighIndex] & 0x0f; info.y = ((touchData[P1_Y_POS_H] & POS_H_POS_MASK) << 8) | touchData[P1_Y_POS_L];
uint8_t xLow = touchData[touchXLowIndex]; // Evaluate number of touch points
uint16_t x = (xHigh << 8) | xLow; info.touching = (touchData[TD_STATUS] & TD_STATUS_MASK) > 0;
uint8_t yHigh = touchData[touchYHighIndex] & 0x0f; // Decode gesture ID
uint8_t yLow = touchData[touchYLowIndex]; info.gesture = static_cast<Gestures>(touchData[GESTURE_ID]);
uint16_t y = (yHigh << 8) | yLow;
Gestures gesture = static_cast<Gestures>(touchData[gestureIndex]);
// Validity check // Validity check, verify value ranges
if (x >= maxX || y >= maxY || info.isValid = (info.x < maxX && info.y < maxY &&
(gesture != Gestures::None && gesture != Gestures::SlideDown && gesture != Gestures::SlideUp && gesture != Gestures::SlideLeft && (info.gesture == Gestures::None || info.gesture == Gestures::SlideDown || info.gesture == Gestures::SlideUp ||
gesture != Gestures::SlideRight && gesture != Gestures::SingleTap && gesture != Gestures::DoubleTap && info.gesture == Gestures::SlideLeft || info.gesture == Gestures::SlideRight || info.gesture == Gestures::SingleTap ||
gesture != Gestures::LongPress)) { info.gesture == Gestures::DoubleTap || info.gesture == Gestures::LongPress));
info.isValid = false;
return info;
}
info.x = x;
info.y = y;
info.touching = (nbTouchPoints > 0);
info.gesture = gesture;
info.isValid = true;
return info; return info;
} }
void Cst816S::Sleep() { void Cst816S::Sleep() {
nrf_gpio_pin_clear(PinMap::Cst816sReset); // This only controls the CST716, the CST816S will ignore this register.
vTaskDelay(5); // The CST816S power state is managed using auto-sleep.
nrf_gpio_pin_set(PinMap::Cst816sReset);
vTaskDelay(50); static constexpr uint8_t sleepValue = PWR_MODE_DEEP_SLEEP;
static constexpr uint8_t sleepValue = 0x03; twiMaster.Write(twiAddress, PWR_MODE_CST716, &sleepValue, 1);
twiMaster.Write(twiAddress, 0xA5, &sleepValue, 1);
NRF_LOG_INFO("[TOUCHPANEL] Sleep"); NRF_LOG_INFO("[TOUCHPANEL] Sleep");
} }
@ -110,21 +105,3 @@ void Cst816S::Wakeup() {
Init(); Init();
NRF_LOG_INFO("[TOUCHPANEL] Wakeup"); NRF_LOG_INFO("[TOUCHPANEL] Wakeup");
} }
bool Cst816S::CheckDeviceIds() {
// There's mixed information about which register contains which information
if (twiMaster.Read(twiAddress, 0xA7, &chipId, 1) == TwiMaster::ErrorCodes::TransactionFailed) {
chipId = 0xFF;
return false;
}
if (twiMaster.Read(twiAddress, 0xA8, &vendorId, 1) == TwiMaster::ErrorCodes::TransactionFailed) {
vendorId = 0xFF;
return false;
}
if (twiMaster.Read(twiAddress, 0xA9, &fwVersion, 1) == TwiMaster::ErrorCodes::TransactionFailed) {
fwVersion = 0xFF;
return false;
}
return chipId == 0xb4 && vendorId == 0 && fwVersion == 1;
}

View File

@ -1,5 +1,6 @@
#pragma once #pragma once
#include "drivers/Cst816s_registers.h"
#include "drivers/TwiMaster.h" #include "drivers/TwiMaster.h"
namespace Pinetime { namespace Pinetime {
@ -7,14 +8,15 @@ namespace Pinetime {
class Cst816S { class Cst816S {
public: public:
enum class Gestures : uint8_t { enum class Gestures : uint8_t {
None = 0x00, None = GESTURE_ID_NONE,
SlideDown = 0x01, SlideDown = GESTURE_ID_SLIDE_DOWN,
SlideUp = 0x02, SlideUp = GESTURE_ID_SLIDE_UP,
SlideLeft = 0x03, SlideLeft = GESTURE_ID_SLIDE_LEFT,
SlideRight = 0x04, SlideRight = GESTURE_ID_SLIDE_RIGHT,
SingleTap = 0x05, SingleTap = GESTURE_ID_SINGLE_TAP,
DoubleTap = 0x0B, DoubleTap = GESTURE_ID_DOUBLE_TAP,
LongPress = 0x0C LongPress = GESTURE_ID_LONG_PRESS,
Invalid = 0xFF
}; };
struct TouchInfos { struct TouchInfos {
uint16_t x = 0; uint16_t x = 0;
@ -46,21 +48,6 @@ namespace Pinetime {
} }
private: private:
bool CheckDeviceIds();
// Unused/Unavailable commented out
static constexpr uint8_t gestureIndex = 1;
static constexpr uint8_t touchPointNumIndex = 2;
// static constexpr uint8_t touchEventIndex = 3;
static constexpr uint8_t touchXHighIndex = 3;
static constexpr uint8_t touchXLowIndex = 4;
// static constexpr uint8_t touchIdIndex = 5;
static constexpr uint8_t touchYHighIndex = 5;
static constexpr uint8_t touchYLowIndex = 6;
// static constexpr uint8_t touchStep = 6;
// static constexpr uint8_t touchXYIndex = 7;
// static constexpr uint8_t touchMiscIndex = 8;
static constexpr uint8_t maxX = 240; static constexpr uint8_t maxX = 240;
static constexpr uint8_t maxY = 240; static constexpr uint8_t maxY = 240;
@ -70,6 +57,8 @@ namespace Pinetime {
uint8_t chipId; uint8_t chipId;
uint8_t vendorId; uint8_t vendorId;
uint8_t fwVersion; uint8_t fwVersion;
bool firstEvent = true;
}; };
} }

View File

@ -379,8 +379,14 @@ void SystemTask::Work() {
// Double Tap needs the touch screen to be in normal mode // Double Tap needs the touch screen to be in normal mode
if (!settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::DoubleTap)) { if (!settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::DoubleTap)) {
// REPORT and GESTURE mode sensors must be normal mode for single tap as well
#if !defined(DRIVER_TOUCH_DYNAMIC)
if (!settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::SingleTap))
#endif
{
touchPanel.Sleep(); touchPanel.Sleep();
} }
}
state = SystemTaskState::Sleeping; state = SystemTaskState::Sleeping;
break; break;

View File

@ -50,11 +50,36 @@ Pinetime::Applications::TouchEvents TouchHandler::GestureGet() {
bool TouchHandler::GetNewTouchInfo() { bool TouchHandler::GetNewTouchInfo() {
info = touchPanel.GetTouchInfo(); info = touchPanel.GetTouchInfo();
if (!info.isValid)
if (!info.isValid) {
return false; return false;
}
// REPORT configurations (P8b variants) of the fused (usually) Cst716
// generate multiple "none" gesture events with info.touching == true during the physical gesture.
// The last event is a e.g. "slide" event with info.touching == true.
// gestureReleased state does not have to be computed manually, instead it occurs when event != "none".
// GESTURE configurations (P8a variants) of the fused (usually) Cst716 generate no events during the physical gesture.
// The only event is a e.g. "slide" event with info.touching == true.
// gestureReleased state does not have to be computed manually, instead it occurs everytime.
// DYNAMIC configurations (PineTime) are configured in reporting mode during initialisation.
// Usually based on the Cst816s, they generate multiple e.g. "slide" gesture events with info.touching == true during the physical
// gesture. The last of these e.g. "slide" events has info.touching == false. gestureReleased state is computed manually by checking for
// the transition to info.touching == false.
// Unfortunately, there is no way to reliably obtain which configuration is used at runtime.
// In all cases, the event is bubbled up once the gesture is released.
#if defined(DRIVER_TOUCH_REPORT)
if (info.gesture != Pinetime::Drivers::Cst816S::Gestures::None) {
gesture = ConvertGesture(info.gesture);
info.touching = false;
}
#elif defined(DRIVER_TOUCH_GESTURE)
if (info.gesture != Pinetime::Drivers::Cst816S::Gestures::None) {
gesture = ConvertGesture(info.gesture);
}
#elif defined(DRIVER_TOUCH_DYNAMIC)
if (info.gesture != Pinetime::Drivers::Cst816S::Gestures::None) { if (info.gesture != Pinetime::Drivers::Cst816S::Gestures::None) {
if (gestureReleased) { if (gestureReleased) {
if (info.gesture == Pinetime::Drivers::Cst816S::Gestures::SlideDown || if (info.gesture == Pinetime::Drivers::Cst816S::Gestures::SlideDown ||
@ -75,15 +100,22 @@ bool TouchHandler::GetNewTouchInfo() {
if (!info.touching) { if (!info.touching) {
gestureReleased = true; gestureReleased = true;
} }
#endif
return true; return true;
} }
void TouchHandler::UpdateLvglTouchPoint() { void TouchHandler::UpdateLvglTouchPoint() {
if (info.touching) { if (info.touching) {
#if defined(DRIVER_TOUCH_GESTURE)
// GESTURE config only generates a single event / state change
// so the LVGL wrapper is used to generate a successive release state update
lvgl.SetNewTap(info.x, info.y);
#else
if (!isCancelled) { if (!isCancelled) {
lvgl.SetNewTouchPoint(info.x, info.y, true); lvgl.SetNewTouchPoint(info.x, info.y, true);
} }
#endif
} else { } else {
if (isCancelled) { if (isCancelled) {
lvgl.SetNewTouchPoint(-1, -1, false); lvgl.SetNewTouchPoint(-1, -1, false);