The way block reward is computed today is :
CAmount blockReward = nFees + GetBlockSubsidy(pindex->nHeight, chainparams.GetConsensus());
Which means that the block reward is the total fees in the block plus the current base block subsidy.
It's definitely possible to change this line to (pseudocode) :
CAmount blockReward = std::min(nFees + GetBlockSubsidy(pindex->nHeight, chainparams.GetConsensus()), 25000000);
This will be a soft fork which doesn't allow the reward to be larger than 0.25 BTC
To explain what I mean in English, the reward is set minimum taken between the current
block subsidy + fees, or
0.25 BTC. This is a constraint on the current rules (where the base subsidy is larger than 0.25 BTC), and forward compatible with the period in the future when the base subsidy is lower than 0.25 BTC.
Adding @pieter-wuille's point from the comment, the current rules don't limit a miner in how low they can set their reward to be (where the minimum is zero), only how high. That means that a miner doesn't have to reward themselves with the maximum allowed reward. Such occurences have happened on chain before :
Rootstock accidentally set a zero amount as their reward :
The first satoshi taken out of money supply :