【SWM320】学习使用UART

上一次学习了使用时钟和定时器,较为简单

本次学习UART,比之前的要难一丢丢

1. 用串口接收一个字节

波特率公式:

目标波特率 = 系统主时钟 / (16 * (BAUD + 1))

板载22.1184MHz晶振,经过PLL倍频后,系统主时钟为110.592MHz。

​ 假设要将串口的波特率设置为115200,求BAUD的值,则:

​ BAUD = 110.592MHz / 115200/16 - 1 = 59

|----|------------------------------|---------------------------------|---------------------------------|
| | (1)单字节接收 | (2)FIFO接收 | (3)DMA传输 |
| 优点 | 没有数据接收时,对CPU的占用几乎为0 | 接收较多数据时,降低了CPU的开销 | 接收大量数据时,大大降低了CPU的开销 |
| 缺点 | 接收较多的数据时,中断产生过于频繁,占用较多的CPU资源 | 没有数据接收时,仍然会定时进入中断(接收超时中断),有基础开销 | 没有数据接收时,仍然会定时进入中断(接收超时中断),有基础开销 |

(1)单字节接收

​ ->直接查询方式发送数据

​ ->每接收一个数据,产生一次中断

单字节接收数据编程

直接看官方例程

cpp 复制代码
void SerialInit(void)
{
    UART_InitStructure UART_initStruct;                        //配置变量

    //初始化引脚功能
    PORT_Init(PORTA, PIN2, FUNMUX0_UART0_RXD, 1);    //GPIOA.2配置为UART0输入引脚
    PORT_Init(PORTA, PIN3, FUNMUX1_UART0_TXD, 0);    //GPIOA.3配置为UART0输出引脚

     UART_initStruct.Baudrate = 115200;
    UART_initStruct.DataBits = UART_DATA_8BIT;        //8bit数据位
    UART_initStruct.Parity = UART_PARITY_NONE;        //无校验
    UART_initStruct.StopBits = UART_STOP_1BIT;        //1位停止位

    //当RX FIFO中数据个数 >  RXThreshold时触发中断
    UART_initStruct.RXThreshold = 0;                            
    UART_initStruct.RXThresholdIEn = 1;                        //使用接收中断

    //当TX FIFO中数据个数 <= TXThreshold时触发中断
    UART_initStruct.TXThreshold = 0;
    UART_initStruct.TXThresholdIEn = 0;                        //不使用发送中断

    //超时中断,RX FIFO非空,且超过 TimeoutTime/(Baudrate/10) 秒没有在RX线上接收到数据时触发中断

    UART_initStruct.TimeoutTime = 10;        
    UART_initStruct.TimeoutIEn = 0;

     UART_Init(UART0, &UART_initStruct);
    UART_Open(UART0);
}

当RX FIFO中数据个数 > RXThreshold时触发中断

UART_initStruct.RXThreshold = 3;

UART_initStruct.RXThresholdIEn = 1;


UART_initStruct.TimeoutTime = 10; // 超时时间

UART_initStruct.TimeoutIEn = 0; // 当前未使能

触发条件:

  • RX FIFO非空
  • 超过 TimeoutTime/(Baudrate/10) 秒没有收到新数据

Baudrate = 115200 每秒传输115200个比特

但一个字符不是1比特,而是:

  • 1个起始位
  • 8个数据位(你的配置)
  • 1个停止位
  • 0个校验位(你的配置)

总计 = 10位/字符

所以:

每秒能传输的字符数 = 115200 / 10 = 11520 字符/秒

每个字符的传输时间 = 1 / 11520 ≈ 0.0000868秒 = 86.8微秒

10个字符的传输时间=10*86.8us=868us=0.868ms

验证:超时时长 = TimeoutTime / (Baudrate/10)= 10 / (115200/10) ≈ 0.000868秒 = 0.868毫秒

中断服务函数:

cpp 复制代码
void UART0_Handler(void)
{
    uint32_t chr;

    //查询接收FIFO中断
    if(UART_INTStat(UART0, UART_IT_RX_THR))
    {
        while((UART0->FIFO & UART_FIFO_RXLVL_Msk) > 1)
        {
            if(UART_ReadByte(UART0, &chr) == 0)
            {
                //在中断中使用printf,仅作为演示,实际项目,不要使用。
                printf("%c",chr);
            }
        }
    }

 //查询接收超时中断

    if(UART_INTStat(UART0, UART_IT_RX_TOUT))
    {
        while(UART_IsRXFIFOEmpty(UART0) == 0)
        {
            if(UART_ReadByte(UART0, &chr) == 0)
            {
                //在中断中使用printf,仅作为演示,实际项目,不要使用。
                printf("%c",chr);
            }
        }
    }

    //发送FIFO中断
    if(UART_INTStat(UART0, UART_IT_TX_THR))
    {
        //不用发送中断
    }

}

(2)FIFO接收

->直接查询方式发送数据

->接收FIFO溢出时/半满时,产生一次中断 (缺陷: 如果对方停止发送数据,则无法产生中断)

->补充:定时产生接收超时中断,把未溢出/未达量的数据,拿出来处理。

cpp 复制代码
void SerialInit(void)
{
    UART_InitStructure UART_initStruct;                        //配置变量

    //初始化引脚功能
    PORT_Init(PORTA, PIN2, FUNMUX0_UART0_RXD, 1);    //GPIOA.2配置为UART0输入引脚
    PORT_Init(PORTA, PIN3, FUNMUX1_UART0_TXD, 0);    //GPIOA.3配置为UART0输出引脚

     UART_initStruct.Baudrate = 115200;
    UART_initStruct.DataBits = UART_DATA_8BIT;        //8bit数据位
    UART_initStruct.Parity = UART_PARITY_NONE;        //无校验
    UART_initStruct.StopBits = UART_STOP_1BIT;        //1位停止位


    //当RX FIFO中数据个数 >  RXThreshold时触发中断
    UART_initStruct.RXThreshold = 7;                            
    UART_initStruct.RXThresholdIEn = 1;                        //使用接收中断

    //当TX FIFO中数据个数 <= TXThreshold时触发中断
    UART_initStruct.TXThreshold = 0;
    UART_initStruct.TXThresholdIEn = 0;                        //不使用发送中断

    //超时中断,RX FIFO非空,且超过 TimeoutTime/(Baudrate/10) 秒没有在RX线上接收到数据时触发中断

    UART_initStruct.TimeoutTime = 10;        //10个字符时间内未接收到新的数据则触发超时中断
    UART_initStruct.TimeoutIEn = 1;

    UART_Init(UART0, &UART_initStruct);
    UART_Open(UART0);
}

(3)DMA传输

传输大量数据,尽可能不适用CPU资料

->直接查询方式发送数据

->接收FIFO溢出时,DMA直接取走数据( DMA取数据,几乎不消耗CPU资源),可以等数据多了,再处理

->定时产生接收超时中断,处理未溢出的数据。

2. 从环形缓冲区里面把数据读出来

main()函数的while(1)循环中,从环形缓冲区里面把数据读出来,解析

(1)实现环形缓冲区

启用豆包

loopbuf.h

cpp 复制代码
#ifnedf __loopbuf_h__
#define __loopbuf_h__

typedef struct {
    unsigned char* buffer;
    unsigned int size;
    unsigned int head;
    unsigned int tail;
} loopbuf_t;

extern loopbuf_t* loopbuf_init(unsigned int size);
extern void loopbuf_free(loopbuf_t* lb);
extern unsigned int loopbuf_write(loopbuf_t* lb, const unsigned char* data, unsigned int n);
extern unsigned int loopbuf_read(loopbuf_t* lb, unsigned char* data, unsigned int n);

#endif

loopbuf.c

cpp 复制代码
#include <stdlib.h>

// 内存分配函数替代
void* my_malloc(unsigned int size) {

    return malloc( size );
}

// 内存释放函数替代
void my_free(void* ptr) {
    // 此处应实现平台无关的内存释放
    free( ptr );
}

// 内存复制函数替代
void my_memcpy(unsigned char* dest, const unsigned char* src, unsigned int n) {
    unsigned int i;
    for (i = 0; i < n; i++) {
        dest[i] = src[i];
    }
}

loopbuf_t* loopbuf_init(unsigned int size) {
    loopbuf_t* lb = (loopbuf_t*)my_malloc(sizeof(loopbuf_t));
    if (!lb) return 0;

    lb->buffer = (unsigned char*)my_malloc(size);
    if (!lb->buffer) {
        my_free(lb);
        return 0;
    }

    lb->size = size;
    lb->head = 0;
    lb->tail = 0;
    return lb;

}

void loopbuf_free(loopbuf_t* lb) {
    if (lb) {
        if (lb->buffer) my_free(lb->buffer);
        my_free(lb);
    }
}

写:

cpp 复制代码
unsigned int loopbuf_write(loopbuf_t* lb, const unsigned char* data, unsigned int n)
{
    // 参数检查
    if (!lb || !data || n == 0) return 0;
    
    // ========== 步骤1:计算剩余空间 ==========
    unsigned int used;  // 已使用的空间
    
    if (lb->head >= lb->tail) {
        // 情况A:写指针在读指针后面或相同位置(没有绕回)
        used = lb->head - lb->tail;
    } else {
        // 情况B:读指针超过了写指针(已经绕回过一次)
        used = lb->size - (lb->tail - lb->head);
    }
    
    unsigned int space = lb->size - used;  // 剩余空间 = 总大小 - 已使用
    
    // 如果要写入的数据大于剩余空间,只写入能放下的部分
    if (n > space) n = space;
    if (n == 0) return 0;  // 没有空间可写
    
    // ========== 步骤2:计算从head到缓冲区末尾的剩余长度 ==========
    unsigned int part1 = lb->size - lb->head;  // 从head到末尾还有多少空间
    
    // ========== 步骤3:分情况写入 ==========
    if (n <= part1) {
        // 情况1:要写入的数据不需要绕回
        // 例如:head=3, part1=5, n=4 (4 <= 5)
        my_memcpy(lb->buffer + lb->head, data, n);
        lb->head = (lb->head + n) % lb->size;  // 更新写指针
    } else {
        // 情况2:要写入的数据需要分成两段(绕回)
        // 例如:head=6, part1=2, n=4 (4 > 2)
        
        // 第一段:从head写到缓冲区末尾
        my_memcpy(lb->buffer + lb->head, data, part1);
        
        // 第二段:从缓冲区开头继续写
        my_memcpy(lb->buffer, data + part1, n - part1);
        
        // 更新写指针:第二段写完后停在的位置
        lb->head = n - part1;
    }
    
    return n;  // 返回实际写入的字节数
}

情况1:不需绕回写入

情况2:需绕回写入

读:

cpp 复制代码
unsigned int loopbuf_read(loopbuf_t* lb, unsigned char* data, unsigned int n)
{
    if (!lb || !data || n == 0) return 0;
    
    // ========== 步骤1:计算可读数据量 ==========
    unsigned int available;  // 缓冲区中已有的数据量
    
    if (lb->head >= lb->tail) {
        // 情况A:写指针在读数针后面(没有绕回)
        available = lb->head - lb->tail;
    } else {
        // 情况B:读指针超过了写指针(已经绕回)
        available = lb->size - (lb->tail - lb->head);
    }
    
    // 如果要读取的大于可用数据,只读能读到的部分
    if (n > available) n = available;
    if (n == 0) return 0;  // 没有数据可读
    
    // ========== 步骤2:计算从tail到缓冲区末尾的剩余长度 ==========
    unsigned int part1 = lb->size - lb->tail;  // 从tail到末尾还有多少空间
    
    // ========== 步骤3:分情况读取 ==========
    if (n <= part1) {
        // 情况1:不需要绕回就能读完
        my_memcpy(data, lb->buffer + lb->tail, n);
        lb->tail = (lb->tail + n) % lb->size;  // 更新读指针
    } else {
        // 情况2:需要分成两段读(因为数据绕回了)
        
        // 第一段:从tail读到缓冲区末尾
        my_memcpy(data, lb->buffer + lb->tail, part1);
        
        // 第二段:从缓冲区开头继续读
        my_memcpy(data + part1, lb->buffer, n - part1);
        
        // 更新读指针:第二段读完后停在的位置
        lb->tail = n - part1;
    }
    
    return n;  // 返回实际读取的字节数
}

情况1:不需绕回

情况2:需绕回

(2)接收数据写入环形缓冲区

用串口0的中断服务器函数实现

cpp 复制代码
void UART0_Handler(void)
{
    uint32_t chr;    
    //接收FIFO中断
    if(UART_INTStat(UART0, UART_IT_RX_THR))
    {
    // 当FIFO中数据个数 > 阈值(你设为3)时触发
    
    while((UART0->FIFO & UART_FIFO_RXLVL_Msk) > 1)
    {
        if(UART_ReadByte(UART0, &chr) == 0)  // 从硬件读取一个字节
        {
            // 存入环形缓冲区
            loopbuf_write(lb_uart0, (unsigned char*)(&chr), 1);
        }
    }
    }
    //接收超时中断
    if(UART_INTStat(UART0, UART_IT_RX_TOUT))
    {
    // 当接收超时(你设的10个字符时间)触发
    
    while(UART_IsRXFIFOEmpty(UART0) == 0)  // FIFO非空就继续读
    {
        if(UART_ReadByte(UART0, &chr) == 0)
        {
            // 同样存入环形缓冲区
            loopbuf_write(lb_uart0, (unsigned char*)(&chr), 1);
        }
    }
    }
    //发送FIFO中断
    if(UART_INTStat(UART0, UART_IT_TX_THR))
    {
        //不使用中断发送
    }

}

(3)获取一条合法的帧

cpp 复制代码
#define FrameHead     0xA5                //固定帧头
#define FrameTail    0x5A                //固定帧尾
#define FrameLen 12                        //固定帧长度
int rx_sta = 0;                            //描述接收的状态
int rx_len = 0;                            //描述已经接收到的数据长度
uint8_t rx_buf[ FrameLen ];                //接收缓冲区
uint32_t cntr = 0;
void rx_cmd_data( void )
{
    int ret =0,i=0;


    uint8_t sum = 0;
    switch( rx_sta )
    {
        //等待帧头
        case 0:
            ret = loopbuf_read(lb_uart0, rx_buf, 1);
            if( ret == 0 ) break;

            if( rx_buf[0] !=  FrameHead ) break;

            //执行到这里,说明可能收到了帧头
            rx_len++;
            rx_sta++;
            break;

        //等待接收完数据
        case 1:
            ret = loopbuf_read(lb_uart0, &(rx_buf[rx_len]), FrameLen - rx_len );
            rx_len += ret;
            if( rx_len < FrameLen ) break;

            //执行到这里,说明接收到了数据长度为 帧长度 FrameLen

            //①判断 rx_buf[ FrameLen - 1 ] 是否为帧尾
            if( rx_buf[ FrameLen - 1 ] != FrameTail )
            {//成立,说明接收到的数据不合法
                rx_sta = 0;
                rx_len = 0;
                break;
            }
            //②判断检验和是否正确
            sum = 0;
            for( i=0;i<9;i++ )
            {
                sum = sum + rx_buf[1+i];
            }

            if( rx_buf[ 10 ] != sum )
            {//成立,说明接收到的数据不合法
                rx_sta = 0;
                rx_len = 0;
                break;
            }

            //如果都正确,则说明命令合法,解析数据
            printf("%d\r\n",cntr++);
            analysis_cmd( rx_buf );
            rx_sta = 0;
            rx_len = 0;

            break;

        default:
            break;

    }    

}

(4)按协议编写

cpp 复制代码
// 游戏指令协议
typedef struct{
    uint8_t head;     // 帧头 0xAA
    uint8_t cmd;      // 命令: 0x01=出拳, 0x02=查询
    uint8_t player;   // 玩家ID
    uint8_t gesture;  // 手势: 1=石头, 2=剪刀, 3=布
    uint16_t bet;     // 下注积分
    uint8_t reserve;  // 保留
    uint8_t check;    // 校验
    uint8_t tail;     // 帧尾 0x55
} GameCmd_t;

void parse_game_cmd(uint8_t* data)
{
    if (data[1] == 0x01)  // 出拳命令
    {
        GameCmd_t* cmd = (GameCmd_t*)data;
        
        printf("玩家 %d 出了", cmd->player);
        switch(cmd->gesture) {
            case 1: printf("石头"); break;
            case 2: printf("剪刀"); break;
            case 3: printf("布"); break;
        }
        printf(",下注 %d 积分\n", cmd->bet);
    }

     if( data[1] == 0x02 ) // 查询命令
    {
        //修改输出
        printf(" look\r\n ");
        return;
    }    

    }
}

3. 实现输入基础命令

希望的效果

​ 在串口接收数据中断时,将数据写进环形缓冲区lb_uart0后,实现命令行代码如下:

(1)编写命令行

cpp 复制代码
typedef int (*deal_cmd_t)(int argc,char** argv);
//命令行结构体
typedef struct{
    char* name;                    //字符串
    deal_cmd_t entry;            //操作函数

}cmd_t;

int cmd_test(int argc, char** argv) 
{
    if (argc == 1) {
        // 只有命令名,没有参数
        printf("test - 测试命令\n");
        printf("用法: test [参数...]\n");
        return 0;
    }
    
    printf("收到 %d 个参数:\n", argc-1);
    for(int i = 1; i < argc; i++) {
        printf("  [%d]: %s\n", i, argv[i]);
    }
    return 0;
}

int cmd_clear(int argc, char** argv) 
{
    // ANSI转义序列清屏
    printf("\033[2J\033[H");
    return 0;
}

int cmd_help(int argc, char** argv) 
{
    printf("\n可用命令:\n");
    for(int i = 0; i < ITEM_NUM(__cmd_list); i++) {
        printf("  %-10s - %s\n", 
               __cmd_list[i].name, 
               __cmd_list[i].help);
    }
    return 0;
}

int cmd_reboot(int argc, char** argv) 
{
    printf("系统将在3秒后重启...\n");
    delay_ms(3000);
    NVIC_SystemReset();  // 对于STM32
    return 0;  // 不会执行到这里
}

(2)切割字符串函数

cpp 复制代码
//切割字符串
//max 最多解析几个参数
//str 需要处理的字符串
//result 存放处理的结果
//delimiters 分隔符 " \t\n\r"
static int split_string_n(int max,char *str, char **result, const char *delimiters) 
{
    int count = 0;
    char *token;
    char *saveptr; // 用于保存上下文的指针

    // 使用 strtok_r 函数拆分字符串
    token = strtok_r(str, delimiters, &saveptr);

    while (token != NULL) {
        result[count] = token; // 保存当前的 token
        count++;
                if( count>=max ) break;
        // 获取下一个 token
        token = strtok_r(NULL, delimiters, &saveptr);
    }

    return count;

}

(3)使用命令行

cpp 复制代码
#define MaxArgc    10        //最多支持10个输入参数
#define MaxLen 128
static char rxbuf[ MaxLen ];    //接收缓冲区,存储从环形缓冲区读取出来数据
static int rx_len = 0;                //描述已获取数据长度

static int sta = 0;                        //

void cmd_line_work( void )
{
    uint8_t* buf = NULL;
    int argc = 0;
    char* argv[MaxArgc];

    switch( sta )
    {
        //打印提示符
       case 0:
            printf("\r\n>");           // 打印提示符 ">"
            memset(rxbuf, 0, sizeof(rxbuf));  // 清空接收缓冲区
            rx_len = 0;                 // 重置接收长度
            sta++;                      // 进入状态1
            break;

        //等回车符
        case 1:
            //每次读取一个字节数据出来
            buf = (uint8_t*)( &(rxbuf[rx_len])  );
            int ret = loopbuf_read( lb_uart0,buf,1 );
            if( ret == 0 ) break;
            rx_len += ret;    

            //需要判断缓冲区是否溢出
            if( rx_len >= MaxLen )
            {//成立,说明缓冲区满了
                sta = 0;
                break;
            }    

            switch( buf[0] )
            {
                case 0x09://TAB键
                    //检索适配命令,并打印出来提示

                    break;

                case 0x7F:
                case '\b':                    
                    //需要先搞懂 rx_len 代表什么含义
                    //rx_len 此时,代表了收到退格符之后,下一个接收数据的数组下标
                    rx_len --;
                    //rx_len 此时,代表了退格符的数组下标
                    rxbuf[ rx_len ] = 0;    //清除退格符号

                    //按下退格键的目的,是为了把原有的数据,清掉一个

                    //先判断原来有没有数据
                    if( rx_len == 0 )
                    {//说明,原本是没有数据的

                    }
                    else
                    {//说明,原本是有数据的
                        rx_len--;
                        //rx_len 此时,代表了原有数据的最后一个字符的数组下标
                        rxbuf[ rx_len ] = '\0';    //清除原有数据的最后一个字符 

                    }
                    printf("\r\n>%s",rxbuf);
                    break;

                case '\r':
                    argc = split_string_n( MaxArgc,rxbuf,argv," \t\n\r" );
                    if( argc != 0 )
                    {

                        //在命令表里面匹配 argv[0]
                        for( int i=0;i< (ITEM_NUM( __cmd_list ) );i++ )
                        {
                            if( 0 == strncmp( rxbuf,__cmd_list[i].name ,strlen( __cmd_list[i].name ) ) )
                            {//成立,说明字符串一致,找到相应的命令了
                                printf("\r\n");
                                __cmd_list[i].entry( argc,argv );

                                sta = 0;
                                return;
                            }

                        }
                        printf("\r\n\tunknow");
                    }

                    sta = 0;

                    break;

                default:
                    //直接打印出来(回显)
                    printf("%c",buf[0] );
                    break;

            }

            break;

        default:
            break;

    }

}

main函数调用

cpp 复制代码
int main(void)
{
    SystemInit();            //初始化系统时钟


    SerialInit( 115200 );            //初始化串口

    printf("system run...\r\n");

    while(1==1)
    {
            cmd_line_work();

    }

}

下一次学习:使用FreeRTOS搭建工程项目

相关推荐
逐步前行2 小时前
STM32_SysTick_系统定时器
stm32·单片机·嵌入式硬件
果果燕2 小时前
网络编程-TCP 协议学习笔记
网络·学习·tcp/ip
蒸蒸yyyyzwd2 小时前
设计模式之美学习笔记
笔记·学习·设计模式
非凡ghost2 小时前
Smart Launcher安卓版(安卓桌面启动器)
android·windows·学习·音视频·软件需求
智慧化智能化数字化方案2 小时前
向华为学习——解读华为工业与AI融合应用指南【】
人工智能·学习·华为工业与ai融合应用指南
盐水冰2 小时前
【烘焙坊项目】后端搭建(11)- 用户&商家订单板块
java·后端·学习
头疼的程序员2 小时前
计算机网络:自顶向下方法(第七版)第五章 学习分享(一)
学习·计算机网络
zyb11475824332 小时前
集合的学习
开发语言·python·学习
逐步前行2 小时前
STM32_外部中断_寄存器操作
stm32·单片机·嵌入式硬件