summaryrefslogtreecommitdiff
path: root/_posts/en/2025-07-26-picoshock_devlog_3.md
blob: 6f1312bc2ae6af2f4b0eca43d4fa7e5172775858 (plain)
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
---
layout: post
title: "PicoShock Devlog #3"
lang: en
date: 2025-07-26
tag: ["Programming", "PicoShock"]
---

Well, it's been a while. Since [last time](https://silosneeded.com/2025/04/picoshock_devlog_2) I had success reading
the data the Playstation 2 was sending me. After that, I took the logical next step: sending data back. That was my first
block.

After a couple of days of banging my head against the wall I took a rest that prolonged itself more than it should have.
Just today I got back at it. And I think I've made respectable progress, but with a twist. Because nothing can
be as simple as I'd like things to be.

# First off: debugging

So the problem was knowing what in God's name I was actually putting on the line. That's when I got into the rabbit hole of logic analyzers.
I came across a couple of pretty interesting projects: [sigrok](https://sigrok.org), its GUI, [pulseview](http://sigrok.org/wiki/PulseView) and
a driver for using a raspberry pi pico as the analyzer: [sigrok-pico](http://sigrok.org/wiki/PulseView).

Installing these tools is outside of the scope of this article, but the only thing worth mentioning here is that compiling sigrok and pulseview
oneself is necessary if you pretend to use a raspberry pi pico as your logic analyzer. I remember struggling with that but in the end it was
possible.

![Pulseview screenshot](/assets/images/2025/pulseview.jpg)

# Taking a peek at the output line

With sigrok I could actually see the output I was sending. And it was nonsense, for the most part. But something functional reared its head from time
to time. Let's remember the table of bytes I was receiving, and the ones I'd like to send in response:

|Byte Number|Received|Sent|
|-----------|--------|----|
|1          |0x01    |0xff|
|2          |0x42    |0x41|
|3          |0x00    |0x5a|
|4          |0x00    |0xff|
|5          |0x00    |0x40|

Bytes 4 and 5 are entirely mine, so to speak: the playstation 2 drives its line low and expects two bytes from my end. In these bytes is where the magic happens:
mode setting, button presses... In this example I want to send a button press, the cross button if I remember correctly. This button corresponds to the seventh
bit of the second byte: in binary, 0100 0000. In hex, 0x40. If there's nothing pressed then I must send all 1s (0xff).

What was strange about the data I was sending is, well, first, that it was wrong, but not every time. Sometimes, a correct byte was sent, but as the third or 
fourth one for example, instead of its intended order. This also meant that the preceding and following bytes were gibberish, well, not TOTAL gibberish, they 
sometimes differed from the actual byte I wanted to send for just a bit. For example, I wanted to send 0x5a (0101 1010 in binary) but the byte that was being sent 
was 0xda (1101 1010). Pretty mystical isn't it?

# First steps towards actually fixing this: the C portion

After some time suffering with this and posting a help thread on the forums I decided to revisit my bible on this project, yes, the 
[curious inventor's guide](https://store.curiousinventor.com/guides/PS2).

The paragraph that caught my eye was this one:

> The play station sends a byte at the same time as it receives one (full duplex) via serial communication.

I already knew this, but I'm such a newbie on this that I actually pondered on this. This means that the TX FIFO must have the byte I want to send BEFORE
I consume the RX FIFO byte, i.e. I must send my 0xff byte before I consume the 0x01 the console sends me. The same for every byte. I do have to pattern match
for the second byte, as that is the command (poll, configure...) but MY second byte is independent from the one from the console. The next bytes will be the ones
that will act as the response to the console's command.

So I decided to change my main loop, from pattern matching the bytes I was receiving to jamming the output FIFO with my data. When I start writing the library
proper I will have to add logic checking for the data I'm receiving, but for now I want to emulate the base case exposed in the guide.

```c
while (true) {
	// The TX FIFO will have data ready to simultaneously send while
	// the RX FIFO is being emptied.
    pio_sm_put_blocking(data_pio, data_sm, (0xff ^ 0xff));
    data = pio_sm_get_blocking(pio, sm) >> 24;
    pio_sm_put_blocking(data_pio, data_sm, (0xff ^ 0x41));
    data = pio_sm_get_blocking(pio, sm) >> 24;

    // Receiving all zeros. Send button presses.
    pio_sm_put_blocking(data_pio, data_sm, (0xff ^ 0x5a));
    uint32_t buf = pio_sm_get_blocking(pio, sm) >> 24;
      
    if (buf != 0x00)
      continue;

    pio_sm_put_blocking(data_pio, data_sm, (0xff ^ 0xff));
    buf = pio_sm_get_blocking(pio, sm) >> 24;
    
    if (buf != 0x00)
      continue;

    pio_sm_put_blocking(data_pio, data_sm, (0xff ^ 0x40));
 
    printf("%x\n", data);
}
```

# A small change in the PIO program

In my previous version of my PIO program, after sending my byte, the program looped to the `set pindirs 0` instruction:

```asm
.program dualshock_data
	
	.wrap_target
	set pindirs 0
wait_for_output:
	jmp !osre wait_for_att  ;; Stall while we don't have data to output
	jmp wait_for_output
	
wait_for_att:
	set x, 7                ;; Set bit counter: we want to send a byte.
	wait 0 pin 0            ;; Wait for ATT.

out_loop:
	wait 0 pin 1            ;; When clock goes from low to high,
	wait 1 pin 1            ;; send our data.
	out pindirs, 1          ;; Send a bit to the PS2.
	jmp x-- out_loop        ;; Decrement bit counter.
.wrap
```

I told myself, well, isn't it more appropriate to jump to the label that waits for more data to be present in the TX FIFO than to let the program wrap?
I wasn't thinking about the importance of the `set pindirs 0` (for those not in the loop, this instruction drives the data pin high, as it is an open collector
output... now that I'm reading my program's config... does having the data pin as a SET pin makes sense? Will check in the future) so I decided to jump to
`wait_for_output` instead after every byte:

```asm
out_loop:
	wait 0 pin 1            ;; When clock goes from low to high,
	wait 1 pin 1            ;; send our data.
	out pindirs, 1          ;; Send a bit to the PS2.
	jmp x-- out_loop        ;; Decrement bit counter.
	jmp wait_for_output     ;; We sent the byte; let's check if there's another one to send.
```

What came after was a pretty big surprise

# Now (it seems) I'm sending what I actually want to send

After these changes, it seems I've made some progress. I say that it seems because I have no way of actually verifying it as for now I'm not capable of seeing what's
going on the Playstation 2, I gave the monitor I was using to test this to a friend and started using another one I had on a corner. But it doesn't like 
to receive a 1080p resolution image. The other one wasn't compatible either but at least showed something, so I'm pretty much blind: I can only peek at my logic 
analyzer.

And this is what the logic analyzer shows me:

![Success! Maybe](/assets/images/2025/successful_output.jpg)

If we see the data line, and count the bits (on the rising edge of the clock, that's the moment when the Playstation 2 reads our bit), those are the actual bytes
I want to send. But... there's a caveat. The data line is an open collector output, this means that it's normally held high as it is the console that actually puts
the voltage on the line, not us. I'm not entirely sure of the consequences of the line being by default low, as 0 is the last bit of our last byte. And I can't 
see the playstation's screen so I'm not sure if it's actually listening to me and registering the button press I want it to register.

So, for the next time around...

# What comes next

The reason for the data pin to be a set pin (and also an output pin) is that that was the recommendation I was given on the forums. When it is a set pin, I can
drive it high or low at convenience.

So, what I must do is look for a compatible monitor or small CRT TV to do my testing and know if, once and for all, I'm doing something correctly for a change, 
and the console is registering my button press. If it's not doing that, maybe the fact that the line is pulled low and stays low after every packet has
something to do with it.

Well, see you next time!