| 1 | /* |
| 2 | * Allwinner H3 SDRAM Controller emulation |
| 3 | * |
| 4 | * Copyright (C) 2019 Niek Linnenbank <nieklinnenbank@gmail.com> |
| 5 | * |
| 6 | * This program is free software: you can redistribute it and/or modify |
| 7 | * it under the terms of the GNU General Public License as published by |
| 8 | * the Free Software Foundation, either version 2 of the License, or |
| 9 | * (at your option) any later version. |
| 10 | * |
| 11 | * This program is distributed in the hope that it will be useful, |
| 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | * GNU General Public License for more details. |
| 15 | * |
| 16 | * You should have received a copy of the GNU General Public License |
| 17 | * along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 18 | */ |
| 19 | |
| 20 | #include "qemu/osdep.h" |
| 21 | #include "qemu/units.h" |
| 22 | #include "qemu/error-report.h" |
| 23 | #include "hw/core/sysbus.h" |
| 24 | #include "migration/vmstate.h" |
| 25 | #include "qemu/log.h" |
| 26 | #include "qemu/module.h" |
| 27 | #include "system/address-spaces.h" |
| 28 | #include "hw/core/qdev-properties.h" |
| 29 | #include "qapi/error.h" |
| 30 | #include "hw/misc/allwinner-h3-dramc.h" |
| 31 | #include "trace.h" |
| 32 | |
| 33 | #define REG_INDEX(offset) (offset / sizeof(uint32_t)) |
| 34 | |
| 35 | /* DRAMCOM register offsets */ |
| 36 | enum { |
| 37 | REG_DRAMCOM_CR = 0x0000, /* Control Register */ |
| 38 | }; |
| 39 | |
| 40 | /* DRAMCTL register offsets */ |
| 41 | enum { |
| 42 | REG_DRAMCTL_PIR = 0x0000, /* PHY Initialization Register */ |
| 43 | REG_DRAMCTL_PGSR = 0x0010, /* PHY General Status Register */ |
| 44 | REG_DRAMCTL_STATR = 0x0018, /* Status Register */ |
| 45 | }; |
| 46 | |
| 47 | /* DRAMCTL register flags */ |
| 48 | enum { |
| 49 | REG_DRAMCTL_PGSR_INITDONE = (1 << 0), |
| 50 | }; |
| 51 | |
| 52 | enum { |
| 53 | REG_DRAMCTL_STATR_ACTIVE = (1 << 0), |
| 54 | }; |
| 55 | |
| 56 | static void allwinner_h3_dramc_map_rows(AwH3DramCtlState *s, uint8_t row_bits, |
| 57 | uint8_t bank_bits, uint16_t page_size) |
| 58 | { |
| 59 | /* |
| 60 | * This function simulates row addressing behavior when bootloader |
| 61 | * software attempts to detect the amount of available SDRAM. In U-Boot |
| 62 | * the controller is configured with the widest row addressing available. |
| 63 | * Then a pattern is written to RAM at an offset on the row boundary size. |
| 64 | * If the value read back equals the value read back from the |
| 65 | * start of RAM, the bootloader knows the amount of row bits. |
| 66 | * |
| 67 | * This function inserts a mirrored memory region when the configured row |
| 68 | * bits are not matching the actual emulated memory, to simulate the |
| 69 | * same behavior on hardware as expected by the bootloader. |
| 70 | */ |
| 71 | uint8_t row_bits_actual = 0; |
| 72 | |
| 73 | /* Calculate the actual row bits using the ram_size property */ |
| 74 | for (uint8_t i = 8; i < 12; i++) { |
| 75 | if (1 << i == s->ram_size) { |
| 76 | row_bits_actual = i + 3; |
| 77 | break; |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | if (s->ram_size == (1 << (row_bits - 3))) { |
| 82 | /* When row bits is the expected value, remove the mirror */ |
| 83 | memory_region_set_enabled(&s->row_mirror_alias, false); |
| 84 | trace_allwinner_h3_dramc_rowmirror_disable(); |
| 85 | |
| 86 | } else if (row_bits_actual) { |
| 87 | /* Row bits not matching ram_size, install the rows mirror */ |
| 88 | hwaddr row_mirror = s->ram_addr + ((1ULL << (row_bits_actual + |
| 89 | bank_bits)) * page_size); |
| 90 | |
| 91 | memory_region_set_enabled(&s->row_mirror_alias, true); |
| 92 | memory_region_set_address(&s->row_mirror_alias, row_mirror); |
| 93 | |
| 94 | trace_allwinner_h3_dramc_rowmirror_enable(row_mirror); |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | static uint64_t allwinner_h3_dramcom_read(void *opaque, hwaddr offset, |
| 99 | unsigned size) |
| 100 | { |
| 101 | const AwH3DramCtlState *s = AW_H3_DRAMC(opaque); |
| 102 | const uint32_t idx = REG_INDEX(offset); |
| 103 | |
| 104 | if (idx >= AW_H3_DRAMCOM_REGS_NUM) { |
| 105 | qemu_log_mask(LOG_GUEST_ERROR, "%s: out-of-bounds offset 0x%04x\n", |
| 106 | __func__, (uint32_t)offset); |
| 107 | return 0; |
| 108 | } |
| 109 | |
| 110 | trace_allwinner_h3_dramcom_read(offset, s->dramcom[idx], size); |
| 111 | |
| 112 | return s->dramcom[idx]; |
| 113 | } |
| 114 | |
| 115 | static void allwinner_h3_dramcom_write(void *opaque, hwaddr offset, |
| 116 | uint64_t val, unsigned size) |
| 117 | { |
| 118 | AwH3DramCtlState *s = AW_H3_DRAMC(opaque); |
| 119 | const uint32_t idx = REG_INDEX(offset); |
| 120 | |
| 121 | trace_allwinner_h3_dramcom_write(offset, val, size); |
| 122 | |
| 123 | if (idx >= AW_H3_DRAMCOM_REGS_NUM) { |
| 124 | qemu_log_mask(LOG_GUEST_ERROR, "%s: out-of-bounds offset 0x%04x\n", |
| 125 | __func__, (uint32_t)offset); |
| 126 | return; |
| 127 | } |
| 128 | |
| 129 | switch (offset) { |
| 130 | case REG_DRAMCOM_CR: /* Control Register */ |
| 131 | allwinner_h3_dramc_map_rows(s, ((val >> 4) & 0xf) + 1, |
| 132 | ((val >> 2) & 0x1) + 2, |
| 133 | 1 << (((val >> 8) & 0xf) + 3)); |
| 134 | break; |
| 135 | default: |
| 136 | break; |
| 137 | }; |
| 138 | |
| 139 | s->dramcom[idx] = (uint32_t) val; |
| 140 | } |
| 141 | |
| 142 | static uint64_t allwinner_h3_dramctl_read(void *opaque, hwaddr offset, |
| 143 | unsigned size) |
| 144 | { |
| 145 | const AwH3DramCtlState *s = AW_H3_DRAMC(opaque); |
| 146 | const uint32_t idx = REG_INDEX(offset); |
| 147 | |
| 148 | if (idx >= AW_H3_DRAMCTL_REGS_NUM) { |
| 149 | qemu_log_mask(LOG_GUEST_ERROR, "%s: out-of-bounds offset 0x%04x\n", |
| 150 | __func__, (uint32_t)offset); |
| 151 | return 0; |
| 152 | } |
| 153 | |
| 154 | trace_allwinner_h3_dramctl_read(offset, s->dramctl[idx], size); |
| 155 | |
| 156 | return s->dramctl[idx]; |
| 157 | } |
| 158 | |
| 159 | static void allwinner_h3_dramctl_write(void *opaque, hwaddr offset, |
| 160 | uint64_t val, unsigned size) |
| 161 | { |
| 162 | AwH3DramCtlState *s = AW_H3_DRAMC(opaque); |
| 163 | const uint32_t idx = REG_INDEX(offset); |
| 164 | |
| 165 | trace_allwinner_h3_dramctl_write(offset, val, size); |
| 166 | |
| 167 | if (idx >= AW_H3_DRAMCTL_REGS_NUM) { |
| 168 | qemu_log_mask(LOG_GUEST_ERROR, "%s: out-of-bounds offset 0x%04x\n", |
| 169 | __func__, (uint32_t)offset); |
| 170 | return; |
| 171 | } |
| 172 | |
| 173 | switch (offset) { |
| 174 | case REG_DRAMCTL_PIR: /* PHY Initialization Register */ |
| 175 | s->dramctl[REG_INDEX(REG_DRAMCTL_PGSR)] |= REG_DRAMCTL_PGSR_INITDONE; |
| 176 | s->dramctl[REG_INDEX(REG_DRAMCTL_STATR)] |= REG_DRAMCTL_STATR_ACTIVE; |
| 177 | break; |
| 178 | default: |
| 179 | break; |
| 180 | } |
| 181 | |
| 182 | s->dramctl[idx] = (uint32_t) val; |
| 183 | } |
| 184 | |
| 185 | static uint64_t allwinner_h3_dramphy_read(void *opaque, hwaddr offset, |
| 186 | unsigned size) |
| 187 | { |
| 188 | const AwH3DramCtlState *s = AW_H3_DRAMC(opaque); |
| 189 | const uint32_t idx = REG_INDEX(offset); |
| 190 | |
| 191 | if (idx >= AW_H3_DRAMPHY_REGS_NUM) { |
| 192 | qemu_log_mask(LOG_GUEST_ERROR, "%s: out-of-bounds offset 0x%04x\n", |
| 193 | __func__, (uint32_t)offset); |
| 194 | return 0; |
| 195 | } |
| 196 | |
| 197 | trace_allwinner_h3_dramphy_read(offset, s->dramphy[idx], size); |
| 198 | |
| 199 | return s->dramphy[idx]; |
| 200 | } |
| 201 | |
| 202 | static void allwinner_h3_dramphy_write(void *opaque, hwaddr offset, |
| 203 | uint64_t val, unsigned size) |
| 204 | { |
| 205 | AwH3DramCtlState *s = AW_H3_DRAMC(opaque); |
| 206 | const uint32_t idx = REG_INDEX(offset); |
| 207 | |
| 208 | trace_allwinner_h3_dramphy_write(offset, val, size); |
| 209 | |
| 210 | if (idx >= AW_H3_DRAMPHY_REGS_NUM) { |
| 211 | qemu_log_mask(LOG_GUEST_ERROR, "%s: out-of-bounds offset 0x%04x\n", |
| 212 | __func__, (uint32_t)offset); |
| 213 | return; |
| 214 | } |
| 215 | |
| 216 | s->dramphy[idx] = (uint32_t) val; |
| 217 | } |
| 218 | |
| 219 | static const MemoryRegionOps allwinner_h3_dramcom_ops = { |
| 220 | .read = allwinner_h3_dramcom_read, |
| 221 | .write = allwinner_h3_dramcom_write, |
| 222 | .endianness = DEVICE_LITTLE_ENDIAN, |
| 223 | .valid = { |
| 224 | .min_access_size = 4, |
| 225 | .max_access_size = 4, |
| 226 | }, |
| 227 | .impl.min_access_size = 4, |
| 228 | }; |
| 229 | |
| 230 | static const MemoryRegionOps allwinner_h3_dramctl_ops = { |
| 231 | .read = allwinner_h3_dramctl_read, |
| 232 | .write = allwinner_h3_dramctl_write, |
| 233 | .endianness = DEVICE_LITTLE_ENDIAN, |
| 234 | .valid = { |
| 235 | .min_access_size = 4, |
| 236 | .max_access_size = 4, |
| 237 | }, |
| 238 | .impl.min_access_size = 4, |
| 239 | }; |
| 240 | |
| 241 | static const MemoryRegionOps allwinner_h3_dramphy_ops = { |
| 242 | .read = allwinner_h3_dramphy_read, |
| 243 | .write = allwinner_h3_dramphy_write, |
| 244 | .endianness = DEVICE_LITTLE_ENDIAN, |
| 245 | .valid = { |
| 246 | .min_access_size = 4, |
| 247 | .max_access_size = 4, |
| 248 | }, |
| 249 | .impl.min_access_size = 4, |
| 250 | }; |
| 251 | |
| 252 | static void allwinner_h3_dramc_reset(DeviceState *dev) |
| 253 | { |
| 254 | AwH3DramCtlState *s = AW_H3_DRAMC(dev); |
| 255 | |
| 256 | /* Set default values for registers */ |
| 257 | memset(&s->dramcom, 0, sizeof(s->dramcom)); |
| 258 | memset(&s->dramctl, 0, sizeof(s->dramctl)); |
| 259 | memset(&s->dramphy, 0, sizeof(s->dramphy)); |
| 260 | } |
| 261 | |
| 262 | static void allwinner_h3_dramc_realize(DeviceState *dev, Error **errp) |
| 263 | { |
| 264 | AwH3DramCtlState *s = AW_H3_DRAMC(dev); |
| 265 | |
| 266 | /* Only power of 2 RAM sizes from 256MiB up to 2048MiB are supported */ |
| 267 | for (uint8_t i = 8; i < 13; i++) { |
| 268 | if (1 << i == s->ram_size) { |
| 269 | break; |
| 270 | } else if (i == 12) { |
| 271 | error_report("%s: ram-size %u MiB is not supported", |
| 272 | __func__, s->ram_size); |
| 273 | exit(1); |
| 274 | } |
| 275 | } |
| 276 | |
| 277 | /* Setup row mirror mappings */ |
| 278 | memory_region_init_ram(&s->row_mirror, OBJECT(s), |
| 279 | "allwinner-h3-dramc.row-mirror", |
| 280 | 4 * KiB, &error_abort); |
| 281 | memory_region_add_subregion_overlap(get_system_memory(), s->ram_addr, |
| 282 | &s->row_mirror, 10); |
| 283 | |
| 284 | memory_region_init_alias(&s->row_mirror_alias, OBJECT(s), |
| 285 | "allwinner-h3-dramc.row-mirror-alias", |
| 286 | &s->row_mirror, 0, 4 * KiB); |
| 287 | memory_region_add_subregion_overlap(get_system_memory(), |
| 288 | s->ram_addr + 1 * MiB, |
| 289 | &s->row_mirror_alias, 10); |
| 290 | memory_region_set_enabled(&s->row_mirror_alias, false); |
| 291 | } |
| 292 | |
| 293 | static void allwinner_h3_dramc_init(Object *obj) |
| 294 | { |
| 295 | SysBusDevice *sbd = SYS_BUS_DEVICE(obj); |
| 296 | AwH3DramCtlState *s = AW_H3_DRAMC(obj); |
| 297 | |
| 298 | /* DRAMCOM registers */ |
| 299 | memory_region_init_io(&s->dramcom_iomem, OBJECT(s), |
| 300 | &allwinner_h3_dramcom_ops, s, |
| 301 | TYPE_AW_H3_DRAMC, 4 * KiB); |
| 302 | sysbus_init_mmio(sbd, &s->dramcom_iomem); |
| 303 | |
| 304 | /* DRAMCTL registers */ |
| 305 | memory_region_init_io(&s->dramctl_iomem, OBJECT(s), |
| 306 | &allwinner_h3_dramctl_ops, s, |
| 307 | TYPE_AW_H3_DRAMC, 4 * KiB); |
| 308 | sysbus_init_mmio(sbd, &s->dramctl_iomem); |
| 309 | |
| 310 | /* DRAMPHY registers */ |
| 311 | memory_region_init_io(&s->dramphy_iomem, OBJECT(s), |
| 312 | &allwinner_h3_dramphy_ops, s, |
| 313 | TYPE_AW_H3_DRAMC, 4 * KiB); |
| 314 | sysbus_init_mmio(sbd, &s->dramphy_iomem); |
| 315 | } |
| 316 | |
| 317 | static const Property allwinner_h3_dramc_properties[] = { |
| 318 | DEFINE_PROP_UINT64("ram-addr", AwH3DramCtlState, ram_addr, 0x0), |
| 319 | DEFINE_PROP_UINT32("ram-size", AwH3DramCtlState, ram_size, 256 * MiB), |
| 320 | }; |
| 321 | |
| 322 | static const VMStateDescription allwinner_h3_dramc_vmstate = { |
| 323 | .name = "allwinner-h3-dramc", |
| 324 | .version_id = 1, |
| 325 | .minimum_version_id = 1, |
| 326 | .fields = (const VMStateField[]) { |
| 327 | VMSTATE_UINT32_ARRAY(dramcom, AwH3DramCtlState, AW_H3_DRAMCOM_REGS_NUM), |
| 328 | VMSTATE_UINT32_ARRAY(dramctl, AwH3DramCtlState, AW_H3_DRAMCTL_REGS_NUM), |
| 329 | VMSTATE_UINT32_ARRAY(dramphy, AwH3DramCtlState, AW_H3_DRAMPHY_REGS_NUM), |
| 330 | VMSTATE_END_OF_LIST() |
| 331 | } |
| 332 | }; |
| 333 | |
| 334 | static void allwinner_h3_dramc_class_init(ObjectClass *klass, const void *data) |
| 335 | { |
| 336 | DeviceClass *dc = DEVICE_CLASS(klass); |
| 337 | |
| 338 | device_class_set_legacy_reset(dc, allwinner_h3_dramc_reset); |
| 339 | dc->vmsd = &allwinner_h3_dramc_vmstate; |
| 340 | dc->realize = allwinner_h3_dramc_realize; |
| 341 | device_class_set_props(dc, allwinner_h3_dramc_properties); |
| 342 | } |
| 343 | |
| 344 | static const TypeInfo allwinner_h3_dramc_info = { |
| 345 | .name = TYPE_AW_H3_DRAMC, |
| 346 | .parent = TYPE_SYS_BUS_DEVICE, |
| 347 | .instance_init = allwinner_h3_dramc_init, |
| 348 | .instance_size = sizeof(AwH3DramCtlState), |
| 349 | .class_init = allwinner_h3_dramc_class_init, |
| 350 | }; |
| 351 | |
| 352 | static void allwinner_h3_dramc_register(void) |
| 353 | { |
| 354 | type_register_static(&allwinner_h3_dramc_info); |
| 355 | } |
| 356 | |
| 357 | type_init(allwinner_h3_dramc_register) |