//go:build pinetime package board import ( "device/arm" "device/nrf" "machine" "time" "tinygo.org/x/drivers" "tinygo.org/x/drivers/bma42x" "tinygo.org/x/drivers/pixel" "tinygo.org/x/drivers/st7789" ) const ( Name = "pinetime" touchInterruptPin = 28 spiFlashCSPin = machine.Pin(5) chargeIndicationPin = machine.Pin(12) vibrationPin = machine.Pin(16) powerPresencePin = machine.Pin(19) batteryVoltagePin = machine.Pin(31) ) var ( Power = &mainBattery{} Sensors = allSensors{} Display = mainDisplay{} Buttons = &singleButton{} ) func init() { // Enable the DC/DC regulator. // This doesn't affect sleep power consumption, but significantly reduces // runtime power consumpton of the CPU core (almost halving the current // required). nrf.POWER.DCDCEN.Set(nrf.POWER_DCDCEN_DCDCEN) // The UART is left enabled in the Wasp-OS bootloader. // This causes a 1.25mA increase in current consumption. // https://github.com/wasp-os/wasp-bootloader/pull/3 nrf.UART0.ENABLE.Set(0) vibrationPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) } type mainBattery struct { lastPercent int8 chargePPM int32 } var batteryPercent = batteryApproximation{ // Data is taken from this pull request: // https://github.com/InfiniTimeOrg/InfiniTime/pull/1444/files voltages: [6]uint16{3500, 3600, 3700, 3750, 3900, 4180}, percents: [6]int8{0, 10, 25, 50, 75, 100}, } func (b *mainBattery) Configure() { chargeIndicationPin.Configure(machine.PinConfig{Mode: machine.PinInput}) powerPresencePin.Configure(machine.PinConfig{Mode: machine.PinInput}) // Configure the ADC. // Using just one sample (instead of 256 for example), because we have our // own filtering and long sample times actually drain a lot of power: around // 6µA when measuing the battery every 5 seconds. machine.InitADC() machine.ADC{Pin: batteryVoltagePin}.Configure(machine.ADCConfig{ Reference: 3000, SampleTime: 40, // use the longest acquisition time Samples: 1, }) } func (b *mainBattery) Status() (status ChargeState, microvolts uint32, percent int8) { rawValue := machine.ADC{Pin: batteryVoltagePin}.Get() // Formula to calculate microvolts: // rawValue * 6000_000 / 0x10000 // Simlified, to fit in 32-bit integers: // rawValue * (6000_000/128) / (0x1000/128) // rawValue * 46875 / 512 microvolts = uint32(rawValue) * 46875 / 512 isCharging := chargeIndicationPin.Get() == false // low when charging isPowerPresent := powerPresencePin.Get() == false // low when present if isCharging { status = Charging } else if isPowerPresent { status = NotCharging } else { status = Discharging } // TODO: percent while charging percentPPM := batteryPercent.approximatePPM(microvolts) if b.chargePPM == 0 { // first measurement, probably b.chargePPM = percentPPM } else { b.chargePPM = (b.chargePPM*255 + percentPPM) / 256 } newPercent := b.chargePPM / 10000 if newPercent < int32(b.lastPercent) || newPercent > int32(b.lastPercent)+1 { // do some basic hysteresis b.lastPercent = int8(newPercent) } percent = b.lastPercent return } var spi0Configured bool // Return SPI0 initialized and ready to use, configuring it if not already done. func getSPI0() machine.SPI { spi := machine.SPI0 if !spi0Configured { // Set the chip select line for the flash chip to inactive. spiFlashCSPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) spiFlashCSPin.High() // Set the chip select line for the LCD controller to inactive. machine.LCD_CS.Configure(machine.PinConfig{Mode: machine.PinOutput}) machine.LCD_CS.High() // Configure the SPI bus. spi.Configure(machine.SPIConfig{ Frequency: 8_000_000, // 8MHz is the maximum the nrf52832 supports SCK: machine.SPI0_SCK_PIN, SDO: machine.SPI0_SDO_PIN, SDI: machine.SPI0_SDI_PIN, Mode: 3, }) // Put the flash controller in deep power-down. // This is done so that as long as the SPI flash isn't explicitly // initialized, it won't waste any power. spiFlashCSPin.Low() spi.Tx([]byte{0xB9}, nil) // deep power down spiFlashCSPin.High() } return spi } type mainDisplay struct{} var display *st7789.DeviceOf[pixel.RGB444BE] func (d mainDisplay) Configure() Displayer[pixel.RGB444BE] { // Configure the display. // RGB444 reduces theoretic update time by up to 25%, from 115.2ms to 86.4ms // (28.8ms reduction). spi := getSPI0() disp := st7789.NewOf[pixel.RGB444BE](spi, machine.LCD_RESET, machine.LCD_RS, // data/command machine.LCD_CS, machine.LCD_BACKLIGHT_HIGH) // TODO: allow better backlight control disp.Configure(st7789.Config{ Width: 240, Height: 240, Rotation: drivers.Rotation0, RowOffset: 80, FrameRate: st7789.FRAMERATE_39, VSyncLines: 32, // needed for VBlank, not sure why }) disp.EnableBacklight(true) // disable the backlight // Initialize these pins as regular pins too, for WaitForVBlank. machine.LCD_SCK.Configure(machine.PinConfig{Mode: machine.PinOutput}) machine.LCD_SCK.Low() machine.LCD_SDI.Configure(machine.PinConfig{Mode: machine.PinOutput}) display = &disp return display } func (d mainDisplay) MaxBrightness() int { return 1 // TODO: 0-7 is supported } func (d mainDisplay) SetBrightness(level int) { machine.LCD_BACKLIGHT_HIGH.Set(!(level > 0)) // low means on, high means off } func (d mainDisplay) WaitForVBlank(defaultInterval time.Duration) { // Disable the SPI so we can manually communicate with the display. machine.SPI0.Bus.ENABLE.Set(nrf.SPIM_ENABLE_ENABLE_Disabled) // Wait until the scanline wraps around to 0. // This is also what the TE line does internally. // TODO: use time.Sleep() if we can, to save power. for readDisplayValue(st7789.GSCAN, 16) == 0 { } for readDisplayValue(st7789.GSCAN, 16) != 0 { } // Re-enable the SPI. machine.SPI0.Bus.ENABLE.Set(nrf.SPIM_ENABLE_ENABLE_Enabled) } // Wait for enough time between bitbanged high and low SPI pulses. func delaySPIClock() { // 4 cycles, or 62.5ns. // Together with the store, it is 6 cycles or 93.75ns. arm.Asm("nop\nnop\nnop\nnop") } // Read a single value from the display, for example GSCAN, RDDID, etc. // The bits parameter indicates the number of bits that will be received. func readDisplayValue(cmd uint8, bits int) uint32 { const ( cs = machine.LCD_CS dc = machine.LCD_RS sdi = machine.LCD_SDI sck = machine.LCD_SCK ) // Initialize bitbanged SPI. delaySPIClock() cs.Low() dc.Low() sdi.Configure(machine.PinConfig{Mode: machine.PinOutput}) // Clock out the command. for i := 0; i < 8; i++ { sdi.Set(cmd&0x80 != 0) delaySPIClock() sck.High() delaySPIClock() sck.Low() cmd <<= 1 } delaySPIClock() // Dummy clock cycle (necessary for 24-bit and 32-bit read commands, // according to the datasheet). if bits >= 24 { sck.High() delaySPIClock() sck.Low() delaySPIClock() } // Read the result over SPI. sdi.Configure(machine.PinConfig{Mode: machine.PinInputPulldown}) dc.High() value := uint32(0) for i := 0; i < bits; i++ { sck.High() delaySPIClock() value <<= 1 if sdi.Get() { value |= 1 } sck.Low() delaySPIClock() } // Dummy clock cycle, according to the datasheet needed in all cases but in // my exprience only needed for 16-bit reads (GSCAN). if bits == 16 { sck.High() delaySPIClock() sck.Low() delaySPIClock() } // Finish the transaction. cs.High() dc.High() return value } func (d mainDisplay) PPI() int { return 261 } func (d mainDisplay) ConfigureTouch() TouchInput { // Configure touch interrupt pin. // After the pin goes low (for a very short time), the touch controller is // accessible over I2C for as long as a finger touches the screen and a // short time afterwards (a second or so) before going back to sleep. // // We don't actually use an interrupt here because pin change interrupts // result in far too much current consumption (jumping from 0.19mA to // 0.65mA), probably due to anomaly 97: // https://infocenter.nordicsemi.com/index.jsp?topic=%2Ferrata_nRF52832_Rev2%2FERR%2FnRF52832%2FRev2%2Flatest%2Fanomaly_832_97.html // Also see: // https://devzone.nordicsemi.com/f/nordic-q-a/50624/about-current-consumption-of-gpio-and-gpiote // We could use a PORT interrupt in GPIOTE, using it as a level interrupt. // And it would be a good idea to implement this in TinyGo directly (as a // level interrupt), but in the meantime we'll use this quick-n-dirty hack. nrf.P0.PIN_CNF[touchInterruptPin].Set(nrf.GPIO_PIN_CNF_DIR_Input<= 240 || rawY >= 240 { // X or Y are erroneous (this happens quite frequently). // Just return the previous value as a fallback. if touchPoints[0].ID != 0 { return touchPoints[:1] } return nil } x := int16(rawX) y := int16(rawY) if display != nil { // The screen is upside down from the configured rotation, so also // rotate the touch coordinates. if display.Rotation() == drivers.Rotation180 { x = 239 - x y = 239 - y } } touchPoints[0] = TouchPoint{ X: x, Y: y, ID: touchID, } return touchPoints[:1] } return nil } // State for the one and only button on the PineTime. type singleButton struct { state bool previousState bool } func (b *singleButton) Configure() { // BUTTON_OUT must be held high for BUTTON_IN to read anything useful. machine.BUTTON_OUT.Configure(machine.PinConfig{Mode: machine.PinOutput}) machine.BUTTON_OUT.Low() machine.BUTTON_IN.Configure(machine.PinConfig{Mode: machine.PinInput}) } func (b *singleButton) ReadInput() { // BUTTON_OUT needs to be kept low most of the time to avoid a ~34µA current // increase. However, setting it to high just before reading doesn't appear // to be enough: a small delay is needed. This can be done by setting // BUTTON_OUT high multiple times in a row, which doesn't do anything except // introduce the needed delay. // Four stores appear to be enough to get readings, I have added a few more // for more reliable readings (especially as this is important for the // watchdog timer). machine.BUTTON_OUT.High() machine.BUTTON_OUT.High() machine.BUTTON_OUT.High() machine.BUTTON_OUT.High() machine.BUTTON_OUT.High() machine.BUTTON_OUT.High() machine.BUTTON_OUT.High() machine.BUTTON_OUT.High() state := machine.BUTTON_IN.Get() machine.BUTTON_OUT.Low() b.state = state // Reset the watchdog timer only when the button is not pressed. // The watchdog is configured in the Wasp-OS bootloader, and we have to be // careful not to reset the watchdog while the button is pressed so that a // long press forces a WDT reset and lets us enter the bootloader. // For details, see: // https://wasp-os.readthedocs.io/en/latest/wasp.html#watchdog-protocol if !state { nrf.WDT.RR[0].Set(0x6E524635) } } func (b *singleButton) NextEvent() KeyEvent { if b.state == b.previousState { return NoKeyEvent } e := KeyEvent(KeyEnter) if !b.state { e |= keyReleased } b.previousState = b.state return e } var i2cBus *machine.I2C func initI2CBus() { // Run I2C at a high speed (400KHz). i2cBus.Configure(machine.I2CConfig{ Frequency: 400 * machine.KHz, SDA: machine.Pin(6), SCL: machine.Pin(7), }) } func configureI2CBus() { if i2cBus == nil { i2cBus = machine.I2C1 initI2CBus() // Disable the heart rate sensor on startup, to be enabled when a driver // configures it. It consumes around 110µA when left enabled. machine.I2C1.WriteRegister(0x44, 0x0C, []byte{0x00}) } } type allSensors struct { } var accel *bma42x.Device func (s allSensors) Configure(which drivers.Measurement) error { // Configure the accelerometer (either BMA421 or BMA425, depending on the // PineTime variant). accel = bma42x.NewI2C(machine.I2C1, bma42x.Address) err := accel.Configure(bma42x.Config{ Device: bma42x.DeviceBMA421 | bma42x.DeviceBMA425, Features: bma42x.FeatureStepCounting, }) if err != nil { // Restart the I2C bus. // I don't know why, but configuring the BMA421 while it is already // configured freezes the I2C bus. The only recovery appears to be to // restart the I2C bus entirely. initI2CBus() err = accel.Configure(bma42x.Config{ Device: bma42x.DeviceBMA421 | bma42x.DeviceBMA425, Features: bma42x.FeatureStepCounting, }) } return err } func (s allSensors) Update(which drivers.Measurement) error { if which&(drivers.Acceleration|drivers.Temperature) != 0 { err := accel.Update(which & (drivers.Acceleration | drivers.Temperature)) if err != nil { return err } } return nil } func (s allSensors) Acceleration() (x, y, z int32) { rawX, rawY, rawZ := accel.Acceleration() // Adjust accelerometer to match standard axes. x = -rawY y = -rawX z = -rawZ return } func (s allSensors) Steps() (steps uint32) { return accel.Steps() } func (s allSensors) Temperature() int32 { return accel.Temperature() } type Vibro struct{} func (Vibro) High() { vibrationPin.High() } func (Vibro) Low() { vibrationPin.Low() }