8-bit PIC: 2 hex characters to one binary byte - Page 1
Forum section: electronics / general

author: MIS42N
author_rank: Frequent Contributor
timestamp: October 07, 2021, 08:10:56 am
content: I am working on a bootloader that reads, via the UART, the .hex files output by the compiler (using Tera Term as an Xmodem sender). The file consists of many lines of hex e.g.:020000020000FC:10200000BC03F40330047004B404FC0448059805D0:08201000EC054806A8060C07C8:100014009D91909389009D9190938800B2FDB1E0E9:100024008BB59AB59FBF18959FB79ABD8BBD86B502and the program has to collapse each hex pair of characters into binary bytes. It could be tackled by the mask, shift, add extra system I've seen elsewhere, but this is quicker and shorter. It only works if the alpha hex character (A to F) are upper case. Which they are in all the hex files I've looked at. In PIC assembler:Code:SWAPFCHAR1,WADDLW0x55BTFSSCHAR1,6 ; test if letterADDLW0x71; 2nd hex charADDWFCHAR2,WBTFSSCHAR2,6 ; test if letterADDLW0x07There is a SWAP for ATmega so it should be possible to do the same for it.It works as follows: consider the character as two nibbles (eg the letter F is two nibbles 4 and 6). The second nibble becomes a nibble in the final byte. For a letter, that nibble has to be increased by 9. Because the algorithm doesn't remove the first nibble of either character, it is included in additions. For a letter that value is 4. So to convert two letters to binary is as simple as:Swap the first character into a registeradd the second character ; the two significant nibbles each have 4 added to themadd 0x55 ; already added 4 to each nibble, need to add 9 to each, so add another 5The numeric characters are not quite as neat. This is solved by converting (by stages) a 0x3n into a 0x4n. This requires adding 16. If the numeric is the second character, its position already has 9 added so add another 7 (last line) causing a carry in to the most significant nibble increasing a 3 to a 4. If the numeric is the first character, 7 is added to the most significant nibble (for 16 added in total) but its overflow is lost. So the 3 in the least significant nibble has to explicitly have a 1 added.Someone probably did this already, I just haven't found it. Probably of little importance unless (as in a bootloader) is trying to reduce code to a minimum. I have the Xmodem transfer and the hex file decoder working, in about 260 instructions. I'm guessing about 400 after adding the code to read and write flash memory.

author: retiredfeline
author_rank: Frequent Contributor
timestamp: October 07, 2021, 08:28:57 am
content: Maybe it's thehttps://en.wikipedia.org/wiki/Double_dabble#Reverse_double_dabble?

author: jpanhalt
author_rank: Super Contributor
timestamp: October 07, 2021, 09:25:04 am
content: I believe the double dabble is for BCD.Try this (Source:http://www.piclist.com/techref/microchip/math/radix/a2b-2h8b-pf.htm):Code:ascii2byteswapf hichar,w;swap hi nibble into resultbtfsc hichar,6;check if in range 'A'-'F'addlw 0x8F;add correction swapf ('1'-'A'+1)addwf lochar,w;add lo nibblebtfsc lochar,6;check if in range 'A'-'F'addlw 0xF9;add correction '9'-'A'+1addlw 0xCD;adjust final result -0x33returnLots of good stuff on PicListIt's not identical per se, but same length and execution time. There are some other routines there too.

author: MIS42N
author_rank: Frequent Contributor
timestamp: October 07, 2021, 11:44:24 am
content: Quote from: jpanhalt on October 07, 2021, 09:25:04 amThat is quite fascinating. It is almost the inverse of what I did but probably more logical. I adjusted everything up by 9 by including the other nibble and adding another 5 then fixed up the number characters by adding 7. This one leaves the number characters alone and adjusts the letters (A-F) by converted from 4:X to 3:X+9 by adding 15 to the 4 which modulo 16 is -1. since there's a carry out the first add to the significant bits is 8+carry, not 9. Then at the end it removes the surplus 3s.I didn't think to use anything but addition. This routine does both add and effectively subtraction by using twos complement additions.I think I'll not change my program since there's no performance gain, 'if it ain't broke, don't fix it'. But if the problem arises again I'll keep PicList in mind. I thought it looked familiar, I made a contribution in 2009.

author: Peabody
author_rank: Super Contributor
timestamp: October 08, 2021, 03:00:54 pm
content: I don't know PIC assembler, and am having trouble matching your algorithm description to the code. Could you possibly explain what the seven intructions do, or if anyone has an AVR or MSP430 version, that would be helpful.As for the mask and add 9 if it's a letter method, in higher level languages where you can't swap nibbles, having calculated the value of the most significant character, instead of shifting it left four times, you can use that as the index into a 16-byte lookup table consisting of 0, 0x10, 0x20 ... 0xF0 which is just added, or ORed, to the result. That would take more code space, but might be faster than shifting four times.

author: mariush
author_rank: Super Contributor
timestamp: October 08, 2021, 04:40:45 pm
content: Look up an ascii table ...'0' is 0x30 or 0011 0000 , '9' is 0x39 or 0011 1001'A' .. 'F' is 0x41 or 0100 0001 'F' is 0x46 or 0100 0110So you could just split into two nibbles .. if the first nibble is 4 (0x100) then add 10 to the second nibble.or sayvalue = character - 0x30if value > 9 value = value - 7;ex A = 0x41 = 65value = 65 - 48 = 17value is bigger than 9, so value = value - 7 = 17-7 = 10make sure you ignore the : character and the CR LF characters (carriage return, line feed 0x0D 0x0A ... linux file may have only 0x0A, mac may have only 0x0D)

author: SiliconWizard
author_rank: Super Contributor
timestamp: October 08, 2021, 05:02:46 pm
content: A table-based approach may be good on some targets and less so on others. For the MCU the OP is using, I'm not sure a table-based approach would be more efficient.

author: jpanhalt
author_rank: Super Contributor
timestamp: October 08, 2021, 05:26:45 pm
content: Tables are super easy in Assembly, particularly in the enhanced mid-range and later chips, and I use them often, but for something like this, 7 instructions (plus a little house keeping) and 9 TCY, a table wastes program instruction space.As for figuring out what's happening, are the instructions unclear or the "bit banging?" If the latter, I suggest writing it out in binary and seeing. I started doing that, but stopped, as maybe it's just the instructions.

author: MIS42N
author_rank: Frequent Contributor
timestamp: October 08, 2021, 10:32:18 pm
content: People are losing sight of the purpose of this code. It is part of a bootloader. A criteria of a bootloader is to have minimum instructions, leaving plenty of program memory for the program being loaded. In a different contex, a table lookup would be preferable as it is easy to understand and maintain.@mariush mentioned ignoring other characters. Fortunately the data is highly structured, each record starting with a colon ( : ) and ending in a checksum. So having processed the checksum of one record the program ignores everything up to the next colon. If the data is corrupt the hope is it will be picked up either in the Xmodem checksum or the data checksum. The bootloader asks that the program to be loaded be sent twice, the first time to make sure it can be read without error, the second to actually write to memory.And I thought I had explained in the first post how the code worked. It may be easier to look at the actions and result as two separate nibbles (4 bit values) rather than one byte. The final value of a nibble is the original low nibble (the 0 to 9 of a numeric, or the 1 to 6 of a letter A thru F), plus 5 (explicitly added) plus 4 (either the high nibble of the other character if it was a letter, or the high nibble plus either a carry or explicit add if it was a numeric). If the original was a letter, the nibble has had 9 added so is correct. If it was a number, it is 9 too high so add 7 for a total of 16. Since a nibble is modulo 16 the nibble now has its correct original value. Still clear as mud.

author: westfw
author_rank: Super Contributor
timestamp: October 08, 2021, 11:10:32 pm
content: QuoteOh, that's cute!I never thought of checking bits before the subtract of '0', or "assuming" a letter vs number, (and of course it doesn't work after subtracting 0.)The PIC has skipping bit-tests of file registers ("memory"), but no compare instructions, so this is a big win. (But it's probably less useful on other CPUs.)QuoteCode:;; Enter with char1 = '7' (0x37), char2 = 'C' (0x43)SWAPF CHAR1,W ; swap nibbles of char1, result into W;;; W now has 0x73ADDLW 0x55 ; add literal;;; W now has 0xC8BTFSS CHAR1,6 ; bit test original char if letter, skipADDLW 0x71 ; if a number, add 0x71;;; W now has 0x39 - since char1 was a number; 2nd hex charADDWF CHAR2,W ;add in char2. Note this adds 3 or 4 to; the left nibble as well.;;; W now has 0x7CBTFSS CHAR2,6 ;test if letter. skip, because it isADDLW 0x07;;; result is 7C.

author: Peabody
author_rank: Super Contributor
timestamp: October 08, 2021, 11:16:33 pm
content: What I'm having trouble with is trying to match the code to the description. I see lots of "if"s in the description, but no conditions or branches in the code. I assume they are there, but as I said I don't know PIC assembler. Anyway, I guess it's something I can figure out if I ever need to do this and code space is limited.

author: Peabody
author_rank: Super Contributor
timestamp: October 08, 2021, 11:18:58 pm
content: Thanks westfw. And "skip" means what? The next instruction?

author: westfw
author_rank: Super Contributor
timestamp: October 08, 2021, 11:32:36 pm
content: Quote"BTFSS" is "Bit Test Filereg and Skip if Set" - it skips the next instruction if the given bit is set in the file register.This is similar to the seldom-used (?) "SBRS" AVR instruction, except that SBRS only works on one of the 32 CPU registers, while BTFSS works on one of the 32 (low-end) to 256 (PIC18) "file registers" in the current bank, which are what passes for RAM on PIC.(The PIC has 1 real "register" that can do math, and a bank of file registers that permit "some" operations. The AVR has 16 to 32 "general purpose" registers (depending on how you count "general purpose" - only 16 support immediate operations), 32 IO registers with "some" test/bitio capabilities, and a couple K of RAM that can only do load/store.)Edit: oops - I left out a SWAP instruction!Code:;;; AVR version. tmp, char1, and char2 are all registers. tmp must be a "high" register.mov tmp, char1 ;char1 must be register.swap tmpsubi tmp, -0x55 ; (no add immediate on AVR)sbrs char1, 6 ;check for lettersubi tmp, -0x71add tmp, char2sbrs char2, 6subi tmp, -7

author: Peabody
author_rank: Super Contributor
timestamp: October 08, 2021, 11:41:36 pm
content: Ok, thanks very much.

author: MIS42N
author_rank: Frequent Contributor
timestamp: October 09, 2021, 04:08:16 am
content: The 8-bit PIC processors are a bit weird. I think because Microchip have retained a lot of backward compatibility with the original processors. These had small programmable memories and even smaller RAM so a 7-bit RAM address was enough and an 11 bit program counter was overkill. Each instruction takes only 14 bits. So quite efficient use of memory, which originally was a scarce resource.There is really only one working register which in assembler instructions is called (appropriately) W. Everything else is addressed as being in a data memory location. This includes special registers for interrupt flags, status registers, and register(s) for indirect memory access. Then there are peripheral control registers for the timers, I/O ports etc. So it is tempting to call the actual RAM 'general purpose registers' but in reality it is just RAM.The skip instruction really is a skip - skip the next instruction. There are a number of them the most common being test a bit and skip if it is (set/clear) so BTFSS is Bit Test F(I call it flag but call it what you like) Skip if Set. SC is Skip if Clear. The bits of any location can be addressed as 0 through 7. The assembler has include files to give mnemonics so it is more meaningful to say BTFSS STATUS,Z (skip if result of previous operation was zero) rather than BTFSS 3,2 although they generate the same instruction. Other useful ones are decrement (or increment) the value in a location and skip if it becomes zero (DECFSZ INCFSZ).The real pain with the low end PIC processors is the instructions retain the 7-bit RAM address and 11-bit program addresses. To access more RAM there's a RAM bank register (or status bits) that determine which bank of RAM is addressed. There's also a program memory bank register to determine which 2K word bank of instructions are addressed by GOTO or CALL. There's a lot of bank switching required, and losing track of which bank is current usually results in a disaster.Even so, there's a lot of peripheral goodness in most PIC processors and they don't draw much current. I'm working with a PIC16F1455 and it draws about 5mA with a 10MHz instruction rate. There's LF models that are more frugal.

author: Dabbot
author_rank: Regular Contributor
timestamp: October 09, 2021, 06:17:18 am
content: Quote from: MIS42N on October 09, 2021, 04:08:16 amNot quite. The next instruction isn't actually skipped. Instead, it's executed as a NOP.This is an important distinction, especially if you're interested in execution time.Quote from: MIS42N on October 09, 2021, 04:08:16 amThis could be said for anything else you're keeping track of in your code. It's actually not that bad.An approach that works for me is to stay in BANK 0 unless I need to be in a different bank. And if I do require a different bank, switch to it, do the deed, and switch back to BANK 0.

author: westfw
author_rank: Super Contributor
timestamp: October 09, 2021, 06:53:05 am
content: QuoteThose are the BIG 8-bit PICs. Digikey still lists 6+ pages of PIC16F chips with less than 128bytes of RAM, many of which have 12bit instruction words. It's the "mid-range" and "enhanced mid-range" chips with 14bit instructions, and the PIC18s go to a 16bit instruction word while retaining a very similar architecture. (that's the "nice" thing about true Harvard-Architecture CPUs. If you want to address more code or data space, you can just add some bits to the instruction word fields where data/program memory addresses normally live.(IIRC, the famous Padauk microcontrollers essentially copy the low-end PIC architecture, but go to a 13bit instruction word to fix some of the more annoying issues with the elder PICs.)QuoteYou lose the pre-fetched next instruction, so it takes an extra cycle. Whether it's "executed as a NOP" or "discarded" is a fine distinction. (Might come into play on PIC18, which has two-word GOTO and CALL instructions.)I grew up on a mainframe that used skip instructions for most conditional operations. (and boy, did it have a lot of them. Test Left halfward with immediate value, Complement the bits, and skip if they were Zero (TLCE). Skip if memory location is zero. Skip always. Skip never. Skip on successful return from subroutine. It all worked fine until everyone started building machines with variable length instructions.

author: MIS42N
author_rank: Frequent Contributor
timestamp: October 09, 2021, 09:29:46 am
content: Quote from: Dabbot on October 09, 2021, 06:17:18 amThe PIC16F1455 has 13 banks mostly of 80 bytes, the last is 48. The peripherals are liberally sprinkled through the banks - asynchronous serial I/O is in bank 3, synchronous serial in bank 4, interrupt on change control in bank 5, PWM control in bank 12, etc.I ended up allocating banks to specific tasks - 3 for controlling serial I/O with a send buffer in 4, receive buffers in 10 and 11, arithmetic in bank 9, etc.The program memory was a little messy. I ended up allocating all start up, shut down and interrupt code to bank 0x0000 along with fixed data, the mainline in bank 0x0800, subroutines in bank 0x1000, specials in bank 0x1800. Fortunately the PIC16F1455 has a BRA instruction, branch relative to the current location, that doesn't involve the memory bank register (PCLATH) so I've set PCLATH to the subroutine bank. The mainline as far as possible branches using BRA and I have two macros, GoMain (go to a location in the mainline) and GoCallSub (set the program bank back to subroutines) to cater for the times when the branch is out of the scope of the BRA instruction, which fortunately is not often. Sometimes I am amazed it all works so smoothly.

author: jpanhalt
author_rank: Super Contributor
timestamp: October 09, 2021, 10:06:02 am
content: The 16F1455 also has 1024 bytes of "linear RAM" that is accessed using the FSR registers. That is quite useful when you need more than 80 bytes of contiguous memory.I usually set the beginning of my linear memory after those banks I want to address in the usual way, such as Bank 4 for SPI, as I also make liberal use of RAM that is not in Bank 0. Some people prefer to use Banksel liberally. I simply keep track of what bank is being used with a column on the far right side, e.g., label instruction ; <comment> |B3

author: Benta
author_rank: Super Contributor
timestamp: October 10, 2021, 08:54:25 pm
content: The algorithm ain't that hard. I know nothing about PIC assembly code, so I'll try it as text:0...9 are 0x30...0x39.A...F are 0x41...0x46.1: test a pair of incoming characters if A...F by checkingbit 7. If it's 1, add 9 (0b00001001) to the character byte. CORRECTION: bit 6.2: store the two consequetive characters as bytes somewhere.3: shift/rotate the first byte left four places.4: logical AND the first byte with 0b111100005: logical AND the second byte with 0b000011116: logical OR the two bytes.Done, store the result as the binary byte.NOTE: this only works, if your input is really only 00...FF in ASCII format.A more robust implementation would be enhancing point one to do real checking if the characters are 0x30...0x39 and 0x41...0x46 and add additional exception handling.

author: westfw
author_rank: Super Contributor
timestamp: October 11, 2021, 02:04:52 am
content: QuoteUmm. No ASCII characters have bit 7 set. Check bit 6, subtract 7? (7 the integer, not 7 the character. (or ('A'-10))But the point here is that the PIC code presented uses far fewer instructions than a typical implementation (or compilation) of the "obvious" algorithm.(maybe interesting: on an AVR, the compiler on the obvious algorithm produces an 8-instruction sequence, the same as the implementation of the "strange" algorithm. I don't think you'd be so lucky with a PIC.)Code:uint8_t asc2bin(uint8_t char1, uint8_t char2) {uint8_t output;if (char1 & 0x40)80: 86 fd sbrc r24, 6char1 -= 7;82: 87 50 subi r24, 0x07 ; 7if (char2 & 0x40)84: 66 fd sbrc r22, 6char2 -= 7;86: 67 50 subi r22, 0x07 ; 7output = char1<<4;output |= char2 & 0xF;88: 6f 70 andi r22, 0x0F ; 158a: 82 95 swap r248c: 80 7f andi r24, 0xF0 ; 240return output;}8e: 86 2b or r24, r2290: 08 95 retHmm. PIC: 26 instructions instead of 7. Although, this is using the "free" version of the compiler, which lacks some optimizations.Code:!uint8_t asc2bin(uint8_t char1, uint8_t char2) {0x7C0: MOVWF char1! uint8_t output;! if (char1 & 0x40)0x7C1: BTFSS char1, 0x60x7C2: GOTO 0x7C5! char1 -= 7;0x7C3: MOVLW 0x70x7C4: SUBWF char1, F! if (char2 & 0x40)0x7C5: BTFSS __pcstackCOMMON, 0x60x7C6: GOTO 0x7C9! char2 -= 7;0x7C7: MOVLW 0x70x7C8: SUBWF __pcstackCOMMON, F! output = char1<<4;0x7C9: MOVF char1, W0x7CA: MOVWF 0x710x7CB: MOVLW 0x30x7CC: LSLF 0x71, F0x7CD: ADDLW 0xFF0x7CE: BTFSS STATUS, 0x20x7CF: GOTO 0x7CC0x7D0: LSLF 0x71, W0x7D1: MOVWF 0x720x7D2: MOVF 0x72, W0x7D3: MOVWF output! output |= char2 & 0xF;0x7D4: MOVF __pcstackCOMMON, W0x7D5: ANDLW 0xF0x7D6: MOVWF 0x710x7D7: MOVF 0x71, W0x7D8: IORWF output, F! return output;0x7D9: MOVF output, W

author: MIS42N
author_rank: Frequent Contributor
timestamp: October 11, 2021, 04:29:32 am
content: Quote from: westfw on October 11, 2021, 02:04:52 am"lacks some optimizations" - That's a candidate for understatement of the year. It's awful.Interesting that the AVR C compiler produces quite good code. I was going to point out that it assumes the characters to be processed were already in registers, whereas the PIC code retrieves them from RAM. But in practice the usage is retrieving from a string, which AVR can do with pre/post indexing of the indirect addressing register, but PIC is unable to do that. The PIC RAM to register instructions can use indirect addressing, but only Load and Store (to use the AVR terms) have pre/post indexing. The two instructions used (SWAP, ADDWF) for RAM access don't have the ability.Maybe I should come to grips with Microprocessor C. So far I've stuck with Assembler, mainly because I am suspicious of included black boxes. I also like pushing the limit of what 8-bits can do. No personal challenge in knitting together a load of pre written modules for a 32 bit ARM processor, even if it is a whole lot more functional.