最近在做用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