• 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
    ");
      while (1)
      {
        HAL_StatusTypeDef status = HAL_UART_Receive(&huart1, buffer, 1, 1000);
        if (status == HAL_OK)
        {
          uart_transmit("received: ");
          uart_transmit(buffer);
          uart_transmit("
    ");
        }
        else
          uart_transmit("timeout
    ");
      }
    }
    
    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
    ");
      const char* info = finished ? "already finished
    " : "still transmitting
    ";
      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("
    ");
      }
    }
    
    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
    ");
      while (1)
      {
        usart1_receive(buffer, '
    ');
        usart1_transmit("received: ");
        usart1_transmit(buffer);
        usart1_transmit("
    ");
      }
    }
    
    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 = '';
    }
    
    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已经在路上了。

  • 相关阅读:
    LIKE谓词
    [C#网络编程系列]专题一:网络协议简介
    (zz)Sql Server 2005中的架构(Schema)、用户(User)、角色(Role)和登录(Login)(三)
    zz让你成功的九个心理定律
    zz给 VSTO 插件、文档传送参数
    重构笔记
    (zz)Sql Server 2005中的架构(Schema)、用户(User)、角色(Role)和登录(Login)(二)
    zzVSTO 先瘦身再发布:客户端配置文件
    zz将 VSTO 插件部署给所有用户
    (zz)Sql Server 2005中的架构(Schema)、用户(User)、角色(Role)和登录(Login)(一)
  • 原文地址:https://www.cnblogs.com/jerry-fuyi/p/12907026.html
Copyright © 2020-2023  润新知