The audio codec is a peripheral that enables an analog signal to be converted and coded to a digital data stream or conversely the data stream to be decoded and converted back to an analog signal (https://en.wikipedia.org/wiki/Codec). The MCBSTM32F400 evaluation board uses a CS42L52 device that is manufactured by Cirrus Logic (http://www.cirrus.com/en/products/). As, this codec is not yet included in Board Support, and as no CMSIS-compliant device driver is available, we are faced with the task of having to write our own driver.
However, this is not as daunting as it first appears because the code to set up and manage data transfer across the I2C bus can be lifted from the previous recipe (the Touch_STMPE811.c
file) and the configuration of the CS42L52 codec is described in the data sheet. The recipe to develop and test this codec driver is called codecDemo_c6v0
.
Perform the following steps to write a driver for the audio codec:
codecDemo
, and using the Run-Time Environment manager, include Board Support for the Graphic LCD. Remember to configure Software Support for CMSIS and Device as in earlier projects.codecDemo.c
. Add the boilerplate to configure clocks, and so on, and a skeleton main()
function:int main (void) { HAL_Init (); /* Init Hardware Abstraction Layer */ SystemClock_Config (); /* Config Clocks */ }
#include
files for the codecDemo.c
file:#include "stm32f4xx_hal.h" #include "cmsis_os.h" #include "codec_CS42L52.h" #include "GLCD_Config.h" #include "Board_GLCD.h" #include <stdio.h>
timer.c
and add this to the source code group. Add a function named TIM3_Initialize()
to this file:void TIM3_Initialize (void) { const uint16_t ARR_val = 7; /* enable clock for TIM3 */ RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; TIM3->CCMR1 = 0x00000070; /* Set PWM Mode 2 */ TIM3->ARR = ARR_val - 1; /* set auto-reload */ TIM3->CCR1 = 3; /* Duty cycle (~50%) */ /* Enable capture/compare on Chan 1 */ TIM3->CCER = 0x000B00001; TIM3->CR1 = 0x000B00001; /* Enable counter */ }
codec_CS42L52.c
and add this to the source code group. Copy the first 75 lines of the Touch_STMPE811.c
file to codec_CS42L52.c
, the first part of the file, including the Touch_Read()
and Touch_Write()
functions.#include
directives in the codec_CS42L52.c
file to the following:#include "CS42L52.h" #include "codec_CS42L52.h" #include "stm32f4xx_hal.h" #include "Driver_I2C.h" #include "timer.h"
TSC_I2C_ADDR
with CODEC_I2C_ADDR
.TSC_I2C_PORT
with CODEC_I2C_PORT
.TSC_I2C_ADDR
with that given in the CS42L52 data sheet, as follows:/* 7-bit I2C Address = 1001010b */ #define CODEC_I2C_ADDR 0x4A
Touch_Read()
and Touch_Write()
functions to Codec_Read()
and Codec_Write()
, respectively.typedef
to the codec_CS42L52.c
file:/* Global TypeDef - Register value */ typedef struct { uint8_t Addr; uint8_t Val; } REG_VAL;
configureCodec()
to the codec_CS42L52.c
file. The first two statements of configureCodec power the device down and wait for 10 ms. Note #define delay_ms HAL_Delay
:void configureCodec ( ) { Codec_Write(0x02, 0x01); /* Keep Codec Power-down */ delay_ms(10); for (i = 0; i < ARR_SZ(CODEC_Config_Init); i++) Codec_Write (CODEC_Config_Init[i].Addr, CODEC_Config_Init[i].Val); for (i = 0; i < ARR_SZ(CODEC_Config_Beep); i++) Codec_Write (CODEC_Config_Beep[i].Addr, CODEC_Config_Beep[i].Val); } /* configureCodec */
const
) array, as follows:/* Calculate array size */ #define ARR_SZ(x) (sizeof (x) / sizeof(x[0]))
CODEC_Config_Init
:/*** * CODEC initialization based on p38 * of CS42L52 data sheet DS680F2 *****/ REG_VAL CODEC_Config_Init[] = { {0x00, 0x99}, {0x3E, 0xBA}, {0x47, 0x80}, {0x32, 0x80}, {0x32, 0x00}, {0x00, 0x00}, };
/*** * CODEC initialization for Beep Generator * of CS42L52 (Grant Ashton) *****/ REG_VAL CODEC_Config_Beep[] ={ /* Set I2S Ser. Mstr Op Only, for Beep Gen */ {CS42L52_IFACE_CTL1, 0x80}, /* Speaker Vol B=A, MONO */ {CS42L52_PB_CTL2, 0x0A}, /* Set master vol for A */ {CS42L52_MASTERA_VOL, 0xC0}, /* Ignore jpr setting */ {CS42L52_PWRCTL3, 0xAA} };
CS42L52.h
defining symbolic names (for example, CS42L52_IFACE_CTL1
, CS42L52_PB_CTL2
, CS42L52_MASTERA_VOL
, and so on) for CS42L52 register addresses. For example, as in the following addresses:/* Register addresses */ #define CS42L52_CHIP_ID 0x01 #define CS42L52_PWRCTL1 0x02 #define CS42L52_PWRCTL2 0x03 #define CS42L52_PWRCTL3 0x04 #define CS42L52_CLK_CTL 0x05 // etc.
genMCLK()
to the codec_CS42L52.c
file:static void genMCLK(void) { GPIO_InitTypeDef GPIO_InitStruct; TIM3_Initialize(); __GPIOC_CLK_ENABLE(); /* Configure GPIO pin: PC6 */ GPIO_InitStruct.Pin = GPIO_PIN_6; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FAST; GPIO_InitStruct.Alternate = GPIO_AF2_TIM3; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); }
codecInitialize()
to the codec_CS42L52.c
file. Note that the code to configure the I2C bus is identical to the code in Touch_Initialize()
:int32_t codecInitialize() { int32_t status; /* Configure I2C */ ptrI2C->Initialize (NULL); ptrI2C->PowerControl (ARM_POWER_FULL); ptrI2C->Control (ARM_I2C_BUS_SPEED, ARM_I2C_BUS_SPEED_FAST); ptrI2C->Control (ARM_I2C_BUS_SPEED, /* Configure CODEC */ configureCodec(); genMCLK(); /* CODEC Power up */ status = Codec_Write(CS42L52_PWRCTL1, 0x00); delay_ms(10); /* Wait 10ms */ return status; }
readCodecChipID()
to the codec_CS42L52.c
file:int32_t readCodecChipID(uint8_t *val) { int32_t status = Codec_Read(1, val); return status; }
Beep()
to the codec_CS42L52.c
file:void Beep(noteInfo note ) { /* Beep off time 1.23s and volume 0dB */ Codec_Write(CS42L52_BEEP_VOL, 0x00); /* Set beep note and beep duration */ Codec_Write(CS42L52_BEEP_FREQ, note.pitch | note.duration); /* play single beep */ Codec_Write(CS42L52_BEEP_TONE_CTL, 0x40); /* Disable beep */ Codec_Write(CS42L52_BEEP_TONE_CTL, 0x00); }
timer.h
header file containing the timer.c
function prototypes.codec_CS42L52.h
header file containing the codec_CS42L52.c
function prototypes.codec_CS42L52.h
file, for example, as in the following frequencies:// Beep note frequency #define A5 0x60 #define A6 0xD0 #define B5 0x70 #define B6 0xE0 // etc.
codec_CS42L52.h
file, for example, as in the following:#define TENTH_SECOND 0x00 #define HALF_SECOND 0x01 #define ONE_SECOND 0x02 // etc.
main()
function by adding code to initialize the GLCD and Codec. Define a super-loop that outputs a beep every 0.5 seconds:int main (void) { noteInfo note = {G5, 0x02}; uint8_t codecID; char buffer[128]; HAL_Init (); /* Init Hardware Abstraction Layer */ SystemClock_Config (); /* Config Clocks */ GLCD_Initialize(); setDisplay(); showStatus(CodecInitialize()); showStatus(readCodecChipID(&codecID)); sprintf(buffer, "Chip ID: 0x%x", codecID); GLCD_DrawString (1*16, 9*24, buffer); while (1) { Beep(note); /* Play the note */ wait_delay(500); /* pause */ } /* WHILE */ }
setDisplay()
(copy the first 12 lines of the similarly-named function used in touchScreenDemo_c6v0
).showStatus( )
:void showStatus(int32_t stat) { if (stat==0) GLCD_DrawString (1*16, 8*24,"Codec OK "); else GLCD_DrawString (1*16, 8*24,"Codec FAIL"); }
codec_CS42L52.c
and timer.c
files are added to the project.RTE_Device.h
and RTX_Conf_CM.c
files, as we did for the touchScreenDemo_c6v0
folder from the Setting the RTE for the I2C Peripheral Bus recipe.A Linux driver for the CS42L52 device has been written by Cirrus Logic (http://lxr.free-electrons.com/source/sound/soc/codecs/cs42l52.c) and is freely distributed under the terms of the GNU General Public License. So, we can use this together with information from the datasheet (http://www.cirrus.com) as a basis for our driver for the MCBSTM32F400 evaluation board. As the audio codec is also connected to the I2C serial bus, the touchscreen driver that we met in the previous section provides a good template for our audio codec driver. Therefore, we will organize the codec driver in three files that mirror those of the touchscreen driver, as follows:
The code in the Codec_CS42L52.c
file first defines the I2C port that is used to communicate with the audio codec. The board schematic confirms that the touchscreen and the audio codec are connected to the same I2C port (that is, serial clock SCL = PB8 and SDA = PB9), so we configure the RTE and RTX exactly as touchScreenDemo_c6v0
using I2C port 1 (I2C1). The following preprocessor directives define the port number:
#ifndef CODEC_I2C_PORT #define CODEC_I2C_PORT 1 /* I2C Port number*/ #endif
The following preprocessor macro ensures that the ptrI2C
identifier points to the appropriate I2C driver:
/* I2C Driver */ #define _I2C_Driver_(n) Driver_I2C##n #define I2C_Driver_(n) _I2C_Driver_(n) extern ARM_DRIVER_I2C I2C_Driver_(CODEC_I2C_PORT); #define ptrI2C (&I2C_Driver_(CODEC_I2C_PORT))
The most-significant 6-bit audio codec's I2C address is shown on the board schematic and the CS42L52 datasheet as 1001012. Bit-0 reflects the logic level of the AD0 pin (that is, 0 V), and the LSB is 0 (for write operations). So, our codec's I2C address is 0x94, that is, the following:
#define CODEC_I2C_ADDR 0x4A /* I2C address */
Note that in practice, all accesses to the codec are writes because the read protocol uses an abortive write cycle first to select the codec register before reading its contents (refer to http://www.cirrus.com for further details).
We declare two functions: Codec_Write( )
and Codec_Read( )
, which mirror Touch_Write( )
and Touch_Read( )
, which were declared in Touch.c
to read and write to the audio codec.
The function named CodecInitialize()
performs three tasks. It configures the I2C interface, then it generates the 12 MHz master clock MCLK (codec Pin 37), and finally, it performs the codec's initialization sequence.
The function named genMCLK()
configures TIM3 to generate a 12-MHz clock and maps this onto the Alternate Function (AF) GPIO Port C pin 6 output. The initialization for TIM3 is similar to that described in the previous chapter except that we use the PWM mode with the capture/compare register to give an approximate 50% duty cycle. The code to configure the GPIO pin that is used to source MCLK is similar to the one that we saw in the LED_Initialize()
function.
The initialization sequence for the audio codec is given on page 38 of the CS42L52 data sheet. The initialization sequence is stored in an array named CODEC_RegInit[]
. The array entries are structured as follows:
/* Register value */ typedefstruct { uint8_tAddr; uint8_t Val; } REG_VAL;
The register names (for example, MASTERA_VOL
, and so on) are defined in the CS42L52.h
header file (note that the register names can be copied from the Linux CS42L52 driver). To prevent odd pops and crackles, the data sheet advises that the chip is powered down before initialization and then powered up. This configuration code is included in the configureCodec()
function. This function includes a nice example of a macro named ARR_SZ to compute the size of the array:
/* Calculate array size */ #define ARR_SZ(x) (sizeof (x) / sizeof(x[0]))
Note that unlike some languages, such as Java, C doesn't perform any array bounds checking, so it can be quite difficult to track errors due to incorrect array access; because of this, this macro is particularly useful.
In this recipe, we're only using the codec's beep generator (section 4.3 of the data sheet), and the values stored in the CODEC_Config_Beep[]
array are concerned with setting the codec up for this task. The remaining functions declared in the codec_CS42L52.c
file are concerned with generating beeps and adjusting the volume of the speaker. The beep generator can be configured to produce single, multiple, or continuous beeps, but we only need single beeps to play our tune. The Beep( )
function generates a single beep. This function takes an input parameter that determines the pitch and duration of the beep, and this is combined into one byte and written to the codec register address offset 0x1C in the format shown in the following table:
Bit-7 |
Bit-6 |
Bit-5 |
Bit-4 |
Bit-3 |
Bit-2 |
Bit-1 |
Bit-0 |
---|---|---|---|---|---|---|---|
FREQ3 |
FREQ2 |
FREQ1 |
FREQ0 |
ONTIME3 |
ONTIME2 |
ONTIME1 |
ONTIME0 |
3.147.126.211