├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md └── read_SENT.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | 19 | *.rb linguist-language=Kicad 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | 49 | # Directories for Blog Graphics 50 | BlogGraphics 51 | # KC_NixieSupply5vTo170v 52 | 53 | # LTSPICE Raw files 54 | *.raw 55 | 56 | #Kicad files 57 | *.bak 58 | *.kicad_pcb-bak 59 | 60 | # python backup files 61 | *.pyc 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mark Smith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Class for Reading Single Edge Nibble Transmission (SENT) using the Raspberry Pi 2 | 3 | A full description of this Python script is described at [www.surfncircuits.com](https://surfncircuits.com) in the blog entry: [Implementing a Single Edge Nibble Transmission (SENT) protocol in Python for the Raspberry Pi Zero](https://surfncircuits.com/?p=3725) 4 | 5 | 6 | This python library will read a Raspberry Pi GPIO pin connected. Start the pigpiod daemon with one microsecond sampling to read SENT transmissions with three microsecond tick times. 7 | 8 | ## To start the daemon on Raspberry Pi 9 | - sudo pigpiod -s 1 10 | 11 | ## SENT packet frame summary 12 | 13 | - Sync Pulse: 56 ticks 14 | - 4 bit Status and Message Pulse: 17-32 ticks 15 | - 4 bit (9:12) Data1 Field: 17-32 ticks 16 | - 4 bit (5:8) Data1 Field: 17-32 ticks 17 | - 4 bit (1:4) Data1 Field: 17-32 ticks 18 | - 4 bit (9-12) Data2 Field: 17-32 ticks 19 | - 4 bit (5-8) Data2 Field: 17-32 ticks 20 | - 4 bit (1-4) Data2 Field: 17-32 ticks 21 | - 4 bit CRC: 17-32 ticks 22 | 23 | 24 | ## requirements 25 | [pigpiod](http://abyz.me.uk/rpi/pigpio/) library required 26 | 27 | ## To run the script for a signal attached to GPIO BCD 18 (pin 12) 28 | - python3 sent_READ.py 29 | -------------------------------------------------------------------------------- /read_SENT.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # read_PWM.py 4 | # Public Domain by mark smith, www.surfncircuits.com 5 | # blog:https://surfncircuits.com/2020/11/27/implementing-a-single-edge-nibble-transmission-sent-protocol-in-python-for-the-raspberry-pi-zero/ 6 | 7 | import time 8 | import pigpio # http://abyz.co.uk/rpi/pigpio/python.html 9 | import threading 10 | 11 | class SENTReader: 12 | """ 13 | A class to read short Format SENT frames 14 | (see the LX3302A datasheet for a SENT reference from Microchip) 15 | (also using sent transmission mode where ) 16 | from wikiPedia: The SAE J2716 SENT (Single Edge Nibble Transmission) protocol 17 | is a point-to-point scheme for transmitting signal values 18 | from a sensor to a controller. It is intended to allow for 19 | transmission of high resolution data with a low system cost. 20 | 21 | Short sensor format: 22 | The first is the SYNC pulse (56 ticks) 23 | first Nibble : Status (4 bits) 24 | 2nd NIbble : DAta1 (4 bits) 25 | 3nd Nibble : Data2 (4 bits) 26 | 4th Nibble : Data3 (4 bits) 27 | 5th Nibble : Data1 (4 bits) 28 | 6th Nibble : Data2 (4 bits) 29 | 7th Nibble : Data3 (4 bits) 30 | 8th Nibble : CRC (4 bits) 31 | """ 32 | def __init__(self, pi, gpio, Mode = 0): 33 | """ 34 | Instantiate with the Pi and gpio of the SENT signal 35 | to monitor. 36 | SENT mode = A0: Microchip LX3302A where the two 12 bit data values are identical. there are other modes 37 | 38 | """ 39 | self.pi = pi 40 | self.gpio = gpio 41 | self.SENTMode = Mode 42 | 43 | # the time that pulse goes high 44 | self._high_tick = 0 45 | # the period of the low tick 46 | self._low_tick = 0 47 | # the period of the pulse (total data) 48 | self._period = 0 49 | # the time the item was low during the period 50 | self._low = 0 51 | # the time the output was high during the period 52 | self._high = 0 53 | # setting initial value to 100 54 | self.syncTick = 100 55 | 56 | 57 | #keep track of the periods 58 | self.syncWidth = 0 59 | self.status = 0 60 | self.data1 = 0 61 | self.data2 = 0 62 | self.data3 = 0 63 | self.data4 = 0 64 | self.data5 = 0 65 | self.data6 = 0 66 | self.crc = 0 67 | #initize the sent frame . Need to use hex for data 68 | #self.frame = [0,0,0,'0x0','0x0','0x0','0x0','0x0','0x0',0] 69 | self.frame = [0,0,0,0,0,0,0,0,0,0] 70 | self.syncFound = False 71 | self.frameComplete = False 72 | self.nibble = 0 73 | self.numberFrames = 0 74 | self.SampleStopped = False 75 | 76 | self.pi.set_mode(gpio, pigpio.INPUT) 77 | 78 | #self._cb = pi.callback(gpio, pigpio.EITHER_EDGE, self._cbf) 79 | #sleep enougth to start reading SENT 80 | #time.sleep(0.05) 81 | self.ThreadStop = False 82 | #start thread to sample the SENT property 83 | # this is needed for piGPIO sample of 1us and sensing the 3us 84 | self.OutputSampleThread = threading.Thread(target = self.SampleCallBack) 85 | self.OutputSampleThread.daemon = True 86 | self.OutputSampleThread.start() 87 | 88 | 89 | 90 | #give time for thread to start capturing data 91 | time.sleep(.05) 92 | 93 | 94 | def SampleCallBack(self): 95 | 96 | # this will run in a loop and sample the SENT path 97 | # this sampling is required when 1us sample rate for SENT 3us tick time 98 | while self.ThreadStop==False: 99 | 100 | self.SampleStopped = False 101 | self._cb = self.pi.callback(self.gpio, pigpio.EITHER_EDGE, self._cbf) 102 | # wait until sample stopped 103 | while self.SampleStopped == False: 104 | #do nothing 105 | time.sleep(.001) 106 | 107 | # gives the callback time to cancel so we can start again. 108 | time.sleep(0.20) 109 | 110 | def _cbf(self, gpio, level, tick): 111 | # depending on the system state set the tick times. 112 | # first look for sync pulse. this is found when duty ratio >90 113 | #print(pgio) 114 | #print("inside _cpf") 115 | #print(tick) 116 | if self.syncFound == False: 117 | if level == 1: 118 | self._high_tick = tick 119 | self._low = pigpio.tickDiff(self._low_tick,tick) 120 | elif level == 0: 121 | # this may be a syncpulse if the duty is 51/56 122 | self._period = pigpio.tickDiff(self._low_tick,tick) 123 | # not reset the self._low_tick 124 | self._low_tick = tick 125 | self._high = pigpio.tickDiff(self._high_tick,tick) 126 | # sync pulse is detected by finding duty ratio. 51/56 127 | # but also filter if period is > 90us*56 = 5040 128 | if (100*self._high/self._period) > 87 and (self._period<5100): 129 | self.syncFound = True 130 | self.syncWidth = self._high 131 | self.syncPeriod = self._period 132 | #self.syncTick = round(self.syncPeriod/56.0,2) 133 | self.syncTick = self.syncPeriod 134 | # reset the nibble to zero 135 | self.nibble = 0 136 | self.SampleStopped = False 137 | else: 138 | # now look for the nibble information for each nibble (8 Nibbles) 139 | if level == 1: 140 | self._high_tick = tick 141 | self._low = pigpio.tickDiff(self._low_tick,tick) 142 | elif level == 0: 143 | # This will be a data nibble 144 | self._period = pigpio.tickDiff(self._low_tick,tick) 145 | # not reset the self._low_tick 146 | self._low_tick = tick 147 | self._high = pigpio.tickDiff(self._high_tick,tick) 148 | self.nibble = self.nibble + 1 149 | if self.nibble == 1: 150 | self.status = self._period 151 | elif self.nibble == 2: 152 | #self.data1 = hex(int(round(self._period / self.syncTick)-12)) 153 | self.data1 = self._period 154 | elif self.nibble == 3: 155 | self.data2 = self._period 156 | elif self.nibble == 4: 157 | self.data3 = self._period 158 | elif self.nibble == 5: 159 | self.data4 = self._period 160 | elif self.nibble == 6: 161 | self.data5 = self._period 162 | elif self.nibble == 7: 163 | self.data6 = self._period 164 | elif self.nibble == 8: 165 | self.crc = self._period 166 | # now send all to the SENT Frame 167 | self.frame = [self.syncPeriod,self.syncTick,self.status,self.data1,self.data2,self.data3,self.data4,self.data5,self.data6,self.crc] 168 | self.syncFound = False 169 | self.nibble = 0 170 | self.numberFrames += 1 171 | if self.numberFrames > 2: 172 | self.cancel() 173 | self.SampleStopped = True 174 | self.numberFrames = 0 175 | 176 | def ConvertData(self,tickdata,tickTime): 177 | if tickdata == 0: 178 | t = '0x0' 179 | else: 180 | t = hex(int(round(tickdata / tickTime)-12)) 181 | if t[0] =='-': 182 | t='0x0' 183 | return t 184 | 185 | def SENTData(self): 186 | # check that data1 = Data2 if they are not equal return fault = True 187 | # will check the CRC code for faults. if fault, return = true 188 | # returns status, data1, data2, crc, fault 189 | #self._cb = self.pi.callback(self.gpio, pigpio.EITHER_EDGE, self._cbf) 190 | #time.sleep(0.1) 191 | fault = False 192 | SentFrame = self.frame[:] 193 | SENTTick = round(SentFrame[1]/56.0,2) 194 | 195 | # the greatest SYNC sync is 90us. So trip a fault if this occurs 196 | if SENTTick > 90: 197 | fault = True 198 | 199 | #print(SentFrame) 200 | # convert SentFrame to HEX Format including the status and Crc bits 201 | for x in range (2,10): 202 | SentFrame[x] = self.ConvertData(SentFrame[x],SENTTick) 203 | SENTCrc = SentFrame[9] 204 | SENTStatus = SentFrame[2] 205 | SENTPeriod = SentFrame[0] 206 | #print(SentFrame) 207 | # combine the datafield nibbles 208 | datanibble = '0x' 209 | datanibble2 = '0x' 210 | for x in range (3,6): 211 | datanibble = datanibble + str((SentFrame[x]))[2:] 212 | for x in range (6,9): 213 | datanibble2 = datanibble2 + str((SentFrame[x]))[2:] 214 | # if using SENT mode 0, then data nibbles should be equal 215 | #if self.SENTMode == 0 : 216 | # if datanibble != datanibble2: 217 | # fault = True 218 | # if datanibble or datanibble2 == 0 then fault = true 219 | if (int(datanibble,16) == 0) or (int(datanibble2,16) ==0): 220 | fault = True 221 | # if datanibble or datanibble2 > FFF (4096) then fault = True 222 | if ( (int(datanibble,16) > 0xFFF) or (int(datanibble2,16) > 0xFFF)): 223 | fault = True 224 | #print(datanibble) 225 | # CRC checking 226 | # converting the datanibble values to a binary bit string. 227 | # remove the first two characters. Not needed for crcCheck 228 | InputBitString = bin(int((datanibble + datanibble2[2:]),16))[2:] 229 | # converting Crcvalue to bin but remove the first two characters 0b 230 | # format is set to remove the leading 0b, 4 charactors long 231 | crcBitValue = format(int(str(SENTCrc),16),'04b') 232 | #checking the crcValue 233 | # polybitstring is 1*X^4+1*X^3+1*x^2+0*X+1 = '11101' 234 | if self.crcCheck(InputBitString,'11101',crcBitValue) == False: 235 | fault = True 236 | 237 | # converter to decimnal 238 | returnData = int(datanibble,16) 239 | returnData2 = int(datanibble2,16) 240 | #returns both Data values and if there is a FAULT 241 | return (SENTStatus, returnData, returnData2,SENTTick, SENTCrc, fault, SENTPeriod) 242 | 243 | def tick(self): 244 | status, data1, data2, ticktime, crc, errors, syncPulse = self.SENTData() 245 | return ticktime 246 | 247 | def crcNibble(self): 248 | status, data1, data2, ticktime, crc, errors, syncPulse = self.SENTData() 249 | return crc 250 | 251 | def dataField1(self): 252 | status, data1, data2, ticktime, crc, errors, syncPulse = self.SENTData() 253 | return data1 254 | 255 | def dataField2(self): 256 | status, data1, data2, ticktime, crc, errors, syncPulse = self.SENTData() 257 | return data2 258 | 259 | def statusNibble(self): 260 | status, data1, data2, ticktime, crc, errors, syncPulse = self.SENTData() 261 | return status 262 | 263 | def syncPulse(self): 264 | status, data1, data2, ticktime, crc, errors, syncPulse = self.SENTData() 265 | return syncPulse 266 | 267 | def errorFrame(self): 268 | status, data1, data2, ticktime, crc, errors, syncPulse = self.SENTData() 269 | return errors 270 | 271 | def cancel(self): 272 | self._cb.cancel() 273 | 274 | def stop(self): 275 | self.ThreadStop == True 276 | 277 | def crcCheck(self, InputBitString, PolyBitString, crcValue ): 278 | # the input string will be a binary string all 6 nibbles of the SENT data 279 | # the seed value ( = '0101) is appended to the input string. Do not use zeros for SENT protocal 280 | # this uses the SENT CRC recommended implementation. 281 | checkOK = False 282 | 283 | LenPolyBitString = len(PolyBitString) 284 | PolyBitString = PolyBitString.lstrip('0') 285 | LenInput = len(InputBitString) 286 | InputPaddedArray = list(InputBitString + '0101') 287 | while '1' in InputPaddedArray[:LenInput]: 288 | cur_shift = InputPaddedArray.index('1') 289 | for i in range(len(PolyBitString)): 290 | InputPaddedArray[cur_shift + i] = str(int(PolyBitString[i] != InputPaddedArray[cur_shift + i])) 291 | 292 | if (InputPaddedArray[LenInput:] == list(crcValue)): 293 | checkOK = True 294 | 295 | return checkOK 296 | 297 | if __name__ == "__main__": 298 | 299 | import time 300 | import pigpio 301 | import read_SENT 302 | 303 | SENT_GPIO = 24 304 | RUN_TIME = 6000000000.0 305 | SAMPLE_TIME = 0.1 306 | 307 | pi = pigpio.pi() 308 | 309 | p = read_SENT.SENTReader(pi, SENT_GPIO) 310 | 311 | start = time.time() 312 | 313 | while (time.time() - start) < RUN_TIME: 314 | 315 | time.sleep(SAMPLE_TIME) 316 | 317 | status, data1, data2, ticktime, crc, errors, syncPulse = p.SENTData() 318 | print("Sent Status= %s - 12-bit DATA 1= %4.0f - DATA 2= %4.0f - tickTime(uS)= %4.2f - CRC= %s - Errors= %s - PERIOD = %s" % (status,data1,data2,ticktime,crc,errors,syncPulse)) 319 | print("Sent Stat2s= %s - 12-bit DATA 1= %4.0f - DATA 2= %4.0f - tickTime(uS)= %4.2f - CRC= %s - Errors= %s - PERIOD = %s" % (p.statusNibble(),p.dataField1(),p.dataField2(),p.tick(),p.crcNibble(),p.errorFrame(),p.syncPulse())) 320 | 321 | # stop the thread in SENTReader 322 | p.stop() 323 | # clear the pi object instance 324 | pi.stop() 325 | --------------------------------------------------------------------------------