多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

曼彻斯特与差分曼彻斯特编码:从原理推导到STM32实现

曼彻斯特与差分曼彻斯特编码:从原理推导到STM32实现 数字信号编码这块内容我在做嵌入式通信和工业总线的时候踩过不少坑。最早接触曼彻斯特编码是因为一个STM32项目里需要把传感器数据通过单根信号线传出去同时还要让接收端能自己恢复时钟——当时试过NRZ编码结果接收端时钟漂移严重数据错得一塌糊涂。后来换成曼彻斯特编码问题迎刃而解。但真正把曼彻斯特和差分曼彻斯特的推导过程吃透是在一次需要手动解码RFID信号的时候那时候没有现成的解码芯片只能用MCU的定时器捕获边沿然后靠软件还原比特流。也就是从那时候起我才真正理解了这两种编码方式在波形层面的每一个细节。这篇文章我会从最基础的推导开始把曼彻斯特编码和差分曼彻斯特编码的每一个电平跳变、每一个比特周期讲清楚。不管你是刚开始学通信原理的学生还是需要在STM32上实现软件编码的工程师或者是在做RFID、以太网、工业总线解码的从业者这篇内容都能给你一套可以直接复现的完整方案。我会把推导过程、代码实现、示波器实测波形、以及常见问题排查都串起来讲尽量做到你看完就能动手。1. 从NRZ到曼彻斯特为什么需要自同步编码1.1 NRZ编码的致命缺陷NRZ也就是不归零编码是最直观的数字信号编码方式。高电平代表1低电平代表0一个比特周期内电平保持不变。这种编码方式实现简单频谱效率高在短距离、有时钟线的通信中非常常见比如SPI、UART加上起始位和停止位后都在用类似思路。但NRZ有一个致命问题没有自同步能力。所谓自同步就是接收端能从数据信号本身提取出时钟信息而不需要额外的一根时钟线。NRZ信号里如果连续发送多个相同的比特比如连续八个1信号线会一直保持高电平接收端根本不知道每一位的边界在哪里。时间一长收发两端的时钟如果有微小偏差累积起来就会导致采样点偏移最终解码错误。我当初在STM32上做单线通信时就遇到了这个问题。发送端用72MHz主频分频出来的波特率接收端用内部RC振荡器两者偏差大概在2%左右。发送端连续发了32个0接收端到后面几位就开始错位读出来的数据完全不对。这就是NRZ在没有独立时钟线时的硬伤。1.2 曼彻斯特编码的核心思想曼彻斯特编码的思路很巧妙把时钟信息嵌入到数据波形里。具体做法是在每个比特周期的中间时刻强制产生一次电平跳变。这个跳变既承载了数据信息又给接收端提供了时钟参考。标准曼彻斯特编码的约定是比特1从高电平跳到低电平也就是前半周期高、后半周期低比特0从低电平跳到高电平也就是前半周期低、后半周期高这样一来无论数据内容是什么每个比特周期中间一定有一次跳变。接收端只要检测到这个中间跳变就能锁定比特边界从而实现自同步。这个中间跳变我习惯叫它“时钟跳变”而比特边界处的跳变叫“数据跳变”——不过要注意在标准曼彻斯特编码里比特边界处不一定有跳变取决于相邻两个比特的值。1.3 自同步带来的代价与收益自同步不是免费的。曼彻斯特编码的跳变频率是NRZ的两倍这意味着信号带宽翻倍。换句话说同样的数据速率曼彻斯特编码需要更大的信道带宽。在以太网早期标准里10Mbps的曼彻斯特编码实际占用的带宽相当于20MHz的方波频率成分这就是代价。但收益也很明显只需要一根信号线接收端就能恢复时钟和数据信号的平均直流分量为零适合变压器耦合和AC耦合的传输链路跳变频繁便于接收端做边沿检测和同步。所以在10BASE-T以太网、RFID的某些协议、红外遥控、以及一些工业现场总线里曼彻斯特编码都被广泛采用。注意曼彻斯特编码的“1”和“0”定义并不是唯一的。有些协议规定比特1是低到高比特0是高到低正好反过来。实际使用时必须和收发双方约定一致否则解出来的数据全是反的。我在第一次做RFID解码时就因为搞反了定义折腾了半天才发现。2. 曼彻斯特编码的完整推导与波形分析2.1 比特周期与跳变时刻的数学定义设比特周期为T码元速率为R1/T。在标准曼彻斯特编码中每个比特周期被分成两个相等的半周期每个半周期长度为T/2。中间跳变发生在tT/2时刻比特边界跳变发生在tT时刻也就是下一个比特的起始。用数学方式描述设第n个比特为b_nb_n∈{0,1}。标准曼彻斯特编码的信号s(t)在区间[nT,(n1)T)内可以表示为当b_n1时s(t)At∈[nT, nTT/2)s(t)-At∈[nTT/2, (n1)T)当b_n0时s(t)-At∈[nT, nTT/2)s(t)At∈[nTT/2, (n1)T)这里A是信号幅度。可以看到无论b_n取什么值在tnTT/2处一定有一次电平翻转。这就是自同步的物理基础。2.2 逐比特推导以序列1011001为例光看公式不够直观我拿一个具体序列来推。假设要发送的比特序列是1 0 1 1 0 0 1比特周期T1μs高电平为3.3V低电平为0V。按照标准曼彻斯特编码1高到低0低到高比特前半周期电平后半周期电平中间跳变方向边界处是否有跳变1高(3.3V)低(0V)下降沿起始无结束到下一比特0低(0V)高(3.3V)上升沿与前一比特结束电平相同无跳变1高(3.3V)低(0V)下降沿有跳变0V到3.3V1高(3.3V)低(0V)下降沿有跳变0V到3.3V0低(0V)高(3.3V)上升沿有跳变0V到0V不对前一比特结束是0V本比特起始是0V无跳变0低(0V)高(3.3V)上升沿有跳变3.3V到0V1高(3.3V)低(0V)下降沿有跳变3.3V到3.3V前一比特结束是3.3V本比特起始是3.3V无跳变这里需要仔细核对边界跳变。让我重新梳理比特1前半高后半低。结束电平是低(0V)。比特0前半低后半高。起始电平是低(0V)和前一比特结束电平相同所以边界处无跳变。结束电平是高(3.3V)。比特1前半高后半低。起始电平是高(3.3V)和前一比特结束电平相同边界处无跳变。结束电平是低(0V)。比特1前半高后半低。起始电平是高(3.3V)但前一比特结束是低(0V)所以边界处有跳变上升沿。结束电平是低(0V)。比特0前半低后半高。起始电平是低(0V)和前一比特结束相同边界无跳变。结束电平是高(3.3V)。比特0前半低后半高。起始电平是低(0V)但前一比特结束是高(3.3V)边界有跳变下降沿。结束电平是高(3.3V)。比特1前半高后半低。起始电平是高(3.3V)和前一比特结束相同边界无跳变。结束电平是低(0V)。所以边界跳变并不是每个比特都有而是取决于相邻比特的值。但中间跳变是每个比特都有的这是关键。2.3 波形特征与频谱含义把上面的序列画成波形你会看到一串宽度为T/2的方波每个比特中间必有一次翻转。从频谱角度看曼彻斯特编码的主瓣宽度是NRZ的两倍零点出现在2R处R是比特率。这意味着如果比特率是1Mbps曼彻斯特编码的第一零点在2MHz。这个频谱特性带来两个实际影响一是需要更宽的传输带宽二是不含直流分量。不含直流分量这点很重要因为很多传输介质比如变压器、电容耦合链路无法传递直流。NRZ如果连续发送长串的1或0信号会偏向一边导致耦合失效。曼彻斯特编码因为每个比特都有跳变平均直流为零所以天然适合AC耦合。2.4 用STM32定时器生成曼彻斯特波形在实际项目中我经常用STM32的定时器和GPIO来软件生成曼彻斯特波形。思路很简单用定时器产生T/2周期的中断在中断里根据当前比特和半周期位置翻转GPIO。下面是一段基于STM32 HAL库的伪代码展示核心逻辑// 假设定时器中断周期为T/2 volatile uint8_t bit_index 0; volatile uint8_t half_phase 0; // 0表示前半周期1表示后半周期 volatile uint8_t data_bits[8] {1,0,1,1,0,0,1,0}; volatile uint8_t total_bits 8; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim htim2) { uint8_t current_bit data_bits[bit_index]; if (half_phase 0) { // 前半周期 if (current_bit 1) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // 高 } else { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // 低 } half_phase 1; } else { // 后半周期 if (current_bit 1) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // 低 } else { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // 高 } half_phase 0; bit_index; if (bit_index total_bits) { bit_index 0; // 循环发送 } } } }这段代码的关键点在于定时器中断频率必须是比特率的两倍。比如你要发1Mbps定时器中断频率就是2MHz也就是每500ns进一次中断。在STM32F103上72MHz主频定时器预分频设为0自动重装载值为35就能得到2MHz中断72M/(351)2M。实际调试时中断服务程序的执行时间必须远小于500ns否则会丢中断。如果中断里操作太复杂建议用DMA加定时器的方式或者用硬件SPI加编码电路。实操心得软件生成曼彻斯特波形时中断优先级一定要设高而且中断里尽量只做GPIO翻转不要做复杂运算。我最早把数据数组的索引计算放在中断里结果因为除法运算耗时太长波形出现了抖动。后来改成查表法把每个比特对应的前半周期和后半周期电平预先算好存到数组里中断里直接查表输出波形就干净了。3. 差分曼彻斯特编码的推导与对比3.1 差分曼彻斯特的核心规则差分曼彻斯特编码和标准曼彻斯特编码最大的区别在于数据不再由跳变方向表示而是由比特起始处是否有跳变来表示。中间跳变依然保留只用于时钟同步。具体规则是每个比特周期的中间时刻一定有一次电平跳变和标准曼彻斯特一样比特的起始处如果和前一个比特的结束电平相比有跳变表示比特0如果没有跳变表示比特1注意这个“0”和“1”的定义也可以反过来取决于协议约定。有些协议规定有跳变是1无跳变是0。这里我采用常见的一种约定起始有跳变0起始无跳变1。差分曼彻斯特的好处是极性无关。也就是说如果你把信号线反接或者传输过程中信号被反相了解码结果依然正确。因为解码只看跳变的有无不看电平的高低。这在变压器耦合或差分传输中非常有用。3.2 逐比特推导同样以1011001为例为了和前面对比我还是用序列1 0 1 1 0 0 1比特周期T1μs。假设初始参考电平第一个比特开始前的电平为低(0V)。按照规则中间必跳变起始有跳变0起始无跳变1。比特起始是否有跳变起始电平前半周期电平后半周期电平结束电平1无跳变参考为低低(0V)低(0V)高(3.3V)高(3.3V)0有跳变高(3.3V)高(3.3V)低(0V)低(0V)1有跳变前一结束是低本比特起始无跳变才是1所以起始无跳变低(0V)低(0V)高(3.3V)高(3.3V)1起始无跳变高(3.3V)高(3.3V)低(0V)低(0V)0起始有跳变低(0V)低(0V)高(3.3V)高(3.3V)0起始有跳变高(3.3V)高(3.3V)低(0V)低(0V)1起始无跳变低(0V)低(0V)高(3.3V)高(3.3V)这里需要仔细核对。差分曼彻斯特的推导容易绕晕我建议用状态机的方式理解维护一个“当前电平”状态每个比特开始时根据比特值决定是否翻转当前电平0翻转1不翻转然后前半周期保持这个电平后半周期再翻转一次。用状态机重新推初始电平低(0V)比特1不翻转前半低后半高结束高比特0翻转前半低从高翻到低后半高结束高比特1不翻转前半高后半低结束低比特1不翻转前半低后半高结束高比特0翻转前半低从高翻到低后半高结束高比特0翻转前半低从高翻到低后半高结束高比特1不翻转前半高后半低结束低这个结果和上面的表格有出入因为表格里我手动判断起始跳变时容易出错。状态机方法更可靠。让我用状态机的结果为准。对比标准曼彻斯特的波形差分曼彻斯特的中间跳变依然存在但起始跳变的位置和方向不同。标准曼彻斯特的跳变方向直接对应比特值而差分曼彻斯特的跳变有无对应比特值。3.3 两种编码的对比表格特性标准曼彻斯特差分曼彻斯特数据表示中间跳变方向高到低1低到高0起始跳变有无有跳变0无跳变1中间跳变每个比特必有每个比特必有自同步能力有有极性敏感性敏感反接后数据全反不敏感反接后数据不变直流分量零零带宽需求约2倍比特率约2倍比特率实现复杂度简单稍复杂需要维护状态典型应用10BASE-T以太网、红外遥控Token Ring、某些RFID协议3.4 差分曼彻斯特的解码状态机实现差分曼彻斯特的解码比编码更需要小心。在MCU上实现时我通常用外部中断捕获边沿然后测量边沿之间的时间间隔来判断是中间跳变还是起始跳变。解码思路捕获第一个边沿记录时间戳捕获后续边沿计算与上一个边沿的时间差如果时间差接近T/2说明是中间跳变继续等待如果时间差接近T说明从上一个中间跳变到当前起始跳变之间没有中间跳变不对中间跳变每个比特都有所以边沿间隔要么是T/2中间到起始或起始到中间要么是T如果起始无跳变则上一个中间跳变到下一个中间跳变之间是T实际上差分曼彻斯特的边沿间隔只有两种T/2和T。当起始有跳变时边沿序列是起始跳变、中间跳变、起始跳变、中间跳变……间隔都是T/2。当起始无跳变时边沿序列是中间跳变、中间跳变……间隔是T。所以解码逻辑可以简化为测量相邻边沿的时间间隔。如果间隔是T/2说明这个比特起始有跳变比特0如果间隔是T说明这个比特起始无跳变比特1。但要注意第一个边沿可能是起始跳变也可能是中间跳变需要先同步。下面是一段基于STM32输入捕获的解码伪代码volatile uint32_t last_capture 0; volatile uint32_t current_capture 0; volatile uint32_t interval 0; volatile uint8_t decoded_bits[32]; volatile uint8_t bit_count 0; volatile uint8_t synced 0; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim htim3) { current_capture HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); if (last_capture ! 0) { interval current_capture - last_capture; // 假设T/2对应定时器计数值为HALF_TT对应FULL_T if (interval HALF_T * 0.75 interval HALF_T * 1.25) { // 间隔约T/2起始有跳变解码为0 if (synced) { decoded_bits[bit_count] 0; } } else if (interval FULL_T * 0.75 interval FULL_T * 1.25) { // 间隔约T起始无跳变解码为1 if (synced) { decoded_bits[bit_count] 1; } } else { // 间隔异常重新同步 synced 0; bit_count 0; } } last_capture current_capture; if (!synced bit_count 0) { synced 1; // 简化处理实际需要更严谨的同步逻辑 } } }这段代码只是示意实际应用中还需要处理定时器溢出、边沿抖动、以及同步头的识别。我在做RFID解码时通常会在数据前面加一个已知的同步头比如“10101010”接收端先检测同步头锁定比特边界后再开始解码有效数据。常见问题差分曼彻斯特解码时如果第一个边沿判断错误后面所有比特都会错位。解决办法是发送端在数据前加足够长的前导码接收端用前导码做同步。前导码一般用交替的0和1这样边沿间隔规律容易锁定。4. 实际项目中的编码选择与STM32实现细节4.1 什么时候选曼彻斯特什么时候选差分曼彻斯特这个问题我在不同项目里反复权衡过。总结下来如果传输链路极性固定收发双方共地标准曼彻斯特更简单编解码代码少CPU开销低。如果传输链路可能反相比如变压器耦合、差分线对、或者无线RFID差分曼彻斯特更稳妥。如果对EMI敏感两种编码的频谱差不多但差分曼彻斯特的跳变模式更均匀某些频点的能量分布更平滑。如果MCU资源紧张标准曼彻斯特的编码可以用硬件SPI加异或门实现差分曼彻斯特则需要软件维护状态开销稍大。我在一个工业传感器项目里传输线长达50米用的是RS-485差分传输。虽然RS-485本身有极性但现场接线时工人偶尔会把A、B线接反。用标准曼彻斯特时接反后数据全错换成差分曼彻斯特后接反也能正常解码现场调试时间大幅缩短。4.2 STM32硬件编码方案SPI加外部逻辑如果MCU的SPI外设支持可以用SPI的MOSI输出NRZ数据然后用一个异或门把时钟和数据异或得到曼彻斯特编码。具体电路是SPI的SCK和MOSI接到异或门两个输入异或门输出就是曼彻斯特编码。因为SPI在SCK的每个周期输出一位数据异或门在SCK为高时输出MOSI的反相SCK为低时输出MOSI的原相正好对应曼彻斯特的前半周期和后半周期。这个方案的优点是CPU几乎不参与编码速率可以很高。缺点是只能实现标准曼彻斯特差分曼彻斯特需要额外的逻辑。而且SPI的时钟极性需要配置正确否则编码会反。4.3 软件编码的定时器参数计算以STM32F103为例假设系统时钟72MHz要发送500kbps的曼彻斯特编码。比特周期T2μs半周期T/21μs。定时器中断频率需要是1MHz也就是每1μs进一次中断。定时器配置预分频器PSC71自动重装载值ARR0。定时器时钟72MHz/(711)1MHz计数周期1μs。这样每次更新中断就是1μs正好对应半周期。如果要发送1Mbps半周期500ns定时器需要2MHz中断。PSC35ARR0定时器时钟72MHz/362MHz。但500ns的中断间隔对STM32F103来说压力很大中断服务程序必须极短。我实测下来如果中断里只有GPIO翻转和简单的变量自增可以稳定运行。但如果加上数组索引和条件判断就会丢中断。这时候建议用DMA加定时器触发GPIO的方式或者换用更高主频的MCU。4.4 接收端解码的过采样与滤波接收端解码时如果直接用边沿中断信号上的毛刺会导致误触发。我通常会在GPIO输入前加一个RC低通滤波截止频率设为比特率的2到3倍。比如500kbps的曼彻斯特编码最高跳变频率是1MHzRC截止频率设为2MHz左右R1kΩC100pF时间常数100ns对1MHz信号影响不大但能滤掉几十纳秒的毛刺。另外用定时器输入捕获时可以开启输入滤波STM32的TIM输入捕获有数字滤波器可以设置采样频率和滤波长度。我一般设为采样频率f_DTS/4滤波长度8个采样点这样能有效抑制高频噪声。实操心得接收端解码时不要一检测到边沿就立即解码而是先测量边沿间隔如果间隔在合理范围内才认为是有效边沿。我最早做RFID解码时没有做间隔校验结果环境噪声导致大量误码。后来加了间隔窗口判断只有间隔在T/2的±25%或T的±25%范围内才接受误码率大幅下降。5. 常见问题与排查技巧实录5.1 波形抖动与中断丢失现象示波器上看曼彻斯特波形某些半周期宽度不均匀有的宽有的窄。原因定时器中断被更高优先级中断打断或者中断服务程序执行时间过长导致下一次中断响应延迟。排查用示波器同时观察GPIO翻转和中断标志或者用MCU的DWT计数器测量中断服务程序执行时间。如果执行时间接近半周期必须优化代码。解决把中断服务程序里的复杂运算移到主循环中断里只做标志置位和GPIO翻转。或者改用DMA方式让DMA在定时器触发下自动搬运数据到GPIO。5.2 解码错位与同步丢失现象接收端解码数据偶尔错位重新上电后又正常。原因接收端在数据流中间开始解码没有找到比特边界。排查检查发送端是否有前导码接收端的同步逻辑是否健壮。解决发送端在每帧数据前加至少8个比特的前导码比如“10101010”。接收端先检测前导码锁定边沿间隔规律后再开始解码。前导码的边沿间隔是固定的T/2很容易识别。5.3 极性接反导致数据全错现象标准曼彻斯特编码接收端解出来的数据全是发送数据的反码。原因传输线接反或者变压器耦合的极性反了。解决改用差分曼彻斯特编码或者在接收端加一个极性检测电路自动翻转。极性检测可以用前导码实现如果前导码解出来是“01010101”而不是“10101010”说明极性反了软件里把后续数据全部取反即可。5.4 常见问题速查表问题现象可能原因排查方法解决方案波形半周期不均匀中断延迟或丢失示波器观察中断响应优化中断代码改用DMA解码数据错位未同步到比特边界检查前导码和同步逻辑加前导码改进同步算法数据全反极性接反对比发送和接收数据改用差分曼彻斯特或软件取反误码率高噪声毛刺观察信号质量加RC滤波开启输入数字滤波高速时丢数据CPU处理不过来测量中断执行时间降低速率或换更高主频MCU长线传输失败反射和衰减测量眼图加终端匹配电阻降低速率5.5 一个真实的调试案例我在做一个红外遥控解码项目时遥控器用的是曼彻斯特编码比特率大概2kbps。一开始用STM32的输入捕获解码成功率只有70%左右。后来用示波器看波形发现红外接收头的输出信号在边沿处有大约50μs的振铃。这个振铃导致输入捕获触发了多次解码自然就乱了。解决办法是在软件里加一个“消隐时间”每次捕获到边沿后在接下来的100μs内忽略所有边沿。因为比特周期是500μs半周期250μs100μs的消隐不会影响正常边沿检测但能滤掉振铃。加上消隐后解码成功率到了99.9%。这个经验告诉我曼彻斯特解码的难点往往不在编码理论而在信号完整性和噪声处理。理论推导再清楚实际信号上的毛刺和振铃不解决照样解不出数据。6. 从理论到落地我的完整实现建议如果你要在STM32上从零实现曼彻斯特或差分曼彻斯特编解码我建议按这个顺序来第一步先用示波器或者逻辑分析仪确认发送端的波形。不要急着写接收端代码先把发送波形调对。发送端可以用定时器中断也可以用PWM加DMA。我通常先用最简单的定时器中断方式把波形调出来确认每个比特的中间跳变和起始跳变都符合预期。第二步用逻辑分析仪的协议解码功能验证。很多逻辑分析仪自带曼彻斯特解码你可以把发送波形接上去看解码结果是否和发送数据一致。这一步能快速验证编码逻辑。第三步写接收端解码。先用输入捕获测量边沿间隔把间隔数据打印出来人工分析。确认间隔只有T/2和T两种并且和发送数据对应。然后再写自动解码逻辑。第四步做压力测试。连续发送大量随机数据统计误码率。如果误码率高于预期检查信号质量和同步逻辑。第五步优化性能。如果速率要求高把软件编码改成DMA加定时器把软件解码改成DMA加边沿检测。如果MCU资源允许可以用硬件SPI加异或门实现标准曼彻斯特编码。最后分享一个小技巧在调试曼彻斯特编解码时我习惯在数据里插入一个已知的“指纹”序列比如每帧数据末尾加“11001100”。接收端解码后检查指纹如果指纹不对说明这一帧解码有误直接丢弃。这样能避免错误数据进入后续处理提高系统的鲁棒性。这个技巧在无线通信和长线传输里特别有用因为这两种场景误码率相对较高指纹校验能挡掉大部分坏帧。
返回列表