📜 ⬆️ ⬇️

C ++ drivers for STM8L051F3

Driver Problems and Requirements


Every experienced microprocessor programmer has come across writing drivers. When implementing small projects or transferring a ready-made debugged code to another processor, writing and debugging drivers can take 50% or more development time. Moreover, the process of writing a driver for a new processor, and matching the existing code can be very unpleasant due to the lack of structure and commonality in the driver. For a programmer, this becomes a nervous routine. We define important problems when writing a driver:

  1. Lack of driver structure. The solution to this problem allows you to better navigate the driver, therefore, debugging becomes easier.
  2. Lack of common driver. That is, the driver interface should be the same, for at least the processor line, and ideally for similar processors from different companies. This will allow to transfer the code tied to the driver, without changes or with minimal changes.
  3. Maintaining efficiency in driver execution speed.
  4. Optimal use of memory.


In this post, I will show how you can eliminate these problems and make the process of writing a driver a more enjoyable task using the example of two UART and DMA drivers implemented in C ++. For this, I formulated the requirements for the driver:

  1. The driver must be structured. The driver will be implemented as a C ++ class, which will allow you to create a driver object. This is convenient and logical, if in the processor, for example, there are three UART and four DMA channels, then you can create three driver objects, for each UART, and four driver objects for each DMA channel. If only one UART is used in the project, then only one object can be created for the corresponding UART, etc.
  2. The driver must have the same interface for multiple processors. It is very difficult to fulfill this requirement, since, in order to isolate a common interface, it is necessary for a multitude of processors to work through this multitude of processors, which will take a lot of time. Therefore, I created an interface convenient for STM8L051F3, and then as the project appears on the new processor I will adjust this interface. Thus, it is possible to single out the most general and optimal interface for all processors with time.
  3. The driver must be the maximum effective. Ideally, the execution speed and the amount of occupied memory should be no more than if you write this driver directly referring to the registers. Considerably the speed of the driver can be increased by using the template C ++. The C ++ template allows you to access the direct memory, thereby replacing pointers, the dereference of which takes time. But there are also costs for calling the driver interface function. With the help of the inline operator, the call to the function could not be eliminated; this is evident from the assembler code. Maybe when you turn on the optimizer of the compiler, this statement will affect ... Since the driver setting is performed one-time or at a slow periodicity, the cost of calling the function is not critical.

')

Using the driver


So, to begin with, consider the use of the DMA driver, and then the implementation. Using DMA, we transfer data from RAM to the UART shift register and receive data from the UART shift register to RAM.

#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(); //    } 


As can be seen from the previous listing, the code becomes more readable, and self-documenting.

Driver interface


The interface is defined in the EnumDMA structure, in which enums are defined, which are used as arguments to driver methods.

 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, }; }; 


The DMASel enumeration allows you to select a DMA module, and the ChannelSel enumeration determines the memory offset between the DMA channels. In the processor STM8L051F3 one DMA module, so the choice is not great. The DMA1 enumeration is assigned the address of the DMA1 module.
DriverDMA class design:

 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); //    00000  01FFF 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 module registers are defined in the private section, using the C ++ template, the DMA module and the module channel are selected.

Description of the driver implementation


Implementing a class constructor and two methods:

 //---------------------------------------------------------------------------------------------- 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 } 

where __DMA and __DMACHANNEL are defined as follows:
 #define __DMA ((DMA_struct*) DMA) #define __DMACHANNEL ((DMA_Channel_struct*) ((u32)DMA + (u32)DMAChannel)) 

At first glance, a complex structure:
 __DMACHANNEL->CSPR.bit.TSIZE = db; 

interpreted by the compiler as 3 assembler commands, and the command:
 __DMA->GCSR.bit.GEN = 1; 

occupies one assembly command.

Interruptions


To create an interrupt, you must enable it, write an interrupt function in which you need to perform handshaking. For example, create an interrupt upon completion of a DMA transaction:
 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) } 


Implementing a UART Driver with a DMA Driver


The UART driver is also implemented in a similar way using a class, but without using the C ++ template, since there is only one UART module in this processor. The UART driver uses two DMA channels to transmit and receive data. Since the driver user does not need access to DMA, we initialize the DMA in the private section:

 class DriverUART { private: ... u8 UartBuffer[128]; DriverDMA<EnumDMA::DMA1,EnumDMA::CHANNEL1> DMAChannelTX; DriverDMA<EnumDMA::DMA1,EnumDMA::CHANNEL2> DMAChannelRX; ... 


DMA configuration can be performed in the UART driver constructor, so when you create a UART object, the DMA driver will be immediately initialized and ready to go.

 //----------------------------------------------------------------------- // 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(); } 


Implementation of data transfer:

 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(); } 


Usage example:

 u8 buffer[] = "hello world!" Uart1.transmit(buffer, sizeof(buffer)); 


Upon reception, an interrupt is generated in which you must reset the receiving DMA channel:

 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(); } 


In the interrupt, you can get a pointer to the internal buffer in which the received data lies, and the size of the received packet. Since in this case the interrupt is created inside the class, the handler must be implemented as follows:

 //-------------------------------------------------------------------------- //    //-------------------------------------------------------------------------- void DriverUART::receive_handle() { u16 size; u8* pnt; Uart1.reception_handshake(); size = Uart1.get_received_size(); pnt = Uart1.get_pointer_on_internal_buffer(); //      } 


Conclusion


These drivers have a clear structure that allows you to navigate well through them, and thanks to which the driver is better remembered. The driver is now perceived as an object that can be configured, through it you can transfer something and get something from it.

An interface has been created in the form of C ++ lists, which helps to better understand the properties and capabilities of the driver and minimize communication with the processor. The code thanks to this interface becomes self-documenting. This all allows you to quickly understand a new driver and remember the essence of your own code to an experienced programmer.

The driver code can be used as a template for writing a similar driver from another processor of this line of drivers or from other manufacturers, without significantly changing the interface.

Through the use of the C ++ template, it was possible to significantly increase the speed of the driver, but the costs of calling a function can be neglected, where execution speed is not critical.

DMA and UART driver files can be downloaded from this link STM8L051F3_Drivers .

In the file "Init_UART.cpp" an example of using the UART driver.

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


All Articles