Проблемы и требования к драйверу
Каждый опытный программист микропроцессоров сталкивался с написанием драйверов. При реализации небольших проектов или при переносе уже готового отлаженного кода на другой процессор, написание и отладка драйверов может занимать 50% и больше времени разработки. Причем процесс написания драйвера, для нового процессора, и состыковка существующего кода может быть очень не приятной из-за отсутствия структуры и общности в драйвере. Для программиста это становится нервной рутиной. Определим важные проблемы, при написании драйвера:
- Отсутствие структуры драйвера. Решение данной проблемы позволить лучше ориентироваться по драйверу, следовательно, отладка становится проще.
- Отсутствие общности драйвера. То есть интерфейс драйвера должен быть одинаковый, для, хотя бы, линейки процессоров, и в идеале для аналогичных процессоров разных фирм. Это позволит переносить код, завязанный на драйвер, без изменений или с минимальными изменениями.
- Сохранение эффективности по скорости выполнения драйвера.
- Оптимальное использование памяти.
В данном посте я покажу, как можно ликвидировать эти проблемы и сделать процесс написания драйвера более приятной задачей на примере двух драйверов UART и DMA реализованных на языке С++. Для этого я сформулировал требования, для драйвера:
- Драйвер должен быть структурированным. Драйвер будет реализован в виде класса С++, что позволит создавать объект драйвера. Это удобно и логично, если в процессоре, например, есть три UART и четыре канала DMA, то можно создать три объекта драйвера, для каждого UART, и четыре объекта драйвера для каждого канала DMA. В случае, если в проекте используется только один UART, то можно создать только один объект, для соответствующего UART и т.п.
- Драйвер должен иметь одинаковый интерфейс, для множества процессоров. Выполнить это требование весьма сложно, так как, что бы выделить общий интерфейс удобный, для множества процессоров, нужно проработать это множество процессоров, что займет множество времени. Поэтому я создал интерфейс удобный для STM8L051F3, а затем по мере появления проекта на новом процессоре буду корректировать этот интерфейс. Таким образом, можно выделить максимально общий и оптимальный, для всех процессоров интерфейс со временем.
- Драйвер должен быть максимум эффективный. В идеале скорость выполнения и объем занятой память должны быть не более, чем если написать этот драйвер напрямую обращаясь к регистрам. Значительно скорость выполнения драйвера можно увеличить с помощью использования шаблона С++. Шаблон С++ позволяет обращаться на прямую к памяти, тем самым заменить указатели, разыменование которых занимает время. Но также есть затраты на вызов функции интерфейса драйвера. С помощью оператора inline вызов функции устранить не удалось, это видно по ассемблерному коду. Может быть, при включении оптимизатора компилятора данный оператор будет влиять… Так как настройка драйвера выполняется одноразово или с медленной периодичностью, то затраты на вызов функции являются не критичными.
Использование драйвера
Итак, для начала, рассмотрим использование драйвера DMA, а затем реализацию. С помощью DMA выполняем передачу данных из ОЗУ в сдвиговый регистр UART и прием данных из сдвигового регистра UART в ОЗУ.
#include "iostm8l051f3.h"
#include "Driver_DMA.hpp"
char UartBuffer[128];
// создание объектов драйвера DMA
DriverDMA<EnumDMA::DMA1,EnumDMA::CHANNEL1> DMAChannelTX; // DMA1 канал 1
DriverDMA<EnumDMA::DMA1,EnumDMA::CHANNEL2> DMAChannelRX; // DMA1 канал 2
void func()
{
DMAChannelTX.global_disable(); // отключение всех каналов
// настройка передающего канала DMA
DMAChannelTX.set_periph_addr(&USART1_DR); // установка периферийного адреса
DMAChannelTX.set_memory0_addr(UartBuffer); // установка адреса в ОЗУ
DMAChannelTX = EnumDMA::DATABLOCK_8bit; // передача по-байтно
DMAChannelTX = EnumDMA::PRIORITY_MEDIUM; // приоритет средний
DMAChannelTX = EnumDMA::MEMPNT_INCREMENT; // инкрементирование адреса после передачи
DMAChannelTX = EnumDMA::MEMORY_TO_PHERIPH; // передача из ОЗУ в сдвиговый регистр UART
DMAChannelTX = EnumDMA::CIRCULAR_DISABLE; // циклическая передача отключена
// настройка приемного канала DMA
DMAChannelRX.set_periph_addr(&USART1_DR);
DMAChannelRX.set_memory0_addr(UartBuffer);
DMAChannelRX = EnumDMA::DATABLOCK_8bit;
DMAChannelRX = EnumDMA::PRIORITY_MEDIUM;
DMAChannelRX = EnumDMA::MEMPNT_INCREMENT;
DMAChannelRX = EnumDMA::PHERIPH_TO_MEMORY; // передача из сдвигового регистра UART в ОЗУ
DMAChannelRX = EnumDMA::CIRCULAR_DISABLE;
DMAChannelRX.set_number_of_transfers(sizeof(UartBuffer));
DMAChannelRX.channel_enable(); // текущий канал включен
DMAChannelTX.global_enable(); // включение всех каналов
}
Как видно из предыдущего листинга, код становится более понятный, для чтения, и самодокументируемым.
Интерфейс драйвера
Интерфейс определен в структуре EnumDMA, в которой определены перечисления, которые используются в качестве аргументов методов драйвера.
struct EnumDMA
{
enum DMASel{
DMA1 = 0x5070
};
enum ChannelSel{
CHANNEL0 = 0x05,
CHANNEL1 = 0x0F,
CHANNEL2 = 0x19,
CHANNEL3 = 0x23
};
enum DataBlock{
DATABLOCK_8bit, DATABLOCK_16bit
};
enum ChannelPriority{
PRIORITY_LOW,
PRIORITY_MEDIUM,
PRIORITY_HIGH,
PRIORITY_VERYHIGH
};
enum MemoryPointerMode{
MEMPNT_DECREMENT,
MEMPNT_INCREMENT
};
enum CircularBufferMode{
CIRCULAR_DISABLE,
CIRCULAR_ENABLE
};
enum TransferType{ // только для канала 3
TRANS_TYPE_PHERIPH_TO_MEMORY,
TRANS_TYPE_MEMORY0_TO_MEMORY1
};
enum TransferDirection{
PHERIPH_TO_MEMORY,
MEMORY_TO_PHERIPH
};
enum InterruptSelection{
INTERRUPT_HALF_TRANSACTION_COMPLETE,
INTERRUPT_TRANSACTION_COMPLETE
};
enum InterruptVectors{
VECTOR_DMA1_CHANNEL0_HALF_TRANSACTION_COMPLETE = DMA1_CH0_HT_vector,
VECTOR_DMA1_CHANNEL0_TRANSACTION_COMPLETE = DMA1_CH0_TC_vector,
VECTOR_DMA1_CHANNEL1_HALF_TRANSACTION_COMPLETE = DMA1_CH1_HT_vector,
VECTOR_DMA1_CHANNEL1_TRANSACTION_COMPLETE = DMA1_CH1_TC_vector,
VECTOR_DMA1_CHANNEL2_HALF_TRANSACTION_COMPLETE = DMA1_CH2_HT_vector,
VECTOR_DMA1_CHANNEL2_TRANSACTION_COMPLETE = DMA1_CH2_TC_vector,
VECTOR_DMA1_CHANNEL3_HALF_TRANSACTION_COMPLETE = DMA1_CH3_HT_vector,
VECTOR_DMA1_CHANNEL3_TRANSACTION_COMPLETE = DMA1_CH3_TC_vector,
};
};
Перечисление DMASel позволяет выбрать модуль DMA, а перечисление ChannelSel определяет смещение в памяти между каналами DMA. В процессоре STM8L051F3 один модуль DMA, поэтому выбор не велик. Перечислению DMA1 присвоен адрес модуля DMA1.
Дизайн класса DriverDMA:
template <EnumDMA::DMASel DMA, EnumDMA::ChannelSel DMAChannel>
class DriverDMA
{
private:
...
struct DMA_struct // регистры модуля DMA влияющие на все каналы
{
volatile GCSR_REG GCSR; /*!< Global configuration and status register */
volatile GIR_REG GIR1; /*!< Global interrupt register 1 */
};
...
struct DMA_Channel_struct // регистры каналов модуля DMA
{
volatile CCR_REG CCR; /*!< CHx Control register */
volatile CSPR_REG CSPR; /*!< CHx Status & Priority register */
volatile unsigned char CNDTR; /*!< CHx Number of Bytes to Tranfer register */
volatile unsigned char CPARH; /*!< Peripheral Address High register */
volatile unsigned char CPARL; /*!< Peripheral Address Low register */
volatile unsigned char CM0EAR; /*!< Memory 0 Extended Address register (for channel3)*/
volatile unsigned char CM0ARH; /*!< Memory 0 Address High register */
volatile unsigned char CM0ARL; /*!< Memory 0 Address Low register */
};
u8 number_of_transfers;
u8 NumChannel;
public:
DriverDMA();
void operator= (EnumDMA::DataBlock);
void operator= (EnumDMA::ChannelPriority);
void operator= (EnumDMA::MemoryPointerMode);
void operator= (EnumDMA::CircularBufferMode);
void operator= (EnumDMA::TransferDirection);
void operator= (EnumDMA::TransferType); // только для канала 3
void global_enable();
void global_disable();
void channel_enable();
void channel_disable();
void set_number_of_transfers(const u16 trans_num);
void set_periph_addr(u8 volatile* addr);
void set_memory0_addr(u8* addr);
// ДОСТУПНЫЕ АДРЕСА ОТ 0х0000 до 0х1FFF
void set_memory1_addr(u8* addr); // только для канала 3
bool is_busy();
u16 get_amount_of_last_transation();
void interrupt_enable(EnumDMA::InterruptSelection);
void interrupt_disable(EnumDMA::InterruptSelection);
void interrupt_clear_pending_flag(EnumDMA::InterruptSelection);
};
Регистры модуля DMA определены в секции private, с помощью шаблона С++ выбирается модуль DMA и канал модуля.
Описание реализации драйвера
Реализация конструктора класса и двух методов:
//----------------------------------------------------------------------------------------------
template <EnumDMA::DMASel DMA, EnumDMA::ChannelSel DMAChannel>
DriverDMA<DMA,DMAChannel>::DriverDMA()
{
CLK_PCKENR2_bit.PCKEN24 = 1; // DMA clock enable
__DMA->GCSR.bit.TO = 31;
NumChannel = (u8)DMAChannel;
}
//----------------------------------------------------------------------------------------------
template <EnumDMA::DMASel DMA, EnumDMA::ChannelSel DMAChannel>
void DriverDMA<DMA,DMAChannel>::operator= (EnumDMA::DataBlock db)
{
__DMACHANNEL->CSPR.bit.TSIZE = db;
}
//----------------------------------------------------------------------------------------------
template <EnumDMA::DMASel DMA, EnumDMA::ChannelSel DMAChannel>
void DriverDMA<DMA,DMAChannel>::global_enable()
{
__DMA->GCSR.bit.GEN = 1; // Global enable of DMA1
}
где __DMA и __DMACHANNEL определены следующим образом:
#define __DMA ((DMA_struct*) DMA)
#define __DMACHANNEL ((DMA_Channel_struct*) ((u32)DMA + (u32)DMAChannel))
На первый взгляд сложная конструкция:
__DMACHANNEL->CSPR.bit.TSIZE = db;
интерпретируется компилятором как 3 ассемблерных команды, а команда:
__DMA->GCSR.bit.GEN = 1;
занимает одну ассемблерную команду.
Прерывания
Что бы создать прерывание необходимо его разрешить, написать функцию прерывания, в которой нужно выполнить квитирование. Например создадим прерывание по окончанию транзакции DMA:
void func()
{
// разрешаем прерывание по завершению транзакции
DMAChannelTX.interrupt_enable(EnumDMA::INTERRUPT_TRANSACTION_COMPLETE)
}
#pragma vector = EnumDMA::VECTOR_DMA1_CHANNEL1_TRANSACTION_COMPLETE
__interrupt void DMA_transaction_complete()
{
// здесь обработка прерывания
DMAChannelTX.interrupt_clear_pending_flag(EnumDMA::INTERRUPT_TRANSACTION_COMPLETE)
}
Реализация драйвера UART с помощью драйвера DMA
Драйвер UART также реализован подобным образом с помощью класса, но без применения шаблона С++, так как модуль UART только один в данном процессоре. Драйвер UART использует два канала DMA, для приема и передачи данных. Так как пользователю драйвера доступ к DMA не нужен, то инициализируем DMA в секции private:
class DriverUART
{
private:
...
u8 UartBuffer[128];
DriverDMA<EnumDMA::DMA1,EnumDMA::CHANNEL1> DMAChannelTX;
DriverDMA<EnumDMA::DMA1,EnumDMA::CHANNEL2> DMAChannelRX;
...
Настройку DMA можно выполнить в конструкторе драйвера UART, таким образом, при создании объекта UART драйвера DMA будет сразу инициализировано и готово к работе.
//-----------------------------------------------------------------------
// Main DriverUART Constructor
//-----------------------------------------------------------------------
DriverUART::DriverUART(SELECTUART uart, u32 baud_rate, u32 sys_clock, PinconfigUART confPin)
{
CLK_PCKENR1_bit.PCKEN15 = 1; // UART clock enable
USART1_CR1_bit.USARTD = 0;
USART1_CR5_bit.DMAT = 1; // DMA enable transmitter
USART1_CR5_bit.DMAR = 1; // DMA enable receiver
DMAChannelTX.global_disable();
DMAChannelTX.set_periph_addr(&USART1_DR);
DMAChannelTX.set_memory0_addr(UartBuffer);
DMAChannelTX = EnumDMA::DATABLOCK_8bit;
DMAChannelTX = EnumDMA::PRIORITY_MEDIUM;
DMAChannelTX = EnumDMA::MEMPNT_INCREMENT;
DMAChannelTX = EnumDMA::MEMORY_TO_PHERIPH;
DMAChannelTX = EnumDMA::CIRCULAR_DISABLE;
DMAChannelRX.set_periph_addr(&USART1_DR);
DMAChannelRX.set_memory0_addr(UartBuffer);
DMAChannelRX = EnumDMA::DATABLOCK_8bit;
DMAChannelRX = EnumDMA::PRIORITY_MEDIUM;
DMAChannelRX = EnumDMA::MEMPNT_INCREMENT;
DMAChannelRX = EnumDMA::PHERIPH_TO_MEMORY;
DMAChannelRX = EnumDMA::CIRCULAR_DISABLE;
DMAChannelRX.set_number_of_transfers(sizeof(UartBuffer));
DMAChannelRX.channel_enable();
DMAChannelTX.global_enable();
set_sysclock(sys_clock, baud_rate);
USART1_CR2_bit.TCIEN = 1; // вкл. прерывания по окончанию передачи
USART1_CR2_bit.ILIEN = 1; // вкл. прерывание по приему
USART1_CR5_bit.EIE = 1; // вкл. прерывание по ошибке приема
__enable_interrupt();
close();
}
Реализация передачи данных:
void DriverUART::transmit(u8 * source, u16 size)
{
while(DMAChannelTX.is_busy()) ;
select_direction(TRANSMITION);
__disable_interrupt();
DMAChannelTX.global_disable();
DMAChannelTX.channel_disable();
DMAChannelTX.set_number_of_transfers(size);
DMAChannelTX.set_memory0_addr(source);
DMAChannelTX.global_enable();
DMAChannelTX.channel_enable();
__enable_interrupt();
}
Пример использования:
u8 buffer[] = "hello world!"
Uart1.transmit(buffer, sizeof(buffer));
По приему генерируется прерывание, в котором необходимо выполнять сброс приемного канала DMA:
void DriverUART::reception_handshake()
{
__disable_interrupt();
DMAChannelRX.global_disable();
DMAChannelRX.channel_disable();
received_size = DMAChannelRX.get_amount_of_last_transation();
DMAChannelRX.set_number_of_transfers(sizeof(UartBuffer));
DMAChannelRX.global_enable();
DMAChannelRX.channel_enable();
__enable_interrupt();
}
В прерывании можно получить указатель на внутренний буфер, в котором лежат полученные данные, и размер полученного пакета. Так как в данном случае прерывание создано внутри класса, то обработчик необходимо реализовать следующим образом:
//--------------------------------------------------------------------------
// Прерывание по приему
//--------------------------------------------------------------------------
void DriverUART::receive_handle()
{
u16 size;
u8* pnt;
Uart1.reception_handshake();
size = Uart1.get_received_size();
pnt = Uart1.get_pointer_on_internal_buffer();
// здесь может быть обработчик прерывания
}
Заключение
Данные драйвера имеют четкую структуру, которая позволяет хорошо ориентироваться по ним, и благодаря которой драйвер лучше запоминается. Драйвер теперь воспринимается как объект, который можно настроить, через него можно что-то передать и что-то получить из него.
Создан интерфейс в виде перечней С++, который помогает лучше понять свойства и возможности драйвера и минимизировать общение с дата шитом процессора. Код благодаря данному интерфейсу становится самодокументируемым. Это все позволяет быстрее разобраться новичку в драйвере и вспомнить суть собственного кода опытному программисту.
Код драйвера можно использовать как шаблон, для написания аналогичного драйвера другого процессора данной линейки драйверов или других фирм производителей, значительно не меняя интерфейс.
Благодаря использованию шаблона С++ удалось значительно повысить скорость выполнения драйвера, но остаются затраты на вызов функции, которыми можно пренебречь, где не критична скорость выполнения.
Файлы драйверов DMA и UART можно скачать данной ссылке http://f-bit.ru/457683.
В файле «Init_UART.cpp» пример использования драйвера UART.
Автор: SiYu