Search code examples
cstm32usart

Reading PS/2 Keyboard Scan Codes on STM32 L476RG


I am trying to read scan codes from a PS/2 keyboard on an STM32 L476RG (NUCLEO L476RG dev board).

PS/2 and USART:

I believe that I can use the USART hardware on the STM32 to read PS/2 data, if there is a reason why not then please help me understand. From my reading and observations with a scope, the PS/2 protocol sends the following packet format on the data line:

  1. 1 start bit. This is always 0.
  2. 8 data bits, least significant bit first.
  3. 1 parity bit (odd parity).
  4. 1 stop bit. This is always 1.

Each bit is captured on the falling edge of a separate clock signal that idles high. The clock on my particular keyboard sits at around 13.1 kHz.

Both systems will output at 3.3V from the keyboard if given 3.3V power, a a scope trace shoes that these waveforms are just fine while connected to the STM32.

What's Going Wrong

Basically, the system is not generating an interrupt when a key is pressed. The RXNE (receive buffer not empty flag) stays low and no interrupts are generated.

I go into more detail below, but if I use auto baud rate in "0x55" mode, an interrupt is triggered, but only because the auto baud rate error flag is set. All other modes do nothing (no interrupts).

What I've Tried

I use the following code to initialize USART1 on the STM32 to read from the keyboard:

void init_keyboard(uint32_t baud)
{
// Enable clock to GPIOA
RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN;

// Enable clock to USART1
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;

// Configure UART
USART1->CR1 |= USART_CR1_RE  |   // Receiver enabled
               USART_CR1_PCE |   // Parity control enabled
               USART_CR1_PS  |   // Odd parity
               USART_CR1_M0  |   // 8 data bits
               USART_CR1_RXNEIE; // Enable RX interrupt

USART1->CR1 &= ~(USART_CR1_TE);

USART1->CR2 |= USART_CR2_CLKEN     | // Enable USART clock (sync mode)
               USART_CR2_ABREN     | // Enable auto Baud Rate
               USART_CR2_CPOL;       // Clock polarit 1 (idle high)


// The following need to be cleared for sync mode
USART1->CR2 &= ~(USART_CR2_LINEN);
USART1->CR3 &= ~(USART_CR3_SCEN  |
                 USART_CR3_HDSEL |
                 USART_CR3_IREN);

USART1->BRR |= (SystemCoreClock / baud); // set baud rate

USART1->CR1 |= USART_CR1_UE;

NVIC->ISER[1] |= (1 << (USART1_IRQn & 0x1F));

// Configure AF pins 8 and 10 to AF7 (USART)
GPIOA->AFR[1]  |= (GPIO_AF7_USART1 << 0);
GPIOA->AFR[1]  |= (GPIO_AF7_USART1 << 8);

// Configure PA8 (CK) and PA10 (RX) in alt. func. mode
GPIOA->MODER &= ~(GPIO_MODER_MODE8   | GPIO_MODER_MODE10);
GPIOA->MODER |=  (GPIO_MODER_MODE8_1 | GPIO_MODER_MODE10_1);

// Enable Global Interrupts
__enable_irq();
}

The data bits are LSB first by default, the capture is on the first edge (falling edge) by default and I confirmed that this baud rate / alternate function code works on a standard UART connection at 9600 baud.

I am also trying to use the auto baud rate detect since the baud rate of the keyboard seems to drift throughout the day (12.9 - 13.2 kHz) and I don't trust the baud rate to be stable. I tried with and without the auto baud rate and I tried auto baud rate in all 4 modes: (start bit measurement, falling-to-falling measurement, 0x55 frame detection, 0x7F frame detection). The only one that gives me anything is 0x7F, which throws an auto baud rate error whenever I read a code other than 0x55 (press a key besides '+'), but does nothing when I send a frame with 0x55 (press '+' key).

And just in case you're wondering, I did call the function in main. :)

/* ISR for handling keypresses */
void USART1_IRQHandler(void);

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  init_keyboard(13120);

  while (1) {/* wait for interrupts */ }
}


void USART1_IRQHandler(void)
{
    if (USART1->ISR & USART_ISR_RXNE)
    {
        char read_byte = USART1->RDR;
        while (1) {/* catch here for debugging */ }
    }
}

Solution

  • Thanks to Flexz for spotting the following:

    STM32L476 is missing synchronous slave description section and SLVEN bit in the CR2 register, which are present in the STM32L4+ series, i.e L476 can only be a master in USART-sync mode.

    This means that I can't read from the PS/2 device as a slave, at least not using the clock. I can, however still read from the RX port asynchronously. This means that I do not want to enable CLKEN in USARTx->CR2.

    If I treat the line asynchronously, then I can use the auto baud rate in mode 00 to capture the data even if the baud rate drifts, as it does on this keyboard.

    Final Code:

    void init_keyboard(uint32_t baud)
    {
        //clear_buffer();
    
        // Enable clock to GPIOA
        RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN;
    
        // Enable clock to USART1
        RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    
        // Configure UART
        USART1->CR1 |= USART_CR1_RE  |   // Receiver enabled
                       USART_CR1_PCE |   // Parity control enabled
                       USART_CR1_PS  |   // Odd parity
                       USART_CR1_M0  |   // 8 data bits
                       USART_CR1_RXNEIE; // Enable RX interrupt
    
        USART1->CR1 &= ~(USART_CR1_TE);
    
        USART1->CR2 |= USART_CR2_ABREN;  // Enable auto Baud Rate
    
        USART1->CR3 |= USART_CR3_OVRDIS; // Disable overrun detection
    
        USART1->BRR |= (SystemCoreClock / baud);
    
        USART1->CR1 |= USART_CR1_UE;
    
        NVIC->ISER[1] |= (1 << (USART1_IRQn & 0x1F));
    
        // Configure AF pin10 to AF7 (USART)
        GPIOA->AFR[1]  |= (GPIO_AF7_USART1 << 8);
    
        // Configure PA10 (RX) in alt. func. mode
        GPIOA->MODER &= ~(GPIO_MODER_MODE10);
        GPIOA->MODER |=  (GPIO_MODER_MODE10_1);
    
        // Enable Global Interrupts
        __enable_irq();
    }