Since you chose LM358, let’s see if we could make it work. If that’s the part you got to work with, you’ll get results now rather than whenever you get an inevitably more expensive and/or hard to get part that’d be more suited. LM358 should be available anywhere electronic components are sold, it’s one of the most basic active parts to have in one’s assortment.
LM358 is a tricky part to get going in this application. At low voltages, it doesn’t have much headroom. It has common mode across full temperature range of 0V to VCC-1.5V. It also cannot sink much current when the output is close to ground - just a couple uA across temperature range. As soon as you’re sinking 0.1mA or more, the output voltage is 1V at the minimum, although this can be overcome by adding a pull-down resistor to ground.
So, the differential amplifier is not the best solution with such an op-amp. Its common mode voltage is too high for LM358 at low voltages. But even at higher voltages, we could use a difference amplifier with gain 1 - with a twist :)
Instead of a diff-amp, let’s use an Improved Howland Current Pump, and use a load resistor to convert the current into voltage. This has benefits:
Common mode voltage can be set independently of the output current.
The output voltage range can be adjusted by scaling the current produced by the pump.
The circuit below demonstrates how it could be done.
An Example Circuit
This one is tested and it works reasonably well between 5-30V across the op-amp’s temperature range. At room temperature, it works down to 4V supply voltage. Given more time, I probably could make it work across the op-amp’s full operating voltage range of 3V-30V, but you didn’t specify the lower battery voltage limit, so I didn’t bother :)
Resistors are 1% metal film.
There’s 0.1uF || 10uF decoupling across the op-amp’s power supply pins.
The current consumption is <2mA.
The full scale output voltage range on R2 is about 0.15V to 1.0V. Properly shielded and laid out, the resolution is ~30mA with a 10 bit ADC.
CircuitLab simulator can't handle this one and is downright misleading vs. real parts, so I didn't bother with that.
The PCB or breadboard layout must keep parasitic impedances in mind. Due to high node impedances, the circuit will need to be shielded to get rid of line frequency common mode interference.
I presume that you’re also measuring the battery voltage with the Arduino. Thus, you have the information necessary to detect when the battery voltage is below 8V. When that happens, the LVMODE output should be set to a logic high value. LVMODE can be driven from 3-5V logic outputs directly, perhaps through a 100k isolation resistor to limit the noise coupling from the MCU to the analog circuitry.
The 5kOhm potentiometer needs to be trimmed for maximum output impedance of the current pump. This is done by repeatedly flipping the “TRIM/OP” switch, and observing the change in voltage on the voltmeter M1 as the switch is flipped back and forth. When the trim is done properly, the voltage will not change when switching between the TRIM/OP positions. This trimming has to be done only once, since it’s independent of operating voltage, but the trimming procedure must be carried out at battery voltage of 6V minimum. Once trimming is done, the switch should be left in the OP position.
Q1 + U1b are a 280uA current source. This produces 0.13V drop across R2, and ensures that the current pump’s input is always positive. Note that a current source’s output impedance is very high: Q1’s drain (pin 3) acts like a resistor of several MOhms, paralleled with an ideal current sink. Thus, this circuit introduces the necessary offset voltage to set the operating point (0A value) of the primary current pump, but otherwise doesn’t affect the gains, output impedance, nor DC CMRR of said pump.
D1 and D2 drop about 1V and raise the op-amp’s common mode voltage when operating at higher supply voltages. Below 8-10V, the Arduino code uses Q2 to bypass the diodes.
R8 || C2 are the load, and convert current into voltage that’s then measured by the voltmeter M1 (during trimming) and by the A/D converter of the Arduino.
U1 a with R3, R4, R5, R6, R7 and R10 form the Improved Howland Current Pump, with +/-210uA output range around the operating point, full scale, for the shunt’s full scale +/-0.1V sense voltage.
R10 is the 470Ohm resistor on the output of the op amp - I forgot to mark it with a reference number.
C1 stabilizes the op-amp. It could be reduced to 5pF.
Q1 and Q2 are 2N7000 N-channel low-power mosfets, typically used for signal switching.
This approach makes sense for LM358, but it’s not universal. An op-amp with common mode voltage extending to VCC, like TL072, could use a simpler current source circuit. A rail-to-rail input/output amplifier could use either approach of course.
As far as layout goes, symmetry and compactness is key. There’s no point to duplicating the output branch, since it couples more to the output than to the inputs, and thus I crossed it out. There’s also possibly not much point to duplicating the current source parasitics, since they couple mostly to low impedance nodes of the shunt. But the remainder of the circuit should be symmetric to maintain symmetry of parasitic capacitances. It could also be laid a bit tighter than I drew below. Probably a dummy trimmer set to midpoint would be a good idea in the top branch, even though on the layout below I’ve shorted out the dummy trimmer footprint with a trace. The nodes on the op-amp input are extremely sensitive to asymmetric impedances, and the trimmers are large enough to couple nicely to adjacent components and shielding.
It’d take some experimentation, but the idea below would be a starting point, and convey how it could be done.