上一次学习了使用时钟和定时器,较为简单
本次学习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搭建工程项目

