| 1 | /* |
| 2 | * QEMU DM163 8x3-channel constant current led driver |
| 3 | * driving columns of associated 8x8 RGB matrix. |
| 4 | * |
| 5 | * Copyright (C) 2024 Samuel Tardieu <sam@rfc1149.net> |
| 6 | * Copyright (C) 2024 Arnaud Minier <arnaud.minier@telecom-paris.fr> |
| 7 | * Copyright (C) 2024 Inès Varhol <ines.varhol@telecom-paris.fr> |
| 8 | * |
| 9 | * SPDX-License-Identifier: GPL-2.0-or-later |
| 10 | */ |
| 11 | |
| 12 | /* |
| 13 | * The reference used for the DM163 is the following : |
| 14 | * http://www.siti.com.tw/product/spec/LED/DM163.pdf |
| 15 | */ |
| 16 | |
| 17 | #include "qemu/osdep.h" |
| 18 | #include "qapi/error.h" |
| 19 | #include "migration/vmstate.h" |
| 20 | #include "hw/core/irq.h" |
| 21 | #include "hw/core/qdev-properties.h" |
| 22 | #include "hw/display/dm163.h" |
| 23 | #include "ui/console.h" |
| 24 | #include "trace.h" |
| 25 | |
| 26 | #define LED_SQUARE_SIZE 100 |
| 27 | /* Number of frames a row stays visible after being turned off. */ |
| 28 | #define ROW_PERSISTENCE 3 |
| 29 | #define TURNED_OFF_ROW (COLOR_BUFFER_SIZE - 1) |
| 30 | |
| 31 | static const VMStateDescription vmstate_dm163 = { |
| 32 | .name = TYPE_DM163, |
| 33 | .version_id = 1, |
| 34 | .minimum_version_id = 1, |
| 35 | .fields = (const VMStateField[]) { |
| 36 | VMSTATE_UINT64_ARRAY(bank0_shift_register, DM163State, 3), |
| 37 | VMSTATE_UINT64_ARRAY(bank1_shift_register, DM163State, 3), |
| 38 | VMSTATE_UINT16_ARRAY(latched_outputs, DM163State, DM163_NUM_LEDS), |
| 39 | VMSTATE_UINT16_ARRAY(outputs, DM163State, DM163_NUM_LEDS), |
| 40 | VMSTATE_UINT8(dck, DM163State), |
| 41 | VMSTATE_UINT8(en_b, DM163State), |
| 42 | VMSTATE_UINT8(lat_b, DM163State), |
| 43 | VMSTATE_UINT8(rst_b, DM163State), |
| 44 | VMSTATE_UINT8(selbk, DM163State), |
| 45 | VMSTATE_UINT8(sin, DM163State), |
| 46 | VMSTATE_UINT8(activated_rows, DM163State), |
| 47 | VMSTATE_UINT32_2DARRAY(buffer, DM163State, COLOR_BUFFER_SIZE, |
| 48 | RGB_MATRIX_NUM_COLS), |
| 49 | VMSTATE_UINT8(last_buffer_idx, DM163State), |
| 50 | VMSTATE_UINT8_ARRAY(buffer_idx_of_row, DM163State, RGB_MATRIX_NUM_ROWS), |
| 51 | VMSTATE_UINT8_ARRAY(row_persistence_delay, DM163State, |
| 52 | RGB_MATRIX_NUM_ROWS), |
| 53 | VMSTATE_END_OF_LIST() |
| 54 | } |
| 55 | }; |
| 56 | |
| 57 | static void dm163_reset_hold(Object *obj, ResetType type) |
| 58 | { |
| 59 | DM163State *s = DM163(obj); |
| 60 | |
| 61 | s->sin = 0; |
| 62 | s->dck = 0; |
| 63 | s->rst_b = 0; |
| 64 | /* Ensuring the first falling edge of lat_b isn't missed */ |
| 65 | s->lat_b = 1; |
| 66 | s->selbk = 0; |
| 67 | s->en_b = 0; |
| 68 | /* Reset stops the PWM, not the shift and latched registers. */ |
| 69 | memset(s->outputs, 0, sizeof(s->outputs)); |
| 70 | |
| 71 | s->activated_rows = 0; |
| 72 | s->redraw = 0; |
| 73 | trace_dm163_redraw(s->redraw); |
| 74 | for (unsigned i = 0; i < COLOR_BUFFER_SIZE; i++) { |
| 75 | memset(s->buffer[i], 0, sizeof(s->buffer[0])); |
| 76 | } |
| 77 | s->last_buffer_idx = 0; |
| 78 | memset(s->buffer_idx_of_row, TURNED_OFF_ROW, sizeof(s->buffer_idx_of_row)); |
| 79 | memset(s->row_persistence_delay, 0, sizeof(s->row_persistence_delay)); |
| 80 | } |
| 81 | |
| 82 | static void dm163_dck_gpio_handler(void *opaque, int line, int new_state) |
| 83 | { |
| 84 | DM163State *s = opaque; |
| 85 | |
| 86 | if (new_state && !s->dck) { |
| 87 | /* |
| 88 | * On raising dck, sample selbk to get the bank to use, and |
| 89 | * sample sin for the bit to enter into the bank shift buffer. |
| 90 | */ |
| 91 | uint64_t *sb = |
| 92 | s->selbk ? s->bank1_shift_register : s->bank0_shift_register; |
| 93 | /* Output the outgoing bit on sout */ |
| 94 | const bool sout = (s->selbk ? sb[2] & MAKE_64BIT_MASK(63, 1) : |
| 95 | sb[2] & MAKE_64BIT_MASK(15, 1)) != 0; |
| 96 | qemu_set_irq(s->sout, sout); |
| 97 | /* Enter sin into the shift buffer */ |
| 98 | sb[2] = (sb[2] << 1) | ((sb[1] >> 63) & 1); |
| 99 | sb[1] = (sb[1] << 1) | ((sb[0] >> 63) & 1); |
| 100 | sb[0] = (sb[0] << 1) | s->sin; |
| 101 | } |
| 102 | |
| 103 | s->dck = new_state; |
| 104 | trace_dm163_dck(new_state); |
| 105 | } |
| 106 | |
| 107 | static void dm163_propagate_outputs(DM163State *s) |
| 108 | { |
| 109 | s->last_buffer_idx = (s->last_buffer_idx + 1) % RGB_MATRIX_NUM_ROWS; |
| 110 | /* Values are output when reset is high and enable is low. */ |
| 111 | if (s->rst_b && !s->en_b) { |
| 112 | memcpy(s->outputs, s->latched_outputs, sizeof(s->outputs)); |
| 113 | } else { |
| 114 | memset(s->outputs, 0, sizeof(s->outputs)); |
| 115 | } |
| 116 | for (unsigned x = 0; x < RGB_MATRIX_NUM_COLS; x++) { |
| 117 | /* Grouping the 3 RGB channels in a pixel value */ |
| 118 | const uint16_t b = extract16(s->outputs[3 * x + 0], 6, 8); |
| 119 | const uint16_t g = extract16(s->outputs[3 * x + 1], 6, 8); |
| 120 | const uint16_t r = extract16(s->outputs[3 * x + 2], 6, 8); |
| 121 | uint32_t rgba = 0; |
| 122 | |
| 123 | trace_dm163_channels(3 * x + 2, r); |
| 124 | trace_dm163_channels(3 * x + 1, g); |
| 125 | trace_dm163_channels(3 * x + 0, b); |
| 126 | |
| 127 | rgba = deposit32(rgba, 0, 8, r); |
| 128 | rgba = deposit32(rgba, 8, 8, g); |
| 129 | rgba = deposit32(rgba, 16, 8, b); |
| 130 | |
| 131 | /* Led values are sent from the last one to the first one */ |
| 132 | s->buffer[s->last_buffer_idx][RGB_MATRIX_NUM_COLS - x - 1] = rgba; |
| 133 | } |
| 134 | for (unsigned row = 0; row < RGB_MATRIX_NUM_ROWS; row++) { |
| 135 | if (s->activated_rows & (1 << row)) { |
| 136 | s->buffer_idx_of_row[row] = s->last_buffer_idx; |
| 137 | s->redraw |= (1 << row); |
| 138 | trace_dm163_redraw(s->redraw); |
| 139 | } |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | static void dm163_en_b_gpio_handler(void *opaque, int line, int new_state) |
| 144 | { |
| 145 | DM163State *s = opaque; |
| 146 | |
| 147 | s->en_b = new_state; |
| 148 | dm163_propagate_outputs(s); |
| 149 | trace_dm163_en_b(new_state); |
| 150 | } |
| 151 | |
| 152 | static uint8_t dm163_bank0(const DM163State *s, uint8_t led) |
| 153 | { |
| 154 | /* |
| 155 | * Bank 0 uses 6 bits per led, so a value may be stored accross |
| 156 | * two uint64_t entries. |
| 157 | */ |
| 158 | const uint8_t low_bit = 6 * led; |
| 159 | const uint8_t low_word = low_bit / 64; |
| 160 | const uint8_t high_word = (low_bit + 5) / 64; |
| 161 | const uint8_t low_shift = low_bit % 64; |
| 162 | |
| 163 | if (low_word == high_word) { |
| 164 | /* Simple case: the value belongs to one entry. */ |
| 165 | return extract64(s->bank0_shift_register[low_word], low_shift, 6); |
| 166 | } |
| 167 | |
| 168 | const uint8_t nb_bits_in_low_word = 64 - low_shift; |
| 169 | const uint8_t nb_bits_in_high_word = 6 - nb_bits_in_low_word; |
| 170 | |
| 171 | const uint64_t bits_in_low_word = \ |
| 172 | extract64(s->bank0_shift_register[low_word], low_shift, |
| 173 | nb_bits_in_low_word); |
| 174 | const uint64_t bits_in_high_word = \ |
| 175 | extract64(s->bank0_shift_register[high_word], 0, |
| 176 | nb_bits_in_high_word); |
| 177 | uint8_t val = 0; |
| 178 | |
| 179 | val = deposit32(val, 0, nb_bits_in_low_word, bits_in_low_word); |
| 180 | val = deposit32(val, nb_bits_in_low_word, nb_bits_in_high_word, |
| 181 | bits_in_high_word); |
| 182 | |
| 183 | return val; |
| 184 | } |
| 185 | |
| 186 | static uint8_t dm163_bank1(const DM163State *s, uint8_t led) |
| 187 | { |
| 188 | const uint64_t entry = s->bank1_shift_register[led / RGB_MATRIX_NUM_COLS]; |
| 189 | return extract64(entry, 8 * (led % RGB_MATRIX_NUM_COLS), 8); |
| 190 | } |
| 191 | |
| 192 | static void dm163_lat_b_gpio_handler(void *opaque, int line, int new_state) |
| 193 | { |
| 194 | DM163State *s = opaque; |
| 195 | |
| 196 | if (s->lat_b && !new_state) { |
| 197 | for (int led = 0; led < DM163_NUM_LEDS; led++) { |
| 198 | s->latched_outputs[led] = dm163_bank0(s, led) * dm163_bank1(s, led); |
| 199 | } |
| 200 | dm163_propagate_outputs(s); |
| 201 | } |
| 202 | |
| 203 | s->lat_b = new_state; |
| 204 | trace_dm163_lat_b(new_state); |
| 205 | } |
| 206 | |
| 207 | static void dm163_rst_b_gpio_handler(void *opaque, int line, int new_state) |
| 208 | { |
| 209 | DM163State *s = opaque; |
| 210 | |
| 211 | s->rst_b = new_state; |
| 212 | dm163_propagate_outputs(s); |
| 213 | trace_dm163_rst_b(new_state); |
| 214 | } |
| 215 | |
| 216 | static void dm163_selbk_gpio_handler(void *opaque, int line, int new_state) |
| 217 | { |
| 218 | DM163State *s = opaque; |
| 219 | |
| 220 | s->selbk = new_state; |
| 221 | trace_dm163_selbk(new_state); |
| 222 | } |
| 223 | |
| 224 | static void dm163_sin_gpio_handler(void *opaque, int line, int new_state) |
| 225 | { |
| 226 | DM163State *s = opaque; |
| 227 | |
| 228 | s->sin = new_state; |
| 229 | trace_dm163_sin(new_state); |
| 230 | } |
| 231 | |
| 232 | static void dm163_rows_gpio_handler(void *opaque, int line, int new_state) |
| 233 | { |
| 234 | DM163State *s = opaque; |
| 235 | |
| 236 | if (new_state) { |
| 237 | s->activated_rows |= (1 << line); |
| 238 | s->buffer_idx_of_row[line] = s->last_buffer_idx; |
| 239 | s->redraw |= (1 << line); |
| 240 | trace_dm163_redraw(s->redraw); |
| 241 | } else { |
| 242 | s->activated_rows &= ~(1 << line); |
| 243 | s->row_persistence_delay[line] = ROW_PERSISTENCE; |
| 244 | } |
| 245 | trace_dm163_activated_rows(s->activated_rows); |
| 246 | } |
| 247 | |
| 248 | static void dm163_invalidate_display(void *opaque) |
| 249 | { |
| 250 | DM163State *s = (DM163State *)opaque; |
| 251 | s->redraw = 0xFF; |
| 252 | trace_dm163_redraw(s->redraw); |
| 253 | } |
| 254 | |
| 255 | static void update_row_persistence_delay(DM163State *s, unsigned row) |
| 256 | { |
| 257 | if (s->row_persistence_delay[row]) { |
| 258 | s->row_persistence_delay[row]--; |
| 259 | } else { |
| 260 | /* |
| 261 | * If the ROW_PERSISTENCE delay is up, |
| 262 | * the row is turned off. |
| 263 | */ |
| 264 | s->buffer_idx_of_row[row] = TURNED_OFF_ROW; |
| 265 | s->redraw |= (1 << row); |
| 266 | trace_dm163_redraw(s->redraw); |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | static uint32_t *update_display_of_row(DM163State *s, uint32_t *dest, |
| 271 | unsigned row) |
| 272 | { |
| 273 | for (unsigned _ = 0; _ < LED_SQUARE_SIZE; _++) { |
| 274 | for (int x = RGB_MATRIX_NUM_COLS * LED_SQUARE_SIZE - 1; x >= 0; x--) { |
| 275 | /* UI layer guarantees that there's 32 bits per pixel (Mar 2024) */ |
| 276 | *dest++ = s->buffer[s->buffer_idx_of_row[row]][x / LED_SQUARE_SIZE]; |
| 277 | } |
| 278 | } |
| 279 | |
| 280 | qemu_console_update(s->console, 0, LED_SQUARE_SIZE * row, |
| 281 | RGB_MATRIX_NUM_COLS * LED_SQUARE_SIZE, LED_SQUARE_SIZE); |
| 282 | s->redraw &= ~(1 << row); |
| 283 | trace_dm163_redraw(s->redraw); |
| 284 | |
| 285 | return dest; |
| 286 | } |
| 287 | |
| 288 | static bool dm163_update_display(void *opaque) |
| 289 | { |
| 290 | DM163State *s = (DM163State *)opaque; |
| 291 | DisplaySurface *surface = qemu_console_surface(s->console); |
| 292 | uint32_t *dest; |
| 293 | |
| 294 | dest = surface_data(surface); |
| 295 | for (unsigned row = 0; row < RGB_MATRIX_NUM_ROWS; row++) { |
| 296 | update_row_persistence_delay(s, row); |
| 297 | if (!extract8(s->redraw, row, 1)) { |
| 298 | dest += LED_SQUARE_SIZE * LED_SQUARE_SIZE * RGB_MATRIX_NUM_COLS; |
| 299 | continue; |
| 300 | } |
| 301 | dest = update_display_of_row(s, dest, row); |
| 302 | } |
| 303 | |
| 304 | return true; |
| 305 | } |
| 306 | |
| 307 | static const GraphicHwOps dm163_ops = { |
| 308 | .invalidate = dm163_invalidate_display, |
| 309 | .gfx_update = dm163_update_display, |
| 310 | }; |
| 311 | |
| 312 | static void dm163_realize(DeviceState *dev, Error **errp) |
| 313 | { |
| 314 | DM163State *s = DM163(dev); |
| 315 | |
| 316 | qdev_init_gpio_in(dev, dm163_rows_gpio_handler, RGB_MATRIX_NUM_ROWS); |
| 317 | qdev_init_gpio_in(dev, dm163_sin_gpio_handler, 1); |
| 318 | qdev_init_gpio_in(dev, dm163_dck_gpio_handler, 1); |
| 319 | qdev_init_gpio_in(dev, dm163_rst_b_gpio_handler, 1); |
| 320 | qdev_init_gpio_in(dev, dm163_lat_b_gpio_handler, 1); |
| 321 | qdev_init_gpio_in(dev, dm163_selbk_gpio_handler, 1); |
| 322 | qdev_init_gpio_in(dev, dm163_en_b_gpio_handler, 1); |
| 323 | qdev_init_gpio_out_named(dev, &s->sout, "sout", 1); |
| 324 | |
| 325 | s->console = qemu_graphic_console_create(dev, 0, &dm163_ops, s); |
| 326 | qemu_console_resize(s->console, RGB_MATRIX_NUM_COLS * LED_SQUARE_SIZE, |
| 327 | RGB_MATRIX_NUM_ROWS * LED_SQUARE_SIZE); |
| 328 | } |
| 329 | |
| 330 | static void dm163_class_init(ObjectClass *klass, const void *data) |
| 331 | { |
| 332 | DeviceClass *dc = DEVICE_CLASS(klass); |
| 333 | ResettableClass *rc = RESETTABLE_CLASS(klass); |
| 334 | |
| 335 | dc->desc = "DM163 8x3-channel constant current LED driver"; |
| 336 | dc->vmsd = &vmstate_dm163; |
| 337 | dc->realize = dm163_realize; |
| 338 | rc->phases.hold = dm163_reset_hold; |
| 339 | set_bit(DEVICE_CATEGORY_DISPLAY, dc->categories); |
| 340 | } |
| 341 | |
| 342 | static const TypeInfo dm163_types[] = { |
| 343 | { |
| 344 | .name = TYPE_DM163, |
| 345 | .parent = TYPE_DEVICE, |
| 346 | .instance_size = sizeof(DM163State), |
| 347 | .class_init = dm163_class_init |
| 348 | } |
| 349 | }; |
| 350 | |
| 351 | DEFINE_TYPES(dm163_types) |