-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathchapter-next-ula.tex
288 lines (220 loc) · 14.8 KB
/
chapter-next-ula.tex
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
\section{ULA Layer}
\label{zx_next_ula}
% ──────────────────────────────────────────────
% ─██████──██████─██████─────────██████████████─
% ─██░░██──██░░██─██░░██─────────██░░░░░░░░░░██─
% ─██░░██──██░░██─██░░██─────────██░░██████░░██─
% ─██░░██──██░░██─██░░██─────────██░░██──██░░██─
% ─██░░██──██░░██─██░░██─────────██░░██████░░██─
% ─██░░██──██░░██─██░░██─────────██░░░░░░░░░░██─
% ─██░░██──██░░██─██░░██─────────██░░██████░░██─
% ─██░░██──██░░██─██░░██─────────██░░██──██░░██─
% ─██░░██████░░██─██░░██████████─██░░██──██░░██─
% ─██░░░░░░░░░░██─██░░░░░░░░░░██─██░░██──██░░██─
% ─██████████████─██████████████─██████──██████─
% ──────────────────────────────────────────────
Original ZX Spectrum didn't have a dedicated graphics chip. To keep the price as low as possible, screen rendering was performed by ULA (``Uncommitted Logic Array'') chip.
ZX Spectrum Next inherits ULA mode. The resolution of the screen in this mode is 256$\times$192 pixels. If we translate this to 8$\times$8 pixels characters, it gives us 32 character columns in 24 character rows.
ULA always reads from 16K bank 5 which is assigned to the second 16K slot at addresses \MemAddr{4000}-\MemAddr{7FFF} by default. Similar to the memory configuration of other contemporary computers, pixel memory is separate from attributes/colour memory. If using default memory configuration:
\begin{ElegantTable}{|c|c|c|c|}[
\setlength{\tabcolsep}{5pt}
]
\ElegantHeader{ROM & \multicolumn{3}{c|}{RAM}}
16K & 16K & 16K & 16K \\
&
\begin{tabular}{c|c|c}
\arrayrulecolor{gray}
\hline
Pixels & Attributes & (free) \\
\MemAddr{4000}-\MemAddr{57FF} &
\MemAddr{5800}-\MemAddr{5AFF} &
\MemAddr{5B00}-\MemAddr{7FFF} \\
\end{tabular}
& & \\
\end{ElegantTable}
\subsection{Pixel Memory}
Each screen pixel is represented by a single bit, meaning 1 byte holds 8 screen pixels. So, for each line of 256 pixels, 32 bytes are needed. However, for sake of efficiency, the original Spectrum optimized screen memory layout for speed but made it inconvenient for programming.
Pixel memory is not linear but is instead divided to fill character rows line by line. The first 32 bytes of memory represent the first line of the first character row, followed by 32 bytes representing the first line of the second character row and so on until the first line of 8 character rows is filled. Then next 32 bytes of screen memory represent the second line of the first character row, again followed by the second line of the second character row, until all 8 character rows are covered:
{
\newcommand{\PixelTitle}{Addr. & Ln. & Ch.}
\newcommand{\PixelData}[4]{{\tt \$#1} & {\tt #2} & {\tt #3}/{\tt #4}}
\begin{tabularx}{0.9\linewidth}{lccXlccXlcccX}
\addtolength{\tabcolsep}{-2pt}
\PixelTitle & & \PixelTitle & & \PixelTitle & & \\
\PixelData{4000}{0}{0}{0} & & \PixelData{4100}{1}{0}{1} & & \PixelData{4200}{2}{0}{2} & & \multirow{8}{*}{\ddd} \\
\PixelData{4020}{8}{1}{0} & & \PixelData{4120}{9}{1}{1} & & \PixelData{4220}{10}{1}{2} & & \\
\PixelData{4040}{16}{2}{0} & & \PixelData{4140}{17}{2}{1} & & \PixelData{4240}{18}{2}{2} & & \\
\PixelData{4060}{24}{3}{0} & & \PixelData{4160}{25}{3}{1} & & \PixelData{4260}{26}{3}{2} & & \\
\PixelData{4080}{32}{4}{0} & & \PixelData{4180}{32}{4}{1} & & \PixelData{4280}{33}{4}{2} & & \\
\PixelData{40A0}{40}{5}{0} & & \PixelData{41A0}{41}{5}{1} & & \PixelData{42A0}{42}{5}{2} & & \\
\PixelData{40C0}{48}{6}{0} & & \PixelData{41C0}{49}{6}{1} & & \PixelData{42C0}{50}{6}{2} & & \\
\PixelData{40E0}{56}{7}{0} & & \PixelData{41E0}{57}{7}{1} & & \PixelData{42E0}{58}{7}{2} & & \\[1ex]
\multicolumn{4}{l}{\textbf{Ln.} Screen line (0-191)} & \multicolumn{9}{l}{\textbf{Ch.} Character {\tt <row>}/{\tt <line>} (0-23/0-7)} \\
\end{tabularx}
}
But this is not the end of the peculiarities of Spectrum ULA mode. If you attempt to fill the screen memory byte by byte, you'll realize the top third of the screen fills in first, then middle third and lastly bottom third. The reason is, ULA mode divides the screen into 3 banks. Each bank covers 8 character rows, so 8$\times$8$\times$32 or 2048 bytes:
\begin{tabular}{ccc}
Memory Range & Screen Lines & Char. Rows \\
\MemAddr{4000} - \MemAddr{47FF} &
{\tt ~~0} - {\tt 63~} &
{\tt ~0} - {\tt 8~} \\
\MemAddr{4800} - \MemAddr{4FFF} &
{\tt ~64} - {\tt 127} &
{\tt ~9} - {\tt 16} \\
\MemAddr{5000} - \MemAddr{57FF} &
{\tt 128} - {\tt 191} &
{\tt 17} - {\tt 23} \\
\end{tabular}
In fact, to calculate the address of memory for any given (x,y) coordinate, we'd need to prepare a 16-bit value like this:
\begin{BitTableWord}
\BitMono{0} &
\BitMono{1} &
\BitMono{0} &
\BitSmall{$Y_7$} &
\BitSmall{$Y_6$} &
\BitSmall{$Y_2$} &
\BitSmall{$Y_1$} &
\BitSmall{$Y_0$} &
\BitSmall{$Y_5$} &
\BitSmall{$Y_4$} &
\BitSmall{$Y_3$} &
\BitSmall{$X_7$} &
\BitSmall{$X_6$} &
\BitSmall{$X_5$} &
\BitSmall{$X_4$} &
\BitSmall{$X_3$} \\
\hline
\BitMono{0} &
\BitMono{1} &
\BitMono{0} &
\BitMulti{8}{$Y$} &
\BitMulti{5}{$X$} \\
\end{BitTableWord}
As you can see, X is straightforward; we simply need to take the upper 5 bits and fill them into the lower 5 bits of a 16-bit register pair. Y coordinate requires all 8 bits written into bits 12-5 of 16-bit register pair. However, notice how individual bits are scrambled. It makes incrementing address for next character row simple operation of {\tt INC H} (assuming {\tt HL} stores the address of the previous row), which is likely one of the reasons for such implementation. But imagine for a second how complex a Z80 program would need to be to handle all of this. Sure, nothing couple shifts and masking operations couldn't handle but still, lots of wasted CPU cycles. However, on ZX Spectrum Next we have 3 new instructions that take care of all of the complexity for us:
\begin{itemize}[topsep=1pt,itemsep=1pt]
\item {\tt PIXELAD} calculates the address of a pixel with coordinates from {\tt DE} register pair where {\tt D} is Y and {\tt E} is X coordinate and stores the memory location address into {\tt HL} register pair for ready consumption
\item {\tt PIXELDN} takes the address of a pixel in {\tt HL} and updates it to point to the same X coordinate but one screen line down
\item {\tt SETAE} takes X coordinate from {\tt E} register and prepares mask in register {\tt A} for reading or writing to ULA screen
\end{itemize}
Furthermore; each instruction only uses 8 t-states, which is far less than the corresponding Z80 assembly program would require. Somewhat naive program for drawing vertical line write from the pixel at coordinate (16,32) to (16,50):
\begin{tcblisting}{}
LD DE, &1020 ; Y=16, X=32
PIXELAD ; HL=address of pixel (E,D)
loop:
SETAE ; A=pixel mask
OR (HL) ; we'll write the pixel
LD (HL), A ; actually write the pixel
INC D ; Y=Y+1
LD A, D ; copy new Y coordinate to A
CP 51 ; are we at 51 already?
RET NC ; yes, return
PIXELDN ; no, update HL to next line
JR loop ; continue with next pixel
\end{tcblisting}
Note: because we're updating our Y coordinate in {\tt D} register within the loop, we could also use {\tt PIXELAD} instead of {\tt PIXELDN} in line 13. Both instructions require 8 T states for execution, so there's no difference performance-wise.
If we instead wanted to check if the pixel at the given coordinate is set or not, we would use {\tt AND (HL)} instead of {\tt OR (HL)}. For example:
\begin{tcblisting}{}
LD DE, &1020 ; Y=16, X=32
PIXELAD ; HL=address of pixel (E,D)
SETAE ; A=pixel mask
AND (HL) ; we'll read the pixel
RET Z ; exit if pixel is not set
\end{tcblisting}
\subsection{Attributes Memory}
Now that we know how to draw individual pixels, it's time to handle colour. Memory wise, it's stored immediately after pixel RAM, at memory locations \MemAddr{5800} - \MemAddr{5AFF}. Each byte represents colour and attributes for 8$\times$8 pixel block on the screen. Byte contents are as follows:
\begin{BitTableByte}
\BitSmall{$F$} &
\BitSmall{$B$} &
\BitSmall{$P_2$} &
\BitSmall{$P_1$} &
\BitSmall{$P_0$} &
\BitSmall{$I_2$} &
\BitSmall{$I_1$} &
\BitSmall{$I_0$} \\
\hline
\BitSmall{$F$} &
\BitSmall{$B$} &
\BitMulti{3}{Paper} &
\BitMulti{3}{Ink} \\
\end{BitTableByte}
\begin{itemize}[topsep=1pt,itemsep=1pt]
\item Bit 7: {\tt 1} to enable flashing, {\tt 0} to disables it
\item Bit 6: {\tt 1} to enable bright colours, {\tt 0} for normal colours
\item Bits 5-3: paper colour {\tt 0-7}
\item Bits 2-0: ink colour {\tt 0-7}
\end{itemize}
Colour value {\tt 0-7} corresponds to:
\begin{tabular}{ccll}
\BitHead{Value} & \BitHead{Binary} & \BitHead{Colour} & \BitHead{Bright} \\
\BitMono{0} & \BitMono{000} & Black & Black \\
\BitMono{1} & \BitMono{001} & Blue & Bright blue \\
\BitMono{2} & \BitMono{010} & Red & Bright red \\
\BitMono{3} & \BitMono{011} & Magenta & Bright magenta \\
\BitMono{4} & \BitMono{100} & Green & Bright green \\
\BitMono{5} & \BitMono{101} & Cyan & Bright cyan \\
\BitMono{6} & \BitMono{110} & Yellow & Bright yellow \\
\BitMono{7} & \BitMono{111} & Gray & White \\
\end{tabular}
Spectrum only requires 768 bytes to configure colour and attributes for the whole screen. And memory is contiguous so it's simple to manage. However, it comes at expense of restricting to only 2 colours per character block - the reason for the (in)famous colour clash.
Note: on Next, default ULA colours can be changed, see Palette chapter \XRef{zx_next_palette} for details.
\subsection{Border}
% note: use of "previous page" in parenthesis below - this works as long as colours table will be physically on previous page in printed book, but make sure it's adjusted if additional content it added, or layout changes otherwise in the future ("in previous section" or "above" for example, or, if several pages are inserted in between, use \XRef)
Next inherits Spectrum border colour handling through \PortTextXRef{xxFEWrite}. The bottom 3 bits are used to specify one of 8 possible colours (see table on the previous page for full list). Example:
\begin{tcblisting}{}
LD A, 1 ; Select blue colour
OUT (&FE), A ; Set border colour from A
\end{tcblisting}
Note: border colour is set the same way regardless of graphics mode used. However, some Layer 2 modes and Tileset may partially or fully cover the border, effectively making it invisible to the user.
\subsection{Shadow Screen}
As mentioned, ULA uses 16K bank 5 by default to determine what to show on the screen. However, it's possible to change this to bank 7 instead by using bit 3 of \PortTextXRef{7FFD}. Bank 7 mode is called the ``shadow'' screen. It gives us two separate memory spaces for rendering ULA data and means for quickly swapping between them. It allows always drawing into inactive bank and only swapping it in when ready thus help eliminating flicker.
Note: \PortTextXRef{7FFD} only controls which of the two possible banks is being used by ULA, but it doesn't map the bank into any of the memory slots. This needs to be done by one of the paging modes as described in the Memory Map and Paging chapter, section \XRef{zx_next_memorypaging}. Using MMU, we could do something like:
\begin{tcblisting}{}
LD HL, &5800 ; we'll be swapping colours
NEXTREG &52, 10 ; swap first half of 16K bank 5 to 8K slot 2
LD A, %00000000 ; paper=black, ink=black
LD (HL), A ; write data to screen (immediately visible)
NEXTREG &52, 14 ; swap first half of 16K bank 7 to 8K slot 2
LD A, %00000101 ; paper=black, ink=cyan
LD (HL), A ; write to 16K bank 7 (not visible)
LD BC, &7FFD ; prepare port for changing layers
LD A, %00001000 ; activate shadow layer
OUT (C), A ; top left char now has black background
LD A, %00000000 ; deactivate shadow layer
OUT (C), A ; top left char now has cyan background
\end{tcblisting}
Remember: 16K bank 7 corresponds to 8K banks 14 and 15. And because pixel and attributes combined fit within single 8K, only single bank needs to be swapped in.
\subsection{Enhanced ULA Modes}
ZX Spectrum Next also supports several enhanced ULA modes like Timex Sinclair Double Buffering, Timex Sinclair Hi-Res and Hi-Colour, etc. However, with the presence of Layer 2 and Tilemap modes, it's unlikely these will be used when programming new software on Next. Therefore they are not described here. If interested, read more on:
\url{https://wiki.specnext.dev/Video_Modes}
\subsection{ULA Registers}
\label{zx_next_ula_registers}
\subsubsection{\PortDeclaration{xxFEWrite}}
\begin{NextPort}
\PortBits{7-5}
\PortDesc{Reserved, use {\tt 0}}
\PortBits{4}
\PortDesc{EAR output (connected to internal speaker)}
\PortBits{3}
\PortDesc{MIC output (saving to tape via audio jack)}
\PortBits{2-0}
\PortDesc{Border colour}
\end{NextPort}
% note: we don't show page next to port since we use more verbose link to the section+page afterwards
Note: when read with certain high byte values, \PortTextXRef[]{xxFE} will read keyboard status. See Keyboard, section \PortXRef{xxFE} for details.
\subsubsection{\PortTextXRef[]{7FFD}}
\PortDeclarationXRef{Memory Map and Paging}{7FFD}
\subsubsection{\PortTextXRef[]{40}}
\vspace*{-2ex}
\subsubsection{\PortTextXRef[]{41}}
\vspace*{-2ex}
\subsubsection{\PortTextXRef[]{42}}
\vspace*{-2ex}
\subsubsection{\PortTextXRef[]{43}}
\vspace*{-2ex}
\subsubsection{\PortTextXRef[]{44}}
\vspace*{-2ex}
\subsubsection{\PortTextXRef[]{4A}}
\PortDeclarationXRefMultiple{Palette}{40}{4A}
\pagebreak
\IntentionallyEmpty
\pagebreak