深入理解UART串口通信(基于STM32F103)


参考手册

USART框图分析

image-20241128192635013

STM32F101xx, STM32F102xx, STM32F103xx, STM32F105xx and STM32F107xx advanced Arm®-based 32-bit MCUs P789

发送/接收引脚

发送/接收引脚说明

RX: Receive Data Input is the serial data input. Oversampling techniques are used for data recovery by discriminating between valid incoming data and noise.

TX: Transmit Data Output. When the transmitter is disabled, the output pin returns to its IO port configuration. When the transmitter is enabled and nothing is to be transmitted, the TX pin is at high level. In single-wire and smartcard modes, this IO is used to transmit and receive the data (at USART level, data are then received on SW_RX)

  • RX:串行数据输入引脚。
  • TX:发送数据输出引脚。当发送器没有启用时,这个输出引脚充当通用的GPIO引脚;当启用发送器并且没有内容被发送是什时,该引脚保持高电平。在单线核智能卡模式下,该引脚被用来发送和接收数据(虽然物理上是用的一个引脚,但在USART内部,数据接收是通过SW_RX逻辑通道来完成的)。

GPIO复用

image-20241128195845755

[!NOTE]

值得注意的是:RX复用IO引脚时,IO口的读通道仍然是连通的;但是TX复用IO引脚时,IO口的写通道是断开的,这意味着该IO引脚完全由对应的复用外设(例如USART)来操控了。

那么我要怎么知道USART复用哪个引脚呢?这个在GPIO复用功能章节(9.3.8 USART alternate function remapping)能够找到相应的说明:

image-20241128200513629

默认使用PA9/10作为USART1的TX/RX,如果需要还可以通过AFIO重映射将其配置为PB6/7:

image-20241128200656325

image-20241128200735197

GPIO模式配置

现在USART使用引脚我们知道了,那么引脚该配置什么工作模式呢,在GPIO外设配置章节(9.1.11 GPIO configurations for device peripherals)也有说明:

image-20241128200958021

TX需要配置为复用推挽,RX需要配置为浮动输入或上拉输入

RTS/CTS引脚

The following pins are required in Hardware flow control mode:
 CTS: Clear To Send blocks the data transmission at the end of the current transfer
when high
 RTS: Request to send indicates that the USART is ready to receive a data (when low)

image-20241128201344123

这两个引脚在硬件流控模式中需要被用到(通信双方通过这两个引脚来互相协调数据的发送,避免一个发送太快,另一个接收不过来):

  • RTS(请求发送):箭头方向是从内向外的,和接收控制器相连。当前串口可以通过拉低该引脚来告知对方可以发送数据了(当前MCU准备好接收数据了)。
  • CTS(清除发送,允许发送的条件已经清除):箭头方向是由外向内的,和发送控制器相连。对方可以通过拉高该引脚来告知当前串口在完成当前传输后停止数据的发送(对方接收不过来了)

硬件流控制流程:

image-20241128204540542

下面以串口A要给串口B发送数据为例:

  • 串口A要给B发送数据时,首先会通过A的CTS读取B的RTS状态,如果B可以接收数据,那么B会拉低RTS(请求发送),该低电平状态在A需要发送数据时可以通过CTS读取到:
    • 读到CTS为高,那么说明B拉高了RTS(对方忙碌);如果读到CTS为低,那么说明B拉低了RTS(对方空闲)
  • A通过CTS读到低电平后,就开始向B发送数据了,如果某一时刻发现CTS被拉高了,说明B处理不过来了,因此会在当前帧传输完后停止后续数据的发送,当B重新将RTS置为低电平时继续传输剩余帧,重复这一个过程直到所有帧传输完成。

CK引脚

image-20241128204932608

该引脚在同步传输模式下用于时钟信号的传输。

image-20241128205127278

其中UART4和UART5是不支持同步模式的。

USART数据帧分析

image-20241128205612552

数据位字长

Word length may be selected as being either 8 or 9 bits by programming the M bit in the USART_CR1 register (see Figure 280).

字长可以选择8bit或9bit,可以通过USART_CR1寄存器中的M位来设置:

image-20241128205946525

起始位和停止位

The TX pin is in low state during the start bit. It is in high state during the stop bit

发送引脚在起始位保持低电平,在停止位保持高电平

空闲帧

An Idle character is interpreted as an entire frame of “1”s followed by the start bit of the next frame which contains data (The number of “1” ‘s will include the number of stop bits).

空闲帧是指比特位全为1的帧,帧的长度包括起始比特位、字长、停止位(对应比特位数量不固定)

波特率和时钟

Transmission and reception are driven by a common baud rate generator, the clock for each is generated when the enable bit is set respectively for the transmitter and receiver.

发送和接收是由一个通用的波特率发生器来驱动的,

发送器

低位优先&底层发送流程

[!IMPORTANT]

During a USART transmission, data shifts out least significant bit first on the TX pin.

In thismode, the USART_DR register consists of a buffer (TDR) between the internal bus and the transmit shift register (see Figure 279)

在一次USART传输中,数据通过TX引脚低位优先移位发送出去的。在这个场景下,USART_DR表现为一个位于内部总线和移位寄存器之间的发送缓冲区(TDR),写入USART_DR的数据会先被写入TDR,然后在硬件的支持下通过移位寄存器逐位发送出去。

image-20241128212821427

可配置的停止位宽度

image-20241128213716372

停止位默认占一个bit,可以通过USART_CR2寄存器配置为其他的比特位宽,以支持不同的串口通信模式(例如单线、调制解调器、智能卡等)

image-20241128213822036

image-20241128214412870

TXE标志(传输缓冲区为空)

The TXE bit is always cleared by a write to the data register.The TXE bit is set by hardware and it indicates:
 The data has been moved from TDR to the shift register and the data transmission has
started.
 The TDR register is empty.
 The next data can be written in the USART_DR register without overwriting the
previous data.

This flag generates an interrupt if the TXEIE bit is set.

USART_SR寄存器中TXE标志(Tranmist Buffer Empty)总是由一个写USART_DR 寄存器的操作来清除的。TXE由硬件置位,用于指示:

  • 数据已经从TDR传送到了移位寄存器并且该数据的发送已经开始
  • TDR寄存器为空
  • 可以将写一个数据写入USART_DR寄存器,并且先前写入的数据不会被覆盖(可能正在通过移位寄存器逐位发送,也可能已经发送完毕)

该标志被置位时会产生一个TXEIE中断(如果使能了该中断)。

When a transmission is taking place, a write instruction to the USART_DR register stores the data in the TDR register and which is copied in the shift register at the end of the current transmission

当正在进行一个数据的发送时,向USART_DR写入数据会将数据存储到TDR寄存器中,并在当前传输结束后将其拷贝到移位寄存器中。

When no transmission is taking place, a write instruction to the USART_DR register places the data directly in the shift register, the data transmission starts, and the TXE bit is immediately set.

如果当前没有发送数据,向USART_DR写入数据会直接将数据放到移位寄存器中,并且开始该数据的传输,同时TXE会被置位。

TC标志(硬件传输完成&传输缓冲区为空)

If a frame is transmitted (after the stop bit) and the TXE bit is set, the TC bit goes high. An interrupt is generated if the TCIE bit is set in the USART_CR1 register.

如果一个数据帧传输完成(包括停止位)并且TXE被置位,此时表明了两点:

  • 硬件没有正在通过移位寄存器传输数据
  • TDR缓冲区也没有带发送的数据

此时,TC标志会被置位,同时产生一个TCIE中断(如果使能了该中断)。这通常标志着一连串数据发送的完成。

image-20241128221900956

[!WARNING]

After writing the last data into the USART_DR register, it is mandatory to wait for TC=1 before disabling the USART or causing the microcontroller to enter the low-power mode

在写入最后一个数据到DR寄存器后,如果要禁用USART或使MCU进入低功耗模式之前,应该要等TC被置位,这样才能确保底层的硬件真的将所有递交到DR的数据发送出去了。

The TC bit is cleared by the following software sequence:

  1. A read from the USART_SR register
  2. A write to the USART_DR register

TC标志会被如下的软件操作序列清除:

  1. 读USART_SR寄存器
  2. 写USART_DR 寄存器

例如我们需要一次性发送多个数据帧,会使用如下轮询的方式:

  1. 等待USART_SR中的TXE被置位(需要轮询读取USART_SR中的TXE位并判断)
  2. TXE被置位后,向DR递交下一个待发送的数据

空闲帧

Setting the TE bit drives the USART to send an idle frame before the first data frame.

TE bitUSART_CR1(USART控制寄存器1)中的一个控制位,全称为 Transmitter Enable,其作用是启用 USART 的发送功能。具体来说,设置 TE bit 会使 USART 进入发送模式,并且在开始传输第一个数据帧之前,它会先发送一个空闲帧(idle frame)。

什么是空闲帧(Idle Frame)?

空闲帧指的是一种 没有有效数据 的帧,通常它是由逻辑 “1” 组成的。在 USART 中,这个空闲帧起到 信号稳定传输准备 的作用。空闲帧的存在确保了接收端能够正确同步到即将开始的有效数据传输。

具体工作原理

  1. 设置 TE 位:当你通过软件设置 TE bit(即 Transmitter Enable 位)时,USART 将被启用为 发送模式,并准备开始传输数据。
  2. 发送空闲帧:在开始传输数据之前,USART 会首先发送一个 空闲帧(一个全为“1”的逻辑帧),这个空闲帧可以被视为“开始”信号,告诉接收方接下来会有数据传输。
  3. 发送数据帧:一旦空闲帧发送完成,USART 就开始传输第一个有效的数据帧。

为什么要发送空闲帧?

  • 同步作用:在某些情况下,接收设备需要一定的时间来同步到发送设备的时钟。空闲帧为接收端提供了同步的信号,确保接收设备能够准备好接收即将传输的数据。
  • 信号稳定:在数据传输开始之前,空闲帧有助于确保信号线处于稳定状态,以便接收设备能够准确地捕获数据。
  • 时序准备:通过发送空闲帧,发送器给接收器一个明确的指示,表明接下来的数据是有效的,而不是噪声或无效数据。

接收器

The USART can receive data words of either 8 or 9 bits depending on the M bit in the USART_CR1 register.

USRAT可以接收8或9个bit字长的数据,这取决于USART_CR1中M位的配置。

起始位检测

image-20241129091530313

In the USART, the start bit is detected when a specific sequence of samples is recognized. This sequence is: 1 1 1 0 X 0 X 0 X 0 0 0 0.

If the sequence is not complete, the start bit detection aborts and the receiver returns to the
idle state (no flag is set) where it waits for a falling edge.

在USART中,如果识别到 1 1 1 0 X 0 X 0 X 0 0 0 0模式的特定采样序列(X表示不关心是0还是1),则表明检测到一个有效起始位。(1 1 1 0表明产生了一个下降沿,其中 0可能是一个数据帧的起始位)

如果发生下降沿时得到的采样序列和上述模式相比不完整,那么该起始位会被丢弃,并且接收器返回到空闲状态继续等待下降沿。

The start bit is confirmed (RXNE flag set, interrupt generated if RXNEIE=1) if the 3 sampled bits are at 0 (first sampling on the 3rd, 5th and 7th bits finds the 3 bits at 0 and second sampling on the 8th, 9th and 10th bits also finds the 3 bits at 0).

如图,USART通过过采样(波特率的16倍)在一个bit时间内进行了16次采样,如果将下降沿序列( 1 1 1 0)中的 0作为一个起始位中的第一个样本点,那么如果随后的第3、5、7样本点为0,且第8、9、10样本点为0,那么该起始位被确认为有效起始位,RXNE标志(接收缓冲区不为空,RDR有数据可读)会被置位,并且产生RXNEIE中断(如果使能了该中断)

[!IMPORTANT]

这里值得注意的是:实际上RXNE置位应该是在硬件完成数据的传输之后(数据通过硬件传入移位寄存器并递交到RDR寄存器)

The start bit is validated (RXNE flag set, interrupt generated if RXNEIE=1) but the NE noise flag is set if, for both samplings, at least 2 out of the 3 sampled bits are at 0 (sampling on the 3rd, 5th and 7th bits and sampling on the 8th, 9th and 10th bits). If this condition is not met, the start detection aborts and the receiver returns to the idle state (no flag is set).

image-20241129093604361

数据帧的接收

During a USART reception, data shifts in least significant bit first through the RX pin. In this mode, the USART_DR register consists of a buffer (RDR) between the internal bus and the
received shift register.

在一个USART数据帧接收过程中,数据以LSB低位优先模式传输到RX引脚。在这个场景下,USART_DR寄存器表现为位于内部总线和移位寄存器之间的接收缓冲区(RDR,Receive Data Register)

image-20241129094459395

Procedure:

  1. Enable the USART by writing the UE bit in USART_CR1 register to 1.
  2. Program the M bit in USART_CR1 to define the word length.
  3. Program the number of stop bits in USART_CR2.
  4. Select DMA enable (DMAR) in USART_CR3 if multibuffer communication is to take
    place. Configure the DMA register as explained in multibuffer communication. STEP 3
  5. Select the desired baud rate using the baud rate register USART_BRR
  6. Set the RE bit USART_CR1. This enables the receiver which begins searching for a
    start bit.

接收程序如下:

  1. 启用USART:设置USART_CR1中的UE为1
  2. 通过USART_CR1中的M设置字长
  3. 通过USART_CR2设置停止位占用的bit数量
  4. 多缓冲DMA相关
  5. 通过USART_BRR选择期望的波特率
  6. 通过USART_CR1的RE启用接收器

When a character is received
 The RXNE bit is set. It indicates that the content of the shift register is transferred to the
RDR. In other words, data has been received and can be read (as well as its
associated error flags).
 An interrupt is generated if the RXNEIE bit is set.
 The error flags can be set if a frame error, noise or an overrun error has been detected
during reception.
 In multibuffer, RXNE is set after every byte received and is cleared by the DMA read to
the Data register.
 In single buffer mode, clearing the RXNE bit is performed by a software read to the
USART_DR register. The RXNE flag can also be cleared by writing a zero to it. The
RXNE bit must be cleared before the end of the reception of the next character to avoid
an overrun error.

当收到了一个数据帧后:

  • RXNE会被置位:有数据被传输到了RDR中,可以通过DR来读取它
  • 产生RXNEIE中断(如果使能了该中断)
  • 如果接收过程中发生了帧错误、噪声错误、缓冲区溢出错误,状态寄存器USART_SR中的相关错误标志位会被设置
  • 在多缓冲DMA中,RXNE在每个字节接收完成后由DMA的读DR操作来清除
  • 单缓冲模式下,RXNE的清除可由软件的读DR操作来清除,也可以通过向RXNE写0来清除。RXNE必须要在下一个数据帧接收完成之前被清除,以避免产生缓冲区溢出错误

[!IMPORTANT]

这里值得注意的是:如何确保在下一个数据帧之前清除RXNE?

image-20241129100957026 image-20241129101031527

空闲帧

When an idle frame is detected, there is the same procedure as a data received character plus an interrupt if the IDLEIE bit is set.

当检测到空闲帧(注意这里是指跟在数据帧后的第一个空闲帧)时,硬件会执行与接收到一个数据帧类似的流程(参考上一节):

  1. 设置USART_SR 寄存器的 IDLE(空闲帧检测) 标志位(接收到数据帧则是设置RXNE标志位)。
  2. 如果 IDLEIE 位=1(空闲帧中断使能),会触发中断。

但是,空闲帧不会影响接收数据缓冲区的内容,因此不会产生 RXNE 标志,也不会修改接收数据寄存器(RDR)。

image-20241129193651750

It is cleared by a software sequence (an read to the USART_SR register followed by a read to the USART_DR register).

这里值得注意的是,IDLE标志位可以通过如下软件操作序列来清除:

  1. 读USART_SR寄存器(例如轮询IDLE标志位)
  2. 读USART_DR(例如轮询到IDLE被置后,执行一个USART_DR读到的空操作)

[!NOTE]

Note: The IDLE bit will not be set again until the RXNE bit has been set itself (i.e. a new idle
line occurs).

这里需要注意的是,在RXNE被置位后(接收到了数据)如果检测到了空闲帧才会将IDLE置位,并且如果有多个空闲帧不会多次将IDLE置位,即每次接收数据后的第一个空闲帧(指示一次不定长数据传输已经完成)才会置位IDLE

波特率设置

image-20241129105108831

image-20241129102353746

接收器和发送器的波特率由外设时钟fCK和可编程的USART分频参数USARTDIV来计算。

其中USARTDIV又可以通过波特率配置寄存器USART_BRR中的DIV_Mantissa(配置USARTDIV的整数部分)和DIV_Fraction (配置USARTDIV的小数部分,通过DIV_Fraction/16来计算对应的小数)

image-20241129102223806

校验控制

image-20241128210319460

[!NOTE]

校验位是包含在字长里面的,作为字长的最后一个bit

可以通过如下寄存器配置是否开启校验位,以及选择奇偶校验模式

image-20241128210525491

轮询方式收发单字节实践

原理图

image-20241129103540732

使能时钟

image-20241129111334989

image-20241129111749460

// enable clock
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;

GPIO配置

image-20241129104039053

// GPIO CONFIG
    // PA9 TX  alternate pp output
    GPIOA->CRH |= GPIO_CRH_MODE9;   // output
    GPIOA->CRH |= GPIO_CRH_CNF9_1;   // alternate pp
    GPIOA->CRH &= ~GPIO_CRH_CNF9_0;
    // PA10 RX alternate float input
    GPIOA->CRH &= ~GPIO_CRH_MODE10;   // input mode
    GPIOA->CRH &= ~GPIO_CRH_CNF10_1;
    GPIOA->CRH |= GPIO_CRH_CNF10_0;   // float input

AFIO配置

image-20241129104211418

image-20241129104116889

image-20241129114655724

// AFIO CONFIG
    AFIO->MAPR &= ~AFIO_MAPR_USART1_REMAP;

USART配置

波特率配置

image-20241129104313401

如果我们想得到115200的波特率,以72M外设时钟为例计算USARTDIV:72000000/(16 * 115200) = 39.0625

因此整数部分可以配置为 0x27(39),小数部分可以配置为 0x1(1/16=0.625)

// baud rate
 USART1->BRR &= ~USART_BRR_DIV_Mantissa;
 USART1->BRR &= ~USART_BRR_DIV_Fraction;
 USART1->BRR |= (0x27 << 4 | 0x01);

进一步可以优化为:

USART1->BRR = 0x271;

USART控制器配置

image-20241129105601999

image-20241129110700834

数据帧相关

image-20241129110611145

// data frame
   USART1->CR1 &= ~USART_CR1_M;   // 8 bit word length
   USART1->CR1 &= ~USART_CR1_PCE; // diable parity
   USART1->CR2 &= ~USART_CR2_STOP; // 1 stop bit

使能相关

image-20241129110403540

// enable
USART1->CR1 |= (USART_CR1_UE | USART_CR1_RE | USART_CR1_TE);

轮询收发

void uart_send_byte(uint8_t byte) {
    // waits until transmit buffer is empty
    while ((USART1->SR & USART_SR_TXE) == 0) {}
    USART1->DR = byte;
}

uint8_t uart_receive_byte(void) {
    // waits until receive buffer is not empty
    while ((USART1->SR & USART_SR_RXNE) == 0) {}
    return USART1->DR;
}

完整代码

#ifndef __UART_H__
#define __UART_H__

#include "stm32f10x.h"

void uart_init(void);

void uart_send_byte(uint8_t byte);

uint8_t uart_receive_byte(void);

#endif /* __UART_H__ */
#include "uart.h"

void uart_init(void) {
    // enable clock
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;

    // GPIO CONFIG
    // PA9 TX  alternate pp output
    GPIOA->CRH |= GPIO_CRH_MODE9;   // output
    GPIOA->CRH |= GPIO_CRH_CNF9_1;   // alternate pp
    GPIOA->CRH &= ~GPIO_CRH_CNF9_0;
    // PA10 RX alternate float input
    GPIOA->CRH &= ~GPIO_CRH_MODE10;   // input mode
    GPIOA->CRH &= ~GPIO_CRH_CNF10_1;
    GPIOA->CRH |= GPIO_CRH_CNF10_0;   // float input

    // AFIO CONFIG
    AFIO->MAPR &= ~AFIO_MAPR_USART1_REMAP;

    // USART CONFIG
    // baud rate
    USART1->BRR &= ~USART_BRR_DIV_Mantissa;
    USART1->BRR &= ~USART_BRR_DIV_Fraction;
    USART1->BRR |= (0x27 << 4 | 0x01);
    //USART1->BRR = 0x271;
    // data frame
    USART1->CR1 &= ~USART_CR1_M;   // 8 bit word length
    USART1->CR1 &= ~USART_CR1_PCE; // diable parity
    USART1->CR2 &= ~USART_CR2_STOP; // 1 stop bit
    // enable
    USART1->CR1 |= (USART_CR1_UE | USART_CR1_RE | USART_CR1_TE);
}

void uart_send_byte(uint8_t byte) {
    // waits until transmit buffer is empty
    while ((USART1->SR & USART_SR_TXE) == 0) {}
    USART1->DR = byte;
}

uint8_t uart_receive_byte(void) {
    // waits until receive buffer is not empty
    while ((USART1->SR & USART_SR_RXNE) == 0) {}
    return USART1->DR;
}
int main() {
    uart_init();
    uart_send_byte('a');
    while (1) {}
}

轮询方式收发多字节实践

void uart_send_bytes(uint8_t* bytes, u16 size) {
    for (u16 i = 0; i < size; i++) {
        uart_send_byte(bytes[i]);
    }
}

void uart_receive_bytes(uint8_t* bytes_buf, uint16_t* sizebuf) {
    uint16_t i = 0;
    while (1) {
        while ((USART1->SR & USART_SR_RXNE) == 0) {
            if (USART1->SR & USART_SR_IDLE) {
                USART1->DR;
                *sizebuf = i;
                return;
            }
        }
        bytes_buf[i++] = USART1->DR;
    }
}

这里既要通过轮询来检测RXNE从而实现多字节的读取,又要轮询IDLE来实现在检测到空闲帧后(发送方发送完了最后一个字节)终止轮询读取的逻辑。

[!NOTE]

这里在检测到IDLE标志位(通过读USART1->SR)后,追加了一个USART1->DR读操作,这是为了遵循手册对于IDLE标志位描述中的规范:

It is cleared by a software sequence (an read to theUSART_SR register followed by a read to the USART_DR register).

值得注意的是,轮询RXNE和IDLE应该配合使用,如下代码试图通过在每次从DR读取到一个字节后检测一次IDLE,如果IDLE刚好在这个 if (USART1->SR & USART_SR_IDLE) 执行后被置位,那么程序就会卡在第4行

void uart_receive_bytes(uint8_t* bytes_buf, uint16_t* sizebuf) {
    uint16_t i = 0;
    while (1) {
        while ((USART1->SR & USART_SR_RXNE) == 0) {}
        bytes_buf[i++] = USART1->DR;
        if (USART1->SR & USART_SR_IDLE) { 
            USART1->DR;
            *sizebuf = i;
            return;
        }
    }
}

[!WARNING]

可以发现,对于硬件置位的标志位(例如RXNE、IDLE),应该不断轮询去检测,而不要期望在某个时机检测仅仅一次。

例如上述程序期望在收到最后一个字节后通过一次 if判断来检测IDLE,虽然对于接收最后一个字节而言,IDLE置位确实发生在 bytes_buf[i++] = USART1->DR之后,但是程序的执行速度是很快的,而空闲帧的检测需要硬件检测到一个帧长的所有bit全为1时才会将IDLE置1。因此上述程序中的 if (USART1->SR & USART_SR_IDLE) 并不能在接收完最后一个字节后如期检测到IDLE被置位,接着又回到了 while ((USART1->SR & USART_SR_RXNE) == 0)空转无法跳出。

因此如下代码将RXNE和IDLE的轮询结合到了一起,在RXNE被置位时执行数据的读取 bytes_buf[i++] = USART1->DR,在IDLE被置位时执行函数的返回

void uart_receive_bytes(uint8_t* bytes_buf, uint16_t* sizebuf) {
    uint16_t i = 0;
    while (1) {
        while ((USART1->SR & USART_SR_RXNE) == 0) {
            if (USART1->SR & USART_SR_IDLE) {
                USART1->DR;
                *sizebuf = i;
                return;
            }
        }
        bytes_buf[i++] = USART1->DR;
    }
}

优化

uint8_t uart_receive_byte(void) {
    // waits until receive buffer is not empty
    while ((USART1->SR & USART_SR_RXNE) == 0) {
        if (USART1->SR & USART_SR_IDLE) {   // 如果检测到了空闲帧则停止接收数据
            return 0;
        }
    }
    return USART1->DR;
}

void uart_receive_bytes(uint8_t* bytes_buf, uint16_t* sizebuf) {
    /*     uint16_t i = 0;
    while (1) {
        while ((USART1->SR & USART_SR_RXNE) == 0) {
            if (USART1->SR & USART_SR_IDLE) {
                USART1->DR;
                *sizebuf = i;
                return;
            }
        }
        bytes_buf[i++] = USART1->DR;
    } */
    uint16_t i = 0;
    while ((USART1->SR & USART_SR_IDLE) == 0) { // 如果没有检测到空闲帧,则轮询接收数据
        bytes_buf[i++] = uart_receive_byte();
    }
    USART1->DR; // 通过USART_SR读操作之后跟随一个USART_DR读来清除IDLE标志
    *sizebuf = --i; // uart_receive_byte返回的最后一个字节是因为空闲帧返回的无效数据
}

再看TXE/TC、RXNE、IDEL清除逻辑

image-20241129220628220

image-20241129220926870

image-20241129221419149

[!NOTE]

这里需要注意的是,在RXNE被置位后(接收到了数据)如果检测到了空闲帧才会将IDLE置位,并且如果有多个空闲帧不会多次将IDLE置位,即每次接收数据后的第一个空闲帧(指示一次不定长数据传输已经完成)才会置位IDLE

完整代码

#include "uart.h"

void uart_init(void) {
    // enable clock
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;

    // GPIO CONFIG
    // PA9 TX  alternate pp output
    GPIOA->CRH |= GPIO_CRH_MODE9;   // output
    GPIOA->CRH |= GPIO_CRH_CNF9_1;   // alternate pp
    GPIOA->CRH &= ~GPIO_CRH_CNF9_0;
    // PA10 RX alternate float input
    GPIOA->CRH &= ~GPIO_CRH_MODE10;   // input mode
    GPIOA->CRH &= ~GPIO_CRH_CNF10_1;
    GPIOA->CRH |= GPIO_CRH_CNF10_0;   // float input

    // AFIO CONFIG
    AFIO->MAPR &= ~AFIO_MAPR_USART1_REMAP;

    // USART CONFIG
    // baud rate
    USART1->BRR &= ~USART_BRR_DIV_Mantissa;
    USART1->BRR &= ~USART_BRR_DIV_Fraction;
    USART1->BRR |= (0x27 << 4 | 0x01);
    //USART1->BRR = 0x271;
    // data frame
    USART1->CR1 &= ~USART_CR1_M;   // 8 bit word length
    USART1->CR1 &= ~USART_CR1_PCE;   // diable parity
    USART1->CR2 &= ~USART_CR2_STOP;   // 1 stop bit
    // enable
    USART1->CR1 |= (USART_CR1_UE | USART_CR1_RE | USART_CR1_TE);
}

void uart_send_byte(uint8_t byte) {
    // waits until transmit buffer is empty
    while ((USART1->SR & USART_SR_TXE) == 0) {}
    USART1->DR = byte;
}

uint8_t uart_receive_byte(void) {
    // waits until receive buffer is not empty
    while ((USART1->SR & USART_SR_RXNE) == 0) {
        if (USART1->SR & USART_SR_IDLE) {   // 如果检测到了空闲帧则停止接收数据
            return 0;
        }
    }
    return USART1->DR;
}

void uart_send_bytes(uint8_t* bytes, u16 size) {
    for (u16 i = 0; i < size; i++) {
        uart_send_byte(bytes[i]);
    }
}

void uart_receive_bytes(uint8_t* bytes_buf, uint16_t* sizebuf) {
    /*     uint16_t i = 0;
    while (1) {
        while ((USART1->SR & USART_SR_RXNE) == 0) {
            if (USART1->SR & USART_SR_IDLE) {
                USART1->DR;
                *sizebuf = i;
                return;
            }
        }
        bytes_buf[i++] = USART1->DR;
    } */
    uint16_t i = 0;
    while ((USART1->SR & USART_SR_IDLE) == 0) { // 如果没有检测到空闲帧,则轮询接收数据
        bytes_buf[i++] = uart_receive_byte();
    }
    USART1->DR; // 通过USART_SR读操作之后跟随一个USART_DR读来清除IDLE标志
    *sizebuf = --i; // uart_receive_byte返回的最后一个字节是因为空闲帧返回的无效数据
}

中断方式接收单字节和变长数据

image-20241129223248999

image-20241129223318399

使能中断

image-20241129223405148

// interrupt config
   USART1->CR1 |= USART_CR1_RXNEIE;
   USART1->CR1 |= USART_CR1_IDLEIE;
   NVIC_SetPriorityGrouping(3);   // only group priority
   NVIC_SetPriority(USART1_IRQn, 1);
   NVIC_EnableIRQ(USART1_IRQn);

定义中断处理函数

uint8_t buffer[100] = {0};
uint8_t size = 0;

__weak void uart1_received_callback(uint8_t buf[], uint8_t size) {
    
}

void USART1_IRQHandler(void) {
    if(USART1->SR & USART_SR_RXNE) {
        buffer[size++] = USART1->DR;
    }
    if(USART1->SR & USART_SR_IDLE) {
        USART1->DR; // dummy read for clearing IDLE flag
        uart1_received_callback(buffer, size);
        size = 0;
    }
}

使用方重写回调函数

void uart1_received_callback(uint8_t buf[], uint8_t size) {
    uart_send_bytes(buf, size);
}

int main() {
    uart_init();
    //uart_send_byte('a');
    uint8_t* str = "hello world!\n";
    uart_send_bytes(str, strlen((char*)str));
    while (1) {}
}

完整代码

uart.h

#ifndef __UART_H__
#define __UART_H__

#include "stm32f10x.h"

void uart_init(void);

void uart_send_byte(uint8_t byte);

void uart_send_bytes(uint8_t *bytes, u16 size);

void uart1_received_callback(uint8_t *buf, uint8_t size);
#endif /* __UART_H__ */

uart.c

#include "uart.h"

void uart_init(void) {
    // enable clock
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;

    // GPIO CONFIG
    // PA9 TX  alternate pp output
    GPIOA->CRH |= GPIO_CRH_MODE9;   // output
    GPIOA->CRH |= GPIO_CRH_CNF9_1;   // alternate pp
    GPIOA->CRH &= ~GPIO_CRH_CNF9_0;
    // PA10 RX alternate float input
    GPIOA->CRH &= ~GPIO_CRH_MODE10;   // input mode
    GPIOA->CRH &= ~GPIO_CRH_CNF10_1;
    GPIOA->CRH |= GPIO_CRH_CNF10_0;   // float input

    // AFIO CONFIG
    AFIO->MAPR &= ~AFIO_MAPR_USART1_REMAP;

    // USART CONFIG
    // baud rate
    USART1->BRR &= ~USART_BRR_DIV_Mantissa;
    USART1->BRR &= ~USART_BRR_DIV_Fraction;
    USART1->BRR |= (0x27 << 4 | 0x01);
    //USART1->BRR = 0x271;
    // data frame
    USART1->CR1 &= ~USART_CR1_M;   // 8 bit word length
    USART1->CR1 &= ~USART_CR1_PCE;   // diable parity
    USART1->CR2 &= ~USART_CR2_STOP;   // 1 stop bit
    // enable
    USART1->CR1 |= (USART_CR1_UE | USART_CR1_RE | USART_CR1_TE);

    // interrupt config
    USART1->CR1 |= USART_CR1_RXNEIE;
    USART1->CR1 |= USART_CR1_IDLEIE;
    NVIC_SetPriorityGrouping(3);   // only group priority
    NVIC_SetPriority(USART1_IRQn, 1);
    NVIC_EnableIRQ(USART1_IRQn);
}

uint8_t buffer[100] = {0};
uint8_t size = 0;
void USART1_IRQHandler(void) {
    if(USART1->SR & USART_SR_RXNE) {
        buffer[size++] = USART1->DR;
    }
    if(USART1->SR & USART_SR_IDLE) {
        USART1->DR; // dummy read for clearing IDLE flag
        uart1_received_callback(buffer, size);
        size = 0;
    }
}

__weak void uart1_received_callback(uint8_t *buf, uint8_t size) {
    
}

void uart_send_byte(uint8_t byte) {
    // waits until transmit buffer is empty
    while ((USART1->SR & USART_SR_TXE) == 0) {}
    USART1->DR = byte;
}

void uart_send_bytes(uint8_t* bytes, u16 size) {
    for (u16 i = 0; i < size; i++) {
        uart_send_byte(bytes[i]);
    }
}

main.c

void uart1_received_callback(uint8_t buf[], uint8_t size) {
    uart_send_bytes(buf, size);
}

int main() {
    uart_init();
    //uart_send_byte('a');
    uint8_t* str = "hello world!\n";
    uart_send_bytes(str, strlen((char*)str));
    while (1) {}
}

STM32CubeMX/HAL库实践

轮询方式收发定长数据

Cube配置启用USART,相关的GPIO会被自动配置

image-20241130085637544

AFIO重映射问题

需要注意的是,如果希望PB6/7作为TX/RX,可以直接点击图中的可视化引脚视图中的PB6选择USART1_TX,那么PA9/10就会被切换为PB6/7,并且自动在生成的代码中做好AFIO的remap配置

image-20241130085714543

image-20241130090020406

image-20241130090037660

同样的,再次点击PA9引脚选择USART1_TX则可以切换回PA9/10

轮询收发定长函数调用

stm32f1xx_hal_uart.c

*** Polling mode IO operation ***
=================================
[..]
  (+) Send an amount of data in blocking mode using HAL_UART_Transmit()
  (+) Receive an amount of data in blocking mode using HAL_UART_Receive()
  • HAL_UART_Transmit:轮询发送指定数量数据,需要指定超时时间(ms)
  • HAL_UART_Receive:轮询接收指定数量的数据,需要指定超时时间(ms)
  • HAL_StatusTypeDef:上述两个函数的返回值,如果成功则返回 HAL_OK,如果超时则返回 HAL_TIMEOUT

如下示例实现了不断接收8个数据并原样发送8个数据

int main(void) {
    ...

    /* Initialize all configured peripherals */
    MX_GPIO_Init();
    MX_USART1_UART_Init();
    /* USER CODE BEGIN 2 */
    uint8_t* str = "hello world\n";
    HAL_UART_Transmit(&huart1, str, strlen((char*)str), 10);

    uint8_t buffer[100] = { 0 };
    /* USER CODE END 2 */

    /* Infinite loop */
    /* USER CODE BEGIN WHILE */
    while (1) {
        if (HAL_UART_Receive(&huart1, buffer, 8, 10) == HAL_OK) {
            HAL_UART_Transmit(&huart1, buffer, 8, 10);
            /* code */
        }
        /* USER CODE END WHILE */
        
        if (HAL_UART_Receive(&huart1, buffer, 8, 10) == HAL_OK) {
            HAL_UART_Transmit(&huart1, buffer, 8, 10);
            /* code */
        }

        /* USER CODE BEGIN 3 */
    }
    /* USER CODE END 3 */
}

轮询方式接收变长数据

stm32f1xx_hal_uart.c

(+) Blocking mode: The reception is performed in polling mode, until either expected number of data is received,
   or till IDLE event occurs. Reception is handled only during function execution.
   When function exits, no data reception could occur. HAL status and number of actually received data elements,
   are returned by function after finishing transfer.
   
(#) Blocking mode API:
    (+) HAL_UARTEx_ReceiveToIdle()

HAL_UARTEx_ReceiveToIdle:轮询接收数据直到检测到空闲帧

  • uint8_t *pData:指定缓冲区的地址
  • uint16_t Size:指定需要接收多少数量数据到缓冲区,通常可以设置为缓冲区的大小
  • uint16_t *RxLen:检测到空闲帧时实际接收到的数据数量,调用方需要准备一个 uint16_t变量并将其地址通过该参数传入,函数会根据实际情况来设置该变量的值
uint8_t buffer[100] = { 0 };
uint16_t received_size = 0;
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1) {
    if (HAL_UARTEx_ReceiveToIdle(&huart1, buffer, 100, &received_size, 10) == HAL_OK) {
        HAL_UART_Transmit(&huart1, buffer, received_size, 10);
        /* code */
    }
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */

中断方式接收定长数据

stm32f1xx_hal_uart.c

*** Interrupt mode IO operation ***
    ===================================
    [..]
      (+) Send an amount of data in non blocking mode using HAL_UART_Transmit_IT()
      (+) At transmission end of transfer HAL_UART_TxCpltCallback is executed and user can
           add his own code by customization of function pointer HAL_UART_TxCpltCallback
      (+) Receive an amount of data in non blocking mode using HAL_UART_Receive_IT()
      (+) At reception end of transfer HAL_UART_RxCpltCallback is executed and user can
           add his own code by customization of function pointer HAL_UART_RxCpltCallback
      (+) In case of transfer Error, HAL_UART_ErrorCallback() function is executed and user can
           add his own code by customization of function pointer HAL_UART_ErrorCallback
  • HAL_UART_Receive_IT:以中断方式接收指定数量的数据到接收缓冲区中(由用户定义),调用该函数不会阻塞当前程序

  • HAL_UART_RxCpltCallback:接收完成回调,在接收了指定数量(底层通过RXNE中断逐个接收)数据并放到接收缓冲区中(由用户定义)后,该函数会被调用,我们可以通过重写改函数来处理接收到的数据

[!TIP]

stm32f1xx_hal_uart.c提供了 HAL_UART_RxCpltCallback函数的弱定义形式(相当于提供了一个接口),用户可以重写该函数(去掉 __weak关键字)以实现自定义逻辑。

该设计和钩子函数是一脉相承的。

/**
  * @brief  Rx Transfer completed callbacks.
  * @param  huart  Pointer to a UART_HandleTypeDef structure that contains
  *                the configuration information for the specified UART module.
  * @retval None
  */
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  /* Prevent unused argument(s) compilation warning */
  UNUSED(huart);
  /* NOTE: This function should not be modified, when the callback is needed,
           the HAL_UART_RxCpltCallback could be implemented in the user file
   */
}

示例代码:

/* USER CODE BEGIN 0 */
uint8_t buffer[100] = {0};
uint16_t fixed_data_size = 5;
uint8_t is_over = 0;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart == &huart1)
  {
    is_over = 1;
  }
}
/* USER CODE END 0 */

/**
 * @brief  The application entry point.
 * @retval int
 */
int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    HAL_UART_Receive_IT(&huart1, buffer, fixed_data_size);
    if (is_over)
    {
      HAL_UART_Transmit(&huart1, buffer, fixed_data_size, 10);
      is_over = 0;
    }

    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

[!NOTE]

由于是接收定长数据,如果发送方发送的数据不等于既定的fixed_data_size,那么该程序的echo回传逻辑会有问题

中断方式接收变长数据

stm32f1xx_hal_uart.c

(+) Non-Blocking mode: The reception is performed using Interrupts or DMA.
       These API's return the HAL status.
       The end of the data processing will be indicated through the
       dedicated UART IRQ when using Interrupt mode or the DMA IRQ when using DMA mode.
       The HAL_UARTEx_RxEventCallback() user callback will be executed during Receive process
       The HAL_UART_ErrorCallback()user callback will be executed when a reception error is detected.

(#) Non-Blocking mode API with Interrupt:
    (+) HAL_UARTEx_ReceiveToIdle_IT()

HAL_UARTEx_ReceiveToIdle_IT:以中断方式接收变长数据,接收数据到缓冲区(由用户定义)直到检测到空闲帧,,该函数不会阻塞当前程序

  • uint8_t *pData:接收缓冲区
  • uint16_t Size:需要接收的字结束,通常可以给定为缓冲区的大小

HAL_UARTEx_RxEventCallback:接收变长数据完成回调,可以通过入参 Size获取实际接收到的数据数量

/**
  * @brief  Reception Event Callback (Rx event notification called after use of advanced reception service).
  * @param  huart UART handle
  * @param  Size  Number of data available in application reception buffer (indicates a position in
  *               reception buffer until which, data are available)
  * @retval None
  */
__weak void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
  /* Prevent unused argument(s) compilation warning */
  UNUSED(huart);
  UNUSED(Size);

  /* NOTE : This function should not be modified, when the callback is needed,
            the HAL_UARTEx_RxEventCallback can be implemented in the user file.
   */
}

示例代码:

/* USER CODE BEGIN 0 */
uint8_t buffer[100] = {0};
uint8_t is_over = 0;
uint8_t received_size = 0;
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
  if (huart == &huart1)
  {
    is_over = 1;
    received_size = Size;
  }
}
/* USER CODE END 0 */

/**
 * @brief  The application entry point.
 * @retval int
 */
int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    HAL_UARTEx_ReceiveToIdle_IT(&huart1, buffer, 100);
    if (is_over)
    {
      HAL_UART_Transmit(&huart1, buffer, received_size, 10);
      is_over = 0;
    }

    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

Cube生成代码自动配置分析

main.c

/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();

MX_GPIO_Init开启了GPIO时钟

void MX_GPIO_Init(void)
{

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();

}

MX_USART1_UART_Init将UART相关参数保存到了 huart1结构体中

UART_HandleTypeDef huart1;
void MX_USART1_UART_Init(void)
{

  /* USER CODE BEGIN USART1_Init 0 */

  /* USER CODE END USART1_Init 0 */

  /* USER CODE BEGIN USART1_Init 1 */

  /* USER CODE END USART1_Init 1 */
  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();
  }
  /* USER CODE BEGIN USART1_Init 2 */

  /* USER CODE END USART1_Init 2 */

}

HAL_UART_Init主要关注一下两个函数的调用

HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart)
{
    HAL_UART_MspInit(huart);
    UART_SetConfig(huart);
}

usart.c重写了 HAL_UART_MspInit函数

  • 对USART1依赖的GPIO进行配置
  • 使能USART1中断
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{

  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(uartHandle->Instance==USART1)
  {
  /* USER CODE BEGIN USART1_MspInit 0 */

  /* USER CODE END USART1_MspInit 0 */
    /* USART1 clock enable */
    __HAL_RCC_USART1_CLK_ENABLE();

    __HAL_RCC_GPIOA_CLK_ENABLE();
    /**USART1 GPIO Configuration
    PA9     ------> USART1_TX
    PA10     ------> USART1_RX
    */
    GPIO_InitStruct.Pin = GPIO_PIN_9;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = GPIO_PIN_10;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    /* USART1 interrupt Init */
    HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(USART1_IRQn);
  /* USER CODE BEGIN USART1_MspInit 1 */

  /* USER CODE END USART1_MspInit 1 */
  }
}

printf重定向

重写fputc

printfD:\Keil_v5\ARM\ARMCC\include\stdio.h中定义的函数,底层会循环调用 fputc将要发送的字符串逐个字符地写入指定的文件流:

extern _ARMABI int fputc(int /*c*/, FILE * /*stream*/) __attribute__((__nonnull__(2)));
   /*
    * writes the character specified by c (converted to an unsigned char) to
    * the output stream pointed to by stream, at the position indicated by the
    * asociated file position indicator (if defined), and advances the
    * indicator appropriately. If the file position indicator is not defined,
    * the character is appended to the output stream.
    * Returns: the character written. If a write error occurs, the error
    *          indicator is set and fputc returns EOF.
    */

我们可以重写该函数以实现将字符通过UART发送的逻辑,这样串口调式工具就能显示 printf输出的调试信息了。

#include "stdio.h"
// printf retarget
int fputc(int ch, FILE * stream) {
    uart_send_byte((uint8_t)ch);
    return ch;
}

链接printf

需要注意的是,嵌入式要有平台的概念,基于Linux/Windows的C程序中的 printf需要链接GCC/MinGW中的glibc库;而在MDK嵌入式开发场景,下我们可以链接MDK提供的 Micro LIB,这样在链接阶段才能找到 printf所在的目标文件。

image-20241130121901654

完整代码

uart.c

#include "uart.h"
#include "stdio.h"

void uart_init(void) {
    // enable clock
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;

    // GPIO CONFIG
    // PA9 TX  alternate pp output
    GPIOA->CRH |= GPIO_CRH_MODE9;   // output
    GPIOA->CRH |= GPIO_CRH_CNF9_1;   // alternate pp
    GPIOA->CRH &= ~GPIO_CRH_CNF9_0;
    // PA10 RX alternate float input
    GPIOA->CRH &= ~GPIO_CRH_MODE10;   // input mode
    GPIOA->CRH &= ~GPIO_CRH_CNF10_1;
    GPIOA->CRH |= GPIO_CRH_CNF10_0;   // float input

    // AFIO CONFIG
    AFIO->MAPR &= ~AFIO_MAPR_USART1_REMAP;

    // USART CONFIG
    // baud rate
    USART1->BRR &= ~USART_BRR_DIV_Mantissa;
    USART1->BRR &= ~USART_BRR_DIV_Fraction;
    USART1->BRR |= (0x27 << 4 | 0x01);
    //USART1->BRR = 0x271;
    // data frame
    USART1->CR1 &= ~USART_CR1_M;   // 8 bit word length
    USART1->CR1 &= ~USART_CR1_PCE;   // diable parity
    USART1->CR2 &= ~USART_CR2_STOP;   // 1 stop bit
    // enable
    USART1->CR1 |= (USART_CR1_UE | USART_CR1_RE | USART_CR1_TE);

    // interrupt config
    USART1->CR1 |= USART_CR1_RXNEIE;
    USART1->CR1 |= USART_CR1_IDLEIE;
    NVIC_SetPriorityGrouping(3);   // only group priority
    NVIC_SetPriority(USART1_IRQn, 1);
    NVIC_EnableIRQ(USART1_IRQn);
}

uint8_t buffer[100] = {0};
uint8_t size = 0;
void USART1_IRQHandler(void) {
    if(USART1->SR & USART_SR_RXNE) {
        buffer[size++] = USART1->DR;
    }
    if(USART1->SR & USART_SR_IDLE) {
        USART1->DR; // dummy read for clearing IDLE flag
        uart1_received_callback(buffer, size);
        size = 0;
    }
}

__weak void uart1_received_callback(uint8_t *buf, uint8_t size) {
    
}

void uart_send_byte(uint8_t byte) {
    // waits until transmit buffer is empty
    while ((USART1->SR & USART_SR_TXE) == 0) {}
    USART1->DR = byte;
}

void uart_send_bytes(uint8_t* bytes, u16 size) {
    for (u16 i = 0; i < size; i++) {
        uart_send_byte(bytes[i]);
    }
}

// printf retarget
int fputc(int ch, FILE * stream) {
    uart_send_byte((uint8_t)ch);
    return ch;
}

main.c

int main() {
    uart_init();
    int num = 100;
    printf("hello world\n");
    printf("num = %d\n", num);
    LOG_DEBUG("this is a debug info. num = %d", num)
    while (1) {}
}

日志打印工具(格式化/高亮/行号)

logger.c

#include "logger.h"

void log_dump(const uint8_t *data, uint16_t len) {
    uint8_t ch, cl;
    for (int i = 0; i < len; i++) {
        ch = data[i] >> 4;
        cl = data[i] & 0x0F;
        if (ch < 10) ch += '0';
        else
            ch += 'A' - 10;
        if (cl < 10) cl += '0';
        else
            cl += 'A' - 10;
        putchar(ch);
        putchar(cl);
        printf(" ");
        if ((i + 1) % 16 == 0) {
            printf("\r\n");
        } else if ((i + 1) % 8 == 0) {
            ch = ' ';
            putchar(ch);
            putchar(ch);
            putchar(ch);
            putchar(ch);
        } else {
            ch = ' ';
            putchar(ch);
        }
    }
//    printf("\r\n");
}

logger.h

#ifndef LOGGER_H
#define LOGGER_H

#ifdef __cplusplus
extern "C"
{
#endif

#include "stm32f10x.h"
#include "stdio.h"
#include <string.h>

#ifndef USE_LOG
#define USE_LOG 1
#endif

#ifndef USE_DUMP
#define USE_DUMP 1
#endif

#ifndef USE_BLOCK
#define USE_BLOCK 1
#endif

#define ONLY_FILENAME(x) (strrchr(x, '\\') ? strrchr(x, '\\') + 1 : x)

#if USE_BLOCK > 0
#define BLOCK(s, ...)                              \
	do                                             \
	{                                              \
		printf("\r\n[BLOCK %s:%d] ",               \
			   ONLY_FILENAME(__FILE__), __LINE__); \
		printf(s, ##__VA_ARGS__);                  \
		printf(",按任意键继续");                   \
		block_flag = 0;                            \
		while (block_flag == 0)                    \
			;                                      \
		printf("\r\n");                            \
	} while (0)

#else
#define BLOCK(x)
#endif

#if USE_LOG > 0
#define LOG_DEBUG(s, ...)                          \
	do                                             \
	{                                              \
		printf("\033[1;36m\r\n[DEBUG %s:%d] ",     \
			   ONLY_FILENAME(__FILE__), __LINE__); \
		printf(s, ##__VA_ARGS__);                  \
		printf("\33[0m\r\n");                      \
	} while (0);
#define LOG_INFO(s, ...)                 \
	do                                   \
	{                                    \
		printf("\033[0;32m\r\n[INFO] "); \
		printf(s, ##__VA_ARGS__);        \
		printf("\33[0m\r\n");            \
	} while (0);
#define LOG_ERROR(s, ...)                          \
	do                                             \
	{                                              \
		printf("\033[0;31m\r\n[ERROR %s:%d] ",     \
			   ONLY_FILENAME(__FILE__), __LINE__); \
		printf(s, ##__VA_ARGS__);                  \
		printf("\33[0m\r\n");                      \
	} while (0);
#define LOG_ASSERT(cond)                                                                                                         \
	do                                                                                                                           \
	{                                                                                                                            \
		if (!(cond))                                                                                                             \
		{                                                                                                                        \
			printf("\r\n[ASSERT] File=[%s],Line=[%ld] Failed to vertify thc condition [\"%s\"]\r\n", __FILE__, __LINE__, #cond); \
		}                                                                                                                        \
	} while (0);
#else
#define LOG_DEBUG(s, ...)
#define LOG_INFO(s, ...)
#define LOG_ERROR(s, ...)
#define LOG_ERROR2()
#define LOG_ERROR3(cond)
#define LOG_ERROR4(s, ...)
#define LOG_ASSERT(cond)
#endif

#if USE_DUMP > 0
#define LOG_DUMP(info, data, len, ...)   \
	do                                   \
	{                                    \
		printf("\033[1;36m\r\n[DUMP] "); \
		printf(info, ##__VA_ARGS__);     \
		printf("\r\n\r\n");              \
		log_dump(data, len);             \
		printf("\33[0m\r\n");            \
	} while (0);

	void log_dump(const uint8_t *data, uint16_t len);
#else
#define LOG_DUMP(info, data, len)
#endif

#ifdef __cplusplus
}
#endif

#endif /* LOGGER_H */

THE END


文章作者: 安文
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 安文 !
  目录