Thursday, 10 September 2026

GameBoy Pixmap Demo (Part 3)

 This is the third of a series of blog posts on my Nintendo GameBoy demo. The first summarised the machine; covered part of the tiled graphics system and introduced my concept for implementing full frame buffers on the unit. The second discussed the Game Boy's Sharp SM83 CPU as it's quite intriguing, and in some ways quite different to its Z80 and Intel 8080 relatives and very different to the 6502 used in the Nintendo NES, Atari VCS 2600 and many 1970s/early 1980s 8-bit computers.

This one explores the frame buffer proof-of-concept and defines the font I'll be using for text output.

Recap

The GameBoy uses tiled graphics, where the 160x144 pixels screen is divided into a 20x18 2-D tile map of bytes that reference one of 256, 8x8 pixel tiles. This would appear to prevent a full pixel mapped image to be displayed, because there are 360 tile map locations and only 256 tiles they can reference.

However, it's possible to switch the set of tiles used by the Game Boy to provide access to another 128 tiles (0..127) with the higher numbered tiles (128..255) being the same as in the first set.  In addition, the set of tiles can be switched on the fly by generating an interrupt just at the end of the first set. This means we can use a total of 384 tiles, more than enough to uniquely cover the screen.


Goals

My goals are fairly simple. I want to be able to recreate the kinds of text and graphics primitives available on early 8-bit home computers. These are:
  • Being able to display a character at any character location. Because I want to support at least a 40x18 display, all my characters will be on a 4x8 matrix.
  • Being able to clear the screen, by clearing the frame buffer (you'll see this isn't as trivial as I thought).
  • Being able to scroll the screen by a single line and clear the bottom line.
  • Being able to plot points and draw lines anywhere on the screen (this was surprisingly easy).
I also wanted to be able to perform all these operations using the full set of 4 colours (greyscales) for both foreground and background colours. With my Sinclair background I refer to these as Ink (foreground) and Paper (background).

Development Process

Generating the Full Frame Buffer

This was fairly easy. I started with the default Hello demo you can find on the https://gbdev.io/rgbds-live/ web page. That demo simply sets up a static image on the background tile map using 69 tiles.


The code is remarkably simple, it just turns off the audio; waits for Blank; turns the LCD off; copies the tile data (first), then the data for the Tile Map; then turns the LCD on and initialises the main display register in the first (blank) frame. The whole code (but not data) fits within a single editor window.

To support a simple frame buffer; all the tiles needed to be in order and I needed a demo image. The easiest way to do this when bringing up a new system IMHO, is to fill the frame buffer with successive values: 0..255. Then you can see them all as binary patterns. I made the first set of tiles black binary patterns and the second set of 128 tiles light grey patterns. The only other change I made was to use the first set of tiles rather than the second set (in memory order), so my code to turn on the LCD looked like:

; Turn the LCD on, use 1st tile block.
ld a, LCDC_ON | LCDC_BG_ON | LCDC_BLOCKS
ldh [rLCDC], a

So, initially I could see that it was using the same set of tiles twice, because they were all dark. Then I'd know the interrupt worked because the remaining 360-256=104 tiles would look light. As a general principle that works for both this kind of game development and embedded programming, incremental changes are far more preferable, because errors are caught quickly and early.

SECTION "StatVec", ROM0[INT_HANDLER_STAT]
  jp LcdStat ;This needs to go near the start of the ROM

;.. then after the ds $150 - @, 0 header line..

LcdStat: ;switch between 96 and 144 and switch tile set.
  push af
  ldh a,[rLCDC] ;get the old LCD tile mode.
  xor LCDC_BLOCKS ;swap the tile set.
  ldh [rLCDC],a
  ldh a,[rLYC];get the old scan line number.
  xor ((256/20)*8)^144 ;(See explanation below)
  ldh [rLYC],a
  pop af
  reti

  ;And later to turn on interrupts..
  ld a,(256/20)*8 ;this calculates the right scan (scan 96)
  ldh [rLYC],a
  ld a,STAT_LYC
  ldh [rSTAT],a
  ld a,IE_STAT ;initially only use the STAT interrupt.
  ldh [rIE],a
  ei

Xors can be used to swap between any two values, not merely to turn a bit on and off, because of the identity: b^a^b=a. So, if a=numberX and we want to swap with numberY then xor numberX^numberY will generate numberX^numberX^numberY=numberY the first time around and numberY^numberX^numberY=numberX the second time around. This saves on an if.. else.. control flow. The GameBoy then generates the display:


You can see that where the light-green patterns begin they're the same patterns as in the top-left, but they aren't the same tiles, because the tiles contain the colours too.

Adding a 4x8 Font

This is actually a fairly small change to the firmware, but took quite a bit of work. I wanted to use a 4x8 font, so that I could display 40x18 characters on the screen instead of 20x18 (a 4x6 font could achieve 40x24, but they're harder to read and it'd involve crossing tile boundaries). The font looked like this:


(It's the Tasword 2 font, except I inverted the © sign). To save space, in the ROM I also just wanted to store them as 1bpp images and combine two characters per 8x8 pixels.

GameBoy tiles are 8x8, but 2 bits per pixel. The pixels aren't paired, instead there's 8 light-grey pixels followed by 8 dark-grey pixels (and setting the same bits on both bytes gives black).This means I would need to unpack my font bytes, but I don't need to expand 4-bits into 8-bits. So, for black on white text that's easy, I just repeat both bytes for every scan on the tile.

GenTiles10:
  ld a,[de] ;de^font.
  inc de
  ldi [hl],a ;hl^frame buffer (i.e. tiles)
  ldi [hl],a ;copy twice
  dec bc ;bc was set to the number of bytes in the font
  ld a,b
  or c
  jr nz,GenTiles10

I've ended up using font like this (or a 6x6 font) several times, and I always end up having to convert it to hex, so I thought I'd just publish it here:

CharSet:
  db 0x00, 0x02, 0x02, 0x02, 0x02, 0x00, 0x02, 0x00 ; !
  db 0x00, 0x50, 0x52, 0x07, 0x02, 0x07, 0x02, 0x00 ;"#
  db 0x00, 0x25, 0x71, 0x42, 0x72, 0x14, 0x75, 0x20 ;$%
  db 0x00, 0x21, 0x52, 0x20, 0x60, 0x50, 0x60, 0x00 ;&'
  db 0x00, 0x14, 0x22, 0x22, 0x22, 0x22, 0x14, 0x00 ;()
  db 0x00, 0x00, 0x52, 0x22, 0x77, 0x22, 0x52, 0x00 ;*+
  db 0x00, 0x00, 0x00, 0x00, 0x07, 0x20, 0x20, 0x40 ;,-
  db 0x00, 0x01, 0x01, 0x02, 0x02, 0x64, 0x64, 0x00 ;.
  db 0x00, 0x22, 0x56, 0x52, 0x52, 0x52, 0x27, 0x00 ;01
  db 0x00, 0x22, 0x55, 0x12, 0x21, 0x45, 0x72, 0x00 ;23
  db 0x00, 0x57, 0x54, 0x76, 0x11, 0x15, 0x12, 0x00 ;45
  db 0x00, 0x37, 0x41, 0x61, 0x52, 0x54, 0x24, 0x00 ;67
  db 0x00, 0x22, 0x55, 0x25, 0x53, 0x55, 0x22, 0x00 ;89
  db 0x00, 0x00, 0x02, 0x20, 0x02, 0x22, 0x04, 0x00 ;:;
  db 0x00, 0x00, 0x10, 0x27, 0x40, 0x27, 0x10, 0x00 ;<=
  db 0x00, 0x02, 0x45, 0x21, 0x12, 0x20, 0x42, 0x00 ;>?
  
  db 0x00, 0x62, 0x95, 0xb7, 0xb5, 0x85, 0x65, 0x00 ;@A
  db 0x00, 0x62, 0x55, 0x64, 0x54, 0x55, 0x62, 0x00 ;BC
  db 0x00, 0x67, 0x54, 0x56, 0x54, 0x54, 0x67, 0x00 ;DE
  db 0x00, 0x72, 0x45, 0x74, 0x47, 0x45, 0x42, 0x00 ;FG
  db 0x00, 0x57, 0x52, 0x72, 0x52, 0x52, 0x57, 0x00 ;HI
  db 0x00, 0x35, 0x15, 0x16, 0x15, 0x55, 0x25, 0x00 ;JK
  db 0x00, 0x45, 0x47, 0x47, 0x45, 0x45, 0x75, 0x00 ;LM
  db 0x00, 0x52, 0x55, 0x75, 0x75, 0x55, 0x52, 0x00 ;NO
  db 0x00, 0x62, 0x55, 0x55, 0x67, 0x47, 0x43, 0x00 ;PQ
  db 0x00, 0x62, 0x55, 0x52, 0x61, 0x55, 0x52, 0x00 ;RS
  db 0x00, 0x75, 0x25, 0x25, 0x25, 0x25, 0x22, 0x00 ;TU
  db 0x00, 0x55, 0x55, 0x55, 0x57, 0x27, 0x25, 0x00 ;VW
  db 0x00, 0x55, 0x55, 0x25, 0x52, 0x52, 0x52, 0x00 ;XY
  db 0x00, 0x77, 0x14, 0x24, 0x24, 0x44, 0x77, 0x00 ;Z[
  db 0x00, 0x47, 0x41, 0x21, 0x21, 0x11, 0x17, 0x00 ;\]
  db 0x00, 0x20, 0x50, 0x00, 0x00, 0x00, 0x07, 0x00 ;^_
  
  db 0x00, 0x20, 0x56, 0x41, 0x63, 0x45, 0x73, 0x00 ;£a
  db 0x00, 0x40, 0x42, 0x65, 0x54, 0x55, 0x62, 0x00 ;bc
  db 0x00, 0x10, 0x12, 0x35, 0x56, 0x54, 0x23, 0x00 ;de
  db 0x00, 0x20, 0x52, 0x45, 0x65, 0x43, 0x45, 0x02 ;fg
  db 0x00, 0x42, 0x40, 0x66, 0x52, 0x52, 0x57, 0x00 ;hi
  db 0x00, 0x14, 0x04, 0x35, 0x16, 0x15, 0x55, 0x20 ;jk
  db 0x00, 0x40, 0x45, 0x47, 0x47, 0x55, 0x25, 0x00 ;lm
  db 0x00, 0x00, 0x62, 0x55, 0x55, 0x55, 0x52, 0x00 ;no
  db 0x00, 0x00, 0x63, 0x55, 0x55, 0x63, 0x41, 0x41 ;pq
  db 0x00, 0x00, 0x63, 0x54, 0x42, 0x41, 0x46, 0x00 ;rs
  db 0x00, 0x40, 0x75, 0x45, 0x45, 0x55, 0x22, 0x00 ;tu
  db 0x00, 0x00, 0x55, 0x55, 0x57, 0x27, 0x25, 0x00 ;vw
  db 0x00, 0x00, 0x55, 0x55, 0x23, 0x51, 0x55, 0x02 ;xy
  db 0x00, 0x00, 0x71, 0x12, 0x26, 0x42, 0x71, 0x00 ;z{
  db 0x00, 0x20, 0x24, 0x22, 0x23, 0x22, 0x24, 0x00 ;|}
  db 0x00, 0x06, 0xaf, 0x59, 0x0b, 0x09, 0x0f, 0x06 ;~©

As you can tell, every 8 bytes contains a pair of characters. It's just the printable 96 characters, so they take 384 bytes in total. The display then shows...


(The rest of the screen is blank).

Conclusion

Starting with a basic Tile-mapped display demo, I added a line match interrupt which was triggered on scans 96 and 144, inverting the tile set each time. I set the TileMap to point to successive tiles (mod 256). The real GameBoy Tile Map is 32x32, so in reality I had to skip 12 bytes in the TileMap after each 20 byte row. I hand-converted a 4x8 font I had lying around: two characters will fit in each half of an 8x8 bitmap. That maps nicely onto the GameBoy Tiles (I just repeat each byte), but for real text display I'll have to properly mask off the correct half of each character and combine it with the existing background.

That's the subject for the next post in this series. 

GameBoy Pixmap Demo (Part 2)

 In the my most recent post, I started to discuss a recent interest in the Nintendo GameBoy (original version), and how to use its Tile-based graphics to implement a full frame buffer.

This post discusses some more intriguing aspects of the hardware architecture before moving on to the development process.

GameBoy CPU (SM83)

The GameBoy CPU is a fairly strange design. Most of the references to it describe it as a variant of a Z80, mostly I guess because the assembly-based development environments use Z80 mnemonics.

However, the CPU is, in my opinion, much closer to the 8080 than a Z80, because it lacks a number of features forcing clumsy solutions to programming problems and often far less efficiency.

Deficiencies

Registers

Amongst the most significant is the lack of a second bank of registers. The Z80 is register-rich for an 8-bit CPU, whereas the 8080 is somewhat meh, borrowing its register set from the original 8008. Like the 8080 there are just 7 main registers; of which 6 can be paired up as 3x 16 bit address registers.

A small register set would be fairly usable if the CPU provided indexed addressing modes, but the 8080, Z80 and SM83 only support register-pair indirection from these registers. The SM83 also lacks some instructions available on the 8080, namely being able to load HL from the contents of a 16-bit address: absolute addressing is only possible for loading and storing A.

The 8080 and Z80 also have the ability to exchange DE with HL and HL with (SP) (i.e. the top of stack). The SM83 lacks these which often forces two LD instructions to copy a 16-bit register and major limitations on using the stack for operands.

Jumps

The SM83 also lacks a number of jump conditions present on the 8080 and Z80, namely JP P (Jump positive), JP M (Jump Negative), JP PO (Jump Parity Odd), JP PE (Jump Parity Even). And this is because the SM83 lacks the parity and negative flags present on the others. However, having said that, most jumps are based on the zero and carry flags, which the SM83, 8080 and Z80 all have.

The Z80 has a very helpful looping instruction, DJNZ which isn't present on the SM83.

16-bit Operations

The SM83 lacks 16-bit operations found on the Z80, namely 16-bit SBC and ADC instructions.

Timing

All of the Z80, 8080 and SM83 have variable length timings with a minimum of 4 cycles per instruction. However, the SM83's timings are all multiples of 4 cycles. This means that all the memory operations that take 7 cycles on a Z80 and 8080 take 8 cycles on an SM83. Loads which take 10 cycles on a Z80 and 8080, take 12 cycles on an SM83. Long jumps which take 10 cycles on the other two take 16 on the SM83. Call/Returns which take 17/10 cycles on the 8080 (16/10 on the Z80) take a stunning 24/16 cycles on the SM83 (it's really not good for call/return). 16-bit INCs and DECs take 6 cycles on a Z80 (5 on the 8080), but 8 on the SM83.

Z80 Features

As per many discussions, the SM83 includes the full set of the Z80's bit manipulation instructions as well as introducing the SWAP nybble instructions.

Advantages

However, the SM83 does have a few advantages over the other CPUs:

  1. Because all the instructions are a multiple of 4 cycles, it's easier to count cycles, which is often important on gaming devices of this era.
  2.  16-bit ADD instructions are actually 27% faster on the SM83 than the Z80 (20% faster than an 8080).
  3. ADD SP,n8 allows you to allocate and deallocate stack frames quickly.
  4. Even though loading data from the stack is slow, LD HLSP+d8 can be used to point HL to locals or parameters.
  5. There's a whole set of what amounts to zero-page memory you can access with LDH A,[n8] and LDH [n8],A instructions as well as LDH A,[C] and LDH [C],A, in place of the IN/OUT instructions on the 8080 and Z80. These access the I/O registers too.
  6. LDI [HL],A and LDI A,[HL] with both post-increment and post-decrement addressing modes provide for very fast FIFO or buffer operations as they're only 8 cycles each.
  7. LD [A16],SP goes some way to making context switching easier.
  8. Finally, the 8-cycle SWAP r instructions help significantly with bit shifting. For example, SWAP A, AND 15 performs a >>4 in 16 cycles (it would take 23 on a Z80, RRCA * 4, then AND 15).

Microarchitecture

At first I thought that the SM83 must be highly microcoded, due to the multiple of 4 instruction timings, but it turns out that it's all hard-wired.

Conclusion

The SM83 is quite an interesting 8080 / Z80 hybrid-variant with architectural and instruction level features missing from both; some Z80 features and a few features not found in either.

My Demo program was written entirely in SM83 assembly via the very helpful https://gbdev.io/rgbds-live/ IDE and emulator. It's quite a strange experience, because of the different timing and instructions that aren't available.

If I'd been designing it I would have made a few different choices. Firstly, I would have removed all the conditional long jump instructions, because the short +/-128, relative jumps cover nearly all cases. I would have kept the unconditional jump.

I would have included the EX DE,HL instruction, because it's just so much more useful.

I would have added LD BC/DE,[SP+d8] and LD [SP+d8],DE/HL instructions as they would have been really useful for managing stack frames (though this probably isn't very important to games developers) and making up for the lack of alternate registers on the Z80. That's 4 more instructions. I would also have added LD HL/DE, [a16] and LD [a16],HL/DE instructions, because loading globals is important. That's another 2 instructions, a net gain of 1, but I would have removed LD [a16],SP, because LD HL,SP+0 then LD [a16],HL is equivalent (so is LD HL,0; ADD HL,SP; LD [a16],HL). INC SP and DEC SP are also redundant.

I would have replaced the redundant prefixed, RL A, RLC A, RR A, RLC A, and SLA A instructions with ADC HL, HL; ADC HL,DE; SBC HL,DE; RR DE and ADC HL,HL instructions to make other 16-bit operations quicker.




GameBoy Pixmap Demo

Apologies in advance as this is my first GameBoy coding attempt!

I've had a sudden interest in the original Nintendo GameBoy after a 68KMLA article started discussing an emulator for it, that runs on a 68K Mac, at near full speed:

https://68kmla.org/bb/threads/gb6-game-boy-emulator-for-system-6.41600/

And that's quite impressive given that the original GameBoy ran on a 4MHz Z80-type CPU and has a whole pile of quirky graphics tricks that can both slow down the unit by shocking amounts (as we'll find out), but is also the source of its capabilities when programs are tailored to them.

Of course, I don't want to use the kind of normal tiling, sprite and window tricks we'd expect on a GameBoy. Instead I want to treat the screen as a simple pixel-mapped frame buffer upon which I can write text anywhere and do the kinds of graphics operations that were normal on 8-bit computers. I am interested in whether that's at all practical.

Tile-Based Graphics

And it might not be. I'm not going to fully go into the architecture here, because PanDocs does a far better job than I could. I just want to mention the most relevant aspects for my demo.

GameBoy graphics are tile-based.  It's a half-way house between early character-based computer graphics and full-fledged frame-buffer graphics that became dominant in computers from the mid-1980s and in game systems from the end of the 1980s. The way they work is that video ram contains a number of, typically 256 x 8x8 pixel blocks, which are called 'Tiles' and then the screen is divided into a 2-d array of values, called the Tile Map, each of which references a Tile. Typically, the values are 8-bits, so each entry in the tile map can reference any one of the Tiles.



When the video generator scans the screen, it scans the Tile Map from top left to bottom right, reading a TileMap entry's value to get the Tile and then it reads a row of pixels from the tile (typically 8 pixels) and transfers those pixels to the screen. When a scan line has been completed, the video generator starts on the next scan line, which for the next 7 rows means reading the same Tile Map entries as before, leading to the same Tiles, but a new row on each Tile.


At one level, this is inefficient, because for every row of pixels sent to the screen, both a Tile Map entry and the pixels themselves need to be read, which might mean up to twice the number of memory accesses.

But on early 8-bit computers (of which the GameBoy is effectively one), it's worth it, because firstly, most of the effort for manipulating the screen can be done by changing Tile Map entries rather than the pixel data. Secondly, because these 8-bit computers often lacked sufficient memory to represent an entire pixel-mapped screen, a lot of video RAM can be saved by re-using the same Tile in multiple Tile Map entries. And because most 8-bit video games had a lot of repeated graphics, it's a massive advantage. So, in summary: Tiled graphics can be much faster to manipulate and use far less memory.

Tiled graphics are best for representing relatively static background images, because otherwise the programmer has to compose the animated parts of a screen using dynamically allocated tiles, which undermines the performance advantage. So, in many systems (like the Commodore-64, the Nintendo NES, Sega Megadrive and of course the GameBoy), tiles are augmented by Sprites which are additional tiles that can overlay the tiled image at any location. The video hardware then handles the effort of composing the background tiles and Sprite data, and typically this also means a limitation in the number of Sprites that can appear on the screen at any point, due to a limitation in hardware registers for Sprites or the sheer amount of hardware effort needed to compose them on top of the tiled background.

Thus, games oriented around tiles and sprites tend to be platform-oriented: a somewhat repetitive 2-d background that can scroll around and upon which the main game characters act.

Well, that's enough about Sprites, because I'm not interested in them for my demo. Suffice to say that the GameBoy implements plenty of them; using them takes up extra video processing and also adds a second video layer called the "Window". I'm not interested in that either.

A Proposed GameBoy Full Frame Buffer

The GameBoy TileMap is 20x18 and each entry is 8-bits, which means that there are 360 tile map locations, but only 256 tile map entries are possible. This would normally preclude being able to support a full frame buffer, which would require a unique tile for each tile map entry (i.e. 360 tiles).

However, it should be possible to achieve that. The GameBoy also has an LCD control register which allows you to select an alternate set of tiles so that the same tile map entry could point to a different tile in a different part of the screen. And it's possible to generate an interrupt on a given scan line. This means that we can use the first 256 tiles for most of the screen, then on the given scan line interrupt switch to the other set of tiles; and then at the end of a video image - in what's called the VBlank region (of 10 scan lines), reset the LCD control register back to the original set of tiles.



Cleverly, the GameBoy tile mapping works so that tile map values $80 to $ff on the first set of tiles map to the same tiles as for the the second set, because the first set are treated as unsigned tile map values (so $80 = 128, which comes after $00 = 0), but the second set are signed ( because $80= -128, which comes before $00 = 0).

Conclusion

The Nintendo GameBoy was an incredibly successful handheld games console beating even more advanced handheld games consoles, thanks to its lower price and power consumption.

To save memory and performance (given its lowly 8-bit CPU), it used Tiled Graphics techniques borrowed common 8-bit graphics techniques, particularly from the Nintendo Entertainment System. Normally (as on the NES), this would heavily restrict the versatility of possible images, because the number of unique tiles will be less (256) than the number of tile locations in the tile map (360 in the case of the GameBoy). However, the GameBoy supports a second set of tiles and a raster trick which should make it possible.

In the next blog post I'll discuss the SM83 CPU at the heart of the system.

Thursday, 6 August 2026

Asymptotic Wildfires

Introduction

Amidst the record-breaking wildfires being experienced in Europe over summer 2026, many people believe the solution is to reduce the number of causes - and in fact attach agency, such as arson, to those causes in preference to the scientific explanation, global warming.

The Climate Science argument is that the critical issue is global warming itself, because it supercharges wildfires: heatwaves get more common and intense. Then land gets drier and therefore easier to combust. Once a fire starts, conditions make fires burn harder, faster and hotter.

This article explores one major reason why this property is the critical one, over and above that of the number of fires - and it's due to some fairly simple maths. People who can understand and communicate the maths, will have an advantage over climate science deniers who will repeatedly pick the wrong solution.

The Problem

The problem occurs because there is essentially a finite amount of resources we can apply to extinguishing (denoted by 'x' here) wildfires. As the speed of a wildfire front (denoted by 'f' here) increases, it has a disproportionate impact on the rate of progress (denoted by 'p' here) at which fires can be put out.

We can understand some of this intuitively. x can be measured as a speed: the rate at which we can put a fire out given reasonable resources, in m/s. Similarly, f represents the speed of the front of a wildfire, also measured in m/s. Thus if f = 0, i.e. the fire is not moving (but is still burning), then we know that as long as x is greater than 0, we can put the fire out. And the general rule here is that f≥x means the fire can never be put out. If the fire already has an extent, then for every metre of fire we put out, the fire has advanced by over 1m. So, this establishes hard limits.

Early Stages

The early stages of a fire is a front that would tend to expand outwards in a circle from its source at a constant rate. In this model we'll ignore wind speed and wind direction, in order to simplify the maths.

One thing we can observe is this: the initial part of the fire will be slower than when the fire is more mature. And this is because when the fire starts, there's a substantial curvature at the front and this means that the heat distribution at any one point at the front has to set light to a wider span ahead of it, that is: the fire is fanning out. But because it's setting light to a wider span ahead it means that there's less heat being applied to any point ahead of the front, because it has to be distributed to the span ahead.




This means that the fire will initially travel slower and is therefore easier to extinguish. It's possibly also why in the early stages of a wildfire, people believe they can stop the fire relatively easily - because it's slower and the temperature curve ahead of the front is less intense. And this is partly true, it will be easier to stop.

That's why we ignore this in our model.

Maturity

When the fire is mature, the front is essentially a straight line. Obviously terrain will affect this, but this will be a reasonable approximation. The fire is hotter now and is travelling faster, because there is no fan-out.

So, as the front advances by f metres, the amount extinguished will be along the hypotenuse of a right-angled triangle (because the fire-fighters want to move along the fire, but also outward as it advances). The amount of progress must be p.




Thus we can see that p=√(x²-f²), by a simple application of Pythagoras' theorem. So, the key question is this: as f approaches x, what's the ratio of x:p, the amount we extinguish, vs the amount of progress? So, if we normalise it, by making x=1, we get p=√(1-f²) and so we want to know 1/√(1-f²) .

Results

The results are actually quite shocking. At low rates of f, e.g. f=0.1, x/p is also very small (in this case 1.005). But as f gets increasingly close to x, even tiny changes in f result in large changes to x/p. Thus at this stage, as global warming increases, even tiny increases in local temperatures and burn rates in a wildfire result in worse than exponentially more land burned than as a result of direct human causes such as barbecues or cigarettes (which is linear).

Conclusion

Climate Science deniers always ascribe agency to anything they disagree with: Climate Scientists just say what their bosses tell them to; floods happen because people don't dredge rivers (dredging rivers doesn't really help in fact); wildfires happen because of arsonists. Conversely, they use "common sense" to determine policy, which allows them to bypass all the science they want, but in turn means their policies are driven by human agency rather than objective criteria, by definition.

But this kind of thinking means they can't plan properly for climate extremes, because they'd never trust the models that predict the future.

Wildfire Sim

MinX: MaxX: Your browser does not support the canvas element.


Tuesday, 28 April 2026

Shorter QL GetHead

 A couple of days ago I wrote a blog post about being able to read QL executable file headers so that you can restore them from original QL applications on QL disks, onto a modern OS which lacks them when running a QL emulator.

https://oneweekwonder.blogspot.com/

It's actually fairly involved here, involving one SuperBASIC program to load a machine code hex program (and then save it on disk); then write another SuperBASIC program to load that code, which adds a couple of functions to SuperBasic. And to make it even more complex I ended up wrapping them with SuperBASIC procedures to make opening the channel so you can use the SuperBASIC extensions and then closing it at the end.

It turns out though you can make it much shorter, a single, simple SuperBASIC program:

10 mcode=RESPR(8):buffer=RESPR(64):POKE_L mcode,0+"536956483":POKE_L mcode+4,0+"1879068277"
100 DEFine PROCedure GetHead(fName$)
110 OPEN #3,fName$:CALL mcode,71,64,500,0,0,0,0,ChanId(3),buffer
120 PRINT "Len:";PEEK_L(buffer);" Dataspace:";PEEK_L(buffer+6)
130 CLOSE #3
140 END DEFine
150 DEFine FuNction ChanId(chan):LOCal a6:a6=PEEK_L(163856)+104
160 RETURN PEEK_L(PEEK_L(a6+48)+a6+chan*40):END DEFine

It does the same thing. You should run the program, and then you can type things like: GetHead "mdv1_quill" to return the size of the program and the data space.

It nicely illustrates some of the impacts of having a fancy version of Basic (for the day) and the relative complexities of Sinclair's QDOS operating system.

In 1984, when the QL appeared, not many computers had 128kB of RAM. The Macintosh had just appeared, with 128kB. Most PCs were still in an era where 128kB was fairly normal; the PC/XT came with a standard 128kB as did the Sirius One. Probably most original PCs were still living with ≤128kB. The Atari ST, Amiga, PC/AT, BBC Master 128, Commodore 128, Amstrad CPC128 were all in the future.

So, to see a new computer with 128kB was quite a luxury. And since it had a structured BASIC it was very tempting to try and write elegant, i.e. wordy, code. Hence the Hex loader, which would have been a tiny thing on a ZX81 or ZX Spectrum was a couple of dozen lines long, containing multiple procedures, just to prove the programmer understood the concepts.

Wordiness can obscure as much as it reveals though. For example, I think this version is actually simpler to understand, so let's go through it.

Firstly, we allocate just 8 bytes for machine code and poke_l it in directly. It's just 4 instructions:

2001    MOVE.L d1,d0; because d0=function code and we can't pass d0 in CALL.
4E43    TRAP #3 ;all the other parameters are supplied in the CALL.
7000    MOVEQ #0,d0 ;return with no errors regardless.
4E75    RTS ;back to BASIC.

I had to use 0+"integer" in the POKE_L statements, because SuperBASIC will convert large integers into scientific notation with just 6 significant figures. 0+"integer" will perform a VAL("integer") thanks to SuperBASIC's type coercion.

As before, we also set up a 64 byte buffer.

The GetHead procedure takes fName$ as before; we open up the channel (3) and pass that to the CALL statement along with the function code (71), the size of the buffer (64), the timeout (10s); then channel Id (ChanId(3)) and finally the buffer address. All these parameters are copied directly to 68000 registers.

The complexity lies in the function ChanId(3). QDOS doesn't use SuperBASIC channel numbers as its Channel IDs, instead they're a 32-bit number that's not trivial to derive. So, you have to indirect it via the system variables into the SuperBasic variables and then from there calculate the offset into the channel table!

My method isn't fool-proof. A proper routine should calculate all of the relative to A6, because it could function as part of a multitasking job, where A6 could end up getting moved around. In my simplistic implementation, SuperBASIC is the only job running, so A6 won't change and can be calculated statically.

But the upshot is that you can simplify GetHead. You could also use the same machine code routine to implement SetHead, since it's really just a TRAP #3.



Saturday, 25 April 2026

Recovering Sinclair QL App DataSpaces

Recently I've been playing with a QL emulator again, because I found a printed copy of my third year Computer Science dissertation, and wanted to retype it in the default word processor, QUILL.

However, I couldn't run QUILL, because when the program was copied to my Mac's file system it didn't copy the header, which contains the data and stack space allocated to the program. And that's part of how the QL works, executable files contain meta-data providing this information and it gets lost on modern operating systems including Linux and Windows.

In theory, fixing it is as easy as reserving memory for the program (progCode=RESPR(sizeOfFile)), then loading the code (lbytes fileName,progCode), then re-saving it under a different name: (sexec_w newFileName,progCode,dataSpace). But this means finding out how much data space has been allocated for static data and the stack. And... this information isn't generally available, even though many QL owners have had to face the problem.

The nearest I got was a web site which contained a BASIC program which could tweak QUILL for a few features I didn't care about (see the section called QUILL Mod). But at the end it did sexec_w QUILL with an actual data space which worked. I was then able to use QUILL to type in the first couple of pages of my dissertation, which was fun.

This wasn't a solution for all the other programs I could run on my QL emulator, e.g. Forth79! Amazingly though I found an intriguing program on one of my QL directories on my Mac called: HeadRead_bas. This turned out to be a machine code program and hexloader for it, which would then save the machine code in a file. Could this be it? Here's the program:

100 CLS:RESTORE:READ space:start=RESPR(space)
140 PRINT 'loading hex..':endAddr=hex_load(start)
150 INPUT 'save to file';f$
160 SBYTES f$,start,endAddr-start
170 STOP
180 DEFine FuNction dec(h$):RETurn h$(1) INSTR "0123456789ABCDEF"-1:END DEFine
190 DEFine FuNction hex_load(start)
195 LOCal sum,addr
200 PRINT 'Data entered at:',start
220 sum=0:addr=start
230 REPeat load_hex_digits
240 READ h$:IF LEN(h$)<>2*INT(LEN(h$)/2) THEN PRINT "Odd Hex digit Count";h$:STOP
300 FOR b=0 TO LEN(h$) STEP 2
360 byte=16*dec(h$(b+1))+dec(h$(b+2)):POKE addr,byte
370 sum=sum+byte
380 addr=addr+1
390 END FOR b
400 END REPeat load_hex_digits
410 READ check
420 IF check=sum then print "Sum OK":else print "Bad Sum"
430 RETurn addr
490 END DEFine
500 DATA 144
510 DATA '43FA000A34790000','01104ED20002001E'
520 DATA '0747657448454144','0010075365744845'
530 DATA '4144000000000000','784660027847BBCB'
540 DATA '675A2A0D4BEB0008','3479000001124E92'
550 DATA '664C3031E80054AE','0058264D2A45C0FC'
560 DATA '0028D0AE0030B0AE','00346C2C2A360800'
570 DATA '6B26347900000118','4E9266225343661C'
580 DATA '2031E80008000000','6612204522407440'
590 DATA '766420044E434E75','70FA4E7570F14E75'
600 DATA '*',10007

The program doesn't read QL program headers, it just creates the machine code file you can then use in another program to read headers. And that program was elsewhere in the same directory too:

10 hdrMod=RESPR(144):LBYTES mdv1_GetSetHead_bin,hdrMod:CALL hdrMod
100 BUFFER=RESPR(64)
120 INPUT 'ENTER DEVICE:';F$
130 OPEN #3,F$
140 GetHEAD #3,BUFFER
150 PRINT F$;', ';PEEK_L(BUFFER);' BYTES'
160 PRINT 'LAST ALTERED ';DATE$(PEEK_L(BUFFER+52))
170 PRINT 'CURRENT DATA SPACE ';PEEK_L(BUFFER+6)
210 CLOSE #3

It loads in the machine code first. It turns out the machine code adds the command GetHEAD to BASIC. GetHEAD reads the header from a file at the given channel and stores it in an allocated buffer. Then we can look at offsets in the file for the actual size of the executable and the data space.

This solves part of the problem: I now had a program which could read the headers. However, all the reported dataspace values were reported as 0. Fortunately, I still have my real Sinclair QL and a floppy disk system which is still largely reliable! I could either look for the same BASIC programs on a floppy disk, or type it out by hand again. Indeed, the programs were on floppy disk too!

Now I was able to list all the data spaces for the executable files I had. It turns out that all the PSION programs for version 2.3 (though Easel is version 2.0) had a data space of 1280 bytes. So, then I could get all of them to work! Mostly I used QUILL and the Spreadsheet, ABACUS. I used the ARCHIVE database a bit and EASEL very little.

The rest can be summarised in this scrappy table:

Program                Size DataSpace
Computer One Assembler: ASSEMB 18094 256
Computer One Editor: EDITOR 12714 256
Computer One Linker:LINKER 4278 256
And Linker_A (??): LINKER_A 8616 4800
Debugger: debug_exc 2272 500
eda         13653 256
eye_q_dp         31476 43008
forth79         12616 57528

You might like to know what the assembly code for Header read is? I disassembled it using the Alan Giles disassembler written in BASIC. It's slow, about 1 or 2 lines per second but good enough for this.

3FF00 43FA000A               LEA     $000A(PC)=$3FF0C,A1
3FF04 347900000110           MOVE.W  $00000110,A2
3FF0A 4ED2                   JMP     (A2)
3FF0C 0002001E               OR.B    #$1E,D2
3FF10 0747                   BCHG    D3,D7
3FF12 6574                   BCS.S   $74(PC)=$3FF88
3FF14 4845                   SWAP    D5
3FF16 4144                   DC.B    'A','D'
3FF18 0010                   DC.B    0,16
3FF1A 0753                   BCHG    D3,(A3)
3FF1C 6574                   BCS.S   $74(PC)=$3FF92
3FF1E 4845                   SWAP    D5
3FF20 4144                   DC.B    'A','D'
3FF22 00000000               OR.B    #$00,D0
3FF26 0000                   DC.B    0,0
3FF28 7846                   MOVEQ   #$46,D4
3FF2A 6002                   BRA.S   $02(PC)=$3FF2E
3FF2C 7847                   MOVEQ   #$47,D4
3FF2E BBCB                   CMP.L   A3,A5
3FF30 675A                   BEQ.S   $5A(PC)=$3FF8C
3FF32 2A0D                   MOVE.L  A5,D5
3FF34 4BEB0008               LEA     $0008(A3),A5
3FF38 347900000112           MOVE.W  $00000112,A2
3FF3E 4E92                   JSR     (A2)
3FF40 664C                   BNE.S   $4C(PC)=$3FF8E
3FF42 3031E800               MOVE.W  $00(A1,A6.L),D0
3FF46 54AE0058               ADDQ.L  #2,$0058(A6)
3FF4A 264D                   MOVE.L  A5,A3
3FF4C 2A45                   MOVE.L  D5,A5
3FF4E C0FC0028               MULU    #$0028,D0
3FF52 D0AE0030               ADD.L   $0030(A6),D0
3FF56 B0AE0034               CMP.L   $0034(A6),D0
3FF5A 6C2C                   BGE.S   $2C(PC)=$3FF88
3FF5C 2A360800               MOVE.L  $00(A6,D0.L),D5
3FF60 6B26                   BMI.S   $26(PC)=$3FF88
3FF62 347900000118           MOVE.W  $00000118,A2
3FF68 4E92                   JSR     (A2)
3FF6A 6622                   BNE.S   $22(PC)=$3FF8E
3FF6C 5343                   SUBQ.W  #1,D3
3FF6E 661C                   BNE.S   $1C(PC)=$3FF8C
3FF70 2031E800               MOVE.L  $00(A1,A6.L),D0
3FF74 08000000               BTST    #$00,D0
3FF78 6612                   BNE.S   $12(PC)=$3FF8C
3FF7A 2045                   MOVE.L  D5,A0
3FF7C 2240                   MOVE.L  D0,A1
3FF7E 7440                   MOVEQ   #$40,D2
3FF80 7664                   MOVEQ   #$64,D3
3FF82 2004                   MOVE.L  D4,D0
3FF84 4E43                   TRAP    #$3
3FF86 4E75                   RTS
3FF88 70FA                   MOVEQ   #$FA,D0
3FF8A 4E75                   RTS
3FF8C 70F1                   MOVEQ   #$F1,D0
3FF8E 4E75                   RTS
3FF90 0000                   DC.B    0,0

The section hilighted in yellow is actually the information passed to SuperBASIC for the new command names and their syntax. In a future edit I hope to annotate it better.

Anyway, armed with this information you too, can go back to your old, actual QL and work out the data spaces for all the executables you couldn't otherwise run on your QL. Feel free to add them in comments!


Tuesday, 24 February 2026

Bike vs BEV

Introduction

We're frequently told that non-motorised bicycles are the most energy efficient transport in the world. So, there's no way a full EV (BEV) can compete with a bike for energy efficiency.

Or can it?

For example, it's easy to show that a bicycle at 20km/h requires about 75W of power; whereas a typical BEV might use 8.2kW at 50km/h. So, it's no contest.

Or is it?

The problem is that when people make these kinds of comparisons, they're comparing the energy required for the final product rather than the energy required at the source. For a non-motorised bike, that energy comes from the sunlight used to grow crops or farm animals + the energy used to process the food, but for a BEV that energy can come from a renewable energy source (such as Solar PV). Thus the right question to ask is whether replacing the crops making the additional food the cyclist needs, with Solar PV is more than the Solar PV needed for the BEV for the same distance.

With a bit of maths and some publicly available data, we can work it out!

Method

The basic concept is to make some simple (but not unreasonable assumptions) about how we power the cyclist, alongside some corresponding calculations for the BEV. The bike conversions we need are:

We'll see that when we do the calculations, available data is often in different units, so we'll have to do some unit conversions too. Also, we'll end up calculating backwards. For the EV, it's:


We can see already there are fewer obvious areas of loss, but that's because I've combined the motor for the BEV and the BEV into the same box (whereas for the Bike, the human is the motor).

Bike Calculations

This website provides us with a handy table for calculating power consumption for a given bicycle speed:


At 20km/h it gives 75W, which means that travelling 20km requires 75Wh of energy (3.75Wh/km). A Wh is 860 calories, so 75Wh is 860*75=64,500 calories, or 64.5kcal.

Humans, being biological systems burn fat (and other chemical energy), and emit CO₂, much like a combustion car burns petrol, except that it outputs CO₂ from fossil fuels, which adds to the atmosphere, increasing global temperatures. But the key thing is that the mechanism is similar, because chemicals are burned, so the efficiency is similar. In fact it's about 25%[1].

So, the amount of energy the human needs is 64.5/0.25=258 kcal to travel 20km. That's 258/20=12.9 kcal per km.

My big assumption is where the human gets that energy from. I assume they're getting it from a sandwich with no filling, i.e. from bread. And I approximate that with flour, because most of it will be flour and the other stuff, e.g. butter will be less energy efficient, because the energy conversion factor going from the Sun to butter also goes through crops and cows, and therefore can't be better than crops alone. The same reasoning applies to the filling from a sandwich, e.g. egg mayonnaise. The eggs have to go through a conversion factor involving crops and chickens, and therefore can't be better than crops.

It turns out that there are 3.58 calories per gramme of flour. So, 12.9 kcal requires 3.6g of flour and that's tiny. For white flour, we are interested in the energy content of the flour, which is 353kcal per 11.3g of protein[5]. From [4] we can see there's about 32kg of protein per Hectare, which means there's 32000/11.3*353kcal=999646 kcal per hectare. A hectare is 100m*100m so there's 999646/(100*100)=99.965 kcal per m². So, we need 12.9/99.965=0.129 m² of cropland per km.

BEV Calculations

This is a bit simpler, I'll take our Renault Zoe and our SolarPV as an example. Our Zoe achieves about 3.8miles per kWh, which is 3.8*1.609=6.114 km/kWh. So, 1 km uses 1/6.114=0.164kWh which is 164Wh.

We have 3.96kW of SolarPV on our roof, which usually provides 3200kWh per year. Current solar panels are rated at 450W and on average are 1.6m² which means that 0.45/3.96*3200/1.6=227.273kWh is generated per m².

Thus we need 0.164/227.273=0.000722m² of SolarPV per km with a Battery Electric Vehicle.

Results

It takes 0.129m² of cropland per km of cycling, but 0.000722m² of SolarPV per km in our BEV. Thus a BEV is 0.129/0.000722=178.67 x more efficient than a cyclist in terms of land area, a truly astounding result!

Conclusion

Cycling takes up land area to feed cyclists. We can estimate the land area based on the energy used by the cyclist; and the most optimal amount is if the cyclist eats only plant-based food. Thus every km of cycling (per year) corresponds to an area of crop land on a yearly basis. With some simplified, but reasonable assumptions, the value is 0.129m² of cropland per km, for a cyclist travelling at 20km/h.

A BEV only takes 0.000722m² of SolarPV per km per year, about 180x more efficient than a cyclist even though the BEV needs 164/3.75=43.733 more final energy per km.

There are two main reasons for this. Firstly people, being biological systems are inefficient, roughly as inefficient as a combustion car because they both burn chemicals for energy (though as said before, combustion cars add to atmospheric CO₂ and global temperatures). Secondly, and most importantly, crops are astoundingly inefficient compared to solar panels when it comes to energy conversion efficiency.

There are flaws with my method and conclusion, but not flaws amounting to 2.3 orders of magnitude. I only picked a single crop (wheat), but perhaps other crops have better energy yields per m² per year. I picked our Renault Zoe, which has a pretty good range/km: other BEVs, especially bigger ones can be worse (but some are better). On the other hand, practical food intake is far more than just flour, sandwich fillings will be more energy inefficient and I ignored food processing (though it would be a minor contribution). Also, I could have picked an e-bike: that same 75W would translate to 43.7x better efficiency!

But the upshot is clear: when we estimate land-use for transport, cycling requires over 200x the area of a decent Battery Electric Vehicle, yet no-one would consider the land-use requirements for cycling to be excessive.

Refs:

[1] “Thermal energy generated during the chemical reactions that power muscle contractions along with friction in joints and other tissues reduce the efficiency of humans to about 25 %.” 
https://phys.libretexts.org/Bookshelves/Conceptual_Physics/Body_Physics_-_Motion_to_Metabolism_(Davis)/10%3A_Powering_the_Body/10.09%3A_Efficiency_of_the_Human_Body




Wednesday, 31 December 2025

Personal Computer World Sun SparcStation-1 Review

While doing some research on the Sun Sparcstation-1 I was finding it difficult to search for pages that actually gave the prices of anything other than the basic diskless configuration. However, the review of the SparcStation in Personal Computer World (June 1989) in fact does. It's a comprehensive review and word reading! 

This post is almost entirely just pictures!








The critical information I was looking for is near the end.

The entry-level price for a SPARCstation 1 with 8Mbytes of RAM, a single 1.44Mbyte floppy drive, a standard 1152x900 video board and a 17in grey-scale screen is £7400, while a system with two
104Mbyte hard disks and the same video board driving a 16in 256-colour screen costs £12,700. With two hard disks, a 19in colour screen and the GX graphics accelerator, the price goes up to £16,400.

Conclusion

The Sun SparcStation 1 was a ground-breaking RISC workstation with a performance and price that matched x86 (i.e. the recently released i486) PCs for at least the next 12 months. I'm writing a series of blog posts on the machine. However, because data relating to the computer is getting lost to the internet over time it shows that having actual hard-copy magazines of the era can provide better and faster answers than Google (or another search engine).












TI-30LCD Says Hello & Does Stats!

There were a wide variety of scientific calculators in our Maths class in 1981. In 2025 I bought a Casio FX-180P to compensate for my original Casio FX-180P that got broken! It's great calculator with lots of features.

Although Casio calculators were the most popular at school at the time, other schoolmates had different makes. In my mind, I recall the TI-30LCD was one of the worst, but was it really that bad?

So, I bought one off eBay too! It turns out it's quite easy to make it says Hello!



Introduction

This was Texas Instruments' first attempt to update their much earlier TI-30 LED scientific calculator, which had been quite popular. They copied the late 70's brushed-aluminium look to make it seem more trendy and Japanese. The sideways view was pretty ugly though, because they tried to make it really thin, but then stuffed it up by adding a bulky 'AA' battery compartment instead of lithium coin cells!



 
Why? Cheap batteries? To make it incline to compensate for the poor LCD contrast?

It does have one nice touch, the [ON/C] button is slightly recessed: 1mm lower than the others and has a prominent surround to stop users accidentally turning it on when it's bouncing around in a haversack.

The buttons are pretty stiff though, requiring some force before clunking down, which although I think these are a bit worn after 44 years, it's much like I remember when borrowing a school-mate's TI-30 briefly. The layout is quite clever though; adding keys on both sides of the numbers makes for a pleasing symmetry.

On the positive side it can do the basic scientific stuff: π, Trig, logs, factorial and BOOBS! It's only got 8-digits + a dedicated '-', so 99-BOOBS- is its limit! Also interestingly, it displays "Error" in text using the LCD segments whereas Casio calculators just said: "E"

It's slow and inaccurate. 69! takes 1.34s on my FX-180P, but 7s on the TI-30 😮 ! The sin⁻ⁱ(cos⁻ⁱ(tan⁻ⁱ(tan(cos(sin(1)) test says 1.4756033, whereas my FX-180P says: 1.00020289 (it should be 1).

Finally, it lacks a specific stats mode which makes computing standard deviations a real pain and only has 4 levels of brackets (the Casios had 6). OTOH it can convert from degrees to radians if you press a number then [INV] [DRG>].

This calculator travelled 122 miles to get to me, but originally it was bought for a company, just 16 miles from where I live!

Stats

I played around with the calculator for a while and cleaned it up. Then I did a bit of thinking and realised that it is in fact possible to compute statistics fairly easily!

The standard Casio Calculator stats mode could compute all the functions you needed (like population and standard deviation, or averages) from three variables:

  • n: the number of items entered.
  • ∑x: the sum of the items.
  • ∑x²: the sum of the squares of all the items.
In theory this means you need 3 memories. We can reduce it to 2 by simply counting n, the number of items. But the TI-30 only has one memory. Or does it?

The trick is to use the internal memory to compute the sums, and the calculator stack to compute the ∑x² terms. And this is possible, because as well as a [STO] and [RCL] button on the calculator, it also has a [SUM] button.

So, we can use [SUM] to store the ∑x terms as you enter each one and [x²] [+] to compute the running ∑x² total.

For example, let's say we have the data: {5, 4, 8, 3, 4, 5, 7, 4, 3}.

First, you'd press: [0] [STO] to clear memory and the display.

Then

Number ∑x Term Generate x² (Display) ∑ (Display)
5 [SUM] [x²] 25 [+] 25
4 [SUM] [x²] 16 [+] 41
8 [SUM] [x²] 64 [+] 105
3 [SUM] [x²] 9 [+] 114
4 [SUM] [x²] 16 [+] 130
5 [SUM] [x²] 25 [+] 155
7 [SUM] [x²] 49 [+] 204
4 [SUM] [x²] 16 [+] 220
3 [SUM] [x²] 9 [+] 229

At this point the calculator shows 229 (∑x²). Pressing [RCL] gives you 43 (∑x) and manually you count n=9.

So, then:
  • Average=[RCL]/9=4.78;
  • Standard Deviation=√(∑x²/n -(∑x/n)²= √(229/9-(43/9)²)= 1.62.
It's better to clear the memory first and then use [SUM] all the time even though you could do 5 [STO] for the first one, because it allows you to repeat the pattern and avoid thinking.

I don't think this was actually taught as a technique at school. Instead they expected you to compute ∑x terms, then go back and compute the ∑x² in a new column, before summing each column and then calculating the variance (rather than the Standard Deviation). This technique avoids entering the numbers twice. It's still somewhat slower than using a Casio with its Sd mode, but at least it's quicker than a purely manual mode.

Correcting Errors

I've also worked out how to correct data entries too. Let's say you mis-entered 5307 as 5607 by typing:
5607 [SUM] [x²] [+]

On an FX-82 you'd just do 5607 [DEL] and it would correct it. But on a TI-30 there's no such button (just as there isn't an [x] button). But correcting data is almost as easy. You type:

[-] 5607 [+/-] [SUM] [X²] [+]

On a real TI-30 LCD you're much more likely to miss or double-type a digit due to the dodgy debounce (it shares LCD and keypad pins so it can't display and read keys at the same time), so correcting errors is important.

The TI-30 isn't RPN, so the [+] at the end of each line signifies that the next calculation will be another addition, but also gives you the running total. So, the initial [-] overrides the previous [+] so that when the [+] is hit at the end, it subtracts 5607². But when you enter the number, you can't type [SUM] to delete the ∑x term, because the number on the display will be positive. You need to hit [+/-] to make it properly negative and then hit [SUM]. When you hit [X²] it makes it positive again so the earlier [-] will subtract, as you intend. Also, it wouldn't work to type in 5607 [+/-], because when you later hit [X²] it'll become positive again.

Conclusion

The TI-LCD was already a poor calculator by the standards of the early 1980s. It lacked functions other calculators had as standard and accuracy was poor. However, a critical statistical mode can be implemented fairly easily, and can roughly halve the number of keypresses to compute.