STC8G1K08A串口通信实战:从Hello World到中断处理
手里拿到一颗STC8G1K08A想用串口给PC发个Hello World这事看着简单做起来却有一堆细节引脚选哪组、波特率怎么配、发送是轮询还是中断、接收又该在哪里处理。这篇文章打算按我自己实际调通的顺序把串口通信从零到中断处理整个过一遍顺便把Boot和App共存时中断向量怎么安排也讲清楚。适合刚接触STC8G1K08A的初学者也适合用8051内核做产品、想整理一套稳定串口代码的工程师。STC8G1K08A这颗芯片最大的特点就是便宜、够用8KB Flash、1KB RAM在大多数需要串口做调试或通信的小项目里完全够打。很多朋友上来就把串口调通却不知道为什么那样配导致换一个波特率、换一个引脚就抓瞎。所以我打算从“为什么选这颗料”开始讲讲清楚寄存器背后的原理再给可以直接抄的代码最后把中断处理那些坑填平。1. 先搞清楚STC8G1K08A这颗料再谈串口1.1 一颗8脚单片机为什么值得玩STC8G1K08A是STC宏晶推出的8051内核增强型单片机常见封装SOP8、SOP16、DFN等。我平时用得最多的是SOP88个引脚去掉电源和地剩下6个IO在这么小的体积下居然还集成了8KB Flash、1KB SRAM、ADC、比较器、PWM、SPI、I2C和多路UART扩展性比很多人想象中强很多。选它做串口相关的小项目核心原因是开发和下载足够简单一个USB转TTL模块就能下载程序不需要仿真器也不需要外部晶振和复杂复位电路非常契合“最小系统串口”的定位。内部IRC振荡器出厂校准过精度够用这意味着硬件上省掉了两颗负载电容加一颗晶振BOM成本可以压得很低。实际使用中1KB SRAM是最大的限制。如果串口缓冲区开太大其他全局变量就没地方放。但经过合理规划16字节接收环形缓冲加16字节发送缓冲完全够用8KB Flash也能在Bootloader和App同时存在的情况下装下不少代码逻辑。正因为这些资源特点这颗芯片非常适合做小家电控制、传感器数据上报、简单协议转换等串口类应用。1.2 UART的资源分配与引脚选择STC8G1K08A的UART1默认映射在P3.0RXD和P3.1TXD。SOP8封装下这两个引脚通常都能正常引出但很多项目里P3.0和P3.1还要复用下载功能所以搞清楚引脚切换非常关键。UART1的引脚映射由P_SW1寄存器控制。P_SW1的高两位S1_S1、S1_S0决定UART1接在哪一组引脚上默认00对应P3.0/P3.1。代码里不要直接对P_SW1整个赋值因为高两位之外还有别的外设引脚配置正确做法是读-改-写P_SW1 0x3F; // 清零S1_S1, S1_S0选择UART1默认引脚P3.0/P3.1如果你需要把UART1切换到其他引脚去看数据手册里P_SW1的完整表格即可。这里建议优先用默认引脚原因只有一个STC-ISP下载默认也从P3.0/P3.1走程序里如果不小心把引脚切走下载器可能连不上芯片还得靠按住复位键重新上电恢复很折腾。1.3 开发环境与工程建立的关键一步我用的开发环境是Keil C51但Keil自带的器件列表里通常没有STC8G1K08A。解决方法是在安装好Keil后打开STC-ISP下载软件进入“Keil仿真设置”把STC型号添加到Keil的器件数据库里。这一步漏掉的话新建工程时会发现找不到芯片型号头文件也没法自动匹配。添加完成后新建Keil工程选择“STC8G1K08A”编译器会自动引入STC8G系列头文件例如STC8G.H。如果老工程没有这个头文件可以去STC-ISP安装目录里找复制到工程目录再把头文件包含路径加上。这个文件里包含了所有SFR地址定义没有它后面用到的P_SW1、AUXR、SCON、SBUF全都得自己用sfr关键字声明写起来又烦又容易出错。工程建好后还有两件小事建议提前做Output标签页勾选“Create HEX File”否则STC-ISP没有hex文件可以下载实际项目建议先用一个点灯程序验证下载链路正常再开始写串口。这样如果串口调不通至少能排除下载问题省得来回怀疑硬件。2. 从零配置UART1寄存器、波特率与第一个字符2.1 波特率是怎么算出来的为什么我用11.0592MHz晶振串口模式1是异步8位UART波特率由定时器1的溢出率决定核心关系是波特率 定时器溢出率 / 16如果定时器1工作在1T模式定时器时钟就是系统时钟那么溢出率 系统时钟 / (256 - 定时器初值)所以波特率 系统时钟 / [16 × (256 - TH1)]以11.0592MHz和9600波特率为例256 - TH1 11059200 / (16 × 9600) 72TH1 256 - 72 184 0xB8这就是为什么网上所有STC串口例程里TH1、TL1都写0xB8。11.0592MHz是经过特意选择的频率能整除常见波特率误差为零。如果系统时钟跑12MHz算9600还勉强算115200就完全不能用。所以我的习惯是涉及串口的项目系统时钟优先选11.0592MHz直接用内部IRC振荡器不需要外部晶振。初始化代码SCON 0x50; // 串口1模式18位UARTREN1使能接收 AUXR | 0x40; // T1x121定时器1工作在1T模式 TMOD 0x0F; // 只修改定时器1不干扰定时器0 TMOD | 0x20; // 定时器1模式28位自动重装 TH1 0xB8; // 9600波特率重装值 TL1 0xB8; TR1 1; // 启动定时器1 TI 0; // 清发送完成标志 RI 0; // 清接收标志这里用定时器1的模式28位自动重装是因为8051传统架构下这种模式溢出后硬件自动把TH1重新装入TL1不需要在中断里手动赋初值作为波特率发生器非常稳定。T1x12位置1让定时器走1T模式如果把这个位清零代码里的初值还要重新按12分频计算很多抄来的例程时序不对多半就出在这个地方。2.2 最简发送代码往SBUF写一个字节串口发送的核心动作就两个把数据写进SBUF寄存器等发送完成标志TI置1。代码如下void UART1_SendByte(unsigned char dat) { SBUF dat; while (!TI); // 等待发送完成 TI 0; // 清标志准备下一次发送 }TI是串口硬件在帧发送完毕后自动置1的。只要TI没置1说明数据还在移位寄存器里没发完程序就在while处等着。这个函数简单但有个顺序细节一定要等到TI1之后才清标志不要一开始就清。有些代码为了保险在发送前加一句TI 0如果上一次的TI本来就为0那没问题但如果上一次TI还没被处理发送前清标志会导致函数直接从while跳出数据根本没发出去排查起来还挺隐蔽。有了单字节发送字符串发送就是这个函数的循环void UART1_SendString(char *s) { while (*s) { UART1_SendByte((unsigned char)*s); } }调用时不要忘记回车换行UART1_SendString(Hello World\r\n)。串口助手里如果没有\r\n所有输出会连在一行看起来像乱码或数据粘连。2.3 实测用示波器看第一个Hello World把USB转TTL模块的TXD接单片机P3.1TXDRXD接P3.0RXDGND共地打开串口助手选择9600波特率下载运行后应该能看到“Hello World”。如果看不到别急着怀疑代码用示波器或逻辑分析仪看P3.1有没有波形。比较好的验证方法是先发一个0x55也就是二进制01010101。它对应的波形是间隔均匀的方波每个bit宽度约104微秒1/9600。如果波形宽度明显不对说明定时器初值和实际系统时钟不匹配如果波形幅度不对多半是供电或者接线问题。这里必须反复强调一点USB转TTL的TXD接单片机RXDRXD接单片机TXD交叉接。很多新手在这里栽跟头信号怎么都出不来。另外USB转TTL模块的电平最好和单片机系统一致5V单片机就选5V输出的模块3.3V模块在某些情况下也能用但高电平识别可能不可靠不如统一供电省心。3. Hello World工程化printf重定向、字符串发送与打包3.1 重定向printf到串口putchar与底层发送的配合很多人在PC上写惯了printf想在单片机上直接用printf打印变量。Keil C51的printf底层会调用putchar函数所以只需要自己实现putcharchar putchar(char c) { UART1_SendByte((unsigned char)c); return c; }然后就可以直接写int temp 25; printf(temp %d\r\n, temp);这里有一个针对STC8G1K08A的提醒SRAM只有1KBprintf默认会占较多栈空间如果程序里还有大数组、结构体很容易栈溢出。而且C51的printf对浮点支持很弱打印float需要额外配置浮点库代码体积会明显变大。所以我实际项目里的习惯是调试阶段用printf快速验证逻辑产品发布前把关键日志改回自写的字符串发送函数避免资源浪费。3.2 发送逻辑统一封装日志分级与模块化工程上串口输出不要满工程到处直接调用UART1_SendString最好统一封装一层。一个很简单的日志模块可以这样设计#define LOG_LEVEL_NONE 0 #define LOG_LEVEL_INFO 1 #define LOG_LEVEL_ERROR 2 unsigned char g_log_level LOG_LEVEL_INFO; void log_msg(unsigned char level, char *msg) { if (level g_log_level) { return; } UART1_SendString([LOG] ); UART1_SendByte(0 level); UART1_SendString( ); UART1_SendString(msg); UART1_SendString(\r\n); }这样调试时把g_log_level设为INFO全量输出量产时改成ERROR只留关键错误信息。改动只在一处比到处删打印语句清爽得多。这种封装对串口项目来说不是多余而是后面加协议解析、Boot模式切换时都会用到的地基。3.3 乱码的真正原因与排查顺序乱码是串口调试最常碰见的问题没有之一。我一般按这个顺序排查波特率对不对。PC端串口助手和单片机两端波特率必须一致超过一半的乱码都是这里。时钟对不对。内部IRC设置成了11.0592MHz代码却按12MHz计算初值即使两端波特率显示一样实际偏差也会很大。数据格式对不对。模式1是8位数据、无校验、1停止位串口助手要配成8N1。共地问题。USB转TTL和单片机系统必须共地否则电平参考点不一致。电压问题。5V单片机接3.3V串口模块时有些情况下高电平识别不够稳定尽量统一供电。乱码看着烦但只要把逻辑分析仪接上TXD一眼就能看出位宽是否正常比盲猜快得多。4. 串口接收与中断处理从轮询到中断的工程化写法4.1 查询接收为什么够用但不好用接收数据在轮询模式下也不难void UART1_ReceivePoll(void) { if (RI) { RI 0; unsigned char dat SBUF; // 处理数据 } }问题是单片机在查询RI期间如果去干了别的事比如延时、刷屏、解析命令接收到的字节就会丢。对于一秒几个字节的简单指令轮询勉强能忍一旦协议帧长一点、主循环忙一点轮询接收就会让你怀疑人生。真正可靠的串口接收必须用中断数据来了硬件自动打断CPU把字节存到缓冲主循环再去处理。这也是从Hello World往工程化迈进的必经一步。4.2 串口1中断服务函数的正确写法STC8G的串口1使用中断号4。初始化时要打开串口中断和总中断ES 1; // 串口1中断允许 EA 1; // 总中断允许中断服务函数里先判断RI还是TI再分别处理。接收数据时先读SBUF保存起来再清RI这个顺序非常重要#define RX_BUFFER_SIZE 16 volatile unsigned char rx_buffer[RX_BUFFER_SIZE]; volatile unsigned char rx_head 0; volatile unsigned char rx_tail 0; void UART1_ISR(void) interrupt 4 { unsigned char dat; if (RI) { dat SBUF; // 先读数据 RI 0; // 再清接收标志 rx_buffer[rx_head] dat; rx_head (rx_head 1) % RX_BUFFER_SIZE; } if (TI) { TI 0; // 如果要实现中断发送可以在这里从发送缓冲取下一字节写入SBUF } }我用的是环形缓冲区rx_head指向下一个写入位置rx_tail指向下一个取出位置。主循环只要判断rx_head ! rx_tail就说明有数据。为什么用环形而不是简单累加索引因为缓冲区循环利用不需要定期清空代码逻辑更稳定。ISR里不要做复杂解析只负责存字节。真正解析协议要在主循环里做。中断里写大数组、循环几百次都会拖慢主循环甚至影响其他中断的实时性。这是个很重要的工程习惯不是代码功能性问题但会影响整个系统的稳定性。4.3 不定长帧的结束判断超时方法比想象中好用接收字节容易难的是知道一帧数据什么时候结束。常用办法有两种固定帧长或者“帧头帧尾校验”。还有一种是利用串口空闲时间判断也就是收到第一个字节后启动定时器定时时间到且没有新字节到来就认为一帧结束。STC8G没有STM32那种硬件空闲中断但用定时器完全能做到差不多的效果。思路很简单串口中断每收到一个字节就重新装载定时器超时值并清除帧完成标志定时器超时中断里把帧完成标志置1。主循环检测到帧完成标志后去环形缓冲区按顺序解析数据。这种超时判断特别适合AT指令这类不定长文本协议因为不需要约定特殊帧尾字符数据内容里出现任何字符都不会误判。我自己的经验是如果协议是自己定义的优先用“固定帧头固定帧长校验”逻辑最简单定位问题也容易如果需要兼容AT指令这种不定长文本再考虑超时判断。两种方法都不复杂但要根据场景选不要一套方案打天下。4.4 踩过的坑中断里清RI的时机我有一次做传感器数据上传串口中断里只做数据缓存但隔一段时间就感觉丢字节。后来发现是中断服务函数里先清RI再读SBUF清RI后读SBUF的瞬间如果又有新字节进来RI会再次置1但这次中断还没退出SBUF还没被读走新数据可能覆盖掉旧值。改成先读SBUF再清RI之后丢字节问题立刻消失。这个坑特别容易出现在网上抄来的代码里。中断函数中寄存器的操作顺序不是随便写的要严格按“先保存数据、再清标志”来。对应的发送方向也一样要等TI置1后再清顺序反了都会出诡异问题。4.5 Boot与App共存时的中断处理最稳妥的做法标题里既然提到中断处理我多说一个跟Boot/App相关的常见问题。很多同学做IAP升级Boot程序里用了串口中断接收升级包App程序里也用了串口中断跳转之后发现App的串口中断永远不触发。原因是8051中断向量表位置固定Boot程序位于低地址中断入口也在低地址。Boot的中断服务函数如果存在App的中断触发后会跳到同一个向量执行的是Boot的中断代码而不是App里的中断处理。传统8051内核不像ARM Cortex-M有中断向量表偏移机制多套固件共存时需要自己处理。我的建议是Boot程序尽量不用中断串口接收在Boot阶段全部用轮询加超时完成。升级包接收本来就是可控流程Boot里没必要引入中断。跳转到App之前必须做这几件事ES 0关闭串口中断实际上是关闭所有中断清除RI、TI标志把SP恢复到芯片复位时的初值关闭定时器、外设恢复默认时钟最后通过函数指针跳转到App复位向量。void jump_to_app(void) { EA 0; ES 0; RI 0; TI 0; SP 0x07; // 恢复SP到初值 // App复位地址根据实际链接定位决定 ((void (code *)(void))0x0000)(); }如果App实际起始地址不是0x0000而是0x0400或0x1000这类地址需要根据工程链接设置跳转到对应地址。STC8G1K08A只有8KB FlashBoot留1KBApp就可以从0x0400开始具体以自己项目的脚本为准。最稳妥的方案永远是Boot不用中断App独占中断这也是目前很多量产Bootloader的标准做法。5. 实测验证与避坑清单5.1 下载与供电STC下载器的常见坑STC单片机使用STC-ISP软件下载操作顺序是先点击下载然后给芯片冷启动上电或者按复位键重新上电。如果一直提示“正在检测目标单片机”大概率是TXD/RXD接反或者没有共地或者波特率选太高导致握手失败。解决方法很简单把最低波特率和最高波特率都设为2400降低握手速率基本都能连上。供电方面SOP8封装要仔细看封装图区分VCC和GND。使用USB转TTL模块的5V输出供电时小心电流不够。串口通信本身电流不大但如果还带了LED、传感器建议单独稳压供电USB转TTL只做信号线连接。所有模块必须共地这条说多少次都不为过。5.2 波特率误差的容限串口异步通信允许收发双方有一定波特率误差通常不超过2%到3%但115200这类高速率最好控制在1%以内。使用11.0592MHz时钟时常见波特率误差为0。使用12MHz时钟时我整理了一张表目标波特率系统时钟1T模式下分频系数实际波特率误差960011.0592MHz7296000%960012MHz7896150.16%1920011.0592MHz36192000%11520011.0592MHz61152000%11520012MHz61250008.51%看到没有12MHz跑115200直接没法用。如果板子已经定了12MHz只能降低目标波特率或者换用外部晶振。这也是我前面反复强调串口项目尽量用11.0592MHz的原因不是玄学是数学上算出来的。5.3 用逻辑分析仪和串口助手配合定位问题最后一个实用建议手边备一个USB逻辑分析仪24MHz采样率的十几二十块就很够用。把逻辑分析仪通道夹在单片机TXD上地线共地在软件里选择UART解码设成9600、8N1发送Hello World后就能直接看到解码出的字符串。如果解码出来是乱码多半是波特率偏了如果波形高电平幅度不对检查供电和接线。逻辑分析仪比示波器强在能直接解码不用自己数位宽。串口助手适合看最终结果但中间环节出了问题还是得靠逻辑分析仪定位。调试串口不是靠猜而是靠看波形。我自己每次写串口程序都会先用逻辑分析仪把初始化后发的0x55抓一下确认位宽正常再继续后面的逻辑这个习惯帮我省了很多排查时间。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →