Hlavní navigace

Připojujeme se na sběrnici CAN z Linuxu

9. 8. 2022
Doba čtení: 12 minut

Sdílet

 Autor: Depositphotos
V tomto článku se dozvíte, jak se připojit na sběrnici CAN z linuxové příkazové řádky pomocí adaptéru USB-CAN Analyzer (V7.10). Podíváme se, jak sběrnice funguje, a pak s ní budeme pracovat.

Co je to CAN a jak funguje

CAN je zkratka z anglického výrazu Controller Area Network, což v praxi znamená, že je to síť, která pokrývá relativně malou vzdálenost. Na rozdíl od LAN, která je projektovaná na vzdálenosti řádově sto metrů, aby pokryla běžnou budovu, CAN byla vyvinuta německou firmou Bosch v osmdesátých letech minulého století, aby propojila řídicí jednotky v automobilu. Dnes v běžném automobilu najdeme dokonce několik nezávislých sítí CAN. Postupem času se CAN začala používat i mimo automobilový průmysl, dnes ji najdeme i v solárních elektrárnách, v lithiových bateriích pro solární elektrárny, v řídicích jednotkách elektromotorů a v mnoha dalších oblastech. V automobilech se používá k řízení světel, posílání informací o rychlosti a otáčkách do tachometru, posílání signálu z tlačítek na volantu do rádia a podobně.

Z elektrického hlediska je CAN velmi podobná lince RS485. Je tvořena dvěma diferenciálními signály v jediném krouceném páru. Má topologii bus se dvěma konci ukončenými terminály tvořenými odpory 120R. Linka nesmí mít odbočky, jakákoliv odbočka delší než pár desítek centimetrů může způsobit rušení odrazy.

Nevýhoda je samozřejmě v tom, že zkrat nebo přerušení kdekoliv na lince způsobí rozpad celé sítě, v lepším případě se slyší pouze zařízení v rámci neporušeného segmentu, v horším případě nikdo neslyší nikoho. Takže pozor, až budete experimentovat s CAN ve svém autě. Linka je, stejně jako RS485, typu half duplex, takže současně může vysílat jenom jedno zařízení, ovšem CAN už na hardwarové úrovni řeší opakování vysílání po konfliktu, priority zařízení a potvrzení o bezchybném vyslání paketu.

Linka CAN má dva stavy: Klidový a Aktivní. Logická nula se vysílá vybuzením linky do Aktivního stavu, logická jednička se nevysílá, takže linka je po dobu jedničky v Klidovém stavu. Pokud dvě zařízení vysílají současně jedničku, linka je stále v Klidovém stavu, na lince je jednička. Jestliže dvě zařízení vysílají obě současně nulu, je linka v Aktivním stavu a obě zařízení si myslí, že je všechno v pořádku. Pokud ovšem jedno vysílá jedničku a druhé nulu, na lince je Aktivní stav, t.j. nula a dojde ke konfliktu.

Ten, kdo vysílá nulu ruší vysílání toho, kdo vysílá jedničku. Ten, kdo vysílá, ovšem současně kontroluje, co se děje na lince. Pokud zjistí, že je tam nula místo jedničky, pochopí, že došlo ke konfliktu a okamžitě přestane vysílat. Ten druhý si ničeho nevšiml (vysílá nulu, je tam nula), takže jede dál, jako by se nic nestalo. Data na lince jsou platná, kdo přijímá si ničeho nevšimne. Ten, kdo si všiml kolize a přestal vysílat, vyšle automaticky svůj paket okamžitě, jak se uvolní linka.

Ovšem pozor – tento mechanismus funguje pouze v první části paketu, tam, kde se vysílá adresa. Pokud by současně vysílala dvě zařízení a obě by vysílala různé pakety ale se stejnou adresou, došlo by ke konfliktu v datové části paketu a tam už tento mechanismus nefunguje. Pravděpodobně je to chyba v návrhu standardu, ovšem dnes už s tím nikdo nic nenadělá. Je pouze potřeba si na to dávat zatraceně velký pozor při návrhu protokolu komunikace na CAN.

Tímto mechanismem se současně řeší priorita paketů. Jedna z prvních informací, které se vysílají, je adresa. Kdo má nižší adresu, bude v určitém okamžiku vysílat nulu, a vyhraje nad tím, kdo tam má vyšší adresu a má v tomto bitu jedničku. Ten bude muset svůj paket vyslat později.

Všichni, kdo jsou na lince připojeni, neustále poslouchají, co se na lince děje. Ten, kdo vysílá, těsně po posledním vyslaném bitu nastaví linku do Klidového stavu a očekává potvrzení. Kdokoliv, kdo slyšel platný paket, t.j. paket, ve kterém je platné CRC, v tomto okamžiku pošle na linku logickou nulu, tím ji vybudí do Aktivního stavu a vysílající zařízení dostane potvrzení, že data odešla v pořádku.

Tohle je velmi důležitý detail – paket není potvrzen příjemcem. Příjemců totiž může být několik nebo taky nikdo. Vysílající zařízení dostane jen signál, že paket byl „čitelný“, že „byl na drátě“, ovšem není jisté, jestli ten, komu byl určen, ho zachytil. Je to obdoba UDP broadcastu. Pokud je na lince CAN zařízení samo a nikdo mu nepotvrzuje pakety, bude vysílat do zblbnutí stále dokola ten samý paket. Pak půjde do chyby, z ní se bude pár set milisekund vzpamatovávat, načež znovu začne vysílat stále dokola ten samý paket.

Stejně jako v případě UDP, paket může být určen více příjemcům. Například signál pro rozsvícení světel v automobilu – stejný paket, který vyslalo tlačítko pro zapnutí světel je přijat dvěma jednotkami v zadní části automobilu, jedna rozsvítí levé zadní světlo, druhá rozsvítí pravé zadní světlo. Další dvě jednotky vpředu rozsvítí přední světla.

Pakety bývají vysílány periodicky, takže i v případě, že některá jednotka paket neslyšela, rozsvítí své světlo o pár desítek až stovek milisekund později. Z tohoto důvodu brzdová světla nesmí být řízena přes CAN, ale pěkně drátem přímo od spínače na pedálu do žárovky. Na brzdách je každá milisekunda dobrá a drát je podstatně spolehlivější než dvě řídicí jednotky a jedna sdílená sběrnice po cestě.

Detailnější popis fungování CAN by vydal na celý článek, v případě zájmu bych tuto problematiku mohl rozvést do podrobností příště.

Připojujeme se na CAN

Po tomto relativně nudném úvodu se konečně dostaneme k jádru věci – jak se na tuto sběrnici připojíme z Linuxu. Na trhu existuje několik typů rozhraní. Nejběžnější jsou adaptéry Bluetooth, které se připojí přímo do zásuvky OBD2 v autě.

Bluetooth to CAN adapter ELM327

Autor: eBay.com

Adaptér ELM327 je dobře zdokumentován, na Internetu je snadné najít seznam příkazů pro jeho ovládání [PDF]. Pokud si s ním chceme začít hrát, stačí jakýkoliv emulátor terminálu, například Minicom.

Existuje spousta programů pro Android pro monitorování provozu na CAN, které jsou napsány přímo pro tento nebo podobný adaptér, například Car Scanner ELM OBD2.

USB-CAN Analyzer (V7.10)

Autor: Sparkfun.com

Ovšem mým cílem bylo připojit se na sběrnici CAN z normálního PC a to po drátě. Potřeboval jsem vyvinout zařízení připojené na CAN a k monitorování a ladění je nejlepší příkazový řádek. Různé klikací programy jsou sice krásné a pohodlné, když se někdo chce jenom podívat, co se mu děje v autě, ale rozhodně se nehodí na ladění v laboratoři.

Volba padla na jednoduchý čínský model USB-CAN Analyzer (V7.10). Je to malá krabička velikosti přibližně 50×30×15 mm, která má na jedné straně konektor mini USB a na druhé straně odpojitelnou třípinovou svorkovnici, na které je zem a dva signály CAN. Pak už je tam jenom jumper, kterým se dá zapnout nebo vypnout terminátor a dvě LED, jedna blikne při vysílání a druhá při příjmu paketu. Dá se koupit v Číně za řádově 25 eur nebo v Evropě (také od Číňana) za řádově dvojnásobek.

USB-CAN Analyzer se na USB hlásí jako USB seriálka, v device je vidět jako  /dev/ttyUSB0.

Bus 003 Device 055: ID 1a86:7523 QinHeng Electronics HL-340 USB-Serial adapter

K Analyzeru se dá na internetu najít program, ovšem jen pro Windows a pouze klikací. Nic, co by bylo použitelné pro normální práci. Ovšem díky tomuto programu a pár desítkám minut analýzy komunikace mezi programem a Analyzerem pomocí mocného nástroje Wireshark se podařilo najít všechny potřebné informace k tomu, abychom mohli zapomenout na Windows, zapomenout na otřesně vypadající čínský klikací program a užít si Analyzátoru přímo z oblíbeného čistého command line.

Komunikační protokol USB-CAN Analyzer (V7.10)

Komunikační protokol je celkem jednoduchý. Sériová linka musí být nastavená na 2MBd N81. Před použitím je potřeba nastavit rychlost sběrnice CAN, typ frame (standard nebo extended), případné filtry, pokud nechceme přijímat pakety ze všech adres a pár dalších drobností. Všechny parametry se nastavují jediným paketem vyslaným na seriálku.

Formát konfiguračního paketu:

offset obsah délka popis
0 AA 55 2 bytes začátek paketu
2 12 1 byte délka paketu mimo AA 55
3 SPEED 1 byte rychlost CAN, viz tabulka
4 FRAME TYPE 1 byte 1 standard, 2 extended
5 FILTER 4 bytes filter adres, které cheme přijímat. Nejméně významný byte je první (jako Intel)
9 MASK 4 bytes maska pro filter. Kde je 0, bit filtru se ignoruje.
13 MODE 1 byte 0 normal, 1 loopback, 2 silent (*1), 3 loopback+silent
14 SEND_ONCE 1 byte 0 normal, 1 „only send once“ (*2)
15 00 00 00 00 4 bytes pravděpodobně nepoužito, čínský program posílal nuly
19 CHECKSUM 1 byte aritmetická suma všech bytes kromě počátečních AA 55 modulo 256

Tabulka rychlostí CAN:

1 = 1M 2 = 800k 3 = 500k 4 = 400k
5 = 250k 6 = 200k 7 = 125k 8 = 100k
9 = 50k A = 20k B = 10k C = 5k

Poznámky:

  1. nepotvrzuje pakety, chová se, jako by tam nebyl
  2. pravděpodobně nečeká na potvrzení paketu, nebude opakovat při chybě (nezkoušel jsem)

Příklad nastavení rychlosti CAN na 250kbaud, standardní frame, normální mód, žádná maska:

aa 55 12 05 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 18

Formát datového paketu:

offset obsah délka popis
0 AA 1 byte začátek paketu
1 frame a délka 1 byte první nibble je typ frame: C standard, D remote, E extended, F remote extended Druhý nibble je počet datových bytes
2 ADDR 2 bytes adresa, méně významný byte je první
4 DATA 1–8 bytes data
4+len(DATA) 55 1 byte konec paketu

Příklad datového paketu:

aa c6 23 01 01 23 45 67 89 ab 55

Vstupní i výstupní pakety mají stejný formát.

Jednoduchý program pro komunikaci

Když už známe komunikační protokol, nezbývá než si napsat jednoduchý prográmek na monitorování komunikace a na vyslání paketu. Sice nejsem přítelem hadů, ale Python se mi jevil jako nejschůdnější řešení tohoto problému. Nechci dělat nic komplikovaného, stačí mi program, ze kterého poleze to, co se děje na lince obohacené pouze o timestamp a jednoduchý program, kterým mohu na linku poslat jakýkoliv paket.

Program canmonitor

Program inicializuje sériovou linku na příslušnou rychlost (2MBaud) a pošle na ni konfigurační paket pro nastavení parametrů linky CAN. Pak jen v nekonečném cyklu čeká na pakety a vypisuje je současně s časem příjmu. Pro vytvoření jednoduchého klienta je dostatečné spustit tento monitor, jeho výstup přesměrovat na grep a při příchodu očekávaného paketu spustit příslušnou operaci. Nebo přidat pár testů do tohoto programu a přes os.system() nebo subprocess.call() spustit skript, který bude reagovat na očekávaný paket.

#!/usr/bin/python3

import sys
from datetime import datetime
import serial

if len(sys.argv)<3:
  print("Usage: canmonitor <serial port> <CAN speed> [extframe=0] [filter (hex) =00000000] [mask (hex) =00000000] [mode=0] [send_once=0]")
  print("Example: canmonitor /dev/ttyUSB0 500000")
  sys.exit(1)

def asHex(str):
  return ' '.join(format(x, '02x') for x in str)

def init_can(ser, speed, extframe=0, filter=0, mask=0, mode=0, send_once=0):
  speed = int(speed)
  class Packet:
    checksum=0x12
    data=b'\xaa\x55\x12'

    def add(self, x):
      self.checksum+=x
      self.data+=bytes([int(x)])

    def addInt(self, x):
      for i in range(4):
        self.add(x % 256)
        x=x//256

  packet=Packet()
  try:
    sp=[1000000,800000,500000,400000,250000,200000,125000,100000,50000,20000,10000,50000].index(speed)+1
  except ValueError:
    print("invalid CAN speed, allowed speeds are: 1000000, 800000, 500000, 400000, 250000, 200000, 125000, 100000, 50000, 20000, 10000, 50000")
    sys.exit(1)
  packet.add(sp)
  packet.add(2 if int(extframe)!=0 else 1)
  packet.addInt(filter)
  packet.addInt(mask)
  packet.add(mode)
  packet.add(send_once)
  packet.addInt(0)
  packet.add(packet.checksum % 256)
#  print("init: "+asHex(packet.data))
  ser.write(packet.data)

class ReadPacket:
  def __init__(self, ser):
    self.ser=ser

  def read(self):
    self.buffer=b''
#    while self.ser.in_waiting>0:
    while True:
      if len(self.buffer)==0 and self.ser.read()!=b'\xaa':
        continue
      self.buffer+=self.ser.read(1)
      if len(self.buffer)>0 and len(self.buffer)==self.buffer[0]%0x10+4:
#        print("got: "+self.buffer.hex())
        if self.buffer[-1]==0x55:
          return self.buffer[:-1]
        self.buffer=b''
    return False

  def sender(self): return self.buffer[2]*256+self.buffer[1]
  def data(self):   return self.buffer[3:-1]

# main

with serial.Serial(sys.argv[1], 2000000) as ser: # yes, 2Mbaud :-)
  init_can(ser, sys.argv[2], int(sys.argv[3]) if len(sys.argv)>3 else 0, int(sys.argv[4],16) if len(sys.argv)>4 else 0, int(sys.argv[5],16) if len(sys.argv)>5 else 0, int(sys.argv[6]) if len(sys.argv)>6 else 0, int(sys.argv[7]) if len(sys.argv)>7 else 0)
  pack=ReadPacket(ser)
  while True:
    pack.read()
    timestr=datetime.now().strftime("%H:%M:%S.%f")[:-3]
    print("%s: %04x: %s" % (timestr, pack.sender(), asHex(pack.data())))
    sys.stdout.flush()
    # v tomto místě je možno přidat test a reakce na očekávané pakety

Příklad použití programu canmonitor:

$ ./canmonitor /dev/ttyUSB1 250000
13:33:24.035: 0041: ff 7f 41 00 98 00 00 00
13:33:29.034: 0041: ff 7f 41 00 97 00 00 00
13:33:30.967: 00c1: 04 09 01 15 ff
13:33:32.403: 00c1: 09 02 01 01 ff 13:33:34.034: 0041: ff 7f 41 00 97 00 00 00 13:33:35.854: 00c1: 12 34 56 13:33:39.034: 0041: ff 7f 41 00 98 00 00 00

Program cansend

Tento jednoduchý skript nedělá nic jiného, než že pošle paket na již inicializovanou linku CAN. Jednoduše otevře sériový port a pošle příslušně naformátovaný paket. Očekává se, že rozhraní je již inicializováno programem canmonitor. Je možné nechat běžet canmonitor v jednom okně terminálu a z druhého posílat pomocí cansend pakety.

#!/usr/bin/python3

import sys
import serial

if len(sys.argv)<3:
  print("Usage: cansend <serial port> <frametype> <addr (hex)> [up to 8 hex data bytes]")
  print("frametype is one of:")
  print("  0: standard frame")
  print("  1: remote frame")
  print("  2: extended frame")
  print("  3: extended remote frame")
  print("Example: cansend /dev/ttyUSB0 0 0041 12 34 56 78")
  sys.exit(1)

def asHex(str):
  return ' '.join(format(x, '02x') for x in str)

def can_send(ser, frametype, addr, data):
  class Packet:
    data=b'\xaa'

    def add(self, x):
      self.data+=bytes([int(x)])

    def addInt2(self, x):
      for i in range(2):
        self.add(x % 256)
        x=x//256

    def addData(self, data):
      for i in range(len(data)):
        self.add(int(data[i],16))

  packet=Packet()
  packet.add(0xc0 | (frametype<<4) | (len(data)))
  packet.addInt2(addr)
  packet.addData(data)
  packet.add(0x55)
#  print("sending: "+asHex(packet.data))
  ser.write(packet.data)



# main

with serial.Serial(sys.argv[1], 2000000) as ser: # yes, 2Mbaud :-)
    can_send(ser, int(sys.argv[2]), int(sys.argv[3],16), sys.argv[4:])

Příklad použití programu cansend:

UX DAy - tip 2

$ ./cansend /dev/ttyUSB1 0 0123 11 22 33 44

Oba programy jsou dostupné na GitHubu a oba vyžadují knihovnu pyserial. Pokud ještě v systému není, nainstalujeme ji jednoduše pomocí  pip:

pip install pyserial

Poznámka: Při psaní obou programů mi celkem dobře pomáhal Copilot, o kterém jsem se před pár dny dočetl právě tady na Rootu. Zatím z něj mám velmi dobrý pocit. Je neuvěřitelné, do jakých podrobností byl schopen odhadnout, co chci napsat. V Pythonu píšu velmi málo, znám jej spíš pasivně. Takže i na takové jednoduché věci jako je počítaná smyčka for se musím jít podívat na Internet. S Copilotem to šlo podstatně lépe. Například poslední řádek programu cansend napsal copilot úplně sám. Evidentně z printů Usage pochopil význam parametrů argv, dokonce i jejich formát a ze jmen parametrů funkce can_send pochopil, které argumenty má převzít a jak je zkonvertovat.

Byl pro vás článek přínosný?

Autor článku

Josef Pavlík vystudoval FE VUT v Brně. Od roku 1991 žije v Itálii a v současné době pracuje ve společnosti SpinTec s.r.l., kde vyvíjí hardware, firmware a software nízké úrovně.