Настройка UART по прерываниям для Atmel SAM D20/D21

Работа с UART'ом по прерываниям — это одна из базовых задач, которую необходимо освоить в огромном числе проектов при работе с любым контроллером. Разумеется, что в ASF (атмеловский аналог ST'шной SPL) есть драйвер работы с UART по прерываниям, но его реализация для семейств Cortex M0+ весьма спорная.

Хочу поделиться двумя вещами:
1. Как «допилить» стандартный пример из ASF
2. Как настроить UART при помощи регистров, без использования библиотек

Как «допилить» стандартный пример из ASF

Если запустить пример проекта с работой по uart из ASF в основном коде мы увидим функцию приема данных по uart:
usart_read_buffer_job(&usart_instance,	(uint8_t *)rx_buffer, MAX_RX_BUFFER_LENGTH);

Казалось бы, все ок. Входные параметры: номер uarta, указатель на приемный буфер и длина пакета. Стоп! А если длину пакета мы не знаем или она переменная?

Удобнее всего сделать прием побайтно. Тут два пути: передавать в качестве длины единицу или залезть в исходники и все поправить). Первый путь неудобен тем, что для получения данных необходимо вызывать постоянно функцию usart_read_buffer_job, что убивает всю концепцию работы по прерываниям.
Поэтому пойдем по второму пути.

Итак, исходный код usart_read_buffer_job:
enum status_code usart_read_buffer_job(
		struct usart_module *const module,
		uint8_t *rx_data,
		uint16_t length)
{
	/* Sanity check arguments */
	Assert(module);
	Assert(rx_data);

	if (length == 0) {
		return STATUS_ERR_INVALID_ARG;
	}

	/* Check that the receiver is enabled */
	if (!(module->receiver_enabled)) {
		return STATUS_ERR_DENIED;
	}

	/* Issue internal asynchronous read */
	return _usart_read_buffer(module, rx_data, length);
}

Фактически функция лишь проверяет параметры и передает бразды правления функции _usart_read_buffer, полезем в нее:
enum status_code _usart_read_buffer(
		struct usart_module *const module,
		uint8_t *rx_data,
		uint16_t length)
{
	/* Sanity check arguments */
	Assert(module);
	Assert(module->hw);
	Assert(rx_data);

	/* Get a pointer to the hardware module instance */
	SercomUsart *const usart_hw = &(module->hw->USART);

	system_interrupt_enter_critical_section();

	/* Check if the USART receiver is busy */
	if (module->remaining_rx_buffer_length > 0) {
		system_interrupt_leave_critical_section();
		return STATUS_BUSY;
	}

	/* Set length for the buffer and the pointer, and let
	 * the interrupt handler do the rest */
	module->remaining_rx_buffer_length = length;

	system_interrupt_leave_critical_section();

	module->rx_buffer_ptr              = rx_data;
	module->rx_status                  = STATUS_BUSY;

	/* Enable the RX Complete Interrupt */
	usart_hw->INTENSET.reg = SERCOM_USART_INTFLAG_RXC;

#ifdef FEATURE_USART_LIN_SLAVE
	/* Enable the break character is received Interrupt */
	if(module->lin_slave_enabled) {
		usart_hw->INTENSET.reg = SERCOM_USART_INTFLAG_RXBRK;
	}
#endif

#ifdef FEATURE_USART_START_FRAME_DECTION
	/* Enable a start condition is detected Interrupt */
	if(module->start_frame_detection_enabled) {
		usart_hw->INTENSET.reg = SERCOM_USART_INTFLAG_RXS;
	}
#endif

	return STATUS_OK;
}

Если нам не нужен LIN и прерывания по старту фрейма, то все в #ifdef убираем. Assert для наглядности тоже уберем. Остается:
enum status_code _usart_read_buffer(
		struct usart_module *const module,
		uint8_t *rx_data,
		uint16_t length)
{

	/* Get a pointer to the hardware module instance */
	SercomUsart *const usart_hw = &(module->hw->USART);

	system_interrupt_enter_critical_section();

	/* Check if the USART receiver is busy */
	if (module->remaining_rx_buffer_length > 0) {
		system_interrupt_leave_critical_section();
		return STATUS_BUSY;
	}

	/* Set length for the buffer and the pointer, and let
	 * the interrupt handler do the rest */
	module->remaining_rx_buffer_length = length;

	system_interrupt_leave_critical_section();

	module->rx_buffer_ptr              = rx_data;
	module->rx_status                  = STATUS_BUSY;

	/* Enable the RX Complete Interrupt */
	usart_hw->INTENSET.reg = SERCOM_USART_INTFLAG_RXC;

}

Что у нас осталось? Проверка на наличие отправляемых данных (if (module->remaining_rx_buffer_length > 0) ), присвоение длины пакета в структуре uart, присвоение указателю на принятые данные в структуре реального адреса и выставление статуса busy.

Все что касается длины, нам не нужно. Так как она всегда 1.
Фактически тогда остается разрешение прерываний по приему данных (сюрприз-сюрприз! Оно разрешается только при вызове функции, а не при инициализации uart). Таким образом, если мы в самом начале при инициализации разрешим прерывание по приему и укажем, куда складывать данные, то от этих двух функций можно вообще избавится и делать обработку пакетов в callback, который и будет вызываться при получении каждого байта. В callback надо внести запись пришедшего байта в массив (при необходимости) и, возможно, небольшой парсер.
Функция для разрешения прерывания и указания, куда складывать данные может выглядеть так:
enum status_code usart_enable_rx_interrupt(struct usart_module *const module,uint8_t *rx_data)
{
	// Sanity check arguments
	Assert(module);
	Assert(rx_data);
	// Issue internal asynchronous read
	// Get a pointer to the hardware module instance 
	SercomUsart *const usart_hw = &(module->hw->USART);
	module->rx_buffer_ptr = rx_data;
	// Enable the RX Complete Interrupt 
	usart_hw->INTENSET.reg = SERCOM_USART_INTFLAG_RXC;
	return STATUS_OK;
}

Вызов:
usart_enable_rx_interrupt(&usart_plc_instance,&plc_rx_byte);

А callback так:
void USART_PLC_RX_callback(const struct usart_module *const usart_module)
{   
   if (rx0_cnt < UARTC0_RX_BUF_LEN) 
   {
      usart_rx_buf[rx0_idx++]=plc_rx_byte;
      if (rx0_idx >= UARTC0_RX_BUF_LEN) rx0_idx -= UARTC0_RX_BUF_LEN;
      ++rx0_cnt;
   }
}

2. Как настроить UART при помощи регистров, без использования библиотек

Для ценителей олдскула, разберемся как настроить UART «ручками».

Зависимости
Настройка портов
Замечания: PORT->Group[0] можно определить как PORTA в заголовочном файле. Это улучшает читаемость кода, но в этом случае Atmel Studio не предлагает автозаполнение для структуры порта, что снижает производительность и общее удобство. Поэтому мы будем использовать вариант с Group[0] и Group[1] для портов A и B соответственно.
RS485_TXD_pin_num и т.п. это определения для номера пина на соответствующем порту.
Для включения режима периферии на пинах смотрите буквенные обозначения функции пинов в даташите, параграф “I/O Multiplexing and Considerations”.
Назначая эти функции через регистр PMUX, обратите внимание на какие пины заходят контакты SERCOM'а, например, TXD это SERCOM/PAD[2] и RXD это SERCOM/PAD[3]; это играет роль при настройке битов USART_CTRLA_RXPO and _TXPO в регистре USART->CTRLA.

Код инициализации порта и пинов под USART:

/***************************************************************************//**
 * @brief Pins initialization
 * @details Initializes pins for UART
 ******************************************************************************/
void ports_init(void)
{
    /* RS-485 pins*/
    /* RS485_TXD - цифровой выход */
    PORT->Group[0].DIRSET.reg = (1 << RS485_TXD_pin_num);    // настройка как выход
    PORT->Group[0].OUTSET.reg = (1 << RS485_TXD_pin_num);    // выставить высокий уровень

    /* RS485_RXD - цифровой вход */
    PORT->Group[0].DIRCLR.reg = (1 << RS485_RXD_pin_num);    // настройка как вход
    PORT->Group[0].OUTSET.reg = (1 << RS485_RXD_pin_num);    // включает подтяжку к питанию

    /* Если используется выход RTS, настраиваем как цифровой выход */
    PORT->Group[0].DIRSET.reg = (1 << RS485_RTS_pin_num);    // выход
    PORT->Group[0].OUTCLR.reg = (1 << RS485_RTS_pin_num);    // низкий уровень, модуль настроен на прием данных

    /* Настраиваем функцию USART SERCOM2 на пинах порта A через регистры PINCFG и PMUX */
    PORT->Group[0].PINCFG[RS485_TXD_pin_num].reg = PORT_PINCFG_PMUXEN;    // Задействуем функцию периферии на пине
    /* Задействуем функцию периферии, выставляем бит INEN для включения входного буфера пина и возможности чтения состояния */
    PORT->Group[0].PINCFG[RS485_RXD_pin_num].reg = PORT_PINCFG_PMUXEN | PORT_PINCFG_INEN;
    /* Включаем мультиплексор периферии для обоих пинов. Делается это через регистр PMUX с номером, равным (номер любого пина из пары/2) и
     * заданием требуемой функции (см. таблицу 5.1 даташита) для чётного и нечётного номера пина. В нашем случае из таблицы пинам PA08
     * и PA09 для назначения на них SERCOM2 необходимо включить функцию D. PORT_PMUX_PMUXE_D задает функцию D для чётного пина (PA08),
     * PORT_PMUXO_D - для нечётного PA09 */
    PORT->Group[0].PMUX[RS485_TXD_pin_num/2].reg = PORT_PMUX_PMUXE_D | PORT_PMUX_PMUXO_D;
}


Настройка тактирования
Генератор шины SERCOM (CLK_SERCOMx_APB) необходимо задействовать в Power Manager (регистр PM), т.к. значение по умолчанию — отключен.
Для тактирования ядра SERCOM необходим задающий генератор частоты (GCLK_SERCOMx_CORE). Его нужно настроить и задействовать в контроллере задающих генераторов до включения ядра SERCOM.
Тактирование настраивается первым делом в примере функции usart_init ниже.

Инициализация UART
Замечания: всегда проверяйте значение, которое записывается в регистр BAUD, чтобы избежать неточной настройки скорости. При некратных скоростях и частотах ядра контроллер округлит значение BAUD до целого числа. Иногда погрешность округления может быть слишком велика и лучше заставить контроллер округлить значение в другую сторону. Обратите внимание, что BAUD — 16-битный регистр и максимальная величина для него 65535.
Также, будет нелишним проверить скорость передачи осциллографом, убедившись, что контроллер генерит бодрейт именно тот, на какой вы рассчитываете.
Синхронизация регистров: многие регистры требуют синхронизации доменов тактирования (это указано в описании регистра как Write-Synchronized), поэтому после записи значения в регистр необходимо ждать снятия флага SYNCBUSY в регистре STATUS.

/***************************************************************************//**
 * @brief Init of UART
 * @details Initializes RS-485 on SERCOM2 for communication with gateway
 * @param baudrate Requested baudrate
 * @note Does not use ASF. \n
 *        Enables receiver and transmitter of USART module. \n
 *        Pinout: UART_TX - SERCOM2/PAD[0]; \n
 *                UART_RX - SERCOM2/PAD[1].
 ******************************************************************************/
void uart_gw_init(uint32_t baudrate)
{
    uint64_t br;
    /* Рассчет величины для регистра BAUD */	
    br = (uint64_t)65536 * (F_CPU - (16 * baudrate)) / F_CPU;
    /* Настраиваем задающий генератор SERCOM2. Мы не используем деление частоты, поэтому скорость генератора SERCOM2 равна скорости ядра */
    GCLK->CLKCTRL.reg = (GCLK_CLKCTRL_ID_SERCOM2_CORE | GCLK_CLKCTRL_GEN_GCLK0 | GCLK_CLKCTRL_CLKEN);

    /* Задействуем генератор SERCOM2. Обратите внимание, что мы только выставляем нужный бит в регистре, не трогая остальные. */
    PM->APBCMASK.reg |= PM_APBCMASK_SERCOM2; 

    /* Задаем LSB first (DORD); заводим RX на PAD1, т.е. PA09 в нашем примере (RXPO): SERCOM2/PAD[1] = Rx, пин TX по умолчанию заводится
     * на PAD0, т.е. PA08, т.к. TXPO = 0; тактирование от Internal clock (MODE); по умолчанию No Parity */
    SERCOM2->USART.CTRLA.reg = (SERCOM_USART_CTRLA_DORD | SERCOM_USART_CTRLA_RXPO(1) | SERCOM_USART_CTRLA_MODE(1));
    while(SERCOM2->USART.STATUS.reg & SERCOM_USART_STATUS_SYNCBUSY);
	
    /* Задействуем приемник (RXEN) и передатчик (TXEN), задаем формат 8 бит (CHSIZE 0) и 2 стоп-бита (SBMODE) */
    SERCOM2->USART.CTRLB.reg = (SERCOM_USART_CTRLB_RXEN | SERCOM_USART_CTRLB_TXEN |
                        SERCOM_USART_CTRLB_SBMODE | SERCOM_USART_CTRLB_CHSIZE(0));
    while(SERCOM2->USART.STATUS.reg & SERCOM_USART_STATUS_SYNCBUSY);

    /* Задаем скорость передачи */
    SERCOM2->USART.BAUD.reg = (uint16_t)br;

    /* Задействуем прерывание по приему */
    SERCOM2->USART.INTENSET.reg = (SERCOM_USART_INTENSET_RXC);
    /* Включаем прерывания от SERCOM2 в контроллере прерываний */
    NVIC_EnableIRQ(SERCOM2_IRQn);
    
    /* Включаем SERCOM2 заданием бита Enable. Другие биты не трогаем. */
    SERCOM2->USART.CTRLA.reg |= SERCOM_USART_CTRLA_ENABLE;
    while(SERCOM2->USART.STATUS.reg & SERCOM_USART_STATUS_SYNCBUSY);
}


Обработчик прерывания UART
У SAMD20/21 один обработчик на все возможные прерывания от SERCOM2. Поэтому необходимо в самом обработчике по флагам определять какое именно прерывание сработало. Снимать соответствующий флаг необходимо вручную, при этом заметьте, что большинство флагов у SAMD20/21 снимается выставлением бита в регистре INTFLAG, а не его обнулением.

Ниже представлен простой пример обработчика прерывания от SERCOM2:

void SERCOM2_Handler()
{
    usart_in_use = USB_USART;

    led_toggle(LED_GREEN_PIN_NUM);

    /* Проверка флагов и определение сработавшего прерывания. */
    /* Перывание по приему */    
    if (SERCOM2->USART.INTFLAG.bit.RXC)
    {
        /* Снять флаг прерывания RX */
        SERCOM2->USART.INTFLAG.reg = SERCOM_USART_INTFLAG_RXC;

        /* Полезные действия */
    }

    /* Прерывание по передаче */
    if (SERCOM2->USART.INTFLAG.bit.TXC)
    {
        /* Снять флаг */
        SERCOM2->USART.INTFLAG.reg = SERCOM_USART_INTFLAG_TXC;

        /* Какие-то полезные действия */
    }

    /* Прерывания по другим флагам аналогично */
}
  • +1
  • 11 декабря 2015, 13:37
  • Den1s

Комментарии (2)

RSS свернуть / развернуть
не читал что там унутрях этого вашего Atmel SAM D20/D21 (сколько uartов, какие gpio), но привык делать «универсальные» инициализации с функциями подобными:
void UARTInit(UART_TypeDef* UARTx, PORT_TypeDef* PORTx, uint32_t TX, uint32_t RX)
void UARTBaudSet(UART_TypeDef* UARTx, uint32_t Baud)

зы в void uart_gw_init(uint32_t baudrate) что-то baudrate нигде не используется, вроде гвоздями прибито 115200
зы2 *p_buffer++ = data_temp; — шикарная конструкция по порче памяти
0
Да, пример взят из простого проекта, где некогда универсальная функция была переделана в 115200, декларация при этом не менялась.
Конструкция по порче памяти достаточно безопасна, если проверять границы записи, плюс была использована в пробном проекте лишь бы запустить.
Во избежание недоразумений, уберём код обработчика, оставив только основу. В конце концов пример нацелен на настройку UART, а не приём данных.

Хотя общие принципы инициализации похожи, сделать универсальные функции будет сложно, т.к. SAMD20 и 21 между собой отличаются как настройками, так и составом регистров. Тот же регистр BAUD несовместим, поскольку у D21 добавлена генерация дробного бодрейта.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.