1. 为什么一块“老”芯片还值得单独写一篇我前阵子翻工具柜翻出一片ATmega162。说它“老”一点不为过——这是8位AVR单片机里很有年代感的型号现在的新人大概率连名字都没听过。但这颗芯片有个让我一直记着的特点它自带两个硬件串口USART。在入门级8位AVR里双硬件串口是相当稀缺的配置而M162恰好就是这种“冷门但有绝活”的型号。如果你只在ATmega8、ATmega16、ATmega32或者后来流行的ATmega328P上玩过串口那你接触到的通常都只有一个USART。想要第二个串口怎么办最常用的办法是软件模拟用定时器加GPIO去模拟UART时序。软件串口不是不能用但它占用CPU、依赖定时器、波特率高了容易出问题在收发同时进行的场景下更是捉襟见肘。M162直接给你两个完整硬件USART这件事从硬件层面就解决了。1.1 双串口在8位AVR家族里的稀有程度AVR家族其实很大但大多数人所熟悉的“经典款”串口资源并不宽裕。我整理了一个粗略对照型号Flash硬件USART数量我的备注ATmega88KB1入门常用单串口够用但扩展难ATmega16 / ATmega3216KB / 32KB1教科书常客串口只有一个ATmega328P32KB1玩Arduino的应该很熟ATmega16216KB240脚级别里少见地给足了双串口ATmega64 / ATmega12864KB / 128KB2高端大容量但封装大成本高M162的定位很有意思它有16KB Flash、1KB SRAM、512字节EEPROM容量不大不小但外设列表里直接给出了两个USART。这颗芯片在当年就是冲着“通信控制”去的不是那种全功能通用MCU而是“串口密集型”场景的专用件。1.2 我心目中的M162典型工作画像在我的实际经验里M162最常见的舞台是这几类协议转换盒子一路RS232接上位机一路RS485接现场设备M162在中间做格式转换、地址解析、数据缓存。双总线汇聚两个不同波特率或不同协议的子设备都要和主机通信M162分别伺候。串口调试与业务分离一路串口跑业务数据另一路专门输出调试日志。这个用法特别香业务代码和调试信息互不干扰。老设备维修与替换一些工业控制板上用的就是M162板子坏了备件难找能看懂它就能修。所以这篇文章不是让你去“考古”而是想告诉你这颗老芯片在特定场景下依然能打。接下来我就把它双串口的底细、可用代码、常见坑和选型边界一次讲清楚。2. 先摸清楚M162双串口的家底很多人一听到“双串口”就觉得是“两个能发能收的串口而已”但真正动手时才会发现双串口带来的不只是多一根TX、RX而是一整套独立资源。M162的两个USART每一个都拥有自己的状态寄存器、控制寄存器、数据寄存器、波特率寄存器以及独立的中断向量。这意味着它们可以互不干扰地工作在不同的波特率、不同的帧格式下。这种“每个串口各自独立”的特性在工程上非常重要。比如USART0跑9600波特率8数据位、无校验、1停止位USART1跑115200波特率甚至可以配成9位多机通信模式。两个串口互不牵制硬件上天然支持“异构总线桥接”软件上也不用担心某个寄存器的配置把另一个串口给带偏。2.1 两个独立USART到底独立在哪我用一个类比来解释很多单串口MCU像只有一个电话亭所有人都要排队打电话M162像是有两个电话亭每个电话亭都有自己的号码、线路、铃声互不干扰。这里的“电话亭”就是USART外设而“线路”就是对应的引脚。具体到寄存器层面两个串口各自拥有一套UCSRA/UCSRB/UCSRC、UDR、UBRRH/UBRRL。你操作UDR0不会影响UDR1使能USART0的接收中断也不会触发USART1的中断服务。这个隔离性让程序结构变得清晰你可以把两路通信当成两个独立任务来处理顶多在主循环里做数据交换。M162的两个USART在AVR Libc里通常写成UCSR0A/UCSR0B/UCSR0C、UDR0、UBRR0H/UBRR0L和UCSR1A/UCSR1B/UCSR1C、UDR1、UBRR1H/UBRR1L。代码里看到“0”和“1”后缀就分别对应USART0和USART1。2.2 波特率计算与UBRR寄存器算一个给你看AVR的USART波特率设置并不玄乎核心公式是异步正常模式U2X0BAUD F_CPU / (16 * (UBRR 1))异步倍速模式U2X1BAUD F_CPU / (8 * (UBRR 1))反过来已知目标波特率求UBRR就是UBRR F_CPU / (16 * BAUD) - 1 正常模式我用最常见的16MHz晶振来算个实际例子。目标波特率9600UBRR 16000000 / (16 * 9600) - 1 103.17取整数103实际波特率 16000000 / (16 * (103 1)) 9615.38误差 (9615.38 - 9600) / 9600 ≈ 0.16%0.16%的误差在UART通信中非常安全这也是9600波特率在16MHz晶振下很稳的原因。但如果你把目标波特率提到115200UBRR 16000000 / (16 * 115200) - 1 ≈ 7.68取8实际波特率 16000000 / (16 * (8 1)) 111111误差 ≈ -3.55%-3.55%就比较难受了。UART虽然有一定容错能力但这个误差已经逼近链路双方时钟偏差的容忍极限容易偶发乱码。我在后面“踩坑”章节会专门讲这个问题这里先记住结论16MHz晶振下跑9600很舒服跑115200最好换晶振或启用U2X。UBRR寄存器是16位的分UBRRH高字节和UBRRL低字节写的时候要注意顺序先写高字节再写低字节避免中间状态让硬件读到半个值。2.3 引脚复用与中断向量别等板子不跑才查手册M162的两个串口引脚都“挤”在PD端口上这也是为什么我提醒你拿到芯片后第一件事就是打开数据手册找到引脚复用表对着看。不同封装40脚DIP、44脚TQFP等的引脚号不完全一样但逻辑上USART0和USART1都从PD口引出。这里有个很容易忽略的细节USART的RX引脚是输入方向TX引脚是输出方向。很多人初始化时只记得打开RXEN和TXEN却忘了检查DDR方向寄存器的设置。如果TX引脚方向不对发送时引脚输出不了正确的电平硬件上看起来就是“串口没反应”。中断方面两个USART都有自己独立的接收完成、发送完成、数据寄存器空中断向量。在C代码里中断服务函数通常写作ISR(USART0_RXC_vect) { } ISR(USART1_RXC_vect) { }如果你的头文件版本比较新向量名可能略有差异搜一下头文件里的USART相关宏即可。中断向量的独立意味着两路串口可以同时进入“接收完成”事件处理不会因为只用一个串口中断而丢数据。3. 实用代码骨架把两个串口真正用起来理论说了一堆还是得上代码。下面这套代码我在类似AVR型号上反复用过逻辑可以直接抄寄存器名根据你的编译器头文件稍作调整即可。3.1 初始化8N1与波特率设置的固定套路初始化两个USART的标准套路其实和初始化一个串口差别不大只是把“0号串口初始化”复制一份改成“1号串口初始化”。以8数据位、无校验、1停止位8N1为例#include avr/io.h #include stdint.h #define F_CPU 16000000UL static void uart0_init(uint32_t baud) { uint16_t ubrr F_CPU / 16 / baud - 1; UBRR0H (uint8_t)(ubrr 8); UBRR0L (uint8_t)ubrr; UCSR0B (1 RXEN0) | (1 TXEN0); // 使能接收和发送 UCSR0C (1 UCSZ01) | (1 UCSZ00); // 8N1 } static void uart1_init(uint32_t baud) { uint16_t ubrr F_CPU / 16 / baud - 1; UBRR1H (uint8_t)(ubrr 8); UBRR1L (uint8_t)ubrr; UCSR1B (1 RXEN1) | (1 TXEN1); UCSR1C (1 UCSZ11) | (1 UCSZ10); }值得说明的是UCSR0C/UCSR1C这个寄存器负责帧格式和校验位UCSZ01/UCSZ00这两位共同决定数据位长度。8N1是这个世界的默认语言串口设备之间互通的概率最大。如果你的项目需要9位多机通信或者需要偶校验那就得额外设置UPM位和UCSZ位而不是只改波特率。3.2 透明桥接最简单也最实用的双串口玩法两个串口最直接的应用就是透明桥从USART0收到的每一个字节原封不动地从USART1发出去从USART1收到的字节也从USART0发出去。这相当于用M162做了一根“无线缆线”可以把一个旧设备的串口电平转换、延长、或者连接到一个不同接口类型的主机上。轮询版代码非常短int main(void) { uart0_init(9600); uart1_init(9600); while (1) { // USART0 - USART1 if (UCSR0A (1 RXC0)) { uint8_t d UDR0; while (!(UCSR1A (1 UDRE1))); UDR1 d; } // USART1 - USART0 if (UCSR1A (1 RXC1)) { uint8_t d UDR1; while (!(UCSR0A (1 UDRE0))); UDR0 d; } } }这段代码的工作方式是查询接收完成标志位有数据就立刻转发到另一个串口。它有两个值得说的地方。第一发送前一定要等UDRE标志这个标志表示发送数据寄存器为空可以安全写入下一个字节。第二如果USART0和USART1的波特率不同这个“桥”依然成立字节在中间被M162缓存了一下不会因为两端速度不一致而丢数据——除非一端疯狂灌数据另一端来不及发CPU又没有额外的缓冲机制。轮询版适合低速、低流量的场景。如果你要桥接的是115200波特率持续不断的数据流单靠主循环轮询很容易在某一瞬间漏字节。3.3 升级版轮询改中断配一个极简FIFO更实用的做法是给每个串口配一个环形缓冲区ring buffer接收完成中断里只做一件事把数据放入缓冲区。主循环负责从缓冲区取出数据、转发到另一个串口。这样就算某一路串口突然涌来一堆数据FIFO也能扛住不会因为主循环正在处理另一路而丢字节。一个极简环形缓冲区可以这样定义#define RING_SIZE 64 typedef struct { volatile uint8_t buf[RING_SIZE]; volatile uint8_t head; volatile uint8_t tail; } ringbuf_t; static void rb_put(volatile ringbuf_t *r, uint8_t d) { uint8_t next (uint8_t)((r-head 1) % RING_SIZE); if (next ! r-tail) { r-buf[r-head] d; r-head next; } } static int rb_get(volatile ringbuf_t *r, uint8_t *d) { if (r-head r-tail) { return 0; } *d r-buf[r-tail]; r-tail (uint8_t)((r-tail 1) % RING_SIZE); return 1; }注意缓冲区满时我选择直接丢弃新数据这是最简单也最安全的策略。如果你的应用不能丢任何一字节那要么把缓冲区开大要么做暂停上游的流控。中断服务函数里只负责入队static ringbuf_t rb0; static ringbuf_t rb1; ISR(USART0_RXC_vect) { rb_put(rb0, UDR0); } ISR(USART1_RXC_vect) { rb_put(rb1, UDR1); }主循环则负责转发int main(void) { uart0_init(9600); uart1_init(9600); sei(); while (1) { uint8_t d; if (rb_get(rb1, d)) { while (!(UCSR0A (1 UDRE0))); UDR0 d; } if (rb_get(rb0, d)) { while (!(UCSR1A (1 UDRE1))); UDR1 d; } } }这套结构最大的好处是中断处理极短不会阻塞其他中断主循环按自己的节奏转发哪怕某一时刻两个串口同时来数据缓冲区也能暂存。对M162这种老8位MCU来说FIFO设计比花哨的DMA好用得多。4. 双串口项目里最容易踩的坑老芯片用起来有它特有的“脾气”下面这几个坑我在实际项目里都踩过写出来帮你省点调试时间。4.1 波特率误差9600很稳115200开始有讲究波特率误差是串口项目里最隐蔽的坑。平时用9600波特率收发都很正常一旦换成115200偶尔就出现乱码尤其两个设备之间通信线又长的时候表现得特别明显。问题根源常常就在晶振频率和波特率不匹配。我在前面已经算了16MHz晶振跑115200误差约-3.55%。这个误差在UART的容错边缘附近晃运气好能用运气不好就丢字节。解决思路有几种换一颗能让波特率整除的晶振比如14.7456MHz跑115200时UBRR7完美零误差。启用U2X倍速模式公式从16分频变成8分频有时能把误差压低。把通信速率降到19200或38400让误差回到1%以内。我把常见组合列个表方便你直接查晶振频率目标波特率UBRR取值实际波特率误差16.000MHz96001039615.380.16%16.000MHz1152008111111.11-3.55%14.7456MHz11520071152000%11.0592MHz11520051152000%从表里能看出一个规律老工程师爱用11.0592MHz、14.7456MHz这类晶振从来不是为了好看而是它们和串口波特率是“天生一对”。如果项目里串口是主要通信手段晶振选择优先于MCU选型。4.2 外部电路TTL、RS232、RS485之间的电平转换M162的串口头不是直接连电脑或者连工业设备的。它的UART引脚输出的是TTL电平0V代表低电平、5V代表高电平。和不同设备对接时需要对应的电平转换电路对接设备常见方案注意事项电脑USB口USB转TTL模块注意模块是3.3V还是5V供电老式串口RS232电平MAX232类芯片正负电压摆幅需要电荷泵电容工业总线RS485MAX485类芯片需要控制发送/接收方向引脚传感器/北斗模块直接TTL对接确认模块电平是否兼容这里最容易犯的错误是忘记共地。TTL电平通信是单端信号两端必须共地。电源隔离的板子之间如果共地不好串口会表现为“偶发乱码”“丢字节”甚至静态时就有干扰。RS485则需要注意A、B接线方向和终端电阻接线反了只是收不到数据而忘了终端电阻在长距离高速率下会反射导致通信不稳定。4.3 老芯片的外设拼图它不是什么都有M162虽然给了你两个串口但它不是万能芯片。最让我意外的一点是这个型号并没有集成ADC模块。在AVR家族里ATmega8、ATmega16、ATmega32都有10位ADC但M162偏偏不含ADC它的模拟外设主要是一个模拟比较器没有多路模拟量采集模块。这让它不适合直接挂电位器、温度传感器模拟输出、电压采样这类应用。如果你想用它做模拟量采集要么外接ADC芯片要么换带ADC的型号。另外M162的Flash是16KB看起来不小但双串口项目一旦涉及协议解析、缓冲区管理、日志输出代码量涨得很快。C语言编写时你可能觉得16KB够用但编译完发现已经用了90%以上。老芯片没有大容量Flash版本可选所以写代码时要精简少用重型库。M162也支持JTAG调试接口。JTAG引脚如果被使能它占用的IO口不能当普通GPIO用。在需要把所有引脚都派上用场的项目里记得检查熔丝位是否关闭了JTAG否则你的“引脚不够用”可能根本不是硬件问题而是调试接口在占地方。5. 选型边界什么时候用M162什么时候别硬扛聊了这么多M162的好最后必须冷静说一句这块芯片有它适用的边界不是什么地方都适合。它适合的场景和现在流行的32位MCU并不完全重叠更多时候是一种“刚刚好”的选择。5.1 适合与不适合的判断维度适合用M162不建议用M162串口数量需要两个硬件串口需要四五个串口M162不够处理复杂度协议简单、数据量小需要大量浮点运算、复杂协议栈模拟信号采集不涉及或外接ADC需要多路ADC直接采样功耗不在乎5V工作功耗电池供电、低功耗睡眠需求高开发环境喜欢寄存器级开发、追求稳定需要现代IDE、SDK、调试丰富生态从实际项目看我见过M162用得最舒服的项目是一个把多个水位传感器数据汇聚到上位机的网关。两个串口一路接传感器总线、一路接上位机M162在中间做协议转换和心跳上报程序总共写了不到2000行运行几年没出过问题。这类项目不需要高算力、不需要图形界面、不需要复杂的传感器融合M162的双串口加上稳定5V供电反而成了优点。5.2 我个人用下来的体会这颗芯片让我最舒服的地方是它的“透明感”。寄存器就那么多手册也薄遇到问题翻几页就能找到答案。相比现在动辄上千页的老大难MCU参考手册M162的学习成本低到可以忽略。而独立双串口的硬件设计又能直接支撑真实的多设备通信项目不需要临时搭软件串口去救场。我也得提醒你M162现在基本买不到全新原厂货了市场上流通的多是拆机片或库存片。买回来先焊到板子上连续通电跑几天收发测试确认没问题再批量用。老芯片不是不能用但“老”本身就意味着要更谨慎地对待供应链。如果你手头正好有一批M162或者从旧设备上拆到了它不妨用它做点串口相关的小项目。趁它还活着把这颗双串口老将最后的价值榨出来这比让它躺在抽屉里积灰有意义得多。
阅读完成 · 觉得有帮助?