forked from MirrorRepos/RomWBW
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
469 lines
16 KiB
469 lines
16 KiB
;::::::::::::::::::::::::::::::::::::::::::::::::**************************
|
|
; Hard disk routines as implemented for ** Hardware Dependent **
|
|
; Tilmann Reh's GIDE board adapted to the ** for exact interface **
|
|
; D-X Designs Pty Ltd P112. **************************
|
|
; Portions derived from GIDE.Z80 with tips from GIDETEST.PAS by Tilmann Reh.
|
|
;--------------------------------------------------------------------------
|
|
; This file uses modifications of the definitions in ICFG-xx to reflect
|
|
; Physical and/or logical definitions for IDE drives. A controller type of
|
|
; 8xH signifies IDE/ATA drives, in which case the Drive byte at HDRVx is:
|
|
; 7 6 5 4 3 2 1 0
|
|
; | | | | | | | +- Unit Number (0 = Master, 1 = Slave)
|
|
; | | | | +-+-+--- (reserved)
|
|
; | | | +--------- 1 = Active, 0 = Inactive
|
|
; +-+-+----------- (reserved)
|
|
; Additionally, the first byte of the Reduced Write Cylinder word is re-
|
|
; defined to be the number of physical/logical Sectors-Per-Track.
|
|
; These parameters are used to convert the Track & 16 Sector/Track format
|
|
; assumed in the B/P Bios definitions for Hard Drives into Track/Sector/Head
|
|
; Sector Number needed for IDE/ATA Data accesses. Direct driver IO routines
|
|
; to Select (SELHD), Read (HDREAD) and Write (HDWRIT) are all included here.
|
|
;--------------------------------------------------------------------------
|
|
; 1.0 - 26 Aug 01 - Cleaned up source and included fixes from SCSI. HFB
|
|
; 0.2 - 28 Jun 97 - Added Home Drive, Retry Disable bit handling. HFB
|
|
; 0.1 - 25 Apr 97 - Initial Test Release HFB
|
|
;***************************************************************************
|
|
|
|
IF BANKED
|
|
COMMON /BANK2/
|
|
ELSE
|
|
CSEG
|
|
ENDIF
|
|
|
|
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
; Function 0 - Set User Data Area Adress for Direct Disk IO, Return
|
|
; Number of Bytes in the driver Command Block (SCSI-"like")
|
|
; For IDE, a minimum of 6 Bytes is needed (Comnd,Trk(2),Sctr,Head,Drive)
|
|
; Enter: DE = Address of User Data Area
|
|
; Exit : A = Number of bytes available in the Command Block
|
|
; Uses : A,HL
|
|
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
|
|
HDVALS: LD (DATADR),DE ; Save the Users Data Area
|
|
LD A,CMDSIZ
|
|
RET
|
|
|
|
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
; Function 1 - Set Drive bit Command Block from A
|
|
; Enter: A = Drive Byte
|
|
; Exit : A = Drive Bit in LSB (00/01H, for Master/Slave)
|
|
; Uses : AF
|
|
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
|
|
HDSLCT: AND 01H ; Strip any garbage
|
|
LD (hdUnit),A ; save in Command Block
|
|
RET
|
|
|
|
;========================================================================
|
|
; Select Hard Disk (Unit 0/1, Master/Slave) < Internal Bios routine >
|
|
|
|
SELHD: LD A,(SEKPDN) ; Load Device and Unit # to select
|
|
SELHDA: LD C,A ; position for calculations
|
|
LD B,HDRV1-HDRV0 ; with size
|
|
MLT BC ; Calculate offset into table
|
|
LD HL,HDRV0 ; from first Physical drive
|
|
ADD HL,BC
|
|
LD A,(CNTRLR) ; Set Controller type
|
|
BIT 7,A ; Is it IDE/ATA?
|
|
JP Z,SELERR ; ..jump if Not to return Error
|
|
|
|
LD A,(HL) ; Fetch Device/LUN byte
|
|
CALL HDSLCT ; setting variables for Device and LUN
|
|
; Wait until Drive is ready or 10 Seconds
|
|
LD A,100 ; 100-100mS ticks
|
|
SelWt: CALL BsyWt ; Wait this long for Drive to come Ready
|
|
JP C,SELERR ; ..Error if Timeout
|
|
; Else gather drive geometry specified in ICFG-xx
|
|
INC HL ; Advance to Number of Tracks Word
|
|
LD BC,4 ; Move Number of Tracks (Word)
|
|
LD DE,hdTrks ; Number of Heads (Byte)
|
|
LDIR ; and Sctrs/Trk to local storage
|
|
SetPmV: JP SETPARMS ; then set parameters for DPH/DPB
|
|
|
|
;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
; Write to Hard Disk Drive < Internal BIOS Routine >
|
|
; Writes from HSTBUF using HSTTRK and HSTSEC to build Block Number.
|
|
; NOTE: This routine uses physical drive characteristics from ICFG-xx.
|
|
|
|
HDWRIT: XOR A
|
|
LD (HSTACT),A ; Show no active writes pending
|
|
|
|
LD HL,7CH*256+CMDWR ; Set the IDE Write Command
|
|
JR HDRW ; ..continue
|
|
|
|
;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
; Read from Hard Disk Drive < Internal BIOS Routine >
|
|
; Reads to HSTBUF using HSTTRK and HSTSEC to build Block Number.
|
|
; NOTE: This routine uses physical drive characteristics from ICFG-xx.
|
|
; The routine computes a sequential block number (as with SCSI) with
|
|
; the algorithm; Trk * 16 + Sector, then computes Head, Sector and Track
|
|
; using Physical characteristics (hdHds = Number_of_Heads,
|
|
; hdSPT = Sectors_per_Track) according to the algorithm:
|
|
;
|
|
; Sector := (Block# MOD hdSPT)+1 (* Quotient1 := Block# DIV hdSPT *)
|
|
; Head := Quotient1 MOD hdHds (* Quotient2 := Quotient1 DIV hdHds *)
|
|
; Track := Quotient2
|
|
|
|
HDREAD: LD HL,7EH*256+CMDRD ; Set the IDE Read Command
|
|
HDRW: LD (hdComd),HL ; save
|
|
|
|
; Prepare for Disk Read/Write by Preloading all Registers
|
|
|
|
LD HL,(HSTDPH) ; Get pointer to desired DPH
|
|
DEC HL ; back up to Device #
|
|
LD A,(HL) ; and fetch
|
|
CALL SELHDA ; Select this device from A-Reg
|
|
LD BC,4*256+0 ; Count = 4, MSB of Block # = 0
|
|
LD HL,(HSTTRK) ; Get requested track
|
|
MUL16: ADD HL,HL ; Multiply C,H,L by 16 for 21-bit block #
|
|
RL C ; shifting overflow bit to C
|
|
DJNZ MUL16 ; ..and looping til * 16
|
|
LD A,(HSTSEC) ; Get Logical Host Sector # (4-bits)
|
|
ADD A,L ; add in Hi 4-bits of low Block #
|
|
LD L,A ; save back
|
|
LD A,(hdSPT) ; Get Sctrs-Per-Trk
|
|
LD E,A
|
|
CALL Divide ; Divide CHL by E
|
|
INC A ; Make Sctr # Base at 1
|
|
LD (hdSec),A ; (save)
|
|
LD A,(hdHds) ; Get Number of Heads
|
|
LD E,A
|
|
CALL Divide ; Divide CHL (Quotient from above) by E
|
|
LD (hdHead),A ; (save)
|
|
LD A,H ; Swap
|
|
LD H,L ; Bytes
|
|
LD L,A ; in Track Word
|
|
LD (hdTrkH),HL ; save Quotient (Track, Hi & Lo)
|
|
LD A,0AAH
|
|
LD (hdErr),A ; Activate Retries in case Reading
|
|
LD A,1
|
|
LD (hdSCnt),A ; Do only One Sector
|
|
|
|
LD A,5 ; Give it a few tries
|
|
HDOps0: LD (HRTrys),A ; Save Count
|
|
CALL GoGIDE ; Try Read/Write Operation
|
|
RET Z ; ..quit if Ok
|
|
LD A,(HRTrys) ; Else
|
|
DEC A ; Any tries remaining?
|
|
JR NZ,HDOps0 ; ..loop if So
|
|
DEC A ; Else Return 0FFH
|
|
RET
|
|
|
|
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
; Function 2 - Direct IDE/ATA driver. This routine performs the function
|
|
; described by the command in the HD Command Block with Data area
|
|
; addressed by DE. At the end of the function, 512 bytes of data are
|
|
; transferred from the Bios IO Buffer to the Users Space set by Fcn 0.
|
|
; The IDE/ATA Command Block layout is:
|
|
;
|
|
; Byte Format R/W/V Init RdID Power Diag PwrSet Home
|
|
; 0 [Command ] 50 20/30/40 91 EC E0/E1/E5 90 E2/E3 10
|
|
; 1 [BitMap ] 70 7C/(7E) 74 40 00 00 04 40
|
|
; 2 [Drv/Hd ] 0AnH 0AnH 0AnH-1 0AnH - - - 0An0
|
|
; 3 [Cyl#(Hi)] CylHi CylHi CylHi - - - - -
|
|
; 4 [Cyl#(Lo)] CylLo CylLo CylLo - - - - -
|
|
; 5 [Sector# ] - Sctr - - - - - -
|
|
; 6 [Sctr Cnt] - SCnt NSecs - - - n*5Secs -
|
|
; 7 [Err Reg ] - (0AA) - - - - - -
|
|
; 8 [Dgtl Out] - - - - - - - -
|
|
; Rslts/Stat: Stat Stat Stat Stat SCnt Err - Stat
|
|
; Reg Reg Reg Reg Reg Reg Reg
|
|
;
|
|
; Enter: DE = Pointer to User IDE/ATA Command Block
|
|
; "hdComd" contains pre-filled Command Block
|
|
; A = 0 if No Data to be Written, FF if User-supplied data to write
|
|
; Exit : H = Error Byte value (If any)
|
|
; L = Status Byte value (If any)
|
|
; A = Status byte, Flags set accordingly.
|
|
; Uses : AF,BC,DE,HL
|
|
; NOTE : Routine assumes the Command Block is properly configured for the
|
|
; desired function and device. Timeout returns 0FFH, Unsupported
|
|
; command returns 7FH.
|
|
; For external access, It assumes the user has used Functions 0 and 1 to
|
|
; set the data transfer source/dest address drive number.
|
|
;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
|
|
HD_RW: PUSH AF ; Save User Data Flag
|
|
PUSH DE ; and ptr to User's CDB
|
|
CALL FLUSH ; Insure Host Buffer is Free
|
|
POP HL ; restore User CDB ptr to HL for move
|
|
IF BANKED
|
|
CALL SHDBNK ; Load Banks for transfer to System fm User
|
|
CALL XMOVE ; and Set
|
|
ENDIF ;Banked
|
|
LD DE,hdComd
|
|
LD BC,10 ; Move a 10-byte block
|
|
CALL MOVE ; into the Command area
|
|
POP AF ; Restore Flag
|
|
OR A ; Any User data to write?
|
|
JR Z,HD_RW0 ; ..bypass move if not
|
|
IF BANKED
|
|
CALL SHDBNK ; Load for move from User's to System Banks
|
|
CALL XMOVE ; and Set
|
|
ENDIF ;Banked
|
|
CALL HDDMOV ; Set to move 512 bytes from User to Hstbuf
|
|
CALL MOVE ; Do It!
|
|
HD_RW0: CALL GoGIDE ; Set Data Addr and do the operation
|
|
PUSH HL ; save Status/Err
|
|
IF BANKED
|
|
CALL SHDBNK ; Load Bank Numbers
|
|
LD A,B ; Swap
|
|
LD B,C ; Source
|
|
LD C,A ; and Destination Banks
|
|
CALL XMOVE ; Set Source/Dest
|
|
ENDIF ;Banked
|
|
CALL HDDMOV ; set Addresses and Length
|
|
EX DE,HL ; write back to User's area
|
|
CALL MOVE ; move without affecting status in A
|
|
POP HL ; Restore Status/Err bytes for checks
|
|
LD A,L ; Get Status
|
|
OR A ; set flags
|
|
RET ; ..and quit
|
|
|
|
;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
; Raw GIDE Driver. Assumes Command in hdComd and Parms in Comnd Data Block
|
|
|
|
GoGIDE:
|
|
IF NOWAIT ; If Wait States not desired..
|
|
IN0 A,(DCNTL) ; Get current settings
|
|
LD (WTSAVE),A ; save for exit
|
|
AND 11001111B ; Keep everything but IO Waits
|
|
OUT0 (DCNTL),A ; and set to No IO Wait States
|
|
ENDIF ;nowait
|
|
|
|
; Wait approximately 5 Seconds for Target to become Busy.
|
|
|
|
LD L,0FFH ; Preset Timeout Error Status
|
|
LD A,50 ; 50-100mS ticks
|
|
CALL BsyWt ; Monitor Timeout while checking Busy Bit
|
|
JR C,TIMOUT ; ..Reporting Error if Timed Out
|
|
;..else fall thru..
|
|
LD HL,hdHead
|
|
LD A,(hdUnit) ; Get Drive #
|
|
ADD A,A ; Shift
|
|
ADD A,A ; Drive Bit
|
|
ADD A,A ; into
|
|
ADD A,A ; position
|
|
OR (HL) ; Add Head # bits
|
|
OR 0A0H ; and Fixed Bits
|
|
LD (HL),A
|
|
LD BC,IDESDH ; Set Port Address (GIDE+0EH)
|
|
DEC HL ; back ptr up to hdBMap byte
|
|
LD E,(HL) ; fetch
|
|
LD D,7 ; and set number of bytes
|
|
RL E ; Shift B7 out
|
|
|
|
GoGID0: RL E ; Test bit to Carry
|
|
INC HL ; bump Byte ptr
|
|
JR NC,GoGID1 ; ..jump if No byte to write
|
|
LD A,(HL) ; Else get byte
|
|
OUT (C),A ; set IDE Register
|
|
GoGID1: DEC BC ; Down to next Register address
|
|
DEC D ; Finished?
|
|
JR NZ,GoGID0 ; ..loop if Not
|
|
|
|
LD HL,HSTBUF ; Always IO to/from Host Buffer
|
|
LD BC,IDEDat ; Pre-load Data Reg Adr in C, 0 in B
|
|
LD A,(hdComd) ; Get Command Byte
|
|
CP CMDVER+2 ; Is it in the range w/retry bit?
|
|
JR NC,GoGID2 ; ..jump if Not
|
|
AND 0FEH ; Else Ignore Retry disable bit (B0)
|
|
CP CMDRD ; Read Sector?
|
|
JR Z,HRead
|
|
CP CMDWR ; Write Sector?
|
|
JR Z,HWrite
|
|
CP CMDVER ; Verify Sector(s)?
|
|
JR Z,HMisc
|
|
BIT 4,A ; Is it Home Comnd?
|
|
JR Z,HMisc
|
|
;..else fall thru to error exit..
|
|
GoGID2: CP CMDID ; Read ID Information?
|
|
JR Z,HRead
|
|
CP CMDDIAG ; Perform Diagnostics?
|
|
JR Z,HDiag
|
|
CP CMDINIT ; Initialize Drive Parameters?
|
|
JR Z,HInit
|
|
CP CMDPWQ ; Query Power Status?
|
|
JR Z,HPwrQ
|
|
CP CMDFMT ; Format Track?
|
|
JR Z,HWrite
|
|
CP CMDPW0 ; Low range of Power Set Commands?
|
|
JR C,GoGIDX ; ..jump if < 0E0H
|
|
CP CMDPW3+1 ; High Range of Power Set Commands?
|
|
JR C,HMisc ; ..jump if in [E0..E3]
|
|
GoGIDX: RES 7,L ; If Not legal Command, Return Error 7FH
|
|
POP AF ; (clear command from stack)
|
|
TIMOUT:
|
|
IF NOWAIT
|
|
LD A,(WTSAVE) ; Get entry Wait state settings
|
|
OUT0 (DCNTL),A ; and restore
|
|
ENDIF
|
|
LD A,L ; get Status Byte
|
|
LD (ERFLAG),A ; (store)
|
|
OR A ; set flags
|
|
RET ; and return
|
|
|
|
;.....
|
|
; Read a Sector from the Disk, or Disk Parameters to the Buffer
|
|
|
|
HRead: CALL Cmd_Wt ; Send Command in A, Return when Ready
|
|
HRead0: IN A,(IDECmd) ; Get Status
|
|
BIT 3,A ; Ready?
|
|
JR Z,HRead0 ; ..loop if Not
|
|
INIR ; Read 512 bytes
|
|
INIR ; in two-256 byte sequences
|
|
HdFini: CALL Wt_Rdy ; Wait for drive to become Ready
|
|
;; -- May need this with some Older Drives that send ECC bytes with no warning!
|
|
;; BIT 4,A ; DRQ Shifted?
|
|
;; JR Z,HdFnQ ; ..jump if Not
|
|
;; IN A,(IDEDat) ; Else Read data reg (ECC Bytes?
|
|
;; JR HdFini ; ..loop til no more data
|
|
|
|
HdFnQ: IN A,(IDECmd) ; Restore byte
|
|
AND 10001001B ; Busy, DRQ, or Error?
|
|
JR Z,HdFnOk ; ..exit if Ok
|
|
LD A,02H ; Else Set Error status = 2
|
|
HdFnOk: LD L,A ; store
|
|
IN A,(IDEErr) ; Get Any Error Status
|
|
LD H,A ; save
|
|
JR TIMOUT ; ..and exit thru common location
|
|
|
|
;.....
|
|
; Write a 512-byte Sector to the Disk
|
|
|
|
HWrite: CALL Cmd_Wt ; Send Command in A, Return when Ready
|
|
HWrit2: IN A,(IDECmd)
|
|
BIT 3,A ; Data Request?
|
|
JR Z,HWrit2 ; ..loop if Not
|
|
OTIR ; Else Write 512 bytes
|
|
OTIR ; in two-256 byte operations
|
|
HInit: CALL Wt_Rdy
|
|
JR HdFnQ ; ..and finish off above
|
|
|
|
;.....
|
|
; Execute Drive Diagnostics
|
|
|
|
HDiag: CALL Cmd_Wt ; Send Command in A, Return when Ready
|
|
IN A,(IDEErr) ; Read Status of Tests
|
|
JR HdFnOk ; ..exit with Status
|
|
|
|
;.....
|
|
; Query Power Save Status
|
|
|
|
HPwrQ: CALL Cmd_Wt ; Send Command in A, Return when Ready
|
|
IN A,(IDESCnt) ; Get Status (00=Running, FF=Stopped)
|
|
JR HdFnOk ; ..exit with Status
|
|
|
|
;.....
|
|
; Miscellaneous Commands such as Set Various Power Control Features
|
|
|
|
HMisc: CALL Cmd_Wt ; Send Command in A, Return When Ready
|
|
JR HdFnQ ; ..exit checking Status
|
|
|
|
;================== SUPPORT ROUTINES ==================
|
|
; Divide 24-bit Number by 8-bit Number returning Quotient and Remainder
|
|
; Enter: CHL = 24-bit Unsigned Dividend
|
|
; E = 8-bit Unsigned Divisor
|
|
; Exit : CHL = 24-bit Quotient
|
|
; A = 8-bit Remainder
|
|
; Uses : AF,BC,HL
|
|
|
|
Divide: LD B,24+1 ; 25 times thru Loop
|
|
XOR A ; Clear Remainder and Carry
|
|
Div: ADC A,A ; Shift Accum Left + Carry
|
|
SBC A,E ; Subtract Divisor
|
|
JR NC,Div0 ; ..jump if it Worked
|
|
ADD A,E ; Else restore Accum & Carry
|
|
Div0: CCF ; Flip Carry Bit
|
|
ADC HL,HL ; Shift any Carry into
|
|
RL C ; Dividend/Quotient
|
|
DJNZ Div ; ..loop til Done
|
|
RET
|
|
|
|
;.....
|
|
; Wait for Drive to become Ready (With Timeout)
|
|
; Enter: A = Number of 100 mS increments to wait before timeout
|
|
; Exit : Carry Set (C) if Timeout, else Carry Clear (NC), Unit Ready
|
|
; Uses : MTM Timer variable, AF
|
|
|
|
BsyWt: LD (MTM),A ; Set Timer value
|
|
BsyW0: LD A,(MTM) ; Get Current Count
|
|
OR A ; Have we timed out?
|
|
SCF ; (Set error flag in case)
|
|
RET Z ; ..exit w/Error flag if So
|
|
IN A,(IDECmd) ; Else Get the Busy Bit
|
|
RLA ; Is it BSY?
|
|
JR C,BSYW0 ; ..loop if So
|
|
RET ; else back to Caller, Carry Clear
|
|
|
|
;.....
|
|
; Send command to the IDE Command Register, fall thru to wait for Ready Status
|
|
|
|
Cmd_Wt: LD A,(hdComd) ; Get Command Byte in case bit stripped
|
|
OUT (IDECmd),A ; Start Operation
|
|
;..fall thru to wait for Ready
|
|
;.....
|
|
; Wait for Drive to become Ready (No Timeout)
|
|
; Enter: None
|
|
; Exit : None
|
|
; Uses : AF
|
|
|
|
Wt_Rdy: IN A,(IDECmd) ; Get Drive Status
|
|
RLA ; Ready?
|
|
JR C,Wt_Rdy ; ..loop if Not
|
|
RET
|
|
|
|
;.....
|
|
; Set registers for Whole Block Move
|
|
|
|
HDDMOV: LD HL,(DATADR) ; Get ptr to User's Area
|
|
LD DE,HSTBUF ; Pt to local Host Buffer
|
|
LD BC,512 ; set length
|
|
RET ; ..and return
|
|
|
|
;.....
|
|
; Set banks for Interbank move
|
|
|
|
IF BANKED
|
|
SHDBNK: LD A,(USP-1) ; Get Source Bank Byte
|
|
RRA ; shift to
|
|
RRA ;
|
|
RRA ; Bank #
|
|
AND 1FH ; Mask off any Junk
|
|
LD C,A ; position
|
|
LD A,(SYSBNK) ; Get System Bank
|
|
LD B,A ; position it too
|
|
RET ; and return
|
|
ENDIF ;banked
|
|
|
|
IF BANKED
|
|
COMMON /B2RAM/
|
|
ELSE
|
|
DSEG
|
|
ENDIF
|
|
|
|
HRTrys: DEFS 1 ; Retry counter storage
|
|
hdUnit: DEFS 1 ; IDE Drive (0 = Master, 1 = Slave)
|
|
hdTrks: DEFS 2 ; Number of Tracks on IDE Drive
|
|
hdHds: DEFS 1 ; Number of Heads on IDE Drive
|
|
hdSPT: DEFS 1 ; Number of Sectors-Per-Track on IDE Drive
|
|
|
|
; IDE Command Block for User Direct Driver Access
|
|
|
|
hdComd: DEFS 1 ; Command Byte
|
|
hdBMap: DEFS 1 ; Bit Map (B6..0) of Following Bytes to Set
|
|
hdHead: DEFS 1 ; Head Number/Number of Heads in B3..0
|
|
hdTrkH: DEFS 1 ; Hi-Track (Cylinder) Byte
|
|
hdTrkL: DEFS 1 ; Lo-Track (Cylinder) Byte
|
|
hdSec: DEFS 1 ; Sector Number
|
|
hdSCnt: DEFS 1 ; Sector Count
|
|
hdErr: DEFS 1 ; Error Reg Value
|
|
hdDigO: DEFS 1 ; Digital Output Reg Value
|
|
CMDSIZ EQU $-hdComd ; Size of Command Block
|
|
|
|
DATADR: DEFS 2 ; Pointer to User Buffer Space (user bank)
|
|
IF NOWAIT
|
|
WTSAVE: DEFS 1 ; Storage for Entry Wait State Setting
|
|
ENDIF
|
|
;======================= End of HARDIDE ===========================
|
|
|