/* * awinic AW20108 / AW20072 / AW20054 / AW20036 LED Matrix Driver * * originally written by true in 2022 * while sleep deprived for a super constrained mcu * * some bugs fixed for defcon 32 on aug 6-7, 2024 * * driver assumptions: * - rows and columns are used in order on the chip, lowest to highest * (if any are skipped, just skip this data in your buffer) * - duty cycle will be set according to the column count * - all AW20xxx chips will operate on the same i2c bus * - the only i2c write routine does not have register arguments * * driver notices: * - updates only happen one column at a time, and are blocking * (future version may implement a callback when each column is done) * - this driver has not yet implemented the pattern controller * - this driver has not yet implemented the GAIN register, so only operates 8-bit * (will be implemented later to allow for beyond-8-bit operation) * - this driver has not yet implemented FADEDIM mode * - all transfers result in copies of data, which is wasteful * (future version may transfer LED data directly from the buffer) * * if you need anything different, write it yourself */ #include "aw20xxx.h" #define AW20X_THIS_PAGE (aw->state & AW20X_STATE_PAGE_MASK) #define AW20X_SET_PAGE(x) if (AW20X_THIS_PAGE != (x)) { \ aw20x_page(aw, x); \ aw->state &= ~AW20X_STATE_PAGE_MASK; \ aw->state |= x; } static uint8_t aw_buf[25]; // enough bytes for register and single column FADEDIM update (1 + (12*2)) void aw20x_page(struct AW20x *aw, uint8_t page) { // the datasheet isn't clear on this. the default is zero, and only the lower // three bits are specified. yet the DS says to send 0xCy where y is the page bits. // we'll just do what the DS says even though it contradicts itself. aw_buf[0] = 0xc0 | (page & AW20X_PAGE_MASK); AW20X_I2C_writereg(aw->addr, AW20X_REG_PAGE, aw_buf, 1); } void aw20x_init(struct AW20x *aw, uint8_t addr, uint8_t cols, uint8_t rows, uint8_t imax) { // set config register as specified aw->addr = addr; aw->cols = cols; aw->rows = rows; aw->config = imax & AW20X_CONF_IMAX_MASK; // ensure we are on page 0 to start aw20x_page(aw, 0); while (AW20X_I2C_busy()); // wake up aw20x_sleep(aw, 0); // enabled columns aw_buf[0] = cols - 1; AW20X_I2C_writereg(aw->addr, AW20X_REG_SIZE, aw_buf, 1); // general config aw_buf[0] = imax & AW20X_CONF_IMAX_MASK; AW20X_I2C_writereg(aw->addr, AW20X_REG_GCCR, aw_buf, 1); while (AW20X_I2C_busy()); } void aw20x_sleep(struct AW20x *aw, uint8_t sleep) { // make sure we're on the config page AW20X_SET_PAGE(AW20X_PAGE0_CONFIG); // don't touch the buffer until we are allowed while (AW20X_I2C_busy()); // send sleep bit aw_buf[0] = sleep ? AW20X_SLPCR_SLEEP : 0; AW20X_I2C_writereg(aw->addr, AW20X_REG_SLPCR, aw_buf, 1); // set state if (sleep) aw->state |= AW20X_STATE_SLEEP_MASK; else aw->state &= ~AW20X_STATE_SLEEP_MASK; // burn some cycles if we woke up if (!sleep) PLATFORM_INIT_DELAY(); } void aw20x_imax(struct AW20x *aw, uint8_t imax) { AW20X_SET_PAGE(AW20X_PAGE0_CONFIG); // todo: implement } /* * sends LED values to the chip */ void aw20x_commit_fade(struct AW20x *aw) { uint8_t c; uint8_t row; // make sure we're on the fade page AW20X_SET_PAGE(AW20X_PAGE2_FADE); // don't touch the buffer until we are allowed while (AW20X_I2C_busy()); row = 0; for (c = 0; c < aw->cols; c++) { // write to chip AW20X_I2C_writereg(aw->addr, row, aw->fade + row, aw->rows); while (AW20X_I2C_busy()); row += AW20X_MAX_ROWS; } } void aw20x_commit_dim(struct AW20x *aw) { // todo: implement } /* * sets all LEDs to the specified 6-bit DIM value. * used when just using FADE and 8-bit mode * to set initial and fine tune from IMAX the output current. */ void aw20x_commit_dim_global(struct AW20x *aw, uint8_t dim) { uint8_t i; uint8_t row = 0; // ceil if (dim > 0x3f) dim = 0x3f; // make sure we're on the dim page AW20X_SET_PAGE(AW20X_PAGE1_DIM); // don't touch the buffer until we are allowed while (AW20X_I2C_busy()); // clear buffer for (i = 0; i <= aw->rows; i++) aw_buf[i] = dim; // send buffer for each column for (i = 0; i < aw->cols; i++) { AW20X_I2C_writereg(aw->addr, row, aw_buf, aw->rows); while (AW20X_I2C_busy()); row += AW20X_MAX_ROWS; } } void aw20x_commit_fadedim(struct AW20x *aw) { } static inline void aw20x_led_on(struct AW20x *aw, uint8_t first, uint8_t last, uint8_t on_bit) { } /* * enables LEDs based on user LED count, zero-indexed * AW20036 would be 0-35, AW00054 would be 0-53, and so on * for example, LEDs 8-12 on AW20054 would enable C0R8, C1R0, C1R1, C1R2 * * todo: * - read current state, and apply bitfields to the currently active state * - allow bypassing the readback for faster operation (such as setting all LEDs on at startup) * - make this more efficient (36 LEDs takes ~0.3ms on a 48MHz PIC!) */ void aw20x_led_enable(struct AW20x *aw, uint8_t first, uint8_t last) { uint8_t c, r; uint8_t boff; // make sure we're on the config page AW20X_SET_PAGE(AW20X_PAGE0_CONFIG); // don't touch the buffer until we are allowed while (AW20X_I2C_busy()); // bits are stored 6 bits per byte, 2 bytes per column, one bit for each row // we only want to touch bits that exist on the chip and in the correct order boff = 0; for (c = 0; c < (aw->cols * 2); c++) { aw_buf[c] = 0; for (r = 0; r < AW20X_MAX_LEDON_BITS; r++) { if (r+boff >= first) { if (r+boff <= last) { aw_buf[c] |= (1 << r); } } } boff += AW20X_MAX_LEDON_BITS; } AW20X_I2C_writereg(aw->addr, AW20X_REG_LEDON0, aw_buf, c); } /* * disables LEDs based on user LED count, zero-indexed * (for example, LEDs 8-12 on AW20054 would enable C0R8, C0R9, C1R0, C1R1) */ void aw20x_led_disable(struct AW20x *aw, uint8_t first, uint8_t last) { }