prettyprint

2022年11月9日 星期三

Raspberry Pi Pico PIO(Programmable IO) Episode 4: I2S audio (Play WAVE File In SDCard from MAX98357A DAC)

本篇文章探討Raspberry Pi Pico 使用PIO功能,模擬I2S界面,連接MAX98357A DAC播放Wave聲音檔。 開發界面使用VSCode + C-SDK。



一、PIO與MAX98357A I2S界面對應

根據MAX98357A  datasheet可支援16、24、32bit, 8kHz, 16kHz, 32kHz, 44.1kHz,
48kHz, 88.2kHz取樣速率。分成左右兩聲道。

若是檔案為單聲道時,則左右聲道播放相同內容。

根據Datasheet 16與32 bit的時間脈序如下圖所示,因此24bit時為後補0成為32bit再送出給MAX98357A。


根據以上分析,PIO 程式碼分成mono與streo兩段,分別處理。 

  • 使用pio side-set pin 輸出LRCLK與BCLK。 bit0:BCLK, bit1:LRCLK
  • Mono audio

根據上圖每輸出1bit,pio StateMachine使用4 clock cycle。 因為register ISR未使用,因此可用來當scratch register用,第9行先將OSR存在ISR中,第19行相同內容輸出到右聲道。單聲道時autopull=false。

  • Stereo audio

每輸出1bit,pio StateMachine 使用2 clock cycles,使用autopull=true


 PIO 程式開始時將bits per sample存到scratch register y。

 
PIO StateMachine clock frequency = 取樣速率*(每個bit的clock cycle數)*取樣位元數*2
註:   2: 為左右聲道



二、SDCard code:

使用no-OS-FatFS-SD-SPI-RPi-Pico程式碼。

三、PIO DMA:

將檔案讀取的資料透過DMA直接送PIO StateMachin TX_FIFO中:


 

四、成果影片



五、原始程式碼:

 1. PIO

;=====  mono ====== 
; 4 pio clock cycles for 1 bit
.program i2s_pio_dma_mono
.side_set 2
loop1:
out pins, 1 side 0b00 [1]
jmp x--, loop1 side 0b01 [1]
out pins, 1 side 0b10
mov osr, isr side 0b10   ;isr as scratch register, the same output for right channel

mov x, y side 0b11 [1]
loop2:
out pins, 1  side 0b10 [1]
jmp x--, loop2 side 0b11 [1]
out pins, 1  side 0b00

public entry_point_m:
pull side 0b00
mov isr, osr side 0b01
mov x,y side 0b01

;=====  stereo ====== 
; 2 pio clock cycles for 1 bit
.program i2s_pio_dma_stereo
.side_set 2
loop1:
out pins, 1 side 0b00
jmp x--, loop1 side 0b01
out pins, 1 side 0b10

mov x, y side 0b11
loop2:
out pins, 1 side 0b10
jmp x--, loop2 side 0b11
out pins, 1  side 0b00

public entry_point_s:
mov x,y side 0b01

2. main.c
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/dma.h"
#include "hardware/pio.h"
#include "sd_card.h"
#include "ff.h"
#include "pio_dma.h"
#include "hardware/clocks.h"


uint8_t f_buf0[4010], f_buf1[4010]; //double buffer 
uint8_t *file_ptr = f_buf0;
uint8_t *fifo_ptr = f_buf1;
uint8_t *temp_ptr=NULL;
bool read_flag=true;

uint byte_trans_count=0;
uint MAX_BYTE_TRANS=4000;

struct i2s_pio_dma_param_t {
    //for pio
    pio_program_t program;
    uint8_t bitsPerSample;
    uint8_t offset;
    uint8_t entry_point;
    uint8_t bitInsCycles; // 4 pio clock cycles for mono, 2 pio clocks for stereo
    //for dma
    int chan_num;
    uint8_t dma_size;
} i2s_pio_dma_param;

struct wave_file_header_t {
    char blockID[4];
    uint32_t totalSize;
    char typeHeader[4];
    char fmt[4];
    uint32_t headerLen;
    uint16_t typeOfFormat;
    uint16_t numbeOfChannel;
    uint32_t sampleRate;
    uint32_t byteRate;
    uint16_t blockAlign;
    uint16_t bitsPerSample;
    char dataHeader[4];
    uint32_t dataSize;
} wave_file_header;


void trans_wav();


void i2s_pio_init(PIO pio, uint sm, uint sideset_base, uint out_base, uint numberOfChannels, uint bitsPerSample, uint freq) {
    uint offset = 0;
    pio_sm_config c;
    if (numberOfChannels == 1) {
        offset = pio_add_program(pio, &i2s_pio_dma_mono_program);
        c = i2s_pio_dma_mono_program_get_default_config(offset);
        i2s_pio_dma_param.program = i2s_pio_dma_mono_program;
        i2s_pio_dma_param.entry_point = i2s_pio_dma_mono_offset_entry_point_m;
        sm_config_set_out_shift(&c, false, false, bitsPerSample);
    } else {
        offset = pio_add_program(pio, &i2s_pio_dma_stereo_program);
        c = i2s_pio_dma_stereo_program_get_default_config(offset);
        i2s_pio_dma_param.program = i2s_pio_dma_stereo_program;
        i2s_pio_dma_param.entry_point = i2s_pio_dma_stereo_offset_entry_point_s;
        sm_config_set_out_shift(&c, false, true, bitsPerSample);
    }
    i2s_pio_dma_param.offset = offset;
    i2s_pio_dma_param.bitsPerSample = bitsPerSample;
    pio_gpio_init(pio, out_base);
    pio_gpio_init(pio, sideset_base);
    pio_gpio_init(pio, sideset_base+1);
    pio_sm_set_consecutive_pindirs(pio, sm, sideset_base, 2, true);
    pio_sm_set_consecutive_pindirs(pio, sm, out_base, 1, true);
    sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);
    sm_config_set_out_pins(&c, out_base, 1);
    sm_config_set_sideset_pins(&c, sideset_base);
    float div = clock_get_hz(clk_sys)/freq;
    sm_config_set_clkdiv(&c, div);
    pio_sm_init(pio, sm, offset, &c);
}

void i2s_pio_enable(PIO pio, uint sm) {
    dma_channel_set_irq1_enabled(i2s_pio_dma_param.chan_num, true);
    pio_sm_set_enabled(pio, sm, true);
    pio_sm_exec(pio, sm, pio_encode_set(pio_y, i2s_pio_dma_param.bitsPerSample-2));
    pio_sm_exec(pio, sm, pio_encode_jmp(i2s_pio_dma_param.offset+i2s_pio_dma_param.entry_point));
}

void i2s_reset(PIO pio, uint sm) {
    pio_sm_drain_tx_fifo(pio, sm);
    pio_sm_set_enabled(pio, sm, false);

    pio_remove_program(pio, &i2s_pio_dma_param.program, i2s_pio_dma_param.offset);
    pio_clear_instruction_memory(pio);

    dma_channel_unclaim(i2s_pio_dma_param.chan_num);
    irq_set_enabled(DMA_IRQ_1,false);
    irq_remove_handler(DMA_IRQ_1, trans_wav);
    dma_channel_set_irq1_enabled(i2s_pio_dma_param.chan_num, false);
}

void i2s_pio_dma_init(PIO pio, uint sm) {
    i2s_pio_dma_param.chan_num = dma_claim_unused_channel(true);
    dma_channel_config c = dma_channel_get_default_config(i2s_pio_dma_param.chan_num);
    channel_config_set_write_increment(&c, false);
    channel_config_set_read_increment(&c, true);
    channel_config_set_dreq(&c, pio_get_dreq(pio, sm, true));
    channel_config_set_transfer_data_size(&c, i2s_pio_dma_param.dma_size); //DMA_SIZE_8,16,32
    dma_channel_acknowledge_irq1(i2s_pio_dma_param.chan_num);  // clear channel interrupt(ints1)
    dma_channel_set_irq1_enabled(i2s_pio_dma_param.chan_num, true); // enable irq1 (inte1)
    irq_add_shared_handler(DMA_IRQ_1, trans_wav, PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY);
    irq_set_enabled(DMA_IRQ_1, true);
    dma_channel_configure(i2s_pio_dma_param.chan_num, &c, (void*) (PIO0_BASE+PIO_TXF0_OFFSET), 
             fifo_ptr, MAX_BYTE_TRANS>> i2s_pio_dma_param.dma_size, false); //DMA_SIZE_8 or 16 or 32
}

void trans_wav() {
    if (dma_channel_get_irq1_status(i2s_pio_dma_param.chan_num)) {
        dma_channel_acknowledge_irq1(i2s_pio_dma_param.chan_num);
        temp_ptr = file_ptr;
        file_ptr=fifo_ptr;
        fifo_ptr=temp_ptr;
        
        dma_channel_set_trans_count(i2s_pio_dma_param.chan_num, byte_trans_count >> i2s_pio_dma_param.dma_size, false);
        dma_channel_set_read_addr(i2s_pio_dma_param.chan_num, fifo_ptr,true);
        
        read_flag=true;
        if (byte_trans_count < MAX_BYTE_TRANS) {
            dma_channel_set_irq1_enabled(i2s_pio_dma_param.chan_num, false);
        }
    }
}

void i2s_init() {
    i2s_pio_dma_param.bitInsCycles=2;
    if (wave_file_header.numbeOfChannel == 1) i2s_pio_dma_param.bitInsCycles=4;
    uint freq = wave_file_header.sampleRate*i2s_pio_dma_param.bitInsCycles*wave_file_header.bitsPerSample*2;
    i2s_pio_dma_param.dma_size = wave_file_header.bitsPerSample >> 4; // DMA_SIZE_8, 16, 32

    i2s_pio_dma_init(pio0, 0);
    i2s_pio_init(pio0, 0, 16, 18, wave_file_header.numbeOfChannel,wave_file_header.bitsPerSample, freq);
    i2s_pio_enable(pio0, 0);
}

void playWave(char* fn) {
    printf("%s\n", fn);
    FRESULT fr;
    uint br;
    FIL pfile;
    fr = f_open(&pfile, fn, FA_READ);
    if (fr != FR_OK) {
        printf("open file error\n");
        return;
    }
    f_read(&pfile, &wave_file_header, 44, &br);
    if (fr != FR_OK) {
        printf("open file error\n");
        return;
    }

    i2s_init();

    f_read(&pfile, fifo_ptr, MAX_BYTE_TRANS, &byte_trans_count);
    read_flag=true;

    dma_channel_set_trans_count(i2s_pio_dma_param.chan_num, byte_trans_count >> i2s_pio_dma_param.dma_size, false);
    dma_channel_set_read_addr(i2s_pio_dma_param.chan_num, fifo_ptr,true);

    while (1) {
        if (read_flag) {
            read_flag=false;
            f_read(&pfile, file_ptr, MAX_BYTE_TRANS, &byte_trans_count);
            if (byte_trans_count < MAX_BYTE_TRANS) {
                f_close(&pfile);
                i2s_reset(pio0,0);
                break;
            }
        }
        
    }
}
int main()
{
    FRESULT fr;
    FATFS fs;

    stdio_init_all();
    
    if (!sd_init_driver()) {
        printf("SD Initial Error\n");
        return 0;
    }
    fr = f_mount(&fs, "0:", 1);
    if (fr != FR_OK) {
        printf("bool read_flag=true;mount error\n");
        return 0;
    }
    
    playWave(demo1.wav");
    playWave("demo2.wav");
    

    while(true) {
        tight_loop_contents();
    }

    return 0;
}


3. CMakeLists.txt

  # Generated Cmake Pico project file

cmake_minimum_required(VERSION 3.13)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
# Initialise pico_sdk from installed location
# (note this can come from environment, CMake cache etc)
set(PICO_SDK_PATH "/your-path/pico/pico-sdk")
set(PICO_BOARD pico CACHE STRING "Board type")
# Pull in Raspberry Pi Pico SDK (must be before project)
include(pico_sdk_import.cmake)
if (PICO_SDK_VERSION_STRING VERSION_LESS "1.4.0")
  message(FATAL_ERROR "Raspberry Pi Pico SDK version 1.3.0 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}")
endif()
project(pio_dma C CXX ASM)
# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()
# Add executable. Default name is the project name, version 0.1

add_executable(pio_dma main.c )
pico_set_program_name(pio_dma "pio_dma")
pico_set_program_version(pio_dma "0.1")
pico_enable_stdio_uart(pio_dma 1)
pico_enable_stdio_usb(pio_dma 0)
# Add the standard library to the build
target_link_libraries(pio_dma
        pico_stdlib)
# Add the standard include files to the build
target_include_directories(pio_dma PRIVATE
  ${CMAKE_CURRENT_LIST_DIR}
  ${CMAKE_CURRENT_LIST_DIR}/.. # for our common lwipopts or any other standard includes, if required
)
# Add any user requested libraries
target_link_libraries(pio_dma
        hardware_dma
        hardware_pio
        FatFs_SPI
        )
# Tell CMake where to find other source code
add_subdirectory(FatFs_SPI build)
pico_add_extra_outputs(pio_dma)
# Enable usb output, disable uart output
pico_enable_stdio_usb(${PROJECT_NAME} 0)
pico_enable_stdio_uart(${PROJECT_NAME} 1)


2022年10月14日 星期五

Raspberry Pi Pico PIO(Programmable IO) Episode 3: DHT11 (Humidity and Temperature sensor)

本文介紹使用Raspberry Pi Pico PIO功能來讀取DHT11溫濕度資料,顯示在1602A LCD上。


一、硬體接線:

LCD 1602A:     Raspberry Pi Pico

VSS:                GND
VDD:              Pin40(VBUS), 5V
VO:                10k可變電阻
RS:                 Pin 11
RW:                GND
E:                  Pin 10
D4:               Pin 6
D5:               Pin 7
D6:               Pin 8
D7:               Pin 9
A:                5V
K:                GND

DHT11        Raspberry Pi Pico
Pin2:             Pin 16

VDD:         5V


二、PIO程式碼與DHT11 datasheet對照說明:







設定autopush=True, push_thresh=32所以前4bytes共32bits,StateMachine自動push到RX_FIFO中。
最後第五個byte(CRC)因為未滿threshold所以需要下push指定到RX_FIFO。

三、成果影片:



四、程式碼:
 DHT11.py
from machine import Pin
import rp2
from utime import sleep_ms
class DHT11():
    """ output humidity(rh) and temperature(temp), call get_data to get values """
    @rp2.asm_pio(set_init=rp2.PIO.OUT_HIGH, autopush=True,  push_thresh=32, in_shiftdir=rp2.PIO.SHIFT_LEFT)
    def dht11():
        wrap_target()
        set(pindirs,1)                # set pin as output
        set(pins,0)                   # set low for 20ms (at least 18ms)
        set(x,29)                     #freq=500000, 1 cycle = 2us (30*30*11*2 = 19.8ms)
        label("start_loop_1")
        set(y,29)
        label("start_loop_2")
        jmp(y_dec, "start_loop_2") [10]
        jmp(x_dec, "start_loop_1")
        set(pins,1) [14]             # pull-up for 2us*15 = 30us (20~40 us)
        set(pindirs,0)               # set pin as input
        wait(0,pin,0) [19] 
        nop() [19]                   # wait for low 2*(20+20)=80us
        wait(1,pin,0) [19] 
        nop()  [19]                  # wait for high 2*(20+20)=80us
        
        set(x,4)                     # 5 bytes
        label("bytes")
        set(y,7)                     # each byte: 8 bits
        label("bits")
        wait(0,pin,0) [14]           # LOW:  wait for 2us*15=30us (50us)  
        wait(1,pin,0) [19]           # HIGH: wait at least 2*20=40 us (26~28us: 0, 70us: 1)
        in_(pins,1)   
        jmp(y_dec,"bits")            # autopush and push_thresh=32, auto push 4 data byte
        jmp(x_dec,"bytes")
        push()                       # push last one CRC byte
        nop() [31]                   #transmition completed, low for 50us and pull-high
        wait(0,pin,0)                # block for next
        
        wrap()
    
    def __init__(self, pin_num, smid=0):
        self.pin = Pin(pin_num, Pin.IN, Pin.PULL_UP)
        self.sm=rp2.StateMachine(smid, self.dht11, freq=500000, in_base=self.pin, set_base=self.pin)
        self.sm.active(1)
        self.rh=0.0
        self.temp=0.0
    
    def get_data(self):
        self.sm.restart()
        data=self.sm.get()
        checksum=self.sm.get()
        bytes=[]
        for i in range(3,-1,-1):
            bytes.append((data>>8*i)&0xff)
        if ((bytes[0]+bytes[1]+bytes[2]+bytes[3]) == checksum):
            self.rh = float('{}.{}'.format(bytes[0], bytes[1]))
            self.temp = float('{}.{}'.format(bytes[2], bytes[3]))
        else:
            self.rh=0.0
            self.temp=0.0
        del bytes
        sleep_ms(1120) #Sampling period at intervals should be no less than 1 second.
LCD1602A.py
# base on https://github.com/wjdp/micropython-lcd/blob/master/lcd.py

from machine import Pin
from utime import sleep_us

class LCD1602A():
    
    PIN_NAMES = ['RS','E','D4','D5','D6','D7']
    
    # Define some device constants
    LCD_WIDTH = 16    # Maximum characters per line
    # Designation of T/F for character and command modes
    LCD_CHR = True
    LCD_CMD = False

    LINES = {
        0: 0x80, # LCD RAM address for the 1st line
        1: 0xC0, # LCD RAM address for the 2nd line
        # Add more if desired
    }

    # Timing constants
    E_PULSE = 50
    E_DELAY = 50

    def __init__(self, rs, e,*,d4=None, d5=None, d6=None, d7=None):
        self.pins={}
        self.PINS=[rs, e, d4, d5, d6, d7]
        # Initialise pins
        for pin, pin_name in zip(self.PINS, self.PIN_NAMES):
            self.pins['LCD_'+pin_name] = Pin(pin, Pin.OUT)
        # Initialise display
        self.lcd_byte(0x33,self.LCD_CMD)
        self.lcd_byte(0x32,self.LCD_CMD)
        self.lcd_byte(0x28,self.LCD_CMD)
        self.lcd_byte(0x0C,self.LCD_CMD)
        self.lcd_byte(0x06,self.LCD_CMD)
        self.lcd_byte(0x01,self.LCD_CMD)
        

    def clear(self):
        # Clear the display
        self.lcd_byte(0x01,self.LCD_CMD)

    def set_line(self, line):
        # Set the line that we're going to print to
        self.lcd_byte(self.LINES[line], self.LCD_CMD)

    def set_string(self, message):
        # Pad string out to LCD_WIDTH
        # message = message.ljust(LCD_WIDTH," ")
        m_length = len(message)
        if m_length < self.LCD_WIDTH:
            short = self.LCD_WIDTH - m_length
            blanks=str()
            for i in range(short):
                blanks+=' '
            message+=blanks
        for i in range(self.LCD_WIDTH):
            self.lcd_byte(ord(message[i]), self.LCD_CHR)

    def lcd_byte(self, bits, mode):
        # Send byte to data pins
        # bits = data
        # mode = True  for character
        #        False for command

        self.pin_action('LCD_RS', mode) # RS

        # High bits
        self.pin_action('LCD_D4', False)
        self.pin_action('LCD_D5', False)
        self.pin_action('LCD_D6', False)
        self.pin_action('LCD_D7', False)
        if bits&0x10==0x10:
            self.pin_action('LCD_D4', True)
        if bits&0x20==0x20:
            self.pin_action('LCD_D5', True)
        if bits&0x40==0x40:
            self.pin_action('LCD_D6', True)
        if bits&0x80==0x80:
            self.pin_action('LCD_D7', True)

        # Toggle 'Enable' pin
        self.udelay(self.E_DELAY)
        self.pin_action('LCD_E', True)
        self.udelay(self.E_PULSE)
        self.pin_action('LCD_E', False)
        self.udelay(self.E_DELAY)

        # Low bits
        self.pin_action('LCD_D4', False)
        self.pin_action('LCD_D5', False)
        self.pin_action('LCD_D6', False)
        self.pin_action('LCD_D7', False)
        if bits&0x01==0x01:
            self.pin_action('LCD_D4', True)
        if bits&0x02==0x02:
            self.pin_action('LCD_D5', True)
        if bits&0x04==0x04:
            self.pin_action('LCD_D6', True)
        if bits&0x08==0x08:
            self.pin_action('LCD_D7', True)

        # Toggle 'Enable' pin
        self.udelay(self.E_DELAY)
        self.pin_action('LCD_E', True)
        self.udelay(self.E_PULSE)
        self.pin_action('LCD_E', False)
        self.udelay(self.E_DELAY)

    def udelay(self, us):
        # Delay by us microseconds, set as function for portability
        #pyb.udelay(us)
        sleep_us(us)

    def pin_action(self, pin, high):
        # Pin high/low functions, set as function for portability
        if high:
            self.pins[pin].on()
        else:
            self.pins[pin].off()
            
            

main.py
from machine import Pin
from DHT import DHT11
from LCD1602A import LCD1602A
import utime


lcd=LCD1602A(11,10, d4=6,d5=7,d6=8,d7=9)
dht=DHT11(16)

while True:
    dht.get_data()
    lcd.set_line(0)
    lcd.set_string("Temp:"+str(dht.temp)+"C")
    lcd.set_line(1)
    lcd.set_string("RH:  "+str(dht.rh)+"%")
    #utime.sleep(0.5)

2022年10月8日 星期六

Raspberry Pi Pico PIO(Programmable IO) Episode 2: IR remote controller

 本篇文章介紹使用Raspberry Pi Pico的PIO(Programmable IO)功能來建立一個紅外線遙控器。測試使用NEC 與SAMSUNG protocol。

NEC protocol測試設備使用中華電信MOD機上盒,SAMSUNG protocol使用SAMSUNG TV測試。

使用元件:
  1. 4X4 matrix keypad
  2. IR LED 940nm
  3. 200歐姆電阻
  4. Raspberry Pi Pico
使用MicroPython語言與Thonny開發環境,作業系統為FreeBSD 13.1 + KDE Plasma。

一、紅外線遙控器協定簡介:

使用頻率約為38KHz,
  1. pulse: period 26.3 us, duty factor=1/3
  2. space: period 26.3us的low
  3. pulse burst: 為一連串pulse組成。
如下圖所示:



NEC Protocol:
  1. logical 0:一個維持562.5µs pulse burst 跟著一個維持562.5µs space, 時間長度為1.125ms
  2. logical 1: 一個維持562.5µs pulse burst 跟著一個維持1687.5µs space, 時間長度為2.25ms
一個按鍵按下時傳送的資料如下:
總時間長度為108ms
  1. 9ms pulse burst
  2. 4.5ms space
  3. 8-bit(八個logical 0 or logical 1組成)位址
  4. 8-bit反向位址(不反向亦可)
  5. 8-bit command
  6. 8-bit logical inverse of the command
  7. 562.5µs pulse burst結束訊息
  8. 剩下送出space直到108ms。
    如下圖所示:


若按鍵按下可送出多的repeat code:
  1. 9ms pulse burst
  2. 2.25ms space
  3. 562.5us pulse space
  4. 剩下送出space直到108ms。
SAMSUNG protocol:
  1. Start bits: 4.5ms pulse burst + 4.5ms space
  2. 32 bits address + command(logical 0: 590us pulse+590us space, logical 1: 590us pulse burst+1690us space)
  3. stop bits: 590us pulse burst + 590us space

二、4x4 matrix keypad:

本文將前一篇的程式碼精簡為在一個StateMachine中完成。詳細說明在下一節。

三、程式碼說明:

  • 腳位接線:IR LED接GPIO16,4x4 matrix keypad左至右pin1~pin8分別接GPIO2~9。
  • RP2040有兩個PIO blocks,為PIO0與PIO1,每個PIO有4個StateMachine共用大小為32指令的Instruction Memory,因此每個PIO的程式碼不能多於32指令。
  • PIO0 StateMachine(0)負責keypad,PIO1 StateMachine(4)負責IR LED(GIPO16)訊號發送。
  • 每個StateMachine有ClockDiv register因此每個StateMachine可以擁有自己的clock frequency。
  • StateMachine共有9個指令(jmp, wait, in_, out, pull, push, mov, irq, set)每個instruction只需要一個clock cycle即可完成執行。
keypad的StateMachine(0)負責按鍵,所以速度使用較慢freq=2000,每個clcye使用1/2000sec=0.5ms

IR遙控器使用的頻率約為38kHz,我們使用的pulse duty factor為1/3,每個pulse或space使用3個clock cycle,所以StateMachine(4)的freq=3*28k=114k。

pulse:1 clock cycle HIGH, 2 clock cycles LOW

space: 3 clock cycles LOW(延續前一個狀態LOW)

  • NEC IR protocol:
    message: (pulse burst) --> (space) --> (address+command) --> (pulse burst) --> (space)
    repeat code: (pulse burst) --> (space) -->  (pulse burst) --> (space)
  • SAMSUNG IR protocol:
     message格式為:(pulse burst) --> (space) --> (address+command) --> (pulse burst) --> (space)
每個StateMachine的instruction memory只有32 instructions大小,因此加入pseudo address+command的概念,則每個message可視為執行兩次(pulse burst) --> (space) --> (address+command)。
  • NEC IR protocol:
    message: (pulse burst) --> (space) --> (address+command) --> (pulse burst) --> (space)-->(pseudo)
    repeat code: (pulse burst) --> (space)-->(pseudo) -->  (pulse burst) --> (space)-->(pseudo)
  • SAMSUNG IR protocol:
     message格式為:(pulse burst) --> (space) --> (address+command) --> (pulse burst) --> (space)-->(pseudo)
每個pulse或space的時間為1/38k sec=26.3us, 
9ms = 342x26.3us
4.45ms = 171*26.3us
2.25ms = 86*26.3us
562.5us=21*26.3us
將所需的時間換成次數輸入到StateMachine的TX FIFO中,


完整程式碼附在文章後面。

四、成果影片:



五、程式碼

from machine import Pin
import rp2

@rp2.asm_pio(out_init=rp2.PIO.OUT_LOW,set_init=rp2.PIO.OUT_LOW,out_shiftdir=1, autopull=False, pull_thresh=32)
def sendNECIRCommand():
    wrap_target()
    set(y,1)
    label("loop")
    pull()
    mov(x,osr)
    label("pulse")
    set(pins,1)
    set(pins,0)
    jmp(x_dec,"pulse")
    pull()
    mov(x,osr)
    label("space")
    jmp(x_dec, "space") [2]
    #check if command data
    pull()
    mov(x, osr)
    jmp(not_x, "return_here")
    jmp("command")
    label("return_here")
    jmp(y_dec, "loop")
    irq(rel(0))
    wait(1,pin,0)
    
    label("command")
    #address and command
    set(y,31)
    label("bit")
    set(x, 20)
    label("data_pulse")
    set(pins,1)
    set(pins,0)
    jmp(x_dec, "data_pulse") #total cycles 3*21=63, 553us
    set(x,20)
    label("data_space")
    jmp(x_dec, "data_space") [2] # total cycle:3*21=62, 553us
    out(x,1)
    jmp(not_x, "not_x_bit")
    set(x,20)
    label("data_space_1")
    jmp(x_dec, "data_space_1")[5] # 6*21=126, 1105us (1105+552)=1657us
    label("not_x_bit")
    jmp(y_dec,"bit")
    jmp("return_here")
    
    wrap() # total 29 instructions


@rp2.asm_pio(set_init=(rp2.PIO.OUT_LOW,)*4)
def keypad():
    wrap_target()
    label("set_row_1")
    set(pins, 1) [31]  #set row_1 High and wait for button bounce(0.5ms*32=16ms)
    set(x,0x1)
    in_(pins,4)
    mov(y, isr)
    jmp(not_y,"set_row_2")
    jmp("rx_fifo")
    
    label("set_row_2")
    set(pins, 2) [31] #set row_2 High and wait for button bounce
    set(x,0x2)
    in_(pins,4)
    mov(y, isr)
    jmp(not_y,"set_row_3")
    jmp("rx_fifo")
    
    label("set_row_3")
    set(pins, 4) [31] #set row_3 High and wait for button bounce
    set(x,0x4)
    in_(pins,4)
    mov(y, isr)
    jmp(not_y,"set_row_4")
    jmp("rx_fifo")
    
    label("set_row_4")
    set(pins, 8) [31] #set row_4 High and wait for button bounce
    set(x,0x8)
    in_(pins,4)
    mov(y, isr)
    jmp(not_y,"set_row_1")
  
    
    label("rx_fifo")
    push()              #push y(col) 
    in_(x,4)[2]
    push()              #and then x(row)
    irq(rel(0))
    
    wait(0,pin,0)  # check whether key is released
    wait(0,pin,1)
    wait(0,pin,2)
    wait(0,pin,3)
    wrap()   # total 31 instructions
#NEC    
keys=[[0xde21bebe,0xdd22bebe,0xdc23bebe,0xc936bebe], # 1,2,3,A => 1,2,3,VOL+
      [0xdb24bebe,0xda25bebe,0xd926bebe,0xc837bebe], # 4,5,6,B => 1,2,3,VOL-
      [0xd827bebe,0xd728bebe,0xd629bebe,0x847bbebe], # 7,8,9,C => 1,2,3,CH+
      [0x8f70bebe,0xdf20bebe,0x807fbebe,0x837cbebe]] # *,0,#,D => 1,2,3,CH-
#SANSUNG
#keys=[[0xfb040707,0xfa050707,0xf9060707,0xf8070707], # 1,2,3,A => 1,2,3,VOL+
#      [0xf7080707,0xf6090707,0xf50a0707,0xf40b0707], # 4,5,6,B => 1,2,3,VOL-
#      [0xf30c0707,0xf20d0707,0xf10e0707,0xed120707], # 7,8,9,C => 1,2,3,CH+
#      [0xfd020707,0xee110707,0x97680707,0xef100707]] # *,0,#,D => 1,2,3,CH-

def sendIRCommand(cmd):
    #NEC
    smIR.restart()
    smIR.put(341)
    smIR.put(170)
    smIR.put(cmd)
    smIR.put(20)
    smIR.put(1290)
    smIR.put(0)
    #send repeat once
    smIR.put(341)
    smIR.put(85)
    smIR.put(0)
    smIR.put(20)
    smIR.put(3653)
    smIR.put(0)
    
    #SAMSUNG
    #smIR.restart()
    #smIR.put(170)
    #smIR.put(170)
    #smIR.put(cmd)
    #smIR.put(20)
    #smIR.put(20)
    #smIR.put(0)


def keypad_irq(sm):
    y=sm.get()
    x=sm.get()
    for i in range(4):
        if x >> i == 1:
            break;
    for j in range(4):
        if y >> j == 1:
            break;
    sendIRCommand(keys[i][j])

IRpin=Pin(16, Pin.OUT, Pin.PULL_DOWN)
smkeypad=rp2.StateMachine(0, keypad, freq=2000, set_base=Pin(2), in_base=Pin(6))
smIR=rp2.StateMachine(4, sendNECIRCommand, freq=114000, set_base=IRpin) #freq=3*38k=114k
smkeypad.irq(keypad_irq)
smkeypad.active(1)
smIR.active(1)


2022年10月1日 星期六

Raspberry Pi Pico PIO(Programmable IO) Episode 1: 4x4 matrix keypad

一、Raspberry PIO簡述: 

Raspberry Pi pico比較獨特的是具有PIO(programmable IO),相當於具有8小型的專門處理IO的處理器,分為兩個PIO區塊,每個區塊有4個StateMachine,其架構如下圖所示:

(PIO block, 取自RP2040 datasheet)
  1. 每個PIO block的4個StateMachine共用32 Instructions 的Instruction Memory,雖然小但也夠用。
  2. 每個StateMachine有4個32 bits output FIFO與4個32 bits input FIFO,有可結合成單一方向的8個32 bits FIFO。
  3. 有兩個通用32 bits register: X and Y
  4. ISR 與OSR: input shift register and output shift register
  5. Clock Div: 可除頻1~65536。

(StateMachine, 取自RP2040 datasheet)
指令集:
(取自RP2040 datasheet)


二、4x4 Keypad:


腳位從左到右為pin1~pin8,pin1~4同一時間只能有一個腳位設為High,當pin1為high時,按下key 1,則pin5輸出為hight。

三、實做Raspberry Pi Pico PIO:

使用MicroPython程式語言來實作。
4x4 keypad與raspberry pi pico腳位接線如上圖所示。

pin 1~4同時間只能有一個腳位為高電位,使用PIO1 block的StateMachine(4) set(pins,4)指令達成需求,程式碼如下:

@rp2.asm_pio(set_init=(rp2.PIO.OUT_LOW,rp2.PIO.OUT_LOW,rp2.PIO.OUT_LOW,rp2.PIO.OUT_LOW))
def scanRowHigh():
    wrap_target()
    set(pins, 1) [3] # each pin is high for at least 8 cycles (1+3+1+1+1+1)
    wait(0,pin,0)
    wait(0,pin,1)
    wait(0,pin,2)
    wait(0,pin,3) 
    set(pins, 2) [3] 
    wait(0,pin,0)
    wait(0,pin,1)
    wait(0,pin,2)
    wait(0,pin,3) 
    set(pins, 4) [3] 
    wait(0,pin,0)
    wait(0,pin,1)
    wait(0,pin,2)
    wait(0,pin,3) 
    set(pins, 8) [3]
    wait(0,pin,0)
    wait(0,pin,1)
    wait(0,pin,2)
    wait(0,pin,3) 
    wrap()

smr = rp2.StateMachine(4, scanRowHigh, freq=2000, set_base=Pin(2), in_base=col1pin)
smr.active(1)

第一行(1,4,7,*)鍵按下時,則pin5為高電位,使用PIO0 StateMachine(0) 的指令jmp(pin, "which_row")來判斷是否有按鍵按下。若有按鍵按下則使用in_(pins,4)來讀取GPIO2~5是那一個pin是高電位,則可判斷是那一個按鍵按下。接下來觸發irq,使用指令irq(rel(0))輸出按鍵內容。最後等待按鍵釋放(wait(0, pin, 0)。第二行按鍵使用PIO0 StateMachine(1) ,第三行按鍵使用PIO0 StateMachine(2),第四行按鍵使用PIO0 StateMachine(3)。程式碼如下所示。

@rp2.asm_pio()
def keypad():
    wrap_target()
    label("start")
    jmp(pin, "which_row") 
    jmp("start")
    label("which_row")
    in_(pins,4)
    push() 
    irq(rel(0)) [31] #wait for button bounce(at least 30ms: (32+32)*0.5ms)
    label("delay")
    nop() [31]
    jmp(pin, "delay") # wait for key to be released
    wrap()
    
def getkeyvalue(sm):
    row=-1
    if sm == rp2.StateMachine(0):
        row=0
    elif sm == rp2.StateMachine(1):
        row=1
    elif sm == rp2.StateMachine(2):
        row=2
    elif sm == rp2.StateMachine(3):
        row=3
    
    while sm.rx_fifo() :
        term = sm.get()
        for i in range(4):
            if term >> i == 1:
                print(keyvalues[row][i])
                break;

keyvalues=[["1","4","7","*"],
           ["2","5","8","0"],
           ["3","6","9","#"],
           ["A","B","C","D"]]

col1pin=Pin(6, Pin.IN, Pin.PULL_DOWN)
col2pin=Pin(7, Pin.IN, Pin.PULL_DOWN)
col3pin=Pin(8, Pin.IN, Pin.PULL_DOWN)
col4pin=Pin(9, Pin.IN, Pin.PULL_DOWN)

smc1=rp2.StateMachine(0, keypad, freq=2000,  in_base=Pin(2),jmp_pin=col1pin)
smc2=rp2.StateMachine(1, keypad, freq=2000,  in_base=Pin(2),jmp_pin=col2pin)
smc3=rp2.StateMachine(2, keypad, freq=2000,  in_base=Pin(2),jmp_pin=col3pin)
smc4=rp2.StateMachine(3, keypad, freq=2000,  in_base=Pin(2),jmp_pin=col4pin)

smc1.irq(getkeyvalue)
smc2.irq(getkeyvalue)
smc3.irq(getkeyvalue)
smc4.irq(getkeyvalue)
smc1.active(1)
smc2.active(1)
smc3.active(1)
smc4.active(1)

四、成果影片:


五、完整程式碼:

第二個程式碼為修正第二版,程式碼更簡潔。
👉第一版
from machine import Pin
import rp2

@rp2.asm_pio(set_init=(rp2.PIO.OUT_LOW,rp2.PIO.OUT_LOW,rp2.PIO.OUT_LOW,rp2.PIO.OUT_LOW))
def scanRowHigh():
    wrap_target()
    set(pins, 1) [3] 
    wait(0,pin,0)
    wait(0,pin,1)
    wait(0,pin,2)
    wait(0,pin,3) 
    set(pins, 2) [3] 
    wait(0,pin,0)
    wait(0,pin,1)
    wait(0,pin,2)
    wait(0,pin,3) 
    set(pins, 4) [3] 
    wait(0,pin,0)
    wait(0,pin,1)
    wait(0,pin,2)
    wait(0,pin,3) 
    set(pins, 8) [3]
    wait(0,pin,0)
    wait(0,pin,1)
    wait(0,pin,2)
    wait(0,pin,3) 
    wrap()
    
@rp2.asm_pio()
def keypad():
    wrap_target()
    label("start")
    jmp(pin, "which_row") 
    jmp("start")
    label("which_row")
    in_(pins,4)
    push() 
    irq(rel(0)) [31] 
    label("delay")
    nop() [31]
    jmp(pin, "delay") # wait for key to be released
    wrap()
    
def getkeyvalue(sm):
    row=-1
    if sm == rp2.StateMachine(0):
        row=0
    elif sm == rp2.StateMachine(1):
        row=1
    elif sm == rp2.StateMachine(2):
        row=2
    elif sm == rp2.StateMachine(3):
        row=3
    
    while sm.rx_fifo() :
        term = sm.get()
        for i in range(4):
            if term >> i == 1:
                print(keyvalues[row][i])
                break;

keyvalues=[["1","4","7","*"],
           ["2","5","8","0"],
           ["3","6","9","#"],
           ["A","B","C","D"]]

col1pin=Pin(6, Pin.IN, Pin.PULL_DOWN)
col2pin=Pin(7, Pin.IN, Pin.PULL_DOWN)
col3pin=Pin(8, Pin.IN, Pin.PULL_DOWN)
col4pin=Pin(9, Pin.IN, Pin.PULL_DOWN)

smc1=rp2.StateMachine(0, keypad, freq=2000,  in_base=Pin(2),jmp_pin=col1pin)
smc2=rp2.StateMachine(1, keypad, freq=2000,  in_base=Pin(2),jmp_pin=col2pin)
smc3=rp2.StateMachine(2, keypad, freq=2000,  in_base=Pin(2),jmp_pin=col3pin)
smc4=rp2.StateMachine(3, keypad, freq=2000,  in_base=Pin(2),jmp_pin=col4pin)

smr = rp2.StateMachine(4, scanRowHigh, freq=2000, set_base=Pin(2), in_base=col1pin)
smr.active(1)

smc1.irq(getkeyvalue)
smc2.irq(getkeyvalue)
smc3.irq(getkeyvalue)
smc4.irq(getkeyvalue)
smc1.active(1)
smc2.active(1)
smc3.active(1)
smc4.active(1)
👉第二版
from machine import Pin
import rp2

@rp2.asm_pio(set_init=(rp2.PIO.OUT_LOW,)*4)
def keypad():
    wrap_target()
    label("set_row_1")
    set(pins, 1) [31]  #set row_1 High and wait for button bounce
    set(x,0x1)
    in_(pins,4)
    mov(y, isr)
    jmp(not_y,"set_row_2")
    jmp("rx_fifo")
    
    label("set_row_2")
    set(pins, 2) [31]
    set(x,0x2)
    in_(pins,4)
    mov(y, isr)
    jmp(not_y,"set_row_3")
    jmp("rx_fifo")
    
    label("set_row_3")
    set(pins, 4) [31]
    set(x,0x4)
    in_(pins,4)
    mov(y, isr)
    jmp(not_y,"set_row_4")
    jmp("rx_fifo")
    
    label("set_row_4")
    set(pins, 8) [31]
    set(x,0x8)
    in_(pins,4)
    mov(y, isr)
    jmp(not_y,"set_row_1")
  
    
    label("rx_fifo")
    push()              #push y(col) 
    in_(x,4)[2]
    push()              #and then x(row)
    irq(rel(0))
    
    wait(0,pin,0)  # check whether key is released
    wait(0,pin,1)
    wait(0,pin,2)
    wait(0,pin,3)
    wrap()   # total 31 instructions
    
keys=[["1","2","3","A"],
      ["4","5","6","B"],
      ["7","8","9","C"],
      ["*","0","#","D"]]

def keypad_irq(sm):
    y=sm.get()
    x=sm.get()
    for i in range(4):
        if x >> i == 1:
            break;
    for j in range(4):
        if y >> j == 1:
            break;
    print(keys[i][j])
    
smkeypad=rp2.StateMachine(0, keypad, freq=2000, set_base=Pin(2), in_base=Pin(6))
smkeypad.irq(keypad_irq)
smkeypad.active(1)

2022年9月2日 星期五

Step by step to build a PPPoE DualStack(IPv6 & IPv4)Wi-Fi Router with FreeBSD 13.1

本文介紹使用FreeBSD建構一個透過PPPoE連線ISP,具有IPv6與IPv4的Wi-Fi Router。架構如下圖所示:

FreeBSD有一張有線網卡連接ISP,無線網卡設成hostap模式,讓client連線。並同時具有IPv6與IPv4模式。

一、PPPoE連線設定:

使用FreeBSD內定的ppp程式連線,設定擋在/etc/ppp/ppp.conf 、 /etc/ppp/ppp.linkup和/etc/ppp/ppp.linkdown。ppp提供IPv4 nat功能,所以無須在防火牆開啟nat功能。
  1.  /etc/rc.conf的設定:
    ppp_nat="YES",可不設定,內定是開啟的。

  2. /etc/ppp/ppp.confg設定:
    ipcp與ipv6cp預設是開啟的,因此無須再額外指定,更改authname與authkey內容,
    set device PPPoE:em0,em0是連接ISP的網路介面名稱。

  3. /etc/ppp/ppp.linkup設定:
    此設定檔的目的是針對IPv6使用的,若僅是使用IPv4時無須設定此檔案內容。此設定檔的目的在Wi-Fi IPv6時再做說明。
  4. 在/etc/rc.conf增加:
    ipv6_activate_all_interfaces="YES"

    讓所有網路介面啟用IPv6功能。
  5. 重新啟動後,新增tun0已具有IPv4 nat功能,並透過ISP的IPv6 Router Advertisement(RA) 取得IPv6 address。

二、WiFi hostap設定:

  1. 查詢Wi-Fi介面driver是否具有hostap功能:
    檢查iwlwifi0與rtwn0兩個wifi介面,iwlwifi0不具有hostap功能。
  2. 在/etc/rc.conf中設定:
  3. 在/etc/hostap.conf設定ssid, key等:

  4. 使用dnsmasq作為Wi-Fi client的DNS, DHCP:
    # pkg install dnsmasq
    安裝dnsmasq package,dnsmasq可用來作為dns, IPv6 & IPv4 DHCP與Router Advertisement(RA),因此不用啟用FreeBSD內建的rtadvd的RA功能。

    在/etc/rc.conf中設定
    dnsmasq_enable="YES"
    啟用dnsmasq。

  5. /usr/local/etc/dnsmasq.conf設定內容:
    enable-ra: 在wlan0啟用IPv6 RA功能,因此不用啟用rtadvd。
    dhcp-range:分別為IPv4 range與IPv6 使用網路prefix自動建立IPv6 address。

三、使用DHCP-PD取得Wi-Fi端的IPv6 prefix delegation:

  1. wlan0介面無法直接透夠RS(router solicitation)向ISP取得IPv6 prefix,因此使用dhcp-pd透過tun0取得prefix delegation(PD),再自動設定wlan0的IPv6 address。然後在wlan0上執行RA(dnsmasq 的RA功能),則Wi-Fi Client連上AP時就可自動取得IPv6 Address
  2. 安裝dhcpv6 client:
    # pkg install dhcp6
    安裝KAME DHCP6 client。
  3. /usr/local/etc/dhcp6c.conf設定內容:
  4. 在/etc/rc.conf設定啟用:
    紅框框內為啟用dhcp ipv6 client,黃框框為啟用ip forwarding,綠色框框為啟用ipfw firewall,若啟用firewall則需注意放行dhcp ipv6 udp port與ipv6-icmp ra,rs, na,ns的type。

  5. 重啟動後將會發現wlan0無法取得IPv6 address。因此所有Wi-Fi Clients無法使用IPv6。因為系統啟動後執行dhcp6c(dhcp6c_enable="YES")要取得wlan0 的PD時,有可能tun0尚未自ISP透過RA取得IPv6 address, 因此執行dhcp6c失敗。所以在/etc/ppp/ppp.linkup 設定當tun0 link正常啟用後restart dhcp6c service,透過DHCP-PD取得IPv6 prefix delegation。

四、firewall要放行的ports

  1. ipv6-icmp types: RS(133), RA(134), NS(135), NA(136)
  2. ipv6 dhcp udp ports:  546, 547

五、Wi-Fi clients測試:

  1. android phone:

       

  2. iPhone:

       



  3. Windows:


六、完整設定檔:

  1. /etc/rc.conf
    #=== PPPoE ===
    ppp_enable="YES"
    ppp_mode="ddial"
    ppp_profile="myISP"
    #==== ipv6 dhcp client ===
    dhcp6c_enable="YES"
    dhcp6c_interfaces="tun0"
    ipv6_activate_all_interfaces="YES"
    #=== Firewall ====
    firewall_enable="YES"
    firewall_script="/etc/ipfw.rules"
    firewall_logging="YES"
    gateway_enable="YES"  
    ipv6_gateway_enable="YES"
    #=== WiFi hostap===
    wlans_rtwn0="wlan0"
    create_args_wlan0="wlanmode hostap"
    ifconfig_wlan0="inet 192.168.2.1 netmask 255.255.255.0"
    hostapd_enable="YES"
    dnsmasq_enable="YES"


  2. /etc/ppp/ppp.conf
    default:
    set log Phase tun IPV6CP command
    set timeout 180                        # 3 minute idle timer (the default)
    #=== myISP PPPoE
    myISP:
    set device PPPoE:em0     #em0 is your interface connected to ISP
    set authname your_ISP_authname  
    set authkey your_ISP_authkey
    set dial
    set login
    set mru 1492
    set mtu 1492
    delete ALL
    resolv reload
    enable dns
    add! default HISADDR                   # Add a (sticky) default route
    add! default HISADDR6


  3. /etc/ppp/ppp.linkup
    myISP:
    ! sh -c "/usr/sbin/service dhcp6c restart"


  4. /etc/hostapd.conf
    interface=wlan0
    debug=1
    ctrl_interface=/var/run/hostapd
    ctrl_interface_group=wheel
    ssid=myAP_SSID
    wpa_passphrase=passw0rd
    wpa=2
    wpa_key_mgmt=WPA-PSK
    wpa_pairwise=CCMP


  5. /usr/local/etc/dhcp6c.conf
    interface tun0 {
           send    ia-pd 0;
           send    rapid-commit;
           request domain-name-servers;
    };

    id-assoc pd 0 {
    #  prefix ::/64 1800;
     prefix-interface wlan0 {
       sla-id 0;
       sla-len 0;
     };
    };


  6. /usr/local/etc/dnsmasq.conf
    interface=wlan0
    dhcp-range=192.168.2.100,192.168.2.200,255.255.255.0,12h
    dhcp-range=::,constructor:wlan0,ra-only,12h
    enable-ra


七、相關影片連結: