简介:这是一套基于STM32与CubeMX+HAL库开发的PS/2解码完整工程,面向需要掌握键盘/鼠标通信协议的嵌入式开发者。工程以STM32F1系列为主,展示了从CubeMX图形化配置时钟、GPIO、DMA到HAL库编写解码逻辑的完整链路,覆盖PS/2协议中的数据线、时钟线采样、起始位/停止位判断与奇偶校验等关键环节,适合学习外设驱动和串行通信的入门及进阶者。压缩包共176个文件,包括62个h头文件与24个c源码文件,另有uvprojx、ioc、mxproject等工程配置,以及axf、hex、crf等编译中间产物,便于直接查看或重新构建工程,整体大小约5.07MB。目前已有969人学习。解包后可按目录定位到初始化代码、HAL库封装和主逻辑,既能直接参考PS/2解码实现,也能迁移其中的CubeMX+HAL项目搭建思路;结合资源内对同步串行帧格式的解析,可加深对MSB优先、奇偶校验等机制的理解,对后续开发其他串行外设驱动很有帮助。
1. PS2解码,为什么值得正经做一个CUBEMX+HAL库工程
很多人在做设备兼容时会遇到这样一个需求:手头有一个还在服役的老式键盘或鼠标,只留下PS2接口,而主控板是STM32。把PS2解码加进去,看起来只是数脉冲,但如果直接在HAL库的GPIO中断回调里数高低电平,十个工程有五个会在奇偶校验和边沿抖动上翻车。PS2协议虽然老,但它的时序是双向的:设备发数据时设备控制时钟,主机发命令时主机要先拉低时钟申请总线,这个角色切换在HAL库里没有现成封装,只能自己维护一套状态机。CUBEMX能帮你把引脚、中断和时钟树一次配好,但真正的解码逻辑必须落到代码里。
这套工程方案适合这些人:用STM32做键鼠输入的开发者、要在工业面板或某个跨平台系统里兼容老外设的人,以及想弄懂PS2时序却不希望靠玄学调参的新手。它会先讲清时序,再给可复现的初始化步骤和解码代码,最后把几个常见的坑摊开讲。
2. PS2协议:帧结构、采样点与两套通信规则
2.1 一帧11位:起始位、数据位、奇校验、停止位
PS2协议的核心是一帧11位,无论键盘还是鼠标,无论数据方向,帧结构都一样。空闲状态时时钟线和数据线同时为高,设备要发送数据时,先把数据线拉低作为起始位,然后时钟线产生下降沿,之后每一个时钟周期对应一位数据。主机要做的事情,就是在合适的采样点把数据线电平读进来,凑满11位后做校验和解析。
一帧的位分布按发送顺序展开是这样:
| 位序 | 用途 | 电平约定 |
|---|---|---|
| 第1位 | 起始位 | 必须为低电平0 |
| 第2~9位 | 数据位 | 8位,LSB在前 |
| 第10位 | 奇偶校验位 | 保证数据位+校验位中1的个数为奇数 |
| 第11位 | 停止位 | 必须为高电平1 |
关键点是LSB在前。比如发送扫描码0x1C,二进制是00011100,线上实际发送顺序是0、0、1、1、1、0、0、0,与直觉相反。很多工程第一次解析出来数值翻倍或者高低字节错位,就是因为没做位序翻转。
关于采样点,这里有一个血泪经验:数据线在时钟下降沿附近变化,在上升沿时稳定。所以不要在下降沿中断里立刻读数据线,那样读到的是上一个位的旧值,或者正处于跳变中的不稳定电平。正确做法是把时钟线的上升沿作为采样点。
2.2 设备发送与主机发送:角色不同,时序完全不同
PS2最容易被低估的地方在这里。设备向主机发送时,时钟完全由设备产生,主机只是被动采样,这时候主机侧的中断配置成上升沿触发即可。但主机要向设备发送命令时,主机必须先拉低时钟线至少100微秒,申请总线使用权,然后由主机控制时钟向设备逐位写入数据。
设备发送数据和主机发送数据的差异可以看这张表:
| 项目 | 设备发送 | 主机发送 |
|---|---|---|
| 时钟谁产生 | 设备 | 主机申请后由设备接管产生时钟 |
| 起始位 | 数据线先拉低 | 主机拉低数据线 |
| 采样点 | 主机在上升沿采样 | 设备在上升沿采样,主机在低电平期间准备好数据 |
| 结束信号 | 停止位后释放总线 | 停止位后设备拉低数据线应答ACK |
如果只做了设备解码,没做主机发送,那键盘的指示灯控制、鼠标滚轮模式切换全都没法实现。很多教程只讲单向解码,导致用户做到一半发现无法向设备发命令,这是常见的认知断层。一个完整的PS2工程,必须把两套时序都覆盖。
2.3 解码策略选型:为什么选外部中断+状态机
解码方案有三条路线。第一条是轮询,主循环里不停读时钟线电平,检测边沿。这条路线在裸机下会被其他中断频繁打断,PS2时钟约10到40kHz,位周期最短只有几十微秒,一旦被打断超过半个位周期,采样点就偏了。第二条是定时器输入捕获,用TIM的输入捕获通道直接捕获时钟边沿,硬件完成时间戳记录,精度很高,但占用一个高级定时器,且数据位提取仍需额外逻辑。第三条是外部中断+软件状态机,这也是我在这套工程里采用的做法。
外部中断+状态机的优势在于:GPIO上升沿和下降沿都能触发,中断里只做最轻量的操作——记录边沿间隔、采样数据线、累加位计数,不做任何延时和打印。这样即使系统其他地方响应慢一点,只要EXTI中断优先级最高,位采样就不会丢。对比下来,轮询适合时间片极短的RTOS任务,捕获适合对时序强依赖的场景,而中断+状态机是通用性最好、CUBEMX里配置最简单的一种。
3. CUBEMX初始化:引脚分配、EXTI与NVIC优先级
3.1 CUBEMX外设配置:时钟线、数据线与调试串口
以常用的F103系列为例,我在CUBEMX里给PS2分配PC6作为时钟线、PC7作为数据线。这两个引脚都是FT引脚,能容忍5V电平,这点很重要。PS2设备可能是5V逻辑的老外设,虽然信号是开漏输出、高电平由上拉电阻决定,但引脚本身能容忍5V输入会更稳妥。
在CUBEMX的Pinout视图里把PC6配置成GPIO_EXTI6,PC7配置成GPIO_Input,模式都选Pull-up。时钟线开上升沿和下降沿触发,因为状态机同时需要下降沿标记起始和数据线电平变化。数据线不需要外部中断,它只是被读取的对象。
| 功能 | 引脚 | CUBEMX配置 | 说明 |
|---|---|---|---|
| PS2时钟 | PC6 | EXTI6,上升/下降沿,上拉 | 边沿触发中断 |
| PS2数据 | PC7 | GPIO_Input,上拉 | 只读数据线 |
| 调试串口 | PA9/PA10 | USART1,115200 8N1 | 打印解码结果 |
| 时基 | SysTick | 默认1ms | HAL_Delay和超时判断 |
| 快速计数 | TIM3 | 内部时钟,计数频率1MHz | 边沿间隔过滤 |
时钟树方面,外部高速时钟配72MHz主频,APB1分频后的TIM3时钟也要确认好,因为后面要用TIM3的计数器来做微秒级时间戳。CUBEMX生成的代码里TIM3默认是1ms中断模式,这里要改成不使能中断、只保留计数功能,在Parameter Settings里把Prescaler和Period配成让计数频率为1MHz。
3.2 NVIC优先级:EXTI最高,定时器其次
PS2解码的成败很大程度在NVIC配置。时钟边沿中断必须抢占最高优先级,否则系统里任何一个中断抖动都会导致边沿丢失。在CUBEMX的NVIC设置页里,把EXTI6的抢占优先级设为0,TIM3设为1,USART1设为2。SysTick保持默认即可。
| 中断源 | 抢占优先级 | 用途 |
|---|---|---|
| EXTI6 | 0 | PS2时钟边沿,不可丢 |
| TIM3 | 1 | 微秒时间戳与超时判断 |
| USART1 | 2 | 调试打印 |
| SysTick | 默认 | HAL_Delay |
这里有一个容易踩的坑:不要在定时器中断里去做PS2的边沿轮询,也不要让某个外设中断的优先级和EXTI相同。HAL库的中断回调都是统一入口,如果两个同优先级中断同时到达,NVIC会按硬件编号顺序执行,EXTI6不一定能保证立刻响应,极端情况下位采样就错位了。
3.3 生成代码后的初始化顺序与总线自检
CUBEMX生成代码后,main函数里的初始化顺序是固定的:HAL_Init、时钟配置、GPIO配置、外设配置。PS2的初始化顺序有一个额外要求:设备上电后可能立即开始发送数据,如果主机侧的EXTI还没使能,第一帧就会丢失。
常见做法是在PS2初始化函数里做一次总线复位。先把时钟线和数据线都配置成开漏输出,拉低时钟线保持100微秒以上,再释放时钟线,让设备重新同步。然后延时200毫秒,等待设备上电稳定,再主动丢弃接收缓冲里的残留数据。这一段逻辑直接放在初始化函数末尾:
void ps2_init(void) { GPIO_InitTypeDef ini = {0}; // 时钟线:开漏输出,用于总线复位 ini.Pin = PS2_CLK_PIN; ini.Mode = GPIO_MODE_OUTPUT_OD; ini.Pull = GPIO_PULLUP; ini.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(PS2_CLK_PORT, &ini); // 数据线:开漏输出 ini.Pin = PS2_DATA_PIN; HAL_GPIO_Init(PS2_DATA_PORT, &ini); // 拉低时钟线100us以上,请求设备复位 HAL_GPIO_WritePin(PS2_CLK_PORT, PS2_CLK_PIN, GPIO_PIN_RESET); delay_us(150); HAL_GPIO_WritePin(PS2_CLK_PORT, PS2_CLK_PIN, GPIO_PIN_SET); // 恢复接收模式:CLK为EXTI上升/下降沿,DAT为输入上拉 ps2_line_mode(0, 0); // 等待设备重新上电应答,丢弃不稳定数据 HAL_Delay(200); ps2_rx_active = 0; ps2_rx_done = 0; }这段代码的逻辑是:先把两条线都变成开漏输出,主动拉低时钟线触发设备复位,再恢复成接收模式。ps2_line_mode是方向切换函数,后面的章节会给出完整实现。初始化完成后不要立刻收数据,先等200毫秒让键盘或鼠标完成自检,否则收到的可能是设备上电自检的0xAA字节,混入正常按键流里会干扰解析。
4. 解码代码实现:边沿采集、键盘扫描码与鼠标数据包
4.1 外部中断回调:去毛刺、置起始、采数据
解码核心在HAL_GPIO_EXTI_Callback里。这段代码的全部逻辑都在中断上下文执行,所以要保证每条语句都是微秒级操作,不能调用HAL_Delay、不能做串口打印、不能处理复杂协议解析。
#define PS2_CLK_PORT GPIOC #define PS2_CLK_PIN GPIO_PIN_6 #define PS2_DATA_PORT GPIOC #define PS2_DATA_PIN GPIO_PIN_7 volatile uint8_t ps2_rx_active; // 1=正在接收一帧 volatile uint8_t ps2_rx_done; // 1=完整帧已接收,待处理 volatile uint16_t ps2_rx_buf; // 收满11位,bit0为起始位 volatile uint8_t ps2_rx_bit_count;// 已采样的上升沿个数 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { uint32_t now; uint32_t interval; if (GPIO_Pin != PS2_CLK_PIN) { return; } // 利用TIM3的1MHz计数器计算边沿间隔,滤掉毛刺 now = __HAL_TIM_GET_COUNTER(&htim3); static uint32_t last_edge; interval = now - last_edge; last_edge = now; // 正常位周期最小也在30us以上,间隔小于20us视为抖动 if (interval < 20) { return; } if (HAL_GPIO_ReadPin(PS2_CLK_PORT, PS2_CLK_PIN) == GPIO_PIN_SET) { // 上升沿:数据线稳定,采样并累加 if (ps2_rx_active) { uint8_t bit = HAL_GPIO_ReadPin(PS2_DATA_PORT, PS2_DATA_PIN); ps2_rx_buf |= ((uint16_t)bit << ps2_rx_bit_count); ps2_rx_bit_count++; if (ps2_rx_bit_count >= 11) { // 已收满起始位+8数据位+校验位+停止位 ps2_rx_active = 0; ps2_rx_done = 1; ps2_rx_bit_count = 0; } } } else { // 下降沿:标志一帧开始,不读数据线 if (!ps2_rx_active && !ps2_rx_done) { ps2_rx_active = 1; ps2_rx_bit_count = 0; ps2_rx_buf = 0; } } }代码里的帧位分布需要说清楚。按采样先后顺序,bit0是起始位,bit1到bit8是数据位(LSB在bit1),bit9是奇校验位,bit10是停止位。解析时取出数据用(ps2_rx_buf >> 1) & 0xFF。
去毛刺的20微秒阈值不是拍脑袋定的。PS2规范允许时钟频率最低10kHz,对应位周期100微秒,半周期50微秒;即使高速模式40kHz,半周期也有约25微秒。阈值设在20微秒,既不会滤掉正常边沿,又能干掉大多数线缆感应噪声。
有一个细节容易忽略:ps2_rx_done置1后,如果主循环没有及时处理,下一个下降沿到来时会因为ps2_rx_done为1而不会开启新帧。所以主循环处理完一帧后,必须把ps2_rx_done清0。这个标志位的生命周期要约定好:中断只负责置1,主循环负责清0。
4.2 键盘解码:通码、断码与重复帧处理
键盘发送的是扫描码,不是ASCII码。扫描码分通码和断码,按下按键发送通码,松开时发送0xF0加通码。比如A键的通码是0x1C,松开时发送0xF0、0x1C两个字节。还有扩展按键,比如方向键和编辑键,会先发送0xE0前缀。
主循环里的帧处理逻辑如下:
void ps2_keyboard_process(uint16_t frame) { uint8_t data; uint8_t parity; uint8_t stop_bit; uint8_t ones = 0; int i; // 拆帧 data = (frame >> 1) & 0xFF; parity = (frame >> 9) & 0x01; stop_bit = (frame >> 10) & 0x01; // 丢弃:起始位不为0、停止位不为1 if ((frame & 0x01) != 0 || stop_bit != 1) { return; } // 奇校验:数据位+校验位中1的总数必须为奇数 for (i = 0; i < 8; i++) { if (data & (1 << i)) ones++; } if (((ones + parity) & 0x01) != 1) { return; } // 进入扫描码状态机 if (data == 0xE0) { ps2_ext_flag = 1; return; } if (data == 0xF0) { ps2_release_flag = 1; return; } // 查表得到键值,送入上层事件队列 ps2_key_event(ps2_keyboard_map(data), ps2_release_flag); ps2_ext_flag = 0; ps2_release_flag = 0; }校验逻辑里,起始位和停止位是硬条件,开场白就过滤掉一半的错帧。奇校验的实现要点是统计8个数据位中1的个数,加上校验位后,总数必须是奇数。这里最容易算反,后面避坑章节会专门展开。
扫描码到键值的映射表在工程里是一个常量数组,展示其中一小段:
uint8_t ps2_keyboard_map(uint8_t scancode) { switch (scancode) { case 0x1C: return 'A'; case 0x1B: return 'S'; case 0x23: return 'D'; case 0x2B: return 'F'; case 0x34: return 'G'; case 0x33: return 'H'; case 0x29: return ' '; case 0x5A: return '\r'; default: return 0; } }完整映射表覆盖全部按键,包括Shift、Ctrl、方向键和数字键盘,工程里按需补齐。重复帧是另一个容易被忽略的点:键盘在长按时会周期性重发通码,间隔约100毫秒。如果上层逻辑需要对重复按键去抖,可以在解析时记录每个键的最后事件时间戳,相同通码在500毫秒内再次到达就视为重复帧,不生成新事件。
4.3 鼠标解码:三字节数据包与增量位移
鼠标和键盘的帧格式一样,但逻辑上每三帧组成一个完整报告。第一字节是标志位,第二字节是X方向位移增量,第三字节是Y方向位移增量。位移是带符号的8位值,用二进制补码表示,范围是-128到127。
typedef struct { uint8_t buttons; // bit0左键 bit1右键 bit2中键 int8_t x; // X增量,右正左负 int8_t y; // Y增量,注意屏幕方向映射 } ps2_mouse_report_t; void ps2_mouse_parse(uint8_t *buf) { ps2_mouse_report_t report; // bit3必须为1,这是数据包的标识位 if ((buf[0] & 0x08) == 0) { return; } // bit7和bit6分别是X和Y溢出,溢出时丢弃当帧位移 if ((buf[0] & 0xC0) != 0) { return; } report.buttons = buf[0] & 0x07; report.x = (int8_t)buf[1]; report.y = (int8_t)buf[2]; // 鼠标坐标:Y轴正方向是向上,应用层做屏幕映射时注意翻转 ps2_mouse_handle(&report); }这里的字节序不需要翻转,因为三字节是按顺序接收的,第一帧是标志字节,第二帧是X,第三帧是Y。接收端要做的事是把三帧拼成一个报告。鼠标还有一个滚轮扩展协议,启用后每个报告多一字节滚轮增量,这个需要主机主动发送命令切换模式,放在第6章里说。
5. PS2解码避坑清单:5个让工程翻车的细节
5.1 中断回调里调用HAL_Delay,时钟边沿被吞
现象:键盘偶尔能解析出正确按键,但多数按键错乱,尤其是快速打字时丢键严重。
原因:在HAL_GPIO_EXTI_Callback里加入了延时或串口打印。PS2时钟位周期只有几十微秒,而HAL_Delay依赖SysTick中断,至少阻塞1毫秒,足够丢失好几个边沿。串口打印如果阻塞发送,同样致命。
解决:中断回调里只做状态机操作,所有耗时的解析、打印全部放到主循环。如果确实需要调试信息,用标志位通知主循环打印,或者用DMA串口发送。
5.2 奇校验算反,每一帧都被丢弃
现象:接收中断一直在触发,帧也收满了11位,但解析函数里校验始终失败,数据无一通过。
原因:对奇校验的理解偏差。奇校验要求数据位中1的个数加上校验位中1的个数等于奇数。数据位里1的个数是奇数时,校验位必须是0,这样总和才是奇数;数据位里1的个数是偶数时,校验位必须是1。很多实现写反,把校验位设置成和数据的奇偶性相同,导致校验永远报错。
解决:按前面解析代码里的方式实现:统计8个数据位中1的个数,加上校验位后取最低位,结果为1才算通过。单独写一个函数验证:用0x00、0xFF、0x1C几个已知扫描码逐个跑一遍,确认校验位逻辑正确后再接入解码流程。
5.3 时钟毛刺误启帧,状态机陷入错乱
现象:键盘闲置时也会产生错误帧,有时一个按键触发两三个事件,用示波器看时钟线有短脉冲毛刺。
原因:线缆过长或布局靠近电机、继电器等干扰源,GPIO边沿中断对毛刺非常敏感。一个毛刺下降沿会被当成起始位,之后的采样数据错乱,整个帧被污染。
解决:代码层面做最小脉冲宽度过滤,也就是4.1节里的20微秒阈值判断。硬件层面在时钟线和数据线上各加一个RC低通滤波器,电阻选1kΩ、电容选100pF左右,能吸收大部分高频毛刺。两个手段同时做,不要只靠软件。
5.4 上电第一帧丢失或收到0xAA
现象:设备刚供电时,前几个按键无响应,之后恢复正常;或者复位后收到的第一个字节固定是0xAA。
原因:键盘鼠标上电后会自动执行自检,自检完成后发送0xAA表示正常。如果主机在设备自检完成前就已进入正常接收状态,会把0xAA当成普通扫描码处理。设备自检期间时钟和数据线的状态也不稳定,容易产生半帧。
解决:初始化时按3.3节的流程,主动复位设备并延时200毫秒,然后清空接收缓冲区和状态机标志。把0xAA、0xFA这类设备应答码和普通扫描码在状态机里分开处理,应答码只走握手逻辑,不进按键映射。
5.5 3.3V供电板子接5V键鼠,时好时坏
现象:同样的解码代码,在一套板子上稳定运行,换一个供电设计后就频繁丢帧、错帧。
原因:PS2设备多数是5V供电,信号线是开漏结构。如果主机侧上拉电阻接的是3.3V,且阻值偏大,信号上升沿会变缓,时钟边沿的触发点在抖动区间里反复横跳,采样位置不稳定。部分老设备的输入阈值也需要更高电平才能可靠识别。
解决:时钟线和数据线外接2.2kΩ上拉电阻到3.3V,如果设备仍然不稳定,改用5V上拉,并在STM32的FT引脚侧加串联电阻限制电流。选型时优先选FT引脚,像PC6和PC7这类本身兼容5V的引脚,能少很多麻烦。
6. 进阶:双缓冲、滚轮模式与键盘LED命令
6.1 双缓冲帧管理,解码不丢帧
如果接收中断速度很快而主循环处理不过来,会出现上一帧还没处理完、下一帧已经把ps2_rx_buf覆盖的情况。常见做法是用双缓冲:中断写一个缓冲,主循环读另一个缓冲,处理完后交换。实现上可以简化,把ps2_rx_done当作可重入标志,主循环检测到置位后立刻把ps2_rx_buf拷贝到本地变量,再清标志。这个拷贝操作只要几个指令周期,就能避免覆盖。
if (ps2_rx_done) { uint16_t frame = ps2_rx_buf; ps2_rx_done = 0; // 先清标志再处理,避免中断又写同一帧 ps2_keyboard_process(frame); }先拷贝、再清标志、最后处理,这个顺序是刻进肌肉记忆的。反过来会丢数据。
6.2 主到设备命令:切换滚轮模式与键盘LED
向设备发送命令要走完整的主机发送时序。常见做法是写一个ps2_send_byte函数,发送前把时钟线和数据线都切换成开漏输出,发送完恢复成接收。滚轮鼠标的启用方式是依次写入采样率200、100、80,设备收到后进入滚轮模式。键盘LED控制的命令是0xED,后面跟一个LED状态字节。
编写这个函数时留意开漏输出的切换顺序:先把输出寄存器置高,再切模式,防止引脚瞬间输出低电平被设备误读为起始位。
void ps2_send_byte(uint8_t data) { uint8_t i, ones = 0, parity; for (i = 0; i < 8; i++) { if (data & (1 << i)) ones++; } parity = (ones % 2) ? 0 : 1; ps2_line_mode(1, 1); // CLK和DAT都切成开漏输出 HAL_GPIO_WritePin(PS2_CLK_PORT, PS2_CLK_PIN, GPIO_PIN_RESET); delay_us(120); // 拉低时钟申请总线 HAL_GPIO_WritePin(PS2_DATA_PORT, PS2_DATA_PIN, GPIO_PIN_RESET); delay_us(20); // 起始位 for (i = 0; i < 8; i++) { // 每个位:等待时钟低电平,设置数据线,等待时钟高电平 while (HAL_GPIO_ReadPin(PS2_CLK_PORT, PS2_CLK_PIN) == GPIO_PIN_SET); HAL_GPIO_WritePin(PS2_DATA_PORT, PS2_DATA_PIN, (data & (1 << i)) ? GPIO_PIN_SET : GPIO_PIN_RESET); while (HAL_GPIO_ReadPin(PS2_CLK_PORT, PS2_CLK_PIN) == GPIO_PIN_RESET); } // 校验位 while (HAL_GPIO_ReadPin(PS2_CLK_PORT, PS2_CLK_PIN) == GPIO_PIN_SET); HAL_GPIO_WritePin(PS2_DATA_PORT, PS2_DATA_PIN, parity ? GPIO_PIN_SET : GPIO_PIN_RESET); while (HAL_GPIO_ReadPin(PS2_CLK_PORT, PS2_CLK_PIN) == GPIO_PIN_RESET); // 停止位,释放数据线 HAL_GPIO_WritePin(PS2_DATA_PORT, PS2_DATA_PIN, GPIO_PIN_SET); delay_us(20); ps2_line_mode(0, 0); // 切回接收模式 }这套发送逻辑我在实际调试中确认过:起始位后的第一个等待时钟低电平,对应设备接管时钟后的第一个脉冲,主机在这个低电平期间设置D0,上升沿时设备采样。停止位释放数据线后设备会拉低应答,这里没有再读ACK,工程够用,严格场景可以补一个等待设备拉低数据线的循环。
6.3 用串口打印和逻辑分析仪做验收
验收时先把原始帧以十六进制打印出来,对照协议逐一核对位序和校验。我用串口助手连上USART1,在解析函数里临时加一行打印帧值和校验结果。全部字节符合预期后,再关掉调试打印跑通链路。逻辑分析仪抓时钟和数据线,确认位宽度、起始位和停止位的位置,这一步能快速定位毛刺和采样点偏移。回头总结一条教训:做PS2解码,先接逻辑分析仪看波形,再动手写代码,能省掉一半的扯皮时间。希望帮到你。
本文还有配套的精品资源,点击获取