1. Blog>
  2. Juggling STM32 Blue Pill For Arduino Jugglers

Juggling STM32 Blue Pill For Arduino Jugglers

by: Aug 18,2020 7006 Views 0 Comments Posted in Technology

Aiduino Stm32 IoT Bluepill Internet of Things

Have you outgrown the Arduino Uno? Do you feel like Arduino Uno is giving you a straw to sip a big slice of cake, one tiny crumb at a time?

You are not alone! Many Arduino Jugglers have the same sentiment — Arduino developers who juggle multiple sensors, aggregate the data from sensors and stream the aggregated data to the cloud, all at the same time. We all wished for something better than a straw.

The Arduino Uno is a great tool for learning IoT. Friendly development tools, affordable development kits, huge base of open source code. The real world however doesn’t run on Arduino microcontrollers and 8-bit AVR CPUs. We are surrounded by cheap and powerful 32-bit Arm CPUs and microcontrollers by STMicroelectronics (STM), Texas Instruments, ….

Upsizing from Arduino Uno to a 32-bit STM microcontroller doesn’t have to be hard, this tutorial will show you how. All you need is some open source software and these two gadgets…

STM32 Blue Pill with headers (left) and ST-Link V2 USB Debugger (right)

1.“Blue Pill”: STM32 microcontroller (STM32F103C8T6 with 32-bit Arm Cortex-M3 CPU) that’s soldered on a breakout board for easy prototyping (connects to a breadboard). The Blue Pill is cheap (under US$ 2) and very popular with embedded developers in China. It has 20 KB of Static RAM and 64 KB of Flash Memory. (Compare that with Arduino Uno’s 2 KB Static RAM, 32 KB Flash Memory!)

2.ST-Link V2 USB Debugger (about US$ 2): Connects the Blue Pill to your computer via USB. We’ll be using the ST-Link to flash our compiled program into the Blue Pill.

You can buy the Blue Pill and the ST-Link Debugger on Asian hardware sites like AliExpress. Just search for the keywords:

  • stm32f103c8t6 development board
  • st-link v2


Preparing The Blue Pill

For this tutorial there’s no need to solder the pointy yellow headers onto the Blue Pill. You don’t need any sensors, transceivers or even a breadboard — we’ll be using two components simulated in STM32 code: (1) Bosch BME280 sensor (for temperature, humidity and altitude) and (2) Wisol WSSFM10R4AT transceiver (for the Sigfox network).

Connect the Blue Pill to the ST-Link USB Debugger like this:

Connecting the Blue Pill to the ST-Link USB Debugger

Note: For the ST-Link, only the bottom row of pins is used. The Micro USB Port of the Blue Pill doesn’t need power — the program in this article runs fine on the Blue Pill powered by the ST-Link.

Both jumpers should be set to the “0” positions

Ensure that both yellow jumpers on the Blue Pill (BOOT0 and BOOT1 near the Micro USB Port) are set to the “0” positions (closer to the Micro USB Port). This configures the Blue Pill to operate in Flash Mode so that it’s ready to burn your programs into flash memory.

Jumper settings for the Blue Pill

Don’t connect the ST-Link to your computer yet, we need to install the Windows drivers for ST-Link, as described below.


While You’re Downloading And Installing…

You’re now reading the third article in the “juggling” series of tutorials about crafting multitasking IoT programs on the Arduino Uno with the cocoOS task scheduler. The first article shows you how to read the temperature, humidity and altitude sensors of the BME280 as separate tasks, instead of using the normal single-threaded Arduino loop…

The second article adds two tasks — for sending sensor data to the Sigfox network and for sending commands to the Wisol module over a serial port…

Now that we have stretched the Arduino Uno to the limit with FIVE concurrent tasks, this article will explain how we migrated that IoT program from Arduino to Blue Pill without changing the code base!

While you’re waiting to download and install the software, check out the two articles above to learn more about the send_altitude_cocoos program that we’ll be running shortly on the Blue Pill.

If you’re writing your own IoT program for Arduino or Blue Pill, you might consider using send_altitude_cocoos as the base since it already provides the necessary multitasking and networking functions.


Installing OpenOCD and ST-Link USB Driver

For Windows:

1.Download OpenOCD from the

Look for gnu-mcu-eclipse-openocd-...-win64.zip

2.Unzip the downloaded file and copy the OpenOCD files into c:\openocd such that openocd.exe is located in the folder c:\openocd\bin

3.Download the ST-Link USB driver from the

Click Get Software

4.Unzip the downloaded file. Double-click the driver installer:

dpinst_amd64.exe


For Mac:

1.Install brew

2.Enter the following in a command prompt:

brew install openocd


For Ubuntu:

1.Install the udev rules for the ST-Link

2.Enter the following in a command prompt:

sudo apt install openocd


What have we just installed?

1.OpenOCD (Open On-Chip Debugger) is the software that connects to the ST-Link Debugger and displays the debug log for the Blue Pill

2.ST-Link USB Driver is needed on Windows for OpenOCD to communicate with the ST-Link Debugger. It’s not needed on Mac and Ubuntu.


Downloading the source files

Open a Windows, Mac or Ubuntu command prompt and enter:

(For Windows, install git from Git For Windows)

git clone https://github.com/lupyuen/send_altitude_cocoos.git

cd send_altitude_cocoos

Download cocoOS_5.0.3 from http://www.cocoos.net/download.html

Copy the cocoOS_5.0.3 source files (inc/*.h, src/*.c) into send_altitude_cocoos at this subfolder:

lib/cocoOS_5.0.3/src

There should not be any folders inside lib/cocoOS_5.0.3/src

Refer to the screenshot.

Then enter the following into the command prompt…


For Windows:

scripts\linksrc.cmd


For Mac and Ubuntu:

chmod +x scripts/*.sh

scripts/linksrc.sh

The linksrc step is needed because PlatformIO expects the source files to be located in the src folder, so we create symbolic links for the source files. The linksrc step also replaces the default cocoOS configuration file os_defines.h with our custom configuration.


Installing Visual Studio Code and PlatformIO

For Arduino we use the Arduino IDE to compile and upload programs. For the Blue Pill, we’ll use Visual Studio Code and the free, unregistered version of PlatformIO. Both tools are open source and they work together so well as the Blue Pill IDE that it might even be better than the Arduino IDE.

Download and install Visual Studio Code from

For Windows, choose the User Installer

Launch Visual Studio Code. In the left menu bar, click the Extensions icon.

Install the following extensions:

1.PlatformIO (by PlatformIO — see above)

2.C/C++ (by Microsoft — see below)

If the Install button is missing for the C/C++ extension, then it’s already installed.

After installing, click Reload to restart Visual Studio Code

Click File → Open Workspace

Browse to the send_altitude_cocoos folder.

Select workspace.code-workspace

In a while you should have a fully configured Visual Studio Code workspace with PlatformIO. In the Workspace pane at left, double-click on platform.h and uncomment the following line so that you’re using the right features for this article…

#define CONFIG_ARTICLE3 // Uncomment to support Article #3

Check that the other #define CONFIG_ARTICLE... lines are commented out.

In the Workspace pane at left, double-click on main.cpp and you should see this…

Our Blue Pill IDE — Visual Studio Code with PlatformIO


Really, it’s the same Arduino code running on Blue Pill!

Let’s pause and look at main.cpp. That’s the exact same code that runs on both the Arduino and Blue Pill!

We’re not cheating here — the Blue Pill is not running the Arduino bootloader and pretending to be an Arduino. We’re using the full power of the Blue Pill through the native libopencm3 library!

platform_setup() and platform_start_timer() are the only two functions that were written specifically for Arduino and Blue Pill. All other functions are the same!

The overall flow of the program should be familiar if you have read the first two articles. Recall that we used these multitasking functions from cocoOS…

task_create() to create three Sensor Tasks (temperature, humidity, altitude), the UART Task and the Network Tasks, all running at different task priorities.

task_open() … task_close() to specify the code run by each task.

msg_post() … msg_receive() to send/receive sensor data and UART commands across tasks.

sem_wait() … sem_signal() to lock and unlock shared resources: I2C Bus and Network Buffer.

event_wait() … event_signal() for the UART Task to notify the Network Task that it has completed the UART command.

Now think for a moment… Why can’t we easily port any Arduino program to the Blue Pill? Hint: It has to do with the loop() program structure of Arduino. More about this in a while…

Command buttons in the status bar


Build, Upload and Monitor your Blue Pill program

Connect the ST-Link USB Debugger to your computer. Notice the buttons at the lower left? (See screen above) We’ll click them now. If the buttons are not visible, click the menu Terminal → Run Task → PlatformIO… to run the commands instead.

Building your Blue Pill program

Click the Build button.

This compiles your Blue Pill program and links it with the libopencm3 library.

The build step produces a Blue Pill executable program.

Uploading your program to the Blue Pill

Click the Upload button.

Our IDE connects to your Blue Pill via the ST-Link USB Debugger and writes the executable into the Flash Memory.

Your program will remain in the Flash Memory even after disconnecting the power. Just like the Arduino.

Connect to view Blue Pill debug log

Lastly, click in the menu: Terminal → Run Task

Select Connect To STM32 Blue Pill to see the debug log (displayed by OpenOCD). It works like the Arduino Serial Monitor.

Your very first Blue Pill program!

If the green onboard LED is blinking — Congratulations! Your Blue Pill program is now running on a real 32-bit Arm microcontroller! Not so hard right? Let’s take a closer look at the debug log.

But remember to click the “Trash” icon at the right when you’re done with the debug log.

This closes the Blue Pill connection so that you can continue uploading programs to the Blue Pill. Unfortunately the “Close” icon doesn’t close the connection — it only hides the debug log, keeping the connection open.

Blue Pill Debug Log and the task actions that triggered the messages

Here’s proof that your Blue Pill is really juggling multiple tasks

In the debug log above I have labelled the messages (1 to 7) and the corresponding tasks that triggered the messages. And this is what exactly we would expect a multitasking IoT program to do— reading the sensors concurrently while aggregating the data in another task and sending the data in yet another task. We call this the Intent of the program.

Could we deduce the program’s Intent by looking at the way main.cpp calls the cocoOS multitasking functions? Let’s try…

When we see task_create(), we deduce the actions that the program will perform as a concurrent task: Sensor Task, Network Task, UART Task.

When we see msg_post() … msg_receive(), we deduce which tasks are communicating and what data they send (e.g. sensor data).

When we see sem_wait() … sem_signal(), we deduce the shared resources: I2C Bus and Network Buffer.

Once again the mighty semaphore unfurls the true intent

This is highly unusual — it’s as though the program is sentient enough to tell us its true Intent! Understand the Intent is important because we can then reason about the program…

  • The sensors may be read concurrently as Sensor Tasks. Which can run independently with its own polling interval. And we can start more Sensor Tasks to handle more sensors.
  • So long as we control the sharing of resources: I2C Bus and Network Buffer.
  • There’s no need to transmit to the network every sample of sensor data that we read. The Network Task aggregates the data intelligently and transmits only when necessary. So we could throttle the transmission independently of the sensor polling. Or we could add more transceiver modules and Network Tasks to send data more frequently.

That’s terrific! We now have a solution to scale up our program, increase our concurrency and use up everything in the Blue Pill, until 64-bit microcontrollers become affordable! But what about Arduino?


Hidden Intentions of Arduino Programs

void loop() // Will be called repeatedly.
 // Read the ambient temperature, humidity, altitude.
 int scaledTemp = bme.readTemperature() * 10;
 int scaledHumidity = bme.readHumidity() * 10;   
 int scaledAltitude = bme.readAltitude(SEALEVELPRESSURE_HPA) * 10;
 Message msg(transceiver)// Will contain the structured sensor data.
 msg.addField("tmp", scaledTemp); // 4 bytes for the temperature (1 decimal place).
 msg.addField("hmd", scaledHumidity); // 4 bytes for the humidity (1 decimal place).
 msg.addField("alt", scaledAltitude); // 4 bytes for the altitude (1 decimal place).  
 msg.send(); // Send the encoded structured message.
 delay(10000); // Wait a while before looping. 10000 milliseconds = 10 seconds.
}

https://github.com/lupyuen/unabiz-arduino/blob/master/examples/send-altitude-structured/send-altitude-structured.ino#L91:1

Here’s the original Arduino code that inspired main.cpp. It’s clearly reading some sensors and sending to the network.

But does it really need to send everything that it reads?

Do we need to read the temperature as often as we read humidity?

Must we wait for the acknowledgement from the network before we read the sensors again?

The Intent is somewhere in there but it’s hidden like “Crouching Tiger Hidden Dragon”. It’s precisely because we don’t understand the intent of the program that it’s hard to scale up the Arduino program and use all the memory and CPU cycles in the Blue Pill.

That’s not the fault of the Arduino programmer — the Arduino has so little capacity that Arduino programmers are forced to code the Implementation directly into a single-threaded loop(), instead of declaring the Intent for the sake of the future.

We have learnt an important lesson today — when we are programming we should always make the Intent obvious (using cocoOS really helps) instead of diving straight into the Implementation. And that also answers the question why most Arduino programs can’t be ported easily to the Blue Pill — we simply don’t understand the Intent of these programs without rewriting them. So to make our Intent really clear, let’s all copy and paste the send_altitude_cocoos code in this article and use that as the Pattern for our IoT programs!


But if you really wish to migrate a classical Arduino program to the Blue Pill…

Here’s what you could do…

1.Merge your Arduino program’s setup() and loop() functions into a main() function. Delete the setup() and loop() functions and confirm that your program still runs on Arduino. Arduino runs the main() function if it finds it.

2.Start with the send_altitude_cocoos code from this article and replace the main() function in main.cpp by your own. Replace all debugging code like Serial.print() / println() by debug_print() / debug_println(). Build the code for Blue Pill.

3.You will encounter build errors because most Arduino APIs are not defined. Add stub functions to the folder stm32/porting until the Arduino APIs have been migrated to Blue Pill.


Highlights of the Blue Pill migration

bluepill Library

void enable_debug(void);  // Enable ARM Semihosting for displaying debug messages.
void disable_debug(void);  // Disable ARM Semihosting for displaying debug messages.
void platform_setup(void)// Initialise the STM32 Blue Pill platform.
void platform_start_timer(void (*tickFunc0)(void))// Start the STM32 Blue Pill Timer to generate interrupt ticks for cocoOS to perform task switching.
uint32_t millis(void)// Number of elapsed millisecond ticks. Compatible with Arduino.

void led_setup(void);  // Initialise the onboard LED.
void led_on(void);   // Switch the onboard LED on.
void led_off(void);   // Switch the onboard LED off.
void led_toggle(void)// Toggle the onboard LED on or off.
void led_wait(void);  // Delay a while before updating the LED state.

https://github.com/lupyuen/send_altitude_cocoos/blob/master/stm32/bluepill/bluepill.h

Arduino developers will surely miss Arduino.h, the huge library of common functions that’s available to all Arduino programs.

I don’t think it’s right to port the entire Arduino library to the Blue Pill because we’ll end up muddling Intent with Implementation again. But it’s perfectly OK to #include <bluepill.h> to switch the onboard LED on and off via led_on() and led_off(). That’s a valid Intent and it will probably cause Implementation issues if we don’t provide the LED functions.

// For Blue Pill LED: Enable GPIOC clock.
rcc_periph_clock_enable(RCC_GPIOC);
// Set GPIO13 (in GPIO port C) to 'output push-pull'.
gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO13);
gpio_clear(GPIOC, GPIO13); // Switch Blue Pill LED on.
gpio_set(GPIOC, GPIO13);  // Switch Blue Pill LED off.

// For Arduino LED: Initialise the Arduino Uno's onboard LED at D13 for output.
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH); // Switch Arduino LED on.
digitalWrite(LED_BUILTIN, LOW);  // Switch Arduino LED on.

https://github.com/lupyuen/send_altitude_cocoos/blob/master/stm32/bluepill/led.cpp

FYI controlling the Blue Pill LED is more complicated than the Arduino LED.

As you see from the code, it involves calling libopencm3 to set up the clock and enable the block of GPIO pins. The Blue Pill LED is connected to GPIO13 which is part of GPIO Port C.

And that’s before we actually set GPIO13 on and off to switch the LED on and off. The STM32 family of microcontrollers is huge, and libopencm3 has all the tedious nuances to cater for the tiniest differences in the microcontrollers.


Logging and Arm Semihosting

static int __semihost(int command, void* message) {
	// Send an ARM Semihosting command to the debugger, e.g. to print a message.
	// Warning: This code will trigger a breakpoint and hang unless a debugger is connected.
	// That's how ARM Semihosting sends a command to the debugger to print a message.
	// This code MUST be disabled on production devices.
  if (!logEnabled) return -1;
  __asm( 
   "mov r0, %[cmd] \n"
   "mov r1, %[msg] \n" 
   "bkpt #0xAB \n"// Output operand list: (nothing)// Input operand list:
		[cmd] "r" (command), 
		[msg] "r" (message)
	: // Clobbered register list:
		"r0", "r1", "memory"
	);
	return 0// TODO
}

https://github.com/lupyuen/send_altitude_cocoos/blob/master/stm32/logger/logger.cpp

Arm Semihosting is like Arm Wrestling, Semisweet Chocolate and 50% Sweetness Bubble Tea all rolled into one — nice but painful. When a Blue Pill program calls debug_print() to display a message on the debug console, it executes a Debug Breakpoint Instruction (bkpt #0xAB) to notify the debugger (that’s OpenOCD in our case).

But if our Blue Pill is running in production and there’s no debugger attached, the program will stop until a debugger is attached.

https://github.com/lupyuen/send_altitude_cocoos/blob/master/main.cpp

That’s why in main.cpp we make it mandatory for the developer to call enable_debug() or disable_debug() to indicate whether we have connected the debugger.

What if you forget to call disable_debug()? We have added a startup blink sequence to help you troubleshoot. At startup the onboard LED should blink…

On — Off — On — Off — On — …

But if you forgot to call disable_debug(), it will blink like this…

On — Off — On — Off — Stays Off


I2CInterface

uint8_t I2CInterface::requestFrom(uint8_t addr, uint8_t length) { // Used by BME280I2C.cpp
  // Simulate the handling of a request to read "length" number of bytes from the register at "dataRegister".
  // debug_print("i2c_request reg: "); debug_println(dataRegister);
  dataIndex = 0;
  switch (dataRegister) {
    case ID_ADDR: data = ID_DATA; dataLength = (uint8_t) sizeof(ID_DATA); break;
    case TEMP_DIG_ADDR: data = TEMP_DIG_DATA; dataLength = (uint8_t) sizeof(TEMP_DIG_DATA); break;
    case HUM_DIG_ADDR1: data = HUM_DIG_DATA1; dataLength = (uint8_t) sizeof(HUM_DIG_DATA1); break;
    case HUM_DIG_ADDR2: data = HUM_DIG_DATA2; dataLength = (uint8_t) sizeof(HUM_DIG_DATA2); break;
    case PRESS_ADDR: data = PRESS_DATA; dataLength = (uint8_t) sizeof(PRESS_DATA); break;
    case PRESS_DIG_ADDR: data = PRESS_DIG_DATA; dataLength = (uint8_t) sizeof(PRESS_DIG_DATA); break;
    default: debug_print("Unknown i2c reg: "); debug_println(dataRegister); data = ID_DATA; dataLength = (uint8_t) sizeof(ID_DATA);
  }
  return dataLength;
}

https://github.com/lupyuen/send_altitude_cocoos/blob/master/stm32/i2cint/i2cint.cpp

In this article we use a simulated BME280 I2C sensor because it’s easier to test. And because the Blue Pill lets us do simulated sensors! (Arduino Uno was too small to run any simulation code)

Our Blue Pill program uses the same BME280 library as Arduino without any changes. How did we do it? The sneaky way…

The BME280 library calls the Arduino Wire library to access the I2C Bus. Since the Wire library is not defined in Blue Pill, we provided our own version of the Wire library, which is injected via our own Wire.h.

Our version of the Wire library is in a C++ class named I2CInterface. This is where we intercept the I2C commands from the BME280 library and replay the responses that we have captured earlier on a real Arduino device.

To disable the BME280 I2C simulator and use the actual I2C port to connect to a real BME280 I2C module, check the instructions for “Enabling I2C Interface on STM32 Blue Pill”.

[ UPDATE: The complete I2C code has been added to I2C Interface ]


UARTInterface

static void simulateCommand(const char *cmd) {
  data[0] = 0;
  dataIndex = 0;
  dataLength = 0;
  dataTimestamp = millis() + 2000// Delay 2 seconds by default.
  const char *response = "OK"// Default response.
  if (strcmp(cmd, CMD_NONE) == 0) { // "AT": Empty placeholder command.
    // Default to "OK".
  } else if (strcmp(cmd, CMD_OUTPUT_POWER_MAX) == 0) { // "ATS302=15": For RCZ1: Set output power to maximum power level.
    // Default to "OK".
  } else if (strcmp(cmd, CMD_GET_CHANNEL) == 0) { // "AT$GI?": For RCZ2, 4: Get current and next TX macro channel usage. Returns X,Y.
    response = "0,3";
  } else if (strcmp(cmd, CMD_RESET_CHANNEL) == 0) { // "AT$RC": For RCZ2, 4: Reset default channel. Send this command if CMD_GET_CHANNEL returns X=0 or Y<3.
    // Default to "OK".
  } else if (strcmp(cmd, CMD_GET_ID) == 0) { // "AT$I=10": Get Sigfox device ID.
    response = "002C2EA1"// Must be 6 chars.
  } else if (strcmp(cmd, CMD_GET_PAC) == 0) { // "AT$I=11": Get Sigfox device PAC, used for registering the device.
    response = "5BEB8CF64E869BD1"// Must be 16 chars.
  } else if (strcmp(cmd, CMD_EMULATOR_DISABLE) == 0) { // "ATS410=0": Device will only talk to Sigfox network.
    // Default to "OK".
  } else if (strcmp(cmd, CMD_EMULATOR_ENABLE) == 0) { // "ATS410=1": Device will only talk to SNEK emulator.
    debug_print("***** Error: Emulation mode not supported");
  } else if (strncmp(cmd, CMD_SEND_MESSAGE, strlen(CMD_SEND_MESSAGE)) == 0) {  
    // "AT$SF=": Prefix to send a message to Sigfox.
    // If it ends with ",1", request for downlink.
    int pos = strlen(cmd) - strlen(CMD_SEND_MESSAGE_RESPONSE);
    if (strcmp(cmd + pos, CMD_SEND_MESSAGE_RESPONSE) == 0) {
      // Downlink. Return response 1 min later.
      response = "OK\r\nRX=01 23 45 67 89 AB CD EF";
      dataTimestamp = millis() + DOWNLINK_TIMEOUT;
    } else {
      // No downlink. Default to "OK" after send delay.
      dataTimestamp = millis() + UPLINK_TIMEOUT;
    }
  }

https://github.com/lupyuen/send_altitude_cocoos/blob/master/stm32/uartint/uartint.cpp

We also simulated the Wisol WSSFM10R4AT Sigfox module because this module is not available as a breakout board in some parts of the world.

The UART Task creates an instance of our UARTInterface class to send and receive AT commands.

The UARTInterface class intercepts the AT commands and returns simulated responses. If Sigfox downlink is requested, the response is returned in 1 minute, just like a real Wisol module.

To disable the Wisol Sigfox simulator and connect to a real Wisol Sigfox module via the UART port, check the instructions for “Enabling UART Interface on STM32 Blue Pill”.

[ UPDATE: The complete UART code has been added to UART Interface ]


The End?

Many thanks to Peter Eckstrand for creating cocoOS, the amazing portable task scheduler that runs on Arduino, Blue Pill and many many more devices! I must also thank Peter for rescuing me from so many Blue Pill traps — hopefully we have eliminated the dangerous ones!

I have barely scratched the surface of Blue Pill programming. Warren Gay has written an excellent book on STM32 Blue Pill programming, the first such book I have seen! I highly recommend it…

Some parts of the send_altitude_cocoos code came from the source code provided with the book… (check out the other examples too)

If you’re looking for code to control I/O devices you may also refer to the sample code for libopencm3 here:

https://github.com/libopencm3/libopencm3-examples/tree/master/examples/stm32/f1/stm32-maple

https://github.com/libopencm3/libopencm3-examples/tree/master/examples/stm32/f1/other

And lastly my code is here…

Getting a non-trivial IoT program to run on both Arduino and Blue Pill has been a dream for me. I hope it helps my IoT students and all IoT learners worldwide! There’s still a lot more work to be done to deliver the IoT promise — Please let me know how I can help! :-)


Note: The content and the pictures in this article are contributed by the author. The opinions expressed by contributors are their own and not those of PCBWay. If there is any infringement of content or pictures, please contact our editor (zoey@pcbway.com) for deleting.


Written by

Join us
Wanna be a dedicated PCBWay writer? We definately look forward to having you with us.
  • Comments(0)
Upload photo
You can only upload 5 files in total. Each file cannot exceed 2MB. Supports JPG, JPEG, GIF, PNG, BMP
0 / 10000
    Back to top