// SPDX-License-Identifier: GPL-2.0
//
// setup.S collects the memory map information from the BIOS, disables APM,
// enables A20, and performs the switch from real mode to protected mode
// before jumping to the main program entry point.
//
// The memory map information is stored in the 4KB block of memory immediately
// following the setup code. The layout of the information matches the Linux
// boot_params struct. A pointer to this block is passed to the main program,
// for compatiblity with the Linux 32-bit boot protocol.
//
// Copyright (C) 2020-2021 Martin Whitaker.
//
// Derived from memtest86+ setup.S and head.S:
//
// 1-Jan-96 Modified by Chris Brady for use as a boot/loader for memtest-86.

#define __ASSEMBLY__

#include "boot.h"
#include "build_version.h"

#define	BOOT_PARAMS_START	(SETUP_SECS * 512)
#define	BOOT_PARAMS_END		(BOOT_PARAMS_START + 4096)

	.section ".setup", "ax", @progbits
	.code16

# Emulate the Linux boot header, to allow loading by other boot loaders.
# Indicate that the main program code should be loaded in high memory.
# bootsect.S will fix up the values if we are booted directly from the BIOS.

	.globl setup
setup:
	jmp	do_setup
header:
	.ascii	"HdrS"
version:
	.word	0x020c
realmode_swtch:
	.long	0
start_sys_seg:
	.word	0x1000
kernel_version:
	.word	mt86plus_version-512
type_of_loader:
	.byte	0
loadflags:
	.byte	0x1		# LOADED_HIGH
setup_move_size:
	.word	0
	.globl	code32_start
code32_start:
	.long	HIGH_LOAD_ADDR
ramdisk_image:
	.long	0
ramdisk_size:
	.long	0
bootsect_kludge:
	.long	0
heap_end_ptr:
	.word	0
ext_loader_ver:
	.byte	0
ext_loader_type:
	.byte	0
cmd_line_ptr:
	.long	0
initrd_addr_max:
	.long	0xffffffff
kernel_alignment:
	.long	4096
relocatable_kernel:
	.byte	0
min_alignment:
	.byte	12
xload_flags:
#ifdef __x86_64__
	.word	0x9		# XLF_KERNEL_64,XLF_EFI_HANDOVER_64
#else
	.word	0x4		# XLF_EFI_HANDOVER_32
#endif
cmd_line_size:
	.long	255
hardware_subarch:
	.long	0
hardware_subarch_data:
	.quad	0
payload_offset:
	.long	0
payload_length:
	.long	0
setup_data:
	.quad	0
pref_address:
	.quad	HIGH_LOAD_ADDR
init_size:
	.long	_init_size
handover_offset:
	.long	0x10

do_setup:
	# Reload the segment registers, except for the stack.

	movw	%cs, %ax
	movw	%ax, %ds
	movw	%ax, %es

	# Get the memory map and disable APM.

	call	get_mem_info
	call	disable_apm

	# Disable interrupts.

	cli
	movb	$0x80, %al		# disable NMI
	outb	%al, $0x70

	# Enable A20.

	# Try to switch using the fast A20 gate.
	movw	$0x92, %dx
	inb	%dx, %al
	# Skip if it's unimplemented (read returns 0xff).
	cmpb	$0xff, %al
	jz	0f
	orb	$0x02, %al		# set the ALT_A20_GATE bit
	andb	$0xfe, %al		# clear the INIT_NOW bit
	outb	%al, %dx
0:
	# Use the keyboard controller method anyway.
	call	empty_8042
	movb	$0xd1, %al		# send write command
	outb	%al, $0x64
	call	empty_8042
	movb	$0xdf, %al		# A20 on
	outb	%al, $0x60
	call	empty_8042

	# Set up a minimal GDT and IDT.

	xorl	%eax, %eax
	movw	%cs, %ax
	shll	$4, %eax
	addl	%eax, gdt_descr - setup + 2
	lgdt	gdt_descr - setup
	lidt	idt_descr - setup

	# Load a pointer to the boot_params block into ESI.

	xorl	%esi, %esi
	movw	%cs, %si
	shll	$4, %esi
	addl	$BOOT_PARAMS_START, %esi

	# Fix up the jump address.

	movl	(code32_start - setup), %eax
	movl	%eax, (jump - setup + 2)

	# Copy code32_start to the boot_params struct.

	movl	%eax, (BOOT_PARAMS_START + 0x214)

	# Copy cmd_line_ptr and cmd_line_size to the boot_params struct.

	movl	(cmd_line_ptr  - setup), %eax
	movl	%eax, (BOOT_PARAMS_START + 0x228)
	movl	(cmd_line_size - setup), %eax
	movl	%eax, (BOOT_PARAMS_START + 0x238)

	# Switch to protected mode.

	movl	%cr0, %eax
	orl	$1, %eax
	movl	%eax, %cr0
	jmp	flush
flush:
	# Reload the segment registers and jump to the main test program.

	movw	$KERNEL_DS, %ax
	movw	%ax, %ds
	movw	%ax, %es
	movw	%ax, %ss
	movw	%ax, %fs
	movw	%ax, %gs
jump:
data32	ljmp	$KERNEL_CS, $0


# This subroutine queries the BIOS to determine the system memory map
# and stores the results in the boot_params structure that we pass to
# the startup code.

#define SMAP	0x534d4150

get_mem_info:
	push	%ds
	push	%es

	# Set DS and ES to point to the start of the boot_params structure.

	movw	%ds, %ax
	addw	$(BOOT_PARAMS_START >> 4), %ax
	movw	%ax, %ds
	movw	%ax, %es

	# Zero the entire boot_params structure.

	movw	$0x0000, %di
	movw	$0x0400, %cx
	xorl	%eax, %eax
	cld
	rep	stosl

	# First try method E820. E820 returns memory classified into a whole
	# bunch of different types, and allows memory holes and everything.

mem_e820:
	movw	$E820_MAP, %di		# destination pointer
	xorl	%ebx, %ebx		# continuation counter

loop_e820:
	movl	$0x0000e820, %eax	# e820, upper word zeroed
	movl	$SMAP, %edx		# ASCII 'SMAP'
	movl	$20, %ecx		# size of the e820 record
	int	$0x15			# make the call
	jc	done_e820		# bail out if it fails

	cmpl	$SMAP, %eax		# check the return is 'SMAP'
	jne	done_e820		# bail out if it fails

	incb	(E820_ENTRIES)
	addw	$E820_ENTRY_SIZE, %di

	movb	(E820_ENTRIES), %al	# check for table full
	cmpb	$E820_MAP_SIZE, %al
	je	done_e820

	cmpl	$0, %ebx		# any more entries?
	jne	loop_e820

done_e820:
	cmpb	$0, (E820_ENTRIES)
	jnz	get_mem_done

	# Next try method E801.

mem_e801:
	stc				# Fix to work around buggy BIOSs
	xorw	%cx,%cx 		# which don't clear/set carry on
	xorw	%dx,%dx 		# pass/error of e801h memory size
					# call or merely pass cx,dx through
					# without changing them.
	movw	$0xe801, %ax
	int	$0x15
	jc	mem_88

	cmpw	$0x0, %cx		# Kludge to handle BIOSes which
	jne	0f			# report their extended memory in
	cmpw	$0x0, %dx		# AX/BX rather than CX/DX. The spec
	jne	0f			# I have read seems to indicate that
	movw	%ax, %cx		# AX/BX are more reasonable anyway.
	movw	%bx, %dx
0:
	jmp	fake_e820

	# Finally try method 88.

mem_88:
	movb	$0x88, %ah
	int	$0x15
	movw	%ax, %cx
	movw	$0, %dx

fake_e820:
	# Write entry for memory below 1MB.
	movl	$0x0,      E820_ADDR(%di)
	movl	$0xa0000,  E820_SIZE(%di)
	movl	$1,        E820_TYPE(%di)
	incb	(E820_ENTRIES)
	addw	$E820_ENTRY_SIZE, %di

	# Write entry for memory between 1MB and 16MB.

	andl	$0xffff, %ecx		# convert to 32-bits
	jz	0f
	shll	$10, %ecx		# convert to bytes
	movl	$0x100000, E820_ADDR(%di)
	movl	%ecx,      E820_SIZE(%di)
	movl	$1,        E820_TYPE(%di)
	incb	(E820_ENTRIES)
	addw	$E820_ENTRY_SIZE, %di
0:
	# Write entry for memory above 16MB.

	andl	$0xffff, %edx		# convert to 32-bits
	jz	1f
	shll	$16, %edx		# convert to bytes
	movl	$0x1000000, E820_ADDR(%di)
	movl	%edx,       E820_SIZE(%di)
	movl	$1,         E820_TYPE(%di)
	incb	(E820_ENTRIES)
	addw	$E820_ENTRY_SIZE, %di
1:

get_mem_done:
	pop	%es
	pop	%ds
	ret

# This subroutine disables APM if it is present.

disable_apm:
	movw	$0x5300, %ax		# APM BIOS installation check
	xorw	%bx, %bx
	int	$0x15
	jc	disable_apm_done	# error -> no APM BIOS

	cmpw	$0x504d, %bx		# check for "PM" signature
	jne	disable_apm_done	# no signature -> no APM BIOS

	movw	$0x5304, %ax		# Disconnect first just in case
	xorw	%bx, %bx
	int	$0x15			# ignore return code

	movw	$0x5301, %ax		# Real Mode connect
	xorw	%bx, %bx
	int	$0x15
	jc	disable_apm_done	# error

	movw	$0x5308, %ax		# Disable APM
	mov	$0xffff, %bx
	xorw	%cx, %cx
	int	$0x15

disable_apm_done:
	ret

# This subroutine checks that the keyboard command queue is empty (after
# emptying the output buffers). No timeout is used - if this hangs there
# is something wrong with the machine, and we probably couldn't proceed
# anyway.

empty_8042:
	call	delay
	inb	$0x64, %al		# 8042 status port
	cmpb	$0xff, %al		# skip if not implemented
	jz	empty_8042_ret
	testb	$1, %al 		# anything in the output buffer?
	jz	no_output
	call	delay
	inb	$0x60, %al		# read it
	jmp	empty_8042

no_output:
	testb	$2, %al 		# is input buffer full?
	jnz	empty_8042		# yes - loop
empty_8042_ret:
	ret

# This subroutine provides a short delay.

delay:
	.word	0x00eb			# jmp $+2
	ret

# A minimal GDT and IDT.

	.align	4
gdt:
	.quad	0x0000000000000000	# NULL descriptor
	.quad	0x0000000000000000	# not used
	.quad	0x00c09a0000007fff	# 128MB 32-bit code at 0x000000
	.quad	0x00c0920000007fff	# 128MB 32-bit code at 0x000000
gdt_end:

	.word	0			# for alignment
gdt_descr:
	.word	gdt_end - gdt - 1	# gdt limit
	.long	gdt - setup		# gdt base - relocated at run time

	.word	0			# for alignment
idt_descr:
	.word	0			# idt limit=0
	.long	0			# idt base=0

mt86plus_version:
	.ascii "Memtest86+ v" , MT_VERSION
	.byte   0

# Pad to the declared size.

	.org	(SETUP_SECS*512)
