Every embedded application software will eventually access the lowest level firmware and perform some hardware control. The design and implementation of drivers are critical to ensuring that a system can meet its real-time requirements. The following five tips are what every developer should consider when designing drivers.
Design patterns are solutions to recurring problems in software. Developers can either waste valuable time and budget reinventing a solution from scratch or choose the most suitable solution from their toolbox. When microprocessors first emerged, low-level drivers were already well established, so why not leverage existing mature solutions?
Driver design patterns can be broadly categorized into the following four types: Bit bang, polling, interrupt-driven, and Direct Memory Access (DMA).
Bit bang mode: When the microcontroller has no peripherals to perform functions, or when all peripherals are already in use and a new request arises, developers should choose the Bit bang design pattern. The Bit bang solution is efficient but usually requires a significant software overhead to ensure its implementation. Bit bang mode allows developers to manually complete communication protocols or external behaviors.
Polling mode is used to simply monitor events in a polling schedule. Polling mode is suitable for very simple systems, but many modern applications require interrupts.
Interrupts allow developers to handle events as they occur without waiting for the code to check manually.
DMA (Direct Memory Access) mode allows other peripherals to handle data transfer requests without driver intervention.
A real-time system’s ability to meet real-time demands depends on its drivers. Poorly written drivers are inefficient and may lead unaware developers to compromise the system’s performance. Designers need to consider two characteristics of drivers: blocking and non-blocking. A blocking driver prevents any other software from executing until it completes its task. For example, a USART driver may load a character into the transmission buffer and wait for the transmission end flag before proceeding to the next operation.
On the other hand, non-blocking drivers generally utilize interrupts to achieve their functionality. The use of interrupts can prevent the driver from intercepting other software’s execution while waiting for an event to occur. A USART driver can load a character into the transmission buffer and then wait for the main program to issue the next command. The setting of the transmission end flag will trigger an interrupt to let the driver proceed to the next operation.
Regardless of the type, to maintain real-time performance and prevent failures in the system, developers must understand the average execution time and worst-case execution time of the driver. A complete system can pose greater safety risks due to a potential issue.
Why reinvent the wheel when time and budget are tight? In driver development, reuse, portability, and maintainability are key requirements for driver design. Many of these features can be illustrated through the design and use of a Hardware Abstraction Layer (HAL).
The Hardware Abstraction Layer (HAL) provides developers with a way to create a standard interface to control the peripherals of microcontrollers. Abstraction hides implementation details and instead provides visual functions like Usart_Init and Usart_Transmit. This method allows any USART, SPI, PWM, or other peripherals to have common characteristics supported by all microcontrollers. Using HAL hides the details of the underlying specific devices, allowing application developers to focus on the application’s needs rather than how the underlying hardware works. At the same time, HAL provides a container for reuse.
Microcontrollers have become increasingly complex over the past few years. Previously, to fully understand a microcontroller, one had to master a single data sheet consisting of about 500 pages. Nowadays, a 32-bit microcontroller typically includes data sheets for parts, data sheets for entire microcontroller families, hundreds of data sheets for each peripheral, and all errata. Developers who want to fully grasp this content need to understand thousands of pages of documents.
Unfortunately, all these data sheets are what a driver truly needs for reasonable implementation. Developers must collect and sort the information contained in each data sheet from the very beginning. Typically, each of them needs to be accessed to get the peripherals up and running. Critical information is scattered (or hidden) across various types of data sheets.
Recently, I had the opportunity to port a series of microcontroller drivers to other microprocessors. Manufacturers and data sheets indicated that the PWM peripherals were the same between the two series of microcontrollers. However, the reality was that there were significant differences when running the PWM drivers. The driver only worked on the original microcontroller and failed on the new series of microcontrollers.
After repeatedly reviewing the data sheets, I found a completely unrelated footnote in the data sheet indicating that the PWM peripheral would enter a fault state upon power-up and required a flag hidden in a register to be cleared.
At the beginning of driver implementation, confirm potential failures of peripherals and check other seemingly unrelated register errors.
The “Learning Space” column is officially launched and is continuously updated, come and follow us! Reply directly with “Microcontroller”, “Embedded Systems”, “IoT”, “Smart Hardware”, “Embedded Engineer”, “CPU”, “GPU”, “FPGA”, “Linux”, “Android”, “ARM” keywords to read past related wonderful content anytime!
Disclaimer: This article is a network reprint, and the copyright belongs to the original author. If there are copyright issues, please contact us, and we will confirm the copyright based on the copyright certificate you provide and pay the manuscript fee or delete the content.