{"id":27486853,"url":"https://github.com/jjsch-dev/pico_i2c_sniffer","last_synced_at":"2025-07-24T04:34:14.594Z","repository":{"id":49145589,"uuid":"380341708","full_name":"jjsch-dev/pico_i2c_sniffer","owner":"jjsch-dev","description":"i2c sniffer with a Raspberry Pi Pico","archived":false,"fork":false,"pushed_at":"2021-06-30T22:16:21.000Z","size":9902,"stargazers_count":79,"open_issues_count":0,"forks_count":14,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-17T01:09:55.054Z","etag":null,"topics":["i2c","pico","rp2040","sniffer"],"latest_commit_sha":null,"homepage":"","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jjsch-dev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-06-25T19:58:54.000Z","updated_at":"2025-04-07T02:27:39.000Z","dependencies_parsed_at":"2022-08-25T15:20:38.413Z","dependency_job_id":null,"html_url":"https://github.com/jjsch-dev/pico_i2c_sniffer","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/jjsch-dev/pico_i2c_sniffer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jjsch-dev%2Fpico_i2c_sniffer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jjsch-dev%2Fpico_i2c_sniffer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jjsch-dev%2Fpico_i2c_sniffer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jjsch-dev%2Fpico_i2c_sniffer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jjsch-dev","download_url":"https://codeload.github.com/jjsch-dev/pico_i2c_sniffer/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jjsch-dev%2Fpico_i2c_sniffer/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266792939,"owners_count":23984838,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-24T02:00:09.469Z","response_time":99,"last_error":null,"robots_txt_status":null,"robots_txt_updated_at":null,"robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["i2c","pico","rp2040","sniffer"],"created_at":"2025-04-16T18:02:11.110Z","updated_at":"2025-07-24T04:34:14.575Z","avatar_url":"https://github.com/jjsch-dev.png","language":"C","funding_links":[],"categories":["C"],"sub_categories":[],"readme":"# I2C sniffer - RP2040 - PICO\n\nThe purpose of this project is to develop a sniffer for the I2C BUS that can capture at 400 KHZ. \n\nFor 100 Khz, it is possible to use bit-banding (IRQ or Polling) with an 8/32 bit processor, for example an AVR8, but since for 400 Khz it is necessary to react in less than 1.5 uS, a CPLD or FPGA is required. \n\nAn intermediate way is to use the processor [RP2040](https://www.raspberrypi.org/products/raspberry-pi-pico/) that has a PIO unit that  executes instructions in one cycle independent of central CPUs. The operating speed of the PIO can reach 125 Mhz.\n\n## I2C protocol\n\nA simplification of the i2c working principle could be that there are three conditions to detect: START DATA and STOP. And when the clock pin level (SCL) is high, the data pin transitions (SDA) indicate start or stop, but if (SDA) remains stable then it is a valid data bit.\n\n![alt text](images/I2C_data_transfer.png)\n\n## PIO-based sniffer working principle\n\nEach PIO has 4 state machines that can be programmed to decode a part of the protocol and using an IRQ / WAIT scheme plus two GPIOs communicate the event.\n\nFor example, SM0 executes the program that decodes the START condition and sets the two auxiliary pins with the event code (0x01) and triggers IRQ 7 that SM3 listens, which executes the main program that is waiting for the IRQ to PUSH the event in the transmit FIFO.\n\n![alt text](images/block_diagram_pio.png)\n\nExcerpt from the code of the START DATA and MAIN state machine.\n\n```assembly\n.program i2c_start\n.wrap_target\nwait_sda_low:    \n    wait 0 gpio SDA_PIN     ; Wait for the sda pin to go down.\n    jmp pin detected        ; If the SCL is high, the condition was detected.\n    jmp wait_sda_high       ;\n\ndetected:\n    set pins EV_START       ; Set the event code for START\n    irq wait IRQ_EVENT      ; Fire the irq event  \n\nwait_sda_high:\n    wait 1 gpio SDA_PIN     ; \n.wrap\n\n.program i2c_data\n.wrap_target\n    wait 1 gpio SCL_PIN     ; Wait for the scl pin to go high.\n    set pins EV_DATA        ; Clear event code.\n    irq wait IRQ_EVENT   \n    wait 0 gpio SCL_PIN     ; Wait for the scl pin to go low.\n.wrap\n\n.program i2c_main\n.wrap_target\nwait_irq_event:    \n    wait 1 irq IRQ_EVENT    ; Wait for the irq event.\n    jmp pin send_event      ; If the lsb of the event code is zero, read the data bit.\n    in pins, 1              ; Update the ISR register with the SDA value. \n    jmp wait_irq_event      ; loads it into the FIFO when it reaches 9 bits.\n\nsend_event:\n    mov isr, pins           ; Load the EV1(3), EV0(1), SDA(0). \n    in NULL, 9              ; The event code starts at bit 11 and ends at 12.\n.wrap\n```\n### Pin sequence to encode the event\n\n    GPIO    Channel       Signal\n    ---- ------------  -------------\n    0   - 1 (Yellow)  - SDA\n    3   - 2 (Blue)    - SCL\n    1   - 3 (Violet)  - EV0\n    2   - 4 (Green)   - EV1\n\nIn the graph below, you can see how the state machines (START/STOP/DATA) encode the event code before calling IRQ7 using the EV pins. When it detects the START condition it sets EV0 high and EV1 low, when it detects DATA put EV0 and EV1 in low, and for STOP condition put EV0 and EV1 in high.     \n\n![alt text](images/tek_sda_scl_ev0_ev1.png)\n\n### Coding of sniffer events\n\nWhen the main state machine inserts a 32-bit event into the FIFO, it uses the following format: the least significant nines are reserved for the address or data that is made up of 8 bits plus the ack/nack, and the 11th bits and 12 encode the event code.\n\n![alt text](images/fifo_encode_format.png)\n\n### Connection diagram\n\nThe sniffer connection to the I2C bus is as follows: the SDA line connects to the GPIO0, the SCL line to the GPIO3 and the GND is shared.\n\nNOTE: the bus is 3.3V logic.\n\n![alt text](images/sniffer_diagram.png)\n\n## Firmware description\n\nAlthough each state machine has an 8-level FIFO, for frames composed of more than 8 digits it is not enough, so it was decided to take advantage of the fact that the PICO has two CPUs, and dedicate CORE0 to wait for the i2c events of the main state machine and send them to CORE1 through the 8-level multicore FIFO to be decoded and sent through the USB serial port to a console on a PC.\n\nTo increase the level of FIFOs, one was implemented in RAM (40K events) controlled by CORE0 for buffering when the multicore FIFO is full.\n\n![alt text](images/firmware_cores.png)\n\n## Ascii event encoding\n\nThe ascii output is a succession of events in the sequence in which they were detected by the sniffer. It must be assumed that the slave address is the one immediately after the START event, and that after the 8 bits encoded in ascii the ack / nack bit continues. \nTo improve readability, a CR LF is added each time the STOP condition is detected.\n\n`s = START CONDITION`\n`p = STOP CONDITION`\n`a = ACK DETECTED`\n`n = NAK DETECTED`\n\nBelow is an excerpt of the command to get range from the VL530X sensor using the [Pololu Arduino library](https://github.com/pololu/vl53l0x-arduino/blob/master/VL53L0X.cpp). \n\n    Capture         Source Code\n    -------         -------------\n    s52a13ap    -   readReg(RESULT_INTERRUPT_STATUS)\n    s53a40np    -   status = 40\n    s52a13ap    -   readReg(RESULT_INTERRUPT_STATUS)\n    s53a44np    -   status = 44\n    s52a1Eap    -   readReg16Bit(RESULT_RANGE_STATUS + 10);\n    s53a08a05np -   range = 0805\n    s52a0Ba01ap -   writeReg(SYSTEM_INTERRUPT_CLEAR, 0x01);\n    s52a80a01ap -   writeReg(0x80, 0x01);\n    s52aFFa01ap -   writeReg(0xFF, 0x01);\n    s52a00a00ap -   writeReg(0x00, 0x00);\n    s52a91a3Cap -   writeReg(0x91, stop_variable);\n    s52a00a01ap -   writeReg(0x00, 0x01);\n    s52aFFa00ap -   writeReg(0xFF, 0x00);\n    s52a80a00ap -   writeReg(0x80, 0x00);\n    s52a00a01ap -   writeReg(SYSRANGE_START, 0x01);\n    s52a00ap    -   readReg(SYSRANGE_START)\n    s53a00np    -   answer = 00\n\n\n### Download the firmware in Raspberry Pi Pico\n\nHold down the BOOTSEL button while plugging the board into USB. The uf2 file [i2c_sniffer_pio.uf2](https://github.com/jjsch-dev/pico_i2c_sniffer/blob/master/bin/i2c_sniffer_pio.uf2) should then be copied to the USB mass storage device that appears. Once programming of the new firmware is complete the device will automatically reset and be ready for use.\n\n### Test scenario \n\nTo test the capture, an arduino nano was used as a master that requests the status of a VL530 TOF every 10 mS on a 400 Khz i2c BUS..\n\n![alt text](images/test_device.png)\n\n### Preliminary results\n\nThe following video shows the arduino monitor consulting the status, and the serial console that sends the result of the sniff of the i2c bus.\nNote: Given the nature of the test, it has not been possible to check for loss of frames or data.\n\n![](images/i2c_sniff_400khz_10mS_TOF.gif)\n\n### TinyUSB - serial console\n\nTo make the usb port behave like a serial port (CDC) pico uses the TinyUSB library, and with the option pico_enable_stdio_usb ($ {PROJECT_NAME} 1) it is integrated into the output console (printf).\n\nFor this case, the print conversions (% c% x% s) add a lot of delay, so it was decided to do the conversion locally by nibbles.\n\n```c\nstatic inline char nibble_to_hex( uint8_t nibble ) {\n    nibble \u0026= 0x0F;\n  \n    if (nibble \u003e 9) {\n        nibble += 'A' - '0' - 10;\n    }\n\n    return( nibble + '0' );\n}\n```\n\nTo further optimize speed, MUTEX and CR and LF conversion were disabled with PUBLIC PICO_STDOUT_MUTEX = 0 PICO_STDIO_ENABLE_CRLF_SUPPORT = 0.\n\n## Print using buffered string\n\nWhen the output is via USB CDC, the data is sent in packets of maximum 64 bytes every 1mS. As the decoding of the i2c frame is composed of more than one event (Start / Stop / Data) that are separated by a few uS, to optimize the output they are stored in buffer waiting for: STOP, the buffer is full, or that elapsed more than 100 uS since the last event.\n\n```c\nvoid buff_print( void ) {\n    if (ascii_index \u003e 0) {\n        ascii_buff[ascii_index] = '\\0';\n        printf(ascii_buff); \n        ascii_index = 0;\n    }\n}\n\nstatic inline void buff_putchar(char c) {\n    // Reserve one byte for the NULL character.\n    if (ascii_index \u003e= sizeof(ascii_buff)-1) {\n        buff_print();\n    } \n\n    ascii_buff[ascii_index++] = c;\n}\n```\n\n### Led indicator\n\nThe LED is used to indicate that the board has initialized successfully (ON), flashes when there is activity on the i2c bus, and turns off when it detects a RAM overflow.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjjsch-dev%2Fpico_i2c_sniffer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjjsch-dev%2Fpico_i2c_sniffer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjjsch-dev%2Fpico_i2c_sniffer/lists"}