====== PIC18: 7-segment displays ====== {{:pic18_7_segments:7-seg-3d.jpg?200 |}} 7-segment displays are an effective and still widely used way of displaying alphanumeric data. In fact they are quite simple, because they consist of 8 leds (decimal point included) connected in parallel and with the cathode (or anode) in common; by turning on specific leds (//'segments'//) a number or letter is displayed. Here we see the two types of displays: {{:pic18_7_segments:7segledlayout.gif|}} When used with PICs the different anodes of a single 7-segment display are connected to different PORTx pins and the common cathodes (from now on we will talk about common cathode displays only) are grounded: {{:pic18_7_segments:7seg_pic_0.png|}} By making certain PORTD pins high the corrispondent display segments are lit and numbers or letters appear; for instance to display **//'0'//** the correct sequence is //'abcdef'//, and the value **//b'00111111'//** must be sent to PORTD. Here is the complete table for all values from //0// to //9//: {{:pic18_7_segments:display-sequences_0.png|}} ===== Multiplexing ===== In case we have multiple displays PORT lines should be doubled or multiplied; even if a single PIC could have that many pins a better approach is to use a trick called **//strobing//** or **//multiplexing//**, by which **the same PIC pins are connected to the same segment on all digits** and **other PIC pins, which drive the base of two or more transistors, turn on each digit just for a fraction of time**, by sinking current. {{:pic18_7_segments:3-7seg-pic.png|}} The common cathodes cannot be directly connected to RE0:RE2 to sink current, because a single PIC port can only sink a maximum of 25mA; with 330ohm resistors and considering the voltage drop of a LED, the current that passes through a single segment is 10mA, so even with only 3 segments lit (for instance to display the digit //'7'//) the risk of damaging ports and the PIC itself is high. This is the reason why transistors have been used in this case: a small base current controls the larger collector current. Let's say we want to display the number //248//. We want to display the //units// and so we copy the binary value //'01111111'// to PORTD; then we set PORTE,0 high to display the sequence for //8// on the rightmost digit for a bunch of microseconds; the other displays stay off, because there is no base current on the other transistors. Then, to display the //tenths//, after a few microseconds we: * clear PORTE,0 and PORTE,2 * output //'01100110'// (binary value to display //4//) to PORTD * set PORTE,1 The same process will be followed for the //hundreds// digit. Just to give an idea, if we keep the delay between a digit and another artificially high, this is what we get: {{:pic18_7_segments:7-seg-248-animation_slow.gif|}} The smaller we keep the delay, the faster the switch between the digits will be: {{:pic18_7_segments:7-seg-248-animation_fast.gif|}} If the **delay is small enough**, the human eye won't notice the switch and **all digits will appear on at the same time**: {{:pic18_7_segments:two-four-eight-7-segments.png|}} Enough with the theory for now: let's display some real values on the digits! ===== Assembly code Main routine ===== For this example we are going to use **three digits** as the image below, **which will display the value of the A/D conversion from the analog trimmer** of Freedom II. The **main routine** will be very light because it will only wait for the following interrupts to occur: * A/D conversion interrupt from trimmer * TMR0 overflow interrupt The first one intercepts the operation of the trimmer and, if activated, calls the appropriate sub to do the A/D conversion. The purpose of the second interrupt routine is to decide which digit has to be turned on; when the timer overflows **//1// is left shifted in //DIGITS// register**, from bit 0 to bit 2. What happens next is, if for instance DIGITS,0 is set: * only PORTE,0 is set high, which lets pass current to the base of the transistor of the //units// digit (the rightmost one), in order to //activate// it * the value of //units// is transformed in the proper way to display them, as in the table above * PORTD is loaded with this value * only the //units// digit displays it, the others not being activated by the saturation of the corresponding transistor As soon as TMR0 overflows, //DIGITS// file register is shifted left and now DIGITS,1 is set, which sets high PORTE,1 and saturates the //tenths// digit's transistor; tenth's value is loaded in PORTD and displayed only on the corresponding digit. When TMR0 overflows once more it's the turn of the //hundreds// to be displayed; the fourth overflow re-sets DIGITS back to bit 0. {{:pic18_7_segments:digits.png|}} Now that the overall behaviour of the program should be clear, let's dig a bit deeper into **the assembly code**. ===== From bytes to units, tenths and hundreds ===== When an A/D conversion occurs, the resulting byte value is passed to the sub //Byte2Digits//; the purpose of this sub is to **split a byte into its units, tenths and hundreds**. It first obtains the hundreds: LoopHundreds subwf number,F ; number = number - 100 bnc GetTenths ; branch if not carry (that is borrow) incf hundreds,F ; get hundreds digit goto LoopHundreds In the first instruction .100 is subtracted from _number_; if there is no borrow (that is //Carry=1// - see **[[pic18_branch_instructions|branch instructions]]**), that means that //number// is higher than 100, and so //hundreds// is incremented by one and the program goes back to //LoopHundreds//. If instead there is a borrow (Carry=0) the remaining value is lower than 100, we have found the value for hundreds and we can go on calculating the tenths. Once tenths are obtained, in much the same way as in hundreds, the remaining value represents //units//. Those single values can now be displayed onto the corresponding digits. ===== Look-up tables ===== In order to display a digit, //8 units// for instance, its binary value b'00001000' loaded into PORTD doesn't make sense: b'01111111' should be loaded instead; the same goes for the other values: there should be a mechanism to establish a one-to-one correspondence between each //real// value, from '0' to '9', and its _display_ value. A way of doing this is by using **look-up tables**; the //real// value is **added to the Program Counter (PC), which jumps to a location which, in turn, returns the literal value of the //display// value**. Like this: (movf realValue,W) movwf PCL retlw b'00111111' ; value for digit '0' retlw b'00000110' ; value for digit '1' retlw b'01011011' ; value for digit '2' retlw b'01001111' ; value for digit '3' retlw b'01100110' ; value for digit '4' retlw b'01101101' ; value for digit '5' retlw b'01111101' ; value for digit '6' retlw b'00000111' ; value for digit '7' retlw b'01111111' ; value for digit '8' retlw b'01101111' ; value for digit '9' Here the //real// value is copied to W and then added to Program Counter Low to get the //display// value returned; cool, isn't it? Well, things are not so simple... ==== WREG doubled ==== As we have seen in a **[[pic18_guide_assembling_linking_programming_linux|previous article]]**, **istructions takes two bytes** (a word); for instance that's how a //listing file (.lst)// for the look-up table code could be: Address Value Disassembly Source ------- ----- ----------- ------ ... 000004 0c3f retlw 0x3f retlw b'00111111' ; value for digit '0' 000006 0c06 retlw 0x6 retlw b'00000110' ; value for digit '1' 000008 0c5b retlw 0x5b retlw b'01011011' ; value for digit '2' 00000a 0c4f retlw 0x4f retlw b'01001111' ; value for digit '3' 00000c 0c66 retlw 0x66 retlw b'01100110' ; value for digit '4' 00000e 0c6d retlw 0x6d retlw b'01101101' ; value for digit '5' 000010 0c7d retlw 0x7d retlw b'01111101' ; value for digit '6' 000012 0c07 retlw 0x7 retlw b'00000111' ; value for digit '7' 000014 0c7f retlw 0x7f retlw b'01111111' ; value for digit '8' 000016 0c6f retlw 0x6f retlw b'01101111' ; value for digit '9' ... If 8 (the real value) is added to PCL, this last one become address 0x00000c (0x000004 + 0x8) and the displayed value is '4', not '8'! We need first to **double WREG** in order to take care of the double byte words that program memory is made of; so the last operation will be 0x000004 + 0xf = 0x000014 and '8' is aptly displayed. The code: addwf WREG,W ; W+W = 2W (16bit program words); offset x2 movwf PCL retlw b'00111111' ; value for digit '0' ... That one above works...if we are lucky! There's one important piece missing: we only take care of PCL, Program Counter Low (the first 8 address bits), what about the other bits of PC? ==== PCLATH and PCLATU ==== When we have to change PC it's important that all 21 bits be modified //simultaneously//; we can modify only PCL, which is a SFR which reads and writes to the lower bits of PC, but there aren't SFRs to direcly change the other 13 bits of PC. Instead there are two registers, _PCLATH_ and _PCLATU_, whose contents are written synchronously to their corresponding PC sectors whenever PCL is written to (such as in _movwf PCL_): {{:pic18_7_segments:pcl-pclath-pclatu.png|}} In the above way all 21 bits of PC are modified at once; so it's important, **before writing to PCL, to have PCLATH and PCLATU ready with the correct values**; secondly the code should be robust enough to take into account possible //carry-outs// from the previous bytes (more on this later). 0000f6 24e8 addwf 0xe8, 0, 0 addwf WREG,W ; W+W = 2W (16bit program ; words) 0000f8 26f9 addwf 0xf9, 0x1, 0 addwf PCL,F ; program counter jumps ; to location based on the ; value of W reg and then ; returns with literal 0000fa 0c3f retlw 0x3f retlw b'00111111' ; value for digit '0' 0000fc 0c06 retlw 0x6 retlw b'00000110' ; value for digit '1' 0000fe 0c5b retlw 0x5b retlw b'01011011' ; value for digit '2' 000100 0c4f retlw 0x4f retlw b'01001111' ; value for digit '3' 000102 0c66 retlw 0x66 retlw b'01100110' ; value for digit '4' 000104 0c6d retlw 0x6d retlw b'01101101' ; value for digit '5' 000106 0c7d retlw 0x7d retlw b'01111101' ; value for digit '6' 000108 0c07 retlw 0x7 retlw b'00000111' ; value for digit '7' 00010a 0c7f retlw 0x7f retlw b'01111111' ; value for digit '8' 00010c 0c6f retlw 0x6f retlw b'01101111' ; value for digit '9' ... In the above (not working) example PCL takes care of the lower bits (from 0xF6 to 0xFE and then 0x00,0x02 and so on), but PCLATH should handle the middle bits and PCLATU the leftmost higher bits; initially PCLATH should be 0x00, then it should change to 0x01. _7SegDisplayTable andlw b'00001111' ; Mask out any erroneous upper nibble addwf WREG,W ; W+W = 2W (16bit program words); offset x2 addlw LOW Table_7 ; W = offset x2 + LOW PC [7:0] movwf TableTemp ; Store away LOW PC; PCL is ready, now take care of PCLATH and PCLATU movlw HIGH Table_7 ; W = HIGH PC [15:8] movwf PCLATH ; copy W to PCLATH clrf WREG ; W = 0 addwfc PCLATH,F ; PCLATH = 0 + any carry from previous 'addlw LOW Table_7' ; (clrf and mov* don't touch C flag) movlw UPPER Table_7 ; W = UPPER PC [20:16] movwf PCLATU ; copy W to PCLATU clrf WREG ; W = 0 addwfc PCLATU,F ; PCLATU = 0 + any carry from previous 'addwfc PCLATH,F' ; (clrf and mov* don't touch C flag) movf TableTemp,W ; Restore LOW PC (TableTemp) together with the correct values of ; PCLATH and PCLATU movwf PCL ; "The contents of PCLATH and PCLATU are transferred to the program ; counter by any operation that writes PCL." Table_7 retlw b'00111111' ; value for digit '0' retlw b'00000110' ; value for digit '1' retlw b'01011011' ; value for digit '2' retlw b'01001111' ; value for digit '3' retlw b'01100110' ; value for digit '4' retlw b'01101101' ; value for digit '5' retlw b'01111101' ; value for digit '6' retlw b'00000111' ; value for digit '7' retlw b'01111111' ; value for digit '8' retlw b'01101111' ; value for digit '9' So, here is what we call a robust code! Let's see the relevant lines in depth. ==== Low, High and Upper ==== First of all W is //masked//, in order to avoid spurious values higher than 0xFF: andlw b'00001111' Then we have WREG doubled and we have it added to the //low byte// of the address located at the label Table_7, which marks the beginning of the look-up table: addlw LOW Table_7 PCL, PCLATH and PCLATU represent different bytes of a 21-bit address; a way to split the address into those parts is by using the **operands LOW, HIGH and UPPER**. Here's a definition taken from MPASM Assembler User's Guide: {{:pic18_7_segments:low-high-upper.png|}} Let's see this thing in practice; suppose we want to display //'8'// and we got the look-up table saved in the same program locations as above, that is: 0x0000fa ... retlw b'00111111' ; value for digit '0' ... 0x00010a ... retlw b'01111111' ; value for digit '8' 0x00010c ... retlw b'01101111' ; value for digit '9' So first we have //8// doubled, that is //0x10//; we add this value to //0x0000fa// and we save the result into //TableTemp//: 0x fa + 0x 10 = --------- 0x(1)0a --> PCL |------> Carry But since //TableTemp//, as all data locations, is an 8-byte value, **only //0x0a// is saved into it and a Carry is originated**; the first value will be our PCL. Let's deal with PCLATH now and so let's consider the [15-8] bytes of the address: movlw HIGH Table_7 movwf PCLATH Hey, but wait! We **got a Carry** from our previous sum (//addlw LOW Table_7//), so let's add it to PCLATH; as written in the comments CLRF and MOV* operations in between don't touch C flag. The instruction _addwfc_ is just what we need for this purpose (PCLATH + Carry); we just have to clear WREG, which in this case is not really needed: clrf WREG addwfc PCLATH,F Here's the sum of PCLATH and Carry: 0x00 + 0x01 = ------- 0x01 --> PCLATH The same process is used to get PCLATU, but in this case it's just 0x0; **as soon as PCL is written to (//movwf PCL//) at the same time PCLATH and PCLATU are transferred to their corrisponing positions in the 21-bit address** (see image above), that is //0x00010a// which is indeed the value for digit '8'! And that pretty much completes the description of the code, which can be **downloaded in the .zip file below**. Here's we can watch the final result: {{youtube>U7h-i9cG6cA}} ===== Reference ===== "The Essential PIC18 Microcontroller" by Sid Katzen