- commit
- a3d4bd2
- parent
- ddad765
- author
- Neale Pickett
- date
- 2026-02-16 19:22:05 -0700 MST
gpio driver and layouts
12 files changed,
+372,
-61
+46,
-16
1@@ -8,6 +8,7 @@ import (
2 "tinygo.org/x/drivers"
3 "tinygo.org/x/drivers/ssd1306"
4 "git.woozle.org/neale/concertina/pkg/led"
5+ "git.woozle.org/neale/concertina/pkg/pcf8575"
6 )
7
8 // For crying out loud.
9@@ -18,10 +19,10 @@ var Black = color.RGBAModel.Convert(color.Black).(color.RGBA)
10 type Concertina struct {
11 display *ssd1306.Device
12 tonegen MidiWriter
13- i2c drivers.I2C
14+ expanders[4] pcf8575.Device
15 }
16
17-func NewConcertina() (*Concertina, error) {
18+func NewConcertina(i2c drivers.I2C) (*Concertina, error) {
19 // Initialize tone generator
20 if err := machine.DefaultUART.Configure(machine.UARTConfig{
21 BaudRate: 31250,
22@@ -41,13 +42,19 @@ func NewConcertina() (*Concertina, error) {
23
24 c := &Concertina {
25 tonegen: MidiWriter{Writer: machine.DefaultUART},
26- i2c: machine.I2C0,
27+ }
28+
29+ for i := range(4) {
30+ c.expanders[i] = pcf8575.Device{
31+ Addr: 0x20,
32+ I2C: i2c,
33+ }
34 }
35
36 <-resetTimer // Wait until this timer has elapsed
37 VS1053_RESET.High()
38
39- c.display = ssd1306.NewI2C(c.i2c)
40+ c.display = ssd1306.NewI2C(i2c)
41 c.display.Configure(
42 ssd1306.Config{
43 Width: 128,
44@@ -55,6 +62,8 @@ func NewConcertina() (*Concertina, error) {
45 Address: 0x3C,
46 },
47 )
48+ c.display.ClearDisplay()
49+ c.display.Display()
50
51
52 c.tonegen.SetPatch(22) // General MIDI harmonica
53@@ -62,31 +71,52 @@ func NewConcertina() (*Concertina, error) {
54 return c, nil
55 }
56
57+var x int16
58+func (c *Concertina) Loop() {
59+ var buttons [4]uint16
60+
61+ led.Builtin.SetColor(color.RGBA{uint8(x % 2), 0, 0, 0})
62+
63+ c.display.ClearBuffer()
64+ for i := range(4) {
65+ buttons[i] = c.expanders[i].Get()
66+ for bit := range(16) {
67+ pc := White
68+ if buttons[i] & (1<<bit) != 0 {
69+ pc = Black
70+ }
71+ c.display.SetPixel(int16(bit), int16(16+3*i), pc)
72+ }
73+ }
74+ c.display.FillRectangle(x, 0, 5, 5, White)
75+ x = (x + 1) % 120
76+ c.display.Display()
77+
78+ //c.tonegen.NoteOn(byte(x))
79+ time.Sleep(200 * time.Millisecond)
80+ //c.tonegen.NoteOff(byte(x))
81+}
82+
83 func main() {
84 // Blink the LED during startup.
85 blinkCtx, blinkCancel := context.WithCancel(context.Background())
86 go led.Flash(blinkCtx, led.Builtin, time.Second / 4, time.Second / 4)
87
88- c, err := NewConcertina()
89+ c, err := NewConcertina(machine.I2C0)
90 if err != nil {
91 // let the little light blink forever and ever
92 for {
93 time.Sleep(1 * time.Hour)
94 }
95 }
96- blinkCancel()
97-
98- var x int
99- for {
100- c.display.ClearBuffer()
101- c.display.FillRectangle(int16(x), 28 - int16(x % 28), 4, 4, White)
102- c.display.Display()
103- x = (x + 1) % 120
104
105- c.tonegen.NoteOn(byte(x))
106+ // Okay, you can stop blinking now
107+ blinkCancel()
108+ //led.Builtin.Disable()
109
110- time.Sleep(200 * time.Millisecond)
111+ led.Builtin.SetColor(color.RGBA{0, 0, 10, 0})
112
113- c.tonegen.NoteOff(byte(x))
114+ for {
115+ c.Loop()
116 }
117 }
+12,
-0
1@@ -0,0 +1,12 @@
2+package main
3+
4+import (
5+ "tinygo.org/x/tinyfont"
6+ "tinygo.org/x/tinyfont/gophers"
7+)
8+
9+func (c *Concertina) Print(s string) {
10+ c.display.ClearDisplay()
11+ tinyfont.WriteLine(c.display, &gophers.Regular14pt, 18, 90, s, Black)
12+ c.display.Display()
13+}
M
go.mod
+4,
-1
1@@ -2,6 +2,9 @@ module git.woozle.org/neale/concertina
2
3 go 1.24.4
4
5-require tinygo.org/x/drivers v0.34.0
6+require (
7+ tinygo.org/x/drivers v0.34.0
8+ tinygo.org/x/tinyfont v0.3.0
9+)
10
11 require github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
M
go.sum
+2,
-0
1@@ -2,3 +2,5 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU
2 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
3 tinygo.org/x/drivers v0.34.0 h1:lw8ePJeUSn9oICKBvQXHC9TIE+J00OfXfkGTrpXM9Iw=
4 tinygo.org/x/drivers v0.34.0/go.mod h1:ZdErNrApSABdVXjA1RejD67R8SNRI6RKVfYgQDZtKtk=
5+tinygo.org/x/tinyfont v0.3.0 h1:HIRLQoI3oc+2CMhPcfv+Ig88EcTImE/5npjqOnMD4lM=
6+tinygo.org/x/tinyfont v0.3.0/go.mod h1:+TV5q0KpwSGRWnN+ITijsIhrWYJkoUCp9MYELjKpAXk=
+100,
-0
1@@ -0,0 +1,100 @@
2+package layouts
3+
4+var baseMidiNote = map[byte]int8{
5+ 'C': 60,
6+ 'D': 62,
7+ 'E': 64,
8+ 'F': 65,
9+ 'G': 67,
10+ 'A': 69,
11+ 'B': 71,
12+ 'c': 72,
13+ 'd': 74,
14+ 'e': 76,
15+ 'f': 77,
16+ 'g': 79,
17+ 'a': 81,
18+ 'b': 83,
19+}
20+
21+func ABC2MIDI(note string) int8 {
22+ if note == "" {
23+ return -1
24+ }
25+
26+ val, ok := baseMidiNote[note[0]]
27+ if !ok {
28+ return -1
29+ }
30+
31+ for _, m := range note[1:] {
32+ switch m {
33+ case '♯':
34+ fallthrough
35+ case '^':
36+ val += 1
37+
38+ case '♭':
39+ fallthrough
40+ case '_':
41+ val -= 1
42+
43+ case '\'':
44+ val += 12
45+
46+ case ',':
47+ val -= 12
48+
49+ default:
50+ return -1
51+ }
52+ }
53+
54+ return val
55+}
56+
57+func MIDI2ABC(val int8) string {
58+ octave := make([]byte, 0, 8)
59+ for val < baseMidiNote['C'] {
60+ octave = append(octave, ',')
61+ val += 12
62+ }
63+ for val > baseMidiNote['b'] {
64+ octave = append(octave, '\'')
65+ val -= 12
66+ }
67+
68+ var base string
69+ switch val % 12 {
70+ case 0:
71+ base = "C"
72+ case 1:
73+ base = "C^"
74+ case 2:
75+ base = "D"
76+ case 3:
77+ base = "D^"
78+ case 4:
79+ base = "E"
80+ case 5:
81+ base = "F"
82+ case 6:
83+ base = "F^"
84+ case 7:
85+ base = "G"
86+ case 8:
87+ base = "G^"
88+ case 9:
89+ base = "A"
90+ case 10:
91+ base = "B_"
92+ case 11:
93+ base = "B"
94+ }
95+
96+ if val > baseMidiNote['B'] {
97+ base = string(base[0] + 0x20) + base[1:]
98+ }
99+
100+ return base + string(octave)
101+}
+40,
-0
1@@ -0,0 +1,40 @@
2+package layouts
3+
4+import "testing"
5+
6+func abcTest(t *testing.T, s string, n int8) {
7+ if r := ABC2MIDI(s); r != n {
8+ t.Errorf("ABC2MIDI(%#v): wanted %d, got %d", s, n, r)
9+ }
10+}
11+
12+func midiTest(t *testing.T, n int8, s string) {
13+ if v := MIDI2ABC(n); v != s {
14+ t.Errorf("MIDI2ABC(%d); wanted %#v, got %#v", n, s, v)
15+ }
16+}
17+
18+func bothTest(t *testing.T, s string, n int8) {
19+ abcTest(t, s, n)
20+ midiTest(t, n, s)
21+}
22+
23+func TestABC(t *testing.T) {
24+ abcTest(t, "C", 60)
25+ abcTest(t, "D", 62)
26+ abcTest(t, "E", 64)
27+ abcTest(t, "F", 65)
28+ abcTest(t, "G", 67)
29+ abcTest(t, "A", 69)
30+ abcTest(t, "B", 71)
31+ abcTest(t, "c", 72)
32+ abcTest(t, "d", 74)
33+ abcTest(t, "e", 76)
34+ abcTest(t, "f", 77)
35+ abcTest(t, "g", 79)
36+ abcTest(t, "a", 81)
37+ abcTest(t, "b", 83)
38+ abcTest(t, "c^", 73)
39+ abcTest(t, "b_'", 94)
40+ abcTest(t, "G^,,", 44)
41+}
+99,
-0
1@@ -0,0 +1,99 @@
2+// Package layouts parses text layouts into []Layout.
3+//
4+// Because Go doesn't appear to have any built-in mechanism to output Go source code literals,
5+// it's either this or some intermediate format like JSON or gobs.
6+// This bespoke package is pretty small compared to the alternatives.
7+package layouts
8+
9+import (
10+ _ "embed"
11+ "strconv"
12+ "strings"
13+)
14+
15+//go:embed layouts.txt
16+var layouts_txt string
17+
18+const (
19+ Sides = 2
20+ Rows = 4
21+ Columns = 5
22+)
23+
24+type ParseError struct {
25+ line int
26+ desc string
27+}
28+
29+func (e *ParseError) Error() string {
30+ return "line " + strconv.Itoa(e.line) + ": " + e.desc
31+}
32+
33+type Layout struct {
34+ Name string
35+ Notes [Sides][Rows][Columns][2]int8
36+}
37+
38+func ParseLayouts(s string) ([]*Layout, error) {
39+ layouts := make([]*Layout, 0)
40+ var current *Layout
41+
42+ row := 0
43+ for lineno, line := range strings.Split(s, "\n") {
44+ line = strings.TrimSpace(line)
45+
46+ // Skip blank lines and comments
47+ if (line == "") || strings.HasPrefix(line, "#") {
48+ continue
49+ }
50+
51+ if row >= Rows {
52+ return nil, &ParseError{lineno, "too many rows"}
53+ }
54+
55+ // Split the line up into fields
56+ fields := strings.Fields(line)
57+
58+ // New layouts are anything other than 11-field lines with | in the middle
59+ if (len(fields) != 11) || (fields[5] != "|") {
60+ current = &Layout{
61+ Name: line,
62+ }
63+ layouts = append(layouts, current)
64+ row = 0
65+ continue
66+ }
67+
68+ // We can skip the middle | now
69+ fields = append(fields[:5], fields[6:]...)
70+ for col, button := range fields {
71+ side := col / Columns
72+ col = col % Columns
73+ notes := strings.Split(button, "/")
74+
75+ // If only one note is listed, push and draw are the same
76+ if len(notes) == 1 {
77+ notes = append(notes, notes[0])
78+ }
79+
80+ if len(notes) != 2 {
81+ return nil, &ParseError{lineno, "invalid button entry"}
82+ }
83+
84+ for i, note := range notes {
85+ midi := ABC2MIDI(note)
86+ if (midi == -1) && (note != "-") {
87+ return nil, &ParseError{lineno, "unrecognized note: " + note}
88+ }
89+ current.Notes[side][row][col][i] = midi
90+ }
91+ }
92+ row += 1
93+ }
94+
95+ return layouts, nil
96+}
97+
98+func DefaultLayouts() ([]*Layout, error) {
99+ return ParseLayouts(layouts_txt)
100+}
R src/layouts.txt =>
pkg/layouts/layouts.txt
+0,
-0
+42,
-0
1@@ -0,0 +1,42 @@
2+package layouts
3+
4+import (
5+ "encoding/json"
6+ "testing"
7+)
8+
9+func TestParser(t *testing.T) {
10+ txt := `Rochelle C/G
11+ E,/F, A,/B♭, C♯/D♯ A/G G♯/B♭ | c♯/d♯ a/c♯ g♯/g c♯'/b♭ a'/d'
12+ C,/G, G,/B, C/D E/F G/A | c/B e/d g/f c'/a e'/b
13+ B,/A, D/F♯ G/A B/c d/e | g/f♯ b/a d'/c' g'/e' b'/f♯'
14+ - - - - D,/D, | - - - - -
15+`
16+
17+ layouts, err := ParseLayouts(txt)
18+ if err != nil {
19+ t.Fatal(err)
20+ }
21+
22+ if l := len(layouts); l != 1 {
23+ t.Fatalf("wrong length: %d", l)
24+ }
25+
26+ layout := layouts[0]
27+ if layout.Name != "Rochelle C/G" {
28+ t.Error("wrong name")
29+ }
30+
31+ if b, err := json.Marshal(layout); err != nil {
32+ t.Error(err)
33+ } else {
34+ t.Log(string(b))
35+ }
36+
37+ if v := layout.Notes[0][0][0][0]; v != ABC2MIDI("E,") {
38+ t.Errorf("wrong note: %d", v)
39+ }
40+ if layout.Notes[0][0][0][1] != ABC2MIDI("F,") {
41+ t.Error("wrong note")
42+ }
43+}
+27,
-0
1@@ -0,0 +1,27 @@
2+package pcf8575
3+
4+import (
5+ "encoding/binary"
6+ "tinygo.org/x/drivers"
7+)
8+
9+type Device struct {
10+ Addr uint16
11+ I2C drivers.I2C
12+}
13+
14+// Read value of all pins
15+func (c *Device) Get() uint16 {
16+ rxbuf := make([]byte, 2)
17+ // If len(w) == 0, the low bit gets set
18+ // see /usr/local/lib/tinygo/src/machine/machine_atsamd21.go
19+ c.I2C.Tx(c.Addr, nil, rxbuf)
20+ return binary.LittleEndian.Uint16(rxbuf)
21+}
22+
23+// Put val onto all pins
24+func (c *Device) Put(val uint16) {
25+ txbuf := binary.LittleEndian.AppendUint16(nil, val)
26+ rxbuf := []byte{}
27+ c.I2C.Tx(c.Addr, txbuf, rxbuf)
28+}
+0,
-30
1@@ -1,30 +0,0 @@
2-#include <stdbool.h>
3-#include <stdint.h>
4-#include <avr/io.h>
5-#include <avr/interrupt.h>
6-
7-// CPU frequency in Hertz
8-#define F_CPU (1 * 1000000UL)
9-
10-void init(void) {
11- DDRB = 0xff; // All port B pins are outputs
12-}
13-
14-void loop(void) {
15- static uint32_t count = 0;
16-
17- if (count++ == 20000) {
18- PORTB ^= 0x01; // Toggle pin 0
19- count = 0;
20- }
21-}
22-
23-int main(void) {
24- init();
25-
26- for (;;) {
27- loop();
28- }
29-
30- return 0;
31-}
+0,
-14
1@@ -1,14 +0,0 @@
2-// 'ruby', 30x32px
3-const unsigned char ruby_xbm[] = {
4- 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x80, 0x3f,
5- 0x00, 0x00, 0xc0, 0x3f, 0x00, 0x00, 0xe0, 0x3f, 0xc0, 0x01, 0xf0, 0x3f,
6- 0xe0, 0x03, 0xf8, 0x3f, 0xff, 0x07, 0xf8, 0x3f, 0xff, 0x07, 0xf8, 0x1f,
7- 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0x0f,
8- 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, 0xfe, 0xff, 0xff, 0x07,
9- 0xfc, 0xff, 0xff, 0x03, 0xf8, 0xff, 0xff, 0x01, 0xf0, 0xff, 0xff, 0x00,
10- 0xf0, 0xff, 0xff, 0x00, 0xf0, 0xff, 0xff, 0x00, 0xf0, 0xff, 0xff, 0x00,
11- 0xf0, 0xff, 0xff, 0x00, 0xe0, 0xff, 0xff, 0x00, 0xe0, 0xff, 0xff, 0x00,
12- 0xc0, 0xff, 0x7f, 0x00, 0x80, 0xff, 0x3f, 0x00, 0x00, 0xff, 0x1f, 0x00,
13- 0x00, 0xfe, 0x1f, 0x00, 0x00, 0xfe, 0x0f, 0x00, 0x00, 0xfc, 0x0f, 0x00,
14- 0x00, 0xf8, 0x07, 0x00, 0x00, 0xf0, 0x03, 0x00
15-};