From cd82532c26f2f1d8a004bba3c274717ae5facdcf Mon Sep 17 00:00:00 2001 From: Zeal8bit Date: Fri, 12 Apr 2024 21:39:08 +0800 Subject: [PATCH] video: add support for the Zeal 8-bit Video Card It is now possible to use the bootloader with the Zeal 8-bit video card and the PS/2 keyboard. The standard output can now be chosen in a configuration file. Note that the UART is still enabled even if it not the standard output because it is required to receive files. --- Makefile | 45 ++-- README.md | 41 ++-- concat.sh | 25 +++ img/menu.png | Bin 25354 -> 0 bytes img/menu_uart.png | Bin 0 -> 39017 bytes img/menu_video.png | Bin 0 -> 11945 bytes include/config.asm | 25 +++ include/keyboard_h.asm | 30 +++ include/pio_h.asm | 5 +- include/stdout_h.asm | 83 +++++++ include/uart_h.asm | 10 +- include/video_h.asm | 156 ++++++++++--- src/boot.asm | 115 ++-------- src/format.asm | 20 +- src/keyboard.asm | 123 +++++++++++ src/menu.asm | 151 ++++++++----- src/pio.asm | 67 ++++++ src/systems.asm | 13 +- src/tester.asm | 35 ++- src/tileset.bin | Bin 0 -> 648 bytes src/uart.asm | 158 ++++++++++---- src/video.asm | 483 +++++++++++++++++++++++++++++++++++++---- 22 files changed, 1248 insertions(+), 337 deletions(-) create mode 100755 concat.sh delete mode 100644 img/menu.png create mode 100644 img/menu_uart.png create mode 100644 img/menu_video.png create mode 100644 include/config.asm create mode 100644 include/keyboard_h.asm create mode 100644 include/stdout_h.asm create mode 100644 src/keyboard.asm create mode 100644 src/pio.asm create mode 100644 src/tileset.bin diff --git a/Makefile b/Makefile index 15ab07a..ea964ce 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,28 @@ CC=$(shell which z88dk-z80asm z88dk.z88dk-z80asm | head -1) BIN=bootloader.bin -DUMP=bootloader.dump -ENABLE_TESTER = 1 -ASMFLAGS= -FILES=rst_vectors.asm boot.asm uart.asm systems.asm video.asm menu.asm format.asm +MAP=bootloader.map +CONFIG_FILE := include/config.asm + +# Process the config file to include only lines of the form 'CONFIG_... = ..' and save them to +# a file named 'bootloader.conf' +$(shell grep -o 'CONFIG_.*=.*' $(CONFIG_FILE) > bootloader.config) +-include bootloader.config + +# When ACK_CONTINUE is set, a message will always be displayed before redrawing the main menu +ASMFLAGS=-Iinclude/ +FILES=rst_vectors.asm boot.asm uart.asm systems.asm pio.asm menu.asm format.asm i2c.asm + # SRCS must be lazily evaluated since it depends FILES, which may be altered below SRCS=$(addprefix src/,$(FILES)) DISASSEMBLER=$(shell which z88dk-dis z88dk.z88dk-dis | head -1) BUILDIR=build -ifeq ($(ENABLE_TESTER),1) - ASMFLAGS += -DENABLE_TESTER - FILES += tester.asm i2c.asm +ifeq ($(CONFIG_ENABLE_TESTER),1) + FILES += tester.asm +endif + +ifneq ($(CONFIG_UART_AS_STDOUT),1) + FILES += video.asm keyboard.asm endif define FIND_ADDRESS = @@ -20,21 +31,15 @@ endef phony: all clean -# First, we need to build all the source files -# Then move and rename the main binary code to $(BUILDIR)/$(BIN) -# Finally, merge the system/os table to it, its address needs to be retrieved dynamically +# We need to build all the source files and then merge the resulting bootloader binary with the system/os table, +# which address needs to be retrieved dynamically all: clean version.txt - $(CC) -O$(BUILDIR) $(ASMFLAGS) -Iinclude/ -m -b $(SRCS) - cp $(BUILDIR)/src/rst_vectors_RST_VECTORS.bin $(BUILDIR)/$(BIN) + $(CC) -O$(BUILDIR) $(ASMFLAGS) -m -b $(SRCS) @# Retrieve the address for the SYS_TABLE. Here is becomes... weird, $(call FIND_ADDRESS) will be replace - @# by the macro itself, we need to interpret it at runtime, so we surround it with $$(...) - @# This gives us a hex value, truncate only accepts decimal values so we surround it with $$((...)) to let - @# bash interpret it to a decimal value - @truncate -s $$((0x$$($(call FIND_ADDRESS)))) $(BUILDIR)/$(BIN) - @# Concatenate the SYS_TABLE - cat $(BUILDIR)/src/*SYS_TABLE.bin >> $(BUILDIR)/$(BIN) - @# Generate the disassembly dump for debugging - $(DISASSEMBLER) -o 0x0000 -x $(BUILDIR)/src/*.map $(BUILDIR)/$(BIN) > $(BUILDIR)/$(DUMP) + @# by the macro itself, we need to interpret it at runtime, so we surround it with $$(...). + ./concat.sh $(BUILDIR)/$(BIN) 0 $(BUILDIR)/src/rst_vectors_RST_VECTORS.bin 0x$$($(call FIND_ADDRESS)) $(BUILDIR)/src/*SYS_TABLE.bin + @# Copy the map file to the `build/` directory + cp $(BUILDIR)/src/*.map $(BUILDIR)/$(MAP) version.txt: @echo `git describe --always --tags` > version.txt diff --git a/README.md b/README.md index 36311c9..6e90c75 100644 --- a/README.md +++ b/README.md @@ -5,30 +5,39 @@ Licence -

A simple bootloader entirely written in Z80 assembly, for Zeal 8-bit Computer, a homebrew 8-bit computer based on a Z80 CPU.

+

A bootloader entirely written in Z80 assembly, for Zeal 8-bit Computer, a homebrew 8-bit computer based on a real Z80 CPU

## About this project -The goal of this project is to provide a small bootloader for Zeal 8-bit Computer that lets us perform several operations directly from the computer itself, such as booting different operating systems, flashing the ROM, testing the hardware, without the need of any external devices. +The goal of this project is to provide a compact bootloader for Zeal 8-bit Computer that lets us perform several operations directly from the computer itself, such as booting different operating systems, flashing the ROM, testing the hardware, etc..., without the need of any external devices. -## Features +## Screenshots

- Zeal Bootloader menu + Zeal Bootloader menu +

Zeal 8-bit Bootloader using the UART
+

+

+ Zeal Bootloader menu +

Zeal 8-bit Bootloader using Zeal 8-bit Video Card

+## Features + As you can see from the menu itself, several feature have been implemented: -* Communication over UART (no need for the video board) -* 5 seconds auto-boot on startup +* Communication over UART or over the video board¹ +* Auto-boot on startup after after a few seconds¹ * Up to 9 operating systems or programs can be set up to boot from the menu. In the example above, only one entry is present: Zeal 8-bit OS. The numbers between the parenthesis represent the physical address (in ROM) and the virtual address respectively. In the output above, Zeal 8-bit OS is flashed at address **0x4000** in the ROM, it will be mapped at virtual address **0x0000** when booted. -* Program can be sent through UART, and booted **directly**. No need to flash the ROM. This is handy for development when testing a program. +* Program can be sent through UART, and booted **directly**. No need to flash the ROM, handy for development and testing programs. * New entries can be added as bootable systems, directly from the bootloader itself. -* Symmetrically, entries can be deleted directly from the bootloader. Note that it is not possible to delete all entries, there must at least be one remaining entry at all time. +* Entries can be deleted directly from the bootloader. Note that it is not possible to delete all entries, there must at least be one remaining entry at all time. * Buffered changes. To prevent mistakes when adding and deleting entries, the changes are first saved to RAM. The changes can then be flushed (**s** option) or discard (reboot the system). -* Baudrate can be changed for the current bootloader session. Handy if there are corrupted data when transferring programs over UART. -* Test Zeal 8-bit Computer hardware. This includes tests for the RAM size, the ROM size, the I2C EEPROM, the I2C RTC, and the PS/2 keyboard. +* Baudrate can be changed for the current bootloader session (not saved after a reboot). +* Test Zeal 8-bit Computer hardware: including RAM size, Flash/ROM size, I2C EEPROM, I2C RTC, and PS/2 keyboard. + +¹: These options are configurable via the `config.asm` file. ## Building the project @@ -43,16 +52,22 @@ At the moment, the project has only been assembled on Linux (Ubuntu 20.04 and 22 To install z88dk, please [check out their Github project](https://github.com/z88dk/z88dk). +### Configuration + +Before building the bootloader, open and modify, if necessary, the configuration file `include/config.asm`. + +In this file, any macro that is not set (commented) or set to 0 will be considered disabled. + ### Building -To build the bootloader, simply use the command: +After the configuration is saved, you can build the bootloader thanks to the command: ``` make ``` -After compiling, the folder `build/` should contain the binary `bootloader.bin` and `bootloader.dump`. The first one is the program itself, whereas the second is a text file containing the disassembled binary with debug symbols. +After compiling, the folder `build/` should contain the binary `bootloader.bin` and `bootloader.map`. The first one is the program itself, whereas the second is a text file containing all the (debug) symbols of the binary. -That dump is only useful when debugging the bootloader (mainly with [Zeal 8-bit Computer emulator](https://github.com/Zeal8bit/Zeal-WebEmulator)). +That map file is only useful when debugging the bootloader, mainly with [Zeal 8-bit Computer emulator](https://github.com/Zeal8bit/Zeal-WebEmulator). ## Transferring data over UART diff --git a/concat.sh b/concat.sh new file mode 100755 index 0000000..4b11023 --- /dev/null +++ b/concat.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +usage() { + echo "usage: $0 output_file address_1 file_1 ... address_n file_n" >&2 + exit 1 +} + +# Check if the number of arguments is odd +if [ $# -lt 3 ] || [ $(($# % 2)) -ne 1 ]; then + usage +fi + +# Create the output file +output_file="$1" +touch "$output_file" +shift + +# Loop through the arguments in pairs: (address, file) +while (( "$#" )); do + addr="$1" + file="$2" + printf "0x%04x - %s\n" "$addr" "$file" + dd if="$file" of="$output_file" bs=1 seek="$(($addr))" status="none" + shift 2 +done diff --git a/img/menu.png b/img/menu.png deleted file mode 100644 index 10065cfc28c45ac988ed4682f70885a6ca8fcccd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25354 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 diff --git a/img/menu_uart.png b/img/menu_uart.png new file mode 100644 index 0000000000000000000000000000000000000000..b0929f98ca238de093a65a6fe9db49b6b6bf024e GIT binary patch literal 39017 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�jKT3YIXFPGJ_Sd# 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 literal 0 HcmV?d00001 diff --git a/img/menu_video.png b/img/menu_video.png new file mode 100644 index 0000000000000000000000000000000000000000..731f9c1d8b5f0ee6c2908070f7e45c911f4d9a2e GIT binary patch literal 11945 zcmeHt=U$=vp*1GTg zedj5yYJ9eo2x?{)AiQOu|lcw5>wZNa9F_&Gw+fm)4x42`+u^p~I zoV%L1b7j0YUcWI@|2paq2o)#a5`553_$!)}eEe58Q+l`gh8~vo&9ykPL==XKPGl*Y;hGE81$H8D=^Z3-R6g%l&hClKpD*CoZDCQO=dJ_k0Rfvg- zhl<6tu!=0m*XT*HLI6{UztBd<_58>)GHCl!{qxPEs_$%tR}7N$q|^1>_ zcSgO*EmD}cORmE9D?Xdw+EustmEJ92l5~~n3(|D6t|#v<4O!J1YD%(4csCQWeZ=z5 z7{LzW>PVdCENT8{gol22cOwhbp|`)?d>pDxN@A!WKT z^eo#J`a0W{mTPH66G+S$7+>5)JB^AN7*(U`M(1pdp5e|CnAuzteG#94bihqi$j$My zBDH+?myqua9dF@P?Lf4C)~)^PYvyD~7;(e`Av9-cAO9Y(EklDdG+zi3nc(Tjh-Nz| z$b3O}l^obrJ-O*ez|vcmX$j?*TGFMfPwAO(n_AkVn&+MS<{uwF+E$@QCj^aPy+EAJ z;jrzYG4}&rLxmhKq*IwO;5A^&moR*ka4Z*jLh$Ml%WvbaXRroofwg0LITuMQIaWXY z%Cz0C?fPa5WB3=%+Fmd8fiXrYyup$ckJv0 zTt&Je4#l5Hs=c${36|HPBsvA6|2++El52@(QQGNv1Rx~;jc z^Q3x{CvEyL?P`wMAvKU+G`RM8{er~H%EpdbytFx7mY}NxjD1Q4MX%cIj`ZP&hU_V} zbPd_HC`b(?1&KtQo2a6dVDi1W6^hOHTDRmRIqA9f`l89=7=2odo2OT-H|}Yw@%3+y z5-#42M#p&R&YbrwuCHHiIC@_l0L%du)BrHDf6YvH2aJLSS$c%*@~rzwV5ZTqUoivo zXE;v~br&?E+M_aq>8M$14Y{m#rdC1b%IID4-6_)vOvC6q8G!4zexE`XdT-6WInh{w ztUac2g?TPiO^L~~6SxBlg_gWRPsEj^Q??_eHEEaNp8zetmO@tSqlo{ab-?+<=Gpps z7AC-=fWZtcKGX+J6_$Yp^|END<(!FY$6dG4D8HRT*3dD;i|r9^EK9~d1r+M?wd*#5 zeN`pSemwb1`C>V|Mq^)DO((aygw(MD zP@G%*xv8u1k9~H2BEH+7zprA((1LZ10dwRZ_%|i)N&!Ai_+R~$i8;0ThvL1zVh9mX z^P@udR#b}+Wq8s_=vF_?IbXU-7nCM`OJA#?Hyg}1T+D>}&S)A9FZbQN8nBlL0GGfJ zpokK`Sz=F87nf$A6UQe>0|qFk*=GB7Hsvafw$c~>*_6yKjbO;hExKU~t8$Q^70h?K z%pO9I55h~VomLUBm?DrVt{WqhWKXF+^&=!NRWQ6l22B%(gZjhro=Gc%6?H}->hIJI z=McFWyago1yj>majh|Kb#%t<~&C@utpsszJrphUu{xhY18$_Z|pOrM~LrBwS@rUE* zv5*e}F@L&H@|j~Oom%C>JXGqt%kyRvngXUpXuAkBbtPM5TjG+Bw@p)C7rR~ z;@oL_`K&GUn~eNdy#p1OPNz#NE>$S%rP!SbZpdDUXx-ww!aU z*64iqd(6t(GWtlfz<}6=hGCKE>IQczB_};HtOvx^Fy7ZJ4Qz;~_c*7pJR;XE)lEOm zE2rEbz9>~12(z&{YUHnR#0k&7O{f+J+>HRV!DO` z;?I)uw{N5jU#q@p;?;J87Ljzy%6K)cn}-p;6#Uk zxFZS3)pk@1DqindhJHfU+oby-`|9z|%tZ+}Sh(~&ueE;P`$xlN4yMh=qRk!Ob9P_u zv*W!GS4PP1RE-59kIr9`3uLN zY;ZL5csUXZ7=U*Bx0SG?YKKi5ZrI;+o+U%W-i=suI^O#C@yv%$$&x`mrGkYI?fMDS zSMi3}yQFxxki3aGCp*-gbWb6jue!L!n()`ZTr>gK@+qExquy1Bz|F}jieJx0cC(iM z72MR=g&B+K4Z#@RlEb=%v&8NX?hAI7nZJ)%!kz1IIHd2axGD^JX?X^4j`hQ` z=28F~#Y-kW24FGQW~!O>VcWVCmEFGQPQe^%45( zx~#=P*KKL_`R4bkSkahu5^~}yYZvXcQKLhw4ao7h=_gedFp)?Ftm@)En>8^UWDLc7 zl*X7*;)*DDM-<;J!{A?9uGxf@T# zG=s_I+p(r+CuUV>Qcj*zX@Hm= z?tBagPrr%8Ed4B(7gy1y_VEVv0ZJD0RhHpBF6>#!w7CJkBvi8*Q|f$26Usavns;m* zdoFi{*O1k8Oys@GsOR?jVpc5U0FH6eAQ~FrKC7u;ITbOE+J{RAb-jV(ug3d~#PhG^ zKUaoCnj1TY_ZuJbqeI5*HfZ4R(|B4zVITJR_kgs#o-+L-!VPWshh<}$^yw&%V1QV6 zVyTw9-fkHfe9|;G_t-!FKvV#{k8g1)*=y01o>lZ`> zt8<7hs_EC0ia60OK#Z23{xq;%{t&xu`9MA+7jm1Ds$+I-G$b#urJKmNEAzj|&+mwg z%jOgSK(S}N@*FaY-$}aw969^=nk1ZK7>ndh#@Hd-jVkA+V{XN|&HX%TSW6s6Wjo&668?vy4yL()dC#cn0?M6Hk!t?IA?_mF=8U_0m4{A z?VWyI`nKZ7e`no;S44hv=~0xC4Pku0@#oXHv%Y{J-Kj!BKWdJrFvyUP=sDg`ID;U6 za|~;M|7Jcph~PpozOPI^Tr?gw-=uV*-}Z4mc}Cck$4dFLCfdV+4!z&nT>$vaRsilX z#Z-^hA?nN!UlP(ZiAZ_guJvQUhu2i5Ex1SIZy8&uShEXqUsY&5$6>5sTst|NZGlOi z<0gH?%PnTNRCgl*^!dvgG#d1=p8aZy>qVm`ZT1}q0bFco)v|B%GlOm;0N#PRn`FB| z^kBV>xQgm~w@kbtRQ%x>_!mgarA9HEV}`M6&f&UyFeDZsKG`+Fm$R|}7sj=r@Lkof z51TF^wLZ`A%0bbrl9ZB&T-#c9vWmMNXo+}i79|+d(I5NeVKK<9C~$*r)#k1!PQJzt zxu-LR5BhCm6+Q{(yw;!3(I5BjZc4s_59C5}jw%(hqecs7Ng^`ftKlB&$`TtXGrv0v zBAx{LL7?J7>cej$&4>DOU+ouxna6HCVcWe*V`=!CUpngQaQ8V_^Fn(;dcX zflrQEu45rAo+!H{EL}Y^vb0)ET2?%4DxFr85C3uUVo~64W)Si{Y>nm>yjyDVQsQpC zWnyNDXNRgXWBEb_B3Y7gD9Qa%YgpKZg<)V-c` zA2HzWep)|+4%03>`-%U)KW4d3Ru70MstrEiKM2SjkH^(+Y}zh#*k*SWAE5(DqP4ArORZ)^2s4= zS=Z>UGSeJEAhXbnpNqHcU3(sIk zeahOG&9|mhp+_I3TI&&nOwg&Ehe1OLPOD|swgnHT2+R!Kwd>?~y^E39SDtaE(W!Qf zG1@TxmlNDNxOuos_bbIKQ~hmM(@~YU5Aihc-`vvS5a)`o`{KH&K^^<_npZrBHuP~N z*m5Zt8jf>!=1<=K?UBAHj4L@XAA`y1S%35DqG#M#$Wv}2`{(X}fmo62OnB<4?WQHN{fQVd^Snh3+!sb9uNhTohO*=x8w2HR;M%tyzKrHv9}Bew7S)$zJ% z__<8Phi(LWmC0Lr>;)gzuNIC7YykfOpc$sI?#|>TBvx?ai@AYQn#!kZ^Sfu77iGD< zn7I_G6?@W2(RF3??k6wac3z0bg!bm0ESF(S^k9FnVySCj=q(Y<7$9?#Sr!b!mS7Ig zT|-)CE4_YDWg2{l8iiU{Bs3JAX;T!6xeAn9d>izwT_MX2B{#ObQEaL%2?TP5mctk_ zwb1u#r9{#G{7@9tdTXcln)HA)lZ(M)mM{-^Cerw<@!BxRKQ_c#hNDB~0Iphm14%FW z;?<3)ZyJ)n45jf7u1Y{X74>y81*LFxgU;y|JgUR>hrs*(T0)xDo<3K1?P4a^3zVG- zI#c?wQJYP#_H0Rg_1tj28`bwENHqJj(Q-7bS-~=^&A*VLn!itJrK9Q(bNdgi?($cg zjv6*{QXzW7yf%#Lu4^5u3%bF(hR?4w1RD;#+{OB|f)eUa85$mCZ+|}{l zr%&uxE*hcEv24gKifY+;%FQbBuC<#_YDQU8g`wC0d`~ov7#Nt0;=|2l&9`WTK!%>~ zYByritH>Mi;UdRrpXM0s`O{sb8~&Ph-x#ezX@W9~x# z;;vAy`+^hdHU9Sz=DdLjKJzPG%wXuv{lrP$4xm&}!6@c>-Mcbq)`IWObn2||M5`Gw zsq4CcF_eXcm#}|eI3>weN-{pSR;4N_WY#<6QI&81!hva3Wet8R&Hhl(1&n<#H)rlR2-%*&=<+fOmmlV~efPPOjU;7hs2O;n~FYWKs z!JHenchd%xlvH&4t?aulLBP|dp@4xF3xzlx&wZ&g`$dzu(B5am@*k)P%)}wmcnm&Z zgZ@4F<$#&z&u3B~us~T0s@HSeebmk5+F*DhAgh*juPmKv7A;Dqi3_rLpN}EXXm0Uc zyK_Adt)wmpbNyOYsVO0ln% z;^N!It-K@6m6KjFFE9w&mUu2q*f4ZH^nof=tz_i|!|EJl#9=aTDU}rqpwSu-9o!I$ zm~6(#$@iJO&Eu=jbmL`Si0Y?@N*x~<_t^?K6 zp@)f(71=83$0^kkF`)F4Jnjt4FRl6GO{4xn^8zOA;}trV1_Ff~YEHp{SjAiQ;vMKx zhJDvS{PE>9lSX2bd1t2A=lB_sH&CTGJJGHps~zDH>K2Qm-a*x-NiTD&+ycJ#w;D9S zF9SRVNygS#BgU-h0fm6gHdtHN(3C}R;mW?s0_(0fpkWJ97y5nmVj>q}Q({k5C6F^{ zn|T~k(}XL-3y%Re5By8iyViW3_*3*1p!`F9ba z?k0-R4;wcSF~u1hP^s`BsS|~cbBqKPo#IRxi@;~*UwD$!z`^rSq%EQ=fq##h_)k>A zVF2x0rY~7G76ZJfAOE2$H%XC))l6d+?dyO(LF+}eK}7fH%AYR^NN%kVyk$xMzy8Ao zzqW>}@(b68{vw=Q96L9c{iYA!}EC)K(4dJNnCFrH|h4;Vxbi667)ayrt`Wuf@e~_Y0w#TjNspq zlA!iZx;}ew9D{-`fAcQF6ZDYHvD%Y7%%iT5zi0A>X*0V#Mp66BfP$1?+vZklX^xWR zqxQe=Tz_Bymfavx+j1;yi|0BnDvi?4Mmp^_}lzHX?Gx~w8 zHv$dz&h6w_F zoE-Fe3c0$MHUb{~SM#k>hkX0dxW^{6z)F8xSL~#heAp*bc}!@XL);JSxd7`1E+iB& z9IaK8iONj?w&RmGadRopkPaB3KGaj+ZYA~YbYb{GgS5&eriZD=hip-_rE5`ORku~! zdv;V%j&d-$Z(z&!Fzg*Rrc1bHZV(2}+1LvzezA;D4rEYmkH0Zst1M{Bht-N^p{D*; zH}BMav7QM>x>cKkY)6i4Z!bz^`H?t|+bxyTn8vibPqy|9!i||!cJZ3%+!AdsDw}cm z@*0AoFsLfe_GB*LYodFFBmEp-PJYCRC>Vw30}XpGISTB?lUMSz*{ln-(qt6Xx^mo2ZX746|u$S zxhGWImY0lYT;h5i#30roP*J%|3=Zl;cps4P&)U`A$SeqUxK{i0Fr^^e+xS<*4Av};B-W=<3JLch#2P%;EY9@5}iareV-4OCdiycff*nxB8&~FHQAMF<% zbR}C1JH`U)cmSy>N+T_6Zg(6mr0~*@P;)u!tYUw~+KM7G2F+O+D;vi>VXBrV33}gU z_Cx3i-PWtYkj)MpqeyIiXxy8+pIe)sY`}4M4i$6g4I#u!SuM&PUkM~xV!63K5;-sN za7f-WcFzsA%SaX}{)FKbpV2li_tU_c=lfSB=M`8F5a;^M7L3NtpS6XuGq z1rhlGWtWK^&Zb)&!rNu&b&5J-d-}ZuJ!ST7Uigh)j~``>0>vMKo4QgjKvZNk3!4~o zG|szwbBi=n-@A|>luEF>uFJ6n0c;s%wuyRqQ`qrjF(_xG%r+JZum)kT)U#%G}{%>Wq2w@g6EUsp3)~&?re! z99$4xq!B;5GB)a#f>UmNOVsB6@3Hp(o&MJDRv`J6oEhFt~on{$i$I)V#qQ>gf#GJl`%Y!yOwU>ZWB9 z;IP@g7OAnv%MKeiEB<1<=DUh%2w__H=k-M`{jvx^0i)Pvw#@I8taAKiz*pj;KD&vw5oa{$+^P?5)0i%CP) zU{h#D@lr9u$()$($uF5(GWP5c7JzCY4>W;2l(s@Cj?M~OJ>6l4E2aDmi`*YdokO(e z(+2E2n`4OXZN-(RDY9Nv&*_|pTj0^XxM;1czIfB9}BOI+6C{c=DM6OBCQra=a3bw_!HkMyIy~`CI^{@ zWrK6~)&Z5}%Zm(4>baol=6wW z&rS3DIai)8N=O(RxN89rt$~PwJCG<}GOTY$=sMp%(Ph-Gr*Q~|Jq>;%xp26VwGHF=6iKK!$0!l0Lx{DrvNYrO1%j@hC5Ij~<<^-`80J1>iU zRCu)~$t~1-o8~YLXRA|GEin_l;+u)tuz7k!YGF*g`;@9-!!gloi!4TO?K15uR`%Sq zeVaNrKU0$&OcGE3nS?1>naSF6@bVrvyz^VtB{8k^nQ>^UH()O%(I`Fc}#ud$tp5C&Cf`t6_zN))V#UJhG z_1bbHE6ykn9C_gx@jf=H0DPi(aV@PBEPQF``?}VNQ`;eAkbYO9VmDGZg?AsrABaC8 zKIzG~o!?9_6i%)ZA{1Lk_W3Kj0TTyQlG;TBqx82Fw5%zq9akRrS7kRx_T&mAhwBSF z@NkO?bQaCn=c1xH7`zjBI+{MRKo85oP0+8~Zf8>5-Rl<3o-g?peLVDmmHcW?1*x*W znf>BuJiaHPs9>Y{zV!x@O#qin9t!n3eFW&X*n`nIl*5%^>3pHac8|4{V*1$e8D%HV z!q|5CH+bGu^iEE@^bK!VnOT;E%{%ryah46Aq`o>W2F5{a(kf7S^6<$`Kt%<{zF$k) zr7SfZTl3VY%#DlhwjC2=aYOX4&)37F8Q8>ipi)@@wBE>7A1YOg1MDvFUWC()RODRlfv20{ZLT`Qg zZ~s*do5G18{JLS=QY{8+1C-AJmp-W?wUE;7Cj6p~ue!m^#}%J3kONB%Q9X~Jp|fUs!wNCioq-(6OgKPkeEsjb4_sBUhm#0uEpEiTBvw<{ z<0q7g{_{<16;w!ki$Ne`%xSp!KIDTUH2lh^sWa5qtC+mxhoG2^M21B(mF69{>)+RU z%2k#RIWeM>W=C%SNxEp}_qLC8XuZ6xmmP`SOUnmw;}5)KZK|+2&`8`oVFS j+yBgo(7&}RcH#HO3=x@&UjTQ{cewuO@k8}@fvNum>775! literal 0 HcmV?d00001 diff --git a/include/config.asm b/include/config.asm new file mode 100644 index 0000000..a25ab1b --- /dev/null +++ b/include/config.asm @@ -0,0 +1,25 @@ +; SPDX-FileCopyrightText: 2024 Zeal 8-bit Computer +; +; SPDX-License-Identifier: Apache-2.0 + + IFNDEF CONFIG_H + DEFINE CONFIG_H + +; When set, the UART will be used as the standard output. When not set or set to 0, the video driver will +; be used. The UART driver will still be used to receive files. +DEFC CONFIG_UART_AS_STDOUT = 1 + +; When set, the Zeal logo will be shown on screen, this takes more RAM at runtime but won't affect the +; OS or program running after the bootloader as the modified tiles are saved and restored. +DEFC CONFIG_VIDEO_SHOW_LOGO = 1 + +; When set, the hardware tester will be available in the menu. +DEFC CONFIG_ENABLE_TESTER = 1 + +; When set, the user will be asked to press a key before returnig to the menu +DEFC CONFIG_ACK_CONTINUE = 1 + +; Number of secodns to wait before autoboot +DEFC CONFIG_AUTOBOOT_DELAY_SECONDS = 3 + + ENDIF ; CONFIG_H \ No newline at end of file diff --git a/include/keyboard_h.asm b/include/keyboard_h.asm new file mode 100644 index 0000000..5543244 --- /dev/null +++ b/include/keyboard_h.asm @@ -0,0 +1,30 @@ +; SPDX-FileCopyrightText: 2024 Zeal 8-bit Computer +; +; SPDX-License-Identifier: Apache-2.0 + + IFNDEF KEYBOARD_H + DEFINE KEYBOARD_H + + EXTERN keyboard_get_char + EXTERN keyboard_next_char + EXTERN keyboard_has_char + EXTERN keyboard_set_synchronous + + DEFC KB_IO_ADDRESS = 0xe8 + + DEFC KB_PRINTABLE_CNT = 0x60 + DEFC KB_SPECIAL_START = 0x66 ; Between 0x60 and 0x66, nothing special + DEFC KB_EXTENDED_SCAN = 0xe0 ; Extended characters such as keypad or arrows + DEFC KB_RELEASE_SCAN = 0xf0 + DEFC KB_RIGHT_ALT_SCAN = 0x11 + DEFC KB_RIGHT_CTRL_SCAN = 0x14 + DEFC KB_LEFT_SUPER_SCAN = 0x1f + DEFC KB_NUMPAD_DIV_SCAN = 0x4a + DEFC KB_NUMPAD_RET_SCAN = 0x5a + DEFC KB_PRT_SCREEN_SCAN = 0x12 ; When Print Screen is received, the scan is 0xE0 0x12 + DEFC KB_MAPPED_EXT_SCANS = 0x69 ; Extended characters which scan code is 0xE0 0x69 and above + ; are treated with a mapped array + + DEFC KB_ESC = 0x80 + + ENDIF diff --git a/include/pio_h.asm b/include/pio_h.asm index 4b78178..564e049 100644 --- a/include/pio_h.asm +++ b/include/pio_h.asm @@ -12,7 +12,7 @@ DEFC IO_PIO_DATA_B = 0xd1 DEFC IO_PIO_CTRL_A = 0xd2 DEFC IO_PIO_CTRL_B = 0xd3 - + ; PIO Modes DEFC IO_PIO_MODE0 = 0x0f DEFC IO_PIO_MODE1 = 0x4f @@ -70,7 +70,4 @@ ; * Provide a mask DEFC IO_PIO_SYSTEM_INT_CTRL = 0x90 | IO_PIO_CTRLW_INT - ; NOTE: 0 means monitored! - DEFC IO_PIO_SYSTEM_INT_MASK = ~(1 << IO_UART_RX_PIN) & 0xff - ENDIF diff --git a/include/stdout_h.asm b/include/stdout_h.asm new file mode 100644 index 0000000..07cb928 --- /dev/null +++ b/include/stdout_h.asm @@ -0,0 +1,83 @@ +; SPDX-FileCopyrightText: 2024 Zeal 8-bit Computer +; +; SPDX-License-Identifier: Apache-2.0 + + INCLUDE "config.asm" + + IFNDEF STDOUT_H + DEFINE STDOUT_H + + ; It is possible to choose either the UART or video board as the standard output + IF CONFIG_UART_AS_STDOUT + INCLUDE "uart_h.asm" + + DEFC stdout_initialize = uart_initialize + DEFC stdout_autoboot = uart_autoboot + DEFC stdout_write = uart_send_bytes + DEFC stdout_put_char = uart_send_one_byte + DEFC stdout_newline = uart_newline + DEFC stdin_get_char = uart_receive_one_byte + DEFC stdin_has_char = uart_available_read + DEFC stdin_set_synchronous = uart_disable_fifo + DEFC stdout_prepare_menu = uart_clear_screen + + MACRO YELLOW_COLOR _ + DEFM 0x1b, "[1;33m" + ENDM + MACRO GREEN_COLOR _ + DEFM 0x1b, "[1;32m" + ENDM + MACRO RED_COLOR _ + DEFM 0x1b, "[1;31m" + ENDM + + MACRO END_COLOR _ + DEFM 0x1b, "[0m" + ENDM + + ELSE ; !CONFIG_UART_AS_STDOUT + + INCLUDE "video_h.asm" + INCLUDE "keyboard_h.asm" + + DEFC stdout_initialize = video_initialize + DEFC stdout_autoboot = video_autoboot + DEFC stdout_write = video_write + DEFC stdout_put_char = video_put_char + DEFC stdout_newline = video_newline + DEFC stdin_get_char = keyboard_next_char + DEFC stdin_has_char = keyboard_has_char + DEFC stdin_set_synchronous = keyboard_set_synchronous + DEFC stdout_prepare_menu = video_clear_screen + + ; Use a prefix for the colors + MACRO YELLOW_COLOR _ + DEFM 0xFE + ENDM + MACRO GREEN_COLOR _ + DEFM 0xF2 + ENDM + MACRO RED_COLOR _ + DEFM 0xF4 + ENDM + + MACRO END_COLOR _ + DEFM 0xff + ENDM + + ENDIF + + + MACRO PRINT_STR label + ld hl, label + ld bc, label ## _end - label + call stdout_write + ENDM + + MACRO PRINT_STR_UART label + ld hl, label + ld bc, label ## _end - label + call uart_send_bytes + ENDM + + ENDIF \ No newline at end of file diff --git a/include/uart_h.asm b/include/uart_h.asm index 51cf4df..3bf6f09 100644 --- a/include/uart_h.asm +++ b/include/uart_h.asm @@ -15,14 +15,10 @@ ; Default baudrate for UART DEFC UART_BAUDRATE_DEFAULT = UART_BAUDRATE_57600 - MACRO PRINT_STR label - ld hl, label - ld bc, label ## _end - label - call uart_send_bytes - ENDM - ; Public routines EXTERN uart_initialize + EXTERN uart_autoboot + EXTERN uart_clear_screen EXTERN uart_send_bytes EXTERN uart_send_one_byte EXTERN uart_available_read @@ -31,6 +27,6 @@ EXTERN uart_receive_one_byte EXTERN uart_receive_big_file EXTERN uart_set_baudrate - EXTERN newline + EXTERN uart_newline ENDIF diff --git a/include/video_h.asm b/include/video_h.asm index 35b7ca1..388eb1f 100644 --- a/include/video_h.asm +++ b/include/video_h.asm @@ -1,46 +1,138 @@ -; SPDX-FileCopyrightText: 2022 Zeal 8-bit Computer +; SPDX-FileCopyrightText: 2024 Zeal 8-bit Computer ; ; SPDX-License-Identifier: Apache-2.0 +; This file has been taken from Zeal 8-bit OS source code. + IFNDEF VIDEO_H DEFINE VIDEO_H - ; Screen macros - DEFC SCREEN_SCROLL_ENABLED = 0x1 + EXTERN video_initialize + EXTERN video_autoboot + EXTERN video_write + EXTERN video_put_char + EXTERN video_newline + EXTERN video_clear_screen + + ; Screen flags bit (maximum 8) + DEFC SCREEN_SCROLL_ENABLED = 0 + DEFC SCREEN_CURSOR_VISIBLE = 1 + DEFC SCREEN_TEXT_640 = 2 + DEFC SCREEN_TEXT_320 = 3 + DEFC SCREEN_TILE_640 = 4 + DEFC SCREEN_TILE_320 = 5 + + ; Flag helpers + DEFC SCREEN_TEXT_MODE_MASK = (1 << SCREEN_TEXT_640) | (1 << SCREEN_TEXT_320) ; Colors used by default DEFC DEFAULT_CHARS_COLOR = 0x0f ; Black background, white foreground DEFC DEFAULT_CHARS_COLOR_INV = 0xf0 - - ; Physical address of the FPGA video - DEFC IO_VIDEO_PHYS_ADDR_START = 0x100000 - DEFC IO_VIDEO_PHYS_ADDR_TEXT = IO_VIDEO_PHYS_ADDR_START - DEFC IO_VIDEO_PHYS_ADDR_COLORS = IO_VIDEO_PHYS_ADDR_TEXT + 0x2000 - - ; Macros for video chip I/O registers and memory mapping - DEFC IO_VIDEO_SET_CHAR = 0x80 - DEFC IO_VIDEO_SET_MODE = 0x83 - DEFC IO_VIDEO_SCROLL_Y = 0x85 - DEFC IO_VIDEO_SET_COLOR = 0x86 - - ; Video modes - DEFC TEXT_MODE_640 = 1; - DEFC TILE_MODE_640 = 3; + + ; -------------------------------------------------------------------------- ; + ; Hardware mapping related ; + ; -------------------------------------------------------------------------- ; + + ; Physical address for the memory components. + ; It is also possible to access the I/O components via the memory bus, but for the + ; sake of simplicity, we don't do it here. + DEFC VID_MEM_PHYS_ADDR_START = 0x100000 + DEFC VID_MEM_LAYER0_ADDR = VID_MEM_PHYS_ADDR_START + DEFC VID_MEM_PALETTE_ADDR = VID_MEM_PHYS_ADDR_START + 0xE00 + DEFC VID_MEM_LAYER1_ADDR = VID_MEM_PHYS_ADDR_START + 0x1000 + DEFC VID_MEM_SPRITE_ADDR = VID_MEM_PHYS_ADDR_START + 0x2800 + DEFC VID_MEM_FONT_ADDR = VID_MEM_PHYS_ADDR_START + 0x3000 + DEFC VID_MEM_TILESET_ADDR = VID_MEM_PHYS_ADDR_START + 0x10000 + + + ; Physical address for the I/O components. + ; The video mapper is responsible for mapping the I/O component in the I/O bank + ; starting at address 0xA0, up to 0xAF (16 registers) + ; It also contains the current version of the video chip. + DEFC VID_IO_MAPPER = 0x80 + DEFC IO_MAPPER_REV = VID_IO_MAPPER + 0x0 + DEFC IO_MAPPER_MIN = VID_IO_MAPPER + 0x1 + DEFC IO_MAPPER_MAJ = VID_IO_MAPPER + 0x2 + ; Reserved = 0x3 <-> 0xD + DEFC IO_MAPPER_BANK = VID_IO_MAPPER + 0xE ; I/O device bank, accessible in 0xA0 + DEFC IO_MAPPER_PHYS = VID_IO_MAPPER + 0xF ; Physical address start of the video chip + + + ; The video control and status module is non-banked, so it is available at anytime for reads + ; and writes. It is reponsible for the screen control (mode, enable, scrolling X and Y, etc...) + ; and the screen status (current raster position, v-blank and h-blank, etc...) + DEFC VID_IO_CTRL_STAT = 0x90 + ; 16-bit values representing the current raster position (RO). Values latched when LSB read. + DEFC IO_STAT_VPOS_LOW = VID_IO_CTRL_STAT + 0x0 ; 16-bit value flushed when read + DEFC IO_STAT_VPOS_HIGH = VID_IO_CTRL_STAT + 0x1 + DEFC IO_STAT_HPOS_LOW = VID_IO_CTRL_STAT + 0x2 ; 16-bit value flushed when read + DEFC IO_STAT_HPOS_HIGH = VID_IO_CTRL_STAT + 0x3 + ; 16-bit Y scrolling value for Layer0, in GFX mode (R/W). Value latched when MSB written. + DEFC IO_CTRL_L0_SCR_Y_LOW = VID_IO_CTRL_STAT + 0x4 + DEFC IO_CTRL_L0_SCR_Y_HIGH = VID_IO_CTRL_STAT + 0x5 + ; 16-bit X scrolling value for Layer0, in GFX mode (R/W). Value latched when MSB written. + DEFC IO_CTRL_L0_SCR_X_LOW = VID_IO_CTRL_STAT + 0x6 + DEFC IO_CTRL_L0_SCR_X_HIGH = VID_IO_CTRL_STAT + 0x7 + ; Similarly for Layer1 (R/W) + DEFC IO_CTRL_L1_SCR_Y_LOW = VID_IO_CTRL_STAT + 0x8 + DEFC IO_CTRL_L1_SCR_Y_HIGH = VID_IO_CTRL_STAT + 0x9 + DEFC IO_CTRL_L1_SCR_X_LOW = VID_IO_CTRL_STAT + 0xa + DEFC IO_CTRL_L1_SCR_X_HIGH = VID_IO_CTRL_STAT + 0xb + ; Video mode register (R/W). Only takes effect after a V-blank occurs. + DEFC IO_CTRL_VID_MODE = VID_IO_CTRL_STAT + 0xc + ; Video mode status + ; Bit 0 - Set when in H-blank (RO) + ; Bit 1 - Set when in V-blank (RO) + ; Bit 2:6 - Reserved + ; Bit 7 - Set to enable screen. Black screen when unset. (R/W) + DEFC IO_CTRL_STATUS_REG = VID_IO_CTRL_STAT + 0xd + + + ; I/O modules that can be banked will appear at address 0xA0 on the I/O bus. + DEFC VID_IO_BANKED_ADDR = 0xA0 + + ; Text control module, usable in text mode (640x480 or 320x240) + DEFC BANK_IO_TEXT_NUM = 0; + + DEFC IO_TEXT_PRINT_CHAR = VID_IO_BANKED_ADDR + 0x0 + DEFC IO_TEXT_CURS_Y = VID_IO_BANKED_ADDR + 0x1 ; Cursor Y position (in characters count) + DEFC IO_TEXT_CURS_X = VID_IO_BANKED_ADDR + 0x2 ; Cursor X position (in characters count) + DEFC IO_TEXT_SCROLL_Y = VID_IO_BANKED_ADDR + 0x3 ; Scroll Y + DEFC IO_TEXT_SCROLL_X = VID_IO_BANKED_ADDR + 0x4 ; Scroll X + DEFC IO_TEXT_COLOR = VID_IO_BANKED_ADDR + 0x5 ; Current character color + DEFC IO_TEXT_CURS_TIME = VID_IO_BANKED_ADDR + 0x6 ; Blink time, in frames, for the cursor + DEFC IO_TEXT_CURS_CHAR = VID_IO_BANKED_ADDR + 0x7 ; Blink time, in frames, for the cursor + DEFC IO_TEXT_CURS_COLOR = VID_IO_BANKED_ADDR + 0x8 ; Blink time, in frames, for the cursor + ; Control register, check the flags below to see what can be achieved + DEFC IO_TEXT_CTRL_REG = VID_IO_BANKED_ADDR + 0x9 + DEFC IO_TEXT_SAVE_CURSOR_BIT = 7 ; Save the current cursor position (single save only) + DEFC IO_TEXT_RESTORE_CURSOR_BIT = 6 ; Restore the previously saved position + DEFC IO_TEXT_AUTO_SCROLL_X_BIT = 5 + DEFC IO_TEXT_AUTO_SCROLL_Y_BIT = 4 + ; When the cursor is about to wrap to the next line (maximum amount of characters sent + ; to the screen), this flag can wait for the next character to come before resetting + ; the cursor X position to 0 and potentially scroll the whole screen. + ; Useful to implement an eat-newline fix. + DEFC IO_TEXT_WAIT_ON_WRAP_BIT = 3 + ; On READ, tells if the previous PRINT_CHAR (or NEWLINE) triggered a scroll in Y + ; On WRITE, makes the cursor go to the next line + DEFC IO_TEXT_SCROLL_Y_OCCURRED = 0 + DEFC IO_TEXT_CURSOR_NEXTLINE = 0 + + + ; Video modes that can be given to IO_CTRL_VID_MODE register + DEFC VID_MODE_TEXT_640 = 0; + DEFC VID_MODE_TEXT_320 = 1; + DEFC VID_MODE_GFX_640_8BIT = 4; + DEFC VID_MODE_GFX_320_8BIT = 5; + DEFC VID_MODE_GFX_640_4BIT = 6; + DEFC VID_MODE_GFX_320_4BIT = 7; ; Macros for text-mode - DEFC TEXT_MODE_640_480 = 1 - DEFC IO_VIDEO_640480_WIDTH = 640 - DEFC IO_VIDEO_640480_HEIGHT = 480 - DEFC IO_VIDEO_640480_X_MAX = 80 - DEFC IO_VIDEO_640480_Y_MAX = 40 - - DEFC IO_VIDEO_X_MAX = IO_VIDEO_640480_X_MAX - DEFC IO_VIDEO_Y_MAX = IO_VIDEO_640480_Y_MAX - DEFC IO_VIDEO_WIDTH = IO_VIDEO_640480_WIDTH - DEFC IO_VIDEO_HEIGHT = IO_VIDEO_640480_HEIGHT - - DEFC IO_VIDEO_MAX_CHAR = IO_VIDEO_X_MAX * IO_VIDEO_Y_MAX - DEFC BIG_SPRITES_PER_LINE = IO_VIDEO_WIDTH / 16 - DEFC BIG_SPRITES_PER_COL = IO_VIDEO_HEIGHT / 16 + DEFC VID_640480_WIDTH = 640 + DEFC VID_640480_HEIGHT = 480 + DEFC VID_640480_X_MAX = 80 + DEFC VID_640480_Y_MAX = 40 + DEFC VID_640480_TOTAL = VID_640480_X_MAX * VID_640480_Y_MAX ENDIF diff --git a/src/boot.asm b/src/boot.asm index 5cfd3c9..4e59857 100644 --- a/src/boot.asm +++ b/src/boot.asm @@ -1,9 +1,12 @@ -; SPDX-FileCopyrightText: 2022 Zeal 8-bit Computer +; SPDX-FileCopyrightText: 2022-2024 Zeal 8-bit Computer ; ; SPDX-License-Identifier: Apache-2.0 + + INCLUDE "config.asm" INCLUDE "mmu_h.asm" INCLUDE "video_h.asm" INCLUDE "sys_table_h.asm" + INCLUDE "stdout_h.asm" INCLUDE "uart_h.asm" SECTION BOOTLOADER @@ -11,11 +14,9 @@ EXTERN __BSS_head EXTERN __BSS_tail EXTERN video_initialize + EXTERN pio_initialize EXTERN bootloader_menu - DEFC AUTOBOOT_DELAY_SECONDS = 5 - DEFC CPU_FREQ = 10000000 - PUBLIC bootloader_entry bootloader_entry: ; Map the first 48KB of ROM @@ -35,101 +36,26 @@ bootloader_entry: ldir call sys_table_init + call stdout_initialize + ; UART will be used to receive files in all cases call uart_initialize + call pio_initialize ; Ready to send and receive data over UART, show welcome message ld hl, start_message ld bc, start_message_end - start_message - call uart_send_bytes + call stdout_write ; Wait for a user input, on return, if A is 0, autoboot shall be performed ; else, enter the menu - call autoboot + call stdout_autoboot or a jp nz, bootloader_menu ; Boot the first system of the entry table - call newline + call stdout_newline call sys_table_get_first jp bootloader_print_boot_entry - ; Routine showing the autoboot message and waiting for a keypress. - ; Returns: - ; A - 0 if autoboot, 1 if key pressed - DEFC MOVE_BACKWARD_COLS = seconds_message_end - seconds_message + 1 -autoboot: - ; Prepare the escape sequence for moving cursor backward: - ; ESC[#D where # is the number of columns to move backward - ld hl, escape - ld (hl), 0x1b ; ESC - inc hl - ld (hl), '[' - inc hl - ; Size of " ...seconds" message + 1, in ASCII - ld (hl), MOVE_BACKWARD_COLS / 10 + '0' - inc hl - ld (hl), MOVE_BACKWARD_COLS % 10 + '0' - inc hl - ld (hl), 'D' - ; Print the autoboot message - ld hl, boot_message - ld bc, boot_message_end - boot_message - call uart_send_bytes - ; Loop until E (not altered by uart_send_bytes) is 0 (included) - ld e, AUTOBOOT_DELAY_SECONDS + 1 -autoboot_loop: - ; Convert E - 1 to an ASCII character - ld a, e - add '0' - 1 - call uart_send_one_byte - ; Send "seconds" word then - ld hl, seconds_message - ld bc, seconds_message_end - seconds_message - call uart_send_bytes - dec e - jp z, autoboot_loop_end - ; Wait a bit less than 1 second, should also check keypress or UART receive - push de - ld de, 900 - call sleep_ms - pop de - ; Check if a character arrived - or a - ret nz - ; No character arrived, move the cursor backward down to the seconds count - ld hl, escape - ld bc, 5 - call uart_send_bytes - jr autoboot_loop -autoboot_loop_end: - ; No keypress, no UART receive, return 0 - xor a - ret - - ; Sleep for DE milliseconds while checking for a byte on the UART - ; Returns: - ; A - 0 no character received, non-zero else - ; Alters: - ; A, DE, BC -sleep_ms: -_sleep_ms_again: - ; Divide by 1000 to get the number of T-states per milliseconds - ; 50 is the number of T-states below - ld bc, CPU_FREQ / 1000 / 50 -_sleep_ms_waste_time: - ; 50 T-states for the following, until 'jp nz, _zos_waste_time' - call uart_available_read - or a - ret nz - dec bc - ld a, b - or c - jp nz, _sleep_ms_waste_time - ; If we are here, a milliseconds has elapsed - dec de - ld a, d - or e - jp nz, _sleep_ms_again - ret ; Print the name of the system pointed by the entry and boot it ; Parameters: @@ -144,12 +70,12 @@ bootloader_print_boot_entry: ; Print the "Now booting" message ld hl, autoboot_msg ld bc, autoboot_msg_end - autoboot_msg - call uart_send_bytes + call stdout_write pop hl push hl ld bc, SYS_NAME_MAX_LENGTH - call uart_send_bytes - call newline + call stdout_write + call stdout_newline ; Pop the first entry address and boot it! pop hl jp sys_table_boot_entry @@ -158,21 +84,16 @@ autoboot_msg: autoboot_msg_end: + PUBLIC version_message + PUBLIC version_message_end start_message: DEFM "\r\nZeal 8-bit Computer bootloader " +version_message: INCBIN "version.txt" +version_message_end: DEFM "\r\n\r\n" start_message_end: -boot_message: - DEFM "Press any key to enter menu. Booting automatically in " -boot_message_end: -seconds_message: - DEFM " seconds..." -seconds_message_end: SECTION BSS ORG 0xC000 -escape: DEFS 8 - ; Need to align on 16 for UART_FIFO - ALIGN 16 \ No newline at end of file diff --git a/src/format.asm b/src/format.asm index 9359e95..88d56b0 100644 --- a/src/format.asm +++ b/src/format.asm @@ -3,13 +3,11 @@ ; SPDX-License-Identifier: Apache-2.0 INCLUDE "mmu_h.asm" - INCLUDE "video_h.asm" INCLUDE "pio_h.asm" INCLUDE "sys_table_h.asm" - INCLUDE "uart_h.asm" + INCLUDE "stdout_h.asm" EXTERN newline - EXTERN uart_send_one_byte EXTERN i2c_write_device DEFC I2C_EEPROM_ADDRESS = 0x50 @@ -20,11 +18,11 @@ format_eeprom: PRINT_STR(size_choice) ; Wait for the user input - call uart_receive_one_byte + call stdin_get_char ; Print it back push af - call uart_send_one_byte - call newline + call stdout_put_char + call stdout_newline pop af ; Convert the baudrate choice to the actual values cp '0' @@ -149,11 +147,17 @@ _sleep_ms_waste_time: success_msg: - DEFM 0x1b, "[1;32mSuccess", 0x1b, "[0m\r\n" + GREEN_COLOR() + DEFM "Success" + END_COLOR() + DEFM "\r\n" success_msg_end: write_error_msg: - DEFM 0x1b, "[1;31mI2C write error", 0x1b, "[0m\r\n" + RED_COLOR() + DEFM "I2C write error" + END_COLOR() + DEFM "\r\n" write_error_msg_end: size_choice: diff --git a/src/keyboard.asm b/src/keyboard.asm new file mode 100644 index 0000000..84aa15b --- /dev/null +++ b/src/keyboard.asm @@ -0,0 +1,123 @@ +; SPDX-FileCopyrightText: 2022-2024 Zeal 8-bit Computer +; +; SPDX-License-Identifier: Apache-2.0 + + INCLUDE "keyboard_h.asm" + INCLUDE "pio_h.asm" + + EXTERN pio_disable_interrupt + + SECTION BOOTLOADER + + ; Receive a character from the keyboard (synchronous, blocking) + ; Parameters: + ; None + ; Returns: + ; A - ASCII byte received + ; Alters: + ; A, B, DE + PUBLIC keyboard_next_char +keyboard_next_char: + xor a + ld (received), a + + PUBLIC keyboard_get_char +keyboard_get_char: + ld de, received + +_keyboard_get_char_loop: + ld a, (de) + or a + jr z, _keyboard_get_char_loop + ; Check if it's release key + cp KB_RELEASE_SCAN + jr z, _release_scan + ; Character is not a "release command" + ; Check if the character is a printable char + push hl + cp KB_PRINTABLE_CNT - 1 + jp nc, _special_code ; jp nc <=> A >= KB_PRINTABLE_CNT - 1 + ; Save the scan size in B + ld hl, base_scan + ld b, base_scan_end - base_scan + jr get_from_scan +_special_code: + ; Special character is still in A + cp KB_EXTENDED_SCAN + jr z, _pop_ret + add -KB_SPECIAL_START + ld hl, special_scan + ld b, special_scan_end - special_scan +get_from_scan: + ; Check if there would be an overflow + cp b + ; If there no carry, A is equal or bigger than the length + jr nc, _overflow_pop_continue + add l + ld l, a + adc h + sub l + ld h, a + ld a, (hl) +_pop_ret: + pop hl + ret +_overflow_pop_continue: + pop hl + jr _keyboard_get_char_loop + +_release_scan: + ; Ignore the next character + call keyboard_next_char + ; Return the next character + jp keyboard_next_char + +base_scan: + DEFB 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '\t', '`', 0 + DEFB 0, 0, 0, 0, 0, 'q', '1', 0, 0, 0, 'z', 's', 'a', 'w', '2', 0 + DEFB 0, 'c', 'x', 'd', 'e', '4', '3', 0, 0, ' ', 'v', 'f', 't', 'r', '5', 0 + DEFB 0, 'n', 'b', 'h', 'g', 'y', '6', 0, 0, 0, 'm', 'j', 'u', '7', '8', 0 + DEFB 0, ',', 'k', 'i', 'o', '0', '9', 0, 0, '.', '/', 'l', ';', 'p', '-', 0 + DEFB 0, 0, '\'', 0, '[', '=', 0, 0, 0, 0, '\n', ']', 0, '\\' +base_scan_end: +special_scan: + DEFB '\b', 0, 0, '1', 0, '4', '7', 0, 0, 0, '0' + DEFB '.', '2', '5', '6', '8', KB_ESC + DEFB 0, 0, '+', '3', '-', '*' + DEFB '9', 0, 0, 0, 0, 0, 0, 0, 0 +special_scan_end: + + + + ; Checks if any byte was pressed on the keyboard + ; Parameters: + ; None + ; Returns: + ; A - 0 if no key was pressed, any non-zero value else + ; Alters: + ; None + PUBLIC keyboard_has_char +keyboard_has_char: + ld a, (received) + ret + + + ; Set the keyboard input to synchronous + PUBLIC keyboard_set_synchronous +keyboard_set_synchronous: + ret + + + ; Interrupt handler + PUBLIC keyboard_int_handler +keyboard_int_handler: + ex af, af' + in a, (KB_IO_ADDRESS) + ld (received), a + ex af, af' + ei + reti + + + SECTION BSS +received: DEFS 1 \ No newline at end of file diff --git a/src/menu.asm b/src/menu.asm index 51ee306..80ca8f3 100644 --- a/src/menu.asm +++ b/src/menu.asm @@ -1,9 +1,11 @@ -; SPDX-FileCopyrightText: 2022 Zeal 8-bit Computer +; SPDX-FileCopyrightText: 2022-2024 Zeal 8-bit Computer ; ; SPDX-License-Identifier: Apache-2.0 + + INCLUDE "config.asm" INCLUDE "mmu_h.asm" - INCLUDE "video_h.asm" INCLUDE "sys_table_h.asm" + INCLUDE "stdout_h.asm" INCLUDE "uart_h.asm" DEFC BUFFER_SIZE = 32 @@ -16,25 +18,32 @@ PUBLIC bootloader_menu bootloader_menu: - call uart_disable_fifo + call stdout_prepare_menu + call stdin_set_synchronous ; Print the entering menu message ld hl, menu_msg ld bc, menu_msg_end - menu_msg - call uart_send_bytes + call stdout_write ; Populate our "systems" table while printing each entry call populate_systems ; Show the advanced options ld hl, advanced_msg ld bc, advanced_msg_end - advanced_msg - call uart_send_bytes + call stdout_write ; Wait for the user input - call uart_receive_one_byte + call stdin_get_char ; Print is back push af - call uart_send_one_byte - call newline + call stdout_put_char + call stdout_newline pop af call process_menu_choice + IF CONFIG_ACK_CONTINUE + ld hl, ack_str + ld bc, ack_str_end - ack_str + call stdout_write + call stdin_get_char + ENDIF ; No matter what the result is, print back the main menu jr bootloader_menu @@ -61,10 +70,10 @@ populate_systems_loop: ; Print C in ASCII followed by ' - Boot ' ld a, c add '0' - call uart_send_one_byte + call stdout_put_char ld hl, separator ld bc, separator_end - separator - call uart_send_bytes + call stdout_write ; Print the actual entry now pop hl push hl @@ -101,12 +110,12 @@ populate_systems_empty: ; A, HL, BC, DE print_entry: ld bc, SYS_NAME_MAX_LENGTH - call uart_send_bytes + call stdout_write ; HL is now pointing to the physical address push hl ld hl, parenthesis ld bc, parenthesis_end - parenthesis - call uart_send_bytes + call stdout_write ; Print the physical address pop hl ld c, (hl) @@ -120,7 +129,7 @@ print_entry: push hl ld hl, arrow ld bc, arrow_end - arrow - call uart_send_bytes + call stdout_write pop hl ; Finally dereference the virtual address and print it ld c, (hl) @@ -129,8 +138,8 @@ print_entry: call print_bc_hex ; Close parenthesis ld a, ')' - call uart_send_one_byte - jp newline + call stdout_put_char + jp stdout_newline ; Print 24-bit value in ABC on the UART @@ -168,11 +177,11 @@ print_a_hex: rlca and 0xf call _byte_to_ascii_nibble - call uart_send_one_byte + call stdout_put_char ld a, e and 0xf call _byte_to_ascii_nibble - jp uart_send_one_byte + jp stdout_put_char _byte_to_ascii_nibble: ; If the byte is between 0 and 9 included, add '0' sub 10 @@ -210,21 +219,23 @@ process_menu_choice: jp z, menu_delete_entry cp 's' jp z, menu_save_new_entry + IF CONFIG_UART_AS_STDOUT cp 'b' jp z, menu_change_baudrate + ENDIF cp 'f' jp z, menu_flash_rom - IFDEF ENABLE_TESTER + IF CONFIG_ENABLE_TESTER cp 't' jp z, test_hardware - ENDIF ; ENABLE_TESTER + ENDIF ; CONFIG_ENABLE_TESTER cp 'q' jp z, format_eeprom ; Fall-through invalid_choice: ld hl, invalid_str ld bc, invalid_str_end - invalid_str - jp uart_send_bytes + jp stdout_write ; Routine called when the given choice is a number between [0,systems_count[ @@ -332,8 +343,11 @@ _menu_flash_ask_size: ; Flash the received file to the ROM(NOR Flash) call sys_table_flash_file_to_rom or a - ; Error is A is not 0 - ret z + ; Error if A is not zero + jr nz, _flash_erase_error + PRINT_STR(success_str) + ret +_flash_erase_error: PRINT_STR(flash_erase_error_str) ret _flash_and_reboot: @@ -354,15 +368,16 @@ _menu_flash_rom_zero: jr _menu_flash_ask_size - ; Routine called when "Change baudrate" is selected + ; Routine called when "Change baudrate" is selected. This is useful even if the + ; standard output is the video, since we can receive files. menu_change_baudrate: PRINT_STR(baudrate_choice) ; Wait for the user input - call uart_receive_one_byte + call stdin_get_char ; Print it back push af - call uart_send_one_byte - call newline + call stdout_put_char + call stdout_newline pop af ; Convert the baudrate choice to the actual values cp '1' @@ -379,6 +394,14 @@ menu_change_baudrate: jp z, uart_set_baudrate ; Invalid choice, ask again jp menu_change_baudrate +baudrate_choice: + DEFM "1 - 57600\r\n" + DEFM "2 - 38400\r\n" + DEFM "3 - 19200\r\n" + DEFM "4 - 9600\r\n" + DEFM "\r\nChoose a baudrate [1-4]:" +baudrate_choice_end: + ; Routine called when "Load from UART" is selected menu_load_from_uart: @@ -558,12 +581,12 @@ menu_delete_entry_valid: ; Print the rest of the buffer ld hl, buffer ld bc, 3 - call uart_send_bytes + call stdout_write ; Wait for the answer - call uart_receive_one_byte + call stdin_get_char push af - call uart_send_one_byte - call newline + call stdout_put_char + call stdout_newline pop af ; Make sure the answer is between '1' and 'systems_count' sub '1' @@ -609,7 +632,7 @@ menu_read_input: ; C contains the current size received ld c, 0 _menu_read_input_loop: - call uart_receive_one_byte + call stdin_get_char cp '\b' jp z, _menu_read_input_backspace cp 0x7f ; DEL key @@ -634,7 +657,7 @@ _menu_read_input_loop: ld (hl), a inc hl ld e, c - call uart_send_one_byte + call stdout_put_char ld c, e inc c jp _menu_read_input_loop @@ -648,18 +671,18 @@ _menu_read_input_backspace: dec c ld e, c ld a, '\b' - call uart_send_one_byte + call stdout_put_char ld a, ' ' - call uart_send_one_byte + call stdout_put_char ld a, '\b' - call uart_send_one_byte + call stdout_put_char ld c, e jp _menu_read_input_loop _menu_read_input_cr: ; Terminate the buffer ld (hl), 0 ; Print a newline - jp newline + jp stdout_newline ; Calculate the length of a NULL-terminated string @@ -772,14 +795,29 @@ _parse_not_hex_digit: ; Group together all the strings used in the routines above + IF CONFIG_ACK_CONTINUE +ack_str: + DEFM "\r\nPress any key to return..." +ack_str_end: + ENDIF +success_str: + GREEN_COLOR() + DEFM "Success\r\n" + END_COLOR() +success_str_end: + invalid_str: - DEFM 0x1b, "[1;31mInvalid choice, please try again", 0x1b, "[0m" + RED_COLOR() + DEFM "Invalid choice, please try again" + END_COLOR() invalid_str_end: flash_erase_success_str: DEFM "Configuration saved successfully" flash_erase_success_str_end: flash_erase_error_str: - DEFM 0x1b, "[1;31mError while writing to flash", 0x1b, "[0m" + RED_COLOR() + DEFM "Error while writing to flash" + END_COLOR() flash_erase_error_str_end: system_table_full_str: DEFM "Systems table is full, consider deleting an entry first" @@ -788,10 +826,16 @@ delete_entry_str: DEFM "Choose the entry to delete [1-" delete_entry_str_end: delete_entry_too_less: - DEFM 0x1b, "[1;31mOnly one entry remaining, cannot delete it", 0x1b, "[0m\r\n" + RED_COLOR() + DEFM "Only one entry remaining, cannot delete it" + END_COLOR() + DEFM "\r\n" delete_entry_too_less_end: new_entry_notice_str: - DEFM 0x1b, "[1;33mNumbers must be provided in hexadecimal", 0x1b, "[0m\r\n" + YELLOW_COLOR() + DEFM "Numbers must be provided in hexadecimal" + END_COLOR() + DEFM "\r\n" new_entry_notice_str_end: new_entry_name_str: DEFM "New entry name (max 32 chars): " @@ -800,7 +844,10 @@ flash_rom_addr_str: DEFM "ROM address to flash, aligned on 4KB: " flash_rom_addr_str_end: flashing_flash_str: - DEFM 0x1b, "[1;33mFlashing in progress, do not turn off...", 0x1b, "[0m\r\n" + YELLOW_COLOR() + DEFM "Flashing in progress, do not turn off..." + END_COLOR() + DEFM "\r\n" flashing_flash_str_end: phys_addr_str: DEFM "22-bit physical address, aligned on 16KB: " @@ -809,7 +856,10 @@ virt_dest_addr_str: DEFM "16-bit virtual address: " virt_dest_addr_str_end: aligned_addr_str: - DEFM 0x1b, "[1;31mAddress must be aligned on 16KB", 0x1b, "[0m\r\n" + YELLOW_COLOR() + DEFM "Address must be aligned on 16KB" + END_COLOR() + DEFM "\r\n" aligned_addr_str_end: binary_size_str: DEFM "Binary size: " @@ -821,28 +871,25 @@ booting_ram_str: DEFM "Booting from RAM\r\n" booting_ram_str_end: menu_msg: - DEFM "\r\n\r\nEntering menu. Please select an option:\r\n\r\n" + DEFM "\r\n\r\nPlease select an option:\r\n\r\n" menu_msg_end: advanced_msg: DEFM "p - Load program from UART\r\n" DEFM "a - Add a new entry\r\n" DEFM "d - Delete an existing entry\r\n" DEFM "s - Save configuration to flash\r\n" + IF CONFIG_UART_AS_STDOUT DEFM "b - Change baudrate\r\n" + ELSE ; VIDEO as stdout + DEFM "b - Change UART baudrate (for sending files)\r\n" + ENDIF ; CONFIG_UART_AS_STDOUT DEFM "f - Flash/Program the ROM\r\n" DEFM "q - Quick format I2C EEPROM (ZealFS)\r\n" -IFDEF ENABLE_TESTER + IF CONFIG_ENABLE_TESTER DEFM "t - Test hardware\r\n" -ENDIF + ENDIF DEFM "\r\nEnter your choice: " advanced_msg_end: -baudrate_choice: - DEFM "1 - 57600\r\n" - DEFM "2 - 38400\r\n" - DEFM "3 - 19200\r\n" - DEFM "4 - 9600\r\n" - DEFM "\r\nChoose a baudrate [1-4]:" -baudrate_choice_end: separator: DEFM " - Boot " separator_end: diff --git a/src/pio.asm b/src/pio.asm new file mode 100644 index 0000000..7c78823 --- /dev/null +++ b/src/pio.asm @@ -0,0 +1,67 @@ +; SPDX-FileCopyrightText: 2024 Zeal 8-bit Computer +; +; SPDX-License-Identifier: Apache-2.0 + + INCLUDE "config.asm" + INCLUDE "pio_h.asm" + + IF CONFIG_UART_AS_STDOUT + DEFC IO_PIO_SYSTEM_INT_MASK = ~(1 << IO_UART_RX_PIN) & 0xff + DEFC int_handler = uart_int_handler + ELSE ; !CONFIG_UART_AS_STDOUT + DEFC IO_PIO_SYSTEM_INT_MASK = ~(1 << IO_KEYBOARD_PIN) & 0xff + DEFC int_handler = keyboard_int_handler + ENDIF ; CONFIG_UART_AS_STDOUT + + EXTERN int_handlers_table + EXTERN uart_int_handler + EXTERN keyboard_int_handler + + + SECTION BOOTLOADER + + PUBLIC pio_initialize +pio_initialize: + ; Set system port as bit-control + ld a, IO_PIO_BITCTRL + out (IO_PIO_SYSTEM_CTRL), a + ; Set the proper direction for each pin + ld a, IO_PIO_SYSTEM_DIR + out (IO_PIO_SYSTEM_CTRL), a + ; Set default value for all the (output) pins + ld a, IO_PIO_SYSTEM_VAL + out (IO_PIO_SYSTEM_DATA), a + ; Set interrupt vector to 2 + ld a, 2 + out (IO_PIO_SYSTEM_CTRL), a + ; Enable the interrupts globally for the system port + ld a, IO_PIO_ENABLE_INT + out (IO_PIO_SYSTEM_CTRL), a + ; Enable interrupts, for the required pins only + ld a, IO_PIO_SYSTEM_INT_CTRL + out (IO_PIO_SYSTEM_CTRL), a + ; Mask must follow + ld a, IO_PIO_SYSTEM_INT_MASK + out (IO_PIO_SYSTEM_CTRL), a + ; Initalize user port as input + ld a, IO_PIO_INPUT + out (IO_PIO_USER_CTRL), a + ld a, IO_PIO_DISABLE_INT + out (IO_PIO_USER_CTRL), a + ; Enable interrupts + ld a, int_handlers_table >> 8 + ld i, a + im 2 + ei + ret + + PUBLIC pio_disable_interrupt +pio_disable_interrupt: + ld a, IO_PIO_DISABLE_INT + out (IO_PIO_SYSTEM_CTRL), a + ret + + + PUBLIC default_handler +default_handler: + jp int_handler \ No newline at end of file diff --git a/src/systems.asm b/src/systems.asm index 91f2ad1..13b301d 100644 --- a/src/systems.asm +++ b/src/systems.asm @@ -1,13 +1,16 @@ -; SPDX-FileCopyrightText: 2022 Zeal 8-bit Computer +; SPDX-FileCopyrightText: 2022-2024 Zeal 8-bit Computer ; ; SPDX-License-Identifier: Apache-2.0 + + INCLUDE "config.asm" INCLUDE "mmu_h.asm" INCLUDE "sys_table_h.asm" EXTERN __SYS_TABLE_head + EXTERN video_unload_assets DEFC CPU_FREQ = 10000000 - DEFC RAM_CODE_DEST = 0xD500 + DEFC RAM_CODE_DEST = 0xD600 SECTION BOOTLOADER @@ -68,6 +71,9 @@ _sys_table_get_first_loop: ; No return PUBLIC sys_table_boot_entry sys_table_boot_entry: + IF !CONFIG_UART_AS_STDOUT + call video_unload_assets + ENDIF ; Jump to the physical address directly ld de, SYS_NAME_MAX_LENGTH add hl, de @@ -225,6 +231,9 @@ _sys_table_get_first_empty_found: ; - PUBLIC sys_boot_from_ram sys_boot_from_ram: + IF !CONFIG_UART_AS_STDOUT + call video_unload_assets + ENDIF ; The user's program is in RAM page 0 (0x80000) ld b, 0x80000 >> 14 ex de, hl diff --git a/src/tester.asm b/src/tester.asm index ddfac94..060e0fb 100644 --- a/src/tester.asm +++ b/src/tester.asm @@ -6,10 +6,9 @@ INCLUDE "video_h.asm" INCLUDE "pio_h.asm" INCLUDE "sys_table_h.asm" - INCLUDE "uart_h.asm" + INCLUDE "stdout_h.asm" EXTERN newline - EXTERN uart_send_one_byte EXTERN print_a_hex SECTION BOOTLOADER @@ -82,16 +81,19 @@ _print_array_loop: push bc call print_a_hex ld a, ' ' - call uart_send_one_byte + call stdout_put_char pop bc pop hl djnz _print_array_loop - call newline + call stdout_newline ret test_success: - DEFM 0x1b, "[1;32mSuccess", 0x1b, "[0m\r\n" + GREEN_COLOR() + DEFM "Success" + END_COLOR() + DEFM "\r\n" test_success_end: test_message: @@ -101,10 +103,16 @@ test_terminated: DEFM "\r\nTests finished\r\n" test_terminated_end: read_error_msg: - DEFM 0x1b, "[1;31mRead error", 0x1b, "[0m\r\n" + RED_COLOR() + DEFM "Read error" + END_COLOR() + DEFM "\r\n" read_error_msg_end: write_error_msg: - DEFM 0x1b, "[1;31mWrite error", 0x1b, "[0m\r\n" + RED_COLOR() + DEFM "Write error" + END_COLOR() + DEFM "\r\n" write_error_msg_end: @@ -276,7 +284,7 @@ _skip_zero: ld (hl), a ld hl, buffer ld bc, 3 - call uart_send_bytes + call stdout_write PRINT_STR(nor_flash_detected) test_nor_flash_write: ; We will write a sector of the flash, to make sure we can write @@ -439,7 +447,7 @@ _ram_skip_zero: ld (hl), a ld hl, buffer ld bc, 3 - call uart_send_bytes + call stdout_write PRINT_STR(ram_detected) ret @@ -530,7 +538,10 @@ date_msg: date_msg_end: rtc_warning_msg: - DEFM 0x1b, "[1;33mDisabled (no battery?)", 0x1b, "[0m\r\n" + RED_COLOR() + DEFM "Disabled (no battery?)" + END_COLOR() + DEFM "\r\n" rtc_warning_msg_end: @@ -670,7 +681,7 @@ _test_keyboard_wait: or a jp z, test_keyboard_end push de - call uart_send_one_byte + call stdout_put_char pop de ld b, (hl) ld c, 0xf0 ; Release scan @@ -685,7 +696,7 @@ _test_keyboard_check_code: ld a, '\b' push de - call uart_send_one_byte + call stdout_put_char pop de inc hl inc de diff --git a/src/tileset.bin b/src/tileset.bin new file mode 100644 index 0000000000000000000000000000000000000000..b7f229c95f2413658bdcb958546925780acc52f4 GIT binary patch literal 648 zcmb`FI}U>|42FYJm8F8U15(r*bVRBISD`nc8$(4i(QBn*;2KoD03!o}`cLD&#E2ym z$BsW6Cql?jrD=ICib9A9B3`nTZMuFK$|On3bCQOkFFjX4o0dXls<3x%4Vx;rc^n1K z#$t1(X^i5$HE?KrJn2K}2QK5#chJ_@2%>LMUqw-~?=qwXJ}Ck3^)-kjDcG8_pv+}9 zB6DmU{THyoWj46X{^^;1^wYC8LaYLHIPJHawX{r{5YK^YFuQK53VkzI^8s4X^V}_B q+@m|kmw@fHKH8er=W3E~S2edRk5y<>?7MJ}V +; SPDX-FileCopyrightText: 2022-2024 Zeal 8-bit Computer ; ; SPDX-License-Identifier: Apache-2.0 + + INCLUDE "config.asm" INCLUDE "uart_h.asm" INCLUDE "pio_h.asm" INCLUDE "mmu_h.asm" @@ -8,8 +10,9 @@ DEFC PINS_DEFAULT_STATE = IO_PIO_SYSTEM_VAL & ~(1 << IO_UART_TX_PIN) DEFC UART_FIFO_SIZE = 16 + DEFC CPU_FREQ = 10000000 - EXTERN int_handlers_table + EXTERN pio_disable_interrupt SECTION BOOTLOADER ; Initialize the PIO and the UART @@ -28,49 +31,109 @@ uart_initialize: ld hl, uart_fifo ld (uart_fifo_wr), hl ld (uart_fifo_rd), hl - call pio_init_ports ret - ; Initialize the PIO system and user port -pio_init_ports: - ; Set system port as bit-control - ld a, IO_PIO_BITCTRL - out (IO_PIO_SYSTEM_CTRL), a - ; Set the proper direction for each pin - ld a, IO_PIO_SYSTEM_DIR - out (IO_PIO_SYSTEM_CTRL), a - ; Set default value for all the (output) pins - ld a, IO_PIO_SYSTEM_VAL - out (IO_PIO_SYSTEM_DATA), a - ; Set interrupt vector to 2 - ld a, 2 - out (IO_PIO_SYSTEM_CTRL), a - ; Enable the interrupts globally for the system port - ld a, IO_PIO_ENABLE_INT - out (IO_PIO_SYSTEM_CTRL), a - ; Enable interrupts, for the required pins only - ld a, IO_PIO_SYSTEM_INT_CTRL - out (IO_PIO_SYSTEM_CTRL), a - ; Mask must follow - ld a, IO_PIO_SYSTEM_INT_MASK - out (IO_PIO_SYSTEM_CTRL), a - ; Initalize user port as input - ld a, IO_PIO_INPUT - out (IO_PIO_USER_CTRL), a - ld a, IO_PIO_DISABLE_INT - out (IO_PIO_USER_CTRL), a - - ; Enable interrupts - ld a, int_handlers_table >> 8 - ld i, a - im 2 - ei + IF CONFIG_UART_AS_STDOUT + + ; Function called everytime the menu is about to be shown, do nothing in the case of the UART. + PUBLIC uart_clear_screen +uart_clear_screen: + ret + + DEFC MOVE_BACKWARD_COLS = seconds_message_end - seconds_message + 1 + + ; Routine showing the autoboot message and waiting for a keypress. + ; Returns: + ; A - 0 if autoboot, 1 if key pressed + PUBLIC uart_autoboot +uart_autoboot: + ; Prepare the escape sequence for moving cursor backward: + ; ESC[#D where # is the number of columns to move backward + ld hl, escape + ld (hl), 0x1b ; ESC + inc hl + ld (hl), '[' + inc hl + ; Size of " ...seconds" message + 1, in ASCII + ld (hl), MOVE_BACKWARD_COLS / 10 + '0' + inc hl + ld (hl), MOVE_BACKWARD_COLS % 10 + '0' + inc hl + ld (hl), 'D' + ; Print the autoboot message + ld hl, boot_message + ld bc, boot_message_end - boot_message + call uart_send_bytes + ; Loop until E (not altered by uart_send_bytes) is 0 (included) + ld e, CONFIG_AUTOBOOT_DELAY_SECONDS + 1 +autoboot_loop: + ; Convert E - 1 to an ASCII character + ld a, e + add '0' - 1 + call uart_send_one_byte + ; Send "seconds" word then + ld hl, seconds_message + ld bc, seconds_message_end - seconds_message + call uart_send_bytes + dec e + jp z, autoboot_loop_end + ; Wait a bit less than 1 second, should also check keypress or UART receive + push de + ld de, 900 + call sleep_ms_check + pop de + ; Check if a character arrived + or a + ret nz + ; No character arrived, move the cursor backward down to the seconds count + ld hl, escape + ld bc, 5 + call uart_send_bytes + jr autoboot_loop +autoboot_loop_end: + ; No keypress, no UART receive, return 0 + xor a ret + ; Sleep for DE milliseconds while checking for a byte on the UART + ; Parameters: + ; DE - Milliseconds + ; Returns: + ; A - 0 no character received, non-zero else + ; Alters: + ; A, DE, BC +sleep_ms_check: +_sleep_ms_again: + ; Divide by 1000 to get the number of T-states per milliseconds + ; 50 is the number of T-states below + ld bc, CPU_FREQ / 1000 / 50 +_sleep_ms_waste_time: + ; 50 T-states for the following, until 'jp nz, _zos_waste_time' + call uart_available_read + or a + ret nz + dec bc + ld a, b + or c + jp nz, _sleep_ms_waste_time + ; If we are here, a milliseconds has elapsed + dec de + ld a, d + or e + jp nz, _sleep_ms_again + ret + +boot_message: + DEFM "Press any key to enter menu. Booting automatically in " +boot_message_end: +seconds_message: + DEFM " seconds..." +seconds_message_end: + - PUBLIC default_handler -default_handler: + PUBLIC uart_int_handler +uart_int_handler: ex af, af' exx ; Triggered by UART RX pin @@ -153,11 +216,13 @@ _uart_dequeue_available: pop hl ret - PUBLIc uart_disable_fifo + ENDIF ; CONFIG_UART_AS_STDOUT + + + PUBLIC uart_disable_fifo uart_disable_fifo: - ld a, IO_PIO_DISABLE_INT - out (IO_PIO_SYSTEM_CTRL), a - ret + jp pio_disable_interrupt + ; Reset the UART FIFO ; Parameters: @@ -209,8 +274,8 @@ uart_set_baudrate: ; None ; Alters: ; A, BC, D - PUBLIC newline -newline: + PUBLIC uart_newline +uart_newline: ; Print \r\n ld a, '\r' call uart_send_one_byte @@ -256,7 +321,7 @@ _uart_send_next_byte: ; A - ASCII byte to send on the UART ; Alters: ; A, BC ,D - PUBLIc uart_send_one_byte + PUBLIC uart_send_one_byte uart_send_one_byte: ld b, a ; Set the baudrate in D @@ -570,6 +635,7 @@ wait_tstates_next_bit_87_tstates: SECTION BSS ALIGN 16 uart_fifo: DEFS UART_FIFO_SIZE +escape: DEFS 8 uart_fifo_wr: DEFS 2 uart_fifo_rd: DEFS 2 uart_fifo_size: DEFS 1 diff --git a/src/video.asm b/src/video.asm index 4fc1940..324949b 100644 --- a/src/video.asm +++ b/src/video.asm @@ -1,48 +1,443 @@ -; SPDX-FileCopyrightText: 2022 Zeal 8-bit Computer +; SPDX-FileCopyrightText: 2022-2024 Zeal 8-bit Computer ; ; SPDX-License-Identifier: Apache-2.0 - INCLUDE "uart_h.asm" - INCLUDE "pio_h.asm" - INCLUDE "video_h.asm" - SECTION BOOTLOADER - - PUBLIC video_initialize -video_initialize: - ; Check if we have the video board connected - ld a, 0x42 - ld b, a - out (IO_VIDEO_SCROLL_Y), a - ; Read back this register - in a, (IO_VIDEO_SCROLL_Y) - cp b - ret nz - ; We have the video board connected, save this info - ld a, 1 - ld (has_video), a - ; Set scroll to 0 + INCLUDE "config.asm" + INCLUDE "mmu_h.asm" + INCLUDE "uart_h.asm" + INCLUDE "pio_h.asm" + INCLUDE "video_h.asm" + INCLUDE "keyboard_h.asm" + + SECTION BOOTLOADER + + EXTERN keyboard_has_char + EXTERN keyboard_get_char + + DEFC DEFAULT_VIDEO_MODE = VID_MODE_TEXT_640 + DEFC DEFAULT_CURSOR_BLINK = 30 + DEFC DEFAULT_TEXT_CTRL = 1 << IO_TEXT_AUTO_SCROLL_Y_BIT | 1 << IO_TEXT_WAIT_ON_WRAP_BIT + + MACRO MAP_TEXT_CTRL _ xor a - out (IO_VIDEO_SCROLL_Y), a - ; Initialize the characters color - ld a, 0x0f - jp set_chars_color - - - ; Set the characters colors for the video board -set_chars_color: - ld (chars_color), a - ; Let video chip save this default color - out (IO_VIDEO_SET_COLOR), a - ; Save the color where background and foreground are inverted - rlca - rlca - rlca - rlca - ld (invert_color), a - ret - - - SECTION BSS -has_video: DEFS 1 -chars_color: DEFS 1 -invert_color: DEFS 1 + out (IO_MAPPER_BANK), a + ENDM + + + + ; Initialize the video card + ; Parameters: + ; None + ; Returns: + ; None + ; Alters: + ; A + PUBLIC video_initialize +video_initialize: + ; Map the text controller to the banked I/O + xor a + out (IO_MAPPER_BANK), a + + ld a, DEFAULT_VIDEO_MODE + out (IO_CTRL_VID_MODE), a + + IF CONFIG_VIDEO_SHOW_LOGO + ; Load the logo in memory + call video_prepare_logo + ENDIF + + ; Reset the cursor position, the scroll value and the color, it should already be set to default + ; on coldboot, but maybe not on warmboot + call video_clear_screen + + xor a + out (IO_TEXT_CURS_CHAR), a + ld a, DEFAULT_CHARS_COLOR + out (IO_TEXT_COLOR), a + ld a, DEFAULT_CHARS_COLOR_INV + out (IO_TEXT_CURS_COLOR), a + + ; Hide the cursor, it shall only be shown if the user enters the menu + xor a + out (IO_TEXT_CURS_TIME), a + + ; Enable the screen + ld a, 0x80 + out (IO_CTRL_STATUS_REG), a + ; Enable auto scroll Y as well as wait-on-wrap + ld a, DEFAULT_TEXT_CTRL + out (IO_TEXT_CTRL_REG), a + ret + + + ; Print the given bytes on screen + ; Parameters: + ; HL - Pointer to the sequence of bytes + ; BC - Size of the sequence + ; Returns: + ; A - 0 on success, not zero else + ; HL - HL + BC + ; Alters: + ; A, BC, HL + PUBLIC video_write +video_write: + ld a, b + or c + ret z + ; DE and BC won't be altered by print_char. Use DE for the buffer address. + push de + ex de, hl +_video_write_loop: + ld a, (de) + call print_char + inc de + dec bc + ld a, b + or c + jp nz, _video_write_loop + ex de, hl + pop de + ret + + + ; Routine showing the autoboot message and waiting for a keypress. + ; Returns: + ; A - 0 if autoboot, 1 if key pressed + PUBLIC video_autoboot +video_autoboot: + ; Print the autoboot message + ld hl, boot_message + ld bc, boot_message_end - boot_message + call video_write + ASSERT(CONFIG_AUTOBOOT_DELAY_SECONDS * 60 < 0x10000) + ; Wait CONFIG_AUTOBOOT_DELAY_SECONDS seconds while checking keyboard input + ; We will wait (CONFIG_AUTOBOOT_DELAY_SECONDS * 60) v-blanks since the refresh rate is 60Hz + ; TODO: Use the V-blank interrupts + ld hl, CONFIG_AUTOBOOT_DELAY_SECONDS * 60 + ; Save the previous control status register in C + ; This will let us check the edge and not the level of the v-blank + xor a +_video_autoboot_loop: + ld c, a ; previous ctrl status in C + ; Check if any character is ready + ; Preserves HL and C + call keyboard_has_char + or a + jr z, _video_autoboot_loop_continue + ; A character was received on the keyboard, check if it's the ESC key + ; Preserves HL and C + call keyboard_get_char + sub KB_ESC + ; If it's not the ESC key, ignore it and continue the loop + jr nz, _video_autoboot_loop_continue + ; It's the ESC key! Return a success ( > 0) + inc a + ret +_video_autoboot_loop_continue: + ; No key pressed, check if we are in a v-blank + in a, (IO_CTRL_STATUS_REG) + and 2 + ; If A is 0, no v-blank, continue the loop + jr z, _video_autoboot_loop + ; Currently in V-blank, if the previous status was 0, we have to count this v-blank, else, skip it + ; before we already counted it. In other words, we try to detect the rising edge. + dec c + inc c + jr nz, _video_autoboot_loop + ; Rising edge, count it! + dec hl + ld a, h + or l + ; If HL is 0, it's a timeout, return directly + ret z + ; Else, continue the loop with the previous status reg being 2 + ld a, 2 + jp _video_autoboot_loop + +boot_message: DEFM "Booting...\n\nPress ESC key to enter menu" +boot_message_end: + + + ; Print a single byte on the screen + ; Parameters: + ; A - ASCII byte to print + ; Alters: + ; A, BC + PUBLIC video_put_char +video_put_char: + ld b, h + ld c, l + call print_char + ld h, b + ld l, c + ret + + + PUBLIC video_newline +video_newline: + push hl + call _print_char_newline + pop hl + ret + + + ; Alters: + ; A, HL +print_char: + or a + ret z ; NULL-character, don't do anything + cp '\n' + jr z, _print_char_newline + cp '\r' + jr z, _print_char_carriage_return + cp '\b' + jr z, _print_char_backspace + cp 0xF0 + jr nc, _print_char_set_color + ; Tabulation is considered a space. Do nothing special. + ; If by putting a character we end up scrolling the screen, we'll have to erase a line +_print_any_char: + out (IO_TEXT_PRINT_CHAR), a + ; X should be 1 if a scroll occured after outputting a character + ld l, 1 +_print_check_scroll: + ; Check if scrolled in Y occurred + in a, (IO_TEXT_CTRL_REG) + ; Make the assumption that the flag is the bit 0 + ASSERT (IO_TEXT_SCROLL_Y_OCCURRED == 0) + rrca + ; No carry <=> No scroll Y + ret nc + ; Erase the current line else + jp erase_line +_print_char_newline: + ; Use the dedicated register to output a newline + ld a, DEFAULT_TEXT_CTRL | 1 << IO_TEXT_CURSOR_NEXTLINE + out (IO_TEXT_CTRL_REG), a + ; If a scroll occurred, we need to clear the whole line, X is 0 + ld l, 0 + jp _print_check_scroll +_print_char_carriage_return: + ; Reset cursor X to 0 + xor a + out (IO_TEXT_CURS_X), a + ret +_print_char_backspace: + ; It is unlikely that X is 0 and even more unlikely that Y is too + ; so save some time for the "best" case. + in a, (IO_TEXT_CURS_X) + dec a + ; We know that the cursor X can be signed (0-127), so if the result is + ; negative, it means that it was 0 + jp m, _print_char_backspace_x_negative + ; X is valid, we can update it and return + out (IO_TEXT_CURS_X), a + ret +_print_char_backspace_x_negative: + ; Set X to the maximum possible value + ld a, VID_640480_X_MAX - 1 + out (IO_TEXT_CURS_X), a + ; Y must be decremented + in a, (IO_TEXT_CURS_Y) + dec a + jp p, _print_char_backspace_y_non_zero + ; Y was 0, roll it back + ld a, VID_640480_Y_MAX - 1 +_print_char_backspace_y_non_zero: + out (IO_TEXT_CURS_Y), a + ; Should we manage the scroll? + ret +_print_char_set_color: + ; Get the color out of the character + and 0x0f + ; High byte is now 0 (black color) + out (IO_TEXT_COLOR), a + ret + + + ; Erase a whole video line (writes blank character on the current line) + ; Parameters: + ; L - Cursor X position + ; Returns: + ; None + ; Alters: + ; A, HL +erase_line: + ld h, b ; BC must not be altered + ; Calculate the number of characters remaining on the current line + ld a, VID_640480_X_MAX + sub l + ld b, a + ld a, ' ' +_erase_line_loop: + out (IO_TEXT_PRINT_CHAR), a + djnz _erase_line_loop + ; Restore B register + ld b, h + ; Reset X cursor position + ld a, l + out (IO_TEXT_CURS_X), a + ret + + + ; Clear the whole screen + PUBLIC video_clear_screen +video_clear_screen: + ; Reset the cursor on screen and the scrolling values + xor a + out (IO_TEXT_CURS_X), a + out (IO_TEXT_CURS_Y), a + out (IO_TEXT_SCROLL_Y), a + out (IO_TEXT_SCROLL_X), a + + ; Make the cursor blink every 30 frames (~500ms) + ld a, DEFAULT_CURSOR_BLINK + out (IO_TEXT_CURS_TIME), a + + + ; Save the current mapping + MMU_GET_PAGE_NUMBER(MMU_PAGE_1) + push af + MAP_PHYS_ADDR(MMU_PAGE_1, 0x100000) + ; Clear layer 0 + ld hl, 0x4000 + ld bc, 3200 + ; D = 0 + ld d, l +_clear_screen_loop: + ld (hl), d + inc hl + dec bc + ld a, b + or c + jp nz, _clear_screen_loop + IF CONFIG_VIDEO_SHOW_LOGO + call _video_show_version + jp video_show_logo + ELSE ; CONFIG_VIDEO_SHOW_LOGO + pop af + MMU_SET_PAGE_NUMBER(MMU_PAGE_1) + ret + ENDIF + + + + IF CONFIG_VIDEO_SHOW_LOGO + + EXTERN version_message + EXTERN version_message_end + DEFC VERSION_LENGTH = version_message_end - version_message - 1 + + ; Show the current at the bottom right at the screen +_video_show_version: + ; The VRAM is mapped at the first page (0x4000) + ld de, 0x4000 + 3200 - VERSION_LENGTH + ld bc, VERSION_LENGTH + ld hl, version_message + ldir + ; Set the attributes too + ld b, VERSION_LENGTH + ld a, 0x0f ; Black background, white foreground + ld hl, 0x5000 + 3200 - VERSION_LENGTH +_show_version_loop: + ld (hl), a + inc hl + djnz _show_version_loop + ret + + +video_prepare_logo: + ; Save the current mapping + MMU_GET_PAGE_NUMBER(MMU_PAGE_1) + push af + ; Map the video memory + MAP_PHYS_ADDR(MMU_PAGE_1, 0x100000) + + ; Save the font that we are going to erase + ld hl, 0x4000 + 0x3000 + 128 * 12 + push hl ; VRAM address on the stack + ld de, font_backup + ld bc, tileset_end - tileset ; Size on the stack too + push bc + ldir + + ; Destination font table, starting from character 128 (12 byte per char) + pop bc + pop de + ld hl, tileset + ldir + + ; Set the violet color for the logo: 0x6A8E + ld hl, 0x6A8E + ; Use the color of index 5 (2 bytes per color) + ld (0x4000 + 0xE00 + 5 * 2), hl + jr _remap_page_1 + +video_show_logo: + ld a, 0x80 ; Character to print + ; 9 columns, 6 lines + ld b, 9 + ld c, 6 + ld de, 71 ; 80 characters per line - 9 chars per column + ld hl, 0x4000 + 0x1000 + 149 ; Start at character 149 +_logo_loop: + ; Set the color to 0x05 + ld (hl), 0x05 + ; Set the character (unset bit) + res 4, h + ld (hl), a + set 4, h + inc hl + inc a + djnz _logo_loop + ; B is 0, go to the next line + add hl, de + ld b, 9 + dec c + jr nz, _logo_loop +_remap_page_1: + pop af + MMU_SET_PAGE_NUMBER(MMU_PAGE_1) + ret + + + PUBLIC video_unload_assets +video_unload_assets: + push hl + MMU_GET_PAGE_NUMBER(MMU_PAGE_1) + push af + MAP_PHYS_ADDR(MMU_PAGE_1, 0x100000) + ; Reset the color of index 5 + ld hl, 0xa815 + ld (0x4000 + 0xE00 + 5 * 2), hl + ; Restore the font data + ld de, 0x4000 + 0x3000 + 128 * 12 + ld hl, font_backup + ld bc, tileset_end - tileset + ldir + ; Restore original page + pop af + MMU_SET_PAGE_NUMBER(MMU_PAGE_1) + pop hl + ret + + +tileset: + INCBIN "tileset.bin" +tileset_end: + + + SECTION BSS +font_backup: DEFS 1024 + + + ELSE + + ; Unload the custom color out of the palette and the fonts + ; DO NOT ALTER HL + PUBLIC video_unload_assets +video_unload_assets: + ret + + + ENDIF ; CONFIG_VIDEO_SHOW_LOGO \ No newline at end of file