第六章 UART ------ 让单片机与世界"对话"
到目前为止,我们的单片机还像一个"独行侠",只能通过闪烁的LED来表达自己。本章,我们将为它开启一扇与外部世界沟通的窗户------UART (通用异步收发器)。通过它,我们可以让单片机向电脑发送调试信息,也可以通过电脑向单片机发送控制指令,实现真正的双向交互。
1. 什么是UART?------ 两个设备间的"电话线"
UART是一种串行(Serial)、**异步(Asynchronous)**的通信协议。
- 串行:想象一条单车道,所有数据(0和1)像一列自行车一样,一个接一个地在一条线上进行传输。这与一次能走8辆车的"并行"通信相比,大大节省了引脚资源。
- 异步 :通信的双方没有共享一根时钟线。它们就像两个约定好语速的人打电话,只要双方的"语速"(即波特率 Baud Rate )一致,就能正确理解对方。为了在没有时钟的情况下同步,UART在每个数据包前后都加上了特殊的信号:
- 起始位 (Start Bit):一个固定的低电平,通知接收方:"嘿,我要开始说话了!"
- 数据位 (Data Bits):通常是8位,是我们真正要传输的数据。
- 停止位 (Stop Bit):一个或多个固定的高电平,表示:"这句话说完了。"
(文字时序图: 空闲(高) -> 起始位(低) -> [D0,D1,D2,D3,D4,D5,D6,D7] -> 停止位(高) -> 空闲(高))
2. 硬件连接:从MCU引脚到PC的"翻译官"
如您所述,我们的开发板上集成了一颗CH340E芯片。这颗芯片扮演着"翻译官"的角色:
- 它将PC的USB信号,翻译成MCU能听懂的TTL电平串口信号。
- 它将MCU的TTL电平串口信号,翻译成PC能识别的USB信号。
在我们的原理图中:
- MCU的
PA22 (UART0_RX)引脚连接到CH340E的发送端。 - MCU的
PA23 (UART0_TX)引脚连接到CH340E的接收端。
注意 :通信双方的接线永远是 TX (发送) → RX (接收) ,RX (接收) → TX (发送)。
3. 软件设计:从SysConfig到健壮的应用
3.1 SysConfig:图形化配置UART
和之前一样,我们优先使用SysConfig来完成底层的初始化配置,而不是手写这些复杂的结构体。
- 打开
.syscfg文件,在"SOFTWARE"中添加并选择 UART 模块。 - 基本配置 (Basic Configuration):
- Mode :
Normal - Direction :
TX and RX - Parity :
None - Desired Baud Rate : 输入
9600。SysConfig会自动计算出下方的IBRD和FBRD值(也就是您代码中的26和3)。
- 引脚配置 (Pin Configuration):
- RX Pin : 选择
PA22。 - TX Pin : 选择
PA23。
- 中断配置 (Interrupt Configuration):
- 勾选 Enable RX Interrupt,允许接收中断。
- 在NVIC配置中,确保UART0的中断是使能的,并可以为其分配合理的优先级。
- 保存 。SysConfig会自动生成您草稿中展示的
SYSCFG_DL_UART_0_init()函数及相关的gUART_0ClockConfig和gUART_0Config结构体。
3.2 发送数据:让单片机"开口说话"
3.2.1 阻塞式发送:最简单但需谨慎
您的草稿中提供了一个基础的发送函数:
c
// 发送单个字节
void uart_send_char(uint8_t ch)
{
// 等待,直到发送缓冲区为空(硬件发送完上一个字节)
while(DL_UART_Main_isBusy(UART_0_INST));
DL_UART_Main_transmitData(UART_0_INST, ch);
}
// 发送字符串
void uart_send_string(char* str)
{
while(*str != '\0')
{
uart_send_char(*str++);
}
}
【深度剖析】 : while(DL_UART_Main_isBusy(...)) 是一种阻塞式操作。如果UART因为某些原因一直处于忙碌状态,程序就会卡死在这里。在简单的应用中尚可接受,但在需要高实时性的多任务系统中,应尽量避免或为其增加超时退出机制。
3.2.2 printf重定向:开发者的福音
重定向printf函数,能让我们像在PC上编程一样,方便地打印变量和格式化字符串。您的代码已非常标准。
c
// 在你的UART驱动文件中 (e.g., UART.c)
#include <stdio.h>
/* ... Keil MDK的兼容性代码 ... */
// #if !defined(__MICROLIB) ... #endif
// 重定向fputc函数,将printf的输出导向到UART
int fputc(int ch, FILE *stream)
{
// 等待发送缓冲区空闲
while(DL_UART_Main_isBusy(UART_0_INST));
// 将字符写入UART的发送数据寄存器
DL_UART_Main_transmitData(UART_0_INST, (uint8_t)ch);
return ch;
}
现在,我们可以在main.c中愉快地使用printf了!
3.3 接收数据:从"简单回显"到"环形缓冲"
您的草稿中,中断服务函数只是简单地将收到的数据再发回去:
c
// 简单的回显式中断
void UART0_IRQHandler(void)
{
...
uart_data = DL_UART_Main_receiveData(UART_0_INST);
uart_send_char(uart_data);
...
}
这种方式只能用于测试,无法构建复杂的应用。因为它有一个致命缺陷:中断处理快,主循环处理慢 。如果PC快速发送一串字符"LED_ON",中断会快速地将这6个字符放入uart_data变量中,但由于uart_data只能存一个字符,前5个字符都会被覆盖丢失!
【解决方案】引入环形缓冲区 (Ring Buffer)
环形缓冲区是一个先进先出(FIFO)的定长数组,它利用两个指针(读指针tail和写指针head)来巧妙地实现"循环"写入和读取,是处理异步数据流的标准数据结构。
- 定义环形缓冲区 (在
UART.c中)
c
#define RX_BUFFER_SIZE 64 // 定义缓冲区大小
volatile uint8_t g_rxBuffer[RX_BUFFER_SIZE];
volatile uint32_t g_rxHead = 0; // 写指针,由中断操作
volatile uint32_t g_rxTail = 0; // 读指针,由主循环操作
- 重写中断服务函数 (在
UART.c中)
c
void UART0_IRQHandler(void)
{
switch (DL_UART_Main_getPendingInterrupt(UART_0_INST)) {
case DL_UART_MAIN_IIDX_RX:
{
// 从硬件接收数据
uint8_t data = DL_UART_Main_receiveData(UART_0_INST);
// 计算下一个写指针位置
uint32_t next_head = (g_rxHead + 1) % RX_BUFFER_SIZE;
// 如果缓冲区未满,则存入数据
if (next_head != g_rxTail) {
g_rxBuffer[g_rxHead] = data;
g_rxHead = next_head;
}
// 如果缓冲区满了,则丢弃数据(或进行其他错误处理)
// (可选) 为了调试,可以回显刚收到的字符
//uart_send_char(data);
break;
}
default:
break;
}
}
- 提供读取函数 (供
main函数调用)
c
// 从环形缓冲区读取一个字节
bool uart_get_char(uint8_t *ch)
{
// 如果读写指针相同,说明缓冲区为空
if (g_rxHead == g_rxTail) {
return false;
}
*ch = g_rxBuffer[g_rxTail];
g_rxTail = (g_rxTail + 1) % RX_BUFFER_SIZE;
return true;
}
3.4 主函数测试:打造一个交互式LED控制器
现在,主函数不再是盲目地闪烁LED,而是等待并解析来自PC的命令。
c
#include "ti_msp_dl_config.h"
#include <stdio.h>
#include <string.h> // 用于字符串比较
#include "LED.h"
#include "SYSTICK.h"
#include "UART.h" // 包含我们新加的 uart_get_char
// 命令缓冲区
#define CMD_BUFFER_SIZE 32
char g_cmdBuffer[CMD_BUFFER_SIZE];
uint8_t g_cmdIndex = 0;
// 解析并执行命令
void process_command(void)
{
if (strcmp(g_cmdBuffer, "LED_ON") == 0) {
LED_ON();
printf("Command accepted: LED is now ON.\r\n");
} else if (strcmp(g_cmdBuffer, "LED_OFF") == 0) {
LED_OFF();
printf("Command accepted: LED is now OFF.\r\n");
} else {
printf("Unknown command: %s\r\n", g_cmdBuffer);
}
// 清空命令缓冲区
g_cmdIndex = 0;
memset(g_cmdBuffer, 0, CMD_BUFFER_SIZE);
}
int main(void)
{
SYSCFG_DL_init();
// LED_Init(); // 已被SYSCFG_DL_init()包含
// SysTick_init(); // 如需延时则保留
// UART初始化和中断使能也由 SYSCFG_DL_init() 完成
__enable_irq();
printf("\r\n--- MSPM0 UART Interactive LED Control ---\r\n");
printf("Send 'LED_ON' or 'LED_OFF' to control the LED.\r\n");
while (1)
{
uint8_t received_char;
// 检查环形缓冲区是否有新数据
if (uart_get_char(&received_char)) {
// 如果收到的是回车符或换行符,认为一条命令结束
if (received_char == '\r' || received_char == '\n') {
if (g_cmdIndex > 0) {
process_command();
}
} else {
// 将字符存入命令缓冲区
if (g_cmdIndex < CMD_BUFFER_SIZE - 1) {
g_cmdBuffer[g_cmdIndex++] = received_char;
}
}
}
}
}
测试步骤:
- 编译并烧录程序。
- 打开一个串口调试助手(如SSCOM、MobaXterm)。
- 设置波特率为9600,8-N-1。
- 在发送框中输入
LED_ON,然后点击发送。 - 观察:开发板上的LED会点亮,同时串口助手会收到回复 "Command accepted: LED is now ON."
- 输入
LED_OFF并发送,LED会熄灭。
通过环形缓冲区,我们成功构建了一个健壮的、非阻塞的串口通信框架,将中断与主循环的职责清晰地分离开来,这是嵌入式系统编程中一个极其重要的设计模式。