STM32學習筆記——USART


STM32的USART組件支持異步、同步、單線半雙工、多處理器、IrDA、LIN、SmartCard等模式,本文介紹的是異步即UART模式。

總線通信有三種模型:輪詢、中斷和DMA。DMA對我來說是陌生的內容,以后單獨開篇細講。

HAL

HAL把寄存器組組織成組件,組件包含外設的各個寄存器。在USART這里,寄存器不足以描述外設的所有狀態,HAL用handle來包裝組件。一個handle包含指向組件的指針、初始化參數、狀態、與其他組件的鏈接(如DMA)和內部狀態等。

圖源ST官方MOOC,打開之前注意調低音量。

USART的初始化除了USART本身的寄存器以外,還要設置GPIO的復用功能,這兩項任務分別在stm32f4xx_hal_uart.c中的HAL_UART_Initstm32f4xx_hal_msp.cHAL_UART_MspInit中完成(MSP意為“MCU Specific Package”)。stm32f4xx_hal_uart.c中也定義了HAL_UART_MspInit,添加了weak屬性(提供實現,允許被覆寫)。

輪詢

輪詢是與中斷相對的。對於發送,輪詢是指寫一個字節(或一個packet),等待它發送完,再寫下一個字節,直到所有數據被發送完才返回;對於接受,輪詢是指等待直到接收到一定長度的數據。輪詢相對簡單,但是效率很低。

#include "main.h"
#include <string.h>

UART_HandleTypeDef huart1;

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);
void uart_transmit(const char* string);

int main(void)
{
  char buffer[2] = {0};
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  uart_transmit("hello\n");
  while (1)
  {
    HAL_StatusTypeDef status = HAL_UART_Receive(&huart1, buffer, 1, 1000);
    if (status == HAL_OK)
    {
      uart_transmit("received: ");
      uart_transmit(buffer);
      uart_transmit("\n");
    }
    else
      uart_transmit("timeout\n");
  }
}

void uart_transmit(const char* string)
{
  HAL_UART_Transmit(&huart1, string, strlen(string), 1000);
}

static void MX_USART1_UART_Init(void)
{
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }
}

// ...

HAL的UART接收只能指定數據長度而不能指定終止符。在輪詢模式下,可以設置數據長度為1,即每次讀取一個字節,判斷它是否為終止符。

中斷

在中斷模式下,函數立即返回,數據在中斷中發送或接收。在發送或接收完成后,相應的回調函數會被調用。

#include "main.h"
#include <stdbool.h>
UART_HandleTypeDef huart1;
volatile bool finished = false;
char buffer[3] = {0};

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);
void uart_transmit(const char* string);
void uart_transmit_it(const char* string);

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  uart_transmit_it("hello\n");
  const char* info = finished ? "already finished\n" : "still transmitting\n";
  while (!finished)
    ;
  finished = false;
  uart_transmit_it(info);
  uart_transmit_it(info);
  while (!finished)
    ;
  while (1)
  {
    finished = false;
    HAL_UART_Receive_IT(&huart1, buffer, 2);
    while (!finished)
      ;
    uart_transmit("received: ");
    uart_transmit(buffer);
    uart_transmit("\n");
  }
}

void uart_transmit(const char* string)
{
  HAL_UART_Transmit(&huart1, string, strlen(string), 1000);
}

void uart_transmit_it(const char* string)
{
  HAL_UART_Transmit_IT(&huart1, string, strlen(string));
}

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart == &huart1)
  {
    finished = true;
  }
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart == &huart1)
  {
    finished = true;
  }
}

// ...

串口輸出still transmitting,說明HAL_UART_Transmit_IT確實是發送完成前就返回的;still transmitting只出現一次,因為第二次調用時第一次的發送還沒結束。

讀了HAL的源碼,我發現中斷發送的數據是拷貝指針的,也就是淺拷貝的,需要保證發送期間該地址上的數據有效。比如,如果一個函數把局部變量數組作為參數傳給HAL_UART_Transmit_IT,未等待發送完成便返回,那么發送的數據將會是錯誤的,甚至導致程序行為未定義。

如果給單片機發送了多余所需量的數據,程序會崩潰,我沒有debug出問題在哪。

緩沖區

這樣的接收連差強人意都算不上,我的終極目標是實現scanf那樣的接收函數。中斷發送只能緩沖一次和淺拷貝等問題也相當愚蠢,我想順便把發送也改造成printf。改造的工具是用循環隊列實現的緩沖區,這個我在AVR單片機教程中還煞有其事地寫過,正好可以作為現在的練習。

queue.h

#ifndef QUEUE_H
#define QUEUE_H

#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>

#ifdef __cplusplus
extern "C"
{
#endif

typedef struct
{
    uint16_t mask;
    uint16_t head;
    uint16_t tail;
    queue_element_t data[0];
} queue_t;

static inline queue_t* queue_create(uint16_t _size)
{
    if (_size & (_size - 1))
        _size = 256;
    queue_t* q = malloc(sizeof(queue_t) + _size * sizeof(queue_element_t));
    if (q)
    {
        q->mask = _size - 1;
        q->head = q->tail = 0;
    }
    return q;
}

static inline bool queue_empty(const volatile queue_t* _queue)
{
    return _queue->head == _queue->tail;
}

static inline bool queue_full(const volatile queue_t* _queue)
{
    return ((_queue->tail + 1) & _queue->mask) == _queue->head;
}

static inline uint16_t queue_size(const volatile queue_t* _queue)
{
    return (_queue->tail - _queue->head) & _queue->mask;
}

static inline uint16_t queue_capacity(const volatile queue_t* _queue)
{
    return _queue->mask;
}

static inline queue_element_t queue_peek(const volatile queue_t* _queue)
{
    return _queue->data[_queue->head];
}

static inline void queue_push(volatile queue_t* _queue, const queue_element_t _ele)
{
    _queue->data[_queue->tail] = _ele;
    _queue->tail = (_queue->tail + 1) & _queue->mask;
}

static inline void queue_pop(volatile queue_t* _queue)
{
    _queue->head = (_queue->head + 1) & _queue->mask;
}

#ifdef __cplusplus
}
#endif

#endif

inline遇到了點問題,原來C和C++中的inline是不一樣的!改成static inline就好了。有空再去深究這個問題。

main.c

#include "main.h"
#include <string.h>
#include "cmsis_gcc.h"
typedef char queue_element_t;
#include "queue.h"
UART_HandleTypeDef huart1;
queue_t* tx_buffer;
queue_t* rx_buffer;

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);
static void usart1_init_0();
static void usart1_init_2();
static void usart1_transmit(const char* string);
static void usart1_receive(char* dest, char delim);

int main(void)
{
  char buffer[80];
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  usart1_transmit("hello\n");
  while (1)
  {
    usart1_receive(buffer, '\n');
    usart1_transmit("received: ");
    usart1_transmit(buffer);
    usart1_transmit("\n");
  }
}

void usart1_init_0()
{
  tx_buffer = queue_create(1024);
  rx_buffer = queue_create(1024);
}

void usart1_init_2()
{
  USART1->CR1 |= USART_CR1_RXNEIE & UART_IT_MASK;
}

void usart1_transmit(const char* string)
{
  uint16_t capacity = queue_capacity(tx_buffer);
  uint16_t size = strlen(string);
  bool ok = false;
  while (1)
  {
    __disable_irq();
    ok = capacity - queue_size(tx_buffer) >= size;
    if (ok)
        break;
    __enable_irq();
    __NOP();
  }
  for (uint16_t i = 0; i != size; ++i)
      queue_push(tx_buffer, string[i]);
  USART1->CR1 |= USART_CR1_TXEIE & UART_IT_MASK;
  __enable_irq();
}

void usart1_receive(char* dest, char delim)
{
  while (1)
  {
    bool ok = false;
    while (1)
    {
      __disable_irq();
      ok = !queue_empty(rx_buffer);
      if (ok)
        break;
      __enable_irq();
      __NOP();
    }
    char c = queue_peek(rx_buffer);
    queue_pop(rx_buffer);
    __enable_irq();
    if (c == delim)
      break;
    *dest++ = c;
  }
  *dest = '\0';
}

void usart1_transmit_handler()
{
  USART1->DR = queue_peek(tx_buffer);
  queue_pop(tx_buffer);
  if (queue_empty(tx_buffer))
    USART1->CR1 &= ~USART_CR1_TXEIE & UART_IT_MASK;
}

void usart1_receive_handler()
{
  queue_push(rx_buffer, USART1->DR);
}

void USART1_IRQHandler(void)
{
  uint32_t isrflags   = USART1->SR;
  uint32_t cr1its     = USART1->CR1;
  uint32_t errorflags = 0x00U;
  errorflags = (isrflags & (uint32_t)(USART_SR_PE | USART_SR_FE | USART_SR_ORE | USART_SR_NE));
  if (errorflags == RESET)
  {
    if (((isrflags & USART_SR_RXNE) != RESET) && ((cr1its & USART_CR1_RXNEIE) != RESET))
    {
      usart1_receive_handler();
      return;
    }
    if (((isrflags & USART_SR_TXE) != RESET) && ((cr1its & USART_CR1_TXEIE) != RESET))
    {
      usart1_transmit_handler();
      return;
    }
  }
  
  HAL_UART_IRQHandler(&huart1);
}

static void MX_USART1_UART_Init(void)
{
  usart1_init_0();
  
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }
  
  usart1_init_2();
}

中斷的調用流程是:USART1中斷請求調用USART1_IRQHandler(這個名字在startup_stm32f407vetx.s中定義),由STM32CubeMX生成的USART1_IRQHandler調用HAL_UART_IRQHandler,里面進行各種判斷和處理,在合適的時機調用HAL_UART_TxCpltCallback等。我在USART1_IRQHandler中插入了一些代碼,把TXERXNE兩種中斷攔截了下來,其余還是丟給HAL_UART_IRQHandler處理(Chain of Responsibility設計模式?)。

queue上的操作不是原子的,主函數與中斷共享需要加鎖。__disable_irq關閉全局中斷,__enable_irq開啟全局中斷。ARM說在開中斷之后Cortex-M3/4還可能執行2條指令才響應中斷,而在匯編代碼中cpsie后第二句就是cpsid,所以我在__enable_irq后加一句__NOP空指令,以保證中斷請求能被響應。

printfscanf只有一步之遙了,但我想把它放到下一篇。20pin的ST-LINK/V2已經在路上了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM