Hello, dear members of Habrahabr.
Given that Habr is a portal focused on programmers, I noticed that recently there are many articles about programming microcontrollers and creating devices based on them. I decided to share one of his development. In the past, I wrote a lot for MK, I even worked as a software and circuit design developer at one of the firms, and before that I had a programmer on AFM under Z80 and i8080. Now, in adulthood, I mostly write in PHP / MySQL for my own Internet projects and programming MK has not returned for a long time. I cannot call myself a full-fledged programmer, because for example, I could not master OOP, but I write some C as needed.
Some time ago I had a task to create a USB keyboard emulator for the CarPC project. It had to be used in the Becker BE2580 radio tape recorder installed on German-made cars of the 2000s. The emulator had to poll regular radio buttons and generate clicks on a virtual USB keyboard connected to an Android-based CarPC motherboard. What came out of it, under the cut.
Briefly about CarPC itself: initially, I had the seditious idea to plug a new-fashioned Android-based recorder into the car with a removable front-panel tablet, which recently began to be produced by Chinese manufacturers. However, the comrades in the auto-community dissuaded me from this idea, urging me not to disturb the classic look of the car’s interior and the design conceived by the manufacturer. As a result, the motherboard from the Iconbit Toucan Nano media player was built into the radio, I installed it in place of the cassette tape player. At the same time, he noted how progress is progressing: I decided to check the cassette for the sake of interest, but at home there was not a single cassette.
')
Toucan Nano is an ideal candidate for use in CarPC, at a price of about $ 100 (in the Russian Federation) it has a TV output (composite), a component (subtractive with a dedicated clock signal), though, unfortunately, there is no RGB needed for output images on the display in the radio itself. There is HDMI, USB host (2 ports). The PL2303 chip support (USB2COM converter, ie RS232) was compiled back into the core - which allowed connecting an external GPS receiver, as well as supporting WiFi whistles on the Ralink chip (I used Dlink DWA-140). The board was installed in the radio, the signal from the TV output was fed to the RGB inputs of the standard monitor of the radio using the self-assembled TV -> RGB converter based on the TDA8362. In the radio, a video signal switch is provided, so the entire native functionality of the radio tape recorder has been saved. It looks like this:

The red color shows what is placed on the second floor (under the board). With the Toucan Nano in the kit comes a remote, paired with a radio whistle. The console features an interesting Fly Mouse technology: the mouse reacts to an accelerometer built into the console, thus simply tilting the console, moving the mouse around the screen of the device, it turns out something like a pointer. However, it seemed uncomfortable and stupid to use it in the car: it’s still a radio tape recorder, not a computer! Therefore, it was decided to assemble a radio keyboard keyboard emulator that would connect to the USB port and behave like a standard PC keyboard, while polling the radio button. In fact, this solution is universal and can be used with any OS: it’s no secret that many CarPC systems are built on Intel platforms.
It so happened that at that time I had no experience with PIC microcontrollers, in my youth I was an Atmel programmer, but this experience was no longer relevant today, since 10 years have passed and now, probably, nobody remembers my favorite AT89c2051 now. The programs were then written strictly on ASMe, since On C, it would just be harder, for 2051 there are only 2K ROMs. In those years, the PIC also existed, but we refused to use it, because the development tools were too expensive, and for 2051 I then developed a ROM emulator on my own based on the i51 with an external ROM that had the same instruction set as the Atmel 89c2051. Now, having understood that I will have to go through this path practically anew, I gathered my spirit and sat down for literature. I think that the experience will be useful to the readers because I am painting everything in steps, in development.
At first I began to study the Internet on the topic of microcontrollers with USB support and immediately came across a device from the glorious company PIC, it was 18F2550. Its declared functions turned out to be very good: USB 2.0 support, 1K RAM, a fully configurable USB port, in fact, the 2550 can become
any USB device. Plus all the standard PIC features. This chip can be purchased in a DIP28 package, which is convenient for mounting and desoldering, and the price is quite low. In ancient times, microcontrollers were sewed by the programmer, but now bootloaders have become popular. The meaning of the bootloader is that it is enough to write it into the microcontroller only once, after which you can access it by any other means (in our case via the USB port) and update the firmware of the device without removing it from the circuit. Ready-made whales with pre-flashed microcontrollers and ready-made software for uploading firmware through a bootloader are sold, but I didn’t want to look for such a whale (or rather, wait for it to be sent). So I just went to the nearest radio store and bought a clean PIC18F2550.
Next, it was necessary to assemble a programmer to fill the bootloader. Poshariv on an Internet, found this:
products.foxdelta.com/progparallel.htmThe scheme is attached there. Simplified it, of course, removing unnecessary details, because soldering was too lazy. In fact, the pins of the microcontroller connected directly to the LPT. Software used WinPic800. About the bootloader itself is a separate story. It is loaded from the address 0x000, respectively, the program itself must begin with the address 0x800, which is indicated in the compiler settings. I found several different bootloaders, they pour the firmware in different modes (for example, HID), respectively, designed for different IDEs. USB PIC Driver for the stitching computer can also be used differently, because PIC "turns" into different USB devices. I constantly came across bootloaders for 18F4550, and its config is different and the bootloader does not work on it from my MK. As a result, I stopped at the bootloader and the firmware loader based on the MCHPUSB PIC-based demo board. The bootloader itself had to be slightly modified under my circuitry (since I placed the bootloader button on another pin of the microcontroller). In general, there turned out to be many different parameters (for example, the type of a quartz resonator, a frequency multiplier, and a lot of similar nonsense). If these parameters are incorrectly specified, the device is sewn and even the LED flashes according to the program, but then refuses to work as a USB device, producing my “favorite” error “USB device not recognized”, since frequencies are wrong. With this problem, I spent a couple of days, until I finally found a workable configuration. As a result, my bootloader is designed for quartz 4Mhz. The LED is soldered on the RA5, the button for activating the firmware mode for the bootloader is on the RA3 (and 1kOhm plus), 26 feet to the ground (for operation in LVP mode). In order for the MC to enter the bootloader mode, hold down the reset, press the specified button on the RA3. The LVP bit (turning off the low-voltage firmware mode) for some reason failed to reflash me, well, okay. In theory, it is needed to protect the bootloader from overwriting. The multiplier is tuned to a maximum frequency of 48 Mhz. Unfortunately, if the microcontroller is used in USB mode, the frequency cannot be raised, because USB frequencies are fixed, and the multiplier has multiplicity, but it turned out and is not necessary.
As a result, the bootloader corrected, the driver for the computer from Microchip installed. As a basis for the program, I immediately took a free sample from Bradley A. Minch, which sequentially issues the symbols 'f', 'o', 'o', 'b', 'a', 'r' to the virtual keyboard. The source code of this program can be found here:
code.google.com/p/picusb/source/browse/lab2_pickit2/lab2.asm?spec=svn69&r=69Having compiled the program, having previously configured a bunch of PIC configuration registers, I achieved that it started typing
foobar on my computer in a loop! Hooray! In reality, all this did not happen in one day either, but I omit the details. And here I was apprehended for a second by the fear that all this miracle might not work on Iconbit, which can, without explanation, just ignore almost a week of my work - and I will never know why. But, having connected the device to the Android motherboard, I saw that everything worked: a search window opened right away in which the familiar letters of foobar ran. Android on pressing any keys responds by opening the search window.
Then it was necessary to modify the program to my needs, in particular, to write code that will read data from the radio itself. How it will work, at that time I had no idea. I thought I would have to connect to the matrix of the radio and read from it. Typically, such devices use a dynamic scan of the matrix, when along the X axis "runs" one, and on the Y axis information is read. Ie, say, 16 keys can be processed with 8 pins (4 + 4). To simultaneously read such a matrix, you also need 8 pins, but tuned to the input. However, having rummaged on the board of the radio tape recorder, I found that the caring Germans had isolated a separate processor for processing the entire keyboard, the only thing is that the encoder handles went directly to the central percent. As a result, at the keyboard of the radio, I found an exit on which impulses appear when I press any keys of the radio. The pulse repetition rate turned out to be quite high and, to begin with, I just stupidly connected them to the converter based on PL2303, i.e. just served on the RS-232 port and started experimenting with the port settings. As a result, it turned out that meaningful bytes can be seen by setting the port to 480 kbps. For me, still a mystery why the Germans needed to transfer data from the keyboard at such a high speed.
In order to write a program for the PIC, we had to analyze in detail the nature of the pulses of the keyboard processor of the radio. For this, I bought an oscilloscope. In my youth, of course, I had all these devices, but all this remained with my parents, and I already moved 5 years ago to Moscow. Therefore, I decided to slowly collect an instrument park: I return to radio amateur from time to time.
As a result, it turned out that the processor in front of each packet of information (which consists of 2 bytes), gives a long pulse, which serves as a “pilot” warning about the beginning of the transfer of a block of information. It was surprising that the length of this block (as well as the intervals between bytes) change in an arbitrary manner, even when you press the same button. Another problem was that the PIC could not programmatically reliably read data at that speed. After all, he must also manage to maintain the USB bus! If a key is pressed at the wrong moment, it simply skips it. As I wrote above, I raised the core frequency to the maximum with the help of multipliers, but even with this setting, the processor worked at the limit and sometimes skipped bits, as a result, the probability of correctly defining the buttons was about 70%, which is unacceptable in a car. Here I have already collected my mind and thought that, probably, there is a second signal (strobe). So it turned out. But initially I did not find him because for some reason the impulses are constantly present on it, but it would be logical if they would appear when the buttons are pressed. Apparently, the radio processor polls the keyboard in a continuous cycle and generates a pulse when any bit is transmitted, even if the state of the keys does not change. Ie, just sending zeros if there is no activity. In general, the processor gates every transmitted bit, as a result they can be read absolutely clearly. I rewrote the PIC program, after which everything began to work stably and beautifully. When a button is pressed, one code is formed, when released, another. Thus, it was possible to still implement the button holding mode, but I did not do this, because buttons and so it turned out to be abundant.
Here I will give excerpts from my program, the parts that I wrote. You can see the original of the program at the link above. In the program, I have, of course, a table that compares the read key codes and USB codes that need to be transferred to the head device. Immediately, I note that I usually write comments in English (in order not to lose time on switching the register). If someone is interested in the full version of the program, I can send it without any problems. The full text is not given here for reasons of copyright compliance. I can also send a bootloader and a program for its firmware, a program for uploading firmware.
Recoding buttons.
unsigned char recode(long n) { if (n==0x3550) return 'Q'; // <> REVERSE if (n==0x1580) return 'U'; // up - 1 if (n==0x6aa0) return 'X'; // next - 2 if (n==0x2540) return 'D'; // down - 3 if (n==0x4a80) return 'Z'; // prev - 4 if (n==0x1570) return 'L'; // left - 5 if (n==0x0560) return 'R'; // right - 6 if (n==0x4520) return 'H'; // home if (n==0x0ac0) return 'A'; // player if (n==0x6500) return 'T'; // TEL ?? if (n==0x5530) return 'N'; // navi if (n==0x7510) return 'S'; // settings if (n==0x1b28) return 'B'; // back if (n==0x4500) return 'E'; // enter if (n==0x28e0) return 'M'; // (MAP) ?? if (n==0x1320) return 'O'; // notification area - REPEAT if (n==0x0330) return 'F'; // search/find - CALL return 0; } int remap(int n) { // launcher if (n=='S') return 0x3a; // F1 settings/menu if (n=='F') return 0x3b; // F2 if (n=='O') return 0x3c; // F3 notification area // programs if (n=='H') return 0x3D; // F4 home if (n=='A') return 0x3f; // F6 audio/player if (n=='N') return 0x40; // F7 navi if (n=='M') return 0x44; // F11 map if (n=='T') return 0x45; // F12 tel // player controls if (n=='Z') return 0x3e; // F5 prev song if (n=='X') return 0x43; // F10 next song //cursor if (n=='L') return 0x50; // left if (n=='R') return 0x4f; // right if (n=='U') return 0x52; // up if (n=='D') return 0x51; // down if (n=='E') return 0x28; // enter if (n=='B') return 0x29; // ESC back return 0x0; }
Configs:
#pragma config DEBUG = OFF // 1L 30000 #pragma config PLLDIV = 1 // PLL prescaler //#pragma config CPUDIV = OSC3_PLL4 // sysclock postscaler - working #pragma config CPUDIV = OSC1_PLL2 // sysclock postscaler #pragma config USBDIV = 2 // usb clock comes from PLL div 2 // 1H 30001 // #pragma config FOSC = XTPLL_XT // low speed mode #pragma config FOSC = HSPLL_HS // highspeed mode #pragma config FCMEN = OFF // fail safe clock #pragma config IESO = OFF // int/ext oscillator // 2L 30002 #pragma config PWRT = OFF // power up timer #pragma config BOR = OFF // brown out #pragma config BORV = 0 #pragma config VREGEN = ON // usb voltage regulator // 2H 30003 #pragma config WDT = OFF // watchdog #pragma config WDTPS = 32768 // 3H 30005 #pragma config CCP2MX = ON // ccp2 mux #pragma config PBADEN = OFF // portb a/d enable #pragma config LPT1OSC = ON // low power timer 1 oscillator #pragma config MCLRE = ON // MCLR pin enable // 4L 30006 #pragma config STVREN = ON // stack full #pragma config LVP = ON #pragma config XINST = OFF // extended instructions // 30008 //#pragma config ICPRT = OFF #pragma config CP0 = OFF #pragma config CP1 = OFF #pragma config CP2 = OFF #pragma config CP3 = OFF // 30009 #pragma config CPB = OFF #pragma config CPD = OFF // 3000a #pragma config WRT0 = OFF #pragma config WRT1 = OFF #pragma config WRT2 = OFF #pragma config WRT3 = OFF // 3000b #pragma config WRTB = OFF #pragma config WRTC = OFF #pragma config WRTD = OFF // 3000c #pragma config EBTR0 = OFF #pragma config EBTR1 = OFF #pragma config EBTR2 = OFF #pragma config EBTR3 = OFF // 3000d #pragma config EBTRB = OFF //#define SHOW_ENUM_STATUS #define LED PORTAbits.RA5 #define CAR PORTAbits.RA0 // keypad data line #define EN1 PORTAbits.RA1 #define EN2 PORTAbits.RA2 #define SYN PORTAbits.RA3 // keypad strobe line
Part of the configs is redefined by the programmer when uploading the bootloader.
Transfer key code to USB interface:
void SendKeyBuffer(void) { unsigned char n; for (n = 0; n<8; n++) BD1I.address[n] = Key_buffer[n]; // copy Key_buffer to EP1 IN buffer BD1I.bytecount = 0x08; // set the EP1 IN byte count to 8 BD1I.status = ((BD1I.status&0x40)^0x40)|0x88; // toggle the DATA01 bit of the EP1 IN status register and set the UOWN and DTS bits } int sendKey1(unsigned char c) { int n; long timeout=0; for (n = 0; n<8; n++) Key_buffer[n] = 0x00; // clear key buffer // ?? hang place 2 while ((BD1I.status&0x80)) { // wait to see if UOWN bit of EP1 IN status register is clear, (ie, PIC owns EP1 IN buffer) timeout++; if (timeout>250000) { return 0; } ServiceUSB(); } *(Key_buffer+2)=c; SendKeyBuffer(); ServiceUSB(); return 1; } int sendKey(unsigned char c) { if (sendKey1(c)) return (sendKey1(0x00)); return 0; }
Subroutines read radio buttons. To maximize speed
the loop is not used. Every beat is important.
char readByte() { unsigned char b = 0; // unsigned char i = 0; while (SYN==0); b = (b << 1) | CAR; while (SYN==1); while (SYN==0); b = (b << 1) | CAR; while (SYN==1); while (SYN==0); b = (b << 1) | CAR; while (SYN==1); while (SYN==0); b = (b << 1) | CAR; while (SYN==1); while (SYN==0); b = (b << 1) | CAR; while (SYN==1); while (SYN==0); b = (b << 1) | CAR; while (SYN==1); while (SYN==0); b = (b << 1) | CAR; while (SYN==1); while (SYN==0); b = (b << 1) | CAR; while (SYN==1); return b; } int countZero() { unsigned short i=1; while (CAR==0 && i++); return i/4; } int readCar() { int i; int d = 0; unsigned char k; unsigned char r; unsigned long vl; //#define analyzer //#define dbg // debug mode if (CAR==0) return 1; #ifdef analyzer for (i=0;i<110;i++) { carbuff[i]=readByte(); carbuff[i+110]=ssd; // strobe data } for (i=0;i<221;i++) { printHex(carbuff[i]); } crlf(); crlf(); return 1; #endif // end analyzer while (CAR==1); // short intro 1 while (CAR==0); // short intro 0 while (CAR==1); // long intro 1 vl = readByte() * 0x100 + readByte(); k = recode(vl); #ifdef dbg printDword(vl); // dbg crlf(); // dbg #endif if (k!=0) { #ifdef dbg printChar(k); // dbg crlf(); // dbg #endif #ifndef dbg r=remap(k); // send actual keys we need return sendKey(r); #endif } }
CAR - pin to which the signal from the keyboard of the radio tape recorder is connected. SYN - strobe signal from the keyboard processor radio.
In the process of debugging, I ran into a number of problems. In some cases, the microcontroller hung and the USB device simply disappeared. The USB exchange protocol is very complicated and it was not easy to delve into all its nuances. Through trial and error, I found problem areas and added a software watchdog there, which simply restarted the processor in case of a hang in these places. As a result, I have achieved a stable trouble-free operation of the program without lags with the accuracy of determining keystrokes 99%. The USB port in the Toucan itself does not work very well, especially since I used a USB splitter. Sometimes even “serious” devices like USB mouse whistle disappear until the next reboot. What to say about my homemade. However, I managed to achieve stability at the level of factory USB devices.
Next, it was necessary to connect the encoder. I decided to send the data from the encoder to emulate the “up” and “down” buttons, and press the encoder knob to the Enter key. Well, pressing processes the processor, i.e. it is already working. As for the encoder, it is easy to implement its reading. If its pins are connected to two bits, it outputs the sequence 0, 1, 3, 2 (in binary code 00 01 11 10). The two with the three is reversed, it turns out 0 1 2 3. Next, we subtract the current result from the previous one, if the difference is 1 or -3, then the rotation in one direction, if -1 or 3, is in the other. I saw that reading the encoder is implemented by some kind of table, in general, nothing needs to be done. Here is the procedure:
int readEncoder() { int k; int enc; int subb; int res = 1; static int enc1 = 0; static int init = 0; if (!init) { // prevent encoder key command sent on startup init = 1; enc1 = enc = EN1 + EN2 * 2; if (enc==3 || enc==2) enc=enc ^ 1; } enc = EN1 + EN2 * 2; if (enc==3 || enc==2) enc=enc ^ 1; if (enc != enc1) { subb = enc - enc1; if (subb==1 || subb==-3) k='U'; if (subb==-1 || subb==3) k='D'; res = sendKey(remap(k)); // send up or down key command } enc1 = enc; return res; }
Now PowerAMP is controlled clearly: a pen-encoder and it is enter! Use the pen to go through the folders with the music, with the button we enter them and launch the songs. Dream!
I will add that the codes generated by the virtual keyboard are not ASCII codes or even Android key codes (which also differ from ASCII codes). These are separate USB keyboard codes. You can find them in this table:
www.win.tue.nl/~aeb/linux/kbd/scancodes-14.htmlAccordingly, in Android there is a special config that is responsible for handling specials. keys. Of course, I had to launch both navigation and use the Home button and launch the player. For this purpose, you need to edit the keylayout file, a description of these files is here:
source.android.com/tech/input/key-layout-files.htmlHere is my config:
key 1 BACK # ESC ("AC" button)
key 59 MENU # F1 ("BC" button)
#key 60 CAMERA # F2 ("CALL" button)
key 61 NOTIFICATION # F3 ("repeat" key on deck)
key 62 HOME # F4 ("main" button)
key 63 MEDIA_PREVIOUS # F5 ("4" button)
key 64 HEADSETHOOK # PLAY / PAUSE F6 ("audio" button)
#key 65 INFO # F7 ("navi" button)
key 66 VOLUME_UP # F8 ??
key 67 VOLUME_DOWN # F9 ??
key 200 HEADSETHOOK # hardware key # MEDIA_STOP not working
key 68 MEDIA_NEXT # F10 ("6" button)
key 87 POWER # F11 ('map' key on deck)
#key 88 EMAIL # F12 ("tel" button)Of course, you can edit this file only on rooted devices.
It so happened that some keys do not work after installing the latest update on Toucan Nano. Why - I never managed to find out. For example, the most important HOME button has stopped working. Therefore, I wrote a simple command file that starts with me when I start the system. Here he is:
#! / system / bin / sh
stty -F / dev / ttyUSB0 ispeed 4800
chmod 0777 / dev / ttyUSB0
sleep 20
am broadcast -a info.mapcam.droid.SERVICE_START # mapcam.info
while true
do
s = $ (getevent -v0 -c1) # read one event from all input devices
# -v0 so that it does not sprinkle a bunch of unnecessary garbage
s = $ (echo $ s | awk '{print $ 4}') # select the keycode
case $ s in # execute the necessary command
0007003f) am start -a android.intent.action.MAIN -c android.intent.category.HOME -n com.maxmpz.audioplayer / .StartupActivity # AUDIO
# am start -a android.intent.action.MAIN -c android.intent.category.HOME -n com.maxmpz.audioplayer / .PlayListActivity # AUDIO
sleep 1
;;
00070040) am start -n ru.yandex.yandexmaps / .MapActivity # NAVI
sleep 1
;;
0007003d) am start -a android.intent.action.MAIN -c android.intent.category.HOME
sleep 1
;;
00070045) am start -a android.intent.action.MAIN -n com.speedsoftware.rootexplorer / .RootExplorer # TEL
sleep 1
;;
0007003b) am startservice -a "org.broeuschmeul.android.gps.usb.provider.nmea.intent.action.START_GPS_PROVIDER"
sleep 5
am broadcast -a info.mapcam.droid.SERVICE_START
# am start -n info.mapcam.droid / .SpeedometrActivity # CALL
sleep 1
;;
esac
doneThis emulator has been successfully used on the machine for almost a year. Plans to emulate a mouse so that it can be moved around the screen with buttons. Integrating the touchscreen into the system, I, unfortunately, have not mastered it yet. Unfortunately, a number of programs (for example, Rambler Maps) are not controlled by buttons and require a touchscreen. Therefore, all the same for such cases in the car have to carry a USB mouse.
About the Android project, I can somehow tell you separately if it will be interesting.
