🤖Have you ever tried Chat.M5Stack.com before asking??😎
    M5Stack Community
    • Categories
    • Recent
    • Tags
    • Popular
    • Users
    • Groups
    • Search
    • Register
    • Login

    WiFi Glitches, Linux Kernel Woes & Multi-Encoder Hacks | M5Stack Dev Diary

    General
    1
    1
    27
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • Y
      yuyun2000
      last edited by

      Hey Makers! 👋

      We've been diving into some gnarly troubleshooting cases lately—from button callbacks on the Fire v2.7 to wrestling with Linux 6.1 kernel drivers, and even wiring up multiple encoders for Matter lighting. If you've been banging your head against similar issues, this one's for you. Let's break it down! 🛠️


      01 | Fire v2.7 Button Events in UiFlow2 Python

      Product: M5Stack Fire v2.7 (K007-V27)
      Category: Firmware / Library Compatibility

      The Problem:
      You're trying to use button callbacks on the Fire v2.7 with UiFlow2 Python, but you keep getting ImportError: no module named 'm5ui' or no module named 'm5stack'. Sound familiar?

      What's Going On:
      This happens when you've flashed a generic MicroPython firmware instead of the UiFlow2-specific one. The generic build doesn't include the m5ui or M5Unified modules. Also, some folks are using outdated import syntax (like from m5stack import M5), which is no longer supported.

      The Fix:
      Use the M5Unified library correctly. Here's a clean example with short/long press detection:

      import M5
      from M5 import *
      import time
      
      # Initialize M5 hardware (MUST be first!)
      M5.begin()
      
      # Define button callback (supports short/long press)
      def btnC_callback(state):
          start_time = time.ticks_ms()
          
          # Measure press duration (max 4s to prevent lockup)
          while M5.BtnC.isPressed() and time.ticks_diff(time.ticks_ms(), start_time) < 4000:
              time.sleep_ms(50)
          
          press_time = time.ticks_diff(time.ticks_ms(), start_time)
          
          # Short press (150ms-1200ms)
          if 150 <= press_time < 1200:
              print(f"Short press ({press_time}ms)")
              # Add your action here (e.g., send sensor data)
          
          # Long press (≥1800ms)
          elif press_time >= 1800:
              print(f"Long press ({press_time}ms)")
              # Add your action here (e.g., trigger camera)
      
      # Register the callback (WAS_PRESSED fires on button down)
      M5.BtnC.setCallback(type=BtnC.CB_TYPE.WAS_PRESSED, cb=btnC_callback)
      
      # Main loop (M5.update() is REQUIRED for event handling)
      while True:
          M5.update()  # Process button events and hardware state
          time.sleep_ms(50)
      

      Real-World Example: Button + WiFi + Camera:

      import M5
      from M5 import *
      import time
      import wifi  # Your custom WiFi module
      import cam   # Your custom camera module
      import svr   # Your custom server module
      
      M5.begin()
      
      def btnC_callback(state):
          start = time.ticks_ms()
          while M5.BtnC.isPressed() and time.ticks_diff(time.ticks_ms(), start) < 4000:
              time.sleep_ms(50)
          press = time.ticks_diff(time.ticks_ms(), start)
          
          # Short press: Send sensor data
          if 150 <= press < 1200:
              print("[Short] Sending sensor data")
              # send.add_to_queue()
              # send.send_queue()
              return
          
          # Long press: Capture photos and upload
          if press >= 1800:
              print("[Long] Starting photo capture")
              
              # Switch to camera WiFi
              if not wifi.to_cam():
                  print("WiFi switch failed")
                  wifi.to_svr()  # Revert to server network
                  return
              
              time.sleep(1)  # Let WiFi stabilize
              
              # Capture from multiple cameras
              images_ok = []
              for cam_no in [1, 2, 3]:
                  if cam.snap(cam_no):
                      images_ok.append(cam_no)
                      print(f"CAM{cam_no} captured")
                  time.sleep(0.3)
              
              # Switch back to server WiFi
              wifi.to_svr()
              time.sleep(1)
              
              # Upload images
              for cam_no in images_ok:
                  svr.up(cam_idx=cam_no)
                  print(f"CAM{cam_no} uploaded")
      
      M5.BtnC.setCallback(type=BtnC.CB_TYPE.WAS_PRESSED, cb=btnC_callback)
      
      while True:
          M5.update()
          time.sleep_ms(50)
      

      Gotchas:

      • Flash the right firmware! Use M5Burner to install the UiFlow2 Fire-specific firmware (v1.1.0+). Generic MicroPython won't work.
      • M5.begin() MUST be called before any M5 operations, or your button objects won't initialize.
      • Avoid blocking operations (like long time.sleep() calls) inside callbacks—they'll freeze the main loop.
      • When switching WiFi networks, add 1-2 second delays to ensure stable connections. Check connection status after wifi.to_cam().
      • The 50ms sleep in the main loop balances CPU usage and button responsiveness. Don't go below 10ms.

      02 | LLM-8850 Card Driver Fails on Linux 6.1 Kernel

      Product: LLM-8850 Card (K151)
      Category: Kernel Compatibility / Driver Compilation

      The Problem:
      You're trying to install the axclhost driver on a Linux 6.1.75 kernel via apt, but DKMS compilation fails with:
      ERROR: kernel package linux-headers-6.1.75 is not supported

      What's Going On:
      The precompiled axclhost driver package (v3.6.5) uses DKMS for dynamic compilation, but its source code only supports Linux 5.15.x and 6.8.x kernels. Linux 6.1.75 has incompatible PCIe interrupt management APIs (like pci_alloc_irq_vectors), causing the build to fail.

      The Fix:

      Step 1: Nuke the broken installation

      # Force remove the corrupted package
      sudo dpkg --force-remove-reinstreq --purge axclhost
      
      # Clean up DKMS cache and driver source
      sudo rm -rf /var/lib/dkms/axclhost/
      sudo rm -rf /usr/src/axclhost-1.0/
      
      # Remove unused kernel packages
      sudo apt autoremove -y && sudo apt clean
      

      Step 2: Verify kernel headers are installed

      # Install headers and build tools for your kernel
      sudo apt install -y linux-headers-$(uname -r) build-essential dkms
      
      # Verify headers path
      ls /lib/modules/$(uname -r)/build  # Should show Makefile, etc.
      

      Step 3: Manually compile the driver from AX SDK (Recommended)

      # Download and extract AX650 SDK (replace with actual version)
      wget https://repo.llm.m5stack.com/ax_sdk/AX650_SDK_V2.23.1.tgz
      tar -xvf AX650_SDK_V2.23.1.tgz
      cd AX650_SDK_V2.23.1
      
      # Unpack SDK and pull kernel sources
      ./sdk_unpack.sh
      
      # Compile host driver (ARM64 architecture)
      cd axcl/build
      make clean
      make host=arm64 KERNEL_DIR=/lib/modules/$(uname -r)/build all -j$(nproc)
      
      # Install driver module
      sudo cp ../out/axcl_linux_arm64/driver/axcl_host.ko \
           /lib/modules/$(uname -r)/kernel/drivers/pci/
      sudo depmod -a
      sudo modprobe axcl_host
      

      Step 4: Verify driver loading

      # Check if driver is loaded
      lsmod | grep axcl_host
      
      # Use axcl-smi to detect device
      axcl-smi
      
      # Expected output:
      # | Card  Name           Firmware | Bus-Id       | Memory-Usage          |
      # |    0  AX650N           V3.6.5 | 0001:01:00.0 | 148 MiB / 945 MiB     |
      

      Step 5 (Advanced): Patch driver source for 6.1 kernel

      If compilation still fails, you'll need to adapt the driver source:

      # Enter driver source directory
      cd AX650_SDK_V2.23.1/drv/pcie/driver
      
      # Edit axcl_host.c (example)
      nano axcl_host.c
      
      # Replace pci_enable_msi() with pci_alloc_irq_vectors()
      # Reference: https://www.kernel.org/doc/html/v6.1/
      
      # Rebuild
      make clean && make host=arm64 KERNEL_DIR=/lib/modules/$(uname -r)/build all
      

      Gotchas:

      • Official precompiled drivers only support Linux 5.15.x and 6.8.x. For 6.1.x, manual compilation is required.
      • If the SDK doesn't explicitly support 6.1, you'll need to patch the PCIe interrupt APIs in the driver source (e.g., replace pci_enable_msi with pci_alloc_irq_vectors).
      • Check detailed logs on failure: cat /var/lib/dkms/axclhost/1.0/build/make.log
      • For best compatibility, use the officially recommended Linux 5.15.73 kernel.
      • Download links for AX SDK: Contact official support at support@m5stack.com.
      • Driver compilation requires gcc-12 or higher: sudo apt install gcc-12 g++-12
      • If you have multiple kernel versions, ensure you specify the correct KERNEL_DIR path during compilation.
      • Before loading the driver, confirm PCIe device is detected: lspci | grep AX650 should show device info.
      • After loading, set device permissions: sudo chmod 666 /dev/axcl*

      03 | Connecting Module Audio (M144) to Basic v2.7

      Product: M5Stack Basic v2.7 (K001) / Module Audio (M144)
      Category: Hardware Wiring / Audio Driver Configuration

      The Problem:
      You need to connect the Module Audio M144 to a Basic v2.7 for microphone input and speaker output, but you're not sure how to wire it or configure the drivers.

      What's Going On:
      The M144 uses the M5-Bus interface to communicate with the Basic v2.7 via I2C (for controlling the STM32G030 and ES8388 codec) and I2S (for audio data transfer). You need to configure the I2S pin mapping (Mode A) and ES8388 registers correctly to get audio working.

      The Fix:

      Hardware Connection:

      Direct Stack Connection:
      - Stack the M144 module directly onto the bottom of the Basic v2.7 via M5-Bus
      - No extra wiring needed (power and signals auto-connect through M5-Bus)
      - Ensure M144's A/B mode jumper is set to Mode A (default for Basic v2.7)
      
      Pin Mapping:
      Basic v2.7 GPIO → M144 Function
      G21 (SDA)        → I2C Data (controls STM32G030 and ES8388)
      G22 (SCL)        → I2C Clock
      G12              → I2S_BCK (audio clock)
      G13              → I2S_LRCK (left/right channel select)
      G15              → I2S_DOUT (speaker output)
      G34              → I2S_DIN (microphone input)
      

      Arduino Code Example:

      // Install required libraries via Arduino Library Manager:
      // 1. M5Unified (Basic v2.7 hardware abstraction)
      // 2. M5Module-Audio (M144 driver)
      
      #include <M5Unified.h>
      #include <M5ModuleAudio.h>
      #include <driver/i2s.h>
      
      M5ModuleAudio audio_module;
      ES8388 es8388;
      
      #define SAMPLE_RATE 44100
      #define BUFFER_SIZE 1024
      
      void setup() {
        auto cfg = M5.config();
        M5.begin(cfg);
        
        // Initialize M144 (I2C address 0x33)
        audio_module.begin(&Wire, 21, 22);  // SDA=G21, SCL=G22
        audio_module.setHPMode(AUDIO_HPMODE_CTIA);  // Set headphone standard
        audio_module.setMICStatus(AUDIO_MIC_OPEN);  // Enable microphone
        
        // Initialize ES8388 codec
        es8388.init(&Wire, 21, 22);
        es8388.setADCInput(ADC_INPUT_LINPUT1_RINPUT1);  // Mic input
        es8388.setDACOutput(DAC_OUTPUT_OUT1);           // Headphone output
        es8388.setSampleRate(SAMPLE_RATE_44K);
        
        // Configure I2S audio transfer
        i2s_config_t i2s_cfg = {
          .mode = I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_RX,
          .sample_rate = SAMPLE_RATE,
          .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
          .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
          .communication_format = I2S_COMM_FORMAT_STAND_I2S,
          .dma_buf_count = 8,
          .dma_buf_len = BUFFER_SIZE,
        };
        i2s_driver_install(I2S_NUM_0, &i2s_cfg, 0, NULL);
        
        // Set I2S pins (Basic v2.7)
        i2s_pin_config_t pin_cfg = {
          .bck_io_num = 12,    // I2S_BCK
          .ws_io_num = 13,     // I2S_LRCK
          .data_out_num = 15,  // I2S_DOUT (speaker)
          .data_in_num = 34,   // I2S_DIN (microphone)
        };
        i2s_set_pin(I2S_NUM_0, &pin_cfg);
      }
      
      void loop() {
        M5.update();
        
        // Real-time audio passthrough (mic → speaker)
        uint16_t audio_buf[BUFFER_SIZE];
        size_t bytes_read;
        
        // Read from microphone
        i2s_read(I2S_NUM_0, audio_buf, sizeof(audio_buf), &bytes_read, portMAX_DELAY);
        
        // Write to speaker (add audio processing here if needed)
        i2s_write(I2S_NUM_0, audio_buf, sizeof(audio_buf), &bytes_read, portMAX_DELAY);
      }
      

      Key Configuration Parameters:

      // Headphone standard switch (choose based on your headphones)
      audio_module.setHPMode(AUDIO_HPMODE_CTIA);  // CTIA standard (international)
      // or
      audio_module.setHPMode(AUDIO_HPMODE_OMTP);  // OMTP standard (some Chinese brands)
      
      // Microphone gain adjustment (0dB~48dB)
      es8388.setMicGain(MIC_GAIN_24DB);  // Start with 24dB
      
      // RGB LED indicator control
      audio_module.setRGBLED(0, 0x00FF00);  // Set to green (recording)
      

      Gotchas:

      • M144's A/B mode jumper MUST be set to Mode A (default for Basic v2.7). Mode B is for CoreS3 to avoid conflicts with onboard ES7210.
      • I2C addresses: STM32G030 is 0x33, ES8388 is 0x10. Make sure there are no address conflicts on the bus.
      • Recommended sample rate is 44.1kHz (CD quality). Lowering to 16kHz reduces CPU usage.
      • Microphone input supports TRRS headphones (with mic) and TRS standalone mics. Switch via setMICStatus().
      • Check headphone insertion with audio_module.getHPInsertStatus() (returns 0=not inserted, 1=inserted).
      • Buffer size (BUFFER_SIZE) affects latency and stability. Recommended range: 512-2048 bytes.
      • If you hear noise, add a 0.1µF decoupling capacitor near the M144's power pins.
      • I2S data transfer uses DMA. Avoid long blocking operations in loop().
      • ES8388 supports hardware EQ. Configure bass/treble boost via registers.
      • To record to SD card, configure the file system separately (use M5.SD library).
      • Power optimization: Turn off the mic when not in use via audio_module.setMICStatus(AUDIO_MIC_CLOSE).

      04 | Using PaHub v2.1 for Multi-Encoder Matter Lighting

      Product: Unit Encoder (U135) / Nano C6 (C125) / Unit PaHub v2.1 (U040-B-V21)
      Category: Hardware Selection / I2C Address Conflict Resolution

      The Problem:
      You want to connect 3 Unit Encoders to a Nano C6 for Matter over Thread smart lighting control, but all encoders share the same I2C address (0x40). How do you tell them apart?

      What's Going On:
      Unit Encoder uses a fixed I2C address of 0x40. Connecting multiple encoders directly to the same I2C bus causes address conflicts, and the controller can't distinguish between devices. You need an I2C multiplexer or address reconfiguration to solve this.

      The Fix:

      Use Unit PaHub v2.1 for I2C Channel Isolation:

      Hardware Connection:
      Nano C6 Grove Port → Unit PaHub v2.1 Main Port
      PaHub v2.1 CH0~CH2 → 3x Unit Encoders
      
      How It Works:
      - PaHub v2.1 uses a PCA9548AP chip to split 1 I2C port into 6 independent channels
      - Each channel can connect devices with the same I2C address (0x40)
      - Software switches channels to select devices (no need to modify encoder addresses)
      

      Arduino Code Example (Channel Switching + Encoder Reading):

      #include <M5Unified.h>
      #include <Wire.h>
      
      // PaHub v2.1 default I2C address (adjustable via DIP switch: 0x70~0x77)
      #define PAHUB_ADDR 0x70
      
      // Unit Encoder default I2C address
      #define ENCODER_ADDR 0x40
      
      // Encoder register addresses
      #define REG_ENCODER_VALUE 0x10  // Encoder count (int32)
      #define REG_BUTTON_STATUS 0x20  // Button status (0=not pressed, 1=pressed)
      
      void setup() {
        auto cfg = M5.config();
        M5.begin(cfg);
        Wire.begin(1, 2);  // Nano C6 I2C pins (SDA=G1, SCL=G2)
      }
      
      // Switch PaHub channel (0~5)
      void selectChannel(uint8_t channel) {
        Wire.beginTransmission(PAHUB_ADDR);
        Wire.write(1 << channel);  // Channel select bitmask
        Wire.endTransmission();
        delay(10);  // Wait for channel to stabilize
      }
      
      // Read encoder count value
      int32_t readEncoderValue(uint8_t channel) {
        selectChannel(channel);
        
        Wire.beginTransmission(ENCODER_ADDR);
        Wire.write(REG_ENCODER_VALUE);
        Wire.endTransmission();
        
        Wire.requestFrom(ENCODER_ADDR, 4);
        int32_t value = 0;
        for (int i = 0; i < 4; i++) {
          value |= ((int32_t)Wire.read()) << (i * 8);
        }
        return value;
      }
      
      // Read button status
      bool readButtonStatus(uint8_t channel) {
        selectChannel(channel);
        
        Wire.beginTransmission(ENCODER_ADDR);
        Wire.write(REG_BUTTON_STATUS);
        Wire.endTransmission();
        
        Wire.requestFrom(ENCODER_ADDR, 1);
        return Wire.read() == 1;
      }
      
      void loop() {
        // Read status of 3 encoders
        for (uint8_t ch = 0; ch < 3; ch++) {
          int32_t value = readEncoderValue(ch);
          bool pressed = readButtonStatus(ch);
          
          Serial.printf("Encoder %d: Value=%d, Button=%s\n", 
                        ch, value, pressed ? "Pressed" : "Released");
          
          // Send Matter commands based on encoder state (example)
          if (pressed) {
            // Button press: Toggle light on/off
            sendMatterCommand(ch, "OnOff", "Toggle");
          } else if (value != 0) {
            // Encoder rotation: Adjust brightness
            sendMatterCommand(ch, "LevelControl", value > 0 ? "Up" : "Down");
          }
        }
        
        delay(100);  // Polling interval
      }
      
      // Matter command function (implement based on your Matter library)
      void sendMatterCommand(uint8_t lightID, const char* cluster, const char* action) {
        // Example: Send command to corresponding light via Matter over Thread
        Serial.printf("Send to Light %d: %s -> %s\n", lightID, cluster, action);
        // Actual implementation requires Matter SDK API calls
      }
      

      UIFlow2 Implementation (Block Coding):

      # Import PaHub and Encoder libraries
      from unit import PaHubUnit, EncoderUnit
      import time
      
      # Initialize PaHub (I2C address 0x70)
      pahub = PaHubUnit(i2c_addr=0x70)
      
      # Initialize 3 encoders (via different PaHub channels)
      encoder1 = EncoderUnit(pahub=pahub, channel=0)
      encoder2 = EncoderUnit(pahub=pahub, channel=1)
      encoder3 = EncoderUnit(pahub=pahub, channel=2)
      
      while True:
          # Read encoder 1
          value1 = encoder1.get_rotary_value()
          button1 = encoder1.get_button_status()
          
          # Read encoder 2
          value2 = encoder2.get_rotary_value()
          button2 = encoder2.get_button_status()
          
          # Read encoder 3
          value3 = encoder3.get_rotary_value()
          button3 = encoder3.get_button_status()
          
          # Control Matter lights based on state (requires Matter library integration)
          if button1:
              matter.send_command(light_id=1, cluster="OnOff", action="Toggle")
          if value2 != 0:
              matter.send_command(light_id=2, cluster="LevelControl", value=value2)
          
          time.sleep(0.1)
      

      Gotchas:

      • PaHub v2.1's I2C address is adjustable via onboard DIP switch (0x70~0x77). You can cascade multiple PaHubs to expand up to 36 channels.
      • Each channel switch requires 10ms stabilization time. Avoid frequent switching to prevent communication errors.
      • PaHub v2.1 supports 6 channels max. 3 encoders occupy CH0~CH2. Remaining CH3~CH5 can be used for other I2C devices.
      • Encoder power consumption is ~17mA each. 3 encoders total 51mA, which PaHub v2.1 can supply via Nano C6's Grove port (no external power needed).
      • Matter over Thread command sending requires ESP Matter SDK integration (see ESP Matter GitHub).
      • Encoder buttons support long-press detection. Poll the REG_BUTTON_STATUS register and use a timer to implement this.
      • For faster response, use interrupt mode (connect encoder's INT pin to Nano C6's GPIO).
      • PaHub v2.1 channel switching is done by writing a 1-byte bitmask (e.g., 0x01=CH0, 0x02=CH1, 0x04=CH2).
      • Avoid selecting multiple channels simultaneously (non-single-bit bitmask)—it causes I2C bus conflicts.
      • Encoder count value is a signed 32-bit integer. Clockwise rotation increments, counterclockwise decrements.
      • Reset encoder count to 0 by writing to the REG_ENCODER_VALUE register.
      • Matter device binding requires pairing within a Thread network (use Nano C6's Thread Border Router functionality).

      💬 Discussion

      Have you run into similar "gotchas" with your M5Stack projects? Found a clever workaround? Drop a comment below or share your own debugging stories!

      📌 Resources

      • 📚 Docs: https://docs.m5stack.com
      • 🗣️ Forum: https://community.m5stack.com
      • 🛒 Shop: https://shop.m5stack.com

      Note: This series shares common troubleshooting tips from the M5Stack engineering desk. All user data has been anonymized.

      1 Reply Last reply Reply Quote 0
      • First post
        Last post