-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathboard-pinetime.go
521 lines (451 loc) · 14.8 KB
/
board-pinetime.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
//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)
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)
}
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<<nrf.GPIO_PIN_CNF_DIR_Pos | nrf.GPIO_PIN_CNF_INPUT_Connect<<nrf.GPIO_PIN_CNF_INPUT_Pos | nrf.GPIO_PIN_CNF_SENSE_Low<<nrf.GPIO_PIN_CNF_SENSE_Pos)
configureI2CBus()
return touchInput{}
}
var touchPoints [1]TouchPoint
type touchInput struct{}
var touchID uint32 = 1
var touchData = make([]byte, 6)
var touchInitialized bool
const touchI2CAddress = 0x15
func (input touchInput) ReadTouch() []TouchPoint {
// The touch controller is very sparsely documented. You can find datasheet
// in English and Chinese on the PineTime wiki:
// https://wiki.pine64.org/wiki/PineTime#Component_Datasheets
// The best documentation is in the Chinese documentation, you can use
// Google Translate to translate it to English.
// Read the bit from the LATCH reister, which is set to high when TP_INT
// goes high but doesn't go low on its own. We do that manually once no more
// touches are read from the touch controller.
if nrf.P0.LATCH.Get()&(1<<touchInterruptPin) != 0 {
if !touchInitialized {
// Initialize the touch controller once we get the first touch.
// Doing it this way as the I2C bus appears unresponsive outside a
// touch event.
touchInitialized = true
// These are the values as set by InfiniTime.
// i2cBus.Tx(touchI2CAddress, []byte{0xEC, 0b00000101}, nil)
// i2cBus.Tx(touchI2CAddress, []byte{0xFA, 0b01110000}, nil)
// MotionMask register:
// [0] EnDClick (disabled, enabled in InfiniTime)
// [1] EnConUD (disabled)
// [2] EnConLR (enabled)
i2cBus.Tx(touchI2CAddress, []byte{0xEC, 0b0000_0100}, nil)
// IrqCtl register:
// [7] EnTest (disabled)
// [6] EnTouch (enabled)
// [5] EnChange (enabled)
// [4] EnMotion (enabled)
// [0] OnceWLP (disabled)
i2cBus.Tx(touchI2CAddress, []byte{0xFA, 0b0111_0000}, nil)
}
i2cBus.ReadRegister(touchI2CAddress, 1, touchData)
num := touchData[1] & 0x0f
if num == 0 {
touchID++ // for the next time
// Stop reading touch events.
// There may be a small race condition here, if the touch controller
// detects another touch while reading the touch data over I2C.
nrf.P0.LATCH.Set(1 << touchInterruptPin)
touchPoints[0].ID = 0
return nil
}
rawX := (uint16(touchData[2]&0xf) << 8) | uint16(touchData[3]) // x coord
rawY := (uint16(touchData[4]&0xf) << 8) | uint16(touchData[5]) // y coord
// Filter out erroneous data.
if rawX >= 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()
}