I can't really comment on SIESTA but here is how it's done in ATK.
If you want to simulate a truly charged cell, you give the keyword charge=XXX. In this case you need to use the Multipole Poisson solver, and ATK will internally apply the relevant corrections.
For doping, you can see some of the latest tutorials for more details. In short, you give the opposite charge as your intended doping as a "compensation charge" on each atom. For instance, if your cell volume is V (can be computed via bulk_configuration.bravaisLattice().unitCellVolume()) and you want a doping of 1e20 cm^-3 and you have N atoms, then the compensation charge on each atom should be 1e20*V(in cm^3)/N. Example code for doped bulk Si:
lattice = FaceCenteredCubic(5.431*Angstrom)
elements = [Silicon, Silicon]
fractional_coordinates = [[ 0. , 0. , 0. ],
[ 0.25, 0.25, 0.25]]
bulk_configuration = BulkConfiguration(
bravais_lattice=lattice,
elements=elements,
fractional_coordinates=fractional_coordinates
)
# Convert doping to total charge in unit cell volume
# and set up compensation charge
cell_volume = lattice.unitCellVolume()
cm = Units.m/100
doping = 1e20/(cm**3)
charge = float(doping*cell_volume)
n_si = len(bulk_configuration.elements())
# Note opposite sign!!!
compensation_charge = -charge/n_si
bulk_configuration.setExternalPotential(AtomicCompensationCharge((Silicon, compensation_charge)))
The charge will then automatically be balanced to create a neutral system, so you shold not give the "charge" keyword in this case (unless you want a charged, doped cell).