ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

杰理AC63串口通信实战:从引脚复用到环形缓冲设计

杰理AC63串口通信实战:从引脚复用到环形缓冲设计 做杰理AC63项目的串口通信最坑的从来不是UART协议本身而是你把代码写完、板子接好却发现串口调试助手上面要么一片空白要么全是乱码。很多刚接触杰理平台的开发者沿用STM32那套思路在AC63上写串口结果连日志都打不出来。这篇文章记录的是我从零开始在AC63核心板上打通串口通信、实现数据双向收发的完整过程包括SDK初始化、引脚复用、中断接收、环形缓冲区设计和实测踩坑关键代码段会逐一拆开讲。适合手里有AC63开发板、想做蓝牙透传或者调试日志输出但被官方SDK绕得头晕的人参考。1. AC63的串口资源与硬件接线先解决“线”的问题1.1 AC63的UART外设到底有几个能干什么AC63系列是杰理面向低功耗蓝牙音频市场的一颗SoC常见的如AC630N、AC631N、AC632N集成蓝牙双模协议栈CPU用的RISC-V内核Flash和RAM都比较紧凑。正因为资源不算宽裕串口这个看似简单的模块在AC63上反而需要仔细规划。一般AC63会提供至少两路UART资源一路经常被SDK用作日志输出口另外一路可以留给业务逻辑做数据通信。拿我手头的这颗AC6328S为例它在GPIO上同时映射了UART0和UART1支持常见的8N1格式波特率可以配置到最高1Mbps以上具体上限要看系统时钟和分频参数手册里一般会给出最大波特率表格。注意不是所有GPIO都能复用成UART引脚每个引脚的可复用功能是固定的接错引脚、复用了错误的功能编号代码怎么调都白搭。我建议拿到SDK的第一步不要急着写应用先去SDK的gpio.h和uart.h头文件里翻一下引脚复用表确认你板子上实际引出的TX和RX分别属于哪组UART。很多开发板的丝印标注和SDK默认的log口不一致先确认这一点能省掉后面两个小时。1.2 硬件接线电平、共地、交叉连接缺一不可串口通信的硬件接线看起来简单实际上有三个非常容易忽略的点电平匹配AC63的UART引脚是3.3V电平如果你的USB转串口模块不是3.3V供电而是5V供电尤其某些便宜模块在5V模式下TX引脚输出高电平接近5V长期使用可能损伤AC63引脚。优选支持3.3V电平的模块或者确认模块的VCC跳线帽在3.3V档位。TX接RX、RX接TX必须交叉连接。开发板上的UART_TX要接转换模块的RXUART_RX要接模块的TX。反了的表现通常是你发数据它没反应但模块的TX/RX灯还在闪。共地开发板和USB转串口模块必须共地GND不接收发大概率是乱码或者完全没数据。这个新手经常漏。我实测中比较稳的连接方式是AC63核心板通过板载的USB转串口如果有直接连电脑省去外部接线如果是裸板用CP2102或CH340G模块接VCC、GND、TX、RX四个脚把串口助手波特率设为115200其余默认。这么接完硬件部分就算通了。2. 开发环境与最小工程先让串口“开口说话”2.1 工具链准备JieLi IDE、SDK包和编译链杰理AC63的开发环境用的是官方提供的JieLi IDE基于Eclipse二次开发内置了RISC-V交叉编译链和烧录工具。也有部分工程直接用Makefile配合命令行编译视SDK版本而定。我第一次用的时候走了一点弯路以为跟STM32一样装个Keil就能搞结果AC63的SDK根本不支持Keil老老实实装了官方IDE。工具链清单大致是JieLi IDE对应你芯片型号的版本AC63xx SDK包建议用和芯片型号匹配的release版本USB下载/调试线通常是USB转SPI或者专用烧录器看开发板配置USB转串口模块或板载串口SDK解压后目录结构一般包含apps、include、src、ld等应用代码主要在apps目录下。不同版本的目录名略有差异但大体逻辑一致。建议不要直接用空工程从零开始而是拷贝SDK里最接近的demo工程改因为AC63的工程依赖很多系统配置和链接脚本从零手写很容易漏配置。2.2 改log口最快验证串口通路的方式先把SDK自带的日志功能打开。AC63的log口本质上也是一个UARTSDK会初始化一个调试串口来输出printf内容。如果你板子上的log口引脚和SDK默认配置不一致就需要改引脚复用。拿我用的SDK版本为例日志串口的初始化逻辑大致这样// log_init 内部会配置uart引脚和波特率 // 不同SDK版本函数名不一样常见的形式是 void log_init(void) { uart_init(LOG_UART_ID, 115200); // 重定向printf到该uart }改完引脚和波特率后编译烧录如果开发板上有LED周期性闪烁或者某些启动打印串口助手能看到日志说明log通路已经通了。到这里串口“开口说话”的第一步就算完成。如果log口怎么都出不来按这个顺序排查串口助手波特率是否和代码里一致别115200和9600来回试了半天。USB转串口的驱动是否正常打开设备管理器看COM口号。引脚复用是否配置正确查SDK的gpio复用表。确认开发板供电正常AC63进入运行状态。这个阶段不需要写任何业务代码纯粹验证环境但很多人就在这一步卡住所以别跳过。3. 串口数据收发的完整实现发送、接收与环形缓冲3.1 初始化流程时钟、GPIO复用、UART参数、中断注册业务UART不能跟log口冲突。如果你用UART0做log那么业务数据就放到UART1或者反过来。下面的初始化代码以UART1为例按“开时钟、配引脚、设参数、挂中断”四步走。#include uart.h #include gpio.h #define BUSSINESS_UART_ID 1 #define UART1_TX_PIN GPIO_PIN_2 #define UART1_RX_PIN GPIO_PIN_3 void app_uart1_init(uint32_t baud) { // 1. 使能UART1时钟部分SDK在uart_init里已经做了可省略 uart_clk_enable(BUSSINESS_UART_ID); // 2. 配置TX、RX引脚复用 gpio_set_fun(UART1_TX_PIN, GPIO_FUN_UART1_TX); gpio_set_fun(UART1_RX_PIN, GPIO_FUN_UART1_RX); // 有些板子需要额外配置上下拉RX建议加上拉避免悬空误触发 gpio_set_pull_up(UART1_RX_PIN, 1); // 3. 设置波特率、数据位、停止位、校验位 uart_init(BUSSINESS_UART_ID, baud); // 默认8N1 uart_set_baudrate(BUSSINESS_UART_ID, baud); // 4. 注册接收中断回调中断里只做数据搬运不要做耗时处理 uart_set_rx_callback(BUSSINESS_UART_ID, uart1_rx_handler); uart_irq_enable(BUSSINESS_UART_ID); }这段代码的重点在于操作顺序。有些开发者习惯先初始化UART再配置GPIO这在某些MCU上没问题但是AC63的引脚复用优先级较高如果复用没配置好UART外设发出的信号根本到不了引脚上。另外RX引脚加上拉是实战经验悬空的RX容易被环境噪声干扰产生一连串0x00、0xFF之类的伪数据。3.2 发送路径阻塞发送与数据搬运的取舍发送最简单的方式是轮询查询发送寄存器空闲然后一字节一字节往外丢。在AC63这种资源不算充裕的平台上如果只是发少量调试信息这种方式完全够用代码也直观void uart1_send_byte(uint8_t data) { while (uart_tx_busy(BUSSINESS_UART_ID)); // 等待发送寄存器空闲 uart_write_byte(BUSSINESS_UART_ID, data); } void uart1_send_buf(uint8_t *buf, uint16_t len) { for (uint16_t i 0; i len; i) { uart1_send_byte(buf[i]); } }这里必须解释一个我实际踩过的坑不要在主循环里频繁调用这种阻塞发送去发送大块数据。原因是UART波特率决定了字节发送速度115200波特率下每秒最多约11.5KB发送1KB数据需要差不多100ms期间CPU全部空转在while循环里。如果你的主循环还要处理蓝牙协议栈这100ms的阻塞会导致协议栈喂狗超时、连接断开等问题。所以实际项目中如果数据量稍大我更推荐做一个简单的发送缓冲队列把待发送的数据放进队列然后在UART发送完成中断里逐个取出发送让CPU在等待期间可以干别的事。3.3 接收路径为什么必须用环形缓冲区接收是串口开发里最容易出bug的地方。如果只用单个字节变量接数据只处理一个字节还好但一旦对端发来一帧几十字节的数据单字节方案要么丢字节要么忙不过来。经典解法是环形缓冲区Ring Buffer。它的思路说白了就是一块固定大小的内存用两个指针分别记录“读到哪里”和“写到哪里”写指针由中断服务函数推进读指针由主循环业务代码推进二者互不干扰。当写指针追到读指针时说明缓冲区满了此时根据策略可以丢弃旧数据或丢弃新数据。#define RX_BUF_SIZE 512 static uint8_t rx_ring_buf[RX_BUF_SIZE]; static volatile uint16_t rx_head 0; // 写指针中断里更新 static volatile uint16_t rx_tail 0; // 读指针主循环里更新 static uint16_t rx_ring_count(void) { return (uint16_t)(rx_head - rx_tail); } // UART接收中断回调中断上下文尽量精简 void uart1_rx_handler(void *priv) { uint8_t data; while (uart_read_byte(BUSSINESS_UART_ID, data) 0) { // 读成功返回0 uint16_t next (uint16_t)((rx_head 1) % RX_BUF_SIZE); if (next ! rx_tail) { // 缓冲区未满 rx_ring_buf[rx_head] data; rx_head next; } // 满了就丢实际项目可以置溢出标志 } } // 主循环/业务逻辑调用取走一个字节 uint8_t uart1_get_char(uint8_t *byte) { if (rx_head rx_tail) { return 1; // 无数据 } *byte rx_ring_buf[rx_tail]; rx_tail (uint16_t)((rx_tail 1) % RX_BUF_SIZE); return 0; }这段代码里要注意三点中断里不要做耗时的数据处理比如解析协议、回调业务逻辑。中断里干得越少越好数据先放到缓冲区业务逻辑在主循环里处理。我在最初版本里直接把协议解析写进了中断结果波特率高的时候中断时间过长直接影响蓝牙协议栈时序。读写指针用了volatile因为读指针在主循环里改写指针在中断里改编译器优化时可能把频繁访问的变量优化到寄存器里导致一边看不到另一边的更新。volatile能防止这种问题。取模操作(rx_head 1) % RX_BUF_SIZE在缓冲区大小为2的幂时可以改成位运算(rx_head 1) (RX_BUF_SIZE - 1)效率高一些。AC63的主频不高能在中断里省一点是一点。接收中断回调在SDK里不同的版本名字不一样有的叫uart_rx_cb有的需要自己在中断向量里注册但本质都是“有数据来了就调这个函数”。你可以照着把手里的SDK对应函数名替换进去。3.4 Echo回环测试验证数据通路是否完整初始化好UART写完收发函数我建议先做一个最简单的echo测试串口发什么AC63就回什么。这样只要串口助手里能看到自己发的内容原样返回整条数据链路就是通的。void uart1_task(void) { uint8_t ch; if (uart1_get_char(ch) 0) { uart1_send_byte(ch); // 原样回显 } }把这个任务放进主循环波特率设置为115200串口助手里输入“ABC123”正常回显“ABC123”。如果回显出现丢字符、多字符、乱码就进入下一步排查。4. 参数调整、乱码排查与信号验证实测联调高频踩坑这一节是真正的实战部分。我把联调过程中遇到的高频问题按现象分类用表格列出来再逐个说排查思路。这些问题在AC63上我都实际遇到过包括一些折腾了半天的。现象可能原因排查方式完全无数据TX/RX灯不闪引脚复用错误、接线交叉错、串口未初始化检查GPIO复用表确认TX接对方RX示波器量引脚电平完全无数据但TX/RX灯闪波特率不匹配、芯片处于低功耗模式串口助手换常见波特率逐一试确认系统没有进入睡眠出现连续乱码波特率偏差过大、地线没接、外部晶振频率和SDK配置不一致用示波器量单字节发送波形计算实际波特率能收不能发TX引脚复用没配置、发送阻塞在busy判断上量TX引脚是否正常翻转检查发送中断是否误占用能发不能收RX引脚没配复用、接收中断未使能、回调函数未注册检查RX引脚复用确认回调注册执行了丢字节或偶发乱码中断里耗时太长、缓冲区太小、FIFO溢出缩短中断处理逻辑加大环形缓冲区开启UART FIFO前确认溢出标志4.1 乱码问题的根因不仅仅是波特率乱码是串口开发里最常见的问题。很多人第一反应是波特率不对但实际上AC63平台上乱码还有一个根因容易被忽略——外部晶振频率和SDK系统时钟配置不匹配。AC63的UART波特率是由系统时钟分频得到的如果SDK配置的系统时钟是96MHz但板子实际焊的是24MHz晶振或者反过来UART产生的波特率和设置值会有明显偏差。对于115200波特率如果分频误差超过2%就会出现间歇性乱码严重时全乱。我遇到过一次比较棘手的情况板子用外部24MHz晶振但SDK的时钟初始化代码默认开了内部高频RC或者锁相环倍频导致串口输出字符中前几个字节能识别、后半段乱码。后来用示波器测量发现UART TX引脚的单bit宽度比理论值窄了约5%确认是时钟源配置问题。所以排查乱码的顺序建议是先用示波器或逻辑分析仪抓取TX引脚波形测量一个起始位加8个数据位的时宽反推实际波特率。如果实际波特率和设定值偏差较大检查SDK的系统时钟初始化配置确认外部晶振、PLL倍频系数正确。如果波形正常仍然乱码再怀疑USB转串口模块本身或者换一个模块验证。检查两端的数据格式是否一致数据位8位还是9位、停止位1位还是2位、是否带校验。这个因素在AC63上同样重要两端配置不一致5%的机率乱码95%的机率完全读不出。4.2 用示波器给串口数据“验明正身”很多人手上没有示波器但排查串口问题最好的工具恰恰是示波器或逻辑分析仪。不说测高级协议只需要把探头夹在UART TX引脚和GND上让设备持续发送0x55二进制01010101这时波形应该是标准的方波。UART空闲时是高电平发送一个字节时先拉低一个bit宽度的起始位然后依次输出8个数据位最后拉高一个bit宽度的停止位。通过测量一个bit的宽度就可以算出实际波特率实际波特率 1 / 单个bit时间宽度假设设置115200波特率理论单bit时间约为8.68us。如果示波器上量出来是9.5us或者7.8us说明波特率已经偏差很大了这时候乱码是必然的。我一般会在产品联调阶段把下面的测试程序烧进去循环发送0x55和0xAA方便抓波形while (1) { uart1_send_byte(0x55); delay_ms(2); uart1_send_byte(0xAA); delay_ms(2); }0x55和0xAA作为交替的0101和1010能最快暴露波特率误差。波形每bit的宽度均匀且接近理论值硬件层就过关了。4.3 中断里绝对不能做的事串口接收中断的处理是丢数据的重灾区尤其AC63这种带蓝牙协议栈的SoC中断优先级和时间约束很敏感。我在排查一个丢帧问题时发现中断里做了一件事很耗时解析GPS协议并更新OLED显示。波特率9600下每帧数据约100字节中断里全部处理完要好几毫秒。结果就是蓝牙协议栈任务被挤占出现周期性卡顿同时串口缓冲区频繁溢出。正确做法是中断里只做“搬数据”把数据放进环形缓冲区。中断里只做状态机的最小一步比如更新一个标志位。协议解析、数据分析、显示刷新全部放到主循环、单独任务或者空闲回调里做。一句话总结中断是数据入口不是数据加工车间。另外一个容易被忽略的点是AC63的UART接收FIFO可能不止一级。如果SDK默认开启了FIFO且溢出中断没处理好数据在FIFO满的时候会直接丢弃。我建议初期调试先把UART FIFO关掉用裸中断环形缓冲测试等逻辑稳定后再开启FIFO提升吞吐。这样能把缓冲溢出问题隔离出来避免“缓冲区溢出”和“FIFO溢出”两个问题混在一起。5. 从“能用”到“好用”DMA、低功耗与帧协议思路串口收到数据、能发回去这只是第一步。实际产品中还要考虑传输效率、功耗和协议可靠性。下面这几个点我在AC63项目里都实践过简单分享一些思路。5.1 什么时候值得上DMAAC63的UART外设支持DMA搬运。DMA的好处是数据在内存和外设之间搬运不需要CPU干预适合大量、连续的数据收发。比如通过蓝牙把手机APP传过来的大文件转发给外部MCU这种情况用DMA能大幅降低CPU占用。但是DMA不是银弹。DMA需要固定的接收缓冲区对“不知道数据什么时候来、来多少字节”的串口接收场景处理起来反而麻烦要么用空闲中断配合DMA实现不定长接收要么定长接收后自己拆分。对于大部分数据量不超过几百字节的交互协议中断环形缓冲已经足够上DMA增加代码复杂度收益不明显。我建议的判断标准是波特率超过460800或者每秒钟收发数据量超过2KB再考虑DMA否则先用中断方案简单稳定。5.2 低功耗场景串口怎么既省电又不漏数据AC63是低功耗蓝牙SoC产品如果是电池供电串口就不能一直开着等数据。最常用的策略是平时不进睡眠让RX引脚保持高电平不使能UART接收中断通过外部中断监测RX引脚的下降沿起始位有下降沿说明对端开始发数据了这时唤醒内核、初始化UART、开始正常接收。或者使用UART的空闲中断在总线空闲一段时间后休眠但是空闲中断仍然需要UART时钟保持运行省电效果有限。一个务实建议是如果串口只在配对或者升级时使用低功耗模式下直接把UART关闭用GPIO外部中断唤醒后再初始化UART。这样最省电代价是从唤醒到UART初始化完成需要一点时间对端可能要重发一两个字节。5.3 分包与粘包一个简单的帧协议模板串口收发数据时如果对端发来的数据长度不定且数据间隔不稳定就会出现所谓的粘包和半包问题。产品级代码建议从一开始就定义帧协议不要裸发裸收。我常用的最简单帧格式是帧头(0xAA 0x55) 长度(1字节) 数据(N字节) 校验和(1字节)接收端的状态机按“找帧头 - 收长度 - 收数据 - 校验”推进把完整的一帧数据组装好再交给业务解析。这种协议虽然带了4字节额外开销但能解决绝大多数串口通信的边界问题。enum { STATE_WAIT_HDR1, // 等待第一帧头 STATE_WAIT_HDR2, // 等待第二帧头 STATE_WAIT_LEN, // 等待长度 STATE_DATA, // 接收数据 STATE_CHECK // 校验 };简单说就是中断把收到的字节喂给状态机状态机攒够一帧后置一个frame_ready标志主循环再去处理。用状态机的好处是逻辑清晰、可测试而且不会因为某次数据残缺而崩溃。5.4 当SDK的库函数是“黑盒”反汇编定位寄存器配置最后说一个老手才会用到的技巧。AC63的SDK有些底层驱动是以库文件形式提供的头文件里只暴露了函数声明源码却看不到。如果你想确认某个波特率下UART的寄存器配置是否正常可以把编译好的elf文件拖进反汇编工具找到对应的UART初始化函数看它往哪些寄存器写了什么值。比如用GDB或者riscv-objdump反汇编可以看到类似这样的片段li a0, 0x40002000 ; UART基地址寄存器 li a1, 0x2d ; 分频值表示对系统时钟的分频 sw a1, 0x0(a0) ; 写入波特率寄存器根据系统时钟和分频数就能反推实际波特率。这个方法在碰到“SDK配置了波特率但实测波形不对”的问题时非常有效能绕过黑盒直接验证底层配置。当然普通应用开发不需要走到这一步但如果你要做产品级稳定性排查这招值得掌握。关于AC63的串口通信我自己最深的体会是四个字先简后繁。不要一上来就上DMA、上复杂协议先把log打通做echo回环确认硬件的每一根线、每一个引脚复用都正确再逐步加中断、加缓冲区、加协议解析。串口这个模块技术本身不难难的是把环境、引脚、时钟、中断这四样东西同时搞对。踩过几次坑之后你会发现调试效率反而高了因为每次怀疑都是先查基础配置而不是凭空猜代码。
返回列表