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已经在路上了。

STM32学习笔记——USART的更多相关文章

  1. STM32学习笔记——USART串口

    转载自:http://www.cnblogs.com/microxiami/p/3752715.html 一.USART简介 通用同步异步收发器(USART)提供了一种灵活的方法与使用工业标准NRZ异 ...

  2. STM32学习笔记——USART串口(向原子哥和火哥学习)

    一.USART简介 通用同步异步收发器(USART)提供了一种灵活的方法与使用工业标准NRZ异步串行数据格式的外部设备之间进行全双工数据交换.USART利用分数波特率发生器提供宽范围的波特率选择. S ...

  3. STM32学习笔记——OLED屏

    STM32学习笔记--OLED屏 OLED屏的特点: 1.  模块有单色和双色可选,单色为纯蓝色,双色为黄蓝双色(本人选用双色): 2.  显示尺寸为0.96寸 3.  分辨率为128*64 4.   ...

  4. STM32学习笔记——点亮LED

    STM32学习笔记——点亮LED 本人学习STM32是直接通过操作stm32的寄存器,使用的开发板是野火ISO-V2版本: 先简单的介绍一下stm32的GPIO: stm32的GPIO有多种模式: 1 ...

  5. stm32学习笔记----双串口同时打开时的printf()问题

    stm32学习笔记----双串口同时打开时的printf()问题 最近因为要使用串口2外接PN532芯片实现通信,另一方面,要使用串口1来将一些提示信息输出到上位机,于是重定义了printf(),使其 ...

  6. stm32学习笔记——外部中断的使用

    stm32学习笔记——外部中断的使用 基本概念 stm32中,每一个GPIO都可以触发一个外部中断,但是,GPIO的中断是以组为一个单位的,同组间的外部中断同一时间只能使用一个.比如说,PA0,PB0 ...

  7. STM32学习笔记(四)——串口控制LED(中断方式)

    目录: 一.时钟使能,包括GPIO的时钟和串口的时钟使能 二.设置引脚复用映射 三.GPIO的初始化配置,注意要设置为复用模式 四.串口参数初始化配置 五.中断分组和中断优先级配置 六.设置串口中断类 ...

  8. STM32学习笔记-NVIC中断知识点

    STM32学习笔记-NVIC中断知识点总结 中断优先级设置步骤 1. 系统运行后先设置中断优先级分组 函数:void NVIC_PriorityGroupConfig(uint32_tNVIC_Pri ...

  9. STM32学习笔记之一(初窥STM32)

    怎么做好学习笔记? 答:自我感知-->学习知识-->归纳总结-->自我升华(真正属于自己的知识是,抛开书本,运用时,你还能记得的思想) 自我感知--看到知识概念,先自我感觉那应该是个 ...

随机推荐

  1. iOS GIF图片转UIImage

    多平台保持统一风格的UI设计,少不了一些动态图片的使用 1.本地GIF图片使用 1.1 将本地GIF转为NSdata类型 NSData *tempdata = [NSData dataWithCont ...

  2. 用 Python 黄图批量鉴别审核

    前言 最近写了一款微信小程序需要用到图片审核,人工审核是不可能的人工审核的太费精力了,所以我就写了一个多线程批量识别脚本来处理,主要是调用百度AI的接口,这里我是付费了也不贵审核一条1分钱不到,再说我 ...

  3. Python logging 模块打印异常 exception

    logger.exception(sys.exc_info())

  4. Component Object Model (COM) 是什么?

    本文主要介绍 COM 的基础知识,倾向于理论性的理解,面向初学者,浅尝辄止. 1. COM 是什么: COM 的英文全称是,Component Object Model,中文译为,组件对象模型.它官方 ...

  5. java中Future的使用

    文章目录 创建Future 从Future获取结果 取消Future 多线程环境中运行 java中Future的使用 Future是java 1.5引入的一个interface,可以方便的用于异步结果 ...

  6. Linux中的常用符号

    >, 1>     输出重定向符stdout,代码为1,重定向内容到文件,清除已有的内容,然后加入新内容,如果文件不存在还会创建文件 >>, 1>>   追加输出重 ...

  7. 用三维的视角理解二维世界:完美解释meshgrid函数,三维曲面,等高线,看完你就懂了。...

    完美解释meshgrid函数,三维曲面,等高线 #用三维的视角理解二维世界 #完美解释meshgrid函数,三维曲面,等高线 import numpy as np import matplotlib. ...

  8. python基础1习题练习

    python基础1习题练习: #encoding:utf-8 #1.实现用户输入用户名和密码,当用户名为 seven 且 密码为 123 时,显示登陆成功,否则登陆失败! name=input('na ...

  9. CodeForces - 1047CEnlarge GCD(这题很难,快来看题解,超级详细,骗浏览量)

    C. Enlarge GCD time limit per test1 second memory limit per test256 megabytes inputstandard input ou ...

  10. PyCharm 集成 SVN,检出、提交代码

    1.安装 SVN,解决 SVN 目录中没有 svn.exe 问题 重新打开 TortoiseSVN 安装文件 选择 Modify 后在command line client tools 选项修改为 W ...