☰
杰理AC63串口通信实战:从初始化到中断环形缓冲区调试指南
2026/9/28 2:20:09 网站建设 项目流程

最近调试杰理AC63平台,最让我头疼的不是蓝牙协议栈,反而是最基础的串口通信。串口这东西,写起来简单,但一旦涉及引脚复用、时钟配置、中断优先级、缓冲区管理,问题立刻变得很立体。尤其AC63这类面向音频蓝牙的SoC,官方SDK给的例程大多围绕音频链路展开,串口相关的demo相对零散,新手很容易在“明明初始化了却收不到数据”这个坎上卡上好几天。

这篇文章就把我从零开始搭建AC63串口通信的全过程拆开来讲,涵盖硬件接线、SDK工程配置、串口驱动编写、数据收发验证,以及实战中踩过的典型坑。代码会给出关键实现并逐段解析,目标是让任何一个拿到AC63开发板的人,都能在一个小时内打通串口这条调试生命线。

1. 项目概述与整体设计思路

1.1 为什么做杰理AC63的串口通信

杰理AC63系列是低功耗蓝牙音频SoC,常用于TWS耳机、蓝牙音箱、智能穿戴等产品。这类芯片有一个共同特点:外设资源不像通用MCU那么丰富,但麻雀虽小五脏俱全。UART作为最基本、最可靠的调试手段,在所有嵌入式项目里都是刚需,尤其在AC63这种没有JTAG/SWD标准调试口的平台上,串口几乎是唯一能“看到”芯片内部运行状态的窗口。

我在实际项目里遇到过一个场景:蓝牙连接偶发断链,用示波器抓RF信号不现实,只能靠串口打印协议栈事件、连接间隔、RSSI变化等关键信息。这时候如果串口还没打通,整个调试工作就会陷入盲人摸象的状态。所以,串口通信对AC63开发来说不是“锦上添花”,而是“保命技能”。

1.2 一次串口通信的完整链路

从数据流的角度看,一次完整的串口通信链路包含以下环节:

  • MCU内部:CPU把数据写入UART发送FIFO,硬件按照配置的波特率逐位移出;接收方向则是硬件把采样到的电平组装成字节,放入接收FIFO,再触发中断通知CPU取走。
  • 外部接线:MCU的TX引脚连接到USB转TTL模块的RX引脚,MCU的RX连接模块的TX,双方必须共地。
  • PC端:串口助手或终端软件通过虚拟串口与USB转TTL模块通信,把收到的字节显示出来,也能把用户输入发送给MCU。

任何一个环节断了,现象都是“收不到数据”或“乱码”,这也是后面排查的难点所在。

1.3 本实战的工程目标

为了让整个流程有明确的验收标准,我给这次实战定了三个目标:

  1. 打通AC63与PC之间的双向数据通道,实现“PC发什么,MCU回什么”的echo功能。
  2. 实现基于中断的接收机制,并配合环形缓冲区,确保连续发送大包数据时不丢字节。
  3. 重定向printf到串口,后续所有日志打印都可以直接复用。

这三个目标覆盖了串口开发的核心场景:发送、接收、日志输出。完成它们之后,你手里的串口通信能力已经能满足绝大多数调试需求。

2. 开发环境与硬件接线准备

2.1 芯片与板子信息确认

做任何外设开发之前,先把芯片型号、封装、引脚定义确认清楚。AC63系列下有多个子型号,管脚数量从几十到上百不等,我手头这块板子主控是AC631N,QFN封装,板载USB口、按键、LED和音频相关电路。

建议你拿到板子后先做三件事:

  • 找到原理图中MCU部分的电源网络,确认VDDIO电压域,通常为3.3V,也有部分型号支持1.8V,串口电平必须与之匹配。
  • 确认UART引脚是否被其他功能复用了,比如某些封装下UART_TX和UART_RX默认接到了音频或LED驱动上,需要改硬件或者看SDK是否支持引脚映射。
  • 查看SDK自带的board配置头文件,里面通常有GPIO复用表,例如board_AC631N_demo.h这类文件,里面定义了UART_TX_PIN、UART_RX_PIN的具体引脚。

提示:AC63的开发流程高度依赖SDK模板工程,不同版本的SDK引脚定义方式可能有差异,不要凭记忆写死引脚,务必以SDK头文件里的宏定义为准。

2.2 开发工具链与SDK搭建

杰理官方提供了完整的SDK和IDE工具链,通常是基于Eclipse或自家的集成开发环境。你需要准备:

  • AC63系列SDK,建议直接从官方代理商或开发者社区获取最新版本,找资料时注意辨别版本新旧,不同SDK对子型号支持力度差别不小。
  • 编译工具链:SDK通常自带交叉编译工具链,或者需要单独安装,Linux环境下一般就是riscv64-unknown-elf-gcc这类工具链。如果SDK文档没有明确说明,建议在Linux下编译,整个过程会顺畅很多。
  • 烧录工具:杰理芯片一般通过USB口烧录,配套烧录工具里有专门的下载模式操作,通常是按住板载按键再插USB进入烧录状态。

我实际使用的环境是Ubuntu 20.04 + SDK自带的编译脚本,编译命令一般是./build.sh或make加目标板配置。Windows下也有对应的IDE,但路径和脚本方式有些区别,建议入门优先考虑Linux环境,遇到编译报错时网上的问题沉淀更多,排查效率更高。

2.3 UART引脚选择与电平匹配

串口通信中最容易被忽略的就是电平问题。AC63的UART引脚属于芯片IO口的一部分,输出电平直接由VDDIO决定。如果VDDIO是3.3V,那串口就是3.3V TTL电平,绝对不能用RS232电平的设备直连,否则轻则通信失败,重则烧毁IO口。

另外还要关注USB转TTL模块本身的电平范围。市面上常见的CH340G模块支持5V和3.3V两种电平跳线,CP2102模块通常是3.3V,FT232模块则根据型号差异较大。务必把模块跳到3.3V档位,并与AC63共地。

如果板子上没有单独的串口引出排针,可以从MCU引脚飞线出来,注意线长不要超过15厘米,否则高速波特率下信号质量会明显下降。

2.4 串口转USB模块选型与接线表

我使用过CH340、CP2102、FT232三种模块,稳定性排名大致是FT232 > CP2102 > CH340,但CH340性价比最高,日常调试完全够用。关键是驱动支持好,Windows和Linux都免驱或自动识别。

接线方式并不复杂,但方向容易搞反,给你一张对照表:

AC63引脚USB转TTL模块引脚说明
UART_TXRX发送接对方接收,交叉
UART_RXTX接收接对方发送,交叉
GNDGND必须共地,否则参考电位不一致
VDDIO(可选)VCC(3.3V)若模块需要供电才接,不要用5V

接好之后,在PC端查看设备管理器,应该能看到新增的COM口(Windows)或/dev/ttyUSB0(Linux)。

3. 串口初始化与代码实现

3.1 从SDK工程里找出串口外设驱动

AC63 SDK的架构通常是:apps目录放应用代码,sdk目录放底层驱动库。串口相关驱动一般在sdk/src/device/uart目录下,文件常见命名有uart.c、uart.h,有的版本会分平台实现,比如uart_ac63xx.c。

其实写应用层代码之前,先用一句话解释清楚UART的初始化本质:把引脚从普通的GPIO模式切换到UART复用功能,然后设置波特率、数据位、停止位、校验位,最后使能发送和接收通道。至于时钟树、FIFO深度、中断使能这些,SDK的驱动接口已经封装好了,我们只需要调用对应API即可。

我在SDK中看到的常用接口大致长这样:

/* 初始化串口,设置波特率,8数据位,1停止位,无校验 */ void uart_init(u32 baudrate); /* 发送一个字节 */ void uart_putc(u8 ch); /* 发送一组数据 */ void uart_send_buf(u8 *buf, u16 len); /* 查询接收缓冲区是否有数据 */ u8 uart_rx_is_empty(void); /* 读取一个字节 */ u8 uart_getc(void);

具体函数名每个SDK版本可能略有不同,但整体风格非常接近,拿到头文件看一眼就能对上号。下面的代码我会基于这组常用API来写,你再对照自己SDK里的头文件稍作修改即可。

3.2 初始化串口,完成一次性配置

初始化代码是整个串口通信的地基。地基没打好,后面一切功能都是空中楼阁。参考实现如下:

#include "uart.h" #include "gpio.h" void uart_config_init(void) { /* 1. 配置TX和RX引脚的复用功能 */ gpio_set_fun(GPIO_PIN_TX, GPIO_FUN_UART_TX); gpio_set_fun(GPIO_PIN_RX, GPIO_FUN_UART_RX); /* 2. 初始化UART外设,波特率115200 */ uart_init(115200); /* 3. 使能接收中断,下面小节会详细说明 */ uart_rx_irq_enable(); }

这里有个容易踩的坑:先配置引脚复用,再初始化UART外设。如果把顺序颠倒,UART初始化可能把引脚重新复位成GPIO模式,导致数据发不出去也收不到。有些SDK的uart_init内部会自动处理引脚配置,但保险起见,显式调用gpio_set_fun做了双保险总是没错的。

波特率的选择上,115200是默认首选。它兼顾了速度和稳定性,大多数USB转TTL模块在115200下表现都很稳定,PC端串口助手设置也方便。如果你的调试环境电磁干扰大或者线比较长,可以降到57600或38400,通信稳定性会更好,代价是传输速度下降。921600这种高波特率在日常调试中不推荐,信号稍有畸变就会出现连续乱码。

3.3 实现发送:轮询发送与printf重定向

发送数据有两种实现思路:一种是轮询发送,简单直接;另一种是中断发送或DMA发送,效率高但逻辑复杂。对调试场景来说,轮询发送完全够用,代码也不容易出问题。

void uart_send_string(char *str) { while (*str) { uart_putc((u8)*str++); } } void uart_send_hex(u8 val) { char buf[3]; /* 将val转为两位十六进制字符串 */ buf[0] = (val >> 4) > 9 ? (val >> 4) - 10 + 'A' : (val >> 4) + '0'; buf[1] = (val & 0x0F) > 9 ? (val & 0x0F) - 10 + 'A' : (val & 0x0F) + '0'; buf[2] = '\0'; uart_send_string(buf); }

printf重定向是调试利器。把标准库的printf输出重定向到串口,就能像在PC上写代码一样打印格式化日志。不同编译环境的重定向方式不同,在GCC工具链下,只需要实现_write或_putc这类底层接口。SDK里的回调风格是定义一个输出函数交给标准库,你可以这么写:

int _write(int fd, char *buf, int size) { for (int i = 0; i < size; i++) { uart_putc((u8)buf[i]); } return size; }

之后在代码里直接调用printf("BT now state: %d\n", state);就能往串口打日志了。要注意的是,printf这类格式化输出是有执行开销的,如果你的主循环里有实时性要求较高的逻辑,建议只在调试版本里启用printf,量产固件里换成uart_send_string这种轻量输出。

3.4 实现接收:中断配合环形缓冲区

接收数据是串口通信中最容易翻车的部分。轮询接收虽然简单,但要求CPU持续查询接收标志位,稍微一忙就会丢数据。更好的方案是:串口收到一个字节就触发一次中断,中断服务程序把字节塞进一个环形缓冲区,主循环定时或按需从缓冲区取数据处理。

环形缓冲区可以简单理解成一块循环使用的内存区域,写指针追读指针,读指针追写指针,只要缓冲区容量足够大,短时间内即使主循环来不及处理,数据也不会丢。

先定义缓冲区结构体:

#define UART_RX_BUF_SIZE 256 typedef struct { u8 buf[UART_RX_BUF_SIZE]; volatile u16 head; volatile u16 tail; } uart_ring_buf_t; static uart_ring_buf_t g_rx_ring; void ring_buf_init(uart_ring_buf_t *ring) { ring->head = 0; ring->tail = 0; } u16 ring_buf_count(uart_ring_buf_t *ring) { return (ring->head - ring->tail + UART_RX_BUF_SIZE) % UART_RX_BUF_SIZE; } u8 ring_buf_read(uart_ring_buf_t *ring) { u8 ch = ring->buf[ring->tail]; ring->tail = (ring->tail + 1) % UART_RX_BUF_SIZE; return ch; } void ring_buf_write(uart_ring_buf_t *ring, u8 ch) { ring->buf[ring->head] = ch; ring->head = (ring->head + 1) % UART_RX_BUF_SIZE; }

串口接收中断服务程序里,把收到的字节写入环形缓冲区:

void uart_rx_isr(void) { u8 ch = uart_getc(); /* 缓冲区满时覆盖最旧数据,也就是丢弃最旧数据,保留最新 */ if (ring_buf_count(&g_rx_ring) < UART_RX_BUF_SIZE - 1) { ring_buf_write(&g_rx_ring, ch); } }

主循环里取数据的逻辑也很简单:

void uart_poll_process(void) { while (ring_buf_count(&g_rx_ring) > 0) { u8 ch = ring_buf_read(&g_rx_ring); /* 处理接收到的字节,比如回显 */ uart_putc(ch); } }

注意:中断服务程序里绝对不能做耗时操作,比如往串口发数据、解析协议、调用printf。中断里只做“把数据搬进缓冲区”这件事,处理逻辑全部放到主循环,这能有效避免中断重入和长时间屏蔽其他中断的问题。

3.5 完整代码工程整合

把上面几块拼到一起,一个能跑的串口echo程序就成型了。主程序里初始化完串口之后,在主循环不断调用uart_poll_process()处理接收数据,同时每隔一秒用printf打一条计数日志。SDK工程里还会有其它外设初始化函数,比如时钟初始化、音频初始化等,保留下来即可,核心就是串口部分。

4. 数据收发完整流程与验证

4.1 编译烧录与串口助手配置

代码写好之后,先编译。编译过程中最常遇到的问题就是函数名不一致,比如你看到SDK里的收发接口叫uart_write,而我上面示例用的是uart_putc,这种情况对照uart.h头文件做一次批量替换就好。

烧录步骤如下:

  1. 按住板子上标记有“DOWNLOAD”或“BOOT”字样的按键,保持按住。
  2. 用USB线连接板子到PC。
  3. 松开按键,此时芯片进入烧录模式。
  4. 打开烧录工具,选择编译生成的固件文件,点击开始下载。

烧录完成后,按一下板子上的复位按键或重新上电,程序开始运行。打开串口助手,配置参数如下:

  • 波特率:115200
  • 数据位:8
  • 停止位:1
  • 校验位:None
  • 流控:None

如果串口助手界面上持续出现计数器递增的日志,说明PC端到芯片的链路已经通了,发送方向工作正常。接下来最关键的验证是回环测试。

4.2 回环测试:验证双向通信链路

回环测试的目的很简单:从PC端串口助手发送一串字符,看AC63能不能完整地收回来。如果你用的是我上面的代码,AC63收到什么就会通过uart_putc原样返回什么。

具体操作:

  1. 在串口助手的发送输入框里填入Hello AC63 UART Test。
  2. 点击发送。
  3. 观察接收区,如果显示同样的字符串,说明接收中断、环形缓冲区、发送通道全部工作正常。

如果发送后没有回显,按以下顺序排查:

  • 确认波特率一致:PC端串口助手和MCU初始化参数必须完全一致,多一个零少一个零都会变成乱码或完全没反应。
  • 确认TX/RX没有接反:用万用表量一下MCU的TX引脚电压,空闲状态下应该有高电平,约等于VDDIO电压。
  • 确认中断服务函数真的被调用了:在中断里加一个计数器,打印出来看是否有递增。

回环测试通过后,基本可以断定串口硬件链路和驱动逻辑都是正确的,可以进入下一阶段了。

4.3 典型应用场景:日志打印与命令交互

回环测试只是练手,实际项目里串口用得最多的是两块:一是系统日志打印,二是命令行交互调试。

日志打印的核心价值在于在代码里埋点。比如蓝牙连接状态机,每个状态切换都打一条带时间戳的日志:

printf("[tick %d] BT conn state -> %s\r\n", get_tick(), conn_state_name(state));

这些日志可以在问题复现时第一时间定位是链路层、协议栈还是应用层的问题。

命令行交互则更高级一点,它把串口变成一个可以操作系统的终端。实现思路是先定义一个命令表:

typedef struct { const char *cmd; void (*handler)(char *args); } cmd_entry_t; static void cmd_help(char *args); static void cmd_get_ver(char *args); static void cmd_set_gain(char *args); static const cmd_entry_t cmd_table[] = { {"help", cmd_help}, {"version", cmd_get_ver}, {"set_gain", cmd_set_gain}, };

串口收到一行以\r\n结尾的字符串后,先按空格拆出命令词,再在命令表里线性查找对应处理函数,找到就调用并传入剩余参数,找不到就打印“unknown command”。这样调试音频增益、开关外设、读取寄存器,都可以直接在串口里敲命令完成,效率比重新编译烧录固件高一个数量级。

我甚至有段时间在连蓝牙一直失败时,通过在串口命令里增加一个“重置协议栈”操作,省去了反复插拔板子电源的麻烦。

5. 常见问题与排查技巧实录

5.1 串口乱码:四种常见原因对照表

乱码是串口调试里最让人抓狂的问题,但原因其实就那么几类,挨个排查就好。

现象原因解决方案
输出全部是“0F 0F”或特殊字符波特率不匹配核对PC端波特率与MCU初始化是否一致
内容正确但首字符乱码电平不稳定或上电瞬间TX被拉低加一个10k上拉电阻,或检查硬件设计
正常输出一段时间后乱码晶振或内部RC时钟漂移检查主时钟配置,确认串口时钟源选择正确
只有个别字节乱码线缆过长或干扰严重缩短杜邦线,避开电源线,降低波特率

其中,时钟源导致的乱码最容易误导人。AC63支持多种时钟源,如果你初始化串口时选择的时钟源和实际UART外设使用的时钟源不一致,波特率就会偏差,且偏差可能随着温度变化而漂移,表现为“时而正常时而乱码”。碰到这种问题,建议先固定使用内部高速RC或外部晶振,只改参数对比测试。

5.2 完全收不到数据:从硬件到软件逐层定位

数据一条都收不到,一般是硬件层面的问题,但也有可能是中断没配置对。

我的排查顺序是这样的:

  1. 看空闲电平:用万用表量MCU的TX引脚,空闲状态应该是高电平(3.3V左右)。如果为低电平或0V,怀疑引脚配置被改成普通GPIO且输出低电平,或者芯片没正常跑起来。
  2. 看USB转TTL模块的接收指示灯:PC发送数据时,模块上的RX指示灯会闪烁。如果不闪,先看PC端串口助手发送的是否真是当前选中的串口;如果闪了但MCU没反应,问题大概率在TX/RX接反或者共地缺失。
  3. 看中断配置:如果硬件检查都正常,再怀疑软件。在uart_rx_isr中加一个全局变量g_rx_irq_cnt,每进一次中断自增一次,主循环里把这个值打印出来,观察是否在增长。如果不增长,说明中断根本没触发,回头检查中断初始化函数是否真的调用了。
  4. 看GPIO复用:这一步很容易忽略。有些SDK模板工程里默认把某些引脚初始化为GPIO模式控制LED,如果你的UART_RX引脚恰好和LED引脚复用,而LED初始化代码在UART初始化之后执行,那引脚模式会被覆盖,数据自然收不到。解决办法是把LED初始化安排在UART之后,或者干脆把LED改成别的引脚。

5.3 丢数据与死机问题:中断上下文检查清单

数据丢包或程序跑飞,绝大多数情况都和中断上下文有关。我自己遇到过一次典型的丢数据问题:串口接收中断正常工作,但一旦蓝牙开始传输音频,串口就丢字节。后来定位发现,是音频相关的高优先级中断长时间占用CPU,导致UART接收中断无法及时响应,接收FIFO溢出。

解决思路有几个方向:

  • 提高UART接收中断优先级,确保它能在高负载下也能及时响应。
  • 在UART中断里只做最快的搬移操作,任何多余代码都不要写。
  • 增加接收FIFO触发阈值,利用硬件FIFO做缓冲,SDK一般支持设置不同的FIFO触发水平。

死机问题则更多与栈溢出和中断重入有关。可以这样自查:

  • 检查是否有代码在UART中断里调用了printf或uart_send_string。发送函数内部如果有等待发送FIFO空的逻辑,进入等待后如果又有新数据触发中断,可能造成重入,严重时直接栈溢出。
  • 检查中断里是否有动态内存分配,比如malloc,这类操作在中断上下文里容易破坏堆结构。
  • 检查含不可重入设计的函数,比如一些状态机结构,是否在中断和主循环中同时操作了同一份数据而没有加保护。

5.4 排查经验分享:快速定位串口问题的三板斧

调试串口问题时,我习惯用“三板斧”快速缩小范围:

第一板斧:先用示波器或逻辑分析仪看波形。如果MCU发出了正确的UART波形,说明MCU侧发送没问题;如果波形完全安静,问题在MCU软件侧。

第二板斧:写一个极简测试程序,只测试UART不涉及其它外设。很多问题时隔了其它外设才暴露,简化程序后能快速判定问题是不是由串口本身引起。

第三板斧:把上层协议全部拿掉,直接做echo测试。无论你最终要跑多复杂的协议栈,echo通过是第一步。echo通了,再逐步加逻辑,每加一层验证一次,问题就出在哪一层一目了然。

6. 一些实战细节与后续扩展建议

6.1 打印风格与log开关策略

串口打通之后,打印格式的设计直接影响后续调试效率。我习惯在一开始就定好日志格式:时间戳、模块名、事件名、关键参数,统一用文本输出。比如:

[bluetooth] [conn] state=connected addr=00:11:22:33:44:55 rssi=-45

同时,用编译宏控制日志等级,平时只开INFO和WARNING,排查问题时把DEBUG也打开,问题解决了再关掉。这样可以避免大量日志拖慢系统实时性,也方便量产版本保留最小化日志输出。

6.2 波特率与DMA进一步优化

如果你后续处理的业务对串口吞吐量有更高要求,比如通过串口升级固件、透传大量音频数据,可以考虑使用DMA方式发送和接收。DMA接收在AC63这类芯片上配置略微复杂,需要处理空闲中断和不定长数据帧,但换来的是极低的CPU占用率。我自己会在串口固件升级场景中用DMA,平时调试日志则继续用中断+轮询,兼顾稳定性和效率。

波特率方面,高频通信建议使用57600或115200,如果链路质量好、线短,也可以尝试460800。但注意,一旦把波特率调高,要重新验证长时间稳定性和抗干扰能力,不能只测成功一次就草率上线。

6.3 把串口调试方法论复制到其它平台

这次AC63串口折腾下来,最大的收获其实不是代码本身,而是调试方法论。后来我再接触其它国产SoC平台,比如一些Wi-Fi模组、MCU,遇到串口问题,思路都完全一致:先查电平,再查接线,再查时钟与复用配置,最后查中断和缓冲区。这套打法几乎通吃所有UART调试场景。

最后分享一个小技巧:把串口接收缓冲区的使用率打出来。在环形缓冲区写入和读出的代码里各加一个最大使用水位的统计指标,在日志里周期性输出。如果缓冲区平时只用了十几字节,说明整个链路很健康;如果动不动就飙到接近上限,说明处理速度跟不上了,这时候就该考虑中断优化或DMA了。这个指标很不起眼,但能帮你提前发现很多潜在风险,强烈建议保留在代码里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询