Inspirel banner

8. Digital Input

We have already managed to configure two pins for digital output. This allows us to drive output state of selected pins, but is not enough for any reasonable control system that can react to external events. For this we also need the ability to read the state of selected pins.

Fortunately, most of the conceptual work was already done and the exploration paths established in previous chapters are still valid. Still, there is one thing that deserves additional explanation.

The general-purpose input pins can work in several modes that differ in their electrical properties. Note that not all modes are available in each microcontroller:

Neither of these modes is better or worse than the other, as they solve different problems and can be both useful in different designs - we will try to handle both of these two cases.

Note also that in all of our boards, the peripheral needs to be fed by the clock signal, otherwise reading the pin level will not be possible, and that clock is disabled by default after reset. This means that as part of the I/O configuration we also need to enable appropriate clock line - this was already necessary with the Nucleo boards even for simple output, but for digital input all microcontrollers require proper peripheral clocking.

In short, the above rules can be summarized as follows, differently for each board, but with some common patterns that should be easy to follow.

Arduino M0

Since the I/O pins are ready for operation by default, the configuration step does not need to involve enabling the clock signal, as is the case in other boards. The procedure for configuring the pin for digital input will be:

Note that the GPIOx_PINCFGy register should be accessed as an 8-bit entity - in order to make it easier we can add the following definition to the package Registers:

   type Byte is mod 2**8;

and we will use this type in later definitions of relevant register objects.

Once the pin is configured for input, and in order to read its current input state:

Arduino Due

In order to configure the pin for digital input we need to:

and in order to read the current input state:

STM32 Nucleo-32 and Nucleo-144

In order to configure the pin for digital input we need to:

and in order to read the current input state:


The Arduino-compatible boards have several rows of pins. We can focus on pins numbered from 2 to 13 - we have already discovered how to control pins 11 and 12, now we can extend this set.

Arduino M0

For Arduino M0 the controller mapping is:

Some of the registers that are needed here were already covered in previous examples, but our linker script will have to be extended to the following set of entries:

    GPIOA_DIRCLR = 0x41004404;
    GPIOA_DIRSET = 0x41004408;
    GPIOA_OUTCLR = 0x41004414;
    GPIOA_OUTSET = 0x41004418;
    GPIOA_IN = 0x41004420;
    GPIOA_PINCFG14 = 0x4100444e;
    GPIOA_PINCFG09 = 0x41004449;
    GPIOA_PINCFG08 = 0x41004448;
    GPIOA_PINCFG15 = 0x4100444f;
    GPIOA_PINCFG20 = 0x41004454;
    GPIOA_PINCFG21 = 0x41004455;
    GPIOA_PINCFG06 = 0x41004446;
    GPIOA_PINCFG07 = 0x41004447;
    GPIOA_PINCFG18 = 0x41004452;
    GPIOA_PINCFG16 = 0x41004450;
    GPIOA_PINCFG19 = 0x41004453;
    GPIOA_PINCFG17 = 0x41004451;

Fortunately, all pins that we want to cover are mapped to lines belonging to the same I/O port.

Arduino Due

For Arduino Due the controller mapping is:

Some of the registers that are needed here were already covered in previous examples, but our linker script will have to be extended by these entries:

    PIOB_PER = 0x400E1000;
    PIOB_OER = 0x400E1010;
    PIOB_ODR = 0x400E1014;
    PIOB_PUER = 0x400E1064;
    PIOB_PUDR = 0x400E1060;
    PIOB_SODR = 0x400E1030;
    PIOB_CODR = 0x400E1034;
    PIOB_PDSR = 0x400E103C;

    PIOC_PER = 0x400E1200;
    PIOC_OER = 0x400E1210;
    PIOC_ODR = 0x400E1214;
    PIOC_PUER = 0x400E1264;
    PIOC_PUDR = 0x400E1260;
    PIOC_SODR = 0x400E1230;
    PIOC_CODR = 0x400E1234;
    PIOC_PDSR = 0x400E123C;

    PIOD_ODR = 0x400E1414;
    PIOD_PUER = 0x400E1464;
    PIOD_PUDR = 0x400E1460;
    PIOD_PDSR = 0x400E143C;
    
    PMC_PCER0 = 0x400E0610;

STM32 Nucleo-32

For Nucleo-32 the pin mapping is:

Some of the registers that are needed here were already covered in previous examples, but our linker script will have to be extended by these entries:

    GPIOA_MODER = 0x48000000;
    GPIOA_PUPDR = 0x4800000c;
    GPIOA_IDR = 0x48000010;
    GPIOA_BSRR = 0x48000018;
    GPIOA_BRR = 0x48000028;
    
    GPIOB_MODER = 0x48000400;
    GPIOB_PUPDR = 0x4800040c;
    GPIOB_IDR = 0x48000410;
    GPIOB_BSRR = 0x48000418;
    GPIOB_BRR = 0x48000428;
    
    GPIOF_MODER = 0x48001400;
    GPIOF_PUPDR = 0x4800140c;
    GPIOF_IDR = 0x48001410;
    GPIOF_BSRR = 0x48001418;
    GPIOF_BRR = 0x48001428;

STM32 Nucleo-144

For Nucleo-144 the pin mapping is:

Some of the registers that are needed here were already covered in previous examples, but our linker script will have to be extended by these entries:

    GPIOA_MODER = 0x40020000;
    GPIOA_PUPDR = 0x4002000c;
    GPIOA_IDR = 0x40020010;
    GPIOA_BSRR = 0x40020018;
    
    GPIOD_MODER = 0x40020c00;
    GPIOD_PUPDR = 0x4002c0c;
    GPIOD_IDR = 0x40020c10;
    GPIOD_BSRR = 0x40020c18;
    
    GPIOE_MODER = 0x40021000;
    GPIOE_PUPDR = 0x4002100c;
    GPIOE_IDR = 0x40021010;
    GPIOE_BSRR = 0x40021018;
    
    GPIOF_MODER = 0x40021400;
    GPIOF_PUPDR = 0x4002140c;
    GPIOF_IDR = 0x40021410;
    GPIOF_BSRR = 0x40021418;

Apart from extending the linker scripts, the Registers package should be extended with appropriate definitions as well, appropriately for each board.

Adding the input capability requires an extension of the Pins package, which now contains the following procedure:

   procedure Enable_Output (Pin : in Pin_ID);

An obvious extension of this interface would be a new Enable_Input procedure, but with two different modes of operation we will use additional parameter to select the required mode (the two input modes seem to be common for our boards):

   type Input_Mode is (Direct, Pulled_Up);
   
   procedure Enable_Input (Pin : in Pin_ID; Mode : in Input_Mode);

Note that the choice of this interface is subjective and other options, like using separate procedures for each mode, could be used as well.

The Enable_Output procedure body (in the pins.adb file) will have to be extended to cover more pins, but the extension is straightforward. The new procedure for configuring input pins is more interesting.

Arduino M0

For Arduino M0 the Enable_Input can have the following structure:

   procedure Enable_Input (Pin : in Pin_ID; Mode : in Input_Mode) is
   begin
      case Pin is
         when Pin_2 =>
            Registers.GPIOA_DIRCLR := 2#100_0000_0000_0000#; -- bit 14
            
            if Mode = Pulled_Up then
               Registers.GPIOA_PINCFG16 := 2#0000_0110#;
               Registers.GPIOA_OUTSET := 2#100_0000_0000_0000#;
            else
               Registers.GPIOA_PINCFG16 := 2#0000_0010#;
            end if;

         --  other pins accordingly
         --  ...

      end case;
   end Enable_Input;

Note that, interestingly, the output register GPIOA_OUTSET takes part in configuring the pull-up resistor as well.

Arduino Due

For Arduino Due the Enable_Input can have the following structure:

   procedure Enable_Input (Pin : in Pin_ID; Mode : in Input_Mode) is
   begin
      case Pin is
         when Pin_2 =>
            Registers.PMC_PCER0 :=
               2#1_0000_0000_0000#; -- bit 12
            Registers.PIOB_PER :=
               2#10_0000_0000_0000_0000_0000_0000#; -- bit 25
            Registers.PIOB_ODR :=
               2#10_0000_0000_0000_0000_0000_0000#; -- bit 25
            
            if Mode = Pulled_Up then
               Registers.PIOB_PUER :=
                  2#10_0000_0000_0000_0000_0000_0000#; -- bit 25
            else
               Registers.PIOB_PUDR :=
                  2#10_0000_0000_0000_0000_0000_0000#; -- bit 25
            end if;

         --  other pins accordingly
         --  ...
         
      end case;
   end Enable_Input;

STM32 Nucleo-32 and Nucleo-144

For Nucleo-32 the Enable_Input can have the following structure:

   procedure Enable_Input (Pin : in Pin_ID; Mode : in Input_Mode) is
      use type Registers.Word;
   begin
      case Pin is
         when Pin_2 =>
            Registers.RCC_AHBENR := Registers.RCC_AHBENR
               or 2#10_0000_0000_0000_0000#;

            Registers.GPIOA_MODER := Registers.GPIOA_MODER
               and 2#1111_1100_1111_1111_1111_1111_1111_1111#;
            
            if Mode = Pulled_Up then
               Registers.GPIOA_PUPDR := (Registers.GPIOA_PUPDR
                  and 2#1111_1100_1111_1111_1111_1111_1111_1111#)
                   or 2#0000_0001_0000_0000_0000_0000_0000_0000#;
            else
               Registers.GPIOA_PUPDR := Registers.GPIOA_PUPDR
                  and 2#1111_1100_1111_1111_1111_1111_1111_1111#;
            end if;

         --  other pins accordingly
         --  ...
         
      end case;
   end Enable_Input;

The code structure for Nucleo-144 will be similar, differing in the use of RCC_AHB1ENR instead of RCC_AHBENR and in the actual bit patterns, appropriately to the pin mappings.


Last but not least, we need some operation for reading current state of the input pin. We can try with the following, complementary to the existing Write procedure.

Arduino M0

For Arduino Due, reading the state of input pin can be done as follows:

   procedure Read (Pin : in Pin_ID; State : out Boolean) is
      Data : Registers.Word;
   begin
      case Pin is
         when Pin_2 =>
            Data := Registers.GPIOA_IN;
            State := (Data and 2#100_0000_0000_0000#) /= 0;

         --  other pins accordingly
         --  ...
         
      end case;
   end Read;

Arduino Due

For Arduino Due, reading the state of input pin can be done as follows:

   procedure Read (Pin : in Pid_ID; State : out Boolean) is
      Data : Registers.Word;
   begin
      case Pin is
         when Pin_2 =>
            Data := Registers.PIOB_PDSR;
            State :=
               (Data and 2#10_0000_0000_0000_0000_0000_0000#) /= 0;
            
         --  other pins accordingly
         --  ...
         
      end case;
   end Read;

STM32 Nucleo-32 and Nucleo-144

For Nucleo-32, reading the state of input pin can be done as follows:

   procedure Read (Pin : in Pid_ID; State : out Boolean) is
      Data : Registers.Word;
   begin
      case Pin is
         when Pin_2 =>
            Data := Registers.GPIOB_IDR;
            State :=
               (Data and 2#1_0000_0000_0000#) /= 0;
            
         --  other pins accordingly
         --  ...
         
      end case;
   end Read;

As already noticed above, the code for Nucleo-144 will be very similar and will differ only in actual ports and bit patterns.


Note that in the Read procedure the State parameter has an output mode, similarly to our earlier procedure for reading random numbers.

The new procedures and register definitions allow us to configure any of the pins numbered from 2 to 13 for both input and output as well as to read and write them. Just for the sake of basic testing, the following program copies the logical state from pin 2 (configured as input) to pin 3 (configured as output):

with Pins;

package body Program is

   procedure Run is
      State : Boolean;
   begin
      Pins.Enable_Input (Pins.Pin_2, Pins.Pulled_Up);
      Pins.Enable_Output (Pins.Pin_3);
      
      loop
         Pins.Read (Pins.Pin_2, State);
         Pins.Write (Pins.Pin_3, State);
      end loop;
   end Run;
   
end Program;

There were no new Ada elements in this chapter, but with the complete control over the whole row of digital pins we are now well prepared for some more realistic projects involving interactions with the external world.

Previous: Random Numbers, next: Finite State Machines, Part 1.
See also Table of Contents.