📜 ⬆️ ⬇️

Sound output to Arduino Due

In this article I will talk about how to output audio to the Arduino Due without active use of the processor.

After receiving a fee and experimenting with sketches, I realized that I would not write firmware in a standard IDE and began to look for an alternative. My choice was Atmel Studio version 6.0. I really liked this IDE, which is made on the basis of Visual Studio 2010. I liked it especially because it all worked out of the box. In the wizard to create a new project, I chose the Arduino Due board, chose the project a la “Hello World” (blinking LED), compiled and launched. Particularly pleased that there were no layers and libraries hidden from me. The firmware was completely assembled from the source code, it eventually bribed me, and I stayed at the Atmel Studio. By the way, Visual Assist is already built into it, which makes writing code even more comfortable.

And so, I was faced with the task of outputting audio through DACC (Analog Converter Controller), but without 100% of the processor load. Ideally, I wanted to send the next portion of data to the DACC and forget about it until it is necessary to send a new portion.
For this I had to use PDC (Peripheral DMA Controller) and TC (Timer Counter). In the end, everything turned out to be quite simple, but I suffered a little before it all worked. If interested, then I ask under the cat.

By itself, the sound output is not any difficulty. We simply write the next 12-bit value to the DACC channel, pausing between these operations. An example of audio output is available in Atmel Studio (DAC Sinewave Example). In this example, the system timer is initialized (System Timer), in the interrupt handler of which the amplitude value is written to the DACC channel. Also, at first glance, the processor should not be too heavy, but for a frequency of 32 kHz, we already need 32,000 interrupts per second, not to mention that for other purposes the system timer will not work, because it will have to reconfigure each time to the desired frequency.
')
In order to send data to the DACC in portions, you can use the PDC, but it sends data without a pause, i.e expects when the next half-word will be taken by iron and immediately sends the next one. To output the sound is not enough for us, because data should be recorded with a specific frequency. Here, one of the DACC modes of operation comes to our rescue, namely, he knows how to use TC as a trigger. So, it remains for us to tune in the desired TC frequency and tell the DACC to use one of the TC channels.

TC is set to waveform mode. To do this, a counter value is written to the Ra register, at which a signal appears at the output, and a value (in our case, Ra + 1), at which the output signal is reset, is written to the Rc register. Thus, we generate a signal of a given frequency, which is transmitted to the input of the DACC.

When using the PDC, it must be borne in mind that it does not know how to transfer data from ROM, only from RAM.
The sound processing module is located in the sound.c file.
//   static uint16_t g_buffer[4096]; //   ,        static volatile uint16_t g_buffer_head, g_buffer_tail, g_buffer_size; //         static inline uint16_t get_continuous_data_size_irq(void) { return g_buffer_head <= g_buffer_tail ? g_buffer_tail - g_buffer_head : ARRAYSIZE(g_buffer) - g_buffer_head; } //       static inline uint16_t get_buffer_capacity(void) { return g_buffer_size; } //     static uint16_t copy_in_buffer(const uint16_t * data, uint16_t size) { uint16_t tail = g_buffer_tail; //        uint32_t to_copy = min(size, ARRAYSIZE(g_buffer) - tail); memcpy((void *)(g_buffer + tail), (void *)data, (size_t)(to_copy << 1)); if (to_copy < size) { //        memcpy(g_buffer, data + to_copy, (size - to_copy) << 1); tail = size - to_copy; } else { //        tail += size; } return tail; } //     ,    static inline void move_head_irq(uint16_t size) { g_buffer_head += size; g_buffer_size += size; if (g_buffer_head >= ARRAYSIZE(g_buffer) && g_buffer_head > g_buffer_tail) g_buffer_head -= ARRAYSIZE(g_buffer); } //      PDC //    static pdc_packet_t g_pdc_packet; //     PDC static volatile uint32_t g_pdc_packet_size; //   0,  PDC    static volatile uint8_t g_pdc_active; //    DACC static inline void disable_irq(void) { dacc_disable_interrupt(DACC, DACC_IER_ENDTX); } //  PDC ,     DACC static inline void restore_irq(void) { if (g_pdc_active) dacc_enable_interrupt(DACC, DACC_IER_ENDTX); } #define DACC_PDC_PACKET_MAX_SIZE 512 //   DACC // ,      PDC void DACC_Handler(void) { //   uint32_t status = dacc_get_interrupt_status(DACC); if (status & DACC_IER_ENDTX) { //   ,   move_head_irq(g_pdc_packet_size); uint16_t data_size = get_continuous_data_size_irq(); if (data_size == 0) { //   g_pdc_packet_size = 0; g_pdc_active = 0; //    DACC dacc_disable_interrupt(DACC, DACC_IDR_ENDTX); } else { //  PDC     g_pdc_packet.ul_addr = (uint32_t)(g_buffer + g_buffer_head); g_pdc_packet_size = min(DACC_PDC_PACKET_MAX_SIZE, data_size); g_pdc_packet.ul_size = g_pdc_packet_size; //  pdc_tx_init(PDC_DACC, &g_pdc_packet, NULL); } } } //   0 TC      static void tc_adjust_frequency(uint16_t frequency) { uint32_t divider = sysclk_get_cpu_hz() / frequency; divider >>= 1; tc_write_ra(TC0, 0, divider); tc_write_rc(TC0, 0, divider + 1); } //   void sound_init_hardware(void) { //   TC sysclk_enable_peripheral_clock(ID_TC0); //  TC0   waveform tc_init(TC0, 0, TC_CMR_TCCLKS_TIMER_CLOCK1 | TC_CMR_WAVE /* Waveform mode is enabled */ | TC_CMR_ACPA_SET /* RA Compare Effect: set */ | TC_CMR_ACPC_CLEAR /* RC Compare Effect: clear */ | TC_CMR_CPCTRG /* UP mode with automatic trigger on RC Compare */ ); //  -   TC,    tc_adjust_frequency(8000); //   tc_start(TC0, 0); //   DACC sysclk_enable_peripheral_clock(ID_DACC); //    DACC dacc_reset(DACC); //  DACC   "half word" dacc_set_transfer_mode(DACC, 0); //    dacc_set_power_save(DACC, 0, 0); //   TC0    dacc_set_trigger(DACC, 1); //  TAG     DACC dacc_set_channel_selection(DACC, 1); //    DACC dacc_enable_channel(DACC, 1); //     dacc_set_analog_control(DACC, DACC_ACR_IBCTLCH0(0x02) | DACC_ACR_IBCTLCH1(0x02) | DACC_ACR_IBCTLDACCORE(0x01)); //    DACC   NVIC_DisableIRQ(DACC_IRQn); NVIC_ClearPendingIRQ(DACC_IRQn); NVIC_EnableIRQ(DACC_IRQn); //   PDC     DACC pdc_enable_transfer(PDC_DACC, PERIPH_PTCR_TXTEN); //    g_pdc_packet_size = 0; g_pdc_active = 0; } //        void sound_start(uint16_t frequency) { //     sound_stop(); //   tc_adjust_frequency(frequency); } //    void sound_stop(void) { //   DACC  dacc_disable_interrupt(DACC, DACC_IER_ENDTX); g_pdc_packet_size = 0; g_buffer_head = g_buffer_tail = 0; g_buffer_size = ARRAYSIZE(g_buffer); g_pdc_active = 0; } //          uint8_t sound_data(const uint16_t * data, uint16_t size) { //    uint16_t capacity = get_buffer_capacity(); if (capacity < size) return 0; //     uint16_t tail = copy_in_buffer(data, size); //    disable_irq(); g_buffer_tail = tail; g_buffer_size -= size; if (g_buffer_head >= ARRAYSIZE(g_buffer)) g_buffer_head -= ARRAYSIZE(g_buffer); g_pdc_active = 1; restore_irq(); //    return 1; } 


I wrote a small example of use.
 #include <asf.h> #include "sound.h" #define STRING_HEADER "-- Sound firmware --\r\n" \ "-- "BOARD_NAME" --\r\n" \ "-- Compiled: "__DATE__" "__TIME__" --\r" static void configure_console(void) { const usart_serial_options_t uart_serial_options = { .baudrate = 115200, .paritytype = UART_MR_PAR_NO }; sysclk_enable_peripheral_clock(ID_UART); stdio_serial_init(UART, &uart_serial_options); } #include "sound_data.c" static uint32_t g_sound_pos = 0; int main (void) { sysclk_init(); board_init(); configure_console(); puts(STRING_HEADER); printf("CPU Frequency %li MHz\r\n", sysclk_get_cpu_hz() / 1000000); sound_init_hardware(); sound_start(16000); puts("Work...\r"); while (1) { //    512   ,   uint32_t size = min(512, ARRAYSIZE(g_sound_data) - g_sound_pos); if (sound_data(g_sound_data + g_sound_pos, size)) { //      g_sound_pos += size; if (g_sound_pos >= ARRAYSIZE(g_sound_data)) g_sound_pos = 0; } else { //    ,    } } } 


Here in the loop we send data from the array to the output queue, in case of success, we move to the end of the array, having reached the end, we return to the beginning. We work endlessly.

I use this module in another project, I output the sound when receiving the next packet via USB, so the task not to load the processor was completed.
You can also send audio data with a pre-calculated pause between packets, for example, if the packet size is 512 half words, and the sound frequency is 8 kHz, then the pause between sending will be 512/8000 = 64 ms.

It would be nice to get rid of copying data, if we are sure that they do not come from ROM. And the PDC can transmit two packets at once, this can also be used.

Dynamically changing the frequency of the sound, you can smooth out the unevenness in its content. For example, when filling the buffer by 2/3 of its volume, we slightly increase the frequency so that it is emptied faster.

I stitched the board using the standard utility from the Arduino IDE, it was too lazy to look for alternative ways, and this one completely suited me. You can send an example to the controller with the command:
bossac.exe --port=COM4 -U false -e -w -v -b sound.bin –R

bossac.exe - utility from Arduino IDE
Specify the COM port name.
The firmware file can be found in the Debug or Release directory of your project.

The entire project can be found here github.com/artalex/arduino_due_sound

Atmel Studio has a huge number of examples for Arduino Due, I strongly advise you to study them.

The pinout of the board is described here arduino.cc/en/Hacking/PinMappingSAM3X .
Here is a link to the description of the SAM3X controller: www.atmel.com/Images/doc11057.pdf .

An example array was obtained by the wave2hex.py script. It lies in the scripts directory in the project.
Use this: wave2hex.py –I some.wav , where some.wav is the name of the file to be converted.

Source: https://habr.com/ru/post/208436/


All Articles