diff options
Diffstat (limited to 'firmware/target/arm/as3525/ascodec-as3525.c')
-rw-r--r-- | firmware/target/arm/as3525/ascodec-as3525.c | 571 |
1 files changed, 451 insertions, 120 deletions
diff --git a/firmware/target/arm/as3525/ascodec-as3525.c b/firmware/target/arm/as3525/ascodec-as3525.c index ec25a415a5..e144f07ed4 100644 --- a/firmware/target/arm/as3525/ascodec-as3525.c +++ b/firmware/target/arm/as3525/ascodec-as3525.c @@ -50,7 +50,7 @@ #include "system.h" #include "as3525.h" #include "i2c.h" -#include "logf.h" +#include <linked_list.h> #define I2C2_DATA *((volatile unsigned int *)(I2C_AUDIO_BASE + 0x00)) #define I2C2_SLAD0 *((volatile unsigned int *)(I2C_AUDIO_BASE + 0x04)) @@ -78,234 +78,495 @@ #define I2C2_IRQ_RXOVER 0x10 #define I2C2_IRQ_ACKTIMEO 0x80 +#define REQ_UNFINISHED 0 +#define REQ_FINISHED 1 +#define REQ_RETRY 2 -static struct mutex as_mtx; - -#if CONFIG_CHARGING -static bool chg_status = false; -static bool endofch = false; +#ifdef DEBUG +#define IFDEBUG(x) x +#else +#define IFDEBUG(x) #endif -/* returns true when busy */ -static inline bool i2c_busy(void) +#define ASCODEC_REQ_READ 0 +#define ASCODEC_REQ_WRITE 1 + +/* + * How many bytes we using in struct ascodec_request for the data buffer. + * 4 fits the alignment best right now. + * We don't actually use more than 3 at the moment (when reading interrupts) + * Upper limit would be 255 since DACNT is 8 bits! + */ +#define ASCODEC_REQ_MAXLEN 4 + +struct ascodec_request; +typedef void (ascodec_cb_fn)(struct ascodec_request *req); + +struct ascodec_request { + /* standard request members */ + struct ll_node node; /* request list link (first!) */ + unsigned char type; /* reqest type (read or write) */ + unsigned char index; /* initial i2c sub address */ + unsigned char cnt; /* bytes remaining */ + unsigned char data[ASCODEC_REQ_MAXLEN]; /* actual I/O data */ + + /* members relevant when a callback is specified (callback != NULL) */ + ascodec_cb_fn *callback; /* pointer to callback function */ + intptr_t cbdata; /* data for callback function */ + int len_done; /* amount actually transferred */ +}; + +/* I2C driver data */ +static struct mutex as_mtx; +static struct ll_head req_list; +static unsigned char *req_data_ptr; +#define REQ_FIRST ((struct ascodec_request *)req_list.head) + +/* INT_AUDIO interrupt data */ +static void ascodec_int_audio_cb(struct ascodec_request *req); +void INT_I2C_AUDIO(void); +static struct ascodec_request as_audio_req; +static struct semaphore adc_done_sem; +static unsigned long ascodec_enrd0_shadow = 0; + +static void ascodec_wait_cb(struct ascodec_request *req); + +/** --debugging help-- **/ + +#ifdef DEBUG +/* counters for debugging INT_AUDIO */ +static struct int_audio_counters { + int int_audio; + int int_chg_finished; + int int_chg_insert; + int int_chg_remove; + int int_usb_insert; + int int_usb_remove; + int int_rtc; + int int_adc; +} int_audio_counters; +#endif /* DEBUG */ + +#define COUNT_INT(x) IFDEBUG((int_audio_counters.int_##x)++) + + +/** --stock request and callback functionality -- **/ + +/* init for common request data (call before submitting) */ +static inline void ascodec_req_init(struct ascodec_request *req, int type, + unsigned int index, unsigned int cnt) { - return (I2C2_SR & 1); + req->type = type; + req->index = index; + req->cnt = cnt; } -void i2c_init(void) +/* stock no-wait init for request (use any callback and data) */ +static inline void ascodec_async_init(struct ascodec_request *req, + ascodec_cb_fn *callback, intptr_t cbdata) { + /* cbdata is unused if no callback is used */ + if ((req->callback = callback)) + req->cbdata = cbdata; } -static void i2c2_init(void) +/* initialize the stock completion callback */ +static inline void ascodec_wait_init(struct ascodec_request *req, + struct semaphore *completep) { - int prescaler; - - /* prescaler for i2c clock */ - prescaler = AS3525_I2C_PRESCALER; - I2C2_CPSR0 = prescaler & 0xFF; /* 8 lsb */ - I2C2_CPSR1 = (prescaler >> 8) & 0x3; /* 2 msb */ + req->callback = ascodec_wait_cb; + req->cbdata = (intptr_t)completep; + semaphore_init(completep, 1, 0); +} - /* set i2c slave address of codec part */ - I2C2_SLAD0 = AS3514_I2C_ADDR << 1; +/* caller waits here when using ascodec_wait_cb to do synchronous transfers */ +static void ascodec_wait(struct ascodec_request *req) +{ + struct semaphore *completep = (struct semaphore *)req->cbdata; + int timeout = TIMEOUT_BLOCK; - I2C2_CNTRL = I2C2_CNTRL_DEFAULT; + if (!irq_enabled() || !is_thread_context()) { + timeout = TIMEOUT_NOBLOCK; /* poll semaphore, no block */ + } + while (semaphore_wait(completep, timeout) == OBJ_WAIT_TIMEDOUT) { + /* pump the i2c interrupts ourselves (only waiting can do this!) */ + if (I2C2_MIS) { + INT_I2C_AUDIO(); + } + } } -/* initialises the internal i2c bus and prepares for transfers to the codec */ -void ascodec_init(void) +/* stock callback used in order to wait for a transfer to complete */ +static void ascodec_wait_cb(struct ascodec_request *req) { + struct semaphore *completep = (struct semaphore *)req->cbdata; + semaphore_release(completep); +} - mutex_init(&as_mtx); + +/**-- I2C2 interrupt handling --**/ + +/* start the controller on the next transfer */ +static void ascodec_start_req(struct ascodec_request *req) +{ + int unmask = 0; /* enable clock */ bitset32(&CGU_PERI, CGU_I2C_AUDIO_MASTER_CLOCK_ENABLE); - i2c2_init(); + /* start transfer */ + I2C2_SADDR = req->index; + + if (req->type == ASCODEC_REQ_READ) { + req_data_ptr = req->data; + I2C2_CNTRL = I2C2_CNTRL_DEFAULT | I2C2_CNTRL_READ; + unmask = I2C2_IRQ_RXFULL|I2C2_IRQ_RXOVER; + } else { + req_data_ptr = &req->data[1]; + I2C2_CNTRL = I2C2_CNTRL_DEFAULT | I2C2_CNTRL_WRITE; + I2C2_DATA = req->data[0]; + unmask = I2C2_IRQ_TXEMPTY|I2C2_IRQ_ACKTIMEO; + } - /* Generate irq for usb+charge status change */ - ascodec_write(AS3514_IRQ_ENRD0, -#if CONFIG_CHARGING /* m200v4 can't charge */ - IRQ_CHGSTAT | IRQ_ENDOFCH | -#endif - IRQ_USBSTAT); + I2C2_DACNT = req->cnt; + I2C2_IMR |= unmask; /* enable interrupts */ +} -#if CONFIG_CPU == AS3525v2 - /* XIRQ = IRQ, active low reset signal, 6mA push-pull output */ - ascodec_write_pmu(0x1a, 3, (1<<2)|3); /* 1A-3 = Out_Cntr3 register */ - /* reset for compatible with old bootloader */ - ascodec_write(AS3514_IRQ_ENRD2, 0x0); -#else - /* Generate irq for push-pull, active high, irq on rtc+adc change */ - ascodec_write(AS3514_IRQ_ENRD2, IRQ_PUSHPULL | IRQ_HIGHACTIVE); -#endif +/* send the next bytes or read bytes received */ +static int ascodec_continue_req(struct ascodec_request *req, int irq_status) +{ + if ((irq_status & (I2C2_IRQ_RXOVER|I2C2_IRQ_ACKTIMEO)) > 0) { + /* some error occured, restart the request */ + return REQ_RETRY; + } - VIC_INT_ENABLE = INTERRUPT_AUDIO; + if (req->type == ASCODEC_REQ_READ && + (irq_status & I2C2_IRQ_RXFULL) > 0) { + *req_data_ptr++ = I2C2_DATA; + } else { + if (req->cnt > 1 && + (irq_status & I2C2_IRQ_TXEMPTY) > 0) { + I2C2_DATA = *req_data_ptr++; + } + } - /* detect if USB was connected at startup since there is no transition */ - int data = ascodec_read(AS3514_IRQ_ENRD0); + req->index++; + if (--req->cnt > 0) + return REQ_UNFINISHED; - if(data & USB_STATUS) - usb_insert_int(); + return REQ_FINISHED; +} -#if CONFIG_CHARGING - chg_status = data & CHG_STATUS; -#endif +/* complete the request and call the completion callback, if any */ +static void ascodec_finish_req(struct ascodec_request *req) +{ + /* + * Wait if still busy, unfortunately this happens since + * the controller is running at a low divisor, so it's + * still busy when we serviced the interrupt. + * I tried upping the i2c speed to 4MHz which + * made the number of busywait cycles much smaller + * (none for reads and only a few for writes), + * but who knows if it's reliable at that frequency. ;) + * For one thing, 8MHz doesn't work, so 4MHz is likely + * borderline. + * In general writes need much more wait cycles than reads + * for some reason, possibly because we read the data register + * for reads, which will likely block the processor while + * the i2c controller responds to the register read. + */ + while (I2C2_SR & 1); + + if (req->callback) { + req->len_done = req_data_ptr - req->data; + req->callback(req); + } } -/* returns false if transfer incomplete */ -static bool i2c2_transfer(void) +/* ISR for I2C2 */ +void INT_I2C_AUDIO(void) { - static int try = 0; + struct ascodec_request *req = REQ_FIRST; - /* wait for transfer*/ - int i = 10000; - while (I2C2_DACNT != 0 && i--); + int irq_status = I2C2_MIS; + int status = ascodec_continue_req(req, irq_status); - if (!i) { - if (try == 5) - panicf("I2C2 reset failed"); + I2C2_INT_CLR = irq_status; /* clear interrupt status */ - logf("reset I2C2 %d", try); + if (status != REQ_UNFINISHED) { + /* mask rx/tx interrupts */ + I2C2_IMR &= ~(I2C2_IRQ_TXEMPTY|I2C2_IRQ_RXFULL| + I2C2_IRQ_RXOVER|I2C2_IRQ_ACKTIMEO); - i2c2_init(); + if (status == REQ_FINISHED) + ascodec_finish_req(req); - try++; - return false; - } + int oldlevel = disable_irq_save(); /* IRQs are stacked */ + + /* + * If status == REQ_RETRY, this will restart the request from where + * it left off because we didn't remove it from the request list + */ - try = 0; - return true; + if (status == REQ_FINISHED) { + ll_remove_next(&req_list, NULL); + } + + req = REQ_FIRST; + if (req == NULL) { + /* disable clock */ + bitclr32(&CGU_PERI, CGU_I2C_AUDIO_MASTER_CLOCK_ENABLE); + } else { + ascodec_start_req(req); + } + + restore_irq(oldlevel); + } } -void ascodec_write(unsigned int index, unsigned int value) + +/** --Routines for reading and writing data on the bus-- **/ + +/* add the request to the queue */ +static void ascodec_submit(struct ascodec_request *req) { - ascodec_lock(); + int oldlevel = disable_irq_save(); + + ll_insert_last(&req_list, &req->node); + + if (REQ_FIRST == req) { + /* first on list? start driver */ + ascodec_start_req(req); + } + + restore_irq(oldlevel); +} +/* + * The request struct passed in must be allocated statically. + * If you call ascodec_async_write from different places, each + * call needs it's own request struct. + */ +static void ascodec_async_write(struct ascodec_request *req, + unsigned int index, unsigned int value) +{ #ifndef HAVE_AS3543 if (index == AS3514_CVDD_DCDC3) /* prevent setting of the LREG_CP_not bit */ value &= ~(1 << 5); #endif - do { - /* wait if still busy */ - while (i2c_busy()); + ascodec_req_init(req, ASCODEC_REQ_WRITE, index, 1); + req->data[0] = value; + ascodec_submit(req); +} + +void ascodec_write(unsigned int index, unsigned int value) +{ + struct ascodec_request req; + struct semaphore complete; - /* start transfer */ - I2C2_SADDR = index; - I2C2_CNTRL &= ~(1 << 1); - I2C2_DATA = value; - I2C2_DACNT = 1; + ascodec_wait_init(&req, &complete); + ascodec_async_write(&req, index, value); + ascodec_wait(&req); +} - } while (!i2c2_transfer()); +/* + * The request struct passed in must be allocated statically. + * If you call ascodec_async_read from different places, each + * call needs it's own request struct. + * If len is bigger than ASCODEC_REQ_MAXLEN it will be + * set to ASCODEC_REQ_MAXLEN. + */ +static void ascodec_async_read(struct ascodec_request *req, + unsigned int index, unsigned int len) +{ + if (len > ASCODEC_REQ_MAXLEN) + len = ASCODEC_REQ_MAXLEN; /* can't fit more in one request */ - ascodec_unlock(); + ascodec_req_init(req, ASCODEC_REQ_READ, index, len); + ascodec_submit(req); } +/* read data synchronously */ int ascodec_read(unsigned int index) { - int data; + struct ascodec_request req; + struct semaphore complete; - ascodec_lock(); + ascodec_wait_init(&req, &complete); + ascodec_async_read(&req, index, 1); + ascodec_wait(&req); - do { - /* wait if still busy */ - while (i2c_busy()); + return req.data[0]; +} - /* start transfer */ - I2C2_SADDR = index; - I2C2_CNTRL |= (1 << 1); - I2C2_DACNT = 1; +/* read an array of bytes */ +void ascodec_readbytes(unsigned int index, unsigned int len, unsigned char *data) +{ + struct ascodec_request req; + struct semaphore complete; - } while (!i2c2_transfer()); + ascodec_wait_init(&req, &complete); - data = I2C2_DATA; + /* index and cnt will be filled in later, just use 0 */ + ascodec_req_init(&req, ASCODEC_REQ_READ, 0, 0); - ascodec_unlock(); + int i = 0; - return data; -} + while (len > 0) { + int cnt = MIN(len, ASCODEC_REQ_MAXLEN); -void ascodec_readbytes(unsigned int index, unsigned int len, unsigned char *data) -{ - unsigned int i; + req.index = index; + req.cnt = cnt; + + ascodec_submit(&req); + ascodec_wait(&req); + + for (int j = 0; j < cnt; j++) + data[i++] = req.data[j]; - for(i = 0; i < len; i++) - data[i] = ascodec_read(index+i); + len -= cnt; + index += cnt; + } } #if CONFIG_CPU == AS3525v2 +/* write special PMU subregisters */ void ascodec_write_pmu(unsigned int index, unsigned int subreg, unsigned int value) { - ascodec_lock(); + struct ascodec_request reqs[2]; + struct semaphore complete; + + ascodec_async_init(&reqs[0], NULL, 0); + ascodec_wait_init(&reqs[1], &complete); + + int oldstatus = disable_irq_save(); /* we submit consecutive requests to make sure no operations happen on the * i2c bus between selecting the sub register and writing to it */ - ascodec_write(AS3543_PMU_ENABLE, 8 | subreg); - ascodec_write(index, value); + ascodec_async_write(&reqs[0], AS3543_PMU_ENABLE, 8 | subreg); + ascodec_async_write(&reqs[1], index, value); + restore_irq(oldstatus); - ascodec_unlock(); + /* Wait for second request to finish */ + ascodec_wait(&reqs[1]); } +/* read special PMU subregisters */ int ascodec_read_pmu(unsigned int index, unsigned int subreg) { - ascodec_lock(); + struct ascodec_request reqs[2]; + struct semaphore complete; + + ascodec_async_init(&reqs[0], NULL, 0); + ascodec_wait_init(&reqs[1], &complete); + + int oldstatus = disable_irq_save(); /* we submit consecutive requests to make sure no operations happen on the * i2c bus between selecting the sub register and reading it */ - ascodec_write(AS3543_PMU_ENABLE, subreg); - int ret = ascodec_read(index); + ascodec_async_write(&reqs[0], AS3543_PMU_ENABLE, subreg); + ascodec_async_read(&reqs[1], index, 1); + restore_irq(oldstatus); - ascodec_unlock(); + /* Wait for second request to finish */ + ascodec_wait(&reqs[1]); - return ret; + return reqs[1].data[0]; } #endif /* CONFIG_CPU == AS3525v2 */ -void INT_AUDIO(void) +/* callback that receives results of reading INT_AUDIO status register */ +static void ascodec_int_audio_cb(struct ascodec_request *req) { - int oldstatus = disable_irq_save(); - int data = ascodec_read(AS3514_IRQ_ENRD0); + unsigned char * const data = req->data; -#if CONFIG_CHARGING - if (data & CHG_ENDOFCH) { /* chg finished */ - endofch = true; + if (UNLIKELY(req->len_done != 3)) { /* some error happened? */ + panicf("INT_AUDIO callback got %d regs", req->len_done); } - chg_status = data & CHG_STATUS; -#endif + if (data[0] & CHG_ENDOFCH) { /* chg finished */ + COUNT_INT(chg_finished); + ascodec_enrd0_shadow |= CHG_ENDOFCH; + } + + if (data[0] & CHG_CHANGED) { /* chg status changed */ + if (data[0] & CHG_STATUS) { + COUNT_INT(chg_insert); + ascodec_enrd0_shadow |= CHG_STATUS; + } else { + COUNT_INT(chg_remove); + ascodec_enrd0_shadow &= ~CHG_STATUS; + } + } - if (data & USB_CHANGED) { /* usb status changed */ - if (data & USB_STATUS) { + if (data[0] & USB_CHANGED) { /* usb status changed */ + if (data[0] & USB_STATUS) { + COUNT_INT(usb_insert); usb_insert_int(); } else { + COUNT_INT(usb_remove); usb_remove_int(); } } - restore_irq(oldstatus); + if (data[2] & IRQ_RTC) { /* rtc irq */ + /* + * Can be configured for once per second or once per minute, + * default is once per second + */ + COUNT_INT(rtc); + } + + if (data[2] & IRQ_ADC) { /* adc finished */ + COUNT_INT(adc); + semaphore_release(&adc_done_sem); + } + + VIC_INT_ENABLE = INTERRUPT_AUDIO; +} + +/* ISR for all various ascodec events */ +void INT_AUDIO(void) +{ + VIC_INT_EN_CLEAR = INTERRUPT_AUDIO; + COUNT_INT(audio); + + ascodec_async_read(&as_audio_req, AS3514_IRQ_ENRD0, 3); +} + +/* wait for ADC to finish conversion */ +void ascodec_wait_adc_finished(void) +{ + semaphore_wait(&adc_done_sem, TIMEOUT_BLOCK); } #if CONFIG_CHARGING +/* read sticky end-of-charge bit and clear it */ bool ascodec_endofch(void) { - int oldstatus = disable_irq_save(); - bool ret = endofch; - endofch = false; - restore_irq(oldstatus); + int oldlevel = disable_irq_save(); + + bool ret = ascodec_enrd0_shadow & CHG_ENDOFCH; + ascodec_enrd0_shadow &= ~CHG_ENDOFCH; /* clear interrupt */ + + restore_irq(oldlevel); return ret; } +/* read the presence state of the charger */ bool ascodec_chg_status(void) { - return chg_status; + return ascodec_enrd0_shadow & CHG_STATUS; } void ascodec_monitor_endofch(void) { - /* already enabled */ + /* end of charge status interrupt already enabled */ } +/* write charger control register */ void ascodec_write_charger(int value) { #if CONFIG_CPU == AS3525 @@ -315,6 +576,7 @@ void ascodec_write_charger(int value) #endif } +/* read charger control register */ int ascodec_read_charger(void) { #if CONFIG_CPU == AS3525 @@ -325,6 +587,15 @@ int ascodec_read_charger(void) } #endif /* CONFIG_CHARGING */ +/* + * NOTE: + * After the conversion to interrupts, ascodec_(lock|unlock) are only used by + * adc-as3514.c to protect against other threads corrupting the result by using + * the ADC at the same time. + * Concurrent ascodec_(async_)?(read|write) calls are instead protected + * because ascodec_submit() is atomic and concurrent requests will wait + * in the queue until the current request is finished. + */ void ascodec_lock(void) { mutex_lock(&as_mtx); @@ -334,3 +605,63 @@ void ascodec_unlock(void) { mutex_unlock(&as_mtx); } + + +/** --Startup initialization-- **/ + +void i2c_init(void) +{ + /* required function but called too late for our needs */ +} + +/* initialises the internal i2c bus and prepares for transfers to the codec */ +void ascodec_init(void) +{ + int prescaler; + + ll_init(&req_list); + mutex_init(&as_mtx); + ascodec_async_init(&as_audio_req, ascodec_int_audio_cb, 0); + semaphore_init(&adc_done_sem, 1, 0); + + /* enable clock */ + bitset32(&CGU_PERI, CGU_I2C_AUDIO_MASTER_CLOCK_ENABLE); + + /* prescaler for i2c clock */ + prescaler = AS3525_I2C_PRESCALER; + I2C2_CPSR0 = prescaler & 0xFF; /* 8 lsb */ + I2C2_CPSR1 = (prescaler >> 8) & 0x3; /* 2 msb */ + + /* set i2c slave address of codec part */ + I2C2_SLAD0 = AS3514_I2C_ADDR << 1; + + I2C2_CNTRL = I2C2_CNTRL_DEFAULT; + + I2C2_IMR = 0x00; /* disable interrupts */ + I2C2_INT_CLR = I2C2_RIS; /* clear interrupt status */ + VIC_INT_ENABLE = INTERRUPT_I2C_AUDIO; + VIC_INT_ENABLE = INTERRUPT_AUDIO; + + /* detect if USB was connected at startup since there is no transition */ + ascodec_enrd0_shadow = ascodec_read(AS3514_IRQ_ENRD0); + if(ascodec_enrd0_shadow & USB_STATUS) + usb_insert_int(); + + /* Generate irq for usb+charge status change */ + ascodec_write(AS3514_IRQ_ENRD0, +#if CONFIG_CHARGING /* m200v4 can't charge */ + IRQ_CHGSTAT | IRQ_ENDOFCH | +#endif + IRQ_USBSTAT); + +#if CONFIG_CPU == AS3525v2 + /* XIRQ = IRQ, active low reset signal, 6mA push-pull output */ + ascodec_write_pmu(0x1a, 3, (1<<2)|3); /* 1A-3 = Out_Cntr3 register */ + /* Generate irq on (rtc,) adc change */ + ascodec_write(AS3514_IRQ_ENRD2, /*IRQ_RTC |*/ IRQ_ADC); +#else + /* Generate irq for push-pull, active high, irq on rtc+adc change */ + ascodec_write(AS3514_IRQ_ENRD2, IRQ_PUSHPULL | IRQ_HIGHACTIVE | + /*IRQ_RTC |*/ IRQ_ADC); +#endif +} |