当前位置: 首页 > news >正文

模板

        此功能允许把某些源文件中代码的 .text、.rodata、.data 和 .bss 字段放置在指定的内存区域中。这里的源文件可以是一个单独的 xxx.c 文件,也可以是一个库。指定的内存可以是片内的 RAM 也可以是片外的 QSPI flash。nRF connect SDK 通过脚本 zephyr/scripts/build/gen_relocate_app.py 来实现这一功能。将代码放置到片外 flash 直接通过 XIP 运行,目前只适用于 nRF52840 和 nRF5340 两款芯片。XIP是外设 QSPI 的一部分,通过普通 SPI 扩展的 flash 不能通过 XIP 直接运行代码。由于在 nRF52840 上,  Errata [215] 和 Errata [216] 会对 XIP 的使用造成很大的局限。所以我们更推荐在 nRF5340 上使用这个功能。

准备工作

  • 本文使用 NCS 2.9.1 中的例程代码 zephyr\samples\application_development\code_relocation_nocopy。
  • 硬件使用 nRF5340DK

 

GPIO 控制

  • 首先我们演示如何删除原有的按键和LED的 node 。按照下面的代码,来修改 devicetree,就可以删除 button3 和 led3。
     / {aliases {/delete-property/ sw3;/delete-property/ led3;};
    };/delete-node/ &button3;
    /delete-node/ &led3;
    

      

  • 接着我们来更改控制 led2 的管脚。这里我们用 P0.04 控制 led2。
    &led2 {gpios = <&gpio0 4 GPIO_ACTIVE_LOW>;
    };
    

     

  • 最后我们添加一个用户 GPIO 。这里添加了一个名为 user_gpios 的 node。然后又定义了 user_io0,它是 user_gpios 的 subnode。            
    / {user_gpios {compatible = "gpio-leds";user_io0: user_io0 {gpios = <&gpio0 16 GPIO_ACTIVE_LOW>;label = "user gpio 0";};};
    };
    
    我们不仅在 devicetree 里添加这个 GPIO ,还要在 main.c 里添加代码使用这个GPIO。下面这句代码中,我们声明了结构体变量  user_gpio0,并用宏 GPIO_DT_SPEC_GET 根据 devicetree 里的定义初始化它。
    const struct gpio_dt_spec user_gpio_0 = GPIO_DT_SPEC_GET(DT_NODELABEL(user_io0),gpios);
    
    下面这段代码中  gpio_is_ready_dt 是用来检查 GPIO 的状态是否是就绪。用函数 gpio_pin_configure_dt 把 user_gpio_0 配置成输出。gpio_pin_toggle_dt 用来翻转 GPIO。
    	if (!gpio_is_ready_dt(&user_gpio_0)) {printk("%s: device not ready.\n", user_gpio_0.port->name);return 0;}gpio_pin_configure_dt(&user_gpio_0, GPIO_OUTPUT_ACTIVE);for (index = 0; index < 100; index++) {gpio_pin_toggle_dt(&user_gpio_0);k_sleep(K_MSEC(100));}
    
    从下面的代码可以看出翻转 GPIO 这个操作有两种 API 可以调用。二者的主要区别是 gpio_pin_toggle_dt 不需要指明引脚 。
    /*** @brief Toggle pin level from a @p gpio_dt_spec.** This is equivalent to:**     gpio_pin_toggle(spec->port, spec->pin);** @param spec GPIO specification from devicetree* @return a value from gpio_pin_toggle()*/
    static inline int gpio_pin_toggle_dt(const struct gpio_dt_spec *spec)
    {return gpio_pin_toggle(spec->port, spec->pin);
    }
    

 

Button 控制

static void button_changed(uint32_t button_state, uint32_t has_changed)
{uint32_t buttons = button_state & has_changed;if (buttons & DK_BTN1_MSK) {printk("Button 1 pressed\n");}if (buttons & DK_BTN2_MSK) {printk("Button 2 pressed\n");}
}

 

I2C 设备控制

Nordic 的芯片中 I2C 接口是由外设 TWI 来实现的,I2C master 由 TWIM 实现, I2C master 由 TWIS 实现。这里将演示如何用一个 TWIM 来连接两个 I2C slave 设备。

  • 首先我们还是先修改 devicetree。我们使用 i2c1 这个 node。 一方面按照应用的要求修改这个 node 的 propertise,另一方面在这个 node 里创建两个 sub-node。
    1. i2c 的时钟频率通过 clock-frequency 来定义。
    2. i2c 的引脚通过 pinctrl-0 和 pinctrl-1 定义。我们将在后面分析 i2c1_default 和 i2c1_sleep 的定义。
    3. 这两个 sub-node 一个是 user_i2c_sensor,另一个是 user_i2c_eeprom。这两个 sub-node 通过 propertise reg 来定义各自的 I2C 地址。  
      &i2c1 {status = "ok";clock-frequency = <I2C_BITRATE_STANDARD>;pinctrl-0 = < &i2c1_default >;pinctrl-1 = < &i2c1_sleep >;pinctrl-names = "default", "sleep";user_i2c_sensor: user_i2c_sensor@0 {compatible = "i2c-user-define";reg = <0xA>;};user_i2c_eeprom: user_i2c_eeprom@0 {compatible = "i2c-user-define";reg = <0x5>;};       
      };        
      

         

    4. i2c1_default 和 i2c1_sleep的定义如下。TWIM_SDA 信号使用的是引脚 P0.04,TWIM_SCL 信号使用的是引脚 P0.03。          
      &pinctrl {i2c1_default: i2c1_default {group1 {psels = <NRF_PSEL(TWIM_SDA, 0, 4)>,<NRF_PSEL(TWIM_SCL, 0, 3)>;};};i2c1_sleep: i2c1_sleep {group1 {psels = <NRF_PSEL(TWIM_SDA, 0, 4)>,<NRF_PSEL(TWIM_SCL, 0, 3)>;low-power-enable;};};};
      

 

  • 修改 prj.conf 添加 CONFIG_I2C=y
  • 修改完 devicetree 我们在来添加操作 i2c 的代码。分别定义 i2c1_sensor 和 i2c1_eeprom,它们对应刚才 i2c1 的两个子节点。
    const struct i2c_dt_spec i2c1_sensor = I2C_DT_SPEC_GET(DT_NODELABEL(user_i2c_sensor));
    const struct i2c_dt_spec i2c1_eeprom= I2C_DT_SPEC_GET(DT_NODELABEL(user_i2c_eeprom));
    
    i2c 设备在读写操作前无需调用 API 来配置 ,直接调用下面的写函数。
    	err = i2c_write_dt(&i2c1_sensor, buf, 1);err = i2c_write_dt(&i2c1_eeprom, buf, 1);
    
    通过逻辑分析仪我们可以看到如下的总线数据,操作的目标地址分别是我们在 devicetree 里设置的数值 0x05 和 0x0A 。

 

SPI 设备控制

Nordic 的芯片中 SPI 接口的 master 端通过 SPIM 实现, slave 端通过 SPIS 实现。这里将演示如何用一个 SPIM 来连接两个 SPI slave 设备。

  • 首先修改 devicetree。
    1. 这里我们使用 spi2, 并且关闭 spi1。在 nordic 的nRF52 系列芯片中,相同数字编号的 TWIM, TWIS, SPIM, SPIS 是共用一组硬件模块的。在上面 i2c 中我们已经使用 i2c1, 所以这里我们就不能同时使用 spi1了。
    2. cs-gpios 定义了 P0.26 和 P0.27 两 个CS 信号。 SPI 用不同的片选信号,区分不同的 slave 设备。
    3. devicetree node spi2 下定义了两个 sub-node 分别是 user_spi_adc 和 user_spi_flash。 sub-node 里定义了三个 propertise。propertise compatible 的取值来自于我们在工程里新添加的文件 dts\bindings\spi-user-define.yaml。 propertise reg 的取值和前面的 propertise cs-gpios 呼应,reg = <0> 的 sub-node 使用 cs-gpios 里面定义的第一个 CS 引脚。reg = <1> 的 sub-node 使用 cs-gpios 里面定义的第二个 CS 引脚。propertise spi-max-frequency 定义 SPI 的时钟频率。两个不同的 SPI 设备可以使用不同的时钟频率驱动。                                                            
      &spi1 {status = "disabled";
      };	
      &spi2 {status = "okay";cs-gpios = <&gpio0 26 GPIO_ACTIVE_LOW>,<&gpio0 27 GPIO_ACTIVE_LOW>;pinctrl-0 = < &spi2_default >;pinctrl-1 = < &spi2_sleep >;pinctrl-names = "default", "sleep";user_spi_adc: user_spi_adc@0 {compatible = "spi-user-define";reg = <0>;spi-max-frequency = <DT_FREQ_M(8)>;}; user_spi_flash: user_spi_flash@0 {compatible = "spi-user-define";reg = <1>;spi-max-frequency = <DT_FREQ_M(8)>;};        
      };       
      

                                                                                                                                                                                                                                                                                                             

    4.  来看一下我们新添加的 dts\bindings\spi-user-define.yaml 里面的内容。如下图 spi-user-define.yaml 里面包含了 spi-device.yaml 文件,这个文件的位置在目录 zephyr\dts\bindings\spi 。
      compatible: "spi-user-define"include: [spi-device.yaml]
      

        

      spi-device.yaml 文件里面定义了 spi 节点需要的一些 propertise。   比如我们在 sub-node 里定义的 propertise spi-max-frequency。 
      # Copyright (c) 2018, I-SENSE group of ICCS
      # SPDX-License-Identifier: Apache-2.0# Common fields for SPI devicesinclude: [base.yaml, power.yaml]on-bus: spiproperties:reg:required: truespi-max-frequency:type: intrequired: truedescription: Maximum clock frequency of device's SPI interface in Hzduplex:type: intdefault: 0description: |Duplex mode, full or half. By default it's always full duplex thus 0as this is, by far, the most common mode.Use the macros not the actual enum value, here is the concordancelist (see dt-bindings/spi/spi.h)0    SPI_FULL_DUPLEX2048 SPI_HALF_DUPLEXenum:- 0- 2048frame-format:type: intdefault: 0description: |Motorola or TI frame format. By default it's always Motorola's,thus 0 as this is, by far, the most common format.Use the macros not the actual enum value, here is the concordancelist (see dt-bindings/spi/spi.h)0     SPI_FRAME_FORMAT_MOTOROLA32768 SPI_FRAME_FORMAT_TIenum:- 0- 32768spi-cpol:
      

       

    5. SPI 引脚定义如下 CLK P0.28, MISO P0.29, MOSI P0.30。
          spi2_default: spi2_default {group1 {psels = <NRF_PSEL(SPIM_SCK, 0, 28)>,<NRF_PSEL(SPIM_MISO, 0, 29)>,<NRF_PSEL(SPIM_MOSI, 0, 30)>;};};spi2_sleep: spi2_sleep {group1 {psels = <NRF_PSEL(SPIM_SCK, 0, 28)>,<NRF_PSEL(SPIM_MISO, 0, 29)>,<NRF_PSEL(SPIM_MOSI, 0, 30)>;low-power-enable;};};  
      

        

  • 修改 prj.conf 添加 CONFIG_SPI=y   CONFIG_SPI_ASYNC=y。
  • 在 main.c 里添加 SPI 的应用代码。下面这段代码定义了两个结构体变量,并通过宏 SPI_DT_SPEC_GET 用 devicetree 里的参数初始化了这两个结构体变量。
    #define SPI_OP     SPI_OP_MODE_MASTER | SPI_MODE_CPOL | SPI_MODE_CPHA \| SPI_WORD_SET(8) | SPI_LINES_SINGLE
    static struct spi_dt_spec spim2_adc = SPI_DT_SPEC_GET(DT_NODELABEL(user_spi_adc), SPI_OP, 0);
    static struct spi_dt_spec spim2_flash = SPI_DT_SPEC_GET(DT_NODELABEL(user_spi_flash), SPI_OP, 0);
    

      

    spi 驱动支持多 buffer 所以要定义 buffer 个数,和每个 buffer 的长度。同样 spi 在读写之前无需调用配置函数,直接调用读写函数就行  
    	struct spi_buf_set tx_bufs;struct spi_buf spi_tx_buf;tx_bufs.buffers = &spi_tx_buf;tx_bufs.count = 1;spi_tx_buf.buf = buf;spi_tx_buf.len = 2;err = spi_write_dt(&spim2_adc, &tx_bufs);err = spi_write_dt(&spim2_flash, &tx_bufs);
    

 

   下面是SPI的波形。可以看到和不同的 spi slave 设备通讯的时候, spi master 会拉低不同的 CS 引脚。            

 

  

UART 控制

Nordic 的芯片中 UART 接口叫做 UARTE。这里的 E 是指 EasyDMA , UART 可以使用 DMA 来连续收发。

  • 修改 Devicetree。这里使用 uart1。propertise current-speed 设置 uart 的波特率。 
    &uart1 {status = "okay";current-speed = <115200>;pinctrl-0 = < &uart1_default >;pinctrl-1 = < &uart1_sleep >;pinctrl-names = "default", "sleep";
    };
    

      

    TXD pin 为 P1.02, RXD pin 为 P1.01。                                                                                       
    	uart1_default: uart1_default {group1 {psels = <NRF_PSEL(UART_RX, 1, 1)>;bias-pull-up;};group2 {psels = <NRF_PSEL(UART_TX, 1, 2)>;};};uart1_sleep: uart1_sleep {group1 {psels = <NRF_PSEL(UART_RX, 1, 1)>,<NRF_PSEL(UART_TX, 1, 2)>;low-power-enable;};};
    

 

  • 修改 prj.conf 在里面添加 CONFIG_UART_ASYNC_API=y    CONFIG_UART_ASYNC_RX_HELPER=y。
  • 修改 main.c 添加 uart 收发代码。 uart_callback_set 设置 callback 函数 uart_cb。因为这里采用的是异步收发的模式,所以设置callback 函数是必备的。uart_rx_enable 使能接收。uart_tx 发送数据。
    	err = uart_callback_set(uart1, uart_cb, NULL);//printk("uart_callback_set return %d\n", err);err = uart_rx_enable(uart1, uart_rx_buf, MAX_UART_BUF_LEN, UART_RX_TIMEOUT_MS);//printk("uart_rx_enable return %d\n", err);err = uart_tx(uart1, uart_tx_buf, 6, SYS_FOREVER_MS);//printk("uart_tx return %d\n", err);
    

      

    callback 函数 uart_cb 可能由多种事件触发。比如当接收到数据后会触发回调,并在参数 EVT 传递 UART_RX_RDY 和接收到的数据和长度。
    static void uart_cb(const struct device *dev, struct uart_event *evt, void *user_data)
    {ARG_UNUSED(dev);//LOG_INF("uart_cb evt->type:%d", evt->type);switch (evt->type) {case UART_TX_DONE:printk("UART_TX_DONE\n");break;case UART_RX_RDY:printk("UART_RX_RDY\n");printk("received %d bytes\n", evt->data.rx.len);break;case UART_RX_DISABLED:printk("UART_RX_DISABLED\n");break;case UART_RX_BUF_REQUEST:printk("UART_RX_BUF_REQUEST\n");uart_rx_buf_rsp(uart1, uart_rx_buf2, MAX_UART_BUF_LEN);break;case UART_RX_BUF_RELEASED:printk("UART_RX_BUF_RELEASED\n");break;case UART_TX_ABORTED:printk("UART_TX_ABORTED\n");break;default:break;}
    }
    

 

  • 我们在 DK 上把 TXD 引脚和 RXD 引脚短接来测试 UART 的收发,可以看到如下的 log 信息。UART 收到了自己发送的6字节的数据。 

  

UART 应用代码的优化

上面的 uart 演示代码中,我们只实现了简单的收发。下面我们将进一步在此基础上优化 UART 的收发代码。这一部分的修改都在 main.c 里,主要涉及下面几个部分:

  1. Thread 线程
  2. Semaphore 信号量
  3. 线程间通讯 Message queue

 

  • 线程 下面的代码中通过 K_THREAD_DEFINE 定义了 一个独立的线程来处理 uart 相关的代码。线程处理函数 uart_thread_task 中:也是先用 uart_callback_set 设置了回调函数;再用 uart_rx_enable 使能了接收;然后是一个 for 循环,在里面不断的接收消息,根据消息中的指令发送数据,或者处理接收到的数据。     
    #define UART_THREAD_STACK_SIZE    512
    #define UART_THREAD_PRIORITY      -1void uart_thread_task(void)
    {int err;struct uart_data_item_type uart_msgq;k_sem_take(&uart_thread_start, K_FOREVER);printk("uart_thread_task\n");err = uart_callback_set(uart1, uart_cb, NULL);err = uart_rx_enable(uart1, uart_rx_buf, MAX_UART_BUF_LEN, UART_RX_TIMEOUT_MS);for (;;) {k_msgq_get(&uart_data_msgq, &uart_msgq, K_FOREVER);printk("received uart data item\n");switch(uart_msgq.cmd) {case UART_CMD_TX:memcpy(uart_tx_buf,&uart_msgq.data, sizeof(uint32_t));err = uart_tx(uart1, uart_tx_buf, sizeof(uint32_t), SYS_FOREVER_MS);break;case UART_CMD_DATA_PROCESS:break;default:break;		}}
    }K_THREAD_DEFINE(uart_thread_id, UART_THREAD_STACK_SIZE, uart_thread_task, NULL, NULL,NULL, UART_THREAD_PRIORITY, 0, 0);
    

    上面的代码中用 K_THREAD_DEFINE 定义线程的时候,需要指定此线程的优先级 UART_THREAD_PRIORITY。UART_THREAD_PRIORITY 的数据类型是 integer,可以是正数也可以是负数。优先级的数字越小,优先级越高,负数的优先级比正数高。thread 的优先级取值为负数时,此 thread 为协同线程 cooperative thread 。当这种线程正在执行的时候,其它更高优先级的线程不能打断它,必须等它执行完再执行下一个线程。当 thread 的优先级取值为正数,此 thread 为抢占线程 preemptible thread。当这种线程正在执行的时候,其它更高优先级的线程可以打断它,跳转到高优先级的任务。等高优先级的线程执行完才返回原 thread 继续执行。回到例程代码,从应用的角度出发,我们希望 uart_thread_task 的执行优先级大于 main 函数。通过查询文件 build\zephyr\.config 我们得知 CONFIG_MAIN_THREAD_PRIORITY 的取值为 0,也就是说 main thread 当前的优先级为 0, 所以我们定义了 UART_THREAD_PRIORITY 为 -1。这样 uart thread 的优先级就高于 main thread, 而且 uart thread 的执行不会被其它更高优先级的 thread 打断。需要注意的是这里的不能被打断只是对 thread 而言,中断是可以打断 cooperative thread 的。

  • 信号量 函数 uart_thread_task 的优先级比 main 函数高,所以会先于main 函数执行。如果之前的函数 uart_thread_task 里没有 k_sem_take(&uart_thread_start, K_FOREVER),就会出现如下图的现象。我们看到 uart thread 的 log 是先于 main thread 被打印出来的。 

     

     

    从应用的角度,我们希望 uart_thread_task  在 main 函数启动完广播之后再执行。这就引入了一个不同线程之间的同步问题。Zephyr RTOS 中可以通过 semaphore 解决不同 thread 间的同步问题。下面的代码中通过 K_SEM_DEFINE 定义了一个为 uart_thread_start 的 semaphore 。 函数 uart_thread_task 执行到函数 k_sem_take 时,如果 uart_thread_start 没有被释放,当前 thread 会被挂起等待,直到 semaphore 被释放。    
    static K_SEM_DEFINE(uart_thread_start, 0, 1);#define UART_THREAD_STACK_SIZE    512
    #define UART_THREAD_PRIORITY      -1void uart_thread_task(void)
    {int err;struct uart_data_item_type uart_msgq;k_sem_take(&uart_thread_start, K_FOREVER);printk("uart_thread_task\n");err = uart_callback_set(uart1, uart_cb, NULL);err = uart_rx_enable(uart1, uart_rx_buf, MAX_UART_BUF_LEN, UART_RX_TIMEOUT_MS);for (;;) {
    

      

    在 main 里通过 k_sem_give 释放 uart_thread_start。uart 线程会打断当前的 main thread 从 k_sem_take 继续执行。
    	err = spi_write_dt(&spim2_adc, &tx_bufs);err = spi_write_dt(&spim2_flash, &tx_bufs);k_sem_give(&uart_thread_start);struct uart_data_item_type main_msgq;main_msgq.cmd = UART_CMD_TX;main_msgq.data = 0;for (;;) {while (k_msgq_put(&uart_data_msgq, &main_msgq, K_NO_WAIT) != 0) {/* message queue is full: purge old data & try again */k_msgq_purge(&uart_data_msgq);}main_msgq.data++;dk_set_led(RUN_STATUS_LED, (++blink_status) % 2);k_sleep(K_MSEC(RUN_LED_BLINK_INTERVAL));}
    

 

  • 线程间通讯 演示代码中 main thread 会把要发送的数据通过线程通讯发送到  uart thread, uart thread 调用驱动函数发送。zephyr 中提供了多种线程间通讯方式,具体如下图,这里使用的是 message queue。下面的代码中 K_MSGQ_DEFINE 定义了一个名为 uart_data_msgq 的 message queue。uart_data_msgq 的缓冲区里最多可以容纳 8 个消息。
    struct uart_data_item_type {uint8_t cmd;uint32_t data;
    };K_MSGQ_DEFINE(uart_data_msgq, sizeof(struct uart_data_item_type), 8, 4);
    

      

    下面这段代码来自于 main thread 的 main 函数。代码会定时循环把待发送的数据打包成一个 message,然后推送到 message queue 里面。
        struct uart_data_item_type main_msgq;main_msgq.cmd = UART_CMD_TX;main_msgq.data = 0;for (;;) {while (k_msgq_put(&uart_data_msgq, &main_msgq, K_NO_WAIT) != 0) {/* message queue is full: purge old data & try again */k_msgq_purge(&uart_data_msgq);}main_msgq.data++;dk_set_led(RUN_STATUS_LED, (++blink_status) % 2);k_sleep(K_MSEC(RUN_LED_BLINK_INTERVAL));}
    

      

    下面的代码来自 uart thread 的 uart_thread_task 函数。 函数等待 message queue 里推送来的 message。得到 message 后,根据里面的 cmd 字段来处理发送或者接收数据。
    void uart_thread_task(void)
    {int err;struct uart_data_item_type uart_msgq;k_sem_take(&uart_thread_start, K_FOREVER);printk("uart_thread_task\n");err = uart_callback_set(uart1, uart_cb, NULL);err = uart_rx_enable(uart1, uart_rx_buf, MAX_UART_BUF_LEN, UART_RX_TIMEOUT_MS);for (;;) {k_msgq_get(&uart_data_msgq, &uart_msgq, K_FOREVER);printk("received uart data item\n");switch(uart_msgq.cmd) {case UART_CMD_TX:memcpy(uart_tx_buf,&uart_msgq.data, sizeof(uint32_t));err = uart_tx(uart1, uart_tx_buf, sizeof(uint32_t), SYS_FOREVER_MS);break;case UART_CMD_DATA_PROCESS:break;default:break;		}}
    }
    

      

    下面是加入线程间通讯的代码后得到的 log,当我们把 TX 和 RX 引脚短接后可以看出 uart thread 不断的发送从 main thread 传输的数据。                                                  

                                                 

 总结

本文从实际操作出发,介绍了用户最常用的一些外设如 GPIO, I2C, SPI, UART 的配置和使用方法。并介绍了一些简单 RTOS 组件的应用如 thread, semaphore, message queue。希望能帮助 Nordic 用户加快 nRF Connect SDK 的开发速度。                                                                                                                                                                                                                                                                                                                               

 

http://www.hskmm.com/?act=detail&tid=185

相关文章:

  • kylin V11安装mysql8.0
  • 【Kubernetes】 PVC 和 PV
  • Docker镜像
  • idea 允许多运行java示例 idea2022版本
  • ROS2环境配置
  • 2025年第五届电子信息工程与计算机科学国际会议(EIECS 2025)
  • P6477 [NOI Online #2 提高组] 子序列问题 题解
  • iframe 跨域通信实战:可视化编辑器的技术实现
  • windows项目下统计代码行数
  • 。。。
  • ETF 简介
  • 实时流式响应的 SSE 技术实现
  • 2025年艺术、教育和管理国际学术会议(ICAEM 2025)- 第五期
  • CF 1048 Div.2 解题报告
  • reLeetCode 热题 100-1 两数之和-扩展1 unordered_map实现 - MKT
  • 读书笔记:什么是对象表?
  • AI 服务路由策略:如何实现智能负载均衡
  • 在SQL语句中的别名
  • 多维度排序算法在企业级应用中的性能优化
  • 正则表达式在代码解析中的高级应用
  • vue3 项目中优雅的使用 SVG 图标(vite-plugin-svg-icons)
  • 自我介绍+软工5问
  • 车道线检测资料
  • 实现Jenkins不同账号只能看到各自任务的权限
  • 6 个最佳无代码 IT 资产管理工具推荐
  • python开发mcp入门
  • 建造者模式进阶:复杂AI服务的优雅构建
  • 代理模式在AI应用中的安全实践:AOP + 限流 + 权限控制
  • ​​高压差分探头:高电压测量的精密之眼​​
  • OCP认证烂大街了吗?别跟风问这个问题了