BLHeli 解析DShot信号

最近在做用fpga输出dshot信号, 发现输出的dshot信号电调死活识别不了,无奈下去看了下代码,才发现原来dshot信号要一直持续发送,否则电调会自己停下(可能退出dshot模式了?)。而我之前测试都是在linux发送一次命令,发送一次dshot信号的,这样就导致电调无法正常响应。而且电调初始化的时候,也要发送 cmd 0, 持续时间在300ms左右,解锁电调后,才能继续驱动电调。

不过既然都去看代码了,看都看了,不能浪费,稍微记录一下BLHeli是怎么识别dshot信号的。由于c51的汇编我也第一次看,好多地方还得现查。c51汇编可以在这里查看:https://www.win.tue.nl/~aeb/comp/8051/set8051.html

首先是定时器的初始化,timer0 和 1 都运行在8bit 模式

    ; Setup timers for DShot
    mov IT01CF, #(80h+(RTX_PIN SHL 4)+(RTX_PIN))    ; Route RCP input to INT0/1, with INT1 inverted
    mov TCON, #51h      ; Timer 0/1 run and INT0 edge triggered
    mov CKCON0, #01h        ; Timer 0/1 clock is system clock divided by 4 (for DShot150)
    mov TMOD, #0AAh     ; Timer 0/1 set to 8bits auto reload and gated by INT0
    mov TH0, #0         ; Auto reload value zero
    mov TH1, #0

然后他是通过int0来记录每个dshot bit的高电平脉宽的(int0 似乎是某种特殊用的中断?不太清楚):

int0_int:   ; Used for RC pulse timing
    push    ACC
    mov A, TL0          ; Read pwm for DShot immediately
    ; Test for DShot
    jnb Flags2.RCP_DSHOT, int0_int_not_dshot

    mov TL1, DShot_Timer_Preset ; Reset sync timer
    movx    @DPTR, A            ; Store pwm
    inc DPTR       ; DPTR是数据指针, 后面会用这个指针挨个把数据取出来识别
    pop ACC
    ret

然后是timer1 的中断,在这里已经开始解析了:

t1_int:
    clr     IE_EA
    clr IE_EX0          ; Disable int0 interrupts
    anl EIE1, #0EFh     ; Disable pca interrupts
    clr TCON_TR1            ; Stop timer 1
    mov TL1, DShot_Timer_Preset ; Reset sync timer
    push    PSW
    setb    PSW.3           ; Select register bank 1 for this interrupt
    push    ACC
    push    B               ; Will be poped by int0 exit
    clr TMR2CN0_TR2     ; Timer 2 disabled
    mov Temp1, TMR2L        ; Read timer value
    mov Temp2, TMR2H
    setb    TMR2CN0_TR2     ; Timer 2 enabled
    setb    IE_EA
    ; Reset timer 0
    mov TL0, #0
    ; Check frame time length
    clr C
    mov A, Temp1
    subb    A, DShot_Frame_Start_L
    mov Temp1, A
    mov A, Temp2
    subb    A, DShot_Frame_Start_H
    mov Temp2, A
    ; Divide by 2 (or 4 for 48MHz). Unit is then us
    ; 注意,这里会将TEMP2 和TEMP1的单位转化为us, 实际上TIMER2 只是用来记录一整个dshot帧的长度,并判断是否超出限度而已,跟具体的bit 无关
    clr C
    mov A, Temp2
    rrc A
    mov Temp2, A
    mov A, Temp1
    rrc A
    mov Temp1, A
    mov A, Clock_Set_At_48MHz
    jz  t1_int_frame_time_scaled

    clr C
    mov A, Temp2
    rrc A
    mov Temp2, A
    mov A, Temp1
    rrc A
    mov Temp1, A

接下来就是判断帧是不是太长或者太短了:

t1_int_frame_time_scaled:
    mov A, Temp2
    jnz t1_int_msb_fail ; Frame too long
    mov A, Temp1
    subb    A, DShot_Frame_Length_Thr
    jc  t1_int_msb_fail ; Frame too short
    subb    A, DShot_Frame_Length_Thr
    jnc t1_int_msb_fail ; Frame too long

    ; Check that correct number of pulses is received
    mov A, DPL          ; Read current pointer
    cjne    A, #16, t1_int_msb_fail

    ; Decode transmitted data
    mov Temp5, #0           ; Reset timestamp
    mov Temp4, #0           ; High byte of receive buffer
    mov Temp3, #0           ; Low byte of receive buffer
    mov Temp2, #8           ; Number of bits per byte
    mov DPTR, #0            ; Set pointer
    mov Temp1, DShot_Pwm_Thr; DShot pulse width criteria
    mov A, Clock_Set_At_48MHz
    jnz t1_int_decode

    clr C
    mov A, Temp1            ; Scale pulse width criteria
    rrc A
    mov Temp1, A

t1_int_decode:
    ajmp    t1_int_decode_msb

然后开始解析dshot 的高8位了:

t1_int_decode_msb:
    ; Decode DShot data Msb. Use more code space to save time (by not using loop)
    Decode_DShot_2Msb
    Decode_DShot_2Msb
    Decode_DShot_2Msb
    Decode_DShot_2Msb
    ajmp    t1_int_decode_lsb

为了节省篇幅,低8位就不写了,差别不大。这里他提到,为了省电时间,他连循环都不要了,手动展开了。一个Decode_DShot_2Msb 可以解析2个bit,因此调用了4次。

具体的bit 解析过程:

Decode_Dshot_2Msb MACRO
    movx    A, @DPTR
    mov Temp6, A
    clr C
    subb    A, Temp5                    ; Subtract previous timestamp
    ; 减去上一个bit的时间戳,这样减完才是真正的脉宽
    clr C
    subb    A, Temp1 ; TEMP1 是一个常量DShot_Pwm_Thr,表示Dshot的最小脉宽,不同的Dshot频率这个值不一样。根据计算,在DShot 600下,这个值是16, 还要乘以每个定时器count 的时间。 因此是16 * 1/48M, 大概是333ns
    jc  t1_int_msb_fail         ; Check that bit is longer than minimum

    subb    A, Temp1                    ; Check if bit is zero or one
    ; 这里又减了一次DShot_Pwm_Thr, 相当于检查这个脉宽是否大于2 * DShot_Pwm_Thr, DShot 600 下也就是666 ns左右
    mov A, Temp4                    ; Shift bit into data byte
    rlc A ; 如果小于666ns,也就是dshot信号的0, 这里把这一位置为1,这样会导致最后得出的结果是反的,因此最后应该需要取反
    mov Temp4, A
    inc DPL                     ; Next bit
    movx    A, @DPTR
    mov Temp5, A
    clr C
    subb    A, Temp6
    clr C
    subb    A, Temp1
    jc  t1_int_msb_fail

    subb    A, Temp1
    mov A, Temp4
    rlc A
    mov Temp4, A
    inc DPL

后面校验完CRC有提到需要invert,具体代码就不解析了,不是很重要:

t1_int_xor_ok:
    ; Swap to be LSB aligned to 12 bits (and invert)
    mov A, Temp4
    cpl A
    ...

最后再看一下Dshot 600检测的一些常量是怎么设置的:

    ; Setup variables for DShot600
    mov CKCON0, #0Ch                    ; Timer 0/1 clock is system clock (for DShot600) 设置完这个分频系数后,TIMER0 和TIMER1 就都是sysclk(48MHz)
IF MCU_48MHZ >= 1
    mov DShot_Timer_Preset, #128            ; Load DShot sync timer preset (for DShot600)
    ; 这里其实我看的不是很懂, 设置这个Preset,其实就是设置定时器的Count值,本来人家是8bit 定时器,这样一设就变成只剩7bit了。考虑到48Mhz的频率,似乎是将溢出的时间控制在128 * 1/48M = 2.6us 的样子,但是我没找到这个溢出时间有什么玄机。
ELSE
    mov DShot_Timer_Preset, #192
ENDIF
    mov DShot_Pwm_Thr, #20              ; Load DShot qualification pwm threshold (for DShot600) 这个似乎是用来解析是不是Dshot600的,正常解析bit 不用他(我猜的)
    mov DShot_Frame_Length_Thr, #20     ; Load DShot frame length criteria 帧长度判断,单位 us
    ; Test whether signal is DShot600
    mov Rcp_Outside_Range_Cnt, #10      ; Set out of range counter
    call wait100ms                      ; Wait for new RC pulse
    mov DShot_Pwm_Thr, #16              ; Load DShot regular pwm threshold 16 对应的时间为 16/48M = 333ns
    clr C
    mov A, Rcp_Outside_Range_Cnt            ; Check if pulses were accepted
    subb    A, #10
    mov     Dshot_Cmd, #0
    mov     Dshot_Cmd_Cnt, #0
    jc  validate_rcp_start

发表评论