蓝牙模块与STC单片机串口透传实战:电平匹配、AT指令与配对调试
简介基于STC单片机的蓝牙通信示例工程包面向正在学习主从模块开发的嵌入式初学者以经过实测验证的例程展示如何借助UART串口实现无线数据收发。压缩包共21个文件以C源码、头文件、工程配置文件为主包含编译生成的目标文件、列表文件以及可直接烧录的HEX文件整体仅29KB便于对照学习或移植。目前已有394人学习下载。通过研究该工程读者可以掌握STC单片机I/O口配置、定时器与中断处理等基础操作理解蓝牙协议栈及主从设备连接管理机制并学会利用蓝牙调试助手进行无线联调。工程中的system.c、Uart.c等模块化代码清晰划分了初始化、数据收发与错误处理流程能帮助开发者快速搭建蓝牙通信应用提升嵌入式系统开发能力。1. 蓝牙模块与STC单片机把串口透传变无线入门先解决三个前提手机连上蓝牙模块串口助手收到的却全是乱码这种问题我调试时遇到过不止一次。蓝牙主从模块接STC单片机原理上就是把单片机串口的TXD和RXD换成无线链路省掉两根物理线代价是要同时满足三个前提电平匹配、波特率一致、主从关系正确缺一个就翻车。这篇笔记适合手头有STC开发板、想用蓝牙做无线调试、遥控小车或者传感器数据回传的人也适合第一次接触主从配对、被AT指令搞懵的同学。顺序很关键先选对模块、接对线再写程序最后配置AT指令整个链路就能跑通。2. 接线与选型主从模块通电前先解决三件事蓝牙模块的坑大多不是在程序里而是在通电之前就埋下了。很多同学拿到模块就往开发板上插结果不是模块不响应就是烧坏引脚。这一章先把选型、电平、接线的常见做法讲清楚写程序之前心里就有底。2.1 主从一体和从机模块怎么选市面上常见的蓝牙串口模块分两类。一类是从机模块上电后只能被别的蓝牙设备搜索和连接典型场景是手机连它模块没有按键AT指令支持很少波特率往往是出厂写死的。另一类是主从一体模块既能做从机也能做主机主动发起连接还支持配对、回环测试这类调试功能模块上通常有一个按键或者EN引脚。怎么选取决于你的实际场景。只是手机蓝牙连单片机比如做一个蓝牙台灯、蓝牙锁从机模块就够用成本低、配置少。如果是两个单片机之间无线通信比如遥控小车、双机数据透传那就必须买主从一体模块因为其中一边要作为主机主动去连另一边。我一般建议初学者直接买主从一体的模块一个模块既能当从机连手机也能当主机连另一个模块调试范围大很多不会出现“模块买错了还得重新下单”的尴尬。注意从机模块和主从一体模块的AT指令集不完全通用网上教程大多讲的是主从一体的指令如果手里是从机模块很多指令会返回ERROR。拿到模块先看丝印和说明书确认是哪种类型再动手。2.2 电平匹配5V和3.3V之间别直连STC单片机绝大多数是5V供电而蓝牙模块的芯片是3.3V电平。把STC的TXD直接接到蓝牙模块的RXD5V高电平会长期超过模块引脚的耐压范围短时间能工作时间一长就可能出现模块发热、随机重启甚至烧毁。反过来蓝牙模块TXD输出的3.3V高电平STC的RXD通常能识别为高电平但边缘情况比较多不建议赌。常见做法是电阻分压在STC的TXD到模块RXD之间串一个1kΩ电阻同时在模块RXD对地接一个2kΩ电阻这样分压后大约3.3V简单可靠成本低。供电方面模块VCC最好接独立的3.3V稳压输出如果板上只有5V要确认模块是否标注支持5V供电。多数主从一体模块的板载稳压芯片能承受5V输入但直接从5V引脚取电时建议在模块VCC和GND之间加一个100μF电解电容防止电机或继电器动作时电压跌落导致蓝牙掉线。提示模块的GND和单片机的GND必须共地否则串口信号没有参考电平表现就是数据完全乱码。2.3 接线端子与上电顺序以常用的主从一体模块为例引脚一般有VCC、GND、TXD、RXD、KEY或EN、STATE。与STC的接线对应关系如下STC引脚蓝牙模块引脚说明RXDTXD蓝牙发单片机收可直连TXDRXD单片机发蓝牙收需分压5V或3.3VVCC按模块标称供电GNDGND共地必须任意IOSTATE连接状态输出可选空或接高电平KEY/EN进入AT模式时接高上电顺序也有讲究。先给模块上电等LED状态稳定再给STC上电或者再打开串口调试助手。如果STC和模块同时上电模块初始化期间的引脚抖动可能被单片机当成串口数据接收产生一个脏字节。这个字节平时没啥影响但如果你做了严格的帧协议帧头校验对不上就会觉得“怎么一开机就收到了一个乱码帧”。2.4 LED灯的四种状态蓝牙模块的LED状态灯是最好的调试信号比任何调试助手都直观。不同模块颜色和闪烁频率略有差异但规律基本一致上电后若LED慢闪间隔大约2秒一次说明模块处于AT命令模式或者等待配对状态LED快速闪烁说明模块处于可被搜索状态LED常亮说明已经建立连接。还有一种情况是LED不亮也不闪先查供电再查模块是否损坏。我自己的习惯是每次上电先看LED状态再决定下一步动作。如果读到的状态和预期不符比如想进AT模式却看到常亮说明模块已经连上了某个设备此时发AT指令不会得到响应。这时候不是程序的问题而是模块状态不对先断开连接再操作。3. 串口程序让STC用9600波特率和蓝牙模块对话蓝牙模块对单片机来说就是一个外部串口设备单片机侧的程序核心就是把串口初始化成和模块一致的波特率然后处理收发。绝大多数模块出厂默认波特率是9600校验位无停止位1。这一章以STC89C52为例把串口配置和收发代码拆开讲。3.1 串口初始化定时器1的重装值怎么算STC89C52的串口工作在模式18位UART波特率由定时器1溢出决定。选择晶振很关键11.0592MHz是经典值因为它在9600波特率下误差为0。如果你的板子上是12MHz晶振9600波特率会有约2%的误差短距离透传勉强能用但连续传大量数据时出错率会明显上升。void Uart1_Init(void) { SCON 0x50; // 串口模式1REN1使能接收 TMOD 0x0F; // 保留定时器0的原配置 TMOD | 0x20; // 定时器1模式28位自动重装 TH1 0xFD; // 波特率重装值11.0592MHz时为9600 TL1 0xFD; // 初次装入值和重装值一致 PCON 0x7F; // SMOD0波特率不加倍 TR1 1; // 启动定时器1 ES 1; // 使能串口中断 EA 1; // 使能总中断 }波特率计算公式是波特率 晶振频率 / (12 × (256 − 重装值) × 32)。代入11.0592MHz和重装值0xFD也就是256 − 253 3得到11059200 / (12 × 3 × 32) 9600。这个32是模式1特有的分频系数不是16很多资料写错照着算出来的重装值会对不上。如果你把SMOD置1分频系数变成16同样重装值下波特率会翻倍到19200所以初始化里显式把SMOD清零避免后续踩坑。3.2 发送与接收轮询发、中断收发送和接收可以采用不同策略发送用轮询接收用中断。发送频率由主循环控制轮询不会阻塞太久而接收是异步的数据随时可能到中断处理最及时。void Uart1_SendByte(unsigned char dat) { SBUF dat; // 写入SBUF立即开始发送 while (!TI); // 等待发送完成标志置位 TI 0; // 软件清除发送完成标志 } void Uart1_ISR(void) interrupt 4 { unsigned char dat; if (RI) // 接收中断标志 { RI 0; // 软件清除接收标志 dat SBUF; // 读取接收数据 if (rx_len RX_BUF_MAX) { rx_buf[rx_len] dat; // 存入接收缓冲 } } if (TI) // 发送中断标志轮询发送时也清掉 { TI 0; } }注意SBUF这个特殊功能寄存器读它得到接收数据写它触发发送虽然地址相同但是两个独立物理行为。中断里必须把数据从SBUF取走否则下一个字节进来会覆盖当前值。接收缓冲定义成全局数组rx_bufrx_len记录长度主循环轮询这个变量就能知道有没有新数据。RX_BUF_MAX一般设成16或32就够透传场景下不推荐把缓冲开得太大因为缓冲越大丢帧时越难判断数据边界。3.3 中断里只搬数据不做解析初学者容易犯的错是在串口中断里做协议判断比如判断帧头、提取命令、甚至调用延时等待下一个字节。这在51这类低主频单片机上很危险中断服务程序执行时间过长下一个字节到达时RI已经置位但没来得及处理就会丢字节表现是“偶发性乱码”。我一般只让中断做一件事把SBUF的数据搬到一个数组里置一个标志就返回。所有的帧解析、命令分发全放主循环。这样中断耗时只有几微秒即使串口连续收发也不会丢数据。等数据到了主循环缓冲区里的内容随便折腾都安全。3.4 主循环从缓冲里取数据回显一个最简单的验证程序把收到字节原样加1再发回去用来确认波特率和收发通路没问题。void main(void) { unsigned char cmd; Uart1_Init(); while (1) { if (rx_len 0) { cmd rx_buf[0]; rx_len 0; Uart1_SendByte(cmd 1); } } }这段代码在只有单条数据时没问题但如果蓝牙发来的消息超过一个字节比如“AT”这种两个字符或者完整指令帧就必须按协议处理rx_len不然会丢前半截。真实项目里不会这么简单而是进入第6章讲的帧协议。先用回显程序确认通路再扩展协议是我每次调试串口设备的固定顺序。4. AT指令配置主从配对与透传验证的完整顺序程序烧好只能说明单片机和模块之间的硬件链路通了要让两个蓝牙模块互相通信还要通过AT指令配置角色、密码和连接关系。这一章把AT模式进入方式、常用指令和配对步骤完整走一遍。4.1 进入AT模式按键时序与波特率版本差异主从一体模块进入AT模式有两种常见做法。一种是把KEY或EN引脚接高电平再上电LED慢闪即表示进入AT模式另一种是模块板载按键按住按键再上电松开后进入AT模式。这里最大的坑是AT模式下的波特率不一定是9600。常见的主从一体模块出厂固件里AT模式波特率有两种版本一种固定9600另一种固定38400。网上教程多数写38400如果你的模块偏偏是9600串口助手发AT就没有回应。血泪经验是拿到模块先确认丝印或说明书上写的AT波特率或者两个波特率都试一遍发一条AT能收到OK的就是对的。试的时候记得勾选“加回车换行”AT指令的结尾必须是\r\n只发\r在很多固件里不会触发响应。进入AT模式后模块不会再作为蓝牙设备被搜索到所以如果手机扫不到模块先检查是不是不小心进了AT模式。4.2 常用AT指令角色、密码、波特率、连接地址下面这张表是主从一体模块最常用的指令不同固件细节略有差异但功能基本一致指令功能示例AT测试通信ATATRESTORE恢复出厂设置ATRESTOREATROLE0设为从机ATROLE0ATROLE1设为主机ATROLE1ATUART9600,0,0设置波特率、校验位、停止位ATUART9600,0,0ATPSWD1234设置配对密码ATPSWD1234ATCMODE0固定地址连接模式ATCMODE0ATADDR?查询模块自身地址ATADDR?ATLINK地址主机主动连接从机ATLINK0012,05,140501配置前建议先执行ATRESTORE恢复出厂避免前一个使用者留下奇怪配置。注意RESTORE执行后模块会重启重新进入AT模式之前要先断开电源再按上电时序重新操作不要指望一条指令马上生效。4.3 主从点对点连接的标准顺序两个模块配对一定先把从机配置好再配置主机顺序反了很容易出现主机先启动却找不到目标设备的情况。从机侧配置先恢复出厂再设角色为从机、波特率9600、密码1234。然后查询并记录从机地址比如ATADDR?返回0012,05,140501这个地址后面要用。从机配置完成后断电重启等待主机连接。主机侧配置恢复出厂设角色为主机固定地址连接模式密码和从机保持一致。然后向从机地址发起连接执行ATLINK0012,05,140501。如果返回OK查看两个模块LED是否变为常亮。常亮则配对成功。注意ATLINK的参数地址是完整字符串中间那两个逗号不能省略也不要加引号。有些固件在ATLINK之前还要求ATPAIR配对做法是先执行ATPAIR0012,05,140501,1010代表超时10秒返回OK后再执行ATLINK。如果你的模块返回ERROR先试这个两步流程。4.4 验证链路回环测试与两机互发配对成功后怎么确认链路真的通了我一般分两步验证。第一步是单模块回环测试。把模块的TXD和RXD用杜邦线短接手机蓝牙连上模块用串口助手发送一串字符手机端能收到同样的字符说明模块本身收发正常。这个测试不需要单片机参与能把问题范围缩小到模块自身。第二步是双机测试。将主机和从机分别通过USB转TTL接电脑的两个串口各开一个串口助手主机侧发送字符串从机侧能收到反向同样就是真正的点对点透传通了。这里容易出现的假象是串口助手里勾选了“本地回显”导致主机侧也显示自己发出的数据误以为数据发过去了。关闭回显功能再测收到数据的就一定是远端模块。两个模块都正常后再把从机接回STC单片机用第3章的串口程序做回显验证整个项目链路就完整了。5. 避坑蓝牙串口开发最常见的五个问题配置过程里反复出现的坑基本集中在这五个方向每一条都是实测踩过的现象、原因、解决方法一起列出来。5.1 电压直连导致模块异常现象模块上电能亮灯但连接后通信乱码或者用一段时间后模块发热、无法配置。原因STC的5V TXD直接接模块3.3V RXD长期过压导致模块内部电平转换电路工作异常甚至部分引脚损坏。解决在TXD到模块RXD之间加1kΩ和2kΩ分压电阻先分压再进模块。同时检查模块供电是否超过标称电压降压到3.3V后再给模块。这个改动成本不到一毛钱但能省下一个模块的钱。5.2 模块不响应任何AT指令现象串口助手发送AT模块无回应或者返回一堆乱码字符。原因最常见是AT模式波特率选错模块是9600固件却用38400发指令。其次是串口助手里没有勾选加回车换行模块一直没收到完整指令。还有一种情况是模块其实没进AT模式LED还处于快速闪烁的可连接状态。解决先看LED状态确认处于慢闪的AT模式。然后轮流试9600和38400两个波特率每次发AT加\r\n。如果两个波特率都没反应断电按住KEY重新上电再试一遍。5.3 连续发送丢字节现象手机连上模块发单个字符正常连续发一串字符串单片机收到的数据中间有缺字或者顺序错乱。原因中断服务程序里做了太多事情比如在中断里解析协议、调用延时导致下一个字节到达时数据被覆盖。也可能主循环读取rx_buf的速度跟不上串口接收速度。解决按第3章的做法中断里只把SBUF读到缓冲区所有解析放主循环。连续发送场景下还要考虑缓冲区长度一次发超过缓冲区长度的数据后面部分会被丢弃。可以把缓冲区加大或者采用覆盖旧数据的策略从最新数据开始解析。5.4 能搜到设备但配对不上现象手机能搜到从机模块输入密码1234后提示配对失败或者主机发送ATLINK一直ERROR。原因密码不一致是最常见的从机和主机虽然都设了PSWD但一个没生效一个生效了。另外CMODE被设成1也就是任意地址连接模式导致主机不断尝试连接附近其他蓝牙设备无法锁定目标。解决两个模块都执行ATRESTORE恢复出厂然后按第4章的顺序重新配置。先设从机密码再设主机密码保持完全一致。主机设置ATCMODE0固定地址模式用查询到的物理地址去连接而不是靠自动扫描。5.5 连接成功但数据不通现象两个模块LED都常亮看起来已经连接但A模块发数据B模块收不到或者只有一边通。原因透传模式下收发引脚接反A模块的TXD接到了B模块的TXD也有可能是接线时分压电阻加错了位置把蓝牙发出来的3.3V信号也分压了一遍导致STC识别不到高电平。解决重新核对连线A模块TXD接B模块RXDA模块RXD接B模块TXD。分压电阻只加在单片机TXD到模块RXD这条路径上模块TXD到单片机RXD方向不加电阻。用万用表量一下模块TXD空闲时的电压正常应该在3.3V左右如果接近0V先查模块是否进入透传状态。6. 进阶用帧协议和掉线自检提升透传可靠性裸透传只能保证字节能过去不能保证数据完整和链路可用。真实项目里无线串口随时可能被干扰、被遮挡所以我在第3章的收发代码上加了帧协议和掉线检测这套组合在多个模拟项目中验证过数据完整性明显提升。6.1 设计一个带长度和校验的帧协议帧格式定为帧头0xAA 长度字节 数据域 校验字节。长度字节表示数据域长度校验字节是长度字节到数据域末尾的累加和不包含帧头。#define FRAME_HEAD 0xAA #define FRAME_MAX 32 unsigned char tx_buf[FRAME_MAX]; unsigned char Uart1_MakeFrame(unsigned char cmd, unsigned char *data, unsigned char len) { unsigned char i; unsigned char sum 0; tx_buf[0] FRAME_HEAD; tx_buf[1] len 1; // 长度域 命令字节1 数据长度 tx_buf[2] cmd; for (i 0; i len; i) { tx_buf[3 i] data[i]; sum data[i]; } sum cmd; // 校验长度域和命令 tx_buf[2 len 1] sum 0xFF; return len 4; // 返回整帧长度 }长度域用len1目的是把cmd算进数据范围校验也覆盖cmd和data。接收方解析时先读长度域就知道后面要收多少个字节避免用结束符判断导致数据内容里出现0xAA就误判帧头。把数据发出来之前我先在串口助手里勾选HEX显示核对帧尾确认协议没有自相矛盾。6.2 状态机逐字节解析裸透传和帧协议之间最大的区别在接收端。裸接收收一字节回一字节帧协议需要按状态逐字节判断当前处于哪个阶段。unsigned char rx_state 0; unsigned char rx_buf[FRAME_MAX]; unsigned char rx_cnt 0; unsigned char rx_data_len 0; void ParseByte(unsigned char dat) { unsigned char i; unsigned char sum 0; switch (rx_state) { case 0: if (dat FRAME_HEAD) { rx_state 1; rx_cnt 0; } break; case 1: rx_data_len dat; rx_buf[rx_cnt] dat; if (rx_data_len 0 rx_data_len FRAME_MAX - 4) { rx_state 2; } else { rx_state 0; // 长度非法重新找帧头 } break; case 2: rx_buf[rx_cnt] dat; if (rx_cnt rx_data_len 2) // 2长度字节和校验字节 { for (i 1; i rx_cnt - 1; i) { sum rx_buf[i]; } if (sum rx_buf[rx_cnt - 1]) { // 校验通过rx_buf[2]是命令后面是数据 } rx_state 0; // 无论是否合法回到找帧头 } break; } }状态机的好处是即使中间收到干扰字节也会在长度校验或累加和校验失败后自动回到帧头搜索状态不会卡死。我在主循环里对rx_buf里的每一个字节调用ParseByte相当于把串口数据流逐字节喂给状态机。这样干扰帧只会被丢弃不会污染下一条正常帧。6.3 用STATE引脚做掉线检测和自动重连STATE引脚在模块连接成功后输出高电平断开时输出低电平。把它接到STC的一个普通IO口就可以用软件检测链路状态。这里注意STATE输出是3.3VSTC的IO识别这个电压没问题如果担心边界情况可以再加一个1k限流电阻。sbit BT_STATE_PIN P3^3; bit link_ok_flag 0; unsigned char link_idle_cnt 0; void CheckBtLink(void) // 每200ms调用一次 { if (BT_STATE_PIN) { if (!link_ok_flag) { link_ok_flag 1; // 刚建立连接 link_idle_cnt 0; } } else { if (link_ok_flag) { link_ok_flag 0; // 刚断开 } if (link_idle_cnt 10) // 超过2秒未恢复 { link_idle_cnt 0; Uart1_SendString(ATLINK0012,05,140501\r\n); } } }掉线后重连不是越频繁越好AT指令本身要占用串口如果每200ms重发一次主从之间会持续冲突。我把超时阈值设在2秒既不会让用户觉得“彻底死掉”也不会干扰正在进行的透传。如果重连三次还没成功就置一个错误标志在主循环里触发蜂鸣器或指示灯提示。从那以后我每次写蓝牙串口程序都强制走一遍这个流程先确认模块类型和AT波特率再分压接线回显验证通路最后加帧协议和STATE掉线检测。这套流程让我从“玄学调蓝牙”变成了“按步排查”希望帮到你。本文还有配套的精品资源点击获取