------------------------------------------------------------------------------ -- -- -- Copyright (C) 2017-2020, AdaCore -- -- -- -- Redistribution and use in source and binary forms, with or without -- -- modification, are permitted provided that the following conditions are -- -- met: -- -- 1. Redistributions of source code must retain the above copyright -- -- notice, this list of conditions and the following disclaimer. -- -- 2. Redistributions in binary form must reproduce the above copyright -- -- notice, this list of conditions and the following disclaimer in -- -- the documentation and/or other materials provided with the -- -- distribution. -- -- 3. Neither the name of the copyright holder nor the names of its -- -- contributors may be used to endorse or promote products derived -- -- from this software without specific prior written permission. -- -- -- -- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -- -- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -- -- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -- -- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -- -- HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -- -- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -- -- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -- -- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -- -- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -- -- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -- -- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -- -- -- ------------------------------------------------------------------------------ with HAL; use HAL; with nRF.ADC; use nRF.ADC; with nRF.Device; use nRF.Device; with nRF.PPI; use nRF.PPI; with nRF.Timers; use nRF.Timers; with nRF.GPIO.Tasks_And_Events; use nRF.GPIO.Tasks_And_Events; with nRF.Events; use nRF.Events; with nRF.Interrupts; use nRF.Interrupts; package body MicroBit.IOs is -- The analog out feature is implemented as PWM signal. To generate the PWM -- signals we use a timer with the configuration described bellow. -- -- Because of the limited number of timer comparators and GPIOTE channels, -- we can only have 3 PWMs on the system at the same time. However there -- are 5 pins allowed to use PWM, so we need to dynamicaly allocate the -- PWM based on user requests. -- -- Timer configuration: -- -- Comparator 0, 1, 2 are used to control the pulse width of the 3 PWMs. -- Each of those comparator is associated with a PWM and a pin. When the -- timer counter reaches the value of a comparator, the associated pin -- toggles. -- -- Comparator 3 is use to control the period. When the timer counter reaches -- its value, all pins toggle. -- -- Comparator 3 also trigger an interrupt. In the handler for this -- interrupt, we update all the comparator values and start the timer again. -- -- -- Int handler and timer start Cmp 0 Cmp 1 Cmp 2 Cmp3, Timer stop and interrupt -- v v v v v -- _______________________________ ____ -- |_______________________| -- ______________________________________ ____ -- |________________| -- _____________________________________________ ____ -- |_________| -- -- ^------------------ Timer loop sequence -------------------^ -- -- Since all the timer events trigger a toggle of the pin, we have to make -- sure that the pin is at a good state (high) when starting the timer, -- otherwise the waveform could be inverted. This is why the GPIO channels -- are always configured when the timer is reconfigured. -- -- PPI and GPIOTE: -- -- To trigger a pin toggle from the timer compare events we use the -- following configuation. -- -- Two PPI channels are used for each PWM pin. For a PWM X, one PPI channel -- is used to trigger a GPIOTE task on comparator X event, a second PPI -- channel is used to trigger a GPIOTE event on comparator 3 event. So -- the comparator 3 event is used by all PWMs. -- -- For a PWM X, GPIOTE channel X is configure to do a pin toggle when its -- task is activated by one of the two PPI channels described above. -- We keep track of the current mode of the pin to be able to detect when a -- change of configuration is needed. type Pin_Mode is (None, Digital_In, Digital_Out, Analog_In, Analog_Out); Current_Mode : array (Pin_Id) of Pin_Mode := (others => None); -- PWM -- Number_Of_PWMs : constant := 3; type PWM_Allocated is range 0 .. Number_Of_PWMs; subtype PWM_Id is PWM_Allocated range 0 .. Number_Of_PWMs - 1; No_PWM : constant PWM_Allocated := Number_Of_PWMs; PWM_Alloc : array (Pin_Id) of PWM_Allocated := (others => No_PWM); PWM_Timer : Timer renames Timer_0; PWM_Interrupt : constant Interrupt_Name := TIMER0_Interrupt; PWM_Global_Compare : constant Timer_Channel := 3; PWM_Precision : constant := 4; PWM_Period : UInt32 := 2_000 / PWM_Precision; type PWM_Status is record Taken : Boolean := False; Pulse_Width : Analog_Value; Cmp : UInt32 := 10; Pin : Pin_Id; end record; PWMs : array (PWM_Id) of PWM_Status; function Has_PWM (Pin : Pin_Id) return Boolean is (PWM_Alloc (Pin) /= No_PWM); procedure Allocate_PWM (Pin : Pin_Id; Success : out Boolean) with Pre => not Has_PWM (Pin); procedure Deallocate_PWM (Pin : Pin_Id) with Pre => Has_PWM (Pin), Post => not Has_PWM (Pin); procedure Configure_PPI (Id : PWM_Id); procedure Configure_GPIOTE (Id : PWM_Id); procedure Init_PWM_Timer; function To_Compare_Value (V : Analog_Value) return UInt32; procedure PWM_Timer_Handler; ---------------------- -- To_Compare_Value -- ---------------------- function To_Compare_Value (V : Analog_Value) return UInt32 is Cmp : constant UInt32 := UInt32 (Float (PWM_Period) * (Float (V) / Float (Analog_Value'Last))); begin if Cmp = 0 then return 1; elsif Cmp >= PWM_Period then return PWM_Period - 1; else return Cmp; end if; end To_Compare_Value; ------------------ -- Allocate_PWM -- ------------------ procedure Allocate_PWM (Pin : Pin_Id; Success : out Boolean) is begin for Id in PWM_Id loop if not PWMs (Id).Taken then PWMs (Id).Taken := True; PWMs (Id).Pin := Pin; PWM_Alloc (Pin) := Id; Configure_PPI (Id); Success := True; return; end if; end loop; Success := False; end Allocate_PWM; -------------------- -- Deallocate_PWM -- -------------------- procedure Deallocate_PWM (Pin : Pin_Id) is begin if PWM_Alloc (Pin) /= No_PWM then nRF.GPIO.Tasks_And_Events.Disable (GPIOTE_Channel (PWM_Alloc (Pin))); PWMs (PWM_Alloc (Pin)).Taken := False; PWM_Alloc (Pin) := No_PWM; end if; end Deallocate_PWM; ------------------- -- Configure_PPI -- ------------------- procedure Configure_PPI (Id : PWM_Id) is Chan1 : constant Channel_ID := Channel_ID (Id) * 2; Chan2 : constant Channel_ID := Chan1 + 1; begin -- Use one PPI channel to triggerd GPTIOTE OUT task on the compare event -- associated with this PWM_Id; nRF.PPI.Configure (Chan => Chan1, Evt_EP => PWM_Timer.Compare_Event (Timer_Channel (Id)), Task_EP => Out_Task (GPIOTE_Channel (Id))); -- Use another PPI channel to triggerd GPTIOTE OUT task on compare 3 event nRF.PPI.Configure (Chan => Chan2, Evt_EP => PWM_Timer.Compare_Event (PWM_Global_Compare), Task_EP => Out_Task (GPIOTE_Channel (Id))); nRF.PPI.Enable_Channel (Chan1); nRF.PPI.Enable_Channel (Chan2); end Configure_PPI; ---------------------- -- Configure_GPIOTE -- ---------------------- procedure Configure_GPIOTE (Id : PWM_Id) is begin -- Configure the GPIOTE OUT task to toggle the pin nRF.GPIO.Tasks_And_Events.Enable_Task (Chan => GPIOTE_Channel (Id), GPIO_Pin => Points (PWMs (Id).Pin).Pin, Action => Toggle_Pin, Initial_Value => Init_Set); end Configure_GPIOTE; ----------------------- -- PWM_Timer_Handler -- ----------------------- procedure PWM_Timer_Handler is begin Clear (PWM_Timer.Compare_Event (PWM_Global_Compare)); PWM_Timer.Set_Compare (PWM_Global_Compare, PWM_Period); PWM_Timer.Set_Compare (0, PWMs (0).Cmp); PWM_Timer.Set_Compare (1, PWMs (1).Cmp); PWM_Timer.Set_Compare (2, PWMs (2).Cmp); PWM_Timer.Start; end PWM_Timer_Handler; -------------------- -- Init_PWM_Timer -- -------------------- procedure Init_PWM_Timer is begin PWM_Timer.Set_Mode (Mode_Timer); PWM_Timer.Set_Prescaler (6); PWM_Timer.Set_Bitmode (Bitmode_32bit); -- Clear counter internal register and stop when timer reaches compare -- value 3. PWM_Timer.Compare_Shortcut (Chan => PWM_Global_Compare, Stop => True, Clear => True); PWM_Timer.Set_Compare (PWM_Global_Compare, PWM_Period); for Id in PWM_Id loop PWM_Timer.Set_Compare (Timer_Channel (Id), To_Compare_Value (PWMs (Id).Pulse_Width)); if PWMs (Id).Taken then Configure_GPIOTE (Id); end if; end loop; Enable_Interrupt (PWM_Timer.Compare_Event (PWM_Global_Compare)); nRF.Interrupts.Register (PWM_Interrupt, PWM_Timer_Handler'Access); nRF.Interrupts.Enable (PWM_Interrupt); end Init_PWM_Timer; --------- -- Set -- --------- procedure Set (Pin : Pin_Id; Value : Boolean) is Pt : GPIO_Point renames Points (Pin); Conf : GPIO_Configuration; begin if Current_Mode (Pin) /= Digital_Out then if Has_PWM (Pin) then Deallocate_PWM (Pin); end if; Conf.Mode := Mode_Out; Conf.Resistors := No_Pull; Conf.Input_Buffer := Input_Buffer_Connect; Conf.Sense := Sense_Disabled; Pt.Configure_IO (Conf); Current_Mode (Pin) := Digital_Out; end if; if Value then Pt.Set; else Pt.Clear; end if; end Set; --------- -- Set -- --------- function Set (Pin : Pin_Id) return Boolean is Pt : GPIO_Point renames Points (Pin); Conf : GPIO_Configuration; begin if Current_Mode (Pin) /= Digital_In then if Has_PWM (Pin) then Deallocate_PWM (Pin); end if; Conf.Mode := Mode_In; Conf.Resistors := No_Pull; Conf.Input_Buffer := Input_Buffer_Connect; Conf.Sense := Sense_Disabled; Pt.Configure_IO (Conf); Current_Mode (Pin) := Digital_In; end if; return Pt.Set; end Set; -------------------------- -- Set_Analog_Period_Us -- -------------------------- procedure Set_Analog_Period_Us (Period : Natural) is begin PWM_Period := UInt32 (Period) / PWM_Precision; -- Update the comparator values for ech PWM for PWM of PWMs loop PWM.Cmp := To_Compare_Value (PWM.Pulse_Width); end loop; end Set_Analog_Period_Us; ----------- -- Write -- ----------- procedure Write (Pin : Pin_Id; Value : Analog_Value) is Success : Boolean; Pt : GPIO_Point renames Points (Pin); Conf : GPIO_Configuration; begin if not Has_PWM (Pin) then -- Stop the timer while we configure a new pin PWM_Timer.Stop; PWM_Timer.Clear; Allocate_PWM (Pin, Success); if not Success then raise Program_Error with "No PWM available"; end if; -- Set the pin as output Conf.Mode := Mode_Out; Conf.Resistors := No_Pull; Conf.Input_Buffer := Input_Buffer_Connect; Conf.Sense := Sense_Disabled; Pt.Configure_IO (Conf); Pt.Clear; Current_Mode (Pin) := Analog_Out; Init_PWM_Timer; PWM_Timer.Start; end if; PWMs (PWM_Alloc (Pin)).Pulse_Width := Value; PWMs (PWM_Alloc (Pin)).Cmp := To_Compare_Value (Value); end Write; ------------ -- Analog -- ------------ function Analog (Pin : Pin_Id) return Analog_Value is Result : UInt16; begin if Current_Mode (Pin) /= Analog_In then if Has_PWM (Pin) then Deallocate_PWM (Pin); end if; Current_Mode (Pin) := Analog_In; end if; Result := Do_Pin_Conversion (Pin => (case Pin is when 0 => 0, when 1 => 1, when 2 => 2, when 3 => 7, when 4 => 4, when 10 => 6, when others => 5), Input => Pin_One_Forth, Ref => VDD_One_Forth, Res => Res_10bit); if Result > UInt16(Analog_Value'Last) then Result := 0; end if; return Analog_Value (Result); end Analog; end MicroBit.IOs;