Another Ada STM32 MCU Drivers Library
A lightweight, generic-based driver library for STM32 microcontrollers. This library provides low-level drivers for common peripherals while prioritizing simplicity and efficiency.
- Simple and efficient peripheral drivers for:
- GPIO
- UART
- I2C
- SPI
- Timers
- MCUs UID
- Flash for
stm32f429
- Flexible static memory controller (FSMC) for
stm32f40x
- Support for:
- STM32F407 (using the
light-tasking-stm32f4
Ada runtime library) - STM32F429 (using the
light-tasking-stm32f429disco
Ada runtime library)
- STM32F407 (using the
This library avoids using tagged types (object-oriented programming, as used in the Ada Drivers Library and HAL) in favor of Ada generics. The rationale for this decision includes:
- Most embedded applications use a fixed hardware configuration and don't require runtime polymorphism to switch between different peripheral implementations.
- Generic units provide compile-time specialization, leading to more efficient code.
- A simpler implementation without the overhead of tagged types.
- Better optimization opportunities for the compiler.
Add this library to your project using Alire:
alr with aa_stm32_drivers --use=https://github.com/reznikmm/aa_stm32_drivers
STM32 devices use pins grouped into ports. To specify a pin, provide a port
name (enumeration literals PA
, PB
, etc) and a pin number (0 to 15).
To configure a GPIO pin for output, use the Configure_Output
procedure:
STM32.GPIO.Configure_Output (Pin => (STM32.PA, 1));
Set the pin state using:
STM32.GPIO.Set_Output (Pin => (STM32.PA, 1), Value => 0); -- Clear pin
STM32.GPIO.Set_Output (Pin => (STM32.PA, 1), Value => 1); -- Set pin
For input pins, configure them as interrupt sources and optionally enable pull-up or pull-down resistors:
STM32.GPIO.Configure_Interrupt (Pin => (STM32.PA, 1)); -- No pull-up/down resistors
STM32.GPIO.Configure_Interrupt (Pin => (STM32.PA, 1), Pull_Up => True);
STM32.GPIO.Configure_Interrupt (Pin => (STM32.PA, 1), Pull_Down => True);
Don't forget to clear the interrupt flag when handling an interrupt:
STM32.GPIO.Clear_Interrupt (Pin => (STM32.PA, 1));
The library supports USART 1, 2, 3, 6, and UART 4, 5. To use UART/USART,
with
the corresponding package, declare a device object, and configure
it with TX/RX pins and a baud rate. Define an interrupt priority for
the protected object.
with STM32.UART.USART_1;
procedure Main is
package USART_1 is new STM32.UART.USART_1 (Priority => 241);
begin
USART_1.Configure
(TX => (STM32.PA, 9),
RX => (STM32.PA, 10),
Speed => 115_200);
end Main;
Use Start_Reading
to initiate reading and Start_Writing
for writing.
The library leverages the
A0B.Callbacks
crate
for callbacks.
with Ada.Synchronous_Task_Control;
with A0B.Callbacks.Generic_Subprogram;
with STM32.UART.USART_1;
procedure Main is
package Suspension_Object_Callbacks is new
A0B.Callbacks.Generic_Subprogram
(Ada.Synchronous_Task_Control.Suspension_Object,
Ada.Synchronous_Task_Control.Set_True);
package USART_1 is new STM32.UART.USART_1 (Priority => 241);
Buffer : String (1 .. 8);
Signal : aliased Ada.Synchronous_Task_Control.Suspension_Object;
Done : constant A0B.Callbacks.Callback :=
Suspension_Object_Callbacks.Create_Callback (Signal);
begin
USART_1.Configure
(TX => (STM32.PA, 9),
RX => (STM32.PA, 10),
Speed => 115_200);
loop
USART_1.Start_Reading
(Buffer'Address, Buffer'Length, Done);
Ada.Synchronous_Task_Control.Suspend_Until_True (Signal);
-- Process Buffer here
end loop;
end Main;
Configure an I2C device by providing SDA, SCL pins, and the speed:
with STM32.I2C.I2C_1;
procedure Main is
package I2C_1 is new STM32.I2C.I2C_1 (Priority => 241);
begin
I2C_1.Configure
(SDA => (STM32.PB, 7),
SCL => (STM32.PB, 8),
Speed => 400_000);
end Main;
Use Start_Data_Exchange
to initiate transfers. The transfer can read data
from the slave, write data to the slave or write some data and then read
as a single transaction enclosed in start/stop condition signals.
I2C_1.Start_Data_Exchange
(Slave => 16#1E#, -- Slave address 0 .. 0x7F
Buffer => Buffer'Address, -- Buffer to read/write
Write => 1, -- Number of bytes to write
Read => Buffer'Length, -- Number of bytes to read
Callback => Done);
The callback is called when the transfer is complete.
Configure an SPI device by specifying SCK, MISO, MOSI pins, and the speed:
SPI_1.Configure
(SCK => (STM32.PB, 3),
MISO => (STM32.PB, 4),
MOSI => (STM32.PB, 5),
Speed => 2_800_000); -- 2.8 MHz
Several SPI devices can be connected to the same SPI bus.
A specific device is activated by a dedicated pin usually called CS
(Chip Select).
SPI transfers are bidirectional. The user provides data to write in the buffer.
When the transfer is complete the buffer is filled with read data.
Initiate transfers using Start_Data_Exchange
:
SPI_1.Start_Data_Exchange
(CS => (STM32.PB, 6),
Buffer => Buffer'Address,
Size => Buffer'Length,
Callback => Done);
The callback is called when the transfer is complete.
A timer can be configured to generate a PWM (pulse width modulation) signal. To configure a timer provide a pin to which the PWM signal will be output and a base frequency.
TIM_3.Configure_PWM
(Pin => (STM32.PC, 8),
Speed => 1_000_000); -- 1 MHz (1µs per tick)
Start PWM signal generation with Start_PWM
providing
- period in cycles of base frequency
- duty cycle in cycles of base frequency
- a callback to be called when next PWM parameters could be set.
TIM_3.Start_PWM
(Period => 30_000, -- 30 ms = 30_000 * 1µs
Duty => 600, -- 600 µs = 600 * 1µs
Done => Done);
The timer with DMA support can be configured by passing it a pin for each active channel, the base frequency, and the period and duty values in pulses of this base frequency. For example
TIM_3.Configure_PWM
(Pins =>
(1 => (STM32.PA, 6), -- Channel 1
2 => (STM32.PC, 7)), -- Channel 2
Speed => 1_000_000, -- 1 MHz (1µs per tick)
Period => 30_000, -- 30 ms
Duty => 600); -- 600 µs
Now you can start continuous generation of the PWM signal in three different ways.
-
If you need a different period for each pulse, leave the pulse width constant you need to call
Start_PWM_With_Period
, passing it an array filled with the values of the desired periods and a callback. The callback will be called each time half of the buffer is transferred to the timer, allowing the user to change the values while the next half is being transferred to the timer.Buffer : STM32.Timers.Unsigned_16 (1 .. 10) := (2_000, 3_000, others => 1_000); begin Start_PWM_With_Period (Buffer, Done); -- Wait for Done is called, change Buffer (1 .. 5)
This will emit 2ms pulse, pause for 28ms, emit 3ms pulse, pause for 27ms, emit 1ms pulse, pause for 29ms, etc.
-
If you need a different pulse width for each pulse, leave the pulse period constant you need to call
Start_PWM_With_Duty
, passing it an array filled with the values of the desired widthws and a callback. Each active channel can have its own pulse width, so the array sequentially stores the width values for all active channels. Consider next example with two active channelsBuffer : STM32.Timers.Unsigned_16 (1 .. 10) := (2_000, 3_000, others => 1_000); begin Start_PWM_With_Duty (Buffer, Done); -- Wait for Done is called, change Buffer (1 .. 5)
This will emit 2ms pulse, pause for 28ms on
PA6
, at the same time emit 3ms pulse, pause for 27ms onPC7
, then next cyclce is started. -
It is possible to change both the pulse width in each active channel and the pulse period (common for all channels) with each pulse, but for this, channel 1 must be the first active channel (
Pins'First = 1
when callingConfigure_PWM
). In this case, the buffer will contain the period and width values for all active channels, the next period, and so on.Buffer : STM32.Timers.Unsigned_16 (1 .. 12) := (20_000, 2_000, 3_000, others => 1_000); begin Start_PWM (Buffer, Done); -- Wait for Done is called, change Buffer (1 .. 6)
This will emit 2ms pulse, pause for 18ms on
PA6
, at the same time emit 3ms pulse, pause for 17ms onPC7
, then next cyclce is started.
To stop generation call Stop
.
You can get unique identifier of the device using UID
and UID_Image
functions. They return the same value, but use different types
(Unsigned_64
and String (1 .. 8)
).
Put_Line (STM32.UIDs.UID_Image);
Procedures for flash memory include:
Unlock
andLock
for protection control.Erase_Sector
to erase a sector (it returns sector size).Programming
to enable writing.
Note. On STM32F407 the code can't be executed from flash memory while flash is being written/erased. On STM32F429 the code can be executed from one flash memory bank while another is being written/erased.
STM32.Flash.Unlock;
STM32.Flash.Erase_Sector
(Address => System'To_Address (16#0800_0000#),
Size => Sector_Size,
Done => Done);
-- Wait for the sector to erase, then write:
for J in 1 .. Sector_Size loop
STM32.Flash.Programming;
-- Write a word to 0x800_0000 + J - 1
end loop;
STM32.Flash.Lock;
This component of STM32F40x/41x processors allows for the management of static memory, flash memory, and PC Cards. Unlike the FMC in more advanced MCU models, dynamic memory is not supported. Each type of memory has its address bank. The controller offers flexible settings for various operating modes. For instance, extended modes can have different delays set for read and write operations. It is recommended to refer to the STM32 Reference Manual (RM0090) for a detailed description of possible modes and corresponding settings.
Example: Configuring Bank_1 for ILI9341 LCD controller:
- half word (16 bit access)
- with write enabled
- distinct read and write timings
STM32.FSMC.Configure
(Bank_1 =>
(1 => -- ILI9341 is connected to sub-bank 1
(Is_Set => True,
Value =>
(Write_Enable => True,
Bus_Width => STM32.FSMC.Half_Word,
Memory_Type => STM32.FSMC.SRAM,
Bus_Turn => 15, -- 90ns
Data_Setup => 57, -- 342ns
Address_Setup => 0,
Extended =>
(STM32.FSMC.Mode_A,
Write_Bus_Turn => 3, -- 18ns
Write_Data_Setup => 2, -- 12ns
Write_Address_Setup => 0),
others => <>)),
others => <>));
Contributions are welcome! Feel free to submit a pull request.
This project is licensed under the Apache 2.0 License with LLVM Exceptions. See the LICENSES files for details.