#!/usr/bin/env python3
# Copyright (c) 2010 Arista Networks, Inc.  All rights reserved.
# Arista Networks, Inc. Confidential and Proprietary.

# pylint: disable=consider-using-with

# diag to margin voltage on Raven.
# See BIOS and Kernel Develper's Guide (BKDG) for AMD Family 10h Processors for info
# on the registers accessed in this diag. Section 2.4.1 discusses power

# this code assumes that F3xA0[SlamVidMode] == 1 (see data sheet for what this means)

import optparse, os, Tac, time # pylint: disable=deprecated-module

op = optparse.OptionParser( usage = "%prog -m marginPercent" )
op.add_option( "-m", "--margin", help="amount to margin voltages in percent", 
               action="store", type=float, default=0 )
opts, args = op.parse_args()

if opts.margin < -5 or opts.margin > 5:
   op.error( "Margin range is +-5%" )

margin = opts.margin


VIDmap = {
0b0000000:1.5500,
0b0000001:1.5375,
0b0000010:1.5250,
0b0000011:1.5125,
0b0000100:1.5000,
0b0000101:1.4875,
0b0000110:1.4750,
0b0000111:1.4625,
0b0001000:1.4500,
0b0001001:1.4375,
0b0001010:1.4250,
0b0001011:1.4125,
0b0001100:1.4000,
0b0001101:1.3875,
0b0001110:1.3750,
0b0001111:1.3625,
0b0010000:1.3500,
0b0010001:1.3375,
0b0010010:1.3250,
0b0010011:1.3125,
0b0010100:1.3000,
0b0010101:1.2875,
0b0010110:1.2750,
0b0010111:1.2625,
0b0011000:1.2500,
0b0011001:1.2375,
0b0011010:1.2250,
0b0011011:1.2125,
0b0011100:1.2000,
0b0011101:1.1875,
0b0011110:1.1750,
0b0011111:1.1625,
0b0100000:1.1500,
0b0100001:1.1375,
0b0100010:1.1250,
0b0100011:1.1125,
0b0100100:1.1000,
0b0100101:1.0875,
0b0100110:1.0750,
0b0100111:1.0625,
0b0101000:1.0500,
0b0101001:1.0375,
0b0101010:1.0250,
0b0101011:1.0125,
0b0101100:1.0000,
0b0101101:0.9875,
0b0101110:0.9750,
0b0101111:0.9625,
0b0110000:0.9500,
0b0110001:0.9375,
0b0110010:0.9250,
0b0110011:0.9125,
0b0110100:0.9000,
0b0110101:0.8875,
0b0110110:0.8750,
0b0110111:0.8625,
0b0111000:0.8500,
0b0111001:0.8375,
0b0111010:0.8250,
0b0111011:0.8125,
0b0111100:0.8000,
0b0111101:0.7875,
0b0111110:0.7750,
0b0111111:0.7625,
0b1000000:0.7500,
0b1000001:0.7375,
0b1000010:0.7250,
0b1000011:0.7125,
0b1000100:0.7000,
0b1000101:0.6875,
0b1000110:0.6750,
0b1000111:0.6625,
0b1001000:0.6500,
0b1001001:0.6375,
0b1001010:0.6250,
0b1001011:0.6125,
0b1001100:0.6000,
0b1001101:0.5875,
0b1001110:0.5750,
0b1001111:0.5625,
0b1010000:0.5500,
0b1010001:0.5375,
0b1010010:0.5250,
0b1010011:0.5125,
0b1010100:0.5000,
0b1010101:0.4875,
0b1010110:0.4750,
0b1010111:0.4625,
0b1011000:0.4500,
0b1011001:0.4375,
0b1011010:0.4250,
0b1011011:0.4125,
0b1011100:0.4000,
0b1011101:0.3875,
0b1011110:0.3750,
0b1011111:0.3625,
0b1100000:0.3500,
0b1100001:0.3375,
0b1100010:0.3250,
0b1100011:0.3125,
0b1100100:0.3000,
0b1100101:0.2875,
0b1100110:0.2750,
0b1100111:0.2625,
0b1101000:0.2500,
0b1101001:0.2375,
0b1101010:0.2250,
0b1101011:0.2125,
0b1101100:0.2000,
0b1101101:0.1875,
0b1101110:0.1750,
0b1101111:0.1625,
0b1110000:0.1500,
0b1110001:0.1375,
0b1110010:0.1250,
0b1110011:0.1125,
0b1110100:0.1000,
0b1110101:0.0875,
0b1110110:0.0750,
0b1110111:0.0625,
0b1111000:0.0500,
0b1111001:0.0375,
0b1111010:0.0250,
0b1111011:0.0125,
0b1111100:0.0000,
0b1111101:0.0000,
0b1111110:0.0000,
0b1111111:0.0000,
}

def readInt( file, offset, length ):
   file.seek( offset )
   raw = file.read( length )
   val = 0
   for i in range( length-1, -1, -1 ):
      val = ( val << 8 ) | ord ( raw[ i ] )
   return val

def readMsr( reg, cpu=0 ):
   # pylint: disable-next=consider-using-f-string
   f = open( "/dev/cpu/%d/msr" % cpu, "rb" )
   return readInt( f, reg, 8 )

def writeMsr( reg, val, cpu=0 ):
   # pylint: disable-next=consider-using-f-string
   f = open( "/dev/cpu/%d/msr" % cpu, "rb+" )
   f.seek( reg )
   outstr = ""
   for i in range( 8 ):
      outstr += chr ( ( val >> ( i * 8 ) ) & 0xff )
   return f.write( outstr )
   
def getPstateDefaults( state ):
   # valid state are 0-4
   assert state >=0 and state < 5 # pylint: disable=chained-comparison
   f = open( "/sys/bus/pci/devices/0000:00:18.4/config", "rb" )
   return readInt( f, 0x1e0 + ( state * 4 ), 4 )

def getSlamVidMode():
   f = open( "/sys/bus/pci/devices/0000:00:18.3/config", "rb" )
   # pylint: disable-next=superfluous-parens
   return ( ( readInt( f, 0xa0, 4 ) >> 29 ) & 1 )

def getNbPstateDefaults():
   f = open( "/sys/bus/pci/devices/0000:00:18.4/config", "rb" )
   return readInt( f, 0x1f4, 4 )

def getVSSlamTime():
   f = open( "/sys/bus/pci/devices/0000:00:18.3/config", "rb" )
   val = readInt( f, 0xd8, 4 ) & 7
   waitTimeMap = { 0:10, 1:20, 2:30, 3:40, 4:60, 5:100, 6:200, 7:500 }
   return waitTimeMap[ val ]

os.system( "modprobe msr" )

assert( getSlamVidMode() == 1 ) # pylint: disable=superfluous-parens

COFVIDstatus = readMsr( 0xc0010071 )
print( hex( COFVIDstatus ) )
coreVid = ( COFVIDstatus >> 9 ) & 0x7f
nbVid = ( COFVIDstatus >> 25 ) & 0x7f
pState = ( COFVIDstatus >> 16 ) & 0x7
nbDid = ( COFVIDstatus >> 22 ) & 0x1
print( "current power state", pState )
print( "Current core VID", hex( coreVid ), VIDmap[ coreVid ], "V" )
print( "Current northbridge VID", hex( nbVid ), VIDmap[ nbVid ], "V" )
print( "Current ndDid", nbDid )

# insist pState is 0, Raven should be running in full power mode
# if not something is wrong
assert pState == 0
assert pState < 5

# MSRC001_0064 contains the VID for pstate 0
# we use the default value of this register (contained in F4x1E0 ) rather than
# the current value to calculate the margin value so we can run the program
# multiple times without having to store the initial value.
# F4x1E0 is in the extended pcie config space for 0000:00:18.4 (Link Control)
defaultVIDs = getPstateDefaults( pState )
defaultNbVIDs = getNbPstateDefaults()

msrReg = 0xc0010064 + pState 

# when testing the nbDid (northbridge divisor ID) was always 0, this effects the 
# voltage, I don't want to just let this run without testing if the nbDid is 1
assert nbDid == 0

defaultCoreVid = ( defaultVIDs >> 9 ) & 0x7f
defaultNbVid = {}
defaultNbVid[ 0 ] = ( defaultNbVIDs ) & 0x7f
defaultNbVid[ 1 ] = ( defaultNbVIDs >> 7 ) & 0x7f

print( "Default core VID", defaultCoreVid, VIDmap[ defaultCoreVid ], "V" )
print( "Default northbridge VID", hex( defaultNbVid[ nbDid ] ),
       VIDmap[ defaultNbVid[ nbDid ] ], "V" )


# calculate the new values for the VIDs
coreDelta = VIDmap[ defaultCoreVid ] * ( abs(margin) // 100 )
nbDelta = VIDmap[ defaultNbVid[ nbDid ] ] * ( abs(margin) // 100 )

if margin < 0.1 and margin > -0.1: # pylint: disable=chained-comparison
   newCoreVid = defaultCoreVid
   newNbVid = defaultNbVid[ nbDid ]
elif margin < 0:
   desiredCoreV = VIDmap[ defaultCoreVid ] - coreDelta
   desiredNbV = VIDmap[ defaultNbVid[ nbDid ] ] - nbDelta
   # walk down the vid map to find the closest voltage ABOVE the desired voltage
   newCoreVid = defaultCoreVid
   while VIDmap[ newCoreVid ] >= desiredCoreV:
      newCoreVid += 1
   newCoreVid -= 1
   newNbVid = defaultNbVid[ nbDid ]
   while VIDmap[ newNbVid ] >= desiredNbV:
      newNbVid += 1
   newNbVid -= 1
else:
   desiredCoreV = VIDmap[ defaultCoreVid ] + coreDelta
   desiredNbV = VIDmap[ defaultNbVid[ nbDid ] ] + nbDelta
   # walk down the vid map to find the closest voltage BELOW the desired voltage
   newCoreVid = defaultCoreVid
   while VIDmap[ newCoreVid ] <= desiredCoreV:
      newCoreVid -= 1
   newCoreVid += 1
   newNbVid = defaultNbVid[ nbDid ]
   while VIDmap[ newNbVid ] <= desiredNbV:
      newNbVid -= 1
   newNbVid += 1

# pylint: disable-next=consider-using-f-string
print( "\nAttempting to margin %f%%\n" % margin )
print( "Desired core VID", hex( newCoreVid ), VIDmap[ newCoreVid ], "V" )
print( "Desired nb VID", hex( newNbVid ), VIDmap[ newNbVid ], "V" )
# pylint: disable-next=consider-using-f-string
print( "\nActual core voltage %f%% of normal" % ( 100.0 * ( VIDmap[ newCoreVid ] / 
                                                   VIDmap[ defaultCoreVid ] ) ) )   
# pylint: disable-next=consider-using-f-string
print( "Actual nb voltage %f%% of normal" % ( 100.0 * ( VIDmap[ newNbVid ] / 
                                             VIDmap[ defaultNbVid[ nbDid ] ] ) ) )

# the voltage is changed by putting the bew VID into 0xc0010070
slamTime = getVSSlamTime()
print( "Slam wait time", slamTime, "ms" )
newSetting = COFVIDstatus & 0x4701FF
newSetting |= ( newCoreVid << 9 )
newSetting |= ( newNbVid << 25 )
print( "Changing software override to", hex( newSetting ) )             
writeMsr( 0xc0010070, newSetting, 0 )
writeMsr( 0xc0010070, newSetting, 1 )
time.sleep( slamTime / 1000 )

print( hex( readMsr( 0xc0010070 ) ) )

# read the settings back and verify them
COFVIDstatus = readMsr( 0xc0010071 )

coreVid = ( COFVIDstatus >> 9 ) & 0x7f
nbVid = ( COFVIDstatus >> 25 ) & 0x7f
pState = ( COFVIDstatus >> 16 ) & 0x7
nbDid = ( COFVIDstatus >> 22 ) & 0x1
print( "\nNew power state", pState )
print( "New core VID", hex( coreVid ), VIDmap[ coreVid ], "V" )
print( "New northbridge VID", hex( nbVid ), VIDmap[ nbVid ], "V" )
print( "New ndDid", nbDid )

assert coreVid == newCoreVid
assert newNbVid == nbVid

